From 2d0ba56295264b3f6d79f604f71e5dbc34f04db1 Mon Sep 17 00:00:00 2001 From: yuxue Date: Thu, 2 Jul 2020 17:09:37 +0800 Subject: [PATCH] no commit message --- .../{easypr => easypr-c++}/ann_train.cpp | 0 .../core => easypr-c++}/chars_identify.cpp | 0 .../core => easypr-c++}/chars_recognise.cpp | 0 .../core => easypr-c++}/chars_segment.cpp | 0 .../{easypr/core => easypr-c++}/core_func.cpp | 0 .../{easypr/core => easypr-c++}/feature.cpp | 0 .../{easypr/core => easypr-c++}/params.cpp | 0 .../core => easypr-c++}/plate_detect.cpp | 0 .../core => easypr-c++}/plate_judge.cpp | 0 .../core => easypr-c++}/plate_locate.cpp | 0 .../core => easypr-c++}/plate_recognize.cpp | 0 .../{easypr => easypr-c++}/province_mapping | 0 .../{easypr => easypr-c++}/svm_train.cpp | 0 src/main/resources/easypr-java/ANNTrain1.java | 159 ++++++ .../resources/easypr-java/CharsIdentify.java | 83 ++++ .../resources/easypr-java/CharsRecognise.java | 136 ++++++ .../resources/easypr-java/CharsSegment.java | 453 ++++++++++++++++++ src/main/resources/easypr-java/CoreFunc.java | 269 +++++++++++ src/main/resources/easypr-java/Features.java | 90 ++++ .../resources/easypr-java/PlateDetect.java | 111 +++++ .../resources/easypr-java/PlateJudge.java | 107 +++++ .../resources/easypr-java/PlateLocate.java | 354 ++++++++++++++ .../resources/easypr-java/SVMCallback.java | 44 ++ src/main/resources/easypr-java/SVMTrain1.java | 382 +++++++++++++++ 24 files changed, 2188 insertions(+) rename src/main/resources/{easypr => easypr-c++}/ann_train.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/chars_identify.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/chars_recognise.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/chars_segment.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/core_func.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/feature.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/params.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/plate_detect.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/plate_judge.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/plate_locate.cpp (100%) rename src/main/resources/{easypr/core => easypr-c++}/plate_recognize.cpp (100%) rename src/main/resources/{easypr => easypr-c++}/province_mapping (100%) rename src/main/resources/{easypr => easypr-c++}/svm_train.cpp (100%) create mode 100644 src/main/resources/easypr-java/ANNTrain1.java create mode 100644 src/main/resources/easypr-java/CharsIdentify.java create mode 100644 src/main/resources/easypr-java/CharsRecognise.java create mode 100644 src/main/resources/easypr-java/CharsSegment.java create mode 100644 src/main/resources/easypr-java/CoreFunc.java create mode 100644 src/main/resources/easypr-java/Features.java create mode 100644 src/main/resources/easypr-java/PlateDetect.java create mode 100644 src/main/resources/easypr-java/PlateJudge.java create mode 100644 src/main/resources/easypr-java/PlateLocate.java create mode 100644 src/main/resources/easypr-java/SVMCallback.java create mode 100644 src/main/resources/easypr-java/SVMTrain1.java diff --git a/src/main/resources/easypr/ann_train.cpp b/src/main/resources/easypr-c++/ann_train.cpp similarity index 100% rename from src/main/resources/easypr/ann_train.cpp rename to src/main/resources/easypr-c++/ann_train.cpp diff --git a/src/main/resources/easypr/core/chars_identify.cpp b/src/main/resources/easypr-c++/chars_identify.cpp similarity index 100% rename from src/main/resources/easypr/core/chars_identify.cpp rename to src/main/resources/easypr-c++/chars_identify.cpp diff --git a/src/main/resources/easypr/core/chars_recognise.cpp b/src/main/resources/easypr-c++/chars_recognise.cpp similarity index 100% rename from src/main/resources/easypr/core/chars_recognise.cpp rename to src/main/resources/easypr-c++/chars_recognise.cpp diff --git a/src/main/resources/easypr/core/chars_segment.cpp b/src/main/resources/easypr-c++/chars_segment.cpp similarity index 100% rename from src/main/resources/easypr/core/chars_segment.cpp rename to src/main/resources/easypr-c++/chars_segment.cpp diff --git a/src/main/resources/easypr/core/core_func.cpp b/src/main/resources/easypr-c++/core_func.cpp similarity index 100% rename from src/main/resources/easypr/core/core_func.cpp rename to src/main/resources/easypr-c++/core_func.cpp diff --git a/src/main/resources/easypr/core/feature.cpp b/src/main/resources/easypr-c++/feature.cpp similarity index 100% rename from src/main/resources/easypr/core/feature.cpp rename to src/main/resources/easypr-c++/feature.cpp diff --git a/src/main/resources/easypr/core/params.cpp b/src/main/resources/easypr-c++/params.cpp similarity index 100% rename from src/main/resources/easypr/core/params.cpp rename to src/main/resources/easypr-c++/params.cpp diff --git a/src/main/resources/easypr/core/plate_detect.cpp b/src/main/resources/easypr-c++/plate_detect.cpp similarity index 100% rename from src/main/resources/easypr/core/plate_detect.cpp rename to src/main/resources/easypr-c++/plate_detect.cpp diff --git a/src/main/resources/easypr/core/plate_judge.cpp b/src/main/resources/easypr-c++/plate_judge.cpp similarity index 100% rename from src/main/resources/easypr/core/plate_judge.cpp rename to src/main/resources/easypr-c++/plate_judge.cpp diff --git a/src/main/resources/easypr/core/plate_locate.cpp b/src/main/resources/easypr-c++/plate_locate.cpp similarity index 100% rename from src/main/resources/easypr/core/plate_locate.cpp rename to src/main/resources/easypr-c++/plate_locate.cpp diff --git a/src/main/resources/easypr/core/plate_recognize.cpp b/src/main/resources/easypr-c++/plate_recognize.cpp similarity index 100% rename from src/main/resources/easypr/core/plate_recognize.cpp rename to src/main/resources/easypr-c++/plate_recognize.cpp diff --git a/src/main/resources/easypr/province_mapping b/src/main/resources/easypr-c++/province_mapping similarity index 100% rename from src/main/resources/easypr/province_mapping rename to src/main/resources/easypr-c++/province_mapping diff --git a/src/main/resources/easypr/svm_train.cpp b/src/main/resources/easypr-c++/svm_train.cpp similarity index 100% rename from src/main/resources/easypr/svm_train.cpp rename to src/main/resources/easypr-c++/svm_train.cpp diff --git a/src/main/resources/easypr-java/ANNTrain1.java b/src/main/resources/easypr-java/ANNTrain1.java new file mode 100644 index 00000000..a177ac24 --- /dev/null +++ b/src/main/resources/easypr-java/ANNTrain1.java @@ -0,0 +1,159 @@ +package com.yuxue.train; + +import java.util.Vector; + +import static org.bytedeco.javacpp.opencv_core.*; +import static org.bytedeco.javacpp.opencv_ml.*; + +import org.bytedeco.javacpp.opencv_imgcodecs; +import org.bytedeco.javacpp.opencv_core.Mat; + +import com.yuxue.constant.Constant; +import com.yuxue.easypr.core.CoreFunc; +import com.yuxue.util.FileUtil; + +/** + * 基于org.bytedeco.javacpp包实现的训练 + * + * 图片文字识别训练 + * 训练出来的库文件,用于识别图片中的文字 + * + * 训练的ann.xml应用: + * 1、替换res/model/ann.xml文件 + * 2、修改com.yuxue.easypr.core.CharsIdentify.charsIdentify(Mat, Boolean, Boolean)方法 + * + * @author yuxue + * @date 2020-05-14 22:16 + */ +public class ANNTrain1 { + + private ANN_MLP ann = ANN_MLP.create(); + + // 默认的训练操作的根目录 + private static final String DEFAULT_PATH = "D:/PlateDetect/train/chars_recognise_ann/"; + + // 训练模型文件保存位置 + private static final String MODEL_PATH = "res/model/ann.xml"; + + + public void train(int _predictsize, int _neurons) { + Mat samples = new Mat(); // 使用push_back,行数列数不能赋初始值 + Vector trainingLabels = new Vector(); + // 加载数字及字母字符 + for (int i = 0; i < Constant.numCharacter; i++) { + String str = DEFAULT_PATH + "learn/" + Constant.strCharacters[i]; + Vector files = new Vector(); + FileUtil.getFiles(str, files); + + int size = (int) files.size(); + for (int j = 0; j < size; j++) { + Mat img = opencv_imgcodecs.imread(files.get(j), 0); + // System.err.println(files.get(j)); // 文件名不能包含中文 + Mat f = CoreFunc.features(img, _predictsize); + samples.push_back(f); + trainingLabels.add(i); // 每一幅字符图片所对应的字符类别索引下标 + } + } + + // 加载汉字字符 + for (int i = 0; i < Constant.strChinese.length; i++) { + String str = DEFAULT_PATH + "learn/" + Constant.strChinese[i]; + Vector files = new Vector(); + FileUtil.getFiles(str, files); + + int size = (int) files.size(); + for (int j = 0; j < size; j++) { + Mat img = opencv_imgcodecs.imread(files.get(j), 0); + // System.err.println(files.get(j)); // 文件名不能包含中文 + Mat f = CoreFunc.features(img, _predictsize); + samples.push_back(f); + trainingLabels.add(i + Constant.numCharacter); + } + } + + + //440 vhist.length + hhist.length + lowData.cols() * lowData.rows(); + // CV_32FC1 CV_32SC1 CV_32F + Mat classes = new Mat(trainingLabels.size(), Constant.numAll, CV_32F); + + float[] labels = new float[trainingLabels.size()]; + for (int i = 0; i < labels.length; ++i) { + classes.ptr(i, trainingLabels.get(i)).putFloat(1.f); + + } + + // samples.type() == CV_32F || samples.type() == CV_32S + TrainData train_data = TrainData.create(samples, ROW_SAMPLE, classes); + + ann.clear(); + Mat layers = new Mat(1, 3, CV_32SC1); + layers.ptr(0, 0).putInt(samples.cols()); + layers.ptr(0, 1).putInt(_neurons); + layers.ptr(0, 2).putInt(classes.cols()); + + System.out.println(layers); + + ann.setLayerSizes(layers); + ann.setActivationFunction(ANN_MLP.SIGMOID_SYM, 1, 1); + ann.setTrainMethod(ANN_MLP.BACKPROP); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 30000, 0.0001); + ann.setTermCriteria(criteria); + ann.setBackpropWeightScale(0.1); + ann.setBackpropMomentumScale(0.1); + ann.train(train_data); + + //FileStorage fsto = new FileStorage(MODEL_PATH, FileStorage.WRITE); + //ann.write(fsto, "ann"); + ann.save(MODEL_PATH); + } + + + public void predict() { + ann.clear(); + ann = ANN_MLP.load(MODEL_PATH); + //ann = ANN_MLP.loadANN_MLP(MODEL_PATH, "ann"); + Vector files = new Vector(); + FileUtil.getFiles(DEFAULT_PATH + "test/", files); + + for (String string : files) { + Mat img = opencv_imgcodecs.imread(string); + Mat f = CoreFunc.features(img, Constant.predictSize); + + // 140 predictSize = 10; vhist.length + hhist.length + lowData.cols() * lowData.rows(); + // 440 predictSize = 20; + Mat output = new Mat(1, 140, CV_32F); + //ann.predict(f, output, 0); // 预测结果 + // System.err.println(string + "===>" + (int) ann.predict(f, output, 0)); + + int index = (int) ann.predict(f, output, 0); + + String result = ""; + if (index < Constant.numCharacter) { + result = String.valueOf(Constant.strCharacters[index]); + } else { + String s = Constant.strChinese[index - Constant.numCharacter]; + result = Constant.KEY_CHINESE_MAP.get(s); // 编码转中文 + } + System.err.println(string + "===>" + result); + + // ann.predict(f, output, 0); + // System.err.println(string + "===>" + output.get(0, 0)[0]); + + } + } + + public static void main(String[] args) { + + ANNTrain1 annT = new ANNTrain1(); + // 这里演示只训练model文件夹下的ann.xml,此模型是一个predictSize=10,neurons=40的ANN模型 + // 可根据需要训练不同的predictSize或者neurons的ANN模型 + // 根据机器的不同,训练时间不一样,但一般需要10分钟左右,所以慢慢等一会吧。 + annT.train(Constant.predictSize, Constant.neurons); + + annT.predict(); + + System.out.println("The end."); + } + + +} \ No newline at end of file diff --git a/src/main/resources/easypr-java/CharsIdentify.java b/src/main/resources/easypr-java/CharsIdentify.java new file mode 100644 index 00000000..6daf5b84 --- /dev/null +++ b/src/main/resources/easypr-java/CharsIdentify.java @@ -0,0 +1,83 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_ml.ANN_MLP; + +import com.yuxue.constant.Constant; +import com.yuxue.util.Convert; + + +/** + * 字符检测 + * @author yuxue + * @date 2020-04-24 15:31 + */ +public class CharsIdentify { + + private ANN_MLP ann=ANN_MLP.create(); + + public CharsIdentify() { + loadModel(Constant.DEFAULT_ANN_PATH); + } + + public void loadModel(String path) { + this.ann.clear(); + // 加载ann配置文件 图像转文字的训练库文件 + //ann=ANN_MLP.loadANN_MLP(path, "ann"); + ann = ANN_MLP.load(path); + } + + + /** + * @param input + * @param isChinese + * @return + */ + public String charsIdentify(final Mat input, final Boolean isChinese, final Boolean isSpeci) { + String result = ""; + + /*String name = "D:/PlateDetect/train/chars_recognise_ann/" + System.currentTimeMillis() + ".jpg"; + opencv_imgcodecs.imwrite(name, input); + Mat img = opencv_imgcodecs.imread(name); + Mat f = CoreFunc.features(img, Constant.predictSize);*/ + + Mat f = CoreFunc.features(input, Constant.predictSize); + + int index = this.classify(f, isChinese, isSpeci); + + System.err.print(index); + if (index < Constant.numCharacter) { + result = String.valueOf(Constant.strCharacters[index]); + } else { + String s = Constant.strChinese[index - Constant.numCharacter]; + result = Constant.KEY_CHINESE_MAP.get(s); // 编码转中文 + } + System.err.println(result); + return result; + } + + private int classify(final Mat f, final Boolean isChinses, final Boolean isSpeci) { + int result = -1; + + Mat output = new Mat(1, 140, opencv_core.CV_32F); + + ann.predict(f, output, 0); // 预测结果 + + int ann_min = (!isChinses) ? ((isSpeci) ? 10 : 0) : Constant.numCharacter; + int ann_max = (!isChinses) ? Constant.numCharacter : Constant.numAll; + + float maxVal = -2; + + for (int j = ann_min; j < ann_max; j++) { + float val = Convert.toFloat(output.ptr(0, j)); + if (val > maxVal) { + maxVal = val; + result = j; + } + } + return result; + } + + +} diff --git a/src/main/resources/easypr-java/CharsRecognise.java b/src/main/resources/easypr-java/CharsRecognise.java new file mode 100644 index 00000000..ebed2e0c --- /dev/null +++ b/src/main/resources/easypr-java/CharsRecognise.java @@ -0,0 +1,136 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; + +import com.yuxue.enumtype.PlateColor; + +/** + * 字符识别 + * + * @author yuxue + * @date 2020-04-24 15:31 + */ +public class CharsRecognise { + + private CharsSegment charsSegment = new CharsSegment(); + + private CharsIdentify charsIdentify = new CharsIdentify(); + + + public void loadANN(final String s) { + charsIdentify.loadModel(s); + } + + /** + * Chars segment and identify 字符分割与识别 + * + * @param plate: the input plate + * @return the result of plate recognition + */ + public String charsRecognise(final Mat plate, String tempPath) { + + // 车牌字符方块集合 + Vector matVec = new Vector(); + // 车牌识别结果 + String plateIdentify = ""; + + int result = charsSegment.charsSegment(plate, matVec, tempPath); + if (0 == result) { + for (int j = 0; j < matVec.size(); j++) { + Mat charMat = matVec.get(j); + // 默认首个字符块是中文字符 第二个字符块是字母 + String charcater = charsIdentify.charsIdentify(charMat, (0 == j), (1 == j)); + plateIdentify = plateIdentify + charcater; + } + } + + return plateIdentify; + } + + /** + * 是否开启调试模式 + * + * @param isDebug + */ + public void setCRDebug(final boolean isDebug) { + charsSegment.setDebug(isDebug); + } + + /** + * 获取调试模式状态 + * + * @return + */ + public boolean getCRDebug() { + return charsSegment.getDebug(); + } + + /** + * 获得车牌颜色 + * + * @param input + * @return + */ + public final String getPlateType(final Mat input) { + PlateColor result = CoreFunc.getPlateType(input, true); + return result.desc; + } + + /** + * 设置柳丁大小变量 + * + * @param param + */ + public void setLiuDingSize(final int param) { + charsSegment.setLiuDingSize(param); + } + + /** + * 设置颜色阈值 + * + * @param param + */ + public void setColorThreshold(final int param) { + charsSegment.setColorThreshold(param); + } + + /** + * 设置蓝色百分比 + * + * @param param + */ + public void setBluePercent(final float param) { + charsSegment.setBluePercent(param); + } + + /** + * 得到蓝色百分比 + * + * @param param + */ + public final float getBluePercent() { + return charsSegment.getBluePercent(); + } + + /** + * 设置白色百分比 + * + * @param param + */ + public void setWhitePercent(final float param) { + charsSegment.setWhitePercent(param); + } + + /** + * 得到白色百分比 + * + * @param param + */ + public final float getWhitePercent() { + return charsSegment.getWhitePercent(); + } + + +} diff --git a/src/main/resources/easypr-java/CharsSegment.java b/src/main/resources/easypr-java/CharsSegment.java new file mode 100644 index 00000000..340e8f0e --- /dev/null +++ b/src/main/resources/easypr-java/CharsSegment.java @@ -0,0 +1,453 @@ +package com.yuxue.easypr.core; + +import static com.yuxue.easypr.core.CoreFunc.getPlateType; +import static org.bytedeco.javacpp.opencv_core.CV_32F; +import static org.bytedeco.javacpp.opencv_core.countNonZero; +import static org.bytedeco.javacpp.opencv_imgproc.CV_CHAIN_APPROX_NONE; +import static org.bytedeco.javacpp.opencv_imgproc.CV_RETR_EXTERNAL; +import static org.bytedeco.javacpp.opencv_imgproc.CV_RGB2GRAY; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_BINARY; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_BINARY_INV; +import static org.bytedeco.javacpp.opencv_imgproc.CV_THRESH_OTSU; +import static org.bytedeco.javacpp.opencv_imgproc.INTER_LINEAR; +import static org.bytedeco.javacpp.opencv_imgproc.boundingRect; +import static org.bytedeco.javacpp.opencv_imgproc.cvtColor; +import static org.bytedeco.javacpp.opencv_imgproc.findContours; +import static org.bytedeco.javacpp.opencv_imgproc.resize; +import static org.bytedeco.javacpp.opencv_imgproc.threshold; +import static org.bytedeco.javacpp.opencv_imgproc.warpAffine; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Rect; +import org.bytedeco.javacpp.opencv_core.Scalar; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_imgcodecs; + +import com.yuxue.enumtype.PlateColor; +import com.yuxue.util.Convert; + +/** + * 字符分割 + * @author yuxue + * @date 2020-04-28 09:45 + */ +public class CharsSegment { + + // preprocessChar所用常量 + final static int CHAR_SIZE = 20; + final static int HORIZONTAL = 1; + final static int VERTICAL = 0; + + final static int DEFAULT_LIUDING_SIZE = 7; + final static int DEFAULT_MAT_WIDTH = 136; + + final static int DEFAULT_COLORTHRESHOLD = 150; + final static float DEFAULT_BLUEPERCEMT = 0.3f; + final static float DEFAULT_WHITEPERCEMT = 0.1f; + + private int liuDingSize = DEFAULT_LIUDING_SIZE; + private int theMatWidth = DEFAULT_MAT_WIDTH; + + private int colorThreshold = DEFAULT_COLORTHRESHOLD; + + private float bluePercent = DEFAULT_BLUEPERCEMT; + private float whitePercent = DEFAULT_WHITEPERCEMT; + + private boolean isDebug = true; + + + /** + * 字符分割 + * + * @param input + * @param resultVec + * @return
    + *
  • more than zero: the number of chars; + *
  • -3: null; + *
+ */ + public int charsSegment(final Mat input, Vector resultVec, String tempPath) { + if (input.data().isNull()) { + return -3; + } + + // 判断车牌颜色以此确认threshold方法 + Mat img_threshold = new Mat(); + + Mat input_grey = new Mat(); + cvtColor(input, input_grey, CV_RGB2GRAY); + + int w = input.cols(); + int h = input.rows(); + Mat tmpMat = new Mat(input, new Rect((int) (w * 0.1), (int) (h * 0.1), (int) (w * 0.8), (int) (h * 0.8))); + + PlateColor color= getPlateType(tmpMat, true); + switch (color) { + case BLUE: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); + break; + + case YELLOW: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); + break; + + case GREEN: + threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); + break; + + default: + return -3; + } + + if (this.isDebug) { + opencv_imgcodecs.imwrite(tempPath + "debug_char_threshold.jpg", img_threshold); + } + + // 去除车牌上方的柳钉以及下方的横线等干扰 //会导致虚拟机崩溃 + // clearLiuDing(img_threshold); + /*if (this.isDebug) { + String str = tempPath + "debug_char_clearLiuDing.jpg"; + opencv_imgcodecs.imwrite(str, img_threshold); + }*/ + + // 找轮廓 + Mat img_contours = new Mat(); + img_threshold.copyTo(img_contours); + + MatVector contours = new MatVector(); + + findContours(img_contours, contours, // a vector of contours + CV_RETR_EXTERNAL, // retrieve the external contours + CV_CHAIN_APPROX_NONE); // all pixels of each contours + + // Remove patch that are no inside limits of aspect ratio and area. + // 将不符合特定尺寸的图块排除出去 + Vector vecRect = new Vector(); + for (int i = 0; i < contours.size(); ++i) { + Rect mr = boundingRect(contours.get(i)); + Mat contour = new Mat(img_threshold, mr); + + if (this.isDebug) { + String str = tempPath + "debug_char_contour"+i+".jpg"; + opencv_imgcodecs.imwrite(str, contour); + } + + if (verifySizes(contour)) { // 将不符合特定尺寸的图块排除出去 + vecRect.add(mr); + } + } + if (vecRect.size() == 0) { + return -3; + } + + Vector sortedRect = new Vector(); + // 对符合尺寸的图块按照从左到右进行排序 + SortRect(vecRect, sortedRect); + + // 获得指示城市的特定Rect,如苏A的"A" + int specIndex = GetSpecificRect(sortedRect, color); + + if (this.isDebug) { + if (specIndex < sortedRect.size()) { + Mat specMat = new Mat(img_threshold, sortedRect.get(specIndex)); + String str = tempPath + "debug_specMat.jpg"; + opencv_imgcodecs.imwrite(str, specMat); + } + } + + // 根据特定Rect向左反推出中文字符 + // 这样做的主要原因是根据findContours方法很难捕捉到中文字符的准确Rect,因此仅能 + // 通过特定算法来指定 + Rect chineseRect = new Rect(); + if (specIndex < sortedRect.size()) { + chineseRect = GetChineseRect(sortedRect.get(specIndex)); + } else { + return -3; + } + + if (this.isDebug) { + Mat chineseMat = new Mat(img_threshold, chineseRect); + String str = tempPath + "debug_chineseMat.jpg"; + opencv_imgcodecs.imwrite(str, chineseMat); + } + + // 新建一个全新的排序Rect + // 将中文字符Rect第一个加进来,因为它肯定是最左边的 + // 其余的Rect只按照顺序去6个,车牌只可能是7个字符!这样可以避免阴影导致的“1”字符 + Vector newSortedRect = new Vector(); + newSortedRect.add(chineseRect); + RebuildRect(sortedRect, newSortedRect, specIndex, color); + + if (newSortedRect.size() == 0) { + return -3; + } + + for (int i = 0; i < newSortedRect.size(); i++) { + Rect mr = newSortedRect.get(i); + Mat auxRoi = new Mat(img_threshold, mr); + + auxRoi = preprocessChar(auxRoi); + if (this.isDebug) { + String str = tempPath + "debug_char_auxRoi_" + Integer.valueOf(i).toString() + ".jpg"; + opencv_imgcodecs.imwrite(str, auxRoi); + } + resultVec.add(auxRoi); + } + return 0; + } + + /** + * 字符尺寸验证;去掉尺寸不符合的图块 + * @param r + * @return + */ + public static Boolean verifySizes(Mat r) { + float aspect = 45.0f / 90.0f; + float charAspect = (float) r.cols() / (float) r.rows(); + float error = 0.7f; + float minHeight = 10f; + float maxHeight = 35f; + // We have a different aspect ratio for number 1, and it can be ~0.2 + float minAspect = 0.05f; + float maxAspect = aspect + aspect * error; + // area of pixels + float area = countNonZero(r); + // bb area + float bbArea = r.cols() * r.rows(); + // % of pixel in area + float percPixels = area / bbArea; + + return percPixels <= 1 && charAspect > minAspect && charAspect < maxAspect && r.rows() >= minHeight && r.rows() < maxHeight; + } + + /** + * 字符预处理: 统一每个字符的大小 + * + * @param in + * @return + */ + private Mat preprocessChar(Mat in) { + int h = in.rows(); + int w = in.cols(); + int charSize = CHAR_SIZE; + Mat transformMat = Mat.eye(2, 3, CV_32F).asMat(); + int m = Math.max(w, h); + transformMat.ptr(0, 2).put(Convert.getBytes(((m - w) / 2f))); + transformMat.ptr(1, 2).put(Convert.getBytes((m - h) / 2f)); + + Mat warpImage = new Mat(m, m, in.type()); + warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, opencv_core.BORDER_CONSTANT, new Scalar(0)); + + Mat out = new Mat(); + resize(warpImage, out, new Size(charSize, charSize)); + + return out; + } + + /** + * 去除车牌上方的钮钉 + *

+ * 计算每行元素的阶跃数,如果小于X认为是柳丁,将此行全部填0(涂黑), X可根据实际调整 + * + * @param img + * @return + */ + private Mat clearLiuDing(Mat img) { + final int x = this.liuDingSize; + + Mat jump = Mat.zeros(1, img.rows(), CV_32F).asMat(); + for (int i = 0; i < img.rows(); i++) { + int jumpCount = 0; + for (int j = 0; j < img.cols() - 1; j++) { + if (img.ptr(i, j).get() != img.ptr(i, j + 1).get()) + jumpCount++; + } + jump.ptr(i).put(Convert.getBytes((float) jumpCount)); + } + for (int i = 0; i < img.rows(); i++) { + if (Convert.toFloat(jump.ptr(i)) <= x) { + for (int j = 0; j < img.cols(); j++) { + img.ptr(i, j).put((byte) 0); + } + } + } + return img; + } + + /** + * 根据特殊车牌来构造猜测中文字符的位置和大小 + * + * @param rectSpe + * @return + */ + private Rect GetChineseRect(final Rect rectSpe) { + int height = rectSpe.height(); + float newwidth = rectSpe.width() * 1.15f; + int x = rectSpe.x(); + int y = rectSpe.y(); + + int newx = x - (int) (newwidth * 1.15); + newx = Math.max(newx, 0); + Rect a = new Rect(newx, y, (int) newwidth, height); + return a; + } + + /** + * 找出指示城市的字符的Rect,例如苏A7003X,就是A的位置 + * 之所以选择城市的字符位置,是因为该位置不管什么字母,占用的宽度跟高度的差不多,能大大提高位置的准确性 + * @param vecRect + * @return + */ + private int GetSpecificRect(final Vector vecRect, PlateColor color) { + Vector xpositions = new Vector(); + int maxHeight = 0; + int maxWidth = 0; + for (int i = 0; i < vecRect.size(); i++) { + xpositions.add(vecRect.get(i).x()); + + if (vecRect.get(i).height() > maxHeight) { + maxHeight = vecRect.get(i).height(); + } + if (vecRect.get(i).width() > maxWidth) { + maxWidth = vecRect.get(i).width(); + } + } + + int specIndex = 0; + for (int i = 0; i < vecRect.size(); i++) { + Rect mr = vecRect.get(i); + int midx = mr.x() + mr.width() / 2; + + if(PlateColor.GREEN.equals(color)) { + if ((mr.width() > maxWidth * 0.8 || mr.height() > maxHeight * 0.8) + && (midx < this.theMatWidth * 2 / 8 && midx > this.theMatWidth / 8)) { + specIndex = i; + } + } else { + // 如果一个字符有一定的大小,并且在整个车牌的1/7到2/7之间,则是我们要找的特殊车牌 + if ((mr.width() > maxWidth * 0.8 || mr.height() > maxHeight * 0.8) + && (midx < this.theMatWidth * 2 / 7 && midx > this.theMatWidth / 7)) { + specIndex = i; + } + } + } + + return specIndex; + } + + /** + * 这个函数做两个事情 + *

    + *
  • 把特殊字符Rect左边的全部Rect去掉,后面再重建中文字符的位置; + *
  • 从特殊字符Rect开始,依次选择6个Rect,多余的舍去。 + *
      + * + * @param vecRect + * @param outRect + * @param specIndex + * @return + */ + private int RebuildRect(final Vector vecRect, Vector outRect, int specIndex, PlateColor color) { + // 最大只能有7个Rect,减去中文的就只有6个Rect + int count = 6; + if(PlateColor.GREEN.equals(color)) { + count = 7; // 绿牌要多一个 + } + for (int i = 0; i < vecRect.size(); i++) { + // 将特殊字符左边的Rect去掉,这个可能会去掉中文Rect,不过没关系,我们后面会重建。 + if (i < specIndex) + continue; + + outRect.add(vecRect.get(i)); + if (--count == 0) + break; + } + + return 0; + } + + /** + * 将Rect按位置从左到右进行排序 + * + * @param vecRect + * @param out + * @return + */ + public static void SortRect(final Vector vecRect, Vector out) { + Vector orderIndex = new Vector(); + Vector xpositions = new Vector(); + for (int i = 0; i < vecRect.size(); ++i) { + orderIndex.add(i); + xpositions.add(vecRect.get(i).x()); + } + + float min = xpositions.get(0); + int minIdx; + for (int i = 0; i < xpositions.size(); ++i) { + min = xpositions.get(i); + minIdx = i; + for (int j = i; j < xpositions.size(); ++j) { + if (xpositions.get(j) < min) { + min = xpositions.get(j); + minIdx = j; + } + } + int aux_i = orderIndex.get(i); + int aux_min = orderIndex.get(minIdx); + orderIndex.remove(i); + orderIndex.insertElementAt(aux_min, i); + orderIndex.remove(minIdx); + orderIndex.insertElementAt(aux_i, minIdx); + + float aux_xi = xpositions.get(i); + float aux_xmin = xpositions.get(minIdx); + xpositions.remove(i); + xpositions.insertElementAt((int) aux_xmin, i); + xpositions.remove(minIdx); + xpositions.insertElementAt((int) aux_xi, minIdx); + } + + for (int i = 0; i < orderIndex.size(); i++) + out.add(vecRect.get(orderIndex.get(i))); + + return; + } + + public void setLiuDingSize(int param) { + this.liuDingSize = param; + } + + public void setColorThreshold(int param) { + this.colorThreshold = param; + } + + public void setBluePercent(float param) { + this.bluePercent = param; + } + + public final float getBluePercent() { + return this.bluePercent; + } + + public void setWhitePercent(float param) { + this.whitePercent = param; + } + + public final float getWhitePercent() { + return this.whitePercent; + } + + public boolean getDebug() { + return this.isDebug; + } + + public void setDebug(boolean isDebug) { + this.isDebug = isDebug; + } + + +} diff --git a/src/main/resources/easypr-java/CoreFunc.java b/src/main/resources/easypr-java/CoreFunc.java new file mode 100644 index 00000000..e0f8c226 --- /dev/null +++ b/src/main/resources/easypr-java/CoreFunc.java @@ -0,0 +1,269 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_highgui; +import org.bytedeco.javacpp.opencv_imgproc; +import org.bytedeco.javacpp.indexer.FloatIndexer; + +import com.yuxue.enumtype.Direction; +import com.yuxue.enumtype.PlateColor; + + +/** + * + * @author yuxue + * @date 2020-05-16 21:09 + */ +public class CoreFunc { + + /** + * 根据一幅图像与颜色模板获取对应的二值图 + * + * @param src + * 输入RGB图像 + * @param r + * 颜色模板(蓝色、黄色) + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return 输出灰度图(只有0和255两个值,255代表匹配,0代表不匹配) + */ + public static Mat colorMatch(final Mat src, final PlateColor r, final boolean adaptive_minsv) { + final float max_sv = 255; + final float minref_sv = 64; + final float minabs_sv = 95; + + // 转到HSV空间进行处理,颜色搜索主要使用的是H分量进行蓝色与黄色的匹配工作 + Mat src_hsv = new Mat(); + opencv_imgproc.cvtColor(src, src_hsv, opencv_imgproc.CV_BGR2HSV); + MatVector hsvSplit = new MatVector(); + opencv_core.split(src_hsv, hsvSplit); + opencv_imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + opencv_core.merge(hsvSplit, src_hsv); + + // 匹配模板基色,切换以查找想要的基色 + 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); + + int channels = src_hsv.channels(); + int nRows = src_hsv.rows(); + // 图像数据列需要考虑通道数的影响; + int nCols = src_hsv.cols() * channels; + + // 连续存储的数据,按一行处理 + if (src_hsv.isContinuous()) { + nCols *= nRows; + nRows = 1; + } + + for (int i = 0; i < nRows; ++i) { + BytePointer p = src_hsv.ptr(i); + for (int j = 0; j < nCols; j += 3) { + int H = p.get(j) & 0xFF; + int S = p.get(j + 1) & 0xFF; + int V = p.get(j + 2) & 0xFF; + + boolean colorMatched = false; + + if (H > min_h && H < max_h) { + int Hdiff = 0; + if (H > avg_h) + Hdiff = H - avg_h; + else + Hdiff = avg_h - H; + + float Hdiff_p = Hdiff / diff_h; + + float min_sv = 0; + if (true == adaptive_minsv) + min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); + else + min_sv = minabs_sv; + + if ((S > min_sv && S <= max_sv) && (V > min_sv && V <= max_sv)) + colorMatched = true; + } + + if (colorMatched == true) { + p.put(j, (byte) 0); + p.put(j + 1, (byte) 0); + p.put(j + 2, (byte) 255); + } else { + p.put(j, (byte) 0); + p.put(j + 1, (byte) 0); + p.put(j + 2, (byte) 0); + } + } + } + + // 获取颜色匹配后的二值灰度图 + MatVector hsvSplit_done = new MatVector(); + opencv_core.split(src_hsv, hsvSplit_done); + Mat src_grey = hsvSplit_done.get(2); + + return src_grey; + } + + /** + * 判断一个车牌的颜色 + * + * @param src + * 车牌mat + * @param r + * 颜色模板 + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return + */ + public static boolean plateColorJudge(final Mat src, final PlateColor color, final boolean adaptive_minsv) { + // 判断阈值 + final float thresh = 0.49f; + + Mat gray = colorMatch(src, color, adaptive_minsv); + + float percent = (float) opencv_core.countNonZero(gray) / (gray.rows() * gray.cols()); + + return (percent > thresh) ? true : false; + } + + /** + * getPlateType 判断车牌的类型 + * + * @param src + * @param adaptive_minsv + * S和V的最小值由adaptive_minsv这个bool值判断 + *
        + *
      • 如果为true,则最小值取决于H值,按比例衰减 + *
      • 如果为false,则不再自适应,使用固定的最小值minabs_sv + *
      + * @return + */ + public static PlateColor getPlateType(final Mat src, final boolean adaptive_minsv) { + if (plateColorJudge(src, PlateColor.BLUE, adaptive_minsv) == true) { + return PlateColor.BLUE; + } else if (plateColorJudge(src, PlateColor.YELLOW, adaptive_minsv) == true) { + return PlateColor.YELLOW; + } else if (plateColorJudge(src, PlateColor.GREEN, adaptive_minsv) == true) { + return PlateColor.GREEN; + } else { + return PlateColor.UNKNOWN; + } + } + + /** + * 获取垂直或水平方向直方图 + * + * @param img + * @param direction + * @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]; + opencv_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 = opencv_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; + } + + /** + * Assign values to feature + *

      + * 样本特征为水平、垂直直方图和低分辨率图像所组成的矢量 + * + * @param in + * @param sizeData + * 低分辨率图像size = sizeData*sizeData, 可以为0 + * @return + */ + public static Mat features(final Mat in, final int sizeData) { + + float[] vhist = projectedHistogram(in, Direction.VERTICAL); + float[] hhist = projectedHistogram(in, Direction.HORIZONTAL); + + Mat lowData = new Mat(); + if (sizeData > 0) { + // resize.cpp:3784: error: (-215:Assertion failed) !ssize.empty() in function 'cv::resize' + opencv_imgproc.resize(in, lowData, new Size(sizeData, sizeData)); + } + + int numCols = vhist.length + hhist.length + lowData.cols() * lowData.rows(); + Mat out = Mat.zeros(1, numCols, opencv_core.CV_32F).asMat(); + FloatIndexer idx = out.createIndexer(); + + int j = 0; + for (int i = 0; i < vhist.length; ++i, ++j) { + idx.put(0, j, vhist[i]); + } + for (int i = 0; i < hhist.length; ++i, ++j) { + idx.put(0, j, hhist[i]); + } + for (int x = 0; x < lowData.cols(); x++) { + for (int y = 0; y < lowData.rows(); y++, ++j) { + float val = lowData.ptr(x, y).get(0) & 0xFF; + idx.put(0, j, val); + } + } + + return out; + } + + + + /** + * 显示图像 + * @param title + * @param src + */ + public static void showImage(final String title, final Mat src) { + if (src != null) { + opencv_highgui.imshow(title, src); + opencv_highgui.cvWaitKey(0); + } + } + +} diff --git a/src/main/resources/easypr-java/Features.java b/src/main/resources/easypr-java/Features.java new file mode 100644 index 00000000..6384828b --- /dev/null +++ b/src/main/resources/easypr-java/Features.java @@ -0,0 +1,90 @@ +package com.yuxue.easypr.core; + +import static com.yuxue.easypr.core.CoreFunc.features; +import static org.bytedeco.javacpp.opencv_core.merge; +import static org.bytedeco.javacpp.opencv_core.split; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_imgproc; + +/** + * + * @author yuxue + * @date 2020-05-05 08:26 + */ +public class Features implements SVMCallback { + + /*** + * EasyPR的getFeatures回调函数 + * 本函数是生成直方图均衡特征的回调函数 + * @param image + * @return + */ + @Override + public Mat getHisteqFeatures(final Mat image) { + return histeq(image); + } + + private Mat histeq(Mat in) { + Mat out = new Mat(in.size(), in.type()); + if (in.channels() == 3) { + Mat hsv = new Mat(); + MatVector hsvSplit = new MatVector(); + opencv_imgproc.cvtColor(in, hsv, opencv_imgproc.CV_BGR2HSV); + split(hsv, hsvSplit); + opencv_imgproc.equalizeHist(hsvSplit.get(2), hsvSplit.get(2)); + merge(hsvSplit, hsv); + opencv_imgproc.cvtColor(hsv, out, opencv_imgproc.CV_HSV2BGR); + hsv = null; + hsvSplit = null; + System.gc(); + } else if (in.channels() == 1) { + opencv_imgproc.equalizeHist(in, out); + } + return out; + } + + /** + * EasyPR的getFeatures回调函数 + * 本函数是获取垂直和水平的直方图图值 + * @param image + * @return + */ + @Override + public Mat getHistogramFeatures(Mat image) { + Mat grayImage = new Mat(); + opencv_imgproc.cvtColor(image, grayImage, opencv_imgproc.CV_RGB2GRAY); + + Mat img_threshold = new Mat(); + opencv_imgproc.threshold(grayImage, img_threshold, 0, 255, opencv_imgproc.CV_THRESH_OTSU + opencv_imgproc.CV_THRESH_BINARY); + + return features(img_threshold, 0); + } + + /** + * 本函数是获取SITF特征子的回调函数 + * + * @param image + * @return + */ + @Override + public Mat getSIFTFeatures(final Mat image) { + // TODO: 待完善 + return null; + } + + /** + * 本函数是获取HOG特征子的回调函数 + * + * @param image + * @return + */ + @Override + public Mat getHOGFeatures(final Mat image) { + // TODO: 待完善 + return null; + } + + +} diff --git a/src/main/resources/easypr-java/PlateDetect.java b/src/main/resources/easypr-java/PlateDetect.java new file mode 100644 index 00000000..d3041770 --- /dev/null +++ b/src/main/resources/easypr-java/PlateDetect.java @@ -0,0 +1,111 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; + + +/** + * 车牌检测识别 + * 分两个步骤: 1、车牌定位 2、车牌判断 + * @author yuxue + * @date 2020-04-24 15:33 + */ +public class PlateDetect { + + // 车牌定位, 图片处理对象 + private PlateLocate plateLocate = new PlateLocate(); + + // 切图判断对象 + private PlateJudge plateJudge = new PlateJudge(); + + /** + * @param src 图片路径,不能包含中文及特殊字符 + * @param resultVec 车牌的图块集合 + * @return the error number + *

        + *
      • 0: plate detected successfully; + *
      • -1: source Mat is empty; + *
      • -2: plate not detected. + *
      + */ + public int plateDetect(final Mat src, Vector resultVec) { + Vector matVec = plateLocate.plateLocate(src); // 定位 + + if (0 == matVec.size()) { + return -1; + } + + if (0 != plateJudge.plateJudge(matVec, resultVec)) { //对多幅图像进行SVM判断 + return -2; + } + return 0; + } + + + /** + * 生活模式与工业模式切换 + * @param pdLifemode + */ + public void setPDLifemode(boolean pdLifemode) { + plateLocate.setLifemode(pdLifemode); + } + + public void setGaussianBlurSize(int gaussianBlurSize) { + plateLocate.setGaussianBlurSize(gaussianBlurSize); + } + + public final int getGaussianBlurSize() { + return plateLocate.getGaussianBlurSize(); + } + + public void setMorphSizeWidth(int morphSizeWidth) { + plateLocate.setMorphSizeWidth(morphSizeWidth); + } + + public final int getMorphSizeWidth() { + return plateLocate.getMorphSizeWidth(); + } + + public void setMorphSizeHeight(int morphSizeHeight) { + plateLocate.setMorphSizeHeight(morphSizeHeight); + } + + public final int getMorphSizeHeight() { + return plateLocate.getMorphSizeHeight(); + } + + public void setVerifyError(float verifyError) { + plateLocate.setVerifyError(verifyError); + } + + public final float getVerifyError() { + return plateLocate.getVerifyError(); + } + + public void setVerifyAspect(float verifyAspect) { + plateLocate.setVerifyAspect(verifyAspect); + } + + public final float getVerifyAspect() { + return plateLocate.getVerifyAspect(); + } + + public void setVerifyMin(int verifyMin) { + plateLocate.setVerifyMin(verifyMin); + } + + public void setVerifyMax(int verifyMax) { + plateLocate.setVerifyMax(verifyMax); + } + + public void setJudgeAngle(int judgeAngle) { + plateLocate.setJudgeAngle(judgeAngle); + } + + public void setDebug(boolean debug, String tempPath) { + plateLocate.setDebug(debug); + plateLocate.setTempPath(tempPath); + } + +} diff --git a/src/main/resources/easypr-java/PlateJudge.java b/src/main/resources/easypr-java/PlateJudge.java new file mode 100644 index 00000000..a743ea45 --- /dev/null +++ b/src/main/resources/easypr-java/PlateJudge.java @@ -0,0 +1,107 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_imgproc; + +import java.util.Vector; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.Rect; +import org.bytedeco.javacpp.opencv_core.Size; +import org.bytedeco.javacpp.opencv_ml.SVM; + +import com.yuxue.constant.Constant; + + +/** + * 车牌判断 + * @author yuxue + * @date 2020-04-26 15:21 + */ +public class PlateJudge { + + private SVM svm = SVM.create(); + + public PlateJudge() { + loadSVM(Constant.DEFAULT_SVM_PATH); + } + + public void loadSVM(String path) { + svm.clear(); + // svm=SVM.loadSVM(path, "svm"); + svm=SVM.load(path); + } + + /** + * EasyPR的getFeatures回调函数, 用于从车牌的image生成svm的训练特征features + */ + private SVMCallback features = new Features(); + + + /** + * 对单幅图像进行SVM判断 + * @param inMat + * @return + */ + public int plateJudge(final Mat inMat) { + int ret = 1; + // 使用com.yuxue.train.SVMTrain 生成的训练库文件 + Mat features = this.features.getHistogramFeatures(inMat); + /*Mat samples = features.reshape(1, 1); + samples.convertTo(samples, opencv_core.CV_32F);*/ + + Mat p = features.reshape(1, 1); + p.convertTo(p, opencv_core.CV_32FC1); + ret = (int) svm.predict(features); + return ret; + + // 使用com.yuxue.train.PlateRecoTrain 生成的训练库文件 + // 在使用的过程中,传入的样本切图要跟训练的时候处理切图的方法一致 + /*Mat grayImage = new Mat(); + opencv_imgproc.cvtColor(inMat, grayImage, opencv_imgproc.CV_RGB2GRAY); + Mat dst = new Mat(); + opencv_imgproc.Canny(grayImage, dst, 130, 250); + Mat samples = dst.reshape(1, 1); + samples.convertTo(samples, opencv_core.CV_32F);*/ + + // 正样本为0 负样本为1 + /*if(svm.predict(samples) <= 0) { + ret = 1; + }*/ + /*ret = (int)svm.predict(samples); + System.err.println(ret); + return ret ;*/ + + } + + /** + * 对多幅图像进行SVM判断 + * @param inVec + * @param resultVec + * @return + */ + public int plateJudge(Vector inVec, Vector resultVec) { + + for (int j = 0; j < inVec.size(); j++) { + Mat inMat = inVec.get(j); + + if (1 == plateJudge(inMat)) { + resultVec.add(inMat); + } else { // 再取中间部分判断一次 + int w = inMat.cols(); + int h = inMat.rows(); + + Mat tmpDes = inMat.clone(); + Mat tmpMat = new Mat(inMat, new Rect((int) (w * 0.05), (int) (h * 0.1), (int) (w * 0.9), (int) (h * 0.8))); + opencv_imgproc.resize(tmpMat, tmpDes, new Size(inMat.size())); + + if (plateJudge(tmpDes) == 1) { + resultVec.add(inMat); + } + } + } + return 0; + } + + +} diff --git a/src/main/resources/easypr-java/PlateLocate.java b/src/main/resources/easypr-java/PlateLocate.java new file mode 100644 index 00000000..c889d80d --- /dev/null +++ b/src/main/resources/easypr-java/PlateLocate.java @@ -0,0 +1,354 @@ +package com.yuxue.easypr.core; + +import java.util.Vector; + +import static org.bytedeco.javacpp.opencv_core.*; +import static org.bytedeco.javacpp.opencv_imgproc.*; + +import com.yuxue.constant.Constant; +import org.bytedeco.javacpp.opencv_imgcodecs; + +import org.bytedeco.javacpp.opencv_core.CvPoint2D32f; +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_core.MatVector; +import org.bytedeco.javacpp.opencv_core.Point; +import org.bytedeco.javacpp.opencv_core.Point2f; +import org.bytedeco.javacpp.opencv_core.RotatedRect; +import org.bytedeco.javacpp.opencv_core.Scalar; +import org.bytedeco.javacpp.opencv_core.Size; + + +/** + * 车牌定位 + * @author yuxue + * @date 2020-04-24 15:33 + */ +public class PlateLocate { + + // PlateLocate所用常量 + public static final int DEFAULT_GAUSSIANBLUR_SIZE = 5; + public static final int SOBEL_SCALE = 1; + public static final int SOBEL_DELTA = 0; + public static final int SOBEL_DDEPTH = CV_16S; + public static final int SOBEL_X_WEIGHT = 1; + public static final int SOBEL_Y_WEIGHT = 0; + public static final int DEFAULT_MORPH_SIZE_WIDTH = 17; + public static final int DEFAULT_MORPH_SIZE_HEIGHT = 3; + + // showResultMat所用常量 + public static final int WIDTH = 136; + public static final int HEIGHT = 36; + public static final int TYPE = CV_8UC3; + + // verifySize所用常量 + public static final int DEFAULT_VERIFY_MIN = 3; + public static final int DEFAULT_VERIFY_MAX = 20; + + final float DEFAULT_ERROR = 0.6f; + final float DEFAULT_ASPECT = 3.75f; + + // 角度判断所用常量 + public static final int DEFAULT_ANGLE = 30; + + // 高斯模糊所用变量 + protected int gaussianBlurSize = DEFAULT_GAUSSIANBLUR_SIZE; + + // 连接操作所用变量 + protected int morphSizeWidth = DEFAULT_MORPH_SIZE_WIDTH; + protected int morphSizeHeight = DEFAULT_MORPH_SIZE_HEIGHT; + + // verifySize所用变量 + protected float error = DEFAULT_ERROR; + protected float aspect = DEFAULT_ASPECT; + protected int verifyMin = DEFAULT_VERIFY_MIN; + protected int verifyMax = DEFAULT_VERIFY_MAX; + + // 角度判断所用变量 + protected int angle = DEFAULT_ANGLE; + + // 是否开启调试模式,0关闭,非0开启 + protected boolean debug = true; + + // 开启调试模式之后,切图文件保存路径 + protected String tempPath = Constant.DEFAULT_TEMP_DIR + System.currentTimeMillis() + "/"; + + /** + * 生活模式与工业模式切换 + * @param islifemode + * 如果为真,则设置各项参数为定位生活场景照片(如百度图片)的参数,否则恢复默认值。 + * + */ + public void setLifemode(boolean islifemode) { + if (islifemode) { + setGaussianBlurSize(5); + setMorphSizeWidth(9); + setMorphSizeHeight(3); + setVerifyError(0.9f); + setVerifyAspect(4); + setVerifyMin(1); + setVerifyMax(30); + } else { + setGaussianBlurSize(DEFAULT_GAUSSIANBLUR_SIZE); + setMorphSizeWidth(DEFAULT_MORPH_SIZE_WIDTH); + setMorphSizeHeight(DEFAULT_MORPH_SIZE_HEIGHT); + setVerifyError(DEFAULT_ERROR); + setVerifyAspect(DEFAULT_ASPECT); + setVerifyMin(DEFAULT_VERIFY_MIN); + setVerifyMax(DEFAULT_VERIFY_MAX); + } + } + + /** + * 定位车牌图像 + * @param src 原始图像 + * @return 一个Mat的向量,存储所有抓取到的图像 + */ + public Vector plateLocate(Mat src) { + Vector resultVec = new Vector(); + + Mat src_blur = new Mat(); + Mat src_gray = new Mat(); + Mat grad = new Mat(); + + int scale = SOBEL_SCALE; + int delta = SOBEL_DELTA; + int ddepth = SOBEL_DDEPTH; + + // 高斯模糊。Size中的数字影响车牌定位的效果。 + GaussianBlur(src, src_blur, new Size(gaussianBlurSize, gaussianBlurSize), 0, 0, BORDER_DEFAULT); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_GaussianBlur.jpg", src_blur); + } + + // Convert it to gray 将图像进行灰度化 + cvtColor(src_blur, src_gray, CV_RGB2GRAY); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_gray.jpg", src_gray); + } + + // 对图像进行Sobel 运算,得到的是图像的一阶水平方向导数。 + + // Generate grad_x and grad_y + Mat grad_x = new Mat(); + Mat grad_y = new Mat(); + Mat abs_grad_x = new Mat(); + Mat abs_grad_y = new Mat(); + + Sobel(src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT); + convertScaleAbs(grad_x, abs_grad_x); + + Sobel(src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT); + convertScaleAbs(grad_y, abs_grad_y); + + // Total Gradient (approximate) + addWeighted(abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_Sobel.jpg", grad); + } + + // 对图像进行二值化。将灰度图像(每个像素点有256 个取值可能)转化为二值图像(每个像素点仅有1 和0 两个取值可能)。 + + Mat img_threshold = new Mat(); + threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_threshold.jpg", img_threshold); + } + + // 使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域。 + + Mat element = getStructuringElement(MORPH_RECT, new Size(morphSizeWidth, morphSizeHeight)); + morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_morphology.jpg", img_threshold); + } + + // Find 轮廓 of possibles plates 求轮廓。求出图中所有的轮廓。这个算法会把全图的轮廓都计算出来,因此要进行筛选。 + + MatVector contours = new MatVector(); + findContours(img_threshold, contours, // a vector of contours + CV_RETR_EXTERNAL, // 提取外部轮廓 + CV_CHAIN_APPROX_NONE); // all pixels of each contours + + Mat result = new Mat(); + if (debug) { + src.copyTo(result); + // 将轮廓描绘到图上输出 + drawContours(result, contours, -1, new Scalar(0, 0, 255, 255)); + opencv_imgcodecs.imwrite(tempPath + "debug_Contours.jpg", result); + } + + // Start to iterate to each contour founded + // 筛选。对轮廓求最小外接矩形,然后验证,不满足条件的淘汰。 + Vector rects = new Vector(); + + for (int i = 0; i < contours.size(); ++i) { + RotatedRect mr = minAreaRect(contours.get(i)); + if (verifySizes(mr)) + rects.add(mr); + } + + int k = 1; + for (int i = 0; i < rects.size(); i++) { + RotatedRect minRect = rects.get(i); + /*if (debug) { + Point2f rect_points = new Point2f(4); + minRect.points(rect_points); + + for (int j = 0; j < 4; j++) { + Point pt1 = new Point(new CvPoint2D32f(rect_points.position(j))); + Point pt2 = new Point(new CvPoint2D32f(rect_points.position((j + 1) % 4))); + + line(result, pt1, pt2, new Scalar(0, 255, 255, 255), 1, 8, 0); + } + }*/ + + // rotated rectangle drawing + // 旋转这部分代码确实可以将某些倾斜的车牌调整正,但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。 + // 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试这段代码。 + + float r = minRect.size().width() / minRect.size().height(); + float angle = minRect.angle(); + Size rect_size = new Size((int) minRect.size().width(), (int) minRect.size().height()); + if (r < 1) { + angle = 90 + angle; + rect_size = new Size(rect_size.height(), rect_size.width()); + } + // 如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理 + if (angle - this.angle < 0 && angle + this.angle > 0) { + Mat img_rotated = new Mat(); + Mat rotmat = getRotationMatrix2D(minRect.center(), angle, 1); + warpAffine(src, img_rotated, rotmat, src.size()); // CV_INTER_CUBIC + + Mat resultMat = showResultMat(img_rotated, rect_size, minRect.center(), k++); + resultVec.add(resultMat); + } + } + + return resultVec; + } + + + /** + * 对minAreaRect获得的最小外接矩形,用纵横比进行判断 + * + * @param mr + * @return + */ + private boolean verifySizes(RotatedRect mr) { + float error = this.error; + + // China car plate size: 440mm*140mm,aspect 3.142857 + float aspect = this.aspect; + int min = 44 * 14 * verifyMin; // minimum area + int max = 44 * 14 * verifyMax; // maximum area + + // Get only patchs that match to a respect ratio. + float rmin = aspect - aspect * error; + float rmax = aspect + aspect * error; + + int area = (int) (mr.size().height() * mr.size().width()); + float r = mr.size().width() / mr.size().height(); + if (r < 1) + r = mr.size().height() / mr.size().width(); + + return area >= min && area <= max && r >= rmin && r <= rmax; + } + + /** + * 显示最终生成的车牌图像,便于判断是否成功进行了旋转。 + * @param src + * @param rect_size + * @param center + * @param index + * @return + */ + private Mat showResultMat(Mat src, Size rect_size, Point2f center, int index) { + Mat img_crop = new Mat(); + getRectSubPix(src, rect_size, center, img_crop); + + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_crop_" + index + ".jpg", img_crop); + } + + Mat resultResized = new Mat(); + resultResized.create(HEIGHT, WIDTH, TYPE); + resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC); + if (debug) { + opencv_imgcodecs.imwrite(tempPath + "debug_resize_" + index + ".jpg", resultResized); + } + return resultResized; + } + + + + public String getTempPath() { + return tempPath; + } + + public void setTempPath(String tempPath) { + this.tempPath = tempPath; + } + + public void setGaussianBlurSize(int gaussianBlurSize) { + this.gaussianBlurSize = gaussianBlurSize; + } + + public final int getGaussianBlurSize() { + return this.gaussianBlurSize; + } + + public void setMorphSizeWidth(int morphSizeWidth) { + this.morphSizeWidth = morphSizeWidth; + } + + public final int getMorphSizeWidth() { + return this.morphSizeWidth; + } + + public void setMorphSizeHeight(int morphSizeHeight) { + this.morphSizeHeight = morphSizeHeight; + } + + public final int getMorphSizeHeight() { + return this.morphSizeHeight; + } + + public void setVerifyError(float error) { + this.error = error; + } + + public final float getVerifyError() { + return this.error; + } + + public void setVerifyAspect(float aspect) { + this.aspect = aspect; + } + + public final float getVerifyAspect() { + return this.aspect; + } + + public void setVerifyMin(int verifyMin) { + this.verifyMin = verifyMin; + } + + public void setVerifyMax(int verifyMax) { + this.verifyMax = verifyMax; + } + + public void setJudgeAngle(int angle) { + this.angle = angle; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + public boolean getDebug() { + return debug; + } + +} diff --git a/src/main/resources/easypr-java/SVMCallback.java b/src/main/resources/easypr-java/SVMCallback.java new file mode 100644 index 00000000..037c198e --- /dev/null +++ b/src/main/resources/easypr-java/SVMCallback.java @@ -0,0 +1,44 @@ +package com.yuxue.easypr.core; + +import org.bytedeco.javacpp.opencv_core.Mat; + + +/** + * @author Created by fanwenjie + * @author lin.yao + * + */ +public interface SVMCallback { + + /*** + * EasyPR的getFeatures回调函数,本函数是生成直方图均衡特征的回调函数 + * + * @param image + * @return + */ + public abstract Mat getHisteqFeatures(final Mat image); + + /** + * EasyPR的getFeatures回调函数, 本函数是获取垂直和水平的直方图图值 + * + * @param image + * @return + */ + public abstract Mat getHistogramFeatures(final Mat image); + + /** + * 本函数是获取SITF特征子的回调函数 + * + * @param image + * @return + */ + public abstract Mat getSIFTFeatures(final Mat image); + + /** + * 本函数是获取HOG特征子的回调函数 + * + * @param image + * @return + */ + public abstract Mat getHOGFeatures(final Mat image); +} diff --git a/src/main/resources/easypr-java/SVMTrain1.java b/src/main/resources/easypr-java/SVMTrain1.java new file mode 100644 index 00000000..d2f01432 --- /dev/null +++ b/src/main/resources/easypr-java/SVMTrain1.java @@ -0,0 +1,382 @@ +package com.yuxue.train; + +import java.util.*; + +import static org.bytedeco.javacpp.opencv_core.*; +import static org.bytedeco.javacpp.opencv_ml.*; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_imgcodecs; + +import com.yuxue.easypr.core.Features; +import com.yuxue.easypr.core.SVMCallback; +import com.yuxue.util.Convert; +import com.yuxue.util.FileUtil; + +/** + * 基于org.bytedeco.javacpp包实现的训练 + * JavaCPP 是一个开源库,它提供了在 Java 中高效访问本地 C++的方法 + * + * 图片识别车牌训练 + * 训练出来的库文件,用于判断切图是否包含车牌 + * + * 训练的svm.xml应用: + * 1、替换res/model/svm.xml文件 + * 2、修改com.yuxue.easypr.core.PlateJudge.plateJudge(Mat) 方法 + * 将样本处理方法切换一下,即将对应被注释掉的模块代码取消注释 + * @author yuxue + * @date 2020-05-14 22:16 + */ +public class SVMTrain1 { + + private SVMCallback callback = new Features(); + + // 默认的训练操作的根目录 + private static final String DEFAULT_PATH = "D:/PlateDetect/train/plate_detect_svm/"; + + // 训练模型文件保存位置 + private static final String MODEL_PATH = DEFAULT_PATH + "svm.xml"; + + private static final String hasPlate = "HasPlate"; + private static final String noPlate = "NoPlate"; + + public SVMTrain1() { + } + + public SVMTrain1(SVMCallback callback) { + this.callback = callback; + } + + /** + * 将learn文件夹下的图片,转存到tain test文件夹下,区分hasPalte noPlate + * 随机选取bound%作为训练数据,30%作为测试数据 + * @param bound + * @param name + */ + private void learn2Plate(float bound, final String name) { + final String filePath = DEFAULT_PATH + "learn/" + name; + Vector files = new Vector(); + + //// 获取该路径下的所有文件 + FileUtil.getFiles(filePath, files); + int size = files.size(); + if (0 == size) { + System.err.println("当前目录下没有文件: " + filePath); + return; + } + Collections.shuffle(files, new Random(new Date().getTime())); + + //// 随机选取70%作为训练数据,30%作为测试数据 + int boundry = (int) (bound * size); + + // 重新创建目录 + FileUtil.recreateDir(DEFAULT_PATH + "train/" + name); + FileUtil.recreateDir(DEFAULT_PATH + "test/" + name); + + for (int i = 0; i < boundry; i++) { + Mat img = opencv_imgcodecs.imread(files.get(i)); + String str = DEFAULT_PATH + "train/" + name + "/" + name + "_" + Integer.valueOf(i).toString() + ".jpg"; + opencv_imgcodecs.imwrite(str, img); + } + + for (int i = boundry; i < size; i++) { + Mat img = opencv_imgcodecs.imread(files.get(i)); + String str = DEFAULT_PATH + "test/" + name + "/" + name + "_" + Integer.valueOf(i).toString() + ".jpg"; + opencv_imgcodecs.imwrite(str, img); + } + } + + /** + * 获取训练图片 + * @param trainingImages + * @param trainingLabels + * @param name + */ + private void getPlateTrain(Mat trainingImages, Vector trainingLabels, final String name, int label) { + // int label = 1; + final String filePath = DEFAULT_PATH + "train/" + name; + Vector files = new Vector(); + + // 获取该路径下的所有文件 + FileUtil.getFiles(filePath, files); + + int size = files.size(); + if (null == files || size <= 0) { + System.out.println("File not found in " + filePath); + return; + } + for (int i = 0; i < size; i++) { + // System.out.println(files.get(i)); + Mat inMat = opencv_imgcodecs.imread(files.get(i)); + // 调用回调函数决定特征 + // Mat features = this.callback.getHisteqFeatures(inMat); + Mat features = this.callback.getHistogramFeatures(inMat); + // 通过直方图均衡化后的彩色图进行预测 + Mat p = features.reshape(1, 1); + p.convertTo(p, opencv_core.CV_32F); + + // 136 36 14688 1 变换尺寸 + // System.err.println(inMat.cols() + "\t" + inMat.rows() + "\t" + p.cols() + "\t" + p.rows()); + + trainingImages.push_back(p); // 合并成一张图片 + trainingLabels.add(label); + } + } + + private void getPlateTest(MatVector testingImages, Vector testingLabels, final String name, int label) { + // int label = 1; + final String filePath = DEFAULT_PATH + "test/" + name; + Vector files = new Vector(); + FileUtil.getFiles(filePath, files); + + int size = files.size(); + if (0 == size) { + System.out.println("File not found in " + filePath); + return; + } + System.out.println("get " + name + " test!"); + for (int i = 0; i < size; i++) { + Mat inMat = opencv_imgcodecs.imread(files.get(i)); + testingImages.push_back(inMat); + testingLabels.add(label); + } + } + + // ! 测试SVM的准确率,回归率以及FScore + public void getAccuracy(Mat testingclasses_preditc, Mat testingclasses_real) { + int channels = testingclasses_preditc.channels(); + System.out.println("channels: " + Integer.valueOf(channels).toString()); + int nRows = testingclasses_preditc.rows(); + System.out.println("nRows: " + Integer.valueOf(nRows).toString()); + int nCols = testingclasses_preditc.cols() * channels; + System.out.println("nCols: " + Integer.valueOf(nCols).toString()); + int channels_real = testingclasses_real.channels(); + System.out.println("channels_real: " + Integer.valueOf(channels_real).toString()); + int nRows_real = testingclasses_real.rows(); + System.out.println("nRows_real: " + Integer.valueOf(nRows_real).toString()); + int nCols_real = testingclasses_real.cols() * channels; + System.out.println("nCols_real: " + Integer.valueOf(nCols_real).toString()); + + double count_all = 0; + double ptrue_rtrue = 0; + double ptrue_rfalse = 0; + double pfalse_rtrue = 0; + double pfalse_rfalse = 0; + + for (int i = 0; i < nRows; i++) { + + final float predict = Convert.toFloat(testingclasses_preditc.ptr(i)); + final float real = Convert.toFloat(testingclasses_real.ptr(i)); + + count_all++; + + // System.out.println("predict:" << predict).toString()); + // System.out.println("real:" << real).toString()); + + if (predict == 1.0 && real == 1.0) + ptrue_rtrue++; + if (predict == 1.0 && real == 0) + ptrue_rfalse++; + if (predict == 0 && real == 1.0) + pfalse_rtrue++; + if (predict == 0 && real == 0) + pfalse_rfalse++; + } + + System.out.println("count_all: " + Double.valueOf(count_all).toString()); + System.out.println("ptrue_rtrue: " + Double.valueOf(ptrue_rtrue).toString()); + System.out.println("ptrue_rfalse: " + Double.valueOf(ptrue_rfalse).toString()); + System.out.println("pfalse_rtrue: " + Double.valueOf(pfalse_rtrue).toString()); + System.out.println("pfalse_rfalse: " + Double.valueOf(pfalse_rfalse).toString()); + + double precise = 0; + if (ptrue_rtrue + ptrue_rfalse != 0) { + precise = ptrue_rtrue / (ptrue_rtrue + ptrue_rfalse); + System.out.println("precise: " + Double.valueOf(precise).toString()); + } else { + System.out.println("precise: NA"); + } + + double recall = 0; + if (ptrue_rtrue + pfalse_rtrue != 0) { + recall = ptrue_rtrue / (ptrue_rtrue + pfalse_rtrue); + System.out.println("recall: " + Double.valueOf(recall).toString()); + } else { + System.out.println("recall: NA"); + } + + if (precise + recall != 0) { + double F = (precise * recall) / (precise + recall); + System.out.println("F: " + Double.valueOf(F).toString()); + } else { + System.out.println("F: NA"); + } + } + + /** + * 训练 + * @param dividePrepared + * @return + */ + public int svmTrain(boolean dividePrepared) { + + Mat classes = new Mat(); + Mat trainingData = new Mat(); + Mat trainingImages = new Mat(); + Vector trainingLabels = new Vector(); + + // 分割learn里的数据到train和test里 // 从库里面选取训练样本 + if (!dividePrepared) { + learn2Plate(0.1f, hasPlate); // 性能不好的机器,最好不要挑选太多的样本,这个方案太消耗资源了。 + learn2Plate(0.1f, noPlate); + } + + // System.err.println("Begin to get train data to memory"); + + getPlateTrain(trainingImages, trainingLabels, hasPlate, 0); + getPlateTrain(trainingImages, trainingLabels, noPlate, 1); + + // System.err.println(trainingImages.cols()); + + trainingImages.copyTo(trainingData); + trainingData.convertTo(trainingData, CV_32F); + + int[] labels = new int[trainingLabels.size()]; + for (int i = 0; i < trainingLabels.size(); ++i) { + labels[i] = trainingLabels.get(i).intValue(); + } + new Mat(labels).copyTo(classes); + + TrainData train_data = TrainData.create(trainingData, ROW_SAMPLE, classes); + + SVM svm = SVM.create(); + + try { + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 20000, 0.0001); + svm.setTermCriteria(criteria); // 指定 + svm.setKernel(SVM.RBF); // 使用预先定义的内核初始化 + svm.setType(SVM.C_SVC); // SVM的类型,默认是:SVM.C_SVC + svm.setGamma(0.1); // 核函数的参数 + svm.setNu(0.1); // SVM优化问题参数 + svm.setC(1); // SVM优化问题的参数C + svm.setP(0.1); + svm.setDegree(0.1); + svm.setCoef0(0.1); + + svm.trainAuto(train_data, 10, + SVM.getDefaultGrid(SVM.C), + SVM.getDefaultGrid(SVM.GAMMA), + SVM.getDefaultGrid(SVM.P), + SVM.getDefaultGrid(SVM.NU), + SVM.getDefaultGrid(SVM.COEF), + SVM.getDefaultGrid(SVM.DEGREE), + true); + + } catch (Exception err) { + System.out.println(err.getMessage()); + } + + System.out.println("Svm generate done!"); + + /*FileStorage fsTo = new FileStorage(MODEL_PATH, FileStorage.WRITE); + svm.write(fsTo, "svm");*/ + svm.save(MODEL_PATH); + return 0; + } + + // 测试 + public int svmPredict() { + SVM svm = SVM.create(); + try { + svm.clear(); + // svm = SVM.loadSVM(MODEL_PATH, "svm"); + svm = SVM.load(MODEL_PATH); + } catch (Exception err) { + System.err.println(err.getMessage()); + return 0; // next predict requires svm + } + + System.out.println("Begin to predict"); + // Test SVM + MatVector testingImages = new MatVector(); + Vector testingLabels_real = new Vector(); + + // 将测试数据加载入内存 + getPlateTest(testingImages, testingLabels_real, hasPlate, 0); + getPlateTest(testingImages, testingLabels_real, noPlate, 1); + + double count_all = 0; + double ptrue_rtrue = 0; + double ptrue_rfalse = 0; + double pfalse_rtrue = 0; + double pfalse_rfalse = 0; + + long size = testingImages.size(); + System.err.println(size); + + for (int i = 0; i < size; i++) { + Mat inMat = testingImages.get(i); + + // Mat features = callback.getHisteqFeatures(inMat); + Mat features = callback.getHistogramFeatures(inMat); + Mat p = features.reshape(1, 1); + p.convertTo(p, opencv_core.CV_32F); + + // System.out.println(p.cols() + "\t" + p.rows() + "\t" + p.type()); + + // samples.cols == var_count && samples.type() == CV_32F + // var_count 的值会在svm.xml库文件中有体现 + float predoct = svm.predict(features); + + int predict = (int) predoct; // 预期值 + int real = testingLabels_real.get(i); // 实际值 + + if (predict == 1 && real == 1) + ptrue_rtrue++; + if (predict == 1 && real == 0) + ptrue_rfalse++; + if (predict == 0 && real == 1) + pfalse_rtrue++; + if (predict == 0 && real == 0) + pfalse_rfalse++; + } + + count_all = size; + System.out.println("Get the Accuracy!"); + + System.out.println("count_all: " + Double.valueOf(count_all).toString()); + System.out.println("ptrue_rtrue: " + Double.valueOf(ptrue_rtrue).toString()); + System.out.println("ptrue_rfalse: " + Double.valueOf(ptrue_rfalse).toString()); + System.out.println("pfalse_rtrue: " + Double.valueOf(pfalse_rtrue).toString()); + System.out.println("pfalse_rfalse: " + Double.valueOf(pfalse_rfalse).toString()); + + double precise = 0; + if (ptrue_rtrue + ptrue_rfalse != 0) { + precise = ptrue_rtrue / (ptrue_rtrue + ptrue_rfalse); + System.out.println("precise: " + Double.valueOf(precise).toString()); + } else + System.out.println("precise: NA"); + + double recall = 0; + if (ptrue_rtrue + pfalse_rtrue != 0) { + recall = ptrue_rtrue / (ptrue_rtrue + pfalse_rtrue); + System.out.println("recall: " + Double.valueOf(recall).toString()); + } else + System.out.println("recall: NA"); + + double Fsocre = 0; + if (precise + recall != 0) { + Fsocre = 2 * (precise * recall) / (precise + recall); + System.out.println("Fsocre: " + Double.valueOf(Fsocre).toString()); + } else + System.out.println("Fsocre: NA"); + return 0; + } + + public static void main(String[] args) { + SVMTrain1 s = new SVMTrain1(); + s.svmTrain(true); + s.svmPredict(); + } + +}