Translate

2017년 11월 28일 화요일

2017년 9월 16일 토요일

[R] Data Frame, List에서 name을 변수로 받는 방법


Laptop
운영체제Windows 10 Home 64bit
개발프로그램RStudio Version 1.0.153

자료형 중 name 속성을 가지는 것들에는 모두 해당될 것 같다. 
(Data Frame, List 밖에 몰라서 제목은 그 2개만 썼다.)
아래 소스는 data.frame을 list로 바꿔도 동일한 결과가 나온다.

df <- data.frame(X=1, Y=2, Z=3)

이런식으로 X, Y, Z 라는 name들을 가진 Data Frame이 있다고 하자.

일반적으로 각 값에 접근할 때, $ 기호를 이용하여



이런식으로 실행된다.
단 이게 적용이 되지 않는 경우가 있는데, 바로 아래와 같이 name을 변수로 받을 때이다.

for (i in names(df)){
  show(i)
  show(df$i)
}








결과를 보면 names(df)에 해당하는 i를 출력했을 때 X, Y, Z는 제대로 출력되지만,
df$i 의 경우 접근하지 못하고 NULL이 출력된다.


이 경우, [[]] 를 대신 사용한다.

소스를 고쳐 다음과 같이 쓰면, 정상적인 결과가 출력된다.

for (i in names(df)){
  show(i)
  show(df[[i]])
}









꼭 반복문에서만 이런게 아니라, $ 기호는 변수 자체를 받지 못하는 것 같다.
이걸 보면 알 수 있다.







기초적인 문법인 것 같으나, 독학을 하다보니 약간 헤맸다.

2017년 7월 25일 화요일

[Python] mongod 실행 중 ERROR: Insufficient free space for journal files 오류 해결법


Laptop
운영체제Ubuntu 16.04 LTS 64bit

mongod 명령어를 입력했을 때 ERROR: Insufficient free space for journal files 가 포함된 로그가 출력되면서 제대로 되지 않을 때가 있는데, --smallfiles 옵션을 추가하면 해결된다.
(에러 바로 아래에 [initandlisten] Please make at least 3379MB available in /data/db/journal or use --smallfiles 라는 로그가 함께 나오긴 한다.)

$ sudo mongod --smallfiles

혹은 mongod.conf 파일을 생성하여 smallfiles=true를 입력하면 된다.
/etc/ 에 생성했을 경우, 실행은 다음과 같이 한다.

$ sudo mongod -f /etc/mongod.conf



그 외에, listen이나 bind 등 연결 관련 오류가 난다면 이전 연결이 종료가 제대로 되지 않은 상태이므로
$ sudo killall -15 mongod 를 해보고 실행해보면 된다.

참고 사이트:
https://stackoverflow.com/questions/14584393/why-getting-error-mongod-dead-but-subsys-locked-and-insufficient-free-space-for

https://stackoverflow.com/questions/6478113/unable-to-start-mongodb-local-server

[R] installation of package ‘curl’ had non-zero exit status 오류 해결법


Laptop
운영체제Ubuntu 16.04 LTS 64bit
개발프로그램RStudio Version 1.0.143
(R version 3.4.1)

KoNLP 패키지 설치 중 curl 관련 오류가 뜨길래 install.packages('curl') 을 했더니 다시 오류가 나왔다.
installation of package ‘curl’ had non-zero exit status

libcurl4-openssl-dev를 설치하면 해결된다. 
$ sudo apt-get install libcurl4-openssl-dev


추가로 아래와 같은 패키지들을 요구할 수도 있다. 마찬가지로 sudo apt-get install로 설치하면 된다.

libssl-dev    (package on e.g. Debian and Ubuntu)
openssl-devel (package on e.g. Fedora, CentOS and RHEL)
openssl

참고 사이트: https://stackoverflow.com/questions/31686470/install-curl-and-readr-on-r

[R] conftest.c:1:17: fatal error: jni.h: No such file or directory 오류 해결법


Laptop
운영체제Ubuntu 16.04 LTS 64bit
개발프로그램RStudio Version 1.0.143
(R version 3.4.1)

(Java 설치, 환경변수까지 모두 되어 있다는 가정)

R에 Java를 연결하는 과정에서

$ sudo R CMD javareconf
명령어를 입력하면

conftest.c:1:17: fatal error: jni.h: No such file or directory
이 문장이 포함된 에러 메시지가 출력되면서 실패할 때가 있다.

이것은 Java 요소가 전부 설치되지 않았다는 뜻이며,
재설치 후 다시 하면 해결된다. 8은 버전이므로 바꿔도 상관없다.
$ sudo apt install openjdk-8-*

이 때, JAVA_HOME은 /usr/lib/jvm/java-8-openjdk-amd64 로 하면 된다. (마지막 파일명은 Ubuntu 버전에 따라 좀 다를 수 있다.)

[R] ERROR: dependencies ‘jsonlite’, ‘plyr’ are not available for package 오류 해결법


Laptop
운영체제Ubuntu 16.04 LTS 64bit
개발프로그램RStudio Version 1.0.143
(R version 3.4.1)

ERROR: dependencies ‘jsonlite’, ‘plyr’ are not available for package


install.packages 를 사용하다보면 종종 나타나는 오류.
json을 사용하는 패키지에서 나타나는 것 같다.
r-cran-jsonlite를 설치하면 해결된다.

통합 패키지인 것 같은데, 다른 dependencies 오류의 경우에도 해결방안이 될 수 있을 것 같다.

$ sudo apt-get install r-cran-jsonlite

참고 사이트: https://stackoverflow.com/questions/25721884/how-should-i-deal-with-package-xxx-is-not-available-for-r-version-x-y-z-wa

R CRAN에서 삭제된, 혹은 구버전 패키지 다운로드하기


Laptop
운영체제Ubuntu 16.04 LTS 64bit
개발프로그램RStudio Version 1.0.143
(R version 3.4.1)

