图片处理工具

参考文章系列:https://blog.csdn.net/qq_18604209/category_9674847.html

模糊处理

  • 模糊主要应用就是降低噪声

  • 模糊属于线性滤波

  • 例如颜色识别,如果不进行模糊操作,可能会误差很大

均值模糊

  1. blur(Mat src, Mat dst, Size ksize)
  2. blur(Mat src, Mat dst, Size ksize, Point anchor)
  3. blur(Mat src, Mat dst, Size ksize, Point anchor, int borderType)
  • src:表示输入图像
  • dst:输出图像
  • ksize:卷积核大小
  • anchor 卷积核中心位置
  • borderType:填充边缘类型

如果想要水平方向模糊或者垂直方向模糊,那么只需要把size对应的参数改成1

ksize = new Size(9,1)//水平方向模糊
ksize = new Size(1,9)//垂直方向模糊

图片处理工具 - 图1

代码示例:

Mat m1 = Imgcodecs.imread("C:\\test\\256_256_t1.png" );
HighGui.imshow("原图",m1);

Mat s1 = new Mat();
Imgproc.blur(m1,s1,new Size(9,9));//均值模糊

Mat s3 = new Mat();
Imgproc.blur(m1,s3,new Size(9,1));//x(水平)方向模糊

Mat s4 = new Mat();
Imgproc.blur(m1,s4,new Size(1,9));//y(垂直)方向模糊

HighGui.imshow("均值模糊",s1);

HighGui.imshow("x",s3);
HighGui.imshow("y",s4);

高斯模糊

高斯模糊是越接近于卷积核中心的地方 权重越高、越接近于边缘的权重越低

GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX)
GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX, double sigmaY)
GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX, double sigmaY, int borderType)
  • src:表示输入图像
  • dst:输出图像
  • ksize:卷积核大小(当输入的是Size(0,0)的时候,自动从sigmaX计算)
  • sigmaX:X方向模糊程度
  • sigmaY:Y方向模糊程度(可以不填写,代表自动从X方向计算)
  • borderType:填充边缘类型

图片处理工具 - 图2

代码示例:

Mat m1 = Imgcodecs.imread("C:\\test\\256_256_t1.png" );
HighGui.imshow("原图",m1);

Mat s1 = new Mat();
Imgproc.blur(m1,s1,new Size(9,9));
Mat s2 = new Mat();
Imgproc.GaussianBlur(m1,s2,new Size(0,0),3);

HighGui.imshow("均值模糊",s1);
HighGui.imshow("高斯模糊",s2);

高斯滤波

在高斯滤波中,卷积核中的值不再都是1。如下图所示:

图片处理工具 - 图3

在实际使用中,高斯滤波使用的可能是不同大小的卷积核,核的宽度和高度可以不相同,但是它们都必须是奇数,可以根据使用需求选定合适的卷积核。每一种尺寸的卷积核都可以有多种不同形式的权重比例。在实际计算中,卷积核是归一化处理的,严格来讲,使用没有进行归一化处理的卷积核进行滤波,得到的结果往往是错误的。

核心代码

/**
     *
     * <p>Title: Gaussian</p>
     * <p>Description: 高斯模糊</p>
     */
    public static void getGaussian(Map<String,Object> map ,String fileName){
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        org.opencv.core.Mat src = Imgcodecs.imread(fileName);
        if (src.empty()) {
            map.put("error","加载文件失败!");
        }
        int width = src.cols();
        int height = src.rows();
        int dims = src.channels();
        byte[] data = new byte[width * height * dims];
        src.get(0, 0, data);
        int r = 0, g = 0, b = 0;
        Random random = new Random();
        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width; col++) {
                //高斯模糊加的值
                double rf = random.nextGaussian() * 30;
                double gf = random.nextGaussian() * 30;
                double bf = random.nextGaussian() * 30;

                b = data[row * width * dims + col * dims] & 0xff;
                g = data[row * width * dims + col * dims + 1] & 0xff;
                r = data[row * width * dims + col * dims + 2] & 0xff;
                b = clamp(b + bf);
                r = clamp(r + rf);
                g = clamp(g + gf);

                //加上高斯噪声
                data[row * width * dims + col * dims] = (byte)b;
                data[row * width * dims + col * dims + 1] = (byte)g;
                data[row * width * dims + col * dims + 2] =(byte)r;
            }
        }
        src.put(0, 0, data);
        map.put("Gaussian_Noise",new ImageUI().imshow(src));
        //高斯去噪
        Mat dst = new Mat();
        //高斯滤波,调整Size(5, 5)值
        Imgproc.GaussianBlur(src, dst, new Size(5, 5), 15);
        map.put("Gaussian_Blur",new ImageUI().imshow(dst));
    }

    /**
     * 把值控制在0~255之间
     */
    public static int clamp(double d) {
        if (d > 255) {
            return 255;
        } else if (d < 0) {
            return 0;
        } else {
            return (int)d;
        }
    }

