[OpenCV] 이진 영상 처리

메르센고수·2023년 12월 29일
0

OpenCV

목록 보기
7/9


0과 1의 컴퓨터세계에서 이미지도 예외는 아니다. 픽셀 값을 0 (검정색), 255(흰색)으로 구분해서 객체를 인식할 수 있고, 영역을 설정할 수도 있다.
예를 들어, 세포 이미지를 이진화 할 경우, 다음과 같은 영상이 출력된다.

이 때, threshold 값을 설정해서 특정 범위로 값을 제한시킬 수도 있다.

threshold 함수

cv2.threshold(src, thresh, maxval, type, dst=None) -> retval, dst

1) src : 입력 영상. 다채널, 8비트 또는 32비트 실수형
2) thresh : 사용자 지정 임계값
3) maxval : cv2.THRESHBINARY 또는 cv2.THRESH_BINARY_INV 사용. 보통 255로 지정한다.
4) type : cv2.THRESH
로 시작하는 플래그로, 임계값 함수 동작 지정 또는 자동 임계값 결정 방법을 지정할 때 사용한다.
5) retval : 사용된 임계값
6) dst : 출력 영상. src와 동일한 크기, 타입, 채널 수를 갖는다.

  • Example
src=cv2.imread('cells.png', cv2.IMREAD_GRAYSCALE)

_, dst1=cv2.threshold(src, 100, 255, cv2.THRESH_BINARY)
_, dst2=cv2.threshold(src, 210, 255, cv2.THRESH_BINARY)

dst1은 threshold를 100으로 지정했고, dst2는 threshold를 210으로 지정했다. 그랬더니 결과는 꽤나 큰 차이를 보였다.

threshold 값에 의해 이진화의 차이가 달라진다는 것을 알았으므로, 일일히 threshold 값을 함수의 인자로 지정해주는 대신, 이전에 배웠던 trackbar로 사용자가 threshold값을 조절해서 조절된 threshold 값에 대응하는 이미지를 출력하도록 코드를 구현했다.

import sys
import numpy as np
import cv2


src = cv2.imread('rice.png', cv2.IMREAD_GRAYSCALE)

if src is None:
    print('Image load failed!')
    sys.exit()

# th, dst = cv2.threshold(src, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
# print("otsu's threshold:", th)  # 131

def on_threshold(pos):
    _, dst = cv2.threshold(src, pos, 255, cv2.THRESH_BINARY)
    cv2.imshow('dst', dst)
    
cv2.namedWindow('dst')
cv2.createTrackbar('Threshold', 'dst', 0, 255, on_threshold)
cv2.setTrackbarPos('Threshold', 'dst', 128)
# cv2.imshow('src', src)
# cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

자동 이진화 : Otsu 방법


: 일본의 Otsu라는 사람이 만든 알고리즘인데, 임의의 임계값 T에 의해 나눠지는 두 픽셀 분포 그룹의 분산이 최소가 되는 T를 선택하는 방식이다.
말이 좀 어렵게 느껴지지만 나름 간단히 요약을 해보자면 임계값에 의해 나눠지는 분산에 각각의 weight값을 곱한 값으로 최적의 임계값을 찾아주는 방식이다.

위키피디아에 있는 이미지인데, 히스토그램처럼 생긴 그래프를 보면 픽셀의 분포가 있고 임계값이 있는데 가중치를 곱해가면서 위에 있는 빨간색 실선 그래프가 그려지고, 그 그래프를 기반으로 새로운 임계값을 정해서 파란색과 빨간색을 이진화하여 오른쪽 그림과 같이 흰색과 검정색 만으로 구성된 이미지가 만들어지게 되는 원리이다.
예시 코드를 보면,

src=cv2.imread('rice.png', cv2.IMREAD_GRAYSCALE)

