opencv3-编程入门学习笔记

文档才是最重要的,所有函数都能在文档中找到opencv文档
顺便说我用的IDE QTcreator,工作空间不在源码处,而在debug里,因此所需文件要放在那里
具体目录在
项目-运行-working dictionary查看

关于坐标轴

注意opencv内置的所有函数的坐标系都是左上角为原点,右方为x轴,下方为y轴
rows指y轴方向,cols指x轴方向
Mat的构造函数是先列后行,即先y后x,因为考虑他是一个数组,数组的索引是先列后行的
其余的用到size Point类的都遵循坐标系
在元素遍历中,三种遍历方式都是先y后x的,因为它们都把它当作内存中的数据进行遍历

鼠标事件

卡在鼠标操作卡了一晚上,关键就是鼠标回调函数SetMouseCallback()的最后一个参数看不懂,它的定义是

void setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);

最后一个参数是void *类型,就是void类型的指针,作为函数参数主要是限制参数类型,这个参数同时作为传入回调函数on_mouse 的最后一个参数 void * param传入回调函数,然后调用的时候写法就有点奇怪了

void on_MouseHandle(int event, int x, int y, int flags, void *param){
    Mat& image = *(Mat*)param;

主要就是这句话看不懂,最后才明白
(Mat*)是强制转换,把param转换成 Mat*类型的指针,然后前面的那个 * 是解析,等号右边实际是一个Mat类型的值,等号左侧则是Mat类型的引用,就是对应值的一个别名,所以image实际就是一个Mat类型变量

opencv中Mat类型图像,赋值和拷贝构造函数只复制信息头,而不复制图像,因此他们都共用一个图像矩阵,如果想要复制图像的话,使用clone()方法或copyTo()方法

注意On_Mouse函数的参数
首先event,就是常见的事件,有10种
而flag代表一些特殊状态,例如按住左键滑动、按住CTRL滑动等,有6种,它一般和event一起使用,注意一点用来判断flags和EVENT_FLAG_XXX是否匹配的时候,用的是& 按位与,而不是==
这里我测试了一下按住左键滑动 的flags 是33 ,但是EVENT_FLAG_LBUTTON的值是1,就是一个是00100001 和 1 按位与之后是1 为真
在这里插入图片描述
键盘事件配合鼠标事件的时候,键盘事件要while(1),因为要使用waitKey接收键盘输入
而鼠标事件不需要配合循环,和滑动条一样,回调函数会一直等待事件

关于opencv头文件

opencv常用头文件总结
#include <opencv2/opencv.h>包含了大部分的头文件

ubuntu 在QT creator中使用opencv库,用qmake编译,在.pro文件中添加

###################################对opencv的支持
INCLUDEPATH += /usr/local/include \
/usr/local/include/opencv \
/usr/local/include/opencv2

LIBS += /usr/local/lib/libopencv_calib3d.so \
/usr/local/lib/libopencv_core.so \
/usr/local/lib/libopencv_features2d.so \
/usr/local/lib/libopencv_flann.so \
/usr/local/lib/libopencv_highgui.so \
/usr/local/lib/libopencv_imgcodecs.so \
/usr/local/lib/libopencv_imgproc.so \
/usr/local/lib/libopencv_ml.so \
/usr/local/lib/libopencv_objdetect.so \
/usr/local/lib/libopencv_photo.so \
/usr/local/lib/libopencv_shape.so \
/usr/local/lib/libopencv_stitching.so \
/usr/local/lib/libopencv_superres.so \
/usr/local/lib/libopencv_videoio.so \
/usr/local/lib/libopencv_video.so \
/usr/local/lib/libopencv_videostab.so
###################################

关于imshow

比较头疼的是,到底这个imshow对0-255的矩阵和对0-1的矩阵为什么有时候都能显示正常的图像,有时候0-1的二值图像是全黑的,完全摸不着头脑
后来才知道,imshow()对不同数据结构的矩阵显示方法不同,如果是8位正数型,就显示0-255
如果是浮点数型,就显示乘上255以后的,就会把0-1映射到0-255

opencv常用函数

normalize() 归一化函数
在这里插入图片描述
convertScaleAbs() 将图像映射到(0-255)的8位无符号整形

    	cv::InputArray src, // 输入数组
    	cv::OutputArray dst, // 输出数组
    	double alpha = 1.0, // 乘数因子
    	double beta = 0.0 // 偏移量
    );

使用opencv读取摄像头,按q退出

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main()
{
    VideoCapture capture(0);
    while(1){
        Mat frame;
        capture>>frame;
        imshow("result",frame);
        if(waitKey(1) == 113)//113是'q'的acsii码
            break;
        }

    return 0;
}

Mat类型

最重要的图片类
1)初始化方法,常用的有直接调用构造函数

Mat M(2,2,CV_8UC3,Scalar(0,0,255));

还有类似numpy的用法

Mat Z = Mat::zeros(3,3,CV_8UC1);

还有个create成员函数的方法

M.create(4,4,CV_8UC(2));

2)传递已存在的Mat对象,是引用传递(共用内存)

Mat A,C
A = imread('xxx')
Mat B(A);//用A初始化B,拷贝构造函数,引用传递
C = A;//赋值运算符,引用传递
//结果ABC指向一个内存

利用这个可以巧用Rect

Mat D(A,Rect(10,10,100,100));//用Rect 提取ROI
Mat E;
E = A(Range:all(),Range(1,3));  //用行和列来界定 ,这个有点厉害啊

3)想要值传递,要用clone()或者copyTo()

//这里都是值传递,开辟新的内存空间
Mat F = A.clone();
Mat G;
A.copyTo(G);

4)图像在内存中保存方式
单通道就是行列的一个矩阵
三通道也是二维矩阵,然后每一行是按照BGRBGRBGR这样保存的,它也可以看作是一个三维矩阵,只不过内存的保存形式也可以通过看作二维矩阵来访问。

5)常用的数据成员/成员函数
数据成员:
rows 行数
cols 列数
size 高和宽
data 指向图像第一个字节,常用来判断是否正确打开
成员函数:
type() //Mat的数据格式 例如8UC3 输出的是个数字,对应下图
在这里插入图片描述
size() //返回高*宽
copyTo()
clone()
create()

其他常用类

这里看似定义的都是Point Scalar这些类,其实它们都是重命名的,它们应该都是模板类,只不过把常见的重命名了以下方便用,非重命名的是后面加下划线,然后<数据类型>
point类
数据成员x,y

Point point;
point.x = 10;
point.y = 8;

Scalar类
这个一般不定义出显式的对象,而是直接用构造函数,他就是一个4维数组,并且如果用不到第四个参数就不写
Scalar::all(0) 把所有的元素设置为0
用法比较奇怪,没有输入的图像,找到的例程是这么用的

Mat img(600,600,CV_8UC3);
img = Scalar::all(0);
//比如用在Mat里
Mat M(2,2,CV_8UC3,Scalar(0,0,255));

Size类
这个构造函数有点多,一般用到的就
Size(width,hidth)这个构造函数

Size(5,5)

Rect类
矩形类,挺常用(比如在取ROI的时候)
数据成员有 x,y,width,height 分别是左上角x,y 和矩形的宽度和高度(注意坐标系,跟矩阵正好x,y相反)
成员函数有 area()返回面积 contains(Point)判断点是否在矩形内 inside(Rect)判断矩形是否在矩形内, tl()返回左上角点坐标,br()返回右下角点坐标
还有一些重载运算符 &求举行的交集 |求矩形并集
rect+point平移
rect+size 缩放

Rect rect = rect1 & rect2;//两个矩形交集
Rect rect = rect1 | rect2;//两个矩形并集
Rect rectshift = rect + point; //平移
Rect rectScale = rect + size;//缩放

画各种图形
椭圆、直线、实心圆等

LUT:look up table操作
配合颜色空间缩减,就是把256映射到26中,放在表格里索引,用这个LUT函数可以快速、批量索引

//I是输入 J是输出,lookUpTable是一个Mat型
Mat lookUpTable(1,256,CV_8U);
LUT(I,lookUpTable,J);

计时函数
getTickCount()函数,返回一个绝对时钟周期数(被作为起始时间)
getTickFrequency()函数,返回一秒始终周期数的频率,被当作分母

double time0 = static_cast<double> (getTickCount());//记录当前时间(时钟周期)
//各种处理操作,为的就是算这些处理的时间
time0 = ((double)getTickCount() - time0)/getTickFrequency();

逐个访问图片像素

因为它的内存保存方式,因此不能像python一样遍历三维数组,而是当成二维数组遍历
遍历方法一
用索引遍历,使用 ptr()成员函数 能访问到图片任一行的首地址,然后它的地址是连续保存的,继续++访问元素就可以了(有点像一维数组表示二维数组的感觉)

for(int i = 0; i<rowNumber; i++){
	uchar * data = outputImage.ptr<uchar>(i);
	for(int j = 0;j<colNumber;j++){
		data[j] = data[j]/div*div + div/2;
}
}

遍历方法二
用迭代器遍历,看了好久发现,Mat的begin 和 end分别是x,y的二维数组的头指针和尾后指针,因此遍历的时候,这个指针又指向了一个存放了每个通道BGR值的数组的头指针。

void colorReduce(Mat & inputImage, Mat & outputImage, int div){
	outputImage = inputImage.clone();
	Mat::iterator it = outputImage.begin();
	Mat::iterator itend = outputImage.end();
	for(;it != itend; ++it){
		(*it)[0] = (*it)[0]/div*div + div/2;
		(*it)[1] = (*it)[1]/div*div + div/2;
		(*it)[2] = (*it)[2]/div*div + div/2;
}
}

遍历方法三
动态地址计算,用到了at成员函数,这个就是直接用x,y访问到一个像素点,再对像素点操作了,最好理解的,但是依靠了成员函数
at**** at不是重命名过的,因此作为模板函数需要指明数据类型

void colorReduce(Mat & inputImage, Mat & outputImage , int div){
	outputImage = inputImage.clone();
	int rowNember  = outputImage.rows;
	int colNumber = outputImage.cols;
	for(int i=0;i<rowNumber; i++)
		for(int j = 0; j<colNumber;j++){
			outputImage.at(i,j)[0] = outputImage.at<Vec3b>(i,j)[0]/div*div + div/2;
			outputImage.at(i,j)[1] = outputImage.at<Vec3b>(i,j)[0]/div*div + div/2;
			outputImage.at(i,j)[2] = outputImage.at<Vec3b>(i,j)[0]/div*div + div/2;
			}
}

获得感兴趣区域

1)之前说的用Rect取

int main(){
    Mat img = imread("test.jpg");
    Mat imgROI = img(Rect(50,50,200,200));
    imshow("result",imgROI);
    waitKey();
    return 0;
}

2)用Range()取

int main(){
    Mat img = imread("test.jpg");
    Mat imgROI = img(Range(50,250),Range(50,250));
    imshow("result",imgROI);
    waitKey();
    return 0;
}

掩膜操作

这个操作太帅以至于我流下了感动的泪水
终于知道怎么加!水!印!了!
对于copyTo成员函数,有两种用法
1)A.copyTo(B)
把A复制给B,并且使用不同内存,是值传递
2)A.copyTo(B,mask)
mask是一个灰度图,由mask来判断,如果mask是0,就是黑色,那么保留B的值不变,如果mask不为0,那就把A对应像素的值赋值给B,这样就能把一个黑底的logo作为水印加到图里去了

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main(){
    Mat img = imread("test.jpg");//读取原图
    Mat logo = imread("black_logo.jpg");//读取logo
    Mat imgROI = img(Rect(50,50,logo.rows,logo.cols));//选取要插入水印的区域
    Mat mask = imread("black_logo.jpg",0);//读取logo的灰度图,作为mask
    logo.copyTo(imgROI,mask);//执行添加掩膜操作,黑底部分还是原图的样子,其他部分换成logo
    imshow("result",img);//img和imgROI共享内存,img随之改变
    waitKey();
    return 0;
}

图像融合

和水印差不多,只不过一个是覆盖,一个是按比例叠加
图像融合用到函数
addWeighted(img1,alpha,img2,beta,gamma,dstimg)
计算公式是
dstimg = alpha * img1 + beta * img2 + gamma

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#define WINDOW_WIDTH 600

using namespace std;
using namespace cv;


int main(){
    Mat srcImage1 = imread("test_861.jpg");
    Mat logoImage = imread("test_862.jpg");
    if(!srcImage1.data)
        cout<<"error1"<<endl;
    if(!logoImage.data)
        cout<<"error2"<<endl;
    Mat dstImage;
    double alpha = 0.5;
    double beta = 1 - alpha;
    double gamma = 0.0;
    addWeighted(srcImage1,alpha,logoImage,beta,gamma,dstImage);
    namedWindow("source pic",1);
    imshow("source pic",srcImage1);
    namedWindow("dst pic",1);
    imshow("dst pic",dstImage);
    waitKey();
    return 0;
}

分离颜色通道

还是注意,opencv里的赋值操作、用对象初始化对象的操作都是引用,共享内存,一个变另外一个也跟着变
split(Mat img , vector channels)
把img分离成多个单通道的 Mat图,都存放在vector里(vector也可以用at()访问)

vector<Mat> channels;
Mat imgBluechannel;
Mat imgGreenchannel;
Mat imgRedchannel;
Mat srcimage = imread("xxx.jpg");
split(srcimages,channels);

imgBluechannel = channels.at(0);
imgGreenchannel = channels.at(1);
imgRedchannel = channels.at(2);

合并颜色通道

有两个原型,区别在于
1)第一个三个参数,当第一个参数为空,第二个参数代表输入矩阵的个数,第三个为输出
2)第二个两个参数,第一个输入vector,第二个输出合成Mat

#接上面
Mat mergeImage;
merge(channels,mergeImage);

点处理

对单个像素点处理,就是调整对比度(增益) 和亮度(偏置)的过程
g(x) = a * f(x) + b
a增益gain,调整对比度
b偏置bias,调整亮度
比较关键的是 saturate_cast 这个溢出保护函数,保证映射在uchar范围内(0-255)
它也是没有重命名的模板函数,因此要指定数据类型

int main(){
    int g_nContrastValue = 80;
    int g_nBrightValue = 80;
    Mat srcImage = imread("/home/xbw/qt_src/learn_opencv/test.jpg");
    Mat rowimage = srcImage.clone();
    for(int i=0;i<srcImage.rows;++i)
        for(int j=0;j<srcImage.cols;++j)
            for(int k=0;k<3;++k){
                srcImage.at<Vec3b>(i,j)[k] = saturate_cast<uchar>(g_nContrastValue * 0.01 * srcImage.at<Vec3b>(i,j)[k] + g_nBrightValue);
            }
    namedWindow("src pic",1);
    imshow("src pic",rowimage);
    namedWindow("dst pic",1);
    imshow("dst pic",srcImage);
    waitKey();
    return 0;
}

亮度+对比度配合滑动条

滑动条也没什么难的,主要就是等待鼠标事件,有几个点
1)不用循环,滑动条函数会自动等待事件
2)如果响应函数定义在main前面要先声明

#include <iostream>
#include <opencv2/opencv.hpp>

#define WINDOW_WIDTH 600

using namespace std;
using namespace cv;

void on_ContrastAndBright(int, void*);

int g_nContrastValue;
int g_nBrightValue;
Mat srcImage,dstImage;


int main(){

    srcImage = imread("/home/xbw/qt_src/learn_opencv/test.jpg");
    dstImage = Mat::zeros(srcImage.size(),srcImage.type());
    g_nContrastValue = 80;
    g_nBrightValue = 80;
    namedWindow("src pic",1);
    createTrackbar("duibidu","src pic",&g_nContrastValue,300,on_ContrastAndBright);
    createTrackbar("liangdu","src pic",&g_nBrightValue,200,on_ContrastAndBright);

    on_ContrastAndBright(g_nContrastValue,0);
    on_ContrastAndBright(g_nBrightValue,0);

    waitKey();
    return 0;
}

void on_ContrastAndBright(int, void*){
    for(int i=0;i<srcImage.rows;++i)
        for(int j=0;j<srcImage.cols;++j)
            for(int k=0;k<3;++k){
                dstImage.at<Vec3b>(i,j)[k] = saturate_cast<uchar>(g_nContrastValue * 0.01 * srcImage.at<Vec3b>(i,j)[k] + g_nBrightValue);
            }
    imshow("src pic", dstImage);
}

离散傅里叶变换

傅里叶变换:把空间域变换到频域,方便进行低通、高通滤波,同时也可以除去某一特定频率的噪声
傅里叶变换在图像处理中可以做到图像增强与图像去噪、图像分割之边缘检测、图像特征提取、图像压缩等

XML和YAML的写入和读取

XML和YAML我觉得就是类似于HTML的一种保存数据的文件格式
书上只说了YAML,应该两者的操作是一样的
使用了opencv自带的,FileStorage库
1)输入和输入利用了文件流 << 和 >>
2)文本和数字输入输出,直接<<“名字“<<数据
3)vector输入和输出 要在第一个元素之前加”[" 在最后一个元素之后加”]“
4)map输入输出 要在第一个元素之前加”{" 在最后一个元素之后加”}“
5)读取文件里的数据的时候,读取方法是fs[“xxx”],fs是定义的对象,类似于python读取字典
6)读取vector的时候,读出来的格式是Filenode,然后再用FileNodeIterator 类型的迭代器读取
7)关于”[“和"{”无冒号和“[:” "{:"有冒号的问题:
如果没有,结果是这样
在这里插入图片描述如果有,结果这样
在这里插入图片描述
看起来是。。有冒号横着堆叠,没冒号竖着堆叠

常用线性滤波器

方框滤波(如果normalize为true就是均值滤波了)
boxfilter()
均值滤波
blur()
高斯滤波
GaussianBlur()

#include <iostream>
#include <opencv2/opencv.hpp>


using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg");
    if(!srcimg.data){
        cout<<"can't load picture!"<<endl;
        return 0;
        }
    Mat dstimg_box;
    namedWindow("sourceimg");
    namedWindow("boxFilter");
    boxFilter(srcimg,dstimg_box,srcimg.depth(),Size(3,3));
    Mat dstimg_blur;
    namedWindow("blur");
    blur(srcimg,dstimg_blur,Size(3,3));
    Mat dstimg_Gauss;
    namedWindow("Gaussian");
    GaussianBlur(srcimg,dstimg_Gauss,Size(3,3),0.5);

    imshow("blur",dstimg_blur);
    imshow("boxFilter",dstimg_box);
    imshow("sourceimg",srcimg);
    imshow("Gaussian",dstimg_Gauss);
    while(1)
        if(waitKey()==113)
            break;
    return 0;
}

常用非线性滤波器

中值滤波
medianBlur()
双边滤波,这里参考了https://www.jianshu.com/p/8d11e26c9665
双边滤波优点在于,既能保证边缘,又能滤除噪声,但是对高频噪声无能为力,看一下它的公式
在这里插入图片描述权重分为空域核和值域核的乘积。空域核就是高斯核,另外一个值域核是高斯方差。从空域核公式看出,离的越近的点权重越大,从值域核可以看出,像素相近的点权重越大,那么某点在边缘位置,离它很近的地方有个跟他像素差别特别大的点,但是因为后面值域核的影响使得该点权重很小,那就保证了边缘没被影响。但是如果是高斯核,它不管像素大小,只考虑空间远近,那么边缘一定就被模糊了。
双边滤波缺点是由于它保证了太多边缘它不能滤掉彩色图片里的高频噪声,只能对低频信息较好滤波
bilateralFilter()

#include <iostream>
#include <opencv2/opencv.hpp>


using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg");
    if(!srcimg.data){
        cout<<"can't load picture!"<<endl;
        return 0;
        }
    Mat dstimg_median;
    Mat dstimg_bilateral;
    namedWindow("sourceimg");
    namedWindow("median");
    namedWindow("bilateral");
    medianBlur(srcimg,dstimg_median,3);
    bilateralFilter(srcimg,dstimg_bilateral,25,25*2,25/2);
    imshow("sourceimg",srcimg);
    imshow("median",dstimg_median);
    imshow("bilateral",dstimg_bilateral);
    while(1)
        if(waitKey()==113)
            break;
    return 0;
}

