188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
"""PyTorch 引擎实现
|
||
|
||
支持加载 ultralytics YOLOv8/v5/v7 .pt 模型(依赖 ultralytics 包),
|
||
或原生 torch.hub YOLOv5 custom 加载。
|
||
依赖:torch, ultralytics(推荐)
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
|
||
from app.analysis.engines.base import BaseEngine, DetectionResult
|
||
|
||
logger = logging.getLogger("analysis.engines.pytorch")
|
||
|
||
try:
|
||
import numpy as np
|
||
_NP_AVAILABLE = True
|
||
except Exception:
|
||
np = None
|
||
_NP_AVAILABLE = False
|
||
|
||
try:
|
||
import cv2
|
||
_CV2_AVAILABLE = True
|
||
except Exception:
|
||
cv2 = None
|
||
_CV2_AVAILABLE = False
|
||
|
||
try:
|
||
import torch
|
||
_TORCH_AVAILABLE = True
|
||
except Exception:
|
||
torch = None
|
||
_TORCH_AVAILABLE = False
|
||
|
||
try:
|
||
from ultralytics import YOLO as _UltralyticsYOLO
|
||
_ULTRALYTICS_AVAILABLE = True
|
||
except Exception:
|
||
_UltralyticsYOLO = None
|
||
_ULTRALYTICS_AVAILABLE = False
|
||
|
||
|
||
class PyTorchEngine(BaseEngine):
|
||
ENGINE_NAME = "pytorch"
|
||
|
||
def __init__(self, **kwargs):
|
||
super(PyTorchEngine, self).__init__(**kwargs)
|
||
self._model = None
|
||
self._kind = None # "ultralytics" / "torchhub"
|
||
|
||
@staticmethod
|
||
def is_available():
|
||
return _TORCH_AVAILABLE and _CV2_AVAILABLE and _NP_AVAILABLE
|
||
|
||
@staticmethod
|
||
def version():
|
||
if not _TORCH_AVAILABLE:
|
||
return None
|
||
try:
|
||
return getattr(torch, "__version__", "unknown")
|
||
except Exception:
|
||
return "unknown"
|
||
|
||
@staticmethod
|
||
def ultralytics_available():
|
||
return _ULTRALYTICS_AVAILABLE
|
||
|
||
def load(self):
|
||
if not self.is_available():
|
||
logger.warning("PyTorchEngine: 依赖未安装 (torch=%s)", _TORCH_AVAILABLE)
|
||
return False
|
||
if not self.model_file or not os.path.exists(self.model_file):
|
||
logger.warning("PyTorchEngine: 模型文件不存在: %s", self.model_file)
|
||
return False
|
||
from app.utils.ModelTrust import require_trusted_model
|
||
require_trusted_model(self.model_file)
|
||
if not self.labels:
|
||
self.labels = self._resolve_labels(self.model_file)
|
||
|
||
# 优先用 ultralytics 加载 .pt / .engine / .onnx
|
||
if _ULTRALYTICS_AVAILABLE:
|
||
try:
|
||
self._model = _UltralyticsYOLO(self.model_file)
|
||
self._kind = "ultralytics"
|
||
self._loaded = True
|
||
# 推断 input_size
|
||
try:
|
||
cfg = getattr(self._model, "overrides", {}) or {}
|
||
imgsz = cfg.get("imgsz", None)
|
||
if isinstance(imgsz, int) and imgsz > 0:
|
||
self.input_size = (int(imgsz), int(imgsz))
|
||
except Exception:
|
||
pass
|
||
logger.info("PyTorchEngine(ultralytics): 已加载 %s, labels=%d", self.model_file, len(self.labels))
|
||
return True
|
||
except Exception as e:
|
||
logger.warning("PyTorchEngine(ultralytics) 加载失败,尝试 torch.hub: %s", e)
|
||
|
||
# 退化:torch.hub YOLOv5 custom
|
||
try:
|
||
self._model = torch.hub.load("ultralytics/yolov5", "custom", path=self.model_file, trust_repo=True)
|
||
self._kind = "torchhub"
|
||
self._loaded = True
|
||
logger.info("PyTorchEngine(torchhub): 已加载 %s", self.model_file)
|
||
return True
|
||
except Exception as e:
|
||
logger.error("PyTorchEngine: 加载失败: %s", e)
|
||
self._loaded = False
|
||
self._model = None
|
||
return False
|
||
|
||
def detect(self, frame_bgr):
|
||
if not self.ready() or frame_bgr is None or self._model is None:
|
||
return []
|
||
try:
|
||
if self._kind == "ultralytics":
|
||
iw, ih = self.input_size
|
||
res = self._model.predict(frame_bgr, imgsz=max(iw, ih), conf=self.conf_threshold,
|
||
iou=self.iou_threshold, verbose=False)
|
||
return self._parse_ultralytics(res, frame_bgr.shape[:2])
|
||
else:
|
||
# torchhub yolov5
|
||
res = self._model(frame_bgr, size=max(self.input_size))
|
||
return self._parse_torchhub(res, frame_bgr.shape[:2])
|
||
except Exception as e:
|
||
logger.warning("PyTorchEngine.detect() err: %s", e)
|
||
return []
|
||
|
||
def _parse_ultralytics(self, results, orig_shape):
|
||
out = []
|
||
try:
|
||
r = results[0]
|
||
if hasattr(r, "boxes") and r.boxes is not None:
|
||
boxes = r.boxes.xyxy.cpu().numpy().astype(int)
|
||
confs = r.boxes.conf.cpu().numpy()
|
||
cls_ids = r.boxes.cls.cpu().numpy().astype(int)
|
||
names = getattr(r, "names", {}) or {}
|
||
for (x1, y1, x2, y2), s, cid in zip(boxes, confs, cls_ids):
|
||
if s < self.conf_threshold:
|
||
continue
|
||
label = names.get(int(cid), str(int(cid))) if isinstance(names, dict) else (
|
||
self.labels[int(cid)] if 0 <= int(cid) < len(self.labels) else str(int(cid)))
|
||
out.append(DetectionResult(box=[int(x1), int(y1), int(x2), int(y2)],
|
||
label=str(label), score=float(s)))
|
||
except Exception as e:
|
||
logger.warning("PyTorchEngine._parse_ultralytics err: %s", e)
|
||
return out
|
||
|
||
def _parse_torchhub(self, results, orig_shape):
|
||
out = []
|
||
try:
|
||
df = results.pandas().xyxy[0]
|
||
for _, row in df.iterrows():
|
||
s = float(row["confidence"])
|
||
if s < self.conf_threshold:
|
||
continue
|
||
out.append(DetectionResult(box=[int(row["xmin"]), int(row["ymin"]), int(row["xmax"]), int(row["ymax"])],
|
||
label=str(row["name"]), score=s))
|
||
except Exception as e:
|
||
logger.warning("PyTorchEngine._parse_torchhub err: %s", e)
|
||
return out
|
||
|
||
def info(self):
|
||
d = super(PyTorchEngine, self).info()
|
||
d["version"] = self.version()
|
||
d["ultralytics_available"] = _ULTRALYTICS_AVAILABLE
|
||
d["kind"] = self._kind
|
||
d["device"] = "cuda" if (_TORCH_AVAILABLE and torch.cuda.is_available()) else "cpu"
|
||
return d
|
||
|
||
def probe(self):
|
||
info = {"engine": self.ENGINE_NAME, "available": self.is_available(),
|
||
"version": self.version(), "ultralytics_available": _ULTRALYTICS_AVAILABLE,
|
||
"input_shape": None, "output_shape": None,
|
||
"labels": self.labels, "model_file": self.model_file}
|
||
if not self.is_available() or not self.model_file or not os.path.exists(self.model_file):
|
||
return info
|
||
try:
|
||
# 仅读文件大小,不实际加载(torch 模型加载慢且占内存)
|
||
info["model_file_size"] = os.path.getsize(self.model_file)
|
||
if not self.labels:
|
||
self.labels = self._resolve_labels(self.model_file)
|
||
info["labels"] = self.labels
|
||
except Exception as e:
|
||
info["error"] = str(e)
|
||
return info
|