CRAN이란, R 언어에서 사용되는 패키지 등의 자료의 모음이라고 보면 된다.

이 중 같은 기능을 수행하는 다른 패키지로 대체되고 사라지는 패키지들이 존재한다.
https://cran.r-project.org/web/packages/[패키지명]/index.html
[패키지명]부분을 바꿔 주소창에 입력해 들어가면 확인해 볼 수 있다.
Package ‘[패키지명]’ was removed from the CRAN repository. 라는 메시지가 뜰 것이다.

이 경우 install.packages('[패키지명]') 을 입력해도
package ‘[패키지명]’ is not available (for R version [R 버전])
라는 메시지만 돌아올 뿐이다.

하지만 경우에 따라 그 패키지를 사용하고 싶을 수 있다.
혹은 install.packages를 사용하는 경우 최신 버전만 설치되므로, 그 이전 버전을 설치하고 싶을 수도 있다.

당연히 방법이 없는 건 아니고, 현 시점에서는 삭제되었더라도 CRAN에서 기록을 모두 가지고 있다.

여기서 Ctrl+F 로 패키지명을 찾고, 들어가서 버전명을 확인하면 된다.
혹은 상단 링크에 들어간 상태로 두번째 줄에 archive를 클릭해도 같은 경로로 연결된다.
https://cran.r-project.org/src/contrib/Archive/


설치 방법은 두가지가 있는데

첫번째는 다운로드 링크를 그대로 입력하는 방법 (인터넷 연결 필수):
install.packages("https://cran.r-project.org/src/contrib/Archive/[패키지명]/[원하는 버전 파일명].tar.gz", repos = NULL, type="source")

두번째는 wget 등을 이용하여 tar.gz 파일을 다운로드 후, 로컬 안에 저장하여 그 경로를 입력하는 방법이다.
/home 에 저장했다면
install.packages("/home/[파일명].tar.gz", repos = NULL, type="source")

물론 다운받을 때는 인터넷 연결이 필요하지만, 그 후에 혹시 R 개발환경을 다시 설정할 때
tar.gz 파일을 갖고 있다면 그 때는 연결이 필요하지 않다.

+ RStudio 에서는 메뉴에서 탐색기를 이용하여 더 간편하게 선택이 가능하다.




이 후 탐색기 창이 뜨는데, 거기서 tar.gz 파일을 열고 Install 버튼을 눌러 마치면 된다.

Install from을 기본 선택 상태인 Repository(CRAN)으로 하면 CRAN에 존재하는 패키지의 경우에도 더 쉽게 설치할 수 있다.
(여러 개의 패키지를 동시에 다운받을 경우 이름을 , 로 구분하라고 적혀있다.)




2017년 7월 24일 월요일

Ubuntu E: Could not get lock /var/lib/dpkg/lock - open (11 Resource temporarily unavailable) E: Unable to lock the administration directory (/var/lib/dpkg/) is another process using it? 오류 해결법



Laptop
운영체제Ubuntu 16.04 LTS 64bit

E: Could not get lock /var/lib/dpkg/lock - open (11 Resource temporarily unavailable) 
E: Unable to lock the administration directory (/var/lib/dpkg/) is another process using it?

apt install 작업을 하던 중 종종 발생하는 오류.

이전 apt 프로세스가 정상적으로 끝나지 않았을 때 잠금상태가 풀리지 않은 상태라는 듯 하다.

아래 명령어를 입력 후 다시 해보면 된다.


$ sudo rm /var/lib/dpkg/lock
$ sudo dpkg --configure -a

아마 잠금 파일을 삭제함으로써 해제하고, 패키지 관리에 쓰이는 dpkg 명령어로 현재 설정을 전체 패키지에 적용하는 것 같다.




2017년 6월 26일 월요일

[Android Studio] Spinner setRotation 시 dropdown 창 함께 회전하기


Laptop
운영체제Windows 10 Home 64bit
개발프로그램Android Studio 2.3.3
(buildToolsVersion "25.0.2")
프로젝트명Spinner-rotation-test

Mobile
모델LG X5
운영체제Android 6.0.1 Marshmallow


Android Studio에서 Spinner 뷰에 setRotation(90); 메소드를 적용하면, 아래와 같은 문제점이 발생한다.


 
바로 dropdown 창이 함께 회전하지 않는다는 것이다.
아쉽게도 뷰 메소드 자체에서는 해결방법이 없는 듯하다.
좀 번거롭긴 하지만, 커스텀 뷰를 제작할 때 쓰는 ArrayAdapter를 사용하면 어느정도 해결할 수 있다. 정확히는 오버라이딩이 필요하다. 

먼저 res/layout에 spinner_adapter.xml 라는 레이아웃을 하나 만들어주자.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="20dp"
    >
    <!-- padding을 적용하지 않을 시 화면에 표시되지 않는다. -->

</TextView>


그리고 MyArrayAdapter 라는 ArrayAdapter를 상속받는 클래스를 하나 생성하고, 생성자와 getDropDownView 메소드를 오버라이딩한다.

그리고 아래와 같이 소스를 작성한다.
(setRotation 메소드는 각도 값을 입력받기 위해 임의로 만들었다.)

MyArrayAdapter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.spinner_rotation_test;

import android.content.Context;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;

import java.util.List;

public class MyArrayAdapter extends ArrayAdapter<String> {

    private int m_degree;

    public MyArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List<String> objects) {
        super(context, resource, objects);
    }


    @Override
    public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        parent.setRotation(m_degree);

        // spinnerMode 가 dialog 일 때는 위치 조정을 해야 화면에 나타난다.
        // parent.setY(120);

        return super.getDropDownView(position, convertView, parent);
    }

    public void setRotation(int degree)
    {
        m_degree = degree;
    }

}


