VIA VAB-950 object detection app

VIA VAB-950を使用した初めてのカスタムコンピュータビジョンアプリケーションの作成方法

VIA VAB-950は、VIAの最新組み込みボードであり、多くの新しい機能を備えています。このブログでは、Androidアプリケーション開発における、その役割にフォーカスを当てています。特にAndroidでのコンピュータビジョンと機械学習に関して、単純なオブジェクト検出アプリを作成するプロセスについて詳しく説明します。このアプリは、GoogleのCameraXおよびML Kit APIとともにJava(プログラミング言語の一種)で記述されています。これにより、カメラを実装し、アプリにカスタムtfileモデルを読み込むことが可能となります。

以下のソースコードは、Phillip Wei によって作成されたものです。詳細はこちらをご覧ください。

Android、CameraX APIとML KitAPIの簡単な紹介

Androidアプリ開発では、コーディング環境は「アクティビティ」と「レイアウト」の2つの領域に分けられます。「アクティビティ」はアプリのマインロジックを定義するようなものです。様々なコンポーネントがどのように機能して、アプリを期待通りに機能させるか。「レイアウト」はその名が示すように、アプリの最終的なデザインを構成する「Views(ビュー)」で構成されています。このチュートリアルで使用するIDEはAndroidoStudioであり、MainActivity.javaファイルとactivity_main.xmlファイルを自動的に生成します。これら2つは主要なファイルです。

CameraX APIは、Googleの最新のカメラAPIであり、その前身であるCamera2を基に構築されています。これまでの使用例とそれらの技術を組み合わせることで、はるかにシンプルなカメラアプリ開発プロセスが可能になり、APIを使用した開発が根本的にユーザーが利用しやすいものになります。構文的には、コードをクリーンで構造化され、整理された状態に保つのに役立ちます。このアプリで使用される2つの主な使用例は、「プレビュー」と「画像分析」です。その他の使用例として「画像キャプチャ」もありますが、このアプリはライブビデオストリームで動作するため、画像キャプチャは現実には必要ありません。

最後に、ML Kit APIを使用すると、開発者はCameraXと組み合わせて機械学習アプリを効率的に展開することができます。このチュートリアルでは、カスタムオブジェクト分類tfileモデルをアプリに読み込む方法について説明していますが、アプリは、将来の機械学習のアイディアやモデルを実装するための基本的なテンプレートとしても機能します。

設定方法

アプリの開発を開始する前に、Java Development Kit (JDK) Android StudioをダウンロードしてAndroid環境をセットアップする必要があります。(Windows用のAndroid Studioのインストールに関する便利なチュートリアルについては、ここをクリックください。Macの場合はここをクリックしてください)。新しいプロジェクトを作成するときは、最小のSDKをAPI21に設定してください。これは、CameraX APIでサポートされている最小のSDKです。

次に、使用するAPIのセットアップを実装して、生成された「build.gradle」ファイルと「main_activity.xml」ファイル内のコードを変更する必要があります。モジュールレベルのbuild.gradleファイルで、Androidと依存関係のセクションに次のコードを追加します。

android {
    aaptOptions {
        noCompress "tflite"
    }   
}
dependencies {
    // CameraX API
    def camerax_version = '1.1.0-alpha02'
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-view:1.0.0-alpha21"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"

    // Object detection & tracking feature with custom model from ML Kit API
    implementation 'com.google.mlkit:object-detection-custom:16.3.1'
}

次に、activity_main.xmlファイルにコードを追加する必要があります(これらは既存の「ConstrainLayout」セクションないに配置されます)。これを行うには、XMLファイルをクリックして開き、右上に「コード」、「分割」、「デザイン」の3つのオプションが表示されます。

そこで「コード」を選択し、次のコードを組みます。

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="584dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.camera.view.PreviewView>

    <TextView
        android:id="@+id/resultText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/previewView"
        app:layout_constraintHorizontal_bias="0.498" />

    <TextView
        android:id="@+id/confidence"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/resultText"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintVertical_bias="0.659" />


