ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SAHI 切片推理结果合并(Combine)完全指南:NMS、NMM 与 GreedyNMM 原理、后端调度与实战调优

SAHI 切片推理结果合并(Combine)完全指南:NMS、NMM 与 GreedyNMM 原理、后端调度与实战调优
  • 计算机视觉
  • 人工智能

【免费下载链接】sahi

Framework agnostic sliced/tiled inference + interactive ui + error analysis plots

项目地址:https://gitcode.com/gh_mirrors/sa/sahi
点击查看免费下载

本篇技术指南以 docs/zh/postprocess/combine.md 为骨架,系统讲解 SAHI 在切片推理(Sliced Inference)之后如何通过sahi.postprocess.combine模块合并或抑制重复检测结果。你将掌握nms、greedy_nmm、nmm三组函数级 API 与NMSPostprocess、NMMPostprocess、GreedyNMMPostprocess类级 API 的完整用法,理解 IOU/IOS 两种匹配指标的差异,学会通过get_sliced_prediction的后处理参数与 numpy/numba/torchvision 后端调度进行实战调优,并能读懂底层源码与测试用例。

一、为什么切片推理需要"合并"后处理

SAHI 的核心工作流是"切图 → 逐切片推理 → 结果映射回原图 → 合并去重"。由于相邻切片之间存在重叠区域(如overlap_height_ratio=0.2、overlap_width_ratio=0.2),同一个目标物体通常会在多个切片中被重复检测到,产生大量近乎重合的边界框。此外perform_standard_pred=True时还会额外对整幅图像做一次标准推理,进一步叠加重复检测。

因此get_sliced_prediction在收集完所有切片的预测后,必须执行一次后处理来消除重复。这一步骤正是由 sahi/postprocess/combine.py 中的后处理策略完成的。从源码看,get_sliced_prediction会在推理结束后调用postprocess(object_prediction_list)(见 sahi/predict.py),把全部检测结果压缩为最终输出。

二、核心 API 一览:函数级与类级

sahi.postprocess.combine同时提供两类接口:直接操作(N, 6)numpy 数组的函数级 API,以及操作ObjectPrediction对象列表的类级 API。

2.1 统一的数组约定

所有函数级 API 共享同一输入约定:形状为(N, 6)的 numpy 数组,列顺序为[x1, y1, x2, y2, score, category_id],其中坐标采用xyxy(左上角 + 右下角)格式。这一约定由 sahi/postprocess/utils.py 中的object_prediction_list_to_numpy定义——它会把ObjectPrediction的bbox.to_xyxy()、score.value和category.id依次填入数组。

2.2 函数级 API:nms / greedy_nmm / nmm 及其 batched 变体

combine 模块共导出 6 个函数,按语义分为"抑制型"(返回保留索引列表)与"合并型"(返回保留索引到被合并索引的映射):

函数语义返回值类型
nms(predictions, match_metric="IOU", match_threshold=0.5)非极大值抑制:保留重叠框中分数最高的,丢弃其余list[int](按分数降序的保留索引)
batched_nms(...)按类别分别执行 NMSlist[int]
greedy_nmm(...)贪心非极大值合并:每个保留框只合并与其直接重叠的框dict[int, list[int]]
batched_greedy_nmm(...)按类别分别执行贪心 NMMdict[int, list[int]]
nmm(...)非极大值合并(传递式):A 合并 B、B 合并 C 时,A 会获得全部三个框dict[int, list[int]]
batched_nmm(...)按类别分别执行传递式 NMMdict[int, list[int]]

直接使用示例(源自 docs/zh/postprocess/backends.md):

import numpy as np from sahi.postprocess.combine import nms, batched_nms, greedy_nmm, nmm, batched_greedy_nmm predictions = np.array([ [100, 100, 200, 200, 0.95, 0], [105, 105, 205, 205, 0.80, 0], [300, 300, 400, 400, 0.90, 1], ]) # 全局 NMS:所有类别一起参与比较 keep = nms(predictions, match_metric="IOU", match_threshold=0.5) print(predictions[keep]) # 按类别执行 NMS:类别 0 与类别 1 分别独立处理 keep = batched_nms(predictions, match_metric="IOU", match_threshold=0.5) print(predictions[keep]) # 贪心 NMM:{kept_index: [merged_index, ...], ...} keep_to_merge = greedy_nmm(predictions, match_metric="IOU", match_threshold=0.5) # 完整 NMM:传递式合并 keep_to_merge = nmm(predictions, match_metric="IOU", match_threshold=0.5) # 按类别执行贪心 NMM keep_to_merge = batched_greedy_nmm(predictions, match_threshold=0.5)

实现细节(sahi/postprocess/combine.py):nms/greedy_nmm/nmm三个"全局"函数直接通过后端分发执行;batched_*变体则经由_batched_apply(combine.py)按category_id分组,对每个类别独立调用对应函数,再把局部索引映射回全局索引。注意batched_greedy_nmm与batched_nmm对空输入返回{},batched_nms对空输入返回[](有专门的边界测试覆盖,见下文)。

2.3 类级 API:PostprocessPredictions 及其子类

类级 API 直接与 SAHI 的ObjectPrediction列表集成,是get_sliced_prediction内部使用的形态。所有类继承自抽象基类PostprocessPredictions(combine.py),构造函数统一接收三个参数:

参数类型默认值含义
match_thresholdfloat0.5判定两个预测"匹配"的最小重叠度(IoU 或 IoS)
match_metricstr"IOU"重叠指标,"IOU"或"IOS"
class_agnosticboolTrue为True时跨类别统一后处理;为False时按类别独立处理

四个具体实现:

  • NMSPostprocess:保留最高分框、丢弃重叠框,不合并边界框或掩码(combine.py);
  • NMMPostprocess:传递式非极大值合并,会合并边界框、掩码与分数(combine.py);
  • GreedyNMMPostprocess:贪心版本,每个保留框只合并与其直接重叠的框,无传递合并,速度更快、合并出的框更紧凑(combine.py);
  • LSNMSPostprocess:基于lsnms包的空间索引 NMS,适合大量预测,仅支持 IOU 指标,且标注为实验性、不建议生产使用(combine.py)。

类级用法示例:

from sahi.postprocess.combine import NMSPostprocess, NMMPostprocess, GreedyNMMPostprocess # NMS:保留最佳边界框,丢弃其余 postprocessor = NMSPostprocess( match_threshold=0.5, match_metric="IOU", class_agnostic=True, # False 表示按类别处理 ) filtered = postprocessor(object_prediction_list) # 贪心 NMM:合并重叠边界框(速度快) postprocessor = GreedyNMMPostprocess(match_threshold=0.5) merged = postprocessor(object_prediction_list) # 完整 NMM:传递式合并 postprocessor = NMMPostprocess(match_threshold=0.5) merged = postprocessor(object_prediction_list)

传入class_agnostic=False后,每个后处理器都会按类别独立运行,因此"car"的预测不会抑制"person"的预测。需要安装LSNMSPostprocess时执行pip install lsnms>0.3.1;若match_metric设为"IOS",该类会抛出NotImplementedError。

三、三种合并策略的语义差异

理解三种策略的区别是调参的前提:

  1. NMS(抑制):按分数从高到低遍历,保留当前最高分框,把与之重叠度 ≥ 阈值的所有框标记为被抑制并丢弃。最终输出是一组"互不重叠"的独立检测框。
  2. GreedyNMM(贪心合并):同样按分数遍历,但重叠框不是被丢弃,而是被合并进保留框。合并后的边界框变为两框的最小外接包围盒(union),分数取两者最大值,类别取分数更高的一方。由于只考虑直接相邻的框,它不会把"间接重叠"的检测连成一条。
  3. NMM(传递合并):在贪心的基础上支持传递性——如果 A 与 B 重叠、B 与 C 重叠,即使 A 与 C 不直接重叠,三者也会被合并为一组。这在目标被切成多个碎片、且碎片之间形成链式重叠时非常有用。

从源码看,合并的具体规则由 sahi/postprocess/utils.py 中的merge_object_prediction_pair定义:边界框取 union(calculate_box_union)、分数取最大值(get_merged_score)、类别取分数较高的预测(get_merged_category)、掩码则做几何并集(get_merged_mask,通过 shapely 的buffer(0)修复无效多边形后取union)。

