基于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 / h
    ratio = 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]) // 2
    y = (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_width
        normalized_y = (kp[1]  + (target_height - img_resized.shape[0]) / 2) / target_height
        normalized_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.txt
                train_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 np

def 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_nb
        self.num_lms = num_lms
        self.input_size = input_size
        self.net_stride = net_stride
        self.features = mbnet.features
        self.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_conv2d
        self.features[18][1] = new_bn

        self.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传不到这里来。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

天津python培训课程,普通人学python有用吗?

选择一个正确的行业对于个人的发展至关重要,在当今时代,IT行业高薪的特点吸引了越来越多的人转行“入坑”,而作为目前的热门编程语言,python也是很多人转行的选择。 Python培训学费选择 python培训班学费可能会因为培训机构所在…

C#简化工作之实现网页爬虫获取数据

1、需求 想要获取网站上所有的气象信息,网站如下所示: 目前总共有67页,随便点开一个如下所示: 需要获取所有天气数据,如果靠一个个点开再一个个复制粘贴那么也不知道什么时候才能完成,这个时候就可以使用C…

Isaac Sim教程06 OmniGraph图编程

Isaac Sim OmniGraph图编程 版权信息 Copyright 2023 Herman YeAuromix. All rights reserved.This course and all of its associated content, including but not limited to text, images, videos, and any other materials, are protected by copyright law. The autho…

mac shortcut keys cheat sheet【mac 快捷键清单】

文章目录 剪切、拷贝、粘贴和其他常用快捷键访达和系统快捷键 Mac 键盘快捷键 Command(或 Cmd)⌘ Shift ⇧ Option(或 Alt)⌥ Control(或 Ctrl)⌃ Caps Lock ⇪ Fn 剪切、拷贝、粘贴和其他常用快捷…

【数据结构与算法】JavaScript实现二叉搜索树

文章目录 一、二叉搜索树的封装1.插入数据2.遍历数据2.1.先序遍历2.2.中序遍历2.3.后续遍历 3.查找数据3.1.查找最大值&最小值3.2.查找特定值 4.删除数据4.1.情况1:没有子节点4.2.情况2:有一个子节点4.3.情况3:有两个子节点4.4.完整实现 …

【性能测试】LR录制回放事务检查点

前言 上一次推文我们分享了性能测试分类和应用领域,今天带大家学习性能测试工作原理、事务、检查点!后续文章都会系统分享干货,带大家从0到1学会性能测试,另外还有教程等同步资料,文末免费获取~ 01、LR工作原理 ​通…

CSS特效026:扇骨打开关闭的动画

CSS常用示例100专栏目录 本专栏记录的是经常使用的CSS示例与技巧,主要包含CSS布局,CSS特效,CSS花边信息三部分内容。其中CSS布局主要是列出一些常用的CSS布局信息点,CSS特效主要是一些动画示例,CSS花边是描述了一些CSS…

Vue项目图片预览v-viewer插件使用,图片预览,图片查看;antdesign+vue2+v-viewer实现图片查看器并可删除图片

Vue项目图片预览v-viewer插件使用 1. 安装 v-viewer 你可以使用 npm 或者 yarn 来安装 v-viewer: npm install v-viewer 或者 yarn add v-viewer 2. 导入和配置 v-viewer 在你的 Vue 项目中,你需要在入口文件(通常是 main.js&#xff09…

做一个类似东郊到家的上门服务类系统有哪些功能?

上门服务系统是一款便捷的技师接单、上门提供理疗服务的软件。我们拥有优秀的开发团队,为您量身定制解决方案,价格合理,用心服务。 预约上门:该功能是预约上门推拿理疗按摩系统软件小程序APP的核心功能。消费者通过系统预约下单&a…

python打包exe,打包好后,启动exe报错找不到paddleocr

目录 1、安装pyinstaller 2、生成脚本文件的.spce文件 3、资源文件配置 4、生成exe文件 5、使用了paddleocr启动exe后报错 6、配置.spce文件 7、重新生成exe文件 8、关于图片找不到的问题 参考:PaddleOCR打包exe--Pyinstaller_paddleocr 打包exe_mjiansun的博…

签名应用APP分发平台的微服务化部署是什么?其有哪些优势?

在信息技术的世界里,软件开发和部署的模式不断演进。从单体架构到服务化,再到今日备受瞩目的微服务架构。微服务化部署作为一种新兴的软件架构风格,正被越来越多的企业采用。它使得应用可以被分解成一套相互独立的最小服务单元。而“分发平台…

Web安全-初识SQL注入(一)

1、初识SQL注入 1.1、什么是注入? 将不受信任的数据作为命令或查询的一部分发送到解析器时,会产生诸如SQL注入、NoSQL注入、OS 注入和LDAP注入的注入缺陷。攻击者的恶意数据可以诱使解析器在没有适当授权的情况下执行非预期命令或访问数据。 注入能导…

基于 springboot + vue 健身房管理系统 毕业设计-附源码

qq(2829419543)获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:springboot 前端:采用vue技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件&#xf…

Temu数据软件:如何使用Temu数据软件优化您的Temu店铺运营

在如今竞争激烈的电商市场中,了解市场趋势、优化产品和店铺运营、了解竞争对手等方面的数据分析变得至关重要。为了帮助Temu平台上的商家更好地了解市场和消费者需求,提高运营效果,Temu数据软件成为了一项强大的工具。本文将介绍一些建议的Te…

【Tomcat】java.net.BindException “Address already in use: NET_Bind“

问题 17:37 Error running Tomcat 7.0.76: Unable to open debugger port (127.0.0.1:14255): java.net.BindException "Address already in use: NET_Bind"调整 把14255 改成 49963就正常了 附件 netstat -aon|findstr "49963" taskkill -f -pid xxx…

WiFi模块ESP8266(超详细)---(含固件库、AP、STA、原子云使用)

写在前面:本节我们学习使用一个常见的模块--WIFI模块,在前面我们学习了蓝牙(HC--08)模块,为学习WIFI模块打下了一定的基础;但是在学习WIFI模块的时候,我也遇到了很多的问题,查阅了很…

常见的分布式事务解决方案,你会几种?

今天我们来聊一聊分布式事务,在传统的单体应用中,事务的控制非常简单,Spring框架都为我们做了封装,我们只需简单地使用Transactional注解就能进行事务的控制,然而在分布式应用中,传统的事务方案就出现了极大…

通信标准化协会,信通院及量子信息网络产业联盟调研玻色量子,共绘实用化量子未来!

8月14日,中国通信标准化协会,信通院标准所及量子信息网络产业联盟等单位领导走访调研北京玻色量子科技有限公司(以下简称“玻色量子”),参观了玻色量子公司及自建的十万颗粒洁净度的光量子信息技术实验室🔗…

ROS2教程02 ROS2的安装、配置和测试

ROS2的安装和配置 版权信息 Copyright 2023 Herman YeAuromix. All rights reserved.This course and all of its associated content, including but not limited to text, images, videos, and any other materials, are protected by copyright law. The author holds a…

python超详细基础文件操作【建议收藏】

文章目录 前言1 文件操作1.1 文件打开与关闭1.1.1 打开文件1.1.2 关闭文件 1.2 访问模式及说明 2 文件读写2.1 写数据(write)2.2 读数据(read)2.3 读数据(readlines)2.3 读数据(readline&#x…