這篇是 SSIM 系列第三篇,接續前篇 使用 PyTorch 實做 2D 影像捲積

要來談一下 SSIM 如何在不均勻光源下優化 SSIM。

如果你是剛開始接觸 SSIM 的人,可能會跟我一樣看過一篇幫愛因斯坦去噪的實現文章

沒錯,當初我也是看到愛因斯坦的臉逐漸浮現,才覺得 SSIM 可以拿來解工程問題!

只是愛因斯坦那篇的調節亮度方法對於工程來說太過理想化,

問題在於他是把整張圖片的像素值乘以 0.9 調亮度!

這樣的話 SSIM 算法裡面的變異數項是不會變的,只有考慮到平均數項,

所以他做出來的 SSIM 值不會差太多。

但是現實世界是很絢爛多彩且難以預測的,
就像我前兩天載室友出門,停等紅燈時他突然發生奇怪的聲音,

轉頭問他在幹麻?

:我在跟你的機車排氣管對頻。

實務上,我們可能會面對不均勻光源的問題,就像是街邊的燈光:

這燈光很糟糕,比忘記簽聯絡簿的國小學童還糟糕,

因為它的光源只會影響照片的一部分,而且還會隨著距離遞減。

如果使用原版的 SSIM 方法對比這種有無開燈的照片,

則 SSIM值會不可避免的大幅下降,

但這兩張照片的前景內容物(e.g. 人、車、直升機、脫光衣服在路上奔跑的人)一樣對吧?

所以在基於前景的情況下,有些時候我們會希望有無開燈的照片應該要一樣,

數學上也就是得到理想值為 1 的 SSIM 值。

(實在是繞口令,讓我想到事件攝影機。)

要解這個問題就要先了解 SSIM 的本質,

SSIM 是由 sliding window 來解圖片中各自區域均值、變異數,還有兩張圖片的共變數所構成。

而二維的 window 又是由一維的 kernel (通常是 Gaussian)矩陣相乘得到,

基本上 OpenCV 裡面提供的方法是這樣:

nkernel = cv2.getGaussianKernel(11, 1.5)
nwindow = np.outer(nkernel, nkernel.transpose())

往 cv2.getGaussianKernel 裡面看,我們可以知道這條函式來自 getGaussianKernelBitExact:

再往 getGaussianKernelBitExact 裡面看:

重點是 158, 181 行運算的兩個迴圈,他們在幫忙算高斯核並歸一化。

我們的重點是歸一化,以前的簡易版 OpenCV Source Code 大概長這樣:

def getGaussianKernel(M, std):
    n = np.arange(0, M) - (M - 1.0) / 2.0
    sig2 = 2 * std * std
    w = np.exp(-n ** 2 / sig2)
    muli = 1/sum(w[:])
    wn = w * muli

    return wn

這種歸一化的一維高斯核長得像是:

扁扁有點可愛,像是軟趴趴的史萊姆。

歸一化的好處是能夠保證像素值永遠恆等(因為也只是重新分配)。

但是這樣可能會造成一些問題,

想像一下你是該像素當事人站在 X=5 的位置,
現在要過年了,大家手上都有一筆要發出去的紅包錢,俗稱紅包預算。
假設你的紅包預算是一萬塊:

X=5 是你自己,辛苦了一整年,你決定給自己紅包預算 * 0.26 倍,恭喜獲得 2600 元。
X=4, X=6 這個漢明距離為一的,是你的父母,得到他們的紅包預算 *0.21 倍;
X=3, X=7 這個漢明距離為二的,是你的手足,得到他們的紅包預算 *0.10 倍;
一路發下去,你會發現不管身邊的人多有錢,你最多只會得到原來的一萬xD
因為你早就被歸一化了!
這種 SSIM 分配框架下,就會產生很多問題,例如:
你跟小明是同事,都有一萬塊的紅包預算。
你身邊的人都很有錢,分完紅包你還有一萬塊;
但是小明就不一樣了,他身邊的人都超窮,分完紅包後他只剩兩千六。

小明知道這件事情後,會不會沒事就想從背後捅你這個好野死囝仔?
這就會造成很多社會問題,因為貧富差距(變異數)過大。
要怎麼解決這個問題呢?

讓我們看看非歸一化的原始高斯核:

這個版本就好多了,
無論小明身邊的人再怎麼窮,他始終可以拿回自己的紅包預算一萬塊,
剩下的都是多的。
於是小明跟你的貧富差距就減少了(某種程度上減少變異數),
所以這個問題就得到了緩解。
結論:

如果要讓兩張前景相同,
但是其中一張有受燈光(或是部份遮罩影響)的圖片做 SSIM 計算,
使用非歸一化的高斯核可以有效提昇 SSIM 值。

以下附上我的實驗源碼:

def ourGaussianKernel(M, std):
    n = np.arange(0, M) - (M - 1.0) / 2.0
    sig2 = 2 * std * std
    w = np.exp(-n ** 2 / sig2)
    # muli = 1/sum(w[:])
    # wn = w * muli

    return w

def ssim_Normalized(img1, img2):
    C1 = (0.01 * 255)**2
    C2 = (0.03 * 255)**2

    img1 = img1.astype(np.float64)
    img2 = img2.astype(np.float64)
    nkernel = cv2.getGaussianKernel(11, 1.5)
    nwindow = np.outer(nkernel, nkernel.transpose())
    nmu1 = cv2.filter2D(img1, -1, nwindow)[5:-5, 5:-5]
    nmu2 = cv2.filter2D(img2, -1, nwindow)[5:-5, 5:-5]
    nmu1_sq = nmu1**2
    nmu2_sq = nmu2**2
    nmu1_mu2 = nmu1 * nmu2
    nsigma1_sq = cv2.filter2D(img1**2, -1, nwindow)[5:-5, 5:-5] - nmu1_sq
    nsigma2_sq = cv2.filter2D(img2**2, -1, nwindow)[5:-5, 5:-5] - nmu2_sq
    nsigma12 = cv2.filter2D(img1 * img2, -1, nwindow)[5:-5, 5:-5] - nmu1_mu2
    nssim_map = ((2 * nmu1_mu2 + C1) *
                (2 * nsigma12 + C2)) / ((nmu1_sq + nmu2_sq + C1) *
                                       (nsigma1_sq + nsigma2_sq + C2))
    return nssim_map.mean()

def ssim_Unnormalized(img1, img2):
    C1 = (0.01 * 255)**2
    C2 = (0.03 * 255)**2

    img1 = img1.astype(np.float64)
    img2 = img2.astype(np.float64)
    kernel = ourGaussianKernel(11, 1.5)
    window = np.outer(kernel, kernel.transpose())
    mu1 = cv2.filter2D(img1, -1, window)[5:-5, 5:-5]
    mu2 = cv2.filter2D(img2, -1, window)[5:-5, 5:-5]
    mu1_sq = mu1**2
    mu2_sq = mu2**2
    mu1_mu2 = mu1 * mu2
    sigma1_sq = cv2.filter2D(img1**2, -1, window)[5:-5, 5:-5] - mu1_sq
    sigma2_sq = cv2.filter2D(img2**2, -1, window)[5:-5, 5:-5] - mu2_sq
    sigma12 = cv2.filter2D(img1 * img2, -1, window)[5:-5, 5:-5] - mu1_mu2
    ssim_map = ((2 * mu1_mu2 + C1) *
                (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) *
                                       (sigma1_sq + sigma2_sq + C2))
    return ssim_map.mean()
if __name__=='__main__':
    img1_path = 'img1.png'
    img2_path = 'img2.png'
    kernelSize = 21
    light = list(map(np.uint8, gkern(kernlen=kernelSize, std=1.5)*100))
    img1 = np.random.randint(0, 150, size=(100, 100)).astype(np.uint8)
    img2 = np.zeros_like(img1)
    img2[:] = img1[:]

    row = np.random.randint(int(kernelSize/2), img1.shape[0] - int(kernelSize/2))
    col = np.random.randint(int(kernelSize/2), img1.shape[1] - int(kernelSize/2))
    times = np.random.randint(1, 100)
    for i in range(0, times):
        img2[row:row + kernelSize, col:col + kernelSize] += light
    img2 = np.clip(img2, 0, 255)
    
    print("This is Normalized): " + str("{:.5f}".format(ssim_Normalized(img1, img2))))
    print("This is Unnormalized): " + str("{:.5f}".format(ssim_Unnormalized(img1, img2))))

親測可用:

有任何問題或筆誤歡迎聯繫我:wuyiulin@gmail.com

By wuyiulin

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

發佈留言

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