목록으로

프로그래밍 · Python

Python 완전정복 시리즈 #31: OpenCV 완벽 이해하기

BeanCon
Python OpenCV로 이미지와 영상을 분석하는 컴퓨터 비전 입문을 설명하는 대표 이미지

Python 완전정복 시리즈 31편입니다. OpenCV로 이미지와 영상을 읽고 분석하는 컴퓨터 비전 입문 내용을 실습 중심으로 정리했습니다. 이미지 불러오기, 픽셀과 좌표, shape와 dtype, BGR과 RGB, 이미지 자르기와 크기 변경, 회전, 도형과 글자 그리기, 색상 공간 변환, HSV 색상 검출, 이미지 합성, 블러, 임계처리, 모폴로지, Canny 엣지 검출, 윤곽선 분석, 히스토그램, 밝기와 대비 조절, 웹캠과 동영상 처리, 움직임 감지 카메라, Haar Cascade 얼굴 검출, PyTorch 연결, 성능 최적화와 자주 발생하는 오류까지 다룹니다.

목차

메타 설명
OpenCV는 이미지와 영상을 어떻게 읽고 분석할까요? Python과 OpenCV를 이용해 이미지 불러오기, 픽셀과 좌표, 크기 변경, 자르기, 색상 변환, 블러, 임계처리, 모폴로지, 엣지 검출, 윤곽선 분석, 웹캠 영상 처리, 움직임 감지, 얼굴 검출까지 실습 중심으로 알아봅니다.

지난 시간에는 PyTorch를 이용해 FashionMNIST 의류 이미지를 분류하는 CNN 모델을 만들었습니다.

신경망에 이미지를 넣으니 모델이 다음과 같은 결과를 출력했습니다.

입력 이미지
→ CNN 특징 추출
→ 클래스 점수 계산
→ 운동화로 예측

그런데 신경망에 이미지를 넣기 전에 해야 할 일이 아주 많습니다.

이미지가 너무 크다면?

사진이 옆으로 누워 있다면?

필요한 부분만 잘라야 한다면?

영상에서 움직이는 물체만 찾아야 한다면?

사진의 글자나 도형 경계를 검출해야 한다면?

특정 색상의 물체만 추적해야 한다면?

이런 작업을 모두 딥러닝 모델에 맡길 필요는 없습니다.

이미지를 자르고, 크기를 변경하고, 색상을 변환하고, 선과 윤곽선을 찾는 전통적인 영상 처리 작업은 OpenCV만으로도 빠르게 해결할 수 있습니다.

이번 시간의 주인공은 OpenCV입니다.

OpenCV를 한 문장으로 표현하면 다음과 같습니다.

컴퓨터에게 눈을 달아 주기 전에
이미지를 숫자로 읽고 다루는 방법을 제공하는 도구 상자

단, 컴퓨터는 사진을 보고 이렇게 말하지 않습니다.

“바다가 정말 아름답네요.”

컴퓨터가 보는 것은 이런 숫자입니다.

[125, 84, 32]
[130, 91, 40]
[201, 180, 145]

우리가 감성으로 보는 풍경을 컴퓨터는 픽셀 회의록으로 읽습니다. 📸🔍

1. OpenCV란?

OpenCV는 Open Source Computer Vision Library의 줄임말입니다.

이미지 처리, 영상 분석, 객체 검출, 특징점 추출, 카메라 보정, 영상 추적, 머신러닝과 딥러닝 추론 등 컴퓨터 비전에 필요한 다양한 기능을 제공하는 오픈소스 라이브러리입니다.

OpenCV 5는 2026년 공개된 현재 주요 버전 계열이며, 공식 문서는 이미지 처리, 영상 분석, 객체 검출, 특징 검출, 3차원 복원, 이미지 스티칭 등 광범위한 컴퓨터 비전 기능을 안내하고 있습니다.

OpenCV는 원래 C++ 중심으로 개발되었지만 Python에서도 사용할 수 있습니다.

Python 코드에서는 다음 이름으로 불러옵니다.

import cv2

설치 패키지 이름은 `opencv-python`인데 불러올 때는 `cv2`라는 이름을 사용합니다.

설치 이름:
opencv-python

Python import 이름:
cv2

첫 만남부터 이름이 두 개입니다.

컴퓨터 비전계의 예명과 본명이라고 생각하면 됩니다.

2. 컴퓨터 비전이란?

컴퓨터 비전은 컴퓨터가 이미지와 영상에서 의미 있는 정보를 추출하도록 만드는 기술 분야입니다.

사람은 사진을 보는 순간 여러 정보를 자연스럽게 파악합니다.

사람이 있다.

빨간 자동차가 있다.

차량이 오른쪽으로 움직인다.

사람이 웃고 있다.

신호등이 빨간색이다.

하지만 컴퓨터가 처음 받는 것은 픽셀값의 배열입니다.

높이 × 너비 × 색상 채널

컴퓨터 비전은 이 숫자에서 다음 정보를 찾아냅니다.

밝기

색상

경계

도형

움직임

특징점

물체의 위치

객체의 종류

대표적인 활용 분야는 다음과 같습니다.

자율주행 차량

CCTV 이상 행동 분석

제품 불량 검사

의료 영상 분석

문서 스캔과 OCR

얼굴 검출

증강현실

스포츠 영상 분석

로봇 시각

위성·드론 영상 분석

3. OpenCV와 딥러닝의 관계

OpenCV와 PyTorch는 경쟁 관계가 아닙니다.

역할이 다릅니다.

OpenCV

이미지를 읽는다.

크기를 조절한다.

색상을 변환한다.

노이즈를 제거한다.

경계와 윤곽선을 찾는다.

카메라 영상을 처리한다.

PyTorch

신경망을 만든다.

이미지의 특징을 학습한다.

객체 종류를 분류한다.

복잡한 패턴을 학습한다.

실제 컴퓨터 비전 시스템에서는 두 도구를 함께 사용할 수 있습니다.

OpenCV
→ 카메라 영상 읽기

OpenCV
→ 이미지 크기와 색상 변환

PyTorch
→ 객체 분류 또는 검출

OpenCV
→ 결과 박스와 글자 그리기

OpenCV
→ 화면 출력과 영상 저장

OpenCV가 촬영팀과 편집팀이라면 PyTorch는 영상을 보고 판단하는 분석팀입니다.

4. OpenCV 설치하기

일반적인 데스크톱 환경에서는 다음 명령어로 설치할 수 있습니다.

python -m pip install opencv-python

NumPy와 Matplotlib도 함께 설치하겠습니다.

python -m pip install opencv-python numpy matplotlib

Windows 가상환경

python -m venv venv
venv\Scripts\activate

python -m pip install --upgrade pip
python -m pip install opencv-python numpy matplotlib

macOS 또는 Linux

python3 -m venv venv
source venv/bin/activate

python -m pip install --upgrade pip
python -m pip install opencv-python numpy matplotlib

PyPI의 `opencv-python` 패키지는 미리 빌드된 CPU용 OpenCV Python 패키지를 제공하며, GUI 포함 여부와 추가 모듈 포함 여부에 따라 네 가지 패키지 종류를 제공합니다. 동일한 환경에는 이 중 하나만 설치해야 합니다.

5. OpenCV 패키지 종류

OpenCV Python 패키지는 크게 네 종류로 나뉩니다.

일반 데스크톱용

python -m pip install opencv-python

주요 OpenCV 모듈과 `cv2.imshow()` 같은 GUI 기능을 포함합니다.

추가 모듈 포함

python -m pip install opencv-contrib-python

기본 모듈과 함께 contrib 추가 모듈이 포함됩니다.

서버·Docker용

python -m pip install opencv-python-headless

GUI 기능을 제외한 서버용 패키지입니다.

서버용 추가 모듈 포함

python -m pip install opencv-contrib-python-headless

GUI는 제외하고 contrib 모듈을 포함합니다.

어떤 것을 선택해야 할까?

로컬 PC에서 imshow 사용
→ opencv-python

추가 기능도 필요
→ opencv-contrib-python

Docker·서버·클라우드
→ opencv-python-headless

서버에서 contrib까지 필요
→ opencv-contrib-python-headless

같은 가상환경에 여러 패키지를 동시에 설치하면 모두 `cv2`라는 동일한 네임스페이스를 사용하기 때문에 충돌할 수 있습니다. 공식 패키지 안내에서도 네 패키지 중 하나만 선택하도록 설명합니다.

opencv-python
+ opencv-contrib-python
+ headless

= 기능이 세 배?

