012、小目标标注总被忽略SAHI 切片策略与超大图标注的工程化方案从一次深夜调试说起去年做遥感图像检测项目客户给了一张 2 万像素 × 2 万像素的卫星图里面密密麻麻的车辆、船只最小的目标只有 8×8 像素。我直接扔进 YOLOv8 训练mAP 0.5 勉强到 0.3小目标 recall 几乎为零。更离谱的是标注人员告诉我“那些小点我根本没标因为看不清而且标注工具里缩放太麻烦。”这不是个例。小目标标注被忽略本质上是两个问题叠加一是标注工具在大图上操作困难二是模型在原始分辨率下对小目标感知力不足。SAHISlicing Aided Hyper Inference切片策略就是专门解决这个痛点的工程化方案。为什么直接训练大图会失败先别急着上切片。你得理解为什么大图直接训练不行。YOLO 系列在输入尺寸上有限制通常 640×640 或 1280×1280。你把 2 万像素的图 resize 到 640小目标直接变成 0.2 像素——模型根本看不见。有人会说“那我用大分辨率输入”但显存扛不住batch size 降到 1 都爆显存。更隐蔽的问题是标注质量。标注员在超大图上标注缩放比例失调小目标很容易被漏标、错标。我见过一个项目标注员把 10 像素的汽车标成了 5 像素的矩形框因为缩放后鼠标点不准。这种标注噪声比模型本身的误差更致命。SAHI 切片策略把大图拆成小图SAHI 的核心思想很简单训练和推理时把大图切成若干小图每个小图独立检测最后合并结果。但这里有几个坑我踩过之后才明白。切片参数怎么设切片大小和重叠率是关键。切片太小目标被切碎切片太大小目标依然不明显。我的经验公式切片大小 模型输入尺寸 × 1.5 到 2 倍。比如 YOLOv8 输入 640切片用 960 或 1280。重叠率至少 30%否则边界处的目标会被截断。遥感图像我常用 50% 重叠因为目标分布密集。代码实现时注意边界处理。别这样写# 错误示范直接整除切片边界目标丢失foriinrange(0,img_height,slice_size):forjinrange(0,img_width,slice_size):slice_imgimg[i:islice_size,j:jslice_size]这里踩过坑当图像尺寸不是切片大小的整数倍时最后一行/列会丢失。正确做法是计算实际切片数量确保覆盖全图# 正确做法计算切片数量处理边界num_slices_ymath.ceil((img_height-overlap)/(slice_size-overlap))num_slices_xmath.ceil((img_width-overlap)/(slice_size-overlap))foriinrange(num_slices_y):forjinrange(num_slices_x):y_starti*(slice_size-overlap)x_startj*(slice_size-overlap)# 确保不越界y_endmin(y_startslice_size,img_height)x_endmin(x_startslice_size,img_width)# 如果切片尺寸不足用padding补全ify_end-y_startslice_sizeorx_end-x_startslice_size:# 这里用0填充或镜像填充看场景pass标注怎么同步切片训练时标注框也要跟着切片。SAHI 官方库提供了切片标注的功能但有个坑它默认只保留完全落在切片内的目标。对于跨切片的目标直接丢弃会导致训练数据丢失。我的做法是保留与切片有交集的任何目标但只取交集部分作为新标注。这样虽然会引入一些截断的目标但总比丢掉好。推理时再用 NMS 合并。# 保留跨切片目标只取交集defslice_annotation(bbox,slice_bbox):x1max(bbox[0],slice_bbox[0])y1max(bbox[1],slice_bbox[1])x2min(bbox[2],slice_bbox[2])y2min(bbox[3],slice_bbox[3])ifx2x1andy2y1:# 转换到切片坐标系return[x1-slice_bbox[0],y1-slice_bbox[1],x2-slice_bbox[0],y2-slice_bbox[1]]returnNone超大图标注的工程化方案切片解决了模型训练的问题但标注环节才是真正的瓶颈。标注员面对 2 万像素的图效率极低。我试过几种方案最终选了一个折中。方案一先切片再标注把大图切成 640×640 的小图让标注员在小图上标注。优点是标注精度高小目标不会被忽略。缺点是切片数量爆炸2 万像素的图切成 640 大小重叠 50%能切出上千张小图。标注员要标注上千张图重复劳动多。方案二标注大图训练时切片标注员在大图上标注训练时程序自动切片。优点是标注工作量小一张图标一次。缺点是大图上标注小目标容易漏而且标注工具在大图上操作卡顿。方案三混合策略推荐我最终用的是这个先在大图上标注大目标比如建筑、道路然后自动切片让标注员在小图上补充标注小目标车辆、行人。这样大目标不会漏小目标也能精确标注。具体流程标注员在大图上标注面积大于 1000 像素的目标大目标程序自动将大图切成 640×640 小图重叠 30%标注员在小图上标注面积小于 1000 像素的目标小目标程序合并标注结果去重去重逻辑要注意同一个目标可能出现在多个切片中用 IoU 阈值 0.5 合并。这里踩过坑如果阈值设太低会把相邻的不同目标合并成一个。推理时的 SAHI 合并策略训练完模型推理时也要切片。SAHI 的推理流程将大图切成小图每个小图独立推理每个小图的检测结果转换回原图坐标系用 NMS 合并重叠的检测框NMS 的 IoU 阈值要调低我通常用 0.3。因为同一个目标可能出现在多个切片中检测框位置有偏移阈值太高会保留重复框。还有一个细节切片边缘的目标检测置信度通常较低因为目标被截断。我加了一个后处理如果检测框距离切片边界小于 10 像素且置信度低于 0.5直接丢弃。这能减少大量假阳性。# 边缘低置信度过滤deffilter_edge_boxes(boxes,scores,slice_bbox,margin10):filtered_boxes,filtered_scores[],[]forbox,scoreinzip(boxes,scores):x1,y1,x2,y2box# 检查是否靠近切片边界if(x1-slice_bbox[0]marginory1-slice_bbox[1]marginorslice_bbox[2]-x2marginorslice_bbox[3]-y2margin):ifscore0.5:continuefiltered_boxes.append(box)filtered_scores.append(score)returnfiltered_boxes,filtered_scores性能优化别让切片拖慢推理切片推理的最大问题是速度。一张大图切成 100 张小图每张都要过模型推理时间增加 100 倍。我试过几种优化批量推理把切片组成 batch一次推理多张。注意 batch size 不要太大否则显存爆炸。我通常用 4 或 8。跳过空白切片如果切片内没有目标比如全是天空或水面直接跳过。可以用简单的像素方差判断方差小于阈值就跳过。多尺度切片大目标用大切片小目标用小切片。比如 1280 切片检测大目标640 切片检测小目标合并结果。这能减少切片数量。ONNX 加速把模型转 ONNX推理速度提升 30% 以上。YOLOv8 官方支持导出 ONNX一行代码搞定。个人经验性建议别迷信 SAHI 能解决所有小目标问题。它只是工程手段模型本身对小目标的感知能力才是根本。如果目标只有 4×4 像素切片也救不了。考虑超分辨率或特征金字塔改进。标注质量比模型更重要。我见过太多项目花大量时间调模型结果标注数据一塌糊涂。先花一周做标注质量检查比调一周模型参数有效。切片大小不是越大越好。有人觉得切片大能保留更多上下文但小目标在切片内占比反而更小。我做过实验640 切片比 1280 切片在小目标 recall 上高 5 个点。重叠率 30% 是底线。低于 30%边界目标丢失严重。高于 50%计算量翻倍收益递减。标注工具选对能省一半时间。LabelImg 在大图上卡死用 CVAT 或 Supervisely 的远程标注功能支持大图缩放和切片标注。别省这个钱。最后说一句小目标检测没有银弹。SAHI 切片是工程上的妥协它让不可能变成可能但代价是计算量和标注工作量。如果你的项目对实时性要求高切片推理可能不适用。这时候考虑模型结构改进比如 YOLOv8 的 P2 层或 YOLOv6 的 RepVGG 结构才是正道。