RGBA / Opacity / Composition

7주차 2교시: 이미지 합성

1교시의 변형 함수에 알파를 더하고, 조각의 위치와 순서를 정해 앨범 커버 한 장을 완성합니다.

바닷가 원본 사진과 실제 코드로 만든 오래된 기억 앨범 커버
레이어의 합성 · 같은 사진을 다른 투명도와 순서로 겹치기

투명도와 레이어 합성

핵심 개념
RGBA, 알파 값, 위치, 합성 순서
이전 학습
1교시의 이미지 변형과 직접 정의한 변형 함수
학습 구성
완성작 관찰 · 필요성과 원리 · 단계별 제작 · 결과 해석
다음 학습
자기 재료를 사용한 앨범 커버 제작

학습 목표

  • 완성작의 겹침을 알파·위치·합성 순서로 나누어 설명한다.
  • 1교시의 변형 함수에 알파 처리를 추가하고 사진과 빈 모서리의 투명도를 구분한다.
  • 새 캔버스에 한 장 → 두 장 → 세 장으로 조각을 쌓아 커버를 완성한다.
  • 동일한 조각의 위치와 순서를 바꾸고 결과의 차이를 설명한다.

학습 내용

1교시에 저장한 같은 노트북에서 2교시 구역으로 이동합니다. 세션이 끊겼다면 공통 준비와 1교시 셀부터 차례로 실행합니다.
완성된 결과를 먼저 비교하고, 해설 → 코드 시연 → 결과 확인으로 이어갑니다. 실습 코드와 결과 비교를 펼쳐 바뀐 줄과 이미지의 차이를 확인합니다. 예제는 앞 단계의 결과를 이어서 사용합니다. 공통 준비부터 위에서 아래로 실행하고, 앞 값을 고치면 그 뒤 제작 셀도 차례로 다시 실행합니다.

45분 수업 진행안

설명·시연·짧은 실행을 포함한 운영 예시입니다. 비교 코드는 제공된 셀을 활용하고, 실행 전 예상과 실행 뒤 이유 설명에 시간을 배분합니다. 학생의 진행 속도에 따라 각 단원의 시간을 조절합니다.

단원별 진행 예시 · 합계 45분
단원시간확인할 활동
1. 이미지 합성의 필요성4분완성작의 겹침 관찰
2. RGBA·알파와 배경5분같은 알파에서 배경과 겹침 비교
3. 알파 적용과 함수 확장6분함수의 두 수정 지점 확인
4. 캔버스와 위치 계산6분왼쪽 위 좌표와 중앙 배치 계산
5. 여러 조각 쌓기5분합성 한 줄마다 생기는 변화 확인
6. 합성 순서 비교4분위치를 고정하고 앞뒤 관계 비교
7. 복사와 재실행5분대입·복사·합성 횟수의 차이 설명
8. 반복문으로 합성 코드 정리4분캔버스 생성 위치가 다른 두 결과 비교
9. 커버 완성과 결과 설명6분제목을 추가하고 결과를 코드와 연결

1. 이미지 합성의 필요성

핵심 질문

1교시에서 만든 세 조각을 겹치면, 뒤의 사진도 보일까요?

나란히 놓았을 때는 따로 보이던 사진도 겹쳐 놓으면 서로의 인상을 바꿉니다. 같은 바다 위에 의자가 다시 나타나고, 한 줄이던 수평선이 여러 방향으로 이어집니다. 이번에는 세 조각이 함께 만드는 화면을 살펴봅니다.

1교시에는 자르기·크기 변경·회전을 transform_piece()로 묶어 크기와 각도가 다른 세 조각을 만들었습니다. 이제 투명도를 조절하고 위치와 순서를 정해 앨범 커버 한 장을 완성합니다.

이번 시간의 제작 순서
남은 문제필요한 작업확인할 결과
뒤 사진이 가려짐알파 적용반투명 조각
최종 화면과 위치가 없음캔버스 생성과 좌표 지정한 장 배치
여러 조각의 관계가 없음조각 추가와 합성 순서 결정두 장 → 세 장
제목이 없음제공된 제목 코드 추가완성 커버

이어서 실행할 준비

1교시의 원본 준비·변형 코드와 transform_piece() 정의·호출을 실행한 공통 노트북에서 이어갑니다. 기준 예제는 working_image, demo_crop_box, resized_image, first_layer_angle을 사용합니다. 세션을 다시 시작했다면 준비 A·B와 1교시 제작 셀을 위에서부터 실행합니다.

실습 1 · 겹침 결과 미리보기

공통 원리
완성 커버에서 알파만 달리해 관찰합니다.
유지 요소
같은 사진·크기·각도·배치

제공 코드를 실행하거나 아래 비교 그림을 펼칩니다. 의자와 수평선은 어느 결과에서 잘 보이고, 아래 사진은 어디에서 더 많이 비치나요?

코드 읽기 · 완성 커버에서 알파만 비교하기

이번 코드는 관찰을 위한 제공 코드입니다. 실제 알파 적용은 다음 실습에서 직접 작성합니다.

코드하는 일결과
make_cover(..., alpha=80)제공된 커버 함수에 알파 값을 전달합니다.같은 배치에서 사진이 비치는 정도가 달라집니다.
comparison_sheet([alpha_low, alpha_mid, alpha_high], ...)세 결과를 제공된 비교판으로 표시합니다.원본·각도·배치를 고정한 80 / 160 / 240 비교

확인할 점 · 기울기와 배치를 고정하고, 의자의 선명함과 겹친 부분의 색을 관찰합니다.

실습 코드
실습 코드
실습 코드
# @title 실습 1 · 제공 코드로 겹침 결과 미리보기
alpha_low = make_cover(demo_source, demo_crop_box, style="echo", angle=12, alpha=80)
alpha_mid = make_cover(demo_source, demo_crop_box, style="echo", angle=12, alpha=160)
alpha_high = make_cover(demo_source, demo_crop_box, style="echo", angle=12, alpha=240)
display(comparison_sheet([alpha_low, alpha_mid, alpha_high], ["alpha = 80", "alpha = 160", "alpha = 240"]))

이번에는 완성 결과를 관찰합니다. make_cover()는 준비 B의 제공 함수입니다. 실제 알파 적용은 실습 2에서 직접 작성합니다.

결과 비교와 해설
알파 80 / 160 / 240 · 사진이 얼마나 또렷하게 보이는지, 겹친 부분의 색은 어떻게 다른지 비교합니다.
알파 80 / 160 / 240 · 사진이 얼마나 또렷하게 보이는지, 겹친 부분의 색은 어떻게 다른지 비교합니다.

2. RGBA·알파와 배경

핵심 질문

사진을 어둡게 하는 것과 뒤 사진이 비치게 하는 것은 어떻게 다를까요?

RGB는 빨강·초록·파랑의 색 정보이고, RGBA는 여기에 A, 즉 알파를 더한 형식입니다. 알파 0은 완전 투명, 255는 완전 불투명입니다. 알파를 낮추면 사진의 RGB를 검게 바꾸는 것이 아니라 아래 이미지의 색이 더 많이 섞여 보입니다.

알파 128은 약 절반, 160은 약 63%의 불투명도입니다. 여러 조각이 겹치면 새로 올린 사진의 색과 그 아래 화면의 색이 섞입니다. 의자의 윤곽을 또렷하게 남길지, 뒤의 바다가 더 많이 비치게 할지 살펴보며 값을 정합니다.

관찰용 제공 결과 · A와 B는 같은 조각·알파·위치에서 배경만 다릅니다. A와 C는 같은 배경에서 두 번째 조각을 더한 차이입니다. 제작 코드는 4장에서 배치 방법을 배운 뒤 확인합니다.
관찰용 제공 결과 · A와 B는 같은 조각·알파·위치에서 배경만 다릅니다. A와 C는 같은 배경에서 두 번째 조각을 더한 차이입니다. 제작 코드는 4장에서 배치 방법을 배운 뒤 확인합니다.
알파를 결과 색으로 읽기
알파불투명한 배경 위에서확인할 것
0사진은 보이지 않고 아래 색이 보임사진이 사라졌는가
128사진과 아래 색이 대략 절반씩 섞임배경색도 결과에 영향을 주는가
255사진의 색이 아래 색을 가림아래 사진이 가려지는가

예를 들어 검정 픽셀이 흰 배경 위에 알파 128로 놓이면 중간 회색에 가깝게 보입니다. 같은 픽셀을 검정 배경에 놓으면 검정으로 보입니다. 사진의 RGB가 같아도 아래 색과 섞여 보이는 결과는 다릅니다.

핵심 질문

A와 B에서 사진의 색을 직접 바꿨을까요? C의 겹친 부분에서는 어떤 이미지 위에 다시 합성했을까요?

A·B에서는 배경색만 바꿨습니다. C에서는 이미 사진이 합성된 A 위에 가운데 사진을 더했습니다. 두 알파를 단순히 더하는 것이 아니라, 각 단계에서 아래쪽 합성 결과와 다시 섞습니다. 밝아졌다는 관찰만으로 알파 값이 달라졌다고 판단할 수 없습니다.

3. 알파 적용과 함수 확장

앞에서 본 겹침을 직접 만들어 볼 차례입니다. 먼저 사진 한 장에 알파를 적용해 아래가 비치는 정도를 확인합니다. 그다음 같은 명령을 1교시의 함수 안에 넣어, 조각을 만들 때 알파도 함께 정하도록 바꿉니다.

실습 2 · 알파 값 적용

공통 원리
회전 전에 알파를 적용합니다.
유지 요소
1교시의 760 × 760 조각 · −12도

먼저 회전 전 이미지를 복사해 알파 160을 넣고 회전합니다. 불투명한 기준 결과와 비교한 뒤 80 또는 240으로 바꾸어 보고, 160으로 되돌립니다.

코드 읽기 · 복사한 이미지의 알파 바꾸기

putalpha()는 기존 이미지의 알파를 직접 바꿉니다. 새 이미지를 반환하는 crop·resize·rotate와 구분합니다.

코드하는 일결과
faded_image = resized_image.copy()회전 전 기준 이미지를 복사합니다.resized_image와 별개로 바꿀 작업본이 생깁니다.
faded_image.putalpha(layer_alpha)작업본의 모든 픽셀에 같은 알파를 넣습니다.작업본이 직접 바뀝니다. 반환값 None을 이미지 변수에 대입하지 않습니다.
transparent_layer = faded_image.rotate(...)알파를 바꾼 뒤 회전합니다.사진은 반투명하고 회전으로 생긴 빈 모서리는 알파 0으로 남습니다.

확인할 점 · 수업용 JPG처럼 원래 빈 부분이 없는 사진에서 사용합니다. 회전된 조각에 putalpha를 적용하면 빈 모서리도 덮어씁니다.

실습 코드
실습 코드
실습 코드
layer_alpha = 160
faded_image = resized_image.copy()
faded_image.putalpha(layer_alpha)
transparent_layer = faded_image.rotate(first_layer_angle, expand=True, resample=Image.Resampling.BICUBIC, fillcolor=(0, 0, 0, 0))
display(comparison_sheet([rotated_image, transparent_layer], ["alpha 255", f"alpha {layer_alpha}"], preserve_scale=True))

copy()로 기준 이미지를 남겨 두고 putalpha()로 복사본을 바꿉니다. faded_image = faded_image.putalpha(160)처럼 반환값을 이미지 변수에 대입하지 않습니다.

결과 비교와 해설
불투명 사진과 알파 160 사진 · 밝은 비교판은 투명도를 보여 주는 바탕이며 사진에 칠해진 색이 아닙니다.
불투명 사진과 알파 160 사진 · 밝은 비교판은 투명도를 보여 주는 바탕이며 사진에 칠해진 색이 아닙니다.
회전한 뒤 putalpha(160)을 적용하면 안 되나요?

회전 뒤에는 사진이 없는 투명한 모서리가 생깁니다. 그때 모든 픽셀의 알파를 160으로 덮으면 빈 모서리까지 반투명한 사각형이 됩니다. 크기 변경 → 알파 적용 → 회전 순서로 만들면 새 모서리는 알파 0으로 남습니다.

1교시의 함수에 알파 처리 추가

1교시의 함수에서 두 곳을 수정합니다. 매개변수에 alpha=255를 추가하고, 크기 변경 다음에 resized.putalpha(alpha)를 넣습니다. 자르기·크기 변경·회전과 반환은 그대로입니다. 아래 전체 정의에서 강조된 두 곳을 확인합니다.

코드 읽기 · 기존 함수에서 수정할 두 곳

1교시의 transform_piece()에 방금 실행한 알파 처리만 추가합니다. 이름과 반환값의 역할은 같습니다.

코드하는 일결과
def transform_piece(..., angle, alpha=255):마지막 입력에 기본값 255를 둡니다.생략하면 불투명, 160을 넣으면 알파 160을 사용합니다.
resized.putalpha(alpha)크기 변경 뒤, 회전 전에 이 한 줄을 추가합니다.원하는 알파를 넣고 회전의 투명 모서리를 보존합니다.
transform_piece(working_image, demo_crop_box, (760, 760), -12, 160)같은 크기·각도에 알파 입력을 추가해 호출합니다.직접 만든 transparent_layer와 같은 조각을 반환합니다.

확인할 점 · 수정한 함수 정의 셀을 먼저 실행합니다. 이후에 1교시의 정의 셀을 실행하면 이전 함수로 돌아가므로, 알파를 추가한 정의 셀을 다시 실행해야 합니다.

기존 함수 수정 · 전체 정의를 실행한 뒤 호출
기존 함수 수정 · 전체 정의를 실행한 뒤 호출
def transform_piece(source, crop_box, target_size, angle, alpha=255):
    validate_crop(source, crop_box)
    cropped = source.crop(crop_box)
    resized = cropped.resize(target_size, Image.Resampling.LANCZOS)
    resized.putalpha(alpha)
    rotated = resized.rotate(
        angle,
        expand=True,
        resample=Image.Resampling.BICUBIC,
        fillcolor=(0, 0, 0, 0),
    )
    return rotated

# 위에서 직접 만든 알파 160 조각과 같은지 확인합니다.
function_layer = transform_piece(working_image, demo_crop_box, (760, 760), -12, layer_alpha)
display(comparison_sheet([transparent_layer, function_layer], ["direct alpha", "function alpha"], preserve_scale=True))

앞에서 직접 만든 transparent_layer와 함수로 만든 function_layer가 같은지 비교합니다. 다음 제작 셀에서는 이 함수에 알파 160을 전달해 세 조각을 다시 만듭니다. 1교시의 결과에 알파를 덮어쓰지 않고 함수가 회전 전에 적용하게 합니다.

원래 투명한 PNG에도 같은 코드를 쓰나요? · 보충

이번 transform_piece의 putalpha는 모든 픽셀을 같은 알파로 바꾸므로 빈 부분이 없는 수업용 JPG에 사용합니다. 3교시의 제공 make_photo_cover도 준비 B의 make_layer를 사용합니다. 이 보조 함수는 기존 알파 × 선택한 알파 / 255로 계산해 PNG의 투명 영역을 보존합니다. getchannel·point·lambda로 된 세부 처리는 준비 B에 포함된 제공 코드이며 이번 직접 작성 범위에는 포함하지 않습니다.

4. 캔버스와 위치 계산

핵심 질문

사진 조각의 크기와 완성 커버의 크기는 어디에서 각각 정할까요?

사진을 한쪽으로 옮기면 반대편에 여백이 생깁니다. 사진의 내용은 그대로여도 화면에서 시선이 향하는 곳은 달라질 수 있습니다. 마음에 드는 자리를 찾았다면, 그 위치를 좌표로 남겨 봅니다.

레이어는 올릴 사진 조각, 캔버스는 조각들이 모이는 최종 화면입니다. 회전으로 조각의 경계가 커져도 캔버스는 별도로 1000 × 1000으로 정합니다. Image.new()의 마지막 색 값 255는 배경이 불투명하다는 뜻입니다.

canvas.alpha_composite(layer, dest=(x, y))는 레이어의 왼쪽 위를 캔버스의 (x, y)에 맞춥니다. 기준점은 회전으로 생긴 빈 모서리까지 포함한 사각형의 왼쪽 위입니다. 사진의 중심이나 의자의 위치와 구분해야 합니다. 캔버스 밖으로 나간 부분은 결과에 보이지 않습니다.

실습 3 · 첫 레이어 배치

공통 원리
같은 함수로 세 조각을 준비하고 첫 조각부터 배치합니다.
유지 요소
1000 × 1000 배경 · 알파 160 · y=100

알파를 추가한 함수 정의를 먼저 실행합니다. 새 제작 셀에서 x=40으로 시작해 240, 800으로 바꾸고 이동과 잘림을 비교합니다. 다음 단계 전에 40으로 되돌립니다.

