简介
笔者曾在《Face swapping》一文中提到过人脸检测,使用流行的“哈尔特征(Haar-like features)级联”算法逐步执行,就能在图像中获得初级的人脸轮廓。
在本文中,笔者将会详细介绍一个转化这种人脸检测算法的自制脚本:取代原本在图像中确认是否包含人脸的做法,这种新办法会通过单纯的级联数据生成一张人脸图片。
跟其他文章一样,这篇文章的全部代码也能在GitHub下载到。
哈尔级联(Haar Cascades)
在2001年,Viola和Jones两位大牛推出了革命性的对象检测算法,这种算法基于哈尔特征级联,首次支持实时监测视频数据中的人脸(及其他对象)。
其核心在于通过一张小图片(一般是20x20)与一些预先算好的级联数据(见下文描述),返回对象是否存在的信息。之后用多个大小与位置不同的窗口使用核心算法对完整图片进行检测,区分目标与非目标:
图片描述
笔者尝试修改的就是这个核心算法。不过,如何操作呢?实际上是基于所谓的“哈尔特征”:
图片描述图片描述
图片描述图片描述
每个特征都与构成所谓“弱分类器”的阈值相关。如果用黑色区域的单个像素点总和减去白色区域的单个像素点总和,结果超出阈值的话则代表通过了弱分类器。例如,在第一张图片中,弱分类器检测到,与脸颊上方对比,眼睛周围有一片深色区域。
所有特征均由轴对齐矩形定义,采用如上图所示的基本形式之一。检测窗口使用与输出图片尺寸相同的小型网格(比如20x20)。
弱分类器组合成阶段。该阶段是否通过取决于相关的弱分类器是否通过;每个弱分类器都有相关权值,如果所有通过的弱分类器总和的权值超过了阶段阈值,那么就表示这个阶段通过了。
阶段、若分类器与相关权值通过运行训练算法来计算,可以在Viola和Jones的论文中查看更多信息。
如果所有阶段都通过了,那么算法就会返回检测到该对象的信息。
相应的Python代码:
def detect(cascade, im):
for stage in cascade.stages:
total = 0
for classifier in stage.weak_classifiers:
if numpy.sum(classifier.feature * im) >= classifier.threshold:
total += classifier.weight
if total < stage.threshold:
return False
return True
假定输入图片im已经调整为与小型级联大小一致。classifier.feature是一个与im形状相同的数组,白色特征区域记为1s,黑色特征区域记为-1s,其他区域为0。注意,Viola和Jones所描述的算法实际上使用了积分图来统计像素值,因此这种算法速度很快。
分为多个阶段的原因主要是为了效率:一般来说,第一阶段只包含少量特征,却能否定大多非人脸图像。在一个特定的级联中,一般有数百个特征,与十几个阶段。
混合整数线性规划(MILP)
为了转化上述的检测功能,笔者用混合整数线性规划表述了这个问题,然后在线性规划中使用MILP求解器。
下面是按照MILP约束描述的检测功能。首先约束能够确保弱分类器在需要时通过。我们称其为特征约束:
图片描述
……还有一系列约束可确保每个阶段通过,我们称其为阶段约束:
图片描述
其中:
图片描述是输入图像的像素值(在代码中以im指代)。
图片描述是与弱分类器j相关联的特征值权值。(在代码中用classifier.feature指代)
图片描述是弱分类器j的阈值。(在代码中以classifier.threshold指代)
图片描述是弱分类器j的权值(在代码中以classifier.weight指代)。
图片描述是一系列指示正权值的分类器,即图片描述
图片描述是一系列指示负权值的分类器,即图片描述
图片描述是一个二进制指示变量,指代弱分类器j是否通过。
图片描述是选中足够大的数字,确保所在项非零,不等式正确。
图片描述是一系列指示k阶段的弱分类器。
图片描述是k阶段的阶段阈值(在代码中以stage.threshold指代)。
MILP求解器寻找的变量是图片描述和图片描述的值。其余值源自级联定义自身。
主要注意点就是将图片描述用作指示变量,即其状态如何打开或关闭其中一个特征约束。比如,取图片描述,如果特定的解决方案作为1通过图片描述,则我们最好确定特征j实际上超过了分类器的阈值,因为它对阶段约束通过产生了积极作用。在这种情况下,关联特征约束中的图片描述项为0,即约束“打开”。
相反,在分类器j为负权值时,我们只关心在图片描述为0的情况下,该特征没有通过分类器阈值。
考虑到这一点,很明显当且仅当有线性规划的解决方案取自cascade:图片描述变量取im中的相应像素值时,detect(cascade, im)为True。
在Python中的MILP
笔者选用了docplex模块以Python编写上述约束。将通过这个模块所编写的约束发送到IBM的DoCloud服务,用远程解决。注意:DoCloud需要注册,而且是收费的,尽管有一个月的免费试用期。我最初用免费的PuLP模块尝试解决这个问题,但由于底层求解器过于简单或是我机器自身的限制,在这方面进展有限。
例如,下面是定义变量的方式:
from docplex.mp.model import Model
model = Model(“Inverse haar cascade”, docloud_context=docloud_context)
pixel_vars = {(x, y): model.continuous_var(
name=”pixel_{}_{}”.format(x, y)
lb=0., ub=1.0)
for y in range(cascade.height)
for x in range(cascade.width)}
passed_vars = [self.binary_var(name=”passed_{}”.format(idx))
for idx in range(len(cascade.features))]
(节选)片段,增加阶段约束:
for stage in cascade.stages:
model.add_constraint(sum(c.weight * passed_vars[c.feature_idx]
for c in stage.weak_classifiers) >=
stage.threshold)
通过调用model.solve()解决掉了问题。
如果成功的话,会从解决方案中提取像素变量值,并转化为图像(numpy数组),之后可以用cv2(或类似的)写入磁盘。
im = numpy.array([[pixel_vars[x, y].solution_value
for x in range(cascade.width)]
for y in range(cascade.width)])
cv2.imwrite(“out.png”, im * 255.)
默认docplex只会寻找可行的解决方案。但可以设置下面这样的对象:
model.set_objective(“max”,
sum((c.weight * passed_vars[c.feature_idx]
for s in self.cascade.stages
for c in s.weak_classifiers)))
该对象会尝试找到解决方案,让大多数结果超过阶段约束。由于可能花费过长事件来找寻真正的最大值,我们将时间设置为:
model.set_time_limit(60 * 60)
求解器会在一个小时后自动停止运算,输出当时的最佳方案(如果有的话)。
更多细节请参见源码。注意:由于支持tilted feature,另外还因为级联数据是OpenCV格式的,因此代码可能会有点复杂,不过本质是相同的。
结果
下面是在OpenCV的haarcascade_frontalface_alt.xml级联中运行程序一小时后的输出结果:
图片描述
还不错。抱歉分辨率太低,不过由于仅定义在20x20的网格中,这点难以避免。下面是同样的一幅图,也很模糊,不过更像一张脸了:
图片描述
要想测试检测的极限,我们需要将阶段约束最小化,而不是最大化:
图片描述图片描述
绝对不太像人脸,不过用OpenCV应当能检测出来。
下面是最好的眼睛图像(基于haarcascade_eye.xml)。
图片描述图片描述
……这幅是最差的:
图片描述图片描述
这幅是最好的侧脸图像(基于haarcascade_profileface.xml):
图片描述 图片描述
……这幅是最差的:
图片描述 图片描述
英文:Making faces with Haar cascades and mixed integer linear programming
作者:Matthew Earl
翻译:孙薇
审校/责任编辑:唐小引