Perspective transformation

Perspective 변환(투시 변환)은 이미지 또는 이미지 내의 객체를 나란히 직선으로 만들 때 매우 유용한 변환이다. Perspective 변환을 적용하는 아주 좋은 예시는 테이블 위의 문서를 가지런하게 만드는 것이다.

Perspective 변환을 시작하기 앞서 간단히 어떠한 방식으로 이러한 작업을 할 수 있는지 알아보자. 

우선 원본 이미지 상에서 사변형의 좌표와 최종적으로 변형된 사변형의 좌표를 알아야 한다. 두 사변형의 좌표들을 통해 이들의 관계를 변환 행렬로 나타낼 수 있다. 이 변환 행렬을 통해 원본 이미지에 적용해 원하는 결과물을 얻을 수 있다. (변환 행렬을 구하는 수식은 생략한다) 

조금 더 직관적으로 접근하자면, 우리가 책상위에 있는 인쇄물(object)을 카메라(eye)로 찍는 각도에 따라 화면(screen)상에서 보이는 인쇄물의 모양이 달라보이는 것을 상상해보면 조금 더 이해 하기 쉬울것이다. 결국 3차원 상의 객체를 2차원(평면 이미지)에 투영(projection) 하는 것이 이 포스팅에서 말하고자 하는 핵심이고, 이를 수학적으로 계산한 결과가 (변환)행렬로 나온다. 

OpenCV에서는 이 행렬을 구해주는 다음과 같은 함수를 제공한다.

Mat getPerspectiveTransform(Mat src, Mat dst)
src : 원본 이미지에서 4개의 좌표
dst : 출력 이미지에서 4개의 좌표

변환행렬을 구했다면 warpPerspective 함수를 통해 원하는 결과 영상을 얻을 수 있게 된다.

warpPerspective(Mat src, Mat dst, Mat M, Size dsize)
src : 원본 이미지
dst : 출력 이미지
M : 변환 행렬
dsize : 출력할 결과 영상의 사이즈
// A4 용지 크기: 210x297cm
val width = 210.0
val height = 297.0

// 원본 영상
val src:Mat = .. 

// 원본 이미지 상의 4개 좌표
val srcQuad = MatOfPoint2f(
    Point(58.0, 130.0),
    Point(420.0, 130.0),
    Point(88.0, 710.0),
    Point(570.0, 635.0)
)

// 출력 이미지상의 4개 좌표
val dstQuad = MatOfPoint2f(
    Point(0.0, 0.0),
    Point(width - 1, 0.0),
    Point(0.0, height - 1),
    Point(width - 1, height - 1),
)

// 투시 변환 행렬 구하기
val perspectiveTransform = Imgproc.getPerspectiveTransform(srcQuad, dstQuad)

// 인쇄물 기준으로 가지런하게 만들기
var dst = Mat()
Imgproc.warpPerspective(src, dst, perspectiveTransform, Size(0.0, 0.0))

// 인쇄물만 추출
dst = dst.submat(0, height.toInt(), 0, width.toInt())

가지런히 출력된 인쇄물 영상 (예제코드)

스캐너 앱 만들기

Perspective 변환을 통해 간단한 문서(명함) 스캐너 앱을 만들어 보자. (예제코드)

  • View에 TouchListener를 추가하여 ACTION_DOWN 이벤트 좌표에 인접한 앵커가 있는지 확인
  • 특정 앵커가 선택 되었다면 ACTION_MOVE 이벤트 시 앵커의 좌표를 갱신
  • ACTION_UP 이벤트 시의 앵커 좌표를 기록
  • ‘완료’ 버튼을 누를 때 앵커들의 좌표와 미리 입력해둔 명함 좌표를 통해 투시 변환 행렬을 계산
  • 계산 된 투시 변환 행렬을 이용하여 원본이미지를 투시 변환하여 이미지뷰에 출력

 

 

카테고리: OpenCV

1개의 댓글

ggg · 2023년 5월 11일 2:21 오후

혹시 이 예제코드 링크는 존재하지 않다고 하는데 볼 수 있는 방법은 따로 없을까요…?

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.