哎我昨天还真被这事儿折腾了一晚上……就是那个行政小姐姐,丢给我一堆团建照片,说“东哥你不是会点Python嘛,能不能把谁是谁自动标出来,不然我一个个问人要疯了”。我当时嘴硬说小意思,回家打开电脑一看,卧槽,脸是脸,光线跟鬼片似的,角度还贼刁钻……反正就,开始写。
人脸识别这词吧,很多人以为就是“框出脸”,其实那叫检测;真要“识别是谁”,得先把脸变成一串特征向量(embedding),再去比相似度。你要只是“图片里有没有人脸 + 画框”,OpenCV一把梭就行;你要“这张是不是张三”,就得加第二段“特征提取 + 匹配”。
我就按我当时真实干活的套路来,别整太学术,跑得起来最重要。
先说最便宜的版本:只框人脸,顺便把脸裁出来存一份,行政拿去做通讯录也行。你装个 opencv-python 就能跑。代码我当时是这么写的(我这人喜欢写成脚本,跑完就出结果那种):
import cv2
from pathlib import Path
defdetect_and_crop(input_img: str, out_dir: str):
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
img = cv2.imread(input_img)
if img isNone:
raise FileNotFoundError(f"读不到图片:{input_img}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# OpenCV自带的人脸检测器(Haar),便宜但偶尔会漏/误
cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60))
print("检测到人脸数量:", len(faces))
# 画框 + 裁剪
for i, (x, y, w, h) in enumerate(faces):
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
face = img[y:y+h, x:x+w]
cv2.imwrite(str(out_dir / f"face_{i}.jpg"), face)
marked_path = str(out_dir / "marked.jpg")
cv2.imwrite(marked_path, img)
print("输出:", marked_path)
if __name__ == "__main__":
detect_and_crop("group_photo.jpg", "out_faces")
这段你就当“先救命”,效果一般般,但胜在简单,跑起来不费劲。然后呢,我那晚最烦的点来了:行政真正想要的是“标名字”。那就得搞识别。
我这边用的是 OpenCV 的 DNN 人脸检测 + 人脸特征提取(embedding)+ 余弦相似度比对。模型文件你得自己准备一下(我一般放 models/ 目录),比如:
deploy.prototxt + res10_300x300_ssd_iter_140000_fp16.caffemodel(做人脸检测)openface_nn4.small2.v1.t7(做人脸特征) 这些文件名你别死抠,反正你拿到对应的模型就行,路径对就能跑。
核心思路就是: 同一个人先准备几张“登记照”(清晰、正脸、光线正常),提特征存起来;新照片来了也提特征,然后算相似度,超过阈值就认为是同一个人。阈值这玩意儿很玄学,我当时在公司那堆烂光线照片里,0.75~0.82 之间来回试,气得我一边吃泡面一边调参。
代码我给你整一份“能落地”的,分两步:建库 + 识别标注。
import cv2
import numpy as np
from pathlib import Path
defcosine_sim(a: np.ndarray, b: np.ndarray) -> float:
a = a.astype(np.float32)
b = b.astype(np.float32)
na = np.linalg.norm(a) + 1e-9
nb = np.linalg.norm(b) + 1e-9
return float(np.dot(a, b) / (na * nb))
classFaceRecognizer:
def__init__(self, det_proto, det_model, emb_model):
self.detector = cv2.dnn.readNetFromCaffe(det_proto, det_model)
self.embedder = cv2.dnn.readNetFromTorch(emb_model)
self.db = {} # name -> list[embedding]
def_detect_faces(self, img, conf_th=0.6):
h, w = img.shape[:2]
blob = cv2.dnn.blobFromImage(img, 1.0, (300, 300), (104.0, 177.0, 123.0))
self.detector.setInput(blob)
dets = self.detector.forward()
boxes = []
for i in range(dets.shape[2]):
conf = float(dets[0, 0, i, 2])
if conf < conf_th:
continue
box = dets[0, 0, i, 3:7] * np.array([w, h, w, h])
x1, y1, x2, y2 = box.astype(int)
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w - 1, x2), min(h - 1, y2)
if x2 - x1 < 40or y2 - y1 < 40:
continue
boxes.append((x1, y1, x2, y2, conf))
return boxes
def_embedding(self, face_bgr):
face = cv2.resize(face_bgr, (96, 96))
blob = cv2.dnn.blobFromImage(face, 1.0/255, (96, 96), (0, 0, 0), swapRB=True, crop=False)
self.embedder.setInput(blob)
vec = self.embedder.forward()
return vec.flatten()
defbuild_db(self, enroll_dir: str):
enroll_dir = Path(enroll_dir)
for person_dir in enroll_dir.iterdir():
ifnot person_dir.is_dir():
continue
name = person_dir.name
self.db.setdefault(name, [])
for img_path in person_dir.glob("*.*"):
img = cv2.imread(str(img_path))
if img isNone:
continue
boxes = self._detect_faces(img, conf_th=0.6)
ifnot boxes:
continue
# 默认取最大脸(登记照一般就一张脸)
boxes.sort(key=lambda b: (b[2]-b[0])*(b[3]-b[1]), reverse=True)
x1, y1, x2, y2, _ = boxes[0]
emb = self._embedding(img[y1:y2, x1:x2])
self.db[name].append(emb)
print(name, "登记样本数:", len(self.db[name]))
defidentify(self, emb, threshold=0.80):
best_name, best_score = "unknown", -1.0
for name, embs in self.db.items():
for ref in embs:
s = cosine_sim(emb, ref)
if s > best_score:
best_score = s
best_name = name
if best_score >= threshold:
return best_name, best_score
return"unknown", best_score
defannotate_image(self, input_img: str, out_img: str, threshold=0.80):
img = cv2.imread(input_img)
if img isNone:
raise FileNotFoundError(f"读不到图片:{input_img}")
boxes = self._detect_faces(img, conf_th=0.6)
for (x1, y1, x2, y2, conf) in boxes:
face = img[y1:y2, x1:x2]
emb = self._embedding(face)
name, score = self.identify(emb, threshold=threshold)
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
text = f"{name}{score:.2f}"
cv2.putText(img, text, (x1, max(0, y1 - 8)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.imwrite(out_img, img)
print("写出:", out_img)
if __name__ == "__main__":
fr = FaceRecognizer(
det_proto="models/deploy.prototxt",
det_model="models/res10_300x300_ssd_iter_140000_fp16.caffemodel",
emb_model="models/openface_nn4.small2.v1.t7"
)
# 目录结构大概这样:
# enroll/
# zhangsan/ 1.jpg 2.jpg ...
# lisi/ a.jpg b.jpg ...
fr.build_db("enroll")
fr.annotate_image("test.jpg", "out_marked.jpg", threshold=0.80)
你看我这代码里有个点,特别像我当时那种“干活人的臭毛病”:我不搞什么数据库存特征,我就内存里放着,反正行政一次处理也就几十张,够用了。你要上量再考虑把 embedding 存成 .npy 或者塞进 SQLite,都行。
然后我再说俩坑,不说真不行,我昨晚就是被这俩坑恶心的,差点把键盘拍了。
一个是“同一个人不同光线差别巨大”。你登记照在办公室拍的,识别照片在KTV那种紫光灯下拍的,余弦相似度会直接跳水,你就会看到一堆 unknown,行政问你“是不是你写坏了”,我:……你让她换张正常点的照片试试,真的。或者你把阈值调低一点,但调低就会误认,张三能被识别成李四,这更社死。
另一个是“人脸太小”。团建合照里人脸就几十像素,提出来的特征很飘,基本靠玄学。我在代码里有个过滤:脸框小于 40x40 直接不算,你也可以改成更大,比如 60 或 80,少报错,宁愿漏也别瞎认。