이미지 변형과 합성의 응용
- 형식
- 목표 달성형 개인 실습
- 이전 학습
- 1교시 이미지 변형, 2교시 투명도와 합성
- 결과물
- 내 사진의 구성 비교 · 한 값이 다른 세 후보 · 제목을 넣은 커버
- 제출
- 실행 결과가 있는 공통 .ipynb + 선택한 .png
학습 목표
- 자신이 찍은 사진을 읽고, 남길 대상에 맞춰 정사각형 영역을 선택한다.
- 세 가지 구성을 비교하고 한 가지를 골라, 변수 하나가 다른 후보 세 장을 만든다.
- 선택 이유를 사진의 특징과 연결하고 자신의 제목을 넣어 커버를 완성한다.
- 원본과 최종 결과를 비교하고 새 세션에서 재현한 노트북과 PNG를 제출한다.
1·2교시에 사용한 같은 공통 노트북에서 3교시 구역으로 이동합니다. 사진과 앞 교시의 출력·코드를 유지하고 개인 작품을 완성합니다.
학습 내용
- 1. 내 사진으로 만드는 커버
- 2. 사진과 표현 의도 정하기
- 3. 내 사진 업로드와 원본 확인
- 4. 남길 영역 선택
- 5. 세 가지 구성 미리보기
- 6. 한 가지를 바꾸어 세 후보 만들기
- 7. 선택과 한 번의 수정
- 8. 제목을 넣고 작품으로 확인
- 9. 저장·검사·제출
1. 내 사진으로 만드는 커버
휴대폰에 남아 있던 사진 한 장을 꺼냅니다. 지나쳤던 풍경이나 책상 위 물건도, 어디를 남기고 어떻게 놓느냐에 따라 새로운 표지가 됩니다. 오늘은 내 사진으로 1000 × 1000 앨범·플레이리스트 커버를 만듭니다.

큰 사진과 작은 부분 확대를 함께 놓아 장면의 중심을 만듭니다.

같은 사진을 세로로 나누고 간격과 높이를 달리 놓습니다.

