OpenCV를 활용한 스캐너 및 스노우 앱 기능 구현 (1)

탄팥빵·2022년 1월 19일
0

Data Science

목록 보기
7/7

OpenCV

OpenCV란 Open Source Computer Vision의 약자로 영상처리와 컴퓨터 비전 관련 오픈 소스 라이브러리 입니다. OpenCV의 영상처리 및 컴퓨터 비전의 라이브러리를 사용하여 응용 프로그램을 쉽게 빠르게 만들 수 있습니다.

이번 글에서는 OpenCV를 활용하여 아래의 기능을 구현해 보겠습니다.
(1) 스캐너
(2) 스노우 앱 효과


문서 스캐너 구현

저도 종종 사용하는 앱 중에 CamScanner라는 앱이 있습니다. CamScanner를 사용하면 문서를 아무렇게나 찍어도 문서 부분만을 잡아내어 확대한 뒤 스캔 된 것처럼 보이게 하는 등 이미지를 변환해 주는 매우 유용한 앱입니다.

이와 비슷하게, 이미지 속의 대각선으로 놓여있는 문서를 똑바른 직사각형 크기의 영상으로 변환해 주는 기능을 OpenCV로 구현해 보겠습니다.

이것을 위해 구현할 기능은 아래의 3가지 입니다.
1. 마우스로 문서 모서리 선택과 이동시키기
2. 키보드 ENTER키 인식
3. 왜곡된 문서 영상을 직사각형 형태로 똑바로 펴기(투시변환 이용)

그럼 시작해 보겠습니다.

일단 관심영역을 모서리 네개로 선택할 함수를 생성합니다.

import sys
import numpy as np
import cv2

# 관심영역을 모서리 네개로 선택하는 함수
def drawROI(img, corners): # corners는 아래의 scrQuad 좌표
    cpy = img.copy() # 전송받은 이미지의 복사본을 만들어서 그 위에 그림을 그린다

    c1 = (192, 192, 255) # 모서리 색상 BGR
    c2 = (128, 128, 255) # 선 색상 BGR

    # 모서리 수 만큼 원 생성, corners 정보 이용
    for pt in corners: 
        print(pt)
        cv2.circle(cpy, (int(pt[0]), int(pt[1])), 25, c1, -1, cv2.LINE_AA)

    # 모서리를 잇는 선, 점들의 좌표는 튜플
    cv2.line(cpy, (int(corners[0][0]), int(corners[0][1])), (int(corners[1][0]), int(corners[1][1])), c2, 2, cv2.LINE_AA)
    cv2.line(cpy, (int(corners[1][0]), int(corners[1][1])), (int(corners[2][0]), int(corners[2][1])), c2, 2, cv2.LINE_AA)
    cv2.line(cpy, (int(corners[2][0]), int(corners[2][1])), (int(corners[3][0]), int(corners[3][1])), c2, 2, cv2.LINE_AA)
    cv2.line(cpy, (int(corners[3][0]), int(corners[3][1])), (int(corners[0][0]), int(corners[0][1])), c2, 2, cv2.LINE_AA)

    # addWeighted를 이용해서 입력 영상과 cpy영상에 가중치를 적용하여 투명도 적용
    disp = cv2.addWeighted(img, 0.3, cpy, 0.7, 0) # 모서리와 선 밑에 있는 글씨도 보임. 하지만 연산이 오래 걸린다

    return disp

이어서 마우스 이벤트를 처리해줄 함수를 생성합니다.

왼쪽 마우스가 눌렸을 때, 마우스를 뗐을 때, 마우스 왼쪽 버튼이 눌려있을 때의 움직임에 대한 이벤트를 인지하고 처리해줄 함수 입니다.

