책 부분만 회전하고 잘라내는 북스캔 프로그램 코드

알림
|
X

페이지 정보

작성자 no_profile 몬발켜 112.♡.83.184
작성일 2024.04.19 15:09
1,140 조회
10 추천
글쓰기

본문

책을 낱장으로 분리합니다.
책을 한 페이지씩 스마트폰으로 찍습니다. 이 때 스마트폰과 바닥이 수직으로 놓여야 합니다.
책을 놓을 배경색은 검은색을 추천합니다. 상대적으로 돋보여서 책 영역 선택이 더 잘 됩니다.
책 사진에서 책 영역만 추출하여 파일로 저장하려고 합니다. 
 
책 사진은 수직 수평이 안 맞게 찍히는 경우가 많습니다.
그래서 기울기를 알아내고, 기울기만큼 회전해야 합니다.
책 영역만 선택하여 파일로 저장하면 파일의 크기가 줄어듭니다. 
 
python과 opencv를 주로 사용해서 기능을 구현했습니다. 
저는 프로그래머가 아니며, chatGPT4에게 질문+수정을 반복하여 만들었습니다.
이 코드는 59번째로 수정한 코드입니다. 
다른 어떤 사람의 코드도 삽입하지 않았습니다. 
퍼블릭 도메인입니다. 그러니 여러분도 마음껏 사용/추가/변형하셔도 됩니다.
 
pyqt5를 사용해서 GUI까지 추가한 코드도 만들었습니다만,  
좀 허접한 GUI라서 그 코드는 공개하지 않았습니다. 
컴파일해서 실행 파일을 제공하면 일반 유저에게 더 편리하겠지만,
파일 용량이 100MB 정도 됩니다.
 
C:\input 폴더에 책 사진 파일들을 넣고 이 프로그램을 실행하면, 
C:\output 폴더에 책 영역만 저장된 파일로 저장됩니다. 
모든 파일들을 처리하면 프로그램이 자동으로 종료됩니다. 
 사진 파일 1개를 처리하는 데에 대략 1초 정도 걸립니다. 
실행 파일을 더블 클릭하는 것 외에 어떠한 추가 동작도 필요하지 않습니다. 
pyqt5를 사용해서 만든 GUI에는 처리된 파일의 수/전체 파일의 수를 표시하는 코드가 들어 있습니다.
 
openCV에는 컨투어를 찾는 함수가 있는데요, 
이 함수는 우리가 기대하는 방식으로 동작하지 않았습니다.
그래서 면적이 가장 큰 컨투어를 찾은 다음에 '가장 큰 폐곡선으로 이루어진 영역'을 추가로 찾아야 합니다.  
 
책 사진을 찍을 때 조명 때문에 비스듬한 각도로 찍으면 책 모양이 직사각형이 안 됩니다. 
이 경우에는 회전하고 자르는 함수 대신에 형태변환 함수를 사용해야 합니다.
이 코드도 만들기는 했는데, 작동 방식이 이해가 잘 안 되었습니다.. 쩝
 
수동으로 책 사진을 회전하고 잘라야 할 때도 생길 겁니다. 
이 코드는 이미지 뷰어 코드도 포함되어야 하고, 확대 축소하는 코드도 포함되어야 하는데,
제가 만들기에는 너무 복잡해서 접었습니다.  
 
 
 
 
import cv2
import os
import numpy as np
import sys

 

folder_path = "C:/input"  # 작업하려는 폴더 경로
file_list = os.listdir(folder_path)

 

image_extensions = [".jpg", ".jpeg", ".png"] # 그림 파일만 불러온다
image_files = [file for file in file_list if any(file.lower().endswith(ext) for ext in image_extensions)]

 

# 각 파일을 하나씩 불러온다
for image_name in image_files:

 

    # 이미지 파일을 불러온다
    image = cv2.imread("C:\\input\\{}".format(image_name))

 

    # 그레이 스케일 및 이진화
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU )

 

    # 컨투어 찾기
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

 

    # 가장 큰 컨투어 찾기
    largest_contour = max(contours, key=cv2.contourArea)

 

    # 가장 큰 컨투어에 불필요한 점/점들이 포함된 경우가 종종 발생하기 때문에 제거할 필요가 있다
    # 새로운 이미지 생성 (검은색 배경)
    new_image = np.zeros_like(binary)

 

    # 가장 큰 컨투어 중에서 면적이 가장 큰 폐곡선 찾기
    largest_closed_contour = max([c for c in contours if cv2.contourArea(c) > 0], key=cv2.contourArea)

 

    # 면적이 가장 큰 폐곡선 안에 있는 흰색 점들만 남기고 나머지는 삭제하여 새 이미지에 저장
    cv2.drawContours(new_image, [largest_closed_contour], -1, (255), thickness=cv2.FILLED)

 

    # 컨투어를 둘러싼 가장 작은 사각형 찾기
    min_rect = cv2.minAreaRect(largest_closed_contour)

 

    # minAreaRect에서 반환한 4개 점은 실수값을 갖으므로, 사용하려면 정수로 변환한다
    box_points = cv2.boxPoints(min_rect)
    box_points = np.intp(box_points)

 

    # 4개 점을 좌표값에 따라서 번호를 부여한다
    # 원래의 의도는 좌상, 우상, 좌하, 우하 순서로 P1, P2, P3, P4를 부여하려고 하였다
    # 그러나 프로그래머의 일반적인 코드 규칙에 따라서 점의 번호를 부여하기로 생각을 바꿨다.
    # The order will be P1 (top-left), P2 (top-right), P3 (bottom-left), P4 (bottom-right)
    box_points = sorted(box_points, key=lambda x: (x[1], x[0]))
    if box_points[0][0] > box_points[1][0]:
        box_points[0], box_points[1] = box_points[1], box_points[0]
    if box_points[2][0] > box_points[3][0]:
        box_points[2], box_points[3] = box_points[3], box_points[2]

 

    # 프로그래머의 일반적인 코드 부여 방법에 따라서 점의 번호를 정했다
    P1 = box_points[0]
    P2 = box_points[1]
    P3 = box_points[2]
    P4 = box_points[3]

 

    if P1[0] < P2[0]: # 기울기가 양수일 때(왼쪽으로 기울었다:시계방향으로 회전해야 한다)
        x1, y1 = P1[0], P1[1]
        x2, y2 = P2[0], P2[1]

 

        # 기울기 slope를 계산한다.
        delta_x = x2 - x1
        delta_y = y2 - y1
        slope = delta_y / delta_x if delta_x != 0 else None

 

        # 기울기에 따라서 회전할 각과 방향을 계산한다
        # Calculate the angle in radians and then convert to degrees
        # The angle must be negative for a clockwise rotation
        angle_of_rotation = -np.degrees(np.arctan(slope))

 

        # 시계방향으로 angle만큼 회전하고, 변수에 저장한다
        # center에 들어갈 점 P의 좌표는 데이터형을 변환해야 한다
        center = (int(P1[0]), int(P1[1]))
        angle = -angle_of_rotation
        M = cv2.getRotationMatrix2D(center, angle, 1)
        rotated_image = cv2.warpAffine(image, M, (image.shape[1], image.shape[0]))

 

        # 회전한 이미지를 저장하지 않고 변수를 바로 사용하여 다음 단계의 작업을 진행한다
        #그레이 스케일 및 이진화
        gray1 = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
        _, binary1 = cv2.threshold(gray1, 127, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU )

 

       
        # 회전한 이미지의 컨투어 찾기
        contours1, _ = cv2.findContours(binary1, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

 

        # 회전한 이미지의 가장 큰 컨투어 찾기
        largest_contour1 = max(contours1, key=cv2.contourArea)

 

        # 새로운 이미지 생성 (검은색 배경)
        new_image1 = np.zeros_like(binary1)

 

        # 가장 큰 컨투어에 불필요한 점/점들이 포함된 경우가 종종 발생하기 때문에 제거할 필요가 있다
        # 회전한 이미지의 가장 큰 컨투어 중에서 면적이 가장 큰 폐곡선 찾기
        largest_closed_contour1 = max([c for c in contours1 if cv2.contourArea(c) > 0], key=cv2.contourArea)

 

        # 면적이 가장 큰 폐곡선 안에 있는 흰색 점들만 남기고 나머지는 삭제하여 새 이미지에 저장
        cv2.drawContours(new_image1, [largest_closed_contour1], -1, (255), thickness=cv2.FILLED)

 

        # 회전한 이미지의 컨투어를 포함하는 가장 작은 사각형을 찾는다
        min_rect1 = cv2.minAreaRect(largest_closed_contour1)

 

        # minAreaRect에서 반환한 4개 점은 실수값을 갖으므로, 사용하려면 정수로 변환한다
        # Convert it to box points (four points)
        box_points1 = cv2.boxPoints(min_rect1)
        box_points1 = np.intp(box_points1)

 

        # # 4개 점을 좌표값에 따라서 번호를 부여한다
        # 원래의 의도는 좌상, 우상, 좌하, 우하 순서로 P1, P2, P3, P4를 부여하려고 하였다
        # 그러나 프로그래머의 일반적인 코드 규칙에 따라서 점의 번호를 부여하기로 생각을 바꿨다.
        # The order will be P1 (top-left), P2 (top-right), P3 (bottom-left), P4 (bottom-right)
        box_points1 = sorted(box_points1, key=lambda x: (x[1], x[0]))
        if box_points1[0][0] > box_points1[1][0]:
            box_points1[0], box_points1[1] = box_points1[1], box_points1[0]
        if box_points1[2][0] > box_points1[3][0]:
            box_points1[2], box_points1[3] = box_points1[3], box_points1[2]

 

        # 프로그래머의 일반적인 코드 부여 방법에 따라서 점의 번호를 정했다
        # 점의 번호를 앞의 점과 다르게 해서 혼동을 피했다    
        P11 = box_points1[0]
        P22 = box_points1[1]
        P33 = box_points1[2]
        P44 = box_points1[3]
       
        # 4개 점의 좌표를 이용하여 사각형 영역을 택하여 새 그림 파일로 저장한다
        k = rotated_image[P11[1]:P33[1], P11[0]:P22[0]]

 

        cv2.imwrite("C:\\output\\{}".format(image_name), k)

 

    else: # 기울기가 양수가 아닐 때(오른쪽으로 기울었다:반시계방향으로 회전해야 한다)

 

        x1, y1 = P1[0], P1[1]
        x2, y2 = P2[0], P2[1]

 

        # 기울기를 계산한다
        delta_x = x2 - x1
        delta_y = y2 - y1
        slope = delta_y / delta_x if delta_x != 0 else None

 

        # 기울기에 따라서 회전할 각과 방향을 계산한다
        # If the slope is zero (horizontal line), we do not need to rotate.
        # If the slope is negative or undefined (vertical line), we rotate counterclockwise.
        # The angle must be positive for a counterclockwise rotation
        angle_of_rotation = np.degrees(np.arctan(-slope)) if slope is not None else 90

 

        # 반시계방향으로 angle만큼 회전하여 변수에 저장한다
        # center에 들어갈 점 P의 좌표는 데이터형을 변환해야 한다
        center = (int(P1[0]), int(P1[1]))
        angle = angle_of_rotation
        M = cv2.getRotationMatrix2D(center, angle, 1)
        rotated_image = cv2.warpAffine(image, M, (image.shape[1], image.shape[0]))

 

        # 회전한 이미지를 저장하지 않고 변수를 바로 사용하여 다음 단계의 작업을 진행한다  
        #그레이 스케일 및 이진화
        gray1 = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
        _, binary1 = cv2.threshold(gray1, 127, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU )

 

        # 회전한 이미지의 컨투어 찾기
        contours1, _ = cv2.findContours(binary1, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

 

        # 회전한 이미지의 가장 큰 컨투어 찾기
        largest_contour1 = max(contours1, key=cv2.contourArea)

 

        # 새로운 이미지 생성 (검은색 배경)
        new_image1 = np.zeros_like(binary1)

 

        # 회전한 이미지의 가장 큰 컨투어 중에서 면적이 가장 큰 폐곡선 찾기
        largest_closed_contour1 = max([c for c in contours1 if cv2.contourArea(c) > 0], key=cv2.contourArea)

 

        # 면적이 가장 큰 폐곡선 안에 있는 흰색 점들만 남기고 나머지는 삭제하여 새 이미지에 저장
        cv2.drawContours(new_image1, [largest_closed_contour1], -1, (255), thickness=cv2.FILLED)

 

        # 회전한 이미지의 컨투어를 포함하는 가장 작은 사각형을 찾는다
        min_rect1 = cv2.minAreaRect(largest_closed_contour1)

 

        # minAreaRect에서 반환한 4개 점은 실수값을 갖으므로, 사용하려면 정수로 변환한다
        # Convert it to box points (four points)
        box_points1 = cv2.boxPoints(min_rect1)
        box_points1 = np.intp(box_points1)

 

        # 4개 점을 좌표값에 따라서 번호를 부여한다
        # 원래의 의도는 좌상, 우상, 좌하, 우하 순서로 P1, P2, P3, P4를 부여하려고 하였다
        # 그러나 프로그래머의 일반적인 코드 규칙에 따라서 점의 번호를 부여하기로 생각을 바꿨다.
        # The order will be P1 (top-left), P2 (top-right), P3 (bottom-left), P4 (bottom-right)
        box_points1 = sorted(box_points1, key=lambda x: (x[1], x[0]))
        if box_points1[0][0] > box_points1[1][0]:
            box_points1[0], box_points1[1] = box_points1[1], box_points1[0]
        if box_points1[2][0] > box_points1[3][0]:
            box_points1[2], box_points1[3] = box_points1[3], box_points1[2]

 

        # 프로그래머의 일반적인 코드 부여 방법에 따라서 점의 번호를 정했다
        # 점의 번호를 앞의 점과 다르게 해서 혼동을 피했다  
        P11 = box_points1[0]
        P22 = box_points1[1]
        P33 = box_points1[2]
        P44 = box_points1[3]
       
        # 4개 점의 좌표를 이용하여 사각형 영역을 택하여 새 그림 파일로 저장한다
        k = rotated_image[P11[1]:P33[1], P11[0]:P22[0]]

 

        cv2.imwrite("C:\\output\\{}".format(image_name), k)

 

       

 

sys.exit()



댓글 3 / 1 페이지

담연님의 댓글

작성자 no_profile 담연 (146.♡.105.128)
작성일 04.22 14:44
대단하십니다, 공유 감사합니다.

몬발켜님의 댓글의 댓글

대댓글 작성자 no_profile 몬발켜 (112.♡.83.184)
작성일 04.22 14:48
@담연님에게 답글 별말씀을요... 프로그래머가 만들면 하루도 안 걸리는 프로그램인데, 저는 30일쯤 걸려서 만들었어요... ㅎㅎ
필요하시면 pyqt5로 GUI를 추가한 코드도 드릴 수 있습니다. 아니면 아예 실행파일을 보내드릴 수도 있고요.

몬발켜님의 댓글

작성자 no_profile 몬발켜 (121.♡.109.251)
작성일 04.23 11:56
본문 수정이 안 되므로 댓글로 한 가지 더 알려드립니다.
openCV에서는 한글을 잘 처리하지 못하는 모양입니다.
그러므로 input 폴더에 들어갈 사진/그림들은 반드시 영어 이름으로 된 파일이어야 합니다.
글쓰기
홈으로 전체메뉴 마이메뉴 새글/새댓글
전체 검색