컴퓨터공학 300 주제 시리즈의 285번째 글이다. 전체 지도는 여기.

한 줄 요약

디지털 이미지는 숫자의 격자다. 이미지 처리는 그 격자에 점 연산(밝기·임계값), 이웃 연산(합성곱 필터), 전역 연산(히스토그램)을 적용해 원하는 정보를 남기는 일이다.

왜 필요한가

이미지 처리는 생각보다 많은 곳에 있다.

  • 업로드된 사진의 썸네일 생성, 회전 보정, 압축
  • 문서 스캔 앱의 이진화와 기울기 보정
  • OCR 이나 바코드 인식 전의 전처리
  • 딥러닝 비전 모델의 입력 정규화, 데이터 증강
  • CNN 의 “합성곱” 층 자체

특히 CNN 을 이해하려면 고전적인 합성곱 필터를 먼저 알아야 한다. CNN 은 사람이 손으로 설계하던 필터 값을 데이터에서 학습하는 구조이기 때문이다.

핵심 개념

이미지의 표현

회색조 이미지는 높이 x 너비의 2차원 배열이다. 컬러는 채널 축이 하나 더 붙는다. NumPy 로 읽으면 보통 (H, W, C) 모양이고, 좌표는 img[y, x] 순서다. 수학 시간의 (x, y) 와 순서가 반대라서 자주 헷갈린다.

img[y, x]       y 는 행(위에서 아래로), x 는 열(왼쪽에서 오른쪽으로)
dtype uint8     0~255. 더하면 넘친다 -> 계산 전에 float 로 바꾸는 습관

uint8 배열에서 200 + 100 은 44 가 된다. 256 으로 나눈 나머지로 넘치기 때문이다. 처리 전에 실수형으로 바꾸고, 끝에서 0~255 로 잘라(clip) 다시 정수로 만드는 것이 안전하다.

점 연산

픽셀 하나의 값만 보고 새 값을 정한다.

연산 식 쓰임
밝기 조절 v + b 전체를 밝게·어둡게
대비 조절 a * (v - 128) + 128 대비 강화
반전 255 - v 음화
임계값 이진화 255 if v > t else 0 문서 스캔, 마스크
감마 보정 255 * (v/255) ** g 어두운 영역 강조

히스토그램

밝기 값의 분포다. 어두운 사진은 히스토그램이 왼쪽에 몰리고, 대비가 낮은 사진은 가운데 좁게 몰린다. 히스토그램 평활화(equalization)는 누적 분포를 써서 값을 고르게 펴 대비를 높인다. 이진화 임계값을 자동으로 고르는 오츠(Otsu) 방법도 히스토그램으로 계산한다. 두 집단(배경과 물체)의 클래스 내 분산이 최소가 되는 임계값을 찾는다.

이웃 연산과 합성곱

픽셀 주변의 작은 창(예: 3x3)을 보고 새 값을 정한다. 창에 곱할 가중치 행렬을 커널(kernel) 또는 필터라 한다.

출력[y, x] = Σ Σ 커널[i, j] * 입력[y - i, x - j]       (합성곱)
           i j

엄밀한 합성곱은 커널을 뒤집어 적용한다. 뒤집지 않으면 상관(correlation)이다. 대칭 커널에서는 둘이 같고, 딥러닝 프레임워크의 “convolution” 층은 실제로는 상관을 계산하는 경우가 많다. 커널을 학습하므로 차이가 의미 없기 때문이다.

대표 커널은 다음과 같다.

평균(블러)           가우시안 근사          소벨 x (세로 경계)
1/9 * [1 1 1]       1/16 * [1 2 1]        [-1 0 1]
      [1 1 1]              [2 4 2]        [-2 0 2]
      [1 1 1]              [1 2 1]        [-1 0 1]
  • 블러: 이웃을 평균내 잡음을 줄인다. 대신 경계도 흐려진다.
  • 소벨: 좌우(또는 상하) 밝기 차이를 계산한다. 값이 크면 경계다. x 방향과 y 방향 결과를 sqrt(gx² + gy²) 로 합치면 경계의 세기가 된다.
  • 샤프닝: 원본에서 블러를 빼 경계 성분을 얻고, 그것을 원본에 더한다.

가장자리 처리

이미지 끝 픽셀은 이웃이 부족하다. 0 으로 채우기, 가장자리 값 복제, 거울 반사 등 패딩 방식을 정해야 한다. 0 패딩은 테두리를 어둡게 만들어 가짜 경계를 만든다.

기하 변환과 보간

회전·확대는 출력 픽셀마다 “입력의 어디서 왔는가”를 거꾸로 계산한다(역방향 매핑). 그 위치가 정수가 아니면 보간한다. 최근접, 쌍선형, 쌍삼차가 대표적이다. 정방향으로 입력 픽셀을 출력에 뿌리면 구멍이 생기기 때문이다.

직접 해 보기

