painting-vision-robotics-kit / test_window_postprocess.py
constructelligence's picture
Upload test_window_postprocess.py with huggingface_hub
41833eb verified
Raw History Blame Contribute Delete
4.06 kB
#!/usr/bin/env python3
"""Regression tests for window_postprocess (numpy/scipy only).
Run with ``python3 test_window_postprocess.py``. These pin the behaviours that
matter for painting safety: merging mullion-split panes, fitting an oriented
rectangle at any angle, filtering specks and non-window shapes, and rasterising
a clean keep-out.
"""
import numpy as np
from PIL import Image, ImageDraw
import window_postprocess as wp
def rotated_rectangle(height, width, center, angle_deg, side_w, side_h):
canvas = Image.new("L", (width, height), 0)
draw = ImageDraw.Draw(canvas)
theta = np.radians(angle_deg)
axes = np.array([[np.cos(theta), np.sin(theta)], [-np.sin(theta), np.cos(theta)]])
half = np.array([side_w, side_h]) / 2.0
corners = []
for sx, sy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
point = np.array(center) + np.array([sx, sy]) * half @ axes
corners.append(tuple(point))
draw.polygon(corners, fill=255)
return np.asarray(canvas) > 0
def test_minimum_area_rectangle_recovers_orientation():
for angle in (0, 30, 45, 75):
mask = rotated_rectangle(400, 400, (200, 200), angle, 140, 60)
ys, xs = np.nonzero(mask)
corners, width, height, fitted = wp.minimum_area_rectangle(np.c_[xs, ys])
long_side, short_side = max(width, height), min(width, height)
assert abs(long_side - 140) < 8, (angle, width, height)
assert abs(short_side - 60) < 8, (angle, width, height)
# Angle is only defined modulo 90 degrees for an axis-aligned box.
assert min(abs((fitted - angle) % 90), abs((fitted - angle) % 90 - 90)) < 6, (angle, fitted)
def test_merge_fragments_joins_mullion_panes_only():
mask = np.zeros((120, 160), bool)
mask[30:90, 10:60] = True # left pane
mask[30:90, 66:120] = True # right pane, 6px gap
merged = wp.merge_fragments(mask, gap=8)
assert merged.max() == 1, "panes within the gap should merge"
far = mask.copy()
far[30:90, 140:159] = True # a third pane far away
assert wp.merge_fragments(far, gap=8).max() == 2
def test_window_instances_merges_and_filters():
mask = np.zeros((200, 200), bool)
mask[50:110, 40:80] = True
mask[50:110, 84:130] = True # mullion gap of 4px
mask[10:12, 10:12] = True # stray speck
instances, labels = wp.window_instances(mask, min_area=64, merge_gap=8, min_fill=0.5, max_aspect=8.0)
assert len(instances) == 1, instances
window = instances[0]
assert window["area_pixels"] > 5000
assert window["fill_ratio"] > 0.85
assert window["bbox_xyxy"][0] == 40 and window["bbox_xyxy"][2] == 129
def test_window_instances_rejects_elongated_shape():
mask = np.zeros((200, 400), bool)
mask[95:105, 10:390] = True # a long thin stripe, aspect ~38
instances, _ = wp.window_instances(mask, min_area=64, merge_gap=0, max_aspect=8.0)
assert instances == []
def test_regularized_mask_fills_mullion_gap():
mask = np.zeros((120, 200), bool)
mask[30:90, 20:80] = True
mask[30:90, 90:150] = True # 10px gap
instances, _ = wp.window_instances(mask, min_area=64, merge_gap=14, min_fill=0.5)
assert len(instances) == 1
regular = wp.regularized_mask(instances, mask.shape)
# The fitted rectangle should cover the gap the raw mask left empty.
assert regular[60, 85], "fitted window should fill the mullion gap"
assert int(regular.sum()) > int(mask.sum())
def test_window_metrics():
target = np.zeros((50, 50), bool)
target[10:40, 10:40] = True
assert wp.window_metrics(target, target)["iou"] == 1.0
disjoint = np.zeros_like(target)
disjoint[0:5, 0:5] = True
result = wp.window_metrics(disjoint, target)
assert result["precision"] == 0.0 and result["recall"] == 0.0 and result["iou"] == 0.0
def main():
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
for test in tests:
test()
print(f"ok {test.__name__}")
print(f"{len(tests)} tests passed")
if __name__ == "__main__":
main()