基本操作
1. 读写图像文件:
src = imread("图像路径(文件夹斜杠是/,直接复制过来的是错的)");
if (src.empty())
{
printf("这什么狗屁图片打不开\r\n");
return -1;
}
imwrite("图像路径", dst);
src
、dst
是Mat
对象。if
语句用于判断图像是否正常打开。
2. 打印图像:
namedWindow("output", WINDOW_AUTOSIZE);
imshow("output", dst);
output
是输出窗口名字,不人工新建也没关系,opencv会自己建一个;dst
也是Mat
对象。
3. 图像线性叠加
addWeighted(src1, alpha, src2, beta, gamma, dst);
src1
、src2
是两张加数图片,图像长宽像素必须完全一致。alpha
和beta
是加权系数。一般她们的和取为1。gamma
是常数。这个函数的作用可以由公式表示:
d
s
t
(
i
)
=
a
l
p
h
a
×
s
r
c
1
(
i
)
+
b
e
t
a
×
s
r
c
2
(
i
)
+
g
a
m
m
a
dst(i)=alpha\times src1(i)+beta\times src2(i)+gamma
dst(i)=alpha×src1(i)+beta×src2(i)+gamma
4. 图像相减
subtract(src2, src1, dst, Mat());
src2
是被减数,src
是减数,dst
是差。
5. 图像归一化
normalize(src, dst, max, min, NORM_MINMAX);
将图像像素灰度归一化到上下限规定的值以内。(感觉max和min就算交换一下也没什么影响) 最后一个参数是归一化类型,一般都用NORM_MINMAX
。
6. 像素范围处理
saturate_cast<uchar>(像素值);
就是对像素限幅,0-255
7. 指针遍历图像
for (int i = 0; i < src.rows; i++)//遍历每一行
{
uchar* point = src.ptr<uchar>(i);
for (int j = 0; j < src.cols * src.channels; j++)//遍历每一列
{
point[j];//当前像素点
//亦可以用src.ptr<uchar>[i][j];
}
}
8. 轨迹条操作
createTrackbar("轨迹条名", "输出窗口名", int* value, int count, Call_Back);
value
是轨迹条默认值,count
是轨迹条最大值,Call_Back
是回调函数。这个函数的入口参数之一必须是void*
。回调函数可以设置为Null。
每一次滑动轨迹条的时候都会调用回调函数,执行其中的代码。回调函数结尾可以弄个return;
。使用轨迹条操作之前注意变量和函数的声明。
轨迹条value
取值范围感觉最好不要弄太大,要不然会很卡。
9.图像绘制
定点
Point p;
p.x = 数值;
p.y = 数值;
或者
p = Point(x, y);
坐标原点从左上角开始。其他定义直角坐标系一样。
画几何图形
。。。
10.通道分割和合并
vector<Mat> channels;
split(image1, channels);//分割image1的通道
Mat channels1 = channels[0];//获取通道1
Mat channels2 = channels[1];//获取通道2
Mat channels3 = channels[2];//获取通道3
channel[0]、[1]、[2]
按照 b、g、r 成分的顺序分割。
Mat MultiImage;
merge(channels, MultiImage);
合并是这个样子。合完之后应该跟拆分之前一毛一样的。
图像预处理
1. 亮度和对比度
原理很简单,可由如下公式表示: d s t ( i ) = a l p h a × s r c ( i ) + b e t a dst(i)=alpha\times src(i)+beta dst(i)=alpha×src(i)+beta
dst.at<Vec3b>(i, j)[0] = saturate_cast<uchar>(alpha*src1.at<Vec3b>(i, j)[0] + beta);
dst.at<Vec3b>(i, j)[1] = saturate_cast<uchar>(alpha*src1.at<Vec3b>(i, j)[1] + beta);
dst.at<Vec3b>(i, j)[2] = saturate_cast<uchar>(alpha*src1.at<Vec3b>(i, j)[2] + beta);
<Vec3b>
是一种长度为3的向量数据类型;alpha
是像素点系数,其值>0。如果alpha
>1那么亮度和对比度提高,<1的话对比度和亮度是降低的。beta
就是增加或者减小亮度的。如果是单通道的灰度图像,那就不需要分成三个数组来写了。
这个是像素点操作,需要配合遍历图像算法来完成操作。
还有一种更加简单的方法:
src.convertTo(dst, -1, alpha, beta);
-1代表图像深度和原来相同。
2. 色彩空间转化
cvtColor(src, srcInGray, COLOR_BGR2GRAY);
3.图像尺寸变化
resize(src,dst,Size(x像素,y像素),double x变化倍数,double y变化倍数,int interpolation);
可以通过Size直接指定放缩之后图像像素大小,也可以通过放缩倍数来调节。确定Size之后,后面的xy可以直接置零,反过来也是。
最后的int参数是插值方法,一共有5种:
INTER_NEAREST
- 最近邻插值法INTER_LINEAR
- 双线性插值法(默认)INTER_AREA
- 基于局部像素的重采样INTER_CUBIC
- 基于4x4像素邻域的3次插值法INTER_LANCZOS4
- 基于8x8像素邻域的Lanczos插值
图像放大的时候最好用INTER_LINEAR
,缩小时最好选 INTER_AREA
3. 平滑
平滑滤波方式很多种,基本原理都是掩膜操作。
3.1 均值滤波
blur(src, dst, Size(n,m), Point(-1, -1));
Size(n, m)是卷积核的大小,越大平滑效果越强烈。一般n、m都取奇数。
3.2 高斯滤波
GaussianBlur(src, dst, Size(3, 3), 0, 0);
Size
可以自己调节大小,必须是奇数。越大模糊效果越强。最后两个参数是默认的 0, 0。
3.3 中值滤波
medianBlur(src, dst, 3);
第三个参数是卷积核大小。
3.4 双边滤波
相当于平原和高地分别滤波,保留了悬崖,即图像中明显边界。
bilateralFilter(src, dat, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT);
第三个参数是邻域半径,就是滤波的邻域范围;
第四个参数是颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有月宽广的颜色会被混合到一起,产生较大的半相等颜色区域;
第五个参数是坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。
最后一个参数是边界条件,有默认值不用管。
4. 锐化
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
filter2D(src, dst, -1, kernel, Point(-1, -1));
先建立一个卷积核kernel,然后用filter2D
函数(源,目标,深度(默认-1即可),卷积核,锚定点(默认-1,-1))
5. 均值漂移
bilateralFilter(src, dst, double sp, double sr, int maxLevel);
第三、四个参数是漂移的物理空间和色彩空间大小。这个函数需要迭代运算的,轨迹条跑的有点慢。难受。
6. 二值化(阈值处理)
threshold(src, dst, threshold, max, THRESH_BINARY);
两个double数字分别代表阈值,灰度最大值。然后最后一个参数是阈值类型:第五个参数,int类型的type,阈值类型:
THRESH_BINARY
当前点值大于阈值时,取Maxval,也就是第四个参数,下面再不说明,否则设置为0
THRESH_BINARY_INV
跟上面相反,当前点值大于阈值时,设置为0,否则设置为Maxval
THRESH_TRUNC
当前点值大于阈值时,设置为阈值,否则不改变
THRESH_TOZERO
当前点值大于阈值时,不改变,否则设置为0
THRESH_TOZERO_INV
当前点值大于阈值时,设置为0,否则不改变
7. 形态学操作
6.1 形态结构:
Mat struct = getStructuringElement(MORPH_RECT, Size(3,3), Point(-1, -1));
形态学操作之前,先建立一个struct
核,包括结构的形状:
- 矩形:
MORPH_RECT
- 交叉形:
MORPH_CROSS
- 椭圆形:
MORPH_ELLIPSE
然后尺寸,尺寸的设置在开闭操作当中很有用。
7.2 腐蚀和膨胀
先建立一个结构对象,然后
erode(src,dst,struct);//腐蚀
dilate(src, dst, struct);//膨胀
腐蚀是图像中深色向浅色侵蚀。膨胀是图像中浅色向深色扩张。
7.3 高级形态学变化
包括开操作,闭操作,形态学梯度,礼帽,黑帽。
开操作就是先腐蚀后膨胀,闭操作相反。可以用一个封装好的函数进行操作:
morphologyEx(src, dst, CV_MOPOPEN, struct);
MORPH_ERODE = CV_MOP_ERODE
,
MORPH_DILATE = CV_MOP_DILATE
,
MORPH_OPEN = CV_MOP_OPEN
,
MORPH_CLOSE = CV_MOP_CLOSE
,
MORPH_GRADIENT = CV_MOP_GRADIENT
,
MORPH_TOPHAT = CV_MOP_TOPHAT
,
MORPH_BLACKHAT = CV_MOP_BLACKHAT
开操作可以将深色区域中的亮色小洞洞消掉。闭操作就是反过来,将白色背景里的黑点去掉。如果要去掉的部分有特定的形状,比如水平线,竖直线,那就将核的形状设成跟要去掉的东西一样。比如如果要去掉水平/竖直线,struct
的形状可以设成矩形,size
设成一个长条。
8. 图像金字塔
图像金字塔顾名思义,就是通过处理得到的一堆大小不同的图像一张张叠起来,空间中看就像个金字塔一样。
得到图像金字塔分为两步,第一步先对图像进行高斯模糊,然后删除/增加行列。
8.1 上采样
pyrUp(src, dst, Size(src.cols * 2, src.rows * 2));
每个像素点复制,可以将图片放大
8.2 降采样
pyrDown(src, dst, Size(src.cols / 2, src.rows / 2));
隔一行/列删除,将图片缩小。
8.3 高斯不同
就是把同一张图像在不同的参数下做高斯模糊之后的结果相减,得到的输出图像。称为高斯不同(DOG)
高斯不同是图像的内在特征,在灰度图像增强、角点检测中经常用到。
GaussianBlur(g1, g2, Size(3, 3), 0, 0);
GaussianBlur(g2, g3, Size(3, 3), 0, 0);
subtract(g2, g3, dog, Mat());
将高斯模糊的size
增大,高斯不同会更加明显。
8. 边缘检测
8.1 Sobel算子
横向: [ − 1 − 2 − 1 0 0 0 1 2 1 ] \begin{bmatrix} -1 & -2 &-1\\ 0&0&0\\1&2&1 \\ \end{bmatrix} ⎣⎡−101−202−101⎦⎤,可以检测水平方向的边缘;纵向的话就是这个矩阵转置,检测竖直边缘。
Sobel(src, dst, CV_32F, int x, int y, 3, 1, 0, BORDER_DEFAULT);
src为灰度图像,第三个参数是图像深度,有CV_16/32/64等等很多,-1表示和,但这里最好设置成比原图深度大。后面两个参数表示x、y方向差分的阶数,再后面一个是卷积核大小,还是只能为奇数,一般3。最后三个是缩放系数、灰度增加常数,还有个不知道,反正都有默认值,可以直接不用。
关于x、y方向的阶数,可以一起差分,也可以分开差分然后再将得到的结果叠加,这两种方法得到的结果会有细微差别。
Sobel算子噪声敏感,所以前面一般加上高斯滤波。后面为了防止差分之后结果超限,一般也加上限幅。
8.2 Laplace算子
图像求二阶导,也可以用来检测边缘。跟上面的很相似。
Laplacian(src, dst, CV_16F, 3);
src是灰度图,第三个参数是深度,然后卷积核大小,后面其实也有三个参数,可缺省。
拿bliss试了一下,结果贼tm奇葩。
8.3 Canny边缘检测
Canny(src, dst, threshold1, threshold2, 3, false);
src
必须是8位图,threshold1
和threshold2
是下限、上限两个阈值,一般设置成1:2或者1:3的关系,后面两个默认值不用管。在Canny之前,需要先滤波和灰度转换。不模糊也没关系,Canny自带。
Canny算法其实分为3步:
- Sobel算法计算梯度
- 非最大信号抑制
- 按阈值输出二值图像
第一步,计算梯度之前必须平滑降噪,因为噪声对梯度计算影响很大,canny自带的滤波有可能不靠谱;第二步非最大信号抑制,实际上就是得到边缘之后,沿着梯度法向方向,把不是最大值的地方去掉,把本来比较宽的边缘变窄一点;完了之后还要使用双阈值筛选,输出一个二值图像,具体方法是:高于第二个阈值的保留,然后从高于阈值的点出发,寻找亮度不断降低的像素点,直到亮度低于第一个阈值为止,低于第一个阈值的就直接去掉。
9.霍夫变换
这概念好特喵的难懂
笛卡尔坐标系内,一条直线可以由斜截式表达:
y
=
a
x
+
b
y=ax+b
y=ax+b这个式子里面,
x
x
x、
y
y
y是变量,
a
a
a、
b
b
b是参数,如果固定
x
x
x、
y
y
y而改变
a
a
a、
b
b
b,就可以得到经过同一点的一组直线;如果固定
a
a
a、
b
b
b而反过来改变
x
x
x、
y
y
y,得到的就是同一根直线上不同的点点。
如果把 a a a、 b b b当做变量, x x x、 y y y当做参数,那就将 x x x、 y y y坐标空间变成了 a a a、 b b b参数空间,这种变换就是直角坐标系下的霍夫变换。变换之后,经过同一点的一组直线,变成了同一根直线上不同的点。
根据对称性原则可以推出来,某一空间内多条经过同一点的一组曲线,霍夫变换之后就是一条直线。如果霍夫变换发生在极坐标系,上面这个法则依然成立。极坐标下,直线霍夫变换之后会变成经过同一点的一组曲线。这一组曲线有几根,就意味着这条直线有多长(多少个像素点)。这就是霍夫变换直线检测的原理。
霍夫原检测一样。直角坐标系内确定一个圆需要
a
a
a、
b
b
b、
r
r
r三个参数。变换到参数坐标系内,就变成经过点
(
a
,
b
,
r
)
(a, b,r)
(a,b,r)的一组曲面。变换之后的参数空间变成了三维的,计算难度加大。因此实际的算法中,通常不采用这种,而是另一种方法:圆心一定在圆上各点切线的法线上。因此在边缘图像上寻找梯度曲线的交点,交点越多说明就越有可能是圆心,然后根据圆心找半径。因此其准确性很依赖第一步圆心的寻找。
9.1 霍夫直线检测
vector<Vec4f> plines;
HoughLinesP(srcInGray, plines, 1, CV_PI / 180.0, threshold, minLenth, maxGap);
直线检测必须在canny边缘检测的基础上完成。输入是8位灰度图,输出直线极坐标;后面两个参数是扫描步长,一般就默认这么多;最后几个参数是:阈值,最小直线长度,最大直线间隔。
vector
容器类需要using namespace std;
阈值就是经过同一交点的直线根数,代表直线长度;最小直线长度默认设为0;最大间隔就是距离很近(最小间隔)的几条线会被认为是同一根线。
Scalar color = Scalar(0, 0, 0);
for (int i = 0; i < plines.size(); i++)
{
line(dst, Point(plines[i][0], plines[i][1]), Point(plines[i][2], plines[i][3]), color, 1, LINE_AA);
}
最后可以用这个函数把找到的直线显示出来。
9.2 霍夫圆检测
vector<Vec3f> pCircles;
HoughCircles(src, pCircles, int HOUGH_GRADIENT, double dp, double minDist, double para1, double para2, int minRadius, int maxRadius);
参数很多。前两个是输入输出。第三个室检测方法。一般用HOUGH_GRADIENT
这个。第四个是图片缩小,一般取1。第5个是分辨率,同心圆之间的最小距离。小于这个距离就认为是同一个园。第6个是canny边缘检测的低阈值。第7个是候选圆心累加的阈值。最后两个是找到的园的最大最小半径。
Canny阈值和累加阈值越高,检测要求越严格。
10.直方图操作
不仅每个像素,其实图像梯度等所有的图像统计数据都可以建立直方图。
10.1 直方图均衡化
equalizeHist(src, dst);
src
必须是8位单通道图像。直方图均衡化就是把直方图在灰度刻度上弄得分布更加均匀,具体计算方法设计复杂的概率论知识,可惜之前学的概率论已经全忘光了。
可用于增强对比度。