# -*- coding: utf-8 -*- import json import logging import os from typing import List, Optional, Tuple import numpy as np import onnxruntime as ort from PIL import Image logger = logging.getLogger(__name__) IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32) IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32) class OnnxClassifier: def __init__(self, model_path: str, image_size: int = 224): self.image_size = image_size self.session: Optional[ort.InferenceSession] = None if model_path and os.path.isfile(model_path): self.session = ort.InferenceSession( model_path, providers=["CPUExecutionProvider"], ) logger.info("加载 ONNX: %s", model_path) @property def loaded(self) -> bool: return self.session is not None def preprocess(self, image_path: str) -> np.ndarray: img = Image.open(image_path).convert("RGB").resize((self.image_size, self.image_size)) arr = np.array(img).astype(np.float32) / 255.0 arr = (arr - IMAGENET_MEAN) / IMAGENET_STD return arr.transpose(2, 0, 1)[None] def predict_batch(self, image_paths: List[str], batch_size: int = 16) -> List[float]: if not self.session: return [0.0] * len(image_paths) scores = [] for i in range(0, len(image_paths), batch_size): batch_paths = image_paths[i : i + batch_size] batch = np.concatenate([self.preprocess(p) for p in batch_paths], axis=0) logits = self.session.run(None, {"input": batch})[0] for logit in logits: scores.append(float(1.0 / (1.0 + np.exp(-logit[0])))) return scores class ModelRegistry: def __init__(self, model_dir: str): self.model_dir = model_dir self.version = "unknown" self.image_size = 224 self.storefront: Optional[OnnxClassifier] = None self.handover: Optional[OnnxClassifier] = None self._load() def _load(self): version_path = os.path.join(self.model_dir, "version.json") sf_name = ho_name = None if os.path.isfile(version_path): with open(version_path, encoding="utf-8") as f: meta = json.load(f) self.version = meta.get("model_version", "1.0.0") self.image_size = int(meta.get("image_size", 224)) sf_name = meta.get("storefront_model") ho_name = meta.get("handover_model") def resolve(name: str, fallback: str) -> str: if name: path = os.path.join(self.model_dir, name) if os.path.isfile(path): return path for suffix in (f"{fallback}_int8.onnx", f"{fallback}.onnx"): path = os.path.join(self.model_dir, suffix) if os.path.isfile(path): return path return "" sf_path = resolve(sf_name, "storefront") ho_path = resolve(ho_name, "handover") self.storefront = OnnxClassifier(sf_path, self.image_size) if sf_path else OnnxClassifier("", self.image_size) self.handover = OnnxClassifier(ho_path, self.image_size) if ho_path else OnnxClassifier("", self.image_size) @property def ready(self) -> bool: return self.storefront.loaded and self.handover.loaded def score_frames( registry: ModelRegistry, frames: List[Tuple[float, str]], ) -> Tuple[List[Tuple[float, float]], List[Tuple[float, float]]]: times = [f[0] for f in frames] paths = [f[1] for f in frames] sf_scores = registry.storefront.predict_batch(paths) ho_scores = registry.handover.predict_batch(paths) return list(zip(times, sf_scores)), list(zip(times, ho_scores))