또렷한 중심 사진 뒤에 옅은 사진 두 장을 겹칩니다.
위 이미지는 같은 수업용 사진·자르기·팔레트로 제공 함수를 실제 실행한 결과입니다. 합성 방식이 달라지면 같은 장면도 다르게 보입니다. 자기 사진을 넣으면 어떤 구성이 어울릴지 먼저 상상해 봅니다.
수업 전 준비
본인이 찍은 사진 1~3장을 JPG 또는 PNG로 준비합니다. 얼굴 사진일 필요는 없습니다. 풍경·물건·반려동물처럼 남기고 싶은 대상이 있는 사진이면 됩니다. HEIC는 JPG/PNG로 내보냅니다. 사진이 준비되지 않았다면 수업용 사진이나 6주차 작품을 사용합니다.
완료 기준
| 결과 | 확인할 내용 |
|---|---|
| 사진과 자르기 | 원본 기록과 선택 영역, 자른 결과가 노트북에 남음 |
| 구성 비교 | 자기 사진에 세 가지 구성을 적용하고 하나를 선택함 |
| 후보 세 장 | 같은 원본·영역·구성·팔레트에서 한 값만 바꾸어 비교함 |
| 최종 커버 | 선택한 사진 구성에 자기 제목·이름을 넣은 1000 × 1000 PNG |
| 설명과 제출 | 눈에 보이는 차이와 선택 이유를 기록하고 공통 노트북 + PNG 두 파일 제출 |
실습 준비
1·2교시에 사용한 같은 공통 노트북의 3교시로 이동합니다. 새 노트북을 만들지 않습니다. ‘준비 C · 개인 커버 제작 도구’가 보이는지 확인합니다. 이전 버전의 사본이라면 최신 공통 노트북의 3교시 셀로 기존 3교시 구역을 교체합니다. 앞 교시의 코드와 관찰은 남겨 둡니다.
최신 공통 실습 노트북 받기 · 세션을 새로 시작했다면 준비 A → 준비 B → 1·2교시 → 3교시 순서로 실행합니다.
45분 실습 진행안
| 단계 | 시간 | 활동 |
|---|---|---|
| 1·2장 | 5분 | 완성작을 보고 사진·표현 의도 정하기 |
| 3장 | 5분 | 사진 업로드·작업본 확인 |
| 4장 | 7분 | 남길 영역 조절 |
| 5장 | 5분 | 세 구성과 팔레트 비교 |
| 6·7장 | 12분 | 한 값 비교·선택·한 번 수정 |
| 8장 | 6분 | 제목·썸네일·원본 비교 |
| 9장 | 5분 | 저장·전체 실행·검사·제출 |
설명과 실행을 포함한 운영 예시입니다. 업로드 시간을 줄이도록 사진은 미리 준비합니다. 완료한 학생은 제출 확인 후 즉시 귀가할 수 있습니다.
2. 사진과 표현 의도 정하기
이 사진에서 가장 오래 남기고 싶은 것은 무엇인가요?
처음부터 멋진 설명을 만들 필요는 없습니다. “버스를 기다리며 본 노을”, “책상 위에 놓인 컵”처럼 사진 속 장면에서 시작합니다. 그 장면에 어울리는 음악이나 분위기를 짧게 적습니다.
| 관찰 | 표현 의도 | 살펴볼 구성 |
|---|---|---|
| 한 대상과 주변 공간이 눈에 들어옴 | 주인공과 여백을 함께 남기고 싶음 | 장면 강조 · focus |
| 수평선·건물·그림자가 이어짐 | 장면의 연결과 끊김을 보여 주고 싶음 | 분할과 재배열 · strips |
| 반복해서 떠오르는 장면이 있음 | 선명한 장면 주위에 잔상을 남기고 싶음 | 잔상과 중첩 · echo |
위 연결은 출발점입니다. 자신의 사진을 세 구성으로 본 뒤 다른 방향을 골라도 됩니다. 작거나 흐린 대상을 무리하게 키우기보다, 자른 뒤에도 형태가 남는 사진을 고릅니다.
실습 1 · 내 사진의 주인공 정하기
학번·이름과 세 안내 문장을 자신의 내용으로 바꿉니다. photo_subject는 사진에서 남길 대상이며, 작품 제목은 뒤에서 따로 정합니다.
student_id = "학번"
student_name = "이름 또는 별명"
photo_subject = "사진에서 남기고 싶은 대상을 적으세요."
music_brief = "어떤 음악이나 장면을 위한 커버인지 적으세요."
mood_words = "원하는 분위기를 두세 단어로 적으세요."3. 내 사진 업로드와 원본 확인
먼저 실행 · 준비 C
공통 노트북의 접힌 ‘준비 C · 개인 커버 제작 도구와 한글 글꼴’ 셀을 그대로 실행합니다. ‘개인 커버 준비 완료’가 나오면 다음으로 진행합니다. make_photo_cover()와 add_cover_title()는 이 셀에서 정의하는 수업용 함수이며 Pillow 기본 함수가 아닙니다.
이 도구는 1·2교시에서 배운 자르기·크기 변경·회전·알파 합성을 세 가지 구성으로 묶습니다. 준비 C에 전체 정의와 한글 글꼴이 들어 있으므로 다른 곳에서 함수를 찾거나 설치할 필요가 없습니다.
- Colab 왼쪽 파일 → 업로드에서 JPG/PNG 한 장을 올립니다.
- 파일 목록의 이름을 복사해
own_path에 붙입니다. source_choice = "own"을 유지하고 셀을 실행합니다.
실습 2 · 업로드한 사진 읽기
자신의 파일명과 원본 기록을 확인합니다. 파일 처리와 검사는 제공 코드입니다. 파일이 준비되지 않았으면 source_choice를 provided로 바꿉니다.
코드 읽기 · 업로드한 파일과 작업본
학생이 바꿀 부분은 source_choice·own_path와 원본 기록입니다. 파일 검사와 방향 보정은 제공 코드입니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
source_choice = "own" | 업로드한 자기 사진을 사용합니다. | provided로 바꾸면 노트북에 포함된 수업용 사진을 사용합니다. |
own_path = "my_photo.jpg" | Colab 파일 목록에 있는 실제 파일명을 적습니다. | 문자열은 파일의 위치이며 이미지 객체가 아닙니다. |
load_source(source_path) | 준비 B의 제공 함수로 파일을 읽습니다. | 방향을 바로잡고 긴 변을 최대 1600픽셀로 맞춘 RGBA 작업본을 반환합니다. |
source_preview.thumbnail((450, 450)) | 미리보기 복사본을 비율을 유지해 줄입니다. | 이 메서드는 복사본을 직접 바꿉니다. source_image와 원본 파일은 그대로입니다. |
확인할 점 · 파일 업로드 후 source 셀을 실행해야 합니다. 이름과 확장자가 own_path와 같은지 확인합니다.
source_choice = "own" # own: 내 사진 / provided: 수업용 사진
own_path = "my_photo.jpg" # 업로드한 실제 파일명으로 변경
own_title = photo_subject
own_creator = student_name
own_source = own_path
own_license = "직접 촬영" # 6주차 작품은 "직접 제작"
assert source_choice in {"provided", "own"}, "source_choice는 provided 또는 own입니다."
if source_choice == "provided":
source_path = PROVIDED_PATH
source_record = ("Seaside chair at dusk", "OpenAI image generation / 수업 제작", "노트북에 포함된 수업용 이미지", "수업 제공 이미지: 이 수업의 변형·제출용")
else:
source_path = own_path
source_record = (own_title, own_creator, own_source, own_license)
assert Path(source_path).resolve() != Path(PROVIDED_PATH).resolve(), "자신의 파일을 올리거나 source_choice를 provided로 바꾸세요."
assert Path(source_path).exists(), f"파일을 찾을 수 없습니다: {source_path}. Colab 왼쪽 파일 → 업로드 후 own_path를 실제 파일명으로 바꾸세요. 사진이 없다면 source_choice를 provided로 바꾸세요."
source_bytes_before = Path(source_path).read_bytes()
source_image = load_source(source_path)
print("작업본 크기:", source_image.size, "/ 중앙 자르기:", default_crop(source_image))
source_preview = source_image.copy()
source_preview.thumbnail((450, 450), Image.Resampling.LANCZOS)
display(source_preview)원본의 제목·창작자·원본 주소·라이선스 또는 이용 근거를 함께 기록합니다. 직접 찍은 사진은 자신의 파일명을 원본 주소로 남기고, 6주차 작품은 이용 근거를 직접 제작으로 적습니다.
사진이 옆으로 눕거나 원본이 너무 크면?
제공 함수가 사진의 방향 정보를 반영하고 긴 변을 최대 1600픽셀로 줄입니다. 원본 파일은 그대로입니다. 이후 좌표는 출력된 작업본 크기를 기준으로 정합니다.
FileNotFoundError 또는 파일을 찾을 수 없다는 안내가 나오면?
Colab에 올린 실제 파일명과 own_path의 대소문자·확장자를 확인합니다. 사진이 준비되지 않았다면 source_choice를 provided로 바꿉니다. 세션을 새로 시작해 파일이 사라졌다면 같은 사진을 다시 올립니다.
6주차 작품으로 진행하려면?
artwork.png의 실제 파일명을 own_path에 적고 own_license를 “직접 제작”으로 바꿉니다. 제목 영역을 포함할지, 도형 부분만 자를지 먼저 고릅니다. 나머지 제작 순서는 같습니다.
4. 남길 영역 선택
사진을 바꾸기 전에 무엇을 남길지 고릅니다. 중앙에 있는 것이 언제나 사진의 주인공은 아닙니다. 노란 테두리를 움직이며, 자른 뒤에도 처음 고른 대상이 눈에 들어오는지 살펴봅니다.
실습 3 · 자르기 전후 비교
먼저 자동 중앙 자르기로 실행합니다. 이어서 crop_box를 네 좌표로 바꾸고 실행해 결과를 확인합니다. 새 원본을 읽었다면 이 셀부터 다시 실행합니다.
코드 읽기 · 중앙 자르기에서 내 좌표로
1교시의 crop 좌표를 자신의 사진에 적용합니다. 세 구성은 정사각형 영역을 같은 비율로 사용합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
default_crop(source_image) | 짧은 변의 90%를 한 변으로 하는 중앙 정사각형을 고릅니다. | 네 경계 좌표를 반환합니다. 사진의 주인공을 자동으로 찾는 기능은 아닙니다. |
validate_square_crop(source_image, crop_box) | 이미지 안의 정사각형인지 검사하는 제공 함수입니다. | 범위를 벗어나거나 너비·높이가 다르면 한글 안내가 나옵니다. |
show_crop(source_image, crop_box) | 원본 복사본에 테두리를 그리고 자른 결과와 나란히 놓습니다. | 제공된 표시 도구이며 원본을 수정하지 않습니다. |
확인할 점 · 왼쪽 위는 (0, 0), x는 오른쪽, y는 아래로 커집니다. 오른쪽−왼쪽과 아래−위가 같아야 합니다.
crop_box = default_crop(source_image) # 먼저 실행한 뒤 자신의 좌표로 변경
validate_square_crop(source_image, crop_box)
display(show_crop(source_image, crop_box))
print("자른 크기:", source_image.crop(crop_box).size)
| 확인 | 예 |
|---|---|
| 이미지 범위 안인가 | 출력된 작업본의 너비·높이보다 끝 경계가 크지 않아야 함 |
| 정사각형인가 | (250, 400, 850, 1000): 너비 600, 높이 600 |
| 대상이 남았는가 | 자른 결과에서 주인공과 필요한 주변 공간을 직접 확인 |
세로 사진도 사용할 수 있나요?
가능합니다. 세로 사진 안에서 남길 정사각형을 고릅니다. 원본 전체를 정사각형으로 눌러 바꾸지 않으므로 비율이 유지됩니다. 전체 장면이 꼭 필요하다면 더 넓은 영역을 담을 수 있는 다른 사진을 고를 수도 있습니다.
5. 세 가지 구성 미리보기
사진 한 장이 서로 다른 화면이 되는 순간입니다. 지금은 세부 숫자보다 어떤 구성에서 자기 사진이 더 잘 보이는지 살펴봅니다.
| layout | 사진을 놓는 방식 | 사용한 기능 |
|---|---|---|
| focus · 장면 강조 | 큰 장면 + 아래쪽 중앙 영역을 확대한 작은 사진 | 자르기·크기 변경·위치 |
| strips · 분할과 재배열 | 세로 세 조각의 가로 간격과 높이 차이 | 자르기·반복·합성 |
| echo · 잔상과 중첩 | 중심 사진은 불투명, 뒤 사진 두 장은 반투명 | 회전·알파·합성 순서 |
실습 4 · 같은 사진의 세 구성
palette를 paper·night·coral 중 하나로 골라 실행합니다. 세 구성에 같은 팔레트가 적용됩니다. 미리보기의 제목은 임시 문구입니다.
코드 읽기 · 제공 함수로 구성 미리보기
준비 C를 실행하면 make_photo_cover와 add_cover_title을 사용할 수 있습니다. 두 함수 모두 수업용 제공 함수입니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
make_photo_cover(source_image, crop_box, layout=layout_name, palette=palette) | 같은 사진과 영역을 지정한 구성과 팔레트로 합성합니다. | 제목 없는 1000 × 1000 RGBA 이미지 한 장을 반환합니다. |
add_cover_title(preview, "사진의 기록", "LAYOUT PREVIEW", palette=palette) | 미리보기 복사본에 임시 제목을 넣습니다. | 입력 이미지를 보존하고 제목이 있는 새 이미지를 반환합니다. |
layout_previews.append(preview) | 목록 끝에 커버 한 장을 추가합니다. | 합성하지 않고 서로 독립된 이미지를 보관합니다. |
확인할 점 · 이번에는 전체 구성을 고릅니다. 다음 비교에서는 구성과 팔레트를 고정하고 한 값만 바꿉니다.
palette = "paper" # paper / night / coral
layout_names = ["focus", "strips", "echo"]
layout_previews = []
for layout_name in layout_names:
preview = make_photo_cover(source_image, crop_box, layout=layout_name, palette=palette)
preview = add_cover_title(preview, "사진의 기록", "LAYOUT PREVIEW", palette=palette)
layout_previews.append(preview)
display(comparison_sheet(layout_previews, ["1 / focus", "2 / strips", "3 / echo"]))구성을 고르는 비교와 값을 고르는 비교
여기서는 배치 방식 전체를 비교합니다. 다음 장에서는 구성 하나를 고정하고 값 하나만 바꿉니다. 여러 조건을 함께 바꿨을 때의 인상 비교와, 어떤 값 때문에 달라졌는지 확인하는 실험을 구분합니다.
6. 한 가지를 바꾸어 세 후보 만들기
좋아하는 구성을 골랐다면, 이제 그 안에서 차이를 살펴봅니다. 사진을 조금 더 가까이 볼지, 조각 사이를 넓힐지, 뒤의 잔상을 기울일지 정합니다.
| layout | 비교 항목 | 시작값 / 허용 범위 | 확인할 부분 |
|---|---|---|---|
| focus | 중앙 확대 배율 zoom | 1.0 / 1.2 / 1.4 · 1.0~1.6 | 주인공의 크기와 남는 주변 공간 |
| strips | 가로 간격 gap | 8 / 28 / 56 · 8~72 정수 | 이어지는 선과 조각 사이의 여백 |
| echo | 뒤 사진의 각도 angle | −16 / 0 / 16 · −20~20도 | 또렷한 중심과 주변 잔상의 방향 |
실습 5 · 한 값만 바꾸기
layout을 고르고 실행합니다. 더 비교하려면 선택된 분기의 compare_values만 서로 다른 세 값으로 바꿉니다. 원본·crop_box·layout·palette는 같은 조건으로 유지합니다.
코드 읽기 · 구성별 입력과 후보 목록
5주차 조건문으로 비교 항목을 고르고, 4주차 반복문으로 함수 호출을 세 번 실행합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
if layout == "focus" / elif layout == "strips" / else | 고른 구성에 해당하는 분기만 실행합니다. | 확대 배율·간격·각도 중 하나의 compare_values가 준비됩니다. |
for value in compare_values | 세 값을 하나씩 꺼냅니다. | 이번 호출의 value에 숫자 하나가 들어갑니다. |
make_photo_cover(..., value=value) | 고른 구성의 비교 값 하나를 전달합니다. | focus: 배율 / strips: 픽셀 간격 / echo: 뒤 조각의 각도 |
candidates.append(cover) | 반환된 커버를 목록 끝에 보관합니다. | 반복이 끝나면 candidates에 커버 세 장이 남습니다. |
enumerate(compare_values) | 각 값에 0부터 시작하는 순번을 붙입니다. | 아래 제공 표시 코드에서 index + 1로 후보 번호 1·2·3을 만듭니다. |
확인할 점 · set을 이용한 중복 검사와 candidate_settings 기록은 제공 코드입니다. 학생은 layout과 선택된 분기의 세 값을 바꿉니다.
layout = "focus" # focus / strips / echo
if layout == "focus":
compare_name = "zoom"
compare_values = [1.0, 1.2, 1.4]
elif layout == "strips":
compare_name = "gap"
compare_values = [8, 28, 56]
else:
assert layout == "echo", "layout은 focus, strips, echo 중 하나입니다."
compare_name = "angle"
compare_values = [-16, 0, 16]
assert len(compare_values) == 3 and len(set(compare_values)) == 3, "비교 값은 서로 다른 세 값으로 정하세요."
candidates = []
for value in compare_values:
cover = make_photo_cover(source_image, crop_box, layout=layout, palette=palette, value=value)
candidates.append(cover)
candidate_labels = [f"{index + 1} / {compare_name} = {value}" for index, value in enumerate(compare_values)]
candidate_sheet = comparison_sheet(candidates, candidate_labels)
candidate_settings = (tuple(crop_box), layout, palette, tuple(compare_values))
display(candidate_sheet)결과 비교와 해설
반복문의 역할
2교시는 한 캔버스에 여러 조각을 올렸습니다. 이번 바깥 반복은 함수를 세 번 호출해 독립된 커버 세 장을 만듭니다. append()는 목록에 보관하는 동작입니다. 후보에는 아직 제목이 없습니다.
세 결과가 거의 같아 보이면?
선택된 분기의 세 값을 충분히 다르게 정합니다. 단색 영역만 자르면 확대해도 차이가 보이지 않을 수 있습니다. 먼저 사진 속 형태가 남아 있는지 확인하고, 자르기를 바꿨다면 구성 미리보기와 후보를 다시 실행합니다.
7. 선택과 한 번의 수정
세 결과 중 정답인 한 장이 정해져 있지는 않습니다. 처음 남기고 싶었던 대상이 잘 보이는지, 원하는 분위기에 가까워졌는지 살펴봅니다. 마음에 드는 점과 바꾸고 싶은 점을 하나씩 찾아봅니다.
| 관찰 | 돌아갈 선택 |
|---|---|
| 중요한 대상이 잘림 | crop_box 또는 focus 확대 배율 |
| 조각이 너무 멀리 떨어져 보임 | strips 간격 |
| 잔상이 산만해 보임 | echo 각도 |
| 배경과 사진의 인상이 어울리지 않음 | palette를 바꾼 뒤 세 후보를 모두 다시 생성 |
실습 6 · 한 장 선택하고 이유 남기기
selected_number에 1·2·3 중 하나를 적습니다. 선택 이유는 “눈에 보이는 특징 + 다른 후보와의 차이 + 음악·장면과의 관계”로 씁니다.
코드 읽기 · 목록에서 한 장 선택하기
후보 번호와 Python의 목록 인덱스를 구분합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
candidates[selected_number - 1] | 번호 1·2·3을 목록 위치 0·1·2로 바꿉니다. | 2를 고르면 candidates[1]을 읽습니다. |
chosen_cover = ... .copy() | 선택한 후보를 복사합니다. | 원래 후보를 유지하고 제목을 붙일 작업본을 만듭니다. |
확인할 점 · 후보를 새로 만들거나 번호를 바꾸었다면 이 셀과 뒤의 제목·저장 셀도 다시 실행합니다.
selected_number = 2
selection_reason = "선택한 후보의 특징과 다른 후보와의 차이를 음악에 연결해 적으세요."
change_description = "원본에서 어떤 부분을 잘라 크기, 각도, 투명도, 배치를 어떻게 바꿨는지 적으세요."
assert type(selected_number) is int and 1 <= selected_number <= 3, "선택 번호는 1, 2, 3 중 하나입니다."
chosen_cover = candidates[selected_number - 1].copy()
display(chosen_cover.resize((500, 500)))
print("선택:", selected_number, "/", selection_reason)선택 이유의 예
“1번은 의자와 주변 바다가 함께 남아, 의자가 크게 잘린 3번보다 조용히 걷는 장면에 어울린다.” 자기 사진·구성·번호에 맞게 적습니다. 변형 기록에는 실제로 사용한 영역·구성·비교 값과 선택한 값을 남깁니다.
8. 제목을 넣고 작품으로 확인
사진에 이름을 붙이면 처음 고른 장면을 다시 보게 됩니다. 곡 이름이나 그날의 짧은 인상처럼, 이 사진에 붙이고 싶은 말을 정합니다. 자신이 고른 문장이 실제 커버의 제목이 됩니다.
실습 7 · 나의 제목으로 완성
한글·영문·숫자로 제목 1~24자와 이름 1~32자를 적습니다. 한글 글꼴은 포함되어 있으며 이모지는 사용하지 않습니다. 글자의 크기·정렬은 제공 함수가 처리합니다.
코드 읽기 · 제목이 있는 최종 이미지 만들기
한글 글꼴과 글자 배치는 준비 C의 제공 도구가 처리합니다. 학생은 제목과 창작자 이름을 정합니다.
| 코드 | 하는 일 | 결과 |
|---|---|---|
cover_title / cover_creator | 작품에 실제로 들어갈 제목과 이름입니다. | 제목 1~24자, 이름 1~32자. 한글·영문·숫자를 사용합니다. |
final_cover = add_cover_title(chosen_cover, cover_title, cover_creator, palette=palette) | 선택한 이미지의 복사본에 제목을 그립니다. | 사진 후보는 그대로 두고 최종 이미지 한 장을 반환합니다. |
final_cover.resize((160, 160), Image.Resampling.LANCZOS) | 작은 썸네일로 중심 대상과 제목을 확인합니다. | 표시용 새 이미지이며 final_cover의 1000 × 1000 크기는 유지됩니다. |
확인할 점 · 사진 위에 글자를 겹치지 않도록 제목 영역을 비워 두었습니다. 긴 제목은 글자가 작아지므로 출력도 확인합니다.
cover_title = "나의 사진에 붙일 제목" # 자신의 제목으로 변경, 1~24자
cover_creator = student_name
final_cover = add_cover_title(chosen_cover, cover_title, cover_creator, palette=palette)
display(final_cover.resize((600, 600)))
display(comparison_sheet([source_image, final_cover], ["SOURCE", "MY COVER"]))
print("작게 보아도 대상과 제목이 보이나요?")
display(final_cover.resize((160, 160), Image.Resampling.LANCZOS))
| 크게 보기 | 작게 보기 |
|---|---|
| 중요한 대상과 사진의 비율이 의도에 맞는가 | 160픽셀에서도 중심 대상이 구분되는가 |
| 제목이 정확하며 너무 작아지지 않았는가 | 제목이 사진과 구분되어 보이는가 |
| 자르기·구성·선택 이유가 서로 맞는가 | 여러 장 사이에서 어떤 인상으로 보일까 |
제목만 바꿨다면 이 셀부터 다시 실행합니다. 사진이나 비교 값을 바꿨다면 후보 생성 → 선택 → 제목 순서로 다시 실행합니다. 화면의 작은 미리보기와 저장할 1000 × 1000 원본은 구분합니다.
9. 저장·검사·제출
완성된 커버를 파일로 남깁니다. 노트북에는 사진을 고르고 바꾼 과정이, PNG에는 마지막으로 선택한 한 장이 담깁니다.
assert student_id.strip() and student_id != "학번", "학번을 작성하세요."
assert student_name.strip() and student_name != "이름 또는 별명", "이름 또는 별명을 작성하세요."
assert not any(ch in student_id + student_name for ch in '/\\'), "학번과 이름에 / 또는 역슬래시를 쓰지 마세요."
output_filename = f"week07_{student_id}_{student_name}.png"
comparison_filename = f"week07_{student_id}_{student_name}_comparison.png"
assert Path(output_filename).resolve() != Path(source_path).resolve(), "원본과 결과물의 이름을 다르게 정하세요."
assert Path(comparison_filename).resolve() != Path(source_path).resolve(), "비교본과 원본의 이름을 다르게 정하세요."
final_cover.save(output_filename)
candidate_sheet.save(comparison_filename)
print("최종 커버:", output_filename, "/ 비교본:", comparison_filename)전체 실행과 검사
설정과 기록을 저장한 뒤 런타임 → 세션 다시 시작 → 모두 실행으로 재현합니다. 업로드한 파일이 사라졌다면 같은 파일을 다시 올립니다. 제공 검사 셀은 수정하지 않고 마지막 한글 안내가 가리키는 작업 셀로 돌아갑니다.
제공 검사 코드 보기
# 제공 검사 도구. 음악 해석과 미적 취향은 자동 채점하지 않습니다.
for label, value in {"사진의 대상": photo_subject, "음악 또는 장면": music_brief, "분위기": mood_words, "선택 이유": selection_reason, "변형 내용": change_description}.items():
assert isinstance(value, str) and value.strip() and "적으세요" not in value, f"{label} 안내를 자신의 내용으로 바꾸세요."
assert cover_title != "나의 사진에 붙일 제목", "커버 제목을 자신의 말로 바꾸세요."
for label, value in zip(("원본 제목", "창작자", "원본 주소", "이용 근거"), source_record):
assert isinstance(value, str) and value.strip() and "적으세요" not in value, f"{label} 기록을 작성하세요."
assert Path(source_path).read_bytes() == source_bytes_before, "원본 파일이 변경되었습니다. 원본 준비부터 다시 실행하세요."
reloaded = load_source(source_path)
assert reloaded.size == source_image.size and reloaded.tobytes() == source_image.tobytes(), "작업본이 원본과 다릅니다. 원본 준비부터 다시 실행하세요."
assert candidate_settings == (tuple(crop_box), layout, palette, tuple(compare_values)), "설정이 바뀌었습니다. 후보 만들기부터 다시 실행하세요."
assert len(candidates) == 3 and len(set(compare_values)) == 3, "비교 값은 서로 다른 세 값으로 정하세요."
rebuilt = [make_photo_cover(source_image, crop_box, layout=layout, palette=palette, value=value) for value in compare_values]
assert all(old.tobytes() == new.tobytes() for old, new in zip(candidates, rebuilt)), "현재 설정과 후보가 다릅니다. 후보 만들기부터 다시 실행하세요."
assert len({hashlib.sha256(image.tobytes()).digest() for image in candidates}) == 3, "세 후보가 같습니다. 값의 차이를 늘리거나 형태가 있는 영역을 선택하세요."
assert type(selected_number) is int and 1 <= selected_number <= 3, "선택 번호는 1, 2, 3입니다."
assert chosen_cover.tobytes() == candidates[selected_number - 1].tobytes(), "선택 번호가 바뀌었습니다. 선택 셀부터 다시 실행하세요."
expected_final = add_cover_title(chosen_cover, cover_title, cover_creator, palette=palette)
assert final_cover.mode == "RGBA" and final_cover.size == (1000, 1000), "최종 이미지는 1000 × 1000 RGBA입니다."
assert final_cover.tobytes() == expected_final.tobytes(), "제목이나 창작자 이름이 바뀌었습니다. 제목 셀부터 다시 실행하세요."
expected_name = {"focus": "zoom", "strips": "gap", "echo": "angle"}[layout]
expected_sheet = comparison_sheet(candidates, [f"{index + 1} / {expected_name} = {value}" for index, value in enumerate(compare_values)])
assert candidate_sheet.tobytes() == expected_sheet.tobytes(), "비교본이 이전 결과입니다. 후보 만들기부터 다시 실행하세요."
assert output_filename == f"week07_{student_id}_{student_name}.png", "제출 정보가 바뀌었습니다. 저장부터 다시 실행하세요."
assert comparison_filename == f"week07_{student_id}_{student_name}_comparison.png", "비교본 이름이 바뀌었습니다. 저장부터 다시 실행하세요."
with Image.open(output_filename) as saved:
assert saved.format == "PNG" and saved.mode == "RGBA" and saved.size == (1000, 1000), "PNG 파일의 형식과 크기를 확인하세요."
assert saved.tobytes() == final_cover.tobytes(), "저장된 PNG가 이전 결과입니다. 저장부터 다시 실행하세요."
with Image.open(comparison_filename) as saved_sheet:
assert saved_sheet.convert("RGB").tobytes() == expected_sheet.tobytes(), "저장된 비교본이 이전 결과입니다. 저장부터 다시 실행하세요."
print("PASS / 원본 보존, 서로 다른 후보 세 장, 제목과 선택 결과, 저장 PNG 확인")
print("WEEK 07 ALBUM COVER COMPLETE")
print("새 세션 모두 실행도 마쳤다면, 실행 결과가 담긴 노트북과 최종 커버 PNG 두 파일을 제출하세요.")| 안내 | 다시 실행할 순서 |
|---|---|
| 파일을 찾을 수 없음 | 사진 업로드 → 원본 → 자르기 → 이후 순서 |
| 설정이 바뀜 / 현재 설정과 후보가 다름 | 후보 생성 → 선택 → 제목 → 저장 → 검사 |
| 선택 번호가 바뀜 | 선택 → 제목 → 저장 → 검사 |
| 제목이나 창작자 이름이 바뀜 | 제목 → 저장 → 검사 |
| 저장된 PNG가 이전 결과임 | 저장 → 검사 |
음악 해석과 미적 취향은 자동 채점하지 않습니다. 검사는 원본 보존과 후보·최종 이미지·저장 파일의 일치를 확인합니다. 선택 이유가 실제 작품과 연결되는지도 직접 확인합니다.
결과물 제출
- 파일 → 다운로드 → .ipynb로 실행 결과가 있는 공통 노트북을 받습니다.
- 왼쪽 파일 목록에서 week07_학번_이름.png를 내려받아 실제로 열어 봅니다.
- 노트북과 최종 PNG 두 파일을 제출합니다. 비교 PNG는 개인 보관용입니다.
완료와 귀가
검사를 통과하고 두 파일의 제출 완료를 확인하면 즉시 귀가할 수 있습니다. 작업 속도와 남은 시간은 평가에 반영하지 않습니다.
선택 확장
다른 사진이나 6주차 작품을 같은 구성에 넣어 봅니다. 사진이 바뀌어도 유지하고 싶은 선택이 무엇인지 기록합니다. 필수 제출을 마친 학생이 선택해서 진행하며 추가 제출 조건은 없습니다.
다음 학습
이번에는 자신이 고른 재료를 어떻게 보여 줄지 결정했습니다. 8주차 중간고사에서는 기말 작품에 사용할 데이터와 수집 방법·표현 방향을 기획합니다.
참고 자료
- Pillow: Image불러오기, 자르기, 크기 변경, 회전과 알파 합성의 공식 설명
- Pillow: ImageOps휴대폰 사진의 방향 정보를 반영하는 exif_transpose




