integrated finger process pipeline on remapped 2d image
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import numpy as np
|
||||
|
||||
from orisys import Sensor
|
||||
from orisys.get_depth import Contact3D
|
||||
from orisys.hhd import nHHD
|
||||
|
||||
|
||||
def _circle_mask(shape, radius_ratio=0.35):
|
||||
h, w = shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
cy = (h - 1) / 2.0
|
||||
cx = (w - 1) / 2.0
|
||||
radius = min(h, w) * radius_ratio
|
||||
return ((yy - cy) ** 2 + (xx - cx) ** 2) <= radius ** 2
|
||||
|
||||
|
||||
def _radial_flow(shape, scale=0.05):
|
||||
h, w = shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
cy = (h - 1) / 2.0
|
||||
cx = (w - 1) / 2.0
|
||||
flow = np.zeros((h, w, 2), dtype=np.float32)
|
||||
flow[:, :, 0] = (xx - cx) * scale
|
||||
flow[:, :, 1] = (yy - cy) * scale
|
||||
return flow
|
||||
|
||||
|
||||
def test_masked_hhd_zeroes_invalid_region():
|
||||
shape = (48, 56)
|
||||
mask = _circle_mask(shape)
|
||||
flow = _radial_flow(shape, scale=0.03)
|
||||
flow[~mask] = 0
|
||||
|
||||
solver = nHHD(cuda=False, grid=shape, spacings=(1.0, 1.0), poisson_backend="cpu")
|
||||
_, forces = solver.get_force(flow, valid_mask=mask)
|
||||
|
||||
vnormal = solver.get_normal()
|
||||
vshear = solver.get_shear()
|
||||
|
||||
assert np.isfinite(forces).all()
|
||||
assert np.allclose(vnormal[~mask], 0.0, atol=1e-5)
|
||||
assert np.allclose(vshear[~mask], 0.0, atol=1e-5)
|
||||
|
||||
|
||||
def test_masked_hhd_ignores_invalid_background_spike():
|
||||
shape = (48, 56)
|
||||
mask = _circle_mask(shape)
|
||||
base = _radial_flow(shape, scale=0.02)
|
||||
base[~mask] = 0
|
||||
|
||||
contaminated = base.copy()
|
||||
contaminated[~mask, 0] = 25.0
|
||||
contaminated[~mask, 1] = -15.0
|
||||
|
||||
solver = nHHD(cuda=False, grid=shape, spacings=(1.0, 1.0), poisson_backend="cpu")
|
||||
_, forces_base = solver.get_force(base, valid_mask=mask)
|
||||
_, forces_contaminated = solver.get_force(contaminated, valid_mask=mask)
|
||||
|
||||
assert np.allclose(forces_base, forces_contaminated, atol=1e-3, rtol=1e-3)
|
||||
|
||||
|
||||
def test_sensor_detect_motion_ignores_invalid_region():
|
||||
sensor = Sensor(
|
||||
vid_src="external",
|
||||
cuda=False,
|
||||
isstitch=False,
|
||||
verbose=False,
|
||||
config_name="dd02-ov",
|
||||
mirror=False,
|
||||
backend="cpu",
|
||||
)
|
||||
try:
|
||||
frame = np.zeros((728, 672, 3), dtype=np.uint8)
|
||||
mask = np.zeros((728, 672), dtype=bool)
|
||||
mask[180:540, 160:512] = True
|
||||
sensor.get_img(frame=frame, already_processed=True, valid_mask=mask)
|
||||
|
||||
sensor.flow = np.zeros((728, 672, 2), dtype=np.float32)
|
||||
sensor.flow[~mask] = 10.0
|
||||
assert not sensor.detect_motion(threshold=1.0)
|
||||
|
||||
sensor.flow[:] = 0.0
|
||||
sensor.flow[mask] = 10.0
|
||||
assert sensor.detect_motion(threshold=1.0)
|
||||
finally:
|
||||
sensor.disconnect()
|
||||
|
||||
|
||||
def test_contact_centroid_respects_domain_mask():
|
||||
config_contact = {
|
||||
"min_contour_area": 25,
|
||||
"flow_threshold": 0.01,
|
||||
"div_threshold": 0.0,
|
||||
"expansion_threshold": 0.0,
|
||||
"kernel_size": [3, 3],
|
||||
"denoise_sigma": 0.0,
|
||||
}
|
||||
shape = (96, 96)
|
||||
mask = _circle_mask(shape, radius_ratio=0.28)
|
||||
flow = _radial_flow(shape, scale=0.05)
|
||||
flow[~mask] = 0.0
|
||||
flow[:, :8, 0] = 100.0
|
||||
flow[:, :8, 1] = 100.0
|
||||
|
||||
contact = Contact3D(config_contact=config_contact, verbose=False)
|
||||
result = contact.compute_foe(flow, valid_mask=mask)
|
||||
|
||||
assert result["centroid_found"]
|
||||
cx, cy = result["centroid"]
|
||||
assert mask[cy, cx]
|
||||
@@ -0,0 +1,162 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys.hhd import PoissonSolver, nHHD
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _circle_mask(shape, radius_ratio=0.35):
|
||||
h, w = shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
cy = (h - 1) / 2.0
|
||||
cx = (w - 1) / 2.0
|
||||
radius = min(h, w) * radius_ratio
|
||||
return ((yy - cy) ** 2 + (xx - cx) ** 2) <= radius ** 2
|
||||
|
||||
|
||||
def _smooth_rhs(shape):
|
||||
h, w = shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
field = (
|
||||
np.sin(np.pi * xx / max(1, w - 1))
|
||||
+ 0.5 * np.cos(2.0 * np.pi * yy / max(1, h - 1))
|
||||
)
|
||||
return field.astype(np.float32)
|
||||
|
||||
|
||||
def _radial_flow(shape, scale=0.05):
|
||||
h, w = shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
cy = (h - 1) / 2.0
|
||||
cx = (w - 1) / 2.0
|
||||
flow = np.zeros((h, w, 2), dtype=np.float32)
|
||||
flow[:, :, 0] = (xx - cx) * scale
|
||||
flow[:, :, 1] = (yy - cy) * scale
|
||||
return flow
|
||||
|
||||
|
||||
def _masked_rhs_vector(rhs, mask, pinned):
|
||||
vec = rhs[mask].astype(np.float64)
|
||||
vec -= vec.mean()
|
||||
vec[pinned] = 0.0
|
||||
return vec
|
||||
|
||||
|
||||
def _masked_residual_norm(solver, field, rhs, mask):
|
||||
cache = solver._masked_cache if solver._masked_cache_matches(mask) else solver._build_masked_cache(mask)
|
||||
rhs_vec = _masked_rhs_vector(rhs, mask, cache["pinned"])
|
||||
residual = cache["lap"].dot(field[mask].astype(np.float64)) - rhs_vec
|
||||
return float(np.linalg.norm(residual))
|
||||
|
||||
|
||||
def test_capfft_solver_improves_over_raw_fft_on_synthetic_mask():
|
||||
shape = (40, 48)
|
||||
mask = _circle_mask(shape)
|
||||
rhs = _smooth_rhs(shape)
|
||||
rhs_masked = np.where(mask, rhs, 0).astype(np.float32)
|
||||
|
||||
sparse_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="sparse",
|
||||
)
|
||||
capfft_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=48,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
sparse = sparse_solver.solve(rhs_masked, mask=mask)
|
||||
capfft = capfft_solver.solve(rhs_masked, mask=mask)
|
||||
raw_fft = sparse_solver.solve(rhs_masked, mask=None)
|
||||
|
||||
assert np.allclose(capfft[~mask], 0.0, atol=1e-6)
|
||||
|
||||
err_cap = float(np.linalg.norm((capfft - sparse)[mask]))
|
||||
err_raw = float(np.linalg.norm((raw_fft - sparse)[mask]))
|
||||
assert err_cap < err_raw
|
||||
|
||||
res_cap = _masked_residual_norm(sparse_solver, capfft, rhs_masked, mask)
|
||||
res_raw = _masked_residual_norm(sparse_solver, raw_fft, rhs_masked, mask)
|
||||
assert res_cap < res_raw
|
||||
|
||||
|
||||
def test_capfft_hhd_forces_track_sparse_reference():
|
||||
shape = (36, 44)
|
||||
mask = _circle_mask(shape)
|
||||
flow = _radial_flow(shape, scale=0.03)
|
||||
flow[~mask] = 0.0
|
||||
|
||||
sparse_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="sparse",
|
||||
)
|
||||
capfft_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
_, sparse_forces = sparse_hhd.get_force(flow, valid_mask=mask)
|
||||
_, capfft_forces = capfft_hhd.get_force(flow, valid_mask=mask)
|
||||
|
||||
denom = max(1.0, float(np.linalg.norm(sparse_forces)))
|
||||
rel = float(np.linalg.norm(capfft_forces - sparse_forces) / denom)
|
||||
assert rel < 0.6
|
||||
|
||||
vnormal = capfft_hhd.get_normal()
|
||||
vshear = capfft_hhd.get_shear()
|
||||
assert np.allclose(vnormal[~mask], 0.0, atol=1e-5)
|
||||
assert np.allclose(vshear[~mask], 0.0, atol=1e-5)
|
||||
|
||||
|
||||
def test_capfft_uv_mask_fixture_improves_over_raw_fft():
|
||||
uv_path = REPO_ROOT / "unroll_map_uv.npz"
|
||||
if not uv_path.exists():
|
||||
return
|
||||
|
||||
full_mask = np.load(uv_path)["valid_mask"].astype(bool)
|
||||
mask = full_mask[::24, ::24]
|
||||
if mask.sum() < 16:
|
||||
mask = full_mask[::16, ::16]
|
||||
|
||||
shape = mask.shape
|
||||
rhs = _smooth_rhs(shape)
|
||||
rhs_masked = np.where(mask, rhs, 0).astype(np.float32)
|
||||
|
||||
sparse_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="sparse",
|
||||
)
|
||||
capfft_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=24,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
sparse = sparse_solver.solve(rhs_masked, mask=mask)
|
||||
capfft = capfft_solver.solve(rhs_masked, mask=mask)
|
||||
raw_fft = sparse_solver.solve(rhs_masked, mask=None)
|
||||
|
||||
err_cap = float(np.linalg.norm((capfft - sparse)[mask]))
|
||||
err_raw = float(np.linalg.norm((raw_fft - sparse)[mask]))
|
||||
assert err_cap < err_raw
|
||||
@@ -0,0 +1,96 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys.unroll import UVDomain, UVRemap, UVSurfaceMap, flatten_frame, remap_valid_ratio
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
UV_MAP_PATH = REPO_ROOT / "unroll_map_uv.npz"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def uv_domain():
|
||||
if not UV_MAP_PATH.is_file():
|
||||
pytest.skip(f"UV remap cache not found: {UV_MAP_PATH}")
|
||||
return UVDomain.load(str(UV_MAP_PATH))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def uv_remap(uv_domain):
|
||||
return uv_domain.remap
|
||||
|
||||
|
||||
def test_uv_remap_load_and_flatten(uv_remap):
|
||||
h, w = uv_remap.out_height, uv_remap.out_width
|
||||
src_h = int(uv_remap.metadata.get("source_height", h))
|
||||
src_w = int(uv_remap.metadata.get("source_width", w))
|
||||
|
||||
source = np.random.randint(0, 255, (src_h, src_w, 3), dtype=np.uint8)
|
||||
flat = uv_remap.flatten(source, border_value=0)
|
||||
|
||||
assert flat.shape == (h, w, 3)
|
||||
assert flat.dtype == np.uint8
|
||||
invalid = ~uv_remap.valid_mask
|
||||
if np.any(invalid):
|
||||
assert np.all(flat[invalid] == 0)
|
||||
|
||||
|
||||
def test_flatten_frame_dict_compat(uv_remap):
|
||||
rd = uv_remap.to_dict()
|
||||
source = np.zeros((int(rd["source_height"]), int(rd["source_width"]), 3), dtype=np.uint8)
|
||||
flat = flatten_frame(source, rd, border_value=0)
|
||||
assert flat.shape == uv_remap.map_x.shape + (3,)
|
||||
|
||||
|
||||
def test_remap_valid_ratio(uv_remap):
|
||||
ratio = remap_valid_ratio(uv_remap)
|
||||
assert 0.0 < ratio <= 1.0
|
||||
|
||||
|
||||
def test_uv_domain_load(uv_domain):
|
||||
assert isinstance(uv_domain.remap, UVRemap)
|
||||
assert uv_domain.remap.valid_mask.shape == (
|
||||
uv_domain.remap.out_height,
|
||||
uv_domain.remap.out_width,
|
||||
)
|
||||
|
||||
|
||||
def test_uv_remap_fast_maps_match_float(uv_remap):
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
source = np.random.randint(0, 255, (src_h, src_w, 3), dtype=np.uint8)
|
||||
|
||||
uv_remap._map1_fixed = None
|
||||
uv_remap._map2_fixed = None
|
||||
float_flat = flatten_frame(source, {
|
||||
"map_x": uv_remap.map_x,
|
||||
"map_y": uv_remap.map_y,
|
||||
"valid_mask": uv_remap.valid_mask,
|
||||
}, border_value=0)
|
||||
uv_remap.ensure_fast_maps()
|
||||
fast_flat = flatten_frame(source, uv_remap, border_value=0)
|
||||
assert np.array_equal(fast_flat, float_flat)
|
||||
|
||||
|
||||
def test_uv_remap_reuses_provided_dst_buffer(uv_remap):
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
source = np.random.randint(0, 255, (src_h, src_w), dtype=np.uint8)
|
||||
dst = np.empty(uv_remap.valid_mask.shape, dtype=np.uint8)
|
||||
flat = flatten_frame(source, uv_remap, border_value=0, dst=dst)
|
||||
assert flat is dst
|
||||
assert flat.shape == uv_remap.valid_mask.shape
|
||||
|
||||
|
||||
def test_uv_surface_map_when_present(uv_domain):
|
||||
if uv_domain.surface is None:
|
||||
pytest.skip("Cache has no world_xyz yet; regenerate domain to test surface")
|
||||
surface = uv_domain.surface
|
||||
assert isinstance(surface, UVSurfaceMap)
|
||||
assert surface.world_xyz.shape[:2] == uv_domain.remap.valid_mask.shape
|
||||
assert surface.flat_xyz is surface.world_xyz
|
||||
points = surface.valid_points(uv_domain.valid_mask)
|
||||
assert points.ndim == 2 and points.shape[1] == 3
|
||||
assert np.all(np.isfinite(points))
|
||||
@@ -0,0 +1,93 @@
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys.frame_processors import UVUnrollFrameProcessor
|
||||
from orisys.unroll import UVDomain
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
UV_MAP_PATH = REPO_ROOT / "unroll_map_uv.npz"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def uv_domain():
|
||||
if not UV_MAP_PATH.is_file():
|
||||
pytest.skip(f"UV remap cache not found: {UV_MAP_PATH}")
|
||||
return UVDomain.load(str(UV_MAP_PATH))
|
||||
|
||||
|
||||
def test_uv_unroll_processor_output_shape(uv_domain):
|
||||
uv_remap = uv_domain.remap
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
|
||||
processor = UVUnrollFrameProcessor(
|
||||
uv_remap,
|
||||
source_width=src_w,
|
||||
source_height=src_h,
|
||||
flip_source_vertical=True,
|
||||
border_value=(0, 0, 0),
|
||||
)
|
||||
|
||||
frame = np.random.randint(0, 255, (src_h, src_w, 3), dtype=np.uint8)
|
||||
unrolled = processor.process(frame)
|
||||
|
||||
assert unrolled.shape == (uv_remap.out_height, uv_remap.out_width, 3)
|
||||
assert processor.valid_mask.shape == (uv_remap.out_height, uv_remap.out_width)
|
||||
assert unrolled.dtype == np.uint8
|
||||
|
||||
|
||||
def test_uv_unroll_processor_prepare_source(uv_domain):
|
||||
uv_remap = uv_domain.remap
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
|
||||
processor = UVUnrollFrameProcessor(
|
||||
uv_remap,
|
||||
source_width=src_w,
|
||||
source_height=src_h,
|
||||
flip_source_vertical=False,
|
||||
)
|
||||
frame = np.ones((src_h, src_w, 3), dtype=np.uint8) * 128
|
||||
prepared = processor.prepare_source(frame)
|
||||
assert prepared.shape == (src_h, src_w, 3)
|
||||
|
||||
|
||||
def test_uv_unroll_processor_flatten_prepared_matches_process(uv_domain):
|
||||
uv_remap = uv_domain.remap
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
|
||||
processor = UVUnrollFrameProcessor(
|
||||
uv_remap,
|
||||
source_width=src_w,
|
||||
source_height=src_h,
|
||||
flip_source_vertical=True,
|
||||
border_value=(0, 0, 0),
|
||||
)
|
||||
frame = np.random.randint(0, 255, (src_h, src_w, 3), dtype=np.uint8)
|
||||
prepared = processor.prepare_source(frame)
|
||||
assert np.array_equal(processor.flatten_prepared(prepared), processor.process(frame))
|
||||
|
||||
|
||||
def test_uv_unroll_processor_gray_fast_path_matches_bgr_result(uv_domain):
|
||||
uv_remap = uv_domain.remap
|
||||
src_h = int(uv_remap.metadata.get("source_height", uv_remap.out_height))
|
||||
src_w = int(uv_remap.metadata.get("source_width", uv_remap.out_width))
|
||||
|
||||
processor = UVUnrollFrameProcessor(
|
||||
uv_remap,
|
||||
source_width=src_w,
|
||||
source_height=src_h,
|
||||
flip_source_vertical=True,
|
||||
border_value=(0, 0, 0),
|
||||
)
|
||||
frame = np.random.randint(0, 255, (src_h, src_w, 3), dtype=np.uint8)
|
||||
prepared_gray = processor.prepare_source_gray(frame)
|
||||
gray_unrolled = processor.flatten_prepared_gray(prepared_gray)
|
||||
bgr_unrolled = processor.process(frame)
|
||||
assert gray_unrolled.shape == (uv_remap.out_height, uv_remap.out_width)
|
||||
assert np.allclose(gray_unrolled, cv2.cvtColor(bgr_unrolled, cv2.COLOR_BGR2GRAY), atol=1)
|
||||
Reference in New Issue
Block a user