图像处理:变换——Canny 边缘检测器 OpenCV v4.8.0

上一个教程拉普拉斯算子

下一个教程霍夫线变换

原作者Ana Huamán
兼容性OpenCV >= 3.0

目标

在本教程中,您将学习如何

  • 使用 OpenCV 函数 cv::Canny 实现 Canny 边缘检测器。

原理

Canny 边缘检测器 [45] 由 John F. Canny 于 1986 年开发。Canny 算法也被许多人称为最优检测器,它旨在满足三个主要标准:

  • 低错误率: 即只对存在的边缘进行良好的检测。
  • 良好的定位: 检测到的边缘像素与真实边缘像素之间的距离必须最小。
  • 最小响应: 每个边缘只有一个检测器响应。

步骤

  1. 滤除噪音。为此可使用高斯滤波器。下面是可能使用的尺寸=5 的高斯内核的示例:
    K = 1 159 [ 2 4 5 4 2 4 9 12 9 4 5 12 15 12 5 4 2 9 4 12 5 9 4 4 2 ] K=\frac{1}{159}\left[ \begin{matrix} 2& 4& 5& \begin{matrix} 4& 2\\ \end{matrix}\\ 4& 9& 12& \begin{matrix} 9& 4\\ \end{matrix}\\ 5& 12& 15& \begin{matrix} 12& 5\\ \end{matrix}\\ \begin{array}{c} 4\\ 2\\ \end{array}& \begin{array}{c} 9\\ 4\\ \end{array}& \begin{array}{c} 12\\ 5\\ \end{array}& \begin{matrix} \begin{array}{c} 9\\ 4\\ \end{array}& \begin{array}{c} 4\\ 2\\ \end{array}\\ \end{matrix}\\ \end{matrix} \right] K=1591 245424912945121512542941259442

  2. 找到图像的强度梯度。为此,我们采用与索贝尔类似的程序:
    a. 应用一对卷积掩码(x 和 y 方向):
    G x = [ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] G_x=\left[ \begin{matrix} -1& 0& +1\\ -2& 0& +2\\ -1& 0& +1\\ \end{matrix} \right] Gx= 121000+1+2+1
    G y = [ − 1 − 2 − 1 0 0 0 + 1 + 2 + 1 ] G_y=\left[ \begin{matrix} -1& -2& -1\\ 0& 0& 0\\ +1& +2& +1\\ \end{matrix} \right] Gy= 10+120+210+1
    b. 求梯度强度和方向:
    G = G x 2 + G x 2 G=\sqrt{G_x^2+G_x^2} G=Gx2+Gx2
    θ = arctan ⁡ ( G y G x ) \theta =\arctan \left( \frac{G_y}{G_x} \right) θ=arctan(GxGy)

    方向取整为四个可能角度之一(即 0、45、90 或 135)

  3. 应用非最大值抑制。这将删除不被认为是边缘一部分的像素。因此,只会保留细线(候选边缘)。

  4. 滞后: 最后一步。Canny 使用两个阈值(上限和下限):

    a. 如果像素梯度值高于上阈值,则该像素被视为边缘
    b. 如果像素梯度值低于下阈值,则会被拒绝。
    c. 如果像素梯度值介于两个阈值之间,那么只有当它与高于上阈值的像素相连时才会被接受。

Canny 推荐的上下限比例在 2:1 和 3:1 之间。

  1. 如需了解更多详情,您可以随时查阅自己喜欢的《计算机视觉》(Computer Vision)书籍。

代码

C++
教程代码如下所示。您也可以从此处下载


#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
Mat src, src_gray;
Mat dst, detected_edges;
int lowThreshold = 0;
const int max_lowThreshold = 100;
const int ratio = 3;
const int kernel_size = 3;
const char* window_name = "Edge Map";
static void CannyThreshold(int, void*)
{
 blur( src_gray, detected_edges, Size(3,3) );
 Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
 dst = Scalar::all(0);
 src.copyTo( dst, detected_edges);
 imshow( window_name, dst );
}
int main( int argc, char** argv )
{
 CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
 src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // Load an image
 if( src.empty() )
 {
 std::cout << "Could not open or find the image!\n" << std::endl;
 std::cout << "Usage: " << argv[0] << " <Input image>" << std::endl;
 return -1;
 }
 dst.create( src.size(), src.type() );
 cvtColor( src, src_gray, COLOR_BGR2GRAY );
 namedWindow( window_name, WINDOW_AUTOSIZE );
 createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
 CannyThreshold(0, 0);
 waitKey(0);
 return 0;
}

Java
教程代码如下所示。您也可以从此处下载

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Image;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
public class CannyDetectorDemo {
 private static final int MAX_LOW_THRESHOLD = 100;
 private static final int RATIO = 3;
 private static final int KERNEL_SIZE = 3;
 private static final Size BLUR_SIZE = new Size(3,3);
 private int lowThresh = 0;
 private Mat src;
 private Mat srcBlur = new Mat();
 private Mat detectedEdges = new Mat();
 private Mat dst = new Mat();
 private JFrame frame;
 private JLabel imgLabel;
 public CannyDetectorDemo(String[] args) {
 String imagePath = args.length > 0 ? args[0] : "../data/fruits.jpg";
 src = Imgcodecs.imread(imagePath);
 if (src.empty()) {
 System.out.println("Empty image: " + imagePath);
 System.exit(0);
 }
 // 创建并设置窗口。
 frame = new JFrame("Edge Map (Canny detector demo)");
 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)// 设置内容窗格。
 Image img = HighGui.toBufferedImage(src)addComponentsToPane(frame.getContentPane(), img)// 使用内容窗格的默认边框布局。无需
 // setLayout(new BorderLayout());
 // 显示窗口。
 frame.pack();
 frame.setVisible(true);
 }
 private void addComponentsToPane(Container pane, Image img) {
 if (!(pane.getLayout() instanceof BorderLayout)) {
 pane.add(new JLabel("Container doesn't use BorderLayout!"));
 return;
 }
 JPanel sliderPanel = new JPanel();
 sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.PAGE_AXIS));
 sliderPanel.add(new JLabel("Min Threshold:"));
 JSlider slider = new JSlider(0, MAX_LOW_THRESHOLD, 0);
 slider.setMajorTickSpacing(10);
 slider.setMinorTickSpacing(5);
 slider.setPaintTicks(true);
 slider.setPaintLabels(true);
 slider.addChangeListener(new ChangeListener() {
 @Override
 public void stateChanged(ChangeEvent e) {
 JSlider source = (JSlider) e.getSource();
 lowThresh = source.getValue();
 update();
 }
 });
 sliderPanel.add(slider);
 pane.add(sliderPanel, BorderLayout.PAGE_START);
 imgLabel = new JLabel(new ImageIcon(img));
 pane.add(imgLabel, BorderLayout.CENTER);
 }
 private void update() {
 Imgproc.blur(src, srcBlur, BLUR_SIZE);
 Imgproc.Canny(srcBlur, detectedEdges, lowThresh, lowThresh * RATIO, KERNEL_SIZE, false);
 dst = new Mat(src.size(), CvType.CV_8UC3, Scalar.all(0));
 src.copyTo(dst, detectedEdges);
 Image img = HighGui.toBufferedImage(dst);
 imgLabel.setIcon(new ImageIcon(img));
 frame.repaint();
 }
 public static void main(String[] args) {
 // 加载本地 OpenCV 库
 System.loadLibrary(Core.NATIVE_LIBRARY_NAME)// 为事件派发线程安排任务:
 // 创建并显示此应用程序的图形用户界面。
 javax.swing.SwingUtilities.invokeLater(new Runnable() {
 @Override
 public void run() {
 new CannyDetectorDemo(args);
 }
 });
 }
}

Python
教程代码如下所示。您也可以从此处下载

