简介:这份资源是面向计算机视觉初学者与目标检测入门者的选择性搜索(Selective Search)Python 实践示例,帮助读者理解如何通过超像素分割与区域生长策略生成候选目标区域,并衔接 R-CNN、Fast R-CNN 等检测模型。压缩包共 19 个文件,以 12 张 png 与 2 张 jpg 图像素材、3 个 py 脚本、1 份 md 说明及 1 份 license 为主,整体约 53KB,体积轻便,便于快速下载与本地运行。其中脚本按步骤拆分,覆盖搜索、过滤与特征提取等环节,图像素材可直接用于验证算法效果,md 文档则提供必要的使用说明。目前已有 285 人学习下载,适合希望以最小成本跑通选择性搜索流程、理解候选区域生成原理的读者参考。
1. 选择性搜索到底在解决什么问题
如果你跑过 R-CNN 系列的代码,一定见过 selective_search 这个名字。它不负责分类,也不负责回归框,它只干一件事:从一张图里生成几百到两千个候选区域,交给后面的网络去判断。很多人第一次跑目标检测 demo 时卡住,不是模型加载失败,而是候选区域生成这一步没跑通,或者跑出来框的位置完全不对。这个项目就是一个选择性搜索的简单示例,用 Python 把分层分组、区域合并、多样性排序这条链路完整走了一遍。适合谁?适合正在复现 R-CNN、想搞懂候选区域怎么来的开发者,也适合想拿它当图像分割预处理练手的人。它不依赖深度学习框架,纯 CPU 就能跑,对刚入门计算机视觉的人比较友好。
2. 项目结构与核心模块拆解
2.1 目录布局与文件职责
拿到源码后先别急着跑,花两分钟把目录看一遍,后面改代码会省很多事。这个示例项目的结构通常比较扁平,常见做法是下面这样:
selective_search/ ├── main.py # 入口,读图、调用搜索、画框、保存结果 ├── selective_search.py # 核心算法实现 ├── utils.py # 图像读写、颜色空间转换、相似度计算 ├── config.py # 阈值、尺度、最小区域面积等参数 └── images/ # 测试图片main.py只做流程编排,不掺算法逻辑;selective_search.py是重点,分层分组和区域合并都在这里;utils.py放的是被反复调用的工具函数,比如把 RGB 转成 HSV、计算两个区域的颜色直方图相似度。我一般会把所有可调参数抽到config.py,这样调参时不用在算法文件里翻来翻去。如果你拿到的版本没有 config,建议自己拆一个出来,这是二次开发时最省心的习惯。
2.2 分层分组:把像素先切成小区域
选择性搜索的第一步不是直接合并,而是先做过度分割,把图像切成很多小块。常见做法是用 Felzenszwalb 或者类似的分割方法,把颜色相近、位置相邻的像素聚成初始区域。这一步决定了后面合并的起点,切得太碎合并次数多、速度慢,切得太粗又容易漏掉小目标。
# utils.py import numpy as np def initial_segmentation(image, scale=200, sigma=0.8, min_size=50): """ 对输入图像做初始过度分割 :param image: RGB 图像, shape=(H, W, 3) :param scale: 分割尺度, 越大区域越少 :param sigma: 高斯平滑核宽度, 抑制噪点 :param min_size: 最小区域像素数, 小于该值的区域会被强制合并 :return: labels, 每个像素所属的区域 id """ # 这里调用具体分割实现, 返回每个像素的标签 labels = _segment(image, scale=scale, sigma=sigma, min_size=min_size) return labelsscale控制分割的粗细,值越大切出来的区域越少;sigma是平滑参数,图像噪点多的时候调大一点;min_size防止出现只有几个像素的碎片区域。这三个参数在config.py里改,不要写死在函数里。初始分割做完后,每个区域会带上颜色直方图、纹理梯度直方图和面积、位置信息,这些就是后面合并时的判断依据。
2.3 区域合并:相似度怎么算、按什么顺序合
合并是选择性搜索的核心。它维护一个相邻区域对列表,每次挑相似度最高的一对合并,合并后更新这对新区域和它邻居的相似度,再放回列表。相似度由四个分量加权组成:颜色、纹理、尺寸、填充。颜色和纹理决定“像不像”,尺寸和填充决定“合不合理”。
# selective_search.py import numpy as np def similarity(r1, r2, w_color=1.0, w_texture=1.0, w_size=0.5, w_fill=0.5): """ 计算两个区域的综合相似度 :param r1, r2: 区域对象, 含颜色直方图、纹理直方图、bbox、面积 :param w_color: 颜色相似度权重 :param w_texture: 纹理相似度权重 :param w_size: 尺寸相似度权重, 鼓励小区域先合并 :param w_fill: 填充相似度权重, 鼓励合并后外接框更紧凑 :return: 标量相似度, 越大越该合并 """ s_color = hist_intersection(r1.color_hist, r2.color_hist) s_texture = hist_intersection(r1.texture_hist, r2.texture_hist) s_size = 1.0 - (r1.area + r2.area) / total_image_area s_fill = 1.0 - (r1.area + r2.area) / bbox_area(union_bbox(r1, r2)) return (w_color * s_color + w_texture * s_texture + w_size * s_size + w_fill * s_fill)颜色和纹理用直方图交集来算,值在 0 到 1 之间。尺寸相似度这一项容易被忽略,它的作用是让面积小的区域优先合并,避免一个大区域过早吞掉周围的小块。填充相似度衡量合并后的外接框有多“实”,如果两个区域合起来外接框里空了一大片,说明它们其实不该合。四个权重在config.py里调,默认值在多数自然图像上表现稳定,遇到特定场景再微调。
合并过程用一个优先队列维护,每次弹出相似度最高的一对,合并后把新区域和邻居的相似度重新算一遍压回去。这里有个细节:合并后要更新的是新区域与它所有邻居的相似度,不是只更新被合并的那两个。漏掉这一步,后面生成的框会明显偏。
2.4 多样性排序:为什么最后还要再筛一遍
合并到只剩一个区域时,我们其实已经记录了整个合并过程中的所有中间区域。如果全拿出来,数量太多,而且很多是高度重叠的。所以最后要做一次多样性排序:按面积从小到大遍历,如果某个区域和已选区域的交并比超过阈值就跳过,否则保留。
# selective_search.py def diverse_selection(regions, iou_thresh=0.8): """ 对候选区域做多样性筛选, 去掉高度重叠的框 :param regions: 合并过程中记录的所有区域 :param iou_thresh: 交并比阈值, 超过则视为重复 :return: 筛选后的候选区域列表 """ regions = sorted(regions, key=lambda r: r.area) # 小区域优先 selected = [] for r in regions: if all(iou(r.bbox, s.bbox) < iou_thresh for s in selected): selected.append(r) return selectediou_thresh调小,留下的框更少、更分散;调大,框更多、重叠更严重。R-CNN 原论文里这个值一般取 0.8 左右。排序时按面积从小到大,是为了让小的、可能包含独立目标的区域先被选中,大区域如果和已选的小区域重叠太多就被丢掉。这一步做完,通常一张图能剩下几百到两千个候选框,正好够后面的网络用。
3. 从零跑通:环境、命令与结果验证
3.1 环境准备与依赖安装
这个示例不依赖 GPU,也不需要深度学习框架,Python 3.7 以上就能跑。依赖主要是 numpy、scikit-image、opencv-python 和 matplotlib。我一般用虚拟环境隔离,避免和系统里的包打架。
# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install numpy scikit-image opencv-python matplotlib如果你的机器上 scikit-image 装得慢,可以先升级 pip。装完后在 Python 里import skimage不报错,环境就算好了。注意 opencv-python 和 opencv-contrib-python 不要同时装,容易冲突,选一个就行。
3.2 运行入口与参数说明
入口在main.py,常见写法是读一张图,调用selective_search,把候选框画出来保存。
# main.py import cv2 from selective_search import selective_search from config import SS_CONFIG if __name__ == "__main__": img = cv2.imread("images/test.jpg")[:, :, ::-1] # BGR 转 RGB regions = selective_search( img, scale=SS_CONFIG["scale"], sigma=SS_CONFIG["sigma"], min_size=SS_CONFIG["min_size"], iou_thresh=SS_CONFIG["iou_thresh"], ) print(f"生成候选区域数量: {len(regions)}") vis = draw_boxes(img, regions[:200]) # 只画前 200 个, 避免太乱 cv2.imwrite("output.jpg", vis[:, :, ::-1])scale、sigma、min_size控制初始分割,iou_thresh控制最后筛选。第一次跑建议先用默认值,看输出数量是否在合理范围。如果打印出来只有几十个框,多半是scale太大或者min_size太大;如果几千个框,把iou_thresh调小一点。
3.3 结果怎么看才算对
跑完后打开output.jpg,框应该覆盖住图中主要物体,而且同一个物体上不会叠太多重复框。判断标准不是框多,而是框的位置有没有落在物体上。如果框全挤在背景或者边缘,检查一下颜色空间转换是不是漏了,RGB 和 BGR 搞反是新手最常见的翻车点。另外可以打印一下每个区域的面积分布,正常情况应该从小到大多个尺度都有,如果全是特别大的框,说明合并顺序有问题。
提示:第一次跑建议用一张背景干净、物体突出的图,比如单个杯子或一本书,方便肉眼判断框准不准。
4. 避坑与常见问题排查
4.1 框的位置整体偏移
现象:画出来的框和物体对不上,整体偏了一个方向。原因:读图时用了 BGR,算法内部按 RGB 处理,颜色直方图算出来是错的,合并顺序跟着乱。解决:在入口处统一转成 RGB,cv2.imread之后加[:, :, ::-1],或者用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。
4.2 候选框数量异常少
现象:打印出来只有几十个区域。原因:min_size设得太大,初始分割阶段小区域被强制合并,后面可合并的起点就少了。解决:把min_size降到 20 到 50 之间,同时检查scale是不是超过 300。这两个参数一起调,先保证初始区域数量在几百以上。
4.3 运行速度慢到无法接受
现象:一张 800x600 的图跑了几分钟。原因:相似度计算里直方图交集是逐对算的,区域多的时候是平方级开销。解决:把颜色直方图维度从 75 降到 25 左右,纹理直方图维度也降一半;另外在合并时只更新新区域和直接邻居的相似度,不要全量重算。这两步做完,速度通常能快三到五倍。
4.4 同一个物体上叠了十几个框
现象:输出图里一个杯子被十几个框套住。原因:iou_thresh设得太大,多样性筛选没起到去重作用。解决:把iou_thresh从 0.9 降到 0.7 到 0.8 之间,重新跑一遍。如果还是多,检查排序是不是按面积从小到大,排序反了会导致大框先占位,小框全被滤掉。
4.5 换图后效果波动大
现象:这张图还行,换一张就完全不对。原因:固定参数对不同分辨率、不同光照的图适应性有限。解决:按图像短边做归一化,把图缩到短边 500 左右再跑;光照差异大的话,在初始分割前加一步直方图均衡。这不是算法 bug,是参数没跟着数据走。
5. 二次开发:把示例改成能用的模块
5.1 封装成可调用的候选区域生成器
示例代码是脚本式的,直接拿去嵌到 R-CNN 流程里不方便。我一般会把它包成一个类,初始化时传配置,调用时只传图。
class SelectiveSearchGenerator: def __init__(self, scale=200, sigma=0.8, min_size=50, iou_thresh=0.8): self.scale = scale self.sigma = sigma self.min_size = min_size self.iou_thresh = iou_thresh def generate(self, image_rgb): """输入 RGB 图, 返回候选框列表 [(x1, y1, x2, y2), ...]""" regions = selective_search( image_rgb, self.scale, self.sigma, self.min_size, self.iou_thresh ) return [r.bbox for r in regions]这样在检测流程里就是generator.generate(img)一行,参数在初始化时定好,换数据集时只改构造参数,不动调用处。如果你要批量处理,可以在类里加一个generate_batch,内部用多进程池,因为算法本身是 CPU 密集的,多进程比多线程有效。
5.2 参数调优的验证方法
调参不能靠感觉。我一般固定三张图:一张单物体、一张多物体、一张背景复杂。每次改完参数,记录三个数:候选框总数、平均 IoU(和标注框比)、召回率(标注框被覆盖的比例)。下面这张表是我在某次调参时的记录,供参考:
| scale | min_size | iou_thresh | 框数量 | 召回率 |
|---|---|---|---|---|
| 200 | 50 | 0.8 | 1200 | 0.82 |
| 300 | 50 | 0.8 | 700 | 0.76 |
| 200 | 100 | 0.8 | 600 | 0.71 |
| 200 | 50 | 0.7 | 800 | 0.80 |
从表里能看出来,scale从 200 提到 300,框数量掉了一半,召回率也掉了六个点。min_size翻倍,召回率掉得更明显。所以如果下游检测模型对小目标敏感,min_size不要轻易调大。iou_thresh从 0.8 降到 0.7,框少了但召回率只掉两个点,说明去重去掉的多数是冗余框,这种调整是划算的。
5.3 一个容易忽略的改造点
示例里相似度权重是写死的,但不同数据集对颜色和纹理的敏感度不一样。比如医学图像纹理更重要,自然场景颜色更重要。我一般会把四个权重也提到配置里,调参时一起动。另外,初始分割如果换成基于超像素的方法,对小目标更友好,但速度会慢一些,这个取舍看你的场景。
从那以后我每次改候选区域生成模块,都强制先跑一遍三张固定图的召回率对比,不看到数字不动下游模型。希望帮到你。
本文还有配套的精品资源,点击获取