OpenCV

인화·2026년 6월 17일

영상처리

목록 보기
3/9

1. OpenCV 기본 함수

  • cv2.imread(filename, flages) : 이미지를 읽어오는 함수
    • flags : 색상 모드 지정 (ex) cv2.IMREAD_COLOR - 컬러 이미지로 읽기, cv2.IMREAD_GRAYSCALE - 그레이스케일로 읽기, cv2.IMREAD_UNCHANGED - 알파 채널까지 그대로 읽
  • cv2.imshow(winname, mat) : 이미지를 화면에 보여주는 함수
    • winname (str) : 이미지를 표시할 윈도우 창의 제목
    • mat (numpy.ndarray) : 표시할 이미지 데이터 (보통 imread로 불러온 행렬)
# 이미지를 컬러로 읽기 (file_name, flags)
img_color = cv2.imread(file_name, cv2.IMREAD_COLOR)

# 읽어온 이미지를 Colab 노트북 창에서 띄우기
cv2_imshow(img_color)
  • cv2.imwrite(filename, img) : 이미지를 파일로 저장하는 함수
    • filename : 저장될 파일 경로
    • img : 저장할 이미지(Mat) 객체
cv2.imwrite("saved_image.jpg", img_color)
print("이미지가 저장되었습니다 : saved_image.jpg")
  • cv2.cvtColor(src, code) : 이미지의 색상 공간을 변환하는 함수
    • src : 변환할 원본 이미지
    • code : 변환 방법 코드 (ex) cv2.COLOR_BGR2GRAY, cv2.COLOR_BGR2HSV
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
  • cv2.resize(src, dsize) : 이미지 크기를 변경하는 함수
    • src : 리사이즈할 원본 이미지
    • dsize : (가로, 세로) 순서로 새로운 이미지 크기
    • fx, fy : 비율을 지정해 리사이즈 가능
      • 리사이즈 알고리즘(interpolation)은 기본적으로 cv2.INTER_LINEAR가 적용됨.
# 이미지 크기 변경 (50% 축소)
h, w = img_color.shape[:2]
img_resized = cv2.resize(img_color, (w // 2, h // 2))

# 크기 변경된 이미지 출력
cv2_imshow(img_resized)

# 저장
cv2.imwrite("resized_image.jpg", img_resized)

2. 색상 공간

OpenCV는 기본적으로 이미지를 BGR(Blue, Green, Red) 순서로 읽으나, 필요에 따라 다른 색상 공간으로 변환할 수 있다.

HSV

HSI는 Hue-channel, Saturation-channel, Intensity-channel로 구성된다. 여기서는 색상, 채도, 명도라는 3가지 특성들이 색을 설명하는 데 사용되며, 어떤 컬러 모델을 만들어 낼 때 얼마나 색을 사용했는지 알 필요 없이 단순히 채도와 명도를 통해 색을 조절한다.

  • Intensity (Brightness) : 어떤 색상의 밝기 값을 의미하며, 색상 정보는 포함되어 있지 않음.

  • Hue : 색조 (우세한 파장이 무엇인지에 관한 것)S의 파장이 크다면 clear color

    • I의 파장이 크다면 bright color
  • Saturation : 색상의 상대적인 순도 혹은 백색이 섞인 양. 순도가 높을수록(색이 선명할 수록) 이 값이 커진다.S가 크면 선명한 색상, I가 크면 밝기가 밝은 색상이다.

    HSI 모델에서 Hue는 원의 각도, Saturation은 원의 bound, Intensity는 높이를 통해 알 수 있다.

이러한 HSV를 사용하는 이유는 이미지에서 색상 정보를 검출하고자 할 때, Hue를 사용하면 되므로 RGB보다 분류가 더 쉽기 때문이다. 아래의 예시를 보면 Hue를 통해 특정 범위의 색상만 마스킹해 추출한다.

이렇게 색상을 추출할 땐 다음과 같은 주의 사항이 있다.

  1. 실제 조명 환경이나 이미지 색상에 따라 HSV 범위를 세밀하게 조절해야 한다.
  2. 보통 Hue(색상)의 범위를 설정하고 Saturation(채도) 및 (명도) 범위를 적절히 좁히거나 넓혀 원하는 결과를 얻는다.
  3. 단순히 한 구간(Hue)만으로는 충분하지 않을 수 있으므로 구간을 모두 고려하는 식으로 범위를 늘리는 경우도 있음.
# 특정 색상 범위를 HSV로 정의해둔 예시
# 각 색은 HSV에서의 범위를 lower(최소값), upper(최대값)로 저장.
color_ranges = {
    'red':   {'lower': (0, 100, 100),   'upper': (10, 255, 255)},   # 빨간색 범위
    'green': {'lower': (40, 100, 100),  'upper': (70, 255, 255)},   # 초록색 범위
    'blue':  {'lower': (100, 100, 100), 'upper': (130, 255, 255)}   # 파란색 범위
}

# 사용자로부터 색상 입력 받기
user_color = 'red'

# HSV 이미지 준비
img_hsv = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV)

# 색상 범위 가져오기
lower_val = color_ranges[user_color]['lower']
upper_val = color_ranges[user_color]['upper']

# inRange 함수를 사용해 특정 범위 색상만 추출 (마스크 생성)
mask = cv2.inRange(img_hsv, lower_val, upper_val)

# 마스크를 이용해 원본 이미지에서 특정 색상 영역만 추출
# bitwise_and 연산으로 마스크가 1인 영역만 남김
img_masked = cv2.bitwise_and(img_color, img_color, mask=mask)

# 결과 출력
print(f"선택된 색상 : {user_color}")
cv2_imshow(mask)
cv2_imshow(img_masked)

OpenCV와 matplotlib 사이에서 색이 달라 보이는 이유

  • 채널 순서가 다르기 때문임.
  • OpenCV는 이미지를 기본적으로 BGR 순서로 읽어오지만, matplotlib은 이미지를 RGB 순서로 해석하여 OpenCV에서 읽은 이미지를 그대로 matplotlib에 보여주면, matplotlib는 BGR 데이터를 RGB로 해석하여 다른 결과가 나온다.

HSV가 특정 색상 추출에 왜 유리한지

  • HSV는 색상(Hue), 채도(Saturation), 밝기(Value)를 분리해서 표현한다. 따라서 원하는 색상은 Hue 범위로 쉽게 지정할 수 있고, 밝기 변화는 Value로 분리되어 RGB보다 특정 색상 추출이 더 쉽고 직관적이다.
  • H (Hue) : 무슨 색인가
  • S (Saturation) : 얼마나 선명한가
  • V (Value) : 얼마나 밝은가

3. 이미지 연산

기본 함수

  • image.shape : (Height, Width, Channels)
  • image.size : 전체 픽셀 수 (Height × Width × Channels)
  • image[y, x] : (y, x) 위치의 픽셀(BGR) 정보를 배열 형태로 얻음.
    • OpenCV는 BGR 순서를 기본으로 하므로 image[y, x][0] = Blue, image[y, x][1] = Green, image[y, x][2] = Red
# Colab에 이미지 업로드
uploaded = files.upload()

# 업로드된 첫 번째 이미지 파일 이름 가져오기
filename = list(uploaded.keys())[0]

# 이미지를 BGR 컬러로 읽기
image = cv2.imread(filename, cv2.IMREAD_COLOR)
print("이미지 로드 완료! 파일명 : ", filename)

# 이미지 기본 정보 확인
print("이미지 크기 (Height, Width, Channels) : ", image.shape)
print("이미지 전체 픽셀 수 : ", image.size)

# 특정 픽셀의 BGR 값 확인
y, x = 100, 100 # 예 : (100, 100) 위치
if y < image.shape[0] and x < image.shape[1]:
  px = image[y, x]
  print(f"({y}, {x}) 위치 픽셀의 BGR 값 :", px)
  print(f"({y}, {x}) 위치 픽셀의 B 값 :", px[0])
  print(f"({y}, {x}) 위치 픽셀의 G 값 :", px[1])
  print(f"({y}, {x}) 위치 픽셀의 R 값 :", px[2])
else:
  print(f"이미지 크기가 100*100보다 작아서 ({y}, {x}) 픽셀이 존재하지 않습니다.")
  • 코드 실행 결과

특정 범위 픽셀 변경 - 반복문 VS 슬라이싱

OpenCV 이미지 객체는 numpy 배열과 호환되므로, 2차원(또는 3차원) 슬라이싱을 통해 손쉽게 특정 영역을 변경할 수 있다. 이때, 특정 영역을 변경하는 방법에는 반복문을 사용해 픽셀을 하나씩 변경하는 방법슬라이싱을 통해 한 번에 처리하는 방법이 있다.

  • 반복문 방식 : for i in range(...): for j in range(...) ...각 픽셀에 직접 접근해 값을 대입하는 방식으로, 반복문을 통해 하나의 픽셀씩 변경하므로 이미지 크기가 커질수록 상대적으로 속도가 느려진다는 단점이 있다.
  • 슬라이싱 방식 : image[y1:y2, x1:x2] = [B, G, R]
    • 범위를 지정해 한 번에 값을 대입하는 방식으로, 반복문을 사용하지 않아 빠르게 동작한다는 장점이 있음.
# 이미지 복사본 생성
image_copy = image.copy()

# (1) 반복문을 이용한 특정 범위 변경
start_time = time.time()
rows = min(100, image_copy.shape[0]) # 이미지가 100보다 작을 수 있으므로
cols = min(100, image_copy.shape[1])

for i in range(rows):
  for j in range(cols):
    image_copy[i, j] = [255, 255, 255] # 흰색으로 변경
end_time = time.time()
print("-- 반복문 방식 수행 시간 : %.6f seconds --" % (end_time - start_time))

# (2) 슬라이싱을 이용한 특정 범위 변경
image_copy2 = image.copy()
start_time = time.time()
image_copy2[0:rows, 0:cols] = [0, 0, 0] # 검정색으로 변경
end_time = time.time()
print("-- 슬라이싱 방식 수행 시간 : %.6f seconds --" % (end_time - start_time))

# 결과 이미지 보기
print("\n[반복문 방식] 상단 100*100 영역을 흰색으로 변경")
cv2_imshow(image_copy)

print("\n[슬라이싱 방식] 상단 100*100 영역을 검정색으로 변경")
cv2_imshow(image_copy2)

ROI(Region of Interest) 추출 및 복사

  • 이미지에서 특정 영역(ROI)을 잘라내어(Numpy 슬라이싱 활용) 다른 위치에 복사하는 것
  • 원하는 위치의 일부 영역에만 합성하기 위해 ROI 합성을 활용함. 방법
    1. roi = image[y1:y2, x1:x2] : ROI 영역 추출
    2. image_copy[새로운 위치] = roi : 추출된 영역을 다른 위치에 복사
  • 워터마크, 로고, 관심 영역 분석 등에 유용하게 활용
  • 이때, 마스크를 활용하면 원하는 부분만을 처리할 수 있고, 배경을 제거할 수 있고, 정밀한 합성을 할 수 있다는 장점이 있다. 방법
    1. 원본 영상에서 로고를 넣을 위치 설정 (ROI)
    2. 로고 영상과 크기를 맞춤
    3. 로고에서 Mask 생성
    4. ROI 배경에서 로고가 들어갈 부분을 제거
    5. Mask를 통해 로고 영역만 추출
    6. ROI와 로고를 합성 (비워둔 ROI 부분 로고 부분 결합)
    7. 합성 결과를 원본 영상에 다시 삽입
# ROI 추출 예시 : (y : 20~150, x : 70~200)
image_copy3 = image.copy()

# 안전하게 min() 함수를 사용하여 이미지 범위 내에서만 추출
roi = image_copy3[20:min(150, image_copy3.shape[0]), 70:min(200, image_copy3.shape[1])]

# 잘라낸 ROI 확인
print("[ROI] 20:150, 70:200 영역")
cv2_imshow(roi)

# ROI를 다른 위치(0:130, 0:130)에 복사
h_roi, w_roi = roi.shape[:2]
end_y = min(h_roi, image_copy3.shape[0])
end_x = min(w_roi, image_copy3.shape[1])
image_copy3[0:end_y, 0:end_x] = roi

print("[Modified image] ROI를 좌측 상단(0:130, 0, 130)에 복사")
cv2_imshow(image_copy3)

픽셀별로 다루기

OpenCV 이미지(3채널)는 BGR 순서로 채널을 저장하며, 특정 채널만 확인하거나 특정 채널만 제거할 수도 있다.

  • image[:, :, 0] : Blue 채널
  • image[:, :, 1] : Green 채널
  • image[:, :, 2] : Red 채널
# (1) Blue 채널만 확인
blue_channel = image[:, :, 0]
print("[Blue Channel]")
cv2_imshow(blue_channel)

# (2) 특정 색상 채널 제거 (예 : Red 채널을 0으로)
image_no_red = image.copy()
image_no_red[:, :, 2] = 0 # Red 채널 제거
print("[Red 채널 제거]")
cv2_imshow(image_no_red)

4. 이미지 변환

이미지 크기 조절 (Resizing)

이미지의 크기를 변경(확대/축소)할 때에는 cv2.resize() 함수를 사용함.

resized = cv2.resize(
    src,                # 원본 이미지
    dsize,             # (width, height) - 직접 지정하거나 None으로 둘 수 있음
    fx=1.0,            # 가로 방향 배율
    fy=1.0,            # 세로 방향 배율
    interpolation=cv2.INTER_LINEAR  # 보간법(기본값: INTER_LINEAR)
)

보간법

이미지를 확대/축소할 때 실제 존재하지 않는 픽셀 사이의 값을 채우거나 제거해야 하는데, 이 픽셀을 어떻게 계산해서 값을 구할지 결정하는 방법을 보간법(Interpolation)이라고 한다.

  • cv2.INTER_NEAREST : 최근접 이웃 보간법 (Nearest Neighbor)

    • 새 픽셀 위치가 원본에서 어떤 위치에 대응될 때, 그 주변을 계산하지 않고 가장 가까운 한 점의 값을 활용하는
    • 가장 단순하고 빠르지만 이미지 품질이 거칠 수 있고, 계단 현상이 나타날 수 있음.
    • 계단 현상 : 대각선이나 곡선 가장자리가 계단이나 톱니모양으로 보이는 현상
  • cv2.INTER_LINEAR : 양선형 보간법 (Bilinear Interpolation)

    • 새 픽셀 주변 2*2 이웃 픽셀을 참고해 계산하는 것 (x축, y축 각각 방향으로 선형 보간)
    • 확대, 축소 모두 적절한 품질과 속도 제공 / 계산량이 크지 않음
  • cv2.INTER_CUBIC : 4*4 이웃 픽셀을 고려하는 삼차 보간법 (Bicubic Interpolation)

    • 확대 시 매우 높은 품질을 제공하나, 계산 비용이 큼.
  • cv2.INTER_AREA : 영역 보간법

    • 주로 축소 시 좋은 결과를 냄. (평균화 과정)
    • 줄어드는 영역 안에 포함되는 원본 픽셀들의 정보를 평균적으로 반영함. → 다시 말해, 축소 시 여러 원본 픽셀이 1개의 새 픽셀로 합쳐지는데, 그 영역의 정보를 대표값으로 요약하는 방식으로 보면 됨. (새 픽셀이 원본에서 차지하는 영역 안의 정보들을 평균 냄) → 단순히 픽셀 하나만 고르면 어떤 패턴은 사라지고 어떤 줄무늬는 이상하게 남는 등 aliasing 문제가 발생하는데, 영역 보간법은 여러 픽셀을 평균적으로 반영해 이러한 문제를 줄이는 데 유리하다.
  • cv2.INTER_LANCZOS4 : Lanczos 보간법

    • 8*8 이웃 픽셀을 고려해 고품질 확대/축소 구현

    가장 단순한 방법은 Nearest로, 이웃 픽셀 정보를 이용하기에 단순하지만 이미지의 연속성이 소실될 수 있다. 양선형 보간법인 LINEAR는 일반적으로 좋은 성능을 내기에 기본값으로 사용되고, 큐빅(삼차 보간법)은 4*4 이웃 픽셀을 고려하므로 확대 시 성능이 가장 좋다. AREA는 영역 보간법으로 주로 축소 시에 좋은 결과를 내고, Lanczos는 고품질에 유용하다. (확대 = CUBIC, 축소 = AREA)

이미지 위치 변경 (Translation)

  • 이미지를 평면 상에서 옮기는(이동시키는) 방법으로, 영상의 모든 픽셀을 동일한 거리만큼 x축, y축 방향으로 이동시키는 변환임. (영상의 모양이나 방향은 그대로이고 위치만 바뀜)
  • 이를 위해 이동 변환 행렬(Translation Matrix)을 만들어야 함.
  • cv2.warpAffine(src, M, dsize)
    • tx : x축으로 이동하는 픽셀 수 (양수 → 오른쪽 이동, 음수 → 왼쪽 이동)
    • ty : y축으로 이동하는 픽셀 수 (양수 → 아래쪽 이동, 음수 → 위쪽 이동)
    • dsize : 결과 이미지의 크기. 일반적으로 원본과 같은 크기를 지정합니다.
M = np.float32([
    [1, 0, tx],  # x 방향 이동
    [0, 1, ty]   # y 방향 이동
])
dst = cv2.warpAffine(
    src=image,
    M=M,               # 변환 행렬
    dsize=(width, height)  # 결과 이미지 크기
)

이미지 회전 (Rotation)

  • 이미지를 특정 각도로 회전시키는 방법
  • 이미지 위치 변경과 마찬가지로 cv2.warpAffine() 함수를 사용하지만, cv2.getRotationMatrix2D()를 이용해 회전 행렬을 먼저 구해야 함.
  • cv2.getRotationMatrix2D(center, angle, scale)
    • center : (cx, cy) 형태의 회전 중심점 좌표
    • angle : 회전 각도 (+일 때 반시계 방향)
    • scale : 회전하면서 이미지 크기를 확대/축소하는 비율 (1.0 → 원본)
M = cv2.getRotationMatrix2D(
    center,   # 회전 중심 (tuple)
    angle,    # 회전 각도 (도 단위, +값 = 반시계방향)
    scale     # 크기 비율 (1.0이면 원본 크기 유지)
)
rotated = cv2.warpAffine(image, M, (width, height))
# 예시 코드
height, width = image.shape[:2] # 이미지의 높이, 너비

# 회전 중심 (이미지의 중앙)
center = (width / 2, height / 2)

# 90도 회전 + 0.5배 스케일 (반시계방향)
M = cv2.getRotationMatrix2D(
    center=center,
    angle=90,
    scale=0.5
)

# 회전 적용
rotated = cv2.warpAffine(
    image, # 원본 이미지
    M, # 회전 행렬
    (width, height) # 출력 이미지 크기
)

print("[이미지 회전 : 90도 스케일 0.5]")
cv2_imshow(rotated)

5. 산술 연산과 합성

cv2.add() vs Numpy +

  • 이미지 데이터는 보통 uint8이므로 값의 범위는 주로 0~255이다.
  • cv2.add() : 포화 연산(saturation arithmetic)을 사용하므로 255를 넘으면 더이상 커지지 않고 값이 255로 고정되는 특성이 있음. (ex) 250 + 10 = 255 → 영상 픽셀의 범위가 uint8 일 때, 0 ≤ pixel ≤ 255를 유지하도록 함.
  • Numpy + : uint8 배열끼리 + 를 하면 overflow가 발생할 수 있고, uint8에서 overflow가 나면 wrap-around가 발생할 수 있음.
    • wrap-around : 값이 표현 가능한 범위를 넘었을 때, 끝에서 다시 처음으로 돌아가는 것처럼 보이는 현상

Saturation과 Overflow의 차이

  • Saturation : 값이 허용 범위를 넘으면 최댓값 또는 최솟값에 고정하는 방식
  • Overflow : 값이 범위를 넘으면 순환하면서 값이 바뀌는 현상, 즉 경계를 넘으면 다시 돌아감. → 255 다음은 다시 0부터 시작하는 것처럼 동작함. (ex) 255 + 1 = 0, 250 + 10 = 4

Alpha Blending

Alpha blending은 두 영상을 일정 비율로 섞는 방법이다.

  • alpha가 크면 첫 번째 영상이 더 많이 반영되고, alpha가 작으면 두 번째 영상이 더 많이 반영됨.
  • blended = cv2.addWeighted(src1, alpha, src2, beta, gamma)
    • alpha : 첫 번째 이미지의 가중치
      • 각 영상이 최종 결과에 얼마나 기여하는지 결정하는 가중치
    • beta : 두 번째 이미지의 가중치
    • gamma : 전체 결과에 추가되는 상수 값
      • blending 결과 전체에 일정 값을 더해 결과를 더 밝게 만들 수 있음.
  • 보통 alpha + beta = 1로 두면 자연스러운 혼합이 됨.
dst=αimg1+βimg2+γdst = \alpha⋅img1+β⋅img2+γ

Mask의 의미

  • 어디를 선택할지 를 결정하는 지도(map)
  • 영상에서 어떤 부분만 선택적으로 처리할지 결정하는 필터 역할을 하는 배열
    • 흰색 (255) : 처리할 영역
    • 검은색 (0) : 처리하지 않을 영역
  • 특정 영역만 추출 / 배경과 객체 분리 / 부분 합성 / 로고 삽입, 워터마크, 세그멘테이션 결과 시각화 등에 활용됨.
h, w = image_1_aligned.shape[:2]
mask_circle = np.zeros((h, w), dtype=np.uint8)
center = (w // 2, h // 2)
radius = min(h, w) // 4
cv2.circle(mask_circle, center, radius, 255, -1) # 마스크에 흰색 원 그리기

# 원 안쪽 = 255, 바깥 = 0
mask_circle_inv = cv2.bitwise_not(mask_circle) # 마스크 반전
# img1에서 원 안쪽만 추출 (전경)
fg_circle = cv2.bitwise_and(image_1_aligned, image_1_aligned, mask=mask_circle)
# img2에서 원 바깥쪽만 추출 (배경)
bg_circle = cv2.bitwise_and(image_2_aligned, image_2_aligned, mask=mask_circle_inv)
combined_circle = cv2.add(fg_circle, bg_circle) # 최종 합성

show_images_grid([
    ("원형 마스크", mask_circle),
    ("mask inverse", mask_circle_inv),
    ("image_1의 선택 영역", fg_circle),
    ("image_2의 배경 영역", bg_circle),
    ("최종 합성", combined_circle),
], cols=3, figsize=(15, 9))

6. 임계점(Threshold) 처리

기본 이진화 (Global Thresholding)

ret, dst = cv2.threshold(
    src,            # 입력 그레이스케일 이미지 (single-channel 이미지)
    thresh,         # 임계값
    maxval,         # 임계값 이상일 경우 적용할 값
    type            # Threshold 타입, ex) cv2.THRESH_BINARY
)

주요 Threshold 타입

  • cv2.THRESH_BINARY : 픽셀값 ≥ 임계값 → maxval, 픽셀값 < 임계값 → 0
  • cv2.THRESH_BINARY_INV : THRESH_BINARY의 반전
  • cv2.THRESH_TRUNC : 픽셀값 > 임계값 → 임계값, 픽셀값 ≤ 임계값 → 픽셀값 그대로
  • cv2.THRESH_TOZERO : 픽셀값 ≥ 임계값 → 픽셀값 그대로, 픽셀값 < 임계값 → 0
  • cv2.THRESH_TOZERO_INV : THRESH_TOZERO의 반대
if gray_image is not None:
  # 임계값 설정
  threshold_value = 127
  max_value = 255

  # (1) THRESH_BINARY - 픽셀 값이 127보다 크면 255, 작으면 0
  # 픽셀값 >= 임계값 -> maxval, 픽셀값 < 임계값 -> 0
  ret, thres_binary = cv2.threshold(gray_image, threshold_value, max_value, cv2.THRESH_BINARY)
  print('[THRESH_BINARY]')
  cv2_imshow(thres_binary)

  # (2) THRESH_BINARY_INV - 픽셀 값이 127보다 크면 0, 작으면 255
  # THRESH_BINARY 반전
  ret, thres_binary_inv = cv2.threshold(gray_image, threshold_value, max_value, cv2.THRESH_BINARY_INV)
  print('[THRESH_BINARY_INV]')
  cv2_imshow(thres_binary_inv)

  # (3) THRESH_TRUNC - 픽셀 값이 127보다 크면 127, 이하면 원래 값 유지
  # 픽셀값 > 임계값 -> 임계값, 픽셀값 <= 임계값 -> 픽셀값 그대로
  ret, thres_trunc = cv2.threshold(gray_image, threshold_value, max_value, cv2.THRESH_TRUNC)
  print('[THRESH_TRUNC]')
  cv2_imshow(thres_trunc)

  # (4) THRESH_TOZERO - 픽셀 값이 127보다 크면 원래 값 유지, 이하면 0
  # 픽셀값 >= 임계값 -> 픽셀값 그대로, 픽셀값 < 임계값 -> 0
  ret, thres_tozero = cv2.threshold(gray_image, threshold_value, max_value, cv2.THRESH_TOZERO)
  print('[THRESH_ZERO]')
  cv2_imshow(thres_tozero)

  # (5) THRESH_TOZERO_INV - 픽셀 값이 127보다 크면 0, 이하면 원래 값 유지
  # THRESH_TOZERO의 반전
  ret, thres_tozero_inv = cv2.threshold(gray_image, threshold_value, max_value, cv2.THRESH_TOZERO_INV)
  print('[THRESH_TOZERO_INV]')
  cv2_imshow(thres_tozero_inv)
else:
  print("gray_image가 None입니다. 확인 후 다시 시도하세요.")

적응형 임계점(Adaptive Threshold)

글씨나 조명이 고르지 않은 이미지에서는 글로벌한 임계값(고정된 임계값)만으로는 좋은 결과를 얻기 어렵다. 글로벌 임계값은 영상 전체에 하나의 고정된 threshold를 적용한다. 그러나 글씨나 문서 이미지에서 조명이 고르지 않으면 배경과 글씨의 밝기값이 위치에 따라 달라진다. 이 경우 어떤 영역에서는 배경과 글씨가 잘 구분되지만, 다른 영역에서는 배경이 지나치게 어둡거나 글씨가 흐려져 동일한 threshold로는 정확한 분리가 어렵다. 따라서 조명 변화가 큰 영상에서는 글로벌 threshold보다 영역별로 threshold를 다르게 적용하는 adaptive threshold가 더 적절하다. 적응형 임계점 처리를 활용하면 이미지를 작은 영역(블록)으로 나누어 각 영역에서 동적으로 임계값을 계산한다.

  • src : 입력 영상. 그레이스케일 영상
  • maxValue : 임계값 함수 최댓값. 보통 255.
  • adaptiveMethod : 블록 평균 계산 방법 지정. cv2.ADAPTIVE_THRESH_MEAN_C는 산술 평균, cv2.ADAPTIVE_THRESH_GAUSSIAN_C는 가우시안 가중치 평균
  • thresholdType : cv2.THRESH_BINARY 또는 cv2.THRESH_BINARY_INV 지정
  • block_size : 주변 픽셀의 영역 크기 (반드시 3 이상의 홀수)
    • 셀 주변 영역의 크기를 설정
    • 임계값 이 클수록 더 넓은 영역을 고려하여 임계값을 결정하므로 크고 불규칙한 조명 변화에 더 잘 대처
    • 임계값이 작을수록 미세한 디테일을 표현하는 데 더 적합
  • C : 실제 계산된 임계값에서 감산할 값 / 블록 내 평균값 또는 블록 내 가중 평균값에서 뺄 값. (x, y) 픽셀의 임계값으로 𝑇(𝑥, 𝑦) = 𝜇(𝑥, 𝑦 )− 𝐶 를 사용
    • C 값이 클수록 임계값이 낮아져 더 많은 영역이 흰색(객체)으로 변환
    • 아래 예시에선 block_size가 커지면 검은색 영역이, C 값이 커지면 흰색 영역이 커짐.
    • 임계값 설정 결과를 세밀하게 조정
    • 일반적으로 양수 값이지만 0 또는 음수 값도 가능
    • MEAN_C : blockSize 내의 픽셀 값 평균을 이용해 임계값을 결정
    • GAUSSIAN_C : blockSize 내의 픽셀 값에 가우시안 가중치를 주어 평균을 계산
dst = cv2.adaptiveThreshold(
    src,                          # 그레이스케일 이미지
    maxValue,                     # 임계값 이상일 때 적용할 값
    adaptiveMethod,               # (cv2.ADAPTIVE_THRESH_MEAN_C or cv2.ADAPTIVE_THRESH_GAUSSIAN_C)
    thresholdType,                # 보통 THRESH_BINARY 또는 THRESH_BINARY_INV
    blockSize,                    # 주변 픽셀을 참고할 영역 크기 (홀수)
    C                             # 평균이나 가우시안으로 계산된 임계값에서 빼줄 상수
)

Otsu's Binarization

  • 오츄 알고리즘은 임계값을 사용자 지정 없이 자동으로 결정할 수 있도록 해준다.
  • 영상의 히스토그램을 보고 배경과 객체를 가장 잘 구분하는 임계값(threshold)자동으로 찾는 방법
  • 영상을 두 클래스(배경/객체)로 나눈다 생각하고, 이 둘이 가장 잘 분리되도록 하는 threshold를 찾음.
  • 즉, 같은 그룹 안에서는 픽셀값이 비슷하고 서로 다른 그룹끼리는 픽셀값 차이가 크게 되도록 임계값을 골라 배경과 객체를 가장 깔끔하게 나눌 수 있는 기준값을 찾음.
  • 다시 말해, 클래스 내 분산(within-class variance)을 최소화하거나, 클래스 간 분산(between-class variance)을 최대화하는 임계값을 선택함.
  • 임계값 파라미터를 0으로 주고, cv2.THRESH_OTSU를 추가로 지정하면 자동으로 임계값을 결정한다.
  • 오츄 알고리즘 적용 전 이미지를 가우시안 블러로 미리 스무딩하면 더 나은 결과가 나올 수 있다.
ret, dst = cv2.threshold(
    src, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
  • Basic thresholding where you have to manually supply a threshold value, T
  • Otsu’s thresholding, which automatically determines the threshold value
  • 일반 threshold는 이미지 전체에 대해, adaptive threshold는 block 단위로 적용하므로 성능이 더 좋음. 다만, 후자는 연산량이 더 큼.

7. 도형 그리기

각 함수는 img 배열을 직접 수정합니다. 반환값을 다시 변수에 할당해도 되지만, img 자체가 변형됩니다.

직선 (Line) 그리기

  • cv2.line(img, start_point, end_point, color, thickness, lineType)
    • img : 도형을 그릴 대상 이미지 (numpy array)
    • start_point : 시작 좌표 (x, y)
    • end_point : 끝 좌표 (x, y)
    • color : BGR 튜플 (ex) (255, 0, 0)
    • thickness : 선 두께
    • lineType : 선의 형태 (ex. cv2.LINE_8, cv2.LINE_AA 등)
      • cv2.LINE_4는 4-연결성을 이용하여 선을 그리는 방식으로, 상하좌우 방향 중심으로 픽셀이 연결되어 비교적 거친 선이 나타날 수 있다. cv2.LINE_8은 대각선 방향까지 포함하는 8-연결성을 사용하므로 더 자연스러운 선 표현이 가능하다. cv2.LINE_AA는 안티앨리어싱을 적용하여 경계 픽셀의 밝기를 조절함으로써 계단 현상을 줄이고 가장 부드러운 선을 만든다.
# 직선 그리기 예제
image_line = np.full((512, 512, 3), 255, np.uint8)  # 흰색 배경
image_line = cv2.line(
    image_line,
    (0, 0),         # 시작 좌표
    (255, 255),     # 끝 좌표
    (255, 0, 0),    # 파란색 (BGR)
    3               # 두께
)
show_image("Line Drawing", image_line)

사각형 (Rectangle) 그리기

  • cv2.rectangle(img, pt1, pt2, color, thickness)
    • pt1 : 사각형의 한 꼭짓점 (x, y)
    • pt2 : 대각선 반대쪽 꼭짓점 (x, y)
# 사각형 그리기 예제
image_rect = np.full((512, 512, 3), 255, np.uint8)
image_rect = cv2.rectangle(
    image_rect,
    (20, 20),       # 왼쪽 상단 꼭짓점
    (255, 255),     # 오른쪽 하단 꼭짓점
    (255, 0, 0),    # 파란색
    3               # 선 두께
)
show_image("Rectangle Drawing", image_rect)

원 (Circle) 그리기

  • cv2.circle(img, center, radius, color, thickness)
    • center : 원의 중심 좌표 (x, y)
    • radius : 원의 반지름
# 원 그리기 예제
image_circle = np.full((512, 512, 3), 255, np.uint8)
image_circle = cv2.circle(
    image_circle,
    (256, 256),     # 중심
    80,             # 반지름
    (255, 0, 0),    # 파란색
    3               # 선 두께
)
show_image("Circle Drawing", image_circle)

다각형 (Polygon) 그리기

  • cv2.polylines(img, [points], isClosed, color, thickness)
    • points : (N, 1, 2) 형태의 정수 numpy array (다차원 배열)
    • isClosed : 닫힌 도형 여부 (True/False)
      • isClosed=True로 설정하면 마지막 점과 첫 번째 점이 이어짐.
# 다각형 그리기 예제
image_poly = np.full((512, 512, 3), 255, np.uint8)

# 점들을 정의 (x, y)
points = np.array([[50, 50], [150, 200], [300, 100], [400, 400]], np.int32)
points = points.reshape((-1, 1, 2))  # (N, 2) -> (N, 1, 2)
image_poly = cv2.polylines(
    image_poly,
    [points],      # 리스트 형태로
    True,          # isClosed=True → 닫힌 도형
    (0, 0, 255),   # 빨간색
    3
)

show_image("Polygon Drawing", image_poly)

타원 (Ellipse) 그리기

  • cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness)
    • center : (x, y)
    • axes : (major_axis_length/2, minor_axis_length/2) 즉 (가로축, 세로축)
    • angle : 타원의 회전 각도 (도 단위) / 0이면 수평.
    • startAngle , endAngle : 타원의 그릴 시작 각도와 끝 각도 / 0~360 전체 사용하면 완전한 타원
cv2.ellipse(
    img, center, axes,
    angle,       # 타원(회전각, 도 단위)
    startAngle,  # 시작 각도
    endAngle,    # 끝 각도
    color, thickness
)
# 타원 그리기 예제
image_ellipse = np.full((512, 512, 3), 255, np.uint8)

image_ellipse = cv2.ellipse(
    image_ellipse,
    (256, 256),             # 중심
    (100, 50),              # (가로축/2, 세로축/2)
    30,                     # 회전 각도
    0, 360,                 # 시작 각도, 끝 각도
    (0, 255, 0),            # 초록색
    3                       # 선 두께
)

show_image("Ellipse Drawing", image_ellipse)

텍스트 (Text) 추가

  • cv2.putText(img, text, org, fontFace, fontScale, color, thickness)
    • org : 텍스트 시작 좌표 (x, y)
    • fontFace : 글꼴 (예: cv2.FONT_HERSHEY_SIMPLEX)
    • fontScale : 글자 크기 (배율)
cv2.putText(
    img, text, org,
    fontFace, fontScale,
    color, thickness,
    lineType
)
# 텍스트 추가 예제
image_text = np.full((512, 512, 3), 255, np.uint8)

image_text = cv2.putText(
    image_text,
    'Hello World',         # 텍스트 내용
    (50, 250),             # 시작 위치 (x, y)
    cv2.FONT_HERSHEY_SIMPLEX,   # 폰트
    1,                      # 크기 (배율)
    (255, 0, 0),            # 파란색
    2                       # 두께
)

show_image("Text Drawing", image_text)

8. 애니메이션을 GIF로 저장하기

애니메이션 GIF 저장 방법

  1. 여러 프레임을 반복문 등을 통해 생성 후, frames 리스트에 저장
  2. imageio.mimsave() 함수를 사용하여 GIF 파일로 저장
    1. imageio.mimsave('output.gif', frames, fps=2)
      1. fps : 초당 프레임 수 (2 → 1초에 2프레임, 0.5초 간격)
      2. frames : RGB 형식의 numpy 배열로 구성
    2. (ex) imageio.mimsave('output.gif', frames, fps=2)
  3. GIF 재생 확인
# Colab에서 output.gif를 직접 표시
with open('output.gif','rb') as f:
    display(Image(data=f.read()))

9. Contours 처리

Contours

  • 영상에서 관심있는 객체(물체)의 경계선(외곽)을 따라서 연결된 점들의 집합
  • Contours를 찾는 것은 이미지에서 물체의 여러 특성을 계산 가능하기 때문에 중요함.
  • 예를 들어, 물체의 위치, 면적, 둘레 길이, 중심점, 모양 확인(분류 - 삼각형, 사각형 ...), 객체 개수 파악, Bounding box 등에 활용 가능함.

cv2.findContours()

  • images : 8비트 단일 채널(Grayscale). 일반적으로 Threshold나 Canny 에지 검출 등으로 전처리가 된 이진화 이미지
  • mode : 윤곽선을 찾는 방식 (어떤 윤곽선을 어떤 범위까지 찾을 것인가?)
    • cv2.RETR_EXTERNAL가장 바깥쪽 윤곽선만 검색 (도넛의 경우 바깥 테두리는 찾지만, 안쪽 테두리는 무시) - 보통 물체 개수 셀 때 이용함.
    • cv2.RETR_TREE모든 윤곽선을 검색하고 계층 구조를 형성 (바깥, 안쪽, 그 외 다른 경계를 모두 찾음) - 복잡한 형태 분석에 주로 사용함.
  • method : 윤곽선 근사화 방식 (찾은 윤곽선을 어떤 방식으로 얼마나 자세히 저장할 것인가?)
    • 모든 경계선을 다 저장할 수도 있고, 예를 들면 직선 구간의 경우 중간 점은 생략하고 시작/끝 점만 저장 가능
    • cv2.CHAIN_APPROX_NONE : 윤곽선 위에 있는 모든 픽셀들의 모든 좌표를 저장. 가장 자세히 표현, 요구 메모리/연산량 증가
    • cv2.CHAIN_APPROX_SIMPLE : 직선 구간은 시작점과 끝점만 저장함으로써 메모리 절약
  • contours : 윤곽선의 점들 좌표를 담고 있는 리스트
    • 리스트의 각 원소가 하나의 윤곽선 (이미지 안에 물체가 1개 있으면 contours도 1개, 여러 개이면 contours도 여러 개)
    • 각 contour는 다시 점 좌표들의 배열
  • hierarchy : 윤곽선의 계층 구조를 담고 있는 배열임.
    • 예를 들어, 도넛 모양의 경우 아래가 둘 다 존재함.
      • 바깥 테두리 contour (부모)
      • 안쪽 테두리 contour (자식)
    • hierarchy는 계층적 관계 표현
      • 이전/다음 contour index
      • 부모/첫 번째 자식 contour index
# 이진화된 이미지에서 경계선을 찾아서 contours를 반환 (Binarized image)
contours, hierarchy = cv2.findContours (
  image, # 입력 이미지 (이진화된 형태가 일반적)
  # 어떤 형태의 윤곽선을 찾을 건지
  mode,  # 윤곽선 검색 방법 (cv2.RETR_EXTERNAL, cv2.RETR_TREE 등)
  # 찾은 윤곽선을 어떻게 저장할 것인지
  method # 윤곽선 근사화 방법 (cv2.CHAIN_APPROX_SIMPLE 등)
)

cv2.drawContours()

output = cv2.drawContours(
  image, # 윤곽선을 그릴 대상 이미지 (BGR 컬러)
  contours, # 찾은 윤곽선 리스트
  contouridx, # -1이면 모든 윤곽선을 그림
  color, # BGR
  thickness # 선 두께
)

Contour와 Edge의 차이

  • Contour는 객체 중심, 외곽선을 모두 딴 것 (binary object에서 전체 boundary 경계선 따는 거)
    • 닫힌 형태 / 전처리가 안 된 경우 불완전하게 표현되기도 함.
    • 컬러 이미지 자체를 보면 색상에 따라서 판단 기준이 모호해지므로 Grayscale/Binary 이미지로 변환해서 명확히 봄.
  • Edge는 밝기 변화가 큰 지점을 찾는 것

사각형 외곽(Bounding Rectangle)

  • 윤곽선으로부터 가장 작은(Axis-Aligned) 사각형 영역을 구하는 것
  • Contour로부터 가장 fit한 사각형을 찾는 것
  • Bounding Rectangle은 contour를 감싸는 가장 작은 직사각형을 구하는데, 이 직사각형은 기울어질 수 없고, 항상 이미지의 x축, y축 방향에 맞춰진 상태여야 함.
  • 객체는 기울어져 있어도 Bounding Box는 기울어지지 않은 형태
  • 기울이지 않는 조건 하에서 contour를 전부 포함하는 최소 직사각형
+---------+
|   /---- |
|  / obj  |
| /----   |
+---------+

x, y, w, h = cv2.boundingRect(contour)
- (x, y)는 사각형의 왼쪽 위 꼭짓점 좌표
- (w, h)는 사각형 너비와 높이
- 직교된 사각형(Axis-Aligned Bounding Box)이므로 이미지 축에 평행함.

Convex Hull

  • 윤곽선의 점들을 모두 포함하는 블록 다각형(Convex Polygon) 중 가장 작은 것
  • 윤곽선(contour)에 있는 점들을 전부 감싸는 가장 바깥쪽의 볼록한 껍데기를 구하는 것
    • Contour: 실제 물체의 경계, 오목한 부분까지 그대로 포함
    • Convex Hull: 그 경계를 바깥에서 볼록하게 감싼 형태
  • 유클리드 공간에서 점들의 집합에 속하는 모든 점들을 연결하는 선분을 포함하는 집합
  • Contour를 모두 감싸는 작은 hull을 계산하는 것
hull = cv2.convexHull(contour)
cv2.drawContours(hull, [hull], -1, (0, 0, 255), 2)
  • hull은 점들의 집합을 반환하며, 이를 drawContours로 시각화할 수 있음.

유사 다각형 근사 (Polygon Approximation)

  • 더 적은 점으로 윤곽선 근사화 (윤곽선의 직선 구간이 많은 경우 점을 크게 줄여 효율적 표현 가능)
  • Contour를 다각형 형태로 근사
approx = cv2.approxPolyDP(contour, epsilon, closed)
  • epsilon : 근사 오차(정밀도) 파라미터. epsilon이 클수록 점이 더 많이 생략
  • closed : 윤곽선이 닫혀있는지 여부 (True/False)
# Approximate the contour to a polygon
epsilon = 0.02 * cv2.arcLength(contour, True) # 2% of the contour perimeter
approx = cv2.approxPolyDP(contour, epsilon, True)

print("Number of points in original contour : ", len(contour))
print("Number of points in approximated polygon : ", len(approx))

# Draw the approximated polygon in yellow
output_approx = output.copy()
cv2.drawContours(output_approx, [approx], -1, (0, 255, 255), 2)

show_image("Approximated Polygon", output_approx)

Contours 기본 정보

  • 윤곽선(Contour)에 대해 다양한 속성 계산 가능
    1. 면적(Area) : cv2.arcLength(contour)
    2. 둘레(Perimeter) : cv2.arcLength(contour, closed=True)
    3. 모멘트(Moments) : cv2.moments(contour)
      1. 모양의 기하학적 분포를 수치로 표현하는 값
      2. 질량 중심(centroid) 등을 구하는데 사용 가능
  • M = cv2.moments(contour)라 하면,
    • cx = moments['m10'] / moments['m00']
    • cy = moments['m01'] / moments['m00']
    • 이때 M10, M01이 의미하는 바는?
    • m10 : x 방향 공간 모멘트 / m01 : y 방향 공간 모멘트 (도형의 픽셀들이 어디에 얼마나 분포하는지)
    • cv2.moments(contour)에서 반환되는 M10과 M01은 공간 모멘트(Spatial Moments) 중 1차 모멘트들로, 윤곽선(contour)의 위치 특성을 나타내는 픽셀의 합임.
# Contour basic info
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
moments = cv2.moments(contour)
print("Contour Area:", area)
print("Contour Perimeter:", perimeter)
print("Moments:", moments)
# Calculate centroid using moments
if moments['m00'] != 0:
    cx = int(moments['m10'] / moments['m00'])
    cy = int(moments['m01'] / moments['m00'])
    print(f"Centroid: ({cx}, {cy})")
else:
    print("Contour area is zero, cannot compute centroid.")

minAreaRect

  • Contours에서 기울어진 사각형을 구하는 함수
  • BoundingRect은 축에 평행한 직사각형을 구하지만, minAreaRect은 회전된 직사각형 중 면적이 가장 작은 것을 찾음.
# rect 는 ( (cx, cy), (width, height), angle ) 형태로 반환
# cv2.boxPoints(rect) 를 이용해 사각형 4점의 좌표를 얻을 수 있음.
rect = cv2.minAreaRect(contour)
box = cv2.boxPoints(rect)
box = np.int0(box)
profile
얼렁뚱땅 바보 학부생...

0개의 댓글