一份 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,Y161280 × 720,30 FPS,MJPG两路各收到一帧后退出

第二次运行时,先收到彩色帧,再收到深度帧。两次取帧各自成功,只说明两路都能读通。之后如果要按彩色图上的位置查询深度,还要处理两件事:用设备时间戳配对帧,再把像素坐标对齐。相同的帧率并不能替代这两步。

如果要读完整段录像

首帧示例读够就调用 pipe.stop()。完整回放可以用 setPlaybackStatusChangeCallback() 接收状态变化。采用 Pipeline::start(config, callback) 异步收帧时,文件读取结束和最后几个帧回调可能先后到达。项目里的做法是收到 OB_PLAYBACK_STOPPED 后先标记结束待处理,给尾帧留出排空时间,再结束读取。这里只验证了两份录像的首帧,不以这次运行推算完整录像的帧数。