머신러닝/openCV

OpenCV 기초, 스캐너 만들기

Olivia-BlackCherry 2025. 2. 23. 16:41

 

단계

#Steps:
#1. Convert the Image to Gray Scale
#2. Find the edges in the image using Canny Edge Detector
#3. Find and Draw the Contours
#4. Find the Corner Points
#5. Apply Warp Perspective

 

 

 

0. 이미지 불러오기

#Import All the Required Libraries
import cv2
import numpy as np

#Read the image using OpenCV
image = cv2.imread("../Resources/Images/documentscanner.jpg")

#Display the Image
cv2.imshow("Image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

사이즈 줄이기

이유 : 크기 표준화 / 해상도 줄이기 / 출력화면에 맞추기

#resize(원본, (가로, 세로))
image = cv2.resize(image, (768, 800))
#shape (세로, 가로, 채널)
print("Image shape", image.shape, image.size)

 

 

이미지 카피해놓기

cv2.imshow("Image Contour", imageDrawContour)

 

 

1. 이미지를 그레이스케일로 바꾸기

그레이스케일 = 흑백

컬러 정보를 없애면 연산이 단순해지고, 엣지 감지가 효과적임

imageGray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

 

 

2. 엣지 검출기로 경계 찾기

엣지 = 경계선

이미지에서 경계 부분만 강조하여 검출하기

낮은 임계값과 높은 임계값을 설정하여 강한 엣지만 남긴다.

하한 임계값 = threshold1 : 이 값보다 작은 픽셀들은 엣지로 간주하지 않음 : 노이즈 줄임

상한 임계값 = threshold2 : 이 값보다 큰 픽셀들은 엣지로 확정 : 중요한 엣지 검출

일반적으로는 하한100, 상한 200으로 잡음

각 이미지 특징에 대해 경계값을 설정하는 방법은 아주 다양하기 때문에 그때마다 적이하게 사용할 것.

imageCanny = cv2.Canny(imageGray, 200, 200)

 

def preprocessing(image):
    # 1. Convert the Image to Gray Scale
    imageGray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 2. Find the edges in the image using Canny Edge Detector
    imageCanny = cv2.Canny(imageGray, 200, 200)
    return imageCanny
    
imageCanny = preprocessing(image)
cv2.imshow("Image Canny", imageCanny)

 

 

 

3. 컨투어 찾고 그리기

컨투어 = 연속된 경계선

검출된 엣지를 기반으로 컨투어를 찾고 객체를 감싸는 선을 그린다.

cv2/findContours(image, mode, method)

image: 반드시 흑백

mode: 윤곽선 찾는 방법 - external 바깥쪽..등등!

method: 윤곽선을 근사화하는 방법: chain_approx_none 윤곽선의 모든 점을 저장(많은 점), simple 직선 부분은 끝 점만 저장(메모리 절약)

def findDrawContours(imageCanny):
    #3. Find and Draw the Contours
    #가장 바깥쪽 윤곽선만 검출 : cv2.RETR_EXTERNAL
    #꼭 필요한 점들만 남겨 컨투어 단순화 : cv2.CHAIN_APPROX_NONE
    #Contour : 윤곽선 리스트
    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        cv2.drawContours(imageCopy, cnt, -1, (255, 0,0), 2)
    return imageCopy
    
 imageDrawContour = findDrawContours(imageCanny)

cv2.imshow("Image Contour", imageDrawContour)

 

 

위에 사진을 보면 contour가 내가 관심있는 문서 뿐만이 아니라 바깥 배경화면에도 이리저리 파란 점이 찍혀있다. 따라서 문서만 제외하고 나머지는 포함되지 않도록 area면적을 이용해 코드를 수정한다.

    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        area = cv2.contourArea(cnt)
        print(area)

contour리스트 중 유독 큰 면적인 것이 보인다. 그것이 바로 문서의 영역이다.

 

def findDrawContours(imageCanny):
    #3. Find and Draw the Contours
    #가장 바깥쪽 윤곽선만 검출 : cv2.RETR_EXTERNAL
    #꼭 필요한 점들만 남겨 컨투어 단순화 : cv2.CHAIN_APPROX_NONE
    #Contour : 윤곽선 리스트
    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 30000:
            cv2.drawContours(imageCopy, cnt, -1, (255, 0,0), 2)
    return imageCopy

 

 

 

4. 코너 포인트 찾기

코너 = 꼭짓점

검출된 컨투어에서 꼭짓점을 찾는다.

1) cv2.arcLength() 객체의 둘레 perimeter 구하기

peri = cv2.arcLength(cnt, True)

2) epsilon 값을 설정 : 원본 윤곽선과 근사 윤곽선 사이의 최대 오차 허용량

3) cv2.approxPolyDP() 윤곽선을 단순화하여 최소한의 꼭짓점 남김

approx = cv2.approxPolyDP(cnt, 0.02*peri, True)

approx

 

 

4) 그리기

cv2.drawContours(imageCopy, approx, -1, (255, 255,0), 10)

 

< 전체 코드>

def findDrawContours(imageCanny):
    #3. Find and Draw the Contours
    #가장 바깥쪽 윤곽선만 검출 : cv2.RETR_EXTERNAL
    #꼭 필요한 점들만 남겨 컨투어 단순화 : cv2.CHAIN_APPROX_NONE
    #Contour : 윤곽선 리스트
    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 30000:
            cv2.drawContours(imageCopy, cnt, -1, (255, 0,0), 2)
            # 4. Find the Corner Points
            #윤곽선 길이 계산 - 객체의 둘레 :perimeter 구하기
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, 0.02*peri, True)
            print(len(approx))
            cv2.drawContours(imageCopy, approx, -1, (255, 255,0), 10)
    return imageCopy

 

 

최종 코너포인트 찾기 

def findDrawContours(imageCanny):
    maxarea=0
    #특정한 윤곽선(예: 가장 큰 사각형)을 찾을 때 초기값을 설정하는 용도
    biggest = np.array([])
    #3. Find and Draw the Contours
    #가장 바깥쪽 윤곽선만 검출 : cv2.RETR_EXTERNAL
    #꼭 필요한 점들만 남겨 컨투어 단순화 : cv2.CHAIN_APPROX_NONE
    #Contour : 윤곽선 리스트
    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 30000:
            cv2.drawContours(imageCopy, cnt, -1, (255, 0,0), 2)
            # 4. Find the Corner Points
            #윤곽선 길이 계산 - 객체의 둘레 :perimeter 구하기
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, 0.02*peri, True)
            print(len(approx))
            # print(approx)
            if area >maxarea and len(approx)==4:
                biggest = approx
                maxarea = area
            cv2.drawContours(imageCopy, approx, -1, (255, 255,0), 10)
    return biggest, imageCopy


#Display the Image
imageCanny = preprocessing(image)
biggest, imageDrawContour = findDrawContours(imageCanny)
print("Corner Points", biggest)

 

 

 

5. 원근 변환 적용

Warp Perspective = 원극 변환

검출된 꼭짓점으로 이미지를 사각형 형태로 변환한다. 

문서를 스캔할 때 왜곡된 종이를 평평하게 만들 때 사용함

 

1) 가로, 세로 길이 정하기

#resize(원본, (가로, 세로))
image = cv2.resize(image, (768, 800))
#shape (세로, 가로, 채널)
print("Image shape", image.shape, image.size)
imageCopy = image.copy()
width = 768
height = 800

 

2) 원본이미지를 원근변환

def getWarp(image, biggest):
    # 원본 이미지에서 변환할 4개의 좌표
    pts1 = np.float32(biggest)
    #변환 후 새로운 4개 꼭짓점 (새로운 크기로 조정)
    pts2 = np.float32([[0,0], [width, 0], [0, height], [width, height]])
    #원근 변환을 위한 변환 행렬 생성
    matrix = cv2.getPerspectiveTransform(pts1, pts2)
    #변환 행렬을 적용하여 새로운 이미지 생성
    imageOutput = cv2.warpPerspective(image, matrix, (width, height))
    return imageOutput
#Display the Image
imageCanny = preprocessing(image)
biggest, imageDrawContour = findDrawContours(imageCanny)
print(biggest.size)
if biggest.size != 0:
    imageOutput = getWarp(image, biggest)
    print("Corner Points", biggest)
    cv2.imshow("Image", imageOutput)
    cv2.waitKey(0)
else:
    cv2.imshow("Image", image)
    cv2.waitKey(0)

 

실패함

biggest의 shape이 다르기 때문이다.

(original)

biggest.shape
biggest

(4, 1, 2)
[[[571 117]]

 [[ 29 178]]

 [[131 770]]

 [[728 672]]]

 

(변환)

def reorder(biggest):
    print(biggest.shape)
    print(biggest)
    biggest = biggest.reshape((4,2))
    print(biggest.shape)
    print(biggest)

 

 

(4, 2)
[[571 117]
 [ 29 178]
 [131 770]
 [728 672]]

 

biggestNew[0]: 왼쪽 상단 코너
biggestNew[1]: 오른쪽 상단 코너
biggestNew[2]: 왼쪽 하단 코너
biggestNew[3]: 오른쪽 하단 코너
# biggest 배열에 저장된 4개의 코너 좌표들을 시계방향 순서대로 정렬
def reorder(biggest):
    biggest = biggest.reshape((4,2))
    biggestNew = np.zeros((4,1,2), np.int32)
    add = biggest.sum(1)
    print(add)
    biggestNew[0] = biggest[np.argmin(add)]
    biggestNew[3] = biggest[np.argmax(add)]
    diff = np.diff(biggest, axis=1)
    biggestNew[1] = biggest[np.argmin(diff)]
    biggestNew[2] = biggest[np.argmax(diff)]
    return biggestNew

