
接触图像处理时间久了你会发现一个问题算法学了一大堆滤波、边缘检测、形态学操作、阈值分割都能写可一旦丢一个真实项目过来——比如让你检测出一张街景照片里的所有行人——很多人就卡住了。问题不在于某个函数不会调而在于脑子里只有“像素操作”的碎片没有把图像处理这件事拆成有层次的体系。这篇深度篇第一篇我打算把两件事彻底讲透一是图像处理的三个层次到底是怎么回事二是selective search这个在目标检测史上非常重要的算法它凭什么能从一张图里“挑出”可能装着目标的区域。搞懂这两件事你再看如今各种基于深度学习的检测框架会顺很多。1. 先把层次感建立起来图像处理到底在“处理”什么1.1 从像素到语义三个层次的划分依据很多人学图像处理是直接从API入手的cv2.GaussianBlur会模糊、cv2.Canny会出边缘、cv2.threshold会做二值化。但你有没有想过这些操作其实是完全不同性质的工作我习惯把图像处理分成三个递进层次来看。底层像素层或者说预处理层。这个层次的操作对象是“单个像素及其邻域”目标是改善图像本身的质量或者突出某种像素级的特征。典型的操作包括高斯滤波、中值滤波、直方图均衡化、形态学膨胀腐蚀、颜色空间变换。它的输出仍然是一张图像只是这张图更容易被后续环节“读懂”。中层结构与特征层。到这个层次操作对象已经不是像素了而是“像素组成的局部结构”或者“有统计意义的特征”。边缘线段、角点、纹理、超像素区域、梯度直方图这些都是中层产物。它的输出往往不是一张图片而是一组有数学意义的结构描述。比如 SIFT 特征点描述子比如 HOG 特征比如经过分割得到的区域。高层语义层。这一层处理的对象是“物体”和“场景”。目标是回答“图里有什么、在哪里、是什么状态”。目标检测、实例分割、场景分类、行为识别都属于这一层。这三个层次不是孤立的而是严格的递进关系底层输出是中层输入中层输出是高层输入。你完全可以把它理解成一个工业流水线原料矿石底层——提取金属材料中层——组装成产品高层。1.2 三个层次的典型任务与代表算法我用一张表格把这几个层次的关键信息拉通平时选技术方案时可以直接对照层次核心问题代表算法/操作典型应用底层像素层这个像素/邻域该如何调整高斯滤波、直方图均衡、Canny边缘、膨胀腐蚀、颜色空间转换图像增强、去噪、图像预处理中层结构层哪些像素组合成一个有意义的结构SIFT/SURF、Felzenszwalb分割、HOG、selective search特征提取、超像素分割、候选区域生成高层语义层这个结构代表什么对象SVM分类、R-CNN系列、YOLO、语义分割网络目标检测、图像识别、场景理解做传统视觉项目的时候这三个层次往往要分别设计和调优做深度学习项目的时候卷积网络把部分层次内化了但候选区域生成这种中层逻辑依然会以各种形式出现。1.3 为什么很多人学完OpenCV仍然不会做项目我接触过不少初学者也带过实习生我发现大家普遍卡在一个点上只会在底层像素层打转。比如要检测图像里的螺丝缺陷新手的第一反应通常是“调阈值、找轮廓”。但真实工况下光照不均匀、背景干扰多阈值怎么调都不稳定。问题出在哪出在他没有向上一个层次抽象——螺丝和背景的本质区别可能不在灰度值上而在纹理梯度的一致性上也可能在于“区域”而不是“边缘”上。selective search就是一个典型的“中层结构”工具它要做的事是站在中层的角度去思考一张图里哪些像素区域组合起来最有可能构成一个完整目标。2. selective search 为什么会出现从滑动窗口到候选区域2.1 目标检测里的“暴力枚举”困境在深度学习之前做目标检测最朴素的想法就是“把图上所有可能的窗口都看一遍”——这就是滑动窗口法。听起来很直接但算一下复杂度你就明白了。假设输入图像是 1000×600 像素我们只考虑一个 100×100 的窗口步长取 10 像素那么横向要滑 (1000−100)/10 ≈ 90 次纵向要滑 (600−100)/10 ≈ 50 次总共 4500 个位置。看起来还能接受但实际目标检测不可能只用一种固定尺度。一辆远处的汽车在图上可能只有 50×50 像素近处的同一辆车可能占到 300×200 像素。你至少要覆盖 10~20 种尺度每种尺度下还要考虑 2~3 种宽高比。这么一乘候选窗口数量轻松突破 50 万。每个窗口都要送进分类器提取特征一次哪怕只要 1 毫秒50 万次就是 500 秒将近 10 分钟。这还只是一张图。这种“暴力枚举”在工程上根本跑不动。2.2 区域提议思想的诞生逻辑人眼看图的时候从来不这么干。你不会从图的左上角到右下角一行一行去“搜索”物体在哪里而是第一时间就被颜色差异、边缘轮廓吸引到几个局部区域上。这是底层视觉注意力机制在起作用。区域提议Region Proposal的核心思想就是模仿这种机制先用颜色、纹理、边缘这类底层线索把图像划分成若干可能有目标的区域然后只对这些区域对应的矩形框做后续分类。这样做的好处非常明显候选框数量从几十万降到一两千同时因为区域边界基于图像结构产生对目标的定位比固定窗口精准得多。你可以这样理解滑动窗口是“闭着眼睛在图上摸”区域提议是“先睁眼扫一遍找到几个疑似有东西的地方”。2.3 selective search 在整个检测流程中的位置selective search在这条流程里的位置正好卡在“图像结构”和“语义判断”之间。以经典的 R-CNN 目标检测流程为例输入图像 → 生成候选区域selective search→ 将每个候选区域裁剪并缩放到固定尺寸 → 用卷积网络提取特征 → 用 SVM 分类 框回归 → 非极大值抑制NMS去重 → 输出最终检测框注意关键一点selective search不负责识别任何目标它只负责“建议”。它给你一堆矩形框告诉你“我觉得这些框里可能有东西”。后面的分类器才负责判断框里到底是什么类别。这有个很微妙的后果如果某个目标压根没有出现在候选框里就算后面的分类器再强也不可能把它找出来。所以在 R-CNN 那个时代检测效果的上限往往不是由 CNN 决定的而是由selective search的召回率决定的。这件事直到今天依然有参考意义——你在设计任何检测管线时都要先问一句我的候选区域生成环节有没有可能把目标漏掉3. selective search 的核心怎么“选出”那些可能装目标的框3.1 初始区域基于图的分割selective search的起点是对图像做一次“过分割”把图像拆成很多小块。它选用的是一种名叫 Felzenszwalb 的基于图的分割算法。为什么偏偏选它因为selective search的后续步骤是“合并”而合并的前提是初始区域不能太大也不能太小。Felzenszwalb 算法的特点是速度快、对边缘贴合度高。我当时第一次看到它分割结果的时候印象最深的是它不会像普通阈值分割那样产生一堆破碎噪声也不会像某些超像素算法那样把物体边界抹得太平滑。这一步的产物是一张“区域标签图”——图像里每个像素都被分配了一个区域编号相邻的像素如果颜色接近通常会被划分到同一个初始区域。3.2 相似度怎么算四类互补策略有了初始区域之后算法要做的是从小到大不断合并相邻区域。但“能合”和“该合”不是一回事这就要靠相似度来把关。selective search用了四类相似度不是随意选的四个角度各有分工。第一是颜色相似度。计算两个区域在每个颜色通道上的颜色直方图直方图越接近说明颜色分布越像。它解决的是“同一块区域颜色渐变”的情况比如天空和远处的湖面。第二是纹理相似度。算法会提取区域内的纹理统计特征纹理相似才容易被合并。它解决的是“同一类材质”的情况比如草地、树木、砖墙这类区域内部的颜色可能差距很大但纹理模式是一致的。第三是尺寸相似度。当两个区域都比较小的时候优先合并避免大的区域慢慢吞掉旁边的小区域。这个策略很聪明它保证了合并过程“尽量从局部到整体”不会一开始就出现一个超级大的区域把几个目标一起包进去。第四是填充相似度。这个概念有点反直觉它衡量的是两个区域能不能互相“补全”。如果区域 A 几乎完全包含在区域 B 的包围盒里说明 A 很可能是 B 内部的子结构两者应该合并。你看这四个角度其实是互补的颜色解决整体外观纹理解决内部结构尺寸控制合并节奏填充处理嵌套关系。只用一个角度的算法往往不稳定这就是为什么selective search的设计值得反复咀嚼——它不是在某个单一规则上做得很极端而是用多个互补规则把“区域合并”这个模糊问题拆解得足够稳。3.3 多样化策略为什么要搞那么多颜色空间和相似度组合selective search的作者在做实验时发现一件尴尬的事不管你怎么调分割参数单次分割和单组相似度组合下总有一些目标会被漏掉。原因也不难理解。自然图像的内容太复杂同一只猫在 RGB 空间里可能和背景颜色混杂但在 HSV 空间里饱和度对比却非常明显同一栋楼的纹理在普通灰度图里杂乱无章站远了看却是一个整体。于是作者引入了“多样化”策略在不同的颜色空间里分别做分割和相似度计算同时使用不同的相似度组合方式相当于让“多个视角不同的专家”各自提一批候选框最后把所有的框汇总起来。每一路专家都可能漏掉某些目标但几路专家漏掉的目标集合基本不重叠汇总后的召回率就上去了。代价也很直接计算量成倍增加。这也解释了为什么selective search在 CPU 上跑一张图要花几秒钟——它背后可能跑了多组分段的完整合并流程。3.4 层次合并的完整流程把上面这些东西串起来selective search的完整算法流程是这样的用 Felzenszwalb 分割算法得到初始区域集合。计算所有相邻区域两两之间的相似度相似度由颜色、纹理、尺寸、填充四部分组成。找到相似度最高的一对相邻区域把它们合并成一个新区域。删掉与这两个旧区域相关的相似度记录计算新区域与其邻居的相似度。把新区域对应的外接矩形加入候选框列表。回到第三步重复直到没有任何区域可以合并。对候选框列表做重叠度去重得到最终的候选区域集合。这个过程本质上是一个自底向上的层次聚类。我个人的经验是只看文字描述会觉得抽象但一旦你把中间每一步的区域标签图可视化出来就非常好懂了——你会看到分割图上的小块区域两两抱团慢慢从小碎块聚成中块再到能把整个目标轮廓包起来的大块每一轮合并出来的外接矩形都被记录下来当候选框。4. 实操跑通 selective search 并调出可用的候选框4.1 环境与工具选择实践部分必须落地。目前用selective search主要有两条路我两个都试过区别还挺明显。一是用 OpenCV 的扩展模块ximgproc里的selectiveSearchSegmentation类。安装方式是pip install opencv-contrib-python然后通过cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()创建对象。它有switchToSelectiveSearchFast()和switchToSelectiveSearchQuality()两个预设模式前者更快但候选框少后者慢但召回更好。二是用独立的selectivesearchPython 库pip install selectivesearch即可。底层依赖 OpenCV 和 scikit-image接口非常直观适合做实验和可视化。我的建议是快速验证用selectivesearch库工程集成用 OpenCV 的版本因为它不需要额外维护依赖而且可以通过setBaseImage和addImage灵活地做多策略扩展。4.2 核心参数与调优经验如果你用的是selectivesearch库核心调用就一行import selectivesearch # 读入图像建议先调整到合适尺寸 img cv2.cvtColor(cv2.imread(street.jpg), cv2.COLOR_BGR2RGB) # scale 控制分割粒度sigma 控制平滑程度min_size 控制最小区域面积 img_label, regions selectivesearch.selective_search( img, scale300, sigma0.8, min_size50 )返回的regions是一个列表每个元素是一个字典主要包含两个字段rect是候选框的(x, y, w, h)size是该区域包含的像素数量。我一般会先做一轮过滤把太小且没有实际意义的区域扔掉candidates [] for r in regions: rect r[rect] # 去掉重复框和极小区域 if rect in candidates: continue if r[size] 200: continue candidates.append(rect)这里每个参数都直接影响最终候选框质量我的调试经验如下scale是分割粒度值越大初始区域越粗糙。如果你发现候选框里面目标总是被拆成好几个碎片试着把scale调大比如从 300 调到 400~500。反之如果背景区域太大或目标框太粗糙就调小。sigma控制分割前的平滑程度它作用于高斯模糊。默认的 0.8~1.0 通常够用图像噪声大时可以稍微提高到 1.5但我很少动它因为它对最终结果的影响最不明显。min_size控制初始分割时区块的最小像素数量。它太小会让初始区域碎成渣候选框数量爆炸它太大又可能把小目标直接抹掉。通常我会取 30~60再结合输入分辨率调整——输入图越大这个值可以适当大一点。4.3 与深度学习检测模型配合时的注意点selective search当年最经典的用法是作为 R-CNN 的区域提议模块。现在虽然很少直接用了但只要你手里还有老代码需要维护或者你想复现经典论文下面这些配合细节仍然很有用。第一候选框数量要卡上限。R-CNN 训练时一般取 2000 个候选区域。不需要把selective search生成的所有框都送进网络因为大量框是高度重叠的对训练没有增益反而拖慢速度。你可以先按size排序保留重叠度不同的前 2000 个。第二正负样本的 IoU 阈值要定好。基于候选框和真实标注框的交并比IoU一般 IoU 大于 0.5 的当作正样本IoU 小于 0.3 的当作负样本。注意这个问题在selective search时代就存在——它生成的框天然偏向“包含整个目标的外接框”所以正样本采样比例通常要反复实验。第三记得做非极大值抑制之外的框去重。selective search本身会去重但不同参数组合和多策略模式可能生成多个高度重合的框。如果直接在可视化和评估阶段使用会被重复计数。4.4 常见坑与性能优化selective search最明显的槽点是慢。我自己在一张 1000×600 的街景图上跑默认配置CPU 环境下大致需要 3~6 秒。这个速度在今天的检测框架里几乎不可接受。缓解办法有几个先把输入图缩放到宽边不超过 600 像素。selective search对分辨率非常敏感缩放后速度能提升数倍候选框质量损失其实不大。使用 OpenCV 版本的switchToSelectiveSearchFast()它内部只使用有限组的颜色空间和相似度组合速度能到几百毫秒级别适合先做验证。如果目标是密集小物体不要把min_size调太大否则小目标直接被合并没了此时牺牲一点速度为代价保留更碎的初始区域。另外还有一个非常隐蔽的坑selective search的输入图像通道顺序。用 OpenCV 读图默认是 BGR而selectivesearch库内部是按 RGB 处理的。我见过很多人在这一步翻车候选框的位置倒是没问题但分割效果莫名其妙差很多。直接读图后先cv2.cvtColor再传给selective_search稳定得多。5. 从 selective search 往后看区域提议技术的演进5.1 EdgeBoxes 与 RPN 的对比区域提议这个思路后来延续了很久也出现了几个重要变体。我把主流方案放在一起做过对比最有代表性的有三个selective search、EdgeBoxes、以及深度学习时代的 RPN。方法核心思路单张图速度CPU/GPU特点selective search颜色/纹理/尺寸/填充多策略合并区域1~6 秒CPU通用性强召回率高速度慢EdgeBoxes根据边缘密度和边缘包围度打分0.2~0.5 秒CPU速度快但纹理复杂场景下召回不如 selective searchRPNFaster R-CNN在卷积特征图上直接回归候选框并给出目标性分数约 10 毫秒GPU与检测网络端到端联合训练速度和精度都大幅提升我当时刚接触 Faster R-CNN 时有一个很深的感触RPN 之所以是革命性的不只是因为它快而是它把“候选区域生成”从独立的预处理变成检测网络内部的一个模块让区域质量和分类目标共享同一个特征空间。这背后是一个更大的趋势传统视觉里层层解耦的 pipeline被深度学习逐步“折叠”进一个可微分的端到端网络里。但这不代表selective search失去了学习价值。恰恰相反你要想真正理解 RPN 的标签生成、锚点设计、正负样本分配就必须知道selective search当年是怎么解决“如何不让目标漏出候选框”这个问题的。5.2 在三个层次中重新定位selective search 的桥梁作用把目光拉回到文章最开始讲的三个层次你会发现selective search的位置非常特殊。它的输入是底层分割结构——像素的区域标签本质是颜色、纹理这些中层特征它的输出却是一组“疑似目标的矩形框”这已经一脚踩进高层语义的地盘了。换句话说selective search是经典视觉体系里“中层结构”向“高层语义”过渡的典型代表。这件事给我们的启发是图像处理的层次之间不是非得严格串行。你可以用中层的分割结果直接推测高层的目标假设也可以反过来用高层的检测结果指导底层分割的参数。这就是各种“Active Contour”“显著性驱动分割”方法在做的事。5.3 后续扩展方向如果你真的把selective search的源码和原理啃透了能做延展的方向其实很多。一是在无标注数据的场景里做自动标注工具。我试过用selective search生成候选区域配合一个弱分类器先自动筛掉大部分背景框再人工确认目标框标注效率能提升不少。这在数据清洗阶段非常实用。二是把它的“多样化策略”思想复用在你自己的算法里。比如你要做工业质检单一分割参数不稳定你可以学selective search的做法并行跑多组参数汇总多个分割结果用投票或共识来抑制单参数过拟合。三是和现代分割模型结合。现在很多无监督目标发现、开放世界检测的工作其实仍然需要从图像里找出“显著性区域”再交给语言模型或检测头去判断。这时候回顾selective search的底层逻辑能给你不少灵感。我在 R-CNN 时代做检测实验几乎每天都要等selective search那几秒钟。后来换了 Faster R-CNN再也不用等了但当模型出现漏检的时候我第一时间怀疑的依然是候选区域生成这边有没有把目标“放走”。一个算法最值得留下的不是它的实现代码有多精巧而是它逼着你去思考“目标在图像里以什么形式存在”这个根本问题。做图像处理的人手里得有像素操作的手感脑子里得有层次结构的视角这两样配齐了读任何一种新方法都会快得多。