자동차 번호판 인식기 - Python, 이미지 프로세싱 따라하기¶
Dependencies:
- Python
- numpy
- pytesseract
- OpenCV
- matplotlib
Reference
In [3]:
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pytesseract
plt.style.use('dark_background')
In [4]:
# !pip3 install opencv-python
In [5]:
# !pip3 install pytesseract
In [6]:
img_ori = cv2.imread('./data/1.jpg')
height, width, channel = img_ori.shape
plt.figure(figsize=(12,10))
plt.imshow(img_ori, cmap='gray')
Out[6]:
<matplotlib.image.AxesImage at 0x7fbdc72bd310>
이미지를 불러오고, 이미지 컬러 체계를 변경한다. cv2.cvtColor()
In [7]:
# hsv = cv2.cvtColor(img_ori, cv2.COLOR_BGR2HSV)
# gray = hsv[:,:,2]
gray = cv2.cvtColor(img_ori, cv2.COLOR_BGR2GRAY)
plt.figure(figsize=(12,10))
plt.imshow(gray, cmap='gray')
Out[7]:
<matplotlib.image.AxesImage at 0x7f10f7c7a880>
In [8]:
structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
imgTopHat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, structuringElement)
imgBlackHat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, structuringElement)
imgGrayscalePlusTopHat = cv2.add(gray, imgTopHat)
gray = cv2.subtract(imgGrayscalePlusTopHat, imgBlackHat)
plt.figure(figsize=(12, 10))
plt.imshow(gray, cmap='gray')
Out[8]:
<matplotlib.image.AxesImage at 0x7f10f7f040d0>
Adaptive Thresholding
In [9]:
#GaussianBlur: 노이즈를 줄이기 위해서
#adaptiveThreshold: 이미지 구별 쉽게 0 or 255 로 (검정 or 흰색으로)
img_blurred = cv2.GaussianBlur(gray, ksize=(5, 5), sigmaX=0)
img_thresh = cv2.adaptiveThreshold(
img_blurred,
maxValue=255.0,
adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
thresholdType=cv2.THRESH_BINARY_INV,
blockSize=19,
C=9
)
plt.figure(figsize=(12, 10))
plt.imshow(img_thresh, cmap='gray')
Out[9]:
<matplotlib.image.AxesImage at 0x7f10f7ed48b0>
Find Contours
cv2.findContours() 윤곽선을 그린다. cv2.drawContours() 으로 contours을 그린다. contourIdx=-1 으로 전체 contours을 그려주겠다.
In [10]:
contours, _ = cv2.findContours(
img_thresh,
mode=cv2.RETR_LIST,
method=cv2.CHAIN_APPROX_SIMPLE
)
temp_result = np.zeros((height, width, channel), dtype=np.uint8)
cv2.drawContours(temp_result, contours=contours, contourIdx=-1, color=(255, 255, 255))
plt.figure(figsize=(12, 10))
plt.imshow(temp_result)
Out[10]:
<matplotlib.image.AxesImage at 0x7f10f7e31d90>
번호판 찾기¶
Prepare Data
contours_dict = [] 에 for문을 써서 저장해준다. cv2.boundingRect() 윤곽선을 감싸는 사각형을 구한다.
In [11]:
temp_result = np.zeros((height, width, channel), dtype=np.uint8)
contours_dict = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(temp_result, pt1=(x, y), pt2=(x+w, y+h), color=(255, 255, 255), thickness=2)
# insert to dict
contours_dict.append({
'contour': contour,
'x': x,
'y': y,
'w': w,
'h': h,
'cx': x + (w / 2),
'cy': y + (h / 2)
})
plt.figure(figsize=(12, 10))
plt.imshow(temp_result, cmap='gray')
Out[11]:
<matplotlib.image.AxesImage at 0x7f10f7f41250>
contour을 감싸는 사각형 모습이 나타난다.
Select Candidates by Char Size
- 어느 것이 번호판 처럼 생긴건지 걸러내는 작업을 하자
- 글자의 크기를 대략 가정
In [12]:
MIN_AREA = 80 # 번호판 윤곽선 최소 범위 지정
MIN_WIDTH, MIN_HEIGHT = 2, 8 # 최소 너비 높이 범위 지정
MIN_RATIO, MAX_RATIO = 0.25, 1.0 # 최소 비율 범위 지정
possible_contours = [] # possible_contours에 저장
cnt = 0
for d in contours_dict:
area = d['w'] * d['h']
ratio = d['w'] / d['h']
# 위에 설정한 범위의 조건을 비교, 맞추면서 다시한번 possible_contours에 저장해준다.
# 각 윤곽선의 idx값을 매겨놓고, 나중에 조건에 맞는 윤곽선들의 idx만 따로 빼낼 것이다. d['idx'] = cnt
if area > MIN_AREA \
and d['w'] > MIN_WIDTH and d['h'] > MIN_HEIGHT \
and MIN_RATIO < ratio < MAX_RATIO:
d['idx'] = cnt
cnt += 1
possible_contours.append(d)
# visualize possible contours
# possible contours의 정렬방식을 보고 번호판 후보들을 추려낸다. 번호판은 어느정도 규칙적으로 일렬로 나타난다. 순차적,각도,배열모양..
temp_result = np.zeros((height, width, channel), dtype=np.uint8)
for d in possible_contours:
# cv2.drawContours(temp_result, d['contour'], -1, (255, 255, 255))
cv2.rectangle(temp_result, pt1=(d['x'], d['y']), pt2=(d['x']+d['w'], d['y']+d['h']), color=(255, 255, 255), thickness=2)
plt.figure(figsize=(12, 10))
plt.imshow(temp_result, cmap='gray')
Out[12]:
<matplotlib.image.AxesImage at 0x7f10f7d86a60>
Select Candidates by Arrangement of Contours
In [13]:
MAX_DIAG_MULTIPLYER = 5 # 5 대각선길이
MAX_ANGLE_DIFF = 12.0 # 12.0 1번째 contour와 2번째 contour 의 각도
MAX_AREA_DIFF = 0.5 # 0.5 면적의 차이
MAX_WIDTH_DIFF = 0.8 # 너비 차이
MAX_HEIGHT_DIFF = 0.2 # 높이 차이
MIN_N_MATCHED = 3 # 3 # 위에 조건들이 3개이상 충족해야 번호판이다
# find_chars 함수로 지정한다. 나중에 재귀함수로 반복해서 찾기 위함이다. idx값 저장
def find_chars(contour_list):
matched_result_idx = []
# 이중for문으로 예를들면 첫번째 contour와 두번째 contour를 비교
for d1 in contour_list:
matched_contours_idx = []
for d2 in contour_list:
if d1['idx'] == d2['idx']:
continue
dx = abs(d1['cx'] - d2['cx'])
dy = abs(d1['cy'] - d2['cy'])
diagonal_length1 = np.sqrt(d1['w'] ** 2 + d1['h'] ** 2)
# np.linalg.norm(a - b) 벡터 a와 벡터 b 사이의 거리를 구한다.
# 삼각함수 사용
distance = np.linalg.norm(np.array([d1['cx'], d1['cy']]) - np.array([d2['cx'], d2['cy']]))
if dx == 0:
angle_diff = 90
else:
angle_diff = np.degrees(np.arctan(dy / dx))
area_diff = abs(d1['w'] * d1['h'] - d2['w'] * d2['h']) / (d1['w'] * d1['h']) #면적의 비율
width_diff = abs(d1['w'] - d2['w']) / d1['w'] # 너비의 비율
height_diff = abs(d1['h'] - d2['h']) / d1['h'] # 높이의 비율
if distance < diagonal_length1 * MAX_DIAG_MULTIPLYER \
and angle_diff < MAX_ANGLE_DIFF and area_diff < MAX_AREA_DIFF \
and width_diff < MAX_WIDTH_DIFF and height_diff < MAX_HEIGHT_DIFF:
matched_contours_idx.append(d2['idx'])
# append this contour
# 번호판 후보군의 윤곽선 개수가 3보다 작으면 번호판일 확률이 낮다. 이유는 한국 번호판은 총 7자리 이기 때문이다.
matched_contours_idx.append(d1['idx'])
# 육관선 개수가 3보다 작을때는 continue를 통해 제외
if len(matched_contours_idx) < MIN_N_MATCHED:
continue
# 최종 호부군에 넣어주기
matched_result_idx.append(matched_contours_idx)
# 아닌 것들을 다시 한번 비교하고 넣어준다.
unmatched_contour_idx = []
for d4 in contour_list:
if d4['idx'] not in matched_contours_idx:
unmatched_contour_idx.append(d4['idx'])
unmatched_contour = np.take(possible_contours, unmatched_contour_idx)
# recursive
recursive_contour_list = find_chars(unmatched_contour)
for idx in recursive_contour_list:
matched_result_idx.append(idx)
break
return matched_result_idx
result_idx = find_chars(possible_contours)
matched_result = []
for idx_list in result_idx:
matched_result.append(np.take(possible_contours, idx_list))
# visualize possible contours
temp_result = np.zeros((height, width, channel), dtype=np.uint8)
for r in matched_result:
for d in r:
# cv2.drawContours(temp_result, d['contour'], -1, (255, 255, 255))
cv2.rectangle(temp_result, pt1=(d['x'], d['y']), pt2=(d['x']+d['w'], d['y']+d['h']), color=(255, 255, 255), thickness=2)
plt.figure(figsize=(12, 10))
plt.imshow(temp_result, cmap='gray')
Out[13]:
<matplotlib.image.AxesImage at 0x7f10f4bbd5b0>
결과화면을 보면 번호판 후보군 나온 것을 확인
Rotate Plate Images
In [14]:
PLATE_WIDTH_PADDING = 1.3 # 1.3
PLATE_HEIGHT_PADDING = 1.5 # 1.5
MIN_PLATE_RATIO = 3
MAX_PLATE_RATIO = 10
plate_imgs = []
plate_infos = []
for i, matched_chars in enumerate(matched_result):
sorted_chars = sorted(matched_chars, key=lambda x: x['cx'])
# 센터 좌표 구하기
plate_cx = (sorted_chars[0]['cx'] + sorted_chars[-1]['cx']) / 2
plate_cy = (sorted_chars[0]['cy'] + sorted_chars[-1]['cy']) / 2
plate_width = (sorted_chars[-1]['x'] + sorted_chars[-1]['w'] - sorted_chars[0]['x']) * PLATE_WIDTH_PADDING
sum_height = 0
for d in sorted_chars:
sum_height += d['h']
plate_height = int(sum_height / len(sorted_chars) * PLATE_HEIGHT_PADDING)
# 번호판의 기울어진 각도를 구하기 (삼각함수 이용)
triangle_height = sorted_chars[-1]['cy'] - sorted_chars[0]['cy']
triangle_hypotenus = np.linalg.norm(
np.array([sorted_chars[0]['cx'], sorted_chars[0]['cy']]) -
np.array([sorted_chars[-1]['cx'], sorted_chars[-1]['cy']])
)
angle = np.degrees(np.arcsin(triangle_height / triangle_hypotenus))
# cv2.getRotationMatrix2D() 로테이션매트릭스를 구한다.
# cv2.warpAffine() 이미지를 변현한다.
# cv2.getRectSubPix() 회전된 이미지에서 원하는 부분만을 잘라낸다.
rotation_matrix = cv2.getRotationMatrix2D(center=(plate_cx, plate_cy), angle=angle, scale=1.0)
img_rotated = cv2.warpAffine(img_thresh, M=rotation_matrix, dsize=(width, height))
img_cropped = cv2.getRectSubPix(
img_rotated,
patchSize=(int(plate_width), int(plate_height)),
center=(int(plate_cx), int(plate_cy))
)
if img_cropped.shape[1] / img_cropped.shape[0] < MIN_PLATE_RATIO or img_cropped.shape[1] / img_cropped.shape[0] < MIN_PLATE_RATIO > MAX_PLATE_RATIO:
continue
plate_imgs.append(img_cropped)
plate_infos.append({
'x': int(plate_cx - plate_width / 2),
'y': int(plate_cy - plate_height / 2),
'w': int(plate_width),
'h': int(plate_height)
})
plt.subplot(len(matched_result), 1, i+1)
plt.imshow(img_cropped, cmap='gray')
회전 시키고, 원하는 부분만을 잘라낸 결과
'파이썬 예제' 카테고리의 다른 글
KoNLPy _ "코엔엘파이" (0) | 2021.05.18 |
---|---|
소수 구하기 with 파이썬 (0) | 2021.05.12 |
파이썬 for문으로 구구단 프로그래밍 (0) | 2021.05.09 |