woshidan's loose leaf

ぼんやり勉強しています

CameraDevice closeのタイミングとCameraCaptureSession closeのタイミングについて

https://github.com/googlesamples/android-Camera2Basic のサンプルから確認してメモする。

まとめ

  • CameraDeviceCameraDevice.StateCallback のコールバックの中でcloseする
    • onDisconnectedonError の中でclose
    • セマフォのロックを解放しながら処理をしていた。このセマフォのロックは CameraDevice をオープンする時に獲得しようとされている
      • セマフォはカメラを解放する前にアプリの処理が終了するのを防ぐため
        • onPauseで処理をする場合など、onPause -> 次の処理へ行かないようにする働きがある… のか?
        • カメラを解放する前後の処理は一つのスレッドしか処理を進めないようにしている?
  • CameraCaptureSessiononPauseclose していた

コード

https://github.com/googlesamples/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L184-L215

    /**
     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
     */
    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            // This method is called when the camera is opened.  We start camera preview here.
            mCameraOpenCloseLock.release();
            mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
            Activity activity = getActivity();
            if (null != activity) {
                activity.finish();
            }
        }

    };

https://github.com/googlesamples/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L456-L461

    @Override
    public void onPause() {
        closeCamera();
        stopBackgroundThread();
        super.onPause();
    }

