refactor ZED capture into modular camera class with env config
Extract camera lifecycle (open, warmup, grab, close) and
environment-based configuration into src/zed/camera.{h,cc}.
Thin main.cc now only handles the dora event loop.
Closes #2
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to AI coding assistants when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
A dora-rs C++ node that interfaces with ZED stereo cameras. On each tick it grabs a frame and publishes three outputs over the dora dataflow: a BGR image, an XYZRGBA point cloud, and camera intrinsics. Camera management lives in `src/zed/` and the dora event loop in `src/main.cc`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p build && cd build && cmake .. && make install
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is installed to `bin/dora_zed` relative to the repo root.
|
||||||
|
|
||||||
|
**Requirements:** CMake 3.21+, ZED SDK 4.x (at `/usr/local/zed`), CUDA Toolkit, OpenCV 4.x, Rust stable toolchain (for dora-rs C++ bindings).
|
||||||
|
|
||||||
|
The first build clones dora-rs from GitHub and compiles `dora-node-api-cxx` via Cargo. This takes a while. Subsequent builds are incremental. If `DORA_ROOT_DIR` is set to a local dora checkout, it builds against that instead.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dora up
|
||||||
|
dora start dataflow.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Camera behavior is configured entirely through environment variables (see README.md for the full list: `ZED_RESOLUTION`, `ZED_FPS`, `ZED_DEPTH_MODE`, etc.).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **`src/zed/camera.h`** — `zed::Config`, `zed::Calibration`, and `zed::Camera` class. Camera wraps the ZED SDK lifecycle: open (with retry loop), warmup, grab (image + point cloud), calibration, and close.
|
||||||
|
- **`src/zed/camera.cc`** — Implementation. Env-variable parsing helpers and SDK enum parsers live in an anonymous namespace.
|
||||||
|
- **`src/main.cc`** — Dora event loop. Receives `tick` inputs, calls `camera.grab()`, and sends `image_bgr`, `point_cloud`, and `camera_info` outputs with metadata. Handles reconnection on grab failure.
|
||||||
|
- **`DoraTargets.cmake`** — Handles building or fetching dora-rs and generating the CXX bridge files. Two paths: local (`DORA_ROOT_DIR`) or git clone. Produces `dora_node_api_cxx` library and the `node_bridge.cc` glue.
|
||||||
|
- **`dataflow.yml`** — Example dora dataflow definition using the `git:` source format.
|
||||||
|
|
||||||
|
## Key APIs
|
||||||
|
|
||||||
|
- **dora C++ API** (`<dora-node-api.h>`): `init_dora_node()`, `dora_node.events->next()`, `event_type()`, `send_output_with_metadata()`, `new_metadata()`.
|
||||||
|
- **ZED SDK** (`<sl/Camera.hpp>`): `sl::Camera`, `sl::InitParameters`, `sl::RuntimeParameters`, `sl::Mat`.
|
||||||
|
- Output data is sent as raw byte slices (`rust::Slice<const uint8_t>`) with typed metadata (shape, dtype, encoding).
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Do not mention AI tools in commit messages or documentation.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
See [AGENTS.md](AGENTS.md) for repository guidance.
|
||||||
+2
-1
@@ -16,12 +16,13 @@ find_package(OpenCV REQUIRED)
|
|||||||
link_directories(${dora_link_dirs})
|
link_directories(${dora_link_dirs})
|
||||||
link_directories(${ZED_LIBRARY_DIR})
|
link_directories(${ZED_LIBRARY_DIR})
|
||||||
|
|
||||||
add_executable(dora_zed main.cc ${node_bridge})
|
add_executable(dora_zed src/main.cc src/zed/camera.cc ${node_bridge})
|
||||||
add_dependencies(dora_zed Dora_cxx)
|
add_dependencies(dora_zed Dora_cxx)
|
||||||
|
|
||||||
target_include_directories(
|
target_include_directories(
|
||||||
dora_zed
|
dora_zed
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
${dora_cxx_include_dir}
|
${dora_cxx_include_dir}
|
||||||
${dora_c_include_dir}
|
${dora_c_include_dir}
|
||||||
${ZED_INCLUDE_DIRS}
|
${ZED_INCLUDE_DIRS}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# GEMINI.md
|
||||||
|
|
||||||
|
See [AGENTS.md](AGENTS.md) for repository guidance.
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
#include <dora-node-api.h>
|
|
||||||
#include <sl/Camera.hpp>
|
|
||||||
#include <opencv2/opencv.hpp>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cctype>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <initializer_list>
|
|
||||||
#include <iostream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <tuple>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int get_env_int(const char* name, int default_value) {
|
|
||||||
const char* value = std::getenv(name);
|
|
||||||
if (!value || std::string(value).empty()) {
|
|
||||||
return default_value;
|
|
||||||
}
|
|
||||||
return std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get_env_bool(const char* name, bool default_value) {
|
|
||||||
const char* value = std::getenv(name);
|
|
||||||
if (!value || std::string(value).empty()) {
|
|
||||||
return default_value;
|
|
||||||
}
|
|
||||||
std::string v(value);
|
|
||||||
for (auto& c : v) {
|
|
||||||
c = static_cast<char>(std::tolower(c));
|
|
||||||
}
|
|
||||||
return v == "1" || v == "true" || v == "yes" || v == "on";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string get_env_str(const char* name, const std::string& default_value) {
|
|
||||||
const char* value = std::getenv(name);
|
|
||||||
if (!value || std::string(value).empty()) {
|
|
||||||
return default_value;
|
|
||||||
}
|
|
||||||
return std::string(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
sl::RESOLUTION parse_resolution(int resolution) {
|
|
||||||
switch (resolution) {
|
|
||||||
case 1080:
|
|
||||||
return sl::RESOLUTION::HD1080;
|
|
||||||
case 2000:
|
|
||||||
return sl::RESOLUTION::HD2K;
|
|
||||||
case 720:
|
|
||||||
default:
|
|
||||||
return sl::RESOLUTION::HD720;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sl::DEPTH_MODE parse_depth_mode(const std::string& mode) {
|
|
||||||
if (mode == "PERFORMANCE") {
|
|
||||||
return sl::DEPTH_MODE::PERFORMANCE;
|
|
||||||
}
|
|
||||||
if (mode == "QUALITY") {
|
|
||||||
return sl::DEPTH_MODE::QUALITY;
|
|
||||||
}
|
|
||||||
if (mode == "NONE") {
|
|
||||||
return sl::DEPTH_MODE::NONE;
|
|
||||||
}
|
|
||||||
return sl::DEPTH_MODE::NEURAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
sl::FLIP_MODE parse_flip(const std::string& mode) {
|
|
||||||
if (mode == "OFF") {
|
|
||||||
return sl::FLIP_MODE::OFF;
|
|
||||||
}
|
|
||||||
return sl::FLIP_MODE::ON;
|
|
||||||
}
|
|
||||||
|
|
||||||
rust::Vec<int64_t> make_shape(int height, int width, int channels) {
|
|
||||||
rust::Vec<int64_t> shape;
|
|
||||||
shape.push_back(height);
|
|
||||||
shape.push_back(width);
|
|
||||||
shape.push_back(channels);
|
|
||||||
return shape;
|
|
||||||
}
|
|
||||||
|
|
||||||
rust::Vec<double> make_distortion(const std::vector<double>& distortion) {
|
|
||||||
rust::Vec<double> values;
|
|
||||||
for (double v : distortion) {
|
|
||||||
values.push_back(v);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
rust::Vec<int64_t> make_vector_int(std::initializer_list<int64_t> values) {
|
|
||||||
rust::Vec<int64_t> out;
|
|
||||||
for (int64_t v : values) {
|
|
||||||
out.push_back(v);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
void list_available_cameras() {
|
|
||||||
auto devices = sl::Camera::getDeviceList();
|
|
||||||
std::cout << "Detected ZED cameras: " << devices.size() << std::endl;
|
|
||||||
for (std::size_t i = 0; i < devices.size(); i++) {
|
|
||||||
const auto& dev = devices[i];
|
|
||||||
std::cout << " - [" << i << "] model=" << sl::toString(dev.camera_model)
|
|
||||||
<< ", serial=" << dev.serial_number << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool open_camera(sl::Camera& zed, const sl::InitParameters& init_params) {
|
|
||||||
sl::ERROR_CODE err = zed.open(init_params);
|
|
||||||
if (err != sl::ERROR_CODE::SUCCESS) {
|
|
||||||
std::cerr << "Failed to open ZED camera: " << sl::toString(err) << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ensure_camera_ready(
|
|
||||||
sl::Camera& zed,
|
|
||||||
const sl::InitParameters& init_params,
|
|
||||||
int sleep_ms
|
|
||||||
) {
|
|
||||||
while (true) {
|
|
||||||
if (open_camera(zed, init_params)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
auto dora_node = init_dora_node();
|
|
||||||
|
|
||||||
const int resolution = get_env_int("ZED_RESOLUTION", 720);
|
|
||||||
const int fps = get_env_int("ZED_FPS", 15);
|
|
||||||
const std::string depth_mode = get_env_str("ZED_DEPTH_MODE", "NEURAL");
|
|
||||||
const int depth_min_mm = get_env_int("ZED_DEPTH_MIN_MM", 10);
|
|
||||||
const int depth_max_mm = get_env_int("ZED_DEPTH_MAX_MM", 500);
|
|
||||||
const bool depth_fill = get_env_bool("ZED_DEPTH_FILL", false);
|
|
||||||
const std::string flip = get_env_str("ZED_FLIP", "ON");
|
|
||||||
const int warmup_frames = get_env_int("ZED_WARMUP_FRAMES", 30);
|
|
||||||
const int retry_interval_seconds = get_env_int("ZED_RETRY_INTERVAL_SECONDS", 5);
|
|
||||||
const int retry_interval_ms = retry_interval_seconds * 1000;
|
|
||||||
|
|
||||||
sl::InitParameters init_params;
|
|
||||||
init_params.camera_resolution = parse_resolution(resolution);
|
|
||||||
init_params.camera_fps = fps;
|
|
||||||
init_params.depth_mode = parse_depth_mode(depth_mode);
|
|
||||||
init_params.coordinate_units = sl::UNIT::MILLIMETER;
|
|
||||||
init_params.camera_image_flip = parse_flip(flip);
|
|
||||||
init_params.depth_minimum_distance = depth_min_mm;
|
|
||||||
init_params.depth_maximum_distance = depth_max_mm;
|
|
||||||
|
|
||||||
list_available_cameras();
|
|
||||||
|
|
||||||
sl::Camera zed;
|
|
||||||
ensure_camera_ready(zed, init_params, retry_interval_ms);
|
|
||||||
|
|
||||||
sl::RuntimeParameters runtime_params;
|
|
||||||
runtime_params.enable_depth = true;
|
|
||||||
runtime_params.enable_fill_mode = depth_fill;
|
|
||||||
|
|
||||||
sl::Mat image_mat;
|
|
||||||
sl::Mat point_cloud_mat;
|
|
||||||
|
|
||||||
for (int i = 0; i < warmup_frames; i++) {
|
|
||||||
zed.grab(runtime_params);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto reload_calibration = [&]() {
|
|
||||||
sl::CameraInformation info = zed.getCameraInformation();
|
|
||||||
auto calib = info.camera_configuration.calibration_parameters;
|
|
||||||
auto left = calib.left_cam;
|
|
||||||
|
|
||||||
std::vector<double> dist;
|
|
||||||
dist.reserve(std::size(left.disto));
|
|
||||||
for (double v : left.disto) {
|
|
||||||
dist.push_back(v);
|
|
||||||
}
|
|
||||||
return std::tuple<float, float, float, float, std::vector<double>>(
|
|
||||||
left.fx, left.fy, left.cx, left.cy, std::move(dist));
|
|
||||||
};
|
|
||||||
|
|
||||||
auto [fx, fy, cx, cy, distortion] = reload_calibration();
|
|
||||||
|
|
||||||
int64_t frame_id = 0;
|
|
||||||
while (true) {
|
|
||||||
auto event = dora_node.events->next();
|
|
||||||
auto type = event_type(event);
|
|
||||||
|
|
||||||
if (type == DoraEventType::Stop || type == DoraEventType::AllInputsClosed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type != DoraEventType::Input) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto input = event_as_input(std::move(event));
|
|
||||||
(void)input;
|
|
||||||
|
|
||||||
if (zed.grab(runtime_params) != sl::ERROR_CODE::SUCCESS) {
|
|
||||||
std::cerr << "Grab failed; attempting to reconnect..." << std::endl;
|
|
||||||
zed.close();
|
|
||||||
list_available_cameras();
|
|
||||||
ensure_camera_ready(zed, init_params, retry_interval_ms);
|
|
||||||
std::tie(fx, fy, cx, cy, distortion) = reload_calibration();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
zed.retrieveImage(image_mat, sl::VIEW::LEFT);
|
|
||||||
zed.retrieveMeasure(point_cloud_mat, sl::MEASURE::XYZRGBA);
|
|
||||||
|
|
||||||
int width = image_mat.getWidth();
|
|
||||||
int height = image_mat.getHeight();
|
|
||||||
|
|
||||||
cv::Mat image_rgba(height, width, CV_8UC4, image_mat.getPtr<sl::uchar1>());
|
|
||||||
cv::Mat image_bgr;
|
|
||||||
cv::cvtColor(image_rgba, image_bgr, cv::COLOR_BGRA2BGR);
|
|
||||||
|
|
||||||
const auto image_size = static_cast<size_t>(image_bgr.total() * image_bgr.elemSize());
|
|
||||||
rust::Slice<const uint8_t> image_slice{
|
|
||||||
reinterpret_cast<const uint8_t*>(image_bgr.data), image_size};
|
|
||||||
|
|
||||||
auto image_metadata = new_metadata();
|
|
||||||
image_metadata->set_string("dtype", "uint8");
|
|
||||||
image_metadata->set_string("encoding", "bgr8");
|
|
||||||
image_metadata->set_string("layout", "HWC");
|
|
||||||
image_metadata->set_list_int("shape", make_shape(height, width, 3));
|
|
||||||
image_metadata->set_float("fx", fx);
|
|
||||||
image_metadata->set_float("fy", fy);
|
|
||||||
image_metadata->set_float("cx", cx);
|
|
||||||
image_metadata->set_float("cy", cy);
|
|
||||||
image_metadata->set_list_float("distortion", make_distortion(distortion));
|
|
||||||
image_metadata->set_int("frame_id", frame_id++);
|
|
||||||
image_metadata->set_int("timestamp_ns", static_cast<int64_t>(
|
|
||||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
||||||
std::chrono::steady_clock::now().time_since_epoch()
|
|
||||||
).count()
|
|
||||||
));
|
|
||||||
|
|
||||||
auto image_result = send_output_with_metadata(
|
|
||||||
dora_node.send_output, "image_bgr", image_slice, std::move(image_metadata));
|
|
||||||
if (!std::string(image_result.error).empty()) {
|
|
||||||
std::cerr << "Error sending image_bgr: " << std::string(image_result.error) << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
int pc_width = point_cloud_mat.getWidth();
|
|
||||||
int pc_height = point_cloud_mat.getHeight();
|
|
||||||
const size_t pc_size = static_cast<size_t>(pc_width * pc_height * sizeof(sl::float4));
|
|
||||||
const auto* pc_ptr = reinterpret_cast<const uint8_t*>(point_cloud_mat.getPtr<sl::float4>());
|
|
||||||
rust::Slice<const uint8_t> pc_slice{pc_ptr, pc_size};
|
|
||||||
|
|
||||||
auto pc_metadata = new_metadata();
|
|
||||||
pc_metadata->set_string("dtype", "float32");
|
|
||||||
pc_metadata->set_string("layout", "HWC");
|
|
||||||
pc_metadata->set_string("channels", "XYZRGBA");
|
|
||||||
pc_metadata->set_string("units", "mm");
|
|
||||||
pc_metadata->set_list_int("shape", make_shape(pc_height, pc_width, 4));
|
|
||||||
pc_metadata->set_float("fx", fx);
|
|
||||||
pc_metadata->set_float("fy", fy);
|
|
||||||
pc_metadata->set_float("cx", cx);
|
|
||||||
pc_metadata->set_float("cy", cy);
|
|
||||||
pc_metadata->set_list_float("distortion", make_distortion(distortion));
|
|
||||||
|
|
||||||
auto pc_result = send_output_with_metadata(
|
|
||||||
dora_node.send_output, "point_cloud", pc_slice, std::move(pc_metadata));
|
|
||||||
if (!std::string(pc_result.error).empty()) {
|
|
||||||
std::cerr << "Error sending point_cloud: " << std::string(pc_result.error) << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
float camera_data[4] = {fx, fy, cx, cy};
|
|
||||||
rust::Slice<const uint8_t> cam_slice{
|
|
||||||
reinterpret_cast<const uint8_t*>(camera_data), sizeof(camera_data)};
|
|
||||||
|
|
||||||
auto cam_metadata = new_metadata();
|
|
||||||
cam_metadata->set_string("dtype", "float32");
|
|
||||||
cam_metadata->set_string("layout", "C");
|
|
||||||
cam_metadata->set_list_int("shape", make_vector_int({4}));
|
|
||||||
cam_metadata->set_list_float("distortion", make_distortion(distortion));
|
|
||||||
|
|
||||||
auto cam_result = send_output_with_metadata(
|
|
||||||
dora_node.send_output, "camera_info", cam_slice, std::move(cam_metadata));
|
|
||||||
if (!std::string(cam_result.error).empty()) {
|
|
||||||
std::cerr << "Error sending camera_info: " << std::string(cam_result.error) << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zed.close();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#include "zed/camera.h"
|
||||||
|
|
||||||
|
#include <dora-node-api.h>
|
||||||
|
#include <opencv2/opencv.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
rust::Vec<int64_t> make_shape(int height, int width, int channels) {
|
||||||
|
rust::Vec<int64_t> shape;
|
||||||
|
shape.push_back(height);
|
||||||
|
shape.push_back(width);
|
||||||
|
shape.push_back(channels);
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
rust::Vec<double> make_distortion(const std::vector<double>& distortion) {
|
||||||
|
rust::Vec<double> values;
|
||||||
|
for (double v : distortion) {
|
||||||
|
values.push_back(v);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
rust::Vec<int64_t> make_vector_int(std::initializer_list<int64_t> values) {
|
||||||
|
rust::Vec<int64_t> out;
|
||||||
|
for (int64_t v : values) {
|
||||||
|
out.push_back(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto dora_node = init_dora_node();
|
||||||
|
|
||||||
|
auto config = zed::Config::from_env();
|
||||||
|
zed::Camera camera(config);
|
||||||
|
camera.open();
|
||||||
|
camera.warmup();
|
||||||
|
auto calib = camera.calibration();
|
||||||
|
|
||||||
|
int64_t frame_id = 0;
|
||||||
|
while (true) {
|
||||||
|
auto event = dora_node.events->next();
|
||||||
|
auto type = event_type(event);
|
||||||
|
|
||||||
|
if (type == DoraEventType::Stop || type == DoraEventType::AllInputsClosed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != DoraEventType::Input) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto input = event_as_input(std::move(event));
|
||||||
|
(void)input;
|
||||||
|
|
||||||
|
if (!camera.grab()) {
|
||||||
|
std::cerr << "Grab failed; attempting to reconnect..." << std::endl;
|
||||||
|
camera.close();
|
||||||
|
camera.open();
|
||||||
|
calib = camera.calibration();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::Mat& image_mat = camera.image();
|
||||||
|
sl::Mat& point_cloud_mat = camera.point_cloud();
|
||||||
|
|
||||||
|
int width = image_mat.getWidth();
|
||||||
|
int height = image_mat.getHeight();
|
||||||
|
|
||||||
|
cv::Mat image_rgba(height, width, CV_8UC4, image_mat.getPtr<sl::uchar1>());
|
||||||
|
cv::Mat image_bgr;
|
||||||
|
cv::cvtColor(image_rgba, image_bgr, cv::COLOR_BGRA2BGR);
|
||||||
|
|
||||||
|
const auto image_size = static_cast<size_t>(image_bgr.total() * image_bgr.elemSize());
|
||||||
|
rust::Slice<const uint8_t> image_slice{
|
||||||
|
reinterpret_cast<const uint8_t*>(image_bgr.data), image_size};
|
||||||
|
|
||||||
|
auto image_metadata = new_metadata();
|
||||||
|
image_metadata->set_string("dtype", "uint8");
|
||||||
|
image_metadata->set_string("encoding", "bgr8");
|
||||||
|
image_metadata->set_string("layout", "HWC");
|
||||||
|
image_metadata->set_list_int("shape", make_shape(height, width, 3));
|
||||||
|
image_metadata->set_float("fx", calib.fx);
|
||||||
|
image_metadata->set_float("fy", calib.fy);
|
||||||
|
image_metadata->set_float("cx", calib.cx);
|
||||||
|
image_metadata->set_float("cy", calib.cy);
|
||||||
|
image_metadata->set_list_float("distortion", make_distortion(calib.distortion));
|
||||||
|
image_metadata->set_int("frame_id", frame_id++);
|
||||||
|
image_metadata->set_int("timestamp_ns", static_cast<int64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()
|
||||||
|
).count()
|
||||||
|
));
|
||||||
|
|
||||||
|
auto image_result = send_output_with_metadata(
|
||||||
|
dora_node.send_output, "image_bgr", image_slice, std::move(image_metadata));
|
||||||
|
if (!std::string(image_result.error).empty()) {
|
||||||
|
std::cerr << "Error sending image_bgr: " << std::string(image_result.error) << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pc_width = point_cloud_mat.getWidth();
|
||||||
|
int pc_height = point_cloud_mat.getHeight();
|
||||||
|
const size_t pc_size = static_cast<size_t>(pc_width * pc_height * sizeof(sl::float4));
|
||||||
|
const auto* pc_ptr = reinterpret_cast<const uint8_t*>(point_cloud_mat.getPtr<sl::float4>());
|
||||||
|
rust::Slice<const uint8_t> pc_slice{pc_ptr, pc_size};
|
||||||
|
|
||||||
|
auto pc_metadata = new_metadata();
|
||||||
|
pc_metadata->set_string("dtype", "float32");
|
||||||
|
pc_metadata->set_string("layout", "HWC");
|
||||||
|
pc_metadata->set_string("channels", "XYZRGBA");
|
||||||
|
pc_metadata->set_string("units", "mm");
|
||||||
|
pc_metadata->set_list_int("shape", make_shape(pc_height, pc_width, 4));
|
||||||
|
pc_metadata->set_float("fx", calib.fx);
|
||||||
|
pc_metadata->set_float("fy", calib.fy);
|
||||||
|
pc_metadata->set_float("cx", calib.cx);
|
||||||
|
pc_metadata->set_float("cy", calib.cy);
|
||||||
|
pc_metadata->set_list_float("distortion", make_distortion(calib.distortion));
|
||||||
|
|
||||||
|
auto pc_result = send_output_with_metadata(
|
||||||
|
dora_node.send_output, "point_cloud", pc_slice, std::move(pc_metadata));
|
||||||
|
if (!std::string(pc_result.error).empty()) {
|
||||||
|
std::cerr << "Error sending point_cloud: " << std::string(pc_result.error) << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
float camera_data[4] = {calib.fx, calib.fy, calib.cx, calib.cy};
|
||||||
|
rust::Slice<const uint8_t> cam_slice{
|
||||||
|
reinterpret_cast<const uint8_t*>(camera_data), sizeof(camera_data)};
|
||||||
|
|
||||||
|
auto cam_metadata = new_metadata();
|
||||||
|
cam_metadata->set_string("dtype", "float32");
|
||||||
|
cam_metadata->set_string("layout", "C");
|
||||||
|
cam_metadata->set_list_int("shape", make_vector_int({4}));
|
||||||
|
cam_metadata->set_list_float("distortion", make_distortion(calib.distortion));
|
||||||
|
|
||||||
|
auto cam_result = send_output_with_metadata(
|
||||||
|
dora_node.send_output, "camera_info", cam_slice, std::move(cam_metadata));
|
||||||
|
if (!std::string(cam_result.error).empty()) {
|
||||||
|
std::cerr << "Error sending camera_info: " << std::string(cam_result.error) << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include "zed/camera.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int get_env_int(const char* name, int default_value) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
if (!value || std::string(value).empty()) {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return std::stoi(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_env_bool(const char* name, bool default_value) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
if (!value || std::string(value).empty()) {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
std::string v(value);
|
||||||
|
for (auto& c : v) {
|
||||||
|
c = static_cast<char>(std::tolower(c));
|
||||||
|
}
|
||||||
|
return v == "1" || v == "true" || v == "yes" || v == "on";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_env_str(const char* name, const std::string& default_value) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
if (!value || std::string(value).empty()) {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return std::string(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::RESOLUTION parse_resolution(int resolution) {
|
||||||
|
switch (resolution) {
|
||||||
|
case 1080:
|
||||||
|
return sl::RESOLUTION::HD1080;
|
||||||
|
case 2000:
|
||||||
|
return sl::RESOLUTION::HD2K;
|
||||||
|
case 720:
|
||||||
|
default:
|
||||||
|
return sl::RESOLUTION::HD720;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::DEPTH_MODE parse_depth_mode(const std::string& mode) {
|
||||||
|
if (mode == "PERFORMANCE") {
|
||||||
|
return sl::DEPTH_MODE::PERFORMANCE;
|
||||||
|
}
|
||||||
|
if (mode == "QUALITY") {
|
||||||
|
return sl::DEPTH_MODE::QUALITY;
|
||||||
|
}
|
||||||
|
if (mode == "NONE") {
|
||||||
|
return sl::DEPTH_MODE::NONE;
|
||||||
|
}
|
||||||
|
return sl::DEPTH_MODE::NEURAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::FLIP_MODE parse_flip(const std::string& mode) {
|
||||||
|
if (mode == "OFF") {
|
||||||
|
return sl::FLIP_MODE::OFF;
|
||||||
|
}
|
||||||
|
return sl::FLIP_MODE::ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
void list_available_cameras() {
|
||||||
|
auto devices = sl::Camera::getDeviceList();
|
||||||
|
std::cout << "Detected ZED cameras: " << devices.size() << std::endl;
|
||||||
|
for (std::size_t i = 0; i < devices.size(); i++) {
|
||||||
|
const auto& dev = devices[i];
|
||||||
|
std::cout << " - [" << i << "] model=" << sl::toString(dev.camera_model)
|
||||||
|
<< ", serial=" << dev.serial_number << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace zed {
|
||||||
|
|
||||||
|
Config Config::from_env() {
|
||||||
|
Config c;
|
||||||
|
c.resolution = get_env_int("ZED_RESOLUTION", c.resolution);
|
||||||
|
c.fps = get_env_int("ZED_FPS", c.fps);
|
||||||
|
c.depth_mode = get_env_str("ZED_DEPTH_MODE", c.depth_mode);
|
||||||
|
c.depth_min_mm = get_env_int("ZED_DEPTH_MIN_MM", c.depth_min_mm);
|
||||||
|
c.depth_max_mm = get_env_int("ZED_DEPTH_MAX_MM", c.depth_max_mm);
|
||||||
|
c.depth_fill = get_env_bool("ZED_DEPTH_FILL", c.depth_fill);
|
||||||
|
c.flip = get_env_str("ZED_FLIP", c.flip);
|
||||||
|
c.warmup_frames = get_env_int("ZED_WARMUP_FRAMES", c.warmup_frames);
|
||||||
|
int retry_seconds = get_env_int("ZED_RETRY_INTERVAL_SECONDS", 5);
|
||||||
|
c.retry_interval_ms = retry_seconds * 1000;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera::Camera(const Config& config) : config_(config) {
|
||||||
|
init_params_.camera_resolution = parse_resolution(config_.resolution);
|
||||||
|
init_params_.camera_fps = config_.fps;
|
||||||
|
init_params_.depth_mode = parse_depth_mode(config_.depth_mode);
|
||||||
|
init_params_.coordinate_units = sl::UNIT::MILLIMETER;
|
||||||
|
init_params_.camera_image_flip = parse_flip(config_.flip);
|
||||||
|
init_params_.depth_minimum_distance = config_.depth_min_mm;
|
||||||
|
init_params_.depth_maximum_distance = config_.depth_max_mm;
|
||||||
|
|
||||||
|
runtime_params_.enable_depth = true;
|
||||||
|
runtime_params_.enable_fill_mode = config_.depth_fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera::~Camera() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::open() {
|
||||||
|
list_available_cameras();
|
||||||
|
while (true) {
|
||||||
|
sl::ERROR_CODE err = zed_.open(init_params_);
|
||||||
|
if (err == sl::ERROR_CODE::SUCCESS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cerr << "Failed to open ZED camera: " << sl::toString(err) << std::endl;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(config_.retry_interval_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::close() {
|
||||||
|
zed_.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::warmup() {
|
||||||
|
for (int i = 0; i < config_.warmup_frames; i++) {
|
||||||
|
zed_.grab(runtime_params_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Camera::grab() {
|
||||||
|
if (zed_.grab(runtime_params_) != sl::ERROR_CODE::SUCCESS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
zed_.retrieveImage(image_mat_, sl::VIEW::LEFT);
|
||||||
|
zed_.retrieveMeasure(point_cloud_mat_, sl::MEASURE::XYZRGBA);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Calibration Camera::calibration() const {
|
||||||
|
sl::CameraInformation info = zed_.getCameraInformation();
|
||||||
|
auto calib = info.camera_configuration.calibration_parameters;
|
||||||
|
auto left = calib.left_cam;
|
||||||
|
|
||||||
|
Calibration result;
|
||||||
|
result.fx = left.fx;
|
||||||
|
result.fy = left.fy;
|
||||||
|
result.cx = left.cx;
|
||||||
|
result.cy = left.cy;
|
||||||
|
result.distortion.reserve(std::size(left.disto));
|
||||||
|
for (double v : left.disto) {
|
||||||
|
result.distortion.push_back(v);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::Mat& Camera::image() {
|
||||||
|
return image_mat_;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl::Mat& Camera::point_cloud() {
|
||||||
|
return point_cloud_mat_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace zed
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sl/Camera.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace zed {
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
int resolution = 720;
|
||||||
|
int fps = 15;
|
||||||
|
std::string depth_mode = "NEURAL";
|
||||||
|
int depth_min_mm = 10;
|
||||||
|
int depth_max_mm = 500;
|
||||||
|
bool depth_fill = false;
|
||||||
|
std::string flip = "ON";
|
||||||
|
int warmup_frames = 30;
|
||||||
|
int retry_interval_ms = 5000;
|
||||||
|
|
||||||
|
static Config from_env();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Calibration {
|
||||||
|
float fx, fy, cx, cy;
|
||||||
|
std::vector<double> distortion;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Camera {
|
||||||
|
public:
|
||||||
|
explicit Camera(const Config& config);
|
||||||
|
~Camera();
|
||||||
|
|
||||||
|
void open();
|
||||||
|
void close();
|
||||||
|
void warmup();
|
||||||
|
bool grab();
|
||||||
|
Calibration calibration() const;
|
||||||
|
sl::Mat& image();
|
||||||
|
sl::Mat& point_cloud();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Config config_;
|
||||||
|
sl::Camera zed_;
|
||||||
|
sl::InitParameters init_params_;
|
||||||
|
sl::RuntimeParameters runtime_params_;
|
||||||
|
sl::Mat image_mat_;
|
||||||
|
sl::Mat point_cloud_mat_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace zed
|
||||||
Reference in New Issue
Block a user