高専 Advent Calendar 2018 2日目
この記事は高専 Advent Calendar 2018の2日目の記事です。 前日の記事はTorikaraHNMさんの英語キーボードを使い始めた話です。
予定では、量子コンピュータに関して書く予定だったのですが、何を思ったか先に書いてしまったという失態...。
なので、題目を変更しました...。
Kotlin + Camera v2
タイトルにもあるように、KotlinでCamera API 2を使って実装する記事が結構少ないように思われました。なので、最低限の実装で撮影ができるようになるまでを書いていきたいと思います。
とりあえず、Kotlin等のインストールは省略します。
Camera API v2
Camera API v2はCamera API v1と比べて、かなり自由度は高くなりました。しかし、v1と違ってカメラのイベント関係をJava層まで落とし込んだインターフェスになっているので扱いは難しいです。
パーミッションの追加
カメラを利用するために、Manifestにuses-permission
を追加します。カメラで撮影した写真を保存するために、ストレージへアクセスすることになるため、ストレージへのアクセスも許可します。
// AndroidManifest.xml <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.hardware.camera.autofocus" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
requestPermission
このrequestPermission
でカメラの利用許可を行わないと、まずカメラが使えません。なので、カメラ使用のリクエストを発行します。このrequestPermissionを呼び出す順番としては以下の通りで、openCamera
関数では、カメラ利用のための許可申請と、カメラを利用するための設定等を行います。そのため、この関数からカメラを使うためのいくつかの設定を行います。
TextureView に surfaceTextureListener を設定する ↓ コールバック関数が呼び出され、関数内のオーバーライドメソッド(onSurfaceTextureAvailable)が呼ばれる ↓ openCamera() を呼び出す → requestPermission() を呼び出す ↓ Cameraの状態をコールバックする関数を設定する(stateCallback) ↓ ↓ カメラの準備ができたら ↓ Cameraから取得したデータをプレビューする(createCameraPreviewSession)
使う変数としてはこんな感じですね。もっと簡略化できるのかもしれませんが、僕のではこうなりました^^;
companion object { const val PERMISION_CAMERA = 200 const val PERMISION_WRITE_STORAGE = 1000 const val PERMISION_READ_STORAGE = 1001 } /** * 各レイアウトオブジェクト変数を生成 */ private lateinit var shutterButton : ImageButton private lateinit var numberPicker : NumberPicker private lateinit var previewView : TextureView private lateinit var imageReader : ImageReader /** * 各種変数初期化 */ private lateinit var previewRequestBuilder : CaptureRequest.Builder private lateinit var previewRequest : CaptureRequest private var backgroundHandler : Handler? = null private var backgroundThread : HandlerThread? = null private var cameraDevice : CameraDevice? = null private lateinit var captureSession : CameraCaptureSession
backgroundThread
カメラの処理はバックグラウンドで起動させるために、Thread処理を挟みます。カメラをバックグラウンドで起動させるための関数を作成して、onCreate
で呼び出します。backgroundThread
変数は後述するcreateCameraPreviewSession
関数内でのカメラ設定に利用します。
/** * カメラをバックグラウンドで実行 */ private fun startBackgroundThread() { backgroundThread = HandlerThread("CameraBackground").also { it.start() } backgroundHandler = Handler(backgroundThread?.looper) } // onCreate内で呼び出す startBackgroundThread()
surfaceTextureListener
プレビューを表示するためのTextureView(previewView)にListenerを持たせます。これによってpreviewViewの変化をトリガーとして使うことができます。TextureViewが有効になったらプレビューをする準備をして、openCamera
をコールします
/** * テクスチャビューにイベント作成 */ previewView.surfaceTextureListener = surfaceTextureListener /** * TextureView Listener */ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { // TextureViewが有効になった override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG,2) openCamera() } // TextureViewのサイズが変わった override fun onSurfaceTextureSizeChanged(p0: SurfaceTexture?, p1: Int, p2: Int) { } // TextureViewが更新された override fun onSurfaceTextureUpdated(p0: SurfaceTexture?) { } // TextureViewが破棄された override fun onSurfaceTextureDestroyed(p0: SurfaceTexture?): Boolean { return false } }
openCamera
Camera API 2ではカメラを起動する際に、「カメラマネジャー」を使って、カメラを管理します。そのため、カメラマネジャーの設定、カメラIDの取得などを行います。
/** * カメラ起動処理関数 */ private fun openCamera() { /** * カメラマネジャーの取得 */ val manager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager try { /** * カメラIDの取得 */ val camerId: String = manager.cameraIdList[0] /** * カメラ起動パーミッションの確認 */ val permission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) if (permission != PackageManager.PERMISSION_GRANTED) { requestCameraPermission() return } /** * カメラ起動 */ manager.openCamera(camerId, stateCallback, null) } catch (e: Exception) { e.printStackTrace() } }
requestPermission
さて、openCamere
でパーミッションの確認が行われるわけですが、すでに許可されている時には、何事もなかったようにカメラが起動されますが、初回起動時などには、おなじみの許可ダイアログが表示されるようにします。
/** * カメラ利用許可取得ダイアログを表示 */ private fun requestCameraPermission() { if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { AlertDialog.Builder(baseContext) .setMessage("Permission Check") .setPositiveButton(android.R.string.ok) { _, _ -> requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISION) } .setNegativeButton(android.R.string.cancel) { _, _ -> finish() } .show() } else { requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISION) } }
.setPositiveButton(button_string) { dialog, which -> }.show()
って書き方がどうも慣れない人が多いようです。{ _, _ -> }
となっていても問題はなくて、dialog, which
を使わないのであれば不要です。って言っているようなものだと思ってください。
stateCallback
カメラの許可も取れて、あとは起動するだけですが、起動する時にカメラに接続できているかどうかを確認する必要があります。なので、manager.openCamera(camerId, stateCallback, null)
でカメラの状態を取得するためのコールバック関数を指定して、その関数の中で状態を把握します。もし、カメラになんらかの変化があれば、この関数にあるメソッドが呼ばれることになります。
/** * カメラ状態取得コールバック関数 */ private val stateCallback = object : CameraDevice.StateCallback() { /** * カメラ接続完了 */ override fun onOpened(cameraDevice: CameraDevice) { this@TakePicture.cameraDevice = cameraDevice createCameraPreviewSession() } /** * カメラ切断 */ override fun onDisconnected(cameraDevice: CameraDevice) { cameraDevice.close() this@TakePicture.cameraDevice = null } /** * カメラエラー */ override fun onError(cameraDevice: CameraDevice, error: Int) { onDisconnected(cameraDevice) finish() } }
createCameraPreviewSession
createCameraPreviewSession
では、カメラから取得したデータをTextureViewにプレビューするための設定をします。setDefaultBufferSize
でpreviewView
の実機での表示サイズを取得して、textureのサイズを決めます。その後、カメラから取得したデータをtextureViewに投影します。
/** * カメラ画像生成許可取得ダイアログを表示 */ private fun createCameraPreviewSession() { try { val texture = previewView.surfaceTexture texture.setDefaultBufferSize(previewView.width, previewView.height) val surface = Surface(texture) previewRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) previewRequestBuilder.addTarget(surface) cameraDevice?.createCaptureSession(Arrays.asList(surface, imageReader.surface), @RequiresApi(Build.VERSION_CODES.LOLLIPOP) object : CameraCaptureSession.StateCallback() { override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { if (cameraDevice == null) return try { captureSession = cameraCaptureSession previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) previewRequest = previewRequestBuilder.build() cameraCaptureSession.setRepeatingRequest(previewRequest, null, Handler(backgroundThread?.looper)) } catch (e: CameraAccessException) { Log.e("erfs", e.toString()) } } override fun onConfigureFailed(session: CameraCaptureSession) { //Tools.makeToast(baseContext, "Failed") } }, null) } catch (e: CameraAccessException) { Log.e("erf", e.toString()) } }
主に、カメラのデータをTextureViewにプレビューしている部分はここらへんになります。
cameraDevice?.createCaptureSession(Arrays.asList(surface, imageReader.surface), @RequiresApi(Build.VERSION_CODES.LOLLIPOP) object : CameraCaptureSession.StateCallback() { override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { if (cameraDevice == null) return try { captureSession = cameraCaptureSession previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) previewRequest = previewRequestBuilder.build() cameraCaptureSession.setRepeatingRequest(previewRequest, null, Handler(backgroundThread?.looper)) } catch (e: CameraAccessException) { Log.e("erfs", e.toString()) } } override fun onConfigureFailed(session: CameraCaptureSession) { //Tools.makeToast(baseContext, "Failed") } }, null )
requestStoragePermission
カメラのデータを保存するために、ストレージアクセスをします。そのため、ストレージにアクセスするための権限を取得します。要領はカメラの時と同じです。以下のコードはパーミッションを確認する時に呼び出すものです。基本的には、アプリ起動時にすぐ呼び出す方がいいかと思います。が、ご自由にどうぞ。
/** * ストレージ読み書きパーミッションの確認 */ val writePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) val readPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) if ( (writePermission != PackageManager.PERMISSION_GRANTED) || (readPermission != PackageManager.PERMISSION_GRANTED) ) { requestStoragePermission() }
requestStoragePermission
関数の内容は以下の通りです。カメラとの違いは、書き込みと読み込みの2つを行なっていることです。もしかしたら片方すればいいかと思うんですが、とりあえず2つしておきます。詳しい人教えてください...。
private fun requestStoragePermission() { /** * 書き込み権限 */ if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { AlertDialog.Builder(baseContext) .setMessage("Permission Here") .setPositiveButton(android.R.string.ok) { _, _ -> requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), WRITE_STORAGE_PERMISION) } .setNegativeButton(android.R.string.cancel) { _, _ -> finish() } .create() } else { requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), WRITE_STORAGE_PERMISION) } /** * 読み込み権限 */ if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) { AlertDialog.Builder(baseContext) .setMessage("Permission Here") .setPositiveButton(android.R.string.ok) { _, _ -> requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_PERMISION) } .setNegativeButton(android.R.string.cancel) { _, _ -> finish() } .create() } else { requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_PERMISION) } }
シャッターボタンへのアクション
これでカメラのプレビューまで終わったわけですが、撮影しないと始まらないので、シャッターボタンを作成して押されたら表示してるプレビュー画像をファイルとして保存します。保存先はDocuments
になります。DIRECTORY_DOCUMENTS
の部分を変更すれば、別のディレクトリに対してフォルダを作成することができます(この場合だとcameraPreview
というフォルダを作成しています)
シャッターボタンが押されると、TextureViewの更新を停止して表示されている画像をファイルとして保存します。そして、再度TextureViewの更新を行うことでサイクルが完成します。
/** * シャッターボタンにイベント生成 */ shutterButton.setOnClickListener { val appDir = File(getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS), "cameraPreview") try { val filename = "test_picture.jpg" var savefile : File? = null /** * プレビューの更新を止める */ captureSession.stopRepeating() if (previewView.isAvailable) { savefile = File(appDir, filename) val fos = FileOutputStream(savefile) val bitmap: Bitmap = previewView.bitmap bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) fos.close() } if (savefile != null) { Log.d("edulog", "Image Saved On: $savefile") Toast.makeText(this, "Saved: $savefile", Toast.LENGTH_SHORT).show() } } catch (e: CameraAccessException) { Log.d("edulog", "CameraAccessException_Error: $e") } catch (e: FileNotFoundException) { Log.d("edulog", "FileNotFoundException_Error: $e") } catch (e: IOException) { Log.d("edulog", "IOException_Error: $e") } /** * プレビューを再開 */ captureSession.setRepeatingRequest(previewRequest, null, null) }
まとめ
最低限必要な実装だけで、Camera API 2をKotlinで使うことができるようになったはずです。(これが最小限かどうかはわかりませんが...。)ただし、画面の回転でTextureViewの変更などは考えていません。画面の回転等はちょっと面倒なんですよねー。端末の物理的回転とシステム上で認識されている回転とがずれているので、そこを補助する必要があります...。回転とか、カメラのいろいろに関しては、こちらのソースを参考にしてください。こちらのOSSは自分が一昔前に関わらさせていただいていたものです。更新はされてないようですが。。。Javaで書かれているんですが、カメラ部分などは参考にできるかと(結構面倒な処理とかもして大変でした...。)
実はiPhone版もあったりするので、みるだけでも損はないと思います!笑
ソースコード
ソースコードですが、現在開発中のシステムで、これを構築していたので全体の公開はすることができません。そのため、コメントやTwitterなどでリクエストがあれば、詳しく説明 or 一部コードの提供をさせていただきます!!!
次の記事
次の「高専 Advent Calendar 2018」記事は、ykbr_さんの「Goで機能を追加しつつ私のクローンを書き直した」です。ぜひそちらもみてみてください!!