Code Review(Drawing_Using_OpenCV)


본 장은 Drawing_Using_OpenCV에 대한 코드 리뷰 페이지이며 궁극적으로는 Drawing_Using_OpenCV를 활용하여 Table Extraction을 진행하기위한 전처리 과정입니다. Table Extraction에 대한 내용도 관심이 있다면 Table_Extraction_Kor-benchmark에 한 번 들러주세요.

더불어, 본 장에서 활용한 OpenCV 코드 설명을 보다 상세히 Code Explanation(Drawing_Using_OpenCV)에 업로드해두었습니다. 참고하며 이해하시기 바랍니다.

목차

1. image_scale 함수
2. cut_image 함수
3. search_x 함수
4. remove_horizontal & remove_vertical 함수
5. dilate_and_erode 함수
6. preprocess_image 함수
7. draw_line 함수
8. Line Drawing

img = cv2.imread(image, cv2.IMREAD_COLOR)
cv2_imshow(img)

1번

Note : 본 장에서 사용한 OpenCV 코드에 보다 대한 자세한 설명은 https://jjonhwa.github.io/2021-06-06-Code_Explanation/에서 확인할 수 있다.

1. image_scale 함수

이미지를 가공하기 위한 가장 기본적인 전처리 단계.

def image_scale(img) :
  '''
  img : numpy array형태의 image
  return : scaled image
  '''
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
  return thresh

def image_scale_sub(img) :
  '''
  img : numpy array형태의 image
  return : scaled image
  '''
  canny = cv2.Canny(img, 50, 50)
  thresh = cv2.threshold(canny, 0, 255, cv2.THRESH_OTSU)[1]
  return thresh
  • grayscale : gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  • 이미지 임계처리 : thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

grayscale - threshold 처리가 가장 기본적이며 본 장의 경우 데이터프레임기반 이미지를 가공하기 위한 단계로서 Table에서 선이 옅은 경우 다음의 edge알고리즘을 사용하여 전처리를 진행한다.

  • Edge 알고리즘을 활용한 line detect : canny = cv2.Canny(img, 50, 50)
  • 이미지 임계처리 : cv2.threshold(canny, 0, 255, cv2.THRESH_OTSU)[1]
scale_image = md.image_scale(img)
cv2_imshow(scale_image)

2번

2. cut_image 함수

이미지에서 원하는 길이만 자르기 위해 만든 함수.

본 장에서 활용하는 예시 데이터의 경우 Table이미지가 Table만 있는 것이 아니라 다른 Text들이 섞여 있기 때문에 Table만 가져와 Drawing하기 위하여 본 코드를 사용하였습니다.

만약, Table이미지에서 필요한 부분을 자르거나 발췌할 때 본 코드를 응용하여 사용할 수 있습니다.

def cut_image(scale_img, threshold = 800) :
  '''
  scale_img : 임계처리된 이미지(thresh)
  threshold : 이미지를 자르기 위한 선들 사이의 간격
  return : 원하는 길이만큼 잘려진 이미지
  '''
  horizontal_kernel = cv2.getSTructuringElement(cv2.MORPH_RECT, (81,1))
  detect_horizontal = cv2.morphologyEx(scale_img, cv2.MORPH_OPEN, horizontal_kernel, iterations = 3)
  cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  cnts = cnts[0] if len(cnts) == 2 else cnts[1]

  for i in range(len(cnts)) :
    if i == 0 :
      continue
    first_line = cv2.boundingRect(cnts[i-1])[1]
    second_line = cv2.boundingRect(cnts[i])[1]
    
    if abs(first_line - second_line) >= threshold : 
      start_line = second_line-5
      break
  clean = scale_img[start_line:, :]
  return start_line, clean
  • 구조화 커널의 생성(수평선) : horizontal_kernel = cv2.getSTructuringElement(cv2.MORPH_RECT, (81,1))
  • 열림연산을 활용한 모폴로지 변환 : detect_horizontal = cv2.morphologyEx(scale_img, cv2.MORPH_OPEN, horizontal_kernel, iterations = 3)
  • 이미지 윤곽선 검출 : cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  • 계층구조를 제외한 윤곽선만 입력 : cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  • 시작점 도출 : for문
  • 인덱싱을 활용하여 이미지 자르기 : clean = scale_img[start_line:, :]
