基于PIPNet的人脸106关键点检测

做美颜需要使用到人脸关键点,所以整理了一下最近的想法。

按模型结构分类:

1.Top-Down: 分为两个步骤,首先,对于原始输入图片做目标检测,比如做人脸检测,将人脸区域抠出,单独送进关键点检测模型,最终输出关键点的坐标。

2.Bottom-up: 只有一个模型,不需要进行目标检测,直接将原始图像送进关键点检测模型,即可输出所有关键点。如果有多个目标,则无法区分哪些点属于目标1,哪些点属于目标2。因此有多个目标时,还需要在后面接一个聚类的模块,将各个目标的关键点进行区分。

按回归/热力图分类:

1.回归(Regression): 优点:训练和推理速度快;是端到端全微分模型;缺点:空间泛化能力丢失,被reshape成一维向量,严重依赖于训练数据的分布,容易过拟合。

2.热力图(Heatmap): 输出特征图大,空间泛化能力强,精度高;缺点:训练和推理慢;不是端到端全微分模型;结果输出是整数(全连接输出是浮点数),会丢失精度,存在理论误差下界

个人选择使用的是Pixel-in-Pixel Net: Towards Efficient Facial Landmark Detection in the Wild

论文地址:https://arxiv.org/abs/2003.03771

开源地址:jhb86253817/PIPNet: Efficient facial landmark detector (github.com)

亮点:PIP+NRM邻居节点回归模块(NRM, Neighbor Regression Module)。但NRM其实就是让原本每一格预测一个关键点,变成了预测多个关键点,也就是说,一块特征除了预测自己那个点外,还要预测周围最近的关键点,对于邻近点的定义,是从平均脸型上用欧氏距离计算得到的。

这个算法经过与PFLD还有RTMPose对比感觉要好一些(个人数据集上)。

但是代码中并没有给106人脸关键点的预处理等脚本,所以根据LaPa格式自己写了个,也可以在preprocess中自己复写。我原始的label是txt格式的,每个txt名与图片名一样,里面是106*2,每行是x y坐标(没有归一化),

import cv2
import os
import numpy as np
folder_path = r'data/hyy_image'for filename in os.listdir(folder_path):if filename.endswith('.png'):new_filename = filename.replace('.png', '.jpg')os.rename(os.path.join(folder_path, filename), os.path.join(folder_path, new_filename))
def resize_image_with_keypoints(image_path, label_path, target_width=256, target_height=256):img = cv2.imread(image_path)#print(img)h, w = img.shape[:2]# 计算缩放比例c = max(w,h)#ratio_w = target_width / w#ratio_h = target_height / hratio = target_width/c# 等比例缩放图像img_resized = cv2.resize(img, (int(w * ratio), int(h * ratio)), interpolation=cv2.INTER_AREA)print(img_resized.shape)# 读取标签文件with open(label_path, 'r') as label_file:lines = label_file.readlines()keypoints = []for line in lines:x, y = map(float, line.strip().split())keypoints.append((x * ratio, y * ratio))  # 调整关键点坐标# 创建新的空白图像result = np.zeros((target_height, target_width, 3), dtype=np.uint8)x = (target_width - img_resized.shape[1]) // 2y = (target_height - img_resized.shape[0]) // 2# 将调整后的图像粘贴到新图像中心result[y:y + img_resized.shape[0], x:x + img_resized.shape[1]] = img_resized# 归一化关键点坐标normalized_keypoints = []for kp in keypoints:# 考虑了填充操作的影响,对关键点坐标进行调整normalized_x = (kp[0]  + (target_width - img_resized.shape[1]) / 2) / target_widthnormalized_y = (kp[1]  + (target_height - img_resized.shape[0]) / 2) / target_heightnormalized_keypoints.append((normalized_x, normalized_y))return result, normalized_keypoints# 源图片路径
folder_A = r'data/xxx_image'
# 源label路径
folder_B = r'data/xxx_txt'
#resize后保存图片路径
folder_C = r'data/xxx/images_train'
# 生成 train.txt 的路径
train_txt_path = r'data/xxx/train.txt'# 遍历 A 文件夹中的图片
with open(train_txt_path, 'w') as train_txt:for filename in os.listdir(folder_B):if filename.endswith('.txt'):print(filename)label_path = os.path.join(folder_B, filename)image_path = os.path.join(folder_A, filename.replace('.txt', '.jpg'))filename_image = os.path.join(folder_C,filename.replace('.txt', '.jpg'))print(filename_image)print(image_path)#label_path = os.path.join(folder_B, filename.replace('.jpg', '.txt'))if os.path.exists(label_path):# 调整图片和关键点坐标resized_img, normalized_keypoints = resize_image_with_keypoints(image_path, label_path)# 保存调整后的图片cv2.imwrite(f'{filename_image}', resized_img)# 写入 train.txttrain_txt.write(filename.replace('.txt', '.jpg'))for kp in normalized_keypoints:train_txt.write(f' {kp[0]} {kp[1]}')train_txt.write('\n')

