본문 바로가기

리액트 네이티브

RN Perspective Crop - OpenCV편

Perspective Crop은 원본 이미지에서 선택한 영역(직사각형 아님)직사각형으로 잘라주는 자르기 방식입니다.

 

OpenCV라는 이미지 처리 라이브러리를 사용해서 이 기능을 구현할 수 있습니다. OpenCV 공식 홈페이지에서 Perspective Crop뿐만 아니라 해당 라이브러리를 사용한 다양한 이미지 처리 방식과 예제를 확인할 수 있습니다.

 

OpenCV는 다양한 언어로 바인딩되어 배포되어 있습니다.

 

자바스크립트는 NodeJS용 라이브러리와 브라우저에서 사용할 수 있는 라이브러리(opencv.js)가 배포되어 있습니다.

그리고 리액트 네이티브에는 react-native-opencv3 라이브러리가 있습니다.

react-native-opencv3

처음에는 react-native-opencv3를 선택했습니다. 자바와 스위프트로 만들어진 안드로이드 및 iOS 전용 OpenCV 라이브러리를 래핑한 것이라 속도도 빠르고 코딩도 단순해질 것이라고 생각했습니다.

 

하지만 실제 사용해 보니 다음과 같은 단점이 있었습니다.

  • 느리다.
  • 개발문서가 없다. (몇 개의 예제 정도만 있습니다.)
  • 미완성이다.

마지막 문제 때문에 react-native-opencv3 라이브러리를 채택하지 못했습니다.

 

Perspective 크롭을 위해선 getPerspectiveTransform 함수를 사용하여 소스 이미지와 결과 이미지의 4x1 매트릭스(사각형의 4개 꼭지점 좌표 정보)를 매칭시켜서, 변환에 사용될 3x3 매트릭스를 만들어야 하는데, react-native-opencv3 라이브러리의 getPerspectiveTransform 함수가 작동하지 않았습니다.

 

전에 react-native-opencv3 라이브러리로 threshold를 구현할 때는 안드로이드에서 deleteMat 함수가 오류가 났지만, 사용한 메모리를 풀어주는 함수이고, 이미지 에디터를 많이 쓰지 않는한 문제될 일은 없을 것 같아서 deleteMat 함수를 사용하지 않고 구현할 수 있었지만, gerPerspectiveTransform 함수는 반드시 필요한 함수였습니다.

 

더 이상 관리되고 있지도 않고, 마냥 기다릴 수만도 없기 때문에 이 라이브러리는 포기했습니다.

opencv.js

같은 자바스크립트 환경이라도 NodeJS나 브라우저용으로 만들어졌다면 브라우저나 리액트 네이티브에서 사용하지 못할 가능성이 큽니다. 내부적으로 NodeJS나 브라우저에서 제공하는 기능이 사용되었을 가능성이 크기 때문입니다. 

 

마찬가지로 브라우저용으로 만들어진 opencv.js 라이브러리도 리액트 네이티브에서 바로 사용하지 못합니다.

 

opencv.js 라이브러리는 OpenCV 기능을 HTML의 캔버스와 함께 사용하기 위해 만들어졌습니다.

 

하지만 안드로이드와 iOS SDK에는 WebView가 포함되어 있고, 네이티브 코드와 통신할 수 있는 기능도 포함되어 있습니다. 당연히 이것들을 래핑하고 있는 react-native-webview도 동일한 기능을 제공합니다. 그리고 이미지를 base64 형태로 변환하여 크기가 0인 WebView에 전달하고, WebView 안에서 opencv.js와 캔버스를 사용하여 이미지를 변환하여 캔버스의 이미지를 다시 base64 형태로 변환하여 네이티브로 수신할 수 있습니다.

 

구현하기 전에는 성능면에서 많이 불리할 줄 알았는데, react-native-opencv3 라이브러리와 성능면에서 차이를 느끼지 못했습니다.

 

