학습 자료

업무 자동화 ② 사진 (1) — 한 장을 열고 줄인다


Article

앞 두 강의에서 폴더를 훑어 엑셀을 만들었다. 이번에도 틀은 똑같다.

폴더를 훑는다  →  하나씩 연다  →  바꾼다  →  저장한다

바뀌는 건 다루는 대상이 글자에서 사진으로 바뀌는 것뿐이다.

행사 사진 100장을 게시판에 올려야 한다

손으로 하면

한 시간 남짓

  • 그림판을 켠다
  • 사진 열기 → 크기 조정 → 다른 이름으로 저장 → 닫기
  • 100번 반복한다
  • 로고까지 넣으라면 두 배

코드로 하면

몇 초

  • 폴더 경로를 알려주고 실행한다
  • 전부 같은 크기로 줄어 새 폴더에 저장된다
  • 구석에 워터마크까지 찍혀 있다
  • 원본은 손대지 않는다

이번 강의는 한 장을 제대로 다루고, 다음 강의에서 그걸 폴더째 돌린다.

준비

pip install pillow

파일은 계속 ai-course 폴더에 넣는다. 이번 강의는 71번부터다.

1. 실습 재료 — 사진도 코드로 만든다

남의 사진을 받아오지 않는다. 큰 사진을 코드로 그린다. 스마트폰 사진 크기(3000×2000)로 만든다.

ai-course/71_사진만들기.py
import os
from PIL import Image, ImageDraw

os.makedirs("사진", exist_ok=True)

# (하늘색, 해 색) 세 벌
scenes = [
    ((70, 130, 180), (240, 220, 120)),
    ((90, 150, 90),  (250, 250, 250)),
    ((160, 90, 110), (255, 200, 90)),
]

for i, (sky, sun) in enumerate(scenes, start=1):
    # "RGB" 는 색 방식, (3000, 2000) 은 가로x세로, 마지막이 배경색
    img = Image.new("RGB", (3000, 2000), sky)

    # ImageDraw 는 그림 위에 도형을 그리는 붓이다
    d = ImageDraw.Draw(img)
    d.ellipse((2300, 200, 2700, 600), fill=sun)                          # 해
    d.polygon([(0, 2000), (900, 900), (1800, 2000)], fill=(60, 90, 60))  # 산
    d.rectangle((0, 1750, 3000, 2000), fill=(80, 70, 55))                # 땅

    # quality 는 JPEG 압축 정도다. 높을수록 선명하고 용량이 크다 (1~95)
    # f"{i:03d}" 는 3 을 003 으로 만든다. 안 그러면 정렬이 1, 10, 2 순서가 된다
    img.save(f"사진/행사_{i:03d}.jpg", quality=92)

print(sorted(os.listdir("사진")))
터미널
  • ['행사_001.jpg', '행사_002.jpg', '행사_003.jpg']

ai-course/사진 폴더를 열어 확인해본다. 한 장이 3000×2000 이라 화면에 꽉 찬다.

2. 한 장을 열어본다

ai-course/72_한장열기.py
import glob
from PIL import Image

paths = sorted(glob.glob("사진/*.jpg"))

img = Image.open(paths[0])       # 파일을 여는 것뿐. 아직 아무것도 안 바꿨다

print(img.size)        # (가로, 세로)
print(img.width)
print(img.height)
print(img.format)
print(img.mode)
터미널
  • (3000, 2000)
  • 3000
  • 2000
  • JPEG
  • RGB
사진 한 장이 들고 있는 것

img = Image.open(...)

img.size
(3000, 2000)
가로 · 세로를 담은 튜플
img.width / .height
3000 / 2000
따로 꺼낼 수도 있다
img.format
JPEG
원래 파일 형식
img.mode
RGB
색 방식. 흑백이면 L

괄호가 없다. img.size 는 함수가 아니라 값이다. 4강에서 본 a.shape 와 같다.

3. 한 장을 줄여본다

ai-course/73_한장줄이기.py
import os
import glob
from PIL import Image

paths = sorted(glob.glob("사진/*.jpg"))
img = Image.open(paths[0])

# thumbnail 은 "이 상자 안에 들어가게" 줄인다. 비율은 알아서 지킨다.
# 800x800 상자에 3000x2000 을 넣으면 800x533 이 된다.
img.thumbnail((800, 800))

img.save("한장_작게.jpg", quality=85)

print(img.size)
print(round(os.path.getsize(paths[0]) / 1024, 1))          # 원본 KB
print(round(os.path.getsize("한장_작게.jpg") / 1024, 1))    # 결과 KB
터미널
  • (800, 533)
  • 120.0
  • 11.9

120KB 가 12KB 가 됐다. 가로는 3.75배 줄었는데 용량은 10배 줄었다. 사진은 넓이로 계산되기 때문이다 — 3000×2000 = 600만 점이 800×533 = 42만 점이 되어 점의 개수가 14배 차이다. 거기에 quality=85 로 압축을 조금 더 준 결과가 10배다.

4. 바꾸는 방법은 갈아끼우면 된다

thumbnail 자리에 다른 것을 넣으면 다른 일을 한다. 틀은 그대로다.

실무에서 자주 쓰는 것
하고 싶은 일바꿀 한 줄
크기 줄이기img.thumbnail((800, 800))
회전img = img.rotate(90, expand=True)
흑백img = img.convert("L")
정사각형으로 자르기img = img.crop((0, 0, 400, 400))
좌우 뒤집기img = img.transpose(Image.FLIP_LEFT_RIGHT)
형식 바꾸기img.save("x.png") — 확장자만 바꾸면 된다

표를 자세히 보면 첫 줄만 img = 가 없다. 이게 이 라이브러리에서 가장 자주 걸리는 함정이다.

이번 강의에 나온 것

정리
쓴 것하는 일
pip install pillow설치는 pillow, import 는 PIL
Image.new("RGB", (w, h), 색)빈 사진을 만든다
ImageDraw.Draw(img)이 사진 위에 그릴 붓
Image.open(path)사진을 연다
img.size · .width · .height크기. 괄호가 없다 — 값이다
img.format · img.mode파일 형식 · 색 방식
img.thumbnail((800, 800))비율 유지하며 줄인다. 원본 변수를 바꾼다
img.save(path, quality=85)저장. quality 는 1~95
f"{i:03d}"1 -> 001. 정렬이 맞아진다

스스로 해보기

확인 문제 둘
  1. 01

    quality 를 바꿔가며 용량을 잰다

    ai-course/74_연습1.py. 같은 800px 사진을 quality 30 · 60 · 85 · 95 로 저장해 용량을 비교한다. 어디서부터 눈에 띄게 커지나.

  2. 02

    세로가 더 긴 사진을 넣어본다

    ai-course/75_연습2.py. Image.new("RGB", (2000, 3000), ...) 로 세로 사진을 만들고 thumbnail((800, 800)) 을 걸어본다. 결과가 (800, ...) 이 되나, (..., 800) 이 되나.

1번 — quality 별 용량충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요

실측 — 30 → 8.8KB · 60 → 9.7KB · 85 → 11.9KB · 95 → 15.6KB 다.

30에서 85까지는 3KB 남짓 느는데, 85에서 95로 10 올리면 혼자 3.7KB 가 는다. 높은 쪽 끝에서 급하게 커진다.

import os
import glob
from PIL import Image

path = sorted(glob.glob("사진/*.jpg"))[0]

for q in (30, 60, 85, 95):
    img = Image.open(path)          # 매번 새로 연다. thumbnail 이 원본 변수를 바꾸기 때문
    img.thumbnail((800, 800))
    out = f"q{q}.jpg"
    img.save(out, quality=q)
    print(f"quality {q:2d}  ->  {os.path.getsize(out)/1024:5.1f} KB")

반복문 안에서 매번 `Image.open` 을 다시 하는 것이 요령이다. 밖에서 한 번만 열면 첫 thumbnailimg 를 800으로 줄여버려서, 두 번째부터는 이미 작아진 것을 또 줄이게 된다.

저장한 q30.jpgq95.jpg 를 나란히 열어 눈으로도 비교해본다. 우리 실습 사진은 하늘·산·해뿐이라 차이가 거의 안 보인다. 사람이 많은 실제 사진이면 30에서는 뭉개진 자국이 보인다.

게시판에 올릴 사진이면 85면 충분하다. 인쇄용이면 얘기가 다르고, 원본을 지우지 않는 이유가 이것이다.

2번 — 세로가 긴 사진충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요

`(533, 800)` 이 된다. 세로가 800에 맞춰지고 가로가 따라 줄었다.

thumbnail 은 「이 상자 안에 들어가게」 줄이는 것이라, 긴 쪽이 800에 맞는다. 가로 사진이면 가로가, 세로 사진이면 세로가 800이 된다.

from PIL import Image

tall = Image.new("RGB", (2000, 3000), (70, 130, 180))
tall.save("세로.jpg", quality=92)

img = Image.open("세로.jpg")
print(img.size)                 # (2000, 3000)
img.thumbnail((800, 800))
print(img.size)                 # (533, 800)

가로·세로를 직접 지정하는 `resize` 와 다른 점이 이것이다. img.resize((800, 800)) 은 비율을 무시하고 정사각형으로 찌그러뜨린다.

폴더에 가로 사진과 세로 사진이 섞여 있어도 thumbnail 하나로 전부 처리되는 이유다. 다음 강의에서 그걸 폴더째 돌린다.

정리하면

사진 한 장은 Image.open 으로 열고, img.size 로 재고, thumbnail 로 줄이고, save 로 저장한다. 엑셀과 순서가 똑같다.

그리고 함정을 하나 봤다. 어떤 것은 원본 변수를 바꾸고 어떤 것은 새 것을 돌려준다. 결과가 안 바뀌거나 None 이 나오면 여기부터 본다.

다음 강의에서 이걸 폴더째 한꺼번에 돌리고, 워터마크까지 찍는다.