[Kotlin/Android] CameraX

Programming/Android 2021. 8. 23. 16:47 Posted by 생각하는로뎅
반응형

 

 

CameraX 개요  |  Android 개발자  |  Android Developers

CameraX 개요   Android Jetpack의 구성요소. CameraX는 카메라 앱 개발을 더 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리입니다. 이는 대부분의 Android 기기에서 작동하는 일관되고 사용하기 쉬운

developer.android.com




Jatpeck 기능 중, CameraX 를 사용해보았다.

기존 Camera 기능 구현과 비교해 볼때, 간단하게 구현해야함에도 불구하고 여러가지 신경을 써야하는 기존 방식보다 안정적이고, 심플했다.

기존에는 자원반납이 제대로 이러우지지 않으면, 카메라가 먹통되는 현상이 있었다. 하지만, CameraX는 Life Cycle 에 맞춰 자동으로 자원반납이 이루어져 보다 안정적있게 사용이 가능했다.

  그리고, 접근하기 어려운 Camera 기법들을 좀더 쉽게 접근이 가능해졌다.

  Image Preview, Image analyze, Image capture 3가지로 구성되어 있다.
  심플해보이지만,  Powerfull한 기능들이 탑제되어  있다.  자세한 내용과 사례는 제일 밑에 있는 유튜브 영상을 참고하면된다.

코드 또한 심플해 보였다.


1. Gradle : app

 

plugins {
    ...
    id 'kotlin-android-extensions'
}

android {
    
    ...
    
    defaultConfig {
        ...
        minSdk 21 // cameraX는 최소 Android 5.0 이상 지원한다.
        ...
    }
    
    // CameraX에는 Java 8의 일부인 메서드가 필요하므로 그에 따라 컴파일 옵션을 설정
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}


dependencies {

    ...

    def camerax_version = "1.0.1"
    // CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:$camerax_version"
    // CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:$camerax_version"
    // CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha27"

}



2. AndroidManifest.xml

 

 <manifest>
 
    <!--
        android.hardware.camera.any하면 장치에 카메라가 있는지 확인합니다.
        .any한다는 것은 전면 카메라 또는 후면 카메라가 될 수 있음을 의미합니다.
    -->
    <uses-feature android:name="android.hardware.camera.any" />
    <uses-permission android:name="android.permission.CAMERA" />
    
     <application ...>
        ...
     </application>
     
 </manifest>

 

3. res/layout/activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/camera_capture_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginBottom="50dp"
        android:scaleType="fitCenter"
        android:text="Take Photo"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:elevation="2dp" />

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>



4. MainActivity.kt

 

import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

private const val TAG = "CameraXBasic"

class MainActivity : AppCompatActivity() {

    companion object {
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSION = arrayOf(Manifest.permission.CAMERA)
    }

    private var imageCapture : ImageCapture? = null

    private lateinit var outputDirectory : File
    private lateinit var cameraExcutor : ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 퍼미션 체크
        if (allPermissionGranted()) {
            // 카메라 시작
            startCamera()
        } else {
            // 권한 요청
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSION, REQUEST_CODE_PERMISSIONS)
        }

        // 캡처 버튼
        camera_capture_button.setOnClickListener{ takePhoto() }

        // 저장할 디렉토리
        outputDirectory = getOutputDirectory()

        // single thread
        cameraExcutor = Executors.newSingleThreadExecutor()

    }

    /**
     * 퍼미션 승인 결과
     */
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionGranted()) {
                startCamera()
            } else {
                Toast.makeText(this, "권한 승인을 하지 않았습니다.", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    /**
     * 사진 찍기
     */
    private fun takePhoto(){

        // null인 경우 함수를 종료합니다.
        // 이미지 캡처가 설정되기 전에 사진 버튼을 탭하면 null이 됩니다.
        // return문이 없으면 앱이 충돌합니다
        val imageCapture = imageCapture ?: return

        // 이미지를 저장할 타임 스탬프 출력 파일 생성
        val photoFile = File(
            outputDirectory,
            SimpleDateFormat(FILENAME_FORMAT, Locale.KOREAN)
                .format(System.currentTimeMillis()) + "jpg")

        // 파일 + 메타데이터를 포함하는 출력 옵션 객체 생성
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    // 촬영에 성공하고, 저장됨을 사용자에게 알리기
                    val savedUri = Uri.fromFile(photoFile)
                    val msg = "사진 캡쳐 성공 : $savedUri"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }

                override fun onError(exception: ImageCaptureException) {
                    // 이미지 캡처에 실패하거나 이미지 캡처 저장에 실패한 경우 오류 사례를 추가하여 실패했음을 기록
                    Log.e(TAG, "사진 캡쳐 실패 : ${exception.message}", exception)
                }
            }
        )

    }

    /**
     * 카메라 시작
     */
    private fun startCamera(){

        // 카메라의 수명 주기를 수명 주기 소유자에게 바인딩하는 데 사용됩니다.
        // CameraX는 수명 주기를 인식하므로 카메라를 열고 닫는 작업이 필요하지 않습니다.
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener(Runnable { // 카메라의 수명 주기를 LifecycleOwner응용 프로그램 프로세스 내에서 바인딩하는 데 사용됩니다 .

            // 카메라의 수명 주기를 수명 주기 소유자에게 바인딩하는 데 사용됩니다.
            val cameraProvider : ProcessCameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    // Preview객체를 초기화하고 빌드를 호출하고 뷰파인더에서 표면 공급자를 가져온 다음 미리보기에서 설정합니다.
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }

            imageCapture = ImageCapture.Builder()
                .build()

            // 평균 이미지 광도 
            var imageLuminosityAnalyzer  = ImageAnalysis.Builder()
                .build()
                .also {
                    it.setAnalyzer(cameraExcutor, LuminosityAnalyzer {
                        luma ->
                        Log.d(TAG, "평균 광도 : $luma")
                    })
                }

            // 기본 뒤쪽 카메라 선택
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {

                // 사용한 binding 모두 해제
                cameraProvider.unbindAll()

                // bind 카메라
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageLuminosityAnalyzer)

            } catch(exc : Exception) {
                // 앱이 더 이상 포커스에 있지 않은 경우와 같이 이 코드가 실패할 수 있음
                Log.e(TAG, "bind 에러", exc)
            }

        }, ContextCompat.getMainExecutor(this)) // 메인 스레드 실행

    }

    /**
     * 권한 체크
     */
    private fun allPermissionGranted() = REQUIRED_PERMISSION.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    private fun getOutputDirectory(): File {
        val mediaDir = externalMediaDirs.firstOrNull()?.let{
            File(it, resources.getString(R.string.app_name)).apply {
                mkdir()
            }
        }
        return if (mediaDir != null && mediaDir.exists()) {
            mediaDir
        } else {
            filesDir
        }

    }

}

