by museonghwang

Tokenization 개념과 영어 및 한국어 특성

|

  • Tokenization 개념
  • 단어 토큰화(Word Tokenization)
  • Tokenization 고려사항
  • 영어 Word Tokenization
    1. NLTK의 토크나이저 - word_tokenize
    2. NLTK의 토크나이저 - WordPunctTokenizer
    3. NLTK의 토크나이저 - TreebankWordTokenizer
    4. Keras의 토크나이저 - text_to_word_sequence
    5. 띄어쓰기를 기준으로 하는 단어 토큰화(잘 되는 것 같아도 하지마세요)
  • 한국어 토큰화의 특징
    1. 한국어는 띄어쓰기가 영어보다 잘 지켜지지 않는다. -> 띄어쓰기 보정 : PyKoSpacing
    2. 한국어는 주어 생략은 물론 어순도 중요하지 않다.
    3. 한국어는 교착어이다.
  • 한국어 Word Tokenization(KoNLPy)
    1. 띄어쓰기를 기준으로 하는 단어 토큰화(가급적 하지마세요)
    2. 형태소 분석기 KoNLPy 설치
    3. KoNLPy - 형태소 분석기 Okt
    4. KoNLPy - 형태소 분석기 꼬꼬마
    5. KoNLPy - 형태소 분석기 코모란
    6. KoNLPy - 형태소 분석기 한나눔
    7. KoNLPy - 형태소 분석기 Mecab
  • 문장 토큰화(Sentence Tokenization)
  • 영어 Sentence Tokenization(NLTK)
  • 한국어 Sentence Tokenization(KSS)


Tokenization 개념

주어진 텍스트(corpus)에서 토큰(token)이라 불리는 단위로 나누는 작업토큰화(tokenization) 라고 부릅니다. 토큰의 단위가 상황에 따라 다르지만, 보통 의미있는 단위로 토큰을 정의 합니다. 일반적으로 토큰의 단위는 크게는 ‘문장’ 작게는 ‘단어’ 라고 보시면 됩니다.

자연어 처리를 위해서는 우선 텍스트에 대한 정보를 단위별로 나누는 것이 일반적인데, 왜냐하면 기계에게 어느 구간까지가 문장이고, 단어인지를 알려주어야 하기 때문 입니다. 문장 토큰화, 단어 토큰화, subword 토큰화 등 다양한 단위의 토큰화가 존재합니다.

image

image


단어 토큰화(Word Tokenization)

토큰의 기준을 단어(word)(단어구, 의미를 갖는 문자열 등) 로 하는 경우, 단어 토큰화(word tokenization) 라고 합니다.

예를들어 아래의 입력으로부터 구두점(punctuation) 과 같은 문자를 제외시키는 간단한 단어 토큰화 작업을 해보겠습니다. 구두점이란 마침표(.), 컴마(,), 물음표(?), 세미콜론(;), 느낌표(!) 등과 같은 기호를 말합니다.

  • 입력 : Time is an illusion. Lunchtime double so!
  • 출력 : “Time”, “is”, “an”, “illustion”, “Lunchtime”, “double”, “so”


위 예제에서 토큰화 작업은 굉장히 간단합니다. 구두점을 지운 뒤에 띄어쓰기(whitespace)를 기준으로 잘라냈습니다. 하지만, 보통 토큰화 작업은 단순히 구두점이나 특수문자를 전부 제거하는 정제(cleaning) 작업을 수행하는 것만으로 해결되지 않습니다. 구두점이나 특수문자를 전부 제거하면 토큰이 의미를 잃어버리는 경우가 발생 하기도 하며, 심지어 띄어쓰기 단위로 자르면 사실상 단어 토큰이 구분되는 영어와 달리, 한국어는 띄어쓰기만으로는 단어 토큰을 구분하기 어렵습니다.


Tokenization 고려사항

토큰화 작업은 단순하게 코퍼스에서 구두점을 제외하고 공백 기준으로 잘라내는 작업이라고 간주할 수는 없습니다. 이러한 일은 보다 섬세한 알고리즘이 필요한데 그 이유를 정리하면 다음과 같습니다.

  • 단순 띄어쓰기로 단어를 구분하면 안됨
    • We are the One!! -> [‘We’, ‘are’, ‘the’, ‘One!!’]
    • We are the One -> [‘We’, ‘are’, ‘the’, ‘One’]
    • 특수문자로 인해 다른 단어로 인식 되는 경우가 발생
  • 구두점 및 특수문자를 단순 제외하면 안됨
    • 구두점 : 마침표(.)의 경우 문장의 경계를 알 수 있는데 도움이 됨.
    • 단어 자체에 구두점 : Ph.D -> Ph D -> [‘Ph’, ‘D’]
    • 가격 : $45.55 -> 45 55 -> [‘45’, ‘55’]
    • 날짜 : 01/02/06 -> 01 02 06 -> [‘01’, ‘02’, ‘06’]
    • 본래의 의미가 상실 되는 경우가 발생
  • 줄임말과 단어 내에 띄어쓰기가 있는 경우
    • 줄임말
      • what’re -> what are의 줄임말(re:접어(clitic))
      • we’re -> we are의 줄임말
      • I’m -> I am의 줄임말(m:접어(clitic))
    • 단어 내 띄어쓰기
      • New York
      • rock ‘n’ roll


영어 Word Tokenization

영어로 토큰화를 할 때는 일반적으로 NLTK 라는 패키지로, 영어 자연어 처리를 위한 패키지 라고 보면 됩니다. NLTK 에서는 다양한 영어 토크나이저(토큰화를 수행하는 도구)를 제공하고 있으며, 토큰화 결과는 토크나이저마다 규칙이 조금씩 다르므로 어떤 토크나이저를 사용할 지 정답은 없습니다.

토큰화를 하다보면, 예상하지 못한 경우가 있어서 토큰화의 기준 을 생각해봐야 하는 경우가 발생합니다. 물론, 이러한 선택은 해당 데이터를 가지고 어떤 용도로 사용할 것인지에 따라서 그 용도에 영향이 없는 기준 으로 정하면 됩니다.


1. NLTK의 토크나이저 - word_tokenize

import nltk
nltk.download('punkt')


아래의 문장을 보면 Don’t와 Jone’s에는 아포스트로피(‘)가 들어가있습니다.

sentence = "Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop."


아포스트로피가 들어간 상황에서 Don’t와 Jone’s는 word_tokenize 에 의해 어떻게 토큰화되는지 살펴보겠습니다.

from nltk.tokenize import word_tokenize
print(word_tokenize(sentence))
[output]
['Do', "n't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr.', 'Jone', "'s", 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']


word_tokenize 는 Don’t를 Do와 n’t로 분리하였으며, Jone’s는 Jone과 ‘s로 분리한 것을 확인할 수 있습니다.


2. NLTK의 토크나이저 - WordPunctTokenizer

