基于 CRNN 与 ML Kit 的高性能移动端扫描技术方案

26 阅读4分钟

1 核心架构: AI 识别的 三剑客

1.1 区域建议网络 (RPN) MobileNetV2 :高效率的定位器

在扫描卡片时,算法的第一步不是识别,而是“寻找”。

  • MobileNetV2:这是该方案的骨干网络( Backbone 。它采用了“倒残差结构(Inverted Residuals)”和“深度可分离卷积”,极大地减少了模型参数量。对于 Android 设备而言,这意味着更低的内存占用和发热。
  • RPN (Region Proposal Network) :在特征图上,RPN 会生成成千上万个候选框(Anchors),通过分类器判断哪个框内包含“文字”,并对框的坐标进行微调。它确保了即便卡片在镜头中是倾斜或偏离中心的,系统也能精准锁定卡号所在的矩形区域。

1.2 LSTM ( 长短期记忆网络 ) :序列的守护者

传统的 OCR 是一次识别一个字符,但这在光影复杂的银行卡场景下表现极差。

  • 核心逻辑:LSTM 能够捕捉图像切片之间的时序关系。当识别到“6222”时,LSTM 会利用内部记忆状态预判后续大概率依然是数字。这种对上下文的建模能力,使得它在处理模糊、反光或字体粘连时,具有极高的鲁棒性。

1.3  CTC ( 联结主义时序分类 ) :解决 对齐 难题

在训练模型时,我们很难精确标注图像中每个数字的像素起始位置。

  • 核心逻辑:CTC 允许模型输出一个比实际字符更长的序列(包含重复字符和占位符 -)。通过一套折叠算法(例如将 6--6-22--2 映射为 6622),CTC 解决了“图像宽度与字符长度不对等”的对齐难题,实现了端到端的识别。

2 数据防线:什么是 Luhn 校验?

AI 识别即便再精准,也存在 1% 的误报(例如将 0 误认为 8)。在金融场景下,这 1% 是不可接受的。因此,Luhn 算法(模10校验) 成为了最后一道防线。

2.1 算法原理

Luhn 算法是一种简单的校验和公式,广泛用于验证银行卡号(如 Visa, Mastercard)的有效性。其计算步骤如下:

  1. 从卡号最后一位数字(校验位)开始,从右向左,每隔一位数字乘以 2。
  2. 如果乘以 2 后的结果大于 9(例如 6×2=126 \times 2 = 12),则将结果的个位和十位相加(即 1+2=31+2=3),或者直接减去 9。
  3. 将所有处理后的数字与未经处理的数字全部相加。
  4. 如果总和能被 10 整除(Sum(mod10)==0Sum \pmod{10} == 0),则卡号合法。

2.2 为什么它很重要?

Luhn 校验能捕捉到单点数字错误或大部分相邻数字换位错误。通过在 OCR 识别后立即运行 Luhn 校验,APP 可以自动过滤掉错误的识别结果,要求用户继续扫描,直到获得合法的卡号。

3 Android 开发实践:如何落地?

在 Android 端实现上述方案,目前最成熟的路径是 CameraX + Google ML Kit

3.1 依赖配置

在 build.gradle 中引入 ML Kit 的文本识别库。

dependencies {

    // 使用捆绑模式(对应你看到的 assets 中的模型文件)

    implementation 'com.google.android.gms:play-services-mlkit-text-recognition:19.0.1'

    // CameraX 核心库

    implementation "androidx.camera:camera-camera2:1.3.0"

}

3. 2 核心代码实现: Luhn 校验器

首先,编写一个健壮的 Luhn 校验工具类:



object CardValidator {

    fun isValidLuhn(number: String): Boolean {

        var sum = 0

        var alternate = false

        // 从右往左处理

        for (i in number.length - 1 downTo 0) {

            var n = number[i] - '0'

            if (alternate) {

                n *= 2

                if (n > 9) n -= 9

            }

            sum += n

            alternate = !alternate

        }

        return sum % 10 == 0

    }

}

3. 3 集成 CameraX ML Kit 推理

在 ImageAnalysis.Analyzer 中,我们将每一帧图像传给 ML Kit 的识别引擎:


class CardAnalyzer(private val onCardDetected: (String) -> Unit) : ImageAnalysis.Analyzer {

    private val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

    @OptIn(ExperimentalGetImage::class)

    override fun analyze(imageProxy: ImageProxy) {

        val mediaImage = imageProxy.image

        if (mediaImage != null) {

            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

            recognizer.process(image)

                .addOnSuccessListener { visionText ->

                    // 1. 提取所有数字行

                    val rawText = visionText.text.replace(Regex("\\s"), "")

                    // 2. 使用正则过滤可能的卡号串(通常为 13-19 位)

                    val potentialNumbers = Regex("\\d{13,19}").find(rawText)?.value

                    

                    // 3. 运行 Luhn 校验

                    if (potentialNumbers != null && CardValidator.isValidLuhn(potentialNumbers)) {

                        onCardDetected(potentialNumbers)

                    }

                }

                .addOnCompleteListener {

                    imageProxy.close() // 必须关闭,否则无法接收下一帧

                }

        }

    }

}

4 进阶优化建议

为了达到你所反编译的应用那种“秒开秒扫”的体验,你还可以进行以下优化:

  1. 设置 ROI ( 感兴趣区域 ) :不要处理整张图片。在 UI 上画一个框,并在代码中通过裁剪 ImageProxy 的 buffer,只将框内的像素送入模型。这能节省约 50% 的计算量。
  2. 帧平滑( Smoothing :OCR 偶尔会跳变。建议维护一个小的 HashMap,记录最近 5 帧的识别结果。只有当某个卡号出现的频率最高且通过了 Luhn 校验时,才最终确认为结果。
  3. 硬件加速:ML Kit 默认会尝试使用 NNAPI。如果是在搭载 M4 Pro 芯片的设备或高端安卓旗舰上,确保通过 TfLiteInitializationOptions 显式开启 GPU 加速。