Android智慧識別
在真正開始看這篇文章之前,希望我們能先去了解一下這篇文章《NDK開發前奏 - 實現支付寶人臉識別功能》,此篇文章閱讀起來可能會有些許難度,因此我們只要能看懂 c/c++ 語法,能跟上我分析問題的思路就可以了。後面我們會講一些演算法,會去介紹 opencv 的常用函式。當然我們學習 Ndk 主要還是為我們的 Android 來服務的,以便我們能讀懂 android 底層原始碼,能做一些 Ndk 專案開發。
今天下班回家特意試了一下支付寶,用支付寶掃描了一下我的銀行卡效果不是很佳,當然我相信大多數情況下是正常的。我這麼講只是想說明智慧識別本就用一定的侷限性,並不是 100% 非常正確。下面是支付寶的識別效果,上個圖 (少了一位,錯了兩位):
像身份證、銀行卡和人臉識別這些,一般網上都有技術支援的(部分需要付費),我們能不自己折騰的儘量不要自己折騰,但瞭解其原理,知道修改其原始碼還是有必要的。接下來我們來分析一下,且看我下面這兩張銀行卡,卡是真不少就是沒錢。
我們想要去識別銀行卡其實思路也不難,只要找到他們的共性和特色就可以了。且看我下面畫的這張圖:
有幾個特徵,最外層是銀行卡區域,左上角一般是 logo 和銀行名稱標識,右下角是銀聯區域,中間是我們要識別的卡號區域。有了這幾個特徵我們就有了思路了:
1. 篩選過濾擷取銀行卡區域;
2. 根據標識篩選擷取銀行卡號區域;
3. 二值分析與特徵提取;
4. 數字分割識別顯示。
1.篩選過濾擷取銀行卡區域
opencv 操作的是 mat 型別,我們拍照獲取的是 Bitmap 型別,後面我們會獲取每一幀進行處理,原理和套路都是一致的,由易到難,先來簡單點的。所以第一個要寫的方法就是 Bitmap 和 Mat 相互轉換:
void MatBitmapUtil::bitmap2mat(JNIEnv *env, Mat &mat, jobject &bitmap) {
// 鎖定畫布 獲取首地址畫素
void* pixels = 0;
AndroidBitmap_lockPixels(env,bitmap,&pixels);
// 獲取資訊判斷格式
AndroidBitmapInfo *bitmapInfo = new AndroidBitmapInfo();
AndroidBitmap_getInfo(env,bitmap,bitmapInfo);
mat.create(bitmapInfo->height,bitmapInfo->width,CV_8UC4);
if(bitmapInfo->format == ANDROID_BITMAP_FORMAT_RGBA_8888){
// argb
Mat temp(bitmapInfo->height,bitmapInfo->width,CV_8UC4,pixels);
temp.copyTo(mat);
} else if(bitmapInfo->format == ANDROID_BITMAP_FORMAT_RGB_565){
// rgb
Mat temp(bitmapInfo->height,bitmapInfo->width,CV_8UC2,pixels);
temp.copyTo(mat,COLOR_BGR5652BGRA);
}
// 其他要自己去轉
// 解鎖畫布
AndroidBitmap_unlockPixels(env,bitmap);
delete(bitmapInfo);
}
void MatBitmapUtil::mat2Bitmap(JNIEnv *env, Mat &mat, jobject &bitmap) {
// 1. 獲取 bitmap 資訊
AndroidBitmapInfo info;
void* pixels;
AndroidBitmap_getInfo(env,bitmap,&info);
// 鎖定 Bitmap 畫布
AndroidBitmap_lockPixels(env,bitmap,&pixels);
if(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888){// C4
Mat temp(info.height,info.width,CV_8UC4,pixels);
if(mat.type() == CV_8UC4){
mat.copyTo(temp);
}
else if(mat.type() == CV_8UC2){
cvtColor(mat,temp,COLOR_BGR5652BGRA);
}
else if(mat.type() == CV_8UC1){// 灰度 mat
cvtColor(mat,temp,COLOR_GRAY2BGRA);
}
} else if(info.format == ANDROID_BITMAP_FORMAT_RGB_565){// C2
Mat temp(info.height,info.width,CV_8UC2,pixels);
if(mat.type() == CV_8UC4){
cvtColor(mat,temp,COLOR_BGRA2BGR565);
}
else if(mat.type() == CV_8UC2){
mat.copyTo(temp);
}
else if(mat.type() == CV_8UC1){// 灰度 mat
cvtColor(mat,temp,COLOR_GRAY2BGR565);
}
}
// 其他要自己去轉
// 解鎖 Bitmap 畫布
AndroidBitmap_unlockPixels(env,bitmap);
}
Bitmap 轉 Mat 之後我們就可以用 opencv 去操作了,我們第一步是要找到銀行卡區域,然後進行擷取儲存。所以我們首先需要對輪廓進行梯度增強,然後對圖片進行二值化輪廓查詢,篩選出最符合的區域。
Rect co1::find_card_area(const Mat &mat) {
// 首先降噪
Mat blur;
GaussianBlur(mat, blur, Size(5, 5), BORDER_DEFAULT, BORDER_DEFAULT);
// 梯度增強 , x 軸和 y 軸
Mat grad_x, grad_y;
Scharr(blur, grad_x, CV_32F, 1, 0);
Scharr(blur, grad_y, CV_32F, 0, 1);
Mat grad_abs_x, grad_abs_y;
convertScaleAbs(grad_x, grad_abs_x);
convertScaleAbs(grad_y, grad_abs_y);
Mat grad;
addWeighted(grad_abs_x, 0.5, grad_abs_y, 0.5, 0, grad);
imwrite("/storage/emulated/0/ocr/grad_n.jpg",grad);
// 二值化,進行輪廓查詢
Mat gray;
cvtColor(grad, gray, COLOR_BGRA2GRAY);
Mat binary;
threshold(gray, binary, 40, 255, THRESH_BINARY);
imwrite("/storage/emulated/0/ocr/binary_n.jpg",binary);
// 輪廓查詢
vector<vector<Point> > contours;
findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Rect card_rect;
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) {
card_rect = rect;
break;
}
}
// release source
blur.release();
grad_x.release();
grad_y.release();
grad_abs_x.release();
grad_abs_y.release();
grad.release();
gray.release();
binary.release();
// return card rect
return card_rect;
}
2. 根據標識篩選擷取銀行卡號區域
擷取到銀行卡區域後,我們就可以根據我們的銀行卡標識,去篩選擷取我們的卡號區域。
JNIEXPORT jint JNICALL
Java_com_darren_ndk_day05_FaceDetection_bankOcr(JNIEnv *env, jobject instance,
jobject bitmap) {
// Bitmap 轉成 opencv 能操作的 C++ 物件 Mat , Mat 是一個矩陣
Mat mat;
MatBitmapUtil::bitmap2mat(env, mat, bitmap);
// 1.篩選過濾擷取銀行卡區域
Rect card_rect = co1::find_card_area(mat);
Mat card_mat(mat,card_rect);
// 2. 根據標識篩選擷取銀行卡號區域;
Rect card_number_rect = co1::find_card_number_area(mat);
Mat card_number_mat(card_mat,card_number_mat);
// 3. 寫入檔案看一看
imwrite("/storage/emulated/0/ocr/card_number.jpg",card_mat);
LOGE("處理完畢");
return 0;
}
這其實才剛剛開始,假設光線不強呢?很多銀行卡沒有銀聯的標識,又或者某些銀行卡的干擾太多。後面的文章我們將陸續去完善,直到可以獲取相機的每一幀進行處理。