pjj19854头像
关注

OpenCV-年龄检测2中文显示与主循环

一、承上启下

上篇完成了模型加载和 getBoxes 函数,本篇贴出年龄检测.py 剩下的完整代码:cv2AddChineseText 函数和 main 主循环,一字未删,并逐段讲解。

二、完整代码(下):中文显示函数和主循环

python

def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
    if isinstance(img, np.ndarray):
        img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

    draw = ImageDraw.Draw(img)
    try:
        fontStyle = ImageFont.truetype(
            r"C:\Windows\Fonts\simsun.ttc", textSize, encoding="utf-8"
        )
    except OSError:
        try:
            fontStyle = ImageFont.truetype(
                r"C:\Windows\Fonts\msyh.ttc", textSize, encoding="utf-8"
            )
        except OSError:
            fontStyle = ImageFont.load_default()

    draw.text(position, text, textColor, font=fontStyle)
    return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)


def main():
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("无法打开摄像头")
        return

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        frame = cv2.flip(frame, 1)
        frame, faceBoxes = getBoxes(faceNet, frame)

        for faceBox in faceBoxes:
            x1, y1, x2, y2 = faceBox
            face = frame[y1:y2, x1:x2]
            if face.size == 0:
                continue

            blob = cv2.dnn.blobFromImage(face, 1.0, (227, 227), MODEL_MEAN_VALUES, swapRB=False)

            genderNet.setInput(blob)
            gender = genderList[genderNet.forward()[0].argmax()]

            ageNet.setInput(blob)
            age = ageList[ageNet.forward()[0].argmax()]

            result = "{},{}".format(gender, age)
            frame = cv2AddChineseText(frame, result, (x1, max(0, y1 - 30)))

        cv2.imshow("frame", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

三、cv2AddChineseText:解决中文显示难题

OpenCV 自带的 cv2.putText 只能显示英文,遇到中文会变成问号。解决办法是借用 PIL:先把 OpenCV 图片(BGR 颜色顺序)转成 PIL 图片(RGB),用 ImageDraw 配合中文字体把字画上去,再转回 OpenCV 格式。函数依次尝试宋体 simsun.ttc、微软雅黑 msyh.ttc,都找不到就用默认字体,所以几乎不会报错。这个函数可以直接复制到任何项目里用。

3.1 函数签名

python

def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
参数含义
imgOpenCV 图像(BGR,np.ndarray)
text要写的中文字符串
position左上角坐标 (x, y)
textColor颜色,RGB 顺序(默认绿色 (0,255,0))
textSize字号

⚠️ 这个函数内部要经过 PIL,所以 textColor 是 RGB,不是 OpenCV 习惯的 BGR。默认 (0,255,0) 恰好绿红蓝对称,看不出问题;若改成红色,就要注意是 (255,0,0) 而非 (0,0,255)。

3.2 OpenCV → PIL

python

    if isinstance(img, np.ndarray):
        img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
  • isinstance 判断:如果已经是 PIL Image 就跳过转换(函数兼容两种输入)

  • cvtColor(..., BGR2RGB):OpenCV 是 BGR,PIL 要 RGB,必须换通道顺序

  • Image.fromarray(...):把 numpy 数组包成 PIL Image 对象

踩坑点:忘了 BGR2RGB 会让颜色红蓝颠倒,比如蓝框里的字显示成红色。

3.3 创建画笔 + 找字体(三级降级)

python

    draw = ImageDraw.Draw(img)
    try:
        fontStyle = ImageFont.truetype(
            r"C:\Windows\Fonts\simsun.ttc", textSize, encoding="utf-8"
        )
    except OSError:
        try:
            fontStyle = ImageFont.truetype(
                r"C:\Windows\Fonts\msyh.ttc", textSize, encoding="utf-8"
            )
        except OSError:
            fontStyle = ImageFont.load_default()
  • ImageDraw.Draw(img):得到画笔对象,后面 draw.text 用它写字

  • 三级降级策略:

    1. 宋体 simsun.ttc(Win 都有)

    2. 微软雅黑 msyh.ttc(更现代)

    3. ImageFont.load_default()(最后的兜底,能显示但字体很小、可能不支持中文)

  • r"..." 是原始字符串,避免 \W、\s 被当转义

  • encoding="utf-8" 处理 ttc 里可能的多语言编码

踩坑点:ImageFont.truetype 找不到文件时抛 OSError,不是 FileNotFoundError(后者是它的子类,但这里写 OSError 更宽),层级捕获很稳。

3.4 画字 + PIL → OpenCV

python

    draw.text(position, text, textColor, font=fontStyle)
    return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
  • draw.text:把字符串画到 PIL 图上

  • np.asarray(img):PIL Image 转回 numpy 数组

  • cvtColor(..., RGB2BGR):换回 OpenCV 的 BGR 顺序

  • 返回新的帧给主循环继续用

为什么要转来转去?
OpenCV 的 putText 字库只支持 ASCII;PIL 支持 TrueType 字体,能渲染中文,代价就是每次画字都要转两遍颜色顺序。

性能提示:每帧每张脸都做一次这种转换,摄像头视频还能撑住;但如果你要处理大量文字/高清帧,可能会掉帧。

四、main 主循环逐段讲解(详版)

4.1 打开摄像头

python

def main():
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("无法打开摄像头")
        return
  • VideoCapture(0):打开 0 号摄像头(笔记本内置摄像头一般就是 0)

  • 若接了多个摄像头,可以试 1、2

  • isOpened() 为 False 通常因为:摄像头被占用、驱动问题、无摄像头

  • 打不开就 早返回(return),不再进循环

踩坑点:不要写 while True: ret, frame = cap.read() 而不检查 isOpened,否则 cap.read() 会一直返回 (False, None),frame 是 None 后面全崩。

4.2 读帧 + 镜像

python

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        frame = cv2.flip(frame, 1)
  • ret:布尔,是否成功读到

  • frame:当前帧图像

  • if not ret: break:处理“读不到帧”(摄像头断开等)

  • cv2.flip(frame, 1):水平翻转(1 表示沿 y 轴翻)

    • 不翻的话:你往右动,画面里的人往左动,像看别人

    • 翻了之后:像照镜子,操作直觉自然

4.3 调用 getBoxes 找人脸

python

        frame, faceBoxes = getBoxes(faceNet, frame)
  • 这就是上篇那个函数,faceNet 是上篇加载好的人脸检测模型

  • 注意必须接两个返回值:frame 和 faceBoxes

  • 返回的 frame 已经画好蓝框(上篇函数里就画了)

主循环数据流:

text

frame (原始镜像)
   │
   ▼ getBoxes
frame (画好蓝框) + faceBoxes ([x1,y1,x2,y2], ...)

4.4 逐张脸判断

python

        for faceBox in faceBoxes:
            x1, y1, x2, y2 = faceBox
            face = frame[y1:y2, x1:x2]
            if face.size == 0:
                continue
  • 切片取脸:frame[y1:y2, x1:x2] 是 numpy 切片,先取行(y),再取列(x)

    • Python 切片是“左闭右开”,y1:y2 取 y1 到 y2-1 行

    • 顺序是 [y, x],不是 [x, y],初学者经常写反

  • face.size == 0:防止切到空区域(比如框贴着边缘)

为什么要单独抠脸?
整张画面里背景(墙、衣服、灯)会干扰年龄/性别模型,抠出来只留脸,模型判断更准。

4.5 做 blob(换均值 + 不换通道)

python

            blob = cv2.dnn.blobFromImage(face, 1.0, (227, 227), MODEL_MEAN_VALUES, swapRB=False)

对比上篇人脸检测那行:

参数人脸检测年龄/性别
均值[104, 117, 123]MODEL_MEAN_VALUES = (78.43, 87.77, 114.90)
swapRBTrueFalse

为什么不一样?

  • 每个模型训练时作者自己定的预处理方式,推理时必须“照抄训练时的配置”

  • 人脸检测是 TensorFlow 模型,作者用了 [104,117,123] 均值 + RGB

  • 年龄/性别是 Caffe 模型,作者用了 Caffe 的 ImageNet 均值 + BGR(不换通道)

记法:blobFromImage 前 4 个参数(图、缩放、尺寸、均值)和 swapRB 都由“模型的训练配置”决定,换模型就得查对应文档。

4.6 性别 + 年龄推理

python

            genderNet.setInput(blob)
            gender = genderList[genderNet.forward()[0].argmax()]

            ageNet.setInput(blob)
            age = ageList[ageNet.forward()[0].argmax()]

拆开看:

  1. genderNet.setInput(blob):把同一个 blob 喂给性别网络

  2. genderNet.forward():前向计算

  3. forward() 返回形状 (1, 2):batch=1,2 个类别的分数

  4. [0] 取第一维,得到长度 2 的数组

  5. .argmax() 找最高分的下标(0 或 1)

  6. genderList[下标] → “男性” 或 “女性”

年龄同理,只是输出 (1, 8),argmax() 是 0~7,查 ageList 得年龄段。

复用同一个 blob 是合理的:

  • 两个网络输入尺寸一样(227×227)

  • 输入预处理一样(同一个均值 + BGR)

  • 所以一次 blob 可以喂两个网络,省一次预处理

踩坑点:

  • argmax() 是 numpy 方法,不是 Python 内置 max()

  • forward() 必须 setInput 之后调用,否则用上一次的输入

4.7 组合文本 + 写中文

python

            result = "{},{}".format(gender, age)
            frame = cv2AddChineseText(frame, result, (x1, max(0, y1 - 30)))
  • "{},{}".format(gender, age) → 例:"男性,25-32岁"

  • 也可以写 f-string:f"{gender},{age}"(更现代)

  • 位置 (x1, max(0, y1 - 30)):

    • x 对齐框左边

    • y 在框上方 30 像素

    • max(0, ...) 防止框贴顶时 y 变负数(PIL 对负数坐标处理可能不友好)

  • 必须重新赋值给 frame:函数返回新数组,不接返回值就等于没写

4.8 显示 + 退出

python

        cv2.imshow("frame", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
  • imshow:显示当前帧

  • waitKey(1):

    • 每帧等 1 毫秒

    • 返回值是按键的 ASCII 码,没按键返回 -1

    • 必须有,否则窗口不刷新 / 卡死

  • & 0xFF:兼容不同系统(某些系统按键码超过 8 位,& 255 只留低 8 位)

  • ord("q"):字符 'q' 的 ASCII 码(113)

  • 按 q 就 break 出循环

踩坑点:

  • 用 cv2.waitKey(0) 会让画面卡在第一帧

  • 忘了 & 0xFF 在某些 Linux/OpenCV 版本上 ord("q") 匹配不上

4.9 收尾

python

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()
  • cap.release():释放摄像头,否则其他程序用不了

  • cv2.destroyAllWindows():关掉所有 OpenCV 窗口

  • if __name__ == "__main__"::只有直接运行本文件才执行 main(),被 import 时不执行(模块复用友好)

五、main 主循环快速回顾

(原教程的简版讲解,保留作为快速索引)

  1. 打开摄像头:cv2.VideoCapture(0) 打开 0 号摄像头;cap.isOpened() 检查是否成功,打不开就打印提示并直接返回。

  2. 读帧:cap.read() 每次读一帧画面,ret 为 False 说明读不到(比如摄像头被拔了),break 跳出循环。

  3. 镜像:cv2.flip(frame, 1) 做水平翻转,画面像照镜子一样自然。不翻转的话,你往右动画面里的人往左动,会很别扭。

  4. 找人脸:调用上篇写好的 getBoxes(faceNet, frame),返回画好蓝框的画面和所有脸的坐标列表。

  5. 逐张脸判断:先把每张脸单独“切”出来——face = frame[y1:y2, x1:x2] 是 Python 切片写法,意思是取这张图 y1 到 y2 行、x1 到 x2 列的这块区域。为什么要切?因为整张画面里背景干扰太多,把脸单独抠出来,网络看得更清楚。face.size == 0 是防止空框的保护。

  6. 做选择题:对切出的脸做 blobFromImage(注意这里减的平均值是 MODEL_MEAN_VALUES,和人脸检测用的不一样,这是训练时定好的,照抄即可),分别喂给性别网络和年龄网络;forward()[0] 拿到一串分数,argmax() 找出最高分的下标,再去列表里取对应的中文标签。

  7. 写结果:result = "{},{}".format(gender, age) 拼成“性别,年龄”,用 cv2AddChineseText 写到人脸框上方——y1-30 就是往上挪 30 像素,max 防止坐标变成负数。

  8. 退出:imshow 显示画面;waitKey(1) 每帧等 1 毫秒,同时返回按键值,按 q 键(& 0xFF 是与 255 做位运算,兼容不同系统)就 break;最后 release 释放摄像头、destroyAllWindows 关闭窗口。

六、整体数据流与上篇衔接

6.1 整体数据流(下篇)

text

cap.read() → frame
    │
    ▼ flip 水平镜像
frame'
    │
    ▼ getBoxes(faceNet, frame')
frame''(带蓝框) + faceBoxes
    │
    ▼ 对每个框
    裁剪 face
    │
    ▼ blobFromImage(face, MODEL_MEAN_VALUES, BGR)
    blob
    ├──► genderNet → argmax → genderList → "男/女"
    └──► ageNet    → argmax → ageList    → "25-32岁"
    │
    ▼ 拼字符串 "性别,年龄段"
    │
    ▼ cv2AddChineseText(BGR→RGB→画字→RGB→BGR)
frame'''
    │
    ▼ imshow
    │
    ▼ waitKey(1) == 'q' ? break : 继续

五、运行效果与小结

运行后,摄像头画面里每张脸都被蓝框圈住,上方显示"性别,年龄段"。提醒一句:这些模型是用国外人脸数据训练的,猜亚洲人的年龄可能有偏差,属于正常现象。

作业:把 getBoxes 的 conf_threshold 默认值 0.7 改成 0.5,观察框是变多了还是变少了,想一想为什么。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/pjj19854/article/details/166255865

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--