openCv page Dewarp 분석 -4

저번 포스트에 이어서 계속 윤곽선 정보 검출에 대해서 알아보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def get_contours(name, small, pagemask, masktype):

mask = get_mask(name, small, pagemask, masktype)

if DEBUG_LEVEL >= 3:
debug_show(name, 0.7, 'get_mask', mask)

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

contours_out = []

for contour in contours:

rect = cv2.boundingRect(contour)
xmin, ymin, width, height = rect

if (width < TEXT_MIN_WIDTH or
height < TEXT_MIN_HEIGHT or
width < TEXT_MIN_ASPECT*height):
continue

tight_mask = make_tight_mask(contour, xmin, ymin, width, height)

if tight_mask.sum(axis=0).max() > TEXT_MAX_THICKNESS:
continue

contours_out.append(ContourInfo(contour, rect, tight_mask))

if DEBUG_LEVEL >= 2:
visualize_contours(name, small, contours_out)

return contours_out

윤곽선 검출

1
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

openCV에서 윤곽선 검출은 cv2.findContours함수를 통해 이루어집니다.

기본형은 contours, hierarchy = findContours(image, mode, method, contours=None, hierarchy=None, offset=None)입니다.

리턴형은 다음을 뜻합니다.

  • contours : 검출된 윤곽선
  • hierarchy : 계층 구조

파라미터는 다음을 뜻합니다.

  • image : 이미지
  • mode : 윤곽선을 검출해 어떤 계층 구조의 형태를 사용할지 설정
    • cv2.RETR_EXTERNAL : 최외곽 윤곽선만 검색
  • method : 윤곽점의 표시 방법을 설정
    • cv2.CHAIN_APPROX_NONE : 검출된 윤곽선의 모든 윤곽점들을 좌푯값으로 반환한다. 반환된 좌푯값을 중심으로 8개의 이웃 중 하나 이상의 윤곽점들이 포함되 있습니다.
1
2
3
4
5
6
7
8
9
10
for contour in contours:

rect = cv2.boundingRect(contour)
xmin, ymin, width, height = rect

if (width < TEXT_MIN_WIDTH or
height < TEXT_MIN_HEIGHT or
width < TEXT_MIN_ASPECT*height):
continue
...

for문을 통해서 윤곽선 정보를 하나식 처리합니다.

첫번째로 cv2.boundingRect함수를 통해 윤곽선 경계의 사각형을 그립니다.

윤곽선의 경계면을 둘러싸는 사각형을 구합니다. 반환되는 결과는 회전이 고려되지 않은 직사각형 형태를 띠는데, 경계면의 윤곽점들을 둘러싸는 최소 사각형의 형태를 뜁니다.

boundingRect 예

openCV에서 좌표는 좌측 상단이 0,0 x축이 늘어나면 오른쪽으로, y축이 늘어나면 아래쪽으로 좌표가 이동됩니다. 따라서 사각형은 아래와 같이 표현할 수 있습니다.

  • xmin, ymin : 사각형 좌측 상단 좌표
  • xmin + width, ymin + height : 사각형 우측 하단 좌표

다음 조건문을 통해서 외곽선을 분석해서 너무 길거나(15이상 또는 높이의 1.5배 이상) 텍스트가 되기엔 너무 두꺼운(2이상) 얼룩은 무시합니다.

윤곽선을 담은 최소크기의 이미지를 생성

1
tight_mask = make_tight_mask(contour, xmin, ymin, width, height)

make_tight_mask을 통해서 윤곽선만을 담은 그림을 생성합니다.

1
2
3
4
5
6
7
8
9
10
def make_tight_mask(contour, xmin, ymin, width, height):

tight_mask = np.zeros((height, width), dtype=np.uint8)

tight_contour = contour - np.array((xmin, ymin)).reshape((-1, 1, 2))

cv2.drawContours(tight_mask, [tight_contour], 0,
(1, 1, 1), -1)

return tight_mask

np.zeros((height, width), dtype=np.uint8)를 통해 높이, 넓이 만큼의 0으로 채워진 배열을 생성합니다.(윤곽선을 감싼 사각형 만큼의 캔버스 느낌으로)

tight_contour = contour - np.array((xmin, ymin)).reshape((-1, 1, 2))에서 reshape는 새로운 차원의 배열을 생성합니다. 여기서 새로운 형태의 배열은 데이터의 총 갯수가 같아야합니다.

-1의 의미는 다른 요소를 먼저 설정하고 거기에 최적화된 값으로 자동 선택 됩니다. 여기서는 (x, 1, 2)가 선정되고 x값은 자동 선택됩니다.

tight_contour는 윤곽선을 좌측 상단(0,0)을 기준으로 한 좌표로 최대한 이동해서 그린 선의 좌표가 됩니다.

cv2.drawContours(tight_mask, [tight_contour], 0, (1, 1, 1), -1)은 윤곽선을 그리는 함수입니다.

기본형은 drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None)입니다.

여기서 contourIdx는 지정된 윤곽선 번호만 그림니다. 음수면 모든 윤곽선을 그립니다. 여기서는 0이기 때문에 첫번째 윤곽선을 그립니다.

결론적으로 윤곽선을 담은 최소크기의 이미지를 생성해서 리턴합니다.

윤곽선 결과 리턴

1
2
3
4
5
6
7
8
9
    if tight_mask.sum(axis=0).max() > TEXT_MAX_THICKNESS:
continue

contours_out.append(ContourInfo(contour, rect, tight_mask))

if DEBUG_LEVEL >= 2:
visualize_contours(name, small, contours_out)

return contours_out

sum(axis=0)의 뜻은 x축의 모든 요소를 더해서 배열로 만드는 것입다.

여기서는 x=0의 모든 y값을 더함. x=1의 모든 y값을 더함 …. 결국 같은 x축의 y값의 합이므로 두께가 됩니다.

max는 요소중 최댓값을 구합니다. 결국 제일 두꺼운 값을 구합니다.

text의 제일 두꺼운 길이를 10을 기준으로 했기 때문에 이보다 두꺼우면 text라 보지 않고 무시합니다.

최종적으로 append를 사용해서 contours_out에 자료를 추가한 후 리턴하게 됩니다.

ContourInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ContourInfo(object):

def __init__(self, contour, rect, mask):

self.contour = contour
self.rect = rect
self.mask = mask

self.center, self.tangent = blob_mean_and_tangent(contour)

self.angle = np.arctan2(self.tangent[1], self.tangent[0])

clx = [self.proj_x(point) for point in contour]

lxmin = min(clx)
lxmax = max(clx)

self.local_xrng = (lxmin, lxmax)

self.point0 = self.center + self.tangent * lxmin
self.point1 = self.center + self.tangent * lxmax

self.pred = None
self.succ = None

def proj_x(self, point):
return np.dot(self.tangent, point.flatten()-self.center)

def local_overlap(self, other):
xmin = self.proj_x(other.point0)
xmax = self.proj_x(other.point1)
return interval_measure_overlap(self.local_xrng, (xmin, xmax))

ContourInfo클래스는 윤곽선의 정보를 담은 클래스입니다.

