这次我们来看一个把树莓派5当嵌入式主机、把深度相机当测量探头、把点云算法当量尺的完整方案:无接触 3D 智能腰臀围测量仪。它的核心不是“拉软尺”,也不是让测量员贴身去量,而是让被测者站到设备前面,3D 相机采集人体表面点云,后端通过点云切片、轮廓提取和周长计算,直接输出腰围、臀围数据。整条链路在本地完成,树莓派5 既是工控主机,同时也是 Web UI 和 API 服务节点。
先给结论:这个方向适合做服装远程尺码、健身房体测、健康筛查、康复随访这类偏“无接触体表尺寸”的设备原型。树莓派5 的优势很明确,4 核 Cortex-A76、8GB 内存版本可以撑住 Python 视觉栈,USB 3.0 可以接结构光或 ToF 深度相机,HDMI 接一体机屏幕,GPIO 还能接人体感应和补光灯控制。相比 x86 工控机,它的体积、功耗和部署灵活性更贴近“嵌入式一体机”的产品形态。
这篇文章会把整套系统的可复制方案拆开来讲,包括一体机硬件组成、树莓派5 系统环境准备、深度相机接入、点云转 3D 坐标、腰臀围切片算法流程、API 接口、批量测量模式、性能观察和常见坑。如果你正在做人体尺寸测量、体围扫描或者树莓派视觉一体机,这篇可以直接当搭建清单用。
阅读门槛不高。需要你熟悉一点 Python、Linux 基础命令,手里有一块树莓派5 和一台 USB 3D 相机。没有 GPU 也能跑,因为整个测量核心主要吃 CPU 和内存,单帧点云处理对树莓派5 的压力可控。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 测量方式 | 无接触 3D 点云切片,通过相机采集人体表面,不接触被测者 |
| 主控平台 | 树莓派5,建议 8GB 内存版本,Ubuntu/Debian/Raspberry Pi OS 均可 |
| 传感器输入 | USB 3.0 结构光或 ToF 深度相机,需能输出深度图和彩色图 |
| 软件栈 | Python3、NumPy、OpenCV、SciPy,可选 Open3D/PCL 辅助显示 |
| 对外能力 | 本地 Web UI、REST API、人员编号记录、CSV/SQLite 落盘 |
| 批量任务 | 支持人员排队逐个测量,结果按编号写入记录文件 |
| 硬件扩展 | GPIO 可接人体感应开关、拍照按钮、补光灯继电器 |
| 隐私策略 | 默认不保存原始彩色影像,体围数据匿名化处理 |
| 测量精度 | 需现场标定,先用标准人体模型和软尺结果做误差校准 |
| 不适用边界 | 非医疗器械,不适合医疗诊断级测量 |
这套能力里,最容易先验证的是“深度相机能否输出干净点云”和“同一高度切片轮廓是否稳定”。只要这两步跑通,后续接口和批量任务基本是水到渠成。
2. 适用场景与使用边界
适合用无接触 3D 腰臀围测量仪的场景有几类。
第一类是服装行业远程尺码采集。用户可以站在设备前扫一下,得到净体腰围、臀围,避免门店里人工贴身测量带来的尴尬和误差。第二类是健身房和运动康复。健身教练需要定期记录会员体围变化,无接触方案比卷尺更高效,还能留存每次测量的体围曲线。第三类是健康体检初筛,部分体检中心需要快速记录腰围、臀围并计算腰臀比,这种设备能减少人工操作。
不适合的场景也要说清楚。首先它不适合医疗诊断级测量,不能替代医生或营养师的专业工具。其次它要求被测者能够保持站立姿态并在设备前停留数秒,婴幼儿、无法站立的人就不适用。最后,单相机方案只能覆盖一个方向的人体表面,对侧面凹陷区域会有盲区。如果对精度要求很高,建议后期加第二台或第三台相机做点云融合,而不是只靠单相机硬算。
隐私和合规是必须重点处理的环节。腰围、臀围、体重这类数据属于个体健康信息,采集前要取得被测者同意。原始深度图和彩色图里可能包含人脸和周边环境,系统默认只保留测量数值和人员编号,不保留原图。如果为了算法优化需要保存原始点云,至少要做人脸区域遮盖和匿名化,数据存储在本地,不随意上传。
3. 整体架构与硬件选型
这套“树莓派5 嵌入式一体机”的方案,整机架构可以分成四层:感知层、主控层、算法层、服务层。
- 感知层:3D 深度相机 + 人体感应传感器。相机负责出深度图和彩色图,人体感应器负责判断是否有人靠近,从而触发测量流程。
- 主控层:树莓派5。运行操作系统、相机 SDK、Python 环境,也负责 GPIO 输入输出和供电管理。
- 算法层:点云预处理、人物区域提取、腰臀水平定位、切片轮廓周长计算。
- 服务层:本地 Web UI 和 REST API,提供给触屏或第三方体检系统调用。
硬件组成可以参考这张表。
| 硬件 | 选型要点 | 作用 |
|---|---|---|
| 树莓派5 8GB | 内存尽量选 8GB,跑 Python 视觉栈更从容 | 系统主控、算法运算、接口服务 |
| 3D 深度相机 | 优先 USB 3.0 接口,提供深度 SDK | 输出深度图、彩色图或点云 |
| 触摸显示屏 | HDMI 显示 + USB 触摸 | 现场操作界面 |
| 补光灯 | 可接 GPIO 或继电器 | 改善环境光照,减少深度图暗部噪点 |
| 稳定电源/UPS | 树莓派5 建议 5V 5A 供电 | 防止相机启动瞬间电流不足 |
| 机柜或支架 | 固定相机高度和角度 | 保证测量位置一致 |
树莓派5 的 GPIO 引脚在这套设备里不是必须的,但很有用。你可以把一个人体红外感应模块接到 GPIO,有人在设备前站定时自动触发“开始测量”;也可以在测量开始时通过 GPIO 点亮补光灯,结束后关闭。这样能降低相机一直运行带来的发热和功耗。
相机型号不在这里绑定死,因为实际项目里有人用结构光相机,有人用 ToF 相机。只要是能输出深度图并且官方有 Linux/ARM64 Python SDK 的相机就能接入。选型时优先看三件事:是否支持树莓派/ARM64 平台、是否提供 Python API、深度分辨率是否支持至少 640x480@15fps。
4. 环境准备与前置条件
开始之前先把树莓派5 的系统准备好。
我推荐直接用官方适配的 Raspberry Pi OS Bookworm 64 位版本,或者 Ubuntu 24.04 Server 这类新镜像。这里说一句:如果搜资料看到“树莓派5 上用 Ubuntu 22.04”,复制前要谨慎,树莓派5 官方适配是从较新的内核版本才开始的,硬装 Ubuntu 22.04 很容易卡在内核、蓝牙和相机驱动上。想省时间就选官方适配的系统。
系统安装完成后,先做源更新和基础包安装。如果树莓派网络下载软件包比较慢,可以把 APT 源切换成清华镜像源,注意选择与当前系统版本匹配的源地址。
sudo apt update && sudo apt upgrade -y sudo apt install -y python3-pip python3-venv git build-essential \ python3-opencv python3-numpyPython 环境建议建一个虚拟环境,避免把系统 Python 弄乱。后面所有依赖都装进measure_env里。
mkdir -p ~/body-measure cd ~/body-measure python3 -m venv measure_env source measure_env/bin/activate pip install numpy opencv-python scipy flask fastapi uvicorn如果你计划用 Open3D 做点云可视化和网格处理,可以在虚拟环境里继续安装 open3d。但要注意一点:树莓派是 ARM64 架构,部分 Open3D pip 包可能没有对应的 aarch64 wheel,装不上时不要硬刚,可以去掉 Open3D,只用 NumPy/CV 做后台处理,可视化放到电脑端完成。
环境检查项整理如下。
| 检查项 | 要求 |
|---|---|
| 系统架构 | aarch64/arm64 |
| Python | 3.9 以上 |
| numpy / opencv | 能 import 成功 |
| 相机权限 | 当前用户能访问 /dev/video* |
| 磁盘空间 | 至少留 10GB,存放日志和测试素材 |
5. 3D 相机接入与点云采集
环境准备好后,先验证相机能不能被树莓派5 识别。把深度相机插到树莓派5 的 USB 3.0 蓝色接口上,然后执行 lsusb 看设备:
lsusb ls /dev/video*如果设备出现在 lsusb 里但没有生成 /dev/video*,多半是相机驱动或权限问题。不同相机的驱动方式差别很大,结构光相机往往需要先安装厂商 SDK,ToF 相机可能直接走 UVC 协议。以厂商官方文档为准。
相机接入后,先用厂商提供的 viewer 工具验证深度图。没有 viewer 时,直接用 Python SDK 读取一帧深度图和彩色图,然后保存成本地图片。这时候能看到正常深度影像,说明相机 OK。
5.1 深度图转 3D 点云的通用公式
相机输出的深度图本质是一张二维图,每个像素值代表该点到相机平面的距离。要知道点在 3D 空间中的坐标,必须结合相机内参和外参。通用的转换公式如下:
import numpy as np def depth_to_points(depth_img, fx, fy, cx, cy, depth_scale=0.001): """ depth_img: H x W 的深度图, 单位毫米或由 depth_scale 控制 fx, fy, cx, cy: 相机内参 depth_scale: 深度像素值换算成米的倍率 返回 N x 3 的点云数组 """ h, w = depth_img.shape v, u = np.indices((h, w)) # 深度像素值可能是 uint16, 先转 float z = depth_img.astype(np.float32) * depth_scale x = (u - cx) * z / fx y = (v - cy) * z / fy points = np.stack([x, y, z], axis=-1) # 去掉深度为 0 的无效点 valid = z > 0 return points[valid]这段代码的逻辑在所有深度相机上都适用。不同厂商 SDK 的差异只在内参获取方式、深度单位、是否做好彩色图和深度图对齐。如果你的相机会输出彩色图,建议先把彩色图和深度图对齐,再做点云生成,这样后面可以直接用彩色图来定位测量水平线。
5.2 固定机位的人体背景滤除
树莓派5 算力有限,不要把所有背景点都送进算法。固定机位测量时,人体大致站在相机前方 1 米左右,可以先做一个深度范围裁剪,把 0.5 米以内、2 米以远的点全部去掉。
def crop_by_depth(points, z_min=0.4, z_max=2.0): mask = (points[:, 2] > z_min) & (points[:, 2] < z_max) return points[mask]如果现场有复杂背景,单纯深度裁剪不够。建议在相机前留出较空的测量区,或者在算法里加一个 2D 人体检测框,先锁定人体在画面中的位置,再取可见深度点。自己训练 YOLOv5 系列模型时,可以先导出 ONNX,再通过 OpenCV DNN 或 NCNN 在树莓派 CPU 上运行。只做人体位姿粗定位的话,不需要高帧率,一秒一两帧就够。
6. 腰臀围测量算法流程与效果验证
点云拿到手之后,真正的难点是“在哪里算腰围,在哪里算臀围,以及怎么把点云周长变成贴近软尺的体表长度”。
6.1 腰围和臀围在点云里的定义
从人体测量学角度看,腰围通常指肋弓下缘与髂嵴之间最细处的水平围长;臀围则是臀部最丰满处的水平围长。这些位置在点云里没有明确的颜色标签,需要先用人体关键点或人工参考来缩小范围,再在局部区域搜索。
工程上比较稳的方式是混合定位:
- 先通过 2D 彩色图人体关键点模型,获得髋关节、肚脐等参考点的高度。
- 根据参考点高度在点云中圈出“腰部可能区域”和“臀部可能区域”。
- 在腰部区域内沿垂直方向每隔 5-10mm 做一个水平切片,计算轮廓周长。
- 取腰部区域内周长最接近极小值的位置,作为腰围测量高度。
- 在臀部区域内取周长最大位置,作为臀围测量高度。
这样做的原因是腰腹之间最细处不一定正好落在肚脐高度,臀围也不一定刚好在关键点高度,只有通过连续搜索才能找到实际表面变化。
6.2 水平切片轮廓周长计算
核心算法是:把某个高度附近的一组 3D 点投影到水平面,得到一堆 2D 点,然后用扇形分桶法取该角度方向的表层点,最后沿角度顺序计算折线周长。
下面给出一段示意代码,能用,但工程落地时还需要结合点云密度和噪声做平滑。
import numpy as np def body_slice_circumference(points, z_center, z_half=0.02, n_bins=72): """ 在 z_center 附近取厚度约 2*z_half 的点云切片, 将点投影到水平面, 按角度等分成 n_bins 个扇区, 每个扇区取距离质心最远的点作为表面包络点, 再计算扇形包络点的多边形周长。 points: N x 3 的点云, z 轴近似表示身体高度方向 返回弧长估算值, 单位与 points 一致 """ mask = (points[:, 2] > z_center - z_half) & (points[:, 2] < z_center + z_half) slice_pts = points[mask][:, :2] if len(slice_pts) < 20: raise RuntimeError("该高度切片有效点数过少") # 用中位数作为中心, 降低离群点干扰 cx, cy = np.median(slice_pts, axis=0) center = np.array([cx, cy]) # 距离中心太远的点可能是背景, 先剔除 dist = np.linalg.norm(slice_pts - center, axis=1) slice_pts = slice_pts[(dist > 0.05) & (dist < 1.0)] angles = np.arctan2(slice_pts[:, 1] - cy, slice_pts[:, 0] - cx) radii = np.linalg.norm(slice_pts - center, axis=1) bin_idx = np.floor((angles + np.pi) / (2 * np.pi / n_bins)).astype(int) bin_idx = bin_idx % n_bins border_points = [] for b in range(n_bins): pts_bin = slice_pts[bin_idx == b] if len(pts_bin) > 2: d = np.linalg.norm(pts_bin - center, axis=1) border_points.append(pts_bin[np.argmax(d)]) if len(border_points) < 12: raise RuntimeError("包络点数量不足,无法计算可靠周长") ordered = np.array(border_points) order_angles = np.arctan2(ordered[:, 1] - cy, ordered[:, 0] - cx) ordered = ordered[np.argsort(order_angles)] perimeter = 0.0 n = len(ordered) for i in range(n): p1 = ordered[i] p2 = ordered[(i + 1) % n] perimeter += np.linalg.norm(p1 - p2) return perimeter这段代码的含义是先取表面某高度的一个“横截面外轮廓”,然后按角度找最外层点并算周长。如果你的被测者体形比较均匀,这种方法得到的近似值可以作为基础结果。真正做产品时,不建议直接用简单凸包算法,因为人的腰臀横截面并不是标准凸形,凸包会把腹部两侧的凹陷段也算大。
6.3 功能测试与效果验证
算法写到能输出数值后,先不要急着接第三方系统,应该做一轮对比验证。验证流程建议这样安排:
- 找标准人体模型或几位测试者,保持站立姿势。
- 先用软尺在人工标记位置测量腰围和臀围,记录为基准值。
- 让同一人站在树莓派5 测量设备前,重复测量 3 次。
- 对比点云结果与软尺结果,记录每次偏差。
- 如果点云结果系统偏大或偏小,不要先调阈值,先检查切片高度定位和噪点。
这里要提醒一点:点云切片周长指的是人体表面某个水平高度切面外轮廓的弧长,软尺量的是体表软组织的贴体长度。两者天然存在细小差异,实际产品都要建立“相机测量值 → 标准软尺值”的校正关系,也就是线性回归修正。不能直接说“算法算出来多少就是真实腰围”。
判断算法是否成功的标准有三个:
- 同一人重复测三次,结果是否稳定在可接受范围内。
- 腰围搜索结果是否落在肉眼可见的最细位置附近。
- 臀围搜索结果是否落在臀部最丰满位置附近。
如果连续测量结果抖动很大,先检查是不是点云里混入了手、手臂或者衣服下摆,而不是急着优化周长公式。
7. 接口 API 与批量任务模式
测量一体机不可能只在树莓派本地 Python 脚本里敲命令使用。实际产品里,触摸屏需要一个交互页面,后台需要把测量记录同步出去,这时候就要把测量能力封装成 REST API。
下面用一个 Flask 示例说明接口服务怎么写。实际使用时,把measure_current_person换成你自己的测量函数即可。
from flask import Flask, request, jsonify from measure_core import measure_current_person app = Flask(__name__) @app.route("/api/measure", methods=["POST"]) def measure(): data = request.get_json(force=True) person_id = data.get("person_id", "unknown") # person_id 用于编号,不要把姓名直接放在日志里 result = measure_current_person(person_id=person_id) return jsonify({ "person_id": person_id, "waist_cm": round(result["waist_cm"], 1), "hip_cm": round(result["hip_cm"], 1), "status": "ok" }) @app.route("/api/records", methods=["GET"]) def records(): # 查询历史记录,可以对接 SQLite return jsonify({"query": "ok"}) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)服务启动后,用 curl 测试接口:
curl -X POST http://127.0.0.1:8000/api/measure \ -H "Content-Type: application/json" \ -d '{"person_id": "T001", "clothing": "贴身衣物"}'期望返回类似这样的 JSON:
{ "person_id": "T001", "waist_cm": 86.4, "hip_cm": 98.2, "status": "ok" }如果需要在体检或服装门店批量使用,可以加一个“人员队列”模式。最省事的做法是准备一个 CSV 人员列表,脚本按顺序读取编号,每个编号执行一次测量并写入结果。树莓派5 的 CPU 不适合并行处理多个深度流,所以测量任务建议串行执行。
# 批量测量脚本示意 python batch_measure.py --input ./person_list.csv --output ./result.csv批量模式要特别处理失败重试。比如某次没有检测到人体,脚本不应该直接跳过,而应该等 3 秒后重试,重试 3 次仍失败才记录 error。否则一个人没站好,后面所有记录都错位。
树莓派一体机开机自启时,建议用 systemd 托管 Flask 服务。创建/etc/systemd/system/measure-api.service:
[Unit] Description=Body Measure API Service After=network.target [Service] User=pi WorkingDirectory=/home/pi/body-measure ExecStart=/home/pi/body-measure/measure_env/bin/python app.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target然后执行:
sudo systemctl enable --now measure-api sudo systemctl status measure-api这样树莓派5 上电后,测量