移動端車牌識別OCR,結合OpenCV

原文鏈接:https://juejin.im/post/5d4260cd6fb9a06af92b827b

移動端車牌識別OCR,結合OpenCV【原文地址】


需求

最近產品那邊說APP上要加個車牌識別的功能,用戶不用手動輸入,我說沒問題啊加就加唄。腦子中第一反應就是第三方SDK,最終用了百度車牌識別SDK,完成了需求,集成方法詳見“百度SDK文檔”,好了文章到這裏在可以結束了。 文章要是真結束了,提刀的兄弟估計又要砍我了,標題黨一個,老是做這些脫褲子放屁的事情,哈哈~~~。皮一下很開心。

問題

一開始我們確實用的百度車牌識別,但是識別率不是太高,而且車牌圖片要上傳到百度那邊去,也會受網速影響,最重要的是,百度每天只能調用200次,多於200次要掏錢的,產品那邊就說,能不能做成本地識別, 能啊,肯定可以啊,但是我還是個算法小白,怎麼搞這個識別算法嘛,最後找了幾個識別平臺,某泊車平臺,開口就要了八萬,他們說還有另一種授權方式接入,一臺機器400塊,20臺起售。雖說貴了點,但是識別率確實可以,我倒是想直接接過來,多省事了,但是公司肯定不想掏這個錢的啊,最後還是讓開發想辦法 。最苦逼的還是開發~~~~。

找方法

在百度上找了一大圈,大多數都是識別平臺的廣吿,也有幾個說到了識別,但是說的比較模糊,還不提供源碼,有的只是打着識別名號賺積分,所以我寫文章的時候,只會寫一些很實用的,真正能幫到大家的東西,廢話不多說了,直奔主題,最後找到了兩個識別的庫:

一,EasyPR EasyPR github上 star 有五千多個了,但是由於長期沒有更新了,新能源車牌,也不支持,所以沒有使用這個庫。

二, HyperLPR
HyperLPR 作者現在還在維護着,不止Android還支持其他平臺的識別,最終選擇了這個 ,但是作者關於Android方面的文檔寫的不是太多,以致於在集成過程中會遇到很多問題。下面我們一步一步來做。

實現

一,下載OpenCV : OpenCV官網:opencv.org/ OpenCV Android 3.4.6 版本 下載地址:nchc.dl.sourceforge.net/project/ope… 用3.4.6版本的,直接打開鏈接可下載,下載過程有點慢,沒辦法,多等會吧,我也是下了好長時間的。下載完解壓。

二,新建項目,依賴OpenCV 在AS新建一個新項目名字隨便取啦,然後在 APP 上右鍵 New-----> Module 在彈窗裏選擇 Import Eclipse ADT Project 點Next 。 選擇你剛纔解壓的OpenCV目錄下的 sdk/java 目錄 點擊 OK ,然後填下 Module Name ,我填的 "openCV" 然後 點Next -----> Finish。

接下來項目會報錯的,打開 剛導入 的OpenCV 的 AndroidManifest.xml 刪除 <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" /> 這行代碼。

深度æªå¾_éæ©åºå_20190723113856.png

再打開 OpenCV 的 build.gradle 文件,把版本改成和APP 的build.gradle 文件想同的版本,如下:深度æªå¾_éæ©åºå_20190723114042.png

在APP 的 build.gradle 加入 implementation project(path: ':openCV') 這行代碼 OK,現在同步下項目。 還要添加 SO 文件, 在App 的 buidl.gradle 中 defaultConfig 下加入

 ndk   {
            //選擇要添加的對應 cpu 類型的 .so 庫。
            abiFilters 'armeabi-v7a'
            // 還可以添加 'x86', 'x86_64', 'mips', 'mips64'
        }
複製代碼

在main下新建jinLibs 目錄 ,把OpenCV SDK 的解壓目錄下 sdk/native/libs/armeabi-v7a 目錄拷到 jniLibs 下 好了OpenCV 現在完成了,檢驗下是否可用。

在MainActivity中的代碼:

public class MainActivity extends AppCompatActivity {

