내가 보려고 정리한 OpenCV 정리 with GPT

1. OpenCV 환경 확인과 전체 코드의 주제

OpenCV는 컴퓨터 비전 작업을 위한 라이브러리지만, 실제 코드의 시작점은 복잡한 모델이 아니라 입력과 출력이다. 이미지가 어떻게 메모리에 올라가는지, 화면에 무엇이 표시되는지, 키보드나 마우스 이벤트가 어떤 식으로 들어오는지, 비디오 프레임이 반복문 안에서 어떻게 순차적으로 처리되는지를 이해해야 이후의 전처리, 검출, 추론 코드도 자연스럽게 읽힌다.

가장 먼저 등장하는 코드는 OpenCV 버전을 출력하는 아주 짧은 예제다.

import cv2

print("OpenCV version:", cv2.__version__)
# 출력 예시
# OpenCV version: 4.x.x

이 코드는 단순해 보이지만 두 가지 의미가 있다.

첫째, cv2 모듈이 정상적으로 설치되었는지 확인한다.
둘째, 이후 사용하는 메서드들이 현재 환경에서 지원되는지 점검하는 기준이 된다.

주요 메서드와 변수의 역할

cv2.version

OpenCV 패키지의 버전 문자열을 반환한다.
입력은 없고, 출력은 문자열이다.

version = cv2.__version__
print(type(version))
# <class 'str'>

shape 변화는 없지만, 이후 코드 호환성을 점검하는 데 중요한 값이다. OpenCV 예제는 버전에 따라 세부 동작이나 상수 이름이 조금씩 달라질 수 있기 때문이다.

2. 이미지는 무엇인가

OpenCV에서 이미지는 객체처럼 보이지만 실제로는 NumPy 배열이다. 이 점을 정확히 이해해야 이후의 모든 연산이 자연스럽다. 픽셀 값을 바꾸는 일, 채널 순서를 바꾸는 일, 흑백과 컬러를 구분하는 일, 나아가 모델 입력 텐서로 넘기는 일도 결국 배열 변환 문제이기 때문이다.

왜 이것이 중요한가 하면, 이미지를 단순히 "사진"으로만 이해하면 읽기와 표시까지만 가능하고, "배열"로 이해해야 전처리와 분석이 가능해지기 때문이다. 예를 들어 특정 채널만 수정하거나, 일부 영역만 잘라내거나, 색상을 반전시키는 작업은 모두 배열 연산으로 수행된다.

먼저 같은 파일을 그레이스케일과 컬러로 각각 읽는다.

img_gray = cv2.imread('./images/dog.bmp', cv2.IMREAD_GRAYSCALE)
img_color = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)

print("img_gray type:", type(img_gray))
print("img_gray shape:", img_gray.shape)
print("img_gray dtype:", img_gray.dtype)

print("img_color type:", type(img_color))
print("img_color shape:", img_color.shape)
print("img_color dtype:", img_color.dtype)

# 예시
# img_gray type: <class 'numpy.ndarray'>
# img_gray shape: (364, 548)
# img_gray dtype: uint8
#
# img_color type: <class 'numpy.ndarray'>
# img_color shape: (364, 548, 3)
# img_color dtype: uint8

여기서 가장 중요한 차이는 shape다.

그레이스케일 이미지는 채널 축이 없는 2차원 배열이다.

[
\text{gray image shape} = (H, W)
]

컬러 이미지는 채널 축이 포함된 3차원 배열이다.

[
\text{color image shape} = (H, W, C)
]

보통 OpenCV의 컬러 채널 수는 3이며, 순서는 RGB가 아니라 BGR이다. 이 차이를 이해하지 못하면 Matplotlib로 그렸을 때 색이 이상하게 보이는 이유를 설명하기 어렵다.

주요 메서드와 함수 설명

cv2.imread

파일 경로와 읽기 플래그를 받아 이미지를 NumPy 배열로 반환한다.

img = cv2.imread(path, flag)

입력

  • path: 이미지 파일 경로
  • flag: 읽기 방식

출력

  • 성공 시 numpy.ndarray
  • 실패 시 None

대표 플래그는 다음과 같다.

cv2.IMREAD_GRAYSCALE   # 흑백
cv2.IMREAD_COLOR       # 컬러(BGR)

shape 변화는 플래그에 따라 달라진다.

img_gray = cv2.imread('./images/dog.bmp', cv2.IMREAD_GRAYSCALE)
# shape: (H, W)

img_color = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)
# shape: (H, W, 3)

type, shape, dtype

이 세 가지는 이미지를 배열 관점에서 이해할 때 가장 먼저 확인해야 하는 속성이다.

print(type(img_color))   # numpy.ndarray
print(img_color.shape)   # (H, W, 3)
print(img_color.dtype)   # uint8

dtype이 uint8이라는 것은 각 픽셀 값이 0부터 255 사이의 정수라는 뜻이다. 그래서 색상 반전도 간단히 비트 반전 또는 $255 - x$ 형태로 이해할 수 있다.

len(img.shape)

차원 수를 확인하는 간단한 방법이다.

if len(img_gray.shape) == 2:
    print("그레이스케일 영상")
elif len(img_color.shape) == 3:
    print("컬러영상")

초보자가 자주 헷갈리는 부분은, 그레이스케일인데도 채널 수가 1일 것이라고 기대하는 경우다. 그러나 OpenCV의 기본 흑백 이미지는 보통 $(H, W)$이며, $(H, W, 1)$가 아니다.

입력과 출력, shape 변화

img_gray = cv2.imread('./images/dog.bmp', cv2.IMREAD_GRAYSCALE)
# 입력: 파일 경로 + grayscale 플래그
# 출력: numpy.ndarray
# shape: (H, W)

img_color = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)
# 입력: 파일 경로 + color 플래그
# 출력: numpy.ndarray
# shape: (H, W, 3)

AI가 추천하는 심화예제

이미지를 읽은 뒤 채널별 최소값, 최대값, 평균값을 출력해 보면 배열 감각을 더 빨리 익힐 수 있다.

print(img_color[:, :, 0].mean())  # B 채널 평균
print(img_color[:, :, 1].mean())  # G 채널 평균
print(img_color[:, :, 2].mean())  # R 채널 평균

이 연습은 이후 정규화, 히스토그램 분석, 밝기 조정으로 자연스럽게 이어진다.

3. BGR과 RGB의 차이, 그리고 화면 표시 방식

이미지를 읽는 것과 올바르게 표시하는 것은 별개 문제다. OpenCV는 기본적으로 BGR 순서를 사용하고, Matplotlib는 RGB 순서를 기대한다. 그래서 같은 배열을 그대로 넘기면 파란색과 빨간색이 뒤바뀐 것처럼 보인다.

이 차이를 해결하기 위해 코드에서는 cv2.cvtColor를 사용한다.

img2 = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)
img_color = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)

이 변환이 필요한 이유는 라이브러리 간의 색상 채널 기준이 다르기 때문이다. OpenCV 내부 처리에서는 BGR이 자연스럽지만, 시각화 라이브러리와 함께 쓸 때는 RGB 변환이 거의 필수다.

주요 메서드 설명

cv2.cvtColor

색 공간 또는 채널 표현을 바꾸는 함수다.

img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

입력

  • 변환할 이미지 배열
  • 변환 코드

출력

  • 변환된 이미지 배열

shape는 보통 유지된다.

img_bgr.shape   # (H, W, 3)
img_rgb.shape   # (H, W, 3)

값의 의미만 바뀌고 배열 크기는 그대로다.

plt.imshow

Matplotlib에서 이미지를 그릴 때 사용한다.

plt.imshow(img1, cmap='gray')
plt.imshow(img_color)

그레이스케일 이미지는 색상 맵을 지정해 주는 것이 안전하다. 컬러 이미지는 RGB 순서로 넣어야 자연스럽게 출력된다.

cv2.imshow

OpenCV 창에 이미지를 띄운다.

cv2.imshow('dog color', img2)
cv2.imshow('dog gray', img1)

OpenCV의 imshow는 BGR 배열을 그대로 받아도 정상적으로 보인다. OpenCV 자신이 BGR 기준으로 동작하기 때문이다.

cv2.waitKey

키 입력을 기다리는 함수다.

cv2.waitKey()

입력

  • 지연 시간(ms), 생략 시 내부적으로 키 입력을 기다림

출력

  • 눌린 키의 아스키 코드 또는 정수값

이 함수가 없으면 창이 뜨더라도 곧바로 닫히거나 이벤트가 처리되지 않을 수 있다. OpenCV의 화면 출력은 이벤트 루프와 연결되어 있으므로, 단순히 imshow만 호출해서는 충분하지 않다.

입력과 출력, shape 변화

img2 = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)
# shape: (H, W, 3), BGR

img_color = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
# shape: (H, W, 3), RGB
# 크기는 같고 채널의 의미만 바뀜

4. 이미지를 직접 생성한다는 것의 의미

OpenCV는 파일을 읽기만 하는 도구가 아니다. 배열을 직접 생성하면 빈 캔버스를 만들 수 있고, 그 위에 도형을 그리거나 픽셀을 채우는 식으로 영상 처리 로직을 실험할 수 있다. 실제 서비스에서도 마스크, 오버레이, 시각화 결과, 캔버스 생성은 매우 흔하다.

코드에서는 NumPy를 이용해 여러 종류의 이미지를 만든다.

img1 = np.zeros((240, 320, 3), dtype=np.uint8)
img2 = np.empty((240, 320), dtype=np.uint8)
img3 = np.ones((240, 320), dtype=np.uint8) * 200
img4 = np.full((240, 320, 3), (255, 0, 255), dtype=np.uint8)

이 네 줄은 단순 예제가 아니라 이미지 생성의 핵심 패턴을 보여준다.

  • zeros는 검은 배경
  • empty는 초기화되지 않은 메모리
  • ones에 상수를 곱하면 일정 밝기의 회색 배경
  • full은 특정 색으로 채워진 컬러 배경

즉, OpenCV 영상 처리의 본질은 "이미지를 배열로 읽고, 배열을 가공하고, 다시 보여주는 것"이다.

주요 함수 설명

np.zeros

모든 원소가 0인 배열을 만든다.

img1 = np.zeros((240, 320, 3), dtype=np.uint8)
# shape: (240, 320, 3)
# 모든 픽셀 값: 0
# 검은색 컬러 이미지

np.empty

초기화되지 않은 배열을 만든다.

img2 = np.empty((240, 320), dtype=np.uint8)
# shape: (240, 320)
# 내부 값은 정의되지 않음

초보자가 가장 조심해야 하는 부분이다. empty는 빠르지만 쓰레기 값이 들어 있을 수 있다. 즉, 이미지를 만들 목적으로는 보통 zeros, ones, full이 더 안전하다.

np.ones

모든 원소가 1인 배열을 만든다.

img3 = np.ones((240, 320), dtype=np.uint8) * 200
# shape: (240, 320)
# 모든 픽셀 값: 200
# 밝은 회색 이미지

np.full

지정한 값으로 배열을 채운다.

img4 = np.full((240, 320, 3), (255, 0, 255), dtype=np.uint8)
# shape: (240, 320, 3)
# BGR = (255, 0, 255)
# 자홍색에 가까운 컬러 이미지

cv2.cvtColor with COLOR_GRAY2BGR

2차원 흑백 배열을 3채널 컬러 배열로 바꾼다.

img2 = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
# shape: (240, 320) -> (240, 320, 3)

이 변환이 필요한 이유는 OpenCV의 일부 그리기 함수나 컬러 표시 작업이 3채널 이미지를 기대하기 때문이다. 단일 채널에서는 색을 다루는 연산이 제한될 수 있다.

입력과 출력, shape 변화

img2 = np.empty((240, 320), dtype=np.uint8)
# shape: (240, 320)

img2 = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
# shape: (240, 320, 3)

이후 전체 픽셀을 한 번에 채운다.

img2[:, :] = (255, 102, 255)
# shape 유지: (240, 320, 3)
# 모든 픽셀이 같은 BGR 값으로 설정됨

이 코드는 반복문 없이 전체 이미지를 한 번에 수정하는 NumPy 브로드캐스팅의 예다. 주석 처리된 이중 반복문보다 훨씬 빠르고 간결하다.

AI가 추천하는 심화예제

직접 그라데이션 이미지를 생성해 보면 배열 인덱싱 감각을 익히기 좋다.

img = np.zeros((256, 256), dtype=np.uint8)
for y in range(256):
    img[y, :] = y
# shape: (256, 256)
# 위에서 아래로 밝기가 증가하는 grayscale 이미지

이후 이를 BGR로 변환하면 컬러 오버레이나 알파 블렌딩 실험으로 이어질 수 있다.

5. 키보드 입력과 픽셀 반전

OpenCV 창은 정적인 출력만 담당하지 않는다. 키보드 입력과 결합하면 간단한 상호작용 도구가 된다. 여기서는 사용자가 키를 눌렀을 때 이미지를 반전시키는 예제가 등장한다.

while True:
    keyvalue = cv2.waitKey()
    print(keyvalue)
    if keyvalue == 27:
        break
    elif keyvalue == ord('i') or keyvalue == ord('I'):
        img_color = ~img_color
        cv2.imshow('dog color', img_color)

이 구조는 이후 이미지 뷰어, 증강 미리보기, 라벨링 도구, ROI 선택기 같은 인터랙티브 프로그램의 기본 골격이 된다. 반복문 안에서 키를 받고, 조건에 따라 배열을 수정하고, 다시 imshow로 보여주는 방식이다.

주요 메서드와 함수 설명

cv2.waitKey

키 입력을 기다리고 해당 키 코드를 반환한다.

keyvalue = cv2.waitKey()
# 사용자가 키를 누를 때까지 대기
# 출력: 정수형 키 코드

ord

문자를 아스키 코드 정수로 바꾼다.

ord('i')   # 105
ord('I')   # 73

즉, waitKey가 반환한 값과 직접 비교할 수 있다.

비트 반전 연산자 ~

NumPy uint8 배열에 대해 적용하면 각 픽셀 값이 반전된다.

[
\tilde{x} = 255 - x
]

정확히는 비트 단위 반전이지만 uint8 범위에서는 밝기 반전처럼 동작한다.

img_color = ~img_color
# shape 유지: (H, W, 3)
# 값만 반전

입력과 출력, shape 변화

img_color = cv2.imread('./images/dog.bmp', cv2.IMREAD_COLOR)
# shape: (H, W, 3)

img_color = ~img_color
# shape: (H, W, 3)
# 픽셀 값 반전

shape는 그대로 유지되고, 픽셀 값만 바뀐다. 이 점은 영상 처리에서 매우 중요하다. 많은 연산은 이미지의 공간 구조를 유지한 채 값만 바꾸기 때문이다.

6. 마우스 이벤트는 어떻게 연결되는가

마우스 이벤트는 GUI 프로그래밍과 영상 처리가 만나는 지점이다. 클릭 좌표를 얻거나, 드래그로 선을 그리거나, 특정 영역을 선택하는 기능은 모두 이벤트 콜백 구조로 구현된다.

왜 필요한가 하면, 이미지 위에서 사용자가 직접 상호작용해야 하는 상황이 매우 많기 때문이다. 예를 들어 관심 영역 지정, 주석 달기, 그림판처럼 선 그리기, 좌표 추출, 객체 위치 수동 보정 등이 모두 이 방식에 기반한다.

코드에서는 콜백 함수 on_mouse를 정의하고, 특정 창 이름에 연결한다.

oldx, oldy = 0, 0

def on_mouse(event, x, y, flags, param):
    global oldx, oldy
    if event == cv2.EVENT_LBUTTONDOWN:
        oldx, oldy = x, y
    elif event == cv2.EVENT_MOUSEMOVE:
        if flags:
            cv2.line(img, (oldx, oldy), (x, y), (0, 0, 0), 3)
            cv2.imshow('Mouse Event', img)
            oldx, oldy = x, y

cv2.imshow('Mouse Event', img)
cv2.setMouseCallback('Mouse Event', on_mouse)
cv2.waitKey()

이 코드는 콜백 기반 구조의 핵심을 잘 보여준다. 프로그램이 마우스 움직임을 계속 감시하는 것이 아니라, 이벤트가 발생했을 때 OpenCV가 등록된 함수를 호출한다.

주요 함수와 메서드 설명

cv2.setMouseCallback

특정 창에 마우스 이벤트 핸들러를 등록한다.

cv2.setMouseCallback(window_name, callback)

입력

  • window_name: 이미 생성된 창 이름
  • callback: 이벤트 발생 시 호출될 함수

출력은 없다. 대신 이후 해당 창에서 이벤트가 발생할 때 callback이 실행된다.

on_mouse(event, x, y, flags, param)

콜백 함수의 표준 형태다.

입력 인자는 다음 의미를 가진다.

  • event: 어떤 이벤트가 발생했는가
  • x, y: 현재 마우스 좌표
  • flags: 버튼 눌림 등 부가 상태
  • param: 추가 전달 데이터

대표 event 값은 다음과 같다.

cv2.EVENT_LBUTTONDOWN   # 왼쪽 버튼 누름
cv2.EVENT_RBUTTONDOWN   # 오른쪽 버튼 누름
cv2.EVENT_MBUTTONDOWN   # 가운데 버튼 누름
cv2.EVENT_MOUSEMOVE     # 마우스 이동

global oldx, oldy

이전 좌표를 저장하기 위해 전역 변수를 사용한다. 드래그 선을 자연스럽게 잇기 위해서는 이전 점과 현재 점을 연결해야 한다.

cv2.line

두 점을 연결하는 선을 이미지 위에 그린다.

cv2.line(img, (oldx, oldy), (x, y), (0, 0, 0), 3)

입력

  • img: 대상 이미지
  • 시작점, 끝점
  • 색상(BGR)
  • 두께

출력

  • 내부적으로 img 배열이 수정됨

shape는 바뀌지 않는다.

img.shape   # (500, 500, 3)
# line 호출 후에도 동일

flags의 의미

이 코드에서는 if flags: 형태로만 검사하고 있지만, 실전에서는 구체적으로 어떤 버튼이 눌렸는지 비트 플래그로 확인하는 경우가 많다. 지금 구조는 단순화된 드래그 감지 용도다.

초보자가 오해하기 쉬운 부분은, 마우스를 움직일 때마다 자동으로 선이 이어지는 것이 아니라, 이전 좌표를 계속 갱신해야 선이 끊기지 않는다는 점이다. 그래서 oldx, oldy를 매번 현재 좌표로 덮어쓴다.

도형을 그리는 메서드

이 예제는 마우스 이벤트 외에도 OpenCV의 기본 그리기 메서드를 함께 보여준다.

img = np.ones((500, 500, 3), dtype=np.uint8) * 255

cv2.rectangle(img, (50, 200, 150, 100), (0, 255, 0), 3)
cv2.rectangle(img, (300, 200, 150, 100), (255, 0, 0), -1)
cv2.circle(img, (150, 400), 50, (255,255,0), 3)
cv2.putText(img, '2026', (50, 200), cv2.FONT_HERSHEY_COMPLEX, 0.7, (0,0,0), 2)

cv2.rectangle

사각형을 그린다. 이 코드의 첫 번째 인자 형식은 일반적으로 많이 쓰는 두 점 방식과 다소 다르게 보이는데, 현재 예제는 하나의 좌표와 폭·높이를 묶어 넣는 형태로 작성되어 있다. 실전에서는 보통 시작점과 끝점을 명시하는 아래 형태가 더 직관적이다.

cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness)

cv2.circle

원 중심, 반지름, 색상, 두께를 받아 원을 그린다.

cv2.putText

이미지 위에 문자열을 출력한다.

이들 모두 공통적으로 배열 shape는 유지하고, 픽셀 값만 수정한다.

입력과 출력, shape 변화

img = np.ones((500, 500, 3), dtype=np.uint8) * 255
# shape: (500, 500, 3)

cv2.line(img, (10, 10), (100, 100), (0, 0, 0), 3)
# shape 변화 없음: (500, 500, 3)

cv2.putText(img, '2026', (50, 200), cv2.FONT_HERSHEY_COMPLEX, 0.7, (0,0,0), 2)
# shape 변화 없음: (500, 500, 3)

AI가 추천하는 심화예제

마우스로 사각형 ROI를 선택하도록 바꿔 보면 실제 데이터 라벨링 도구와 가까워진다. 마우스 다운 시 시작점 저장, 마우스 업 시 끝점 저장, 선택 영역 crop까지 연결하면 이미지 처리에서 매우 유용한 구조가 된다.

7. 비디오 입력은 결국 프레임 반복 처리다

비디오 파일도 본질적으로는 이미지의 연속이다. 따라서 이미지를 이해했다면, 비디오를 이해하는 핵심은 "한 장씩 읽어 반복 처리한다"는 점뿐이다. OpenCV는 이를 VideoCapture 객체로 추상화한다.

코드에서는 파일 경로를 열고, 속성을 읽고, 프레임을 반복적으로 가져온다.

cap = cv2.VideoCapture("./movies/311660_tiny.mp4")

if not cap.isOpened():
    print('동영상을 불러올 수 없음')
    sys.exit()

width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
fps = cap.get(cv2.CAP_PROP_FPS)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    cv2.imshow('Video', frame)

    if cv2.waitKey(int(1000/fps)) == 27:
        break

cap.release()

여기서 가장 중요한 구조는 read가 반환하는 두 값이다.

  • ret: 프레임 읽기 성공 여부
  • frame: 실제 영상 프레임 배열

즉, 비디오 처리는 파일 전체를 한 번에 메모리에 올리는 방식이 아니라, 매 반복마다 다음 프레임을 읽는 스트리밍 처리에 가깝다.

주요 메서드 설명

cv2.VideoCapture

비디오 파일 또는 카메라 장치를 열기 위한 객체다.

cap = cv2.VideoCapture(source)

입력

  • source가 문자열이면 비디오 파일 경로
  • source가 정수면 카메라 인덱스

출력

  • VideoCapture 객체

cap.isOpened

열기 성공 여부를 반환한다.

if not cap.isOpened():
    sys.exit()

이 검사는 매우 중요하다. 파일 경로가 틀렸거나 코덱 문제로 열 수 없을 수 있기 때문이다.

cap.get

비디오 또는 카메라의 속성을 조회한다.

width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
fps = cap.get(cv2.CAP_PROP_FPS)

입력

  • 속성 상수

출력

  • 보통 float

주의할 점은 width와 height도 float로 반환될 수 있다는 점이다. 실제 저장이나 배열 크기에 쓸 때는 int 변환이 필요할 때가 많다.

cap.read

다음 프레임을 읽는다.

ret, frame = cap.read()

입력은 없다. 출력은 두 값이다.

ret    # bool
frame  # numpy.ndarray 또는 None

shape는 컬러 비디오 기준 보통 다음과 같다.

frame.shape
# (H, W, 3)

즉, 한 프레임은 결국 컬러 이미지 한 장이다.

cv2.waitKey(int(1000/fps))

재생 속도를 FPS에 맞추기 위한 지연 시간 계산이다.

비디오의 FPS가 초당 프레임 수라면, 프레임 하나당 대기 시간은 대략 다음과 같다.

[
\text{delay(ms)} = \frac{1000}{FPS}
]

이 계산을 하지 않으면 너무 빠르거나 느리게 재생될 수 있다.

입력과 출력, shape 변화

cap = cv2.VideoCapture("./movies/311660_tiny.mp4")
# 입력: 파일 경로
# 출력: VideoCapture 객체

ret, frame = cap.read()
# ret: True or False
# frame: numpy.ndarray
# shape: (H, W, 3)

AI가 추천하는 심화예제

프레임마다 회색조 변환을 적용해 실시간 전처리 흐름을 연습하면 좋다.

ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# frame shape: (H, W, 3)
# gray shape:  (H, W)

이 구조는 이후 에지 검출, 객체 검출, 세그멘테이션 전처리로 확장된다.

8. 카메라 입력은 비디오 입력과 거의 같은 구조다

카메라 입력은 비디오 파일 입력과 형태가 거의 같다. 차이는 source가 파일 경로가 아니라 장치 번호라는 점이다. 보통 기본 웹캠은 0번이다.

cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print('카메라를 열 수 없음')
    sys.exit()

width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    cv2.imshow('Camera', frame)

    if cv2.waitKey(10) == 27:
        break

cap.release()
cv2.destroyAllWindows()

카메라 스트림은 프레임 수가 미리 정해져 있지 않기 때문에 frame_count 같은 값보다 현재 프레임을 계속 읽는 반복 구조가 핵심이다. 비디오 파일과 달리 종료 조건이 파일 끝이 아니라 사용자의 중단이다.

주요 메서드 설명

cv2.VideoCapture(0)

기본 카메라 장치를 연다.

cap = cv2.VideoCapture(0)

입력

  • 정수 인덱스

출력

  • VideoCapture 객체

cv2.destroyAllWindows

OpenCV에서 생성한 모든 창을 닫는다.

cv2.destroyAllWindows()

release가 장치를 해제하는 역할이라면, destroyAllWindows는 GUI 자원을 정리하는 역할이다. 둘은 함께 써 주는 것이 안전하다.

파일 입력과 카메라 입력의 차이

둘 다 read로 프레임을 받고 shape도 비슷하지만, 속성 해석에서 차이가 있다.

  • 파일 입력은 FPS와 frame_count가 비교적 안정적
  • 카메라 입력은 장치 환경에 따라 값이 다르거나 일부 속성이 0으로 나올 수 있음

즉, 카메라 코드는 파일 기반 코드보다 더 방어적으로 작성하는 것이 좋다.

