머신러닝/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 기초 공부