th, dst=cv2.threshold(src, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print("otsu's threshold:", th)  #131

GRAYSCALE 이미지로 불러와서 Binary이진화를 하거나 Otsu 방식으로 이진화를 해서 threshold 값을 출력하는 프로그램이다. 이 경우, 중간값인 128과 거의 유사한 131이 출력된다.

지역 이진화

지역 이진화는 특정 부분만 이진화를 하는 방식인데, 균일하지 않은 조명환경에서 촬영한 이미지같은 것들을 이진화할 때 사용한다.

이러한 불균일한 조명의 영향을 해결하기 위해서는 불균일한 조명 성분을 보상한 후 전역 이진화를 수행해야 한다.

그 다음, 픽셀 주변에 작은 윈도우를 설정해서 필요한 부분만 지역 이진화를 수행한다.

src=cv2.imread('rice.png', cv2.IMREAD_GRAYSCALE)

# 전역 이진화 by Otsu's method
_, dst1 = cv2.threshold(src, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# 지역 이진화 by Otsu's method
dst2 = np.zeros(src.shape, np.uint8)

bw=src.shape[1] // 4
bh=src.shape[0] // 4

for y in range(4):
	for x in range(4):
		src_=src[y*bh:(y+1)*bh, x*bh:(x+1)*bw]
		dst_=dst2[y*bh:(y+1)*bh, x*bw:(x+1)*bw]
		cv1.threshold(src_, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU, dst_)

이런 식의 코드를 작성해서 돌리면

이런 식으로 출력이 되는데, dst1과 dst2의 차이점을 보면 dst1에 약간의 noise가 더 많이 검출되는 것을 확인할 수 있다.

OpenCV 적응형 이진화

cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, 
					blockSize, C, dst=None) -> dst

1) src : 입력 영상, 그레이스케일 영상
2) maxValue : 임계값 함수 최댓값. 보통 255
3) adaptiveMethod : 블록 평균 계산 방법 지정. cv2.ADAPTIVE_THRESH_MEAN_C는 산술평균, cv2.ADAPTIVE_THRESH_GAUSSIAN_C는 가우시안 가중치 평균
4) thresholdType : cv2.THRESH_BINARY 또는 cv2.THRESH_BINARY_INV 지정
5) blockSize : 블록 크기. 3 이상의 홀수
6) C : 블록 내 평균값 또는 블록 내 가중 평균값에서 뺄 값
(x,y) 픽셀의 임계값으로 T(x,y)=μB(x,y)CT(x,y)=\mu_B(x,y)-C 사용

import sys
import numpy as np
import cv2


src = cv2.imread('sudoku.jpg', cv2.IMREAD_GRAYSCALE)

if src is None:
    print('Image load failed!')
    sys.exit()


def on_trackbar(pos):
    bsize = pos
    if bsize % 2 == 0:
        bsize = bsize - 1
    if bsize < 3:
        bsize = 3

    dst = cv2.adaptiveThreshold(src, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                cv2.THRESH_BINARY, bsize, 5)

    cv2.imshow('dst', dst)


cv2.imshow('src', src)
cv2.namedWindow('dst')
cv2.createTrackbar('Block Size', 'dst', 0, 200, on_trackbar)
cv2.setTrackbarPos('Block Size', 'dst', 11)

cv2.waitKey()
cv2.destroyAllWindows()

모폴로지

1. 침식과 팽창

모폴로지 (Morphology)

: 영단어 뜻을 그대로 해석하면 '형태학적인'이란 뜻이다. 고로 영상을 형태학적인 측면에서 다루는 기법이다.

  • 다양한 영상 처리 시스템에서 전처리, 후처리 형태로 널리 사용
  • 수학적 모폴로지

구조 요소

: 모폴로지 연산의 결과를 결정하는 Kernel, Mask, Window를 의미한다.
예를 들어,

이러한 5가지 이상의 구조 요소가 가능한데, 가운데의 Anchor가 침식과 팽창에 중요한 영향을 미친다.

이진 영상의 침식 연산

  • 구조 요소가 객체 영역 내부에 완전히 포함될 경우 고정점 픽셀을 255로 설정
  • 침식 연산은 객체 외곽을 깎아내는 연산으로, 객체의 크기는 감소하고 배경은 확대된다.
  • 모폴로지 침식 연산
cv2.erode(src, kernel, dst=None, anchor=None, iterations=None, 
					borderType=None, borderValue=None) -> dst

1) src : 입력 영상
2) kernel : 구조 요소. getStructuringElement() 함수에 의해 생성 가능
만약 None을 지정하면 3x3 사각형 구성 요소를 사용
3) dst : 출력 영상. src와 동일한 크기와 타입
4) anchor : 고정점 위치. 기본값 (-1,-1)을 사용하면 중앙점을 사용
5) iterations : 반복 횟수. 기본값은 1
6) borderType : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT인 경우, 확장된 가장자리 픽셀을 채울 값

이진 영상의 팽창 연산

  • 구조 요소와 객체 영역이 한 픽셀이라도 만날 경우 고정점 픽셀을 255로 설정
  • 팽창 연산은 객체 외곽을 확대시키는 연산으로, 객체의 크기는 감소하고 배경은 확대된다.
  • 모폴로지 팽창 연산
cv2.dilate(src, kernel, dst=None, anchor=None, iterations=None,
			borderType=None, borderValue=None) -> dst

