Post

NumPy로 이미지 데이터의 본질 이해하기

shape·dtype·range로 이미지 배열을 이해하고 OpenCV 전처리 버그를 예방하는 핵심 가이드

NumPy로 이미지 데이터의 본질 이해하기

TL;DR

  • 이미지는 “그림”이 아니라 NumPy 배열이다.
  • 모든 이미지 버그의 80%는 shape / dtype / value range misunderstanding에서 발생한다.
  • OpenCV 문제는 대부분 연산 자체가 아니라 데이터 상태의 문제다.
  • uint8 ↔ float32 전환은 결과를 완전히 바꾼다.
  • slicing / broadcasting은 이미지 조작의 핵심 도구다.
  • 이 글을 이해하지 못하면 이후 모든 CV 학습은 운에 맡기게 된다.

1. 핵심 개념: 이미지 = 배열이라는 말의 정확한 의미

이미지를 배열이라고 말할 때, 이는 비유가 아니다. 정확히 NumPy ndarray다.

기본 구조

1
2
3
4
5
6
7
8
9
10
11
이미지 배열의 3요소
├─ shape
│  ├─ H: height (행)
│  ├─ W: width (열)
│  └─ C: channel (색상)
├─ dtype
│  ├─ uint8  (0~255)
│  └─ float32 (0.0~1.0 or 그 이상)
└─ value range
   ├─ 정수 이미지: [0, 255]
   └─ 실수 이미지: [0.0, 1.0] (관례)

OpenCV 기본 이미지는 다음 형태다:

1
(H, W, 3), dtype=uint8, BGR

2. 왜 이걸 반드시 이해해야 하는가

이미지 연산은 수학 연산이다. 문제는 이미지가 “보여서” 직관을 속인다는 점이다.

  • 같은 연산이라도 dtype이 다르면 결과가 완전히 달라진다.
  • 값 범위를 넘으면 overflow / clip이 조용히 발생한다.
  • slicing은 복사(copy)가 아니라 뷰(view) 일 수 있다.

👉 즉, 코드가 돌아가도 결과가 틀릴 수 있다.


3. 언제 이 문제가 실제로 터지는가

  • 밝기/대비 조절이 이상할 때
  • threshold 결과가 예상과 다를 때
  • 색상이 갑자기 깨질 때
  • 모델 입력 전처리에서 성능이 안 나올 때
  • 같은 코드를 다른 환경에서 돌렸을 때

4. 최소 실습: NumPy로 이미지의 본질 직접 만져보기

외부 이미지 ❌ NumPy로 직접 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import numpy as np

def print_info(name, arr):
    print(f"[{name}] shape={arr.shape}, dtype={arr.dtype}, "
          f"min={arr.min()}, max={arr.max()}")

# 1. 가짜 이미지 생성 (그라디언트)
H, W = 100, 200
img = np.tile(np.linspace(0, 255, W, dtype=np.uint8), (H, 1))
img = np.stack([img, img, img], axis=-1)  # (H, W, 3)

print_info("original", img)

# 2. 밝기 증가 (uint8)
bright = img + 50
print_info("bright_uint8", bright)

# 3. float 변환 후 밝기 조절
img_f = img.astype(np.float32) / 255.0
bright_f = np.clip(img_f + 0.2, 0.0, 1.0)

print_info("bright_float", bright_f)

# 4. 마스킹 (중앙 영역만)
mask = np.zeros((H, W), dtype=bool)
mask[30:70, 60:140] = True

masked = img.copy()
masked[~mask] = 0
print_info("masked", masked)

# 5. 채널 스왑 (RGB <-> BGR 실험용)
swapped = img[..., ::-1]
print_info("channel_swapped", swapped)

결과 해석 포인트

  • uint8에서 덧셈은 255를 넘어가면 overflow
  • float32에서는 값 범위를 직접 관리해야 한다
  • 마스킹은 좌표계(H, W)에 대한 정확한 이해가 필수
  • 채널 순서가 바뀌어도 shape은 같아서 버그가 조용히 발생

5. 이미지 연산이 어려운 이유 (실수 사례 기반)

이미지 연산이 어려운 이유는 하나다.

눈에 보이는 결과가 항상 논리적으로 옳지 않다

대표적인 함정:

  • 값은 정상인데 dtype이 틀림
  • 연산은 맞는데 범위가 틀림
  • shape은 맞는데 의미가 틀림
  • 복사한 줄 알았는데 view였음

6. 흔한 실수 / 디버깅 체크리스트 (중요)

  1. uint8 상태에서 덧셈/곱셈 수행
  2. overflow 발생했는데 눈치 못 챔
  3. float 이미지인데 값 범위 0~255 유지
  4. BGR/RGB 혼동
  5. (H,W)(H,W,1) 혼동
  6. slicing 결과가 view인지 copy인지 모름
  7. mask dtype이 bool이 아님
  8. 채널 축(axis=-1) 개념 불명확
  9. astype 위치가 잘못됨
  10. min/max 출력 없이 연산 진행
  11. 정규화 후 다시 uint8로 안 돌림

7. OpenCV로 넘어가기 전에 반드시 점검할 5문장

  1. 나는 이미지의 shape / dtype / range를 항상 출력한다.
  2. uint8 연산의 위험성을 설명할 수 있다.
  3. float 이미지에서 clip/정규화 이유를 안다.
  4. slicing이 view인지 copy인지 구분한다.
  5. BGR/RGB 문제를 즉시 의심한다.

섹션 요약

  • 이미지는 수치 데이터다.
  • OpenCV 문제의 대부분은 NumPy 문제다.
  • 이 글을 기준으로 이후 모든 이미지 처리는 “데이터 상태 점검 → 연산” 순서로 진행해야 한다.

다음 글 / 다음 학습

➡️ 다음 글: OpenCV 이미지 처리 연산: 목적별 분류와 전처리 파이프라인

다음 글에서는 NumPy로 이해한 이 개념들이 OpenCV API에서 어떻게 구현되는지를 본격적으로 연결한다.


This post is licensed under CC BY 4.0 by the author.