【低照度图像增强系列(2)】Retinex(SSR/MSR/MSRCR)算法详解与代码实现

小明 2025-05-07 23:09:40 7

前言 

��☀️ 在低照度场景下进行目标检测任务,常存在图像RGB特征信息少、提取特征困难、目标识别和定位精度低等问题,给检测带来一定的难度。

     🌻使用图像增强模块对原始图像进行画质提升,恢复各类图像信息,再使用目标检测网络对增强图像进行特定目标检测,有效提高检测的精确度。

      ⭐本专栏会介绍传统方法、Retinex、EnlightenGAN、SCI、Zero-DCE、IceNet、RRDNet、URetinex-Net等低照度图像增强算法。

👑完整代码已打包上传至资源→低照度图像增强代码汇总资源-CSDN文库

目录

前言 

🚀一、Retinex简介

🚀二、Retinex原理

🚀三、基于Retinex理论的增强算法

🎄3.1 SSR(Single Scale Retinex)单尺度Retinex算法

简介 

原理

代码实现

🎄3.2 多尺度MSR(Multi-Scale Retinex)多尺度Retinex算法

 简介 

原理

代码实现

🎄3.3 MSRCR(Multi-Scale Retinex with Color Restoration)具有色彩恢复的多尺度Retinex算法

 简介  

原理

代码实现 

🚀总结

🚀整体代码


🚀一、Retinex简介

Retinex是Edwin.H.Land于1963年提出的算法。该算法认为人眼可以感知近似一致的色彩信息,这种性质称为色彩恒定性。这种恒定性是视网膜(Retina)与大脑皮层(Cortex)所共同作用的结果。常见的算法包括:单尺度Retinex(SSR)算法、多尺度Retinex(MSR)算法和色彩恢复多尺度Retinex(MSRCR)算法,MSRCR算法是对SSR算法及MSR算法进行修正的一种算法。

物体的颜色是由物体对长波(红色)、中波(绿色)、短波(蓝色)光线的反射能力来决定的,而不是由反射光强度的绝对值来决定的,物体的色彩不受光照非均匀性的影响,具有一致性,即Retinex是以色感一致性(颜色恒常性)为基础的。

不同于传统的线性、非线性的只能增强图像某一类特征的方法,Retinex可以在动态范围压缩、边缘增强和颜色恒常三个方面达到平衡,因此可以对各种不同类型的图像进行自适应的增强。

发展历程: 