边缘检测

边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。 这些包括(i)深度上的不连续、(ii)表面方向不连续、(iii)物质属性变化和(iv)场景照明变化。 边缘检测是图像处理和[计算机视觉中,尤其是特征提取中的一个研究领域。

技术流程

边缘检测分为如下几个步骤:

步骤1:去噪。噪声会影响边缘检测的准确性,因此首先要将噪声过滤掉。

由于图像边缘非常容易受到噪声的干扰,因此为了避免检测到错误的边缘信息,通常 需要对图像进行滤波以去除噪声。滤波的目的是平滑一些纹理较弱的非边缘区域,以便得 到更准确的边缘。在实际处理过程中,通常采用高斯滤波去除图像中的噪声。

步骤2:计算梯度的幅度与方向。

在这里,我们关注梯度的方向,梯度的方向与边缘的方向是垂直的。边缘检测算子返回水平方向的Gx和垂直方向的Gy。梯度的幅度G和方向Θ(用角度值表示)为:

图片处理工具 - 图4
图片处理工具 - 图5

式中,atan2(•)表示具有两个参数的arctan函数。梯度的方向总是与边缘垂直的,通常就近取值为水平(左、右)、垂直(上、下)、对角线(右上、左上、左下、右下)等8个不同的方向。 因此,在计算梯度时,我们会得到梯度的幅度和角度(代表梯度的方向)两个值。

步骤3:非极大值抑制,即适当地让边缘“变瘦”。

在获得了梯度的幅度和方向后,遍历图像中的像素点,去除所有非边缘的点。在具体实现时,逐一遍历像素点,判断当前像素点是否是周围像素点中具有相同梯度方向的最大值,并根据判断结果决定是否抑制该点。通过以上描述可知,该步骤是边缘细化的过程。针对每一个像素点:

● 如果该点是正/负梯度方向上的局部最大值,则保留该点。

● 如果不是,则抑制该点(归零)。

步骤4:确定边缘。使用双阈值算法确定最终的边缘信息。

完成上述步骤后,图像内的强边缘已经在当前获取的边缘图像内。但是,一些虚边缘可能也在边缘图像内。这些虚边缘可能是真实图像产生的,也可能是由于噪声所产生的。对于后者,必须将其剔除。设置两个阈值,其中一个为高阈值maxVal,另一个为低阈值minVal。根据当前边缘 像素的梯度值(指的是梯度幅度,下同)与这两个阈值之间的关系,判断边缘的属性。具体步骤为:

(1)如果当前边缘像素的梯度值大于或等于maxVal,则将当前边缘像素标记为强边缘。

(2)如果当前边缘像素的梯度值介于maxVal与minVal之间,则将当前边缘像素标记为虚边缘(需要保留)。

(3)如果当前边缘像素的梯度值小于或等于minVal,则抑制当前边缘像素。

在上述过程中,我们得到了虚边缘,需要对其做进一步处理。一般通过判断虚边缘与强边缘是否连接,来确定虚边缘到底属于哪种情况。通常情况下,如果一个虚边缘:

● 与强边缘连接,则将该边缘处理为边缘。

● 与强边缘无连接,则该边缘为弱边缘,将其抑制。

注意,高阈值maxVal和低阈值minVal不是固定的,需要针对不同的图像进行定义。

功能示例

边缘检测算法是一种对噪声比较敏感的边缘检测算法,所以通常使用Canny检测之前,收先对图像进行降噪。

一个完整的Canny边缘检测有以下几个步骤组成:

  • 高斯模糊:完成噪声抑制
  • 灰度转换:在灰度图像上计算梯度值
  • 计算梯度:使用Sobel/Scharr
  • 非最大信号抑制:在梯度图像上寻找局部最大轮廓
  • 高低阈值连接:吧边缘像素连接为线段,形成完整边缘轮廓

边缘检测函数

Canny(Mat image, Mat edges, double threshold1, double threshold2, int apertureSize, boolean L2gradient)
  • image:输入图像
  • edges:输出的二值边缘图像
  • threshold1:最低阈值T1
  • threshold2:最高阈值T2
  • apertureSize:用于内部计算梯度的ksize值
  • L2gradient:计算图像梯度计算方法

示例代码:

Mat m1 = Imgcodecs.imread("C:\\test\\256_256_t1.png" );

Mat s1 = new Mat();
Imgproc.Canny(m1,s1,50,150,3,true)

HighGui.imshow("Canny",s1);

OpenCV支持从两个已经计算出来的X方向梯度和Y方向梯度,来检测边缘。

API如下:

Canny(Mat dx, Mat dy, Mat edges, double threshold1, double threshold2)
  • dx:X方向的梯度图像
  • dy:Y方向的梯度图像
  • edges :输出的二值边缘图像
  • threshold1:最低阈值T1
  • threshold2:最高阈值T2

这里需要注意的是 这里的dx个dy图像深度 必须是CV_16S

示例代码:

Mat m1 = Imgcodecs.imread("C:\\test\\256_256_t1.png" );
HighGui.imshow("原图",m1);

Mat s1 = new Mat();
Imgproc.Sobel(m1,s1, CvType.CV_16S,1,0);

Mat s2 = new Mat();
Imgproc.Sobel(m1,s2, CvType.CV_16S,0,1);

Mat s3 = new Mat();
Imgproc.Canny(s1,s2,s3,50,150);

//为了展示s1和s2 转换深度
Core.convertScaleAbs(s1,s1);
Core.convertScaleAbs(s2,s2);

HighGui.imshow("X方向梯度",s1);
HighGui.imshow("Y方向梯度",s2);
HighGui.imshow("Canny边缘检测",s3);

图片处理工具 - 图6

核心代码

 /**
     *
     * <p>Title: Canny</p>
     * <p>Description:Canny边缘检测</p>
     */

   public static void getCanny(Map<String,Object> map ,String fileName){
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        Mat src = Imgcodecs.imread(fileName);//输入图像
       if (src.empty()) {
           map.put("error","加载文件失败!");
       }
        //ImageUI ui = new ImageUI();
        //ui.imshow("OpenCV_23_Input", src);

        //使用OpenCV Canny API用法一
        /*//1.高斯模糊
        Mat dst = new Mat();
        Imgproc.GaussianBlur(src, dst, new Size(3, 3), 0);
        //2.灰度处理
        Mat gray = new Mat();
        Imgproc.cvtColor(dst, gray, Imgproc.COLOR_BGR2GRAY);
        //3.边缘提取
        Mat output = new Mat();
        Imgproc.Canny(gray, output, 50, 150, 3, true);
        ImageUI out = new ImageUI();
        out.imshow("OpenCV_Canny_Output", output);*/

        //使用OpenCV Canny API用法二
        Mat dst = new Mat();
        Imgproc.GaussianBlur(src, dst, new Size(3, 3), 0);

        Mat gray = new Mat();
        Imgproc.cvtColor(dst, gray, Imgproc.COLOR_BGR2GRAY);

        Mat xgrad = new Mat();
        Imgproc.Sobel(gray, xgrad, CvType.CV_16S, 1, 0);

        Mat ygrad = new Mat();
        Imgproc.Sobel(gray, ygrad, CvType.CV_16S, 0, 1);
        //边缘提取
        Mat output = new Mat();
        Imgproc.Canny(xgrad, ygrad, output, 50, 150);

        //让提取边缘结果变彩色
        Mat edges = new Mat();
        src.copyTo(edges, output);

        //ImageUI out = new ImageUI();
        //out.imshow("OpenCV_Canny_Output", edges);
        map.put("canny",new ImageUI().imshow(edges));

        //使用OpenCV Canny API用法三
        /*Mat dst = new Mat();
        Imgproc.GaussianBlur(src, dst, new Size(3, 3), 0);

        Mat output = new Mat();
        Imgproc.Canny(dst, output, 50, 150);
        ImageUI out = new ImageUI();
        out.imshow("OpenCV_Canny_Output", output);*/
    }

轮廓识别

有时候,我们希望Canny边缘提取出来的图像是完整的轮廓,但是Canny出来的却是一些边缘的像素信息,并没有向我们提供完整的轮廓

OpenCV]中有一组轮廓发现与绘制的函数,能帮助我们发现轮廓与绘制轮廓