typealias LumaListener = (luma: Double) -> Unit

/**
 * 이미지 분석이 가능하다.
 */
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy) {

        val buffer = image.planes[0].buffer
        val data = buffer.toByteArray()
        val pixels = data.map { it.toInt() and 0xFF }
        val luma = pixels.average()

        // 카메라 회전 각도 (카메라 이미지 회전이 아니라, 디바이스 retation 이다.)
        val rotationDegrees = image.imageInfo.rotationDegrees
        Log.d(TAG, "회전 각도 : $rotationDegrees")
        
        // 평균 이미지 광도 리스너 반환
        listener(luma)

        image.close()
    }
}



5. 촬영한 사진을 sdcard에 옮기기


위 소스의 촬영한 사진은 유저가 접근을 하지 못하는 영역에 담겨 있으므로, adb로 따로 불러와야한다.
(물론 외부 저장소에 저장이 가능하도록, 디렉토리 설정은 소스로 수정이 가능)

1) cmd 창을 연다.

adb shell



2) cmd로 촬영한 사진들을 외부 저장소로 copy 한다.

cp -r /storage/emulated/0/Android/media/{앱 패키지명}/{앱 이름}/ /sdcard/Download/



6. 실시간 카메라 데이터


기존 Camera에서 실시간으로 이미지 데이터를 뽑아오려면, Surfaceview 에서 byte[]로 넘어온 YUV 값을 이용해서 카메라 영상 데이터를 실시간으로 취득했다.
CameraX는 analyze에 YUV_420_888 형식으로 넘어온다. 이 값으로 이미지 분석이 가능하다.


7. 기타 링크


1) cameraX

 

CameraX 개요  |  Android 개발자  |  Android Developers

CameraX 개요   Android Jetpack의 구성요소. CameraX는 카메라 앱 개발을 더 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리입니다. 이는 대부분의 Android 기기에서 작동하는 일관되고 사용하기 쉬운

developer.android.com

2) analyze (이미지 분석)

 

이미지 분석  |  Android 개발자  |  Android Developers

이미지 분석 사용 사례에서는 이미지 처리, 컴퓨터 비전 또는 머신러닝 추론을 진행할 수 있도록 CPU에서 액세스 가능한 이미지를 앱에 제공합니다. 애플리케이션은 각 프레임에서 실행되는 분

developer.android.com


3) 공급업체 확장 프로그램

 

공급업체 확장 프로그램  |  Android 개발자  |  Android Developers

CameraX는 휴대전화 제조업체에서 특정 휴대전화용으로 구현한 효과(bokeh, HDR 등)에 액세스하기 위한 API를 제공합니다. 기기가 공급업체 확장 프로그램을 지원하려면 다음 사항이 모두 충족되어야

developer.android.com



8. Android Jetpack: Understand the CameraX camera-support library (Google I/O'19)

 

반응형