身体检测
一个 Flutter 插件,可在 iOS/Android 平台上使用 MLKit,通过姿势检测和自拍分割 API,为静态图像和实时摄像头流启用身体姿势和遮罩检测。在使用实时摄像头检测时,它在原生端运行摄像头图像采集和检测器,这比单独使用 Flutter 摄像头插件更快。
功能
- 身体姿势检测。
- 身体遮罩检测(自拍分割)。
- 在单个图像或实时摄像头馈送上运行。
- 摄像头图像采集和检测器在原生端一起运行,这比为两者使用单独的插件并在它们之间传递数据更快。
- 使用 MLKit 进行检测,因此共享其优缺点。
安装
iOS
最低要求的 iOS 版本为 10.0,因此如果针对较低的目标进行编译,请确保在调用此库的函数之前以编程方式检查 iOS 版本。
为了能够使用基于摄像头的检测,您需要使用 Xcode 在 ios/Runner/Info.plist 文件中添加一行
隐私 - 相机使用说明并设置使用说明消息。
如果以纯文本编辑文件,请添加以下内容
<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
Android
在 android/app/build.gradle 文件中将最低 Android SDK 版本设置为 21(或更高)。
android {
defaultConfig {
minSdkVersion 21
}
}
要使用摄像头,您需要将 uses-permission 声明添加到 android/app/src/main/AndroidManifest.xml manifest 文件中
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="YOUR_PACKAGE_NAME">
<uses-permission android:name="android.permission.CAMERA"/>
...
用法
该插件有两种模式,您可以对单个图像进行姿势或身体遮罩检测,或者启动摄像头流并在其运行时启用或禁用任一检测器。
单张图像检测
最简单的用例是在单个图像上检测姿势或身体遮罩。该插件接受 PNG 编码的图像并将其发送到原生端,在那里它使用 Google MLKit 的 Vision API 来完成繁重的工作。之后,结果会传回 Flutter。
import 'package:body_detection/body_detection.dart';
import 'package:body_detection/png_image.dart';
PngImage pngImage = PngImage.from(bytes, width: width, height: height);
final pose = await BodyDetection.detectPose(image: pngImage);
final bodyMask = await BodyDetection.detectBodyMask(image: pngImage);
该插件为 Flutter 的 Image widget 提供了一个扩展,可将其转换为 PNG 格式,因此您可以使用该 widget 支持的任何图像源作为输入:本地资源、网络、内存等。
import 'package:body_detection/body_detection.dart';
import 'package:body_detection/png_image.dart';
import 'package:flutter/widgets.dart';
void detectImagePose(Image source) async {
PngImage? pngImage = await source.toPngImage();
if (pngImage == null) return;
final pose = await BodyDetection.detectPose(image: pngImage);
...
}
如果您出于布局目的需要编码的 PNG 图像的大小(例如图像的纵横比),您可以从返回的 PngImage 对象中获取。
import 'package:body_detection/png_image.dart';
import 'package:flutter/widgets.dart';
Image source;
PngImage? pngImage = await source.toPngImage();
final imageSize = pngImage != null
? Size(pngImage!.width.toDouble(), pngImage!.height.toDouble())
: Size.zero;
BodyDetection.detectPose 调用返回的 Pose 对象包含 MLKit 检测器返回的地标列表。每个地标的结构都遵循 MLKit 建立的结构。
final pose = await BodyDetection.detectPose(image: pngImage);
for (final landmark in pose!.landmarks) {
// The detector estimate of how likely it is that the landmark is within the image's frame.
double inFrameLikelihood = landmark.inFrameLikelihood;
// Position of the landmark in image plane coordinates with z value estimated by the detector.
Point3d position = landmark.position;
// One of the 33 detectable body landmarks.
PoseLandmarkType type = landmark.type;
}
您可以使用自定义绘制器绘制姿势
class PosePainter extends CustomPainter {
PosePainter({
required this.pose,
required this.imageSize,
});
final Pose pose;
final Size imageSize;
final circlePaint = Paint()..color = const Color.fromRGBO(0, 255, 0, 0.8);
final linePaint = Paint()
..color = const Color.fromRGBO(255, 0, 0, 0.8)
..strokeWidth = 2;
@override
void paint(Canvas canvas, Size size) {
final double hRatio =
imageSize.width == 0 ? 1 : size.width / imageSize.width;
final double vRatio =
imageSize.height == 0 ? 1 : size.height / imageSize.height;
offsetForPart(PoseLandmark part) =>
Offset(part.position.x * hRatio, part.position.y * vRatio);
for (final part in pose.landmarks) {
// Draw a circular indicator for the landmark.
canvas.drawCircle(offsetForPart(part), 5, circlePaint);
// Draw text label for the landmark.
TextSpan span = TextSpan(
text: part.type.toString().substring(16),
style: const TextStyle(
color: Color.fromRGBO(0, 128, 255, 1),
fontSize: 10,
),
);
TextPainter tp = TextPainter(text: span, textAlign: TextAlign.left);
tp.textDirection = TextDirection.ltr;
tp.layout();
tp.paint(canvas, offsetForPart(part));
}
// Draw connections between the landmarks.
final landmarksByType = {for (final it in pose.landmarks) it.type: it};
for (final connection in connections) {
final point1 = offsetForPart(landmarksByType[connection[0]]!);
final point2 = offsetForPart(landmarksByType[connection[1]]!);
canvas.drawLine(point1, point2, linePaint);
}
}
...
}
并在您的 widget 树中使用它
@override
Widget build(BuildContext context) {
// Use ClipRect so that custom painter doesn't draw outside of the widget area.
return ClipRect(
child: CustomPaint(
child: _sourceImage,
foregroundPainter: PosePainter(
pose: _detectedPose,
imageSize: _imageSize,
),
),
);
}
有关详细信息,请参阅 Google MLKit 姿势检测 API 文档。
检测到的身体遮罩以双精度值缓冲区返回,长度为 width * height,表示特定像素覆盖的区域是识别为身体的置信度。身体遮罩的大小可能与输入图像的大小不同,因为这可以加快计算速度,并且在较慢的设备上也能以可接受的性能运行。
要显示遮罩,您可以例如使用缓冲区值作为 alpha 分量,将缓冲区解码为 Dart 图像。
final mask = await BodyDetection.detectBodyMask(image: pngImage);
final bytes = mask.buffer
.expand((it) => [0, 0, 0, (it * 255).toInt()])
.toList();
ui.decodeImageFromPixels(Uint8List.fromList(bytes), mask.width, mask.height, ui.PixelFormat.rgba8888, (image) {
// Do something with the image, for example set it as a widget's state field so you can pass it to a custom painter for drawing in the build method.
});
然后,例如,要绘制透明的蓝色叠加层,您可以使用如下自定义绘制器:
class MaskPainter extends CustomPainter {
MaskPainter({
required this.mask,
});
final ui.Image mask;
final maskPaint = Paint()
..colorFilter = const ColorFilter.mode(
Color.fromRGBO(0, 0, 255, 0.5), BlendMode.srcOut);
@override
void paint(Canvas canvas, Size size) {
canvas.drawImageRect(
mask,
Rect.fromLTWH(0, 0, mask.width.toDouble(), mask.height.toDouble()),
Rect.fromLTWH(0, 0, size.width, size.height),
maskPaint);
}
}
并与原始图像一起在您的 widget 树中使用
@override
Widget build(BuildContext context) {
return CustomPaint(
child: _sourceImage,
foregroundPainter: MaskPainter(mask: _maskImage),
);
有关详细信息,请参阅 Google MLKit 自拍分割 API 文档。
您可以在 插件的存储库 中查看完整的示例。
摄像头馈送检测
除了单张图像检测外,该插件还支持从摄像头馈送中实时检测姿势和/或身体遮罩。摄像头图像采集和检测都在原生端运行,因此我们消除了如果为每个单独的 Flutter 插件使用单独的插件所需要的数据序列化成本。
要启动和停止摄像头流,请使用以下方法,并为摄像头帧或检测结果准备就绪时传入回调。
await BodyDetection.startCameraStream(
onFrameAvailable: (ImageResult image) {},
onPoseAvailable: (Pose? pose) {},
onMaskAvailable: (BodyMask? mask) {},
);
await BodyDetection.stopCameraStream();
检测器默认禁用。要启用或禁用特定检测器,请使用以下方法。
await BodyDetection.enablePoseDetection();
await BodyDetection.disablePoseDetection();
await BodyDetection.enableBodyMaskDetection();
await BodyDetection.disableBodyMaskDetection();
您可以在启动摄像头流之前运行它们,也可以在流已运行时运行它们。
要显示摄像头图像,您可以创建一个字节图像。
void _handleCameraImage(ImageResult result) {
// Ignore callback if navigated out of the page.
if (!mounted) return;
// To avoid a memory leak issue.
// https://github.com/flutter/flutter/issues/60160
PaintingBinding.instance?.imageCache?.clear();
PaintingBinding.instance?.imageCache?.clearLiveImages();
final image = Image.memory(
result.bytes,
gaplessPlayback: true,
fit: BoxFit.contain,
);
setState(() {
_cameraImage = image;
_imageSize = result.size;
});
}
ImageResult 的 bytes 字段包含来自摄像头的 JPEG 编码帧,Image.memory() 工厂构造函数接受其作为输入。
示例应用
运行姿势检测
运行身体遮罩检测
未来发展
该库在将图像从摄像头编码并传递给 Flutter 时对其进行序列化。这使得可以轻松访问图像数据,从而可以进行额外的处理、将帧保存到存储或通过网络发送它们,但这也增加了开销,如果它仅传递一个纹理 ID,那么可以使用与摄像头插件相同的方式使用 Texture widget 进行显示,则可以避免这种开销。
它尚未提供任何配置选项,并依赖于默认设置(“准确”姿势检测模型、自拍分割的原始大小遮罩、使用第一个可用的前置摄像头、默认摄像头图像大小、JPEG 编码质量设置为 60)。让用户选择这些设置以匹配他们的用例将是很好的。
异常处理尚未统一,并且可以做得更好。
通知
此库尚未经过实战测试。由于它是早期版本,其功能和 API 可能会发生变化。如果您遇到问题或想请求功能,请随时在项目的 GitHub 页面上 打开一个 issue。
MLKit 的姿势检测和自拍分割仍处于测试阶段,因此此软件也应被视为处于测试阶段。

