单人脸的关键点检测

news2024/11/28 14:51:44

闲暇之余做了一个简单的单人的脸部关键点检测,使用的pytorch框架,别人训练好的现成模型。其中人脸检测模型是YOLOface5(onnx格式的权重),关键点检测模型是PFLD(能检测98个关键点,是别人在原论文中用MobileOne改了骨干网训练好的,我直接拿来用了。这里感谢AnthonyF333,其训练和pytorch转onnx代码均已给出,地址为GitHub - AnthonyF333/PFLD_GhostOne。最终我的项目放到了github上,地址为:https://github.com/luoyoutao/FaceKeyPoints。

废话不多说,我直接上代码了(这个代码可以直接运行,不过有些路径参数需要根据自己的实际项目来更改一下)。

首先,导入需要的包:

import cv2
import time
import numpy as np
from PIL import Image, ImageDraw, ImageFont

import torch
import onnxruntime
import torchvision.transforms as transforms

下面是检测的主函数(会调用一些其他函数,在后面):

整体的流程是:

1、设置一些路径参数;

2、加载模型;

3、onnx的预热,即模型第一次预测需要花费大量的时间启动GPU,为了让真实检测时顺畅,先给模型喂一个数据。

检测的流程:

1、将原图复制一份,缩放到640*640。如果直接让输入视频或者摄像头的尺寸为640*640,那么检测就更快,且更方便。

2、送入yolo5face模型,输出shape是(12500, 16),即有12500个box,每个box16个数据,分别是center_x, center_y, w, h, threse, ....。然后将center_x, center_y, w, h转换成x1, y1, x2, y2,即对角线坐标。可能会检测到多个人脸,我这里由于检测单人的,所有直接取检测分数值最大的box,nms函数就简单得多。先sort排序,返回第一个box即可。我参考的这个项目,他是按照多人检测的,你可以根据自己的需求来即可。

3、将检测出的人脸送入PFLD模型,出来的结果直接是98个x, y坐标,然后再将坐标对应到原图上去,就可以在原图上绘制点和框了。

if __name__ == "__main__":

    # ---1、参数设置---
    use_cuda = True                     # 使用cuda - gpu
    facedetect_input_size = (640, 640)  # 人脸检测器的输入大小
    pfld_input_size = (112, 112)        # 关键点检测器的输入大小
    face_path = "./onnx_models/yolov5face_n_640.onnx"  # 人脸检测器器路径
    pfld_path = "./onnx_models/PFLD_GhostOne_112_1_opt_sim.onnx"  # 关键点检测器路径
    video_path = "./kk.mp4"    # 检测的视频文件路径,如果你用摄像头,就不管这个,注释掉即可

    # ---2、获取模型---   # 这里需要注意,如果onnx需要使用gpu,则只能且仅安装onnxruntime-gpu这个包,若你装了onnxruntime,是用CPU,需要把其卸载掉再装-GPU版本的
    facedetect_session = onnxruntime.InferenceSession(   # 加载检测人脸的模型
        path_or_bytes=face_path,
        # providers=['CPUExecutionProvider'],
        providers=['CUDAExecutionProvider']
    )
    pfld_session = onnxruntime.InferenceSession(    # 加载检测关键点的模型
        path_or_bytes=pfld_path,
        # providers=['CPUExecutionProvider'],
        providers=['CUDAExecutionProvider']
    )

    # 3、tensor设置
    detect_transform = transforms.Compose([transforms.ToTensor()])    # 人脸的
    pfld_transform = transforms.Compose([    # 关键点的
        transforms.ToTensor(),
        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])   # 归一化
    ])

    # 4、加载视频
    cap = cv2.VideoCapture(video_path)      # 如果不填路径参数就是获取摄像头

    # 5、先预热一下onnx
    data_test = torch.FloatTensor(1, 3, 640, 640)
    input_test = {facedetect_session.get_inputs()[0].name: to_numpy(data_test)}  # 把输入包装成字典
    _ = facedetect_session.run(None, input_test)

    # 6、下面开始繁琐的检测和处理程序
    x = [0 for i in range(98)]  # 初始化存放需要检测的关键点的x坐标
    y = [0 for i in range(98)]  # 初始化存放需要检测的关键点的y坐标
    while cap.isOpened():
        ret, frame = cap.read()   # type(frame) = <class 'numpy.ndarray'>
        if not ret:     # 读取失败或已经读取完毕
            break

        start = time.time()

        # 先将每一帧,即frame转成RGB,再实现ndarray到image的转换
        img0 = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        iw, ih = img0.size

        # 检测人脸前的图像处理
        # 处理方法是:先缩放,使宽或者高到640,且选择缩放比例小的那个维度(宽/高)
        #  pad_image函数参数:Image格式的图像、人脸检测器的输入大小640 * 640
        # 返回处理过的图像,最小的缩放尺度(宽高谁大缩放谁),填充的宽、填充的高(宽高只有一个需要填充)
        pil_img_pad, scale, pad_w, pad_h = pad_image(img0, facedetect_input_size)  # 尺寸处理
        # 转换成tensor
        tensor_img = detect_transform(pil_img_pad)
        detect_tensor_img = torch.unsqueeze(tensor_img, 0)  # 给tensor_img加一个维度,维度大小为1
        if use_cuda:
            detect_tensor_img = detect_tensor_img.cuda()

        # 先检测到人脸
        inputs = {facedetect_session.get_inputs()[0].name: to_numpy(detect_tensor_img)}  # 把输入包装成字典
        outputs = facedetect_session.run(None, inputs)  # type(outputs) <list>
        preds = outputs[0][0]  # shape=(25200, 16) 每一维的组成: center_x、center_y、w、h、thresh, ...

        # batch_process_output参数:人脸预测结果、阈值、缩放尺度、填充宽、填充高、原宽、原高
        # 返回经过筛选的框 type(preds) = list   preds[0].shape = 5即,[x1, y1, x2, y2, score]
        preds = np.array(batch_process_output(preds, 0.5, scale, pad_w, pad_h, iw, ih))
        if preds.shape[0] == 0:     # 如果当前帧没有检测出人脸来,继续检测人脸
            cv_img = cv2.cvtColor(np.asarray(img0), cv2.COLOR_RGB2BGR)
            cv2.imshow('ii', cv_img)
            cv2.waitKey(1)
            continue

        # nms处理,为了简便,我直接返回score最大的box
        det = nms(preds)

        # 得到裁剪出输入关键点检测器的人脸图、缩放尺度、x_offset、y_offset
        cut_face_img, scale_l, x_offset, y_offset = cut_resize_letterbox(img0, det, pfld_input_size)
        # 转换成tensor
        tensor_img = pfld_transform(cut_face_img)
        pfld_tensor_img = torch.unsqueeze(tensor_img, 0)  # 给tensor_img加一个维度,维度大小为1
        if use_cuda:
            pfld_tensor_img = pfld_tensor_img.cuda()

        # 送入关键点检测器进行检测
        inputs = {'input': to_numpy(pfld_tensor_img)}
        outputs = pfld_session.run(None, inputs)
        preds = outputs[0][0]

        radius = 2
        draw = ImageDraw.Draw(img0)  # 通过draw在原图img0上绘制人脸框和关键点
        for i in range(98):
            x[i] = preds[i * 2] * pfld_input_size[0] * scale_l + x_offset
            y[i] = preds[i * 2 + 1] * pfld_input_size[1] * scale_l + y_offset
            draw.ellipse((x[i] - radius, y[i] - radius, x[i] + radius,
                          y[i] + radius), (0, 255, 127))
        draw.text(xy=(90, 30), text='FPS: ' + str(int(1 / (time.time() - start))),
                  fill=(255, 0, 0), font=ImageFont.truetype("consola.ttf", 50))
        draw.rectangle((det[0], det[1], det[2], det[3]), outline='yellow', width=4)

        cv_img = cv2.cvtColor(np.asarray(img0), cv2.COLOR_RGB2BGR) 
        cv2.imshow('ii', cv_img)
        cv2.waitKey(1)

    cap.release()

下面是一些检测过程中用到的函数(处理检测过程中的数据,按照使用的先后顺序介绍):

1、pad_image()函数。功能:将原图复制一份,对复制的图像进行尺度缩放和填充。具体操作是,先将图像尺寸设置到640*640,将宽或高大的边resize到640,然后小的边按照尺度resize到640,这时小边一定\leq640,因此填充黑色像素即可,这个过程是需要记录压缩的尺度和填充的长度的,便于后面检测到的关键点坐标对应到原图上。

def pad_image(image, target_size):
    '''
    image: 图像
    target_size: 输入网络中的大小
    return: 新图像、缩放比例、填充的宽、填充的高
    '''
    iw, ih = image.size  # 原图像尺寸
    w, h = target_size   # 640, 640

    scale = min(w / iw, h / ih)     # 缩放比例选择最小的那个(宽高谁大缩放谁)(缩放大的,填充小的)
    nw = int(iw * scale + 0.5)
    nh = int(ih * scale + 0.5)

    pad_w = (w - nw) // 2   # 需要填充的宽
    pad_h = (h - nh) // 2      # 需要填充的高

    image = image.resize((nw, nh), Image.Resampling.BICUBIC)    # 缩放图像(Resampling需要PIL最新版,python3.7以上)
    new_image = Image.new('RGB', target_size, (128, 128, 128))  # 生成灰色的新图像
    new_image.paste(image, (pad_w, pad_h))      # 将image张贴在生成的灰色图像new_image上

    return new_image, scale, pad_w, pad_h  # 返回新图像、缩放比例、填充的宽、填充的高

2、to_numpy()函数。将tensor转换为numpy,这里是推理,故不用计算梯度,直接转为无梯度的numpy。

def to_numpy(tensor_data):
    return tensor_data.detach().cpu().numpy() if tensor_data.requires_grad else tensor_data.cpu().numpy()

3、batch_process_output()函数。预测出来的人脸框数据是中心点+宽高,我们需要转换为左上右下的对角线坐标,且需要转换到对应原图上的坐标,因此需要原图缩放尺度和填充的像素值。最后返回了前五个数据,即x1, y1, x2, y2, score。

def batch_process_output(pred, thresh, scale, pad_w, pad_h, iw, ih):
    '''
    iw, ih为图像原尺寸
    '''
    bool1 = pred[..., 4] > thresh  # bool1.shape = [num_box] 里面的值为bool(True/False)
    pred = pred[bool1]  # pred.shape = [n, 16],即筛选出了置信度大于thresh阈值的n个box

    ans = np.copy(pred)
    ans[:, 0] = (pred[:, 0] - pred[:, 2] / 2 - pad_w) / scale  # x1
    np.putmask(ans[..., 0], ans[..., 0] < 0., 0.)   # 将所有box的小于0.的x1换成0.

    ans[:, 1] = (pred[:, 1] - pred[:, 3] / 2 - pad_h) / scale  # y1
    np.putmask(ans[..., 1], ans[..., 1] < 0., 0.)   # 将所有box的小于0.的y1换成0.

    ans[:, 2] = (pred[:, 0] + pred[:, 2] / 2 - pad_w) / scale  # x2
    np.putmask(ans[..., 2], ans[..., 2] > iw, iw)  # 将所有box的大于iw的x2换成iw

    ans[:, 3] = (pred[:, 1] + pred[:, 3] / 2 - pad_h) / scale  # y2
    np.putmask(ans[..., 3], ans[..., 3] > ih, ih)  # 将所有box的大于ih的y2换成ih

    ans[..., 4] = ans[..., 4] * ans[..., 15]  # score

    return ans[:, 0:5]

4、nms()函数。这里预测单人脸,我直接返回score第一的box。

def nms(preds):    # NMS筛选box
    arg_sort = np.argsort(preds[:, 4])[::-1]
    nms = preds[arg_sort]       # 按照score降序将box排序

    # 单脸检测,直接返回分数最大的box
    return nms[0]

5、 cut_resize_letterbox()函数。从原图中裁剪出人脸框再resize到112*112。

def cut_resize_letterbox(image, det, target_size):
    # 参数分别是:原图像、检测到的某个脸的数据[x1,y1,x2,y2,score]、关键点检测器输入大小
    iw, ih = image.size

    x, y = det[0], det[1]
    w, h = det[2] - det[0], det[3] - det[1]

    facebox_max_length = max(w, h)      # 以最大的边来缩放
    width_margin_length = (facebox_max_length - w) / 2  # 需要填充的宽
    height_margin_length = (facebox_max_length - h) / 2     # 需要填充的高

    face_letterbox_x = x - width_margin_length
    face_letterbox_y = y - height_margin_length
    face_letterbox_w = facebox_max_length
    face_letterbox_h = facebox_max_length

    top = -face_letterbox_y if face_letterbox_y < 0 else 0
    left = -face_letterbox_x if face_letterbox_x < 0 else 0
    bottom = face_letterbox_y + face_letterbox_h - ih if face_letterbox_y + face_letterbox_h - ih > 0 else 0
    right = face_letterbox_x + face_letterbox_w - iw if face_letterbox_x + face_letterbox_w - iw > 0 else 0

    margin_image = Image.new('RGB', (iw + right - left, ih + bottom - top), (0, 0, 0))  # 新图像,全黑的z
    margin_image.paste(image, (left, top))      # 将image贴到margin_image,从左上角(left, top)位置开始

    face_letterbox = margin_image.crop(     # 从margin_image中裁剪图像
        (face_letterbox_x, face_letterbox_y, face_letterbox_x + face_letterbox_w, face_letterbox_y + face_letterbox_h))

    face_letterbox = face_letterbox.resize(target_size, Image.Resampling.BICUBIC)   # 重新设置图像尺寸大小

    # 返回:被裁剪出的图像也是即将被送入关键点检测器的图像、缩放尺度、x偏移、y偏移
    return face_letterbox, facebox_max_length / target_size[0], face_letterbox_x, face_letterbox_y

上面即是所有的代码了,若有指教,欢迎评论交流。

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

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

相关文章

计算机网络学习笔记(Ⅳ):网络层

目录 1 网络层内容 1.1 功能概述 1.任务 2.主要功能 1.2 数据交换方式 1.电路交换 2.报文交换 3.分组交换 4.方法对比 1.3 分组交换 1.数据报方式 2.虚电路方式 3.对比 2 路由算法与路由协议 2.1 路由算法 2.2 路由选择协议 3 IPv4 3.1 IP数据报格式 1.TCP/…

快2023年了,还不会性能调优?阿里技术官亲授“Java性能调优技术宝典”看完直接涨薪5K

一、前言 什么是性能调优&#xff1f; 性能调优其实很好理解&#xff0c;就是优化硬件、操作系统、应用之间的一个充分的协作&#xff0c;最大化的发挥出硬件的极致性能&#xff0c;来应对高负载的业务需求。 为什么需要性能优化&#xff1f; 其实说到底就是两个原因&#…

2023年湖北安全员ABC三类人员延期多久一次?甘建二

2023年湖北安全员ABC三类人员延期多久一次&#xff1f; 2023年湖北安全员ABC延期快来找甘建二报名&#xff0c;建设厅指定的 2023年湖北安全员ABC新办快来找甘建二报名&#xff0c;建设厅指定的 首先安全员分为三类&#xff1a;A证、B证、C证&#xff0c;每个证书都有相应的…

Spring源码解析-环境变量(下)

“不积跬步&#xff0c;无以至千里”。 接着聊上一篇文章中遗留的两个重要问题&#xff1a; 如何往Spring环境变量中添加自定义的环境变量&#xff1f;工作原理是什么&#xff1f;PropertyPlaceholderConfigurer这个类是怎么完成bean属性填充时“$”符号解析工作的&#xff1f…

数据库系统概论第2章 关系数据库

易错点1&#xff1a;实体完整性 实体完整性要求主属性不能取空值 而主属性不能取空值≠候选码不为空 因为候选码可以是两个属性的组合&#xff0c;而主属性是候选码的属性 举个例子&#xff1a; SC表中候选码为&#xff08;学号&#xff0c;课程号&#xff09; 主属性为学…

fetch向后端请求数据:get/post/put/patch/delete方式、解决catch不能主动捕获错误问题(超详细笔记)

1、什么是fetch&#xff1a; fetch是ES6出现的&#xff0c;它使用了 ES6 提出的 promise 对象&#xff0c;为了取代XMLHttpRequest而诞生的&#xff1b;提到XMLHttpRequest就不得不提ajax&#xff0c;ajax是一种实现前后端交互的技术&#xff0c;而ajax是基于XMLHttpRequest模块…

C++ Reference: Standard C++ Library reference: Containers: map: map: count

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/count/ 公有成员函数 <map> std::map::count size_type count (const key_type& k) const;计数具有特定键的元素 在容器中搜索键值等于k的元素&#xff0c;并返回匹配的数量。 因为map容器中的所…

搞懂商业智能 BI 、数据仓库、数据中台及其关系,此文足以

数字化如火如荼&#xff0c;企业的 IT 信息化也越演越烈&#xff0c;企业管理者对数据管理也是越来越重视&#xff0c;认识到数据资产带来的价值&#xff0c;本文对这些名词术语及内涵进行系统的解析&#xff0c;便于读者对数据平台相关的概念有全面的认识。 商业智能BI 商业…

本文分享Unity中的AI算法和实现3-有限状态机FSM(下)

本文分享Unity中的AI算法和实现3-有限状态机FSM(下) 回家生孩子, 暂停了一个多月的更新, 今天重新续上, ^_^. 在上一篇文章中, 我们分享了状态机的基本理论, 然后结合Unity的Animator来熟悉了这些理论, 最后设计了我们自己的状态机并实现了框架部分. 这一篇文章, 我们将继续…

20221212 SpringCloud Alibaba

Spring Cloud Alibaba介绍主要功能组件注册中心脚手架创建实例使用RestTemplate实现微服务调用使用openfeign实现微服务调用负载均衡的使用创建多实例修改负载均衡Spring Cloud Alibaba 介绍 官方文档&#xff1a; https://spring.io/projects/spring-cloud-alibaba https://…

git stash 命令详解

1. 应用场景 2. 添加储藏 3. 查看储藏 4. 删除储藏 5. 使用储藏 6. 常见用法 1. 应用场景 git stash 命令用于将工作区中的更改和暂存区中的内容储存起来 日常开发中&#xff0c;会经常遇到这种场景 我们正在 dev 分支开发新功能&#xff0c;做到一半时&#xff0c;产品经理…

模块化、组件化和插件化

模块化&#xff1a;业务解耦、代码重用 组件化&#xff1a;模块化为基础、开发阶段每个moudle都是一个app &#xff0c;可以单独编译,并行开发 互不干扰&#xff0c;不用编译整个工程&#xff0c;打包的时候每个moudle又是moudle 不是app 只有一个app 插件化&#xff1a;也是…

【愚公系列】2022年12月 Elasticsearch数据库-ELK环境的搭建(一)

文章目录前言一、ELK环境的搭建1.前提条件2.启动Elasticsearch3.配置可视化工具 head-master3.配置kibana前言 ELK是三个开源软件的缩写&#xff0c;分别表示&#xff1a;Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat&#xff0c;它是一个轻量级…

大学生可以在校搞搞副业吗?尝试做外卖跑腿项目有没有市场?

随着大学寒假的即将到来&#xff0c;40多天的假期&#xff0c;为什么大学生不利用这个机会去想明年的校园生活该如何度过&#xff0c;想要自己的校园生活过得精彩&#xff0c;就给自己找一个副业吧&#xff01; 副业&#xff01;这两个词应该是针对工作的&#xff0c;而不是针…

MapReduce 编程实例:词频统计

文章目录MapReduce 编程实例&#xff1a;词频统计一&#xff0c;准备数据文件&#xff08;1&#xff09;在虚拟机上创建文本文件&#xff08;2&#xff09;上传文件到HDFS指定目录二&#xff0c;使用IDEA创建Maven项目三&#xff0c;添加相关依赖四&#xff0c;创建日志属性文件…

【AI with ML】第 3 章 :超越基础知识:检测图像中的特征

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

树上操作【点分治】 - 原理 中心分解 【POJ No. 1741】 树上两点之间的路径数 Tree

树上操作【点分治】 - 原理 中心分解 分治法指将规模较大的问题分解为规模较小的子问题&#xff0c;解决各个子问题后合并得到原问题的答案。树上的分治算法分为点分治和边分治。 点分治经常用于带权树上的路径统计&#xff0c;本质上是一种带优化的暴力算法&#xff0c;并融…

【内网安全-基础】基础知识、信息收集、工具

目录 一、基础知识 1、内网&#xff1a; 2、工作组&#xff1a; 3、域(Domain)&#xff1a; 二、基础信息收集 1、判断是否在域内 2、机器角色判断 3、出网协议判断 4、端口判断 三、常规信息收集 1、常用命令 2、常用命令 3、工具&插件 LadonGO CS插件 Adfi…

基于Java(Spring+Struts+Hibernate 框架)实现(Web)学生课程管理系统【100010038】

课程管理系统设计文档 二、引言 2.1 目的 ​ 本文档详细描述了课程管理系统的设计&#xff0c;达到引导开发的作用&#xff0c;同时实现测试人员以及用户的沟通。 ​ 本文档面向开发人员&#xff0c;测试人员以及最终用户编写&#xff0c;是了解系统的导航。 2.2 范围 ​…

Win10系统电脑连接打印机的操作方法教学

Win10系统电脑连接打印机的操作方法教学分享&#xff0c;很多用户在办公的时候都会需要使用到打印机。用用户自己购买了打印机之后&#xff0c;不懂怎么去连接自己的电脑来进行使用的方法&#xff0c;接下来我们一起来看看Win10系统电脑连接打印机的操作方法分享吧。 Win10连接…