ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

选择性搜索算法实战:区域提议与候选框生成全解析

选择性搜索算法实战:区域提议与候选框生成全解析 简介一份面向Python开发者与计算机视觉初学者的选择性搜索实现示例聚焦如何通过超像素分割、区域生长与特征排序生成目标候选区域为后续接入RCNN、Fast RCNN等检测模型打下基础。压缩包共19个文件包含3个Python脚本分别对应搜索、过滤、提取步骤、14张示例图像12张png与2张jpg、1份markdown说明文档及license文件整体仅53KB结构精简且目录层级清晰适合快速上手。已有283人学习代码中附有可视化中间结果可直观看到超像素合并与候选框生成过程脚本还演示了图像预处理、SLIC超像素分割及颜色直方图等特征提取方法。通过阅读搜索、过滤、提取三个脚本能学习如何用PIL和numpy实现完整流程并进一步理解候选区域输入到检测模型进行集成与推理的思路是入门目标检测区域提议的实用示例。1. 选择性搜索到底是什么区域提议算法为什么值得自己动手写一遍做目标检测的人迟早会遇到这个词selective_search选择性搜索。它的历史地位很明确——在 Faster R-CNN 把区域提议网络RPN做进模型之前R-CNN 系列全靠它来生成候选区域先找出图上“可能含有目标”的几千个框再逐个交给分类器确认。今天虽然端到端检测器是主流但选择性搜索依然是理解检测流程、做无类别目标定位、或者快速验证一个分类器能不能用于检测任务时最顺手的起点工具。这篇笔记想解决的不只是“跑通一个示例”而是帮你在本机把候选框真正生成出来、画到图上、接到后续检测管线里顺带告诉你参数该怎么调、坐标系为什么总翻车、同一张图为什么不同库跑出来的框不一样。适合正在入门检测算法的人也适合需要区域提议但不想依赖重型检测框架的工程同学。一句话它解决的是“目标可能在哪儿”的问题接下来才轮到“这是什么”。2. 算法核心拆解相似度度量与层次分组的选择逻辑2.1 从分割到分组Felzenszwalb初始区域的生成逻辑选择性搜索的第一步不是滑动窗口而是先用基于图的图像分割算法生成一批初始小区域。这一步常用的就是 Felzenszwalb 的分割方法它把像素看作图上的节点相邻像素之间的颜色差异作为边的权重然后按最小生成树的思路合并相似区域。合并的关键是一个判断准则两个区域是否能合并取决于它们内部差异和边界差异的相对大小。这一设计让初始分割对噪声有一定容忍度。参数 scale 会直接影响合并的宽松程度——scale 越大越倾向于合并成大块区域scale 越小碎片越多。sigma 则是分割前高斯平滑的标准差平滑越多小纹理越容易被抹掉。一般我用 scale 在 300 到 600 之间起步sigma 取 0.8 左右min_size 设 10 到 20过滤掉像素数极少的碎片。# 用 skimage 感受一下 Felzenszwalb 分割的粒度变化 import cv2 import numpy as np from skimage.segmentation import felzenszwalb img cv2.imread(sample.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) segments felzenszwalb( img_rgb, scale500, sigma0.8, min_size20 ) print(分割出的独立区域数量:, segments.max() 1)分割后的每个整数标签就是一个初始区域选择性搜索之后做的所有层次分组都是在这批小区域上进行的。注意 felzenszwalb 返回的标签并不保证满足物体边界它更偏“超像素式”的颜色一致块所以后面才需要用相似度度量反复合并。2.2 贪心层次分组相似度函数怎么把碎片合成语义块拿到初始区域之后选择性搜索进入核心阶段贪心层次分组。它不断计算相邻两个区域的相似度把最相似的一对合并更新邻接关系重复直到整张图只剩一个区域。这个过程会把碎小的块逐步聚成能对应“一只猫”“一辆车”的中等尺度块也会保留从单个超像素到全图的各种尺度最终形成候选区域的金字塔。相似度由四个分量加权求和得到。颜色相似度比较两个区域在 Lab 空间下的颜色直方图每通道 25 个 bin用直方图交集衡量重合程度纹理相似度用局部二值模式或 SIFT 风格的高斯导数响应直方图做同样比较。尺寸相似度鼓励小区域先合并两个区域的面积之和越小得分越高这避免某个大块吞掉周围所有小块。填充相似度衡量合并后外接框能多大程度包住内部空洞奖励那些形状贴合目标的合并。# 伪代码层次分组主循环理解结构比背库函数重要 def hierarchical_grouping(regions): adjacency build_adjacency(regions) while len(adjacency) 1: # 找到相似度最高的相邻区域对 i, j, max_score find_most_similar(regions, adjacency) if max_score 0: break # 合并并更新特征与邻接表 merged merge_features(regions[i], regions[j]) adjacency rebuild_adjacency(adjacency, i, j, merged) regions replace(regions, i, j, merged) return regions这里的 merge_features 需要同步更新颜色直方图、纹理直方图、尺寸和外接框信息。实际工程中很少有人手写这层但理解主循环后你才能明白为什么提示图一旦出现一条被切断的车灯候选框质量差时问题多半出在相似度权重或初始分割上。2.3 为什么选选择性搜索而非滑动窗口或暴力网格滑动窗口在图像金字塔上暴力滑理论上穷尽但候选框数量爆炸、速度慢、且大量窗口落在背景上。选择性搜索的意义是把“可能的位置”先筛到一两千个质量还明显好于随机采样。相比后来 Faster R-CNN 的 RPN它不依赖训练好的网络也就意味着你可以在没有任何检测模型的情况下只靠图像自身结构生成候选框。这在冷启动、数据探索、或者给弱监督任务找初始框时非常实用。它的主要代价是速度CPU 上单张图约 0.5 到 2 秒视参数与图片尺寸而定。好在多数场景下候选框只离线生成一次存下来后续复用。另一个代价是候选框质量上限受初始分割制约小到像远处一只鸟那么小的目标如果初始分割直接把它并入背景后面层次分组基本捞不回来。这点在后文参数调优里我会详细说怎么规避。3. 跑通第一个示例环境准备、最小代码与结果可视化3.1 环境准备与实现选择两种常用实现库的取舍目前最容易上手的实现有两个Python 的 selectivesearch 库以及 OpenCV contrib 模块里的 ximgproc 选择性搜索接口。selectivesearch 完全基于 skimage 和 numpy纯 Python 实现代码短、可读性好、参数直观适合学习与定制缺点是慢。OpenCV 版本底层用 C速度快不少适合批量处理但 API 较绕且 OpenCV 版本之间行为不完全一致。我的建议是学习阶段用 selectivesearch跑通逻辑、看懂参数进入批量生产再换成 OpenCV。两个版本都要求图像通道顺序是 RGB 还是 BGR要特别小心。selectivesearch 按 skimage 惯例吃 RGB你把 OpenCV 读出来的 BGR 图直接丢进去候选框方向性差异可能不大但颜色相似度分量的统计就会对不上。# 最小安装清单 pip install selectivesearch scikit-image opencv-python# 验证环境 import selectivesearch import skimage import cv2 print(selectivesearch OK:, selectivesearch.__version__ if hasattr(selectivesearch, __version__) else unknown) print(skimage OK:, skimage.__version__) print(OpenCV OK:, cv2.__version__)3.2 最小示例代码用 selectivesearch 库跑出候选框下面这段是完整可跑的示例读图、转 RGB、执行选择性搜索、把结果整理成 Nx4 数组。注意 selectivesearch 返回的 regions每个元素是一个字典包含 rect、labels 和 size。rect 的格式是 [x, y, w, h]x 和 y 是左上角坐标这个格式非常容易在后续忘掉。import cv2 from selectivesearch import selective_search # 1. 读图并转 RGB img cv2.imread(sample.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 2. 执行选择性搜索 _, regions selective_search( img_rgb, scale500, sigma0.9, min_size20 ) # 3. 转化为 [x1, y1, x2, y2] 格式候选框 candidates [] for r in regions: x, y, w, h r[rect] if w * h 500: # 过滤过小区域避免全是噪声 continue candidates.append([x, y, x w, y h]) print(f候选框数量: {len(candidates)}) print(前 5 个候选框:, candidates[:5])scale 控制初始分割的粗糙度值越大初始区域越少最终候选框也越少适合图中目标大而少的情况。sigma 影响分割前的平滑程度0.9 是常用起步值。min_size 会过滤掉分割时的小块这里设 20。第三个返回值 regions 里还带 label 信息如果后面想做区域特征提取可以按 label 取掩膜但大多数检测任务只用到 rect。3.3 可视化候选框把区域画在原图上确认效果光有数值没法判断质量画框是确认效果的第一步。把前 100 个候选框画在原图上你立刻能看出框的尺度分布是否合理。好的效果是多个不同尺度框围绕目标分布背景区域也有所覆盖但不会占满全图。# 画前 100 个候选框直观确认覆盖效果 vis img.copy() for x1, y1, x2, y2 in candidates[:150]: cv2.rectangle(vis, (x1, y1), (x2, y2), (0, 255, 0), 1) cv2.imwrite(candidates_top150.jpg, vis) # 再画一张全量候选框图感受数量规模 vis_all img.copy() for x1, y1, x2, y2 in candidates: cv2.rectangle(vis_all, (x1, y1), (x2, y2), (255, 0, 0), 1) cv2.imwrite(candidates_all.jpg, vis_all)如果画出来的框几乎全是整张图的外接框加几个随机碎片多半是初始分割没拆开需要调大 min_size 或者降低 scale。如果大量极端瘦长条框出现说明纹理相似度权重过强或者平滑不足。可视化不是可选项它是你后续调参数的“眼睛”每次改参都要存一张对比图。4. 四个必调参数从候选框数量到质量的工程取舍4.1 scale 与 sigma先调整哪一档影响最大多数人第一次跑完示例看到几千个候选框第一反应是怀疑模型写错了。其实候选框数量正常区间就是 1000 到 3000关键看你要喂给后续的模型还是仅做定位探索。scale 是影响最直接的一档它不直接限制候选框数量而是决定初始分割的尺度进而影响整棵合并树的结构。我常用的调参顺序是先固定 sigma0.8、min_size20只动 scale。用 300、500、800 各生成一遍候选框打印数量并可视化观察目标上的覆盖。scale 过低时初始碎片太多候选框数量暴涨但大量是小碎块scale 过高时初始区域已经贴近整个目标合并树深度不够候选框数量骤降但误检率也降低。sigma 的作用相对温和主要影响边缘粘连问题对线条状物体更明显一般不需要频繁动它。# 对同一张图测试多个 scale for scale in 300 500 800; do python selectivesearch_demo.py --scale $scale --out scale_${scale}.jpg done不同 scale 生成的结果甚至可以合并起来把两次或三次不同 scale 的候选框做并集能提升多尺度目标的召回。这是选择性搜索的“多策略”思想官方论文里也提到组合不同颜色空间和参数能得到更多样化的候选框但代价是候选框数量线性增长后续 NMS 压力更大。4.2 min_size 与 min_region_size候选框数量控制的关键min_size 在分割阶段起作用过滤掉初始分割中像素少于该值的区域直接决定了碎片总量。min_region_size 是 selectivesearch 库的特有参数它过滤合并结果中过小的最终区域这个值设得太大会丢掉真正的小目标。经验上对常规 800x600 图片min_size 取 20 到 50min_region_size 取 500 到 2000 都算合理区间。这两个参数影响最大的是边界把 min_size 设成 200边缘的小目标、远处行人、小动物都会因为初始分割阶段就被并掉而无法出现在任何候选框里。反过来设得太小会让颜色直方图稀疏相似度计算不稳定候选框大量重叠。我的习惯是先看目标面积占图面积比例目标约占 5% 到 10% 时min_size 取 20 左右目标较大时再提高。参数作用阶段典型范围调大后的影响scale初始分割300-800候选框变少大目标更完整sigma分割前平滑0.4-1.0弱化纹理边缘更钝min_size初始分割过滤10-50碎片减少小目标丢失min_region_size合并结果过滤500-2500最终候选框总数下降4.3 候选框数量压缩NMS阈值与坐标归一化的配合几千个候选框直接喂给分类器每张图跑一次前向几百毫秒数据集大一点就等不起。压缩数量主要有两步先做 NMS 去掉高度重叠的框再做得分或尺寸筛选保留 top-N。NMS 的 IoU 阈值一般取 0.5 到 0.7阈值越低框越少但可能漏目标阈值越高保留的框越接近原始分布。import numpy as np def nms(rects, thresh0.5): rects np.asarray(rects, dtypenp.float32) if len(rects) 0: return [] x1, y1, x2, y2 rects[:, 0], rects[:, 1], rects[:, 2], rects[:, 3] areas (x2 - x1 1) * (y2 - y1 1) order areas.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1 1) * np.maximum(0.0, yy2 - yy1 1) iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou thresh)[0] order order[inds 1] return rects[keep]执行完 NMS 后再按面积排序取前 1000 到 1500 个框基本能满足 R-CNN 风格训练的数据需求。坐标归一化这一步容易忽略如果 GT 框的坐标已经除以了图像宽高候选框还是像素坐标后面算 IoU 就会彻底乱掉。统一办法是候选框也除以原图宽高存成 0 到 1 的相对坐标逻辑上更安全。5. 避坑指南选择性搜索实战中的常见锅与排查办法5.1 坐标格式翻车bbox 到底是左上角宽高还是中心点现象用候选框去算 IoU结果数值小得离谱或者画框时框整体错位到左下角。 原因selectivesearch 返回的 rect 是 [left, top, width, height]而 OpenCV 的 rectangle 需要 [x1, y1, x2, y2]很多人直接拿 w、h 当 x2、y2 用框自然歪了。更隐蔽的是有些教程把 rect 转成了中心点格式混用时全部错位。 解决写一个统一的正反向转换函数入口和出口都走同一个在代码里加上断言确保 x2 x1 且 y2 y1数值异常时立刻打印排查。def xywh_to_xyxy(rect): x, y, w, h rect return [x, y, x w, y h]5.2 候选框质量差目标只出一半框怎么办现象可视化时发现目标被一个框框住了上半身下半身被另一个更大框包含或者目标整体没被任何框完整覆盖。 原因最常见的是 scale 太大初始分割已经把目标分成两块合并阶段又没有某个相邻对能让它们合在一起另一种是目标和背景颜色接近颜色相似度误判导致过早与背景合并。 解决先降低 scale 到 300 试试观察是否出现更完整的包围盒再把 sigma 调到 0.6 到 0.8 减少边缘干扰若仍是老样子把图片长边缩到 500 左右再跑很多分割问题在大尺寸图上反而更明显。5.3 内存与速度告急大批量图片如何避免卡死现象几百张图跑下来越来越慢内存从几百 MB 涨到几 GB甚至直接 OOM。 原因selectivesearch 把每张图的所有中间区域全部保留返回数据集一大Python 对象开销叠加内存就爆了。另一个隐性原因是没有及时释放过大的原始图循环里不断读图叠加内存引用。 解决用 OpenCV 版替代纯 Python 版速度提升明显图片先缩放到长边 1000 以内候选框生成后立即转成紧凑的 numpy 数组并丢弃 regions每个进程处理完一批就 gc.collect()。import gc def process_batch(image_paths): result [] for path in image_paths: img cv2.imread(path) img cv2.resize(img, (800, int(800 * img.shape[0] / img.shape[1]))) # 执行 selective search 并提取候选框 result.append(np.asarray(candidates, dtypenp.float32)) gc.collect() return result5.4 OpenCV版与selectivesearch库结果对不上现象同一张图OpenCV 跑出来 800 个候选框selectivesearch 跑出来 2600 个目标覆盖情况也不一致以为是哪个库装错了。 原因两个实现的相似度度量细节不同OpenCV 的 ximgproc 模块使用了不同的特征初化与合并策略行为并不保证完全一致。 解决选一个版本固定下来训练和推理全程用同一个。对结果做对比时只比较“数量级”和“是否覆盖目标”这两个层面不要逐框对齐。如果一定要在论文或报告中保持一致性建议在代码里封装统一接口换库时只改一行。5.5 IoU匹配奇怪正负样本数量异常现象用候选框和 GT 做正负样本分配时正样本数几乎为 0或者负样本全是背景垃圾框。 原因坐标系统不统一候选框像素坐标对 GT 归一化坐标、IoU 阈值设得太高、候选框数量不够导致没有框与 GT 达到阈值。 解决先确认两边坐标都在同一体系中正样本 IoU 阈值建议从 0.5 起步如果候选框覆盖不到 GT优先检查 scale 和 min_size而不是调阈值掩盖问题。# 快速验证一条 GT 是否被候选框覆盖 gt np.array([120, 80, 320, 280], dtypenp.float32) for cand in candidates: iou compute_iou(gt, cand) if iou 0.5: print(找到覆盖框:, cand, IoU:, iou) break else: print(该 GT 没有被任何候选框覆盖请调整 scale)6. 进阶一步把候选框接到目标检测管线里的验证技巧6.1 用召回率曲线验证候选框质量候选框数量只是表面指标真正该看的是“目标有没有被框到”。正确做法是取二十张带 GT 的验证图对每张图生成候选框计算每个 GT 与所有候选框的最大 IoU统计达到 0.5 和 0.7 的 GT 比例。这个数字就是区域提议召回率它能直观反映参数调优的价值。def proposal_recall(gts, candidates, iou_thresh0.5): recalled 0 for gt in gts: ious [compute_iou(gt, cand) for cand in candidates] if max(ious) iou_thresh: recalled 1 return recalled / len(gts)我一般会分别打印候选框数量与召回率画一张表格scale 300、500、800 各对应多少框、多少召回。召回率到 0.9 以上时候选框数量通常在 2000 附近再多就对检测精度帮助不大了。6.2 把候选框接到分类器做一个最简 R-CNN 验证如果只到“生成框”为止你没法确认这些框是否真的有用。最常见的验证方式是把候选框剪裁出来缩放成 224x224送进一个预训练分类器比如 ResNet取置信度最高的类别作为框标签再 NMS 一下你就得到了一个不训练的简易检测器。这个验证能快速暴露两个问题候选框是否对目标提供了足够的上下文以及分类器对小目标的敏感度。from torchvision import models, transforms from PIL import Image model models.resnet18(pretrainedTrue) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) for x1, y1, x2, y2 in candidates[:50]: crop Image.open(sample.jpg).crop((x1, y1, x2, y2)) tensor transform(crop).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(tensor), dim1) print(f框 {x1},{y1},{x2},{y2}: {probs.max().item():.2f})这种验证不用训练任何检测头半小时就能跑完是检验区域提议质量性价比最高的一步。我自己的习惯是永远先把候选框可视化打印到本地再检查召回率最后才引入分类器——因为候选框阶段出的错误到了分类阶段会被放大成“假正例”到时候查错成本高得多。最后一个血泪经验不要相信“调好一组参数就能跑全部数据”这种话。换数据集、换分辨率、甚至换摄像头角度候选框分布都会变。每次换数据先跑一遍召回率和可视化确认没问题再往下走。这样做虽然慢几步但能省下大量排错时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表