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; } }