코드 읽기 · 캔버스 생성과 위치 지정

조각을 만드는 함수는 이미지를 반환하고, 합성 메서드는 캔버스를 직접 수정합니다.

코드하는 일결과
Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))크기와 배경색을 정해 빈 화면을 만듭니다.1000 × 1000의 불투명한 짙은 캔버스
lesson_canvas.alpha_composite(transparent_layer, dest=(40, 100))조각의 왼쪽 위를 x=40, y=100에 맞추어 겹칩니다.lesson_canvas가 직접 바뀝니다. 반환값은 대입하지 않습니다.
one_layer = lesson_canvas.copy()첫 조각이 놓인 순간을 복사합니다.이후 합성에도 유지되는 비교용 이미지

확인할 점 · dest는 회전 후 사각형의 왼쪽 위입니다. 중심 좌표가 아닙니다. 재실행은 새 캔버스를 만드는 줄부터 시작합니다.

실습 코드

알파를 추가한 transform_piece 정의를 실행한 뒤, 새 제작 셀에 아래 코드를 작성합니다. 이후에는 출력 줄 위에 합성 코드를 추가합니다.

현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
# 1. 1교시의 함수로 조각을 준비하고 알파를 적용
layer_alpha = 160
transparent_layer = transform_piece(working_image, demo_crop_box, (760, 760), -12, layer_alpha)
middle_layer = transform_piece(working_image, demo_crop_box, (560, 560), 0, layer_alpha)
front_layer = transform_piece(working_image, demo_crop_box, (390, 390), 12, layer_alpha)

# 2. 새 캔버스에 첫 조각 배치
lesson_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
lesson_canvas.alpha_composite(transparent_layer, dest=(40, 100))
one_layer = lesson_canvas.copy()

display(one_layer.resize((500, 500)))

첫 세 호출의 크기와 각도는 1교시와 같고 알파 입력만 추가되었습니다. 캔버스를 매번 새로 만들어 첫 조각을 올린 뒤 one_layer로 복사합니다. 다음 단계에서는 이 제작 코드에 합성을 계속 추가합니다.

결과 비교와 해설
x=40 / 240 / 800 · 오른쪽으로 옮길수록 캔버스 밖으로 잘리는 부분이 늘어납니다.
x=40 / 240 / 800 · 오른쪽으로 옮길수록 캔버스 밖으로 잘리는 부분이 늘어납니다.
400 × 400 조각으로 좌표 확인
dest가로 경계1000픽셀 캔버스에 남는 부분
(100, 100)100 이상 500 미만전부 들어옴
(800, 100)800 이상 1200 미만오른쪽 200픽셀이 잘림
(-100, 100)−100 이상 300 미만왼쪽 100픽셀이 잘림
같은 합성 줄만 반복하면 왜 사진이 진해지나요?

이전 캔버스 위에 같은 반투명 사진이 다시 쌓이기 때문입니다. 제작을 다시 시작할 때는 Image.new부터 실행해야 합니다. 이후 전체 코드에도 새 캔버스를 만드는 줄을 유지합니다.

비교 실습 · 좌표 (500, 500)과 중앙 배치

공통 원리
회전 후 조각의 실제 크기로 여백을 계산합니다.
유지 요소
1000 × 1000 캔버스 · 같은 front_layer

먼저 464 × 464 조각의 왼쪽 위를 (500, 500)에 놓은 결과를 예상합니다. 가운데에 놓으려면 남는 536픽셀을 어떻게 나눠야 할까요? 계산한 뒤 실행합니다.

코드 읽기 · 남는 공간을 반으로 나누기

중앙 배치에서도 dest는 왼쪽 위 좌표입니다. 회전한 조각의 실제 크기를 사용해 좌표를 계산합니다.

코드하는 일결과
front_layer.width / front_layer.height회전 후 조각의 너비와 높이를 따로 읽습니다.기본값에서는 각각 464픽셀. 속성이므로 괄호를 붙이지 않습니다.
(1000 - front_layer.width) // 2캔버스에서 조각 너비를 뺀 여백을 좌우로 나눕니다.(1000 − 464) // 2 = 268. //는 정수 나눗셈입니다.
dest=(center_x, center_y)계산한 여백을 조각의 왼쪽 위 좌표로 넣습니다.기본값에서 (268, 268). 홀수 여백이면 양쪽이 1픽셀 차이 날 수 있습니다.

확인할 점 · 1000은 이 예제의 캔버스 크기입니다. 다른 크기의 캔버스에서는 해당 너비와 높이로 바꿉니다.

실습 코드
비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# 좌표는 회전이 끝난 front_layer의 실제 크기로 계산합니다.
center_x = (1000 - front_layer.width) // 2
center_y = (1000 - front_layer.height) // 2
print("회전한 조각 크기:", front_layer.size)
print("중앙 배치의 왼쪽 위:", (center_x, center_y))

corner_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
anchor_canvas = corner_canvas.copy()
centered_canvas = corner_canvas.copy()
corner_canvas.alpha_composite(front_layer, dest=(0, 0))
anchor_canvas.alpha_composite(front_layer, dest=(500, 500))
centered_canvas.alpha_composite(front_layer, dest=(center_x, center_y))
display(comparison_sheet([corner_canvas, anchor_canvas, centered_canvas], ["A / (0, 0)", "B / (500, 500)", "C / centered"]))

중앙 배치 좌표는 (268, 268)입니다. dest=(500, 500)은 사진의 왼쪽 위를 화면 중심에 맞추므로 사진 전체는 오른쪽 아래에 놓입니다. 회전 전 390 대신 회전이 끝난 front_layer의 width·height를 사용합니다.

결과 비교와 해설
A · 왼쪽 위 (0, 0) / B · 왼쪽 위를 (500, 500)에 맞춤 / C · 남는 공간을 반으로 나누어 중앙 배치
A · 왼쪽 위 (0, 0) / B · 왼쪽 위를 (500, 500)에 맞춤 / C · 남는 공간을 반으로 나누어 중앙 배치
같은 조각을 오른쪽 아래에 붙이려면?

x는 1000 − front_layer.width, y는 1000 − front_layer.height입니다. 기본값에서는 (536, 536)이며, 조각의 오른쪽·아래 경계가 캔버스 끝에 맞습니다. 크기가 바뀌어도 숫자를 외우지 않고 실제 크기로 다시 계산할 수 있습니다.

2장의 배경 비교를 코드로 재현하기

이제 배치 방법을 배웠으니 2장에서 본 배경 비교도 직접 실행해 봅니다. A·B는 배경색만, A·C는 두 번째 조각의 유무만 다릅니다. 별도 비교 셀에서 실행하므로 만들고 있는 커버는 그대로 남습니다.

비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# 같은 조각·알파·위치에서 배경색만 비교합니다.
dark_background = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
light_background = Image.new("RGBA", (1000, 1000), (242, 236, 220, 255))
dark_background.alpha_composite(transparent_layer, dest=(40, 100))
light_background.alpha_composite(transparent_layer, dest=(40, 100))
overlapping_background = dark_background.copy()
overlapping_background.alpha_composite(middle_layer, dest=(260, 300))
display(comparison_sheet(
    [dark_background, light_background, overlapping_background],
    ["A / dark background", "B / light background", "C / second layer"],
))

5. 여러 조각 쌓기

핵심 질문

같은 조각과 위치를 사용해도, 올리는 순서가 바뀌면 무엇이 달라질까요?

한 장을 올릴 때마다 의자와 수평선이 하나씩 더 나타납니다. 어디에서 장면이 이어지고 어디에서 겹치는지, 한 장씩 잠깐 멈춰 보며 확인합니다.

첫 사진이 놓인 캔버스에 가운데 조각과 앞 조각을 추가합니다. 명령은 위에서 아래로 실행되므로 나중에 올린 사진이 앞에 놓입니다.

세 조각의 합성 설정
조각회전 전 크기 / 각도왼쪽 위 위치알파
transparent_layer · 뒤760 × 760 / −12°(40, 100)160
middle_layer · 가운데560 × 560 / 0°(260, 300)160
front_layer · 앞390 × 390 / +12°(480, 490)160

실습 4 · 두 장, 세 장으로 쌓기

공통 원리
한 캔버스에 합성 명령을 차례로 추가합니다.
유지 요소
동일한 세 조각·위치·알파

실습 3의 제작 코드에서 출력 줄 위에 가운데 조각과 앞 조각의 합성을 추가합니다. 실행 전 새 의자와 수평선이 어디에 나타날지 예상합니다.

코드 읽기 · 한 캔버스에 이어서 합성하기

앞 단계에서 만든 세 조각 중 가운데와 앞 조각을 같은 캔버스에 추가합니다.

코드하는 일결과
lesson_canvas.alpha_composite(middle_layer, dest=(260, 300))첫 조각이 있는 화면에 가운데 조각을 올립니다.사진 두 장이 겹친 화면
two_layers = lesson_canvas.copy()세 번째 조각을 올리기 전에 복사합니다.두 장일 때의 결과를 보관합니다.
lesson_canvas.alpha_composite(front_layer, dest=(480, 490))작은 조각을 마지막에 올립니다.작은 사진이 앞에 놓인 세 장의 합성

확인할 점 · two_layers = lesson_canvas는 복사가 아닙니다. 같은 이미지를 가리키므로 이후 합성도 함께 보입니다.

실습 코드

실습 3의 one_layer = lesson_canvas.copy() 아래에 ‘# 3. 가운데 조각과 앞 조각 추가’를 붙입니다. 마지막 출력은 세 중간 결과를 비교하도록 바꿉니다.

현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
# 1. 1교시의 함수로 조각을 준비하고 알파를 적용
layer_alpha = 160
transparent_layer = transform_piece(working_image, demo_crop_box, (760, 760), -12, layer_alpha)
middle_layer = transform_piece(working_image, demo_crop_box, (560, 560), 0, layer_alpha)
front_layer = transform_piece(working_image, demo_crop_box, (390, 390), 12, layer_alpha)

# 2. 새 캔버스에 첫 조각 배치
lesson_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
lesson_canvas.alpha_composite(transparent_layer, dest=(40, 100))
one_layer = lesson_canvas.copy()

# 3. 가운데 조각과 앞 조각 추가
lesson_canvas.alpha_composite(middle_layer, dest=(260, 300))
two_layers = lesson_canvas.copy()
lesson_canvas.alpha_composite(front_layer, dest=(480, 490))
three_layers = lesson_canvas.copy()

display(comparison_sheet([one_layer, two_layers, three_layers], ["1 layer", "2 layers", "3 layers"]))

합성 직후마다 copy()로 그 순간을 남깁니다. 출력 세 장은 각각 한 장, 두 장, 세 장이 놓인 결과입니다. 아직 제목을 추가하지 않아 하단 사진이 보입니다.

결과 비교와 해설
two_layers = lesson_canvas라고 쓰면 안 되나요?

두 이름이 같은 이미지 객체를 가리키게 됩니다. 이후 앞 조각을 올리면 two_layers도 세 장이 놓인 모습이 됩니다. 두 장일 때의 상태를 따로 남기려면 copy()로 복사해야 합니다.

6. 합성 순서 비교

핵심 질문

사진을 이동하지 않고 큰 사진을 맨 앞으로 가져올 수 있을까요?

겹친 사진에서는 앞에 놓인 장면이 먼저 눈에 들어오기도 합니다. 어느 의자를 더 또렷하게 남기고 싶은지 생각하며 순서를 바꿔 봅니다. 같은 재료라도 먼저 보이는 장면이 달라지면 커버의 인상도 달라질 수 있습니다.

제작 코드는 남겨 두고 별도 비교 셀에서 reverse_canvas를 만듭니다. 합성 명령 세 줄의 순서를 앞 → 가운데 → 뒤로 바꿉니다. 각 사진의 좌표는 그대로 두고, 겹친 의자와 수평선 중 어느 것이 앞에 보이는지 비교합니다.

비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# 비교용 셀: 같은 조각과 위치를 사용하고, 올리는 순서만 바꿉니다.
reverse_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
reverse_canvas.alpha_composite(front_layer, dest=(480, 490))
reverse_canvas.alpha_composite(middle_layer, dest=(260, 300))
reverse_canvas.alpha_composite(transparent_layer, dest=(40, 100))
display(comparison_sheet([three_layers, reverse_canvas], ["1 > 2 > 3", "3 > 2 > 1"]))

결과 비교와 해설

같은 알파 160이라도 순서에 따라 겹친 곳의 색이 달라집니다. 큰 사진이 앞에 와도 아래 사진이 완전히 사라지지는 않습니다. 비교 셀은 three_layers를 바꾸지 않으므로 제작 코드로 바로 돌아갈 수 있습니다.

7. 복사와 재실행

핵심 질문

같은 코드인데 실행할 때마다 결과가 달라진다면, 어떤 이미지 위에서 다시 작업했는지 확인할 수 있을까요?

마음에 드는 중간 결과를 남겨 두면 다음 시도를 하더라도 돌아올 곳이 생깁니다. 하지만 변수에 새 이름을 붙이는 것만으로 그 순간의 이미지가 보관되지는 않습니다.

지금까지 쓴 명령을 새 이미지를 반환하는 작업과 기존 이미지를 직접 바꾸는 작업으로 나누어 봅니다. 이 차이를 알면 어느 시점에 복사해야 하는지, 다시 실행했을 때 왜 결과가 달라졌는지 이해할 수 있습니다.

변형과 합성의 결과 받기
코드이미지의 변화결과를 받는 방법
crop / resize / rotate / copy새 이미지 반환새 변수에 대입
putalpha해당 이미지의 알파를 직접 수정반환값을 이미지 변수에 대입하지 않음
canvas.alpha_composite해당 캔버스에 합성을 누적캔버스 이름은 그대로 사용

비교 실습 A · 새 이름과 복사본

공통 원리
단순 대입과 copy의 차이를 이미지로 확인합니다.
유지 요소
같은 첫 레이어 결과

shared_canvas와 saved_canvas를 준비한 뒤 원본에 가운데 사진을 추가하면 어느 결과가 달라질지 예상합니다.

코드 읽기 · 대입과 복사 구분하기

이미지 객체를 다른 이름에 대입하면 같은 이미지를 가리킵니다. 독립된 결과를 보관하려면 copy가 필요합니다.

코드하는 일결과
shared_canvas = copy_canvas같은 이미지에 다른 이름을 붙입니다.한쪽으로 합성해도 두 이름에서 같은 변경을 봅니다.
saved_canvas = copy_canvas.copy()현재 픽셀을 별도 이미지에 복사합니다.이후 copy_canvas를 바꿔도 saved_canvas는 그대로입니다.
copy_canvas.alpha_composite(middle_layer, ...)가운데 조각을 비교 캔버스에 추가합니다.copy_canvas와 shared_canvas에는 두 장, saved_canvas에는 한 장

확인할 점 · copy를 어느 시점에 실행했는지도 확인합니다. 세 장을 합성한 뒤 복사하면 세 장일 때의 결과가 보관됩니다.

실습 코드
비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# 비교용 이미지에서만 실험합니다. 공동 제작 one_layer는 그대로입니다.
copy_canvas = one_layer.copy()
shared_canvas = copy_canvas
saved_canvas = copy_canvas.copy()

copy_canvas.alpha_composite(middle_layer, dest=(260, 300))
display(comparison_sheet(
    [copy_canvas, shared_canvas, saved_canvas],
    ["A / edited canvas", "B / assigned name", "C / copied before edit"],
))

A·B는 같은 객체를 가리켜 둘 다 바뀝니다. C는 변경 전에 만든 별도 복사본이어서 한 장일 때를 유지합니다. 복사는 중간 결과를 보관하는 작업이며, 복사한 시점도 결과를 결정합니다.

결과 비교와 해설
A · 수정한 캔버스 / B · 같은 객체에 붙인 이름 / C · 수정 전에 복사한 이미지
A · 수정한 캔버스 / B · 같은 객체에 붙인 이름 / C · 수정 전에 복사한 이미지

비교 실습 B · 합성 줄만 다시 실행

공통 원리
알파와 위치를 유지하고 합성 횟수만 비교합니다.
유지 요소
같은 transparent_layer · dest=(40, 100)

한 번 합성한 A 위에 다시 같은 사진을 올린 B와, 새 캔버스에서 다시 만든 C를 예측합니다. 어떤 두 결과가 같을까요?

실습 코드
비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# A. 빈 캔버스에 한 번 합성합니다.
once_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
once_canvas.alpha_composite(transparent_layer, dest=(40, 100))

# B. 합성 줄만 다시 실행한 상황을 만듭니다.
twice_canvas = once_canvas.copy()
twice_canvas.alpha_composite(transparent_layer, dest=(40, 100))

# C. 새 캔버스부터 다시 실행한 상황입니다.
reset_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
reset_canvas.alpha_composite(transparent_layer, dest=(40, 100))
display(comparison_sheet([once_canvas, twice_canvas, reset_canvas], ["A / once", "B / twice", "C / reset then once"]))

A·C는 같고 B는 사진의 색이 더 강하게 반영됩니다. 입력 알파 160은 그대로지만 합성한 횟수가 달라졌습니다. 재실행할 때는 새 캔버스 생성부터 포함된 제작 셀 전체를 실행합니다.

결과 비교와 해설
A · 한 번 합성 / B · 같은 화면에 두 번 합성 / C · 새 화면에서 다시 한 번 합성
A · 한 번 합성 / B · 같은 화면에 두 번 합성 / C · 새 화면에서 다시 한 번 합성
결과가 예상과 다를 때 확인할 곳
관찰가능한 원인돌아갈 곳
이전 비교 이미지까지 바뀜복사 대신 같은 객체에 이름만 붙임보관하는 줄의 .copy()
실행할수록 사진 색이 강해짐이미 합성된 캔버스에 다시 누적Image.new부터 실행
값을 고쳤는데 결과는 그대로정의나 호출 셀을 다시 실행하지 않음정의 → 호출 → 합성 → 표시

8. 반복문으로 합성 코드 정리

사진 세 장을 올린 코드를 다시 보면, 조각과 위치만 바뀌고 합성 명령은 반복됩니다. 조각 목록과 위치 목록에서 같은 순번의 값을 꺼내면 이 작업을 반복문으로 쓸 수 있습니다. 1교시의 함수는 조각을 만들고, 반복문은 그 조각들을 한 캔버스에 차례로 올립니다.

코드 읽기 · 레이어와 위치를 같은 순번으로 읽기

사진 세 장을 직접 합성한 코드를 4주차 반복문으로 정리합니다. 같은 조각과 위치를 사용해 같은 결과를 얻습니다.

코드하는 일결과
layers = [transparent_layer, middle_layer, front_layer]이미 만든 조각을 배치 순서대로 담습니다.이미지 세 개의 리스트
for index in range(3):0, 1, 2를 차례로 선택합니다.index는 이번에 배치할 조각의 순번입니다.
layers[index] / layer_positions[index]두 목록에서 같은 순번의 값을 읽습니다.이번 조각과 그 조각을 놓을 좌표가 짝을 이룹니다.

확인할 점 · Image.new는 반복 밖에서 한 번 실행합니다. 안에서 매번 새로 만들면 마지막 조각만 남습니다.

세 합성 명령을 반복문으로 정리
세 합성 명령을 반복문으로 정리
# 코드 정리: 이미 만든 조각들을 한 캔버스에 차례로 올립니다.
layers = [transparent_layer, middle_layer, front_layer]
layer_positions = [(40, 100), (260, 300), (480, 490)]
loop_cover = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))

for index in range(3):
    loop_cover.alpha_composite(layers[index], dest=layer_positions[index])

display(comparison_sheet([three_layers, loop_cover], ["three calls", "one loop"]))

순번에 따라 선택되는 조각과 위치
indexlayers[index]layer_positions[index]
0transparent_layer(40, 100)
1middle_layer(260, 300)
2front_layer(480, 490)

비교 실습 · 캔버스 생성 위치

공통 원리
Image.new가 반복 안인지 밖인지 비교합니다.
유지 요소
세 조각·순서·위치는 동일

먼저 매 반복의 시작에서 이전 화면이 남는지 따라가 봅니다. 반복 안에서 새로 만들면 마지막에 어떤 조각이 보일까요?

실습 코드
비교용 셀 · 제작 코드와 별도로 실행
비교용 셀 · 제작 코드와 별도로 실행
# 비교용 잘못된 예: 반복할 때마다 앞의 합성 결과를 버립니다.
for index in range(3):
    reset_each_cover = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
    reset_each_cover.alpha_composite(layers[index], dest=layer_positions[index])

display(comparison_sheet([loop_cover, reset_each_cover], ["A / canvas before loop", "B / canvas inside loop"]))

B는 매번 앞 결과를 버리므로 마지막 작은 조각만 남습니다. 비교 코드에서 Image.new를 for 앞에 한 번만 실행하도록 옮기면 A처럼 세 장이 쌓입니다. 합성 명령은 반복 안에 유지합니다.

결과 비교와 해설
A · 반복 전에 캔버스 생성 / B · 반복할 때마다 새 캔버스 생성
A · 반복 전에 캔버스 생성 / B · 반복할 때마다 새 캔버스 생성

제작 코드로 돌아가기

비교와 반복문 정리는 별도 변수에서 실행했습니다. 다음 장에서는 실습 4의 누적 제작 코드에 제목을 추가합니다. 아래 전체 코드로도 같은 커버를 완성할 수 있습니다.

9. 커버 완성과 결과 설명

처음에는 완성된 모습만 보았던 커버를 이제 한 단계씩 설명할 수 있습니다. 의자 주변에 남긴 공간, 기울어진 수평선, 뒤로 비치는 바다는 각각의 선택이 쌓인 결과입니다. 마지막으로 제목을 더해 한 장을 마무리합니다.

제작 코드에 제공된 제목 띠와 글자를 추가합니다. 글자가 가려지지 않도록 사진을 모두 합성한 뒤에 그립니다.

실습 5 · 제목을 더해 커버 완성

공통 원리
완성된 사진 합성 위에 제공 디자인을 추가합니다.
유지 요소
기본 사진·크기·각도·알파·배치

