203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
"""OpenVINO 引擎实现 —— YOLO 5/8/11/26 + 全任务 + 设备支持
|
||
|
||
依赖:openvino, opencv-python, numpy
|
||
推理设备:
|
||
cpu -> CPU
|
||
gpu -> GPU
|
||
cuda -> 不适用,回退 CPU(OpenVINO 不走 CUDA)
|
||
"""
|
||
import logging
|
||
import os
|
||
|
||
from app.analysis.engines.base import BaseEngine, DetectionResult
|
||
|
||
logger = logging.getLogger("analysis.engines.openvino")
|
||
|
||
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:
|
||
from openvino import Core
|
||
_OV_AVAILABLE = True
|
||
except Exception:
|
||
try:
|
||
from openvino.runtime import Core
|
||
_OV_AVAILABLE = True
|
||
except Exception:
|
||
Core = None
|
||
_OV_AVAILABLE = False
|
||
|
||
|
||
def _device_for(device):
|
||
d = (device or "cpu").lower()
|
||
if d in ("gpu", "cuda", "0"):
|
||
return "GPU"
|
||
return "CPU"
|
||
|
||
|
||
class OpenVinoEngine(BaseEngine):
|
||
ENGINE_NAME = "openvino"
|
||
|
||
def __init__(self, **kwargs):
|
||
super(OpenVinoEngine, self).__init__(**kwargs)
|
||
self._compiled = None
|
||
self._infer_req = None
|
||
self._input_key = None
|
||
self._output_keys = None
|
||
self.task_type = (kwargs.get("task_type") or "detect").lower()
|
||
self.device = kwargs.get("device") or "cpu"
|
||
|
||
@staticmethod
|
||
def is_available():
|
||
return _OV_AVAILABLE and _CV2_AVAILABLE and _NP_AVAILABLE
|
||
|
||
@staticmethod
|
||
def version():
|
||
if not _OV_AVAILABLE:
|
||
return None
|
||
try:
|
||
from openvino.runtime import get_version
|
||
return get_version()
|
||
except Exception:
|
||
pass
|
||
try:
|
||
import openvino
|
||
return getattr(openvino, "__version__", "unknown")
|
||
except Exception:
|
||
return "unknown"
|
||
|
||
def _resolve_model_path(self):
|
||
if not self.model_file:
|
||
return None
|
||
if self.model_file.lower().endswith(".onnx"):
|
||
return self.model_file
|
||
base, ext = os.path.splitext(self.model_file)
|
||
if ext.lower() == ".xml":
|
||
return self.model_file
|
||
xml_p = base + ".xml"
|
||
if os.path.exists(xml_p):
|
||
return xml_p
|
||
return None
|
||
|
||
def load(self):
|
||
if not self.is_available():
|
||
logger.warning("OpenVinoEngine: 依赖未安装")
|
||
return False
|
||
path = self._resolve_model_path()
|
||
if not path or not os.path.exists(path):
|
||
logger.warning("OpenVinoEngine: 模型文件不可用: %s", self.model_file)
|
||
return False
|
||
if not self.labels:
|
||
self.labels = self._resolve_labels(self.model_file)
|
||
try:
|
||
core = Core()
|
||
model = core.read_model(path)
|
||
dev = _device_for(self.device)
|
||
# GPU 不可用时回退 CPU
|
||
try:
|
||
self._compiled = core.compile_model(model, dev)
|
||
except Exception as e:
|
||
logger.warning("OpenVinoEngine: 设备 %s 编译失败,回退 CPU: %s", dev, e)
|
||
self._compiled = core.compile_model(model, "CPU")
|
||
dev = "CPU"
|
||
self._infer_req = self._compiled.create_infer_request()
|
||
self._input_key = list(self._compiled.inputs)[0]
|
||
self._output_keys = list(self._compiled.outputs)
|
||
self._loaded = True
|
||
logger.info("OpenVinoEngine: 已加载 %s, task=%s, device=%s, labels=%d",
|
||
path, self.task_type, dev, len(self.labels))
|
||
return True
|
||
except Exception as e:
|
||
logger.error("OpenVinoEngine: 加载失败: %s", e)
|
||
self._loaded = False
|
||
return False
|
||
|
||
def _preprocess(self, frame_bgr):
|
||
iw, ih = self.input_size
|
||
resized = cv2.resize(frame_bgr, (iw, ih))
|
||
rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
|
||
blob = rgb.astype(np.float32) / 255.0
|
||
blob = np.transpose(blob, (2, 0, 1))[None, ...]
|
||
return blob
|
||
|
||
def detect(self, frame_bgr):
|
||
if not self.ready() or frame_bgr is None:
|
||
return []
|
||
try:
|
||
h, w = frame_bgr.shape[:2]
|
||
blob = self._preprocess(frame_bgr)
|
||
self._infer_req.infer({self._input_key: blob})
|
||
outputs = []
|
||
out_count = len(self._output_keys)
|
||
for i in range(out_count):
|
||
try:
|
||
outputs.append(self._infer_req.get_output_tensor(i).data)
|
||
except Exception:
|
||
try:
|
||
outputs.append(self._infer_req.get_output_tensor().data)
|
||
except Exception as e:
|
||
logger.warning("OpenVinoEngine: 读取输出 tensor[%d] 失败: %s", i, e)
|
||
if not outputs:
|
||
return []
|
||
from app.analysis.engines.yolo_postprocess import decode_outputs
|
||
results = decode_outputs(
|
||
outputs=outputs,
|
||
algorithm_type=self.algorithm_type,
|
||
task_type=self.task_type,
|
||
labels=self.labels,
|
||
input_size=self.input_size,
|
||
conf_threshold=self.conf_threshold,
|
||
iou_threshold=self.iou_threshold,
|
||
orig_size=(w, h),
|
||
)
|
||
return [DetectionResult(**r) for r in results]
|
||
except Exception as e:
|
||
logger.warning("OpenVinoEngine.detect() err: %s", e)
|
||
return []
|
||
|
||
def info(self):
|
||
d = super(OpenVinoEngine, self).info()
|
||
d["version"] = self.version()
|
||
d["task_type"] = self.task_type
|
||
d["device"] = _device_for(self.device)
|
||
return d
|
||
|
||
def probe(self):
|
||
info = {"engine": self.ENGINE_NAME, "available": self.is_available(),
|
||
"version": self.version(), "input_shape": None, "output_shape": None,
|
||
"labels": self.labels, "model_file": self.model_file,
|
||
"task_type": self.task_type, "device": _device_for(self.device)}
|
||
if not self.is_available() or not self.model_file:
|
||
return info
|
||
path = self._resolve_model_path()
|
||
if not path or not os.path.exists(path):
|
||
info["error"] = "model file not resolvable"
|
||
return info
|
||
try:
|
||
core = Core()
|
||
model = core.read_model(path)
|
||
if model.inputs:
|
||
shape = list(model.inputs[0].shape)
|
||
info["input_shape"] = shape
|
||
if len(shape) >= 4:
|
||
info["input_size_inferred"] = (int(shape[-1]), int(shape[-2]))
|
||
if model.outputs:
|
||
info["output_shape"] = [list(o.shape) for o in model.outputs]
|
||
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
|