supervision InferenceSlicer 窗口化 GeoTIFF 推理:大栅格分块读取、坐标合并与 CRS 校验
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.InferenceSlicer、sv.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表示宽高相同。默认值分别为640和100;overlap_wh在两个维度上都必须小于slice_wh以保证步距为正,否则抛出ValueError(_validate_overlap,见 inference_slicer.py)。overlap_filter=NONE:本例显式关闭重叠区过滤,让每个切片的检测结果原样保留,便于观察合并行为。生产场景中更常用默认的NON_MAX_SUPPRESSION(对重叠检测做 NMS)或NON_MAX_MERGE(NMM 合并),阈值由iou_threshold(默认0.5)控制,度量可选IOU或IOS(定义于 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]: ...
实际运行时,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")
)
从源码结构看,这一设计带来两个直接好处:
- rasterio 保持可选依赖——未安装 rasterio 的环境里,NumPy 数组和 PIL 图像因为不具备
read/crs/width/height这组属性而自然走内存图像分支,无需任何条件导入; - 任何暴露这组成员的类都可以被当作数据集传入,官方测试正是利用这一点构造了轻量的
_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_wh(inference_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_slicing(None 与投影 CRS 均通过)和 test_geographic_crs_raises(地理 CRS 抛错)(test_inference_slicer_geotiff.py)。
七、并发模型:读锁串行化与 OBB 顺序回退
thread_workers > 1 时,模型推理并行执行,但对数据集的窗口读取必须串行,因为 rasterio 数据集句柄并不保证多线程读取安全。源码在 __init__ 中创建专用锁 _raster_read_lock(inference_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_offset 或 move_masks,inference_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_integration用rasterio.io.MemoryFile构造真实的 GTiff 数据集(安装 rasterio 时才会执行),验证真实窗口读取的检测结果与数组路径零差异(test_inference_slicer_geotiff.py)。
这意味着:你在内存图像上调通的分块参数、回调与合并策略,可以直接迁移到多 GB 的 GeoTIFF 上而无需重新验证几何正确性。
九、InferenceSlicer 参数速查
结合 inference_slicer.py 的 __init__ 签名与文档字符串,完整参数如下(本文示例仅展示了 GeoTIFF 场景常用的子集):
| 参数 | 默认值 | 说明 |
|---|---|---|
callback |
必填 | 单图契约:np.ndarray → Detections;批量契约(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 |
重叠度量:IOU 或 IOS(Intersection over Smaller) |
thread_workers |
1 |
并行切片推理线程数,正整数;首个切片恒为同步执行;OBB 结果触发顺序回退;数据集窗口读取始终经读锁串行 |
compact_masks |
False |
为 True 时将回调返回的稠密 mask 转为 CompactMask,避免高分辨率图上 (N, H, W) 大数组分配导致 OOM |
batch_size |
1 |
每次回调传入的切片数;GPU 场景推荐 batch_size > 1 且 thread_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 模型优先用批量契约而非多并发单图调用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00