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<Mat> plateLocate(Mat src) {
        Vector<Mat> resultVec = new Vector<Mat>();

        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<RotatedRect> rects = new Vector<RotatedRect>();

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

}
