OpenCV C++ 图像处理教程:灰度变换与直方图分析

在数字图像处理领域,灰度变换与直方图分析是最基础且核心的技术,它们如同 "图像的化妆师",能够通过调整像素灰度分布显著改善图像视觉效果,为后续的目标检测、图像分割等高级任务奠定基础。无论是校正图像的亮度与对比度,还是从低质量图像中提取有效信息,掌握这些技术都是图像处理从业者的必备技能。

一、点运算(Point Operation)

1. 概念

点运算是图像处理中最基础的操作之一,指对图像中每个像素点的灰度值进行独立变换,输出像素仅取决于输入像素的灰度值,与像素的空间位置无关。其数学表达式为:

g ( x , y ) = T [ f ( x , y ) ] g(x,y) = T[f( x,y)] g(x,y)=T[f(x,y)]

其中, f ( x , y ) f(x,y) f(x,y) 是输入图像像素值, g ( x , y ) g(x,y) g(x,y) 是输出图像像素值, T T T 是点运算变换函数。

2. 分类

  • 灰度变换:直接对像素灰度值进行映射变换(如线性变换、伽马变换)。

  • 直方图操作:基于像素灰度分布的全局变换(如直方图均衡化、直方图规定化)。

3. 特点

  • 运算仅依赖单个像素值,不涉及邻域信息。

  • 可高效实现图像对比度增强、动态范围调整等功能。

4. 应用

  • 图像预处理(灰度化、噪声抑制)。

  • 图像增强(对比度调整、动态范围压缩)。

二、灰度变换(Gray Level Transformation)

1. 概述

灰度变换是点运算的核心分支,通过设计特定变换函数,调整图像灰度范围或分布,改善视觉效果或突出感兴趣区域。

2. 主要作用

  • 扩展或压缩图像灰度动态范围。

  • 增强图像对比度或校正非线性光照影响。

  • 将图像灰度值映射到指定区间(如 [0, 255])。

3. 常用方法

3.1 灰度化(Gray Scale Conversion)

将彩色图像转换为灰度图像,常用加权平均法

Gray = 0.299 R + 0.587 G + 0.114 B \text{Gray} = 0.299R + 0.587G + 0.114B Gray=0.299R+0.587G+0.114B

OpenCV 实现

cpp 复制代码
#include <opencv2/opencv.hpp>

cv::Mat grayScale(const cv::Mat& img) {
    cv::Mat gray;
    cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); // 内置灰度化函数
    return gray;
}
3.2 线性变换(Linear Transformation)

公式

g ( x , y ) = a ⋅ f ( x , y ) + b g(x,y) = a \cdot f(x,y) + b g(x,y)=a⋅f(x,y)+b

  • a > 1 a > 1 a>1:增强对比度; a < 1 a < 1 a<1:压缩对比度; b b b 调整整体亮度。

    代码实现

cpp 复制代码
cv::Mat linearTransform(const cv::Mat& img, double a, double b) {
    cv::Mat result = cv::Mat::zeros(img.size(), img.type());
    for (int i = 0; i < img.rows; i++) {
        for (int j = 0; j < img.cols; j++) {
            // saturate_cast确保像素值在[0,255]范围内,避免溢出
            result.at<uchar>(i,j) = cv::saturate_cast<uchar>(a * img.at<uchar>(i,j) + b);
        }
    }
    return result;
}
3.3 分段线性变换(Piecewise Linear Transformation)

将灰度区间分为多段,每段采用不同线性变换,突出感兴趣区域的对比度。

公式(三段变换)

