以下將介紹五種不同的影像處理方式:
add noise, color reduction, image enhancement,
image addition, and remapping
作業環境:
Windows 11
OpenCV 4.6.0
Visual Studio 2022
雜訊產生於傳遞過程中受到干擾。salt-and-pepper noise 是其中一種雜訊,當傳遞時受到干擾,影像便會隨機且以極值的方式呈現白點(0)或黑點(255)的雜訊,畫面似被撒上胡椒和鹽,故稱之salt-and-pepper noise,白色點常出現於過曝時,而夜間拍攝則常出現黑點。
以下模擬將圖片加上salt-and-pepper noise,以模擬圖片受到雜訊干擾的情形。我將程式碼拆為三部分來呈現,分別為:salt (將雜訊加於彩色照片中)、salt2 (將雜訊加於灰階照片中)、main (主函式)。
輸入參數:
cv:Mat image
//將彩色照片以array存取,且彩色照片是以8 bit per channel 的形式呈現,意指R、G、B三種元素各使用8 bit(0~255)存取。
int n
//設定欲產生n個亂數雜訊點
程式碼:
首先,使用 std::uniform_int_distribution產生亂數,先判斷圖片的大小,並產生小於照片大小的亂數,且區間內選定的亂數值機率均相同。
接下來,設定i, j 兩個變數分別用來存取橫軸(row)和縱軸(column)的亂數,k用作計數器,用來控制迭代的圈數需小於n。
接著,判斷圖片是灰階還是彩色照片。
最後,將i, j所選到的pixel替換成白點(255)。
作者提供了第二種加入salt-and-pepper noise的方法,和上一種方法不同的是,僅能用在灰階相片。
輸入參數:
cv:Mat image //將照片以array存取,以灰階 的形式呈現
int n //設定欲產生n個亂數雜訊點
程式碼:
第二種方法,基本上使用的想法跟上一種方法很像。
首先先以assert() 來判斷,若圖片類型是灰階則,程式執行,若判斷是呈現false則會產生錯誤訊息,並終止執行。
接著,i, j 兩個變數分別用來存取row、column的亂數,k用作計數器,用來控制迭代的圈數需小於n
最後,將i, j所選到的pixel替換成白點(255)。
我加入了前五行的程式碼,為了呈現還未加入雜訊前的照片,以方便最後相互對照分析模擬結果。
使用cv::Mat image = cv::imread();載入要改寫的圖片,有兩個參數加入,引號內填入圖片的路徑,第二個寫照片的資料型態。在模擬的過程中,一開始我認為相片為灰階或是彩色是於副函是中做更改,但我在opencv的網站找的imread()所對應的資料型態,如下:
cv::IMREAD_UNCHANGED = -1,
cv::IMREAD_GRAYSCALE = 0,
cv::IMREAD_COLOR = 1,
可以了解到,是在讀取資料時就讓照片以灰階的形式來儲存。
程式碼的主要目的是,將圖片載入,接著進到副迴圈裡加入雜訊,最後輸出圖片結果。
我更改了一些參數
salt(image, 30000);
salt2(image, 500);
將兩組參數的差距拉大,以便後續分析結果。
以下為模擬結果,可以觀察到加入salt-and-pepper noise 效果,若將參數n數值設定很大,圖片會佈滿了白點,若將n值設定數值很小,僅會有零星小點在圖片上。
原相片
cv::imshow("pure_Image", pure);
使用第一種方法加入30000個雜訊點
salt(image, 30000);
將相片轉為灰階
使用第二種方法加入500個雜訊點
salt2(image, 500);
color reduction色彩縮減,是為了減少色彩總量,以達到壓縮的效果,以減少硬體的消耗。或是有些圖片再特出的處理器下可以額外增加一些數值,例如增加代表透明程度,但除非特殊處理否則這額外增加的pixel不會被儲存或是被呈現出來,那顯示出來的的圖片會有色彩縮減的效果。
以下介紹了十五種不同color reduction的方法,因為方法很多,所以我挑選速度最快的兩支副函式和一支時間所需時間最長的副函式做說明。
colorReduce9 使用迭代的方式來描述。
作者使用cv::MatIterator_<cv::Vec3b>迭代器來對像數點做更動。使用.begin和.end來抓取圖片的第一個和最後一個像數點位置,再依序對每個pixel做更動。
我為了觀察這支程式對這張圖顏色縮減的情形,所修改了一行如下
(*it)[2] = (*it)[1] / div * div + div / 2;
僅更動紅色channel,效果如下圖,可以發現畫面偏紅色。
原圖
*it=*it/div*div+offset;
(*it)[2]=(*it)[1]/div*div+div/2;
colorReduce13使用以arithmetic shaft operation
欲將資料除以64,還可以從計算機架構來思考,除以6相當於將資料往左shift 6, 意思如下:
mask =(0xFF) = 1111 1111
shift 6 後 = 1100 0000
我想測試mask的效果,所以將程式碼改為
uchar mask = 0xFF << 7;
結果如下,將三張圖做比較可以發現,shift的值越大,意思是除越大的數,圖片裡的顏色越單一,顏色縮減的情況越顯著。
原圖
uchar mask = 0xFF << n; //n=6
uchar mask = 0xFF << 7;
colorReduce10 使用look up table
首先,先建立一個table,使用.at() 直接改變矩陣裡的值。
有了table,使用LUT(output, table, input),先前建立好的table就像是input array 和output array的關係圖,將input array映射到output array。
因為此實驗是模擬顏色縮減,所以我挑選的圖片的顏色是以漸層的方式慢慢變化的,在做完顏色縮減後,會容易觀察到顏色變一個區塊一個區塊的樣子。由此模擬可以理解到,一種效果會有很多種的寫法,作者加入了getTickCount()來計算每種方法所需的時間,以比較每種做法在速度方面的優劣程度。
下圖和右圖均為實驗結果,但可以發現每一次執行所需的秒數不同。
接著為解決每次執行所需時間不同的問題,作者在迴圈內每跑一次就打印紀錄一次(如左圖),可以知道15種副函式都執行且每種跑10次。雖然每一次所需秒數不同,但執行多次後,每種方式速度上的差異就可以判斷出來。右圖為其中一次的執行結果。
欲清楚比較速度的快慢,我將實驗結果製作成圖表如下,從中可以知道速度最快的是colorReduce14(look-up table),所需最多時間的則是colorReduce(Vec3b iterators)。
模擬圖片做銳化(sharpen)後的效果,經過sharpen 的圖片會變得黑白分明,顏色看起來很對比。主要原理是將圖片和kernel做convolution,但做完convolution後,一些pixel值會小於0或是大於255,所以最後還要將小於0的值改為0,大於255的值改為255。
以下作者提供三種不同的方法我將程式碼分為五部分分別敘述,模擬所使用的kernel如右圖所示,最後比較三種方法的所花費的時間。
在了解程式碼之前,需先分析和kernel做convolution後的結果,如右圖所示。
在解讀程式碼的時候,我不能理解
for (int i = nchannels; i < (image.cols - 1) * nchannels; i++)
所描述的意思,所以我找了一張圖來協助我理解如右圖,每一次計數需跳一組BGR。
首先先判斷影像中每個pixel有幾個channel。
這個副函式主要使用pointer來撰寫,因此先設定三個unsigned char的pointer,來描述橫軸(row)。
接著,要進入第二個迴圈,使用i來計數縱軸(column),以計算convolution,描述的方式如 PART1所提及的,再將小於0的值改為0,大於255的值改為255。
最後,將沒有改變的pixel填0。
第二種方式,和前一種不同的部分是,並非使用pointer來寫,使用一般迭代的方式,且這個方式限定用在灰階圖片上,和前一個相比,敘述方式本支副函式比較直觀,。
首先,使用CV_Assert()畔對圖片是否為灰階,如果true 則繼續執行,false則報錯結束程式。
定義參數,若和PART1所描述的相互對照,則B=itup, D=i-1, E=i, F=i+1, H=itdown。
接著進入迴圈做convolution,計算方式如PART1所提及的,然後將小於0的值改為0,大於255的值改為255。
最後再將未改變得pixel定為0。
這部分的副函式式直接使用kernel來描述。
使用 .at() 直接更動,後方向量所指向的矩陣裡面的數值,和前面兩種方式相比程式碼更精簡易懂,因此我用這支副函式來測試,當我改變kernel的數值時會對圖片帶來什麼改變,如下圖。
由以下四張圖相互比較可以發現,若中心點的數值越大,整體顏色會偏白,欲將圖片整體顏色調黑,則將數值低。
原圖
kernel.at<float>(1, 1) = 5.0;
kernel.at<float>(0, 1) = -1.0;
kernel.at<float>(2, 1) = -1.0;
kernel.at<float>(1, 0) = -1.0;
kernel.at<float>(1, 2) = -1.0;
kernel.at<float>(1, 1) = 6.0;
kernel.at<float>(0, 1) = -1.0;
kernel.at<float>(2, 1) = -1.0;
kernel.at<float>(1, 0) = -1.0;
kernel.at<float>(1, 2) = -1.0;
kernel.at<float>(1, 1) = 5.0;
kernel.at<float>(0, 1) = -1.2;
kernel.at<float>(2, 1) = -1.2;
kernel.at<float>(1, 0) = -1.2;
kernel.at<float>(1, 2) = -1.2;
我加入了前五行的程式碼,為了呈現原始照片,以方便最後相互對照分析模擬結果。
首先,使用cv::imread()並加入欲讀取的照片的路徑。因為為模擬sharpen,所以我挑選的顏色變化大且線條感比較明顯的照片,較容易看出差異。
接著在副函式前後使用cv::getTickCount()來抓取經過幾個clock,接著再除以cv::getTickFrequency(),以計算花費多少時間。
以下為模擬結果,且每個副函式所花費的時間依序為:使用pointer= 0.0133757,使用iterator=0.149346,使用kernel=0.128079。由實驗結果可以了解到使用pointer來撰寫程式,所花的時間最短,接著是使用kernel,最慢的是使用iterator。
原圖
pointer
time= 0.0133757
iterator
time = 0.149346
kernel
time = 0.128079
欲將兩張圖合成一張,使用的原理是,將兩張圖對應的pixel乘上倍率後相加,因此須注意兩張圖大小必須相同,倍率用以調整圖片的亮度,於底下模擬可觀察之。作者提供三種不同的合成方式,因此我分為三部分分別分析。
以下模擬圖片來源: https://www.laihao.com.tw/blog/posts/4-recommend-pattern-glass-designs-stationery
首先,先創建兩個矩陣,並使用imread()分別將兩張圖存入,中間填入要合成的圖片路徑。
使用 .data()指向image1和image2內第一個元素,確認兩矩陣裡有數值,若無數值則結束程式,若有數值則繼續執行。
接著,使用addWeighted()來合成圖片,因為原程式碼使用的倍率效果不佳,所以我調整倍率,讓合成後的圖片能清出辨認出是由兩張圖合成的。addWeighted()的操作方式如下:
result = 0.8*image1 + 0.4*image2
最後,將模擬結果顯示出來。
第二種方式,並沒有使用合成的function,而是以直觀的方式直接乘倍數後相加。
這部分作者欲呈現channel的合成方式。
首先,先將創建向量,大小為容得下圖片的三個channel。
接著,使用split()分離三個channel並依BGR的順序存入向量中,並將image2和image1的藍色channel合成,並將結果顯示出來。
最後,使用merge(),將三張1-channel合成一張3-channel
因為,一開始不清楚planes的存放情形,所以我做了以下測試協助我理解,可以了解到planes是以BGR的順序來存放資料。
planes[0] += image2;
planes[1] += image2;
planes[2] += image2;
本次模擬,需注意的圖片的大小,所以找到的圖都須經過裁切確保大小一致才能開始模擬。我將右側兩張圖乘上不同的倍率後相加,結果如下。
result = 0.8*image1+0.4*image2
result = 0.4*image1+0.5*image2
可以觀察到倍率越大圖片的亮度就越大,在合成後倍率大的圖層就比較明顯。在挑選圖片的過程,嘗試了很多種圖片,發現其中一張的顏色單調且僅有線條像是圖騰的圖片效果最佳。
remapping的原理是,利用改變圖片中pixel的位置但不改變每個pixel的值,來達到扭曲照片的效果。以下將以這個概念來將照片扭曲,左者提供兩種方法,我將分為兩部分來解析。
因為會使用到cos(),所以需加入函式庫<math.h>
首先,於主函式中先以灰階的資料型態載入照片,於imread()寫入欲處理的圖片路徑,將照片顯示於螢幕上以方便之後做對照分析。
接著進入副函式處理照片。先創建兩個矩陣,分別儲存圖片的橫軸(row)和縱軸(column)座標,資料型態為32bit的浮點數。
使用.at()直接更動(i, j)所對應的位置裡的數值,我將數值調大以方便觀察結果。數值調整為:橫軸(row)不變,將縱軸(column)平移 i 後加上5*cos(j/6.0)
使用remap()將原作標裡的pixel值,移到改變後的座標點,且是使用線性差值法(linear interpolation) 。
最後,將結果現示於螢幕上。
在主函式一開始載入照片時,後方的資料型態填 1代表,以彩色的方式載入照片。
在副函式中,進入到for迴圈後,將橫軸(row)做 image.cols-j-50,將照片左右翻轉,接著向左平移50個像數點,我有調整數值,以方便觀察。
接下來,和上一個部分相同使用remap()將原作標裡的pixel值移到改變後的座標點。
最後,將結果現示於螢幕上。
以下為模擬結果,和第一張員圖做比較可以觀察到,中間的圖以cos()的方式被扭曲畫面呈現波浪狀且色彩為灰階,我將參數調整比作者原先設定的還大,可以發現圖片更為扭曲,右邊的圖左右相反、往左平移了一段並以黑色(0)填補畫面。
8-Bit vs 16-Bit Images , MADHU MANICKAM, LAST UPDATED ON APRIL 22, 2021
漫談 std::uniform_int_distribution 的原理與最佳化 , 14 August 2022
Flags used for image file reading and writing
opencv中Assert()函数, 2018-01-29 11
使用 Matiterator 替代畫素訪問 , November-22, 2018
【OpenCV】06 Look Up Table(LUT)查找表 , 2020-03-01
OpenCV C++ 之 LUT 函數介紹 , May 1, 2020
https://web.ntnu.edu.tw/~algo/Image.html#1
opencv图像像素操作方法 , 2017-01-08
opencv学习笔记之Mat::at , 2014-07-29
https://docs.opencv.org/3.4/d5/dc4/tutorial_adding_images.html
【沒錢ps,我用OpenCV!】Day 14 - 進階修圖1,運用 OpenCV 顯示圖片直方圖、分離與合併RGB通道 show histogram, split, merge RGB channel , 2020-09-26
【c++】OpenCV型別CV_32F和CV_32FC1之間的區別 , 2020-10-26