input으로는 contour(윤곽선), rect(윤곽선을 둘러싼 사각형), mask(윤곽선만을 담은 작은 그림(배경은 검정(0))을 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def blob_mean_and_tangent(contour):

moments = cv2.moments(contour)

area = moments['m00']

mean_x = moments['m10'] / area
mean_y = moments['m01'] / area

moments_matrix = np.array([
[moments['mu20'], moments['mu11']],
[moments['mu11'], moments['mu02']]
]) / area

_, svd_u, _ = cv2.SVDecomp(moments_matrix)

center = np.array([mean_x, mean_y])
tangent = svd_u[:, 0].flatten().copy()

return center, tangent

blob_mean_and_tangent은 무게중심과 탄젠트값을 구하기 위한 함수입니다.

cv2.moments(contour)를 통해서 윤곽선의 모멘트값을 구합니다. 모멘트는 1XN 또는 Nx1의 형태, contour는 1xN입니다.

이미지 모멘트는 컨투어에 관한 특징값을 뜻합니다. OpenCV에서는 moments 함수로 이미지 모멘트를 구합니다. 컨투어 포인트 배열을 입력하면 해당 컨투어의 모멘트를 딕셔너리 타입으로 반환합니다. 반환하는 모멘트는 총 24개로 10개의 위치 모멘트, 7개의 중심 모멘트, 7개의 정규화된 중심 모멘트로 이루어져 있습니다.

  • Spatial Moments : M00, M01, M02, M03, M10, M11, M12, M20, M21, M30
  • Central Moments : Mu02, Mu03, Mu11, Mu12, Mu20, Mu21, Mu30
  • Central Normalized Moments : Nu02, Nu03, Nu11, Nu12, Nu20, Nu21, Nu30

moments['m00']는 0차 모멘트로서 폐곡선의 면적을 뜻합니다.

1
2
mean_x = moments['m10'] / area  
mean_y = moments['m01'] / area

위 부분은 윤곽선의 무게 중심을 구합니다.

다음은 2차 중심 모멘트의 covariance matrix(공분산 행렬)를 구합니다.
해당 설명은 여기를 참조하세요.

1
2
3
4
moments_matrix = np.array([
[moments['mu20'], moments['mu11']],
[moments['mu11'], moments['mu02']]
]) / area

_, svd_u, _ = cv2.SVDecomp(moments_matrix)를 사용해서 계산된 왼쪽 특이 벡터값을 구합니다.(calculated left singular vectors)

cv2.SVDecomp의 설명은 링크를 참조하세요.
SVD 특이값 분해설명입니다. 위키 설명
openCV의 PCA설명입니다.
PCA 설명입니다.

간단히 설명하면 PCA란 분포되어 있는 여러 데이터(좌표)의 의미있는 선 또는 축을 찾는 것 입니다.
공분산 행렬은 데이터의 분포 형태를 나타냅니다. x축과 y축의 분산은 각 변수의 분산으로 표현되고, 대각선 방향의 분산은 공분산으로 표현됩니다.

공분산을 SVD했을때

위 그림은 여기를 참조했습니다. 위 그림에서 e₁ = svd_u으로 보면 가장 분산이 큰 방향의 벡터입니다.

tangent = svd_u[:, 0].flatten().copy()에서 svd_u[:, 0]의 의미는 열에서 인덱스가 0인 값을 추출하는 것입니다.
flatten()은 배열을 1차원으로 만들어 줍니다. 결과적으로 [[x₁, y₁] [x₂, y₂]][x₁, x₂]로 변환합니다.

다음은 실제적인 한 윤곽선에 대한 결과값입니다.

1
2
3
4
5
moments_matrix =  [[86.45154677  2.15778746]
[ 2.15778746 1.94461791]]
svd_u = [[ 0.99967459 -0.02550892]
[ 0.02550892 0.99967459]]
tangent = [0.99967459 0.02550892]

self.angle = np.arctan2(self.tangent[1], self.tangent[0]) 아크탄젠트를 통해서 각도를 구합니다.
아크탄젠트의 기본형은 p = np.arctan2(y,x)입니다.

아크탄젠트로 각도 구하기

1
2
def proj_x(self, point):
return np.dot(self.tangent, point.flatten()-self.center)

np.dot()은 행렬의 곱을 뜻합니다. input으로 받는 point는 윤곽선의 좌표를 뜻합니다.
함수를 풀어보면 tangent와 윤곽선 좌표에서 무게중심을 뺀 좌표값을 핼렬의 곱 연산을 합니다. 그후 나온 결과를 proj_x로 돌려줍니다.

1
2
3
self.point0 = self.center + self.tangent * lxmin

self.point1 = self.center + self.tangent * lxmax
  • point0은 x축은 contour의 좌측끝, y축은 contour의 중간을 뜻합니다.
  • point1은 x축은 contour의 우측끝, y축은 contour의 중간을 뜻합니다.
1
2
3
4
5
6
7
    def local_overlap(self, other):
xmin = self.proj_x(other.point0)
xmax = self.proj_x(other.point1)
return interval_measure_overlap(self.local_xrng, (xmin, xmax))

def interval_measure_overlap(int_a, int_b):
return min(int_a[1], int_b[1]) - max(int_a[0], int_b[0])

local_overlap은 input값 두개의 차이값을 돌려줍니다. 여기서 차이 값은 x좌표에 한합니다.

visualize_contours

디버그를 위해 이미지에서 윤곽선을 보여주기 위해서 사용하는 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def visualize_contours(name, small, cinfo_list):

regions = np.zeros_like(small)

for j, cinfo in enumerate(cinfo_list):

cv2.drawContours(regions, [cinfo.contour], 0,
CCOLORS[j % len(CCOLORS)], -1)

mask = (regions.max(axis=2) != 0)

display = small.copy()
display[mask] = (display[mask]/2) + (regions[mask]/2)

for j, cinfo in enumerate(cinfo_list):
color = CCOLORS[j % len(CCOLORS)]
color = tuple([c/4 for c in color])

cv2.circle(display, fltp(cinfo.center), 3,
(255, 255, 255), 1, cv2.LINE_AA)

cv2.line(display, fltp(cinfo.point0), fltp(cinfo.point1),
(255, 255, 255), 1, cv2.LINE_AA)

debug_show(name, 1, 'contours', display)

regions = np.zeros_like(small) : np.zeros_like는 input array와 같은 크기를 가지는 0으로 채워진 배열을 생성합니다.

enumerate : 반복문을 사용할때 몇번째 반복문인지 알 필요가 있을때 사용합니다. j값은 0~ 순서를 나타냅니다.

cv2.drawContours(regions, [cinfo.contour], 0, CCOLORS[j % len(CCOLORS)], -1) : for문을 통해 윤곽선에 색깔을 입힙니다. 결과는 아래와 같습니다.

regions

mask = (regions.max(axis=2) != 0) : max(axis=2) : z축 기준으로 각열의 요소를 그룹으로 해서 그중 제일 큰 값을 나타냅니다.

axis에 관해서 더 알아보고 싶으면 여기를 참고 바랍니다.

display[mask] = (display[mask]/2) + (regions[mask]/2) : 색깔을 입히는 작업을 합니다. 아래 그림을 참고 하세요.

regions[mask]/2

display[mask]/2

display[mask]

마지막으로 cv2.circle을 통해 무게 중심점을 찍고 cv2.line을 통해 하얀 선을 그립니다.

윤곽선 표시

공유하기