업무 자동화 ② 사진 (1) — 한 장을 열고 줄인다
Article
앞 두 강의에서 폴더를 훑어 엑셀을 만들었다. 이번에도 틀은 똑같다.
폴더를 훑는다 → 하나씩 연다 → 바꾼다 → 저장한다
바뀌는 건 다루는 대상이 글자에서 사진으로 바뀌는 것뿐이다.
손으로 하면
한 시간 남짓
- 그림판을 켠다
- 사진 열기 → 크기 조정 → 다른 이름으로 저장 → 닫기
- 100번 반복한다
- 로고까지 넣으라면 두 배
코드로 하면
몇 초
- 폴더 경로를 알려주고 실행한다
- 전부 같은 크기로 줄어 새 폴더에 저장된다
- 구석에 워터마크까지 찍혀 있다
- 원본은 손대지 않는다
이번 강의는 한 장을 제대로 다루고, 다음 강의에서 그걸 폴더째 돌린다.
준비
pip install pillow
파일은 계속 ai-course 폴더에 넣는다. 이번 강의는 71번부터다.
1. 실습 재료 — 사진도 코드로 만든다
남의 사진을 받아오지 않는다. 큰 사진을 코드로 그린다. 스마트폰 사진 크기(3000×2000)로 만든다.
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. 한 장을 열어본다
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. 한 장을 줄여본다
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. 정렬이 맞아진다 |
스스로 해보기
- 01
quality 를 바꿔가며 용량을 잰다
ai-course/74_연습1.py. 같은 800px 사진을quality30 · 60 · 85 · 95 로 저장해 용량을 비교한다. 어디서부터 눈에 띄게 커지나. - 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` 을 다시 하는 것이 요령이다. 밖에서 한 번만 열면 첫 thumbnail 이 img 를 800으로 줄여버려서, 두 번째부터는 이미 작아진 것을 또 줄이게 된다.
저장한 q30.jpg 와 q95.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 이 나오면 여기부터 본다.
다음 강의에서 이걸 폴더째 한꺼번에 돌리고, 워터마크까지 찍는다.