硬件平台:K210 Sipeed Maix Dock
软件平台:maixpy 、kmodelv2
实现功能 检测并识别人脸
需要用到三个模型,
task_fd = kpu.load(0x300000) #从flash 0 0x300000 加载人脸检测模型
task_ld = kpu.load(0x400000) #从flash 0 0x400000 加载人脸五点关键点检测模型
task_fe = kpu.load(0x500000) #从flash 0 0x500000 加载人脸196维特征值模型
脚本如下:
import sensor
import image
import lcd
import KPU as kpu
import time
from Maix import FPIOA, GPIO
import gc
from fpioa_manager import fm
#from board import board_info
import utime
#import 相关库
task_fd = kpu.load(0x300000) #从flash 0 0x300000 加载人脸检测模型
task_ld = kpu.load(0x400000) #从flash 0 0x400000 加载人脸五点关键点检测模型
task_fe = kpu.load(0x500000) #从flash 0 0x500000 加载人脸196维特征值模型
clock = time.clock() #初始化系统时钟,计算帧率
fm.register(16, fm.fpioa.GPIOHS0) #设置 boot按键 的io
key_gpio = GPIO(GPIO.GPIOHS0, GPIO.IN)
start_processing = False
BOUNCE_PROTECTION = 50
def set_key_state(*_): #按键中断
global start_processing
start_processing = True
utime.sleep_ms(BOUNCE_PROTECTION)
key_gpio.irq(set_key_state, GPIO.IRQ_RISING, GPIO.WAKEUP_NOT_SUPPORT)
lcd.init() #初始化 lcd
sensor.reset()#初始化sensor摄像头
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_hmirror(1) #设置摄像头镜像
sensor.set_vflip(1) #设置摄像头翻转
sensor.run(1) #试能摄像头
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437,
6.92275, 6.718375, 9.01025) # 人脸检测锚
dst_point = [(44, 59), (84, 59), (64, 82), (47, 105),
(81, 105)] # 标准人脸关键点位置 标准正脸的5关键点坐标 分别为 左眼 右眼 鼻子 左嘴角 右嘴角
a = kpu.init_yolo2(task_fd, 0.5, 0.3, 5, anchor) #初始化人脸检测模型
img_lcd = image.Image() #设置显示buf
img_face = image.Image(size=(128, 128))#设置 128*128 人脸图片buf
a = img_face.pix_to_ai() #将图片转换为kpu接受的格式
record_ftr = [] #空列表 用于存储当前196维特征值
record_ftrs = [] #空列表 用于存储按键记录下人脸特征,可以将特征以txt等文件形式保存到sd卡后,读取到此列表,即可实现人脸断电存储。
names = ['Mr.1', 'Mr.2', 'Mr.3', 'Mr.4', 'Mr.5',
'Mr.6', 'Mr.7', 'Mr.8', 'Mr.9', 'Mr.10'] # # 人名标签,与上面列表特征值一一对应。
ACCURACY = 85 # 设定分数标准 人脸阈值
while (1):
img = sensor.snapshot()
clock.tick()
code = kpu.run_yolo2(task_fd, img) # 运行人脸检测模型,获取人脸坐标位置
if code: #如果检测到人脸
for i in code: #迭代坐标框
# Cut face and resize to 128x128
a = img.draw_rectangle(i.rect()) #在屏幕显示人脸方框
face_cut = img.cut(i.x(), i.y(), i.w(), i.h()) #裁剪人脸部分照片到 face_cut
face_cut_128 = face_cut.resize(128, 128) #将裁剪出的人脸照片缩放到 128*128 像素
a = face_cut_128.pix_to_ai() #将裁剪出的照片转换为kpu接受的格式
# a = img.draw_image(face_cut_128, (0,0))
# Landmark for face 5 points
fmap = kpu.forward(task_ld, face_cut_128) #运行人脸5点关键点模型
plist = fmap[:] #获取关键点预测结果
le = (i.x() + int(plist[0] * i.w() - 10), i.y() + int(plist[1] * i.h())) #计算左眼位置,这里在w方向-10 用来补偿模型转换带来的精度损失
re = (i.x() + int(plist[2] * i.w()), i.y() + int(plist[3] * i.h())) #计算右眼位置
nose = (i.x() + int(plist[4] * i.w()), i.y() + int(plist[5] * i.h())) #计算鼻子位置
lm = (i.x() + int(plist[6] * i.w()), i.y() + int(plist[7] * i.h())) #计算左嘴角位置
rm = (i.x() + int(plist[8] * i.w()), i.y() + int(plist[9] * i.h())) #计算右嘴角位置
#在相应位置处画小圈圈
a = img.draw_circle(le[0], le[1], 4)
a = img.draw_circle(re[0], re[1], 4)
a = img.draw_circle(nose[0], nose[1], 4)
a = img.draw_circle(lm[0], lm[1], 4)
a = img.draw_circle(rm[0], rm[1], 4)
#在相应位置处画小圈圈
# align face to standard position 将面对齐到标准位置
src_point = [le, re, nose, lm, rm] #图片中五点坐标的位置
T = image.get_affine_transform(src_point, dst_point) #根据获得的5点坐标和标准正脸坐标获取仿射变换矩阵
a = image.warp_affine_ai(img, img_face, T) #对原始图片人脸图片进行仿射变换,变换为正脸图像
a = img_face.ai_to_pix() #将图片转换为kpu接受的格式
# a = img.draw_image(img_face, (128,0))
del (face_cut_128) #释放裁剪人脸部分照片
# calculate face feature vector 计算人脸特征向量
fmap = kpu.forward(task_fe, img_face) #计算正脸图片的196维特征值
feature = kpu.face_encode(fmap[:]) #获得计算结果
reg_flag = False
scores = [] #存储特征比对分数
for j in range(len(record_ftrs)): #迭代已存特征值
score = kpu.face_compare(record_ftrs[j], feature) #计算当前人脸特征值与已存特征值的分数
scores.append(score) #添加分数总表
max_score = 0
index = 0
for k in range(len(scores)): #迭代所有比对分数,找到最大分数和索引值
if max_score < scores[k]:
max_score = scores[k]
index = k
if max_score > ACCURACY: #如果最大分数大于85, 可以被认定为同一个人
a = img.draw_string(i.x(), i.y(), ("%s :%2.1f" % (
names[index], max_score)), color=(0, 255, 0), scale=2) # 显示人名 与 分数
else:
a = img.draw_string(i.x(), i.y(), ("X :%2.1f" % (
max_score)), color=(255, 0, 0), scale=2) #显示未知 与 分数
if start_processing:
record_ftr = feature
record_ftrs.append(record_ftr)
start_processing = False
break
fps = clock.fps() #计算帧率
print("%2.1f fps" % fps) #打印帧率
a = lcd.display(img) #刷屏显示
gc.collect()
# kpu.memtest()
# a = kpu.deinit(task_fe)
# a = kpu.deinit(task_ld)
# a = kpu.deinit(task_fd)
上面这个示例脚本 在运行时每次都需要重新录入人脸的特征,将进行拍照。
拓展:
从flash中读取人脸特征:
#人脸检测的阈值
ACCURACY = 85
#开机时从flash中读取人脸特征数据
try:
with open("/flash/ftrs.dat",'rb') as f:
while 1:
l = f.read(4)
if not l:
break
length = stc.unpack('i', l)[0]
s = bytearray(f.read(length))
record_ftrs.append(s)
except :
f = open("/flash/ftrs.dat",'w')
f.close()
print('find ' + str(len(record_ftrs)) + ' faces')
while (1):
img = sensor.snapshot()
#运行人脸检测模型
code = kpu.run_yolo2(task_fd, img)
#如果存在人脸
if code:
也可以改成从sd卡中读取人脸特征,将flash改为sd就可以了:
with open("/sd/ftrs.dat",'rb') as f:
passf = open("/sd/ftrs.dat",'w')
老规矩 用kflash
直接运行 会出现以下报错,原因是内存不足
解决方法: 烧录>1.5MB的固件,更改gc大小
内存不够 (MemoryError: Out of normal MicroPython Heap Memory!)
解决方法参考:https://neucrack.com/p/325
k210 有 6MiB 通用内存, 需要用到内存的有固件(K210 是一次性将所有代码加载到内存的….),一些功能所需比如摄像头缓冲区等,还有存放模型, 另外有 2MiB 给 KPU 专用的内存(如果使用 KPU 的话)
如果你不需要使用 KPU, 想给更多的内存给 CPU使用, 则最多可以用 8MiB 连续的内存, 方法是不初始化 KPU (时钟), 则可以有连续的 8MiB通用内存使用. 当然,在本文必须要用到 KPU, 所以只作为知识补充
所以需要总内存 = 固件大小 + 静态变量申请内存 + 其它功能动态申请(比如摄像头 屏幕显示申请的缓冲区, RGB888 缓冲区, Micropython 的GC 内存块) + 模型使用 + 剩余内存, 模型使用的内存在使用nncase转换的时候会有输出, V3 输出了 main memory usage, V4 输出了working memory usage.
另外,因为 KPU 专用内存只有 2MiB, 会用来放输入和输出层的数据, 所以在设计模型时, 一个层的输入和输出层, 以及上一层的输出层和下一层的输入层的大小和必须小于 2MiB
MaixPy 固件内有两个内存池,一个是系统内存,一个是 GC 内存,前者主要用来给模型还有系统内的一些功能使用,包括摄像头和屏幕的缓冲区等都来自这里;后者是 maixpy 解析器层面的内存,可以给代码的变量使用。
GC 剩余内存可以用下面的代码来打印
import gc
print(gc.mem_free())
GC 的总内存大小可以通过下面的代码来设置, GC 的大了, 系统的就小了,如果模型大这里就不要设置太大了。 另外注意!!要重启才生效。
from Maix import utils
import machine
old = utils.gc_heap_size()
print(old)
new = 512*1024
utils.gc_heap_size(new)
machine.reset()
另外,可以用kpu.memtest()查看一下大致上还剩多少,这个现实的系统内存不一定准确,只作参考
所以,解决内存不足有以下几种方法:
最基础的方法就是减少内存的使用,比如全局变量,不使用了尽量删除(通过del 变量名),删除之后还可以手动回收 GC 内存(通过gc.collect())。图片分辨率也可以尽量不要用太大(一般QVGA)
另一个方法就是压缩固件体积,通过裁减功能来减少内存占用,这个在前面固件升级部分有说明,使用在线编译定制固件,或者自己本机编译,方法见这里
还有一个方法就是设置 GC 内存池总大小,如果 GC 内存不够,就设置大点;如果系统内存不够用比如模型加载不了,在够用的范围内减小 GC 的大小, 留给加载模型使用
另外,如果模型太大,可以使用kpu.load_flash()函数来加载模型(只支持kmodel):
这会在需要模型时实时从flash读取内容,这样就可以装载大模型了,效率会低一点,帧率会有所降低(原理有兴趣可以见另一篇文章K210 从flash实时加载大模型)。
使用方法见这里,注意,模型需要先用脚本转一下大小端,别漏了!!
另外, 如果你在操作 image时或者lcd画图时遇到这个问题,可以合理利用lcd的display(img, oft=(x,y))的oft参数来实现在lcd指定区域画图,而不是画整副图。
比如,有个img的大小为200x200,现在需要画到屏幕,周围填充颜色,但是屏幕是320x240,可以先创建一个320x240的image填充颜色,然后拷贝这个200x200的图到这张图上,然后lcd.dispaly这张320x240的图,但是会需要浪费一个320x240图像的内存。可以直接lcd.clear(color)一次,或者lcd.fill_rectangle(0, 0, 320, 20)这样填充四周的颜色, 这个操作只需要一次,然后不断调用lcd.display(img_200x200, oft=(60, 20))刷新中间的区域的图像就好了,周围会保持最开始画的颜色。 画图片同理。
这次用到了新的运行方法——使用 putty.exe
打开后按开发板的reset按钮,可以看到终端打印了启动信息
可以直接运行python代码