제작 셀의 three_layers 아래에 강조된 제목 코드를 추가하고 마지막 출력은 rebuilt_cover로 바꿉니다. 글꼴과 색상 옵션은 제공값을 사용합니다.

코드 읽기 · 사진 위에 제목 디자인 더하기

6주차의 그리기 도구로 하단 띠와 글자를 추가하는 제공 코드입니다. 사진 합성 뒤에 실행합니다.

코드하는 일결과
rebuilt_cover = three_layers.copy()사진 합성 결과를 복사합니다.제목을 더할 최종 이미지. 사진만 있는 원본도 남습니다.
draw = ImageDraw.Draw(rebuilt_cover)최종 이미지에 그릴 도구를 연결합니다.아래 draw 명령은 rebuilt_cover를 직접 수정합니다.
draw.rectangle(...) / draw.line(...) / draw.text(...)배경 띠·구분선·제목을 차례로 그립니다.사진 아래쪽을 덮고 제목이 보이게 합니다. 글꼴 옵션은 제공값을 사용합니다.

확인할 점 · 사진을 모두 합성한 다음 그립니다. 아래 전체 코드의 앞부분은 앞 실습과 같고, 마지막 제목 부분만 추가합니다.

실습 코드

실습 4의 three_layers 아래에 ‘# 4. 제공된 제목 디자인 추가’를 붙입니다. 아래 전체 코드는 새 캔버스부터 다시 만들므로 반복 실행해도 사진이 중복해서 쌓이지 않습니다.

현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
현재 단계까지의 전체 코드 · 배경색은 이번에 추가할 부분
# 1. 1교시의 함수로 조각을 준비하고 알파를 적용
layer_alpha = 160
transparent_layer = transform_piece(working_image, demo_crop_box, (760, 760), -12, layer_alpha)
middle_layer = transform_piece(working_image, demo_crop_box, (560, 560), 0, layer_alpha)
front_layer = transform_piece(working_image, demo_crop_box, (390, 390), 12, layer_alpha)

# 2. 새 캔버스에 첫 조각 배치
lesson_canvas = Image.new("RGBA", (1000, 1000), (20, 40, 44, 255))
lesson_canvas.alpha_composite(transparent_layer, dest=(40, 100))
one_layer = lesson_canvas.copy()

# 3. 가운데 조각과 앞 조각 추가
lesson_canvas.alpha_composite(middle_layer, dest=(260, 300))
two_layers = lesson_canvas.copy()
lesson_canvas.alpha_composite(front_layer, dest=(480, 490))
three_layers = lesson_canvas.copy()

# 4. 제공된 제목 디자인 추가
rebuilt_cover = three_layers.copy()
draw = ImageDraw.Draw(rebuilt_cover)
draw.rectangle((0, 850, 1000, 1000), fill=(20, 40, 44, 255))
draw.line((42, 866, 958, 866), fill=(245, 232, 202), width=2)
draw.text((42, 882), "AFTER HOURS", fill=(245, 232, 202), font=ImageFont.load_default(size=66))
draw.text((44, 964), "01 / A QUIET REPEAT", fill=(245, 232, 202), font=ImageFont.load_default(size=16))

display(rebuilt_cover.resize((500, 500)))

rebuilt_cover = three_layers.copy()로 사진만 있는 결과를 남겨 두고 복사본에 제목을 그립니다. 아래 전체 코드는 캔버스부터 다시 만드므로 선택 비교나 반복문을 실행하지 않아도 완성됩니다.

결과 비교와 해설
기본값으로 실행하면 처음 본 echo 커버와 같은 결과가 됩니다.
기본값으로 실행하면 처음 본 echo 커버와 같은 결과가 됩니다.

한 장을 완성한 과정

원본의 영역·크기·각도는 1교시의 함수에서 결정했습니다. 이번에는 그 함수에 알파를 더하고, 새 캔버스에서 위치·순서를 정했습니다. make_cover()는 이 제작 과정과 제목 디자인을 묶어 완성 이미지 한 장을 반환하는 제공 함수입니다.

결과 확인과 다음 실습

지금 만든 결과로 설명하기
확인할 것코드에서 찾을 부분
뒤 사진이 비치는 이유회전 전에 적용한 putalpha
큰 사진과 작은 사진의 위치각 alpha_composite의 dest
작은 사진이 앞인 이유합성 명령의 실행 순서
재실행해도 같은 결과인 이유새 캔버스를 만드는 Image.new

완성된 커버를 처음 본 원본과 다시 나란히 놓아 봅니다. 가장 먼저 눈에 들어오는 곳은 어디인가요? 그 차이를 만든 코드 한 줄을 짚을 수 있다면, 다음 사진에서는 무엇을 바꿀지도 정할 수 있습니다.

3교시에는 자신의 사진 한 장에서 출발합니다. 원본의 출처를 기록하고 세 가지 구성을 미리 본 뒤, 고른 구성에서 한 값만 바꾼 후보 세 장을 만듭니다. 표현 의도와 원본 선택부터 이어갑니다. 후보를 만드는 반복문은 실제로 세 장을 만들 때 함께 살펴봅니다.

Pillow 문서에서는 alpha_composite의 두 이미지 크기가 같아야 한다는데요? · 보충

Image.alpha_composite(im1, im2) 모듈 함수와 canvas.alpha_composite(layer, dest=...) 인스턴스 메서드는 다릅니다. 수업에서는 크기가 다른 레이어도 위치를 정해 올릴 수 있는 인스턴스 메서드를 사용합니다.

참고 자료

  • Pillow: Image불러오기, 자르기, 크기 변경, 회전과 알파 합성의 공식 설명
  • Pillow: ImageOps휴대폰 사진의 방향 정보를 반영하는 exif_transpose
이 페이지의 목차