#Display the Image
imageCanny = preprocessing(image)
biggest, imageDrawContour = findDrawContours(imageCanny)
# print(biggest.size)
if biggest.size != 0:
    biggestNew = reorder(biggest)
    imageOutput = getWarp(image, biggestNew)
    # print("Corner Points", biggest)
    cv2.imshow("Image Oputput", imageOutput)
    cv2.waitKey(0)
else:
    cv2.imshow("Image", image)
    cv2.waitKey(0)

 

 

이미지 크롭 

배경이 살짝 나오기 때문에 각각의 방향마다 20씩 사진을 줄인다.

    imageCropped = imageOutput[20:imageOutput.shape[0] -20, 20:imageOutput.shape[1]-20]

 

 

 

<전체 코드

#Steps:
#1. Convert the Image to Gray Scale
#2. Find the edges in the image using Canny Edge Detector
#3. Find and Draw the Contours
#4. Find the Corner Points
#5. Apply Warp Perspective

#Import All the Required Libraries
import cv2
import numpy as np

#Read the image using OpenCV
image = cv2.imread("../Resources/Images/documentscanner.jpg")
print("Image shape", image.shape, image.size)


#resize(원본, (가로, 세로))
image = cv2.resize(image, (768, 800))
#shape (세로, 가로, 채널)
print("Image shape", image.shape, image.size)
imageCopy = image.copy()
width = 768
height = 800


def preprocessing(image):
    # 1. Convert the Image to Gray Scale
    imageGray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 2. Find the edges in the image using Canny Edge Detector
    imageCanny = cv2.Canny(imageGray, 200, 200)
    return imageCanny

def findDrawContours(imageCanny):
    maxarea=0
    #특정한 윤곽선(예: 가장 큰 사각형)을 찾을 때 초기값을 설정하는 용도
    biggest = np.array([])
    #3. Find and Draw the Contours
    #가장 바깥쪽 윤곽선만 검출 : cv2.RETR_EXTERNAL
    #꼭 필요한 점들만 남겨 컨투어 단순화 : cv2.CHAIN_APPROX_NONE
    #Contour : 윤곽선 리스트
    contours, hirearchy = cv2.findContours(imageCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    for cnt in contours: # -1: 모든 윤곽선을 그림
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 30000:
            cv2.drawContours(imageCopy, cnt, -1, (255, 0,0), 2)
            # 4. Find the Corner Points
            #윤곽선 길이 계산 - 객체의 둘레 :perimeter 구하기
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, 0.02*peri, True)
            print(len(approx))
            # print(approx)
            if area >maxarea and len(approx)==4:
                biggest = approx
                maxarea = area
            cv2.drawContours(imageCopy, approx, -1, (255, 255,0), 10)
    return biggest, imageCopy

def getWarp(image, biggest):
    # 원본 이미지에서 변환할 4개의 좌표
    pts1 = np.float32(biggest)
    #변환 후 새로운 4개 꼭짓점 (새로운 크기로 조정)
    pts2 = np.float32([[0,0], [width, 0], [0, height], [width, height]])
    #원근 변환을 위한 변환 행렬 생성
    matrix = cv2.getPerspectiveTransform(pts1, pts2)
    #변환 행렬을 적용하여 새로운 이미지 생성
    imageOutput = cv2.warpPerspective(image, matrix, (width, height))
    imageCropped = imageOutput[20:imageOutput.shape[0] -20, 20:imageOutput.shape[1]-20]
    return imageCropped

# biggest 배열에 저장된 4개의 코너 좌표들을 시계방향 순서대로 정렬
def reorder(biggest):
    biggest = biggest.reshape((4,2))
    biggestNew = np.zeros((4,1,2), np.int32)
    add = biggest.sum(1)
    print(add)
    biggestNew[0] = biggest[np.argmin(add)]
    biggestNew[3] = biggest[np.argmax(add)]
    diff = np.diff(biggest, axis=1)
    biggestNew[1] = biggest[np.argmin(diff)]
    biggestNew[2] = biggest[np.argmax(diff)]
    return biggestNew

#Display the Image
imageCanny = preprocessing(image)
biggest, imageDrawContour = findDrawContours(imageCanny)
# print(biggest.size)
if biggest.size != 0:
    biggestNew = reorder(biggest)
    imageOutput = getWarp(image, biggestNew)
    # print("Corner Points", biggest)
    # cv2.imshow("Image Oputput", imageOutput)
    cv2.imshow("Image Cropped", imageOutput)
    cv2.waitKey(0)
else:
    cv2.imshow("Image", image)
    cv2.waitKey(0)


cv2.destroyAllWindows()

 

 

openCV 기초 공부