This commit is contained in:
orisys
2026-08-21 14:51:57 +08:00
commit 4ad53f4e97
2056 changed files with 6272 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
"""统一检测器接口:YOLO / Faster R-CNN,供推理与二阶段脚本共用。"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import cv2
import numpy as np
import torch
ROOT = Path(__file__).resolve().parent
@dataclass
class DetBox:
xyxy: tuple[int, int, int, int]
conf: float
cls_id: int = 0
def import_yolo():
"""导入 YOLO。项目下 ultralytics/ 源码目录会挡住包,需优先指向仓库根。"""
import sys
repo = ROOT / "ultralytics"
if (repo / "ultralytics" / "__init__.py").exists():
repo_s = str(repo)
while repo_s in sys.path:
sys.path.remove(repo_s)
sys.path.insert(0, repo_s)
mod = sys.modules.get("ultralytics")
if mod is not None and getattr(mod, "__file__", None) is None:
for key in list(sys.modules):
if key == "ultralytics" or key.startswith("ultralytics."):
del sys.modules[key]
from ultralytics import YOLO
return YOLO
def resolve_device(device: str | int) -> torch.device:
text = str(device).strip().lower()
if text in {"cpu", "-1"}:
return torch.device("cpu")
if torch.cuda.is_available():
try:
return torch.device(f"cuda:{int(text)}")
except Exception:
return torch.device("cuda:0")
return torch.device("cpu")
def build_faster_rcnn(num_classes: int, pretrained_backbone: bool = True):
"""num_classes = 前景类数 + 1(含背景)。"""
from torchvision.models.detection import (
FasterRCNN_ResNet50_FPN_Weights,
fasterrcnn_resnet50_fpn,
)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
weights = FasterRCNN_ResNet50_FPN_Weights.DEFAULT if pretrained_backbone else None
model = fasterrcnn_resnet50_fpn(weights=weights)
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
return model
class YOLODetector:
backend = "yolo"
def __init__(self, weights: str | Path, device: str | int = "0") -> None:
YOLO = import_yolo()
self.model = YOLO(str(weights))
self.device = str(device)
def detect(self, image_bgr: np.ndarray, conf: float = 0.25, iou: float = 0.7, imgsz: int = 640) -> list[DetBox]:
results = self.model.predict(
source=image_bgr,
imgsz=imgsz,
conf=conf,
iou=iou,
device=self.device,
verbose=False,
max_det=50,
)
if not results:
return []
boxes = results[0].boxes
if boxes is None or len(boxes) == 0:
return []
out: list[DetBox] = []
xyxy = boxes.xyxy.cpu().numpy()
confs = boxes.conf.cpu().numpy()
clss = boxes.cls.cpu().numpy() if boxes.cls is not None else np.zeros(len(boxes))
for i in range(len(boxes)):
x1, y1, x2, y2 = xyxy[i]
out.append(
DetBox(
xyxy=(int(round(x1)), int(round(y1)), int(round(x2)), int(round(y2))),
conf=float(confs[i]),
cls_id=int(clss[i]),
)
)
out.sort(key=lambda b: b.conf, reverse=True)
return out
def plot(self, image_bgr: np.ndarray, conf: float = 0.25, iou: float = 0.7, imgsz: int = 640) -> np.ndarray:
results = self.model.predict(
source=image_bgr,
imgsz=imgsz,
conf=conf,
iou=iou,
device=self.device,
verbose=False,
)
if not results:
return image_bgr.copy()
return results[0].plot()
class FasterRCNNDetector:
backend = "faster_rcnn"
def __init__(
self,
weights: str | Path | None = None,
device: str | int = "0",
num_classes: int = 2,
score_thresh: float = 0.25,
) -> None:
self.device = resolve_device(device)
self.score_thresh = float(score_thresh)
self.num_classes = int(num_classes)
self.model = build_faster_rcnn(self.num_classes, pretrained_backbone=weights is None)
if weights is not None:
ckpt = torch.load(str(weights), map_location="cpu", weights_only=False)
state = ckpt["model"] if isinstance(ckpt, dict) and "model" in ckpt else ckpt
self.model.load_state_dict(state)
self.model.to(self.device)
self.model.eval()
@torch.inference_mode()
def detect(self, image_bgr: np.ndarray, conf: float | None = None, **_: Any) -> list[DetBox]:
thr = self.score_thresh if conf is None else float(conf)
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
tensor = torch.from_numpy(rgb).permute(2, 0, 1).float() / 255.0
tensor = tensor.to(self.device)
out = self.model([tensor])[0]
boxes = out["boxes"].detach().cpu().numpy()
scores = out["scores"].detach().cpu().numpy()
labels = out["labels"].detach().cpu().numpy()
dets: list[DetBox] = []
for box, score, label in zip(boxes, scores, labels):
if float(score) < thr:
continue
# Faster R-CNN: 0=背景,前景从 1 开始;对外统一成 0-based 类别
cls_id = int(label) - 1 if int(label) > 0 else 0
x1, y1, x2, y2 = box
dets.append(
DetBox(
xyxy=(int(round(x1)), int(round(y1)), int(round(x2)), int(round(y2))),
conf=float(score),
cls_id=cls_id,
)
)
dets.sort(key=lambda b: b.conf, reverse=True)
return dets
def plot(self, image_bgr: np.ndarray, conf: float | None = None, **kwargs: Any) -> np.ndarray:
dets = self.detect(image_bgr, conf=conf, **kwargs)
vis = image_bgr.copy()
for det in dets:
x1, y1, x2, y2 = det.xyxy
cv2.rectangle(vis, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(
vis,
f"id{det.cls_id} {det.conf:.2f}",
(x1, max(20, y1 - 6)),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 0),
2,
)
return vis
def guess_backend(weights: str | Path) -> str:
path = Path(weights)
name = path.name.lower()
suffix = path.suffix.lower()
if "faster" in name or "rcnn" in name or "frcnn" in name:
return "faster_rcnn"
if suffix == ".pt":
# Ultralytics YOLO 常用 .pttorchvision 训练脚本默认 .pth
return "yolo"
if suffix in {".pth", ".pkl"}:
return "faster_rcnn"
return "yolo"
def load_detector(
backend: str,
weights: str | Path,
device: str | int = "0",
num_classes: int = 2,
conf: float = 0.25,
):
"""
backend: yolo | faster_rcnn | auto
Faster R-CNN 的 num_classes = 前景类数 + 1(背景)
"""
kind = backend.strip().lower()
if kind == "auto":
kind = guess_backend(weights)
if kind in {"yolo", "ultralytics"}:
return YOLODetector(weights, device=device)
if kind in {"faster_rcnn", "rcnn", "faster-rcnn", "frcnn"}:
return FasterRCNNDetector(weights, device=device, num_classes=num_classes, score_thresh=conf)
raise ValueError(f"不支持的检测后端: {backend}")
def best_box(dets: list[DetBox], conf_thr: float = 0.25) -> DetBox | None:
for det in dets:
if det.conf >= conf_thr:
return det
return None