Created
June 12, 2025 05:58
-
-
Save haandol/bf07d733e4950ec314594e40f5b2115e to your computer and use it in GitHub Desktop.
vertical image split
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| 이미지 분할 도구 | |
| 위아래로 긴 이미지에서 중간 여백을 찾아 여러 개의 이미지로 분할합니다. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| import os | |
| import argparse | |
| from typing import List, Tuple | |
| import matplotlib.pyplot as plt | |
| class ImageSplitter: | |
| def __init__(self, threshold: int = 250, min_gap_height: int = 100, min_content_height: int = 50): | |
| """ | |
| 이미지 분할기 초기화 | |
| Args: | |
| threshold: 여백으로 판단할 픽셀 밝기 임계값 (0-255, 기본값: 250 - 흰색에 가까운 영역) | |
| min_gap_height: 여백으로 인정할 최소 높이 (픽셀, 기본값: 100 - 100픽셀 이상 연속된 흰색 줄) | |
| min_content_height: 분할된 이미지의 최소 높이 (픽셀) | |
| """ | |
| self.threshold = threshold | |
| self.min_gap_height = min_gap_height | |
| self.min_content_height = min_content_height | |
| def load_image(self, image_path: str) -> np.ndarray: | |
| """이미지 파일을 로드합니다.""" | |
| if not os.path.exists(image_path): | |
| raise FileNotFoundError(f"이미지 파일을 찾을 수 없습니다: {image_path}") | |
| image = cv2.imread(image_path) | |
| if image is None: | |
| raise ValueError(f"이미지를 읽을 수 없습니다: {image_path}") | |
| return image | |
| def detect_horizontal_gaps(self, image: np.ndarray) -> List[Tuple[int, int]]: | |
| """ | |
| 이미지에서 수평 여백 영역을 감지합니다. | |
| 100픽셀 이상 연속된 흰색 줄을 찾아 분할 지점으로 사용합니다. | |
| Args: | |
| image: 입력 이미지 (BGR 형식) | |
| Returns: | |
| 여백 영역의 (시작_y, 끝_y) 좌표 리스트 | |
| """ | |
| # 그레이스케일로 변환 | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| height, width = gray.shape | |
| # 각 행의 평균 밝기 계산 | |
| row_means = np.mean(gray, axis=1) | |
| # 여백 행 찾기 (임계값보다 밝은 행 - 흰색에 가까운 행) | |
| gap_rows = row_means > self.threshold | |
| # 연속된 여백 영역 찾기 | |
| gaps = [] | |
| in_gap = False | |
| gap_start = 0 | |
| for i, is_gap in enumerate(gap_rows): | |
| if is_gap and not in_gap: | |
| # 여백 시작 | |
| gap_start = i | |
| in_gap = True | |
| elif not is_gap and in_gap: | |
| # 여백 끝 | |
| gap_height = i - gap_start | |
| if gap_height >= self.min_gap_height: # 100픽셀 이상인 경우만 | |
| gaps.append((gap_start, i)) | |
| in_gap = False | |
| # 마지막 여백 처리 | |
| if in_gap: | |
| gap_height = height - gap_start | |
| if gap_height >= self.min_gap_height: # 100픽셀 이상인 경우만 | |
| gaps.append((gap_start, height)) | |
| return gaps | |
| def split_image_by_gaps(self, image: np.ndarray, gaps: List[Tuple[int, int]]) -> List[np.ndarray]: | |
| """ | |
| 여백을 기준으로 이미지를 분할합니다. | |
| Args: | |
| image: 원본 이미지 | |
| gaps: 여백 영역 리스트 | |
| Returns: | |
| 분할된 이미지 리스트 | |
| """ | |
| if not gaps: | |
| return [image] | |
| height = image.shape[0] | |
| split_images = [] | |
| # 첫 번째 이미지 (시작부터 첫 번째 여백까지) | |
| if gaps[0][0] > 0: | |
| first_img = image[0:gaps[0][0]] | |
| if first_img.shape[0] >= self.min_content_height: | |
| split_images.append(first_img) | |
| # 중간 이미지들 (여백 사이의 내용들) | |
| for i in range(len(gaps) - 1): | |
| start_y = gaps[i][1] # 현재 여백의 끝 | |
| end_y = gaps[i + 1][0] # 다음 여백의 시작 | |
| if end_y > start_y: | |
| img_segment = image[start_y:end_y] | |
| if img_segment.shape[0] >= self.min_content_height: | |
| split_images.append(img_segment) | |
| # 마지막 이미지 (마지막 여백부터 끝까지) | |
| if gaps[-1][1] < height: | |
| last_img = image[gaps[-1][1]:height] | |
| if last_img.shape[0] >= self.min_content_height: | |
| split_images.append(last_img) | |
| return split_images | |
| def visualize_gaps(self, image: np.ndarray, gaps: List[Tuple[int, int]], save_path: str = None): | |
| """ | |
| 감지된 여백을 시각화합니다. | |
| Args: | |
| image: 원본 이미지 | |
| gaps: 여백 영역 리스트 | |
| save_path: 저장할 경로 (선택사항) | |
| """ | |
| # 그레이스케일로 변환 | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| height, width = gray.shape | |
| # 각 행의 평균 밝기 계산 | |
| row_means = np.mean(gray, axis=1) | |
| # 시각화 | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 8)) | |
| # 원본 이미지 표시 | |
| ax1.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) | |
| ax1.set_title('원본 이미지') | |
| ax1.set_xlabel('Width') | |
| ax1.set_ylabel('Height') | |
| # 여백 영역 표시 | |
| for gap_start, gap_end in gaps: | |
| ax1.axhspan(gap_start, gap_end, alpha=0.3, color='red', label='여백 영역') | |
| # 행별 밝기 그래프 | |
| ax2.plot(row_means, range(height)) | |
| ax2.axvline(x=self.threshold, color='red', linestyle='--', label=f'임계값 ({self.threshold})') | |
| ax2.set_title('행별 평균 밝기') | |
| ax2.set_xlabel('평균 밝기') | |
| ax2.set_ylabel('Height') | |
| ax2.invert_yaxis() | |
| ax2.legend() | |
| # 여백 영역 표시 | |
| for gap_start, gap_end in gaps: | |
| ax2.axhspan(gap_start, gap_end, alpha=0.3, color='red') | |
| plt.tight_layout() | |
| if save_path: | |
| plt.savefig(save_path, dpi=150, bbox_inches='tight') | |
| print(f"시각화 결과가 저장되었습니다: {save_path}") | |
| plt.show() | |
| def save_split_images(self, split_images: List[np.ndarray], output_dir: str, base_name: str): | |
| """ | |
| 분할된 이미지들을 저장합니다. | |
| Args: | |
| split_images: 분할된 이미지 리스트 | |
| output_dir: 출력 디렉토리 | |
| base_name: 기본 파일명 | |
| """ | |
| os.makedirs(output_dir, exist_ok=True) | |
| saved_files = [] | |
| for i, img in enumerate(split_images): | |
| filename = f"{base_name}_part_{i+1:03d}.png" | |
| filepath = os.path.join(output_dir, filename) | |
| cv2.imwrite(filepath, img) | |
| saved_files.append(filepath) | |
| print(f"저장됨: {filepath} (크기: {img.shape[1]}x{img.shape[0]})") | |
| return saved_files | |
| def process_image(self, image_path: str, output_dir: str = None, visualize: bool = False) -> List[str]: | |
| """ | |
| 이미지를 처리하여 분할합니다. | |
| Args: | |
| image_path: 입력 이미지 경로 | |
| output_dir: 출력 디렉토리 (기본값: 입력 파일과 같은 디렉토리) | |
| visualize: 여백 감지 결과 시각화 여부 | |
| Returns: | |
| 저장된 파일 경로 리스트 | |
| """ | |
| # 이미지 로드 | |
| print(f"이미지 로딩 중: {image_path}") | |
| image = self.load_image(image_path) | |
| print(f"이미지 크기: {image.shape[1]}x{image.shape[0]}") | |
| # 여백 감지 | |
| print("여백 영역 감지 중...") | |
| gaps = self.detect_horizontal_gaps(image) | |
| print(f"감지된 여백 영역 수: {len(gaps)}") | |
| for i, (start, end) in enumerate(gaps): | |
| print(f" 여백 {i+1}: y={start}~{end} (높이: {end-start}px)") | |
| # 시각화 (선택사항) | |
| if visualize: | |
| base_name = os.path.splitext(os.path.basename(image_path))[0] | |
| viz_path = os.path.join(os.path.dirname(image_path), f"{base_name}_gaps_visualization.png") | |
| self.visualize_gaps(image, gaps, viz_path) | |
| # 이미지 분할 | |
| print("이미지 분할 중...") | |
| split_images = self.split_image_by_gaps(image, gaps) | |
| print(f"분할된 이미지 수: {len(split_images)}") | |
| # 출력 디렉토리 설정 | |
| if output_dir is None: | |
| output_dir = os.path.join(os.path.dirname(image_path), "split_images") | |
| # 분할된 이미지 저장 | |
| base_name = os.path.splitext(os.path.basename(image_path))[0] | |
| saved_files = self.save_split_images(split_images, output_dir, base_name) | |
| print(f"\n처리 완료! {len(saved_files)}개의 이미지가 저장되었습니다.") | |
| print(f"출력 디렉토리: {output_dir}") | |
| return saved_files | |
| def main(): | |
| parser = argparse.ArgumentParser(description="이미지를 여백 기준으로 분할합니다. (100픽셀 이상 연속된 흰색 줄에서 자름)") | |
| parser.add_argument("input_image", help="입력 이미지 파일 경로") | |
| parser.add_argument("-o", "--output", help="출력 디렉토리 (기본값: 입력 파일과 같은 디렉토리의 split_images 폴더)") | |
| parser.add_argument("-t", "--threshold", type=int, default=250, help="여백 판단 임계값 (0-255, 기본값: 250 - 흰색에 가까운 영역)") | |
| parser.add_argument("--min-gap-height", type=int, default=100, help="여백으로 인정할 최소 높이 (기본값: 100px - 100픽셀 이상 연속된 흰색 줄)") | |
| parser.add_argument("--min-content-height", type=int, default=50, help="분할된 이미지의 최소 높이 (기본값: 50px)") | |
| parser.add_argument("-v", "--visualize", action="store_true", help="여백 감지 결과 시각화") | |
| args = parser.parse_args() | |
| # 이미지 분할기 생성 | |
| splitter = ImageSplitter( | |
| threshold=args.threshold, | |
| min_gap_height=args.min_gap_height, | |
| min_content_height=args.min_content_height | |
| ) | |
| try: | |
| # 이미지 처리 | |
| saved_files = splitter.process_image( | |
| image_path=args.input_image, | |
| output_dir=args.output, | |
| visualize=args.visualize | |
| ) | |
| print(f"\n성공적으로 완료되었습니다!") | |
| except Exception as e: | |
| print(f"오류 발생: {e}") | |
| return 1 | |
| return 0 | |
| if __name__ == "__main__": | |
| exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment