计算机视觉中的边缘检测算法

摘要: 本文全面深入地探讨了计算机视觉中的边缘检测算法。首先阐述了边缘检测的重要性及其在计算机视觉领域的基础地位,随后详细介绍了经典的边缘检测算法,包括基于梯度的 Sobel 算子算法、Canny 边缘检测算法等,深入剖析了它们的原理、数学模型、算法步骤以及各自的优缺点。接着探讨了这些算法在不同应用场景下的表现,如在图像分析、目标识别、计算机辅助设计等领域的应用。最后,分别使用 C# 和 Python 语言实现了 Sobel 算子和 Canny 边缘检测算法,并对实现代码进行了详细的注释和讲解,通过实际代码展示了算法的具体操作流程,旨在为计算机视觉领域的研究人员、开发者以及相关专业学生提供系统的边缘检测算法知识及实用的代码参考,帮助他们更好地理解和应用边缘检测技术。

一、引言

在计算机视觉领域,边缘检测是一项极为关键的基础任务。图像中的边缘包含了丰富的信息,它是图像中不同区域的边界,能够表征物体的轮廓、形状以及物体与背景之间的关系等重要特征。通过边缘检测,可以将这些有意义的边缘信息提取出来,为后续的图像分析、目标识别、图像分割等高级任务提供有力的支持。例如,在自动驾驶技术中,准确的边缘检测能够帮助识别道路、车辆和行人的轮廓,从而实现安全的驾驶决策;在医学图像处理中,边缘检测有助于医生清晰地观察病变组织的边界,辅助疾病的诊断和治疗方案的制定;在计算机辅助设计(CAD)领域,边缘检测可用于提取设计图纸中的线条和轮廓,便于进行后续的模型构建和编辑。

二、边缘检测的基本概念

边缘是指图像中像素值发生急剧变化的位置,这种变化可以是灰度值的突变、颜色的差异或者纹理的改变等。边缘检测的目标就是找到这些像素值变化剧烈的点,并将它们连接成边缘曲线或轮廓。从数学角度来看,边缘通常对应着图像函数的一阶导数或二阶导数的局部极值点。

三、基于梯度的边缘检测算法 - Sobel 算子

  1. 原理
    • Sobel 算子是一种常用的基于梯度的边缘检测算子。它通过计算图像在水平和垂直方向上的灰度变化率(即梯度)来确定边缘的位置。Sobel 算子使用两个3x3的卷积核,一个用于检测水平方向的边缘,另一个用于检测垂直方向的边缘。
    • 水平方向的卷积核Gx为:
    • 垂直方向的卷积核Gy为:
    • 对于图像中的每个像素(x,y),将其邻域与这两个卷积核分别进行卷积运算,得到水平方向的梯度值Gx(x,y)和垂直方向的梯度值Gy(x,y)。然后,根据梯度幅值公式计算该像素点的梯度幅值,梯度方向为。通常,将梯度幅值大于某个阈值的点视为边缘点。
  2. 算法步骤
    • 读取图像,获取图像的宽度、高度和像素数据。
    • 初始化两个与图像大小相同的矩阵,用于存储水平和垂直方向的梯度值。
    • 遍历图像中的每个像素(除了边缘像素,因为边缘像素的邻域不完整)。
    • 对于每个像素,使用水平方向卷积核对其邻域进行卷积计算,得到水平方向梯度值并存储到对应的矩阵中。
    • 同样,使用垂直方向卷积核对其邻域进行卷积计算,得到垂直方向梯度值并存储到另一个矩阵中。
    • 根据梯度幅值公式计算每个像素的梯度幅值。
    • 设定一个阈值,将梯度幅值大于阈值的像素标记为边缘点,可以通过将这些边缘点的像素值设置为特定值(如白色)来显示边缘图像。

四、Canny 边缘检测算法

  1. 原理
    • Canny 边缘检测算法是一种较为复杂但效果优秀的边缘检测算法,它主要包括以下几个步骤:
    • 噪声平滑:首先使用高斯滤波器对图像进行平滑处理,以去除噪声对边缘检测的干扰。高斯滤波器能够在平滑图像的同时保留图像的边缘信息,其二维高斯函数为,其中为标准差,它决定了高斯滤波器的平滑程度。
    • 计算梯度幅值和方向:与 Sobel 算子类似,使用合适的卷积核(如 Sobel 卷积核)计算图像在水平和垂直方向的梯度值,进而得到梯度幅值和方向。
    • 非极大值抑制:在得到梯度幅值图像后,对其进行非极大值抑制。其目的是将局部范围内梯度幅值不是最大的像素点抑制为非边缘点。具体做法是,对于每个像素点,比较其在梯度方向上的邻域像素的梯度幅值,如果该像素点的梯度幅值不是局部最大,则将其标记为非边缘点,这样可以细化边缘,使边缘更精确。
    • 双阈值检测与边缘连接:设定两个阈值,高阈值Th和低阈值Tl(通常Th>Tl)。首先,将梯度幅值大于高阈值的像素点确定为强边缘点,这些点肯定是边缘点。然后,对于梯度幅值在低阈值和高阈值之间的像素点,如果它们与强边缘点相邻,则将其确定为弱边缘点并保留;否则,将其视为非边缘点而丢弃。最后,通过边缘连接算法将弱边缘点与强边缘点连接起来,形成完整的边缘。
  2. 算法步骤
    • 读取图像,获取图像的宽度、高度和像素数据。
    • 使用高斯滤波器对图像进行平滑处理,确定高斯核大小和标准差,计算滤波后的图像数据。
    • 计算滤波后图像的水平和垂直方向梯度值、梯度幅值和方向,可使用类似 Sobel 算子的计算方法。
    • 对梯度幅值图像进行非极大值抑制,遍历图像中的每个像素,根据其梯度方向和邻域像素的梯度幅值判断是否抑制该像素。
    • 设定双阈值Th和Tl,进行双阈值检测与边缘连接。首先标记强边缘点,然后遍历梯度幅值在低阈值和高阈值之间的像素点,判断其与强边缘点的相邻关系并确定是否保留为弱边缘点,最后连接弱边缘点和强边缘点形成边缘图像。

五、Sobel 算子与 Canny 边缘检测算法的优缺点

(一)Sobel 算子

  1. 优点
    • 计算简单,速度较快,能够快速地检测出图像中的边缘信息,对于一些简单的图像边缘检测任务具有较好的效果。
    • 可以分别得到水平和垂直方向的边缘信息,在某些特定应用场景下(如检测图像中的水平或垂直线条)较为有用。
  2. 缺点
    • 对噪声比较敏感,由于没有专门的噪声平滑步骤,在噪声较多的图像中可能会检测出大量的伪边缘,导致边缘检测结果不准确。
    • 边缘检测的精度相对较低,得到的边缘较粗,可能无法准确地描绘出物体的精细轮廓。

(二)Canny 边缘检测算法

  1. 优点
    • 检测精度高,通过非极大值抑制和双阈值检测等步骤能够得到较为精确和连续的边缘,对图像中的弱边缘也有较好的检测能力,能够更准确地描绘物体的轮廓。
    • 对噪声具有一定的鲁棒性,因为在算法开始阶段使用了高斯滤波器进行噪声平滑,减少了噪声对边缘检测的影响。
  2. 缺点
    • 算法相对复杂,计算量较大,尤其是在非极大值抑制和边缘连接步骤中需要对图像中的每个像素进行多次比较和判断,导致处理速度较慢,在实时性要求较高的应用场景中可能不太适用。

六、Sobel 算子的 C# 实现

以下是使用 C# 实现 Sobel 算子边缘检测的代码示例:

using System;
using System.Drawing;
using System.Drawing.Imaging;

class SobelOperator
{
    // 计算水平方向梯度
    private static void SobelHorizontalGradient(Bitmap sourceImage, int[,] horizontalGradient)
    {
        int width = sourceImage.Width;
        int height = sourceImage.Height;
        BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* sourcePtr = (byte*)sourceData.Scan0.ToPointer();
            int[,] sobelX = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
            for (int y = 1; y < height - 1; y++)
            {
                for (int x = 1; x < width - 1; x++)
                {
                    int sumX = 0;
                    for (int i = -1; i <= 1; i++)
                    {
                        for (int j = -1; j <= 1; j++)
                        {
                            int xIndex = x + j;
                            int yIndex = y + i;
                            int sourceIndex = (yIndex * sourceData.Stride) + (xIndex * 3);
                            sumX += sobelX[i + 1, j + 1] * sourcePtr[sourceIndex];
                        }
                    }
                    horizontalGradient[y, x] = sumX;
                }
            }
        }
        sourceImage.UnlockBits(sourceData);
    }

    // 计算垂直方向梯度
    private static void SobelVerticalGradient(Bitmap sourceImage, int[,] verticalGradient)
    {
        int width = sourceImage.Width;
        int height = sourceImage.Height;
        BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* sourcePtr = (byte*)sourceData.Scan0.ToPointer();
            int[,] sobelY = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };
            for (int y = 1; y < height - 1; y++)
            {
                for (int x = 1; x < width - 1; x++)
                {
                    int sumY = 0;
                    for (int i = -1; i <= 1; i++)
                    {
                        for (int j = -1; j <= 1; j++)
                        {
                            int xIndex = x + j;
                            int yIndex = y + i;
                            int sourceIndex = (yIndex * sourceData.Stride) + (xIndex * 3);
                            sumY += sobelY[i + 1, j + 1] * sourcePtr[sourceIndex + 1];
                        }
                    }
                    verticalGradient[y, x] = sumY;
                }
            }
        }
        sourceImage.UnlockBits(sourceData);
    }

    // 计算梯度幅值
    private static void GradientMagnitude(int[,] horizontalGradient, int[,] verticalGradient, Bitmap outputImage)
    {
        int width = outputImage.Width;
        int height = outputImage.Height;
        BitmapData outputData = outputImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* outputPtr = (byte*)outputData.Scan0.ToPointer();
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int gx = horizontalGradient[y, x];
                    int gy = verticalGradient[y, x];
                    double magnitude = Math.Sqrt(gx * gx + gy * gy);
                    // 归一化梯度幅值到0-255范围
                    int value = (int)(magnitude * 255.0 / Math.Sqrt(2 * 255 * 255));
                    value = Math.Min(255, Math.Max(0, value));
                    int outputIndex = (y * outputData.Stride) + (x * 3);
                    outputPtr[outputIndex] = (byte)value;
                    outputPtr[outputIndex + 1] = (byte)value;
                    outputPtr[outputIndex + 2] = (byte)value;
                }
            }
        }
        outputImage.UnlockBits(outputData);
    }

    // Sobel算子边缘检测主函数
    public static Bitmap SobelEdgeDetection(Bitmap sourceImage)
    {
        int width = sourceImage.Width;
        int height = sourceImage.Height;
        int[,] horizontalGradient = new int[height, width];
        int[,] verticalGradient = new int[height, width];
        // 计算水平和垂直方向梯度
        SobelHorizontalGradient(sourceImage, horizontalGradient);
        SobelVerticalGradient(sourceImage, verticalGradient);
        // 创建输出图像
        Bitmap outputImage = new Bitmap(width, height);
        // 计算梯度幅值并生成边缘图像
        GradientMagnitude(horizontalGradient, verticalGradient, outputImage);
        return outputImage;
    }
}

在上述代码中,SobelHorizontalGradient方法使用水平方向的 Sobel 卷积核计算图像的水平方向梯度值并存储在horizontalGradient矩阵中。SobelVerticalGradient方法类似地计算垂直方向梯度值并存储在verticalGradient矩阵中。GradientMagnitude方法根据水平和垂直方向梯度值计算梯度幅值,并将其归一化后设置到输出图像的像素值中,最后SobelEdgeDetection方法作为主函数,调用前面的方法完成整个 Sobel 算子边缘检测过程并返回边缘图像。

七、Canny 边缘检测算法的 C# 实现

以下是使用 C# 实现 Canny 边缘检测算法的代码示例:

using System;
using System.Drawing;
using System.Drawing.Imaging;

class CannyEdgeDetector
{
    // 高斯滤波函数
    private static void GaussianFilter(Bitmap sourceImage, double sigma, Bitmap outputImage)
    {
        int width = sourceImage.Width;
        int height = sourceImage.Height;
        BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        BitmapData outputData = outputImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
        double[,] kernel = GenerateGaussianKernel(3, sigma);
        int center = 1;
        unsafe
        {
            byte* sourcePtr = (byte*)sourceData.Scan0.ToPointer();
            byte* outputPtr = (byte*)outputData.Scan0.ToPointer();
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    double red = 0, green = 0, blue = 0;
                    for (int i = -center; i <= center; i++)
                    {
                        for (int j = -center; j <= center; j++)
                        {
                            int xIndex = Math.Max(0, Math.Min(x + j, width - 1));
                            int yIndex = Math.Max(0, Math.Min(y + i, height - 1));
                            int sourceIndex = (yIndex * sourceData.Stride) + (xIndex * 3);
                            red += kernel[i + center, j + center] * sourcePtr[sourceIndex];
                            green += kernel[i + center, j + center] * sourcePtr[sourceIndex + 1];
                            blue += kernel[i + center, j + center] * sourcePtr[sourceIndex + 2];
                        }
                    }
                    int outputIndex = (y * outputData.Stride) + (x * 3);
                    outputPtr[outputIndex] = (byte)Math.Min(255, Math.Max(0, red));
                    outputPtr[outputIndex + 1] = (byte)Math.Min(255, Math.Max(0, green));
                    outputPtr[outputIndex + 2] = (byte)Math.Min(255, Math.Max(0, blue));
                }
            }
        }
        sourceImage.UnlockBits(sourceData);
        outputImage.UnlockBits(outputData);
    }

    // 计算梯度幅值和方向
    private static void Gradient(Bitmap sourceImage, int[,] gradientMagnitude, double[,] gradientDirection)
    {
        int width = sourceImage.Width;
        int height = sourceImage.Height;
        BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* sourcePtr = (byte*)sourceData.Scan0.ToPointer();
            int[,] sobelX = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
            int[,] sobelY = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };
            for (int y = 1; y < height - 1; y++)
            {
                for (int x = 1; x < width - 1; x++)
                {
                    int gx = 0, gy = 0;
                    for (int i = -1; i <= 1; i++)
                    {
                        for (int j = -1; j <= 1; j++)
                        {
                            int xIndex = x + j;
                            int yIndex = y + i;
                            int sourceIndex = (yIndex * sourceData.Stride) + (xIndex * 3);
                            gx += sobelX[i + 1, j + 1] * sourcePtr[sourceIndex];
                            gy += sobelY[i + 1, j + 1] * sourcePtr[sourceIndex + 1];
                        }
                    }
                    gradientMagnitude[y, x] = (int)Math.Sqrt(gx * gx + gy * gy);
                    gradientDirection[y, x] = Math.Atan2(gy, gx);
                }
            }
        }
        sourceImage.UnlockBits(sourceData);
    }

    // 非极大值抑制
    private static void NonMaxSuppression(int[,] gradientMagnitude, double[,] gradientDirection, int[,] nmsOutput)
    {
        int width = gradientMagnitude.GetLength(1);
        int height = gradientMagnitude.GetLength(0);
        for (int y = 1; y < height - 1; y++)
        {
            for (int x = 1; x < width - 1; x++)
            {
                double angle = gradientDirection[y, x];
                if ((angle >= -Math.PI / 8 && angle < Math.PI / 8) || (angle >= 7 * Math.PI / 8 || angle < -7 * Math.PI / 8))
                {
                    // 水平方向比较
                    if (gradientMagnitude[y, x] <= gradientMagnitude[y, x - 1] || gradientMagnitude[y, x] <= gradientMagnitude[y, x + 1])
                    {
                        nmsOutput[y, x] = 0;
                    }
                    else
                    {
                        nmsOutput[y, x] = gradientMagnitude[y, x];
                    }
                }
                else if ((angle >= Math.PI / 8 && angle < 3 * Math.PI / 8) || (angle >= -7 * Math.PI / 8 && angle < -5 * Math.PI / 8))
                {
                    // 对角线方向(右上 - 左下)比较
                    if (gradientMagnitude[y, x] <= gradientMagnitude[y - 1, x + 1] || gradientMagnitude[y, x] <= gradientMagnitude[y + 1, x - 1])
                    {
                        nmsOutput[y, x] = 0;
                    }
                    else
                    {
                        nmsOutput[y, x] = gradientMagnitude[y, x];
                    }
                }
                else if ((angle >= 3 * Math.PI / 8 && angle < 5 * Math.PI / 8) || (angle >= -5 * Math.PI / 8 && angle < -3 * Math.PI / 8))
                {
                    // 垂直方向比较
                    if (gradientMagnitude[y, x] <= gradientMagnitude[y - 1, x] || gradientMagnitude[y, x] <= gradientMagnitude[y + 1, x])
                    {
                        nmsOutput[y, x] = 0;
                    }
                    else
                    {
                        nmsOutput[y, x] = gradientMagnitude[y, x];
                    }
                }
                else if ((angle >= 5 * Math.PI / 8 && angle < 7 * Math.PI / 8) || (angle >= -3 * Math.PI / 8 && angle < -Math.PI / 8))
                {
                    // 对角线方向(左上 - 右下)比较
                    if (gradientMagnitude[y, x] <= gradientMagnitude[y - 1, x - 1] || gradientMagnitude[y, x] <= gradientMagnitude[y + 1, x + 1])
                    {
                        nmsOutput[y, x] = 0;
                    }
                    else
                    {
                        nmsOutput[y, x] = gradientMagnitude[y, x];
                    }
                }
            }
        }
    }

    // 双阈值检测与边缘连接
    private static void Hysteresis(int[,] nmsOutput, int lowThreshold, int highThreshold, Bitmap outputImage)
    {
        int width = nmsOutput.GetLength(1);
        int height = nmsOutput.GetLength(0);
        BitmapData outputData = outputImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* outputPtr = (byte*)outputData.Scan0.ToPointer();
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    if (nmsOutput[y, x] >= highThreshold)
                    {
                        outputPtr[(y * outputData.Stride) + (x * 3)] = 255;
                        outputPtr[(y * outputData.Stride) + (x * 3) + 1] = 255;
                        outputPtr[(y * outputData.Stride) + (x * 3) + 2] = 255;
                    }
                    else if (nmsOutput[y, x] >= lowThreshold)
                    {
                        // 检查邻域像素是否有强边缘点
                        bool hasStrongNeighbor = false;
                        for (int i = -1; i <= 1; i++)
                        {
                            for (int j = -1; j <= 1; j++)
                            {
                                int xIndex = Math.Max(0, Math.Min(x + j, width - 1));
                                int yIndex = Math.Max(0, Math.Min(y + i, height - 1));
                                if (nmsOutput[yIndex, xIndex] >= highThreshold)
                                {
                                    hasStrongNeighbor = true;
                                    break;
                                }
                            }
                            if (hasStrongNeighbor)
                            {
                                break;
                            }
                        }
                        if (hasStrongNeighbor)
                        {
                            outputPtr[(y * outputData.Stride) + (x * 3)] = 255;
                            outputPtr[(y * outputData.Stride) + (x * 3) + 1] = 255;
                            outputPtr[(y * outputData.Stride) + (x * 3) + 2] = 255;
                        }
                        else
                        {
                            outputPtr[(y * outputData.Stride) + (x * 3)] = 0;
                            outputPtr[(y * outputData.Stride) + (x * 3) + 1] = 0;
                            outputPtr[(y * outputData.Stride) + (x * 3) + 2] = 0;
                        }
                    }
                    else
                    {
                        outputPtr[(y * outputData.Stride) + (x * 3)] = 0;
                        outputPtr[(y * outputData.Stride) + (x * 3) + 1] = 0;
                        outputPtr[(y * outputData.Stride) + (x * 3) + 2] = 0;
                    }
                }
            }
        }
        outputImage.UnlockBits(outputData);
    }

    // 生成高斯核
    private static double[,] GenerateGaussianKernel(int size, double sigma)
    {
        double[,] kernel = new double[size, size];
        int center = size / 2;
        double sum = 0;
        for (int i = 0; i < size; i++)
        {
            for (int j = 0; j < size; j++)
            {
                int x = i - center;
                int y = j - center;
                kernel[i, j] = (1.0 / (2 * Math.PI * sigma * sigma)) * Math.Exp(-(x * x + y * y) / (2 * sigma * sigma));
                sum += kernel[i, j];
            }
        }
        // 归一化高斯核
        for (int i = 0; i < size; i++)
        {
            for (int j = 0; j < size; j++)
            {
                kernel[i, j] /= sum;
            }
        }
        return kernel;
    }

    // Canny边缘检测主函数
    public static Bitmap CannyEdgeDetection(Bitmap sourceImage, double sigma, int lowThreshold, int highThreshold)
    {
        // 高斯滤波
        Bitmap filteredImage = new Bitmap(sourceImage.Width, sourceImage.Height);
        GaussianFilter(sourceImage, sigma, filteredImage);
        // 计算梯度幅值和方向
        int[,] gradientMagnitude = new int[filteredImage.Height, filteredImage.Width];
        double[,] gradientDirection = new double[filteredImage.Height, filteredImage.Width];
        Gradient(filteredImage, gradientMagnitude, gradientDirection);
        // 非极大值抑制
        int[,] nmsOutput = new int[filteredImage.Height, filteredImage.Width];
        NonMaxSuppression(gradientMagnitude, gradientDirection, nmsOutput);
        // 双阈值检测与边缘连接
        Bitmap outputImage = new Bitmap(filteredImage.Width, filteredImage.Height);
        Hysteresis(nmsOutput, lowThreshold, highThreshold, outputImage);
        return outputImage;
    }
}

在上述 C# 代码中,GaussianFilter方法实现了高斯滤波功能,对输入图像进行平滑处理。Gradient方法使用 Sobel 算子计算滤波后图像的梯度幅值和方向。NonMaxSuppression方法执行非极大值抑制操作,细化边缘。Hysteresis方法进行双阈值检测与边缘连接,确定最终的边缘图像。GenerateGaussianKernel方法用于生成高斯核。CannyEdgeDetection作为主函数,依次调用上述方法完成整个 Canny 边缘检测过程并返回边缘图像。

八、Sobel 算子的 Python 实现

import cv2
import numpy as np

def sobel_edge_detection(image):
    """
    使用Sobel算子进行边缘检测
    :param image: 输入图像
    :return: 边缘检测后的图像
    """
    # 获取图像的高度、宽度和通道数
    height, width, channels = image.shape
    # 转换为灰度图像(Sobel算子通常在灰度图像上操作)
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 计算水平方向梯度
    sobel_x = cv2.Sobel(gray_image, cv2.CV_64F, 1, 0, ksize=3)
    # 计算垂直方向梯度
    sobel_y = cv2.Sobel(gray_image, cv2.CV_64F, 0, 1, ksize=3)
    # 计算梯度幅值
    gradient_magnitude = np.sqrt(sobel_x ** 2 + sobel_y ** 2)
    # 归一化梯度幅值到0-255范围
    gradient_magnitude = (gradient_magnitude / np.max(gradient_magnitude)) * 255
    # 将梯度幅值转换为无符号8位整数类型
    gradient_magnitude = gradient_magnitude.astype(np.uint8)
    return gradient_magnitude

在这个 Python 代码中,首先使用cv2.cvtColor将输入图像转换为灰度图像,因为 Sobel 算子通常在灰度图像上进行边缘检测。然后使用cv2.Sobel函数分别计算水平和垂直方向的梯度,其中cv2.CV_64F表示输出数据类型为 64 位浮点数,1, 0表示计算水平方向梯度(dx = 1, dy = 0),0, 1表示计算垂直方向梯度(dx = 0, dy = 1),ksize = 3表示使用的 Sobel 卷积核。接着计算梯度幅值,并进行归一化和数据类型转换,最后返回边缘检测后的图像。

九、Canny 边缘检测算法的 Python 实现

import cv2
import numpy as np

def canny_edge_detection(image, sigma=1.4, low_threshold=50, high_threshold=150):
    """
    使用Canny边缘检测算法进行边缘检测
    :param image: 输入图像
    :param sigma: 高斯滤波标准差
    :param low_threshold: 低阈值
    :param high_threshold: 高阈值
    :return: 边缘检测后的图像
    """
    # 高斯滤波
    blurred_image = cv2.GaussianBlur(image, (3, 3), sigma)
    # 转换为灰度图像
    gray_image = cv2.cvtColor(blurred_image, cv2.COLOR_BGR2GRAY)
    # 计算梯度幅值和方向
    gradient_magnitude, gradient_direction = cv2.Canny(gray_image, low_threshold, high_threshold, L2gradient=True)
    return gradient_magnitude

在上述 Python 代码中,cv2.GaussianBlur函数用于对输入图像进行高斯滤波,(3, 3)表示高斯核大小为3x3。然后将滤波后的图像转换为灰度图像,再使用cv2.Canny函数进行 Canny 边缘检测,其中L2gradient=True表示使用精确的梯度幅值计算(即使用欧几里得距离计算梯度幅值),函数返回计算得到的梯度幅值图像,即边缘检测后的图像。通过这些 Python 代码实现,可以方便地在 Python 环境中应用 Sobel 算子和 Canny 边缘检测算法进行图像边缘检测任务。

十、总结

边缘检测算法在计算机视觉领域具有举足轻重的地位。Sobel 算子以其简单快速的特点,在一些对边缘检测精度要求不高且实时性较强的场景中有着广泛的应用,如简单的图像预处理、实时监控中的初步轮廓提取等。然而,对于复杂图像和高精度需求场景,Canny 边缘检测算法凭借其出色的检测精度和对噪声的鲁棒性脱颖而出。它在医学影像分析、工业零件检测等领域能够精准地勾勒出目标边缘,为后续的诊断、测量等工作提供可靠依据。无论是 C# 还是 Python 语言的实现,都为开发者提供了便利的工具,使其能够根据项目的具体需求和运行环境灵活选择合适的语言来部署边缘检测算法,从而推动计算机视觉技术在众多领域的深入发展与广泛应用,不断提升图像处理和分析的效率与准确性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/937883.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ComfyUI 与 Stable Diffusion WebUI 的优缺点比较

ComfyUI与Stable Diffusion WebUI都是AI绘画领域比较知名两款产品&#xff0c;两者存在诸多差异&#xff0c;本篇就带你熟悉二者的优劣&#xff0c;方便自己做出决策。 界面与操作 ComfyUI&#xff1a;界面简洁直观&#xff0c;通过节点和连线的方式构建工作流&#xff0c;用…

2024年12月16日Github流行趋势

项目名称&#xff1a;PDFMathTranslate 项目维护者&#xff1a;Byaidu reycn hellofinch Wybxc YadominJinta项目介绍&#xff1a;基于 AI 完整保留排版的 PDF 文档全文双语翻译&#xff0c;支持 Google/DeepL/Ollama/OpenAI 等服务&#xff0c;提供 CLI/GUI/Docker。项目star数…

CDGA|“数据池塘资源”理论的出现对数据治理有怎样的影响?

“数据池塘资源”这一理论实践&#xff0c;可以理解为将数据集视为一个池塘&#xff0c;其中蕴含着丰富的信息和资源&#xff0c;有待于人们去挖掘和利用。这一理论实践对数据管理、分析和应用等领域可能会产生一系列深远的影响。以下是对其可能影响的详细分析&#xff1a; 一、…

linux学习笔记02 linux中的基础设置(修改主机名、ip、防火墙、网络配置管理)

目录 修改主机名 ​编辑 修改ip地址 防火墙 关闭networkmanage 修改主机名 查看主机名 hostnamectl status 修改主机名 vim /etc/hostname 修改ip地址 vim /etc/sysconfig/network-scripts/ifcfg-ens33 输入这个命令后对照以下文件修改 TYPE"Ethernet" PROXY_M…

用户发送请求后服务端i/o工作过程

华子目录 服务端i/o介绍磁盘i/o机械磁盘的寻道时间、旋转延迟和数据传输时间常见的机械磁盘平均寻道时间值常见磁盘的平均延迟时间每秒最大IOPS的计算方法 网络i/o网络I/O处理过程磁盘和网络i/o 一次完整的请求在内部的执行过程 服务端i/o介绍 i/o在计算机中指Input/Output&am…

240004基于Jamva+ssm+maven+mysql的房屋租赁系统的设计与实现

基于ssmmavenmysql的房屋租赁系统的设计与实现 1.项目描述2.运行环境3.项目截图4.源码获取 1.项目描述 该项目在原有的基础上进行了优化&#xff0c;包括新增了注册功能&#xff0c;房屋模糊查询功能&#xff0c;管理员和用户信息管理等功能&#xff0c;以及对网站界面进行了优…

MinerU(2):GPU加速

目录 遗留问题&#xff1a;ubuntu使用特定conda环境CUDA加速速度对比 解析效果公式解析表格解析实验结论 遗留问题&#xff1a;ubuntu使用特定conda环境 发现在vscode中能查看到版本&#xff0c; 但是到虚拟机&#xff0c;同样的目录下查不到 可能是vscode能自己切换Python环…

C# 生成随机数的方法

C# 提供了一种强大而方便的工具类 Random &#xff0c;用于生成随机数。这里将分类讨论如何通过 C# 实现随机数生成&#xff0c;以及应用于实际情况中的一些具体方案。 一、Random 类概述 Random 类表示一个伪随机数生成器&#xff0c;用于生成满足随机性统计要求的数字序列。…

wazuh-modules-sca-scan

