灰度变换,又称图像的点运算,是一种仅针对单个像素进行处理的图像操作。作为最直接、最简单的图像处理技术之一,它奠定了许多高级图像处理算法的基础。其变换形式如下:

其中,T表示灰度变换函数,r为输入灰度值,s为输出像素值。公式虽简洁,但应用潜力巨大。那么,灰度变换主要有哪些用途呢?
其主要作用包括:
- 提升图像质量,增强细节表现——即对比度拉伸;
- 有选择性地突出感兴趣的特征,同时抑制不需要的信息;
- 有效调整图像的直方图分布,使像素灰度分布更加均匀。
常见的灰度变换
灰度变换函数本质上描述了输入灰度与输出灰度之间的一一映射关系。一旦函数确定,输出值便唯一确定。因此,函数的性质直接决定了变换效果。常见的三类灰度变换函数包括:
- 线性函数(图像反转)
- 对数函数:对数和反对数变换
- 幂律函数:n次幂和n次开方变换

上图展示了多种常见变换函数的曲线。从曲线形状可以直观判断每种变换的效果——例如,对数变换和幂律变换均可实现灰度级的扩展或压缩,而对数变换还具有一项重要附加能力:能够压缩动态范围极大的图像(如傅立叶变换的频谱显示)。
线性变换
设r为变换前灰度值,s为变换后灰度值,线性变换的数学表达式为:

其中a是斜率,b是y轴截距。不同的a、b值会带来完全不同的效果:
- a > 1:增强图像对比度
- a < 1:降低图像对比度
- a = 1且b ≠ 0:图像整体亮度调整,对比度保持不变
- a < 0且b = 0:图像亮度反转,亮区变暗、暗区变亮
- a = 1且b = 0:恒等变换,图像不变
- a = -1且b = 255:图像反转(负片效果)
在实际图像增强应用中,最常用的线性变换是图像反转。当取a=-1、b=255时,公式变为 s = 255 - r。反转后的图像呈现负片效果,能够有效突出暗区域中的白色或灰色细节。效果展示如下:
实现极为简便,OpenCV中直接重载了运算符——Mat r = 255 - img,或使用 ~img 也能达到同样效果。
对数变换
对数变换的通用公式如下:

其中c为常数,且r ≥ 0。从对数函数曲线可知:对数变换将源图像中较窄的低灰度值映射到较宽区间,同时将较宽的高灰度值压缩到较窄区间——简而言之,扩展暗像素、压缩亮像素,专门用于增强图像低灰度区域的细节。
反对数函数的曲线与对数函数对称,效果恰恰相反:压缩低灰度区间、扩展高灰度区间。
基于OpenCV的实现,代码长这样:
float pixels[256];
for (int i = 0; i < 256; i++)
pixels[i] = log(1 + i);
Mat imageLog(image.size(), CV_32FC3);
for (int i = 0; i < image.rows; i++)
{
for (int j = 0; j < image.cols; j++)
{
imageLog.at(i, j)[0] = pixels[image.at(i, j)[0]];
imageLog.at(i, j)[1] = pixels[image.at(i, j)[1]];
imageLog.at(i, j)[2] = pixels[image.at(i, j)[2]];
}
}
//归一化到0~255
normalize(imageLog, imageLog, 0, 255, CV_MINMAX);
//转换成8bit图像显示
convertScaleAbs(imageLog, imageLog);
本例中使用的对数底数为10。由于灰度变换是一对一映射,且灰度值范围通常为[0,255],实际应用中常采用查表法——预先计算每个灰度值的映射结果,变换时直接查表。上述代码的运行效果如下:
左图为原图,拍摄环境较暗,细节难以辨认;右图为对数变换后的结果,整体亮度显著提升,暗区域中狗狗的细节清晰可见。
对数变换的另一重要特性是能够压缩图像像素的动态范围。例如,在傅立叶变换中,频谱范围可能达到[0, 10⁶]甚至更高——显示器无法完整呈现如此宽的灰度范围,导致大量细节丢失。然而,对频谱值进行对数变换后,动态范围被压缩到合适区间,从而展示出更多细节信息。
幂律变换(伽马变换)
伽马变换的数学表达式为:

其中c和γ均为正常数。其效果与对数变换类似:当γ > 1时,将较窄的低灰度区间映射到较宽范围,同时压缩较宽的高灰度区间;当γ < 1时,效果相反,类似于反对数变换。函数曲线如下:

当γ < 1时,γ值越小,对低灰度区域的扩展效果越显著;当γ > 1时,γ值越大,对高灰度区域的扩展效果越明显。因此,伽马变换能够有效显示更多低灰度或高灰度区域的细节。
伽马变换主要应用于图像校正——对过亮或过暗的图像进行修正,提升对比度,改善显示效果。基于OpenCV的实现代码如下:
float pixels[256];
for (int i = 0; i < 256; i++)
pixels[i] = i * i * i;
Mat imageLog(image.size(), CV_32FC3);
for (int i = 0; i < image.rows; i++)
{
for (int j = 0; j < image.cols; j++)
{
imageLog.at(i, j)[0] = pixels[image.at(i, j)[0]];
imageLog.at(i, j)[1] = pixels[image.at(i, j)[1]];
imageLog.at(i, j)[2] = pixels[image.at(i, j)[2]];
}
}
//归一化到0~255
normalize(imageLog, imageLog, 0, 255, CV_MINMAX);
//转换成8bit图像显示
convertScaleAbs(imageLog, imageLog);
例如,选择c=1、γ=3以扩展高灰度区域,效果如下:
若选择c=1、γ=0.4以扩展低灰度区域,则效果截然不同:
结合伽马变换的曲线图,可以总结如下:
- γ > 1:将低于阈值K的灰度区压缩到较小区间,高于K的区域扩展到较大区间。例如,设L为最大灰度值,K=3L/4,则[0, 3L/4]映射到[0, L/8],[3L/4, L]映射到[L/8, L]。结果导致暗部更暗、亮部相对变暗,整体对比度增强。
- γ < 1:低灰度区被扩展并变亮,高灰度区被压缩但同样变亮。因此整体效果是图像整体变亮。
基于OpenCV的灰度变换实现
如前所述,灰度变换是点对点映射,采用查表法最为高效。先计算[0,255]内每个灰度值变换后的结果,再直接查表。通用实现代码如下:
/////////////////////////////////////////////////////////////////////////
// 灰度线性变换函数
// 参数:
// src,输入原图像
// dst,输出图像,类型为CV_32F,大小及通道数与原图像相同
// mapping,灰度映射表,可以根据不同的变换函数,提前计算好图像的灰度映射表
///////////////////////////////////////////////////////////////////////
void gray_trans(const Mat& src, Mat& dst, float* mapping)
{
int channels = src.channels();
if (channels == 1)
{
dst = Mat(src.size(), CV_32FC1);
for (int i = 0; i < src.rows; i++)
{
float* p1 = dst.ptr(i);
const uchar* p2 = src.ptr(i);
for (int j = 0; j < src.cols; j++)
p1[j] = mapping[p2[j]];
}
}
else if (channels == 3)
{
dst = Mat(src.size(), CV_32FC3);
for (int i = 0; i < src.rows; i++)
{
float* p1 = dst.ptr(i);
const uchar* p2 = src.ptr(i);
for (int j = 0; j < src.cols * 3; j+=3)
{
p1[j] = mapping[p2[j]];
p1[j+1] = mapping[p2[j+1]];
p1[j+2] = mapping[p2[j+2]];
}
}
}
}
调用方式非常简单,只需填充灰度映射表即可。例如,伽马变换的调用示例:
float pixels[256];
for (int i = 0; i < 256; i++)
pixels[i] = powf(i, 1.5);
Mat imageLog;
gray_trans(image, imageLog, pixels);
总结
回顾上述几种常见灰度变换的核心要点:
- 图像反转:线性变换的特殊形式,生成负片,能够有效增强暗区域中的白色或灰色细节。
- 对数变换:扩展低灰度区、压缩高灰度区,专用于增强暗部细节;同时具备压缩动态范围的能力,在傅立叶变换频谱显示中极具价值。反对数变换则作用相反。
- 伽马变换:主要用于图像校正——γ > 1可修正过亮图像,γ < 1可修正过暗图像,通过调整参数获得更佳的对比度效果。
