feat: recognition server API, reference stream client, and live label memory
app.py becomes the recognition server: /v1 REST (health, catalog + template images, samples, recognize, classify, inspect, stock) plus /v1/stream over WebSocket, with token auth, size limits, one consistent error shape, and the legacy /api/sessions routes kept but now backed by the real engine instead of the fake adapter. Default bind is 127.0.0.1; LAN and TLS are opt-in. stream_client.py is the reference non-browser client (camera, video file, or a pan across a still) and prints results/s, RTT, detect time and label stability. Live quality work driven by measurements: - Labels are remembered per place: a fresh track covering a recently recognized product inherits the label instead of queueing for classification again. On a continuous pan this raised recognized products in view from 9/66 to 17-21/66 and cut time-to-first-label to ~1.6 s. - Classification priority is newest-first, because sorting by area spends the budget on products that are already leaving the frame. - Per-session classification concurrency raised to 12. hybrid_fast (ORB shortlist then hybrid) is implemented but NOT the default: it is 3.4x faster (13 ms vs 44 ms p50) yet agreed with hybrid on only 47% of accepted crops across 553 crops, far below the 98% adoption rule. It stays available for experiments; the numbers are in the commit and the benchmark. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
afb59b487f
commit
5f84f4f2a7
5 changed files with 1152 additions and 1170 deletions
|
|
@ -47,7 +47,7 @@ STATE_RECOGNIZED = "recognized"
|
|||
STATE_NEEDS_CONFIRMATION = "needs_confirmation"
|
||||
STATE_UNRECOGNIZED = "unrecognized"
|
||||
|
||||
CLASSIFY_METHODS = ("surf", "orb", "brisk", "hybrid")
|
||||
CLASSIFY_METHODS = ("surf", "orb", "brisk", "hybrid", "hybrid_fast")
|
||||
|
||||
|
||||
class UndecodableImage(ValueError):
|
||||
|
|
@ -71,6 +71,12 @@ class EngineSettings:
|
|||
# p50 while MPS takes ~112 ms with identical boxes, so CPU is the default.
|
||||
# Override with --device / DORAYAKI_DEVICE (cpu | mps | cuda | cuda:0).
|
||||
device: Optional[str] = "cpu"
|
||||
# Live-mode defaults, measured with stream_client.py --pace result:
|
||||
# hybrid classifies at ~44 ms/crop p50 and is the most accurate method
|
||||
# (21/21 SKUs on image.jpg). hybrid_fast (ORB shortlist) is 3.4x faster but
|
||||
# agreed with hybrid on only 47% of accepted crops, so it is NOT the default
|
||||
# and stays available for experiments only (bench_engine.py --compare).
|
||||
live_inflight: int = 12
|
||||
imgsz: int = 640
|
||||
classify_workers: int = 0 # 0 -> max(2, cpu_count - 2)
|
||||
live_method: str = "hybrid"
|
||||
|
|
@ -348,7 +354,10 @@ class RecognitionEngine:
|
|||
method = (method or self.settings.live_method).lower()
|
||||
if method not in CLASSIFY_METHODS:
|
||||
raise UnknownMethod(method)
|
||||
scores, _ = self.matcher.score_crop(crop, method=method)
|
||||
if method == "hybrid_fast":
|
||||
scores, _ = self.matcher.score_crop_fast(crop)
|
||||
else:
|
||||
scores, _ = self.matcher.score_crop(crop, method=method)
|
||||
return {str(sku): float(score) for sku, score in scores.items()}
|
||||
|
||||
def classify_boxes(
|
||||
|
|
@ -367,7 +376,10 @@ class RecognitionEngine:
|
|||
return []
|
||||
ordered = sorted(boxes, key=lambda b: b["width"] * b["height"], reverse=True)
|
||||
crops = [self.crop(image, box["box"]) for box in ordered]
|
||||
futures = [self._executor.submit(self.matcher.score_crop, crop, method) for crop in crops]
|
||||
if method == "hybrid_fast":
|
||||
futures = [self._executor.submit(self.matcher.score_crop_fast, crop) for crop in crops]
|
||||
else:
|
||||
futures = [self._executor.submit(self.matcher.score_crop, crop, method) for crop in crops]
|
||||
results: List[Dict[str, Any]] = []
|
||||
for box, future in zip(ordered, futures):
|
||||
try:
|
||||
|
|
@ -392,7 +404,10 @@ class RecognitionEngine:
|
|||
if method not in CLASSIFY_METHODS:
|
||||
raise UnknownMethod(method)
|
||||
crop = self.crop(image, box)
|
||||
scores, details = self.matcher.score_crop(crop, method=method)
|
||||
if method == "hybrid_fast":
|
||||
scores, details = self.matcher.score_crop_fast(crop)
|
||||
else:
|
||||
scores, details = self.matcher.score_crop(crop, method=method)
|
||||
decision = decide_identity(scores, details=details, method=method)
|
||||
identity = identity_payload(decision)
|
||||
|
||||
|
|
@ -416,7 +431,7 @@ class RecognitionEngine:
|
|||
if visualized_sku:
|
||||
try:
|
||||
visualization = self.matcher.generate_match_visualization(
|
||||
crop, visualized_sku, method=method
|
||||
crop, visualized_sku, method=method if method != "hybrid_fast" else "hybrid"
|
||||
)
|
||||
except Exception:
|
||||
visualization = None
|
||||
|
|
@ -647,11 +662,12 @@ def settings_from_env(env: Optional[Dict[str, str]] = None) -> EngineSettings:
|
|||
base,
|
||||
weights_path=env.get("DORAYAKI_WEIGHTS", base.weights_path),
|
||||
stock_dir=env.get("DORAYAKI_STOCK_DIR", base.stock_dir),
|
||||
device=env.get("DORAYAKI_DEVICE") or None,
|
||||
device=env.get("DORAYAKI_DEVICE") or base.device,
|
||||
imgsz=int(env.get("DORAYAKI_IMGSZ", base.imgsz)),
|
||||
classify_workers=int(env.get("DORAYAKI_CLASSIFY_WORKERS", base.classify_workers)),
|
||||
live_method=env.get("DORAYAKI_LIVE_METHOD", base.live_method),
|
||||
live_conf=float(env.get("DORAYAKI_LIVE_CONF", base.live_conf)),
|
||||
live_inflight=int(env.get("DORAYAKI_LIVE_INFLIGHT", base.live_inflight)),
|
||||
detect_conf=float(env.get("DORAYAKI_DETECT_CONF", base.detect_conf)),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue