728x90
728x90

문제인식

태블릿 PC를 구매하게 되었고 도서를 스캔하며 공부할 계획을 세웠다.

vFlat으로 스캔을 해보았지만 문제가 있었다. (물론 내 잘못)

한 번에 두 페이지를 스캔했기 때문에 각 페이지를 개별적으로 분할해야 했다.

물론 vFlat에는 두 페이지를 동시에 촬영하고 한 장씩 저장하는 기능이 있었지만, 유료 서비스였다.

매달 결제하고 싶지 않았기에 Python 코딩으로 해결하고자 했다.

 

code

마음에 들지는 않았지만, 그래도 그럭저럭 볼 만하게 분할되었다.

시간이 걸리더라도 한 번에 두 장씩 말고 한 장씩 스캔하는 것이 좋을 듯하다.

import os
from tkinter import Tk
from tkinter import filedialog

import cv2
import numpy as np


def split_scanned_pages(image_paths):
    for image_path in image_paths:
        # 이미지 읽기
        normalized_path = os.path.normpath(image_path)
        image = cv2.imread(normalized_path)
        if image is None:
            print(f"이미지를 불러올 수 없습니다: {normalized_path}")
            continue

        # 이미지 크기 얻기
        height, width, _ = image.shape

        # 가로 세로 비율을 기반으로 한 페이지 여부 판단
        aspect_ratio = width / height
        if aspect_ratio < 1.2:
            print(f"한 페이지로 간주됨, 분할하지 않습니다: {normalized_path}")
            # 한 페이지로 간주되면 복사하여 저장
            dir_name, file_name = os.path.split(image_path)
            left_page_path = os.path.join(dir_name, f"split_{os.path.splitext(file_name)[0]}_1{os.path.splitext(file_name)[1]}")
            cv2.imwrite(left_page_path, image)
            print(f"이미지 저장 완료: {left_page_path}")
            continue

        # 그레이스케일로 변환
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 가우시안 블러 적용 (노이즈 제거)
        blurred = cv2.GaussianBlur(gray, (5, 5), 0)

        # Adaptive Threshold 적용 (그림자 및 텍스트 영역 분리)
        adaptive_thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)

        # Sobel Edge Detection 적용 (가장자리 검출)
        sobelx = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=5)
        sobely = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=5)
        edges = cv2.magnitude(sobelx, sobely)

        # Morphological Closing 적용 (가장자리 연결 강화)
        kernel = np.ones((5, 5), np.uint8)
        edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)

        # 세로 히스토그램 계산 (가장자리 합계 및 Adaptive Threshold 결과를 결합하여 수직 분포 확인)
        combined = np.sum(edges, axis=0) + np.sum(adaptive_thresh, axis=0)

        # 1D 가우시안 스무딩 적용 (히스토그램 스무딩)
        smoothed_combined = cv2.GaussianBlur(combined.reshape(1, -1), (1, 21), 0).flatten()

        # 중앙에서 밀도가 가장 낮은 지점을 찾기 위한 탐색 범위 설정 (47.5% ~ 52.5%)
        search_start = int(width * 0.475)
        search_end = int(width * 0.525)
        split_x = search_start
        min_density = float('inf')

        # 중앙 근처에서 밀도가 가장 낮은 지점 찾기
        for i in range(search_start, search_end):
            if smoothed_combined[i] < min_density:
                min_density = smoothed_combined[i]
                split_x = i

        # 컨투어 기반 추가 검토 (가장 큰 컨투어의 중심을 분할 지점으로 설정)
        contours, _ = cv2.findContours(adaptive_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if contours:
            largest_contour = max(contours, key=cv2.contourArea)
            x, y, w, h = cv2.boundingRect(largest_contour)
            contour_center = x + w // 2
            if search_start < contour_center < search_end:
                split_x = contour_center

        # 파일명 및 확장자 분리
        dir_name, file_name = os.path.split(image_path)
        base_name, ext = os.path.splitext(file_name)

        # 이미지를 두 개로 나누기
        left_page = image[:, :split_x]
        right_page = image[:, split_x:]

        # 분할된 이미지가 비어 있는지 확인
        if left_page.size == 0 or right_page.size == 0:
            print(f"분할된 이미지가 비어 있습니다. 분할을 건너뜁니다: {normalized_path}")
            continue

        # 저장할 파일명 설정
        left_page_path = os.path.join(dir_name, f"split_{base_name}_1{ext}")
        right_page_path = os.path.join(dir_name, f"split_{base_name}_2{ext}")

        # 결과 저장
        cv2.imwrite(left_page_path, left_page)
        cv2.imwrite(right_page_path, right_page)

        print(f"이미지 저장 완료: {left_page_path}, {right_page_path}")


# 파일 선택창 열기

Tk().withdraw()  # Tkinter 기본 창 숨기기
file_paths = filedialog.askopenfilenames(initialdir='D:/', title='이미지 파일 선택', filetypes=[("Image Files", "*.jpg *.jpeg *.png *.bmp")])

# 선택된 파일 처리
if file_paths:
    split_scanned_pages(file_paths)
else:
    print("파일이 선택되지 않았습니다.")
728x90
728x90