无人考评利用旋转目标检测识别验电器

前言

在千人千面无人考评任务中,遇到一个验电器考核,其任务是识别验电器并判断其有没有拉伸开,此时博主的第一个念头便是通过目标检测的方式识别验电器,随后计算检测框的长度来进行识别,然而,在使用目标检测的方式时,发现检测效果极差。。。。

目标检测效果拉跨

标注效果如下:

检测效果如下:

训练结果分析表明,模型的收敛过程极不稳定。究其根源,在于验电器本身属于细长型目标,当其处于倾斜状态时,常规的矩形标注框会不可避免地引入大量冗余的背景噪声。这种无效信息的混入,严重干扰了模型的特征提取,进而制约了拟合效果。

旋转目标检测

遇到这个问题后,博主果断放弃了目标检测的方式,转而采用旋转目标检测(obb)的方式,效果较为满意

如下图所示,旋转目标检测方式能够较好的识别到验电器

从训练日志来看,模型的拟合效果很好。

模型部署

由于博主需要将模型深度集成到软件中,并最终打包为独立的 EXE 可执行文件进行分发,原生的 PyTorch(.pt)格式显然无法满足这一工程化落地的需求。具体原因如下:

对比维度 原生 PyTorch (.pt) ONNX Runtime (.onnx)
环境依赖与体积 极其臃肿。打包需携带完整的 PyTorch 库及各类依赖,导致 EXE 体积庞大(动辄数 GB),分发困难。 轻量级 。仅需携带 onnxruntime 库,依赖极少,能极大压缩 EXE 体积,便于分发和部署。
部署兼容性 较差。极易受目标机器 Python 版本、CUDA 驱动及 C++ 运行库的影响,常出现"在我电脑上能跑,在别人那报错"的情况。 极佳。作为通用的中间表示格式,跨平台、跨环境兼容性极强,在各类 Windows 环境下表现极其稳定。
推理性能上限 受限。默认使用 PyTorch 原生后端,虽然支持 GPU,但难以直接调用 TensorRT 等硬件专属的深度优化引擎。 极高。完美支持 TensorRT、OpenVINO 等高性能推理后端,能充分榨干显卡(如 RTX 3060)的算力。
显存资源管理 较粗糙。在多线程并发推理时,显存管理机制相对僵化,容易引发显存溢出(OOM)导致程序崩溃。 精细化。具备先进的显存复用与优化机制,在多线程高并发场景下显存占用更低、运行更稳健。

因为我们首先需要对当前的pt文件进行格式转换,导出格式为onnx

python 复制代码
from ultralytics import YOLO
# 加载模型,路径为 YOLO 模型的 .pt 文件
model = YOLO(r"\runs\obb\train3\weights\best.pt")
# 导出模型,设置多种参数
model.export(
    imgsz=640,
    format="onnx",
    simplify=True, dynamic=True, half=False,
    opset=18,
    nms=False,
)

随后我们需要编写onnx旋转检测的代码:

模型初始化

python 复制代码
class YOLORotatedDetector:
    def __init__(self, model_path, input_size=(640, 640), conf_threshold=0.4, iou_threshold=0.1,num_class=7):
        """
        初始化检测器
        :param model_path: TFLite 模型路径
        :param input_size: 输入图像尺寸 (height, width)
        :param conf_threshold: 置信度阈值
        :param iou_threshold: IOU 阈值
        """
        self.model_path = model_path
        self.input_size = input_size
        self.num_class=num_class
        self.conf_threshold = conf_threshold
        self.iou_threshold = iou_threshold
        self.initialize_model(self.model_path)

    def initialize_model(self, path):
        cuda_options = {
            'device_id': 0,
            'gpu_mem_limit': int(1 * 1024 * 1024 * 1024),  # 2GB 上限
            'arena_extend_strategy': 'kSameAsRequested',   # 严格按需分配
            'cudnn_conv_algo_search': 'DEFAULT'            # 【关键新增】禁用穷举搜索,使用默认算法
        }
        providers = [('CUDAExecutionProvider',cuda_options), 'CPUExecutionProvider']
        self.session = ort.InferenceSession(path, providers=providers )
        # 获取输入和输出信息
        self.get_input_details()
        self.get_output_details()
        self.pad_w=0
        self.pad_h=0
        self.ratio=0
        self.ratio_w=0
        self.ratio_h=0
        self.orig_img_height=640
        self.orig_img_width=640

    def get_input_details(self):
        model_inputs = self.session.get_inputs()
        self.input_names = [model_inputs[i].name for i in range(len(model_inputs))]

        self.input_shape = model_inputs[0].shape
        self.input_height = 640 #self.input_shape[2]
        self.input_width = 640 #self.input_shape[3]

    # 获取输出信息
    def get_output_details(self):
        model_outputs = self.session.get_outputs()
        self.output_names = [model_outputs[i].name for i in range(len(model_outputs))]
