人脸检测
视觉应用最为广泛的领域可能就是人脸识别,不论是家用的门锁上安装的摄像头或者是小区、公司中的门禁机,甚至是机场等公共场所中用于追犯人的人脸检测系统,都是人脸识别的实际应用。而人脸识别的第一步就是要准确的在图像中检测到人脸。这也是人脸识别的第一步,人脸检测。
经过查询,最常用也最基础的人脸检测算法是Haar算子特征检测。但是K230将AI开发的过程进行了非常多的简化。如下图所示,是嘉楠官方提供的一个AI开发的框架,我们需要做的仅是完成AI部分中的下面五个步骤,分别是配置前处理、前处理、推理、后处理以及显示结果。后面的AI的代码开发也基本上会围绕着这些内容开展。通常是创建一个类,然后在类中分别定义几个方法,实现下面的几个步骤,然后实例这个类,并在合适的时候调用对应的方法即可实现对应的功能。
为了方便我们开发AI相关的内容,官方提供给我们几个封装好的API接口,分别是PineLine、Ai2d、AIBase。他们所做的事情是完成了图像的采集和显示、预处理的相关接口以及模型推理的相关接口。使用上述的接口来实现后面的AI的视觉例程。
Python中有类的概念,通常一个类的第一个方法都是init,这个方法会在实例化的时候自动进行初始化,同时将传入的参数赋值给这个实例化,如果没有这个init的函数的话,就像是它短暂的存在了一段时间,但是这个东西本身是不存在的一样。所以加入init函数还是很重要的。在后面的AI视觉中的Python知识较为复杂一些,所以有必要同时补充一些Python的基础知识。
下面我们直接参考官方提供的代码,进行分析。
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
import ujson
from media.media import *
from time import *
import nncase_runtime as nn
import ulab.numpy as np
import time
import utime
import image
import random
import gc
import sys
import aidemo
# 自定义人脸检测类,继承自AIBase基类
class FaceDetectionApp(AIBase):
def __init__(self, kmodel_path, model_input_size, anchors, confidence_threshold=0.5, nms_threshold=0.2, rgb888p_size=[224,224], display_size=[1920,1080], debug_mode=0):
super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode) # 调用基类的构造函数
self.kmodel_path = kmodel_path # 模型文件路径
self.model_input_size = model_input_size # 模型输入分辨率
self.confidence_threshold = confidence_threshold # 置信度阈值
self.nms_threshold = nms_threshold # NMS(非极大值抑制)阈值
self.anchors = anchors # 锚点数据,用于目标检测
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 显示分辨率,并对宽度进行16的对齐
self.debug_mode = debug_mode # 是否开启调试模式
self.ai2d = Ai2d(debug_mode) # 实例化Ai2d,用于实现模型预处理
self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 设置Ai2d的输入输出格式和类型
# 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/libs/AI2D.py查看
def config_preprocess(self, input_image_size=None):
with ScopedTiming("set preprocess config", self.debug_mode > 0): # 计时器,如果debug_mode大于0则开启
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size # 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
top, bottom, left, right = self.get_padding_param() # 获取padding参数
self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123]) # 填充边缘
self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 缩放图像
self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]]) # 构建预处理流程
# 自定义当前任务的后处理,results是模型输出array列表,这里使用了aidemo库的face_det_post_process接口
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
post_ret = aidemo.face_det_post_process(self.confidence_threshold, self.nms_threshold, self.model_input_size[1], self.anchors, self.rgb888p_size, results)
if len(post_ret) == 0:
return post_ret
else:
return post_ret[0]
# 绘制检测结果到画面上
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
if dets:
pl.osd_img.clear() # 清除OSD图像
for det in dets:
# 将检测框的坐标转换为显示分辨率下的坐标
x, y, w, h = map(lambda x: int(round(x, 0)), det[:4])
x = x * self.display_size[0] // self.rgb888p_size[0]
y = y * self.display_size[1] // self.rgb888p_size[1]
w = w * self.display_size[0] // self.rgb888p_size[0]
h = h * self.display_size[1] // self.rgb888p_size[1]
pl.osd_img.draw_rectangle(x, y, w, h, color=(255, 255, 0, 255), thickness=2) # 绘制矩形框
else:
pl.osd_img.clear()
# 获取padding参数
def get_padding_param(self):
dst_w = self.model_input_size[0] # 模型输入宽度
dst_h = self.model_input_size[1] # 模型输入高度
ratio_w = dst_w / self.rgb888p_size[0] # 宽度缩放比例
ratio_h = dst_h / self.rgb888p_size[1] # 高度缩放比例
ratio = min(ratio_w, ratio_h) # 取较小的缩放比例
new_w = int(ratio * self.rgb888p_size[0]) # 新宽度
new_h = int(ratio * self.rgb888p_size[1]) # 新高度
dw = (dst_w - new_w) / 2 # 宽度差
dh = (dst_h - new_h) / 2 # 高度差
top = int(round(0))
bottom = int(round(dh * 2 + 0.1))
left = int(round(0))
right = int(round(dw * 2 - 0.1))
return top, bottom, left, right
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径和其他参数
kmodel_path = "/sdcard/app/tests/kmodel/face_detection_320.kmodel"
# 其它参数
confidence_threshold = 0.5
nms_threshold = 0.2
anchor_len = 4200
det_dim = 4
anchors_path = "/sdcard/app/tests/utils/prior_data_320.bin"
anchors = np.fromfile(anchors_path, dtype=np.float)
anchors = anchors.reshape((anchor_len, det_dim))
rgb888p_size = [1920, 1080]
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义人脸检测实例
face_det = FaceDetectionApp(kmodel_path, model_input_size=[320, 320], anchors=anchors, confidence_threshold=confidence_threshold, nms_threshold=nms_threshold, rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
face_det.config_preprocess() # 配置预处理
clock = time.clock()
try:
while True:
os.exitpoint() # 检查是否有退出信号
clock.tick()
img = pl.get_frame() # 获取当前帧数据
res = face_det.run(img) # 推理当前帧
# 当检测到人脸时,打印结果
if res:
print(res)
face_det.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
print(clock.fps()) #打印帧率
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
face_det.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
首先是在代码中引入了大量的库文件,包括了AI相关的库和一些基础的库文件。
然后紧接着就是定义了一个人脸检测的类。在这个类中一共创建了五个方法:初始化、配置预处理、后处理、显示结果、获取padding参数。
方法-初始化:在这个方法中,主要是利用了基类的构造函数进行了构造,然后将初始化的参数传入到创建的实例中,这些参数包括了模型的路径、模型的分辨率等等。
方法-配置预处理:在这个方法中主要就是调用了Ai2d相关的一些函数,进行了诸如crop/shift/pad/resize/affine等的操作,这些操作就是对采集的图像进行一些裁剪、平移等等。在这个实验中主要就是进行了pad和resize的操作。这个方法中调用了后面要定义的方法get_padding_param来获取想要pad的一些参数,然后使用这些参数进行pad。并完成resize缩放图像到一个合适的尺寸后,进行构建预处理,这样就完成了图像的一些预处理的工作。
方法-后处理:这个方法中主要就是调用了aidemo库中的一个人脸检测的接口,传入一些阈值信息进行检测,若有相关的数据,则返回数据,否则就返回空的。
方法-显示结果:这个方法就是去检测有没有检测到的人脸对象,如果有,就开始清除当前的OSD,即屏幕上显示的东西,然后将返回的数据转成在屏幕上显示的对应的数据,并调用pl即PineLine中关于绘制图像相关的方法,将检测的结果标识在图像上进行显示。
方法-获取padding参数:这个方法主要是为了给配置预处理中的pad提供一些参数,这里会获取到模型输入的高度和宽度然后对高度宽度进行缩放和处理后,将这个整理好的数据传给预处理过程中,实现一个pad的操作。
完成人脸检测类的创建后,下面就要开始实例,并调用对应的AI相关的操作,开始进行识别和检测了。
首先是给一些参数一些确定的数值,比如说模型的路径、设定的阈值、显示的尺寸等等。
然后首先是实例一个PineLine,叫做pl,并进行一个创建。这样就确保了图像的获取、显示、画图可以正常实现。
紧接着就是实例一个人脸检测的类,这里需要传入init方法中的参数,然后进行预处理的配置,这个预处理的配置实际上使用了两个方法,就是第二个和第五个方法。
然后开始进行图像的获取和推理。
首先使用pl中的方法get frame获取一个图像帧,传入到img中,并将img传入到之前实例的人脸检测类中,这里使用的方法是run,很明显没有定义这个方法,是因为这个run是继承自AIBase父类的。开始进行推理当前帧。
当检测到人脸的时候,就会打印出结果。然后执行一下人脸检测类中的第四个方法,就是绘制结果。同时调用pl将结果进行显示。
这样就完成了一个AI的人脸检测的过程。
下面我们来演示一下效果。在这里使用手机找了一张图片,可以看到准确的识别出了照片上的三个人,并使用框进行了准确的框选。
后面,我们就可以在这个的基础上进行更多的AI视觉相关的开发和学习。
手掌检测
对于手掌的检测和识别,可以实现手势的识别,从而用来使用手势实现一些控制的操作。
手掌检测和人脸检测的识别其实是大致类似的一个流程。在手掌检测程序中定义一个新的类,在里面实现前处理、配置等相关的函数后,再在主函数中实例类,然后初始化函数进行操作即可。
和人脸检测相比,需要更改的首先就是模型,不同的识别要使用不同的模型,这里我使用了官方提供的模型。
然后还有一个参数,叫做anchors,中文叫做锚点。其含义是存储了一组预设的边框,在使用的时候,会优先使用anchors中的参数进行边框绘制,在其基础上进行一些偏移,所以anchors中的参数越多所能检测到的情况也会越多。但是其参数的设置也要合理,要大概根据手掌的形状分配这个边框的长和宽。例程中提供的参数有这些[26,27, 53,52, 75,71, 80,99, 106,82, 99,134, 140,113, 161,172, 245,276]。当减少到仅剩26,27这组参数的时候,就会发现,只能在特定的一个角度,很困难的识别到手的存在。因此,这个列表中的参数多一些,会增加识别的概率,但是同时可能也会增加误检测的概率。
下面是手掌检测的主要代码部分。
if __name__=="__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 模型路径
kmodel_path="/sdcard/app/tests/kmodel/hand_det.kmodel"
# 其它参数设置
confidence_threshold = 0.2
nms_threshold = 0.5
rgb888p_size=[1920,1080]
labels = ["hand"]
anchors = [26,27, 53,52, 75,71, 80,99, 106,82, 99,134, 140,113, 161,172, 245,276] #anchor设置
# 初始化PipeLine
pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode)
pl.create()
# 初始化自定义手掌检测实例
hand_det=HandDetectionApp(kmodel_path,model_input_size=[512,512],labels=labels,anchors=anchors,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,nms_option=False,strides=[8,16,32],rgb888p_size=rgb888p_size,display_size=display_size,debug_mode=0)
hand_det.config_preprocess()
clock = time.clock()
try:
while True:
os.exitpoint() # 检查是否有退出信号
clock.tick()
img=pl.get_frame() # 获取当前帧数据
res=hand_det.run(img) # 推理当前帧
hand_det.draw_result(pl,res) # 绘制结果到PipeLine的osd图像
print(res) # 打印结果
pl.show_image() # 显示当前的绘制结果
gc.collect() # 垃圾回收
print(clock.fps()) #打印帧率
except Exception as e:
sys.print_exception(e)
finally:
hand_det.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
下面是演示效果。
手势判断
实现手掌识别后,就要对每个手指进行划线。如下图所示,来获取五个手指头的弯曲的角度,并将这个值可以返回到一个列表中。
通过下面的代码,实现将角度返回到一个列表中。
for i in range(5):
angle = self.hk_vector_2d_angle([(results[0]-results[i*8+4]), (results[1]-results[i*8+5])],[(results[i*8+6]-results[i*8+8]),(results[i*8+7]-results[i*8+9])])
angle_list.append(angle)
然后对angle_list列表中的每一位进行单独的角度判断,从而可以实现手势的识别。
从0-5对应的依次是拇指、食指、中指、无名指、小拇指。
参数thr_angle为弯转角度阈值,即超过这个数值就认为手指发生了弯曲。
参数thr_angle_thumb为拇指弯转角度阈值,即超过这个数值就认为拇指发生了弯曲。
参数thr_angle_s为放松角度阈值,即小于这个数值就认为手指处于伸展状态。
参数gesture_str 为手势字符串,用来存储手势的名称,返回进行显示。
下面就针对gun和love手势进行一下分析。
gun手势,大拇指和食指处于伸展放松状态,剩余三个手指处于弯曲状态。因此大拇指和食指的条件应该是小于thr_angle_s,其余三指为大于thr_angle。可以看到代码中列表的前两位判断条件是小于thr_angle_s,后三位判断条件是大于thr_angle。
love手势,大拇指、食指和小拇指处于伸展放松状态,剩余两个手指处于弯曲状态。因此大拇指、食指和小拇指的条件应该是小于thr_angle_s,其余两指为大于thr_angle。可以看到代码中列表的前两位和最后一位判断条件是小于thr_angle_s,中间两位判断条件是大于thr_angle。
thr_angle,thr_angle_thumb,thr_angle_s,gesture_str = 65.,53.,49.,None
if 65535. not in angle_list:
if (angle_list[0]>thr_angle_thumb) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "fist"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "five"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "gun"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]<thr_angle_s):
gesture_str = "love"
elif (angle_list[0]>5) and (angle_list[1]<thr_angle_s) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "one"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]<thr_angle_s):
gesture_str = "six"
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]>thr_angle):
gesture_str = "three"
elif (angle_list[0]<thr_angle_s) and (angle_list[1]>thr_angle) and (angle_list[2]>thr_angle) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "thumbUp"
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]>thr_angle) and (angle_list[4]>thr_angle):
gesture_str = "yeah"
return gesture_str
下面我们尝试补充一个OK的手势。OK的手势为拇指和食指弯曲,剩余三指处于放松的状态。但是经过测试后,发现,拇指需要弯曲角度很大才能实现识别。因此更改一下设定的thr_angle_thumb阈值,通过将这个阈值改小一点,可以实现更好的识别。
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]>thr_angle) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "OK"
因为是凭借角度识别手势,所以对于手的翻转、旋转可能会出现不太准确的情况。下面再添加一个Four的手势。Four手势,拇指弯曲,剩余四指伸展放松。
elif (angle_list[0]>thr_angle_thumb) and (angle_list[1]<thr_angle_s) and (angle_list[2]<thr_angle_s) and (angle_list[3]<thr_angle_s) and (angle_list[4]<thr_angle_s):
gesture_str = "four"