1. 实现思路
1>:新建项目,然后拷贝 jniLibs目录到项目中;
2>:配置 CMakeList文件;
2. 银行卡数字识别
如果是扫描银行卡,就需要把 银行卡放到那个扫描的方框区域中,这种情况的话:获取银行卡区域方法就可以省略;
获取银行卡区域截图:
获取银行卡号区域截图:
红色矩形框中的黑色属于干扰区域:
去掉红色矩形框中的黑色干扰区域:可以看到黑色干扰区域没了
3. 代码实现
效果:点击下图识别,就会直接识别下图的银行卡图片,然后把识别的结果显示到 识别结果位置上;
1>:activity_main布局文件:
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} )