简介:选择性搜索(Selective Search)是目标检测中生成候选区域的关键技术,这份Python示例资源面向计算机视觉入门者,清晰演示了该算法的核心实现流程。压缩包共19个文件,包含3个Python脚本、12张png示例图片、2张jpg图像,以及说明文档和许可证,整体仅53KB,代码与配套图片相结合,方便按步骤对照理解。已有283人学习,适合正在学习区域提议、希望替代传统滑动窗口检测方式的读者。资源通过分阶段脚本展示超像素分割、相邻区域合并和候选框提取过程,结合具体图像输出直观结果,可帮助读者快速掌握选择性搜索的工作原理,并为后续衔接RCNN、Fast RCNN等目标检测模型打下良好基础。
1. selective_search:为什么 R-CNN 时代的候选框生成方法今天还值得跑一遍
selective_search(选择性搜索)在目标检测里的地位有点像“老木匠的手艺”——深度模型火起来之前,R-CNN 靠它给每张图生成约 2000 个候选框,把检测问题硬生生转成了分类问题。这个资源是一个跑通全流程的最小示例,从输入一张图片到输出一批可能包含目标的矩形框,代码短到不需要单独建工程。它解决的是“候选区域从哪里来”的问题:不靠滑动窗口穷举,而是用分割结果反推目标位置。适合刚入门目标检测、想搞懂 R-CNN 第一块砖的人,也适合现在需要给检测任务做候选框预生成、又不想重新啃论文的从业者。几分钟跑完,你就能亲眼看到候选框是怎么从图像分割里长出来的。
2. 选择性搜索原理:从过分割到 2000 个候选框的合并策略
2.1 滑动窗口穷举为什么被淘汰
检测一个物体,最朴素的想法是滑窗。以不同尺度、不同宽高比遍历整张图,每个位置截一块交给分类器判断。问题是组合爆炸:一张 1920 宽度的图像,步长 4、5 个尺度、3 个宽高比,框的数量轻松超过 20 万。即使把步长放宽到 32,再加上图像金字塔,也还有数万框。这些框绝大多数是背景,分类器大部分时间在做无用功,训练和推理都慢到不可接受。
选择性搜索的思路完全不同:先用分割算法把图像切成小区域,再按相似度不断合并,合并过程中把每一次合并得到的区域记录下来,作为候选框。这样做等于先在分割层面假设“目标通常会被切成若干相邻小块”,再把小块按规律拼回去。候选框数量从几万降到一两千,而且边界贴合真实目标,不是滑窗那种正方形硬框。
R-CNN 当年选它,核心看中的就是两个数字:每张图约 2000 个候选框,与标注框的召回率能到 98% 以上。边界贴合意味着候选框和真实框重叠度高,后接的分类器更容易区分正负样本,这一点在后面的 IOU 校验中还会反复遇到。
2.2 分层分组:四个相似度怎么决定区域合并
初始分割在论文里用的是 Felzenszwalb 的图分割算法,它基于最小生成树的思想,把颜色和纹理相近的像素不断并成区域。这个分割的粒度偏细,输出的是过分割的小碎块,比如一只猫可能被切成头、身体、前腿、尾巴七八块。这些碎块太小,必须往上一层合并。
合并过程是贪心的:每次找全局相似度最高的相邻区域对,合并后更新与周围邻居的相似度,再继续找下一对,直到全图并成一个区域。选择性搜索的相似度由四部分加权组成,这也是论文里最值得记的内容:
| 相似度 | 计算方式 | 要解决什么问题 |
|---|---|---|
| s_color | 每个通道 25 个 bin 的颜色直方图交集 | 颜色一致的区域先合并 |
| s_texture | 基于 SIFT-like 梯度的 8 方向纹理直方图 | 纹理一致但颜色有差异的区域也能合在一起 |
| s_size | 鼓励小区域优先合并 | 防止大区域不断吞并小区域,导致尺度失衡 |
| s_fill | 衡量两区域合并后能否互相填补空洞 | 让形状互补的碎块更容易合并成一个完整目标 |
四个相似度加权求和后得到综合相似度,权值可以按场景调整。注意这里整个算法没有任何可学习参数,合并规则是人工设计的,这也是选择性搜索如今依然有用的原因——它不需要训练数据,拿来当 baseline 永远稳定。
2.3 多尺度策略:候选框数量怎么控制在 2000 上下
同一个目标在不同分辨率下呈现的分割结果不一样。一张大图上能分割出的细节,缩小后可能就被合并没了。为了捕捉这种尺度差异,选择性搜索用多种分割参数分别跑几轮,把各轮产生的中间区域全部叠加起来。叠加的结果就是候选框数量膨胀,一般控制在 2000 左右,这个数字就是后来 R-CNN 每张图约 2000 个 region proposals 的来源。
实际算下来,候选框并不全是矩形框直接用,还会做一层去重和过滤。区域合并过程中许多中间区域高度重叠,只保留在层次结构上更完整的那些。论文的实现里对重复框做了抑制,但 OpenCV 和独立库版本的实现细节略有差异,输出数量也依赖参数的设置。
到这个层面可以看出,选择性搜索本质上是一个纯前处理工具,输入是图像,输出是一批x, y, w, h坐标列表。结构非常简单,这也是这个示例资源可以做得短小的原因。后面两章就走代码,把这条链路跑起来。
3. 用 Python 跑通选择性搜索:OpenCV 与独立库两条最小实现
3.1 环境准备:注意 opencv-contrib 与独立库的区别
OpenCV 里选择性搜索实现在cv2.ximgproc.segmentation下,这个扩展模块不在默认的opencv-python包里。很多人踩的第一个坑就在这里:import cv2 成功,但访问cv2.ximgproc直接报错,后面详说。先按标准流程装:
# 先卸载默认版本,避免两个 OpenCV 包共存产生符号冲突 pip uninstall opencv-python # 安装带 contrib 扩展的版本 pip install opencv-contrib-python如果还需要独立库版本,单独再装一个:
pip install selectivesearch两套可以共存:OpenCV 版走createSelectiveSearchSegmentation接口,独立库走selective_search函数。我的习惯是先用 OpenCV 版快速看效果,再用独立库精细调参,因为独立库直接暴露了 scale、sigma、min_size 三个核心参数。
3.2 OpenCV 最小示例:三步生成候选框
OpenCV 的接口封装得很紧凑,核心就三步:创建分割器、设置基准图、切换模式然后 process。
import cv2 # 读取图像,注意用彩色图,选择性搜索依赖颜色信息 image = cv2.imread("demo.jpg") # 第一步:创建选择性搜索分割器 ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation() # 第二步:设置基准图 ss.setBaseImage(image) # 第三步:切换快速模式并执行 ss.switchToSelectiveSearchFast() rects = ss.process() # rects 是 N x 4 的数组,每行是 [x, y, w, h] print("候选框数量:", rects.shape[0]) for i, (x, y, w, h) in enumerate(rects[:10]): print(f"box[{i}]: x={x}, y={y}, w={w}, h={h}")逻辑很直白:setBaseImage 告诉算法在哪个图上做分割和合并,switchToSelectiveSearchFast 走快速模式,process 执行整个流程。rects的类型是 numpy 数组,坐标格式是x, y, w, h,不是左上右下两个点。画框时要自己换算成(x, y)和(x + w, y + h),这个换算错位是后面排查章节的高频事故。
如果想要质量更好、数量更多的候选框,把切换行改成:
# 质量模式:候选框更多、边界更准,耗时明显增加 ss.switchToSelectiveSearchQuality() rects = ss.process()两种模式在 OpenCV 里的实现是封装好的,fast 模式候选框数量通常在几百量级,quality 模式在几千量级。具体数量由图像内容决定,不设上限。
把候选框画出来,直观验证结果是否合理:
def draw_boxes(image, boxes, max_draw=500): vis = image.copy() for x, y, w, h in boxes[:max_draw]: cv2.rectangle(vis, (int(x), int(y)), (int(x + w), int(y + h)), (0, 255, 0), 2) return vis # 用快速模式的结果画图 vis_img = draw_boxes(image, rects) cv2.imwrite("selective_search_result.jpg", vis_img)画图时只画前 500 个框,否则密密麻麻全是绿框,看不出任何有效信息。这一步建议每换一张测试图都做一次,候选框数量和分布能直观反映参数是否合理。
3.3 独立库版本:读 regions 字典的结构
selectivesearch库返回的结构和 OpenCV 完全不同,它返回两个对象:img_lbl是每个像素归属区域编号的二维数组,regions是区域字典的列表。对候选框来说,主要取 regions 里的 rect 字段。
import cv2 import selectivesearch image = cv2.imread("demo.jpg") # scale 控制分割粒度,sigma 控制高斯滤波强度,min_size 过滤过小碎片 img_lbl, regions = selectivesearch.selective_search( image, scale=500, sigma=0.9, min_size=10 ) candidates = set() for r in regions: # 每个 r 是一个字典,rect 字段是 (x, y, w, h) x, y, w, h = r["rect"] # 过滤掉明显过小的碎片框 if w < 20 or h < 20: continue candidates.add((x, y, x + w, y + h)) print("候选区域数量:", len(candidates))这里有几个值得注意的点:img_lbl虽然不直接产框,但做像素级别的相似度分析时很有用;regions里每个字典还带size、labels、color_hist等键,labels表示该区域由哪些原始分割块组成;rect出来的坐标是整数元组,但在极端情况下可能有值为 0,画图前最好做一次类型转换和边界检查。
两套实现对比下来,OpenCV 版适合集成进现成流程,独立库版适合做算法实验和调参。同一个资源把两条路都覆盖了,这也是我把它定义为“简单示例但能直接落地”的原因。
4. 调参实践:fast 与 quality、scale 与 minSize 怎么组合出想要的候选框
4.1 fast 和 quality 的选择逻辑
不少新手默认用 quality,认为候选框越全越好。实际工程里要分场景:纯跑流程 demo,fast 足够,单张图几十到几百毫秒;目标是生成训练标注或者做 proposal 预生成,quality 更合适,代价是单张图可能到一两秒甚至更久。图像分辨率越高,quality 的耗时差距越明显,1920 宽度的图翻三到五倍是常态。
我一般定两条规则:视频流或大批量离线预处理用 fast;单图质量敏感的任务先用 quality 跑几张确认效果,再决定要不要降级。注意 fast 不是“劣质”,它和 quality 的差异更多在候选框覆盖度和边界精度,对后续 IOU 大于 0.5 的召回率判定,差距没有想象中大。
4.2 scale、sigma、minSize 的参数语义
独立库版本直接暴露了三个核心参数,理解它们才能把候选框调成你想要的形状:
| 参数 | 作用 | 推荐起始值 | 调参方向 |
|---|---|---|---|
| scale | 分割粒度,相当于 Felzenszwalb 分割的 k | 300~600 | 调大后分割更完整,候选框更少但边界更整;调小后碎块多,候选框碎片化 |
| sigma | 高斯模糊半径,影响分割前的平滑强度 | 0.8~1.0 | 过小抗噪差,过小边缘模糊;过大会丢失细目标边界 |
| min_size | 最小分割区域面积,小于该值的区域直接并入邻居 | 10~50 | 目标小而密时调到 5 左右;大目标场景调到 50 减少碎片 |
跑一组对比脚本,用数据理解 scale 的影响,比看任何解释都直观:
for s in [200, 500, 800]: img_lbl, regions = selectivesearch.selective_search( image, scale=s, sigma=0.9, min_size=10 ) if not regions: continue total_area = sum(r["size"] for r in regions) area_avg = total_area / len(regions) print(f"scale={s}: 区域数={len(regions)}, 平均面积={area_avg:.0f}")输出结果通常符合预期:scale 从 200 调到 800,区域数量下降,平均面积上升。如果区域数少到只剩几个大块,说明 scale 调过头了,目标被并进背景区域里。反过来,区域数高达几千且平均面积只有几百像素,多半是 sigma 设置太小导致分割太碎。
4.3 候选框过滤与数量截断
候选框不是越多越好。框多了,后续特征提取的计算量线性上涨,而且大量高度重叠的冗余框会让分类器训练样本失衡。常用的做法是先过滤再截断:
def filter_candidates(regions, min_side=20, max_ratio=5): kept = [] for r in regions: x, y, w, h = r["rect"] # 去掉过小碎片 if min(w, h) < min_side: continue # 去掉宽高比极端的细长条 if max(w, h) / max(min(w, h), 1) > max_ratio: continue kept.append((x, y, w, h)) return kept过滤逻辑的核心是两条经验:候选框太小没有分类意义,宽高比太极端多半是合并错误产生的横跨背景的条带。OpenCV 版的 rects 也可以套同样的过滤,只需要把r["rect"]换成直接遍历矩阵行。
数量截断方面,原论文推荐 2000 左右,但实际场景里 1000 到 1500 已经覆盖绝大多数目标。截断前最好按框面积排序,优先保留面积大的完整框,而不是盲目取前 N 个。
5. 常见问题排查:模块缺失、坐标越界与慢到怀疑人生
5.1 导入 cv2 成功却找不到 cv2.ximgproc
现象:import cv2正常,但访问cv2.ximgproc.segmentation.createSelectiveSearchSegmentation时报 AttributeError,提示 ximgproc 不存在。
原因:当前环境下装的是标准的opencv-python,它只包含主模块,不包含 contrib 扩展。选择性搜索实现在ximgproc模块里,而这个模块只在 contrib 版里发布。
解决:卸载标准版后安装 contrib 版。
pip uninstall opencv-python pip install opencv-contrib-python装完验证一下版本和模块是否存在:
import cv2 print(cv2.__version__) print(hasattr(cv2, "ximgproc"))如果有ximgproc属性,说明安装成功。检查时优先在干净的虚拟环境里操作,避免系统 Python 里残留多个 OpenCV 包互相干扰。
5.2 画框全在左上角或者全部越界
现象:用cv2.rectangle画候选框,画出来的全是斜线或者都挤在左上角,有些框还超出图像边界。
原因:这是最典型的坐标格式错误。OpenCV 返回的 rect 是x, y, w, h,如果直接把(w, h)当作第二个点的坐标,画出来就是从(x, y)指向(w, h)的斜线段。另一个常见原因是使用归一化坐标没有还原成像素。
解决:画框前统一转换成x1, y1, x2, y2:
for x, y, w, h in rects: pt1 = (int(x), int(y)) pt2 = (int(x + w), int(y + h)) cv2.rectangle(image, pt1, pt2, (0, 255, 0), 2)同时加一层边界裁剪,防止框超出图像宽高,特别是在图像边缘检测出目标时:
h_img, w_img = image.shape[:2] x2 = min(int(x + w), w_img) y2 = min(int(y + h), h_img) x1 = max(int(x), 0) y1 = max(int(y), 0)5.3 quality 模式单张图跑了十几秒
现象:4K 分辨率下切到 quality 模式,单张图耗时超过 15 秒,看起来像程序卡死。
原因:quality 模式内部运行的合并策略更复杂,配合超大分辨率图像,初始分割区域数量暴涨,贪心合并的轮次接近指数级上升。这是算法本身的复杂度,不是设备性能问题。
解决:先降采样再生成候选框。这个做法在 R-CNN 时代就是标准操作,候选框对分辨率不敏感,但耗时对分辨率极其敏感。
scale = 1600 / max(image.shape[:2]) if scale < 1.0: resized = cv2.resize(image, (int(image.shape[1] * scale), int(image.shape[0] * scale))) else: resized = image # 在 resized 上跑候选框,最后把坐标除以 scale 还原到原始尺寸还原坐标时注意除回去的精度损失,转成 int 之前先算 float,最后统一转换。
5.4 独立库返回的候选框全是 0 或数量极少
现象:selectivesearch.selective_search返回的 regions 总量很少,有的 rect 是(0, 0, 0, 0),或者结果全是整图大小的大框。
原因:最常见的是输入图像不是三通道彩色图。灰度图没有颜色直方图特征,s_color相似度计算失效,合并方向完全偏向纹理,导致分割结果全被快速吞并成一个大区域。另一个可能是全空白或纯色图像,没有任何可区分的内容。
解决:统一转成三通道 BGR 再传入。
if len(image.shape) == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)对纯色图像属于算法极限,没有候选框可产是正常行为,不用过度调参。遇到这种图先检查图像本身有没有有效内容。
5.5 候选框与标注框的 IOU 整体偏低
现象:用候选框和已有标注框计算最大 IOU,大部分不足 0.6,看起来框住了目标但总差一截。
原因:scale 设置和标注框尺度不匹配。目标整体被分割成多个子区域,合并过程中又没合到一起,最后候选框只包住目标的一半。另一个原因是 min_size 设置过大,小目标被完全吞并进背景。
解决:调大 scale,比如从 500 调到 800,同时把 min_size 降到 20 以下,让分割更完整。标注框尺度分布很宽的数据集,建议对每个尺度分别生成候选框后合并去重,再进入后续逻辑。
6. 验证候选框质量:IOU 匹配与召回率抽查脚本
6.1 手动实现 IOU
候选框质量不能凭眼睛判断,得数值化。最直接的指标是 IOU,即候选框与标注框的 IoU 重叠度。手工实现代码很短:
def compute_iou(box, gt): # 输入都是 (x, y, w, h) x1 = max(box[0], gt[0]) y1 = max(box[1], gt[1]) x2 = min(box[0] + box[2], gt[0] + gt[2]) y2 = min(box[1] + box[3], gt[1] + gt[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_box = box[2] * box[3] area_gt = gt[2] * gt[3] return inter / (area_box + area_gt - inter)这段代码的前提是两个框都按x, y, w, h传入。如果标注文件给的是x1, y1, x2, y2,先转换再调用,否则算出来的 IOU 没有任何意义。
6.2 统计召回率
对每个标注框,只要存在任何一个候选框与其 IOU 大于等于 0.5,就认为这个目标被候选框队伍成功召回。统计所有标注框的召回率:
def recall_at_threshold(candidates, gt_boxes, thresh=0.5): hit = 0 for gt in gt_boxes: if any(compute_iou(cand, gt) >= thresh for cand in candidates): hit += 1 return hit / len(gt_boxes)0.5 这个阈值不是随便定的,R-CNN 时代用候选框生成正样本时阈值就是 0.5,低于这个重叠度训练出来的分类器很难把目标框和背景分开。注意召回率高不代表候选框质量好,如果一个数据集只有 10 个目标,候选框把整图覆盖也能刷到高召回率。所以还要结合候选框数量一起看:同样 1000 个框,召回率 95% 和 80% 之间差距巨大。
我现在的习惯是拿一批有标注的测试图,跑完候选框生成脚本后强制输出平均候选框数量、平均最大 IOU、召回率三个数字。从那以后每次换数据集或者调整分割参数,都会先把这段召回率脚本走一遍,确认候选框质量没有回归才继续往下做。希望帮到你。
本文还有配套的精品资源,点击获取