[Android App] 명언 제조기

앱 설명

간단하게 사진과 그리고 이름, 명언, 년도를 입력하면 명언을 제조해주는 애플리케이션이다.
 

기능

제공하는 기능은 다음과 같다.

  • 입력된 사진에 배경을 제거하여, 인물의 얼굴만 남는다 (removal background)
  • 입력된 정보를 바탕으로 명언을 제조한다.
  • 화면 캡쳐기능을 통해 사진을 갤러리에 저장한다.

 

구현

Removal Background

배경 제거의 경우에는 opencv에 grabcut 알고리즘을 사용해서 구현해보고자 했다.
구현하는데 어려움이 크고, 비전관련 배경지식이 없는터라 만들어진 라이브러리를 찾아보고자 했다.
 
만들어진 라이브러리 중 유로를 제외하고 오픈소스로 제공되는 라이브러리 중 
 
https://github.com/GhayasAhmad/auto-background-remover

GitHub - GhayasAhmad/auto-background-remover: A library for auto removing background from your photos.

A library for auto removing background from your photos. - GitHub - GhayasAhmad/auto-background-remover: A library for auto removing background from your photos.

github.com

해당 라이브러리를 사용했다. 
 
 하지만, 외곽선이 깔끔하게 제거되지 않는 문제점와 배경과 인물의 경계가 모호한 경우 배경 제거가 제대로 되지 않는 문제가  있었다. 지금 당장은 크게 급한 문제가 아니기에 그냥 사용하기로 했다.
 
사용법은 간단하다. 이미지 bitmap만 입력값으로 주면 된다. 
아래와 같이 bitmap을 입력값으로 주고, 해당 처리에 대한 결과가 성공을 하면, 해당 상황에서의 처리를 해주면 된다. 
성공의 결과는 bitmap에 담겨있기 때문에, Glide를 통해 화면에 보여지도록 하였다.

BackgroundRemover.bitmapForProcessing(
                    bitmap!!,
                    true,
                    object: OnBackgroundChangeListener {
                        override fun onSuccess(bitmap: Bitmap) {
                            //do what ever you want to do with this bitmap
                            binding.iv.apply {
                                Glide.with(this@MainActivity).load(bitmap).into(this)
                                val matrix = ColorMatrix()
                                matrix.setSaturation(0f)
                                colorFilter = ColorMatrixColorFilter(matrix)
                            }
                        }

                        override fun onFailed(exception: Exception) {
                            //exception
                        }
                    }
                )

아래 코드의 경우에는 명언 사진을 보면 대부분 흑백 화면 느낌이 많다. (뭔가 흑백이면 신뢰도가 높아보인다고 해야하나?) 
그렇기에 이미지를 흑백으로 변환해주는 과정을 추가하였다.

val matrix = ColorMatrix()
matrix.setSaturation(0f)
colorFilter = ColorMatrixColorFilter(matrix)

 

Serializable

입력된 데이터를 Activity 간 전달을 해주어야 한다. 
이 때, Data Class 형태로 데이터를 공유하기 위해 Serializable을 사용했다.
 

data class Quote(
    val name : String?,
    val date : String?,
    val quote : String?,
) : Serializable

해당 data class는 [이름 / 날짜 / 명언] 이다.
 
intent로 념겨 줄 때는 putExtra를 통해 전달을 해주고, 받는 액티비티에서는 getSerializableExtra를 이용해서 받으면 된다. 
이 때 아래와 같이 as Quote로 형변환을 해주어야 한다.

intent.getSerializableExtra("quote") as Quote

 

갤러리에서 사진 가져오기

배경제거를 위해서는 사진이 필요하다. 사진은 갤러리에서 사진을 선택하도록 하였다.
 
사진을 가져오기 위해 다음과 같이 작성해주어야 한다.

private fun openGallery() {
        val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        startActivityForResult(intent, PICK_IMAGE_REQUEST)
}

 
사진을 선택한 결과로는 onActivityResult를 사용해서 선택에 결과로 자신이 원하는대로 구현을 해주면 된다.
PICK_IMAGE_REQUEST의 경우에는 상수값이 들어가며, requestCode를 통해 요청을 구분할 수 있다.
사진 선택의 결과로 Glide를 통해 화면에 보여지도록 하였다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when(requestCode)
        {
            PICK_IMAGE_REQUEST -> {
                uri = data?.data
                Glide.with(this@SettingActivity).load(uri).into(binding.photoIv)
            }
        }
    }

캡쳐 기능

캡쳐 기능은 floating 버튼을 눌렀을 떄, 화면을 캡쳐하여 갤러리에 저장하도록 하였다.
floating 버튼을 눌렀을 때, 캡쳐하고 싶은 영역의 View를 입력값으로 넣어주게 되면, 해당 영역의 bitmap을 반환한다.
반환된 bitmap을 현재 시간을 이름으로 file을 만들어준다.
이때, SDK 버전에 따라 다르게 저장을 해주어야하므로, 구분을 해주었다. 사진 저장이 완료되면 Toast 메세지를 통해 알림을 제공한다.
 

 private fun saveMediaToStorage(bitmap: Bitmap) {
        val filename = "${System.currentTimeMillis()}.jpg"
        var fos: OutputStream? = null
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            this.contentResolver?.also { resolver ->
                val contentValues = ContentValues().apply {
                    put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
                    put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
                }
                val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
                fos = imageUri?.let { resolver.openOutputStream(it) }
            }
        } else {
            val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
            val image = File(imagesDir, filename)
            fos = FileOutputStream(image)
        }

        fos?.use {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
            Toast.makeText(this , "갤러리에 사진이 저장되었습니다." , Toast.LENGTH_SHORT).show()
            binding.fl.visibility = View.VISIBLE
        }
    }
    private fun getScreenShotFromView(v: View): Bitmap? {
        var screenshot: Bitmap? = null
        try {
            screenshot =
                Bitmap.createBitmap(v.measuredWidth, v.measuredHeight, Bitmap.Config.ARGB_8888)
            val canvas = Canvas(screenshot)
            v.draw(canvas)
        } catch (e: Exception) {
            Log.e("GFG", "Failed to capture screenshot because:" + e.message)
        }
        return screenshot
    }

전체 코드

https://github.com/Myeongcheol-shin/quote-maker

GitHub - Myeongcheol-shin/quote-maker: 사진과 명언 그리고 명언을 말한 사람을 입력하면 명언을 만들어줍

사진과 명언 그리고 명언을 말한 사람을 입력하면 명언을 만들어줍니다. Contribute to Myeongcheol-shin/quote-maker development by creating an account on GitHub.

github.com

완성 화면