본문 바로가기
AI/컴퓨터 비전

OpenCV 영상처리 | 영상이진화 | 오츠이진화 | 적응형 이진화

by 바다의 공간 2025. 3. 19.

▶ 영상처리

import cv2
import sys

 

IMPORT SYS는

내 컴퓨터에 시스템에 관련된 모듈입니다.

 

cap = cv2.VideoCapture('260397_tiny.mp4')

 

VideoCapture라는 클래스가 하는 일!

1. 외부에있는 동영상을 불러오기!

2. 웹캠을 실행해줄 수 있습니다

 

import cv2
import sys

cap = cv2.VideoCapture('260397_tiny.mp4')

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

print('동영상 불러오기 성공')
print('가로 사이즈: ', int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))
print('세로 사이즈: ', int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print('프레임 수: ', int(cap.get(cv2.CAP_PROP_FRAME_COUNT)))
print('FPS: ', cap.get(cv2.CAP_PROP_FPS))  # 1초당 보이는 프레임 수

 

결과는>>>>>>>>

동영상 불러오기 성공
가로 사이즈:  1280
세로 사이즈:  720
프레임 수:  1022
FPS:  25.0

가 나옵니다 FPS는 1초당 이미지를 몇개를 흐르고있니? 라는 뜻입니다.

1초당 25장이 후다다다다다다다다다닥 지나간다는것입니다.

그러면 프레임수가 1022고 1초당 25장이니까 40초짜리네요!

 


 

▶ 임의 영상실행

#영상실행
while True:
    ret, frame = cap.read()
    if not ret:
        break
    cv2.imshow('frame', frame)
    if cv2.waitKey(10) == 27:
        break

#내가 썼던 비디오를 메모리에서 삭제하는것!
cap.release()

 

 

 

이 부분을 좀 더 뜯어보면 while로 평생 돌리게 만든다음에

ret(리턴), frame값을 가져옵니다 이 값은 한 프레임이겠죠.

cap에서 만약 ret이 없으면(프레임 다돌린경우) 멈추고 frame을 보여줍니다.

 

한 프레임을 읽어왔다는건 1022개 중에서 1개를 뽑아서 frame에서 저장되고 

frame은 행렬(ndarray)이다. ret는 True가 들어오게 됩니다. 영상이 있으면 true, 없으면 False가 들어오게됩니다.

 

waitkey(10)은 0.001초동안 기다리겠다는 이야기입니다. 글서 영상을 보는 동안 esc를 누르면 영상을 재생하지 않고 멈추겠다는 의미입니다.

 

 


▶ 실시간 웹캠 실행

import cv2
import sys

cap = cv2.VideoCapture(0) # 파일경로: 동영상 불러옴, 숫자: 해당 인덱스에 설치된 카메라를 불러옴

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

print('카메라 연결 성공')
print('가로 사이즈: ', int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))
print('세로 사이즈: ', int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print('FPS: ', cap.get(cv2.CAP_PROP_FPS))

while True:
    ret, frame = cap.read()
    if not ret:
        break
    cv2.imshow('frame', frame)
    if cv2.waitKey(10) == 27:
        break

cap.release(

 

 


▶ 2개의 영상 합치기

어떻게 합치는지 이야기를 해보자면 1번영상이 끝나면 이어서 2번으로 안끊기고 나오게 하게 하려고합니다.

그러면 영상이 끊어지지않아야되겠죠

 

여기서 조건은 영상의 크기가 같아야 합니다. 

resize해서 할수도있지만 일단은! 같은 영상의 해상도(사이즈)를 가지고 와서 붙여보는 것으로 예제를 해보려고 합니다.

 

▶  size확인

 

import cv2

cap1 = cv2.VideoCapture('D:/PJ/DLHJ/1.mp4')
cap2 = cv2.VideoCapture('D:/PJ/DLHJ/2.mp4')

#frame확인
w = int(cap1.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap1.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(w, h)

w = int(cap2.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap2.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(w, h)

 

>>

640 360
640 360


 

▶  frame개수 확인

#각 frame 개수 확인
frame_cnt1=int(cap1.get(cv2.CAP_PROP_FRAME_COUNT))
frame_cnt2=int(cap2.get(cv2.CAP_PROP_FRAME_COUNT))
print(frame_cnt1, frame_cnt2)

>> 541 , 242로 나옵니다.

 

길이가 다르다는게 느껴집니다.

 

▶  fps 확인

 FPS1도 구해보겠습니다

#FPS확인
fps1 = int(cap1.get(cv2.CAP_PROP_FPS))
fps2 = int(cap2.get(cv2.CAP_PROP_FPS))
print(fps1, fps2)

 

> 23,24로 나옵니다.

 

▶ 영상길이확인

#영상 길이 확인
leng1 = frame_cnt1/fps1
leng2 = frame_cnt2/fps2

print(f'cap1의 영상 길이 : {leng1}, cap2의 영상길이 : {leng2}')

cap1의 영상 길이 : 23.52173913043478, cap2의 영상길이 : 10.083333333333334

 

 


▶ 객체만들기

cv2.VideoWriter => 파이썬에서 비디오객체를 만들겠다는 매소드입니다.

fourcc => 포멧종류를 적어줌 (*'DIVX')를 사용하면 확장명이 AVI로 저장됩니다.

 

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

 

fourcc라는 변수를 이용해서 divx를 만들어주었고

 

out = cv2.VideoWriter('mix.avi', fourcc, fps1, (w,h))

 

out으로 넣을 양식을 만들어줍니다. videoWriter라는 클래스를 사용하면서 만들어줍니다.

저장할 준비는 끝났습니다!

더보기
cv2.VideoWriter(filename, fourcc, fps, frameSize, isColor=True)

 

매개변수 설명

filename 저장할 비디오 파일의 이름 ('output.avi' 등)

fourcc 코덱 (ex: cv2.VideoWriter.fourcc(*'XVID'))

fps 초당 프레임 수 (Frames Per Second)

frameSize 비디오 프레임 크기 ((w, h))

isColor True이면 컬러 영상, False이면 흑백

이제 이렇게 저장을 할 수 있을뿐이지 어떤 영상을 넣을지는 전혀 넣지 않았습니다.

 


▶ 영상 합치기

for i in range(frame_cnt1):
    ret, frame = cap1.read()
    cv2.imshow('output', frame)
    out.write(frame)
    if cv2.waitKey(10) == 27:
        break

for i in range(frame_cnt2):
    ret, frame = cap2.read()
    cv2.imshow('output', frame)
    out.write(frame)
    if cv2.waitKey(10) == 27:
        break

cap1.release()
cap2.release()
out.release()

 

for문을 돌려서 둘이 합쳐주는 for문을 구축했고 영상이 잘 합쳐지는것까지 확인했습니다.

 

 

영상을 다루는것 자체가 중요한 이유가 cctv나 실시간 객체를 확인하기 위해서는 매우매우 필요한 역량입니당.


▶ webcam으로 만들기

import cv2

cap = cv2.VideoCapture(0)

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

fourcc = cv2.VideoWriter.fourcc(*'DIVX')
out = cv2.VideoWriter('output.avi', fourcc, fps, (w, h))

while True:
    ret, frame = cap.read()
    if not ret:
        break
    out.write(frame)
    cv2.imshow('frame', frame)
    if cv2.waitKey(10) == 27:
        break

cap.release()
out.release()

▶ 영상의 이진화

이진화(Binary Thresholding) 는 영상을 흑백(0 또는 255)으로 변환하여 특정 임계값(threshold) 이상인 픽셀을 흰색(255)으로, 이하인 픽셀을 검은색(0)으로 변환하는 과정입니다.

 

이는 객체 검출, OCR(광학 문자 인식), 엣지 검출 등의 전처리 과정에서 중요한 역할을 합니다.

OpenCV에서는 cv2.threshold() 함수를 사용하여 고정 임계값 이진화, 적응형 이진화, Otsu의 이진화 등을 적용할 수 있습니다.

특히, cv2.THRESH_BINARY는 기본적인 이진화 방법이며, cv2.ADAPTIVE_THRESH_GAUSSIAN_C는 조명이 균일하지 않은 경우에도 효과적으로 이진화를 수행할 수 있습니다..

 

지난번에도 한번 진행했던 적이있어서 실습 위주로 재학습했다.

그래서 함수의 기본 형식으로 좀 학습을 하고 정리를 하려고 한다.


cv2.calcHist(images, channels, mask, histSize, ranges, hist, accumulate)

▶ 매개변수 설명

images 입력 이미지 리스트 ([img] ← 대괄호 필요!) <하나의 영상뿐만아니라 여러개를 넣으면 여러개 계산함>

channels 히스토그램을 계산할 채널 (0=Blue, 1=Green, 2=Red)

mask 특정 영역만 계산할 때 사용 (None이면 전체 이미지)

histSize 히스토그램의 빈(bin) 개수 ([256] ← 256단계) 그럼 항목이 256개가 나오게 됨

ranges 픽셀 값 범위 ([0, 256] ← 0~255)

hist (옵션) 기존 히스토그램을 입력하면 이어서 계산

accumulate True이면 기존 히스토그램에 추가 계산

 

 


 

히스토그램 계산해보기 (claHist())

- 이미지의 픽셀 값 분포를 나타냄

hist = cv2.calcHist([img], [0], None, [266], [0,255])

 


▶ 스레숄드 (임계값) 기준으로 이미지 이진화 (cv2.THRESHOLD_BINARY)

a, dst1 = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)
b, dst2 = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)

 

처음은 이미지를 넣고, 임계값, 임계값을 넘는 픽셀에 부여할 최대값, 임계값 적용방식)

그 중에서 THRESHOLD_BINARY는 픽셀값이 임계값보다 크면! 최대값으로 설정하고, 작거나 같으면 0으로 설정하라는 의미입니다.

 

픽셀값이 100보다 크면 무조건 255고 이거보다 작으면 0으로 바꿔라 라는 의미입니다.

 

또한 여기서 a, dst1의 의미도 정리하자면,

threshold가 리턴하는 값이 2개입니다.

2진화를 한 이미지(dst1)고,a는 임계값(100)이 리턴됩니다.

그러니까 즉 dst는 행렬이 됩니다.

 

#그림확인
img = cv2.imread('D:/PJ/DLHJ/apple.png', cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([img], [0], None, [266], [0,255])

a, dst1 = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)
b, dst2 = cv2.threshold(img, 150, 255, cv2.THRESH_BINARY)

cv2.imshow('img1', img)
cv2.imshow('img2', dst1)
cv2.imshow('img3', dst2)


plt.plot(hist)
plt.show()
cv2.waitKey()

 

 

당연히 dst1의 스레숄드로 하니 흰색에 가까워지고 dst2는 검정색이 더 많이 보이게 됩니다.

좀 더 풀어서 리뷰하자면

img2은 스레숄드값이 100보다 크면  흰색 + 100보다 작으면 검정색 

img3는 150보다 크면 흰색 + 150보다 작으면 검정색

그래프보면 img1은 100보다 큰 부분이 많으니 흰색이 많이 보이고, img2는 히스토그램에서 150보다 값이 적다면 150기준으로 이진화했을때 흰색이 줄어들고 검정색 분포가 더 넓어진다!


▶오츠 이진화

오츠 이진화(Otsu's Binarization) 는 OpenCV에서 제공하는 자동 임계값 결정 기법으로, 영

상의 히스토그램을 분석하여 객체와 배경을 가장 잘 구분할 수 있는 최적의 임계값을 자동으로 찾는 방법입니다.

 

이는 cv2.THRESH_OTSU 옵션을 사용하여 cv2.threshold() 함수에서 적용할 수 있으며,

특히 배경과 객체 간의 명암 대비가 뚜렷한 경우 효과적으로 동작합니다.

 

오츠 이진화는 모든 픽셀 값의 분포를 기반으로 클래스 내 분산(intra-class variance)을 최소화하는 임계값을

자동으로 선택하여 수동으로 임계값을 설정하는 불편함을 줄여줍니다.

 

일반적으로 cv2.THRESH_BINARY + cv2.THRESH_OTSU를 함께 사용하여 이진화를 수행하며,

그레이스케일 변환이 선행되어야 합니다.

 

단 여기 문제가 뭐냐면 최적의 임계값을 자동으로 찾긴하는데 클래스 내 분산을 최소화하는 임계값을 자동으로 선택하기때문에 전체적으로 균일하고 균등한 이미지여야 잘 됩니다.

import cv2
import matplotlib.pyplot as plt

#그림확인
img = cv2.imread('D:/PJ/DLHJ/apple.png', cv2.IMREAD_GRAYSCALE)


th, dst = cv2.threshold(img, 0,255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('threshold: ', th)

cv2.imshow('img1', img)
cv2.imshow('dst', dst)
cv2.waitKey()

 

임의로 잡아주었는데 결과먼저보면 아래와 같다.

 

그래서 이렇게 자동으로 잡은 값은 >> 128이나왔다. 

우리가 아까 100과 150으로 했을때랑은 조금 다른 결과가 보이긴하지만 아쉽게 아래에 그림자는 조금 아쉽다.


▶적응형 이진화

 

적응형 이진화(Adaptive Thresholding) 는 OpenCV에서 제공하는 이진화 기법 중 하나로, 조명 변화가 심한 영상에서도 적절한 임계값을 적용하여 이진화를 수행하는 방법입니다.

 

일반적인 cv2.threshold()는 하나의 고정된 임계값을 사용하지만, cv2.adaptiveThreshold()는 영상의 작은 영역마다 서로 다른 임계값을 계산하여 적용합니다.

 

이를 통해 명암 차이가 고르지 않은 이미지에서도 효과적으로 객체와 배경을 분리할 수 있습니다.

 

OpenCV에서는 평균값을 이용하는 cv2.ADAPTIVE_THRESH_MEAN_C와 가우시안 가중치를 적용하는 cv2.ADAPTIVE_THRESH_GAUSSIAN_C 두 가지 방식을 제공하며, 블록 크기(blockSize)와 상수 값(C)을 조절하여 최적의 결과를 얻을 수 있습니다.

 

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('D:/PJ/DLHJ/apple.png', cv2.IMREAD_GRAYSCALE)

th, dst1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

dst2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 5)
dst3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 5)

dic = {'img': img, 'dst1': dst1, 'dst2': dst2, 'dst3': dst3}

for i, (k, v) in enumerate(dic.items()):
    plt.subplot(2, 2, i+1)
    plt.title(k)
    plt.imshow(v, 'gray')

plt.show()

 

여기서 ADAPTIVE_THRESH_MEAN_C : 지정한 영역 내 픽셀들의 평균을 계산하여서 임계값으로 사용합니다.

그 부분이 바로 바로 뒤에있는 9입니다.

 

좀 더 풀어서 보자면

dst = cv2.adaptiveThreshold(
    img, 255, adaptiveMethod, thresholdType, blockSize, C
)

 

 

  • img → 입력 이미지 (그레이스케일 이미지 여야 함)
  • 255 → 최댓값 (이진화 후 흰색을 255로 설정)
  • adaptiveMethod → 적응형 임계값을 계산하는 방식
    • cv2.ADAPTIVE_THRESH_MEAN_C → 주변 픽셀들의 평균값을 사용
    • cv2.ADAPTIVE_THRESH_GAUSSIAN_C → 주변 픽셀들의 가중 평균(가우시안 필터 적용)을 사용
  • thresholdType → 임계값 적용 방식
    • cv2.THRESH_BINARY → 기준값보다 크면 255(흰색), 작으면 0(검정색)
    • (cv2.THRESH_BINARY_INV 도 있음: 흑백 반전)
  • blockSize → 임계값을 계산할 영역 크기 (홀수만 가능)
    • 예: 9이면 9×9 크기의 영역을 기준으로 임계값을 계산
  • C → 보정 값 (Threshold 값에서 빼줄 값, 조정용)
    • C가 클수록 결과 이미지가 더 어두워짐
    • 작은 값을 사용하면 경계가 더 뚜렷해짐

dst2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 5)

 

 

  • Mean Adaptive Thresholding 적용
  • 각 9×9 영역의 평균값 - 5 를 임계값으로 사용
  • 결과적으로 지역별로 서로 다른 밝기 값이 적용된 이진화된 이미지 생성

 

dst3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 5)

 

 

  • Gaussian Adaptive Thresholding 적용
  • 각 9×9 영역의 가우시안 가중 평균 - 5 를 임계값으로 사용
  • Gaussian 방식은 경계가 더 부드럽고 자연스러움