一、Sobel边缘检测算子
-
卷积应用 — 提取边缘
图像在边缘处会发生像素值的跃迁,即像素值发生显著变化。对图像进行一阶求导,导数值变化越大,说明边缘信号越强。
Sobel 算子是一个离散微分算子 (discrete differentiation operator)。 它结合了高斯平滑和微分求导。
通过权重不同扩大水平(垂直)方向上的差异,求出图像灰度函数的近似梯度。
-
API
Sobel( src , dst, int depth, int dx, int dy, int ksize=3, double scale=1,double delta=0,intborderType=BORDER_DEFAULT )
各参数的意义如下:
src – 输入图像;
dst – 输出图像;
depth –输出图片深度;
dx – x方向导数运算参数;
dy – y方向导数运算参数;
ksize – Sobel内核的大小,可以是:1,3,5,7;
(以下三个参数一般不做改动)
scale – 可选的缩放导数的比例常数,默认值是1;
delta – 可选的增量常数被叠加到导数中,默认值是0;
borderType – 用于判断图像边界的模式,默认模式是BORDER_DEFAULT。
输入图像支持深度和输出图像支持深度的关系如下:
(当 depth为-1时, 输出图像将和输入图像有相同的深度。输入8位图像则会截取顶端的导数。 )
- 操作步骤
1.高斯模糊
GaussianBlur( src, src, Size(3,3), 0, 0 );
2.转化为灰度图像
cvtColor( src, src_gray,COLOR_RGB2GRAY );
3.x、y方向上梯度值计算
Mat grad_x, grad_y;
//x方向梯度计算
Sobel( src_gray, grad_x, CV_16S, 1, 0, 3,);
//y方向梯度计算
Sobel( src_gray, grad_y, CV_16S, 0, 1, 3);
4.得到最终梯度图像
计算近似梯度值公式:
方法一:用相关API
Mat abs_grad_x, abs_grad_y;
convertScaleAbs( grad_x, abs_grad_x ); //取x方向像素绝对值
convertScaleAbs( grad_y, abs_grad_y ); //取y方向像素绝对值
addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, zuizhong ); //加权和
方法二:用自定义算法对每一个像素进处理
Mat grad_xy = Mat(grad_x.size(),grad_x.type());
int width = grad_x.cols;
int height = grad_y.rows;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int x = grad_x.at<uchar>(row, col);
int y = grad_y.at<uchar>(row, col);
int x = x + y;
grad_xy.at<uchar>(row, col) = saturate_cast<uchar>(xy);
}
}
二、Laplance算子
在一阶导数的极值位置,二阶导数为0。所以我们也可以用这个特点来作为检测图像边缘的方法。
- API
Laplacian( src, dst, int depth, int ksize=3, double scale=1,double delta=0,intborderType=BORDER_DEFAULT );
由于 Laplacian使用了图像梯度,它内部调用了 Sobel 算子,具体参数意义参考上面Sobel算子部分。
- 处理步骤
1.高斯模糊 — 去噪声
GaussianBlur( src, src, Size(3,3), 0, 0 );
2.转换为灰度图像
cvtColor( src, src_gray, COLOR_RGB2GRAY );
3.拉普拉斯 — 二阶导数计算
Mat abs_dst;
Laplacian( src_gray, dst, CV_16S, 3 );
4.取绝对值
convertScaleAbs( dst, abs_dst );
三、Canny边缘检测算法
- API
Canny( src, dst, lowThreshold, lowThreshold*ratio, kernel_size );
输入参数:
detected_edges: 原灰度图像
detected_edges: 输出图像 (支持原地计算,可为输入图像)
lowThreshold: 用户通过 trackbar设定的值。
highThreshold: 设定为低阈值的2倍
kernel_size: 设定为 3 (Sobel内核大小,内部使用)
- 处理步骤
1.创建与 src 同类型和大小的矩阵(dst)
dst.create( src.size(), src.type() );
2.转换为灰度图像
cvtColor( src, src_gray, CV_BGR2GRAY );
3.创建trackbar,来获取用户交互输入的低阈值
int threshold_value = 50;//定义阈值
int threshold_max = 255;
createTrackbar( "Min Threshold:", output, &threshold_value, threshold_max, Canny_Demo );
4.首先, 使用 3x3的内核平滑图像
blur(src_gray, src_gray, Size(3, 3));
5.用Canny检测边缘
Canny(src_gray, output, threshold_value, threshold_value * 2, 3);
6.填充 dst 图像
dst = Scalar::all(0);
7.使用函数 copyTo 标识被检测到的边缘部分
src.copyTo(dst, output);
完整代码如下
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, src_gray, dst;
int threshold_value = 50;
int threshold_max = 255;
const char* OUTPUT_TITLE = "Canny Result";
void Canny_Demo(int, void*);
int main(int argc, char** argv)
{
src = imread("C:/Users/XWT11/Desktop/pp.png");
if (!src.data)
{
return -1;
}
namedWindow(OUTPUT_TITLE, WINDOW_NORMAL);
dst.create(src.size(), src.type());
cvtColor(src, src_gray, COLOR_BGR2GRAY);
createTrackbar( "Min Threshold:", output, &threshold_value, threshold_max, Canny_Demo );
Canny_Demo(0, 0);
waitKey(0);
return 0;
}
void Canny_Demo(int, void*)
{
Mat output;
blur(src_gray, src_gray, Size(3, 3));
Canny(src_gray, output, threshold_value, threshold_value * 2, 3, false);
dst = Scalar::all(0);
src.copyTo(dst, output);
imshow(OUTPUT_TITLE,~ output);
}
}
四、霍夫直线变换
-原理(以下摘自《OpenCv3编程入门》)
对于霍夫变换, 我们将用 极坐标系 来表示直线.
化简得:
对于点 (x,
y), 我们可以将通过这个点的一族直线统一定义为:
这就意味着每一对 (r,theta) 代表一条通过点 (x, y) 的直线. 如果对于一个给定点 (x, y)
我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点 x = 8 and y= 6 我们可以绘出下图
我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面 \theta - r 相交,
这就意味着它们通过同一条直线.
一条直线能够通过在平面 theta - r 寻找交于一点的曲线数量来 检测. 越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成.
一般来说我们可以通过设置直线上点的 阈值 来定义多少条曲线交于一点我们才认为 检测 到了一条直线.这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点. 如果交于一点的曲线的数量超过了 阈值, 那么可以认为这个交点所代表的参数对
(theta,r) 在原图像中为一条直线.
在执行霍夫线变换之前应先用Canny算子对图像进行边缘检测
Canny(src, src_gray, 150, 200);
1.标准霍夫线变换
API
执行变换
vector<Vec2f> lines;
HoughLines(src_gray, lines, 1, CV_PI / 180, 150, 0, 0);
画出检测到的直线
for (size_t i = 0; i < lines.size(); i++)
{
float rho = lines[i][0]; // 极坐标中的r长度
float theta = lines[i][1]; // 极坐标中的角度
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
// 转换为平面坐标的四个点
pt1.x = cvRound(x0 + 1000 * (-b));
pt1.y = cvRound(y0 + 1000 * (a));
pt2.x = cvRound(x0 - 1000 * (-b));
pt2.y = cvRound(y0 - 1000 * (a));
line(dst, pt1, pt2, Scalar(255, 255, 255), 1, LINE_AA);
}
2.统计概率霍夫线变换
这是执行起来效率更高的霍夫线变换. 输出检测到的直线的两个端点
- API
执行变换
vector<Vec4f> plines;
HoughLinesP(src_gray, plines, 1, CV_PI / 180.0, 10, 0, 10);
画出检测到的直线
for (size_t i = 0; i < plines.size(); i++)
{
Vec4f hline = plines[i];
line(dst, Point(hline[0], hline[1]), Point(hline[2], hline[3]),Scalar color = Scalar(255, 255, 255);, 3, LINE_AA);
五、像素重映射
- 概念
把一个图像中一个位置的像素放置到另一个图片指定位置的过程.
为了完成映射过程, 有必要获得一些插值为非整数像素坐标,因为源图像与目标图像的像素坐标不是一一对应的.
我们通过重映射来表达每个像素的位置 (x,y) :
g(x,y) = f ( h(x,y) )
这里 g() 是目标图像, f() 是源图像, h(x,y) 是作用于 (x,y) 的映射方法函数.
- API
remap( src, dst, map_x, map_y, CV_INTER_LINEAR, BORDER_CONSTANT, color)
参数说明:
src: 源图像
dst: 目标图像,与 src 相同大小
map_x: x方向的映射参数. 它相当于方法 h(i,j) 的第一个参数
map_y: y方向的映射参数. 注意 map_y 和 map_x 与 src 的大小一致。
CV_INTER_LINEAR: 非整数像素坐标插值标志. 这里给出的是默认值(双线性插值).
BORDER_CONSTANT: 默认
- 处理步骤
1.创建目标图像和两个映射矩阵.( x 和 y )
dst.create( src.size(), src.type() );
map_x.create( src.size(), CV_32FC1 );
map_y.create( src.size(), CV_32FC1 );
2.更新重映射矩阵
//双层循环,历遍每一个像素点,改变map_x、map_y的值
for (int row = 0; row < src.rows; row++)
{
for (int col = 0; col < src.cols; col++)
{
//此处加入改变map_x、map_y的值的函数
}
四种不同映射:
a.图像宽高缩小一半,并显示在中间:
if (col > (src.cols * 0.25) && col <= (src.cols*0.75) && row > (src.rows*0.25) && row <= (src.rows*0.75))
{
map_x.at<float>(row, col) = 2 * (col - (src.cols*0.25));
map_y.at<float>(row, col) = 2 * (row - (src.rows*0.25));
}
else {
map_x.at<float>(row, col) = 0;
map_y.at<float>(row, col) = 0;
}
b.图像上下颠倒:
map_x.at<float>(row, col) = (src.cols - col - 1);
map_y.at<float>(row, col) = row;
c.图像左右颠倒:
map_x.at<float>(row, col) = col;
map_y.at<float>(row, col) = (src.rows - row - 1);
d.同时执行b和c的操作:
map_x.at<float>(row, col) = (src.cols - col - 1);
map_y.at<float>(row, col) = (src.rows - row - 1);
3.进行重映射操作
remap(src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 255, 255));
效果
a.
b.
c.
d.