从OpenCV时代的模板滑动窗口,到后来大家人手一个SSD和YOLO,人脸检测这个方向的演进脉络其实特别清晰。RCNN虽然现在看起来像个老古董,速度也确实慢得让人抓狂,但它在目标检测史上的位置非常特殊——第一次把“区域提议”和“深度卷积特征”揉在一起,告诉整个行业检测问题可以拆解成两步走。我在几年前做过一个完整的RCNN人脸检测项目,踩过不少坑,也有些自己的心得体会,这篇就把思路和细节展开聊聊。
这个项目适合两拨人看。一拨是想把检测基础打牢的学生和初级工程师,RCNN的组件化设计会让你明白现代检测器里每个模块到底为什么存在;另一拨是需要在生产场景里做特种识别、对速度不太敏感、但对精度和可解释性有要求的开发者,RCNN拆出来的人脸分类器和回归器可以单独替换,调试起来反而比端到端模型直观得多。
1. 整体设计与方案选型:为什么拿人脸开刀,又为什么选RCNN
1.1 人脸场景的特殊性
人脸检测是目标检测里最经典也最刁钻的场景之一。和通用物体检测相比,人脸有几个独特挑战:一是多尺度问题严重,同一个人从一米到十米,在画面里的尺寸能差几十倍;二是人脸密集程度高,会议室、地铁站这种人头攒动的画面里,几十张脸挤在一起,候选框之间高度重叠;三是姿态和光照变化,侧脸、低头、逆光都会让特征分布发生巨大漂移。
这些特性恰好能把RCNN框架的优势逼出来。RCNN的思路是不再做穷举式滑窗,而是先用传统视觉方法从图像里挑出一两千个“可能含目标”的区域,再做深度特征分类。这种“先粗筛、再精判”的思路,在人脸这种候选区域极其密集的场景里,能大幅减少计算浪费,也让分类器面对的正负样本比更合理。
1.2 三段式架构的优势
RCNN把检测拆分成了三个相对独立的模块:区域提议、卷积特征提取、分类与回归。这个拆法在工程上的价值常被低估。我实际做下来最深的体会是:模块化让每个环节都能单独调优。区域提议漏检了,换提议算法就行;分类误检多,光调SVM不动其它模块;边框框得不准,单独重训回归器。调参范围被限制在局部,问题定位非常快。
如果用端到端的单阶段检测器,这些问题全都耦合在一起。你可能改了损失函数权重,结果框不准和漏检同时变严重,根本不知道是特征提取的问题还是预测头的问题。RCNN这种“老派”设计反而给了我们极佳的调试路径,特别适合在做技术方案验证和基准测评时使用。
1.3 明确放弃反向传播微调检测头的考量
RCNN的设计里还有一点值得玩味:它的分类器用的是SVM,而不是神经网络末尾的Softmax层。最原始的做法是先用图像分类任务预训练CNN,然后只在目标检测数据集上微调CNN的最后一层,再把全连接层特征捞出来喂给SVM。
当时做这个选择,说实话是因为实验对比下来SVM在小样本上的表现更稳。人脸检测的正负样本天然存在数量失衡,负样本(背景区域)比正样本(人脸)多几个数量级。线性SVM在样本不均衡时,只要调整好C参数和负样本采样策略,往往比直接用Softmax更容易控住误检率。不过这个优势在今天已经不是那么明显了,端到端的Focal Loss等方案对类别不平衡的处理已经非常成熟。但在RCNN这个框架里,SVM依然是性价比最高的选择。
2. 核心细节解析:候选区域、深度特征与回归器
2.1 候选区域生成:Selective Search的参数门道
RCNN的第一步是生成候选区域,我这里是基于Selective Search的实现,它通过层次化的颜色、纹理、大小、形状相似度合并,产生一组可能包含物体的区域。给人脸场景调它的参数有几个关键设置。
首先是“最小区域尺寸”,也就是最终保留候选框的面积下限。这个值设大了,远处的小脸会被直接过滤掉;设小了,候选框数量爆炸,后面的CNN特征提取会慢到怀疑人生。我试下来,在720p的输入图像里把最小区域尺寸设在100到200像素之间比较合适。
其次是合并策略的权重分配。Selective Search默认把颜色相似度权重调得比较高,但人脸区域的肤色一致性其实并没有想象中强,受光照影响极大。我实测里增加纹理相似度的权重,对找回逆光、半张脸被阴影覆盖的人脸有明显帮助。改完之后召回率大约提升了三到五个百分点。
2.2 候选区域的归一化与CNN特征提取
区域提议出来之后,每个候选框都得先缩放到固定尺寸才能喂进CNN,我这里统一缩放成227乘227。这个缩放方式有讲究。直接拉伸会让长宽比严重失真,人脸被拉胖压扁,特征提取器很容易被误导。我采用的方法是先等比缩放候选框的长边到227,然后从中心区域直接裁出正方形,这样在最大程度上保留了人脸内部的结构比例。
CNN结构上,我选的是一个模拟AlexNet风格的八层网络,五层卷积加三层全连接。最关键的是要把最后一个全连接层的输出,也就是那4096维特征,好好地标准化,再统一缓存到磁盘上。这一步是整个项目工程效率的分水岭——所有候选区域共享CNN特征,训练SVM和回归器时都不需要再重新跑一遍网络前向传播,节省了大量时间。
2.3 SVM分类器训练与难例挖掘
样本标注规则这段值得多说两句,因为很多人都栽在这个环节。
正样本的定义是:候选区域跟某个真实人脸框的IoU大于0.5,并且是重合度最大的那个框。这里强调“最大的那个”,是为了保证每个人脸最多只对应一个高质量的正样本,避免分类器对同一目标的多个相似框产生困惑。负样本则是IoU小于0.3的区域,这些区域和真实人脸只有部分重叠甚至完全不重叠,是纯净的背景。
难例挖掘在整个人脸检测管线里的价值,再怎么强调都不过分。我第一轮训练出的SVM,误检率在办公室和车内场景高得吓人。原因很简单:第一轮的负样本是随机采样的,而现实中真正的困难负样本是那种含有类人脸纹理的东西——带纹理的木质桌面、方向盘上的皮革纹路、远处垃圾桶的圆形轮廓。随机采样根本抽不中这些。做硬负例挖掘时,把当前模型误检率最高的一批框加进训练集,重新训练,如此循环三四个迭代,误检数直接下降了一个数量级。
2.4 边框回归:Delta回归与尺度归一化
分类器负责判断框里有没有脸,边框回归器负责把有脸的框修正得更贴近真实边界。回归器的目标不是直接预测绝对坐标,而是预测一个带尺度归一化的偏移量。
具体来说,给定候选框的中心坐标(cx, cy)以及宽高(w, h),回归目标是用真实框的中心坐标和宽高,减去候选框的对应值,再除以候选框的宽高,得到四个量纲被归一化到0到1之间的delta值。这样一来,不管候选框是大是小,回归目标都在同一尺度上,模型不需要去记忆绝对位置,只用学相对的调整量。
训练回归器时有一个细节容易出错:样本绝不是所有正样本都能用,而是只选跟真实框IoU比较大的那一批。因为IoU太小意味着候选框本身定位就差得离谱,硬学它的偏移量反而会让回归器整体行为变得不稳定。我这里的阈值经验是IoU大于0.6才参与回归训练。
3. 实操过程:从数据准备到推理全流程
3.1 数据准备与预处理
我用的数据集是从某公开人脸检测基准里筛出来的子集,剔除掉标注框宽高小于20像素的极小人脸,这些样本对特征提取器的贡献太小,但容易增加训练不稳定因素。总共保留了大约1.2万张图像,按7比2比1划分成训练、验证和测试。
数据增强我做了三件事。第一个是水平翻转,人脸虽然大体对称,但刘海、耳环、胡子会让翻转后的图像产生新的纹理变化,对分类器抗干扰能力很有帮助。第二个是随机HSV扰动,尤其把饱和度和色相的扰动幅度加大,模拟不同色温下的肤色变化。第三个是随机裁剪,裁剪范围控制在原图面积的80%到100%之间,同时保证裁剪后真实人脸框仍然完整可见。
这里有个我自己踩过的坑:裁剪人脸密集区域时,经常会把旁边一个人的半张脸裁进框里,这样标签本身就被污染了。所以随机裁剪之后,必须重新检查每个标注框是否仍然完全落在裁剪区域内,同时过滤掉中心偏移过大的标注框。
3.2 训练流水线与关键参数
整个训练分成三个阶段,顺序不能乱。第一阶段是预训练模型的领域微调,用标准的人脸分类任务——人脸类别加背景类别——把CNN在ImageNet上学到的通用特征迁移到人脸特定任务上。第二阶段是离线特征抽取和SVM训练。第三阶段是在SVM分类之后的粗选结果上,训练边框回归器。
SVM训练时负样本数量要控制住。我曾经试过把所有负样本全部灌进去,样本量到了几十万量级,线性SVM的训练时间拉到让人无法接受的程度,而且精度并没有明显提高。后来的做法是把负样本数量控制在正样本数量的四倍左右,再通过难例挖掘动态补充。
训练参数上,CNN微调阶段的学习率我设置成0.001,batch size是128,迭代轮数不要太多,十来个epoch就够,因为我们的目标不是重新学一个图像分类模型,只需要让特征向人脸分布靠近。SVM的正则化参数C大一点,我设成0.01,让分类边界更宽容一些,配合难例挖掘效果更好。
3.3 推理流程与NMS后处理
推理时,对一张图像先跑Selective Search拿到约两千个候选区域,缩放到统一尺寸,批量送入CNN提取特征。特征矩阵同时喂给SVM做分类和边框回归器做位置修正。原始RCNN把分类和回归串行执行,先做分类再筛选出高置信度候选框去跑回归,我并行执行了这两个分支,省了一部分时间。
NMS的设置这里是一个双刃剑。NMS的IoU阈值低,比如0.3,会把互相重叠的检测框大量合并掉,适合人脸之间本来就不太挤的场景;阈值高,比如0.6,保留的框更多,能抵抗密集场景下的漏检,但误检也相应增加了。我调的最终值是0.4,在测试集上漏检和误检相对平衡。迭代式NMS的效果也确实比普通的全局NMS好一些,因为逐轮处理的思路可以让高置信度的框优先保留。
4. 常见问题与排查技巧实录
4.1 误检多但漏检少的困境
我在初版模型上遇到的最大问题是误检和漏检比例失衡。现象是:分类器给出的正样本框特别多,测试集上平均每张图有七八个框,但真实人脸只有三四个,剩下的全是误检。
排查思路是这样的。先看误检框的位置分布,如果误检集中在图像边缘和角落,那大概率是Selective Search生成了大量不合理的边缘区域,把这些区域的候选框直接过滤掉,或者缩小提议区域范围就能解决。如果误检集中在类人脸纹理的区域,那问题出在分类器的正负样本边界,需要加强难例挖掘。
还有个容易忽略的陷阱是SVM的阈值。分类器输出的分数分布并不是0到1之间的概率,而是带正负的原始决策值。我一开始用决策值大于0作为判定条件,误检自然高,后来在验证集上统计分数分布,把阈值调到中位数偏上的位置,误检数砍掉了一半。
4.2 边框回归偏移量异常
边框回归器训完之后,经常出现预测框漂移到图像外面的情况。检查了回归目标的计算代码,发现了一个边界bug:当真实框贴近图像边缘时,IoU很大、满足回归训练条件,但真实框的中心坐标经过候选框宽高归一化之后,得到的delta值仍然非常极端,比如接近正负0.9。这种极端样本会把回归器带偏。
处理方式是在训练数据里直接过滤掉delta绝对值超过0.7的样本。这个阈值设得比较保守,但效果很稳。回归器从此再没有出现过越界框。
4.3 特征缓存与显存管理的平衡
RCNN的特征提取是整条管线里最重的一环,每张图的候选框如果全量提特征,单张图需要跑两千次CNN前向传播。把全部候选框拼成一个batch一次性前传,显存直接爆掉;一个一个跑,速度受不了。
我用的折中方案是按候选框数量分批,每批128个,循环前传,过程中用半精度浮点数存储特征并写回磁盘。这样可以显著减少显存占用,而在精度上几乎无损。后来再复盘,其实可以先做一个粗糙的全图人脸区域预筛——用传统方法把明显不含人脸的候选框先扔掉,比如那些面积和长宽比极不合理的框,特征提取量能降三成以上。
4.4 密集人脸场景的互相抑制问题
在人头攒动的场景里,NMS有时候会把本该保留的两个人脸框合并成一个,因为两个人的脸靠得足够近,IoU超过了阈值。这个问题在标准RCNN框架下很难完全消除。
我采用的补救方案是NMS之前增加一个置信度加权项:当两个框的IoU很高,但置信度分数也都很高时,倾向于同时保留。实现方式是修改NMS的抑制条件,从纯粹的IoU判定变成IoU乘以分数差异的联合判定。这套方案的召回率提升了两个百分点,代价是误检略微增加,在可接受范围内。
5. 对RCNN框架的扩展思考与实际体会
RCNN的训练推理速度放在今天确实不够看,我在CPU上跑一张图要几秒钟,GPU上也要小几百毫秒。但在项目里学到的东西远不止一套人脸检测模型。
这个框架真正教会我的,是“检测问题可分解”的思维模式。后来我看Faster RCNN的Region Proposal Network、YOLO的Grid-Based Detection,心里都能把它们对应到RCNN的哪个模块:区域提议、特征抽取、分类、回归。理解了这个演化脉络之后,再去读新论文、调新模型,思路会清晰得多。
而且RCNN的模块化特性特别适合做人脸相关的垂直场景定制。比如业务方要求优先保证戴眼镜和口罩人群的召回率,我可以不动区域提议和CNN主干,只调整SVM的训练样本权重;如果业务方反馈小脸检测不足,我可以替换Selective Search的合并策略或者直接改图像金字塔层数。这种针对性调优,在端到端模型里做起来要麻烦很多。
最后分享一个从这次项目里沉淀下来的技巧:无论用什么检测框架,先把候选区域的可视化做扎实。把Selective Search生成的所有候选框画在原始图像上,逐帧看完之后,你对自己数据集的难度、对算法到底在哪一环丢分,会有一个非常直观的认知。这一步带来的调试效率提升,远大于调任何一个超参数。RCNN这套老方法的价值就在这里——它把检测的每一步都摊开在桌面上,让你看清问题到底出在哪里。