Files
Presa-Mano/tests/test_depth_align.py
T
Adriano Dal PastroandClaude Opus 4.7 5354c51d60 feat(perception): vectorised IR→RGB depth alignment
Forward-project IR depth pixels into the RGB pixel grid with a z-buffer.
Output uint16 mm at RGB resolution, ready to feed SAM21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:23:11 +02:00

89 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit tests for align_depth_to_rgb."""
from __future__ import annotations
import numpy as np
from presa_mano.perception.depth_align import align_depth_to_rgb
from presa_mano.perception.depth_to_rgb import CameraRegistration
def _identity_reg(W: int = 64, H: int = 48) -> CameraRegistration:
"""Same K for RGB and IR, no extrinsic offset."""
K = np.array([[60.0, 0.0, W / 2.0],
[0.0, 60.0, H / 2.0],
[0.0, 0.0, 1.0]], dtype=np.float64)
return CameraRegistration(
K_rgb=K.copy(), K_ir=K.copy(),
R_ir_to_rgb=np.eye(3, dtype=np.float64),
t_ir_to_rgb=np.zeros(3, dtype=np.float64),
rgb_width=W, rgb_height=H,
ir_width=W, ir_height=H,
)
def test_identity_alignment_preserves_depth():
"""K_ir == K_rgb and no extrinsic → every IR pixel maps to itself."""
reg = _identity_reg()
depth_ir = np.full((reg.ir_height, reg.ir_width), 1000, dtype=np.uint16)
depth_rgb = align_depth_to_rgb(depth_ir, reg)
assert depth_rgb.shape == (reg.rgb_height, reg.rgb_width)
assert depth_rgb.dtype == np.uint16
nonzero = depth_rgb[depth_rgb > 0]
# Coverage close to full (rounding can drop a few border pixels).
assert nonzero.size >= 0.95 * depth_rgb.size, f"coverage too low: {nonzero.size}/{depth_rgb.size}"
# All filled pixels equal the input depth (±1 mm rounding).
assert np.all(np.abs(nonzero.astype(int) - 1000) <= 1)
def test_zero_depth_pixels_stay_zero():
"""IR pixels with depth=0 must not produce any RGB output."""
reg = _identity_reg()
depth_ir = np.zeros((reg.ir_height, reg.ir_width), dtype=np.uint16)
depth_rgb = align_depth_to_rgb(depth_ir, reg)
assert depth_rgb.shape == (reg.rgb_height, reg.rgb_width)
assert int(depth_rgb.sum()) == 0
def test_sparse_pixels_preserved():
"""Two isolated IR depths produce exactly two filled RGB pixels (identity reg)."""
reg = _identity_reg(W=64, H=48)
depth_ir = np.zeros((reg.ir_height, reg.ir_width), dtype=np.uint16)
depth_ir[0, 0] = 1000
depth_ir[reg.ir_height - 1, reg.ir_width - 1] = 1000
depth_rgb = align_depth_to_rgb(depth_ir, reg)
assert (depth_rgb > 0).sum() == 2
def test_z_buffer_keeps_closer_point():
"""When two IR pixels collide on the same RGB pixel, the smaller depth wins.
Force a collision with K_ir = 2 × K_rgb (same resolution), so adjacent IR
pixels round to the same RGB pixel."""
W = H = 16
K_rgb = np.array([[60.0, 0.0, W / 2.0],
[0.0, 60.0, H / 2.0],
[0.0, 0.0, 1.0]], dtype=np.float64)
K_ir = K_rgb.copy()
K_ir[0, 0] *= 2.0 # double focal length → two IR pixels collapse to one RGB pixel
K_ir[1, 1] *= 2.0
reg = CameraRegistration(
K_rgb=K_rgb, K_ir=K_ir,
R_ir_to_rgb=np.eye(3, dtype=np.float64),
t_ir_to_rgb=np.zeros(3, dtype=np.float64),
rgb_width=W, rgb_height=H, ir_width=W, ir_height=H,
)
depth_ir = np.zeros((H, W), dtype=np.uint16)
# Two IR pixels (5, 5) and (6, 5) both project to the same RGB pixel near (4, 5)
# (when projected through K_ir with depth 1m and re-projected through K_rgb).
depth_ir[5, 5] = 1500
depth_ir[5, 6] = 800
depth_rgb = align_depth_to_rgb(depth_ir, reg)
nonzero = depth_rgb[depth_rgb > 0]
# At least one RGB pixel must receive the closer (800) value; nothing further
# may be ≥ 1500 if a collision actually occurred. If they didn't collide, we
# see exactly two values [800, 1500]; if they did, we see only [800].
assert nonzero.size in (1, 2)
assert int(nonzero.min()) == 800
if nonzero.size == 1:
assert int(nonzero[0]) == 800 # collision happened, closer point won