实验项目名称:图像基本操作
- 【实验摘要】
图像像素遍历是进行图像处理中常用的操作,表示访问图中每一个像素元素,本实验主要练习对图像像素的访问。
- 【实验原理】
Mat 数据类型
Mat 是 OpenCV 中新定义的类类型,用于存储图像数据,其数据类型灵活 多变,可以方便的用于存储各类图像。Mat 类是 OpenCV 里使用广泛的一个 类,其中最重要的一个作用就是作为存储图像的数据结构。Mat 类可以存储彩 色图像和灰度图像,并且数据类型可以是多种,如 8 位,16 位,32 位等的整型 和浮点型。另外,存储的图像不管是彩色的还是灰度图像,都是二维的矩阵, 但彩色图像每一像素的值都是一个向量。一般我们用 OpenCV 读取的灰度图像的数据类型为 uchar 类型的,而彩色图像的一个像素的数据类型为<Vec3b>类型 的,灰度图一个像素占用 1 个字节,而彩色图像一个像素 3 个字节。 下面列出 Mat 的部分用法(Mat image):
构造:double m[2][2]={{1.0,2.0},{3.0,4.0}};Mat M(2,2,CV_32F,m)
Mat M(100,100,CV_32FC2,Scalar(1,3));
M.create(300,300,CV_8UC(15));产生 300*300 的 15 通道的矩阵。
Mat image(
240,320,CV_8U,Scalar(100));产生 240*320 的灰度图像,
灰度为 100。
Mat image=Mat::zeros(1000,1000,CV_32F);产生零矩阵。
Image.at<double>(i,j);获取矩阵第 i 行 j 列的值,值为 double 类型。
Image.row(1),image.col(3)分别表示获取矩阵第 1 行和第 3 列的值。
Image.rowRange(2,5),image.colRange(2,4);image.rowRange(2,5).colRange(2,4);
Image.size().height,image.size().width.
Image.cols,image.rows;2.1.2 像素的遍历(即访问)
要实现图像的处理,必须掌握如何访问图像中各像素元素,或者设置图像元素 值,创建图像。OpenCV 提供了几种图像像素的遍历方式,可根据实际需要选择 不同的访问方式进行。简要介绍如下:
(1)随机存取像素值
使用 Mat 中的公有成员 cols 和 rows 可以获得图像的调度和宽度,使用属性 channels 表示通道数,若为灰度图像,则值为 1,若为彩色图像,则值为 3。使用 成员函数 at(int y,int x)可以用来访问图像的元素,使用时必须指定数据类型,如 image.at<uchar>(j,i),对于彩色图像,每个像素由红、绿、蓝三通道构成,因此返 回的是一个向量,向量的每一元素为一个 unsigned char 变量,如 image.at<cv::Vec3b>(j,i)[chnnel]=value;
(2)采用指针对图像进行访问
上述随机遍历图像方便我们随意访问像素元素,但访问的效率很低,因此通常使用基 于指针的访问方式更快速。事实上,在 Mat 类中定义了一个指针变量存放图像数据的地 址,因此可以通过该变量访问图像数据,Mat 提供的函数 ptr<uchar>(i)可以获得第 i 行的首地址,从而可以实现对每一行的访问。
- 【实验过程及结果】
(一)实验过程
创建一个 VS 工程,完成系统环境的配置,实现图像的读取与显示;
1.新建一幅图像,使用随机访问(at 函数)和按行访问的方式
把读入的图像赋值给新建图像,同时显示两幅图像。
2.对图像进行二值化、反色等简单操作,并显示结果。
3.使用教师提供的添加噪声的程序,给图像添加盐噪声,编写程
序时注意区分单通道与三通道的处理。
- 实验结果
- 配置环境的步骤。
(1)第一步:配置环境变量
下载安装完成之后,我的电脑-属性-高级系统设置-系统变量-path;
将我们下载安装好的opencv文件夹打开,一直打开到opencv\\build\\x64\\vc15\\bin,复制该路径,粘贴到系统的环境变量中。
(2)第二步:在Visual Studio上配置OpenCV
1)打开visual studio 2022创建新项目,选择C++控制台应用。
2)点击顶部项目中“属性”。
3)编辑“VC++”目录中的“包含目录”,选择路径:
\\opencv\\build\\include,添加到我们的包含目录中。
- 编辑“VC++”目录中的“库目录”,选择路径:\\opencv\\build\\x64\\vc15\\lib,添加到我们的库目录中
- 编辑 “链接器”的“输入”中的“附加依赖项”。
- 复制文件夹:\\opencv\\build\\x64\\vc15\\lib中的opencv_world454d.lib文件,粘贴到附加依赖项中。
- 测试运行代码。
- 编写图像文件读取与图像显示的程序。
(1)二值化
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
#include <opencv2/imgproc/types_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("d://yaya.jpg");
resize(img, img, Size(400, 400));//resize 为 400*400 的图像
imshow("ori", img);
cvtColor(img, img, CV_RGB2GRAY);
imshow("huidu",img);
Mat img1 = Mat::zeros(img.size(),img.type());
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
//at<类型>(i,j)进行操作,对于灰度图
float m=img.at<uchar>(i, j);//对于蓝色通道进行操作
//float g=img.at<Vec3b>(i, j)[1];//对于绿色通道进行操作
//float r=img.at<Vec3b>(i, j)[2];//对于红色通道进行操作
//变换
if (m<128) {
m = 0;
}
else {
m = 255;
}
img1.at<uchar>(i, j) = m;
}
}
imshow("result", img1);
waitKey(0);
return 0;
}
(2)反色
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
#include <opencv2/imgproc/types_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("d://yaya.jpg");
resize(img, img, Size(400, 400));//resize 为 400*400 的图像
imshow("ori", img);
//cvtColor(img, img, CV_RGB2GRAY);
//imshow("huidu",img);
Mat img1 = Mat::zeros(img.size(),img.type());
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
//at<类型>(i,j)进行操作,对于灰度图
float b=img.at<Vec3b>(i, j)[0];//对于蓝色通道进行操作
float g=img.at<Vec3b>(i, j)[1];//对于绿色通道进行操作
float r=img.at<Vec3b>(i, j)[2];//对于红色通道进行操作
//变换
float b1 = 255 - b;
float g1 = 255 - g;
float r1 = 255 - r;
//赋值
img1.at<Vec3b>(i, j)[0] = b1;
img1.at<Vec3b>(i, j)[1] = g1;
img1.at<Vec3b>(i, j)[2] = r1;
}
}
imshow("result", img1);
waitKey(0);
return 0;
}
(3)加噪
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
int n;
Mat image = imread("d://yaya.jpg");
resize(image, image, Size(400, 400));//resize 为 400*400 的图像
imshow("ori", image);
void salt(cv::Mat & image, int n);
{
n = image.cols * image.rows;
for (int k = 0; k < n; k++)
{
int i = rand() % image.cols;
int j = rand() % image.rows;
if (image.channels() == 1)
{
image.at<uchar>(j, i)=128;
}
else if (image.channels() == 3)
{
image.at<cv::Vec3b>(j, i)[0]=128 ;
image.at<cv::Vec3b>(j, i)[1]=128 ;
image.at<cv::Vec3b>(j, i)[2]=128 ;
}
}
}
imshow("result", image);
waitKey(0);
return 0;
(3)实现图像二值化与反色操作及图像添加噪声操作。
(4)写出程序的流程图(简单描述)。
1.二值化
2.反色
3.加噪
- 描述程序的功能。
- 二值化:划定阈值,如果像素值大于设定的阈值,则给该像素点赋值为255,反之则赋值为0,用两种灰度级表示该幅图像。
- 反色:
- 给出程序运行结果
- 像素的遍历
随机访问
按行访问
- 二值化
- 反色
- 加噪
四、【实验结果讨论】
一、项目名称:
图像变换
二、实验目的:
掌握图像几何变换及频域变换的基本原理;掌握在OpenCv中实现图像基本几何变换的实现,学会使用OpenCV库中dft函数实现图像的傅立叶变换并显示频谱的方法。
三、实验原理:
1.图像几何变换
几何运算可改变图像中各物体之间的空间关系。这种运算可以被看成是将(各)物体在图像内移动。一个几何运算需要两个独立的算法。首先,需要一个算法来定义空间变换本身,用它来描述每个像素如何从其初始位置“移动”到终止位置,即每个像素的“运动”。同时,还需要一个用于灰度插值的算法,这是因为,在一般情况下,输入图像的位置坐标(x,y)为整数,而输出图像的位置坐标为非整数,反过来也如此。因此插值就是对变换之后的整数坐标位置的像素值进行估计。
1.1常见的几何变换
设原图像的坐标为,变换后坐标为。则图像平移表示为:
图像水平镜像
图像垂直镜像
图像放大、缩小
图像旋转
1.2插值算法
插值是常用的数学运算,通常是利用曲线拟合的方法,通过离散的采样点建立一个连续函数来逼近真实的曲线,用这个重建的函数便可以求出任意位置的函数值。
最近邻插值是最简便的插值,在这种算法中,每一个插值输出像素的值就是在输入图像中与其最临近的采样点的值。该算法的数学表示为:
如果
最近邻插值是工具箱函数默认使用的插值方法,而且这种插值方法的运算量非常小。不过,当图像中包含像素之间灰度级变化的细微结构时,最近邻插值法会在图像中产生人工的痕迹。
双线性插值法的输出像素值是它在输入图像中2×2领域采样点的平均值,它根据某像素周围4个像素的灰度值在水平和垂直两个方向上对其插值。
设,和是要插值点的坐标,则双线性插值的公式为:
把按照上式计算出来的值赋予图像几何变换对应于处的像素,即可实现双线性插值。
双三次插值的插值核为三次函数,其插值邻域的大小为4×4。它的插值效果比较好,但相应的计算量也比较大。如下图所示:
2.图像傅立叶变换
2.1 原理
二维傅立叶变换对的公式如下所示:
由于数字图像通常为离散信号,因此采样后变为离散傅立叶变换对:
根据上述公式的可分离性,可以使用一维傅立叶变换实现二维傅立叶变换的计算,通过根据相关性质,可以傅立叶正变换实现傅立叶反变换的计算。
2.2 OpenCV中实现傅立叶变换
1. dft()函数
OpenCv提供的傅里叶变换函数dft(),其定义如下:
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0);
参数解释:
InputArray src: 输入图像,可以是实数或虚数
OutputArray dst: 输出图像,其大小和类型取决于第三个参数flags
int flags = 0: 转换的标识符,有默认值0.其可取的值如下所示:
DFT_INVERSE: 用一维或二维逆变换取代默认的正向变换
DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有N个元素,则输出结果以1/N缩放输出,常与DFT_INVERSE搭配使用。
DFT_ROWS: 对输入矩阵的每行进行正向或反向的傅里叶变换;此标识符可在处理多种适量的时候用于减小资源的开销,这些处理常常是三维或高维变换等复杂操作。
DFT_COMPLEX_OUTPUT: 对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),通过设置标志位可以使函数生成一个全尺寸的复数输出数组。
DFT_REAL_OUTPUT: 对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。
int nonzeroRows = 0: 当这个参数不为0,函数会假设只有输入数组(没有设置DFT_INVERSE)的第一行或第一个输出数组(设置了DFT_INVERSE)包含非零值。这样的话函数就可以对其他的行进行更高效的处理节省一些时间,这项技术尤其是在采用DFT计算矩阵卷积时非常有效。
2.getOptimalDFTSize()返回给定向量尺寸经过DFT变换后结果的最优尺寸大小。其函数定义如下: int getOptimalDFTSize(int vecsize);
参数解释:
int vecsize: 输入向量尺寸大小(vector size),DFT变换在一个向量尺寸上不是一个单调函数,当计算两个数组卷积或对一个数组进行光学分析,它常常会用0扩充一些数组来得到稍微大点的数组以达到比原来数组计算更快的目的。一个尺寸是2阶指数(2,4,8,16,32…)的数组计算速度最快,一个数组尺寸是2、3、5的倍数(例如:300 = 5*5*3*2*2)同样有很高的处理效率。
getOptimalDFTSize()函数返回大于或等于vecsize的最小数值N,这样尺寸为N的向量进行DFT变换能得到更高的处理效率。在当前N通过p,q,r等一些整数得出N = 2^p*3^q*5^r
这个函数不能直接用于DCT(离散余弦变换)最优尺寸的估计,可以通过getOptimalDFTSize((vecsize+1)/2)*2得到。
3.magnitude()计算二维矢量的幅值,其定义如下:
void magnitude(InputArray x, InputArray y, OutputArray magnitude);
参数解释:
InputArray x: 浮点型数组的x坐标矢量,也就是实部
InputArray y: 浮点型数组的y坐标矢量,必须和x尺寸相同
OutputArray magnitude: 与x类型和尺寸相同的输出数组
其计算公式如下:
4. copyMakeBorder()
扩充图像边界,其函数定义如下:
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value=Scalar() );
参数解释:
InputArray src: 输入图像
OutputArray dst: 输出图像,与src图像有相同的类型,其尺寸应为Size(src.cols+left+right, src.rows+top+bottom)
int类型的top、bottom、left、right: 在图像的四个方向上扩充像素的值
int borderType: 边界类型,由borderInterpolate()来定义,常见的取值为BORDER_CONSTANT
const Scalar& value = Scalar(): 如果边界类型为BORDER_CONSTANT则表示为边界值
5. normalize()
归一化就是把要处理的数据经过某种算法的处理限制在所需要的范围内。首先归一化是为了后面数据处理的方便,其次归一化能够保证程序运行时收敛加快。归一化的具体作用是归纳同意样本的统计分布性,归一化在0-1之间是统计的概率分布,归一化在某个区间上是统计的坐标分布,在机器学习算法的数据预处理阶段,归一化也是非常重要的步骤。其定义如下:
void normalize(InputArray src, OutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray()
参数解释:
InputArray src: 输入图像
OutputArray dst: 输出图像,尺寸大小和src相同
double alpha = 1: range normalization模式的最小值
double beta = 0: range normalization模式的最大值,不用于norm normalization(范数归一化)模式
int norm_type = NORM_L2: 归一化的类型,主要有
NORM_INF: 归一化数组的C-范数(绝对值的最大值)
NORM_L1: 归一化数组的L1-范数(绝对值的和)
NORM_L2: 归一化数组的L2-范数(欧几里得)
NORM_MINMAX: 数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。
int dtype = -1: 当该参数为负数时,输出数组的类型与输入数组的类型相同,否则输出数组与输入数组只是通道数相同,而depth = CV_MAT_DEPTH(dtype)
InputArray mask = noArray(): 操作掩膜版,用于指示函数是否仅仅对指定的元素进行操作。
以灰度(单通道)方式读入一幅图像,对
四、【实验内容及要求】
- 图像进行平移操作、水平镜像、竖直镜像操作并显示;将图像尺寸放大1.5倍、3倍、5倍,显示放大后的图像,比较放大5倍时使用向前映射和向后映射时变换图像的差异。
平移
镜像
1.5x,3x,5x
- 图像缩小0.8、0.5倍,显示并比较结果有什么差异。
- 图像分别顺时针旋转30度、45度,显示旋转后的图像并比较结果有什么不同。
- (选做内容)编写两种插值算法,加入上述代码中,体会不同插值方法的效果。
- 认真阅读给定的代码,请修改给定的代码并实现图像傅立叶变换,显示其幅度谱和相位谱。
代码
- 图像的几何变换
(1)平移
#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 srcImage, dstImage;
int xOffset=10, yOffset=10; //x和y方向的平移量
srcImage = imread("d:\\yaya.jpg");
resize(srcImage, srcImage, Size(400, 400));
dstImage.create(srcImage.size(), srcImage.type());
int rowNumber = srcImage.rows;
int colNumber = srcImage.cols;
//进行遍历图像
for (int i = 0; i < rowNumber; i++)
{
for (int j = 0; j < colNumber; j++)
{
//平移变换
int x = j - xOffset;
int y = i - yOffset;
//判断边界情况
if (x >= 0 && y >= 0 && x < colNumber && y < rowNumber)
dstImage.at<Vec3b>(i, j) = srcImage.at<Vec3b>(y, x);
}
}
imshow("原图像", srcImage);
imshow("平移后的图像", dstImage);
waitKey();
return 0;
}
- 镜像
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
/*载入图像并显示*/
Mat img1,img2;
Mat img = imread("d:\\yaya.jpg");
resize(img, img, Size(400, 400));
imshow("原图", img);
void mirrorY(Mat img, Mat & img1);
{
int row = img.rows;
int col = img.cols;
img1 = img.clone();
img2 = img.clone();
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
img1.at<Vec3b>(i, j) = img.at<Vec3b>(i, col - 1 - j);
img2.at<Vec3b>(i, j) = img.at<Vec3b>(row - 1 - i, j);
}
}
}
imshow("水平镜像", img1);
imshow("垂直镜像", img2);
waitKey(0);
return 0;
}
- 放大缩小
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
//#include <opencv2/imgproc/types_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("d://yaya.jpg");
resize(img, img, Size(400, 400));//resize 为 400*400 的图像
imshow("原图", img);
resize(img, img, Size(400 * 0.8, 400 * 0.8));
imshow("0.8x", img);
resize(img, img, Size(400 * 0.5, 400 * 0.5));
imshow("0.5x", img);
waitKey(0);
return 0;
}
3.旋转
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
#include <opencv2/imgproc/types_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("d://yaya.jpg");
resize(img, img, Size(400, 400));//resize 为 400*400 的图像
imshow("原图", img);
//cvtColor(img, img, CV_RGB2GRAY);
//imshow("huidu",img);
Mat img1 = Mat::zeros(img.size(), img.type());
Mat img2 = Mat::zeros(img.size(), img.type());
//图像旋转
void rotate_demo(Mat & img);
{
Mat M,M1;
int w = img.cols;
int h = img.rows;
M = getRotationMatrix2D(Point2f(w / 2, h / 2), -45, 1.0);
M1 = getRotationMatrix2D(Point2f(w / 2, h / 2), -30, 1.0);
double cos = abs(M.at<double>(0, 0));
double sin = abs(M.at<double>(0, 1));
int nw = cos * w + sin * h;
int nh = sin * w + cos * h;
M.at<double>(0, 2) += (nw / 2 - w / 2);
M.at<double>(1, 2) += (nh / 2 - h / 2);
M1.at<double>(0, 2) += (nw / 2 - w / 2);
M1.at<double>(1, 2) += (nh / 2 - h / 2);
warpAffine(img, img1, M, Size(nw, nh), INTER_LINEAR, 0, Scalar(255, 255, 0));
warpAffine(img, img2, M1, Size(nw, nh), INTER_LINEAR, 0, Scalar(255, 255, 0));
}
imshow("旋转45", img1);
imshow("旋转30", img2);
waitKey(0);
return 0;
}
5.傅里叶变换
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat I = imread("d:\\yaya.jpg", IMREAD_GRAYSCALE); //读入图像灰度图
//判断图像是否加载成功
if (I.empty())
{
cout << "图像加载失败!" << endl;
return -1;
}
else
cout << "图像加载成功!" << endl << endl;
Mat padded; //以0填充输入图像矩阵
int m = getOptimalDFTSize(I.rows);
int n = getOptimalDFTSize(I.cols);
//填充输入图像I,输入矩阵为padded,上方和左方不做填充处理
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(),CV_32F) };
Mat complexI;
merge(planes, 2, complexI); //将planes融合合并成一个多通道数组complexI
dft(complexI, complexI); //进行傅里叶变换
//计算幅值,转换到对数尺度(logarithmic scale)
//=> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
split(complexI, planes); //planes[0] = Re(DFT(I),planes[1] = Im(DFT(I))
//即planes[0]为实部,planes[1]为虚部
magnitude(planes[0], planes[1], planes[0]); //planes[0] = magnitude
Mat magI = planes[0];
magI += Scalar::all(1);
log(magI, magI); //转换到对数尺度(logarithmic scale)
//如果有奇数行或列,则对频谱进行裁剪
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
//重新排列傅里叶图像中的象限,使得原点位于图像中心
int cx = magI.cols / 2;
int cy = magI.rows / 2;
Mat q0(magI, Rect(0, 0, cx, cy)); //左上角图像划定ROI区域
Mat q1(magI, Rect(cx, 0, cx, cy)); //右上角图像
Mat q2(magI, Rect(0, cy, cx, cy)); //左下角图像
Mat q3(magI, Rect(cx, cy, cx, cy)); //右下角图像
//变换左上角和右下角象限
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
//变换右上角和左下角象限
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
//归一化处理,用0-1之间的浮点数将矩阵变换为可视的图像格式
normalize(magI, magI, 0, 1, NORM_MINMAX);
resize(I, I, Size(400, 400));
resize(magI, magI, Size(400, 400));
imshow("输入图像", I);
imshow("频谱图", magI);
waitKey(0);
return 0;
}