이미지의 구조와 변형
- 핵심 개념
- 자르기, 크기 변경, 회전, 변형 함수의 입력과 반환
- 이전 학습
- 반복문, 함수의 호출과 반환값
- 학습 구성
- 완성작 관찰 · 필요성과 원리 · 단계별 제작 · 결과 해석
- 다음 학습
- 투명도와 레이어 합성
학습 목표
- 완성작의 영역·크기·기울기를 관찰하고 각 변형이 필요한 이유를 설명한다.
- 이미지 파일을 읽고 크기를 확인한 뒤 자르기 → 크기 변경 → 회전을 순서대로 실행한다.
- 자르기 좌표, 가로세로 비율, 회전 경계를 비교 결과와 연결해 설명한다.
- 직접 연결한 변형을 함수로 묶고 입력값·return·반환값을 받는 변수를 설명한다.
- 같은 함수를 크기와 각도가 다른 값으로 호출하여 합성할 사진 조각 세 개를 준비한다.
학습 내용
- 1. 이미지 변형의 필요성
- 2. 완성 결과 미리보기
- 3. 이미지 파일·객체·픽셀 크기
- 4. 이미지 자르기
- 5. 크기 변경과 비율
- 6. 이미지 회전과 경계
- 7. 변형 순서와 결과 비교
- 8. 변형 코드를 함수로 묶기
- 9. 세 조각 준비와 결과 확인
공통 실습 노트북을 Colab의 파일 → 노트북 열기 → 업로드로 열고 Drive 사본을 week07_학번_이름.ipynb로 저장합니다. 1·2·3교시는 같은 사본에서 이어갑니다.
완성된 결과를 먼저 비교하고, 해설 → 코드 시연 → 결과 확인으로 이어갑니다. 실습 코드와 결과 비교를 펼쳐 바뀐 줄과 이미지의 차이를 확인합니다. 실습 1에서는 제공 코드로 완성 결과를 미리 보고, 실습 2부터 이미지를 직접 변형합니다. 원본을 읽는 코드에 자르기·크기 변경·회전을 차례로 추가합니다. 각 단계에는 앞 코드까지 포함한 전체 코드와 추가할 위치가 안내되어 있습니다.
45분 수업 진행안
설명·시연·짧은 실행을 포함한 운영 예시입니다. 비교 코드는 제공된 셀을 활용하고, 실행 전 예상과 실행 뒤 이유 설명에 시간을 배분합니다. 학생의 진행 속도에 따라 각 단원의 시간을 조절합니다.
| 단원 | 시간 | 확인할 활동 |
|---|---|---|
| 1. 이미지 변형의 필요성 | 4분 | 완성작에서 달라진 점 찾기 |
| 2. 완성 결과 미리보기 | 3분 | 값 하나를 바꾸고 인상 비교 |
| 3. 이미지 파일·객체·픽셀 크기 | 5분 | 같은 원본의 표시 폭과 실제 크기 구분 |
| 4. 자르기와 좌표 | 6분 | 출력 크기를 계산하고 영역 비교 |
| 5. 크기 변경과 비율 | 5분 | 비율과 표시용 복사본 확인 |
| 6. 회전과 출력 경계 | 5분 | 각도·expand를 예측하고 실행 |
| 7. 변형 순서와 결과 비교 | 5분 | 같은 좌표가 다른 영역이 되는 이유 설명 |
| 8. 변형 코드를 함수로 묶기 | 7분 | 반복되는 코드와 입력·반환 연결 |
| 9. 세 조각 준비와 결과 확인 | 5분 | 호출별 결과를 확인하고 다음 교시 연결 |
1. 이미지 변형의 필요성
같은 바닷가 사진을 보아도 시선이 머무는 곳은 다릅니다. 누군가는 의자를 먼저 보고, 누군가는 수평선을 따라갑니다. 사진을 바꾸는 일은 그중 무엇을 더 오래 바라보게 할지 고르는 데서 시작합니다.
아래는 한 장의 사진으로 만든 앨범 커버입니다. 완성된 커버와 원본 사진을 나란히 보며, 어떤 선택이 화면을 바꾸었는지 찾아봅니다.
원본 한 장이 커버가 되면서 무엇이 달라졌나요? 남은 장면, 사진의 크기, 수평선의 방향을 비교해 봅니다.
커버에는 큰 조각부터 작은 조각까지 사진 세 장이 겹쳐 있습니다. 이번 시간에는 가장 큰 조각을 직접 만든 뒤, 그 과정을 함수로 묶어 크기와 각도가 다른 세 조각을 준비합니다. 다음 시간에는 투명도와 배치를 조절해 커버를 완성합니다.
| 관찰할 것 | 필요한 결정 | 이번 주의 작업 |
|---|---|---|
| 조각마다 같은 의자와 바다가 보임 | 원본의 어느 영역을 사용할까 | 1교시 · 자르기 |
| 뒤의 사진이 앞의 사진보다 큼 | 각 조각의 비중을 얼마나 줄까 | 1교시 · 크기 변경 |
| 수평선과 사진의 변이 기울어짐 | 어느 방향으로 얼마나 돌릴까 | 1교시 · 회전 |
| 사진 아래로 다른 사진이 비침 | 얼마나 비치게, 어디에 놓을까 | 2교시 · 알파와 합성 |
이미지 변형을 코드로 표현하는 이유
마음에 드는 구도를 찾았어도 어떻게 만들었는지 기억나지 않을 때가 있습니다. 자른 영역과 크기·각도를 코드에 남겨 두면 같은 결과를 다시 만들고, 값 하나만 바꾼 결과와 비교할 수 있습니다. 6주차에 배운 함수로 제작 과정을 묶고, 매개변수로 바꿀 값을 전달합니다. 사진이 달라져도 같은 제작 방법을 다시 쓸 수 있습니다.
오늘의 도착점
자르기·크기·회전을 연결해 사진 조각을 만들고, 그 과정을 직접 정의한 함수로 묶습니다. 같은 함수의 크기와 각도를 바꿔 호출하면 다음 교시에 합성할 세 조각이 준비됩니다.
2. 완성 결과 미리보기
먼저 완성된 커버를 잠깐 바라봅니다. 같은 의자와 바다라도 기울기와 겹침이 달라지면 다른 인상을 줄 수 있습니다. 제공된 코드를 실행하고 값 하나를 바꾸며, 눈에 띄는 차이를 찾아봅니다. make_cover()는 미리보기를 위해 수업에서 준비한 함수입니다.
이번 활동의 목표
마음에 드는 커버를 고르고, 기울기와 겹침이 어떤 느낌을 주는지 말해 봅니다. 이미지를 직접 변형하는 학습은 실습 2의 자르기부터 시작하며, 회전 원리는 실습 4에서 배웁니다. 그 과정을 따라가며 지금 본 커버의 제작 방법도 이해하게 됩니다.
미리보기 실행 준비
공통 노트북에서 다음 두 준비 셀을 그대로 실행합니다. 코드는 접힌 상태에서 재생 버튼으로 실행할 수 있습니다. 공통 실습 노트북 받기
- 준비 A · 원본 사진 준비를 실행합니다.
- 준비 B · 제공 함수 정의와 기준 결과 보기를 실행합니다.
- ‘제공 함수 준비 완료: make_cover()’와 기준 커버가 표시되면 아래 미리보기로 이동합니다.
실행 준비용 제공 코드 · make_cover 전체 정의
노트북의 준비 B와 같은 코드입니다. 이번 활동에서는 준비 코드로 그대로 실행합니다. 직접 입력할 경우 블록 전체를 한 셀에 넣고 실행해야 보조 함수와 설정까지 함께 준비됩니다.
코드 읽기 · 함수 정의를 먼저 실행하기
make_cover()는 수업에서 만든 사용자 정의 함수입니다. 6주차처럼 def로 정의하는 코드를 먼저 실행해야 다음 셀에서 호출할 수 있습니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
from PIL import Image, ImageDraw, ImageFont, ImageOps | Pillow의 이미지 도구를 불러옵니다. | Image 등의 도구를 사용할 수 있습니다. make_cover는 별도로 정의합니다. |
def make_cover(source, crop_box, style="echo", angle=12, ...): | 커버 제작 과정을 함수로 정의합니다. | 이 셀을 실행한 뒤 make_cover()를 호출할 수 있습니다. |
STYLES · validate_crop() · make_layer() | 커버 함수에 필요한 설정과 보조 함수입니다. | 전체 제공 코드에 포함되어 있으므로 함께 실행합니다. |
demo_source = load_source(PROVIDED_PATH) | 제공된 함수로 원본 사진을 읽습니다. | demo_source에 이미지 객체가 준비됩니다. |
확인할 점 · 원본 사진 준비 → 제공 코드 전체 실행 → 준비 완료 문장과 커버 확인 → 실습 1의 함수 호출 순서입니다. 세션을 새로 시작하면 준비 A와 B를 다시 실행합니다.
# @title 준비 B · 제공 함수 정의와 기준 결과 보기
"""The same Pillow renderer is embedded in the notebook and used for lesson images."""
from pathlib import Path
import math
from PIL import Image, ImageDraw, ImageFont, ImageOps
STYLES = {
"echo": {
"background": (20, 40, 44, 255), "ink": (245, 232, 202),
"sizes": (760, 560, 390), "positions": ((40, 100), (260, 300), (480, 490)),
"title": "AFTER HOURS", "subtitle": "01 / A QUIET REPEAT",
},
"archive": {
"background": (236, 223, 195, 255), "ink": (48, 49, 42),
"sizes": (590, 440, 300), "positions": ((70, 130), (420, 330), (170, 610)),
"title": "STILL HERE", "subtitle": "02 / NOTES FROM A SUMMER",
},
"pulse": {
"background": (177, 54, 36, 255), "ink": (250, 235, 204),
"sizes": (640, 460, 330), "positions": ((-80, 30), (490, 240), (180, 590)),
"title": "OFF BEAT", "subtitle": "03 / TURN IT UP",
},
}
def load_source(path):
"""Honor phone orientation; leave the file untouched; bound working resolution."""
with Image.open(path) as opened:
image = ImageOps.exif_transpose(opened).convert("RGBA")
image.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
return image
def default_crop(source):
side = max(1, int(min(source.size) * 0.9))
left = (source.width - side) // 2
top = (source.height - side) // 2
return (left, top, left + side, top + side)
def validate_crop(source, crop_box):
assert isinstance(crop_box, (tuple, list)) and len(crop_box) == 4, "crop_box는 (왼쪽, 위, 오른쪽, 아래) 네 값입니다."
assert all(type(value) is int for value in crop_box), "자르기 좌표는 정수로 적으세요."
left, top, right, bottom = crop_box
assert 0 <= left < right <= source.width and 0 <= top < bottom <= source.height, "자르기 좌표를 출력된 작업본 크기 안에서 정하세요."
def make_layer(source, crop_box, target_size, angle, alpha):
validate_crop(source, crop_box)
layer = source.copy().crop(crop_box)
layer = layer.resize(target_size, Image.Resampling.LANCZOS)
# Multiply existing alpha: transparent source pixels must stay transparent.
layer.putalpha(layer.getchannel("A").point(lambda value: round(value * alpha / 255)))
return layer.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC, fillcolor=(0, 0, 0, 0))
def make_cover(source, crop_box, style="echo", angle=12, alpha=160, shift=0, reverse=False):
validate_crop(source, crop_box)
assert style in STYLES, "style은 echo, archive, pulse 중 하나입니다."
assert isinstance(angle, (int, float)) and math.isfinite(angle) and -30 <= angle <= 30, "angle은 -30~30 사이입니다."
assert type(alpha) is int and 0 <= alpha <= 255, "alpha는 0~255 정수입니다."
assert type(shift) is int and -100 <= shift <= 100, "shift는 -100~100 정수입니다."
spec = STYLES[style]
canvas = Image.new("RGBA", (1000, 1000), spec["background"])
layers = []
for index, (size, position) in enumerate(zip(spec["sizes"], spec["positions"])):
layer = make_layer(source, crop_box, (size, size), angle * (index - 1), alpha)
layers.append((layer, (position[0] + shift * index, position[1])))
if reverse:
layers.reverse()
for layer, position in layers:
canvas.alpha_composite(layer, dest=position)
# A supplied title strip. Typography is not a required coding task this week.
draw = ImageDraw.Draw(canvas)
draw.rectangle((0, 850, 1000, 1000), fill=spec["background"])
draw.line((42, 866, 958, 866), fill=spec["ink"], width=2)
draw.text((42, 882), spec["title"], fill=spec["ink"], font=ImageFont.load_default(size=66))
draw.text((44, 964), spec["subtitle"], fill=spec["ink"], font=ImageFont.load_default(size=16))
return canvas
def comparison_sheet(images, labels=None, preserve_scale=False):
labels = labels or [str(index + 1) for index in range(len(images))]
assert len(images) == len(labels) and len(images) > 0
columns = min(3, len(images))
rows = (len(images) + columns - 1) // columns
sheet = Image.new("RGB", (columns * 340, rows * 375), (244, 240, 230))
draw = ImageDraw.Draw(sheet)
reference_size = max(max(image.size) for image in images)
for index, (image, label) in enumerate(zip(images, labels)):
x, y = index % columns * 340, index // columns * 375
scale = 320 / (reference_size if preserve_scale else max(image.size))
preview = image.convert("RGBA").resize(
(max(1, round(image.width * scale)), max(1, round(image.height * scale))),
Image.Resampling.LANCZOS,
)
position = (x + 10 + (320 - preview.width) // 2, y + 10 + (320 - preview.height) // 2)
sheet.paste(preview, position, preview.getchannel("A"))
draw.text((x + 10, y + 340), label, fill=(35, 43, 40), font=ImageFont.load_default(size=18))
return sheet
from IPython.display import display
# 위의 함수 정의를 실행한 다음, 원본을 읽어 기준 결과를 만듭니다.
PROVIDED_PATH = "week07_provided_source.jpg"
demo_source = load_source(PROVIDED_PATH)
demo_crop = default_crop(demo_source)
demo_covers = [make_cover(demo_source, demo_crop, style=name) for name in STYLES]
display(demo_covers[0].resize((500, 500)))
print("제공 함수 준비 완료: make_cover()")
print("기준 결과: echo. 같은 사진이 서로 다른 크기와 각도로 놓인 부분을 찾으세요.")빈 노트북에서 교재 코드로 시작한다면?
수업용 원본 사진 받기로 사진을 내려받아 Colab 왼쪽 파일 → 업로드로 올립니다. 파일명이 week07_provided_source.jpg인지 확인한 뒤 위의 준비 B 전체 코드를 복사해 실행합니다. 원본을 직접 올렸으므로 준비 A는 필요하지 않습니다. 커버가 표시되면 아래 미리보기 코드로 이어갑니다.
실습 1 · 완성 결과 미리보기
- 관찰 목표
- 값 하나를 바꿨을 때 완성 커버의 인상이 어떻게 달라지는지 봅니다.
- 유지 요소
- 같은 원본 사진 · 사진의 크기 · 배경과 배치
준비된 커버를 실행해 보고, first_angle만 A=12, B=0, C=-18로 바꿉니다. 각 결과에서 수평선의 기울기와 사진의 겹침, 전체 인상이 어떻게 달라지는지 관찰합니다.
코드 읽기 · 미리보기에서 바꿀 값
make_cover()는 준비 B에 포함된 수업용 함수입니다. 준비가 끝나면 아래 코드를 그대로 실행해 완성 커버를 볼 수 있습니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
first_angle = 12 | 이 숫자만 12 → 0 → -18로 바꿉니다. | 완성 커버의 기울기와 겹침이 달라집니다. |
make_cover(..., style=favorite_style, angle=first_angle) | 정한 값을 수업용 커버 함수에 전달합니다. | 반환된 커버를 first_cover에 받습니다. 나머지 입력은 유지합니다. |
display(first_cover.resize((500, 500))) | 완성 커버를 500 × 500 미리보기로 표시합니다. | first_cover 자체의 크기는 그대로입니다. resize 원리는 실습 3에서 배웁니다. |
확인할 점 · 이번에는 결과의 기울기·겹침·인상을 관찰합니다. 이미지 변형 코드는 실습 2부터 직접 작성하고, 회전 원리는 실습 4에서 배웁니다.
A기준 커버
first_angle = 12로 설정합니다.
A · 미리보기 코드
favorite_style = "echo"
first_angle = 12
first_cover = make_cover(demo_source, demo_crop, style=favorite_style, angle=first_angle)
display(first_cover.resize((500, 500)))B수평 구도
first_angle = 0로 설정합니다.
B · 미리보기 코드
favorite_style = "echo"
first_angle = 0
first_cover = make_cover(demo_source, demo_crop, style=favorite_style, angle=first_angle)
display(first_cover.resize((500, 500)))C반대 기울기
first_angle = -18로 설정합니다.
C · 미리보기 코드
favorite_style = "echo"
first_angle = -18
first_cover = make_cover(demo_source, demo_crop, style=favorite_style, angle=first_angle)
display(first_cover.resize((500, 500)))make_cover가 정의되지 않았다는 NameError가 나오면?
실습 1의 호출 코드보다 먼저 준비 B 전체를 실행해야 합니다. 준비 A만 실행하면 사진만 저장되고 함수는 아직 없습니다. 원본 파일이 없다는 오류가 나오면 준비 A부터 다시 실행합니다. 세션을 새로 시작한 경우에도 준비 A → 준비 B → 실습 1 순서로 실행합니다.
어느 커버가 가장 마음에 드나요? 기울기와 겹침에서 눈에 띈 차이 한 가지를 말해 봅니다.
이제 사진 조각부터 직접 만들기
방금 고른 커버에서 눈에 들어왔던 부분을 기억해 둡니다. 이제 그 결과를 한 단계씩 만들어 봅니다. 원본 사진을 읽는 코드에 실습 2의 자르기 → 실습 3의 크기 변경 → 실습 4의 회전을 차례로 추가합니다. 2교시에는 완성된 조각들을 합성합니다.
3. 이미지 파일·객체·픽셀 크기
보관함의 사진과 작업대 위의 사진은 각각 어떤 역할을 할까요?

보관해 둔 사진을 꺼내 작업대에 놓으면, 어디를 남기고 어떻게 바꿀지 살펴볼 수 있습니다. 컴퓨터에서도 저장된 파일을 읽어 와야 사진을 코드로 다룰 수 있습니다. 보관함 속 사진은 이미지 파일에, 작업대 위 사진은 Python이 다루는 이미지 객체에 해당합니다. 이번 실습에서는 읽어 온 이미지로 새 결과를 만듭니다. 화면에 표시하는 것만으로 원본 파일이 바뀌지는 않습니다.
6주차에는 빈 이미지를 만들었습니다. 이번에는 이미 있는 사진을 읽어 작업 재료로 준비합니다.
| 코드 | 준비되는 것 | 사용할 상황 |
|---|---|---|
| Image.new() | 지정한 크기와 색의 새 이미지 | 빈 화면에 도형 그리기 |
| Image.open() | 파일에 들어 있던 이미지 | 사진이나 이전 작품을 재료로 사용 |
이미지 파일은 디스크에 저장된 JPG나 PNG입니다. 이 파일을 Python에서 읽으면 코드로 다룰 수 있는 이미지 객체가 생깁니다. PROVIDED_PATH에는 준비 셀이 저장한 파일명 week07_provided_source.jpg가 들어 있습니다. Image.open()에 이 파일명을 전달하고, 읽은 이미지를 photo로 받습니다.
실습 진행 · 한 프로그램에 차례로 추가하기
이제 원본을 읽는 코드부터 직접 작성합니다. 다음 단계마다 앞 코드는 유지하고, 마지막 출력 두 줄 위에 새 작업을 추가합니다. 출력 두 줄은 그 단계의 이미지 이름으로 바꿉니다. 각 단계에 전체 누적 코드를 함께 두었으니 추가 위치와 변수 이름을 확인하세요. 노트북의 단계별 코드도 같은 내용입니다.
제작 순서는 1. 원본과 작업본 → 2. 자르기 → 3. 크기 변경 → 4. 회전 → 5. 경계 확장입니다. 중간의 A·B·C 비교는 별도 셀에서 실행한 뒤 이 순서로 돌아옵니다. 각 누적 코드는 공통 준비만 실행하면 앞의 비교 셀 없이도 실행할 수 있습니다.
코드 읽기 · 파일을 읽고 작업본 준비하기
파일에서 읽은 이미지를 변수에 담고, 크기를 확인하거나 변형할 준비를 합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
photo = Image.open(PROVIDED_PATH) | Pillow의 Image 도구로 파일을 엽니다. | 읽은 이미지 객체를 photo에 받습니다. |
working_image = photo.convert("RGBA") | 빨강·초록·파랑·투명도 정보를 가진 작업본을 만듭니다. | 새 이미지를 working_image에 받습니다. 투명도는 2교시에 배웁니다. |
photo.close() | 읽기를 마친 photo의 자원을 닫습니다. | 원본 파일과 이미 만든 working_image는 남습니다. |
working_image.size | 이미지의 (너비, 높이) 속성을 읽습니다. | (1000, 1000). 속성이므로 size 뒤에 ()를 붙이지 않습니다. |
print("결과 크기:", working_image.size) | 설명과 크기를 글자로 출력합니다. | 결과 크기: (1000, 1000) |
display(working_image) | 이미지를 화면에 표시합니다. | 사진 미리보기. 단계마다 표시할 이미지 이름을 바꿉니다. |
확인할 점 · convert()·close()처럼 객체 뒤에 점을 붙여 실행하는 기능을 ‘메서드’라고 합니다. 이후 변형은 working_image에 적용합니다.
1단계 · 원본과 작업본 준비
이 코드로 제작을 시작합니다. 아래 단계에서는 print() 위에 새 작업을 추가하고, 맨 아래 print()와 display()가 보여 줄 이미지 이름을 바꿉니다.
# 1. 원본을 읽고 작업본 준비
photo = Image.open(PROVIDED_PATH)
working_image = photo.convert("RGBA")
photo.close()
# 현재 단계의 결과 확인
print("결과 크기:", working_image.size)
display(working_image)출력 (1000, 1000)은 너비와 높이가 각각 1000픽셀이라는 뜻입니다. 브라우저가 그림을 화면 폭에 맞춰 보여주더라도 실제 픽셀 수는 이 값으로 확인합니다.
픽셀 크기와 화면 표시 크기
눈에는 하나의 장면으로 보이지만, 컴퓨터는 사진을 색이 담긴 작은 칸들의 모임으로 다룹니다. 이 칸을 픽셀이라고 합니다. 1000 × 1000은 가로 1000칸, 세로 1000칸으로 모두 1,000,000픽셀이라는 뜻입니다. 화면에서 얼마나 크게 보여 줄지는 표시 설정에 따라 달라집니다.
두 사진의 표시 폭은 다릅니다. 파일을 열어 .size를 읽으면 서로 다른 값이 나올까요?
두 그림은 같은 JPG 파일을 사용하므로 실제 크기는 둘 다 1000 × 1000입니다. 브라우저를 좁히거나 화면에서 작게 보여도 파일의 픽셀 수는 바뀌지 않습니다. 5장에서 배우는 resize()는 지정한 픽셀 크기의 새 이미지를 만드는 작업입니다.
| 종류 | 무엇을 뜻하나 | 확인 방법 |
|---|---|---|
| 픽셀 크기 | 이미지의 가로·세로 픽셀 수 | 이미지 객체의 .size |
| 화면 표시 크기 | 현재 화면에서 차지하는 폭과 높이 | 브라우저·노트북의 표시 설정 |
| 파일 용량 | 저장된 파일의 바이트 수 | 파일 정보의 KB·MB. 포맷과 압축 등에 따라 달라짐 |
파일명 문자열과 photo는 같은 것인가요?
파일명은 재료의 위치를 가리키고, photo는 그 파일에서 읽은 이미지 객체입니다. 자르기나 회전은 파일명 문자열이 아니라 읽어 온 이미지에 적용합니다. display()는 표시만 하며 원본 파일을 덮어쓰지 않습니다.
여기부터 변형의 기준은 working_image입니다. 왼쪽 위가 (0, 0)이고 x는 오른쪽, y는 아래로 커집니다. 다음 절에서 이 좌표를 실제 사진 위에 표시해 봅니다. 원본 파일은 유지하며, 결과를 파일로 남길 때는 새 이름으로 저장합니다.
4. 이미지 자르기
의자를 강조하려면 사진을 작게 만들면 될까요, 남길 영역을 골라야 할까요?

종이 프레임을 의자 가까이 옮겨 봅니다. 바다가 넓게 보일 때는 작았던 의자가, 주변을 덜어 내자 눈에 들어옵니다. 사진 속 의자의 크기는 그대로지만 무엇을 남기느냐에 따라 시선이 머무는 곳이 달라집니다. 코드의 자르기도 원본에서 선택한 영역을 새 이미지로 가져오는 작업입니다.
사진 전체를 줄이면 하늘·바다·바닥도 함께 남습니다. 특정 대상을 강조하려면 먼저 보여 줄 영역을 선택해야 합니다. crop()에는 (left, top, right, bottom), 즉 왼쪽·위·오른쪽·아래 경계를 넣습니다. 마지막 두 값은 너비와 높이가 아니라 끝 경계입니다.


B의 너비는 850−250=600, 높이는 1000−400=600입니다. 원본의 좌표로 영역을 선택했지만, 자른 이미지 안에서는 새 왼쪽 위부터 좌표를 읽습니다. 먼저 끝 경계를 보고 크기를 계산한 뒤 결과와 맞는지 확인합니다.
함께 만들 커버에는 A의 (50, 50, 950, 950), 즉 가운데 900 × 900 영역을 사용합니다. 앞서 작성한 원본 준비 코드 아래에 자르기를 추가합니다.
코드 읽기 · 자르기 좌표와 반환값
crop()은 이미지에서 사각형 영역을 선택해 새 이미지로 돌려주는 메서드입니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
demo_crop_box = (50, 50, 950, 950) | 경계 네 값을 순서대로 묶습니다. 이 묶음을 ‘튜플’이라고 합니다. | 왼쪽 50 · 위 50 · 오른쪽 950 · 아래 950 |
validate_crop(working_image, demo_crop_box) | 좌표가 이미지 안에 있고 너비·높이가 양수인지 검사합니다. 수업용 제공 함수입니다. | 검사만 수행합니다. 이미지 변화는 없습니다. |
cropped_image = working_image.crop(demo_crop_box) | 지정한 영역을 잘라 새 이미지를 만듭니다. | cropped_image에 900 × 900 이미지가 담깁니다. working_image는 그대로입니다. |
확인할 점 · 950, 950은 오른쪽·아래의 끝 경계입니다. 너비와 높이는 각각 950 − 50 = 900픽셀입니다.
2단계 · 자르기 추가
1단계의 photo.close() 아래에 ‘# 2. 자르기’의 세 줄을 추가합니다. 마지막 두 줄은 cropped_image를 확인하도록 바꿉니다.
# 1. 원본을 읽고 작업본 준비
photo = Image.open(PROVIDED_PATH)
working_image = photo.convert("RGBA")
photo.close()
# 2. 자르기
demo_crop_box = (50, 50, 950, 950)
validate_crop(working_image, demo_crop_box)
cropped_image = working_image.crop(demo_crop_box)
# 현재 단계의 결과 확인
print("결과 크기:", cropped_image.size)
display(cropped_image)실습 2 · 자르기 영역 비교
- 공통 원리
- 같은 원본에서 선택 영역만 달리합니다.
- 유지 요소
- 작업본 1000 × 1000 · 기준 cropped_image
2단계 코드를 실행한 뒤, 별도의 비교 셀에서 A·B·C를 확인합니다. 출력 크기를 먼저 예측하고, 세 결과에서 의자와 수평선이 어디에 남는지 비교합니다. 비교를 마치면 제작 코드의 3단계로 이어갑니다.
코드 읽기 · 여러 결과와 이름표를 함께 표시하기
comparison_sheet()는 수업에서 제공하는 비교판 제작 함수입니다. Pillow의 기본 기능이 아니라 준비 셀에 정의한 도구입니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
[crop_a, crop_b, crop_c] | 이미지 세 장을 순서대로 담습니다. 대괄호로 묶은 것을 ‘리스트’라고 합니다. | A → B → C 순서의 이미지 목록 |
f"B / {crop_b.size}" | f 문자열은 중괄호 안의 값을 글자에 넣습니다. | B / (600, 600)이라는 이름표 |
display(comparison_sheet(이미지_목록, 이름표_목록)) | 제공 함수가 만든 비교판을 화면에 표시합니다. | 이미지와 이름표가 같은 순서로 나란히 표시됩니다. |
확인할 점 · 비교판에 맞춘 표시 크기와 실제 이미지 크기는 다를 수 있습니다. 실제 픽셀 수는 이름표의 .size로 확인합니다.
실습 코드
crop_a = working_image.crop((50, 50, 950, 950))
crop_b = working_image.crop((250, 400, 850, 1000))
crop_c = working_image.crop((0, 200, 1000, 600))
display(comparison_sheet([crop_a, crop_b, crop_c], [f"A / {crop_a.size}", f"B / {crop_b.size}", f"C / {crop_c.size}"]))각 결과를 crop_a, crop_b, crop_c로 받아 한 비교판에 남깁니다. 기준 cropped_image는 바뀌지 않아 다음 제작 단계에 그대로 이어집니다. comparison_sheet()는 제공된 표시 도구입니다.
| 버전 | 경계 좌표 | 결과 크기 |
|---|---|---|
| A | (50, 50, 950, 950) | 900 × 900 |
| B | (250, 400, 850, 1000) | 600 × 600 |
| C | (0, 200, 1000, 600) | 1000 × 400 |
(250, 400)에서 600 × 600을 남기려면 끝 경계는 무엇인가요?
오른쪽은 250+600=850, 아래는 400+600=1000입니다. 시작점에 너비와 높이를 더해 끝 경계를 정합니다. 수업의 validate_crop()는 네 좌표가 작업본 안에 있는지 확인하는 제공 함수입니다.
다음 단계의 입력
cropped_image에는 A의 900 × 900 영역이 남아 있습니다. 무엇을 보여 줄지 골랐으니, 이제 그 장면을 얼마나 크게 놓을지 정합니다.
5. 크기 변경과 비율
같은 사진인데 왜 왼쪽 표지에서는 사진이 더 눈에 띌까요?

사진을 작게 놓으면 주위에 빈 공간이 생깁니다. 그 여백 때문에 같은 장면을 조금 떨어져 바라보는 듯한 느낌이 들기도 합니다. 두 표지의 크기와 사진에 담긴 장면은 같고, 사진이 차지하는 면적만 다릅니다. 코드에서는 목표 너비와 높이로 이 면적을 조절합니다. 가로와 세로를 같은 비율로 줄이면 사진의 형태도 유지됩니다.
resize((width, height))는 선택한 영역의 목표 너비와 높이를 정합니다. 자르기는 남길 내용을 고르고, 크기 변경은 그 내용을 몇 픽셀로 표현할지 정합니다. 기준 커버는 큰 조각을 회전 전에 760 × 760으로 만듭니다.


760은 큰 사진의 비중을 정한 예제의 디자인 값입니다. 다른 작품에서도 반드시 이 숫자를 쓸 필요는 없습니다. 위 두 틀의 표시 크기는 같고, 안에 놓은 사진의 실제 픽셀 수가 다릅니다. 앞 절에서 각 자르기 결과를 비슷한 크기로 보여 준 미리보기와 구분합니다.
코드 읽기 · 목표 크기 지정하기
resize()는 원본을 유지하면서 지정한 너비·높이의 새 이미지를 반환합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
cropped_image.resize((760, 760), Image.Resampling.LANCZOS) | 안쪽 (760, 760)은 목표 (너비, 높이)입니다. 바깥 괄호로 메서드를 호출합니다. | 760 × 760의 새 이미지를 반환합니다. |
Image.Resampling.LANCZOS | 크기가 바뀔 때 픽셀의 색을 계산하는 방식을 지정합니다. | 이번 실습에서는 그대로 사용하는 제공 옵션 |
확인할 점 · 크기를 정하는 값은 resize() 안의 두 숫자입니다. 원본 준비와 자르기는 그대로 두고, 자른 결과를 다음 입력으로 사용합니다.
3단계 · 크기 변경 추가
2단계의 cropped_image를 만드는 줄 아래에 ‘# 3. 크기 변경’의 한 줄을 추가합니다. 마지막 두 줄은 resized_image를 확인하도록 바꿉니다.
# 1. 원본을 읽고 작업본 준비
photo = Image.open(PROVIDED_PATH)
working_image = photo.convert("RGBA")
photo.close()
# 2. 자르기
demo_crop_box = (50, 50, 950, 950)
validate_crop(working_image, demo_crop_box)
cropped_image = working_image.crop(demo_crop_box)
# 3. 크기 변경
resized_image = cropped_image.resize((760, 760), Image.Resampling.LANCZOS)
# 현재 단계의 결과 확인
print("결과 크기:", resized_image.size)
display(resized_image)새로 만든 resized_image는 760 × 760입니다. 자르기 결과인 cropped_image는 900 × 900인 채 남고, 다음 회전 작업에는 resized_image를 사용합니다.
760 × 760에서 가로를 절반인 380으로 줄일 때, 형태를 유지하려면 세로는 얼마여야 할까요?
실습 3 · 크기와 가로세로 비율 비교
- 공통 원리
- 같은 장면의 가로·세로 배율을 비교합니다.
- 유지 요소
- 900 × 900 자르기 결과 · 기준 resized_image
3단계 코드를 실행한 뒤, 별도의 비교 셀에서 B와 C의 결과를 예측하고 확인합니다. 형태가 유지되는 이유와 눌리는 이유를 설명한 뒤 제작 코드의 4단계로 이어갑니다.
코드 읽기 · 비교 축척 지정하기
비교판에서는 작은 이미지가 실제로 얼마나 작아졌는지도 함께 확인합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
preserve_scale=True | 비교판의 이미지를 같은 축척으로 표시합니다. | 작은 조각은 비교판에서도 상대적으로 작게 보입니다. |
True / False | 따옴표 없이 쓰는 참·거짓 값입니다. | 이 옵션에 True를 넣으면 같은 축척 표시를 켭니다. |
확인할 점 · preserve_scale은 비교판의 표시 방식이며 조각의 실제 크기를 바꾸지 않습니다.
실습 코드
resize_a = cropped_image.resize((760, 760), Image.Resampling.LANCZOS)
resize_b = cropped_image.resize((380, 380), Image.Resampling.LANCZOS)
resize_c = cropped_image.resize((380, 760), Image.Resampling.LANCZOS)
display(comparison_sheet([resize_a, resize_b, resize_c], [f"A / {resize_a.size}", f"B / {resize_b.size}", f"C / {resize_c.size}"], preserve_scale=True))B는 가로·세로를 모두 절반으로 줄입니다. C는 가로만 절반이어서 좁아집니다. 비율을 유지하려면 두 길이에 같은 배율을 적용합니다. 비교 변수만 바뀌고 기준 예제는 유지됩니다.
C처럼 가로로 긴 자르기 결과를 정사각형으로 바꾸면 어떻게 되나요?
입력과 목표의 가로세로 비율이 달라 형태가 눌리거나 늘어납니다. 정사각형 커버의 조각이라면 정사각형으로 자르는 방법이 있고, 전체 장면을 남겨야 한다면 비율을 유지해 줄이고 여백을 둘 수도 있습니다.
작게 줄인 조각을 다시 키우면 원래의 세부가 돌아오나요?
축소하면서 사라진 세부가 단순 확대로 복원되지는 않습니다. 크기가 다른 조각은 충분히 큰 같은 cropped_image에서 각각 만듭니다.
비교 실습 · 작은 미리보기와 원본 크기
- 공통 원리
- 새 이미지를 반환받아 표시용 복사본만 줄입니다.
- 유지 요소
- 작업본 working_image는 1000 × 1000
작은 미리보기를 만들면 작업본의 크기도 바뀔지 예상한 뒤 실행합니다. 두 변수의 .size를 각각 읽습니다.
코드 읽기 · 표시용 복사본과 작업본
이미지의 픽셀 크기는 그 이미지 객체의 size로 확인합니다. 화면의 표시 폭과 구분합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
screen_preview = working_image.resize((250, 250), ...) | 작은 새 이미지를 만들어 별도 변수에 받습니다. | screen_preview만 250 × 250입니다. |
working_image.size / screen_preview.size | 각 이미지의 실제 픽셀 크기를 읽습니다. | 작업본 1000 × 1000 / 미리보기 250 × 250 |
확인할 점 · 표시용 결과를 원본 이름에 다시 대입하지 않았으므로 뒤의 제작은 1000 × 1000 작업본에서 계속됩니다.
실습 코드
# 표시용 작은 복사본을 만들어도 working_image는 유지됩니다.
screen_preview = working_image.resize((250, 250), Image.Resampling.LANCZOS)
print("작업본 픽셀 크기:", working_image.size)
print("표시용 복사본 픽셀 크기:", screen_preview.size)
display(screen_preview)작업본은 1000 × 1000이고 새 미리보기만 250 × 250입니다. 단순히 브라우저에서 표시 폭을 줄이는 것과 달리, 이 코드는 작은 픽셀 크기의 이미지를 새로 만듭니다. 결과를 다른 변수에 받았으므로 뒤의 제작은 원래 작업본에서 이어집니다.
결과 비교와 해설
출력 확인
작업본 픽셀 크기: (1000, 1000)
표시용 복사본 픽셀 크기: (250, 250)
6. 이미지 회전과 경계
사진 크기와 기울기는 같은데, 왜 한쪽에서는 모서리가 보이지 않을까요?

반듯했던 수평선을 조금 기울이면 익숙한 바다도 다르게 보입니다. 다만 사진을 돌리면 모서리가 원래의 가로·세로 범위 밖으로 나갑니다. 좁은 프레임에서는 그 부분이 가려지고, 넓은 프레임에서는 사진 전체가 보입니다. 회전 코드의 expand는 이처럼 결과를 담을 사각형을 넓힐지 정합니다. 사진 자체의 크기는 그대로입니다.
이제 사진 한 장의 방향을 바꿉니다. 아래는 같은 사진을 서로 다른 각도로 기울인 모습입니다. 음수는 시계 방향, 양수는 반시계 방향이며, 0이면 회전하지 않습니다. 사진의 위쪽 변과 수평선이 어느 쪽으로 기울어지는지 비교합니다.

이번 제작 코드에서는 first_layer_angle = -12로 정해, 크기를 바꾼 사진을 시계 방향으로 12도 돌립니다. 먼저 방향을 확인하고, 실행 결과에서 사진의 모서리가 잘리는 문제를 살펴봅니다.
실습 4 · 회전과 경계 비교
- 공통 원리
- 회전 방향을 확인한 뒤 출력 경계를 조절합니다.
- 유지 요소
- 760 × 760 기준 조각 · -12도
A각도 적용
크기를 바꾼 이미지에 rotate()를 이어 붙입니다. 각도는 -12도이며, 아직 출력 경계는 넓히지 않습니다. 사진의 어느 쪽이 내려갈지 예상한 뒤 실행하고, 수평선과 네 모서리를 봅니다. 품질과 빈 모서리 옵션은 아래 설명을 읽고 제공값을 사용합니다.
코드 읽기 · 회전 각도와 결과 받기
rotate()는 이미지를 지정한 각도만큼 돌린 새 이미지를 반환합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
first_layer_angle = -12 | 회전 각도를 도(°)로 정합니다. 양수는 반시계, 음수는 시계 방향입니다. | 시계 방향으로 12도 회전. 0이면 회전하지 않습니다. |
clipped_image = resized_image.rotate(...) | 크기를 바꾼 이미지를 회전시킵니다. | clipped_image에 새 이미지가 담깁니다. expand를 생략하면 760 × 760을 유지합니다. |
resample=Image.Resampling.BICUBIC | 회전한 위치의 픽셀 색을 계산하는 방식을 정합니다. | 그대로 사용하는 회전 품질 옵션 |
fillcolor=(0, 0, 0, 0) | 빈 모서리를 채울 RGBA 값을 정합니다. 마지막 0은 완전 투명입니다. | 회전으로 생긴 빈 모서리가 투명해집니다. |
rotate( … ) | 괄호 안에서 입력을 쉼표로 나누어 여러 줄에 씁니다. | 여러 줄로 보여도 한 번의 메서드 호출입니다. |
확인할 점 · resized_image는 회전 전, clipped_image는 회전 후입니다. 두 이미지의 크기와 모서리를 함께 비교합니다.
4단계 · 회전 추가
3단계의 resized_image를 만드는 줄 아래에 ‘# 4. 회전’을 추가합니다. 마지막 두 줄은 clipped_image를 확인하도록 바꿉니다. 아직 출력 경계를 넓히지 않습니다.
# 1. 원본을 읽고 작업본 준비
photo = Image.open(PROVIDED_PATH)
working_image = photo.convert("RGBA")
photo.close()
# 2. 자르기
demo_crop_box = (50, 50, 950, 950)
validate_crop(working_image, demo_crop_box)
cropped_image = working_image.crop(demo_crop_box)
# 3. 크기 변경
resized_image = cropped_image.resize((760, 760), Image.Resampling.LANCZOS)
# 4. 회전
first_layer_angle = -12
clipped_image = resized_image.rotate(
first_layer_angle,
resample=Image.Resampling.BICUBIC,
fillcolor=(0, 0, 0, 0),
)
# 현재 단계의 결과 확인
print("결과 크기:", clipped_image.size)
display(clipped_image)
B출력 경계 확장
같은 크기의 사각형을 유지해서 모서리가 잘렸습니다. 사진 전체를 남기려면 expand=True를 더해 회전한 조각이 들어갈 출력 경계를 넓힙니다. 이번에는 출력 크기도 달라질지 예측합니다.
코드 읽기 · expand로 회전 경계 넓히기
rotate()에 expand=True를 추가해 사진의 모서리가 잘리지 않도록 결과 이미지의 경계를 넓힙니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
expand=True | 회전한 사진 전체가 들어가도록 출력 경계를 넓힙니다. | 902 × 902. 사진의 모서리가 보존됩니다. |
expand=False | 출력 경계를 원래 크기로 유지합니다. 생략할 때도 적용됩니다. | 760 × 760. 경계 밖의 모서리가 잘립니다. |
확인할 점 · -12도는 그대로 두고 expand만 비교합니다. expand는 출력 경계를 넓히는 옵션이며 사진 자체를 확대하지 않습니다.
5단계 · 회전 경계 확장
4단계의 회전 코드 아래에 ‘# 5. 경계 확장’을 추가합니다. 같은 입력에서 expand=True인 결과를 rotated_image로 만들고, 마지막 두 줄도 이 이름으로 바꿉니다. 사진 조각 하나가 완성되면 실습 5에서 이 과정을 함수로 묶습니다.
# 1. 원본을 읽고 작업본 준비
photo = Image.open(PROVIDED_PATH)
working_image = photo.convert("RGBA")
photo.close()
# 2. 자르기
demo_crop_box = (50, 50, 950, 950)
validate_crop(working_image, demo_crop_box)
cropped_image = working_image.crop(demo_crop_box)
# 3. 크기 변경
resized_image = cropped_image.resize((760, 760), Image.Resampling.LANCZOS)
# 4. 회전
first_layer_angle = -12
clipped_image = resized_image.rotate(
first_layer_angle,
resample=Image.Resampling.BICUBIC,
fillcolor=(0, 0, 0, 0),
)
# 5. 경계 확장
rotated_image = resized_image.rotate(
first_layer_angle,
expand=True,
resample=Image.Resampling.BICUBIC,
fillcolor=(0, 0, 0, 0),
)
# 현재 단계의 결과 확인
print("결과 크기:", rotated_image.size)
display(rotated_image)5단계의 전체 코드를 실행하면 rotated_image에 902 × 902 사진 조각이 남습니다. 원본 준비부터 회전 경계까지 한 코드로 연결했으며, 다음 교시에도 이 기준 결과를 사용합니다.
expand=True이면 최종 커버도 902 × 902가 되나요?
902 × 902는 회전한 조각의 크기입니다. 조각을 올릴 캔버스는 2교시에 별도로 1000 × 1000으로 만듭니다. 회전 경계와 최종 화면 경계는 서로 다른 선택입니다.
7. 변형 순서와 결과 비교
사진을 조금씩 바꾸다 보면 지금 보이는 장면이 어느 단계에서 만들어졌는지 놓치기 쉽습니다. 잠시 과정을 거슬러 올라가 봅니다. 어느 영역을 남겼고, 얼마나 줄였으며, 어느 쪽으로 기울였나요?

| 단계 | 입력 → 출력 | 결정하는 것 |
|---|---|---|
| 자르기 | working_image → cropped_image | 어떤 장면을 남길지 |
| 크기 | cropped_image → resized_image | 몇 픽셀로 표현할지 |
| 회전 | resized_image → rotated_image | 방향과 출력 경계 |
순서를 바꾸면 좌표의 기준도 바뀝니다. 먼저 원본을 500 × 500으로 줄이고 원래 자르기 좌표를 그대로 넣으면 같은 영역을 선택하지 못합니다. 순서를 언제나 고정해야 한다는 뜻이 아니라, 어느 단계의 크기와 좌표를 쓰는지 알아야 한다는 뜻입니다.
비교 실습 · 같은 좌표, 다른 기준 이미지
- 공통 원리
- 결과 크기를 같게 두고 작업 순서와 좌표의 기준을 비교합니다.
- 유지 요소
- 작업본 1000 × 1000 · 결과 세 장 모두 200 × 200
실행 전 A와 B에 같은 장면이 남을지 예상합니다. B에서 좌표를 읽는 이미지의 크기를 먼저 확인한 뒤, C처럼 좌표도 절반으로 바꾸어 비교합니다.
코드 읽기 · 어느 이미지의 좌표인지 읽기
image.crop(box)는 image의 왼쪽 위를 (0, 0)으로 삼아 영역을 자릅니다. 같은 좌표를 넣어도 image의 크기가 달라졌다면 다른 영역이 선택될 수 있습니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
working_image.crop(order_box) | 1000 × 1000 작업본에서 좌표를 읽습니다. | 원본의 (100, 100)부터 (500, 500)까지 |
half_source.crop(order_box) | 500 × 500으로 줄인 이미지에서 같은 숫자를 읽습니다. | 원본의 (200, 200)부터 (1000, 1000)에 대응하는 영역 |
half_source.crop((50, 50, 250, 250)) | 축소 비율 1/2을 좌표 네 값에도 적용합니다. | A와 같은 원본 영역. 출력은 200 × 200 |
확인할 점 · 각 단계에서 사용하는 이미지의 크기를 확인합니다. A·C는 같은 원본 영역을 선택하지만, 크기 변경 때 계산에 쓰는 주변 픽셀이 달라 세부 픽셀은 조금 다를 수 있습니다.
실습 코드
# A. 원본에서 400 × 400을 자른 뒤 200 × 200으로 줄입니다.
order_box = (100, 100, 500, 500)
crop_first = working_image.crop(order_box)
order_a = crop_first.resize((200, 200), Image.Resampling.LANCZOS)
# B. 원본을 절반으로 줄이고, 같은 숫자의 좌표를 씁니다.
half_source = working_image.resize((500, 500), Image.Resampling.LANCZOS)
resize_first = half_source.crop(order_box)
order_b = resize_first.resize((200, 200), Image.Resampling.LANCZOS)
# C. 원본 크기가 절반이면 자르기 좌표도 절반으로 바꿉니다.
scaled_box = (50, 50, 250, 250)
order_c = half_source.crop(scaled_box)
display(comparison_sheet([order_a, order_b, order_c], ["A / crop then resize", "B / same numbers", "C / scaled coordinates"]))B의 (100, 100, 500, 500)은 원본의 (200, 200, 1000, 1000)에 대응합니다. C는 좌표도 절반으로 바꿔 A와 같은 원본 영역을 선택합니다. 같은 크기로 출력했다고 같은 장면을 고른 것은 아닙니다.
원본을 1/4로 줄였다면 같은 영역의 좌표는?
(100, 100, 500, 500)의 각 값을 4로 나눈 (25, 25, 125, 125)입니다. 한 변의 픽셀 수가 줄어든 비율을 좌표에도 적용합니다. 순서를 바꾸려면 이후 입력의 좌표와 크기를 함께 검토해야 합니다.
8. 변형 코드를 함수로 묶기
다시 만들고 싶은 조각이 생기면, 그 조각을 만든 방법도 남겨 둘 필요가 있습니다. 먼저 자신의 사진 조각을 만들고, 두 번째 조각에서 반복되는 작업을 찾아봅니다. 그 작업을 함수로 묶으면 다음에는 바꿀 값만 정해 새 조각을 만들 수 있습니다.
실습 5 · 변형 코드에서 재사용 함수로
- 만들 결과
- 직접 정의한 변형 함수와 크기·각도가 다른 사진 조각 세 개
- 진행 순서
- 직접 연결 → 다른 조각 → 함수로 묶기 → 세 번 호출
A변형 과정 직접 연결
의자·바다·하늘 중 강조할 부분을 정합니다. 아래는 시작 예시이므로 대상과 이유를 자신의 말로 적고, 영역·크기·각도 중 하나 이상을 바꿉니다. 결과를 보기 전에 자른 크기와 회전 후 크기 변화를 예상합니다.
선택과 제작을 하나의 셀에 작성합니다. 아래 설정 다음에 세 변형을 이어 붙이세요. 첫 입력은 working_image이며 다음 줄은 앞 줄이 반환한 이미지를 받아야 합니다. 해설을 펼치기 전에 display(personal_piece)로 확인합니다.
# 선택과 이유
personal_subject = "의자와 바닥의 그림자"
personal_crop_box = (250, 400, 850, 1000)
personal_size = (500, 500)
personal_angle = -8
personal_reason = "의자를 크게 남기고 조금 기울여 기다리는 장면을 표현한다."
# 위의 설정을 사용해 세 줄을 직접 작성합니다.
# 1. working_image에서 영역을 잘라 personal_cropped에 받습니다.
# 2. 잘린 결과의 크기를 바꾸어 personal_resized에 받습니다.
# 3. 회전 경계를 넓혀 personal_piece에 받습니다.
# 마지막에 display(personal_piece)로 확인합니다.완성 코드와 결과 비교
코드 읽기 · 해설에 포함된 입력 검사
해설의 첫 두 줄은 자르기 영역과 목표 크기가 올바른지 검사하는 제공 코드입니다. 그다음은 직접 작성한 자르기·크기 변경·회전입니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
assert 조건, "안내 문장" | 조건이 참인지 확인합니다. | 참이면 계속 진행하고, 거짓이면 안내 문장과 함께 중단합니다. |
len(personal_size) == 2 | len()으로 너비·높이 값이 두 개인지 셉니다. | 값이 두 개일 때 True |
all(type(value) is int and 1 <= value <= 1000 for value in personal_size) | for로 값을 하나씩 읽습니다. type으로 정수인지, and로 범위도 맞는지 확인합니다. | 너비·높이가 모두 1~1000 사이 정수이면 all()이 True를 반환합니다. |
확인할 점 · 검사 코드는 제공된 그대로 사용합니다. 안내가 나오면 personal_size를 1~1000 사이 정수 두 개로 고친 뒤 설정부터 다시 실행합니다.
# @title 개별 실습 해설: 작성한 세 단계와 비교
validate_crop(working_image, personal_crop_box)
assert len(personal_size) == 2 and all(type(value) is int and 1 <= value <= 1000 for value in personal_size), "목표 크기는 1~1000 사이 정수 두 개입니다."
personal_cropped = working_image.crop(personal_crop_box)
personal_resized = personal_cropped.resize(personal_size, Image.Resampling.LANCZOS)
personal_piece = personal_resized.rotate(personal_angle, expand=True, resample=Image.Resampling.BICUBIC, fillcolor=(0, 0, 0, 0))
print("선택한 대상:", personal_subject, "/ 선택 이유:", personal_reason)
print("자르기 / 크기 변경 / 회전:", personal_cropped.size, personal_resized.size, personal_piece.size)
display(comparison_sheet([personal_cropped, personal_resized, personal_piece], ["my crop", "my resize", "my rotation"], preserve_scale=True))
처음 강조하려던 대상이 눈에 들어오는지 살펴봅니다. 가로세로 비율과 기울기는 의도에 맞는지, 모서리는 예상대로 남았는지도 확인합니다. 해설 코드의 보간 옵션은 제공값을 사용해도 됩니다. 자르기 결과가 크기 변경에, 크기 변경 결과가 회전에 어떻게 쓰였는지 자신의 코드에서 짚어 봅니다.
B두 번째 조각에서 반복 찾기
첫 조각은 남겨 두고 같은 영역에서 크기와 각도가 다른 조각을 하나 더 만듭니다. A의 세 변형과 아래 코드를 비교하며 반복되는 작업과 달라지는 값을 찾아보세요. A에서 작성한 셀 또는 해설을 실행한 뒤 진행합니다.
# 같은 개인 영역에서 크기와 각도만 다른 조각을 추가합니다.
second_cropped = working_image.crop(personal_crop_box)
second_resized = second_cropped.resize((320, 320), Image.Resampling.LANCZOS)
second_piece = second_resized.rotate(
12, expand=True, resample=Image.Resampling.BICUBIC, fillcolor=(0, 0, 0, 0)
)
display(comparison_sheet([personal_piece, second_piece], ["first piece", "second piece"], preserve_scale=True))사진 조각이 열 개라면 자르기·크기 변경·회전 코드를 열 번 써야 할까요? 공통 작업과 바꿀 값을 어떻게 나눌 수 있을까요?
C작성한 코드를 함수로 묶기
6주차처럼 같은 작업을 함수 안에 두고, 달라질 값을 매개변수로 받습니다. transform_piece()는 지금 직접 만드는 사용자 정의 함수이며 Pillow의 기본 함수가 아닙니다. 새 셀에 아래 전체 정의를 작성하고, 그 아래에서 호출합니다. 기존 세 변형의 순서는 유지합니다.
코드 읽기 · 변형 코드를 함수로 옮기기
transform_piece()는 우리가 이름을 정해 만드는 함수입니다. 6주차의 def·매개변수·return을 방금 작성한 이미지 변형에 적용합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
def transform_piece(source, crop_box, target_size, angle): | 사용할 이미지·영역·크기·각도를 입력으로 받도록 정의합니다. | 정의를 실행하면 호출할 준비가 됩니다. 아직 조각은 만들지 않습니다. |
cropped = source.crop(crop_box) | 받은 이미지와 영역으로 자릅니다. | 함수 안의 cropped에 이번 호출의 결과가 담깁니다. |
resized = cropped.resize(target_size, ...) | 받은 크기로 바꿉니다. 품질 옵션은 앞 실습과 같습니다. | resized가 다음 회전의 입력이 됩니다. |
rotated = resized.rotate(angle, expand=True, ...) | 받은 각도로 회전하고 경계를 넓힙니다. | rotated에 아직 배치하지 않은 사진 조각이 담깁니다. |
return rotated | 완성된 조각을 호출한 곳으로 돌려줍니다. | 반환된 이미지를 함수 밖의 변수로 받을 수 있습니다. |
function_piece = transform_piece(...) | 정의한 함수에 실제 값을 넣어 실행합니다. | function_piece에 이번 호출에서 만든 조각이 담깁니다. |
확인할 점 · def 아래부터 return까지는 들여쓰기합니다. 함수 밖의 호출은 들여쓰기하지 않습니다. return을 display로 바꾸면 조각이 반환되지 않아 뒤에서 합성할 수 없습니다.
def transform_piece(source, crop_box, target_size, angle):
validate_crop(source, crop_box)
cropped = source.crop(crop_box)
resized = cropped.resize(target_size, Image.Resampling.LANCZOS)
rotated = resized.rotate(
angle,
expand=True,
resample=Image.Resampling.BICUBIC,
fillcolor=(0, 0, 0, 0),
)
return rotated
# 정의를 실행한 뒤, 앞에서 선택한 값으로 호출합니다.
function_piece = transform_piece(working_image, personal_crop_box, personal_size, personal_angle)
display(comparison_sheet([personal_piece, function_piece], ["direct code", "function call"], preserve_scale=True))두 출력은 같은 개인 설정으로 만들어졌으므로 모양과 크기가 같아야 합니다. 함수 안의 rotated가 return을 거쳐 함수 밖의 function_piece에 담깁니다. 정의만 실행하면 제작 방법을 등록한 상태이고, 아래의 호출이 실제 조각을 만듭니다.
return 대신 display를 쓰면 안 되나요?
display는 화면에 보여 주지만 이미지를 호출한 곳으로 돌려주지 않습니다. return이 없으면 function_piece에는 None이 담겨 다음 합성에 사용할 수 없습니다. 함수는 이미지를 반환하고, 함수 밖에서 그 이미지를 표시합니다.
비교 실습 · 그림은 보이는데 반환값은 None
- 공통 원리
- 화면에 표시하는 일과 호출한 곳으로 돌려주는 일을 구분합니다.
- 유지 요소
- 같은 사진·영역·크기·각도
실행 전에 missing_piece에 무엇이 담길지 예상합니다. 비교 함수의 마지막 줄을 보고, 사진이 보이는 것과 반환값을 받는 것이 같은지 확인합니다.
코드 읽기 · 화면 표시와 함수 반환
비교 함수 transform_without_return은 이미지를 표시하지만 호출한 곳에 전달하지 않습니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
display(rotated) | 함수 안에서 결과를 화면에 표시합니다. | 사진이 보입니다. return을 대신하지는 않습니다. |
missing_piece = transform_without_return(...) | 비교 함수의 반환값을 받습니다. | return이 없으므로 None. 전달된 이미지가 없다는 뜻입니다. |
return rotated | 만든 이미지를 호출한 곳으로 보냅니다. | 받은 이미지에 .size나 합성 기능을 사용할 수 있습니다. |
확인할 점 · 비교 함수의 마지막 줄을 return rotated로 고친 뒤 정의와 호출을 다시 실행합니다. 화면 표시는 함수 밖의 display(missing_piece)로 확인합니다.
실습 코드
# 비교용 잘못된 예: 변형을 했지만 반환 대신 표시만 합니다.
def transform_without_return(source, crop_box, target_size, angle):
cropped = source.crop(crop_box)
resized = cropped.resize(target_size, Image.Resampling.LANCZOS)
rotated = resized.rotate(
angle, expand=True, resample=Image.Resampling.BICUBIC,
fillcolor=(0, 0, 0, 0),
)
display(rotated)
missing_piece = transform_without_return(working_image, demo_crop_box, (390, 390), 12)
print("표시만 한 함수의 반환값:", missing_piece)
returned_piece = transform_piece(working_image, demo_crop_box, (390, 390), 12)
print("이미지를 반환한 함수의 결과 크기:", returned_piece.size)사진이 화면에 보여도 return이 없으면 missing_piece는 None입니다. 비교 함수의 display(rotated)를 return rotated로 고치고 정의와 호출을 다시 실행합니다. 화면 표시는 함수 밖에서 display(missing_piece)로 확인합니다.
결과 비교와 해설
기본 코드의 출력
표시만 한 함수의 반환값: None
이미지를 반환한 함수의 결과 크기: (464, 464). 기존 transform_piece는 수정하지 않았으므로 다음 제작에 그대로 사용합니다.
함수 읽기 순서
호출의 네 입력 → 정의의 네 매개변수 → 세 변형 → return → 함수 밖에서 받은 변수. 이 순서대로 방금 만든 조각의 경로를 말해 봅니다.
9. 세 조각 준비와 결과 확인
실습 6 · 같은 함수로 세 조각 준비
- 유지할 것
- 같은 함수와 원본 영역
- 바꿀 것
- 회전 전 크기와 각도
이제 기준 예제로 돌아옵니다. 개인 영역 대신 demo_crop_box를 사용하고, 같은 함수에 크기와 각도만 다르게 넣어 세 번 호출합니다. 아래 호출 코드를 함수 정의 아래에 이어 붙입니다.
코드 읽기 · 같은 함수에 다른 값 전달하기
정의는 한 번 두고 호출마다 크기와 각도만 바꿉니다. 각 호출은 새 이미지 하나를 반환합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
working_image → source | 원본에서 읽은 작업본을 첫 입력으로 보냅니다. | 호출마다 같은 작업본에서 시작합니다. |
demo_crop_box → crop_box | 기준 예제의 영역 네 값을 두 번째 입력으로 보냅니다. | 세 조각에 같은 의자와 바다가 남습니다. |
(760, 760) → target_size / -12 → angle | 회전 전 크기와 회전 각도를 순서대로 전달합니다. | 큰 조각은 시계 방향 12도로 기울어집니다. |
back_piece / middle_piece / front_piece | 세 번의 반환값을 서로 다른 이름으로 받습니다. | 아직 합성하지 않은 독립된 조각 세 개가 남습니다. |
확인할 점 · 작은 조각도 back_piece를 다시 줄이지 않고 working_image에서 만듭니다. 함수 정의를 고쳤다면 정의 셀 → 호출 셀 순서로 다시 실행합니다.
# 공동 제작으로 돌아와, 합성에 쓸 세 조각을 준비합니다.
back_piece = transform_piece(working_image, demo_crop_box, (760, 760), -12)
middle_piece = transform_piece(working_image, demo_crop_box, (560, 560), 0)
front_piece = transform_piece(working_image, demo_crop_box, (390, 390), 12)
display(comparison_sheet(
[back_piece, middle_piece, front_piece],
["760 / -12 degrees", "560 / 0 degrees", "390 / +12 degrees"],
preserve_scale=True,
))| 받는 변수 | 회전 전 크기 | 각도 | 다음 교시의 역할 |
|---|---|---|---|
| back_piece | 760 × 760 | −12° | 뒤에 놓을 큰 조각 |
| middle_piece | 560 × 560 | 0° | 가운데 조각 |
| front_piece | 390 × 390 | +12° | 앞에 놓을 작은 조각 |
변수 이름에 back이나 front가 있어도 아직 앞뒤 순서가 정해진 것은 아닙니다. 현재는 이미지 세 개가 따로 있고, 실제 앞뒤 관계는 2교시에서 캔버스에 올리는 순서로 결정합니다.
남길 기록
강조한 대상 / 자른 영역의 크기 / 목표 크기 / 회전 후 크기 / 이 선택을 한 이유. 선택을 바꾸었다면 작성한 셀 전체를 다시 실행하여 결과와 기록을 맞춥니다.
이전 학습을 실제 제작에 연결하기
4주차에 배운 반복문은 다음 교시에 여러 조각을 배치할 때 사용합니다. 5주차처럼 한 번에 바꿀 값과 고정할 값을 나누면 결과를 비교하기 쉽습니다. 6주차의 함수는 방금 만든 transform_piece()에서 다시 사용했습니다. 그때 만든 artwork.png도 3교시의 재료가 될 수 있습니다.
다음 교시의 합성으로 연결

같은 영역에서 크기와 각도를 달리해 만든 실제 출력. 세 조각은 아직 서로 독립된 이미지입니다.

같은 설정의 조각에 알파를 더하고 위치·순서를 정하면 이 커버가 됩니다.
처음에는 한 장이었던 사진이 크기와 기울기가 다른 세 조각이 되었습니다. 각 조각에는 무엇을 남기고 얼마나 크게 보여 줄지 정한 선택이 담겨 있습니다. 다음 시간에는 이 조각들이 함께 놓일 자리를 정합니다.
transform_piece()에 알파 입력을 더하고, 같은 크기·각도로 조각을 다시 만들어 캔버스에 올립니다. 함수 정의와 호출 코드를 공통 노트북에 남겨 두세요.
기준 예제는 working_image와 demo_crop_box를 이어서 사용합니다. 개인 선택과 이유는 personal_ 변수에 따로 남겨 3교시의 표현 계획에 활용합니다.
수업 후 개별 복습
값을 고쳤는데 다음 결과가 그대로라면?
작성 중인 누적 코드 안의 값을 고치고 셀 전체를 다시 실행합니다. 앞의 학습용 셀만 고쳐도 뒤 셀에 적힌 코드는 자동으로 바뀌지 않습니다. 5단계에서는 자르기·크기·각도를 같은 코드 안에서 확인할 수 있습니다. 비교용 셀의 값을 바꾼 경우에는 기준 예제의 값이 그대로인 것이 정상입니다.
함수 안의 코드를 수정했는데 조각이 그대로라면?
수정한 함수 정의를 먼저 실행하고 조각을 만드는 호출도 다시 실행합니다. 이미 변수에 담긴 이미지는 정의만 고쳐도 자동으로 바뀌지 않습니다. 정의 → 호출 → 결과 확인 순서입니다.
참고 자료
- Pillow: Image불러오기, 자르기, 크기 변경, 회전과 알파 합성의 공식 설명
- Pillow: ImageOps휴대폰 사진의 방향 정보를 반영하는 exif_transpose










