ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

大图切块与小目标检测:从切图到结果合并的完整实践指南

大图切块与小目标检测:从切图到结果合并的完整实践指南 做目标检测的人迟早会遇到这么一类项目图特别大目标特别小。你拿着一张四千乘三千的航拍图里面要找的是十几个像素的车辆或屋顶直接丢进检测器要么显存不够跑不起来要么跑起来了但漏检漏到你怀疑人生。这个时候大多数人第一反应都一样把图拆开拆成四份、九份甚至更多每块单独训练、单独检测最后再把检测结果拼回原图坐标。这个思路看起来很朴素但真正落地的时候问题比想象中多得多子图切多大合适要不要重叠标注文件怎么跟着转换检测结果怎么映射回大图两个子图同时检测到同一个目标时要不要合并这些细节没有处理好切块不仅不会带来提升反而会让检测结果乱成一团。这篇内容我就把“大图拆块 小目标检测”这条路线从头到尾讲清楚包括方案设计、切图脚本、标注转换、推理合并、常见坑位以及什么时候该换用更进阶的做法。这些都是我实际跑过项目之后总结出来的不绕弯子直接说重点。1. 先弄明白为什么大图里的小目标总是漏检1.1 小目标到底有多“小”“小目标”在学术圈有个常用参考口径COCO数据集里把像素面积小于32×32的目标归为小目标。但实际项目里这个定义远远不够用。大图场景下的目标往往不是32×32而是更极端的情况一张5000×4000的遥感影像目标可能只有20×20像素甚至更小。按照“面积占整张图比例”来算这类目标大概只占全图面积的十万分之一。落到检测任务里小目标的核心难点有三层第一是像素信息少能用来区分的特征本身就非常有限第二是经过网络下采样之后特征进一步丢失到了高层特征图上可能只剩一两个像素点第三是正负样本比例悬殊一张大图里背景区域占绝大多数模型很容易被背景带偏。1.2 整图缩放对小目标很不友好很多人第一个想法是大图没法直接训练那就先缩放到模型输入尺寸比如640×640。这个操作的副作用非常直接。假设原图是4000×3000模型输入是640×480那么缩放比例大约是0.16。原图里一个20×20的目标缩放后变成大约3×3像素。3×3像素的物体任何一个检测器都很难区分它到底是目标还是噪点。更不用说网络内部还有多层stride为2的下采样经过几次池化或卷积步长之后这个小目标几乎完全从特征图里消失。这就像你把一张城市地图缩到手机屏幕大小想找到某条街道上的一个报亭根本不可能。但如果你把地图裁成四块分别放大显示报亭的细节就能看清楚。切块的本质就是“局部放大”人眼这种视觉机制放在检测器上同样成立。1.3 切块到底解决了什么切块不是玄学它本质上解决了三个问题。第一保留了目标的原始像素分辨率。目标在大图里是20×20切成子图后依然是20×20没有因为统一缩放而变小。第二提高了目标在输入图像中的相对占比。同样一个20×20的目标放在4000×3000的大图里占比极小但放在1000×750的子图里就清晰多了。第三把大图对显存和算力的压力拆散。单张4K图可能根本塞不进显卡但切出来的小块可以轻松进入模型前向计算。所以切块方案的核心价值在于通过空间划分把“大图小目标”问题转化成“小图相对大目标”问题。这个思路本身不难难的是转化过程中那些数据加工和结果合并的细节。2. 切块方案的整体设计先想清楚再动手2.1 切四块还是切N块标题里说的是“拆成四个”这也是最自然的直觉2×2网格简单干脆。但真上线前必须想清楚一个问题切四块之后子图尺寸是不是仍然远超模型输入尺寸。举个例子一块4000×3000的图切成2×2四份每份是2000×1500。如果你用的是640×480输入的检测器子图依然要缩放到640×480这轮缩放比例是0.32。相比整图缩放的0.16目标像素从3.2×3.2变成6.4×6.4确实有提升但对很多项目来说6.4像素依然不够。这种情况下切成3×3每块约1333×1000或4×4每块约1000×750反而更合理目标在缩放后的尺寸能达到9.6甚至12.8像素明显更利于检测。所以我的判断标准很简单先统计目标在原图中的典型尺寸再倒推子图尺寸。假设模型输入长边是S你希望小目标在经过切块和缩放后至少有8到16像素那么子图长边可以粗略按“S × 目标尺寸 / 期望像素”来估算。实际项目里不需要算得那么精确但“根据目标大小决定切块数量”这个思路一定要有而不是无脑四等分。2.2 子图尺寸和模型输入怎么对齐子图尺寸不要拍脑袋定至少要满足两个约束。第一个约束是模型有最佳输入范围。大多数检测器在某个分辨率区间内效果稳定比如YOLO系列常用640Faster RCNN常用800×800或1333×800。子图长边建议控制在模型输入尺寸的1到2倍之间。太大了缩放损失依然严重太小了又失去了切块的意义。第二个约束是子图的长宽比不要和模型输入差太多。比如把一张超长全景图切成1×4的长条每个长条宽度可能只有高度的四分之一resize进正方形输入时会发生严重形变或者为了保持比例而大量填充黑边这两种情况都会损伤检测精度。对于这类图应该按长宽比灵活调整网格而不是机械地切成四份。另外工程上的经验是如果原图尺寸只是模型输入的两倍以内而且目标不算太小可以先试不切块直接缩放训练很多时候也没问题。切块不是越多越好而是“刚好让目标在输入图上达到可检测尺寸”最好同时还要尽量保留目标周围的上下文信息。子图太碎目标周边环境信息丢失反而会增加误检。2.3 重叠率是很容易被忽略的参数完全没有重叠的切块会带来一个非常具体的问题目标正好落在切分边界上。比如一个宽40像素的目标横跨两个子图左半边在块A右半边在块B两块里都只有一个残缺的目标模型很难正确识别。更极端的情况是目标的位置恰好落在四条边界交汇点四个子图各占一小块哪边都认不出来。解决办法就是切块时留重叠区域。重叠率通常取子图尺寸的10%到25%同时考虑目标最大尺寸重叠宽度至少覆盖目标最大边的一半以上。我的经验是普通场景下重叠64到128像素比较稳妥目标较大的场景重叠区域要更大。比如目标最大尺寸是100像素那重叠至少留50到100像素否则一个目标很可能同时出现在两个子图里或者在所有子图里都是残缺的。训练阶段和推理阶段的重叠处理方式稍有不同。训练时建议一个目标的标签只分配给目标中心点所在的子图避免同一个目标在多个子图里被重复学习推理时则不同所有子图都正常检测重叠区域里的同一个目标可能被多个子图同时检出来这时候靠后处理NMS去合并。3. 完整实操从切图到合并检测结果3.1 一次讲清切图脚本我这里按最常见的YOLO格式来演示切图流程因为目前工程里YOLO系列的普及率最高。假设每张原图对应一个同名的txt标签文件每行是“类别 cx cy w h”坐标都是相对于原图宽高的归一化值。切图脚本的核心逻辑如下确定网格行列数、重叠像素然后遍历每个网格位置用OpenCV切出子图同时把落在该子图内的目标标签一并转换写入新文件。import cv2 import os import numpy as np def slice_image(img_path, label_path, out_img_dir, out_label_dir, grid_rows2, grid_cols2, overlap64): img cv2.imread(img_path) H, W img.shape[:2] # 计算步长最后一行/列做边界对齐 step_h max(1, (H - overlap) // grid_rows) step_w max(1, (W - overlap) // grid_cols) # 读取标签 boxes [] with open(label_path, r) as f: for line in f.readlines(): parts line.strip().split() cls int(parts[0]) cx, cy, w, h map(float, parts[1:]) # 转成绝对像素坐标 x1 (cx - w / 2) * W y1 (cy - h / 2) * H x2 (cx w / 2) * W y2 (cy h / 2) * H boxes.append([cls, x1, y1, x2, y2]) for r in range(grid_rows): for c in range(grid_cols): y1 int(r * step_h) x1 int(c * step_w) y2 min(y1 step_h overlap, H) x2 min(x1 step_w overlap, W) # 边界块向左上对齐保证大小基本一致 y1 max(0, y2 - (step_h overlap)) x1 max(0, x2 - (step_w overlap)) sub_img img[y1:y2, x1:x2] sub_h, sub_w sub_img.shape[:2] # 保存子图 base_name f{os.path.splitext(os.path.basename(img_path))[0]}_{r}_{c} cv2.imwrite(os.path.join(out_img_dir, base_name .jpg), sub_img) # 过滤落在当前子图内的目标 new_lines [] for cls, bx1, by1, bx2, by2 in boxes: # 中心点是否在该子图内 bcx (bx1 bx2) / 2 bcy (by1 by2) / 2 if x1 bcx x2 and y1 bcy y2: # 裁剪到子图范围内 nx1 (bx1 - x1) / sub_w ny1 (by1 - y1) / sub_h nx2 (bx2 - x1) / sub_w ny2 (by2 - y1) / sub_h nx1 min(max(nx1, 0), 1) ny1 min(max(ny1, 0), 1) nx2 min(max(nx2, 0), 1) ny2 min(max(ny2, 0), 1) ncx (nx1 nx2) / 2 ncy (ny1 ny2) / 2 nw nx2 - nx1 nh ny2 - ny1 new_lines.append(f{cls} {ncx:.6f} {ncy:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(out_label_dir, base_name .txt), w) as f: f.write(\n.join(new_lines))上面代码里最关键的一步是“目标中心点在该子图内”这个条件。中心点落在哪个块就把目标完整分配给那个块。这样能避免同一个目标被多个子图重复学习也能保证至少有一个子图包含完整目标。当然如果你的场景里目标特别密集每个目标都可能跨多个块也可以按“与子图的交并比大于阈值”来分配但那种情况下标签信息会出现冗余需要更小心的后处理。3.2 标注转换千万别算错标注转换是切块里最容易出错的一步坐标算错会导致检测框偏移、尺寸异常甚至模型训练直接崩掉。出错的原因几乎都是“归一化坐标”和“绝对像素坐标”混用。转换逻辑其实很清晰。第一步把原图标签里的归一化坐标还原成原图上的绝对像素坐标第二步判断目标中心是否落在当前子图内第三步把绝对像素坐标减去子图的左上角偏移量变成子图内的绝对坐标第四步再除以子图的宽高转成子图内的归一化坐标。举个例子原图宽4000一个目标的中心cx是0.5那么原图上绝对中心x是2000。如果子图左上角x偏移是1000子图宽是2000那么该目标在子图内的归一化cx就是(2000-1000)/2000 0.5。这个数看起来好像没变但如果你忘了减偏移量直接拿2000除以2000就会得到1.0检测框被硬生生推算到子图最右边位置就错了。切图完成之后我强烈建议做一次可视化校验。用脚本把转换后的标签画到子图上人工抽查几十张看看框和目标是否对齐。这一步花不了几分钟但能避免你在训练了好几天之后才发现数据有问题。我见过太多项目模型效果起不来最后查了半天原因就是标签坐标映射错了一个偏移量。3.3 训练阶段的配置调整切块完成后训练数据集的大小会变成原来的N倍切N块就近似N倍。这种情况下有几个参数需要调整。首先是迭代轮次。数据量翻了几倍如果还保持原来的epoch数训练步数会大增。一般可以适当降低epoch或者用步数来控制训练长度。其次是学习率数据量变大后一个epoch内的step数量也变多了学习率调度策略要跟着调整尤其是余弦退火的周期。更重要的是切块后的子图可能有很多完全没有目标。这些空块需要处理策略。如果全部保留负样本比例会严重失衡模型容易偏向“什么都检测不到”。我的做法是过滤掉大部分空块但保留一小部分比如总量的10%到20%作为难负例尤其在推理阶段会检测全图的场景下保留一定空块能让模型学会抑制背景区域的误检。还有一个很容易踩的坑是数据增强的适配。常规的随机裁剪增强在切块场景下要小心因为子图本身已经很小再随机裁剪可能把唯一的目标裁掉。我建议优先使用翻转、旋转、色彩扰动、马赛克增强裁剪类增强要克制或者使用带目标约束的裁剪。3.4 推理阶段坐标映射与NMS合并推理时先对测试大图做和训练时完全相同的切块处理相同网格数、相同重叠率、相同子图尺寸。然后对每块子图单独做检测拿到检测结果后把所有框映射回原图坐标。假设模型在子图上输出的归一化框为(cx, cy, w, h)子图左上角在原图中的坐标是(off_x, off_y)子图宽高是(sub_w, sub_h)原图宽高是(orig_w, orig_h)那么映射公式如下def map_box_to_original(cx, cy, w, h, off_x, off_y, sub_w, sub_h, orig_w, orig_h): abs_cx cx * sub_w off_x abs_cy cy * sub_h off_y abs_w w * sub_w abs_h h * sub_h orig_cx abs_cx / orig_w orig_cy abs_cy / orig_h orig_w_ratio abs_w / orig_w orig_h_ratio abs_h / orig_h return orig_cx, orig_cy, orig_w_ratio, orig_h_ratio映射完之后所有子图的框都到了同一张原图坐标系里这时候需要做全局NMS。因为重叠区域的存在同一个目标可能被相邻两个子图各检测一次产生两个高度重叠的框。NMS的作用就是保留置信度更高的框抑制重复框。NMS要注意两点一是按类别做不同类别的框不要互相抑制二是建议加一个比较温和的IOU阈值比如0.5到0.6。如果场景里目标本身密集阈值可以放宽到0.4避免把相邻的不同目标误合并。实际项目里还有一种特殊情况目标在切分边界上中心点所在的子图检测出了完整框但隔壁子图由于看到了残缺目标也可能输出一个位置偏移的半截框。这两个框的IOU不一定很高普通NMS可能合并不掉。遇到这种情况可以先检查重叠率是否足够如果问题频繁出现可以考虑放宽合并条件比如对同一类别且中心距离小于某个像素阈值的框做合并处理。4. 常见问题与避坑实录4.1 检测框位置偏移、尺寸不对这类问题十有八九出在标注转换或推理映射环节。最常见的错误包括减偏移量时减错方向、归一化时除以了原图尺寸而不是子图尺寸、推理映射时忘了加上子图偏移。排查方法很简单写一个可视化脚本把检测结果画在原图上对着问题图看。如果所有框都偏移到右上方向那基本是偏移量计算错误如果框的宽高整体偏大或偏小那多半是缩放系数弄混了。我自己的习惯是每次改完切图或推理脚本先跑一张图把“子图检测框”和“原图映射框”同时画出来核对一遍确认无误再批量跑。4.2 目标被切到边界漏检或重复如果训练后模型对跨边界目标的检测效果明显偏差先检查切图重叠率。没有重叠或重叠太少跨边界目标就会变成“两边都只剩一半”的残缺样本。增大重叠率让任何目标至少完整出现在一个子图里是最直接的解决办法。重复检测的处理主要靠NMS但如果发现同一目标稳定地被输出两个不完全重叠的框说明目标经常横跨多个子图并且多个子图都检测到了。这时可以回到标签分配逻辑检查训练时是否也应该采用“每个目标只分配给中心点所在块”的策略。另外对边界框的置信度做位置补偿也是一种思路给距离子图中心越近的框越高的置信度权重但这会引入额外超参数一般不作为首选。4.3 空块太多导致训练失衡大图场景通常目标稀疏切块后一半以上的子图可能都是空块。把空块全部喂进训练集会导致模型训练时大部分时间都在看背景整体收敛变慢甚至可能出现“全部输出零检测框”的退化结果。我的建议是先统计块内目标数量保留所有含目标的正样本块对于空块随机保留一部分数量控制在正样本块数量的三分之一以内。如果任务本身误检率要求很高可以适当多留如果更看重召回率可以少留甚至不留。这个比例需要根据验证集的实际表现微调。4.4 训练时间、显存与I/O成本切块后数据量膨胀训练I/O会成为新的瓶颈。几千张原图切成几万个子图如果每次都从磁盘随机读小文件训练速度会慢得让人崩溃。可行的方案有两个。第一个是离线切图把切好的子图直接保存到本地再配合数据加载器做缓存或预取简单直接缺点是占用磁盘空间。第二个是在线切图在数据加载阶段动态从原图切块优点是灵活可以随机调整切块偏移来增加数据多样性缺点是每次训练都可能重复计算需要做好缓存。显存方面切块后的子图尺寸变小单张进入显存基本没问题瓶颈反而转移到了数据读取和增强的CPU占用上建议加大num_workers或者用内存映射方式读取大图。5. 什么时候切块不够用进阶扩展思路5.1 推理时切片也是一条路训练时切块是最稳的方案但不是唯一的选择。还有一种做法是训练时整图训练推理时再把大图切片分别检测业内叫SAHI切片辅助推理已经有现成的开源库可以快速实验。训练时切片的好处是让模型在训练阶段就适应小图视角推理时切片的好处是训练数据可以保持原始上下文模型看到的目标更完整。实际操作中前者通常更稳因为训练和推理的数据分布一致不容易出现尺度不一致导致的性能下降。如果你暂时不想改动训练流程直接拿SAHI在测试阶段做切片推理也是一个低成本验证方案。5.2 切块、多尺度与高分辨率特征层互补切块本质上是数据层面的尺度放大它和多尺度训练、特征金字塔这些策略并不冲突可以组合使用。多尺度训练能增加模型对目标尺寸变化的鲁棒性但不会解决“目标在输入图上只有3个像素”的根本问题。切块先解决了这个底层问题多尺度再在子图基础上去增强多样性两者配合效果更好。另外很多检测器的特征金字塔默认从P3层开始小目标特征到了P3已经损失不少可以考虑使用带P2层的检测头让高分辨率特征图直接参与到小目标预测。这类改进在切块方案之后通常还能带来几个点的提升。5.3 超分重建与切块结合如果目标实在太小切块加放大之后仍然不够识别可以考虑对局部区域做超分辨率重建先放大再检测。比如先通过一个快速筛选器找到可能包含目标的子图区域对这些区域做超分再进入精细检测器。这个方案的代价是推理链路变长、耗时增加适合离线处理或对召回率要求极高但对实时性要求不高的场景。我尝试过在部分遥感项目里用这个组合小目标的召回率确实有提升但速度会慢好几倍是否值得需要根据业务具体权衡。5.4 部署阶段的性能注意点切块方案上线部署时计算量会变成原来的N倍这是一个经常被忽略的成本。比如4张子图推理次数就是原来的4倍如果再叠加多尺度或TTA计算量可能膨胀到十几倍。部署时可以做两件事优化第一重叠区域的卷积特征可以缓存复用但通常实现复杂收益不一定明显第二多个子图可以放到同一个batch里并行推理比逐张推理更高效。另外前后处理的逻辑要封装成独立模块输入一张大图输出完整坐标的检测结果对上层应用屏蔽切块细节这样后续切换算法或调整切块参数时不会影响业务流程。最后几句实在话切块训练不是银弹但在大图小目标场景里它确实是性价比最高、最容易落地的一步。我做过的一个遥感影像检测项目目标像素只有二三十像素原图四千多像素宽最初直接缩放训练mAP惨不忍睹后来改成切块加重叠什么网络结构都没动mAP直接涨了十几个点。这个提升几乎全部来自数据预处理层面的尺度还原。如果你正准备上手这类项目我的建议是动手前先花半天统计目标尺寸和分布想清楚切多少块、子图多大、重叠多少切完图做好可视化校验确认标签没有出错训练和推理的切块参数保持一致后处理NMS别偷懒。这几件事做扎实了大图小目标的检测基本就成功了一大半。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表