add typed Arrow payloads and a dora-image-view dataflow
dora-image-view takes std/media/v1/Image, and dora's compatibility graph has no edge from std/core/v1/UInt8 to it, so wiring the two failed `dora validate` whatever the node actually sent. Its flat-buffer fallback would not have decoded these frames either: it reads width/height metadata, and this node only published shape. image and depth now go out as the Image struct, camera_info and point_cloud as Float32 arrays and camera_state as a string, all built against the Arrow C data interface in src/arrow so the node still links no Arrow C++. Shape, dtype and encoding metadata are dropped where the payload describes itself; intrinsics, frame_id and both timestamps are unchanged. tests/run.sh covers the packing without a camera or the SDK, round-tripping each array through pyarrow and rebuilding it under the sanitizers. Breaking: image_bgr is renamed to image and every payload changes shape. dataflow_plot.yml is replaced by dataflow_view.yml, which wires the viewer from a tag and repeats its contract in input_types so validate checks both sides of the wiring.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// C entry points for tests/test_arrow.py.
|
||||
//
|
||||
// The Python side cannot call the C++ builders, so each one is wrapped here and
|
||||
// the payload is passed in from the test rather than fixed in this file.
|
||||
|
||||
#include "arrow/array.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
void hand_over(void* array, void* schema, const arrow_c::Exported& exported) {
|
||||
// The caller owns the two structs, exactly as dora does: it reads them and
|
||||
// is then responsible for releasing what they point at.
|
||||
std::memcpy(array, &exported.array, sizeof(exported.array));
|
||||
std::memcpy(schema, &exported.schema, sizeof(exported.schema));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void zed_export_image(void* array, void* schema, uint32_t width, uint32_t height,
|
||||
const char* encoding, const uint8_t* data, size_t size) {
|
||||
hand_over(array, schema, arrow_c::image(width, height, encoding, data, size));
|
||||
}
|
||||
|
||||
void zed_export_float32(void* array, void* schema, const float* values, size_t count) {
|
||||
hand_over(array, schema, arrow_c::float32(values, count));
|
||||
}
|
||||
|
||||
void zed_export_string(void* array, void* schema, const char* text, size_t size) {
|
||||
hand_over(array, schema, arrow_c::string(text, size));
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,54 @@
|
||||
// Release has to free the whole tree: the array nodes, the schema nodes, and
|
||||
// the storage their buffers point into. Nothing here asserts that directly -
|
||||
// the sanitizers tests/run.sh builds this with are the assertion, and a node
|
||||
// that leaks per frame is a node that dies on a long run.
|
||||
|
||||
#include "arrow/array.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void release(arrow_c::Exported& exported) {
|
||||
exported.array.release(&exported.array);
|
||||
exported.schema.release(&exported.schema);
|
||||
|
||||
// A released struct marks itself, which is how a consumer knows not to
|
||||
// release it twice.
|
||||
assert(exported.array.release == nullptr);
|
||||
assert(exported.schema.release == nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::vector<uint8_t> pixels(4 * 4 * 3, 7);
|
||||
const std::vector<float> depth(4 * 4, 1500.0F);
|
||||
const std::string state = "streaming";
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
arrow_c::Exported image =
|
||||
arrow_c::image(4, 4, "bgr8", pixels.data(), pixels.size());
|
||||
assert(image.array.length == 1);
|
||||
assert(image.array.n_children == 4);
|
||||
release(image);
|
||||
|
||||
arrow_c::Exported depth_image =
|
||||
arrow_c::image(4, 4, "32FC1", reinterpret_cast<const uint8_t*>(depth.data()),
|
||||
depth.size() * sizeof(float));
|
||||
release(depth_image);
|
||||
|
||||
arrow_c::Exported values = arrow_c::float32(depth.data(), depth.size());
|
||||
assert(values.array.length == static_cast<int64_t>(depth.size()));
|
||||
release(values);
|
||||
|
||||
arrow_c::Exported text = arrow_c::string(state.data(), state.size());
|
||||
assert(text.array.length == 1);
|
||||
release(text);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and run the payload tests.
|
||||
#
|
||||
# They cover src/arrow only, which is the part of the node that has no ZED SDK,
|
||||
# no CUDA and no dora dependency - so this runs anywhere, and is what CI runs.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
OUT_DIR="${REPO_DIR}/build/tests"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
# Sanitized: the release callbacks have to free the whole exported tree, and a
|
||||
# leak there would be one per frame.
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -g -fsanitize=address,undefined \
|
||||
-I "${REPO_DIR}/src" \
|
||||
-o "${OUT_DIR}/arrow_test" \
|
||||
"${REPO_DIR}/src/arrow/array.cc" "${REPO_DIR}/tests/arrow_test.cc"
|
||||
"${OUT_DIR}/arrow_test"
|
||||
echo "ok arrow_test"
|
||||
|
||||
# Unsanitized: pyarrow is the consumer standing in for dora, and loads the same
|
||||
# code as a shared library.
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -O2 -shared -fPIC \
|
||||
-I "${REPO_DIR}/src" \
|
||||
-o "${OUT_DIR}/libzed_arrow.so" \
|
||||
"${REPO_DIR}/src/arrow/array.cc" "${REPO_DIR}/tests/arrow_export.cc"
|
||||
uv run --no-project --with pyarrow python "${REPO_DIR}/tests/test_arrow.py"
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Round-trip every exported payload through a real Arrow consumer.
|
||||
|
||||
The node builds its Arrow arrays by hand against the C data interface rather
|
||||
than linking Arrow C++, so this is what shows that what dora imports on the
|
||||
other side of `send_arrow_output` is what went in. It needs neither a camera nor
|
||||
the ZED SDK.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
LIBRARY = pathlib.Path(__file__).resolve().parent.parent / "build" / "tests" / "libzed_arrow.so"
|
||||
|
||||
# ArrowArray is ten machine words and ArrowSchema nine, and both want the
|
||||
# 8-byte alignment an array of uint64 gives.
|
||||
WORDS = 16
|
||||
# Index of the `release` field in ArrowArray: a consumer that took ownership
|
||||
# zeroes it, which is how this test sees the move happen.
|
||||
RELEASE = 8
|
||||
|
||||
IMAGE_TYPE = pa.struct([
|
||||
("width", pa.uint32()),
|
||||
("height", pa.uint32()),
|
||||
("encoding", pa.utf8()),
|
||||
("data", pa.large_binary()),
|
||||
])
|
||||
|
||||
|
||||
def load():
|
||||
"""Load the test library and declare the argument types of its exports."""
|
||||
library = ctypes.CDLL(str(LIBRARY))
|
||||
|
||||
library.zed_export_image.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32,
|
||||
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t,
|
||||
]
|
||||
library.zed_export_float32.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_size_t,
|
||||
]
|
||||
library.zed_export_string.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t,
|
||||
]
|
||||
return library
|
||||
|
||||
|
||||
def structs():
|
||||
"""Allocate the ArrowArray and ArrowSchema the producer fills in."""
|
||||
return (ctypes.c_uint64 * WORDS)(), (ctypes.c_uint64 * WORDS)()
|
||||
|
||||
|
||||
def imported(array, schema):
|
||||
"""Import the pair as pyarrow does, taking ownership of both."""
|
||||
result = pa.Array._import_from_c(ctypes.addressof(array), ctypes.addressof(schema))
|
||||
assert array[RELEASE] == 0, "the consumer should have taken ownership of the array"
|
||||
return result
|
||||
|
||||
|
||||
def test_image(library):
|
||||
"""An image row carries its own dimensions, encoding and bytes."""
|
||||
array, schema = structs()
|
||||
pixels = bytes(range(12))
|
||||
|
||||
library.zed_export_image(
|
||||
ctypes.byref(array), ctypes.byref(schema), 2, 2, b"bgr8", pixels, len(pixels)
|
||||
)
|
||||
result = imported(array, schema)
|
||||
|
||||
assert result.type == IMAGE_TYPE, result.type
|
||||
assert result.to_pylist() == [
|
||||
{"width": 2, "height": 2, "encoding": "bgr8", "data": pixels}
|
||||
]
|
||||
|
||||
|
||||
def test_depth(library):
|
||||
"""A depth map is an image whose bytes are float32 millimetres."""
|
||||
array, schema = structs()
|
||||
millimetres = [1500.0, 0.0, 250.5, 4000.0]
|
||||
payload = pa.array(millimetres, type=pa.float32()).buffers()[1].to_pybytes()
|
||||
|
||||
library.zed_export_image(
|
||||
ctypes.byref(array), ctypes.byref(schema), 2, 2, b"32FC1", payload, len(payload)
|
||||
)
|
||||
row = imported(array, schema).to_pylist()[0]
|
||||
|
||||
assert row["encoding"] == "32FC1"
|
||||
assert pa.Array.from_buffers(
|
||||
pa.float32(), 4, [None, pa.py_buffer(row["data"])]
|
||||
).to_pylist() == millimetres
|
||||
|
||||
|
||||
def test_float32(library):
|
||||
"""Intrinsics and the point cloud go out as plain float32 values."""
|
||||
array, schema = structs()
|
||||
values = (ctypes.c_float * 4)(700.5, 700.5, 640.0, 360.0)
|
||||
|
||||
library.zed_export_float32(ctypes.byref(array), ctypes.byref(schema), values, 4)
|
||||
result = imported(array, schema)
|
||||
|
||||
assert result.type == pa.float32()
|
||||
assert result.to_pylist() == [700.5, 700.5, 640.0, 360.0]
|
||||
|
||||
|
||||
def test_string(library):
|
||||
"""The camera state goes out as one UTF-8 string."""
|
||||
array, schema = structs()
|
||||
|
||||
library.zed_export_string(ctypes.byref(array), ctypes.byref(schema), b"streaming", 9)
|
||||
result = imported(array, schema)
|
||||
|
||||
assert result.type == pa.utf8()
|
||||
assert result.to_pylist() == ["streaming"]
|
||||
|
||||
|
||||
def main():
|
||||
"""Run every test, reporting the first failure."""
|
||||
library = load()
|
||||
|
||||
for test in (test_image, test_depth, test_float32, test_string):
|
||||
test(library)
|
||||
print(f"ok {test.__name__}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user