一份 Orbbec 录像只有深度流,另一份同时录了深度和彩色,而且两路分辨率不同。如果程序一开始就要求两路图像齐全,第一份录像连一帧也交不出来。
这个系列从离线回放写起。本文使用 macOS arm64 和 Orbbec SDK v2.10.6,目标是给程序一个 .bag 文件,让它列出自己能处理的流,从每一路取到第一帧,然后退出。回放不需要连接相机。
录制设备:Orbbec Gemini 338Le
用 SDK 分别打开两份 bag,回放设备都报告型号 Orbbec Gemini 338Le(Playback),录制时的相机固件为 1.8.17。下文用的 2.10.6 是 macOS 上的回放 SDK 版本。要核对自己的录像,可以在创建 ob::PlaybackDevice 后读取设备信息。
const auto info = device->getDeviceInfo();
std::cout << info->getName() << " | firmware " << info->getFirmwareVersion() << '\n';
| Gemini 338Le 标称参数 | 规格 |
|---|---|
| 测深方式 | 主动红外双目,基线 95 mm |
| 理想工作距离 | 0.25–6 m |
| 深度视场角 | 水平 90° × 垂直 65°(±3°,距离 2 m) |
| 深度输出档位 | 最高 1280 × 800 @ 30 FPS,也支持 848 × 530 @ 60 FPS |
| 设备接口 | 千兆以太网,M12 X-coded 接口,支持 PoE 供电 |
表中列的是相机支持的规格。本文的纯深度录像实际采用 640 × 400 @ 10 FPS,双流录像的深度为 424 × 266 @ 30 FPS,彩色为 1280 × 720 @ 30 FPS。回放时应选 bag 中枚举到的档位。奥比中光 Gemini 338Le 官方规格
先问录像里录了什么
把文件路径传给 ob::PlaybackDevice,就得到了一个可以枚举传感器的回放设备。沿着 getSensorList()、getStreamProfileList() 查下去,每个视频 profile 都有格式、宽、高和帧率。保留选中的 profile,直接交给 Config::enableStream(),这样用的就是录像实际提供的配置。录像时长可以通过 getDuration() 读取,单位是毫秒。回放设备接口(v2.10.6)
随录像保存的同名 JSON 是 Viewer 的预设。在这次用到的一份录像里,JSON 把流标成关闭,SDK 却能从 bag 中读到深度和彩色两路数据。决定开哪路流时,以回放设备枚举的 profile 为准。
从打开文件到拿到帧
下面三段代码顺序拼成一个 read_bag.cpp。第一段选择 Y16 深度,以及 MJPG、RGB 或 BGR 彩色格式。这是示例能处理的范围。每个传感器只启用第一个匹配的 profile,没有彩色传感器的录像也能继续。
#include <libobsensor/ObSensor.hpp>
#include <libobsensor/hpp/RecordPlayback.hpp>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <memory>
int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "Usage: read_bag EXTENSIONS_DIR RECORDING.bag\n";
return 1;
}
try {
ob::Context::setExtensionsDirectory(argv[1]);
auto device = std::make_shared<ob::PlaybackDevice>(argv[2]);
auto config = std::make_shared<ob::Config>();
config->setFrameAggregateOutputMode(OB_FRAME_AGGREGATE_OUTPUT_DISABLE);
auto sensors = device->getSensorList();
bool want_depth = false, want_color = false;
for (uint32_t i = 0; i < sensors->getCount(); ++i) {
auto type = sensors->getSensorType(i);
if (type != OB_SENSOR_DEPTH && type != OB_SENSOR_COLOR) continue;
auto profiles = sensors->getSensor(i)->getStreamProfileList();
for (uint32_t j = 0; j < profiles->getCount(); ++j) {
auto profile = profiles->getProfile(j);
auto fmt = profile->getFormat();
bool depth = type == OB_SENSOR_DEPTH && fmt == OB_FORMAT_Y16;
bool color = type == OB_SENSOR_COLOR &&
(fmt == OB_FORMAT_MJPG || fmt == OB_FORMAT_RGB || fmt == OB_FORMAT_BGR);
if (!depth && !color) continue;
config->enableStream(profile);
want_depth |= depth;
want_color |= color;
auto video = profile->as<ob::VideoStreamProfile>();
std::cout << (depth ? "Depth " : "Color ") << video->getWidth() << "x"
<< video->getHeight() << " @ " << video->getFps() << "\n";
break;
}
}
if (!want_depth && !want_color) {
std::cerr << "No supported recorded streams\n";
return 1;
}第二段把回放设备传给 Pipeline。这里关闭帧同步,并在前面的配置中禁用聚合,哪一路先到就先处理哪一路。waitForFrameset(500) 最多等待 500 毫秒,单次没有返回帧并不能判断录像已经播完。外层的 5 秒只是这个首帧示例的等待上限。Pipeline 与 Config 接口
ob::Pipeline pipe(device);
pipe.disableFrameSync();
pipe.start(config);
bool got_depth = false, got_color = false;
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (std::chrono::steady_clock::now() < deadline) {
auto frames = pipe.waitForFrameset(500);
if (!frames) continue;第三段分别检查 getDepthFrame() 和 getColorFrame()。读到深度帧时,示例顺便取左上角像素。对这里的 Y16 距离数据,原始值乘当前帧的 getValueScale() 才是毫米数。左上角为零时仍算收到了深度帧,只是不输出该点的距离。MJPG 则是压缩数据,所以这里只打印彩色帧的字节数,不能直接按三个字节一个像素取 RGB。FrameSet、DepthFrame 与时间戳接口
if (auto d = frames->getDepthFrame(); d && !got_depth) {
got_depth = true;
std::cout << "Depth timestamp_us=" << d->getTimeStampUs() << "\n";
const auto scale = d->getValueScale();
if (d->getPixelType() == OB_PIXEL_DEPTH && std::isfinite(scale) &&
scale > 0 && d->getData() && d->getDataSize() >= sizeof(uint16_t)) {
uint16_t raw = 0;
std::memcpy(&raw, d->getData(), sizeof(raw));
if (raw) std::cout << "Depth(0,0)_mm=" << raw * scale << "\n";
}
}
if (auto c = frames->getColorFrame(); c && !got_color) {
got_color = true;
std::cout << "Color timestamp_us=" << c->getTimeStampUs()
<< ", bytes=" << c->getDataSize() << "\n";
}
if ((!want_depth || got_depth) && (!want_color || got_color)) break;
}
pipe.stop();
if ((want_depth && !got_depth) || (want_color && !got_color)) {
std::cerr << "Timed out waiting for a selected stream\n";
return 2;
}
} catch (const std::exception& e) {
std::cerr << e.what() << "\n";
return 1;
}
}getData() 返回的内存由帧对象持有。上面的代码在帧还有效时就读完了像素。如果把数据交给另一个线程,队列里应保留帧的 shared_ptr,或者复制所需字节。只传裸指针,等帧对象释放后再读取就不可靠了。
在 macOS arm64 上运行
从官方发布页取得 macOS 包。下面的 sdk_dir 指向含有 include 和 lib 的 SDK 目录。编译时链接 libOrbbecSDK,运行时把同一版本的 extensions 目录传给程序。Orbbec SDK v2.10.6 发布包
sdk_dir=/absolute/path/to/orbbec-sdk
lib_dir="$sdk_dir/lib"
clang++ -std=c++17 -I"$sdk_dir/include" read_bag.cpp \
-L"$lib_dir" -lOrbbecSDK -Wl,-rpath,"$lib_dir" -o read_bag
./read_bag "$lib_dir/extensions" recording.bag我用两份录像跑了同一段代码。以下是程序枚举到的流,帧率来自各自的 profile。
| 录像 | 深度流 | 彩色流 | 读取结果 |
|---|---|---|---|
| 纯深度 | 640 × 400,10 FPS,Y16 | 无 | 收到深度帧后退出 |
| 深度和彩色 | 424 × 266,30 FPS,Y16 | 1280 × 720,30 FPS,MJPG | 两路各收到一帧后退出 |
第二次运行时,先收到彩色帧,再收到深度帧。两次取帧各自成功,只说明两路都能读通。之后如果要按彩色图上的位置查询深度,还要处理两件事:用设备时间戳配对帧,再把像素坐标对齐。相同的帧率并不能替代这两步。
如果要读完整段录像
首帧示例读够就调用 pipe.stop()。完整回放可以用 setPlaybackStatusChangeCallback() 接收状态变化。采用 Pipeline::start(config, callback) 异步收帧时,文件读取结束和最后几个帧回调可能先后到达。项目里的做法是收到 OB_PLAYBACK_STOPPED 后先标记结束待处理,给尾帧留出排空时间,再结束读取。这里只验证了两份录像的首帧,不以这次运行推算完整录像的帧数。
Orbbec SDK 使用笔记(一):在 macOS 上读取 .bag 录像的深度和彩色帧
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法