☰
Stitcher切片策略:小目标检测精度提升与工程实践
2026/10/19 8:01:47 网站建设 项目流程

1. 小目标检测的痛点与Stitcher的切入逻辑

做过目标检测的人都有一个共识:大目标好检,小目标难缠。所谓小目标,通常指在图像中占据像素面积很小的物体,比如航拍图里的车辆、监控画面远处的人、工业质检中的微小瑕疵。这些目标经过骨干网络多次下采样之后,特征信息几乎被稀释殆尽,检测头拿到的特征图上可能只剩下一两个像素的响应,漏检和误检就成了家常便饭。

传统应对思路无非几种:增大输入分辨率、用FPN做多尺度融合、设计更密集的Anchor、引入注意力机制。这些方法各有各的效果,但都有一个共同代价——计算量上去了,推理速度下来了。尤其在对实时性有要求的场景里,你不可能无限制地放大输入尺寸。

Stitcher这篇工作有意思的地方在于,它没有去改网络结构,也没有加什么花哨的模块,而是从数据组织和训练策略层面入手,用一个非常朴素的想法来提升小目标的检测表现。核心逻辑可以概括成一句话:把大图切碎,让小目标在切片里变成大目标。

这个思路听起来简单,但真正落地时有大量细节需要处理。切片怎么切、切完怎么拼、训练时怎么组织batch、推理时怎么合并结果、边界处的目标怎么处理——每一个环节都有坑。我在这篇笔记里会把这些问题逐一拆开,结合我自己的实操经验,把Stitcher这套方法讲透。

注意:Stitcher不是某个特定框架的官方模块,它更像是一种通用的训练和推理策略,可以挂载到YOLO系列、Faster R-CNN、DETR等多种检测器上。本文的讨论不绑定任何特定代码库,重点在于方法论和实操细节。

2. 切片策略的设计:为什么这样切而不是那样切

2.1 切片尺寸与重叠区域的权衡

Stitcher最核心的操作就是把原始大图切成若干个小图(patch),每个小图单独送入检测器。这里第一个要决策的就是切片尺寸。

切片太小,单张切片内的上下文信息不足,模型容易把局部纹理误判成目标;切片太大,小目标在切片里依然是小目标,失去了切片的本来意义。我的经验是,切片尺寸应该和检测器的训练分辨率匹配。比如你用YOLOv8训练时输入是640×640,那切片尺寸就设在640左右,这样切片送入网络时不需要额外缩放,避免了缩放带来的信息损失。

但光有切片尺寸还不够,重叠区域(overlap)的设置同样关键。如果切片之间没有重叠,一个目标恰好落在切割线上,就会被切成两半,两半都不完整,检测器很可能都检不出来。重叠区域的作用就是保证切割线附近的目标至少在某一张切片里是完整的。

重叠比例一般设在切片尺寸的10%到25%之间。我实测下来,15%到20%是一个比较稳妥的区间。重叠太小,边界目标仍然有被切断的风险;重叠太大,切片数量急剧增加,推理时间成倍上涨,而且重复检测的合并逻辑也会更复杂。

假设原图是4000×3000,切片尺寸640,重叠128像素,那么横向切片数为 ceil((4000-128)/(640-128)) = ceil(3872/512) = 8,纵向为 ceil((3000-128)/512) = 6,总共48张切片。如果重叠降到64像素,横向变成 ceil(3936/576) = 7,纵向 ceil(2936/576) = 6,总共42张。看起来只差了6张,但推理时间差了将近15%。所以重叠比例的选择需要根据你的精度要求和时间预算来平衡。

2.2 切片步长的计算与边界处理

切片步长(stride)的计算公式是:stride = patch_size - overlap。这个公式很直白,但实际操作中有个容易忽略的点——最后一行和最后一列的切片位置。

如果你严格按照固定步长切,最后一块切片可能超出图像边界。这时候有两种处理方式:一是把最后一块切片的位置往前挪,让它刚好贴住图像右边界或下边界,这样会导致最后一块和倒数第二块的重叠比设定的更大;二是对超出边界的部分做padding填充。我倾向于第一种方式,因为padding引入的无效像素可能干扰检测,而多出来的重叠只是增加一点点计算量,不会引入噪声。