https://github.com/googlesamples/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L624-L647

    /**
     * Closes the current {@link CameraDevice}.
     */
    private void closeCamera() {
        try {
            mCameraOpenCloseLock.acquire();
            if (null != mCaptureSession) {
                mCaptureSession.close();
                mCaptureSession = null;
            }
            if (null != mCameraDevice) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
            if (null != mImageReader) {
                mImageReader.close();
                mImageReader = null;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
        } finally {
            mCameraOpenCloseLock.release();
        }
    }

気になってることについては

  • [既存気になること]デバイスの向きとアプリの向きの差分
  • [新しい気になること]Javaセマフォの使い方のイメージがない(並行処理Java読む?)

とりあえずウェブオペレーションが2周目読んでからじゃないと次行っちゃダメっぽい感じで並行処理は来月かなー。

リリースについてのetc.

なんかあったら追加する。

  • Android
    • ベータ版リリースはクローズドベータで指定したメアドのGoogleアカウントでのみインストール、アップデート可能に
    • ベータ版リリースは2回目以降は
      • 更新に~1h程度かかる
      • 同じVersionCodeのものは出せない
        • その辺はアルファ使う?
      • ベータ版から本番へ適用する時のステップはGoogle Play Consoleのアップデートによって変わることがあるが、こないだ試したらベータ版リリース -> 「ROLLOUT TO PRODUCTION」-> 本番へのドラフト追加 -> 本番からリリースだった

アプリで決めたActivityの向きがLANDSCAPEの時、Camera2のCameraDeviceから受け取るカメラのプレビューの向きがずれる

昨日のImageReaderのメモで プレビューの向きがおかしいのが気になる、という話を書きました。

その件について調べると

https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape

などどうもアプリの画面の向きがLANDSCAPEの場合、そういうことがあるらしい。

一つには、TextureView に画像変換用の Matrix をセットすることで、TextureViewにセットされる画像をいじることができて、例えば下記のように記述した場合、

        // https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape
        Matrix rotate = new Matrix();
        rotate.postScale(0.5f, 0.5f);
        rotate.postTranslate(IMAGE_HEIGHT / 2, 0);
        rotate.postRotate(60);
        mTextureView.setTransform(rotate);

みたいな感じとなる。

これをもうちょっとうまくやる。

        // Landscapeのアプリの場合、カメラの向きとプレビューの向きがずれるっぽい
        // https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape
        Matrix rotate = new Matrix();
        rotate.postRotate(270); // デバイスの向きとアプリの向きの差分から決めると良い 
        // 関連: https://github.com/googlesamples/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L520-L541
        rotate.postTranslate(0, mTextureView.getWidth());
        rotate.postScale(1.0f * 1.15f, 0.56f * 1.15f); // とりあえず頑張ってうめた
        mTextureView.setTransform(rotate);

これでだいたいプレビューの向きとサイズがあってくる。

このMatrixでの操作手順は、前操作した結果にさらに次の操作が反映されるので実際紙などを使って考えた方が早い。

さて、さっきのコードについてScaleが手打ちだとちょっと困るのでもう少しだけ頑張って

    private void createCameraPreviewSession() {
        // Landscapeのアプリの場合、カメラの向きとプレビューの向きがずれるっぽい
        // https://stackoverflow.com/questions/34536798/android-camera2-preview-is-rotated-90deg-while-in-landscape
        Matrix rotate = new Matrix();
        rotate.postRotate(270); // デバイスの向きとアプリの向きの差分から決めると良い
        // 関連: https://github.com/googlesamples/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L520-L541
        rotate.postTranslate(0, mTextureView.getWidth());
        rotate.postScale((1.0f * IMAGE_WIDTH / IMAGE_HEIGHT) * (mTextureView.getWidth() / mTextureView.getHeight()), (1.0f * IMAGE_HEIGHT / IMAGE_WIDTH) * (mTextureView.getWidth() / mTextureView.getHeight()));
        mTextureView.setTransform(rotate);
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        texture.setDefaultBufferSize(IMAGE_WIDTH, IMAGE_HEIGHT); // 自分の手元のデバイスで決めうちしてます

こう。

今日時点での全体のコードは https://gist.github.com/woshidan/5443e4d0d779ffff036862d7010e14ef

気になってることについては

  • [新しい気になること]デバイスの向きとアプリの向きの差分
  • [既存気になること]CameraDevice解放のタイミングについて
  • [既存気になること]CameraCaptureSession closeのタイミングについて

View AnimationとProperty Animationの違いについて

  • View Animation
    • Animationクラスのサブクラス
    • Viewの見た目の特徴のうち一つをいじるアニメーションのクラス
    • 具体例:
      • TranslateAnimation
      • ScaleAnimation
      • RotateAnimation
      • AlphaAnimation
    • 変化するのはrenderの結果だけで本当の値は変化していない(出典: https://developer.android.com/guide/topics/graphics/prop-animation.html#property-vs-view )
    • テスト用アプリでView Animationのボタンを複数押してもそれらの結果は同時に適用されない
      • それぞれのアニメーション前後でViewの状態が変化していないから、それぞれアニメーション実行前の状態から変化させた結果になる
    • ボタンを押す前後で見た目は変わってもViewのプロパティを調べるログの結果は変化しない
  • Property Animation
    • Viewに生えているメソッド
    • Viewのプロパティを変更するアニメーションのクラス
    • 具体例:
      • View.setTranslationX/Y
      • View.setScaleX/Y
      • View.setRotation, setPivotX/Y
      • View.setAlpha
    • Viewのプロパティを変更している
    • テスト用アプリでProperty Animationのボタンを複数押すとそれらの結果は同時に適用される
    • ボタンを押す前後で見た目は変わってもViewのプロパティを調べるログの結果は変化する

テスト用アプリコードはこちら View Animation vs Property Animation · GitHub

アニメーション: テスト用アプリでView Animationのボタンを複数押してもそれらの結果は同時に適用されない

アニメーション: テスト用アプリでProperty Animationのボタンを複数押すとそれらの結果は同時に適用される

参考

developer.android.com

ImageReaderクラスを触ってみた

今日はまとめる余裕がないのでこっちにおくんじゃ。

  • ImageReaderはAPI19で追加された、他のSurface(入力元=カメラなど)から画像を読み取る & 読み取ったコールバックで加工して他の部分へ流す(たとえばBitmapにしてImageViewに渡す)という使い方ができるSurface
    • ImageはImageReaderがonImageAvailableコールバックが呼び出された時に持ってる、画像のバイト列などが扱えるオブジェクト
      • MaxImagesを見ていると、何枚分かデータが貯められるそうだが、よくわからず
      • 生のbyte列を扱えるが、Bitmapと違いUIへ渡せない
    • 今回は http://woshidan.hatenablog.com/entry/2017/09/06/083000 をもとにして https://developer.android.com/things/training/doorbell/camera-input.html を参考にプレビューを見ながらシャッターを押したらさっき撮った画像が小窓に表示される、みたいなのを書いた
  • 追加でCamera2 APIについて気づいたこと
    • mCameraDevice.createCaptureSession にはそのセッションで出力を送りうるすべてのSurfaceを配列で渡す
    • このタイプのリクエストの結果はこのデバイスに… というのは、 CaptureRequest.Builder.addTarget で設定
    • すべてのSurfaceで画面サイズが適切に設定されていないとセッションが開始されないっぽい
      • 新しく追加した ImageReaderSurface の設定がおかしかった時のエラーが以下
        • Surface with size (w=180, h=180) and format 0x21 is not valid, size not in valid set: [5248x3936, 5248x2952, 3840x2160, 3264x2448, 2048x1536, 1920x1080, 1280x720, 640x480, 480x320, 320x240]
        • CameraDevice-JV-0: Stream configuration failed
  • 追加でもんにゃりしていること
    • Preview画面の向きについて(一番下を見ればわかるが、ずれた)
    • CameraDevice解放のタイミングについて
    • CameraCaptureSession closeのタイミングについて

こちらはコード全体のgist ImageReader初見メモ · GitHub

メモがてらのコメント付きコード断片。

        // ImageReaderインスタンスの初期化
        // MAX_IMAGES個だけ同時にImageオブジェクトが取得できる
        mImageReader = ImageReader.newInstance(IMAGE_WIDTH, IMAGE_HEIGHT, ImageFormat.JPEG, MAX_IMAGES);

        // imageAvailableListener -> 画像準備できた時反応するくん
        // backgroundHandler -> Listenerが実行された時に呼び出されるHandler
        // (に対応したLooperって形で処理を実行するバックグラウンドスレッドの指定)
        mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
    private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        // Surfaceから画像が利用できるようになった時に呼び出される
        @Override
        public void onImageAvailable(ImageReader reader) {
            // Imageは各種コーデック(圧縮方法みたいなもの)で圧縮したりする、画像のByteBufferを扱うためのオブジェクト
            Image image = reader.acquireLatestImage();
            // 何枚か画像を扱うことができて(?)、それぞれはPlanesに入っている
            // この辺のコードは https://developer.android.com/things/training/doorbell/camera-input.html のサンプルより
            ByteBuffer imageBuf = image.getPlanes()[0].getBuffer();
            final byte[] imageBytes = new byte[imageBuf.remaining()];
            imageBuf.get(imageBytes);
            image.close();

            final Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);

            MainActivity.this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ImageView imageView = (ImageView) findViewById(R.id.picture);
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    };
    private void copyPreview() {
        // ImageViewへ静止画を送るためのCaptureRequestを作る
        // 静止画を送ってもらうためのリクエストのビルダーですよ
        CaptureRequest.Builder copyPreviewRequestBuilder = null;
        try {
            copyPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        // 送り先はImageReaderにしてね
        copyPreviewRequestBuilder.addTarget(mImageReader.getSurface());
        CaptureRequest copyPreviewRequest = copyPreviewRequestBuilder.build();

        // (プレビュー時にセッションは開いたままで、)追加で静止画送ってくれリクエストを送る
        try {
            mCaptureSession.capture(copyPreviewRequest, null, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

画面は以下のような感じ。

f:id:woshidan:20170910022201p:plainf:id:woshidan:20170910022212p:plain

参考

Camera API(カメラ関係の古い方)を使ってみる件

まとめ

  • カメラ撮影中の写ってる範囲を表示している部分はプレビューと呼んで SurfaceView で扱う
  • Camera.setParametersでプレビューの時の画像サイズや、プレビュー後(撮影後)の画像サイズが指定できたはずなのだが、compileSDKVersion 25以上で新規プロジェクト作ったらその関数がAndroid Studioから見つからず。。
    • Camera.getParameters().setPreviewSize() などだった
    • PreviewSize(プレビュー中に表示する画像のサイズ)やPictureSize(撮影後にコールバックに渡す画像のサイズ)は機種ごとにサポートしているサイズが決まっているので
  • 撮影後、カメラで撮った画像を処理させるには Camera.PictureCallback を定義して、 Camera.takePicture(ShutterCallback, PictureCallback, PictureCallback)
    • ここで2つ目の PictureCallbackに渡す方のクラスの onPictureTaken(byte[], Camera)を定義することで撮影した画像の処理を書くことができる
    • Camera.takePicture()を呼び出すと、プレビュー部分の画像の更新が止まる.
    • Camera.startPreview() で再びPreview表示を動かすことができる
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.1"
    defaultConfig {
        applicationId "com.example.woshidan.cameratest"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

のとき、Camera APII(カメラ関係の古い方)をちょっと触ってみた。 CameraPreview はほぼ公式のチュートリアルのまま。 以下のコードでは、一回シャッターボタンを押すと画像がボタンを押したタイミングで止まり、もう一度押すとプレビューが再開される、みたいなもの。

public class MainActivity extends Activity {

    private Camera mCamera;
    private CameraPreview mPreview;
    private boolean isCaptured = false;

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

        // Create an instance of Camera
        mCamera = getCameraInstance();

        // mCamera.getParameters().setXXX...でプレビュー画面の設定などのパラメータを設定できる
        List<Camera.Size> supportedSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mCamera.getParameters().setPreviewSize(supportedSizes.get(supportedSizes.size() - 1).width, supportedSizes.get(supportedSizes.size() - 1).height);

        // Create our Preview view and set it as the content of our activity.
        mPreview = new CameraPreview(this, mCamera);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);

        Button captureButton = (Button) findViewById(R.id.button_capture);
        captureButton.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // get an image from the camera
                        if (isCaptured) {
                            mCamera.startPreview(); // 撮影用のプレビューをもう一度動かす
                            isCaptured = false;
                        } else {
                            mCamera.takePicture(null, null, mPicture);
                            // 撮影結果を撮影結果をあれこれするコールバック(Camera.PictureCallback)へ渡す
                            // この関数を呼び出すと、Preview部分の表示の更新は止まる
                            isCaptured = true;
                        }
                    }
                }
        );
    }

    /** A safe way to get an instance of the Camera object. */
    public static Camera getCameraInstance(){
        Camera c = null;
        try {
            c = Camera.open(0); // attempt to get a Camera instance
        }
        catch (Exception e){
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }


    private Camera.PictureCallback mPicture = new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            // 結果をSDカードに書き込んだりする
        }
    };
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.woshidan.cameratest">
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" /><!-- カメラが使えない端末でプレイストアで検索したときこのアプリを表示させない -->
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Holo">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape"
            android:theme="@android:style/Theme.Holo.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ボタンのタッチ領域を広げる

iphone - How to expand the hitTest area of a UIButton without extruding it's background image? - Stack Overflow

CGRectInset - Core Graphics | Apple Developer Documentation

@implementation MyButton : UIButton

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    int expandedMargin = 10;
    CGRect expandedFrame = CGRectInset(self.bounds, - (expandedMargin * 2), - (expandedMargin * 2));
     // Insetの返り値で広がった長方形が欲しいときは値をマイナスに
    return (CGRectContainsPoint(expandedFrame , point) == 1) ? self : nil;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    MyButton* button = [[MyButton alloc] init];
    button.frame = CGRectMake(0, 0, 100, 100);
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
    
    [self.view addSubview:button];
}

- (void)onClick:(UIView *)view {
    NSLog(@"clicked");
}