OCR의 경우 관련지식이 없으면 API의 이해가 힘들 수 있으므로 소개해 보고자합니다.

버전은 Tesseract 3.05이며 모든 함수명은 tess-two에서 제공하는 TessBaseAPI를 기준으로 합니다. C코드로 되어있는 Tesseract 원본의 경우 함수명이 약간 다릅니다만, 함수명이 비슷하고 매개변수도 포인터로 되어있다 뿐이지 원하는 정보는 크게 다를 게 없기 때문에 원본 Tesseract를 사용하시는 분도 이해하시는 데 큰 문제는 없을겁니다.



1. 가장 기본적인 API

- init(String datapath, String language) : Tesseract를 사용하도록 초기화합니다. datapath는 traineddata가 있는 주소, language는 사용할 언어를 입력하시면 됩니다.( ex. eng / kor / eng+kor )

- setImage(Bitmap bitmap) : 읽어들일 이미지를 지정합니다. 입력값으로 활용할 수 있는 타입은 비트맵, 파일주소, Leptonica에서 사용하는 pix, byte[]로 된 이미지 데이터가 있습니다.)

- getUTF8Text() : 인식한 결과값을 String형으로 출력합니다.

- end() : 메모리를 해제합니다.


2. 추가 기능 API

- setRectangle(Rect rect) : 전체 이미지에서 원하는 영역만 지정해서 OCR 인식할 때 사용합니다. x, y, width, height값으로 영역을 지정할 수 있고 동일한 결과를 낼 수 있는 방법으로는 전체 이미지에서 createBitmap으로 원하는 영역의 이미지만 crop해서 setImage 후 getUTF8Text를 실행하는 방법이 있습니다.

- setVariable(String var, String value) : 일반적으로 Whitelist나 Blacklist를 사용할 때 사용합니다. WhiteList는 입력한 값만 출력되도록 하는 것이고 BlackList는 입력한 값을 출력에서 배제하는 것입니다. var에는 API에서 제공하는 속성값을 넣고 value에 값을 넣으시면 됩니다.

*setVariable("VAR_CHAR_WHITELIST", "1234567890") -> 숫자만 출력하고 싶을 때 사용합니다.

*setVariable("VAR_CHAR_BLACKLIST", "1234567890") -> 출력값에서 숫자를 배제하고 싶을 때 사용합니다.

- setPageSegMode(int mode) : 텍스트 이미지를 인식할 때 분할 방법을 결정합니다. 이부분은 OCR의 구동 원리와 가까운 내용이라 자세한 건 나중에 원리에 대해 설명하게 되면 다시 언급하겠습니다. 전체를 인식할 땐 기본 모드를 쓰는 게 낫기 때문에 그대로 두시고, 한줄만 읽고 싶으시다면 PSM_SINGLE_LINE을 사용하시는 것을 추천드립니다.



실제로 쓰이는 건 이정도입니다. 그나마도 setVariable은 전처리가 완벽히 확보된 이미지가 아니라면 안쓰시는 것이 인식결과가 좋기 때문에 실제로 자주 사용되는 것은 필수적인 API를 제외하고는 setRectangle과 setPageSegMode 정도입니다. 다음 Tesseract 관련 포스팅은 Tesseract로 가장 인식률을 높힐 수 있는 방법과 인식률이 떨어지는 사례가 될 것 같습니다.



참고

Android Tesseract Github : https://github.com/rmtheis/tess-two

private class HastTest {
   int value1, value2, value3;
   String key;

   HastTest(String key, int a, int b, int c) {
      this.key = key;
      this.value1 = a;
      this.value2 = b;
      this.value3 = c;
   }
}

public static void main(String args[]) {
   Map mHastTest = new HastMap() {
   HastTest hastTest = new HastTest("testKey", 10, 20, 30);
   
   mHastTest.put("testKey", hastTest);

   for(String key : mHastTest.keySet()) {
      System.out.println("Result : " + key + ", " + mHastTest.get(key).value1 + ", " + mHastTest.get(key).value2 + ", " + mHastTest.get(key).value3);
   }
}



이렇게 Class로 만들어서 value값에 넣어주시면 하나의 key값에 여러 value값을 저장하실 수 있습니다.

코드 자체가 단순하니 어렵지 않게 응용하실 수 있을겁니다.


요즘은 안드로이드에서는 SparseArray를 쓰지만 자바에서는 계속 HashMap을 쓰는 것 같아서 작성해봤습니다.

블로깅을 시작하면서 느낀건 데 공부할 게 너무 많네요..ㅠㅠ

SQLite를 사용하기 때문에 별 생각없이 CursorAdapter로 Listview를 구성하면 DB를 수정, 삭제하거나 Listview의 정렬방식 변경, 검색등과 같이 Listview상의 순서를 기존과 다르게 변경하는 경우 Item position 값이 꼬이는 문제가 생깁니다. 그래서 일반적으로 ArrayAdapter를 사용하는 경우처럼 Listview상의 Item을 터치하여 해당 Item값을 불러오는 Activity를 띄우려고 할 때 setOnItemClickListener에서 제공하는 position값을 그대로 사용하면 엉뚱한 결과값이 출력되거나 아에 죽어버리는 상황을 맞이하게 됩니다.




간단히 DB 삭제의 경우를 예로 들어보겠습니다.

위와 같이, setOnItemClickListener에서 제공하는 position값으로 DB를 삭제해버리는 경우, 겉으로 보기에는 제대로 지워진 것처럼 보이지만 실제로는 ListView의 position값과 DB값은 꼬여버립니다. 만약 position=1에 해당하는 item을 삭제한 후에 position=2에 해당하는 아이템을 터치하면 D에 해당하는 DB값이 출력되는 게 아니라 C에 해당하는 DB값이 출력될겁니다. 물론 position=1에 해당하는 아이템을 다시 터치하게 된다면 index=1이었던 DB값은 삭제된 후이기 때문에 예외처리를 안했다면 죽는 문제가 생기겠죠.


따라서, Listview의 position값을 그대로 사용하는 게 아니라 position에 해당하는 DB의 _id값을 불러와서 제어하는 것이 좋습니다.


listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
   @Override
   public void onItemClick(AdapterView adapterView, View view, int position, long l) {
      Intent intent = new Intent(MainActivity.this, InfoActivity.class);

      Cursor cursor = (Cursor) cursorAdapter.getItem(position);
      String index = cursor.getString(cursor.getColumnIndex("_id));
      int id = Integer.parseInt(index);

      intent.putExtra("id", id);
      startActivityForResult(intent, 0);
   }
});


먼저 getItem으로 현재 position에 있는 item값을 불러오고, 그 중 getColumnIndex로 _id값을 불러와서 그 id값을 기준으로 불러오시면 정상적으로 원하시는 DB값이 호출되는 것을 보실 수 있습니다.




참고


http://arabiannight.tistory.com/entry/368

안드로이드 개발하면서 절대 빠질 수 없는 것 중 하나가 AsyncTask일겁니다.

그만큼 AsyncTask의 개념이나 사용법에 대한 설명이 매우 많은 데, 중요하지만 자주 언급되지 않는 기능 하나를 소개하겠습니다.




AsyncTask를 사용하게 되면 일반적으로 현재 이 프로그램이 실행되는 지 아닌 지 확인하기 위해 Dialog를 띄웁니다. 문제는 작업량이 많아서 로딩이 길어지는 경우인 데, 로딩 중에 사용자가 무심코 주위를 터치하게 되면 Dialog창이 꺼져버려서 현재 이게 먹통이 된건지, 어느 정도 처리가 된 상태인 지 알 방도가 없습니다. 버튼 이벤트에 대한 처리를 딱히 안했다면 그 상태에서 중복터치를 해서 프로그램이 완전히 꼬여버리는 문제까지 생기기도 합니다. 이를 방지하기 위해 있는 기능이 setCanceledOnTouchOutSide(boolean)입니다.


@Override
protected void onProgressUpdate(Void... values) {
   dialog.setCanceledOnTouchOutside(false);
}

@Override
protected void onPostExecute() {
   dialog.setCanceledOnTouchOutside(true);
   dialog.dismiss();
}


어렵지 않죠? 하지만 로딩 시간이 1초만 돼도 꼭 필요한 기능입니다.

물론 그 이하도 웬만하면 넣어주시는 게 안정성을 향상시키는 데 있어서 좋다고 생각합니다.




출처 : https://developer.android.com/reference/android/app/Dialog.html#setCanceledOnTouchOutside(boolean)

이번 글은 webnautes님께서 작성해주신 방법으로 진행하다가 제게 생긴 에러의 해결법을 공유하고자 합니다.


능력자이신 분을 찬양하며, 제가 참고한 글의 링크입니다. 

http://webnautes.tistory.com/923



불가피하게 구 버전에서 작업하게 되어 안드로이드 스튜디오 2.1.2로 작업하게 되었습니다.

제게 발생한 에러는 모두 버전이 낮아서 생긴 문제였기 때문에, 저처럼 구버전으로 작업하시는 분들께 도움이 될 것 같습니다.


개발 환경

- Windows 10

- Android Studio 2.1.2

- OpenCV 3.2.0

- Android API Level 26



에러 문구는 간단히 요약하여 적겠습니다.


1. NDK integration is deprecated in the current plugin.

  -> gradle.properties에서 android.useDeprecatedNdk=true 를 작성해주시면 됩니다.


2. Gradle dsl method not found externalnativebuild.

  -> build.gradle에서 classpath 'com.android.tools.build:gradle:2.2.0' 으로 변경 (2.2.0이상의 버전에서만 지원한다고 합니다.)


3. Error: minimum supported gradle version is 2.14.1.

  -> gradle-wrapper.properties에서 distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 으로 변경


2, 3번의 경우 저는 최소버전에 맞춰 변환했지만 아래 내용을 참조하여 유동적으로 최신버전을 맞추어도 됩니다. 2번 에러 해결 후 3번 에러가 나는 이유는 Plugin version과 Required Gradle version이 맞지 않기 때문에 생기는 문제입니다.



여기까지가 구버전에서 webnautes님의 글대로 진행했을 때의 에러 해결법입니다. 이 후 정상적으로 동작하는 것을 확인하였으며, 이 외에 생기는 에러들은 jni상의 헤더나 cpp파일에서 JNICALL이 잘못 적혔을 확률이 높습니다. 매개변수에 배열을 넣어주실 분들은 jnit value[]가 아니라 jintArray로 표현해야하니 혹시 처음 하시는 분들은 참고하시기 바랍니다. 전 이것 때문에 많이 해맸거든요..




출처


개발 환경 설치 법 - http://webnautes.tistory.com/923

2번 에러 - https://stackoverflow.com/questions/39506379/could-not-find-method-externalnativebuild-for-arguments

3번 에러 - https://developer.android.com/studio/releases/gradle-plugin.html

NDK(Native Development Kit) : Android단에서 C/C++ 언어를 사용하게 하기 위한 개발 도구

JNI (Java Native Interface) : Java에서 C/C++로 짠 로직을 사용하게 해주는 인터페이스


정도로 전 이해하고 있습니다.

그러니 'JNI를 이용해서 NDK를 호출하다.' 보다는 'NDK를 이용해서 JNI를 호출하다.'가 맞겠죠?


개발하는 데에 크게 신경쓰지 않아도 될 차이같아 보이지만 협업자간의 원할한 커뮤니케이션을 위해서는

정확한 단어 사용은 필수라고 생각합니다.



참고 URL

- https://developer.android.com/ndk/index.html?hl=ko (NDK)

- http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/intro.html#wp725 (JNI)

모바일계에선 유독 1인 개발자분들이 많습니다.

하지만 디자인감각이나 제작까지 할 수 있는 분들은 많지 않기 때문에 

프리로 디자이너를 고용하거나 지인을 통해 부탁하는 수밖에 없었습니다. 과거에는요.


하지만 지금은 갓구글님께서 1인 개발자분들께 무료로 이미지를 배포하고 있습니다.

아실 분들은 다 아실만한 Material Icon입니다.


https://material.io/icons/




위에 보시다시피 재판매만 하지 않는다면 가져다써도 문제없다고 명시해놨습니다.





이미지 목록은 이런식으로 되어있고 다운받으면 안드로이드 버전 뿐만 아니라 iOS, WEB 개발에 최적화된 이미지도 같이 제공합니다. 최적화가 잘되어있기 때문에 이미지가 깨져서 나인패치를 쓸 필요도 없고 검정색과 흰색을 제공해서 색이 반전되어도 문제없이 사용할 수 있습니다.



이제 이미지 걱정없이 즐코딩하세요 ^^


제가 블로그를 만든 취지는 어디까지나 주니어 개발자분들이나 학생분들을 위함이니 편리하게 코딩할 수 있는 팁을 하나 적어볼까 합니다.

제가 회사 다니는 3년 동안 이렇게 해보라고 말씀해주신 분이 없어서요..ㅠㅠ 물론 회사에서야 대형 프로젝트이고 협업하니까 그닥 필요는 필요는 없는 팁이지만, 모바일 분야의 경우 개인적인 재미로 개발하는 사람도 있고, 소규모 프로젝트도 많기 때문에 필요하다고는 생각합니다.


본론으로 넘어가서, 저는 자주 사용하는 공통적인 유틸 클래스를 만들어서 프로젝트를 새로 만들 때마다 재사용합니다.

LogUtil, FileUtil, MemoryUtil 등을 만들어서요. 매번 Log를 태그와 같이 찍고 구글링 뒤져가며 Memory size 관리하는 소스 찾아서 적용하고.. 어차피 매번 사용하는 데 계속 이렇게 사용하니 불편하더군요. 


만드는 방법은 아래와 같습니다.


public class MemoryUtil {

   // VM Heap Max Size
   public static String getMaxMemorySize() {
      long maxMemory = (Runtime.getRuntime().maxMemory()) / (1024 * 1024);
      return maxMemory + "MB";
   }

   // VM Heap Total Size
   public static String getTotalMemorySize() {
      long totalMemory = (Runtime.getRuntime().totalMemory()) / (1024 * 1024);
      return totalMemory + "MB";
   }

   // VM Heap Allocation Size
   public static String getAllocMemorySize() {
      long allocMemory = (Runtime.getRuntime().totalMemory()) - (Runtime.getRuntime().freeMemory())) / (1024 * 1024);
      return allocMemory + "MB";
   }
}