아니요.
= 충돌 가능성 세 배

6. 설치 확인하기

import cv2

print(
    cv2.__version__
)

간단한 정보도 확인할 수 있습니다.

import cv2

print(
    cv2.getBuildInformation()
)

출력 내용이 매우 길 수 있습니다.

다음과 같은 정보를 확인할 때 유용합니다.

OpenCV 버전

운영체제

GUI 지원

FFmpeg 지원

CPU 최적화

사용 가능한 코덱

추가 모듈

7. 이미지는 NumPy 배열이다

OpenCV에서 이미지는 NumPy 배열로 표현됩니다.

import cv2

image = cv2.imread(
    "sample.jpg"
)

print(
    type(image)
)

결과:

<class 'numpy.ndarray'>

컬러 이미지의 기본 구조는 다음과 같습니다.

높이 × 너비 × 채널

예:

1080 × 1920 × 3
높이:
1080픽셀

너비:
1920픽셀

색상 채널:
3개

흑백 이미지는 일반적으로 2차원입니다.

높이 × 너비

OpenCV의 이미지 처리 기능과 NumPy의 인덱싱, 통계, 배열 연산을 함께 사용할 수 있는 이유가 바로 이것입니다.

이미지가 갑자기 사진에서 엑셀표처럼 느껴질 수 있습니다.

단, 행과 열이 수백만 개이고 각 칸이 색상을 품고 있는 아주 거대한 표입니다.

8. 이미지 불러오기

이미지는 `cv2.imread()`로 불러옵니다.

import cv2

image = cv2.imread(
    "sample.jpg"
)

컬러 이미지

image = cv2.imread(
    "sample.jpg",
    cv2.IMREAD_COLOR
)

흑백 이미지

gray_image = cv2.imread(
    "sample.jpg",
    cv2.IMREAD_GRAYSCALE
)

원본 채널 유지

알파 채널이 있는 PNG 파일을 그대로 불러오려면 다음 옵션을 사용할 수 있습니다.

image = cv2.imread(
    "sample.png",
    cv2.IMREAD_UNCHANGED
)

OpenCV의 이미지 코덱 기능은 `imread()`로 파일을 읽고 `imwrite()`로 결과를 저장하는 인터페이스를 제공합니다.

9. 이미지 불러오기 실패 처리

파일이 없거나 지원되지 않는 형식이면 `cv2.imread()`가 `None`을 반환할 수 있습니다.

import cv2


image = cv2.imread(
    "sample.jpg"
)

if image is None:
    raise FileNotFoundError(
        "이미지를 불러오지 못했습니다. "
        "파일 경로와 형식을 확인하세요."
    )

현재 작업 폴더를 확인합니다.

from pathlib import Path

print(
    Path.cwd()
)

파일 존재 여부:

from pathlib import Path

image_path = Path(
    "sample.jpg"
)

print(
    image_path.exists()
)

안전한 함수로 만들 수 있습니다.

from pathlib import Path

import cv2
import numpy as np


def load_image(
    file_path: str | Path,
    flags: int = cv2.IMREAD_COLOR
) -> np.ndarray:
    path = Path(file_path)

    if not path.exists():
        raise FileNotFoundError(
            f"파일을 찾을 수 없습니다: {path}"
        )

    image = cv2.imread(
        str(path),
        flags
    )

    if image is None:
        raise ValueError(
            f"이미지를 해석하지 못했습니다: {path}"
        )

    return image

10. 이미지 출력하기

OpenCV 창으로 이미지를 출력합니다.

import cv2


image = cv2.imread(
    "sample.jpg"
)

if image is None:
    raise FileNotFoundError(
        "이미지 파일을 확인하세요."
    )

cv2.imshow(
    "Image",
    image
)

cv2.waitKey(0)
cv2.destroyAllWindows()

waitKey

cv2.waitKey(0)

키가 입력될 때까지 기다립니다.

숫자를 지정하면 밀리초 단위로 기다립니다.

cv2.waitKey(1000)

약 1초 기다립니다.

destroyAllWindows

cv2.destroyAllWindows()

OpenCV가 생성한 창을 닫습니다.

서버에서는 imshow가 동작하지 않을 수 있다

다음 환경에서는 GUI 창을 사용할 수 없을 수 있습니다.

Docker

원격 Linux 서버

Jupyter 환경

클라우드 노트북

headless 패키지

이런 환경에서는 Matplotlib로 표시하거나 파일로 저장합니다.

import cv2
import matplotlib.pyplot as plt


image = cv2.imread(
    "sample.jpg"
)

rgb_image = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

plt.imshow(
    rgb_image
)

plt.axis("off")
plt.show()

11. 이미지 저장하기

import cv2


success = cv2.imwrite(
    "result.jpg",
    image
)

if not success:
    raise IOError(
        "이미지를 저장하지 못했습니다."
    )

PNG 압축 수준을 지정할 수 있습니다.

cv2.imwrite(
    "result.png",
    image,
    [
        cv2.IMWRITE_PNG_COMPRESSION,
        3
    ]
)

JPEG 품질:

cv2.imwrite(
    "result.jpg",
    image,
    [
        cv2.IMWRITE_JPEG_QUALITY,
        90
    ]
)
JPEG 품질 높음
→ 파일 크기 증가
→ 압축 손실 감소

JPEG 품질 낮음
→ 파일 크기 감소
→ 압축 흔적 증가

12. 한글 경로 이미지 처리하기

일부 Windows 환경이나 특정 라이브러리 조합에서는 한글이 포함된 파일 경로에서 문제가 발생할 수 있습니다.

이때 NumPy와 `cv2.imdecode()`를 사용할 수 있습니다.

한글 경로 읽기

from pathlib import Path

import cv2
import numpy as np


def read_image_unicode(
    file_path: str | Path,
    flags: int = cv2.IMREAD_COLOR
) -> np.ndarray:
    path = Path(file_path)

    if not path.exists():
        raise FileNotFoundError(
            f"파일을 찾을 수 없습니다: {path}"
        )

    file_bytes = np.fromfile(
        str(path),
        dtype=np.uint8
    )

    image = cv2.imdecode(
        file_bytes,
        flags
    )

    if image is None:
        raise ValueError(
            f"이미지를 해석하지 못했습니다: {path}"
        )

    return image

한글 경로 저장

from pathlib import Path

import cv2


def write_image_unicode(
    file_path: str | Path,
    image
) -> None:
    path = Path(file_path)

    extension = path.suffix

    if not extension:
        raise ValueError(
            "파일 확장자가 필요합니다."
        )

    success, encoded = cv2.imencode(
        extension,
        image
    )

    if not success:
        raise IOError(
            "이미지 인코딩에 실패했습니다."
        )

    encoded.tofile(
        str(path)
    )

사용:

image = read_image_unicode(
    "사진폴더/바다사진.jpg"
)

write_image_unicode(
    "결과폴더/변환결과.jpg",
    image
)

13. 이미지의 shape와 dtype

import cv2


image = cv2.imread(
    "sample.jpg"
)

height, width, channels = image.shape

print(
    f"높이: {height}"
)

print(
    f"너비: {width}"
)

print(
    f"채널: {channels}"
)

print(
    f"자료형: {image.dtype}"
)

일반적인 이미지 자료형:

uint8

값의 범위:

0 ~ 255

흑백 이미지

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

print(
    gray.shape
)

결과 예:

(1080, 1920)

채널 차원이 없습니다.

다음처럼 무조건 세 변수로 받으면 오류가 발생합니다.

# 흑백 이미지에서는 오류 가능
height, width, channels = gray.shape

안전하게 처리합니다.

height, width = gray.shape

또는:

height, width = image.shape[:2]

14. OpenCV의 BGR 색상 순서

OpenCV는 기본적으로 컬러 이미지를 BGR 순서로 다룹니다.

B:
Blue

G:
Green

R:
Red

일반적인 RGB 순서와 반대입니다.

OpenCV:
BGR

Matplotlib:
RGB

OpenCV 이미지를 Matplotlib에 그대로 표시하면 빨간색과 파란색이 뒤바뀔 수 있습니다.

import cv2
import matplotlib.pyplot as plt


image = cv2.imread(
    "sample.jpg"
)

rgb_image = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

plt.imshow(
    rgb_image
)

plt.axis("off")
plt.show()

색상 공간 변환에는 `cv2.cvtColor()`를 사용하며 OpenCV는 BGR·RGB·Gray·HSV를 포함한 다양한 변환 코드를 제공합니다.

BGR을 모르고 Matplotlib에 이미지를 띄우면 파란 바다가 붉게 변할 수 있습니다.

갑자기 재난 영화 포스터가 완성됩니다.

15. 픽셀 값 읽고 수정하기

OpenCV 이미지는 NumPy 배열이므로 인덱스로 픽셀에 접근할 수 있습니다.

pixel = image[
    100,
    200
]

print(pixel)

주의할 점:

image[y, x]
첫 번째 인덱스:
행, y 좌표

두 번째 인덱스:
열, x 좌표

수학 좌표는 보통 `(x, y)`로 쓰지만 배열 접근은 `[y, x]`입니다.

이 작은 차이로 사각형이 예상치 못한 곳에 출현할 수 있습니다.

픽셀 수정

image[
    100,
    200
] = [
    0,
    0,
    255
]

BGR 기준으로 빨간색입니다.

영역 수정

image[
    50:150,
    100:300
] = [
    0,
    255,
    0
]

해당 영역이 초록색으로 변경됩니다.

16. 이미지 자르기

관심 영역 ROI를 NumPy 슬라이싱으로 추출할 수 있습니다.

cropped = image[
    100:400,
    200:600
]

의미:

y:
100부터 399

x:
200부터 599

복사본 만들기

cropped = image[
    100:400,
    200:600
].copy()

`copy()`를 사용하지 않으면 원본 배열과 메모리를 공유하는 뷰가 될 수 있습니다.

함수 만들기

import numpy as np


def crop_image(
    image: np.ndarray,
    x: int,
    y: int,
    width: int,
    height: int
) -> np.ndarray:
    image_height, image_width = (
        image.shape[:2]
    )

    x1 = max(
        0,
        x
    )

    y1 = max(
        0,
        y
    )

    x2 = min(
        image_width,
        x + width
    )

    y2 = min(
        image_height,
        y + height
    )

    if x1 >= x2 or y1 >= y2:
        raise ValueError(
            "유효하지 않은 자르기 영역입니다."
        )

    return image[
        y1:y2,
        x1:x2
    ].copy()

17. 이미지 크기 변경하기

resized = cv2.resize(
    image,
    (
        640,
        480
    )
)

크기 순서:

(width, height)

배열의 `shape` 순서와 반대입니다.

image.shape:
(height, width)

cv2.resize:
(width, height)

OpenCV가 좌표 순서로 초보자의 집중력을 시험하는 구간입니다.

축소 보간법

resized = cv2.resize(
    image,
    (
        640,
        480
    ),
    interpolation=cv2.INTER_AREA
)

확대 보간법

resized = cv2.resize(
    image,
    (
        1280,
        960
    ),
    interpolation=cv2.INTER_CUBIC
)

대표적인 보간 방식:

방식특징
`INTER_NEAREST`빠르지만 계단 현상 가능
`INTER_LINEAR`기본적인 확대·축소
`INTER_AREA`이미지 축소에 자주 사용
`INTER_CUBIC`확대 품질이 좋은 편
`INTER_LANCZOS4`고품질 확대·축소

18. 비율을 유지하며 크기 변경하기

원본 비율을 무시하면 이미지가 찌그러질 수 있습니다.

import cv2
import numpy as np


def resize_by_width(
    image: np.ndarray,
    target_width: int
) -> np.ndarray:
    height, width = image.shape[:2]

    scale = (
        target_width
        / width
    )

    target_height = round(
        height * scale
    )

    return cv2.resize(
        image,
        (
            target_width,
            target_height
        ),
        interpolation=cv2.INTER_AREA
    )

사용:

resized = resize_by_width(
    image,
    800
)

최대 크기 안에 맞추기

def fit_image(
    image: np.ndarray,
    max_width: int,
    max_height: int
) -> np.ndarray:
    height, width = image.shape[:2]

    scale = min(
        max_width / width,
        max_height / height,
        1.0
    )

    new_width = round(
        width * scale
    )

    new_height = round(
        height * scale
    )

    return cv2.resize(
        image,
        (
            new_width,
            new_height
        ),
        interpolation=cv2.INTER_AREA
    )

19. 이미지 뒤집기와 회전

좌우 반전

flipped = cv2.flip(
    image,
    1
)

상하 반전

flipped = cv2.flip(
    image,
    0
)

상하좌우 반전

flipped = cv2.flip(
    image,
    -1
)

90도 회전

rotated = cv2.rotate(
    image,
    cv2.ROTATE_90_CLOCKWISE
)

다른 옵션:

ROTATE_90_CLOCKWISE

ROTATE_90_COUNTERCLOCKWISE

ROTATE_180

원하는 각도로 회전

height, width = image.shape[:2]

center = (
    width / 2,
    height / 2
)

rotation_matrix = cv2.getRotationMatrix2D(
    center,
    30,
    1.0
)

rotated = cv2.warpAffine(
    image,
    rotation_matrix,
    (
        width,
        height
    )
)
30:
회전 각도

1.0:
크기 배율

회전하면 이미지 일부가 잘릴 수 있습니다.

회전된 전체 영역을 유지하려면 출력 크기와 이동 행렬을 추가로 계산해야 합니다.

20. 도형과 글자 그리기

OpenCV는 이미지 위에 선, 사각형, 원, 다각형, 글자를 그릴 수 있습니다.

cv2.line(
    image,
    (
        50,
        50
    ),
    (
        500,
        50
    ),
    (
        255,
        0,
        0
    ),
    thickness=3
)

사각형

cv2.rectangle(
    image,
    (
        100,
        100
    ),
    (
        400,
        350
    ),
    (
        0,
        255,
        0
    ),
    thickness=3
)

채워진 사각형:

cv2.rectangle(
    image,
    (
        100,
        100
    ),
    (
        400,
        350
    ),
    (
        0,
        255,
        0
    ),
    thickness=-1
)

cv2.circle(
    image,
    (
        300,
        200
    ),
    radius=80,
    color=(
        0,
        0,
        255
    ),
    thickness=4
)

글자

cv2.putText(
    image,
    "OpenCV",
    (
        50,
        100
    ),
    cv2.FONT_HERSHEY_SIMPLEX,
    1.2,
    (
        255,
        255,
        255
    ),
    thickness=2,
    lineType=cv2.LINE_AA
)

OpenCV 기본 글꼴은 한글을 제대로 표시하지 못할 수 있습니다.

한글 글자를 넣으려면 Pillow와 한글 폰트를 함께 사용하는 방법을 검토할 수 있습니다.

OpenCV 공식 그리기 기능은 선, 원, 사각형, 다각형과 같은 기본 도형을 이미지 위에 그릴 수 있도록 제공합니다.

21. 색상 공간 변환

색상 공간은 색상을 숫자로 표현하는 방법입니다.

BGR에서 Gray

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

흑백 변환은 단순히 채널 하나를 삭제하는 것이 아닙니다.

여러 색상 채널의 밝기 정보를 조합합니다.

BGR에서 RGB

rgb = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

BGR에서 HSV

hsv = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2HSV
)

HSV:

H:
Hue, 색상 종류

S:
Saturation, 채도

V:
Value, 밝기

특정 색상을 검출할 때 BGR보다 HSV가 편리한 경우가 많습니다.

OpenCV의 8비트 HSV 표현에서는 Hue 범위가 일반적으로 0부터 180까지 사용됩니다.

22. HSV로 특정 색상 검출하기

파란색 물체를 찾는 예제입니다.

import cv2
import numpy as np


image = cv2.imread(
    "objects.jpg"
)

if image is None:
    raise FileNotFoundError(
        "objects.jpg 파일을 확인하세요."
    )

hsv = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2HSV
)

lower_blue = np.array([
    90,
    80,
    50
])

upper_blue = np.array([
    130,
    255,
    255
])

mask = cv2.inRange(
    hsv,
    lower_blue,
    upper_blue
)

result = cv2.bitwise_and(
    image,
    image,
    mask=mask
)

cv2.imshow(
    "Original",
    image
)

cv2.imshow(
    "Mask",
    mask
)

cv2.imshow(
    "Blue Objects",
    result
)

cv2.waitKey(0)
cv2.destroyAllWindows()

inRange

mask = cv2.inRange(
    hsv,
    lower_blue,
    upper_blue
)

지정한 범위 안의 픽셀은 흰색, 나머지는 검은색으로 만듭니다.

범위 안:
255

범위 밖:
0

색상 범위는 환경에 따라 달라진다

조명

카메라 노출

그림자

반사

물체 재질

화이트밸런스

같은 파란색 물체라도 촬영 환경에 따라 HSV 값이 달라질 수 있습니다.