具体实现时,可以用这样的逻辑:

def generate_patches(img_w, img_h, patch_size, overlap): stride = patch_size - overlap patches = [] # 横向起点 x_starts = list(range(0, img_w - patch_size + 1, stride)) if x_starts[-1] + patch_size < img_w: x_starts.append(img_w - patch_size) # 纵向起点 y_starts = list(range(0, img_h - patch_size + 1, stride)) if y_starts[-1] + patch_size < img_h: y_starts.append(img_h - patch_size) for y in y_starts: for x in x_starts: patches.append((x, y, x + patch_size, y + patch_size)) return patches

这段代码的关键在于最后那个判断:如果最后一个起点加上切片尺寸还没到图像边界,就补一个贴边的切片。这样保证图像右下角区域也被完整覆盖。

2.3 切片数量与推理耗时的实际测算

很多人第一次用Stitcher时会低估切片带来的计算量增长。原图直接推理一次,和切成48张切片各推理一次,时间差距不是48倍那么夸张(因为切片尺寸小,单次推理快),但通常也在10到20倍之间。

我做过一组实测:一张4000×3000的航拍图,用YOLOv8s在RTX 3060上直接推理(缩放到640×640)大约需要12毫秒,但切成48张640×640的切片逐张推理,总耗时约180毫秒。这个差距在离线批处理场景里完全可以接受,但在实时视频流里就是致命的。

所以Stitcher的适用场景需要明确:它更适合离线分析、高精度巡检、单张图片精细检测这类对时间不敏感但对精度敏感的任务。如果你要做实时检测,要么降低切片数量(增大切片尺寸、减小重叠),要么用更轻量的检测器来补偿。

3. 训练阶段的Stitcher:让模型学会在切片上工作

3.1 训练数据的重组织方式

Stitcher在训练阶段的做法直接影响推理效果。最直接的方式是:把训练集里的大图也按同样的切片策略切成小图,然后用这些小图去训练检测器。这样模型在训练时见到的目标尺寸分布,和推理时切片里的目标尺寸分布是一致的。

但这里有个问题:如果只是简单地把大图切碎,那些原本就在切片边界上的目标会被切断,变成不完整的样本。这些不完整样本如果直接拿来训练,会让模型学到错误的形状先验。我的处理方式是:在生成训练切片时,过滤掉那些被切割线截断且截断比例超过30%的目标框。截断比例较小的目标可以保留,因为模型本身就需要一定的鲁棒性来应对边界情况。

另一种更精细的做法是只在包含目标的区域周围生成切片,而不是均匀网格切割。这样可以大幅减少无效切片(全是背景的切片),提高训练效率。具体来说,可以先对原图做一次粗检测或者利用标注信息,找到所有目标的位置,然后以每个目标为中心生成切片,保证目标落在切片中心区域。这种方式训练出来的模型对切片内的目标响应更强,但缺点是可能丢失目标之间的上下文关系。

我通常采用折中方案:均匀网格切片为主,但对那些目标密集的区域额外增加一些以目标为中心的切片。这样既保留了上下文,又强化了重点区域。

3.2 正负样本平衡与切片筛选

切片之后,一个不可避免的问题是:大量切片可能只包含背景,没有目标。如果全部送入训练,正负样本比例会严重失衡。假设原图有10个目标,切成48张切片,可能只有8张切片包含目标,其余40张全是背景。这种比例下模型会偏向于预测背景。

解决办法有两个方向。一是在采样阶段做筛选,只保留包含目标的切片,以及少量纯背景切片(用于抑制误检)。纯背景切片的比例一般控制在总切片数的10%到20%。二是在损失函数层面做加权,对正样本的分类损失和回归损失加大权重。两种方式可以叠加使用。

我自己的习惯是:每个batch里,包含目标的切片和纯背景切片按4:1的比例混合。这样既保证了正样本的学习信号足够强,又不会让模型完全忘记背景长什么样。

还有一个细节值得注意:切片里的目标框需要重新映射坐标。原图上的标注框坐标是相对于原图的,切片之后要减去切片左上角的偏移量,变成相对于切片的坐标。这个映射逻辑必须和推理时的坐标还原逻辑严格对应,否则训练和推理就不一致了。