from nltk.tokenize import WordPunctTokenizer  
print(WordPunctTokenizer().tokenize(sentence))
[output]
['Don', "'", 't', 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr', '.', 'Jone', "'", 's', 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']


WordPunctTokenizer 는 Don’t를 Don과 ‘와 t로 분리하였으며, Jone’s를 Jone과 ‘와 s로 분리한 것을 확인할 수 있습니다.


3. NLTK의 토크나이저 - TreebankWordTokenizer

TreebankWordTokenizer 는 표준 토큰화 규칙인 Penn Treebank Tokenization 를 따르는 토크나이저 입니다.

  • 규칙 1. 하이푼으로 구성된 단어는 하나로 유지한다.
  • 규칙 2. doesn’t와 같이 아포스트로피로 ‘접어’가 함께하는 단어는 분리해준다.


from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
text = "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."
print(tokenizer.tokenize(text))
[output]
['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own', '.']


print(tokenizer.tokenize(sentence))
[output]
['Do', "n't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr.', 'Jone', "'s", 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']


4. Keras의 토크나이저 - text_to_word_sequence

from tensorflow.keras.preprocessing.text import text_to_word_sequence
print(text_to_word_sequence(sentence))

[output]
["don't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', 'mr', "jone's", 'orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']


지금까지 주어진 문자열로부터 토크나이저를 사용하여 단어 토큰화를 수행해봤습니다. 다시 말하지만 뭐가 더 좋은지 정답은 없습니다. 사실 토크나이저마다 각자 규칙이 다르기 때문에 사용하고자 하는 목적에 따라 토크나이저를 선택하는 것이 중요 합니다.


5. 띄어쓰기를 기준으로 하는 단어 토큰화(잘 되는 것 같아도 하지마세요)

사실 영어는 띄어쓰기를 기준으로 단어 토큰화를 한다고 하더라도 꽤 잘 되는 편입니다. 하지만 그럼에도 띄어쓰기를 기준으로 단어 토큰화를 하는 것은 하지 않는 것이 좋은데 그 이유를 이해해보겠습니다.

우선 다음과 같은 영어 문장에 대해 NLTK 로 토큰화를 하겠습니다.

from nltk.tokenize import word_tokenize

en_text = "A Dog Run back corner near spare bedrooms!!!!"
print(word_tokenize(en_text))
[output]
['A', 'Dog', 'Run', 'back', 'corner', 'near', 'spare', 'bedrooms', '!', '!', '!', '!']


잘 동작합니다. 이번에는 NLTK 가 아닌 그냥 띄어쓰기 단위로 토큰화를 해보겠습니다. 파이썬은 주어진 문자열에 .split() 을 하면 띄어쓰기를 기준으로 전부 원소를 잘라서 리스트 형태로 리턴합니다.

print(en_text.split())
[output]
['A', 'Dog', 'Run', 'back', 'corner', 'near', 'spare', 'bedrooms!!!!']


이데 바로 띄어쓰기를 기준으로 단어 토큰화를 수행한 결과입니다.

사실 영어는 NLTK 라는 패키지를 사용하면 좀 더 섬세한 토큰화를 하기는 하지만, 띄어쓰기를 하는 것만으로도 거의 토큰화가 잘 되는 편입니다. 하지만 그럼에도 띄어쓰기를 기준으로 하는 것을 지양(하지마세요)하라는 것은 이유가 있습니다. 예를 들어 영어 문장에 특수 문자를 추가하여 NLTK 로 토큰화를 하겠습니다.

from nltk.tokenize import word_tokenize

en_text = "A Dog Run back corner near spare bedrooms... bedrooms!!"
print(word_tokenize(en_text))
[output]
['A', 'Dog', 'Run', 'back', 'corner', 'near', 'spare', 'bedrooms', '...', 'bedrooms', '!', '!']


보면 특수문자들도 알아서 다 띄워서 bedrooms이 정상적으로 분리되었습니다. 하지만 띄어쓰기 단위로 토큰화를 한다면 어떻게 되는지 보겠습니다.

print(en_text.split())
[output]
['A', 'Dog', 'Run', 'back', 'corner', 'near', 'spare', 'bedrooms...', 'bedrooms!!']


bedrooms와 …가 붙어서 bedrooms…가 나오고, bedrooms와 !!!가 붙어서 bedrooms!!!가 나옵니다. 파이썬이 보기에 이들은 전부 다른 단어로 인식합니다.

if 'bedrooms' == 'bedrooms...': 
    print('이 둘은 같습니다.')
else:
    print('이 둘은 다릅니다.')
[output]
이 둘은 다릅니다.


NLTK 가 훨씬 섬세하게 동작한다는 것을 알 수 있습니다.


한국어 토큰화의 특징

한국어 토큰화는 영어보다 자연어 처리가 훨씬 어렵습니다. 대표적으로 다음과 같은 이유가 있습니다.

  1. 한국어는 교착어이다.
  2. 한국어는 띄어쓰기가 잘 지켜지지 않는다.
  3. 한국어는 어순이 그렇게 중요하지 않다.
  4. 한자어라는 특성상 하나의 음절조차도 다른 의미를 가질 수 있다.
  5. 주어가 손쉽게 생략된다.
  6. 데이터가 영어에 비해 너무 부족하다.
  7. 언어 특화 오픈 소스 부족
  8. OpenAI, Meta의 언어 모델은 영어 위주이므로 한국어에 대한 성능은 상대적으로 저하.


영어는 New York 과 같은 합성어나 he’s 와 같이 줄임말에 대한 예외처리만 한다면, 띄어쓰기(whitespace)를 기준으로 하는 띄어쓰기 토큰화를 수행해도 단어 토큰화가 잘 작동합니다. 하지만 한국어는 영어와는 달리 띄어쓰기만으로는 토큰화를 하기에 부족 합니다.

한국어의 경우에는 띄어쓰기 단위가 되는 단위‘어절’ 이라고 하는데 어절 토큰화와 단어 토큰화는 같지 않기 때문에, 어절 토큰화는 한국어 NLP에서 지양 되고 있습니다. 그 근본적인 이유는 한국어 가 영어와는 다른 형태를 가지는 언어인 교착어 라는 점에서 기인합니다. 한국어 토큰화가 어려운 점을 살펴보겠습니다.


1. 한국어는 띄어쓰기가 영어보다 잘 지켜지지 않는다. -> 띄어쓰기 보정 : PyKoSpacing

한국어는 영어권 언어와 비교하여 띄어쓰기가 어렵고 잘 지켜지지 않는 경향 이 있습니다. 그 이유는 여러 견해가 있으나, 가장 기본적인 견해는 한국어의 경우 띄어쓰기가 지켜지지 않아도 글을 쉽게 이해할 수 있는 언어라는 점입니다. 대부분의 데이터에서 띄어쓰기가 잘 지켜지지 않는 경향이 있습니다.

띄어쓰기를 전혀 하지 않은 한국어와 영어 두 가지 경우를 봅시다.

  • 제가이렇게띄어쓰기를전혀하지않고글을썼다고하더라도글을이해할수있습니다.
  • Tobeornottobethatisthequestion


영어의 경우에는 띄어쓰기를 하지 않으면 손쉽게 알아보기 어려운 문장들이 생깁니다. 이는 한국어(모아쓰기 방식)와 영어(풀어쓰기 방식)라는 언어적 특성의 차이에 기인 하므로, 결론적으로 한국어는 수많은 코퍼스에서 띄어쓰기가 무시되는 경우가 많아 자연어 처리가 어려워졌다는 것입니다. 결국 띄어쓰기를 보정해주어야 하는 전처리가 필요할 수도 있습니다.


PyKoSpacing 은 띄어쓰기가 되어있지 않은 문장을 띄어쓰기를 한 문장으로 변환해주는 딥러닝 기반의 패키지입니다. PyKoSpacing 은 대용량 코퍼스를 학습하여 만들어진 띄어쓰기 딥 러닝 모델로 준수한 성능을 가지고 있습니다.

image


pip install git+https://github.com/haven-jeon/PyKoSpacing.git
sent = '김철수는 극중 두 인격의 사나이 이광수 역을 맡았다. 철수는 한국 유일의 태권도 전승자를 가리는 결전의 날을 앞두고 10년간 함께 훈련한 사형인 유연재(김광수 분)를 찾으러 속세로 내려온 인물이다.'


임의의 문장을 임의로 띄어쓰기가 없는 문장으로 만들고, 이를 PyKoSpacing 의 입력으로 사용하여 원 문장과 비교해보겠습니다.

new_sent = sent.replace(" ", '') # 띄어쓰기가 없는 문장 임의로 만들기
print(new_sent)
[output]
김철수는극중두인격의사나이이광수역을맡았다.철수는한국유일의태권도전승자를가리는결전의날을앞두고10년간함께훈련한사형인유연재(김광수분)를찾으러속세로내려온인물이다.


from pykospacing import Spacing

spacing = Spacing()
kospacing_sent = spacing(new_sent) 

print(sent)
print(kospacing_sent)
[output]
김철수는 극중 두 인격의 사나이 이광수 역을 맡았다. 철수는 한국 유일의 태권도 전승자를 가리는 결전의 날을 앞두고 10년간 함께 훈련한 사형인 유연재(김광수 분)를 찾으러 속세로 내려온 인물이다.
김철수는 극중 두 인격의 사나이 이광수 역을 맡았다. 철수는 한국 유일의 태권도 전승자를 가리는 결전의 날을 앞두고 10년간 함께 훈련한 사형인 유연재(김광수 분)를 찾으러 속세로 내려온 인물이다.


2. 한국어는 주어 생략은 물론 어순도 중요하지 않다.

같은 의미의 문장을 다음과 같이 자유롭게 쓸수있습니다.

image


3번의 경우 주어까지 생략했지만 의미를 알아차릴 수 있습니다. 즉, 다음 단어를 예측하는 Language Model에게는 매우 혼란스러운 상황입니다.


3. 한국어는 교착어이다.

교착어실질적인 의미를 가지는 어간에 조사나, 어미와 같은 문법 형태소들이 결합하여 문법적인 기능이 부여되는 언어 를 말합니다. 가령, 한국어에는 영어에 없는 ‘은, 는, 이, 가, 를’ 등과 같은 조사 가 존재합니다. 예를 들어 한국어에 ‘그(he/him)’ 라는 주어나 목적어가 들어간 문장이 있다고 할 때 이 경우, ‘그’ 라는 단어 하나에도 ‘그가’, ‘그에게’, ‘그를’, ‘그와’, ‘그는’과 같이 다양한 조사가 ‘그’ 라는 글자 뒤에 띄어쓰기 없이 바로 붙게됩니다.

image


이 예시문을 띄어쓰기 단위로 토큰화를 할 경우에는 ‘사과가’, ‘사과에’, ‘사과를’, ‘사과’ 가 전부 다른 단어로 간주됩니다. 즉, 같은 단어임에도 서로 다른 조사가 붙어서 다른 단어로 인식 이 되면 자연어 처리가 힘들고 번거로워지는 경우가 많으므로 대부분의 한국어 NLP에서 조사는 분리해줄 필요가 있습니다.

띄어쓰기 단위가 영어처럼 독립적인 단어라면 띄어쓰기 단위로 토큰화를 하면 되겠지만 한국어는 어절이 독립적인 단어로 구성되는 것이 아니라, 조사 등의 무언가가 붙어있는 경우가 많아서 이를 전부 분리해줘야 한다는 의미 입니다.


한국어 토큰화 에서는 형태소(morpheme) 란 개념을 반드시 이해해야 합니다. 형태소(morpheme)뜻을 가진 가장 작은 말의 단위 를 말합니다. 형태소에는 두 가지 형태소가 있는데 자립 형태소의존 형태소 입니다.

  • 자립 형태소
    • 접사, 어미, 조사와 상관없이 자립하여 사용할 수 있는 형태소. 그 자체로 단어가 된다.
    • 체언(명사, 대명사, 수사), 수식언(관형사, 부사), 감탄사 등이 있다.
  • 의존 형태소
    • 다른 형태소와 결합하여 사용되는 형태소.
    • 접사, 어미, 조사, 어간을 말한다.


예를 들어 다음과 같은 문장에 띄어쓰기 단위 토큰화 를 수행한다면 다음과 같은 결과를 얻습니다.

  • 문장 : 에디가 책을 읽었다
  • 결과 : [‘에디가’, ‘책을’, ‘읽었다’]


하지만 이를 형태소 단위로 분해 하면 다음과 같습니다.

  • 자립 형태소 : 에디, 책
  • 의존 형태소 : -가, -을, 읽-, -었, -다


‘에디’라는 사람 이름과 ‘책’이라는 명사를 얻어낼 수 있습니다. 이를 통해 유추할 수 있는 것은 한국어 에서 영어에서의 단어 토큰화와 유사한 형태를 얻으려면 어절 토큰화가 아니라 형태소 토큰화를 수행해야 한다는 것입니다. 교착어인 한국어의 특성으로 인해 한국어는 토크나이저로 형태소 분석기를 사용하는 것이 보편적 입니다.

image


한국어 자연어 처리를 위해서는 KoNLPy(코엔엘파이) 라는 파이썬 패키지를 사용할 수 있습니다. 코엔엘파이를 통해서 사용할 수 있는 한국어 형태소 분석기로 Okt(Open Korea Text), 메캅(Mecab), 코모란(Komoran), 한나눔(Hannanum), 꼬꼬마(Kkma), Khaii, Soynlp 등이 있습니다. 다양한 형태소 분석기가 존재하므로 원하는 Task에 맞는 형태소 분석기를 선택하면됩니다.

image


한국어 Word Tokenization(KoNLPy)

1. 띄어쓰기를 기준으로 하는 단어 토큰화(가급적 하지마세요)

영어는 띄어쓰기 단위로 토큰화를 해도 단어들 간 구분이 꽤나 명확한 편이지만, 한국어의 경우에는 토큰화 작업이 훨씬 까다롭습니다. 그 이유는 한국어는 조사, 접사 등으로 인해 단순 띄어쓰기 단위로 나누면 같은 단어가 다른 단어로 인식되는 경우가 너무 너무 많기 때문 입니다. 한국어는 띄어쓰기로 토큰화하는 것은 명확한 실험 목적이 없다면 거의 쓰지 않는 것이 좋습니다. 예시를 통해서 이해해봅시다.

kor_text = "사과의 놀라운 효능이라는 글을 봤어. 그래서 오늘 사과를 먹으려고 했는데 사과가 썩어서 슈퍼에 가서 사과랑 오렌지 사왔어"
print(kor_text.split())
[output]
['사과의', '놀라운', '효능이라는', '글을', '봤어.', '그래서', '오늘', '사과를', '먹으려고', '했는데', '사과가', '썩어서', '슈퍼에', '가서', '사과랑', '오렌지', '사왔어']


위의 예제에서는 ‘사과’란 단어가 총 4번 등장했는데 모두 ‘의’, ‘를’, ‘가’, ‘랑’ 등이 붙어있어 이를 제거해주지 않으면 기계는 전부 다른 단어로 인식하게 됩니다.

print('사과' == '사과의')
print('사과의' == '사과를')
print('사과를' == '사과가')
print('사과가' == '사과랑')
[output]
False
False
False
False


2. 형태소 분석기 KoNLPy 설치

단어 토큰화를 위해서 영어에 NLTK 가 있다면 한국어에는 형태소 분석기 패키지KoNLPy(코엔엘파이) 가 존재합니다.

pip install konlpy


NLTK 도 내부적으로 여러 토크나이저가 있던 것처럼 KoNLPy 또한 다양한 형태소 분석기 를 가지고 있습니다. 또한 Mecab 이라는 형태소 분석기는 특이하게도 별도 설치를 해주어야 합니다.

# Colab에 Mecab 설치
!pip install konlpy
!pip install mecab-python
!bash <(curl -s https://raw.githubusercontent.com/konlpy/konlpy/master/scripts/mecab.sh)


from konlpy.tag import *

hannanum = Hannanum()
kkma = Kkma()
komoran = Komoran()
okt = Okt()
mecab = Mecab()


위 형태소 분석기들은 공통적으로 아래의 함수를 제공합니다.

  • nouns : 명사 추출
  • morphs : 형태소 추출
  • pos : 품사 부착


3. KoNLPy - 형태소 분석기 Okt

print('Okt 명사 추출 :', okt.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('Okt 형태소 분석 :', okt.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('Okt 품사 태깅 :', okt.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
[output]
Okt 명사 추출 : ['코딩', '당신', '연휴', '여행']
Okt 형태소 분석 : ['열심히', '코딩', '한', '당신', ',', '연휴', '에는', '여행', '을', '가봐요']
Okt 품사 태깅 : [('열심히', 'Adverb'), ('코딩', 'Noun'), ('한', 'Josa'), ('당신', 'Noun'), (',', 'Punctuation'), ('연휴', 'Noun'), ('에는', 'Josa'), ('여행', 'Noun'), ('을', 'Josa'), ('가봐요', 'Verb')]


4. KoNLPy - 형태소 분석기 꼬꼬마

print('kkma 명사 추출 :', kkma.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('kkma 형태소 분석 :', kkma.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('kkma 품사 태깅 :', kkma.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
[output]
kkma 명사 추출 : ['코딩', '당신', '연휴', '여행']
kkma 형태소 분석 : ['열심히', '코딩', '하', 'ㄴ', '당신', ',', '연휴', '에', '는', '여행', '을', '가보', '아요']
kkma 품사 태깅 : [('열심히', 'MAG'), ('코딩', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETD'), ('당신', 'NP'), (',', 'SP'), ('연휴', 'NNG'), ('에', 'JKM'), ('는', 'JX'), ('여행', 'NNG'), ('을', 'JKO'), ('가보', 'VV'), ('아요', 'EFN')]


5. KoNLPy - 형태소 분석기 코모란

print('komoran 명사 추출 :', komoran.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('komoran 형태소 분석 :', komoran.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('komoran 품사 태깅 :', komoran.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
[output]
komoran 명사 추출 : ['코', '당신', '연휴', '여행']
komoran 형태소 분석 : ['열심히', '코', '딩', '하', 'ㄴ', '당신', ',', '연휴', '에', '는', '여행', '을', '가', '아', '보', '아요']
komoran 품사 태깅 : [('열심히', 'MAG'), ('코', 'NNG'), ('딩', 'MAG'), ('하', 'XSV'), ('ㄴ', 'ETM'), ('당신', 'NNP'), (',', 'SP'), ('연휴', 'NNG'), ('에', 'JKB'), ('는', 'JX'), ('여행', 'NNG'), ('을', 'JKO'), ('가', 'VV'), ('아', 'EC'), ('보', 'VX'), ('아요', 'EC')]


6. KoNLPy - 형태소 분석기 한나눔

print('hannanum 명사 추출 :', hannanum.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('hannanum 형태소 분석 :', hannanum.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('hannanum 품사 태깅 :', hannanum.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
[output]
hannanum 명사 추출 : ['코딩', '당신', '연휴', '여행']
hannanum 형태소 분석 : ['열심히', '코딩', '하', 'ㄴ', '당신', ',', '연휴', '에는', '여행', '을', '가', '아', '보', '아']
hannanum 품사 태깅 : [('열심히', 'M'), ('코딩', 'N'), ('하', 'X'), ('ㄴ', 'E'), ('당신', 'N'), (',', 'S'), ('연휴', 'N'), ('에는', 'J'), ('여행', 'N'), ('을', 'J'), ('가', 'P'), ('아', 'E'), ('보', 'P'), ('아', 'E')]


7. KoNLPy - 형태소 분석기 Mecab

print('mecab 명사 추출 :', mecab.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('mecab 형태소 분석 :', mecab.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('mecab 품사 태깅 :', mecab.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
[output]
mecab 명사 추출 : ['코딩', '당신', '연휴', '여행']
mecab 형태소 분석 : ['열심히', '코딩', '한', '당신', ',', '연휴', '에', '는', '여행', '을', '가', '봐요']
mecab 품사 태깅 : [('열심히', 'MAG'), ('코딩', 'NNG'), ('한', 'XSA+ETM'), ('당신', 'NP'), (',', 'SC'), ('연휴', 'NNG'), ('에', 'JKB'), ('는', 'JX'), ('여행', 'NNG'), ('을', 'JKO'), ('가', 'VV'), ('봐요', 'EC+VX+EC')]


‘못’ 이라는 단어는 명사일때도있고, 부사일때도 있습니다.

print('mecab 명사 추출 :', mecab.nouns("망치로 못을 두드리다."))
print('mecab 명사 추출 :', mecab.nouns("나 그 일 못해요."))
[output]
mecab 명사 추출 : ['망치', '못']
mecab 명사 추출 : ['나', '일']


각 형태소 분석기는 성능과 결과가 다르게 나오기 때문에, 형태소 분석기의 선택은 사용하고자 하는 필요 용도에 어떤 형태소 분석기가 가장 적절한지를 판단하고 사용하면 됩니다. 예를 들어서 속도를 중시한다면 메캅을 사용할 수 있습니다.


문장 토큰화(Sentence Tokenization)

토큰의 단위가 문장(sentence) 인 문장 단위로 토큰화할 필요가 있는 경우가 있습니다. 보통 갖고있는 코퍼스가 정제되지 않은 상태라면, 코퍼스는 문장 단위로 구분되어 있지 않아서 이를 사용하고자 하는 용도에 맞게 문장 토큰화가 필요할 수 있습니다.

직관적으로 생각해봤을 때 온점(.) 이나 ‘!’ 나 ‘?’ 로 구분하면 되지 않을까?착각 을 할 수 있습니다.

image


! 나 ? 는 문장의 구분을 위한 꽤 명확한 구분자(boundary) 역할을 하지만 마침표는 그렇지 않기 때문 입니다. 마침표는 문장의 끝이 아니더라도 등장할 수 있습니다.

image

image


제대로 된 결과라면 Ph.D.는 분리되어서는 안되며, 문장 구분조차 보다 섬세한 규칙이 필요합니다.


영어 Sentence Tokenization(NLTK)

우선 문자열이 주어졌을 떄, 문장 단위로 나눠보겠습니다. 문자열.split(‘자르는 기준’) 을 사용하면 해당 기준으로 문자열들을 분리하여 리스트 형태로 반환합니다. 아래의 코드는 온점을 기준으로 문자열을 자르는 코드입니다.

temp = 'Yonsei University is a private research university in Seoul, South Korea. Yonsei University is deemed as one of the three most prestigious institutions in the country. It is particularly respected in the studies of medicine and business administration.'
temp.split('. ')
[output]
['Yonsei University is a private research university in Seoul, South Korea',
 'Yonsei University is deemed as one of the three most prestigious institutions in the country',
 'It is particularly respected in the studies of medicine and business administration.']


직관적으로 생각해봤을 때는 ?나 온점(.)이나 ! 기준으로 문장을 잘라내면 되지 않을까라고 생각할 수 있지만, 꼭 그렇지만은 않습니다. !나 ?는 문장의 구분을 위한 꽤 명확한 구분자(boundary) 역할을 하지만 온점은 꼭 그렇지 않기 때문입니다. 다시 말해, 온점은 문장의 끝이 아니더라도 등장할 수 있습니다. 온점을 기준으로 문장을 구분할 경우에는 예외사항이 너무 많습니다.

NLTK 에서는 영어 문장의 토큰화를 수행하는 sent_tokenize 를 지원하고 있습니다.

text = "His barber kept his word. But keeping such a huge secret to himself was driving him crazy. Finally, the barber went up a mountain and almost to the edge of a cliff. He dug a hole in the midst of some reeds. He looked about, to mae sure no one was near."
from nltk.tokenize import sent_tokenize
print(sent_tokenize(text))
[output]
['His barber kept his word.', 'But keeping such a huge secret to himself was driving him crazy.', 'Finally, the barber went up a mountain and almost to the edge of a cliff.', 'He dug a hole in the midst of some reeds.', 'He looked about, to mae sure no one was near.']


text="I am actively looking for Ph.D. students. and you are a Ph.D student."
print(sent_tokenize(text))
[output]
['I am actively looking for Ph.D. students.', 'and you are a Ph.D student.']


한국어 Sentence Tokenization(KSS)

한국어 문장 토크나이저 라이브러리로는 대표적으로 KSSkiwi 가 있습니다.

pip install kss
import kss

text = '딥 러닝 자연어 처리가 재미있기는 합니다. 그런데 문제는 영어보다 한국어로 할 때 너무 어려워요. 이제 해보면 알걸요?'
print(kss.split_sentences(text))
[output]
['딥 러닝 자연어 처리가 재미있기는 합니다.', '그런데 문제는 영어보다 한국어로 할 때 너무 어려워요.', '이제 해보면 알걸요?']

Read more

Text Analytics을 위한 텍스트 전처리 개요

|

텍스트 분석(Text Analytics)

NLP(National Language Processing)텍스트 분석(Text Analytics, TA) 을 구분하는 것이 큰 의미는 없어 보이지만, 굳이 구분하자면 NLP머신이 인간의 언어를 이해하고 해석하는 데 더 중점 을 두며, 텍스트 마이닝(Text Mining)이라고도 불리는 텍스트 분석비정형 텍스트에서 의미 있는 정보를 추출하는 것에 좀 더 중점 을 두고 있습니다.

텍스트 분석 은 머신러닝, 언어 이해, 통계 등을 활용해 모델을 수립하고 정보를 추출해 비즈니스 인텔리전스(Business Intelligence)나 예측 분석 등의 분석 작업을 주로 수행 합니다. 주로 다음과 같은 기술 영역에 집중합니다.

  • 텍스트 분류(Text Classification)
    • 문서가 특정 분류 또는 카테고리에 속하는 것을 예측하는 기법을 통칭합니다.
    • 예를 들어 특정 신문 기사 내용이 연예/정치/사회/문화 중 어떤 카테고리에 속하는지 자동으로 분류하거나 스팸 메일 검출 같은 프로그램이 이에 속합니다.
    • 지도학습을 적용합니다.
  • 감성 분석(Sentiment Analysis)
    • 텍스트에서 나타나는 감정/판단/믿음/의견/기분 등의 주관적인 요소를 분석하는 기법을 총칭합니다.
    • 소셜 미디어 감정 분석, 영화나 제품에 대한 긍정 또는 리뷰, 여론조사 의견 분석 등의 다양한 영역에서 활용됩니다.
    • 지도학습 방법뿐만 아니라 비지도학습을 이용해 적용할 수 있습니다.
  • 텍스트 요약(Summarization)
    • 텍스트 내에서 중요한 주제나 중심 사상을 추출하는 기법을 말합니다.
    • 대표적으로 토픽 모델링(Topic Modeling)이 있습니다.
  • 텍스트 군집화(Clustering)와 유사도 측정
    • 비슷한 유형의 문서에 대해 군집화를 수행하는 기법을 말합니다.
    • 텍스트 분류를 비지도학습으로 수행하는 방법의 일환으로 사용될 수 있습니다.
    • 유사도 측정 역시 문서들간의 유사도를 측정해 비슷한 문서끼리 모을 수 있는 방법입니다.


텍스트 분석 이해

텍스트 분석은 비정형 데이터인 텍스트를 분석하는 것입니다. 머신러닝 알고리즘은 숫자형의 피처 기반 데이터만 입력받을 수 있기 때문에 텍스트를 머신러닝에 적용하기 위해서는 비정형 텍스트 데이터를 어떻게 피처 형태로 추출하고 추출된 피처에 의미 있는 값을 부여하는가 하는 것이 매우 중요한 요소 이며, 이렇게 텍스트를 변환하는 것을 피처 벡터화(Feature Vectorization) 또는 피처 추출(Feature Extraction) 이라고 합니다. 텍스트를 벡터값을 가지는 피처로 변환하는 것은 머신러닝 모델을 적용하기 전에 수행해야 할 매우 중요한 요소 입니다.


텍스트 분석 수행 프로세스

머신러닝 기반의 텍스트 분석 프로세스 는 다음과 같은 프로세스 순으로 수행합니다.

  1. 텍스트 사전 준비작업(텍스트 전처리)
    • 텍스트를 피처로 만들기 전에 미리 클렌징, 대/소문자 변경, 특수문자 삭제등의 클렌징 작업, 단어(Word) 등의 토큰화 작업, 의미 없는 단어(Stop word) 제거 작업, 어근 추출(Stemming/Lemmatization) 등의 텍스트 정규화 작업을 수행하는 것을 통칭합니다.
  2. 피처 벡터화/추출
    • 사전 준비 작업으로 가공된 텍스트에서 피처를 추출하고 여기에 벡터 값을 할당합니다.
    • 대표적인 방법은 BOW와 Word2Vec이 있으며, BOW는 대표적으로 Count 기반과 TF-IDF 기반 벡터화가 있습니다.
  3. ML 모델 수립 및 학습/예측/평가
    • 피처 벡터화된 데이터 세트에 ML 모델을 적용해 학습/예측 및 평가를 수행합니다.


image


텍스트 사전 준비 작업(텍스트 전처리) - 텍스트 정규화

텍스트 자체를 바로 피처로 만들 수는 없습니다. 이를 위해 사전에 텍스트를 가공하는 준비 작업이 필요 합니다. 텍스트 정규화는 텍스트를 머신러닝 알고리즘이나 NLP 애플리케이션에 입력 데이터로 사용하기 위해 클렌징, 정제, 토큰화, 어근화 등의 다양한 텍스트 데이터의 사전 작업을 수행하는 것을 의미합니다. 텍스트 분석은 이러한 텍스트 정규화 작업이 매우 중요 하며, 이러한 텍스트 정규화 작업은 크게 다음과 같이 분류할 수 있습니다.

  • 클렌징(Cleansing)
  • 토큰화(Tokenization)
  • 필터링/스톱 워드 제거/철자 수정
  • Stemming
  • Lemmatization


텍스트 정규화의 주요 작업을 NLTK 패키지를 이용해 실습해 보겠습니다.


1. 클렌징(Cleansing)

텍스트에서 분석에 오히려 방해가 되는 불필요한 문자, 기호 등을 사전에 제거하는 작업 입니다. 예를들어 HTML, XML 태그나 특정 기호 등을 사전에 제거합니다.


2. 텍스트 토큰화(Tokenization)

토큰화의 유형은 문서에서 문장을 분리하는 문장 토큰화 와 문장에서 단어를 토큰으로 분리하는 단어 토큰화 로 나눌 수 있습니다.


문장 토큰화(sentence tokenization)

문장 토큰화는 문장의 마침표(.), 개행문자(\n) 등 문장의 마지막을 뜻하는 기호에 따라 분리하는 것이 일반적 입니다. 또한 정규 표현식에 따른 문장 토큰화도 가능합니다. NTLK 에서 일반적으로 많이 쓰이는 sent_tokenize 를 이용해 토큰화를 수행해 보겠습니다. 다음은 3개의 문장으로 이루어진 텍스트 문서를 문장으로 각각 분리하는 예제 입니다. nltk.download(‘punkt’) 는 마침표, 개행 문자등의 데이터 세트를 다운로드합니다.

from nltk import sent_tokenize
import nltk
nltk.download('punkt')

text_sample = 'The Matrix is everywhere its all around us, here even in this room. \
               You can see it out your window or on your television. \
               You feel it when you go to work, or go to church or pay your taxes.'

sentences = sent_tokenize(text=text_sample)
print(type(sentences), len(sentences))
print(sentences)
[output]
<class 'list'> 3
['The Matrix is everywhere its all around us, here even in this room.', 'You can see it out your window or on your television.', 'You feel it when you go to work, or go to church or pay your taxes.']


sent_tokenize() 가 반환하는 것은 각각의 문장으로 구성된 list 객체로, 3개의 문장으로 된 문자열을 가지고 있는 것을 알 수 있습니다.


단어 토큰화(Word Tokenization)

단어 토큰화는 문장을 단어로 토큰화하는 것 입니다. 기본적으로 공백, 콤마(,), 마침표(.), 개행문자 등으로 단어를 분리하지만, 정규표현식을 이용해 다양한 유형으로 토큰화를 수행할 수 있습니다.

일반적으로 문장 토큰화는 각 문장이 가지는 시맨틱적인 의미가 중요한 요소로 사용될 때 사용 합니다. Bag of Word 와 같이 단어의 순서가 중요하지 않은 경우 문장 토큰화를 사용하지 않고 단어 토큰화만 사용해도 충분합니다. NTLK 에서 기본으로 제공하는 word_tokenize() 를 이용해 단어로 토큰화해 보겠습니다.

from nltk import word_tokenize

sentence = "The Matrix is everywhere its all around us, here even in this room."
words = word_tokenize(sentence)
print(type(words), len(words))
print(words)
[output]
<class 'list'> 15
['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', ',', 'here', 'even', 'in', 'this', 'room', '.']


이번에는 sent_tokenizeword_tokenize 를 조합해 문서에 대해서 모든 단어를 토큰화해 보겠습니다. 이전 예제에서 선언된 3개의 문장으로 된 text_sample 을 문장별로 단어 토큰화를 적용합니다. 이를 위해 문서를 먼저 문장으로 나누고, 개별 문장을 다시 단어로 토큰화하는 tokenize_text() 함수를 생성하겠습니다.

from nltk import word_tokenize, sent_tokenize

#여러개의 문장으로 된 입력 데이터를 문장별로 단어 토큰화 만드는 함수 생성
def tokenize_text(text):
    
    # 문장별로 분리 토큰
    sentences = sent_tokenize(text)
    
    # 분리된 문장별 단어 토큰화
    word_tokens = [word_tokenize(sentence) for sentence in sentences]
    return word_tokens

#여러 문장들에 대해 문장별 단어 토큰화 수행. 
word_tokens = tokenize_text(text_sample)
print(type(word_tokens), len(word_tokens))
print(word_tokens)
[output]
<class 'list'> 3
[['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', ',', 'here', 'even', 'in', 'this', 'room', '.'], ['You', 'can', 'see', 'it', 'out', 'your', 'window', 'or', 'on', 'your', 'television', '.'], ['You', 'feel', 'it', 'when', 'you', 'go', 'to', 'work', ',', 'or', 'go', 'to', 'church', 'or', 'pay', 'your', 'taxes', '.']]


3. 스톱 워드(Stop word) 제거

스톱 워드(Stop word)분석에 큰 의미가 없는 단어를 지칭 합니다. 가령 영어에서 is, the, a, will 등 문장을 구성하는 필수 문법 요소지만 문맥적으로 큰 의미가 없는 단어가 이에 해당하는데, 이 단어의 경우 문법적인 특성으로 인해 특히 빈번하게 텍스트에 나타나므로 이것들을 사전에 제거하지 않으면 그 빈번함으로 인해 오히려 중요한 단어로 인지될 수 있습니다. 따라서 이 의미 없는 단어를 제거하는것이 중요한 전처리 작업 입니다.

NLTK 의 경우 다양한 언어의 스톱 워드를 제공합니다. NTLK 의 스톱 워드에는 어떤 것이 있는지 확인해 보겠습니다. 이를 위해 먼저 NLTKstopwords 목록을 내려받고, 다운로드가 완료되고 나면 NTLKEnglish 의 경우 몇 개의 stopwords 가 있는지 알아보고 그중 20개만 확인해 보겠습니다.

import nltk
nltk.download('stopwords')

print('영어 stop words 갯수:', len(nltk.corpus.stopwords.words('english')))
print(nltk.corpus.stopwords.words('english')[:20])
[output]
영어 stop words 갯수: 179
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his']


영어의 경우 스톱 워드의 개수가 179개이며, 그중 20개만 살펴보면 위의 결과와 같습니다. 위 예제에서 3개의 문장별로 단어를 토큰화해 생성된 word_tokens 리스트에 대해서 stopwords를 필터링으로 제거해 분석을 위한 의미 있는 단어만 추출해 보겠습니다.

import nltk

stopwords = nltk.corpus.stopwords.words('english')
all_tokens = []

# 위 예제의 3개의 문장별로 얻은 word_tokens list 에 대해 stop word 제거 Loop
for sentence in word_tokens:
    filtered_words=[]
    
    # 개별 문장별로 tokenize된 sentence list에 대해 stop word 제거 Loop
    for word in sentence:
        #소문자로 모두 변환합니다. 
        word = word.lower()
        
        # tokenize 된 개별 word가 stop words 들의 단어에 포함되지 않으면 word_tokens에 추가
        if word not in stopwords:
            filtered_words.append(word)

    all_tokens.append(filtered_words)
    
print(all_tokens)
[output]
[['matrix', 'everywhere', 'around', 'us', ',', 'even', 'room', '.'], ['see', 'window', 'television', '.'], ['feel', 'go', 'work', ',', 'go', 'church', 'pay', 'taxes', '.']]


is, this 와 같은 스톱 워드가 필터링을 통해 제거됐음을 알 수 있습니다.


4. Stemming과 Lemmatization

많은 언어에서 문법적인 요소에 따라 단어가 다양하게 변합니다. 가령 work 는 동사 원형인 단어지만, 과거형은 worked, 3인칭 단수일 때 works, 진행형인 경우 working 등 다양하게 달라집니다. StemmingLemmatization문법적 또는 의미적으로 변화하는 단어의 원형을 찾는 것 입니다.

  • Stemming
    • 원형 단어로 변환 시 일반적인 방법을 적용하거나 더 단순화된 방법을 적용해 원래 단어에서 일부 철자가 훼손된 어근 단어를 추출하는 경향이 있습니다.
  • Lemmatization
    • 품사와 같은 문법적인 요소와 더 의미적인 부분을 감안해 정확한 철자로 된 어근 단어를 찾아줍니다.
    • Stemming과 Lemmatization은 원형 단어를 찾는다는 목적은 유사하지만, Lemmatization이 Stemming보다 정교하며 의미론적인 기반에서 단어의 원형을 찾습니다.


먼저 NLTKLancasterStemmer 를 이용해 Stemmer 부터 살펴보겠습니다. 진행형, 3인칭 단수, 과거형에 따른 동사, 그리고 비교, 최상에 따른 형용사의 변화에 따라 Stemming 은 더 단순하게 원형 단어를 찾아줍니다. NTLK 에서는 LancasterStemmer() 와 같이 필요한 Stemmer 객체를 생성한 뒤 이 객체의 stem(‘단어’) 메서드를 호출하면 원하는 '단어'의 Stemming이 가능 합니다.

from nltk.stem import LancasterStemmer
stemmer = LancasterStemmer()

print(stemmer.stem('working'), stemmer.stem('works'), stemmer.stem('worked'))
print(stemmer.stem('amusing'), stemmer.stem('amuses'), stemmer.stem('amused'))
print(stemmer.stem('happier'), stemmer.stem('happiest'))
print(stemmer.stem('fancier'), stemmer.stem('fanciest'))
[output]
work work work
amus amus amus
happy happiest
fant fanciest


work 의 경우 진행형(working), 3인칭 단수(works), 과거형(worked) 모두 기본 단어인 working, s, ed 가 붙는 단순한 변화이므로 원형 단어로 work 를 제대로 인식합니다. 하지만 amuse 의 경우, 각 변화가 amuse 가 아닌 amusing, s, ed 가 붙으므로 정확한 단어인 amuse 가 아닌 amus 를 원형 단어로 인식합니다. 형용사인 happy, fancy 의 경우도 비교형, 최상급형으로 변형된 단어의 정확한 원형을 찾지 못하고 원형 단어에서 철자가 다른 어근 단어로 인식하는 경우가 발생합니다.

이번에는 WordNetLemmatizer 를 이용해 Lemmatization 을 수행해 보겠습니다. 일반적으로 Lemmatization 은 보다 정확한 원형 단어 추출을 위해 단어의 '품사'를 입력해줘야 합니다. 다음 예제에서 볼 수 있듯이 lemmatize() 의 파라미터로 동사의 경우 ‘v’, 형용사의 경우 ‘a’ 를 입력합니다.

from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')

lemma = WordNetLemmatizer()
print(lemma.lemmatize('amusing','v'), lemma.lemmatize('amuses','v'), lemma.lemmatize('amused','v'))
print(lemma.lemmatize('happier','a'), lemma.lemmatize('happiest','a'))
print(lemma.lemmatize('fancier','a'), lemma.lemmatize('fanciest','a'))
[output]
amuse amuse amuse
happy happy
fancy fancy


앞의 Stemmer 보다 정확하게 원형 단어를 추출해줌을 알 수 있습니다.

Read more

OpenCV geometric transform 이동, 확대/축소, 회전

|

해당 게시물은 파이썬으로 만드는 OpenCV 프로젝트(이세우 저) 를 바탕으로 작성되었습니다.


이동, 확대/축소, 회전

기하학적 변환(geometric transform)영상의 좌표에 기하학적인 연산을 가해서 변환된 새로운 좌표를 얻는 것입니다. 영상에 기하학적 변환을 하면 이동, 확대, 축소, 회전 등 일상생활에서 흔히 접하는 변환에서부터 볼록 거울에 비친 모습이나 일렁이는 물결에 비친 모습과 같은 여러 가지 왜곡된 모양으로도 변환할 수 있습니다.

영상의 기하학적 변환은 기존의 영상을 원하는 모양이나 방향 등으로 변환하기 위해 각 픽셀을 새로운 위치로 옮기는 것이 작업의 대부분입니다. 그러기 위해서는 각 픽셀의 $x$, $y$ 좌표에 대해 옮기고자 하는 새로운 좌표 $x’$, $y’$ 을 구하는 연산이 필요합니다. 그러려면 픽셀 전체를 순회하면서 각 좌표에 대해 연산식을 적용해서 새로운 좌표를 구해야 하는데, 이때 사용할 연산식을 가장 효과적으로 표현하는 방법이 행렬식입니다.

1. 이동

2차원 공간에서 물체를 다른 곳으로 이동시키려면 원래 있던 좌표에 이동시키려는 거리만큼 더해서 이동할 새로운 좌표를 구하면 됩니다.

image

위 그림은 물고기 그림을 오른쪽 위로 이동하는 모습을 표현하고 있습니다. 이 그림에서 물고기의 어떤 점 $p(x, y)$를 $d_x$ 와 $d_y$ 만큼 옮기면 새로운 위치의 좌표 $p(x’, y’)$ 을 구할 수 있습니다. 이것을 수식으로 작성하면 아래와 같습니다.

[x’ = x + d_x y’ = y + d_y]

위 방정식을 행렬식으로 바꾸어 표현하면 아래와 같습니다.

[\begin{bmatrix} x’
y’ \end{bmatrix}= \begin{bmatrix} 1 & 0 & d_x
0 & 1 & d_y \end{bmatrix} \begin{bmatrix} x
y
1 \end{bmatrix}]

위의 행렬식을 아래와 같이 풀어서 표현하면 원래의 방정식과 같다는 것을 알 수 있습니다.

[\begin{bmatrix} x’
y’ \end{bmatrix}= \begin{bmatrix} x + d_x
y + d_y \end{bmatrix} \begin{bmatrix} 1x + 0y + 1d_x
0x + 1y + 1d_y \end{bmatrix}]

여기서 굳이 행렬식을 언급하는 이유는, 좌표를 변환하는 과정은 OpenCV가 알아서 해주지만 어떻게 변환할 것인지는 개발자가 표현해야 하는데, 변환할 방정식을 함수에 전달할 때 행렬식이 표현하기 훨씬 더 적절하기 때문입니다. 행렬식 중에서도 $x$, $y$ 는 이미 원본 이미지의 좌표 값으로 제공되므로 $2 \times 3$ 변환행렬만 전달하면 연산이 가능합니다. OpenCV는 $2 \times 3$ 행렬로 영상의 좌표를 변환시켜 주는 함수를 다음과 같이 제공합니다.

  • dst = cv2.warpAffine(src, mtrx, dsize [, dst, flags, borderMode, borderValue])
    • src : 원본 영상, NumPy 배열
    • mtrx : 2 × 3 변환행렬, NumPy 배열, dtype = float32
    • dsize : 결과 이미지 크기, tuple(width, height)
    • flags : 보간법 알고리즘 선택 플래그
      • cv2.INTER_LINEAR : 기본 값, 인접한 4개 픽셀 값에 거리 가중치 사용
      • cv2.INTER_NEAREST : 가장 가까운 픽셀 값 사용
      • cv2.INTER_AREA : 픽셀 영역 관계를 이용한 재샘플링
      • cv2.INTER_CUBIC : 인접한 16개 픽셀 값에 거리 가중치 사용
      • cv2.INTER_LANCZOS4 : 인접한 8개 픽셀을 이용한 란초의 알고리즘
    • borderMode : 외곽 영역 보정 플래그
      • cv2.BORDER_CONSTANT : 고정 색상 값(999 | 12345 | 999)
      • cv2.BORDER_REPLICATE : 가장 자리 복제 (111 | 12345 | 555)
      • cv2.BORDER_WRAP : 반복(345 | 12345 | 123)
      • cv2.BORDER_REFLECT : 반사(321 | 12345 | 543)
    • borderValue : cv2.BORDER_CONSTANT 의 경우 사용할 색상 값(기본값 = 0)
    • dst : 결과 이미지, NumPy 배열

cv2.warpAffine() 함수는 src 영상을 mtrx 행렬에 따라 변환해서 dsize 크기로 만들어서 반환합니다. 그뿐만 아니라 변환에 대부분 나타나는 픽셀 탈락 현상을 보정해주는 보간법 알고리즘과 경계 부분의 보정 방법도 선택할 수 있습니다. 다음 예제는 cv2.warpAffine() 함수와 변환행렬을 이용해서 영상을 이동 변환하는 예제입니다.

'''평행 이동'''
import cv2
import numpy as np

img = cv2.imread('./img/fish.jpg')
rows, cols = img.shape[0:2] # 영상의 크기

dx, dy = 100, 50            # 이동할 픽셀 거리

# ---① 변환 행렬 생성 
mtrx = np.float32([[1, 0, dx],
                   [0, 1, dy]])  
# ---② 단순 이동
dst = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy))   

# ---③ 탈락된 외곽 픽셀을 파랑색으로 보정
dst2 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                        cv2.INTER_LINEAR, cv2.BORDER_CONSTANT, (255,0,0) )

# ---④ 탈락된 외곽 픽셀을 원본을 반사 시켜서 보정
dst3 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                                cv2.INTER_LINEAR, cv2.BORDER_REFLECT)

cv2.imshow('original', img)
cv2.imshow('trans', dst)
cv2.imshow('BORDER_CONSTATNT', dst2)
cv2.imshow('BORDER_FEFLECT', dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

image

image

위 코드는 물고기 그림을 가로(x) 방향으로 100픽셀, 세로(y) 방향으로 50픽셀을 이동시키는 예제입니다. 코드 ①에서는 앞서 설명한 형식으로 변환행렬을 생성하고, 코드 ②에서는 cv2.warpAffine() 함수로 영상을 이동하게 만들었습니다. 이때 출력 영상의 크기를 원래의 크기보다 이동한 만큼 더 크게 지정해서 그림이 잘리지 않게 했는데, 영상의 좌측과 윗부분은 원래 없던 픽셀이 추가돼서 외곽 영역이 검게 표현됩니다. 코드 ⑧은 이 외곽 영역을 고정 값 파란색(255, 0, 0)으로 보정했으며, 코드 ④에서는 원본 영상을 거울에 비친 것처럼 복제해서 보정했습니다.

영상 이동에는 외곽 영역 이외에는 픽셀의 탈락이 발생하지 않으므로 이 예제에서 보간법 알고리즘을 선택하는 네 번째 인자는 의미가 없습니다.


2. 확대/축소

image

영상을 확대 또는 축소하려면 원래 있던 좌표에 원하는 비율만큼 곱해서 새로운 좌표를 구할 수 있습니다. 이때 확대/축소 비율을 가로와 세로 방향으로 각각 $\alpha$ 와 $\beta$ 라고 하면 변환행렬은 아래와 같습니다.

[\begin{bmatrix} x’
y’ \end{bmatrix}= \begin{bmatrix} \alpha & 0 & 0
0 & \beta & 0 \end{bmatrix} \begin{bmatrix} x
y
1 \end{bmatrix}]

확대 혹은 축소를 하려면 $2 \times 2$ 행렬로도 충분히 표현이 가능한데 굳이 마지막 열에 0으로 채워진 열을 추가해서 $2 \times 3$ 행렬로 표현한 이유는 cv2.warpAffine() 함수와 이동 변환 때문입니다. 앞서 다룬 이동을 위한 행렬식은 $2 \times 3$ 행렬로 표현해야 하므로 여러 가지 기하학적 변환을 지원해야 하는 cv2.warpAffine() 함수는 $2 \times 3$ 행렬이 아니면 오류를 발생합니다. 행렬의 마지막 열에 $d_x$, $d_y$에 해당하는 값을 지정하면 확대와 축소뿐만 아니라 이동도 가능합니다.

'''행렬을 이용한 확대와 축소'''
import cv2
import numpy as np

img = cv2.imread('./img/fish.jpg')
height, width = img.shape[:2]

# --① 0.5배 축소 변환 행렬
m_small = np.float32([[0.5, 0, 0],
                      [0, 0.5, 0]])  
# --② 2배 확대 변환 행렬
m_big = np.float32([[2, 0, 0],
                    [0, 2, 0]])  

# --③ 보간법 적용 없이 확대 축소
dst1 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)))
dst2 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)))

# --④ 보간법 적용한 확대 축소
dst3 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)), \
                        None, cv2.INTER_AREA)
dst4 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)), \
                        None, cv2.INTER_CUBIC)

# 결과 출력
cv2.imshow("original", img)
cv2.imshow("small", dst1)
cv2.imshow("big", dst2)
cv2.imshow("small INTER_AREA", dst3)
cv2.imshow("big INTER_CUBIC", dst4)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

image

image

image

위 코드는 변환행렬을 이용해서 0.5배 축소와 2배 확대를 하는 예제입니다. 코드①과 ②에서 각각 축소와 확대에 필요한 변환행렬을 생성한 다음, 코드 ③에서는 보간법 알고리즘을 따로 지정하지 않았고, 코드 ④에서는 보간법 알고리즘을 따로 지정했습니다. 보간법 알고리즘으로는 축소에는 cv2.INTER_AREA 가 효과적이고, 확대에는 cv2.INTER_CUBICcv2.INTER_LINEAR 가 효과적인 것으로 알려져 있습니다.

OpenCV는 변환행렬을 작성하지 않고도 확대와 축소 기능을 사용할 수 있게 cv2.resize() 함수를 별도로 제공합니다.

  • dst = cv2.resize(src, dsize, dst, fx, fy, interpolation)
    • src : 입력 영상, NumPy 배열
    • dsize : 출력 영상 크기(확대/축소 목표 크기), 생략하면 fx, fy를 적용
      • (width, height)
    • fx, fy : 크기 배율, 생략하면 dsize를 적용
    • interpolation : 보간법 알고리즘 선택 플래그(cv2.warpAffine()과 동일)
    • dst : 결과 영상, NumPy 배열

cv2.resize() 함수는 확대 혹은 축소할 때 몇 픽셀로 할지 아니면 몇 퍼센트로 할지 선택할 수 있습니다. dsize로 변경하고 싶은 픽셀 크기를 직접 지정하거나 fx와 fy로 변경할 배율을 지정할 수 있습니다. 만약 dsize와 fx, fy 모두 값을 전달하면 dsize만 적용합니다.

'''cv2.resize()로 확대와 축소'''
import cv2
import numpy as np

img = cv2.imread('./img/fish.jpg')
height, width = img.shape[:2]

#--① 크기 지정으로 축소
#dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)),\
#                        None, 0, 0, cv2.INTER_AREA)
dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)), \
                         interpolation=cv2.INTER_AREA)

#--② 배율 지정으로 확대
dst2 = cv2.resize(img, None,  None, 2, 2, cv2.INTER_CUBIC)

#--③ 결과 출력
cv2.imshow("original", img)
cv2.imshow("small", dst1)
cv2.imshow("big", dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

image

코드 ①에서는 원본 크기의 0.5배를 곱한 후 결과 크기를 구해서 전달하고 있으며, 배율은 None으로 처리했습니다. 반대로, 코드 ②에서는 크기 인자를 None으로 처리했고 배율을 각각 두 배로 전달합니다. cv2.resize() 함수가 변환행렬을 이용하는 코드보다 사용하기 쉽고 간결한 것을 알 수 있습니다.


3. 회전

영상을 회전하려면 삼각함수를 써야 합니다.

image

위 그림에서 $p$ 라는 점을 원을 따라 $p’$ 으로 옮기는 것회전이라고 하고, 그러기 위해서는 당연히 새로운 점 $p’$ 의 좌표 $x’$, $y’$ 을 구해야 합니다. 좌표를 구하기 전에 미리 정리해 둘 것이 있는데, 원의 반지름은 원 어디서나 동일하므로 원점 $O$ 와 $p$ 의 거리는 원점 $O$ 와 $p’$ 의 거리와 같고 그 값이 바로 원래 있던 점 $p$ 의 $x$ 좌표라는 것입니다.

이제 새로운 점 $p’$ 의 $x’$ 좌표를 구하기 위해 원 안에 가상의 직각삼각형을 그려보면 $\theta$ 각에 따라 변 $\overline{Op’}$ 와 변 $\overline{Ox’}$ 의 비율은 $cos\theta$ 임을 알 수 있습니다. 같은 방법으로 좌표 $y’$ 는 원 안의 직각삼각형의 변 $\overline{p’x’}$ 와 같으므로 변 $\overline{Op’}$ 와의 비율을 나타내는 $sin\theta$ 임을 알 수 있습니다. 변 $\overline{Op’}$ 는 원래의 좌표 $x$ 와 같으므로 새로운 점의 좌표는 $p’(x cos\theta, x sin\theta)$ 입니다.

image

회전은 원을 중심으로 진행되므로 위의 경우도 따져봐야 합니다. 이 경우도 원래의 점 $p$ 에서 원을 따라 회전한 $p’$ 의 좌표 $x’$, $y’$ 를 구해야 하는데, 이것도 이전과 같이 원 안의 직각삼각형으로 설명할 수 있습니다. 결국 새로운 점의 좌표는 $p’( -y sin\theta, y cos\theta)$ 입니다.

image

위 그림은 위 두 경우의 수가 모두 반영된 모습을 보여주고 있으며, 이것을 행렬식으로 표현하면 다음과 같습니다.

[\begin{bmatrix} x’
y’ \end{bmatrix}= \begin{bmatrix} cos\theta & -sin\theta & 0
sin\theta & cos\theta & 0 \end{bmatrix} \begin{bmatrix} x
y
1 \end{bmatrix}]

'''변환행렬로 회전'''
import cv2
import numpy as np

img = cv2.imread('./img/fish.jpg')
rows, cols = img.shape[0:2]

# ---① 라디안 각도 계산(60진법을 호도법으로 변경)
d45 = 45.0 * np.pi / 180    # 45도
d90 = 90.0 * np.pi / 180    # 90도

# ---② 회전을 위한 변환 행렬 생성
m45 = np.float32( [[ np.cos(d45), -1* np.sin(d45), rows//2],
                    [np.sin(d45), np.cos(d45), -1*cols//4]])
m90 = np.float32( [[ np.cos(d90), -1* np.sin(d90), rows],
                    [np.sin(d90), np.cos(d90), 0]])

# ---③ 회전 변환 행렬 적용
r45 = cv2.warpAffine(img, m45, (cols,rows))
r90 = cv2.warpAffine(img, m90, (rows,cols))

# ---④ 결과 출력
cv2.imshow("origin", img)
cv2.imshow("45", r45)
cv2.imshow("90", r90)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

image

코드 ①은 변환행렬에 사용할 회전 각을 60진법에서 라디안(radian)으로 변경합니다. 코드 ②에서 변환행렬을 생성하는데, 삼각함수는 NumPy의 np.cos(), np.sin() 함수를 사용했습니다. 변환행렬의 마지막 열에 0이 아닌 rows//2, -1*cols//4, rows 를 사용한 이유는 영상의 회전 기준 축이 좌측 상단이 되므로 회전한 영상은 보여지는 영역 바깥으로 벗어나게 돼서 좌표를 가운데로 옮기기 위한 것으로 회전 축을 지정하는 효과와 같습니다. 변환행렬의 마지막 열을 이동에 사용한다는 내용은 앞서 다루었습니다.

회전을 위한 변환행렬 생성은 다소 까다로운 데다가 회전 축까지 반영하려면 일이 조금 복잡해집니다. OpenCV는 개발자가 복잡한 계산을 하지 않고도 변환행렬을 생성할 수 있게 아래와 같은 함수를 제공합니다.

  • mtrx = cv2.getRotationMatrix2D(center, angle, scale)
    • center : 회전 축 중심 좌표, 튜플(x, y)
    • angle : 회전 각도, 60진법
    • scale : 확대/축소 배율

이 함수를 쓰면 중심축 지정과 확대/축소까지 반영해서 손쉽게 변환행렬을 얻을 수있습니다.

'''회전 변환행렬 구하기'''
import cv2

img = cv2.imread('./img/fish.jpg')
rows,cols = img.shape[0:2]

#---① 회전을 위한 변환 행렬 구하기
# 회전축:중앙, 각도:45, 배율:0.5
m45 = cv2.getRotationMatrix2D((cols/2,rows/2), 45, 0.5) 
# 회전축:중앙, 각도:90, 배율:1.5
m90 = cv2.getRotationMatrix2D((cols/2,rows/2), 90, 1.5) 

#---② 변환 행렬 적용
img45 = cv2.warpAffine(img, m45,(cols, rows))
img90 = cv2.warpAffine(img, m90,(cols, rows))

#---③ 결과 출력
cv2.imshow('origin',img)
cv2.imshow("45", img45)
cv2.imshow("90", img90)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

image

Read more

M1 MacOS iTerm2 설치 및 꾸미기

|

image

MacOS 기본 터미널을 열어보면 위 사진과 같이 코드 하이라이팅도 전혀 되지 않고 밋밋한 기본 검정색 테마라서 가독성이 떨어지는 아쉬운 점이 있습니다. 명령어를 입력해도 전혀 티가 나지않아서 생산성이 좀 떨어지는 것 같습니다.

image

iTerm2를 설치하여 기본 터미널을 꾸며보겠습니다. iTerm2는 MacOS에서 공식 터미널 어플리캐이션 대신에 사용할 수 있는 가상 터미널 어플리케이션입니다. 기존 터미널에서 제공하는 기능보다 더욱 편리하고 가독성을 높여주는 터미널이라고 생각하시면 될 것 같습니다.


✨ 참고 사이트

1️⃣ Homebrew - macOS 패키지 관리자 https://brew.sh/index_ko

2️⃣ iTerm2 - mac용 가상 터미널 애플리케이션 https://iterm2.com/

3️⃣ Homebrew 폰트 설치 github https://github.com/Homebrew/homebrew-cask-fonts

4️⃣ iTerm2 컬러 테마 저장소 https://iterm2colorschemes.com/


🍎 단축키

⌨️ iterm2 설정화면 진입 ⌘ + ,

⌨️ vi편집기 수정모드 cursor + i

⌨️ vi편집기 파일 저장후 종료 ESC + :wq

⌨️ iterm2 상하/좌우 분할 ⌘ + d / ⌘ + ⇧ + d


1. Home-brew 설치하기

iTerm2 패키지를 설치하기 전에 먼저 Homebrew를 설치하도록 하겠습니다. Homebrew는 MacOS의 패키지 관리 소프트웨어인데 쉽게 패키지를 설치하고 삭제할 수 있게 해줍니다.

image

Homebrew 홈페이지에 들어가셔서 스크립트를 복사하시고 터미널로 돌아와 명령어를 입력해주겠습니다.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

설치가 정상적으로 완료되었는지 버전을 확인해보겠습니다. 정상적으로 버전이 나옵니다.

brew -v
Homebrew 3.6.11
Homebrew/homebrew-core (git revision 1513bfd3b36; last commit 2022-11-17)
Homebrew/homebrew-cask (git revision ad786f1638; last commit 2022-11-17)


2. iTerm2 설치하기

이제 iTerm2를 설치해보겠습니다. 터미널에 다음 명령어를 그대로 입력하면됩니다.

brew install iterm2
🍺  iterm2 was successfully installed!

맥주 이모티콘과 함께 성공적으로 인스톨되었다는 문구를 확인했다면, 그 다음으로 설치할 패키지는 Oh-My-Zsh 입니다.


3. Oh-My-Zsh 설치

이 패키지는 Zsh 터미널을 보기 좋게 꾸며주는 프레임워크 입니다. Oh-My-Zsh을 설치하기 위해 다음 명령어를 입력해주세요. 해당 명령어는 oh-my-zsh github에 있습니다.

sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

image

위 이미지처럼 출력되면 정상적으로 설치가 완료되었습니다. 만약 MacOS에 Zsh이 없다면 다음 명령어로 설치를 먼저 해주시면 됩니다.

brew install zsh

설치 후 iTerm를 열어주면 다음과 같은 화면이 나옵니다.

image

이게 기본 iTerm 터미널인데요, 아직은 코드 하이라이팅도 전혀 되지 않고, 예쁜 모습도 아닙니다. 이제 원하는 테마로 꾸며보도록 하겠습니다.


4. status bar 설정

우선 iTerm 터미널에서 ⌘ + , 를 눌러서 Preferences에 들어갑니다.

image

Profiles 탭으로 이동 후 Session 탭을 들어가면, 하단에 Status bar enabled 체크 박스 부분이 나오는데 해당 부분을 체크해 주세요.

image

Configure Status Bar를 누르면 다음과 같은 창이 나오는데, 이 부분에서 커스텀으로 수정해 줄 수 있습니다. 여기에 굉장히 많은 스테이터스바를 지원해주며, 저는 4가지를 선택해 주었습니다.

image

그리고 Status bar의 위치를 바꾸고 싶다면 Appearance 탭으로 들어가서 Status bar location 부분을 위 아래로 설정해 줄 수 있습니다.

image


5. 폰트 다운로드

테마를 변경하면 폰트가 깨질 수 있기 때문에 폰트를 다운로드 받아주겠습니다. homebrew-cask-fonts github에 들어가면 설치과정이 있습니다. 그리고 fira-code 라는 폰트를 설치하겠습니다. 그럼 다음 명령어를 입력해주겠습니다.

brew tap homebrew/cask-fonts
brew install font-fira-code

다시 iTerm 터미널에서 ⌘ + , 를 눌러서 Profiles탭으로 들어가서 Text 탭을 선택하면 다음과 같은 화면이 나옵니다.

image

Fira Code로 선택되어 있는데, 만약 다른걸로 선택되어 있다면 Fira Code로 바꿔주시고, Use ligatures 체크박스도 눌러주세요. 그럼 이제 폰트 설정은 끝났습니다.

이제 본격적으로 테마를 바꿔보겠습니다.


6. iterm2 테마설정

다시 iTerm 터미널에서 ⌘ + , 를 눌러서 Profiles탭으로 들어가서 Colors 탭을 선택하면 다음과 같은 화면이 나옵니다.

image

각자 하나하나씩 커스텀을 지정해 줄 수도 있고, 기본적으로 내장되어 있는 테마를 사용해도 되며, 마음에 드는게 없으면 부가적으로 제공하는 컬러 테마도 사용 가능합니다.

부가적으로 사용할 수 있는 테마에 대해서 알려드리겠습니다.

image

Iterm2-color-schemes 사이트에 들어가면 여러가지 테마들을 볼 수 있습니다. 타이틀을 클릭하시면 XML 형식의 코드들이 나오는데 파일을 저장해주시면 됩니다.

image

image

다운로드가 완료됐으면 저장한 폴더로 들어가 주셔서 txt라는 확장자를 지워주세요.

image

iTerm2에서 다시 설정 화면으로 들어가서 Color Presets에 해당 파일을 Import 해주고나서 원하는 테마를 선택하시면 됩니다.

image


7. iterm2 External themes

그리고 엔터를 쳤을때 나오는 테마도 변경하겠습니다. oh my zsh github을 들어가면 테마부분이 나옵니다.

image

우선 명령어로 zsh 설정파일을 열어주겠습니다.

vi ~/.zshrc

image

위 화면에서 ZSH_THEME 가 있는 라인데 커서를 두고 i를 입력하면 수정모드로 변경됩니다. 다음과 같이 입력해주세요.

ZSH_THEME="agnoster"

esc를 누르시고 저장하고 나가 보도록 하겠습니다. :wq 를 입력하시면 vi 저장 후 종료가 됩니다. 적용된 모습을 보려면 이 패키지를 껐다가 다시 켜도 되고 저는 설정 파일을 저장하는 명령어를 입력해줍니다.

source ~/.zshrc

정상적으로 적용되었다면 다음과 같은 화면이 등장합니다.

image


8. 빈 줄 넣기

그 다음으로는 중간에 빈 줄을 넣어서 가독성을 더 높여 보겠습니다. 다음 명령어로 테마 설정 파일을 열어줍니다.

vi ~/.oh-my-zsh/themes/agnoster.zsh-theme

image

위 이미지와같이 build_prompt()라는 Main prompt가 있습니다. i를 눌러 수정모드로 들어가서 build_prompt()prompt_newline 을 입력해 주시고 함수를 정의하고 추가하겠습니다.

image

다시 ESC 누르시고 :wq 명령어로 저장하고 나가 줍니다. 그리고 적용된 모습을 보려면 이 패키지를 껐다가 다시 켜도 되고, 설정 파일을 저장하는 명령어를 입력해줍니다.

source ~/.zshrc

image

중간에 빈 줄이 추가되서 위에보다 훨씬 더 가독성이 높아졌습니다.


9. 명령어 하이라이팅

그 다음으로는 명령어에 하이라이트 기능을 주도록 하겠습니다. 그러려면 zsh-syntax-highlighting 이라는 패키지를 설치해줘야 합니다.

brew install zsh-syntax-highlighting

다시 설정 파일을 열어 주시고 다음 소스를 맨 마지막 줄에 추가해주고 저장해 주겠습니다.

vi ~/.zshrc
📌 M1이상
source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

📌 intel Mac
source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

image

터미널을 다시 열어서 명령어를 작성하면, 이렇게 모든 명령어에 하이라이트 처리가 되는 걸 볼 수 있습니다.

image


10. 이모지 넣기

마지막으로 텍스트와 이모지를 넣어 주겠습니다. 그럼 다시 설정 파일을 열어주시고 맨 아래로 내려가서 prompt_context() 라는 함수를 덮어 써주겠습니다.

vi ~/.zshrc
prompt_context() {
  # Custom (Random emoji)
  emojis=("⚡️" "🔥" "🇰 " "👑" "😎" "🐸" "🐵" "🦄" "🌈" "🍻" "🚀" "💡" "🎉" "🔑" "🚦" "🌙")
  RAND_EMOJI_N=$(( $RANDOM % ${#emojis[@]} + 1))
  prompt_segment black default "{하고싶은이름} ${emojis[$RAND_EMOJI_N]} "
}

image

모든 과정이 정상적으로 진행되었습니다.

Read more

OpenCV Image Processing 히스토그램

|

해당 게시물은 파이썬으로 만드는 OpenCV 프로젝트(이세우 저) 를 바탕으로 작성되었습니다.


히스토그램

히스토그램(histogram)은 뭐가 몇 개 있는지 개수를 세어 놓은 것을 그림으로 표시한 것을 말합니다. 히스토그램은 영상을 분석하는 데 도움이 많이 됩니다.

1. 히스토그램 계산과 표시

영상 분야에서의 히스토그램은 전체 영상에서 픽셀 값이 1인 픽셀이 몇 개이고 2인 픽셀이 몇 개이고 하는 식으로 픽셀 값이 255인 픽셀이 몇 개인지까지 세는 것을 말합니다. 그렇게 하는 이유는 전체 영상에서 픽셀들의 색상이나 명암의 분포를 파악하기 위해서입니다.

OpenCV는 영상에서 히스토그램을 계산하는 cv2.calcHist() 함수를 제공합니다.

  • cv2.calcHist(img, channel, mask, histSize, ranges)
    • img : 입력 영상, [img]처럼 리스트로 감싸서 표현
    • channel : 처리할 채널, 리스트로 감싸서 표현
      • 1채널: [0], 2채널: [0, 1], 3채널: [0, 1, 2]
    • mask : 마스크에 지정한 픽셀만 히스토그램 계산
    • histSize : 계급(bin)의 개수, 채널 개수에 맞게 리스트로 표현
      • 1채널: [256], 2채널: [256, 256], 3채널: [256, 256, 256]
    • ranges : 각 픽셀이 가질 수 있는 값의 범위, RGB인 경우 [0, 256]

가장 간단하게 그레이 스케일 이미지의 히스토그램을 계산해서 그려보겠습니다.

'''그레이 스케일 1채널 히스토그램'''
import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 이미지 그레이 스케일로 읽기 및 출력
img = cv2.imread('./img/mountain.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imshow('img', img)

#--② 히스토그램 계산 및 그리기
hist = cv2.calcHist([img], [0], None, [256], [0,255])
plt.plot(hist)

print("hist.shape:", hist.shape)  #--③ 히스토그램의 shape (256,1)
print("hist.sum():", hist.sum(), "img.shape:",img.shape) #--④ 히스토그램 총 합계와 이미지의 크기
plt.show()

[output]
hist.shape: (256, 1)
hist.sum(): 270000.0 img.shape: (450, 600)

image

image

위 코드는 영상을 그레이 스케일로 읽어서 1차원 히스토그램으로 출력하는 예제입니다. 코드 ②가 가장 핵심적인 코드입니다. 여기서 cv2.calcHist() 함수 호출에 사용한 인자를 순서대로 설명하면, 히스토그램 대상 이미지는 [img], 1채널만 있어서 [0], 마스크는 사용하지 않으므로 None, 가로축(x축)에 표시할 계급(bin)의 개수는 [256], 픽셀 값 중 최소 값과 최대 값은 [0, 256]이라는 의미입니다. 여기서 최대값은 범위에 포함되지 않으므로 255보다 1 큰 값을 전달합니다. 이렇게 얻은 결과를 plt.plot() 함수에 전달하면 히스토그램을 그림으로 보여줍니다.

코드 ③에서 출력한 히스토그램 배열의 shape는 (256, 1)입니다. 256개의 계급에 각각 픽셀 수가 몇 개인지 저장한 모양새입니다. 코드 ④에서는 히스토그램의 전체 합과 이미지의 크기를 출력하고 있는데, 이 값으로 이미지의 폭과 높이의 곱과 히스토그램의 합(450×600 = 270,000)이 같은 것을 알 수 있습니다.

그레이 스케일이 아닌 컬러 스케일에 대한 히스토그램은 3개 채널, 즉 R, G, B를 각각 따로 계산해서 그려볼 수 있습니다.

'''컬러 히스토그램'''
import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 이미지 읽기 및 출력
img = cv2.imread('./img/mountain.jpg')
cv2.imshow('img', img)

#--② 히스토그램 계산 및 그리기
channels = cv2.split(img)
colors = ('b', 'g', 'r')
for (ch, color) in zip (channels, colors):
    hist = cv2.calcHist([ch], [0], None, [256], [0, 255])
    plt.plot(hist, color = color)
    
plt.show()

image

image

위 예제는 컬러 스케일 이미지의 3개 채널에 대해서 1차원 히스토그램을 각각 구하고 나서 하나의 플롯에 그렸습니다. 히스토그램을 보면 파란 하늘이 가장 넓은 영역을 차지하고 있으므로 파란색 분포가 크고 초록 나무와 단풍 때문에 초록색과 빨간색의 분포가 그 뒤를 따르는 것으로 보입니다.


2. 노멀라이즈

노멀라이즈(normalize, 정규화)는 원래 기준이 서로 다른 값을 같은 기준이 되게 만드는 것을 말합니다. 노멀라이즈는 서로 다른 기준을 하나의 절대적인 기준으로 만들기도 하지만 절대적인 기준 대신 특정 구간으로 노멀라이즈하면 특정 부분에 몰려 있는 값을 전체 영역으로 골고루 분포하게 할 수도 있습니다. 예를 들어 전교생이 5명인 학생들의 성적이 95, 96, 97, 98, 99, 100점일 때 95점 이상에게 A+ 학점을 준다면 전교생이 A+를 받게 되니 이 시험엔 분명 문제가 있습니다. 선생님이 각 학생의 점수를 70 ~ 100점 사이로 다시 환산하고 싶어 한다면, 이때 필요한 것이 바로 구간 노멀라이즈입니다.

원래 점수는 95 ~ 100, 즉 5점 간격이었는데, 새로운 점수는 70 ~ 100, 즉 30점 간격이므로 $30 / 5 = 6$ 으로, 학생들의 성적이 70, 76, 82, 88, 94, 100점으로, 원래 점수 1점 차이는 새로운 점수 6점 차이가 됩니다. 원래 점수가 5점 구간에서 얼마인지 찾아 그 비율(6점)과 곱해서 새로운 시작 구간(70점)에 더하면 새로운 점수를 구할 수 있습니다. 이것을 수학식으로 정리하면 다음과 같습니다.

[I_N = (I - Min)\frac{newMax - newMin}{Max - Min} + newMin]

  • $I$ : 노멀라이즈 이전 값
  • $Min, Max$ : 노멀라이즈 이전 범위의 최소 값, 최대 값
  • $newMin, newMax$ : 노멀라이즈 이후 범위의 최소 값, 최대 값
  • $I_N$ : 노멀라이즈 이후 값

영상 분야에서는 노멀라이즈를 가지고 픽셀 값들이 0~255에 골고루 분포하지 않고 특정 영역에 몰려 있는 경우 화질을 개선하기도 하고 영상 간의 연산을 해야 하는데, 서로 조건이 다른 경우 같은 조건으로 만들기도 합니다.

OpenCV는 노멀라이즈 기능을 아래와 같은 함수로 제공합니다.

  • dst = cv2.normalize(src, dst, alpha, beta, type_flag)
    • src : 노멀라이즈 이전 데이터
    • dst : 노멀라이즈 이후 데이터
    • alpha : 노멀라이즈 구간 1
    • beta : 노멀라이즈 구간 2, 구간 노멀라이즈가 아닌 경우 사용 안함
    • type_flag : 알고리즘 선택 플래그 상수
      • cv2.NORM_MINMAX : alpha와 beta 구간으로 노멀라이즈
      • cv2.NORM_L1 : 전체 합으로 나누기, alpha = 노멀라이즈 전체 합
      • cv2.NORM_L2 : 단위 벡터(unit vector)로 노멀라이즈
      • cv2.NORM_INF : 최대 값으로 나누기

아래의 예제는 뿌연 영상에 노멀라이즈를 적용해서 화질을 개선하는 예제입니다.

'''히스토그램 정규화'''
import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 그레이 스케일로 영상 읽기
img = cv2.imread('./img/abnormal.jpg', cv2.IMREAD_GRAYSCALE)

#--② 직접 연산한 정규화
img_f = img.astype(np.float32)
img_norm = ((img_f - img_f.min()) * (255) / (img_f.max() - img_f.min()))
img_norm = img_norm.astype(np.uint8)

#--③ OpenCV API를 이용한 정규화
img_norm2 = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)

#--④ 히스토그램 계산
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
hist_norm = cv2.calcHist([img_norm], [0], None, [256], [0, 255])
hist_norm2 = cv2.calcHist([img_norm2], [0], None, [256], [0, 255])

cv2.imshow('Before', img)
cv2.imshow('Manual', img_norm)
cv2.imshow('cv2.normalize()', img_norm2)

hists = {'Before' : hist, 'Manual':hist_norm, 'cv2.normalize()':hist_norm2}
for i, (k, v) in enumerate(hists.items()):
    plt.subplot(1,3,i+1)
    plt.title(k)
    plt.plot(v)

plt.show()

image

image

image

image

위 코드 ②는 앞서 설명한 노멀라이즈 공식을 직접 대입해서 연산하였습니다. 코드 ②에서 dtype을 float32로 바꾸었다가 다시 uint8로 바꾼 이유는 연산 과정에서 소수점이 발생하기 때문입니다. 코드 ③은 cv2.normalize() 함수로 노멀라이즈를 적용했습니다. 이때 앞서 설명한 구간 노멀라이즈를 사용하려면 cv2.NORM_MINMAX 플래그 상수를 사용하고 alpha, beta는 대상 구간 값을 전달합니다. 실행 결과는 중앙에 몰려 있던 픽셀들의 분포가 전체적으로 고르게 펴져서 화질이 개선된 것을 보여줍니다.

구간 노멀라이즈가 아니라 서로 다른 히스토그램의 빈도를 같은 조건으로 비교하는 경우에는 전체의 비율로 노멀라이즈해야 하는데, 이때 코드는 다음과 같습니다.

norm = cv2.normalize(hist, None, 1, 0, cv2.NORM_L1)

위 코드에서 cv2.NORM_L1 플래그 상수를 사용하면 결과는 전체를 모두 합했을 때 1이 됩니다. 세 번째 인자 값에 따라 그 합은 달라지고 네 번째 인자는 무시됩니다.


3. 이퀄라이즈

앞서 설명한 노멀라이즈는 분포가 한곳에 집중되어 있는 경우에는 효과적이지만 그 집중된 영역에서 멀리 떨어진 값이 있을 경우에는 효과가 없습니다. 다시 학생들 점수를 예로 들면 전교생 5명의 점수가 70, 96, 98, 98, 100으로 나왔다면 첫 번째 학생의 점수가 70점이므로 구간 노멀라이즈로는 새로운 70 ~ 100 분포로 만들어도 결과는 동일한데, 기존의 범위와 새로운 범위가 같기 때문입니다. 이때에는 이퀄라이즈(equalize, 평탄화)가 필요합니다.

이퀄라이즈는 히스토그램으로 빈도를 구해서 그것을 노멀라이즈한 후 누적값을 전체 개수로 나누어 나온 결과 값을 히스토그램 원래 픽셀 값에 매핑합니다. 히스토그램 이퀄라이즈를 위한 수학식은 아래와 같습니다.

[H’(v) = round\left(\frac{cdf(v) - cdf_{min}}{(M \times N) - cdf_{min}} \times (L - 1)\right)]

  • $cdf(v)$ : 히스토그램 누적 함수
  • $cdf_{min}$ : 누적 최소 값, 1
  • $M × N$ : 픽셀 수, 폭 × 높이
  • $L$ : 분포 영역, 256
  • $round(v)$ : 반올림
  • $H’(v)$ : 이퀄라이즈된 히스토그램 값

이퀄라이즈는 각각의 값이 전체 분포에 차지하는 비중에 따라 분포를 재분배하므로 명암 대비(contrast)를 개선하는 데 효과적입니다.

OpenCV에서 제공하는 이퀄라이즈 함수는 아래와 같습니다.

  • dst = cv2.equalizeHist(src[, dst])
    • src : 대상 이미지, 8비트 1채널
    • dst : 결과 이미지

다음 예제는 어둡게 나온 사진을 그레이 스케일로 바꾸어 이퀄라이즈를 적용해서 개선시키는 예제입니다.

'''그레이 스케일 이퀄라이즈 적용'''
import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 대상 영상으로 그레이 스케일로 읽기
img = cv2.imread('./img/yate.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape[:2]

#--② 이퀄라이즈 연산을 직접 적용
hist = cv2.calcHist([img], [0], None, [256], [0, 256]) # 히스토그램 계산
cdf = hist.cumsum()                                    # 누적 히스토그램 
cdf_m = np.ma.masked_equal(cdf, 0)                     # 0(zero)인 값을 NaN으로 제거
cdf_m = (cdf_m - cdf_m.min()) / (rows * cols) * 255    # 이퀄라이즈 히스토그램 계산
cdf = np.ma.filled(cdf_m,0).astype('uint8')            # NaN을 다시 0으로 환원
print(cdf.shape)
img2 = cdf[img]                                        # 히스토그램을 픽셀로 맵핑

#--③ OpenCV API로 이퀄라이즈 히스토그램 적용
img3 = cv2.equalizeHist(img)

#--④ 이퀄라이즈 결과 히스토그램 계산
hist2 = cv2.calcHist([img2], [0], None, [256], [0, 256])
hist3 = cv2.calcHist([img3], [0], None, [256], [0, 256])

#--⑤ 결과 출력
cv2.imshow('Before', img)
cv2.imshow('Manual', img2)
cv2.imshow('cv2.equalizeHist()', img3)
hists = {'Before':hist, 'Manual':hist2, 'cv2.equalizeHist()':hist3}
for i, (k, v) in enumerate(hists.items()):
    plt.subplot(1,3,i+1)
    plt.title(k)
    plt.plot(v)

plt.show()

image

image

image

image

코드 ②는 히스토그램 이퀄라이즈 수식을 그대로 연산에 적용하고 있습니다. hist.cumsum() 은 누적합을 구하는 함수이고, np.ma.masked_equal(cdf, 0) 은 요소 값이 0인 것을 NaN으로 적용하는데, 불필요한 연산을 줄이고자 하는 이유입니다. 이것을 다시 원래대로 되돌리는 기능이 np.ma.filled(cdf_m, 0) 입니다. img2 = cdf[img] 는 연산 결과를 원래의 픽셀 값에 매핑합니다.

이렇게 복잡한 연산에 OpenCV에서 제공하는 API를 사용하면 코드 ③처럼 단 한줄이면 끝납니다. 실행 결과를 보면 직접 계산을 적용한 결과와 cv2.equalizeHist() 함수를 사용한 것 모두 밝기가 개선된 것을 알 수 있습니다.

히스토그램 이퀄라이즈는 컬러 스케일에도 적용할 수 있는데, 밝기 값을 개선하기 위해서는 3개 채널 모두를 개선해야 하는 BGR 컬러 스페이스보다는 YUV나 HSV로 변환해서 밝기 채널만을 연산해서 최종 이미지에 적용하는 것이 좋습니다.

다음 예제는 YUV 컬러 스페이스로 변경한 컬러 이미지에 대한 이퀄라이즈를 보여줍니다.

'''컬러 이미지에 대한 이퀄라이즈 적용'''
import numpy as np, cv2

img = cv2.imread('./img/yate.jpg') #이미지 읽기, BGR 스케일

#--① 컬러 스케일을 BGR에서 YUV로 변경
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) 

#--② YUV 컬러 스케일의 첫번째 채널에 대해서 이퀄라이즈 적용
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0]) 

#--③ 컬러 스케일을 YUV에서 BGR로 변경
img2 = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR) 

cv2.imshow('Before', img)
cv2.imshow('After', img2)
cv2.waitKey()
cv2.destroyAllWindows()

image

image

요트 부분을 비교해서 보면 훨씬 선명한 결과를 얻은 것을 볼 수 있습니다. HSV의 세 번째 채널에 대해서 이퀄라이즈를 적용해도 비슷한 결과를 얻을 수 있습니다. 코드 ②를 HSV 컬러 스페이스에 적용하면 코드는 아래와 같습니다.

img_hsv[:,:,2] = cv2.equalizeHist(img_hsv[:,:,2])


4. CLAHE

CLAHE(Contrast Limiting Adaptive Histogram Equalization)는 영상 전체에 이퀄라이즈를 적용했을 때 너무 밝은 부분이 날아가는 현상을 막기 위해 영상을 일정한 영역으로 나눠서 이퀄라이즈를 적용하는 것을 말합니다. 노이즈가 증폭되는 것을 막기 위해 어느 히스토그램 계급(bin)이든 지정된 제한 값을 넘으면 그 픽셀은 다른 계급으로 배분하고 나서 이퀄라이즈를 적용합니다.

image

CLAHE를 위한 OpenCV 함수는 다음과 같습니다.

  • clahe = cv2.createCLAHE(clipLimit, tileGridSize) : CLAHE 생성
    • clipLimit : Contrast 제한 경계 값, 기본 40.0
    • tileGridSize : 영역 크기, 기본 8 × 8
    • clahe : 생성된 CLAHE 객체
  • clahe.apply(src) : CLAHE 적용
    • src : 입력 영상
'''CLAHE'''
import cv2
import numpy as np
import matplotlib.pylab as plt

#--①이미지 읽어서 YUV 컬러스페이스로 변경
img = cv2.imread('./img/bright.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

#--② 밝기 채널에 대해서 이퀄라이즈 적용
img_eq = img_yuv.copy()
img_eq[:,:,0] = cv2.equalizeHist(img_eq[:,:,0])
img_eq = cv2.cvtColor(img_eq, cv2.COLOR_YUV2BGR)

#--③ 밝기 채널에 대해서 CLAHE 적용
img_clahe = img_yuv.copy()
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)) #CLAHE 생성
img_clahe[:,:,0] = clahe.apply(img_clahe[:,:,0])           #CLAHE 적용
img_clahe = cv2.cvtColor(img_clahe, cv2.COLOR_YUV2BGR)

#--④ 결과 출력
cv2.imshow('Before', img)
cv2.imshow('CLAHE', img_clahe)
cv2.imshow('equalizeHist', img_eq)
cv2.waitKey()
cv2.destroyAllWindows()

image

image

image

코드 ②는 단순한 이퀄라이즈를 적용했고 코드 ③은 CLAHE를 적용했습니다. cv2.createCLAHE() 에서 clipLimit=3.0 은 기본 값이 40.0이므로 상황에 따라적절한 값으로 바꾸어야 합니다. 원본 사진은 사진을 찍을 때 빛이 너무 많이 들어 갔습니다. 이퀄라이즈를 적용한 결과는 밝은 곳이 날아가는 증상이 발생한 것을 보여주고 있습니다.


5. 2D 히스토그램

1차원 히스토그램은 각 픽셀이 몇 개씩인지 세어서 그래프로 표현하는데, 2차원 히스토그램은 이와 같은 축이 2개이고 각각의 축이 만나는 지점의 개수를 표현합니다. 그래서 이것을 적절히 표현하려면 지금까지 사용한 2차원 그래프가 아닌 3차원 그래프가 필요합니다. 아래의 예제는 다음 그림의 맑고 화창한 가을 하늘의 산을 찍은 사진을 2차원 히스토그램으로 표현한 것입니다.

image

'''2D 히스토그램'''
import cv2
import matplotlib.pylab as plt

plt.style.use('classic')        # --① 컬러 스타일을 1.x 스타일로 사용
img = cv2.imread('./img/mountain.jpg')

plt.subplot(131)
hist = cv2.calcHist([img], [0,1], None, [32,32], [0,256,0,256]) #--②
p = plt.imshow(hist)                                            #--③
plt.title('Blue and Green')                                     #--④
plt.colorbar(p)                                                 #--⑤


plt.subplot(132)
hist = cv2.calcHist([img], [1,2], None, [32,32], [0,256,0,256]) #--⑥
p = plt.imshow(hist)
plt.title('Green and Red')
plt.colorbar(p)

plt.subplot(133)
hist = cv2.calcHist([img], [0,2], None, [32,32], [0,256,0,256]) #--⑦
p = plt.imshow(hist)
plt.title('Blue and Red')
plt.colorbar(p)

plt.show()

image

코드 ②, ⑥, ⑦은 각각 파랑과 초록, 초록과 빨강, 파랑과 빨강에 대한 2차원 히스토그램을 계산합니다. 계급 수는 256으로 조밀하게 하면 색상이 너무 작게 표현되서 32 정도로 큼직하게 잡았습니다. 각 값의 범위는 0~256 이 두 번 반복됩니다. 계산한 히스토그램을 코드 ③에서 imshow() 함수로 표현했습니다. 그래서 이 결과를 보면서 정확한 정보를 읽는 것은 그다지 도움이 되지는 않습니다. 다만, 코드⑤에서 각 색상에 대한 컬러 막대를 범례(legend)로 표시했기 때문에 색상을 보면서 대략의 정보를 알아낼 수 있습니다. 빨간색으로 표시될수록 픽셀의 개수가 많고 파란색은 픽셀이 적은 것을 나타냅니다.

여기서 중요한 것은 2차원 히스토그램의 의미입니다. 왼쪽 그림은 파랑과 초록의 2차원 히스토그램인데, 가장 높은 값을 갖는 부분은 빨간색으로 표시된 x = 15, y = 25 정도의 좌표로 대략 10,000 이상의 값을 갖습니다. 이 의미는 파란색이면서 초록색인 픽셀의 개수가 가장 많다는 의미입니다. 중간과 오른쪽 그림을 봐도 초록과 파랑의 수치가 높은 것을 알 수 있습니다. 2차원 히스토그램의 의미는 x축이면서 y축인 픽셀의 분포를 알 수 있다는 것입니다. 논리 연산의 AND 연산과 같습니다.


6. 역투영

2차원 히스토그램과 HSV 컬러 스페이스를 이용하면 색상으로 특정 물체나 사물의 일부분을 배경에서 분리할 수 있습니다. 기본 원리는 물체가 있는 관심영역의 H와 V값의 분포를 얻어낸 후 전체 영상에서 해당 분포의 픽셀만 찾아내는 것입니다. 다음 예제에서는 마우스로 선택한 특정 물체만 배경에서 분리해 내는 모습을 보여주고 있습니다.

'''마우스로 선택한 영역의 물체 배경 제거'''
import cv2
import numpy as np
import matplotlib.pyplot as plt

win_name = 'back_projection'
img = cv2.imread('./img/pump_horse.jpg')
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
draw = img.copy()

#--⑤ 역투영된 결과를 마스킹해서 결과를 출력하는 공통함수
def masking(bp, win_name):
    disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
    cv2.filter2D(bp,-1,disc,bp)
    _, mask = cv2.threshold(bp, 1, 255, cv2.THRESH_BINARY)
    result = cv2.bitwise_and(img, img, mask=mask)
    cv2.imshow(win_name, result)

#--⑥ 직접 구현한 역투영 함수
def backProject_manual(hist_roi):
    #--⑦ 전체 영상에 대한 H,S 히스토그램 계산
    hist_img = cv2.calcHist([hsv_img], [0,1], None,[180,256], [0,180,0,256])
    #--⑧ 선택영역과 전체 영상에 대한 히스토그램 그램 비율계산
    hist_rate = hist_roi/ (hist_img + 1)
    #--⑨ 비율에 맞는 픽셀 값 매핑
    h,s,v = cv2.split(hsv_img)
    bp = hist_rate[h.ravel(), s.ravel()]

    bp = np.minimum(bp, 1)
    bp = bp.reshape(hsv_img.shape[:2])
    cv2.normalize(bp,bp, 0, 255, cv2.NORM_MINMAX)
    bp = bp.astype(np.uint8)
    #--⑩ 역 투영 결과로 마스킹해서 결과 출력
    masking(bp,'result_manual')
 
# OpenCV API로 구현한 함수 ---⑪ 
def backProject_cv(hist_roi):
    # 역투영 함수 호출 ---⑫
    bp = cv2.calcBackProject([hsv_img], [0, 1], hist_roi,  [0, 180, 0, 256], 1)
    # 역 투영 결과로 마스킹해서 결과 출력 ---⑬ 
    masking(bp,'result_cv')

# ROI 선택 ---①
(x,y,w,h) = cv2.selectROI(win_name, img, False)
if w > 0 and h > 0:
    #roi = draw[y:y+h, x:x+w]
    roi = img[y:y+h, x:x+w]
    cv2.rectangle(draw, (x, y), (x+w, y+h), (0,0,255), 2)
    #--② 선택한 ROI를 HSV 컬러 스페이스로 변경
    hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
    #--③ H,S 채널에 대한 히스토그램 계산
    hist_roi = cv2.calcHist([hsv_roi],[0, 1], None, [180, 256], [0, 180, 0, 256] )
    #--④ ROI의 히스토그램을 매뉴얼 구현함수와 OpenCV 이용하는 함수에 각각 전달
    backProject_manual(hist_roi)
    backProject_cv(hist_roi)
cv2.imshow(win_name, draw)
cv2.waitKey()
cv2.destroyAllWindows()

image

image

image

코드 ①에서 마우스로 ROI를 선택하게 합니다. ROI를 선택하고 스페이스나 엔터 키를 누르면 코드 ②에서 선택한 관심영역을 HSV컬러 스페이스로 변경하고, 코드 ③에서 H와 S채널에 대한 2차원 히스토그램을 계산한 결과를 직접 구현한 함수와 OpenCV를 이용한 함수에 인자로 전달합니다.

먼저 코드 ⑥의 직접 구현한 함수를 살펴보면, 코드 ⑧에서 전달된 관심영역의 히스토그램을 전체 영상의 히스토그램으로 나누어 비율을 구합니다. 이때 1을 더한 이유는 분모가 0이 되어 오류가 발생하는 일이 없게 하기 위해서입니다. 비율을 구한다는 것은 관심영역과 비슷한 색상 분포를 갖는 히스토그램은 1에 가까운 값을 갖고 그 반대는 0 또는 0에 가까운 값을 갖게 되는 것으로 마스킹에 사용하기 좋다는 뜻입니다. 코드 ⑨는 이렇게 구한 비율을 원래 영상의 H와 S 픽셀 값에 매핑합니다. 여기서 bp = hist_rate[h.ravel(), s.ravel()] 가 핵심적인 코드입니다. hist_rate는 히스토그램 비율을 값으로 가지고 있고, 와 s는 실제 영상의 각 픽셀에 해당합니다. 따라서 H와 S가 교차되는 지점의 비율을 그 픽셀의 값으로 하는 1차원 배열을 얻게 됩니다. 여기서 사용한 NumPy 연산을 단순화시켜서 설명하면 아래의 코드와 같습니다.

>>> v = np.arange(6).reshape(2,3)
>>> v
array([[0, 1, 2],
       [3, 4, 5]])
>>> row = np.array([1,1,1,0,0,0])
>>> col = np.array([0,1,2,0,1,2])
>>> v[row, col]
array([3, 4, 5, 0, 1, 2])

이렇게 얻는 값들은 비율이라서 1을 넘어서는 안 되므로 np.minum(bp,1) 로 1을 넘는 수는 1을 갖게 하고 나서 1차원 배열을 원래의 shape로 만들고 0~255 그레이 스케일에 맞는 픽셀 값으로 노멀라이즈합니다. 비율 연산 도중에 float 타입으로 변경된것을 unit8로 변경하면 작업은 끝나게 됩니다.

이런 복잡한 코드를 OpenCV는 아래와 같은 함수로 제공합니다.

  • cv2.calcBackProject(img, channel, hist, ranges, scale)
    • img : 입력 영상, [img]처럼 리스트로 감싸서 표현
    • channel : 처리할 채널, 리스트로 감싸서 표현
      • 1채널: [0], 2채널: [0,1], 3채널: [0,1,2]
    • hist : 역투영에 사용할 히스토그램
    • ranges : 각 픽셀이 가질 수 있는 값의 범위
    • scale : 결과에 적용할 배율 계수

코드 ⑫에서 호출하는 cv2.calcBackProject() 함수는 세 번째 인자로 역투영에 사용할 히스토그램을 전달하면 역투영 결과를 반환합니다. 마지막 인자인 scale은 결과에 일정한 값을 계수로 적용할 수 있습니다.

코드 ⑤에 구현한 masking() 함수는 앞서 다룬 스레시홀드와 마스킹을 거쳐서 결과를 출력하는 함수인데, 여기에 함께 사용한 cv2.getStructuringElement()cv2.filter2D() 함수는 마스크의 표면을 부드럽게 하기 위한 것입니다.

역투영의 장점은 알파 채널이나 크로마 키 같은 보조 역할이 없어도 복잡한 모양의 사물을 분리할 수 있다는 것입니다. 하지만 대상 사물의 색상과 비슷한 색상이 뒤섞여 있을 때는 효과가 지는 단점도 있습니다.


7. 히스토그램 비교

히스토그램은 영상의 픽셀 값의 분포를 갖는 정보이므로 이것을 비교하면 영상에 사용한 픽셀의 색상 비중이 얼마나 비슷한지 알 수 있습니다. 이것은 영상이 서로 얼마나 비슷한지를 알 수 있는 하나의 방법입니다. OpenCV는 히스토그램을 비교해서그 유사도가 얼마인지 판단해 주는 함수를 아래와 같이 제공합니다.

  • cv2.compareHist(hist1, hist2, method)
    • hist1, hist2 : 비교할 2개의 히스토그램, 크기와 차원이 같아야 함
    • method : 비교 알고리즘 선택 플래그 상수
      • cv2.HISTCMP_CORREL : 상관관계 (1: 완전 일치, -1: 최대 불일치, 0: 무관계)
      • cv2.HISTCMP_CHISQR : 카이제곱 (0: 완전 일치, 큰 값(미정): 최대 불일치)
      • cv2.HISTCMP_INTERSECT : 교차(1: 완전 일치, 0: 최대 불일치(1로 정규화한경우))
      • cv2.HISTCMP_BHATTACHARYYA : 바타차야 (0: 완전 일치, 1: 최대 불일치)
      • cv2.HISTCMP_HELLINGER : HISTCMP_BHATTACHARYYA와 동일

이 함수는 첫 번째와 두 번째 인자에 비교하고자 하는 히스토그램을 전달하고, 마지막 인자에 어떤 플래그 상수를 전달하느냐에 따라 반환 값의 의미가 달라집니다. cv2.HISTCMP_CORREL 은 상관 관계를 기반으로 피어슨 상관계수로 유사성을 측정하고, cv2.HISTCMP_CHISQR 은 피어슨 상관계수 대신 카이제곱으로 유사성을 측정합니다. cv2.HISTCMP_INTERSECT 는 두 히스토그램의 교차점의 작은 값을 선택해서 그 합을 반환합니다. 반환 값을 원래의 히스토그램의 합으로 나누면 1과 0으로 노멀라이즈할 수 있습니다. cv2.HISTCMP_BHATTACHARYYA 는 두 분포의 중첩되는 부분을 측정합니다.

서로 다른 영상의 히스토그램을 같은 조건으로 비교하기 위해서는 먼저 히스토그램을 노멀라이즈해야 합니다. 이미지가 크면 픽셀 수가 많고 당연히 히스토그램의 값도 더 커지기 때문입니다.

다음 예제는 다른 각도에서 찍은 태권브이 장난감 이미지 3개와 코주부 박사 장난감을 찍은 이미지를 비교해서 각 비교 알고리즘에 다른 결과를 보여줍니다.

'''히스토그램 비교'''
import cv2, numpy as np
import matplotlib.pylab as plt

img1 = cv2.imread('./img/taekwonv1.jpg')
img2 = cv2.imread('./img/taekwonv2.jpg')
img3 = cv2.imread('./img/taekwonv3.jpg')
img4 = cv2.imread('./img/dr_ochanomizu.jpg')

cv2.imshow('query', img1)
imgs = [img1, img2, img3, img4]
hists = []
for i, img in enumerate(imgs) :
    plt.subplot(1,len(imgs),i+1)
    plt.title('img%d'% (i+1))
    plt.axis('off') 
    plt.imshow(img[:,:,::-1])
    #---① 각 이미지를 HSV로 변환
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    #---② H,S 채널에 대한 히스토그램 계산
    hist = cv2.calcHist([hsv], [0,1], None, [180,256], [0,180,0, 256])
    #---③ 0~1로 정규화
    cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)
    hists.append(hist)


query = hists[0]
methods = {'CORREL' :cv2.HISTCMP_CORREL, 'CHISQR':cv2.HISTCMP_CHISQR, 
           'INTERSECT':cv2.HISTCMP_INTERSECT,
           'BHATTACHARYYA':cv2.HISTCMP_BHATTACHARYYA}
for j, (name, flag) in enumerate(methods.items()):
    print('%-10s'%name, end='\t')
    for i, (hist, img) in enumerate(zip(hists, imgs)):
        #---④ 각 메서드에 따라 img1과 각 이미지의 히스토그램 비교
        ret = cv2.compareHist(query, hist, flag)
        if flag == cv2.HISTCMP_INTERSECT: # 교차 분석인 경우 
            ret = ret/np.sum(query)       # 비교대상으로 나누어 1로 정규화
        print("img%d:%7.2f"% (i+1 , ret), end='\t')
    print()

plt.show()

[output]
CORREL          img1:   1.00    img2:   0.70    img3:   0.56    img4:   0.23
CHISQR          img1:   0.00    img2:  67.34    img3:  35.71    img4:1129.50
INTERSECT       img1:   1.00    img2:   0.54    img3:   0.40    img4:   0.18
BHATTACHARYYA   img1:   0.00    img2:   0.48    img3:   0.47    img4:   0.79

image

image

코드 ①, ②, ③은 각 영상을 HSV 컬러 스페이스로 바꾸고 H와 V에 대해 2차원 히스토그램을 계산해서 0~1로 노멀라이즈합니다. 코드 ④에서 각각의 비교 알고리즘을 이용해서 각 영상을 차례대로 비교합니다. 이때 cv2.HISTCMP_INTERSECT 인 경우 비교 원본의 히스토그램으로 나누기를 하면 0~1로 노멀라이즈할 수 있고 그러면 결과를 판별하기가 편리합니다.

img1과의 비교 결과는 모두 완전 일치를 보여주고 있으며, img4의 경우 가장 멀어진 값으로 나타나는 것을 확인할 수 있습니다.

Read more