资讯动态

CASIA-WebFace 数据预处理:清点、对齐、清洗与 rec 打包

发布时间:2026/10/29 20:08:43 来源:尧图企业网站定制
简介面向人脸识别学习者和算法开发者整理的 CASIA WebFace 数据集配套资源适合用于深度人脸识别模型的训练、验证与课程实验。资源包共 1 个文件为 docx 文档大小约 11KB文档给出完整数据集的百度网盘获取信息对应数据压缩后约 4.1GB包含约 1 万个人物身份、50 万张人脸照片属于人脸识别领域常用的大规模身份标注数据。借助该数据读者可开展数据清洗、身份划分、训练集与测试集构建并用于特征提取、度量学习及模型性能对比等任务理解大规模人脸数据在真实项目中的组织方式与使用流程。目前已有 547 人学习适合希望从公开基准数据入手实践人脸识别流程的读者参考。1. 一万个人、五十万张图的 CASIA-WebFace解压前先想清楚三件事解压完你会发现它不是一个大文件而是一万多个文件夹、每个文件夹里几十张 250×250 的 jpg加起来四十多万到五十万张。这份数据最直接的用处就是给 ArcFace、CosFace 这类基于大规模分类的人脸识别模型喂身份标签让它从“分一万个类”里学会把人脸映射成可比的向量。但它是 2014 年前后从网络抓下来的没有清洗过、没有官方测试划分、也没有对齐版本所以拿到压缩包并不等于拿到训练集。真正决定模型效果的是后面清点、对齐、清洗这三步你对 CASIA-WebFace 的理解也得从这三步开始。2. 清点、标签映射与第一版 rec把“文件夹名即身份”翻译成训练能吃的格式CASIA-WebFace 的标注信息全藏在目录名里——一个文件夹就是一个身份文件夹名就是这个人在这份数据里的唯一 ID。问题在于不同下载源的目录名格式并不统一有的带前缀、有的带序号、有的混进了系统垃圾文件。所以在写任何训练脚本之前先把目录结构摸清楚把“身份名 → 整数标签”的映射固定下来否则后面每一次重跑都可能得到不一样的标签体系。2.1 全量清点先算清四十九万张图里到底有多少坏图清点这一步很多人会跳过直接拿 glob 扫一遍就开始训结果跑到第几万步突然报一个UnidentifiedImageError整条流水线断在半夜。正确做法是先做一次只读遍历把每个身份目录下的图片数量、图像尺寸分布、损坏文件全部记下来。这里用 Pillow 而不是 OpenCV是因为 Pillow 的verify()能检出截断文件而cv2.imread遇到坏图只会静默返回 None反而更难定位。import os, json, glob from collections import Counter from PIL import Image ROOT /data/CASIA-WebFace # 只取目录文件名里混进来的 .DS_Store / Thumbs.db 之类不算身份 id_dirs sorted(d for d in os.listdir(ROOT) if os.path.isdir(os.path.join(ROOT, d))) print(f身份目录数: {len(id_dirs)}) sizes Counter() bad [] total 0 for d in id_dirs: # 注意后缀大小写源数据里偶尔会有 .JPG for p in glob.glob(os.path.join(ROOT, d, *.[jJ][pP][gG])): total 1 try: with Image.open(p) as im: w, h im.size # 必须在 verify 之前取verify 之后对象不可再用 im.verify() # 只做完整性校验不解码像素 sizes[(w, h)] 1 except Exception as e: bad.append((p, repr(e))) print(f图片总数: {total}, 坏图: {len(bad)}) print(尺寸分布 Top5:, sizes.most_common(5)) json.dump(bad, open(bad_images.json, w), ensure_asciiFalse, indent2)这段跑完你会得到三个数字身份数、图片总数、坏图数。正常情况下身份数在一万上下、图片总数四十多万尺寸分布应该高度集中在(250, 250)——如果 Top5 里出现了几百种不同尺寸说明你拿到的是被人重采样过或者混了别的来源的版本后面做对齐模板时要把缩放系数考虑进去。坏图不用急着删先落到bad_images.json里等对齐阶段一起处理因为有些“坏图”只是 EXIF 异常Pillow 读不了但 OpenCV 能读。提示这一步在大机械盘上跑满四十九万张图大概要十几分钟别用im.load()那会把像素全解出来内存和时间都翻几倍。2.2 目录名到整数标签的稳定映射框架要的是0..N-1的整数标签而目录名通常是0000001、0000002这种七位数字或者干脆是人名拼写。这一步的关键词是“稳定”——同一份数据在两次不同的机器上跑得到的标签必须完全一致否则你之前训练好的 checkpoint 全部作废这点在换机器或者多机训练时是血泪经验。import os, json, glob ROOT /data/CASIA-WebFace id_dirs sorted(d for d in os.listdir(ROOT) if os.path.isdir(os.path.join(ROOT, d))) # 关键排序之后再编号保证可复现 identity2label {name: i for i, name in enumerate(id_dirs)} json.dump(identity2label, open(identity2label.json, w), ensure_asciiFalse, indent2) # 顺手查一下有没有非图片文件混在身份目录里 stray [p for p in glob.glob(os.path.join(ROOT, *, *)) if not p.lower().endswith((.jpg, .jpeg, .png))] print(非图片文件:, len(stray), stray[:5])这里sorted()是整段的灵魂。os.listdir返回的顺序依赖文件系统的 inode 排列同一份数据在 ext4 和 NTFS 上顺序不同在拷贝前后也可能不同。加一句sorted的成本几乎为零省下的是后续所有实验的可复现性。identity2label.json建议跟 checkpoint 放在一起推理的时候要用它把网络输出的类别号反查成身份名。另外一点stray那个检查别省。真实下载的包里经常混进readme.txt、__MACOSX目录、甚至空文件夹这些空目录如果被算进身份会导致某些标签在训练集里一张图都没有损失函数里对应的权重就永远是随机初始化状态。2.3 打第一版 rec 并抽样回读先把原始图直接打一版包目的是把训练链路跑通而不是拿这版去训最终模型。清单文件.lst格式是三列序号、标签浮点写法、绝对路径。自己写而不是用im2rec --list --recursive原因很实在——那个自动生成模式按os.walk遍历顺序编号换台机器标签就错位而且不给你留映射表。lst_path casia_train.lst idx 0 with open(lst_path, w, encodingutf-8, newline\n) as f: # 强制 LF for name, label in identity2label.items(): for p in sorted(glob.glob(os.path.join(ROOT, name, *.[jJ][pP][gG]))): f.write(f{idx}\t{label}.0\t{p}\n) idx 1 print(lst 行数:, idx)三个参数值得说明标签写成3.0而不是3是因为im2rec解析时按浮点读第二列newline\n是为了防止在 Windows 上写出 CRLF\r会跟着路径一起被塞进RecordIO训练时cv2.imread找不到文件序号必须从 0 连续递增MXIndexedRecordIO靠这个序号直接定位中间断一个就会在随机采样时崩。打包本身一条命令python -m mxnet.tools.im2rec \ --num-thread 16 \ --quality 95 \ casia_train /data/CASIA-WebFace--num-thread 16按 CPU 核数的一半给IO 密集开太多反而互相抢锁--quality 95是 JPEG 重编码质量原始图本身是 JPEG重复编码会有轻微损失有条件的话用--quality 100或者干脆不重编码只做打包。产出是casia_train.rec和casia_train.idx两个文件。打完必须回读抽检别信“命令没报错就是对的”import mxnet as mx from mxnet import recordio reader recordio.MXIndexedRecordIO(casia_train.idx, casia_train.rec, r) for key in [0, 12345, 300000]: header, img mx.recordio.unpack_img(reader.read_idx(key)) print(key, label, int(header.label), shape, img.shape)回读的时候重点看两件事label是否落在0..len(id_dirs)-1区间内shape是否是(250, 250, 3)。如果 shape 变成(1, 250, 250)说明打包时把灰度图也塞进去了训练阶段要统一通道数。提示mxnet在新版 Python 上装轮子经常翻车报libmxnet.so not found。别在这上面耗直接建一个 Python 3.8 / 3.9 的独立环境专门用来打包打包完就不再需要它了。3. 检测与对齐250×250 的原图怎么变成模型要的 112×112这一步是整个流水线里收益最高、也最容易被跳过的一步。CASIA-WebFace 的图是从网页上抓的同一个人的不同照片脸在画面里的位置、大小、旋转角全都不同有的照片人脸占满整幅有的只占中间一小块。直接把 250×250 缩到 112×112 喂给网络等于让卷积层自己去学“把脸摆正”等于把本该由预处理解决的问题丢给模型样本利用率会明显下降。3.1 不对齐就开训损失曲线会教你做人先说结论同一份数据、同一个网络、同一套超参只差一个对齐步骤前十轮 loss 的下降速度能差出一倍以上最终在验证集上的准确率差三到五个点很常见。原因不玄学。人脸识别的分类头本质上是在学一个以身份为簇的度量空间而这个空间的前提是“同一个人的不同照片像素级差异主要来自光照和表情而不是姿态”。不做对齐姿态差异会直接变成像素级差异网络必须消耗大量容量去建模姿态变换。对齐做的事情就是用相似变换把两只眼睛和两个嘴角拉到固定坐标上把姿态这个自由度从数据里先消掉。具体到参数上主流做法是统一的五点模板。InsightFace 那套 112×112 的模板坐标如下顺序是图像左侧眼睛、图像右侧眼睛、鼻尖、左侧嘴角、右侧嘴角import numpy as np ARCFACE_112 np.array([ [38.2946, 51.6963], # 左眼图像坐标不是被拍者的左眼 [73.5318, 51.5014], # 右眼 [56.0252, 71.7366], # 鼻尖 [41.5493, 92.3655], # 左嘴角 [70.7299, 92.2041], # 右嘴角 ], dtypenp.float32)这个顺序一旦弄反对齐出来的图会是左右镜像或者上下颠倒而模型依然能训——只是它学到的特征是镜像版的在正常测试集上表现会莫名地差。判断方法很简单把对齐后的图存二十张出来肉眼扫一眼眼睛在上半部分、嘴在下半部分就对了。3.2 对齐矩阵的求解与参数选择求解用的是相似变换只允许旋转、等比缩放、平移四个自由度不允许剪切和非等比缩放。OpenCV 里有现成的import cv2 def align_face(img, kps, out_size112): kps: (5, 2) 关键点返回 out_size x out_size 的对齐图 src ARCFACE_112 * (out_size / 112.0) # 换尺寸时按比例缩放模板 M, inliers cv2.estimateAffinePartial2D( kps.astype(np.float32), src, methodcv2.LMEDS # 最小中值抗离群点 ) if M is None: return None return cv2.warpAffine(img, M, (out_size, out_size), flagscv2.INTER_LINEAR, borderValue0.0)几个参数methodcv2.LMEDS比默认的最小二乘更稳因为五点检测偶尔会有一个点飘掉最小二乘会被那一个点带偏LMEDS 直接把它当离群点扔掉borderValue0.0是黑边填充人脸贴边旋转后角落会露出空白填 0 比填镜像像素更安全INTER_LINEAR足够因为这里是缩小采样用三次插值反而会引入轻微振铃。out_size给 112 是 ArcFace 系列的标准输入如果你的网络是 128 输入把模板整体乘128/112即可别去改模板里的相对位置。3.3 批量落盘与检测失败样本的回捞批量跑的时候要处理三类异常读不出来的图、检测不到脸的图、检测到多张脸的图。第三类在 CASIA-WebFace 里很常见——这张数据集里大量是网页新闻图背景里经常有路人。import os, cv2, json, glob from insightface.app import FaceAnalysis app FaceAnalysis(namebuffalo_l, allowed_modules[detection]) app.prepare(ctx_id0, det_size(320, 320)) # 原图只有 250320 足够 OUT /data/casia_aligned fails [] for name in identity2label: os.makedirs(os.path.join(OUT, name), exist_okTrue) for p in glob.glob(os.path.join(ROOT, name, *.[jJ][pP][gG])): img cv2.imread(p) if img is None: fails.append((p, read_fail)); continue faces app.get(img) if len(faces) 0: fails.append((p, no_face)); continue # 多脸时取面积最大的一张但要求它明显压过第二名 faces.sort(keylambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1]), reverseTrue) if len(faces) 1: a (faces[0].bbox[2]-faces[0].bbox[0]) * (faces[0].bbox[3]-faces[0].bbox[1]) b (faces[1].bbox[2]-faces[1].bbox[0]) * (faces[1].bbox[3]-faces[1].bbox[1]) if a b * 1.5: fails.append((p, ambiguous_face)); continue kps faces[0].kps if kps is None or len(kps) ! 5: fails.append((p, kps_fail)); continue out align_face(img, kps) if out is None: fails.append((p, affine_fail)); continue cv2.imwrite(os.path.join(OUT, name, os.path.basename(p)), out, [cv2.IMWRITE_JPEG_QUALITY, 95]) json.dump(fails, open(align_fails.json, w), ensure_asciiFalse)det_size给(320, 320)而不是常见的(640, 640)是因为原图最长边才 250放大到 640 只会白白多花三倍时间检测框精度并不会提升。1.5这个倍数是我一般会用的判定阈值低于它就认为主脸不明确宁可丢掉也不要把路人的脸当成这个人的样本——标签噪声对度量学习模型的伤害比少几千张图严重得多。回捞阶段对no_face的那批图做两件事先把图放大 1.5 倍再检测一次有些图的人脸只有四十来像素再不行就换成更宽松的检测阈值。剩下的ambiguous_face基本可以直接放弃人工过一遍几千张图的成本不划算。4. 清洗与去重这份数据最费时间也最值钱的一段CASIA-WebFace 被反复引用的一个已知问题就是标签噪声。当年是从搜索结果里按人名批量抓的同名不同人、同一人不同拼写、抓错图的情况都有公开文献里常被提到的量级是几万张噪声样本。对做实验来说全量直接训也能出一个能用的模型但如果你的目标是刷到某个基线之上清洗这一步的收益远大于调参。4.1 身份纯度一个文件夹里混进别人的脸是常态最省力的自动检查方式是借一个预训练识别模型提特征。不用自己训直接用检测模型自带的 recognition 模块把每张对齐图提成 512 维向量L2 归一化之后算每个身份内部的类中心再看每张图到类中心的余弦距离。import numpy as np def flag_outliers(feats, thresh0.45): feats: (N, 512) 已 L2 归一化 center feats.mean(axis0) center / np.linalg.norm(center) 1e-12 dist 1.0 - feats center # 余弦距离 return np.where(dist thresh)[0], dist0.45这个阈值不是标准答案得按你自己的数据分布定先对全部身份跑一遍把每个身份的dist分布打出来看如果大部分身份的最大距离都在 0.3 以内那 0.45 就是个合理的截断点。注意这个检查对“只有两三张图的身份”几乎无效因为类中心就是这两三张图的平均怎么算都不像离群点所以长尾身份必须人工抽检。人工抽检的策略是分层抽样图片数前 100 的身份抽 2 个中间段抽 100 个身份最少的 100 个身份全看。这个工作量大概两三个小时能覆盖大部分系统性错误。4.2 近重复图dHash 粗筛特征精排重复图在这份数据里有两个来源同一张图被两个不同身份目录收录这是标签冲突必须删以及同一个身份下同一张图存了两份影响不大但浪费。跨身份的重复检测才是重点。直接对四十九万张图两两比汉明距离是1.2e11次比较单机跑不动。实用做法是用分块 LSH把一个 64 位哈希切成四段 16 位汉明距离不超过 3 的两张图至少有一段完全相同所以只需要在每段的桶内比较候选对。import numpy as np from collections import defaultdict def dhash64(img, size8): 输入 BGR 图输出 64 位整数哈希 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) small cv2.resize(gray, (size 1, size), interpolationcv2.INTER_AREA) bits (small[:, 1:] small[:, :-1]).flatten() out 0 for b in bits: out (out 1) | int(b) return out def candidates(hashes, block_bits16): 返回汉明距离可能 3 的候选对 (i, j) n_blocks 64 // block_bits mask (1 block_bits) - 1 buckets [defaultdict(list) for _ in range(n_blocks)] for i, h in enumerate(hashes): for b in range(n_blocks): buckets[b][(h (b * block_bits)) mask].append(i) seen set() for b in range(n_blocks): for _, idxs in buckets[b].items(): if len(idxs) 2: continue for a in range(len(idxs)): for c in range(a 1, len(idxs)): pair (min(idxs[a], idxs[c]), max(idxs[a], idxs[c])) if pair not in seen: seen.add(pair) yield pair粗筛出来的候选对再用识别模型的特征做精排余弦相似度高于 0.92 并且分属不同身份目录的标记为标签冲突。这一步下来通常能捞到几千对人工确认后从样本更多的那一侧本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