Mediapipe框架(二)人脸检测
-
MediaPipe
是一款由 Google Research 开发并开源的多媒体机器学习模型应用框架
。谷歌的一系列重要产品,如Google Lens、ARCore、Google Home等都已深度整合了 MediaPipe。 -
MediaPipe
目前支持的解决方案(Solution)及支持的平台如下图所示,除了视觉任务,还支持文本、语音及生成式AI任务(实验中)。作为一款跨平台框架,MediaPipe 不仅可以被部署在Web端,更可以在多个移动端 (Android和苹果 iOS)和嵌入式平台(Google Coral 和树莓派)中作为设备端机器学习推理框架。 -
MediaPipe
的每个解决方案(Solution)包括一个或多个模型,一些解决方案还可以自定义模型(使用Model Maker)。 -
今天,我们主要来了解下
人脸检测(Face Detection)
。
官网地址(需魔法流量): MediaPipe | Google for Developers
Mediapipe框架的核心概念可以参考:Mediapipe框架(一)人手关键点检测
1 MediaPipe人脸检测模型概述
1.1 概述
-
MediaPipe人脸检测是一种超快速的人脸检测解决方案,除了提供
面部边界框
外,还可以输出6个特征点的坐标:双眼中央、耳垂、嘴部中央与鼻尖
。 -
MediaPipe人脸检测所用模型是BlazeFace的变体,BlazeFace 是谷歌19年提出的一种针对移动 GPU 推断进行优化的轻量级且精确的人脸检测器。
- BlazeFace 使用类似 MobileNetV1/V2 的轻量级特征提取网络。
- BlazeFace 在旗舰移动设备上以
200-1000 + FPS
的速度运行。 这种超实时性能使其能够应用于任何对性能要求极高的增强现实应用中。 - 作者在MobileNet-SSD目标检测框架下,改进了网络结构、anchor机制、替换NMS后处理,使算法在人脸检测任务中保持高精度的同时,在移动GPU上速度还很快。
-
该检测器的超实时性能使其可应用于其他需要准确地关注面部区域的模型:
例如
1、3D面部关键点检测
2、面部特征或表情分类,以及面部区域分割等
- 在midiapipe官网中,提到了三个BlazeFace相关模型。
- BlazeFace (short-range),目前
只有此模型提供了预训练权重
。一种轻量级模型,用于在智能手机相机或网络摄像头的自拍式图像中检测单个或多个面部。该模型针对短距离的前置手机摄像头图像进行了优化。模型架构使用了一种带有自定义编码器的Single Shot Detector (SSD)卷积网络技术。 - BlazeFace (full-range)。这是一个相对轻量级的模型,用于在智能手机摄像头或网络摄像头拍摄的图像中检测单个或多个面部。该模型针对全范围图像进行了优化,比如使用后置手机摄像头拍摄的图像。模型架构使用了类似于CenterNet卷积网络的技术,并配备了自定义编码器。
- BlazeFace Sparse (full-range)。这是BlazeFace(full-range)模型的轻量级版本,大小大约缩小了60%。
- BlazeFace (short-range),目前
1.2 BlazeFace模型概述
BlazeFace已经被谷歌用于实际的工程中,因此这篇文章非常值得参考。
论文地址:https://arxiv.org/abs/1907.05047
BlazeFace是Google专为移动端GPU定制的人脸检测方案。主要创新点如下:
- 专为轻量级检测定制的紧凑型特征提取网络,类似于MobielNet。
- 相比SSD模型,对GPU更友好的anchor机制。
- 采用“tie resolution strategy”而非NMS处理重叠的预测结果。
1.2.1 BlazeBlock模块
-
我们知道
深度可分离卷积
是轻量网络经常采用的卷积形式。它使用两步卷积运算,即Depthwise卷积与Pointwise卷积替代常规的单次卷积。 -
作者在iPhone上实测后发现,一个56×56×128大小、16-bit张量的Depthwise卷积运算耗时0.07ms,而伴随其后128-128通道的Pointwise卷积运算耗时0.3ms,是前者的4倍以上(作者认为是内存存取的固定开销造成)。因此作者提出在
Depthwise卷积中使用较大的卷积核(即使用5*5卷积核代替3*3卷积核,扩大感受野)
,这样只需要较少的bottleneck数量就可以获得指定大小的感受野范围。作者将该结构命名为 BlazeBlock。 -
另外为了促进感受野Size的传递,提出了double BlazeBlock 模块。该模块在bottleneck的最前端又塞入了一层深度可分离卷积,并增加了Max Pooling与Channel Padding作为可选旁路。
1.2.2 特征提取
-
对于前置摄像头这种人脸占图像很大面积、人脸尺度变化较小的场景,作者定义了更加轻量级的特征提取。
-
作者使用了128×128像素的RGB输入,后跟1个常规5×5卷积、5个BlazeBlock、6个Double BlazeBlock,其中通道数最大值为96(网络结构可以参考原文)。
-
BlazeFace最低的空间分辨率为8×8(SSD分辨率一直降低到1×1)。
1.2.3 Anchor机制
- BlazeFace的特征提取网络在特征图缩小到8×8时就结束了。作者给出的理由是“Pooling Pyramid Network (PPN) 表明特征图在达到特定分辨率后的附加运算存在冗余”。
- 另外作者指出:在GPU上调度特定运算层存在可察觉的固定损耗,尤其是针对CPU优化的深度低分辨率网络层。
- 综合以上两点,BlazeFace网络的深度止步在8×8大小,但是将anchor的数量扩增到了6个。同时考虑人脸相对固定的形状变化,将anchor的比例固定在1:1。
1.2.4 后处理
- 由于8×8特征图上扩增了anchor数量,大量的检测结果会重叠在一起。传统的处理方式是使用NMS。在SSD的NMS中,只有一个胜出的anchor用于算法输出,这导致在视频中进行检测时,人脸框抖动明显。
- 为了降低这种效应,作者提出使用融合(blending)而非抑制(suppression)策略。具体的做法为将重叠边界框回归参数做加权平均计算,而NMS则是采用重叠结果中的一个。
- 作者称对于在视频中的面部检测任务,此调整导致准确度提高10%。
1.2.5 实验结果
该文重点说明在手机终端真实应用中,检测算法的加速,只是在谷歌的私有数据集上与MobileNetV2-SSD的比较。
- 下图是比较结果,精度高于MobileNetV2-SSD,在iPhone XS上的速度也从2.1毫秒降到0.6毫秒。
- 更多手机上的运算速度比较,BlazeFace在不同的手机上都获得了成倍的速度提升:
2 Mediapipe人脸检测代码实现
- 该任务配置项如下图所示:
配置项 | 描述 | 取值范围 | 默认值 |
---|---|---|---|
running_mode | 设置任务的运行模式。有三种模式:IMAGE:用于单张图像输入。VIDEO:用于视频解码帧。LIVE_STREAM:用于输入数据的实时流模式,例如来自摄像机的数据。在此模式下,必须调用resultListener来设置一个监听器以异步接收结果。 | {IMAGE, VIDEO, LIVE_STREAM } | IMAGE |
min_detection_confidence | 脸部检测被视为成功所需的最低置信度分数。 | Float [0,1] | 0.5 |
min_suppression_threshold | 非极大值抑制阈值 | Float [0,1] | 0.3 |
result_callback | 异步回调结果(仅用于LIVE_STREAM模式) | N/A | Not set |
1、安装mediapipe包
pip install mediapipe
2、下载预训练模型:
下载地址:
https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite
3、检测结果
-
人脸检测器返回一个FaceDetectorResult对象来表示每次检测运行的结果。该结果对象包含检测到的人脸的边界框和每个检测到的人脸的置信度分数等信息。
-
检测到的面部的集合,其中包含边界框和6个关键点。边界框未标准化,不过每个关键点(由x和y组成)通过图像的宽度和高度进行了标准化。
以下是此任务输出数据的示例。
FaceDetectionResult:
Detections:
Detection #0:
BoundingBox: #人脸的bbox
origin_x: 126
origin_y: 100
width: 463
height: 463
Categories:
Category #0:
index: 0
score: 0.9729152917861938 # 人脸检测的置信度
NormalizedKeypoints: # 每张人脸所含的6个关键点
NormalizedKeypoint #0:
x: 0.18298381567001343
y: 0.2961040139198303
NormalizedKeypoint #1:
x: 0.3302789330482483
y: 0.29289937019348145
... (6 keypoints for each face)
Detection #1:
BoundingBox:
origin_x: 616
origin_y: 193
width: 430
height: 430
Categories:
Category #0:
index: 0
score: 0.9251380562782288
NormalizedKeypoints:
NormalizedKeypoint #0:
x: 0.6151331663131714
y: 0.3713381886482239
NormalizedKeypoint #1:
x: 0.7460576295852661
y: 0.38825345039367676
... (6 keypoints for each face)
4、检测速度
- 下图为该模型的推理时间,在CPU仅花费2.94ms。
2.1 单张图像的人脸检测
- 需要下载预训练模型
- utils代码在mediapipe框架小案例 中
import numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import cv2
from utils import visualize
def detect_face_from_image(img_path):
# 1、创建人脸检测器
# 需要先下载预训练模型
base_options = python.BaseOptions(model_asset_path='blaze_face_short_range.tflite')
options = vision.FaceDetectorOptions(base_options=base_options)
detector = vision.FaceDetector.create_from_options(options)
# 2、加载输入图片
image = mp.Image.create_from_file(img_path)
# 3、使用下载好的模型进行人脸检测
detection_result = detector.detect(image)
print(detection_result)
# 4、 可视化
image_copy = np.copy(image.numpy_view())
annotated_image = visualize(image_copy, detection_result)
rgb_annotated_image = cv2.cvtColor(annotated_image, cv2.COLOR_BGR2RGB)
# 在使用OpenCV的cv2.imshow函数显示图像时,它会默认将传入的图像数据解释为BGR格式
# 如果你传入的是RGB格式的图像数据,OpenCV会在显示时进行颜色通道的调整,使图像以BGR格式进行显示。
cv2.imshow('face detection', rgb_annotated_image)
# 输入esc结束捕获
if cv2.waitKey(0) == 27:
cv2.destroyAllWindows()
if __name__ == '__main__':
detect_face_from_image(img_path='face_image.jpg')
检测结果如下:
2.2 视频帧的人脸检测
- 这里用pyside6做了一个简单检测页面
- 可以调用本地电脑摄像头进行在线检测,也可进行离线检测
- 需要设置running_mode为VisionRunningMode.VIDEO
import time
from utils import visualize
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import cv2
from PySide6 import QtWidgets, QtCore, QtGui
from threading import Thread
# 通过pyside6实现一个简单的界面
class MWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# 设置界面
self.setupUI()
# 给按钮绑定事件
self.videoBtn.clicked.connect(self.startVideo)
self.camBtn.clicked.connect(self.startCamera)
self.stopBtn.clicked.connect(self.stop)
# 定义定时器,用于控制显示视频的帧率
self.timer_camera = QtCore.QTimer()
# 定时到了,回调 self.show_image
self.timer_camera.timeout.connect(self.show_image)
# 初始化模型
self.initialize_model()
# 要处理的视频帧图片队列,目前就放1帧图片
self.frameToAnalyze = []
self.frame_timestamp_ms = int(time.time())
# 启动处理视频帧独立线程(设置未守护线程)
Thread(target=self.frameAnalyzeThreadFunc, daemon=True).start()
def initialize_model(self):
# 创建人脸检测器,注意running_mode设置为VIDEO模式
VisionRunningMode = mp.tasks.vision.RunningMode
base_options = python.BaseOptions(model_asset_path='blaze_face_short_range.tflite')
options = vision.FaceDetectorOptions(base_options=base_options, running_mode=VisionRunningMode.VIDEO)
self.detector = vision.FaceDetector.create_from_options(options)
def setupUI(self):
self.resize(1200, 800)
self.setWindowTitle('人脸检测系统')
# 设置窗口在屏幕中间
availableGeometry = QtGui.QGuiApplication.primaryScreen().availableGeometry()
window = self.frameGeometry()
window.moveCenter(availableGeometry.center())
self.move(window.topLeft())
# central Widget
centralWidget = QtWidgets.QWidget(self)
self.setCentralWidget(centralWidget)
# central Widget 里面的 主 layout
mainLayout = QtWidgets.QVBoxLayout(centralWidget)
# 界面的上半部分 : 图形展示部分
topLayout = QtWidgets.QHBoxLayout()
self.label_ori_video = QtWidgets.QLabel(self)
self.label_treated = QtWidgets.QLabel(self)
self.label_ori_video.setMinimumSize(520, 400)
self.label_treated.setMinimumSize(520, 400)
self.label_ori_video.setStyleSheet('border:1px solid #D7E2F9;')
self.label_treated.setStyleSheet('border:1px solid #D7E2F9;')
topLayout.addWidget(self.label_ori_video)
topLayout.addWidget(self.label_treated)
mainLayout.addLayout(topLayout)
# 界面下半部分: 输出框 和 按钮
groupBox = QtWidgets.QGroupBox(self)
bottomLayout = QtWidgets.QHBoxLayout(groupBox)
self.textLog = QtWidgets.QTextBrowser()
bottomLayout.addWidget(self.textLog)
mainLayout.addWidget(groupBox)
btnLayout = QtWidgets.QVBoxLayout()
self.videoBtn = QtWidgets.QPushButton('🎞️视频文件')
self.camBtn = QtWidgets.QPushButton('📹摄像头')
self.stopBtn = QtWidgets.QPushButton('🛑停止')
btnLayout.addWidget(self.videoBtn)
btnLayout.addWidget(self.camBtn)
btnLayout.addWidget(self.stopBtn)
bottomLayout.addLayout(btnLayout)
def startVideo(self):
# 选择检测的视频路径
options = QtWidgets.QFileDialog.Options()
filePath, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select Video File", "", "Video Files (*.mp4 *.avi)",
options=options)
if filePath:
self.textLog.clear()
self.textLog.append('目前检测视频的路径:' + filePath)
self.cap = cv2.VideoCapture(filePath)
if not self.cap.isOpened():
print("startVideo中摄像头不能打开")
return
if self.timer_camera.isActive() == False: # 若定时器未启动
self.timer_camera.start(25)
print('startVideo 中开启了定时器')
# 摄像头按钮绑定的事件
def startCamera(self):
# 在 windows上指定使用 cv2.CAP_DSHOW 会让打开摄像头快很多,
# 在 Linux/Mac上 指定 V4L, FFMPEG 或者 GSTREAMER
self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not self.cap.isOpened():
print("startCamera中摄像头不能打开")
return
self.textLog.clear()
self.textLog.append('当前正在使用摄像头进行人脸检测')
if self.timer_camera.isActive() == False: # 若定时器未启动
self.timer_camera.start(25)
# 定时读取图像,并进行处理
def show_image(self):
ret, frame = self.cap.read() # 从视频流中读取
if not ret:
return
# 把读到的16:10帧的大小重新设置
frame = cv2.resize(frame, (520, 400))
# 视频色彩转换回RGB,OpenCV images as BGR
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
qImage = QtGui.QImage(frame.data, frame.shape[1], frame.shape[0],
QtGui.QImage.Format.Format_RGB888) # 变成QImage形式
# 往显示视频的Label里 显示QImage
self.label_ori_video.setPixmap(QtGui.QPixmap.fromImage(qImage))
# 如果当前没有处理任务
if len(self.frameToAnalyze) < 1:
self.frameToAnalyze.append(frame)
# 单独线程进行人脸检测
def frameAnalyzeThreadFunc(self):
while True:
if not self.frameToAnalyze:
time.sleep(0.01)
continue
numpy_frame_from_opencv = self.frameToAnalyze.pop(0)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=numpy_frame_from_opencv)
# 使用人脸检测器检测单张图像
detection_result = self.detector.detect_for_video(mp_image, self.frame_timestamp_ms)
# 可视化人脸bbox及6个关键点
annotated_image = visualize(numpy_frame_from_opencv, detection_result)
img = cv2.cvtColor(annotated_image, cv2.COLOR_RGB2BGR)
# 变成QImage形式
qImage = QtGui.QImage(img.data, img.shape[1], img.shape[0],
QtGui.QImage.Format.Format_BGR888)
# 往显示Label里 显示QImage
self.label_treated.setPixmap(QtGui.QPixmap.fromImage(qImage))
time.sleep(0.005)
self.frame_timestamp_ms += 500
# 停止按钮绑定的事件
def stop(self):
self.timer_camera.stop() # 关闭定时器
self.cap.release() # 释放视频流
self.label_ori_video.clear() # 清空视频显示区域
self.label_treated.clear() # 清空视频显示区域
print('关闭人脸检测系统')
if __name__ == '__main__':
app = QtWidgets.QApplication()
window = MWindow()
window.show()
app.exec()
检测结果如下:
2.3 实时流的人脸检测
- 需要设置running_mode为VisionRunningMode.LIVE_STREAM
- 这里实现一个回调函数result_callback,用来将检测结果输入到output.avi文件中
- 检测是异步检测detector.detect_async()
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import cv2
import time
from utils import visualize
# 创建一个视频编解码器
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(filename='output.avi', fourcc=fourcc, fps=20.0, frameSize=(640, 480))
# 回调函数,将检测的结果保存为视频
FaceDetectorResult = mp.tasks.vision.FaceDetectorResult
def print_result(result: FaceDetectorResult, output_image: mp.Image, timestamp_ms: int):
if cap.isOpened():
annotated_image = visualize(img, result)
out.write(annotated_image)
# 创建人脸检测器
VisionRunningMode = mp.tasks.vision.RunningMode
base_options = python.BaseOptions(model_asset_path='blaze_face_short_range.tflite')
options = vision.FaceDetectorOptions(base_options=base_options
, running_mode = VisionRunningMode.LIVE_STREAM
, result_callback = print_result
)
detector = vision.FaceDetector.create_from_options(options)
cap = cv2.VideoCapture(0)
frame_timestamp_ms = int(time.time())
while True:
if cap.isOpened():
# 从相机从捕获一帧图片
_, img = cap.read()
cv2.imshow('original', img)
# 将图像从BGR颜色空间转换为RGB颜色空间
numpy_frame_from_opencv = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=numpy_frame_from_opencv)
# 异步检测
detector.detect_async(mp_image, int(frame_timestamp_ms))
# 输入esc结束捕获
if cv2.waitKey(25) == 27:
print('exited')
break
frame_timestamp_ms += 25
cap.release()
out.release()
cv2.destroyAllWindows()
上文所用的检测模型、图片及视频素材也已经上传到仓库。
参考博客连接:
人脸检测方案BlazeFace解读