fix: restore reliable authenticated live recognition

This commit is contained in:
kiyo9w 2026-09-28 12:38:15 +09:00
parent 0e23425ef0
commit 934615b593
40 changed files with 2556 additions and 820 deletions

View file

@ -26,6 +26,7 @@ on the GPU. That is why this can sit in the live loop at all.
from __future__ import annotations
import os
import re
import threading
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Tuple
@ -93,23 +94,39 @@ class ProductGallery:
# ------------------------------------------------------------------ loading
def load_head(self) -> bool:
"""Load the store's fine-tuned head, if it has one."""
"""Load the store's fine-tuned head, if it has one.
weights_only: the file is tensors and strings, not executable code.
A payload that is not that head leaves the raw gallery in place.
"""
import torch
if not os.path.exists(self._head_path):
return False
try:
payload = torch.load(self._head_path, map_location="cpu", weights_only=False)
payload = torch.load(self._head_path, map_location="cpu", weights_only=True)
if not isinstance(payload, dict):
return False
projection_state = payload.get("projection")
prototypes = payload.get("prototypes")
class_ids = payload.get("class_ids")
if not isinstance(projection_state, dict) or not torch.is_tensor(prototypes):
return False
if not isinstance(class_ids, (list, tuple)) or prototypes.ndim != 2:
return False
if prototypes.shape[0] != len(class_ids):
return False
projection = torch.nn.Linear(384, 128)
projection.load_state_dict(projection_state)
projection.eval()
self._head = {
"projection": projection,
"prototypes": torch.nn.functional.normalize(prototypes.float(), dim=1),
"class_ids": [str(sku_id) for sku_id in class_ids],
}
except Exception:
self._head = None
return False
projection = torch.nn.Linear(384, 128)
projection.load_state_dict(payload["projection"])
projection.eval()
self._head = {
"projection": projection,
"prototypes": torch.nn.functional.normalize(payload["prototypes"].float(), dim=1),
"class_ids": list(payload["class_ids"]),
}
return True
def _load_model(self) -> Any:
@ -137,8 +154,12 @@ class ProductGallery:
return self._available
def _views_for(self, sku_id: str) -> List[str]:
"""Every image in the stock directory that belongs to this SKU."""
"""Images whose stem is the SKU or SKU plus _N / -N.
A bare prefix would enroll sku_001 as a view of sku_00.
"""
found: List[str] = []
extra = re.compile(rf"{re.escape(sku_id)}[_-]\d+$")
try:
names = sorted(os.listdir(self.stock_dir))
except OSError:
@ -147,42 +168,50 @@ class ProductGallery:
stem, extension = os.path.splitext(name)
if extension.lower() not in (".jpeg", ".jpg", ".png"):
continue
if stem == sku_id or stem.startswith(f"{sku_id}_") or stem.startswith(f"{sku_id}-"):
if stem == sku_id or extra.fullmatch(stem):
found.append(os.path.join(self.stock_dir, name))
return found
def load(self) -> bool:
"""Embed every enrolled view. Returns False when there is nothing to load."""
"""Embed every enrolled view. Returns False when there is nothing to load.
One lock covers the whole load: two callers cannot each embed the catalog.
"""
with self._lock:
if self._available and self._matrix is not None:
return True
return self._load_locked()
def _load_locked(self) -> bool:
import cv2
import numpy as np
import torch
paths: List[Tuple[str, str]] = []
kept: List[Any] = []
for sku_id in self.sku_ids:
for path in self._views_for(sku_id):
image = cv2.imread(path)
if image is None or image.size == 0:
continue
paths.append((sku_id, path))
if not paths:
kept.append(_prepare(image, self.image_size))
if not kept:
self._available = False
self._entries = []
self._matrix = None
return False
try:
model = self._load_model()
tensor = torch.from_numpy(np.stack(kept)).to(self.device)
with torch.no_grad():
features = model(tensor)
features = torch.nn.functional.normalize(features, dim=1).cpu().numpy()
except Exception:
self._available = False
self._entries = []
self._matrix = None
return False
batch = []
for _, path in paths:
image = cv2.imread(path)
if image is None or image.size == 0:
continue
batch.append(_prepare(image, self.image_size))
if not batch:
self._available = False
return False
tensor = torch.from_numpy(np.stack(batch)).to(self.device)
with torch.no_grad():
features = model(tensor)
features = torch.nn.functional.normalize(features, dim=1).cpu().numpy()
self._entries = [
GalleryEntry(sku_id=sku_id, view=path, embedding=features[index])
for index, (sku_id, path) in enumerate(paths)
@ -209,13 +238,14 @@ class ProductGallery:
if not self._available or self._matrix is None:
return GalleryMatch(sku_id=None, similarity=0.0, rejected=True)
import numpy as np
import torch
limit = threshold
if limit is None:
limit = self.head_threshold if self._head is not None else self.threshold
limit = float(limit)
embedding = self.embed(crop)
if self._head is not None:
import torch
if self._head is not None:
# Two stages, because the two mechanisms fail in opposite directions.
# Measured: raw features reject strangers well (5 % false accepts) but