首页
/ OpenCV 直方图反投影详解:用 cv.calcBackProject 实现基于颜色直方图的目标定位

OpenCV 直方图反投影详解:用 cv.calcBackProject 实现基于颜色直方图的目标定位

2026-09-06 13:38:55作者:廉皓灿Ida

直方图反投影(Histogram Backprojection)是 OpenCV 直方图系列教程中的核心环节,它回答了一个非常实用的问题:给定一个目标的颜色直方图模板,如何在整幅场景图中找出所有“最像”该目标的像素。本文基于 OpenCV 官方 Python 教程 py_histogram_backprojection,完整继承原文档的 NumPy 手动实现与 cv.calcBackProject 两条技术路线,并结合 imgproc 模块头文件histogram.cpp 实现 深入讲解其参数含义与底层原理,读完即可掌握“颜色直方图 → 概率图 → 阈值分割 → 目标提取”的完整实战链路,以及它在 CamShift 颜色目标跟踪中的角色。

一、原理:直方图反投影是什么

直方图反投影由 Michael J. Swain、Dana H. Ballard 在论文 Indexing via color histograms 中提出。用通俗的话说:它用于图像分割或寻找图像中的感兴趣目标。具体来说,它会生成一张与输入图像同尺寸、单通道的“概率图”,图中每个像素的值表示该像素属于目标的可能性——目标区域会显著比背景更亮。

完整的做法可以概括为四步:

  1. 先构造一张目标(Region of Interest,ROI)尽量占满画面的图像,计算它的颜色直方图。原文档强调:颜色直方图通常优于灰度直方图,因为颜色比灰度强度更能定义一个物体。
  2. 把目标直方图“反投影”到待搜索的图像上:对每个像素,查它在目标直方图中对应的概率值。
  3. 对概率图做圆形结构元(disc)卷积,平滑孤立噪点。
  4. 对结果做合适的阈值处理,即可分离出目标区域。

imgproc.hpp 中的官方注释进一步说明了它的统计含义(源码):与 calcHist 逐像素“累加直方图 bin”不同,calcBackProject读取每个像素所在 bin 的值、乘以 scale 后写入 backProject(x,y)——即计算每个像素值相对于该直方图所表示的经验概率分布的“匹配度”。头文件注释中还给出了一个典型的颜色目标跟踪流程:

  • 跟踪前,让物体(例如一个亮色物体)占满画面,计算它的色调(hue)直方图;
  • 跟踪时,对每一帧输入用该预计算直方图做反投影,阈值化以抑制弱颜色,必要时还屏蔽饱和度不足、过暗或过亮的像素;
  • 在结果图中找连通域并选取最大连通域。

头文件注释明确指出:这正是 CamShift 颜色目标跟踪器的近似算法——所以理解反投影是理解 cv2.CamShift 的前提。

二、NumPy 手动实现:四步拆解

下面完整继承原文档的算法,以红玫瑰(rose_red.png)为模板、在整幅玫瑰图(rose.png)中检索为目标为例。

步骤 1:计算目标与搜索图的 HSV 二维直方图

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

# roi is the object or region of object we need to find
roi = cv.imread('rose_red.png')
assert roi is not None, "file could not be read, check with os.path.exists()"
hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)

# target is the image we search in
target = cv.imread('rose.png')
assert target is not None, "file could not be read, check with os.path.exists()"
hsvt = cv.cvtColor(target, cv.COLOR_BGR2HSV)

# Find the histograms using calcHist. Can be done with np.histogram2d also
M = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
I = cv.calcHist([hsvt], [0, 1], None, [180, 256], [0, 180, 0, 256])

参数说明:

  • [0, 1]:选取第 0 通道(Hue)和第 1 通道(Saturation)构成二维直方图;
  • [180, 256]:直方图尺寸,Hue 分 180 个 bin(OpenCV 的 8 位 Hue 取值范围 0~179),Saturation 分 256 个 bin;
  • [0, 180, 0, 256]:每个维度对应的取值范围,顺序与 histSize 一致。

步骤 2:计算比值 R = M / I 并反投影

先求目标直方图与搜索图直方图的逐 bin 比值 \fR = M / I\f,然后以 R 为“调色板”做反投影:对搜索图中 (x,y) 处的像素,取其 Hue 值 h(x,y) 和饱和度 S(x,y),令

\fB(x,y) = R[h(x,y), s(x,y)]\f

再施加约束 \fB(x,y) = \min[B(x,y), 1]\f(比值超过 1 的部分截断,概率图最大值记为 1)。

R = M / I
h, s, v = cv.split(hsvt)
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(hsvt.shape[:2])

这里的 R[h.ravel(), s.ravel()] 是向量的“查表”操作:hs 被展平为二维索引数组,一次性取出每个像素对应 bin 的比值,再 reshape 回原图尺寸,效果与逐像素查表完全一致,这正是 NumPy 版实现的核心技巧。

步骤 3:与圆形结构元卷积平滑

用一个 5×5 椭圆形结构元作为圆盘近似,对概率图做卷积 \fB = D \ast B\f,抑制孤立亮斑:

disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
cv.filter2D(B, -1, disc, B)
B = np.uint8(B)
cv.normalize(B, B, 0, 255, cv.NORM_MINMAX)