形态学

膨胀和腐蚀
膨胀和腐蚀是对白色部分说的,膨胀会增加白色部分,腐蚀会减少白色部分
膨胀和腐蚀是分别找一个块,把它的中心点赋值成它所覆盖的区域的最大值/最小值,也就是说,膨胀和腐蚀可以对彩色图片进行操作
膨胀dilate
腐蚀erode
膨胀和腐蚀一般配合getStructuringElement函数使用,它用来获得膨胀和腐蚀用的核,产生的自定义的核也是一个Mat类型

#include <iostream>
#include <opencv2/opencv.hpp>


using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg");
    if(!srcimg.data){
        cout<<"can't load picture!"<<endl;
        return 0;
        }
    Mat element = getStructuringElement(MORPH_RECT,Size(15,15));
    Mat dstimg_dilate;
    Mat dstimg_erode;
    dilate(srcimg,dstimg_dilate,element);
    erode(srcimg,dstimg_erode,element);
    namedWindow("source");
    namedWindow("dilate");
    namedWindow("erode");
    imshow("source",srcimg);
    imshow("dilate",dstimg_dilate);
    imshow("erode",dstimg_erode);
    while(1){
        if(waitKey()==113)
            break;
    }
    return 0;
}

开运算闭运算
开运算先腐蚀后膨胀
闭运算先膨胀后腐蚀
把图先左膨胀后做腐蚀就ok了,没有特定函数

形态学梯度
膨胀图和腐蚀图之差,获得物体边缘轮廓
所以就是先拿到膨胀、腐蚀后的结果,再用膨胀图-腐蚀图

顶帽运算
原图像与开运算图像之差
用来分离比临近点亮一点的斑块,在一幅图片具有大幅背景,微笑物品比较有规律的时候,可以用顶帽运算进行背景提取

黑帽运算
闭运算与原图像之差,注意这里跟顶帽运算减数是反的
黑帽运算用来分离比临近点暗一点的斑块,效果图有完美的轮廓

漫水填充算法
种子填充,指点一个种子点,然后开始从种子点出发寻找颜色相近(设上下阈值)之内的连通区域,将其像素点填充成指定的颜色

int main(){
    Mat src = imread("./test.jpg");
    imshow("src",src);
    Rect ccomp;
    floodFill(src,Point(50,300),Scalar(155,255,55),&ccomp,Scalar(10,10,10),Scalar(10,10,10));
    imshow("result",src);
    waitKey(0);
    return 0;
}

图像金字塔
高斯金字塔和拉普拉斯金字塔
高斯金字塔就是原图像不断的高斯滤波+下采样形成的
拉普拉斯金字塔是高斯金字塔Gi层-Gi+1层上采样的结果,感觉就是保留了以下残差,就可以使得高斯金字塔还原回去
但是这里不是很懂…既然有原图像的高斯金字塔,为什么还要用拉普拉斯金字塔。。。直接在高斯金字塔里取不就完了吗。。为什么还要还原回去
反正就是上采样和下采样用的就完事了
pyrUp()
pyrDown()

通过插值法改变图像大小
resize()

阈值化
double threshold()
输入灰度图,用固定阈值进行像素级的改变(比如二值化之类的),看自己选择
adaptiveThreshold()
自适应阈值化

还见了一种阈值化方法

Mat srcimg = imread("./test.jpg");
srcimg = srcimg > 119;
//然后就变成二值图片了,黑色为0 白色为255  但是理论上uchar类型的不能比较大小啊  所以这个应该是重载出来的

边缘检测

canny边缘检测
输入是单通道8位,输出也是单通道8位二值图像,只有0和255
输入是三通道也不报错,但出来的还是单通道8位的图片
Canny()
要求是单通道8位
配合霍夫变换使用时,输入进霍夫变换的图片也是要单通道8位的,但是如果直接在这个图上画图,没法画彩色的线,这个时候就要把图转成三通道,再画图

高阶获得彩色边缘的用法是,把canny产生的边缘图作为掩膜,然后使用copyTo()
sobel算子
sobel比我想象的麻烦很多
Laplacian算子
二阶微分算子,没什么好说的
scharr滤波器
为了配合sobel而存在,用法和sobel一样,除了没有ksize

霍夫变换
霍夫变换的输入图要求必须是单通道的二值图像 因此一般配合边缘检测算子(例如canny算子来使用)
原理就是转换坐标空间,求曲线交点投票
问题的关键在于如何求直线,画直线肯定用line(),就是要找到起点和终点,那么画直线就是已知直线两个参数,而找出两个点画直线的过程
这个分解就非常酷炫

xcosθ + ysinθ = r
把x,y分别分解为
x = rcosθ - λsinθ
y = rsinθ + λcosθ
这个λ可以任取随便哪个数,通过这个就可以确定两个点

