【数字图像处理】简单粗暴理解伽马变换(附python代码)

1 概念

伽马(Gamma)变换又称幂律变换,是数字图像处理中的一种常用技术,其作用主要是调节图像的亮度,从而增强图像的对比度。日常生活中我们使用一些图像处理软件调整图片的亮度,其实使用的就是伽马变换。

喜欢打游戏的可能对这样一个场景并不陌生,许多主机游戏第一次启动时会让用户调整显示亮度直到能隐约看见某个东西(某个文字之类的),这其实就是在应用伽马变换调节屏幕亮度,以输出更佳的画面。

其实伽马变换的原理是非常简单的,下面就以最通俗易懂的方式来介绍一下。

2 原理

2.1 数学原理

伽马变换的形式是非常简单的,就是如下公式: s = c r γ ,   r ∈ [ 0 , 1 ] s=cr^\gamma,\ r \in [0,1] s=crγ, r[0,1]其中, r r r是某个点变换前的像素值, s s s是变换后的像素值, c c c γ \gamma γ是正常数( γ \gamma γ 其实就是伽马)。 一般我们会令c等于1。 在一些情况下,考虑到偏移问题(当输入为0时的一个可度量输出), r r r这一项也可以写成 ( r + ε ) (r+\varepsilon) (r+ε),但是多数情况下我们一般对这个偏移忽略不计,所以还是使用上式给出的形式。

2.2 像素值的归一化

这里注意到 r ∈ [ 0 , 1 ] r \in [0,1] r[0,1],我们知道现实中的图像大部分都是256个灰度(如果是彩色图像,每一个通道对应256个等级)。这里很容易理解,如果直接带入过百的数值进行幂律计算,得到的结果会很大,显然无法使用。所以在这里要将 r r r进行归一化处理,将其划入[0, 1]这个区间,这样无论执行怎样的幂运算,最终的计算结果都不会跑出这个区间。

归一化如何实现呢? 其实也非常容易。假设我们现在处理的是256个灰度级的灰度图像(彩色图像同理),在执行伽马变换时只需要将原像素值除以255(因为256个灰度级对应的区间是[0, 255])后进行映射,之后再将映射得到的值乘上255,就可以恢复出我们预期的变换值了。

2.3 关于 γ \gamma γ值如何选取的讨论

上述我们已经介绍了伽马变换的基本公式,也介绍了公式中的几个比较容易理解的参数,现在还剩下 γ \gamma γ这个主角尚待解决。

其实只要懂幂函数,仅限于中学最基本的幂函数知识,这个问题也非常容易理解。首先我们已经知道,公式中的 c c c大部分情况下等于1,所以c对输出结果影响是不大的。其实就算c不是1,它也并不能对结果起到决定性作用,因为c仅仅是一个常数项(这一段有些类似时间复杂度的计算,我们会认为常数项的影响远远不如指数项)。因此我们就将关注点转向   r γ \ r^\gamma  rγ 这一项。

由于我们已经人为对r进行归一化,使r永远在区间[0, 1]内。等于1的情况可以不用考虑,我们来看小于1的情况。根据指数函数的知识,此时如果 γ > 1 \gamma>1 γ>1 ,那么算出的结果肯定比r小,反之如果 γ ∈ ( 0 , 1 ) \gamma \in (0,1) γ(0,1),算出的结果肯定比r大。 这个应该怎么在灰度值层面理解呢?假如对一幅图片进行大于1的伽马处理,那么对这张图片的每一个像素进行变换之后,得到的新像素值都会更小,对应的图片就会变得更暗,反之图片就会变得更亮。因此得出结论: γ > 1 \gamma>1 γ>1,图像会变暗; γ ∈ ( 0 , 1 ) \gamma \in (0,1) γ(0,1),图像会变亮。实际中面对不同亮度的原图像,就可以根据我们的需求动态选择 γ \gamma γ

下图就展示了在不同 γ \gamma γ值下,输入灰度级和输出灰度级之间的映射关系。可以很明显的发现上述得出的结论是正确的。
![[Pasted image 20240929150846.png]]

3 python代码实现(两种方法)

3.1 不查找表法

其实这就是最朴素的方法,即对于每个像素分别计算其映射后的像素值。叫它不查找表法其实是为了与下文中的查找表法进行对比。

# 不使用查找表gamma变换  
def gamma_not_search(src_img, gamma, c=1):  
    """    
    Gamma变换——不使用查找表法
	    :param src_img: 原图像
	    :param gamma: Gamma系数    
	    :param c: 常数项,默认为1,可缺省    
	    :return: Gamma变换后的新图像    
    """    
    # 获取原图像宽高并创建新底图**  
    height, width = src_img.shape[:2]  
    dst_img = np.zeros((height, width, 3), dtype=np.uint8)  
      
    # 遍历每个像素点,对其进行gamma变换  
    for row in range(height):  
        for col in range(width):

            normalized_pixel = src_img[row, col] / 255.0    **# 归一化处理**  
            dst_pixel = pow(c * normalized_pixel, gamma) * 255    
            dst_img[row, col] = np.clip(dst_pixel, 0, 255)  
    return dst_img

3.2 查找表法

不查找表法实际上会执行很多重复的计算。如对于相同像素较多的图片,每一次变化都得重新计算一次该像素的映射值,但显然前面的计算已经得到了结果,这样就造成了冗余计算。

