简介:这份资源是选择性搜索(Selective Search)的 Python 入门示例,面向计算机视觉初学者及目标检测方向的学习者,帮助理解如何用超像素分割与区域生长策略生成候选目标区域,并衔接 R-CNN、Faster R-CNN 等检测模型。压缩包共 19 个文件,以 12 个 png 与 2 个 jpg 图像素材、3 个 py 脚本、1 个 md 说明和 1 个 license 为主,整体约 53KB,体积轻巧便于本地运行调试。代码按步骤拆分为搜索、过滤、特征提取等模块,配合示例图片可直观观察候选框的生成与筛选过程,适合作为算法原理验证与二次开发的起点。目前已有 285 人学习下载,读者可借此掌握选择性搜索的完整流程,理解超像素合并与区域排序思路,为后续目标检测项目打下基础。
1. 从一张图里找出所有候选框:这个 selective_search 示例到底能干什么
如果你写过目标检测相关的代码,大概率遇到过这样的场景:模型训练完了,推理时却不知道该把哪些区域送进网络。总不能把整张图滑窗滑一遍吧,算力扛不住。这时候就需要一种「区域提议」机制,先帮我们把可能藏有目标的候选框筛出来,再交给后续的分类或检测模型。selective_search 就是干这个的经典算法,而这个示例项目,把它的核心流程用 Python 拆成了能直接跑、能直接改的代码。
它适合两类人:一类是刚接触目标检测、想搞明白「候选区域是怎么来的」的新手,另一类是要在自定义数据集上快速验证区域提议效果、需要拿代码当脚手架改的老手。项目本身不依赖深度学习框架,纯 Python 加基础图像处理库就能跑,对环境的容忍度很高,拿来当理解 selective_search 的入口再合适不过。
2. 环境准备与最小运行路径:从零把示例跑起来
2.1 依赖清单与安装方式
这个示例的依赖非常克制,核心就几个:图像读写、数值计算、以及可选的图像显示。常见做法是用numpy做矩阵运算,opencv-python或Pillow做图像加载和绘制,matplotlib用来把候选框可视化出来。如果你不想装 OpenCV,用 Pillow 也能跑通,只是绘制框的时候接口略有差别。
安装命令我一般直接写在一个requirements.txt里,方便复现:
# 基础依赖,版本不锁太死,兼容性更好 pip install numpy pip install opencv-python pip install matplotlib提示:如果你的环境里已经有 Pillow,可以不用装 opencv-python,把代码里
cv2.imread换成Image.open即可,但要注意通道顺序从 BGR 变成 RGB。
2.2 目录结构与文件职责
示例项目的目录通常不会太复杂,我按常见组织方式给你梳理一遍,你拿到源码后可以对照着看:
selective_search/ ├── selective_search.py # 算法主入口,包含区域合并与候选框生成 ├── utils.py # 图像读写、相似度计算、框的绘制等辅助函数 ├── demo.py # 跑通示例的脚本,加载图片并输出候选框 ├── images/ # 放测试图片的目录 └── requirements.txt # 依赖清单selective_search.py是核心,里面会定义区域初始化、相似度度量、合并策略这几个关键部分。utils.py负责把颜色空间转换、纹理梯度计算这些琐碎操作封装起来。demo.py则是给你一个「一键运行」的入口,不需要你手动传一堆参数。
2.3 最小运行命令与预期输出
跑通示例通常只需要一行命令:
python demo.py --image images/test.jpg --output output.jpg执行完之后,你会得到一张画满候选框的图片,框的数量一般在几百到两千之间,取决于图片复杂度和阈值设置。同时终端会打印出生成的区域总数和耗时。如果你看到框密密麻麻盖在物体周围,说明流程是通的。
参数说明:--image指定输入图片路径,--output指定输出图片路径。有些示例还会暴露--scale参数控制图像缩放比例,缩放越小跑得越快,但小目标可能被漏掉。我一般先用默认值跑一遍,确认没问题再调。
2.4 运行失败时先看什么
如果报错ModuleNotFoundError,先检查依赖是否装全,尤其是cv2和matplotlib这两个容易漏。如果图片读进来是 None,多半是路径写错了或者图片格式不支持,换成绝对路径试试。如果跑完输出图是空白的,检查一下绘制框的时候坐标有没有越界,或者框的线宽设成了 0。这些坑我后面还会在避坑章节里细说。
3. 核心模块拆解:区域初始化、相似度度量与合并策略
3.1 区域初始化:把图像切成小碎片
selective_search 的第一步是把图像过度分割成很多小区域,这一步通常用 Felzenszwalb 算法或者类似的图分割方法。示例里一般会调用现成的分割函数,把图像变成几百个小块,每个小块内部颜色和纹理比较一致。
import numpy as np from skimage.segmentation import felzenszwalb def init_regions(image, scale=100, sigma=0.5, min_size=50): """ 用 Felzenszwalb 算法做过度分割 :param image: 输入图像,RGB 格式 :param scale: 控制分割粒度,越大块越少 :param sigma: 高斯平滑参数 :param min_size: 最小区域像素数 :return: 分割标签图,每个像素属于哪个区域 """ segments = felzenszwalb(image, scale=scale, sigma=sigma, min_size=min_size) return segments逻辑说明:felzenszwalb返回的是一个和图像同尺寸的标签矩阵,每个像素值代表它属于哪个区域。scale参数很关键,调大之后区域数量减少,后续合并速度变快,但可能把不同物体合并到一起。min_size防止出现太小的碎块,太小的话相似度计算会不稳定。
参数建议:一般scale设在 100 到 300 之间,sigma保持 0.5 到 1.0,min_size根据图片分辨率调整,小图设 20 到 50,大图设 100 以上。
3.2 相似度度量:颜色、纹理、大小、填充度
有了初始区域之后,需要定义两个区域之间有多「像」。selective_search 原文用了四个维度:颜色相似度、纹理相似度、大小相似度、以及填充度相似度。示例里通常会把这四个加权求和,得到一个综合相似度。
def similarity(r1, r2, image, segments): """ 计算两个区域的综合相似度 :param r1: 区域1的标签 :param r2: 区域2的标签 :param image: 原图 :param segments: 分割标签图 :return: 0到1之间的相似度分数 """ # 颜色相似度:比较两个区域的颜色直方图 color_sim = color_similarity(r1, r2, image, segments) # 纹理相似度:比较梯度直方图 texture_sim = texture_similarity(r1, r2, image, segments) # 大小相似度:小区域优先合并 size_sim = size_similarity(r1, r2, segments) # 填充度相似度:合并后形状越接近矩形越好 fill_sim = fill_similarity(r1, r2, segments) # 加权求和,权重可以根据场景调 return 1.0 * color_sim + 1.0 * texture_sim + 0.5 * size_sim + 0.5 * fill_sim逻辑说明:颜色和纹理相似度是主力,权重通常给 1.0;大小和填充度是辅助,权重给 0.5 左右。颜色相似度一般用 HSV 空间的直方图交集,纹理相似度用 LBP 或者 SIFT 类似的梯度统计。大小相似度让小的区域先合并,避免大区域吞掉小区域。填充度相似度鼓励合并后形状更紧凑。
参数说明:权重不是固定的,如果你发现候选框偏向大物体,可以把大小相似度权重调低;如果框的形状很怪,把填充度权重调高。
3.3 合并策略:贪心合并与候选框生成
有了相似度之后,就是不断找最相似的两个区域合并,直到整张图变成一个区域。每合并一次,就把当前所有区域的外接矩形记录下来作为候选框。
def merge_regions(image, segments, similarity_fn): """ 贪心合并区域,生成候选框 :param image: 原图 :param segments: 初始分割标签 :param similarity_fn: 相似度函数 :return: 候选框列表,每个框是 (x, y, w, h) """ regions = {i: np.where(segments == i) for i in np.unique(segments)} candidates = [] while len(regions) > 1: # 找到最相似的一对区域 best_pair = None best_score = -1 for r1 in regions: for r2 in regions: if r1 >= r2: continue score = similarity_fn(r1, r2, image, segments) if score > best_score: best_score = score best_pair = (r1, r2) # 合并这对区域 r1, r2 = best_pair regions[r1] = np.concatenate([regions[r1], regions[r2]]) del regions[r2] # 记录当前所有区域的外接矩形 for r in regions: ys, xs = regions[r] x, y = xs.min(), ys.min() w, h = xs.max() - x + 1, ys.max() - y + 1 candidates.append((x, y, w, h)) return candidates逻辑说明:每一轮遍历所有区域对,找相似度最高的合并。合并之后把新区域的外接矩形加入候选框列表。这样跑完一轮,候选框数量会非常多,通常需要后续用 NMS 或者按面积过滤掉一部分。
参数说明:regions字典的 key 是区域标签,value 是像素坐标数组。合并时直接拼接坐标数组,简单粗暴但有效。候选框去重可以用集合或者简单的 IoU 过滤。
3.4 候选框后处理:去重与排序
原始候选框数量可能上万,直接送进后续模型不现实。常见做法是按面积排序,保留前 N 个,或者用 IoU 做非极大值抑制。
def nms_boxes(boxes, iou_thresh=0.5): """ 简单的 NMS 去重 :param boxes: 候选框列表 [(x, y, w, h), ...] :param iou_thresh: IoU 阈值,超过就抑制 :return: 去重后的框 """ boxes = sorted(boxes, key=lambda b: b[2] * b[3], reverse=True) keep = [] for box in boxes: if all(iou(box, k) < iou_thresh for k in keep): keep.append(box) return keep逻辑说明:先按面积从大到小排序,然后依次保留与已选框 IoU 低于阈值的框。这样能去掉大量重叠的冗余框。iou_thresh一般设 0.5 到 0.7,设太低会误删不同物体的框。
4. 避坑与常见问题:跑 selective_search 时容易翻车的几个点
4.1 现象:跑完一张图要几十秒甚至几分钟
原因:相似度计算是 O(n²) 的,初始区域数量如果太多,合并轮次会爆炸。常见做法是调大scale参数减少初始区域,或者对图像先做缩放。
解决:把scale从默认的 100 调到 200 以上,或者把输入图片长边缩到 500 像素以内。如果还慢,可以在相似度计算里加缓存,避免重复算同一对区域。
4.2 现象:候选框大量重叠,几乎盖满整张图
原因:合并过程中每一轮都记录所有区域的外接矩形,导致同一个物体被反复记录。另外 NMS 阈值设得太高,没起到去重作用。
解决:降低 NMS 的iou_thresh到 0.5 以下,或者按面积过滤掉过大和过小的框。我一般会保留面积在图像面积 1% 到 50% 之间的框。
4.3 现象:小目标完全找不到候选框
原因:初始分割的min_size设得太大,小目标在第一步就被合并掉了。或者图像缩放太狠,小目标像素太少。
解决:把min_size调到 10 到 20,同时不要过度缩放图像。如果还不行,可以单独对小目标区域做一次高分辨率的分割。
4.4 现象:颜色相似度计算报错,提示直方图维度不匹配
原因:不同区域的像素数量不一样,直方图 bin 数没统一。或者图像通道顺序搞混了,BGR 当成 RGB 算。
解决:固定直方图的 bin 数量,比如每个通道 25 个 bin,然后做归一化。读图之后统一转成 RGB,再送进相似度计算。
4.5 现象:合并到最后只剩一个区域,候选框却很少
原因:合并策略太激进,相似度阈值设得太低,导致区域很快合并成整张图。或者候选框记录逻辑只在合并后记录,漏掉了中间状态。
解决:在每一轮合并前都记录一次当前区域的外接矩形,而不是合并后才记录。另外可以设置一个最大合并轮次,提前停止。
5. 二次开发与改造:把示例改成你自己的区域提议模块
5.1 替换分割算法
示例默认用 Felzenszwalb,但你可以换成 SLIC 超像素或者简单的网格分割。SLIC 对纹理丰富的图像更友好,网格分割速度最快但区域形状固定。
from skimage.segmentation import slic def init_regions_slic(image, n_segments=200, compactness=10): """ 用 SLIC 超像素做初始分割 :param n_segments: 期望的超像素数量 :param compactness: 紧凑度,越大越接近方形 """ segments = slic(image, n_segments=n_segments, compactness=compactness) return segments逻辑说明:n_segments控制超像素数量,一般设 200 到 500。compactness越大,超像素形状越规则,但可能跨越物体边界。替换之后,后面的相似度计算和合并逻辑不用改,直接复用。
5.2 自定义相似度权重
如果你只关心颜色相近的区域,可以把纹理相似度权重降到 0,颜色权重提到 2.0。如果你做的是文本检测,纹理和填充度更重要,可以反过来调。
# 自定义权重配置 WEIGHTS = { 'color': 2.0, 'texture': 0.5, 'size': 0.5, 'fill': 1.0 } def similarity_custom(r1, r2, image, segments): return (WEIGHTS['color'] * color_similarity(r1, r2, image, segments) + WEIGHTS['texture'] * texture_similarity(r1, r2, image, segments) + WEIGHTS['size'] * size_similarity(r1, r2, segments) + WEIGHTS['fill'] * fill_similarity(r1, r2, segments))参数说明:权重没有标准答案,我一般先在验证集上跑几组对比,看哪组候选框的召回率高。颜色权重高适合物体颜色区分明显的场景,纹理权重高适合纹理差异大的场景。
5.3 接入下游检测模型
候选框生成之后,你可以直接把它们裁剪出来送进 CNN 分类器,或者把框的坐标喂给 R-CNN 系列模型。常见做法是把候选框按面积排序,取前 2000 个,然后统一缩放到固定尺寸。
def crop_candidates(image, boxes, target_size=(224, 224)): """ 裁剪候选框并缩放到固定尺寸 :param image: 原图 :param boxes: 候选框列表 :param target_size: 目标尺寸 :return: 裁剪后的图像块列表 """ crops = [] for (x, y, w, h) in boxes: patch = image[y:y+h, x:x+w] patch = cv2.resize(patch, target_size) crops.append(patch) return crops逻辑说明:裁剪时注意边界检查,防止坐标越界。缩放用双线性插值即可,不要用最近邻,否则纹理信息损失大。裁剪后的图像块可以直接送进分类网络。
5.4 验证改造效果的方法
改完之后怎么知道有没有变好?我一般看两个指标:候选框的召回率和平均框数量。召回率是在标注数据上算的,看有多少真实目标被至少一个候选框覆盖到,IoU 阈值设 0.5。平均框数量反映效率,太多说明冗余严重。
def recall(candidates, ground_truth, iou_thresh=0.5): """ 计算候选框对真实目标的召回率 """ hit = 0 for gt in ground_truth: if any(iou(gt, c) >= iou_thresh for c in candidates): hit += 1 return hit / len(ground_truth)参数说明:iou_thresh一般设 0.5,严格一点可以设 0.7。召回率上不去就调低相似度阈值或者增加初始区域数量;框太多就加大 NMS 力度。
5.5 一个我踩过的坑
有一次我把scale参数调得特别大,想着跑快点,结果小目标全被合并没了,召回率直接掉了一半。后来我养成一个习惯:每次改分割参数,都先在一张有大小目标的测试图上跑一遍,肉眼确认小目标还在,再跑全量数据。从那以后我每次调参都强制走一遍可视化检查,再也不敢只看数字了。
希望这个示例能帮你把 selective_search 的流程跑通,后面改起来也有个靠谱的起点。
本文还有配套的精品资源,点击获取