start_line, scale_cut_image = md.cut_image(scale_image)
print(start_line)
cv2_imshow(scale_cut_image)
554

3번

3. search_x 함수

Line Drawing을 깔끔하게 하기 위한 x좌표의 최대 최소를 구하는 함수이다.

이를 응용하여 y좌표의 최대 최소 역시 구할 수 있다.

def search_x(scale_image) :
  '''
  scale_image : 임계처리된 이미지(thresh)
  return : 최대 x, 최소 x 좌표값
  '''

  vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15))
  detect_vertical = cv2.morphologyEx(scale_image, cv2.MORPH_OPEN,
                                    vertical_kernel, iterations = 3)
  cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  cnts = cnts[0] if len(cnts) == 2 else cnts[1]

  x_list = []
  for i in range(len(cnts)) :
    x_list.append(list(cv2.boundingRect(cnts[i][0])))
  
  tmp = pd.DataFrame(x_list)
  max_x = np.max(tmp[0])
  min_x = np.min(tmp[0])
  return min_x, max_x
  • 구조화 커널의 생성(수직선) : vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15))
  • 열림연산을 활용한 모폴로지 변환 : detect_vertical = cv2.morphologyEx(scale_image, cv2.MORPH_OPEN, vertical_kernel, iterations = 3)
  • 이미지 윤곽선 검출 : cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  • 계층구조를 제외한 윤곽선만 입력 : cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  • 수직선들의 x좌표 값 입력 : x_list
  • x좌표의 최대 최소 검출 : max_x, min_x
min_x, max_x = md.search_x(scale_image)
print(min_x, max_x)
47 1598

4. remove_horizontal & remove_vertical 함수

본 장에서는 Text기반 수평선 Line Drawing을 활용하여 Table without cell에서 cell을 만들어준다.

여기에서 Text만을 기준으로 Line Drawing을 하기 위하여 수직, 수평선을 삭제하는 작업을 진행한다.

def remove_horizontal(scale_image) :
  '''
  scale_image : 임계처리된 이미지(tresh)
  return : 임계처리된 이미지에서 수직선이 삭제된 이미지
  '''
  clean = scale_image.copy()
  horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
  detect_horizontal = cv2.morphologyEx(scale_image, cv2.MORPH_OPEN,
                                        horizontal_kernel, iterations  = 2)
  cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  
  for c in cnts :
    cv2.drawContours(clean, [c], -1, 0, 3)
      
  return clean

def remove_vertical(scale_image) :
  '''
  scale_image : 임계처리된 이미지(tresh)
  return : 임계처리된 이미지에서 수평선이 삭제된 이미지
  '''
  clean = scale_image.copy()
  vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15))
  detect_vertical = cv2.morphologyEx(scale_image, cv2.MORPH_OPEN,
                                      vertical_kernel, iterations = 3)
  cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  
  for c in cnts :
      cv2.drawContours(clean, [c], -1,  0, 3)
      
  return clean
  • 수직, 수평선의 구조화 커널 생성 : cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1)) - (수직선의 경우 위 코드와 같이 ksize를 (1, 15)로 변경)
  • 열림연산을 활용한 모폴로지 변환 : detect_horizontal = cv2.morphologyEx(scale_image, cv2.MORPH_OPEN, horizontal_kernel, iterations = 2)
  • 이미지 윤곽선 검출 : cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  • 계층구조를 제외한 윤곽선만 입력 : cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  • 윤곽선을 지워준다 : for문(cv2.drawContours) - scaleimage에 vertical line을 그리는 형식으로 하여 실제로는 윤곽선을 지우는 효과를 나타낸다.(scale되어 있지 않을 경우 윤곽선을 그린다.)
scale_cut_image = md.remove_horizontal(scale_cut_image)
scale_cut_image = md.remove_vertical(scale_cut_image)
cv2_imshow(scale_cut_image)

4번

5. dilate_and_erode 함수