이것들을 이용해서, MainActivity.java를 작성한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.spinner_rotation_test;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Spinner;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private Spinner mSpinner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSpinner = (Spinner) findViewById(R.id.spinner);
        mSpinner.setRotation(90);

        List<String> item_list = Arrays.asList(new String[]{"item 1", "item 2", "item 3"});
        MyArrayAdapter myArrayAdapter = new MyArrayAdapter(this, R.layout.spinner_adapter, item_list);
        myArrayAdapter.setRotation(90);

        mSpinner.setAdapter(myArrayAdapter);


    }
}

activity_main.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">


    <Spinner
        android:id="@+id/spinner"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        />

</RelativeLayout>


결과 화면은 다음과 같다.



+ spinnerMode가 dialog 일 때

spinner에 android:spinnerMode="dialog" 를 추가하고
MyArrayAdapter.java의 Line 27의 주석을 해제하면 다음과 같이 dialog 모드도 적용된다는 걸 확인할 수 있다.




2017년 6월 15일 목요일

Python 프로그램 exe 파일로 만들기 (+ openpyxl, requests 라이브러리 포함)


Laptop
운영체제Windows 10 Home 64bit
개발프로그램PyCharm Community Edition 2017.1.3
Python 3.4.4

개인적인 용도로 엑셀파일을 생성하는 프로그램을 만들었는데, 파이썬 개발환경이 없는 컴퓨터에서 실행시킬 필요가 있었다.

그러기위해선 exe파일로 만들 필요가 있다.

알아본 결과 대표적으로 3가지 라이브러리가 있는데 각각 테스트 결과는 이렇다.

1. cx_freeze
공식에서 배포를 잘못한건지 내가 못하는건지 모르겠지만 실행파일이 없음. 테스트 실패.

2. py2exe
exe 생성은 잘 되나 openpyxl을 포함하지 못함.

3. pyinstaller
성공.


그래서 pyinstaller를 사용한 예제를 써보겠다.
설치는 pip을 이용하면 된다. (pip install pyinstaller)

단 파이썬 버전 2.7과 3.3~3.5 에서 잘 동작한다고 한다. 최신 버전이 3.6 정도 된 걸로 알고있는데 참고하자. 

적당히 openpyxl 공식사이트에 있는 샘플 소스를 exe로 만들어 보겠다.

excel_test.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
print 'hello world!'from openpyxl import Workbook
wb = Workbook()

# grab the active worksheet
ws = wb.active

# Data can be assigned directly to cells
ws['A1'] = 42

# Rows can also be appended
ws.append([1, 2, 3])

# Python types will automatically be converted
import datetime
ws['A2'] = datetime.datetime.now()

# Save the file
wb.save("sample.xlsx")

print("Complete!")

# 라인 정지
a = input()

추가한 Line 20~ 부분은, 완료 확인과 exe 실행 시 프로그램이 종료하면 같이 꺼지는 커맨드창을 고려하여 의미없는 입력 메소드를 하나 추가했다. (이로써 Enter를 쳤을 때 종료하게 된다.)

이제 커맨드 상에서 소스파일이 있는 위치로 이동한다.
cmd에서 cd로 이동할 수도 있지만 탐색기에서 이동 후 Shift + 마우스 오른쪽버튼 - 여기서 명령 창 열기 를 선택하면 더 쉽다.

그리고 아래 명령어를 입력한다.

> pyinstaller -F -n excel.exe excel_test.py

-F: 한 개의 파일로 만들기. 용량은 당연히 조금더 커진다. (없을 시 dll 파일들과 분리되어 폴더로 생성됨)
-n: 이름짓기. 이름은 바로 오른쪽에 기입 (없을 시 소스파일명과 동일한 파일이 생성됨)
--noconsole: 콘솔창 띄우지 않기



이렇게 ~ successfully 라 뜨면 성공이다.

탐색기를 확인해보면 4가지 파일이 새로 생성되어 있다.



여기서 실제 사용될 exe 파일은 dist 폴더에 저장되어 있다.

실행시켜 보면, 정상적으로 엑셀 파일이 생성되는 것을 볼 수 있다.
(파이썬이 설치되지 않은 컴퓨터에서의 테스트도 완료했다.)



+ requests 라이브러리가 import 되어 있을 때, 위와 같은 과정을 거쳐 만든 exe파일을 실행해보면 이런 오류가 발생한다.



마지막을 읽어보면 queue 라는 모듈이 없다고 한다.
해결법은 간단하다. 생성할 때 queue 모듈을 포함해주면 된다.

>pyinstaller -F excel_test.py --hidden-import=queue

참고 사이트: https://stackoverflow.com/questions/36400111/pyinstaller-doesnt-import-queue

Python BeautifulSoup 네이버 블로그 크롤링 + txt 파일 만들기



Laptop
운영체제Windows 10 Home 64bit
개발프로그램PyCharm Community Edition 2017.1.3
Python 3.4.4


크롤링(Crawling)은 웹페이지의 데이터를 가져오는 것을 말한다.

Beautifulsoup라는 라이브러리가 이 역할을 수행하는데, pip을 통한 별도의 설치가 필요하다.

설치과정은 다른 블로그에 소개된 곳이 많으니 링크로 대체한다.

pip 설치: http://codingdojang.com/scode/371
Beautifulsoup 설치: http://shaeod.tistory.com/900

또한 requests 라는 연결 확인을 위한 라이브러리도 필요하다. 
같은 방식으로 커맨드창에 pip install requests 라고 입력하면 된다.

Beautifulsoup에 대한 기본적인 튜토리얼은 공식사이트에 잘 나와 있다.
id검색, class 검색 등 다양한 예제가 있으니 처음 접한다면 읽어보는 게 좋다.
http://beautifulsoup-korean.readthedocs.io/ko/latest/


프로그램 방식은 먼저 블로그 ID와 시작과 끝 페이지 수를 입력받고, 그 범위 안의 포스트(글)들을 긁어와 출력하면서 txt 파일에 저장한다.

naver_blog_crawling.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# --*-- coding: utf-8 --*--
import codecs

