news 2026/10/17 18:16:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大图切块与小目标检测:从切图到结果合并的完整实践指南

做目标检测的人,迟早会遇到这么一类项目:图特别大,目标特别小。你拿着一张四千乘三千的航拍图,里面要找的是十几个像素的车辆或屋顶,直接丢进检测器,要么显存不够跑不起来,要么跑起来了但漏检漏到你怀疑人生。这个时候大多数人第一反应都一样:把图拆开,拆成四份、九份甚至更多,每块单独训练、单独检测,最后再把检测结果拼回原图坐标。

这个思路看起来很朴素,但真正落地的时候,问题比想象中多得多:子图切多大合适?要不要重叠?标注文件怎么跟着转换?检测结果怎么映射回大图?两个子图同时检测到同一个目标时要不要合并?这些细节没有处理好,切块不仅不会带来提升,反而会让检测结果乱成一团。

这篇内容,我就把“大图拆块 + 小目标检测”这条路线从头到尾讲清楚,包括方案设计、切图脚本、标注转换、推理合并、常见坑位,以及什么时候该换用更进阶的做法。这些都是我实际跑过项目之后总结出来的,不绕弯子,直接说重点。

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系列常用640,Faster 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_rows=2, grid_cols=2, overlap=64): 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别偷懒。这几件事做扎实了,大图小目标的检测基本就成功了一大半。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 18:13:57

Codex/Claude Code 集成 Antigravity:Skill 封装与配额管理实战

1. 项目缘起与整体设计思路1.1 这个项目到底在解决什么问题做 AI 辅助编程这一块的朋友&#xff0c;最近应该都有一个共同的体感&#xff1a;模型能力越来越强&#xff0c;但配额限制越来越让人难受。尤其是当你习惯了某个特定模型的代码生成质量之后&#xff0c;突然被限流、被…

作者头像 李华
网站建设 2026/10/17 18:13:53

大模型落地医疗的实践:人机共智与RAG架构设计

医疗信息化圈子里&#xff0c;最近大半年聊得最多的&#xff0c;必然是“大模型在医疗场景里到底怎么落地”。很多人第一反应是做个对话机器人&#xff0c;可真放进生产环境才发现&#xff0c;聊天只是最外层的东西&#xff0c;真正的难点是把它接入到门诊、住院、随访的主流程…

作者头像 李华
网站建设 2026/10/17 18:13:17

GitHub热榜项目怎么读?从日榜趋势到源码速读的技术选型方法论

1. 日榜背后的信息筛选逻辑每天早上刷热榜&#xff0c;大概是很多开发者的固定动作。但说实话&#xff0c;大部分人刷热榜的方式是错的——点开前三个&#xff0c;扫一眼README&#xff0c;关掉&#xff0c;然后什么都没记住。我自己也经历过这个阶段&#xff0c;直到有段时间需…

作者头像 李华
网站建设 2026/10/17 18:13:17

开源大模型本地部署实战:Gemma 4安装配置与推理优化全攻略

最近开源大模型圈子里热度最高的风向&#xff0c;就是本土化部署浪潮。我自己前后折腾过好几个大模型&#xff0c;从最初的C推理框架到现在的轻量化部署方案&#xff0c;说实话&#xff0c;每次踩坑记录都够写半本书了。最近我把Gemma 4成功跑在了自己的工作站上&#xff0c;整…

作者头像 李华
网站建设 2026/10/17 18:13:10

AI PR门禁实战:三步接入GitHub Actions实现低成本自动化审查

一个周五的下午&#xff0c;某跨平台系统项目的发布分支要冻结了&#xff0c;结果一个PR因为把NODE_ENV写死成了production卡在审查环节。写代码的同学觉得没问题&#xff0c;reviewer 轮值到快下班也没细看&#xff0c;差点就这么合进去。后来是灰度环境冒烟测试才炸出来。这种…

作者头像 李华