이미지 좌표계와 픽셀 조작
실습 준비
import cv2 as cv
from yeom import to_pil
image = cv.imread("balloon.webp")
height, width, channels = image.shape
width, height, channels # 가로, 세로, 채널 수
(1024, 1024, 3)
이미지 좌표계
이미지의 원점 (0, 0)은 왼쪽 위. 오른쪽으로 갈수록 x 좌표가 커지고 아래쪽으로 갈수록 y 좌표가 커짐.
(0, 0) ─────────────→ x
│
↓ y
좌표는 보통 (x, y) 순서로 표현하지만 NumPy 배열은 [행, 열], 즉 [y, x] 순서로 접근.
| 표현 | 순서 |
|---|---|
| 이미지 좌표 | (x, y) |
| NumPy 인덱싱 | image[y, x] |
| NumPy 이미지 모양 | (height, width, channels) |
픽셀 조회
이미지 중앙의 좌표와 BGR 픽셀값을 확인.
x = width // 2
y = height // 2
x, y # 중앙 좌표
(512, 512)
image[y, x] # BGR 픽셀값
array([ 17, 108, 47], dtype=uint8)
cv.imread()가 기본 모드로 반환한 컬러 이미지의 마지막 축은 B, G, R 채널 순서.
채널 하나씩 조회하려면 세 번째 인덱스를 사용.
image[y, x, 0] # B 채널 값
np.uint8(17)
image[y, x, 1] # G 채널 값
np.uint8(108)
image[y, x, 2] # R 채널 값
np.uint8(47)
픽셀 변경과 이미지 복사
NumPy 배열은 픽셀값을 직접 변경 가능. 원본을 보존하려면 먼저 copy()로 복사본을 만듦.
edited = image.copy()
edited[y, x] = (0, 0, 255) # BGR의 빨강
to_pil(edited)
edited = image는 같은 배열에 이름만 하나 더 붙이는 것이므로 edited를 바꾸면 image도 바뀜. 독립적인 결과 이미지에는 image.copy()를 사용.
행과 열 선택
배열 슬라이싱으로 이미지의 행과 열을 선택 가능.
top_half = image[: height // 2, :]
to_pil(top_half)
left_half = image[:, : width // 2]
to_pil(left_half)
image[y1:y2, :]: y 방향의 행 범위 선택image[:, x1:x2]: x 방향의 열 범위 선택- 슬라이스의 끝 위치인
y2,x2는 포함하지 않음
관심 영역과 자르기
관심 영역(region of interest, ROI)은 이미지에서 처리하려는 직사각형 영역. (x, y)를 왼쪽 위 좌표, (w, h)를 영역의 가로·세로 크기로 사용.
x = width // 2 # ROI의 시작을 가로 중앙에 배치
y = height // 3 # ROI의 시작을 세로 1/3 지점에 배치
w = width // 4 # 전체 가로의 1/4을 ROI 너비로 사용
h = height // 5 # 전체 세로의 1/5를 ROI 높이로 사용
cropped = image[y : y + h, x : x + w].copy()
to_pil(cropped)
NumPy에서는 세로 범위를 먼저 작성하므로 image[y:y+h, x:x+w] 순서.
cropped.shape # (h, w, channels)
(204, 256, 3)
슬라이스와 원본의 관계
NumPy 슬라이스는 일반적으로 원본 데이터를 공유하는 뷰(view). 슬라이스를 수정하면 원본 역할을 하는 배열도 함께 바뀜.
view_example = image.copy()
roi_view = view_example[y : y + h, x : x + w]
roi_view[:] = (0, 0, 255)
to_pil(view_example)
원본과 독립된 ROI가 필요하면 슬라이스 뒤에 copy()를 사용.
roi_copy = image[y : y + h, x : x + w].copy()
- 슬라이스는 원본과 데이터를 공유하므로, 수정하면 원본 이미지까지 바뀔 수 있음.
- 결과 이미지를 따로 만들 때는 먼저
image.copy()를 사용.
특정 영역 변경
슬라이스에 하나의 값을 대입하면 선택한 모든 픽셀에 브로드캐스팅됨.
검정으로 채우기:
blocked = image.copy()
blocked[y : y + h, x : x + w] = 0
to_pil(blocked)
빨강으로 채우기:
red_region = image.copy()
red_region[y : y + h, x : x + w] = (0, 0, 255)
to_pil(red_region)
선택한 영역의 B 채널만 0으로 만들기:
blue_removed = image.copy()
blue_removed[y : y + h, x : x + w, 0] = 0
to_pil(blue_removed)
현재 영역의 픽셀값을 이용한 연산도 가능. 다음 코드는 0–255 범위의 uint8 영역만 색 반전.
inverted_region = image.copy()
region = inverted_region[y : y + h, x : x + w]
inverted_region[y : y + h, x : x + w] = 255 - region
to_pil(inverted_region)
cv.add()를 ROI에 적용하면 선택한 영역만 밝게 만들 수 있음. 결과가 255를 넘으면 255로 포화됨.
brightened_region = image.copy()
region = brightened_region[y : y + h, x : x + w]
brightness = 60 # ROI의 모든 채널에 더할 밝기 값
brightened_region[y : y + h, x : x + w] = cv.add(region, brightness)
to_pil(brightened_region)
관심 영역 복사
잘라 낸 ROI를 크기가 같은 다른 영역에 대입하면 이미지 안에서 복사 가능.
source = image[y : y + h, x : x + w].copy()
target_x = width // 8 # 붙여 넣을 영역의 x 시작점
target_y = height * 2 // 3 # 붙여 넣을 영역의 y 시작점
pasted = image.copy()
pasted[target_y : target_y + h, target_x : target_x + w] = source
to_pil(pasted)
대입하는 원본과 대상 영역의 높이, 너비, 채널 수가 모두 동일.
source.shape, pasted[target_y : target_y + h, target_x : target_x + w].shape
((204, 256, 3), (204, 256, 3))
퀴즈
이미지 좌표 (x, y) = (30, 20)에 있는 픽셀 전체를 NumPy 배열 image에서 조회하는 표현은?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.