openCv page Dewarp remapping 분석

개요

해당 작업은 최적화 작업 후 기울기 α, β를 취득 한 후 어떻게 이미지가 remmaping 되는가에 대한 설명이다.

get_page_dims

get_page_dims 전체 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_page_dims(corners, rough_dims, params):

dst_br = corners[2].flatten()

dims = np.array(rough_dims)

def objective(dims):
proj_br = project_xy(dims, params)
return np.sum((dst_br - proj_br.flatten())**2)

res = scipy.optimize.minimize(objective, dims, method='Powell')
dims = res.x

print ("got page dims", dims[0], 'x', dims[1])

return dims

get_page_dims 분석

input parameter는 다음과 같다.

  • corners : PCA를 통해 결정된 remapping될 이미지의 4개의 꼭지점의 좌표. debugmode에서 붉은 사각형으로 표시된다.
  • rough_dims : corners의 넓이와 높이 값
  • params : 최적화를 통해 기울기 α, β값등이 들어있는 리스트

corners 표시한 사각형

해당 작업의 진행은 아래와 같다.

1
2
3
4
5
def objective(dims):
proj_br = project_xy(dims, params)
return np.sum((dst_br - proj_br.flatten())**2)

res = scipy.optimize.minimize(objective, dims, method='Powell')

proj_br = project_xy(dims, params)

dims(이미지를 만들 4개의 꼭지점 좌표)를 최적화를 통해 구한 α, β를 이용해 재투영하여 2D 좌표를 얻는다.

scipy.optimize.minimize(objective, dims, method=’Powell’)

4개의 꼭지점에 대해서 재투영된 좌표와 원래의 좌표를 비교한다. 비교 후 재투영 오류가 가장 작은 꼭지점의 좌표를 찾는다.

최종적으로 재투영 오류가 가장 적은 dims(넓이, 높이)값을 찾는 것이 목표이다.

remap_image

remap_image 전체 소스

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
def remap_image(name, img, small, page_dims, params):

height = 0.5 * page_dims[1] * OUTPUT_ZOOM * img.shape[0]
height = round_nearest_multiple(height, REMAP_DECIMATE)

width = round_nearest_multiple(height * page_dims[0] / page_dims[1],
REMAP_DECIMATE)

print ' output will be {}x{}'.format(width, height)

height_small = height / REMAP_DECIMATE
width_small = width / REMAP_DECIMATE

page_x_range = np.linspace(0, page_dims[0], width_small)
page_y_range = np.linspace(0, page_dims[1], height_small)

page_x_coords, page_y_coords = np.meshgrid(page_x_range, page_y_range)

page_xy_coords = np.hstack((page_x_coords.flatten().reshape((-1, 1)),
page_y_coords.flatten().reshape((-1, 1))))

page_xy_coords = page_xy_coords.astype(np.float32)

image_points = project_xy(page_xy_coords, params)
image_points = norm2pix(img.shape, image_points, False)

image_x_coords = image_points[:, 0, 0].reshape(page_x_coords.shape)
image_y_coords = image_points[:, 0, 1].reshape(page_y_coords.shape)

image_x_coords = cv2.resize(image_x_coords, (width, height),
interpolation=cv2.INTER_CUBIC)

image_y_coords = cv2.resize(image_y_coords, (width, height),
interpolation=cv2.INTER_CUBIC)

img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

remapped = cv2.remap(img_gray, image_x_coords, image_y_coords,
cv2.INTER_CUBIC,
None, cv2.BORDER_REPLICATE)

remap_image 분석

height = 0.5 * page_dims[1] * OUTPUT_ZOOM * img.shape[0]

여기서 page_dims[1]의 값은 remapping될 이미지의 높이의 값이다.

여기서 값은 픽셀이 아닌 scl = 2.0/(max(height, width))을 나눈 값으로 대략 1.x의 값을 가진다.

해당 값(비율)과 원래의 이미지 높이를 곱하고 2를 나누어줘 높이를 구한다.

height = round_nearest_multiple(height, REMAP_DECIMATE)

높이를 다운 스케일링을 위한 요소의 배수와 가장 가까운 값으로 올림한다.

다운스케일링시 나머지가 생기지 않도록 하기 위해서다.

width = round_nearest_multiple(height * page_dims[0] / page_dims[1], REMAP_DECIMATE)

넓이는 높이를 구했으므로 앞서 구한 리맵핑될 이미지의 넓이와 높이의 비를 곱해 간단히 구한다.

여기서도 다운스케일링을 위한 올림을 한다.

이미지에서 width, height

np.linspace

1
2
3
4
5
height_small = height // REMAP_DECIMATE
width_small = width // REMAP_DECIMATE

page_x_range = np.linspace(0, page_dims[0], width_small)
page_y_range = np.linspace(0, page_dims[1], height_small)

다운스케일링을 한 높이와 넓이를 구한다. 기본 REMAP_DECIMATE의 값은 16이다.

np.linspace는 등간격을 뜻한다. numpy.linspace(start, end, num=개수, endpoint=True, retstep=False, dtype=자료형)을 사용하여 start ~ end 사이의 값을 개수만큼 생성하여 배열로 반환한다.

endpoint가 True일 경우 end의 값이 마지막 값이 되며, False일 경우 end의 값을 마지막 값으로 사용하지 않는다.

retstep이 True일 경우 값들의 간격을 배열에 포함된다.

page_x_coords, page_y_coords = np.meshgrid(page_x_range, page_y_range)

numpy의 meshgrid 명령을 통해 행단위와 열단위로 각각 해당 배열을 정방(square) 행렬로 선언한다.

page_x_range.shape = (102,), page_y_range.shape = (170,)라면

page_x_coords.shape = (170, 102), page_y_coords.shape = (170, 102)의 형태로 변경시켜준다.

np.hstack

1
2
3
4
5
6
7
page_x_coords.flatten().reshape((-1, 1)) =  [[0.        ]
[0.00928807]
[0.01857613]
...
[0.91951848]
[0.92880654]
[0.93809461]]

형태 변환을 통해 위와 같은 형태로 변경시켜 준다.

np.hstack은 배열을 위에서 아래로 붙이는 작업이다.

해당 작업의 결과물은 아래와 같다.

1
2
3
4
5
6
7
page_xy_coords =  [[0.         0.        ]
[0.00928807 0. ]
[0.01857613 0. ]
...
[0.91951848 1.57070085]
[0.92880654 1.57070085]
[0.93809461 1.57070085]]

image_points 취득

1
2
image_points = project_xy(page_xy_coords, params)
image_points = norm2pix(img.shape, image_points, False)

이제 위에서 얻은 좌표를 통해 재투영 작업을 한다. 재투영한 값은 norm의 값이므로 norm2pix 함수를 통해 실제 좌표로 변경한다.

이를 그림으로 표현하면 아래와 같다.

이미지에서 결과물 설명

위 그림에서 초록색 점은 page_xy_coords의 좌표를 표현한 것이다. 개념 설명을 위한 것으로 점의 갯수는 무시한다.

점의 간격은 높이는 height_small, 넓이는 width_small만큼 떨어져 있다.

이 좌표는 결과물의 좌표를 표시하기 위한것이다. 그러므로 좌측 상단의 좌표는 (0,0), 우측하단은 (page_dims[0], page_dims[1])의 좌표로 매칭된다.

결과적으로 remapping을 위한 좌표 표시를 위한 행렬을 만든 결과가 page_xy_coords인 것이다.

여기서 page의 기준점 (0,0)의 좌표가 어디서 결정되는지 보면 main()함수에 아래와 같은 소스가 존재한다.

1
2
dstpoints = np.vstack((corners[0].reshape((1, 1, 2)),) +
tuple(span_points))

여기서 coners[0]은 page_dims의 좌측 상단 좌표이다. 이점을 span point의 가장 처음에 넣고 해당 점의 포인트가 (0,0)으로 매칭되서 최적화 되도록 아래 함수에서 설정한다.

1
2
3
4
5
6
def project_keypoints(pvec, keypoint_index):

xy_coords = pvec[keypoint_index]
xy_coords[0, :] = 0

return project_xy(xy_coords, pvec)

여기서 xy_coords[0, :] = 0이 앞서 dstpoints의 첫번째 값인 coners[0]값과 대응된다.

결과적으로 coners[0]값이 remapping 후 (0,0)이 되도록 결정되는 것이다.

remapping시 기준은 coners[0]값 (0,0)이되게 하고 coners의 높이와 넓이만큼 remapping을 하기 때문에 자동으로 우리가 원하는 지점에 대한 output을 얻을 수 있다.

해당 점은 디버그 모드시 확인할 수 있다.

coners[0]

resize

1
2
3
4
5
6
7
8
image_x_coords = image_points[:, 0, 0].reshape(page_x_coords.shape)
image_y_coords = image_points[:, 0, 1].reshape(page_y_coords.shape)

image_x_coords = cv2.resize(image_x_coords, (width, height),
interpolation=cv2.INTER_CUBIC)

image_y_coords = cv2.resize(image_y_coords, (width, height),
interpolation=cv2.INTER_CUBIC)

현재까지 좌표 image_x_coords, image_y_coords는 원본이미지가 small = resize_to_screen(img)로 축소된 이미지를 대상으로 변경한 것이다.

원본 이미지의 좌표에 대한 것으로 remapping을 하기 위해 cv2.resize를 통해 좌표를 변환해준다.

cv2.remap

이제 이동할 좌표를 모두 특정하였다. remap 함수에 대해서 설명하면 기본형은 다음과 같다.

cv2.remap(src, map1, map2, interpolation[, dst[, borderMode[, borderValue]]]) → dst

input 파라미터를 설명하면 아래와 같다.

  • src : 원본 이미지
  • map1 : (x,y) 포인트 또는 x 값
  • map2 : y값
  • interpolation : 보간법
    • INTER_NEAREST - a nearest-neighbor interpolation
    • INTER_LINEAR - a bilinear interpolation (used by default)
    • INTER_AREA - resampling using pixel area relation. It may be a preferred method for image decimation, as it gives moire’-free results. But when the image is zoomed, it is similar to the INTER_NEAREST method.
    • INTER_CUBIC - a bicubic interpolation over 4x4 pixel neighborhood
    • INTER_LANCZOS4 - a Lanczos interpolation over 8x8 pixel neighborhood
  • borderMode : 픽셀 외삽 법(Pixel extrapolation method).
    • BORDER_TRANSPARENT` : 소스 이미지의 “특이치(outliers)”에 해당하는 대상 이미지의 픽셀이 함수에 의해 수정되지 않음을 의미.
    • BORDER_CONSTANT : 이미지를 일정한 값으로 채움 (예 : 검은 색 또는 0)
    • BORDER_REPLICATE : 원본의 가장 자리에있는 행 또는 열이 추가 테두리에 복제
  • borderValue : 경계가 일정한 경우에 사용되는 값. 기본적으로 0.

remapped = cv2.remap(img, image_x_coords, image_y_coords, cv2.INTER_CUBIC, None, cv2.BORDER_REPLICATE)

해당 함수를 보면 보간법은 INTER_CUBIC, 픽셀 외삽법은 BORDER_REPLICATE로 가장자리에 있는 행 또는 열이 복제가 되는 것을 지정하였다.

remapping에서 가장 중요한 것은 map1과 map2이다.

map은 map1에 x좌표 또는 x,y좌표, map2에는 y좌표가 들어가 있다.

map의 내용을 분석하기 위해서 image_x_coords을 print로 하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
image_x_coords = [[1082.8455 1082.7078 1082.647  ... 2433.1545 2433.1294 2433.0715]
[1082.8472 1082.7097 1082.6489 ... 2433.164 2433.139 2433.0808]
[1082.848 1082.7104 1082.6497 ... 2433.1682 2433.143 2433.085 ]
...
[1058.0674 1057.9417 1057.8861 ... 2305.5059 2305.4805 2305.4229]
[1058.0681 1057.9423 1057.8867 ... 2305.5093 2305.4841 2305.4263]
[1058.0697 1057.9438 1057.8884 ... 2305.5176 2305.4922 2305.4346]]

image_x_coords.shape = (1920, 1488)

remapped.shape = (1920, 1488, 3)

일단 shape를 보면 image_x_coords와 remapping 결과물인 remapped의 shape가 같은 것을 볼 수 있다.

remapped.shape의 마지막 3은 RGB의 값이므로 높이=1920, 넓이=1488의 지도를 나타내는 것이 image_x_coords라고 할 수 있다.

그럼 image_x_coords의 내용의 의미는 무엇일까?

바로 원본 이미지의 좌표값이다.

image_x_coords[0][0] = 1082.8455 이다. 해당 값은 resizing등 여러 계산에 의한 결과이기 때문에 실수로 표현되었다. 실제적으로 좌표의 값은 정수이다.

그러므로 image_x_coords[0][0] = 1082로 보면 된다.

이 뜻은 remapped의 이미지의 (0,0)의 좌표의 값의 x값이 원본 소스의 x값 1082와 매칭된다는 것을 뜻한다.

똑같은 의미로 image_y_coords[0][0]의 값의 의미는 remapped의 이미지의 (0,0)의 좌표의 값의 y값이 원본 소스의 y값 image_y_coords[0][0]의 값과 매칭된다는 것을 뜻한다.

실제적으로 remapped 이미지의 (0,0)의 RGB값은 원본 소스의 (image_x_coords[0][0],image_y_coords[0][0])의 RGB값이라는 의미이다.

image_x_coords[0][0] = 1082, image_y_coords[0][0] = 1003이고 해당 좌표의 원본이미지의 RGB값이 (125,23,25)일때 remapping의 결과물은 아래 그림과 같다.

remapping

convertMaps

remap에서 x,y좌표가 따로 있기 때문에 파악이 힘든 경우가 있다. 이와 같은 경우 cv2.convertMaps을 사용할 수 있다.

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
mapXY, _ = cv2.convertMaps(image_x_coords, image_y_coords, cv2.CV_16SC2)

print(mapXY)

[[[1082 1003]
[1082 1003]
[1082 1003]
...
[2433 1070]
[2433 1070]
[2433 1070]]

[[1082 1003]
[1082 1003]
[1082 1003]
...
[2433 1070]
[2433 1070]
[2433 1070]]

[[1082 1003]
[1082 1003]
[1082 1003]
...
[2433 1070]
[2433 1070]
[2433 1070]]

...

[[1058 2804]
[1057 2804]
[1057 2804]
...
[2305 2726]
[2305 2726]
[2305 2726]]

[[1058 2804]
[1057 2804]
[1057 2804]
...
[2305 2726]
[2305 2726]
[2305 2726]]

[[1058 2804]
[1057 2804]
[1057 2804]
...
[2305 2726]
[2305 2726]
[2305 2726]]]

위와 같이 x,y의 좌표가 매칭되서 결과를 확인할 수 있다.

mapXY[0][0]의 값 [1082 1003]은 remaping 이미지의 (0,0)의 RGB값이 원본 소스 (1082,1003)의 RGB와 매칭된다는 것을 뜻한다.

공유하기