안드로이드 코틀린 : imageView에 올바르게 회전 된 Bitmap 삽입하기(with CameraX)
Lucy Archive
Lucy 2023
2021. 4. 10. 00:06

Android Kotlin : ImageView에imageView에 올바르게 회전 된 Bitmap 삽입하기(with CameraX) 

안드로이드 imageView에 사진을 보여 줄 때 사진의 방향이 올바르게 보여주지 않는 문제가 있는 것 같습니다. 그 원인은 정확히 원인은 모르겠습니다. 이를 해결하는 방법은 여러 가지가 있는 것 같은데, 이번 포스트에서는

이미지 파일의 메타데이터(Exif)에서 방향(Orientation)을 읽어와서 올바르게 회전된 Bitmap을 ImageView에 넣는 방법

을 소개 합니다. 이 포스트는 CameraX로 카메라를 촬영하는 방법을 사용합니다. 필요하시면 아래의 관련 포스트를 참조해주세요.

CameraX로 사진 찍기 기본 코드

 

안드로이드 코틀린 : CameraX 로 사진 촬영하기

안드로이드 코틀린 : CameraX 라이브러리로 사진 찍기 CameraX 는 카메라 앱 개발을 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리 입니다. CameraX 라이브러리를 사용하여 Snow 어플과 같은 카메라

juahnpop.tistory.com

사진의 메타데이터(Exif) 읽기, 쓰기

 

안드로이드 코틀린 : 사진의 Exif 데이터 읽기, 쓰기 - ExifInterface

Android Kotlin : Exif 데이터 읽기, 쓰기 - ExifInterface Exif는 디지털 카메라 등에 사용되는 이미지 메타데이터 포멧입니다. 이 포스트는 안드로이드 프로그래밍에서 이미지 파일의 Exif 데이터를 읽고

juahnpop.tistory.com

 

사진 회전 하기

사진을 회전 하기 위해 아래 두 가지가 필요합니다.

  • 사진파일의 메타데이터에서 방향(Orientation) 정보를 읽어 오기
  • 방향 정보에 맞게 사진을 회전하기

이미지 파일에서 Orientation 정보 읽어 오기

이미지 파일에서 회전 정보를 불러오기 위해 ExifInterface 클래스를 사용하여 Exif 메타 데이터를 읽어 옵니다. ExifInterface에서 회전 정보를 읽어 오기 위해 TAG_ORIENTATION 태그를 사용합니다. 참고로, 아래 when(orientation) 에 0, 90, 180, 270 4가지 경우만 정의했지만, getAttribute(ExifInterface.TAG_ORIENTATION)의 결과는 좌우 반전, 상하 반전 등의 추가적인 결과 값도 더 있으니 필요에 따라 코드를 추가 해야 합니다. 참고해주세요. 

fun getOrientationOfImage(filepath : String): Int? {
    var exif :ExifInterface? = null
    var result: Int? = null

    try{
        exif = ExifInterface(filepath)
    }catch (e: Exception){
        e.printStackTrace()
        return -1
    }

    val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)
    if(orientation != -1){
        result = when(orientation){
            ExifInterface.ORIENTATION_ROTATE_90 -> 90
            ExifInterface.ORIENTATION_ROTATE_180 -> 180
            ExifInterface.ORIENTATION_ROTATE_270 -> 270
            else -> 0
        }
    }
    return result
}

각도 정보를 이용해 올바르게 회전된 Bitmap 을 얻기

비트맵으로 부터 회전된 비트맵을 가져오기 위해 matrix 클래스를 사용합니다. 새로운 bitmap을 생성하기 위해 Bitmap 클래스의 createBitmap() 메서드를 사용하는데, createBitmap() 메서드는 목적에 따라 다양한 인자를 받을 수 있어, 아래 코드에 사용한 매개변수 설명은 여기를 참고하세요.

private fun rotatedBitmap(bitmap: Bitmap?): Bitmap? {
    val matrix = Matrix()
    var resultBitmap : Bitmap? = null

    when(getOrientationOfImage(filepath)){
        0 -> matrix.setRotate(0F)
        90 -> matrix.setRotate(90F)
        180 -> matrix.setRotate(180F)
        270 -> matrix.setRotate(270F)
    }

    resultBitmap = try{
        bitmap?.let { Bitmap.createBitmap(it, 0, 0, bitmap.width, bitmap.height, matrix, true) }
    }catch (e: Exception){
        e.printStackTrace()
        null
    }
    return resultBitmap
}

 

써먹어 보기

CameraX 라이브러리를 사용하여 사진을 찍은 후, 사진 원본을 미리보기로 보는 예시입니다.

프로젝트 생성

아래와 같이 프로젝트를 생성합니다.

  • 템플릿 : Empty Activity
  • 프로젝트 이름 : CameraX
  • 사용언어 : Kotlin

※ 이 포스트는 Android 4.1.2, Kotlin 1.4.31 버전으로 작성하였습니다.

