diff --git a/util/Convert.java b/util/Convert.java new file mode 100644 index 00000000..4e447336 --- /dev/null +++ b/util/Convert.java @@ -0,0 +1,87 @@ +package com.yuxue.util; + +import org.bytedeco.javacpp.BytePointer; + +/** + * There are 3 kinds of convert functions: + * 1. [float|double|int|long] to[Float|Double|Int|Long](BytePointer pointer) + * 2. byte[] getBytes([float|double|int|long] value) + * 3. [float|double|int|long] to[Float|Double|Int|Long](byte[] value) + * + * @author lin.yao + * + */ +public class Convert { + + public static float toFloat(BytePointer pointer) { + byte[] buffer = new byte[4]; + pointer.get(buffer); + return toFloat(buffer); + } + + public static double toDouble(BytePointer pointer) { + byte[] buffer = new byte[8]; + pointer.get(buffer); + return toDouble(buffer); + } + + public static int toInt(BytePointer pointer) { + byte[] buffer = new byte[4]; + pointer.get(buffer); + return toInt(buffer); + } + + public static long toLong(BytePointer pointer) { + byte[] buffer = new byte[8]; + pointer.get(buffer); + return toLong(buffer); + } + + public static byte[] getBytes(float value) { + return getBytes(Float.floatToIntBits(value)); + } + + public static byte[] getBytes(double value) { + return getBytes(Double.doubleToLongBits(value)); + } + + public static byte[] getBytes(int value) { + final int length = 4; + byte[] buffer = new byte[length]; + for (int i = 0; i < length; ++i) + buffer[i] = (byte) ((value >> (i * 8)) & 0xFF); + return buffer; + } + + public static byte[] getBytes(long value) { + final int length = 8; + byte[] buffer = new byte[length]; + for (int i = 0; i < length; ++i) + buffer[i] = (byte) ((value >> (i * 8)) & 0xFF); + return buffer; + } + + public static int toInt(byte[] value) { + final int length = 4; + int n = 0; + for (int i = 0; i < length; ++i) + n += (value[i] & 0xFF) << (i * 8); + return n; + } + + public static long toLong(byte[] value) { + final int length = 8; + long n = 0; + for (int i = 0; i < length; ++i) + n += ((long) (value[i] & 0xFF)) << (i * 8); + return n; + } + + public static double toDouble(byte[] value) { + return Double.longBitsToDouble(toLong(value)); + } + + public static float toFloat(byte[] value) { + return Float.intBitsToFloat(toInt(value)); + } +} diff --git a/util/FileUtil.java b/util/FileUtil.java new file mode 100644 index 00000000..55c45114 --- /dev/null +++ b/util/FileUtil.java @@ -0,0 +1,196 @@ +package com.yuxue.util; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.yuxue.exception.ResultReturnException; + + +/** + * + * @author yuxue + * @date 2020-04-19 15:23 + */ +public class FileUtil { + + static Lock lock = new ReentrantLock(); + + public static boolean copyAndRename(String from, String to) { + Path sourcePath = Paths.get(from); + Path destinationPath = Paths.get(to); + try { + Files.copy(sourcePath, destinationPath); + } catch(FileAlreadyExistsException e) { + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + public static boolean checkFile(final File file) { + if(file.exists() && file.isFile()) { + return true; + } + return false; + } + + /** + * 重命名文件 + * @param file + * @param newName 可以是文件名,也可以是路径+文件名 + * @return + */ + public static boolean renameFile(String filePath, String newName) { + File file = new File(filePath); + return renameFile(file, newName); + } + + /** + * 重命名文件 + * @param file + * @param newName 可以是文件名,也可以是路径+文件名 + * @return + */ + public static boolean renameFile(File file, String newName) { + if(file.exists()) { + String targetPath = null; + if(newName.indexOf("/") >= 0 || newName.indexOf("\\\\") >= 0) { + targetPath = newName; + } else { + targetPath = file.getParentFile().getAbsolutePath() + "/" + newName; + } + + File targetFile = new File(targetPath); + file.renameTo(targetFile); + return true; + } + return false; + } + + public static void createDir(String dir) { + File file = new File(dir); + if(file.exists() && file.isDirectory()) { + return ; + } else { + file.mkdirs(); + } + } + + /** + * 删除并重新创建目录 + * @param dir + */ + public static void recreateDir(final String dir) { + new File(dir).delete(); + new File(dir).mkdir(); + } + + + /** + * 递归获取文件信息 + * @param path String类型 + * @param files + */ + public static void getFiles(final String path, Vector files) { + getFiles(new File(path), files); + } + + + /** + * 递归获取文件信息 + * @param dir FIle类型 + * @param files + */ + private static void getFiles(final File dir, Vector files) { + File[] filelist = dir.listFiles(); + for (File file : filelist) { + if (file.isDirectory()) { + getFiles(file, files); + } else { + files.add(file.getAbsolutePath()); + } + } + } + + + /** + * + * @param dir + * @param filename + * @param recursive + * @return + */ + public static List listFile(File dir, final String fileType, boolean recursive) { + if (!dir.exists()) { + throw new ResultReturnException("目录:" + dir + "不存在"); + } + + if (!dir.isDirectory()) { + throw new ResultReturnException(dir + "不是目录"); + } + + FileFilter ff = null; + if (fileType == null || fileType.length() == 0) { + ff = new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }; + } else { + ff = new FileFilter() { + @Override + public boolean accept(File pathname) { + if (pathname.isDirectory()) { + return true; + } + String name = pathname.getName().toLowerCase(); + String format = name.substring(name.lastIndexOf(".") + 1); + if (fileType.contains(format)) { + return true; + } else { + return false; + } + } + }; + } + return listFile(dir, ff, recursive); + } + + + + /** + * + * @param dir + * @param ff + * @param recursive 是否遍历子目录 + * @return + */ + public static List listFile(File dir, FileFilter ff, boolean recursive) { + List list = new ArrayList(); + File[] files = dir.listFiles(ff); + if (files != null && files.length > 0) { + for (File f : files) { + // 如果是文件,添加文件到list中 + if (f.isFile() || (f.isDirectory() && !f.getName().startsWith("."))) { + list.add(f); + } else if (recursive) { + // 获取子目录中的文件,添加子目录中的经过过滤的所有文件添加到list + list.addAll(listFile(f, ff, true)); + } + } + } + return list; + } + +} diff --git a/util/ImageUtil.java b/util/ImageUtil.java new file mode 100644 index 00000000..fc22f67c --- /dev/null +++ b/util/ImageUtil.java @@ -0,0 +1,899 @@ +package com.yuxue.util; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Vector; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.yuxue.constant.Constant; +import com.yuxue.enumtype.PlateColor; + + +/** + * 车牌图片处理工具类 + * 将原图,经过算法处理,得到车牌的图块 + * @author yuxue + * @date 2020-05-18 12:07 + */ +public class ImageUtil { + + private static String DEFAULT_BASE_TEST_PATH = "D:/PlateDetect/temp/"; + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + // 车牌定位处理步骤,该map用于表示步骤图片的顺序 + private static Map debugMap = Maps.newLinkedHashMap(); + static { + debugMap.put("yuantu", 0); // 原图 + debugMap.put("gaussianBlur", 0); // 高斯模糊 + debugMap.put("gray", 0); // 图像灰度化 + debugMap.put("sobel", 0); // Sobel 运算,得到图像的一阶水平方向导数 + debugMap.put("threshold", 0); //图像二值化 + debugMap.put("morphology", 0); // 图像闭操作 + debugMap.put("clearInnerHole", 0); // 降噪 + debugMap.put("clearSmallConnArea", 0); // 降噪 + debugMap.put("clearAngleConn", 0); // 降噪 + debugMap.put("clearHole", 0); // 降噪 + debugMap.put("contours", 0); // 提取外部轮廓 + debugMap.put("screenblock", 0); // 外部轮廓筛选 + debugMap.put("crop", 0); // 切图 + debugMap.put("resize", 0); // 切图resize + + // 设置index, 用于debug生成文件时候按名称排序 + Integer index = 100; + for (Entry entry : debugMap.entrySet()) { + entry.setValue(index); + index ++; + } + } + + public static void main(String[] args) { + Instant start = Instant.now(); + String tempPath = DEFAULT_BASE_TEST_PATH + "test/"; + String filename = tempPath + "/100_yuantu.jpg"; + filename = tempPath + "/100_yuantu1.jpg"; + // filename = tempPath + "/109_crop_0.png"; + + // 读取原图 + Mat src = Imgcodecs.imread(filename); + + Boolean debug = true; + + // 高斯模糊 + Mat gsMat = ImageUtil.gaussianBlur(src, debug, tempPath); + + // 灰度图 + Mat gray = ImageUtil.gray(gsMat, debug, tempPath); + + Mat sobel = ImageUtil.sobel(gray, debug, tempPath); + + Mat threshold = ImageUtil.threshold(sobel, debug, tempPath); + + // Mat scharr = ImageUtil.scharr(gray, debug, tempPath); + // Mat threshold = ImageUtil.threshold(scharr, debug, tempPath); + + Mat morphology = ImageUtil.morphology(threshold, debug, tempPath); + + List contours = ImageUtil.contours(src, morphology, debug, tempPath); + + Vector rects = ImageUtil.screenBlock(src, contours, debug, tempPath); + + PlateUtil.loadSvmModel("D:/PlateDetect/train/plate_detect_svm/svm2.xml"); + PlateUtil.loadAnnModel("D:/PlateDetect/train/chars_recognise_ann/ann.xml"); + + Vector dst = new Vector(); + PlateUtil.hasPlate(rects, dst, debug, tempPath); + + System.err.println("识别到的车牌数量:" + dst.size()); + dst.stream().forEach(inMat -> { + PlateColor color = PlateUtil.getPlateColor(inMat, true, debug, tempPath); + System.err.println(color.desc); + + Vector charMat = new Vector(); + PlateUtil.charsSegment(inMat, color, charMat, debug, tempPath); + + + }); + + /*String filename = tempPath + "/hsvMat_1590994270425.jpg"; + Mat src = Imgcodecs.imread(filename); + Vector charMat = new Vector(); + PlateUtil.charsSegment(src, PlateColor.BLUE, charMat, true, tempPath);*/ + + Instant end = Instant.now(); + System.err.println("总耗时:" + Duration.between(start, end).toMillis()); + + // ImageUtil.rgb2Hsv(src, debug, tempPath); + // ImageUtil.getHSVValue(src, debug, tempPath); + } + + + + /** + * 高斯模糊 + * @param inMat + * @param debug + * @return + */ + public static final int DEFAULT_GAUSSIANBLUR_SIZE = 5; + public static Mat gaussianBlur(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.GaussianBlur(inMat, dst, new Size(DEFAULT_GAUSSIANBLUR_SIZE, DEFAULT_GAUSSIANBLUR_SIZE), 0, 0, Core.BORDER_DEFAULT); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("gaussianBlur") + "_gaussianBlur.jpg", dst); + } + return dst; + } + + + /** + * 将图像进行灰度化 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat gray(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.cvtColor(inMat, dst, Imgproc.COLOR_BGR2GRAY); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("gray") + "_gray.jpg", dst); + } + inMat.release(); + return dst; + } + + + /** + * 对图像进行Sobel 运算,得到图像的一阶水平方向导数 + * @param inMat 灰度图 + * @param debug + * @param tempPath + * @return + */ + public static final int SOBEL_SCALE = 1; + public static final int SOBEL_DELTA = 0; + public static final int SOBEL_X_WEIGHT = 1; + public static final int SOBEL_Y_WEIGHT = 0; + public static Mat sobel(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + // Sobel滤波 计算水平方向灰度梯度的绝对值 + Imgproc.Sobel(inMat, grad_x, CvType.CV_16S, 1, 0, 3, SOBEL_SCALE, SOBEL_DELTA, Core.BORDER_DEFAULT); + Core.convertScaleAbs(grad_x, abs_grad_x); // 增强对比度 + + Imgproc.Sobel(inMat, grad_y, CvType.CV_16S, 0, 1, 3, SOBEL_SCALE, SOBEL_DELTA, Core.BORDER_DEFAULT); + Core.convertScaleAbs(grad_y, abs_grad_y); + grad_x.release(); + grad_y.release(); + + // 计算结果梯度 + Core.addWeighted(abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, dst); + abs_grad_x.release(); + abs_grad_y.release(); + + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("sobel") + "_sobel.jpg", dst); + } + return dst; + } + + + /** + * 对图像进行scharr 运算,得到图像的一阶水平方向导数 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat scharr(Mat inMat, Boolean debug, String tempPath) { + + Mat dst = new Mat(); + + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + //注意求梯度的时候我们使用的是Scharr算法,sofia算法容易收到图像细节的干扰 + //所谓梯度运算就是对图像中的像素点进行就导数运算,从而得到相邻两个像素点的差异值 by:Tantuo + Imgproc.Scharr(inMat, grad_x, CvType.CV_32F, 1, 0); + Imgproc.Scharr(inMat, grad_y, CvType.CV_32F, 0, 1); + //openCV中有32位浮点数的CvType用于保存可能是负值的像素数据值 + Core.convertScaleAbs(grad_x, abs_grad_x); + Core.convertScaleAbs(grad_y, abs_grad_y); + //openCV中使用release()释放Mat类图像,使用recycle()释放BitMap类图像 + grad_x.release(); + grad_y.release(); + + Core.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst); + abs_grad_x.release(); + abs_grad_y.release(); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("sobel") + "_sobel.jpg", dst); + } + return dst; + } + + + /** + * 对图像进行二值化。将灰度图像(每个像素点有256个取值可能, 0代表黑色,255代表白色) + * 转化为二值图像(每个像素点仅有1和0两个取值可能) + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat threshold(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(); + Imgproc.threshold(inMat, dst, 100, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("threshold") + "_threshold.jpg", dst); + } + inMat.release(); + return dst; + } + + + /** + * 使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + // public static final int DEFAULT_MORPH_SIZE_WIDTH = 15; + // public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + public static final int DEFAULT_MORPH_SIZE_WIDTH = 9; + public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + public static Mat morphology(Mat inMat, Boolean debug, String tempPath) { + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + Size size = new Size(DEFAULT_MORPH_SIZE_WIDTH, DEFAULT_MORPH_SIZE_HEIGHT); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, size); + Imgproc.morphologyEx(inMat, dst, Imgproc.MORPH_CLOSE, element); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("morphology") + "_morphology0.jpg", dst); + } + + // 填补内部孔洞,为了去除小连通区域的时候,降低影响 + Mat a = clearInnerHole(dst, 8, 16, debug, tempPath); + + // 去除小连通区域 + Mat b = clearSmallConnArea(a, 1, 10, debug, tempPath); + + // 按斜边去除 + // Mat e = clearAngleConn(b, 5, debug, tempPath); + + // 填补边缘孔洞 + // Mat d = clearHole(a, 4, 2, debug, tempPath); + + return b; + } + + + /** + * Find 轮廓 of possibles plates 求轮廓。求出图中所有的轮廓。 + * 这个算法会把全图的轮廓都计算出来,因此要进行筛选。 + * @param src 原图 + * @param inMat morphology Mat + * @param debug + * @param tempPath + * @return + */ + public static List contours(Mat src, Mat inMat, Boolean debug, String tempPath) { + List contours = Lists.newArrayList(); + Mat hierarchy = new Mat(); + // 提取外部轮廓 + // CV_RETR_EXTERNAL只检测最外围轮廓, + // CV_RETR_LIST 检测所有的轮廓 + // CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内 + Imgproc.findContours(inMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + if (debug) { + Mat result = new Mat(); + src.copyTo(result); // 复制一张图,不在原图上进行操作,防止后续需要使用原图 + // 将轮廓描绘到原图 + Imgproc.drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + // 输出带轮廓的原图 + Imgcodecs.imwrite(tempPath + debugMap.get("contours") + "_contours.jpg", result); + } + return contours; + } + + + /** + * 根据轮廓, 筛选出可能是车牌的图块 + * @param src + * @param matVector + * @param debug + * @param tempPath + * @return + */ + public static final int DEFAULT_ANGLE = 30; // 角度判断所用常量 + public static final int TYPE = CvType.CV_8UC3; + public static Vector screenBlock(Mat src, List contours, Boolean debug, String tempPath){ + Vector dst = new Vector(); + List mv = Lists.newArrayList(); // 用于在原图上描绘筛选后的结果 + for (int i = 0, j = 0; i < contours.size(); i++) { + MatOfPoint m1 = contours.get(i); + MatOfPoint2f m2 = new MatOfPoint2f(); + m1.convertTo(m2, CvType.CV_32F); + // RotatedRect 该类表示平面上的旋转矩形,有三个属性: 矩形中心点(质心); 边长(长和宽); 旋转角度 + // boundingRect()得到包覆此轮廓的最小正矩形, minAreaRect()得到包覆轮廓的最小斜矩形 + RotatedRect mr = Imgproc.minAreaRect(m2); + + double angle = Math.abs(mr.angle); + + if (checkPlateSize(mr) && angle <= DEFAULT_ANGLE) { // 判断尺寸及旋转角度 ±30°,排除不合法的图块 + mv.add(contours.get(i)); + Size rect_size = new Size((int) mr.size.width, (int) mr.size.height); + if (mr.size.width / mr.size.height < 1) { // 宽度小于高度 + angle = 90 + angle; // 旋转90° + rect_size = new Size(rect_size.height, rect_size.width); + } + + // 旋转角度,根据需要是否进行角度旋转 + /*Mat img_rotated = new Mat(); + Mat rotmat = Imgproc.getRotationMatrix2D(mr.center, angle, 1); // 旋转 + Imgproc.warpAffine(src, img_rotated, rotmat, src.size()); // 仿射变换 考虑是否需要进行投影变换? + */ + + // 切图 + Mat img_crop = new Mat(); + Imgproc.getRectSubPix(src, rect_size, mr.center, img_crop); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("crop") + "_crop_" + j + ".png", img_crop); + } + + // 处理切图,调整为指定大小 + Mat resized = new Mat(Constant.DEFAULT_HEIGHT, Constant.DEFAULT_WIDTH, TYPE); + Imgproc.resize(img_crop, resized, resized.size(), 0, 0, Imgproc.INTER_CUBIC); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("resize") + "_resize_" + j + ".png", resized); + j++; + } + dst.add(resized); + } + } + if (debug) { + Mat result = new Mat(); + src.copyTo(result); // 复制一张图,不在原图上进行操作,防止后续需要使用原图 + // 将轮廓描绘到原图 + Imgproc.drawContours(result, mv, -1, new Scalar(0, 0, 255, 255)); + // 输出带轮廓的原图 + Imgcodecs.imwrite(tempPath + debugMap.get("screenblock") + "_screenblock.jpg", result); + } + return dst; + } + + /** + * 对minAreaRect获得的最小外接矩形 + * 判断面积以及宽高比是否在制定的范围内 + * 黄牌、蓝牌、绿牌 + * 国内车牌大小: 440mm*140mm,宽高比 3.142857 + * @param mr + * @return + */ + final static float DEFAULT_ERROR = 0.7f; // 宽高比允许70%误差 + final static float DEFAULT_ASPECT = 3.142857f; + public static final int DEFAULT_VERIFY_MIN = 1; + public static final int DEFAULT_VERIFY_MAX = 30; + private static boolean checkPlateSize(RotatedRect mr) { + + // 切图面积取值范围 + int min = 44 * 14 * DEFAULT_VERIFY_MIN; + int max = 44 * 14 * DEFAULT_VERIFY_MAX; + + // 切图横纵比取值范围;关键在于纵横比例 + float rmin = DEFAULT_ASPECT - DEFAULT_ASPECT * DEFAULT_ERROR; + float rmax = DEFAULT_ASPECT + DEFAULT_ASPECT * DEFAULT_ERROR; + + // 切图计算面积 + int area = (int) (mr.size.height * mr.size.width); + // 切图宽高比 + double r = mr.size.width / mr.size.height; + /*if (r < 1) { // 注释掉,不处理width 小于height的图片 + r = mr.size.height / mr.size.width; + }*/ + return min <= area && area <= max && rmin <= r && r <= rmax; + } + + + /** + * rgb图像转换为hsv图像 + * @param inMat + * @param debug + * @param tempPath + * @return + */ + public static Mat rgb2Hsv(Mat inMat, Boolean debug, String tempPath) { + // 转到HSV空间进行处理 + Mat dst = new Mat(); + Imgproc.cvtColor(inMat, dst, Imgproc.COLOR_BGR2HSV); + List hsvSplit = Lists.newArrayList(); + Core.split(dst, hsvSplit); + // 直方图均衡化是一种常见的增强图像对比度的方法,使用该方法可以增强局部图像的对比度,尤其在数据较为相似的图像中作用更加明显 + Imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + Core.merge(hsvSplit, dst); + + if (debug) { + // Imgcodecs.imwrite(tempPath + "hsvMat_"+System.currentTimeMillis()+".jpg", dst); + } + return dst; + } + + + /** + * 获取HSV中各个颜色所对应的H的范围 + * HSV是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是:色调(H, Hue),饱和度(S,Saturation),明度(V, Value) + * 1.PS软件时,H取值范围是0-360,S取值范围是(0%-100%),V取值范围是(0%-100%)。 + * 2.利用openCV中cvSplit函数的在选择图像IPL_DEPTH_32F类型时,H取值范围是0-360,S取值范围是0-1(0%-100%),V取值范围是0-1(0%-100%)。 + * 3.利用openCV中cvSplit函数的在选择图像IPL_DEPTH_8UC类型时,H取值范围是0-180,S取值范围是0-255,V取值范围是0-255 + * @param inMat + * @param debug + */ + public static void getHSVValue(Mat inMat, Boolean debug, String tempPath) { + int nRows = inMat.rows(); + int nCols = inMat.cols(); + Map map = Maps.newHashMap(); + for (int i = 0; i < nRows; ++i) { + for (int j = 0; j < nCols; j += 3) { + int H = (int)inMat.get(i, j)[0]; + // int S = (int)inMat.get(i, j)[1]; + // int V = (int)inMat.get(i, j)[2]; + if(map.containsKey(H)) { + int count = map.get(H); + map.put(H, count+1); + } else { + map.put(H, 1); + } + } + } + Set set = map.keySet(); + Object[] arr = set.toArray(); + Arrays.sort(arr); + for (Object key : arr) { + System.out.println(key + ": " + map.get(key)); + } + return; + } + + + + /** + * 计算最大内接矩形 + * https://blog.csdn.net/cfqcfqcfqcfqcfq/article/details/53084090 + * @param inMat + * @return + */ + public static Rect maxAreaRect(Mat threshold, Point point) { + int edge[] = new int[4]; + edge[0] = (int) point.x + 1;//top + edge[1] = (int) point.y + 1;//right + edge[2] = (int) point.y - 1;//bottom + edge[3] = (int) point.x - 1;//left + + boolean[] expand = { true, true, true, true};//扩展标记位 + int n = 0; + while (expand[0] || expand[1] || expand[2] || expand[3]){ + int edgeID = n % 4; + expand[edgeID] = expandEdge(threshold, edge, edgeID); + n++; + } + Point tl = new Point(edge[3], edge[0]); + Point br = new Point(edge[1], edge[2]); + return new Rect(tl, br); + } + + + /** + * @brief expandEdge 扩展边界函数 + * @param img:输入图像,单通道二值图,深度为8 + * @param edge 边界数组,存放4条边界值 + * @param edgeID 当前边界号 + * @return 布尔值 确定当前边界是否可以扩展 + */ + public static boolean expandEdge(Mat img, int edge[], int edgeID) { + int nc = img.cols(); + int nr = img.rows(); + + switch (edgeID) { + case 0: + if (edge[0] > nr) { + return false; + } + for (int i = edge[3]; i <= edge[1]; ++i) { + if (img.get(edge[0], i)[0]== 255) {// 遇见255像素表明碰到边缘线 + return false; + } + } + edge[0]++; + return true; + case 1: + if (edge[1] > nc) { + return false; + } + for (int i = edge[2]; i <= edge[0]; ++i) { + if (img.get(i, edge[1])[0] == 255) + return false; + } + edge[1]++; + return true; + case 2: + if (edge[2] < 0) { + return false; + } + for (int i = edge[3]; i <= edge[1]; ++i) { + if (img.get(edge[2], i)[0] == 255) + return false; + } + edge[2]--; + return true; + case 3: + if (edge[3] < 0) { + return false; + } + for (int i = edge[2]; i <= edge[0]; ++i) { + if (img.get(i, edge[3])[0] == 255) + return false; + } + edge[3]--; + return true; + default: + return false; + } + } + + + /** + * 清除白色区域的内部黑色孔洞 + * rowLimit != colsLimit, 使用长方形比正方形好 + * 该算法比较耗时 + * @param inMat + * @param rowLimit + * @param colsLimit + * @param debug + * @param tempPath + * @return + */ + public static Mat clearInnerHole(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + // 白色点较少,遍历白色点速度快 + if (inMat.get(i, j)[0] == white && label.get(i, j)[0] == uncheck) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + // 遍历四条边 + for (int k = x1; k < x2; k++) { + if(inMat.get(k, y1)[0] == black || inMat.get(k, y2)[0] == black) { + count++; + } + } + for (int k = y1; k < y2; k++) { + if(inMat.get(x1, k)[0] == black || inMat.get(x2, k)[0] == black) { + count++; + } + } + + // 根据中心点+limit,定位四个角生成一个矩形, + // 矩形四条边都是白色,内部的黑点标记为 要被替换的对象 + if(count == 0 ) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == black && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, white); + } + } + } + label.release(); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearInnerHole") + "_clearInnerHole.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearInnerHole执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + /** + * 清除二值图像的黑洞 + * 按矩形清理 + * @param inMat 二值图像 0代表黑色,255代表白色 + * @param rowLimit 像素值 + * @param colsLimit 像素值 + * @param debug + * @param tempPath + */ + public static Mat clearHole(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == white) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + if(inMat.get(x1, y1)[0] == white) {// 左上角 + count++; + } + if(inMat.get(x1, y2)[0] == white) { // 左下角 + count++; + } + if(inMat.get(x2, y1)[0] == white) { // 右上角 + count++; + } + if(inMat.get(x2, y2)[0] == white) { // 右下角 + count++; + } + + // 根据中心点+limit,定位四个角生成一个矩形, + // 将四个角都是白色的矩形,内部的黑点标记为 要被替换的对象 + if(count >=4 ) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == black && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, white); // 黑色替换成白色 + } + } + } + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearHole") + "_clearHole.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearHole执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + /** + * 清除二值图像的细小连接 + * 按水平或者垂直方向清除 + * @param inMat + * @param rowLimit + * @param colsLimit + * @param debug + * @param tempPath + * @return + */ + public static Mat clearSmallConnArea(Mat inMat, int rowLimit, int colsLimit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == black) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + // 执行两次,交换row 跟col; + int condition = 0; + do { + int x1 = i; + int x2 = i + rowLimit >= inMat.rows() ? inMat.rows() - 1 : i + rowLimit; + int y1 = j; + int y2 = j + colsLimit >= inMat.cols() ? inMat.cols() - 1 : j + colsLimit ; + + int count = 0; + if(inMat.get(x1, y1)[0] == black) {// 左上角 + count++; + } + if(inMat.get(x1, y2)[0] == black) { // 左下角 + count++; + } + if(inMat.get(x2, y1)[0] == black) { // 右上角 + count++; + } + if(inMat.get(x2, y2)[0] == black) { // 右下角 + count++; + } + + // 根据 中心点+limit,定位四个角生成一个矩形, + // 将四个角都是黑色的矩形,内部的白点标记为 要被替换的对象 + if(count >= 4) { + for (int n = x1; n < x2; n++) { + for (int m = y1; m < y2; m++) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + int ex = rowLimit; + rowLimit = colsLimit; + colsLimit = ex; + + condition++; + } while (condition == 1); + } + } + } + + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, black); // 白色替换成黑色 + } + } + } + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearSmallConnArea") + "_clearSmallConnArea.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearSmallConnArea执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + /** + * 清除二值图像的细小连接 + * 按45度斜边清除 + * @param inMat + * @param limit + * @param angle + * @param debug + * @param tempPath + * @return + */ + public static Mat clearAngleConn(Mat inMat, int limit, Boolean debug, String tempPath) { + Instant start = Instant.now(); + int uncheck = 0, normal = 2, replace = 3, white = 255, black = 0; + + Mat dst = new Mat(inMat.size(), CvType.CV_8UC1); + inMat.copyTo(dst); + + // 初始化的图像全部为0,未检查; 全黑图像 + Mat label = new Mat(inMat.size(), CvType.CV_8UC1); + + // 标记所有的白色区域 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if (inMat.get(i, j)[0] == black) { // 对于二值图,0代表黑色,255代表白色 + label.put(i, j, normal); // 中心点 + + int x1 = i; + int x2 = i + limit >= inMat.rows() ? inMat.rows() - 1 : i + limit; + int y1 = j; + int y2 = j + limit >= inMat.cols() ? inMat.cols() - 1 : j + limit ; + + // 根据 中心点+limit,定位四个角生成一个矩形, + // 将2个角都是黑色的线,内部的白点标记为 要被替换的对象 + // 【\】 斜对角线 + if(inMat.get(x1, y1)[0] == black && inMat.get(x2, y2)[0] == black) { + for (int n = x1, m = y1; n < x2 && m < y2; n++, m++) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + if(inMat.get(x1, y2)[0] == black && inMat.get(x2, y1)[0] == black) { + // 【/】 斜对角线 + for (int n = x1, m = y2; n < x2 && m > y1; n++, m--) { + if (inMat.get(n, m)[0] == white && label.get(n, m)[0] == uncheck) { + label.put(n, m, replace); + } + } + } + } + } + } + // 白色替换成黑色 + for (int i = 0; i < inMat.rows(); i++) { + for (int j = 0; j < inMat.cols(); j++) { + if(label.get(i, j)[0] == replace) { + dst.put(i, j, black); + } + } + } + + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("clearAngleConn") + "_clearAngleConn.jpg", dst); + Instant end = Instant.now(); + System.out.println("clearAngleConn执行耗时:" + Duration.between(start, end).toMillis()); + } + return dst; + } + + + +} diff --git a/util/PlateUtil.java b/util/PlateUtil.java new file mode 100644 index 00000000..8cd2704b --- /dev/null +++ b/util/PlateUtil.java @@ -0,0 +1,546 @@ +package com.yuxue.util; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.Vector; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.opencv.ml.ANN_MLP; +import org.opencv.ml.SVM; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.yuxue.constant.Constant; +import com.yuxue.enumtype.Direction; +import com.yuxue.enumtype.PlateColor; +import com.yuxue.train.SVMTrain; + + +/** + * 车牌处理工具类 + * 车牌切图按字符分割 + * 字符识别 + * 未完成 + * @author yuxue + * @date 2020-05-28 15:11 + */ +public class PlateUtil { + + // 车牌定位处理步骤,该map用于表示步骤图片的顺序 + private static Map debugMap = Maps.newLinkedHashMap(); + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + debugMap.put("platePredict", 0); + debugMap.put("colorMatch", 0); + debugMap.put("plateThreshold", 0); + debugMap.put("plateContours", 0); + debugMap.put("plateRect", 0); + debugMap.put("plateCrop", 0); + debugMap.put("char_clearLiuDing", 0); // 去除柳钉 + debugMap.put("specMat", 0); + debugMap.put("chineseMat", 0); + debugMap.put("char_auxRoi", 0); + + // 设置index, 用于debug生成文件时候按名称排序 + Integer index = 200; + for (Entry entry : debugMap.entrySet()) { + entry.setValue(index); + index ++; + } + + // 这个位置加载模型文件会报错,暂时没时间定位啥问题报错 + /*loadSvmModel("D:/PlateDetect/train/plate_detect_svm/svm2.xml"); + loadAnnModel("D:/PlateDetect/train/chars_recognise_ann/ann.xml");*/ + } + + private static SVM svm = SVM.create(); + + private static ANN_MLP ann=ANN_MLP.create(); + + public static void loadSvmModel(String path) { + svm.clear(); + svm=SVM.load(path); + } + + // 加载ann配置文件 图像转文字的训练库文件 + public static void loadAnnModel(String path) { + ann.clear(); + ann = ANN_MLP.load(path); + } + + + public static void main(String[] args) { + /*System.err.println(PalteUtil.isPlate("粤AI234K")); + System.err.println(PalteUtil.isPlate("鄂CD3098"));*/ + + } + + + /** + * 根据正则表达式判断字符串是否是车牌 + * @param str + * @return + */ + public static Boolean isPlate(String str) { + Pattern p = Pattern.compile(Constant.plateReg); + Boolean bl = false; + Matcher m = p.matcher(str); + while(m.find()) { + bl = true; + break; + } + return bl; + } + + + /** + * 输入车牌切图集合,判断是否包含车牌 + * @param inMat + * @param dst 包含车牌的图块 + */ + public static void hasPlate(Vector inMat, Vector dst, Boolean debug, String tempPath) { + int i = 0; + for (Mat src : inMat) { + if(src.rows() == Constant.DEFAULT_HEIGHT && src.cols() == Constant.DEFAULT_WIDTH) { + Mat samples = SVMTrain.getFeature(src); + float flag = svm.predict(samples); + if (flag == 0) { + dst.add(src); + if(debug) { + System.err.println("目标符合"); + Imgcodecs.imwrite(tempPath + debugMap.get("platePredict") + "_platePredict" + i + ".png", src); + } + i++; + } else { + System.out.println("目标不符合"); + } + } else { + System.err.println("非法图块"); + } + } + return; + } + + + /** + * 判断切图车牌颜色 + * @param inMat + * @return + */ + public static PlateColor getPlateColor(Mat inMat, Boolean adaptive_minsv, Boolean debug, String tempPath) { + // 判断阈值 + final float thresh = 0.70f; + if(colorMatch(inMat, PlateColor.GREEN, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.GREEN; + } + if(colorMatch(inMat, PlateColor.YELLOW, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.YELLOW; + } + if(colorMatch(inMat, PlateColor.BLUE, adaptive_minsv, debug, tempPath) > thresh) { + return PlateColor.BLUE; + } + return PlateColor.UNKNOWN; + } + + + /** + * 颜色匹配计算 + * @param inMat + * @param r + * @param adaptive_minsv + * @param debug + * @param tempPath + * @return + */ + public static Float colorMatch(Mat inMat, PlateColor r, Boolean adaptive_minsv, Boolean debug, String tempPath) { + final float max_sv = 255; + final float minref_sv = 64; + final float minabs_sv = 95; + + Mat hsvMat = ImageUtil.rgb2Hsv(inMat, debug, tempPath); + + // 匹配模板基色,切换以查找想要的基色 + int min_h = r.minH; + int max_h = r.maxH; + float diff_h = (float) ((max_h - min_h) / 2); + int avg_h = (int) (min_h + diff_h); + + for (int i = 0; i < hsvMat.rows(); ++i) { + for (int j = 0; j < hsvMat.cols(); j += 3) { + int H = (int)hsvMat.get(i, j)[0]; + int S = (int)hsvMat.get(i, j)[1]; + int V = (int)hsvMat.get(i, j)[2]; + + boolean colorMatched = false; + + if ( min_h < H && H <= max_h) { + int Hdiff = Math.abs(H - avg_h); + float Hdiff_p = Hdiff / diff_h; + float min_sv = 0; + if (adaptive_minsv) { + min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); + } else { + min_sv = minabs_sv; + } + if ((min_sv < S && S <= max_sv) && (min_sv < V && V <= max_sv)) { + colorMatched = true; + } + } + + if (colorMatched == true) { + hsvMat.put(i, j, 0, 0, 255); + } else { + hsvMat.put(i, j, 0, 0, 0); + } + } + } + + // 获取颜色匹配后的二值灰度图 + List hsvSplit = Lists.newArrayList(); + Core.split(hsvMat, hsvSplit); + Mat gray = hsvSplit.get(2); + + float percent = (float) Core.countNonZero(gray) / (gray.rows() * gray.cols()); + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("colorMatch") + "_colorMatch.jpg", gray); + } + return percent; + } + + + + /** + * 车牌切图,分割成单个字符切图 + * @param inMat 输入原始图像 + * @param charMat 返回字符切图vector + * @param debug + * @param tempPath + */ + public static final int DEFAULT_ANGLE = 30; // 角度判断所用常量 + public static void charsSegment(Mat inMat, PlateColor color, Vector charMat, Boolean debug, String tempPath) { + Mat gray = new Mat(); + Imgproc.cvtColor(inMat, gray, Imgproc.COLOR_BGR2GRAY); + + Mat threshold = new Mat(); + switch (color) { + case BLUE: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY); + break; + + case YELLOW: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY_INV); + break; + + case GREEN: + Imgproc.threshold(gray, threshold, 10, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY_INV); + break; + + default: + return; + } + + // 图片处理,降噪等 + if (debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("plateThreshold") + "_plateThreshold.jpg", threshold); + } + + // 获取轮廓 + Mat contour = new Mat(); + threshold.copyTo(contour); + + List contours = Lists.newArrayList(); + // 提取外部轮廓 + Imgproc.findContours(contour, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + if (debug) { + Mat result = new Mat(); + inMat.copyTo(result); + Imgproc.drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + Imgcodecs.imwrite(tempPath + debugMap.get("plateContours") + "_plateContours.jpg", result); + } + + + Vector rt = new Vector(); + for (int i = 0; i < contours.size(); i++) { + Rect mr = Imgproc.boundingRect(contours.get(i)); + /*if(debug) { + Mat mat = new Mat(threshold, mr); + Imgcodecs.imwrite(tempPath + debugMap.get("plateRect") + "_plateRect_" + i + ".jpg", mat); + }*/ + if (checkCharSizes(mr)) { + rt.add(mr); + } + } + if(null == rt || rt.size() <= 0) { + return; + } + Vector sorted = new Vector(); + sortRect(rt, sorted); + + String plate = ""; + Vector dst = new Vector(); + + for (int i = 0; i < sorted.size(); i++) { + Mat img_crop = new Mat(threshold, sorted.get(i)); + img_crop = preprocessChar(img_crop); + dst.add(img_crop); + if(debug) { + Imgcodecs.imwrite(tempPath + debugMap.get("plateCrop") + "_plateCrop_" + i + ".jpg", img_crop); + } + + Mat f = features(img_crop, Constant.predictSize); + + // 字符预测 + Mat output = new Mat(1, 140, CvType.CV_32F); + int index = (int) ann.predict(f, output, 0); + + if (index < Constant.numCharacter) { + plate += String.valueOf(Constant.strCharacters[index]); + } else { + String s = Constant.strChinese[index - Constant.numCharacter]; + plate += Constant.KEY_CHINESE_MAP.get(s); + } + } + System.err.println("===>" + plate); + + return; + } + + /** + * 字符预处理: 统一每个字符的大小 + * @param in + * @return + */ + final static int CHAR_SIZE = 20; + private static Mat preprocessChar(Mat in) { + int h = in.rows(); + int w = in.cols(); + Mat transformMat = Mat.eye(2, 3, CvType.CV_32F); + int m = Math.max(w, h); + transformMat.put(0, 2, (m - w) / 2f); + transformMat.put(1, 2, (m - h) / 2f); + + Mat warpImage = new Mat(m, m, in.type()); + Imgproc.warpAffine(in, warpImage, transformMat, warpImage.size(), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, new Scalar(0)); + + Mat resized = new Mat(CHAR_SIZE, CHAR_SIZE, CvType.CV_8UC3); + Imgproc.resize(warpImage, resized, resized.size(), 0, 0, Imgproc.INTER_CUBIC); + + return resized; + } + + + + /** + * 字符尺寸验证;去掉尺寸不符合的图块 + * 此处计算宽高比意义不大,因为字符 1 的宽高比干扰就已经很大了 + * @param r + * @return + */ + public static Boolean checkCharSizes(Rect r) { + float minHeight = 15f; + float maxHeight = 35f; + double charAspect = r.size().width / r.size().height; + return charAspect <1 && minHeight <= r.size().height && r.size().height < maxHeight; + } + + + + /** + * 将Rect按位置从左到右进行排序 + * @param vecRect + * @param out + * @return + */ + public static void sortRect(Vector vecRect, Vector out) { + Map map = Maps.newHashMap(); + for (int i = 0; i < vecRect.size(); ++i) { + map.put(vecRect.get(i).x, i); + } + Set set = map.keySet(); + Object[] arr = set.toArray(); + Arrays.sort(arr); + for (Object key : arr) { + out.add(vecRect.get(map.get(key))); + } + return; + } + + + + public static float[] projectedHistogram(final Mat img, Direction direction) { + int sz = 0; + switch (direction) { + case HORIZONTAL: + sz = img.rows(); + break; + + case VERTICAL: + sz = img.cols(); + break; + + default: + break; + } + + // 统计这一行或一列中,非零元素的个数,并保存到nonZeroMat中 + float[] nonZeroMat = new float[sz]; + Core.extractChannel(img, img, 0); + for (int j = 0; j < sz; j++) { + Mat data = (direction == Direction.HORIZONTAL) ? img.row(j) : img.col(j); + int count = Core.countNonZero(data); + nonZeroMat[j] = count; + } + // Normalize histogram + float max = 0; + for (int j = 0; j < nonZeroMat.length; ++j) { + max = Math.max(max, nonZeroMat[j]); + } + if (max > 0) { + for (int j = 0; j < nonZeroMat.length; ++j) { + nonZeroMat[j] /= max; + } + } + return nonZeroMat; + } + + + public static Mat features(Mat in, int sizeData) { + + float[] vhist = projectedHistogram(in, Direction.VERTICAL); + float[] hhist = projectedHistogram(in, Direction.HORIZONTAL); + + Mat lowData = new Mat(); + if (sizeData > 0) { + Imgproc.resize(in, lowData, new Size(sizeData, sizeData)); + } + + int numCols = vhist.length + hhist.length + lowData.cols() * lowData.rows(); + Mat out = new Mat(1, numCols, CvType.CV_32F); + + int j = 0; + for (int i = 0; i < vhist.length; ++i, ++j) { + out.put(0, j, vhist[i]); + } + for (int i = 0; i < hhist.length; ++i, ++j) { + out.put(0, j, hhist[i]); + } + + for (int x = 0; x < lowData.cols(); x++) { + for (int y = 0; y < lowData.rows(); y++, ++j) { + double[] val = lowData.get(x, y); + out.put(0, j, val[0]); + } + } + return out; + } + + + + + /** + * 进行膨胀操作 + * @param inMat + * @return + */ + public static Mat dilate(Mat inMat) { + Mat result = inMat.clone(); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2)); + Imgproc.dilate(inMat, result, element); + return result; + } + + /** + * 进行腐蚀操作 + * @param inMat + * @return + */ + public static Mat erode(Mat inMat) { + Mat result = inMat.clone(); + Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2)); + Imgproc.erode(inMat, result, element); + return result; + } + + + /** + * 随机数平移 + * @param inMat + * @return + */ + public static Mat randTranslate(Mat inMat) { + Random rand = new Random(); + Mat result = inMat.clone(); + int ran_x = rand.nextInt(10000) % 5 - 2; // 控制在-2~3个像素范围内 + int ran_y = rand.nextInt(10000) % 5 - 2; + return translateImg(result, ran_x, ran_y); + } + + + /** + * 随机数旋转 + * @param inMat + * @return + */ + public static Mat randRotate(Mat inMat) { + Random rand = new Random(); + Mat result = inMat.clone(); + float angle = (float) (rand.nextInt(10000) % 15 - 7); // 旋转角度控制在-7~8°范围内 + return rotateImg(result, angle); + } + + + /** + * 平移 + * @param img + * @param offsetx + * @param offsety + * @return + */ + public static Mat translateImg(Mat img, int offsetx, int offsety){ + Mat dst = new Mat(); + //定义平移矩阵 + Mat trans_mat = Mat.zeros(2, 3, CvType.CV_32FC1); + trans_mat.put(0, 0, 1); + trans_mat.put(0, 2, offsetx); + trans_mat.put(1, 1, 1); + trans_mat.put(1, 2, offsety); + Imgproc.warpAffine(img, dst, trans_mat, img.size()); // 仿射变换 + return dst; + } + + + /** + * 旋转角度 + * @param source + * @param angle + * @return + */ + public static Mat rotateImg(Mat source, float angle){ + Point src_center = new Point(source.cols() / 2.0F, source.rows() / 2.0F); + Mat rot_mat = Imgproc.getRotationMatrix2D(src_center, angle, 1); + Mat dst = new Mat(); + // 仿射变换 可以考虑使用投影变换; 这里使用放射变换进行旋转,对于实际效果来说感觉意义不大,反而会干扰结果预测 + Imgproc.warpAffine(source, dst, rot_mat, source.size()); + return dst; + } + + + +}