동영상 읽기·쓰기부터 픽셀 연산·히스토그램·색공간까지
OpenCV로는 이미지뿐 아니라 동영상 파일과 카메라를 열어 프레임을 읽고, 반대로 VideoWriter로 프레임을 묶어 동영상으로 저장할 수 있다. 이어서 픽셀 단위 덧셈·뺄셈·가중치 합성·절대 차이, 히스토그램으로 밝기·색상 분포를 보고, 히스토그램 평활화와 YCrCb, HSV와 inRange·copyTo까지 실습 파일 기준으로 정리한다.
(OpenCV - 류지 프로젝트에서 개념 설명을 참고하여 재구성)
1. 동영상 파일 읽기
동영상 파일을 열려면 cv2.VideoCapture(파일경로)를 사용한다. isOpened()로 열기 성공 여부를 확인하고, get(cv2.CAP_PROP_...)으로 너비·높이·프레임 수·fps를 구한 뒤, read()로 한 프레임씩 읽으며 imshow로 재생한다. 끝나면 release()로 자원을 닫는다.
import cv2
import sys
cap = cv2.VideoCapture('./movies/319719_tiny.mp4')
if not cap.isOpened():
print('동영상을 불러올 수 없음')
sys.exit()
print('동영상을 불러올 수 있음')
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
heiget = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
print(width)
print(heiget)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
print(frame_count)
fps = cap.get(cv2.CAP_PROP_FPS)
print(fps)
while True:
ret, frame = cap.read()
if not ret:
break
cv2.imshow('frame', frame)
if cv2.waitKey(10) == 27:
break
cap.release()
waitKey(10)은 약 10ms 대기 후 키를 확인하므로, ESC(27)를 누르면 재생을 멈추고 루프를 빠져 나간다.
※ 영상(이미지·동영상)
영상(image 또는 video)은 픽셀 단위로 구성된 시각 데이터다. 정지 영상은 그레이스케일(1채널) 또는 RGB/BGR(3채널)을 갖고, 동영상은 연속된 이미지 프레임의 집합으로 시간에 따른 영상 데이터로 볼 수 있다.
2. 카메라 입력
카메라를 쓰려면 VideoCapture(0)처럼 장치 인덱스를 넘긴다. 0은 기본 웹캠이다. 열기·속성 확인·read() 루프·release() 흐름은 동영상 파일과 같다.
import cv2
import sys
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print('카메라를 열 수 없음')
sys.exit()
print('카메라 연결 성공')
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
heiget = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
print(width)
print(heiget)
fps = cap.get(cv2.CAP_PROP_FPS)
print(fps)
while True:
ret, frame = cap.read()
if not ret:
break
cv2.imshow('frame', frame)
if cv2.waitKey(10) == 27:
break
cap.release()
3. 동영상 저장 - 두 영상 이어 붙이기
cv2.VideoWriter(경로, fourcc, fps, (width, height))로 쓰기 객체를 만들고, 매 프레임마다 out.write(frame)을 호출하면 된다. fourcc는 코덱 식별자로, 예시처럼 cv2.VideoWriter.fourcc(*'DIVX')로 지정한다. 아래는 두 개 동영상의 프레임을 순서대로 읽어 하나의 avi 파일로 이어 붙이는 예시다.
import cv2
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))
frame_count1 = int(cap1.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count2 = int(cap2.get(cv2.CAP_PROP_FRAME_COUNT))
fps1 = cap1.get(cv2.CAP_PROP_FPS)
fps2 = cap2.get(cv2.CAP_PROP_FPS)
print(w, h)
print(frame_count1, frame_count2)
print(fps1, fps2)
fourcc = cv2.VideoWriter.fourcc(*'DIVX')
out = cv2.VideoWriter('./movies/mix.avi', fourcc, fps1, (w, h))
for i in range(frame_count1):
ret, frame = cap1.read()
cv2.imshow('output', frame)
out.write(frame)
if cv2.waitKey(10) == 27:
break
for i in range(frame_count2):
ret, frame = cap2.read()
cv2.imshow('output', frame)
out.write(frame)
if cv2.waitKey(10) == 27:
break
cap1.release()
cap2.release()
out.release()
4. 카메라 녹화
카메라에서 읽은 프레임을 그대로 VideoWriter에 쓰면 실시간 녹화가 된다. 해상도와 fps는 VideoCapture에서 가져오면 된다.
import cv2
cap = cv2.VideoCapture(0)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
fourcc = cv2.VideoWriter.fourcc(*'DIVX')
out = cv2.VideoWriter('./movies/cam.avi', fourcc, fps, (w, h))
while True:
ret, frame = cap.read()
if not ret:
break
cv2.imshow('output', frame)
out.write(frame)
if cv2.waitKey(10) == 27:
break
cap.release()
out.release()
5. 픽셀 연산 - add, subtract, multiply, divide
영상 배열에 스칼라를 더하거나 빼면 픽셀 밝기가 전반적으로 밝아지거나 어두워진다. cv2.add, cv2.subtract, cv2.multiply, cv2.divide는 연산 결과를 0~255 범위로 포화(saturate) 시켜 준다. 그레이스케일이면 한 값, 컬러면 (B, G, R) 튜플을 넘긴다.
import cv2
img1 = cv2.imread('./images/dog.bmp', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('./images/dog.bmp')
dst1 = cv2.add(img1, 100)
dst2 = cv2.add(img2, (100, 100, 100))
dst3 = cv2.subtract(img1, 100)
dst4 = cv2.multiply(img1, 10)
dst5 = cv2.divide(img1, 10)
cv2.imshow('img1', img1)
cv2.imshow('img2', img2)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)
cv2.imshow('dst3', dst3)
cv2.imshow('dst4', dst4)
cv2.imshow('dst5', dst5)
cv2.waitKey()
6. 픽셀 덧셈 - NumPy 더하기 vs cv2.add
두 이미지를 픽셀끼리 더할 때, NumPy의 img1 + img2는 0~255를 벗어나면 256으로 나눈 나머지로 돌아가서 색이 이상해질 수 있다. cv2.add(img1, img2)는 값을 0 또는 255로 보정하므로 의도한 밝기 합성이 안전하다.
import cv2
import matplotlib.pyplot as plt
img1 = cv2.imread('./images/man.jpg')
img2 = cv2.imread('./images/turkey.jpg')
# 0 ~ 255 값을 벗어나도 256으로 나눈 나머지가 됨
dst1 = img1 + img2
# 0 ~ 255 값을 벗어나도 0 또는 255 값으로 보정
dst2 = cv2.add(img1, img2)
img = {'img1': img1, 'img2': img2, 'dst1': dst1, 'dst2': dst2}
for i, (k, v) in enumerate(img.items()):
plt.subplot(2, 2, i + 1)
plt.imshow(v[:, :, ::-1])
plt.title(k)
plt.show()
v[:, :, ::-1]은 BGR을 RGB로 뒤집어 Matplotlib에 맞춘 것이다.
7. 가중치 합성·차이·절대 차이
두 이미지를 비율로 섞을 때는 cv2.addWeighted(img1, alpha, img2, beta, gamma)를 쓴다. 차이는 cv2.subtract, 절대 차이는 cv2.absdiff로 구한다. 절대 차이는 픽셀별로 |img1 - img2|라서 움직임·변화 영역을 강조할 때 유용하다.
import cv2
import matplotlib.pyplot as plt
img1 = cv2.imread('./images/dog.jpg')
img2 = cv2.imread('./images/square.bmp')
dst1 = cv2.add(img1, img2)
# 가중치 합성 : 두 이미지를 비율로 섞음
dst2 = cv2.addWeighted(img1, 0.5, img2, 0.5, 0)
dst3 = cv2.subtract(img1, img2)
# 절대 차이: 두 이미지 간의 절대 차이값 abs(img1(x, y) - img2(x, y))
dst4 = cv2.absdiff(img1, img2)
img = {'img1': img1, 'img2': img2, 'dst1': dst1, 'dst2': dst2, 'dst3': dst3, 'dst4': dst4}
for i, (k, v) in enumerate(img.items()):
plt.subplot(2, 3, i + 1)
plt.imshow(v[:, :, ::-1])
plt.title(k)
plt.show()
8. 히스토그램
히스토그램은 밝기(또는 색상) 값의 분포를 그래프로 나타낸 것이다. 픽셀 값 0(검정)~255(흰색) 구간별로 픽셀 개수를 세어, 이미지가 어두운지 밝은지, 대비가 어떤지 파악할 수 있다. OpenCV에서는 cv2.calcHist()로 계산한다.
import cv2
import matplotlib.pyplot as plt
img1 = cv2.imread('./images/candies.png', cv2.IMREAD_GRAYSCALE)
"""
히스토그램
밝기 또는 색상 값의 분포를 그래프로 표현
- 어떤 픽셀이 0(검정)인지 255(흰색)인지, 각 값이 몇 개나 있는 지 확인
[img1] : 대상 이미지 리스트
[0]: 분석할 채널 번호(B:0, G:1, R:2)
None: 분석할 영역 마스크(None: 전체)
[256]: 히스토그램의 빈 개수
[0, 255]: 값의 범위
"""
hist1 = cv2.calcHist([img1], [0], None, [256], [0, 255])
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(hist1, color='gray')
img2 = cv2.imread('./images/candies.png')
channels = cv2.split(img2) # B, G, R
colors = ['b', 'g', 'r']
plt.subplot(1, 2, 2)
for ch, color in zip(channels, colors):
hist2 = cv2.calcHist([ch], [0], None, [256], [0, 255])
plt.plot(hist2, color=color, label=color.upper())
plt.show()
그레이스케일은 채널 하나만, 컬러는 cv2.split()으로 B·G·R을 나눈 뒤 채널별로 calcHist를 호출하면 된다.
9. 히스토그램 평활화와 YCrCb, 정규화
히스토그램 평활화는 밝기 분포를 고르게 퍼뜨려 명암 대비를 높이는 기법이다. 그레이스케일은 cv2.equalizeHist() 한 번이면 된다. 컬러는 YCrCb로 바꾼 뒤 Y(밝기) 채널만 평활화하고 다시 BGR로 되돌리면 색은 유지한 채 명도만 보정할 수 있다. cv2.normalize()로 최소·최대값을 지정해 구간을 0~255로 펼치는 방식도 있다.
import cv2
import matplotlib.pyplot as plt
img1 = cv2.imread('./images/Hawkes.jpg', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('./images/field.bmp')
# 히스토그램 평활화: 이미지의 전체 밝기 분포를 고르게 퍼뜨려 명암 대비를 향상
dst1 = cv2.equalizeHist(img1)
# YCrCb: Y=밝기(명도), Cr=빨강 계열, Cb=파랑 계열 색상 정보
dst2 = cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb)
dst2[:, :, 0] = cv2.equalizeHist(dst2[:, :, 0]) # 명도만 평활화
dst2 = cv2.cvtColor(dst2, cv2.COLOR_YCrCb2BGR)
# normalize: 정규화 (최소·최대값을 0~255로 스케일)
dst3 = cv2.normalize(img1, None, 0, 255, cv2.NORM_MINMAX)
cv2.imshow('img1', img1)
cv2.imshow('dst1', dst1)
cv2.imshow('img2', img2)
cv2.imshow('dst2', dst2)
cv2.imshow('dst3', dst3)
hist1 = cv2.calcHist([img1], [0], None, [256], [0, 255])
hist2 = cv2.calcHist([dst1], [0], None, [256], [0, 255])
hist3 = cv2.calcHist([dst2], [0], None, [256], [0, 255])
hist4 = cv2.calcHist([dst3], [0], None, [256], [0, 255])
hists = {'hist1': hist1, 'hist2': hist2, 'hist3': hist3, 'hist4': hist4}
plt.figure(figsize=(15, 8))
for i, (k, v) in enumerate(hists.items()):
plt.subplot(2, 2, i + 1)
plt.title(k)
plt.plot(v)
plt.show()
※ YCrCb 색공간
YCrCb에서 Y는 밝기(명도), Cr은 빨강 계열, Cb는 파랑 계열 색상 정보다. 밝기만 바꾸고 색은 그대로 두려면 Y 채널만 평활화한 뒤 다시 BGR로 합치면 된다.
10. HSV와 색상 추출, copyTo
HSV는 색을 색상(H), 채도(S), 명도(V) 로 표현하는 방식이다. 이미지 처리와 색상 추출에 자주 쓰인다. OpenCV에서는 H는 0
179, S·V는 0
255다. cv2.cvtColor(img, cv2.COLOR_BGR2HSV)로 변환한 뒤 cv2.inRange(hsv, lower, upper)로 원하는 색 범위를 마스크로 뽑을 수 있다. copyTo는 마스크가 0이 아닌 위치만 원본을 목적지에 복사하는 선택적 복사다.
import cv2
img = cv2.imread('./images/candies.png')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# HSV: H=색상(0 또는 179 빨강 등), S=채도, V=명도
# OpenCV H: 빨강 0 또는 179, 주황 10~20, 노랑 20~30, 초록 40~85, 파랑 90~130, 보라 130~160
dst = cv2.inRange(hsv, (90, 150, 0), (130, 255, 255))
airplane = cv2.imread('./images/airplane.bmp')
mask = cv2.imread('./images/mask_plane.bmp')
field = cv2.imread('./images/field.bmp')
# copyTo: 마스크를 이용한 선택적 복사
temp = cv2.copyTo(airplane, mask)
cv2.copyTo(airplane, mask, field)
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.imshow('field', field)
cv2.waitKey()
inRange(hsv, (90, 150, 0), (130, 255, 255))는 파랑 계열(H 90
130, S 150
255, V 0~255) 픽셀만 255로 두고 나머지는 0인 이진 마스크를 만든다.
※ HSV
HSV에서 H(Hue)는 색상(빨강·초록·파랑 등, OpenCV 기준 0~179), S(Saturation)는 채도(색의 선명함), V(Value)는 명도(밝기)다. 특정 색만 추출할 때 H 범위를 주고 S·V로 노이즈를 줄이는 식으로 쓰면 된다.
마치며
- 동영상·카메라는
VideoCapture(경로 또는 0)로 열고,read()루프로 프레임을 읽으며imshow·waitKey로 재생·종료한다. - 동영상 저장은
VideoWriter(fourcc, fps, (w, h))로 생성한 뒤write(frame)으로 프레임을 써 주고, 끝에release()를 호출한다. - 픽셀 연산은
cv2.add·subtract·multiply·divide가 0~255로 포화시키고, 두 영상 합성은+대신cv2.add또는addWeighted를 쓰는 것이 안전하다.absdiff로 절대 차이를 구하면 변화·움직임 영역을 강조할 수 있다. - 히스토그램은
calcHist로 밝기·채널별 분포를 보고, 평활화는equalizeHist로 명암 대비를 높인다. 컬러는 YCrCb의 Y만 평활화하면 색을 유지할 수 있다. - HSV로 변환 후
inRange로 색 마스크를 만들고,copyTo로 마스크 영역만 복사해 합성할 수 있다.
다음으로는 이진화(threshold, Otsu, 적응형), 필터링(블러·엣지), 모폴로지 연산 등을 실습해 보면 좋다.
(OpenCV - 류지 프로젝트 참고)
'AI·머신러닝 > 딥러닝·비전' 카테고리의 다른 글
| 컴퓨터 비전과 OCR - 정의, 프레임워크, 데이터셋, 어노테이션, Tesseract·EasyOCR (0) | 2026.01.06 |
|---|---|
| OpenCV 크로마키·ROI·이진화·기하 변환 - 투시 변환과 명함 스캐너 실습 (0) | 2026.01.05 |
| OpenCV 기초 - 이미지 읽기, 속성, 도형·마우스 이벤트 실습 (0) | 2025.12.31 |
| 전이 학습 실전 - Alien vs Predator · 콘크리트 균열 탐지, AlexNet · VGG19 활용 (0) | 2025.12.29 |
| AlexNet과 CIFAR-10 이미지 분류 - ILSVRC, 전처리, 혼동 행렬 실습 (0) | 2025.12.25 |