
简介fer2013人脸表情识别数据集配套的Python提取代码与图片包面向机器学习、深度学习中图像分类或表情识别的初学者与研究者。资源共约3.5万个文件包括35887张由CSV解析出的jpg表情图、带7类情感标签的fer2013.csv原始文件、提取脚本py、readme说明及bib引用信息压缩包约130MB。7类标签分别为高兴、中性、惊讶、悲伤、生气、害怕和厌恶每张图片均有对应标注适合直接用于卷积神经网络分类任务。Python代码覆盖CSV读取、标签编码、图像尺寸调整、训练集/验证集/测试集划分等关键预处理步骤可配合Keras或TensorFlow快速构建并训练CNN模型。已有8191人浏览学习对希望省去数据解析与整理时间、直接进入模型训练和实验对比的读者很实用。 做表情识别项目的人几乎绕不开FER2013数据集但也几乎都会在第一步卡一下这个数据集里的图片需要自己写Python代码提取。FER2013是Kaggle在2013年举办的面部表情识别挑战赛推出的公开数据集共包含35887张48x48像素的灰度人脸图片覆盖生气、厌恶、恐惧、开心、悲伤、惊讶、中性7类表情。它最大的特点是所有图片不是以常见图片文件形式存放而是被打平成像素字符串存进了一个CSV文件你没法直接双击看也没法直接扔进框架的自动加载接口里面。这篇博文就把我从下载数据到最终生成图片文件夹的完整流程摊开讲包括CSV结构分析、图片提取代码、目录规划、标签分布核对以及我在实际处理过程中踩过的坑和排查思路。做完这一步后面接PyTorch或TensorFlow做表情识别训练或者拿这批图片做数据增强、可视化分析都会顺手非常多。适合正在做人脸表情识别、刚入门深度学习、或者被CSV格式图片数据卡住的朋友直接参考。1. 项目背景与整体设计思路1.1 为什么需要图片提取这一步直接读CSV把像素数组喂给模型技术上确实可行但提取成图片文件有三个实打实的优势。第一是可视化方便。CSV里存的是一长串数字单独看根本不知道是什么。提取成png之后可以直接打开看也能拼成网格图观察整体分布做数据清洗和异常检测的时候特别好用。第二是方便对接现有工具链。多数深度学习框架和迁移学习模型都提供了按目录读取图片的入口比如PyTorch的ImageFolder、TensorFlow的image_dataset_from_directory都要求数据以图片文件形式按目录存放。把图片提取出来后直接就能用这些高层接口省掉一大截自定义Dataset的代码。第三是方便做独立处理。提取成图片文件后你可以在训练之外独立做清洗、去重、裁剪、旋转、加噪声等操作处理完再统一喂给模型。尤其是做数据增强实验时有些预处理需要反复调整参数有实体图片后就不用每次都在CSV里捞数据。不过提取图片并不是唯一解。如果你只想做训练完全可以写一个自定义Dataset从CSV里按索引实时解析像素跳过多余的磁盘读写。但说实话对于绝大多数刚接触FER2013的人尤其是做可视化探索的阶段我建议先把图片提出来后面会舒服很多。1.2 整体流程拆解我的整体设计分成四个步骤下载并确认CSV文件结构搞清楚emotion、pixels、Usage三个字段分别是什么。解析pixels列把空格分隔的像素字符串还原成48x48的像素矩阵。按表情标签和数据集用途分类存放生成规整的目录结构。统计核对图片数量确保每个类别的数量与原始CSV一致。我当时特意没有把“提取”和“后续训练”混在一个脚本里这样职责更清晰排查问题也更方便。提取脚本只做一件事从CSV生成标准目录结构的图片文件。后面不管是训练还是做别的处理都从这批图片重新出发。2. FER2013数据集的内部结构拆解2.1 CSV文件的结构与字段含义打开fer2013.csv第一行是表头后面每一行代表一张图片。去掉表头后共有35887行数据对应35887张图片。每行有3个字段字段含义示例emotion表情标签0-6的整数0pixels48x482304个像素值用空格分隔的字符串98 105 110 ...Usage数据用途Training/PublicTest/PrivateTestTrainingemotion标签的映射关系为0Angry生气、1Disgust厌恶、2Fear恐惧、3Happy开心、4Sad悲伤、5Surprise惊讶、6Neutral中性。这个映射在提取图片、统计分布、做分类训练时都会反复用到建议提前记住或者写进常量里。pixels字段是整个提取过程的核心。它存储的是单张灰度图的像素值取值在0到255之间48x48的图片展开后正好是2304个数字。只要把这个字符串按空格切分转成整数数组再reshape成(48, 48)的二维矩阵就能还原成一张完整的人脸灰度图。2.2 标签分布与数据划分用pandas快速统计一下各标签数量会发现数据分布其实很不均衡。我手里的这份FER2013统计结果大致如下标签情绪样本数0Angry49531Disgust5472Fear51213Happy89894Sad60775Surprise40026Neutral6198Disgust厌恶类别明显是少数样本只有547张不到Happy的十分之一。这说明如果你直接用这个数据集做训练模型对Disgust的识别效果大概率会差这是数据集本身分布决定的不是模型或代码的问题。遇到这种情况常见的应对思路是过采样少数类、做数据增强或者改用带类别权重的损失函数。数据用途方面官方把数据划分成了三份Training共28709张PublicTest共3589张PrivateTest共3589张。PublicTest和PrivateTest数量一样区别在于Kaggle当年的比赛规则中PublicTest用于公开排行榜展示PrivateTest作为最终判定结果两者不重叠。提取图片时我建议把这三个子集分开保存方便后续分别评估。2.3 图片目录结构怎么规划我建议用“数据集用途 标签名”两级目录来组织图片这也是PyTorch的ImageFolder默认约定的结构fer2013_images/ ├── Training/ │ ├── Angry/ │ ├── Disgust/ │ ├── Fear/ │ ├── Happy/ │ ├── Sad/ │ ├── Surprise/ │ └── Neutral/ ├── PublicTest/ │ └── ... └── PrivateTest/ └── ...这样规划的好处是后面用框架加载数据时一行代码就能完成不需要手写复杂的路径映射同时每个类别文件夹直接数文件个数就能核对提取结果是否和原CSV分布一致。如果你不需要区分Usage只想把所有图片按标签放一起去掉中间那层目录即可。3. 提取图片的Python代码实现3.1 环境准备与依赖安装先列一下我用到的依赖Python 3.8pandasnumpyopencv-python或者Pillowtqdm可选用于显示进度安装命令很简单pip install pandas numpy opencv-python tqdm如果你机器上已经有了直接跳过。核心其实只需要pandas加numpy保存图片用cv2.imwrite或者PIL的Image.save都可以。我习惯用OpenCV因为后面做人脸对齐、图像预处理时大概率也会用到它提前装好省事。这里说一个细节保存图片时建议用png格式。原始像素值是0-255的灰度值png是无损压缩保存后不会丢信息。用jpg在低质量参数下会引入压缩伪影虽然不一定影响训练但没必要自找麻烦。3.2 完整提取脚本直接给出完整代码并逐段说明。import os import pandas as pd import numpy as np import cv2 from tqdm import tqdm def load_fer2013(csv_path): df pd.read_csv(csv_path) print(f[INFO] 总样本数: {len(df)}) print(f[INFO] Usage分布:\n{df[Usage].value_counts()}) return df def parse_pixels(pixels_str): return np.array(pixels_str.split(), dtypenp.uint8).reshape(48, 48) def save_images_from_csv(csv_path, output_rootfer2013_images): df load_fer2013(csv_path) emotion_map { 0: Angry, 1: Disgust, 2: Fear, 3: Happy, 4: Sad, 5: Surprise, 6: Neutral } for idx, row in tqdm(df.iterrows(), totallen(df), descExtracting images): emotion int(row[emotion]) usage str(row[Usage]) label_name emotion_map[emotion] dest_dir os.path.join(output_root, usage, label_name) os.makedirs(dest_dir, exist_okTrue) img parse_pixels(row[pixels]) filename f{idx:06d}.png save_path os.path.join(dest_dir, filename) cv2.imwrite(save_path, img) print([INFO] 提取完成) if __name__ __main__: save_images_from_csv(fer2013.csv)运行后会在当前目录生成fer2013_images文件夹里面按Training / PublicTest / PrivateTest以及具体表情标签分好了类。如果你不需要细分Usage就去掉usage那一层目录直接按标签存放。3.3 代码里几个关键细节parse_pixels这步是核心。pixels列是类似“98 105 110 ...”的空格分隔字符串split()之后得到2304个字符串np.array(..., dtypenp.uint8)之后是2304维向量再reshape成48x48的二维灰度图。这里有一个容易踩的坑如果用np.int32或np.float64读取再保存后续处理还要多一步类型转换。直接用uint8既能节省内存又能保证和原数据一致而且cv2.imwrite默认就接受uint8的单通道图像不需要额外处理。另一个细节是文件名用idx的6位编号比如000000.png、000001.png。沿用CSV的行号做文件名好处是后面如果发现某张图有异常可以快速定位到原始CSV的对应行排查起来非常方便。如果文件名乱起追溯起来就是一场灾难。3.4 提取后校验图片数量要对上提取完不要急着走先核对数量确认没漏图、没多图。可以用下面的脚本统计stats df.groupby([Usage, emotion]).size().reset_index(namecount) emotion_map { 0: Angry, 1: Disgust, 2: Fear, 3: Happy, 4: Sad, 5: Surprise, 6: Neutral } for _, row in stats.iterrows(): folder os.path.join(fer2013_images, row[Usage], emotion_map[row[emotion]]) actual len(os.listdir(folder)) expected row[count] status OK if actual expected else MISMATCH print(f{row[Usage]} - {emotion_map[row[emotion]]}: {actual}/{expected} {status})输出结果应当每个都是OK。我之前跑完遇到过MISMATCH的情况原因基本是提取一半中断导致重新生成时部分文件被覆盖或者命名规则冲突。用带前导零的行号命名可以很好规避这类问题。3.5 不用OpenCVPIL版本怎么写有些精简环境里没装OpenCV用Pillow也能完成同样的事。代码差异不大from PIL import Image def save_image_pil(img_array, path): img Image.fromarray(img_array, modeL) img.save(path)注意一定要指定modeL表示单通道灰度图。如果不指定Pillow对二维数组的模式推断偶尔会出问题可能生成索引颜色模式或通道混乱的图像。这个是我实际踩过的小坑单独拎出来说。4. 常见问题与排查技巧实录4.1 像素点数和48x48对不上怎么办有时候split之后的长度不是2304多半是数据源格式有变化。可以先打印出来看看pixels_str row[pixels] length len(pixels_str.split()) if length ! 2304: print(f异常行 {idx}: 像素长度 {length})如果长度不等于2304要么是分隔符不是空格而是逗号或分号要么是文件被二次处理过。FER2013官方版本一般不会出现这个问题但你从网上下载的第三方版本就不好说了。4.2 图片保存出来是全黑或严重失真这种情况我遇到两种原因。一种是数组没 reshape把2304的向量当成单行图像保存出来的图当然不是人脸。另一种是像素值已经被缩放到0-1的浮点数直接按uint8保存后大部分像素变成了0图片几乎全黑。第二个问题需要在保存前先乘255再转uint8。拿到数据后第一时间确认像素范围是0-255还是0-1可以省不少排查时间。4.3 图片看起来有波纹或变形多半是reshape顺序搞错了。FER2013官方说明是按行展开的所以直接用默认的C-order行优先reshape即np.array(...).reshape(48, 48)顺序不会错。如果图像左右上下错位说明原始数据可能是按列展开的这时候需要先reshape成(48, 48)再转置或者调整split的读取顺序。判断方法很简单打印前几个像素值对照一张已知人脸图的左上角基本就能看出来。4.4 遍历3万多行慢不慢df.iterrows()虽然方便但性能不算好。如果觉得提取太慢可以把pixels列一次性解析成大数组再循环保存pixels_matrix [] for seq in df[pixels]: pixels_matrix.append(np.array(seq.split(), dtypenp.uint8)) pixels_matrix np.vstack(pixels_matrix).reshape(-1, 48, 48)实测下来解析部分耗时占比很低大部分时间在文件I/O上。所以日常提取3万多张图迭代方式也就一两分钟不需要过度优化。4.5 CSV文件读取时编码报错个别情况下用pandas读取会出现UnicodeDecodeError大多因为文件编码不是utf-8而是latin-1或cp1252。解决方案df pd.read_csv(csv_path, encodinglatin-1)现在网上流传的FER2013版本不少有的加了额外字段有的把Usage字段去掉或改名。遇到这类变体版本先用df.columns确认列名再调整代码不要拿着旧逻辑硬套。5. 最后再说两句个人经验提取FER2013图片这件事技术上不算复杂但它是整个表情识别项目里绕不开的起点。我一开始也觉得直接读CSV训练就行没必要提取后来真正开始做迁移学习时才发现把图片提取出来的价值不止是省几行代码更重要的是它能让你用上框架自带的数据加载接口、能肉眼审查数据质量、能自由组合增强策略甚至能配合OpenCV做人脸对齐等额外处理。最后一个小建议提取脚本和后续训练脚本分开写目录命名加上版本号。这样当你换了一版数据或者改了一种存放方式时不会把之前的成果搞乱。我自己在这个数据集上反复折腾的过程中体会到越早把数据整理规整后面做起实验来越省心。如果你打算继续往下走可以考虑几个扩展方向一是构建一个直接从CSV读取的自定义Dataloader跳过图片文件适合做分布式训练时减少IO二是针对Disgust这类少数类别做平衡采样改善模型在类别不均衡下的表现三是把FER2013和后续的FER2013等版本做对比实验看看噪声标签对模型的影响。这些方向都很有意思之后有机会我再单独整理分享。本文还有配套的精品资源点击获取