注意如果不在预处理中进行等比例缩放的话,train的时候会直接resize成256*256(或其他自定义尺寸),此时会将图片压缩或拉伸,所以我这里使用了等比例缩放图片,并将关键点坐标也对应处理后,转成train.py需要的train.txt。

这个文件中每一行是一张图片的label信息,第一个值为图片在images_train文件夹下的名字,后续106*2个值为归一化的关键点坐标。如4364054413_3.jpg 0.2863247863247863 0.3805309734513274 ......

哦对了,还需要生成meanface.txt文件:

import os
import numpy as npdef gen_meanface(root_folder, data_name):with open(os.path.join(root_folder, data_name, 'train.txt'), 'r') as f:annos = f.readlines()annos = [x.strip().split()[1:] for x in annos]annos = [[float(x) for x in anno] for anno in annos]annos = np.array(annos)meanface = np.mean(annos, axis=0)meanface = meanface.tolist()meanface = [str(x) for x in meanface]with open(os.path.join(root_folder, data_name, 'meanface.txt'), 'w') as f:f.write(' '.join(meanface))data_name = 'xxx'
root_folder = 'data'
gen_meanface(root_folder, data_name)

然后在network.py中,可以对模型进行自定义更改,比如他原本的Pip_mbnetv2直接用mbnetv2作为骨干网络提取特征,再在head中使用五个卷积做热力图和回归还有近邻回归,但mbnetv2的最后一层输出960维,而我是106关键点,所以卷积输入960,输出106,感觉没必要,所以我想改一下,使用320或640的backbone输出:

class Pip_mbnetv2(nn.Module):def __init__(self, mbnet, num_nb, num_lms=68, input_size=256, net_stride=32):super(Pip_mbnetv2, self).__init__()self.num_nb = num_nbself.num_lms = num_lmsself.input_size = input_sizeself.net_stride = net_strideself.features = mbnet.featuresself.sigmoid = nn.Sigmoid()new_conv2d = nn.Conv2d(320, 640, kernel_size=(1, 1), stride=(1, 1), bias=False)new_bn = nn.BatchNorm2d(640, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)self.features[18][0] = new_conv2dself.features[18][1] = new_bnself.cls_layer = nn.Conv2d(640, num_lms, kernel_size=1, stride=int(net_stride/32), padding=0)self.x_layer = nn.Conv2d(640, num_lms, kernel_size=1, stride=int(net_stride/32), padding=0)self.y_layer = nn.Conv2d(640, num_lms, kernel_size=1, stride=int(net_stride/32), padding=0)self.nb_x_layer = nn.Conv2d(640, num_nb*num_lms, kernel_size=1, stride=int(net_stride/32), padding=0)self.nb_y_layer = nn.Conv2d(640, num_nb*num_lms, kernel_size=1, stride=int(net_stride/32), padding=0)nn.init.normal_(self.cls_layer.weight, std=0.001)if self.cls_layer.bias is not None:nn.init.constant_(self.cls_layer.bias, 0)nn.init.normal_(self.x_layer.weight, std=0.001)if self.x_layer.bias is not None:nn.init.constant_(self.x_layer.bias, 0)nn.init.normal_(self.y_layer.weight, std=0.001)if self.y_layer.bias is not None:nn.init.constant_(self.y_layer.bias, 0)nn.init.normal_(self.nb_x_layer.weight, std=0.001)if self.nb_x_layer.bias is not None:nn.init.constant_(self.nb_x_layer.bias, 0)nn.init.normal_(self.nb_y_layer.weight, std=0.001)if self.nb_y_layer.bias is not None:nn.init.constant_(self.nb_y_layer.bias, 0)def forward(self, x):#print(self.features)x = self.features(x)#print('x.shape',x.shape)x1 = self.cls_layer(x)x2 = self.x_layer(x)x3 = self.y_layer(x)x4 = self.nb_x_layer(x)x5 = self.nb_y_layer(x)return x1, x2, x3, x4, x5

