在行空板上部署MediaPipe实现实时人像分割与抠图

在行空板上部署MediaPipe实现实时人像分割与抠图 1. 项目缘起为什么要在行空板上折腾MediaPipe抠图最近在做一个智能交互展项需要在资源受限的嵌入式设备上实时处理摄像头画面把人物从背景里干净地“抠”出来。一开始想到的是上云或者用高性能工控机跑OpenCVDeeplabv3但成本、延迟和部署复杂度都成了问题。直到我把目光投向了手边的行空板UNIHIKER和Google的MediaPipe一个想法冒了出来能不能在这块小巧的板子上实现一个够用、实时的人物抠图系统行空板是一块基于Linux系统的开源硬件集成了屏幕、按键和丰富的传感器性能对于嵌入式应用来说相当不错。而MediaPipe作为一个跨平台的机器学习管道框架其轻量级模型和高效的推理引擎是出了名的。它的Selfie Segmentation模型专门为实时人像分割优化模型小巧仅几MB在移动端都能流畅运行。那么把它移植到行空板的ARM架构上理论上完全可行。这个组合的意义在于它把需要强大算力的“抠图”这件事下沉到了边缘设备实现了低延迟、高隐私、离线可用的实时人像处理非常适合教育、互动艺术、轻量级智能终端等场景。2. 行空板开发环境搭建与核心依赖部署要在行空板上跑通MediaPipe第一步就是把环境给配齐了。行空板默认运行的是Debian系统这给我们提供了很大的便利。2.1 系统准备与基础包安装首先通过SSH或者直接使用行空板的终端更新软件源并安装一些基础编译工具和Python环境管理工具。sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip python3-venv git wget curl build-essential cmake我强烈建议使用Python虚拟环境来管理项目依赖避免污染系统环境。cd ~ python3 -m venv mp_env source mp_env/bin/activate激活虚拟环境后命令提示符前会出现(mp_env)的标识。2.2 MediaPipe Python包的安装挑战与解决MediaPipe官方提供了Python轮子wheel但主要是针对x86_64架构和特定的ARM64架构如树莓派。行空板使用的处理器可能需要我们寻找兼容的版本或者从源码编译。最稳妥的方法是安装MediaPipe针对Linux ARM的版本。pip3 install --upgrade pip对于大多数基于ARMv8的行空板可以尝试安装官方的Linux ARM版本。但根据我的实测直接pip install mediapipe可能会因为架构不匹配而失败。一个更可靠的方案是使用由社区维护的、针对特定平台预编译的版本或者利用MediaPipe的“自定义包”功能。不过为了最广泛的兼容性我采用了另一种更直接的方法使用MediaPipe的轻量级解决方案即不安装完整的mediapipe包而是使用其提供的.tflite模型文件结合TensorFlow Lite运行时进行推理。这是因为MediaPipe Selfie Segmentation模型本身就是一个TFLite模型我们可以直接下载并使用它。# 安装TensorFlow Lite运行时 pip3 install tflite-runtime如果tflite-runtime安装遇到问题也可以尝试安装完整的TensorFlow稍重但兼容性好pip3 install tensorflow2.3 模型文件获取与OpenCV安装接下来下载MediaPipe的Selfie Segmentation模型。我们可以从MediaPipe的官方GitHub仓库获取。mkdir -p ~/selfie_segmentation/models cd ~/selfie_segmentation/models wget -O selfie_segmentation.tflite https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmentation/float16/1/selfie_segmentation.tflite这个float16版本的模型在精度和速度上取得了很好的平衡非常适合行空板。同时我们还需要安装OpenCV来处理图像。pip3 install opencv-python-headless这里安装的是headless版本因为它不包含GUI相关的库如GTK在服务器或行空板这种无桌面环境或轻量桌面环境下更节省资源。如果你的应用需要利用行空板自带的屏幕显示图像可能需要额外安装opencv-python包含GUI支持但通常headless版本配合行空板自带的显示接口也是可以的。至此最核心的环境和模型就准备好了。这种“TFLite运行时 独立模型文件”的方式比安装完整的MediaPipe Python包更加轻量依赖更少是嵌入式部署的常见做法。3. 图像分割与抠图的核心代码实现有了模型和环境接下来就是编写代码实现从读取图像到输出抠图结果的完整流程。这个过程可以分为几个关键步骤图像预处理、模型推理、后处理生成掩膜和图像合成。3.1 图像预处理与模型输入适配MediaPipe Selfie Segmentation模型的输入要求是RGB三通道图像尺寸为256x256像素像素值归一化到[0, 1]的浮点数。我们需要将任意尺寸的输入图像比如从行空板摄像头读取的转换到这个格式。import cv2 import numpy as np import tflite_runtime.interpreter as tflite def preprocess_image(image): 将输入图像预处理为模型所需的格式。 Args: image: numpy数组BGR格式OpenCV默认。 Returns: input_data: 形状为(1, 256, 256, 3)的numpy数组RGB格式值范围[0,1]。 original_shape: 原始图像的形状用于后续还原。 # 记录原始尺寸 original_shape image.shape[:2] # (height, width) # 将BGR转换为RGB rgb_image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 缩放到256x256 resized_image cv2.resize(rgb_image, (256, 256)) # 将像素值从[0,255]转换为[0,1]的浮点数 input_data resized_image.astype(np.float32) / 255.0 # 添加批次维度变成 (1, 256, 256, 3) input_data np.expand_dims(input_data, axis0) return input_data, original_shape这里有一个关键点OpenCV默认读入的图像是BGR通道顺序而模型通常期望RGB顺序。cv2.cvtColor这个转换必不可少否则颜色通道错乱会导致模型识别异常。3.2 加载TFLite模型并执行推理我们使用TFLite解释器来加载和运行模型。def initialize_interpreter(model_path): 初始化TFLite解释器。 interpreter tflite.Interpreter(model_pathmodel_path) interpreter.allocate_tensors() return interpreter def run_inference(interpreter, input_data): 执行模型推理。 # 获取输入和输出张量的详细信息 input_details interpreter.get_input_details() output_details interpreter.get_output_details() # 将预处理后的数据设置为输入 interpreter.set_tensor(input_details[0][index], input_data) # 执行推理 interpreter.invoke() # 获取输出结果 # Selfie Segmentation模型输出一个形状为(1, 256, 256, 1)的掩膜 output_data interpreter.get_tensor(output_details[0][index]) return output_dataoutput_data就是模型预测的掩膜mask其值在0到1之间通常经过sigmoid激活越接近1表示该像素属于“人物”的概率越高。3.3 掩膜后处理与图像合成得到的掩膜是256x256大小的我们需要将其还原到原始图像的尺寸并利用它来抠图。def postprocess_mask(mask, original_shape): 将模型输出的掩膜后处理并缩放到原始图像尺寸。 Args: mask: 模型输出形状(1, 256, 256, 1)或(256, 256, 1)。 original_shape: 目标尺寸 (height, width)。 Returns: resized_mask: 形状为(original_height, original_width)的二维数组值范围[0,255] (uint8)。 # 去除批次维度得到(256, 256, 1) mask np.squeeze(mask, axis0) if mask.ndim 4 else mask # 如果模型输出是(256, 256, 1)则压缩为(256, 256) mask np.squeeze(mask, axis-1) if mask.shape[-1] 1 else mask # 将概率值[0,1]转换为[0,255]的灰度图 mask_uint8 (mask * 255).astype(np.uint8) # 缩放到原始图像尺寸 resized_mask cv2.resize(mask_uint8, (original_shape[1], original_shape[0]), interpolationcv2.INTER_LINEAR) return resized_mask def apply_mask_to_image(image, mask, background_color(255, 255, 255)): 使用掩膜将人物从原图中抠出并放置在指定背景色上。 Args: image: 原始BGR图像。 mask: 与image同尺寸的灰度掩膜值越大代表越可能是前景。 background_color: 背景的BGR颜色。 Returns: result: 抠图后的BGR图像。 # 将掩膜归一化到[0, 1]作为alpha通道 alpha mask.astype(np.float32) / 255.0 # 为alpha增加通道维度便于广播 (H, W) - (H, W, 1) alpha np.expand_dims(alpha, axis-1) # 创建纯色背景图 background np.full_like(image, background_color, dtypenp.uint8) # 前景 原图 * alpha foreground image.astype(np.float32) * alpha # 背景 背景色 * (1 - alpha) background_part background.astype(np.float32) * (1 - alpha) # 合成 result (foreground background_part).astype(np.uint8) return resultapply_mask_to_image函数实现了最常见的阿尔法混合。这里background_color可以随意替换比如换成另一张图片就能实现“换背景”的效果。alpha通道的平滑过渡值在0到1之间使得抠图边缘不会生硬这是直接使用二值化阈值如mask 128所不具备的优势。4. 在行空板上实现实时摄像头抠图将上述代码模块整合并接入行空板的摄像头就能实现实时抠图了。行空板通常使用/dev/video0作为摄像头设备。import time def main(): # 初始化 model_path models/selfie_segmentation.tflite interpreter initialize_interpreter(model_path) # 打开摄像头行空板USB摄像头通常是video0 cap cv2.VideoCapture(0) # 可以设置分辨率降低分辨率可以提高帧率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) if not cap.isOpened(): print(无法打开摄像头) return print(开始实时抠图按 q 键退出...) while True: # 读取一帧 ret, frame cap.read() if not ret: print(无法获取帧) break # 记录开始时间以计算FPS start_time time.time() # 1. 预处理 input_data, orig_shape preprocess_image(frame) # 2. 推理 output_mask run_inference(interpreter, input_data) # 3. 后处理得到原始尺寸的掩膜 mask postprocess_mask(output_mask, orig_shape) # 4. 应用掩膜生成抠图结果这里背景设为绿色 result apply_mask_to_image(frame, mask, background_color(0, 255, 0)) # 计算并显示FPS fps 1.0 / (time.time() - start_time) cv2.putText(result, fFPS: {fps:.1f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # 显示结果如果行空板连接了屏幕 cv2.imshow(MediaPipe Selfie Segmentation - UNIHIKER, result) # 按q退出 if cv2.waitKey(1) 0xFF ord(q): break # 释放资源 cap.release() cv2.destroyAllWindows() if __name__ __main__: main()这段代码构成了一个完整的实时抠图应用。在行空板上运行你就能看到摄像头画面中的人像被实时地抠出来背景被替换成了绿色。帧率FPS是衡量实时性的关键指标在行空板上根据分辨率和模型复杂度达到10-20 FPS是很有希望的。注意行空板运行带GUI的OpenCV (cv2.imshow) 需要图形界面环境支持。如果你的行空板运行在纯命令行模式无桌面这部分代码会报错。此时你有两个选择1) 将结果图像保存为文件2) 使用行空板自带的pinpong库或其它方式将图像数据发送到板载屏幕上显示。这涉及到行空板特定的显示API调用需要参考其官方文档。5. 性能优化与效果提升实战技巧在资源受限的行空板上让整个流程跑得又快又好需要一些优化技巧。5.1 推理速度优化输入分辨率与模型选择模型的输入分辨率直接影响推理速度。Selfie Segmentation模型固定输入256x256这本身已经很小。但我们可以从预处理和后处理环节省时间。降低摄像头采集分辨率这是最有效的提速方法之一。如果最终显示或处理不需要太高清的图像直接将摄像头分辨率设为320x240或更低能极大减少需要处理的数据量。cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)探索更轻量模型MediaPipe可能还提供更小的模型变体或者可以尝试将模型量化到int8精度tflite模型。量化模型在保持可接受精度损失的前提下能显著提升在ARM处理器上的推理速度。你需要寻找或自己使用TensorFlow工具量化模型。5.2 抠图边缘优化掩膜平滑与腐蚀膨胀直接使用模型输出的掩膜进行混合有时在头发丝、透明物体边缘处会有锯齿感或残留背景色。高斯模糊平滑边缘对掩膜施加一个轻微的高斯模糊可以使alpha过渡更加平滑自然。def smooth_mask(mask, kernel_size5, sigma1.0): blurred cv2.GaussianBlur(mask.astype(np.float32), (kernel_size, kernel_size), sigma) # 重新归一化到0-255防止模糊导致整体变暗 blurred np.clip(blurred, 0, 255).astype(np.uint8) return blurred在postprocess_mask函数中对resized_mask调用smooth_mask后再返回。形态学操作处理噪点如果掩膜中存在小的空洞人物内部被误判为背景或孤立的噪点背景被误判为人可以使用形态学操作。def denoise_mask(mask, kernel_size3): kernel np.ones((kernel_size, kernel_size), np.uint8) # 先闭运算填充小洞再开运算去除小白点 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) return mask注意形态学操作会改变掩膜形状需谨慎调节核大小。5.3 处理复杂背景与多人场景Selfie Segmentation是一个通用模型在复杂背景或多人场景下效果可能会下降。背景复杂度模型在干净、对比度高的背景下表现最好。如果背景和人物颜色相近比如穿白衣服站在白墙前分割效果会变差。在实际应用中可以通过布光或选择背景来改善。多人场景该模型设计用于分割单个人像“自拍”。对于多人同框它会尝试分割出所有人物作为一个整体前景。如果你需要区分不同的个体则需要换用实例分割模型如MediaPipe Holistic或其它模型但那会复杂和沉重得多。对于行空板处理多人作为一个整体前景是更现实的选择。一个实用的技巧是设置置信度阈值。模型输出的掩膜值是置信度。我们可以通过阈值化来获得一个更“干净”的二值掩膜但会丢失边缘平滑度。一种折衷是使用阈值来生成一个“确定前景”区域然后利用cv2.grabCut算法进行精细优化但这在行空板上计算成本较高。def threshold_mask(mask, low_thresh0.3, high_thresh0.7): 使用双阈值生成三区域掩膜确定前景/确定背景/不确定。 可用于后续的grabCut优化。 h, w mask.shape sure_fg np.zeros((h, w), np.uint8) sure_bg np.zeros((h, w), np.uint8) uncertain np.zeros((h, w), np.uint8) sure_fg[mask high_thresh * 255] 1 sure_bg[mask low_thresh * 255] 1 uncertain[(mask low_thresh*255) (mask high_thresh*255)] 1 return sure_fg, sure_bg, uncertain6. 项目集成与进阶应用思路一个基础的实时抠图系统已经完成。我们可以把它作为核心模块集成到更大的行空板项目中。思路一虚拟背景视频通话将抠出的人像与一张静态图片或动态视频流进行混合实现虚拟背景。在行空板上可以预加载几张背景图通过按键切换。思路二互动艺术装置将抠出的人物轮廓用于交互。例如计算轮廓的中心点或外接矩形用来控制一个屏幕上的虚拟物体如一个随着你移动而移动的光斑。OpenCV的cv2.findContours函数可以很容易地从二值化掩膜中提取轮廓。def get_largest_contour(mask): # 将掩膜二值化 _, binary_mask cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) # 查找轮廓 contours, _ cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 找到面积最大的轮廓 largest_contour max(contours, keycv2.contourArea) # 计算轮廓的矩可以获取质心 M cv2.moments(largest_contour) if M[m00] ! 0: cx int(M[m10] / M[m00]) cy int(M[m01] / M[m00]) return largest_contour, (cx, cy) return None, None思路三姿态触发拍照结合MediaPipe的其他模型如Pose Detection在检测到特定姿势如举手时自动保存一张当前抠好图的照片到行空板的存储中。这就需要同时运行两个轻量级模型对行空板的算力是一个考验可能需要采用交替运行或降低检测频率的策略。部署与打包为了让项目脱离开发环境运行可以将所有依赖和代码打包。使用pip freeze requirements.txt生成依赖列表。对于行空板更彻底的方式是使用Docker构建一个包含所有环境的镜像或者利用行空板支持的应用打包方式创建一个一键运行的脚本或应用。在整个开发过程中最大的体会是“平衡”。在嵌入式设备上精度、速度和资源占用永远是一个需要权衡的三角。MediaPipe Selfie Segmentation模型提供了一个极佳的起点它在三者之间取得了很好的平衡。而行空板作为载体让这个平衡得以在真实的物理世界中实现。从一行行代码到屏幕上实时分离出的人像这个过程本身就是边缘智能魅力的最佳诠释。