今回はAndroidのカメラ機能を使ってみました。
ら、ドハマりしたというお話です。
※2016.05.20更新
Android6.0用にコードを更新した記事を追加しました。
はじめに
Android5.0からカメラをコントロールするAPIが「android.hardware.camera2」に変わりました。
そこで、Android4.4以下の「android.hardware.camera」とAndroid5.0以上のActivityをそれぞれ作成して、APIのバージョンにあったActivityが起動するようにします。
今回は以下のサンプルを元に、Android5.0以上のコードについて書き留めておきます。
- Android L Camera2 API sample ver2 - startPreview&takePicture - torvalbill的专栏 - 博客频道 - CSDN.NET
- googlesamples/android-Camera2Basic - GitHub
やったこと
- ネットで拾ったサンプルを使って、Android端末のカメラを起動して、画像を保存する
- 画面の回転に対応する
AndroidManifest
AndroidManifest.xml
< ?xml version="1.0" encoding="utf-8"? > < manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.cameratest" > < uses-permission android:name="android.permission.CAMERA" / > < uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" / > < uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" / > < uses-feature android:name="android.hardware.camera.autofocus" / > < uses-feature android:name="android.hardware.camera" / > < uses-feature android:name="android.hardware.camera2.full" / > < application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > < activity android:name=".MainActivity" android:label="@string/app_name" > < intent-filter > < action android:name="android.intent.action.MAIN" / > < category android:name="android.intent.category.LAUNCHER" / > < /intent-filter > < /activity > < !-- Android5.0以上で使用 -- > < activity android:name=".Camera2Activity" android:label="@string/title_activity_camera2" android:configChanges="orientation|screenSize" > < /activity > < !-- Android4.4以下で使用 -- > < activity android:name=".CameraActivity" android:label="@string/title_activity_camera" android:configChanges="orientation|screenSize" > < /activity > < /application > < /manifest >
- 「uses-feature」を書いておくと、Google Playで未対応のデバイスを弾くことができるということなのですが、
今回のようにOSバージョンで「camera」か「camera2」のどちらかのみ対応している場合は両方書いても良いのでしょうか。
もう少し調べてみたほうが良さそうです。 * 画面を回転させたときにイベントとして通知を受け取るために、Activityに「android:configChanges="orientation|screenSize"」を追加しています。 - 今回はAndroid5.0以上に対応するため、「Camera2Activity」とこれを呼び出すための「MainActivity」を使用します。
MainActivity
サンプルによってはFragmentを使っている場合もあるようですが、前回まで扱っていたUnity用のプラグインとしても使えるように、ActivityでCameraを扱うこととします。
バージョン別のActivityは、一つにまとめてTargetAPIやif文で切り分けてもよいと思いますが、コード量が多くなりすぎる気がしたので別々のActivityにして、呼び出す部分で指定しています。
MainActivity.java
package jp.cameratest; import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; import android.view.View; import android.view.WindowManager; import android.widget.Button; public class MainActivity extends Activity { private final static int SDKVER_LOLLIPOP = 21; private Button mBtnStart; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // フルスクリーン表示. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_main); mBtnStart = (Button)findViewById(R.id.btn_start); mBtnStart.setOnClickListener(mBtnStartClicked); } private final View.OnClickListener mBtnStartClicked = new View.OnClickListener(){ @Override public void onClick(View v) { if (Build.VERSION.SDK_INT >= SDKVER_LOLLIPOP) { // Camera2を使ったActivityを開く. Intent ittMainView_Camera2 = new Intent(MainActivity.this, Camera2Activity.class); // 次画面のアクティビティ起動 startActivity(ittMainView_Camera2); } else { // Cameraを使ったActivityを開く. Intent ittMainView_Camera = new Intent(MainActivity.this, CameraActivity.class); // 次画面のアクティビティ起動 startActivity(ittMainView_Camera); } } }; @Override protected void onResume() { super.onResume(); } @Override protected void onPause() { super.onPause(); } }
カメラを起動してプレビューに表示する
載せておいてなんですが、長いですね〜。
メソッドの実行順は以下の通りです。
- onCreate : シャッターボタン、カメラのプレビューを表示するTextureViewにLayoutを紐付けています。
- onSurfaceTextureAvailable : TextureViewに関連付けられており、TextureViewが有効になったら実行されます。
- prepareCameraView : 2で実行しており、デバイスで使用するカメラを指定するなどの準備を行います。
- onOpened : カメラが起動したら実行され、5を実行します。
- updatePreview : 別スレッドを使ってTextureViewにカメラから取得した画像を表示します。
※なお、各メソッドに片っ端から@TargetAPIが付いているのはAndroid4.1〜Android4.4にも(別Activityで)対応したいからです。
Camera2Activity.java
package jp.cameratest; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.RectF; import android.media.Image; import android.media.ImageReader; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.StrictMode; import android.util.Log; import android.util.Size; import android.util.SparseIntArray; import android.view.Display; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.Toast; import android.graphics.ImageFormat; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.StreamConfigurationMap; public class Camera2Activity extends Activity { private Size mPreviewSize; private TextureView mTextureView; private CameraDevice mCameraDevice; private CaptureRequest.Builder mPreviewBuilder; private CameraCaptureSession mPreviewSession; private Button mBtnTakingPhoto; private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); static { ORIENTATIONS.append(Surface.ROTATION_0, 90); ORIENTATIONS.append(Surface.ROTATION_90, 0); ORIENTATIONS.append(Surface.ROTATION_180, 270); ORIENTATIONS.append(Surface.ROTATION_270, 180); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build(); StrictMode.setThreadPolicy(policy); // フルスクリーン表示. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_camera); mTextureView = (TextureView)findViewById(R.id.camera2_view); mTextureView.setSurfaceTextureListener(mCameraViewStatusChanged); mBtnTakingPhoto = (Button)findViewById(R.id.btn_taking_photo); mBtnTakingPhoto.setOnClickListener(mBtnShotClicked); } private final TextureView.SurfaceTextureListener mCameraViewStatusChanged = new TextureView.SurfaceTextureListener(){ @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // Textureが有効化されたらカメラを初期化. prepareCameraView(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }; private final View.OnClickListener mBtnShotClicked = new View.OnClickListener(){ @Override public void onClick(View v) { takePicture(); } }; @TargetApi(21) private void prepareCameraView() { // Camera機能にアクセスするためのCameraManagerの取得. CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { // Back Cameraを取得してOpen. for (String strCameraId : manager.getCameraIdList()) { // Cameraから情報を取得するためのCharacteristics. CameraCharacteristics characteristics = manager.getCameraCharacteristics(strCameraId); if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) { // Front Cameraならスキップ. continue; } // ストリームの設定を取得(出力サイズを取得する). StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); // TODO: 配列から最大の組み合わせを取得する. mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0]; // プレビュー画面のサイズ調整. this.configureTransform(); manager.openCamera(strCameraId, new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; createCameraPreviewSession(); } @Override public void onDisconnected(CameraDevice cmdCamera) { cmdCamera.close(); mCameraDevice = null; } @Override public void onError(CameraDevice cmdCamera, int error) { cmdCamera.close(); mCameraDevice = null; } }, null); } } catch (CameraAccessException e) { e.printStackTrace(); } } @TargetApi(21) protected void createCameraPreviewSession() { if(null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) { return; } SurfaceTexture texture = mTextureView.getSurfaceTexture(); if(null == texture) { return; } texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(texture); try { mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e) { e.printStackTrace(); } mPreviewBuilder.addTarget(surface); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { mPreviewSession = session; updatePreview(); } @Override public void onConfigureFailed(CameraCaptureSession session) { Toast.makeText(Camera2Activity.this, "onConfigureFailed", Toast.LENGTH_LONG).show(); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @TargetApi(21) protected void updatePreview() { if(null == mCameraDevice) { return; } // オートフォーカスモードに設定する. mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 別スレッドで実行. HandlerThread thread = new HandlerThread("CameraPreview"); thread.start(); Handler backgroundHandler = new Handler(thread.getLooper()); try { // 画像を繰り返し取得してTextureViewに表示する. mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } 〜省略〜
画像を保存する
Camera2Activity.java
※正直ここが一番良くわかっていません(何をしているかはわかっても、なぜそうしているのか、省略したり別のものに置き換えられないかがわからず。。。)。
また、画像を保存する前にFragmentを使って保存か撮り直しかを選択するよう変更するつもりですが、とりあえず現状のものを載せておきます。
〜省略〜 @TargetApi(21) protected void takePicture() { if(null == mCameraDevice) { return; } CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraDevice.getId()); Size[] jpegSizes = null; int width = 640; int height = 480; if (characteristics != null) { // デバイスがサポートしているストリーム設定からJPGの出力サイズを取得する. jpegSizes = characteristics .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(ImageFormat.JPEG); if (jpegSizes != null && 0 < jpegSizes.length) { width = jpegSizes[0].getWidth(); height = jpegSizes[0].getHeight(); } } // 画像を取得するためのImageReaderの作成. ImageReader reader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1); ListoutputSurfaces = new ArrayList (2); outputSurfaces.add(reader.getSurface()); outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture())); final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(reader.getSurface()); captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); // 画像を調整する. int rotation = getWindowManager().getDefaultDisplay().getRotation(); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation)); // ファイルの保存先のディレクトリとファイル名. String strSaveDir = Environment.getExternalStorageDirectory().toString(); String strSaveFileName = "pic_" + System.currentTimeMillis() +".jpg"; final File file = new File(strSaveDir, strSaveFileName); // 別スレッドで画像の保存処理を実行. ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = null; try { image = reader.acquireLatestImage(); // TODO: Fragmentで取得した画像を表示.保存ボタンが押されたら画像の保存を実行する. ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); saveImage(bytes); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (image != null) { image.close(); } } } private void saveImage(byte[] bytes) throws IOException { OutputStream output = null; try { // 生成した画像を出力する. output = new FileOutputStream(file); output.write(bytes); } finally { if (null != output) { output.close(); } } } }; // 別スレッドで実行. HandlerThread thread = new HandlerThread("CameraPicture"); thread.start(); final Handler backgroudHandler = new Handler(thread.getLooper()); reader.setOnImageAvailableListener(readerListener, backgroudHandler); final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { // 画像の保存が終わったらToast表示. super.onCaptureCompleted(session, request, result); Toast.makeText(Camera2Activity.this, "Saved:"+file, Toast.LENGTH_SHORT).show(); // もう一度カメラのプレビュー表示を開始する. createCameraPreviewSession(); } }; mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { try { session.capture(captureBuilder.build(), captureListener, backgroudHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession session) { } }, backgroudHandler); // 保存した画像を反映させる. String[] paths = {strSaveDir + "/" + strSaveFileName}; String[] mimeTypes = {"image/jpeg"}; MediaScannerConnection.scanFile(getApplicationContext(), paths, mimeTypes, mScanSavedFileCompleted); } catch (CameraAccessException e) { e.printStackTrace(); } } private MediaScannerConnection.OnScanCompletedListener mScanSavedFileCompleted = new MediaScannerConnection.OnScanCompletedListener(){ @Override public void onScanCompleted(String path, Uri uri){ // このタイミングでToastを表示する? } }; 〜省略〜
画面の回転
ここまででカメラの基本的な機能は実行できるかと思います。
しかし、このまま画面を回転させると、Viewやボタンの表示位置自体は問題ないのものの表示される映像が傾いてしまいます。
で、TextureViewをそれに合わせて回転させると、TextureViewの大きさが画面いっぱいにならなくなります。
それを(無理やり)なんとかしてみたのがこちら。
Camera2Activity.java
@TargetApi(21) public void onConfigurationChanged(Configuration newConfig) { // 画面の回転・サイズ変更でプレビュー画像の向きを変更する. super.onConfigurationChanged(newConfig); this.configureTransform(); } @TargetApi(21) private void configureTransform() { // 画面の回転に合わせてmTextureViewの向き、サイズを変更する. if (null == mTextureView || null == mPreviewSize) { return; } Display dsply = getWindowManager().getDefaultDisplay(); int rotation = dsply.getRotation(); Matrix matrix = new Matrix(); Point pntDisplay = new Point(); dsply.getSize(pntDisplay); RectF rctView = new RectF(0, 0, pntDisplay.x, pntDisplay.y); RectF rctPreview = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth()); float centerX = rctView.centerX(); float centerY = rctView.centerY(); rctPreview.offset(centerX - rctPreview.centerX(), centerY - rctPreview.centerY()); matrix.setRectToRect(rctView, rctPreview, Matrix.ScaleToFit.FILL); float scale = Math.max( (float) rctView.width() / mPreviewSize.getWidth(), (float) rctView.height() / mPreviewSize.getHeight() ); matrix.postScale(scale, scale, centerX, centerY); switch (rotation) { case Surface.ROTATION_0: matrix.postRotate(0, centerX, centerY); break; case Surface.ROTATION_90: matrix.postRotate(270, centerX, centerY); break; case Surface.ROTATION_180: matrix.postRotate(180, centerX, centerY); break; case Surface.ROTATION_270: matrix.postRotate(90, centerX, centerY); break; } mTextureView.setTransform(matrix); }
- TextureViewの回転にはMatrixを使います。
- Matrixでサイズ変更をするには、直接widthやheightを指定できないため、Scale値を指定します。
- TextureViewをそのまま回転させると位置がずれてしまうため、画面のwidthやheightの中心点を指定して回転させます。
- 先にも触れましたが、AndroidManifest.xmlに「android:configChanges="orientation|screenSize"」を加えて画面の回転時に「onConfigurationChanged」が呼ばれるようにします。
- 現状の問題として、画面を縦にした時と横にした時でプレビューに映るものの大きさが違っている、ということが挙げられます。これも何とかしたいところです。
終わりに
簡単なことしかしていないはずなのに、書くことがずいぶんたくさんありますね。
もう少しシンプルにできないかと思ったりするのですが、皆様どうしているのでしょうか...。
画像を保存する前の確認画面、Android4.4以下向けのカメラ機能を作ったら、OpenCVを使った画像編集にも挑戦したいです。
参考
Camera2
- googlesamples/android-Camera2Basic - GitHub
- Android L Camera2 API sample ver2 - startPreview&takePicture - torvalbill的专栏 - 博客频道 - CSDN.NET
- Lollipopのカメラサンプルアプリ (Camera2Basic, Camera2Video) - Androidアプリ公開までがんばってみるブログ
- Camera2BasicとCamera2 APIの調査 - Androidアプリ公開までがんばってみるブログ
- Camera2 APIでカメラデバイスをopenする - Androidアプリ公開までがんばってみるブログ
- Camera2でカメラのPreviewを表示する(1) - Androidアプリ公開までがんばってみるブログ
- Camera2でカメラのPreviewを表示する(2) - Androidアプリ公開までがんばってみるブログ
- Camera API 3.0を触ってみた - slideshare
- グッときた瞬間を記録するAndroidの新しいカメラAPI - フェンリル - デベロッパーズブログ