File size: 7,334 Bytes
9422383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""End-to-end public runtime and detector behavior on Linux AArch64.

These tests require `YOLO26N_E2E_RUNTIME` and `YOLO26N_E2E_MODELS`. They exercise the runtime,
NumPy, OpenCV, and repository Python code. No local component is replaced.
"""

from __future__ import annotations

import shutil
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory

import numpy as np
from arm_yolo26n import DetectorConfig, Yolo26Detector, Yolo26nError, Yolo26nTensorSession
from tests.support import (
    configured_model,
    configured_models,
    configured_runtime,
    read_json,
)


def _models_by_resolution() -> dict[int, Path]:
    return {
        read_json(model / "manifest.json")["model"]["input_shape"][2]: model
        for model in configured_models()
    }


class TensorSessionBehaviorTests(unittest.TestCase):
    def test_session_executes_each_model_and_owns_its_lifecycle(self) -> None:
        for resolution, model in _models_by_resolution().items():
            with self.subTest(resolution=resolution):
                session = Yolo26nTensorSession(runtime=configured_runtime(), model=model)
                try:
                    output = session.execute(np.zeros(session.input_shape, dtype=np.float32))
                    self.assertEqual(tuple(output.shape), session.output_shape)
                    self.assertTrue(np.isfinite(output).all())
                    self.assertEqual(len(session.class_names), session.output_shape[1] - 4)
                finally:
                    session.close()

                session.close()
                with self.assertRaises(Yolo26nError):
                    session.execute(np.zeros(session.input_shape, dtype=np.float32))

    def test_session_rejects_incompatible_public_inputs_and_model(self) -> None:
        with Yolo26nTensorSession(
            runtime=configured_runtime(), model=configured_model()
        ) as session:
            wrong_shape = (1, 3, session.input_shape[2] - 1, session.input_shape[3])
            cases = (
                np.zeros(wrong_shape, dtype=np.float32),
                np.zeros(session.input_shape, dtype=np.float16),
            )
            for tensor in cases:
                with self.subTest(shape=tuple(tensor.shape), dtype=tensor.dtype):
                    with self.assertRaises((TypeError, ValueError)):
                        session.execute(tensor)

        with TemporaryDirectory() as directory:
            damaged = Path(directory) / "model"
            shutil.copytree(configured_model(), damaged)
            (damaged / "model.bin").write_bytes(b"damaged")
            with self.assertRaisesRegex(
                Yolo26nError, "model artifact 'model.bin' does not match its manifest"
            ):
                Yolo26nTensorSession(runtime=configured_runtime(), model=damaged)


class DetectorBehaviorTests(unittest.TestCase):
    def test_equivalent_bgr_rgb_and_bgrx_frames_produce_equivalent_results(self) -> None:
        with Yolo26Detector(runtime=configured_runtime(), model=configured_model()) as detector:
            resolution = detector.input_shape[2]
            rng = np.random.default_rng(7)
            shapes = (
                (resolution * 3 // 4, resolution),  # Vertical padding.
                (resolution, resolution - 13),  # Horizontal padding and odd width.
                (resolution * 2 // 3 + 1, resolution - 13),  # Resize and odd dimensions.
            )
            for height, width in shapes:
                with self.subTest(shape=(height, width)):
                    bgr = rng.integers(0, 256, (height, width, 3), dtype=np.uint8)
                    rgb = np.ascontiguousarray(bgr[..., ::-1])
                    storage = np.zeros((height, width + 4, 4), dtype=np.uint8)
                    storage[:, :width, :3] = bgr
                    bgrx = storage[:, :width]

                    bgr_result = detector.detect(bgr, pixel_format="BGR")
                    rgb_result = detector.detect(rgb, pixel_format="RGB")
                    bgrx_result = detector.detect(bgrx, pixel_format="BGRX")

                    self.assertEqual(bgr_result.source_shape, bgr.shape[:2])
                    self.assertEqual(bgr_result.detections, rgb_result.detections)
                    self.assertEqual(bgr_result.detections, bgrx_result.detections)

    def test_detector_applies_thresholds_and_reports_source_coordinates(self) -> None:
        """Check the public filtering contract; COCO validation measures accuracy."""
        rng = np.random.default_rng(7)
        resolution = read_json(configured_model() / "manifest.json")["model"]["input_shape"][2]
        height = resolution * 3 // 4
        frame = rng.integers(0, 256, (height, resolution, 3), dtype=np.uint8)
        # This fixed frame has observed detections above 0.001 but none above
        # 0.01, so both threshold outcomes are observable without accepting
        # arbitrary zero-confidence boxes.
        with Yolo26Detector(
            runtime=configured_runtime(),
            model=configured_model(),
            config=DetectorConfig(confidence=0.001, max_detections=5),
        ) as detector:
            detections = detector.detect(frame, pixel_format="BGR").detections

        self.assertTrue(detections)
        self.assertLessEqual(len(detections), 5)
        for detection in detections:
            x1, y1, x2, y2 = detection.box
            self.assertTrue(0 <= x1 < x2 <= resolution)
            self.assertTrue(0 <= y1 < y2 <= height)
            self.assertGreaterEqual(detection.confidence, 0.001)
            self.assertEqual(detection.class_name, detector.class_names[detection.class_id])

        with Yolo26Detector(
            runtime=configured_runtime(),
            model=configured_model(),
            config=DetectorConfig(confidence=0.01),
        ) as detector:
            self.assertEqual(detector.detect(frame, pixel_format="BGR").detections, ())

    def test_detector_reports_public_metadata_callback_and_rejections(self) -> None:
        consumed: list[bool] = []
        with Yolo26Detector(
            runtime=configured_runtime(),
            model=configured_model(),
            config=DetectorConfig(confidence=0.25, iou=0.7, max_detections=20),
        ) as detector:
            resolution = detector.input_shape[2]
            frame = np.zeros((resolution, resolution, 3), dtype=np.uint8)
            baseline = detector.detect(frame.copy(), pixel_format="BGR")

            def reuse_frame() -> None:
                consumed.append(True)
                frame.fill(255)

            result = detector.detect(
                frame,
                pixel_format="BGR",
                on_frame_consumed=reuse_frame,
            )
            self.assertEqual(consumed, [True])
            self.assertEqual(result.detections, baseline.detections)
            self.assertTrue(np.all(frame == 255))
            self.assertEqual(result.source_shape, (resolution, resolution))
            self.assertEqual(len(detector.class_names), detector.output_shape[1] - 4)
            with self.assertRaises(TypeError):
                detector.detect(
                    np.zeros((resolution, resolution, 3), dtype=np.float32),
                    pixel_format="BGR",
                )


if __name__ == "__main__":
    unittest.main()