from __future__ import print_function
import cv2 as cv
import argparse
max_lowThreshold = 100
window_name = 'Edge Map'
title_trackbar = 'Min Threshold:'
ratio = 3
kernel_size = 3
def CannyThreshold(val):
 low_threshold = val
 img_blur = cv.blur(src_gray, (3,3))
 detected_edges = cv.Canny(img_blur, low_threshold, low_threshold*ratio, kernel_size)
 mask = detected_edges != 0
 dst = src * (mask[:,:,None].astype(src.dtype))
 cv.imshow(window_name, dst)
parser = argparse.ArgumentParser(description='Code for Canny Edge Detector tutorial.')
parser.add_argument('--input', help='Path to input image.', default='fruits.jpg')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
 print('Could not open or find the image: ', args.input)
 exit(0)
src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
cv.namedWindow(window_name)
cv.createTrackbar(title_trackbar, window_name , 0, max_lowThreshold, CannyThreshold)
CannyThreshold(0)
cv.waitKey()
  • 该程序有何作用?
    • 要求用户输入一个数值,为我们的 Canny 边缘检测器设置下限(通过 Trackbar)。
    • 应用 Canny 检测器并生成遮罩(黑色背景上代表边缘的亮线)。
    • 将获得的掩码应用于原始图像,并在窗口中显示。

说明(C++ 代码)

  1. 创建一些所需的变量:
Mat src, src_gray;
Mat dst, detected_edges;
int lowThreshold = 0const int max_lowThreshold = 100const int ratio = 3const int kernel_size = 3const char* window_name = "Edge Map"

请注意以下几点:
a. 我们将下阈值与上阈值的比率设定为 3:1(比率可变)。
b. 我们将核大小设置为 3(用于 Canny 函数内部执行的 Sobel 运算)。
c. 我们将下阈值的最大值设置为 100。
2. 加载源图像:

 CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
 src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // 载入图像
 if( src.empty() )
 {
 std::cout << "Could not open or find the image! << std::endl;
 std::cout << "Usage: " << argv[0] << " <输入图像>" << std::endl;
 return -1}
  1. 创建与 src 类型和大小相同的矩阵(即 dst):
 dst.create( src.size(), src.type() )
  1. 使用函数 cv::cvtColor 将图像转换为灰度图像:
 cvtColor( src, src_gray, COLOR_BGR2GRAY )
  1. 创建一个窗口来显示结果:
 namedWindow( window_name, WINDOW_AUTOSIZE )
  1. 创建一个轨迹条,供用户输入 Canny 检测器的下限阈值:
createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold )

请注意以下几点:
a. 要由 Trackbar 控制的变量是 lowThreshold,其限制为 max_lowThreshold(我们之前将其设置为 100)。
b. 每次 Trackbar 注册动作时,都会调用回调函数 CannyThreshold。

  1. 让我们逐步检查 CannyThreshold 函数:
    a. 首先,我们使用内核大小为 3 的滤波器模糊图像:
 blur( src_gray, detected_edges, Size(3,3) )

b. 其次,我们应用 OpenCV 函数 cv::Canny :

 Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size )

其中参数如下

  • detected_edges: 源图像,灰度
  • detected_edges: 检测器的输出(可以与输入相同)
  • lowThreshold:低阈值: 用户移动轨迹条时输入的值
  • highThreshold: 在程序中设置为低阈值的三倍(遵循 Canny 的建议)
  • kernel_size: 我们将其定义为 3(内部使用的 Sobel 内核大小)
  1. 我们将 dst 图像填充为零(即图像全黑)。
    dst = Scalar::all(0);
  2. 最后,我们将使用函数 cv::Mat::copyTo 仅映射图像中被识别为边缘的区域(黑色背景)。不过,它只会复制像素值不为零的位置。由于 Canny 检测器的输出是黑色背景上的边缘轮廓,因此生成的 dst 将在除检测到的边缘以外的所有区域都是黑色的。
 src.copyTo( dst, detected_edges)
  1. 显示结果
 imshow( window_name, dst )

结果

  • 编译完上面的代码后,我们就可以以图像的路径作为参数运行它了。例如,输入以下图片:

在这里插入图片描述

  • 移动滑块,尝试不同的阈值,我们会得到以下结果:

在这里插入图片描述

  • 请注意图像在边缘区域与黑色背景的叠加效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值