查找表法其实是这样的一个思路:对于数字图像,其灰度等级是确定的,我们最常用的等级是256个等级,也就意味着无论图片有多大,每个像素的取值只可能在[0, 255]之间。而在 γ \gamma γ 相同的情况下对于每个输入像素值,其对应的输出像素值是恒定的。因此,我们只需要建立一个映射表,将256个原始像素值分别于它们的映射像素值建立映射关系,之后进行伽马变换时只需要查表替换,就不在需要执行重复的计算,而是直接查表替换即可。

# 使用查找表gamma变换

def gamma_search(src_img, gamma, c=1):  
    """   
    Gamma变换——使用查找表法    
	    :param src_img: 原图像    
	    :param gamma: Gamma系数   
	    :param c: 常数项,默认为1,可缺省   
	    :return: Gamma变换后的新图像    
    """  
    # 获取原图像宽高并创建新底图  
    height, width = src_img.shape[:2]  
    dst_img = np.zeros((height, width, 3), dtype=np.uint8)  
      
    # 创建线性查找表 
    fx = np.empty(256, dtype=np.uint8)  
    for i in range(256):  
        fx[i] = np.clip(pow(c * (i / 255.0), gamma) * 255, 0, 255)  
  
    # 遍历每个像素点,查表进行像素替换
    for row in range(height):  
        for col in range(width):  
            dst_img[row, col] = fx[src_img[row, col]]  
    return dst_img

查找表法能显著的提升效率,读者可以自行对照代码分析一下。但对于非常小的图(分辨率小于256),查找表法其实并不如不查找表法,因为也会执行多余的计算。不过现实中一般很少有这样的场景,即便有,这种小图的计算体量也不大,因此还是建议使用查找表法。

3.3 效果预览

下面展示对两张图片的伽马变换处理实例。第一幅为dark,左上为原图,右上、左下、右下分别是γ为0.3、0.5、0.75时的Gamma变换图像。
在这里插入图片描述
第二幅为light,左上为原图,右上、左下、右下分别是γ为3、4、5时的Gamma变换图像。
在这里插入图片描述

此处在附上两种实现方法的处理时间。
在这里插入图片描述
在这里插入图片描述

可以发现,查找表法确实在效率上提升非常明显。

4 拓展延伸——伽马校正

由于某些电子设备的一些特性,其显示出的图片往往相较于真实值会有一些偏差,如输出的图像会比真实值更暗。这时就可以采用伽马校正的方法。

伽马校正的过程是:在图像输入显示器之前,先对图像进行 γ = 0.4 \gamma=0.4 γ=0.4的伽马变换,即让图像变亮,此时图像经过显示器的输出,由于电子器件的特性会使图像变暗,两种效果一综合就形成了抵消,因此最后输出的图像也就更贴近真实图像。

主机游戏中调整画面亮度其实也有做伽马校正的目的。因为每一台显示器显示效果肯定都会有所差别。设计者是不能根据差别一个个调整设计方案的。所以只能提供给用户一个自己调节的方案。现在很多高端的电脑都会内嵌有电脑公司开发的调整显示的软件,其实就是在尽力消除不同设备间带来的显示差异。

LeNet是由Yann LeCun在1998年提出的经典卷积神经网络模型,被广泛应用于手写数字识别、文本识别等领域。LeNet5是LeNet的一种改进版本,主要的改进在于增加了卷积层和池化层,并使用了Sigmoid和Tanh等激活函数。 下面是LeNet和LeNet5的详细结构和代码实现。 LeNet结构: 输入层(32x32的图像) => 卷积层1(6个5x5的卷积核)=> 池化层1(2x2的最大池化)=> 卷积层2(16个5x5的卷积核)=> 池化层2(2x2的最大池化)=> 全连接层1(120个神经元)=> 全连接层2(84个神经元)=> 输出层(10个神经元) LeNet5结构: 输入层(32x32的图像)=> 卷积层1(6个5x5的卷积核)=> 池化层1(2x2的最大池化)=> 卷积层2(16个5x5的卷积核)=> 池化层2(2x2的最大池化)=> 卷积层3(120个5x5的卷积核)=> 全连接层1(84个神经元)=> 输出层(10个神经元) 完整代码实现: ```python import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5) self.pool1 = nn.MaxPool2d(kernel_size=2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.pool2 = nn.MaxPool2d(kernel_size=2) self.fc1 = nn.Linear(16*5*5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool1(x) x = F.relu(self.conv2(x)) x = self.pool2(x) x = x.view(-1, 16*5*5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x class LeNet5(nn.Module): def __init__(self): super(LeNet5, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5) self.pool1 = nn.MaxPool2d(kernel_size=2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.pool2 = nn.MaxPool2d(kernel_size=2) self.conv3 = nn.Conv2d(16, 120, kernel_size=5) self.fc1 = nn.Linear(120, 84) self.fc2 = nn.Linear(84, 10) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool1(x) x = F.relu(self.conv2(x)) x = self.pool2(x) x = F.relu(self.conv3(x)) x = x.view(-1, 120) x = F.relu(self.fc1(x)) x = self.fc2(x) return x ``` 以上就是LeNet和LeNet5的结构详解和完整代码实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值