From 930856c414e53a01a34a4fee2b6ce52e035e4995 Mon Sep 17 00:00:00 2001 From: origin Date: Fri, 30 Oct 2020 16:59:46 +0800 Subject: [PATCH] # WARNING: head commit changed in the meantime init --- .classpath | 9 +- pom.xml | 6 - .../com/hy/java/uct/umlrecog/OpenCVTest.java | 166 ++++++++++-------- src/test/resources/result.png | Bin 11215 -> 3030 bytes 4 files changed, 98 insertions(+), 83 deletions(-) diff --git a/.classpath b/.classpath index 481b25f..1366856 100644 --- a/.classpath +++ b/.classpath @@ -1,19 +1,20 @@ + + + + - - - - + diff --git a/pom.xml b/pom.xml index 2f09fb0..ee04f5e 100644 --- a/pom.xml +++ b/pom.xml @@ -38,11 +38,5 @@ poi-ooxml 4.1.2 - - - org.openpnp - opencv - 4.3.0-2 - \ No newline at end of file diff --git a/src/test/java/com/hy/java/uct/umlrecog/OpenCVTest.java b/src/test/java/com/hy/java/uct/umlrecog/OpenCVTest.java index ea86df7..fb1f370 100644 --- a/src/test/java/com/hy/java/uct/umlrecog/OpenCVTest.java +++ b/src/test/java/com/hy/java/uct/umlrecog/OpenCVTest.java @@ -3,6 +3,7 @@ package com.hy.java.uct.umlrecog; import java.util.ArrayList; import java.util.List; +import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.MatOfPoint; import org.opencv.core.MatOfPoint2f; @@ -11,117 +12,136 @@ import org.opencv.core.Scalar; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; -import nu.pattern.OpenCV; +import com.hy.java.utility.common.Pair; public class OpenCVTest { - static String image_path = "D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\eclipse.jetty-class diagram.png"; + static String class_diagram_path = "D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\eclipse.jetty-class diagram.png"; public static void main(String[] args) { // TODO Auto-generated method stub - OpenCV.loadShared(); - // System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // 读取图片。并灰度处理 - Mat mat = Imgcodecs.imread(image_path, Imgcodecs.IMREAD_GRAYSCALE); + Mat mat = Imgcodecs.imread(class_diagram_path, Imgcodecs.IMREAD_GRAYSCALE); // 高斯锐化,提升类图图形清晰度 // Imgproc.Laplacian(mat, mat, 2); // 二值化,用于后续处理 Imgproc.threshold(mat, mat, 160, 255, Imgproc.THRESH_BINARY); // 矩形检测 - detectRec(mat); + Pair> rec_detected = detectRec(mat); + // 关系类型检测 + Mat temp = detectRelationType(rec_detected); // 直线检测 - // detectLines(mat); - // 看一眼 - // Imgcodecs.imwrite("D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\result.png",mat); + detectLines(temp); } - private static void detectRec(Mat src) { + /** + * 检测方框 + * + * @param src + * @return + */ + private static Pair> detectRec(Mat src) { // TODO Auto-generated method stub + System.out.println("识别所有类"); + List class_rects = new ArrayList<>(); + // 识别图中所有“轮廓”并保存 List contours = new ArrayList<>(); Imgproc.findContours(src, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); - for (MatOfPoint c : contours) { - if (Imgproc.contourArea(c) < 10) { + // 对每个轮廓,检测是否为矩形 + for (MatOfPoint contour : contours) { + // 如果轮廓面积太小或太大,则直接忽略。阈值可以根据图片像素动态计算 + if (Imgproc.contourArea(contour) < 10 || Imgproc.contourArea(contour) > 100000) { continue; } - - MatOfPoint2f curve = new MatOfPoint2f(c.toArray()); - MatOfPoint2f approxCurve = new MatOfPoint2f(); - // 轮廓逼近 + // 如果轮廓面积合适,则检测是否为矩形。采用多边形逼近法 + MatOfPoint2f curve = new MatOfPoint2f(contour.toArray()); + MatOfPoint2f approx_curve = new MatOfPoint2f(); + // 将轮廓向多边形做逼近 double epsilon = 0.01 * Imgproc.arcLength(curve, true); - Imgproc.approxPolyDP(curve, approxCurve, epsilon, true); -// System.out.print(approxCurve.dump()); - // Drawing lines on the image - if (approxCurve.toArray().length == 4) { - System.out.println(approxCurve.dump()); - double[] data1 = approxCurve.get(0, 0); - double[] data2 = approxCurve.get(1, 0); - double[] data3 = approxCurve.get(2, 0); - double[] data4 = approxCurve.get(3, 0); - Point pt1 = new Point(data1[0], data1[1]); - Point pt2 = new Point(data2[0], data2[1]); - Point pt3 = new Point(data3[0], data3[1]); - Point pt4 = new Point(data4[0], data4[1]); + Imgproc.approxPolyDP(curve, approx_curve, epsilon, true); + // 如果逼近的多边形一共有4个顶点,则可以认为是矩形(其实应该再检查4个顶点的坐标)。将检测结果绘制在原图中 + if (approx_curve.toArray().length == 4) { + // 将每个矩形存在别处用于识别文字。存完后将其从图中抹掉,防止其干扰直线识别 + class_rects.add(contour); + // 将矩形从图中抹掉(涂白)。后面还需对所有边框进行涂白 + Imgproc.fillConvexPoly(src, contour, new Scalar(255, 255, 255)); + // 已下几行其实无用,只是提示如何获取矩形顶点 + // 4个顶点 + Point pt1 = new Point(approx_curve.get(0, 0)[0], approx_curve.get(0, 0)[1]); + Point pt2 = new Point(approx_curve.get(1, 0)[0], approx_curve.get(1, 0)[1]); + Point pt3 = new Point(approx_curve.get(2, 0)[0], approx_curve.get(2, 0)[1]); + Point pt4 = new Point(approx_curve.get(3, 0)[0], approx_curve.get(3, 0)[1]); Imgproc.line(src, pt1, pt2, new Scalar(187, 255, 255), 2); Imgproc.line(src, pt2, pt3, new Scalar(187, 255, 255), 2); Imgproc.line(src, pt3, pt4, new Scalar(187, 255, 255), 2); Imgproc.line(src, pt4, pt1, new Scalar(187, 255, 255), 2); } - System.out.println("<<<<<<<<<<<<<<<<<"); -// double[] data; -// double rho, theta; -// Point pt1 = new Point(); -// Point pt2 = new Point(); -// double a, b; -// double x0, y0; -// -// for (int i = 0; i < approxCurve.rows(); i = i + 4) { -// -// data = approxCurve.get(i, 0); -// pt1.x = data[0]; -// pt1.y = data[1]; -// pt2.x = data[2]; -// pt2.y = data[3]; -// Imgproc.line(src, pt1, pt2, new Scalar(187, 255, 255), 20); -// } - // Writing the image - // Imgcodecs.imwrite("D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\result.png", - // src); - } - - // Imgproc.drawContours(src, contours, -1, new Scalar(0, 0, 255), 1); - // Writing the image + // 对所有边框进行涂白 + Imgproc.drawContours(src, class_rects, -1, new Scalar(255, 255, 255), 8); + // 将图片写入result文件 Imgcodecs.imwrite("D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\result.png", src); + System.out.println("识别类完毕,共" + class_rects.size() + "个"); + return Pair.createPair(src, class_rects); + } + + /** + * 识别关系符号 + * + * @param rec_detected + * @return + */ + private static Mat detectRelationType(Pair> rec_detected) { + // TODO Auto-generated method stub + System.out.println("识别所有关系符号"); + List class_rects = new ArrayList<>(); + // 识别图中所有“轮廓”并保存 + List contours = new ArrayList<>(); + Imgproc.findContours(rec_detected.getLeft(), contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); + // 对每个轮廓,检测是否为“特殊形状” + for (MatOfPoint contour : contours) { + // 如果轮廓面积太小或太大,则直接忽略。阈值可以根据图片像素动态计算 + if (Imgproc.contourArea(contour) < 35 || Imgproc.contourArea(contour) > 1000) { + continue; + } + // 如果轮廓面积合适,则检测是否为关系符号形状。采用多边形逼近法 + MatOfPoint2f curve = new MatOfPoint2f(contour.toArray()); + MatOfPoint2f approx_curve = new MatOfPoint2f(); + // 将轮廓向多边形做逼近 + double epsilon = 0.01 * Imgproc.arcLength(curve, true); + Imgproc.approxPolyDP(curve, approx_curve, epsilon, true); + System.out.print(approx_curve.dump()); + // 将每个关系符号存在别处。存完后将其从图中抹掉,防止其干扰直线识别 + class_rects.add(contour); + // 将关系符号从图中抹掉(涂白)。后面还需对所有边框进行涂白 + Imgproc.fillConvexPoly(rec_detected.getLeft(), contour, new Scalar(15, 225, 25)); + } + // 对所有边框进行涂白 + Imgproc.drawContours(rec_detected.getLeft(), class_rects, -1, new Scalar(255, 255, 255), 8); + // 将图片写入result文件 + Imgcodecs.imwrite("D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\result.png", rec_detected.getLeft()); + System.out.println("识别关系符号完毕,共" + class_rects.size() + "个"); + return rec_detected.getLeft(); } private static void detectLines(Mat src) { // TODO Auto-generated method stub - // Detecting edges of it + System.out.println("识别关系"); + // 先检测边缘。然后从边缘集中检测直线 Mat canny = new Mat(); - Imgproc.Canny(src, canny, 50, 200, 3, false); - - // Detecting the hough lines from (canny) + Imgproc.Canny(src, canny, 50, 150, 3, true); + // 从边缘集中检测直线 Mat lines = new Mat(); - - Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 2, 80, 80, 80); - - // Drawing lines on the image - double[] data; - double rho, theta; - Point pt1 = new Point(); - Point pt2 = new Point(); - double a, b; - double x0, y0; - + Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 4, 12, 17, 20); + // 将检测到的直线绘制到图中 for (int i = 0; i < lines.rows(); i++) { - data = lines.get(i, 0); - pt1.x = data[0]; - pt1.y = data[1]; - pt2.x = data[2]; - pt2.y = data[3]; + Point pt1 = new Point(lines.get(i, 0)[0], lines.get(i, 0)[1]); + Point pt2 = new Point(lines.get(i, 0)[2], lines.get(i, 0)[3]); Imgproc.line(src, pt1, pt2, new Scalar(187, 255, 255), 2); } // Writing the image Imgcodecs.imwrite("D:\\eclipse-committers\\uml-code-trace\\src\\test\\resources\\result.png", src); + System.out.println("识别关系完毕,共" + lines.rows() + "条"); } } diff --git a/src/test/resources/result.png b/src/test/resources/result.png index bd036f17f2d2dccb4198717adc5070adb98a5fbf..d49509128935a2a6340c1f393a42c1a3bfdd31a5 100644 GIT binary patch literal 3030 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYVEn=X6kvFuEw+S#fqSl}i(^OyGt$>bu5=PnB^L+^9+@>JZ6PVFj=XP8RU3rNz)QfR?o>?lhj02d?}Y` zZ5OdWG@py(Ql^&8k{`i80v#s!1ykJ?Dms`ZM#-!S*I$wzY7tx<$bclF!e$M~ z7mw_}sKrSA{j57L4CJ@M>l>QwuFo!LYLi70xVGUir&L)*6v)3vV-NHFia5-to)KZQ z4s61@ME2S>iS5EBI&$G~Gr(HJ`d5R(ZAbPENBQg<4_SD-kAwYn$5iLb&Nf-U3mYn8 zpk_MF&yJ8;ml-Yb4nHac2WB}hckQ~w!WJY~U3e}f=ALomVJ0pG&34y7 zCh++qxdCKF`*Nro-Yn=heM4=ZM!YEIwVty8)g!>rg9imjLANqeSV2632+W1&O?CP% zY&Z;ww0BZszhVwQ;^4KmSqlwqbOmJ>j-v*J^^0VX6>mOl0Y~=kHrX#5!J!Q|6Qrfg z!VwX?AT21)hG~IE5Jsc}odF8HqmRLf3+_&oC_XBP;cDv*fw|F5|Lt{+zt^e-P{K+x%k_ zm;S{U4&CopeGvM}Zgr0De&|Eds^VStKQ>lQv^RUWTWb$654FE`)sI_}*bY<$%xo`o zc&%%~TY)*vF+KAJREd@MVUS*4sj>@^Qe{8Zaphmw(0uOFhQsBd70qq3CDYnu_pLr8 zZSrQpW?-(16$R#J_PI+(jl>g{qE*5rGw!v#Uqy??E3GrfllZM4m2BlyUyFbm?4GWE JF6*2UngB0RD|i3^ literal 11215 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 -- Gitee