Java + OpenCV 实现银行卡号识别 (1)

  • 大体思想

    银行卡号码的识别可简单分为目标检测及字符识别两个部分。
    CardOCR_00@2x.png

现在网络上能找的到资料比较少,而且多是处理印刷类银行卡的方法。按着二值化-腐蚀膨胀就能够定位到卡号区域;而字符识别靠的是固定阈值分割(如大津法)- 垂直投影法分割字符序列,最后做单一字符的识别。

但这些简易的方法几乎无法识别卡号凸起的银行卡。所以我在做了各种尝试后对其中卡号定位和字符分割部分进行了改进,准确率和识别速度都较不错,可在 1s 内完成识别,同时也支持识别身份证号。

  • 操作流程
  1. 若图片太大,归一化图片(否则影响处理速度); 并灰度化
public class CVGrayTransfer{

   public static Mat grayTransferBeforeScale(String fileName) {
        Mat src = Imgcodecs.imread(fileName);
        final int mw = src.width() > 1024 ? 1024 : src.width();
        return grayTransferBeforeScale(src, mw);
    }

    public static Mat grayTransferBeforeScale(Mat m, int resizeWidth) {
        Mat resize;
        resize = resizeMat(m, resizeWidth);
        Mat dst = new Mat();
        Imgproc.cvtColor(resize, dst, Imgproc.COLOR_BGR2GRAY); //灰度化
        return dst;
    }

    public static Mat resizeMat(Mat m, int resizeWidth) {
        Imgproc.resize(m, scaleMat, new Size(resizeWidth, 
          (float)m.height() / m.width() * resizeWidth), 0, 0, INTER_AREA);

        return scaleMat;
    }
}
public class CardOCR {
    public static void main(String []args) {
        String fileName= "/Users/...";
        Mat gray = CVGrayTransfer.grayTransferBeforeScale(fileName);
    }
}
  1. 寻找卡号区域,即进行特征提取。涉及到的图形学算法:Top-hat/Black-hat 形态学变换,高斯模糊,边缘检测,膨胀。先放上这部分代码的效果图。


    preprocessing.png
WX20190711-084122@2x.png

这些算法可以使得灰度图像二值化增强卡号特征,忽略银行卡背景细节,理想情况下应该是保留白色 (255) 长条形区域,其余噪声都被去除 (0).

但由于银行卡面纹理图文信息繁杂,加上光线强弱影响,所以会使得长条形区域不规整,出现其余白色区域块等。这需要我们另外实现算法加以甄别定位,第三部分将会给出我采用的方法。

以下是二值特征化的代码,为了加强算法鲁棒性,这里做了两路处理,分别对应强亮度和低亮度的环境。如果发现强亮度特征化不明显,则再使用低亮度算法。

/**
 * Created by chenqiu on 3/6/19.
 */
public class CVDilate {

    public static Mat dilateBrightRegion(Mat gray0) {
        Mat dst = new Mat();
        // top-hat enhance contrast
        Imgproc.morphologyEx(gray0, dst, Imgproc.MORPH_TOPHAT, 
      Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(9, 3)));

        Imgproc.GaussianBlur(dst, dst, new Size(13, 13), 0);
        Imgproc.Canny(dst, dst, 300, 600, 5, true);
        Imgproc.dilate(dst, dst, new Mat(), new Point(-1, -1), 5);
        Size heavy = new Size(35, 5);
        // apply a second dilate operation to the binary image
        Imgproc.dilate(dst, dst, 
             Imgproc.getStructuringElement(Imgproc.MORPH_RECT, heavy));

