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

news2024/11/29 2:34:11

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

按模型结构分类:

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

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

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

相关文章

sql-SQL练习生

推荐一款inscode内的模板SQL练习生,此文附带目前所有题的答案 如有错误欢迎斧正~ https://inscode.csdn.net/TPEngineer/SQLBoy 为了更好的体验,请按下面的方法打开: 1.运行一下 2.等待加载 3.在网页打开 温馨提醒:此处做题不会保…

【交叉编译】

一、什么是交叉编译 二、为什么要交叉编译? 三、交叉编译要用到的工具(工具链、交叉编译器) 四、交叉编译工具链的安装 五、配置环境变量 六、交叉编译工具编译 七、带wiringPi库的交叉编译如何进行 八、软链接、硬链接 九、Linux创建链接命令…

nodejs+vue+ElementUi牙科诊所信息化系统

该系统将采用B/S结构模式,前端部分主要使用html、css、JavaScript等技术,使用Vue和ElementUI框架搭建前端页面,后端部分将使用Nodejs来搭建服务器,并使用MySQL建立后台数据系统,通过axios完成前后端的交互,…

认识异常 ---java

目录 一. 异常的概念 二. 异常的体系结构 三. 异常的分类 三. 异常的处理 3.1 异常的抛出throw 3.2. 异常声明throws 3.3 捕获并处理try-catch finally 3.4异常的处理流程 四. 自定义异常类 一. 异常的概念 在 Java 中,将程序执行过程中发生的不正常行为称为…

CPP-SCNUOJ-Problem P24. [算法课贪心] 跳跃游戏

Problem P24. [算法课贪心] 跳跃游戏 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度 判断你是否能够到达最后一个下标。 输入 输入一行数组nums 输出 输出true/fasle 样例 标准输入 2 3 1 …

出海风潮:中国母婴品牌征服国际市场的机遇与挑战!

近年来,中国母婴品牌在国内市场蓬勃发展的同时,也逐渐将目光投向国际市场。这一趋势不仅受益于中国经济的崛起,还得益于全球市场对高质量母婴产品的不断需求。然而,面对国际市场的机遇,中国母婴品牌同样面临着一系列挑…

Myblog02-基于ssm,springboot的改进

目录 一、项目概述: 应用技术: 接口实现: 数据库建表,sql脚本: 页面展示:登陆页面 项目源码:myblog01: 初版的个人博客项目-使用基本的javaWeb (gitee.com) 二、对博客系统进行测试 总结…

深入分析爬虫中time.sleep和Request的并发影响

背景介绍 在编写Python爬虫程序时,我们经常会遇到需要控制爬取速度以及处理并发请求的情况。本文将深入探讨Python爬虫中使用time.sleep()和请求对象时可能出现的并发影响,并提供解决方案。 time.sleep()介绍 首先,让我们来了解一下time.s…

【发布小程序配置服务器域名,不配置发布之后访问就会报错request:fail url not in domain list】

小程序在本地开发的时候大家通常会在微信开发者工具中设置“不校验合法域名、web-view (业务域名)、TLS 版本以及HTTPS证书”,久而久之可能会忘掉这个操作,然后打包直接上线发布,结果发现访问会报错request:fail url not in domain list&…

CETN03 - The Evolution of Computers

文章目录 I. IntroductionII. First Modern Digital Computer: ENIAC (1946)III. First Generation ComputerIV. Second Generation ComputerV. Third Generation ComputerVI. Fourth Generation ComputerVII. ConclusionI. 引言II. 第一台现代数字计算机:ENIAC&…

自定义 el-select 和 el-input 样式

文章目录 需求分析el-select 样式el-input 样式el-table 样式 需求 自定义 选择框的下拉框的样式和输入框 分析 el-select 样式 .select_box{// 默认placeholder:deep .el-input__inner::placeholder {font-size: 14px;font-weight: 500;color: #3E534F;}// 默认框状态样式更…

C++ 12.5作业

以下是一个简单的比喻,将多态概念与生活中的实际情况相联系: 比喻:动物园的讲解员和动物表演 想象一下你去了一家动物园,看到了许多不同种类的动物,如狮子、大象、猴子等。现在,动物园里有一位讲解员&…

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

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

云祺副本容灾机制讲解

副本,顾名思义就是一份数据的拷贝。 在系统中,将数据的目的分为了三种:备份、副本和归档。 其中备份数据,通常是存放在备份系统本地,或数据中心本地;副本数据通过是存放在异地的备份系统中,或…

【C语言】【堆排序实现TOPK问题】写一个堆排序,并且在一百万个数中找出最大的前K个

1.堆排序的实现: 如果要升序输出,则实现大堆 如果要降序输出,则实现小堆 逻辑:(升序输出) 将堆顶的元素和最后一个元素交换位置,此时左子树和右子树分别仍是大堆的顺序,交换位置后…

车企数据治理实践案例,实现数据生产、消费的闭环链路 | 数字化标杆

随着业务飞速发展,某汽车制造企业业务系统数量、复杂度和数据量都在呈几何级数的上涨,这就对于企业IT能力和IT架构模式的要求越来越高。加之企业大力发展数字化营销、新能源车等业务,希望通过持续优化客户体验,创造可持续发展的数…

leetcode:统计感冒序列的数目【数学题:组合数含逆元模版】

1. 题目截图 2.题目分析 需要把其分为多个段进行填充 长为k的段,从两端往中间填充的方案数有2 ** (k - 1)种 组合数就是选哪几个数填哪几个段即可 3.组合数含逆元模版 MOD 1_000_000_007 MX 100_000# 组合数模板 fac [0] * MX fac[0] 1 for i in range(1, MX…

一个完整的转录组分析流程

本期的教程代码(部分) #!/bin/bash # # 使用fastq-dump解压sra数据 # 本数据集为双端数据 # 解压格式为fq.gz for i in SRR6929571 SRR6929572 SRR6929573 SRR6929574 SRR6929577 SRR6929578; do pfastq-dump --split-files --threads 20 --gzip -s 00_…

综合指南:如何创建有效的知识地图?

知识地图是知识管理中的重要工具,使企业能够有效地利用其资产。它促进了解决问题、新人整合和组织学习。此外,它还提高了生产力,实现了数据驱动的决策,并优化了流程。通过捕获和组织有价值的知识资产,它确保了专业知识…

如何通过nvm安装多版本nodejs?如果nodejs安装成功,但npm安装失败怎么办?

我们在开发项目的时候,最开始,是只有一个老的项目,老项目单独安装了node版本4.4.7,后来有了新项目,由于有两个项目,但是一个需要老一些版本的node,一个需要新版本的node,因此需要在两…