import re

import collections
import requests

from bs4 import BeautifulSoup


# 에러 리스트 생성 함수
def insert_error(blog_id, error, error_doc):
    for i in error_doc:
        error_log = str(error_doc["page"]) + "page / " + str(error_doc["post_number"]) \
                    + "th post / " + error + " / http://blog.naver.com/PostList.nhn?blogId=" + blog_id + "&currentPage=" + str(error_doc["page"])
    error_list.append(error_log)

total_num = 0;

error_list = []

print("블로그 ID->")
blog_id = input()

print("\n탐색 시작 페이지 수->")
start_p = int(input())

print("\n탐색 종료 페이지 수->")
end_p = int(input())

print("\nCreating File Naver_Blog_Crawling_Result.txt...\n")

# 파일 열기
file = codecs.open("Naver_Blog_Crawling_Result.txt", 'w', encoding="utf-8")

# 페이지 단위
for page in range(start_p, end_p + 1):
    print("=" * 50)
    file.write("=" * 50 + "\n")

    doc = collections.OrderedDict()

    url = "http://blog.naver.com/PostList.nhn?blogId=" + blog_id + "&currentPage=" + str(page)
    r = requests.get(url)
    if (not r.ok):
        print("Page" + page + "연결 실패, Skip")
        continue

    # html 파싱
    soup = BeautifulSoup(r.text.encode("utf-8"), "html.parser")

    # 페이지 당 포스트 수 (printPost_# 형식의 id를 가진 태그 수)
    post_count = len(soup.find_all("table", {"id": re.compile("printPost.")}))

    doc["page"] = page

    # 포스트 단위
    for pidx in range(1, post_count + 1):
        print('-' * 50)
        file.write('-' * 50 + "\n")

        doc["post_number"] = pidx
        post = soup.find("table", {"id": "printPost" + str(pidx)})

        # 제목 찾기---------------------------

        title = post.find("h3", {"class": "se_textarea"})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (title == None):
            title = post.find("span", {"class": "pcol1 itemSubjectBoldfont"})

        if (title != None):
            doc["title"] = title.text.strip()
        else:
            doc["title"] = "TITLE ERROR"

        # 날짜 찾기---------------------------

        date = post.find("span", {"class": re.compile("se_publishDate.")})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (date == None):
            date = post.find("p", {"class": "date fil5 pcol2 _postAddDate"})

        if (date != None):
            doc["date"] = date.text.strip()
        else:
            doc["date"] = "DATE ERROR"

        # 내용 찾기---------------------------

        content = post.find("div", {"class": "se_component_wrap sect_dsc __se_component_area"})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (content == None):
            content = post.find("div", {"id": "postViewArea"})

        if (title != None):
            # Enter 5줄은 하나로
            doc["content"] = "\n" + content.text.strip().replace("\n" * 5, "\n")
        else:
            doc["content"] = "CONTENT ERROR"

        # doc 출력 (UnicodeError - 커맨드 창에서 실행 시 발생)
        for i in doc:

            str_doc = str(i) + ": " + str(doc[i])
            try:
                print(str_doc)
            except UnicodeError:
                print(str_doc.encode("utf-8"))

            # 파일 쓰기
            file.write(str_doc + "\n")

            # 에러 처리
            if ("ERROR" in str(doc[i])):
                insert_error(blog_id, doc[i], doc)

        # 전체 수 증가
        total_num += 1


# 결과 출력 (전체 글 수, 에러 수)
print("=" * 50)
file.write("=" * 50 + "\n")

print("Total : " + str(total_num))

error_num = len(error_list)
print("Error : " + str(error_num))

# 에러가 있을 경우 출력
if (error_num != 0):
    print("Error Post : ")
    for i in error_list:
        print(i)

# 파일 닫기
file.close()


Line 42:
doc 을 초반에 dict 형식으로 선언했다가 출력할 때 순서가 바뀐다는 것을 알고 OrderedDict로 변경했다. 그 후 값 입력 등 사용 방법은 dict 형식과 동일하다.

Line 71:
개발 중 에러가 발생하는 글들을 보면 블로그 페이지 소스보기에서 <!--스마트에디터3 타이틀 제거 임시 적용--> 이라는 주석이 붙은 글들이 있길래 (아마도 구버전으로 추정) 살펴봤더니 제목이나 내용의 태그 속성이 아예 다르다. if문으로 따로 구분해 두었다.
(가끔 이 두 형태가 섞여있는 블로그도 종종 있었다.)

페이지 상에서도 모양이 다르다.



현재 일반적인 모양

스마트에디터3 제거



Line 112:
UnicodeError는 커맨드 창에서 print 출력 시 종종 발생하는데, Pycharm의 경우 결과창 인코딩이 utf-8이기 때문에 문제없이 출력된다.
그래서 커맨드 출력의 경우 유니코드상태로 그대로 출력되는 경우가 많을텐데, txt 파일에 쓸 때는 별 문제 없으니 크게 신경쓰지 않아도 된다.



