使用 OpenCV 進行高動態範圍(HDR)成像
來自專欄 Linux
在本教程中,我們將學習如何使用由不同曝光設置拍攝的多張圖像創建 高動態範圍(High Dynamic Range)(HDR)圖像。 我們將以 C++ 和 Python 兩種形式分享代碼。
什麼是高動態範圍成像?
大多數數碼相機和顯示器都是按照 24 位矩陣捕獲或者顯示彩色圖像。 每個顏色通道有 8 位,因此每個通道的像素值在 0-255 範圍內。 換句話說,普通的相機或者顯示器的動態範圍是有限的。
但是,我們周圍世界動態範圍極大。 在車庫內關燈就會變黑,直接看著太陽就會變得非常亮。 即使不考慮這些極端,在日常情況下,8 位的通道勉強可以捕捉到現場場景。 因此,相機會嘗試去評估光照並且自動設置曝光,這樣圖像的最關注區域就會有良好的動態範圍,並且太暗和太亮的部分會被相應截取為 0 和 255。
在下圖中,左側的圖像是正常曝光的圖像。 請注意,由於相機決定使用拍攝主體(我的兒子)的設置,所以背景中的天空已經完全流失了,但是明亮的天空也因此被刷掉了。 右側的圖像是由 iPhone 生成的HDR圖像。
High Dynamic Range (HDR)
iPhone 是如何拍攝 HDR 圖像的呢? 它實際上採用三種不同的曝光度拍攝了 3 張圖像,3 張圖像拍攝非常迅速,在 3 張圖像之間幾乎沒有產生位移。然後組合三幅圖像來產生 HDR 圖像。 我們將在下一節看到一些細節。
將在不同曝光設置下獲取的相同場景的不同圖像組合的過程稱為高動態範圍(HDR)成像。
高動態範圍(HDR)成像是如何工作的?
在本節中,我們來看下使用 OpenCV 創建 HDR 圖像的步驟。
要想輕鬆學習本教程,請點擊此處下載 C++ 和 Python 代碼還有圖像。 如果您有興趣了解更多關於人工智慧,計算機視覺和機器學習的信息,請訂閱我們的電子雜誌。
第 1 步:捕獲不同曝光度的多張圖像
當我們使用相機拍照時,每個通道只有 8 位來表示場景的動態範圍(亮度範圍)。 但是,通過改變快門速度,我們可以在不同的曝光條件下拍攝多個場景圖像。 大多數單反相機(SLR)有一個功能稱為 自動包圍式曝光(Auto Exposure Bracketing)(AEB),只需按一下按鈕,我們就可以在不同的曝光下拍攝多張照片。 如果你正在使用 iPhone,你可以使用這個自動包圍式 HDR 應用程序,如果你是一個 Android 用戶,你可以嘗試一個更好的相機應用程序。
場景沒有變化時,在相機上使用自動包圍式曝光或在手機上使用自動包圍式應用程序,我們可以一張接一張地快速拍攝多張照片。 當我們在 iPhone 中使用 HDR 模式時,會拍攝三張照片。
- 曝光不足的圖像:該圖像比正確曝光的圖像更暗。 目標是捕捉非常明亮的圖像部分。
- 正確曝光的圖像:這是相機將根據其估計的照明拍攝的常規圖像。
- 曝光過度的圖像:該圖像比正確曝光的圖像更亮。 目標是拍攝非常黑暗的圖像部分。
但是,如果場景的動態範圍很大,我們可以拍攝三張以上的圖片來合成 HDR 圖像。 在本教程中,我們將使用曝光時間為1/30 秒,0.25 秒,2.5 秒和 15 秒的 4 張圖像。 縮略圖如下所示。
Auto Exposure Bracketed HDR image sequence
單反相機或手機的曝光時間和其他設置的信息通常存儲在 JPEG 文件的 EXIF 元數據中。 查看此鏈接可在 Windows 和 Mac 中查看存儲在 JPEG 文件中的 EXIF 元數據。 或者,您可以使用我最喜歡的名為 EXIFTOOL 的查看 EXIF 的命令行工具。
我們先從讀取分配到不同曝光時間的圖像開始。
C++
void readImagesAndTimes(vector<Mat> &images, vector<float> ×){ int numImages = 4; // 曝光時間列表 static const float timesArray[] = {1/30.0f,0.25,2.5,15.0}; times.assign(timesArray, timesArray + numImages); // 圖像文件名稱列表 static const char* filenames[] = {"img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"}; for(int i=0; i < numImages; i++) { Mat im = imread(filenames[i]); images.push_back(im); }}
Python
def readImagesAndTimes(): # 曝光時間列表 times = np.array([ 1/30.0, 0.25, 2.5, 15.0 ], dtype=np.float32) # 圖像文件名稱列表 filenames = ["img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"] images = [] for filename in filenames: im = cv2.imread(filename) images.append(im) return images, times
第 2 步:對齊圖像
合成 HDR 圖像時使用的圖像如果未對齊可能會導致嚴重的偽影。 在下圖中,左側的圖像是使用未對齊的圖像組成的 HDR 圖像,右側的圖像是使用對齊的圖像的圖像。 通過放大圖像的一部分(使用紅色圓圈顯示的)我們會在左側圖像中看到嚴重的鬼影。
Misalignment problem in HDR
在拍攝照片製作 HDR 圖像時,專業攝影師自然是將相機安裝在三腳架上。 他們還使用稱為鏡像鎖定功能來減少額外的振動。 即使如此,圖像可能仍然沒有完美對齊,因為沒有辦法保證無振動的環境。 使用手持相機或手機拍攝圖像時,對齊問題會變得更糟。
幸運的是,OpenCV 提供了一種簡單的方法,使用 AlignMTB
對齊這些圖像。 該演算法將所有圖像轉換為 中值閾值點陣圖(median threshold bitmaps)(MTB)。 圖像的 MTB 生成方式為將比中值亮度的更亮的分配為 1,其餘為 0。 MTB 不隨曝光時間的改變而改變。 因此不需要我們指定曝光時間就可以對齊 MTB。
基於 MTB 的對齊方式的代碼如下。
C++
// 對齊輸入圖像Ptr<AlignMTB> alignMTB = createAlignMTB();alignMTB->process(images, images);
Python
# 對齊輸入圖像alignMTB = cv2.createAlignMTB()alignMTB.process(images, images)
第 3 步:提取相機響應函數
典型相機的響應與場景亮度不成線性關係。 那是什麼意思呢? 假設有兩個物體由同一個相機拍攝,在現實世界中其中一個物體是另一個物體亮度的兩倍。 當您測量照片中兩個物體的像素亮度時,較亮物體的像素值將不會是較暗物體的兩倍。 在不估計 相機響應函數(Camera Response Function)(CRF)的情況下,我們將無法將圖像合併到一個HDR圖像中。
將多個曝光圖像合併為 HDR 圖像意味著什麼?
只考慮圖像的某個位置 (x,y)
一個像素。 如果 CRF 是線性的,則像素值將直接與曝光時間成比例,除非像素在特定圖像中太暗(即接近 0)或太亮(即接近 255)。 我們可以過濾出這些不好的像素(太暗或太亮),並且將像素值除以曝光時間來估計像素的亮度,然後在像素不差的(太暗或太亮)所有圖像上對亮度值取平均。我們可以對所有像素進行這樣的處理,並通過對「好」像素進行平均來獲得所有像素的單張圖像。
但是 CRF 不是線性的, 我們需要評估 CRF 把圖像強度變成線性,然後才能合併或者平均它們。
好消息是,如果我們知道每個圖像的曝光時間,則可以從圖像估計 CRF。 與計算機視覺中的許多問題一樣,找到 CRF 的問題本質是一個最優解問題,其目標是使由數據項和平滑項組成的目標函數最小化。 這些問題通常會降維到線性最小二乘問題,這些問題可以使用 奇異值分解(Singular Value Decomposition)(SVD)來解決,奇異值分解是所有線性代數包的一部分。 CRF 提取演算法的細節在從照片提取高動態範圍輻射圖這篇論文中可以找到。
使用 OpenCV 的 CalibrateDebevec
或者 CalibrateRobertson
就可以用 2 行代碼找到 CRF。本篇教程中我們使用 CalibrateDebevec
C++
// 獲取圖像響應函數 (CRF)Mat responseDebevec;Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();calibrateDebevec->process(images, responseDebevec, times);
Python
# 獲取圖像響應函數 (CRF)calibrateDebevec = cv2.createCalibrateDebevec()responseDebevec = calibrateDebevec.process(images, times)
下圖顯示了使用紅綠藍通道的圖像提取的 CRF。
Camera Response Function
第 4 步:合併圖像
一旦 CRF 評估結束,我們可以使用 MergeDebevec
將曝光圖像合併成一個HDR圖像。 C++ 和 Python 代碼如下所示。
C++
// 將圖像合併為HDR線性圖像Mat hdrDebevec;Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();mergeDebevec->process(images, hdrDebevec, times, responseDebevec);// 保存圖像imwrite("hdrDebevec.hdr", hdrDebevec);
Python
# 將圖像合併為HDR線性圖像mergeDebevec = cv2.createMergeDebevec()hdrDebevec = mergeDebevec.process(images, times, responseDebevec)# 保存圖像cv2.imwrite("hdrDebevec.hdr", hdrDebevec)
上面保存的 HDR 圖像可以在 Photoshop 中載入並進行色調映射。示例圖像如下所示。
HDR Photoshop 色調映射
第 5 步:色調映射
現在我們已經將我們的曝光圖像合併到一個 HDR 圖像中。 你能猜出這個圖像的最小和最大像素值嗎? 對於黑色條件,最小值顯然為 0。 理論最大值是什麼? 無限大! 在實踐中,不同情況下的最大值是不同的。 如果場景包含非常明亮的光源,那麼最大值就會非常大。
儘管我們已經使用多個圖像恢復了相對亮度信息,但是我們現在又面臨了新的挑戰:將這些信息保存為 24 點陣圖像用於顯示。
將高動態範圍(HDR)圖像轉換為 8 位單通道圖像的過程稱為色調映射。這個過程的同時還需要保留儘可能多的細節。
有幾種色調映射演算法。 OpenCV 實現了其中的四個。 要記住的是沒有一個絕對正確的方法來做色調映射。 通常,我們希望在色調映射圖像中看到比任何一個曝光圖像更多的細節。 有時色調映射的目標是產生逼真的圖像,而且往往是產生超現實圖像的目標。 在 OpenCV 中實現的演算法傾向於產生現實的並不那麼生動的結果。
我們來看看各種選項。 以下列出了不同色調映射演算法的一些常見參數。
- 伽馬(gamma):該參數通過應用伽馬校正來壓縮動態範圍。 當伽馬等於 1 時,不應用修正。 小於 1 的伽瑪會使圖像變暗,而大於 1 的伽馬會使圖像變亮。
- 飽和度(saturation):該參數用於增加或減少飽和度。 飽和度高時,色彩更豐富,更濃。 飽和度值接近零,使顏色逐漸消失為灰度。
- 對比度(contrast):控制輸出圖像的對比度(即
log(maxPixelValue/minPixelValue)
)。
讓我們來探索 OpenCV 中可用的四種色調映射演算法。
Drago 色調映射
Drago 色調映射的參數如下所示:
createTonemapDrago(float gamma = 1.0f,float saturation = 1.0f,float bias = 0.85f )
這裡,bias
是 [0, 1]
範圍內偏差函數的值。 從 0.7 到 0.9 的值通常效果較好。 默認值是 0.85。 有關更多技術細節,請參閱這篇論文。
C++ 和 Python 代碼如下所示。 參數是通過反覆試驗獲得的。 最後的結果乘以 3 只是因為它給出了最令人滿意的結果。
C++
// 使用Drago色調映射演算法獲得24位彩色圖像Mat ldrDrago;Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);tonemapDrago->process(hdrDebevec, ldrDrago);ldrDrago = 3 * ldrDrago;imwrite("ldr-Drago.jpg", ldrDrago * 255);
Python
# 使用Drago色調映射演算法獲得24位彩色圖像tonemapDrago = cv2.createTonemapDrago(1.0, 0.7)ldrDrago = tonemapDrago.process(hdrDebevec)ldrDrago = 3 * ldrDragocv2.imwrite("ldr-Drago.jpg", ldrDrago * 255)
結果如下:
使用Drago演算法的HDR色調映射
Durand 色調映射
Durand 色調映射的參數如下所示:
createTonemapDurand ( float gamma = 1.0f, float contrast = 4.0f, float saturation = 1.0f, float sigma_space = 2.0f, float sigma_color = 2.0f );
該演算法基於將圖像分解為基礎層和細節層。 使用稱為雙邊濾波器的邊緣保留濾波器來獲得基本層。 sigma_space
和sigma_color
是雙邊濾波器的參數,分別控制空間域和彩色域中的平滑量。
有關更多詳細信息,請查看這篇論文。
C++
// 使用Durand色調映射演算法獲得24位彩色圖像Mat ldrDurand;Ptr<TonemapDurand> tonemapDurand = createTonemapDurand(1.5,4,1.0,1,1);tonemapDurand->process(hdrDebevec, ldrDurand);ldrDurand = 3 * ldrDurand;imwrite("ldr-Durand.jpg", ldrDurand * 255);
Python
# 使用Durand色調映射演算法獲得24位彩色圖像 tonemapDurand = cv2.createTonemapDurand(1.5,4,1.0,1,1) ldrDurand = tonemapDurand.process(hdrDebevec) ldrDurand = 3 * ldrDurand cv2.imwrite("ldr-Durand.jpg", ldrDurand * 255)
結果如下:
使用Durand演算法的HDR色調映射
Reinhard 色調映射
createTonemapReinhard(float gamma = 1.0f,float intensity = 0.0f,float light_adapt = 1.0f,float color_adapt = 0.0f )
intensity
參數應在 [-8, 8]
範圍內。 更高的亮度值會產生更明亮的結果。 light_adapt
控制燈光,範圍為 [0, 1]
。 值 1 表示僅基於像素值的自適應,而值 0 表示全局自適應。 中間值可以用於兩者的加權組合。 參數 color_adapt
控制色彩,範圍為 [0, 1]
。 如果值被設置為 1,則通道被獨立處理,如果該值被設置為 0,則每個通道的適應級別相同。中間值可以用於兩者的加權組合。
有關更多詳細信息,請查看這篇論文。
C++
// 使用Reinhard色調映射演算法獲得24位彩色圖像Mat ldrReinhard;Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0,0,0);tonemapReinhard->process(hdrDebevec, ldrReinhard);imwrite("ldr-Reinhard.jpg", ldrReinhard * 255);
Python
# 使用Reinhard色調映射演算法獲得24位彩色圖像tonemapReinhard = cv2.createTonemapReinhard(1.5, 0,0,0)ldrReinhard = tonemapReinhard.process(hdrDebevec)cv2.imwrite("ldr-Reinhard.jpg", ldrReinhard * 255)
結果如下:
使用Reinhard演算法的HDR色調映射
Mantiuk 色調映射
createTonemapMantiuk( float gamma = 1.0f,float scale = 0.7f,float saturation = 1.0f )
參數 scale
是對比度比例因子。 從 0.7 到 0.9 的值通常效果較好
有關更多詳細信息,請查看這篇論文。
C++
// 使用Mantiuk色調映射演算法獲得24位彩色圖像Mat ldrMantiuk;Ptr<TonemapMantiuk> tonemapMantiuk = createTonemapMantiuk(2.2,0.85, 1.2);tonemapMantiuk->process(hdrDebevec, ldrMantiuk);ldrMantiuk = 3 * ldrMantiuk;imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255);
Python
# 使用Mantiuk色調映射演算法獲得24位彩色圖像tonemapMantiuk = cv2.createTonemapMantiuk(2.2,0.85, 1.2)ldrMantiuk = tonemapMantiuk.process(hdrDebevec)ldrMantiuk = 3 * ldrMantiukcv2.imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255)
結果如下:
使用Mantiuk演算法的HDR色調映射
訂閱然後下載代碼
如果你喜歡這篇文章,並希望下載本文中使用的代碼(C++ 和 Python)和示例圖片,請訂閱我們的電子雜誌。 您還將獲得免費的計算機視覺資源指南。 在我們的電子雜誌中,我們分享了用 C++ 還有 Python 編寫的 OpenCV 教程和例子,以及計算機視覺和機器學習的演算法和新聞。
點此訂閱
圖片致謝
本文中使用的四個曝光圖像獲得 CC BY-SA 3.0 許可,並從維基百科的 HDR 頁面下載。 圖像由 Kevin McCoy拍攝。
作者簡介:
我是一位熱愛計算機視覺和機器學習的企業家,擁有十多年的實踐經驗(還有博士學位)。
2007 年,在完成博士學位之後,我和我的顧問 David Kriegman 博士還有 Kevin Barnes 共同創辦了 TAAZ 公司。 我們的計算機視覺和機器學習演算法的可擴展性和魯棒性已經經過了試用了我們產品的超過 1 億的用戶的嚴格測試。
via: http://www.learnopencv.com/high-dynamic-range-hdr-imaging-using-opencv-cpp-python/
作者:SATYA MALLICK 譯者:Flowsnow 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出
推薦閱讀:
※黑夜給我了黑色眼睛:OpenCV識別黑色矩形
※Dlib在VS2015上的編譯和配置(人臉檢測人臉識別比OpenCV更好用)
※[171031] Python OpenCV 圖像處理專欄開通啦
※OpenCV機器學習——支持向量機SVM
※opencv和pcl的區別?