前面幾篇文章介紹的是圖像的空間域濾波,其對像素的處理都是基於像素的某一鄰域進行的。本文介紹的圖像的灰度變換則不一樣,其對像素的計算僅僅依賴於當前像素和灰度變換函數。
灰度變換也被稱爲圖像的點運算(只針對圖像的某一像素點)是全部圖像處理技術中最簡單的技術,其變換形式以下:
\[ s = T(r) \]
其中,T是灰度變換函數;r是變換前的灰度;s是變換後的像素。
圖像灰度變換的有如下做用:app
灰度變換函數描述了輸入灰度值和輸出灰度值之間變換關係,一旦灰度變換函數肯定下來了,那麼其輸出的灰度值也就肯定了。可見灰度變換函數的性質就決定了灰度變換所能達到的效果。用於圖像灰度變換的函數主要有如下三種:函數
上圖給出了幾種常見灰度變換函數的曲線圖,根據這幾種常見函數的曲線形狀,能夠知道這幾種變換的所能達到的效果。例如,對數變換和冪律變換都能實現圖像灰度級的擴展/壓縮,另外對數變換還有一個重要的性質,它能壓縮圖像灰度值變換較大的圖像的動態範圍(例如,傅立葉變換的頻譜顯示)。spa
令r爲變換前的灰度,s爲變換後的灰度,則線性變換的函數:
\[ s = a \cdot r + b \]
其中,a爲直線的斜率,b爲在y軸的截距。選擇不一樣的a,b值會有不一樣的效果:code
在進行圖像加強時,上述的線性變換函數用的較多的就是圖像反轉了,根據上面的參數,圖像反轉的變換函數爲:\(s = 255-s\)。圖像反轉獲得的是圖像的負片,可以有效的加強在圖像暗區域的白色或者灰色細節。其效果以下:
orm
圖像反轉的實現是比較簡單的,在OpenCV中有對Mat的運算符重載,能夠直接Mat r = 255 - img
或者~img
來實現。blog
對數變換的通用公式是:
\[s = c\log(1+r)\]
其中,c是一個常數,,假設\(r \geq 0\),根據上圖中的對數函數的曲線能夠看出:對數變換,將源圖像中範圍較窄的低灰度值映射到範圍較寬的灰度區間,同時將範圍較寬的高灰度值區間映射爲較窄的灰度區間,從而擴展了暗像素的值,壓縮了高灰度的值,可以對圖像中低灰度細節進行加強。;從函數曲線也能夠看出,反對數函數的曲線和對數的曲線是對稱的,在應用到圖像變換其結果是相反的,反對數變換的做用是壓縮灰度值較低的區間,擴展高灰度值的區間。
基於OpenCV的實現,其對數變換的代碼以下:it
float pixels[256]; for (int i = 0; i < 256; i++) pixels[i] = log(1 + i); Mat imageLog(image.size(), CV_32FC3); for (int i = 0; i<image.rows; i++) { for (int j = 0; j<image.cols; j++) { imageLog.at<Vec3f>(i, j)[0] = pixels[image.at<Vec3b>(i, j)[0]]; imageLog.at<Vec3f>(i, j)[1] = pixels[image.at<Vec3b>(i, j)[1]]; imageLog.at<Vec3f>(i, j)[2] = pixels[image.at<Vec3b>(i, j)[2]]; } } //歸一化到0~255 normalize(imageLog, imageLog, 0, 255, CV_MINMAX); //轉換成8bit圖像顯示 convertScaleAbs(imageLog, imageLog);
這使用的對數函數的底爲10。因爲灰度變換是灰度值之間的一對一的映射,而灰度值區間一般爲[0,255],因此在進行灰度變換時,一般使用查表法。也就是,現將每一個灰度值的映射後的結果計算出來,在變換時,經過查表獲得變換後的灰度值。執行上面結果獲得的結果以下:
左邊爲原圖像,其拍攝環境較暗,沒法分辨出不少的細節;右邊爲變換後的圖像,整個圖像明亮許多,也能分辨出原圖中處於暗區域的狗狗的更多細節。
對數變換,還有一個很重要的性質,可以壓縮圖像像素的動態範圍。例如,在進行傅立葉變換時,獲得的頻譜的動態範圍較大,頻譜值的範圍一般爲\([0,10^6]\),甚至更高。這樣範圍的值,顯示器是沒法完整的顯示如此大範圍的灰度值的,於是許多灰度細節會被丟失掉。而將獲得的頻譜值進行對數變換,能夠將其動態範圍變換到一個合適區間,這樣就可以顯示更多的細節。圖像處理
伽馬變換的公式爲:
\[s = cr^\gamma\]
其中c和\(\gamma\)爲正常數。
伽馬變換的效果與對數變換有點相似,當\(\gamma > 1\)時將較窄範圍的低灰度值映射爲較寬範圍的灰度值,同時將較寬範圍的高灰度值映射爲較窄範圍的灰度值;當\(\gamma < 1\)時,狀況相反,與反對數變換相似。其函數曲線以下:
opencv
當\(\gamma<1\)時,\(\gamma\)的值越小,對圖像低灰度值的擴展越明顯;當\(\gamma>1\)時,\(\gamma\)的值越大,對圖像高灰度值部分的擴展越明顯。這樣就可以顯示更多的圖像的低灰度或者高灰度細節。
伽馬變換主要用於圖像的校訂,對灰度值太高(圖像過亮)或者太低(圖像過暗)的圖像進行修正,增長圖像的對比度,從而改善圖像的顯示效果。
基於OpenCV的實現:class
float pixels[256]; for (int i = 0; i < 256; i++) pixels[i] = i * i *i; Mat imageLog(image.size(), CV_32FC3); for (int i = 0; i<image.rows; i++) { for (int j = 0; j<image.cols; j++) { imageLog.at<Vec3f>(i, j)[0] = pixels[image.at<Vec3b>(i, j)[0]]; imageLog.at<Vec3f>(i, j)[1] = pixels[image.at<Vec3b>(i, j)[1]]; imageLog.at<Vec3f>(i, j)[2] = pixels[image.at<Vec3b>(i, j)[2]]; } } //歸一化到0~255 normalize(imageLog, imageLog, 0, 255, CV_MINMAX); //轉換成8bit圖像顯示 convertScaleAbs(imageLog, imageLog);
這裏選擇的參數爲c = 1,\(\gamma = 3\),來擴展圖像的高灰度區域,其結果以下:
當選擇參數爲c = 1,\(\gamma = 0.4\),來擴展圖像的低灰度區域,其效果以下:
根據以上的結果,結合伽馬變換的函數曲線圖,作以下總結:
灰度變換屬於點對點的一一變換,在實現的時候,能夠利用查表法。也就是實現將[0,255]區間的各個灰度值的變換後的值計算出來,在變換的時候直接根據灰度值進行查表獲得變換後的結果。其實現以下:
///////////////////////////////////////////////////////////////////// // // 灰度線性變換函數 // 參數: // src,輸入原圖像 // dst,輸出圖像,類型爲CV_32F,大小及通道數與原圖像相同 // mapping,灰度映射表,能夠根據不一樣的變換函數,提早計算好圖像的灰度映射表 // //////////////////////////////////////////////////////////////////// void gray_trans(const Mat& src, Mat& dst,float* mapping) { int channels = src.channels(); if (channels == 1) { dst = Mat(src.size(), CV_32FC1); for (int i = 0; i < src.rows; i++) { float* p1 = dst.ptr<float>(i); const uchar* p2 = src.ptr<uchar>(i); for (int j = 0; j < src.cols; j++) p1[j] = mapping[p2[j]]; } } else if (channels == 3) { dst = Mat(src.size(), CV_32FC3); for (int i = 0; i < src.rows; i++) { float* p1 = dst.ptr<float>(i); const uchar* p2 = src.ptr<uchar>(i); for (int j = 0; j < src.cols * 3; j+=3) { p1[j] = mapping[p2[j]]; p1[j+1] = mapping[p2[j+1]]; p1[j+2] = mapping[p2[j+2]]; } } } }
其調用也比較簡單,根據具體的灰度變換函數,填充灰度映射表便可,以伽馬變換爲例:
float pixels[256]; for (int i = 0; i < 256; i++) pixels[i] = powf(i, 1.5); Mat imageLog; gray_trans(image, imageLog, pixels);
本文主要對圖像的幾種常見的灰度變換進行了總結。