openCv page Dewarp 분석 -6

저번 포스트에서는 윤곽선을 합치는 것에대해서 살펴보았습니다.

이번 포스트에서는 키포인트 검출에대해서 살펴보겠습니다.

sample_spans

1
span_points = sample_spans(small.shape, spans)

sample_spans 함수로 앞서 만들었던 선(span)에서 포인트가 되는 부분을 생성합니다.

input으로는 이미지의 차원 정보(smaa.shape)와 선(span)정보입니다.

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
def sample_spans(shape, spans):

span_points = []

for span in spans:

contour_points = []

for cinfo in span:

yvals = np.arange(cinfo.mask.shape[0]).reshape((-1, 1))
totals = (yvals * cinfo.mask).sum(axis=0)
means = totals / cinfo.mask.sum(axis=0)

xmin, ymin = cinfo.rect[:2]

step = SPAN_PX_PER_STEP
start = ((len(means)-1) % step) / 2

contour_points += [(x+xmin, means[x]+ymin)
for x in range(start, len(means), step)]

contour_points = np.array(contour_points,
dtype=np.float32).reshape((-1, 1, 2))

contour_points = pix2norm(shape, contour_points)

span_points.append(contour_points)

return span_points

함수를 보면 for문을 통해서 선(span)의 윤곽선 정보를 하나씩 추출합니다.

처음으로 np.arange함수를 통해서 yvals를 구합니다.

np.arange는 기본형 np.arange([start, ] stop, [step, ] dtype=None)로 stop은 필수이고 start와 step등은 선택입니다. start와 step이 설정되지 않으면 start = 0, step = 1입니다. 기능은 stop까지의 값을 array형태로 반환하는 것입니다. 여기서 input으로 들어온 값을 살펴보면 cinfo.mask의 차원을 리턴합니다. mask는 윤곽선이 그려져있는 작은 이미지입니다. 이미지 배열에서 shape를 하게되면 리턴의 뜻은 (y축, x축)입니다. 여기서는 y축의 값을 reshape를 통해 n x 1 차원의 배열로 재배열 합니다. 아래 예를 보면 이해가 빠릅니다.

1
2
3
4
5
6
cinfo.mask.shape = (5, 19)
yvals = [[0]
[1]
[2]
[3]
[4]]

다음은 y축의 중간값을 구합니다. 윤곽선은 두께가 있기 때문에 윤곽선의 중심의 y축 좌표를 구하기 위해서 계산을 합니다. 앞서 구한 y축 값을 실제 이미지에 곱해 줍니다. 그러고 곱해준 값을 x축을 기준으로한 y축의 합과 나누면 중간값(평균값)을 취득할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
totals = (yvals * cinfo.mask).sum(axis=0)
# yvals * cinfo.mask = [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
# [0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
# [2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
# [0 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3]
# [0 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0]]

# totals = [ 2 9 10 10 10 10 10 10 10 10 10 10 6 6 6 6 6 6 6]

means = totals // cinfo.mask.sum(axis=0)
# cinfo.mask.sum(axis=0) = [1 3 4 4 4 4 4 4 4 5 5 5 4 4 4 4 4 4 4]
# means = [2 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1]

다음으로 포인트의 좌표를 특정하기 위한 작업을 합니다. 처음으로 xmin, ymin = cinfo.rect[:2]를 통해 윤곽선을 둘러싸는 사각형의 좌측 상단 포인트 좌표 취득합니다. 여기서 기준이 되는 이미지 mask는 (0,0)을 기준으로한 이미지기 때문에 실제 좌표에 대입하기 위해서 xmin과 ymin을 더해줄 필요가 있습니다.

다음으로 start지점과 20px마다의 점의 좌표를 구합니다.

1
2
3
4
5
step = SPAN_PX_PER_STEP
start = ((len(means)-1) % step) // 2

contour_points += [(x+xmin, means[x]+ymin)
for x in range(start, len(means), step)]