复制代码
def preprocess_image_from_array(self, image):
    img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    img_letter, self.ratio, (dw, dh) = letterbox(img_rgb, (self.input_height, self.input_width))
    # 将宽高方向的填充和缩放分别保存,避免后续广播报错
    self.pad_w = dw
    self.pad_h = dh
    self.ratio_w = self.ratio[0] if isinstance(self.ratio, tuple) else self.ratio
    self.ratio_h = self.ratio[1] if isinstance(self.ratio, tuple) else self.ratio
    img_norm = img_letter.astype(np.float32) / 255.0
    input_tensor = np.expand_dims(img_norm.transpose(2, 0, 1), axis=0)
    return input_tensor

def calculate_obb_corners(self, params):
    """
    根据旋转角度计算旋转边界框的四个角点。
    :param params: [x_center, y_center, width, height, angle]
    :return: 四个角点坐标 (4, 2) numpy数组,浮点类型
    """
    x_center, y_center, w, h, angle = params
    cos_a = np.cos(angle)
    sin_a = np.sin(angle)
    dx = w / 2
    dy = h / 2

    corners = [
        [x_center + dx * cos_a - dy * sin_a, y_center + dx * sin_a + dy * cos_a],
        [x_center - dx * cos_a - dy * sin_a, y_center - dx * sin_a + dy * cos_a],
        [x_center - dx * cos_a + dy * sin_a, y_center - dx * sin_a - dy * cos_a],
        [x_center + dx * cos_a + dy * sin_a, y_center + dx * sin_a - dy * cos_a],
    ]
    return np.array(corners, dtype=np.float32)

def xywha2poly(self, boxes):
    """
    将多个 [x_center, y_center, width, height, angle] 转为多边形顶点
    :param boxes: shape=(N, 5) 的数组,每一行为 [x, y, w, h, angle]
    :return: polys: shape=(N, 4, 2)
    """
    polys = []
    for box in boxes:
        poly = self.calculate_obb_corners(box)
        polys.append(poly)
    return np.array(polys, dtype=np.float32)

def rotated_nms(self, boxes, scores, class_ids):
    """多类别旋转框非极大值抑制 (Rotated NMS)"""
    indices = []
    unique_class_ids = set(class_ids)
    for cls_id in unique_class_ids:
        cls_mask = class_ids == cls_id
        cls_boxes = boxes[cls_mask]
        cls_scores = scores[cls_mask]

        keep = []
        order = np.argsort(cls_scores)[::-1]  # 按置信度降序排序
        while order.size > 0:
            i = order[0]
            keep.append(i)
            ious = self.rotated_iou(cls_boxes[i], cls_boxes[order[1:]])
            inds = np.where(ious <= self.iou_threshold)[0]
            order = order[inds + 1]
        indices.extend(np.where(cls_mask)[0][keep])
    return indices

def rotated_iou(self, box1, boxes):
    """计算旋转框之间的 IOU"""
    from shapely.geometry import Polygon

    def box_to_polygon(box):
        rect = ((box[0], box[1]), (box[2], box[3]), box[4])
        points = cv2.boxPoints(rect)
        return Polygon(points)

    poly1 = box_to_polygon(box1)
    polys = [box_to_polygon(box) for box in boxes]
    ious = []
    for poly in polys:
        intersection = poly1.intersection(poly).area
        union = poly1.union(poly).area
        ious.append(intersection / union if union > 0 else 0)
    return np.array(ious)


def postprocess_output(self, outputs):
    """后处理:解析输出并应用 NMS 及角度合并"""
    predictions = np.transpose(outputs[0].squeeze())  # 去掉 batch 维度
    boxes_xywh = predictions[:, :4]               # [x, y, w, h]
    boxes_angle = predictions[:,6:self.num_class]             # [angle]
    boxes = np.hstack((boxes_xywh, boxes_angle))  # 拼接成 [x, y, w, h, angle]

    scores = np.max(predictions[:, 4:self.num_class-1], axis=1)
    class_ids = np.argmax(predictions[:, 4:self.num_class-1], axis=1)
    # 1. 过滤掉低于置信度阈值的预测
    mask = scores > self.conf_threshold
    boxes = boxes[mask]
    scores = scores[mask]
    class_ids = class_ids[mask]

    if len(boxes) == 0:
        return [], [], []

    # 2. 先进行标准的旋转框 NMS,剔除高度重叠的冗余框
    indices = self.rotated_nms(boxes, scores, class_ids)
    nms_boxes = boxes[indices]
    nms_scores = scores[indices]
    nms_class_ids = class_ids[indices]

    # 3. 再调用基于角度分组的拟合算法(将 NMS 后的干净数据传入)
    final_boxes, final_scores, final_class_ids = self.group_and_fit_by_angle(
        nms_boxes, nms_scores, nms_class_ids,
        angle_threshold=5  # 这里保持你设定的 5 度阈值
    )
    if len(final_boxes) == 0:
        return [], [], []
    # 4. 将最终确定的 OBB 框转换为多边形顶点
    polys = self.xywha2poly(final_boxes)
    # 5. 坐标反解与边界校验 (LetterBox 反向映射)
    polys[..., 0] -= self.pad_w  # x 坐标减去宽度方向的黑边
    polys[..., 1] -= self.pad_h  # y 坐标减去高度方向的黑边
    polys[..., 0] /= self.ratio_w
    polys[..., 1] /= self.ratio_h
    # 限制坐标在原始图像范围内,防止越界
    polys[..., 0] = np.clip(polys[..., 0], 0, self.orig_img_width)
    polys[..., 1] = np.clip(polys[..., 1], 0, self.orig_img_height)

    return polys, final_scores, final_class_ids

def draw_detections(self, image, polys, scores, class_ids):
    """在图像上绘制旋转框检测结果"""
    for i in range(len(polys)):
        poly = polys[i].astype(np.int32)  # 转换为整数类型
        label = f"Class {class_ids[i]}: {scores[i]:.2f}"
        color = (0, 255, 0)  # 绿色框

        # 绘制旋转框
        cv2.polylines(image, [poly], isClosed=True, color=color, thickness=2)
        # 绘制标签
        label_pos = tuple(poly[0])  # 标签位置在第一个顶点附近
        cv2.putText(image, label, (label_pos[0], label_pos[1] - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

    return image

def run_inference(self, input_tensor):
    result = self.session.run(self.output_names, {self.input_names[0]: input_tensor})
    return result[0]

def detect(self, image):
    """
    执行完整检测流程,接受 OpenCV 图像作为输入
    :param image: OpenCV 图像 (np.ndarray)
    :return: (polys, scores, class_ids, estimated_distance, movement_dict) 或 None
    """
    if image is None:
        print("No image provided.")
        return None
    # 预处理图像
    self.orig_img_height, self.orig_img_width = image.shape[:2]
    input_tensor = self.preprocess_image_from_array(image)
    # 运行推理
    outputs = self.run_inference(input_tensor)
    # 后处理:解析输出
    polys, scores, class_ids = self.postprocess_output(outputs)
    return polys, scores, class_ids

def calculate_iou(self, box1, box2):
    """
    使用 OpenCV 的 rotatedRectangleIntersection 计算两个旋转框的 IOU
    :param box1: 多边形顶点数组 (4, 2)
    :param box2: 多边形顶点数组 (4, 2)
    :return: IOU 值
    """
    rect1 = cv2.minAreaRect(np.float32(box1))
    rect2 = cv2.minAreaRect(np.float32(box2))

    intersection, inter_area = cv2.rotatedRectangleIntersection(rect1, rect2)

    if intersection == cv2.INTERSECT_NONE:
        return 0.0
    # 获取两个矩形的面积
    area1 = cv2.contourArea(cv2.boxPoints(rect1))
    area2 = cv2.contourArea(cv2.boxPoints(rect2))
    # 计算并集
    union_area = area1 + area2 - inter_area
    iou = inter_area / union_area
    return iou
复制代码
相关推荐
Mr_sst2 小时前
AI 大模型应用开发实习|如何找岗 + 面试真题 + 面经总结
java·人工智能·ai·面试·职场和发展
z小猫不吃鱼2 小时前
12 Prompt Engineering 入门:提示词为什么会影响模型行为?
人工智能·gpt·自然语言处理·prompt
前端不太难2 小时前
内存带宽、超长上下文与解码效率:AI推理的三大核心制约
人工智能·状态模式
风落无尘2 小时前
《智能重生:从垃圾堆到AI工程师》——第十二章 融合与共生
人工智能
GISer_Jing2 小时前
Claude Code 技术深度解析:一个活在终端里的 AI 编程助手
人工智能·elasticsearch·前端框架
AI人工智能+2 小时前
施工许可证智能识别系统通过融合计算机视觉与自然语言处理技术,实现了建筑行业关键证件的自动化信息提取
人工智能·深度学习·计算机视觉·ocr·施工许可证识别
weelinking2 小时前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架
春日见2 小时前
5分钟入门强化学习之蒙特卡洛(MC)算法与实现
运维·服务器·人工智能·深度学习·算法·机器学习
2601_958352902 小时前
AP-0316语音处理模组:适配音频设备的技术优势分析
人工智能·语音识别·硬件开发·回音消除·音频处理模块