我们从下面三个方面来进行相应的数字图像处理
一、图象噪声估计
二、图象分割
三、图像压缩及解压
一、图象噪声估计
•常见的噪声模型:
•1、高斯噪声;
•2、瑞利噪声;
•3、伽马噪声;
•4、指数分布噪声;
•5、均匀分布噪声;
•6、脉冲噪声;
图中噪声分别是:
•a.高斯噪声分布图;
•b.瑞利噪声分布图;
•c.伽马噪声分布图;
•d.指数噪声分布图;
•e.均匀噪声分布图;
•f.脉冲噪声分布图;
我们将用到的库函数是:
CV_EXPORTS_W void blur( InputArray src, OutputArray dst, Size ksize, Point anchor = Point(-1,-1), int borderType = BORDER_DEFAULT );(均值滤波函数)
CV_EXPORTS_W void GaussianBlur( InputArray src, OutputArray dst, Size ksize,double sigmaX, double sigmaY = 0,int borderType = BORDER_DEFAULT );(高斯滤波函数)
CV_EXPORTS_W void medianBlur( InputArray src, OutputArray dst, int ksize );(中值滤波函数)
CV_EXPORTS_W void bilateralFilter( InputArray src, OutputArray dst, int d,double sigmaColor, double sigmaSpace,int borderType = BORDER_DEFAULT );(双边滤波函数)
CV_EXPORTS_W int getOptimalDFTSize(int vecsize);(得到最佳IDFT变换尺寸函数)
CV_EXPORTS_W void dft(InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0);(dft变换函数)
CV_EXPORTS void calcHist( const Mat* images, int nimages,const int* channels, InputArray mask,OutputArray hist, int dims, const int* histSize,const float** ranges, bool uniform = true, bool accumulate = false );(计算直方图函数)
这里我们准备了五种滤波器,分别是均值滤波器,高斯滤波器,中值滤波器,双边滤波器,频域高斯滤波器(主要是滤一些周期噪声)
相应的程序如下:
/*均值滤波函数*/
Mat Blurefilterfunction(Mat &img)
{
namedWindow("均值滤波【效果图】", CV_WINDOW_AUTOSIZE);
Mat out;
blur(img, out,Size(7, 7)); /*这里选用7*7的内核进行均值滤波*/
imshow("均值滤波【效果图】", out);
return out;
}
/*高斯滤波函数*/
Mat GaussianBlurfilterfunction(Mat &img)
{
namedWindow("高斯滤波【效果图】", CV_WINDOW_AUTOSIZE);
Mat out;
GaussianBlur(img, out, Size(3, 3),0,0); /*这里选用3*3的内核进行高斯滤波*/
imshow("高斯滤波【效果图】", out);
return out;
}
/*中值滤波函数*/
Mat MedianBlurfilterfunction(Mat &img)
{
namedWindow("中值滤波【效果图】", CV_WINDOW_AUTOSIZE);
Mat out;
medianBlur(img, out, 7);
imshow("中值滤波【效果图】", out);
return out;
}
/*双边滤波函数*/
Mat BilateralBlurfilterfunction(Mat &img)
{
namedWindow("双边滤波【效果图】", CV_WINDOW_AUTOSIZE);
Mat out;
bilateralFilter(img, out, 25, 25 * 2, 25 / 2);
imshow("双边滤波【效果图】", out);
return out;
}
/*高斯频域低通滤波器*/
//**************************************
//频率域滤波——以高斯低通为例
//**************************************
Mat gaussianlbrf(Mat scr, float sigma);//高斯低通滤波器函数
Mat freqfilt(Mat scr, Mat blur);//频率域滤波函数
Mat FrequencyDomainGaussFiltering(Mat &input)
{
//Mat input = imread("p3-00-01.tif", CV_LOAD_IMAGE_GRAYSCALE);
int w = getOptimalDFTSize(input.cols); //获取进行dtf的最优尺寸
int h = getOptimalDFTSize(input.rows); //获取进行dtf的最优尺寸
Mat padded;
copyMakeBorder(input, padded, 0, h - input.rows, 0, w - input.cols, BORDER_CONSTANT, Scalar::all(0)); //边界填充
padded.convertTo(padded, CV_32FC1); //将图像转换为flaot型
Mat gaussianKernel = gaussianlbrf(padded, 30);//高斯低通滤波器
Mat out = freqfilt(padded, gaussianKernel);//频率域滤波
imshow("结果图 sigma=30", out);
return out;
}
//*****************高斯低通滤波器***********************
Mat gaussianlbrf(Mat scr, float sigma)
{
Mat gaussianBlur(scr.size(), CV_32FC1); //,CV_32FC1
float d0 = 2 * sigma*sigma;//高斯函数参数,越小,频率高斯滤波器越窄,滤除高频成分越多,图像就越平滑
for (int i = 0; i < scr.rows; i++)
{
for (int j = 0; j < scr.cols; j++)
{
float d = pow(float(i - scr.rows / 2), 2) + pow(float(j - scr.cols / 2), 2);//分子,计算pow必须为float型
gaussianBlur.at(i, j) = expf(-d / d0);//expf为以e为底求幂(必须为float型)
}
}
imshow("高斯低通滤波器", gaussianBlur);
return gaussianBlur;
}
//*****************频率域滤波*******************
Mat freqfilt(Mat scr, Mat blur)
{
//***********************DFT*******************
Mat plane[] = { scr, Mat::zeros(scr.size() , CV_32FC1) }; //创建通道,存储dft后的实部与虚部(CV_32F,必须为单通道数)
Mat complexIm;
merge(plane, 2, complexIm);//合并通道 (把两个矩阵合并为一个2通道的Mat类容器)
dft(complexIm, complexIm);//进行傅立叶变换,结果保存在自身
//***************中心化********************
split(complexIm, plane);//分离通道(数组分离)
int cx = plane[0].cols / 2; int cy = plane[0].rows / 2;//以下的操作是移动图像 (零频移到中心)
Mat part1_r(plane[0], Rect(0, 0, cx, cy)); //元素坐标表示为(cx,cy)
Mat part2_r(plane[0], Rect(cx, 0, cx, cy));
Mat part3_r(plane[0], Rect(0, cy, cx, cy));
Mat part4_r(plane[0], Rect(cx, cy, cx, cy));
Mat temp;
part1_r.copyTo(temp); //左上与右下交换位置(实部)
part4_r.copyTo(part1_r);
temp.copyTo(part4_r);
part2_r.copyTo(temp); //右上与左下交换位置(实部)
part3_r.copyTo(part2_r);
temp.copyTo(part3_r);
Mat part1_i(plane[1], Rect(0, 0, cx, cy)); //元素坐标(cx,cy)
Mat part2_i(plane[1], Rect(cx, 0, cx, cy));
Mat part3_i(plane[1], Rect(0, cy, cx, cy));
Mat part4_i(plane[1], Rect(cx, cy, cx, cy));
part1_i.copyTo(temp); //左上与右下交换位置(虚部)
part4_i.copyTo(part1_i);
temp.copyTo(part4_i);
part2_i.copyTo(temp); //右上与左下交换位置(虚部)
part3_i.copyTo(part2_i);
temp.copyTo(part3_i);
//*****************滤波器函数与DFT结果的乘积****************
Mat blur_r, blur_i, BLUR;
multiply(plane[0], blur, blur_r); //滤波(实部与滤波器模板对应元素相乘)
multiply(plane[1], blur, blur_i);//滤波(虚部与滤波器模板对应元素相乘)
Mat plane1[] = { blur_r, blur_i };
merge(plane1, 2, BLUR);//实部与虚部合并
//*********************得到原图频谱图***********************************
magnitude(plane[0], plane[1], plane[0]);//获取幅度图像,0通道为实部通道,1为虚部,因为二维傅立叶变换结果是复数
plane[0] += Scalar::all(1); //傅立叶变o换后的图片不好分析,进行对数处理,结果比较好看
log(plane[0], plane[0]); // float型的灰度空间为[0,1])
normalize(plane[0], plane[0], 1, 0, CV_MINMAX); //归一化便于显示
imshow("原图像频谱图", plane[0]);
idft(BLUR, BLUR); //idft结果也为复数
split(BLUR, plane);//分离通道,主要获取通道
magnitude(plane[0], plane[1], plane[0]); //求幅值(模)
normalize(plane[0], plane[0], 1, 0, CV_MINMAX); //归一化便于显示
return plane[0];//返回参数
}
/*绘制直方图函数*/
void Drawing_one_dimensionalhistogram(Mat &img)
{
/*输入图像转化为灰度图像*/
//cvtColor(img, img, CV_BGR2GRAY);
/*定义变量*/
MatND dstHist;
int dims = 1;
float hranges[] = { 0,255 };
const float *ranges[] = { hranges }; /*这里需要为const类型*/
int size = 256;
int channels = 0;
/*计算图像的直方图*/
calcHist(&img, 1, &channels, Mat(), dstHist, dims, &size, ranges);
int scale = 1;
Mat dstImage(size * scale, size, CV_8U, Scalar(0));
/*获取最大值和最小值*/
double minValue = 0;
double maxValue = 0;
minMaxLoc(dstHist, &minValue, &maxValue, 0, 0);
/*绘制出直方图*/
int hpt = saturate_cast(0.9 * size);
for (int i = 0; i < 256; i++)
{
float binValue = dstHist.at(i);
int realValue = saturate_cast(binValue * hpt / maxValue);
rectangle(dstImage, Point(i*scale, size - 1), Point((i + 1)*scale - 1, size - realValue), Scalar(255));
}
imshow("图像的一维直方图", dstImage);
}
例如下列图片的直方图:
从上到下分别为瑞利噪声(周期噪声),高斯噪声,均匀噪声(周期噪声)
首先我们先对瑞利噪声在空域滤波,效果图如下:
频域滤波,效果图如下:
对于高斯噪声,我们只需要对其高斯滤波即可,但是这里需要进行相应的参数调节,效果图如下:
最后一个均匀噪声,也属于周期噪声的一种,这里我们需要对其在频域滤波,效果图如下:
二、图象分割
大津分割流程图:
我们使用到的库函数为:
CV_EXPORTS_W double threshold( InputArray src, OutputArray dst,double thresh, double maxval, int type );(阀值化函数)
相应的程序如下:
/*计算大津分割的阈值*/
int OtsuAlgThreshold(const Mat image)
{
if (image.channels() != 1)
{
cout << "Please input Gray-image!" << endl;
return 0;
}
int T = 0; //Otsu算法阈值
double varValue = 0; //类间方差中间值保存
double w0 = 0; //前景像素点数所占比例
double w1 = 0; //背景像素点数所占比例
double u0 = 0; //前景平均灰度
double u1 = 0; //背景平均灰度
double Histogram[256] = { 0 }; //灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数
int Histogram1[256] = { 0 };
uchar *data = image.data;
double totalNum = image.rows*image.cols; //像素总数
//计算灰度直方图分布,Histogram数组下标是灰度值,保存内容是灰度值对应像素点数
for (int i = 0; i < image.rows; i++) //为表述清晰,并没有把rows和cols单独提出来
{
for (int j = 0; j < image.cols; j++)
{
Histogram[data[i*image.step + j]]++;
Histogram1[data[i*image.step + j]]++;
}
}
//***********画出图像直方图********************************
Mat image1(255, 255, CV_8UC3);
for (int i = 0; i < 255; i++)
{
Histogram1[i] = Histogram1[i] % 200;
line(image1, Point(i, 235), Point(i, 235 - Histogram1[i]), Scalar(255, 0, 0), 1, 8, 0);
if (i % 50 == 0)
{
char ch[255];
sprintf_s(ch, "%d", i);
string str = ch;
putText(image1, str, Point(i, 250), 1, 1, Scalar(0, 0, 255));
}
}
//***********画出图像直方图********************************
for (int i = 0; i < 255; i++)
{
//每次遍历之前初始化各变量
w1 = 0;
u1 = 0;
w0 = 0;
u0 = 0;
//***********背景各分量值计算**************************
for (int j = 0; j <= i; j++) //背景部分各值计算
{
w1 += Histogram[j]; //背景部分像素点总数
u1 += j * Histogram[j]; //背景部分像素总灰度和
}
if (w1 == 0) //背景部分像素点数为0时退出
{
break;
}
u1 = u1 / w1; //背景像素平均灰度
w1 = w1 / totalNum; // 背景部分像素点数所占比例
//***********背景各分量值计算**************************
//***********前景各分量值计算**************************
for (int k = i + 1; k < 255; k++)
{
w0 += Histogram[k]; //前景部分像素点总数
u0 += k * Histogram[k]; //前景部分像素总灰度和
}
if (w0 == 0) //前景部分像素点数为0时退出
{
break;
}
u0 = u0 / w0; //前景像素平均灰度
w0 = w0 / totalNum; // 前景部分像素点数所占比例
//***********前景各分量值计算**************************
//***********类间方差计算******************************
double varValueI = w0 * w1*(u1 - u0)*(u1 - u0); //当前类间方差计算
if (varValue < varValueI)
{
varValue = varValueI;
T = i;
}
}
//画出以T为阈值的分割线
line(image1, Point(T, 235), Point(T, 0), Scalar(0, 0, 255), 2, 8);
imshow("直方图", image1);
return T;
}
//***************Otsu算法通过求类间方差极大值求自适应阈值*****************
Mat Otsusegmentation(Mat &img)
{
cvtColor(img, img, CV_RGB2GRAY);
Mat imageOutput;
Mat imageOtsu;
int thresholdValue = OtsuAlgThreshold(img);
cout << "类间方差为: " << thresholdValue << endl;
threshold(img, imageOutput, thresholdValue, 255, CV_THRESH_BINARY);
threshold(img, imageOtsu, 0, 255, CV_THRESH_OTSU); //Opencv Otsu算法;
imshow("大津分割【效果图】", imageOutput);
imshow("Opencv Otsu【效果图】", imageOtsu);
return imageOutput;
}
首先我们先计算一张图片的阈值,得到其最小类方差:
然后进行相应的阈值值化操作:
图像分割还有一种称为交互式分割,这里我们直接使用库函数:
CV_EXPORTS_W void grabCut( InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode = GC_EVAL );
img:待分割的源图像,必须是8位3通道(CV_8UC3)图像,在处理的过程中不会被修改;
mask:掩码图像,大小和原图像一致。可以有如下几种取值: GC_BGD(=0),背景; GC_FGD(=1),前景; GC_PR_BGD(=2),可能的背景; GC_PR_FGD(=3),可能的前景。
rect:用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理;
bgdModel:背景模型,如果为null,函数内部会自动创建一个bgdModel;
fgdModel:前景模型,如果为null,函数内部会自动创建一个fgdModel;
iterCount:迭代次数,必须大于0;
mode:用于指示grabCut函数进行什么操作。可以有如下几种选择: GC_INIT_WITH_RECT(=0),用矩形窗初始化GrabCut; GC_INIT_WITH_MASK(=1),用掩码图像初始化GrabCut; GC_EVAL(=2),执行分割。
代码如下:
/*grath_cut*/
void grathcutfunctiong(Mat &img)
{
Mat bgModel, fgModel, mask;
Rect rect;
rect.x = 50;
rect.y = 45;
rect.width = img.cols - (rect.x << 1);
rect.height = img.rows - (rect.y << 1);
rectangle(img, rect, Scalar(0, 0, 255), 3, 8, 0);//用矩形画矩形窗
//循环执行3次,这个可以自己设置
grabCut(img, mask, rect, bgModel, fgModel, 3, GC_INIT_WITH_RECT);
compare(mask, GC_PR_FGD, mask, CMP_EQ);
Mat foreground(img.size(), CV_8UC3, Scalar(255, 255, 255));
img.copyTo(foreground, mask);
imshow("foreground", foreground);
}
相应的效果图如下:
这里再附一个分水岭算法的代码,有兴趣的可以试一试:
Mat g_maskImage, g_scrImage;
Point prevPt(-1, -1);
static void ShowHelpText();
static void on_Mouse(int event, int x, int y, int flags, void*);
/*分水岭算法*/
void Watershedfunction(Mat &g_scrImage)
{
/*初始化掩膜和灰度图*/
Mat scrImage, grayImage;
grayImage.copyTo(scrImage);
cvtColor(g_scrImage, g_maskImage, COLOR_BGR2GRAY);
cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);
g_maskImage = Scalar::all(0);
/*设置鼠标回调函数*/
setMouseCallback(WINDOW_NAME, on_Mouse, 0);
/*轮询按键,进行处理*/
while (1)
{
/*获取键值*/
int c = waitKey(0);
/*若按键值为ESC时,退出*/
if ((char)c == 27)
{
break;
}
/*若按键值为2时,恢复原图*/
if ((char)c == '2')
{
g_maskImage = Scalar::all(0);
scrImage.copyTo(g_scrImage);
imshow("image",g_scrImage);
}
/*若按键值为1或者空格,则进行处理*/
if ((char)c == '1' || (char)c == ' ')
{
/*定义一些参数*/
int i, j, compCount = 0;
vector> contours;
vector hierarchy;
/*寻找轮廓*/
findContours(g_maskImage, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
/*轮廓为空时的处理*/
if (contours.empty())
continue;
/*复制掩膜*/
Mat maskImage(g_maskImage.size(), CV_32S);
maskImage = Scalar::all(0);
/*循环绘制出轮廓*/
for (int index = 0; index > 0; index = hierarchy[index][0], compCount++)
drawContours(maskImage, contours, index, Scalar::all(compCount + 1), -1, 8, hierarchy, INT_FAST16_MAX);
/*compCount为0时的处理*/
if (compCount == 0)
continue;
/*生成随机颜色*/
vector colorTab;
for (i = 0; i < compCount; i++)
{
int b = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int r = theRNG().uniform(0, 255);
colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
/*计算处理时间并输出到窗口中*/
double dTime = (double)getTickCount();
watershed(scrImage, maskImage);
dTime = (double)getTickCount() - dTime;
printf("\t处理时间 = %gms\n",dTime*1000./getTickFrequency());
/*双层循环,将分水岭图像遍历存入watershedImage中*/
Mat watershedImage(maskImage.size(), CV_8UC3);
for(i=0;i(i, j);
if (index == -1)
watershedImage.at(i, j) = Vec3b(255, 255, 255);
else if (index <= 0 || index > compCount)
watershedImage.at(i, j) = Vec3b(0, 0, 0);
else
watershedImage.at(i, j) = colorTab[index - 1];
}
watershedImage = watershedImage * 0.5 + grayImage * 0.5;
imshow("watershed transform",watershedImage);
}
}
}
static void on_Mouse(int event, int x, int y, int flags, void *)
{
/*处理鼠标不在窗口中的情况*/
if (x < 0 || x >= g_scrImage.cols || y < 0 || y >= g_maskImage.rows)
return;
/*数理鼠标左键相关信息*/
if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON))
prevPt = Point(-1, -1);
else if (event == EVENT_LBUTTONDOWN)
prevPt = Point(x, y);
/*鼠标左键按下并移动,绘制出白色线条*/
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
Point pt(x, y);
if (prevPt.x < 0)
prevPt = pt;
line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
line(g_scrImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
prevPt = pt;
imshow(WINDOW_NAME,g_scrImage);
}
}
三、图像压缩及解压
(1)、霍夫曼编码压缩图片:
其流程图如下:
代码如下:
/*哈夫曼编码*/
//几个全局变量,存放读入图像的位图数据、宽、高、颜色表及每像素所占位数(比特)
//此处定义全局变量主要为了后面的图像数据访问及图像存储作准备
unsigned char *pBmpBuf;//读入图像数据的指针
int bmpWidth;//图像的宽
int bmpHeight;//图像的高
int imgSpace;//图像所需空间
RGBQUAD *pColorTable;//颜色表指针
int biBitCount;//图像类型
char str[100];//文件名称
int Num[300];//各灰度值出现的次数
float Feq[300];//各灰度值出现的频率
unsigned char *lpBuf;//指向图像像素的指针
unsigned char *m_pDib;//存放打开文件的DIB
int NodeNum; //Huffman树总节点个数
int NodeStart; //Huffman树起始节点
struct Node
{ //Huffman树节点
int color; //记录叶子节点的灰度值(非叶子节点为 -1)
int lson, rson; //节点的左右儿子(若没有则为 -1)
int num; //节点的数值(编码依据)
int mark; //记录节点是否被用过(用过为1,没用过为0)
}node[600];
char CodeStr[300][300]; //记录编码值
int CodeLen[300]; //编码长度
bool ImgInf[8000000]; //图像信息
int InfLen; //图像信息长度
/***********************************************************************
* 函数名称:
* readBmp()
*
*函数参数:
* char *bmpName -文件名字及路径
*
*返回值:
* 0为失败,1为成功
*
*说明:给定一个图像文件名及其路径,读图像的位图数据、宽、高、颜色表及每像素
* 位数等数据进内存,存放在相应的全局变量中
***********************************************************************/
bool readBmp(char *bmpName)
{
//二进制读方式打开指定的图像文件
FILE *fp = fopen(bmpName, "rb");
if (fp == 0)
{
printf("未找到指定文件!\n");
return 0;
}
//跳过位图文件头结构BITMAPFILEHEADER
fseek(fp, sizeof(BITMAPFILEHEADER), 0);
//定义位图信息头结构变量,读取位图信息头进内存,存放在变量head中
BITMAPINFOHEADER head;
fread(&head, sizeof(BITMAPINFOHEADER), 1, fp);
//获取图像宽、高、每像素所占位数等信息
bmpWidth = head.biWidth;
bmpHeight = head.biHeight;
biBitCount = head.biBitCount;
//定义变量,计算图像每行像素所占的字节数(必须是4的倍数)
int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;
//灰度图像有颜色表,且颜色表表项为256
if (biBitCount == 8)
{
//申请颜色表所需要的空间,读颜色表进内存
pColorTable = new RGBQUAD[256];
fread(pColorTable, sizeof(RGBQUAD), 256, fp);
}
//申请位图数据所需要的空间,读位图数据进内存
pBmpBuf = new unsigned char[lineByte * bmpHeight];
fread(pBmpBuf, 1, lineByte * bmpHeight, fp);
//关闭文件
fclose(fp);
return 1;
}
/***********************************************************************
保存信息
***********************************************************************/
//二进制转十进制
int Change2to10(int pos)
{
int i, j, two = 1;
j = 0;
for (i = pos + 7; i >= pos; i--)
{
j += two * ImgInf[i];
two *= 2;
}
return j;
}
//保存Huffman编码树
int saveInfo(char *writePath, int lineByte)
{
int i, j ;
FILE *fout;
fout = fopen(writePath, "w");
fprintf(fout, "%d %d %d\n", NodeStart, NodeNum, InfLen);//输出起始节点、节点总数、图像所占空间
for (i = 0; i < NodeNum; i++)
{ //输出Huffman树
fprintf(fout, "%d %d %d\n", node[i].color, node[i].lson, node[i].rson);
}
fclose(fout);
return 0;
}
//保存文件
bool saveBmp(char *bmpName, unsigned char *imgBuf, int width, int height,
int biBitCount, RGBQUAD *pColorTable)
{
//如果位图数据指针为0,则没有数据传入,函数返回
if (!imgBuf)
return 0;
//颜色表大小,以字节为单位,灰度图像颜色表为1024字节,彩色图像颜色表大小为0
int colorTablesize = 0;
if (biBitCount == 8)
colorTablesize = 1024;
//待存储图像数据每行字节数为4的倍数
int lineByte = (width * biBitCount / 8 + 3) / 4 * 4;
//以二进制写的方式打开文件
FILE *fp = fopen(bmpName, "wb");
if (fp == 0) return 0;
//申请位图文件头结构变量,填写文件头信息
BITMAPFILEHEADER fileHead;
fileHead.bfType = 0x4D42;//bmp类型
//bfSize是图像文件4个组成部分之和
fileHead.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + colorTablesize + lineByte * height;
fileHead.bfReserved1 = 0;
fileHead.bfReserved2 = 0;
//bfOffBits是图像文件前三个部分所需空间之和
fileHead.bfOffBits = 54 + colorTablesize;
//写文件头进文件
fwrite(&fileHead, sizeof(BITMAPFILEHEADER), 1, fp);
//申请位图信息头结构变量,填写信息头信息
BITMAPINFOHEADER head;
head.biBitCount = biBitCount;
head.biClrImportant = 0;
head.biClrUsed = 0;
head.biCompression = 0;
head.biHeight = height;
head.biPlanes = 1;
head.biSize = 40;
head.biSizeImage = lineByte * height;
head.biWidth = width;
head.biXPelsPerMeter = 0;
head.biYPelsPerMeter = 0;
//写位图信息头进内存
fwrite(&head, sizeof(BITMAPINFOHEADER), 1, fp);
//如果灰度图像,有颜色表,写入文件
if (biBitCount == 8)
fwrite(pColorTable, sizeof(RGBQUAD), 256, fp);
//写位图数据进文件
fwrite(imgBuf, InfLen / 8, 1, fp);
//关闭文件
fclose(fp);
return 1;
}
/*********************
Huffman编码图像解码
*********************/
//读入编码图像
bool readHuffman(char *Name)
{
int i;
char NameStr[100];
//读取Huffman编码信息和编码树
strcpy(NameStr, Name);
strcat(NameStr, ".bpt");
FILE *fin = fopen(NameStr, "r");
if (fin == 0)
{
printf("未找到指定文件!\n");
return 0;
}
fscanf(fin, "%d %d %d", &NodeStart, &NodeNum, &InfLen);
//printf("%d %d %d\n",NodeStart,NodeNum,InfLen);
for (i = 0; i < NodeNum; i++)
{
fscanf(fin, "%d %d %d", &node[i].color, &node[i].lson, &node[i].rson);
}
//二进制读方式打开指定的图像文件
strcpy(NameStr, Name);
strcat(NameStr, ".bhd");
FILE *fp = fopen(NameStr, "rb");
if (fp == 0)
{
printf("未找到指定文件!\n");
return 0;
}
//跳过位图文件头结构BITMAPFILEHEADER
fseek(fp, sizeof(BITMAPFILEHEADER), 0);
//定义位图信息头结构变量,读取位图信息头进内存,存放在变量head中
BITMAPINFOHEADER head;
fread(&head, sizeof(BITMAPINFOHEADER), 1, fp);
//获取图像宽、高、每像素所占位数等信息
bmpWidth = head.biWidth;
bmpHeight = head.biHeight;
biBitCount = head.biBitCount;
//定义变量,计算图像每行像素所占的字节数(必须是4的倍数)
int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;
//灰度图像有颜色表,且颜色表表项为256
if (biBitCount == 8) {
//申请颜色表所需要的空间,读颜色表进内存
pColorTable = new RGBQUAD[256];
fread(pColorTable, sizeof(RGBQUAD), 256, fp);
}
//申请位图数据所需要的空间,读位图数据进内存
pBmpBuf = new unsigned char[lineByte * bmpHeight];
fread(pBmpBuf, 1, InfLen / 8, fp);
//关闭文件
fclose(fp);
return 1;
}
void HuffmanDecode()
{
//获取编码信息
int i, j, tmp;
int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;
for (i = 0; i < InfLen / 8; i++)
{
j = i * 8 + 7;
tmp = *(pBmpBuf + i);
while (tmp > 0)
{
ImgInf[j] = tmp % 2;
tmp /= 2;
j--;
}
}
//解码
int p = NodeStart; //遍历指针位置
j = 0;
i = 0;
do
{
if (node[p].color >= 0)
{
*(pBmpBuf + j) = node[p].color;
j++;
p = NodeStart;
}
if (ImgInf[i] == 1)
p = node[p].lson;
else if (ImgInf[i] == 0)
p = node[p].rson;
i++;
} while (i <= InfLen);
}
/*********************
Huffman编码
*********************/
//Huffman编码初始化
void HuffmanCodeInit()
{
int i;
for (i = 0; i < 256; i++)//灰度值记录清零
Num[i] = 0;
//初始化哈夫曼树
for (i = 0; i < 600; i++)
{
node[i].color = -1;
node[i].lson = node[i].rson = -1;
node[i].num = -1;
node[i].mark = 0;
}
NodeNum = 0;
}
//深搜遍历Huffman树获取编码值
char CodeTmp[300];
void dfs(int pos, int len)
{
//遍历左儿子
if (node[pos].lson != -1)
{
CodeTmp[len] = '1';
dfs(node[pos].lson, len + 1);
}
else
{
if (node[pos].color != -1)
{
CodeLen[node[pos].color] = len;
CodeTmp[len] = '\0';
strcpy(CodeStr[node[pos].color], CodeTmp);
}
}
//遍历右儿子
if (node[pos].lson != -1)
{
CodeTmp[len] = '0';
dfs(node[pos].rson, len + 1);
}
else {
if (node[pos].color != -1)
{
CodeLen[node[pos].color] = len;
CodeTmp[len] = '\0';
strcpy(CodeStr[node[pos].color], CodeTmp);
}
}
}
//寻找值最小的节点
int MinNode()
{
int i, j = -1;
for (i = 0; i < NodeNum; i++)
if (!node[i].mark)
if (j == -1 || node[i].num < node[j].num)
j = i;
if (j != -1)
{
NodeStart = j;
node[j].mark = 1;
}
return j;
}
//编码主函数
void HuffmanCode()
{
int i, j, k, a, b;
for (i = 0; i < 256; i++)
{//创建初始节点
Feq[i] = (float)Num[i] / (float)(bmpHeight * bmpWidth);//计算灰度值频率
if (Num[i] > 0)
{
node[NodeNum].color = i;
node[NodeNum].num = Num[i];
node[NodeNum].lson = node[NodeNum].rson = -1; //叶子节点无左右儿子
NodeNum++;
}
}
while (1)
{ //找到两个值最小的节点,合并成为新的节点
a = MinNode();
if (a == -1)
break;
b = MinNode();
if (b == -1)
break;
//构建新节点
node[NodeNum].color = -1;
node[NodeNum].num = node[a].num + node[b].num;
node[NodeNum].lson = a;
node[NodeNum].rson = b;
NodeNum++;
}
//根据建好的Huffman树编码(深搜实现)
dfs(NodeStart, 0);
//屏幕输出编码
int sum = 0;
printf("Huffman编码信息如下:\n");
for (i = 0; i < 256; i++)
if (Num[i] > 0)
{
sum += CodeLen[i] * Num[i];
printf("灰度值:%3d 频率: %f 码长: %2d 编码: %s\n",i,Feq[i],CodeLen[i],CodeStr[i]);
}
printf("原始总码长:%d\n",bmpWidth * bmpHeight * 8);
printf("Huffman编码总码长:%d\n",sum);
printf("压缩比:%.3f : 1\n",(float)(bmpWidth * bmpHeight * 8) / (float)sum);
for (i = 0; i < 256; i++)
if (Num[i] > 0)
{
sum += CodeLen[i] * Num[i];
}
//记录图像信息
InfLen = 0;
int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;
for (i = 0; i < bmpHeight; i++)
for (j = 0; j < bmpWidth; j++)
{
lpBuf = (unsigned char *)pBmpBuf + lineByte * i + j;
for (k = 0; k < CodeLen[*(lpBuf)]; k++)
{
ImgInf[InfLen++] = (int)(CodeStr[*(lpBuf)][k] - '0');
}
}
//再编码数据
j = 0;
for (i = 0; i < InfLen;)
{
*(pBmpBuf + j) = Change2to10(i);
i += 8;
j++;
}
}
/******************************
主函数
******************************/
void Huffmanfunction()
{
int ord;//命令
char c;
int i, j;
clock_t start, finish;
int total_time;
// CString str;
while (1)
{
printf("本程序提供以下功能\n\n\t1.256色灰度BMP图像Huffman编码\n\t2.Huffman编码BMP文件解码\n\t3.退出\n\n请选择需要执行的命令:");
scanf("%d%c", &ord, &c);
if (ord == 1)
{
printf("\n---256色灰度BMP图像Huffman编码---\n");
printf("\n请输入要编码图像名称:");
scanf("%s", str);
//读入指定BMP文件进内存
char readPath[100];
strcpy(readPath, str);
strcat(readPath, ".bmp");
if (readBmp(readPath))
{
int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;
if (biBitCount == 8)
{
//编码初始化
HuffmanCodeInit();
//计算每个灰度值出现的次数
for (i = 0; i < bmpHeight; i++)
for (j = 0; j < bmpWidth; j++)
{
lpBuf = (unsigned char *)pBmpBuf + lineByte * i + j;
Num[*(lpBuf)] += 1;
}
//调用编码
start = clock();
HuffmanCode();
finish = clock();
total_time = (finish - start);
printf("识别一张耗时: %d毫秒", total_time);
//将图像数据存盘
char writePath[100];
//保存编码后的bmp
strcpy(writePath, str);
strcat(writePath, "_Huffman.bhd");
saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable);
//保存Huffman编码信息和编码树
strcpy(writePath, str);
strcat(writePath, "_Huffman.bpt");
saveInfo(writePath, lineByte);
printf("\n编码完成!编码信息保存在 %s_Huffman 文件中\n\n", str);
}
else
{
printf("本程序只支持256色BMP编码!\n");
}
//清除缓冲区,pBmpBuf和pColorTable是全局变量,在文件读入时申请的空间
delete[]pBmpBuf;
if (biBitCount == 8)
delete[]pColorTable;
}
printf("\n-----------------------------------------------\n\n\n");
}
else if (ord == 2)
{
printf("\n---Huffman编码BMP文件解码---\n");
printf("\n请输入要解码文件名称:");
scanf("%s", str);
//编码解码初始化
HuffmanCodeInit();
if (readHuffman(str))
{ //读取文件
HuffmanDecode(); //Huffman解码
//将图像数据存盘
char writePath[100];
//保存解码后的bmp
strcpy(writePath, str);
strcat(writePath, "_Decode.bmp");
InfLen = bmpWidth * bmpHeight * 8;
saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable);
system(writePath);
printf("\n解码完成!保存为 %s_Decode.bmp\n\n", str);
}
printf("\n-----------------------------------------------\n\n\n");
}
else if (ord == 3)
break;
}
}
效果图如下:
(2)、使用DCT变换进行图像压缩
其流程图如下:
代码如下:
/*DCT压缩*/
void blkproc_DCT(Mat); //功能等同于Matlab的blkproc函数(blkproc(I,[8 8],'P1*x*P2',g,g')),用于对图像做8*8分块DCT
Mat blkproc_IDCT(Mat); //功能等同于Matlab的blkproc函数(blkproc(I,[8 8],'P1*x*P2',g',g)),用于做8*8分块DCT逆变换,恢复原始图像
void regionalCoding(Mat); //区域编码函数,功能等同于Matlab的blkproc函数(blkproc(I1,[8 8],'P1.*x',a))
void thresholdCoding(Mat); //阈值编码函数,功能等同于Matlab的blkproc函数(blkproc(I1,[8 8],'P1.*x',a))
double get_medianNum(Mat &); //获取矩阵的中值,用于阈值编码
void ImageCompression_DCT(Mat &ucharImg)
{
//Mat ucharImg = imread("3.jpg", 0); //以灰度图的形式读入原始的图像
//imshow("srcImg", ucharImg);
Mat doubleImg,OrignalImg;
OrignalImg = ucharImg.clone();
ucharImg.convertTo(doubleImg, CV_64F); //将原始图像转换成double类型的图像,方便后面的8*8分块DCT变换
blkproc_DCT(doubleImg); //对原图片做8*8分块DCT变换
//分别进行区域编码和阈值编码
Mat doubleImgRegion, doubleImgThreshold;
doubleImgRegion = doubleImg.clone();
doubleImgThreshold = doubleImg.clone();
regionalCoding(doubleImgRegion); //对DCT变换后的系数进行区域编码
thresholdCoding(doubleImgThreshold); //对DCT变换后的系数进行阈值编码
//进行逆DCT变换
Mat ucharImgRegion, ucharImgThreshold, differenceimage;
ucharImgRegion = blkproc_IDCT(doubleImgRegion);
//namedWindow("RegionalCoding", CV_WINDOW_AUTOSIZE);
imshow("RegionalCoding", ucharImgRegion);
ucharImgThreshold = blkproc_IDCT(doubleImgThreshold);
//namedWindow("ThresholdCoding", CV_WINDOW_AUTOSIZE);
imshow("ThresholdCoding", ucharImgThreshold);
absdiff(ucharImgRegion, OrignalImg, differenceimage);
imshow("两幅图像的差值",differenceimage);
}
void blkproc_DCT(Mat doubleImgTmp)
{
Mat ucharImgTmp;
Mat DCTMat = Mat(8, 8, CV_64FC1); //用于DCT变换的8*8的矩阵
Mat DCTMatT; //DCTMat矩阵的转置
Mat ROIMat = Mat(8, 8, CV_64FC1); //用于分块处理的时候在原图像上面移动
double a = 0, q; //DCT变换的系数
for (int i = 0; i < DCTMat.rows; i++)
{
for (int j = 0; j < DCTMat.cols; j++)
{
if (i == 0)
{
a = pow(1.0 / DCTMat.rows, 0.5);
}
else
{
a = pow(2.0 / DCTMat.rows, 0.5);
}
q = ((2 * j + 1)*i*M_PI) / (2 * DCTMat.rows);
DCTMat.at(i, j) = a * cos(q);
}
}
DCTMatT = DCTMat.t();
//ROIMat在doubleImgTmp以8为步长移动,达到与Matlab中的分块处理函数blkproc相同的效果
//此程序中,若图片的高或者宽不是8的整数倍的话,最后的不足8的部分不进行处理
int rNum = doubleImgTmp.rows / 8;
int cNum = doubleImgTmp.cols / 8;
for (int i = 0; i < rNum; i++)
{
for (int j = 0; j < cNum; j++)
{
ROIMat = doubleImgTmp(Rect(j * 8, i * 8, 8, 8));
ROIMat = DCTMat * ROIMat*DCTMatT;
}
}
doubleImgTmp.convertTo(ucharImgTmp, CV_8U);
imshow("DCTImg", ucharImgTmp);
}
Mat blkproc_IDCT(Mat doubleImgTmp)
{
//与blkproc_DCT几乎一样,唯一的差别在于:ROIMat = DCTMatT*ROIMat*DCTMat(转置矩阵DCTMatT和DCTMat交换了位置)
Mat ucharImgTmp;
Mat DCTMat = Mat(8, 8, CV_64FC1);
Mat DCTMatT;
Mat ROIMat = Mat(8, 8, CV_64FC1);
double a = 0, q;
for (int i = 0; i < DCTMat.rows; i++)
{
for (int j = 0; j < DCTMat.cols; j++)
{
if (i == 0)
{
a = pow(1.0 / DCTMat.rows, 0.5);
}
else
{
a = pow(2.0 / DCTMat.rows, 0.5);
}
q = ((2 * j + 1)*i*M_PI) / (2 * DCTMat.rows);
DCTMat.at(i, j) = a * cos(q);
}
}
DCTMatT = DCTMat.t();
int rNum = doubleImgTmp.rows / 8;
int cNum = doubleImgTmp.cols / 8;
for (int i = 0; i < rNum; i++)
{
for (int j = 0; j < cNum; j++)
{
ROIMat = doubleImgTmp(Rect(j * 8, i * 8, 8, 8));
ROIMat = DCTMatT * ROIMat*DCTMat;
}
}
doubleImgTmp.convertTo(ucharImgTmp, CV_8U);
return ucharImgTmp;
}
void regionalCoding(Mat doubleImgTmp)
{
int rNum = doubleImgTmp.rows / 8;
int cNum = doubleImgTmp.cols / 8;
Mat ucharImgTmp;
Mat ROIMat = Mat(8, 8, CV_64FC1); //用于分块处理的时候在原图像上面移动
for (int i = 0; i < rNum; i++)
{
for (int j = 0; j < cNum; j++)
{
ROIMat = doubleImgTmp(Rect(j * 8, i * 8, 8, 8));
for (int r = 0; r < ROIMat.rows; r++)
{
for (int c = 0; c < ROIMat.cols; c++)
{
//8*8块中,后四行置0
if (r > 4)
{
ROIMat.at(r, c) = 0.0;
}
}
}
}
}
doubleImgTmp.convertTo(ucharImgTmp, CV_8U);
imshow("regionalCodingImg", ucharImgTmp);
}
void thresholdCoding(Mat doubleImgTmp)
{
int rNum = doubleImgTmp.rows / 8;
int cNum = doubleImgTmp.cols / 8;
double medianNumTmp = 0;
Mat ucharImgTmp;
Mat ROIMat = Mat(8, 8, CV_64FC1); //用于分块处理的时候在原图像上面移动
for (int i = 0; i < rNum; i++)
{
for (int j = 0; j < cNum; j++)
{
ROIMat = doubleImgTmp(Rect(j * 8, i * 8, 8, 8));
medianNumTmp = get_medianNum(ROIMat);
for (int r = 0; r < ROIMat.rows; r++)
{
for (int c = 0; c < ROIMat.cols; c++)
{
if (abs(ROIMat.at(r, c)) < 0)
{
ROIMat.at(r, c) = 0;
}
}
}
}
}
doubleImgTmp.convertTo(ucharImgTmp, CV_8U);
imshow("thresholdCodingImg", ucharImgTmp);
}
double get_medianNum(Mat & imageROI) //获取矩阵的中值
{
vector vectorTemp;
double tmpPixelValue = 0;
for (int i = 0; i < imageROI.rows; i++) //将感兴趣区域矩阵拉成一个向量
{
for (int j = 0; j < imageROI.cols; j++)
{
vectorTemp.push_back(abs(imageROI.at(i, j)));
}
}
for (int i = 0; i < vectorTemp.size() / 2; i++) //进行排序
{
for (int j = i + 1; j < vectorTemp.size(); j++)
{
if (vectorTemp.at(i) > vectorTemp.at(j))
{
double temp;
temp = vectorTemp.at(i);
vectorTemp.at(i) = vectorTemp.at(j);
vectorTemp.at(j) = temp;
}
}
}
return vectorTemp.at(vectorTemp.size() / 2 - 1); //返回中值
}
效果图如下:
(3)、图像JPEG的压缩:
所用到的库函数:
CV_EXPORTS_W Mat imdecode( InputArray buf, int flags );
程序如下:
/*JPEG图片压缩*/
double getPSNR(Mat& src1, Mat& src2, int bb = 0);
void ImageCompression_JPEG(Mat &src)
{
//Mat src = imread("lena.png");
cout << "origin image size: " << src.dataend - src.datastart << endl;
cout << "height: " << src.rows << endl << "width: " << src.cols << endl << "depth: " << src.channels() << endl;
cout << "height*width*depth: " << src.rows*src.cols*src.channels() << endl << endl;
//(1) jpeg compression
vector buff;//buffer for coding
vector param = vector(2);
param[0] = CV_IMWRITE_JPEG_QUALITY;
param[1] = 1;//default(95) 0-100
imencode(".jpg", src, buff, param);
cout << "coded file size(jpg): " << buff.size() << endl;//fit buff size automatically.
Mat jpegimage = imdecode(Mat(buff), CV_LOAD_IMAGE_COLOR);
//(2) png compression
param[0] = CV_IMWRITE_PNG_COMPRESSION;
param[1] = 8;//default(3) 0-9.
imencode(".png", src, buff, param);
cout << "coded file size(png): " << buff.size() << endl;
Mat pngimage = imdecode(Mat(buff), CV_LOAD_IMAGE_COLOR);
//(3) intaractive jpeg compression
char name[64];
namedWindow("jpg");
int q = 95;
createTrackbar("quality", "jpg", &q, 100);
int key = 0;
while (key != 'q')
{
param[0] = CV_IMWRITE_JPEG_QUALITY;
param[1] = q;
imencode(".jpg", src, buff, param);
Mat show = imdecode(Mat(buff), CV_LOAD_IMAGE_COLOR);
double psnr = getPSNR(src, show);//get PSNR
double bpp = 8.0*buff.size() / (show.size().area());//bit/pixe;
sprintf_s(name, "quality:%03d, %.1fdB, %.2fbpp", q, psnr, bpp);
putText(show, name, Point(15, 50), FONT_HERSHEY_SIMPLEX, 1, CV_RGB(255, 255, 255), 2);
imshow("jpg", show);
key = waitKey(33);
if (key == 's')
{
//(4) data writing
sprintf_s(name, "q%03d_%.2fbpp.png", q, bpp);
imwrite(name, show);
sprintf_s(name, "q%03d_%.2fbpp.jpg", q, bpp);
param[0] = CV_IMWRITE_JPEG_QUALITY;
param[1] = q;
imwrite(name, src, param);;
}
}
}
double getPSNR(Mat& src1, Mat& src2, int bb)
{
int i, j;
double sse, mse, psnr;
sse = 0.0;
Mat s1, s2;
cvtColor(src1, s1, CV_BGR2GRAY);
cvtColor(src2, s2, CV_BGR2GRAY);
int count = 0;
for (j = bb; j < s1.rows - bb; j++)
{
uchar* d = s1.ptr(j);
uchar* s = s2.ptr(j);
for (i = bb; i < s1.cols - bb; i++)
{
sse += ((d[i] - s[i])*(d[i] - s[i]));
count++;
}
}
if (sse == 0.0 || count == 0)
{
return 0;
}
else
{
mse = sse / (double)(count);
psnr = 10.0*log10((255 * 255) / mse);
return psnr;
}
}
效果图如下:
主函数如下:
#pragma warning(disable:4996)
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "iostream"
#include "fstream"
#include "regex"
#include "string.h"
#include "time.h"
#include "Windows.h"
#include "math.h"
#include "stdio.h"
#define WINDOW_NAME "(【程序窗口1】)"
#define MAX_CHARS 256//最大字符数
#define MAX_SIZE 1000000
#define M_PI 3.141592653
using namespace cv;
using namespace std;
const double EPS = 1e-12;
const double PI = 3.141592653;
class tmpNode
{
public:
unsigned char uch;//取值范围是0~255,用来表示一个8位的字符
unsigned long freq = 0;//字符出现的频率
};
int main()
{
//Mat img = imread("3.jpg", CV_LOAD_IMAGE_GRAYSCALE);
//Mat img = imread("3.jpg", 1);
//Mat img = imread("3.jpg",0);
//Mat img = imread("1.jpg");
//
//Mat img_1 = imread("p3-01-00_Huffman_Decode.bmp");
//Mat differenceimage;
//absdiff(img, img_1, differenceimage);
/*if (img.empty())
{
std::cout << "图片读取失败!" << "\n";
return -1;
}*/
//namedWindow("【原始图像】", CV_WINDOW_AUTOSIZE);
//imshow("【原始图像】", img);
//namedWindow("【压缩后解码图像】", CV_WINDOW_AUTOSIZE);
//imshow("【压缩后解码图像】", img_1);
/*计算直方图判断噪声并且选择相应的滤波器进行滤波*/
//Mat dstImage = GaussianBlurfilterfunction(img);
//Mat dstImage = Blurefilterfunction(img);
//Mat dstImage = MedianBlurfilterfunction(img);
//Mat dstImage = BilateralBlurfilterfunction(img);
//Mat dstImage = Inversefiltering(img);
//namedWindow("【逆滤波图像】", CV_WINDOW_AUTOSIZE);
//imshow("【逆滤波图像】", dstImage);
//Mat img1 = img.clone();
//Drawing_one_dimensionalhistogram(img1);
//Otsusegmentation(img);
//Graphcutfunction(img);
//Watershedfunction(img);
//ImageCompression();
//ImageCompression_DCT(img);
//ImageCompression_JPEG(img);
//Huffmanfunction();
//Mat dstImg = FrequencyDomainGaussFiltering(img);
//Drawing_one_dimensionalhistogram(dstImg);
//grathcutfunctiong(img);
//namedWindow("【原始图像】", CV_WINDOW_AUTOSIZE);
//imshow("【原始图像】", img);
//namedWindow("【差值图像】", CV_WINDOW_AUTOSIZE);
//imshow("【差值图像】", differenceimage);
waitKey(0);
//destroyAllWindows();
return 0;
}
完。