1. 實(shí)現(xiàn)思路
1>:新建項(xiàng)目,然后拷貝 jniLibs目錄到項(xiàng)目中盅抚;
2>:配置 CMakeList文件;
2. 銀行卡數(shù)字識(shí)別
如果是掃描銀行卡,就需要把 銀行卡放到那個(gè)掃描的方框區(qū)域中玫芦,這種情況的話:獲取銀行卡區(qū)域方法就可以省略;
獲取銀行卡區(qū)域截圖:
圖片.png
獲取銀行卡號(hào)區(qū)域截圖:
圖片.png
紅色矩形框中的黑色屬于干擾區(qū)域:
圖片.png
去掉紅色矩形框中的黑色干擾區(qū)域:可以看到黑色干擾區(qū)域沒(méi)了
圖片.png
3. 代碼實(shí)現(xiàn)
效果:點(diǎn)擊下圖識(shí)別本辐,就會(huì)直接識(shí)別下圖的銀行卡圖片桥帆,然后把識(shí)別的結(jié)果顯示到 識(shí)別結(jié)果位置上;
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);
}
/**
* 點(diǎn)擊識(shí)別慎皱,直接調(diào)用 native方法 BankCardOcr.cardOcr來(lái)識(shí)別
*/
public void cardOcr(View view) {
String bankNumber = BankCardOcr.cardOcr(mCardBitmap);
mCardNumberTv.setText(bankNumber);
}
}
3>:圖像識(shí)別的 native 方法:
/**
* ================================================
* Email: 2185134304@qq.com
* Created by Novate 2018/10/21 10:15
* Version 1.0
* Params:
* Description: 圖像識(shí)別
* ================================================
*/
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文件中實(shí)現(xiàn) cardOcr()方法:
#include <jni.h>
#include <string>
#include "BitmapMatUtils.h" // 導(dǎo)包:自己寫(xiě)的文件
#include <android/log.h> // 導(dǎo)包:日志打印
#include "cardocr.h"
#include <vector>
using namespace std;
extern "C"
JNIEXPORT jstring JNICALL
// 圖像識(shí)別
Java_com_novate_ocr_BankCardOcr_cardOcr(JNIEnv *env, jclass type, jobject bitmap) {
// 1. bitmap 轉(zhuǎn)為 mat老虫,直接操作mat矩陣,就相當(dāng)于操作的是bitmap
Mat mat;
BitmapMatUtils::bitmap2mat(env, bitmap, mat);
// 輪廓增強(qiáng)(就是梯度增強(qiáng))
// Rect card_area;
// co1::find_card_area(mat,card_area);
// 對(duì)過(guò)濾到的銀行卡區(qū)域進(jìn)行裁剪
// Mat card_mat(mat,card_area);
// imwrite("/storage/emulated/0/ocr/card_n.jpg",card_mat);
// 截取到卡號(hào)區(qū)域:方式1:找到銀聯(lián)區(qū)域茫多,然后截取祈匙,精度強(qiáng);方式2:直接截取卡號(hào)
Rect card_number_area;
co1::find_card_number_area(mat,card_number_area);
Mat card_number_mat(mat,card_number_area);
// 把卡號(hào)區(qū)域的card_number_mat 保存到內(nèi)存卡中
imwrite("/storage/emulated/0/ocr/card_number_n.jpg",card_number_mat);
// 獲取數(shù)字
vector<Mat> numbers;
co1::find_card_numbers(card_number_mat,numbers);
return env->NewStringUTF("622848");
}
5>:bitmap和mat互相轉(zhuǎn)換的工具類(lèi):
BitmapMatUtils.h是頭文件天揖,只是用于聲明方法夺欲;
BitmapMatUtils.cpp:用于對(duì) .h頭文件的實(shí)現(xiàn);
BitmapMatUtils.h代碼如下:
#ifndef NDK_DAY31_AS_BITMAPMATUTILS_H
#define NDK_DAY31_AS_BITMAPMATUTILS_H
#include <jni.h> // 導(dǎo)包
#include "opencv2/opencv.hpp" // 導(dǎo)包
using namespace cv; // 使用命名空間
// 工具類(lèi) bitmap今膊、mat互相轉(zhuǎn)換
class BitmapMatUtils {
public:
// 返回int的原因:
// java中: 一般是 把想要的結(jié)果返回
// c/c++中:一般是 把結(jié)果作為參數(shù)傳遞些阅,返回值一般都是成功或失敗的錯(cuò)誤碼
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" // 導(dǎo)包
#include <android/bitmap.h> // 導(dǎo)包
//--------------- BitmapMatUtils.cpp 用于對(duì)BitmapMatUtils.h文件進(jìn)行實(shí)現(xiàn) -----------------//
// 操作mat和操作bitmap一樣
// bitmap 轉(zhuǎn)為 mat
int BitmapMatUtils::bitmap2mat(JNIEnv *env, jobject bitmap, Mat &mat) {
// 1. 鎖定畫(huà)布斑唬,把頭指針拿到
void *pixels ;
AndroidBitmap_lockPixels(env , bitmap , &pixels) ;
// 構(gòu)建 mat 對(duì)象市埋,還需要判斷是幾顏色通道 0-255
// 獲取 bitmap 的信息
AndroidBitmapInfo bitmapInfo ;
AndroidBitmap_getInfo(env , bitmap , &bitmapInfo) ;
// 返回三通道 CV_8UC4對(duì)應(yīng)argb CV_8UC2對(duì)應(yīng)rgb CV_8UC1對(duì)應(yīng)黑白
// 重新創(chuàng)建mat
// 返回三通道的 rgb
Mat createMat(bitmapInfo.height , bitmapInfo.width , CV_8UC4) ;
// 判斷bitmapInfo 的格式
if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888){ // argb 對(duì)應(yīng) mat 中的四顏色通道 CV_8UC4
// 創(chuàng)建臨時(shí) mat
Mat temp(bitmapInfo.height , bitmapInfo.width , CV_8UC4 , pixels) ;
temp.copyTo(createMat) ;
}else if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGB_565){ // argb 對(duì)應(yīng) mat 三顏色 CV_8UC2
Mat temp(bitmapInfo.height , bitmapInfo.width , CV_8UC2 , pixels) ;
cvtColor(temp , createMat , COLOR_BGR5652BGRA) ;
}
createMat.copyTo(mat) ;
// 2. 解鎖畫(huà)布 , 對(duì)應(yīng)鎖定畫(huà)布
AndroidBitmap_unlockPixels(env , bitmap) ;
// 返回0表示成功
return 0;
}
// mat 轉(zhuǎn)為 bitmap
int BitmapMatUtils::mat2bitmap(JNIEnv *env, jobject bitmap, Mat &mat) {
}
6>:創(chuàng)建 圖像識(shí)別cardocr.h文件,在里邊創(chuàng)建幾個(gè)方法恕刘,僅用于聲明:
方法1:找到銀行卡區(qū)域缤谎;
方法2:通過(guò)銀行卡區(qū)域截取到卡號(hào)區(qū)域;
方法3:找到所有的數(shù)字褐着;
方法4:對(duì)字符串進(jìn)行粘連處理坷澡;
#ifndef NDK_DAY31_AS_CARDOCR_H
#define NDK_DAY31_AS_CARDOCR_H
#include "opencv2/opencv.hpp"
#include <vector>
using namespace cv;
// 一般會(huì)針對(duì)不同的場(chǎng)景,做不同的事情:比如拍照或者相冊(cè)選擇的銀行卡
// 使用命名空間
namespace co1 {
/**
* 找到銀行卡區(qū)域
* @param mat 圖片的 mat
* @param area 銀行卡卡號(hào)區(qū)域
* @return 返回是否成功 0 成功含蓉,其他失敗
*/
int find_card_area(const Mat &mat, Rect &area);
/**
* 通過(guò)銀行卡區(qū)域截取到卡號(hào)區(qū)域
* @param mat 銀行卡的 mat矩陣
* @param area 存放卡號(hào)的截取區(qū)域
* @return 是否成功
*/
int find_card_number_area(const Mat &mat, Rect &area);
/**
* 找到所有的數(shù)字
* @param mat 銀行卡號(hào)區(qū)域
* @param numbers 存放所有數(shù)字
* @return 是否成功
*/
int find_card_numbers(const Mat &mat, std::vector<Mat> numbers);
/**
* 字符串進(jìn)行粘連處理
* @param mat
* @return 返回粘連的那一列
*/
int find_split_cols_pos(Mat mat);
}
/**
* 第二套方案:備選方案
*/
namespace co2 {
}
#endif //NDK_DAY31_AS_CARDOCR_H
7>:創(chuàng)建 cardocr.cpp文件洋访,用于實(shí)現(xiàn) cardocr.h文件中定義的方法:
#include "cardocr.h"
#include <vector>
#include <android/log.h>
using namespace std;
/**
* 實(shí)現(xiàn):獲取銀行卡區(qū)域方法:
const:表示常量引用,mat常量不能修改
&:表示傳遞引用谴餐,防止重復(fù)創(chuàng)建對(duì)象
*/
int co1::find_card_area(const Mat &mat, Rect &area) {
// 第一步:首先降噪 - 其實(shí)就是 高斯模糊姻政,對(duì)邊緣進(jìn)行高斯模糊
Mat blur ;
GaussianBlur(mat , blur , Size(5,5) , BORDER_DEFAULT , BORDER_DEFAULT) ;
// 第二步:邊緣梯度的增強(qiáng)(保存圖片,用imwrite保存圖片岂嗓,用于查看)- 其實(shí)就是對(duì) x,y 梯度的增強(qiáng)汁展,比較耗時(shí)
// 有2種方法進(jìn)行梯度增強(qiáng):第一個(gè)Scharr增強(qiáng),第二個(gè)索貝爾增強(qiáng)
Mat gard_x , gard_y;
// 這里用Scharr增強(qiáng),
// 對(duì)X軸增強(qiáng)
Scharr(blur , gard_x , CV_32F , 1 , 0) ;
// 對(duì)y軸增強(qiáng)
Scharr(blur , gard_y , CV_32F , 0 , 1) ;
// 對(duì)gard_x和gard_y 取絕對(duì)值食绿,因?yàn)樘荻仍鰪?qiáng)后可能會(huì)變?yōu)樨?fù)值
Mat abs_gard_x , abs_gard_y ;
// 轉(zhuǎn)換侈咕,取絕對(duì)值
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) ;
// 第三步:二值化,進(jìn)行輪廓篩選
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]) ;
// 對(duì)輪廓進(jìn)行過(guò)濾
if(rect.width > mat.cols/2 && rect.width != mat.cols && rect.height > mat.rows/2){
// 銀行卡區(qū)域的寬高必須大于圖片的一半
area = rect ;
break;
}
}
// 這里沒(méi)有對(duì)返回值 area進(jìn)行成功或失敗的處理耀销,
// 釋放資源:看有沒(méi)有動(dòng)態(tài)開(kāi)辟內(nèi)存,有沒(méi)有 new 對(duì)象
// mat 數(shù)據(jù)類(lèi)提供了釋放函數(shù)铲汪,一般要調(diào)用熊尉,用于釋放內(nèi)存
blur.release() ;
gard_x.release() ;
gard_y.release() ;
abs_gard_x.release();
abs_gard_y.release();
gard.release();
binary.release();
return 0;
}
// 獲取銀行卡數(shù)字區(qū)域
int co1::find_card_number_area(const Mat &mat, Rect &area) {
// 有兩種方式:一種是精確截取,找到銀聯(lián)區(qū)域通過(guò)大小比對(duì)精確的截取
// 一種是粗略截取掌腰,截取高度 1/2 - 3/4 , 寬度 1/12 - 11/12 , 一般采用這種
// 萬(wàn)一找不到銀行卡數(shù)字狰住,可以手動(dòng)的輸入和修改,比如支付寶
area.x = mat.cols / 12;
area.y = mat.rows / 2;
area.width = mat.cols * 5 / 6;
area.height = mat.rows / 4;
return 0;
}
// 獲取銀行卡數(shù)字
int co1::find_card_numbers(const Mat &mat, std::vector<Mat> numbers) {
// 第一步:對(duì)所有數(shù)字二值化齿梁,由于一張圖片如果是彩色的催植,它本身帶有的信息太大,這里需要對(duì)其進(jìn)行灰度處理勺择,
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);
// 降噪過(guò)濾创南,用getStructuringElement()方法和高斯模糊都可以
Mat kernel = getStructuringElement(MORPH_RECT , Size(3,3)) ; // 參數(shù)1:過(guò)濾的區(qū)域,參數(shù)2:過(guò)濾的面積
morphologyEx(binary , binary , MORPH_CLOSE , kernel) ;
// 去掉干擾省核,其實(shí)就是去掉數(shù)字周?chē)囊恍└蓴_數(shù)字的區(qū)域稿辙,實(shí)現(xiàn)方式就是把干擾區(qū)域填充為白色,
// 找數(shù)字就是輪廓查詢 (card.io 16位 芳撒,19位)
// 查找輪廓 白色輪廓binary 黑色背景 白色數(shù)字
// 取反 白黑 - 黑白
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;
// 過(guò)濾銀行卡號(hào)數(shù)字黑色干擾區(qū)域
for(int i=0 ; i < contours.size() ; i++){
Rect rect = boundingRect(contours[i]) ;
// 多個(gè)條件邓深,面積太小的過(guò)濾掉
int area = rect.width*rect.height;
if(area < mar_area / 200){
// 把小面積填充為白色未桥,其實(shí)就是把 黑色的干擾區(qū)域填充為白色
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);
//---------------------- 以上是去掉銀行卡號(hào)數(shù)字黑色干擾區(qū)域 ----------------------------//
// 下邊就是 截取每個(gè)數(shù)字的輪廓
// 截取每個(gè)數(shù)字的輪廓笔刹,binary(是沒(méi)有噪音,不行的)冬耿,binary_not(有噪音)
binary.copyTo(binary_not) ;
// 取反
bitwise_not(binary_not , binary_not) ; // 沒(méi)有噪音的binary_not
contours.clear();
findContours(binary_not , contours , RETR_EXTERNAL , CHAIN_APPROX_SIMPLE) ;
// 先把 Rect 存起來(lái)舌菜,查找有可能會(huì)出現(xiàn)順序錯(cuò)亂,還有可能出現(xiàn)粘連字符
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) ;
// 把粘連左右兩個(gè)數(shù)字都存儲(chǔ)進(jìn)去
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) ;
// 保存數(shù)字
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) {
// 對(duì)粘連字符中心位置1/4左右 進(jìn)行掃描日月,記錄下最少的黑色像素點(diǎn)的這一列的位置
// 就把它 當(dāng)做字符串的粘連位置
// 中心位置
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){
// 獲取像素點(diǎn)
c = mat.at<Vec3b>(row , col)[0] ; // 單通道
if(c == 0){
total++;
}
}
// 比對(duì)
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")
#判斷編譯器類(lèi)型,如果是gcc編譯器,則在編譯選項(xiàng)中加入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)
#需要引入頭文件,以這個(gè)配置的目錄為基準(zhǔn)
include_directories(src/main/jniLibs/include)
# 添加依賴 opencv.so 庫(kù)
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
# 這里需要對(duì)所有的 cpp實(shí)現(xiàn)文件進(jìn)行配置
# 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
#加入該依賴庫(kù)
jnigraphics
# Links the target library to the log library
# included in the NDK.
${log-lib} )