잡음 섞인 사각형 이미지를 만들고 블러 → 이진화 → 소벨 경계 검출을 해 본다. NumPy 만 쓴다. 합성곱은 이해를 위해 이중 루프로 직접 짰다.

import numpy as np

rng = np.random.default_rng(0)
img = np.full((12, 12), 30.0)          # 어두운 배경
img[3:9, 4:10] = 200.0                 # 밝은 사각형
img += rng.normal(0, 15, img.shape)    # 잡음
img = img.clip(0, 255)

def convolve(img, k):
    kh, kw = k.shape
    p = kh // 2
    padded = np.pad(img, p, mode="edge")   # 가장자리는 복제해서 채운다
    out = np.zeros_like(img)
    kf = k[::-1, ::-1]                     # 합성곱은 커널을 뒤집는다
    for y in range(img.shape[0]):
        for x in range(img.shape[1]):
            out[y, x] = (padded[y:y + kh, x:x + kw] * kf).sum()
    return out

box = np.ones((3, 3)) / 9
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=float)
sobel_y = sobel_x.T

blur = convolve(img, box)
mag = np.hypot(convolve(blur, sobel_x), convolve(blur, sobel_y))

print("원본 표준편차(배경 영역):", round(img[0:3, 0:3].std(), 1))
print("블러 후 표준편차(배경 영역):", round(blur[0:3, 0:3].std(), 1))

def show(a, thr):
    for row in a:
        print("".join("#" if v > thr else "." for v in row))

show(blur, 115)
print()
show(mag, 300)

실행 결과다. 두 그림은 보기 쉽게 나란히 옮기고 제목을 붙였다.

원본 표준편차(배경 영역): 12.9
블러 후 표준편차(배경 영역): 1.6

임계값 115 이진화         소벨 경계 크기 > 300
............              ............
............              ............
............              ....######..
.....####...              ...########.
....######..              ...###..###.
....######..              ...##....##.
....######..              ...##....##.
....######..              ...###..###.
.....####...              ...########.
............              ....######..
............              ............
............              ............

블러가 배경 잡음의 표준편차를 크게 줄였다. 이진화한 사각형은 모서리가 깎였다. 블러가 경계를 흐렸기 때문이다. 소벨 결과는 사각형 안쪽이 비고 테두리만 남았다. 밝기가 일정한 내부는 미분값이 0 에 가깝고, 경계에서만 크다.

실무에서는 이중 루프 대신 OpenCV 나 scikit-image 의 최적화된 함수를 쓴다. 결과는 같고 속도는 수십 배 이상 빠르다.

현업에서는

  • 이미지 업로드 서비스는 리사이즈·포맷 변환·EXIF 회전 보정을 거의 항상 한다. EXIF 방향 값을 무시하면 스마트폰 세로 사진이 옆으로 누운 채 저장된다. 또 EXIF 에 위치 정보가 들어 있을 수 있어 공개 전에 메타데이터를 지우는 처리가 필요하다.
  • 비전 모델 학습 파이프라인은 정규화(평균을 빼고 표준편차로 나눔), 무작위 자르기·뒤집기 같은 증강을 한다. 학습 때와 서비스 때 전처리가 다르면 정확도가 소리 없이 떨어진다. 같은 코드를 공유하는 것이 안전하다.
  • 이미지 처리는 CPU·메모리를 많이 쓴다. 홈랩 k3s 같은 작은 클러스터에서 썸네일 작업자를 돌릴 때는 파드에 메모리 상한을 걸고, 큰 이미지를 한꺼번에 여러 장 디코딩하지 않도록 동시 처리 수를 제한한다. 압축된 파일 크기가 작아도 디코딩하면 W x H x C 바이트가 된다.

확인 문제

  1. uint8 배열 두 개를 그대로 더하면 어떤 문제가 생기는가?
  2. 평균 필터를 적용하면 잡음은 줄지만 생기는 부작용은?
  3. 소벨 필터 결과에서 밝기가 일정한 영역의 값이 0 에 가까운 이유는?
  4. 회전 변환을 구현할 때 역방향 매핑을 쓰는 이유는?
  5. CNN 의 합성곱 층과 고전 소벨 필터의 가장 큰 차이는?

풀이

  1. 255 를 넘는 값이 256 으로 나눈 나머지로 넘쳐(wrap around) 밝은 곳이 어두워진다. 실수형으로 계산하고 잘라야 한다.
  2. 경계도 함께 흐려진다. 세밀한 구조가 사라진다.
  3. 소벨은 이웃 간 밝기 차이(미분 근사)를 계산하므로 변화가 없으면 0 이다.
  4. 정방향으로 입력 픽셀을 출력에 옮기면 출력 일부 픽셀에 아무 값도 안 떨어져 구멍이 생긴다. 출력 픽셀마다 입력 위치를 계산하고 보간하면 모든 픽셀이 채워진다.
  5. 소벨은 사람이 정한 고정 가중치이고, CNN 은 가중치를 데이터에서 학습한다.

더 읽을거리 (References)