이 문제는 두가지 방법이 있다. 그려지는 자바스크립트를 분석해서 선과 호를 분석해서 글자를 조합하여 만든후 그 글자의 위치에 따라서 판별해내는 방법과, ocr을 인식해서 푸는 방법이 있다.
난 ocr로 풀었는데, 방법을 하나하나 설명하겠다.
준비과정.
1. 글자들이 엄청나게 작다. 글자들을 인식하려면 일반적인 ocr라이브러리로 그냥 하면 안되고, 글자 하나하나 데이터를 정의해서 특징점 등을 찾은 후 그런것들로 판별해야 한다. 단순한 ocr라이브러리는 정확도가 너무 낮으므로 성공할수가 없다.
2. 각각의 글자들에 대해 트레이닝을 해야한다. 그런데 어떤식으로 어떤 글자들을 트레이닝을 해야할까?
일반적으로 트레이닝이란 글자를 인식하기 위해 훈련하는(특징을 찾아내는) 행위를 말한다.
내가 인식할 ocr방법론은, http://qkqhxla1.tistory.com/344 요기 잘 나와있다. 이전 삽질에서 말했던건데, 저 링크의 빨간색 사각형 부분을 가져와서 인식하고, 10도 돌린 후 인식하고, 계속해서 돌리면서 인식하는 방법이다.
트레이닝이 왜 필요할까? 사진을 회전하면 원본 이미지보다 우그러짐이 발생하는데, 이 우그러짐은 글자를 판별하기 힘들게 만든다. 그러므로 '이정도 우그러져도 이 글자는 4야.' 같은 걸 알려줘야 컴퓨터는 알아듣는다. 얼마나 심하게 우그러지는지는 예전 삽질을 참고하기 바란다. (http://qkqhxla1.tistory.com/257)
그럼 어떤걸 트레이닝해야 적은 양으로 효과를 볼수 있을까? 일반적으로 사진을 회전시 0~90도는 우그러지면서 돌아가는데, 원본에서 90*n도를 회전할 때에는 깨끗하게 회전한다. 100도를 회전할때에는 일반 사진에서 10도 회전한것과 같은 우그러짐이 발생한다는 소리이다. 그렇다면 0,10,20,30,40~80도의 글자들만 트레이닝을 잘 해놓으면 110도같은경우에는 20도의 글자를 참고하고, 280도는 10도의 글자를 참고하면 된다. 그렇다면 0~80도까지의 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F모든 경우의 수를 미리 트레이닝해놓으면 된다.
일반적인 트레이닝은 구글링으로 데이터가 엄청나게 많이 들어있는 압축 파일을 다운받아서 하지만,
내가 할 트레이닝은 이 문제에 특화된 트레이닝이므로 한글자 한글자 판별해서 트레이닝을 한다.
아래는 도화지를 하나 만드는 코딩이다.
# -*- encoding: cp949 -*-
import Image
Image.new('RGB', (180,320), (0,0,0)).save('hackthissite_train.png') #트레이닝을 위한 도화지를 하나 만든다.
코드를 실행시키면 검은색 바탕의 도화지가 하나 만들어진다. 한 글자당 크기는 20 * 20이다. 도화지의 크기는 180 * 320 인데, 이 이유는 0~80도까지의 9개반복의수, 0~9,A~F의 글자의 수는 16개이기 때문에
9*20,16*20의 크기로 만든것이다.
도화지를 만들었으니 도화지에 트레이닝할 글자들을 적어넣는다.
# -*- encoding: cp949 -*-
import pyscreenshot as ImageGrab
import time,Image
import cv2
import pytesser
import urllib2
import PIL
import os
time.sleep(1)
min = [1000,1000]; max = [0,0]
im = ImageGrab.grab(bbox=(0,100,1000,860)).save('cap.png') #적당히 넓은 범위 캡쳐후 저장.
im = Image.open('cap.png').convert('RGB')
min = [1000,760]; max = [0,0]
for i in range(im.size[1]): #범위 조정을 위해 최소점과 최대점을 찾아서 그 부분을 잘라냄.
for j in range(im.size[0]):
if im.getpixel((j,i)) == (0,128,0) and i < min[0]: min[0] = i
if im.getpixel((j,i)) == (0,128,0) and j < min[1]: min[1] = j
if im.getpixel((j,i)) == (0,128,0) and i > max[0]: max[0] = i
if im.getpixel((j,i)) == (0,128,0) and j > max[1]: max[1] = j
im = Image.open('cap.png').crop((min[1],min[0],max[1]+30,max[0]+31)).save('cap.png') #이미지 위치에 맞게 적당히 잘라냄.
im = Image.open('cap.png').convert('RGB') #잘 보기위해서 글자는 흰색으로 칠함.
for i in range(im.size[1]):
for j in range(im.size[0]):
if im.getpixel((j,i)) == (0,128,0):
im.putpixel((j,i),(255,255,255))
else:
im.putpixel((j,i),(0,0,0))
im.save('cap.png')
answer = []
b = Image.open('hackthissite_train.png') #Image.new('RGB', (320,720), (0,0,0))
for i in range(0,360,10): #돌아갈 각도. 0,10,20....
img = cv2.imread('cap.png',0)
rows,cols = img.shape
M = cv2.getRotationMatrix2D((cols/2,rows/2),i,1) #이미지를 돌림.
dst = cv2.warpAffine(img,M,(cols,rows))
cv2.imwrite('cap0.png',dst) #돌린 이미지를 cap0.png라고 저장.
Image.open('cap0.png').crop((335,0,355,270)).save('cap%s.png' %str(i+1))
cnt = 1
num_list = [[15,18],[19,22],[25,28],[31,34],[36,39],[42,47],[50,57]]
minus_list = [[248,268],[228,248],[205,225],[178,198],[143,163],[103,123],[55,75]]
for num in range(7):
for i in range(0,360,10):
im = Image.open('cap%s.png' %str(i+1))
n = num_list[num][0] if i<180 else num_list[num][1]
im.crop((0,minus_list[num][0]-int((i/10)*(n/36.0)),20,minus_list[num][1]-int((i/10)*(n/36.0)))).save('num%s.png' %str(cnt))
cv2.imshow('train',cv2.imread('num%s.png' %str(cnt)))
key = cv2.waitKey(0)
print 'input =',chr(key)
if i<90: index = i
elif i<180: index = i-90
elif i<270: index = i-180
else: index = i-270
if key>=48 and key<=57:
b.paste(Image.open('num%s.png' %str(cnt)),((index/10)*20,(key-48)*20))
else:
if key>=97: key -= 32
b.paste(Image.open('num%s.png' %str(cnt)),((index/10)*20,(key-55)*20))
cnt += 1
b.save('hackthissite_train.png')
도화지를 만든 후 위 코드를 실행시키고 바로 아래 사진이 있는 문제 화면을 키면 알아서 이미지 프로세싱을 시작한다. 코드 실행 2초정도 후에 다시 눌러보면 한글자씩 나타나는걸 알 수 있다. 트레이닝을 위한 글자판별을 사람이 하는것이므로 8이 나왔다면 8을, A가 나왔다면 A를 눌러주면 트레이닝이 전부 끝난 후 위에서 만든 도화지의 적당한 위치에 그림이 붙여진다. 인덱스 계산은 위의 코드를 참조하자.
트레닝이 끝나면 hackthissite_train.png가 거의 꽉 차게 되는데, 빈칸이 있다면 문제를 새로고침해서 다른 문제그림을 가져온 후 트레이닝하자. 그러면 글자가 꽉 차는걸 볼 수 있다.
완성되면 이거처럼 된다. 이제 이걸로 트레이닝후 판별하면 된다.
트레이닝과 코딩.
# -*- encoding: cp949 -*-
import cv2
import numpy as np
SZ=20
bin_n = 16 # Number of bins
svm_params = dict( kernel_type = cv2.SVM_LINEAR,
svm_type = cv2.SVM_C_SVC,
C=2.67, gamma=5.383 )
affine_flags = cv2.WARP_INVERSE_MAP|cv2.INTER_LINEAR
def deskew(img):
m = cv2.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 = cv2.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)
mag, ang = cv2.cartToPolar(gx, gy)
bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
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) # hist is a 64 bit vector
return hist
img = cv2.imread('hackthissite_train.png',0)
cells = [np.hsplit(row,9) for row in np.vsplit(img,16)] #1,2,3,4,5~F까지 16글자이므로 16개로 나누고, 9개씩 트레이닝할것이므로 9개씩 나눔.
im = cv2.imread('num_total.png',0)
cell = [np.hsplit(row,11) for row in np.vsplit(im,23)] #num_total.png가 전체 23줄이며, 좌우로 11개의 글자가 있으므로 이렇게 나눈다.
# First half is trainData, remaining is testData
train_cells = [ i[:] for i in cells ]
test_cells = [ i[:] for i in cell]
###### Now training ########################
deskewed = [map(deskew,row) for row in train_cells]
hogdata = [map(hog,row) for row in deskewed]
trainData = np.float32(hogdata).reshape(-1,64)
responses = np.float32(np.repeat(np.arange(16),9)[:,np.newaxis]) #backs.png의 label을 정하는 작업인데 16개의글자가 9번 반복됬으므로 np.repeat(np.arange(16),9)로 쓴다.
svm = cv2.SVM()
svm.train(trainData,responses, params=svm_params)
svm.save('svm_data.dat')
###### Now testing ########################
deskewed = [map(deskew,row) for row in test_cells]
hogdata = [map(hog,row) for row in deskewed]
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = svm.predict_all(testData)
print len(result) #나온 결과 갯수 출력용
for i in range(len(result)):
if i%11==0: print
if result[i][0]<10:
print chr(int(result[i][0])+48),
else:
print chr(int(result[i][0])+55),
결과.
중간의 한글자인가 빼고 다 맞음을 확인할수 있었다. 정확도가 매우 높아졌다.
여러번 돌리다 보면 성공한다.
나중 참고용 위의 두개 코드를 합친 총 코드.
# -*- encoding: cp949 -*-
import pyscreenshot as ImageGrab
import numpy as np
import time,Image
import cv2
import pytesser
import urllib2
import PIL
import os
SZ=20
bin_n = 16 # Number of bins
svm_params = dict( kernel_type = cv2.SVM_LINEAR,
svm_type = cv2.SVM_C_SVC,
C=2.67, gamma=5.383 )
affine_flags = cv2.WARP_INVERSE_MAP|cv2.INTER_LINEAR
def deskew(img):
m = cv2.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 = cv2.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)
mag, ang = cv2.cartToPolar(gx, gy)
bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
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) # hist is a 64 bit vector
return hist
time.sleep(1) #시작
min = [1000,1000]; max = [0,0]
im = ImageGrab.grab(bbox=(0,100,1000,860)).save('cap.png') #적당히 넓은 범위 캡쳐후 저장.
im = Image.open('cap.png').convert('RGB')
min = [1000,760]; max = [0,0]
for i in range(im.size[1]): #범위 조정을 위해 최소점과 최대점을 찾아서 그 부분을 잘라냄.
for j in range(im.size[0]):
if im.getpixel((j,i)) == (0,128,0) and i < min[0]: min[0] = i
if im.getpixel((j,i)) == (0,128,0) and j < min[1]: min[1] = j
if im.getpixel((j,i)) == (0,128,0) and i > max[0]: max[0] = i
if im.getpixel((j,i)) == (0,128,0) and j > max[1]: max[1] = j
im = Image.open('cap.png').crop((min[1],min[0],max[1]+30,max[0]+31)).save('cap.png') #이미지 위치에 맞게 적당히 잘라냄.
im = Image.open('cap.png').convert('RGB') #잘 보기위해서 글자는 흰색으로 칠함.
for i in range(im.size[1]):
for j in range(im.size[0]):
if im.getpixel((j,i)) == (0,128,0):
im.putpixel((j,i),(255,255,255))
else:
im.putpixel((j,i),(0,0,0))
im.save('cap.png')
answer = []
b = Image.open('hackthissite_train.png') #Image.new('RGB', (320,720), (0,0,0))
for i in range(0,360,10): #돌아갈 각도. 0,10,20....
img = cv2.imread('cap.png',0)
rows,cols = img.shape
M = cv2.getRotationMatrix2D((cols/2,rows/2),i,1) #이미지를 돌림.
dst = cv2.warpAffine(img,M,(cols,rows))
cv2.imwrite('cap0.png',dst) #돌린 이미지를 cap0.png라고 저장.
Image.open('cap0.png').crop((335,0,355,270)).save('cap%s.png' %str(i+1))
cnt = 1
num_list = [[15,18],[19,22],[25,28],[31,34],[36,39],[42,47],[50,57]]
minus_list = [[248,268],[228,248],[205,225],[178,198],[143,163],[103,123],[55,75]]
for num in range(7):
for i in range(0,360,10):
im = Image.open('cap%s.png' %str(i+1))
n = num_list[num][0] if i<180 else num_list[num][1]
im.crop((0,minus_list[num][0]-int((i/10)*(n/36.0)),20,minus_list[num][1]-int((i/10)*(n/36.0)))).save('num%s.png' %str(cnt))
#cv2.imshow('train',cv2.imread('num%s.png' %str(cnt)))
#key = cv2.waitKey(0)
#print 'input =',chr(key)
#if i<90: index = i
#elif i<180: index = i-90
#elif i<270: index = i-180
#else: index = i-270
#if key>=48 and key<=57:
# b.paste(Image.open('num%s.png' %str(cnt)),((index/10)*20,(key-48)*20))
#else:
# if key>=97: key -= 32
# b.paste(Image.open('num%s.png' %str(cnt)),((index/10)*20,(key-55)*20))
cnt += 1
b.save('hackthissite_train.png')
im = Image.open('cap1.png')
im.crop((0,0,20,20)).save('num%s.png' %str(cnt))
sort_image = Image.new('RGB', (220,460), (255, 255, 255))
x = 0; y = 0
for i in range(1,254):
sort_image.paste(Image.open('num%s.png' %i),(x*20,y*20))
x += 1
if i%11==0:
y += 1
x = 0
sort_image.save('num_total.png')
#판별시작
img = cv2.imread('hackthissite_train.png',0)
cells = [np.hsplit(row,9) for row in np.vsplit(img,16)] #1,2,3,4,5~F까지 16글자이므로 16개로 나누고, 2개씩 트레이닝할것이므로 2개씩 나눔.
im = cv2.imread('num_total.png',0)
cell = [np.hsplit(row,11) for row in np.vsplit(im,23)] #num_total.png가 전체 23줄이며, 좌우로 11개의 글자가 있으므로 이렇게 나눈다.
# First half is trainData, remaining is testData
train_cells = [ i[:] for i in cells ]
test_cells = [ i[:] for i in cell]
###### Now training ########################
deskewed = [map(deskew,row) for row in train_cells]
hogdata = [map(hog,row) for row in deskewed]
trainData = np.float32(hogdata).reshape(-1,64)
responses = np.float32(np.repeat(np.arange(16),9)[:,np.newaxis]) #backs.png의 label을 정하는 작업인데 16개의글자가 2번 반복됬으므로 np.repeat(np.arange(16),2)로 쓴다.
svm = cv2.SVM()
svm.train(trainData,responses, params=svm_params)
svm.save('svm_data.dat')
###### Now testing ########################
deskewed = [map(deskew,row) for row in test_cells]
hogdata = [map(hog,row) for row in deskewed]
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = svm.predict_all(testData)
answer = ''
print len(result) #나온 결과 갯수 출력용
for i in range(len(result)):
if i%11==0: print
if result[i][0]<10:
print chr(int(result[i][0])+48),
answer += chr(int(result[i][0])+48)
else:
print chr(int(result[i][0])+55),
answer += chr(int(result[i][0])+55)
req = urllib2.Request('https://www.hackthissite.org/missions/prog/6/','solution='+answer)
req.add_header('cookie','PHPSESSID=sgc5l67h130qn5o7vj57sme785')
req.add_header('referer','https://www.hackthissite.org/missions/prog/6/index.php')
print '\n',urllib2.urlopen(req).read()[:10000]
for i in range(0,360,10): #만들어진 중간 파일 지우기
os.remove('cap%d.png' %(i+1))
for i in range(1,254):
os.remove('num%d.png' %i)