☰
基于D435i深度相机的物体尺寸实时测量系统搭建指南
2026/10/6 20:34:09 网站建设 项目流程

1. 从零搭建一套物体尺寸实时测量系统的整体思路

1.1 为什么选D435i而不是普通USB摄像头

普通USB摄像头只能拿到二维彩色图像,想从一张图里算出物体的真实物理尺寸,必须依赖参照物标定或者已知距离,一旦物体前后移动,测量结果就全废了。D435i是Intel RealSense系列里的深度相机,它同时输出彩色图和深度图,深度图里每个像素都带着到相机平面的距离值(单位毫米),这就把“像素尺寸换算成物理尺寸”这件事变得非常直接。

核心原理其实就一句话:物理尺寸 = 像素尺寸 × 深度值 / 焦距。焦距是相机内参,标定一次就能固定下来;深度值由相机实时给出;像素尺寸就是物体在图像里占了多少个像素。三个量凑齐,尺寸就能实时算出来。

D435i相比D415、D455这些型号,优势在于它自带IMU,做机械臂抓取或者移动平台上的测量时可以做姿态补偿。不过如果只是固定位置测尺寸,D415的精度其实更高,因为它的最小深度距离更近、分辨率下的基线更长。选D435i更多是考虑通用性和后续扩展。

1.2 整体方案拆解:从图像到尺寸的完整链路

整套系统我把它拆成五步:

  1. 相机初始化与对齐:启动D435i,把深度图对齐到彩色图坐标系,这样彩色图上每个像素都能查到对应的深度值。
  2. 物体检测与分割:用颜色阈值、轮廓检测或者简单的背景减除,把目标物体从画面里抠出来。
  3. 像素尺寸提取:对分割出的轮廓求最小外接矩形,拿到长和宽的像素值。
  4. 深度值获取:在物体区域内取深度中位数,避免边缘噪声和空洞影响。
  5. 物理尺寸换算与显示:用内参焦距做换算,把结果显示在画面上。

这个链路里最容易出问题的是第2步和第4步。分割不准,像素尺寸就偏;深度取值不对,换算结果能差出好几厘米。后面我会详细讲怎么处理。

1.3 环境准备:Python、pyrealsense2和OpenCV的安装

先说Python环境。我建议用Python 3.8到3.10,太新的版本有时候pyrealsense2的wheel还没跟上。安装方式用conda或者venv都行,我个人习惯用conda,因为科学计算相关的包管理起来省心。

conda create -n realsense python=3.9 conda activate realsense pip install pyrealsense2 opencv-python numpy

pyrealsense2是Intel官方提供的Python绑定,直接pip就能装。如果装不上,去Intel RealSense的GitHub release页面找对应Python版本的whl文件手动装。OpenCV用opencv-python就够了,不需要contrib版本。

注意:pyrealsense2和librealsense的版本要匹配。如果你之前装过librealsense的SDK,pip装的pyrealsense2可能会和系统里的库冲突。最稳妥的做法是在干净的虚拟环境里只装pip包,不要额外装系统级SDK。

验证安装是否成功:

import pyrealsense2 as rs import cv2 import numpy as np print(rs.__version__) print(cv2.__version__)

能打印出版本号就说明环境没问题。如果报No module named 'pyrealsense2',检查一下是不是在正确的虚拟环境里。

1.4 相机内参标定:焦距到底怎么拿

D435i出厂时已经做了标定,内参直接可以从相机里读出来,不需要自己拿棋盘格去标。读取方式:

pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) profile = pipeline.start(config) color_profile = profile.get_stream(rs.stream.color) intrinsics = color_profile.as_video_stream_profile().get_intrinsics() print(f"fx={intrinsics.fx}, fy={intrinsics.fy}") print(f"cx={intrinsics.ppx}, cy={intrinsics.ppy}")

fx和fy就是焦距(单位是像素),cx和cy是主点坐标。640x480分辨率下,D435i的fx通常在600左右。这个值就是换算公式里的分母。

如果你要自己标定,用OpenCV的calibrateCamera配合棋盘格也行,但说实话没必要,出厂内参的精度已经足够做厘米级测量了。自己标定反而容易因为棋盘格不平、拍摄角度不够多而引入更大误差。