霍夫变换还是有点技巧的,试一下

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg");
    Mat midimg,dstimg;
    Canny(srcimg, midimg,50,200,3);
    cvtColor(midimg,dstimg,CV_GRAY2BGR);
    vector<Vec2f> lines;
    HoughLines(midimg,lines,1,CV_PI/180,150,0,0);
    //draw picture
    for(size_t i=0;i<lines.size();++i){
        float rho = lines[i][0];
        float theta = lines[i][1];
        Point pt1,pt2;
        double a = cos(theta) , b = sin(theta);
        double x0 = a*rho, y0 = b*rho;
        //point1
        pt1.x = cvRound(x0+1000*(-b));
        pt1.y = cvRound(y0 + 1000*(a));
        //point2
        pt2.x = cvRound(x0 - 1000*(-b));
        pt2.y = cvRound(y0 - 1000*(a));
        line(dstimg,pt1,pt2,Scalar(55,100,195),1,LINE_AA);
    }
    imshow("src",dstimg);
    imshow("mid",midimg);
    imshow("dst",dstimg);
    waitKey();
    return 0;
}

概率霍夫变换
HoughlinesP()

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg");
    Mat midimg,dstimg;
    Canny(srcimg, midimg,50,200,3);
    cvtColor(midimg,dstimg,CV_GRAY2BGR);
    vector<Vec4i> lines;
    HoughLinesP(midimg,lines,1,CV_PI/180,80,50,10);
    for(size_t i=0; i<lines.size();i++){
        Vec4i l = lines[i];
        line(dstimg,Point(l[0],l[1]),Point(l[2],l[3]),Scalar(186,88,255),1,CV_AA);
    }
    imshow("src",srcimg);
    imshow("mid",midimg);
    imshow("dst",dstimg);
    waitKey();
    return 0;
}

重映射
看到重映射我一度产生怀疑,为什么不直接写映射,还要这个函数呢,而且这个函数也需要从头遍历整张图片,后来发现它比较方便的是,可以分别考虑x,y。虽然也没觉得特别方便吧。。。可能还是自己不太理解
remap()

仿射变换
就是计算机视觉里的仿射,旋转加平移,写到其次线性空间里就是一个23的矩阵,前22是两组基(旋转+缩放)后2*1是平移
函数是warpAffine()
实现步骤:
首先找到原图像的三个点,和目标图像的三个点
然后用 getAffineTransform()找到映射M
然后warpAffine()做映射就ok

计算二维旋转变换矩阵
getRotationMatrix2D()
这里的参数angle是角度不是弧度

仿射 和 旋转变换的实现
都是2*3的矩阵,旋转变换矩阵是
在这里插入图片描述
在右边加个平移矩阵
因为旋转过程选定的旋转中心不同,所以也相当于产生了平移

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;
#define WINDOW_NAME1 "source"
#define WINDOW_NAME2 "fangshe"
#define WINDOW_NAME3 "fangshe_xuanzhuan"

int main(){

    Point2f srcTriangle[3];
    Point2f dstTriangle[3];
    Mat rotMat(2,3,CV_32FC1);
    Mat warpMat(2,3,CV_32FC1);
    Mat srcImage, dstImage_warp, dstImage_warp_rotate;

    srcImage = imread("./test.jpg");
    if(!srcImage.data){
        cout<<"can't read picture!"<<endl;
        return 1;
    }
    dstImage_warp = Mat::zeros(srcImage.rows, srcImage.cols,srcImage.type());
    srcTriangle[0] = Point2f(0,0);
    srcTriangle[1] = Point2f(static_cast<float>(srcImage.cols - 1),0);
    srcTriangle[2] = Point2f(0, static_cast<float>(srcImage.rows - 1));

    dstTriangle[0] = Point2f(static_cast<float>(srcImage.cols*0.0),static_cast<float>(srcImage.rows*0.33));
    dstTriangle[1] = Point2f(static_cast<float>(srcImage.cols*0.65),static_cast<float>(srcImage.rows*0.35));
    dstTriangle[2] = Point2f(static_cast<float>(srcImage.cols*0.15),static_cast<float>(srcImage.rows*0.6));

    warpMat = getAffineTransform(srcTriangle,dstTriangle);
    warpAffine(srcImage,dstImage_warp,warpMat,dstImage_warp.size());

    Point center = Point(dstImage_warp.cols/2,dstImage_warp.rows/2);
    double angle = -30.0;
    double scale =0.8;
    rotMat = getRotationMatrix2D(center,angle,scale);
    warpAffine(dstImage_warp,dstImage_warp_rotate,rotMat,dstImage_warp.size());
    imshow(WINDOW_NAME1,srcImage);
    imshow(WINDOW_NAME2,dstImage_warp);
    imshow(WINDOW_NAME3,dstImage_warp_rotate);
    cout<<warpMat<<endl;
    cout<<rotMat<<endl;
    waitKey();
    return 0;
}

直方图均衡化
像素级增加对比度的经典方法,函数就个输入输出也没参数
equalizeHist();

图像轮廓与图像分割修复

图像轮廓
寻找轮廓的函数要求输入单通道二值图像,输入多通道会报错,非二值图像结果会出问题
findCoutours() 寻找轮廓
drawCoutours() 画出轮廓
python用过这个,就是输入图像输出一个包含所有轮廓的容器,每个轮廓又是一个点的向量,因此输出的数据结构是
vector< vector < Point > >

