리멤버 모바일 애플리케이션의 핵심 기능은 단연 카메라일 것입니다. 명함으로 연결되는 세상을 꿈꾸는 리멤버에게 카메라는 오프라인의 명함을 온라인상의 내 인맥으로 만드는 출발점이기 때문이죠. 그렇기 때문에 최대한 명함을 깔끔하게 인식하고, 깨끗하게 처리해서 타이피스트가 정확하게 입력할 수 있도록 전달하는 것이 리멤버 앱의 핵심 임무 중 하나입니다. 몇 년 전에 저희의 이러한 고민을 담아 블로그 글을 공개했었는데요, 새롭게 개발된 리멤버 안드로이드 앱의 카메라 기능을 소개합니다.
새로운 카메라 엔진의 필요성
작년 5월, 리멤버가 대대적인 UI 개편을 하면서 카메라 UI 역시 크게 바뀌었습니다. 기존에는 아래와 같이 가로로 명함을 찍도록 유도하는 화면이었다면, 이제는 대부분의 카메라 앱과 마찬가지로 한손으로 편하게 휴대폰을 잡은 방향 그대로 촬영할 수 있도록 하는 것이죠. 하지만 지난 블로그에서 언급한 것처럼, 리멤버 안드로이드 앱에서는 개발의 용이성을 위해서 휴대폰 방향과 평행한 명함만 인식하게 되어있었습니다. 덕분에 많은 개발 리소스를 아낄 수 있었지만, 비스듬히 놓이거나, 휴대폰과 수직으로 놓인 명함은 전혀 인식하지 못했고, 이는 새로운 UI에서 매우 치명적인 허점이었습니다. 따라서 리멤버 리뉴얼을 위해서 새로운 카메라 엔진 개발은 필수적이었습니다.
또한, 리멤버 안드로이드 앱의 카메라 성능에 대한 피드백이 꾸준히 제기되고 있었습니다. 전반적으로는 괜찮은 성능을 보이지만, 미묘하게 가장자리가 제대로 인식되지 못하거나, 복잡한 배경 위에서는 명함을 제대로 인식하지 못하거나, 명함 촬영 가이드라인이 심하게 흔들리는 등 사용자의 촬영 경험에 좋지 않은 영향을 미치는 요소들이 매우 많았습니다. BoofCV 라이브러리의 한계와 레거시 코드의 영향으로 간단하고 사소한 개선이 불가능한 상태였고, 저희는 완전히 새로운 로직과 코드 위에서 카메라 엔진을 새롭게 만들기로 결심했습니다.
명함 인식 알고리즘
저희의 1차적인 목표는 명함이 어떻게 배치되어 있든 명함을 인식할 수 있도록 하는 것이었습니다. 그러기 위해서는 기존에 사용하던 BoofCV 라이브러리를 포기해야 했습니다. 수직, 수평 방향 직선을 찾는 데에만 최적화되어 있고, Hough Transform을 제외한 모든 연산에서 좋지 않은 성능을 보였기 때문에 실시간 이미지 프로세싱에서 가장 방대한 레퍼런스를 제공하고 있는 OpenCV를 사용하기로 했습니다. 리멤버 앱의 명함 인식은 크게 아래와 같은 과정을 거칩니다.
- Pre-Processing
- Edge Detection
- Find Contours
- Find Rectangle
Pre-Processing
리멤버 앱에서는 실시간으로 이미지를 처리하고, 사용자에게 명함의 위치를 보여줘야 했기 때문에 최대한 빠르게(100ms 이내) 이미지를 분석하고 그 안에서 명함을 찾아내야 했습니다. 하지만 요즘 스마트폰에서 보내주는 이미지는 해상도가 매우 높기 때문에 원본을 그대로 사용하면 이미지 처리에 매우 많은 시간이 걸립니다. 명함을 찾기 위해서 고화질의 이미지가 필요하지는 않으므로, 저희는 약 480px 수준으로 원본 이미지를 줄여서 사용했습니다.
이미지를 줄인 후, 최대한 타겟으로 하는 명함 이미지가 두드러져 보이도록 여러 가지 전처리 작업을 해주었습니다. 저희는 명함과 뒷배경의 대비가 명확하고 uniform 한 경우는 물론이고, 명함과 비슷한 색의 배경, 또는 매우 지저분한 배경에서도 명함이 높은 확률로 인식될 수 있게 하고 싶었습니다. 특히 아래와 같은 상황들은 눈으로도 구분 짓기 어려운 경계선을 찾아내거나, 명확한 직선을 찾는 것을 방해하는 수많은 장애물을 최대한 걷어내야 했습니다.
따라서 자잘한 디테일은 숨기고, 큰 변화들이 두드러지는 데에 중점을 두고 이미지를 처리했습니다. 이를 위해서 먼저 OpenCV에서 제공하는 다양한 Image Blurring(Image Smoothing) 필터를 통해서 명함과 배경을 혼동하게 만드는 지저분한 정보들을 지운 뒤, Edge Preserving Filter를 통해 명함과 배경 사이의 흐려진 경계선을 최대한 복구했습니다. Edge Preserving Filter들은 이미지를 전반적으로는 부드럽게 만들지만, Edge라고 판단되는 곳, 즉 색상의 급격한 변화가 있는 곳은 더욱 뚜렷하게 만드는 효과가 있습니다.
전처리를 거친 이미지는 아래와 같이 경계선을 판별하기 매우 좋은 상태로 다시 태어나게 됩니다.
Edge Detection
이전보다 깔끔해진 이미지를 가지고 본격적으로 명함의 경계선을 찾아야 합니다. 이를 위해서 저희는 간단한게 OpenCV의 Canny Edge Detector를 사용했습니다. Canny Edge Detector는 다섯 단계를 통해서 경계선을 추출해냅니다. 일반적인 사무실 책상에서 찍은 아래의 예시를 보겠습니다.
위와 같이 전처리와 경계선 추출을 통해서 드디어 명함의 윤곽이 제대로 드러나기 시작했습니다! 이제 매우 간단하게 직사각형을 찾아낼 수 있을 것 같지만, 저희의 목적은 일반적인 사무실 책상 위의 하얀색 명함이 아니라, 그 어떤 명함과 배경에서도 성공적으로 명함을 찾아내는 것입니다. 따라서 한 가지 작업을 더 해주어야 했습니다. 다양한 배경에 명함이 위치하다보면, 명함의 경계선 중 일부분이 배경색과 같은 경우가 종종 있습니다. 이런 경우에는 Canny Edge Detector에서 찾아낸 Edge가 이어져있지 않고 끊어져있을 때도 있습니다. 사소한 균열일 수도 있지만, 매우 복잡한 배경 속에서 완벽하게 이어져있지 않은 직사각형은 종종 걸림돌이 되곤 했습니다. 따라서 저희는 Morphology 연산을 통해서 최대한 균열을 메꾸고자 했습니다.
Morphology 연산에는 주변의 가장 밝은 픽셀로 자신을 대체하는 팽창(Dilate)과, 가장 어두운 픽셀로 자신을 대체하는 침식(Erode) 연산이 있습니다. 팽창 연산을 하면 밝은 부분이 늘어나면서 서로 조금씩 떨어져 있던 파편들이 하나로 합쳐지게 되겠죠? 그리고 그 과정에서 하얀색이 차지하는 영역도 늘어나게 될 것입니다. 이 이미지에 침식 연산을 하면 영역은 원래의 크기대로 돌아가지만, 파편들은 여전히 붙어있게 됩니다. 결과적으로 영역의 크기는 유지하면서, 최대한 한 덩어리의 영역으로 합쳐진 이미지를 얻게 되는 것이죠. 아래의 그림을 보면 조금 이해가 되시나요?
이처럼 팽창 후 침식 연산을 하는 것을 닫기(Close) 연산이라고 부릅니다. 이 닫기 연산을 통해서 Canny Edge Detection을 거친 뒤의 이미지의 균열을 보정했습니다.
Find Contours
Contour, 혹은 윤곽선은 같은 색을 가지는 모든 연속적인 점들을 둘러싼 곡선이라고 할 수 있습니다. 이전 Edge Detection에서 찾은 명함은 정확히 직사각형도 아닐 것이고, 주변의 수많은 방해물들로 둘러싸여 있을 테니, 일단 이들을 포괄하는 윤곽선들을 찾는 것입니다. 이렇게 찾은 윤곽선의 면적이 원본 이미지의 10% 이상을 차지하면, 이 윤곽선은 저희가 찾는 명함일 가능성이 있다고 판단하고 새로운 캔버스에 그려 넣습니다. 이렇게 작거나 쓸모없는 윤곽선을 쳐내고 나면, 명함 내부의 글씨나 책상 위의 무늬들은 사라지고 명함 자체의 윤곽선만 남게 됩니다.
이미지 처리는 이 단계로 끝입니다. 이제 정말로 직선과 직사각형을 찾으러 가봅시다!
Find Rectangle
가장 먼저 Hough Transform을 이용해 이미지에서 직선을 찾습니다. 하지만 아무리 깔끔하게 윤곽선을 만들어 냈더라도, 아직 이미지에 남아있는 약간의 노이즈와 이미지 축소의 영향으로 완벽한 직선이 만들어지지 않을 수도 있습니다. 그 중 가장 대표적인 케이스는 끊어진 직선입니다. 따라서 저희는 만들어진 직선을 순회하면서 다음과 같은 조건을 찾습니다.
- 평행한 두 직선
- 한 끝점이 다른 직선과 매우 가까이 붙어있는 직선
위 두 조건이 만족하면, 두 직선은 하나의 직선으로 간주하고 병합했습니다. 좀 더 배경이 복잡한 예시 이미지를 들고 와봤습니다. 아래 이미지를 전처리한 후 Hough Transform을 통해 직선을 찾아보니, 처음에는 왼쪽과 같이 14개의 직선이 발견되었습니다. 하지만 위와 같은 조건에 맞는 직선들을 찾아보니, 명함의 윗변과 아랫변을 이루는 각각 세 개의 직선들이 하나로 합쳐진 것을 볼 수 있습니다. 오른쪽 변에서도 직선 두 개가 합쳐져서, 이제 9개의 직선만 남게 되었습니다.
이렇게 찾은 직선들을 가지고 직사각형을 만들어나가는데요, 사실상 직선들의 모든 조합을 가지고 가장 직사각형스러운 조합을 찾는 과정입니다. 하지만 꼭 모든 조합을 테스트해볼 필요는 없었습니다. 일단 직선 하나를 기준으로 본다면, 자신과 평행한 직선들과 수직인 직선들을 찾을 수 있겠죠. 평행한 직선들은 직사각형에서 자신과 마주 보는 변이라고 가정하고, 수직인 직선들은 자신과 인접한 변이라고 생각한다면 이 직선들 간의 관계를 규정할 수 있을 겁니다. 아래는 명함의 윗변을 기준으로 평행인 직선과 수직인 직선을 찾은 예시입니다.
예를 들어서 위의 이미지에서 빨간색 직선이 사각형의 한 변이라고 가정하고, 나머지 직선들을 살펴봅시다. 수직인 직선들을 1, 2, 3번이라고 표시해 두었습니다. 우선 1번과 2번 직선을 뽑아서 평가해 봅시다. 두 직선은 서로 거의 평행이기 때문에 사각형의 마주 보는 두 변이라고 해도 좋을 정도입니다. 하지만 두 직선 사이의 거리를 구해보니, 너무 가까이 붙어있어서 이 두 직선이 사각형의 마주 보는 두 변이라고 판단하기는 어렵습니다. 그렇다면 2번 직선을 두고, 1번과 3번 직선을 바라보면 어떨까요? 두 직선은 기준선과도 수직이고, 서로 평행하며 적당한 거리만큼 떨어져 있습니다. 그렇다면 1번과 3번 직선은 기준선과 함께 직사각형을 이루고 있을 수 있다고 판단하는 것입니다. 이렇게 기준선과 수직인 직선의 쌍을 찾아냈으면, 이들을 왼쪽 이미지에 있는 모든 평행한 직선과 조합해보면서 직사각형이 될 수 없는 조합들을 탈락시킵니다. 이렇게 상대적인 위치를 기준으로 직사각형이 될 수 있는 4개의 직선의 조합들을 찾을 수 있습니다.
이렇게 만들어진 직선의 조합들을 모두 합당한 명함이라고 보기는 어려울 것입니다. 그래서 직사각형의 후보군을 가지고 형태가 너무 일그러져 있지는 않은지, 저희가 찾고자 하는 평균적인 명함의 비율(1 : 1.8)과 너무 크게 벗어나지는 않는지를 확인합니다. 그렇게 해서 남은 직사각형들 중 가장 큰 직사각형을 명함이라고 판단합니다. 이 모든 과정을 거치면 아래 그림과 같이 아주 깔끔하게 명함을 찾을 수 있습니다.
맺으며
리멤버의 명함 인식 기능은 많은 시행착오를 통해서 만들어졌습니다. 수없이 많은 환경에서 촬영을 하며 필터값을 조정하고, 최대한 Edge case를 걷어낼 수 있는 로직을 추가해 나갔습니다. 이 블로그 글이 마법처럼 코드 한 줄로 돌아가는 명함 인식 기능을 소개해드리지는 못하지만, 비슷한 목표를 가진 분들께 좋은 출발점이 되기를 바랍니다.
사용자들에게 최대한 빠르게 보다 좋은 퀄리티의 이미지를 제공하기 위해서는 앞으로도 갈 길이 멉니다. 좀 더 다양한 환경에서 더 정확하게 명함 이미지를 크랍해내야 하고, 잘라낸 이미지를 타이피스트가 읽기 쉽게 깨끗하게 가공하고 필터링하는 것도 고도화해 나가야 합니다. 더 나아가서는 가로 명함과 세로 명함을 구분 짓고, OCR을 통해 실시간으로 명함 정보를 읽어내는 것도 저희의 남은 과제입니다. 앞으로 계속해서 발전해나갈 리멤버의 카메라를 기대해주세요.
좋은 글 감사합니다!
좋은글감사합니다!! 리맴버 화이팅
OCR 은 Tesseract 와 같은 알고리즘을쓰시는지 아니면 다른 클라우드 API 를 쓰시는지 궁금합니다.
좋은 글 감사합니다!