# 计算机视觉：从像素到感知

> Leon | AI Field Guides

[EN](AI-CV.en.md) · [中文](AI-CV.zh.md)

![Leon — 学习路线图](../assets/maps/cv.zh.svg)

<a id="extensions"></a>

## 继续深入：完整案例

先读对应章节，再跟着故事预测输出、运行完整程序、核对解释。每个案例都在原有概念上增加一个实际问题。

1. [把检测框还原到原始图像](#lab-inference-pipelines-letterbox-coordinate-roundtrip) — 在整个图像流程中传递坐标变换信息。

2. [排除忽略像素，计算分割 IoU](#lab-segmentation-regions-confusion-matrix-miou) — 从计数推导 IoU，并说明平均规则。

十六课从像素与简单 Python 列表出发，逐步进入识别、检测、跟踪、OCR 和可靠图像系统。每课都有故事、逐步计算、可运行代码、已解答问题及两个练习情境。

### 运行 Python 示例

使用 Python 3.12 或更新版本。把代码保存为 .py 文件，在终端中用 `python 文件名.py` 运行；部分系统使用 `python3`。若出现 `import numpy as np`，先运行 `python -m pip install numpy`。这行 import 加载 NumPy 数值工具包，并把它简称为 np。示例无需下载数据集或模型权重。

[Python 官方下载](https://www.python.org/downloads/) · [NumPy 官方安装说明](https://numpy.org/install/)

## 把知识连接起来

这是一份结合 OpenCV、PyTorch 官方资料、Stanford 教学与基础视觉研究编写的原创概念指南。例子使用自拟数据；它是一条连贯的入门路径，并非某门课程的复制品，也不是生产部署手册。

### 开始前需要了解

- 高中算术：加法、乘法、分数与坐标
- 若不熟悉列表、循环与索引，先阅读共享 Python 入门
- 不预设线性代数、微积分或深度学习框架知识

### 你将学到什么

- 追踪图像的形状、尺度与坐标变化
- 根据任务选择分类、检测或分割
- 用明确维度理解卷积与图像块模型
- 避免相关图像泄漏，可靠地评估效果

## 知识框架

### 1 · 基础：看懂数字

- [像素是带有约定的测量值](#pixels-contracts)
- [颜色：先弄清通道](#colour-channels)
- [局部滤波提出局部问题](#filters-neighborhoods)
- [边缘与掩码：先找变化，再清理](#edges-morphology)

### 2 · 核心：比较与定位

- [让图像与标注一起移动](#geometry-augmentation)
- [局部特征：比较有辨识度的图块](#classical-features)
- [借用视觉词汇，再学习新的判断](#recognition-transfer)
- [框定位候选对象，重叠度帮助消除重复](#detection-overlap)

### 3 · 应用：测量、跟踪与读字

- [检测分数：顺着排序列表评估](#detection-average-precision)
- [从一个矩形走向真正属于对象的像素](#segmentation-regions)
- [跟踪：跨帧保留身份](#tracking-identity)
- [OCR：把可见笔画变成文字](#ocr-reading)

### 4 · 进阶：组合与验证

- [让图像块交换信息](#visual-tokens)
- [缺陷检查：发现异常区域](#defect-inspection)
- [用下一次采集检验，而非只看熟悉的邻帧](#evaluation-shift)
- [推理流程：检查完整处理过程](#inference-pipelines)

<a id="pixels-contracts"></a>

## 像素是带有约定的测量值

### 01 / 从一个完整故事开始

Maya 帮社区档案馆扫描手绘戏剧海报。一天早晨，新目录里的蓝戏服变成红色。文件能正常打开，团队先怀疑屏幕。Maya 改为追踪一块已知蓝色区域，逐步查看处理过程。

她发现读取程序先存蓝色，下一步却把第一个值当红色。她记录颜色顺序、图像尺寸与数值范围，修正颜色转换，戏服恢复蓝色。此后团队先检查这些简单约定，再让模型识别。若数值含义不一致，相同数字就可能描述不同颜色。

### 02 / 理解核心概念

数字图像是由像素组成的测量网格。灰度图在每个行列位置存一个数。彩色图通常存三个数：红、绿、蓝，简称 RGB。通道就是其中一种颜色的数据。数组是有组织的数值表；张量是具有一个或多个轴的数组。

形状 (4, 6, 3) 表示四行、六列、三个通道。Python 从零计数，所以 image[0, 1] 取第一行第二列。有些工具先存蓝色，即 BGR。移动通道轴并不会交换颜色的含义。

缩放把字节数值除以 255，使它落在 0 到 1。标准化再减去指定中心，除以正的离散程度。结果为负表示低于中心，并不是不可能的颜色。必须遵循模型训练时使用的输入规则。

### 03 / 如何运用这个概念

搭建视觉流程时，可以先使用一张颜色与位置都已知的小型诊断图片。每次转换后，检查形状、类型、最小值、最大值，以及某个指定像素。采用预训练权重要求的归一化统计量；若自己设计流程，就只用训练数据估计。保留未修改的输入，以免可视化时误把标准化数值当成普通颜色。

### 04 / 官方教程与研究中的应用

OpenCV 的 Basic Operations on Images 官方教程直接展示像素访问、数组属性与通道操作，而不是把图像当作不透明文件。TorchVision 的变换文档介绍了图像与结构化目标的处理。两者共同体现档案馆故事中的实际问题：表示方式本身就是计算的一部分，在模型推理开始之前必须保持一致。

### 05 / 公式与逐项解析

```text
z = (v / 255 − μ) / σ
HWC → CHW: (H, W, C) → (C, H, W)
```

v 是取值为 0 到 255 的八位通道数值。除以 255 后，它被映射到单位区间；μ 和正数 σ 是在相同尺度下使用的均值与标准差。z 是标准化后的数值，可以为负。H、W、C 分别表示高度、宽度与通道数。HWC 和 CHW 只说明轴的顺序，并不说明哪个通道代表红、绿或蓝。公式假设输入确实使用上述字节尺度；已经位于单位区间的浮点图像不应再次除以 255。

### 06 / 一步步算出结果

设自拟通道值 v = 153，μ = 0.5，σ = 0.25。先缩放：153 / 255 = 0.6。再中心化：0.6 − 0.5 = 0.1。最后除以标准差，得到 z = 0.4。

如果输入的 0.6 已经完成缩放，却再次除以 255，标准化结果将约为 −1.9906。形状需要单独检查：一张 4×6 的 RGB 图像形状为 (4, 6, 3)，把通道轴移到前面后得到 (3, 4, 6)，两者都包含 72 个数值。这两种操作本身都不会把 BGR 变成 RGB。只有同时检查数值和通道含义，才能判断输入是否正确。

### 先认识这些词

- **像素:** 图像网格中的一个位置。

- **通道:** 每个像素的一种测量类型，例如红色。

- **形状:** 数组沿每个轴的大小。

### 一步步想清楚

**为什么尺寸正确的图像仍可能显示错误？**

三个通道的含义可能弄错。把 BGR 当 RGB 会交换红蓝。

**v = 153 时的中间步骤是什么？**

153/255 = 0.6。减 0.5 得 0.1，然后才除以 0.25。

**输入已经是 0.6 时还要再除吗？**

只有它仍属于字节尺度才可以。如果已经缩放到 0–1，再除会破坏约定范围。

<a id="lab-pixels-contracts-pixel-scale"></a>

### 追踪一个通道

用列表表示一个已知蓝色像素，不需要图像文件或训练模型。

```python
bgr = [255, 0, 0]
rgb = list(reversed(bgr))
value = 153
scaled = value / 255
standardized = (scaled - 0.5) / 0.25
print("RGB:", rgb)
print(f"scaled={scaled:.1f}; standardized={standardized:.1f}")
print("stored numbers:", 4 * 6 * 3)
```

**在本地运行**

```sh
python pixels-contracts-pixel-scale.py
```

**预期输出**

```text
RGB: [0, 0, 255]
scaled=0.6; standardized=0.4
stored numbers: 72
```

**逐步读懂代码**

第一行按蓝、绿、红存储。reversed 改变顺序，list 保存结果。接着三行把 153 缩放，减 0.5，再除以 0.25。f 字符串保留一位小数。最后的乘法得到 72 个通道数值，不是 72 个像素。这是在检查数据约定，并没有识别物体。

### 这些知识可以用在哪里

**已知颜色检查**

把 bgr 改成 [0, 0, 255]。RGB 应变成红色 [255, 0, 0]。解释为何元素数仍是三。

**不同图像尺寸**

2×5 的 RGB 图有 10 个像素、30 个通道数值。修改最后的乘法，分别检查两个计数。

**类比的边界与注意事项:** 包裹标签的类比解释的是接口一致性，并不能代替色彩科学。相机响应、伽马编码与光照也会影响像素。形状和范围正确，不能证明不同拍摄条件下的图像对应相同的物理测量；它只是排除了一类本可避免的错误。

**记住这一点:** 在让模型学习像素之前，先明确每个接口的轴顺序、颜色含义、数值尺度与归一化约定。

### 本节资料来源

- [OpenCV: 图像基本操作](https://docs.opencv.org/4.x/d3/df2/tutorial_py_basic_ops.html)

- [TorchVision: 变换图像、视频、框等数据](https://docs.pytorch.org/vision/stable/transforms.html)

<a id="colour-channels"></a>

## 颜色：先弄清通道

### 01 / 从一个完整故事开始

学校园艺社拍摄彩色植物标牌。小梅先用高亮度找红牌，结果白标签也通过，阴影里的红牌却消失了。她分别查看三个通道。红牌通常红色分量比绿色、蓝色多，白纸则三个都高。

她在几个已知颜色上测试小规则，立刻看出差别。她还留下阴影样本供后续检查。现在社团能解释规则选中了什么。虽然仍要测试光线变化，但已经不再把亮度与颜色混为一谈。

### 02 / 理解核心概念

RGB 为每个像素保存红、绿、蓝分量。三个大数可以表示明亮白色；仅红分量很大可以表示红色。灰度图把颜色压成一个数，因此不同颜色可能变得无法区分。

简单颜色规则可以直接比较通道。另一种色彩空间 HSV 用色相（颜色家族）、饱和度（颜色强弱）、明度（归一化 RGB 中最大的通道）表示颜色。HSV 可以让颜色范围更好描述，但不能消除光照影响。红色色相范围可能跨越刻度首尾，不同库的刻度也不同。

### 03 / 如何运用这个概念

颜色筛选可选择标牌或为后续检查准备区域。信任掩码前要检查已知颜色、通道顺序与光照。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
red_margin = R − max(G, B)
select_red = red_margin > threshold
```

R、G、B 是同一像素的红、绿、蓝值，使用相同尺度。max 选择绿、蓝中较大者。red_margin 衡量红色超过较强竞争通道多少。threshold 是要求的最小差距，是规则设置，不是学习得到的确定性。

### 06 / 一步步算出结果

取阈值 50。红牌 (200,40,30) 的差为 200−40=160，通过。白纸 (220,220,220) 的差为 0，不通过。

暗红牌 (60,20,20) 的差为 40，也不通过。最后一项揭示局限：固定绝对差阈值可能漏掉阴影中的相同颜色。

### 先认识这些词

- **RGB:** 使用红、绿、蓝通道的颜色表示。

- **色相:** 红或蓝等颜色家族。

- **饱和度:** 相对于灰色，像素颜色的强烈程度。

### 一步步想清楚

**为什么排除明亮白色？**

三个通道同样高，红色没有优势。

**标牌差距怎样得到 160？**

max(40,30)=40，再算 200−40=160。

**把阈值改成 30 能找回阴影红牌吗？**

可以，差距 40 会通过，但也可能放进更多非目标颜色。

<a id="lab-colour-channels-red-channel-rule"></a>

### 测试三种已知颜色

用普通 RGB 元组和透明规则。

```python
pixels = {"marker": (200, 40, 30), "paper": (220, 220, 220), "shade": (60, 20, 20)}
for name, (red, green, blue) in pixels.items():
    margin = red - max(green, blue)
    print(name, "margin:", margin, "selected:", margin > 50)
```

**在本地运行**

```sh
python colour-channels-red-channel-rule.py
```

**预期输出**

```text
marker margin: 160 selected: True
paper margin: 0 selected: False
shade margin: 40 selected: False
```

**逐步读懂代码**

字典把名称对应到三个通道值。items 返回名称与数值对，解包给通道命名。max 选较强竞争通道，相减得到差距，>50 返回 True 或 False。这是手设颜色筛选器，不是训练好的检测器，也未实现完整 HSV 转换。

### 这些知识可以用在哪里

**蓝标牌**

交换红蓝角色，创建蓝色差距规则。测试 (30,40,200)，蓝色差距是 160。

**干扰背景**

添加一个红墙像素。颜色规则无法把它与标牌区分，还需要形状或位置检查。

**类比的边界与注意事项:** 通道规则不等于物体识别。红墙和红牌一样可能通过，相机设置与阴影都会改变数值。

**记住这一点:** 颜色筛选可选择标牌或为后续检查准备区域。信任掩码前要检查已知颜色、通道顺序与光照。

### 本节资料来源

- [OpenCV: 转换色彩空间](https://docs.opencv.org/4.x/df/d9d/tutorial_py_colorspaces.html)

- [OpenCV: 图像基本操作](https://docs.opencv.org/4.x/d3/df2/tutorial_py_basic_ops.html)

<a id="filters-neighborhoods"></a>

## 局部滤波提出局部问题

### 01 / 从一个完整故事开始

Jon 在清理扫描星图。灰尘产生亮点，淡铅笔线标出星座。他先用邻近像素的均值替换每个像素。灰尘变弱，有用铅笔线却也变淡。

Jon 比较已知线条与空白区域，不再仅凭是否平滑判断。他试更小的平均窗口，并将原图放在结果旁。只有所选操作保留了需要的标记，星图才真正改善。他学会同时问局部计算删掉了什么、显示了什么。少见亮点既可能是噪声，也可能是重要证据。

### 02 / 理解核心概念

滤波器根据附近一小组像素替换当前像素。求均值时先把数值相加，再除以个数。这会减弱孤立亮点，也可能削弱真正的细线。

核是一张小型权重表。把每个邻近像素乘对应权重，再相加。3×3 均值核给九个像素各分配 1/9 的权重。检测变化的核可能使用负权重，因此要保留有符号数。

图像边缘会缺少部分邻居。填充用零或边缘像素副本等规则补上，必须明确规则。这里的公式移动权重时不翻转，称为互相关。严格的数学卷积先翻转核。对称均值核在两种方式下结果相同。

### 03 / 如何运用这个概念

选择滤波器前，先说明目标：抑制孤立噪声、估计梯度，还是为后续任务准备输入。既要查看平坦区域，也要查看代表性边界，记录填充规则；若负响应有意义，应保留浮点输出。要比较后续识别效果或标注质量，而不是把视觉上的平滑自动等同于改进。

### 04 / 官方教程与研究中的应用

OpenCV 的 Smoothing Images 教程展示多种邻域滤波方法，而 filter2D 接口参考明确了该操作使用的互相关约定。两类来源可以互补：教程说明流程，接口参考澄清实现细节。学习型卷积网络会从数据中拟合许多核，扩展了共享局部运算的思想；但训练过程与非线性层使它远不止固定平滑滤波器。

### 05 / 公式与逐项解析

```text
filtered_value = pixel₁×weight₁ + pixel₂×weight₂ + …
3×3 mean = (sum of nine pixel values) / 9
```

每个像素数来自指定邻域中的一个位置，对应权重决定贡献。省略号表示继续处理所有位置。3×3 均值中，每个权重为 1/9，九个权重和为 1。公式移动权重时不翻转，称为互相关。边界需要说明怎样补缺失像素。不同操作可分别过滤通道，也可组合通道。

### 06 / 一步步算出结果

考虑一个 3×3 图像块：其中八个值为 10，中心值为 19。均值滤波后的中心值为 (8 × 10 + 19) / 9 = 11。亮点被减弱了，但并非没有代价：真正孤立的星点也会变暗。另看一维边缘探测，把互相关核 [−1, 0, 1] 作用于 [2, 5, 8]，响应为 −2 + 0 + 8 = 6。

若输入顺序反转，结果就是 −6。粗心地使用无符号输出，可能掩盖负响应。这两项计算回答的是不同问题，应根据需要保留的信息选择和评估它们。

### 先认识这些词

- **核:** 小型权重表或邻域位置表。

- **填充:** 在原图边界外补充的数值。

- **加权和:** 每个数乘权重后再相加。

### 一步步想清楚

**为什么亮中心变为 11 而不是 10？**

19 仍占总和的九分之一。求均值会削弱它，并未丢弃它。

**边缘滤波器怎样得到 6？**

计算 2×−1、5×0、8×1，再相加 −2+0+8。

**反转这一行会怎样？**

[8, 5, 2] 得到 −6。符号说明向哪个方向变亮。

<a id="lab-filters-neighborhoods-local-filter"></a>

### 求小块均值

观察亮点与有正负号的边缘响应。这是手写滤波器，不是学习模型。

```python
patch = [10, 10, 10, 10, 19, 10, 10, 10, 10]
mean = sum(patch) / len(patch)
row = [2, 5, 8]
weights = [-1, 0, 1]
response = sum(value * weight for value, weight in zip(row, weights))
print(f"mean={mean:.1f}")
print("edge response:", response)
```

**在本地运行**

```sh
python filters-neighborhoods-local-filter.py
```

**预期输出**

```text
mean=11.0
edge response: 6
```

**逐步读懂代码**

sum 累加九个数，len 计数，得到 99/9 = 11。zip 把每个像素和权重配对。生成式逐对相乘，sum 将 −2、0、8 加起来得到 6。例子只计算一个邻居齐全的位置，因此暂时不涉及填充。

### 这些知识可以用在哪里

**保留细标记**

把中央的 19 看作有用细线的一部分。均值仍为 11。在丢掉需要的证据前，先决定是否适合平滑。

**恒定区域**

把 row 改为 [5, 5, 5]。因为 −5+0+5=0，边缘响应为零。有亮度不等于亮度有变化。

**类比的边界与注意事项:** 邻近证人也可能犯同一种错误。平均无法恢复原本就没有被测到的细节，过强平滑还会损伤边界。边缘响应表示局部强度变化，并不等于语义对象边界；阴影、纹理和传感器噪声都可能产生响应。

**记住这一点:** 滤波器是用权重表达的局部问题；既要检查它去掉了什么，也要检查它保留了什么。

### 本节资料来源

- [OpenCV: 图像平滑](https://docs.opencv.org/4.x/d4/d13/tutorial_py_filtering.html)

- [OpenCV: 图像滤波：filter2D](https://docs.opencv.org/4.x/d4/d86/group__imgproc__filter.html)

<a id="edges-morphology"></a>

## 边缘与掩码：先找变化，再清理

### 01 / 从一个完整故事开始

学生为手工项目扫描模板。阈值化后，图里既有字母，也有细小白色灰尘点。小俊先检查亮度快速变化处，找出可能的边界，再查看是或否的掩码。他选用开运算：先缩小选中区域，再扩展剩余区域。

单个噪点消失，较宽笔画保留。他把结果与原扫描比较后才保存规则。极细笔画也可能消失，因此他不会把所有被删像素都叫噪声。只有同时看清对有用和无用标记的影响，操作才真正有用。

### 02 / 理解核心概念

边缘是明显的局部变化，但不一定是物体轮廓。相减附近亮度可测量变化。阈值化再把数值变成选中或未选中的二值掩码。

形态学使用小邻域改变掩码形状。腐蚀只在所需邻居全部被选中时保留当前位置；膨胀在任一所需邻居被选中时保留。开运算先腐蚀再膨胀，闭运算顺序相反。这些规则处理形状，并未学习物体含义。

### 03 / 如何运用这个概念

可用这些操作清理扫描文字掩码或检查简单形状。每次清理前后都应比较细小但有用的细节。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
change = right_value − left_value
erode[i] = min(mask[i−1], mask[i], mask[i+1])
dilate[i] = max(mask[i−1], mask[i], mask[i+1])
```

i 是一维行中的位置。掩码值为 0 或 1。三个值全为 1 时 min 才为 1，至少一个为 1 时 max 就为 1。行外用零填充。二维实现则在每个像素周围使用指定邻域。

### 06 / 一步步算出结果

对 [0,1,0,0,1,1,1,0] 腐蚀得到 [0,0,0,0,0,1,0,0]。只有长度为三的连续段中心及左右邻居全被选中。再膨胀得到 [0,0,0,0,1,1,1,0]。孤立的位置 1 被删除，较宽连续段恢复。

### 先认识这些词

- **边缘:** 邻近图像数值明显变化的位置。

- **腐蚀:** 只保留所需邻居全部被选中的位置。

- **膨胀:** 只要所需邻居有一个被选中，就选中该位置。

### 一步步想清楚

**为什么孤立的 1 消失？**

它的邻域含零，因此腐蚀取 min=0。之后也没有幸存邻居将它恢复。

**宽度为三的连续段腐蚀后剩下什么？**

仅中心位置，因为 [1,1,1] 的最小值为 1。

**真实的一像素细线会保留吗？**

不会。该操作不知道这条细线有用。

<a id="lab-edges-morphology-binary-opening"></a>

### 移除孤立噪点

使用零填充与三个位置的邻域。

```python
mask = [0, 1, 0, 0, 1, 1, 1, 0]
def local_rule(values, operation):
    padded = [0] + values + [0]
    return [operation(padded[i:i + 3]) for i in range(len(values))]
eroded = local_rule(mask, min)
opened = local_rule(eroded, max)
print("eroded:", eroded)
print("opened:", opened)
```

**在本地运行**

```sh
python edges-morphology-binary-opening.py
```

**预期输出**

```text
eroded: [0, 0, 0, 0, 0, 1, 0, 0]
opened: [0, 0, 0, 0, 1, 1, 1, 0]
```

**逐步读懂代码**

def 定义可复用函数。列表相加在首尾各补一个零。range 遍历原位置；切片 i:i+3 取三个数，不包括终点索引。传入 min 做腐蚀，传入 max 做膨胀。开运算把腐蚀结果用作下一步输入。这是准确的小型形态学规则，不是学习图像系统。

### 这些知识可以用在哪里

**更宽噪点**

把孤立噪点改成三个连续 1。它可能通过开运算，说明清理依赖大小而非含义。

**边界规则**

对 [1,1,1] 使用同样零填充。膨胀前先观察腐蚀结果；边界假设会影响中间形状。

**类比的边界与注意事项:** 开运算可能移除真实小物体。边缘强度不能决定哪一侧属于物体。一维示例没有处理角点与曲线形状。

**记住这一点:** 可用这些操作清理扫描文字掩码或检查简单形状。每次清理前后都应比较细小但有用的细节。

### 本节资料来源

- [OpenCV: 形态学变换](https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html)

- [OpenCV: 图像平滑](https://docs.opencv.org/4.x/d4/d13/tutorial_py_filtering.html)

<a id="geometry-augmentation"></a>

## 让图像与标注一起移动

### 01 / 从一个完整故事开始

学校工坊拍摄桌面工具。Eva 为增加训练变化，放大一张图片并把工具移向角落。图片看着合理，但标记工具的旧矩形仍留在原位。

她将矩形两个角点按图片同样的倍数缩放、平移，新框终于覆盖工具。她还检查是否有部分超出画布。另一张图中，翻转会把方向箭头反过来，她因此拒绝该操作。这个练习说明，改图既要检查含义，也要让标注随图移动。

### 02 / 理解核心概念

几何描述图像内容的位置。缩放乘坐标，平移加偏移量。边界框用两个对角点包住物体。图片移动时，框也必须移动。

输出像素可能对应原图两个像素之间的位置。插值会估计该位置的数值，最简单的方法是选最近的原像素。对于类别标签掩码，普通平均可能创造不存在的标签，因此常使用最近邻采样。

数据增强通过改变训练样本来模拟预期变化。但标签必须仍然正确。翻转锤子可能保留类别，翻转左箭头却会变成右箭头。旋转和裁剪同样需要更新框或掩码坐标，并规定如何处理被截去一部分的物体。

### 03 / 如何运用这个概念

先写下目标相机环境中哪些变化是合理的，以及哪些标注必须随之改变。预览增强样本时应叠加框与掩码，还要包含极端裁剪和边界情况。随机增强通常只用于训练流程，评估采用明确的固定处理。相关视频帧应放在同一数据划分中，避免增强掩盖训练与评估之间的泄漏。

### 04 / 官方教程与研究中的应用

OpenCV 的 Geometric Transformations 教程把坐标映射与图像重采样联系起来。TorchVision 的变换文档说明图像、框、掩码和视频为何需要兼容操作。在官方目标检测教程中，图像与目标的变换也是训练数据准备的一部分。这些资料支持工作间案例的流程，但并不意味着所有变换都适合每一种检测任务或标签体系。

### 05 / 公式与逐项解析

```text
x′ = sₓ × x + tₓ
y′ = sᵧ × y + tᵧ
```

x、y 是变换前的水平与垂直坐标。撇号（′）表示变换后。sₓ、sᵧ 是缩放倍数；2 会把该轴距离加倍。tₓ、tᵧ 是以坐标单位表示的平移量。负平移表示沿轴的负方向移动。先乘再加，理解这两个式子不需要矩阵代数。

### 06 / 一步步算出结果

一个自拟工具框的两个对角为 (10, 20) 与 (30, 50)。设 sₓ = sᵧ = 2，tₓ = 5，tᵧ = −3。第一个顶点变为 (2 × 10 + 5, 2 × 20 − 3) = (25, 37)，第二个变为 (65, 97)。宽从 20 变成 40，高从 30 变成 60，因此面积从 600 变成 2400 个平方坐标单位。

平移不会改变宽或面积。如果输出画布只到 y = 80，框就超出画布，需要一致地执行裁剪与对象保留规则。若只更新像素，原标注框就会留在不相关的位置。

### 先认识这些词

- **边界框:** 用角点记录的包围物体的矩形。

- **插值:** 估计已知采样位置之间的数值。

- **数据增强:** 改变训练样本，同时保持目标一致。

### 一步步想清楚

**为什么框也必须改变？**

标注说明物体在哪里。保留旧坐标会教出错误位置。

**第一个 y 坐标怎样变成 37？**

先把 20 加倍为 40，再减 3 得 37。颠倒顺序会得到不同结果。

**只平移会改变面积吗？**

不会。两个角点加同样偏移，因此坐标差保持不变。

<a id="lab-geometry-augmentation-move-box"></a>

### 移动框的两个角

只用普通数字，先缩放再平移。

```python
corners = [(10, 20), (30, 50)]
moved = [(2 * x + 5, 2 * y - 3) for x, y in corners]
(x1, y1), (x2, y2) = moved
print("corners:", moved)
print("area:", (x2 - x1) * (y2 - y1))
print("clipped bottom:", min(y2, 80))
```

**在本地运行**

```sh
python geometry-augmentation-move-box.py
```

**预期输出**

```text
corners: [(25, 37), (65, 97)]
area: 2400
clipped bottom: 80
```

**逐步读懂代码**

列表保存两个坐标对。列表推导式对每一对使用同样两个公式。解包为四个新坐标命名。相减得到宽 40、高 60，相乘得面积 2400。min 把底边限制在 80，但真实流程还要更新可见比例规则。本例变换标注，没有对图像像素重新采样。

### 这些知识可以用在哪里

**标注检查**

把缩放设为 1，保留平移。新角点是 (15, 17)、(35, 47)，面积仍是 600。

**含义检查**

画左箭头后水平翻转。应把目标改为右箭头，或拒绝该增强。图像形状合法并不能保证标签保留。

**类比的边界与注意事项:** 移动舞台的类比不能保证对象含义在变换后保持不变。裁剪可能移除关键证据，插值也可能改变细小图案。增强只对有用且标签一致的变化有帮助，不能代替来自目标环境的代表性真实图像。

**记住这一点:** 几何变化必须带着标注一起变化；每项增强都要说明，为什么标签含义仍然成立。

### 本节资料来源

- [OpenCV: 图像几何变换](https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html)

- [TorchVision: 变换图像、视频、框等数据](https://docs.pytorch.org/vision/stable/transforms.html)

- [PyTorch: TorchVision 目标检测微调教程](https://docs.pytorch.org/tutorials/intermediate/torchvision_tutorial.html)

<a id="classical-features"></a>

## 局部特征：比较有辨识度的图块

### 01 / 从一个完整故事开始

历史社有同一扇装饰门的两张照片，一张更近且略有旋转。直接逐像素比较效果很差。Sara 改为寻找有辨识度的角点及周围小图案。她用短数值列表描述这些图块，再与另一照片里的候选描述比较。

一个候选明显更接近，而两块重复瓷砖同样合理。她接受清晰的建议，把瓷砖匹配留作未决。这说明有用的匹配器既需要相似度，也需要发现歧义的方法，不要求整张图完全相同。

### 02 / 理解核心概念

关键点是值得关注的图像位置。描述子是概括周围图块的一组数。比较描述子可以提出跨图片的对应关系。SIFT 是一种经典方法，它在多个尺度选择位置，并用含方向信息的局部亮度变化模式进行描述。

使用真实 SIFT 前，可用两个数的列表理解描述子匹配。对应元素相减，各自平方，相加后开方。所得欧几里得距离在列表相似时较小。再比较最近与次近距离：最优项若几乎打平，就应谨慎。还需用几何关系检查多个候选对应是否一致。

### 03 / 如何运用这个概念

局部特征可支持全景对齐或将拍摄物体匹配到参考图。需要检查重复图案和几何一致性。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
d(a,b) = √((a₁−b₁)² + (a₂−b₂)²)
ratio = nearest_distance / second_distance
```

a、b 是描述子列表，这里不是像素坐标。下标 1、2 选择元素。平方是差值乘自身。√ 在最终距离处开方。ratio 比较前两名；越小表示区分越清楚。次近距离必须为正。

### 06 / 一步步算出结果

查询 (1,2) 与 A=(1,3) 的距离为 √(0²+1²)=1，与 B=(4,6) 的距离为 √(3²+4²)=5。比值 1/5=0.2。教学规则 ratio<0.8 保留 A。如果 B 变为 (1,3.1)，距离为 1.1，比值约 0.9091，就拒绝这个有歧义的匹配。

### 先认识这些词

- **关键点:** 周围区域值得比较的选中位置。

- **描述子:** 图像局部区域的数值摘要。

- **欧几里得距离:** 对应元素差平方之和的平方根。

### 一步步想清楚

**为什么还要看第二名？**

即使候选都差或有歧义，也总会有最近项。与第二名的差距提供额外证据。

**B 的距离为什么是五？**

差为 3、4。平方和 9+16=25，√25=5。

**比值 0.2 能证明物体匹配吗？**

不能。它只比较这些描述子。多个一致位置与直接检查才是更强的核查。

<a id="lab-classical-features-descriptor-distance"></a>

### 比较两个图块描述

从手工描述子计算距离。

```python
import math
query = (1, 2)
candidates = {"A": (1, 3), "B": (4, 6)}
def distance(a, b):
    return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
ranked = sorted((distance(query, value), name) for name, value in candidates.items())
ratio = ranked[0][0] / ranked[1][0]
print("ranked:", ranked)
print(f"ratio={ratio:.2f}; accept={ratio < 0.8}")
```

**在本地运行**

```sh
python classical-features-descriptor-distance.py
```

**预期输出**

```text
ranked: [(1.0, 'A'), (5.0, 'B')]
ratio=0.20; accept=True
```

**逐步读懂代码**

zip 配对元素，**2 把差平方，sum 相加，math.sqrt 开方。sorted 按距离从小到大排列 (距离,名称) 对。ranked[0][0] 取第一对的距离，ranked[1][0] 取下一对。比值为 0.20。代码匹配手设描述子，没有寻找图像关键点，也没有实现 SIFT。

### 这些知识可以用在哪里

**有歧义瓷砖**

把 B 设为 (1,3.1)。比值约为 0.9091，规则将匹配保留为未决。

**完全重复**

添加另一个与 A 相同的候选。最近距离打平，比值为一。即使像素清晰，重复图案仍可能让局部匹配失败。

**类比的边界与注意事项:** 双数描述子只用于讲解匹配，并非包含更丰富局部描述的 SIFT 描述子。无纹理区域与重复图案可能几乎没有可靠匹配。

**记住这一点:** 局部特征可支持全景对齐或将拍摄物体匹配到参考图。需要检查重复图案和几何一致性。

### 本节资料来源

- [OpenCV: SIFT 入门](https://docs.opencv.org/4.x/da/df5/tutorial_py_sift_intro.html)

<a id="recognition-transfer"></a>

## 借用视觉词汇，再学习新的判断

### 01 / 从一个完整故事开始

社区花园请 Noor 将植物照片分成三组，但标注样本不多。一个没有已学视觉知识的大模型表现不稳。她改为复用在其他图片上学过模式的模型。

她先固定读图部分，只训练小型新决策部分，再用其他花园访问时拍摄的照片检查。傍晚图仍有错误，于是谨慎允许部分旧权重改变，然后重测。其他图片的经验有帮助，却不能代替本地样本。测试照片说明改动是否对训练之外的例子也有效。

### 02 / 理解核心概念

分类从杯子、书、球等标签列表中选择。首先，特征提取器把像素变成有用的数字。预测头把这些数字组合成每个标签的一个分数。原始分数也叫 logit。

训练利用带标签样本调整权重。损失是衡量当前输出与目标不符程度的数。迁移学习从另一批数据学到的权重开始。冻结使指定权重不变；微调允许它们随新任务改变。被复用的特征提取器通常称为主干。

Softmax 把分数变成总和为一的正数，但只比较列出的类别。未知物体的照片仍可能得到很高的杯子分数，所以高数值不等于识别正确的证据。

### 03 / 如何运用这个概念

先建立冻结特征的简单基线，使预处理符合预训练模型要求，再把预测头替换成自己的类别集合。相邻图像存在关联时，应按采集批次或来源划分数据。微调与基线要使用相同的留出组比较。按光照和视角查看错误，并保留足够评估数据，以便发现所谓更好的模型是否只是记住了局部模式。

### 04 / 官方教程与研究中的应用

PyTorch 的官方 Transfer Learning for Computer Vision 教程对比了调整预训练网络与把它作为固定特征提取器的做法。Stanford 的 CS231n 教学则提供了学习型视觉表示与识别的更广背景。Noor 的三类植物案例是原创例子；它与这些资料的联系，在于复用已学表示，并检验调整后是否能推广到新图像的工作流程。

### 05 / 公式与逐项解析

```text
scoreₖ = wₖ₁ × h₁ + wₖ₂ × h₂ + bₖ
pₖ = exp(scoreₖ) / sum of exp(all scores)
loss = −ln(p_correct)
```

h₁、h₂ 是概括图像的两个特征数。k 选择类别。wₖ₁、wₖ₂ 指定各特征对该类别的贡献，bₖ 加一个固定偏移。exp(a) 表示 e 的 a 次方，e 约为 2.718。把正的指数值除以总和，可让 p 的总和为 1。ln 是自然对数，即 exp 的逆运算。p_correct=1 时损失为零，它降低时损失增加。

### 06 / 一步步算出结果

使用特征 [1,2]。第一类权重 [1,0]，分数 1×1+0×2=1；第二类权重 [0,1]，分数 0×1+1×2=2；第三类权重 [0,0]，得 0。所有常数偏移均为零。

对 [1,2,0] 取指数得到约 [2.7183,7.3891,1]，总和 11.1073。各除以总和得到 [0.2447,0.6652,0.0900]。若第二类正确，损失 −ln(0.6652)≈0.4076。模型偏好正确标签，却仍给其他类分配概率。特征是给定的，从像素学到这些特征仍是另一项工作。

### 先认识这些词

- **特征:** 描述输入某种有用性质的数。

- **权重:** 可以从样本中学习的乘数。

- **损失:** 输出与目标不符时的数值惩罚。

- **Softmax:** 对分数取指数，再除以总和。

### 一步步想清楚

**为什么复用特征提取器？**

早先训练可能学到了边缘等有用模式。新样本可训练较小的预测头，但是否有效仍需检验。

**第二个分数为何是 2？**

该行是 [0, 1]，所以 0×1+1×2=2。

**0.6652 能证明物体属于第二类吗？**

不能。这是模型在这些标签间的相对偏好，模型仍可能出错。

<a id="lab-recognition-transfer-classification-head"></a>

### 把特征变成三个分数

手工特征让我们单独观察预测头。这不是训练好的图像分类器。

```python
import math
features = [1, 2]
weights = [[1, 0], [0, 1], [0, 0]]
scores = [sum(w * h for w, h in zip(row, features)) for row in weights]
exp_scores = [math.exp(s - max(scores)) for s in scores]
probabilities = [v / sum(exp_scores) for v in exp_scores]
print("scores:", scores)
print("probabilities:", [round(p, 4) for p in probabilities])
print(f"loss={-math.log(probabilities[1]):.4f}")
```

**在本地运行**

```sh
python recognition-transfer-classification-head.py
```

**预期输出**

```text
scores: [1, 2, 0]
probabilities: [0.2447, 0.6652, 0.09]
loss=0.4076
```

**逐步读懂代码**

每行权重通过乘法与加法得到一个类别分数。在 exp 前减最大分数可缩小数值，同时保持 softmax 不变。除以总和得到约 0.2447、0.6652、0.0900。索引 1 取第二类。math.log 求自然对数，负号使损失成为 0.4076。本代码没有训练主干。

### 这些知识可以用在哪里

**改变证据**

把特征改为 [2, 1]。前两个概率交换，说明相同预测头如何响应变化的证据。

**未知类别**

想象给杯子/书/球分类器展示自行车。证据不足时设置复核路线；softmax 本身无法添加自行车标签。

**类比的边界与注意事项:** 经验丰富的插画师类比，可能夸大能够迁移的能力。预训练可能包含无关捷径，较大的 softmax 值也可能对应错误类别。冻结特征是有用的基线，却不是通用答案；如何调整应依据数据量、相似性与评估证据决定。

**记住这一点:** 迁移表示，仔细定义新的判断任务，再用真正独立的图像组衡量改进。

### 本节资料来源

- [PyTorch: 计算机视觉迁移学习](https://docs.pytorch.org/tutorials/beginner/transfer_learning_tutorial.html)

- [Stanford University: CS231n：计算机视觉深度学习](https://cs231n.stanford.edu/2025/schedule.html)

<a id="detection-overlap"></a>

## 框定位候选对象，重叠度帮助消除重复

### 01 / 从一个完整故事开始

观鸟社想统计照片里的海鸥。检测器在同一只海鸥周围画了多个矩形，直接数框就会多算。Lea 查看最高分矩形，再与附近框比较。

她计算共享面积占总覆盖面积的比例。低分框若重叠很强，就按指定规则作为可能重复项删除。但拥挤海鸥仍困难：重叠框可能代表两只真实鸟。她测试这种情形后才选择规则。找物体与判断哪些框重复同一物体，是相关但不同的任务。

### 02 / 理解核心概念

检测回答两个问题：有什么物体，它们在哪里？检测器可能为同一物体提出多个矩形。每个候选框都有类别、分数和角点坐标。

交集是两个矩形共享的面积。并集是任一个矩形覆盖的面积，共享部分只算一次。交并比 IoU 用前者除以后者。不重叠时为零，两个相同且非空的框为一。

非极大值抑制 NMS 用于减少重复框。先保留剩余框中分数最高者。如果某个同类低分框与它的 IoU 超过阈值，就删除该低分框，然后重复。TorchVision 的基础 NMS 不接收类别标签，因此调用者需自行按类处理。NMS 阈值用于去重复，评估阈值用于判断是否匹配标注，两者用途不同。

### 03 / 如何运用这个概念

比较检测结果之前，先定义框坐标和类别处理方式。检查包含孤立对象、拥挤邻居与部分遮挡的叠加图。用验证数据选择分数阈值和抑制阈值，在最终评估时保持固定。

记录预测与标注的匹配方法，包括防止多个预测重复认领同一对象的规则。看起来合理的框是需要检查的证据，并不能证明计数正确。

### 04 / 官方教程与研究中的应用

TorchVision 官方检测教程在 Penn-Fudan 行人图像上微调 Mask R-CNN，并把框、类别与对象掩码作为结构化目标。NMS 参考文档则单独说明分数排序，以及重叠度严格大于给定阈值时的移除规则。结合阅读两份资料，可以区分完整检测示例与单个后处理操作，也能精确定义海鸟故事中的去重决策。

### 05 / 公式与逐项解析

```text
intersection = shared_width × shared_height
union = area_A + area_B − intersection
IoU = intersection / union
```

A、B 是两个矩形。每个写作 (x₁, y₁, x₂, y₂)，且 x₂>x₁、y₂>y₁。面积是 (x₂−x₁)×(y₂−y₁)，这里使用连续边界坐标，不额外加 1。共享宽度为 max(0, min(x₂A,x₂B)−max(x₁A,x₁B))，高度同理。min 取较小数，max 取较大数。外层 max 防止出现负重叠。

### 06 / 一步步算出结果

考虑三个自拟海鸥框：A = (0, 0, 4, 4)、B = (1, 1, 5, 5)、C = (6, 0, 8, 2)，分数分别为 0.9、0.8、0.7。A 与 B 的面积均为 16。它们的交集宽为 3、高为 3，因此面积为 9。并集面积为 16 + 16 − 9 = 23，所以 IoU = 9 / 23，约为 0.3913。

当 τ = 0.3 时，保留 A、抑制 B。C 与 A 没有重叠，因此保留 C，最终剩下两个框。当 τ = 0.5 时，B 也被保留，共有三个框。这些结果说明阈值的作用；没有标注，仍无法判断 A 与 B 指向同一只海鸥，还是两只相互遮挡的海鸥。

### 先认识这些词

- **检测:** 预测物体类别和位置。

- **交并比:** 共享面积除以总覆盖面积。

- **抑制:** 按规则删除较低分候选项。

### 一步步想清楚

**计算并集时为什么减交集？**

两面积相加时共享部分算了两次。减一次后就只算一次。

**23 从哪里来？**

两个框面积都是 16，交集为 9，所以 16+16−9=23。

**更高的 NMS 阈值会保留更多框吗？**

在这个双框例子中是的：0.3913 超过 0.3，但未超过 0.5。阈值不是正确概率。

<a id="lab-detection-overlap-box-overlap"></a>

### 测量两个候选框

为两个同类框计算一次抑制判断。

```python
a = (0, 0, 4, 4)
b = (1, 1, 5, 5)
width = max(0, min(a[2], b[2]) - max(a[0], b[0]))
height = max(0, min(a[3], b[3]) - max(a[1], b[1]))
intersection = width * height
area_a = (a[2] - a[0]) * (a[3] - a[1])
area_b = (b[2] - b[0]) * (b[3] - b[1])
iou = intersection / (area_a + area_b - intersection)
print(f"intersection={intersection}; IoU={iou:.4f}")
for threshold in [0.3, 0.5]:
    print(threshold, "suppress B" if iou > threshold else "keep B")
```

**在本地运行**

```sh
python detection-overlap-box-overlap.py
```

**预期输出**

```text
intersection=9; IoU=0.3913
0.3 suppress B
0.5 keep B
```

**逐步读懂代码**

索引 0、2 是左右边，1、3 是上下边。min、max 找到共享部分。接着算两个面积，将共享面积减一次，再相除。for 循环测试两个阈值，行内 if 选择消息。假设 A 分数较高。例子从已有框开始，并没有从像素检测物体。

### 这些知识可以用在哪里

**没有重叠**

把 B 移到 (6,0,8,2)。共享宽度为零，IoU 为零，两个阈值下都会保留 B。

**拥挤物体**

画两只重叠的鸟。若框重叠太多，NMS 可能删掉真实的第二只鸟；要单独检查拥挤情形。

**类比的边界与注意事项:** 铅笔圈的类比解释了重复候选，但高重叠度不能证明它们对应同一对象。贪心抑制可能移除真实邻居，高分框也可能定位不准。不同检测器可能采用其他去重设计；这里介绍的是常见操作，并非所有检测架构都使用的规则。

**记住这一点:** 把框视为候选，精确定义重叠，并把去重规则与检测质量的判定规则分别验证。

### 本节资料来源

- [PyTorch: TorchVision 目标检测微调教程](https://docs.pytorch.org/tutorials/intermediate/torchvision_tutorial.html)

- [TorchVision: 非极大值抑制](https://docs.pytorch.org/vision/stable/generated/torchvision.ops.nms.html)

<a id="detection-average-precision"></a>

## 检测分数：顺着排序列表评估

### 01 / 从一个完整故事开始

观鸟社在同一组照片上比较两个检测器。两者都找到两只鸟，但其中一个最先以高分报告窗户反光。小瑞发现，发现数量忽略了警报顺序。她按分数排序，逐条检查。

每只真实鸟只允许匹配一次，重复框算额外误报，不能算第二次发现。她在每次找到新鸟时记录精确率。现在大家看清了，即使后来放宽阈值能找全鸟，排在前面的错误警报仍会降低排序结果的实用性。

### 02 / 理解核心概念

先选择类别与 IoU 匹配阈值，再按预测分数由高到低排列框。预测若匹配合格且尚未被匹配的参考物体，就是真正例；否则是假正例。随着列表变长，精确率与召回率都会变化。

平均精确率 AP 概括精确率—召回率曲线，但具体定义有多种。本教学计算用非插值方式，把每次召回率增加量乘该时精确率后相加。COCO 使用在指定召回率点采样的插值精确率，并按规定跨 IoU 阈值与类别平均。因此一个小例子的 AP 不是 COCO 基准分数。

### 03 / 如何运用这个概念

在明确匹配规则下使用排序评估比较检测器。应同时保存分数、框、类别与参考数量。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
Pₖ = true_matches_in_first_k / k
Rₖ = true_matches_in_first_k / reference_count
AP_toy = sum of Pₖ × (Rₖ − Rₖ₋₁)
```

k 是从 1 开始的排名。Pₖ、Rₖ 是读完 k 条预测后的精确率、召回率。R₀ 为零。reference_count 包括该类所有参考物体，也包括漏检。总和遍历所有预测，只有新的正确匹配会增加召回率。

### 06 / 一步步算出结果

有两只参考鸟，排序结果为 [正确,错误,正确]。精确率为 1、1/2、2/3，召回率为 1/2、1/2、1。AP_toy=1×1/2+(1/2)×0+(2/3)×1/2=5/6≈0.8333。

如果只在首位找到一只鸟，AP_toy=1×1/2=0.5，并非 1。漏检必须计入。

### 先认识这些词

- **排序:** 按分数排列预测。

- **平均精确率:** 按指定约定概括召回率增加时的精确率。

- **匹配阈值:** 把预测与参考配对所需的最小重叠。

### 一步步想清楚

**为什么一个参考物体只匹配一次？**

否则重复框可以把同一个物体算成多次发现。

**为什么第二名对 AP 的贡献为零？**

它是误报，召回率没有增加，但仍会降低后续精确率。

**若参考有三只鸟呢？**

找到两只时召回率为 2/3，教学 AP 为 (1+2/3)/3≈0.5556。

<a id="lab-detection-average-precision-ranked-ap"></a>

### 读取三条排序检测

输入是已检查的匹配结果，不是原始框。

```python
matches = [True, False, True]
reference_count = 2
found = 0
ap = 0.0
for rank, correct in enumerate(matches, start=1):
    found += int(correct)
    precision = found / rank
    recall = found / reference_count
    if correct:
        ap += precision / reference_count
    print(f"rank={rank}: precision={precision:.4f}, recall={recall:.4f}")
print(f"toy AP={ap:.4f}")
```

**在本地运行**

```sh
python detection-average-precision-ranked-ap.py
```

**预期输出**

```text
rank=1: precision=1.0000, recall=0.5000
rank=2: precision=0.5000, recall=0.5000
rank=3: precision=0.6667, recall=1.0000
toy AP=0.8333
```

**逐步读懂代码**

enumerate 提供排名及对应结果。int 把 True 变成 1、False 变成 0。found 累计匹配数。每次正确匹配使召回率增加 1/reference_count，因此贡献是 precision/reference_count。+= 加入累计值。打印的 0.8333 仅是上述教学 AP，不是完整 COCO 指标。

### 这些知识可以用在哪里

**错误排第一**

改为 [False,True,True]。AP=((1/2)+(2/3))/2≈0.5833。靠前错误影响很大。

**漏检物体**

保留 [True] 和两个参考。确认即使精确率为一，AP 仍为 0.5。

**类比的边界与注意事项:** 本例假设真假匹配已核查，没有实现 COCO 的 crowd 规则、插值、多 IoU 阈值或尺寸分组。

**记住这一点:** 在明确匹配规则下使用排序评估比较检测器。应同时保存分数、框、类别与参考数量。

### 本节资料来源

- [COCO authors: COCO 官方评估实现](https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocotools/cocoeval.py)

- [TorchVision: 非极大值抑制](https://docs.pytorch.org/vision/stable/generated/torchvision.ops.nms.html)

<a id="segmentation-regions"></a>

## 从一个矩形走向真正属于对象的像素

### 01 / 从一个完整故事开始

园艺课想测照片中的叶片面积。叶片边界框包含空背景，所以 Kim 改为逐像素标叶。第一个自动结果把所有像素都当背景。因为叶片小，它大部分像素都判断对，却漏掉了全班真正想测的东西。

Kim 将选中的叶片区域与仔细核实的参考比较，统计共享、多选和漏选像素。新报告清楚揭示失败。她还问，相接叶片应作为一个叶类区域，还是分别编号？这个答案会在选择模型之前改变所需标签。

### 02 / 理解核心概念

边界框说明物体大致在哪里。分割更进一步，为每个像素分配标签。掩码是保存这些标签的网格；二值掩码中，1 表示选中区域，0 表示背景。语义分割标记物体类型，实例分割还区分同类的不同物体。

比较预测前景与核实后的参考掩码时，要数各自选中多少像素，以及共享多少像素。IoU 和 Dice 关注被选区域。普通像素准确率也奖励背景，因此即使漏掉所有小物体，数值仍可能很高。

这些公式使用是或否的掩码，不使用不确定概率。完整训练可能采用相关但不同的损失公式。必须说明按图平均还是合并所有像素。若两个掩码都为空，分母会为零，需要明确约定，不能直接相除。

### 03 / 如何运用这个概念

问题涉及某种材料的覆盖范围时，选择语义标签；需要区分不同对象时，选择实例标签。把掩码叠加在原图上，检查边界与小区域。报告前景重叠度时，同时保留类别频率和代表性错误，并说明是在图像、对象还是类别之间取平均。评估前先定义空掩码处理规则，避免没有前景的图片悄悄改变平均分数的含义。

### 04 / 官方教程与研究中的应用

TorchVision 文档提供了用于语义分割、采用 ResNet-50 主干的 FCN 模型。其检测教程则使用 Mask R-CNN 与 Penn-Fudan 标注，区分不同行人及其掩码。这是两类输出的具体实例。scikit-learn 指标指南给出了相关的 Jaccard 与 F 度量定义；在明确前景约定后，可以把这些定义用于展平的二值像素标签。

### 05 / 公式与逐项解析

```text
IoU = |P ∩ G| / |P ∪ G|
Dice = 2|P ∩ G| / (|P| + |G|)
Dice = 2IoU / (1 + IoU)  (|P ∪ G| > 0)
```

P 是被预测为前景的像素集合，G 是参考前景集合。竖线表示像素数量；∩ 选取同时属于两个集合的像素，∪ 选取属于任一集合的像素。IoU 表示交并比，Dice 是上式定义的重叠系数。分母为正时，两者均介于 0 与 1 之间。如果两个掩码都为空，原始分式没有定义；必须选择并报告一种约定，例如把这种一致情况计为 1，或在指定的平均方式中排除这种情况。

### 06 / 一步步算出结果

设一张自拟图像包含 1000 个像素，参考标注中有 40 个叶片像素。全部预测为背景时，像素准确率达到 960 / 1000 = 96%，但由于没有找到任何叶片像素，前景 IoU 与 Dice 均为 0。第二个预测把 50 个像素标为叶片，其中 30 个与参考前景重合。并集包含 50 + 40 − 30 = 60 个像素，所以 IoU = 30 / 60 = 0.5。

Dice = 60 / 90，约为 0.6667，也等于 2 × 0.5 / 1.5。该预测有 20 个误报前景像素，漏掉 10 个叶片像素。两个重叠分数用不同尺度描述相同掩码；不能把它们与像素准确率直接作数值比较，仿佛三者衡量的是同一种量。

### 先认识这些词

- **掩码:** 保存选中状态或类别标签的像素网格。

- **前景:** 任务关注并选中的区域。

- **实例:** 一个独立物体，例如三片叶子中的一片。

### 一步步想清楚

**为什么全背景准确率会误导？**

大部分像素可能是背景。正确背景判断会掩盖前景物体的遗漏。

**代码中的并集为何是四？**

位置 0、1、2、3 至少被一个掩码选中，4、5 均未被选中。

**两个空掩码是否产生 IoU=0/0？**

是的，原始公式没有定义，评估必须明确处理方式。

**忽略像素还算假阳性吗？**

本规则下不算。我们在计数前完整排除该位置。应写明规则，保证所有模型在相同像素上评估。

<a id="lab-segmentation-regions-mask-counts"></a>

### 统计前景重叠

用小型二值列表代替展平后的掩码。

```python
prediction = [1, 1, 1, 0, 0, 0]
reference = [0, 1, 1, 1, 0, 0]
shared = sum(p == 1 and g == 1 for p, g in zip(prediction, reference))
union = sum(p == 1 or g == 1 for p, g in zip(prediction, reference))
iou = shared / union
dice = 2 * shared / (sum(prediction) + sum(reference))
print("shared:", shared, "union:", union)
print(f"IoU={iou:.4f}; Dice={dice:.4f}")
```

**在本地运行**

```sh
python segmentation-regions-mask-counts.py
```

**预期输出**

```text
shared: 2 union: 4
IoU=0.5000; Dice=0.6667
```

**逐步读懂代码**

zip 遍历对应位置。and 只在两个掩码都选中时计数；or 在任一掩码选中时计数。Python 累加时把 True 当 1、False 当 0。共享像素为二，并集为四。每个列表选中三个像素，因此 Dice 为 4/6。本例评估已有掩码，没有学习或预测掩码。

<a id="lab-segmentation-regions-confusion-matrix-miou"></a>

### 排除忽略像素，计算分割 IoU

道路相机区分背景、道路和小物体。有些像素没有可靠标注。米娜起初把所有像素都计入，分数因此失真。她排除忽略像素，建立混淆矩阵，再从行列读取每个类别。这样，小物体有了自己的分数，不会淹没在大量背景像素中。

```python
from math import isclose

def segmentation_report(truth, prediction, classes, ignore=255):
    if len(truth) != len(prediction) or classes <= 0:
        raise ValueError("aligned arrays and positive class count required")
    matrix = [[0] * classes for _ in range(classes)]
    for actual, predicted in zip(truth, prediction):
        if actual == ignore:
            continue
        if not (0 <= actual < classes and 0 <= predicted < classes):
            raise ValueError("unknown class")
        matrix[actual][predicted] += 1
    ious = []
    for c in range(classes):
        tp = matrix[c][c]
        actual_total = sum(matrix[c])
        predicted_total = sum(row[c] for row in matrix)
        union = actual_total + predicted_total - tp
        ious.append(tp / union if union else None)
    defined = [value for value in ious if value is not None]
    mean = sum(defined) / len(defined) if defined else None
    total = sum(map(sum, matrix))
    accuracy = sum(matrix[c][c] for c in range(classes)) / total if total else None
    return matrix, ious, mean, accuracy

truth = [0, 0, 0, 1, 1, 2, 2, 255]
prediction = [0, 0, 1, 1, 2, 2, 0, 1]
matrix, ious, mean, accuracy = segmentation_report(truth, prediction, 3)
assert matrix == [[2, 1, 0], [0, 1, 1], [1, 0, 1]]
assert isclose(mean, 7 / 18) and isclose(accuracy, 4 / 7)
assert segmentation_report([255], [1], 3)[2] is None
assert segmentation_report([0], [0], 3)[1] == [1.0, None, None]
print("rows = truth; columns = prediction:")
for row in matrix:
    print(row)
print("class IoU:", [round(value, 6) for value in ious])
print(f"mean IoU: {mean:.6f}")
print(f"pixel accuracy: {accuracy:.6f}")
```

**在本地运行**

```sh
python segmentation-regions-confusion-matrix-miou.py
```

**预期输出**

```text
rows = truth; columns = prediction:
[2, 1, 0]
[0, 1, 1]
[1, 0, 1]
class IoU: [0.5, 0.333333, 0.333333]
mean IoU: 0.388889
pixel accuracy: 0.571429
```

**按执行顺序理解**

1. 看到真值为忽略标签时，直接跳过；该位置的预测也不进入任何行列。

2. 对角线是 TP；行总数是真实像素数，列总数是预测像素数。

3. 并集=真实总数+预测总数-TP，减法避免交集重复计数。

4. 对有定义的类别 IoU 等权平均；若真值和预测都没有该类，则返回 None。

**逐步读懂代码**

IoU_c=TP_c/(TP_c+FP_c+FN_c)。背景类 TP=2、FP=1、FN=1，因此 IoU=2/4=0.5；道路和物体均为 1/3。平均 IoU=(1/2+1/3+1/3)/3=7/18，约 0.388889；像素准确率则是 4/7，它们回答的问题不同。

本混淆矩阵汇总所有参与评估的像素。先汇总全部图像再算 IoU，与先逐图算 IoU 再平均，结果可能不同，必须说明采用哪种方式。本实现只在并集为零时排除类别；真值没有某类，但模型预测过它，该类并集非零，IoU 应为零。不同基准可能采用不同缺失类别规则。

全是忽略像素的图像没有有效分数，并不表示完美预测。P 个像素、C 个类别时，本直观实现花费 O(P+C²) 时间、O(C²) 空间。比较模型时，应固定忽略掩码、类别映射、汇总方式与缩放方法。离散标签掩码不能用会产生小数类别编号的方式缩放。

### 这些知识可以用在哪里

**完全匹配区域**

让 prediction 与 reference 相同。交集、并集都为三，IoU 和 Dice 都为一。

**区分叶片**

画两片相接的叶子。语义叶片掩码可能连在一起；实例标签仍需区分叶 A 与叶 B。

**类比的边界与注意事项:** 一个着色区域的含义取决于标注规则。模糊边界、透明对象和相互重叠的实例可能需要细致约定。较高的平均重叠度仍可能掩盖小对象漏检或重要边界错误，而仅凭前景面积无法确定个体数量。

**记住这一点:** 让掩码类型对应实际问题，检查占少数的前景，并明确重叠度与空掩码情况的评分方式。

### 本节资料来源

- [TorchVision: 使用 ResNet-50 主干的 FCN](https://docs.pytorch.org/vision/stable/models/generated/torchvision.models.segmentation.fcn_resnet50.html)

- [PyTorch: TorchVision 目标检测微调教程](https://docs.pytorch.org/tutorials/intermediate/torchvision_tutorial.html)

- [scikit-learn: 指标与评分：量化预测质量](https://scikit-learn.org/stable/modules/model_evaluation.html)

- [Cityscapes: 基准评估：像素级 IoU 与忽略标签](https://www.cityscapes-dataset.com/benchmarks/)

<a id="tracking-identity"></a>

## 跟踪：跨帧保留身份

### 01 / 从一个完整故事开始

学校机器人小组拍摄两辆玩具车穿过桌面。逐帧检测车容易描述，但若每个检测都计数，同一辆车会被重复统计。Ana 给每辆车分配轨迹编号。她先预测已有车下一刻的位置，再把新检测与邻近轨迹配对。

两车交叉时，最近位置规则交换了名称。她保留这段视频作为测试，并考虑运动历史。大家发现，跟踪是序列问题：单帧中合理的框本身无法证明它属于此前看到的同一辆车。

### 02 / 理解核心概念

检测提供每帧观察，跟踪把观察随时间连接起来。轨迹保存身份和近期状态，例如中心位置、速度。在匹配新检测前，预测先把状态向前推进。

简单一维模型中，速度是每帧位置变化。将速度加到上次位置，预测下一位置。只有最近检测的距离在指定门限内才关联。真实多物体系统需要一对一分配，防止同一检测更新两条轨迹。SORT 将运动预测与分配结合；本例只隔离一条轨迹的预测与门限。

### 03 / 如何运用这个概念

跟踪可用于统计移动物体，或在视频中跟随玩具机器人和野生动物。应检查交叉、遮挡与漏检。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
v = x_now − x_previous
x_predicted = x_now + v
match if |x_detection − x_predicted| ≤ gate
```

x 是以像素计的水平中心位置。v 是每帧移动像素数，假设帧间隔相等。竖线表示绝对值，因此距离非负。gate 是本例关联允许的最大距离。

### 06 / 一步步算出结果

车从 x=2 移到 x=5，所以 v=3。下一预测为 5+3=8。检测 8、15 与预测的距离为 0、7。

门限为 2 时选 8。若只有 15，就让轨迹暂时无匹配，不强行连接远处检测。

### 先认识这些词

- **轨迹:** 跨帧连接的身份及变化状态。

- **关联:** 将新检测与已有轨迹配对。

- **遮挡:** 物体被部分或完全遮住。

### 一步步想清楚

**为什么先预测再匹配？**

移动物体预计会离开旧位置。预测提供更合适的比较点。

**为什么预测八？**

上次变化是 5−2=3；向 5 加相同变化得到 8。

**没有检测通过门限怎么办？**

记录观察缺失。完整跟踪器可短暂保留预测，但不应把它当作已确认观察。

<a id="lab-tracking-identity-track-gate"></a>

### 预测一条轨迹并设门限

使用等时间间隔帧中的一辆车。

```python
previous, current = 2, 5
velocity = current - previous
predicted = current + velocity
detections = [8, 15]
nearest = min(detections, key=lambda x: abs(x - predicted))
accepted = abs(nearest - predicted) <= 2
print("predicted:", predicted)
print("nearest:", nearest, "accepted:", accepted)
```

**在本地运行**

```sh
python tracking-identity-track-gate.py
```

**预期输出**

```text
predicted: 8
nearest: 8 accepted: True
```

**逐步读懂代码**

相减估计速度，相加预测下一个中心。min 搜索列表。key 是一个返回距预测绝对距离的 lambda 小函数，因此选的是最小距离而非最小坐标。<= 检查门限。结果接受 8。这是单轨迹关联示例，不是训练好的跟踪器。

### 这些知识可以用在哪里

**突然转向**

把检测改为 [4,15]。最近为 4，但距离 4 超过门限。即使车真的转向，规则也会拒绝。

**两辆车**

画两个预测中心与两个检测。确认身份前应要求每个检测只分给一条轨迹，独立最近选择可能冲突。

**类比的边界与注意事项:** 恒定速度在突然转弯时失效，仅靠位置无法解决相同物体交叉。例子只有一条轨迹，没有实现 SORT 的状态估计或全局分配。

**记住这一点:** 跟踪可用于统计移动物体，或在视频中跟随玩具机器人和野生动物。应检查交叉、遮挡与漏检。

### 本节资料来源

- [Bewley et al.: 简单在线实时跟踪](https://arxiv.org/abs/1602.00763)

<a id="ocr-reading"></a>

## OCR：把可见笔画变成文字

### 01 / 从一个完整故事开始

图书社扫描书架标签，希望输入编号就能查书。一张照片倾斜，字母 O 被读成数字零。Noah 先校正裁剪图，再检查每个字符是否有足够像素。他把识别文字与可见标签比较，并把小图保留在结果旁。

拼写规则有助于普通词，却可能破坏特殊书目编号，所以编号留待复核。改进后的流程把读字当作多个相关判断：找文字、准备图像、识别符号、核对证据。一个通顺句子本身并不能证明转写忠实。

### 02 / 理解核心概念

光学字符识别 OCR 把图像文字转为字符。常见流程先检测文字区域，再校正旋转、识别字符或序列，最后检查输出顺序。页面布局很重要：把两栏当成一行会改变内容。

小型模板匹配可讲解识别步骤。为每个候选符号存一个二值图案，再比较观察图案与每个模板，统计不同位置。差异最少者获选。完整 OCR 系统学习丰富得多的形状和语言模式，还要处理字体、间距与图像质量变化。

### 03 / 如何运用这个概念

OCR 可用于扫描件搜索、标签识别与文档阅读。应保留原图区域及阅读顺序，便于核对不确定文字。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
mismatch(pattern, template) = number of unequal positions
chosen_label = label with the smallest mismatch
```

pattern 是从小图逐行读取的固定长度零一列表。template 是相同大小的参考列表。每个不同位置贡献一，相同位置贡献零。这是二值图案的汉明距离。

### 06 / 一步步算出结果

使用七个位置。H 教学模板为 [1,0,1,1,1,0,1]，I 为 [0,1,0,1,0,1,0]。观察值 [1,0,1,1,1,0,0] 只在末位与 H 不同，距离为 1；与 I 有五处不同。因此在该教学编码下 H 更接近。

### 先认识这些词

- **光学字符识别:** 把图像中的可见文字变为字符数据。

- **模板:** 用于比较的已存参考样本。

- **阅读顺序:** 页面各区域应被阅读的次序。

### 一步步想清楚

**为什么保留裁剪小图？**

它提供核对符号是否读对的直接证据。

**为什么 H 的距离是一？**

只有第七位不同，其他六位相同。

**词典总能纠正 O 与 0 吗？**

不能。书架编号可能合法地包含其中任一种，语言合理性并不足够。

<a id="lab-ocr-reading-ocr-template"></a>

### 统计符号差异

比较虚构二值符号图案；不是 Tesseract，也不是训练 OCR 模型。

```python
templates = {"H": [1, 0, 1, 1, 1, 0, 1], "I": [0, 1, 0, 1, 0, 1, 0]}
observed = [1, 0, 1, 1, 1, 0, 0]
distances = {}
for label, template in templates.items():
    distances[label] = sum(a != b for a, b in zip(observed, template))
print("distances:", distances)
print("closest:", min(distances, key=distances.get))
```

**在本地运行**

```sh
python ocr-reading-ocr-template.py
```

**预期输出**

```text
distances: {'H': 1, 'I': 5}
closest: H
```

**逐步读懂代码**

字典为每个标签保存列表。循环用 zip 配对观察值与模板位置；不等时 != 返回 True，sum 统计 True。distances.get 将每个标签的距离提供给 min，最终选 H。这里假定长度相等，真实输入应检查。未实现文字区域检测。

### 这些知识可以用在哪里

**不可读标记**

试全零观察值并检查两个距离。接受标签前添加最大允许差异数。

**双栏页面**

在纸上写两栏短文字。比较按行读与按栏读。单个字符正确不保证句子顺序正确。

**类比的边界与注意事项:** 七位图案是虚构教学编码，不是真实字形图像。即使输入不可读，也总会有最近模板。未知符号与平局需要明确复核选项。

**记住这一点:** OCR 可用于扫描件搜索、标签识别与文档阅读。应保留原图区域及阅读顺序，便于核对不确定文字。

### 本节资料来源

- [Tesseract: 改善输出质量](https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html)

<a id="visual-tokens"></a>

## 让图像块交换信息

### 01 / 从一个完整故事开始

Mateo 制作一个按样式分类手工风筝照片的教学展示。角落条纹只有与尾部一起看才有意义。他在照片上画网格，用短数字列表表示每格，让这些列表在模型选标签前交换信息。

他起初以为每格宽度减半只会让工作加倍。重新计数才发现格数增至四倍，可比较的配对更多。他把计数放在例子旁。课程因此同时说明远处区域为何能交互，以及保留更细细节为何可能需要更多计算。

### 02 / 理解核心概念

视觉词元是一组表示图像局部的数字。先把图像切成等大图像块。展平按顺序把块内通道值读成一个长列表，不会添加信息。学习得到的投影通过加权和，把长列表转成固定数量的特征。

原始 Vision Transformer 加入位置信息和一个类别词元。自注意力让每个词元与其他词元比较并混合信息。类别词元汇集分类所需的信息。图像块只是网格区域，不一定包含一个完整物体。

有 L 个词元时，全注意力考虑 L 个来源和 L 个目标，因此有 L×L 对。更小的块提供更多细节，也增加许多比较。这个计数只描述模型的一部分，不能直接保证所有实现的总运行时间或内存。

### 03 / 如何运用这个概念

选择图像分辨率与图像块大小之前，先计算图像块数量，以及包括特殊词元在内的序列长度。检查图像尺寸能否整除，还是需要明确的缩放或填充规则。比较细节保留程度与实际资源消耗，并遵循预训练模型的位置处理方式。对于新任务，先检查缩放是否丢失有用证据，再分析注意力是否造成了错误。

### 04 / 官方教程与研究中的应用

原始 ViT 论文通过投影后的图像块与 Transformer 编码器完成图像分类，并研究大规模预训练后的迁移。TorchVision 的 VisionTransformer 实现使图像块投影和额外类别词元可以直接检查。原始 Transformer 论文解释了这种信息交换背后的注意力运算。这些资料支持这里介绍的设计；手工风筝展示及其小型数值例子是原创教学情景。

### 05 / 公式与逐项解析

```text
patch_count = (H / P) × (W / P)
values_per_patch = P × P × C
L = patch_count + 1
pair_count = L × L
```

H、W 是像素高度与宽度。P 是正方形图像块的边长，这里假设能整除图像尺寸。C 是颜色通道数。L 是加上一个类别词元后的词元数；类别词元是原始 Vision Transformer 使用的可学习汇总位置。pair_count 为一个全注意力头统计所有有顺序的词元对。一个头是一组比较与混合操作。

### 06 / 一步步算出结果

32×32 的 RGB 图切成 8×8 块，每边四块，共 4×4=16 块。每块有 8×8×3=192 个通道值。学习投影可计算 64 种加权和，将每个 192 数列表转为例如 64 个特征数。

加入一个类别词元后 L=17，全注意力每头有 17×17=289 对。改用 4×4 块时，有 8×8=64 块、65 词元、65²=4225 对。倍率为 4225/289≈14.62。这是注意力部分的比较计数，不表示完整运行时间有相同倍率。

### 先认识这些词

- **图像块:** 从图像切出的一个小网格区域。

- **词元:** 序列中被处理的一个数值单元。

- **投影:** 用加权和把特征列表转换到另一种大小。

- **自注意力:** 在同一序列的词元之间混合信息。

### 一步步想清楚

**为什么保留块的位置？**

相同图像块以不同方式排列可能形成另一场景。位置顺序携带空间证据。

**8×8 的 RGB 块里有多少数？**

8×8×3=192；64 个像素各有三个通道值。

**块数四倍是否意味着比较对数四倍？**

不是。比较对数对词元数平方。这里因额外类别词元，倍率为 4225/289≈14.62。

<a id="lab-visual-tokens-patch-budget"></a>

### 统计图像块与比较次数

先比较两种块大小，再考虑完整模型。

```python
height, width, channels = 32, 32, 3
for patch_size in [8, 4]:
    patches = (height // patch_size) * (width // patch_size)
    values = patch_size * patch_size * channels
    tokens = patches + 1
    print(f"P={patch_size}: patches={patches}, values={values}, pairs={tokens ** 2}")
```

**在本地运行**

```sh
python visual-tokens-patch-budget.py
```

**预期输出**

```text
P=8: patches=16, values=192, pairs=289
P=4: patches=64, values=48, pairs=4225
```

**逐步读懂代码**

第一行给三个尺寸变量赋值。循环对边长 8 与 4 重复计算。// 是整数除法，这里尺寸都能整除。加一是类别汇总词元。**2 表示词元数乘自身。得到 289、4225 对；并未实现注意力、学习投影或训练好的 Transformer。

### 这些知识可以用在哪里

**细小文字**

考虑一块落在大图像块内部的小标牌。小块可能保留更多细节，也增加比较量。需要同时评估读字质量与成本。

**没有汇总词元**

去掉 +1，模拟对图像块输出汇总的变体。P=8 时有 16 个词元、256 对。这属于不同设计。

**类比的边界与注意事项:** 交换描述的类比，并不意味着词元像人一样推理，也不意味着注意力权重可以解释每次判断。简化计算针对嵌入宽度固定的普通全注意力。其他架构可以使用窗口、池化或不同词元设计，而更细的图像块本身也不保证识别更好。

**记住这一点:** 明确计算图像块与特殊词元：更细的视觉单元会增加可能的交互，而有用细节与实际运行时间仍需测量。

### 本节资料来源

- [Dosovitskiy et al.: 一幅图像相当于 16×16 个词](https://arxiv.org/abs/2010.11929)

- [TorchVision: VisionTransformer 实现](https://docs.pytorch.org/vision/stable/_modules/torchvision/models/vision_transformer.html)

- [Vaswani et al.: 注意力就是你所需要的一切](https://arxiv.org/abs/1706.03762)

<a id="defect-inspection"></a>

## 缺陷检查：发现异常区域

### 01 / 从一个完整故事开始

陶艺社拍摄课堂制作的瓷片。大多数平整，少数有暗色缺口。Elena 先将每张照片与典型完好瓷片比较，一个区域差异很大，值得仔细看。但两次拍摄间台灯移动，也让完好瓷片显得异常。

她固定相机与灯光，查看剩余差异，再将可疑区域交给人检查。团队不再把每个异常像素都叫缺陷。比较有了明确任务：建议看哪里；真实缺口与无害变化样本则用来判断建议是否有用。

### 02 / 理解核心概念

缺陷是任务定义的问题，例如瓷片缺口。异常是与参考数据不同的情况。两者不相同：陌生但无害的花纹也可能异常。异常检测系统可从正常样本学习典型外观，再对偏离程度评分。

最简单基线对对齐后的观察图与参考图逐点求绝对差，再用阈值把差值图变成候选区域。真实系统常比较学习特征而非原始像素。应使用独立采集的正常及缺陷样本，同时评估整图警报与可疑区域位置。

### 03 / 如何运用这个概念

可用异常图辅助检查纹理或物体。必须区分异常外观与已确认缺陷。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
dᵢ = |observedᵢ − referenceᵢ|
flagᵢ = dᵢ > threshold
image_score = max(d₁, …, dₙ)
```

i 选择一个对应像素或特征。dᵢ 是其非负绝对差。n 为比较位置数。max 取最大差。flag 是是或否判断。所有值必须使用同一尺度，并在空间上对齐。

### 06 / 一步步算出结果

参考 [10,10,10,10] 与观察 [11,9,25,10] 得到差 [1,1,15,0]。阈值 5 只标记从零计数的位置 2，整图分数为 15。若光照给每个值都加 8，即使没有局部缺口，也可能四处都被标记。

### 先认识这些词

- **异常:** 与参考样本不同的观察。

- **缺陷:** 由实际检查任务定义的问题。

- **基线:** 作为比较起点的简单方法。

### 一步步想清楚

**为什么区分异常与缺陷？**

新的无害图案可以很异常，细微缺陷也可能看着熟悉。

**为什么标记位置二？**

该处 |25−10|=15，是唯一超过 5 的差。

**更亮的灯会触发警报吗？**

会。即使没有损伤，全局亮度变化也影响原始像素差。

<a id="lab-defect-inspection-difference-inspection"></a>

### 找到最大变化

用四位置窄条单独说明差异图。

```python
reference = [10, 10, 10, 10]
observed = [11, 9, 25, 10]
differences = [abs(x - r) for x, r in zip(observed, reference)]
flagged = [i for i, value in enumerate(differences) if value > 5]
print("differences:", differences)
print("flagged positions:", flagged)
print("image score:", max(differences))
```

**在本地运行**

```sh
python defect-inspection-difference-inspection.py
```

**预期输出**

```text
differences: [1, 1, 15, 0]
flagged positions: [2]
image score: 15
```

**逐步读懂代码**

zip 配对对齐位置。abs 去掉差值符号。第二个列表推导式用 enumerate 保留位置编号，用 if 仅留下大于 5 的值。max 得分 15。高差异只表明复核候选，不证明存在缺陷。这是使用手设参考的未训练基线。

### 这些知识可以用在哪里

**无害变化**

使用 observed=[12,11,9,10]。差为 [2,1,1,0]，该阈值不发警报。

**光照改变**

使用 observed=[18,18,18,18]。每个位置都被标记。说明为何调模型前需要控制拍摄条件。

**类比的边界与注意事项:** 像素差对对齐和光照敏感。MVTec AD 是工业异常检测研究数据集，不能证明这个小规则满足实际检测要求。

**记住这一点:** 可用异常图辅助检查纹理或物体。必须区分异常外观与已确认缺陷。

### 本节资料来源

- [MVTec: MVTec AD 异常检测数据集](https://www.mvtec.com/research-teaching/datasets/mvtec-ad)

<a id="evaluation-shift"></a>

## 用下一次采集检验，而非只看熟悉的邻帧

### 01 / 从一个完整故事开始

野生动物社训练相机图像筛选器，并庆祝很高测试分。Sam 发现同一次连拍的相邻帧同时出现在训练与测试文件夹。测试虽是新文件，却几乎是已见图片的副本。

他将每次连拍放在同组，并另外留出整处相机位置检查。分数下降，尤其是夜间。他统计正确警报、错误警报与漏掉动物，不再只给百分比。新测试更难，却能回答新相机上的实用问题，也指出哪些条件需要更多例子或人工复核。

### 02 / 理解核心概念

评估考察系统面对没有学过的样本时的表现。训练集用于调整权重，验证集用于选择设置，最终测试集用于报告结果。近似重复图像和相关拍摄场次应放在同一部分。否则即使文件名是新的，测试内容也可能早已熟悉。

分组应符合未来使用情境。测试新相机时留出整台相机的数据，测试新物体时按物体身份留出。训练与使用之间的光照、视角或相机变化称为领域偏移。一种条件下表现好，不代表另一条件也好。

对于是或否任务，真正例是正确警报，假正例是错误警报，假负例是漏检。精确率问警报中多少正确，召回率问真实目标中找到多少。要同时报告计数与比例，并单独检查重要条件。

### 03 / 如何运用这个概念

划分数据前，先写清未来使用问题：熟悉相机的新帧、新采集时段，或全新地点，需要不同证据。据此对相关采集记录分组，并保留最终测试集。用验证数据选择阈值，按相关条件检查错误，同时报告总体与各组的计数。应明确测量新环境中的表现，而不是假设一个汇总分数能够原样迁移。

### 04 / 官方教程与研究中的应用

WILDS 研究基准包含野生动物监测中跨野外自动相机的分布偏移，提供了图像来源为何重要的具体记录。scikit-learn 的 GroupKFold 文档展示如何在训练折与测试折之间保持组身份分离，其评估指南则定义了精确率与召回率。这些资料支持考虑采集来源的评估方式；单凭分组划分，仍不能证明模型能适应未来每一台相机或每一个季节。

### 05 / 公式与逐项解析

```text
Precision = TP / (TP + FP)
Recall = TP / (TP + FN)
F₁ = 2TP / (2TP + FP + FN)
```

TP 是真正例：有野生动物且正确警报的图。FP 是假正例：空图却错误警报。FN 是假负例：有动物却漏掉的图。这里数图像，不数像素或框。F₁ 组合精确率和召回率；给出的计数公式无需先学另一平均规则。比较时应固定决策阈值。若分母为零，要说明指标怎样处理并报告计数。

### 06 / 一步步算出结果

假设一个自拟留出组包含 100 张图片，其中 20 张有野生动物，80 张为空场景。在选定阈值下，模型对 25 张图片发出提示，其中 15 张确有动物，因此 TP = 15、FP = 10。它漏掉了 5 张有动物的图片，所以 FN = 5。精确率为 15 / 25 = 0.60，召回率为 15 / 20 = 0.75，F₁ 为 30 / 45，约为 0.6667。

其余 70 张空场景被正确排除。因此总体准确率为 85 / 100 = 85%，但单看这个数字，会掩盖十次误报与五次漏报。这些计数描述的是该抽样组在该阈值下的表现，不能保证模型在未见过的相机地点上也有同样表现。

### 先认识这些词

- **精确率:** 正确警报数除以全部警报数。

- **召回率:** 找到的真实目标数除以全部真实目标数。

- **信息泄漏:** 测试信息进入模型开发，使评估产生误导。

- **领域偏移:** 开发与使用时的数据条件不同。

### 一步步想清楚

**为什么把相邻视频帧放在同一组？**

它们共享大量场景。拆开后可能测到的是识别近似副本，而非泛化。

**召回率为什么是 15/20？**

找到 15 张动物图、漏掉 5 张，因此真实正例共 20 张。

**错误警报翻倍会改变召回率吗？**

若真正例和漏检数不变，召回率仍是 0.75。精确率降为 15/35≈0.4286。

<a id="lab-evaluation-shift-evaluate-alerts"></a>

### 统计警报与漏检

从一个留出组的明确计数计算指标。

```python
true_positive, false_positive, false_negative = 15, 10, 5
precision = true_positive / (true_positive + false_positive)
recall = true_positive / (true_positive + false_negative)
f1 = 2 * true_positive / (2 * true_positive + false_positive + false_negative)
print(f"precision={precision:.2f}; recall={recall:.2f}; F1={f1:.4f}")
sessions = {"train": {"morning-A", "evening-A"}, "test": {"morning-B"}}
print("shared sessions:", len(sessions["train"] & sessions["test"]))
```

**在本地运行**

```sh
python evaluation-shift-evaluate-alerts.py
```

**预期输出**

```text
precision=0.60; recall=0.75; F1=0.6667
shared sessions: 0
```

**逐步读懂代码**

第一行设置三种计数。每个除法使用该指标对应的总体。字典将划分名称映射到拍摄场次集合。& 求集合交集，len 报告共享场次为零。这只检查一种分组规则，不能排除所有可能的泄漏。这里没有模型训练。

### 这些知识可以用在哪里

**隐藏的重复**

把 morning-A 也放入测试集。共享场次计数变为一。解释测试分数前先消除重叠。

**夜间检查**

分别报告白天与夜晚计数。若夜间测试图很少，优秀白天均值不能证明夜间可靠。

**类比的边界与注意事项:** 陌生照片的类比有助于发现泄漏，但陌生程度有多个维度。不同采集时段仍可能共享同一相机背景，新地点也可能改变类别频率。小评估组的估计具有不确定性；应始终保留样本数量，避免把一次划分当成通用性能证明。

**记住这一点:** 可信的视觉分数应明确目标总体，隔离相关采集记录，并揭示平均值背后的误报与漏报。

### 本节资料来源

- [scikit-learn: 指标与评分：量化预测质量](https://scikit-learn.org/stable/modules/model_evaluation.html)

- [scikit-learn: GroupKFold：组间不重叠的交叉验证](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupKFold.html)

- [Koh et al.: WILDS：真实环境中的分布偏移基准](https://arxiv.org/abs/2012.07421)

<a id="inference-pipelines"></a>

## 推理流程：检查完整处理过程

### 01 / 从一个完整故事开始

机器人社做完一个小图像模型，在较慢笔电上试用。模型内部计算很快，结果却总是很迟。Imani 测量完整过程：调整图像大小、运行模型、把框转回原图。她还发现只有一台电脑改了分数阈值。

社团写下统一输入输出规范，再比较使用较小数值表示前后的准确性。只有通过核实样本才保留快版本。速度变成与正确性一起测量的目标，而不是跳过正确性检查的理由。

### 02 / 理解核心概念

推理是使用固定模型权重作预测。流程包括预处理、模型计算和后处理。只有这些步骤在通道、尺寸、坐标、阈值上保持一致，输出才可能正确。

延迟是一个请求等待的时间，吞吐量是每秒完成多少请求。两者不同：批处理可能提高吞吐量，同时让某个请求等待更久。量化使用更少种可能数值存储或计算。合适硬件上可降低成本，但舍入会改数，也可能改变决策。优化后必须重新评估完整流程。

### 03 / 如何运用这个概念

围绕模型建立可复现的预处理与后处理。用实际预期输入比较输出质量、延迟和内存。

### 04 / 官方教程与研究中的应用

用下面的原创计算与 Python 代码观察机制。所链接的一手资料解释完整系统怎样扩展这些机制。故事是虚构的教学情境。

### 05 / 公式与逐项解析

```text
quantized = round(value / step)
restored = quantized × step
error = |restored − value|
serial_latency = preprocess_time + model_time + postprocess_time
```

step 是简单零偏移量化器可表示数值的间隔。round 取最近整数；本例忽略裁剪与数据类型限制。error 是绝对变化。延迟求和假设三步骤依次运行、不重叠，并使用相同时间单位。

### 06 / 一步步算出结果

value=0.26、step=0.1 时，0.26/0.1=2.6，舍入为 3，恢复为 0.3，误差 0.04。4、7、2 毫秒的阶段时间相加为 13 毫秒。若只有模型时间降为 4，总延迟为 10，而非 13 的一半。

### 先认识这些词

- **推理:** 使用当前固定权重作预测。

- **延迟:** 一个请求等待结果的时间。

- **量化:** 用更少种可能数值表示数据。

- **吞吐量:** 单位时间完成的请求数量。

### 一步步想清楚

**为什么不能只测模型计算？**

图像准备和输出转换也消耗时间。用户要等全部必要步骤完成。

**0.26 如何重建为 0.3？**

除以 0.1 得 2.6，舍入为 3，再乘 0.1。

**数值误差很小就保证标签相同吗？**

不能。靠近决策阈值的分数可能因小变化而跨过阈值。

**为什么先减补边，再除缩放比例？**

正向先缩放，再加补边。逆运算需要倒转顺序；顺序错误会造成框的偏移。

<a id="lab-inference-pipelines-pipeline-budget"></a>

### 检查舍入与总延迟

使用明确的假设时间，并非实测。

```python
values = [0.12, 0.26, 0.91]
step = 0.1
encoded = [round(value / step) for value in values]
restored = [number * step for number in encoded]
errors = [abs(a - b) for a, b in zip(values, restored)]
print("encoded:", encoded)
print("restored:", [round(x, 2) for x in restored])
print(f"largest error={max(errors):.2f}")
print("serial latency ms:", sum([4, 7, 2]))
```

**在本地运行**

```sh
python inference-pipelines-pipeline-budget.py
```

**预期输出**

```text
encoded: [1, 3, 9]
restored: [0.1, 0.3, 0.9]
largest error=0.04
serial latency ms: 13
```

**逐步读懂代码**

第一个推导式把每个数除以间隔再舍入。第二个通过乘法重建近似数。zip 将原值和重建值配对，abs 测误差。显示时保留两位小数只影响展示。sum 累加三个假设阶段时间。这个教学量化器没有转换或运行训练模型。

<a id="lab-inference-pipelines-letterbox-coordinate-roundtrip"></a>

### 把检测框还原到原始图像

米娜把宽幅相机图像放入正方形模型输入：先缩放，再补边。模型找到了物体，可直接把模型坐标画回原图时，框却错位了。她记录实际缩放尺寸和补边量，按相反顺序还原坐标，并先做往返测试，再相信显示结果。

```python
from math import isclose

def metadata(width, height, side):
    if min(width, height, side) <= 0:
        raise ValueError("positive dimensions required")
    nominal = min(side / width, side / height)
    rw, rh = max(1, round(width * nominal)), max(1, round(height * nominal))
    return rw / width, rh / height, (side - rw) // 2, (side - rh) // 2

def forward(box, meta):
    sx, sy, px, py = meta
    x1, y1, x2, y2 = box
    return [sx * x1 + px, sy * y1 + py, sx * x2 + px, sy * y2 + py]

def restore(box, meta, width, height):
    sx, sy, px, py = meta
    x1, y1, x2, y2 = box
    restored = [(x1 - px) / sx, (y1 - py) / sy,
                (x2 - px) / sx, (y2 - py) / sy]
    clipped = [min(max(v, 0.0), limit)
               for v, limit in zip(restored, [width, height, width, height])]
    return clipped if clipped[0] < clipped[2] and clipped[1] < clipped[3] else None

original = [100.0, 50.0, 500.0, 300.0]
meta = metadata(800, 400, 640)
model_box = forward(original, meta)
restored = restore(model_box, meta, 800, 400)
assert all(isclose(a, b) for a, b in zip(original, restored))
odd_meta = metadata(853, 480, 640)
odd_back = restore(forward(original, odd_meta), odd_meta, 853, 480)
assert all(isclose(a, b) for a, b in zip(original, odd_back))
assert odd_meta[0] != odd_meta[1]  # Rounded height changes the actual y scale.
assert restore([0, 0, 20, 20], meta, 800, 400) is None
print("scale and padding:", meta)
print("model box:", model_box)
print("original box:", restored)
print("rounded-size round trip:", all(isclose(a, b) for a, b in zip(original, odd_back)))
```

**在本地运行**

```sh
python inference-pipelines-letterbox-coordinate-roundtrip.py
```

**预期输出**

```text
scale and padding: (0.8, 0.8, 0, 160)
model box: [80.0, 200.0, 400.0, 400.0]
original box: [100.0, 50.0, 500.0, 300.0]
rounded-size round trip: True
```

**按执行顺序理解**

1. 800×400 的图像变成 640×320，上下各补 160 像素。

2. 映射两个角点：[100,50,500,300] 变为 [80,200,400,400]。

3. 先减补边，再除缩放比例，最后裁剪到原图的连续边界。

4. 用 853×480 再测一次。取整后的实际尺寸可能导致横纵缩放比例略有不同。完全落在补边中、裁剪后面积为零的框应丢弃。

**逐步读懂代码**

连续框边界满足 x_prime=s_x×x+p_x、y_prime=s_y×y+p_y。s_x 和 s_y 分别是实际缩放后宽度/原宽度、实际缩放后高度/原高度。p_x、p_y 是左侧和顶部补边量。逆变换为 x=(x_prime-p_x)/s_x、y=(y_prime-p_y)/s_y。

第一张图的两个比例都是 0.8。模型中的 y=200 对应原图 (200-160)/0.8=50。853×480 取整后变成 640×360，此时 s_x=640/853，s_y=360/480。两个方向都用名义比例，会引入小误差。应保存预处理实际使用的变换。

本例使用 [0,width]、[0,height] 内的连续框边界，不是最大索引为 width-1、height-1 的整数像素坐标。实际库的像素中心和插值约定可能不同；裁剪、旋转或其他补边规则也需要记录对应变换。往返测试检验坐标记录是否自洽，不能证明检测模型准确。

### 这些知识可以用在哪里

**更细间隔**

将 step 改成 0.01。这些数能更精确重建，但相同范围内需要更多可表示级别。

**主要耗时阶段**

把假设时间改成 [20,4,2]。只加速模型无法消除 20 毫秒预处理成本。

**类比的边界与注意事项:** 代码演示舍入和假设阶段时间，不是硬件基准。ONNX Runtime 文档介绍真实量化选项；不同模型或设备的收益不同。

**记住这一点:** 围绕模型建立可复现的预处理与后处理。用实际预期输入比较输出质量、延迟和内存。

### 本节资料来源

- [ONNX Runtime: 量化 ONNX 模型](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)

- [TorchVision: 变换图像、视频、框等数据](https://docs.pytorch.org/vision/stable/transforms.html)

- [OpenCV: 图像几何变换](https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html)

## 知识速查

### [像素是带有约定的测量值](#pixels-contracts)

在让模型学习像素之前，先明确每个接口的轴顺序、颜色含义、数值尺度与归一化约定。

### [颜色：先弄清通道](#colour-channels)

颜色筛选可选择标牌或为后续检查准备区域。信任掩码前要检查已知颜色、通道顺序与光照。

### [局部滤波提出局部问题](#filters-neighborhoods)

滤波器是用权重表达的局部问题；既要检查它去掉了什么，也要检查它保留了什么。

### [边缘与掩码：先找变化，再清理](#edges-morphology)

可用这些操作清理扫描文字掩码或检查简单形状。每次清理前后都应比较细小但有用的细节。

### [让图像与标注一起移动](#geometry-augmentation)

几何变化必须带着标注一起变化；每项增强都要说明，为什么标签含义仍然成立。

### [局部特征：比较有辨识度的图块](#classical-features)

局部特征可支持全景对齐或将拍摄物体匹配到参考图。需要检查重复图案和几何一致性。

### [借用视觉词汇，再学习新的判断](#recognition-transfer)

迁移表示，仔细定义新的判断任务，再用真正独立的图像组衡量改进。

### [框定位候选对象，重叠度帮助消除重复](#detection-overlap)

把框视为候选，精确定义重叠，并把去重规则与检测质量的判定规则分别验证。

### [检测分数：顺着排序列表评估](#detection-average-precision)

在明确匹配规则下使用排序评估比较检测器。应同时保存分数、框、类别与参考数量。

### [从一个矩形走向真正属于对象的像素](#segmentation-regions)

让掩码类型对应实际问题，检查占少数的前景，并明确重叠度与空掩码情况的评分方式。

### [跟踪：跨帧保留身份](#tracking-identity)

跟踪可用于统计移动物体，或在视频中跟随玩具机器人和野生动物。应检查交叉、遮挡与漏检。

### [OCR：把可见笔画变成文字](#ocr-reading)

OCR 可用于扫描件搜索、标签识别与文档阅读。应保留原图区域及阅读顺序，便于核对不确定文字。

### [让图像块交换信息](#visual-tokens)

明确计算图像块与特殊词元：更细的视觉单元会增加可能的交互，而有用细节与实际运行时间仍需测量。

### [缺陷检查：发现异常区域](#defect-inspection)

可用异常图辅助检查纹理或物体。必须区分异常外观与已确认缺陷。

### [用下一次采集检验，而非只看熟悉的邻帧](#evaluation-shift)

可信的视觉分数应明确目标总体，隔离相关采集记录，并揭示平均值背后的误报与漏报。

### [推理流程：检查完整处理过程](#inference-pipelines)

围绕模型建立可复现的预处理与后处理。用实际预期输入比较输出质量、延迟和内存。

## 官方教程与原始研究

故事、讲解与计算例子由 Leon 原创编写。链接中的教程和研究属于各自作者；本资料为独立学习笔记，并非官方译本或官方认可课程。

- [图像基本操作](https://docs.opencv.org/4.x/d3/df2/tutorial_py_basic_ops.html) — OpenCV

- [图像平滑](https://docs.opencv.org/4.x/d4/d13/tutorial_py_filtering.html) — OpenCV

- [图像滤波：filter2D](https://docs.opencv.org/4.x/d4/d86/group__imgproc__filter.html) — OpenCV

- [图像几何变换](https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html) — OpenCV

- [计算机视觉迁移学习](https://docs.pytorch.org/tutorials/beginner/transfer_learning_tutorial.html) — PyTorch

- [TorchVision 目标检测微调教程](https://docs.pytorch.org/tutorials/intermediate/torchvision_tutorial.html) — PyTorch

- [非极大值抑制](https://docs.pytorch.org/vision/stable/generated/torchvision.ops.nms.html) — TorchVision

- [使用 ResNet-50 主干的 FCN](https://docs.pytorch.org/vision/stable/models/generated/torchvision.models.segmentation.fcn_resnet50.html) — TorchVision

- [变换图像、视频、框等数据](https://docs.pytorch.org/vision/stable/transforms.html) — TorchVision

- [CS231n：计算机视觉深度学习](https://cs231n.stanford.edu/2025/schedule.html) — Stanford University

- [一幅图像相当于 16×16 个词](https://arxiv.org/abs/2010.11929) — Dosovitskiy et al.

- [指标与评分：量化预测质量](https://scikit-learn.org/stable/modules/model_evaluation.html) — scikit-learn

- [VisionTransformer 实现](https://docs.pytorch.org/vision/stable/_modules/torchvision/models/vision_transformer.html) — TorchVision

- [注意力就是你所需要的一切](https://arxiv.org/abs/1706.03762) — Vaswani et al.

- [GroupKFold：组间不重叠的交叉验证](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupKFold.html) — scikit-learn

- [WILDS：真实环境中的分布偏移基准](https://arxiv.org/abs/2012.07421) — Koh et al.

- [转换色彩空间](https://docs.opencv.org/4.x/df/d9d/tutorial_py_colorspaces.html) — OpenCV

- [形态学变换](https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html) — OpenCV

- [SIFT 入门](https://docs.opencv.org/4.x/da/df5/tutorial_py_sift_intro.html) — OpenCV

- [COCO 官方评估实现](https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocotools/cocoeval.py) — COCO authors

- [简单在线实时跟踪](https://arxiv.org/abs/1602.00763) — Bewley et al.

- [改善输出质量](https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html) — Tesseract

- [MVTec AD 异常检测数据集](https://www.mvtec.com/research-teaching/datasets/mvtec-ad) — MVTec

- [量化 ONNX 模型](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html) — ONNX Runtime

- [基准评估：像素级 IoU 与忽略标签](https://www.cityscapes-dataset.com/benchmarks/) — Cityscapes