def remap_boxes(boxes, patch_x, patch_y, patch_size): """将原图坐标映射到切片坐标""" remapped = [] for box in boxes: x1, y1, x2, y2 = box nx1 = max(0, x1 - patch_x) ny1 = max(0, y1 - patch_y) nx2 = min(patch_size, x2 - patch_x) ny2 = min(patch_size, y2 - patch_y) if nx2 - nx1 > 2 and ny2 - ny1 > 2: # 过滤掉太小或完全在外的框 remapped.append([nx1, ny1, nx2, ny2]) return remapped

这段代码里的max(0, ...)和min(patch_size, ...)就是在做裁剪,保证映射后的框不超出切片边界。最后那个判断是为了过滤掉映射后面积过小的框,这些框通常是边界处被严重截断的目标,保留它们反而有害。

3.3 数据增强在切片上的适配调整

常规的数据增强手段(随机翻转、色彩抖动、Mosaic等)在切片场景下需要做一些调整。最大的问题是Mosaic增强:标准Mosaic是把四张图拼成一张,但在Stitcher场景下,如果你把四张切片拼成一张训练图,拼接线附近的目标又会被切断,等于人为制造了边界问题。

我的做法是:在Stitcher训练时关闭Mosaic或者降低Mosaic的触发概率。如果一定要用,就把Mosaic的拼接方式改成不切断目标的方式——比如只做上下拼接或左右拼接,不做四宫格拼接。另外,随机缩放的范围也要收窄,因为切片本身已经是在特定尺度下切的,过度缩放会破坏切片内目标的尺度一致性。

色彩抖动、随机翻转这些不影响空间结构的增强可以正常使用。特别是水平翻转和垂直翻转,对切片训练非常友好,因为它们不会引入新的边界截断问题。

4. 推理阶段的拼接与后处理:把碎片拼回完整画面

4.1 检测结果的坐标还原

推理阶段,每张切片经过检测器后会输出一组检测框,这些框的坐标是相对于切片的。要得到原图上的检测结果,需要把切片坐标加上切片在原图中的偏移量:

def restore_boxes(detections, patch_x, patch_y): """将切片坐标还原到原图坐标""" restored = [] for det in detections: x1, y1, x2, y2, conf, cls = det restored.append([x1 + patch_x, y1 + patch_y, x2 + patch_x, y2 + patch_y, conf, cls]) return restored

这一步逻辑很简单,但必须确保训练时的坐标映射和推理时的坐标还原是互逆操作。我见过有人训练时用的是中心点坐标加宽高的格式,推理时忘了转换,结果框全偏了。这种低级错误在调试时很浪费时间,建议在代码里把坐标格式统一成左上角加右下角,减少混淆。

4.2 跨切片重复检测的合并策略

由于切片之间有重叠区域,同一个目标可能在多张切片里都被检测到。这些重复检测必须合并,否则最终结果里一个目标会出现好几次。

最常用的合并方法是NMS(非极大值抑制),但直接对所有还原后的框做全局NMS有一个问题:不同切片里检测到的同一个目标,框的位置可能有细微偏差,IoU可能不到0.5,导致NMS无法把它们合并。这时候需要适当放低NMS的IoU阈值,比如从默认的0.5降到0.3甚至0.2。

但阈值降得太低又会误合并相邻的不同目标。我的经验是:先按类别分组,然后在每个类别内做NMS,IoU阈值设在0.3左右。如果发现相邻目标被误合并,可以引入中心点距离作为辅助判断——两个框的中心点距离小于某个阈值时才考虑合并。

另一种更精细的方法是加权框融合(WBF)。WBF不是简单地丢弃低分框,而是把多个重叠框的坐标做加权平均,得到一个更准确的框。在Stitcher场景下,WBF通常比NMS效果更好,因为同一个目标在不同切片里的检测框可以互相补充,融合后的框往往比任何单个框都更贴近真实位置。

def merge_detections(all_boxes, all_scores, all_classes, iou_thr=0.3): """按类别分组后做NMS合并""" final_boxes = [] final_scores = [] final_classes = [] for cls in set(all_classes): idx = [i for i, c in enumerate(all_classes) if c == cls] boxes = [all_boxes[i] for i in idx] scores = [all_scores[i] for i in idx] keep = nms(boxes, scores, iou_thr) for k in keep: final_boxes.append(boxes[k]) final_scores.append(scores[k]) final_classes.append(cls) return final_boxes, final_scores, final_classes

