做目标检测的人,应该都绕不开 Selective Search 这篇经典论文。第一次认真读它,是在一个需要自研区域提议模块的检测项目里,当时为了搞清楚“为什么R-CNN要用2000个框而不是直接滑窗”,把论文和代码来回折腾了好几遍。这篇老文章的标题叫 Selective Search for Object Recognition,核心贡献就一句话:用一套不依赖具体类别的候选区域生成方法,先把“物体可能在哪里”这个问题解决掉,再把识别交给后面的分类器。
这篇论文笔记,我打算顺着论文主线,把它解决的问题、层级分组算法、四种相似度设计、多样性策略、实验结论,以及它在整个检测生态里的位置,按我自己复现时的理解完整重讲一遍。适合刚入门检测、想在理论上搞清楚“区域提议”是怎么回事的读者,也适合已经把R-CNN、Faster R-CNN跑得很熟、但没认真回过头的开发者。读完你会发现,今天RPN能做的事,其实几十年前的思路早就埋好种子了。
先摆一个结论:Selective Search的任务不是“认出物体”,而是“猜哪里可能有物体”。它只依赖底层视觉线索,把图像切成小区域,再按规则不断把相邻区域合并,最后输出一组数量可控、尺度丰富的候选框。后面接一个分类器,就可以组成一套完整检测系统——R-CNN当年就是靠它撑起来的。
1. 目标识别里的老难题:为什么滑动窗口不够用
1.1 滑动窗口的问题不在数量,而在“无效搜索”
在深度学习还没有统治检测领域时,最常见的做法就是滑动窗口。想象一下在一个操场上找一只足球:你在操场上铺一个固定大小的网格,把网格一格一格挪过去,每到一个位置就把窗口里的图像送去分类器。然后为了适配不同大小的物体,还得把图像缩放成多级金字塔,在每一层上再做一遍同样的滑动。这样算下来,窗口的数量是“位置数×尺度数×长宽比数”,随便一乘就是几十万甚至上百万个窗口。
数量大并不是最致命的问题,最致命的是这些窗口大部分都是无效搜索。分类器必须逐一判断“这个窗口里是不是包含了一个完整物体”,但滑窗本身对物体一无所知,它不关心边界在哪里、不关心颜色突变点在哪里、也不关心当前窗口是不是恰好把一个物体截成了两半。于是大量计算花在了没意义的背景窗口上,误检率也高得吓人。特别是多类别检测场景,类别越多,单个窗口的分类难度越大,滑窗的性价比就越差。
Selective Search换了一个完全不同的思路:与其在所有可能位置硬搜,不如先从图像本身的结构出发,找出那些“看起来像一块完整东西”的图像区域。这个思路的底气来自一条朴素的先验:物体在图像里通常是一个视觉上连贯的区域,与周围背景在颜色、纹理上存在差异。所以只要把图像分成若干候选区域,再按一定的层次结构合并出完整物体,理论上就可能用很少的窗口覆盖绝大多数真实物体。
1.2 单次分割为什么不够
可能会有人第一反应:直接用普通图像分割不就行了?把图像切成若干区域,然后每个区域取一个外接框,这不就是候选框吗?
问题在于,单个尺度的分割结果几乎永远不可能是“正好对应物体”的。过度分割时,一只狗被切成了头、耳朵、身体、腿好几块,每块单独拿去做识别都认不出来;欠分割时,狗和身后的草地连成一片,外接框把一大堆背景也包进来了。现实场景里,物体尺寸差异极大,一辆车可能占画面的一半,一只鸟可能只有几十个像素,单尺度分割根本不可能同时伺候好它们。
所以论文的核心设计决策是:用层级分组(Hierarchical Grouping)把“不同尺度下的分割候选”全部保留下来,而不是追求一次分割就完美。先做一次过分割,得到大量小区域,然后像拼图一样,每次把最相似的两个相邻区域合并,合并过程每走一步都记录一次外接框。这样最终生成的是一个覆盖多尺度的候选框序列,从细碎的小块到接近全图的大块都有。这个思路借用了一个很自然的直觉:物体在大尺度上是一个整体,在小尺度上由若干零件组成,层级合并刚好同时涵盖这两种形态。
1.3 这篇论文的历史坐标
现在很多人接触目标检测是从YOLO和Faster R-CNN入门的,会觉得Selective Search是个“上古算法”。但从历史坐标看,它是区域提议方法里影响力最大的一支。R-CNN把它的2000个候选框视为标准输入,后续一批两阶段方法也都围绕“提议+分类”的框架展开。即使后来Faster R-CNN用RPN把它替换掉了,理解Selective Search依然能帮你搞清楚一个核心问题:区域提议到底在解决什么,它为什么可以大幅降低检测的搜索成本。这份理解,放到今天的 anchor、query、slot 等各种机制里照样用来得。
2. Selective Search 的核心机制:层级分组与四种相似度
2.1 第一步:基于图的初始过分割
论文第一步是用一种基于图的图像分割算法对整张图做初始分割。这个算法的基本流程是:把像素看成节点,相邻像素之间连边,边的权重由像素颜色差异(或梯度)决定。把所有边按权重从小到大排序,然后贪心合并,合并条件是一个区域的内部差异加上一个随区域大小变化的容差。容差的存在让颜色差异较小的细碎区域先合并成中等块,而跨越明显边界的合并会被阻止。这里的尺度参数 k 直接控制最终区域大小:k 越大,容差越大,越容易合并出大块;k 越小,分割越碎。
这一步和常规分割没什么区别,但它对后面的层级分组很关键。初始分割质量的好坏决定了后续合并的“起点颗粒度”。论文默认σ=0.8的平滑参数来抑制噪声,k值常用100,最小区域像素数也做了限制。分割出来的是一堆超级像素级别的区域,数量通常在几百到上千,这些区域后续就是层级合并的基本单元。
2.2 第二步:贪心层级合并
拿到初始区域后,论文进入了最核心的层级分组过程。伪代码写出来非常短:
- 把所有初始区域放入集合 R;
- 对每一对相邻区域,计算综合相似度 s(ri, rj);
- 找到相似度最高的一对区域,合并成新区域;
- 把新区域的外接框加入候选框集合;
- 删除与原来两个区域相关的相似度记录;
- 计算新区域与它所有相邻区域的相似度,插入记录;
- 重复 2~6,直到 R 中只剩一个区域。
实现上有几个关键工程点。第一,只需要计算相邻区域的相似度,不相邻的区域永远不会合并,这一下把计算量从 O(n²) 降到了近似 O(n log n)。第二,用优先队列维护相似度,每次取最大值是 O(log n)。第三,合并顺序是“最相似者优先”——这就是贪心的含义,它不追求全局最优,而是每一步都选当前看起来最该合并的那一对。论文明确选择了这种策略,理由是它足够快,并且在实际数据集上效果很好,完全够用。
2.3 四种相似度:颜色、纹理、尺寸、填充
层级分组最讲究的就是“两个区域凭什么该合并”。论文没有只用单一特征,而是设计了四种互补的相似度,最后加权相加。这四种相似度我记得非常清楚,因为它们刚好对应了物体合并过程中最常遇到的四种情况。
颜色相似度:每个区域在三个颜色通道上分别统计25个bin的颜色直方图,做L1归一化,然后用直方图交集计算相似度。颜色直方图的好处是对区域内部的微小位移和遮挡不敏感。两只相邻的猫,一只偏黄一只偏白,如果颜色分布接近,就更容易被合并成一个大块。
纹理相似度:使用类似SIFT的特征响应,对每个颜色通道在8个方向上计算高斯导数,每个方向统计10个bin,最后拼成一个240维的纹理直方图,同样用直方图交集比较。这个度量能捕捉方向性纹理的一致性,比如草地、水面、砖墙这些颜色接近但纹路差异明显的区域,靠颜色分不开,但靠纹理可以区隔开。
尺寸相似度:公式是 1 - (size(ri) + size(rj)) / size(image)。也就是说,两个区域越小,尺寸相似度越高,越容易被提前合并。这个设计的意图很明确:如果不加这个度量,大块区域很容易在早期就吞掉周围的小块,导致后续候选框丢失细节;强制小区域优先合并,能让合并过程覆盖更多尺度层次。
填充相似度:公式是 1 - (bbox(rj) - size(ri) - size(rj)) / size(image)。它衡量的是两个区域合并后,它们的新外接框被实际区域填充的程度。如果两个区域合并后能填满边界框、没有大缺口,说明它们构成一个完整物体的可能性很大。这个度量专门用来让最终的候选框更贴合物体轮廓,减少把大量背景框进去的情况。
最终的综合相似度就是:
s(ri, rj) = a1 * s_color(ri, rj) + a2 * s_texture(ri, rj) + a3 * s_size(ri, rj) + a4 * s_fill(ri, rj)
其中每个 a 取0或1,表示这项相似度是否参与计算。不同策略通过切换开关,就能得到行为完全不同的分组结果。这也是论文“多样性策略”的关键入口。
2.4 候选框生成与本节的隐藏细节
每合并一次,就记录一次新区域的外接框,这意味着合并到只剩一个区域时,候选框集合里已经有大约和初始区域数量同量级的框。合并结束后,论文还会把整个图片的外接框也加进去,防止那种几乎占满全图的大物体漏掉。因为在某些图像里,物体就是背景本身,如果不加这个全图框,覆盖全图的物体永远没有对应的候选框。
还有一个经常被忽略的细节:即使层级合并结束,你得到的候选框中,很多外接框会严重重叠。这不是bug,而是设计使然。后来的R-CNN并不依赖候选框的“排序”,而是把所有框都送去分类,所以重叠框多只是增加了少量冗余计算;但在一些把提议数量当作预算的应用里,这个冗余就需要用非极大值抑制之类的后处理来控制。
3. 多样性策略、参数配置与代码实现
3.1 多样性从哪里来
单组参数下的层级合并往往只能覆盖一部分物体。比如用某种颜色空间算颜色直方图,可能让黄色物体特别容易被合并,却把蓝色物体拆得七零八落。论文因此提出了一个非常重要的工程思想:既然单一策略有盲区,那就同时跑多组不同配置,把所有策略生成的候选框合并取并集,靠多样性换取召回率。
论文里多样性的来源主要有三个。第一是初始分割的尺度多样性,把 k 值设成50、100、150、300等不同档位,得到颗粒度不同的初始分割。第二是颜色空间的多样性,分别在HSV、Lab、HSI、归一化rgb、对立色空间等多个颜色空间里计算特征。第三是相似度组合的多样性,比如有的策略只用颜色、尺寸、填充,不用纹理;有的策略反过来只用纹理、尺寸、填充,不用颜色。
把这些选项组合起来,论文实际使用了一组策略集合,常见表述是8个策略。每个策略在同一个颜色空间里使用一组固定的相似度开关,多个策略的结果合并在一起,才构成最终的候选框集合。以R-CNN为例,最终每张图取约2000个候选框,这个数量其实是一整套策略的组合输出,而不是单个策略的产物。
3.2 颜色空间为什么这么重要
颜色空间的选择直接决定颜色直方图对光照、阴影的敏感度。HSV把色相和饱和度分开,对高光区域更稳定;Lab更接近人类视觉感知,颜色距离有意义;归一化rgb对光照强度的变化更鲁棒;对立色空间则在一些特殊场景有优势。Single策略不可能在所有光照条件下都表现好,但多策略并集可以把各种光照条件下的漏检互相补位。
这里有个实操体会:在复现论文时,不要只看相似度公式,要特别注意颜色空间转换的代码顺序。同一个RGB值,转成Lab和转成HSV得到的直方图分布完全不同,直接影响哪些区域会被优先合并。很多复现效果差异大,问题就出在颜色空间的处理上,而不是相似度公式写错了。
3.3 参数速查表与常见取值
我把论文里最常见的一组参数整理成表,方便后面写代码时对照:
| 配置项 | 常见取值与说明 |
|---|---|
| 初始分割平滑半径 σ | 0.8,用来做高斯模糊,抑制噪声 |
| 初始分割尺度 k | 默认100;多样性策略扩展到50、150、300 |
| 最小区域像素数 | 常见约50,过滤过小的碎块 |
| 颜色直方图 | 3通道 × 25 bin,L1归一化,直方图交集 |
| 纹理直方图 | 3通道 × 8方向 × 10 bin = 240维 |
| 综合相似度权重 | a1~a4取0或1,按策略切换 |
| 输出候选框数量 | 单策略数百;组合策略2000~3000不等 |
这个表不是论文原始表的精确抄录,而是根据我实际复现时用的配置做的总结。大家真的动手写代码时,以手上开源实现为准,但大方向不会跑偏。
3.4 代码实现上的三种选择
想跑起来最快的方式是用OpenCV的扩展模块。注意,cv2.ximgproc.segmentation在 opencv-contrib-python 里,不是默认的 opencv-python。用法很简单:
import cv2 img = cv2.imread("demo.jpg") ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation() ss.setBaseImage(img) ss.switchToSelectiveSearchQuality() rects = ss.process() # 返回候选框列表 for x, y, w, h in rects[:200]: cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)OpenCV提供了两个模式,switchToSelectiveSearchFast速度快、框少,switchToSelectiveSearchQuality速度慢、框多、召回更好。训练检测模型时我建议用Quality模式,推理时再根据性能预算考虑Fast模式。
另一个常用的是独立Python库,直接pip安装就能用:
import selectivesearch _, proposals = selectivesearch.selective_search( img, scale=500, sigma=0.8, min_size=50 )这个库更接近论文原版Matlab代码的逻辑,可以调节scale、sigma、min_size,方便做参数实验。如果你需要改相似度定义,那就只能去读原版代码或者自己重写核心逻辑。无论如何,强烈建议第一步先把候选框可视化出来,看看不同参数下框都长什么样,这会比闷头调参高效得多。
4. 论文实验里的关键数字与结论
4.1 评价指标:重叠度与召回率
Selective Search评估自己的标准不是检测mAP,而是“区域提议质量”。最常用的指标是ABO(Average Best Overlap,平均最佳重叠)。对每个类别,计算每个真实物体框和所有候选框的最大IoU,再对类别取平均。ABO越高,说明这套提议方法越容易给出和真实物体足够重合的候选框。
另一个更直观的维度是召回率与候选框数量的权衡曲线。给定N个候选框,统计有多少真实物体能被其中某个框以IoU≥0.5覆盖。候选框数量越多,召回率越高,但下游分类器的负担也越大。论文最核心的竞争力就在这个权衡点上:用很少的框换很高的召回率。
4.2 值得记住的几组数字
论文在VOC 2007上和相关检测数据集上做的实验,有几组数字经常被引用。用约2000个候选框时,召回率能做到接近98%;把候选框加到上万,召回率能逼近99%。作为对比,随机窗口在相同的候选框数量下召回率非常惨,滑动窗口虽然理论上能滑出所有位置,但要在可接受的计算预算内做到同等召回,需要窗口数量直线飙升。另一个常被提到的结论是,Selective Search的ABO在多个类别上能稳定超过0.8,部分类别更高。这些数字放到今天看依然不落伍,毕竟提议问题本身的难度就在那里。
速度方面,以当年的CPU算力,处理一张常见尺寸的图片大约需要1到2秒。这在当时不算快,但对于生成候选区域这种离线任务完全可以接受。R-CNN里2000个框对应的是离线跑一遍Selective Search然后缓存结果,训练阶段并不反复调用它,所以速度短板被绕开了。
4.3 实验给的两个启发
第一个启发是:多样性确实能显著提升召回率。论文里对比了单一策略和多种策略合并的结果,多策略并集的召回率明显高于任何单个策略。工程上的“堆配置”听起来不优雅,但在召回率就是预算的领域,收益是真金白银。
第二个启发是:区域提议和分类是解耦的。Selective Search只负责“怎么找框”,不关心框里是什么;分类器只负责“框里是什么”,不关心框怎么来。这个解耦让R-CNN可以轻松替换掉提议模块,比如后来换成EdgeBoxes、换成RPN,而分类器那一套基本不用动。
4.4 与R-CNN的联动细节
R-CNN的流程是:输入一张图,用Selective Search生成约2000个候选框;每个候选框裁剪出来并缩放成固定尺寸,送进CNN提取特征;最后对每个类别训练一个SVM分类器判断框里是什么,再用一个边界框回归器修正位置。这个流程的成功离不开Selective Search的两个特性:召回率高,且候选框数量可控。2000个框意味着CNN只需要前向计算2000次,这在当年的显存和算力条件下勉强可行。如果把滑窗那百万级的窗口全送进CNN,R-CNN根本跑不动。
R-CNN训练时还用了一个细节:正负样本按照候选框与真实框的IoU划分,IoU≥0.5算正样本,IoU<0.3算负样本。Selective Search生成的框虽然召回率高,但很多框和真实物体并不是严丝合缝,IoU在0.5附近的情况很常见。这个划分规则之所以能成立,正是因为提议框的整体质量足以支撑这样的阈值设定。
5. 复现踩坑实录:候选框生成的常见问题
5.1 候选框质量怎么判断
很多新手复现完第一步就会问:我生成的框看起来又乱又多,是不是代码写错了?先别急,候选框本来就是“宁可多给,不可漏掉”。判断质量的标准不是“框得准”,而是“真实物体有没有被覆盖、覆盖用了多少框”。最直观的做法是拿标注数据算一下召回率:给定2000个框,真实框的召回率有没有到90%以上;如果50个框就能覆盖大多数物体,说明合并策略有效;如果几千个框还漏掉大量物体,那就该检查参数了。
我在复现时踩过的最大一个坑是把IoU阈值设得太高。用IoU≥0.7去评估候选框,几乎不可能有好结果,因为Selective Search生成的框并不是高精度定位框,它的目标是“大致包住物体”。把评估阈值放到0.5,才能看到这个算法的真实水平。
5.2 OpenCV的快速版和质量版不能盲目选
OpenCV里Fast模式和Quality模式的差异不只是速度。Fast模式生成的框更少,通常在几百上下,适合对延迟敏感的场景;Quality模式会按多个策略组合生成大量框,常见在2000~3000。如果你拿Fast模式的结果去R-CNN里当唯一的提议来源,召回率会明显掉一截。反过来,如果拿Quality模式去做实时推理,性能也扛不住。我的建议是:训练和离线评估一律用Quality,部署时单独测试Fast模式是否能满足召回需求。两者之间的差距在不同数据集上差异很大,别想当然。
另一个隐藏问题是OpenCV的接口可调参数很少。如果你想改初始分割的k值、换颜色空间、调整相似度权重,OpenCV的封装很难满足你。这种情况下就直接用Python版selectivesearch,或者读原版代码改。拿OpenCV快速验证可以,想做论文级别的复现,还是得回到更接近原版的实现上。
5.3 小物体和极端场景的漏检
Selective Search对小物体并不友好。小物体容易被初始分割阶段直接并进邻近的大块背景里,层级合并再怎么努力也变不回来。如果你负责的数据集里小目标占比很高,比如航拍图像或者密集小物件场景,Selective Search的召回率会明显下滑。解决思路有三个:把初始分割调得更碎,用更大的k值生成更多尺度;提高纹理相似度的权重,避免小物体因为纹理不匹配被并走;或者干脆换用现代提议方法,比如RPN。
还有一个很容易被忽视的极端场景:物体和背景颜色、纹理几乎完全一致。比如沙地里的浅色昆虫、雪地里的白色鸟。此时颜色、纹理相似度全都失灵,算法只能靠尺寸和填充去瞎猜。这种情况下不必强求Selective Search,任何纯底层视觉的提议方法都会有盲区。
5.4 预处理不一致会导致玄学差距
同一个模型,训练时用Selective Search对原始图生成框,测试时因为工程原因对缩放后的图生成框,结果可能差得非常远。Selective Search对图像分辨率、颜色通道顺序很敏感。比如OpenCV读进来的是BGR,转成Lab时如果用RGB顺序转,颜色直方图就完全错了。我建议把所有预处理固定成一条不可改的管线:统一读图方式、统一缩放逻辑、统一颜色空间转换、统一候选框缓存格式。训练和测试必须走同一套代码,否则你根本分不清是模型问题还是提议问题。
6. 从 Selective Search 到 Faster R-CNN 的 RPN:为什么提议模块被取代了
6.1 RPN到底改了什么
Faster R-CNN的RPN(区域提议网络)把Selective Search完全移出了检测流程。RPN不再“看图猜区域”,而是直接在卷积特征图的每个位置上放置一组预设的anchor框,每个anchor预测一个物体得分和一组坐标修正。它本质上还是一个提议生成器,但有两个关键变化:第一,提议的特征来自全图共享的卷积特征,分支网络只是轻量计算;第二,提议生成模块是端到端可训练的,网络自己学会哪些位置值得提框,而不是靠手工特征。
RPN带来的最大收益是成本重置。Selective Search每张图要花1到2秒离线跑,RPN在GPU上几乎是免费附带在检测网络里。更重要的是,Selective Search的“手工相似度”一旦训练数据分布变了就得重新调,而RPN随检测任务一起训练,天然适配当前数据分布。所以从Faster R-CNN开始,两阶段检测不再需要外部提议器。
6.2 Selective Search在今天还有什么用
被RPN取代不代表它没有价值。在很多无法端到端训练的场景里,Selective Search仍然是个可靠的“免费午餐”:比如你需要给一个没有标注的数据集快速生成伪标签候选;比如你在做弱监督检测,需要先找到可能包含物体的区域再做后续分析;再比如你想在自己的算法里快速验证“区域提议+分类器”这个框架,而不想引入一堆需要训练的模块。它还有个好处是完全可解释、完全确定,对于一个需要审计的处理管线来说,这种透明的预处理模块反而省心。
教育价值更不能忽略。Selective Search是理解“提议-分类”两阶段思想的最佳入门教材。它把“如何减少搜索空间”这个问题抽离出来,用一整套手工特征给了一个工程上极其完整的答案。理解了它,你再看RPN的anchor、看DETR的query、看各种稀疏注意力机制,都会觉得它们本质上都回答同一个问题:目标在哪里。
6.3 一点个人体会
我复现这篇论文最大的感受是:手工特征时代的人是真的会把“多样性”当成工程正事来做。单一颜色空间不好,就同时跑多个;单一相似度不够,就用四个互补。这种思路和后来深度学习里“多尺度、多特征融合、多任务学习”其实是一脉相承的。深度学习让特征学习自动化了,但“用多样性覆盖不确定性”这个思想并没有过时。
如果只想快速上手,我的建议是不要第一遍就硬啃公式。先去把代码跑通,把候选框可视化出来看几遍,然后回头再看相似度定义,你会发现每个公式都对应着某个真实世界里你见过的失败案例。最后分享一个我自己的小习惯:每次读完论文,都会把冷门数据集上的几张复杂场景图输入Selective Search,把三四十个候选框画出来叠在一张图上。当你亲眼看到大量重叠框从物体周围冒出来,再读到它后面的分类器是怎么工作的,那种“原来如此”的感觉,比任何论文笔记都有说服力。