这里不直接拿mbnetv2的960维输出再加卷积变成640,而是直接对mbnetv2的features[18]里的内容进行更改,这样减少了该层和多余的计算量,还避免了特征冗余,如果想使用320的backbone输出的话,可以这样:self.features[18][0] = nn.Identity()

然后stride这里根据config的改动而动态变化,不然config的stride传不到这里来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/199102.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何清理电脑缓存?简单几个步骤轻松搞定

清理电脑缓存的方法 下面我们为大家总结了一些可以用于清理电脑缓存的方法: 清理浏览器缓存 浏览器缓存是电脑为了我们能够更加快速的访问页面而临时保存的数据,随着时间的推移,浏览器缓存也会越来越多,这样不仅不会加快网页访…

广州华锐视点:VR仿真实训室中控系统成为VR课堂教学必备工具

随着科技的不断发展,虚拟现实(VR)技术已经逐渐走进我们的生活。从游戏娱乐到医疗教育,VR技术的应用范围日益广泛。近年来,VR技术在教育领域的应用也取得了显著的成果,为提高教育质量和培养创新人才提供了全…

24V-36v转3.3/5/9/12V芯片3A可调降压ic

一款高效、多功能的24V-36V转3.3/5/9/12V芯片3A可调降压IC 在当今电子设备日益普及的时代,电源管理显得尤为重要。为了满足各种设备的需求,一款高效、多功能的电源转换器成为工程师们的重要选择。本文将为您介绍一款24V-36V转3.3/5/9/12V芯片3A可调降压…

视频推拉流直播点播EasyDSS平台点播文件加密存储的实现方法

视频推拉流直播点播系统EasyDSS平台,可提供流畅的视频直播、点播、视频推拉流、转码、管理、分发、录像、检索、时移回看等功能,可兼容多操作系统,还能支持CDN转推,具备较强的可拓展性与灵活性,在直播点播领域具有广泛…

使用C语言创建高性能网络爬虫IP池

目录 一、引言 二、IP池的设计 1、需求分析 2、架构设计 3、关键技术 三、IP池的实现 1、存储实现 2、调度实现 3、通信实现 4、异常处理实现 四、代码示例 五、性能优化 六、测试与分析 七、结论 一、引言 随着互联网的快速发展,网络爬虫成为了获取…

这个工具真好用!一个网站轻松搞定电子书

相信很多朋友在寻找电子书资源的时候都会遇到一些困难,比如下载慢、格式不兼容等等。小边最近找到了这款制作电子书工具,无需下载,格式也很齐全,几乎可以满足所有人的需求。 想要电子书制作工具的可以在评论区踢我,现…

ffmpeg格式转换 免费使用视频格式转换教程

下载安装 首先去官网下载ffmpeg的软件包https://ffmpeg.org/ 如果是windows可以在直接下载编译好的软件包 https://www.gyan.dev/ffmpeg/builds/ 进入解压后的目录,子目录bin中的ffmpeg.exe就是我们要使用的转换器 视频信息查看 打开cmd控制台,从…

换元法求不定积分