</androidx.constraintlayout.widget.ConstraintLayout>

「Preview View」は、接続されたカメラからの映像を「プレビュー」としてアプリに表示できるようにするためにCameraXが提供しているものです。「Constraint Layout」(XMLファイルのコンポーネントツリーのデフォルトレイアウト)を使用すると、開発者は、使用されているAndroidデバイスに関係なく、特定の「ビュー」を同じ場所に制約できます。「Text View」は、アプリにテキストを表示する単なるビューです。私たちのアプリでは、2つのText Viewが、機械学習の推論の結果を表示するのに役立ちます。

VAB-950に関して必要なのは、マイクロUSB、USB-AケーブルとCSIカメラだけです。

コードの実装

最初の課題は、XMLファイルとMain Activityファイルを相互に接続することです。これを容易にするために、Androidには「find View By ID」と呼ばれる基本的な方法があります。その名が示すように、XMLファイルからビューを対象として具体例化できます。この場合、コードの開始は次のようになります。

public class MainActivity extends AppCompatActivity {

    private PreviewView previewView;
    private final int REQUEST_CODE_PERMISSIONS = 101;
    private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA"};
    private final static String TAG = "Anything unique";
    private Executor executor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        getSupportActionBar().hide();

        previewView = findViewById(R.id.previewView);
        executor = ContextCompat.getMainExecutor(this);
    }

XMLファイルに移動して「Preview View」をクリックすると、右上にそのIDが「Preview View」であることがわかります。これは、R.id.previewView方法を使用してPreview Viewを具体例化するために使用するものです。以前にAndroidデバイスを使用したことがある場合は、特定のタスクを実行するために特定の権限を使用するアプリに与えるように求められたことを思い出すかもしれません。ここでも同じことを行う必要があります。ここでは、ユーザーにカメラの許可を求めるとともに、この許可が与えられていない場合はアプリが実行されないようにする必要があります。まず、AndroidManifest.xmlファイルを開く必要があります。

そして、アプリケーションセクションの前に次の2行を追加します。

<uses-feature android:name="android.hardware.camera.any" /><uses-permission android:name="android.permission.CAMERA" />