색상 검출기는 물체의 주민등록증이 아니라 조명 아래에서 찍힌 현재 사진을 확인합니다.

23. 이미지 합성과 투명도

두 이미지를 같은 크기로 준비합니다.

image_a = cv2.imread(
    "image_a.jpg"
)

image_b = cv2.imread(
    "image_b.jpg"
)

image_b = cv2.resize(
    image_b,
    (
        image_a.shape[1],
        image_a.shape[0]
    )
)

가중치 합성

blended = cv2.addWeighted(
    image_a,
    0.7,
    image_b,
    0.3,
    0
)

의미:

결과
=
image_a × 0.7
+ image_b × 0.3
+ 0

로고 오버레이

overlay = image.copy()

cv2.rectangle(
    overlay,
    (
        20,
        20
    ),
    (
        300,
        100
    ),
    (
        0,
        0,
        0
    ),
    thickness=-1
)

result = cv2.addWeighted(
    overlay,
    0.5,
    image,
    0.5,
    0
)

반투명한 정보 패널을 만들 수 있습니다.

24. 블러와 노이즈 제거

블러는 이미지를 흐리게 만드는 처리입니다.

단순한 장식 효과뿐 아니라 노이즈를 줄이고 엣지 검출을 안정화하는 전처리로 사용됩니다.

평균 블러

blurred = cv2.blur(
    image,
    (
        5,
        5
    )
)

주변 5×5 픽셀의 평균값을 사용합니다.

가우시안 블러

gaussian = cv2.GaussianBlur(
    image,
    (
        5,
        5
    ),
    sigmaX=0
)

가까운 픽셀에 더 큰 가중치를 주는 방식입니다.

중앙값 블러

median = cv2.medianBlur(
    image,
    5
)

소금과 후추처럼 흰색·검은색 점 노이즈를 줄일 때 유용할 수 있습니다.

양방향 필터

bilateral = cv2.bilateralFilter(
    image,
    d=9,
    sigmaColor=75,
    sigmaSpace=75
)

경계는 비교적 유지하면서 노이즈를 줄이는 데 활용할 수 있습니다.

일반 블러:
노이즈와 경계를 함께 흐림

양방향 필터:
경계를 더 보존하면서 평활화

필터를 강하게 적용하면 노이즈뿐 아니라 중요한 세부 정보도 사라집니다.

피부 보정을 너무 강하게 했더니 사람은 사라지고 도자기만 남는 상황을 조심해야 합니다.

25. 이진화와 임계처리

임계처리는 픽셀을 기준값에 따라 두 그룹으로 나누는 작업입니다.

픽셀값이 임계값보다 큼
→ 흰색

픽셀값이 임계값 이하
→ 검은색

기본 이진화

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

threshold_value, binary = cv2.threshold(
    gray,
    127,
    255,
    cv2.THRESH_BINARY
)

반환값:

threshold_value:
사용된 임계값

binary:
결과 이미지

반전 이진화

_, binary_inverse = cv2.threshold(
    gray,
    127,
    255,
    cv2.THRESH_BINARY_INV
)

문서 이미지에서 검은 글자를 흰색 객체로 바꾸고 싶을 때 활용할 수 있습니다.

OpenCV의 임계처리 기능은 고정 임계값, 적응형 임계값, Otsu 방식 등 여러 이진화 방법을 제공합니다.

26. 적응형 임계처리

이미지 전체에 조명이 균일하지 않다면 고정 임계값 하나로는 좋은 결과를 얻기 어렵습니다.

왼쪽:
밝음

오른쪽:
그림자

전체 임계값:
한쪽은 잘 보이고 한쪽은 사라짐

적응형 임계처리는 각 영역 주변의 픽셀을 기준으로 임계값을 계산합니다.

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

adaptive = cv2.adaptiveThreshold(
    gray,
    255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    blockSize=11,
    C=2
)

blockSize

주변 영역의 크기입니다.

홀수여야 합니다.

3, 5, 7, 11, 15...

C

계산된 기준값에서 빼는 상수입니다.

값을 조정해 결과를 세밀하게 변경할 수 있습니다.

27. Otsu 자동 임계값

임계값을 사람이 직접 정하지 않고 이미지 분포를 바탕으로 자동 선택할 수 있습니다.

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

blurred = cv2.GaussianBlur(
    gray,
    (
        5,
        5
    ),
    0
)

threshold_value, otsu = cv2.threshold(
    blurred,
    0,
    255,
    (
        cv2.THRESH_BINARY
        + cv2.THRESH_OTSU
    )
)

print(
    f"선택된 임계값: {threshold_value}"
)

Otsu 방식은 히스토그램이 배경과 전경의 두 그룹으로 비교적 잘 나뉘는 이미지에서 효과적일 수 있습니다.

사람:
“임계값을 127로 할까, 140으로 할까?”

Otsu:
“히스토그램을 확인해 보고 결정하겠습니다.”

자동 방식도 모든 이미지에서 완벽하지 않습니다.

배경이 복잡하거나 조명이 심하게 변하면 전처리나 다른 분할 방법이 필요할 수 있습니다.

28. 모폴로지 연산

모폴로지 연산은 이진 이미지의 형태를 다듬는 작업입니다.

대표 연산:

침식

팽창

열기

닫기

커널 생성

import numpy as np

kernel = np.ones(
    (
        3,
        3
    ),
    dtype=np.uint8
)

침식

eroded = cv2.erode(
    binary,
    kernel,
    iterations=1
)

흰색 영역을 줄입니다.

작은 흰색 노이즈 제거

붙어 있는 물체 분리

팽창

dilated = cv2.dilate(
    binary,
    kernel,
    iterations=1
)

흰색 영역을 확장합니다.

끊어진 부분 연결

작은 구멍 축소

열기

침식 후 팽창합니다.

opened = cv2.morphologyEx(
    binary,
    cv2.MORPH_OPEN,
    kernel
)

작은 흰색 노이즈 제거에 활용할 수 있습니다.

닫기

팽창 후 침식합니다.

closed = cv2.morphologyEx(
    binary,
    cv2.MORPH_CLOSE,
    kernel
)

흰색 물체 내부의 작은 검은 구멍이나 끊어진 부분을 메우는 데 활용할 수 있습니다.

모폴로지 연산은 침식과 팽창을 기초로 하며 OpenCV는 `erode()`, `dilate()`, `morphologyEx()` 같은 함수를 제공합니다.

29. Canny 엣지 검출

엣지는 픽셀 밝기가 크게 변하는 경계입니다.

물체와 배경의 경계

글자의 획

건물의 외곽선

도로 차선

Canny 엣지 검출은 노이즈 감소, 밝기 기울기 계산, 얇은 경계 선택, 두 임계값을 이용한 연결 과정을 거치는 대표적인 다단계 엣지 검출 알고리즘입니다.

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

blurred = cv2.GaussianBlur(
    gray,
    (
        5,
        5
    ),
    0
)

edges = cv2.Canny(
    blurred,
    threshold1=50,
    threshold2=150
)

두 임계값

threshold1:
낮은 임계값

threshold2:
높은 임계값

너무 낮으면 노이즈가 많이 검출될 수 있습니다.

너무 높으면 중요한 경계가 사라질 수 있습니다.

낮은 기준:
동네의 모든 담장을 국경으로 인식

높은 기준:
진짜 국경도 못 찾음

이미지 특성에 맞게 값을 조정해야 합니다.

30. 윤곽선 검출

윤곽선은 같은 색상이나 밝기를 가진 객체의 외곽 경계를 연결한 점들의 집합입니다.

일반적으로 이진 이미지에서 흰색 물체의 윤곽선을 찾습니다.

contours, hierarchy = (
    cv2.findContours(
        binary,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )
)

print(
    f"윤곽선 개수: {len(contours)}"
)

OpenCV의 `findContours()`는 이진 이미지에서 윤곽선을 찾으며 검색 모드와 점 저장 방식을 선택할 수 있습니다.

RETR_EXTERNAL

가장 바깥쪽 윤곽선만 찾습니다.

cv2.RETR_EXTERNAL

RETR_TREE

윤곽선의 계층 구조를 함께 찾습니다.

cv2.RETR_TREE

CHAIN_APPROX_SIMPLE

직선 구간의 불필요한 중간점을 줄여 저장합니다.

cv2.CHAIN_APPROX_SIMPLE

윤곽선 그리기

result = image.copy()

cv2.drawContours(
    result,
    contours,
    contourIdx=-1,
    color=(
        0,
        255,
        0
    ),
    thickness=2
)

`contourIdx=-1`은 모든 윤곽선을 그린다는 뜻입니다.

31. 윤곽선 면적과 경계 상자

면적

for contour in contours:
    area = cv2.contourArea(
        contour
    )

    print(area)

작은 노이즈를 제외할 수 있습니다.

filtered_contours = []

for contour in contours:
    area = cv2.contourArea(
        contour
    )

    if area >= 500:
        filtered_contours.append(
            contour
        )

축에 평행한 경계 상자

for contour in filtered_contours:
    x, y, width, height = (
        cv2.boundingRect(
            contour
        )
    )

    cv2.rectangle(
        result,
        (
            x,
            y
        ),
        (
            x + width,
            y + height
        ),
        (
            0,
            255,
            0
        ),
        thickness=2
    )

중심점 계산

moments = cv2.moments(
    contour
)

if moments["m00"] != 0:
    center_x = int(
        moments["m10"]
        / moments["m00"]
    )

    center_y = int(
        moments["m01"]
        / moments["m00"]
    )

중심 표시:

cv2.circle(
    result,
    (
        center_x,
        center_y
    ),
    radius=5,
    color=(
        0,
        0,
        255
    ),
    thickness=-1
)

32. 도형 모양 분류하기

윤곽선을 단순화해 꼭짓점 수를 계산할 수 있습니다.

for contour in filtered_contours:
    perimeter = cv2.arcLength(
        contour,
        closed=True
    )

    approximation = (
        cv2.approxPolyDP(
            contour,
            epsilon=0.02 * perimeter,
            closed=True
        )
    )

    vertex_count = len(
        approximation
    )

간단한 분류:

if vertex_count == 3:
    shape_name = "Triangle"

elif vertex_count == 4:
    shape_name = "Rectangle"

elif vertex_count >= 8:
    shape_name = "Circle"

else:
    shape_name = "Polygon"

경계 상자 위에 이름 표시:

x, y, width, height = (
    cv2.boundingRect(
        approximation
    )
)

cv2.putText(
    result,
    shape_name,
    (
        x,
        max(
            20,
            y - 10
        )
    ),
    cv2.FONT_HERSHEY_SIMPLEX,
    0.7,
    (
        0,
        0,
        255
    ),
    thickness=2
)

실제 이미지에서는 원근, 노이즈, 그림자 때문에 꼭짓점 수만으로 정확히 분류하기 어렵습니다.

교과서 속 삼각형은 얌전하지만 현실의 삼각형은 구겨지고, 기울고, 조명까지 받습니다.

33. 이미지 히스토그램

히스토그램은 픽셀 밝기나 색상 값이 얼마나 분포하는지 보여 줍니다.

흑백 히스토그램

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

histogram = cv2.calcHist(
    [
        gray
    ],
    channels=[
        0
    ],
    mask=None,
    histSize=[
        256
    ],
    ranges=[
        0,
        256
    ]
)

Matplotlib로 출력:

import matplotlib.pyplot as plt

plt.plot(
    histogram
)

plt.xlim(
    0,
    256
)

plt.xlabel(
    "Pixel value"
)

plt.ylabel(
    "Frequency"
)

plt.title(
    "Grayscale Histogram"
)

plt.grid(
    alpha=0.3
)

plt.show()

해석

왼쪽에 집중
→ 어두운 이미지

오른쪽에 집중
→ 밝은 이미지

좁은 범위에 집중
→ 대비가 낮을 가능성

넓게 분포
→ 대비가 높은 가능성

히스토그램은 사진의 분위기를 보지 못하지만 밝기 예산이 어디에 사용됐는지는 정확히 보고합니다.

34. 밝기와 대비 조절

convertScaleAbs

adjusted = cv2.convertScaleAbs(
    image,
    alpha=1.2,
    beta=20
)

개념:

결과
=
원본 × alpha
+ beta
alpha:
대비

beta:
밝기

밝기 증가

brighter = cv2.convertScaleAbs(
    image,
    alpha=1.0,
    beta=30
)

대비 증가

higher_contrast = (
    cv2.convertScaleAbs(
        image,
        alpha=1.5,
        beta=0
    )
)

히스토그램 평활화

흑백 이미지:

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

equalized = cv2.equalizeHist(
    gray
)

밝기 분포를 재조정해 대비를 개선할 수 있습니다.

다만 너무 강하게 적용하면 노이즈도 강조될 수 있습니다.

35. 동영상과 웹캠 읽기

OpenCV의 `VideoCapture`는 카메라, 동영상 파일, 이미지 시퀀스 등에서 프레임을 읽는 인터페이스를 제공합니다. 영상은 프레임 단위로 반복 처리합니다.

웹캠 열기

import cv2


capture = cv2.VideoCapture(
    0
)

if not capture.isOpened():
    raise RuntimeError(
        "카메라를 열 수 없습니다."
    )

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        print(
            "프레임을 읽지 못했습니다."
        )
        break

    cv2.imshow(
        "Webcam",
        frame
    )

    key = cv2.waitKey(1)

    if key == ord("q"):
        break

capture.release()
cv2.destroyAllWindows()

VideoCapture 번호

0:
기본 카메라

1:
두 번째 카메라

2:
세 번째 카메라

동영상 파일

capture = cv2.VideoCapture(
    "video.mp4"
)

프레임 처리

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        break

    gray = cv2.cvtColor(
        frame,
        cv2.COLOR_BGR2GRAY
    )

    cv2.imshow(
        "Gray Video",
        gray
    )

    if cv2.waitKey(1) == ord("q"):
        break

36. 영상 프레임 저장하기

키를 눌렀을 때 현재 프레임을 저장할 수 있습니다.

from datetime import datetime

import cv2


capture = cv2.VideoCapture(
    0
)

if not capture.isOpened():
    raise RuntimeError(
        "카메라를 열 수 없습니다."
    )

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        break

    cv2.imshow(
        "Camera",
        frame
    )

    key = cv2.waitKey(1)

    if key == ord("s"):
        timestamp = datetime.now().strftime(
            "%Y%m%d_%H%M%S"
        )

        file_name = (
            f"capture_{timestamp}.jpg"
        )

        cv2.imwrite(
            file_name,
            frame
        )

        print(
            f"저장 완료: {file_name}"
        )

    elif key == ord("q"):
        break

capture.release()
cv2.destroyAllWindows()
s:
현재 화면 저장

q:
종료

37. 동영상 파일 저장하기

카메라 영상을 파일로 저장해 보겠습니다.

import cv2


capture = cv2.VideoCapture(
    0
)

if not capture.isOpened():
    raise RuntimeError(
        "카메라를 열 수 없습니다."
    )

width = int(
    capture.get(
        cv2.CAP_PROP_FRAME_WIDTH
    )
)

height = int(
    capture.get(
        cv2.CAP_PROP_FRAME_HEIGHT
    )
)

fps = capture.get(
    cv2.CAP_PROP_FPS
)

if fps <= 0:
    fps = 30.0

fourcc = cv2.VideoWriter_fourcc(
    *"mp4v"
)

writer = cv2.VideoWriter(
    "recorded_video.mp4",
    fourcc,
    fps,
    (
        width,
        height
    )
)

if not writer.isOpened():
    capture.release()

    raise RuntimeError(
        "VideoWriter를 열 수 없습니다."
    )

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        break

    writer.write(
        frame
    )

    cv2.imshow(
        "Recording",
        frame
    )

    if cv2.waitKey(1) == ord("q"):
        break

capture.release()
writer.release()
cv2.destroyAllWindows()

코덱 지원 여부는 운영체제와 OpenCV 빌드에 따라 달라질 수 있습니다.

파일이 생성되지만 재생되지 않는다면 다음을 확인합니다.

코덱

파일 확장자

프레임 크기

FPS

VideoWriter 열기 성공 여부

38. 실전 프로젝트: 움직임 감지 카메라

웹캠 영상에서 움직이는 영역을 찾아 경계 상자를 그려 보겠습니다.

원리

이전 프레임과 현재 프레임 비교

→ 차이가 큰 픽셀 찾기

→ 이진화

→ 노이즈 제거

→ 윤곽선 검출

→ 큰 영역에 경계 상자 표시

전체 코드

from datetime import datetime
from pathlib import Path

import cv2
import numpy as np


OUTPUT_DIR = Path(
    "motion_captures"
)

OUTPUT_DIR.mkdir(
    parents=True,
    exist_ok=True
)


def preprocess_frame(
    frame: np.ndarray
) -> np.ndarray:
    gray = cv2.cvtColor(
        frame,
        cv2.COLOR_BGR2GRAY
    )

    blurred = cv2.GaussianBlur(
        gray,
        (
            21,
            21
        ),
        0
    )

    return blurred


def save_motion_frame(
    frame: np.ndarray
) -> Path:
    timestamp = datetime.now().strftime(
        "%Y%m%d_%H%M%S_%f"
    )

    file_path = OUTPUT_DIR / (
        f"motion_{timestamp}.jpg"
    )

    success = cv2.imwrite(
        str(file_path),
        frame
    )

    if not success:
        raise IOError(
            f"이미지를 저장하지 못했습니다: "
            f"{file_path}"
        )

    return file_path


def main() -> None:
    capture = cv2.VideoCapture(
        0
    )

    if not capture.isOpened():
        raise RuntimeError(
            "카메라를 열 수 없습니다."
        )

    success, first_frame = (
        capture.read()
    )

    if not success:
        capture.release()

        raise RuntimeError(
            "첫 프레임을 읽지 못했습니다."
        )

    previous_frame = preprocess_frame(
        first_frame
    )

    minimum_area = 1500

    save_interval_seconds = 2.0

    last_saved_time = 0.0

    print(
        "움직임 감지를 시작합니다."
    )

    print(
        "q 키를 누르면 종료합니다."
    )

    while True:
        success, frame = (
            capture.read()
        )

        if not success:
            print(
                "프레임을 읽지 못했습니다."
            )
            break

        current_frame = preprocess_frame(
            frame
        )

        difference = cv2.absdiff(
            previous_frame,
            current_frame
        )

        _, threshold = cv2.threshold(
            difference,
            25,
            255,
            cv2.THRESH_BINARY
        )

        kernel = np.ones(
            (
                3,
                3
            ),
            dtype=np.uint8
        )

        threshold = cv2.morphologyEx(
            threshold,
            cv2.MORPH_OPEN,
            kernel,
            iterations=1
        )

        threshold = cv2.dilate(
            threshold,
            kernel,
            iterations=2
        )

        contours, _ = (
            cv2.findContours(
                threshold,
                cv2.RETR_EXTERNAL,
                cv2.CHAIN_APPROX_SIMPLE
            )
        )

        motion_detected = False

        for contour in contours:
            area = cv2.contourArea(
                contour
            )

            if area < minimum_area:
                continue

            x, y, width, height = (
                cv2.boundingRect(
                    contour
                )
            )

            cv2.rectangle(
                frame,
                (
                    x,
                    y
                ),
                (
                    x + width,
                    y + height
                ),
                (
                    0,
                    0,
                    255
                ),
                thickness=2
            )

            motion_detected = True

        status_text = (
            "MOTION DETECTED"
            if motion_detected
            else "NO MOTION"
        )

        status_color = (
            (
                0,
                0,
                255
            )
            if motion_detected
            else (
                0,
                255,
                0
            )
        )

        cv2.putText(
            frame,
            status_text,
            (
                20,
                40
            ),
            cv2.FONT_HERSHEY_SIMPLEX,
            1.0,
            status_color,
            thickness=2,
            lineType=cv2.LINE_AA
        )

        current_time = (
            cv2.getTickCount()
            / cv2.getTickFrequency()
        )

        if (
            motion_detected
            and current_time
            - last_saved_time
            >= save_interval_seconds
        ):
            saved_path = save_motion_frame(
                frame
            )

            print(
                f"움직임 화면 저장: "
                f"{saved_path}"
            )

            last_saved_time = (
                current_time
            )

        cv2.imshow(
            "Motion Detection",
            frame
        )

        cv2.imshow(
            "Motion Mask",
            threshold
        )

        previous_frame = current_frame

        if cv2.waitKey(1) == ord("q"):
            break

    capture.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

1단계: 흑백과 블러

gray = cv2.cvtColor(
    frame,
    cv2.COLOR_BGR2GRAY
)

blurred = cv2.GaussianBlur(
    gray,
    (
        21,
        21
    ),
    0
)

색상 정보가 필요하지 않으므로 흑백으로 변환합니다.

작은 노이즈에 민감하게 반응하지 않도록 블러를 적용합니다.

2단계: 프레임 차이

difference = cv2.absdiff(
    previous_frame,
    current_frame
)

두 프레임 사이의 절댓값 차이를 계산합니다.

변화가 없는 영역은 어둡게 나타나고 변화가 큰 영역은 밝게 나타납니다.

3단계: 이진화

_, threshold = cv2.threshold(
    difference,
    25,
    255,
    cv2.THRESH_BINARY
)

차이가 일정 수준 이상인 픽셀만 흰색으로 만듭니다.

4단계: 노이즈 정리

threshold = cv2.morphologyEx(
    threshold,
    cv2.MORPH_OPEN,
    kernel
)

작은 노이즈를 줄입니다.

threshold = cv2.dilate(
    threshold,
    kernel,
    iterations=2
)

서로 가까운 움직임 영역을 연결합니다.

5단계: 큰 윤곽선 선택

if area < minimum_area:
    continue

작은 빛 변화나 카메라 노이즈를 움직임으로 판단하지 않도록 최소 면적을 설정합니다.

한계

이 방식은 단순한 프레임 차이 방식입니다.

다음 상황에서 오탐지가 발생할 수 있습니다.

카메라 자체가 흔들림

조명이 갑자기 변함

화면 전체에 그림자가 이동

나뭇잎이나 커튼이 움직임

자동 노출이 변경됨

움직임은 찾았지만 그것이 사람인지 고양이인지 택배 상자인지는 알 수 없습니다.

그 판단에는 객체 검출 모델을 추가할 수 있습니다.

39. Haar Cascade 얼굴 검출

OpenCV는 사전 학습된 Haar Cascade 분류기 파일을 이용해 얼굴을 검출할 수 있습니다.

PyPI OpenCV 패키지에는 Haar Cascade 파일이 포함되어 있으며 `cv2.data.haarcascades` 경로를 통해 접근할 수 있습니다.

얼굴 분류기 불러오기

import cv2


cascade_path = (
    cv2.data.haarcascades
    + "haarcascade_frontalface_default.xml"
)

face_cascade = (
    cv2.CascadeClassifier(
        cascade_path
    )
)

if face_cascade.empty():
    raise RuntimeError(
        "얼굴 검출 모델을 "
        "불러오지 못했습니다."
    )

이미지에서 얼굴 검출

image = cv2.imread(
    "people.jpg"
)

if image is None:
    raise FileNotFoundError(
        "people.jpg 파일을 확인하세요."
    )

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

faces = face_cascade.detectMultiScale(
    gray,
    scaleFactor=1.1,
    minNeighbors=5,
    minSize=(
        30,
        30
    )
)

print(
    f"검출된 얼굴: {len(faces)}개"
)

경계 상자 그리기

for x, y, width, height in faces:
    cv2.rectangle(
        image,
        (
            x,
            y
        ),
        (
            x + width,
            y + height
        ),
        (
            0,
            255,
            0
        ),
        thickness=2
    )

Haar Cascade 방식은 여러 크기의 탐색 창을 이용해 학습된 특징에 맞는 영역을 찾습니다. OpenCV 공식 튜토리얼은 `CascadeClassifier`를 이용한 얼굴과 눈 검출 예제를 제공합니다.

웹캠 얼굴 검출

import cv2


cascade_path = (
    cv2.data.haarcascades
    + "haarcascade_frontalface_default.xml"
)

face_cascade = (
    cv2.CascadeClassifier(
        cascade_path
    )
)

capture = cv2.VideoCapture(
    0
)

if not capture.isOpened():
    raise RuntimeError(
        "카메라를 열 수 없습니다."
    )

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        break

    gray = cv2.cvtColor(
        frame,
        cv2.COLOR_BGR2GRAY
    )

    faces = face_cascade.detectMultiScale(
        gray,
        scaleFactor=1.1,
        minNeighbors=5,
        minSize=(
            40,
            40
        )
    )

    for x, y, width, height in faces:
        cv2.rectangle(
            frame,
            (
                x,
                y
            ),
            (
                x + width,
                y + height
            ),
            (
                0,
                255,
                0
            ),
            thickness=2
        )

    cv2.putText(
        frame,
        f"Faces: {len(faces)}",
        (
            20,
            40
        ),
        cv2.FONT_HERSHEY_SIMPLEX,
        1.0,
        (
            0,
            255,
            0
        ),
        thickness=2
    )

    cv2.imshow(
        "Face Detection",
        frame
    )

    if cv2.waitKey(1) == ord("q"):
        break

capture.release()
cv2.destroyAllWindows()

Haar Cascade의 한계

얼굴 각도가 크게 돌아감

조명이 매우 어두움

얼굴 일부가 가려짐

얼굴 크기가 너무 작음

복잡한 배경

최근의 복잡한 얼굴 검출 문제에서는 딥러닝 기반 검출 모델이 더 강력할 수 있습니다.

Haar Cascade는 얼굴을 인식하는 것이 아니라 얼굴처럼 보이는 위치를 검출합니다.

얼굴 검출:
사람 얼굴이 어디에 있는가?

얼굴 인식:
이 얼굴이 누구인가?

두 작업은 다릅니다.

40. OpenCV와 PyTorch 연결하기

OpenCV로 읽은 이미지를 PyTorch 모델에 전달할 수 있습니다.

OpenCV 이미지 준비

import cv2
import torch


image = cv2.imread(
    "sample.jpg"
)

if image is None:
    raise FileNotFoundError(
        "sample.jpg 파일을 확인하세요."
    )

rgb = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

resized = cv2.resize(
    rgb,
    (
        224,
        224
    )
)

텐서 변환

tensor = torch.from_numpy(
    resized
)

현재 형태:

높이 × 너비 × 채널

PyTorch 이미지 모델은 주로 다음 형태를 사용합니다.

채널 × 높이 × 너비

차원 순서를 변경합니다.

tensor = tensor.permute(
    2,
    0,
    1
)

실수형으로 변환하고 정규화합니다.

tensor = (
    tensor.float()
    / 255.0
)

배치 차원을 추가합니다.

tensor = tensor.unsqueeze(
    0
)

최종 형태:

1 × 3 × 224 × 224

장치 이동과 추론

tensor = tensor.to(
    device
)

model.eval()

with torch.inference_mode():
    logits = model(
        tensor
    )

    prediction = logits.argmax(
        dim=1
    )

전체 흐름:

OpenCV BGR 이미지

→ RGB 변환

→ 크기 변경

→ NumPy 배열

→ PyTorch 텐서

→ HWC에서 CHW 변환

→ 배치 차원 추가

→ 모델 추론

이 과정에서 색상 순서와 텐서 차원을 잘못 처리하면 모델 성능이 크게 떨어질 수 있습니다.

모델은 말없이 예측을 내놓기 때문에 더욱 위험합니다.

41. OpenCV 성능 최적화

필요한 크기로 먼저 축소한다

4K 영상을 그대로 처리할 필요가 없다면 작은 크기로 줄일 수 있습니다.

small_frame = cv2.resize(
    frame,
    (
        640,
        360
    ),
    interpolation=cv2.INTER_AREA
)

픽셀 수가 감소하면 처리량도 크게 줄어듭니다.

필요한 영역만 처리한다

roi = frame[
    100:500,
    200:800
]

전체 화면 대신 관심 영역만 분석합니다.

필요 없는 색상 채널을 줄인다

색상이 필요하지 않은 작업:

gray = cv2.cvtColor(
    frame,
    cv2.COLOR_BGR2GRAY
)

모든 프레임을 처리하지 않는다

frame_index = 0

while True:
    success, frame = (
        capture.read()
    )

    if not success:
        break

    frame_index += 1

    if frame_index % 3 != 0:
        continue

    # 세 프레임마다 한 번 분석

단, 빠르게 움직이는 물체를 놓칠 수 있습니다.

Python 반복문보다 배열 연산을 사용한다

느린 방식:

for y in range(height):
    for x in range(width):
        image[y, x] = ...

빠른 방식:

image[
    image > 127
] = 255

NumPy와 OpenCV의 벡터화 연산을 활용합니다.

읽기와 분석을 분리한다

고속 영상에서는 다음 구조를 검토할 수 있습니다.

스레드 1:
카메라 프레임 읽기

스레드 2:
영상 분석

스레드 3:
결과 저장

다만 멀티스레딩에서는 프레임 동기화와 종료 처리가 복잡해집니다.

먼저 단일 루프로 정확성을 확보한 뒤 성능을 측정해야 합니다.

42. OpenCV 사용 시 주의점

BGR과 RGB를 구분한다

OpenCV:
BGR

Matplotlib·Pillow·일반 딥러닝 모델:
RGB인 경우가 많음

좌표와 배열 순서를 구분한다

점 좌표:
(x, y)

배열 접근:
image[y, x]

resize 크기:
(width, height)

image.shape:
(height, width, channels)

OpenCV 초보자에게 제공되는 네 방향 회전 퍼즐입니다.

이미지 로딩 결과를 확인한다

if image is None:
    ...

`None` 상태에서 `.shape`를 호출하면 오류가 발생합니다.

원본을 보존한다

result = image.copy()

그리기 함수는 원본 배열을 직접 변경할 수 있습니다.

uint8 연산을 주의한다

`uint8` 범위는 0부터 255입니다.

NumPy에서 직접 더하면 값이 넘칠 수 있습니다.

import numpy as np

value = np.uint8(250)

print(
    value + np.uint8(20)
)

OpenCV의 `cv2.add()`는 포화 연산을 수행합니다.

result = cv2.add(
    image,
    50
)

카메라와 파일 자원을 해제한다

capture.release()
writer.release()
cv2.destroyAllWindows()

개인정보와 촬영 권한을 확인한다

얼굴, 차량 번호판, 사내 영상, CCTV 데이터를 처리할 때는 다음을 검토해야 합니다.

촬영 동의

수집 목적

접근 권한

보관 기간

암호화

개인정보 비식별화

관련 법과 내부 정책

얼굴 검출과 신원 확인을 혼동하지 않는다

얼굴을 찾는 것과 특정 인물인지 판단하는 것은 다른 문제입니다.

알고리즘 결과를 사실로 단정하지 않는다

윤곽선이 검출됨
≠
실제 물체가 존재한다고 확정

움직임이 검출됨
≠
사람이 침입했다고 확정

얼굴이 검출됨
≠
항상 진짜 얼굴

OpenCV는 픽셀 패턴을 계산합니다.

그 결과를 어떤 의미로 사용할지는 사람이 판단해야 합니다.

43. 자주 발생하는 오류

오류 1. cv2 모듈을 찾을 수 없음

ModuleNotFoundError:
No module named 'cv2'

설치:

python -m pip install opencv-python

환경 확인:

python -m pip show opencv-python
python -c "import cv2; print(cv2.__version__)"

오류 2. 이미지가 None임

AttributeError:
'NoneType' object has no attribute 'shape'

원인:

잘못된 경로

파일이 없음

지원되지 않는 형식

파일 손상

한글 경로 문제

확인:

from pathlib import Path

path = Path(
    "sample.jpg"
)

print(
    path.resolve()
)

print(
    path.exists()
)

오류 3. imshow가 동작하지 않음

The function is not implemented

가능한 원인:

headless 패키지 설치

GUI가 없는 서버

Docker 환경

원격 세션

패키지 확인:

python -m pip list

충돌 패키지를 제거합니다.

python -m pip uninstall \
    opencv-python \
    opencv-contrib-python \
    opencv-python-headless \
    opencv-contrib-python-headless

그다음 필요한 패키지 하나만 다시 설치합니다.

python -m pip install opencv-python

오류 4. Matplotlib 색상이 이상함

BGR을 RGB로 변환합니다.

rgb = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

오류 5. resize 결과가 찌그러짐

원본 비율을 유지해 크기를 계산합니다.

scale = target_width / width

target_height = round(
    height * scale
)

오류 6. 카메라를 열 수 없음

카메라가 다른 프로그램에서 사용 중

잘못된 장치 번호

브라우저·운영체제 권한 차단

원격 환경에 카메라 없음

다른 번호를 확인합니다.

for camera_index in range(5):
    capture = cv2.VideoCapture(
        camera_index
    )

    print(
        camera_index,
        capture.isOpened()
    )

    capture.release()

오류 7. 동영상이 저장되지 않음

확인:

print(
    writer.isOpened()
)

프레임 크기가 VideoWriter 설정과 일치해야 합니다.

print(
    frame.shape
)
frame.shape:
(height, width, channels)

VideoWriter:
(width, height)

오류 8. findContours 반환값 오류

현대 OpenCV Python에서는 일반적으로 다음 형태를 사용합니다.

contours, hierarchy = (
    cv2.findContours(
        binary,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )
)

아주 오래된 예제는 반환값 개수가 다를 수 있습니다.

인터넷 예제를 사용할 때는 작성된 OpenCV 버전을 확인해야 합니다.

오류 9. 윤곽선이 하나도 검출되지 않음

확인할 내용:

전경이 흰색인가?

배경이 검은색인가?

임계값이 적절한가?

이미지가 흑백인가?

노이즈가 너무 심한가?

결과 이미지를 중간 단계마다 출력합니다.

cv2.imshow(
    "Gray",
    gray
)

cv2.imshow(
    "Binary",
    binary
)

오류 10. Canny 결과가 너무 많음

노이즈가 많음

임계값이 너무 낮음

블러가 부족함
blurred = cv2.GaussianBlur(
    gray,
    (
        5,
        5
    ),
    0
)

edges = cv2.Canny(
    blurred,
    80,
    180
)

오류 11. 얼굴을 너무 많이 잘못 검출함

`minNeighbors`를 높여 봅니다.

faces = face_cascade.detectMultiScale(
    gray,
    scaleFactor=1.1,
    minNeighbors=7,
    minSize=(
        50,
        50
    )
)

일반적으로 `minNeighbors`를 높이면 검출은 엄격해지지만 실제 얼굴을 놓칠 수도 있습니다.

오류 12. 반복문이 너무 느림

픽셀별 Python 반복문을 피합니다.

# 느릴 수 있음
for y in range(height):
    for x in range(width):
        ...

OpenCV나 NumPy 연산을 사용합니다.

mask = image > 127

44. 연습 문제

문제 1

`sample.jpg`를 컬러로 불러오고 높이, 너비, 채널 수를 출력하세요.

문제 2

이미지를 흑백으로 변환해 `gray.jpg`로 저장하세요.

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

문제 3

이미지의 가로 크기를 800픽셀로 변경하되 원본 비율을 유지하세요.

문제 4

이미지의 좌표 `(100, 50)`부터 너비 300, 높이 200인 영역을 잘라 저장하세요.

문제 5

이미지를 좌우로 반전하세요.

flipped = cv2.flip(
    image,
    1
)

문제 6

이미지에 빨간색 사각형과 `Object`라는 글자를 표시하세요.

문제 7

HSV 색상 공간을 이용해 초록색 물체만 추출하세요.

문제 8

가우시안 블러를 적용한 뒤 Canny 엣지를 검출하세요.

blurred = cv2.GaussianBlur(
    gray,
    (
        5,
        5
    ),
    0
)

edges = cv2.Canny(
    blurred,
    50,
    150
)

문제 9

Otsu 방식으로 이미지를 이진화하고 선택된 임계값을 출력하세요.

문제 10

이진 이미지에 열기 연산을 적용해 작은 흰색 노이즈를 제거하세요.

문제 11

윤곽선 중 면적이 1,000 이상인 물체에만 경계 상자를 표시하세요.

문제 12

웹캠 영상을 흑백으로 변환해 실시간 출력하고 `q` 키로 종료하세요.

문제 13

웹캠에서 `s` 키를 누르면 현재 프레임을 저장하도록 만드세요.

문제 14

Haar Cascade를 이용해 이미지에서 얼굴 영역을 검출하세요.

문제 15

OpenCV 이미지를 PyTorch 입력 형태인 다음 구조로 변환하세요.

1 × 3 × 224 × 224

45. 핵심 요약

설치

python -m pip install opencv-python

불러오기

import cv2

이미지 읽기

image = cv2.imread(
    "sample.jpg"
)

이미지 저장

cv2.imwrite(
    "result.jpg",
    image
)

이미지 출력

cv2.imshow(
    "Image",
    image
)

cv2.waitKey(0)
cv2.destroyAllWindows()

흑백 변환

gray = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2GRAY
)

RGB 변환

rgb = cv2.cvtColor(
    image,
    cv2.COLOR_BGR2RGB
)

크기 변경

resized = cv2.resize(
    image,
    (
        width,
        height
    )
)

자르기

cropped = image[
    y1:y2,
    x1:x2
]

뒤집기

flipped = cv2.flip(
    image,
    1
)

블러

blurred = cv2.GaussianBlur(
    image,
    (
        5,
        5
    ),
    0
)

이진화

_, binary = cv2.threshold(
    gray,
    127,
    255,
    cv2.THRESH_BINARY
)

Canny 엣지

edges = cv2.Canny(
    gray,
    50,
    150
)

윤곽선

contours, hierarchy = (
    cv2.findContours(
        binary,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )
)

웹캠

capture = cv2.VideoCapture(
    0
)

프레임 읽기

success, frame = (
    capture.read()
)

자원 해제

capture.release()
cv2.destroyAllWindows()

46. 마무리

이번 시간에는 OpenCV를 이용해 이미지와 영상을 Python에서 직접 분석하는 방법을 알아보았습니다.

OpenCV 작업의 기본 흐름은 다음과 같습니다.

이미지 또는 영상 읽기

→ 크기와 색상 변환

→ 노이즈 제거

→ 필요한 특징 추출

→ 물체 영역 분석

→ 결과 표시

→ 이미지 또는 영상 저장

이미지는 NumPy 배열이었습니다.

컬러 이미지:
높이 × 너비 × 채널

흑백 이미지:
높이 × 너비

우리는 이 배열을 이용해 다음 작업을 수행했습니다.

픽셀 수정

이미지 자르기

크기 변경

회전과 반전

색상 검출

이진화

모폴로지 연산

엣지 검출

윤곽선 분석

웹캠 처리

움직임 감지

얼굴 검출

OpenCV 코드의 중심에는 몇 가지 반복되는 질문이 있습니다.

현재 이미지의 shape는 무엇인가?

색상 순서는 BGR인가 RGB인가?

배열 접근 순서는 y, x인가?

전경은 흰색이고 배경은 검은색인가?

임계값이 현재 환경에 적절한가?

중간 처리 결과는 어떻게 생겼는가?

영상 처리 코드가 원하는 대로 동작하지 않을 때 마지막 결과만 바라보지 마세요.

중간 결과를 하나씩 출력해야 합니다.

원본

→ 흑백

→ 블러

→ 이진화

→ 모폴로지

→ 엣지

→ 윤곽선

컴퓨터 비전 디버깅은 수사와 비슷합니다.

최종 범인만 찾으려고 하면 사건이 꼬이지만, 증거물을 단계별로 확인하면 문제가 발생한 지점을 발견할 수 있습니다.

개발자:
“윤곽선이 왜 안 나오지?”

OpenCV:
“이진 이미지가 전부 검은색입니다.”

개발자:
“그 사실을 왜 이제 말해요?”

OpenCV:
“출력해 보지 않으셨잖아요.”

OpenCV는 사진을 사람처럼 감상하지 않습니다.

하지만 픽셀을 누구보다 성실하게 검사합니다.

픽셀의 색상, 밝기, 경계, 움직임을 계산하고 우리가 원하는 조건에 맞는 영역을 찾아냅니다.

그리고 OpenCV의 진짜 힘은 딥러닝과 연결했을 때 더욱 커집니다.

OpenCV
→ 영상을 읽고 전처리

PyTorch
→ 객체를 판단

OpenCV
→ 경계 상자와 결과 표시

OpenCV
→ 영상 저장

이제 이미지 파일이 등장하면 단순한 그림으로만 보지 마세요.

그 안에는 높이, 너비, 채널, 픽셀값, 경계, 윤곽선이라는 거대한 숫자 도시가 숨어 있습니다. 🏙️📷

OpenCV는 그 도시를 탐험할 수 있는 지도입니다.

다음 편 예고

[Python 완전정복 시리즈 #32] MediaPipe 완벽 이해하기 | 웹캠으로 얼굴·손·자세를 실시간 추적하는 방법

다음 시간에는 OpenCV로 읽은 웹캠 영상에서 사람의 손가락, 얼굴 특징점, 전신 자세를 실시간으로 추적하는 방법을 알아봅니다.

손 랜드마크, 손가락 개수 판별, 얼굴 메시, 포즈 랜드마크, 관절 각도 계산, 운동 자세 분석까지 MediaPipe를 이용해 실습해 보겠습니다.

#Python #파이썬 #Python강좌 #파이썬기초 #OpenCV #컴퓨터비전 #ComputerVision #이미지처리 #영상처리 #NumPy #이미지분석 #웹캠 #동영상처리 #색상검출 #HSV #이진화 #임계처리 #Canny #엣지검출 #윤곽선검출 #움직임감지 #얼굴검출 #HaarCascade #PyTorch #딥러닝 #코딩공부 #프로그래밍

댓글

0

댓글을 불러오는 중입니다.