        return dst;
    }

    public static Mat dilateDarkRegion(Mat gray0) {
        Mat dst = new Mat();
        // enhance black area by black-hat
        Imgproc.morphologyEx(gray0, dst, Imgproc.MORPH_BLACKHAT, 
      Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(18,10)));

        Imgproc.GaussianBlur(dst, dst, new Size(13, 13), 0);
        Imgproc.Canny(dst, dst, 300, 600, 5, true);
    
        Size heavy = new Size(35, 3);
        Imgproc.dilate(dst, dst, 
            Imgproc.getStructuringElement(Imgproc.MORPH_RECT, heavy));

        return dst;
}
  1. 银行卡号排列方式有几种,有4-4-4-4的,2-16的,连串的等等,这导致了二值化后卡号区域被膨胀成一块长条形区域或是4块白色区域,见上图。我们的区域定位算法要求能满足所有的特征类型。
    首先对图像边缘用黑色(0) 填充,避免后面连通域分析受到干扰。
    大致过程:CVRegion 内部类 Filter 实现了定位4-4-4-4 区域的算法,即先得到所有的白色连通域,使用 boundingRect 求它们的外接矩形;对矩形的 Y 轴坐标排序得到最相近的 4 块矩形区域,且这其中每个矩形的 Y 坐标最大不超过 15 pixel,若不满足4块,可以降低到 3 块区域,根据此3块矩形长度和间隙估计出第 4 块矩形位置;合并 4 块矩形,得到卡号区域。
public class CVRegion {

   public static final int border = 10;
   
   // 定位后卡号区域二值图像
   private Mat binDigitRegion;

   public CVRegion(Mat graySrc) {
       super(graySrc);
       binDigitRegion = null;
   }

   /**
    * fill image border with black pix
    * @param m
    */
   public static void fillBorder(Mat m) {
       int cols = m.cols();
       int rows = m.rows();
       byte buff[] = new byte[cols * rows];
       m.get(0, 0, buff);
       for (int i = 0; i < cols; i++) {
           for (int j = 0; j < rows; j++) {
               if ((i > border && j > border) && 
                   (i < cols - border && j < rows - border))
                   continue;

               buff[j * cols + i] = 0;
           }
       }
       m.put(0, 0, buff);
   }

   /**
    * loc the digit area
    * @param src mat proc by binary, top-hat, dilate and closed opr
    * @return
    */
   public Rect digitRegion(Mat src) throws Exception {
       if (src.cols() < 20 || src.rows() < 20) 
           throw new Exception("error: image.cols() < 20 || image.rows() < 20 in function 'digitRegion(Mat m)'");

       fillBorder(src);
       // 连通域分析,提取所有白色 (255) 区域
       List<MatOfPoint> contours = new ArrayList<>();
       Imgproc.findContours(src, contours, new Mat(), 
                   Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

       Filter filter = new Filter(src);
       Rect rect;
       rect = filter.boundingIdRect(contours);

       if (rect == null) {
           /**
             检测类型为2-16或连串的长条形区域
             参考到第4节
           */
       }
       if (rect == null)
           return null;
       // 详细请移步第4节
       cutEdgeOfX(rect); 
       /**
         成功定位卡号矩形区域
       */
       return rect;
   }
}
    /**
    *  新增一个内部类,提供简单的区域定位 (4-4-4-4)。
    */
    final static class Filter extends ImgFilter {
        private Mat src;
        public Filter(Mat src) {
            this.src = src;
        }

        private void sortMap(int [][]a) {
            for (int i = 0; i < a[1].length - 1; i++) {
                int k = i;
                for (int j = i + 1; j < a[1].length; j++) {
                    if (a[1][k] > a[1][j]) {
                        k = j;
                    }
                }
                if (k != i) {
                    a[1][k] = a[1][k] + a[1][i];
                    a[1][i] = a[1][k] - a[1][i];
                    a[1][k] = a[1][k] - a[1][i];
                    a[0][i] = a[0][k] + a[0][i];
                    a[0][k] = a[0][i] - a[0][k];
                    a[0][i] = a[0][i] - a[0][k];
                }
            }
        }

        /**
         * get rect area of id numbers, only work at the 4-4-4-4 type
         * @param contours
         * @return null if rect of id area not found
         */
        public Rect boundingIdRect(List<MatOfPoint> contours) {
            Rect rect;
            List<Rect> rectSet = new ArrayList<>();
            for (int i = 0; i < contours.size(); i++) {
                rect = Imgproc.boundingRect(contours.get(i));
                rectSet.add(rect);
            }
            rect = rectSet.get(0);
            int dist[][] = new int[2][rectSet.size()];
            for (int i = 0; i < rectSet.size(); i++) {
                dist[0][i] = i;
                dist[1][i] = rectSet.get(i).y - rect.y;
            }
            sortMap(dist);

            final int verBias = 15;
            for (int i = 0; i < dist[1].length - 2; i++) {
                if (dist[1][i + 2] - dist[1][i] < verBias) {
                    int k;
                    /**
                     * Upper left and lower right corners
                     */
                    int sx = src.width();
                    int sy = src.height();
                    int mx = -1;
                    int my = -1;
                    // max width between these id-digit area
                    int mw = 0;
                    int sw = src.width();
                    for (k = 0; k < 3; k ++) {
                        rect = rectSet.get(dist[0][k + i]);
                        if (!isDigitRegion(rect, src.width(),src.height()))
                            break;

                        sx = Math.min(rect.x, sx);
                        sy = Math.min(rect.y, sy);
                        mx = Math.max(rect.x + rect.width, mx);
                        my = Math.max(rect.y + rect.height, my);
                        mw = Math.max(rect.width, mw);
                        sw = Math.min(rect.width, sw);
                    }

                    // less than 3 area, find next
                    if (k < 3) {
                        continue;
                    }

                    if (i < dist[1].length - 3) {
                        if (dist[1][i + 3] - dist[1][i] < verBias && 
                            isDigitRegion(rect = 
                                  rectSet.get(dist[0][i + 3]), 
                                        src.width(), src.height())) {
                            sx = Math.min(sx, rect.x);
                            sy = Math.min(sy, rect.y);
                            mx = Math.max(rect.x + rect.width, mx);
                            my = Math.max(rect.y + rect.height, my);
                            // finding out all 4 digit area
                            return new Rect(sx, sy, mx - sx, my - sy);
                        }
                    }

                    // completing 4th digit area
                    int mg;
                    //to make the gap largest,avoiding losing digit message
                    int gap = (mx - sx - sw * 3) >> 1;
                    Rect rt;
                    if (sx < (mg = src.width() - mx - gap)) {
                        rt = mg > mw ? 
                          new Rect(sx, sy, mx + mw + gap - sx , my - sy):
                          new Rect(10, sy, src.width() - 20, my - sy);
                    }
                    else {
                        mg = sx - gap;
                        rt = mg > mw ? 
                          new Rect(sx - mw -gap, sy, mx - sx +mw, my -sy):
                          new Rect(10, sy, src.width() - 20, my - sy);
                    }

                    return rt;
                }
            }
            return null;
        }

    }

/**
 * Created by chenqiu on 2/20/19.
 */
public class ImgFilter implements RectFilter {
    @Override
    public boolean isDigitRegion(Rect rect, int srcWidth, int srcHeight) {
        if (rect.width * rect.height < this.MIN_AREA) {
            return false;
        }
        if (srcHeight * this.MIN_HEIGHT_RATE > rect.height ||
                srcHeight * this.MAX_HEIGHT_RATE < rect.height) {
            return false;
        }
        if (srcWidth * this.MIN_WIDTH_RATE > rect.width) {
            return false;
        }
        return true;
    }
    
    /**
      先不管
    */
    @Override
    public int IDRegionSimilarity(Mat m, Rect r, int rows, int cols) {
        int origin = 0;
        if (r.y < this.MIN_HEIGHT_RATE * rows)
            return origin;
        if (r.y > (1 - this.MIN_HEIGHT_RATE) * rows)
            return origin;
        int y_score = 9;
        int bottom = r.y + r.height;
        if (r.y > rows * 0.8)
            y_score = 5;
        if (bottom > rows * 0.9)
            y_score = 3;
        origin += r.y * y_score;
        float avgSimilarity = 0;
        List<MatOfPoint> cnt = new ArrayList<>();
        Imgproc.findContours(m, cnt, new Mat(), Imgproc.RETR_EXTERNAL,
                                           Imgproc.CHAIN_APPROX_SIMPLE);

        for (MatOfPoint contour : cnt) {
            Rect rect = Imgproc.boundingRect(contour);
            if (rect.height < MIN_AREA)
                continue;
            double cntArea = Imgproc.contourArea(contour);
            int frameSize = rect.width * r.height;
            avgSimilarity += (cntArea / frameSize);
            origin += cntArea;
        }

        avgSimilarity /= cnt.size();
        origin *= avgSimilarity;
        return origin;
    }

