Opencv(2)基础
图像金字塔
拉普拉斯金字塔——用来重建一张图象,根据它的上层降采样图片
高斯金字塔——用来对图像进行降采样。降采样常用来简单加速算法,在小尺度上计算,再放大到原图上。
从底向上,逐层降采样,降采样之后,行列对半,即删除偶数行与列,得到降采样之后的上一层图片。
步骤:
- 对当前层进行高斯模糊
- 删除当前层偶数的行与列
得到降采样图象,只有原图的四分之一。
高斯差分(DOG,difference of Gaussian):同一张图片经过不同参数的高斯变换之后结果相减,得到的图象称为高斯差分。常用于特征抽取。
高斯差分是图象的内在特征,常用于灰度图像增强、角点检测。
API:
- 上采样:cv::pyrUp,zoom in ,放大,长宽各放大2倍。
- 降采样:cv::pyrDown,Zoom out,缩小,长宽各取一半。
/*11.图像金字塔*/ void section11() { Mat src; src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR); if (!src.data) { printf("load error!"); return; } namedWindow("input", CV_WINDOW_AUTOSIZE); imshow("input", src); // 上采样 Mat upImg; // 注意,这里的Size参数是(宽,高),对应的就是(cols,rows),而不是一般矩阵计算的(rows,cols) pyrUp(src, upImg, Size(src.cols * 2, src.rows * 2)); imshow("上采样", upImg); // 降采样 Mat downImg; // 默认就是宽高+1/2,应对奇偶问题 pyrDown(src, downImg, Size( (src.cols+1) / 2, (src.rows+1) / 2)); imshow("降采样", downImg); // 高斯差分,彩色 Mat d1, d2, dst; GaussianBlur(src, d1, Size(5, 5), 0, 0); GaussianBlur(d1, d2, Size(5, 5), 0, 0); // 两图相减 subtract(d1, d2, dst, Mat()); normalize(dst, dst, 255, 0, NORM_MINMAX); imshow("DOG-Color", dst); // 高斯差分,灰度 Mat gray,d3,d4,dst2; cvtColor(src, gray, CV_BGR2GRAY); // if both sigmas are zeros, they are computed from ksize.width and ksize.height,根据模板大小自动计算sigma GaussianBlur(gray, d3, Size(5, 5), 0, 0); GaussianBlur(d3, d4, Size(5, 5), 0, 0); // 两图相减 subtract(d3, d4, dst2, Mat()); // 通常高斯差分(差值)很小,需要进行归一化,下列公式以最大最小值归一化公式,实际进行的是强度的线性拉伸操作 normalize(dst2, dst2, 255, 0, NormTypes::NORM_MINMAX); imshow("DOG-Gray", dst2); waitKey(0); }
结果图
图象金字塔
高斯差分(分别为:灰度图DOG,彩色图DOG,原图)
基本阈值操作
阈值,threshold,实际就是一个中介值。图象阈值通常用于图象分割,而对灰度图进行阈值分割通常就是二值化。
- 阈值二值化(threshold binary), THRESH_BINARY = 0
- 阈值反二值化(threshold binary Inverted), THRESH_BINARY_INV = 1
- 阈值截断(truncate), THRESH_TRUNC = 2
- 阈值取零(threshold to zero), THRESH_TOZERO = 3
- 阈值反取零(threshold to zero Inverted), THRESH_TOZERO_INV = 4
阈值计算方法:THRESH_OTSU、THRESH_TRIANGLE.两种自动计算阈值的算法,前者又叫最大类间方差发(OTSU法,大律法)。threshold(gray, dst, &thresh_value, 255, THRESH_OTSU | THRESH_BINARY);//此时指定采用otsu方法自动计算阈值,指定的阈值thresh_value无效
阈值操作,指定输入图象只能为8位单通道图!!!否则出错!!!
完整代码:
// 构建一个用于callback传递数据的结构体
struct UserData {
Mat * gray;
int * thresh_value;
int *thresh_max;
int *op_value;
int *op_max;
// C++ 结构体可以有函数,如初始化函数
UserData(Mat *gray, int * thresh_value, int *thresh_max, int *op_value, int *op_max)
{
this->gray = gray;
this->thresh_value = thresh_value;
this->thresh_max = thresh_max;
this->op_value = op_value;
this->op_max = op_max;
}
};
/*12.阈值化*/
void section12()
{
Mat src,gray;
src = imread("D:/workspace/image/woman.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
cvtColor(src, gray, CV_BGR2GRAY);
int thresh_value = 127; // 自定义阈值初始值
int thresh_max = 255; // max threshold
int op_value = 0; // 对应THRESH_BINARY,二值化
int op_max = 4;
char title[] = "threshold window"; // title基本就是当常量用,就不传参了
UserData userdata(&gray, &thresh_value, &thresh_max, &op_value, &op_max);
// 声明两个Trackbar的回调函数,分别控制阈值及操作类型
void CallBack_Thresh(int, void*);
void CallBack_Operate(int, void*);
// 设置TrackBar
namedWindow(title, CV_WINDOW_AUTOSIZE);
createTrackbar("threshold_value:", title, &thresh_value, thresh_max, CallBack_Thresh, static_cast<void*>(&userdata));
createTrackbar("threshold_op:", title, &op_value, op_max, CallBack_Operate, static_cast<void*>(&userdata));
// 主动调用一下,让图显示出来,以触发后续的回调
CallBack_Thresh(thresh_value, (void*)(&userdata));
waitKey(0);
}
// section12,阈值化中Trackbar,用于threshold调节
void CallBack_Thresh(int threshold_value, void * data) {
// 传进来来的数据类型还原
UserData * userdata = (UserData*)(data);
// 每次在trackbar中调整了value,回去调整主调处的value。
// 这一步我猜不用做,因为createTrackbar中value参数是int*类型,估计内部做了这个操作了。
*(userdata->thresh_value) = threshold_value;
Mat dst;
threshold( *(userdata->gray), dst, *(userdata->thresh_value), *(userdata->thresh_max), *(userdata->op_value));
imshow("threshold window", dst);
}
// threshold operate的trackbar回调函数
void CallBack_Operate(int op_value, void * data) {
UserData * userdata = (UserData*)(data);
Mat dst;
threshold(*(userdata->gray), dst, *(userdata->thresh_value), *(userdata->thresh_max), *(userdata->op_value));
imshow("threshold window", dst);
}
结果图(依次为:原图,二值化,截断,反取零):
自定义线性滤波
卷积:使用kernel(卷积核)覆盖到图像上的某一个图块,然后覆盖的区域做乘加操作(卷积在离散函数上的操作),结果放在卷积核对应的锚点(通常是kernel的中心点)上。使该操作覆盖到图像的每一个像素上。
作用:增强、模糊、锐化、边缘检测。
API:filter2D
常见算子
- Robert
- Sobel
- Laplace
实验如下:
/*自定义线性滤波*/
void section13()
{
Mat src, gray;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
//pyrDown(src, src);
cvtColor(src, src, CV_BGR2GRAY);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
/*
// Robert算子
Mat robx, roby;
// robert x和y 方向算子
Mat k_rob_x = (Mat_<int>(2, 2) << 1, 0, 0, -1);
Mat k_rob_y = (Mat_<int>(2, 2) << 0, 1, -1, 0);
filter2D(src, robx, -1, k_rob_x);
filter2D(src, roby, -1, k_rob_y);
imshow("robert x", robx);
imshow("robert y", roby);
// 这里存在问题:数据溢出处理
imshow("robert x+y", robx + roby);
*/
/*
// Sobel算子
Mat sobx, soby;
Mat k_sob_x = (Mat_<int>(3, 3) << -1, 0, 1, -2, 0, 2, -1, 0, 1);
Mat k_sob_y = (Mat_<int>(3, 3) << -1, -2, -1, 0, 0, 0, 1, 2, 1);
filter2D(src, sobx, -1, k_sob_x);
filter2D(src, soby, -1, k_sob_y);
imshow("sobel x", sobx);
imshow("sobel y", soby);
imshow("sobel x+y", sobx + soby);
*/
// Laplace算子
Mat lap;
Mat k_lap = (Mat_<int>(3, 3) << 0, -1, 0, -1, 4, -1, 0, -1, 0);
filter2D(src, lap, -1, k_lap);
imshow("laplace", lap);
waitKey(0);
}
结果图:
robert算子,观察头发部分。
sobel算子
Laplace算子
卷积边缘问题
卷积的时候发现,图像的边界上存在一些行列,无法进行卷积处理(kernel覆盖的时候,若锚点覆盖到该区域像素上,会发生kernel超出原图的情况)。此时Opencv提供几种处理方案,它默认是BORDER_DEFAULT。
- BORDER_DEFAULT。
- BORDER_CONSTANT-填充边缘用指定的像素值,值由函数最后一个参数指定。
- BORDER_REPLICATE-用边缘的像素扩充复制
- BORDER_WRAP-用另外一边的像素来补偿填充
当然也可以自己给图像添加边缘:copyMakeBoder
代码如下:
/*14.卷积边缘处理*/
void section14() {
Mat src,dst;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
char c;
int bordertype = BORDER_DEFAULT;
int bordersize = src.cols*0.1;
RNG rng(12345);
while (1) {
c = waitKey(500);
if (c == 27){ // ESC pressed to quit
break;
}
if (c == 'c') {
bordertype = BORDER_CONSTANT;
}
else if (c == 'r') {
bordertype = BORDER_REFLECT;
}
else if (c == 'd') {
bordertype = BORDER_DEFAULT;
}
else if (c == 'a') {
bordertype = BORDER_WRAP;
}
// 为图像添加边缘
Scalar color(rng.uniform(0,255),rng.uniform(0,255),rng.uniform(0,255)); // color 仅在 BORDER_CONSTANT下作用
copyMakeBorder(src, dst,bordersize, bordersize, bordersize, bordersize,bordertype,color);
imshow("border demo", dst);
}
}
效果图
Sobel算子
图像边缘提取:边缘的特征——亮度突变,相对(邻域像素)而言,该点值突变,体现到函数上,就是导数值很大。同理,物体内部(表面)通常导数值较小,体现到图像上就是值相近,平滑。
Sobel:离散的微分算子,用来计算图像灰度的近似梯度。集合了高斯平滑和微分求导。(求导在离散上的运算)。
梯度: G=G2x+G2y−−−−−−−√ G = G x 2 + G y 2 ,但是在图像求导中,我们关注的是值得变化率,而不是变化方向(变大还是变小),即导数得量值是重点,符号无所谓。且计算机计算得时候开放、平方操作开销太大,故用以下公式近似梯度。
附:Sobel在kernel=3时不是很准确,Opencv采用了改进得Scharr函数:kernel=(Mat_<int>(3,3)<<-3,0,3,-10,0,10,-3,0,3);
方法:cv::sobel,cv::scharr。
注:一切梯度算子对噪声都很敏感,故都需要先做去噪处理。
代码如下
/*15.Sobel算子*/
void section15() {
Mat src, dst;
src = imread("D:/workspace/image/woman.jpg", IMREAD_COLOR);
pyrDown(src, src);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// blur
GaussianBlur(src, src, Size(3, 3), 0, 0);
// gray
cvtColor(src, src, CV_BGR2GRAY);
imshow("blured_gray", src);
// 1.若不convertScaleAbs,显示图像处问题,因为sobel操作之后会有负值。
// 2.Sobel函数得第三个类型为输出数据类型,一定要范围大于等于输入图像。但若等于输入图像(-1),可能发生数据截断
// 3.Sobel函数得第4、5个参数为卷积模板选择,x还是y方向,不能同时选中(1),结果很差,不知道内部源码如何处理得.
Mat sobelx, sobely,sobelxy;
Sobel(src, sobelx, CV_16S, 1, 0);
Sobel(src, sobely, CV_16S, 0, 1);
convertScaleAbs(sobelx, sobelx);
convertScaleAbs(sobely, sobely);
imshow("x", sobelx);
imshow("y", sobely);
// 存在溢出
sobelxy = sobelx + sobely;
imshow("xy+", sobelxy);
addWeighted(sobelx, 0.5, sobely, 0.5, 0, sobelxy);
imshow("xy_add", sobelxy);
waitKey(0);
}
Laplace算子
Sobel算子的意义是函数的一阶求导,通过导数的最大值可以认为是图像边缘。而Laplace是二阶求导。它采用的是L=X偏导+Y偏导。opencv提供api:cv::Laplace。
流程:gray—blur—laplace—convertScaleAbs—result
代码如下:
/*16.Laplace*/
void section16() {
Mat src,gray,lap;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
// 显示原图
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// gray
cvtColor(src, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3),0);
Laplacian(gray, lap, CV_16S, 3);
convertScaleAbs(lap, lap);
//threshold(lap, lap, 0, 255, THRESH_OTSU | THRESH_BINARY);
imshow("Laplace", lap);
waitKey(0);
}
结果
Canny边缘检测
步骤:Blur-Gray-计算梯度(sobel、scharr)-非最大信号抑制-高低阈值输出二值图像。
Canny算法提出了3个边缘检测的准则:信噪比准则(低错误率)、定位精度准则、单边缘响应准则。
非最大信号抑制:用来解决定位精度问题。问题产生是由于一般的求边缘的微分算子,结果中的边缘总是粗线条的,而非最大信号抑制是根据梯度方向,抑制该方向上的较小的值,只保留最大的梯度值。比如连续的三个像素梯度分别为3,4,5,且方向一致。那么保留5那个像素,3、4抑制为0,这样结果就是精细的边缘线条。至于方向,用arctan(x偏导/y偏导)即可求出,再就近取到0,45,90,135,180,225,270,315,360,上,表达了它指向8个邻域的方向。
高低阈值连接:设置高低阈值T1,T2,小于低阈值的舍弃,认为是可能的误检测,高于高阈值的,认为是强边缘,保留。
API:cv::Canny
参考:Canny算法
代码如下:
/*17.Canny算法*/
void canny()
{
Mat src, gray, can;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
// 显示原图
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
cvtColor(src, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3), 0);
// Canny中的双阈值,这里设置低阈值为ThreshLow通过TrackBar调节。高阈值通过阈值比为2:1求得(通常为2:1或3:1)
int threshLow = 30; // default = 30
Mat * userdata[] = { &can, &gray }; // 用来传递callback中使用到的用户数据
namedWindow("Canny", CV_WINDOW_AUTOSIZE);
void CallBack_Canny(int, void*); // 函数声明
createTrackbar("ThreshLow", "Canny", &threshLow, 255, CallBack_Canny, userdata);
// 普通调用函数显示图像
CallBack_Canny(threshLow, userdata);
waitKey(0);
}
// canny trackbar callback.(注:这是一个普通函数,可用户调用。同时又是一个回调函数,可通过trackbar自动触发)
void CallBack_Canny(int threshLow, void* userdataArr) {
// 这里语法问题,调试了很久。关键:userdataArr是mat*的二维数组。简单说就是[]和*算是一对等价的记号。数组[]还原的时候不知道长度,所以用*,这样就变成了Mat**
Mat ** userdata = (Mat**)userdataArr;
Mat * can = *(userdata+0); // 二维数组第一行(还是一个数组),实际只有一个元素
Mat * gray = *(userdata+1);// 同理,第二行,内部也只有一个元素,所以可以对它直接取值操作就是gray
// 后两个参数分别是
// Sobel的windowsize,取默认的3,窗口为3模板进行梯度计算
// 最后一个参数,取默认的false,意为求梯度时用近似计算(范数为1的公式),而true为标准的2范数的公式
Canny(*gray, *can, threshLow, threshLow * 2, 3, false);
// 显然这里*gray可以用gray[0],*can可以用can[0]
imshow("Canny", *can);
}
结果图:
霍夫变换(提取直线)
霍夫变换的本质是直角坐标系到极坐标中。
前提:边缘检测已经完成。
原理:对于一条直线。直线上的点做变换:X cos(theta) + y sin(theta) = r ,r表示该直线到原点的距离,theta为r与x轴夹角。
API:
HoughLines霍夫变换,将灰度图变换到霍夫空间中。需要自己反变换到平面坐标系。
HoughLinesP,直接返回霍夫变换找到的直线的两端点。
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, doubleminLineLength=0, double maxLineGap=0 )
- image – 8-bit, single-channel binary source image. The image may be modified by the function. 输入的图像应该是8为单通道二值图像
- lines – Output vector of lines. Each line is represented by a 4-element vector (x_1, y_1, x_2, y_2) , where(x_1,y_1) and (x_2, y_2) are the ending points of each detected line segment. 输出的矢量。输出的两点是线段的两个端点 .
- rho – Distance resolution of the accumulator in pixels. 极径参数的距离分辨率
- theta – Angle resolution of the accumulator in radians.
极角参数的角度分辨率 - threshold – Accumulator threshold parameter. Only those lines are returned that get enough votes (>\texttt{threshold} ). 设定的阈值,大于此阈值的交点,才会被认为是一条直线
- minLineLength – Minimum line length. Line segments shorter than that are rejected.
线段的最小长度 - maxLineGap – Maximum allowed gap between points on the same line to link them.
点到直线被允许的最大距离
参考:
opencv中的标准霍夫线变换(HoughLines)和统计霍夫变换(HoughLinesP)
代码:
/*18.Hough变换-直线检测*/
void hough() {
Mat src, gray, can, hou;
src = imread("D:/workspace/image/hough_test.png", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
cvtColor(src, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3), 0);
int threshLow = 70;
Canny(gray, can, threshLow, threshLow * 2, 3, false);
Mat result = Mat::zeros(src.size(), src.type());
vector<Vec4i> lines;
HoughLinesP(can, lines, 1, CV_PI / 180, 80, 30, 8);
for (size_t i = 0; i < lines.size(); i++)
{
line(result, Point(lines[i][0], lines[i][1]),Point(lines[i][2], lines[i][3]), Scalar(0, 0, 255), 1, 8);
}
imshow("gray", gray);
imshow("can", can);
imshow("result", result);
waitKey(0);
}
效果图
霍夫变换(曲线检测)
原理和直线检测一样,其实本质就是方程的坐标变换。对于直线也好,圆也好,都不只有一种表达方式,他们在不同的表达形式下,存在某些特性。
HoughCircles
代码
/*19.Hough检测圆弧*/
void hough_circle() {
Mat src, gray, hou;
src = imread("D:/workspace/image/hough_circle_test.png", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
cvtColor(src, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3), 0);
Mat result = src.clone();
// find circle
vector<Vec3f> circles;
HoughCircles(gray, circles, CV_HOUGH_GRADIENT, 1, 10, 100, 30, 5, 50);
for (int i = 0; i < circles.size(); i++) {
Vec3f cir = circles[i];
circle(result, Point(cir[0], cir[1]), cir[2], RED, 2, LINE_AA);
circle(result, Point(cir[0], cir[1]), 2, RED, 1);
}
imshow("input", src);
imshow("gray", gray);
imshow("result", result);
waitKey(0);
}
效果