赞
踩
最近入坑OpenCV,写一些OpenCV的C++实验的过程,实现方法和代码示例。
注:
以下实例均在VS2019以及OpenCV3.31正常运行
下述例子用到的所有头文件
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/videoio.hpp"
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cmath>
利用imread函数以及对于图像像素操作,实现图像的读取和灰度转换
输入图片路径,利用imread读取图像至Mat中。
需要注意的是,OpenCV的三通道顺序是BGR,而非RGB,因此在后续的各种图像颜色操作中需要留意。
void test1(const String& path) { Mat srcMat = imread(path); //此处的path需要注意格式,输入时应该以“\\”来区分不同的文件夹,例如D:\\Desktop\\TheEarth.png , 在输入时,还需要注意保留文件后缀名,如jpg,png等等 int height = srcMat.rows; int width = srcMat.cols; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { uchar average = (srcMat.at<Vec3b>(j, i)[0] + srcMat.at<Vec3b>(j, i)[1] + srcMat.at<Vec3b>(j, i)[2]) / 3; //在此处将三个通道的颜色统一,以便后续转换为灰度图像 srcMat.at<Vec3b>(j, i)[0] = average; srcMat.at<Vec3b>(j, i)[1] = average; srcMat.at<Vec3b>(j, i)[2] = average; } } imshow("Grey Image", srcMat); waitKey(0); //此处的waitkey很重要,如果注释掉,图片将一闪而过,看不到结果 return ; }
对图像进行二值化处理,简单地说,就是设置某个阈值threshold,若当前像素点的值高于阈值,则将之置为255(白色),当低于阈值时置为0。此处主要针对灰度图像处理。
我们可以声明一个 uchar 变量 uchar threshold =100; 若average>threshold 则average=255,否则为0。 然后再把average 值赋值给像素的3 个通道,(可以利用第一个例子进行改进),并通过imshow 函数观察结果, 然后修改threshold 值,观察输出结果。
void test2(const String& path) { Mat srcMat = imread(path); int height = srcMat.rows; int width = srcMat.cols; for (uchar threshold = 0; threshold < 255; threshold += 5) { cout << "当前阈值" << threshold << endl; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { uchar average = (srcMat.at<Vec3b>(j, i)[0] + srcMat.at<Vec3b>(j, i)[1] + srcMat.at<Vec3b>(j, i)[2]) / 3; if (average > threshold) average = 255; else average = 0; srcMat.at<Vec3b>(j, i)[0] = average; srcMat.at<Vec3b>(j, i)[1] = average; srcMat.at<Vec3b>(j, i)[2] = average; } } imshow("0-1 Image", srcMat); waitKey(0); } return; }
下图给出的示例是在阈值为170时得到的
读取一张图片,存入mat 型变量srcMat,然后声明两个mat 型数据,命名为deepMat 和shallowMat。分别用深复制和浅复制把srcMat 的内容赋值给两个mat。下面的代码中,对于不同的阈值就行了测试,并观察在对于srcMat操作过程中,深复制与浅复制所得到的Mat是否变化。
void test3(const String& path) { Mat srcMat = imread(path), ori = imread(path); Mat shallowMat = srcMat; Mat deepMat = srcMat.clone(); int height = srcMat.rows; int width = srcMat.cols; for (uchar threshold = 0; threshold < 255; threshold += 5) { cout << "当前阈值" << (int)threshold << endl; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { uchar average = (ori.at<Vec3b>(j, i)[0] + ori.at<Vec3b>(j, i)[1] + ori.at<Vec3b>(j, i)[2]) / 3; if (average > threshold) average = 255; else average = 0; srcMat.at<Vec3b>(j, i)[0] = average; srcMat.at<Vec3b>(j, i)[1] = average; srcMat.at<Vec3b>(j, i)[2] = average; } } imshow("Ori ", ori); imshow("scrMat",srcMat); imshow("S (shallow copy)", shallowMat); imshow("D (deep copy)", deepMat); waitKey(0); //此处类似于system("pause"),waitkey等待一段时间,监听键盘动作,返回对应的键盘ASCII值,括号内为等待时间,单位为毫秒,若为0,则无限等待至有键盘动作发声。 } }
浅复制得到的图片会随原图变化而变化
此处的深复制与浅复制在本质上是与CPP动态内存分配紧密相关。
深复制和浅复制:
浅复制:编译器提供的默认复制构造函数只是把传递进来的对象的每个成员变量复制到新的成员变量中去,这样两个对象中变量均指向传入的对象的那块内存区域。就会出现错误。
深复制:开辟新的内存,存放调用对象的成员变量
详细的内容可以查阅有关C++深复制浅复制区别的文章参阅。
利用图像在色彩上的一些特点,可以在图像处理中进行一些巧妙的处理。通过opencv 将图片的rgb 三个通道分离,并观察每个通道的图像。
此处需要注意:
OpenCV的通道颜色顺序为BGR,为非传统的RGB。
1.手工实现三通道颜色的分离
基本思路:维护三个Mat,分别为 bMat,gMat,rMat。分别存储原始图像三个提通道的信息。利用此方法实现三个通道颜色的分离,有时候可以找出隐藏的信息。(例如bMat在B的通道存储原始图像的BLUE,其他两个通道G,R置0即可)
void test4_1(const String& path) { Mat srcMat = imread(path); Mat bMat(srcMat.rows, srcMat.cols, srcMat.type()); Mat gMat(srcMat.rows, srcMat.cols, srcMat.type()); Mat rMat(srcMat.rows, srcMat.cols, srcMat.type()); int height = srcMat.rows; int width = srcMat.cols; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { bMat.at<Vec3b>(i, j)[0] = srcMat.at<Vec3b>(i, j)[0], bMat.at<Vec3b>(i, j)[1] = bMat.at<Vec3b>(i, j)[2] = 0; gMat.at<Vec3b>(i, j)[1] = srcMat.at<Vec3b>(i, j)[1], gMat.at<Vec3b>(i, j)[0] = gMat.at<Vec3b>(i, j)[2] = 0; rMat.at<Vec3b>(i, j)[2] = srcMat.at<Vec3b>(i, j)[2], rMat.at<Vec3b>(i, j)[1] = rMat.at<Vec3b>(i, j)[0] = 0; } } imshow("BULE image", bMat); imshow("GREEN image", gMat); imshow("RED image", rMat); waitKey(0); return; }
2.利用OpenCV给出的split函数,快速实现通道分离
split()函数的C++版本有两个原型,他们分别是:
C++: void split(const Mat& src, Mat*mvbegin); //&为引用操作符
C++: void split(InputArray m, OutputArrayOfArrays mv);
使用cv::split()将多通道矩阵中的通道分成多个单通道。
第一种方法是提供一个指向C风格矩阵的指针,该矩阵包含cv::split()将要保存分离操作结果的cv::Mat对象的指针;
第二种方法是提供一个由cv::Mat对象组成的STL向量,即vector对象。
以下代码给出vector的实例:
void test4_1(const String& path) { Mat image = imread(path); if(!image.data){ cout << "read image error" << endl; return ; } imshow("Orignal", image); vector<Mat> channels; split(image, channels); imshow("B", channels.at(0)); imshow("G", channels.at(1)); imshow("R", channels.at(2)); waitKey(0); return ; }
利用VideoCapture实例化一个capture,博主的参数为((VideoCapture(int index, int apiPreference = CAP_ANY);打开摄像头用来捕捉视频index默认自带摄像头0,其他的外接摄像头一般是1.)
VideoCapture可以用来读取本地视频,也可以利用摄像头捕捉视频。
此处给出本地视频读取的代码实现
void VideoRead(const String& path) { //读取本地视频 VideoCapture capture(path); /* VideoCapture capture; captrue.open(path); */ while (1) { //frame存储每一帧图像 Mat frame; //读取当前帧 capture >> frame; //播放完退出 if (frame.empty()) { printf("播放完成\n"); break; } imshow("读取视频",frame); //延时30ms waitKey(30); } return; }
为实现对读取的视频进行写入,还需要VideoWriter。
利用VideoWriter再实例化一个writer,完成对图像的写入。使用前,需要开启摄像头权限,否则可能无法使用。
构造函数VideoWriter()
VideoWriter(const string& filename, int fourcc, double fps,Size frameSize, bool isColor=true);
下面给出摄像头录像功能:
void test5() { VideoCapture capture(0); VideoWriter writer("VideoTest001.avi", CV_FOURCC('M', 'J', 'P', 'G'), 25.0, Size(640, 480)); Mat frame; while (capture.isOpened()) { capture >> frame; writer << frame; imshow("录像中,按P结束", frame); if (cvWaitKey(30) == 80) //由于P键对应的ASCII码为80,因此若按下P即可结束录像 { capture.release(); //释放设备 return; //结束循环 } } }
1.画圆
利用circle函数,实现画圆的功能
函数原型为
void cvCircle( CvArr* img, CvPoint center, int radius, CvScalar color, int thickness=1, int line_type=8, int shift=0 );
利用opencv给出的point类,实现点的功能,博主这里读入的图片约为900*500,读者在使用过程中可以依据自己的实际情况修改p的x,y坐标。
(如例1中的图像显示,在opencv中 x对应于rows,y为cols,向右为x增加,其中向下为y增加,窗口左上角为原点(0,0))
void test6_1(const String&path)
{
Point p(850,470);
Mat srcMat = imread(path);
circle(srcMat, p, 50, Scalar(0, 0, 255), -1, 7, 0);
imshow("Girl", srcMat);
waitKey(0);
return;
}
测试结果:
2.1 画线段
利用line函数,实现划线功能
void line(Mat& img, Point pt1, Point pt2, const Scalar& color, int thickness=1, int lineType=8, int shift=0)
void test6_21(const String& path)
{
Mat earth = imread(path);
Point p1(100, 30), p2(600, 180);
line(earth, p1, p2, Scalar(0, 255, 0), 5, 8, 0);
imshow("Line Test", earth);
waitKey(0);
return;
}
测试结果:
2.2画矩形框
利用rectangle函数,标定对角线两点即可画出矩形框
void cvRectangle( CvArr* img, CvPoint pt1, CvPoint pt2, CvScalar color,int thickness=1, int line_type=8, int shift=0 );
测试代码:
void test6_22(const String& path)
{
Mat earth = imread("D:\\Desktop\\暑假学习\\TheEarth.png");
Point p1(100, 30), p2(600, 360);
rectangle(earth, p1, p2, Scalar(0, 255, 0), 5, 8, 0);
imshow("Rectangle Test", earth);
waitKey(0);
return;
}
//使用时,后面这三个参数可以不填写
3.1精确地对某个点操作,手工画出抛物线
本例给出y = -x*(x-667)的图像
void test6_31(const String& path) { //利用点近似 Mat earth = imread(path); //parabola function y = -(x-0)*(x-667) = -x * (x-667) //Point p1(0, 0), p2(1, 666); //line(earth, p1, p2, Scalar(0, 255, 0), 5, 8, 0); for (int x = 2 , y = 0; x < earth.cols; x+=2) { y = x * (earth.cols - x) / 350; if (y < earth.rows) { earth.at<Vec3b>(y, x)[0] = earth.at<Vec3b>(y, x)[1] = earth.at<Vec3b>(y, x)[2] = 255; //可以根据喜好更改画出的点的颜色,此处简便起见,置为白色 } } imshow("parabola function y = -(x-0)*(x-667) = -x * (x-667)", earth); waitKey(0); return; }
利用像素点近似画出抛物线:
3.2利用line函数,画出线段,利用循环,拟合出抛物线的图形
本例依然给出y = -x*(x-667)的图像
void test6_32(const String& path) { //利用线段近似 Mat earth = imread(path); //parabola function y = -(x-0)*(x-667) = -x * (x-667) Point p1(0, 0), p2(1, 2); line(earth, p1, p2, Scalar(0, 255, 0), 5, 8, 0); for (int x = 2, y = 0; x < earth.cols; x += 2) { y = x * (earth.cols - x) / 350; p1.x = p2.x, p1.y = p2.y; p2.x = x, p2.y = y; line(earth, p1, p2, Scalar(0, 255, 0), 2, 8, 0); //利用循环迭代每一对点的位置,利用线段拟合抛物线 } imshow("parabola function y = -(x-0)*(x-667) = -x * (x-667)", earth); waitKey(0); return; }
利用直线进行拟合抛物线:
手工实现直方图的计算
1 .定义三个容量为256 的int 型数组,遍历图像的每个像素,得到不同像素值的频数,再由不同像素对应频数除以图像大小,得到频率,并计入float数组中。因为需要计算频率,故数组声明方式 float 型histgram[256];
2. 利用上述结果,以及画线的函数,绘制一副直方图。
本例给出RGB三通道对应的直方图实现,读者也可以自行更改,得到单通道的直方图。
void test7_1(const String& src) { //统计BGR三个通道不同的直方图 int iB[256], iG[256], iR[256], tB = 0, tG = 0, tR = 0; memset(iB, 0, sizeof(iB)); memset(iG, 0, sizeof(iG)); memset(iR, 0, sizeof(iR)); float B[256], G[256], R[256]; Mat srcMat = imread(src); Mat hist = Mat::zeros(600, 800, CV_8UC3); int height = srcMat.rows; int width = srcMat.cols; tB = tG = tR = width * height; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { ++iB[(int)srcMat.at<Vec3b>(i, j)[0]]; ++iG[(int)srcMat.at<Vec3b>(i, j)[1]]; ++iR[(int)srcMat.at<Vec3b>(i, j)[2]]; } } Point p1(0,600), p2(0,0); //绘图 for (int i = 0; i < 256; ++i) { p2.x = p1.x = i * 3 + 10; B[i] = (float)iB[i] / (float)tB; p2.y = 600 - 25 * B[i] * height; line(hist, p2, p1, Scalar(255, 0, 0), 2, 8, 0); G[i] = (float)iG[i] / (float)tG; p2.y = 600 - 25 * G[i] * height; line(hist, p1, p2, Scalar(0, 255, 0), 2, 8, 0); R[i] = (float)iR[i] / (float)tR ; p2.y = 600 - 25 * R[i] * height; line(hist, p1, p2, Scalar(0, 0, 255), 2, 8, 0); } imshow("Original Image", srcMat); imshow("triple color histgram", hist); waitKey(0); return; }
由于某些图像的颜色亮度,对比度和亮度异常,导致某些区域的信息难以正常被人眼看出。
利用变换函数 f(i) = i ^ γ ,即Out= In ^ gamma,遍历每一个像素点实现gamma矫正。
gamma校正可使得图像看起来更符合人眼的特性。
1)遍历所有像素
对所有像素进行如下运算
1.像素值归一化到[0,1]之间
2. Output=Input ^ γ
3. Intensity=Output * 255
void test8_1(const String& src , float gamma) { Mat face = imread(src); imshow("Before Gamma Correct", face); int height = face.rows; int width = face.cols; float b; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { uchar average = (face.at<Vec3b>(j, i)[0] + face.at<Vec3b>(j, i)[1] + face.at<Vec3b>(j, i)[2]) / 3; b = pow((float)(average) / 256, gamma); face.at<Vec3b>(j, i)[0] = face.at<Vec3b>(j, i)[1] = face.at<Vec3b>(j, i)[2] = 256 * b; } } imshow("After Gamma Correct", face); waitKey(0); return; }
gamma矫正前后,后者更符合人眼感受
由结果可知,后者在右半部分能够显示出更多细节。
2)查表法
建立查询表数组并使不同的像素(0-255)均进行gamma矫正,存储到数组中,便于下次使用。本质上是以空间换时间。
1.下标值归一化到[0,1]之间
Output=Input ^ γ
VecGamma[i] = Output * 255 遍历所有像素像素值作为下表,直接从VecGamma 中读出变化后的像素值
void test8_2(const String& src, float gamma) { Mat face = imread(src); imshow("Before Gamma Correct", face); int height = face.rows; int width = face.cols; float vg[256]; //归一化非常重要 for (int i = 0; i < 256; ++i) { vg[i] = 256 * pow((float)i / 256, gamma); //尽可能利用好每一次循环 } uchar average; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { average = (face.at<Vec3b>(j, i)[0] + face.at<Vec3b>(j, i)[1] + face.at<Vec3b>(j, i)[2]) / 3; //此处依然转换为灰度图像进行处理,彩色图像可能会因为gamma矫正失真 face.at<Vec3b>(j, i)[0] = face.at<Vec3b>(j, i)[1] = face.at<Vec3b>(j, i)[2] = vg[average]; } } imshow("After Gamma Correct", face); waitKey(0); return; }
与上述第一种方法一样,但是查表法快于第一种方法。
利用下述代码,博主对两种方法进行了测试
需要导入头文件time.h,得到毫秒级计时
clock_t start, end;
//方法一
start = clock();
for (int i = 0; i < 100; ++i) {
test8_1(path, 0.5);
}
end = clock();
cout << (end - start) << "ms" << endl;
//方法二
start = clock();
for (int i = 0; i < 100; ++i) {
test8_2(path, 0.5);
}
end = clock();
cout << (end - start) << "ms" << endl;
1)手工实现直方图均衡函数,对直方图进行处理
opencv中直方图均衡化算法的输入图像需为八位单通道图像,也就是灰度图像。算法实现步骤如下:
第一步:依次扫描原始灰度图像的每一个像素,计算出图像的直方图H。
第二步:进行归一化处理,即将0~255像素值的每一个像素值在图像中出现的次数除以图像的大小,得到归一化直方图。
第三步:计算直方图积分,公式:
第四步:以H’作为查询表进行图像变换dst(x,y)=H’(src(x,y))
给出单通道的实现方法:
void test9_1(const String& path) { //实现单通道 Mat ori = imread(path), grayImg, eImg; imshow("Original Image", ori); cvtColor(ori, grayImg, CV_RGB2GRAY); grayImg = ori.clone(); cout << "直方图统计" << endl; int height = grayImg.rows, width = grayImg.cols, h[256]; float fh[256], imgSize; imgSize = height * width; memset(h, 0, sizeof(h)); for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { ++h[grayImg.at<Vec3b>(j ,i)[0]]; } } cout << "归一化,建立积分对应关系" << endl; fh[0] = (float)h[0] / imgSize; h[0] = fh[0] * 256; for (int i = 1; i < 256; ++i) { fh[i] = (float)h[i] / imgSize; fh[i] += fh[i - 1]; h[i] = fh[i] * 256; } cout << "修正图像" << endl; eImg = ori.clone(); for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { eImg.at<Vec3b>(j, i)[0] = eImg.at<Vec3b>(j, i)[1] = eImg.at<Vec3b>(j, i)[2] = (uchar)h[grayImg.at<Vec3b>(j, i)[0]]; } } cout << "显示结果" << endl; imshow("After Equal Hist", eImg); waitKey(0); return; }
测试结果如下:
给出三通道的实现方法:
void test9_2(const String& path) { //实现三通道 Mat ori = imread(path); Mat eImg; imshow("Original Image", ori); cout << "直方图统计" << endl; int height = ori.rows, width = ori.cols; int hb[256], hg[256], hr[256]; float fhb[256], fhg[256], fhr[256], imgSize; imgSize = height * width; memset(hb, 0, sizeof(hb)), memset(hg, 0, sizeof(hg)), memset(hr, 0, sizeof(hr)); for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { ++hb[ori.at<Vec3b>(j, i)[0]]; ++hg[ori.at<Vec3b>(j, i)[1]]; ++hr[ori.at<Vec3b>(j, i)[2]]; } } cout << "归一化,建立积分对应关系" << endl; fhb[0] = (float)hb[0] / imgSize, fhg[0] = (float)hg[0] / imgSize, fhr[0] = (float)hr[0] / imgSize; hb[0] = fhb[0] * 256, hg[0] = fhg[0] * 256, hr[0] = fhr[0] * 256; for (int i = 1; i < 256; ++i) { //BLUE 均衡化 fhb[i] = (float)hb[i] / imgSize; fhb[i] += fhb[i - 1]; hb[i] = fhb[i] * 256; //GREEN 均衡化 fhg[i] = (float)hg[i] / imgSize; fhg[i] += fhg[i - 1]; hg[i] = fhg[i] * 256; //RED 均衡化 fhr[i] = (float)hr[i] / imgSize; fhr[i] += fhr[i - 1]; hr[i] = fhr[i] * 256; } cout << "均衡化三通道图像" << endl; eImg = ori.clone(); for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { eImg.at<Vec3b>(j, i)[0] = (uchar)hb[ori.at<Vec3b>(j, i)[0]]; eImg.at<Vec3b>(j, i)[1] = (uchar)hg[ori.at<Vec3b>(j, i)[1]]; eImg.at<Vec3b>(j, i)[2] = (uchar)hr[ori.at<Vec3b>(j, i)[2]]; } } cout << "显示结果" << endl; imshow("After Equal Hist", eImg); waitKey(0); return; }
2)调用直方图均衡函数,进行直方图均衡处理,opencv 原函数只能对单通道图像进行直方图均衡,若想要处计算彩色图像的均衡化图,可以先将图像用split函数进行通道分离,分别处理每一个通道的图像,在用merge函数进行合并。
void test9_31(const String& path) { Mat ori = imread(path), eImg; string s; while(!ori.data) { cout << "Could not load image" << path << endl; cin >> s; ori = imread(s); } cvtColor(ori, ori, CV_BGR2GRAY); imshow("GRAY Original Image", ori); equalizeHist(ori, eImg); imshow("Equalized Hist Image", eImg); waitKey(0); return; }
利用gamma矫正,修正图片,观察出隐藏的信息
给出代码实现,本代码中利用循环,给出不同的gamma值,利用前述的例子,建立查询表,加快图像处理的速度。利用不同的gamma值矫正图像,观察隐藏的信息。
void test10(const String& src) { //最终的文字是 李竹老师超级帅,gamm值在2.5左右比较清晰 Mat gtest ,ori = imread(src); gtest = ori.clone(); imshow("Before Gamma Transform", ori); int height = gtest.rows; int width = gtest.cols; float vg[256]; //查询表 uchar average; string title; for (float gamma = 0.05; gamma < 1.5; gamma += 0.05) { //建立查询表,加快图像处理速度 for (int i = 0; i < 256; ++i) { vg[i] = 256 * pow((float)i / 256, gamma); } for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { //此处实现的是单通道的gamma矫正,会出现一定程度的图像失真 gtest.at<Vec3b>(j, i)[0] = vg[gtest.at<Vec3b>(j, i)[0]]; gtest.at<Vec3b>(j, i)[1] = vg[gtest.at<Vec3b>(j, i)[1]]; gtest.at<Vec3b>(j, i)[2] = vg[gtest.at<Vec3b>(j, i)[2]]; } } title = "After Gamma Correct" + to_string(gamma); imshow(title, gtest); gtest = ori.clone(); waitKey(0); } return; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。