2. 核心细节解析与实操要点

2.1 深度图对齐彩色图:为什么必须做

D435i的深度传感器和彩色传感器是分开的,物理位置不同,所以同一时刻拍到的画面视角有偏差。如果不做对齐,你在彩色图上看到的物体位置,和深度图上同一像素位置对应的深度值,可能根本不是同一个点。

对齐操作:

align_to = rs.stream.color align = rs.align(align_to) frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) color_frame = aligned_frames.get_color_frame() depth_frame = aligned_frames.get_depth_frame()

对齐之后,彩色图上像素(u, v)对应的深度值就是depth_frame.get_distance(u, v)。这一步不做,后面所有测量都是错的。

实操心得:对齐会消耗一定的计算资源,帧率会从30fps降到大概25fps左右。如果对实时性要求特别高,可以考虑只在深度图上做检测,然后用深度图的内参换算,但那样就看不到彩色画面了,调试起来不方便。

2.2 物体分割:颜色阈值、轮廓检测与背景减除怎么选

分割方法取决于你的应用场景。我按难度从低到高列三种:

颜色阈值法:适合物体颜色和背景差异大的场景。比如传送带上的红色零件,背景是灰色。用HSV空间做阈值比RGB更稳,因为HSV对光照变化没那么敏感。

hsv = cv2.cvtColor(color_image, cv2.COLOR_BGR2HSV) lower = np.array([0, 100, 100]) upper = np.array([10, 255, 255]) mask = cv2.inRange(hsv, lower, upper)

轮廓检测法:在mask基础上找轮廓,取面积最大的那个作为目标。

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: target = max(contours, key=cv2.contourArea) rect = cv2.minAreaRect(target) box = cv2.boxPoints(rect) box = np.int0(box) cv2.drawContours(color_image, [box], 0, (0, 255, 0), 2)

背景减除法:适合固定相机、背景不变的场景。先拍一张没有物体的背景图,之后每帧和背景做差。

diff = cv2.absdiff(background, current) gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) _, mask = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY)

三种方法没有绝对优劣,看场景。颜色阈值最快但受光照影响大;背景减除最稳但要求背景固定;轮廓检测通常和其他方法配合使用。

2.3 最小外接矩形:长宽像素值怎么取

cv2.minAreaRect返回的是一个旋转矩形,包含中心点、宽高和旋转角度。注意这里的宽高是相对于矩形自身坐标系的,不是图像坐标系的。对于尺寸测量来说,我们关心的是物体的实际长和宽,所以直接用rect的宽高就行。

(cx, cy), (w, h), angle = rect pixel_width = max(w, h) pixel_height = min(w, h)

取max和min是为了让长边始终对应“长度”,短边对应“宽度”,这样显示结果时不会因为物体旋转而长宽互换。

注意:minAreaRect对轮廓的噪声很敏感。如果mask边缘有毛刺,算出来的矩形会偏大。建议在findContours之前先做一次形态学开运算,去掉小噪点。

kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

2.4 深度值取值:为什么用中位数而不是平均值

物体表面不是完全平的,深度值会有波动。如果直接取物体区域内所有像素的平均深度,边缘像素可能落在背景上,把平均值拉偏。中位数对异常值更鲁棒。

depth_image = np.asanyarray(depth_frame.get_data()) mask_roi = depth_image[cy-10:cy+10, cx-10:cx+10] valid_depths = mask_roi[mask_roi > 0] if len(valid_depths) > 0: depth_value = np.median(valid_depths) * depth_scale

depth_scale是深度单位换算系数,D435i默认是0.001,也就是深度图里的值乘以0.001得到米。

取中心区域而不是整个物体区域,是为了避免边缘深度不准的问题。D435i在物体边缘处深度值容易跳变,取中心20x20的区域最稳。

3. 完整实操流程与代码实现

3.1 主循环代码结构

把前面所有步骤串起来,主循环大概长这样:

import pyrealsense2 as rs import numpy as np import cv2 pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) profile = pipeline.start(config) align = rs.align(rs.stream.color) depth_scale = profile.get_device().first_depth_sensor().get_depth_scale() intrinsics = profile.get_stream(rs.stream.color).as_video_stream_profile().get_intrinsics() fx = intrinsics.fx fy = intrinsics.fy try: while True: frames = pipeline.wait_for_frames() aligned = align.process(frames) color_frame = aligned.get_color_frame() depth_frame = aligned.get_depth_frame() if not color_frame or not depth_frame: continue color_image = np.asanyarray(color_frame.get_data()) depth_image = np.asanyarray(depth_frame.get_data()) hsv = cv2.cvtColor(color_image, cv2.COLOR_BGR2HSV) lower = np.array([0, 100, 100]) upper = np.array([10, 255, 255]) mask = cv2.inRange(hsv, lower, upper) kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: target = max(contours, key=cv2.contourArea) if cv2.contourArea(target) > 500: rect = cv2.minAreaRect(target) (cx, cy), (w, h), angle = rect box = cv2.boxPoints(rect) box = np.int0(box) cv2.drawContours(color_image, [box], 0, (0, 255, 0), 2) cx, cy = int(cx), int(cy) roi = depth_image[max(0,cy-10):cy+10, max(0,cx-10):cx+10] valid = roi[roi > 0] if len(valid) > 0: z = np.median(valid) * depth_scale if z > 0: real_w = max(w, h) * z / fx real_h = min(w, h) * z / fy cv2.putText(color_image, f"W: {real_w*100:.1f}cm", (cx-50, cy-20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.putText(color_image, f"H: {real_h*100:.1f}cm", (cx-50, cy+10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.putText(color_image, f"Z: {z*100:.1f}cm", (cx-50, cy+40), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2) cv2.imshow("Size Measurement", color_image) if cv2.waitKey(1) & 0xFF == ord('q'): break finally: pipeline.stop() cv2.destroyAllWindows()

这段代码可以直接跑。把红色物体放在相机前,画面上会实时显示物体的长、宽和距离。

3.2 参数调优:阈值、面积过滤和深度范围

颜色阈值不是固定的,取决于你的物体颜色。调阈值的方法:先把物体放在画面里,打印出物体区域中心点的HSV值,然后以这个值为中心上下浮动。

center_hsv = hsv[cy, cx] print(f"H={center_hsv[0]}, S={center_hsv[1]}, V={center_hsv[2]}")

比如打印出来是H=5, S=200, V=180,那lower可以设[0, 150, 120],upper设[15, 255, 255]。

面积过滤的阈值cv2.contourArea(target) > 500是为了排除噪点。500这个值在640x480分辨率下大概对应一个20x25像素的区域,太小的轮廓直接忽略。

深度范围也要限制。D435i的有效深度范围大概是0.3米到3米,超出这个范围深度值会不准。可以在代码里加一个判断:

if z < 0.3 or z > 3.0: continue

3.3 测量精度验证:用已知尺寸物体做校准

拿一个已知尺寸的物体,比如A4纸(29.7cm x 21cm),放在不同距离下测量,看误差有多大。我实测下来,在50cm距离下,D435i测A4纸的长边误差大概在±0.5cm以内,短边误差±0.3cm以内。距离越远误差越大,1米以外误差可能到±1cm。

误差来源主要有三个:深度值的噪声、边缘像素的模糊、内参的微小偏差。深度噪声可以通过多帧平均来降低:

depth_buffer = [] # 每帧往buffer里塞一个值,取最近10帧的中位数 depth_buffer.append(z) if len(depth_buffer) > 10: depth_buffer.pop(0) z_smooth = np.median(depth_buffer)

这样处理之后,测量值的抖动会明显减小。

4. 常见问题与排查技巧实录

4.1 深度图大面积空洞怎么办

D435i用的是红外结构光加双目立体匹配,对某些材质特别不友好。黑色物体、反光表面、透明物体,深度图上一大片0值。解决办法:

  • 黑色物体:打一盏补光灯,或者把物体放在白色背景上,利用背景的深度值做参考。
  • 反光表面:改变相机角度,避免镜面反射直接回到相机。或者喷一层显影剂(工业上常用的做法)。
  • 透明物体:基本无解,D435i测不了玻璃。只能换其他原理的传感器。

如果空洞只是零星的,可以用深度图的get_distance在物体中心多采几个点,取有效值的中位数。

4.2 测量值跳动厉害怎么处理

跳动主要来自深度噪声。除了前面说的多帧中位数滤波,还可以做时域滤波。RealSense SDK自带后处理滤波器:

temporal_filter = rs.temporal_filter() spatial_filter = rs.spatial_filter() depth_frame = temporal_filter.process(depth_frame) depth_frame = spatial_filter.process(depth_frame)

temporal_filter做时域平滑,spatial_filter做空域平滑。两个一起用,深度图会稳很多,但会引入一定的延迟。如果对实时性要求高,只用temporal_filter就够了。

4.3 物体旋转后长宽互换怎么解决

minAreaRect返回的宽高是相对于矩形自身的,物体旋转90度,宽高就互换了。如果你需要固定长边为“长度”,用max/min就行。但如果你需要区分物体的“实际长边”和“实际短边”,比如测量一个长方形的零件,那就需要额外逻辑。

我的做法是:如果物体有明确的朝向特征(比如有一个缺口或者标记),用特征点来确定长边方向。如果没有,就用max/min,然后在显示时标注“长边”和“短边”。

4.4 常见问题速查表

问题现象可能原因解决方法
深度图全黑相机未启动或USB供电不足换USB 3.0接口,检查pipeline是否start成功
测量值偏大深度值取到了背景缩小ROI区域,只取物体中心
测量值偏小物体边缘被算进了轮廓做形态学腐蚀,或者用轮廓面积过滤
帧率低对齐和后处理消耗资源降低分辨率到424x240,或者关闭后处理
颜色分割不稳定光照变化改用HSV空间,或者加补光灯
物体移动时测量不准深度和彩色帧不同步用align.process确保对齐,开启帧同步

独家避坑技巧:D435i的USB线质量对稳定性影响巨大。原装线大概1米,如果你需要更长的线,一定要买带信号放大的主动式USB 3.0延长线。普通延长线会导致深度图丢帧甚至相机掉线。这个坑我踩过,换了三根线才找到能稳定跑的。

4.5 进阶扩展:多物体测量与机械臂抓取

如果画面里有多个物体,把findContours的结果遍历一遍,对每个轮廓分别做测量就行。但要注意深度值要分别取,不能混。

for cnt in contours: if cv2.contourArea(cnt) > 500: rect = cv2.minAreaRect(cnt) # 对每个rect单独取深度和计算尺寸

如果要做机械臂抓取,测量出物体的中心像素坐标和深度值之后,可以用rs2_deproject_pixel_to_point把像素坐标反投影到三维空间,得到物体在相机坐标系下的XYZ坐标,再通过手眼标定转换到机械臂坐标系。

point = rs.rs2_deproject_pixel_to_point(intrinsics, [cx, cy], z) # point[0], point[1], point[2] 就是相机坐标系下的XYZ

这一步是D435i在机械臂应用里最核心的价值——不仅知道物体多大,还知道物体在哪。

4.6 性能优化:从25fps到60fps的调优记录

默认配置下,640x480分辨率加对齐加后处理,帧率大概在20到25fps。如果想提到更高:

  1. 降低深度分辨率到424x240,彩色保持640x480。深度图只用来取距离值,不需要高分辨率。
  2. 关闭spatial_filter,只保留temporal_filter。
  3. 把颜色分割和轮廓检测放到单独的线程里,和相机取流并行。

我实测下来,424x240深度加640x480彩色,不开后处理,能跑到55到60fps。对于大多数实时测量场景,这个帧率完全够用了。

最后分享一个小技巧:调试的时候把深度图也显示出来,用cv2.applyColorMap上色,能直观看到哪些区域深度值缺失。很多测量问题一眼就能从深度图上看出原因。

depth_colormap = cv2.applyColorMap(cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET) cv2.imshow("Depth", depth_colormap)

这套方案我从头跑通大概花了两天,大部分时间花在调颜色阈值和深度滤波参数上。硬件本身很成熟,软件层面pyrealsense2的文档也够全,真正的门槛在于理解深度相机的物理限制——它能测什么、不能测什么、在什么条件下测不准。把这些边界摸清楚了,剩下的就是调参的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询