こんにちは、Habr! 過去の記事の 1つでは、Smart IDReader認識エンジンをiOSアプリケーションに埋め込む問題を調査しました。 今度はAndroid OSについての同じ問題について議論します。 システムの多数のバージョンと幅広いデバイス群により、これはiOSよりも複雑になりますが、それでも完全に解決可能なタスクです。 免責事項-以下の情報は最後の手段では真実ではありません。カメラの埋め込み/操作のプロセスを単純化する方法を知っている場合、または異なる方法で行う場合-コメントを歓迎します!
アプリケーションにドキュメント認識機能を追加するとします。これには、次の部分で構成されるSmart IDReader SDKがあります。
-
bin
-32ビットARMv7アーキテクチャ用のlibjniSmartIdEngine.so
カーネルlibjniSmartIdEngine.so
をビルドします -
bin-64
libjniSmartIdEngine.so
ビットARMv8アーキテクチャ用のlibjniSmartIdEngine.so
カーネルlibjniSmartIdEngine.so
-
bin-x86
-x86 32ビットアーキテクチャ用のlibjniSmartIdEngine.so
カーネルlibjniSmartIdEngine.so
を構築する -
bindings
libjniSmartIdEngine.so
ライブラリのJNIjniSmartIdEngineJar.jar
ラッパー -
data
-カーネル構成ファイル -
doc
-SDKドキュメント
SDKの内容に関するいくつかのコメント。
異なるプラットフォーム用の3つのライブラリアセンブリが存在することは、Android OS上のさまざまなデバイスの料金です(このアーキテクチャのデバイスがないため、MIPS用にビルドしません)。 ARMv7およびARMv8のアセンブリが主なものであり、x86バージョンは通常、Intelモバイルプロセッサに基づく特定のデバイス用にお客様が使用します。
JavaアプリケーションからC ++コードを呼び出すには、JNIラッパー(Javaネイティブインターフェイス) jniSmartIdEngineJar.jar
が必要です。 ラッパーのアセンブリは、 SWIG(簡易ラッパーおよびインターフェイスジェネレーター)ツールを使用して自動化されます。
それで、フランス人が言うように、レベノンはノームートンです! SDKがあり、最小限の労力でプロジェクトに組み込み、使用を開始する必要があります。 これには、次の手順が必要です。
- 必要なファイルをプロジェクトに追加する
- データの準備とエンジンの初期化
- カメラをアプリケーションに接続します
- データ転送および受信結果
誰もがライブラリを使用できるようにするために、 GithubにAndroid用Smart IDReader Demoのソースコードを準備して投稿しました。 このプロジェクトはAndroid Studio向けに作成されており、シンプルなアプリケーションに基づいてカメラとカーネルを操作する例を示しています。
必要なファイルをプロジェクトに追加する
このプロセスをAndroid Studioのアプリケーションプロジェクトの例として考えてください;他のIDEのユーザーにとって、プロセスはそれほど変わりません。 デフォルトでは、Android Studioは各プロジェクトでlibs
フォルダーを作成し、そこからGradleコレクターがJARファイルを選択してプロジェクトに追加します。 ここで、JNIラッパーjniSmartIdEngineJar.jar
をコピーします。 カーネルライブラリを追加する方法はいくつかありますが、最も簡単な方法はJARアーカイブを使用することです。 libs
フォルダーにnative-libs.jar
という名前のアーカイブを作成します(これは重要です!)そして、アーカイブ内にサブフォルダーlib/armeabi-v7a
およびlib/arm64-v8a
を作成し、ライブラリの対応するバージョンをそこに置きます(x86ライブラリの場合、サブフォルダーはlib/x86
なります)。
この場合、アプリケーションをインストールした後、Android OSはこのデバイスに必要なライブラリバージョンを自動的に展開します。 付随するエンジン構成ファイルをプロジェクト資産フォルダーに追加します。このフォルダーがない場合は、手動で作成するか、 File|New|Folder|Assets Folder
、 File|New|Folder|Assets Folder
]、[フォルダー]、 File|New|Folder|Assets Folder
コマンドを使用して作成できます。 ご覧のとおり、プロジェクトへのファイルの追加は非常に簡単で、時間もほとんどかかりません。
データの準備とエンジンの初期化
そのため、必要なファイルをアプリケーションに追加し、それをうまく組み立てることさえできました。 手は実際に新しい機能に到達して試してみますが、そのためにはもう少し作業する必要があります:-)つまり、次のようにします。
- アセットからカーネル構成ファイルを展開する
- ライブラリをダウンロードしてエンジンを初期化する
ライブラリが構成ファイルにアクセスするには、それらをアセットからアプリケーションの作業フォルダーに転送する必要があります。 起動時にこれを1回行い、新しいバージョンがリリースされたときにのみ更新するだけで十分です。 このチェックを行う最も簡単な方法は、アプリケーションコードのバージョンに基づいており、変更されている場合はファイルを更新します。
// PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); int version_code = packageInfo.versionCode; SharedPreferences sPref = PreferenceManager.getDefaultSharedPreferences(this); // int version_current = sPref.getInt("version_code", -1); // need_copy_assets = version_code != version_current; // SharedPreferences.Editor ed = sPref.edit(); ed.putInt("version_code", version_code); ed.commit(); … if (need_copy_assets == true) copyAssets();
コピー手順自体は複雑ではなく、アプリケーションのアセットにあるファイルからデータを取得し、このデータを作業ディレクトリのファイルに書き込むことで構成されます。 このようなコピーを実装する関数のコード例は、 Githubの例にあります。
ライブラリをロードしてカーネルを初期化するためだけに残ります。 プロシージャ全体には一定の時間がかかるため、メインGUIスレッドの速度が低下しないように、別のスレッドで実行するのが妥当です。 AsyncTaskの初期化の例
private static RecognitionEngine engine; private static SessionSettings sessionSettings; private static RecognitionSession session; ... lass InitCore extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... unused) { if (need_copy_assets) copyAssets(); // configureEngine(); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if(is_configured) { // (, rus.passport.* ) sessionSettings.AddEnabledDocumentTypes(document_mask); // StringVector document_types = sessionSettings.GetEnabledDocumentTypes(); ... } } } … private void configureEngine() { try { // System.loadLibrary("jniSmartIdEngine"); // String bundle_path = getFilesDir().getAbsolutePath() + File.separator + bundle_name; // engine = new RecognitionEngine(bundle_path); // sessionSettings = engine.CreateSessionSettings(); is_configured = true; } catch (RuntimeException e) { ... } catch(UnsatisfiedLinkError e) { ... } }
カメラをアプリケーションに接続します
アプリケーションがすでにカメラを使用している場合は、このセクションを安全にスキップして次へ進むことができます。 残りについては、Smart IDReaderを介したドキュメント認識のためにカメラを使用してビデオストリームを操作することを検討してください。 APIバージョン21(Android 5.0)から非推奨と宣言されていますが、Camera2ではなくCameraクラスを使用する予約をすぐに行います。 これは、次の理由で意図的に行われます。
- Cameraクラスははるかに使いやすく、必要な機能が含まれています。
- Android 2.3.xおよび4.xxでの古いデバイスのサポートは引き続き関連しています
- Cameraクラスは引き続き十分にサポートされていますが、Android 5.0の発売当初、多くのメーカーはCamera2の実装に問題がありました
カメラサポートをアプリケーションに追加するには、マニフェストに次の行を登録する必要があります。
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" />
Android 6.x以降に実装されているカメラの使用許可をリクエストすることをお勧めします。 また、これらのシステムのユーザーは、設定でアプリケーションからいつでも権限を選択できるため、引き続き確認する必要があります。
// - if( needPermission(Manifest.permission.CAMERA) == true ) requestPermission(Manifest.permission.CAMERA, REQUEST_CAMERA); … public boolean needPermission(String permission) { // int result = ContextCompat.checkSelfPermission(this, permission); return result != PackageManager.PERMISSION_GRANTED; } public void requestPermission(String permission, int request_code) { // ActivityCompat.requestPermissions(this, new String[]{permission}, request_code); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case REQUEST_CAMERA: { // boolean is_granted = false; for(int grantResult : grantResults) { if(grantResult == PackageManager.PERMISSION_GRANTED) // is_granted = true; } if (is_granted == true) { camera = Camera.open(); // .... } else toast("Enable CAMERA permission in Settings"); } default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); } }
カメラでの作業の重要な部分は、パラメータ、つまりフォーカスモードとプレビュー解像度を設定することです。 さまざまなデバイスとカメラの特性のため、この問題には特別な注意を払う必要があります。 カメラがフォーカスをサポートしていない場合は、固定フォーカスで作業するか、無限遠に向ける必要があります。 この場合、特別なことは何もできません。カメラから画像を取得します。 そして、幸運でフォーカスが利用可能な場合、 FOCUS_MODE_CONTINUOUS_VIDEO
モードまたはFOCUS_MODE_CONTINUOUS_VIDEO
モードFOCUS_MODE_CONTINUOUS_PICTURE
FOCUS_MODE_CONTINUOUS_VIDEO
かどうかを確認します。これは、作業中に撮影オブジェクトに焦点を合わせる絶え間ないプロセスを意味します。 これらのモードがサポートされている場合、パラメーターで公開します。 そうでない場合は、次のフェイントを行うことができます-タイマーを開始し、指定された周波数でカメラのフォーカス機能を呼び出します。
Camera.Parameters params = camera.getParameters(); // List<String> focus_modes = params.getSupportedFocusModes(); String focus_mode = Camera.Parameters.FOCUS_MODE_AUTO; boolean isAutoFocus = focus_modes.contains(focus_mode); if (isAutoFocus) { if (focus_modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) focus_mode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE; else if (focus_modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) focus_mode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO; } else { // focus_mode = focus_modes.get(0); } // params.setFocusMode(focus_mode); // if (focus_mode == Camera.Parameters.FOCUS_MODE_AUTO) { timer = new Timer(); timer.schedule(new Focus(), timer_delay, timer_period); } … // private class Focus extends TimerTask { public void run() { focusing(); } } public void focusing() { try{ Camera.Parameters cparams = camera.getParameters(); // if( cparams.getMaxNumFocusAreas() > 0) { camera.cancelAutoFocus(); cparams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); camera.setParameters(cparams); } }catch(RuntimeException e) { ... } }
プレビューの解像度の設定は非常に簡単です。基本的な要件は、プレビューカメラのアスペクト比が表示領域の側面に対応することで、表示中に歪みが発生しないことです。ドキュメント認識の品質はそれに依存するため、解像度は可能な限り高いことが望ましいです。 この例では、アプリケーションはプレビューを全画面で表示するため、画面のアスペクト比に対応する最大解像度を選択します。
DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); // float best_ratio = (float)metrics.heightPixels / (float)metrics.widthPixels; List<Camera.Size> sizes = params.getSupportedPreviewSizes(); Camera.Size preview_size = sizes.get(0); // final float tolerance = 0.1f; float preview_ratio_diff = Math.abs( (float) preview_size.width / (float) preview_size.height - best_ratio); // preview for (int i = 1; i < sizes.size() ; i++) { Camera.Size tmp_size = sizes.get(i); float tmp_ratio_diff = Math.abs( (float) tmp_size.width / (float) tmp_size.height - best_ratio); if( Math.abs(tmp_ratio_diff - preview_ratio_diff) < tolerance && tmp_size.width > preview_size.width || tmp_ratio_diff < preview_ratio_diff) { preview_size = tmp_size; preview_ratio_diff = tmp_ratio_diff; } } // preview params.setPreviewSize(preview_size.width, preview_size.height);
カメラの向きを設定し、アクティビティの表面にプレビューを表示するための残りはほとんどありません。 デフォルトでは、0度の角度はデバイスの横向きに対応します。画面を回転させるときは、それに応じて変更する必要があります。 ここで、GoogleのNexus 5Xといういい言葉を思い出すこともできます。Nexus5Xのマトリックスはデバイスに上下逆さまにインストールされており、個別の方向チェックが必要です。
private boolean is_nexus_5x = Build.MODEL.contains("Nexus 5X"); SurfaceView surface = (SurfaceView) findViewById(R.id.preview); ... // camera.setDisplayOrientation(!is_nexus_5x ? 90: 270); // preview camera.setPreviewDisplay(surface.getHolder()); // preview camera.startPreview();
データ転送および受信結果
だから、カメラは接続されており、最も興味深いものは残っています-カーネルを使用して結果を取得することです。 新しいセッションを開始し、プレビューモードでカメラからフレームを受信するようにコールバックを設定することにより、認識プロセスを開始します。
void start_session() { if (is_configured == true && camera_ready == true) { // , - sessionSettings.SetOption("common.sessionTimeout", "5.0"); // session = engine.SpawnSession(sessionSettings); try { session_working = true; // frame_waiting = new Semaphore(1, true); frame_ready = new Semaphore(0, true); // AsyncTask new EngineTask().execute(); } catch (RuntimeException e) { ... } // callback camera.setPreviewCallback(this); } }
onPreviewFrame()
関数は、カメラから現在の画像をYUV NV21形式のバイト配列として受け取ります。 メインスレッドでのみ呼び出すことができ、イメージ処理のカーネル呼び出しが遅くならないように、AsyncTaskを使用して別のスレッドに配置されるため、プロセスはセマフォを使用して同期されます。 カメラから画像を受け取った後、ワーカースレッドに処理を開始するためのシグナルを与えます。最後に、新しい画像を受け取るためのシグナルを与えます。
// private static volatile byte[] data; ... @Override public void onPreviewFrame(byte[] data_, Camera camera) { if(frame_waiting.tryAcquire() && session_working) { data = data_; // frame_ready.release(); } } … class EngineTask extends AsyncTask<Void, RecognitionResult, Void> { @Override protected Void doInBackground(Void... unused) { while (true) { try { frame_ready.acquire(); // if(session_working == false) // break; Camera.Size size = camera.getParameters().getPreviewSize(); // RecognitionResult result = session.ProcessYUVSnapshot(data, size.width, size.height, !is_nexus_5x ? ImageOrientation.Portrait : ImageOrientation.InvertedPortrait); ... // frame_waiting.release(); }catch(RuntimeException e) { ... } catch(InterruptedException e) { ... } } return null; }
各画像の処理後、カーネルは現在の認識結果を返します。 見つかったドキュメント領域、信頼値とフラグを含むテキストフィールド、および写真やキャプションなどのグラフィックフィールドが含まれます。 データが正しく認識されるか、タイムアウトが発生すると、IsTerminalフラグが設定され、プロセスの完了を通知します。 中間結果の場合、見つかったゾーンとフィールドを描画し、認識品質の現在の進捗状況などを表示できます。すべては想像力に依存します。
void show_result(RecognitionResult result) { // StringVector texts = result.GetStringFieldNames(); // , , StringVector images = result.GetImageFieldNames(); for (int i = 0; i < texts.size(); i++) // { StringField field = result.GetStringField(texts.get(i)); String value = field.GetUtf8Value(); // boolean is_accepted = field.IsAccepted(); .. ... } for (int i = 0; i < images.size(); i++) // { ImageField field = result.GetImageField(images.get(i)); Bitmap image = getBitmap(field.GetValue()); // Bitmap ... } ... }
その後、カメラから画像を取得するプロセスを停止し、認識プロセスを停止することしかできません。
void stop_session() { session_working = false; data = null; frame_waiting.release(); frame_ready.release(); camera.setPreviewCallback(null); // ... }
おわりに
この例からわかるように、Smart IDReader SDKをAndroidアプリケーションに接続してカメラを操作するプロセスは複雑なものではなく、いくつかのルールに従うだけです。 多くのお客様がモバイルアプリケーションに当社の技術をうまく適用しており、新しい機能を追加するプロセスは非常に短時間で完了します。 この記事の助けを借りて、あなたがこれを確信できることを願っています!
PS埋め込み後のパフォーマンスでSmart IDReaderがどのように見えるかを確認するには、 App StoreとGoogle Playからアプリケーションの無料フルバージョンをダウンロードできます。