☰
fer2013数据集CSV提取为图片:完整Python实现
2026/10/12 12:23:45 网站建设 项目流程

简介:fer2013面部表情识别数据集及提取后的图片、Python提取代码,面向机器学习与计算机视觉初学者及需要快速搭建表情识别项目的开发者。资源解决从CSV原始标注到可用图像样本再到模型训练的全流程问题,适合课程设计、论文复现或算法练手。压缩包共2000个文件,大小约130.43MB,主体为JPG图片,另含CSV标注文件、Python脚本、README说明与Bib引用条目;CSV保存原始标签,JPG为提取出的表情图,脚本覆盖数据预处理、训练/验证/测试集划分、CNN模型构建与训练、评估及可视化等步骤。目前已有8191人学习或下载。除可直接使用的数据集外,还提供完整代码框架,可在此基础上尝试数据增强、集成学习或迁移学习,便于深入理解面部表情识别项目从数据到模型的实践细节。

1. 下载完fer2013发现是一堆CSV?这个数据集为什么非提取不可

1.1 fer2013的来头和它藏在一行行CSV里的真相

研究人脸表情识别的小伙们,八成在下载fer2013数据集的时候都愣了一下:说好的图片呢,怎么解压出来只有一个csv文件?我当初第一次拿到这个数据集也是同样的反应,全网搜"fer2013提取代码"才弄明白,这个由ICML 2013 Challenges in Representation Learning组织、后来在Kaggle上开放的人脸表情识别数据集,原始形态就是把35887张灰度人脸图硬塞进了一张CSV表格里。每张图是48x48的8位灰度图,也就是2304个像素点,再加一个表情标签和一条用途标记,三列构成一行。

这里面的表情一共7类:0代表Angry,1代表Disgust,2代表Fear,3代表Happy,4代表Sad,5代表Surprise,6代表Neutral。数据量看着不小,但分布并不均衡,Happy占大头,Disgust只有几百张。这个现象在提取阶段不显眼,真正跑到训练分类模型的时候,类别不平衡带来的头疼会让你记起今天看到的这句话。

1.2 不提取成图片会怎样,提取后又能做什么

CSV作为一种比赛分发格式,好处是文件单一、逻辑统一、几行代码就能整表加载,可一旦进入实际训练流程,它的短板就很明显了。想在文件夹里直接预览某张样本?做不到。想把某个类别单独拎出来做数据增强?还得先解码。最让人难受的是,每次调试数据加载代码,都要把整份CSV重新解析一遍,两百多兆的文本文件反复扫描,耗时又费内存。把fer2013提取成图片,不是为了形式好看,而是让数据变得可检查、可复用、能对接深度学习框架的标准读取方式。

提取成标准图片之后,变化是直接的:你可以打开某个表情文件夹看样本,会发现不少人脸角度偏、光线暗、甚至存在标注错误,对数据质量心里立刻有数;可以扔给torchvision的ImageFolder或者Keras的ImageDataGenerator去做增强;也可以把自己收集的人脸数据补充进来,扩展数据集。至于怎么提取最省事,接下来这套Python代码,就是我从头到尾跑通流程后的完整方案。

其实严格来说,训练时在线解析CSV也完全可行,我最早就是那么干的。但后来发现,一旦进入数据清洗和错误样本筛选阶段,没有实体图片文件,每次筛选都要重跑一遍解析逻辑,笨重得不行。把提取脚本做成一个一次性预处理工具,从训练代码里拆出去,整个流程维护起来要舒服得多。所以我一直建议先落地图片、再谈训练,这也是这篇东西存在的真正理由。

还有人问过,图片格式选png还是jpg。我的建议是png,因为它是无损格式,灰度图存成png能保证像素值不被二次压缩破坏;jpg虽然体积小,但压缩损失在表情识别这种像素级任务里并不划算,同样的道理也适用于大多数图像分类场景。

2. 动手前先看清这三个字段:emotion、pixels和Usage

2.1 一行数据到底包含了什么

打开fer2013.csv,用文本编辑器或者Excel预览,第一行列名就是emotion、pixels、Usage。emotion是整数标签,范围0到6,对应前面说的7类表情;pixels看起来像一大串数字,这一串里每个数字都代表一个像素点的灰度值,范围0到255,中间用空格分隔,一共2304个数字,按48x48自上到下、自左到右排列;Usage则告诉你了这张图的归属,Training代表训练集,PublicTest代表公开测试集,PrivateTest代表私有测试集。

字段名含义示例值
emotion表情标签,0到6整数3(Happy)
pixels2304个灰度值,空格分隔的字符串70 80 82 60 ...
Usage数据子集划分Training / PublicTest / PrivateTest

这里要特别提醒:pixels字段是字符串,不是CSV自动帮你解析好的数组。写提取代码时最核心的一步,就是把这个字符串拆开、转成整数、再reshape成48x48的矩阵。很多人第一次写代码报错,问题多半出在这一行:直接把整个字符串拿去int(),或者split用的分隔符不对。它是空格分隔,所以要写split(' '),不是split(','),一字之差结果就完全对不上。

2.2 训练集、公开测试集和私有测试集是怎么划分的

Usage这列的三个取值,决定了数据集的官方划分。训练集大约有28709张,PublicTest大约3589张,PrivateTest大约3589张,加起来正好35887张。训练集覆盖全部7类表情,但Disgust这类样本少的类别数量惨淡。我提取时建议严格按Usage字段分目录,不要在提取阶段自作主张重划分,原因很简单:大量论文和公开评测都基于这套划分,如果你换成自己的随机比例,以后跟论文指标做对比时,会发现实验可信度直接打折。

PublicTest和PrivateTest虽然都叫测试集,用途其实有区别。PublicTest经常被拿去当验证集做早停和超参数选择,PrivateTest则负责最终评分。提取代码里如果二者混到一起,整个评测流程就乱了。很多人在复现论文时发现自己的结果总比论文低几个点,一部分原因就出在数据划分上,别在这个基础环节翻车。

2.3 环境准备:Python、Pandas、Pillow,缺一不可

代码实现层面,我选择Python搭配pandas和Pillow,这两个库足够轻量。如果你环境还没配好,建议先把Python装好,无论走python.org官方安装包还是Anaconda都行,装完在终端里执行:

pip install pandas pillow tqdm

tqdm不是必需,但35887行数据跑起来时没有进度条会心里没底,我建议装。别在这个阶段纠结cv2,后面真需要图像增强再补也不迟。

有些人习惯用opencv-python来写图片,这会把一个比较大的依赖引进来,而且OpenCV默认通道顺序是BGR,虽然灰度图不受影响,但很多人后续会做彩色图预处理,混来混去容易踩通道的坑。用Pillow有两个实打实的好处:一是只处理图像读写,依赖小;二是Image.fromarray可以直接指定mode='L'创建灰度图像,通道语义清晰。这个取舍会在第3章代码里体现。

3. 完整提取代码:从CSV像素串到一张张实打实的图片

3.1 核心代码直接抄

下面这段代码就是完整的提取脚本,我按"先说结论、后讲原理"的顺序来,方便你直接拿去用。

import os import pandas as pd import numpy as np from PIL import Image from tqdm import tqdm # 1. 读取CSV csv_path = 'fer2013.csv' df = pd.read_csv(csv_path) # 2. 创建目录结构:按Usage/表情标签两层划分 output_root = 'fer2013_images' usages = ['Training', 'PublicTest', 'PrivateTest'] emotion_names = ['Angry', 'Disgust', 'Fear', 'Happy', 'Sad', 'Surprise', 'Neutral'] for usage in usages: for emotion in range(7): os.makedirs(os.path.join(output_root, usage, str(emotion)), exist_ok=True) # 3. 逐行解析并保存图片 for idx, row in tqdm(df.iterrows(), total=len(df)): emotion = int(row['emotion']) usage = row['Usage'] pixels_str = row['pixels'] # 字符串 -> numpy数组 -> 48x48矩阵 pixels = np.array(pixels_str.split(' '), dtype=np.uint8).reshape(48, 48) # 构造灰度图并保存 img = Image.fromarray(pixels, mode='L') save_path = os.path.join( output_root, usage, str(emotion), f'{idx}_{emotion_names[emotion]}.png' ) img.save(save_path) print('提取完成')

