-
이미지 처리-3 번호판이미지 처리 2023. 6. 18. 17:28
1. 이미지 기초 처리
import pytesseract image_path = '../picture/carNum.jpg' image = cv2.imread(image_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) plt.imshow(image) plt.show()
차량 번호판 사진을 불러온다.
gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) plt.imshow(gray_image,'gray') plt.show()
그레이 스케일로 바꿔준다.
structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT,(9,9)) imgTopHat = cv2.morphologyEx(gray_image, cv2.MORPH_TOPHAT, structuringElement) imgBlackHat = cv2.morphologyEx(gray_image, cv2.MORPH_BLACKHAT, structuringElement) imgGrayscalePlusTopHat = cv2.add(gray_image, imgTopHat) gray = cv2.subtract(imgGrayscalePlusTopHat, imgBlackHat) image_gray= np.hstack([ imgTopHat, imgBlackHat, imgGrayscalePlusTopHat, gray ]) plt.imshow(image_gray, 'gray') plt.show()
np.ones의 배열을 cv2.getStructuringElement 함수의 cv2.MORPH_RECT인자로 9*9의 직사각형 모양의 배열을 쉽게 생성할 수 있다. 이것을 커널로 활용하여 tophat과 blackhat 연산을 실행한다.
원본이미지에서 tophat을 더하여 어두운 부분을 강조하고 blackhat의 이미지를 빼서 어두운 부분과 밝은 부분의 차이를 높여서 이미지를 선명하게 만든다.
contours, hierarchy = cv2.findContours( gray, mode = cv2.RETR_LIST, method = cv2.CHAIN_APPROX_SIMPLE ) h1,w1,c1 = image.shape temp_result = np.zeros((h1,w1,c1), dtype = np.uint8) cv2.drawContours(temp_result, contours = contours,contourIdx=-1,color = (255,255,255)) plt.imshow(temp_result,'gray') plt.show()
findCoutours 함수는 물체의 외곽선 정보를 검출할 수 있는 함수이다. RETR_LIST 속성은 모든 외곽선을 검출한다. 첫 번째 값은 외곽선 좌표의 값, 두 번째 값은 외곽선 계층 정보를 추출할 수 있다.
drawContours 함수를 사용하여 추출한 정보를 입력하고 이미지를 그린다. contourIdx를 -1로 지정하면 모든 외곽선을 출력한다.
2. 사각형 감지
temp_result = np.zeros((h1,w1,c1), 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) contours_dict.append({ 'contour': contour, 'x': x, 'y': y, 'w': w, 'h': h, 'cx': x+(w/2), 'cy': y+(h/2) }) plt.imshow(temp_result,'gray') plt.show()
temp_result 이미지는 위에서 구한 원본이미지의 모양을 np.zeros로 채워서 검은색 이미지를 만들고 사각형을 그릴 준비를 한다. 위에서 추출한 contours 값을 을반복하며 for문으로 사각형의 좌표를 추출한다.
cv2.boudingRect 함수는 이미지를 감싸서 작은 직사각형들의 x좌표, y좌표, 너비, 길이를 차례로 반환한다. 이 값을 cv2. rectangle의 pt1에 x,y 시작점 좌표 pt2에 x, y 종료점 좌표, 색상, 선 두께를 지정하여 사각형을 그린다.
MIN_AREA = 80 MIN_WIDTH, MIN_HEIGHT = 2,8 MIN_RATIO, MAX_RATIO = 0.25, 1.0 possible_contours = [] cnt = 0 for d in contours_dict: area = d['w'] * d['h'] ratio = d['w'] / d['h'] 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) temp_result = np.zeros((h1,w1,c1), dtype = np.uint8) for d in possible_contours: 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.imshow(temp_result, 'gray') plt.show()
모든 사각형을 추출하였기 때문에 번호판을 인식하기 위해서 조건을 걸어서 선별해야 한다. 최소 넓이, 높이 길이의 값을 설정하고 최소비율과 최대비율의 값을 설정한다. 번호판이 세로로 긴 모양이므로 너비를 길이로 나눈 값이 1보다 작고 0.25보다 크도록 설정한다.
사각형의 리스트를 넣기 위한 배열을 생성하고 갯수를 세기 위한 변수도 생성한다.
이제 for문에서 위에서 contour_dict에 저장한 높이와, 너비값을 이용하여 넓이를 계산하고 비율도 계산한다. 그리고 이 값이 위에서 생성한 조건에 부합하는 변수를 append 시켜주고 값이 추가될 때마다 cnt를 1씩 증가시켜 d['idx']에 할당한다.
마찬가지로 모양이 같은 검은 화면을 그리고 possible_contours에 있는 사각형의 정보들을 하나씩 꺼내서 사각형을 그리고 이미지를 출력한다.
MAX_DIAG_MULTIPLYER = 5 MAX_ANGLE_DIFF = 12.0 MAX_AREA_DIFF = 0.5 MAX_WIDTH_DIFF = 0.8 MAX_HEIGHT_DIFF = 0.2 MIN_N_MATCHED = 4 def find_chars(contour_list) : matched_result_idx = [] for d1 in contour_list : matched_contour_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_lenghtl = np.sqrt(d1['w'] **2 + d2['h'] **2) 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_lenghtl * 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_contour_idx.append(d2['idx']) matched_contour_idx.append(d1['idx']) if len(matched_contour_idx) < MIN_N_MATCHED : continue matched_result_idx.append(matched_contour_idx) unmatched_contour_idx = [] for d4 in contour_list : if d4['idx'] not in matched_contour_idx : unmatched_contour_idx.append(d4['idx']) unmatched_contour = np.take(possible_contours, unmatched_contour_idx) 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)) temp_result = np.zeros((h1, w1, c1), dtype=np.uint8) for r in matched_result : for d in r : x1_temp = d['x'] y1_temp = d['y'] w1_temp = d['w'] h1_temp = d['h'] cv2.rectangle(temp_result, (x1_temp,y1_temp), (x1_temp+w1_temp, y1_temp+h1_temp), (255,255,255), thickness=2) plt.imshow(temp_result, 'gray') plt.show()
이제 최종적으로 사각형의 후보군에서 번호판을 선별해야 한다. 번호판을 선별하기 위해서는 어떤 사각형이 문자를 포함하고 있는지 판단해야 한다.
find_chars는 contour_list를 후보들을 넣어주면 문자를 포함한다고 판단되는 사각형을 찾기 위한 함수를 선언한다. 모든 상자 후보들을 비교하기 위해서 같은 이미지에 대한 루프문을 두번 돌려준다. 같은 이미지를 비교하기 때문에 동일한 상자의 중복 비교를 피하기 위해서 d1과 d2의 idx값 순서가 같다면 continue로 넘겨버린다.
cx는 위에서 저장한 x에 w/2 값을 더한 가로 중심 좌표이고 cy는 세로 중심 좌표이다. 서로 다른 상자에 대해서 두 값의 차이를 절대값으로 계산하여 dx와 dy를 구한다.
diagonal_length는 d1의 가로를 제곱하고 d2 세로를 제곱하여 루트를 씌워줘서 대각선의 길이를 구한다. distance는np.linalg.norm 함수를 이용하여 두 상자의 중심점 좌표를 넣어주면 유클라디안 계산법으로 두 상자의 거리 차이를 계산해 준다.
만약 dx가 0이라면 두 상자 간의 가로 중심점의 차이가 없다면 두 상자는 동일 선상에 있다고 판단할 수 있다. 동일한 상자는 계산을 넘겼기 때문에 따라서 수직으로 위치해 있다고 판단하고 상자 간 각도 차이를 90도로 설정한다. 0이 아니라면 np.degrees(np.arctan(dy/dx) 공식을 이용하여 직접 각도 차이를 계산한다.
상자 d1과 d2의 크기 비율과 너비 비율, 길이 비율을 구한다. 절댓값을 주고 각 상자의 면적, 너비, 길이의 차이를 d1의 면적, 너비, 길이 값으로 나눠서 그 비율을 계산한다. 즉 이 비율이 작을수록 두 상자가 유사하며 클수록 다르게 생겼다는 것을 알 수 있다.
이제 원하는 상자의 조건을 주면서 상자를 선별한다. 두 상자의 중심점간 거리가 대각선의 길이 * 5보다 작아야 한다. 이 조건은 두 상자가 해당 비율보다 가까운 위치에 있어야 하는 것을 의미한다.
각도 차이는 12도보다 작아야 하며 두 상자의 각도가 비슷한 위치에 있어야 한다. 넓이 비율은 12보다 작아야 하며 면적도 비슷해야 한다. 너비 비율과 높이 비율도 각각 0.8, 0.2보다 낮은 비율을 가진 상자들만 선별한다.
이러한 조건을 부여하는 이유는 번호판을 식별하려는 목적을 달성하기 위해서이다. 번호판은 각 상자들이 비슷한 크기와 각도 등으로 구성되어 있으며 너비보다 높이에 민감하기 때문에 더 빡빡한 조건을 높이에 주는 것이다. 이 조건들을 모두 만족하는 상자들을 mached_contour_idx의 빈 리스트에 d1, d2의 인덱스를 차례로 추가한다.
만약에 이 인덱스의 개수가 4개보다 작다면 그것은 번호판이 아니라고 의심할 수 있는 조건이다. 번호판은 최소 몇 개의 박스를 가지고 있기 때문에 유사한 형태로 모은 박스 상자가 일정 개수보다 작다면 추가 작업을 하지 않고 넘겨버린다.
그리고 인덱스가 5개 이상인 박스들의 인덱스를 matched_result_idx에 추가한다. 다시 한번 contour_list를 반복하면서 만약 matched_contour_index에 속하지 않는 인덱스들을 d4에 담아서 unmatched_contour_idx 리스트에 추가한다.
np.contour 함수를 사용하여 위에서 정의한 possible_contours에서 uumatched_contour_idx에 해당하는 상자들을 unmatched_contour에 할당한다. 이 unmatched_contour을 대상으로 find_chars 함수를 진행하고 recursive_contour_list에 저장한다.
마지막으로 recursive_contour_list의 인덱스들을 matched_result_idx에 추가하고 break로 종료한다. 코드는 조건을 충족하는 matched_contour_idx를 matched_result_idx에 추가하고 조건을 충족하지 못하는 상자에 대해서 recursice_contour_list에 저장하여 find_chars 함수를 반복적으로 실행하여 matched_result_idx에 추가하는 것이다.
possible_contours에 대해 find_chars를 실행하여 result_idx에 저장하고 값들을 하나씩 가져와서 possible_contours에서 해당 인덱스에 해당하는 상자들을 matched_result에 추가한다.
빈 이미지를 생성하고 matched_result를 가져와서 x, y, w, h 좌표를 이용하여 최종적인 사각형을 그려서 출력한다.
3. 번호판 출력
PLATE_WIDTH_PADDING = 1.3 PLATE_HEIGHT_PADDING = 1.5 MIN_PLATE_RATIO = 3 MAX_PLATE_RATIO = 10 plate_img = [] 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)) rotation_matrix = cv2.getRotationMatrix2D(center = (plate_cx, plate_cy), angle=angle, scale=1.0) img_rotated = cv2.warpAffine(img_thresh, M=rotation_matrix, dsize=(w1, h1)) img_cropped = cv2.getRectSubPix( img_rotated, patchSize=(int(plate_width), int(plate_height)), center = (int(plate_cx), int(plate_cy)) ) plate_img.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.imshow(img_cropped, 'gray') plt.show()
번호판이 기울어지거나 회전이 있을 경우 이후 문자 인식이나 후속 작업을 할 때 성능을 저하시킬 우려가 있기 때문에 번호판을 회전시켜야 한다.
이미지를 회전시키기 위해 필요한 좌표와 각도를 얻기 위해서 위에서 얻은 matched_result에서 enumrate 함수로 좌표를 꺼낸다. enumrate 함수는 차례대로 인덱스를 만들어주는 함수이다. 인덱스 i와 상자정보 matched_chars를 얻을 수 있다.
matched_chars를 lambda 함수를 사용하여 cx 크기 순서대로 정렬하여 sorted_chars에 저장한다. lamda 함수는 기존의 함수를 짧은 코드로 구현할 수 있다.
sorted chars를 이용하여 박스의 정보를 크기 순서대로 정렬하였기 때문에 이것을 사용하여 번호판 중심의 x좌표와 y좌표를 구할 수 있다. 가장 첫 번째 값과 마지막 값을 더한 후 2로 나누어 중심좌표를 구한다. 마지막 값의 x좌표에 너비를 더한 후 첫 번째 값의 x를 뺀 후 1.3을 곱하여 패딩을 준 너비를 만든다.
sorted_chars에서 높이를 만들어주기 위해 각 박스의 높이를 더해서 sum_height에 저장하고 이것을 박스 개수만큼 나눈 뒤 1.5의 패딩 값을 곱해서 plate_height에 저장한다.
번호판 영역의 가장 왼쪽 값과 가장 오른쪽 값을 빼서 triangle_height에 저장하고 번호판의 왼쪽 최하단의 값과 번호판 우측 최상단의 값을 유클라디안 거리를 계산해서 triangle_hypotenus에 저장한다.
triangle_height와 triangle-hypotenus 밑변과 대각선의 길이를 알고 있으므로 np.arcsin 함수를 사용하여 역으로 각도를 구할 수 있고 이 각도를 np.degrees로 각도 값으로 변환한다.
이미지를 회전시키기 위해 getRotationMatrix2D 함수를 사용하여 회전 행렬을 생성한다. 중심 값과 각도, 스케일을 입력한다. 생성된 회전행렬을 warpAffine 함수에 이진화된 이미지, 회전행렬, 사이즈를 입력하여 이미지를 중심점을 기반으로 회전시킨다. getRectSubPix 함수를 사용하여 이미지에서 번호판 영역을 사각형으로 추출할 수 있다.
번호판의 이미지를 plate_img 리스트에 추가하고 번호판의 정보를 plate_infos 리스트에 저장한다. 마지막으로 이미지를 출력하며 중심점으로 정렬된 이미지를 확인할 수 있다.
4. 번호판 테두리 처리
for i, plate_imgs in enumerate(plate_img) : plate_imgs = cv2.resize(plate_imgs, dsize=(0,0), fx=1.6, fy=1.6) _, plate_imgs = cv2.threshold(plate_imgs, thresh=0.0, maxval = 255.0, type=cv2.THRESH_BINARY | cv2.THRESH_OTSU) contours, hierarchy = cv2.findContours(plate_imgs, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_SIMPLE) plate_min_x, plate_min_y = plate_imgs.shape[1], plate_imgs.shape[0] plate_max_x, plate_max_y = 0, 0 for contour in contours : contour_x, contour_y, contour_w, contour_h = cv2.boundingRect(contour) area_temp = contour_w * contour_h ratio_temp = contour_w / contour_h if area_temp > MIN_AREA and contour_w > MIN_WIDTH and contour_h > MIN_HEIGHT and MIN_RATIO < ratio_temp < MAX_RATIO : if contour_x < plate_min_x : plate_min_x = contour_x if contour_y < plate_min_y : plate_min_y = contour_y if contour_x + contour_w > plate_max_x : plate_max_x = contour_x + contour_w if contour_y + contour_h > plate_max_y : plate_max_y = contour_y + contour_h img_result = plate_imgs[plate_min_y : plate_max_y, plate_min_x : plate_max_x] img_result = cv2.GaussianBlur(img_result, ksize=(3,3), sigmaX=0) _,img_result = cv2.threshold(img_result, thresh=0.0, maxval = 255.0, type=cv2.THRESH_BINARY | cv2.THRESH_OTSU) img_result = cv2.copyMakeBorder(img_result, top=10, bottom=10, left=10, right=10, borderType=cv2.BORDER_CONSTANT, value=(0,0,0)) plt.imshow(img_result, 'gray') plt.show() text = pytesseract.image_to_string(img_result) print(text) 45223108
pytersseract 모듈은 이미지를 ocr 인식하는 등 여러 가지 작업에 사용된다. 주피터 노트북에서만 import 하는 것으로는 동작하지 않기 때문에 직접 pytersseract 모듈을 다운로드하여야 하며 한글 인식을 위해서는 다운로드 시 additional language에 korean을 추가해야 한다.
C:\Program Files\Tesseract-OCR 경로에 설치하였고 시스템 환경 변수에 path 추가를 눌러서 경로를 추가하여 전역적으로 실행이 가능할 수 있도록 만들어주거나 pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR 이 코
드를 삽입하여 경로를 지정해 주면 path 오류를 해결할 수 있다.
위에서 추가한 plate_img를 enumrate로 순서를 매기고 cv2.resize를 사용하여 1.6배 확대한다. cv2.threshold 함수로 이진화 작업을 실행한다. 이진화 방법과 임계값, maxvalue를 설정해 주고 임계값은 사용하지 않기 때문에 _변수에 할당해 준다.
findContours 함수를 사용하여 이진화된 plate_imgs의 윤곽을 찾는다. 윤곽 모드는 retr_list로 모든 윤곽을 반환하며 윤곽 방법을 설정한다. 윤곽의 좌표와 배열정보를 얻을 수 있다.
윤곽의 경계를 정하기 위해 이미지의 plate_min_x를 너비, plate_miny를 높이로 지정하며 plate_max_x, y는 0으로 설정한다.
boundingRect를 실행하여 contour에 대해서 x, y 좌표와 너비, 높이를 저장한다. 윤곽의 너비와 높이를 곱해서 area_temp에 할당하고 나눠서 ratio_temp에 할당한다.
최초에 설정한 min_area, min_width, min_height, min_ratio, max_ratio의 값에 충족하는 값들만 통과시키고 plate_min_x, y보다 작으면 plate_min_x,y 값과 동기화시키고 x좌표와 y좌표가 plate_max_x, y를 초과한다면 plate_max_x, y값과 동기화시켜서 최소/최대의 면적을 업데이트하여 img_result에 저장한다.
최종 이미지를 가우시안 블러처리하고 이진화를 한다. copyMakeBorder를 사용하여 이미지 주변에 경계선을 추가한다. 경계선 타입과 색상을 지정하여 이미지를 업데이트하고 출력하면 테두리가 깔끔하게 추가된 이미지를 확인할 수 있다.
출력된 이미지를 pytesseract.image_to_string 함수로 확인하면 45223108로 비슷하게 ocr 한 것을 볼 수 있다.
5. 번호판 위치 출력
info = plate_infos[-1] img_out = image.copy() cv2.rectangle(img_out, (info['x'], info['y']), (info['x'] + info['w'], info['y'] + info['h']), (255,255,0), thickness=2) plt.imshow(img_out) plt.show()
plate_infos [-1]을 사용하여 가장 마지막의 번호판의 정보를 info에 저장한다. copy로 새로 저장할 이미지로 복사해 주고 info의 좌표들을 가져와서 사각형을 노란색으로 그리고 이미지를 출력하여 확인한다.
'이미지 처리' 카테고리의 다른 글
이미지 처리-5 이미지 혼합 (0) 2023.06.19 이미지 처리-4 얼굴 이미지 회전 (0) 2023.06.19 이미지 처리-2 이미지 필터 처리 (0) 2023.06.17 이미지 처리-1 이미지 기초 처리 (0) 2023.06.14