4.3 边界目标的特殊处理

边界目标是Stitcher里最棘手的问题。一个目标如果恰好落在两张切片的交界处,可能在两张切片里都只显示了一部分。如果两张切片里的部分检测都被保留,合并后可能得到一个形状奇怪的框;如果都被过滤掉,就漏检了。

我的处理策略是:对于靠近切片边缘的检测框,降低其置信度权重,但不直接丢弃。具体来说,如果一个框的中心点距离切片边界小于切片尺寸的5%,就把它当作边界框,在后续合并时给它一个较低的权重。这样,如果同一个目标在另一张切片里有完整的检测,完整检测会胜出;如果只有边界检测,它仍然有机会被保留。

另外,在切片生成阶段,我会有意让重叠区域覆盖所有可能的边界位置。由于步长是固定的,切割线的位置也是固定的,你可以预先计算出所有切割线的位置,然后检查标注数据里有多少目标跨越了这些切割线。如果某个区域的目标频繁被切割,可以考虑在那个区域局部增大重叠比例。

5. 实测效果与调参经验

5.1 不同切片参数下的精度对比

我在一个航拍车辆检测数据集上做过一组对比实验,原图分辨率约4000×3000,目标车辆在图中平均只占30×30像素左右。用YOLOv8s作为基座检测器,对比了直接推理和不同切片参数下的表现:

方案切片尺寸重叠比例mAP@0.5单图推理耗时
直接推理--0.41212ms
Stitcher-A64010%0.587145ms
Stitcher-B64020%0.623198ms
Stitcher-C51220%0.641267ms
Stitcher-D80015%0.598132ms

从这组数据能看出几个规律。第一,切片确实带来了大幅度的精度提升,mAP从0.412涨到0.6以上,提升超过50%。第二,切片尺寸越小,精度越高,但耗时也越长。512的切片比640的切片mAP高了不到0.02,但耗时多了35%。第三,重叠比例从10%提到20%,mAP涨了0.036,耗时增加了37%。这个交换比是否划算,取决于你的精度要求。

我个人的推荐是:切片尺寸640、重叠15%到20%,这是一个精度和速度比较平衡的配置。如果你的目标特别小(小于20像素),可以考虑降到512;如果目标在30到50像素之间,640到800都行。

5.2 切片数量与漏检率的非线性关系

有一个反直觉的现象值得注意:切片数量增加并不总是带来漏检率下降。当切片数量超过某个阈值后,漏检率反而可能上升。原因在于,切片越多,每张切片里的上下文越少,模型越容易把一些纹理误判成目标,同时合并阶段的错误也可能增加。

我实测发现,当切片数量超过60张时(对于4000×3000的图),漏检率开始趋于平缓,甚至略有回升。所以不要盲目追求小切片,要根据目标尺寸分布来定。一个实用的方法是:先统计训练集里所有目标的尺寸分布,找到目标尺寸的中位数,然后让切片尺寸大约是中位数的15到20倍。比如目标中位数是35像素,切片尺寸设在560到700之间比较合适。

5.3 与其它小目标增强方案的叠加效果

Stitcher和其它小目标增强手段并不互斥。我试过把Stitcher和以下几种方法叠加使用:

  • SAHI(Slicing Aided Hyper Inference):SAHI本质上也是一种切片推理策略,和Stitcher思路相近。两者叠加没有意义,选一个就行。
  • 高分辨率训练:把检测器的训练分辨率从640提到1280,再配合Stitcher切片,mAP能再涨0.02到0.03,但训练时间翻倍。
  • Copy-Paste增强:把小目标复制粘贴到图像的不同位置,增加小目标的样本数量。这个和Stitcher叠加效果不错,因为Stitcher解决了推理时的尺度问题,Copy-Paste解决了训练时的样本不足问题。
  • 注意力模块:在骨干网络里加CBAM或ECA等注意力模块,对切片内的小目标响应有微弱提升,但收益不如Stitcher明显。

综合来看,Stitcher + Copy-Paste增强是我目前用过性价比最高的组合。前者管推理,后者管训练,两者互补,不需要改网络结构,落地成本低。

