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:
kiyo9w 2026-09-24 11:43:41 +09:00
parent 0f1d46f9b3
commit 356f7aa502
5 changed files with 977 additions and 16 deletions

View file

@ -116,34 +116,31 @@ class ShelfRecognitionPipeline:
# Classify with selected method
cls_result = self.matcher.classify_crop(crop, method=method)
if cls_result.get("status") != "success":
return {
"status": "classification_failed",
"target_box": target_box,
"message": cls_result.get("message", "Feature matching failed.")
}
identity_level = cls_result.get("identity_level", "rejected")
best_sku = cls_result.get("best_sku")
product_info = self.inventory.get_product(best_sku) if best_sku else None
best_sku = cls_result["best_sku"]
product_info = self.inventory.get_product(best_sku)
vis_match = None
if best_sku:
vis_match = self.matcher.generate_match_visualization(crop, best_sku, method=method)
# Generate match visualization
vis_match = self.matcher.generate_match_visualization(crop, best_sku, method=method)
# Generate annotated shelf image highlighting the selected item
annotated_shelf = self.detector.draw_detections(
self.current_image, self.current_detections, selected_box_id=target_box["id"]
)
click_status = "success" if identity_level == "accepted" else identity_level
return {
"status": "success",
"status": click_status,
"clicked_point": (x, y),
"target_box": target_box,
"classification": {
"method": method,
"matched_sku": best_sku,
"identity_level": identity_level,
"confidence": cls_result["confidence"],
"score": cls_result["score"],
"top_candidates": cls_result["ranked_candidates"],
"message": cls_result.get("message"),
},
"inventory": product_info,
"crop_image": crop,
@ -172,9 +169,10 @@ if __name__ == "__main__":
res = pipeline.process_click(cx, cy, method=args.method)
print("\n--- Recognition & Inventory Result ---")
print(f"Status: {res['status']}")
if res['status'] == 'success':
prod = res['inventory']
print(f"Matched SKU: {res['classification']['matched_sku']} (Score: {res['classification']['score']})")
cls = res.get("classification") or {}
print(f"Identity: {cls.get('identity_level')} SKU={cls.get('matched_sku')} score={cls.get('score')}")
prod = res.get("inventory")
if prod:
print(f"Product Name: {prod['product_name']}")
print(f"Price: {prod['price']} JPY")
print(f"In Stock: {prod['number_on']} units (Received: {prod['number_in']}, Sold: {prod['number_out']})")