Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bindings/python/src/pipeline/node/ToFBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ void bind_tof(pybind11::module& m, void* pCallstack) {
.def_readonly("depth", &ToFBase::depth, DOC(dai, node, ToFBase, depth), DOC(dai, node, ToFBase, depth))
.def_readonly("amplitude", &ToFBase::amplitude, DOC(dai, node, ToFBase, amplitude), DOC(dai, node, ToFBase, amplitude))
.def_readonly("intensity", &ToFBase::intensity, DOC(dai, node, ToFBase, intensity), DOC(dai, node, ToFBase, intensity))
.def_readonly("confidence", &ToFBase::confidence, DOC(dai, node, ToFBase, confidence), DOC(dai, node, ToFBase, confidence))
.def_readonly("phase", &ToFBase::phase, DOC(dai, node, ToFBase, phase), DOC(dai, node, ToFBase, phase))
.def_readonly("raw", &ToFBase::raw, DOC(dai, node, ToFBase, raw), DOC(dai, node, ToFBase, raw))
.def_readonly("rawInput", &ToFBase::rawInput, DOC(dai, node, ToFBase, rawInput), DOC(dai, node, ToFBase, rawInput))
.def_readonly("initialConfig", &ToFBase::initialConfig, DOC(dai, node, ToFBase, initialConfig), DOC(dai, node, ToFBase, initialConfig))
.def("build",
py::overload_cast<CameraBoardSocket, ToFConfig::Profile, std::optional<float>>(&ToFBase::build),
Expand Down
244 changes: 244 additions & 0 deletions examples/python/ToF/tof_raw_rvc4.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CamelCase instead of snake_case

Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Test script: capture raw depth via Camera + ToFBase pipeline, save to temp dir, verify,
then replay the saved raw sensor frames back into a fresh ToFBase node and plot the result.

Capture pipeline:
┌─────────────────────────┐
│ Camera (CAM_D, ToF) │
└──────────┬──────────────┘
│ .raw
┌──────────────────────────┐
│ ToFBase │
└────┬──────────┬──────────┘
│ .raw │ .depth (also .amplitude, not captured here)
▼ ▼
raw_q depth_q
│ │
▼ ▼
raw_<ts>.npz depth_<ts>.npy <-- saved and validated

Replay pipeline (no Camera node, raw frames fed from disk):
raw_<ts>.npz --> rawInput queue --> ToFBase --> .depth --> depth_q --> matplotlib

Usage:
python tof_raw_rvc4.py
python tof_raw_rvc4.py --socket CAM_D --frames 5
python tof_raw_rvc4.py --ip 192.168.1.100
"""

import argparse
import os
import sys
import tempfile
import time

import matplotlib.pyplot as plt
import numpy as np

import depthai as dai


WARMUP_FRAMES = 10


def parse_args():
parser = argparse.ArgumentParser(description="ToF raw depth save + verify test")
parser.add_argument("--ip", default=None, help="Device IP address (omit for USB)")
parser.add_argument("--socket", default="CAM_D", help="ToF camera board socket (default: CAM_D)")
parser.add_argument("--frames", type=int, default=3,
help="Number of depth frames to save (default: 3)")
parser.add_argument("--fwp", default=None,
help="Optional path to RVC4 firmware package (.tar.xz)")
parser.add_argument("--plot-out", default=None,
help="Path to save the replay depth plot PNG (default: <tempdir>/replay_depth.png)")
return parser.parse_args()


def build_capture_pipeline(socket: dai.CameraBoardSocket, profile):
pipeline = dai.Pipeline()

tof_base = pipeline.create(dai.node.ToFBase)
tof_base.build(boardSocket=socket, profile=profile)

cam = pipeline.create(dai.node.Camera)
cam.setSensorType(dai.CameraSensorType.TOF)
cam.build(boardSocket=tof_base.getBoardSocket())

cam.raw.link(tof_base.rawInput)

# Save the frames actually fed into rawInput, not ToFBase's own (unreliable on RVC4) .raw passthrough.
raw_q = cam.raw.createOutputQueue()
depth_q = tof_base.depth.createOutputQueue()

return pipeline, raw_q, depth_q


def build_replay_pipeline(socket: dai.CameraBoardSocket, profile):
"""Pipeline with only ToFBase (no Camera) -- raw frames are pushed in from the host."""
pipeline = dai.Pipeline()

tof_base = pipeline.create(dai.node.ToFBase)
tof_base.build(boardSocket=socket, profile=profile)

raw_in_q = tof_base.rawInput.createInputQueue()
depth_q = tof_base.depth.createOutputQueue()

return pipeline, raw_in_q, depth_q


def verify_saved_files(out_dir: str, expected_count: int) -> bool:
depth_files = sorted(f for f in os.listdir(out_dir) if f.startswith("depth_") and f.endswith(".npy"))

print(f"\n[Verify] Expected {expected_count} file(s), found {len(depth_files)}")
if len(depth_files) < expected_count:
print(f"[FAIL] Not enough depth files saved.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove extraneous f prefix.

f"[FAIL] Not enough depth files saved." has no placeholders.

🔧 Proposed fix
-        print(f"[FAIL] Not enough depth files saved.")
+        print("Not enough depth files saved.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(f"[FAIL] Not enough depth files saved.")
print("Not enough depth files saved.")
🧰 Tools
🪛 Ruff (0.15.21)

[error] 95-95: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/python/ToF/tof_raw_rvc4.py` at line 95, Remove the unnecessary
f-string prefix from the failure message in the depth-file validation path,
keeping the printed text unchanged.

Source: Linters/SAST tools

return False

all_ok = True
for fname in depth_files:
path = os.path.join(out_dir, fname)
size = os.path.getsize(path)
arr = np.load(path)
nonzero = int(np.count_nonzero(arr))
status = "OK" if nonzero > 0 else "EMPTY"
print(f" {fname}: shape={arr.shape} dtype={arr.dtype} nonzero={nonzero} size={size}B [{status}]")
if nonzero == 0:
print(f" [FAIL] {fname} contains only zeros.")
all_ok = False

return all_ok


def load_raw_frames(out_dir: str):
"""Load raw_<ts>.npz files saved during capture, sorted by timestamp."""
raw_files = sorted(f for f in os.listdir(out_dir) if f.startswith("raw_") and f.endswith(".npz"))
frames = []
for fname in raw_files:
with np.load(os.path.join(out_dir, fname)) as npz:
data = npz["data"]
frame_type = getattr(dai.ImgFrame.Type, str(npz["type"]))
ts = int(fname[len("raw_"):-len(".npz")])
frames.append((ts, data, frame_type))
return frames


def replay_raw_frames(socket: dai.CameraBoardSocket, profile, out_dir: str):
"""Feed saved raw frames back into a fresh ToFBase node and collect the resulting depth frames."""
raw_frames = load_raw_frames(out_dir)
if not raw_frames:
print("[Replay] No raw frames found to replay.")
return []

print(f"\n[Replay] Feeding {len(raw_frames)} saved raw frame(s) back into ToFBase...")
pipeline, raw_in_q, depth_q = build_replay_pipeline(socket, profile)

depth_frames = []
with pipeline as p:
p.start()
for ts, data, frame_type in raw_frames:
img = dai.ImgFrame()
img.setCvFrame(data, frame_type)
raw_in_q.send(img)

depth_msg = depth_q.get()
depth_frames.append((ts, depth_msg.getFrame()))
print(f"[Replay] ts={ts}ms -> depth={depth_frames[-1][1].shape}")

return depth_frames


def plot_depth_frames(depth_frames, out_path: str):
"""Plot replayed depth frames side by side and save to a PNG file."""
n = len(depth_frames)
fig, axes = plt.subplots(1, n, figsize=(5 * n, 4), squeeze=False)

for ax, (ts, depth) in zip(axes[0], depth_frames):
im = ax.imshow(depth, cmap="turbo")
ax.set_title(f"ts={ts}ms")
ax.axis("off")
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)

fig.suptitle("Replayed ToF depth (from saved raw frames)")
fig.tight_layout()
fig.savefig(out_path, dpi=150)
print(f"[Plot] Saved to: {out_path}")

try:
plt.show()
except Exception:
pass
Comment on lines +167 to +170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Broad except Exception: pass silently swallows all errors.

Static analysis flags this as blind exception handling with no logging (S110/BLE001), and suggests contextlib.suppress for the pattern (SIM105). At minimum, narrow the exception or log it so real failures (e.g., a broken matplotlib backend vs. an unrelated bug) aren't indistinguishable.

♻️ Proposed refactor
+import contextlib
...
-    try:
-        plt.show()
-    except Exception:
-        pass
+    with contextlib.suppress(Exception):
+        plt.show()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
plt.show()
except Exception:
pass
import contextlib
with contextlib.suppress(Exception):
plt.show()
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 167-170: Use contextlib.suppress(Exception) instead of try-except-pass

Replace try-except-pass with with contextlib.suppress(Exception): ...

(SIM105)


[error] 169-170: try-except-pass detected, consider logging the exception

(S110)


[warning] 169-169: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/python/ToF/tof_raw_rvc4.py` around lines 167 - 170, Update the
exception handling around plt.show() to avoid a bare Exception that silently
suppresses failures: catch only the expected matplotlib display exception, or
log unexpected errors before continuing. Preserve the intended behavior of
ignoring known display-backend failures while making unrelated failures
distinguishable.

Source: Linters/SAST tools



def main():
args = parse_args()

if args.ip:
os.environ["DEPTHAI_DEVICE_NAME_LIST"] = args.ip
if args.fwp:
os.environ["DEPTHAI_DEVICE_RVC4_FWP"] = args.fwp

socket = getattr(dai.CameraBoardSocket, args.socket)
preset = dai.ToFConfig.Profile.HIGH_RANGE

print(f"[Config] socket={args.socket}, frames={args.frames}")
print(f"[Config] DepthAI {dai.__version__}")

out_dir = tempfile.mkdtemp(prefix="tof_depth_test_")
print(f"[Temp] Saving to: {out_dir}")

pipeline, raw_q, depth_q = build_capture_pipeline(socket, preset)

saved = 0
frame_count = 0
warmup_done = False

with pipeline as p:
p.start()
print(f"[Pipeline] Running (warmup={WARMUP_FRAMES} frames)...")

t_start = time.monotonic()

while p.isRunning() and saved < args.frames:
depth_msg = depth_q.get()
raw_msg = raw_q.get()
frame_count += 1

if not warmup_done:
if frame_count >= WARMUP_FRAMES:
warmup_done = True
print(f"[Warmup] Done after {frame_count} frames. Starting capture...")
continue

depth_data = depth_msg.getFrame()
ts = int(depth_msg.getTimestamp().total_seconds() * 1000)
path = os.path.join(out_dir, f"depth_{ts}.npy")
np.save(path, depth_data)

raw_data = raw_msg.getFrame()
raw_path = os.path.join(out_dir, f"raw_{ts}.npz")
np.savez(raw_path, data=raw_data, type=np.array(raw_msg.getType().name))

saved += 1
print(f"[Save {saved}/{args.frames}] depth={depth_data.shape} raw={raw_data.shape} ts={ts}ms")

elapsed = time.monotonic() - t_start
print(f"[Done] {frame_count} total frames in {elapsed:.1f}s")

passed = verify_saved_files(out_dir, args.frames)

print("\n" + ("=" * 40))
if passed:
print("RESULT: PASS — depth files saved and contain valid data")
else:
print("RESULT: FAIL — see errors above")
sys.exit(1)

depth_frames = replay_raw_frames(socket, preset, out_dir)
if depth_frames:
plot_out = args.plot_out or os.path.join(out_dir, "replay_depth.png")
plot_depth_frames(depth_frames, plot_out)


if __name__ == "__main__":
main()
14 changes: 7 additions & 7 deletions include/depthai/pipeline/node/ToF.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ class ToFBase : public DeviceNodeCRTP<DeviceNode, ToFBase, ToFProperties> {

protected:
Properties& getProperties();
/**
* Input for raw sensor frames used by the RVC4 host implementation.
* This stays internal to the node group API, but must remain on the base
* node so the auto-created ToF camera can be linked in the pipeline schema.
*/
Input rawInput{*this, {"rawInput", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::ImgFrame, false}}}, DEFAULT_WAIT_FOR_MESSAGE}};

public:
ToFBase() = default;
Expand All @@ -59,6 +53,13 @@ class ToFBase : public DeviceNodeCRTP<DeviceNode, ToFBase, ToFProperties> {
Output phase{*this, {"phase", DEFAULT_GROUP, {{{DatatypeEnum::ImgFrame, true}}}}};
Output raw{*this, {"raw", DEFAULT_GROUP, {{{DatatypeEnum::ImgFrame, true}}}}};

/**
* Input for raw sensor frames used by the RVC4 host implementation.
* When using ToFBase directly (instead of the ToF node group), link a
* Camera node's raw output here to feed the ToF processing pipeline.
*/
Input rawInput{*this, {"rawInput", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::ImgFrame, false}}}, DEFAULT_WAIT_FOR_MESSAGE}};

/**
* Build with a specific board socket
*/
Expand All @@ -74,7 +75,6 @@ class ToFBase : public DeviceNodeCRTP<DeviceNode, ToFBase, ToFProperties> {

private:
friend class ToF;

bool isBuilt = false;
uint32_t maxWidth = 0;
uint32_t maxHeight = 0;
Expand Down