    /**
      先不管
    */
    @Override
    public void findMaxRect(Mat m, Rect r) {
        int mainLeft = (int)(r.width * 0.1f) + r.x;
        int mainRight = (int)(r.width * 0.9f) + r.x;
        int mainCenter = (r.width >> 1) + r.x;
        int minWidth = (int)(m.cols() * MIN_WIDTH_RATE);
        List<MatOfPoint> contours = new ArrayList<>();
        Imgproc.findContours(m, contours, new Mat(), Imgproc.RETR_EXTERNAL,
                                        Imgproc.CHAIN_APPROX_SIMPLE);

        for (MatOfPoint contour : contours) {
            Rect rect = Imgproc.boundingRect(contour);
            int center = rect.x + rect.width / 2;
            if (center < mainLeft || center > mainRight || 
                                            rect.height < MIN_AREA)
                continue;

            // rect frame 【 】
            int frameSize = rect.width * r.height;
            // white region size in frame 【==】
            int frameArea = Core.countNonZero(new Mat(m, 
                          new Rect(rect.x, r.y, rect.width, r.height)));

            if (frameArea < frameSize * FULL_AREA_RATIO ||
        (rect.height < r.height * FRAME_H_RATIO && rect.width > minWidth)){

                if (center < mainCenter) {
                    r.width -= (center - r.x);
                    r.x = center;
                } else
                    r.width = center - r.x;

                break;
            }
        }

    }
}

/**
 * Created by chenqiu on 2/20/19.
 */
public interface RectFilter {
    /**
     * minimum height or size in roi
     */
    int MIN_AREA = 10;
    float MIN_HEIGHT_RATE = 0.038f;
    float MAX_HEIGHT_RATE = 0.15f;
    float MIN_WIDTH_RATE = 0.12f;
    /**
     * filter out irrelevant areas of the credit card
     * @param rect
     * @return
     */
    boolean isDigitRegion(Rect rect, int srcWidth, int srcHeight);

    int HEIGHT_SCORE = 6;
    int WIDTH_SCORE = 3;
    int IDRegionSimilarity(Mat m, Rect r, int rows, int cols);

    float FULL_AREA_RATIO = 0.8f;
    float FRAME_H_RATIO = 0.7f;
    void findMaxRect(Mat m, Rect r);

}
  1. 检测非4-4-4-4 特征的算法比较繁琐,也做了比较多的优化提升它的准确度同时保证较快的速度。实现方法是找出近似的长矩形区域,这个矩形区域的长度和高度限制在一定范围内,这样会得到一个多个结果的集合,根据它们的 Y 坐标、长度、矩形相似度/饱满度(白色区域占比)得分对比。最后确定卡号区域。

    近似长矩形的确定给这里带来了比较多的麻烦,因为它可能是这里凹一块,那里凸出来一块,或是与上下的噪声区域粘合在一起,又或者是比较短或扁。所以需要设计一个的算法去筛选判定。
    简略步骤:首先是横向扫描二值图像,找出最长的白线(1 pixel) 作为基线,对基线上每个 pixel 在 Y 方向延伸,即这个位置的高度;如果连续的 k 个 pixel 的高度不在 [0.038H, 0.15H] 范围 (H:图像高度),则舍弃这个区域。重复至所有区域都筛除完为止。

    为了避免不必要的过多的扫描,我们需要先锁定数个可疑的连通域:选取图像中最大的前 5 个连通域,且把它们合并。方法类似,由 boundingIdRect 得到的连通域矩形,合并 Y 方向存在重叠的矩形。


    WX20190711-112631@2x.png
public class CVRegion {

....

    public Rect digitRegion(Mat src) throws Exception {
        ....
        if (rect == null) {
            // if cannot bounding digit area, start separating rect areas which are large
            Collections.sort(contours, new Comparator<MatOfPoint>() {
                @Override
                public int compare(MatOfPoint o1, MatOfPoint o2) {
                    return -((int) (Imgproc.contourArea(o1) - 
                                    Imgproc.contourArea(o2))); // decrease
                }
            });

            final int detectDepth = Math.min(5, contours.size());
            int maxScore = 0;
            List<Rect> brs = new ArrayList<>();
            for (int t = 0; t < detectDepth; t++) {
                Rect br = Imgproc.boundingRect(contours.get(t));
                filter.combineRect(brs, br);
            }
            // detect region
            // 对每一个合并后的矩形区域进行扫描,得到近似长矩形的区域集合
            for (Rect br : brs) {
                List<Rect> separates = this.rectSeparate(src, br);
                for (Rect r : separates) {
                    Mat roi = drawRectRegion(src, r);
                    filter.findMaxRect(roi, r);
                    // 对集合中的每个长矩形区域计算得分
                    int score = filter.IDRegionSimilarity(roi, r, 
                                                src.rows(), src.cols());

                    if (score > maxScore) {
                        maxScore = score;
                        rect = r;
                    }
                }
            }
        }
        ....
      }
      
      public static Mat drawRectRegion(Mat src, Rect roi) {
        byte buff[] = new byte[src.rows() * src.cols()];
        src.get(0, 0, buff);

        Mat m = Mat.zeros(src.size(), src.type());
        byte out[] = new byte[buff.length];
        int row = roi.y + roi.height;
        for (int i = roi.y; i < row; i++) {
            System.arraycopy(buff, i * src.cols() + roi.x, out, 
                                      i * src.cols() + roi.x, roi.width);
        }

        m.put(0, 0, out);
        return m;
      }

    final static class Filter extends ImgFilter {
        ....
        public void combineRect(List<Rect> combinedList, Rect input) {
            int v;
            for (v = 0; v < combinedList.size(); v++) {
                Rect cell = combinedList.get(v);
                int bb = input.y + input.height;
                if (bb >= cell.y) {
                    int bc = cell.y + cell.height;
                    if (bb <= bc) {
                        cell.y = Math.min(input.y, cell.y);
                        // update height
                        cell.height = bc - cell.y;
                        break;
                    }
                    if (input.y <= bc) {
                        cell.y = Math.min(input.y, cell.y);
                        // update height
                        cell.height = bb - cell.y;
                        break;
                    }
                }
            }
            if (v == combinedList.size())
                combinedList.add(input);
        }
    }

}

这里要将 CVRegion 扩展一下,避免类过于臃肿。
使其继承自抽象类 ImgSeparator

public class CVRegion extends ImgSeparator {
    ....
    /**
     * remove left and right edge of id region
     * @param rect
     */
    // (第三节补充)消除图像左右边界的影响
    protected void cutEdgeOfX(Rect rect) {
        Mat dst = new Mat();
        Imgproc.GaussianBlur(grayMat, dst, new Size(13, 13), 0);
        Imgproc.Canny(dst, dst, 300, 600, 5, true);
        Imgproc.dilate(dst, dst, new Mat(), new Point(-1, -1), 1);
        Mat m = new Mat(dst, rect);
        byte buff[] = new byte[m.rows() * m.cols()];
        m.get(0, 0, buff);
        int rows = rect.height;
        int cols = rect.width;
        int left = rect.x;
        int right = rect.x + rect.width;
        int w = 0;
        for (int i = 0; i < (cols >> 1); i++) {
            int h = 0;
            for (int j = 0; j < rows; j++) {
                int at = j * cols + i;
                if (buff[at] == 0 && w == 0) {
                    break;
                }
                if (buff[at] != 0) ++h;
            }
            if (w > 0 && h == 0) break;
            if (h == rows) ++w;
            if (w > 0)
                left = rect.x + i;
        }

        byte b[] = new byte[dst.cols() * dst.rows()];
        dst.get(0, 0 ,b);
        if (w > 0) {
            int max = 0;
            for (int i = 0; i < w; i++) {
                int h = extendHeight(b, dst.cols(), left - i, rect.y);
                max = Math.max(max, h);
            }
            // reset
            if (max < rect.height * 1.5)
                left = rect.x;
        }
        // right edge
        w = 0;
        for (int i = cols - 1; i > (cols >> 1); i--) {
            int h = 0;
            for (int j = 0; j < rows; j++) {
                int at = j * cols + i;
                if (buff[at] == 0 && w == 0)
                    break;
                if (buff[at] != 0) ++h;
            }
            if (w > 0 && h == 0) break;
            if (h == rows) w++;
            if (w > 0)
                right = rect.x + i;
        }
        if (w > 0) {
            int max = 0;
            for (int i = 0; i < w; i++) {
                int h = extendHeight(b, dst.cols(), right + i, rect.y);
                max = Math.max(max, h);
            }
            if (max < rect.height * 1.5)
                right = rect.x + rect.width;
        }
        rect.x = left;
        rect.width = right - left;
    }
    ....
}
/**
 * Created by chenqiu on 2/21/19.
 */
public abstract class ImgSeparator implements RectSeparator, DigitSeparator{
    public Mat grayMat;

    // 用于存放单个字符的列表(第 2 章会用到)
    protected List<Mat> matListOfDigit;

    // 得到的卡号矩形区域
    protected Rect rectOfDigitRow;

    public ImgSeparator(Mat graySrc) {
        this.grayMat = graySrc;
        matListOfDigit = new ArrayList<>();
        rectOfDigitRow = null;
    }
    
    @Override
    public List<Rect> rectSeparate(Mat src, Rect region) throws Exception {

        if (src.channels() != 1)
            throw new Exception("error: image.channels() != 1 in 
                  function 'rectSeparate(Mat m,Rect r)'");

        // fist step, remove abnormal height area, fill with 0
        int cols = src.cols();
        int rows = src.rows();
        byte buff[] = new byte[cols * rows];
        src.get(0, 0, buff);
        List<Rect> stack = new LinkedList<>();
        List<Rect> separates = new ArrayList<>();
        stack.add(region);

        while (!stack.isEmpty()) {
            Rect ret = new Rect();
            Rect head;
            Rect scan = findEdge(buff, cols, rows, 
                                            head = stack.remove(0), ret);
            if (ret.x > 0 && ret.y > 0) {
                separates.add(ret);
            }
            // separate region
            int upper = scan.y - head.y;
            int lower = head.y + head.height - scan.y - scan.height;
            if (upper > 0) {
                stack.add(new Rect(head.x, head.y, head.width, upper));
            }
            if (lower > 0) {
                stack.add(new Rect(head.x, scan.y + scan.height, 
                                                  head.width,  lower));
            }

        }
        return separates;
    }

    /**
     * return rect scanned bounding, remove it for avoiding scanning overtimes
     * <p>if finding failed, out.x = out.y = -1</p>
     * @param buff
     * @param cols
     * @param rows
     * @param region
     * @param out
     * @return 扫描出的近似长矩形区域
     */
    private Rect findEdge(byte buff[], int cols, int rows, Rect region, Rect out) {
        // thresh of `thin`
        final int thinH = (int)(RectFilter.MIN_HEIGHT_RATE * rows);
        out.x = out.y = -1;
        if (region.height < thinH) {
            return region.clone();
        }

        int w = region.x + region.width;
        int h = region.y + region.height;
        int pivot[] = new int[3]; // the longest continuous line
        int len = 0; // length of the line
        //找到最长白线作基线
        for (int i = region.y; i < h; i++) {
            int tLen = 0;
            int start = 0;
            int gap = 0;
            for (int j = 0; j < cols; j++) {
                int index = i * cols + j;
                if (buff[index] != 0) {
                    if (tLen++ == 0)
                        start = j;
                    if (tLen > len) {
                        len = tLen;
                        pivot[0] = start; // start x-pos
                        pivot[1] = i;
                        pivot[2] = j; // end x-pos
                    }
                    gap = 0;
                } else if (++gap > RectFilter.MIN_WIDTH_RATE * cols) {
                        tLen = 0;
                }
            }
        }
        
        int line = pivot[2] - pivot[0];
        if (len < cols * (RectFilter.MIN_WIDTH_RATE * 3)) { // too short
            return region.clone();
        }

        int upperY, lowerY, cnt;
        upperY = lowerY = cnt = 0;
        int []ha = new int[line];
        for (int i = 0; i < line; i++) {
            ha[i] = extendHeight(buff, cols,i + pivot[0], pivot[1]);
        }

        final int normalH = (int)(RectFilter.MAX_HEIGHT_RATE * rows);
        // when continuous thin area is too long, assert fail
        final int thinW = (int)(RectFilter.MIN_WIDTH_RATE * len);
        final int normalW = (int)(0.1 * len);
        int cw = 0; // continuous width that fitted normal height
        int ctl = 0; // continuous thin len
        int y2[][] = new int[2][line];
        byte next = -1;
        // 扩展 Y 方向获得高度
        for (int c = 0; c < line; c++) {
            int []ey2 = extendY(buff, cols, c + pivot[0], pivot[1]);
            if (ha[c] < normalH) {
                if (ha[c] < thinH) {
                    ++ctl;
                    if (ctl > thinW) {
                        next = 0; // cannot be changed
                    }
                } else {
                    ctl = 0;

                    cw ++;
                    upperY += ey2[0];
                    lowerY += ey2[1];
                    cnt++;
                    if (cw > normalW && next != 0) {
                        next = 1;
                    }

                }
            } else {
                cw = 0;
            }
            y2[0][c] = ey2[0];
            y2[1][c] = ey2[1];
        }

        // find median
        Arrays.sort(y2[0]);
        Arrays.sort(y2[1]);
        int my1, my2, b = y2[0].length >> 1;
        my1 = y2[0][b];
        my2 = y2[1][b];
        if ((y2[0].length & 0x1) == 0) {
            my1 = (y2[0][b] + y2[0][b - 1]) >> 1;
            my2 = (y2[1][b] + y2[1][b - 1]) >> 1;
        }
        Rect scanRect = new Rect(region.x, my1, region.width, my2-my1 +1);
        if (next < 1) {
            return scanRect;
        }

        upperY /= cnt;
        lowerY /= cnt;
        Debug.log("upper: " + upperY + ", lower: " + lowerY);
        out.x = pivot[0];
        out.y = upperY;
        out.width = line;
        out.height = lowerY - upperY + 1;
        return scanRect;
    }

    abstract protected Rect cutEdgeOfY(Mat binSingleDigit);
    abstract protected void cutEdgeOfX(Rect rect);

    public void setRectOfDigitRow(Rect rectOfDigitRow) {
        this.rectOfDigitRow = rectOfDigitRow;
    }
}
/**
 * Created by chenqiu on 2/21/19.
 */
public interface RectSeparator {

    List<Rect> rectSeparate(Mat src, Rect region) throws Exception;

}

/**
 * Created by chenqiu on 2/21/19.
 */
public interface DigitSeparator {

    void digitSeparate() throws Exception;
}
  1. 到了这里那么我们的工作就已经完成了一半!!即找到了银行卡号所在位置(矩形区域)。

    通过调试可以画出定位结果,如下图所示,相似度得分已标注。


    WX20190711-140502@2x.png
  2. 最终扩展成的主函数

public class CVGrayTransfer{

   ....
    public static Mat resizeMat(String fileName) {
        Mat m = Imgcodecs.imread(fileName);
        final int mw = src.width() > 1024 ? 1024 : src.width();
        return resizeMat(src, mw);
        return scaleMat;
    }
}
public class CardOCR {

    static class Producer extends CVRegion {

        public Producer(Mat graySrc) {
            super(graySrc);
        }

        /**
          定位矩形位置
        */
        public Rect findMainRect() {
            boolean findBright = false;
            Mat gray = this.grayMat;
            Rect bestRect = new Rect();
            final float fullWidth = gray.cols() - Producer.border * 2;
            boolean chose;
            for ( ; ; findBright = true) {
                Mat dilate = CVDilate.fastDilate(gray, findBright);
                Rect idRect = null;
                chose = false;
                try {
                    idRect = this.digitRegion(dilate);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (idRect != null) {
                    if (bestRect.width == 0)
                        chose = true;
                    else if (idRect.width < fullWidth) {
                        if (bestRect.width == fullWidth || 
                                          idRect.width > bestRect.width)
                            chose = true;
                    }
                    if (chose) {
                        bestRect = idRect;
                    }
                }
                if (findBright) break;
            }
            if (bestRect.width == 0) {
                System.err.println("OCR Failed.");
                exit(1);
            }
            return bestRect;
        }
    }

    public static void main(String []args) {
        String fileName= "/Users/xxx.jpg";
        Mat gray = CVGrayTransfer.grayTransferBeforeScale(fileName);
        Producer producer = new Producer(gray);
        // 定位卡号矩形区域
        Rect mainRect = producer.findMainRect();
        // 设置矩形区域
        producer.setRectOfDigitRow(mainRect);
        // 窗口输出
        // HighGui.imshow("id numbers", new Mat(gray, mainRect));
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容