logo

이미지 좌표계와 픽셀 조작

실습 준비

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))

퀴즈

문제 1 / 5맞음: 0힌트: 0틀림: 0채점중: 0남음: 5

이미지 좌표 (x, y) = (30, 20)에 있는 픽셀 전체를 NumPy 배열 image에서 조회하는 표현은?

  • image[20, 30]
  • image[30, 20]
  • image[20, 30, 3]
  • image[30, 20, :] = 0

퀴즈를 풀려면 대화형 기능을 불러와야 합니다.

Previous
룩업 테이블과 감마 보정