[OpenCV] 08-2-2. OCR of Hand-written Data using SVM
🐍Python/OpenCV

[OpenCV] 08-2-2. OCR of Hand-written Data using SVM

728x90
반응형

< 8-2-2. OCR of Hand-written Data using SVM >

이번 장에서는

  • 손글씨 분류를 kNN이 아닌 SVM으로 다시 해볼 것이다!

OCR of Hand-written Digits

kNN에서, 픽셀의 강도를 feature vector로 사용했었다. 하지만 이번에는 Histogram of Oriented Gradients(HOG)를 feature vector로 사용할 것이다.

  • HOG : HOG는 object detection을 목적으로 하는 computer vision과 이미지 전처리에서 feature descriptor로 사용된다. edge의 gradient의 방향정보를 이용하기에 edge orientation histograms 방법과 유사하다.

HOG를 찾기 전에, 먼저 이미지를 deskew 한다.

  • deskew : 이미지를 똑바르게 정렬시키는 과정

먼저 deskew() 함수를 정의하자.

튜토리얼처럼 하는 deskew와 다른 방식으로 진행해볼 것이다. 다른 방식 먼저 보자.

import cv2
import numpy as np
import matplotlib.pyplot as plt

def deskew(img):
    thresh = cv2.threshold(img,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]

    # 0보다 큰 모든 픽셀 값의 (x,y) 좌표를 구한다.
    # 그리고 이 좌표들을 rotated 연산을 하는데 사용한다.
    coords = np.column_stack(np.where(thresh>0))
    # cv2.minAreaRect : 모양에 외접하면서 면적이 가장 작은 직사각형을 구한다.
    # 그 좌표가 주어짐 마지막 요소는 angle로 사용
    angle = cv2.minAreaRect(coords)[-1]

    if angle < -45:
        angle = -(90 + angle)
    else:
        angle = -angle

    h,w = img.shape[:2]
    center = (w//2,h//2)
    # Affine Transform을 위한 Matrix 구하기
    # 1.0으로 scale 유지
    M = cv2.getRotationMatrix2D(center,angle,1.0)
    # cv2.INTER_CUBIC : 보간법으로 사이즈를 크게 할 때 사용
    # cv2.BORDER_REPLICATE : 원본의 가장 밖의 테두리에서의 행이나 열은 
    # 추가 테두리로 복제된다.
    rotated = cv2.warpAffine(img,M,(w,h),flags= cv2.INTER_CUBIC,
                             borderMode=cv2.BORDER_REPLICATE)
    return rotated

튜토리얼과는 다른 코드이다. Threshold를 적용하고 좌표를 구하고 각도를 구해서 RotationMatrix를 얻고 이를 이용해서 AffineTransform을 적용하는 것이다.


그 다음으로 각 셀에 대해 HOG Descriptor를 구해야 한다. 이를 위해, X, Y 방향에서의 각 셀의 Sobel derivatives를 구한다. 그리고 이들의 크기(magnitude)와 각 픽셀에서의 gradient의 방향을 찾는다. 이 gradient는 16 정수값으로 양자화 된다. 이 이미지를 4개의 하위 사각형으로 나눈다. 각 하위 사각형에 대해, 그 크기에 따라 가중치를 부여한 방향(16 bins)의 히스토그램을 계산한다. 그래서 각 하위 사각형은 16개의 값을 포함하는 벡터를 가지게 된다. 그래서 총 64개(16x4)의 feature vector를 얻게 되는 것이다. 이 feature vector는 데이터를 학습하는데 사용된다.

import cv2 as cv
import numpy as np
SZ=20
bin_n = 16 # Number of bins
affine_flags = cv.WARP_INVERSE_MAP|cv.INTER_LINEAR
def deskew(img):
    thresh = cv2.threshold(img,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]

    # 0보다 큰 모든 픽셀 값의 (x,y) 좌표를 구한다.
    # 그리고 이 좌표들을 rotated 연산을 하는데 사용한다.
    coords = np.column_stack(np.where(thresh>0))
    # cv2.minAreaRect : 모양에 외접하면서 면적이 가장 작은 직사각형을 구한다.
    # 그 좌표가 주어짐 마지막 요소는 angle로 사용
    angle = cv2.minAreaRect(coords)[-1]

    if angle < -45:
        angle = -(90 + angle)
    else:
        angle = -angle

    h,w = img.shape[:2]
    center = (w//2,h//2)
    # Affine Transform을 위한 Matrix 구하기
    # 1.0으로 scale 유지
    M = cv2.getRotationMatrix2D(center,angle,1.0)
    # cv2.INTER_CUBIC : 보간법으로 사이즈를 크게 할 때 사용
    # cv2.BORDER_REPLICATE : 원본의 가장 밖의 테두리에서의 행이나 열은 
    # 추가 테두리로 복제된다.
    rotated = cv2.warpAffine(img,M,(w,h),flags= cv2.INTER_CUBIC,
                             borderMode=cv2.BORDER_REPLICATE)
    return rotated
def hog(img):
    gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
    gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
    # cv2.cartToPolar : 2D 벡터의 magnitude와 angle을 구함
    mag, ang = cv2.cartToPolar(gx, gy)
    # (0...16)으로 양자화
    # 양자화 : 연속적으로 보이는 양을 자연수로 셀 수 있는 양으로 재해석 하는 것
    bins = np.int32(bin_n*ang/(2*np.pi))  
    # 4개의 하위 사각형으로 나누기
    bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
    mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
    hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
    hist = np.hstack(hists)     
    return hist
img = cv.imread('./images/digits.png',0)

## vsplit : row-wise
## hsplit : col-wise
cells = [np.hsplit(row,100) for row in np.vsplit(img,50)]
# First half is trainData, remaining is testData
train_cells = [ i[:50] for i in cells ]
test_cells = [ i[50:] for i in cells]

# 학습
deskewed = [list(map(deskew,row)) for row in train_cells]
hogdata = [list(map(hog,row)) for row in deskewed]
trainData = np.float32(hogdata).reshape(-1,64)
responses = np.repeat(np.arange(10),250)[:,np.newaxis]
svm = cv.ml.SVM_create()
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setType(cv.ml.SVM_C_SVC)
svm.setC(2.67)
svm.setGamma(5.383)
svm.train(trainData, cv.ml.ROW_SAMPLE, responses)
svm.save('svm_data.dat')

# 테스트
deskewed = [list(map(deskew,row)) for row in test_cells]
hogdata = [list(map(hog,row)) for row in deskewed]
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = svm.predict(testData)[1]
mask = result==responses
correct = np.count_nonzero(mask)
print(correct*100.0/result.size)

85.44%의 정확도가 나온다. 그렇다면 튜토리얼의 deskew 방법을 써서 결과를 내면 어떨까?


그 전에 cv2.moments의 종류를 알아야한다.

  • 공간 모멘트(Spatial Moments)
    m00,m10,m01,m20,m11,m02,m30,m21,m12,m03
    공간에 대한 정보이기에 Cx=M10M00,Cy=M01M00처럼 중앙좌표를 구할 수 있다.

  • 중심 모멘트(Central Moments)
    mu20,mu11,mu02,mu30,mu21,mu12,mu03
    skew=MU11MU02로 비틀어진 정도를 구한다.

  • 평준화된 중심 모멘트(Central Normalized Moments)
    nu20,nu11,nu02,nu30,nu21,nu03

reference : https://m.blog.naver.com/PostView.nhn?blogId=samsjang&logNo=220516822775&proxyReferer=https%3A%2F%2Fwww.google.com%2F

튜토리얼의 deskew 결과는 다음과 같다.


import cv2 as cv
import numpy as np
SZ=20
bin_n = 16 # Number of bins
affine_flags = cv.WARP_INVERSE_MAP|cv.INTER_LINEAR
def deskew(img):
    m = cv.moments(img)
    if abs(m['mu02']) < 1e-2:
        return img.copy()
    skew = m['mu11']/m['mu02']
    M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
    img = cv.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
    return img
def hog(img):
    gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
    gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
    # cv2.cartToPolar : 2D 벡터의 magnitude와 angle을 구함
    mag, ang = cv2.cartToPolar(gx, gy)
    # (0...16)으로 양자화
    # 양자화 : 연속적으로 보이는 양을 자연수로 셀 수 있는 양으로 재해석 하는 것
    bins = np.int32(bin_n*ang/(2*np.pi))  
    # 4개의 하위 사각형으로 나누기
    bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
    mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
    hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
    hist = np.hstack(hists)     
    return hist
img = cv.imread('./images/digits.png',0)

## vsplit : row-wise
## hsplit : col-wise
cells = [np.hsplit(row,100) for row in np.vsplit(img,50)]
# First half is trainData, remaining is testData
train_cells = [ i[:50] for i in cells ]
test_cells = [ i[50:] for i in cells]

# 학습
deskewed = [list(map(deskew,row)) for row in train_cells]
hogdata = [list(map(hog,row)) for row in deskewed]
trainData = np.float32(hogdata).reshape(-1,64)
responses = np.repeat(np.arange(10),250)[:,np.newaxis]
svm = cv.ml.SVM_create()
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setType(cv.ml.SVM_C_SVC)
svm.setC(2.67)
svm.setGamma(5.383)
svm.train(trainData, cv.ml.ROW_SAMPLE, responses)
svm.save('svm_data.dat')

# 테스트
deskewed = [list(map(deskew,row)) for row in test_cells]
hogdata = [list(map(hog,row)) for row in deskewed]
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = svm.predict(testData)[1]
mask = result==responses
correct = np.count_nonzero(mask)
print(correct*100.0/result.size)

deskew 함수를 바꾸니 93.8%의 정확도가 나왔다.

결과 사진을 비교해보자. 다른 코드는 같으니 deskew과정에서 다른 모습을 볼 수 있을 것 이다.

<다른 코드>

<튜토리얼 코드>

튜토리얼의 deskew가 조금 더 특성을 살려서 0사이에 공간도 넣어주듯이 이미지를 돌렸다. 이에 비해, 다른 코드는 그저 회전만 시킨 느낌이 강하다. 그러한 점이 정확도 향상에 요인이 되지 않았나 싶다.


728x90
반응형