首页
/ supervision InferenceSlicer 窗口化 GeoTIFF 推理:大栅格分块读取、坐标合并与 CRS 校验

supervision InferenceSlicer 窗口化 GeoTIFF 推理:大栅格分块读取、坐标合并与 CRS 校验

2026-09-05 12:04:28作者:丁柯新Fawn

InferenceSlicer 是 supervision 中用于大图分块推理的核心工具,而本文聚焦它在遥感与地理空间场景下的关键扩展:直接接收一个已打开的 rasterio 数据集,逐窗口(window-by-window)懒加载切片,让多 GB 的 GeoTIFF 无需整体载入内存即可完成检测。读完本文,你将掌握 GeoTIFF 可选依赖的安装方式、可运行的分块推理完整示例、WindowedRasterDataset 结构化协议的设计原理,以及切片读取的 dtype 保持、坐标系校验、读锁串行化等源码级细节与对应的测试验证。

一、为什么 GeoTIFF 需要窗口化读取

正射影像(orthomosaic)、无人机航测成果、卫星底图通常以 GeoTIFF 形式存储,单文件体积常达数 GB 甚至更大。传统做法是先把整个栅格读成 NumPy 数组再交给 InferenceSlicer 分块,这会带来两个问题:

  • 内存峰值等于整幅栅格的大小,高分辨率影像直接导致 OOM;
  • 模型真正需要的只是逐块送入的 (H, W, C) 切片,整幅加载是浪费。

supervision 的解决方案是让 InferenceSlicer 识别 rasterio 风格的"窗口化读取"数据集:传入一个已打开的 rasterio 数据集对象,内部用 dataset.read(window=...) 按切片窗口读取,读完一块、推理一块、再读下一块。这一能力在 InferenceSlicer 的官方文档中也被明确描述为:

"multi-GB GeoTIFFs never need to be loaded into memory at once."

对应实现位于 src/supervision/detection/tools/inference_slicer.py,类与协议均从包顶层导出,可通过 sv.InferenceSlicersv.WindowedRasterDataset 直接访问,见 src/supervision/init.py

二、安装可选依赖并准备测试数据

GeoTIFF 支持是 supervision 的可选依赖,核心安装不包含 rasterio。运行示例前需要先安装扩展包并下载一份测试用的字节型 GeoTIFF:

pip install "supervision[geotiff]"
wget -O RGB.byte.tif https://raw.githubusercontent.com/rasterio/rasterio/main/tests/data/RGB.byte.tif

pyproject.toml 可以看到该 extras 的定义与版本约束:

optional-dependencies.geotiff = [
  "rasterio>=1.3", # 1.3 introduced stable window-read API and CRS.is_projected
]

版本下限 rasterio>=1.3 并非随意设定——1.3 版本引入了稳定的窗口读取 API 与 CRS.is_projected 属性,而这两者正是窗口化推理和 CRS 校验的底层依赖(下文会看到 InferenceSlicer 正是通过 crs.is_projected 判断坐标系类型)。

三、完整示例:对 GeoTIFF 逐窗口推理

以下示例完整继承自官方文档 docs/detection/tools/inference_slicer.md,演示了最典型的窗口化推理用法。回调函数对每个 256×256 的切片返回一个位于切片中心区域的固定检测框,用于验证分块与坐标合并链路:

import numpy as np
import rasterio
import supervision as sv


def callback(tile: np.ndarray) -> sv.Detections:
    h, w = tile.shape[:2]
    return sv.Detections(
        xyxy=np.array([[w * 0.25, h * 0.25, w * 0.75, h * 0.75]], dtype=float),
        confidence=np.array([0.9]),
        class_id=np.array([0]),
    )


slicer = sv.InferenceSlicer(
    callback=callback,
    slice_wh=(256, 256),
    overlap_wh=(64, 64),
    overlap_filter=sv.OverlapFilter.NONE,
)

with rasterio.open("RGB.byte.tif") as dataset:
    detections = slicer(dataset)

print(len(detections))

几个关键点的说明:

  • 回调契约:这里 batch_size 使用默认值 1,回调接收单个 np.ndarray(形状 (H, W, C))并返回单个 Detections。若将 batch_size 设为大于 1,契约变为接收 list[np.ndarray] 并返回等长list[Detections],两种签名不可互换(见 inference_slicer.py 的参数文档)。
  • slice_wh / overlap_wh:均为 (width, height),也可传单个 int 表示宽高相同。默认值分别为 640100overlap_wh 在两个维度上都必须小于 slice_wh 以保证步距为正,否则抛出 ValueError_validate_overlap,见 inference_slicer.py)。
  • overlap_filter=NONE:本例显式关闭重叠区过滤,让每个切片的检测结果原样保留,便于观察合并行为。生产场景中更常用默认的 NON_MAX_SUPPRESSION(对重叠检测做 NMS)或 NON_MAX_MERGE(NMM 合并),阈值由 iou_threshold(默认 0.5)控制,度量可选 IOUIOS(定义于 src/supervision/detection/utils/iou_and_nms.py)。
  • 数据集保持打开状态slicer(dataset) 的调用必须在 with rasterio.open(...) 上下文内执行,因为窗口读取依赖打开中的句柄。

四、WindowedRasterDataset:结构化协议而非强制导入

InferenceSlicer 接受数据集的判定逻辑值得单独展开。源码定义了一个 runtime_checkable 的 Protocol:

@runtime_checkable
class WindowedRasterDataset(Protocol):
    """Structural type for a rasterio-style dataset read window-by-window.

    Matched structurally by `_is_windowed_raster` rather than by import so
    `rasterio` stays an optional dependency — any object exposing these members
    works. `rasterio.io.DatasetReader` satisfies this protocol.
    """

    width: int
    height: int
    crs: object | None

    def read(self, window: Any) -> npt.NDArray[Any]: ...

(见 inference_slicer.py

实际运行时,InferenceSlicer 并不真正 isinstance 检查 rasterio 类型,而是用鸭子类型判定函数(inference_slicer.py):

def _is_windowed_raster(image: object) -> TypeGuard[WindowedRasterDataset]:
    return (
        callable(getattr(image, "read", None))
        and hasattr(image, "crs")
        and hasattr(image, "width")
        and hasattr(image, "height")
    )

从源码结构看,这一设计带来两个直接好处:

  1. rasterio 保持可选依赖——未安装 rasterio 的环境里,NumPy 数组和 PIL 图像因为不具备 read/crs/width/height 这组属性而自然走内存图像分支,无需任何条件导入;
  2. 任何暴露这组成员的类都可以被当作数据集传入,官方测试正是利用这一点构造了轻量的 _FakeRasterDataset 桩对象,在不依赖 rasterio 的前提下验证窗口读取逻辑(tests/detection/tools/test_inference_slicer_geotiff.py)。

五、切片读取细节:窗口坐标、通道转置与 dtype 保持

_run_callback 是单图与数据集两条路径的分叉点(inference_slicer.py):

if _is_windowed_raster(image):
    x_min, y_min, x_max, y_max = (int(v) for v in offset)
    # rasterio tuple window: ((row_start, row_stop), (col_start, col_stop))
    window = ((y_min, y_max), (x_min, x_max))
    with self._raster_read_lock:
        bands = image.read(window=window)  # shape (channels, height, width)
    image_slice = np.ascontiguousarray(
        np.transpose(bands, (1, 2, 0))
    )  # -> (H, W, C)
    resolution_wh = (image.width, image.height)

这里有三处实现细节决定了你写回调时的输入形态:

  • 窗口坐标换算:内部切片偏移量以 (x_min, y_min, x_max, y_max) 表示,而 rasterio 的 window 元组是"行在前、列在后",所以先做 (y, x) 重组;
  • 通道顺序转置dataset.read(window=...) 返回 (C, H, W),supervision 用 np.transpose(..., (1, 2, 0))np.ascontiguousarray 转成回调期望的 (H, W, C)
  • dtype 与波段原样保留:从数据集读出的切片保留源 dtype(16 位传感器的 uint16 依然是 uint16)且保留所有波段,转成你模型所需的 dtype/通道数需要在回调内自行完成。这一点有专门的测试 test_windowed_raster_preserves_band_dtype 验证 uint16 输入逐片不变(test_inference_slicer_geotiff.py);单波段栅格的切片则会以 (H, W, 1) 形态送达回调(test_single_band_raster_produces_hwc1_tiles)。

另外两个边界行为也有测试覆盖:当栅格比 slice_wh 还小时,整幅只产生一个切片(test_raster_smaller_than_slice_produces_single_tile);且窗口化读取与内存数组路径对同样的随机像素数据会产生逐片相同的 tile(test_raster_tiles_match_array_tiles),说明两条路径在分块几何上严格一致。

六、CRS 校验:为什么必须使用投影坐标系

文档中有一句关键约束:"GeoTIFF inputs must use a projected coordinate reference system." 其实现位于 _get_resolution_whinference_slicer.py):

if _is_windowed_raster(image):
    crs = image.crs
    if crs is not None and not getattr(crs, "is_projected", True):
        raise ValueError(
            "InferenceSlicer requires a projected coordinate reference "
            "system for pixel-space tiled inference on a raster dataset. "
            f"The provided dataset uses a geographic CRS ({crs}). Reproject "
            "it to a projected CRS (e.g. with `gdalwarp`) before slicing."
        )
    return (image.width, image.height)

即:如果 crs 存在且不是投影坐标系(地理坐标系,如经纬度 WGS84),直接抛出 ValueError 并提示用 gdalwarp 等工具先重投影;crs is None 的情况则放行。原因可以从"像素空间分块"的视角理解:InferenceSlicer 的整个坐标体系建立在等宽像素网格上——偏移量、xyxy 平移、NMS 重叠计算都假设 x/y 单位是像素且横纵轴尺度一致;经纬度栅格的一个像素在不同纬度代表的米数不同,切片内"正方形"假设不再成立,因此源码选择在入口快速失败而不是产生错误结果。对应的正反测试见 test_crs_allows_slicingNone 与投影 CRS 均通过)和 test_geographic_crs_raises(地理 CRS 抛错)(test_inference_slicer_geotiff.py)。

