Android智能识别 - 实现思路

1. 实现思路

1>:新建项目,然后拷贝 jniLibs目录到项目中;
2>:配置 CMakeList文件;

2. 银行卡数字识别

如果是扫描银行卡,就需要把 银行卡放到那个扫描的方框区域中,这种情况的话:获取银行卡区域方法就可以省略;

获取银行卡区域截图:
图片.png
获取银行卡号区域截图:
图片.png
红色矩形框中的黑色属于干扰区域:
图片.png
去掉红色矩形框中的黑色干扰区域:可以看到黑色干扰区域没了
图片.png
3. 代码实现

效果:点击下图识别,就会直接识别下图的银行卡图片,然后把识别的结果显示到 识别结果位置上;

1>:activity_main布局文件:
图片.png
2>:MainActivity代码如下:
public class MainActivity extends AppCompatActivity {

    private ImageView mCardIv;
    private Bitmap mCardBitmap;
    private TextView mCardNumberTv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCardIv = findViewById(R.id.card_iv);
        mCardNumberTv = findViewById(R.id.card_number_tv);
        mCardBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.card_n);
        mCardIv.setImageBitmap(mCardBitmap);
    }


    /**
     * 点击识别,直接调用 native方法 BankCardOcr.cardOcr来识别
     */
    public void cardOcr(View view) {
        String bankNumber = BankCardOcr.cardOcr(mCardBitmap);
        mCardNumberTv.setText(bankNumber);
    }
}
3>:图像识别的 native 方法:
/**
 * ================================================
 * Email: 2185134304@qq.com
 * Created by Novate 2018/10/21 10:15
 * Version 1.0
 * Params:
 * Description:    图像识别
 * ================================================
*/

public class BankCardOcr {


    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    public static native String cardOcr(Bitmap bitmap);
}
4>:在 native-lib.cpp文件中实现 cardOcr()方法:
#include <jni.h>
#include <string>
#include "BitmapMatUtils.h"    // 导包:自己写的文件
#include <android/log.h>  // 导包:日志打印
#include "cardocr.h"
#include <vector>

using namespace std;

extern "C"
JNIEXPORT jstring JNICALL

// 图像识别
Java_com_novate_ocr_BankCardOcr_cardOcr(JNIEnv *env, jclass type, jobject bitmap) {

    // 1. bitmap 转为 mat,直接操作mat矩阵,就相当于操作的是bitmap
    Mat mat;
    BitmapMatUtils::bitmap2mat(env, bitmap, mat);

    //  轮廓增强(就是梯度增强)
    // Rect card_area;
    // co1::find_card_area(mat,card_area);
    // 对过滤到的银行卡区域进行裁剪
    // Mat card_mat(mat,card_area);
    // imwrite("/storage/emulated/0/ocr/card_n.jpg",card_mat);

    // 截取到卡号区域:方式1:找到银联区域,然后截取,精度强;方式2:直接截取卡号
    Rect card_number_area;
    co1::find_card_number_area(mat,card_number_area);
    Mat card_number_mat(mat,card_number_area);
    // 把卡号区域的card_number_mat 保存到内存卡中
    imwrite("/storage/emulated/0/ocr/card_number_n.jpg",card_number_mat);

    // 获取数字
    vector<Mat> numbers;
    co1::find_card_numbers(card_number_mat,numbers);


    return env->NewStringUTF("622848");
}
5>:bitmap和mat互相转换的工具类:

BitmapMatUtils.h是头文件,只是用于声明方法;
BitmapMatUtils.cpp:用于对 .h头文件的实现;

BitmapMatUtils.h代码如下:
#ifndef NDK_DAY31_AS_BITMAPMATUTILS_H
#define NDK_DAY31_AS_BITMAPMATUTILS_H

#include <jni.h>  // 导包
#include "opencv2/opencv.hpp"  // 导包
using namespace cv;  // 使用命名空间

// 工具类 bitmap、mat互相转换
class BitmapMatUtils {
public:

    // 返回int的原因:

    // java中: 一般是 把想要的结果返回
    // c/c++中:一般是 把结果作为参数传递,返回值一般都是成功或失败的错误码
    static int bitmap2mat(JNIEnv* env , jobject bitmap , Mat &mat) ;  // &mat:引用,可看做指针

    // mat - bitmap
    static int mat2bitmap(JNIEnv* env , jobject bitmap , Mat &mat) ;
};

#endif //NDK_DAY31_AS_BITMAPMATUTILS_H
BitmapMatUtils.cpp代码如下:
#include "BitmapMatUtils.h"  // 导包
#include <android/bitmap.h>  // 导包

//--------------- BitmapMatUtils.cpp 用于对BitmapMatUtils.h文件进行实现 -----------------//

// 操作mat和操作bitmap一样
// bitmap 转为 mat
int BitmapMatUtils::bitmap2mat(JNIEnv *env, jobject bitmap, Mat &mat) {
    // 1. 锁定画布,把头指针拿到
    void *pixels ;
    AndroidBitmap_lockPixels(env , bitmap , &pixels) ;

    // 构建 mat 对象,还需要判断是几颜色通道 0-255
    // 获取 bitmap 的信息
    AndroidBitmapInfo bitmapInfo ;

    AndroidBitmap_getInfo(env , bitmap , &bitmapInfo) ;


    // 返回三通道 CV_8UC4对应argb CV_8UC2对应rgb CV_8UC1对应黑白
    // 重新创建mat
    // 返回三通道的 rgb
    Mat createMat(bitmapInfo.height , bitmapInfo.width , CV_8UC4) ;

    // 判断bitmapInfo 的格式
    if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888){  // argb 对应 mat 中的四颜色通道 CV_8UC4
        // 创建临时 mat
        Mat temp(bitmapInfo.height , bitmapInfo.width , CV_8UC4 , pixels) ;

        temp.copyTo(createMat) ;
    }else if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGB_565){ // argb 对应 mat 三颜色 CV_8UC2
        Mat temp(bitmapInfo.height , bitmapInfo.width , CV_8UC2 , pixels) ;
        cvtColor(temp , createMat , COLOR_BGR5652BGRA) ;
    }

    createMat.copyTo(mat) ;

    // 2. 解锁画布 , 对应锁定画布
    AndroidBitmap_unlockPixels(env , bitmap) ;

    // 返回0表示成功
    return 0;
}


// mat 转为 bitmap
int BitmapMatUtils::mat2bitmap(JNIEnv *env, jobject bitmap, Mat &mat) {

}
6>:创建 图像识别cardocr.h文件,在里边创建几个方法,仅用于声明:

方法1:找到银行卡区域;
方法2:通过银行卡区域截取到卡号区域;
方法3:找到所有的数字;
方法4:对字符串进行粘连处理;



#ifndef NDK_DAY31_AS_CARDOCR_H
#define NDK_DAY31_AS_CARDOCR_H

#include "opencv2/opencv.hpp"
#include <vector>

using namespace cv;

// 一般会针对不同的场景,做不同的事情:比如拍照或者相册选择的银行卡
// 使用命名空间
namespace co1 {

    /**
     * 找到银行卡区域
     * @param mat 图片的 mat
     * @param area 银行卡卡号区域
     * @return 返回是否成功 0 成功,其他失败
     */
    int find_card_area(const Mat &mat, Rect &area);

    /**
     * 通过银行卡区域截取到卡号区域
     * @param mat 银行卡的 mat矩阵
     * @param area 存放卡号的截取区域
     * @return 是否成功
     */
    int find_card_number_area(const Mat &mat, Rect &area);

    /**
     * 找到所有的数字
     * @param mat 银行卡号区域
     * @param numbers 存放所有数字
     * @return 是否成功
     */
    int find_card_numbers(const Mat &mat, std::vector<Mat> numbers);

    /**
     * 字符串进行粘连处理
     * @param mat
     * @return 返回粘连的那一列
     */
    int find_split_cols_pos(Mat mat);
}

/**
 * 第二套方案:备选方案
 */
namespace co2 {

}

#endif //NDK_DAY31_AS_CARDOCR_H
7>:创建 cardocr.cpp文件,用于实现 cardocr.h文件中定义的方法:
#include "cardocr.h"
#include <vector>
#include <android/log.h>

using namespace std;

/**
 * 实现:获取银行卡区域方法:
        const:表示常量引用,mat常量不能修改
        &:表示传递引用,防止重复创建对象
*/
int co1::find_card_area(const Mat &mat, Rect &area) {

    // 第一步:首先降噪 - 其实就是 高斯模糊,对边缘进行高斯模糊
    Mat blur ;
    GaussianBlur(mat , blur , Size(5,5) , BORDER_DEFAULT , BORDER_DEFAULT) ;

    // 第二步:边缘梯度的增强(保存图片,用imwrite保存图片,用于查看)- 其实就是对 x,y 梯度的增强,比较耗时
    // 有2种方法进行梯度增强:第一个Scharr增强,第二个索贝尔增强
    Mat gard_x , gard_y;

    // 这里用Scharr增强,
    // 对X轴增强
    Scharr(blur , gard_x , CV_32F , 1 , 0) ;
    // 对y轴增强
    Scharr(blur , gard_y , CV_32F , 0 , 1) ;

    // 对gard_x和gard_y 取绝对值,因为梯度增强后可能会变为负值
    Mat abs_gard_x , abs_gard_y ;

    // 转换,取绝对值
    convertScaleAbs(gard_x, abs_gard_x);
    convertScaleAbs(gard_y, abs_gard_y);

    Mat gard ;
    addWeighted(abs_gard_x , 0.5 , abs_gard_y , 0.5 , 0 , gard) ;

    // 第三步:二值化,进行轮廓筛选
    Mat gray ;
    cvtColor(gard , gray , COLOR_BGRA2GRAY) ;
    Mat binary ;
    threshold(gray , binary , 40 , 255 , THRESH_BINARY) ;

    // card.io
    vector<vector<Point> > contours ; // vector集合,
    // 查找所有轮廓
    findContours(binary , contours , RETR_EXTERNAL , CHAIN_APPROX_SIMPLE) ;
    // 遍历所有的轮廓
    for(int i=0 ; i<contours.size() ; i++){

        Rect rect = boundingRect(contours[i]) ;
        // 对轮廓进行过滤
        if(rect.width > mat.cols/2 && rect.width != mat.cols && rect.height > mat.rows/2){
            // 银行卡区域的宽高必须大于图片的一半
            area = rect ;
            break;
        }
    }

    // 这里没有对返回值 area进行成功或失败的处理,

    // 释放资源:看有没有动态开辟内存,有没有 new 对象
    // mat 数据类提供了释放函数,一般要调用,用于释放内存
    blur.release() ;
    gard_x.release() ;
    gard_y.release() ;
    abs_gard_x.release();
    abs_gard_y.release();
    gard.release();
    binary.release();

    return 0;
}

// 获取银行卡数字区域
int co1::find_card_number_area(const Mat &mat, Rect &area) {
    // 有两种方式:一种是精确截取,找到银联区域通过大小比对精确的截取
    // 一种是粗略截取,截取高度 1/2 - 3/4 , 宽度 1/12 - 11/12 , 一般采用这种
    // 万一找不到银行卡数字,可以手动的输入和修改,比如支付宝
    area.x = mat.cols / 12;
    area.y = mat.rows / 2;
    area.width = mat.cols * 5 / 6;
    area.height = mat.rows / 4;
    return 0;
}


// 获取银行卡数字
int co1::find_card_numbers(const Mat &mat, std::vector<Mat> numbers) {

    // 第一步:对所有数字二值化,由于一张图片如果是彩色的,它本身带有的信息太大,这里需要对其进行灰度处理,
    Mat gray ;
    ctvColor(mat , gray , COLOR_BGRA2GRAY) ;
    // THRESH_OTSU THRESH_TRIANGLE 自己去找合适的值
    Mat binary ;
    threshold(gray , binary , 39 , 255 , THRESH_BINARY) ;
    imwrite("/storage/emulated/0/ocr/card_number_binary_n.jpg", binary);

    // 降噪过滤,用getStructuringElement()方法和高斯模糊都可以
    Mat kernel = getStructuringElement(MORPH_RECT , Size(3,3)) ; // 参数1:过滤的区域,参数2:过滤的面积
    morphologyEx(binary , binary , MORPH_CLOSE , kernel) ;


    // 去掉干扰,其实就是去掉数字周围的一些干扰数字的区域,实现方式就是把干扰区域填充为白色,
    // 找数字就是轮廓查询 (card.io 16位 ,19位)
    // 查找轮廓 白色轮廓binary  黑色背景 白色数字
    // 取反 白黑 - 黑白
    Mat binary_not = binary.clone();
    bitwise_not(binary_not , binary_not) ;

    imwrite("/storage/emulated/0/ocr/card_number_binary_not_n.jpg", binary_not);

    vector<vector<Point>> contours ;
    findContours(binary_not , contours , RETR_EXTERNAL , CHAIN_APPROX_SIMPLE) ;

    int mat_area = mat.rows * mat.cols ;
    // 最小高度
    int min_h = mat.rows/4;

    // 过滤银行卡号数字黑色干扰区域
    for(int i=0 ; i < contours.size() ; i++){
        Rect rect = boundingRect(contours[i]) ;
        // 多个条件,面积太小的过滤掉
        int area = rect.width*rect.height;
        if(area < mar_area / 200){
            // 把小面积填充为白色,其实就是把 黑色的干扰区域填充为白色
            drawContours(binary , contours , i , Scalar(255) , -1) ;
        }else if(rect.height < min_h){
            drawContours(binary , contours , i , Scalar(255) , -1) ;
        }
    }


    imwrite("/storage/emulated/0/ocr/card_number_binary_noise_n.jpg", binary);


    //---------------------- 以上是去掉银行卡号数字黑色干扰区域 ----------------------------//

    // 下边就是 截取每个数字的轮廓

    // 截取每个数字的轮廓,binary(是没有噪音,不行的),binary_not(有噪音)
    binary.copyTo(binary_not) ;

    // 取反
    bitwise_not(binary_not , binary_not) ;  // 没有噪音的binary_not


    contours.clear();
    findContours(binary_not , contours , RETR_EXTERNAL , CHAIN_APPROX_SIMPLE) ;
    // 先把 Rect 存起来,查找有可能会出现顺序错乱,还有可能出现粘连字符
    Rect rects[contours.size()] ;

    // 白色的图片,单颜色
    Mat contours_mat(binary.size() , CV_8UC1 , Scalar(255));

    // 判断粘连字符
    int min_w = mat.cols ;
    for(int i=0 ; i<contours.size() ; i++){
        rects[i] = boundingRect(contours[i]) ;
        drawContours(contours_mat , contours , i , Scalar(0) , 1) ;
        min_w = min(rects[i].width , min_w) ;
    }

    imwrite("/storage/emulated/0/ocr/card_number_contours_mat_n.jpg", contours_mat);

    // 用冒泡排序
    for(int i = 0 ; i < contours.size() ; i++){
        for(int j=0 ; j<contours.size() -i - 1 ; ++j){
            if(rects[j].x > rects[j+1].x){
                // 交换
                swap(rects[j] , rects[j+1]) ;
            }
        }
    }

    // 裁剪
    numbers.clear() ;


    for(int i = 0 ; i<contours.size() ; i++){
        // 粘连字符最小宽度的2倍
        if(rects[i].width >= min_h*2){
            // 处理粘连字符
            Mat mat(contours_mat , rects[i]) ;
            int cols_pos = col::find_split_cols_pos(mat) ;

            // 把粘连左右两个数字都存储进去
            Rect rect_left(0 , 0 , cols_pos-1 , mat.rows) ;
            numbers.push_back(Mat(mat , rect_left)) ;

            Rect rect_right(cols_pos , 0 , mat.cols , mat.rows) ;
            numbers.push_back(Mat(mat , rect_right)) ;
        }else {
            Mat number(contours_mat , rects[i]) ;
            numbers.push_back(number) ;

            // 保存数字
            char name[50] ;
            sprintf(name, "/storage/emulated/0/ocr/card_number_%d.jpg", i);
            imwrite(name, number);
        }
    }

    // 释放资源
    gray.release() ;
    binary.release() ;
    binary_not.release();
    contours_mat.release() ;

    return 0;
}

// 处理粘连字符
int co1::find_split_cols_pos(Mat mat) {
    // 对粘连字符中心位置1/4左右 进行扫描,记录下最少的黑色像素点的这一列的位置
    // 就把它 当做字符串的粘连位置

    // 中心位置
    int mx = mat.cols / 2 ;
    // 高度
    int height = mat.rows ;

    // 围绕中心扫描 1/4
    int start_x = mx - mx/2 ;
    int end_x = mx + mx/2 ;

    // 首先判断:字符的粘连位置
    int cols_pos = mx ;
    // 获取像素,要么是0,要么是255
    int c = 0 ;
    // 最小的像素值 = 最大值
    int min_h_p = mat.rows ;


    for(int col = start_x ; col<end_x ; ++col ){
        int total = 0 ;
        for(int rows = 0 ; row < height ; ++row){
            // 获取像素点
            c = mat.at<Vec3b>(row , col)[0] ;  // 单通道
            if(c == 0){
                total++;
            }
        }

        // 比对
        if(total < min_h_p){
            min_h_p = total ;
            cols_pos = cols ;
        }
    }

    // 返回列
    return cols_pos;
}
8>:CMakeList配置文件如下:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
#判断编译器类型,如果是gcc编译器,则在编译选项中加入c++11支持
if(CMAKE_COMPILER_IS_GNUCXX)
    set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")
    message(STATUS "optional:-std=c++11")
endif(CMAKE_COMPILER_IS_GNUCXX)

#需要引入头文件,以这个配置的目录为基准
include_directories(src/main/jniLibs/include)

# 添加依赖 opencv.so 库
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../src/main/jniLibs)
add_library(
            opencv_java3
            SHARED
            IMPORTED)
set_target_properties(
            opencv_java3
            PROPERTIES IMPORTED_LOCATION
            ../../../../src/main/jniLibs/armeabi/libopencv_java3.so)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

# 这里需要对所有的 cpp实现文件进行配置
             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp src/main/cpp/BitmapMatUtils.cpp
             src/main/cpp/cardocr.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib opencv_java3

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

推荐阅读更多精彩内容