File size: 4,063 Bytes
41833eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/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()