CameraDevice closeのタイミングとCameraCaptureSession closeのタイミングについて
https://github.com/googlesamples/android-Camera2Basic のサンプルから確認してメモする。
まとめ
CameraDevice
はCameraDevice.StateCallback
のコールバックの中でcloseするCameraCaptureSession
はonPause
でclose
していた
コード
/** * {@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(); } } };
@Override public void onPause() { closeCamera(); stopBackgroundThread(); super.onPause(); }
/** * 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(); } }
気になってることについては
とりあえずウェブオペレーションが2周目読んでからじゃないと次行っちゃダメっぽい感じで並行処理は来月かなー。
リリースについてのetc.
なんかあったら追加する。
- Android
- ベータ版リリースはクローズドベータで指定したメアドのGoogleアカウントでのみインストール、アップデート可能に
- ベータ版リリースは2回目以降は
- 更新に~1h程度かかる
- 同じVersionCodeのものは出せない
- その辺はアルファ使う?
- ベータ版から本番へ適用する時のステップはGoogle Play Consoleのアップデートによって変わることがあるが、こないだ試したらベータ版リリース -> 「ROLLOUT TO PRODUCTION」-> 本番へのドラフト追加 -> 本番からリリースだった
アプリで決めたActivityの向きがLANDSCAPEの時、Camera2のCameraDeviceから受け取るカメラのプレビューの向きがずれる
昨日のImageReaderのメモで プレビューの向きがおかしいのが気になる、という話を書きました。
その件について調べると
などどうもアプリの画面の向きが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のボタンを複数押すとそれらの結果は同時に適用される
参考
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 を参考にプレビューを見ながらシャッターを押したらさっき撮った画像が小窓に表示される、みたいなのを書いた
- ImageはImageReaderがonImageAvailableコールバックが呼び出された時に持ってる、画像のバイト列などが扱えるオブジェクト
- 追加でCamera2 APIについて気づいたこと
- mCameraDevice.createCaptureSession にはそのセッションで出力を送りうるすべてのSurfaceを配列で渡す
- このタイプのリクエストの結果はこのデバイスに… というのは、
CaptureRequest.Builder.addTarget
で設定 - すべてのSurfaceで画面サイズが適切に設定されていないとセッションが開始されないっぽい
- 新しく追加した
ImageReader
のSurface
の設定がおかしかった時のエラーが以下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(); } }
画面は以下のような感じ。
参考
- Add Camera Support | Android Things
- ImageReader | Android Developers
- Image | Android Developers
- https://developer.android.com/about/versions/android-4.4.html
- ImageReaderの概要としてはここの
Surface Image Reader
の項目がわかりやすいです
- ImageReaderの概要としてはここの
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表示を動かすことができる
- ここで2つ目の
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>
ボタンのタッチ領域を広げる
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"); }