奥比中光Geminipro相机实战:Python快速上手深度与彩色视觉

张开发
2026/4/12 19:49:18 15 分钟阅读

分享文章

奥比中光Geminipro相机实战:Python快速上手深度与彩色视觉
1. 开箱即用5分钟搞定Geminipro相机环境刚拿到奥比中光的Geminipro相机你是不是和我当初一样既兴奋又有点懵看着这个小小的“眼睛”知道它能看深度、看彩色但怎么让它动起来把数据抓到电脑里可能第一步就卡住了。别担心我花了半天时间踩坑帮你把最顺滑的路径趟出来了。咱们的目标很简单用Python在最短时间内让相机吐出第一张深度图和彩色图。这个过程我保证你不需要啃几百页的英文手册也不用折腾复杂的编译环境。首先你得确保手头有这几样东西一台安装了Windows 10/11或者Ubuntu 20.04/22.04的电脑我实测下来Windows更省心你的Geminipro相机本体以及配套的USB 3.0数据线。这里有个关键点一定要用蓝色的USB 3.0接口我试过插在USB 2.0口上要么直接认不出设备要么帧率低得可怜深度图还会出现大片噪点。插好线打开设备管理器你应该能看到一个“Orbbec”或者“3D Camera”相关的设备这就成功了一半。接下来是软件环境。官方推荐用Python 3.7到3.9我个人用Python 3.8最稳。咱们先创建一个干净的虚拟环境这是好习惯能避免各种包版本冲突。打开你的命令行Windows用CMD或PowerShellLinux/macOS用终端输入以下命令# 创建并激活一个名为orbbec的虚拟环境 python -m venv orbbec_env # Windows激活 orbbec_env\Scripts\activate # Linux/macOS激活 source orbbec_env/bin/activate环境激活后命令行前面会出现(orbbec_env)的提示。然后安装最核心的Orbbec SDK。这里有个小技巧官方SDK的Python绑定包最方便的方式是通过他们提供的wheel文件安装。你需要去奥比中光官网的开发者中心找到Geminipro对应的SDK下载页面。下载完成后通常是一个.whl文件比如orbbec_sdk-2.x.x-cp38-cp38-win_amd64.whl。用pip直接安装它pip install 你下载的whl文件路径安装完SDK我们还需要两个好帮手opencv-python和numpy。OpenCV用来显示和处理图像NumPy是处理数据数组的基础。一条命令搞定pip install opencv-python numpy matplotlib如果一切顺利没有报红字错误那么恭喜你最磨人的环境配置已经完成了。你可以写一个最简单的测试脚本导入关键模块试试水import cv2 import numpy as np from ObTypes import * import Pipeline import Context print(所有模块导入成功) ctx Context.Context(None) print(上下文创建成功)运行这个脚本如果能看到成功的打印信息说明SDK安装正确Python环境已经就绪。整个过程从拆箱到环境就绪我最快一次只用了不到5分钟。记住核心就是用USB 3.0接口和安装正确的官方wheel包这两步走对了后面就是一马平川。2. 连接与配置让相机“活”起来环境好了现在要让相机真正工作起来。你可以把Geminipro相机想象成一个特别勤快的“数据工人”它内部有两条独立的生产线一条专门生产彩色图像RGB另一条专门生产深度图像Depth。我们的任务就是同时启动这两条生产线并且告诉它们以什么样的规格分辨率、帧率来生产数据。这个过程在SDK里是通过一个叫Pipeline管道和Config配置的核心对象来完成的。首先我们得创建一个“总控室”也就是Context。它负责管理所有硬件资源。创建时通常不需要额外参数传个None就行。同时为了不让调试信息刷屏我们可以把日志级别调高只显示错误信息from ObTypes import * from Property import * import Pipeline import StreamProfile from Error import ObException import Context import sys # 创建上下文并设置日志级别为只显示错误 ctx Context.Context(None) ctx.setLoggerSeverity(OB_PY_LOG_SEVERITY_ERROR)接下来创建最重要的Pipeline对象。这个管道是你和相机数据流之间的桥梁所有数据的获取、控制指令的发送都要通过它。然后我们需要一个Config对象来详细设置我们要开启哪些数据流。try: # 创建数据管道 pipe Pipeline.Pipeline(None, None) # 创建配置对象 config Pipeline.Config()配置的第一步是开启彩色流。我们需要先问问相机“你支持哪些彩色流的格式” 通过getStreamProfileList方法并传入OB_PY_SENSOR_COLOR参数就能拿到一个列表。对于快速上手我们直接选择列表里的第一个索引0这通常是相机默认的、最兼容的配置。# 获取彩色相机所有支持的流配置 color_profiles pipe.getStreamProfileList(OB_PY_SENSOR_COLOR) # 选择第一个默认配置 color_profile color_profiles.getProfile(0) # 转换为具体的视频流配置对象这样才能拿到宽高等信息 color_video_profile color_profile.toConcreteStreamProfile(OB_PY_STREAM_VIDEO) color_width color_video_profile.width() color_height color_video_profile.height() print(f彩色流配置: {color_width} x {color_height}) # 启用彩色流 config.enableStream(color_video_profile)深度流的配置过程一模一样只是把传感器类型换成OB_PY_SENSOR_DEPTH。这里我强烈建议你把获取到的宽高打印出来Geminipro常见的深度流分辨率是640x480或者1280x720这能帮你确认相机是否被正确识别。# 获取深度相机所有支持的流配置 depth_profiles pipe.getStreamProfileList(OB_PY_SENSOR_DEPTH) depth_profile depth_profiles.getProfile(0) depth_video_profile depth_profile.toConcreteStreamProfile(OB_PY_STREAM_VIDEO) depth_width depth_video_profile.width() depth_height depth_video_profile.height() print(f深度流配置: {depth_width} x {depth_height}) config.enableStream(depth_video_profile)配置好两个流之后还有一个至关重要的步骤对齐Alignment。你想啊彩色摄像头和深度红外摄像头在物理上是两个不同的镜头它们看世界的位置有细微差别。这就好比你的左眼和右眼看到的画面略有不同。如果不做对齐你得到的彩色图和深度图上的同一个物体像素位置是对不上的。这对于后续做物体测量、AR叠加等应用来说是灾难。SDK提供了硬件对齐D2C功能能高效地解决这个问题。我们一行代码就能开启# 启用硬件深度到彩色D2C对齐模式 config.setAlignMode(OB_PY_ALIGN_D2C_HW_MODE)最后用这个配置好的config对象启动管道# 启动管道开始流数据 pipe.start(config, None) print(管道启动成功相机开始工作)把上面所有代码放在一个try...except块里是个好习惯因为万一你的电脑没插相机或者驱动有问题SDK会抛出ObException异常我们能捕获并给出友好提示而不是让程序直接崩溃。当看到“管道启动成功”的打印时你的Geminipro相机就已经“活”过来了正源源不断地生产着深度和彩色数据等待你去获取。3. 抓取与解码从数据流到可视图管道启动后相机就像打开了水龙头数据流哗哗地来。但我们不能直接用手去接得用合适的“容器”去接并且把接到的“原始数据水”处理成我们能看懂的“图像”。这个过程的核心是waitForFrames方法它会阻塞等待直到相机采集到一组完整的帧数据包含一帧彩色和一帧深度或者超时。我建议设置一个合理的超时时间比如100毫秒。时间太短可能还没等到数据就返回了时间太长程序会显得卡顿。拿到frames对象后我们就可以分别从中提取彩色帧和深度帧了。# 设置等待超时时间毫秒 timeout_ms 100 # 等待并获取一组帧数据 frames pipe.waitForFrames(timeout_ms) if frames is None: print(等待帧数据超时) # 通常这里会继续循环等待或者退出先来看怎么处理彩色图。从frames里取出彩色帧对象后我们需要拿到它内部的原始数据。这个数据通常是经过压缩的比如JPEG格式不能直接用OpenCV显示。所以我们需要用cv2.imdecode函数进行解码。这里有个细节data()方法返回的是数据的指针我们需要先获取数据大小dataSize()然后将其转换为Python的bytes对象。下面是我封装的一个获取彩色图的函数import cv2 import numpy as np def get_color_image(frames, expected_height, expected_width): 从帧集合中获取并解码彩色图像 color_frame frames.colorFrame() if color_frame is None: print(警告未获取到彩色帧) return None # 获取原始数据的大小和指针 size color_frame.dataSize() data_ptr color_frame.data() # 关键步骤将原始数据转换为NumPy数组。 # 这里假设数据是JPEG压缩格式SDK通常如此。 # 我们需要根据数据大小创建一个uint8类型的数组来接收数据。 # 注意实际拷贝数据的方式可能因SDK版本略有不同但原理一致。 data_np np.frombuffer(data_ptr, dtypenp.uint8, countsize) # 使用OpenCV解码JPEG数据 color_img cv2.imdecode(data_np, cv2.IMREAD_COLOR) # 有时解码后的图像尺寸可能与配置的宽高不符这里按配置调整 if color_img is not None: color_img cv2.resize(color_img, (expected_width, expected_height)) return color_img深度图的处理逻辑和彩色图完全不同。深度传感器采集的不是颜色信息而是每个像素点到相机的距离。这个距离值通常用一个16位无符号整数0-65535来表示单位是毫米的某个缩放比例比如0.1mm。原始数据是以两个8位通道低8位和高8位的形式打包传来的。所以我们的处理步骤是1. 拿到原始字节数据2. 重组为16位数据3. 乘以缩放因子得到真实的毫米值。def get_depth_image(frames, expected_height, expected_width): 从帧集合中获取并处理深度图像单位毫米 depth_frame frames.depthFrame() if depth_frame is None: print(警告未获取到深度帧) return None size depth_frame.dataSize() data_ptr depth_frame.data() # 深度数据通常是16位/像素但以两个8位通道的形式传输 # 先创建一个形状为(高, 宽, 2)的数组 data_np np.frombuffer(data_ptr, dtypenp.uint8, countsize) # 重塑为 (height, width, 2)注意这里height和width是之前配置的深度图尺寸 # 计算应有的像素数总字节数 / 2 (因为每个像素2个字节) pixel_count size // 2 # 我们需要知道深度图的确切尺寸来重塑这里用传入的期望尺寸 # 更严谨的做法是从depth_frame对象获取width和height try: frame_height depth_frame.height() frame_width depth_frame.width() except: # 如果无法从帧获取则使用配置时获取的期望尺寸 frame_height, frame_width expected_height, expected_width # 重塑数组注意总元素数必须等于 size data_np data_np.reshape((frame_height, frame_width, 2)) # 将两个8位通道合并为一个16位深度值 # 公式低字节 高字节 * 256 depth_data data_np[:, :, 0].astype(np.uint16) (data_np[:, :, 1].astype(np.uint16) 8) # 获取缩放因子将原始值转换为毫米 scale depth_frame.getValueScale() depth_mm depth_data * scale return depth_mm.astype(np.float32) # 转换为浮点数方便后续处理这两个函数是数据处理的“心脏”。调用它们传入之前获取的frames对象和对应的图像宽高你就能得到两个NumPy数组color_img是一个HxWx3的BGR格式彩色图像可以直接用OpenCV显示depth_img是一个HxW的浮点数组每个值代表对应像素点的深度距离毫米。到这一步原始数据已经成功转化成了我们程序员最熟悉、最好操作的矩阵形式。4. 可视化与初体验看见三维世界数据拿到了是时候把它们展示出来获得最直观的反馈了。可视化不仅仅是“看个图”更是验证我们前面所有步骤是否正确的关键环节。对于彩色图直接用OpenCV的imshow显示就行。但深度图有点特殊它是一个单通道的、数值范围可能很大的矩阵比如从几百毫米到几万毫米直接显示会是一片黑或者一片白。我们需要将其归一化到一个合理的显示范围。我常用的方法是先设定一个有效的深度范围比如只显示0.5米到5米之间的物体这个范围对于室内场景非常合适。然后把这个范围内的深度值线性映射到0-255的灰度区间。这样越近的物体越白越远的物体越黑视觉效果非常清晰。def visualize_depth(depth_image, min_depth500, max_depth5000): 将深度图可视化。 min_depth: 最小深度值毫米小于此值的设为0黑 max_depth: 最大深度值毫米大于此值的设为max_depth # 创建一个副本避免修改原数据 depth_vis depth_image.copy() # 将超出范围的深度值钳制到[min_depth, max_depth] depth_vis[depth_vis min_depth] min_depth depth_vis[depth_vis max_depth] max_depth # 线性归一化到0-255 depth_vis (depth_vis - min_depth) / (max_depth - min_depth) * 255 # 转换为8位无符号整数OpenCV才能显示 depth_vis depth_vis.astype(np.uint8) # 应用一个颜色映射如JET让深度更直观灰度图也可以 depth_colored cv2.applyColorMap(depth_vis, cv2.COLORMAP_JET) return depth_colored有了显示函数我们可以写一个简单的循环实时显示彩色和深度视频流。这是最激动人心的时刻你第一次通过代码“看见”三维世界。import cv2 import numpy as np print(开始实时显示按 q 键退出...) while True: # 获取一帧数据 frames pipe.waitForFrames(100) if frames is None: continue # 获取并处理彩色图 color_img get_color_image(frames, color_height, color_width) if color_img is not None: # OpenCV默认是BGR如果之前解码正确可以直接显示 cv2.imshow(Color Stream, color_img) # 获取并处理深度图 depth_data get_depth_image(frames, depth_height, depth_width) if depth_data is not None: depth_vis visualize_depth(depth_data, 500, 5000) cv2.imshow(Depth Stream, depth_vis) # 按q键退出循环 if cv2.waitKey(1) 0xFF ord(q): break # 退出前清理资源 cv2.destroyAllWindows() pipe.stop()当你运行这段代码两个窗口弹出来一个是你熟悉的彩色世界另一个是五彩斑斓的深度世界时成就感绝对拉满。在深度图窗口里试着把手靠近或远离相机你会看到颜色的剧烈变化。把手放在桌面和背景墙之间你能清晰地在深度图上看到边缘。这就是结构光技术的魅力它把不可见的距离信息变成了可见的图像。5. 进阶一步获取内参与简单应用看到图像只是第一步就像你有了地图但还需要知道比例尺和坐标系才能真正测量和定位。对于相机来说这个“比例尺和坐标系”就是相机内参Intrinsics。它描述了相机如何将三维空间中的点投影到二维图像上主要包括焦距fx, fy和光学中心cx, cy。有了内参你才能把图像上的一个像素点反向推算回它在相机前方的某条特定射线上的位置这是所有三维视觉应用的基础。幸运的是奥比中光SDK已经帮我们标定好了这些参数并且可以通过管道轻松获取。获取到的是彩色相机和深度相机各自的内参以及两者之间的转换关系外参。# 在管道启动后获取相机参数 camera_params pipe.getCameraParam() print(相机参数对象:, camera_params) # 获取彩色相机的内参矩阵 rgb_intrinsic camera_params.rgbIntrinsic print(\n彩色相机内参矩阵:) print(ffx (x轴焦距): {rgb_intrinsic.fx}) print(ffy (y轴焦距): {rgb_intrinsic.fy}) print(fcx (主点x坐标): {rgb_intrinsic.cx}) print(fcy (主点y坐标): {rgb_intrinsic.cy}) # 获取深度相机的内参矩阵 depth_intrinsic camera_params.depthIntrinsic print(\n深度相机内参矩阵:) print(ffx: {depth_intrinsic.fx}) print(ffy: {depth_intrinsic.fy}) print(fcx: {depth_intrinsic.cx}) print(fcy: {depth_intrinsic.cy})拿到内参能做什么一个最直接、最能建立成就感的应用就是实时测距。我们可以写一个小程序在彩色图像上点击一个点程序就利用该点的深度值从对齐后的深度图获取和相机内参计算出这个点离相机真实的直线距离。原理其实不复杂图像上的一个像素点(u, v)和对应的深度值z单位米可以通过下面的公式反算出它在相机坐标系下的三维坐标 (X, Y, Z)X (u - cx) * z / fx Y (v - cy) * z / fy Z z这个Z值就是该点到相机光心的直线距离。我们来实现一个带鼠标交互的测距demoimport cv2 import numpy as np # 鼠标回调函数 click_point None def mouse_callback(event, x, y, flags, param): global click_point if event cv2.EVENT_LBUTTONDOWN: click_point (x, y) print(f点击坐标: ({x}, {y})) # 创建窗口并设置鼠标回调 cv2.namedWindow(Color Stream) cv2.setMouseCallback(Color Stream, mouse_callback) print(点击彩色图像上的物体即可测量距离按q退出) while True: frames pipe.waitForFrames(100) if frames is None: continue color_img get_color_image(frames, color_height, color_width) depth_data get_depth_image(frames, depth_height, depth_width) if color_img is None or depth_data is None: continue # 显示彩色图 display_img color_img.copy() # 如果有点击事件计算并显示距离 if click_point is not None: u, v click_point # 确保点击点在图像范围内 if 0 v depth_data.shape[0] and 0 u depth_data.shape[1]: depth_mm depth_data[v, u] # 获取深度值毫米 if depth_mm 0: # 有效的深度点 depth_m depth_mm / 1000.0 # 转换为米 # 使用彩色相机内参计算真实距离Z就是深度值 # 这里我们直接显示深度值作为距离因为它就是Z轴坐标 distance_text fDistance: {depth_m:.2f} m cv2.circle(display_img, (u, v), 5, (0, 0, 255), -1) cv2.putText(display_img, distance_text, (u10, v-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) print(f点({u},{v})距离相机: {depth_m:.3f} 米) else: cv2.putText(display_img, Invalid Depth, (u10, v-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) click_point None # 处理一次后重置 cv2.imshow(Color Stream, display_img) if cv2.waitKey(1) 0xFF ord(q): break pipe.stop() cv2.destroyAllWindows()运行这个程序在彩色图窗口里用鼠标点击桌面的水杯、键盘或者你的手屏幕上就会实时显示出那个点到相机的直线距离。我第一次做这个实验时拿着卷尺对比发现误差在厘米级别非常惊喜。这个小小的demo打通了从2D像素到3D空间的任督二脉你可以基于它扩展出更多应用比如测量物体尺寸、构建简单的3D扫描或者作为机器人避障的输入。6. 避坑指南与性能优化走通了整个流程你可能觉得一切都很顺利。但根据我的经验实际开发中总会遇到一些“坑”。我把最常见的几个问题和解决方案列出来希望能帮你节省大量调试时间。第一个坑帧率不稳定或卡顿。如果你发现显示窗口一卡一卡的或者waitForFrames经常超时首先要检查USB连接。务必使用USB 3.0及以上的接口和线缆。你可以尝试换一个主板原生的USB 3.0口通常是蓝色的避免使用机箱前置面板或经过扩展坞的接口。其次检查一下你开启的分辨率。如果同时开启1280x720的彩色流和深度流对带宽和算力要求较高。对于快速验证可以尝试降低分辨率。在配置流时不要直接用getProfile(0)而是遍历color_profiles列表选择一个较低分辨率如640x480的配置。第二个坑深度图有大片黑色或紫色区域。这通常表示那些点的深度数据无效。Geminipro采用结构光技术对于纯黑色、反光表面、透明物体如玻璃或过于遥远的物体结构光图案无法被正确捕捉或匹配会导致深度计算失败。这是物理限制不是bug。在可视化时这些无效点通常被赋值为0。我们在处理深度数据时可以先做一个过滤depth_valid depth_image[depth_image 0]只对有效值进行操作。第三个坑对齐后图像边缘有黑边。当你启用OB_PY_ALIGN_D2C_HW_MODE后得到的彩色图和深度图是“对齐”的即每个像素的彩色和深度信息对应物理空间的同一点。这个对齐过程是通过图像变换如旋转、缩放实现的变换后图像边缘可能会产生一些原本不存在的像素黑边。这是正常现象。如果你需要完整的图像可以考虑使用“深度到彩色”对齐的逆过程或者在对齐后对图像进行适当的裁剪。关于性能优化我有几个实测有效的小技巧按需获取数据流如果你的应用只需要深度图那就只开启深度流config.enableStream(depth_profile)不要开启彩色流。这样可以显著降低USB带宽占用和CPU解码压力提高帧率。使用硬件对齐务必使用config.setAlignMode(OB_PY_ALIGN_D2C_HW_MODE)。这个模式利用相机内部的ASIC芯片进行对齐计算速度极快几乎不占用主机CPU。如果你在代码里用OpenCV做软件对齐速度会慢一个数量级。降低显示开销OpenCV的imshow在高分辨率下刷新会很耗资源。如果你不需要实时观看可以注释掉显示部分。如果需要可以降低显示帧率比如每处理5帧才显示1帧。善用waitForFrames的超时参数在循环中如果处理速度跟不上相机帧率管道内部会堆积帧数据导致延迟越来越大。设置一个较小的超时如30ms如果取不到最新帧就跳过继续下一次循环这样可以保证你始终处理的是最新的数据对于实时控制应用非常重要。最后记得在程序退出前一定要调用pipe.stop()来关闭数据流。虽然Python有垃圾回收但显式关闭是一个好习惯能确保相机资源被正确释放下次连接时不会出现“设备被占用”的错误。

更多文章