1.自定义线性滤波–卷积
卷积工作原理–即中心值根据周围定义大小邻域,从而计算成新值
常见的卷积算子:
//Sobel算子--获取边缘比较强,用于边缘检测
Mat SX, SY;
Mat sobel_x = (Mat_<int>(3, 3) << -1, 0, 1, -2, 0, 2, -1, 0, 1);
filter2D(src, SX, -1, sobel_x, Point(-1, -1), 0.0);
//imshow("sobel_x", SX);
Mat sobel_y = (Mat_<int>(3, 3) << -1, -2, -1, 0, 0, 0, 1, 2, 1);
filter2D(src, SY, -1, sobel_y, Point(-1, -1), 0.0);
//imshow("sobel_y", SY);
//拉普拉斯算子--获取比较完整的边缘信息
Mat laplas;
Mat laplas_k = (Mat_<int>(3, 3) << 0, -1, 0, -1, 4, -1, 0, -1, 0);
filter2D(src, laplas,-1,laplas_k, Point(-1, -1), 0.0);
//imshow("lap", laplas);
//自定义卷积加深模糊--定义卷积并且随着时间而扩大结构面积,从而加大模糊程度
Mat autoK;
int c = 0;
int index = 0;
int ksize = 3;
while (true)
{
c = waitKey(500);//图像变换延迟时间
if ((char)c == 27) {//c==27-->按ESC退出
break;
}
ksize = 4 + (index % 8) * 2 + 1;//卷积最小面积从5开始,随后逐渐加大
Mat kernel = Mat::ones(Size(ksize, ksize), CV_32F) / (float)(ksize * ksize);//卷积计算公式
filter2D(src, autoK, -1, kernel, Point(-1, -1));
index++;
imshow("auto", autoK);
}
int top = (int)(0.05 * src.rows);//定义边缘扩张的值
int bottom = (int)(0.05 * src.rows);//定义边缘扩张的值
int left = (int)(0.05 * src.cols);//定义边缘扩张的值
int right = (int)(0.05 * src.cols);//定义边缘扩张的值
RNG rng(12345);//定义一个随机数种子,方便生成随机数
int borderType = BORDER_DEFAULT;//用周围的行和列图像取填充扩张区域
int c = 0;
while (true)
{
c = waitKey(500);//等待时间
//ESC
if ((char)c == 27) {
break;
}
if ((char)c == 'r') {
borderType = BORDER_REPLICATE;//用边缘的颜色取插值填充
}
else if ((char)c == 'w') {
borderType = BORDER_WRAP;//换行填充,即top用图片下方填充,即用另一边图像来补偿填充
}
else if ((char)c == 'c') {
borderType = BORDER_CONSTANT;//用随机生成的颜色填充
}
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));//生成0-255像素值的颜色
copyMakeBorder(src, dst, top, bottom, left, right, borderType, color);//增加图像边缘
imshow("output_Img", dst);
3. sobel算子–轮廓提取
简言之就是获取变化比较大的数据,也就是轮廓数据
相比于平方数相加再开方,绝对值相加虽然不够精确,但是可以大大减少运算时间
相比于Sobel算子,Scharr算是优化算法,可以获得更加完整的轮廓信息,并且受噪点影响较少,用法相似
具体代码实现:
//Sobel算子提取轮廓
/*
1.高斯模糊/平滑(sobel算子对噪声非常敏感,所以要先模糊一下)
2.转灰度图像
3.求x,y梯度
4.获取最终图像(振幅图像)
*/
Mat gray_src;
GaussianBlur(src, dst, Size(3, 3), 0, 0);
cvtColor(dst, gray_src, CV_BGR2GRAY);
imshow("gray_Img", gray_src);
Mat xgrad, ygrad;
//Sobel(gray_src, xgrad, CV_16S, 1, 0, 3);//CV_16S--输出图像深度,-1为原来深度,深度精度越大越清晰,1-x方向1阶导数,0-y不算,3-ksize
//Sobel(gray_src, ygrad, CV_16S, 0, 1, 3);
//Sobel算子的改进版本--可以获取更加完整的轮廓信息,并且不会受到噪声干扰
Scharr(gray_src, xgrad, CV_16S, 1, 0);
Scharr(gray_src, ygrad, CV_16S, 0, 1);
//因为用sobel算子运算后的结果可能像素为负数,所以要将结果计算绝对值,保证大于0
convertScaleAbs(xgrad, xgrad);
convertScaleAbs(ygrad, ygrad);
imshow("x_img", xgrad);
imshow("y_img", ygrad);
//将x,y各以50%权重合并,合成完整轮廓图
//Mat xygrad;
//addWeighted(xgrad, 0.5, ygrad, 0.5, 0, xygrad);//0-gamma值
//addWeighted--相当于将两张图片的像素相加,如下所示
Mat xygrad = Mat(xgrad.size(), xgrad.type());//定义xygrad和xgrad的大小和类型一样,防止做样
int width = xgrad.cols;
int height = ygrad.rows;
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++) {
//处理每个像素并相加
int xg = xgrad.at<uchar>(row, col);
int yg = xgrad.at<uchar>(row, col);//对象.at<数据类型>(x,y)--获取对象的对应像素值
int xy = xg + yg;//相当于两个绝对值相加(|x|+|y|),相比两个平方后相加再开放可以减少计算量((x^2+y^2)^1/2),优化了算法
xygrad.at<uchar>(row, col) = saturate_cast<uchar>(xy);//saturate_cast--将获取的像素值限定在0-255范围内
}
}
imshow("final_Img", xygrad);
//laplance算子--边缘提取
//处理流程:
//1.高斯模糊--去噪声
//2.转换为灰度图像
//3.拉普拉斯-二阶导数计算
//4.取绝对值
//5.显示结果
Mat gray_src, edge_img;
GaussianBlur(src, dst, Size(3, 3), 0, 0);
cvtColor(dst, gray_src, CV_BGR2GRAY);
Laplacian(gray_src, edge_img, CV_16S, 3);//3-ksize
convertScaleAbs(edge_img, edge_img);
//因为拉普拉斯提取的噪声比较多,所以需要用切割算法,降低一些噪声
//threshold(edge_img, edge_img, 0, 255, THRESH_OTSU | THRESH_BINARY);//THRESH_OTSU--自动取阈值,二值化切割
namedWindow("output_img",WINDOW_AUTOSIZE);
imshow("output_img", edge_img);
非最大信号抑制:
因为即便是sabel算子计算梯度的轮廓还是会有不止一个像素,即比较”粗“。而这个步骤则是细化边缘,见轮廓抑制为1像素,所以通过该算法将局部最大值外的所有值都置为0,保留唯一像素。
1.将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较
2.如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点,否则该像素点将被抑制
bool nonmax_suppress(double theta, Mat &g_mat, Point anchor, double *p1_v, double *p2_v)
{
//计算8邻域灰度
uchar N = g_mat.at<uchar>(Point(anchor.x,anchor.y + 1));
uchar S = g_mat.at<uchar>(Point(anchor.x,anchor.y - 1));
uchar W = g_mat.at<uchar>(Point(anchor.x - 1,anchor.y));
uchar E = g_mat.at<uchar>(Point(anchor.x + 1,anchor.y));
uchar NE = g_mat.at<uchar>(Point(anchor.x + 1,anchor.y + 1));
uchar NW = g_mat.at<uchar>(Point(anchor.x - 1,anchor.y + 1));
uchar SW = g_mat.at<uchar>(Point(anchor.x - 1,anchor.y - 1));
uchar SE = g_mat.at<uchar>(Point(anchor.x + 1,anchor.y - 1));
uchar M = g_mat.at<uchar>(Point(anchor));
double angle = theta * 360 / (2 * CV_PI);//计算角度
//判定角度范围 计算 p1,p2插值
if(angle > 0 && angle < 45)
{
*p1_v = (1- tan(theta)) * E + tan(theta) * NE;
*p2_v = (1- tan(theta)) * W + tan(theta) * SW;
}
else if(angle >= 41 && angle < 90)
{
*p1_v = (1- tan(theta)) * NE + tan(theta) * N;
*p2_v = (1- tan(theta)) * SW + tan(theta) * S;
}
else if(angle >= 90 && angle < 135)
{
*p1_v = (1- tan(theta)) * N + tan(theta) * NW;
*p2_v = (1- tan(theta)) * S + tan(theta) * SE;
}
else
{
*p1_v = (1- tan(theta)) * NW + tan(theta) * W;
*p2_v = (1- tan(theta)) * SE + tan(theta) * E;
}
if(M < *p1_v || M < *p2_v) //非最大抑制
{
return false;
}
else
return true;
}
高低阈值输出二值图像–滞后双阈值化
原理
在施加非极大值抑制之后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素。
为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,可以通过选择高低阈值来实现。
如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;如果边缘像素的梯度值小于低阈值,则会被抑制。阈值的选择取决于给定输入图像的内容。
对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。
另一种方案是用搜索算法,通过强边缘点,搜索8领域是否存在弱边缘,如果有,以弱边缘点为中心继续搜索,直到搜索不到弱边缘截止。
int bfs(Mat &mag, Mat &dst, int i, int j, int low)
{
int flag = 0;
if(dst.at<uchar>(j, i) == 0)//没有搜索过
{
dst.at<uchar>(j, i) = 255;//设置为255代表搜索过
for(int n = -1; n <= 1; n++)
{
for(int m = -1; m <= 1; m++)
{
if(m == 0 && n == 0) continue;
//如果点在图像内,并且高于低阈值
if(range(mag, i+n, j+m) && mag.at<uchar>(j+m, i+n) >= low)
if(bfs(mag, dst, i+n, j+m, low))//迭代搜索直到搜索不到高于低阈值的点。
{
flag = 1;
break;
}
}
if(flag)
break;
}
return 1;
}
return 0;
}
CANNY算法–API
L2–表示用(x^2 + y2)1/2的方法算出G
L1–直接使用绝对值相加,简化算术过程,但是没那么精确
const char* OUTPUT_TITLE = "output_title";
int t1_value = 0;//最小值
int max_value = 255;//最大值
void Canny_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("C:/Users/18929/Desktop/博客项目/项目图片/01.jpg");
if (src.empty()) {
printf("could not load image");
return -1;
}
namedWindow("input_image", WINDOW_AUTOSIZE);
namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
imshow("input_image", src);
//先转换成灰度图像,再放进去操作
cvtColor(src, gray_src,CV_BGR2GRAY);
createTrackbar("Threshold_Value:",OUTPUT_TITLE,&t1_value,max_value,Canny_Demo);//创建一个滚动条,方便测试
Canny_Demo(0, 0);
waitKey(0);
return 0;
}
void Canny_Demo(int, void*) {
Mat edge_output;
blur(gray_src, gray_src, Size(3, 3),Point(-1,-1),BORDER_DEFAULT);
Canny(gray_src, edge_output, t1_value, t1_value * 2 , 3, false);//false表示用默认L1归一化
//也可以将原本图像的颜色赋给边缘图,得到有颜色的图像
dst.create(src.size(), src.type());//生成一张和src一样的对象
src.copyTo(dst, edge_output);//edge_output作为Mask模板,将src的色彩给进去
imshow(OUTPUT_TITLE, dst);
}