{ a 1 f ( x ) + b 1 , f ( x ) ∈ [ 0 , c 1 ] a 2 f ( x ) + b 2 , f ( x ) ∈ ( c 1 , c 2 ] a 3 f ( x ) + b 3 , f ( x ) ∈ ( c 2 , L − 1 ] \begin{cases} a_1 f(x) + b_1, & f(x) \in [0, c_1] \\ a_2 f(x) + b_2, & f(x) \in (c_1, c_2] \\ a_3 f(x) + b_3, & f(x) \in (c_2, L-1] \end{cases} ⎩ ⎨ ⎧a1f(x)+b1,a2f(x)+b2,a3f(x)+b3,f(x)∈[0,c1]f(x)∈(c1,c2]f(x)∈(c2,L−1] 代码实现(以增强中间灰度为例):

cpp 复制代码
cv::Mat piecewiseLinear(const cv::Mat& img, int c1, int c2, double a1, double a2, double a3) {
   cv::Mat result = img.clone();
   for (int i = 0; i < img.rows; i++) {
       const uchar* ptr = img.ptr<uchar>(i);
       uchar* res_ptr = result.ptr<uchar>(i);
       for (int j = 0; j < img.cols; j++) {
           if (ptr[j] <= c1) {
               res_ptr[j] = cv::saturate_cast<uchar>(a1 * ptr[j]);
           } else if (ptr[j] <= c2) {
               res_ptr[j] = cv::saturate_cast<uchar>(a2 * (ptr[j] - c1) + a1 * c1);
           } else {
               res_ptr[j] = cv::saturate_cast<uchar>(a3 * (ptr[j] - c2) + a2 * (c2 - c1) + a1 * c1);
           }
       }
   }
   return result;
}
3.4 对数变换(Logarithmic Transformation)

公式 : g ( x , y ) = c ⋅ log ⁡ ( 1 + f ( x , y ) ) g(x,y) = c \cdot \log(1 + f(x,y)) g(x,y)=c⋅log(1+f(x,y))

作用:压缩高灰度值动态范围,增强低灰度区域对比度(如傅里叶频谱显示)。
代码实现

cpp 复制代码
cv::Mat logTransform(const cv::Mat& img, double c) {
   cv::Mat result;
   img.convertTo(result, cv::CV_64F); // 转换为双精度浮点型,避免溢出
   result = c * cv::log(1 + result); // 应用对数变换
   // 归一化到[0,255]区间,convertTo用于类型转换
   cv::normalize(result, result, 0, 255, cv::NORM_MINMAX, cv::CV_8UC1); 
   return result;
} 
3.5 反对数变换(Antilogarithmic Transformation)

公式 : g ( x , y ) = c ⋅ ( e f ( x , y ) − 1 ) g(x,y) = c \cdot (e^{f(x,y)} - 1) g(x,y)=c⋅(ef(x,y)−1)

作用:与对数变换相反,扩展高灰度区域对比度。
代码实现

cpp 复制代码
cv::Mat antilogTransform(const cv::Mat& img, double c) {
   cv::Mat result;
   img.convertTo(result, cv::CV_64F); // 转换为双精度浮点型
   cv::exp(result, result); // 计算每个像素的指数值(e^f(x,y))
   result = c * (result - 1); // 应用反对数变换公式
   // 归一化到[0,255]区间,确保输出为8位无符号整数
   cv::normalize(result, result, 0, 255, cv::NORM_MINMAX, cv::CV_8UC1); 
   return result;
} 
3.6 伽马变换(Gamma Transformation)

公式 : g ( x , y ) = c ⋅ [ f ( x , y ) ] γ g(x,y) = c \cdot [f(x,y)]^\gamma g(x,y)=c⋅[f(x,y)]γ γ > 1 \gamma > 1 γ>1:压缩高灰度,增强低灰度对比度; γ < 1 \gamma < 1 γ<1:扩展高灰度,增强高灰度对比度(常用于校正显示器非线性响应)。
代码实现

cpp 复制代码
cv::Mat gammaTransform(const cv::Mat& img, double gamma) {
 // 创建查找表(LUT),大小为1x256,数据类型为8位无符号整数
 cv::Mat lookUpTable(1, 256, cv::CV_8UC1);
 uchar* p = lookUpTable.ptr(); // 获取查找表的指针
 
 // 填充查找表:对每个可能的灰度值(0-255)计算对应的伽马变换结果
 for (int i = 0; i < 256; i++) {
     // 1. i / 255.0:将灰度值归一化到 [0,1] 范围
     // 2. std::pow(..., gamma):应用伽马变换
     // 3. ... * 255.0:将结果映射回 [0,255] 范围
     // 4. cv::saturate_cast<uchar>:确保结果在 [0,255] 内,防止溢出
     p[i] = cv::saturate_cast<uchar>(std::pow(i / 255.0, gamma) * 255.0);
 }
 
 // 应用查找表:将原始图像的每个像素值通过查找表快速替换为变换后的值
 cv::Mat result;
 cv::LUT(img, lookUpTable, result); // LUT = Look-Up Table
 
 return result;
}

4. 数据类型转换与范围处理

在图像处理中,数据类型转换和像素值范围控制是核心操作,OpenCV提供了三个重要工具:

  1. cv::convertTo()
    用于将图像转换为指定的数据类型,并可同时进行缩放:
cpp 复制代码
// 语法:src.convertTo(dst, dstType, scale=1, shift=0)
src.convertTo(dst, cv::CV_32F, 1.0/255); // [0,255] → [0,1](浮点型)    

作用 :避免计算过程中数据溢出,支持浮点运算(如对数变换、伽马变换)。

  1. cv::normalize()

用于归一化图像像素值到指定范围(如 [0,255] 或 [0,1]):

cpp 复制代码
// 语法:normalize(src, dst, alpha, beta, norm_type, dtype=src.type())
cv::normalize(img, img_norm, 0, 255, cv::NORM_MINMAX, cv::CV_8UC1); // 归一化到0-255    

作用 :将浮点运算结果映射回8位无符号整数范围,确保图像显示正常。

  1. cv::saturate_cast<>

模板函数,确保像素值在目标数据类型范围内(如 uchar 的 [0,255]),防止溢出:

cpp 复制代码
// 示例:当计算值为300时,返回255;值为-50时,返回0
result.at<uchar>(i,j) = cv::saturate_cast<uchar>(calculated_value);  
复制代码
**作用**:避免因计算误差导致的像素值越界,保证图像数据合法性。  

三、直方图(Histogram)

1. 概念

直方图是图像灰度值的概率分布统计,横轴为灰度值(0~255),纵轴为该灰度值出现的像素数或频率。 数学定义 : H ( k ) = ∑ i = 0 M − 1 ∑ j = 0 N − 1 δ ( f ( i , j ) , k ) H(k) = \sum_{i=0}^{M-1}\sum_{j=0}^{N-1} \delta(f(i,j), k) H(k)=i=0∑M−1j=0∑N−1δ(f(i,j),k)

其中, k k k 为灰度值, δ \delta δ 为指示函数(相等时为1,否则为0), M × N M \times N M×N 为图像尺寸。 作用:直观反映图像的明暗分布特征,是图像增强的重要依据。

2. 直方图获取

使用 OpenCV 的 cv::calcHist 函数,支持单通道或多通道图像统计:

cpp 复制代码
cv::Mat getHistogram(const cv::Mat& img) {
    int histSize = 256; // 灰度级数量
    float range[] = {0, 256}; // 灰度值范围(左闭右开)
    const float* histRanges = {range}; // 范围指针
    cv::Mat hist; // 输出直方图(1x256矩阵)
复制代码
// 参数说明:
// &img: 输入图像指针
// 1: 输入图像数量
// 0: 通道索引(0表示第一个通道,灰度图只有一个通道)
// cv::Mat(): 掩码(无掩码时用空矩阵)
// hist: 输出直方图
// 1: 直方图维度(灰度图为1D)
// &histSize: 各维度的灰度级数量
// &histRanges: 灰度范围
// true: 是否归一化(此处false表示统计像素数)
// false: 直方图是否均匀分桶(此处默认false)
cv::calcHist(&img, 1, 0, cv::Mat(), hist, 1, &histSize, &histRanges, true, false);
return hist;

3. 直方图均衡化(Histogram Equalization)

通过将累计分布函数(CDF)作为变换函数,使图像灰度分布均匀化,增强全局对比度。

3.1 公式推导(详细步骤) 1. 计算原始直方图

统计每个灰度级 k k k 出现的像素数量 h ( k ) h(k) h(k): h ( k ) = ∑ i = 0 M − 1 ∑ j = 0 N − 1 δ ( f ( i , j ) − k ) h(k) = \sum_{i=0}^{M-1}\sum_{j=0}^{N-1} \delta(f(i,j) - k) h(k)=i=0∑M−1j=0∑N−1δ(f(i,j)−k)

结果是一个长度为256的数组, h ( k ) h(k) h(k) 表示灰度值为 k k k 的像素总数。

  1. 计算概率密度函数(PDF, Probability Density Function)

将直方图归一化,得到每个灰度级的出现概率: p r ( k ) = h ( k ) M × N p_r(k) = \frac{h(k)}{M \times N} pr(k)=M×Nh(k)
p r ( k ) p_r(k) pr(k) 表示像素值为 k k k 的概率,取值范围为 [0, 1]。

  1. 计算累计分布函数(CDF, Cumulative Distribution Function)

累计所有小于等于当前灰度级的概率,反映灰度值的分布累积情况: C D F ( k ) = ∑ i = 0 k p r ( i ) = 1 M × N ∑ i = 0 k h ( i ) CDF(k) = \sum_{i=0}^{k} p_r(i) = \frac{1}{M \times N} \sum_{i=0}^{k} h(i) CDF(k)=i=0∑kpr(i)=M×N1i=0∑kh(i)
C D F ( k ) CDF(k) CDF(k) 表示像素值小于等于 k k k 的概率,是一个单调非递减函数,范围为 [0, 1]。

  1. 灰度映射变换

将CDF值映射到新的灰度范围 [0, L-1](L=256),公式为: s ( k ) = ⌊ ( L − 1 ) ⋅ C D F ( k ) ⌋ s(k) = \lfloor (L-1) \cdot CDF(k) \rfloor s(k)=⌊(L−1)⋅CDF(k)⌋

  • ⌊ ⋅ ⌋ \lfloor \cdot \rfloor ⌊⋅⌋ 为向下取整操作 ,确保结果为整数灰度级。

  • 单调性 :CDF的单调特性保证映射后灰度级顺序不变,避免图像反色。

  • 均匀化效果:通过扩展原始图像中频繁出现的灰度级区间,使直方图更平坦,提升对比度。

3.2 实现方法

方法一:OpenCV 内置函数(快速实现)

cpp 复制代码
cv::Mat equalizeHistogram(const cv::Mat& img) {
 cv::Mat eqImg;
 cv::equalizeHist(img, eqImg); // 仅适用于单通道灰度图像
 return eqImg;
} 

方法二:自定义实现(原理)

cpp 复制代码
cv::Mat customEqualizeHist(const cv::Mat& img) {
   // 1. 计算直方图
   cv::Mat hist = getHistogram(img);
   
   // 2. 计算累计分布函数(CDF),存储每个灰度级的累计像素数
   std::vector<float> cdf(256, 0);
   cdf[0] = hist.at<float>(0); // 初始灰度级0的累计像素数
   for (int i = 1; i < 256; i++) {
       cdf[i] = cdf[i-1] + hist.at<float>(i); // 累加后续灰度级的像素数
   }
   
   // 3. 创建灰度映射查找表(LUT, Look-Up Table)
   cv::Mat lookUpTable(1, 256, cv::CV_8UC1);
   uchar* lut = lookUpTable.ptr(); // 查找表指针
   for (int i = 0; i < 256; i++) {
       // 归一化CDF到[0,1]:累计像素数 / 总像素数
       // 乘以255映射到[0,255],saturate_cast确保值在uchar范围内
       lut[i] = cv::saturate_cast<uchar>(255.0 * cdf[i] / (img.rows * img.cols));
   }
   
   // 4. 应用查找表到原始图像(逐像素映射)
   cv::Mat result;
   cv::LUT(img, lookUpTable, result); // LUT函数实现快速映射
   return result;
}
相关推荐
我最厉害。,。7 分钟前
C2远控篇&C&C++&ShellCode分离&File提取&Http协议&Argv参数&Sock管道
c语言·c++·http
Cyrus_柯9 分钟前
C++(面向对象编程——关键字)
开发语言·c++·算法·面向对象
2013编程爱好者18 分钟前
C++二分查找
开发语言·c++·算法·二分查找
叶子椰汁19 分钟前
ORMPP链接MySQL 8.0错误
服务器·数据库·c++·mysql
AI扶我青云志23 分钟前
激活函数-sigmoid、tanh、relu、softmax对比
人工智能·深度学习·神经网络
云云32135 分钟前
封号零风险」策略:用亚矩阵云手机解锁Telegram的100%隐匿工作流
人工智能·智能手机·矩阵
蓦然回首却已人去楼空1 小时前
用mac的ollama访问模型,为什么会出现模型胡乱输出,然后过一会儿再访问,就又变成正常的
人工智能·macos
十五年专注C++开发1 小时前
QSimpleUpdater:解锁 Qt 应用自动更新的全新姿势
开发语言·c++·qt
点云SLAM1 小时前
Pytorch中gather()函数详解和实战示例
人工智能·pytorch·python·深度学习·机器学习·计算视觉·gather函数
杰_happy1 小时前
设计模式:原型模式(C++)
c++·设计模式·原型模式