다음으로 이미지에 대입할 실제값을 구하는 작업을 합니다. reshape((-1, 1, 2)의 의미는 opencv에서 쓰는 배열의 모양이 (x, 1, y)이기 때문입니다. 관련 자료는 여기를 참고하세요.

1
2
3
4
5
6
7
8
9
10
11
#  contour_points =  [(365, 588), (385, 587), (405, 588)]
contour_points = np.array(contour_points,
dtype=np.float32).reshape((-1, 1, 2))
# contour_points = [[[365. 588.]]
# [[385. 587.]]
# [[405. 588.]]]

contour_points = pix2norm(shape, contour_points)
# contour_points = [[[0.36753446 0.8009189 ]]
# [[0.4287902 0.7978561 ]]
# [[0.49004596 0.8009189 ]]]

span point

keypoints_from_samples

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def keypoints_from_samples(name, small, pagemask, page_outline,
span_points):

all_evecs = np.array([[0.0, 0.0]])
all_weights = 0

for points in span_points:
_, evec = cv2.PCACompute(points.reshape((-1, 2)),
None, maxComponents=1)

weight = np.linalg.norm(points[-1] - points[0])


all_evecs += evec * weight
all_weights += weight

evec = all_evecs / all_weights

x_dir = evec.flatten()

if x_dir[0] < 0:
x_dir = -x_dir

y_dir = np.array([-x_dir[1], x_dir[0]])

pagecoords = cv2.convexHull(page_outline)
pagecoords = pix2norm(pagemask.shape, pagecoords.reshape((-1, 1, 2)))
pagecoords = pagecoords.reshape((-1, 2))

px_coords = np.dot(pagecoords, x_dir)
py_coords = np.dot(pagecoords, y_dir)

px0 = px_coords.min()
px1 = px_coords.max()

py0 = py_coords.min()
py1 = py_coords.max()

p00 = px0 * x_dir + py0 * y_dir
p10 = px1 * x_dir + py0 * y_dir
p11 = px1 * x_dir + py1 * y_dir
p01 = px0 * x_dir + py1 * y_dir

corners = np.vstack((p00, p10, p11, p01)).reshape((-1, 1, 2))

ycoords = []
xcoords = []

for points in span_points:
pts = points.reshape((-1, 2))
px_coords = np.dot(pts, x_dir)
py_coords = np.dot(pts, y_dir)
ycoords.append(py_coords.mean() - py0)
xcoords.append(px_coords - px0)

if DEBUG_LEVEL >= 2:
visualize_span_points(name, small, span_points, corners)

return corners, np.array(ycoords), xcoords

keypoints_from_samples함수에서는 일단 cv2.PCACompute를 통해 고유 벡터를 추출합니다. 그 후 norm을 통해 벡터의 크기를 구합니다.(norm 참고)

정해 진 값들을 통해 span의 평균 고유벡터를 추출합니다.

1
2
3
4
5
6
7
8
9
10
11
evec = all_evecs / all_weights
# evec = [[0.999134 0.00630164]]

x_dir = evec.flatten()
# x_dir = [0.999134 0.00630164]

if x_dir[0] < 0:
x_dir = -x_dir

y_dir = np.array([-x_dir[1], x_dir[0]])
# y_dir = [-0.00630164 0.999134 ]

x_dir과 y_dir값을 보면 앞서 본 SVD(특이점 분해)에서 왼쪽 특이 벡터값과 비슷한 유형을 보입니다.

1
pagecoords = cv2.convexHull(page_outline)

cv2.convexHull는 외곽선의 오목한 부분을 볼록하게 만들어 줍니다. 아래 이미지를 참고 합니다.

convexHull

page_outline은 mask 사각형의 4꼭지점 입니다.(관련 포스트)

1
2
3
4
5
pagecoords = cv2.convexHull(page_outline)
# pagecoords1 = [[[440 633]]
# [[ 50 633]]
# [[ 50 20]]
# [[440 20]]]

cv2.convexHull(page_outline)을 통해 mask의 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
    # pagemask.shap =  (653, 490)
# pagecoords.reshape((-1, 1, 2)) = [[[440 633]]
# [[ 50 633]]
# [[ 50 20]]
# [[440 20]]]

pagecoords = pix2norm(pagemask.shape, pagecoords.reshape((-1, 1, 2)))
# pagecoords = [[[ 0.59724349 0.93874426]]
# [[-0.59724349 0.93874426]]
# [[-0.59724349 -0.93874426]]
# [[ 0.59724349 -0.93874426]]]

def pix2norm(shape, pts):
height, width = shape[:2]
scl = 2.0/(max(height, width))
#scl = 0.0030627871362940277

offset = np.array([width, height], dtye=pts.dtype).reshape((-1, 1, 2))*0.5
# offset = [[[245. 326.5]]]

return (pts - offset) * scl
# pts - offset = [[[ 195. 306.5]]
# [[-195. 306.5]]
# [[-195. -306.5]]
# [[ 195. -306.5]]]

pix2norm는 첫번째 파라미터 값(pagemask)의 높이와 폭을 각각 height, width에 저장합니다.
그후 높이와 폭중 큰값(여기선 653)을 2와 나누어 scl값을 구해줍니다.

pagemask.shap은 pagemask의 크기가 됩니다.

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
 pagecoords = pagecoords.reshape((-1, 2))
# pagecoords = [[ 0.59724349 0.93874426]
# [-0.59724349 0.93874426]
# [-0.59724349 -0.93874426]
# [ 0.59724349 -0.93874426]]

px_coords = np.dot(pagecoords, x_dir)
py_coords = np.dot(pagecoords, y_dir)
# px_coords = [ 0.60264191 -0.59081064 -0.60264191 0.59081064]
# py_coords = [ 0.93416769 0.94169492 -0.93416769 -0.94169492]

px0 = px_coords.min()
px1 = px_coords.max()

py0 = py_coords.min()
py1 = py_coords.max()

p00 = px0 * x_dir + py0 * y_dir
p10 = px1 * x_dir + py0 * y_dir
p11 = px1 * x_dir + py1 * y_dir
p01 = px0 * x_dir + py1 * y_dir

corners = np.vstack((p00, p10, p11, p01)).reshape((-1, 1, 2))
# corners = [[[-0.60559029 -0.93613581]]
# [[ 0.59682445 -0.94174861]]
# [[ 0.60559029 0.93613581]]
# [[-0.59682445 0.94174861]]]

corners를 검출하는 작업을 진행합니다. corners는 외곽선을 뜻합니다.

np.vstack은 열의 수가 같은 두 개 이상의 배열을 위아래로 연결하여 행의 수가 더 많은 배열을 만듭니다. 연결할 배열은 마찬가지로 하나의 리스트에 담아야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [39]:
b1 = np.ones((2, 3))
b1
array([[1., 1., 1.],
[1., 1., 1.]])

In [40]:
b2 = np.zeros((3, 3))
b2
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])

In [41]:
np.vstack([b1, b2])
array([[1., 1., 1.],
[1., 1., 1.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
공유하기