轮廓发现

图像的轮廓一般都有啥由一系列的像素点组成的,一般为二值图像,每一组轮廓都是一组像素点,从这些点还可以看出一条曲线上的其他各点,假设图像中有多个轮廓,则会生成多个轮廓描述的数组

函数的Api如下:

findContours(Mat image, List<MatOfPoint> contours, Mat hierarchy, int mode, int method, Point offset)
  • image:输入图像,必须是8位单通道图像

  • conours:是List类型,List里的每一个元素都是一个轮廓对应的像素点集合

  • hierarchy:拓扑信息

  • mode:返回宽阔拓扑模式一共有四种

    • RETR_EXTERNAL = 0 : 表示捕获最外层最大的轮廓
    • RETR_LIST = 1 : 表示获取所有的轮廓,按照List队列顺序组织
    • RETR_CCOMP = 2 : 表示获取所有轮廓呈现的双层结构组织,第一层是外边界,第二程是孔边界
    • RETR_TREE = 3 : 表示回去的轮廓是按照树形结构进行组织,显示归属于嵌套层次
  • method:描述轮廓的方法,一般是基于链式编码

    • CHAIN_APPROX_NONE = 1:将链式编码中的所有点都转换为输出
    • CHAIN_APPROX_SIMPLE = 2:压缩水平,垂直、倾斜部分的轮廓点输出
    • CHAIN_APPROX_TC89_L1 = 3:使用Teh-Chin链式逼近算法中的一种
    • CHAIN_APPROX_TC89_KCOS = 4:使用Teh-Chin链式逼近算法中的一种
  • offset:是否有位移,默认是(0,0)