6. 工程落地中的几个坑与应对

6.1 切片推理的批处理与显存控制

切片推理时,如果你把48张切片逐张送入模型,GPU利用率会很低,因为每张切片都很小,计算量不足以填满GPU。正确的做法是把切片组成batch,一次性送入。但batch size不能太大,否则显存会爆。

我的做法是:先估算单张切片的显存占用,然后根据GPU剩余显存动态设置batch size。比如单张640×640切片在YOLOv8s上推理约占200MB显存,8GB显存的卡留出2GB给系统,剩下6GB可以跑30张切片一个batch。这样48张切片分两个batch就能跑完,GPU利用率能到70%以上。

但要注意,不同尺寸的切片不能放在同一个batch里。如果你的切片策略产生了不同尺寸的切片(比如贴边切片可能小一点),需要按尺寸分组后再组batch。

6.2 多类别场景下的合并冲突

当检测器需要识别多个类别时,跨切片合并会变得更复杂。同一个位置在不同切片里可能被检测成不同的类别,这时候简单的NMS无法解决,需要引入类别置信度投票机制。

我的处理方式是:对于空间上重叠的多个检测框,如果它们的类别不同,就比较各类别的置信度之和,取置信度总和最高的类别作为最终类别。如果两个类别的置信度接近(差距小于0.1),就保留两个结果,让下游逻辑去决定。这种模糊情况在实际中很少见,但一旦出现,强行合并反而容易出错。

6.3 推理速度优化的几个实用手段

如果你的场景对速度有要求,但又不想放弃Stitcher的精度,可以试试这几个优化手段:

  • 跳过低置信度切片:先用一个轻量分类器判断切片是否包含目标,不包含的直接跳过检测。这个分类器可以非常小,推理耗时几乎可以忽略。
  • 降低重叠比例:从20%降到10%,切片数量减少约15%,精度损失通常在0.02以内。
  • 使用半精度推理:FP16推理能提速30%到50%,精度损失微乎其微。
  • 切片尺寸自适应:根据图像内容动态调整切片尺寸,目标密集的区域用小切片,目标稀疏的区域用大切片。

我在一个工业质检项目里用了前两个手段,把单图推理时间从200ms压到了120ms,mAP只掉了0.015,产线完全能接受。

6.4 训练与推理不一致导致的精度骤降

这是最容易踩的坑:训练时用的切片策略和推理时不一样。比如训练时切片尺寸是640、重叠128,推理时改成了512、重叠64,模型见到的目标尺度分布变了,精度会明显下降。

我的建议是:把切片参数写进配置文件,训练和推理共用同一份配置。如果推理时确实需要调整参数(比如换了个分辨率的相机),最好用新参数重新微调一下模型,至少跑几个epoch让模型适应新的尺度分布。

另外,训练时的切片生成逻辑和推理时的切片生成逻辑必须是同一套代码。我见过有人训练用Python脚本切,推理用C++重写了一遍,结果边界处理逻辑不一致,导致推理时边界目标大量漏检。这种问题排查起来非常痛苦,最好的办法就是从一开始就统一代码路径。

7. 一些个人体会

Stitcher这套方法我从去年开始在不同项目里反复用,最大的感受是:它不是什么黑科技,但胜在通用和可控。你不需要理解复杂的网络结构,也不需要调一堆超参数,只要把切片尺寸和重叠比例这两个参数调好,就能拿到稳定的精度提升。

但它也不是万能的。如果你的目标本身就不小(比如占据图像1/10以上面积),Stitcher带来的收益很有限,反而白白增加计算量。另外,如果图像分辨率本身就不高(比如1000×800以下),切片之后每张切片太小,上下文严重不足,效果也会打折扣。

我一般会在项目初期先跑一个基线,看看直接推理的mAP是多少。如果小目标的漏检率超过30%,我就会考虑上Stitcher。如果漏检率在10%以内,优先考虑其他更轻量的优化手段。

最后分享一个调参的小技巧:不要一次性把切片尺寸和重叠比例都定死,先固定重叠比例为15%,然后以64像素为步长调整切片尺寸,找到精度拐点后再微调重叠比例。这样比网格搜索快得多,通常两三组实验就能找到合适的配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询