在数字图像处理领域,灰度变换与直方图分析是最基础且核心的技术,它们如同 “图像的化妆师”,能够通过调整像素灰度分布显著改善图像视觉效果,为后续的目标检测、图像分割等高级任务奠定基础。无论是校正图像的亮度与对比度,还是从低质量图像中提取有效信息,掌握这些技术都是图像处理从业者的必备技能。
一、点运算(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 实现:
#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 调整整体亮度。
代码实现:
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] 代码实现(以增强中间灰度为例):
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))
作用:压缩高灰度值动态范围,增强低灰度区域对比度(如傅里叶频谱显示)。
代码实现:
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)
作用:与对数变换相反,扩展高灰度区域对比度。
代码实现:
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:扩展高灰度,增强高灰度对比度(常用于校正显示器非线性响应)。
代码实现:
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提供了三个重要工具:
cv::convertTo()
用于将图像转换为指定的数据类型,并可同时进行缩放:
// 语法:src.convertTo(dst, dstType, scale=1, shift=0)
src.convertTo(dst, cv::CV_32F, 1.0/255); // [0,255] → [0,1](浮点型)
作用:避免计算过程中数据溢出,支持浮点运算(如对数变换、伽马变换)。
2. cv::normalize()
用于归一化图像像素值到指定范围(如 [0,255] 或 [0,1]):
// 语法: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位无符号整数范围,确保图像显示正常。
3. cv::saturate_cast<>
模板函数,确保像素值在目标数据类型范围内(如 uchar
的 [0,255]),防止溢出:
// 示例:当计算值为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
函数,支持单通道或多通道图像统计:
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 的像素总数。
2. 计算概率密度函数(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]。
3. 计算累计分布函数(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]。
4. 灰度映射变换
将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 内置函数(快速实现)
cv::Mat equalizeHistogram(const cv::Mat& img) {
cv::Mat eqImg;
cv::equalizeHist(img, eqImg); // 仅适用于单通道灰度图像
return eqImg;
}
方法二:自定义实现(原理)
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;
}