고지서 200장을 옮겨 적으라고?! - MLkit을 사용한 고지서 인식 앱

K_Gs·2025년 2월 27일
5
post-thumbnail

반복작업

제가 근무하는 곳에서는 종종 한번씩 고치서 처리 작업이 들어옵니다.
막 어려운 것은 아니고 고지서가 종이로 출력되어 오기에 기록 및 결제를 위해 아래와 같은 작업을 합니다.

  1. 고지서를 펼친다.
  2. 고지서에 있는 고객번호, 금액, 계좌번호를 엑셀에 옮겨 적는다.
  3. 증빙서류 느낌으로 따로 보관한다.

모든 과정은 대략 3시간 정도 걸리는데 1번과 3번은 그냥 하면 되지만, 보통 2번 작업이 반복적이고 오래걸립니다.

3개 항목이 전부 규칙성없는 무작위 번호라 하나하나 확인하고, 옮겨 적어야하기 때문인데, 이게 1NN장을 하다보니 후반부로 갈수록 집중력이 흐트려져 한번씩 실수가 나오기도합니다.

반복작업이 매우 지치기도 하고, 이게 실수를 하면 바로 연체로 이어지기에 좀 더 개선할 방법이 없을까 고민하던 중 OCR을 이용할 생각을 하게 됩니다.

플랫폼은 제가 Android 앱을 만들 줄 알기에 Android, Compose를 이용하기로 하였습니다.

플로우, 화면 구성

먼저 플로우를 어떻게 할지 생각해보았습니다.

사실 무언가 큰 기능이 들어가는게 아니라 고지서를 이미지로 찍고 이를 OCR로 텍스트를 추출하면 되는 것이기에 간단하게 구성하였습니다.

  1. 고지서의 사진을 찍는다.
  2. OCR로 텍스트를 추출한다.
  3. 텍스트에서 필요한 정보를 뽑아낸다.
  4. 스프레드 시트에 이를 추가한다.

화면 구성 또한 플로우와 마찬가지로 카메라 프리뷰와 촬영 버튼, 그리고 성공, 실패 토스트 정도와 같이 가장 필수적인 기능만 넣기로 간단하게 구성하였습니다.

프리뷰와 촬영

가장 처음으로 Jetpack CameraX를 이용한 프리뷰, 촬영 기능을 구현하였습니다.

특별한 것은 없기에 빠르게 넘어가겠습니다.

ML kit

OCR : Optical Charactor Recognition / 광학 문자 인식

다음으로 메인 기능인 OCR을 적용하고자 하였습니다.

OCR의 경우 많은 라이브러리, 방법이 있지만 저의 경우 이전에 다른 블로그를 둘러보다가 Google ML kit이란 것을 발견하여 언젠가 사용해야지 생각했었었기에 Mlkit에서 제공해주는 텍스트 인식 기능을 사용하기로 결정하였습니다.

ML kit의 텍스트 인식 기능을 쓰는 방법은 간단합니다.

위와 같이 기본 인식의존성과 한국어 인식 의존성을 추가해주면 사용할 준비가 완료 됩니다.

val recognizer = TextRecognition.getClient(
                                KoreanTextRecognizerOptions.Builder().build()
                            )
 recognizer.process(InputImage.fromBitmap(bit, 0))
                                .addOnSuccessListener { visionText ->
                                	//...
                                }
                                .addOnFailureListener { e ->
                                	//...
                                }

한국어 인식을 위해 옵션을 추가한 상태로 Reconizer를 빌드해줍니다. 이렇게 되면 이 인식기는 한글, 숫자, 영어, 기타 특수문자 들을 인식할 수 있게 됩니다.

이후 비트맵을 InputImage객체로 만들어 집어넣어 인식을 시작합니다.

ML kit의 인식 결과로 나오는 visionText는 다음과 같은 구조를 지닙니다.

  • 심볼 : 문자 하나
  • 요소 : 한 단어
  • 라인 : 한 줄
  • 블록 : 한 단락

이런식으로 인식된 텍스트들이 계층식으로 나타납니다.

하지만, 제가 하려는 작업은 블록, 라인단위로 인식하여 무언가를 하는게 아닌 특정 값을 확인해야하는 작업이기에 그냥 일반 텍스트로 직렬화해주었습니다.

val text = visionText.text.filter {
   it != ' ' && it != '\n';
}

이후 이 직렬화한 텍스트에서 정규표현식을 통해 필요한 값을 뽑아냈습니다.

val id = Regex("(\\d{10})").find(text)?.groupValues?.get(1)
val cost = Regex("((\\d{1,2}),(\\d{3}))").find(text)?.groupValues?.get(1) 
val acount =Regex("@@은행((\\d{a})(-{0,1})(\\d{b})(-{0,1})(\\d{c}))").find(text)?.groupValues?.get(1)

보면 청구금액, 고객번호와 같은 텍스트로 뽑아내지 않고 숫자의 패턴을 파악하여 뽑아냈는데, 이는 한글, 특수문자의 인식률이 낮아 잘못 인식하는 경우가 많아서 더 인식률을 높이기 위함입니다.(계좌번호의 - 도 마찬가지)

이제 텍스트를 인식하는데 성공하였으니 이를 스프레드 시트에 올려야합니다.

스프레드 시트

스프레드 시트를 이용하는 방법은 두가지가 있습니다.

  1. 구글 클라우드 플랫폼을 통한 Sheet API
  2. 구글 앱 스크립트를 통한 접근

1번의 경우 좀 더 본격적인 실제 서비스를 위한 기능이고, 2번의 경우 개인적인 업무 자동화를 위해 자주 쓰입니다.

저는 개인적으로만 이를 사용할 것이고, 복잡하거나, 어려운 작업없이 그냥 시트에 1줄만 추가하는 기능이기에 구글 앱 스크립트를 사용하였습니다.

먼저 스크립트를 작성합니다.

시트또한 생성한 뒤 id를 복사하여 넣었습니다.

이후 이를 배포하면 이 스크립트를 실행할 수 있는 api 주소가 생깁니다.

이제 이 주소로 앞서 구해둔 데이터들을 JSON으로 만들어서 POST요청을 보냅니다.

val body = RequestBody.create("application/json; charset=utf-8".toMediaType(),json.toString())

                                    
val request = Request.Builder()
                .url("api 주소")
                .post(body)
                .build()
OkHttpClient().newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        CoroutineScope(Dispatchers.Main).launch {
        	Toast.makeText(context, "실패", Toast.LENGTH_LONG).show()
        }
   	}

    override fun onResponse(call: Call, response: Response) {
    	CoroutineScope(Dispatchers.Main).launch {
        	Toast.makeText(context,"성공/$id/$cost/$acount",Toast.LENGTH_LONG).show()
       	}
    }
}


이제 카메라를 켜 고지서를 찍어 테스트를 해보면 시트에 데이터들이 쌓이는 모습을 볼 수 있습니다.

이제 저는 고지서 옮겨 적기에서 벗어날 수 있게 되었어요!

만든 것도 만든거지만 제가 불편한 부분에서 작게라도 서비스를 만들어본게 너무 좋은 것 같습니다.

profile
아직도 모르는게 많으니, 알아가고 싶은 것도 많다

1개의 댓글

comment-user-thumbnail
2025년 3월 8일

불편한걸 이렇게 기술적으로 해결하다니 대단하고 흥미롭네요 ㅋㅋㅋ

답글 달기