轮廓绘制

对发现的轮廓List对象使用循环可以遍历每一个轮廓面对轮廓都可以使用轮廓绘制

下面是轮廓绘制的Api:

drawContours(Mat image, List<MatOfPoint> contours, int contourIdx, Scalar color, int thickness)
  • image:要绘制的轮廓图像,通常可以创建一张空白的黑色背景图像
  • contours:轮廓数据来自轮廓发现的函数输出
  • contourIdx:声明要绘制第几个轮廓
  • color:绘制轮廓的颜色
  • tickness:声明绘制轮廓时使用的线宽,当<0的时候表示填充改轮廓

功能示例

Mat m = Imgcodecs.imread("C:\\test\\tx.jpg",Imgcodecs.IMREAD_REDUCED_COLOR_2);

HighGui.imshow("原始图片",m);

Mat m1 =new Mat();//灰度
Imgproc.cvtColor(m,m1,Imgproc.COLOR_BGR2GRAY);

Mat m2 = new Mat();//二值
Imgproc.threshold(m1,m2,0,255,Imgproc.THRESH_BINARY|Imgproc.THRESH_OTSU);


Mat m3 = new Mat();//轮廓发现
List<MatOfPoint> list = new ArrayList<>();
Imgproc.findContours(m2,list,m3,Imgproc.RETR_TREE,Imgproc.CHAIN_APPROX_NONE);

//轮廓绘制
Mat m4 = new Mat(m2.size(),m2.type());
for (int i =0;i<list.size();i++){
    Imgproc.drawContours(m4,list,i,new Scalar(255,255,255),1);
}

HighGui.imshow("灰度",m1);
HighGui.imshow("二值",m2);
HighGui.imshow("轮廓",m4);

图片处理工具 - 图7

核心代码

 /**
     *
     * <p>Title: FindContours</p>
     * <p>Description: 轮廓发现</p>
     */
    public static void findContours(Map<String,Object> map ,String fileName) {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        Mat src = Imgcodecs.imread(fileName);
        if (src.empty()) {
            map.put("error","加载文件失败!");
        }
        demoTwo(src,map);
        //ImageUI ui = new ImageUI();
        //ui.imshow("OpenCV_26_Input", src);
        //demoOne(src);
    }

    /**
     * 有阈值,复杂图像,比demoOne更精确
     * @param src
     */
    public static void demoTwo(Mat src, Map<String,Object> map) {
        Mat gray = new Mat();
        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
        //双边模糊
        Mat dst = new Mat();
        Imgproc.bilateralFilter(gray, dst, 0, 50, 5);
        //边缘提取
        Mat edges = new Mat();
        Imgproc.Canny(dst, edges, 30, 200);

        map.put("Binary",new ImageUI().imshow(edges));
        //ImageUI ui_binary = new ImageUI();
        //ui_binary.imshow("OpenCV_Binary_Two", edges);

        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(edges, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE, new Point(0, 0));

        Mat result = src.clone();
        for (int i = 0; i < contours.size(); i++) {
            Imgproc.drawContours(result, contours, i, new Scalar(0, 0, 255), 2);
        }
        map.put("Contours",new ImageUI().imshow(result));
        //ImageUI out = new ImageUI();
        //out.imshow("OpenCV_Result_demoTwo", result);
    }

    /**
     * 无阈值,使用于简单图像
     * @param src
     */
    public static void demoOne(Mat src) {
        Mat gray = new Mat();
        Mat binary = new Mat();
        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
        //二值化
        Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);

        ImageUI ui_binary = new ImageUI();
        ui_binary.imshow("OpenCV_Binary_One", binary);

        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE, new Point(0, 0));

        Mat result = src.clone();
        for (int i = 0; i < contours.size(); i++) {
            Imgproc.drawContours(result, contours, i, new Scalar(0, 255, 0), 2);
        }
        ImageUI out = new ImageUI();
        out.imshow("OpenCV_Result_demoOne", result);
    }

后续功能

OpenCV在图片处理的功能还有很多,大家可以前往仓库地址查看:Opencv_Java: OpenCV Java

其中涉及的算法知识大家可以参考文章开篇提到的文章系列,也可以自寻百度求解,这里就不过多赘述。

项目如果后期完善的话,可以将其引进拓展。