同时注意findCoutours()函数中的第三个参数,它是一个4维向量的容器,每个轮廓都有一个四维向量,所以它的元素个数就是轮廓的个数,然后每个四维向量里分别是,前一个轮廓的索引,后一个轮廓的索引,父轮廓索引,内嵌轮廓的索引,只是索引而已,方便你找到和当前轮廓相关的轮廓,比如下面的例程就是每次令画图的索引等于下一个轮廓的索引,代替了i++,可能父轮廓和内嵌轮廓索引会有用一点

在drawCoutours()函数里,要注意的是第5个参数,如果写成FILLED 就会用选定的颜色填充,还是非常nice的
否则就写成线宽

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
    Mat srcimg = imread("./test.jpg",0);
    imshow("source",srcimg);
    Mat dstimg = Mat::zeros(srcimg.rows,srcimg.cols,CV_8UC3);
    srcimg = srcimg > 119;
    imshow("erzhitu",srcimg);
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(srcimg,contours,hierarchy,RETR_CCOMP,CHAIN_APPROX_SIMPLE);
    int index = 0;
    for(;index>=0;index = hierarchy[index][0]){
        Scalar color(rand()&255,rand()&255,rand()&255);
        drawContours(dstimg,contours,index,color,FILLED,8,hierarchy);
    }
    imshow("lunkuotu",dstimg);
    waitKey();
    return 0;
}

凸包检测
书上解释模糊不清,最后才发现
检测的的凸包按照vector< int > 来存放,内容是凸包边缘点的索引,所以每个元素都代表了一个点,要在存放的点集里找

多边形包围轮廓
这个就比较简单,对于凸包时产生一个多边形,比较麻烦
这个是固定的多边形

返回外部矩形边界
boundingRect()函数
寻找最小包围矩形
minAreaRect()函数
寻找最小包围圆形
minEnclosingCircle()函数
用椭圆拟合二维点集
fitEllipse()函数
逼近多边形曲线
approxPolyDP()函数

这些函数使用的方法,都是对点集进行操作的,所以问题就是如何获取点集,那么就要用到上面的获取轮廓的那个函数了
所以方法就是,加载一张图,转成二进制,然后获取轮廓

分水岭算法
有点迷 不知道是干嘛的 暂时pass

图像修补
也是一个函数,inpaint()函数
这里其实主要复习了鼠标事件 + 键盘事件 ,函数的用法倒是很简单

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;

#define WINDOW_NAME1 "source"
#define WINDOW_NAME2 "dstimg"

Mat srcImage1 ,inpaintMask;
Point previousPoint(-1,-1);

static void On_Mouse(int event , int x, int y, int flags, void*){
    if(event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON))
        previousPoint = Point(-1,-1);
    else if(event == EVENT_LBUTTONDOWN)
        previousPoint = Point(x,y);
    else if(event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)){
        Point pt(x,y);
        if( previousPoint.x < 0)
            previousPoint = pt;
        line(inpaintMask,previousPoint,pt,Scalar::all(255),5,8,0);
        line(srcImage1,previousPoint,pt,Scalar::all(255),5,8,0);
        previousPoint = pt;
        imshow(WINDOW_NAME1,srcImage1);
    }
}

int main(){
    Mat srcImage = imread("./test.jpg");
    srcImage1 = srcImage.clone();
    inpaintMask = Mat::zeros(srcImage1.size(),CV_8U);

    imshow(WINDOW_NAME1,srcImage1);

    setMouseCallback(WINDOW_NAME1,On_Mouse,0);
    while(1){
        char c = (char)waitKey();
        if(c == 27)
            break;
        if(c == '2'){
            inpaintMask = Scalar::all(0);
            srcImage.copyTo(srcImage1);
            imshow(WINDOW_NAME1,srcImage1);
        }
        if(c == '1' || c == ' ')
        {
            Mat inpaintedImage;
            inpaint(srcImage1,inpaintMask,inpaintedImage,3,INPAINT_TELEA);
            imshow(WINDOW_NAME2,inpaintedImage);
        }

    }
    return 0;
}

获得直方图与直方图对比

直方图对比
可以说是学到的第一个标准意义上的特征描述子(之前的面积、轮廓不考虑的情况下)
获得直方图之后,可以通过直方图对比来获得直方图的相似性,比较直方图的四种方法
1.相关 CV_COMP_CORREL
2.卡方 CV_COMP_CHISQR
3.直方图相交 CV_COMP_INTERSRCT
4.Bhattacharyya距离 CV_COMP_BHATTACHARYYA
在这里插入图片描述
反向投影
是一个找到图片里明显像素特征的方法,比如一张图片里有一个纹理结构或者独特的物体,那么就可以用反向投影的方式映射进结果,思路是:首先求出整张图片的直方图(比如HS二维直方图)然后从测试图片开始遍历所有像素点,对每个像素点,找到它的像素值对应的区间,把该像素点的结果标记为它对应的区间所在直方图的像素数目。

