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

身為影像工程從業者就會想自己寫 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.

By wuyiulin

喜歡騎單車的影像算法工程師

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *