欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

图像边缘检测 Canny边缘检测

程序员文章站 2024-01-28 09:40:10
...

底下有详细代码

一、介绍

1、图像检测的原理。

        图像检测的原理是检测相邻的几个点像素值之间的变化率,相对于对函数求导。求点P(x,y)的变换率,可以在点P周围选取一些点,求x方向的距离Gx,再求y方向上的距离Gy。最后变换率G等于Gx平方加上Gy平方的和的平方差,即G=Math.sqrt(Gx^2+Gy^2)。

2、Canny算子。

        Canny算子对噪声不敏感。Canny边缘检测算子是John F. Canny于1986年开发出来的一个多级边缘检测算法。更为重要的是Canny创立了“边缘检测计算理论”(computational theory of edge detection)解释这项技术如何工作。
        Canny的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:好的检测- 算法能够尽可能多地标识出图像中的实际边缘。好的定位- 标识出的边缘要与实际图像中的实际边缘尽可能接近。最小响应- 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。为了满足这些要求Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。

3、步骤。

        (1)高斯过滤。
        (2)Sobel边缘检测(其他的边缘检测方法也可以)。
        (3)非极大值抑制。
        (4)双阈值检测。

二、高斯过滤

1、介绍。

        任何边缘检测算法都不可能在未经处理的原始数据上很好地处理,所以第一步是对原始数据与高斯平滑模板作卷积,得到的图像与原始图像相比有些轻微的模糊。这样,单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。

2、核心代码。

        可以参考文章:图像处理 高斯滤波(带权值的均值滤波)

3、结果。而我在canny中,选择的高斯过滤模版是:长度为3,方差sigma为1.5的。即第一张图:高斯滤波_3_2sigma.jpg。

图像边缘检测 Canny边缘检测

三、Sobel边缘检测

1、介绍。

        将经过高斯过滤之后的图像,即上面的“高斯滤波_3_2sigma.jpg”图像进行边缘检测,检测方法使用Sobel算子。可以参考文章:图像边缘检测 Sobel边缘检测

2、核心代码。

        可以参考文章:图像边缘检测 Sobel边缘检测

3、结果。生成图:Lena-canny-sobel.jpg。

图像边缘检测 Canny边缘检测

四、非极大值抑制

1、介绍。

(1)、获得角度图。

        Sobel算子模版。Gx=f(x+1,y-1)+2f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+2f(x-1,y)+f(x-1,y+1))
                                   Gy=f(x-1,y+1)+2f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+2f(x,y-1)+f(x+1,y-1))
                                   Gx=[-1 0  1] , Gy=[-1 -2 -1]
                                         [-2 0  2]          [ 0  0  0]
                                         [-1 0  1]          [ 1  2  1]

        边缘检测的像素图G=Math.sqrt(Gx^2+Gy^2),其实就是 “Lena-canny-sobel.jpg”。而角度图angle=Gy/Gx。

(2)、获取角度图中点(x,y)的方向,可以根据值f(x,y)求导。

        角度图中的值为 -Math.Pi 到 Math.Pi 之间,差不多为 -3.14 到 3.14 之间。将其16等分。

        方向1:f(x-1,y)-->f(x,y)-->f(x+1,y)         (15/16)Pi-->(1/16)Pi、(7/16)Pi-->(9/16)Pi
        方向2:f(x-1,y-1)-->f(x,y)-->f(x+1,y+1)  (1/16)Pi-->(3/16)Pi、(9/16)Pi-->(11/16)Pi
        方向3:f(x,y-1)-->f(x,y)-->f(x,y+1)         (3/5)Pi-->(5/16)Pi、(11/16)Pi-->(13/16)Pi
        方向4:f(x-1,y+1)-->f(x,y)-->f(x+1,y-1)  (5/7)Pi-->(7/16)Pi、(13/16)Pi-->(15/16)Pi

(3)、非极大值抑制。

        在角度图angle中,假如点 P(x,y) 的值是在  (15/16)Pi-->(1/16)Pi 或者 (7/16)Pi-->(9/16)Pi 的范围中,那么点 P(x,y) 的方向就是方向1。那么在其方向上并与之相邻的两个点就是 P1(x-1,y) 和 P2(x+1,y) , 如果点 P 小于 P1 或者 P2 ,那么就将像素图G中点P值为0。

2、核心代码。

private static final double pi16 = Math.PI / 16;

private static final double[] piArray = new double[]{
        0, pi16 * 1, 2, pi16 * 3, 4, pi16 * 5, 6, pi16 * 7, 8,
        pi16 * 9, 10, pi16 * 11, 12, pi16 * 13, 14, pi16 * 15, 16};

/**
 * 非极大值抑制
 * 判断沿着梯度方向的3个像素点(中心像素点与另外两个梯度方向区域内的像素点),
 * 若中心像素点的梯度值不比其他两个像素点的梯度值大,则将该像素点的灰度值置0。
 * 方向1:f(x-1,y)-->f(x,y)-->f(x+1,y)      (15/16)Pi-->(1/16)Pi、(7/16)Pi-->(9/16)Pi
 * 方向2:f(x-1,y-1)-->f(x,y)-->f(x+1,y+1)  (1/16)Pi-->(3/16)Pi、(9/16)Pi-->(11/16)Pi
 * 方向3:f(x,y-1)-->f(x,y)-->f(x,y+1)      (3/5)Pi-->(5/16)Pi、(11/16)Pi-->(13/16)Pi
 * 方向4:f(x-1,y+1)-->f(x,y)-->f(x+1,y-1)  (5/7)Pi-->(7/16)Pi、(13/16)Pi-->(15/16)Pi
 *
 * @param rangeArray 高斯过滤图
 * @param angleArray 角度图
 */
private static int[][] getDirection(int[][] rangeArray, double[][] angleArray) {
    int row = rangeArray.length;
    int column = rangeArray[0].length;
    int[][] array = new int[row][column];
    //注意:x = j = column , y = i = row
    for (int i = 1; i < row - 1; i++) {
        for (int j = 1; j < column - 1; j++) {
            array[i][j] = rangeArray[i][j];
            double angle = angleArray[i][j];
            if ((angle > piArray[15] && angle < piArray[1]) || (angle > piArray[7] && angle < piArray[9])) {
                //方向1:f(x-1,y)-->f(x,y)-->f(x+1,y)      (15/16)Pi-->(1/16)Pi、(7/16)Pi-->(9/16)Pi
                if (rangeArray[i][j] < rangeArray[i][j - 1] || rangeArray[i][j] < rangeArray[i][j + 1])
                    array[i][j] = 0;
            } else if ((angle > piArray[1] && angle < piArray[3]) || (angle > piArray[9] && angle < piArray[11])) {
                //方向2:f(x-1,y-1)-->f(x,y)-->f(x+1,y+1)  (1/16)Pi-->(3/16)Pi、(9/16)Pi-->(11/16)Pi
                if (rangeArray[i][j] < rangeArray[i - 1][j - 1] || rangeArray[i][j] < rangeArray[i + 1][j + 1])
                    array[i][j] = 0;
            } else if ((angle > piArray[3] && angle < piArray[5]) || (angle > piArray[11] && angle < piArray[13])) {
                //方向3:f(x,y-1)-->f(x,y)-->f(x,y+1)      (3/5)Pi-->(5/16)Pi、(11/16)Pi-->(13/16)Pi
                if (rangeArray[i][j] < rangeArray[i - 1][j] || rangeArray[i][j] < rangeArray[i + 1][j])
                    array[i][j] = 0;
            } else {
                //方向4:f(x-1,y+1)-->f(x,y)-->f(x+1,y-1)  (5/7)Pi-->(7/16)Pi、(13/16)Pi-->(15/16)Pi
                if (rangeArray[i][j] < rangeArray[i + 1][j - 1] || rangeArray[i][j] < rangeArray[i - 1][j + 1])
                    array[i][j] = 0;
            }
        }
    }
    return array;
}

3、结果。明显比非极大值抑制前的效果好多了。生成图:Lena-canny-非极大值抑制.jpg。

图像边缘检测 Canny边缘检测

五、双阈值检测

1、介绍。

        将图 “Lena-canny-非极大值抑制.jpg” 进行二值化处理,我们生成两张二值化图片,阈值分别为32和96。阈值为32的先称为低阈值图或者弱边缘图,阈值为96的称为高阈值图或者强边缘图。我们以高阈值图为主,低阈值图为辅,有选择性的从低阈值挑选值补充到高阈值图中。

        方法1:以高阈值图的点为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。这个理解起来比较简单。

       方法2:以高阈值图中边缘的端点(端点指一个点少于两个与之相邻的点)为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。这个理解起来比较难。

2、核心代码。

/**
 * 双阈值检测--以高阈值图的点为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。
 *
 * @param resultArray    结果图片像素数组
 * @param lowBinaryArray 低阈值图片像素数组
 * @param i              第几行
 * @param j              第几列
 * @param row            总行数
 * @param column         总列数
 */
private static void thresholdDetectionV1(int[][] resultArray, int[][] lowBinaryArray, int i, int j, int row, int column) {
    if (i < 0 || i >= row || j < 0 || j >= column) {
        return;
    }
    if (resultArray[i][j] != 0) {//已经被扫描过了
        return;
    } else {//resultArray填充
        resultArray[i][j] = lowBinaryArray[i][j];
    }
    //扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
    for (int k = -1; k <= 1; k++) {
        for (int z = -1; z <= 1; z++) {
            if (k == 0 && z == 0) {
                continue;
            }
            int iAround = i + k;
            int jAround = j + z;
            if (lowBinaryArray[iAround][jAround] != 0) {
                thresholdDetectionV1(resultArray, lowBinaryArray, iAround, jAround, row, column);
            }
        }
    }
}

/**
 * 双阈值检测--以高阈值图中边缘的端点(端点指一个点少于两个与之相邻的点)为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。
 *
 * @param resultArray     结果图片像素数组
 * @param lowBinaryArray  低阈值图片像素数组
 * @param highBinaryArray 高阈值图片像素数组
 * @param i               第几行
 * @param j               第几列
 * @param row             总行数
 * @param column          总列数
 */
private static void thresholdDetectionV2(int[][] resultArray, int[][] lowBinaryArray, int[][] highBinaryArray, int i, int j, int row, int column) {
    if (i < 0 || i >= row || j < 0 || j >= column) {
        return;
    }
    if (resultArray[i][j] != 0) {//已经被扫描过了
        return;
    } else { //resultArray填充
        resultArray[i][j] = highBinaryArray[i][j];
    }
    //高阈值图中扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
    int aroundCount = 0;//有几个连接点
    for (int k = -1; k <= 1; k++) {
        for (int z = -1; z <= 1; z++) {
            if (k == 0 && z == 0) {
                continue;
            }
            int iAround = i + k;
            int jAround = j + z;
            //判断是否为强边缘
            if (highBinaryArray[iAround][jAround] != 0) {
                aroundCount++;
                thresholdDetectionV2(resultArray, lowBinaryArray, highBinaryArray, iAround, jAround, row, column);
            }
        }
    }
    //判断是边缘的端点。如果不是端点的话,至少应该有两个连接点。
    if (aroundCount < 2) {
        //低阈值图中扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
        for (int k = -1; k <= 1; k++) {
            for (int z = -1; z <= 1; z++) {
                if (k == 0 && z == 0) {
                    continue;
                }
                int iAround = i + k;
                int jAround = j + z;
                //判断是否为弱边缘
                if (lowBinaryArray[iAround][jAround] != 0) {
                    highBinaryArray[iAround][jAround] = lowBinaryArray[iAround][jAround];//添加高阈值图的强边缘
                    thresholdDetectionV2(resultArray, lowBinaryArray, highBinaryArray, iAround, jAround, row, column);
                }
            }
        }
    }
}

