基于累积分布函数的图像直方图匹配
引言
在图像处理中,直方图均衡化是一种常用的增强技术,它通过重新分布像素值来使图像的灰度级均匀分布。然而,在某些特定场景下,如需要将一幅图像的灰度分布特性迁移到另一幅图像时,直方图均衡化可能并非最佳选择。此时,直方图匹配(Histogram Matching)技术便应运而生。该方法通过将源图像的直方图转换为目标图像的直方图,实现图像的灰度特性匹配。本文将详细阐述直方图匹配的理论基础、实现步骤及代码示例。
理论基础
直方图匹配的核心思想是利用累积分布函数(Cumulative Distribution Function, CDF)进行映射。其基本原理与直方图均衡化类似,都需要计算图像的概率密度函数(Probability Density Function, PDF)和累积分布函数(CDF)。
设源图像的灰度级为 r,其CDF记为 T(r)。设目标图像的灰度级为 z,其CDF记为 G(z)。
直方图匹配的目标是找到一个变换函数 H,使得经过变换后的源图像灰度值 s 满足 s = H(r),并且变换后图像的CDF与目标图像的CDF (G(z)) 匹配。即 T(s) = G(z)。
通过令 s = T(r) 和 G(z) = s,我们可以推导出变换函数 H。具体来说,对于源图像中的每一个灰度级 r,我们计算其CDF值 T(r)。然后,在目标图像的CDF G(z) 中寻找与 T(r) 最接近的累积概率值,对应的灰度级 z 即为变换后的灰度值。这个过程可以通过查找表(Lookup Table, LUT)来实现。
实现步骤
1. 直方图统计
首先,需要计算源图像和目标图像各自的直方图。直方图统计是计算每个灰度级出现的频率。
std::vector<int> calculateHistogram(const cv::Mat& image) {
CV_Assert(image.channels() == 1); // 确保是单通道图像
std::vector<int> histogram(256, 0);
for (int row = 0; row < image.rows; ++row) {
const uchar* pixel_ptr = image.ptr<uchar>(row);
for (int col = 0; col < image.cols; ++col) {
histogram[pixel_ptr[col]]++;
}
}
return histogram;
}
2. 累积分布函数(CDF)计算
根据直方图,计算出图像的CDF。CDF表示小于或等于当前灰度级的像素所占的比例。
std::vector<float> calculateCDF(const cv::Mat& image) {
CV_Assert(image.channels() == 1);
std::vector<int> hist = calculateHistogram(image);
std::vector<float> cdf(256, 0.0f);
long long totalPixels = image.rows * image.cols;
cdf[0] = static_cast<float>(hist[0]) / totalPixels;
for (int i = 1; i < 256; ++i) {
cdf[i] = cdf[i - 1] + static_cast<float>(hist[i]) / totalPixels;
}
return cdf;
}
3. 构建直方图映射表(LUT)
利用源图像和目标图像的CDF,构建一个映射表。对于源图像的每个灰度级 i,在目标图像的CDF中查找与 srcCdf[i] 最接近的值,并记录对应的目标灰度级。
std::vector<uchar> buildLookupTable(const std::vector<float>& srcCdf, const std::vector<float>& dstCdf) {
std::vector<uchar> lut(256);
int dstIndex = 0;
for (int srcGrayLevel = 0; srcGrayLevel < 256; ++srcGrayLevel) {
// 寻找目标CDF中第一个大于或等于源CDF的值
while (dstIndex < 256 && dstCdf[dstIndex] < srcCdf[srcGrayLevel]) {
dstIndex++;
}
if (dstIndex == 0) { // 如果源CDF很小,直接映射到0
lut[srcGrayLevel] = 0;
} else if (dstIndex == 256) { // 如果源CDF很大,映射到255
lut[srcGrayLevel] = 255;
} else {
// 线性插值
float prevDstCdf = dstCdf[dstIndex - 1];
float currDstCdf = dstCdf[dstIndex];
if (std::abs(currDstCdf - prevDstCdf) < 1e-8f) { // 避免除零错误
lut[srcGrayLevel] = static_cast<uchar>(dstIndex - 1);
} else {
float interpolatedGray = static_cast<float>(dstIndex - 1) +
(srcCdf[srcGrayLevel] - prevDstCdf) / (currDstCdf - prevDstCdf);
lut[srcGrayLevel] = static_cast<uchar>(cvRound(interpolatedGray));
}
}
}
return lut;
}
4. 应用映射表进行直方图匹配
最后,使用构建好的LUT对源图像进行像素值转换,生成直方图匹配后的图像。
cv::Mat performHistogramMatching(const cv::Mat& sourceImage, const cv::Mat& targetImage) {
CV_Assert((sourceImage.channels() == 1) && (targetImage.channels() == 1));
std::vector<float> srcCdf = calculateCDF(sourceImage);
std::vector<float> dstCdf = calculateCDF(targetImage);
std::vector<uchar> lut = buildLookupTable(srcCdf, dstCdf);
cv::Mat matchedImage = sourceImage.clone();
for (int row = 0; row < sourceImage.rows; ++row) {
uchar* pixel_ptr = matchedImage.ptr<uchar>(row);
for (int col = 0; col < sourceImage.cols; ++col) {
pixel_ptr[col] = lut[pixel_ptr[col]];
}
}
return matchedImage;
}
多通道图像处理
对于彩色图像(如RGB),直接对每个通道(R, G, B)分别进行直方图匹配可能会导致图像失真,因为通道间的相关性被破坏。一种更稳妥的方法是选择一个代表亮度的通道进行匹配,例如在Lab颜色空间中对L通道进行匹配,或在HSV颜色空间中对V通道进行匹配。匹配后再将结果转换回RGB格式。
以下示例展示了如何使用Lab颜色空间的L通道进行直方图匹配:
int main() {
cv::Mat sourceRGB = cv::imread("source_image.jpg");
cv::Mat targetRGB = cv::imread("target_image.jpg");
if (sourceRGB.empty() || targetRGB.empty()) {
// 处理图像加载失败
return -1;
}
// 转换为Lab颜色空间
cv::Mat sourceLab, targetLab;
cv::cvtColor(sourceRGB, sourceLab, cv::COLOR_BGR2Lab);
cv::cvtColor(targetRGB, targetLab, cv::COLOR_BGR2Lab);
// 分离Lab通道
std::vector<cv::Mat> sourceLabChannels, targetLabChannels;
cv::split(sourceLab, sourceLabChannels);
cv::split(targetLab, targetLabChannels);
// 对L通道进行直方图匹配
cv::Mat matchedLChannel = performHistogramMatching(sourceLabChannels[0], targetLabChannels[0]);
// 替换L通道
sourceLabChannels[0] = matchedLChannel;
cv::Mat mergedLab;
cv::merge(sourceLabChannels, mergedLab);
// 转换回RGB
cv::Mat resultRGB;
cv::cvtColor(mergedLab, resultRGB, cv::COLOR_Lab2BGR);
cv::imshow("Original Source", sourceRGB);
cv::imshow("Target Image", targetRGB);
cv::imshow("Histogram Matched Result", resultRGB);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}