分類: ISP

  • Python 寫自動白平衡 – 完美反射核心

     

    最近上班遇到一些圖片顏色偏離原色,

    身為影像工程從業者就會想自己寫 3A算法來校正。

    而自動白平衡便是 3A 中的其中 1A – 南湖大山!

    而自動白平衡便是 3A 中的其中 1A – Auto White Balance

    自動白平衡的核心就是解決圖片色偏的方法,

    大都是找到圖片中的參考值,

    再用參考值及不同方法去對整張圖片做校正。

    像是灰色世界算法就是拿全圖片的加總的三通道均值當作參考值,

    再拿這個參考值除以三通道分別均值做成三通道的增益,

    得到三通道增益後分別對通道相乘。

    今天提到的完美反射核心,它的概念是相信圖片中有一群接近白色的區塊,

    因為 RGB or BRG 的世界裡面 (255, 255, 255) 就是白色嘛,

    所以有點像找到這塊邊長為 255 的正方形中那群最遠離原點 (0, 0, 0) 的那群點。

    找到那群點後,再求得這群的均值作為參考值,

    參考值除以各通道的均值就能得到各通道的增益,

    最後再拉回去與原圖相乘 -> 結案。

      
    def AutoWhiteBalance_PRA(imgPath, ratio=0.2): # 自動白平衡演算法_完美反射核心, 0 < ratio <1.
    
        BGRimg = cv2.imread(imgPath)
        BGRimg = BGRimg.astype(np.float32)    # 將資料格式轉為 float32,避免 OpenCV 讀圖進來為 np.uint8 而造成後續截斷問題。
        pixelSum = BGRimg[:,:,0] + BGRimg[:,:,1] + BGRimg[:,:,2]     
        pexelMax = np.max(BGRimg[:,:,:])    # 求三通道加總最大值作為像素值拉伸指標
        
        row , col, chan = BGRimg.shape[0], BGRimg.shape[1], BGRimg.shape[2]
        
        pixelSum = pixelSum.flatten()
        imgFlatten = BGRimg.reshape(row*col, chan)
        
        thresholdNum =  int(ratio * row * col)
        thresholdList = np.argpartition(pixelSum, kth=-thresholdNum, axis=None)    # 由右至左設定閾值位置,並得到粗略排序後的索引表。
        thresholdList = thresholdList[-thresholdNum:]    # 取得索引表中比閾值大的索引號
        
    
        blueMean, greenMean, redMean, i = 0, 0, 0, 0
        
        for i in thresholdList:
            blueMean  += imgFlatten[i][0]
            greenMean += imgFlatten[i][1]
            redMean   += imgFlatten[i][2]
        
        blueMean  /= thresholdNum
        greenMean /= thresholdNum
        redMean   /= thresholdNum
    
        blueGain  = pexelMax / blueMean
        greenGain = pexelMax / greenMean
        redGain   = pexelMax / redMean
        
        BGRimg[:,:,0] = BGRimg[:,:,0] * blueGain
        BGRimg[:,:,1] = BGRimg[:,:,1] * greenGain
        BGRimg[:,:,2] = BGRimg[:,:,2] * redGain
    
        BGRimg = np.clip(BGRimg, 0, 255)
        BGRimg = BGRimg.astype(np.uint8)
    
        return BGRimg
    

    np.argarptition
    中間那個 np.argarptition 比較迷惑,可以說一下。
    np.argarptition 的功能就是把你指定的值設定好,
    然後把 小於/大於 設定值的數的索引排序在設定值的 左邊/右邊。
    舉個例子,有個數列是:
    list = [3, 2, 5, 6, 1, 4]
    我想要找第二大的值,那我的參考值就設定成 -2。
    (從右邊數過來第二個,換句話說就是第二大的 5。)
    Index = np.argarptition(list, kth=-2, axis=None)
    那麼 Index 就會等於:
    Index = [0, 1, 4, 5, 2, 3] 
    分別是 [3, 2, 5, 6, 1, 4] 的索引中小於/大於 5 的索引。
    如果把 Index 換成值,像是 list[Index] 就會等於:
    list[Index] = [3, 2, 1, 4, 5, 6]
    但在指定值(5) 左右的順序不保證,得看演算法怎麼實現,
    會選這套方法是因為複雜度會比全排序還要快。
    破圖問題
    理論上所有通道的 Gain 都會是大於 1 的正數,
    再加上最後有做 clip(0, 255)
    換言之所有經轉換的像素值上限是 255, 下限是自己的原值。
    但在我計算這套算法的時候,
    看到某個藍色通道值是 80,計算完之後變成 17 我真的是傻了。
    後來排查才知道哪裡爆開:
    1. cv2.imread(imgPath) 讀進來的資料型態是 numpy.uint8
    那個 8 我之前都認為是 8 Byte,
    所以認為存值可以存到 $2^{8*8}$ 是沒問題的。
    沒想到它是 8 bit
    取值範圍直接變成 [0, 255]
    所以當我的 Gain 等於 3.421021082738187 時,
    理論上乘以增益的像素值(80)應該要是 273.多。
    但是為什麼是 17 呢?
    登登登登
    因為二進位下 273 是 100010001,而最終要符合 uin8 轉型則會變成 00010001,
    那就是 十進位 17 。
    所以這個問題才會演變成像素值驟降 -> 破圖。
    以下是我到酒吧喝酒,以被色偏的百富12年做標準的圖片:
    (左邊是原圖;中間是灰色世界校正法;右邊是完美反射法,ratio 開 0.05)
    百富的酒標應該要是純白色的,以這個基準來說我覺得完美反射校正得比較好,
    它連後面的燈光都一併校正,看起來比較舒服,
    而灰色世界感覺上了一層透明深藍色濾鏡。
    如果本文有任何書寫錯誤麻煩聯絡我:
    wuyiulin@gmail.com
    Ref.