1、灰度變換的基本概念
灰度變換指對圖像的單個像素進行操作,主要以對比度和閾值處理為目的。其變換形式如下:
s=T(r)
其中,T是灰度變換函數;r是變換前的灰度;s是變換后的像素。
圖像灰度變換的有以下作用:
- 改善圖像的質量,使圖像能夠顯示更多的細節,提高圖像的對比度(對比度拉伸)
- 有選擇的突出圖像感興趣的特征或者抑制圖像中不需要的特征
- 可以有效的改變圖像的直方圖分布,使像素的分布更為均勻
2常用的灰度變換說明
- 線性函數 (圖像反轉)
- 對數函數:對數和反對數變換
- Gamma變換:n次冪和n次開方變換
- 分段線性變換
3、線性變換
其中,a為直線的斜率,b為在y軸的截距。選擇不同的a,b值會有不同的效果:
- a>1,增加圖像的對比度
- a<1,減小圖像的對比度
- a=0,b!=0,圖像變亮或變暗
- a<0且b=0,圖像的亮區域變暗,暗區域變亮
- a=-1,b=255,圖像亮度反轉
OpenCV的實現如下:
灰度圖實現:
for (int i = 0; i < srcImg.rows; i++) { uchar *srcData = srcImg.ptr<uchar>(i); for (int j = 0; j < srcImg.cols; j++) { dstImg.at<uchar>(i, j) = srcData[j] * k + b; } }
彩色圖的實現只需拓展到三通道即可:
for (int i = 0; i < RowsNum; i++) { for (int j = 0; j < ColsNum; j++) { //c為遍歷圖像的三個通道 for (int c = 0; c < 3; c++) { //使用at操作符,防止越界 dstImg.at<Vec3b>(i, j)[c] = saturate_cast<uchar> (k* (srcImg.at<Vec3b>(i, j)[c]) + b); } } }
示例:
#include "stdafx.h" #include <iostream> #include <opencv2\core\core.hpp> #include <opencv2\highgui\highgui.hpp> #include <opencv2\imgproc\imgproc.hpp> using namespace std; using namespace cv; int main() { Mat srcImg = imread("rice.png",0); if (!srcImg.data) { cout << "讀入圖片失敗" << endl; return -1; } double k, b; cout << "請輸入k和b值:"; cin >> k >> b; int RowsNum = srcImg.rows; int ColsNum = srcImg.cols; Mat dstImg(srcImg.size(), srcImg.type()); //進行遍歷圖像像素,對每個像素進行相應的線性變換 for (int i = 0; i < srcImg.rows; i++) { uchar *srcData = srcImg.ptr<uchar>(i); for (int j = 0; j < srcImg.cols; j++) { dstImg.at<uchar>(i, j) = srcData[j] * k + b; } } imshow("原圖", srcImg); imshow("線性變換后的圖像", dstImg); waitKey(); return 0; }
程序運行結果如下:
4、對數變換
對數變換的通用公式是:
s=log(1+r)/b
其中,b是一個常數,用來控制曲線的彎曲程度,其中,b越小越靠近y軸,b越大越靠近x軸。表達式中的r是原始圖像中的像素值,s是變換后的像素值,可以分析出,當函數自變量較低時,曲線的斜率很大,而自變量較高時,曲線的斜率變得很小。 正是因為對數變
換具有這種壓縮數據的性質,使得它能夠實現圖像灰度拓展和壓縮的功能。即對數變換可以拓展低灰度值而壓縮高灰度級值,讓圖像的灰度分布更加符合人眼的視覺特征。
假設r≥0,根據上圖中的對數函數的曲線可以看出:對數變換,將源圖像中范圍較窄的低灰度值映射到范圍較寬的灰度區間,同時將范圍較寬的高灰度值區間映射為較窄的灰度區間,從而擴展了暗像的值,壓縮了高灰度的值,能夠對圖像中低灰度細節進行增
強。;從函數曲線也可以看出,反對數函數的曲線和對數的曲線是對稱的,在應用到圖像變換其結果是相反的,反對數變換的作用是壓縮灰度值較低的區間,擴展高灰度值的區間。
通過OpenCV實現程序有三種,這里就不一一列舉了,在示例中給出:
示例:
#include "stdafx.h" #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream> using namespace cv; // 對數變換方法1 cv::Mat logTransform1(cv::Mat srcImage, int c) { // 輸入圖像判斷 if (srcImage.empty()) std::cout << "No data!" << std::endl; cv::Mat resultImage = cv::Mat::zeros(srcImage.size(), srcImage.type()); // 計算 1 + r cv::add(srcImage, cv::Scalar(1.0), srcImage); // 轉換為32位浮點數 srcImage.convertTo(srcImage, CV_32F); // 計算 log(1 + r) log(srcImage, resultImage); resultImage = c * resultImage; // 歸一化處理 cv::normalize(resultImage, resultImage, 0, 255, cv::NORM_MINMAX); cv::convertScaleAbs(resultImage, resultImage); return resultImage; } // 對數變換方法2 cv::Mat logTransform2(Mat srcImage, float c) { // 輸入圖像判斷 if (srcImage.empty()) std::cout << "No data!" << std::endl; cv::Mat resultImage = cv::Mat::zeros(srcImage.size(), srcImage.type()); double gray = 0; // 圖像遍歷分別計算每個像素點的對數變換 for (int i = 0; i < srcImage.rows; i++) { for (int j = 0; j < srcImage.cols; j++) { gray = (double)srcImage.at<uchar>(i, j); gray = c * log((double)(1 + gray)); resultImage.at<uchar>(i, j) = saturate_cast<uchar>(gray); } } // 歸一化處理 cv::normalize(resultImage, resultImage, 0, 255, cv::NORM_MINMAX); cv::convertScaleAbs(resultImage, resultImage); return resultImage; } // 對數變換方法3 cv::Mat logTransform3(Mat srcImage, float c) { // 輸入圖像判斷 if (srcImage.empty()) std::cout << "No data!" << std::endl; cv::Mat resultImage = cv::Mat::zeros(srcImage.size(), srcImage.type()); srcImage.convertTo(resultImage, CV_32F); resultImage = resultImage + 1; cv::log(resultImage, resultImage); resultImage = c * resultImage; cv::normalize(resultImage, resultImage, 0, 255, cv::NORM_MINMAX); cv::convertScaleAbs(resultImage, resultImage); return resultImage; } int main() { // 讀取灰度圖像及驗證 cv::Mat srcImage = cv::imread("111.jpg", 0); if (!srcImage.data) return -1; // 驗證三種不同方式的對數變換速度 cv::imshow("srcImage", srcImage); float c = 2; cv::Mat resultImage; double tTime; tTime = (double)getTickCount(); const int nTimes = 10; for (int i = 0; i < nTimes; i++) { resultImage = logTransform3(srcImage, c); } tTime = 1000 * ((double)getTickCount() - tTime) / getTickFrequency(); tTime /= nTimes; std::cout << "第三種方法耗時:" << tTime << std::endl; cv::imshow("resultImage", resultImage); cv::waitKey(0); return 0; }
三種方法運行效果分別如下:
5、伽馬變換
基於冪次變換的Gamma校正是圖像處理中一種非常重要的非線性變換,它與對數變換相反,它是對輸入圖像的灰度值進行指數變換,進而校正亮度上的偏差。通常Gamma校正長應用於拓展暗調的細節。伽馬變換的公式為:
s=crγ
其中c和 γ為正常數.,伽馬變換的效果與對數變換有點類似,當 γ >1時將較窄范圍的低灰度值映射為較寬范圍的灰度值,同時將較寬范圍的高灰度值映射為較窄范圍的灰度值;當 γ <1時,情況相反,與反對數變換類似。其函數曲線如下:
當γ<1時,圖像的高光部分被擴展而暗調備份被壓縮,γ的值越小,對圖像低灰度值的擴展越明顯;當γ>1時,圖像的高光部分被壓縮而暗調部分被擴展,γ的值越大,對圖像高灰度值部分的擴展越明顯。這樣就能夠顯示更多的圖像的低灰度或者高灰度細節。
- 當γ<1時,低灰度區域動態范圍擴大,進而圖像對比度增強,高灰度值區域動態范圍減小,圖像對比度降低,圖像整體灰度值增大,此時與圖像的對數變換類似。
- γ>1時,低灰度區域的動態范圍減小進而對比度降低,高灰度區域動態范圍擴大,圖像的對比度提升,圖像的整體灰度值變小,Gamma校正主要應用在圖像增強。
總之,r<1的冪函數的作用是提高圖像暗區域中的對比度,而降低亮區域的對比度;r>1的冪函數的作用是提高圖像中亮區域的對比度,降低圖像中按區域的對比度。所以Gamma變換主要用於圖像的校正,對於灰度級整體偏暗的圖像,可以使用r<1的冪函數增大動態范圍。對於灰度級整體偏亮的圖像,可以使用r>1的冪函數增大灰度動態范圍。
基於OpenCV的實現:
Mat GammaTrans(Mat &srcImag, float parameter) { //建立查表文件LUT unsigned char LUT[256]; for (int i = 0; i < 256; i++) { //Gamma變換定義 LUT[i] = saturate_cast<uchar>(pow((float)(i / 255.0), parameter)*255.0f); } Mat dstImage = srcImag.clone(); //輸入圖像為單通道時,直接進行Gamma變換 if (srcImag.channels() == 1) { MatIterator_<uchar>iterator = dstImage.begin<uchar>(); MatIterator_<uchar>iteratorEnd = dstImage.end<uchar>(); for (; iterator != iteratorEnd; iterator++) *iterator = LUT[(*iterator)]; } else { //輸入通道為3通道時,需要對每個通道分別進行變換 MatIterator_<Vec3b>iterator = dstImage.begin<Vec3b>(); MatIterator_<Vec3b>iteratorEnd = dstImage.end<Vec3b>(); //通過查表進行轉換 for (; iterator!=iteratorEnd; iterator++) { (*iterator)[0] = LUT[((*iterator)[0])]; (*iterator)[1] = LUT[((*iterator)[1])]; (*iterator)[2] = LUT[((*iterator)[2])]; } } return dstImage; }
示例:
#include "stdafx.h" #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/imgproc/types_c.h> #include <opencv2/highgui/highgui.hpp> #include <opencv2/highgui/highgui_c.h> #include <iostream> using namespace cv; using namespace std; void MyGammaCorrection(Mat& src, Mat& dst, float fGamma) { // build look up table unsigned char lut[256]; for (int i = 0; i < 256; i++) { lut[i] = saturate_cast<uchar>(pow((float)(i / 255.0), fGamma) * 255.0f); } dst = src.clone(); const int channels = dst.channels(); switch (channels) { case 1: //灰度圖的情況 { MatIterator_<uchar> it, end; for (it = dst.begin<uchar>(), end = dst.end<uchar>(); it != end; it++) //*it = pow((float)(((*it))/255.0), fGamma) * 255.0; *it = lut[(*it)]; break; } case 3: //彩色圖的情況 { MatIterator_<Vec3b> it, end; for (it = dst.begin<Vec3b>(), end = dst.end<Vec3b>(); it != end; it++) { //(*it)[0] = pow((float)(((*it)[0])/255.0), fGamma) * 255.0; //(*it)[1] = pow((float)(((*it)[1])/255.0), fGamma) * 255.0; //(*it)[2] = pow((float)(((*it)[2])/255.0), fGamma) * 255.0; (*it)[0] = lut[((*it)[0])]; (*it)[1] = lut[((*it)[1])]; (*it)[2] = lut[((*it)[2])]; } break; } } } int main() { Mat image = imread("111.jpg"); if (image.empty()) { cout << "Error: Could not load image" << endl; return 0; } Mat dst; float fGamma = 1 / 2.2; MyGammaCorrection(image, dst, fGamma); imshow("Source Image", image); imshow("Dst", dst); waitKey(); return 0; }
程序運行結果如下:
6 分段線性變換
6.1、對比度拉伸技術
圖像的對比度拉伸是通過擴展圖像灰度級動態范圍來實現的,它可以擴展對應的全部灰度范圍。圖像的低對比度一般是由於圖像圖像成像亮度不夠、成像元器件參數限制或設置不當造成的。提高圖像的對比度可以增強圖像各個區域的對比效果,對圖像中感興趣的區域進行增強,而對圖像中不感興趣的區域進行相應的抑制作用。對比度拉伸是圖像增強中的重要的技術之一。這里設點(x1,y1)與(x2,y2)是分段線性函數中折點位置坐標。常見的三段式分段線性變換函數的公式如下:
其中
其圖像如下:
需要注意的是,分段線性一般要求函數是單調遞增的,目的是防止圖像中的灰度級不滿足一一映射。
示例:
#include "stdafx.h" #include <iostream> #include <opencv2\core\core.hpp> #include <opencv2\highgui\highgui.hpp> #include <opencv2\imgproc\imgproc.hpp> using namespace cv; using namespace std; int main() { Mat srcImage = imread("111.jpg", 0); if (!srcImage.data) { cout << "讀入圖片錯誤!" << endl; return -1; } imshow("原始圖片", srcImage); Mat dstImage(srcImage); int rowsNum = dstImage.rows; int colsNum = dstImage.cols; //圖像連續性判斷 if (dstImage.isContinuous()) { colsNum = colsNum * rowsNum; rowsNum = 1; } //圖像指針操作 uchar *pDataMat; int pixMax = 0, pixMin = 255; //計算圖像像素的最大值和最小值 for (int j = 0; j < rowsNum; j++) { pDataMat = dstImage.ptr<uchar>(j); for (int i = 0; i < colsNum; i++) { if (pDataMat[i] > pixMax) pixMax = pDataMat[i]; if (pDataMat[i] < pixMin) pixMin = pDataMat[i]; } } //進行對比度拉伸 for (int j = 0; j < rowsNum; j++) { pDataMat = dstImage.ptr<uchar>(j); for (int i = 0; i < colsNum; i++) { pDataMat[i] = (pDataMat[i] - pixMin) * 255 / (pixMax - pixMin); } } imshow("對比度拉伸后的圖像", dstImage); waitKey(); return 0; }

6.2、 灰度級分層
灰度級分層的處理可以突出特定灰度范圍的亮度,可以應用於增強某些特征。
- 將感興趣范圍內的所有灰度值顯示位一個值(如白色),而將其他灰度值顯示為另外一個值(如黑色)。如下圖左圖所示,最后將產生一副二值圖像
- 僅僅改變感興趣范圍的灰度值,使其顯示為一個值,如下圖右圖所示。(這種方法一般用的少,這里就不詳細介紹了)
下面使用OpenCV對灰度級分層進行一個實現
示例:
#include "stdafx.h" #include <iostream> #include <opencv2\core\core.hpp> #include <opencv2\highgui\highgui.hpp> #include <opencv2\imgproc\imgproc.hpp> using namespace cv; using namespace std; int main() { Mat srcImage = imread("111.jpg", 0); if (!srcImage.data) { cout << "讀入圖片錯誤!" << endl; return 0; } imshow("原圖像", srcImage); Mat dstImage = srcImage.clone(); int rowsNum = dstImage.rows; int colsNum = dstImage.cols; //圖像連續性判斷 if (dstImage.isContinuous()) { colsNum *= rowsNum; rowsNum = 1; } //圖像指針操作 uchar *pDataMat; int controlMin = 50; int controlMax = 150; //計算圖像的灰度級分層 for (int j = 0; j < rowsNum; j++) { pDataMat = dstImage.ptr<uchar>(j); for (int i = 0; i < colsNum; i++) { //第一種方法,二值映射 if (pDataMat[i] > controlMin) pDataMat[i] = 255; else pDataMat[i] = 0; //第二種方法:區域映射 //if (pDataMat[i] > controlMax && pDataMat[j] < controlMin) // pDataMat[i] = controlMax; } } imshow("灰度分層后的圖像", dstImage); waitKey(); return 0; }
運行結果如下所示:
6.2、比特平面分層
像素是由比特組成的豎直。例如,在256級灰度圖像中,每個像素的灰度是由8bit組成,替代突出灰度級范圍,我們可以突出比特來突出整個圖像的外觀。一副8比特灰度圖可考慮分層1到8個比特平面。很容易理解的是,4個高階比特平面,特別是最后兩個比特平面,包含了在視覺上很重要的大多數數據。而低階比特平面則在圖像上貢獻了更精細的灰度細節。
示例:
#include "stdafx.h" #include<opencv2/highgui/highgui.hpp> #include<opencv2/imgproc/imgproc.hpp> #include<iostream> using namespace std; using namespace cv; int b[8]; void binary(int num) { for (int i = 0; i < 8; i++) b[i] = 0; int i = 0; while (num != 0) { b[i] = num % 2; num = num / 2; i++; } } int main() { Mat srcImage = imread("111.jpg", 0); resize(srcImage, srcImage, cv::Size(), 0.5, 0.5); Mat d[8]; for (int k = 0; k < 8; k++) d[k].create(srcImage.size(), CV_8UC1); int rowNumber = srcImage.rows, colNumber = srcImage.cols; for (int i = 0; i < rowNumber; i++) for (int j = 0; j < colNumber; j++) { int num = srcImage.at<uchar>(i, j); binary(num); for (int k = 0; k < 8; k++) d[k].at<uchar>(i, j) = b[k] * 255; } imshow("src", srcImage); for (int k = 0; k < 8; k++) { imshow("level" + std::to_string(k), d[k]); } waitKey(0); return 0; }