简介本资源面向计算机、人工智能、自动化等专业学生与开发者提供基于YOLOv5在WoodScape数据集上实现旋转框目标检测与语义分割的完整项目源码适合课程设计、毕业设计、项目立项演示及进阶学习。压缩包共76个文件约6.14MB以46个Python脚本为核心涵盖训练、验证、检测与数据处理流程另含8个YAML模型与数据配置、5个JSON与5张JPG、4张PNG可视化结果、3份Markdown说明及Shell、Dockerfile等辅助文件目录结构清晰便于按模块阅读与二次开发。项目已通过运行测试配套文档说明与远程答疑支持读者可据此掌握旋转框检测与分割分支的联合训练思路、损失权重配置及数据转换脚本并在此基础上修改以适配其他任务。目前已有126人学习关注。1. 从 WoodScape 到旋转框为什么这套组合值得你花一个周末跑通如果你手头只有 COCO 那套水平框标注第一次看到 WoodScape 的鱼眼图像和带角度的旋转框标注大概率会愣一下同一辆车水平框里塞满了背景旋转框却能贴着车身走。这就是旋转框目标检测要解决的问题——在鱼眼、航拍、遥感、工业质检这类目标朝向随意的场景里水平框的 IoU 天然吃亏NMS 之后还容易把相邻目标误删。WoodScape 是面向车载鱼眼环视的数据集四个相机覆盖车身一周标注里既有目标检测框也有语义分割掩码天然适合做「检测 分割」多任务。用 YOLOv5 做旋转框再挂一个分割头是很多人从水平框迈向旋转框的第一站。这套方案适合谁适合已经跑通过 YOLOv5 水平框训练、想扩展到旋转框和语义分割的工程师也适合做自动驾驶感知、遥感解译、工业缺陷检测的从业者。下面我按「数据怎么转、模型怎么改、训练怎么调、坑在哪」的顺序把这条路径讲清楚。2. WoodScape 数据解析与旋转框标注转换从原始标注到 YOLOv5 可读格式2.1 WoodScape 的目录结构与标注字段WoodScape 的原始数据一般按相机分组每个相机目录下有图像和对应的标注文件。标注通常是 JSON 或 XML里面包含目标类别、多边形顶点或旋转框参数。旋转框的表示方式常见有两种一种是(cx, cy, w, h, angle)另一种是多边形顶点。YOLOv5 本身只认水平框的(class, x_center, y_center, w, h)归一化格式所以要做旋转框必须先把标注转成带角度的格式再改模型输出和损失函数。我一般先写一个脚本把 WoodScape 的标注统一读成(cx, cy, w, h, angle)角度范围统一到[-90, 0)或[-45, 45)具体取决于你后面用的旋转框表示法。这里有个容易翻车的点不同标注文件里角度的定义方向可能不一致有的以 x 轴正方向为 0 逆时针为正有的以 y 轴为 0。转换前一定要拿几张图可视化确认否则训练时 loss 会震荡得让你怀疑人生。import json import math import cv2 import numpy as np def poly_to_rbox(points): 将多边形顶点转为 (cx, cy, w, h, angle) 旋转框 points np.array(points, dtypenp.float32) rect cv2.minAreaRect(points) # 返回 ((cx,cy),(w,h),angle) (cx, cy), (w, h), angle rect # OpenCV 的 angle 在 [0,90)统一到 [-45,45) if w h: w, h h, w angle 90 angle angle % 180 if angle 90: angle - 180 return cx, cy, w, h, angle def convert_woodscape(json_path, out_txt, img_w, img_h, class_map): with open(json_path, r) as f: data json.load(f) lines [] for obj in data.get(annotations, []): cls_name obj[category] if cls_name not in class_map: continue cls_id class_map[cls_name] cx, cy, w, h, angle poly_to_rbox(obj[polygon]) # 归一化 cx / img_w cy / img_h w / img_w h / img_h angle angle / 180.0 * math.pi # 转弧度按需 lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f} {angle:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines))这段代码的关键在poly_to_rbox用cv2.minAreaRect求最小外接旋转矩形再把宽高和角度统一到一致的定义。class_map是你自己定义的类别到 id 的映射WoodScape 里常见的车、人、自行车等类别要提前对齐。归一化时注意angle是否要转弧度这取决于你后面损失函数用的是弧度还是角度。转换完一定要抽几张图用cv2.boxPoints画出来看确认框贴合目标。2.2 语义分割掩码的生成与对齐WoodScape 的分割标注通常是逐像素的类别图或者多边形形式的区域。如果原始标注是多边形需要用cv2.fillPoly生成掩码图如果已经是掩码图直接按类别映射成 0 到 N-1 的灰度图即可。分割掩码必须和检测图像严格对齐包括尺寸和裁剪方式。我一般把掩码保存成 PNG像素值就是类别 id训练时用PIL或cv2读取不做归一化只做 resize 和 padding。import cv2 import numpy as np def build_seg_mask(json_path, img_w, img_h, class_map): mask np.zeros((img_h, img_w), dtypenp.uint8) with open(json_path, r) as f: data json.load(f) for obj in data.get(annotations, []): cls_name obj[category] if cls_name not in class_map: continue cls_id class_map[cls_name] poly np.array(obj[polygon], dtypenp.int32) cv2.fillPoly(mask, [poly], cls_id) return mask这里class_map要和检测任务共用同一套类别定义否则多任务训练时类别 id 会打架。掩码保存后建议写一个Dataset类同时返回图像、旋转框标注和分割掩码保证三者经过同样的 resize 和增强。常见做法是把旋转框和掩码都做随机翻转、缩放但旋转框在翻转后角度要重新计算掩码直接翻转即可。这一步不做对齐后面训练时分割头学到的就是错位的特征。3. YOLOv5 旋转框改造检测头、损失函数与分割分支的接入3.1 旋转框检测头的输出维度与角度编码YOLOv5 原版检测头输出(x, y, w, h, obj, cls)要做旋转框最直接的方式是在回归分支多加一个角度维度变成(x, y, w, h, angle, obj, cls)。角度编码有两种常见做法直接回归角度值或者用(sin, cos)双通道编码避免角度周期性带来的不连续。我一般用(sin, cos)因为直接回归角度在 90 度附近容易跳变loss 会突然变大。# 在 YOLOv5 的 Detect 头中修改回归输出通道 # 原版: self.no nc 5 # 旋转框: self.no nc 6 (多一个 angle) # 如果用 sin/cos 编码: self.no nc 7 class DetectRotated(nn.Module): def __init__(self, nc80, anchors(), ch()): super().__init__() self.nc nc self.no nc 6 # x, y, w, h, angle, obj self.nl len(anchors) self.m nn.ModuleList( nn.Conv2d(x, self.no * 3, 1) for x in ch )改完输出维度后损失函数也要跟着改。旋转框的 IoU 计算不能用普通矩形 IoU要用旋转框 IoU常见实现有cv2.rotatedRectangleIntersection或者用shapely。训练时我一般用ProbIoU或KLD作为回归损失比直接算旋转 IoU 更稳定。分类和 obj 损失保持原版 BCE 即可。这里有个血泪经验角度回归的权重不要设太大否则前期 loss 会被角度项主导模型学不到有效的框。3.2 分割分支的挂载位置与特征融合分割分支一般挂在 YOLOv5 的 Neck 后面取 P3、P4、P5 三层特征做上采样融合最后输出和输入同分辨率的掩码。常见做法是加一个轻量的SegHead用nn.Conv2d逐层上采样最后用1x1卷积输出类别数通道。class SegHead(nn.Module): def __init__(self, in_channels, num_classes): super().__init__() self.lateral nn.ModuleList( nn.Conv2d(c, 128, 1) for c in in_channels ) self.smooth nn.Conv2d(128, 128, 3, padding1) self.out nn.Conv2d(128, num_classes, 1) def forward(self, features): # features: [P3, P4, P5] p3, p4, p5 features p5_up F.interpolate(self.lateral[2](p5), sizep4.shape[-2:], modenearest) p4 self.lateral[1](p4) p5_up p4_up F.interpolate(p4, sizep3.shape[-2:], modenearest) p3 self.lateral[0](p3) p4_up x F.relu(self.smooth(p3)) return self.out(x)挂载位置决定了分割分支能拿到多少语义信息。P5 感受野大但分辨率低P3 分辨率高但语义弱三层融合是折中。训练时分割损失用交叉熵即可如果类别不均衡严重可以加weight或换Focal Loss。检测和分割的 loss 要加权求和权重比一般从1:1开始调分割 loss 太大容易让检测分支欠拟合。3.3 多任务训练的 loss 权重与学习率策略多任务训练最怕两个任务互相拖累。我一般先用检测任务单独训几个 epoch让检测头先收敛再打开分割分支一起训。学习率用余弦退火初始lr设0.01warmup 3 个 epoch。检测 loss 权重设1.0分割 loss 权重从0.5开始观察验证集上两个任务的指标如果分割 mIoU 涨得太慢就加到1.0如果检测 mAP 掉得厉害就降到0.3。python train.py \ --data woodscape_rotated.yaml \ --cfg models/yolov5s_rotated_seg.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 8 \ --img 640 \ --hyp data/hyp.rotated.yaml \ --seg-weight 0.5--seg-weight是我自己加的参数原版没有。hyp.rotated.yaml里要调box、cls、obj的增益旋转框的box增益一般比水平框小因为角度项本身波动大。batch size 根据显存调鱼眼图像分辨率高的话640可能都吃紧可以降到512或416。4. 训练、验证与部署从指标异常到可视化排查4.1 训练指标解读mAP、mIoU 与角度误差旋转框检测的 mAP 计算和水平框不同需要用旋转 IoU 作为匹配依据。验证时我一般同时看三个指标mAP0.5、分割mIoU、角度平均误差。如果mAP正常但角度误差大说明框的位置对了但朝向不对这时候要检查角度编码和损失权重。如果mIoU一直上不去先看分割掩码是否对齐再看分割分支的学习率是不是太小。# 验证时计算角度误差 def angle_error(pred_angle, gt_angle): diff np.abs(pred_angle - gt_angle) diff np.minimum(diff, np.pi - diff) # 角度周期性 return np.mean(diff) * 180 / np.pi这个函数假设角度是弧度且范围在[0, pi)如果你的角度定义不同要先统一。角度误差超过 10 度基本可以认为旋转框没学好。4.2 可视化排查把预测框和掩码叠回原图训练过程中一定要定期把预测结果画回原图检测框用cv2.boxPoints画旋转矩形掩码用半透明颜色叠加。我一般每 10 个 epoch 抽 8 张验证图可视化重点看三类问题框的角度是否贴合目标、掩码边缘是否和检测框一致、有没有漏检或误检。如果掩码和检测框明显错位说明两个分支的特征没有共享好可以尝试在 Neck 后加一个共享的1x1卷积再分叉。def visualize(img, boxes, masks, class_names): for box in boxes: pts cv2.boxPoints(box) pts np.int0(pts) cv2.drawContours(img, [pts], 0, (0, 255, 0), 2) overlay img.copy() overlay[masks 0] (0, 0, 255) img cv2.addWeighted(overlay, 0.4, img, 0.6, 0) return img可视化是排查玄学问题最有效的手段很多 loss 异常看几张图就能定位。4.3 导出与推理ONNX 导出时的旋转框后处理训练完导出 ONNX 时旋转框的后处理要自己写。YOLOv5 原版的non_max_suppression只处理水平框旋转框要用旋转 NMS。我一般把角度解码和旋转 NMS 都放在 Python 后处理里ONNX 只负责输出原始预测。def rotated_nms(boxes, scores, iou_threshold0.5): # boxes: (N, 5) cx, cy, w, h, angle # 用 cv2.rotatedRectangleIntersection 计算 IoU keep [] order scores.argsort()[::-1] while order.size 0: i order[0] keep.append(i) ious [] for j in order[1:]: inter, _ cv2.rotatedRectangleIntersection( ((boxes[i][0], boxes[i][1]), (boxes[i][2], boxes[i][3]), boxes[i][4]), ((boxes[j][0], boxes[j][1]), (boxes[j][2], boxes[j][3]), boxes[j][4]) ) # 计算 IoU省略面积计算细节 ious.append(inter_area / (area_i area_j - inter_area)) order order[1:][np.array(ious) iou_threshold] return keep导出 ONNX 时注意opset版本旋转框相关的算子如果 ONNX 不支持就留在 Python 里做。部署到边缘设备时旋转 NMS 的计算量比水平 NMS 大可以考虑用TensorRT插件或者简化角度表示。5. 避坑与常见问题旋转框和分割任务里最容易翻车的 5 个点5.1 角度定义不一致导致 loss 震荡现象训练前几个 epoch loss 正常下降突然跳到很大的值然后一直震荡。原因WoodScape 原始标注的角度定义和你在损失函数里用的定义不一致比如一个以 x 轴为 0 逆时针一个以 y 轴为 0 顺时针。解决转换脚本里统一角度定义并在可视化里确认。我一般会在转换后随机抽 20 张图用cv2.boxPoints画出来人工检查角度方向。5.2 旋转框 IoU 计算慢导致训练卡顿现象每个 batch 的训练时间比水平框慢好几倍GPU 利用率低。原因旋转框 IoU 用 Python 循环逐个计算没有向量化。解决用shapely的STRtree或者自己写向量化的多边形交集计算也可以换用ProbIoU这种可导且计算快的近似。如果实在慢先把角度回归关掉只训水平框确认流程通了再开旋转。5.3 分割掩码和检测框类别 id 不统一现象分割 mIoU 一直很低可视化发现掩码类别和检测框类别对不上。原因检测和分割用了两套class_map或者掩码生成时类别 id 从 1 开始检测从 0 开始。解决全局统一一个class_map掩码生成和检测标注转换都从这个 map 取 id。训练前写个断言检查最大类别 id 是否小于num_classes。5.4 多任务训练时检测分支欠拟合现象分割 mIoU 涨得不错但检测 mAP 比单任务时掉了很多。原因分割 loss 权重太大梯度把检测分支带偏了。解决降低分割 loss 权重或者先冻结分割分支训检测再解冻一起训。我一般先用seg-weight 0.1跑 10 个 epoch看检测 mAP 恢复后再加到0.5。5.5 鱼眼图像边缘畸变导致框和掩码错位现象图像中心区域检测和分割都正常边缘区域框的角度和掩码边缘对不上。原因鱼眼畸变没有校正或者校正后标注没有同步变换。解决如果 WoodScape 提供校正后的图像和标注直接用校正版如果没有要么自己做畸变校正并同步变换标注要么在数据增强里加随机裁剪让模型多学边缘区域。我一般优先用校正版省去同步变换的麻烦。6. 进阶技巧用旋转框的几何一致性反过来提升分割质量跑通基础版本后我习惯做一件事把旋转框的几何信息反过来约束分割。具体做法是在分割 loss 里加一项让分割掩码的外接旋转框和检测头预测的旋转框尽量一致。这项约束不需要额外标注直接用检测分支的输出即可。实现上对每个预测框取其对应的分割掩码区域计算掩码的最小外接旋转矩形然后和检测框算旋转 IoU把1 - IoU作为一致性 loss 加进去。def consistency_loss(seg_mask, det_boxes, num_classes): # seg_mask: (B, C, H, W) softmax 后的概率 # det_boxes: (B, N, 5) cx, cy, w, h, angle loss 0.0 for b in range(seg_mask.shape[0]): for n in range(det_boxes.shape[1]): cls_id det_boxes[b, n, -1].long() mask_bin (seg_mask[b, cls_id] 0.5).float() if mask_bin.sum() 10: continue # 取掩码的最小外接旋转矩形 points torch.nonzero(mask_bin).flip(-1).float() rect cv2.minAreaRect(points.cpu().numpy()) # 和 det_boxes[b, n] 算旋转 IoU省略细节 iou rotated_iou(rect, det_boxes[b, n]) loss (1 - iou) return loss / (seg_mask.shape[0] * det_boxes.shape[1])这项 loss 的权重不要太大0.1左右即可太大反而会让分割掩码过度依赖检测框丢失细节。我一般在训练后期最后 20 个 epoch才打开前期先让两个分支各自学好。验证时看两个指标分割 mIoU 是否提升检测 mAP 是否保持。如果 mIoU 涨了但 mAP 掉了说明约束太强调小权重。另一个技巧是测试时增强TTA用旋转框的多尺度翻转。水平框的 TTA 直接翻转坐标即可旋转框翻转后角度要重新计算。我一般做水平翻转和垂直翻转两种角度分别取pi - angle和-angle然后对预测结果做旋转 NMS。TTA 能涨 1 到 2 个点但推理时间翻倍看场景取舍。最后说个我自己的习惯每次改完模型结构或损失函数先拿 100 张图过拟合一遍。如果 100 张图都过拟合不了说明代码有 bug不用浪费时间去跑全量。这个习惯帮我省了无数个通宵。希望帮到你。本文还有配套的精品资源点击获取
