一、 前言
2021 年底,领导给了个tof 模块,要求基于此开发一个演示程序,实现3D 人脸识别的功能。当时听他说出3D 人脸识别就有点头疼,第一是想自己之前没接触这样的项目;第二是在想3D 人脸数据相比于2D 人脸数据,恐怕没后者那么多。基于快速开发出产品以及自身能力的想法,向领导建议使用2D+ 技术路线,即采用rgb 图做人脸识别,采用深度图做真假脸识别,领导同意了。
rgb 图用到的就是些网上开源、成熟的模型,如retinaface 、mobileface ,这部分不是今天的主题,也没什么好说的,网上博客大把。主要说说深度图吧,简单把过程记录一下,方便自己且抛砖引玉,如果有错漏之处,还请指出,谢谢!
二、 过程
(一)图片预处理过程
先给大伙看看tof 保存的深度图,16 bit png 格式,每个像素值的实际物理意义是距离,单位是mm 。 我没有上传错,原图就是这样,看起来乌漆嘛黑的。因为它是16 bit ,像素值范围是[0, 65536) ,下面给它映射到[0, 256) ,再给它像素反转一下。
import cv2
import numpy as np
def u16_to_u8(depth_image):
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(depth_image)
alpha = 255 / (max_val - min_val)
beta = -min_val
result = ((depth_image + beta) * alpha).astype(np.uint8)
return result
depth_image = cv2.imread(r"test.png", cv2.IMREAD_ANYDEPTH)
depth_image = u16_to_u8(depth_image)
depth_image = 255 - depth_image
cv2.imshow('depth_image', depth_image)
key = cv2.waitKey(0)
显示结果如下 可以看见也没什么卵区别,都是一片糊。如果要根据深度图来做分类,区分真假脸,这样的数据恐怕不好做,所以需要再做进一步处理,比如直方图均衡。先试试opencv 自带的直方图均衡函数。
depth_image = cv2.imread(r"test.png", cv2.IMREAD_ANYDEPTH)
depth_image = u16_to_u8(depth_image)
cv_he = cv2.equalizeHist(depth_image)
cv2.imwrite(r"t1.png", cv_he)
看起来效果好了点,但还是有点不够。下面试试openni 的直方图均衡函数。
def get_his_img(img, his_size = 65536):
if len(img.shape) == 2:
img = img[np.newaxis, :]
_, h, w = img.shape
hist = cv2.calcHist(img, [0], None, [his_size - 1], [1, his_size])
num = np.sum(hist)
hist = np.cumsum(hist.squeeze())
for s in range(0, his_size - 1):
hist[s, ] = int((1.0 - (hist[s, ] / num)) * his_size)
for ii in range(h):
for jj in range(w):
if img[:, ii, jj] > 0:
img[:, ii, jj] = hist[img[:, ii, jj], ]
if len(img.shape) == 3:
img = img.squeeze()
return img
depth_image = cv2.imread(r"test.png", cv2.IMREAD_ANYDEPTH)
my_he = get_his_img(depth_image, his_size = 65536)
my_he = u16_to_u8(my_he)
cv2.imwrite(r"t1.png", my_he)
这样才算好,脸部轮廓明显,估计用个逻辑回归都可以很好拟合。仔细看代码,可以看见openni 对原先像素值为0 的像素是不做处理,它的处理对象是像素值为[1, 256) 的像素。当然你们做个筛选,把近距离与远距离的像素值都置为0 ,再用opencv api 来做,估计效果也不错。
(二)真假脸分类算法开发
先给大家看看真假脸的图片,看完会有更深的认识。出于减少运算量与降噪需要,我们取出face roi 。下面分别是真人脸、图片脸、电子屏幕脸,根据需要缩放到112x112 大小(使用letterbox 方式)。 一开始想无脑直接上机器学习,但是公司服务器没有,办公的PC 太辣鸡(呵呵),不想用,所以想着用传统机器视觉来做。
1. 普通直方图比较 准备一张真人脸(随机抽取的一张正面人脸)与测试人脸(无论真假脸,预处理一致)做比较,看代码吧,一目了然。
def compare_img(true_face, fake_face):
true_face = cv2.resize(true_face, (112, 112)).astype(np.float32)
fake_face = cv2.resize(fake_face, (112, 112)).astype(np.float32)
true_hist = cv2.calcHist([true_face], [0], None, [256], [0, 256])
fake_hist = cv2.calcHist([fake_face], [0], None, [256], [0, 256])
match1 = cv2.compareHist(true_hist, fake_hist, cv2.HISTCMP_BHATTACHARYYA)
match1 = 1 - match1
if (match1 < 0.75):
print(0)
match2 = cv2.compareHist(true_hist, fake_hist, cv2.HISTCMP_CORREL)
if (match2 < 0.965):
print(1)
match3 = cv2.compareHist(true_hist, fake_hist, cv2.HISTCMP_CHISQR)
if (match3 > 8000):
print(2)
print("巴氏距离:%f, 相关性:%f, 卡方:%f\n" %(match1, match2, match3))
img_dir = r"C:\Users\Horizon-Robotics\Pictures"
fake_face_path = os.path.join(img_dir, "test.png")
true_face_path = os.path.join(img_dir, "H5.png")
print(os.path.basename(true_face_path), " vs ", os.path.basename(fake_face_path))
true_face = cv2.imread(true_face_path, cv2.IMREAD_ANYDEPTH)
fake_face = cv2.imread(fake_face_path, cv2.IMREAD_ANYDEPTH)
compare_img(true_face, fake_face)
该方法性能不够,后来采集了个小数据集(真假人脸各130 张图片),利用该算法一测试,结果惨不忍睹。想了一下,失败的原因如下(自己瞎想的,如有大神有心得,还望不吝赐教)。 (1)人脸是随机抽取的,不具有代表性; (2)该算法得到的直方图,只是对图片做了个总体地、粗略的估计。而在全局上,真假人脸可能具有相似的直方图分布。所以我们应该关注局部的特征?
2. LBP-直方图比较 出于更多关注局部特征,引入了LBP算法,正如它的介绍所说:“用于纹理特征提取,提取的特征是图像的局部的纹理特征。LBP就是一种局部信息,它反应的内容是每个像素与周围像素的关系。”
做了lbp 的图片直方图如上,可以看见二者区别还是有的,而且很明显。后来跑了下上面的小数据集,结果准确率是100%,与上面方法相比,该方法是可行的。此处还有个考量:是否统计一下数据集里所有真人脸的lbp 直方图,然后得到一张平均的人脸,之后拿这张平均人脸去做计算,防止其过拟合。代码如下:
def my_LBP(img):
dst = np.zeros(img.shape, dtype=img.dtype)
h, w = img.shape
"""
对于每个cell中的一个像素,将相邻的8个像素的灰度值与其进行比较,
若周围像素值大于中心像素值,则该想点的位置被标记为1,否则标记为0.
"""
for i in range(1, h - 1):
for j in range(1, w - 1):
center = img[i][j]
code = 0
code |= (img[i - 1][j - 1] >= center) << (np.uint8)(7)
code |= (img[i - 1][j] >= center) << (np.uint8)(6)
code |= (img[i - 1][j + 1] >= center) << (np.uint8)(5)
code |= (img[i][j + 1] >= center) << (np.uint8)(4)
code |= (img[i + 1][j + 1] >= center) << (np.uint8)(3)
code |= (img[i + 1][j] >= center) << (np.uint8)(2)
code |= (img[i + 1][j - 1] >= center) << (np.uint8)(1)
code |= (img[i][j - 1] >= center) << (np.uint8)(0)
dst[i - 1][j - 1] = code
return dst
fake_img = cv2.imread(r'D:\AOBI\0\0_26.png', cv2.IMREAD_GRAYSCALE)
true_img = cv2.imread(r'D:\AOBI\1\1_0.png', cv2.IMREAD_GRAYSCALE)
fake_img = my_LBP(fake_img)
true_img = my_LBP(true_img)
plt.subplot(2,1,1)
plt.hist(fake_img.ravel(), 256, [0,256], facecolor='g', label = "fake")
plt.subplot(2,1,2)
plt.hist(true_img.ravel(), 256, [0,256], facecolor='r', label = "true")
plt.show()
3. 逻辑回归 在方法1 遇挫后,还是真香定律地试了下逻辑回归。大致流程就是将face roi 做直方图均衡预处理,再缩放至32x32 ,之后reshape 成向量,喂入逻辑回归模型。最后的测试结果98.4% 准确率。但是它有个问题就是,类间距离不够大,即预测的结果有一些在阈值0.5附近。还有就是那些错分样本的输出概率值都蛮离谱的。放张错分的假人脸图片,给大伙看下。 可以看见还是比较像人脸的,难怪会分类错误。对于这样的问题,想了下,可能是模型的拟合能力不够,对于一些类人脸的假脸图片就有点力不从心。
4. LBP-逻辑回归 对于方法3 的问题,无脑的上CNN 是我不想的,所以我选择提升输入图片的质量。刚好上面用了LBP,就显示看了下。 可以看出,对于真人脸,LBP处理后人脸轮廓还是有的。但对于假人脸,即使其本来像真人脸,但是已经不那么明显了。于是将其输入逻辑回归模型训练,最后测试结果的准确率是100%。
三、 后语
近几年的感受就是,深度学习里要好好结合传统视觉,能取得让人惊喜的效果。以及多总结,多记录,多分享。
|