运行之后,会在当前目录生成一个fer2013_images文件夹,里面有Training、PublicTest、PrivateTest三个子目录,再往下是按0到6编号的表情文件夹,每张图以“行号_表情名.png”命名。整个流程跑完大概需要一两分钟,看到进度条走满就说明提取成功。

3.2 每一段代码为什么这么写

先看读取部分。pd.read_csv用默认参数就能读,不需要设置index_col,因为原始fer2013.csv没有额外索引列。但如果你下载的版本经过二次处理,CSV左侧多了一列行号,就需要改成df = pd.read_csv(csv_path, index_col=0)把它吃掉。后面我按原始文件没有多余列来写,但拿到文件先print(df.columns)确认一下,是成本最低的防呆手段。

再看解析部分。np.array(pixels_str.split(' '), dtype=np.uint8)是全段代码的关键。split(' ')把2304个数字拆成Python列表,np.array转成数组并指定dtype=np.uint8,内存占用比默认int64直接降到八分之一,灰度像素值恰好落在0到255,uint8完全够用。之后reshape(48,48)把一维数组还原成二维图像矩阵。这一步千万别漏,否则后面Image.fromarray会报维度错误。

保存部分,Image.fromarray要求传入NumPy数组,mode='L'明确告诉Pillow这是灰度模式。这里不需要再调用convert('L'),fromarray配合uint8数组已经精确表达了灰度值,多转一次反而多余。文件名里带上行号idx,是为了将来能追溯某张图来自CSV的哪一行,这个细节在数据清洗和异常样本排查时非常有用。比如训练时发现某张图标注明显错误,直接看文件名回头定位CSV行,几分钟就能搞定。

3.3 另一种写法:用OpenCV也不是不行,但我不推荐

网上很多代码用cv2.imwrite来保存,我理解这种选择,毕竟很多人同时装了OpenCV准备做数据增强。但如果只想完成提取这个任务,我不推荐把OpenCV作为首选方案。第一,cv2依赖重,单为保存灰度图装一整套opencv-python,没必要;第二,OpenCV的API对通道模式比较隐式,灰度数组一般不会出问题,可后面一旦换成三通道图片,BGR和RGB的坑就会冒出来,排查起来相当费时间。

Pillow这边逻辑就很直白:输入二维数组,输出灰度图,语义明确。后续读图用Image.open(...).convert('L')也同样简单。而且Pillow保存png默认不做有损压缩,速度也够快。实测下来,用上面的代码跑完整份fer2013没有哪个步骤会慢到离谱,主要耗时还是在pandas迭代和文件IO上,完全在可接受范围。

4. 分类命名与结果验证:图片提取完了,怎么确定没提出错

4.1 按Usage和表情类别划分目录的设计逻辑

目录设计我故意用了两层结构:最外层是Usage,内层是表情标签数字。这个结构有一个肉眼可见的好处:后续用torchvision.datasets.ImageFolder读数据时,它天然把子目录名当成标签。你只要写root='fer2013_images/Training',配上transform就能直接加载。如果只需要做二分类,比如只识别Happy和Sad,提取完之后对目录做过滤复制就行,不影响原始划分。

可能有人问,为什么不用英文名称做文件夹名,而是用0到6的数字。因为模型训练大多用数字标签,ImageFolder默认按目录名字典序映射,用数字目录名可以让分类顺序与CSV里的标签定义完全一致,省去做额外映射表的功夫。emotion_names列表只用于文件名后缀,这样你在文件管理器里瞥一眼也能知道大概类别,兼顾了可读性和自动化。

4.2 验证提取结果:数量、尺寸、灰度模式

代码跑完别急着开心,先做三件事验证,否则文件是否齐全、尺寸是否正确都心里没底。

第一,数量核对。用下面代码统计图片总数,对比官方划分:Training约28709张,PublicTest和PrivateTest各约3589张,总和35887张。

count = 0 for root, dirs, files in os.walk(output_root): count += len(files) print(count)

第二,尺寸核对。随机挑几张图,用Pillow打开,看size是不是(48,48),mode是不是'L'。如果size不是48x48,说明reshape环节出了问题;如果mode不是L,说明保存时可能被转成了RGB,这两种情况都要回头检查代码。

第三,可视化抽查。把每个类别的若干张图拼成一张大图,肉眼扫一遍。Happy类是不是像笑脸,Disgust类是不是又少又杂。这一步非常值得做,因为fer2013本身存在标注噪音,提前看看能帮你判断这个数据集在你任务上的上限在哪里。我抽查时就发现,部分Sad样本表情并不明显,Angry和Disgust之间也有不少混淆,这些都会直接影响模型的上限。

4.3 提取之后的下一步:ImageFolder和训练流程

图片提取完,最直接的使用方式是接入预训练流程。torchvision里可以这样写:

from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Grayscale(num_output_channels=3), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) train_dataset = datasets.ImageFolder('fer2013_images/Training', transform=transform)

这里要留个心:很多预训练模型基于ImageNet权重,要求输入是3通道224x224,而fer2013原始图片是48x48灰度单通道。直接用单通道48x48去喂ResNet,会报维度不匹配。通常做法是先用Grayscale转成三通道,再Resize到224;或者干脆用为单通道设计的轻量模型。这步虽然发生在提取之后,但属于紧接着就会遇到的坑,提前搞清楚能省不少调试时间。

5. 我实际跑完整流程踩过的坑,以及几个实用建议

5.1 pandas读取时多出一列Unnamed的坑

我的原始CSV是从某个镜像站下载的,下载完一读发现df.columns里多了一个"Unnamed: 0"。当时没留意,后面用row['emotion']报KeyError,整个人都懵了。排查了半天,才发现是镜像站在生成CSV时给最左边加了一列行号。那个版本的读法需要改成pd.read_csv(csv_path, index_col=0)才能读对。所以写代码之前,先用df.head()或者print(df.columns)看一下结构,比回头排查报错省时间得多。很多网上流传的二次封装版本都有类似问题,拿到任何CSV先验证前几行,是基本习惯。

另外一个和索引相关的细节:文件名里的行号默认对应Pandas读取后的RangeIndex,如果你用了index_col=0,行号就不对应原始CSV的行了。真遇到这种版本,我建议读进来后reset_index(drop=True)重建索引,让行号与原始顺序对齐,这样后续定位问题才准确。

5.2 别为了省事把整列pixels直接转成数组,内存会告警

我见过有人直接写df['pixels'].apply(lambda x: np.array(x.split(' '), dtype=np.uint8).reshape(48, 48)),最后得到一列二维数组。代码看着简洁,但35887个数组对象全压在内存里,机器配置差一点就会明显卡顿甚至内存溢出。我后来改成逐行提取,每行构造完图片立刻保存,用完即丢,内存占用稳定在低水平。如果你真追求极致速度,也可以一次性预分配一个(35887,48,48)的uint8数组批量填充,但这就意味着要同时把三万五千多张图全部load进内存,对普通电脑并不友好。逐行保存是普适性最好的解法,数据处理这种事,稳定和可复现通常比炫技重要。

5.3 类别不平衡在提取阶段就要心里有数

提取时顺手统计了一下各类别数量,不均衡程度非常明显。下面是我手中这份完整数据集的分布情况,供参考:

emotion表情名全数据集数量(张)
0Angry4593
1Disgust547
2Fear5121
3Happy8989
4Sad6077
5Surprise4002
6Neutral6198

如果你手里的CSV来自二次处理版本,数字可能略有波动,以实际统计为准。类别不平衡在提取阶段无法靠代码解决,但它直接决定后续策略:直接拿交叉熵训练,Disgust几乎会被压死,测试时这个类别的F1会特别难看。我在提取完图片后做了一件事,就是单独统计每个类别在训练集里的张数,为后面用加权采样或过采样做好准备。这种信息越早掌握越好,等模型训练完才发现少数类全部分错,再回头补数据,浪费的时间就太多了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询