次に、これらの行をMainActivity.javaに追加します。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {        

        if (allPermissionsGranted()) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(this,
                    REQUIRED_PERMISSIONS,
                    REQUEST_CODE_PERMISSIONS);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (allPermissionsGranted()) {
            startCamera();
        } else {
            Toast.makeText(this, "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show();
            finish();
        }
    }

   /**
    * Checks if all the permissions in the required permission array are already granted.
    *
    * @return Return true if all the permissions defined are already granted
    */
    private boolean allPermissionsGranted() {
        for (String permission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(getApplicationContext(), permission) !=
                    PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

さらに、「on Create」方法に次のコードを追加して、custom.tfliteモデルをロードする必要もあります。

public class MainActivity extends AppCompatActivity {

    // New field
    private ObjectDetector objectDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {      
        

        // Loads a LocalModel from a custom .tflite file
        LocalModel localModel = new LocalModel.Builder()
                .setAssetFilePath("whatever_your_tflite_file_is_named.tflite")
                .build();

        CustomObjectDetectorOptions customObjectDetectorOptions =
                new CustomObjectDetectorOptions.Builder(localModel)
                        .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
                        .enableClassification()
                        .setClassificationConfidenceThreshold(0.5f)
                        .setMaxPerObjectLabelCount(3)
                        .build();
        objectDetector = ObjectDetection.getClient(customObjectDetectorOptions);
    }
}

「set AssetFilePath」方法は、ルートディレクトリのassetフォルダを調べて、指定された文字列に一致する.tfliteモデルが存在するかどうか確認します。したがって、この文字列は、ダウンロードしたモデルであれ。自分で作成したモデルであれ、実装するtfliteモデルの名前と一致させる必要があります。したがって、appフォルダーを右クリックし、[新規]、[ディレクトリ]の順にクリックして、アセットディレクトリを追加しなければなりません。そこから、「src/ main/ assets」を検索し、生成されたフォルダーにtfliteファイルをドロップできます。

次に、「startCamera」方法の作成を開始します。この方法のコードは以下の通りです。

/**
     * Starts the camera application.
     */
    public void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
                ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(() -> {
            ProcessCameraProvider cameraProvider;
            // Camera provider is now guaranteed to be available
            try {
                cameraProvider = cameraProviderFuture.get();
                bindPreviewAndAnalyzer(cameraProvider);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }, executor);
    }

このメソッド内のほとんどのプロセスは、それ自体が内部で実行されますが、注意する必要がある主要な部分は、CameraX使用事例を「ライフサイクル」にバインドできる対象である「ProcessCameraProvider」です。この場合、ライフサイクルはアプリであり、アプリを開いたり、閉じたりすると、プログラムはこれらの使用事例をオンまたはオフにするように指示されます。このライフサイクルバインディングは「bindPreviewAndAnalyzer」ヘルパーメソッドを介して実現されます。ここで、使用事例がどのようにコーディングされているかを確認します。

まず、プレビューの使用事例が実装されます。

/**
 * Creates camera preview and image analyzer to bind to the app's lifecycle.
 *
 * @param cameraProvider a @NonNull camera provider object
 */
private void bindPreviewAndAnalyzer(@NonNull ProcessCameraProvider cameraProvider) {
    // Set up the view finder use case to display camera preview
    Preview preview = new Preview.Builder()
            .setTargetResolution(new Size(1280, 720))
            .build();
    // Connect the preview use case to the previewView
    preview.setSurfaceProvider(previewView.getSurfaceProvider());
    // Choose the camera by requiring a lens facing
    CameraSelector cameraSelector = new CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build();     

}

activity_mian.xmlのPreview Viewレイヤーに戻ると、「Preview」オブジェクトをPreview Viewに接続し、Preview Viewオブジェクトをカメラプレビューのサーフェスとして使用するようにアプリに指示する必要があります。「Camera Selector」オブジェクトは、ほとんどのAndroidデバイスに全面カメラと背面カメラがあるため、使用するカメラレンズを示します。VIA VAB-950のファームウェアはバックカメラを持つようにプログラミングされているため、「Camera Selector.LENS FACING BACK」を介してリアカメラを検出するようにCamera Selectorを設定しました。

プレビューが何らかの形で、引き伸ばされているように見える場合は「set Target Resolution」の代わりに「set Target Aspect Ratio」メソッドを使用してみてください。これは、特定のデバイス上のCameraXでよく起こる問題です。またこの問題はset Target Aspect Ratioを設定した後も引き続き起こることも予想されますが、この問題に対する完全な解決方法はまだ見つかっていません。Googleは、この問題を対処するためにAPIを更新することをお勧めしています。「Preview.builder」ドキュメントにアクセスも可能です。ご覧いただきたい場合は、ここをクリックしてください。

次に、画像分析の使用事例を設定します。これは、コアの機械学習推論が行われる場所であり、プレビューから画像入力を取得し、カスタムtfileモデルで処理します。

/**
 * Creates camera preview and image analyzer to bind to the app's lifecycle.
 *
 * @param cameraProvider a @NonNull camera provider object
 */
private void bindPreviewAndAnalyzer(@NonNull ProcessCameraProvider cameraProvider) {

    ....

    // Creates an ImageAnalysis for analyzing the camera preview feed
    ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
            .setTargetResolution(new Size(1280, 720))
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build();

    imageAnalysis.setAnalyzer(executor,
            new ImageAnalysis.Analyzer() {
                @Override
                public void analyze(@NonNull ImageProxy imageProxy) {
                    @SuppressLint("UnsafeExperimentalUsageError") Image mediaImage =
                            imageProxy.getImage();
                    if (mediaImage != null) {
                        processImage(mediaImage, imageProxy)
                                .addOnCompleteListener(new OnCompleteListener<List<DetectedObject>>() {
                                    @Override
                                    public void onComplete(@NonNull Task<List<DetectedObject>> task) {
                                        imageProxy.close();
                                    }
                                });
                    }
                }
            });
}

/**
 * Throws an InputImage into the ML Kit ObjectDetector for processing
 *
 * @param mediaImage the Image image converted from the ImageProxy image
 * @param imageProxy the ImageProxy image from the camera preview
 */
private Task<List<DetectedObject>> processImage(Image mediaImage, ImageProxy imageProxy) {
    InputImage image =
            InputImage.fromMediaImage(mediaImage,
                    imageProxy.getImageInfo().getRotationDegrees());
    return objectDetector.process(image)
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    String error = "Failed to process. Error: " + e.getMessage();
                    Log.e(TAG, error);
                }
            })
            .addOnSuccessListener(new OnSuccessListener<List<DetectedObject>>() {
                @Override
                public void onSuccess(List<DetectedObject> results) {
                    String text = "";
                    float confidence = 0;
                    for (DetectedObject detectedObject : results) {
                        for (DetectedObject.Label label : detectedObject.getLabels()) {
                            text = label.getText();
                            confidence = label.getConfidence();
                        }
                    }
                    TextView textView = findViewById(R.id.resultText);
                    TextView confText = findViewById(R.id.confidence);
                    if (!text.equals("")) {
                        textView.setText(text);
                        confText.setText(String.format("Confidence = %f", confidence));
                    } else {
                        textView.setText("Detecting");
                        confText.setText("?");
                    }
                }
            });
}