여기에 더하여 2가지 장점이 더 있었습니다.

  • 용량 감소
    react-native-opencv3 라이브러리는 빌드하면 용량을 50Mb 정도 차지합니다. 이것 때문에 1등 오답노트 3.3.0 이전 버전은 다운로드 용량이 80Mb 정도 되었었는데요. 지금은 28Mb 정도로 줄었습니다.
  • 풍부한 개발문서, 예제, 테스트 환경
    opencv 개발 문서 페이지에서 해당 라이브러리의 원리와 사용 방법을 설명할 때 opencv.js를 사용합니다.
    그리고 각각의 예제는 실제 이미지를 올려서 바로 테스트할 수 있도록 되어있습니다.
    이번에 구현할 Perspective 크롭의 변환 부분도 개발 문서의 예제를 거의 그대로 가져와 만들었습니다.

opencv.js + 캔버스 조합

다음은 WebView에서 사용될 html 코드입니다.

html 파일 한 개만 사용하고 싶어서 opencv.js 파일 내용을 script 태그에 포함시켰습니다.

 

 

body에는 캔버스 엘리먼트 한 개밖에 없습니다.

 

opencv.js와 body가 로드되면 onload 함수를 실행시켜서 WebView외부에 opencv.js가 로드되었다는 것을 알려줍니다.

 

크롭할 이미지가 변경되면, 이미지 에디터에서 변경된 이미지를 base64 문자열로 변환후 WebView의 loadImage 함수의 인자로 전달하여 네이티브의 이미지와 WebView 내부 캔버스의 이미지를 동기화시켜줍니다.

 

사용자가 크롭할 영역을 선택한 후 크롭 버튼을 누르면, 이미지 에디터(RN)에서 WebView의 crop 함수에 사용자가 선택한 사각형 영역의 4개 좌표와 크롭할 크기를 전달하여 Perspective 크롭을 수행하고, base64 문자열로 변환된 결과를 react-native-webview의 WebView 컴포넌트에 기본적으로 포함된 ReactNativeWebView 객체의 postMessage 함수로 전달받습니다.

 

하지만 opencv.js가 거의 7Mb 정도되기 때문에, WebView로 로딩하는데 약 3초 정도가 걸립니다. 그래서 이미지 에디터 내부에 WebView를 위치시킬 경우, 사용자가 에디터를 열 때마다 WebView가 새롭게 생성되고 opencv.js를 로딩하는 시간 동안 crop 함수를 사용할 수 없게 됩니다.

 

그래서 저는 WebView를  컨텍스트의 child로 만들어 사용하고 있습니다. 단점이라면 라이브러리로 독립시키기 어렵다는 것입니다.

 

좌상단이 좌표 (0, 0)이고, 좌상단, 우상단, 좌하단, 우하단 순서로 p1, p2, p3, p4입니다.

 

컨텍스트에는 setCallback, imageLoad, imageUnload, crop이 포함됩니다.

  • setCallback
    WebView에 로드된 html에서 postMessage로 보낸 데이터를 수신하는 onMessage에서 실행되는 callback 함수를 이미지 에디터에서 정의하기 위해 사용됩니다.
    crop 함수에 의해 변환된 base64 이미지 문자열이 onMessage에서 파일로 변환된 후, 변환된 파일의 uri가 callback 함수에 입력되어 호출됩니다.

  • imageLoad
    loadImage가 맞는데 이름을 잘못 지었네요.
    Perspective Cropper에 입력된 이미지를 WebView에 로드된 html의 캔버스와 동기화시키기 위해 사용됩니다.

  • imageUnload
    역시 이름을 잘못 지었네요.
    이미지 에디터가 언마운트될 때, 더 이상의 OpenCV 관련 동작을 차단하기 위해 사용됩니다.

  • crop
    캔버스 상의 4개 좌표를 입력받아 크롭될 이미지 크기를 계산하고, WebView의 html에 포함된 crop 함수에 넣어 실행시킵니다.
    크롭될 이미지의 크기를 왜 위와 같이 계산했는지는 마지막 편에서 설명하겠습니다.