需要特别注意的是匹配阈值是包含边界的:源码中使用metric >= match_threshold判定匹配。测试 tests/test_combine.py 专门验证了"重叠度恰好等于阈值 0.5 的框会被合并而不是被丢弃"这一行为。

四、指标选择:IOU 与 IOS

NMS 与 NMM 都支持两种重叠指标:

  • IOU(Intersection over Union):交集面积 / 并集面积。标准指标,合并行为相对保守。两个完全一样的框 IoU=1.0,不重叠则为 0。
  • IOS(Intersection over Smaller):交集面积 /较小框面积。当目标尺寸差异悬殊时非常实用——例如一个小框完全落在一个大框内部,IoU 可能只有约 1%(交集/大框面积),远低于 0.5 阈值,两者都会保留;而 IoS 是 100%(交集/小框面积),小框会被正确抑制或合并。

测试用例 TestIOSMetric 用[0,0,100,100]的大框与[10,10,20,20]的小框精确复现了这一差异:IOU下两个框都保留(len(keep_iou) == 2),IOS下小框被抑制(len(keep_ios) == 1)。

调用方式:

# IOS 指标:一个框远小于另一个框时更激进 keep = nms(predictions, match_metric="IOS", match_threshold=0.5)

正因如此,get_sliced_prediction的默认postprocess_match_metric是"IOS"(见 sahi/predict.py),因为切片推理产生的重复框往往是"同一目标被部分切片切到",尺寸差异较大,IOS 能更彻底地消除重复。

五、与 get_sliced_prediction 的集成

在完整推理流程中,你不需要手动构造后处理器——通过get_sliced_prediction的后处理参数即可控制合并行为(sahi/predict.py):

参数默认值说明
postprocess_type"GREEDYNMM"可选"NMS"、"NMM"、"GREEDYNMM"、"LSNMS"
postprocess_match_metric"IOS""IOU"或"IOS"
postprocess_match_threshold0.5匹配阈值,值越小合并越积极
postprocess_class_agnosticFalse为True时后处理忽略类别 ID
force_postprocess_typeFalse为True时禁用下述自动切换逻辑

完整示例:

from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="ultralytics", model_path="yolo26n.pt", confidence_threshold=0.25, device="cuda:0", ) result = get_sliced_prediction( "image.jpg", detection_model, slice_height=512, slice_width=512, overlap_height_ratio=0.2, overlap_width_ratio=0.2, postprocess_type="GREEDYNMM", # 默认 postprocess_match_metric="IOS", # 默认 postprocess_match_threshold=0.5, postprocess_class_agnostic=False, )

get_sliced_prediction内部通过POSTPROCESS_NAME_TO_CLASS注册表(predict.py)把字符串映射为后处理类,并实例化后应用到预测列表。源码中还有两条自动切换逻辑,值得注意(predict.py):

  1. 低置信度保护:当模型confidence_threshold < 0.1(常量LOW_MODEL_CONFIDENCE,见 predict.py)且未强制指定后处理类型时,会把postprocess_type强制切换为"NMS"、指标切换为"IOU"并打印警告。原因是低置信度下会产生大量小分数框,合并类后处理容易把噪声也合并进来。
  2. OBB 限制:当检测模型是UltralyticsDetectionModel且输出为旋转框(is_obb)时,强制使用"NMS"——旋转框目前只支持 NMS 抑制。

若不想被上述逻辑干扰,可传入force_postprocess_type=True。此外,在低内存场景可设置merge_buffer_length:当收集到的预测超过该长度时,会先执行一次后处理(predict.py),实现流式合并,代价是可能轻微影响 AP。

命令行(CLI)同样暴露了这些参数,参见 docs/zh/cli.md:--postprocess_type GREEDYNMM(默认)、--postprocess_match_metric IOS(默认)、--postprocess_match_threshold 0.5、--postprocess_class_agnostic。

六、后端调度:numpy / numba / torchvision

NMS/NMM 的计算可以在三种可互换的后端上运行,选择取决于硬件与已安装的包(详见 后处理后端):

后端适用场景额外依赖
numpy仅 CPU,预测结果数量较少或中等无(始终可用)
numbaCPU 且预测结果数量较多;首次调用约 1 秒 JIT 预热,之后较快pip install numba
torchvisionCUDA 或 Apple MPS GPU 可用;处理大型批次时最快pip install torch torchvision

默认采用自动检测(sahi/postprocess/backends.py),优先级为:torchvision(已安装且存在 CUDA 或 Apple MPS GPU)→ numba(已安装)→ numpy(兜底)。解析结果会被缓存,可通过以下方式查看或强制指定:

from sahi.postprocess.backends import get_postprocess_backend, set_postprocess_backend # 检查解析出的后端(触发自动检测) print(get_postprocess_backend()) # 首次执行后处理操作前为 "auto" # 强制使用纯 numpy(无额外依赖,可在所有环境中运行) set_postprocess_backend("numpy") # 强制使用 numba JIT set_postprocess_backend("numba") # 强制使用 torchvision GPU set_postprocess_backend("torchvision") # 恢复自动检测 set_postprocess_backend("auto")

该调用会影响当前进程中后续所有NMS/NMM 操作,包括get_sliced_prediction内部触发的操作。从源码看,set_postprocess_backend还会清空 combine 模块的_dispatch_cache(backends.py),确保下一次调用重新解析函数。

combine.py的分发机制是一个两级查表(combine.py):_BACKEND_MODULE_MAP把后端名映射到模块路径(sahi.postprocess._numpy_backend、sahi.postprocess._numba_backend、sahi.postprocess._torchvision_backend),_FUNC_NAME_MAP再把操作类型(nms/greedy_nmm/nmm)映射到具体函数名,最终通过importlib.import_module动态加载,并按(backend, func_type)缓存结果。

七、底层实现原理(源码级)

7.1 从 ObjectPrediction 到 (N, 6) 数组

类级后处理器先把ObjectPrediction列表包装为ObjectPredictionList(utils.py),调用tonumpy()转为(N, 6)数组,再交给函数级 API;得到保留/合并索引后,用object_prediction_list[keep]按索引取回预测对象。ObjectPredictionList支持整数、列表以及"类张量"(tolist()可调用的对象)作为索引,兼容 numpy 数组与 torch 张量返回的索引。

7.2 密集矩阵、稀疏路径与流式路径

numpy 后端(sahi/postprocess/_numpy_backend.py)默认用向量化的广播计算N×N两两重叠矩阵compute_metric_matrix(L36-L68),超过_MAX_FULL_MATRIX = 8000时按行分批计算以控制中间变量内存。随后在矩阵上跑贪心循环:NMS 用nms_from_matrix(L227-L248)、Greedy NMM 用greedy_nmm_from_matrix(L251-L288)、NMM 用nmm_from_matrix(L291-L367,预先构建"支配关系"矩阵实现传递合并记账)。

对于大规模输入,sahi/postprocess/_sparse_backend.py 提供了更省内存的路径:SPARSE_MIN_BOXES = 2000表示低于该数量时密集矩阵更划算(STRtree 的构建成本超过省下的矩阵);当平均邻居数超过NMM_MAX_DEGREE = 40、SURVIVOR_MAX_DEGREE = 150时改用流式读取(L26-L39)。稀疏路径用 shapelySTRtree只求"是否重叠"的邻接关系,把峰值内存从O(N²)降到O(N + max_degree)。

另一个优化是阈值归零特例:当match_threshold <= 0时,任意两框都满足metric >= threshold,邻接图成为完全图,直接走nms_match_all/greedy_nmm_match_all/nmm_match_all等快速路径(L124-L196),无需计算任何重叠矩阵。

7.3 确定性的分数排序与 tie-break

所有后端在按分数排序时都使用_score_tiebreak_order(L97-L121):通过np.lexsort((y2, x2, y1, x1, -scores))先按分数降序、分数相同时再按框坐标确定顺序,保证结果可复现。测试 test_nms_equal_scores_deterministic 验证了等分场景下两次调用的结果完全一致。

7.4 torchvision 后端的混合策略

torchvision 后端(sahi/postprocess/_torchvision_backend.py)并非把所有逻辑都搬上 GPU:IOU指标下的 NMS 直接调用torchvision.ops.nms原生 CUDA 内核(L86-L92);而 IOS 指标与 NMM 系列则先在 GPU 上用 torch 计算重叠矩阵(_compute_metric_matrix_torch),再把矩阵拷回 CPU,复用 numpy 后端的nms_from_matrix/greedy_nmm_from_matrix/nmm_from_matrix合并逻辑。torchvision 后端在输入过大时同样会回退到 numpy 的稀疏路径。

八、测试与质量保证

tests/test_combine.py 是理解本模块行为的最佳入口,覆盖了以下关键场景:

  • 边界情况:空输入(batched_*返回{}或[])、单框输入、完全相同框、完全不重叠框、等分确定性(L58-L140);
  • IOS 指标:小框完全在大框内时 IOS=1.0 被抑制而 IOU<1.0 被保留,以及 batched IOS 的按类别行为(L148-L218);
  • 合并语义:NMM 的传递合并映射、阈值恰好等于匹配度时框被合并而非丢弃(L250-L368);
  • 后端一致性:TestNumbaBackend断言 numba 与 numpy 在固定样例、IOS 指标、随机数据(50 个框)上的输出完全一致(L536-L618);TestTorchParity断言 torch 张量输入与 numpy 输入的结果一致(L626-L667);
  • 后端注册表:set_postprocess_backend的取值校验(非法值抛ValueError)、缓存失效、CUDA/MPS 自动检测到 torchvision 等(L376-L451)。

这意味着无论选择哪个后端,你得到的后处理结果在语义上是一致的,切换后端只影响性能不影响正确性。

九、实战建议与常见问题

  1. 阈值怎么调:postprocess_match_threshold控制合并/抑制的积极程度。值越小合并越多(0 时甚至所有框合并为一组),值越大保留的独立框越多。默认 0.5 对大多数场景适用;若发现同一目标被切成多个碎片框,可适当调低(如 0.3);若发现不同目标被错误合并,则调高。
  2. GreedyNMM vs NMM:默认GREEDYNMM速度更快、合并框更紧凑;当目标碎片呈链式排列、需要跨链合并时,改用NMM获得传递合并效果。
  3. IOU vs IOS:切片推理场景默认 IOS 更激进地消除重复;在需要保守合并、避免误并的场景改用 IOU。注意LSNMSPostprocess只支持 IOU。
  4. 后端选择:CPU 小批量用 numpy 零依赖;CPU 大批量用 numba;CUDA/Apple Silicon 用 torchvision。注意 numba 首次调用有约 1 秒 JIT 预热。
  5. 低置信度陷阱:当confidence_threshold < 0.1时 SAHI 会自动把后处理切换到 NMS/IOU,这是有意为之;如果你确实需要低置信度下的合并类后处理,请显式传入force_postprocess_type=True。
  6. OBB 模型:Ultralytics 的旋转框(OBB)输出只支持 NMS,postprocess_type会被强制覆盖。

十、延伸阅读

  • 后处理后端(backends.md):后端选择、自动检测与直接调用后处理函数的完整指南;
  • 切片推理指南(sliced-inference.md):切图 → 检测 → 合并的完整工作流与参数调优;
  • CLI 参考(cli.md):命令行下--postprocess_*系列参数;
  • 后处理工具函数(utils.md):ObjectPredictionList与合并算子的细节;
  • 源码: sahi/postprocess/combine.py、sahi/postprocess/backends.py、sahi/postprocess/utils.py、sahi/postprocess/_numpy_backend.py、sahi/postprocess/_sparse_backend.py、sahi/postprocess/_torchvision_backend.py、sahi/predict.py;
  • 测试: tests/test_combine.py。
  • 计算机视觉
  • 人工智能

【免费下载链接】sahi

Framework agnostic sliced/tiled inference + interactive ui + error analysis plots

项目地址:https://gitcode.com/gh_mirrors/sa/sahi
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进