이미지 좌표계와 픽셀 조작
실습 준비
import cv2 as cv
image = cv.imread("balloon.webp")
if image is None:
raise FileNotFoundError("이미지를 읽을 수 없습니다")
이미지 표시
show() 함수는 OpenCV와 이미지·영상 입출력에서 정의한 함수를 사용함.
height, width, channels = image.shape
width, height, channels
이미지 좌표계
이미지의 원점 (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
pixel = image[y, x]
{
"좌표": (x, y),
"BGR": pixel.tolist(),
}
OpenCV 컬러 이미지의 마지막 축은 B, G, R 채널 순서임.
blue, green, red = image[y, x]
{
"B": int(blue),
"G": int(green),
"R": int(red),
}
채널 하나만 조회하려면 세 번째 인덱스를 사용함.
image[y, x, 2] # R 채널 값
픽셀 변경과 이미지 복사
NumPy 배열은 픽셀값을 직접 변경 가능함. 원본을 보존하려면 먼저 copy()로 복사본을 만듦.
edited = image.copy()
edited[y, x] = (0, 0, 255) # BGR의 빨강
show(edited)
edited = image는 같은 배열에 이름만 하나 더 붙이는 것이므로 edited를 바꾸면 image도 바뀜. 독립적인 결과 이미지에는 image.copy()를 사용함.
행과 열 선택
배열 슬라이싱으로 이미지의 행과 열을 선택 가능함.
top_half = image[: height // 2, :]
show(top_half)
left_half = image[:, : width // 2]
show(left_half)
image[y1:y2, :]: y 방향의 행 범위 선택image[:, x1:x2]: x 방향의 열 범위 선택- 슬라이스의 끝 위치인
y2,x2는 포함하지 않음
관심 영역과 자르기
관심 영역(region of interest, ROI)은 이미지에서 처리하려는 직사각형 영역임. (x, y)를 왼쪽 위 좌표, (w, h)를 영역의 가로·세로 크기로 사용함.
x = width // 2
y = height // 3
w = width // 4
h = height // 5
cropped = image[y : y + h, x : x + w].copy()
show(cropped)
NumPy에서는 세로 범위를 먼저 작성하므로 image[y:y+h, x:x+w] 순서임.
cropped.shape # (h, w, channels)
슬라이스와 원본의 관계
NumPy 슬라이스는 일반적으로 원본 데이터를 공유하는 뷰(view)임. 슬라이스를 수정하면 원본 역할을 하는 배열도 함께 바뀜.
view_example = image.copy()
roi_view = view_example[y : y + h, x : x + w]
roi_view[:] = (0, 0, 255)
show(view_example)
원본과 독립된 ROI가 필요하면 슬라이스 뒤에 copy()를 사용함.
roi_copy = image[y : y + h, x : x + w].copy()
ROI를 수정하기 전에 복사 여부 확인
슬라이스가 원본과 데이터를 공유한다는 사실을 모르고 수정하면 원본 이미지까지 바뀔 수 있음. 결과 이미지를 따로 만들 때는 먼저 image.copy()를 사용함.
특정 영역 변경
슬라이스에 하나의 값을 대입하면 선택한 모든 픽셀에 브로드캐스팅됨.
검정으로 채우기:
blocked = image.copy()
blocked[y : y + h, x : x + w] = 0
show(blocked)
빨강으로 채우기:
red_region = image.copy()
red_region[y : y + h, x : x + w] = (0, 0, 255)
show(red_region)
선택한 영역의 B 채널만 0으로 만들기:
blue_removed = image.copy()
blue_removed[y : y + h, x : x + w, 0] = 0
show(blue_removed)
현재 영역의 픽셀값을 이용한 연산도 가능함. 다음 코드는 선택한 영역만 색 반전함.
inverted_region = image.copy()
region = inverted_region[y : y + h, x : x + w]
inverted_region[y : y + h, x : x + w] = 255 - region
show(inverted_region)
cv.add()를 ROI에 적용하면 선택한 영역만 밝게 만들 수 있음. 결과가 255를 넘으면 255로 포화됨.
brightened_region = image.copy()
region = brightened_region[y : y + h, x : x + w]
brightened_region[y : y + h, x : x + w] = cv.add(region, 60)
show(brightened_region)
관심 영역 복사
잘라 낸 ROI를 크기가 같은 다른 영역에 대입하면 이미지 안에서 복사 가능함.
source = image[y : y + h, x : x + w].copy()
target_x = width // 8
target_y = height * 2 // 3
pasted = image.copy()
pasted[target_y : target_y + h, target_x : target_x + w] = source
show(pasted)
대입하는 원본과 대상 영역의 높이, 너비, 채널 수가 같아야 함.
source.shape, pasted[target_y : target_y + h, target_x : target_x + w].shape
좌표로 도형과 글자 그리기
OpenCV의 그리기 함수는 좌표를 (x, y) 순서로 받음. NumPy 인덱싱의 [y, x]와 구분해야 함.
annotated = image.copy()
cv.rectangle(
annotated,
(x, y),
(x + w, y + h),
(0, 255, 0),
3,
)
cv.circle(
annotated,
(x + w // 2, y + h // 2),
6,
(0, 0, 255),
-1,
)
cv.putText(
annotated,
"ROI",
(x, y - 10),
cv.FONT_HERSHEY_SIMPLEX,
0.8,
(0, 255, 0),
2,
cv.LINE_AA,
)
show(annotated)
cv.rectangle(), cv.circle(), cv.putText()는 전달한 배열을 직접 수정함. 원본을 보존하려면 그리기 전에 복사본을 만듦.