OpenCV
고친 사람 github-actions[bot]
OpenCV 는 프로그램이 사진과 영상을 읽고 분석하게 해 줍니다. 크기 바꾸기와 색 바꾸기부터 사진 속 얼굴 찾기까지 이미 짜인 함수로 부릅니다. 핵심은 C++ 로 짜여 있습니다. 파이썬에서도 같은 함수를 씁니다.
쉽고 빠른 이해
- 무슨 일을 하나 — 사진과 영상을 숫자 배열로 읽어 들입니다. 그 배열을 고치거나 분석하는 함수를 수백 개 줍니다. 사진을 흑백으로 바꾸고 윤곽선만 남기는 일이 함수 세 번 부르기로 끝납니다.
- 왜 있나 — 사진의 크기를 바꾸거나 윤곽을 찾는 계산은 직접 짜면 길고 틀리기 쉽습니다. 많이 쓰는 계산을 한곳에 모아 두었으니 가져다 부르면 됩니다.
- 어떻게 도나
- 대가 — 배열에 담을 수 있는 숫자의 종류가 8비트 정수나 64비트 실수처럼 몇 가지로 정해져 있습니다. 그리고 배열을 다른 변수에 넘겨도 픽셀 데이터는 복사되지 않습니다. 한 변수로 고치면 다른 변수에서도 바뀐 값이 보입니다.
- 언제 쓰나 — 사진 속 내용을 계산해야 할 때 씁니다. 크기만 줄이거나 파일 형식만 바꾸는 일이면 더 작은 이미지 라이브러리로도 됩니다.
상세
OpenCV 는 컴퓨터 비전 알고리즘 수백 가지를 묶은 오픈소스 라이브러리입니다. 이름은 Open Source Computer Vision Library 를 줄인 것입니다. 컴퓨터 비전은 컴퓨터가 이미지에서 뜻을 뽑아내는 일입니다. 사진에 무엇이 찍혔는지, 물체가 어디로 움직였는지를 알아내는 일이 여기 듭니다.
이런 일의 밑에는 픽셀 수십만 개를 도는 계산이 깔립니다. 사진을 흐리게 하거나 윤곽을 찾는 계산을 직접 짜면 길고 틀리기 쉽습니다. OpenCV 는 많이 쓰는 계산을 모아 함수로 내놓습니다. 쓰는 쪽은 가져다 부르기만 하면 됩니다.
이 절은 여덟 소절로 갑니다. 처음 둘은 밑바탕입니다. OpenCV 가 이미지를 어떤 모양으로 들고 있는지, 기능을 어떻게 나눴는지를 봅니다.
가운데 넷은 쓸 때 부딪히는 성질입니다. 사진 한 장을 고치는 짧은 코드를 따라간 뒤 색 순서, 배열 공유, 값이 넘칠 때의 처리를 봅니다. 마지막 둘은 다른 언어에서 부르는 법과 OpenCV 가 포기한 것입니다.
이미지를 숫자 배열로 다루는 법
이미지는 작은 점이 바둑판처럼 늘어선 것입니다. 이 점 하나를 픽셀이라고 부릅니다. 픽셀마다 밝기를 숫자로 적어 두면 이미지 한 장이 2차원 숫자 표가 됩니다.
흑백 이미지는 픽셀마다 숫자 하나면 됩니다. 컬러 이미지는 픽셀마다 숫자 셋을 둡니다. 파랑·초록·빨강의 세기를 따로 적습니다. 이 한 가닥 한 가닥을 색 채널이라고 부릅니다.
숫자 하나의 크기는 대개 8비트입니다. 그래서 값은 0에서 255 사이입니다. 0이 가장 어둡고 255가 가장 밝습니다.
OpenCV 는 이 숫자 표를 cv::Mat 라는 클래스에 담습니다. Mat 는 행렬(matrix)에서 온 이름입니다. 가로·세로뿐 아니라 그보다 많은 차원도 담는 배열입니다. 앞에 붙은 cv:: 는 OpenCV 의 모든 클래스와 함수가 들어 있는 네임스페이스입니다.
파이썬에서는 Mat 대신 NumPy 배열을 씁니다. NumPy 는 파이썬에서 숫자 배열을 빠르게 계산하는 라이브러리입니다. OpenCV 함수는 NumPy 배열을 받고 NumPy 배열을 돌려줍니다. 그래서 다른 NumPy 기반 라이브러리와 배열을 바로 주고받습니다.
기능을 모듈로 나눈 구조
OpenCV 는 한 덩어리가 아닙니다. 하는 일에 따라 여러 모듈로 나뉩니다. 모듈 하나가 라이브러리 파일 하나로 빌드됩니다. 쓰는 모듈만 골라 붙일 수 있습니다.
아래 표는 자주 만나는 모듈입니다. 왼쪽이 모듈 이름이고 오른쪽이 맡은 일입니다.
| 모듈 | 맡은 일 |
|---|---|
core |
Mat 같은 기본 자료형과 모든 모듈이 쓰는 기본 함수 |
imgproc |
이미지 필터링 · 크기 바꾸기와 회전 · 색 바꾸기 · 히스토그램 |
imgcodecs |
이미지 파일 읽기와 쓰기 |
videoio |
카메라와 동영상 파일에서 프레임 받기. 프레임은 영상을 이루는 낱장 이미지입니다 |
highgui |
창을 띄워 이미지를 보여 주는 간단한 화면 |
video |
움직임 추정 · 배경 차분 · 물체 따라가기 |
calib3d |
카메라 캘리브레이션 · 여러 시점의 사진으로 3차원 모양 되살리기 |
features2d |
사진에서 눈에 띄는 점을 찾고 서로 짝짓기 |
objdetect |
얼굴 · 사람 · 자동차처럼 정해진 종류의 물체 찾기 |
dnn |
이미 학습된 신경망 모델 돌리기 |
ml |
분류 · 회귀 · 군집 같은 통계적 머신러닝 |
모듈은 서로 남남이 아닙니다. 전부 core 위에 얹혀 core 의 Mat 을 주고받습니다. 이 표에 없는 추가 기능은 별도 저장소인 opencv_contrib 에 모여 있습니다.
사진 한 장을 고치는 흐름
OpenCV 를 쓰는 코드는 대개 같은 모양입니다. 파일을 읽습니다. 몇 단계를 거쳐 고칩니다. 결과를 파일로 씁니다. 아래 그림은 사진에서 윤곽선만 남기는 흐름입니다. 단계마다 어느 모듈의 함수인지 함께 적었습니다.
flowchart TD
A["사진 파일"] -->|"imgcodecs · 읽기"| B["컬러 배열 · 채널 셋"]
B -->|"imgproc · 흑백으로"| C["흑백 배열 · 채널 하나"]
C -->|"imgproc · 흐리게"| D["잡티가 줄어든 배열"]
D -->|"imgproc · 윤곽 찾기"| E["윤곽선만 남은 배열"]
E -->|"imgcodecs · 쓰기"| F["새 사진 파일"]
흐리게 만드는 단계가 끼는 까닭은 잡티입니다. 픽셀 하나가 튀면 윤곽 찾기가 그것을 선으로 잡습니다. 이웃 픽셀과 평균을 내 튀는 값을 눌러 둔 뒤에 윤곽을 찾습니다.
같은 흐름을 파이썬으로 쓰면 아래와 같습니다. 가로 640, 세로 480 짜리 컬러 사진을 넣었다고 칩니다. shape 줄의 주석이 그 배열의 모양입니다.
import cv2
img = cv2.imread("cat.jpg")
img.shape # (480, 640, 3)
gray = cv2.cvtColor(
img, cv2.COLOR_BGR2GRAY)
gray.shape # (480, 640)
blur = cv2.GaussianBlur(
gray, (7, 7), 1.5)
edges = cv2.Canny(blur, 0, 30)
cv2.imwrite("edges.png", edges)
shape 는 배열의 모양입니다. 행 수가 먼저 오고 열 수가 다음입니다. 그래서 세로 480 이 앞에 섭니다. 컬러 배열은 끝에 채널 수 3이 붙습니다. 흑백으로 바꾸면 그 칸이 사라집니다.
GaussianBlur 는 가까운 픽셀일수록 크게 쳐서 평균을 내는 흐리기입니다. 인자 (7, 7) 은 평균을 낼 이웃의 크기로, 가로 7칸 세로 7칸입니다. 1.5 는 흐린 정도이고 클수록 더 뭉개집니다.
Canny 는 밝기가 갑자기 바뀌는 곳을 선으로 남기는 윤곽선 검출 방법입니다. 인자 0 과 30 은 밝기가 얼마나 크게 바뀌어야 선으로 칠지 정하는 두 기준값입니다. GaussianBlur 와 Canny 둘 다 imgproc 에 듭니다.
크기를 바꾸는 cv2.resize 도 같은 모듈입니다. 새 크기의 픽셀은 원래 픽셀 사이에 떨어지므로 그 값을 이웃 픽셀에서 만들어 냅니다. 이 계산이 보간입니다. 어떤 보간을 쓸지는 인자로 고릅니다.
색 채널의 순서 BGR
OpenCV 가 읽은 컬러 이미지는 채널이 파랑·초록·빨강 순서로 놓입니다. 이 순서를 BGR(Blue-Green-Red, 파랑-초록-빨강)이라고 부릅니다.
Pillow 나 matplotlib 같은 다른 도구는 대개 빨강·초록·파랑 순서인 RGB(Red-Green-Blue)로 픽셀을 적습니다. 그래서 OpenCV 로 읽은 배열을 다른 라이브러리에 순서를 안 바꾸고 넘기면 빨강과 파랑이 뒤바뀐 사진이 나옵니다. 사람 얼굴이 파랗게 보이면 대개 이 탓입니다.
위 코드의 COLOR_BGR2GRAY 이름에도 이 순서가 들어 있습니다. BGR 배열을 흑백으로 바꾸라는 뜻입니다. Pillow 같은 도구로 넘기기 전에는 COLOR_BGR2RGB 로 순서를 바꿔 둡니다.
복사하지 않고 나눠 쓰는 배열
Mat 은 두 부분으로 됩니다. 크기와 자료형을 적은 작은 머리 부분과, 픽셀 값이 든 큰 데이터 덩어리입니다. Mat 을 다른 변수에 대입하면 머리만 복사됩니다. 데이터 덩어리는 둘이 같이 씁니다.
같이 쓰는 데이터를 언제 지울지는 참조 카운팅으로 정합니다. 참조 카운팅은 데이터를 가리키는 쪽이 몇인지 세어 두는 방법입니다. 가리키는 쪽이 하나 늘 때마다 1을 더합니다. 하나 사라질 때마다 1을 뺍니다. 0이 되면 아무도 안 쓰니 그때 메모리를 돌려줍니다.
아래 C++ 코드가 이 차이를 보입니다. 첫 줄은 1000행 1000열 배열 a 를 만듭니다. CV_64F 는 원소마다 64비트 실수를 담는다는 표시입니다. 오른쪽 주석이 그 줄에서 일어나는 일입니다.
Mat a(1000, 1000, CV_64F);
Mat b = a; // 머리만 복사
Mat c = a.row(3); // 넷째 행을 가리킴
Mat d = a.clone(); // 데이터까지 복사
b 와 c 는 a 의 데이터를 같이 씁니다. 그래서 b 를 고치면 a 와 c 에서도 바뀐 값이 보입니다. 따로 쓸 사본이 필요하면 clone() 을 부릅니다. d 는 a 와 데이터를 안 나눕니다.
이 방식 덕에 큰 이미지를 함수에 넘겨도 픽셀 데이터를 복사하는 비용이 들지 않습니다.
세는 숫자를 더하고 빼는 일은 원자적으로 합니다. 원자적이라는 말은 다른 스레드가 중간에 끼어들 수 없다는 뜻입니다. 같은 Mat 을 여러 스레드가 동시에 가리키거나 놓아도 세는 숫자는 어긋나지 않습니다.
다만 지켜지는 것은 세는 숫자뿐입니다. 여러 스레드가 같은 픽셀 값을 동시에 고치는 일은 이것과 별개입니다.
8비트 값이 넘칠 때
밝기를 올리거나 색을 바꾸는 계산은 결과가 0에서 255 밖으로 나가기 쉽습니다. 250 에 10 을 더하면 260 이 됩니다. 이 값을 8비트에 아래 비트만 남겨 담으면 4가 됩니다. 아주 밝던 픽셀이 갑자기 거의 검게 변합니다.
OpenCV 는 이런 값을 범위 끝에 붙입니다. 255 를 넘으면 255 로, 0 보다 작으면 0 으로 담습니다. 이것을 포화 연산(saturation arithmetic)이라고 부릅니다. 결과를 8비트 배열에 담는 OpenCV 함수는 이 규칙을 따릅니다.
C++ 에서는 saturate_cast 로 이 규칙을 직접 부를 수 있습니다.
saturate_cast<uchar>(260); // 255
saturate_cast<uchar>(-5); // 0
saturate_cast<uchar>(100); // 100
uchar 는 0에서 255 를 담는 8비트 부호 없는 정수입니다. 범위 안의 값은 손대지 않습니다. 밖의 값만 가까운 끝으로 붙입니다.
C++ 핵심과 다른 언어 바인딩
OpenCV 의 본체는 C++ 코드입니다. 파이썬과 자바에서 쓰는 OpenCV 는 이 C++ 코드를 감싼 바인딩입니다. 바인딩은 다른 언어로 짠 라이브러리를 내 언어에서 부를 수 있게 이어 주는 얇은 층입니다.
파이썬에서 cv2 로 불러 쓰는 패키지가 이것입니다. 함수를 부르면 계산은 뒤에서 C++ 코드가 합니다. 파이썬으로 짜도 픽셀을 도는 계산의 속도는 C++ 와 비슷합니다. 느려지는 것은 파이썬 반복문으로 픽셀을 하나씩 직접 돌 때입니다.
백엔드에서 OpenCV 를 만나는 경우는 대개 파이썬 쪽입니다. 사용자가 올린 사진에서 얼굴을 찾는 일이 그렇습니다. 머신러닝 모델에 넣기 전에 cv2.resize 로 크기를 맞추고 cv2.cvtColor 로 색 순서를 맞추는 일도 그렇습니다.
포기한 것
OpenCV 는 C++ 템플릿을 아껴 씁니다. 템플릿은 자료형만 바꿔 같은 코드를 여러 벌 찍어 내는 C++ 기능입니다. 마음껏 쓰면 컴파일이 느려지고 결과물이 커집니다. 템플릿이 없는 파이썬·자바로 바인딩을 만들기도 어려워집니다.
그래서 OpenCV 는 배열 원소가 될 수 있는 자료형을 몇 개로 못박았습니다. 8·16비트 정수, 32비트 정수, 32·64비트 실수가 기본입니다. 앞 코드의 CV_64F 가 이 가운데 64비트 실수입니다.
여기에 더해 여러 채널을 한 원소로 묶을 수 있습니다. 컬러 픽셀처럼 숫자 셋을 한 원소로 두는 경우입니다. 이보다 복잡한 원소를 담는 배열은 만들 수 없습니다.
함수마다 받는 자료형도 그중 일부로 좁혀집니다. 얼굴 찾기는 8비트 이미지만 받습니다. 선형대수 함수는 실수 배열만 받습니다.
OpenCV 는 이미 있는 이미지를 읽는 쪽입니다. 점·선·면의 좌표 같은 도형 정보를 받아 없던 그림을 픽셀로 그려 내는 일은 그래픽스의 몫입니다. OpenGL(Open Graphics Library) 같은 도구가 그 일을 맡습니다.
신경망 모델을 학습시키는 일도 OpenCV 의 일이 아닙니다. dnn 모듈은 다른 프레임워크에서 학습을 마친 모델을 받아 추론만 합니다. 추론은 모델에 새 입력을 넣어 답을 받는 일입니다.
이 경계에서 쓸 때와 안 쓸 때가 갈립니다. 이미지 안의 내용을 계산해야 하면 OpenCV 를 씁니다. 업로드된 사진의 크기만 줄이거나 파일 형식만 바꾸면 Pillow 같은 더 작은 이미지 라이브러리로도 됩니다.
관련 항목
OpenCV 가 속하는 분야
컴퓨터 비전 · 이미지 처리 · 영상 처리 · 머신러닝 · 딥러닝
OpenCV 가 다루는 이미지의 구성 요소
픽셀 · 색 채널 · 해상도 · 비트 깊이 · 색 공간 · RGB · 그레이스케일 · 프레임
OpenCV 가 제공하는 이미지 처리 방법
보간 · 필터링 · 가우시안 블러 · 윤곽선 검출 · 캐니 에지 검출 · 히스토그램 · 이진화 · 모폴로지 연산 · 아핀 변환 · 원근 변환
OpenCV 가 푸는 컴퓨터 비전 문제
객체 검출 · 얼굴 인식 · 특징 검출기 · 특징점 매칭 · 배경 차분 · 객체 추적 · 카메라 캘리브레이션 · 3D 재현 · 이미지 스티칭 · 광학 흐름
OpenCV 의 배열이 기대는 메모리 관리 방식
참조 카운팅 · 얕은 복사 · 깊은 복사 · 원자성 · 스레드 · 포화 연산 · 스마트 포인터
OpenCV 를 부르는 언어와 바인딩
C++ · Python · Java · NumPy · 바인딩 · 템플릿
OpenCV 와 함께 쓰거나 일을 나누는 도구
NumPy · scikit-image · Pillow · PyTorch · TensorFlow · scikit-learn · FFmpeg
OpenCV 와 방향이 반대인 분야
그래픽스 · 렌더링 · OpenGL · 그래픽스 파이프라인
다른 이름: Open Source Computer Vision Library · opencv-python · cv2