OpenCV-WetChatQR

2021新年伊始,微信团队开源了他们打磨很多年的QR识别代码,微信扫码引擎正式加入OpenCV开源!

依托OpenCV框架,识别代码使用非常简洁。之前应用大多基于ZXing实现,效率一般,一图多码的支持也是个问题!WetChatQR,基于CNN的二维码检测,实现一图多码功能效率非常不错!

照例还是在NDK下做实验,上代码:

CameraX

关于CameraX的详细使用方法,这里不介绍。主要介绍CameraX下面的WetChatQR使用方法。 这里假设你已经熟悉CameraX的用法,只需要增加ImageAnalysis。下面是Kotlin代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private external fun nWechatQrScan(src_addr1: Long): String 

val imageAnalysis = ImageAnalysis.Builder()
                .setTargetResolution(Size(dMetrics.widthPixels, dMetrics.heightPixels))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), { image ->
    if (null != image.image) {
        val frame = JavaCamera2Frame(image.image!!)
        val m = frame.rgba()

        val result = nWechatQrScan(m.nativeObj)
        frame.release()

        Log.i("ABC", "${result}")

        val jsonResult = JsonParser.parseString(result).asJsonArray
        if (null != jsonResult && 0 != jsonResult.size()) {
            handler.postDelayed({
                cameraProvider.unbindAll()
            }, 0)
        }

    }
    image.close()
})

JavaCamera2Frame类代码是从OpenCV拷出来修改过的版本(作者很懒的~ /手动狗头),Mat类自然也不例外来自OpenCV的SDK。站在巨人的肩上,杜绝重复造轮子~
Kotlin代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
private class JavaCamera2Frame(private val image: Image) {
    fun gray(): Mat {
        val planes = image.planes
        val w = image.width
        val h = image.height
        assert(planes[0].pixelStride == 1)
        val y_plane = planes[0].buffer
        val y_plane_step = planes[0].rowStride.toLong()
        mGray = Mat(h, w, CvType.CV_8UC1, y_plane, y_plane_step)
        return mGray
    }

    fun rgba(): Mat {
        val planes = image.planes
        val w = image.width
        val h = image.height
        val chromaPixelStride = planes[1].pixelStride
        return if (chromaPixelStride == 2) { // Chroma channels are interleaved
            assert(planes[0].pixelStride == 1)
            assert(planes[2].pixelStride == 2)
            val y_plane = planes[0].buffer
            val y_plane_step = planes[0].rowStride.toLong()
            val uv_plane1 = planes[1].buffer
            val uv_plane1_step = planes[1].rowStride.toLong()
            val uv_plane2 = planes[2].buffer
            val uv_plane2_step = planes[2].rowStride.toLong()
            val y_mat = Mat(h, w, CvType.CV_8UC1, y_plane, y_plane_step)
            val uv_mat1 = Mat(h / 2, w / 2, CvType.CV_8UC2, uv_plane1, uv_plane1_step)
            val uv_mat2 = Mat(h / 2, w / 2, CvType.CV_8UC2, uv_plane2, uv_plane2_step)
            val addr_diff = uv_mat2.dataAddr() - uv_mat1.dataAddr()
            if (addr_diff > 0) {
                assert(addr_diff == 1L)
                CvHelper.cvtColorTwoPlane(y_mat, uv_mat1, mRgba, Imgproc.COLOR_YUV2RGBA_NV12)
            } else {
                assert(addr_diff == -1L)
                CvHelper.cvtColorTwoPlane(y_mat, uv_mat2, mRgba, Imgproc.COLOR_YUV2RGBA_NV21)
            }
            mRgba
        } else { // Chroma channels are not interleaved
            val yuv_bytes = ByteArray(w * (h + h / 2))
            val y_plane = planes[0].buffer
            val u_plane = planes[1].buffer
            val v_plane = planes[2].buffer
            var yuv_bytes_offset = 0
            val y_plane_step = planes[0].rowStride
            if (y_plane_step == w) {
                y_plane[yuv_bytes, 0, w * h]
                yuv_bytes_offset = w * h
            } else {
                val padding = y_plane_step - w
                for (i in 0 until h) {
                    y_plane[yuv_bytes, yuv_bytes_offset, w]
                    yuv_bytes_offset += w
                    if (i < h - 1) {
                        y_plane.position(y_plane.position() + padding)
                    }
                }
                assert(yuv_bytes_offset == w * h)
            }
            val chromaRowStride = planes[1].rowStride
            val chromaRowPadding = chromaRowStride - w / 2
            if (chromaRowPadding == 0) {
                // When the row stride of the chroma channels equals their width, we can copy
                // the entire channels in one go
                u_plane[yuv_bytes, yuv_bytes_offset, w * h / 4]
                yuv_bytes_offset += w * h / 4
                v_plane[yuv_bytes, yuv_bytes_offset, w * h / 4]
            } else {
                // When not equal, we need to copy the channels row by row
                for (i in 0 until h / 2) {
                    u_plane[yuv_bytes, yuv_bytes_offset, w / 2]
                    yuv_bytes_offset += w / 2
                    if (i < h / 2 - 1) {
                        u_plane.position(u_plane.position() + chromaRowPadding)
                    }
                }
                for (i in 0 until h / 2) {
                    v_plane[yuv_bytes, yuv_bytes_offset, w / 2]
                    yuv_bytes_offset += w / 2
                    if (i < h / 2 - 1) {
                        v_plane.position(v_plane.position() + chromaRowPadding)
                    }
                }
            }
            val yuv_mat = Mat(h + h / 2, w, CvType.CV_8UC1)
            yuv_mat.put(0, 0, yuv_bytes)
            CvHelper.cvtColor(yuv_mat, mRgba, Imgproc.COLOR_YUV2RGBA_I420, 4)
            mRgba
        }
    }

    fun release() {
        mRgba.release()
        mGray.release()
    }

    private val mRgba = Mat()
    private var mGray = Mat()

}

NDK部分

C++部分,把识别结果用jsoncpp转成json字符串输出到Kotlin层使用。可以轻松实现同屏多个二维码扫描功能了~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

#include "opencv2/core.hpp"
#include <opencv2/opencv.hpp>
#include "opencv2/imgproc.hpp"
#include <android/bitmap.h>
#include <string>
#include "json/json.h" //这里用到了jsoncpp
#include <opencv2/wechat_qrcode.hpp>

using namespace cv;
using namespace wechat_qrcode;

//这里是模型文件所在的文件夹路径,需要初始化
static std::string n_workDir;

void nWechatQrScan(JNIEnv *env, jclass, jlong src1_addr) {
    WeChatQRCode *detector;
    std::vector<Mat> points;
    std::vector<std::string> result;
    try {
        Mat &src1 = *(reinterpret_cast<Mat *>(src1_addr));

        detector = new WeChatQRCode(n_workDir + "/wechatqr/detect.prototxt",
                                    n_workDir + "/wechatqr/detect.caffemodel",
                                    n_workDir + "/wechatqr/sr.prototxt",
                                    n_workDir + "/wechatqr/sr.caffemodel");

        result = detector->detectAndDecode(src1, points);
    } catch (cv::Exception e) {
        //LOGI("cv::Exception: %s", e.err.c_str());
    }

    if (NULL != detector) {
        delete detector;
    }


    int size = result.capacity();
    Json::Value rootValue = Json::arrayValue;
    for (int i = 0; i < size; i++) {
        std::string str1 = result[i];

        Json::Value cValue = Json::objectValue;
        cValue["result"] = str1;

        Mat mat = points[i];
//        Point pt1 = Point((int) mat[i].at<float>(0, 0), (int) mat[i].at<float>(0, 1));
//        Point pt2 = Point((int) mat[i].at<float>(1, 0), (int) mat[i].at<float>(1, 1));
//        Point pt3 = Point((int) mat[i].at<float>(2, 0), (int) mat[i].at<float>(2, 1));
//        Point pt4 = Point((int) mat[i].at<float>(3, 0), (int) mat[i].at<float>(3, 1));

        cValue["points"] = Json::arrayValue;
        for (int x = 0; x < 4; x++) {
            Point pt1 = Point((int) mat.at<float>(x, 0), (int) mat.at<float>(x, 1));
            Json::Value pValue = Json::objectValue;
            pValue["x"] = pt1.x;
            pValue["y"] = pt1.y;

            cValue["points"].append(pValue);
        }

        rootValue.append(cValue);
    }


    std::string jsonResult = Json::FastWriter().write(rootValue);

    return charToJString(env, (char *) jsonResult.c_str(), jsonResult.size());
}

上面所用到的四个模型文件,可以从这里下载

感谢腾讯WeChatCV团队的贡献,我相信国内的开源环境会越来越好,至少现在头部公司带了个好头~

参考与引用

charToJString转换
WeChatCV
jsoncpp

Built with Hugo
主题 StackJimmy 设计