各地残疾人联合会网站建设,做网站接广告,岳阳网站建设制作,千库网网站Bubbliiiing 的 Retinaface rknn python推理分析
项目说明
使用的是Bubbliiiing的深度学习教程-Pytorch 搭建自己的Retinaface人脸检测平台的模型#xff0c;下面是项目的Bubbliiiing视频讲解地址以及源码地址和博客地址#xff1b;
作者的项目讲解视频#xff1a;https:…Bubbliiiing 的 Retinaface rknn python推理分析
项目说明
使用的是Bubbliiiing的深度学习教程-Pytorch 搭建自己的Retinaface人脸检测平台的模型下面是项目的Bubbliiiing视频讲解地址以及源码地址和博客地址
作者的项目讲解视频https://www.bilibili.com/video/BV1yK411K79y/?p1vd_source7fc00062d9cd78f73503c26f05fad664
项目源码地址https://github.com/bubbliiiing/retinaface-pytorch
作者博客地址https://blog.csdn.net/weixin_44791964/article/details/106872072
本文的内容相当于是对Bubbliiiing大佬的教程做一个简易的总结
RKNN模型输出
使用Netron观察此网络的输入和输出如下所示 这里模型的输入为1 x 3 x 640 x 640 NCHW
输出结果分为三个分别是框的回归预测结果output分类预测结果output1和人脸关键点的回归预测结果output2共计输出16800个先验框的三个相关信息
16800是什么?
首先RetinaFace在特征金字塔上有3个检测分支分别对应3个stride 32 16和8。
在stride32上一个feature map对应的原图的32 X 32的感受野即 stride32 对应的feature map的一个格子可以看到原图32 x 32的区域可以用来检测较大的区域人脸stride32 对应的feature map的大小为20 × 20这是因为640 / 32 20 stride32是最深的有效特征层其经过不断的卷积后小物体的特征便会消失从这一方面来看也是它更适合取检测大物体的原因同理stride16可用于中等人脸区域的检测stride16对应的feature map大小为40 X 40;stride8用于较小人脸区域的检测stride8对应的feature map大小为80 X 80
其次需要明确的是在retinafce模型上的每个像素点对应的原图位置上生成的anchor个数是两个
stride32对应的feature map的每个位置会在原图上生成两个anchor box即输入大小640 × 640尺寸的图像 stride32 对应的feature map大小为20 × 20 640 / 32那么在stride32对应的feature map上一共可以得到 20 × 20 × 2 800个anchorstride16对应的feature map大小为40 × 40640 / 16共生成40 × 40 × 2 3200个anchorstride8对应的feature map大小为80 x 80640 / 8共生成80 × 80 × 2 12800个anchor
因此3个尺寸总共可以生成800 3200 12800 16800个anchor
anchor的三个相关信息的解释 框的回归预测结果output用于对先验框进行调整获得预测框输出为1 x 16800 x 4 x 1,我们需要使用输出的四个参数对先验框进行调整来获得真实的人脸预测框。输出为num_anchors x 4 分类预测结果output1用于判断先验框内部是否包含物体用于代表每个先验框内部包含人脸的概率其有两个输出第一个输出为先验框内部为背景的概率第二个输出为先验框内部为人脸的概率输出为num_anchors x 2 人脸关键点的回归预测结果output2用于对先验框进行调整获得人脸关键点每一个人脸关键点需要两个调整参数一共有五个人脸关键点故需要10个参数去调整。输出为num_anchors x 10num_anchors x 5 x 2用于代表每个先验框的每个人脸关键点的调整。
模型推理前处理
模型前处理与rockchip的yolov5 rknn python推理分析前处理相同可以参考其讲解
参考作者源码的额外处理
在作者的源码中进行推理测试的时候出现了如下操作
image torch.from_numpy(preprocess_input(image).transpose(2, 0, 1)).unsqueeze(0).type(torch.FloatTensor)def preprocess_input(image):image - np.array((104, 117, 123),np.float32)return image我们将对img经过letterbox和色彩空间转换后也同样进行此操作
img img.astype(dtype np.float32)
img - np.array((104,117,123), np.float32)上面的操作会使得图像数据的分布会变得更加标准化
两个图像
在代码中出现了两个图像分别为img和or_imgimg经过一系列处理后最终用于模型推理而or_img用于画人脸框和人脸关键点信息
# img为模型输入
img cv2.imread(img_path)
img letterbox(img, (IMG_SIZE,IMG_SIZE))
img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# or_img用于画人脸框
or_img np.array(img, np.uint8)
or_img cv2.cvtColor(or_img, cv2.COLOR_RGB2BGR)img img.astype(dtype np.float32)
img - np.array((104,117,123), np.float32)模型推理
执行rknn模型推理inference的时间目前处于:0.04S-0.05S之间
outputs rknn.inference(inputs[img])模型输出的outputs为一个列表这个列别里面分别装了三个数组三个数组的维度为(1, 16800, 4, 1)、(1, 16800, 2, 1)、(1, 16800, 101)
使用numpy的.squeeze()方法去除数组中所有长度为1的维度结果维度如下所示
output_1 outputs[0].squeeze() # (16800, 4)
output_2 outputs[1].squeeze() # (16800, 2)
output_3 outputs[2].squeeze() # (16800, 10)模型后处理
Anchor先验框详解
作者关于Anchor的讲解视频https://www.bilibili.com/video/BV1yK411K79y?p8vd_source7fc00062d9cd78f73503c26f05fad664
先验框就是网络预先设定好的在图像上的框网络的预测结果只是对这些先验框进行判断并调整
前面我们讲过3个检测分支的对应的feature map的每个像素点对应的原图位置上生成两个anchor,同时每个检测分支的生成的anchor尺寸是不同的对于比较深的特征层stride32对应anchor的尺寸大,因为经过不断的卷积,小物体的特征会消失,它更适合取检测大物体它对应的两个anchor尺寸分别为512 × 512和256 × 256stride16对应的feature map生成的anchor尺寸大小分别为128 × 128和64 × 64stride8对应的feature map可以生成的anchor大小为32 × 32 和 16 × 16
在代码中的体现如下所示
# 计算生成先验框anchor
anchors Anchors(cfg_mnet, image_size(640, 640)).get_anchors()cfg_mnet{min_sizes: [[16, 32], [64, 128], [256, 512]], steps: [8, 16, 32],variance: [0.1, 0.2],
}# 得到anchor
class Anchors(object):def __init__(self, cfg, image_sizeNone):super(Anchors, self).__init__()# Anchors先验框基础的边长 self.min_sizes cfg[min_sizes]# 指向了三个有效特征层对输入进来的图片 长和宽压缩的倍数 对于比较浅的输入特征层 长和宽压缩了三次82^3 ,即长和宽变为了原来的1/8 对于最深的有效特征层 会对输入进去的图片进行5次长和宽的压缩 self.steps cfg[steps]# 输入进来的图片的尺寸 根据图片的大小生成先验框self.image_size image_size# 三个有效特征层高和宽self.feature_maps [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]def get_anchors(self): # 获得先验框anchors []for k, f in enumerate(self.feature_maps): # 首先对所有的特征层进行循环min_sizes self.min_sizes[k] # 取出每一个特征层对应的先验框# 对特征层的高和宽网格进行循环迭代for i, j in product(range(f[0]), range(f[1])):for min_size in min_sizes:# 将先验框映射到网格点上s_kx min_size / self.image_size[1]s_ky min_size / self.image_size[0]dense_cx [x * self.steps[k] / self.image_size[1] for x in [j 0.5]]dense_cy [y * self.steps[k] / self.image_size[0] for y in [i 0.5]]for cy, cx in product(dense_cy, dense_cx):# 把获得的先验框添加到anchors列表中anchors [cx, cy, s_kx, s_ky] # 先验框的形式是中心宽高的形式output_npnp.array(anchors).reshape(-1,4)return output_np在作者关于Anchor的讲解视频中作者在最深的有效特征层20 x 20的特征图上绘制了先验框其对应的先验框的尺寸为[256, 512]并以20 x 20特征图的左上角点为例其先验框如下所示 在获取先验框后retinaface的网络预测结果会判断先验框内部是否包含人脸还会对先验框进行调整获得最终的预测框还会对中心进行调整获得五个先验点
解码-先验框的调整
作者解码的讲解视频https://www.bilibili.com/video/BV1yK411K79y?p9vd_source7fc00062d9cd78f73503c26f05fad664
先验框的解码过程就是对先验框的中心和宽高进行调整获得调整后的先验框
# 人脸框解码
boxes decode(output_1, anchors, cfg_mnet[variance])
# 五个人脸关键点解码
landms decode_landm(output_3, anchors, cfg_mnet[variance])# 人脸框坐标解码
def decode(loc, priors, variances):boxes np.concatenate((priors[:, :2] loc[:, :2] * variances[0] * priors[:, 2:],priors[:, 2:] * np.exp(loc[:, 2:] * variances[1])), 1)boxes[:, :2] - boxes[:, 2:] / 2boxes[:, 2:] boxes[:, :2]return boxes# 人脸关键点解码
def decode_landm(pre, priors, variances):landms np.concatenate((priors[:, :2] pre[:, :2] * variances[0] * priors[:, 2:],priors[:, :2] pre[:, 2:4] * variances[0] * priors[:, 2:],priors[:, :2] pre[:, 4:6] * variances[0] * priors[:, 2:],priors[:, :2] pre[:, 6:8] * variances[0] * priors[:, 2:],priors[:, :2] pre[:, 8:10] * variances[0] * priors[:, 2:],), 1)return landmsdecode函数会对先验框进行调整获得最终的预测框
中心调整
priors[:, :2] loc[:, :2] * variances[0] * priors[:, 2:],取出网络回归结果loc中的前两个值乘上一个常数variances[0] (值)进行标准化然后将结果再乘上先验框的宽和高priors[:, 2:]之后再加上先验框的中心priors[:, :2]便获得了调整后的先验框中心即为预测框的中心点loc[:, :2] * variances[0] * priors[:, 2:]相当于先验框中心偏移的部分
宽高调整
priors[:, 2:] * np.exp(loc[:, 2:] * variances[1])取出网络回归结果loc中的后两个值乘上一个常数variances[1] (0.2)进行标准化然后将结果取一个指数再乘上先验框的宽和高priors[:, 2:]便获地了调整后的先验框的宽高
boxes[:, :2] - boxes[:, 2:] / 2
boxes[:, 2:] boxes[:, :2]最后将调整后的先验框形式转化为左上角坐标点和右下角坐标点的形式并返回
下面为作者解码的讲解视频中对先验框调整后的结果进行的演示 对比两边发现右边的图中的蓝色点即为两个先验框调整时两个anchor的中心点的调整情况同时发现右边的先验框的宽和高也发生了变化
decode_landm函数会对先验框的中心进行调整获得五个人脸关键点
priors[:, :2] pre[:, :2] * variances[0] * priors[:, 2:]人脸关键点的解码过程与先验框中心点调整的过程一样
取出相应序号的人脸关键点结果pre[:, :2]为人脸关键点中心点的预测结果
取出关键点的结果*variances[0] 归一化再乘上先验框的宽和高priors[:, 2:]最后再加上先验框的中心priors[:, :2]即可
先验框调整后再进行得分的筛选和非极大值抑制便得到了最终的结果
过滤无用的框
在进行过滤无用框的操作前需要对前面先验框的解码和人脸关键点的解码和人脸的概率合并到一起
conf output_2[:, 1:2] # 置信度序号为0的内容为先验框为背景的概率 序号为1的内容为先验框为人脸的概率#非极大抑制得到最终输出
boxs_conf np.concatenate((boxes, conf, landms), -1)合并后boxs_conf的维度为(16800, 15)
15的组成为
0-3预测框位置信息左上角坐标点和右下角坐标点4预测框包含人脸的概率5-14人脸的十个关键点坐标
过滤掉无用的框
boxs_conf filter_box(boxs_conf, 0.5, 0.45) # 0.5为置信度阈值conf_thres 0.45为非极大值抑制的iou阈值filter_box代码实现如下所示
def filter_box(org_box, conf_thres, iou_thres): #过滤掉无用的框conf org_box[..., 4] conf_thres #删除置信度小于conf_thres的BOXbox org_box[conf True] output []curr_cls_box np.array(box)curr_cls_box[:,:4]curr_cls_box[:,:4]*640curr_cls_box[:,5:]curr_cls_box[:,5:]*640curr_out_box pynms(curr_cls_box, iou_thres) #经过非极大抑制后输出的BOX下标for k in curr_out_box:output.append(curr_cls_box[k]) #利用下标取出非极大抑制后的BOXoutput np.array(output)return output首先根据包含人脸的概率进行筛选保留概率大于conf_thres的人脸框
conf org_box[..., 4] conf_thres
box org_box[conf True] 它返回16800个预测框的是否大于conf_thres的布尔值根据布尔值保留满足要求的预测框
将预测框的位置信息和关键点信息共计7个点,尺寸上乘上640
curr_cls_box[:,:4]curr_cls_box[:,:4]*640
curr_cls_box[:,5:]curr_cls_box[:,5:]*640进行非极大值抑制,返回经过非极大抑制后输出的剩余满足要求的预测框的下标将其保存到output中
curr_out_box pynms(curr_cls_box, iou_thres)
for k in curr_out_box:output.append(curr_cls_box[k]) #利用下标取出非极大抑制后的BOX最后返回剩余的预测框
非极大值抑制
非极大值抑制的代码与rockchip的yolov5 rknn python推理分析所讲述的非极大值抑制代码相同可以去参考这里不做重复讲述
def pynms(dets, thresh): 非极大抑制x1 dets[:, 0]y1 dets[:, 1]x2 dets[:, 2]y2 dets[:, 3]areas (y2 - y1) * (x2 - x1)scores dets[:, 4]keep []index scores.argsort()[::-1] #置信度从大到小排序的索引while index.size 0:i index[0]keep.append(i)# 计算相交面积# 求相交区域的左上角坐标x11 np.maximum(x1[i], x1[index[1:]]) y11 np.maximum(y1[i], y1[index[1:]])、# 求相交区域的右下角坐标x22 np.minimum(x2[i], x2[index[1:]])y22 np.minimum(y2[i], y2[index[1:]])# 当两个框不想交时x22 - x11或y22 - y11 为负数则将两框不相交时把相交面积置0w np.maximum(0, x22 - x11 ) h np.maximum(0, y22 - y11 ) # 计算相交面积overlaps w * h# 计算IOUious overlaps / (areas[i] areas[index[1:]] - overlaps)# IOU小于thresh的框保留下来idx np.where(ious thresh)[0] index index[idx 1]return keep结果绘制
经过置信度过滤和非极大值抑制之后得到预测框的信息为boxs_conf
boxs_conf filter_box(boxs_conf, 0.5, 0.45)在boxs_conf中前四个参数为人脸预测框框的位置信息左上角和右下角的坐标第五个参数为人脸的概率剩下的参数为人脸关键点的位置信息按顺序分别为
左眼、右眼、鼻子、左脸、右脸将boxs_conf传递给draw_img绘制结果
#画出人类框和5个人脸关键并保存图片
if boxs_conf is not None:draw_img(boxs_conf, or_img)draw_img函数的代码如下所示
# 画人脸框和5个关键点
def draw_img(boxes_conf_landms,old_image):for b in boxes_conf_landms:text {:.4f}.format(b[4])b list(map(int, b))# b[0]-b[3]为人脸框的坐标b[4]为得分cv2.rectangle(old_image, (b[0], b[1]), (b[2], b[3]), (0, 0, 255), 2) cx b[0]cy b[1] 12cv2.putText(old_image, text, (cx, cy),cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255))# b[5]-b[14]为人脸关键点的坐标cv2.circle(old_image, (b[5], b[6]), 1, (0, 0, 255), 4)cv2.circle(old_image, (b[7], b[8]), 1, (0, 255, 255), 4)cv2.circle(old_image, (b[9], b[10]), 1, (255, 0, 255), 4)cv2.circle(old_image, (b[11], b[12]), 1, (0, 255, 0), 4)cv2.circle(old_image, (b[13], b[14]), 1, (255, 0, 0), 4)return old_image结果展示如下所示