空间域处理包括灰度变换和空间滤波两类,作用是用来对图像进行增强处理。所谓的增强处理是指对图像进行加工,使之对于特定的应用比原始图像更合适的一种处理。“特定”一词的限定非常重要,这决定了图像增强技术一开始就是面向问题的,没有通用的理论。
空间域指的是图像平面本身,这类图像处理技术直接操作图像中的像素。
空间域处理可以用下列表达式来表示:g(x,y) = T[ f(x,y) ],其中f(x,y)是输入图像,g(x,y)是处理后的图像,T是在点(x,y)的领域上关于f的一种算子。
先学习第一类:灰度变换。
最小领域的大小是1*1,在这种情况下,g仅仅取决于f在任意点(x,y)处的灰度值。令s和r分别表示变量,即g和f在(x,y)处的灰度值,就可以得到灰度变换函数s=T(r)。灰度变换是所有图像处理技术中最简单的一种技术。基本的灰度变换函数有:线性函数(恒等变换和反转变换)、对数函数(对数变换和反对数变换)、幂律函数(n次幂变换和n次根变换)。
一、图像反转变换
定义:对于灰度级范围为[0,L-1]的一幅图像,其反转图像可以由下列表达式给出:s=L-1-r。使用这种方式反转一幅图像的灰度级,可以得到等效的照片底片,这种类型的处理特别适用于增强嵌入图像暗色区域中的白色或者灰色细节,特别是当黑色面积在尺寸上占主导地位时。
算法实现:
#include <iostream>
#include <math.h>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage, dstImage;
srcImage = imread("E:\\code\\VS_Project\\灰度变换\\反转变换\\image.tif",0);
if (srcImage.empty())
{
cout << "load image failed" << endl;
return -1;
}
imshow("【原图】", srcImage);
dstImage = Mat(srcImage.size(), srcImage.type());
for (int i = 0; i < srcImage.rows; i++)
{
for (int j = 0; j < srcImage.cols; j++)
{
uchar value = srcImage.at<uchar>(i, j);
uchar temp = pow(2,8) - 1 - value;
dstImage.at<uchar>(i, j) = saturate_cast<uchar>(temp);
}
}
imshow("【dstImage】", dstImage);
waitKey(0);
return 0;
}
运行结果:
二、对数变换
对数变换的通用形式为s=c log(r+1),c为常数,r>=0。对数曲线的形状如下图所示。从图中可知,对数变换将范围较窄的低灰度值映射为输出中范围较宽的灰度值,将范围较宽的高灰度值映射为输出中范围较窄的灰度值。可见,对数变换的作用是扩展图像中的低灰度值,压缩图像中的高灰度值。
#include <iostream>
#include <math.h>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage, dstImage;
srcImage = imread("image.tif", 0);
if (srcImage.empty())
{
std::cout << "load image failed" << std::endl;
return -1;
}
imshow("【原图】", srcImage);
dstImage = Mat(srcImage.size(), srcImage.type());
for (int i = 0; i < srcImage.rows; i++)
{
for (int j = 0; j < srcImage.cols; j++)
{
double value = (double)srcImage.at<uchar>(i, j);
//c=1
double temp = 10*log((double)(1 + value));
dstImage.at<uchar>(i, j) = saturate_cast<uchar>(temp);
}
}
imshow("【对数转换结果】", dstImage);
waitKey(0);
return 0;
}
运行结果:
三、幂律变换(伽马变换)
幂律变换的基本形式为s=cr^x,表达式中c和x都是正常数。s和r的关系曲线如下图所示。从下图中可以看出,当r<1时,部分r值的幂律曲线能够将较窄范围的低灰度值输入值映射为较宽范围的输出值,或者将较宽范围的高灰度级输入值映射为较窄范围的输出值。幂律变换最重要的作用就是进行伽马校正、增强对比度、灰度值扩展压缩等。
代码实现:
#include <iostream>
#include <math.h>
#include <opencv2\opencv.hpp>
using namespace cv;
Mat GammaTrans(Mat &srcImage, float parameter);
int main()
{
Mat srcImage = imread("image.png", 0);
if (!srcImage.data)
{
std::cout << "读入图片失败!" << std::endl;
return -1;
}
imshow("原始图像", srcImage);
float parameter = 0.4;
Mat dstImage1 = GammaTrans(srcImage, parameter);
imshow("参数0.4下的Gamma变换", dstImage1);
waitKey();
return 0;
}
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;
}
运行结果:
四、分段线性变换
分段线性变换简而言之就是变换函数是分段函数。这没什么好解释的。直接说分段线性变换的两个应用:一个是对比度拉伸,一个是灰度级分层;还有一个是比特平面分层,暂时还没有学,日后再进行补充。
1、对比度拉伸
对比度拉伸是最简单的分段线性函数。低对比度图像是由于照片不足、成像传感器动态范围太小、图像获取过程中镜头光圈设置错误造成。对比度拉伸是扩展图像灰度级动态范围的处理,下列是一个典型的对比度拉伸变换。
当r1=r2且s1=s2时,该变换为线性变换;
当r1=r2,s1=0且s2=L-1时,该变换为阈值处理函数,输出一幅二值图像。
代码实现(实现了第二种情况):
/*
当r1=r2,s1=0,s2=L-1的时候,线性变换函数则成为阈值处理函数
*/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char **argv)
{
Mat srcImage = imread("image.tif", 0);
if (!srcImage.data)
{
std::cout << "load image failed" << std::endl;
}
imshow("【原图】", srcImage);
Mat dstImage(srcImage);
int rowsNum = dstImage.rows;
int colsNum = dstImage.cols;
//一般来说,图像的行与行之间在内存中的存储是不连续的,但是当内存足够大时,有些图像可以是连续的,Mat提供了一个检测图像是否连续的函数isContinuous()
//当图像连续的时候,我们可以把图像完全展开,看成是一行
if (dstImage.isContinuous())
{
colsNum = colsNum*rowsNum;
rowsNum = 1;
}
//图像指针操作,求所有像素值的和
uchar *pDataMat;
int total = 0;
for (int j = 0; j<rowsNum; j++)
{
//ptr<type-name>()是一个模板函数,下行中的pDataMat指向第j行第一个元素
pDataMat = dstImage.ptr<uchar>(j);
for (int i = 0; i<colsNum; i++)
{
//total += (pDataMat[i]);
total += *(pDataMat + i); //这两种相同的含义
}
}
//计算图像的平均灰度值
double ave = total / (rowsNum*colsNum);
//遍历图像,进行二值化
for (int j = 0; j<rowsNum; j++)
{
pDataMat = dstImage.ptr<uchar>(j);
for (int i = 0; i<colsNum; i++)
{
if ((pDataMat[i]) >= ave)
(pDataMat[i]) = 255;
else
(pDataMat[i]) = 0;
}
}
imshow("二值化", dstImage);
waitKey(0);
return 0;
}
运行结果:
2、灰度级分层
灰度级分层的作用是突出图像中特定范围的宽度。通常称之为灰度级分层的处理可以由许多方法实现,但是他们中的大多数是两种基本方法的变形。第一种:将感兴趣范围内的所有灰度值显示为一个值(譬如白色),将其它灰度值显示为一个值(譬如黑色),该变换产生了一幅二值图像。第二种:使感兴趣范围的灰度变亮(或者变暗),而保持图像中的其它灰度级不变。
代码实现(实现了第一种):
/*
灰度级分层方法1:
将感兴趣范围内的所有灰度值显示为白,其它颜色显示为黑,所选范围接近数值的顶端
*/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
Mat srcImage, dstImage;
srcImage = imread("image.tif", 0);
if (!srcImage.data)
{
std::cout << "load image failed!" << std::endl;
return -1;
}
imshow("【原图】", srcImage);
int dataMax=0, rowsNum, colsNum;
uchar *ptrData;
dstImage = Mat(srcImage); //使用拷贝构造函数初始化dstImage
rowsNum = dstImage.rows;
colsNum = dstImage.cols;
//判断图像在内存中的存储是否是连续的,如果是连续的将其初始化为1行
if (dstImage.isContinuous())
{
colsNum = rowsNum*colsNum;
rowsNum = 1;
}
//遍历图像,找到最大的像素值
for (int i = 0; i < rowsNum; i++)
{
ptrData = dstImage.ptr<uchar>(i);
for (int j = 0; j < colsNum; j++)
{
if (ptrData[j] > dataMax)
{
dataMax = ptrData[j];
}
}
}
//遍历图像,将接近最大值的所有像素点置为白,其它的像素点置为黑
for (int i = 0; i < rowsNum; i++)
{
ptrData = dstImage.ptr<uchar>(i);
for (int j = 0; j < colsNum; j++)
{
if ((dataMax - ptrData[j]) <= 85)
ptrData[j] = 255;
else
ptrData[j] = 0;
}
}
imshow("【分层图】", dstImage);
waitKey(0);
return 0;
}
运行结果:
这一部分暂时写到这里,这是我初次接触数字图像处理,有误的内容还希望各位能够指正,以免我在错误的道路上越走越远。