在 CanMV K210 入门和综合项目中,68 点人脸关键点检测是一个很适合拆解的 AI 视觉实验。它不是单纯判断"有没有人脸",而是先找到人脸位置,再在人脸区域内定位眼睛、鼻子、嘴巴和脸部轮廓等关键点。通过这个实验,可以理解摄像头采集、KPU 推理、人脸框裁剪、坐标还原和 LCD 可视化之间的完整关系。
本实验使用 CanMV K210 运行 4.11_face_detect_68lm.py。程序先加载人脸检测模型 /sd/yolo_face_detect.kmodel,检测画面中的人脸框;再加载 68 点关键点模型 /sd/landmark68.kmodel,对裁剪后的人脸区域进行关键点推理。程序会在 LCD 上绘制人脸检测框和 68 个关键点,同时在画面左上角显示 FPS,便于观察识别效果和运行速度。
| 学习目标 | 说明 |
|---|---|
| 理解双模型视觉流程 | 认识人脸检测模型和 68 点关键点模型之间的衔接关系 |
| 掌握人脸框裁剪 | 根据检测框扩大人脸区域,并裁剪出单独的人脸图像 |
| 理解模型输入尺寸 | 人脸检测模型使用 320×256 输入,关键点模型使用 128×128 人脸图像 |
| 学会坐标还原 | 将关键点模型输出的归一化坐标还原到原始画面位置 |
| 建立 AI 视觉调试思路 | 通过 LCD 显示框线、关键点和 FPS,观察模型运行状态 |
文章目录
理论基础
68 点人脸关键点检测并不是一步完成的。程序通常先进行人脸检测,找到画面中人脸的大致位置;再把人脸区域裁剪出来,缩放到关键点模型需要的输入尺寸;最后由关键点模型输出 68 组坐标。这样做的原因是关键点模型需要关注的是人脸局部区域,而不是整张图像中的所有背景。
本实验中使用了两个 KPU 模型。第一个模型 /sd/yolo_face_detect.kmodel 用于检测人脸框,输出人脸在画面中的位置和大小;第二个模型 /sd/landmark68.kmodel 用于检测人脸关键点,输出 68 个关键点的相对坐标。程序再根据裁剪区域的位置和尺寸,把这些相对坐标还原到原始图像中,并用圆点绘制在 LCD 画面上。
人脸检测模型和关键点模型的输入尺寸不同。摄像头采集的是 QVGA 图像,也就是 320×240;代码中额外创建了 od_img = image.Image(size=(320,256), copy_to_fb=False),用于适配人脸检测模型的 320×256 输入。检测到人脸后,程序从原图中裁剪人脸区域,并通过 resize(128, 128) 转换成关键点模型需要的 128×128 输入。
下面这张流程图可以从视觉数据链路理解本实验。摄像头负责采集画面,人脸检测模型负责找到人脸框,关键点模型负责定位 68 个点,绘图函数负责把框和点画回原始图像,LCD 负责显示最终结果。
摄像头采集
sensor.snapshot()
原始图像
320×240 QVGA
检测输入图
od_img 320×256
人脸检测模型
yolo_face_detect.kmodel
人脸框结果
x / y / w / h
扩大人脸框
extend_box()
裁剪人脸区域
img.cut()
缩放到 128×128
face_cut.resize()
68点关键点模型
landmark68.kmodel
关键点输出
68组相对坐标
坐标还原
映射回原图位置
绘制结果
人脸框 / 关键点 / FPS
LCD 显示
lcd.display(img)
SD卡模型文件
/sd/*.kmodel
这条链路的关键在于坐标关系。人脸检测模型得到的是原始画面中的人脸框,关键点模型输出的是裁剪后人脸区域中的相对坐标。代码中使用 KPU.sigmoid(out[2 * j]) 和 KPU.sigmoid(out[2 * j + 1]) 得到归一化后的横纵坐标,再乘以裁剪区域的宽高,并加上裁剪区域左上角坐标 x1、y1,最终得到关键点在原图中的位置。
硬件设施
本实验属于 AI 视觉类实验,核心硬件是 CanMV K210 开发板、摄像头模块、LCD 显示屏和 SD 卡中的模型文件。代码没有使用 GPIO、蜂鸣器、按键、电机或普通传感器,因此硬件检查重点不在排针接线,而在摄像头、LCD、模型文件路径和 KPU 推理环境。
| 硬件 / 软件 | 作用 | 说明 |
|---|---|---|
| CanMV K210 开发板 | 实验运行平台 | 负责运行 MicroPython 程序,并通过 KPU 执行 AI 模型推理 |
| 摄像头模块 | 图像采集设备 | 通过 sensor.snapshot() 获取实时图像 |
| LCD 显示屏 | 结果显示设备 | 通过 lcd.display(img) 显示人脸框、关键点和 FPS |
| SD 卡 | 模型文件存储 | 存放 /sd/yolo_face_detect.kmodel 和 /sd/landmark68.kmodel |
| KPU 神经网络加速器 | AI 推理单元 | 加载 kmodel 模型并执行人脸检测和关键点推理 |
sensor 模块 |
摄像头控制模块 | 用于摄像头初始化、图像格式设置和图像采集 |
image 模块 |
图像处理模块 | 用于创建图像、裁剪人脸、缩放图像和绘制图形 |
lcd 模块 |
显示控制模块 | 用于初始化 LCD 并显示处理后的图像 |
time 模块 |
帧率统计模块 | 通过 time.clock() 统计 FPS |
gc 模块 |
内存管理模块 | 主循环中调用 gc.collect() 回收临时图像对象占用的内存 |
实验检查时,重点关注摄像头是否能正常采集图像,LCD 是否能正常显示画面,SD 卡中是否存在两个模型文件。KPU 模型路径必须和代码中的 load_kmodel() 参数一致,否则程序会在模型加载阶段失败。
| 资源 / 接口 | 代码对象 | 对应作用 | 检查重点 |
|---|---|---|---|
| 摄像头接口 | sensor.reset() / sensor.snapshot() |
初始化摄像头并采集图像 | 确认摄像头连接正常,画面能稳定输出 |
| LCD 接口 | lcd.init() / lcd.display(img) |
显示图像和识别结果 | 确认 LCD 能显示摄像头画面 |
| 人脸检测模型 | /sd/yolo_face_detect.kmodel |
检测画面中的人脸框 | 文件必须存在于 SD 卡指定路径 |
| 关键点模型 | /sd/landmark68.kmodel |
输出 68 个人脸关键点 | 文件名和路径必须与代码一致 |
| 检测输入图像 | od_img |
提供 320×256 的人脸检测模型输入 | 用于适配模型输入尺寸 |
| 人脸裁剪图像 | face_cut |
从原图中裁剪人脸区域 | 裁剪范围由 extend_box() 计算 |
| 关键点输入图像 | face_cut_128 |
提供 128×128 的关键点模型输入 | 裁剪后人脸图像需要缩放到模型尺寸 |
从硬件和资源关系看,本实验的核心不是接几根线,而是保证"摄像头输入、模型文件、KPU 推理、LCD 输出"这一条链路完整。如果 LCD 有画面但没有关键点,通常需要检查模型文件、检测阈值、光照和人脸距离;如果程序在启动阶段报错,通常需要优先检查模型路径和 SD 卡文件。
| 实验现象 | 正常表现 | 异常提示 |
|---|---|---|
| 程序启动 | 串口打印 ready load model |
如果启动后卡住,检查模型文件是否存在 |
| 摄像头画面 | LCD 显示实时画面 | 如果无画面,检查摄像头和 LCD 初始化 |
| 检测到人脸 | 人脸区域出现绿色矩形框 | 如果没有框,检查人脸距离、光照和检测阈值 |
| 关键点显示 | 人脸上出现红色圆点 | 如果有框无点,检查关键点模型是否加载成功 |
| FPS 显示 | 左上角显示当前帧率 | FPS 偏低时减少打印、减少绘制或优化画面条件 |
| 多次检测 | 程序持续刷新人脸框和关键点 | 如果运行一段时间后异常,注意内存回收和临时图像释放 |
软件代码
本实验的软件部分以 4.11_face_detect_68lm.py 为核心。程序导入 sensor、image、time、lcd、KPU 和 gc,随后完成 LCD 初始化、摄像头初始化、人脸检测模型加载、68 点关键点模型加载,并在主循环中持续执行图像采集、模型推理、坐标还原和结果绘制。
| 软件环境 | 作用 | 检查重点 |
|---|---|---|
| CanMV IDE | 编辑、运行和调试 K210 程序 | 能识别开发板串口,并能运行基础摄像头示例 |
| CanMV 固件 | 提供 sensor、image、lcd、KPU 等模块 |
固件环境需要支持 KPU 模型加载和图像处理 |
| SD 卡 | 存放 kmodel 模型文件 | /sd/yolo_face_detect.kmodel 和 /sd/landmark68.kmodel 必须存在 |
| 摄像头环境 | 提供实时画面 | sensor.snapshot() 能正常返回图像 |
| LCD 显示环境 | 显示检测结果 | lcd.display(img) 能正常刷新画面 |
| 串口终端 | 查看模型加载和检测输出 | 能看到 ready load model 和 dect: 等调试信息 |
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----湖南创乐博智能科技有限公司----
# 文件名:4.11_face_detect_68lm.py
# 版本:V2.0
# author: zhulin
# 说明:CanMv K210 基于68点模型人脸检测实验
#####################################################
import sensor, image, time, lcd
from maix import KPU
import gc
lcd.init() # 初始化LCD显示屏
sensor.reset() # 复位并初始化摄像头
sensor.set_pixformat(sensor.RGB565) # 设置摄像头输出格式为 RGB565
sensor.set_framesize(sensor.QVGA) # 设置摄像头输出大小为 QVGA (320x240)
sensor.skip_frames(time = 500) # 等待摄像头稳定
clock = time.clock() # 创建一个clock对象,用来计算帧率
#人脸检测模型需要320*256图输入,这里初始化一个image
od_img = image.Image(size=(320,256), copy_to_fb=False)
anchor = (0.893, 1.463, 0.245, 0.389, 1.55, 2.58, 0.375, 0.594, 3.099, 5.038, 0.057, 0.090, 0.567, 0.904, 0.101, 0.160, 0.159, 0.255)
# 创建一个kpu对象,用于人脸检测
kpu = KPU()
print("ready load model")
# 加载模型
kpu.load_kmodel("/sd/yolo_face_detect.kmodel")
# yolo2初始化
kpu.init_yolo2(anchor, anchor_num=9, img_w=320, img_h=240, net_w=320 , net_h=256 ,layer_w=10 ,layer_h=8, threshold=0.7, nms_value=0.2, classes=1)
# 创建一个kpu对象,用于人脸68关键点检测
lm68_kpu = KPU()
print("ready load model")
# 加载模型
lm68_kpu.load_kmodel("/sd/landmark68.kmodel")
RATIO = 0.08
def extend_box(x, y, w, h, scale):
x1_t = x - scale*w
x2_t = x + w + scale*w
y1_t = y - scale*h
y2_t = y + h + scale*h
x1 = int(x1_t) if x1_t>1 else 1
x2 = int(x2_t) if x2_t<320 else 319
y1 = int(y1_t) if y1_t>1 else 1
y2 = int(y2_t) if y2_t<256 else 255
cut_img_w = x2-x1+1
cut_img_h = y2-y1+1
return x1, y1, cut_img_w, cut_img_h
while 1:
gc.collect()
#print("mem free:",gc.mem_free())
clock.tick() # 更新计算帧率的clock
img = sensor.snapshot() # 拍照,获取一张图像
a = od_img.draw_image(img, 0,0) # 将img图像写到od_img图像的坐标(0,0)位置处
od_img.pix_to_ai() # 对rgb565的image生成ai运算需要的r8g8b8格式存储
kpu.run_with_output(od_img) # 对输入图像进行kpu运算
dect = kpu.regionlayer_yolo2() # yolo2后处理
fps = clock.fps() # 获取帧率
if len(dect) > 0:
print("dect:",dect)
for l in dect :
x1, y1, cut_img_w, cut_img_h = extend_box(l[0], l[1], l[2], l[3], scale=RATIO) # 扩大人脸框
face_cut = img.cut(x1, y1, cut_img_w, cut_img_h) # 从img中裁剪出人脸图
a = img.draw_rectangle(l[0],l[1],l[2],l[3], color=(0, 255, 0))
face_cut_128 = face_cut.resize(128, 128)
face_cut_128.pix_to_ai()
out = lm68_kpu.run_with_output(face_cut_128, getlist=True)
#print("out:",len(out))
for j in range(68):
x = int(KPU.sigmoid(out[2 * j])*cut_img_w + x1)
y = int(KPU.sigmoid(out[2 * j + 1])*cut_img_h + y1)
#a = img.draw_cross(x, y, size=1, color=(0, 0, 255))
a = img.draw_circle(x, y, 2, color=(0, 0, 255), fill=True)
del (face_cut_128)
del (face_cut)
a = img.draw_string(0, 0, "%2.1ffps" %(fps), color=(0, 60, 255), scale=2.0)
lcd.display(img)
gc.collect()
# 创建的kpu对象去初始化,释放模型内存
kpu.deinit()
lm68_kpu.deinit()
这段代码可以分成初始化、模型加载、人脸检测、人脸裁剪、关键点推理、坐标还原、结果绘制和 LCD 显示几个部分。初始化阶段完成 LCD 和摄像头设置,模型加载阶段分别创建 kpu 和 lm68_kpu 两个对象,主循环阶段不断获取图像并执行双模型推理。
od_img 是人脸检测模型的输入缓冲图像。摄像头采集到的 img 是 320×240,而人脸检测模型需要 320×256 的输入,因此代码创建了一个 320×256 的图像对象,并通过 od_img.draw_image(img, 0, 0) 把当前摄像头画面写入其中。随后调用 od_img.pix_to_ai(),把图像转换为 KPU 推理需要的数据格式。
人脸检测由 kpu.run_with_output(od_img) 和 kpu.regionlayer_yolo2() 完成。regionlayer_yolo2() 返回的人脸检测结果保存在 dect 中,每个检测结果包含人脸框的位置和大小。代码用 draw_rectangle() 在原图上绘制绿色人脸框,方便观察模型是否找到了正确的人脸区域。
extend_box() 用于扩大检测框。人脸检测框通常只覆盖主要人脸区域,如果直接送入关键点模型,边缘轮廓可能不完整。代码通过 RATIO = 0.08 向四周略微扩大框,再使用 img.cut() 裁剪人脸图像。裁剪后的人脸图像被缩放为 128×128,并送入 landmark68.kmodel 执行关键点检测。
关键点模型输出的是 68 个点的相对位置,每个点由两个数值表示,分别对应 x 和 y。程序通过 KPU.sigmoid() 把输出结果转换到 0~1 范围,再乘以裁剪区域宽高,并加上裁剪区域左上角坐标,最终得到关键点在原始图像中的坐标。每个关键点通过 draw_circle() 绘制为红色小圆点。
| 函数 / 对象 | 功能 | 对应现象 |
|---|---|---|
lcd.init() |
初始化 LCD 显示屏 | LCD 准备显示摄像头画面 |
sensor.reset() |
初始化摄像头 | 摄像头进入可采集状态 |
sensor.set_pixformat(sensor.RGB565) |
设置图像格式 | 摄像头输出 RGB565 彩色图像 |
sensor.set_framesize(sensor.QVGA) |
设置图像尺寸 | 图像分辨率为 320×240 |
KPU() |
创建 KPU 推理对象 | 分别用于人脸检测和关键点检测 |
load_kmodel() |
加载模型文件 | 从 /sd/ 路径读取 kmodel |
init_yolo2() |
初始化 YOLO2 后处理 | 设置 anchor、阈值、输入尺寸和类别数 |
extend_box() |
扩大人脸检测框 | 裁剪时保留更多脸部边缘信息 |
img.cut() |
裁剪人脸区域 | 从原图中得到单独的人脸图像 |
resize(128, 128) |
调整关键点模型输入尺寸 | 人脸图像适配 68 点模型 |
KPU.sigmoid() |
还原关键点相对坐标 | 将模型输出转换到可计算范围 |
draw_rectangle() |
绘制人脸框 | LCD 上出现绿色矩形框 |
draw_circle() |
绘制关键点 | 人脸上出现红色关键点 |
lcd.display(img) |
显示处理后的图像 | LCD 实时显示检测结果 |
主循环中多次调用 gc.collect(),用于回收临时图像对象占用的内存。关键点检测每一帧都会创建 face_cut 和 face_cut_128,如果不及时释放,长时间运行可能出现内存不足。代码中使用 del 删除临时对象,并配合垃圾回收,适合这类持续推理的视觉实验。
扩展应用
68 点人脸关键点检测实验常见问题集中在模型文件路径、摄像头画面、检测阈值、人脸距离、内存占用和 LCD 显示几个方面。排查时应先确认模型能否加载,再确认摄像头和 LCD 是否正常,最后再调整检测条件和显示效果。
| 问题现象 | 可能原因 | 处理思路 |
|---|---|---|
| 程序启动时报模型加载错误 | kmodel 文件不存在、路径不一致、SD 卡未识别 | 确认 /sd/yolo_face_detect.kmodel 和 /sd/landmark68.kmodel 已放到 SD 卡根目录 |
| LCD 没有画面 | 摄像头或 LCD 初始化失败 | 先运行基础摄像头显示示例,确认 sensor.snapshot() 和 lcd.display() 正常 |
| 能显示画面但没有人脸框 | 人脸太远、角度偏差大、光照不足、阈值过高 | 让人脸正对摄像头,保持适当距离和稳定光照,必要时降低 threshold=0.7 |
| 有人脸框但没有关键点 | 关键点模型未正常加载,或裁剪区域不合适 | 检查 landmark68.kmodel 路径,确认裁剪框中包含完整人脸 |
| 关键点位置偏移 | 裁剪框坐标、缩放比例或坐标还原计算不匹配 | 检查 extend_box() 返回的 x、y、宽高是否合理 |
| 识别结果闪烁 | 光照变化、人脸移动过快、检测框不稳定 | 固定摄像头和人脸距离,增强光照,减少背景干扰 |
| FPS 偏低 | 双模型推理和绘制关键点耗时较高 | 减少串口打印,减少额外绘制,保持 QVGA 输入 |
| 运行一段时间后异常 | 临时图像对象占用内存,垃圾回收不及时 | 保留 del 和 gc.collect(),减少主循环中不必要的对象创建 |
| 检测到多张人脸后变慢 | 每张人脸都会执行一次 68 点模型推理 | 可限制只处理最大人脸,减少关键点推理次数 |
68 点人脸关键点检测的价值不只在于把点画出来,更重要的是获得了面部结构数据。眼睛、鼻子、嘴角、下巴和脸部轮廓点都可以进一步参与计算,例如眼睛开合程度、嘴巴开合程度、脸部方向变化和人脸对齐等。当前程序已经完成了从图像采集到关键点显示的基础链路,后续可以围绕这些关键点继续扩展应用。
| 应用场景 | 实现思路 | 可扩展能力 |
|---|---|---|
| 表情分析 | 根据嘴角、眼睛和眉部关键点的相对位置判断表情变化 | 可扩展为笑脸检测、张嘴检测或简单表情分类 |
| 人脸对齐 | 根据眼睛和鼻梁关键点计算人脸角度并进行校正 | 可用于后续人脸识别或人脸特征提取预处理 |
| 美颜特效 | 根据关键点位置绘制装饰图形或贴纸 | 可扩展为眼镜、胡子、帽子等简易 AR 效果 |
| 疲劳检测 | 根据眼睛关键点计算闭眼状态和持续时间 | 可扩展为驾驶员疲劳提醒实验 |
| 姿态估计 | 根据鼻子、眼睛、嘴巴和脸部轮廓位置估计头部朝向 | 可用于人机交互或视线方向粗略判断 |
| 人脸跟踪云台 | 根据人脸框中心或关键点中心控制舵机方向 | 可结合舵机或步进电机,实现目标跟随 |
| AI 视觉教学 | 用双模型流程讲解检测、裁剪、推理和绘制 | 适合解释边缘 AI 项目中的模块分层 |
| 课堂互动项目 | 根据关键点状态触发屏幕提示、蜂鸣器或 LED 效果 | 可扩展为互动教具或趣味识别实验 |
从工程角度看,建议保持"采集、检测、裁剪、推理、还原、显示"的分层思路。后续如果要增加串口通信、保存图片、按键切换模式、蜂鸣器提示或舵机跟随,只需要在检测结果和关键点结果之后增加动作逻辑,不需要重写摄像头和模型加载部分。
总结
本实验通过 CanMV K210 完成了基于 68 点模型的人脸关键点检测,核心内容包括摄像头初始化、LCD 显示、人脸检测模型加载、YOLO2 后处理、人脸框扩大、人脸区域裁剪、关键点模型推理、坐标还原、关键点绘制和 FPS 显示。程序先找到人脸,再定位 68 个关键点,完整展示了边缘 AI 视觉实验中"双模型串联"的典型流程。
这类实验非常适合作为 CanMV K210 AI 视觉课程中的进阶案例。它比单纯人脸检测多了裁剪、缩放、坐标映射和关键点绘制,也为后续表情分析、人脸对齐、疲劳检测、美颜特效、姿态估计和人脸跟踪云台提供了基础。掌握这条流程后,更多基于目标检测和局部模型推理的视觉项目,都可以按照类似思路继续扩展。