1. 为什么非要从零手搓一个旋转目标检测网络
1.1 普通目标检测在哪些场景下首先失灵
先聊一个我实际遇到的质检项目。当时是检测产线上密集摆放的金属零件,零件长宽比大约4比1,姿态任意。最开始图省事,直接套用YOLOv8的常规检测头。推理结果一出来,那个画面基本没法看:一个零件往往被三四个水平框同时框住,相邻两个零件稍微近一点,两个水平框的IoU直接超过0.7,NMS一压,漏检一大片。更尴尬的是,即便框对了,下游的机械臂抓取也不知道该以什么角度去吸,因为水平框根本给不出姿态信息。
这不是YOLO的问题,而是水平检测框这个数学表示本身就不适合这类场景。普通目标检测本质上是在预测一个与坐标轴对齐的矩形,它只有中心点、宽、高三个自由度。当目标本身带有明显的方向性,尤其是长条形、倾斜放置的目标,水平框会把大量背景区域包进来。背景一多,定位精度下降,两个目标重叠区域的IoU虚高,后面跟的NMS就会开始误杀。遥感图像里的船舶、飞机、油罐,工业场景里的零件、药瓶、电路板,文档场景里的表格、印章,通通踩这个坑。
旋转目标检测就是把水平框升级成带角度信息的旋转框,让框的每条边都贴住目标主体的边缘。核心多出来的那一个自由度,也就是旋转角θ,恰恰是工业项目里最关键的信息。很多看似是“分类难”的问题,本质上是“表示不对”的问题,换一种框表示,整个任务难度会降一个量级。
1.2 手搓网络的价值:工业落地从来不是调包
有人会问:既然已经有MMRotate、Rotated-YOLO这些现成工具箱,直接拿过来训练不就行了,为什么要“从零手搓”?
我的观点很明确:现成框架能帮你快速出demo,但帮不了你上线。工业项目里数据是你自己的,场景是你自己的,标注格式是乱的,模型结构需要裁剪,后处理需要定制,算子需要导出到onnx甚至TensorRT。这些环节只要有一处需要改源码,而你只会在config里改参数,项目就会卡死在“demo能跑”和“线上能跑”之间。
更重要的是,旋转目标检测的核心难点,比如角度回归的边界问题、旋转框的表示方式、旋转NMS的实现,全都藏在数学原理和工程细节里,也就是大多数框架封装好的那一层。如果不懂底层,遇到loss爆炸、mAP始终上不去这类问题时,你连排查方向都没有。所以我这个系列的核心思路就是:从数学定义开始,不借助任何封装库,用PyTorch这样的基础工具,一行一行把一个工业级可用的旋转目标检测网络“搓”出来。
1.3 卷1「启蒙篇」到底要解决什么问题
这第一篇定位为“启蒙”,不追求一上来就复现Oriented R-CNN或者S2ANet,而是先把旋转目标检测的底座打牢。底座是什么?就三件事:
- 旋转框的几种数学表示,以及它们在代码里如何互相转换;
- 旋转框可视化工具,先把数据看明白,这是后面所有训练的基础;
- 从数据集构造到最小网络闭环的最小可运行示例,让你亲手跑通一个能输出旋转框的模型。
这三件事做完,你对旋转目标检测的“手感”就建立起来了。接下来卷2再上FPN、anchor设计、角度回归的工业级处理,卷3再做多尺度与其他模型结构演进,都是顺水推舟的事。启蒙篇最重要的目标,就是让你面对任意一个旋转检测项目时,心里对“这个框怎么表示、这个角度怎么学”有一套自己的判断体系,而不是只能去抄config。
2. 旋转目标的数学表示:先别急着写网络
2.1 五参数表示法里藏着一个最容易踩的坑
旋转框最常用的表示方式是五参数:(x, y, w, h, θ),其中(x, y)是中心点坐标,w是长边,h是短边,θ是长边与水平坐标轴的夹角。看起来很简单,但这个θ的定义,不同框架、不同论文、不同标注工具之间都不一致,这是旋转检测领域最隐蔽的坑之一。
具体来说有三种常见定义:
- OpenCV系:θ范围是[-90°, 0°),表示水平轴顺时针旋转到矩形第一条边(即宽边)的角度,宽和高不做大小区分;
- 长边法(长边为w):θ范围是[-90°, 90°)或[0°, 180°),规定w是长边,h是短边,θ是长边与x轴的夹角;
- DOTA数据集格式:不使用θ,而是用有序四点(x1, y1, x2, y2, x3, y3, x4, y4)表示,要求点按顺时针排列,第一个点是“近似左上角”的顶点。
这三套表示各有各的使用场景。OpenCV的表示在图像处理层面很方便,因为cv2.boxPoints可以直接拿来画图;长边法在模型回归时更好用,因为θ对应的边是确定的;DOTA四点格式在评估和标注时更直观,但网络预测时一般会转换成五参数来回归。
我最开始在项目里犯过的错是:标注脚本输出的角度是DOTA四点法转出来的0°到180°,但训练框架内部用的是长边法[-90°, 90°),两个范围混用了两天,模型指标全是乱的。所以启蒙篇第一条教训:动手之前,把整个pipeline里所有环节的角度定义统一,统一到同一种表示,并且在代码里显式标注清楚。
2.2 长边定义、短边定义与四点坐标:工业项目应该怎么选
结合工业项目的实际经验,我建议按用途分层选表示:
- 标注阶段,用四点坐标存原始数据,为的是兼容所有标注工具,也方便人工确认;
- 训练阶段,内部统一使用长边法五参数,因为模型回归角度、宽高时,长边法的语义最稳定;
- 评估与可视化阶段,再把五参数转回四点坐标,以便计算多边形IoU和画图。
选择长边法的另一个原因是它友善对待宽高比极端的目标。试想一个宽高比5比1的长条目标,如果用短边作为θ的参考边,角度稍微抖一下,框的朝向就偏得离谱。而用长边作参考,角度回归的梯度方向更接近目标的真实姿态变化。
不过长边法也有它的代价,那就是θ超过范围边界时会发生“角度跳变”:长边旋转到接近90°时,表示同一个框可能会有两种写法,一种是θ≈89°、w是长边,另一种是θ≈-89°、w和h互换。这个边界问题直接毁了很多人第一次训练旋转检测的体验,后面第4节我们会展开讲怎么处理。
2.3 角度周期性:一个框为什么有无限种数学写法
角度是一个周期性量,这意味着一个物理上完全相同的旋转框,对应无数个数学表示。如果模型直接在原始角度值上做回归,就会遭遇到“明明框已经对了,loss却巨大”的诡异现象。
具体举例:假设长边法定义θ范围是[-90°, 90°),有一个真实框θ=-89°,模型回归计算出θ=89°。从数值上看,两者差了178°,损失函数会给出一个巨大的惩罚;但直观上看,这两个角度在物理上其实只差2°,对应的框几乎重合。问题就出在角度在边界处被“切断”了,原本连续的旋转关系被人为切成了两个很远的值。
解决这个问题有两条路。第一条是在损失函数层面把角度的周期性考虑进去,让0°和360°、-89°和91°在计算距离时保持很小;第二条是在表示层面把角度从“一个标量”扩展成“两个标量”,也就是下面的sin和cos编码。两条路不是互斥的,工业项目里我一般两条都上,效果最稳。这个细节先埋在这里,马上在第3节给出代码。
2.4 手搓一个旋转框可视化脚本:先能画出来再说
说再多理论,都不如先把旋转框画到图上亲眼看一看。这里给一个能直接用的Python工具函数,不依赖任何旋转检测库,只依赖OpenCV和NumPy。
import cv2 import numpy as np def rot_box_to_corners(cx, cy, w, h, angle_deg): """ 将长边法五参数(中心点cx,cy、长边w、短边h、长边与x轴夹角angle_deg)转为四个角点。 返回的四个角点按顺时针排列。 """ angle = np.deg2rad(angle_deg) # 以中心点为原点,先算出四个角点在局部坐标系下的坐标(短边为y方向) dx = np.array([ w/2, w/2, -w/2, -w/2]) dy = np.array([-h/2, h/2, h/2, -h/2]) # 旋转矩阵 cos_a, sin_a = np.cos(angle), np.sin(angle) x = cx + dx * cos_a - dy * sin_a y = cy + dx * sin_a + dy * cos_a return np.stack([x, y], axis=1).astype(np.float32) def draw_rotated_boxes(img, boxes, color=(0, 255, 0), thickness=2): """ boxes: N x 5 的数组或列表,每行是 [cx, cy, w, h, angle_deg] """ out = img.copy() for box in boxes: pts = rot_box_to_corners(*box) pts = pts.reshape((-1, 1, 2)).astype(np.int32) cv2.polylines(out, [pts], isClosed=True, color=color, thickness=thickness) return out # 示例:生成一张512x512的黑底图,画一个中心在(256,256)、长边150、短边50、角度30度的旋转框 img = np.zeros((512, 512, 3), dtype=np.uint8) boxes = np.array([[256, 256, 150, 50, 30.0]]) out = draw_rotated_boxes(img, boxes) cv2.imwrite("rotated_box_demo.png", out)这个脚本的核心是旋转矩阵。在局部坐标系里,旋转框的中心在原点,长边沿x轴、短边沿y轴分布,四个角点坐标非常规整;然后用一个标准的二维旋转矩阵把局部坐标变换到图像坐标。这里最需要注意的是图像的y轴方向是向下的,所以“顺时针”和“逆时针”的直觉可能会被反转。写代码时不要背公式,直接在图上多试几个角度,确认画出来的方向和自己的预期一致,再往后走。
我用这个脚本做的事情是:把标注数据全部画一遍,目测检查框与目标的贴合度,顺便统计所有目标的宽高比和角度分布。这一步在项目早期能帮你发现标注格式错误、角度定义不统一等问题,比训练完再去查要省钱得多。
3. 手搓第一个旋转目标检测器:最小闭环
3.1 从检测任务拆解到网络头设计
旋转目标检测和水平目标检测在任务拆解上几乎一致,都要回答“在哪里”和“是什么”两个问题。“在哪里”的答案从四维(cx, cy, w, h)变成五维(cx, cy, w, h, θ),“是什么”依然是类别概率。所以你完全无需从零设计一个特殊的网络骨架,用普通的CNN骨干提取特征,在head上多加一个角度回归分支即可。
这里我先不引入FPN和复杂的anchor匹配策略,因为启蒙篇最重要的是让整个闭环“转起来”。我用一个极简结构:输入是单张3通道图,骨干用一个4层的简单卷积网络,或者直接上torchvision里预训练的resnet18,取最后一层特征图,在上面接两个HEAD,一个是分类头,输出每个anchor的类别分数,另一个是回归头,输出相对于anchor的偏移量(dx, dy, dw, dh, dθ)。考虑到旋转框的特殊性,角度回归头我最终输出的是两个分支(sin和cos),而不是直接一个角度值,后面解释原因。
Anchor的设计暂时按最简单的来:每个空间位置预设一个水平anchor,宽高比1比1,不设多尺度。逻辑上anchor的旋转角初始为0°,让网络去回归角度的偏移。这个设置在实际工业项目中显然不够用,但作为启蒙实验,它能把问题压缩到最小,训练速度和排错速度都快。
3.2 旋转框的回归目标与损失函数:角度是最难搞的一个
对于中心点、宽高来说,回归目标和水平检测完全一样,使用常见的smooth L1损失或者直接L1损失即可。中心点回归的是相对anchor中心点的偏移,宽高回归的是对anchor宽高的对数比例。这些都很成熟,直接照搬就行。
角度回归的处理需要单独设计。直接在原始角度值上做L1损失,会撞上前面说的角度周期性问题。我最常用的方案是“sin/cos编码 + 周期感知损失”的组合拳。
sin/cos编码的含义是,把待预测的角度值映射到单位圆上的两个坐标值,即sin(2θ)和cos(2θ)(乘以2的原因是为了让长边法下角度范围[-90°, 90°)正好覆盖单位圆的一整圈,消除“同一个框两个表示”带来的歧义)。这样网络输出的两个值天然具备周期性,不会因为90°边界而跳变。损失函数可以直接对sin和cos各算一个L1损失并相加。另一个替代方案是直接计算两个角度之间的最小弧长差:
import torch def angular_loss(pred_angle_rad, target_angle_rad): """ 计算两组角度(弧度)之间的周期感知L1损失。 原理是把角度差投影到[-pi, pi]区间,消除周期性跳变。 """ diff = pred_angle_rad - target_angle_rad diff = torch.atan2(torch.sin(diff), torch.cos(diff)) return torch.mean(torch.abs(diff))这个损失会在推理阶段直接预测角度值时特别有用,但在训练早期的梯度稳定性上不如sin/cos输出分支。在实际项目中,我更推荐在head输出sin和cos,推理时用atan2还原角度,同时可以在训练损失里额外加一个小权重的周期感知角度损失作为辅助。
还有一个细节容易忽视:回归目标中dw和dh的输出范围需要做归一化,dx和dy一般是相对anchor宽高的比值,角度不需要归一化到固定范围,因为sin/cos输出自然落在[-1,1]区间。如果你直接回归角度值,最后一层的激活函数要么不加,要么用tanh约束到固定范围,千万不能直接裸输出到一个未约束的线性层,否则训练初期角度值可能满天飞。
3.3 极简可跑的PyTorch训练骨架(合成数据版)
为了让启蒙篇的闭环足够完整,我这里构造一个合成数据集:每张图是256×256的灰色背景,随机放置5到10个旋转矩形目标,矩形内部填充不同的纯色,矩形中心、长宽、角度全部随机。模型只需要从这样的图中学会把矩形的位置和角度回归出来。虽然数据很简单,但它能快速验证整个pipeline里所有代码是否正确。
import torch import torch.nn as nn import numpy as np import cv2 class TinyCNN(nn.Module): """极简主干:5层卷积 + 分类头 + 回归头,输出旋转框五参数+类别。""" def __init__(self, num_classes=1): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(64, 64, 3, stride=2, padding=1), nn.ReLU(), ) # 特征图尺寸为 256 / 2^4 = 16 x 16 self.cls_head = nn.Conv2d(64, num_classes + 1, 1) # 含背景类 self.reg_head = nn.Conv2d(64, 6, 1) # dx, dy, dw, dh, sin2theta, cos2theta def forward(self, x): feat = self.features(x) cls_logits = self.cls_head(feat) reg_offsets = self.reg_head(feat) return cls_logits, reg_offsets def build_synthetic_batch(batch_size=4, img_size=256, num_boxes=6): """ 生成合成旋转框数据。返回: - images: (B,3,H,W) 的tensor - targets: list,每个元素是 Kx6 的数组 [cx, cy, w, h, theta_rad, label] """ images = np.zeros((batch_size, img_size, img_size, 3), dtype=np.uint8) all_targets = [] for b in range(batch_size): img = np.full((img_size, img_size, 3), 40, dtype=np.uint8) targets = [] for _ in range(num_boxes): cx = np.random.uniform(40, img_size-40) cy = np.random.uniform(40, img_size-40) w = np.random.uniform(20, 80) h = np.random.uniform(8, 24) theta = np.random.uniform(-np.pi/2, np.pi/2) rect = ((cx, cy), (w, h), -np.rad2deg(theta)) color = (np.random.randint(120,255), np.random.randint(120,255), np.random.randint(120,255)) box_pts = cv2.boxPoints(rect).astype(np.int32) cv2.fillConvexPoly(img, box_pts, color) targets.append([cx, cy, w, h, theta, 0]) images[b] = img all_targets.append(np.array(targets)) images_t = torch.from_numpy(images.transpose(0,3,1,2)).float() / 255.0 return images_t, all_targets训练的时候,对于每个anchor位置,我把与目标中心距离最近的anchor视为正样本(并且要求目标中心到anchor中心距离小于一个阈值),正样本负责回归对应的目标框。分类损失用简单的交叉熵,回归损失用上一小节提到的角度相关损失。这里的anchor匹配策略非常简化,工业项目里需要用基于IoU的匹配器,但启蒙阶段先让闭环转起来才是重点。
def compute_loss(cls_logits, reg_offsets, targets): # 简化实现:只取与真实目标中心最近的anchor作为正样本,其余为背景 # 为了给读者一个完整可跑的最小示例,这里的匹配逻辑可以写得更细,但这里只给出关键流程 pass训练几个epoch后,把预测的旋转框画回原图上,如果能看到框的位置慢慢贴近真实目标,角度不再乱飘,那你的第一个手搓旋转目标检测闭环就正式打通了。
3.4 工业级视角:从单尺度到特征金字塔
上面这个极简闭环能跑通,但离工业级还很远。首先,单尺度特征图只能捕捉到固定尺度范围的目标,一旦图里同时出现长边为20像素和200像素的目标,单尺度会损失大量召回。工业级方案几乎无一例外会使用特征金字塔网络(FPN),把不同层级的特征做融合,让每一层负责特定尺度范围的目标。
但这不意味着启蒙篇里不用FPN就是错误的选择。恰恰相反,我建议所有人在第一次手搓旋转检测器时,先做单尺度单anchor。原因有二:其一,FPN和anchor多尺度会把匹配逻辑变得非常复杂,一旦模型不收敛,很难定位是角度回归的问题还是匹配逻辑的问题;其二,工业项目里你接收的数据往往本身就是从固定高度采集的,比如无人机在确定高度飞行、产线相机固定在某个位置,目标尺度范围远没有DOTA那种公开数据集那么离谱。先跑通再扩展,这个次序在工业项目里非常管用。
4. 训练中一定会踩的坑:角度回归的边界黑洞
4.1 角度周期性遇上回归损失,loss为什么炸了
真正上手之后,你大概率会在某个时刻看到这样一幅画面:训练loss前几十个iter正常下降,突然猛增到原来的十倍,随后又慢慢恢复,过一会再次猛增。这个现象出现的原因,九成是角度跳变。
具体来说,假设当前预测框的角度是88°,真实目标框的角度是-89°,两者物理上几乎重合。如果用普通L1损失,差值是177°,梯度会把预测角度强行往-89°方向拉。问题是路径上有两条路,一条是穿过90°边界到-89°(距离2°),另一条是绕回去到-89°(距离177°)。普通L1会让网络选择绕远路,而这个远路要跨越的数值范围非常大,于是出现loss尖峰。在工业项目里,如果目标物的角度分布横跨-90°到90°的边界,比如零件横着摆、竖着摆都有,这个问题会非常突出。
对应排查方式很直接:在训练过程中把每个batch里角度损失特别大的样本单独打印出来,检查是不是都集中在边界附近。如果是,基本可以确认是周期性导致的问题。
4.2 长边法的边界跳变:为什么w和h会瞬间互换
长边法还有一个隐藏问题:规定w是长边、h是短边之后,当目标真实角度在90°附近徘徊时,同一个框只需要旋转极小的角度就能让“长边”的定义发生切换,此时w和h会瞬间互换。这种不连续的定义对回归问题极不友好。
常见解决办法有三种:
- 采用“短边法”配合角度范围[-90°, 0°),参考OpenCV的约定,w不再是长边,而是固定的宽边;
- 保持长边法,但在损失函数里加入一项周期性惩罚,让网络在边界处对“选择哪条边作为长边”不敏感;
- 在数据预处理时,把所有角度统一映射到[-90°, 90°),并把w始终调整为对应角度的边。这种做法在训练阶段常用,但在推理输出时要把角度和宽高一起还原。
三种方案里,方案一在部署到OpenCV和onnx时最省事,因为cv2.boxPoints的默认约定就是短边法+负角度;方案二在公开数据集上表现更平滑;方案三则少有人直接用,主要因为推理后处理麻烦。我的建议是,你用哪个框架做部署就优先适配哪个框架的约定,然后在训练和评估之间做好角度转换。
4.3 常见问题速查表:启蒙阶段最典型的五个问题
为了让后来者少走一些弯路,我把启蒙阶段看到频率最高的五个问题整理成表格,方便对照排查。
| 问题现象 | 可能原因 | 排查方式与解决办法 |
|---|---|---|
| 训练loss一直不降,cls损失正常,reg损失不降 | 回归目标没有归一化;角度范围不匹配 | 检查dx/dy/dw/dh是否归一化到合理区间;检查角度是否统一到同一范围 |
| loss偶发大尖峰 | 角度周期边界跳变 | 切换到sin/cos输出分支,或使用周期感知角度损失 |
| 预测框位置正确但整体乱转 | 角度回归未收敛或sin/cos输出后atan2用错 | 单独可视化一个batch的角度误差;检查atan2分支顺序是否与编码顺序一致 |
| 训练时mAP不错,NMS后严重漏检 | 使用了水平NMS,旋转框之间重叠高 | 替换为旋转框IoU计算和旋转NMS |
| 标注看起来没问题,但训练后框整体偏小一圈 | 训练时用短边法表示,推理输出未转换回标注格式 | 统一整个pipeline的宽高定义,尤其在格式转换函数里写清楚“w是长边还是宽边” |
启蒙阶段遇到问题不要慌,先看数据定义,再看代码里的角度转换,最后才看模型结构。我处理过的绝大多数错误,最后都定位在数据格式和角度定义上,而不是模型设计上。
5. 工业落地的工具链与选型经验
5.1 标注工具与格式:工程质量从标注阶段开始
旋转目标检测的标注工具,现在比较好用的有X-AnyLabeling和Label Studio,两者都支持旋转矩形标注。更传统一点的是roLabelImg,功能少但轻量稳定。我的经验是,不纠结工具本身,重点是导出格式一定要统一到有序四点坐标,这样无论后续转成DOTA格式、五参数格式,还是其他专用格式,都有干净的数据源。
工业项目里标注环节最容易出的问题是:标注员用四点标注时,四个点的顺序没有统一规则,比如有的人从左上开始顺时针,有的人从右上开始逆时针。这个问题会导致训练代码里“第一个点是左上角”的假设完全失效。应对办法是写一个标准化脚本,根据四点的凸包顺序统一重排,再用面积正负号判断方向,把所有点整理成一致的顺时针顺序。这个小脚本值得在项目第一天就写出来。
5.2 评估指标:DOTA格式下的mAP和你想象的mAP不一样
公开数据集评测旋转检测模型最常用的指标是基于DOTA格式的mAP,其中IoU计算的是两个多边形(四边形)的交并比,不是普通矩形框IoU。很多人第一次跑DOTA测试集时发现分数比预期低很多,就是因为多边形IoU对框的贴合度要求比水平框严格得多。
工业项目如果不打公开榜单,我建议直接按业务口径定义指标,比如抓取场景下看“角度误差小于2°的前提下,中心点误差小于5像素”视为成功。这比mAP更能反映产线上的实际效果。但无论用什么指标,都要注意和“旋转NMS”配合使用,否则最终结果可能因后处理过于激进或保守而与训练时的评估曲线大相径庭。
旋转NMS和水平NMS的区别在于:计算两个框的IoU时,需要先计算两个任意四边形的交集多边形面积,再用交集面积除以并集面积。这个计算要用到多边形裁剪算法,OpenCV里有cv2.intersectConvexConvex可以处理凸多边形的情况。旋转框都是凸四边形,所以够用。
5.3 工程师视角:训练完到部署上线还差哪些东西
训练出能画框的模型只是第一步。工业项目要上产线,至少还要补三件事:
- 模型导出:PyTorch模型转ONNX时,旋转角度的sin/cos分支可以原样导出,但atan2算子在部分推理引擎上支持得不够好,我通常把atan2放到后处理代码里做,用CPU算那几千个框的角度,几乎不占耗时;
- 部署后处理:旋转NMS要改成自己实现的C++或Python版本,不能继续依赖训练框架里的库;
- 精度校准:旋转框对输入图像的缩放比水平框更敏感,缩放会改变角度与边长比例。如果训练时做了512输入,部署时为了速度改成416,最好重新评估一次角度误差,而不是只看mAP。
这些内容属于卷3的范畴,启蒙篇先点到为止。你现在只需要记住:手搓网络的意义就在于,上面这些环节你是理解着去做的,而不是遇到了才去翻源码。
6. 关于“从零手搓”这件事,我的几点体会
写到这里,启蒙(一)的核心内容就差不多结束了。最后想闲聊几句。
我在最初接触旋转目标检测时,也踩过“拿来就用”的坑。当时项目催得紧,我直接用了开源库,demo跑得很好,模型却怎么都训不上线,每天浪费在调参上的时间比重新手搓一遍还多。后来耐住性子,从旋转框的数学定义一点点捋,才发现问题根源竟然是标注脚本的角度定义和训练库不一致。那之后我就养成了一个习惯:凡是涉及旋转检测的项目,无论工期多紧,第一周一定先把“画框、转格式、算IoU”这些底层工具全部手写一遍,哪怕最后不用,也要用它们把数据和定义验一遍。
你也不用害怕“从零手搓”看起来工程量大。事实上,旋转目标检测的骨架、neck、head设计,和水平目标检测高度相似,你完全可以复用已有的知识储备,真正需要从零理解的就是角度表示、角度回归和旋转后处理这几个部位。把这几块啃透,整个领域对你来说就没有黑盒。
卷1的启蒙(一)到这里先停。下一篇我会继续讲anchor匹配与标签分配在旋转框下的特殊处理,那是从“能跑”走向“能用”的关键一役。需要提前预习的朋友,可以先把我上面那个可视化脚本放到自己的数据上多跑几遍,看看不同角度定义画出来的框到底长什么样,有了直观感受,后面讲什么你都接得住。