🚀二、Retinex原理

  • 基本思想:去除照射光影响,保留物体自身的反射属性。
  • 核心思想:保留图像细节信息的前提下,调整图像的对比度和亮度。

    Retinex算法认为图像I(x, y)是由照度图像与反射图像组成。

    • 前者指的是物体的入射分量的信息,用L(x, y) 表示;
    • 后者指的是物体的反射部分,用R(x, y) 表示。

      公式:

      I(x, y) =R(x, y) * L(x, y)

      式中: I(x,y)代表被观察或照相机接收到的图像信号;L(x,y)代表环境光的照射分量 ;R(x,y)表示携带图像细节信息的目标物体的反射分量 。 

      同时,由于对数形式与人类在感受亮度的过程属性最相近,因此将上述过程转换到对数域进行处理,这样做也将复杂的乘法转换为加法: i(x, y) = r(x, y) + l(x, y)


      🚀三、基于Retinex理论的增强算法

      🎄3.1 SSR(Single Scale Retinex)单尺度Retinex算法

      简介 

      SSR (Singal Scale Retinex),即单尺度视网膜算法是 Retinex 算法中最基础的一个算法。运用的就是上面的方法,具体步骤如下:

      1. 输入原始图像 I(x,y) 和滤波的半径范围 sigma;
      2. 计算原始图像 I(x,y) 高斯滤波后的结果,得到 L(x,y);
      3. 按照公式计算,得到 Log[R(x,y)];
      4. 将得到的结果量化为 [0, 255] 范围的像素值,然后输出结果图像。 

      原理

      图像I(x,y)可以看做是入射图像(也有人称之为亮度图像)L(x,y)和反射图像R(x,y)构成,入射光照射在反射物体上,通过反射物体的反射,形成反射光进入人眼。其原理图如下所示:

       

      最后形成的图像可以如下公式表示:

      等式两边取对数得:

      L(x,y)由I(x,y)和一个高斯核的卷积来近似表示:

      上式中*代表卷积,G(x,y)代表高斯核。

      最后,将Log(R(x,y))量化为0-255范围的像素值:


      代码实现

      # SSR
      import cv2
      import numpy as np
      def replaceZeroes(data):
          min_nonzero = np.min(data[np.nonzero(data)])  # 找到数组中最小的非零值
          data = np.where(data == 0, min_nonzero, data)  # 将数组中的零值替换为最小的非零值
          return data  # 返回替换后的数组
      def SSR(src_img, size):
          L_blur = cv2.GaussianBlur(src_img, (size, size), 0)  # 高斯函数
          img = replaceZeroes(src_img)  # 去除0  
          L_blur = replaceZeroes(L_blur)  # 去除0
          dst_Img = cv2.log(img / 255.0)  # 归一化取log
          dst_Lblur = cv2.log(L_blur / 255.0)  # 归一化取log
          dst_IxL = cv2.multiply(dst_Img, dst_Lblur)  # 乘  L(x,y)=S(x,y)*G(x,y)
          log_R = cv2.subtract(dst_Img, dst_IxL)  # 减  log(R(x,y))=log(S(x,y))-log(L(x,y))
          dst_R = cv2.normalize(log_R, None, 0, 255, cv2.NORM_MINMAX)  # 放缩到0-255
          log_uint8 = cv2.convertScaleAbs(dst_R)  # 取整
          return log_uint8
      def SSR_image(image):
          size = 3
          b_gray, g_gray, r_gray = cv2.split(image)  # 拆分三个通道
          # 分别对每一个通道进行 SSR
          b_gray = SSR(b_gray, size)
          g_gray = SSR(g_gray, size)
          r_gray = SSR(r_gray, size)
          result = cv2.merge([b_gray, g_gray, r_gray])  # 通道合并。
          return result
      if __name__ == "__main__":
          input_img = cv2.imread('img_1.png', cv2.IMREAD_COLOR)  # 读取输入图像
          enhanced_img = SSR_image(input_img)  # 调用 SSR 函数得到增强后的图像
          cv2.imwrite('img_2.png', enhanced_img)  # 将增强后的图像保存为 img_2.png
          # 显示原始图像和增强后的图像
          cv2.imshow('Original Image', input_img)
          cv2.imshow('Enhanced(SSR) Image', enhanced_img)
          cv2.waitKey(0)
          cv2.destroyAllWindows()
      
      • 实现效果:
         
        
        •  参数调整:

          主要调整是高斯模糊: 

          L_blur = cv2.GaussianBlur(src_img, (size, size), 0) 
          
          • src_img: 是输入图像,即要进行高斯模糊的图像。
          • (size,size): 是高斯核的大小,其中 size 是一个正整数。高斯核的大小决定了模糊的程度,size 越大,模糊程度越高。
          • 0 :是高斯核的标准差,在 x 方向上。如果这个值为 0,则会根据高斯核的大小自动计算。

            🎄3.2 多尺度MSR(Multi-Scale Retinex)多尺度Retinex算法

             简介 

            MSR (Multi-Scale Retinex),即多尺度视网膜算法是在 SSR 算法的基础上提出的,采用多个不同的 sigma 值,然后将最后得到的不同结果进行加权取值。 

            优点是可以保持图像高保真度与对图像的动态范围进行压缩的同时,MSR也可实现色彩增强、颜色恒常性、局部动态范围压缩、全局动态范围压缩,也可以用于X光图像增强。


            原理

            对原始图像进行每个尺度的高斯滤波,得到模糊后的图像Li(x,y),其中小标i表示尺度数

            然后对每个尺度下进行累加计算:

            其中Weight(i)表示每个尺度对应的权重,要求各尺度权重之和必须为1,经典的取值为等权重。

            基本的计算原理:

            上式中,I为原始输入图像,G是滤波函数,一般为高斯函数,N为尺度的数量,W为每个尺度的权重,一般都为1/N, R表示在对数域的图像的输出。


            代码实现

            # MSR
            import numpy as np
            import cv2
            def replaceZeroes(data):
                min_nonzero = min(data[np.nonzero(data)])
                data[data == 0] = min_nonzero
                return data
            def MSR(img, scales):
                weight = 1 / 3.0
                scales_size = len(scales)
                h, w = img.shape[:2]
                log_R = np.zeros((h, w), dtype=np.float32)
                for i in range(scales_size):
                    img = replaceZeroes(img)
                    L_blur = cv2.GaussianBlur(img, (scales[i], scales[i]), 0)
                    L_blur = replaceZeroes(L_blur)
                    dst_Img = cv2.log(img / 255.0)
                    dst_Lblur = cv2.log(L_blur / 255.0)
                    dst_Ixl = cv2.multiply(dst_Img, dst_Lblur)
                    log_R += weight * cv2.subtract(dst_Img, dst_Ixl)
                dst_R = cv2.normalize(log_R, None, 0, 255, cv2.NORM_MINMAX)
                log_uint8 = cv2.convertScaleAbs(dst_R)
                return log_uint8
            if __name__ == '__main__':
                img = 'img_1.png'
                scales = [15, 101, 301] #可调整的位置
                src_img = cv2.imread(img)
                b_gray, g_gray, r_gray = cv2.split(src_img)
                b_gray = MSR(b_gray, scales)
                g_gray = MSR(g_gray, scales)
                r_gray = MSR(r_gray, scales)
                result = cv2.merge([b_gray, g_gray, r_gray])
                cv2.imshow('Original Image', src_img)
                cv2.imshow('Enhanced(MSR) Image', result)
                cv2.waitKey(0)
                cv2.destroyAllWindows()
            •  实现效果:

              (尺度因子为(15,101,301)时) 

              (尺度因子为(3,5,9)时)  


              •   参数调整:

                 主要调整是尺度因子: 

                    scales = [3, 5, 9] #可调整的位置

                🎄3.3 MSRCR(Multi-Scale Retinex with Color Restoration)具有色彩恢复的多尺度Retinex算法

                 简介  

                在以上的两幅测试图像中,特别是第二幅,我们看到明显的偏色效果,这就是SSR和MSR普遍都存在的问题。

                为此,研究者又开发出一种称之为带色彩恢复的多尺度视网膜增强算法(MSRCR,Multi-Scale Retinex with Color Restoration) ,具体讨论的过程详见《A Multiscale Retinex for Bridging the Gap Between Color Images and the Human Observation of Scenes》论文。


                原理

                在前面的增强过程中,图像可能会因为增加了噪声,而使得图像的局部细节色彩失真,不能显现出物体的真正颜色,整体视觉效果变差。

                针对这一点不足,MSRCR在MSR的基础上,加入了色彩恢复因子C来调节由于图像局部区域对比度增强而导致颜色失真的缺陷。

                论文中提出的算法步骤:

                 (注:如果是灰度图像,只需要计算一次即可,如果是彩色图像,如RGB三通道,则每个通道均需要如上进行计算。)

                上式参数: 

                • G:增益Gain(一般取值:5)
                • b:偏差Offset(一般取值:25)
                • I (x, y):某个通道的图像
                • C:某个通道的彩色回复因子,用来调节3个通道颜色的比例
                • f(·):颜色空间的映射函数
                • β:增益常数(一般取值:46)
                • α:受控制的非线性强度(一般取值:125)

                  代码实现 

                  import cv2
                  import numpy as np
                  import math
                  def replaceZeroes(data):
                      min_nonzero = min(data[np.nonzero(data)])
                      data[data == 0] = min_nonzero
                      return data
                  def simple_color_balance(input_img, s1, s2):
                      h, w = input_img.shape[:2]
                      out_img = np.zeros([h, w])
                      sort_img = input_img.copy()
                      one_dim_array = sort_img.flatten()  # 转化为一维数组
                      sort_array = sorted(one_dim_array)  # 对一维数组按升序排序
                      per1 = int((h * w) * s1 / 100)
                      minvalue = sort_array[per1]
                      per2 = int((h * w) * s2 / 100)
                      maxvalue = sort_array[(h * w) - 1 - per2]
                      # 实施简单白平衡算法
                      if (maxvalue  maxvalue):
                                      out_img[m, n] = 255
                                  else:
                                      out_img[m, n] = scale * (input_img[m, n] - minvalue)  # 映射中间段的图像像素
                      out_img = cv2.convertScaleAbs(out_img)
                      return out_img
                  def MSRCR(img, scales, s1, s2):
                      h, w = img.shape[:2]
                      # print(h, w)
                      scles_size = len(scales)
                      img = np.array(img, dtype=np.float64)
                      # print(img)
                      log_R = np.zeros((h, w), dtype=np.float64)
                      img_sum = np.add(img[:, :, 0], img[:, :, 1], img[:, :, 2])
                      img_sum = replaceZeroes(img_sum)
                      gray_img = []
                      for j in range(3):
                          img[:, :, j] = replaceZeroes(img[:, :, j])
                          for i in range(0, scles_size):
                              L_blur = cv2.GaussianBlur(img[:, :, j], (scales[i], scales[i]), 0)
                              L_blur = replaceZeroes(L_blur)
                              dst_img = cv2.log(img[:, :, j])
                              dst_Lblur = cv2.log(L_blur)
                              log_R += cv2.subtract(dst_img, dst_Lblur)
                          MSR = log_R / 3.0
                          '''
                              img_sum_log = np.zeros((h, w))
                              for i in range(0, h):
                                  for k in range(0, w):
                                      img_sum_log[i,k] = 125.0*math.log(img[i,k,j]) - math.log(img_sum[i,k])
                              MSRCR = MSR * (img_sum_log[:, :])
                              print(img_sum)
                              # x = cv2.log(img_sum)
                              '''
                          MSRCR = MSR * (cv2.log(125.0 * img[:, :, j]) - cv2.log(img_sum))
                          gray = simple_color_balance(MSRCR, s1, s2)
                          gray_img.append(gray)
                      return gray_img
                  if __name__ == '__main__':
                      scales = [15, 101, 301]
                      s1, s2 = 2, 3
                      src_img = cv2.imread('img_1.png')
                      src_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2RGB)
                      cv2.imshow('Original Image', src_img)
                      MSRCR_Out = MSRCR(src_img, scales, s1, s2)
                      result = cv2.merge([MSRCR_Out[0], MSRCR_Out[1], MSRCR_Out[2]])
                      cv2.imshow('Enhanced(MSRCR) Image', result)
                      cv2.waitKey(0)
                      cv2.destroyAllWindows()
                  •  实现效果:

                    • 参数调整:

                      同上~


                      🚀总结

                      基于Retinex理论形成的图像增强算法,其优势在于光源一般不会对图像中各像素的相对明暗关系造成影响,还能一定程度上改变光源影响下的图像质量,以提高图像增强效果让图像看起来更加清晰。

                      但是,基于Retinex理论的图像增强算法也并非是完美的:

                      SSR算法:无法同时提供丰富的动态范围压缩和颜色保真,经低尺度SSR算法增强后的图像存在光晕情况,而经高尺度SSR算法增强后的图像尽管可以消除光晕,但动态范围压缩效果不佳。

                      MSR 算法:弥补了SSR算法的不足,增强后图像细节更加突出,色彩更加丰富,但其增强过程可能会因噪声的增加而使图像局部区域色彩失真,最终影响整体视觉效果。

                      MSRCR算法:又进一步解决了MSR算法存在的这一问题,处理后的图像效果更佳,但计算过程过于复杂。


                      🚀整体代码

                      import numpy as np
                      from .tools import measure_time,eps,gauss_blur,simplest_color_balance
                      ### Frankle-McCann Retinex[2,3]
                      @measure_time
                      def retinex_FM(img,iter=4):
                          '''log(OP(x,y))=1/2{log(OP(x,y))+[log(OP(xs,ys))+log(R(x,y))-log(R(xs,ys))]*}, see
                             matlab code in https://www.cs.sfu.ca/~colour/publications/IST-2000/'''
                          if len(img.shape)==2:
                              img=img[...,None]
                          ret=np.zeros(img.shape,dtype='uint8')
                          def update_OP(x,y):
                              nonlocal OP
                              IP=OP.copy()
                              if x>0 and y==0:
                                  IP[:-x,:]=OP[x:,:]+R[:-x,:]-R[x:,:]
                              if x==0 and y>0:
                                  IP[:,y:]=OP[:,:-y]+R[:,y:]-R[:,:-y]
                              if x=1: #iterations is slow
                                  for k in range(iter):
                                      update_OP(S,0)
                                      update_OP(0,S)
                                  S=int(-S/2)
                              OP=np.exp(OP)
                              mmin=np.min(OP)
                              mmax=np.max(OP)
                              ret[...,i]=(OP-mmin)/(mmax-mmin)*255
                          return ret.squeeze()
                      ### Single-Scale Retinex[4]
                      @measure_time
                      def retinex_SSR(img,sigma):
                          '''log(R(x,y))=log(S(x,y))-log(S(x,y)*G(x,y))=log(S(x,y))-log(L(x,y)), i.e. 
                             r=s-l. S(x,y) and R(x,y) represent input image and retinex output image 
                             respectively, L(x,y):=S(x,y)*G(x,y) represents the lightness function, 
                             defined as the original image S operated with a gaussian filter G(named 
                             as center/surround function)
                          implement ssr on single channel:
                             1) read original image and convert to double(type) as S
                             2) calc coefficient of G with sigma, i.e. normalize the gaussian kernel
                             3) calc r by r=s-l and then convert r to R(from log to real)
                             4) stretch the values of R into the range 0~255
                          issue:
                             we don't convert values from log domain to real domain in step 3 above, 
                             because it will bring terrible effect. In fact nobody does this, but the 
                             reason still remains unknown
                          note:
                             gauss blur is the main operation of SSR, its time complexity is O(mnpq), 
                             m&n is the shape of image, p&q is the size of filter, we can use recursive 
                             gaussian filter(RGF), O(mn), to alternative it(see func fast_gauss_blur). 
                             Or transform from time domain to frequency domain using Fourier Transform 
                             to reduce complexity[4]
                          '''
                          if len(img.shape)==2:
                              img=img[...,None]
                          ret=np.zeros(img.shape,dtype='uint8')
                          for i in range(img.shape[-1]):
                              channel=img[...,i].astype('double')
                              S_log=np.log(channel+1)
                              gaussian=gauss_blur(channel,sigma)
                              #gaussian=cv2.filter2D(channel,-1,get_gauss_kernel(sigma)) #conv may be slow if size too big
                              #gaussian=cv2.GaussianBlur(channel,(0,0),sigma) #always slower
                              L_log=np.log(gaussian+1)
                              r=S_log-L_log
                              R=r #R=np.exp(r)?
                              mmin=np.min(R)
                              mmax=np.max(R)
                              stretch=(R-mmin)/(mmax-mmin)*255 #linear stretch
                              ret[...,i]=stretch
                          return ret.squeeze()
                      ### Multi-Scale Retinex[4]
                      @measure_time
                      def retinex_MSR(img,sigmas=[15,80,250],weights=None):
                          '''r=∑(log(S)-log(S*G))w, MSR combines various SSR with different(or same) weights, 
                             commonly we select 3 scales(sigma) and equal weights, (15,80,250) is a good 
                             choice. If len(sigmas)=1, equal to SSR
                          args:
                             sigmas: a list
                             weights: None or a list, it represents the weight for each SSR, their sum should 
                                be 1, if None, the weights will be [1/t, 1/t, ..., 1/t], t=len(sigmas)
                          '''
                          if weights==None:
                              weights=np.ones(len(sigmas))/len(sigmas)
                          elif not abs(sum(weights)-1)255]=255
                              stretch[stretch
The End
微信