模板匹配
这个在目标检测中很常用很关键!!!!!!!
其实也就是一个函数
matchTenplate()
opencv提供了六种匹配方法
使用方法是,一个完整图片src,一个模板匹配的模板mask,经过匹配之后产生一个单通道的图片dst,size是(src.clos-mask.cols+1,src.rows-mask.rows + 1),这个想一下就知道了,比如33的图片 用22的去匹配,产生的一定是2*2的,每个坐标代表了匹配位置的左上角坐标,而每个坐标的值就是匹配的结果,不同的匹配方法结果不同。
得到匹配结果后,接着利用minMaxLoc函数找到最大、最小值的大小和位置,这个函数要求输入的是指针,因此常用方法是定义几个double的常量(用来保存大小)和point的常量(用来保存位置),然后把他们的取地址输入函数,得到就是值了
得到之后就是用rectangle()函数来画出匹配的位置了
在这里插入图片描述

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

#define WINDOW_NAME1 "source"
#define WINDOW_NAME2 "dstimg"

Mat g_srcImage;
Mat g_templateImage;
Mat g_resultImage;
int g_nMatchMethod;
int g_nMaxTrackbarNum = 5;

void on_Matching(int, void*);


int main(){
    g_srcImage = imread("./test.jpg");
    g_templateImage = imread("./test2.png");
    if(!g_srcImage.data || !g_templateImage.data)
        cout<<"error with opening pic"<<endl;
    namedWindow(WINDOW_NAME1);
    namedWindow(WINDOW_NAME2);
    createTrackbar("method",WINDOW_NAME1,&g_nMatchMethod,g_nMaxTrackbarNum,on_Matching);
    on_Matching(0,0);
    waitKey();
    return 0;
}

void on_Matching(int, void *){
    Mat srcImage;
    g_srcImage.copyTo(srcImage);
    int resultImage_rows = g_srcImage.rows - g_templateImage.rows + 1;
    int resultImage_cols = g_srcImage.cols - g_templateImage.cols + 1;
    g_resultImage.create(resultImage_rows,resultImage_cols,CV_32FC1);
    matchTemplate(g_srcImage,g_templateImage,g_resultImage,g_nMatchMethod);
    normalize(g_resultImage,g_resultImage,0,1,NORM_MINMAX,-1,Mat());
    double minValue;
    double maxValue;
    Point minLocation;
    Point maxLocation;
    Point matchLocation;
    minMaxLoc(g_resultImage,&minValue,&maxValue,&minLocation,&maxLocation,Mat());

    if(g_nMatchMethod == TM_SQDIFF || g_nMatchMethod == TM_SQDIFF_NORMED){
        matchLocation = minLocation;
    }
    else
        matchLocation = maxLocation;
    rectangle(srcImage,matchLocation,Point(matchLocation.x + g_templateImage.cols, matchLocation.y + g_templateImage.rows),Scalar(0,0,255),2,8,0);
    rectangle(g_resultImage,matchLocation,Point(matchLocation.x + g_templateImage.cols,matchLocation.y + g_templateImage.rows),Scalar(0,0,255),2,8,0);
    imshow(WINDOW_NAME1,srcImage);
    imshow(WINDOW_NAME2,g_resultImage);
}

角点检测

角点检测算法分为三类
基于灰度图像的角点检测
基于二值图像的角点检测
基于轮廓曲线的角点检测
harris角点检测
cornerHarris()
配合threshold()使用
harris角点检测的讲解https://www.cnblogs.com/jiahenhe2/p/7930802.html
思想是,对于角点,它的每个像素点的梯度方向和梯度幅度变化都很大,那么衡量一下这个点在x,y增量的梯度变化,通过泰勒展开,就求得了一个梯度的协方差矩阵,它的特征值就反映了两个维度上(x,y)上的窗口内的点的像素大小的变化的大小,根据统计,平坦的时候,每个像素点的梯度都比较小,梯度分布比较集中。边缘的时候,某个方向的梯度分布比较分散,而另一个方向梯度分布比较小(比如有的像素点接近边缘,它的梯度就很大,但是不接近边缘的梯度很小,但是仅限于某个方向上)。而角点的时候,梯度的两个方向都比较分散。
所以就是直接求协方差矩阵M,求法和PCA一样,然后求交点响应(矩阵的行列式-矩阵的迹的平方)
在这里插入图片描述

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
    Mat srcImage = imread("./test.jpg",0);
    imshow("source",srcImage);
    Mat cornerStrength;
    cornerHarris(srcImage,cornerStrength,2,3,0.01);
    imshow("dstImage",cornerStrength);
    Mat harrisCorner;
    threshold(cornerStrength,harrisCorner,0.00001,255,THRESH_BINARY);
    imshow("result",harrisCorner);
    waitKey();
    return 0;
}

shi-Tomasi角点检测
goodFeaturesToTrack()角点检测
是harris角点检测的改进,使用较小特征值来和阈值进行比较,弱两个特征之中较小的一个大于最小阈值,则会得到强角点

亚像素级角点检测
cornerSubPix()

SUSAN角点检测算法
https://www.cnblogs.com/luo-peng/p/5615359.html
SUSAN角点检测算法:用一个圆形模板在图像中滑动,其中的每个像素和中心向素比较,如果差异在阈值范围内就和中心像素相同,找出相同的区域(称为USAN区域)的面积,求得所占模板的比例,考虑比例越低,中心点为角点的概率越大,然后求出每个点的角点响应,最后在阈值以内的认为是角点
在这里插入图片描述

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值