1) src : 입력 영상
2) kernel : 구조 요소. getStructuringElement() 함수에 의해 생성 가능
만약 None을 지정하면 3x3 사각형 구성 요소를 사용
3) dst : 출력 영상. src와 동일한 크기와 타입
4) anchor : 고정점 위치. 기본값 (-1,-1)을 사용하면 중앙점을 사용
5) iterations : 반복 횟수. 기본값은 1
6) borderType : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT인 경우, 확장된 가장자리 픽셀을 채울 값

=> 두 연산의 가장 큰 차이점은 침식 연산은 구조 요소가 객체 영상의 부분집합이 될 경우에만 anker point를 255로 설정하는 것이고, 팽창 연산은 구조 요소가 객체 영상에 겹치기만 해도 anker point를 255로 설정하는 것이다.

이미지로 보면 다음과 같다.

  • 모폴로지 구조 요소 (kernel) 생성
cv2.getStructuringElement(shape, ksize, anchor=None) -> retval

1) shape : 구조 요소 모양을 나타내는 플래그

cv2.MORPH_RECT : 사각형 모양
cv2.MORPH_CROSS : 십자가 모양
cv2.MORPH_ELLIPSE : 사각형에 내접하는 타원

2) ksize : 구조 요소 크기. (width, height) 튜플
3) anchor : MORPH_CROSS 모양의 구조 요소에서 고정점 좌표.
(-1,-1)을 지정하면 구조 요소의 중앙을 고정점으로 사용
4) retval : 0과 1로 구성된 cv2.CV_8UC1 타입 행렬 (1의 위치가 구조 요소 모양을 결정)

src=cv2.imread('circuit.bmp', cv2.IMREAD_GRAYSCALE)

se=cv2.getStructuringElement(cv2.MORPH_RECT,(5,3))
dst1=cv2.erode(src, se)

dst2=cv2.dilate(src, None)

열기와 닫기


  • 범용 모폴로지 연산 함수
cv2.morphologyEx(src, op, kernel, dst=None, anchor=None, iterations=None, 
				borderType=None, borderValue=None) -> dst

1) src : 입력 영상
2) op : 모폴로지 연산 플래그

cv2.MORPH_ERODE : 침식
cv2.MORPH_DILATE : 팽창
cv2.MORPH_OPEN : 열기
cv2.MORPH_CLOSE : 닫기
cv2.MORPH_GRADIENT : 모폴로지 그래디언트 = 팽창 - 침식

3) kernel : 커널
4) dst : 출력 영상

src=cv2.imread('rice.png', cv2.IMREAD_GRAYSCALE)

dst1=np.zeros(src.shape, np.uint8_

# src 영상에 지역 이진화 수행 (lcan_th.py 참고)

cnt1, _ = cv2.connectedComponents(dst1)
print('cnt1:', cvt1)

dst2=cv2.morphologyEx(dst1, cv2.MORPH_OPEN, None)
# dst2 = cv2.erode(dst1, None)
# dst2 = cv2.dilate(dst2, None)

cvt2, _ = cv2.connectedComponents(dst2)
print('cnt2:', cnt2)

레이블링

: 흔히 레이블링을 한다는 의미는 어떤 것을 구분하기 위해 표식을 남겨놓는다는 의미이다. 영상처리에서도 비슷한 의미이다. 영상처리에서의 레이블링은 동일한 객체에 속한 모든 픽셀에 고유한 번호를 부여하는 작업을 의미하는데, 일반적으로 이진 영상에서 수행핟나.

객체 단위 분석

  • (흰색) 객체를 분할하여 특징을 분석
  • 객체 위치 및 크기 정보, ROI 추출, 모양 분석 등

레이블링 (Connected Component Labeling)

  • 서로 연결되어 있는 객체 픽셀에 고유한 번호를 지정
  • 영역기반 모양 분석
  • 레이블맵, 바운딩 박스, 픽셀 개수, 무게 중심 좌표를 반환한다.

외곽선 검출 (Contour Tracing)

  • 각 객체의 외곽선 좌표를 모두 검출한다.
  • 외곽선 기반의 모양 분석
  • 다양한 외곽선 처리 함수에서 활용 가능 (근사화, 컨벡스헐 등)

레이블링 알고리즘의 입력과 출력

  • 레이블링 함수
cv2.connectedComponents(image, labels=None, Connectivity=None, ltype=None)
-> retval, labels

1) image : 8비트 1채널 영상
2) labels : 레이블 맵 행렬. 입력 영상과 같은 크기
3) connectivity : 4 또는 8
4) ltype : labels 타입. cv2.CV_32S 또는 cv2.CV_16S. 기본값은 cv2.CV_32S
5) retval : 객체 개수. N을 반환하면 [0, N-1]의 레이블이 존재하며, 0은 배경을 의미 (실제 흰색 객체 개수는 N-1개)

  • 객체 정보를 함께 반환하는 레이블링 함수
cv2.connectedComponentsWithStats(image, labels=None, stats=None, centroids=None, 
								connectivity=None, ltype=None)
-> retval, labels, stats, centroids

1) image : 8비트 1채널 영상
2) labels : 레이블 맵 행렬. 입력 영상과 같은 크기
3) stats : 각 객체의 바운딩 박스, 픽셀 개수 정보를 담은 행렬
numpy.ndarray.shape=(N,5), dtype=numpy.int32
4) centroids : 각 객체의 무게 중심 위치 정보를 담은 행렬
numpy.ndarray.shape=(N,2), dtype=numpy.float64
5) ltype : labels 행렬 타입. cv2.CV_32S 또는 cv2.CV_16S. 기본값은 cv2.CV_32S

외곽선 검출

: 객체의 외곽선 좌표를 모두 추출하는 작업

  • 외곽선 검출 함수
cv2.findContours(image, mode, method, contours=None, hierarchy=None, offset=None)
-> contours, hierarchy

1) image : 입력 영상. non-zero 픽셀을 객체로 간주함
2) mode : 외곽선 검출 모드. cv2.RETR로 시작하는 상수
3) method : 외곽선 근사화 방법. cv2.CHAIN_APPROX
로 시작하는 상수
4) contours : 검출된 외곽선 좌표. numpy.ndarray로 구성된 리스트
len(contours) = 전체 외곽선 개수(N)
contours[i].shape=(K,1,2).contours[i].dtype=numpy.int32
5) hierarchy : 외곽선 계층 정보. numpy.ndarray.shape=(1,N,4).dtype=numpy.int32
hierarchy[0, i, 0] ~ hierarchy[0, i, 3]이 순서대로 next, prev, child, parent 외곽선 인덱스를 가리킴. 해당 외곽선이 없으면 -1
6) offset : 좌표 값 이동 offset. 기본값은 (0,0)

  • 외곽선 그리기
cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, 
			hierarchy=None, maxLevel=None, maxLevel=None, offset=None) -> image

1) image : 입출력 영상
2) contours : cv2.findContours() 함수로 구한 외곽선 좌표 정보
3) contourIdx : 외곽선 인덱스. (-1)로 지정하면 모든 외곽선을 그린다
4) color : 외곽선 색상
5) thickness : 외곽선 두께. thickness<0이면 내부를 채운다
6) lineType : LINE_4, LINE_8, LINE_AA 중 하나 지정
7) hierarchy : 외곽선 계층 정보
8) maxLevel : 그리기를 수행할 최대 외곽선 레벨. maxLevel=0 이면 contourIdx로 지정된 외곽선만 그린다.

계층 정보를 사용하는 외곽선 검출 예제

import sys
import random
import numpy as np
import cv2


src = cv2.imread('contours.bmp', cv2.IMREAD_GRAYSCALE)

if src is None:
    print('Image load failed!')
    sys.exit()

contours, hier = cv2.findContours(src, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR)

idx = 0
while idx >= 0:
    c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    cv2.drawContours(dst, contours, idx, c, 2, cv2.LINE_8, hier)
    idx = hier[0, idx, 0]

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

계층 정보를 사용하지 않는 외곽선 검출 예제

import sys
import random
import numpy as np
import cv2


src = cv2.imread('milkdrop.bmp', cv2.IMREAD_GRAYSCALE)

if src is None:
    print('Image load failed!')
    sys.exit()

_, src_bin = cv2.threshold(src, 0, 255, cv2.THRESH_OTSU)

contours, _ = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

h, w = src.shape[:2]
dst = np.zeros((h, w, 3), np.uint8)

for i in range(len(contours)):
    c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    cv2.drawContours(dst, contours, i, c, 1, cv2.LINE_AA)

cv2.imshow('src', src)
cv2.imshow('src_bin', src_bin)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

다양한 외곽선 함수

이제 슬슬 어떤 객체를 프로그램이 인식하는 단계까지 도달했다. 곧 이어서는 딥러닝에서 자주 쓰이는 MNIST를 사용하거나 움직이는 객체를 인식하는 것에 대해 다뤄볼 예정이다.

profile
블로그 이전했습니다 (https://phj6724.tistory.com/)

0개의 댓글

관련 채용 정보