# 마우스 이벤트 처리
def onMouse(event, x, y, flags, param): # 외관상 5개 인자. flags는 키가 눌린 여부, param은 전송 데이터
    global srcQuad, dragSrc, ptOld, src # 전역 변수 갖고 옴

    # 왼쪽 마우스가 눌렸을 때
    if event == cv2.EVENT_LBUTTONDOWN:
        for i in range(4):
            if cv2.norm(srcQuad[i] - (x,y)) < 25: # 클릭한 점이 원 안에 있는지 확인
                dragSrc[i] = True
                ptOld = (x,y) # 마우스를 이동할때 모서리도 따라 움직이도록 설정
                break
    # 마우스를 땠다면
    if event == cv2.EVENT_LBUTTONUP:
        for i in range(4):
            dragSrc[i] = False
    
    # 마우스 왼쪽 버튼이 눌려 있을 때 모서리 움직임
    if event == cv2.EVENT_MOUSEMOVE:
        for i in range(4):
            if dragSrc[i]: # dragSrc가 True일 때
                dx = x - ptOld[0] # 이전의 마우스 점에서 dx, dy만큼 이동
                dy = y - ptOld[1]

                srcQuad[i] += (dx, dy) # 이동한 만큼 더해줌

                cpy = drawROI(src, srcQuad)
                cv2.imshow('img', cpy) # 수정된 좌표로 모서리 이동
                ptOld = (x,y) # 현재 점으로 설정
                break

이제 이미지를 읽어들입니다. 그리고 입력된 이미지의 크기와 출력될 이미지의 크기를 설정합니다.

# 입력 이미지 불러오기
src = cv2.imread('scanned.jpg')

if src is None:
    print('Image open failed!')
    sys.exit()

# 입력 영상 크기 및 출력 영상 크기
h, w = src.shape[:2]
dw = 500 # 똑바로 핀 영상의 가로 크기
dh = round(dw*297 / 210) # A4 용지 크기: 210x297cm 이용

그리고 마우스를 드래그하여 모서리를 그릴 때, 그려진 모서리 점들의 좌표와 드래그 상태를 저장할 코드를 작성합니다.

# 모서리 점들의 좌표, 드래그 상태 여부
# 내가 선택하려는 모서리 점 4개를 저장하는 넘파이 행렬, 30은 임의로 초기점의 좌표를 설정
# 완전히 구석이 아니라 모서리를 클릭할 수 있도록 자리를 둠
srcQuad = np.array([[30, 30], [30, h-30], [w-30, h-30], [w-30, 30]], np.float32) # 모서리 위치

# 반시계 방향으로 출력 방향의 위치
dstQuad = np.array([[0, 0], [0, dh-1], [dw-1, dh-1], [dw-1, 0]], np.float32)

# 4개의 점 중에서 현재 어떤 점을 드래고 하고 있나 상태를 저장, 점을 선택하면 True, 떼면 False
dragSrc = [False, False, False, False]

이어서, 모서리를 그리면 좌표를 전송하고 화면에 나타낼 코드를 작성합니다. 전송받은 좌표를 기준으로 선택한 영역의 문서 이미지에 대해 투시 변환이 적용되고 변환된 문서 이미지가 출력됩니다.

이 때, 마우스 드래그로 영역 선택이 완료되며 Enter 키를 누르면 변환과 출력이 진행되며, ESC 키를 누르면 종료됩니다.

# 모서리점, 사각형 그리기
# src에 srcQuad좌표를 전송해서 화면에 나타냄
disp = drawROI(src, srcQuad)

cv2.imshow('img', disp)
cv2.setMouseCallback('img', onMouse)

while True:
    key = cv2.waitKey()
    if key == 13: # Enter 키; 엔터키 누르면 투시 변환과 결과 영상 출력
        break
    elif key == 27: # ESC 키; 종료
        cv2.destroyWindow('img')
        sys.exit()

# 투시 변환
pers = cv2.getPerspectiveTransform(srcQuad, dstQuad) # 3X3 투시 변환 행렬 생성
dst = cv2.warpPerspective(src, pers, (dw, dh), flags=cv2.INTER_CUBIC) # 가로 세로 크기는 자동

# 결과 영상 출력
cv2.imshow('dst', dst)

while True:
    if cv2.waitKey() == 27:
        break

cv2.destroyAllWindows()

다 작성된 코드를 실행시키면 아래와 같이 불러온 이미지가 뜨며, 붉은색 좌표를 드래그하여 변환할 문서 영역을 설정해 줍니다.

영역 지정이 완료되었다면, Enter를 누릅니다. 그러면 아래와 같이 직사각형의 문서 형태로 변환된 이미지가 출력됩니다.

마치 실제 종이를 스캔한 것처럼 보입니다. 신기하네요. OpenCV를 통해 어플로만 사용하던 기능을 이렇게 쉽게 구현할 수 있다는 것이 대단한 것 같습니다.

그럼 다음에는 OpenCV를 활용해 스노우 어플 효과를 구현해 보겠습니다.

0개의 댓글