당연히 사용법은 MemoryUtil.functionName() 으로 사용하시면 됩니다.


이런식으로 공통으로 사용할 유틸을 미리 작성해두면 어느 프로젝트에서든 시간낭비할 필요없이 개발을 보조해줄 함수를 가져다 쓸 수 있습니다. 자바 파일로 가지고 다니기 부담스러우시다면 SDK로 말아서 가지고 다니시는 것도 좋고요.

이번엔 메모리 관리 시 주의해야할 부분에 대해 알아보겠습니다.


안드로이드에서 메모리는 VM Heap과 Native Heap을 사용하는 데, OutofMemoryError의 해결책을 찾기 위해 구글링을 열심히 하다보면 자주 볼 수 있는 내용 중 하나가 "Honeycomb 버전부터는 안드로이드에서 관리되는 비트맵은 모두 VM Heap에서 관리한다." 입니다. (제가 제대로 이해한건지는 모르겠지만;)


실제로 이전 글에 썼던 것과 같이 Recycle을 적절히 활용해주면 중간중간 메모리를 찍어봤을 때 VM Heap에 메모리가 쌓이고 줄어드는 것을 알 수 있습니다. 굳이 찍기 귀찮다 싶으면 안드로이드 스튜디오 상에서 Android Monitor -> Monitors 탭으로 들어가서 정확하진 않아도 가시적으로나마 확인할 수도 있죠.


