简介:CK+人脸表情数据集是计算机视觉领域经典的人脸表情识别资源,源自美国宾夕法尼亚州立大学Cohn-Kanade数据集扩展,面向表情识别算法研究、深度学习训练和心理学实验等场景,涵盖中性、快乐、悲伤、惊讶、愤怒、厌恶、恐惧七类基本表情的动态图像序列。压缩包共928个文件、约139.77MB,主体为920张PNG格式人脸表情帧,并包含rearrangeCK.c等预处理脚本、CVPR2010_CK.pdf文献、Consent for Publication.doc许可文档,兼顾数据使用与学术引用。目前已有2523人学习下载,适合初学者快速上手,也便于科研人员复现实验。包内图像多已灰度化、归一化并附带标注信息,可直接用于模型训练,同时借助脚本和说明文档可梳理数据目录、理解表情连续变化过程,为人脸表情识别项目提供扎实数据基础。 如果你刚开始做人脸表情识别,去搜索“人脸表情数据集”,最先跳出来的几乎肯定是CK+。我第一次跑表情分类实验也是从CK+下手的,这个数据集自带时序帧、表情标签和FACS动作单元标注,到现在依然是表情识别论文里出现频率最高的benchmark之一。这篇内容我从实际使用角度出发,讲清楚CK+到底怎么用、预处理时有哪些坑、以及怎么搭一个靠谱的训练流程。
1. 为什么表情识别入门绕不开CK+
1.1 先从CK+本身说起
CK+全称是Cohn-Kanade Extended Dataset,是在早期Cohn-Kanade数据集基础上扩展出来的版本。原始CK数据集收集了97个人的486段人脸视频序列,CK+扩展后变成123个人的593段序列,每段序列都从neutral表情开始,逐步过渡到峰值表情。它发布于2010年前后,距今十几年,却依然是很多表情识别论文必跑的数据集之一。
它的核心特点有三个:第一,它是一个带时序的数据集,每个样本是一段连续帧,不是单张图片;第二,它同时提供了离散表情标签和FACS(Facial Action Coding System)动作单元标注;第三,图像是在实验室受控条件下拍摄的,光照、角度、遮挡都相对规整。这意味着你拿到的是一个“干净”的研究用数据集,非常适合验证算法思路、跑baseline、对比消融实验。
我自己的体会是,CK+最适合两类人:一类是刚入门表情识别、想快速跑通完整pipeline的初学者,另一类是论文里需要做公开数据集对比验证的研究者。它的样本量对于训练工业级模型来说不大,但作为算法验证和学术对比,价值非常高。
1.2 和其他数据集放在一起看,差距在哪里
很多新手会问:既然CK+数据量这么小,为什么不用更大更新的数据集?这里需要区分一下数据集的定位。CK+走的是“小而精”路线,强调标注质量和多维标注信息;而FER2013、AffectNet这些走的是“大而广”路线,强调覆盖真实场景的多样性。
| 数据集 | 规模 | 环境 | 标注类型 | 是否含时序 | 适合场景 |
|---|---|---|---|---|---|
| CK+ | 593段序列、123人 | 实验室受控 | 离散表情 + FACS AU | 是 | 算法验证、AU分析、消融实验 |
| FER2013 | 约3.5万张 | 互联网野生 | 7类离散表情 | 否 | 深度学习入门baseline |
| AffectNet | 约45万张 | 互联网野生 | 8类离散 + 效价/唤醒 | 否 | 大模型预训练、真实场景 |
| RAF-DB | 约3万张 | 互联网野生 | 7类基本表情 + 复合表情 | 否 | 更细粒度的表情分类 |
从这个表能看出来,CK+的不可替代性不在于数量,而在于它同时具备时序、行为单元标注和表情标签。如果你做基于时序的模型、或者想分析表情的肌肉运动规律,CK+几乎是唯一的选择。FER2013这类静态数据只有一张最终表情图,你没法研究“表情是怎么从neutral演变到peak的”,这是本质区别。
提示:正因为CK+是实验室受控数据,模型在上面跑出高准确率并不代表真实场景泛化能力强。我在实际项目中通常把CK+当作“算法正确性验证集”,真正评估性能还需要结合其他真实场景数据。
2. 拿到原始包后,目录和标注怎么读
2.1 从官方渠道下载后的目录长什么样
CK+需要向官方申请下载,申请通过后会得到一个压缩包,解压后的目录结构大致如下:
CK+/ ├── cohn-kanade-images/ │ ├── S001/ │ │ ├── 001/ │ │ │ ├── 00000001.png │ │ │ ├── 00000002.png │ │ │ └── ... │ │ ├── 002/ │ │ └── ... │ ├── S002/ │ └── ... ├── Emotion/ ├── FACS/ └── Landmarks/先说命名规则。Sxxx是被试编号,S001就是第一个被试者;被试编号下面还有一级目录,比如001、002,表示这个人的第几段视频序列。cohn-kanade-images里就是真实的图像帧,每段序列一个文件夹,帧文件名是纯数字,按时间顺序递增。
Emotion和FACS目录下的文件命名和图像目录是对应的,但只包含有标注的文件。比如Emotion/S001/001.txt就代表S001这个人的第001号序列,txt内容是一个数字标签。Landmarks目录保存的是每一帧的68点人脸关键点坐标,文件格式通常是每两行对应一个点的x、y坐标。
这里尤其要注意:Emotion目录下的标签文件不是每个序列都有。官方只给一部分有明确目标表情的序列提供了语义标签,所以我每次拿到数据第一步就是统计一下有多少序列带表情标签。网上有说大约327个序列带标签,具体数字你下载后数一下Emotion目录下的txt文件数就知道,以你自己拿到的那份为准。
2.2 FACS和AU:CK+的隐藏价值
FACS是一套心理学领域发展出来的面部动作编码系统,它把人的面部表情拆解成一个个独立的动作单元(Action Unit,简称AU)。简单理解,AU就是“面部肌肉运动的最小可标注单位”。比如:
- AU1:眉毛内侧上抬
- AU4:皱眉,眉毛下压
- AU12:嘴角上扬,也就是微笑的核心动作
- AU25:嘴唇微张
- AU26:下颌放松张开
CK+对序列提供了FACS编码标注,一个序列的峰值帧通常会被标注多个AU编号,比如“5+25”表示AU5和AU25同时发生。这个标注的价值非常大,因为离散表情标签是粗粒度的,而AU能揭示表情到底是靠哪些肌肉运动组合出来的。你可以基于AU做表情识别的中间监督,也可以做“表情+AU”的多任务学习,让模型学到的特征更具可解释性。
在官方目录的FACS文件夹里,每个有标注的序列都有一个txt文件,内容是类似“5+20+25”这样的AU组合。如果要把它用在模型训练里,常见做法是把AU组合转成多标签向量,设定一个AU编号列表,出现在组合里就标1,否则标0。
注意:FACS目录的覆盖范围比Emotion目录更广,很多没有语义表情标签的序列也有AU标注。如果你想尽可能多利用CK+的数据量,可以用AU标注做多任务学习;如果只想做表情分类,就以Emotion目录为准。
2.3 把标签文件映射成分类任务时要注意什么
Emotion目录下的txt标签文件是一个数字,对应关系如下:
| 数字 | 表情 | 实际使用中的建议 |
|---|---|---|
| 0 | neutral | 一般不做单独类别,或作为第8类 |
| 1 | anger | 直接参与分类 |
| 2 | contempt | 样本最少,多数人直接剔除 |
| 3 | disgust | 直接参与分类 |
| 4 | fear | 直接参与分类 |
| 5 | happy | 直接参与分类 |
| 6 | sadness | 直接参与分类 |
| 7 | surprise | 直接参与分类 |
这里最需要注意的就是contempt这个类别。CK+在原始版本上增加了contempt表情,但它的样本量比另外7类少很多,如果直接做8分类,模型容易在contempt上表现很差。我个人的做法是:默认使用7分类,也就是剔除contempt;只有论文需要报告8分类结果时才保留,并且用加权损失或者过采样来缓解样本不平衡。
另外,Emotion目录里没有neutral的标注标签。很多教程会把每个序列的第一帧当成neutral样本补充进去,这样能多出一批数据,但也要注意:这些第一帧是否真的全部是neutral表情?我在实际操作中发现大部分是,但有个别序列起始帧已经带了轻微表情。如果你追求严谨,可以先跑一遍人脸表情识别模型过滤掉非neutral帧,或者自己人工抽检一批。
3. 从原始帧到训练样本的全流程预处理
3.1 提取峰值帧,别把整条序列都塞给模型
CK+的每段序列都是从neutral逐渐过渡到目标表情,目标表情在峰值帧上最明显。训练静态分类模型时,最常用的做法是直接取每个序列的最后一帧作为峰值帧。但需要小心,个别序列的最后一帧并不是表情最强烈的帧,更稳妥的方案是读取该序列的FACS标注文件对应的帧号,或者对整段序列逐帧做一次置信度统计,取置信度最高的帧。
下面是一个简化版的峰值帧提取脚本,适合绝大多数情况:
import cv2 from pathlib import Path def get_peak_frame(seq_dir: Path) -> Path: frames = list(seq_dir.glob("*.png")) frames.sort(key=lambda p: int(p.stem)) # 按帧编号排序 return frames[-1] # 遍历所有序列,收集峰值帧 image_root = Path("cohn-kanade-images") peak_frames = [] for subject_dir in sorted(image_root.iterdir()): for seq_dir in sorted(subject_dir.iterdir()): peak_frames.append(get_peak_frame(seq_dir))这段代码的“最后一帧”策略在绝大多数情况下是可行的,因为CK+的序列设计就是到峰值帧结束。如果之后读取Emotion标签时发现某个峰值帧没有对应标签文件,就直接跳过这个样本。
3.2 人脸对齐和裁剪的确定性规则
CK+的图像本身是正脸为主,但不同被试者的人脸位置、大小、左右偏移仍然有差异。如果直接把整张图resize成固定尺寸喂给模型,背景比例不一致会引入噪声。我通常先做人脸对齐,再统一裁剪到112x112或224x224。
常规做法是用dlib检测68个关键点,然后根据两只眼睛的位置做仿射变换,把眼睛摆到同一水平线:
import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") def align_face(img): rect = detector(img, 1)[0] shape = predictor(img, rect) coords = np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) left_eye = coords[36:42].mean(axis=0) right_eye = coords[42:48].mean(axis=0) angle = np.degrees(np.arctan2( right_eye[1] - left_eye[1], right_eye[0] - left_eye[0] )) center = (left_eye + right_eye) / 2 matrix = cv2.getRotationMatrix2D(tuple(center), angle, 1.0) aligned = cv2.warpAffine(img, matrix, (img.shape[1], img.shape[0])) return aligned对齐之后可以按检测框裁剪,也可以固定以人脸中心裁一块正方形区域,再resize到目标尺寸。这里有一个容易被忽略的要求:训练集和测试集必须使用完全相同的对齐和裁剪流程,不能训练时用一个版本、测试时用另一个版本,否则模型性能会崩塌。
提示:
Landmarks目录里官方也提供了68点坐标,可以直接读取,省去dlib检测这一步。但官方landmark可能和某些帧不完全对齐,如果发现对齐失败,回退到dlib检测会更有保障。
3.3 数据划分必须按人分,不能按样本分
这是CK+使用中最容易出问题的一步。很多人下载数据后直接把所有峰值帧放进一个列表,然后train_test_split随机划分,测试准确率高达98%以上,但换了新的人脸就明显下降,原因就是身份信息泄漏。
同一个被试者的多段序列虽然表情不同,但面部结构、肤色、纹理特征高度相似。如果同一个人的一部分序列在训练集、另一部分在测试集,模型实际上记住了“这个人的脸”而不是“这个人的表情”。正确做法是按被试者划分,即同一subject的所有序列只能出现在同一个集合里。sklearn提供了现成的GroupKFold:
from sklearn.model_selection import GroupKFold # subject_ids 是每个样本对应的被试编号,例如 S001、S002... # X 是所有样本的输入,y 是对应的表情标签 gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(X, y, groups=subject_ids): x_train, x_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx]按人划分后,测试准确率通常会比随机划分低几个百分点,但这才是泛化能力的真实反映。我在自己实验里踩过这个坑,后来养成了习惯:任何数据集拿到手,先检查是否存在同一个人出现在多个集合里的情况。
4. 两种典型训练路线和调参经验
4.1 走静态帧分类路线,baseline怎么搭
最省事的方案是只用峰值帧做静态图像分类。输入是112x112的灰度图或转成RGB的三通道图,模型用ResNet18、MobileNetV2这类轻量网络就够了。相比ImageNet动辄上百万张的训练规模,CK+只有几百个样本,模型太大反而容易过拟合。
我自己常用的配置是:ResNet18预训练权重起步,把最后一层全连接改成7维输出,batch size设为32,优化器用Adam,初始学习率1e-4,训练30到50个epoch,配合早停。数据增强只做水平翻转、小角度旋转、轻微亮度对比度调整。增强强度不能太大,因为CK+本身是受控数据,过度增强会把表情的细节破坏掉。
在按subject划分、剔除contempt的7分类设置下,ResNet18的准确率通常能到95%到98%,这个数字看起来很高,但不要因此兴奋。CK+的数据量小、类别区分度大,几乎所有方法都能刷到接近满分。真正有意义的是对比不同方法在相同划分下的相对差异,以及查看每个类别的recall,特别是fear、sadness这种容易混淆的类别。
4.2 用上时序信息,C3D与两帧方案的取舍
CK+带有时序信息,这是它区别于FER2013的最大优势。想充分利用时序,常见路线是用C3D、I3D这类3D卷积网络吃整段序列,或者用RNN/LSTM聚合帧特征。但CK+序列长度差异极大,有的序列只有五六帧,有的几十帧,直接塞给3D卷积很麻烦,需要先做长度归一化到固定帧数,比如统一采样8帧或16帧。
实际体验是这个方案很看运气:受限于样本量,3D卷积很容易过拟合,而且训练速度远低于静态模型。如果你的目标是快速验证想法,我不会推荐一上来就上C3D。
另一个更轻量但也有效果的做法是“两帧方案”:取序列的第一帧作为neutral状态,取峰值帧作为表情状态,把两帧拼接成双通道输入,让模型自己学习“从neutral到目标的动态变化”。这个方案既保留了部分时序信息,又避开了长序列处理问题,在CK+上性价比很高。论文里不少工作也是这么做的,用作baseline很合适。
4.3 小而偏的数据集,增强怎么做
CK+的类别分布并不均匀。happy和surprise样本相对多,fear、sadness、disgust相对少,contempt最少。处理方式有两种:一种是在采样阶段对样本少的类别做加权采样,另一种是在损失函数里给样本少的类别更高的权重。
在实践中,我更倾向于先用加权采样把每个batch的类别分布拉平,再配合一个普通的交叉熵损失;如果模型在少数类上还是不够好,再加权重。
数据增强方面,除了常用的翻转和裁剪,还可以尝试Mixup,即把两张训练图按比例混合生成新样本。Mixup在小数据集上的效果往往比单纯做仿射变换更明显,因为它本质上是做了样本空间的插值,让决策边界更平滑。但要注意,Mixup适合单帧静态模型,不适合带序列结构的输入。
5. 常见问题与排查技巧实录
5.1 标签文件对不上序列
最常见的问题是Emotion目录下的标签文件比图像序列少很多。比如你在cohn-kanade-images下找到593个序列,但Emotion下只有300多个txt文件。这不是数据损坏,而是官方只为有明确目标表情的序列提供了语义标签。
解决办法是:先扫描Emotion目录拿到所有标签文件,再去cohn-kanade-images里找对应的序列,只保留两者匹配的样本。不要反过来先遍历所有序列再找标签,否则你会收到一堆“缺失文件”报错。
5.2 不同序列的长度差异极大
有的序列只有5帧,有的有60帧,如果直接按固定步长采样,短序列会被重复采样很多次,导致特定样本过度出现。处理这类问题要分场景:静态分类只取峰值帧,长度差异无所谓;时序模型需要规定一个目标长度N,短序列循环补齐到N帧,长序列均匀抽样到N帧。
还有一个细节:不要假设“峰值帧一定是最后一帧”。虽然大部分序列如此,但极少数序列在达到峰值后还有回落帧,取最后一帧可能导致标签和画面不匹配。我的排查方法是随机抽100个序列,把最后一帧和标签对应起来人工看一眼,发现问题再改用FACS标注的峰值帧。
5.3 网上的预处理包可能坑过你
很多GitHub仓库和网盘里流传着“CK+预处理完毕版”,直接把图像裁好、对齐好、按文件夹分好类,用起来确实方便。但这类版本不知道经过了谁的处理,划分逻辑不一定规范。
比如有些包是按个人理解随机划分的数据集,你拿它跑出来的98%准确率没法写进论文;又比如有些包把水平翻转后的样本混入原始数据,导致训练集和测试集之间存在关联样本。如果你要复现论文结果,或者拿数据做正式实验,我建议还是从官方渠道下载原始版本,自己跑一遍预处理。纯学习验证那无所谓,省时间更重要。
5.4 有关准确率高低的认知问题
即使按subject划分正确,CK+上能达到95%以上准确率也是正常的。一个常见认知误区是“准确率这么高,说明模型已经很强了”。实际上,CK+是在受控环境下采集的,背景统一、正脸摆拍、表情夸张清晰,模型容易学会“区分表情”而不是“识别表情”。
所以我在做CK+实验时,除了报告准确率,还会做两件事:一是画混淆矩阵,找出fear和surprise、disgust和anger这类容易混淆的类别;二是做一次跨数据集测试,比如在CK+上训练,在FER2013的部分样本上直接测,看看泛化能力剩多少。这才是更接近实际应用的评估方式。
最后分享一个我一直在用的习惯:任何表情识别模型,我都会把训练好的模型跑在一个自拍视频上,看实时预测的帧间抖动情况。CK+是静态或半静态的峰值帧,而真实视频里表情是连续变化的,帧间抖动大说明模型对中间状态的鲁棒性不够。这个测试能帮你发现很多准确率指标暴露不出来的问题。如果你也打算用CK+做实验,建议从一开始就按照subject划分来做数据处理,不要图省事随机切分,否则后面调参过程中看到的所有高分都可能是假象。
本文还有配套的精品资源,点击获取