3、结果。以方法2的图片 “Lena-canny-v2.jpg” 为最终的测试结果,方法1的图片仅供参考。

(1)、方法1。生成图:Lena-canny-v1.jpg。

图像边缘检测 Canny边缘检测

(2)、方法2。生成图:Lena-canny-v2.jpg。

图像边缘检测 Canny边缘检测

六、所有图片对比

图像边缘检测 Canny边缘检测

七、详细代码

        这里仅提供主流程代码 EdegDetectionTest.java 和 EdegDetectionUtils.java 。其他详细代码可以查看文章图像边缘检测 Laplacian边缘检测的详细代码,因为一样的,就不重复写出了。

1、主流程代码 EdegDetectionTest.java。

package com.zxj.reptile.test.image.edge;

import com.zxj.reptile.utils.image.EdgeDetectionUtils;
import com.zxj.reptile.utils.image.ImageService;
import com.zxj.reptile.utils.image.ImageUtils;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

/**
 * 图像边缘检测
 */
public class EdgeDetectionTest {
    public static void main(String[] args) {
        cannyTest();
    }

    private static void cannyTest() {
        String path = "G:\\xiaojie-java-test\\img\\边缘检测\\";
        String sourcePath = path + "Lena灰度图.jpg";
        String targetPath = path + "Lena-canny.jpg";
        canny(sourcePath, targetPath);
    }

