計算攝影中的 Python 圖像處理簡介

已發表: 2022-03-11

計算攝影是關於通過計算增強攝影過程。 雖然我們通常傾向於認為這僅適用於最終結果的後期處理(類似於照片編輯),但可能性要豐富得多,因為可以在攝影過程的每個步驟中啟用計算——從場景照明開始,繼續鏡頭,最終甚至在拍攝圖像的顯示上。

這很重要,因為與普通相機相比,它允許以不同的方式做更多的事情。 這也很重要,因為當今最流行的相機類型 - 移動相機 - 與其較大的兄弟相機(數碼單反相機)相比並不是特別強大,但它通過利用設備上可用的計算能力設法做得很好.

我們將看兩個計算可以增強攝影效果的例子——更準確地說,我們將看到如何簡單地拍攝更多照片並使用一些 Python 將它們組合起來,可以在移動相機硬件無法提供的兩種情況下創造出很好的結果真正閃耀——低光和高動態範圍。

低光攝影

假設我們想拍攝場景的低光照片,但相機的光圈(鏡頭)小且曝光時間有限。 這是手機攝像頭的典型情況,在低光照場景下,可能會產生這樣的圖像(使用 iPhone 6 攝像頭拍攝):

弱光環境下的情侶玩具圖片

如果我們嘗試提高對比度,結果如下,這也是相當糟糕的:

與上面相同的圖像,更亮,但有分散注意力的視覺噪音

發生什麼了? 所有這些噪音是從哪裡來的?

答案是噪音來自傳感器——該設備試圖確定光線何時照射到它以及光線的強度。 然而,在弱光下,它必須大大提高其靈敏度才能記錄任何東西,而高靈敏度意味著它也開始檢測誤報——根本不存在的光子。 (作為旁注,這個問題不僅影響設備,還影響我們人類:下次你在一個黑暗的房間裡時,花點時間注意你視野中存在的噪音。)

成像設備中總會存在一定量的噪聲; 但是,如果信號(有用信息)具有高強度,則噪聲將可以忽略不計(高信噪比)。 當信號較低時(例如在低光照條件下),噪聲會突出(低信噪比)。

儘管如此,我們仍然可以克服噪點問題,即使有所有相機限制,以獲得比上述更好的照片。

為此,我們需要考慮隨著時間的推移會發生什麼:信號將保持不變(相同的場景,我們假設它是靜態的),而噪聲將是完全隨機的。 這意味著,如果我們對場景進行多次拍攝,它們將具有不同版本的噪點,但有用的信息相同。

因此,如果我們對一段時間內拍攝的許多圖像進行平均,則噪聲將被抵消,而信號將不受影響。

下圖顯示了一個簡化示例:我們有一個受噪聲影響的信號(三角形),我們嘗試通過對受不同噪聲影響的同一信號的多個實例進行平均來恢復該信號。

三角形的四面板演示,表示添加了噪聲的三角形的分散圖像,表示 50 個實例的平均值的鋸齒狀三角形,以及 1000 個實例的平均值,看起來與原始三角形幾乎相同。

我們看到,儘管噪聲強到足以在任何單個實例中完全扭曲信號,但平均逐漸降低了噪聲,我們恢復了原始信號。

讓我們看看這個原則是如何應用於圖像的:首先,我們需要在相機允許的最大曝光下為主體拍攝多張照片。 為獲得最佳效果,請使用允許手動拍攝的應用程序。 重要的是從同一位置拍攝照片,因此(臨時)三腳架會有所幫助。

更多的照片通常意味著更好的質量,但確切的數量取決於情況:有多少光線,相機的靈敏度等。一個好的範圍可以在 10 到 100 之間。

一旦我們有了這些圖像(如果可能的話,以原始格式),我們可以在 Python 中讀取和處理它們。

對於那些不熟悉 Python 中的圖像處理的人,我們應該提一下,圖像表示為字節值 (0-255) 的二維數組,即單色或灰度圖像。 彩色圖像可以被認為是一組三個這樣的圖像,一個用於每個顏色通道(R、G、B),或者實際上是一個由垂直位置、水平位置和顏色通道(0、1、2)索引的 3D 數組.

我們將使用兩個庫:NumPy (http://www.numpy.org/) 和 OpenCV (https://opencv.org/)。 第一個允許我們非常有效地對數組執行計算(代碼非常短),而 OpenCV 在這種情況下處理圖像文件的讀/寫,但功能更強大,提供了許多高級圖形程序——其中一些我們將在文章後面使用。

 import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)

結果(應用了自動對比度)表明噪點消失了,與原始圖像相比有了很大的改進。

玩具的原始照片,這次更亮更清晰,幾乎沒有可辨別的噪音

但是,我們仍然注意到一些奇怪的偽影,例如綠色框架和網格狀圖案。 這一次,它不是隨機噪聲,而是固定模式噪聲。 發生了什麼?

上圖左上角的特寫

左上角特寫,顯示綠框和網格圖案

同樣,我們可以將其歸咎於傳感器。 在這種情況下,我們看到傳感器的不同部分對光的反應不同,從而產生可見的圖案。 這些模式中的一些元素是規則的,很可能與傳感器基板(金屬/矽)以及它如何反射/吸收入射光子有關。 其他元素,例如白色像素,只是有缺陷的傳感器像素,它們可能對光過於敏感或過於不敏感。

幸運的是,也有一種方法可以消除這種噪音。 它被稱為暗幀減法。

為此,我們需要圖案噪聲本身的圖像,如果我們拍攝黑暗,就可以獲得這一點。 是的,沒錯——只需蓋住相機孔並以最大曝光時間和 ISO 值拍攝大量照片(比如 100 張),然後按上述方法處理它們。

當對許多黑色幀(實際上不是黑色的,由於隨機噪聲)進行平均時,我們最終會得到固定模式噪聲。 我們可以假設這個固定的噪聲將保持不變,所以這一步只需要一次:生成的圖像可以重複用於所有未來的低光拍攝。

以下是 iPhone 6 圖案噪點的右上方(對比度調整)的樣子:

上一張圖像中顯示的部分幀的圖案噪聲

再次,我們注意到網格狀紋理,甚至是看起來像卡住的白色像素。

一旦我們有了這個暗幀噪聲的值(在average_noise變量中),我們就可以在歸一化之前簡單地從我們的鏡頭中減去它:

 average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)

這是我們的最終照片:

再拍一張照片,這次完全沒有證據表明是在弱光下拍攝的

高動態範圍

小型(移動)相機的另一個限制是它的動態範圍小,這意味著它可以捕捉細節的光強度範圍相當小。

換句話說,相機只能從場景中捕捉到窄帶的光強; 該條帶下方的強度顯示為純黑色,而其上方的強度顯示為純白色,並且這些區域的任何細節都丟失了。

但是,相機(或攝影師)可以使用一個技巧,那就是調整曝光時間(傳感器暴露在光線下的時間),以有效地控製到達傳感器的總光量向上或向下移動範圍,以便為給定場景捕獲最合適的範圍。

但這是一種妥協。 許多細節未能進入最終照片。 在下面的兩張圖片中,我們看到使用不同曝光時間拍攝的同一場景:非常短的曝光(1/1000 秒)、中等曝光(1/50 秒)和長時間曝光(1/4 秒)。

同一張花朵圖片的三個版本,一個太暗以至於大部分照片都是黑色的,一個看起來很正常,儘管光線有點不合適,第三個是光線太高以至於很難看到照片中的花朵前景

如您所見,這三幅圖像都無法捕捉到所有可用的細節:燈絲僅在第一張照片中可見,一些花朵細節在中間或最後一張照片中可見,但不可見兩個都。

好消息是我們可以做一些事情,而且它涉及到使用一些 Python 代碼構建多個鏡頭。

我們將採用的方法基於 Paul Debevec 等人的工作,他在此處的論文中描述了該方法。 該方法的工作原理如下:

首先,它需要對同一場景(靜止)進行多次拍攝,但曝光時間不同。 同樣,與前一種情況一樣,我們需要一個三腳架或支架來確保相機完全不動。 我們還需要一個手動拍攝應用程序(如果使用手機),以便我們可以控制曝光時間並防止相機自動調整。 所需的照片數量取決於圖像中存在的強度範圍(從三個以上),並且曝光時間應該在該範圍內間隔開,以便我們感興趣的細節在至少一張照片中清楚地顯示出來。

接下來,使用一種算法根據相同像素在不同曝光時間內的顏色來重建相機的響應曲線。 這基本上讓我們可以在一個點的真實場景亮度、曝光時間和相應像素在捕獲圖像中的值之間建立映射。 我們將使用 OpenCV 庫中 Debevec 方法的實現。

 # Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)

響應曲線如下所示:

將響應曲線顯示為像素曝光 (log) 與像素值的關係圖

在垂直軸上,我們有一個點的場景亮度和曝光時間的累積效應,而在水平軸上,我們有相應像素將具有的值(每個通道 0 到 255)。

這條曲線允許我們執行相反的操作(這是該過程的下一步)——給定像素值和曝光時間,我們可以計算場景中每個點的真實亮度。 該亮度值稱為輻照度,它測量落在單位傳感器區域上的光能量。 與圖像數據不同,它使用浮點數表示,因為它反映了更廣泛的值範圍(因此,高動態範圍)。 一旦我們有了輻照度圖像(HDR 圖像),我們就可以簡單地保存它:

 # Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)

對於我們這些有幸擁有 HDR 顯示器(這種情況越來越普遍)的人來說,也許可以直接將這張照片的所有榮耀形象化。 不幸的是,HDR 標準仍處於起步階段,因此對於不同的顯示器,執行此操作的過程可能會有所不同。

對於我們其他人來說,好消息是我們仍然可以利用這些數據,儘管正常顯示需要圖像具有字節值 (0-255) 通道。 雖然我們需要放棄輻照度圖的一些豐富性,但至少我們可以控制如何去做。

這個過程稱為色調映射,它涉及將浮點輻照度圖(具有高值範圍)轉換為標準字節值圖像。 有一些技術可以做到這一點,以便保留許多額外的細節。 舉個例子說明這是如何工作的,想像一下,在我們將浮點範圍壓縮為字節值之前,我們增強(銳化)了 HDR 圖像中存在的邊緣。 增強這些邊緣將有助於在低動態範圍圖像中保留它們(以及它們提供的隱含細節)。

OpenCV 提供了一組這些色調映射運算符,例如 Drago、Durand、Mantiuk 或 Reinhardt。 下面是一個示例,說明如何使用這些運算符之一 (Durand) 及其產生的結果。

 durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255) 

上述計算的結果顯示為圖像

如果您需要對流程進行更多控制,還可以使用 Python 創建自己的運算符。 例如,這是使用自定義運算符獲得的結果,該運算符在將值範圍縮小到 8 位之前移除以極少像素表示的強度(隨後是自動對比步驟):

遵循上述過程產生的圖像

這是上述運算符的代碼:

 def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)

結論

我們已經看到瞭如何使用一些 Python 和幾個支持庫,我們可以突破物理相機的限制以改善最終結果。 我們討論的兩個示例都使用多個低質量鏡頭來創建更好的東西,但是對於不同的問題和限制還有許多其他方法。

雖然許多照相手機都有存儲或內置應用程序來處理這些特定示例,但顯然手動編程並享受更高水平的控制和理解並不難。

如果您對移動設備上的圖像計算感興趣,請查看由 Toptaler 和精英 OpenCV 開發人員 Altaibayar Tseveenbayar 編寫的 OpenCV 教程:在 iOS 中使用 MSER 進行實時對象檢測。