画像分析の使用事例は、「Image Analysis」および「Image Analyzer」オブジェクトによって作成されています。ここで、Image Analyzerのanalyzeメソッドは「Image Proxy」オブジェクトを取り込みます。このImage Proxyオブジェクトは、Process Imageヘルパーメソッドを介して実現される、モデルにフィードする必要のある画像入力です。以前にインスタンス化されたObject Detectorは、画像入力を処理して、検出されたオブジェクトのリストを生成しています。このリストには、モデルがカメラに表示されていると考える、分類されたオブジェクトが含まれている必要があります。この結果をユーザーに出力するには、前に作成されたオブジェクトのラベルと信頼度に設定するだけです。

最後に、プレビューと画像分析の使用事例をアプリのライフサイクルにバインドします。この手順省いてしまうと、アプリがプレビューや画像分析を実行できなくなってしまうため、特に重要な手順です。最終的なコードはここにあります。

/**
     * Creates camera preview and image analyzer to bind to the app's lifecycle.
     *
     * @param cameraProvider a @NonNull camera provider object
     */
    private void bindPreviewAndAnalyzer(@NonNull ProcessCameraProvider cameraProvider) {

        ....

        // Unbind all previous use cases before binding new ones
        cameraProvider.unbindAll();

        // Attach use cases to our lifecycle owner, the app itself
        cameraProvider.bindToLifecycle(this,
                cameraSelector,
                preview,
                imageAnalysis);
    }

このプロジェクトは、オブジェクトの分類に焦点を当てていますが、MLキットは、シンプルなカメラアプリで構築できる機械学習機能をさらに多く提供しています。VAB-950を使用すると、必要に応じて、組み込み/エッジAIプロジェクトの開発に目を向けることもできます。VIA VAB-950の未来は明るいものであり、開発者と企業がどこまで同じようにそれを実現できるか、そしてどれだけ遠くまで到達できるかを見るのは非常に楽しみです。ハードウェア機能、ソフトウェア機能の互換性、および完全なデータシートの詳細については、VIA VAB-950ページにアクセスください。

この文章はVIAのAI開発およびマーケティング部のインターンであり、Aberystwyth大学の学生であるDaniel Swiftによって書かれました。

VIA Technologies, Inc.
VIA Technologies, Inc.