    private final String TAG = getClass().getSimpleName();


    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @SuppressLint("StaticFieldLeak")
        @Override
        public void onManagerConnected(int status) {
            super.onManagerConnected(status);
            if (status == LoaderCallbackInterface.SUCCESS) {
                Log.d(TAG, "OpenCV 加載成功");
            } else {
                Log.d(TAG, "OpenCV 加載失敗");
            }
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    @Override
    protected void onResume() {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_4_0, getApplicationContext(), mLoaderCallback);
        } else {
            Log.d(TAG, "OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }
}

運行下項目,能看到日誌加載成功就沒問題,其實OpenCV沒加載成功的話,會有個對話框提示的。

配置NDK,編譯庫文件

現在NDK最新版本應該是 r20 吧,不能用最新的NDK,不然會出現編譯過不去的問題,我們去官網下載 NDK-r14b 的版本,作者應該是在這個版本上開發的,附上下載 地址 : NDK 下載地址 下載對應平臺的NDK,一定要下載 NDK-r14b版本。 配置好NDK,我們在main目錄下面新建 jin 目錄 ,然後把Demo**LPR** 庫下載下來,把裏面的 app/src/main/jni 目錄下的 include 、src、還有javaWarpper.cpp 文件複製到 jni 目錄下,把 assets/lpr/ 目錄下的文件全部複製到我們項目的 assets目錄下的lpr文件夾。 如下圖:

我們在 項目 app文件下新建 CMakeLists.txt 文件,內容如下:

# Sets the minimum version of CMake required to build the native
# library. You should either keep the default value or only pass a
# value of 3.4.0 or lower.

cmake_minimum_required(VERSION 3.4.1)

# 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 it for you.
# Gradle automatically packages shared libraries with your APK.
include_directories(src/main/jni/include)

include_directories(src/main/jni)
aux_source_directory(src/main/jni SOURCE_FILES)
aux_source_directory(src/main/jni/src SOURCE_FILES_CORE)
list(APPEND SOURCE_FILES ${SOURCE_FILES_CORE})

#修改修改爲自己的opencv-android-sdk 的JNI路徑
set(OpenCV_DIR /home/aleyn/Android/TestProject/OpenCV-android-sdk/sdk/native/jni)
find_package(OpenCV REQUIRED)

add_library( # Sets the name of the library.
        lpr
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        # Associated headers in the same location as their source
        # file are automatically included.
        ${SOURCE_FILES})

# Searches for a specified prebuilt library and stores the path as a
# variable. Because system libraries are included 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 the
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        lpr
        ${OpenCV_LIBS}
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

複製代碼

我是用Linux環境開發的,上面的路徑是Linux目錄的路徑,一定要替換成你解壓過的OpenCV SDK的 jni 對應本地目錄

下面我們修改 app的build.gradle 文件,最終配置如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.pcl.lpr"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {
            //選擇要添加的對應 cpu 類型的 .so 庫。
            abiFilters 'armeabi-v7a'
            // 還可以添加 'x86', 'x86_64', 'mips', 'mips64'
        }

