给识别库当门卫的,是一行 cv2.Laplacian().var()
上一篇说录入时要拒收糊照片,这篇讲'糊不糊'是怎么被量出来的。问 Gemini 找到的拉普拉斯方差:二阶导数抓边缘,方差看边缘够不够锐。一行代码,把运动模糊和脏镜头的废帧挡在特征库外面。
给识别库当门卫的,是一行 cv2.Laplacian().var()
上一篇写室友被认成我的那个 bug,结尾说修复之一是”录入时加质量过滤,糊的照片直接拒收”。当时一句话带过了,但那句话背后其实藏着一个具体问题:
程序怎么知道一张照片糊不糊?
人眼一眼就能看出来。可代码里没有”一眼”这个函数。
怎么找到它的
我一开始连这类技术叫什么名字都不知道。用搜索引擎搜”怎么判断图片清晰度”,出来的全是修图教程。
于是换了个思路,直接问 Gemini:有没有一种可以做图像清晰度评估的技术?
它给了几个方向,其中最经典、最常用的一个叫拉普拉斯方差(Variance of Laplacian)——对图像做一次拉普拉斯变换,然后算结果的方差,一个数就是清晰度评分。有了这个术语,剩下的就好办了:顺着关键词找到 OpenCV 官方讲边缘检测的文章(Edge Detection Using OpenCV),把拉普拉斯算子的原理啃了一遍。
顺便的收获:当你不知道一个东西叫什么名字的时候,问 AI”有没有做 X 的技术”比搜索引擎好用得多。它的价值不是给答案,是给术语。拿到术语,文档自己就能读了。
原理
整个算法就一行,拆开是三步:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
lap = cv2.Laplacian(gray, cv2.CV_64F)
score = lap.var()
第一步,转灰度。 清晰度只关乎亮度变化,颜色不参与,先把三个通道压成一个。
第二步,拉普拉斯算子。 它是图像的二阶导数:一阶导数(梯度)测的是亮度变化有多快,二阶导数测的是”变化率本身的变化”,对突变最敏感。而图像里的突变就是边缘——睫毛、发丝、皮肤纹理、眼镜框的轮廓。清晰的照片里这些边缘干脆利落,拉普拉斯响应又强又密;糊的照片相当于先被卷了一层低通滤波,突变全被抹平,响应弱得多。运动模糊和镜头脏污的本质都是这种”抹平”。
CV_64F 这个参数有讲究:边缘两侧一明一暗,拉普拉斯的输出天然有正有负。要是用图像默认的 uint8 来存,负值会被直接截成 0,一半的边缘信息当场丢掉。所以必须用 64 位浮点。
第三步,.var()。 对这个高频边缘矩阵算方差。清晰图的响应里有大量很强的正峰和负峰,数值散得开,方差大;糊图的响应全趴在 0 附近,方差小。于是一个标量就成了清晰度评分——运动模糊的人脸,这个值明显偏低。
为什么用在这里
这是在把照片喂给 AI 模型之前做质检:运动模糊的、镜头有污垢的废帧,直接过滤掉,不许进特征库。
上一篇已经交过学费了:糊脸抽出来的特征向量,五官细节全被抹掉,是”一张糊掉的脸”的平均长相,跟谁都有点像。而匹配又是取全库最高分——一张废帧进库,整个库等于报废。
所以门卫必须站在录入那一步。垃圾数据不进门,比进门之后再想办法补救便宜得多。
它在代码里的位置
打分函数本身:
@staticmethod
def _sharpness(image: np.ndarray) -> float:
"""拉普拉斯方差清晰度:运动模糊的人脸值明显偏低"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return float(cv2.Laplacian(gray, cv2.CV_64F).var())
调用它的地方在注册管线 detect_and_extract 里——检测最大人脸、对齐、抽特征,顺手把清晰度评分一起算出来带回去:
det = self._detect_largest(frame)
aligned = self._recognizer.alignCrop(frame, det)
feature = self._recognizer.feature(aligned).flatten()
return det, feature, self._sharpness(aligned), aligned
有个细节值得说:打分的对象不是整幅画面,而是 aligned——YuNet 对齐之后的 112×112 纯人脸切片,也就是 SFace 抽特征时真正吃进去的那些像素。这么做有两个原因。
一是质检对象要和模型输入一致。整幅画面里背景可能很锐——书架上的字、窗框的边——脸糊了背景清晰,照样能拿高分。只给人脸切片打分,量的才是”这张脸糊不糊”。
二是拉普拉斯方差有个出了名的缺点:分数受分辨率和画面内容影响很大,不同尺寸、不同题材的图之间没法直接比。但对齐切片永远是 112×112 的人脸,尺寸固定、内容固定——分数之间才有可比性,拒收阈值才定得下来。
后记
它不是万能的。方差量的其实是”高频能量”,不是语义上的”清晰”:一张噪点很重的糊图,噪点本身就是高频,能骗到一个不低的分。但在我这个场景里——同一个摄像头、固定尺寸的人脸切片、只需要拦运动模糊和脏镜头——它够用,而且便宜到一帧都不用犹豫。
阈值我是拍了一批故意晃的和正常的样本,看两坨分数中间在哪里切一刀定的。上一篇结尾挖的那个坑——相似度阈值从数据里自动校准——还没填。这篇先把小的这个填了。