一、承上启下
上篇完成了模型加载和 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):
| 参数 | 含义 |
|---|---|
img | OpenCV 图像(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用它写字 -
三级降级策略:
-
宋体
simsun.ttc(Win 都有) -
微软雅黑
msyh.ttc(更现代) -
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) |
| swapRB | True | False |
为什么不一样?
-
每个模型训练时作者自己定的预处理方式,推理时必须“照抄训练时的配置”
-
人脸检测是 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()]
拆开看:
-
genderNet.setInput(blob):把同一个 blob 喂给性别网络 -
genderNet.forward():前向计算 -
forward()返回形状(1, 2):batch=1,2 个类别的分数 -
[0]取第一维,得到长度 2 的数组 -
.argmax()找最高分的下标(0 或 1) -
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 主循环快速回顾
(原教程的简版讲解,保留作为快速索引)
-
打开摄像头:
cv2.VideoCapture(0)打开 0 号摄像头;cap.isOpened()检查是否成功,打不开就打印提示并直接返回。 -
读帧:
cap.read()每次读一帧画面,ret为 False 说明读不到(比如摄像头被拔了),break跳出循环。 -
镜像:
cv2.flip(frame, 1)做水平翻转,画面像照镜子一样自然。不翻转的话,你往右动画面里的人往左动,会很别扭。 -
找人脸:调用上篇写好的
getBoxes(faceNet, frame),返回画好蓝框的画面和所有脸的坐标列表。 -
逐张脸判断:先把每张脸单独“切”出来——
face = frame[y1:y2, x1:x2]是 Python 切片写法,意思是取这张图 y1 到 y2 行、x1 到 x2 列的这块区域。为什么要切?因为整张画面里背景干扰太多,把脸单独抠出来,网络看得更清楚。face.size == 0是防止空框的保护。 -
做选择题:对切出的脸做
blobFromImage(注意这里减的平均值是MODEL_MEAN_VALUES,和人脸检测用的不一样,这是训练时定好的,照抄即可),分别喂给性别网络和年龄网络;forward()[0]拿到一串分数,argmax()找出最高分的下标,再去列表里取对应的中文标签。 -
写结果:
result = "{},{}".format(gender, age)拼成“性别,年龄”,用cv2AddChineseText写到人脸框上方——y1-30就是往上挪 30 像素,max防止坐标变成负数。 -
退出:
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



