Python tech/Computer Vision

[camelot] line_scale이란? (opencv로 라인 추출)

콜레오네 2021. 3. 9. 00:14

camelot은 사용자가 조절할 수 있는 파라미터가 다양합니다.

관련 포스팅 : tech-diary.tistory.com/13

파라미터를 조정해서 PDF에서 테이블을 조금 더 정확하게 추출할 수 있습니다.

 

파라미터는 lattice와 stream 방식에서 조금 차이가 있는데

그 중 lattice 방식에서 line_scale 이라는 이름의 파라미터가 있습니다.

 

이번 포스팅에서는 이 line_scale 이란 녀석이 어떤 목적인지, 어떻게 작동하는지를 분석해보겠습니다.


line_scale이 사용되는 곳은 parser.Lattice() 클래스 내부의 _generate_table_bbox() 함수 내부입니다.

path : parser > class Lattice > def _generate_table_bbox

from ..image_processing import (
    adaptive_threshold,
    find_lines,
    find_contours,
    find_joints,
)

self.image, self.threshold = adaptive_threshold(
            self.imagename,
            process_background=self.process_background,
            blocksize=self.threshold_blocksize,
            c=self.threshold_constant,
        )

위 코드는 adaptive_threshold라는 모듈을 가지고 왔네요.


그럼 adaptive_threshold는 무엇일까요?

path : image_processing > def aptive_threshold

def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2):
    img = cv2.imread(imagename) # 이미지 읽음
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 흑백 전환

    if process_background: # 배경색이 True인 경우
        threshold = cv2.adaptiveThreshold(
            gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c
        )
    else:
        threshold = cv2.adaptiveThreshold(
            np.invert(gray), # grayscale 이미지
            255, # 바이너리 최대값
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C, # 임계값 계산 방법 -가우시안 
            cv2.THRESH_BINARY, # 임계값 종류
            blocksize, # blocksize, 주변 픽셀 박스 크기
            c, # 보정 상수
        )
    return img, threshold

opencv에서 읽어들인 이미지를 binary array로 변환하고,

그 배열 값을 주변 값과 비교하여 2가지로 분류해주는 모듈입니다.

 

먼저 이미지를 흑백으로 전환시켜주고

cv2.adaptiveThreshold 모듈을 활용하여 적절한 파라미터 값을 설정합니다.

 

cv2.adaptiveThreshold( src, max_val, adaptiveMethod, thresholdType, blockSize, C )

src : image array

max_val : Binary 최대값입니다. 0과 해당 값으로 이진분류됩니다.

adaptiveMethod : 주변 값을 활용하여 임계값을 계산하는 방법입니다.

    1. cv2.ADAPTIVE_THRESH_MEAN_C : 평균으로 계산합니다.

    2. cv2.ADAPTIVE_THRESH_GAUSSIAN_C : 가우시안 방법으로 계산합니다. (중앙에 더 집중되어 계산)

thresholdType : 임계값의 종류입니다.

    1. cv2.THRESH_BINARY

    2. cv2.THRESH_BINARY_INV : 1번의 반대 (흑백 전환됩니다.)

blockSize : 주변 임계값의 범위입니다. 박스 형태로 정중앙의 픽셀값을 결정하게 됩니다. 따라서 홀수값만 올 수 있습니다. (짝수값이면 중앙값을 구할 수 없으므로...)

C : 가감 상수입니다. 결정된 값에서 더해주거나 빼주는 방식입니다. 양수면 빼게되고, 음수면 더하게 됩니다.

 

blocksize

 

이렇게 adaptive_threshold 모듈을 확인하였고, 결국 binary 분류된 nd.array가 반환되는 것을 확인하였습니다.


그 다음 단계로서 라인만 남겨주는 코드입니다.

가장 중요한 부분으로 생각되니 잘 살펴보세요.

path : parser > class Lattice > def _generate_table_bbox


vertical_mask, vertical_segments = find_lines(
                self.threshold, # image binary array threshold
                regions=regions, # 사용자가 지정한 테이블 영역(범위)
                direction="vertical", # 방향 (가로선 or 세로선)
                line_scale=self.line_scale, # line scale value
                iterations=self.iterations, # 반복 횟수
            )
            horizontal_mask, horizontal_segments = find_lines(
                self.threshold,
                regions=regions,
                direction="horizontal",
                line_scale=self.line_scale,
                iterations=self.iterations,
            )

find_lines() 모듈을 불러서 여러가지 파라미터를 전송해줍니다.


그럼 다시 find_lines() 모듈로 가보겠습니다.

path : image_processing > def find_lines