입력과 출력, shape 변화

cap = cv2.VideoCapture(0)
# 출력: VideoCapture 객체

ret, frame = cap.read()
# ret: bool
# frame shape: (H, W, 3)

9. 비디오 저장은 왜 VideoWriter가 필요한가

프레임을 읽는 것과 저장하는 것은 다른 문제다. 읽은 프레임을 파일로 남기려면 비디오 코덱, FPS, 프레임 크기 같은 메타 정보를 함께 지정해야 한다. 단순히 이미지 배열을 반복해서 write한다고 해서 자동으로 동영상 파일이 되는 것은 아니다.

그래서 OpenCV는 VideoWriter 객체를 사용한다.

두 개의 예제가 나온다. 하나는 비디오 파일 두 개를 이어 붙여 저장하는 코드이고, 다른 하나는 카메라 영상을 녹화하려는 코드다.

9.1 비디오 파일을 읽어 새 비디오로 저장하기

cap1 = cv2.VideoCapture("./movies/311660_tiny.mp4")
cap2 = cv2.VideoCapture("./movies/319719_tiny.mp4")

w = int(cap1.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap1.get(cv2.CAP_PROP_FRAME_HEIGHT))

fps_count1 = cap1.get(cv2.CAP_PROP_FPS)

fourcc = cv2.VideoWriter.fourcc(*'DIVX')
out = cv2.VideoWriter("./movies/mix.avi", fourcc, fps_count1, (w, h))

이 구조의 핵심은 출력 비디오의 규격을 먼저 정해 놓는 것이다. VideoWriter는 프레임 크기가 일정하다고 가정하므로, write할 프레임의 shape가 생성 시 지정한 크기와 맞아야 한다.

cv2.VideoWriter.fourcc

비디오 코덱 식별자를 만든다.

fourcc = cv2.VideoWriter.fourcc(*'DIVX')

입력

  • 네 글자의 코덱 코드

출력

  • 정수형 코덱 값

cv2.VideoWriter

출력 비디오 파일을 생성한다.

out = cv2.VideoWriter(filename, fourcc, fps, (w, h))

입력

  • 저장 파일명
  • 코덱
  • FPS
  • 프레임 크기

출력

  • VideoWriter 객체

out.write

프레임 한 장을 비디오 파일에 기록한다.

out.write(frame)

입력

  • frame: 보통 shape가 $(H, W, 3)$인 uint8 배열

출력은 없지만 파일에 프레임이 추가된다.

shape 조건이 매우 중요하다.

frame.shape == (h, w, 3)
# 이 조건이 맞지 않으면 저장이 깨지거나 실패할 수 있음

두 영상을 이어 붙이는 반복 구조

코드에서는 첫 번째 비디오의 모든 프레임을 저장하고, 그 다음 두 번째 비디오의 모든 프레임을 저장한다.

for i in range(frame_count1):
    ret, frame = cap1.read()
    out.write(frame)

for i in range(frame_count2):
    ret, frame = cap2.read()
    out.write(frame)

개념적으로는 리스트 concat과 비슷하지만, 실제 구현은 프레임 스트림을 순차적으로 write하는 방식이다. 비디오 편집의 가장 단순한 형태라고 볼 수 있다.

입력과 출력, shape 변화

ret, frame = cap1.read()
# frame shape: (H, W, 3)

out.write(frame)
# 입력 frame shape는 여전히 (H, W, 3)
# 출력 파일에 프레임 추가
# 배열 shape 자체는 바뀌지 않음

9.2 카메라 영상을 저장하는 코드의 구조와 주의점

카메라 저장 예제는 의도는 분명하지만, 현재 코드만 보면 중요한 포인트가 하나 있다.

cap = cv2.VideoCapture()

w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)

fourcc = cv2.VideoWriter.fourcc(*'DIVX')
out = cv2.VideoWriter("./movies/camera.avi", fourcc, fps, (w, h))

while True:
    ret, frame = cap.read()
    if not ret:
        break
    cv2.imshow("camera", frame)
    out.write(frame)

이 코드는 카메라를 열기 위한 source가 지정되지 않았다. 즉, 보통 기대하는 형태인 cv2.VideoCapture(0) 또는 cap.open(0) 같은 단계가 빠져 있다. 따라서 실제 실행 시에는 장치를 제대로 열지 못할 가능성이 크다. 이 점은 구현 의도를 해석할 때 매우 중요하다. 코드는 "카메라 입력을 받아 저장한다"는 구조를 설명하기 위한 예제지만, 실전에서는 반드시 장치 open 단계가 필요하다.

실제로는 아래처럼 작성하는 편이 더 안전하다.

cap = cv2.VideoCapture(0)

if not cap.isOpened():
    raise RuntimeError("카메라를 열 수 없음")

w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)

if fps <= 0:
    fps = 30.0  # 장치가 FPS를 제대로 주지 않는 경우 대비

초보자가 자주 놓치는 부분은 세 가지다.

  • VideoWriter를 만들기 전에 카메라가 먼저 열려 있어야 한다.
  • width, height, fps는 장치에서 유효값을 받아오는지 확인해야 한다.
  • write하는 프레임의 크기와 writer 생성 시의 크기가 일치해야 한다.

AI가 추천하는 심화예제

카메라 입력을 녹화하면서 동시에 화면에 녹화 시간이나 FPS를 표시해 보면 훨씬 실전적인 예제가 된다. 이 경우 cv2.putText와 time 모듈을 조합하면 된다.

10. 자주 등장하는 메서드 정리

핵심은 복잡한 알고리즘이 아니라, OpenCV 입출력 계층을 구성하는 메서드를 몸에 익히는 데 있다. 아래 메서드들은 이후 어떤 비전 프로젝트를 읽더라도 반복해서 만나게 된다.

cv2.imread

파일에서 이미지를 읽어 배열로 반환한다.

img = cv2.imread(path, flag)
# 출력 shape:
# grayscale -> (H, W)
# color     -> (H, W, 3)

cv2.imshow

배열을 창에 띄운다.

cv2.imshow(window_name, img)
# img shape:
# (H, W) 또는 (H, W, 3)

cv2.waitKey

키 입력을 기다리고 이벤트 루프를 유지한다.

key = cv2.waitKey(delay)
# 출력: int

cv2.cvtColor

색상 공간 또는 채널 수를 바꾼다.

rgb  = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
bgr2 = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)

# (H, W, 3) -> (H, W, 3)
# (H, W)    -> (H, W, 3)

cv2.VideoCapture

비디오 파일이나 카메라 입력 소스를 연다.

cap = cv2.VideoCapture(source)

cap.get

입력 소스의 속성을 조회한다.

width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)

cap.read

다음 프레임을 읽는다.

ret, frame = cap.read()
# frame shape: (H, W, 3)

cv2.VideoWriter

프레임들을 비디오 파일로 저장한다.

out = cv2.VideoWriter(filename, fourcc, fps, (w, h))
out.write(frame)
# frame shape must be: (h, w, 3)

cv2.setMouseCallback

창에 마우스 이벤트 콜백을 연결한다.

cv2.setMouseCallback('window', on_mouse)

cv2.line, cv2.rectangle, cv2.circle, cv2.putText

이미지 위에 도형과 텍스트를 그린다. 공통점은 모두 배열 shape를 바꾸지 않고 내부 픽셀 값만 수정한다는 점이다.

11. 코드 실행 흐름은 어떻게 이어지는가

첫 단계에서는 OpenCV가 정상 설치되었는지 확인한다.
그 다음 이미지를 읽고, 그것이 NumPy 배열이라는 점과 shape, dtype 구조를 이해한다.
이후 BGR과 RGB 차이를 익히며 화면 표시 방식을 정리한다.
그다음 직접 배열을 생성하고, 픽셀 값을 바꾸고, 키보드 입력에 따라 이미지를 수정해 보면서 "영상은 수정 가능한 배열"이라는 감각을 익힌다.
마우스 이벤트를 연결하면서 GUI 상호작용 개념을 추가한다.
마지막으로 비디오 파일과 카메라 스트림을 통해 정적인 이미지 한 장이 아니라 연속 프레임 처리 구조로 확장하고, VideoWriter로 저장까지 연결한다.

이 흐름을 이해하고 나면, 이후의 모델 추론 코드도 훨씬 읽기 쉬워진다. 예를 들어 객체 검출 코드가 있다면 결국 다음 구조를 벗어나지 않는다.

  • cap.read로 프레임을 읽는다
  • 필요하면 cvtColor나 resize로 전처리한다
  • 모델에 넣는다
  • 결과를 line, rectangle, putText로 시각화한다
  • imshow로 화면에 띄운다
  • 필요하면 VideoWriter로 저장한다