注意 filter2D 的第一个参数 -1 表示输出深度与输入相同;卷积后先转 uint8,再用 cv.normalize 归一化到 0~255,方便后续按灰度阈值处理。

步骤 4:定位最大强度位置或阈值化分割

概率图中最大强度的位置即目标最可能存在的位置;如果期望得到目标区域,对合适的阈值做二值化即可获得不错的分割结果:

ret, thresh = cv.threshold(B, 50, 255, 0)

至此,NumPy 手动实现流程结束。

三、OpenCV 内置实现:cv.calcBackProject

OpenCV 提供了内置函数 cv.calcBackProject(),其参数与 cv.calcHist() 高度相似,但有几个关键差异需要特别注意(结合 头文件声明):

参数 含义 注意事项
images 待反投影的源图像数组 必须同为 CV_8U / CV_16U / CV_32F 深度且尺寸相同
channels 参与计算的通道列表 通道数必须与直方图维度一致,如 [0, 1] 对应 H-S 二维直方图
hist 输入直方图(目标直方图) 必须先归一化再传入
dst 输出的单通道概率图 与输入图同尺寸、同深度
ranges 各维度 bin 边界 calcHist 用法一致,如 [0, 180, 0, 256]
scale 输出缩放因子,默认 1.0 传入 1 即可

完整可运行的代码(继承原文档示例并补充注释):

import numpy as np
import cv2 as cv

roi = cv.imread('rose_red.png')
assert roi is not None, "file could not be read, check with os.path.exists()"
hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)

target = cv.imread('rose.png')
assert target is not None, "file could not be read, check with os.path.exists()"
hsvt = cv.cvtColor(target, cv.COLOR_BGR2HSV)

# calculating object histogram
roihist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

# normalize histogram and apply backprojection
cv.normalize(roihist, roihist, 0, 255, cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt], [0, 1], roihist, [0, 180, 0, 256], 1)

# Now convolute with circular disc
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
cv.filter2D(dst, -1, disc, dst)

# threshold and binary AND
ret, thresh = cv.threshold(dst, 50, 255, 0)
thresh = cv.merge((thresh, thresh, thresh))
res = cv.bitwise_and(target, thresh)

res = np.vstack((target, thresh, res))
cv.imwrite('res.jpg', res)

这段代码的工作流是:calcBackProject 输出概率图 → filter2D 圆盘卷积平滑 → threshold 二值化 → 复制为三通道后与原图做 bitwise_and 掩膜,得到仅保留目标区域的图像 → 竖向拼接后保存为 res.jpg。其中 cv.normalize(roihist, roihist, 0, 255, cv.NORM_MINMAX) 一步不可省略:原文档特别强调“目标直方图必须先归一化再传入反投影函数”,否则 bin 值直接作为概率图幅值,会导致阈值选择完全失真。

从源码结构看实现细节

histogram.cpp 中,cv::calcBackProject 主入口按输入深度分派到 calcBackProj_8ucalcBackProj_<ushort,ushort>calcBackProj_<float,float> 三条模板路径,这解释了头文件中“源数组必须为 CV_8U、CV_16U 或 CV_32F 之一”的断言。此外,源码中还存在 ocl_calcBackProjecthistogram.cpp)OpenCL 加速分支,说明在启用 OpenCL 的构建中该函数会自动走 GPU 路径——对逐帧做反投影的视频跟踪场景意义不小。

反投影的正确性在测试中也有覆盖:test_histograms.cpp 包含针对 calcBackProject 的单元测试,而 Python 侧的 test_camshift.pycalcBackProject 作为 CamShift 的前置步骤被实际使用,可以印证头文件注释中“CamShift 的近似算法”这一定位。

四、实战示例:提取足球场草坪

原文档给出了一个更具代表性的例子:使用一张球员射门图,取蓝色矩形框内的区域(一小块草地)作为样本目标,目标是提取出整片草地。处理结果如下图所示:上方为原图,中间是反投影后经阈值化的掩膜,下方是掩膜与原图按位与后的提取结果——绿色草坪被完整分离出来,球员与背景则被抑制为黑色。

直方图反投影提取草地结果:上为原图,中为阈值化掩膜,下为按位与后的提取结果

五、使用要点与延伸阅读

  • 颜色空间选择:HSV 的 H-S 平面是默认选择,把“颜色”与“亮度”解耦,抗光照变化能力强于直接在 BGR/灰度上统计;
  • ROI 要尽量占满画面:模板直方图的代表性直接决定反投影质量,背景占比越大,误检越多;
  • 归一化不可省:传入 calcBackProject 的直方图必须先 cv.normalize
  • 后处理二件套:圆盘卷积 + 阈值化是标准流程;若用于跟踪,还可接连通域分析选最大连通域,再升级为 CamShift;
  • 原文档补充资料:"Indexing via color histograms", Swain, Michael J., Third international conference on computer vision, 1990,该论文是直方图反投影方法的原始出处;
  • 相关代码与文档索引:函数声明见 imgproc.hpp,实现见 histogram.cpp,系列教程首页见 py_histograms
登录后查看全文
热门项目推荐
相关项目推荐