4. 目标检测基础:滑动窗口、图像金字塔、非极大值抑制(NMS)
目标检测这个方向,我做了快十年了。说实话,刚入行那会儿,觉得检测不就是分类加个框嘛。后来踩的坑多了才明白,滑动窗口、图像金字塔、NMS 这三个东西,是理解整个检测体系的基石。今天咱们就掰开揉碎了聊聊。
4.1 滑动窗口:最朴素的检测思路
滑动窗口的概念其实特别简单。你想想看,一张图里目标可能出现在任何位置,那怎么办?最直接的办法就是拿一个固定大小的窗口,从左到右、从上到下,把整张图扫一遍。
每个窗口位置,我们都把里面的图像区域送到分类器里判断一下。是猫?是狗?还是背景?这就是滑动窗口的核心思想。
核心要点:滑动窗口 = 穷举搜索 + 区域分类
我在项目中遇到过一个问题:窗口步长设多大?步长小了,计算量爆炸;步长大了,容易漏检。我个人习惯是,先根据目标最小尺寸来定步长,一般取窗口宽高的 10%~20%。
伪代码大概长这样:
def sliding_window(image, window_size, stride):
windows = []
for y in range(0, image.height - window_size[1], stride):
for x in range(0, image.width - window_size[0], stride):
crop = image[y:y+window_size[1], x:x+window_size[0]]
windows.append((x, y, crop))
return windows
嗯,这里要注意:滑动窗口的缺点很明显——窗口大小固定。你拿一个 64x64 的窗口去扫,大目标和小目标就顾此失彼了。怎么解决?这就引出了图像金字塔。
4.2 图像金字塔:多尺度检测的利器
说白了,图像金字塔就是对同一张图做不同尺度的缩放。大的尺度看小目标,小的尺度看大目标。这样,固定大小的窗口就能适应不同尺寸的目标了。
常见的金字塔有两种:
| 类型 | 做法 | 典型应用 |
|---|---|---|
| 高斯金字塔 | 先高斯模糊,再下采样 | 传统目标检测、SIFT特征 |
| 拉普拉斯金字塔 | 高斯金字塔的差分 | 图像融合、超分辨率 |
在目标检测里,我们主要用高斯金字塔。每次把图像缩小为原来的 0.5 倍或 0.8 倍,然后对每一层都做滑动窗口检测。
我的经验:金字塔层数不是越多越好。层数太多,小尺寸图像上的信息损失严重,反而容易误检。一般 3~5 层就够用了。我曾经试过堆到 8 层,结果检测速度慢了 3 倍,精度反而下降了。
为什么会有精度下降?因为图像缩得太小,目标的纹理细节都没了,分类器根本认不出来。所以,金字塔的缩放因子一般取 0.7~0.9 之间,别太激进。
4.3 非极大值抑制(NMS):去掉重复框
好,现在滑动窗口加金字塔,我们确实能检测到目标了。但问题来了——同一个目标,会被多个窗口框住。比如一只猫,窗口稍微挪一点,分类器可能还是判定为猫。结果就是,一只猫周围围了十几个框。
怎么办?NMS 就是干这个的。它的逻辑很简单:
- 把所有检测框按置信度从高到低排序
- 选置信度最高的框,保留它
- 去掉所有跟这个框重叠度(IoU)大于阈值的框
- 重复 2~3 步,直到没有框剩下
重叠度用 IoU(Intersection over Union)来衡量:
def iou(box1, box2):
# box = [x1, y1, x2, y2]
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
inter = max(0, x2 - x1) * max(0, y2 - y1)
area1 = (box1[2]-box1[0]) * (box1[3]-box1[1])
area2 = (box2[2]-box2[0]) * (box2[3]-box2[1])
union = area1 + area2 - inter
return inter / union if union > 0 else 0
NMS 的代码实现也不复杂:
def nms(boxes, scores, iou_threshold=0.5):
indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
keep = []
while indices:
i = indices[0]
keep.append(i)
rest = indices[1:]
indices = [j for j in rest if iou(boxes[i], boxes[j]) < iou_threshold]
return keep
避坑指南:我曾经因为 IoU 阈值设得太低(比如 0.3),导致密集场景下大量目标被误删。反过来,阈值设得太高(比如 0.9),又会有很多重复框残留。一般取 0.5~0.7 比较稳妥。具体调多少,得看你的业务场景——检测稀疏大目标可以低一点,检测密集小目标就要高一点。
4.4 三者如何配合?
这三兄弟在实际系统中是串在一起用的:
- 图像金字塔:生成多尺度图像
- 滑动窗口:在每个尺度上提取候选区域
- 分类器:判断每个候选区域是不是目标
- NMS:合并重复检测结果
举个例子。我做过一个行人检测项目,用的就是这套流程。先构建 3 层金字塔,每层用 64x128 的窗口以 8 像素步长滑动,HOG 特征加 SVM 分类器,最后 NMS 阈值设 0.6。效果还不错,就是慢了点——处理一张 1080p 的图要 2 秒多。
这也是为什么后来大家转向了 R-CNN、YOLO 这些端到端的方法。但说实话,滑动窗口 + 金字塔 + NMS 的思想,至今仍然深深影响着现代检测器的设计。比如 Anchor 机制,本质上就是预置了不同尺度和长宽比的滑动窗口。NMS 更是几乎所有检测器的标配后处理。
一句话总结:滑动窗口负责「在哪找」,图像金字塔负责「找多大」,NMS 负责「去重」。这三板斧,是理解一切目标检测算法的起点。
好了,这一章就聊到这儿。下一章咱们会深入聊聊 Anchor 机制,看看现代检测器是怎么把滑动窗口的思想玩出花的。