본문 바로가기

카테고리 없음

Android Camera 다루기

오늘은 안드로이드 카메라를 다루는 것에 대해서 몇가지 적어보려고 합니다. 

안드로이드 기본카메라나, Google Play Store에 올라와 있는 Thrid Party 카메라 앱들은 어떻게 구현되어 있는지에 대해서 곰곰히 생각해봤습니다. Thrid Party 카메라 앱들은 심지어 필터까지 적용도 했구요..!

너무 궁금해서 여러 레퍼런스들을 찾아보고 구현에 성공하기도 하여서 몇가지 적어보려고 합니다.

저는 이번 공부를 하기전에 카메라를 찍고 이미지를 가져오려면 Intent를 사용하여서 MediaStore.ACTION_IMAGE_CAPTURE 로 생성하여 이미지를 가져오는것 밖에 떠오르지 않았습니다. 

근데 여러 카메라 앱들을 설치해서 어떤식으로 동작하나 조사해보니 굳이 이 방법이 아니더라도 Custom 하여 쓸 수 있더군요..!

 

오늘은 카메라를 띄우고 Custom Button을 누르면 찍고 저장하는 것까지 구현해보겠습니다.

 

우선 API 레벨이 21 전까지 Android.Camera를 쓰기 위해선 andorid.hardware의 Camera()를 썻던 것으로 보입니다. 

그리고 API 21 이후 부터는 deprecated 되어 더 이상 업데이트를 지원하지 않는 다고 합니다. 

그래서 이를 해결하기 위해 여러 레퍼런스를 찾아본결과 SurfaceView라는 것을 이용하거나 androidx의 Camera Core를 이용하는게 일반적으로 보입니다. 

 

그래서 오늘은 androidx의 카메라를 이용해서 한번 핸들링 해보겠습니다! 

우선 gradle에 카메라를 추가해 보겠습니다. 

def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
//비디오를 구현하고 싶다면 
//implementation "androidx.camera:camera-video:${camerax_version}"

Camera Permission도 꼭 추가해야하며 permission 작업은 별도로 블로그에 작성하지 않겠습니다. 

 

그다음 layout은 

원하시는 대로 구현하시면 됩니다!

같이 진행할 예제를 위해 Button 하나와 androidx.camera.core.PreviewView 두가지는 필수적으로 구현 해주세요.

button의 id는 button, androidx.camera.core.PreviewView의 id는 previewView 로 네이밍 지어서 진행해 보겠습니다.

 

해당 기능을 구현하기 위한 함수는 딱 두가지로 만들겠습니다.

camera를 띄우는 것, 사진을 찍는 것

우선 camera를 띄우는 코드 입니다. 

 

private fun openCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener( {
            val cameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build().also {
                    it.setSurfaceProvider(binding.previewView.surfaceProvider)
                }

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)

            } catch (e: Exception) {
                Log.d("camera Control : ", "${e.message}")
            }
        }, ContextCompat.getMainExecutor(this))
    }

ProcessCameraProvider는 Activity나 Fragment의 LifeCycleOwner을 카메라의 생명주기에 binding 해주기 위한 객체 입니다. 

이를 통해 Camera의 생명주기를 지정하고, Camera Preview를 CameraProviderFuture에게 줘야 합니다.(setSurfaceProvider())

CameraSelector로 전면카메라(DEFAULT_FRONT_CAMERA) or 후면카메라(DEFAULT_BACK_CAMERA)를 지정 할 수 있습니다. 

 

여기 까지 하면 Camera를 Preview에서 확인 가능 합니다. 

다르게 말씀드리면 Camera를 통해 미리보기를 구현했다고 표현할 수도 있겠네요.

 

이제 버튼을 누르면 Preview영역에 보이는 것을 캡처하는 사진 찍기 기능을 추가해 보겠습니다. 

 

private fun takePhoto() {
        val name = SimpleDateFormat("yyyyMMddHHmmss", Locale.KOREA).format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/WooJooCamera")
            }
        }

        val outputOptions = ImageCapture.OutputFileOptions.Builder(contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues).build()

        imageCapture?.takePicture(outputOptions, ContextCompat.getMainExecutor(this), object: ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                val msg = "Photo capture succeeded: ${outputFileResults.savedUri}"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d("WooJooCamera", msg)
            }

            override fun onError(exception: ImageCaptureException) {
                Log.e("WooJooCamera save Fail", "Photo capture failed: ${exception.message}", exception)
            }

        })

    }

 

imageCapture 변수는 사진찍을때마다 생성 하는건 아닌 것 같아서 먼저 전역으로 선언하고 onCreate할때

ImageCapture.Builder().setTargetRotation(this.windowManager.defaultDisplay.rotation).build()

으로 정의 하였습니다. 

 

이어서 설명하자면 먼저 image를 저장할 이름을 SimpleDataFormat()을 통해서 이미지 파일의 이름을 정의 합니다.

그 다음 Image를 캡쳐할때 Android 내부 DB에 저장할 Builder를 정의 합니다.

ContextWrapper로 부터 ContentResolver를 받아오고 이미지의 URI, 그리고 ContentValue를 생성자로 넣어 build()를 통해 정의 합니다. (Android 컴포넌트가 필요한게 Intent라면, ContentsResolver는 ContentValues()를 필요로 합니다. 여기서 정의한 값 대로 데이터를 처리 합니다. 필요한 칼럼값과 칼럼에 해당할 값들을 넣어줬습니다. 이미지의 이름, 확장자, 저장할 경로)

 

그 다음 onCreate에 지정한 ImageCapture의 takePicture()를 호출하면 끝입니다.

takePicture() 메소드의 내부는 아래와 같습니다. 

public void takePicture(
            final @NonNull OutputFileOptions outputFileOptions,
            final @NonNull Executor executor,
            final @NonNull OnImageSavedCallback imageSavedCallback) {
        if (Looper.getMainLooper() != Looper.myLooper()) {
            CameraXExecutors.mainThreadExecutor().execute(
                    () -> takePicture(outputFileOptions, executor, imageSavedCallback));
            return;
        }

Executor는 callback을 실행할 Thread를 관리하는 Executor를, OnImageSavedCallback은 이미지를 캡쳐하여 저장할때 성공, 실패를 정의할 수 있는 Interface입니다. 

 

이렇게 하게 되면 Preview영역에 Camera를 확인 할 수 있고 버튼을 누름으로써 Preview에 연결된 ImageCapture를 통해 캡처 할 수 있는 것을 확인 할 수 있습니다.