본문 바로가기
AI 컴퓨터 비전프로젝트

[DL] 모폴로지 처리, 도형이미지 구분하기

by 바다의 공간 2024. 10. 8.

4. 모폴로지 처리

- 영상의 밝은 영역이나 어두운 영역을 축소 또는 확대하는 기법을 말합니다.
    cv2.getStructuringElement(구조 요소의 모양, 사이즈)
 

   구조 요소의 모양

    * 직사각형(cv2.MORPH_RECT)
        - 단순한 형태로 모든 요소가 같은 값을 가지는 정사각형 또는 직사각형
        - 팽창과 침식 연산에서 동일하게 작동
        - 객체 가장자리를 따라 명확한 변화를 줄 때 유용

 

    * 타원형(cv2.MORPH_ELLIPSE)
        - 가장자리 부분을 더 부드럽게 처리
        - 객체의 둥근 모양을유지하면서 노이즈를 제거할 때 유용


    * 십자형(cv2.MORPH_CROSS) / 많이 사용하지는 않음
        - 중심을 기준으로 수직 및 수평 방향으로 영향을 주게 됨
        - 얇은 라인 구조를 강화하거나 제거하는데 유용합니다.

 

- 침식(erosion) 연산
    - cv2.erode(영상, 구조요소, 출력영상, 고정점 위치)
    - 이미지를 깎아 내는 연산을 이야기합니다.
    - 객체의 크기가 감소되고 배경이 확대되는 현상을 볼 수 있음


- 팽창(dilation) 연산
    - cv2.dilate(영상, 구조요소출력영상, 고정점 위칭)
    - 물체의 주변을 확장하는 연산
    - 객체 크기가 증가되고 배경은 감소
    - 객체 내부에 홀이 있다면 홀을 채울 수도 있음 

 

== 침식과 팽창의 출력영상과 고정점위치는 생략하는 경우가 많음

각각의 반환 값이 의미하는 것:

  1. cnt (객체의 개수):
    • 찾아낸 연결된 개체의 개수를 반환합니다. 예를 들어, 이미지에 3개의 물체가 있다면 cnt는 3이 됩니다.
  2. labels (레이블 맵):
    • 각 픽셀에 해당하는 레이블 번호가 저장된 이미지입니다.
    • 이미지의 각 픽셀이 어느 개체에 속하는지 알려줍니다. 예를 들어, 레이블 1번은 첫 번째 개체, 레이블 2번은 두 번째 개체에 해당합니다.
  3. stats (개체의 위치와 크기 정보):
    • 각 개체의 통계 정보를 담고 있습니다. 예를 들어, 개체의 왼쪽 상단 좌표, 너비, 높이, 그리고 **크기(픽셀의 수)**를 알려줍니다.
    **stats[i]**는 각 개체 i의 정보를 나타내며:
    • stats[i][0]: 개체의 왼쪽 상단 x 좌표.
    • stats[i][1]: 개체의 왼쪽 상단 y 좌표.
    • stats[i][2]: 개체의 너비 (가로 길이).
    • stats[i][3]: 개체의 높이 (세로 길이).
    • stats[i][4]: 개체의 픽셀 수 (크기).
  4. centroids (개체의 중심 좌표):
    • 각 개체의 중심점을 반환합니다. 이 값은 개체의 x축과 y축 중심 좌표를 나타냅니다.

더 쉽게 말하면

  • 레이블링 이미지 속에서 연결된 물체를 찾아내고 번호를 붙이는 과정.
  • cv2.connectedComponentsWithStats() 함수는 그 물체들을 찾아내고,
    • 몇 개의 물체가 있는지(cnt),
    • 각각의 물체가 어디에 있는지(labels),
    • 물체의 크기와 위치(stats),
    • 물체의 중심 좌표(centroids)를 반환해 줍니다.

이라고 생각하면 좋을 것 같습니다.


import numpy as np
import cv2

img = cv2.imread('./circuit.bmp', cv2.IMREAD_GRAYSCALE)
# gse = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) # 적당한 사각형
# gse = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) #더 큰 정사각형
gse = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 7)) #직사각형 가능
dst1 = cv2.dilate(img, gse) #팽창
dst2 = cv2.erode(img, gse) #침식

cv2.imshow('img', img)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)
cv2.waitKey()

 

커널사이즈 3*3

 

커널사이즈 7*7
]커널 3*7

 

즉 정사각형이아니라 직사각형으로도 침삭하거나 팽창시킬 수 있다.

그렇게 되면, 가로가아니라 세로가 쪽에서 더 확연하게 두꺼워지는 사실을 볼 수 있습니다.

ksize는 가로 세로 강도를 조절할 수 있다는 점을 알 수 있습니다.


 

5. 레이블링(labeling)


- 이진화, 모폴로지를 수행하게 되면 객체와 배경 영역을 구분할 수 있게 됨 -> 

객체 단위 분석을 통해서 각 객체를 분할하여 특징을 분석하고 객체의 위치, 크기정보, 모양분석, ROI추출 등이 가능함 -> 

서로 연결되어있는 객체 픽셀에 고유번호를 할당하여 영역 기반 모양분석, 레이블맵 바운딩 박스픽셀 개수무게중심좌표 등을 반환할 수 있게 하는것이 목표입니다.

-결국엔 객체가 어딨는지 알아내고나서 객체에 고유번호를 주고 그 번호에 따라 
이름이 뭔지 어디있는지 픽셀이 몇개가 있는지 무게중심이 어디에있는지 알 수 있게됩니다.

-함수
cv2.connectedComponents(영상, 레이블맵)
 레이블맵: 픽셀 연결 관계(4방향 연결(위아래좌우) or 8방향 연결(위아래좌우대각선)을 나타내는 정보)
 반환(return) : 객체 갯수, 레이블 맵 행렬

더 많은 값을 리턴시키는 함수
cv2.connectedComponentsWithStats(영상, 레이블맵)
  레이블맵: 픽셀 연결 관계(4방향 연결(위아래좌우) or 8방향 연결(위아래좌우대각선)을 나타내는 정보)
 반환(return)  : 객체 갯수, 레이블 맵 행렬, (객체 위치, 가로세로길이, 면적 등 행렬), 무게중심정보

 

import cv2
import numpy as np

img = cv2.imread('./keyboard.bmp', cv2.IMREAD_GRAYSCALE)

_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)

dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(img_bin)
# print(cnt)  #38개->객체를 38개로 잡았다는것
# print(labels) #행렬
# print(stats) #38개의 좌표크기
# print(centroids) #무게중심정보

#객체로 보는 아이들을 박스로 체크하기
for i in range(1, cnt):
    (x, y, w, h, area) = stats[i]
    #사각형그리기
    cv2.rectangle(dst, (x, y, w, h), (0,255,255))

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

 

여기서보면 cnt의 값이 38개가 나오는것을 확인할 수 있습니다.

38개는 객체의 개수를 의미합니다. (아무리봐도 38개까지는 안가는데 이건 좀 이상해서 아래부분에서 왜 그런지 확인을 했습니다)

stats는 각 cnt의 각 좌표 크기의 좌표 크기를 확인할 수 있고

centroids는 각 객체의 무게중심정보(x,y 가운데 지점)을 볼 수 있습니다.

 

이미지 전처리하지않았을때의 오류

그래서 일단 객체가 어떻게 38개가 나오는지 확인을 해보았는데(dst) 이미지 전처리를 하지 않았기에 노이즈도 하나의 객체로인식을 해버렸다느것을 확인할 수 있습니다.

import cv2
import numpy as np

img = cv2.imread('./keyboard.bmp', cv2.IMREAD_GRAYSCALE)

_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)

dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(img_bin)
# print(cnt)  #38개->객체를 38개로 잡았다는것
# print(labels) #행렬
# print(stats) #38개의 좌표크기
# print(centroids) #무게중심정보

#객체로 보는 아이들을 박스로 체크하기
for i in range(1, cnt):
    (x, y, w, h, area) = stats[i]
    #사각형그리기
    # cv2.rectangle(dst, (x, y, w, h), (0,255,255))
    if area < 30:
        continue
    cv2.rectangle(dst, (x,y,w,h), (0, 255,255))

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

이제 노이즈를 없애야하니까   if를 넣어서 가장 작은 점의 객체의 면적(area)를 없애줄겁니다.

그런데 이 사진에서는 노이즈의 크기가 정말 작고 조그만하기때문에 면적을 30으로주었습니다.

 

여기서 python의 countinue의 함수를 새로 배웠습니다.

를하게 되면 객체가 깔끔해지고 30픽셀보다 작은 객체는  카운팅되지않는다는것을 확인할 수있습니다.

 

그럼 이제 cnt개수가 18개가 나와야되는데 아직 38개로 카운팅되더라구요.

#필터링된 객체의 수만 알고싶어서 작성한 함수
filter_count = 0
for i in range(1, cnt):
    (x,y,w,h, area) = stats[i]
    if area>30:
        filter_count += 1
        cv2.rectangle(dst, (x,y,w,h), (0,255,255))

print(f'필터링된 객체의 개수: {filter_count}')

 

를 하게 되면 18개를 확인할 수 있습니다.

반복문을 통해서 필터링된 객체의 개수만을 뽑을 수 있습니다.


외곽선 검출

cv2.findContours(영상, 검출모드 , 외곽선 좌표 근사화 방법)
    검출모드:
    1)cv2.RETR_EXTERNAL : 외부 외곽선만 검출하는 함수
    2)cv2.RETR_LIST : 모든 외곽선을 검출, 계층관계는 무시
    3)cv2.RETR_CCOMP: 모든 외곽선을 검출, 계층관계를 2단계로 구성합니다.
        첫번째 계층 : 바깥쪽 외곽선
        두번째 계층 :  내부 윤곽선
    4)cv2.RETR_TREE : 모든 외곽선을 검출하고, 전체 계층관계를 구성합니다.
    3,4의 차이점은 구멍이 몇개있던지 전체적인 계층관계를 구성하고 3은 2단계로 구성하게됩니다.

    근사화방법: 외곽선 점들의 저장방식과 정확도를 정의
    1) cv2.CHAIN_APPROX_NONE : 모든 외곽선 좌표를 저장
    2) cv2.CHAIN_APPROX_SIMPLE : 수평, 수직, 대각선 방향 점들의 끝점만 저장하여 압축(빠르고 간결하게 표현가능)

 

 

 

외곽선 그리기 
cv2.drawContours(영상, 외곽선 좌표 정보, 외곽선 인덱스, 색상, 두께)
    외곽선 인덱스 : 외곽선 인덱스를 리스트로 전달하면 해당 인덱스의 외곽선만 그림
   외관선 인덱스 : -1 (모든 외곽선을 그림)

 

외곽선 근사화
검출한 외곽선 정보를 분석하여 정점수가 적은 외곽선 또는 다각형으로 표현할 수 있게 만드는 것을 이야기합니다.
cv2.approxPolyDP(외곽선좌표, 근사화 정밀도 조절, 폐곡선 여부)
    근사화 정밀도 조절 : 입력 외곽선과 근사화된 외곽선 사이의 최대 길이, 값이 작을수록 다각형이 정확해지고, 꼭지점의 수가 늘어나게됩니다.

 

외곽선의 길이구하기

cv2.arcLength(외곽선 좌표, 폐곡선 여부)

 

import cv2
import random

img = cv2.imread('./contours.bmp', cv2.IMREAD_GRAYSCALE)

#바깥쪽 내부 외부를 이용해서 이미지를 찾아줘, 모든 점들의 정보를 다 가져와줘
#hierarchy: 계층정보
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
color = (random.randint(0, 255),random.randint(0, 255),random.randint(0, 255))

#-1은 채워달라는 뜻, 3은 두께
cv2.drawContours(dst, contours, -1, color, 3)
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()

 

contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

코드해석을 하자면

 

외곽선을 찾아줘 이미지에서 찾아줘 CCOMP라고 했으니 이단계(바깥, 내부)로 가져오고 모든 점들의 정보를 가져오는거니까 CONTOURS는 외곽선, hierarchy는 계층정보를 가져옵니다.

 

 

 

 

 


#문제.
#1. 이미지를 읽어서 그레이스케일로 변환 후, 이진화를 수행하시오.
#2. 자동이진화
#3. 외곽선 검출(finding contours)
#4. 밀크드랍과 같은 컬러영상 생성(cv2.cvtColor)
#5. 컬러영상에 milkdrop에서 추출한 외곽선을 랜덤한 색상으로 그림
#6. 화면 띄우기
import cv2
import random
import numpy as np

#이미지읽은 후 그레이스케일로 변환
img = cv2.imread('./milkdrop.bmp', cv2.IMREAD_GRAYSCALE)

#자동 이진화 수행
_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

#외곽선 검출(FINDING CONTOURS)
contours, _ = cv2.findContours(img_bin, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)


#도화지크기 생성
h, w = img.shape[:2]
#컬러 도화지 만들기
dst = np.zeros((h,w,3), np.uint8)

#외곽선 만들기
for i in range(len(contours)):
    color = (random.randint(0,255),random.randint(0,255),random.randint(0,255),)
    cv2.drawContours(dst, contours, i, color,2)

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

이진화 후 외곽선 그려주기


 

이번 예제에서는 opencv문제중 마지막 예제입니다.

 

이 도형

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)
    cv2.putText(img, label, pt1, cv2.FONT_HERSHEY_DUPLEX, 1, (0,0,255))

img = cv2.imread('./polygon.bmp')
#회색화만들기
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

#이진화 작업
#cv2.THRESH_BINARY_INV를 사용하는 이유:색이 반전색이라서 그렇습니다. 흰색바탕에 검정 객체
_, 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) < 200:
        continue
    approx = cv2.approxPolyDP(pts,cv2.arcLength(pts, True)*0.01, 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)
        ratio = 4. *math.pi * area / (length * length)
        if ratio > 0.7:
            setLabel(img, pts, 'CIR')
        else:
            setLabel(img, pts, 'NONAME')

cv2.imshow('img', img)
cv2.imshow('gray', gray)
cv2.imshow('img_bin', img_bin)
cv2.waitKey()