visual-verifier 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. visual_verifier/__init__.py +80 -0
  2. visual_verifier/__main__.py +6 -0
  3. visual_verifier/api.py +122 -0
  4. visual_verifier/cli.py +883 -0
  5. visual_verifier/config/__init__.py +15 -0
  6. visual_verifier/config/defaults.py +45 -0
  7. visual_verifier/config/targets.py +101 -0
  8. visual_verifier/config/tracking.py +97 -0
  9. visual_verifier/demo.py +310 -0
  10. visual_verifier/detection/__init__.py +29 -0
  11. visual_verifier/detection/filtering.py +203 -0
  12. visual_verifier/detection/region_extraction.py +537 -0
  13. visual_verifier/detection/severity.py +274 -0
  14. visual_verifier/exceptions.py +108 -0
  15. visual_verifier/integrations/__init__.py +8 -0
  16. visual_verifier/media/__init__.py +29 -0
  17. visual_verifier/media/metadata.py +307 -0
  18. visual_verifier/media/normalization.py +243 -0
  19. visual_verifier/media/thumbnails.py +90 -0
  20. visual_verifier/media/video_reader.py +320 -0
  21. visual_verifier/media/video_writer.py +127 -0
  22. visual_verifier/metrics/__init__.py +51 -0
  23. visual_verifier/metrics/geometry.py +209 -0
  24. visual_verifier/metrics/pixel.py +345 -0
  25. visual_verifier/metrics/sharpness.py +249 -0
  26. visual_verifier/models.py +537 -0
  27. visual_verifier/pipeline/__init__.py +9 -0
  28. visual_verifier/pipeline/image_pipeline.py +502 -0
  29. visual_verifier/pipeline/video_pipeline.py +1232 -0
  30. visual_verifier/policies/__init__.py +8 -0
  31. visual_verifier/py.typed +0 -0
  32. visual_verifier/reporting/__init__.py +17 -0
  33. visual_verifier/reporting/annotations.py +302 -0
  34. visual_verifier/reporting/console.py +266 -0
  35. visual_verifier/reporting/csv_report.py +98 -0
  36. visual_verifier/reporting/html_report.py +895 -0
  37. visual_verifier/reporting/json_report.py +133 -0
  38. visual_verifier/reporting/track_report.py +209 -0
  39. visual_verifier/targets/__init__.py +38 -0
  40. visual_verifier/targets/coverage.py +219 -0
  41. visual_verifier/targets/interpolation.py +172 -0
  42. visual_verifier/targets/loading.py +410 -0
  43. visual_verifier/targets/models.py +188 -0
  44. visual_verifier/targets/validation.py +216 -0
  45. visual_verifier/tracking/__init__.py +37 -0
  46. visual_verifier/tracking/analysis.py +337 -0
  47. visual_verifier/tracking/association.py +145 -0
  48. visual_verifier/tracking/events.py +203 -0
  49. visual_verifier/tracking/models.py +387 -0
  50. visual_verifier/tracking/tracker.py +584 -0
  51. visual_verifier/type_aliases.py +37 -0
  52. visual_verifier-0.3.0.dist-info/METADATA +544 -0
  53. visual_verifier-0.3.0.dist-info/RECORD +57 -0
  54. visual_verifier-0.3.0.dist-info/WHEEL +5 -0
  55. visual_verifier-0.3.0.dist-info/entry_points.txt +2 -0
  56. visual_verifier-0.3.0.dist-info/licenses/LICENSE +201 -0
  57. visual_verifier-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ """Expose supported Visual Verifier configuration interfaces."""
2
+
3
+ from visual_verifier.config.defaults import DEFAULT_DETECTION_CONFIG
4
+ from visual_verifier.config.tracking import (
5
+ DEFAULT_TRACKING_CONFIG,
6
+ TrackingConfig,
7
+ )
8
+ from visual_verifier.models import DetectionConfig
9
+
10
+ __all__ = [
11
+ "DEFAULT_DETECTION_CONFIG",
12
+ "DEFAULT_TRACKING_CONFIG",
13
+ "DetectionConfig",
14
+ "TrackingConfig",
15
+ ]
@@ -0,0 +1,45 @@
1
+ """Define immutable default values for visual region verification."""
2
+
3
+ from visual_verifier.models import DetectionConfig
4
+
5
+ DEFAULT_DIFF_THRESHOLD_INT = 30
6
+ DEFAULT_MIN_BOX_AREA_INT = 80
7
+ DEFAULT_MIN_BOX_WIDTH_INT = 6
8
+ DEFAULT_MIN_BOX_HEIGHT_INT = 6
9
+ DEFAULT_MERGE_KERNEL_SIZE_INT = 9
10
+ DEFAULT_BOX_PADDING_INT = 3
11
+ DEFAULT_MIN_CHANGED_RATIO_FLOAT = 0.03
12
+ DEFAULT_MIN_MEAN_DIFF_FLOAT = 5.0
13
+ DEFAULT_MIN_SEVERITY_SCORE_FLOAT = 8.0
14
+ DEFAULT_DIFF_NORMALIZER_FLOAT = 80.0
15
+ DEFAULT_CHANGED_RATIO_NORMALIZER_FLOAT = 0.75
16
+
17
+ DEFAULT_DETECTION_CONFIG = DetectionConfig(
18
+ diff_threshold=DEFAULT_DIFF_THRESHOLD_INT,
19
+ min_box_area=DEFAULT_MIN_BOX_AREA_INT,
20
+ min_box_width=DEFAULT_MIN_BOX_WIDTH_INT,
21
+ min_box_height=DEFAULT_MIN_BOX_HEIGHT_INT,
22
+ merge_kernel_size=DEFAULT_MERGE_KERNEL_SIZE_INT,
23
+ box_padding=DEFAULT_BOX_PADDING_INT,
24
+ min_changed_ratio=DEFAULT_MIN_CHANGED_RATIO_FLOAT,
25
+ min_mean_diff=DEFAULT_MIN_MEAN_DIFF_FLOAT,
26
+ min_severity_score=DEFAULT_MIN_SEVERITY_SCORE_FLOAT,
27
+ diff_normalizer=DEFAULT_DIFF_NORMALIZER_FLOAT,
28
+ changed_ratio_normalizer=(DEFAULT_CHANGED_RATIO_NORMALIZER_FLOAT),
29
+ )
30
+ """Validated immutable defaults used by public verification calls."""
31
+
32
+ __all__ = [
33
+ "DEFAULT_BOX_PADDING_INT",
34
+ "DEFAULT_CHANGED_RATIO_NORMALIZER_FLOAT",
35
+ "DEFAULT_DETECTION_CONFIG",
36
+ "DEFAULT_DIFF_NORMALIZER_FLOAT",
37
+ "DEFAULT_DIFF_THRESHOLD_INT",
38
+ "DEFAULT_MERGE_KERNEL_SIZE_INT",
39
+ "DEFAULT_MIN_BOX_AREA_INT",
40
+ "DEFAULT_MIN_BOX_HEIGHT_INT",
41
+ "DEFAULT_MIN_BOX_WIDTH_INT",
42
+ "DEFAULT_MIN_CHANGED_RATIO_FLOAT",
43
+ "DEFAULT_MIN_MEAN_DIFF_FLOAT",
44
+ "DEFAULT_MIN_SEVERITY_SCORE_FLOAT",
45
+ ]
@@ -0,0 +1,101 @@
1
+ """Configure target-aware verification.
2
+
3
+ The defaults are deliberately strict. A target is the reviewer's
4
+ statement that a specific region had to be anonymized, so accepting
5
+ partial coverage by default would quietly reintroduce the failure this
6
+ feature exists to catch.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Final
13
+
14
+ from visual_verifier.exceptions import ConfigurationError
15
+
16
+ MINIMUM_RATIO_FLOAT: Final[float] = 0.0
17
+ MAXIMUM_RATIO_FLOAT: Final[float] = 1.0
18
+ MAXIMUM_SENSIBLE_GAP_INT: Final[int] = 300
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class TargetConfig:
23
+ """Configure how targets are interpolated and scored.
24
+
25
+ Attributes:
26
+ min_covered_ratio: Fraction of a target's area that accepted
27
+ processing must cover before the target counts as
28
+ anonymized.
29
+ interpolate_missing_frames: Fill frames between two reviewed
30
+ boxes of the same target.
31
+ max_interpolation_gap: Longest run of missing frames that may be
32
+ interpolated. A longer gap is left empty, because a target
33
+ can leave and re-enter a scene and inventing boxes across
34
+ that would fabricate evidence.
35
+ fail_on_uncovered_target: Let an uncovered required target fail
36
+ verification. Turn it off to collect target evidence without
37
+ changing the verdict.
38
+ """
39
+
40
+ min_covered_ratio: float = 0.9
41
+ interpolate_missing_frames: bool = True
42
+ max_interpolation_gap: int = 5
43
+ fail_on_uncovered_target: bool = True
44
+
45
+ def __post_init__(self) -> None:
46
+ """Validate the configuration immediately after construction.
47
+
48
+ Raises:
49
+ ConfigurationError: When a value cannot be applied safely.
50
+ """
51
+
52
+ self._validate_covered_ratio()
53
+ self._validate_interpolation_gap()
54
+
55
+ def _validate_covered_ratio(self) -> None:
56
+ """Validate the minimum coverage ratio.
57
+
58
+ Raises:
59
+ ConfigurationError: When the ratio is outside zero to one.
60
+ """
61
+
62
+ if not (
63
+ MINIMUM_RATIO_FLOAT < self.min_covered_ratio <= MAXIMUM_RATIO_FLOAT
64
+ ):
65
+ raise ConfigurationError(
66
+ "Target coverage ratio must be above 0 and at most 1.",
67
+ context_mapping={
68
+ "min_covered_ratio": self.min_covered_ratio,
69
+ },
70
+ )
71
+
72
+ def _validate_interpolation_gap(self) -> None:
73
+ """Validate the interpolation gap limit.
74
+
75
+ Raises:
76
+ ConfigurationError: When the gap is negative or implausible.
77
+ """
78
+
79
+ if self.max_interpolation_gap < 0:
80
+ raise ConfigurationError(
81
+ "Target interpolation gap cannot be negative.",
82
+ context_mapping={
83
+ "max_interpolation_gap": self.max_interpolation_gap,
84
+ },
85
+ )
86
+ if self.max_interpolation_gap > MAXIMUM_SENSIBLE_GAP_INT:
87
+ raise ConfigurationError(
88
+ "Target interpolation gap is implausibly long.",
89
+ context_mapping={
90
+ "max_interpolation_gap": self.max_interpolation_gap,
91
+ "maximum_supported": MAXIMUM_SENSIBLE_GAP_INT,
92
+ },
93
+ )
94
+
95
+
96
+ DEFAULT_TARGET_CONFIG: Final[TargetConfig] = TargetConfig()
97
+
98
+ __all__ = [
99
+ "DEFAULT_TARGET_CONFIG",
100
+ "TargetConfig",
101
+ ]
@@ -0,0 +1,97 @@
1
+ """Define immutable configuration for temporal region tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from visual_verifier.exceptions import ConfigurationError
8
+
9
+ DEFAULT_ASSOCIATION_IOU_THRESHOLD_FLOAT = 0.20
10
+ DEFAULT_MINIMUM_CONFIRMATION_HITS_INT = 2
11
+ DEFAULT_MAXIMUM_GAP_FRAMES_INT = 3
12
+ DEFAULT_LINEAGE_OVERLAP_THRESHOLD_FLOAT = 0.20
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class TrackingConfig:
17
+ """Configure deterministic temporal association and track lifecycle.
18
+
19
+ Attributes:
20
+ association_iou_threshold: Minimum intersection-over-union required
21
+ to associate one detection with an active track.
22
+ minimum_confirmation_hits: Consecutive or recovered observations
23
+ required before a tentative track becomes confirmed.
24
+ maximum_gap_frames: Number of consecutive missing frames tolerated
25
+ before an active track is closed.
26
+ lineage_overlap_threshold: Minimum overlap used when detecting
27
+ split and merge lineage events.
28
+ detect_lineage_events: Whether split and merge evidence is produced.
29
+ """
30
+
31
+ association_iou_threshold: float = DEFAULT_ASSOCIATION_IOU_THRESHOLD_FLOAT
32
+ minimum_confirmation_hits: int = DEFAULT_MINIMUM_CONFIRMATION_HITS_INT
33
+ maximum_gap_frames: int = DEFAULT_MAXIMUM_GAP_FRAMES_INT
34
+ lineage_overlap_threshold: float = DEFAULT_LINEAGE_OVERLAP_THRESHOLD_FLOAT
35
+ detect_lineage_events: bool = True
36
+
37
+ def __post_init__(self) -> None:
38
+ """Validate temporal tracking settings after construction.
39
+
40
+ Raises:
41
+ ConfigurationError: When one or more settings are invalid.
42
+ """
43
+
44
+ invalid_fields_list = self._find_invalid_fields()
45
+ if not invalid_fields_list:
46
+ return
47
+
48
+ raise ConfigurationError(
49
+ "Tracking configuration contains invalid values.",
50
+ context_mapping={
51
+ "invalid_fields": sorted(set(invalid_fields_list)),
52
+ },
53
+ )
54
+
55
+ def _find_invalid_fields(self) -> list[str]:
56
+ """Return configuration field names that fail validation.
57
+
58
+ Returns:
59
+ Names of invalid threshold or lifecycle settings.
60
+ """
61
+
62
+ invalid_fields_list: list[str] = []
63
+
64
+ if not 0.0 <= self.association_iou_threshold <= 1.0:
65
+ invalid_fields_list.append("association_iou_threshold")
66
+ if self.minimum_confirmation_hits < 1:
67
+ invalid_fields_list.append("minimum_confirmation_hits")
68
+ if self.maximum_gap_frames < 0:
69
+ invalid_fields_list.append("maximum_gap_frames")
70
+ if not 0.0 <= self.lineage_overlap_threshold <= 1.0:
71
+ invalid_fields_list.append("lineage_overlap_threshold")
72
+
73
+ return invalid_fields_list
74
+
75
+ def to_dict(self) -> dict[str, object]:
76
+ """Return serializable temporal tracking settings.
77
+
78
+ Returns:
79
+ Dictionary containing each configuration field.
80
+ """
81
+
82
+ return {
83
+ field_name_str: getattr(self, field_name_str)
84
+ for field_name_str in self.__dataclass_fields__
85
+ }
86
+
87
+
88
+ DEFAULT_TRACKING_CONFIG = TrackingConfig()
89
+
90
+ __all__ = [
91
+ "DEFAULT_ASSOCIATION_IOU_THRESHOLD_FLOAT",
92
+ "DEFAULT_LINEAGE_OVERLAP_THRESHOLD_FLOAT",
93
+ "DEFAULT_MAXIMUM_GAP_FRAMES_INT",
94
+ "DEFAULT_MINIMUM_CONFIRMATION_HITS_INT",
95
+ "DEFAULT_TRACKING_CONFIG",
96
+ "TrackingConfig",
97
+ ]
@@ -0,0 +1,310 @@
1
+ """Generate and verify a self-contained demonstration.
2
+
3
+ A new user should be able to see a real anonymization failure without
4
+ cloning the repository, downloading fixtures, or supplying media of their
5
+ own. This module synthesizes a short scene in which a licence plate is
6
+ blurred on every frame except three, then verifies it.
7
+
8
+ The sample is generated rather than shipped: it keeps the wheel small,
9
+ needs no network access, and is reproducible on every platform. The clip
10
+ is deliberately synthetic, and the command says so.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ import cv2
19
+ import numpy as np
20
+
21
+ from visual_verifier.api import verify_video
22
+ from visual_verifier.media.video_writer import open_annotated_writer
23
+ from visual_verifier.models import BoundingBox, VerificationResult
24
+ from visual_verifier.type_aliases import ImageArray, PathInput
25
+
26
+ DEMONSTRATION_DIRECTORY_NAME_STR = "visual-verifier-demo"
27
+ REFERENCE_FILENAME_STR = "reference.mp4"
28
+ CANDIDATE_FILENAME_STR = "candidate.mp4"
29
+ REPORT_DIRECTORY_NAME_STR = "report"
30
+
31
+ DEMONSTRATION_FRAME_COUNT_INT = 15
32
+ DEMONSTRATION_FRAME_RATE_FLOAT = 2.0
33
+ DEMONSTRATION_WIDTH_INT = 640
34
+ DEMONSTRATION_HEIGHT_INT = 360
35
+ UNPROCESSED_FRAMES_TUPLE = (4, 8, 12)
36
+
37
+ PLATE_WIDTH_INT = 96
38
+ PLATE_HEIGHT_INT = 40
39
+ PLATE_START_X_INT = 210
40
+ PLATE_TOP_Y_INT = 232
41
+ PLATE_STEP_X_INT = 14
42
+ PLATE_BLUR_PADDING_INT = 6
43
+ PLATE_BLUR_KERNEL_TUPLE = (31, 31)
44
+ PLATE_CHARACTER_COUNT_INT = 6
45
+
46
+ ROAD_TOP_Y_INT = 250
47
+ LANE_MARK_TOP_Y_INT = 320
48
+ LANE_MARK_HEIGHT_INT = 10
49
+ LANE_MARK_WIDTH_INT = 46
50
+ LANE_MARK_SPACING_INT = 90
51
+ LANE_MARK_SPEED_INT = 18
52
+
53
+ ROAD_COLOR_BGR = (70, 70, 74)
54
+ LANE_COLOR_BGR = (225, 225, 225)
55
+ VEHICLE_COLOR_BGR = (48, 44, 52)
56
+ WINDOW_COLOR_BGR = (30, 28, 34)
57
+ PLATE_COLOR_BGR = (242, 242, 238)
58
+ PLATE_CHARACTER_COLOR_BGR = (24, 24, 28)
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class DemonstrationResult:
63
+ """Report where the demonstration wrote its media and evidence.
64
+
65
+ Attributes:
66
+ reference_path: Generated original clip.
67
+ candidate_path: Generated processed clip with three gaps.
68
+ report_directory: Directory holding the generated evidence.
69
+ verification_result: Result produced by the normal video API.
70
+ expected_unprocessed_frames: Frames deliberately left unblurred.
71
+ """
72
+
73
+ reference_path: Path
74
+ candidate_path: Path
75
+ report_directory: Path
76
+ verification_result: VerificationResult
77
+ expected_unprocessed_frames: tuple[int, ...]
78
+
79
+ @property
80
+ def behaved_as_documented(self) -> bool:
81
+ """Return whether the demonstration found exactly the gaps sown."""
82
+
83
+ return (
84
+ self.verification_result.failed
85
+ and self.verification_result.failed_frames
86
+ == self.expected_unprocessed_frames
87
+ )
88
+
89
+
90
+ def run_demonstration(
91
+ output_dir: PathInput | None = None,
92
+ ) -> DemonstrationResult:
93
+ """Generate a sample anonymization failure and verify it.
94
+
95
+ Args:
96
+ output_dir: Directory for the generated media and evidence.
97
+ Defaults to ``./visual-verifier-demo``.
98
+
99
+ Returns:
100
+ Paths to every generated artefact and the verification result.
101
+
102
+ Raises:
103
+ ReportWriteError: When the sample media cannot be written, which
104
+ usually means this OpenCV build has no usable video codec.
105
+ """
106
+
107
+ demonstration_path_obj = Path(
108
+ output_dir or DEMONSTRATION_DIRECTORY_NAME_STR
109
+ ).expanduser()
110
+ demonstration_path_obj.mkdir(parents=True, exist_ok=True)
111
+ reference_path_obj = demonstration_path_obj / REFERENCE_FILENAME_STR
112
+ candidate_path_obj = demonstration_path_obj / CANDIDATE_FILENAME_STR
113
+ report_path_obj = demonstration_path_obj / REPORT_DIRECTORY_NAME_STR
114
+
115
+ _write_demonstration_video(reference_path_obj, apply_blur_bool=False)
116
+ _write_demonstration_video(candidate_path_obj, apply_blur_bool=True)
117
+
118
+ return DemonstrationResult(
119
+ reference_path=reference_path_obj,
120
+ candidate_path=candidate_path_obj,
121
+ report_directory=report_path_obj,
122
+ verification_result=verify_video(
123
+ reference_path_obj,
124
+ candidate_path_obj,
125
+ output_dir=report_path_obj,
126
+ ),
127
+ expected_unprocessed_frames=UNPROCESSED_FRAMES_TUPLE,
128
+ )
129
+
130
+
131
+ def _write_demonstration_video(
132
+ output_path_obj: Path,
133
+ *,
134
+ apply_blur_bool: bool,
135
+ ) -> None:
136
+ """Write one generated clip, optionally blurring the licence plate.
137
+
138
+ Args:
139
+ output_path_obj: Destination MP4 path.
140
+ apply_blur_bool: Blur the plate on every frame except the three
141
+ deliberately unprocessed ones.
142
+
143
+ Raises:
144
+ ReportWriteError: When no supported codec can be opened.
145
+ """
146
+
147
+ video_writer_obj = open_annotated_writer(
148
+ output_path_obj,
149
+ DEMONSTRATION_FRAME_RATE_FLOAT,
150
+ (DEMONSTRATION_WIDTH_INT, DEMONSTRATION_HEIGHT_INT),
151
+ )
152
+ try:
153
+ for frame_number_int in range(1, DEMONSTRATION_FRAME_COUNT_INT + 1):
154
+ frame_ndarray = _render_scene_frame(frame_number_int)
155
+ if apply_blur_bool and (
156
+ frame_number_int not in UNPROCESSED_FRAMES_TUPLE
157
+ ):
158
+ _blur_plate_region(frame_ndarray, frame_number_int)
159
+ video_writer_obj.write(frame_ndarray)
160
+ finally:
161
+ video_writer_obj.release()
162
+
163
+
164
+ def _render_scene_frame(frame_number_int: int) -> ImageArray:
165
+ """Render one deterministic frame of the sample scene.
166
+
167
+ Args:
168
+ frame_number_int: One-based frame number.
169
+
170
+ Returns:
171
+ Rendered frame containing a road, a vehicle, and a plate.
172
+ """
173
+
174
+ frame_ndarray: ImageArray = np.zeros(
175
+ (DEMONSTRATION_HEIGHT_INT, DEMONSTRATION_WIDTH_INT, 3),
176
+ dtype=np.uint8,
177
+ )
178
+ _draw_sky_and_road(frame_ndarray)
179
+ _draw_lane_markings(frame_ndarray, frame_number_int)
180
+ _draw_vehicle(frame_ndarray, _plate_box(frame_number_int))
181
+ return frame_ndarray
182
+
183
+
184
+ def _draw_sky_and_road(frame_ndarray: ImageArray) -> None:
185
+ """Fill the frame with a graded sky above a flat road surface."""
186
+
187
+ for row_index_int in range(DEMONSTRATION_HEIGHT_INT):
188
+ frame_ndarray[row_index_int, :] = (
189
+ 170 - row_index_int // 4,
190
+ 140 - row_index_int // 6,
191
+ 110 - row_index_int // 8,
192
+ )
193
+ cv2.rectangle(
194
+ frame_ndarray,
195
+ (0, ROAD_TOP_Y_INT),
196
+ (DEMONSTRATION_WIDTH_INT, DEMONSTRATION_HEIGHT_INT),
197
+ ROAD_COLOR_BGR,
198
+ thickness=-1,
199
+ )
200
+
201
+
202
+ def _draw_lane_markings(
203
+ frame_ndarray: ImageArray,
204
+ frame_number_int: int,
205
+ ) -> None:
206
+ """Draw lane markings that advance with the frame number."""
207
+
208
+ total_span_int = DEMONSTRATION_WIDTH_INT + LANE_MARK_SPACING_INT
209
+ for marking_index_int in range(-1, 9):
210
+ left_x_int = (
211
+ marking_index_int * LANE_MARK_SPACING_INT
212
+ + frame_number_int * LANE_MARK_SPEED_INT
213
+ ) % total_span_int - LANE_MARK_SPACING_INT // 2
214
+ cv2.rectangle(
215
+ frame_ndarray,
216
+ (left_x_int, LANE_MARK_TOP_Y_INT),
217
+ (
218
+ left_x_int + LANE_MARK_WIDTH_INT,
219
+ LANE_MARK_TOP_Y_INT + LANE_MARK_HEIGHT_INT,
220
+ ),
221
+ LANE_COLOR_BGR,
222
+ thickness=-1,
223
+ )
224
+
225
+
226
+ def _draw_vehicle(
227
+ frame_ndarray: ImageArray,
228
+ plate_box_obj: BoundingBox,
229
+ ) -> None:
230
+ """Draw the vehicle body, window, and readable licence plate."""
231
+
232
+ cv2.rectangle(
233
+ frame_ndarray,
234
+ (plate_box_obj.x1 - 62, plate_box_obj.y1 - 96),
235
+ (plate_box_obj.x2 + 62, plate_box_obj.y2 + 26),
236
+ VEHICLE_COLOR_BGR,
237
+ thickness=-1,
238
+ )
239
+ cv2.rectangle(
240
+ frame_ndarray,
241
+ (plate_box_obj.x1 - 54, plate_box_obj.y1 - 84),
242
+ (plate_box_obj.x2 + 54, plate_box_obj.y1 - 30),
243
+ WINDOW_COLOR_BGR,
244
+ thickness=-1,
245
+ )
246
+ cv2.rectangle(
247
+ frame_ndarray,
248
+ (plate_box_obj.x1, plate_box_obj.y1),
249
+ (plate_box_obj.x2, plate_box_obj.y2),
250
+ PLATE_COLOR_BGR,
251
+ thickness=-1,
252
+ )
253
+ for character_index_int in range(PLATE_CHARACTER_COUNT_INT):
254
+ character_x_int = plate_box_obj.x1 + 8 + character_index_int * 14
255
+ cv2.rectangle(
256
+ frame_ndarray,
257
+ (character_x_int, plate_box_obj.y1 + 9),
258
+ (character_x_int + 9, plate_box_obj.y2 - 9),
259
+ PLATE_CHARACTER_COLOR_BGR,
260
+ thickness=-1,
261
+ )
262
+
263
+
264
+ def _plate_box(frame_number_int: int) -> BoundingBox:
265
+ """Return the licence-plate box for one frame of the sample scene.
266
+
267
+ Args:
268
+ frame_number_int: One-based frame number.
269
+
270
+ Returns:
271
+ Plate box that drifts steadily across the clip.
272
+ """
273
+
274
+ left_x_int = PLATE_START_X_INT + PLATE_STEP_X_INT * (frame_number_int - 1)
275
+ return BoundingBox(
276
+ left_x_int,
277
+ PLATE_TOP_Y_INT,
278
+ left_x_int + PLATE_WIDTH_INT,
279
+ PLATE_TOP_Y_INT + PLATE_HEIGHT_INT,
280
+ )
281
+
282
+
283
+ def _blur_plate_region(
284
+ frame_ndarray: ImageArray,
285
+ frame_number_int: int,
286
+ ) -> None:
287
+ """Blur the licence-plate region of one frame in place.
288
+
289
+ Args:
290
+ frame_ndarray: Frame modified in place.
291
+ frame_number_int: One-based frame number.
292
+ """
293
+
294
+ plate_box_obj = _plate_box(frame_number_int)
295
+ top_int = plate_box_obj.y1 - PLATE_BLUR_PADDING_INT
296
+ bottom_int = plate_box_obj.y2 + PLATE_BLUR_PADDING_INT
297
+ left_int = plate_box_obj.x1 - PLATE_BLUR_PADDING_INT
298
+ right_int = plate_box_obj.x2 + PLATE_BLUR_PADDING_INT
299
+ frame_ndarray[top_int:bottom_int, left_int:right_int] = cv2.GaussianBlur(
300
+ frame_ndarray[top_int:bottom_int, left_int:right_int],
301
+ PLATE_BLUR_KERNEL_TUPLE,
302
+ 0,
303
+ )
304
+
305
+
306
+ __all__ = [
307
+ "UNPROCESSED_FRAMES_TUPLE",
308
+ "DemonstrationResult",
309
+ "run_demonstration",
310
+ ]
@@ -0,0 +1,29 @@
1
+ """Expose stable region-detection interfaces for Visual Verifier."""
2
+
3
+ from visual_verifier.detection.filtering import (
4
+ RegionFilterDecision,
5
+ evaluate_region_filtering,
6
+ is_region_accepted,
7
+ rejection_reasons,
8
+ )
9
+ from visual_verifier.detection.region_extraction import (
10
+ RegionDetectionResult,
11
+ detect_regions,
12
+ )
13
+ from visual_verifier.detection.severity import (
14
+ SeverityLabel,
15
+ calculate_severity,
16
+ severity_label,
17
+ )
18
+
19
+ __all__ = [
20
+ "RegionDetectionResult",
21
+ "RegionFilterDecision",
22
+ "SeverityLabel",
23
+ "calculate_severity",
24
+ "detect_regions",
25
+ "evaluate_region_filtering",
26
+ "is_region_accepted",
27
+ "rejection_reasons",
28
+ "severity_label",
29
+ ]