def find_lines(
    threshold, regions=None, direction="horizontal", line_scale=15, iterations=0
):
    lines = [] # line 좌표값을 저장할 리스트 변수
	# 방향으로 가로, 세로 구분
    if direction == "vertical": # 세로선
        size = threshold.shape[0] // line_scale # 페이지 세로 길이 // line_scale
        el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size)) # 구조체 정의
    elif direction == "horizontal": # 가로선
        size = threshold.shape[1] // line_scale # 페이지 가로 길이 // line_scale
        el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1))
    elif direction is None: # 방향이 다른 값이라면 > 오류 메시지 출력
        raise ValueError("Specify direction as either 'vertical' or 'horizontal'")

    if regions is not None: # 테이블 영역 파라미터가 존재한다면
        region_mask = np.zeros(threshold.shape) # 페이지 크기와 동일한 array 생성
        for region in regions: 
            x, y, w, h = region #지정한 영역의 x, y, 가로값, 세로값 
            region_mask[y : y + h, x : x + w] = 1 # 해당 영역을 1로 채워줌, 나머지는 0
        # 지정한 영역만 남기고 나머지는 모두 0으로 만들어줌 (by 곱셈 연산)
        threshold = np.multiply(threshold, region_mask) 

    threshold = cv2.erode(threshold, el) # 침식
    threshold = cv2.dilate(threshold, el) # 팽창
    dmask = cv2.dilate(threshold, el, iterations=iterations)

	# 검출한 선의 좌표 추출
    try:
        _, contours, _ = cv2.findContours(
            threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )
    except ValueError:
        # for opencv backward compatibility
        contours, _ = cv2.findContours(
            threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )
	# 추출한 좌표값을 리스트에 저장
    for c in contours:
        x, y, w, h = cv2.boundingRect(c)
        x1, x2 = x, x + w
        y1, y2 = y, y + h
        if direction == "vertical": # 기존 페이지 크기를 토대로 좌표값 계산
            lines.append(((x1 + x2) // 2, y2, (x1 + x2) // 2, y1))
        elif direction == "horizontal":
            lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2))

    return dmask, lines

 

페이지 전체 크기에서 line_scale 값으로 나누어줍니다.

결국 line_scale이란, 페이지를 몇등분하는지를 나타냅니다.

그리고 결국 그 비율보다 작은 선은 모두 제거하게됩니다.

따라서, 짧은 라인을 검출하고싶으면 line_scale을 높게 설정해야합니다.

line_scale과 감지되는 선의 길이는 반비례합니다.

line_scale과 관계

페이지 길이를 line_scale로 나눈 값을 el 변수에 저장합니다.

이때 침식 및 팽창 작용을 위해 구조체를 정의하는데, 이때 사용되는 연산은 모폴로지(Morphology) 연산입니다.

 

모폴로지 연산에서 구조체 모양을 정의해주어야 하는데 크게 3가지로 구분됩니다.

1. MORPH_RECT : 직사각형

2. MORPH_CROSS : 십자가형

3. MORPH_ELLIPSE : 타원형

 

camelot 코드에서는 직사각형을 적용하였네요.

구조체의 한쪽 side는 line_scale이 적용된 size, 한쪽 side는 1로 정의합니다.

kernel size

이것은 결국 라인 두께가 1픽셀 이상이기만 하면 모두 감지한다는 뜻입니다.

 

라인을 검출하기 전에 영역이 지정된 경우라면

해당 페이지와 동일한 array를 모두 0으로 채웁니다.

그리고 영역에 해당하는 부분을 1로 채우고

이미지 binary array와 곱셈 연산을 하게 됩니다.

regions array

그러면 영역이 지정된 부분만 남겨지고 나머지 부분은 모두 0이 되게 됩니다.

 

이후 침식과 팽창 연산을 하게 됩니다.

erode와 dilate 연산인데요. 앞서 정의한 구조체를 기준으로

erode는 and 연산, dilate는 or 연산을 한다고 보시면 됩니다.

결국 erode -> 하나라도 0이면 0, dilate -> 하나라도 1이면 1로 변환하게 됩니다.

 

다음 cv2 모듈중 하나인 findContours 모듈을 활용합니다.

findContours 모듈은 테두리(edge)를 검출해서 좌표값을 반환해줍니다.

해당 모듈을 활용하여 모든 테이블 라인을 검출한 결과를 페이지 길이와 계산하여 좌표 리스트에 저장하게 됩니다.

이 데이터를 활용하여 PDF에서 테이블이 존재하는 곳의 위치를 인식할 수 있습니다.

 

이렇게 가장 길고 중요한 find_lines()의 설명이 끝났습니다.

차례로 읽어가면서 코드와 비교해보시면 이해가 편하실 겁니다.


여기까지 camelot에서 line_scale이 적용되는 부분에 대해 코드를 뜯어보았습니다.

opencv에 대한 설명보다 camelot에서 라인을 검출하는 부분을 중점으로 설명드리다보니 opencv에 대한 설명이 부족했는데, 관련 내용은 따로 검색해보면서 학습하시길 추천드립니다.

반응형