File size: 20,666 Bytes
699f3cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, ClassVar

from src.core.vision import is_cv2_available
from src.task3.routing_policy import assign_detector

if is_cv2_available():  # pragma: no branch - ortama bagli
    from src.core.vision import cv2, np
else:  # pragma: no cover - cv2 yoksa
    cv2 = None
    np = None

LOGGER = logging.getLogger(__name__)


@dataclass(slots=True)
class ReferenceCache:
    """Referans nesne metadata ve descriptor cache iskeleti."""

    items: dict[str, dict[str, Any]] = field(default_factory=dict)
    auto_routing_summary: dict[str, dict[str, Any]] = field(default_factory=dict)
    overrides_applied: list[dict[str, Any]] = field(default_factory=list)
    per_reference_suppression: bool = False
    IMAGE_EXTENSIONS: ClassVar[tuple[str, ...]] = (".jpg", ".jpeg", ".png", ".bmp", ".webp", ".pgm")
    VALID_DETECTORS: ClassVar[set[str]] = {"yoloe", "orb", "both"}
    VALID_MODALITIES: ClassVar[set[str]] = {"rgb", "thermal", "unknown"}
    VALID_OVERRIDE_FIELDS: ClassVar[set[str]] = {"detector", "detector_modalities", "modality", "rationale"}

    def put(self, reference_id: str, payload: dict[str, Any]) -> None:
        self.items[reference_id] = payload

    def get(self, reference_id: str) -> dict[str, Any] | None:
        return self.items.get(reference_id)

    def list_ids(self) -> list[str]:
        return sorted(self.items.keys())

    def get_auto_routing_summary(self) -> dict[str, dict[str, Any]]:
        return {reference_id: dict(payload) for reference_id, payload in self.auto_routing_summary.items()}

    def get_routing_diagnostics(self) -> dict[str, dict[str, Any]]:
        diagnostics: dict[str, dict[str, Any]] = {}
        for reference_id in self.list_ids():
            item = self.get(reference_id) or {}
            metadata = dict(item.get("reference_metadata") or {})
            diagnostics[reference_id] = {
                "modality": str(item.get("reference_modality") or metadata.get("modality") or "unknown"),
                "detector": str(item.get("detector") or metadata.get("detector") or "yoloe"),
                "detector_modalities": list(item.get("detector_modalities") or metadata.get("detector_modalities") or []),
                "confidence": str(metadata.get("routing_confidence") or "low"),
                "rationale": str(metadata.get("routing_rationale") or ""),
                "signals": dict(metadata.get("routing_signals") or {}),
                "override": dict(metadata.get("override") or {}) if metadata.get("override") else None,
            }
        return diagnostics

    def get_overrides_applied(self) -> list[dict[str, Any]]:
        return [dict(item) for item in self.overrides_applied]

    def get_candidate_suppression_mode(self) -> str:
        return "per_reference_top_1" if self.per_reference_suppression else "global_top_1"

    def set_learned_embedding(self, reference_id: str, backbone_name: str, embedding: Any) -> None:
        if reference_id not in self.items:
            return
        self.items[reference_id].setdefault("learned_embeddings", {})
        self.items[reference_id]["learned_embeddings"][backbone_name] = embedding

    def get_learned_embedding(self, reference_id: str, backbone_name: str) -> Any | None:
        item = self.get(reference_id)
        if not item:
            return None
        return item.get("learned_embeddings", {}).get(backbone_name)

    def get_detector(self, reference_id: str, *, default: str = "yoloe") -> str:
        item = self.get(reference_id) or {}
        detector = str(item.get("detector") or default)
        return detector

    def get_detector_modalities(self, reference_id: str) -> tuple[str, ...] | None:
        item = self.get(reference_id) or {}
        raw_modalities = item.get("detector_modalities")
        if not raw_modalities:
            return None
        return tuple(str(modality) for modality in raw_modalities)

    def allows_detector_for_modality(self, reference_id: str, modality: str | None) -> bool:
        allowed_modalities = self.get_detector_modalities(reference_id)
        if not allowed_modalities or not modality or str(modality) == "unknown":
            return True
        return str(modality) in allowed_modalities

    def filter_reference_ids_by_detector_modality(

        self,

        reference_ids: list[str],

        *,

        modality: str | None,

    ) -> list[str]:
        return [reference_id for reference_id in reference_ids if self.allows_detector_for_modality(reference_id, modality)]

    def split_reference_ids_by_detector(

        self,

        reference_ids: list[str],

        *,

        default_detector: str = "yoloe",

    ) -> tuple[list[str], list[str]]:
        yoloe_ids: list[str] = []
        orb_ids: list[str] = []
        for reference_id in reference_ids:
            detector = self.get_detector(reference_id, default=default_detector)
            if detector == "both":
                yoloe_ids.append(reference_id)
                orb_ids.append(reference_id)
            elif detector == "orb":
                orb_ids.append(reference_id)
            else:
                yoloe_ids.append(reference_id)
        return yoloe_ids, orb_ids

    def preload_from_directory(self, path: str | Path, *, orb_features: int = 256) -> int:
        directory = Path(path)
        if not directory.exists() or not directory.is_dir():
            return 0

        self.items.clear()
        self.auto_routing_summary.clear()
        self.overrides_applied.clear()
        self.per_reference_suppression = False

        spec_payload = self._load_reference_spec(directory)
        reference_metadata = spec_payload["references"]
        overrides = spec_payload["overrides"]
        auto_routing_enabled = bool(spec_payload.get("auto_routing_enabled"))
        self.per_reference_suppression = bool(spec_payload.get("per_reference_suppression", False))
        file_to_reference_id = {
            str(metadata.get("file", "")).lower(): reference_id
            for reference_id, metadata in reference_metadata.items()
            if str(metadata.get("file", "")).strip()
        }
        loaded_count = 0
        orb = cv2.ORB_create(nfeatures=orb_features) if is_cv2_available() else None
        for candidate in sorted(directory.iterdir()):
            if not candidate.is_file():
                continue
            if candidate.suffix.lower() not in self.IMAGE_EXTENSIONS:
                continue
            data = candidate.read_bytes()
            reference_id = file_to_reference_id.get(candidate.name.lower(), candidate.stem)
            metadata = dict(reference_metadata.get(reference_id, {}))
            try:
                if auto_routing_enabled:
                    assignment = assign_detector(candidate)
                    effective_detector = assignment.detector
                    effective_modalities = list(assignment.detector_modalities)
                    effective_modality = str(assignment.signals.get("modality", metadata.get("modality") or "unknown"))
                    effective_confidence = str(assignment.confidence)
                    effective_rationale = str(assignment.rationale)
                    routing_signals = dict(assignment.signals)
                else:
                    effective_detector = "yoloe"
                    effective_modalities = None
                    effective_modality = str(metadata.get("modality") or "unknown")
                    effective_confidence = "low"
                    effective_rationale = "specless preload; auto-routing skipped and detector defaulted to yoloe"
                    routing_signals = {
                        "modality": effective_modality,
                        "modality_method": "none",
                        "modality_confidence": "low",
                        "modality_reason": "manifest/spec unavailable",
                        "modality_exif_signals": [],
                        "modality_pixel_signals": {},
                    }
            except Exception as exc:
                effective_detector = "yoloe"
                effective_modalities = None
                effective_modality = "unknown"
                effective_confidence = "low"
                effective_rationale = f"auto-routing unavailable; defaulted to yoloe ({exc})"
                routing_signals = {
                    "modality": "unknown",
                    "modality_method": "none",
                    "modality_confidence": "low",
                    "modality_reason": str(exc),
                    "modality_exif_signals": [],
                    "modality_pixel_signals": {},
                }
                LOGGER.warning("reference %s auto-routing failed: %s", reference_id, exc)
            manual_override = overrides.get(reference_id)
            override_summary: dict[str, Any] | None = None
            if manual_override is not None:
                auto_assignment_summary = {
                    "detector": effective_detector,
                    "detector_modalities": list(effective_modalities) if effective_modalities is not None else None,
                    "modality": effective_modality,
                }
                effective_detector = str(manual_override["detector"])
                if "detector_modalities" in manual_override:
                    effective_modalities = list(manual_override["detector_modalities"])
                if manual_override.get("modality") is not None:
                    effective_modality = str(manual_override["modality"])
                effective_confidence = "medium" if effective_confidence == "high" else effective_confidence
                override_rationale = str(manual_override.get("rationale") or "manual override")
                effective_rationale = f"{override_rationale}"
                override_summary = {
                    "reference_id": reference_id,
                    "auto_detected": auto_assignment_summary,
                    "override": {
                        "detector": effective_detector,
                        "detector_modalities": list(effective_modalities) if effective_modalities is not None else None,
                        "modality": effective_modality,
                        "rationale": override_rationale,
                    },
                }
                self.overrides_applied.append(override_summary)
                LOGGER.warning(
                    "reference %s has manual override; auto-detected was %s, overridden to %s (%s)",
                    reference_id,
                    auto_assignment_summary,
                    override_summary["override"],
                    override_rationale,
                )

            auto_summary_payload = {
                "modality": effective_modality,
                "detector": effective_detector,
                "detector_modalities": list(effective_modalities) if effective_modalities is not None else None,
                "confidence": effective_confidence,
                "rationale": effective_rationale,
            }
            self.auto_routing_summary[reference_id] = auto_summary_payload
            gray = None
            bgr = None
            descriptors = None
            keypoints = None
            width = 0
            height = 0
            descriptor_mode = "metadata_only"
            if orb is not None:
                bgr = cv2.imdecode(np.frombuffer(data, dtype=np.uint8), cv2.IMREAD_COLOR)
                if bgr is not None:
                    gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
                    height, width = gray.shape[:2]
                    keypoints, descriptors = orb.detectAndCompute(gray, None)
                    descriptor_mode = "orb"
            self.put(
                reference_id,
                {
                    "reference_id": reference_id,
                    "path": str(candidate),
                    "byte_size": len(data),
                    "sha1": hashlib.sha1(data).hexdigest(),
                    "loaded_from": "directory_preload",
                    "bgr": bgr,
                    "gray": gray,
                    "width": width,
                    "height": height,
                    "keypoints": keypoints,
                    "descriptors": descriptors,
                    "descriptor_mode": descriptor_mode,
                    "keypoint_count": len(keypoints or []),
                    "learned_embeddings": {},
                    "detector": effective_detector,
                    "detector_modalities": list(effective_modalities) if effective_modalities is not None else None,
                    "reference_modality": effective_modality,
                    "reference_dimensions": metadata.get("dimensions"),
                    "reference_metadata": {
                        **metadata,
                        "modality": effective_modality,
                        "detector": effective_detector,
                        "detector_modalities": list(effective_modalities) if effective_modalities is not None else None,
                        "routing_confidence": effective_confidence,
                        "routing_rationale": effective_rationale,
                        "routing_signals": routing_signals,
                        "override": override_summary["override"] if override_summary else None,
                    },
                },
            )
            loaded_count += 1
        return loaded_count

    def _load_reference_spec(self, directory: Path) -> dict[str, dict[str, Any]]:
        manifest_path = directory / "manifest.json"
        if not manifest_path.exists():
            return {"references": {}, "overrides": {}, "auto_routing_enabled": False, "per_reference_suppression": False}

        manifest_payload = json.loads(manifest_path.read_text(encoding="utf-8"))
        spec_path_raw = str(manifest_payload.get("spec_path") or "").strip()
        if not spec_path_raw:
            return {"references": {}, "overrides": {}, "auto_routing_enabled": False, "per_reference_suppression": False}

        spec_path = Path(spec_path_raw)
        if not spec_path.is_absolute():
            candidate = directory / spec_path_raw
            spec_path = candidate if candidate.exists() else Path(spec_path_raw)
        if not spec_path.exists():
            return {"references": {}, "overrides": {}, "auto_routing_enabled": False, "per_reference_suppression": False}

        spec_payload = json.loads(spec_path.read_text(encoding="utf-8"))
        references = spec_payload.get("references", {})
        if not isinstance(references, dict):
            raise ValueError(f"Task3 reference spec malformed: {spec_path}")
        per_reference_suppression = spec_payload.get("per_reference_suppression", False)
        if not isinstance(per_reference_suppression, bool):
            raise ValueError(f"Task3 reference spec per_reference_suppression must be boolean: {spec_path}")
        overrides_payload = spec_payload.get("overrides", {})
        if overrides_payload is None:
            overrides_payload = {}
        if not isinstance(overrides_payload, dict):
            raise ValueError(f"Task3 reference spec overrides malformed: {spec_path}")

        metadata_by_reference: dict[str, dict[str, Any]] = {}
        nested_overrides: dict[str, dict[str, Any]] = {}
        for reference_id, payload in references.items():
            if not isinstance(payload, dict):
                raise ValueError(f"Task3 reference spec entry malformed for {reference_id}: {spec_path}")
            reference_id_text = str(reference_id)
            routing_override = payload.get("routing_override")
            if routing_override is not None:
                nested_overrides[reference_id_text] = self._normalize_override_payload(
                    routing_override,
                    reference_id_text,
                    spec_path=spec_path,
                )
            metadata_by_reference[reference_id_text] = {
                "modality": payload.get("modality"),
                "dimensions": payload.get("dimensions"),
                "source_exif": payload.get("source_exif"),
                "file": payload.get("file"),
            }

        normalized_overrides: dict[str, dict[str, Any]] = {}
        for reference_id, payload in overrides_payload.items():
            reference_id_text = str(reference_id)
            normalized_overrides[reference_id_text] = self._normalize_override_payload(
                payload,
                reference_id_text,
                spec_path=spec_path,
            )
        for reference_id, payload in nested_overrides.items():
            if reference_id in normalized_overrides:
                raise ValueError(
                    f"Task3 reference spec override duplicated for {reference_id}: "
                    f"use either top-level overrides or references.{reference_id}.routing_override"
                )
            normalized_overrides[reference_id] = payload

        return {
            "references": metadata_by_reference,
            "overrides": normalized_overrides,
            "auto_routing_enabled": True,
            "per_reference_suppression": per_reference_suppression,
        }

    def _normalize_override_payload(
        self,
        payload: Any,
        reference_id: str,
        *,
        spec_path: Path,
    ) -> dict[str, Any]:
        if not isinstance(payload, dict):
            raise ValueError(f"Task3 reference spec override malformed for {reference_id}: {spec_path}")
        unexpected_keys = sorted(set(payload.keys()) - self.VALID_OVERRIDE_FIELDS)
        if unexpected_keys:
            raise ValueError(
                f"Task3 reference spec override contains unsupported fields for {reference_id}: {unexpected_keys}"
            )
        if "detector" not in payload:
            raise ValueError(f"Task3 reference spec override missing detector for {reference_id}: {spec_path}")
        detector = self._validate_detector(str(payload["detector"]), reference_id)
        modality = payload.get("modality")
        if modality is not None and str(modality) not in self.VALID_MODALITIES:
            raise ValueError(
                f"Task3 reference spec override modality must be within {sorted(self.VALID_MODALITIES)}, "
                f"got {modality!r} for {reference_id}"
            )
        detector_modalities = self._normalize_detector_modalities(payload.get("detector_modalities"), reference_id)
        if detector_modalities is None and modality is not None and str(modality) in {"rgb", "thermal"}:
            detector_modalities = [str(modality)]
        return {
            "detector": detector,
            "detector_modalities": detector_modalities,
            "modality": str(modality) if modality is not None else None,
            "rationale": payload.get("rationale"),
        }

    def _validate_detector(self, detector: str, reference_id: str) -> str:
        if detector not in self.VALID_DETECTORS:
            raise ValueError(
                f"Task3 reference spec detector must be one of {sorted(self.VALID_DETECTORS)}, "
                f"got {detector!r} for {reference_id}"
            )
        return detector

    def _normalize_detector_modalities(self, detector_modalities: Any, reference_id: str) -> list[str] | None:
        if detector_modalities is None:
            return None
        if not isinstance(detector_modalities, list) or not detector_modalities:
            raise ValueError(f"Task3 reference spec detector_modalities must be a non-empty list for {reference_id}")
        normalized_modalities: list[str] = []
        for modality in detector_modalities:
            modality_text = str(modality)
            if modality_text not in self.VALID_MODALITIES:
                raise ValueError(
                    f"Task3 reference spec detector_modalities must be within {sorted(self.VALID_MODALITIES)}, "
                    f"got {modality_text!r} for {reference_id}"
                )
            if modality_text not in normalized_modalities:
                normalized_modalities.append(modality_text)
        return normalized_modalities