결과는 적당히 네이버 공식 블로그(http://blogpeople.blog.me/) 를 파싱한 결과로 캡쳐했다.


PyCharm 결과 화면






Cmd 결과 화면





생성된 txt 파일 (Naver_Blog_Crawling_Result.txt)
메모장으로 열면 \n가 깨진다. 인코딩을 선택할 수 있는 Microsoft Word 등의 프로그램으로 정상적인 결과를 확인할 수 있다.


2017년 6월 11일 일요일

Android Studio OpenCV + Tesseract OCR 어플 만들기


Laptop
운영체제Windows 10 Home 64bit
개발프로그램Android Studio 2.3.3
(buildToolsVersion "25.0.2")
프로젝트명opencvocr

Mobile
모델LG X5
운영체제Android 6.0.1 Marshmallow

Windows 에서 android studio 2.2 이상 + opencv 3.1 + ndk 개발환경 만들기:
http://webnautes.tistory.com/1054

우선 OpenCV 환경을 위해서 위 링크의 2번 과정을 먼저 거쳐야한다.

단, 프레임단위 작업 및 이후 글자 인식의 효율을 높일 기능을 추가할 가능성을 두고 OpenCV를 사용한 것이기 때문에, 일단 이 프로그램에서 C++ 및 JNI 부분까지는 필요하지 않다. 단 그러기 위해 프로젝트 생성할 때 C++ 관련 항목들을 모두 체크해두는 게 좋을 것 같다. (Include C++ support, finish 바로 전에 Customize C++ Support 체크박스 두개)

또한 똑같이 MainActivity가 아니라, CameraView라는 액티비티를 새로 생성하여 MainActivity에서 Intent를 이용하여 띄우는 방식으로 구현하였다.
그대로 MainActivity로 생성했어도 소스를 복사하고 아래와 같이 intent-filter를 변경해주면 된다.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.opencvocr">

    <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera.autofocus"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera.front"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera.front.autofocus"
        android:required="false" />

    <supports-screens
        android:anyDensity="true"
        android:largeScreens="true"
        android:normalScreens="true"
        android:resizeable="true"
        android:smallScreens="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".CameraView"
            android:configChanges="keyboardHidden|orientation"
            android:screenOrientation="landscape"
            android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
    </application>

</manifest>


Tesseract 개발 환경은 간단히 app의 build.gradle에서 다음 한 줄을 추가해주면 된다.
그리고나면 상단에 노란색 알림이 뜰텐데, Sync Now를 클릭해주고 완료를 기다리면 된다.

dependencies {
    ...
    compile 'com.rmtheis:tess-two:6.3.0'    
}

(글 작성 시점 최신버전은 6.3.0 이었다.  아래 링크에서 확인해보면 된다.)
https://github.com/rmtheis/tess-two/releases

또한 아래에서 kor.tessdata를 다운받아 main/assets 폴더를 생성하여 안에 넣어준다.
https://github.com/tesseract-ocr/tessdata





다음은 MainActivity 레이아웃이다. 간단하게 OCR 결과를 받아올 EditView와 카메라를 띄울 버튼으로 구성했다.
(res - values - strings.xml 에
<string name="ocr_result_tip">인식 결과는 여기에 나타납니다.</string>
가 선언되어 있는 상태이다.)

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/edit_ocrresult"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/ocr_result_tip"
        android:text="" />

    <Button
        android:id="@+id/btn_camera"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Camera" />

</LinearLayout>





MainActivity에서는 Tesseract 모듈 초기화 및 버튼을 클릭했을 때 CameraView를 띄우는 작업을 수행한다.
두 Activity간 데이터 전송은 startActivityForResult로 선언한 Intent를 띄우고, onActivityResult 메소드에서 if문을 통해 해당 requestcode에 맞을 때 알맞는 코드를 입력하면 된다.
또한 AssetManager를 통해 아까 assets 폴더에 넣었던 tessdata를 기기에 다운받을 수 있게 된다.

Tesseract 객체는 static으로 선언하여 CameraView에서도 사용될 수 있게 한다.

MainActivity.java
package com.opencvocr;

import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.googlecode.tesseract.android.TessBaseAPI;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


public class MainActivity extends AppCompatActivity {

    private Button mBtnCameraView;
    private EditText mEditOcrResult;
    private String datapath = "";
    private String lang = "";

    private int ACTIVITY_REQUEST_CODE = 1;

    static TessBaseAPI sTess;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        // 뷰 선언
        mBtnCameraView = (Button) findViewById(R.id.btn_camera);
        mEditOcrResult = (EditText) findViewById(R.id.edit_ocrresult);
        sTess = new TessBaseAPI();


        // Tesseract 인식 언어를 한국어로 설정 및 초기화
        lang = "kor";
        datapath = getFilesDir()+ "/tesseract";

        if(checkFile(new File(datapath+"/tessdata")))
        {
            sTess.init(datapath, lang);
        }

        mBtnCameraView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {

                // 버튼 클릭 시

                // Camera 화면 띄우기
                Intent mIttCamera = new Intent(MainActivity.this, CameraView.class);
                startActivityForResult(mIttCamera, ACTIVITY_REQUEST_CODE);

            }
        });
    }



    boolean checkFile(File dir)
    {
        //디렉토리가 없으면 디렉토리를 만들고 그후에 파일을 카피
        if(!dir.exists() && dir.mkdirs()) {
            copyFiles();
        }
        //디렉토리가 있지만 파일이 없으면 파일카피 진행
        if(dir.exists()) {
            String datafilepath = datapath + "/tessdata/" + lang + ".traineddata";
            File datafile = new File(datafilepath);
            if(!datafile.exists()) {
                copyFiles();
            }
        }
        return true;
    }

    void copyFiles()
    {
        AssetManager assetMgr = this.getAssets();

        InputStream is = null;
        OutputStream os = null;

        try {
            is = assetMgr.open("tessdata/"+lang+".traineddata");

            String destFile = datapath + "/tessdata/" + lang + ".traineddata";

            os = new FileOutputStream(destFile);

            byte[] buffer = new byte[1024];
            int read;
            while ((read = is.read(buffer)) != -1) {
                os.write(buffer, 0, read);
            }
            is.close();
            os.flush();
            os.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(resultCode==RESULT_OK)
        {
            if(requestCode== ACTIVITY_REQUEST_CODE)
            {
                // 받아온 OCR 결과 출력
                mEditOcrResult.setText(data.getStringExtra("STRING_OCR_RESULT"));
            }
        }
    }
}





다음은 CameaView 레이아웃이다.
OpenCV에서 사용되는 JavaCameraView 위에 시작 및 종료버튼을 띄우고, SurfaceView 2개를 이용하여 하나는 background 색을 주고, 그 위에 약간 작게 투명한색 (#00ff0000) 을 겹쳐 테두리처럼 보이도록 했다.
그리고 상단에 OCR 결과 텍스트가 출력되고 캡쳐한 이미지는 ImageView를 통해 중앙 ROI 부분에 띄우게 된다.

camera_view.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"

    >

    <org.opencv.android.JavaCameraView
        android:id="@+id/activity_surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <!--ROI 영역-->
    <SurfaceView
        android:id="@+id/surface_roi_border"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#00ff00" />

    <SurfaceView
        android:id="@+id/surface_roi"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#00ff0000" />


    <!--Start 및 뒤로가기 버튼-->
    <Button
        android:id="@+id/btn_ocrstart"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp"
        android:background="#7d010000"
        android:onClick="onClickButton"
        android:text="Start"
        android:textColor="#ffffff"
        android:textSize="15dp" />

    <Button
        android:id="@+id/btn_finish"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="#7d010000"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="10dp"
        android:layout_marginRight="10dp"
        android:onClick="onClickButton"
        android:text="←"
        android:textColor="#ffffff"
        android:textSize="20dp"
         />

    <!-- OCR 결과 및 캡쳐이미지 출력-->
    <TextView
        android:id="@+id/text_ocrresult"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:layout_marginTop="15dp"
        android:layout_marginLeft="15dp"
        android:text="@string/ocr_result_tip"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="20dp" />

    <ImageView
        android:id="@+id/image_capture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
         />
    
</RelativeLayout>

이 후 Java코드에서 방향 센서에 따라 portrait, landscape 에서 각각 ROI가 바뀌므로 여기서는 기본적인 속성을 설정하여 표시하지 않게 했다.
Java까지 작성하면 아래와 같은 결과가 나온다.




마지막으로  CameraView 소스다.

퍼미션 부분은 위의 OpenCV 링크에 나온 소스는 왜인지 잘 동작하지 않아서, 다른 소스를 참고해서 작성했다.
(onCreate 이전까지가 다르다.)

Android 6 이상부터 필요한 기능이고, 그 이하는 필요하지 않다.

Orientation 부분은 예전 글을 참고하자.
http://bluebead38.blogspot.kr/2017/05/android-studio-screenorientation-360.html

CameraView.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
package com.opencvocr;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RequiresApi;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.Mat;
import org.opencv.core.Rect;
import org.opencv.imgproc.Imgproc;

import static com.opencvocr.MainActivity.sTess;


public class CameraView extends Activity implements CameraBridgeViewBase.CvCameraViewListener2 {

    private Mat img_input;
    private static final String TAG = "opencv";
    private CameraBridgeViewBase mOpenCvCameraView;
    private String m_strOcrResult = "";

    private Button mBtnOcrStart;
    private Button mBtnFinish;
    private TextView mTextOcrResult;

    private Bitmap bmp_result;

    private OrientationEventListener mOrientEventListener;

    private Rect mRectRoi;

    private SurfaceView mSurfaceRoi;
    private SurfaceView mSurfaceRoiBorder;

    private int mRoiWidth;
    private int mRoiHeight;
    private int mRoiX;
    private int mRoiY;
    private double m_dWscale;
    private double m_dHscale;

    private View m_viewDeco;
    private int m_nUIOption;
    private android.widget.RelativeLayout.LayoutParams mRelativeParams;
    private ImageView mImageCapture;
    private Mat m_matRoi;
    private boolean mStartFlag = false;

    // 현재 회전 상태 (하단 Home 버튼의 위치)
    private enum mOrientHomeButton {Right, Bottom, Left, Top}

    private mOrientHomeButton mCurrOrientHomeButton = mOrientHomeButton.Right;


    static final int PERMISSION_REQUEST_CODE = 1;
    String[] PERMISSIONS = {"android.permission.CAMERA"};

    /*
    // cpp 관련 부분. 지금은 필요하지 않음
    static {
        System.loadLibrary("opencv_java3");
        System.loadLibrary("native-lib");
        System.loadLibrary("imported-lib");
    }
    */

    private boolean hasPermissions(String[] permissions) {
        // 퍼미션 확인
        int result = -1;
        for (int i = 0; i < permissions.length; i++) {
            result = ContextCompat.checkSelfPermission(getApplicationContext(), permissions[i]);
        }
        if (result == PackageManager.PERMISSION_GRANTED) {
            return true;

        } else {
            return false;
        }
    }


    private void requestNecessaryPermissions(String[] permissions) {
        ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE: {
                //퍼미션을 거절했을 때 메시지 출력 후 종료
                if (!hasPermissions(PERMISSIONS)) {
                    Toast.makeText(getApplicationContext(), "CAMERA PERMISSION FAIL", Toast.LENGTH_LONG).show();
                    finish();
                }
                return;
            }
        }
    }


    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS: {
                    // 퍼미션 확인 후 카메라 활성화
                   if (hasPermissions(PERMISSIONS))
                        mOpenCvCameraView.enableView();
                }
                break;
                default: {
                    super.onManagerConnected(status);
                }
                break;
            }
        }
    };


    @RequiresApi(api = Build.VERSION_CODES.CUPCAKE)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_view);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        if (!hasPermissions(PERMISSIONS)) { //퍼미션 허가를 했었는지 여부를 확인
            requestNecessaryPermissions(PERMISSIONS);//퍼미션 허가안되어 있다면 사용자에게 요청
        } else {
            //이미 사용자에게 퍼미션 허가를 받음.
        }

        // 카메라 설정
        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.activity_surface_view);
        mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
        mOpenCvCameraView.setCameraIndex(0); // front-camera(1),  back-camera(0)
        mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);


        //뷰 선언
        mBtnOcrStart = (Button) findViewById(R.id.btn_ocrstart);
        mBtnFinish = (Button) findViewById(R.id.btn_finish);

        mTextOcrResult = (TextView) findViewById(R.id.text_ocrresult);

        mSurfaceRoi = (SurfaceView) findViewById(R.id.surface_roi);
        mSurfaceRoiBorder = (SurfaceView) findViewById(R.id.surface_roi_border);

        mImageCapture = (ImageView) findViewById(R.id.image_capture);

        //풀스크린 상태 만들기 (상태바, 네비게이션바 없애고 고정시키기)
        m_viewDeco = getWindow().getDecorView();
        m_nUIOption = getWindow().getDecorView().getSystemUiVisibility();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            m_nUIOption |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
            m_nUIOption |= View.SYSTEM_UI_FLAG_FULLSCREEN;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
            m_nUIOption |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

        m_viewDeco.setSystemUiVisibility(m_nUIOption);


        mOrientEventListener = new OrientationEventListener(this,
                SensorManager.SENSOR_DELAY_NORMAL) {

            @Override
            public void onOrientationChanged(int arg0) {

                //방향센서값에 따라 화면 요소들 회전

                // 0˚ (portrait)
                if (arg0 >= 315 || arg0 < 45) {
                    rotateViews(270);
                    mCurrOrientHomeButton = mOrientHomeButton.Bottom;
                    // 90˚
                } else if (arg0 >= 45 && arg0 < 135) {
                    rotateViews(180);
                    mCurrOrientHomeButton = mOrientHomeButton.Left;
                    // 180˚
                } else if (arg0 >= 135 && arg0 < 225) {
                    rotateViews(90);
                    mCurrOrientHomeButton = mOrientHomeButton.Top;
                    // 270˚ (landscape)
                } else {
                    rotateViews(0);
                    mCurrOrientHomeButton = mOrientHomeButton.Right;
                }


                //ROI 선 조정
                mRelativeParams = new android.widget.RelativeLayout.LayoutParams(mRoiWidth + 5, mRoiHeight + 5);
                mRelativeParams.setMargins(mRoiX, mRoiY, 0, 0);
                mSurfaceRoiBorder.setLayoutParams(mRelativeParams);

                //ROI 영역 조정
                mRelativeParams = new android.widget.RelativeLayout.LayoutParams(mRoiWidth - 5, mRoiHeight - 5);
                mRelativeParams.setMargins(mRoiX + 5, mRoiY + 5, 0, 0);
                mSurfaceRoi.setLayoutParams(mRelativeParams);

            }
        };

        //방향센서 핸들러 활성화
        mOrientEventListener.enable();

        //방향센서 인식 오류 시, Toast 메시지 출력 후 종료
        if (!mOrientEventListener.canDetectOrientation()) {
            Toast.makeText(this, "Can't Detect Orientation",
                    Toast.LENGTH_LONG).show();
            finish();
        }
    }

    public void onClickButton(View v) {

        switch (v.getId()) {

            //Start 버튼 클릭 시
            case R.id.btn_ocrstart:
                if (!mStartFlag) {
                    // 인식을 새로 시작하는 경우

                    // 버튼 속성 변경
                    mBtnOcrStart.setEnabled(false);
                    mBtnOcrStart.setText("Working...");
                    mBtnOcrStart.setTextColor(Color.LTGRAY);

                    bmp_result = Bitmap.createBitmap(m_matRoi.cols(), m_matRoi.rows(), Bitmap.Config.ARGB_8888);

                    Utils.matToBitmap(m_matRoi, bmp_result);

                    // 캡쳐한 이미지를 ROI 영역 안에 표시
                    mImageCapture.setVisibility(View.VISIBLE);
                    mImageCapture.setImageBitmap(bmp_result);


                    //Orientation에 따라 Bitmap 회전 (landscape일 때는 미수행)
                    if (mCurrOrientHomeButton != mOrientHomeButton.Right) {
                        switch (mCurrOrientHomeButton) {
                            case Bottom:
                                bmp_result = GetRotatedBitmap(bmp_result, 90);
                                break;
                            case Left:
                                bmp_result = GetRotatedBitmap(bmp_result, 180);
                                break;
                            case Top:
                                bmp_result = GetRotatedBitmap(bmp_result, 270);
                                break;
                        }
                    }

                    new AsyncTess().execute(bmp_result);
                } else {
                    //Retry를 눌렀을 경우

                    // ImageView에서 사용한 캡쳐이미지 제거
                    mImageCapture.setImageBitmap(null);
                    mTextOcrResult.setText(R.string.ocr_result_tip);

                    mBtnOcrStart.setEnabled(true);
                    mBtnOcrStart.setText("Start");
                    mBtnOcrStart.setTextColor(Color.WHITE);

                    mStartFlag = false;
                }

                break;


            // 뒤로 버튼 클릭 시
            case R.id.btn_finish:
                //인식 결과물을 MainActivity에 전달하고 종료
                Intent intent = getIntent();
                intent.putExtra("STRING_OCR_RESULT", m_strOcrResult);
                setResult(RESULT_OK, intent);
                mOpenCvCameraView.disableView();
                finish();
                break;
        }
    }

    public void rotateViews(int degree) {
        mBtnOcrStart.setRotation(degree);
        mBtnFinish.setRotation(degree);
        mTextOcrResult.setRotation(degree);

        switch (degree) {
            // 가로
            case 0:
            case 180:

                //ROI 크기 조정 비율 변경
                m_dWscale = (double) 1 / 2;
                m_dHscale = (double) 1 / 2;


                //결과 TextView 위치 조정
                mRelativeParams = new android.widget.RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                mRelativeParams.setMargins(0, convertDpToPixel(20), 0, 0);
                mRelativeParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
                mTextOcrResult.setLayoutParams(mRelativeParams);

                break;

            // 세로
            case 90:
            case 270:

                m_dWscale = (double) 1 / 4;    //h (반대)
                m_dHscale = (double) 3 / 4;    //w

                mRelativeParams = new android.widget.RelativeLayout.LayoutParams(convertDpToPixel(300), ViewGroup.LayoutParams.WRAP_CONTENT);
                mRelativeParams.setMargins(convertDpToPixel(15), 0, 0, 0);
                mRelativeParams.addRule(RelativeLayout.CENTER_VERTICAL);
                mTextOcrResult.setLayoutParams(mRelativeParams);


                break;
        }
    }

    //dp 단위로 입력하기 위한 변환 함수 (px 그대로 사용 시 기기마다 화면 크기가 다르기 때문에 다른 위치에 가버림)
    public int convertDpToPixel(float dp) {

        Resources resources = getApplicationContext().getResources();

        DisplayMetrics metrics = resources.getDisplayMetrics();

        float px = dp * (metrics.densityDpi / 160f);

        return (int) px;

    }

    public synchronized static Bitmap GetRotatedBitmap(Bitmap bitmap, int degrees) {
        if (degrees != 0 && bitmap != null) {
            Matrix m = new Matrix();
            m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
            try {
                Bitmap b2 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
                if (bitmap != b2) {
                    //bitmap.recycle(); (일반적으로는 하는게 옳으나, ImageView에 쓰이는 Bitmap은 recycle을 하면 오류가 발생함.)
                    bitmap = b2;
                }
            } catch (OutOfMemoryError ex) {
                // We have no memory to rotate. Return the original bitmap.
            }
        }

        return bitmap;
    }

    @Override
    public void onPause() {
        super.onPause();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override
    public void onResume() {
        super.onResume();

        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG, "onResume :: Internal OpenCV library not found.");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, mLoaderCallback);
        } else {
            Log.d(TAG, "onResume :: OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }

    public void onDestroy() {
        super.onDestroy();

        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override
    public void onCameraViewStarted(int width, int height) {

    }

    @Override
    public void onCameraViewStopped() {

    }

    @Override
    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

        // 프레임 획득
        img_input = inputFrame.rgba();


        // 가로, 세로 사이즈 획득
        mRoiWidth = (int) (img_input.size().width * m_dWscale);
        mRoiHeight = (int) (img_input.size().height * m_dHscale);


        // 사이즈로 중심에 맞는 X , Y 좌표값 계산
        mRoiX = (int) (img_input.size().width - mRoiWidth) / 2;
        mRoiY = (int) (img_input.size().height - mRoiHeight) / 2;

        // ROI 영역 생성
        mRectRoi = new Rect(mRoiX, mRoiY, mRoiWidth, mRoiHeight);


        // ROI 영역 흑백으로 전환
        m_matRoi = img_input.submat(mRectRoi);
        Imgproc.cvtColor(m_matRoi, m_matRoi, Imgproc.COLOR_RGBA2GRAY);
        Imgproc.cvtColor(m_matRoi, m_matRoi, Imgproc.COLOR_GRAY2RGBA);
        m_matRoi.copyTo(img_input.submat(mRectRoi));
        return img_input;
    }


    private class AsyncTess extends AsyncTask<Bitmap, Integer, String> {

        @Override
        protected String doInBackground(Bitmap... mRelativeParams) {
            //Tesseract OCR 수행
            sTess.setImage(bmp_result);

            return sTess.getUTF8Text();
        }

        protected void onPostExecute(String result) {
            //완료 후 버튼 속성 변경 및 결과 출력

            mBtnOcrStart.setEnabled(true);
            mBtnOcrStart.setText("Retry");
            mBtnOcrStart.setTextColor(Color.WHITE);

            mStartFlag = true;

            m_strOcrResult = result;
            mTextOcrResult.setText(m_strOcrResult);

        }
    }
}

Line 202:
기기의 회전 상태에 따라 각각의 뷰들이 함께 회전한다.

(이때 왜인지 portrait와 landscape를 제외한 나머지 둘 90, 180도는 TextView가 어딘가에 가려진듯 보이지 않는다.)

Line 258:
Start 버튼을 누르면 Rect형식으로 지정된 ROI 영역을 Mat 형식으로 가져오게 되고, 그것을 Bitmap으로 전환시켜 AsyncTask 객체로 Tesseract OCR 작업을 한다. (일반 함수를 쓰면 처리를 하는 동안 카메라 프레임이 멈춘다.)

Line 268:
기기의 회전 상태에 따라 인식될 비트맵도 함께 회전한다.

Line 287:
Retry를 누르면 모든 뷰들이 처음 상태로 돌아온다.

Line 303:
뒤로가기(화살표)를 누르면 putExtra 메소드를 통해 결과가 MainActivity로 전송되고, 카메라를 종료한다.

Line 441:
ROI 영역은 글자 인식 영역이므로 흑백 처리를 한다.

Line 460:
AsyncTask 종료 후 결과를 TextView에 출력한다.




인식 테스트 결과는 다음과 같다.

사진은 꽤 잘 되는것처럼 나왔지만, 깨끗하게 촬영하지 않을 시 상당히 오랜 시간이 걸렸음에도 제대로 된 결과를 출력하지 못할 때가 많았다.

또한 JavaCameraView도 중간에 문제가 있는지 오랜시간 켜두면 꺼지는 현상이 있는데, 에러로그를 출력하지 않아서 이유를 못찾고 있다.
+ 메모리 초과 문제로 추정된다.





+ Github 등록
https://github.com/bluebead38/OpenCVTesseractOcr

Tesseract 버전 최신으로 변경 및 compile 대신 implement 이용


- 참고 사이트


Tesseract : http://cosmosjs.blog.me/220937785735

Intent : http://arabiannight.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9CAndroid-Intent%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A3%BC%EA%B3%A0-%EB%B0%9B%EA%B8%B0

Permission : http://blog.naver.com/lawra20/220674924962

ROI 흑백처리 : http://answers.opencv.org/question/27677/how-to-convert-roi-in-gray-monochrome-on-android/

pixel dp convert : http://neoroid.tistory.com/entry/Android-pixel-to-dp-dp-to-pixel

Bitmap 회전 : http://snowbora.tistory.com/428