sca模块主函数wm_sca_main -> wm_sca_start 检查policy文件中的每一个项目wm_sca_check_policy static int wm_sca_check_policy(const cJSON * const policy, const cJSON * const checks, OSHash *global_check_list) {if(!policy) {return 1;}const cJSON * const id c…

SpringCloud微服务实战系列:03spring-cloud-gateway业务网关灰度发布

目录 spring-cloud-gateway 和zuul spring webflux 和 spring mvc spring-cloud-gateway 的两种模式 spring-cloud-gateway server 模式下配置说明 grayLb://system-server 灰度发布代码实现 spring-cloud-gateway 和zuul zuul 是spring全家桶的第一代网关组件&#x…

Arm Cortex-M处理器对比表

Arm Cortex-M处理器对比表 当前MCU处理器上主要流行RISC-V和ARM处理器&#xff0c;其他的内核相对比较少&#xff1b;在这两种内核中&#xff0c;又以Arm Cortex-M生态环境相对健全&#xff0c;大部分的厂家都在使用ARM的处理器。本文主要介绍Arm Cortex-M各个不同系列的参数对…

如何实现规范化LabVIEW编程

规范编写LabVIEW程序的目的是提高代码的可读性、可维护性、可扩展性&#xff0c;并确保团队成员能够高效地理解和修改代码。以下是一些关键建议&#xff0c;帮助您编写更专业的LabVIEW代码&#xff0c;并确保它易于后续的升级和维护&#xff1a; ​ 1. 合理的项目结构 目录结构…

深入C语言文件操作:从库函数到系统调用

引言 文件操作是编程中不可或缺的一部分&#xff0c;尤其在C语言中&#xff0c;文件操作不仅是处理数据的基本手段&#xff0c;也是连接程序与外部世界的重要桥梁。C语言提供了丰富的库函数来处理文件&#xff0c;如 fopen、fclose、fread、fwrite 等。然而&#xff0c;这些库…

游戏引擎学习第52天

仓库 : https://gitee.com/mrxiao_com/2d_game 这节的内容相当多 回顾 在游戏中&#xff0c;实体被分为不同的类别&#xff1a;接近玩家的“高频实体”、距离较远并正在模拟的“低频实体”和不进行更新的“休眠实体”。这些实体会根据它们与玩家的距离进行处理&#xff0c;接…

docker 安装mysql 5.7 详细保姆级教程

1. 安装mysql(5.7) docker pull mysql:5.7 若是拉取不了&#xff0c;可以配置下 docker 源 2. 查看是否安装成功 docker images 下图就是成功了 3.创建mysql专用目录、数据挂载目录、配置文件目录 &#xff0c;演示目录在于/home/下 //命令逐条执行cd /home/ mkdir mysql …

宝塔SSL证书申请失败,报错:申请SSL证书错误 module ‘OpenSSL.crypto‘ has no attribute ‘sign‘(已解决)

刚安装宝塔申请SSL就报错&#xff1a;申请SSL证书错误 module OpenSSL.crypto has no attribute sign 面板、插件版本&#xff1a;9.2.0 系统版本&#xff1a;Alibaba Cloud Linux 3.2104 LTS 问题&#xff1a;申请SSL证书错误 module OpenSSL.crypto has no attribute sign…

Three使用WebGPU的关键TSL

Three.js 使用 WebGPU 的关键 TSL TSL: three.js shader language 介绍 three.js 材质转为webgpu的关键流程, 从而引出 TSL. 1、关键类关系 WebGPURenderer|-- library: StandardNodeLibrary|-- _nodes: Nodes|-- _objects: RenderObjects|-- createRenderObject()StandardN…

【蓝桥杯国赛真题15】python质因数个数 蓝桥杯青少年组python编程国赛真题详细解析

目录 python质因数个数 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python质因数个数 第十二届蓝桥杯青少年组python比赛国赛真题详细解析 …

发布/部署WebApi服务器(IIS+.NET8+ASP.NETCore)

CS软件授权注册系统-发布/部署WebApi服务器(IIS.NET8ASP.NETCore) 目录 本文摘要VS2022配置发布VS2022发布WebApiIIS服务器部署WebApi 将程序文件复制到云服务器添加网站配置应用程序池配置dns域名配置端口阿里云ECS服务器配置19980端口配置https协议 (申请ssl证书)测试WebAp…

MybatisPlus-配置加密

配置加密 目前配置文件中的很多参数都是明文&#xff0c;如果开发人员发生流动&#xff0c;很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。 我们以数据库的用户名和密码为例。 生成秘钥 首先&#xff0c;我们利用AES工具生成一个随机秘钥&#…