스크립트·파일 자동화

Pillow로 원본을 유지하면서 폴더의 이미지를 일괄 리사이즈하기

원본 파일을 덮어쓰지 않고 여러 이미지를 새 폴더에 축소하며, 가로세로 비율도 유지합니다. 크기가 명확한 작은 합성 이미지 세트를 이용해 예상 출력 크기를 손으로 확인할 수 있습니다.

목차 보기

이런 분께 맞아요여러 이미지의 원본을 그대로 보존하면서 비율이 유지된 작은 사본을 만들려는 사람을 위한 안내입니다.

준비할 것
  • Python 3.12와 해당 버전을 실행하는 터미널 명령이 준비되어 있어야 합니다.
  • python -m pip install Pillow 명령으로 Pillow가 설치되어 있어야 합니다.
  • 스크립트가 outputs 아래에 새 폴더를 만들 수 있는 작업 폴더가 필요합니다.
  • 원본 합성 이미지와 리사이즈된 사본을 모두 저장할 수 있는 충분한 디스크 공간이 필요합니다.

01먼저 리사이즈 규칙 정하기

목표는 모든 이미지를 가로 300픽셀, 세로 300픽셀 상자 안에 들어가도록 만들면서 원래 가로세로 비율을 유지하는 것입니다. 이미 이 상자보다 작은 이미지는 확대하지 않습니다. 원본 이미지는 변경하지 않고, 리사이즈된 사본은 모두 별도의 출력 폴더에 저장합니다.

이미지의 가로 크기를 W, 세로 크기를 H라고 하면 300/W, 300/H, 1 가운데 가장 작은 값을 scale로 정합니다. 그 하나의 배율을 가로와 세로에 모두 적용합니다. 두 방향에 같은 배율을 사용하면 이미지가 늘어나거나 찌그러지는 것을 막을 수 있습니다.

text
scale = min(300 / width, 300 / height, 1)
new_width = int(width * scale)
new_height = int(height * scale)

02합성 이미지 폴더 만들기

아래 파일들은 이 글을 위해 만든 합성 이미지입니다. 준비용 스크립트는 크기를 알고 있는 단순한 PNG 이미지 4개를 만듭니다. 이미지의 시각적 내용 자체는 중요하지 않으며, 예상되는 리사이즈 결과를 손으로 계산하기 쉽도록 크기를 정했습니다.

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
IMAGES = {
    "landscape.png": ((800, 600), (220, 80, 80)),
    "portrait.png": ((600, 800), (80, 120, 220)),
    "small.png": ((200, 100), (80, 180, 100)),
    "wide.png": ((1200, 300), (180, 120, 60)),
}

SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir()  # Stop if this synthetic source already exists.

for filename, (size, color) in IMAGES.items():
    image = Image.new("RGB", size, color)
    target = SOURCE / filename
    image.save(target, format="PNG")
text
python create_resize_demo.py
파일원본 가로원본 세로가로세로 비율
landscape.png8006004:3
portrait.png6008003:4
small.png2001002:1
wide.png12003004:1

원본 4개의 전체 픽셀 수는 1,240,000개입니다. 계산식은 480,000 + 480,000 + 20,000 + 360,000입니다. 픽셀 수는 예제를 이해하는 데 유용하지만, PNG 압축률은 이미지 내용에 따라 달라지므로 파일의 바이트 크기와 같은 의미는 아닙니다.

03예상 크기 4개를 손으로 계산하기

landscape.png에서는 가로가 제한 조건입니다. 300/800 = 0.375입니다. 가로와 세로에 모두 0.375를 곱하면 300×225가 됩니다. portrait.png에서는 세로가 제한 조건이며, 300/800 = 0.375이므로 결과는 225×300입니다.

small.png는 두 방향 모두 이미 300 이하입니다. 배율의 최댓값을 1로 제한했으므로 200×100 그대로 유지됩니다. wide.png는 가로가 제한 조건입니다. 300/1200 = 0.25이므로 결과는 300×75입니다.

파일배율예상 출력출력 픽셀 수
landscape.png0.375300 × 22567,500
portrait.png0.375225 × 30067,500
small.png1200 × 10020,000
wide.png0.25300 × 7522,500

따라서 리사이즈된 사본의 전체 픽셀 수는 177,500개입니다. 원본은 별도로 그대로 남아 있으므로 이 숫자를 디스크 공간 절약량으로 해석하면 안 됩니다.

04새 대상 폴더로 이미지 리사이즈하기

다음 스크립트를 batch_image_resize.py로 저장하세요. SOURCE 바로 아래에 있는 PNG, JPEG, WebP 파일을 처리합니다. 하위 폴더까지 재귀적으로 검색하지 않습니다. 대상 폴더는 exist_ok 없이 mkdir로 생성하므로, 기존 출력 폴더가 있으면 이전 결과와 새 결과를 섞지 않고 실행을 중단합니다.

python
from pathlib import Path
from PIL import Image, ImageOps

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"
MAX_WIDTH = 300
MAX_HEIGHT = 300
SUPPORTED = {".png", ".jpg", ".jpeg", ".webp"}


def target_size(width: int, height: int) -> tuple[int, int]:
    scale = min(MAX_WIDTH / width, MAX_HEIGHT / height, 1)
    new_width = max(1, int(width * scale))
    new_height = max(1, int(height * scale))
    return new_width, new_height


def main() -> None:
    if not SOURCE.is_dir():
        raise FileNotFoundError(f"Source folder not found: {SOURCE}")
    if MAX_WIDTH < 1 or MAX_HEIGHT < 1:
        raise ValueError("Maximum dimensions must be positive integers.")

    source_resolved = SOURCE.resolve()
    output_resolved = OUTPUT_DIR.resolve()
    if source_resolved == output_resolved:
        raise ValueError("SOURCE and OUTPUT_DIR must be different.")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()  # Refuse to reuse an existing destination.

    candidates = sorted(
        path for path in SOURCE.iterdir()
        if path.is_file() and path.suffix.lower() in SUPPORTED
    )

    resized_count = 0
    unchanged_size_count = 0

    for source_path in candidates:
        output_path = OUTPUT_DIR / source_path.name
        if output_path.exists():
            raise FileExistsError(f"Output already exists: {output_path}")

        with Image.open(source_path) as opened:
            image = ImageOps.exif_transpose(opened)
            image.load()
            old_size = image.size
            new_size = target_size(*old_size)

            if new_size == old_size:
                result = image.copy()
                unchanged_size_count += 1
            else:
                result = image.resize(new_size, Image.Resampling.LANCZOS)
                resized_count += 1

            save_options = {}
            if source_path.suffix.lower() in {".jpg", ".jpeg"}:
                save_options["quality"] = 90
            result.save(output_path, **save_options)
            result.close()

        print(f"{source_path.name}: {old_size[0]}x{old_size[1]} -> {new_size[0]}x{new_size[1]}")

    print(f"Processed {len(candidates)} images.")
    print(f"Resized: {resized_count}; already within limit: {unchanged_size_count}.")
    print(f"Output folder: {OUTPUT_DIR.as_posix()}")


if __name__ == "__main__":
    main()

ImageOps.exif_transpose는 EXIF 방향 정보가 있는 경우 리사이즈 크기를 계산하기 전에 해당 방향을 실제 픽셀에 적용합니다. 이 예제의 합성 PNG에는 그러한 방향 정보가 필요하지 않으므로 표시 크기는 앞에서 적은 값과 같습니다.

05예상 출력과 비교하기

파일명은 정렬된 순서로 처리하므로 예상 콘솔 순서는 landscape.png, portrait.png, small.png, wide.png입니다. 3개 이미지는 작아지고, 1개 이미지는 원래 크기를 유지합니다.

text
landscape.png: 800x600 -> 300x225
portrait.png: 600x800 -> 225x300
small.png: 200x100 -> 200x100
wide.png: 1200x300 -> 300x75
Processed 4 images.
Resized: 3; already within limit: 1.
Output folder: outputs/image_resize_result

이 예상 출력은 실제 실행 로그를 가져온 것이 아니라 손으로 계산한 결과입니다. 중요한 확인 항목은 이미지 크기, 파일 수, 그리고 outputs/image_resize_demo 안의 원본이 그대로 남아 있는지 여부입니다.

확인 항목예상값
원본 폴더의 파일 수4
출력 폴더의 파일 수4
축소된 파일 수3
이미 제한 범위 안에 있던 파일 수1
출력 중 가장 큰 가로300
출력 중 가장 큰 세로300

06생성된 이미지를 직접 검증하기

파일명이나 콘솔 메시지만으로 판단하지 마세요. 출력 파일을 직접 열어 크기를 확인해야 합니다. 아래의 선택적 확인 스크립트는 원본과 출력 폴더를 읽고 크기를 표시하기만 하며 파일에는 아무것도 쓰지 않습니다.

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"

for source_path in sorted(SOURCE.glob("*.png")):
    output_path = OUTPUT_DIR / source_path.name
    with Image.open(source_path) as original, Image.open(output_path) as resized:
        print(
            f"{source_path.name}: "
            f"original={original.size}, output={resized.size}"
        )
  • 모든 출력 이미지의 가로가 300 이하이고 세로도 300 이하인지 확인합니다.
  • landscape.png는 4:3, portrait.png는 3:4, wide.png는 4:1 비율을 유지하는지 확인합니다.
  • small.png가 300×150으로 확대되지 않고 200×100으로 유지되는지 확인합니다.
  • 원본 파일이 실제로 다시 저장되지 않았다는 추가 근거가 필요하면 수정 시간이나 해시값을 비교합니다.
  • OUTPUT_DIR을 바꾸지 않고 리사이즈 스크립트를 두 번째로 실행합니다. 다른 작업을 시작하기 전에 FileExistsError로 중단되어야 합니다.

07자주 발생하는 오류 확인하기

증상확인할 사항
ModuleNotFoundError: No module named PIL스크립트를 실행하는 동일한 Python 환경에 python -m pip install Pillow 명령으로 Pillow를 설치하세요.
FileNotFoundErrorSOURCE와 터미널의 현재 작업 디렉터리를 확인하세요. 이 예제에서는 먼저 합성 데이터 준비용 스크립트를 실행해야 합니다.
FileExistsError출력 폴더가 이미 존재합니다. 자동으로 덮어쓰게 하지 말고 기존 결과를 검토한 뒤 새 대상 폴더를 선택하세요.
이미지를 식별할 수 없음확장자는 지원되는 형식처럼 보이지만 내용이 손상되었거나 실제 이미지가 아닐 수 있습니다. 실패한 배치는 미완성으로 취급하세요.
예상과 다른 회전방향 메타데이터와 실제 픽셀 방향은 다를 수 있습니다. 리사이즈 전에 ImageOps.exif_transpose를 사용하지만 중요한 이미지는 눈으로 확인하세요.
예상과 다른 출력 바이트 크기픽셀 크기만으로 압축된 파일 크기가 결정되는 것은 아닙니다. 형식, 이미지 내용, 메타데이터, JPEG 품질 등이 영향을 줍니다.

일부 파일을 저장한 뒤 처리에 실패하면 스크립트는 이미 생성된 결과를 삭제하지 않습니다. 문제를 조사하기 위해 해당 대상 폴더를 보관하거나, 수정 후에는 새로운 출력 디렉터리를 사용하세요.

08실제 이미지에 적용하기 전 한계 이해하기

이 예제는 한 폴더 바로 아래에 있는 파일만 처리하며 PNG, JPEG, WebP 확장자를 지원합니다. 중첩된 폴더 구조를 그대로 복제하지 않고, 애니메이션의 각 프레임을 개별적으로 리사이즈하지 않으며, 모든 종류의 메타데이터나 색상 관리 정보를 동일하게 보존한다고 보장하지 않습니다.

리샘플링은 픽셀 데이터를 변경합니다. 크기가 그대로인 이미지도 열고 다시 저장하면 인코딩된 바이트나 메타데이터가 달라질 수 있습니다. JPEG 출력은 손실 압축이며, 이 스크립트는 JPEG 사본을 quality 90으로 저장합니다. 따라서 원래 JPEG의 바이트열이나 원래 압축 설정을 보존하는 작업이 아닙니다.

int 계산은 소수점이 있는 크기를 아래쪽 정수로 변환합니다. 따라서 임의의 원본 크기에서는 이미지 크기가 정수여야 하므로 결과의 픽셀 비율이 수학적인 비율과 아주 조금 달라질 수 있습니다. 이 글에서 사용한 합성 이미지 크기는 정확히 나누어지므로 예상 결과 4개에는 이러한 반올림 모호성이 없습니다.

실행·검증 기록

2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · Pillow 필요 · 실행하지 않음

  • 합성 원본 4개의 크기가 각각 800x600, 600x800, 200x100, 1200x300임을 수동으로 확인했습니다.
  • 배율을 각각 0.375, 0.375, 1, 0.25로 수동 계산했습니다.
  • 예상 출력 크기를 각각 300x225, 225x300, 200x100, 300x75로 수동 계산했습니다.
  • 원본 전체 픽셀 수가 1,240,000개이고 출력 전체 픽셀 수가 177,500개임을 수동 계산했습니다.
  • 원본 파일은 읽기 위해 열고 결과는 별도 폴더에 저장하며, 기존 대상 폴더가 있으면 실행을 중단하도록 작성되었는지 코드를 검토했습니다.
  • 예상 콘솔 출력과 파일 개수를 손으로 도출했습니다.
검증 한계
  • 이 응답의 작성자는 코드를 실행하지 않았으며, Pillow 동작이나 파일 시스템 쓰기를 실제로 시험하지 않았습니다.
  • JPEG, WebP, EXIF 방향, 손상된 파일, 색상 프로파일, 애니메이션, 메타데이터 보존은 설명했지만 시험하지 않았습니다.
  • 압축 파일의 바이트 크기는 이미지 내용과 인코딩에 따라 달라지므로 예측하지 않았습니다.
  • 공식 문서 URL은 알려진 문서 위치를 사용했지만 실시간으로 접속해 확인하지 않았습니다.

사이트 전체 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.