ViewBinding 설정

App > Gradle Scripts > build.gradle 에 ViewBinding 설정을 위해 아래의 코드를 추가합니다. 코드 추가가 완료되면 Sync Now 를 클랫해서 Gradle 변경 사항을 적용합니다.

android { 
	... 
    viewBinding { 
    	enabled = true 
    } 
}

Gragle 수정 사항 적용

※ ViewBinding은 Layout에 있는 View의 Id를 코틀린 코드에서 직접 사용 할 수 있도록 해주는 도구입니다. View Binding과 관련된 설명은 아래의 링크를 참조해주세요.

안드로이드 View Binding 사용하기

 

안드로이드 View Binding 사용하기 - kotlin-android-extensions 지원 중단

안드로이드 View Binding 방법 정리 안드로이드 코드에서 레이아웃 View에 접근하기 위해 사용된 kotlin-android-extensions 의 지원이 중단예정으로, 이를 대체하여 사용 할 수 있는 ViewBinding 사용법에 대해

juahnpop.tistory.com

 

권한 설정

아래의 두 가지 권한을 AndroidManifest.xml 에 추가합니다.

  • 카메라 : CameraX를 사용한 카메라 사용
  • 저장소 : CameraX를 사용해 촬영한 사진을 저장 및 불러오기
<!--Camera-->
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<!--Storage-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

 

액티비티 추가

예시 앱은 2개의 Activity를 사용할 예정입니다. 아래와 같이 CameraX이름의 Activity를 추가합니다.

  • MainActivity : 권한 확인, CameraX 액티비티 실행 및 사진 결과 보기
  • CameraX(Activity) : CameraX를 사용한 카메라 동작

Activity 추가 : CameraX

 

레이아웃 코드 작성

activity_main.xml 파일의 코드는 아래와 같이 하나의 ImageView와 Button을 추가하였습니다. 각 기능은 아래와 같이 사용합니다.

  • Button
    • CameraX Activity 를 실행함
    • CameraX Activity 실행시 사진을 저장할 경로를 함께 보냄
  • ImageView
    • CameraX Activity에서 정상적으로 사진 촬영이 완료된 경우 마지막에 촬영한 사진을 보여줌
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:text="Open Camera and Get Image"
        android:textSize="13sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_camera_x.xml 파일의 코드는 아래와 같이 작성하였습니다. 카메라 미리보기를 위한 PreView 하나와 사진 찍는 이벤트 처리를 위한 Button 하나를 추가하였습니다.

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

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="fitCenter"
        android:text="Take Photo"
        android:layout_margin="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

    <androidx.camera.view.PreviewView
        android:id="@+id/preView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:scaleType="fitCenter" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt 코드 작성

mainActivity.kt 의 코드는 아래와 같습니다. 코드를 큰 부분으로 나누면 아래와 같습니다. 권한처리 액티비티 호출과 관련된 코드는 이전 포스트들을 참조해주세요. 

  • 권한처리
  • 엑티비티 호출 및 결과 처리
  • 엑티비티 호출시 전달할 파일경로 및 파일명 생성

onActivityResult 함수내에서 회전된 Bitmap을 ImageView에 적용 시키기 위해 아래와 같이 코드를 작성하였습니다.

  • binding.imageView.setImageBitmap(rotatedBitmap(bitmap))
package com.blacklog.camerax

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityMainBinding
import java.io.File
import java.text.SimpleDateFormat

class MainActivity : AppCompatActivity() {

    // ViewBinding
    private lateinit var binding : ActivityMainBinding

    // Permissions
    private val PERMISSIONS = arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    )
    private val PERMISSIONS_REQ = 100

    // Main Program...
    private lateinit var filepath : String
    private val CAMERAX_REQUEST = 100

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        binding.button.setOnClickListener {
            if(checkPermissions(PERMISSIONS, PERMISSIONS_REQ)){
                takePictureAndGetImage()
            }

        }
    }

    /* Main Code Start */
    private fun takePictureAndGetImage() {
        val intent = Intent(this, CameraX::class.java)
        filepath = File(getMyExternalMediaDirs(), newJpgFileName()).absolutePath
        intent.putExtra("filePath", filepath)
        startActivityForResult(intent, CAMERAX_REQUEST)
    }
    /* Main Code End */

    /* Permission Code Start */
    private fun checkPermissions(permissions: Array<String>, permissionsRequest: Int): Boolean {
        val permissionList : MutableList<String> = mutableListOf()
        for(permission in permissions){
            val result = ContextCompat.checkSelfPermission(this, permission)
            if(result != PackageManager.PERMISSION_GRANTED){
                permissionList.add(permission)
            }
        }
        if(permissionList.isNotEmpty()){
            ActivityCompat.requestPermissions(this, permissionList.toTypedArray(), permissionsRequest)
            return false
        }
        return true
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        for(result in grantResults){
            if(result != PackageManager.PERMISSION_GRANTED){
                Toast.makeText(this, "권한 승인 부탁드립니다.", Toast.LENGTH_SHORT).show()
                finish()
                return
            }
        }
        takePictureAndGetImage()
    }
    /* Permission Code End */

    /* On Activity Result Start */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(resultCode == Activity.RESULT_OK){
            when(requestCode){
                CAMERAX_REQUEST -> {
                    binding.imageView.setImageBitmap(BitmapFactory.decodeFile(filepath))
//                    val bitmap = BitmapFactory.decodeFile(filepath)
//                    binding.imageView.setImageBitmap(rotatedBitmap(bitmap))
                }
            }
        }
    }
    /* On Activity Result End */

    /* MainActivity Private Method Start */
    private fun newJpgFileName(): String {
        val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
        val filename = sdf.format(System.currentTimeMillis())
        return "${filename}.jpg"
    }

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


    private fun rotatedBitmap(bitmap: Bitmap?): Bitmap? {
        val matrix = Matrix()
        var resultBitmap : Bitmap? = null

        when(getOrientationOfImage(filepath)){
            0 -> matrix.setRotate(0F)
            90 -> matrix.setRotate(90F)
            180 -> matrix.setRotate(180F)
            270 -> matrix.setRotate(270F)
        }

        resultBitmap = try{
            bitmap?.let { Bitmap.createBitmap(it, 0, 0, bitmap.width, bitmap.height, matrix, true) }
        }catch (e: Exception){
            e.printStackTrace()
            null
        }
        return resultBitmap
    }

    private fun getOrientationOfImage(filepath : String): Int? {
        var exif :ExifInterface? = null
        var result: Int? = null

        try{
            exif = ExifInterface(filepath)
        }catch (e: Exception){
            e.printStackTrace()
            return -1
        }

        val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)
        if(orientation != -1){
            result = when(orientation){
                ExifInterface.ORIENTATION_ROTATE_90 -> 90
                ExifInterface.ORIENTATION_ROTATE_180 -> 180
                ExifInterface.ORIENTATION_ROTATE_270 -> 270
                else -> 0
            }
        }
        return result
    }
    /* MainActivity Private Method End */
}

 

CameraX.kt 코드 작성

CameraX 를 사용하여 사진찍기를 위한 CameraX.kt 의 코드는 아래와 같이 작성하였습니다. CameraX Activity의 버튼을 누르고 정상적으로 사진 찍기가 완료되면 Intent에 Result값으로 RESULT_OK를 입력 후 액티비티를 종료합니다. 액티비티가 종료되면 MainAcivity의 OnActivityResult() 메서드가 호출 됩니다. 

package com.blacklog.camerax

import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityCameraXBinding
import java.io.File

class CameraX : AppCompatActivity() {

    lateinit var binding : ActivityCameraXBinding

    // CameraX
    private var preview : Preview? = null
    private var imageCapture: ImageCapture? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityCameraXBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        startCamera()
        val filepath = intent.getStringExtra("filePath")

        binding.button.setOnClickListener {
            takePicture(filepath!!)
        }
    }

    private fun takePicture(filepath : String) {

        val photoFile = File(filepath)

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture
            .OutputFileOptions
            .Builder(photoFile)
            .build()

        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture?.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e("CameraX_Debug", "Photo capture failed: ${exc.message}", exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val resultIntent = Intent()
                    setResult(Activity.RESULT_OK, resultIntent)
                    finish()
                }
            })

    }

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

        cameraProviderFuture.addListener(Runnable {
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // Preview
            preview = Preview.Builder()
                .setTargetRotation(windowManager.defaultDisplay.rotation)
                .build()
                .also {
                    it.setSurfaceProvider(binding.preView.surfaceProvider)
                }
            // ImageCapture
            imageCapture = ImageCapture.Builder()
                .build()

            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture)

            } catch(exc: Exception) {
                Log.e("CameraX_Debug", "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
}

 

실행 결과

위 프로그램에서 회전된 이미지 적용유무 코드에 따라 실행 결과는 아래와 같이 차이가 납니다.

onActivityResult() 콜백 함수에서 회전된 이미지 적용 코드

val bitmap = BitmapFactory.decodeFile(filepath)
binding.imageView.setImageBitmap(rotatedBitmap(bitmap))

onActivityResult() 콜백 함수에서 회전된 이미지 미적용 코드

binding.imageView.setImageBitmap(BitmapFactory.decodeFile(filepath))

Bitmap 이미지 회전 코드 유뮤에 따른 결과 차이

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드(코틀린) 이미지파일 관련글 목록 보기

🤞 안드로이드(코틀린) 권한 관련글 목록 보기

🤞 안드로이드(코틀린) Intent 관련글 목록 보기

🤞 안드로이드(코틀린) 저장소 관련글 목록 보기

🤞 안드로이드(코틀린) 파일 및 폴더 관련글 목록 보기

🤞 안드로이드(코틀린) 카메라 관련글 목록 보기