最近在几个开发者社群里隔三差五就能看到类似的问题:Gemini2拿到手,系统是Ubuntu20.04,第一步只想拍一张彩色图像存到本地目录看看效果,结果从插上USB线到代码跑通,硬是折腾了整整一个下午。这个需求听起来确实简单,但里头的环节一点都不少——SDK怎么安装、设备权限怎么给、取到的帧到底是个什么格式、怎么转成OpenCV能认的BGR矩阵、保存路径该写在哪,每一步都可能把人卡住。Gemini2是一台双目结构光3D深度相机,USB Type-C接口,同一时刻能输出彩色图流和深度图流。对做机器人、视觉测量、三维重建的开发者来说,先学会“拍一张图并保存”,等于拿到了3D视觉开发的第一把钥匙,后面做批量采集、做深度转点云、做彩色深度对齐,全是从这条链路往上扩展的。这篇博文我就完完整整走一遍流程,从环境准备、SDK调用、像素格式转换,到最终在本地目录里拿到可用的PNG/JPG文件,全部讲透。适合刚拿到相机的新手,也适合想从查看器工具切换到代码控制的进阶开发者。我个人最推荐的方式是用官方SDK的Python接口。原因很直接:上手快、报错信息直观、几十行代码就能打通全流程,而且后面要做自动化采集或多相机同步,改起来非常方便。接下来的内容就按这条主线展开。1. 方案选型与整体思路1.1 先想清楚:Gemini2到底能给你哪几张“图”Gemini2本质上是一个多模态图像设备。正常工作时,它同时输出两类数据流:彩色流:分辨率最高可以到1920x1080@30fps,像素格式常见为YUYV、NV12或其他编码,这和我们平时理解的普通相机照片维度一致;深度流:默认通常是640x400@30fps,每个像素是16位整数,数值代表该点到相机平面的距离,单位是毫米。深度流是普通RGB相机给不了的信息。所以“拍一张图像”如果只是要彩色图,那问题范围比较小;但如果你还想要深度图,就得多考虑一个环节——16bit数据怎么保存、怎么转成适合人眼看的8bit伪彩图。这些细节我在后面会专门展开。1.2 三条技术路线怎么选路线一:官方查看器工具。打开图形界面,点一下截图保存按钮就能出图,适合刚收到硬件时验证设备是否正常、快速看效果,缺点是没法自动化,也没法写进自己的程序里。路线二:官方SDK的Python接口。代码控制在几十行,能完成打开设备、取流、格式转换、保存全流程,这也是本文选择。路线三:ROS驱动。适合机器人系统集成场景,但如果你只是“保存一张图”,引入ROS一套东西就太重了,光环境依赖都能折腾半天。从这也能看出,标题这个需求看起来小,背后其实是一次“设备控制链路”的完整走通:底层流配置、缓冲读取、像素格式理解、应用层文件写入全都要碰一遍。走通这一遍之后,你再去看官方示例里的彩色深度对齐、点云生成,会轻松很多。1.3 环境前提:先别让硬件坑了你Ubuntu20.04对USB3.0设备的兼容性很好,但有几个硬件层面的坑我得先说在前头:一定要用USB3.0接口。不要插在机箱前面板的USB2.0口上,否则带宽不够,图像会卡顿甚至掉帧,虽然能识别设备,但不稳定。注意供电。部分台式机前面板USB口供电不足,设备会时断时连。我习惯插主板后置的USB口,现场调试更省心。插上之后可以用lsusb快速确认设备是否被发现。如果什么记录都没有,先换线、换口、重启相机,而不是急着改代码。2. 环境准备:依赖与权限一次弄干净2.1 安装Python依赖这个需求里,Python侧的依赖其实只有三个:OpenCV、NumPy、官方SDK的Python绑定包。sudo apt update sudo apt install -y python3-pip pip3 install opencv-python numpy pyorbbecsdkOpenCV负责颜色空间转换和最终写文件,NumPy负责把SDK返回的字节串变成矩阵,pyorbbecsdk则是官方SDK的Python封装。Ubuntu20.04默认自带Python3.8,这几个库都有对应的预编译二进制,安装过程一般很顺利。如果网络条件不好导致下载慢,可以配置国内pip镜像源,但我见过的大部分情况直接装就行。2.2 设置udev规则,避免权限拒绝刚插上设备就直接跑代码,最常见的错误是设备枚举数量为0,或者打开设备时报类似“Failed to open device”的权限错误。原因通常是当前用户没有访问/dev下USB设备的权限。解决方法是安装udev规则。如果你手头有SDK的完整压缩包,里面通常自带scripts/install_udev_rules.sh,执行一下再重插设备:cd ~/OrbbecSDK/scripts sudo bash ./install_udev_rules.sh sudo udevadm control --reload如果没有脚本,也可以手动创建规则文件,核心就是把USB设备节点权限放开。规则写完后记得重新插拔相机,让内核重新加载设备节点。这一步做完,大部分“设备打开失败”的问题就消失了。2.3 跑一个最小设备枚举脚本验证环境环境弄干净没,先别急着写完整采集程序,跑一个最小脚本验证:from pyorbbecsdk import Context ctx = Context() device_count = ctx.query_devices().get_count() print("device count:", device_count)如果打印出device count: 1,说明SDK安装、udev权限、USB连接全部正常。如果一直打印0,先看lsusb里有没有设备,再看udev规则是否生效,最后检查是不是被其他进程占用了。注意:不同版本的SDK在初始化接口上可能有细微差异,遇到属性或方法不存在的报错时,优先看当前安装版本自带的examples,以你手头版本为准。3. 核心实操:从取流到保存一张彩色图3.1 初始化Pipeline并配置彩色流Gemini2的SDK把数据流调度抽象得很干净,核心对象就三个:Context管理设备上下文,Pipeline负责数据流转发,StreamProfile描述分辨率、帧率、格式的组合。下面这段是初始化并配置彩色流的标准写法:from pyorbbecsdk import * import cv2 import numpy as np import os def main(): ctx = Context() device_list = ctx.query_devices() if device_list.get_count() == 0: print("没有发现设备") return device = device_list.get_device_by_index(0) pipeline = Pipeline(device) config = PipelineConfig() color_profile_list = pipeline.get_stream_profile_list(SensorType.COLOR) color_profile = color_profile_list.get_default_video_stream_profile() config.enable_stream(color_profile) pipeline.start(config) print("彩色流已启动")关键一步是get_stream_profile_list(SensorType.COLOR),它会把相机支持的所有彩色流配置(分辨率、帧率、格式的组合)列出来,get_default_video_stream_profile()返回一个默认组合。对“拍一张图”这个需求,默认配置完全够用。如果你以后想要更高分辨率,可以用按参数筛选的方法取特定Profile,但高分辨率会带来更大的带宽压力和更强的光照要求,新手不建议一开始就上最大规格。3.2 等待帧并转换为BGRPipeline启动之后,数据就在内部缓冲里持续流转。程序里用wait_for_frames()阻塞等待一帧数据准备好。这里有一个很重要的经验:相机刚启动时,自动曝光和自动白平衡还没有稳定,前几帧往往是暗的、偏色的,不能直接拿来保存。所以取帧循环里一般会跳过前面若干帧,再取稳定帧。彩色帧的像素格式默认不一定是BGR,常见的有三种情况:YUYV:YUV422格式,OpenCV需要用cv2.cvtColor(data, cv2.COLOR_YUV2BGR_YUYV)转换;NV12:YUV420sp格式,亮度全分辨率、色度隔行存放,转换前要先reshape成(height * 3 // 2, width);MJPG:压缩编码,直接用cv2.imdecode解压成BGR矩阵。def color_frame_to_bgr(frame): w = frame.get_width() h = frame.get_height() fmt = frame.get_format() data = np.asanyarray(frame.get_data(), dtype=np.uint8) if fmt == OBFormat.YUYV: yuyv = data.reshape(h, w * 2) return cv2.cvtColor(yuyv, cv2.COLOR_YUV2BGR_YUYV) elif fmt == OBFormat.NV12: nv12 = data.reshape(h * 3 // 2, w) return cv2.cvtColor(nv12, cv2.COLOR_YUV2BGR_NV12) elif fmt == OBFormat.MJPG: return cv2.imdecode(data, cv2.IMREAD_COLOR) return None这个转换函数是我实际项目里一直在用的模板。新手最容易在这里翻车:直接把frame.get_data()当成RGB矩阵塞给OpenCV,结果要么花屏要么颜色诡异,折腾半天还以为是相机坏了,其实只是格式没转对。3.3 保存到本地目录保存这一步走OpenCV的imwrite就行,但要注意目录创建和路径处理:save_dir = os.path.expanduser("~/workspace/gemini2_shots") os.makedirs(save_dir, exist_ok=True) save_path = os.path.join(save_dir, "shot_0001.png") saved = False for i in range(30): frames = pipeline.wait_for_frames(100) if frames is None: continue color_frame = frames.get_color_frame() if color_frame is None: continue if i < 10: continue bgr = color_frame_to_bgr(color_frame) if bgr is not None: cv2.imwrite(save_path, bgr) print("图像已保存:", save_path) saved = True break pipeline.stop() if not saved: print("未能获取有效帧")os.path.expanduser("~")会把家目录展开,避免硬编码用户名,这样代码换机器也能跑。os.makedirs(..., exist_ok=True)这行不能省,因为cv2.imwrite在目录不存在时不会自动创建。完整代码我一般会放在一个capture_one.py文件里,运行成功后终端会输出保存好的绝对路径,直接用图片查看器打开就能验证。4. 顺带把深度图也拿下:16bit数据的保存与可视化4.1 同时开启深度流彩色和深度并不强制同时开启,但在Gemini2上,我推荐两个都开。原因有两个:一是后面做彩色深度对齐时,同时开启的流往往带有帧同步关系,数据更好处理;二是配置方法几乎一模一样,多几行代码而已。depth_profile_list = pipeline.get_stream_profile_list(SensorType.DEPTH) depth_profile = depth_profile_list.get_default_video_stream_profile() config.enable_stream(depth_profile)一个PipelineConfig可以同时启用多个流,不需要像某些老方案那样开两个Pipeline实例,这是SDK设计得比较方便的地方。4.2 深度帧转成可保存的图像深度帧的get_data()拿到的是一个16位无符号整数数组,单位是毫米。这个数据用cv2.imwrite直接存PNG是可行的,因为PNG格式本身支持16bit单通道。但问题是,普通图片查看器打开这种16bit PNG,你会看到黑乎乎一片,因为深度数值范围很大,而显示默认只映射了低动态范围。正确的做法是分成两条路:一条路保存原始16bit数据,用于算法处理;另一路做归一化,转成8bit伪彩图,用于人眼查看。depth_data = np.asanyarray(depth_frame.get_data(), dtype=np.uint16) depth_img = depth_data.reshape(depth_frame.get_height(), depth_frame.get_width()) # 原始深度,保留毫米精度 cv2.imwrite(os.path.join(save_dir, "depth_raw.png"), depth_img) # 可视化:截断到 0~3000mm,然后映射到8bit vis = np.clip(depth_img, 0, 3000).astype(np.float32) / 3000.0 * 255.0 vis = vis.astype(np.uint8) vis_color = cv2.applyColorMap(vis, cv2.COLORMAP_JET) cv2.imwrite(os.path.join(save_dir, "depth_vis.png"), vis_color)实测下来这组代码能同时得到一张16bit的真实深度图和一张适合人眼看的伪彩图。这个区分对后续调试特别重要——深度图不是照片,它是数据,只有理解这一点,你在处理点云、做距离测量时才不会犯方向性错误。4.3 常见误解:深度图和彩色图的像素不是一一对应很多新手拿到两张图第一反应是直接叠加在一起看,结果发现边缘对不上,误以为设备有问题。其实这不是故障,而是深度图和彩色图的视场角、分辨率、光学中心都不一样。深度图要叠加到彩色图上,需要经过相机内参和外参做对齐计算。SDK里提供坐标系变换工具,保存单张图这个需求用不到,但如果你要“深度图与彩色图严格对齐”,一定要去调用对齐接口,不要自己用像素坐标硬算,否则边缘错位会一直存在。5. 常见问题排查与避坑实录5.1 设备打不开 / 枚举数量为0按照顺序排查:先用lsusb确认设备有没有被系统识别;再检查udev规则是否生效,重插设备后是否生成新的设备节点;最后看是不是有别的进程占用了相机。特别常见的一个场景是用户开着一个官方查看器窗口,再跑自己的脚本,设备被查看器独占,脚本自然打不开。关掉查看器再跑脚本一般就好了。5.2 图像发黑、过曝、偏色图像发黑通常有两个原因:一是取帧循环跳过的帧数不够,自动曝光还没收敛,我把跳过数从5调到20之后问题就消失了;二是环境光太暗,需要补光。图像过曝则相反,可以减小曝光量,或者等更多帧让自动曝光回调。颜色偏黄或偏蓝是自动白平衡还没稳定的表现,同样的策略:多等几帧。花屏或颜色完全错乱,基本可以肯定是格式转换问题,先打印frame.get_format()看看到底是什么格式,再对应选cvtColor的转换参数。现象常见原因处理思路枚举设备数为0USB线/口问题、udev未生效换USB3.0口,重装udev规则打开设备失败被其他进程占用关闭查看器或重启相机图像全黑自动曝光未稳定、环境光不足跳过更多帧,增加补光图像过曝曝光参数过大等待收敛或手动调低曝光花屏、颜色错乱YUV转BGR参数不匹配打印format,按格式转移5.3 保存失败 / 图片大小异常如果cv2.imwrite返回False但没有报错,最可能是数据矩阵的维度、通道数或数据类型不符合预期。保存前先打印bgr.shape和bgr.dtype,确认是(height, width, 3)的uint8数组。另外路径问题也值得注意:目录不存在要用os.makedirs创建;文件名建议一律用英文和数字,不要带中文或特殊符号,部分图像库对Unicode路径支持不稳定,实测容易出奇怪问题。5.4 帧率上不去 / 卡顿如果只是保存单张图,卡顿问题不明显。但做连续采集时,帧率上不去通常会指向三个原因:USB带宽不足,建议直接插主板后置USB3.0口,不要经过HUB;分辨率或帧率配置过高,深度和彩色同时跑最大规格会很快占满带宽,需要按场景取舍;每帧处理耗时过长,比如在取帧循环里做复杂的图像处理,处理时间大于帧间隔,帧率自然掉下去。我在连续采集项目里常用的配置是彩色开720p,深度保持默认640x400,实测输出能稳定在30fps附近。这个组合对大多数视觉任务都够用,又不会给USB带宽和CPU带来太大压力。5.5 多帧取平均的小技巧对质量要求更高的静态场景,我一般不会只取一帧,而是连续取5到10帧稳定帧做平均降噪。具体做法是把多帧BGR矩阵累加后除以帧数,深度图同理。这个操作能明显降低传感器噪声,对后续测量精度有提升。代价是会多花几十毫秒,但如果你的场景是静态物体,这个性价比非常高。把上面这些跑通之后,你会发现后来很多工作都建在这条链路上:批量采集、多帧平均、深度转点云、彩色深度对齐、不同型号相机的替换等等。你完全可以把它当成一个“相机启动模板”,换用同系列其他型号时,大部分代码都不用动,只需要根据设备能力调整Profile配置就行。最后分享一个我自己踩过好几次的小技巧:正式保存第一张图之前,先打印一帧图像的shape和format,确认数据格式,再写保存逻辑。看起来只是多了两行调试代码,实际上它能帮你区分“相机问题”和“代码问题”——是先确认拿到了一帧正常数据,再去做格式转换和文件写入,而不是等存出来的图全黑才开始怀疑相机坏了。这个习惯帮我省了无数次“保存出来全黑但不知道是哪一步出错”的折腾。
最近在几个开发者社群里隔三差五就能看到类似的问题:Gemini2拿到手,系统是Ubuntu20.04,第一步只想拍一张彩色图像存到本地目录看看效果,结果从插上USB线到代码跑通,硬是折腾了整整一个下午。这个需求听起来确实简单,但里头的环节一点都不少——SDK怎么安装、设备权限怎么给、取到的帧到底是个什么格式、怎么转成OpenCV能认的BGR矩阵、保存路径该写在哪,每一步都可能把人卡住。Gemini2是一台双目结构光3D深度相机,USB Type-C接口,同一时刻能输出彩色图流和深度图流。对做机器人、视觉测量、三维重建的开发者来说,先学会“拍一张图并保存”,等于拿到了3D视觉开发的第一把钥匙,后面做批量采集、做深度转点云、做彩色深度对齐,全是从这条链路往上扩展的。这篇博文我就完完整整走一遍流程,从环境准备、SDK调用、像素格式转换,到最终在本地目录里拿到可用的PNG/JPG文件,全部讲透。适合刚拿到相机的新手,也适合想从查看器工具切换到代码控制的进阶开发者。我个人最推荐的方式是用官方SDK的Python接口。原因很直接:上手快、报错信息直观、几十行代码就能打通全流程,而且后面要做自动化采集或多相机同步,改起来非常方便。接下来的内容就按这条主线展开。1. 方案选型与整体思路1.1 先想清楚:Gemini2到底能给你哪几张“图”Gemini2本质上是一个多模态图像设备。正常工作时,它同时输出两类数据流:彩色流:分辨率最高可以到1920x1080@30fps,像素格式常见为YUYV、NV12或其他编码,这和我们平时理解的普通相机照片维度一致;深度流:默认通常是640x400@30fps,每个像素是16位整数,数值代表该点到相机平面的距离,单位是毫米。深度流是普通RGB相机给不了的信息。所以“拍一张图像”如果只是要彩色图,那问题范围比较小;但如果你还想要深度图,就得多考虑一个环节——16bit数据怎么保存、怎么转成适合人眼看的8bit伪彩图。这些细节我在后面会专门展开。1.2 三条技术路线怎么选路线一:官方查看器工具。打开图形界面,点一下截图保存按钮就能出图,适合刚收到硬件时验证设备是否正常、快速看效果,缺点是没法自动化,也没法写进自己的程序里。路线二:官方SDK的Python接口。代码控制在几十行,能完成打开设备、取流、格式转换、保存全流程,这也是本文选择。路线三:ROS驱动。适合机器人系统集成场景,但如果你只是“保存一张图”,引入ROS一套东西就太重了,光环境依赖都能折腾半天。从这也能看出,标题这个需求看起来小,背后其实是一次“设备控制链路”的完整走通:底层流配置、缓冲读取、像素格式理解、应用层文件写入全都要碰一遍。走通这一遍之后,你再去看官方示例里的彩色深度对齐、点云生成,会轻松很多。1.3 环境前提:先别让硬件坑了你Ubuntu20.04对USB3.0设备的兼容性很好,但有几个硬件层面的坑我得先说在前头:一定要用USB3.0接口。不要插在机箱前面板的USB2.0口上,否则带宽不够,图像会卡顿甚至掉帧,虽然能识别设备,但不稳定。注意供电。部分台式机前面板USB口供电不足,设备会时断时连。我习惯插主板后置的USB口,现场调试更省心。插上之后可以用lsusb快速确认设备是否被发现。如果什么记录都没有,先换线、换口、重启相机,而不是急着改代码。2. 环境准备:依赖与权限一次弄干净2.1 安装Python依赖这个需求里,Python侧的依赖其实只有三个:OpenCV、NumPy、官方SDK的Python绑定包。sudo apt update sudo apt install -y python3-pip pip3 install opencv-python numpy pyorbbecsdkOpenCV负责颜色空间转换和最终写文件,NumPy负责把SDK返回的字节串变成矩阵,pyorbbecsdk则是官方SDK的Python封装。Ubuntu20.04默认自带Python3.8,这几个库都有对应的预编译二进制,安装过程一般很顺利。如果网络条件不好导致下载慢,可以配置国内pip镜像源,但我见过的大部分情况直接装就行。2.2 设置udev规则,避免权限拒绝刚插上设备就直接跑代码,最常见的错误是设备枚举数量为0,或者打开设备时报类似“Failed to open device”的权限错误。原因通常是当前用户没有访问/dev下USB设备的权限。解决方法是安装udev规则。如果你手头有SDK的完整压缩包,里面通常自带scripts/install_udev_rules.sh,执行一下再重插设备:cd ~/OrbbecSDK/scripts sudo bash ./install_udev_rules.sh sudo udevadm control --reload如果没有脚本,也可以手动创建规则文件,核心就是把USB设备节点权限放开。规则写完后记得重新插拔相机,让内核重新加载设备节点。这一步做完,大部分“设备打开失败”的问题就消失了。2.3 跑一个最小设备枚举脚本验证环境环境弄干净没,先别急着写完整采集程序,跑一个最小脚本验证:from pyorbbecsdk import Context ctx = Context() device_count = ctx.query_devices().get_count() print("device count:", device_count)如果打印出device count: 1,说明SDK安装、udev权限、USB连接全部正常。如果一直打印0,先看lsusb里有没有设备,再看udev规则是否生效,最后检查是不是被其他进程占用了。注意:不同版本的SDK在初始化接口上可能有细微差异,遇到属性或方法不存在的报错时,优先看当前安装版本自带的examples,以你手头版本为准。3. 核心实操:从取流到保存一张彩色图3.1 初始化Pipeline并配置彩色流Gemini2的SDK把数据流调度抽象得很干净,核心对象就三个:Context管理设备上下文,Pipeline负责数据流转发,StreamProfile描述分辨率、帧率、格式的组合。下面这段是初始化并配置彩色流的标准写法:from pyorbbecsdk import * import cv2 import numpy as np import os def main(): ctx = Context() device_list = ctx.query_devices() if device_list.get_count() == 0: print("没有发现设备") return device = device_list.get_device_by_index(0) pipeline = Pipeline(device) config = PipelineConfig() color_profile_list = pipeline.get_stream_profile_list(SensorType.COLOR) color_profile = color_profile_list.get_default_video_stream_profile() config.enable_stream(color_profile) pipeline.start(config) print("彩色流已启动")关键一步是get_stream_profile_list(SensorType.COLOR),它会把相机支持的所有彩色流配置(分辨率、帧率、格式的组合)列出来,get_default_video_stream_profile()返回一个默认组合。对“拍一张图”这个需求,默认配置完全够用。如果你以后想要更高分辨率,可以用按参数筛选的方法取特定Profile,但高分辨率会带来更大的带宽压力和更强的光照要求,新手不建议一开始就上最大规格。3.2 等待帧并转换为BGRPipeline启动之后,数据就在内部缓冲里持续流转。程序里用wait_for_frames()阻塞等待一帧数据准备好。这里有一个很重要的经验:相机刚启动时,自动曝光和自动白平衡还没有稳定,前几帧往往是暗的、偏色的,不能直接拿来保存。所以取帧循环里一般会跳过前面若干帧,再取稳定帧。彩色帧的像素格式默认不一定是BGR,常见的有三种情况:YUYV:YUV422格式,OpenCV需要用cv2.cvtColor(data, cv2.COLOR_YUV2BGR_YUYV)转换;NV12:YUV420sp格式,亮度全分辨率、色度隔行存放,转换前要先reshape成(height * 3 // 2, width);MJPG:压缩编码,直接用cv2.imdecode解压成BGR矩阵。def color_frame_to_bgr(frame): w = frame.get_width() h = frame.get_height() fmt = frame.get_format() data = np.asanyarray(frame.get_data(), dtype=np.uint8) if fmt == OBFormat.YUYV: yuyv = data.reshape(h, w * 2) return cv2.cvtColor(yuyv, cv2.COLOR_YUV2BGR_YUYV) elif fmt == OBFormat.NV12: nv12 = data.reshape(h * 3 // 2, w) return cv2.cvtColor(nv12, cv2.COLOR_YUV2BGR_NV12) elif fmt == OBFormat.MJPG: return cv2.imdecode(data, cv2.IMREAD_COLOR) return None这个转换函数是我实际项目里一直在用的模板。新手最容易在这里翻车:直接把frame.get_data()当成RGB矩阵塞给OpenCV,结果要么花屏要么颜色诡异,折腾半天还以为是相机坏了,其实只是格式没转对。3.3 保存到本地目录保存这一步走OpenCV的imwrite就行,但要注意目录创建和路径处理:save_dir = os.path.expanduser("~/workspace/gemini2_shots") os.makedirs(save_dir, exist_ok=True) save_path = os.path.join(save_dir, "shot_0001.png") saved = False for i in range(30): frames = pipeline.wait_for_frames(100) if frames is None: continue color_frame = frames.get_color_frame() if color_frame is None: continue if i < 10: continue bgr = color_frame_to_bgr(color_frame) if bgr is not None: cv2.imwrite(save_path, bgr) print("图像已保存:", save_path) saved = True break pipeline.stop() if not saved: print("未能获取有效帧")os.path.expanduser("~")会把家目录展开,避免硬编码用户名,这样代码换机器也能跑。os.makedirs(..., exist_ok=True)这行不能省,因为cv2.imwrite在目录不存在时不会自动创建。完整代码我一般会放在一个capture_one.py文件里,运行成功后终端会输出保存好的绝对路径,直接用图片查看器打开就能验证。4. 顺带把深度图也拿下:16bit数据的保存与可视化4.1 同时开启深度流彩色和深度并不强制同时开启,但在Gemini2上,我推荐两个都开。原因有两个:一是后面做彩色深度对齐时,同时开启的流往往带有帧同步关系,数据更好处理;二是配置方法几乎一模一样,多几行代码而已。depth_profile_list = pipeline.get_stream_profile_list(SensorType.DEPTH) depth_profile = depth_profile_list.get_default_video_stream_profile() config.enable_stream(depth_profile)一个PipelineConfig可以同时启用多个流,不需要像某些老方案那样开两个Pipeline实例,这是SDK设计得比较方便的地方。4.2 深度帧转成可保存的图像深度帧的get_data()拿到的是一个16位无符号整数数组,单位是毫米。这个数据用cv2.imwrite直接存PNG是可行的,因为PNG格式本身支持16bit单通道。但问题是,普通图片查看器打开这种16bit PNG,你会看到黑乎乎一片,因为深度数值范围很大,而显示默认只映射了低动态范围。正确的做法是分成两条路:一条路保存原始16bit数据,用于算法处理;另一路做归一化,转成8bit伪彩图,用于人眼查看。depth_data = np.asanyarray(depth_frame.get_data(), dtype=np.uint16) depth_img = depth_data.reshape(depth_frame.get_height(), depth_frame.get_width()) # 原始深度,保留毫米精度 cv2.imwrite(os.path.join(save_dir, "depth_raw.png"), depth_img) # 可视化:截断到 0~3000mm,然后映射到8bit vis = np.clip(depth_img, 0, 3000).astype(np.float32) / 3000.0 * 255.0 vis = vis.astype(np.uint8) vis_color = cv2.applyColorMap(vis, cv2.COLORMAP_JET) cv2.imwrite(os.path.join(save_dir, "depth_vis.png"), vis_color)实测下来这组代码能同时得到一张16bit的真实深度图和一张适合人眼看的伪彩图。这个区分对后续调试特别重要——深度图不是照片,它是数据,只有理解这一点,你在处理点云、做距离测量时才不会犯方向性错误。4.3 常见误解:深度图和彩色图的像素不是一一对应很多新手拿到两张图第一反应是直接叠加在一起看,结果发现边缘对不上,误以为设备有问题。其实这不是故障,而是深度图和彩色图的视场角、分辨率、光学中心都不一样。深度图要叠加到彩色图上,需要经过相机内参和外参做对齐计算。SDK里提供坐标系变换工具,保存单张图这个需求用不到,但如果你要“深度图与彩色图严格对齐”,一定要去调用对齐接口,不要自己用像素坐标硬算,否则边缘错位会一直存在。5. 常见问题排查与避坑实录5.1 设备打不开 / 枚举数量为0按照顺序排查:先用lsusb确认设备有没有被系统识别;再检查udev规则是否生效,重插设备后是否生成新的设备节点;最后看是不是有别的进程占用了相机。特别常见的一个场景是用户开着一个官方查看器窗口,再跑自己的脚本,设备被查看器独占,脚本自然打不开。关掉查看器再跑脚本一般就好了。5.2 图像发黑、过曝、偏色图像发黑通常有两个原因:一是取帧循环跳过的帧数不够,自动曝光还没收敛,我把跳过数从5调到20之后问题就消失了;二是环境光太暗,需要补光。图像过曝则相反,可以减小曝光量,或者等更多帧让自动曝光回调。颜色偏黄或偏蓝是自动白平衡还没稳定的表现,同样的策略:多等几帧。花屏或颜色完全错乱,基本可以肯定是格式转换问题,先打印frame.get_format()看看到底是什么格式,再对应选cvtColor的转换参数。现象常见原因处理思路枚举设备数为0USB线/口问题、udev未生效换USB3.0口,重装udev规则打开设备失败被其他进程占用关闭查看器或重启相机图像全黑自动曝光未稳定、环境光不足跳过更多帧,增加补光图像过曝曝光参数过大等待收敛或手动调低曝光花屏、颜色错乱YUV转BGR参数不匹配打印format,按格式转移5.3 保存失败 / 图片大小异常如果cv2.imwrite返回False但没有报错,最可能是数据矩阵的维度、通道数或数据类型不符合预期。保存前先打印bgr.shape和bgr.dtype,确认是(height, width, 3)的uint8数组。另外路径问题也值得注意:目录不存在要用os.makedirs创建;文件名建议一律用英文和数字,不要带中文或特殊符号,部分图像库对Unicode路径支持不稳定,实测容易出奇怪问题。5.4 帧率上不去 / 卡顿如果只是保存单张图,卡顿问题不明显。但做连续采集时,帧率上不去通常会指向三个原因:USB带宽不足,建议直接插主板后置USB3.0口,不要经过HUB;分辨率或帧率配置过高,深度和彩色同时跑最大规格会很快占满带宽,需要按场景取舍;每帧处理耗时过长,比如在取帧循环里做复杂的图像处理,处理时间大于帧间隔,帧率自然掉下去。我在连续采集项目里常用的配置是彩色开720p,深度保持默认640x400,实测输出能稳定在30fps附近。这个组合对大多数视觉任务都够用,又不会给USB带宽和CPU带来太大压力。5.5 多帧取平均的小技巧对质量要求更高的静态场景,我一般不会只取一帧,而是连续取5到10帧稳定帧做平均降噪。具体做法是把多帧BGR矩阵累加后除以帧数,深度图同理。这个操作能明显降低传感器噪声,对后续测量精度有提升。代价是会多花几十毫秒,但如果你的场景是静态物体,这个性价比非常高。把上面这些跑通之后,你会发现后来很多工作都建在这条链路上:批量采集、多帧平均、深度转点云、彩色深度对齐、不同型号相机的替换等等。你完全可以把它当成一个“相机启动模板”,换用同系列其他型号时,大部分代码都不用动,只需要根据设备能力调整Profile配置就行。最后分享一个我自己踩过好几次的小技巧:正式保存第一张图之前,先打印一帧图像的shape和format,确认数据格式,再写保存逻辑。看起来只是多了两行调试代码,实际上它能帮你区分“相机问题”和“代码问题”——是先确认拿到了一帧正常数据,再去做格式转换和文件写入,而不是等存出来的图全黑才开始怀疑相机坏了。这个习惯帮我省了无数次“保存出来全黑但不知道是哪一步出错”的折腾。