七、并发模型:读锁串行化与 OBB 顺序回退

thread_workers > 1 时,模型推理并行执行,但对数据集的窗口读取必须串行,因为 rasterio 数据集句柄并不保证多线程读取安全。源码在 __init__ 中创建专用锁 _raster_read_lockinference_slicer.py),每次 image.read(window=...) 都在该锁内进行;批量路径 _run_callback_batch 同样逐片加锁读取(inference_slicer.py)。参数文档对此的表述是:

"When passing a rasterio-style dataset, tile reads are serialized via an internal lock regardless of this setting — model inference runs in parallel, but raster.read() is protected."

测试 test_thread_workers_with_raster_serializes_reads 用一个记录峰值并发的数据集在 thread_workers=4 下运行,断言 peak_concurrent == 1,从行为上证实了读锁的串行化效果(test_inference_slicer_geotiff.py)。

与 GeoTIFF 路径相关但值得注意的另一条规则:第一个切片(或第一个 batch)始终同步执行。从源码看,__call__ 先同步跑第一个切片,目的既是对回调返回值做快速类型校验(fail-fast),也是"OBB 探测"——如果检测结果中带有方向框(oriented bounding box)数据,由于许多 OBB 推理后端在线程间共享时会崩溃,supervision 会把剩余切片回退为顺序处理,并每实例仅警告一次(inference_slicer.py)。另外,切片数很少时(例如两切片的小图)有效并行度会因此降低,这在参数文档中有明确说明。

八、结果合并与"零差异"验证

每个切片上的检测结果先经过 move_detections 平移回全图坐标系——该函数对 xyxy、方向框和 mask 分别做偏移(mask 走 CompactMask.with_offsetmove_masksinference_slicer.py),再经 Detections.merge 汇总、按 overlap_filter 策略后处理,得到最终结果。

针对 GeoTIFF 路径,测试套件给出了两条强保证(tests/detection/tools/test_inference_slicer_geotiff.py):

  • 数据集路径与内存数组路径结果一致test_windowed_raster_matches_in_memory_array 对同一幅随机图像分别以数组和伪数据集调用 slicer,排序后断言检测框完全相等;
  • 真实 rasterio 集成一致test_real_rasterio_memoryfile_integrationrasterio.io.MemoryFile 构造真实的 GTiff 数据集(安装 rasterio 时才会执行),验证真实窗口读取的检测结果与数组路径零差异(test_inference_slicer_geotiff.py)。

这意味着:你在内存图像上调通的分块参数、回调与合并策略,可以直接迁移到多 GB 的 GeoTIFF 上而无需重新验证几何正确性。

九、InferenceSlicer 参数速查

结合 inference_slicer.py__init__ 签名与文档字符串,完整参数如下(本文示例仅展示了 GeoTIFF 场景常用的子集):

参数 默认值 说明
callback 必填 单图契约:np.ndarrayDetections;批量契约(batch_size > 1):list[np.ndarray] → 等长 list[Detections],长度不匹配抛 ValueError
slice_wh 640 每个切片的 (width, height),正数,可传单个 int
overlap_wh 100 切片间重叠 (width, height),非负且两维都必须小于 slice_wh
overlap_filter NON_MAX_SUPPRESSION 重叠检测合并策略:NONE / NON_MAX_SUPPRESSION / NON_MAX_MERGE
iou_threshold 0.5 NMS / NMM 的重叠阈值
overlap_metric IOU 重叠度量:IOUIOS(Intersection over Smaller)
thread_workers 1 并行切片推理线程数,正整数;首个切片恒为同步执行;OBB 结果触发顺序回退;数据集窗口读取始终经读锁串行
compact_masks False True 时将回调返回的稠密 mask 转为 CompactMask,避免高分辨率图上 (N, H, W) 大数组分配导致 OOM
batch_size 1 每次回调传入的切片数;GPU 场景推荐 batch_size > 1thread_workers=1,避免显存峰值被 thread_workers * batch_size 放大

十、小结

supervision 的 InferenceSlicer 通过一个轻量结构化协议 WindowedRasterDataset,把"内存大图分块推理"平滑扩展到了"窗口化 GeoTIFF 分块推理":打开的 rasterio 数据集按需读片,(C, H, W) 转置为 (H, W, C) 后连同原始 dtype 交给回调;入口处的 crs.is_projected 检查杜绝了地理坐标系下的像素几何歧义;读锁与 OBB 顺序回退保证了多线程下的稳定性;而"数据集中间值与内存路径零差异"的测试则把这一扩展的正确性锚定在了已有的分块推理链路上。对处理航测正射影像或卫星底图的开发者的实际建议是:先确认栅格为投影坐标系(必要时先 gdalwarp 重投影),在回调内完成波段选择与 dtype 转换,GPU 模型优先用批量契约而非多并发单图调用。

登录后查看全文
热门项目推荐
相关项目推荐