    private static void canny(String sourcePath, String targetPath) {
        try {
            //获取原图像对象,并获取原图像的二维数组
            BufferedImage image = ImageIO.read(new File(sourcePath));
            int[][] imgArrays = ImageUtils.getImageGray(image);
            //生成新图像的二维数组
            int[][] newImgArrays = EdgeDetectionUtils.canny(imgArrays, 3, 0.5, 32, 128);
            //生成新图片对象,填充像素
            BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
            ImageUtils.setImageRgbByByte(newImage, newImgArrays);
            //生成图片文件
            ImageIO.write(newImage, "JPEG", new File(targetPath));
            Thread.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2、EdegDetectionUtils.java。

package com.zxj.reptile.utils.image;

/**
 * 图像边缘检测
 * G = Math.sqrt(Gx^2 + Gy^2)
 * angle = arctan(Gy / Gx)
 */
public class EdgeDetectionUtils {
    /**
     * 边缘检测--Reberts--噪声敏感
     * Gx=f(x+1,y+1)-f(x,y)
     * Gy=f(x,y+1)-f(x+1,y)
     * Gx=[-1 0] , Gy=[0 -1]
     * ---[ 0 1]      [1  0]
     */
    public static int[][] reberts(int[][] array) {
        int row = array.length;
        int column = array[0].length;
        int[][] newArray = new int[row][column];
        for (int i = 0; i < row - 1; i++) {//图片第几行
            for (int j = 0; j < column - 1; j++) {//图片第几列
                //Gx=f(x+1,y+1)-f(x,y),
                //Gy=f(x,y+1)-f(x+1,y)
                int sumX = array[i + 1][j + 1] - array[i][j];
                int sumY = array[i + 1][j] - array[i][j + 1];
                newArray[i][j] = (int) Math.round(Math.sqrt(sumX * sumX + sumY * sumY));
            }
        }
        return ImageUtils.rangeToByte(newArray);
    }

    /**
     * 边缘检测--Prewitt
     * Gx=f(x+1,y-1)+f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+f(x-1,y)+f(x-1,y+1))
     * Gy=f(x-1,y+1)+f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+f(x,y-1)+f(x+1,y-1))
     * Gx=[-1 0 1] , Gy=[-1 -1 -1]
     * ---[-1 0 1]      [ 0  0  0]
     * ---[-1 0 1]      [ 1  1  1]
     */
    public static int[][] prewitt(int[][] array) {
        int row = array.length;
        int column = array[0].length;
        int[][] newArray = new int[row][column];
        //注意:x = j = column , y = i = row
        for (int i = 1; i < row - 1; i++) {//图片第几行
            for (int j = 1; j < column - 1; j++) {//图片第几列
                //Gx=f(x+1,y-1)+f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+f(x-1,y)+f(x-1,y+1))
                //Gy=f(x-1,y+1)+f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+f(x,y-1)+f(x+1,y-1))
                int sumX = array[i - 1][j + 1] + array[i][j + 1] + array[i + 1][j + 1] -
                        (array[i - 1][j - 1] + array[i][j - 1] + array[i + 1][j - 1]);
                int sumY = array[i + 1][j - 1] + array[i + 1][j] + array[i + 1][j + 1] -
                        (array[i - 1][j - 1] + array[i - 1][j] + array[i - 1][j + 1]);
                newArray[i][j] = (int) Math.round(Math.sqrt(sumX * sumX + sumY * sumY));
            }
        }
        return ImageUtils.rangeToByte(newArray);
    }

    /**
     * 边缘检测--Sobel
     * Gx=f(x+1,y-1)+2f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+2f(x-1,y)+f(x-1,y+1))
     * Gy=f(x-1,y+1)+2f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+2f(x,y-1)+f(x+1,y-1))
     * Gx=[-1 0 1] , Gy=[-1 -2 -1]
     * ---[-2 0 2]      [ 0  0  0]
     * ---[-1 0 1]      [ 1  2  1]
     */
    public static int[][] sobel(int[][] array) {
        int row = array.length;
        int column = array[0].length;
        int[][] newArray = new int[row][column];
        sobel(array, newArray, null);
        return ImageUtils.rangeToByte(newArray);
    }

    /**
     * 边缘检测--Sobel
     * Gx=f(x+1,y-1)+2f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+2f(x-1,y)+f(x-1,y+1))
     * Gy=f(x-1,y+1)+2f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+2f(x,y-1)+f(x+1,y-1))
     * Gx=[-1 0 1] , Gy=[-1 -2 -1]
     * ---[-2 0 2]      [ 0  0  0]
     * ---[-1 0 1]      [ 1  2  1]
     *
     * @param array      原图
     * @param rangeArray 幅值图
     * @param angleArray 角度图
     */
    private static void sobel(int[][] array, int[][] rangeArray, double[][] angleArray) {
        int row = array.length;
        int column = array[0].length;
        //注意:x = j = column , y = i = row
        for (int i = 1; i < row - 1; i++) {//图片第几行
            for (int j = 1; j < column - 1; j++) {//图片第几列
                //Gx=f(x+1,y-1)+f(x+1,y)+f(x+1,y+1)-(f(x-1,y-1)+f(x-1,y)+f(x-1,y+1))
                //Gy=f(x-1,y+1)+f(x,y+1)+f(x+1,y+1)-(f(x-1,y-1)+f(x,y-1)+f(x+1,y-1))
                int sumX = array[i - 1][j + 1] + 2 * array[i][j + 1] + array[i + 1][j + 1] -
                        (array[i - 1][j - 1] + 2 * array[i][j - 1] + array[i + 1][j - 1]);
                int sumY = array[i + 1][j - 1] + 2 * array[i + 1][j] + array[i + 1][j + 1] -
                        (array[i - 1][j - 1] + 2 * array[i - 1][j] + array[i - 1][j + 1]);
                if (rangeArray != null)
                    rangeArray[i][j] = (int) Math.round(Math.sqrt(sumX * sumX + sumY * sumY));
                if (angleArray != null)
                    angleArray[i][j] = Math.atan(sumY * 1.0 / sumX);
            }
        }
    }

    /**
     * 边缘检测--Laplacian--v1--噪声敏感
     * G^2=f(x-1,y)+f(x+1,y)+f(x,y-1)+f(x,y+1)-4f(x,y)
     * G^2=[0  1  0]
     * ----[1 -4  1]
     * ----[0  1  0]
     */
    public static int[][] laplacianV1(int[][] array) {
        int row = array.length;
        int column = array[0].length;
        int[][] newArray = new int[row][column];
        //注意:x = j = column , y = i = row
        for (int i = 1; i < row - 1; i++) {//图片第几行
            for (int j = 1; j < column - 1; j++) {//图片第几列
                //G^2=f(x-1,y)+f(x+1,y)+f(x,y-1)+f(x,y+1)-4f(x,y)
                int sum = array[i][j - 1] + array[i][j + 1] + array[i - 1][j] + array[i + 1][j] - 4 * array[i][j];
                newArray[i][j] = (int) Math.round(Math.sqrt(sum));
            }
        }
        return ImageUtils.rangeToByte(newArray);
    }

    /**
     * 边缘检测--Laplacian--v2--噪声敏感
     * G^2=f(x-1,y-1)+f(x,y-1)+f(x+1,y-1)+f(x-1,y)+f(x+1,y)+f(x-1,y+1)+f(x,y+1)+f(x+1,y+1)-8f(x,y)
     * G^2=[1  1  1]
     * ----[1 -8  1]
     * ----[1  1  1]
     */
    public static int[][] laplacianV2(int[][] array) {
        int row = array.length;
        int column = array[0].length;
        int[][] newArray = new int[row][column];
        //注意:x = j = column , y = i = row
        for (int i = 1; i < row - 1; i++) {//图片第几行
            for (int j = 1; j < column - 1; j++) {//图片第几列
                //G^2=f(x-1,y-1)+f(x,y-1)+f(x+1,y-1)+f(x-1,y)-8f(x,y)+f(x+1,y)+f(x-1,y+1)+f(x,y+1)+f(x+1,y+1)
                int sum = array[i - 1][j - 1] + array[i - 1][j] + array[i - 1][j + 1] +
                        array[i][j - 1] - 8 * array[i][j] + array[i][j + 1] +
                        array[i + 1][j - 1] + array[i + 1][j] + array[i + 1][j + 1];
                newArray[i][j] = (int) Math.round(Math.sqrt(sum));
            }
        }
        return ImageUtils.rangeToByte(newArray);
    }

    /**
     * 边缘检测--Canny,高斯过滤-->Sobel算子-->非极大值抑制
     *
     * @param length        高斯过滤的模版长度,length=6sigma(%99.74), length=4sigma(95%), length=2sigma(68%)
     * @param sigma         高斯过滤的模版标准差,length=6sigma(%99.74), length=4sigma(95%), length=2sigma(68%)
     * @param lowThreshold  双阈值检测的低阈值
     * @param highThreshold 双阈值检测的高阈值
     */
    public static int[][] canny(int[][] array, int length, double sigma, int lowThreshold, int highThreshold) {
        //高斯过滤,length=6sigma(%99.74)
        double[][] gaussTemplateArray = FilterUtils.getGaussTemplate(length, sigma);
        int[][] gaussArray = FilterUtils.getGaussFilter(array, gaussTemplateArray);
        //Sobel算子, 获取高斯过滤图和角度图
        int row = array.length;
        int column = array[0].length;
        int[][] rangeArray = new int[row][column];
        double[][] angleArray = new double[row][column];
        sobel(gaussArray, rangeArray, angleArray);
        //非极大值抑制
        int[][] newArray = getDirection(rangeArray, angleArray);
        //双阈值检测
        int[][] resultArray = thresholdDetection(newArray, lowThreshold, highThreshold);
        return ImageUtils.rangeToByte(resultArray);
    }

    private static final double pi16 = Math.PI / 16;

    private static final double[] piArray = new double[]{
            0, pi16 * 1, 2, pi16 * 3, 4, pi16 * 5, 6, pi16 * 7, 8,
            pi16 * 9, 10, pi16 * 11, 12, pi16 * 13, 14, pi16 * 15, 16};

    /**
     * 非极大值抑制
     * 判断沿着梯度方向的3个像素点(中心像素点与另外两个梯度方向区域内的像素点),
     * 若中心像素点的梯度值不比其他两个像素点的梯度值大,则将该像素点的灰度值置0。
     * 方向1:f(x-1,y)-->f(x,y)-->f(x+1,y)      (15/16)Pi-->(1/16)Pi、(7/16)Pi-->(9/16)Pi
     * 方向2:f(x-1,y-1)-->f(x,y)-->f(x+1,y+1)  (1/16)Pi-->(3/16)Pi、(9/16)Pi-->(11/16)Pi
     * 方向3:f(x,y-1)-->f(x,y)-->f(x,y+1)      (3/5)Pi-->(5/16)Pi、(11/16)Pi-->(13/16)Pi
     * 方向4:f(x-1,y+1)-->f(x,y)-->f(x+1,y-1)  (5/7)Pi-->(7/16)Pi、(13/16)Pi-->(15/16)Pi
     *
     * @param rangeArray 高斯过滤图
     * @param angleArray 角度图
     */
    private static int[][] getDirection(int[][] rangeArray, double[][] angleArray) {
        int row = rangeArray.length;
        int column = rangeArray[0].length;
        int[][] array = new int[row][column];
        //注意:x = j = column , y = i = row
        for (int i = 1; i < row - 1; i++) {
            for (int j = 1; j < column - 1; j++) {
                array[i][j] = rangeArray[i][j];
                double angle = angleArray[i][j];
                if ((angle > piArray[15] && angle < piArray[1]) || (angle > piArray[7] && angle < piArray[9])) {
                    //方向1:f(x-1,y)-->f(x,y)-->f(x+1,y)      (15/16)Pi-->(1/16)Pi、(7/16)Pi-->(9/16)Pi
                    if (rangeArray[i][j] < rangeArray[i][j - 1] || rangeArray[i][j] < rangeArray[i][j + 1])
                        array[i][j] = 0;
                } else if ((angle > piArray[1] && angle < piArray[3]) || (angle > piArray[9] && angle < piArray[11])) {
                    //方向2:f(x-1,y-1)-->f(x,y)-->f(x+1,y+1)  (1/16)Pi-->(3/16)Pi、(9/16)Pi-->(11/16)Pi
                    if (rangeArray[i][j] < rangeArray[i - 1][j - 1] || rangeArray[i][j] < rangeArray[i + 1][j + 1])
                        array[i][j] = 0;
                } else if ((angle > piArray[3] && angle < piArray[5]) || (angle > piArray[11] && angle < piArray[13])) {
                    //方向3:f(x,y-1)-->f(x,y)-->f(x,y+1)      (3/5)Pi-->(5/16)Pi、(11/16)Pi-->(13/16)Pi
                    if (rangeArray[i][j] < rangeArray[i - 1][j] || rangeArray[i][j] < rangeArray[i + 1][j])
                        array[i][j] = 0;
                } else {
                    //方向4:f(x-1,y+1)-->f(x,y)-->f(x+1,y-1)  (5/7)Pi-->(7/16)Pi、(13/16)Pi-->(15/16)Pi
                    if (rangeArray[i][j] < rangeArray[i + 1][j - 1] || rangeArray[i][j] < rangeArray[i - 1][j + 1])
                        array[i][j] = 0;
                }
            }
        }
        return array;
    }

    /**
     * 双阈值检测
     * 高阈值图因为作用的阈值较大,去除了大部分的噪声,同时也有可能去除了有用的边缘信息。
     * 低阈值图则保留了较多的边缘信息,我们利用低阈值图来对高阈值图进行补充。
     *
     * @param array         非极大值抑制之后的图像像素数组
     * @param lowThreshold  双阈值检测的低阈值
     * @param highThreshold 双阈值检测的高阈值
     */
    private static int[][] thresholdDetection(int[][] array, int lowThreshold, int highThreshold) {
        int row = array.length;
        int column = array[0].length;
        int[][] resultArray = new int[row][column];
        //二值化,一个地阈值,一个高阈值
        int[][] lowBinaryArray = ImageUtils.binaryProcess(array, lowThreshold);
        int[][] highBinaryArray = ImageUtils.binaryProcess(array, highThreshold);
        //低阈值图补充高阈值图
        //注意:x = j = column , y = i = row
        int[][] highBinaryArrayCopy = highBinaryArray.clone();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < column; j++) {
                if (highBinaryArrayCopy[i][j] != 0) {
//                    thresholdDetectionV1(resultArray, lowBinaryArray, i, j, row, column);
                    thresholdDetectionV2(resultArray, lowBinaryArray, highBinaryArray, i, j, row, column);
                }
            }
        }
        return resultArray;
    }

    /**
     * 双阈值检测--以高阈值图的点为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。
     *
     * @param resultArray    结果图片像素数组
     * @param lowBinaryArray 低阈值图片像素数组
     * @param i              第几行
     * @param j              第几列
     * @param row            总行数
     * @param column         总列数
     */
    private static void thresholdDetectionV1(int[][] resultArray, int[][] lowBinaryArray, int i, int j, int row, int column) {
        if (i < 0 || i >= row || j < 0 || j >= column) {
            return;
        }
        if (resultArray[i][j] != 0) {//已经被扫描过了
            return;
        } else {//resultArray填充
            resultArray[i][j] = lowBinaryArray[i][j];
        }
        //扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
        for (int k = -1; k <= 1; k++) {
            for (int z = -1; z <= 1; z++) {
                if (k == 0 && z == 0) {
                    continue;
                }
                int iAround = i + k;
                int jAround = j + z;
                if (lowBinaryArray[iAround][jAround] != 0) {
                    thresholdDetectionV1(resultArray, lowBinaryArray, iAround, jAround, row, column);
                }
            }
        }
    }

    /**
     * 双阈值检测--以高阈值图中边缘的端点(端点指一个点少于两个与之相邻的点)为中心,只要低阈值图的点与之相邻,就在高阈值图中新增这点,递归这个操作。
     *
     * @param resultArray     结果图片像素数组
     * @param lowBinaryArray  低阈值图片像素数组
     * @param highBinaryArray 高阈值图片像素数组
     * @param i               第几行
     * @param j               第几列
     * @param row             总行数
     * @param column          总列数
     */
    private static void thresholdDetectionV2(int[][] resultArray, int[][] lowBinaryArray, int[][] highBinaryArray, int i, int j, int row, int column) {
        if (i < 0 || i >= row || j < 0 || j >= column) {
            return;
        }
        if (resultArray[i][j] != 0) {//已经被扫描过了
            return;
        } else { //resultArray填充
            resultArray[i][j] = highBinaryArray[i][j];
        }
        //高阈值图中扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
        int aroundCount = 0;//有几个连接点
        for (int k = -1; k <= 1; k++) {
            for (int z = -1; z <= 1; z++) {
                if (k == 0 && z == 0) {
                    continue;
                }
                int iAround = i + k;
                int jAround = j + z;
                //判断是否为强边缘
                if (highBinaryArray[iAround][jAround] != 0) {
                    aroundCount++;
                    thresholdDetectionV2(resultArray, lowBinaryArray, highBinaryArray, iAround, jAround, row, column);
                }
            }
        }
        //判断是边缘的端点。如果不是端点的话,至少应该有两个连接点。
        if (aroundCount < 2) {
            //低阈值图中扫描周围八个点:f(x-1,y-1)、f(x,y-1)、f(x+1,y-1)、f(x-1,y)、f(x+1,y)、f(x-1,y+1)、f(x,y+1)、f(x+1,y+1)
            for (int k = -1; k <= 1; k++) {
                for (int z = -1; z <= 1; z++) {
                    if (k == 0 && z == 0) {
                        continue;
                    }
                    int iAround = i + k;
                    int jAround = j + z;
                    //判断是否为弱边缘
                    if (lowBinaryArray[iAround][jAround] != 0) {
                        highBinaryArray[iAround][jAround] = lowBinaryArray[iAround][jAround];//添加高阈值图的强边缘
                        thresholdDetectionV2(resultArray, lowBinaryArray, highBinaryArray, iAround, jAround, row, column);
                    }
                }
            }
        }
    }
}