feat: identity decision contract and session-local observation model
Keeps the model's uncertainty out of the UI: decide_identity() separates raw matcher scores into accepted / ambiguous / unknown / rejected, and only an accepted result can name a SKU. Zero evidence is rejected, never a product. observe.py adds frame-local observations and one-zone scan sessions (draft, finalize, cancel, rescan) with replay, staleness and backpressure rules. pipeline.process_click() now reports the identity level instead of failing when the match is not accepted. Incorporates earlier uncommitted session work in this repository (identity.py, observe.py, yolo_adapter.py, test_observe.py, and the pipeline hunk those needed); yolo_adapter keeps its decoded frame per thread so concurrent sessions cannot swap images. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0f1d46f9b3
commit
356f7aa502
5 changed files with 977 additions and 16 deletions
64
recognization/code/yolo_adapter.py
Normal file
64
recognization/code/yolo_adapter.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""Optional YOLO + matcher adapter for PerceptionService.
|
||||
|
||||
Imported only when OpenCV/YOLO are available. Tests use PerceptionAdapter
|
||||
fakes instead. This does not keep process-global active images.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from observe import PerceptionAdapter
|
||||
|
||||
|
||||
class YoloMatcherAdapter(PerceptionAdapter):
|
||||
def __init__(self, detector: Any, matcher: Any, method: str = "hybrid"):
|
||||
super().__init__(
|
||||
model_version=getattr(detector, "weights_path", "yolo-local"),
|
||||
matcher_method=method,
|
||||
feature_extractor="surf-or-sift+orb+brisk",
|
||||
)
|
||||
self.detector = detector
|
||||
self.matcher = matcher
|
||||
# observe() calls detect() then classify_box() on the same thread; keep
|
||||
# the decoded frame per thread so concurrent sessions cannot swap images.
|
||||
self._thread_local = threading.local()
|
||||
|
||||
def detect(self, frame_bytes: bytes) -> Dict[str, Any]:
|
||||
array = np.frombuffer(frame_bytes, dtype=np.uint8)
|
||||
image = cv2.imdecode(array, cv2.IMREAD_COLOR)
|
||||
if image is None:
|
||||
return {
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"boxes": [],
|
||||
"empty_slots": [],
|
||||
"quality": {"limitation": "undecodable-frame"},
|
||||
}
|
||||
self._thread_local.image = image
|
||||
h, w = image.shape[:2]
|
||||
det = self.detector.detect(image)
|
||||
boxes: List[Dict[str, Any]] = det.get("boxes", [])
|
||||
empty = self.detector.detect_empty_slots(image.shape, boxes)
|
||||
return {
|
||||
"width": w,
|
||||
"height": h,
|
||||
"boxes": boxes,
|
||||
"empty_slots": empty,
|
||||
"quality": {"decoded": True},
|
||||
}
|
||||
|
||||
def classify_box(self, box: Dict[str, Any]) -> Dict[str, float]:
|
||||
image = getattr(self._thread_local, "image", None)
|
||||
if image is None:
|
||||
return {}
|
||||
crop = self.detector.crop_roi(image, box["box"])
|
||||
ranked = self.matcher.classify_crop(crop, method=self.matcher_method)
|
||||
return {
|
||||
row["sku_id"]: float(row["score"])
|
||||
for row in ranked.get("ranked_candidates", [])
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue