이진화된 영상에서 객체의 형태를 조작하는 연산
침식(Erosion), 팽창(Dilation), 열기(Open), 닫기(Close) 등
주로 노이즈 제거, 객체의 형태 보정, 경계 강조, 윤곽선 추출 등에 사용된다.
침식(Erosion) : 객체를 축소하여 작은 노이즈를 제거하는 데 사용
팽창(Dilation) : 객체를 확장하여 빈 공간을 채우는 역할
열기(Open) : 작은 노이즈를 효과적으로 제거하는 데 사용
닫기(Close) : 객체의 내부 구멍을 메우는 데 유용
import cv2
import numpy as np
# 모폴로지 연산
# 영상 처리에서 형태(Shape) 기반으로 이미지를 다루는 기법
# 특히 이진 영상, 흑백 영상에서 물체의 외곽선 크기, 형태를 변경하거나 분석하는데 사용
# 팽창
# 밝은 영역을 넓히는 연산
# 흰색 물체가 커짐, 구멍이 메워짐
# 얇은 부분이 굵어짐
# 침식
# 밝은 영역을 줄이는 연산
# 흰색 물체가 작아짐
# 가느다란 연결부가 끊어짐, 노이즈가 제거됨
# 열림
# 침식 후 팽창: 작은 노이즈를 제거한 후 얇은 부분이 굵어짐
# 닫힘
# 팽창 후 침식: 구멍이 메워짐
img = cv2.imread('./images/circuit.bmp', cv2.IMREAD_GRAYSCALE)
# MORPH_RECT: 사각형, MORPH_ELLIPSE: 타원, MORPH_CROSS: 십자
# gse = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# gse = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# gse = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3))
gse = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
dst1 = cv2.dilate(img, gse)
dst2 = cv2.erode(img, gse)
cv2.imshow('img', img)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)
cv2.waitKey()
확실히 dst1(팽창) 은 흰색 부분이 굵어졌고, dst1(침식) 은 흰색 부분이 얇아진 모습을 확인할 수 있었다.
MORPH_RECT: 사각형, MORPH_ELLIPSE: 타원, MORPH_CROSS: 십자
이 세가지 중 MORPH_RECT: 사각형을 가장 많이 사용한다고 한다.
3가지 모양에 따라 결과가 조금씩 달라지는 것을 확인할 수 있다.
| 항목 | MORPH_RECT | MORPH_ELLIPSE | MORPH_CROSS |
|---|---|---|---|
| 연산 강도 | 강함 | 중간 | 약함 |
| 속도/효율 | 빠름 (단순 구조) | 약간 느림 | 빠름 |
| 방향성 | 모든 방향 | 부드러운 방향성 | 수직/수평 강조 |
| 사용 빈도 | 가장 높음 | 특정 상황 | 드물게 사용 |
import cv2
# 라벨링
# 이미지 속에서 연결된 픽셀 덩어리를 찾아서 각각에 번호를 붙이는 작업
# 이진 영상에서 사용되며, 물체별로 분리, 개수 세기, 위치 분석 등에 활용
# 라벨링 과정
# 1. 이진화: 흑백으로 변환
# 2. 픽셀 그룹 찾기: 상, 하, 좌, 우 방향으로 연결된 그룹, 대각선까지 포함한 그룹
# 3. 각 그룹에 라벨 번호를 부여
# 4. 각 객체의 위치, 크기, 면적, 중심점 등을 구함
img = cv2.imread('./images/keyboard.bmp', cv2.IMREAD_GRAYSCALE) # grayscale에 색상있는 사각형 못 그림
_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# connectedComponentsWithStats: 한 번에 라벨링과 통계값 계산
# cnt: 찾은 객체 개수(배경 포함)
# labels: 각 픽셀의 라벨 번호가 저장된 2D 배열
# stats: 각 객체의 [x, y, width, height, area] 정보, area: 픽셀 개수
# centroids: 각 객체의 중심 좌표(부동소수점 값)
cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(img_bin)
print(cnt)
print('-' * 50)
print(labels)
print('-' * 50)
print(stats)
print('-' * 50)
print(centroids)
print('-' * 50)
for i in range(1, cnt):
(x, y, w, h, area) = stats[i]
if area < 20:
continue
cv2.rectangle(dst, (x, y, w, h), (0, 255, 255)) # 노랑색 사각형
cv2.imshow('img_bin', img_bin)
cv2.imshow('dst', dst)
cv2.waitKey()
if area < 20: 의 의미
area는 픽셀 개수인데, 픽셀 개수가 20보다 작은 것들 노이즈일 것으로 취급한다!!!
노이즈 아닌 객체들만 잘 잡아낸 모습을 볼 수 있었다.
import cv2
import random
import numpy as np
# 외곽선
# 영상에서 같은 색 또는 강도를 가진 경계선을 추적해 얻는 좌표 집합
# 영상 분석에서 물체의 모양, 크기, 위치를 찾을 때 유용
img = cv2.imread('./images/contours.bmp', cv2.IMREAD_GRAYSCALE)
milkdrop = cv2.imread('./images/milkdrop.bmp', cv2.IMREAD_GRAYSCALE)
_, img_bin = cv2.threshold(milkdrop, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
h, w = milkdrop.shape[:2]
dst_milk = np.zeros((h, w, 3), np.uint8)
# img: 이진 이미지 사용
# mode: 컨투어 추출 방식
# RETR_CCOMP: 2계층 구조로 모든 컨투어 추출
# RETR_LIST: 모든 컨투어 추출
# RETR_EXTERNAL: 최외곽 컨투어만 추출
# RETR_TREE: 모든 컨투어를 트리 구조로 추출(복잡한 계층 정보 포함)
# method: 컨투어 근사화 방식
# CHAIN_APPROX_NONE: 모든 경계 좌표 저장(정밀하지만 데이터 큼)
# CHAIN_APPROX_SIMPLE: 직선 구간은 시작/끝만 저장(효율적)
# contours: 외곽선의 좌표 목록(리스트)
# hierarchy: 계층 관계를 나타내는 배열
# hierarchy[0][i] = [next, prev, child, parent]
# next: 같은 계층 레벨에서 다음 컨투어의 인덱스(없으면 -1)
# child: 하위(내부) 컨투어의 인덱스(없으면 -1)
# parent: 상위(외부) 컨투어의 인덱스(없으면 -1)
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
milk_coutours, _ = cv2.findContours(img_bin, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
# print(contours)
# print(hierarchy)
dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
# 전체 외곽선을 한 번에 그림
cv2.drawContours(dst, contours, -1, color, 3)
# -1: → 모든 외곽선(contours)을 한꺼번에 그리겠다는 의미
# color: 1개의 랜덤 컬러를 전체 외곽선에 적용
# 3: 선 두께 (굵은 선)
for i in range(len(milk_coutours)):
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
cv2.drawContours(dst_milk, milk_coutours, i, color, 2)
# i: 인덱스를 하나씩 증가시켜서 각 외곽선 개별 처리
# 매 반복마다 color를 새로 지정 → 각 외곽선마다 다른 색
# 2: 선 두께 (약간 얇음)
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.imshow('dst_milk', dst_milk)
cv2.waitKey()
import cv2
import math
def setLabel(img, pts, label):
(x, y, w, h) = cv2.boundingRect(pts)
pt1 = (x, y)
pt2 = (x + w, y + h)
cv2.rectangle(img, pt1, pt2, (0, 0, 255), 2)
# pt1: 좌상단 꼭짓점
# pt2: 우하단 꼭짓점
# (0, 0, 255): 빨간색 (BGR)
# 2: 선 두께 2px
cv2.putText(img, label, pt1, cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 255))
img = cv2.imread('./images/polygon.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# THRESH_BINARY_INV: 임계값 이상이면 검정(0), 임계값 미만이면 흰색(255)
_, img_bin = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
contours, _ = cv2.findContours(img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
for pts in contours:
if cv2.contourArea(pts) < 50:
continue
# approxPolyDP : 꼭짓점 개수를 줄여 간단한 규칙 기반 분류
approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True) * 0.02, True)
vtc = len(approx)
print(vtc)
if vtc == 3:
setLabel(img, pts, 'TRI')
elif vtc == 4:
setLabel(img, pts, 'RECT')
else:
length = cv2.arcLength(pts, True)
area = cv2.contourArea(pts)
# 4 * math.pi * area / (length * length): 원형도를 구함
# 값의 범위: 0 ~ 1, 1에 가까울수록 원에 가까움
ratio = 4. * math.pi * area / (length * length)
if ratio > 0.8:
setLabel(img, pts, 'CIR')
else:
setLabel(img, pts, 'NONAME')
cv2.imshow('img', img)
cv2.waitKey()
approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True) * 0.02, True)
- pts (curve)
원래 외곽선 (점들의 배열)
보통 cv2.findContours()에서 얻은 외곽선 중 하나 (contour[i])cv2.arcLength(pts, True) * 0.02
(= epsilon : "얼마나 단순화할지"를 결정하는 파라미터)
arcLength()는 외곽선의 둘레 길이를 계산함
* 0.02: 허용 오차 비율 (2%)
→ 이 값이 작을수록 더 정밀하게 근사
→ 클수록 꼭짓점 수가 줄어들고 단순화- True (closed)
True: 외곽선이 닫힌 도형이라고 가정
보통 외곽선은 닫혀 있으므로 True 사용
arcLength(pts, True): 외곽선의 둘레 길이 계산contourArea(pts): 외곽선이 감싸는 면적 계산