Text를 기준으로 Line Drawing을 진행하기 위하여 수직, 수평선이 지워지고 Text만 남아있는 image에서 전처리 과정(dilate, erode)를 수행하고 윤곽값을 찾아주는 함수이다.

def dilate_and_erode(scale_image, dil_iterations = 5, erode_iterations = 5) :
  '''
  scale_image : 임계처리된 이미지(thresh)
  dil_iterations : dilate의 반복횟수
  erode_iterations : erode의 반복횟수
  return : Text의 윤곽Box 값
  '''
  kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
  dilate = cv2.dilate(scale_image, kernel, anchor = (-1, -1), iterations = dil_iterations)
  erode = cv2.erode(dilate, kernel, anchor = (-1, -1), iterations = erode_iterations)
  
  cnts = cv2.findContours(erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  cnts = cnts[0] if len(cnts) == 2 else cnts[1]
  
  return cnts
  • 구조화 커널 생성(2,2 Box) : kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
  • 팽창 연산을 활용한 모폴로지 변환 : dilate = cv2.dilate(scale_image, kernel, anchor = (-1, -1), iterations = dil_iterations)
  • 침식 연산을 활용한 모폴로지 변환 : erode = cv2.erode(dilate, kernel, anchor = (-1, -1), iterations = erode_iterations)
  • 이미지 윤곽 Box값 검출 : cnts = cv2.findContours(erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  • 계층구조를 제외한 윤곽선만 입력 : cnts = cnts[0] if len(cnts) == 2 else cnts[1]
contour = md.dilate_and_erode(scale_cut_image, 5, 2)
contour # contour에 대한 출력은 생략하도록 한다.

6. preprocess_image 함수

최종으로 dilate_and_erode에서 검출된 contour를 바탕으로 우리가 정말로 그려줄 Line에 대한 좌표값을 DataFrame형식으로 출력하는 함수

def preprocess_image(contour) :
  '''
  contour : Text의 박스 윤곽값
  return : 실제로 Line Drawing을 할 y값을 포함한 좌표 테이블
  '''
  final_list = []
  for c in contour : 
    final_list.append(list(cv2.boundingRect(c)))
      
  final_data = pd.DataFrame()
  for i in range(len(final_list)) :
    new_row = final_list[i]
    new_row = pd.DataFrame(new_row).T
    
    final_data = pd.concat([final_data, new_row])
    
  final_data.reset_index(drop = True, inplace = True)
  final_data.columns = ['x', 'y', 'w', 'h']
  
  tmp = final_data.groupby('y').agg({'h' : 'max'})
  temp = tmp.reset_index()
  
  
  drop_list = []
  for i in range(len(temp)) :
    if i == 0 :
      continue
    if abs(temp['y'][i-1] - temp['y'][i]) <= 10 and \
      abs(temp['h'][i-1] - temp['h'][i]) <= 25:
      if temp['h'][i-1] + temp['y'][i-1] >= temp['h'][i] + temp['y'][i]:
        drop_list.append(i)
      else :
        drop_list.append(i-1)
              
  temp = temp.drop(drop_list, axis = 0)
  temp.reset_index(drop = True, inplace = True)
  
  drop_list = []
  for i in range(len(temp)) :
    if i == 0 :
      continue
    if abs(temp['y'][i-1] - temp['y'][i]) <= 15 and \
      abs(temp['h'][i-1] - temp['h'][i]) <= 25:
      if temp['h'][i-1] + temp['y'][i-1] >= temp['h'][i] + temp['y'][i] :
        drop_list.append(i)
      else :
        drop_list.append(i-1)
  temp = temp.drop(drop_list, axis = 0)
  temp.reset_index(drop = True, inplace = True)

  drop_list = []
  for i in range(len(temp)) :
    if i == 0 :
      continue
    if abs(temp['y'][i-1] - temp['y'][i]) <= 25 and \
      abs(temp['h'][i-1] - temp['h'][i]) <= 25:
      if temp['h'][i-1] + temp['y'][i-1] >= temp['h'][i] + temp['y'][i] :
        drop_list.append(i)
      else :
        drop_list.append(i-1)
  temp = temp.drop(drop_list, axis = 0)
  temp.reset_index(drop = True, inplace = True)
  
  temp['yh'] = temp['y'] + temp['h']
  temp = temp.sort_values('yh')

  drop_list = []
  for i in range(len(temp['yh'])) :
    if i == 0 :
      continue
    if abs(temp['yh'][i-1] - temp['yh'][i]) <= 25 :
      drop_list.append(i-1)
  temp = temp.drop(drop_list, axis = 0)
  temp.reset_index(drop = True, inplace = True)

  temp = temp.drop(['yh'], axis = 1)
  final = pd.merge(temp, final_data)
  return final
  • 수평선을 그리기 위한 Text들의 박스값의 아래 y값 : 맨 처음 temp를 구하는 과정
  • 겹치거나 간격이 좁을 경우 맨 아래 y값만을 출력 : drop_list를 활용한 box값 drop - for문의 반복
final = md.preprocess_image(contour)
final
yhxw
0232843799
1232816674
2852465226
31182764638
411827304100
51182719250
61182716821
7166296659
82602767237
92602762221
103072764737
113542770226
124022664638
134492664638
144962764638
155442664638
165912810069
176852747722
187792965432
19874277425
2092328126045
2110172791411
2210632766621
2310632764023
24120428125821
25125428126145
26130128126145
27130128121046
28134729112912
2913863296727
3013863281426
3113863266031
3213863250925

7. draw_line 함수

preprocess_image로 부터 구해진 좌표값을 바탕으로 Gaussian Blur을 한 번 더 처리하여 실제로 그려줄 y값만을 출력하는 함수

def draw_line(image, contour, data, min_x, max_x):
  '''
  contour : 기존의 Text Box 윤곽값
  data : preprocess_image함수로 부터 생성된 좌표 테이블
  min_x : Line Drawing할 x의 최소좌표값
  max_x : Line Drawing할 x의 최대좌표값
  return : 실제로 Line Drawing할 y 좌표값
  '''
  draw_line_list = []
  for c in contour :
    for i in range(len(data)) :
      if i == len(data) - 1 :
        x = data['x'][i]
        y = data['y'][i]
        w = data['w'][i]
        h = data['h'][i]
      else :
        x_after = data['x'][i+1]
        y_after = data['y'][i+1]
        w_after = data['w'][i+1]
        h_after = data['h'][i+1]
        x_before = data['x'][i]
        y_before = data['y'][i]
        w_before = data['w'][i]
        h_before = data['h'][i]
        if abs((y_before+h_before) - (y_after + h_after)) < 25 :
          x = data['x'][i+1]
          y = data['y'][i+1]
          w = data['w'][i+1]
          h = data['h'][i+1]
        else :
          x = data['x'][i]
          y = data['y'][i]
          w = data['w'][i]
          h = data['h'][i]
      area = cv2.contourArea(c)
      if area > 40 :
        ROI = image[y:y+h, x:x+w]
        ROI = cv2.GaussianBlur(ROI, (7,7), 0)
        draw_line_list.append(y+h-2)
  return draw_line_list
  • preprocess_image함수로부터 추출된 좌표값들을 입력 : for문 > x,y,w,h값
  • 기존의 Text의 Box 윤곽값들을 바탕으로 GaussianBlur 처리를 하여 실제로 그려줄 y값만 도출 : ROI = cv2.GaussianBlur(ROI, (7,7), 0) > draw_line_list.append(y+h-2)
draw_line_list = md.draw_line(img, contour, final, min_x, max_x)
draw_line_list[:10] # List 형태로 값이 많아 10개만 출력해보도록 한다.
[49, 49, 107, 143, 143, 143, 143, 193, 285, 285]

8. Line Drawing

for i in range(len(draw_line_list)) :
  y_h = draw_line_list[i]
  cv2.line(img, (min_x, y_h+start_line), (max_x, y_h+start_line), (0,0,0), 1)
  • 실제로 기존의 image에 구해진 좌표값들을 활용해 line을 그려준다. : cv2.line(img, (min_x, y_h+start_line), (max_x, y_h+start_line), (0,0,0), 1)
cv2_imshow(img)

5번




© 2019.04. by theorydb

Powered by jjonhwa