去年接了某高校行星遥感课题组的一个模拟项目,对方需求很明确:从月球表面影像里自动标出陨石坑,同时把散落的巨石也找出来,最后还要算出每个坑的直径和半径。一开始我以为这活儿用传统图像处理就能糊弄过去,结果光照、阴影、尺度变化直接把人教做人,最后还是老老实实走了YOLO路线,在检测头之外单独做了圆形拟合和半径估算。这篇就完整复盘一下这套系统的设计与落地过程。
标题里几个关键词其实藏着两条任务线:第一条是“目标检测”,用YOLO找出陨石坑和巨石;第二条是“圆形识别与半径估算”,在检测结果之上把圆的参数解出来。前者解决“在哪”,后者解决“多大”。二者缺一不可,而真正的工程难点几乎全集中在第二条线上。
1. 项目需求拆解:为什么一张图上要同时找“坑”和“石头”
1.1 两类目标的共性与差异
陨石坑和巨石出现在同一类遥感影像里,但它们的特征差别很大。陨石坑是凹陷结构,在低太阳高度角影像下呈现明显的明暗边界,整体接近圆形或椭圆;巨石则是凸起的块状物,形状不规则,投影近似团块,边缘受阴影影响非常严重。
如果只做检测,两类目标都可以当作“物体”来处理,YOLO模型并不关心你是坑还是石头,它只学习外观特征到类别标签的映射。但如果要估直径,事情就变了:陨石坑有明确的边界,半径估算有物理意义;巨石没有稳定的几何中心,硬给它套一个“半径”其实意义有限,更多是作为一个尺度参考量。
所以我把系统拆成两条输出线:陨石坑检测后必须输出圆心坐标和半径,用于后续形貌统计;巨石检测后只输出包围框和面积占比,不强制做圆拟合。这个取舍不是偷懒,而是避免模型在错误的任务上学到错误的表征。
1.2 标题任务的三层拆解
把标题里的需求翻译成技术语言,实际上就是三件事。
第一层是目标检测,用YOLO完成,输出目标的类别和大致位置。第二层是圆形识别,判断目标是否适合用圆来描述——这一步在代码里往往退化为“我只对陨石坑做圆拟合”。第三层是直径/半径估算,把像素尺度的检测结果换算成可用的几何参数,并且最好能和真实物理尺度对应。
很多入门者只做第一层就交差了,输出一个外接矩形框说“这就是直径”。实际一测,椭圆坑的对角线半径能比真实半径偏出百分之十几,放到直径统计里误差被放大两倍。所以这个项目真正花时间的,是把后两层做扎实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据准备:圆形目标的数据集与标注方案
2.1 影像切片与样本分布
遥感影像通常是一整张大图,几万乘几万的像素直接喂给YOLO不现实。我采用的方案是滑动窗口切片,窗口大小1024×1024,重叠128像素。重叠的目的很直接:避免一个坑刚好被切在两张图边缘,导致目标被裁成半个,既影响训练也影响推理。
切片之后要统计目标尺寸分布。我第一批数据里,小坑的直径只有十几个像素,大坑能到三四百像素,跨度非常大。这种尺度分布如果不做处理,模型在小目标上的表现会很难看。解决方案是开启多尺度训练,并加大马赛克增强的概率,让模型在不同缩放级别下都能看到完整的坑。
光源方向也要注意。月球影像的阴影方向是固定的,坑的亮缘和暗缘有很强的一致性。如果数据集里混入了不同光照方向的影像,模型容易把阴影当作边界特征来学,泛化时会出问题。最好在数据清洗阶段按光照方向分组,或者用光照增强把阴影方向多样性打出来。
2.2 矩形框标注与“圆”的天然矛盾
YOLO原生标注格式是中心点加宽高,用矩形框去框一个圆形目标,四个角必然包含大量背景。对于陨石坑来说,坑缘本身是圆形,矩形框的对角线方向其实是空白区域。这个信息冗余对检测影响不大,但对半径估算影响很大。
我试过两条路线:一条是老老实实只用矩形框,半径从框高或框宽推算;另一条是在标注阶段额外给每个坑标注圆的参数,也就是圆心坐标和半径,训练时加一个回归头直接学这两个值。
实际标注下来,矩形框标注一天能标几百个,圆参数标注一天只能标一百多个,因为要反复调整圆心位置和半径大小。最终我采用折中方案:主任务用矩形框,辅助任务用分割掩码。也就是先让标注人员把坑的轮廓画成多边形,训练时用掩码生成最小外接圆的圆心和半径作为回归目标的真值。这样既控制了标注成本,又能拿到足够准确的圆参数。
2.3 数据增强的特殊设计
圆形目标有一个天然优势:旋转对称。普通目标检测做旋转增强时,矩形框里的目标语义可能被破坏——比如一辆车旋转90度就不太像车了。但陨石坑旋转任何角度都还是陨石坑,所以旋转增强的角度范围可以开得很大。
我在训练配置里把旋转增强的范围设到了正负90度,配合水平翻转和垂直翻转,等效数据量翻了好几倍。实测下来,模型的圆形感知能力明显增强,尤其是对坑缘不完整的目标,鲁棒性好了很多。
颜色增强反而要谨慎。月球表面灰度影像为主,色调抖动别开太大,否则模型会学到不真实的纹理。我主要用亮度扰动和对比度扰动,模拟不同太阳高度角下的明暗差异。
3. 模型选型:为什么YOLO检测头不能直接给半径
3.1 传统圆检测与深度学习方案的取舍
有人会问:OpenCV里有现成的HoughCircles,为什么还要上YOLO?我最初也天真地以为拿Canny提边缘再霍夫变换找圆就够了,结果在真实遥感图上完全撑不住。
坑缘受阴影遮挡经常断成一截一截的,Canny出来的边缘密密麻麻,霍夫圆检测的投票结果一片噪声。调参调了两天,要么把阴影边缘错认成圆,要么把真实坑漏掉。最致命的是,HoughCircles对同心圆、嵌套坑这种结构无能为力,而这种结构在大型陨石坑边上非常常见。
深度学习的优势不是找圆找得准,而是它天然把“目标感知”和“形状拟合”分开了。YOLO先判断这里有一个坑,再想办法拟合圆的参数。目标感知这一层由大量标注数据兜底,鲁棒性远超传统方法。
3.2 平替方案A:矩形框转半径,适合精度要求不高的场景
最简单的做法是检测到目标框后,取宽和高的较小值的一半作为半径,等价于假设坑的内切圆能代表坑的形状。对接近正圆的坑,这个估计误差在5%以内;对椭圆形坑,误差会到10%以上。
如果只做粗略统计,比如直径分布直方图,这个方案完全够用。它不需要改模型结构,不增加任何推理负担,YOLO输出什么就直接算什么。但我这个项目对直径精度有要求,数值要参与后续的地貌分析,所以这个方案最终只作为兜底逻辑保留。
3.3 改造方案B:YOLO颈部加回归头输出圆心与半径
真正的改造是给YOLO增加一个平行的圆参数回归头。以YOLOv8为例,主干和颈部完全复用原结构,区别只在检测头上:原来的检测头输出类别概率和矩形框坐标,我在同一个特征层级上并联了一个三通道回归分支,分别输出归一化后的圆心x、圆心y和半径。
训练阶段,真值从掩码的最小外接圆生成。推理阶段,这个分支直接输出目标的圆心和半径,省掉了后处理步骤。损失函数上面要单独设计,这个我在下一章详细讲。
需要说明的是,这个改造在YOLOv8官方仓库里没有现成模块,需要手动改模型定义和损失计算。如果不想动源码,也可以退而求其次用YOLOv8-seg做分割,再对掩码求外接圆,效果接近但推理速度会慢一截。两条路线我都在项目里跑过,各有适用场景。
两种改造路线的对比:
| 方案 | 圆心精度 | 半径精度 | 推理开销 | 工程复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 矩形框转半径 | 差 | 较差 | 无 | 零 | 粗略统计 |
| 平行回归头 | 好 | 好 | 极小增加 | 中 | 需要批量快速处理 |
| 分割掩码求圆 | 很好 | 很好 | 显著增加 | 低 | 离线高精度分析 |
4. 半径估算的核心实现:从模型输出到直径参数
4.1 分割后处理:minEnclosingCircle的精度优势
YOLOv8-seg输出的是实例掩码,拿到掩码后可以用OpenCV的minEnclosingCircle求最小外接圆,返回值是圆心坐标和半径。这个方法的优势是精度完全由掩码质量决定,掩码画得准,圆心和半径就准。
代码很简洁,几行就能跑通:
python复制import cv2
import numpy as np
def mask_to_circle(mask):
# mask: 二维0/1数组
contours, _ = cv2.findContours(mask.astype(np.uint8),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return None
# 取面积最大的轮廓
contour = max(contours, key=cv2.contourArea)
(cx, cy), radius = cv2.minEnclosingCircle(contour)
return (cx, cy, radius)
注意一点,minEnclosingCircle对掩码上的毛刺非常敏感,掩码边缘如果有一两个像素的凸起,外接圆半径会被明显撑大。我通常在求圆之前先对掩码做一次形态学闭运算,去掉细小的凹陷和凸起,半径估计会稳定很多。
4.2 平行回归头的输出换算
回归头输出的是归一化坐标,需要乘回输入图像的宽高才能得到像素值。我在实现时把圆心坐标归一化到[0,1],半径归一化到[0,0.5]——因为半径不可能超过图像宽度的一半。
推理后要还原到原始切片坐标,再根据切片在大图中的偏移量换算到全局坐标。这里最容易出错的地方是数据增强带来的坐标扰动:训练时旋转、翻转都会改变目标的真实位置和半径,所以在计算回归真值时,必须用经过增强变换后的掩码重新生成圆心和半径,而不是直接用原始标注值。
我一开始图省事,用原始标注直接喂回归头,训练怎么也收敛不到预期。后来改成了在数据流里先做变换、再生成圆参数真值,问题立刻消失。这个细节非常关键,很多人踩坑都踩在这里。
4.3 像素尺度到物理尺度的换算
半径估出来是像素值,要变成物理尺度,需要知道每像素对应的实际距离。遥感影像通常自带分辨率元数据,比如每像素对应多少米。如果数据里没有,可以用已知直径的参考坑做标定,取几个明显坑的真实直径与像素直径做比值平均。
换算公式很简单:
code复制真实直径(米) = 像素直径(像素) × 分辨率(米/像素)
但如果影像经过了重投影或几何校正,分辨率可能随位置变化,严格来说应该用逐像素的投影参数去换算。我在项目里使用的是全局平均分辨率,对直径统计来说已经足够,不会引入量级性的偏差。
4.4 损失函数设计:为什么不能照搬检测头的Loss
前面提到平行回归头需要单独设计损失,原因是检测头的CIoU损失和圆形参数回归的坐标空间不一致。矩形框的IoU关注的是四个边界的对齐程度,而圆的误差更关注圆心偏移和半径偏差。
我用的是加权组合损失,圆心部分用欧氏距离,半径部分用相对误差:
python复制epsilon = 1e-6
center_loss = torch.mean(torch.sqrt((cx_pred - cx_true) ** 2
+ (cy_pred - cy_true) ** 2))
radius_loss = torch.mean(torch.abs(r_pred - r_true) / (r_true + epsilon))
circle_loss = center_loss + 2.0 * radius_loss
半径相对误差比绝对误差更合理,因为大坑和小坑的绝对像素误差尺度完全不同,相对误差能把大小坑放在同一标准下衡量。权重比例上我让半径损失占大头,因为项目对直径精度最敏感。
5. 训练与评估:怎么判断“圆检测对了”而不是“框对了”
5.1 评价指标:圆心偏差、半径误差与圆IoU
目标检测的常规评估指标是mAP,但mAP衡量的是矩形框的对齐程度,无法直接反映圆参数的质量。所以我在项目里加了三个自定义指标:圆心偏差小于5像素的比例、半径相对误差小于10%的比例、以及圆IoU。
圆IoU的计算方式是:把预测圆和真值圆分别渲染为二值掩码,再计算两个掩码的交并比。这个指标综合反映圆心和半径的准确性,比单独看某个参数更有说服力。
训练过程中我同时监控这三个指标。如果圆心偏差很小但半径误差大,说明模型对圆的边界尺度把握不准;如果圆IoU低但检测AP高,说明模型定位准确但圆的拟合质量差,通常需要调整回归头的权重或增加掩码监督。
5.2 阈值调参与NMS去重
圆形目标有一个特点:两个坑不会重叠,但相邻小坑可能挨得非常近,边界几乎相切。YOLO的NMS按类别独立执行,在密集场景下容易把紧邻的两个坑框合并成一个,导致漏检。
我把NMS的IoU阈值从默认的0.7调低到0.5,减少了邻近目标的合并风险。同时把置信度阈值分成两档:检测置信度0.25,圆参数输出置信度0.45。低置信度的检测框保留用于后续人工复核,但不要进入自动圆的统计,避免噪声影响统计结果。
另外,如果目标是椭圆形的坑,圆拟合天然存在偏差。我建议在输出结果里附带一个“圆度”指标,计算方式是用最小外接圆的半径与最大内切圆半径的比值。圆度低于0.8的坑,在后续统计中应标记为椭圆近似,而不是直接当作正圆参与直径统计。
5.3 小目标陨石坑的踩坑实录
这个项目里最折磨人的是小坑漏检。第一次跑完训练,大坑的表现还可以,直径50像素以下的坑几乎全军覆没。查了数据分布,发现小坑样本占比本身就低,再加上模型默认输入尺寸缩小后,小坑在特征图上的响应几乎消失。
我的解决思路分三步。第一步把输入分辨率从640提高到1024,小坑在特征图上能占到更多像素。第二步调整锚框策略,YOLOv8默认锚框比例对圆形目标不友好,我手动加入了接近1:1的预设框。第三步加大了密集小坑的过采样权重,让模型在训练中更多看到这种困难样本。
这三步做完,小坑的召回率从不到30%提到了接近70%。后续再想往上提,只能靠增加高分辨率样本和更精细的标注来解决了。
6. 系统落地:推理管线、性能与扩展方向
6.1 端到端推理管线
整个系统的推理流程可以概括为五个步骤:大图切片、模型推理、圆参数还原、坐标映射、结果导出。我把它们串成一个统一接口,输入一张大图,输出一个GeoJSON文件,每条记录包含类别、置信度、圆心坐标、半径和换算后的物理直径。
核心的自动检测和圆参数提取流程演示如下:
python复制def process_tile(tile, model, offset_x, offset_y):
results = model(tile, conf=0.25, iou=0.5)
outputs = []
for r in results:
for box, mask in zip(r.boxes, r.masks):
cx, cy, radius = mask_to_circle(mask)
# 加上切片偏移量映射回全局坐标
outputs.append({
"cx": cx + offset_x,
"cy": cy + offset_y,
"radius": radius,
"confidence": float(box.conf)
})
return outputs
对重叠切片产生的重复检测,我最后用一次全局NMS去重,以置信度最高的结果为准。这一步虽然简单,但能显著降低最终结果里的重复坑数量。
6.2 性能与显存权衡
我用的是一张普通消费级GPU,1024输入尺寸下,单张切片的推理耗时在几十毫秒级别。处理一整幅大型遥感影像,切出来两三千张切片,批量推理加上后处理,总时间能控制在十分钟左右。这个吞吐量对科研场景完全够用。
如果部署到嵌入式设备或实时场景,建议退回输入尺寸640,同时把圆参数回归从分割掩码改回平行回归头,推理速度能快一倍以上,代价是半径精度下降。我的建议是:离线分析用分割掩码方案,在线处理用回归头方案。
6.3 这套思路能迁移到哪些方向
陨石坑和巨石检测只是圆形实体检测的一个典型场景。这套“YOLO检测外接框加圆拟合”的架构,实际可以迁移到很多工业检测任务里:光伏板圆形基座的定位与直径测量、钢卷端面圆心的对准、药片和胶囊的计数与尺寸筛选、无人机航拍里树冠圆度的估算。
共同点是:目标外形接近圆形,既有检测需求,又有尺寸测量需求。我的建议是遇到这类任务,先不要急着定制复杂模型,把YOLO分割掩码加最小外接圆的组合跑通,通常能覆盖大部分需求。当精度达不到时再考虑改回归头,毕竟工程上最稳的路线才是效率最高的路线。
我在实际把系统跑通之后,最大的体会是——这个项目的难点并不在模型结构上,而是在数据规范、坐标换算、掩码后处理这些看似琐碎的环节上。尤其是半径估算,模型输出的只是一个中间量,最终的可靠性取决于你对每一个像素到物理单位的换算链路是否心里有数。把这条链路盯紧了,整个系统的可信度才能立得住。