그런데 로그상에서 메모리 관리가 제대로 되는 걸 확인한 상태임에도 불구하고 수십번을 반복하다보면 어느 순간 에러도 없이 죽어버리는 경우가 있습니다. 저같이 로그와 디버깅만 믿고 살아가는 초보 개발자에겐 정말 멘탈 터지는 상황이 오는거죠. 다른 케이스가 또 있을지는 모르겠으나, 제가 발견한 원인은 Native heap의 메모리 축적이었습니다. 이전 글에서 밝혔다시피, 메모리 문제가 심하게 터졌던 이 프로젝트에는 영상처리가 들어가있었기 때문에 NDK가 적용되어 있는 상태였는 데, Release 함수를 적용한 상태라 신경을 안쓰고 있었는 데, 영상처리 파트에서 메모리 관리를 잘못해서 Native heap이 쌓이고 있었던 겁니다. 그러니 평소에도 내 영역이 아니더라도 내게 영향을 미칠 수 있다면 확인을 하는 습관을 들이는 게 좋을 것 같습니다.



소스도 없고, 그림도 없고, 주저리 주저리 쓸데없는 일기들만 가득찬 글만 있어서 읽기 싫은 분들을 위해 요약해드리자면,


1. 비트맵을 NDK로 넘길 때는 VM Heap이 아니라 Native Heap에서 관리한다. <- 전 이거 몰랐습니다ㅠㅠ

2. 그러므로 NDK를 사용하고 있는 상태라면, Release 함수를 적용하더라도 중간 중간에 Native Heap Size의 로그를 찍어본다.

3. 만약 문제가 있다싶으면 담당자 찾아가서 담당자의 직책에 따라 태세변환하며 메모리 관리 부분을 확인해줄 것을 요청한다.



- 참조 사이트 http://d2.naver.com/helloworld/539525 

                  -> 메모리 관리에 대해 하나부터 열까지 정리되어 있습니다. 정독하면 메모리 관리에 도움이 되실 겁니다.

그동안 하이브리드 앱을 만들다가 작년에 영상처리를 해주는 앱을 만들게 되었는 데, 이때 영상처리 파트 쪽에서 고화질의 이미지를 넘겨주길 원했습니다. 문제는 앱의 프로세스 상 한번에 수십장을 처리해야하는 부분까지 존재했다는 것이었죠. 상황이 이러하다보니 이론이 소홀히 했던 저는 OutofMemoryError를 자주 접했습니다. 따라서 저와같은 상황에 처한 주니어 개발자분들을 위해 제가 극복한 방법을 리마인드겸 모두 적어볼까합니다.



1. Bitmap.recycle의 호출 시점


제가 넘겨주는 이미지의 해상도는 디바이스의 성능 및 지원 해상도에 따라 최소 1920x1080에서 최대 4032x2268 정도였습니다. 당연히 최대 해상도를 가진 이미지의 용량이 어마어마했고 Asynctask로 다섯장만 돌려도 OutofMemoryError가 뜨며 죽는 현상이 일어났죠. 이를 해결하기 위해 열심히 구글링해본결과 onDestroy나 onPostExecute 등 더이상 비트맵을 사용하지 않는 구간에서 bitmap.recycle을 하라는 것이었습니다. 


여러장을 돌리는 저에겐 당연스럽게도 onPostExecute에 들어가기도 전에 죽어버렸고, Bitmap.recycle을 여기저기 들쑤시며 넣어보다가 recycle 후에 bitmap을 사용하려해서 생기는 에러에 맞닥들이게 되었습니다.



 

java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap




@Override
protected void doInBackground(Void... params) {
   Bitmap bitmap = null;
   for (int i = 0; i < imageItems.size(); i++) {
       bitmap = BitmapFactory.decodeFile(imageItems.get(i).getFilePath());
       Bitmap result = jniLib.jniFunction(bitmap);

       bitmapSaveFile(filePath, bitmap, 100);
       
       if (!bitmap.isRecycled()) {
          bitmap.recycle();
       }
   }
}


얼마나 멍청하게 짰는 지 아시는 분은 다 아시겠지만... 학창시절에 이론공부를 소홀히 하고 비전공으로 대학원을 진학하면서 2년을 쉰 저로선 파악이 제대로 안됐었습니다. (이론 공부 열심히 합시다;;)


무엇이 문제인지 제대로 파악이 안되는 저와 같은 처지의(?) 주니어 개발자분들을 위해 설명해드리자면, 어느 변수든 마찬가지지만 초기화를 해주지 않으면 새로운 변수값이 계속 들어간다고 해도 주소값은 계속 남아있습니다. 


따라서 Bitmap 선언을 for문 밖에서 해버리면 처음 프로세스가 진행될 때 recycle을 해버린 상태에서 다시 bitmap을 사용하려고 해서 위와 같은 에러와 함께 앱이 죽어버리죠. 일반적인 변수들은 모두 용량이 적기 때문에 잘 모르고 지나칠 수도 있지만 bitmap은 워낙 용량이 크기 때문에 제대로 초기화를 안해주면 Recycle의 여부를 떠나서 메모리가 쌓이므로 결국 죽어버립니다. 


이를 고려하여 수정한 코드는 아래와 같습니다.


@Override
protected void doInBackground(Void... params) {
   for (int i = 0; i < imageItems.size(); i++) {
       Bitmap bitmap;
       bitmap = BitmapFactory.decodeFile(imageItems.get(i).getFilePath());
       Bitmap result = jniLib.jniFunction(bitmap);

       bitmapSaveFile(filePath, bitmap, 100);
       
       if (!bitmap.isRecycled()) {
          bitmap.recycle();
       }
   }
}


.. 참 쉽죠? Bitmap 선언만 for문 안으로 내렸습니다. 이렇게 하면 한 번 돌릴 때마다 recycle을 실행하더라도 bitmap을 초기화 하기 때문에 다시 recycle을 실행해도 기존 bitmap을 재사용하지 않습니다. 따라서 메모리가 쌓이는 일은 더이상 발생하지 않기 때문에 이제는 bitmap 크기 따위 신경 안쓰고 마음껏 돌리셔도 됩니다!


하나 추가하자면, 모든 bitmap 관련 프로세스들은 외장메모리에 파일로 저장 후 주소값만 넘겨서 다시 불러오는 방식으로 작업해야합니다. bitmap을 그대로 넘겨버리면 아주 쉽게 OutofMemoryError를 접하실 수 있을겁니다. 에러가 안뜨더라도 디바이스가 무거워지니 이왕이면 파일주소로 주고받아서 관리해주시는 게 좋습니다.


다음엔 Heap으로 인해 발생했던 메모리 이슈와 그 원인에 대해 알아보겠습니다.



- 저도 주니어 개발자이고 경험을 통해 작성하는 것이기 때문에 틀린 부분이 있을 수 있습니다. 만약 다른 의견이 있으시거나 틀린 부분이 있으면 댓글로 남겨주시면 감사하겠습니다.


- 오늘도 주니어 개발자분들 가시는 길에 에러가 없길 빕니다.


+ Recent posts