介绍
- 计算图像直方图,分析图像的基本特征。
- 利用查找表快速修改图像的对比度或颜色,提高处理效率。
- 均衡化技术增强图像的对比度,改善视觉效果。
- 直方图反向投影检测特定内容 (适用于目标检测和跟踪)。
- 均值漂移算法寻找对象 (实时目标跟踪或寻找图像中最密集的区域)。
- 基于直方图比较检索相似图像。
- 积分图统计像素。
计算图像直方图
直方图是一个简单的表格,用于统计图像中每个灰度值的像素数量。
在灰度图像中,每个像素的值为0(黑色)到255(白色)之间的整数。
灰度图像的直方图通常有256个条目(或称为“bin”),例如,第0个bin表示值为0的像素数量,第1个bin表示值为1的像素数量,依此类推。直方图的所有条目之和等于图像的总像素数。直方图也可以归一化,使所有bin的值之和为1,此时每个bin表示该灰度值在图像中所占的百分比。
cv::calcHist
cv::calcHist 是 OpenCV 中用于计算图像直方图的函数,支持多种场景。
void calcHist(const Mat* images, // 源图像
int nimages, // 源图像的数量(通常为1)
const int* channels, // 要使用的通道列表
InputArray mask, // 输入掩码(指定要计算的像素)
OutputArray hist, // 输出的直方图
int dims, // 直方图的维度(通道数量)
const int* histSize, // 每个维度的 bin 数量
const float** ranges, // 每个维度的取值范围
bool uniform = true, // 是否均匀分布 bin(默认为 true)
bool accumulate = false) // 是否累积多次调用的结果(默认为 false)
cv::calcHist 计算灰度图像的直方图
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 读取灰度图像
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty())
{
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 定义直方图参数
int histSize = 256; // 灰度级数
float range[] = {0, 256}; // 像素值范围
const float* histRange = {range};
cv::Mat hist;
// 计算直方图
cv::calcHist(&image, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange);
// 打印直方图值
for (int i = 0; i < histSize; i++)
{
std::cout << "灰度值 " << i << " = " << hist.at<float>(i) << std::endl;
}
// 可视化直方图
int hist_w = 512, hist_h = 400;
int bin_w = cvRound((double)hist_w / histSize);
cv::Mat histImage(hist_h, hist_w, CV_8UC3, cv::Scalar(0, 0, 0));
// 归一化直方图
cv::normalize(hist, hist, 0, histImage.rows, cv::NORM_MINMAX, -1, cv::Mat());
// 绘制直方图
for (int i = 1; i < histSize; i++)
{
cv::line(histImage,
cv::Point(bin_w * (i - 1), hist_h - cvRound(hist.at<float>(i - 1))),
cv::Point(bin_w * i, hist_h - cvRound(hist.at<float>(i))),
cv::Scalar(255, 255, 255), 2);
}
// 显示结果
cv::imshow("Histogram", histImage);
cv::waitKey(0);
return 0;
}
计算灰度图像的直方图
定义一个专门用于计算灰度图像直方图的类
#include <opencv2/opencv.hpp>
#include <iostream>
class Histogram1D
{
public:
Histogram1D()
{
histSize[0] = 256; // 256个bins
hranges[0] = 0.0; // 范围从0开始
hranges[1] = 256.0; // 范围到256结束
ranges[0] = hranges;
channels[0] = 0; // 灰度图只有一个通道
}
// 计算直方图
cv::Mat getHistogram(const cv::Mat &image)
{
cv::Mat hist;
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 1, histSize, ranges);
return hist;
}
// 将直方图转换为图像
cv::Mat getHistogramImage(const cv::Mat &image, int zoom = 1)
{
cv::Mat hist = getHistogram(image); // 计算直方图
return getImageOfHistogram(hist, zoom); // 绘制直方图图像
}
// 绘制直方图图像
static cv::Mat getImageOfHistogram(const cv::Mat &hist, int zoom)
{
double maxVal = 0;
cv::minMaxLoc(hist, 0, &maxVal, 0, 0); // 获取最大值
int histSize = hist.rows;
// 创建空白图像
cv::Mat histImg(histSize * zoom, histSize * zoom, CV_8U, cv::Scalar(255));
int hpt = static_cast<int>(0.9 * histSize); // 设置最高点比例
// 绘制每个bin的垂直线
for (int h = 0; h < histSize; h++)
{
float binVal = hist.at<float>(h);
if (binVal > 0)
{
int intensity = static_cast<int>(binVal * hpt / maxVal);
cv::line(histImg,
cv::Point(h * zoom, histSize * zoom),
cv::Point(h * zoom, (histSize - intensity) * zoom),
cv::Scalar(0), zoom);
}
}
return histImg;
}
private:
int histSize[1]; // 直方图的 bin 数量
float hranges[2]; // 像素值范围
const float* ranges[1]; // 指向像素值范围的指针
int channels[1]; // 要计算的通道(灰度图为单通道)
};
int main()
{
// 读取输入图像(灰度模式)
cv::Mat image = cv::imread("princess.jpeg", cv::IMREAD_GRAYSCALE);
if (image.empty())
{
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 创建直方图对象并计算直方图
Histogram1D h;
cv::Mat histo = h.getHistogram(image);
// 输出每个灰度值对应的像素数量
for (int i = 0; i < 256; i++)
{
std::cout << "灰度值 " << i << " = " << histo.at<float>(i) << std::endl;
}
// 显示直方图
cv::namedWindow("Histogram");
cv::imshow("Histogram", h.getHistogramImage(image));
// 应用阈值分割
cv::Mat thresholded; // 输出二值图像
cv::threshold(image, thresholded, 70, 255, cv::THRESH_BINARY);
cv::namedWindow("Thresholded Image");
cv::imshow("Thresholded Image", thresholded);
// 等待用户按键
cv::waitKey(0);
return 0;
}
计算彩色图像的直方图
定义 ColorHistogram 类
class ColorHistogram
{
public:
ColorHistogram()
{
// 默认参数:每个通道 256 个 bins,范围 [0, 256)
histSize[0] = histSize[1] = histSize[2] = 256;
hranges[0] = 0.0; // 范围从 0 开始
hranges[1] = 256.0; // 范围到 256 结束
ranges[0] = hranges; // 所有通道共享相同范围
ranges[1] = hranges;
ranges[2] = hranges;
channels[0] = 0; // B 通道
channels[1] = 1; // G 通道
channels[2] = 2; // R 通道
}
// 计算彩色直方图(返回三维 cv::Mat)
cv::Mat getHistogram(const cv::Mat &image)
{
cv::Mat hist;
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 3, histSize, ranges);
return hist;
}
// 使用稀疏矩阵计算直方图(节省内存)
cv::SparseMat getSparseHistogram(const cv::Mat &image)
{
cv::SparseMat hist(3, histSize, CV_32F); // 三维稀疏矩阵
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 3, histSize, ranges);
return hist;
}
private:
int histSize[3]; // 每个维度的 bin 数量
float hranges[2]; // 像素值范围(三个通道相同)
const float* ranges[3]; // 每个通道的范围
int channels[3]; // 要计算的通道索引
};
分别绘制 R、G、B 直方图
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 读取彩色图像
cv::Mat image = cv::imread("image.jpg");
if (image.empty())
{
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 分离 BGR 通道
std::vector<cv::Mat> bgrChannels;
cv::split(image, bgrChannels);
// 定义直方图参数
int histSize = 256;
float range[] = {0, 256};
const float* histRange = {range};
// 计算每个通道的直方图
cv::Mat bHist, gHist, rHist;
cv::calcHist(&bgrChannels[0], 1, 0, cv::Mat(), bHist, 1, &histSize, &histRange);
cv::calcHist(&bgrChannels[1], 1, 0, cv::Mat(), gHist, 1, &histSize, &histRange);
cv::calcHist(&bgrChannels[2], 1, 0, cv::Mat(), rHist, 1, &histSize, &histRange);
// 归一化并绘制直方图
int histW = 512, histH = 400;
int binW = cvRound((double)histW / histSize);
cv::Mat histImage(histH, histW, CV_8UC3, cv::Scalar(0, 0, 0));
cv::normalize(bHist, bHist, 0, histImage.rows, cv::NORM_MINMAX);
cv::normalize(gHist, gHist, 0, histImage.rows, cv::NORM_MINMAX);
cv::normalize(rHist, rHist, 0, histImage.rows, cv::NORM_MINMAX);
for (int i = 1; i < histSize; i++)
{
cv::line(histImage,
cv::Point(binW * (i - 1), histH - cvRound(bHist.at<float>(i - 1))),
cv::Point(binW * i, histH - cvRound(bHist.at<float>(i))),
cv::Scalar(255, 0, 0), 2); // 蓝色通道
cv::line(histImage,
cv::Point(binW * (i - 1), histH - cvRound(gHist.at<float>(i - 1))),
cv::Point(binW * i, histH - cvRound(gHist.at<float>(i))),
cv::Scalar(0, 255, 0), 2); // 绿色通道
cv::line(histImage,
cv::Point(binW * (i - 1), histH - cvRound(rHist.at<float>(i - 1))),
cv::Point(binW * i, histH - cvRound(rHist.at<float>(i))),
cv::Scalar(0, 0, 255), 2); // 红色通道
}
// 显示结果
cv::imshow("Color Histogram", histImage);
cv::waitKey(0);
return 0;
}
应用查找表修改图像的外观
查找表 (Look-Up Table)
查找表是一个简单的映射函数,用于将原始像素值转换为新的像素值。
对于灰度图像(像素值范围为 0 到 255),查找表通常是一个长度为 256 的一维数组。
每个索引 i 对应的值表示灰度值 i 将被映射到的新灰度值。
公式:
newIntensity = lookup[oldIntensity];
将查找表应用于灰度图像
OpenCV 中的 cv::LUT 函数
// input_image: 输入图像
// lookup_table: 查找表,通常是一个 1x256 的矩阵
// output_image: 输出图像,像素值已经被查找表修改
cv::LUT(input_image, lookup_table, output_image);
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
// 定义 Histogram1D 类
class Histogram1D
{
public:
// 应用查找表的静态方法
static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup)
{
cv::Mat result; // 存储结果图像
// 使用 cv::LUT 函数应用查找表
cv::LUT(image, lookup, result);
return result;
}
};
int main()
{
// 读取输入图像(灰度图像)
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 创建一个查找表,用于反转像素值
cv::Mat lut(1, 256, CV_8U); // 256x1 矩阵,类型为 uchar
for (int i = 0; i < 256; i++) {
lut.at<uchar>(i) = 255 - i; // 反转像素值:0 -> 255, 1 -> 254, ..., 255 -> 0
}
// 使用 Histogram1D 类的 applyLookUp 方法应用查找表
cv::Mat negativeImage = Histogram1D::applyLookUp(image, lut);
// 显示原始图像和负片图像
cv::imshow("Original Image", image);
cv::imshow("Negative Image", negativeImage);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
直方图拉伸以增强图像对比度
直方图拉伸
目标
将原始图像的像素值范围 [imin, imax] 映射到 [0, 255]。
其中,imin 和 imax 是根据指定百分位数计算出的最低和最高像素值。
方法
使用百分位阈值确定最小值 imin 和最大值 imax。
对于中间的像素值 i,使用线性映射公式:
效果
原始图像中像素值低于 imin 的部分被映射为 0,高于 imax 的部分被映射为 255。
中间的像素值按比例拉伸,从而增强图像的对比度。
代码示例
#include <opencv2/opencv.hpp>
#include <iostream>
// 计算直方图
cv::Mat computeHistogram(const cv::Mat& image)
{
cv::Mat hist;
int channels[] = {0}; // 灰度图像只有一个通道
int histSize[] = {256}; // 直方图大小
float range[] = {0, 256};
const float* ranges[] = {range};
// 计算直方图
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 1, histSize, ranges);
return hist;
}
// 直方图拉伸函数
cv::Mat stretchHistogram(const cv::Mat& image, float percentile)
{
// 计算直方图
cv::Mat hist = computeHistogram(image);
// 计算百分位数对应的像素数量
float number = image.total() * percentile;
// 找到左边界 imin
int imin = 0;
for (float count = 0.0; imin < 256; ++ imin)
{
count += hist.at<float>(imin);
if (count >= number) break;
}
// 找到右边界 imax
int imax = 255;
for (float count = 0.0; imax >= 0; -- imax)
{
count += hist.at<float>(imax);
if (count >= number) break;
}
++ imax; // 因为循环结束时 imax 多减了 1
// 创建查找表
cv::Mat lookup(1, 256, CV_8U);
for (int i = 0; i < 256; ++ i)
{
if (i < imin)
{
lookup.at<uchar>(i) = 0; // 小于 imin 的映射为 0
}
else if (i > imax)
{
lookup.at<uchar>(i) = 255; // 大于 imax 的映射为 255
}
else
{
lookup.at<uchar>(i) = static_cast<uchar>(255.0 * (i - imin) / (imax - imin)); // 线性映射
}
}
// 应用查找表
cv::Mat stretchedImage;
cv::LUT(image, lookup, stretchedImage);
return stretchedImage;
}
int main()
{
// 读取输入图像(灰度图像)
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty())
{
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 设置百分位数(例如 1%)
float percentile = 0.01;
// 应用直方图拉伸
cv::Mat stretchedImage = stretchHistogram(image, percentile);
// 显示原始图像和拉伸后的图像
cv::imshow("Original Image", image);
cv::imshow("Stretched Image", stretchedImage);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
将查找表应用于彩色图像
颜色量化(Color Reduction)
一种减少图像中颜色数量的技术。
可以通过将像素值映射到一组离散的颜色值来实现。相比于逐像素循环修改颜色值,使用查找表(Look-Up Table, LUT)的方式更加高效。
目标
将图像中的每个像素的 BGR 值映射到一个更小的颜色集合中
例如,原始图像有 256 种可能的颜色值(0 到 255),通过颜色量化可以将其减少到 div 组
方法
使用查找表预先计算所有可能的颜色映射。
对于每个像素值 i,将其映射为:
这种方式可以将颜色值分组,并使每组颜色的中心值作为新的颜色值。
优势
查找表预计算所有可能的颜色映射后,只需一次操作即可完成对整个图像的修改,效率远高于逐像素循环。
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
// 颜色量化函数
void colorReduce(cv::Mat &image, int div = 64)
{
// 创建一维查找表
cv::Mat lookup(1, 256, CV_8U);
// 定义颜色量化的查找表
for (int i = 0; i < 256; i++)
{
lookup.at<uchar>(i) = static_cast<uchar>(i / div * div + div / 2);
}
// 将查找表应用于所有通道
cv::LUT(image, lookup, image);
}
int main()
{
// 读取输入图像(彩色图像)
cv::Mat image = cv::imread("image.jpg");
if (image.empty())
{
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 显示原始图像
cv::imshow("Original Image", image);
// 应用颜色量化
colorReduce(image, 64); // 将颜色减少到 256 / 64 = 4 组
// 显示颜色量化后的图像
cv::imshow("Reduced Color Image", image);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
均衡图像直方图
在某些情况下,图像的对比度问题并不是因为其灰度值范围过窄,而是因为某些灰度值出现的频率远高于其他值。
例如,中间灰度值(如 128)占主导地位,而较暗或较亮的像素值较少。这种分布会导致图像看起来缺乏细节。
直方图均衡化(Histogram Equalization)
直方图均衡化是一种全局变换,适用于整个图像的所有像素。
用于增强图像的对比度。其核心是通过重新分配像素值,使得图像中所有可能的灰度值被均匀使用,从而使图像的直方图尽可能平坦。
这种方法可以有效提高图像的视觉质量,尤其是在图像的亮度分布不均匀时。
原理
直方图均衡化的具体步骤包括:
- 计算图像的灰度直方图。
统计图像中每个灰度值的出现次数,形成直方图。 - 根据直方图计算累积分布函数(CDF)。
CDF 表示从最小灰度值到当前灰度值的累计像素数。 - 使用 CDF 将原始灰度值映射到新的灰度值。
公式为:
- total_pixels 是图像的总像素数
- max_value 和 min_value 分别是灰度值的最大值和最小值
局限性
- 直方图均衡化是一种全局变换,无法针对局部区域进行优化。
- 对于某些图像(如背景和前景对比强烈的图像),可能会导致过度增强或失真。
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 读取输入图像(灰度图像)
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty())
{
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 创建结果图像
cv::Mat result;
// 应用直方图均衡化
cv::equalizeHist(image, result);
// 显示原始图像和均衡化后的图像
cv::imshow("Original Image", image);
cv::imshow("Equalized Image", result);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
扩展应用
局部直方图均衡化
使用自适应直方图均衡化(CLAHE, Contrast Limited Adaptive Histogram Equalization)来处理局部区域。
CLAHE 可以避免全局均衡化可能导致的过度增强问题。
彩色图像处理
对于彩色图像,可以将 RGB 转换为 HSV 或 YUV 颜色空间,仅对亮度通道(V 或 Y)进行直方图均衡化。
医学图像增强
直方图均衡化常用于医学图像处理中,以增强 X 光片、CT 扫描等图像的对比度。
艺术效果
直方图均衡化还可以用于生成具有特定风格的艺术图像。
检测特定图像内容
直方图反向投影
一种基于直方图的图像处理技术,用于检测图像中与参考区域具有相似特征的内容。
核心思想是通过计算像素值的概率分布,生成一个概率图(Probability Map),从而突出目标区域。
背景
如果我们想检测图像中的某些特定内容(如云朵、纹理或物体),可以利用直方图反向投影。
解决方法
- 选择一个感兴趣的区域(Region of Interest, ROI),提取该区域的直方图。
- 将直方图归一化为概率分布。
- 对输入图像进行反向投影,将每个像素替换为其在归一化直方图中的概率值。
- 最终得到的概率图可以帮助我们定位目标区域。
局限性
- 使用灰度直方图时,可能会因为其他区域的像素值与目标区域相同而产生误检。
- 引入颜色信息或其他多维特征可以提高检测精度。
实现方案
#include <opencv2/opencv.hpp>
#include <iostream>
// 计算一维直方图
cv::Mat computeHistogram(const cv::Mat& image)
{
cv::Mat hist;
int channels[] = {0}; // 灰度图像只有一个通道
int histSize[] = {256}; // 直方图大小
float range[] = {0, 256};
const float* ranges[] = {range};
// 计算直方图
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 1, histSize, ranges);
return hist;
}
int main()
{
// 读取输入图像(彩色图像)
cv::Mat image = cv::imread("image.jpg");
if (image.empty())
{
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 转换为灰度图像
cv::Mat grayImage;
cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
// 定义感兴趣区域(ROI)
cv::Mat imageROI = grayImage(cv::Rect(216, 33, 24, 30)); // 例如:云朵区域
// 计算 ROI 的直方图
cv::Mat roiHist = computeHistogram(imageROI);
// 归一化直方图
cv::normalize(roiHist, roiHist, 1.0);
// 定义反向投影的结果图像
cv::Mat result;
// 执行直方图反向投影
int channels[] = {0}; // 使用灰度通道
float range[] = {0, 256};
const float* ranges[] = {range};
cv::calcBackProject(&grayImage, 1, channels, roiHist, result, ranges, 255.0);
// 应用阈值以突出目标区域
double thresholdValue = 50; // 阈值
cv::Mat thresholdedResult;
cv::threshold(result, thresholdedResult, thresholdValue, 255, cv::THRESH_BINARY);
// 显示结果
cv::imshow("Original Image", image);
cv::imshow("Backprojected Image", result);
cv::imshow("Thresholded Image", thresholdedResult);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
反向投影颜色直方图
多维直方图
多维直方图可以描述多个通道(如 BGR 或 HSV)的联合分布。
通过计算 ROI 的多维直方图并进行反向投影,可以生成概率图,突出目标区域
多维直方图反向投影用于检测图像中的特定内容,例如颜色区域或纹理。
颜色空间选择
BGR 颜色空间通常不是最佳选择,因为其对光照变化敏感。
使用 HSV 或 Lab* 等颜色空间可以提高检测精度。
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
// 定义 ContentFinder 类
class ContentFinder
{
public:
// 构造函数
ContentFinder() : threshold(0.1f)
{
// 假设所有通道具有相同的范围
ranges[0] = hranges;
ranges[1] = hranges;
ranges[2] = hranges;
}
// 设置参考直方图
void setHistogram(const cv::Mat& h)
{
histogram = h;
cv::normalize(histogram, histogram, 1.0); // 归一化直方图
}
// 设置阈值
void setThreshold(float t)
{
threshold = t;
}
// 简化版本的 find 方法,默认使用所有通道,范围为 [0, 256)
cv::Mat find(const cv::Mat& image)
{
cv::Mat result;
hranges[0] = 0.0; // 默认范围 [0, 256)
hranges[1] = 256.0;
channels[0] = 0; // 使用三个通道
channels[1] = 1;
channels[2] = 2;
return find(image, hranges[0], hranges[1], channels);
}
// 更通用的 find 方法
cv::Mat find(const cv::Mat& image, float minValue, float maxValue, int* channels)
{
cv::Mat result;
hranges[0] = minValue;
hranges[1] = maxValue;
// 匹配直方图维度和通道列表
for (int i = 0; i < histogram.dims; i++)
{
this->channels[i] = channels[i];
}
// 执行直方图反向投影
cv::calcBackProject(&image, 1, // 只使用一张图像
this->channels, // 使用的通道
histogram, // 输入直方图
result, // 输出结果
ranges, // 范围
255.0); // 缩放因子
// 如果阈值大于 0,则生成二值图像
if (threshold > 0.0)
{
cv::threshold(result, result, 255.0 * threshold, 255.0, cv::THRESH_BINARY);
}
return result;
}
private:
// 直方图参数
float hranges[2]; // 值范围
const float* ranges[3]; // 每个通道的范围
int channels[3]; // 使用的通道
float threshold; // 决策阈值
cv::Mat histogram; // 输入直方图
};
// 计算 3D 颜色直方图(每个通道 8 个 bin)
class ColorHistogram
{
public:
int size;
ColorHistogram() : size(256) {}
void setSize(int s) { size = s; }
cv::Mat getHistogram(const cv::Mat& image)
{
cv::Mat hist;
int channels[] = {0, 1, 2};
int histSize[] = {size, size, size};
float range[] = {0, 256};
const float* ranges[] = {range, range, range};
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 3, histSize, ranges);
return hist;
}
};
int main()
{
// 加载彩色图像
cv::Mat color = cv::imread("waves.jpg");
if (color.empty()) {
std::cerr << "无法加载图像!请检查文件路径。" << std::endl;
return -1;
}
// 定义感兴趣区域(ROI),例如蓝色天空区域
cv::Mat imageROI = color(cv::Rect(0, 0, 100, 45)); // 蓝天区域
ColorHistogram hc;
hc.setSize(8); // 每个通道 8 个 bin
cv::Mat shist = hc.getHistogram(imageROI);
// 创建 ContentFinder 实例
ContentFinder finder;
// 设置直方图和阈值
finder.setHistogram(shist);
finder.setThreshold(0.05f);
// 执行反向投影
cv::Mat result = finder.find(color);
// 显示结果
cv::imshow("Original Image", color);
cv::imshow("Backprojected Image", result);
// 等待用户按键后退出
cv::waitKey(0);
return 0;
}
使用均值漂移算法查找对象
均值漂移算法
一种基于概率分布的迭代优化算法,常用于在图像中精确定位目标。
通过直方图反向投影生成的概率图,结合均值漂移算法,可以找到目标在图像中的精确位置。
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
// 定义 ColorHistogram 类
class ColorHistogram
{
public:
int minSaturation = 0;
// 计算色相直方图
cv::Mat getHueHistogram(const cv::Mat& image, int minSat = 0)
{
cv::Mat hsv, mask, hist;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV); // 转换为 HSV 空间
// 如果需要,创建饱和度过滤掩码
if (minSat > 0)
{
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
cv::threshold(channels[1], mask, minSat, 255, cv::THRESH_BINARY);
}
// 设置直方图参数
float hranges[] = {0.0, 180.0};
const float* ranges[] = {hranges};
int channels[] = {0}; // 色相通道
int histSize[] = {180};
// 计算直方图
cv::calcHist(&hsv, 1, channels, mask, hist, 1, histSize, ranges);
return hist;
}
};
// 定义 ContentFinder 类
class ContentFinder
{
public:
cv::Mat histogram;
// 设置直方图
void setHistogram(const cv::Mat& h)
{
histogram = h;
cv::normalize(histogram, histogram, 1.0);
}
// 执行反向投影
cv::Mat find(const cv::Mat& image, float minValue, float maxValue, int* channels)
{
cv::Mat result;
float hranges[] = {minValue, maxValue};
const float* ranges[] = {hranges};
cv::calcBackProject(&image, 1, channels, histogram, result, ranges, 255.0);
return result;
}
};
int main()
{
// 加载参考图像并定义 ROI
cv::Mat image = cv::imread("baboon01.jpg");
cv::Rect rect(110, 45, 35, 45); // 猴脸 ROI
cv::Mat imageROI = image(rect);
// 计算色相直方图
ColorHistogram hc;
cv::Mat colorHist = hc.getHueHistogram(imageROI, 65);
// 创建 ContentFinder 实例并设置直方图
ContentFinder finder;
finder.setHistogram(colorHist);
// 加载目标图像并转换为 HSV 空间
cv::Mat targetImage = cv::imread("baboon3.jpg");
cv::Mat hsvTarget;
cv::cvtColor(targetImage, hsvTarget, cv::COLOR_BGR2HSV);
// 反向投影
int channels[] = {0};
cv::Mat result = finder.find(hsvTarget, 0.0f, 180.0f, channels);
// 均值漂移算法
cv::Rect window(110, 260, 35, 40); // 初始窗口位置
cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, 10, 1);
cv::meanShift(result, window, criteria);
// 显示结果
cv::rectangle(targetImage, window, cv::Scalar(0, 255, 0), 2); // 绿色矩形标记目标位置
cv::imshow("Detected Object", targetImage);
cv::waitKey(0);
return 0;
}
使用直方图比较检索相似图像
基于内容的图像检索
目标是从图像库中找到与查询图像内容相似的图像。
直方图是一种有效描述图像内容的方法,因此可以通过比较直方图来实现图像检索。
cv::compareHist
支持多种直方图比较方法,例如:
- 交集(Intersection)
- 相关性(Correlation)
- 卡方距离(Chi-Square Distance)
- 巴氏距离(Bhattacharyya Distance)
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
class ColorHistogram
{
public:
int nBins = 8; // 每个通道的直方图 bin 数量
// 设置直方图 bin 数量
void setSize(int bins)
{
nBins = bins;
}
// 计算 BGR 颜色空间的 3D 直方图
cv::Mat getHistogram(const cv::Mat& image)
{
cv::Mat hist;
int channels[] = {0, 1, 2}; // BGR 三个通道
int histSize[] = {nBins, nBins, nBins};
float bRange[] = {0, 256};
float gRange[] = {0, 256};
float rRange[] = {0, 256};
const float* ranges[] = {bRange, gRange, rRange};
cv::calcHist(&image, 1, channels, cv::Mat(), hist, 3, histSize, ranges);
cv::normalize(hist, hist, 1.0); // 归一化直方图
return hist;
}
};
class ImageComparator
{
private:
cv::Mat refH; // 查询图像的直方图
cv::Mat inputH; // 输入图像的直方图
ColorHistogram hist; // 用于生成直方图
int nBins; // 每个通道的 bin 数量
public:
ImageComparator() : nBins(8) {}
// 设置查询图像并计算其直方图
void setReferenceImage(const cv::Mat& image)
{
hist.setSize(nBins);
refH = hist.getHistogram(image);
}
// 比较查询图像与输入图像的直方图
double compare(const cv::Mat& image)
{
inputH = hist.getHistogram(image);
return cv::compareHist(refH, inputH, cv::HISTCMP_INTERSECT); // 使用交集方法
}
};
int main()
{
// 加载查询图像
cv::Mat queryImage = cv::imread("query.jpg");
if (queryImage.empty()) {
std::cerr << "无法加载查询图像!" << std::endl;
return -1;
}
// 创建 ImageComparator 实例
ImageComparator comparator;
comparator.setReferenceImage(queryImage);
// 加载图像库
std::vector<std::string> imageFiles = {"image1.jpg", "image2.jpg", "image3.jpg"};
std::vector<double> scores;
for (const auto& file : imageFiles)
{
cv::Mat image = cv::imread(file);
if (!image.empty())
{
double score = comparator.compare(image);
scores.push_back(score);
std::cout << "图像 " << file << " 的相似度得分: " << score << std::endl;
}
}
return 0;
}
用积分图像计数像素
积分图
一种高效计算图像子区域像素和的技术。
它通过预计算一个积分图,使得在任意矩形区域内求和的操作只需进行三次加减运算即可完成,而无需遍历每个像素。
定义
积分图中的每个像素值表示从图像左上角到该像素位置的所有像素值的累加和。
其中P(i,j) 是原图像的像素值。
核心思想
将图像中每个像素替换为其左上角区域内所有像素值的累加和。
可以快速计算任意矩形区域内的像素和,而无需逐个遍历像素。
公式推导
- 矩形区域的右下角点 D 的积分值 I(D) 包含了从图像左上角到 D 的所有像素累加和。
- 点 B 和 C 分别表示上方和左侧的累加值,但它们的交集部分(即左上角的 A)被重复减去了两次。
- 因此,为了纠正这一重复减去的部分,需要重新加上 A 的积分值。
最终公式为:
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取灰度图像
cv::Mat image = cv::imread("bike55.bmp", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 定义感兴趣区域 (ROI)
int xo = 97, yo = 112; // 左上角坐标
int width = 25, height = 30; // 宽高
// 方法 1:直接计算 ROI 的像素和
cv::Mat roi(image, cv::Rect(xo, yo, width, height));
cv::Scalar sumDirect = cv::sum(roi);
std::cout << "直接计算的像素和: " << sumDirect[0] << std::endl;
// 方法 2:使用积分图计算 ROI 的像素和
cv::Mat integralImage;
cv::integral(image, integralImage, CV_32S); // 计算积分图
// 快速计算像素和
int sumIntegral = integralImage.at<int>(yo + height, xo + width) -
integralImage.at<int>(yo + height, xo) -
integralImage.at<int>(yo, xo + width) +
integralImage.at<int>(yo, xo);
std::cout << "使用积分图计算的像素和: " << sumIntegral << std::endl;
return 0;
}
应用拓展
积分图在需要进行多次像素求和时非常有用。
积分图还可用于高效计算多个窗口上的直方图。
自适应阈值
据每个像素的局部邻域计算动态阈值,而不是使用全局固定的阈值。这种方法能够更好地适应光照变化。
实现案例
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 读取灰度图像
cv::Mat image = cv::imread("book.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty())
{
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 定义输出二值图像
cv::Mat binary = image.clone();
// 计算积分图
cv::Mat iimage;
cv::integral(image, iimage, CV_32S);
// 参数设置
int blockSize = 21; // 邻域大小
int thresholdValue = 10; // 局部均值偏移量
int halfSize = blockSize / 2;
int nl = image.rows;
int nc = image.cols;
// 遍历图像,逐像素计算局部均值并进行阈值处理
for (int j = halfSize; j < nl - halfSize - 1; j++)
{
uchar* data = binary.ptr<uchar>(j); // 当前行像素
int* idata1 = iimage.ptr<int>(j - halfSize); // 上边界行
int* idata2 = iimage.ptr<int>(j + halfSize + 1); // 下边界行
for (int i = halfSize; i < nc - halfSize - 1; i++)
{
// 通过积分图快速计算邻域像素和
int sum = (idata2[i + halfSize + 1] - idata2[i - halfSize] -
idata1[i + halfSize + 1] + idata1[i - halfSize]) /
(blockSize * blockSize);
// 自适应阈值判断
if (data[i] < (sum - thresholdValue))
{
data[i] = 0; // 黑色
} else {
data[i] = 255; // 白色
}
}
}
// 显示结果
cv::imshow("Adaptive Thresholding", binary);
cv::waitKey(0);
return 0;
}
使用 OpenCV 内置函数实现自适应阈值
cv::Mat binaryAdaptive;
cv::adaptiveThreshold(image, binaryAdaptive, 255,
cv::ADAPTIVE_THRESH_MEAN_C, // 使用局部均值
cv::THRESH_BINARY, // 二值化类型
blockSize, // 邻域大小
thresholdValue); // 偏移量
// 显示结果
cv::imshow("OpenCV Adaptive Thresholding", binaryAdaptive);
cv::waitKey(0);
使用直方图进行视觉跟踪
将灰度图像转换为多通道二值平面
// input:输入灰度图像。
// output:输出多通道二值图像。
// nPlanes:平面数量(必须是 2 的幂)。
void convertToBinaryPlanes(const cv::Mat& input, cv::Mat& output, int nPlanes)
{
int n = 8 - static_cast<int>(log(static_cast<double>(nPlanes)) / log(2.0));
uchar mask = 0xFF << n; // 掩码用于保留高有效位
std::vector<cv::Mat> planes;
cv::Mat reduced = input & mask; // 去除低有效位
for (int i = 0; i < nPlanes; i++)
{
// 每个平面对应一个直方图区间
planes.push_back((reduced == (i << n)) & 0x1);
}
// 合并所有平面为多通道图像
cv::merge(planes, output);
}
积分图类模板
template <typename T, int N>
class IntegralImage
{
public:
IntegralImage(cv::Mat image)
{
// 计算积分图
cv::integral(image, integralImage, cv::DataType<T>::type);
}
// 计算子区域的积分和
cv::Vec<T, N> operator()(int xo, int yo, int width, int height)
{
return (integralImage.at<cv::Vec<T, N>>(yo + height, xo + width) -
integralImage.at<cv::Vec<T, N>>(yo + height, xo) -
integralImage.at<cv::Vec<T, N>>(yo, xo + width) +
integralImage.at<cv::Vec<T, N>>(yo, xo));
}
private:
cv::Mat integralImage;
};
计算参考直方图
// 计算目标物体的参考直方图
Histogram1D h;
h.setNBins(16); // 设置直方图区间数为 16
cv::Mat refHistogram = h.getHistogram(roi); // 计算 ROI 区域的直方图
目标搜索
// 转换为多通道二值图像
cv::Mat planes;
convertToBinaryPlanes(secondImage, planes, 16);
// 计算积分图
IntegralImage<float, 16> intHistogram(planes);
double maxSimilarity = 0.0;
int xbest, ybest;
// 遍历搜索区域
for (int y = 110; y < 120; y++)
{
for (int x = 0; x < secondImage.cols - width; x++)
{
// 使用积分图计算当前窗口的直方图
auto histogram = intHistogram(x, y, width, height);
// 计算与参考直方图的相似度
double distance = cv::compareHist(refHistogram, histogram, cv::HISTCMP_INTERSECT);
// 更新最佳匹配位置
if (distance > maxSimilarity)
{
xbest = x;
ybest = y;
maxSimilarity = distance;
}
}
}
// 绘制最佳匹配位置
cv::rectangle(secondImage, cv::Rect(xbest, ybest, width, height), cv::Scalar(255));
积分图优势:
快速计算任意窗口的直方图。
支持多通道图像,适用于复杂场景。
应用场景:
在光照变化或背景复杂的环境中定位目标物体。
可扩展为多尺度搜索,适应目标大小变化。
性能优化:
减少直方图区间数(如 16 个区间),降低计算复杂度。
利用积分图高效计算窗口直方图,显著提升搜索效率。