作りかけのStarSense Exprolerもどきですが、ソフトを作るのが止まっています。カメラの画像をキャプチャしてsolve-fieldに食わすところは、その機能単体のアプリとしては動いているのですが、自作プラネアプリTeleSkymapBT2に組み込んでみるとうまく動かない。具体的には、プレビューを表示するViewとしてTextureViewをlayoutに設定しますが、このTextureViewが有効にならず、いつまでもキャプチャを開始できない、という現象。星を表示しているGLSurfaceViewと喧嘩しているのかな・・よくわからん。

実際のところカメラのプレビューは表示しなくても良いのですが、プレビューを表示せずに毎フレームの画像だけを取得する方法も分からず、そこで止まっていました。

なんか解決策はないものか、とググっていたら、でてきたのがCameraX。何かとCallbackだらけになってややこしいCamera2 APIを、通常使ういくつかのユースケースに合うようにラップして、簡単に使えるようにしたものらしい。
CameraXはJetPackに含まれています。JetPackはややこしくなりすぎたAndroidのAPIを使いやすく更に機能アップさせるためにGoogleから提供されています。JetPackの存在は知っていますがカメラも含まれているとは知りませんでした。だいぶ長い間まともにAndroidのAPIを追いかけていないので、完全に浦島太郎状態です。



ユースケースとして

・プレビュー
・分析
・画像キャプチャ
・動画キャプチャ

が定義されていて、複数のユースケースを組み合わせて使うことが可能。今回は取り込んだ画像を解析して座標を計算することが目的なので、使うユースケースは分析です。単独でも動くらしいので、プレビューを表示せずに画像だけ取ることもできるみたい。これなら行けそう。

ぐぐってみると、CameraXの使い方はあちこちででてきます。まずはGradleに使うライブラリをbuild.gradleに定義します。
 
dependencies {
 <略>
    implementation "androidx.camera:camera-camera2:1.2.2"
    implementation "androidx.camera:camera-lifecycle:1.2.2"
    implementation "androidx.camera:camera-view:1.2.2"
}
あとJAVAのバージョンは8が必要なので、バージョンも指定。
android {
 <略>
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
で、いきなりですが、CameraXを使ってプレビューと画像解析を行う全ソースです。ここではプレビューを表示しつつ、解析用の画像キャプチャをします。
    public boolean openCamera(PreviewView previewView, AppCompatActivity parent){
        Context context = (Context)parent;
        ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(context);

        cameraProviderFuture.addListener(() -> {
            try {
                // Used to bind the lifecycle of cameras to the lifecycle owner
                mCameraProvider = cameraProviderFuture.get();

                // Select back camera as a default
                CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
                Preview.Builder previewBuilder = new Preview.Builder();
                Preview preview = previewBuilder.build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());

                ImageAnalysis.Builder imageAnalysisBuilder = new ImageAnalysis.Builder();
                mCameraProvider.bindToLifecycle(parent, cameraSelector);

                mUseCaseImageAnalysis = imageAnalysisBuilder
                        .setTargetResolution(new Size(CAPTURE_WIDTH, CAPTURE_HEIGHT))
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build();

                mUseCaseImageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
                    @Override
                    public void analyze(@NonNull ImageProxy image) {
						//do some image processing
                    }
                });
                // Unbind use cases before rebinding
                mCameraProvider.unbindAll();
                // Bind use cases to camera
                mCameraProvider.bindToLifecycle(parent, cameraSelector, preview, mUseCaseImageAnalysis);

            } catch (ExecutionException | InterruptedException e) {
                Log.e(TAG, e.getLocalizedMessage(), e);
            }
        }, ContextCompat.getMainExecutor(context));
        return true;
    }

プレビューを表示するViewは、PreviewViewという専用のViewが用意されているので、それをlayoutの中にいれておいて、そのviewのオブジェクトを引数にしてこのメソッドを呼び出します。

CameraXの使い方は簡単で、基本は利用するユースケースのインスタンス(プレビューならPreviewクラス、分析ならImageAnalysisクラス)を作って(それぞれ、12行目と19行目)、bindToLifecycle()メソッドで、そのインスタンスをバインドしてやるだけでOK(32行目)。bindToLifesycle()メソッドでは、引数に使うユースケースのインスタンスを使うだけ渡してやるのでよいです。

画像解析をする場合はImageAnalysisのインスタンスにsetAnalyzer()メソッドでcallbackを指定(25行目)してやれば、フレームが更新されるたびに取得された画像がこのcallbackで渡されます。このcallbackで渡される画像フォーマットはたいてい YUV420_888なので、こちらのやり方でBitmapに変換して色々使えるようにします。


んー、非常に簡単(^-^)。Camera2 APIはもちろん 初代のCamera APIより簡単です。ただCameraXはCamera2APIのラッパーなので細かい設定はできなくて、そのときはCamea2APIを直接使います。
例えば露光時間を端末で設定できる最長、ISO感度を設定できる最大に設定するなどです。

これがそのソース。
        Camera camera = mCameraProvider.bindToLifecycle(parent, cameraSelector);
        CameraCharacteristics c = Camera2CameraInfo.extractCameraCharacteristics(camera.getCameraInfo());

        Camera2Interop.Extender extender = new Camera2Interop.Extender(imageAnalysisBuilder);

        extender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF);
        long maxExposure = c.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE).getUpper();
        int maxISO = c.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE).getUpper();
        Range[] fpsRanges = c.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
        extender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRanges[0]);
        extender.setCaptureRequestOption(CaptureRequest.SENSOR_EXPOSURE_TIME, maxExposure);
        extender.setCaptureRequestOption(CaptureRequest.SENSOR_SENSITIVITY, maxISO);

まずはなんでもいいのでbindToLifecycle()メソッドでカメラオブジェクトを取得(1行目)し、続いてカメラの設定を取得するためのCameraCharacteristicの取得(2行目)、それから設定のためのCamera2Interop.Extenderオブジェクトを取得(4行目)。あとはCamera2のときと同じように値を設定すればOK。
4行目でCamera2Interop.Extenderを取得するのにユースケースのオブジェクトを渡しますが、複数のユースケースを使用する場合はどれか一つに指定してやれば全てに反映されるみたいです。


注意点は、bindToLifecycle()でユースケースをバインドした後でこれらの設定を行っても反映されないことです。一旦unbindAll()でバインドを外した後、再度使うユースケースをバインドすると反映されるみたいです。

試してみたのがこちら。左はpreviewとImageAnalysisのユースケースをバインドしたもの。下にはプレビュー画像が表示されていて、上半分はキャプチャした画像をsolve-fieldに食わせて出力されれたtextです。右はpreviewをバインドせずImageAnalysisだけバインドしたもの。当然絵は表示されませんが、ちゃんと画像をキャプチャしてsolve-fieldの出力が表示されています。これならPreview用のViewは不要なので、TeleSkymapBT2に持って行っても動きそうです。
PXL_20230527_112527054


というわけで実装してみたのがこちら。端末が水平になっていますので通常であれば真下の星が表示されますが、今回はミラーで水平方向が写るような使い方をするので、この状態で地平線が表示されるように座標を回転させて表示しています。ここらへんは以前にStarsPhotoのプロトをTeleSkymapBT2で作っていたので、すでに実装済みでした。



で、右下にファインダーというボタンが表示されていて、しばらくすると星が表示されます。これはカメラの画像を取得してsolve-fieldに渡し、solve-fieldが成功したらオレンジ、失敗したらグレーの星が表示されるように作っています。室内でやっているので当然失敗しますので、グレーの星が表示されます。これでまずは画像撮影からsolve-fieldの計算、出力結果の取得までは通しで動いたのは確認できたことになりますヽ(=´▽`=)ノ。

次は実際の星でちゃんと座標が取得できるか、それで星の表示が補正されているか、の確認。あとはsolve-fieldの高速化ですね。