如果原图像 f ( x , y ) f(x,y) f(x,y)的灰度范围为 [ m , M ] [m,M] [m,M],经过线性变换之后,我们希望变换后的图像 g ( x , y ) g(x,y) g(x,y)的灰度范围是 [ n , N ] [n,N] [n,N],那么经过下面的简单线性变换就可实现:
g ( x , y ) = ( N − n ) / ( M − m ) [ f ( x , y ) − m ] + n g(x,y)=(N-n)/(M-m) [f(x,y)-m]+n g(x,y)=(N−n)/(M−m)[f(x,y)−m]+n
图1. 简单线性变换关系曲线
令系数 k = ( N − n ) / ( M − m ) k=(N-n)/(M-m) k=(N−n)/(M−m),则 k k k的不同,处理的效果也不同。
当 0 < k < 1 0
当 k = 1 k=1 k=1的时候,图像的灰度范围没变,但是灰度区间可能发生平移;
当 k > 1 k>1 k>1时,图像的灰度范围变大,常常能够更凸显细节。
当 k < 0 k<0 k<0时,图像的灰度反转,即亮部分变成暗的部分,暗部分又变成亮的部分,在本文图像求反部分还将考虑。
此处考虑一个简单情况(n=0,k=1/0.6),代码如下:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("1.jpg");
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = dstImage.rows;
int cols = dstImage.cols;
int channels = srcImage.channels();
float k = 1.7;
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols * channels; j++) {
dst_i[j] = saturate_cast<uchar>(k * (float)src_i[j]);
//saturate_cast将像素值限制在0~255
}
}
imshow("简单线性变换后的图像", dstImage);
waitKey(0);
return 0;
}
分段线性变换是常用的线性变换。仍以 f ( x , y ) f(x,y) f(x,y)表示变换前的图像, g ( x , y ) g(x,y) g(x,y)表示变换后的图像,则变换关系为:
图2.分段线性变换关系曲线
如果令 k 1 < 1 , k 3 < 1 , k 2 > 1 k1<1,k3<1,k2>1 k1<1,k3<1,k2>1,则这种变换使得灰度值在 [ 0 , f 1 ] [0,f1] [0,f1]和 [ f 2 , f 3 ] [f2,f3] [f2,f3]中的像素值被压缩,而 [ f 1 , f 2 ] [f1,f2] [f1,f2]的像素值被扩展。也就是说压缩过亮或过暗的像素,扩展亮度适中的像素。因为人眼在亮度适中的情况下更容易区分细节,所以可以改变图像的视觉效果。
简单修改简单线性变换的代码即可实现分段线性变换功能:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("01.jpg");
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = dstImage.rows;
int cols = dstImage.cols;
int channels = srcImage.channels();
float f1 = 50,f2=200,f3=255;
float g1 = 20, g2 = 230,g3=255;
float b1 = 0;
//
float k1 = (g1 - b1) / f1;
float k2 = (g2 - g1) / (f2 - f1);
float b2 = g1 - k2 * f1;
float k3 = (g3 - g2) / (f3 - f2);
float b3 = g2 - k3 * f2;
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols * channels; j++) {
if ((int)src_i[j] < f1) {
dst_i[j] = saturate_cast<uchar>(k1 * (float)src_i[j]+b1);
}
else if((int)src_i[j] > f2){
dst_i[j] = saturate_cast<uchar>( k3 * (float)src_i[j]+b3);
}
else {
dst_i[j] = saturate_cast<uchar>(k2 * (float)src_i[j]+b2);
}
//saturate_cast将像素值限制在0~255
}
}
imshow("简单线性变换后的图像", dstImage);
waitKey(0);
return 0;
}
其实如果图像在黑色或白色附近(像素值较低或较高)存在干扰,那么使用分段线性变换可以使人眼对干扰感受不明显,从而改善图像的视觉效果。
原理上来说的话,非线性变换是由非线性函数变换而来,自然会有很多变换方法,但较多使用的是对数变换和指数变换。
对数变换的表达式为:
g ( x , y ) = C ∗ l n ( f ( x , y ) + 1 ) g(x,y)=C*ln(f(x,y)+1) g(x,y)=C∗ln(f(x,y)+1)
图3.对数变换曲线示意图
C C C为常数,用于使变换后的图像 g ( x , y ) g(x,y) g(x,y)的灰度值的范围符合要求。
代码实现:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("a.jpg");
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
namedWindow("原图像",NORM_MINMAX);
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = srcImage.rows;
int cols = srcImage.cols;
int channels = srcImage.channels();
double C = 255/log(255);/*此处取C=255/log(255),
使得像素扩展至0~255;*/
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols*channels; j++) {
dst_i[j] = (uchar)(C * log((double)src_i[j] + 1));
}
}
namedWindow("对数变换后的图像", NORM_MINMAX);
imshow("对数变换后的图像", dstImage);
waitKey(0);
return 0;
}
图像经过对数变换后相当于低灰度值被扩展,而高灰度值被压缩,这就使得低灰度值的图像细节更容易看清。图像红圈处低灰度值区域经过对数变换后的确更能分辨细节。
用于图像获取、显示、打印的许多装置的响应往往是指数响应。设 f f f为图像的灰度值, s s s为CCD图像传感器或胶片等的入射光强度,则输入光强度与输出信号之间的关系为:
f = c s γ f=cs^γ f=csγ
其中, c c c为常数, γ \gamma γ值表示摄像装置的特性,在同一装置中 γ \gamma γ值是确定的。当 γ < 1 \gamma<1 γ<1时,低灰度区间扩展而高灰度区间压缩,当 γ > 1 \gamma>1 γ>1则相反。
为了使得变换后的图像与入射光的强度相等或成正比,我们可以进行 γ \gamma γ校正,即:
g = ( f / c ) ( 1 / r ) g=(f/c)^(1/r) g=(f/c)(1/r);
我们这里取 c = 25 5 ( 1 − γ ) c=255^(1-\gamma) c=255(1−γ)以使像素值能够扩展至0~255,代码如下:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("a.jpg");
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
namedWindow("原图像",NORM_MINMAX);
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = srcImage.rows;
int cols = srcImage.cols;
int channels = srcImage.channels();
double r = 4;
double C = pow(255,1-r);/*此处取C = pow(255,1-r),
使得像素扩展至0~255;*/
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols*channels; j++) {
dst_i[j] = (uchar)((pow((double)src_i[j]/C,1/r)));
}
}
namedWindow("指数变换后的图像", NORM_MINMAX);
imshow("指数变换后的图像", dstImage);
waitKey(0);
return 0;
}
灰度切片是将某一范围的灰度取出,转换成较大的灰度加以显示,突出我们感兴趣的灰度在图像中的分布情况。其灰度曲线如下图:
图4.灰度切片变换曲线
实现代码(此处取 a = 150 , b = 200 , g a = 0 , g b = 150 , b = 250 a=150,b=200,ga=0,gb=150,b=250 a=150,b=200,ga=0,gb=150,b=250):
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("b.jpg",0);//以灰度图像读入
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
namedWindow("原图像",NORM_MINMAX);
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = srcImage.rows;
int cols = srcImage.cols;
int channels = srcImage.channels();
uchar a = 150, b = 200;
uchar ga = 0,gb = 250;
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols*channels; j++) {
if ((src_i[j] > a)& ( src_i[j] < b)) {
dst_i[j] = gb;
}
else {
dst_i[j] = ga;
}
}
}
namedWindow("灰度切片后的图像", NORM_MINMAX);
imshow("灰度切片后的图像", dstImage);
waitKey(0);
return 0;
}
图像求反实际上就是简单线性变换中 k < 0 k<0 k<0的情况,它能起到帮我们发现暗部细节的作用。
我们这里令变换关系式为 g ( x , y ) = − f ( x , y ) + 255 g(x,y)=-f(x,y)+255 g(x,y)=−f(x,y)+255,代码如下:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("e.jpg",0);//以灰度图像读入
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
namedWindow("原图像",NORM_MINMAX);
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = srcImage.rows;
int cols = srcImage.cols;
int channels = srcImage.channels();
double k = -1;
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols*channels; j++) {
dst_i[j] = (uchar)((double)src_i[j] * k + 255);
}
}
namedWindow("灰度切片后的图像", NORM_MINMAX);
imshow("灰度切片后的图像", dstImage);
waitKey(0);
return 0;
}
位图分割即对图像像素的特定位进行操作来实现。比如图像量化为8比特,那么图像就可以由8个1比特的平面组成,其范围从最低有效位的位平面0到最高有效位的位平面7。在8比特的字节中,位平面0包含图像中像素的最低位,而平面7则包含最高位。就8比特图像的位平面抽取。
就8比特图像的位平面抽取而言,如果获取位平面7的二值图像,可以通过以下步骤:
(1)把图像中0~127间的所有灰度映射到一个灰度值(如0)
(2)把图像128~255间的灰度映射为令一种灰度值(如255),其他位面图的获取以此类推。
代码如下:
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat srcImage = imread("1.jpg",0);//以灰度图像读入
if (!srcImage.data)
{
cout << "读入图片错误!" << endl;
system("pause");
return -1;
}
namedWindow("原图像",NORM_MINMAX);
imshow("原图像", srcImage);
Mat dstImage(srcImage.size(), srcImage.type());
int rows = srcImage.rows;
int cols = srcImage.cols;
int channels = srcImage.channels();
for (int k = 0; k < 8; k++) {
for (int i = 0; i < rows; i++) {
uchar* src_i = srcImage.ptr<uchar>(i);//原图的当前行
uchar* dst_i = dstImage.ptr<uchar>(i);//目标图的当前行
for (int j = 0; j < cols * channels; j++) {
if (src_i[j]>=pow(2,k)&&src_i[j]<pow(2,k+1))//进行位与
{
dst_i[j] = 250;
}
else {
dst_i[j] = 0;
}
}
}
String str ="位平面" ;
str.append(to_string(k));
namedWindow(str, NORM_MINMAX);
imshow(str, dstImage);
}
cv::waitKey(0);
return 0;
}