Python技术迷

用Python实现人脸识别图片

哎我昨天还真被这事儿折腾了一晚上……就是那个行政小姐姐,丢给我一堆团建照片,说“东哥你不是会点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,少报错,宁愿漏也别瞎认。