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,203 @@
1
+ """Evaluate region measurements against false-positive filters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from math import isfinite
7
+
8
+ from visual_verifier.exceptions import PolicyEvaluationError
9
+ from visual_verifier.models import DetectionConfig
10
+
11
+ CHANGED_RATIO_TOO_LOW_REASON_STR = "changed_ratio_too_low"
12
+ MEAN_DIFFERENCE_TOO_LOW_REASON_STR = "mean_diff_too_low"
13
+ SEVERITY_SCORE_TOO_LOW_REASON_STR = "severity_score_too_low"
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class RegionFilterDecision:
18
+ """Store the acceptance decision for one measured region.
19
+
20
+ Attributes:
21
+ accepted: Whether the region passed every configured filter.
22
+ rejection_reasons: Stable reason codes for failed filters.
23
+ """
24
+
25
+ accepted: bool
26
+ rejection_reasons: tuple[str, ...]
27
+
28
+ def to_dict(self) -> dict[str, object]:
29
+ """Return a machine-readable representation of the decision.
30
+
31
+ Returns:
32
+ Dictionary containing the acceptance flag and reason codes.
33
+ """
34
+
35
+ return {
36
+ "accepted": self.accepted,
37
+ "rejection_reasons": list(self.rejection_reasons),
38
+ }
39
+
40
+
41
+ def evaluate_region_filtering(
42
+ *,
43
+ changed_ratio: float,
44
+ mean_diff: float,
45
+ severity_score: float,
46
+ config: DetectionConfig,
47
+ ) -> RegionFilterDecision:
48
+ """Evaluate one region against all configured filtering thresholds.
49
+
50
+ Args:
51
+ changed_ratio: Fraction of region pixels classified as changed.
52
+ mean_diff: Mean absolute grayscale difference for the region.
53
+ severity_score: Composite processing-severity score.
54
+ config: Detection thresholds controlling region acceptance.
55
+
56
+ Returns:
57
+ Immutable region-filter decision and rejection reason codes.
58
+
59
+ Raises:
60
+ PolicyEvaluationError: If a metric or threshold is invalid.
61
+ """
62
+
63
+ reasons_tuple = rejection_reasons(
64
+ changed_ratio=changed_ratio,
65
+ mean_diff=mean_diff,
66
+ severity_score=severity_score,
67
+ config=config,
68
+ )
69
+ return RegionFilterDecision(
70
+ accepted=not reasons_tuple,
71
+ rejection_reasons=reasons_tuple,
72
+ )
73
+
74
+
75
+ def rejection_reasons(
76
+ *,
77
+ changed_ratio: float,
78
+ mean_diff: float,
79
+ severity_score: float,
80
+ config: DetectionConfig,
81
+ ) -> tuple[str, ...]:
82
+ """Return stable reason codes for every failed region filter.
83
+
84
+ Args:
85
+ changed_ratio: Fraction of region pixels classified as changed.
86
+ mean_diff: Mean absolute grayscale difference for the region.
87
+ severity_score: Composite processing-severity score.
88
+ config: Detection thresholds controlling region acceptance.
89
+
90
+ Returns:
91
+ Rejection reason codes in deterministic evaluation order.
92
+
93
+ Raises:
94
+ PolicyEvaluationError: If a metric or threshold is invalid.
95
+ """
96
+
97
+ _validate_filter_inputs(
98
+ changed_ratio=changed_ratio,
99
+ mean_diff=mean_diff,
100
+ severity_score=severity_score,
101
+ config=config,
102
+ )
103
+ reasons_list: list[str] = []
104
+ if changed_ratio < config.min_changed_ratio:
105
+ reasons_list.append(CHANGED_RATIO_TOO_LOW_REASON_STR)
106
+ if mean_diff < config.min_mean_diff:
107
+ reasons_list.append(MEAN_DIFFERENCE_TOO_LOW_REASON_STR)
108
+ if severity_score < config.min_severity_score:
109
+ reasons_list.append(SEVERITY_SCORE_TOO_LOW_REASON_STR)
110
+ return tuple(reasons_list)
111
+
112
+
113
+ def is_region_accepted(
114
+ *,
115
+ changed_ratio: float,
116
+ mean_diff: float,
117
+ severity_score: float,
118
+ config: DetectionConfig,
119
+ ) -> bool:
120
+ """Return whether one region passes every configured filter.
121
+
122
+ Args:
123
+ changed_ratio: Fraction of region pixels classified as changed.
124
+ mean_diff: Mean absolute grayscale difference for the region.
125
+ severity_score: Composite processing-severity score.
126
+ config: Detection thresholds controlling region acceptance.
127
+
128
+ Returns:
129
+ ``True`` when no rejection reason is produced.
130
+
131
+ Raises:
132
+ PolicyEvaluationError: If a metric or threshold is invalid.
133
+ """
134
+
135
+ decision_obj = evaluate_region_filtering(
136
+ changed_ratio=changed_ratio,
137
+ mean_diff=mean_diff,
138
+ severity_score=severity_score,
139
+ config=config,
140
+ )
141
+ return decision_obj.accepted
142
+
143
+
144
+ def _validate_filter_inputs(
145
+ *,
146
+ changed_ratio: float,
147
+ mean_diff: float,
148
+ severity_score: float,
149
+ config: DetectionConfig,
150
+ ) -> None:
151
+ """Validate runtime measurements and configured thresholds."""
152
+
153
+ value_pairs_tuple = (
154
+ ("changed_ratio", changed_ratio, config.min_changed_ratio),
155
+ ("mean_diff", mean_diff, config.min_mean_diff),
156
+ ("severity_score", severity_score, config.min_severity_score),
157
+ )
158
+ for (
159
+ metric_name_str,
160
+ metric_value_float,
161
+ threshold_float,
162
+ ) in value_pairs_tuple:
163
+ _validate_non_negative_finite_value(
164
+ metric_name_str,
165
+ metric_value_float,
166
+ value_role_str="measurement",
167
+ )
168
+ _validate_non_negative_finite_value(
169
+ metric_name_str,
170
+ threshold_float,
171
+ value_role_str="threshold",
172
+ )
173
+
174
+
175
+ def _validate_non_negative_finite_value(
176
+ metric_name_str: str,
177
+ metric_value_float: float,
178
+ *,
179
+ value_role_str: str,
180
+ ) -> None:
181
+ """Require one measurement or threshold to be finite and non-negative."""
182
+
183
+ if isfinite(metric_value_float) and metric_value_float >= 0.0:
184
+ return
185
+ raise PolicyEvaluationError(
186
+ f"Region-filter {value_role_str} must be finite and non-negative.",
187
+ context_mapping={
188
+ "metric": metric_name_str,
189
+ "role": value_role_str,
190
+ "value": metric_value_float,
191
+ },
192
+ )
193
+
194
+
195
+ __all__ = [
196
+ "CHANGED_RATIO_TOO_LOW_REASON_STR",
197
+ "MEAN_DIFFERENCE_TOO_LOW_REASON_STR",
198
+ "SEVERITY_SCORE_TOO_LOW_REASON_STR",
199
+ "RegionFilterDecision",
200
+ "evaluate_region_filtering",
201
+ "is_region_accepted",
202
+ "rejection_reasons",
203
+ ]