1.一般步骤:选取换元对象(不一定是式子中的值,也可以是式子中的最小公倍数或者最大公因数),然后将dx换为dt*t的导数,再用t将原式表示,化简计算即可 2. 3. 4. 5. 6.

k8s部署单机模式的minio和minio-client

k8s部署单机模式的minio和minio-client 一、k8s部署minio1.1说明1.2 yaml内容1.3 步骤1.3.1 创建资源1.3.2 查看启动日志1.3.3 查看svc并访问控制台 二、docker部署minio-client2.1 查找镜像2.2 运行镜像2.3 绑定minio server 一、k8s部署minio 1.1说明 项目使用minio&#x…

【java】toString() 导致的 StackOverflowError 异常

这是怎么导致的呢? A 类属性包含着 B 类的引用 B 类属性包含着 A 类的引用 代码大概就是这样的: Data public class User{public String name;public Dog dog; }Data public class Dog{public String name;public User user; }User user new User()…

【C++】简单的C++程序编译

一、简单的C程序 //prog.cc int main() {return 0; }二、编译 1. win11命令终端 cc prog.cc 2. win11 Visual Studio命令终端 cl /EHsc /W4 prog.cc 3. GNU编译器 g -Wall -o prog prog.cc 三、运行 1.win11 prog 2.Unix/Linux ./prog 四、查看返回值 1.win11 路…

【FreeRTOS】消息队列——简介、常用API函数、注意事项、项目实现

在嵌入式系统开发中,任务间的通信是非常常见的需求。FreeRTOS提供了多种任务间通信的机制,其中之一就是消息队列。消息队列是一种非常灵活和高效的方式,用于在不同的任务之间传递数据。通过消息队列,任务可以异步地发送和接收消息…

西安安泰Aigtek——ATA-8152射频功率放大器

ATA-8152射频功率放大器简介 ATA-8152是一款射频功率放大器。其P1dB输出功率100W,饱和输出功率200W。增益数控可调,一键保存设置,提供了方便简洁的操作选择,可与主流的信号发生器配套使用,实现射频信号的放大。宽范围供…

Java-内部类

目录 概述 类的五大成员 定义 使用场景 访问特点 分类 成员内部类 如何书写 如何创建对象 变量重名时,内部类访问变量的内存图 静态内部类 局部内部类 匿名内部类 概述 类的五大成员 属性、方法、构造方法、代码块、内部类 定义 在一个类里面再定义…

LeetCode-478. 在圆内随机生成点【几何 数学 拒绝采样 随机化】

LeetCode-478. 在圆内随机生成点【几何 数学 拒绝采样 随机化】 题目描述:解题思路一:一个最简单的方法就是在一个正方形内生成随机采样的点,然后拒绝不在内切圆中的采样点。解题思路二:具体思想是先生成一个0到r的随机数len&…

软件测试要学习的基础知识——黑盒测试

黑盒测试概述 黑盒测试也叫功能测试,通过测试来检测每个功能是否都能正常使用。在测试中,把程序看作是一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,对程序接口进行测试,只检查程序功能是否按照…

Mac电脑如何安装git

一、简介 在Mac上安装Git之前,可以先使用git --version来查看一下是否安装了Git,因为Mac系统可能自带了Git,或者在你安装XCode(或者XCode的命令行工具)时,可能已经安装了 Git。 如果Mac还没有安装Git的话&…

一篇吃透大厂面试题,2024找工作一帆风顺。

🏆作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。 🏆多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。 🎉欢迎 👍点赞✍评论…

二叉树链式结构

1.前置说明 我们手动构建一棵二叉树: 注意:上述代码并不是创建二叉树的方式 从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的 2.二叉树的遍历 2.1前序、中序以及后序遍历 学习二叉树结构&a…

库函数qsort的使用及利用冒泡排序模拟实现qsort

文章目录 🚀前言🚀void*类型指针🚀库函数qsort的使用🚀利用冒泡排序实现库函数qsort() 🚀前言 今天阿辉将为大家介绍库函数qsort的使用,还包括利用冒泡排序模拟实现qsort以及void*类型的指针,关…