fix: restore reliable authenticated live recognition
This commit is contained in:
parent
0e23425ef0
commit
934615b593
40 changed files with 2556 additions and 820 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue