汪建军的博客

休迅飞凫,飘忽若神,凌波微步,罗袜生尘。

Android developer, sometimes thinking, sometimes try it.


Android下实现选取最大矩形-OpenCV与Card.io库

还是公司的项目需求:用户拍照提供银行卡照片,需要自动截取出卡片。当用户对准卡片拍照,那么照片中卡片基本上是在中间而且是整个图片中最大的矩形,那么问题转换为如何截取图片中最大的矩形。

这里提供两种解决途径:OpenCV、开源库card.io

关于card.io与银行卡号识别

OpenCV上篇博客有介绍,这里说说card.io。
实际上他也是建立在opencv的基础上,是国外一个开源的信用卡识别库,能自动从camera中截取出信用卡并准确识别16位凸起的卡号,这里有局限,不能识别大多数国内19位的借记卡。
国内的开源库ios有一个叫ocr_savingCard,解决了银行卡识别的问题,非常好用,Android没有,至少我没找到...
继续说card.io,实际上他是一整套流程,没识别出来也不会给你裁剪出银行卡,我们需要修改他的源码才能实现只要他那非常好用选出图片中的银行卡功能。他的体验也是很棒,微信的银行卡识别的拍照界面和他差不多,四条边对齐就会变成一个完整绿色的方框,没对齐就不完整。

OpenCV的实现

public static Rect findRectangle(Bitmap image) {
    try {
        Mat tempor = new Mat();
        Mat src = new Mat();
        Utils.bitmapToMat(image, tempor);

        Imgproc.cvtColor(tempor, src, Imgproc.COLOR_BGR2RGB);

        Mat blurred = src.clone();
        Imgproc.medianBlur(src, blurred, 9);

        Mat gray0 = new Mat(blurred.size(), CvType.CV_8U), gray = new Mat();

        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();

        List<Mat> blurredChannel = new ArrayList<Mat>();
        blurredChannel.add(blurred);
        List<Mat> gray0Channel = new ArrayList<Mat>();
        gray0Channel.add(gray0);

        MatOfPoint2f approxCurve;

        double maxArea = 0;
        int maxId = -1;

        for (int c = 0; c < 3; c++) {
            int ch[] = {c, 0};
            Core.mixChannels(blurredChannel, gray0Channel, new MatOfInt(ch));

            int thresholdLevel = 1;
            for (int t = 0; t < thresholdLevel; t++) {
                if (t == 0) {
                    Imgproc.Canny(gray0, gray, 10, 20, 3, true); // true ?
                    Imgproc.dilate(gray, gray, new Mat(), new Point(-1, -1), 1); // 1
                    // ?
                } else {
                    Imgproc.adaptiveThreshold(gray0, gray, thresholdLevel,
                            Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
                            Imgproc.THRESH_BINARY,
                            (src.width() + src.height()) / 200, t);
                }

                Imgproc.findContours(gray, contours, new Mat(),
                        Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);

                for (MatOfPoint contour : contours) {
                    MatOfPoint2f temp = new MatOfPoint2f(contour.toArray());

                    double area = Imgproc.contourArea(contour);
                    approxCurve = new MatOfPoint2f();
                    Imgproc.approxPolyDP(temp, approxCurve,
                            Imgproc.arcLength(temp, true) * 0.02, true);

                    if (approxCurve.total() == 4 && area >= maxArea) {
                        double maxCosine = 0;

                        List<Point> curves = approxCurve.toList();
                        for (int j = 2; j < 5; j++) {

                            double cosine = Math.abs(angle(curves.get(j % 4),
                                    curves.get(j - 2), curves.get(j - 1)));
                            maxCosine = Math.max(maxCosine, cosine);
                        }

                        if (maxCosine < 0.3) {
                            maxArea = area;
                            maxId = contours.indexOf(contour);
                        }
                    }
                }
            }
        }

        if (maxId >= 0) {
            Rect rect = Imgproc.boundingRect(contours.get(maxId));

            Imgproc.rectangle(src, rect.tl(), rect.br(), new Scalar(255, 0, 0, .8), 4);

            int mDetectedWidth = rect.width;
            int mDetectedHeight = rect.height;

            Log.d("", "Rectangle width :" + mDetectedWidth + " Rectangle height :" + mDetectedHeight);
            return rect;
        }

    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

private static double angle(Point p1, Point p2, Point p0) {
    double dx1 = p1.x - p0.x;
    double dy1 = p1.y - p0.y;
    double dx2 = p2.x - p0.x;
    double dy2 = p2.y - p0.y;
    return (dx1 * dx2 + dy1 * dy2)
            / Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2)
            + 1e-10);
}

以上代码看得懂的部分都好理解,对于看不懂的部分,opencv没深究,我也不懂o(╯□╰)o

调用findRectangle方法得到的是一个org.opencv.core.Rect(注意不是Android里的Rect)。当然返回可能是null表示没找到,或者找到了但不是我们想要的银行卡矩形,可以通过返回的Rect的cols和rows是否在正常范围内判断:

org.opencv.core.Rect roi = findRectangle(loadedImage); // 自动截出最大矩形
            if (roi == null) {
                // 没找到的处理
                return;
            }
            Mat uncropped = new Mat();
            Utils.bitmapToMat(loadedImage, uncropped);
            Mat cropped = new Mat(uncropped, roi);
            if (cropped.cols() < 340 || cropped.rows() < 210 || cropped.cols() > 880 || cropped.rows() > 580) { // 正确的大约是690,432
                // 没找到的处理
                return;
            }
            Bitmap bitmapIdOriginal = Bitmap.createBitmap(cropped.cols(), cropped.rows(), Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(cropped, bitmapIdOriginal);

这个时候bitmapIdOriginal就是自动裁剪出的最大矩形,也就是我们想要的银行卡或身份证图片。这时还可以处理一下,让这张图片的比例和真实的卡片比例一样,方便以后的截取部分做OCR,以身份证为例(身份证的宽高比是856:544):

bitmapIdOriginal = ThumbnailUtils.extractThumbnail(bitmapIdOriginal, 856, 544);

Card.io的实现

看他源码,主要是CardIOActivity与CardScanner这两个类,我们稍加改动。

首先是CardIOActivity的大概830行。

原来是:

if (mDetectOnly) {
        Intent dataIntent = new Intent();
        Util.writeCapturedCardImageIfNecessary(getIntent(), dataIntent, mOverlay);
        setResultAndFinish(RESULT_SCAN_SUPPRESSED, dataIntent);
}

改为if (mDetectOnly || mDetectedCard == null) {...}因为我们不需要他识别出卡号才返回结果,mDetectedCard 可以为空,我们拿mOverlay里的bitmap就是我们想要的裁剪出的银行卡图片。注意到他调用了一个Util.writeCapturedCardImageIfNecessary(Intent origIntent, Intent dataIntent, OverlayView mOverlay)方法:

static void writeCapturedCardImageIfNecessary(Intent origIntent, Intent dataIntent, OverlayView mOverlay){
    if (origIntent.getBooleanExtra(CardIOActivity.EXTRA_RETURN_CARD_IMAGE, false)
        && mOverlay != null && mOverlay.getBitmap() != null) {
        ByteArrayOutputStream scaledCardBytes = new ByteArrayOutputStream();
        mOverlay.getBitmap().compress(Bitmap.CompressFormat.JPEG, 100, scaledCardBytes); // 原本是80
        dataIntent.putExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE, scaledCardBytes.toByteArray());
    }

}

适当调高他原本转换bitmap的quality。

然后是CardScanner的大概480行。

原来是:

    if (!sufficientFocus) {
        triggerAutoFocus(false);
    } else if (dInfo.predicted() || (mSuppressScan && dInfo.detected())) {
        Log.d(TAG, "detected card: " + dInfo.creditCard());
        mScanActivityRef.get().onCardDetected(detectedBitmap, dInfo);
    }

改为:

    if (!sufficientFocus) {
        triggerAutoFocus(false);
    } else if (dInfo.predicted() || (mSuppressScan && dInfo.detected())) {
        Log.d(TAG, "detected card: " + dInfo.creditCard());
        mScanActivityRef.get().onCardDetected(detectedBitmap, dInfo);
    } else if (dInfo.detected()) {
        scanCount ++;
        Log.d(TAG, "scanCount=" + scanCount);
        if (scanCount == MAX_SCAN_COUNT) {
            mScanActivityRef.get().onCardDetected(detectedBitmap, dInfo);
            scanCount = 0;
        }
    }

其中新增的两个成员变量:

private int scanCount; // 超过MAX_SCAN_COUNT次还未识别成功,则截取银行卡矩形返回
private static final float MAX_SCAN_COUNT = 5;

最后就是在你的Activity中调用:

    // 启动CardIOActivity
    if (CardIOActivity.canReadCardWithCamera()) {
        Intent scanIntent = new Intent(context, CardIOActivity.class);
        scanIntent.putExtra(CardIOActivity.EXTRA_SUPPRESS_MANUAL_ENTRY, false); // 不允许手动修改
        scanIntent.putExtra(CardIOActivity.EXTRA_RETURN_CARD_IMAGE, true); // 返回图片
        scanIntent.putExtra(CardIOActivity.EXTRA_SUPPRESS_CONFIRMATION, true); // 不提示确认卡号
        startActivityForResult(scanIntent, REQUESTCODE_CARDIO);
    }

可以看到他是以startActivityForResult的形式启动的,对应的onActivityResult如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUESTCODE_CARDIO) {
        if (data != null) {
            if (data.hasExtra(CardIOActivity.EXTRA_SCAN_RESULT)) {
                CreditCard scanResult = data.getParcelableExtra(CardIOActivity.EXTRA_SCAN_RESULT);
                // 如果识别成功,这就是识别出的卡号
                char []numbers = scanResult.getFormattedCardNumber().toCharArray();
                // 相应处理
            }
            if (data.hasExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE)) {
                // 得到的自动裁剪出的银行卡图片
                byte[] dataBitmap = data.getByteArrayExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE);
                // 相应处理
            }
        }
    }
}

以上,玩的开心,有什么问题请留言,禁止商业用途转载,其他转载请告知,谢谢!

最近的文章

Android下实现身份证正面信息OCR

想要在Android下实现身份证正面信息的OCR,主要的思路就是: 先把照片上身份证的信息逐条切割出来,再用opencv的方法处理一遍,最后利用tess-two的库在异步线程里逐条识别。 关于te…

杂技继续阅读
更早的文章

Android下图片清晰度识别

还是公司项目的一个需求:判断用户拍的照片是否清晰,不清晰就不让提交。 核心思想:使用opencv的拉普拉斯算法检测图片的边缘数,越模糊的图片边缘数越少。 刚接触的小伙伴可以看一下这篇启发性文章 关于O…

杂技继续阅读
comments powered by Disqus