        externalNativeBuild {
            cmake {
                cppFlags "-std=gnu++11"
                // 注意!!!!       注意!!!!!!
                //如果是用 Linux 開發的的用戶下面這行代碼不用動,如果是 Win 用戶請把 下面這行代碼註釋了
                arguments "-DANDROID_TOOLCHAIN=gcc", "-DANDROID_ARM_NEON=TRUE", "-DANDROID_STL_FORCE_FEATURES=OFF"
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation project(path: ':openCV')
}

複製代碼

OK,我們在項目下新建 PlateRecognition 類和 DeepAssetUtil: PlateRecognition:

package com.pcl.lpr.utils;

/**
 * @auther : Aleyn
 * time   : 2019/04/24
 */
public class PlateRecognition {

    static {
        System.loadLibrary("lpr");
    }

    static native long InitPlateRecognizer(String casacde_detection,
                                           String finemapping_prototxt, String finemapping_caffemodel,
                                           String segmentation_prototxt, String segmentation_caffemodel,
                                           String charRecognization_proto, String charRecognization_caffemodel,
                                           String segmentation_free_prototxt, String segmentation_free_caffemodel);

    static native void ReleasePlateRecognizer(long object);

    public static native String SimpleRecognization(long inputMat, long object);

}
複製代碼

如果你的包名和我的不一樣,一定要到javaWartpper.cpp 文件修改成 和你對應的。

DeepAssetUtil:

package com.pcl.lpr.utils;

import android.content.Context;
import android.os.Environment;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class DeepAssetUtil {


    public static final String ApplicationDir = "lpr";
    public static final String CASCADE_FILENAME = "cascade.xml";
    public static final String FINEMAPPING_PROTOTXT = "HorizonalFinemapping.prototxt";
    public static final String FINEMAPPING_CAFFEMODEL = "HorizonalFinemapping.caffemodel";
    public static final String SEGMENTATION_PROTOTXT = "Segmentation.prototxt";
    public static final String SEGMENTATION_CAFFEMODEL = "Segmentation.caffemodel";
    public static final String RECOGNIZATION_PROTOTXT = "CharacterRecognization.prototxt";
    public static final String RECOGNIZATION_CAFFEMODEL = "CharacterRecognization.caffemodel";
    public static final String FREE_INCEPTION_PROTOTXT = "SegmenationFree-Inception.prototxt";
    public static final String FREE_INCEPTION_CAFFEMODEL = "SegmenationFree-Inception.caffemodel";

    public static final String SDCARD_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ApplicationDir; //解壓文件存放位置


    private static void CopyAssets(Context context, String assetDir, String dir) {
        String[] files;
        try {
            // 獲得Assets一共有幾多文件
            files = context.getAssets().list(assetDir);
        } catch (IOException e1) {
            return;
        }
        File mWorkingPath = new File(dir);
        // 如果文件路徑不存在
        if (!mWorkingPath.exists()) {
            // 創建文件夾
            if (!mWorkingPath.mkdirs()) {
                // 文件夾創建不成功時調用
            }
        }

        for (String file : files) {
            try {
                // 根據路徑判斷是文件夾還是文件
                if (!file.contains(".")) {
                    if (0 == assetDir.length()) {
                        CopyAssets(context, file, dir + file + "/");
                    } else {
                        CopyAssets(context, assetDir + "/" + file, dir + "/" + file + "/");
                    }
                    continue;
                }
                File outFile = new File(mWorkingPath, file);
                if (outFile.exists())
                    continue;
                InputStream in;
                if (0 != assetDir.length()) {
                    in = context.getAssets().open(assetDir + "/" + file);
                } else {
                    in = context.getAssets().open(file);
                }

                OutputStream out = new FileOutputStream(outFile);
                // Transfer bytes from in to out
                byte[] buf = new byte[1024];
                int len;
                while ((len = in.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }

                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void copyFilesFromAssets(Context context) {
        DeepAssetUtil.CopyAssets(context, ApplicationDir, SDCARD_DIR);
    }

    //初始化識別資源
    public static long initRecognizer(Context context) {
        String cascade_filename = SDCARD_DIR + File.separator + CASCADE_FILENAME;
        String finemapping_prototxt = SDCARD_DIR + File.separator + FINEMAPPING_PROTOTXT;
        String finemapping_caffemodel = SDCARD_DIR + File.separator + FINEMAPPING_CAFFEMODEL;
        String segmentation_prototxt = SDCARD_DIR + File.separator + SEGMENTATION_PROTOTXT;
        String segmentation_caffemodel = SDCARD_DIR + File.separator + SEGMENTATION_CAFFEMODEL;
        String character_prototxt = SDCARD_DIR + File.separator + RECOGNIZATION_PROTOTXT;
        String character_caffemodel = SDCARD_DIR + File.separator + RECOGNIZATION_CAFFEMODEL;
        String segmentation_free_prototxt = SDCARD_DIR + File.separator + FREE_INCEPTION_PROTOTXT;
        String segmentation_free_caffemodel = SDCARD_DIR + File.separator + FREE_INCEPTION_CAFFEMODEL;
        copyFilesFromAssets(context);
        //調用JNI 加載資源函數
        return PlateRecognition.InitPlateRecognizer(
                cascade_filename,
                finemapping_prototxt, finemapping_caffemodel,
                segmentation_prototxt, segmentation_caffemodel,
                character_prototxt, character_caffemodel,
                segmentation_free_prototxt, segmentation_free_caffemodel);
    }
}
複製代碼

好了現在識別功能可以用了,差個識別界面

識別界面:

識別界面其實就是一個相機界面,我隨便寫了一個,還做了個識別框不過沒有做屏幕適配,本來想把代碼都貼到文章裏的,但是稍微有一點多,大家還是看下Demo裏邊吧。結合Demo把識別功能,移植到你們的項目中。

Demo運行說明

Demo地址: LPR 打開項目肯定會報錯,做以下修改

  1. 用AS打開項目
  2. 設置項目NDK爲 NDK-r14b
  3. 先修改 CMakeLists.txt 文件, 把第19行修改成你本地的 OpenCV SDK 的對應路徑。
  4. 跟據自己的開發平臺,設置app下的build.gradle 第 23 行代碼 是否要註釋。 完成以上步驟後再運行項目,就沒有問題了。


作者:Aleyn
鏈接:https://juejin.im/post/5d4260cd6fb9a06af92b827b
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章