
1. 从零搭建一套物体尺寸实时测量系统的整体思路1.1 为什么选D435i而不是普通USB摄像头普通USB摄像头只能拿到二维彩色图像想从一张图里算出物体的真实物理尺寸必须依赖参照物标定或者已知距离一旦物体前后移动测量结果就全废了。D435i是Intel RealSense系列里的深度相机它同时输出彩色图和深度图深度图里每个像素都带着到相机平面的距离值单位毫米这就把“像素尺寸换算成物理尺寸”这件事变得非常直接。核心原理其实就一句话物理尺寸 像素尺寸 × 深度值 / 焦距。焦距是相机内参标定一次就能固定下来深度值由相机实时给出像素尺寸就是物体在图像里占了多少个像素。三个量凑齐尺寸就能实时算出来。D435i相比D415、D455这些型号优势在于它自带IMU做机械臂抓取或者移动平台上的测量时可以做姿态补偿。不过如果只是固定位置测尺寸D415的精度其实更高因为它的最小深度距离更近、分辨率下的基线更长。选D435i更多是考虑通用性和后续扩展。1.2 整体方案拆解从图像到尺寸的完整链路整套系统我把它拆成五步相机初始化与对齐启动D435i把深度图对齐到彩色图坐标系这样彩色图上每个像素都能查到对应的深度值。物体检测与分割用颜色阈值、轮廓检测或者简单的背景减除把目标物体从画面里抠出来。像素尺寸提取对分割出的轮廓求最小外接矩形拿到长和宽的像素值。深度值获取在物体区域内取深度中位数避免边缘噪声和空洞影响。物理尺寸换算与显示用内参焦距做换算把结果显示在画面上。这个链路里最容易出问题的是第2步和第4步。分割不准像素尺寸就偏深度取值不对换算结果能差出好几厘米。后面我会详细讲怎么处理。1.3 环境准备Python、pyrealsense2和OpenCV的安装先说Python环境。我建议用Python 3.8到3.10太新的版本有时候pyrealsense2的wheel还没跟上。安装方式用conda或者venv都行我个人习惯用conda因为科学计算相关的包管理起来省心。conda create -n realsense python3.9 conda activate realsense pip install pyrealsense2 opencv-python numpypyrealsense2是Intel官方提供的Python绑定直接pip就能装。如果装不上去Intel RealSense的GitHub release页面找对应Python版本的whl文件手动装。OpenCV用opencv-python就够了不需要contrib版本。注意pyrealsense2和librealsense的版本要匹配。如果你之前装过librealsense的SDKpip装的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(ffx{intrinsics.fx}, fy{intrinsics.fy}) print(fcx{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, keycv2.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:cy10, cx-10:cx10] valid_depths mask_roi[mask_roi 0] if len(valid_depths) 0: depth_value np.median(valid_depths) * depth_scaledepth_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, keycv2.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):cy10, max(0,cx-10):cx10] 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, fW: {real_w*100:.1f}cm, (cx-50, cy-20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.putText(color_image, fH: {real_h*100:.1f}cm, (cx-50, cy10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.putText(color_image, fZ: {z*100:.1f}cm, (cx-50, cy40), 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(fH{center_hsv[0]}, S{center_hsv[1]}, V{center_hsv[2]})比如打印出来是H5, S200, V180那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: continue3.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。如果想提到更高降低深度分辨率到424x240彩色保持640x480。深度图只用来取距离值不需要高分辨率。关闭spatial_filter只保留temporal_filter。把颜色分割和轮廓检测放到单独的线程里和相机取流并行。我实测下来424x240深度加640x480彩色不开后处理能跑到55到60fps。对于大多数实时测量场景这个帧率完全够用了。最后分享一个小技巧调试的时候把深度图也显示出来用cv2.applyColorMap上色能直观看到哪些区域深度值缺失。很多测量问题一眼就能从深度图上看出原因。depth_colormap cv2.applyColorMap(cv2.convertScaleAbs(depth_image, alpha0.03), cv2.COLORMAP_JET) cv2.imshow(Depth, depth_colormap)这套方案我从头跑通大概花了两天大部分时间花在调颜色阈值和深度滤波参数上。硬件本身很成熟软件层面pyrealsense2的文档也够全真正的门槛在于理解深度相机的物理限制——它能测什么、不能测什么、在什么条件下测不准。把这些边界摸清楚了剩下的就是调参的事。