refactor: make the matcher and detector safe for concurrent clients

The recognition server will classify crops from several clients at once. Two
things blocked that:

- OpenCV Feature2D objects were shared across calls, so parallel
  classify_crop() calls could corrupt each other. Each thread now gets its own
  extractor set; the template cache is untouched.
- score_crop() is extracted from classify_crop() so callers that need every SKU
  score (live track aggregation) do not have to re-derive it from the top-5
  list. classify_crop() output is byte-identical to before (verified against the
  pre-change module on 82 real crops x 4 methods).
- ShelfProductDetector accepts device/imgsz so the server can pin inference.

Dependencies: pin ultralytics to the version that wrote the checkpoint and that
the session tracker targets (8.4.153), add lap so ultralytics never
pip-installs it mid-request, and add websockets for the streaming endpoint.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kiyo9w 2026-09-24 11:43:24 +09:00
parent b443e3c381
commit 0f1d46f9b3
4 changed files with 73 additions and 32 deletions

View file

@ -99,8 +99,17 @@ from ultralytics import YOLO
class ShelfProductDetector:
"""Wraps the YOLO detector for supermarket shelf products."""
def __init__(self, weights_path: str = "runs/detect/train/weights/best.pt"):
def __init__(
self,
weights_path: str = "runs/detect/train/weights/best.pt",
device: Optional[str] = None,
imgsz: int = 640,
half: bool = False
):
self.weights_path = weights_path
self.device = device
self.imgsz = imgsz
self.half = half
if not os.path.exists(weights_path):
raise FileNotFoundError(f"YOLO weights not found at: {weights_path}")
@ -129,7 +138,14 @@ class ShelfProductDetector:
return {"status": "error", "message": "Failed to read image", "boxes": []}
h, w = image_bgr.shape[:2]
results = self.model(image_bgr, conf=conf_threshold, iou=iou_threshold, verbose=False)
predict_kwargs: Dict[str, Any] = dict(
conf=conf_threshold, iou=iou_threshold, imgsz=self.imgsz, verbose=False
)
if self.device:
predict_kwargs["device"] = self.device
if self.half:
predict_kwargs["half"] = True
results = self.model(image_bgr, **predict_kwargs)
boxes_list = []
if results and len(results) > 0: