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;
}

