yoncho`s blog

OpenCV 이미지 검출[1]_가장자리 검출, 윤곽선 검출 | 주요 특징점 검출, 이미지내의 객체를 검출한다 본문

기술, 나의 공부를 공유합니다./[ML] Object Detection

OpenCV 이미지 검출[1]_가장자리 검출, 윤곽선 검출 | 주요 특징점 검출, 이미지내의 객체를 검출한다

욘초 2024. 9. 28. 14:37

OpenCV에서 제일 중요한 부분, 이미지에서 특징점 검출 !!

영상내 주요한 특징점 !! Feature Point를 검출하는 방법
특징점이 존재하는 위치나 해당 특징점을 부각시켜준다.
how??.. 바로!!
픽셀의 색상강도, 연속성, 변화량, 의존성, 유사성, 임계점 등을 이용해
특징인 가장자리(Edge), 윤곽선(Contour), 코나(Corner), 블록껍질, 모멘트(Moment), 직선, 원 등을 구분한다.

가장자리(Edge)검출은 픽셀의 그레이디언트의 상위 임계값과 하위임계값을 사용해 검출한다.
픽셀의 연속성, 연결성등이 있어야하며 가장자리로 인식하지않으면 모두 제거한다.

윤곽선(Contour)검출은 동일한 색상이나 비슷한 강도를 가진 연속 픽셀을 하나로 묶어서 처리한다.
윤곽 검출을 토해 모멘트, 코너, 면적, 경계선, 블록껍질 등을 적용할 수 있다.

1. 가장자리(Edge) 검출

가장자리, 엣지는 흔히 알듯 객체의 가장 바깥 둘레를 의미한다.
즉, 객체의 테두리이다.
이미지에서 가장자리는 전경(Foreground)와 배경(Background)이 구분되는 지점이다.
물체의 가장자리 안의 영역은 전경, 즉 해당 물체의 영역이고
물체의 가장자리 밖의 영역은 배경, 즉 배경 또는 다른 물체의 영역인 셈이다.
그리고 이런 전경과 배경 사이에서의 밝기가 큰폭으로 변하는 지점이다.
즉, 가장자리는 픽셀의 밝기가 급변하는 곳으로 볼 수 있다.

입력이미지에서 가장자리를 검출하기 위해선 미분을 진행한다.
그러나 이미지는 샘플링과 양자화가 거친 데이터이므로
밝기의 평균 변화율이 아닌 밝기의 순간 변화율을 구해야한다.
그러므로 인접한 픽셀들의 차이를 구하며, 하나의 컨벌루션 연산이라 볼 수 있다.

  • 1차 미분 형태 : 극댓값이나 극솟값이 가장자리
  • 2차 미분 형태 : 극값이 아닌 제로 크로싱(기울기가 양수에서 음수로 변할 때 0을 갖는 값의 위치)
    픽셀의 밝기에 따른 미분이 진행된다는 점을 보면
    노이즈에 매우 민감하게 작용하겠다는 것을 알 수 있다.
    그래서 노이즈를 제거하는 전처리를 갖고 가장자리를 검출한다.

꼭 !! 기억하자,, 가장자리는 노이즈 제거를 전처리로 진행한다.

(사진 추후 참조)

미분 형태의 가장자리 유형은 Step, Line, Ramp, Roof형태가 존재한다.
Step Edge에서 노이즈를 제거하면 Ramp Edge가 된다.
Line Edge에서 노이즈를 제거하면 Roof Edge가 된다.
가장 대표적인 가장자리 유형은 Step Edge와 Line Edge 이다.
이런 Step과 Line Edge를 검출하는 방법으로는
소벨 미분(Sobel), 샤르 필터(Scharr), 라플라시안(Laplacian), 캐니 엣지(Canny)가 있다.

여기서 중요하게 볼게 소벨 미분 캐니 엣지이다.

1. 소벨 미분 (Sobel)

소벨 미분은 미분값을 구하는데 가장 많이 사용하는 기본적인 미분법이라 생각하면된다.
앞서 말한 것처럼 인접한 픽셀들의 차이로 기울기(Gradient)의 크기를 구한다.
이때 인접한 픽셀간의 계산을 위해 컨벌루션을 진행한다.
그러므로 커널이 필요한데, 소벨 미분은 이 커널을 소벨 마스크로 명명하여
사용해 미분을 하고있다.
소벨 미분은 커널 내부의 모든 픽셀값의 합이 0이 되어야하며,
3x3 ~ 31x31까지 커널 크기가 가능하다.

소벨 마스크는 거의 모든 크기의 커널로 정의할 수 있으며 (3x3 ~ 31x31),
모든 방향으로 가장자리 검출이 가능하다
크기가 작은 커널은 노이즈에 민감하게 반응하며
크기가 큰 커널은 노이즈에 덜 민감하게 반응한다.
소벨 마스크는 다른 마스크들에 비해 노이즈에 강한편이다.

소벨 연산 함수

dst = cv2.Sobel(
	src,
	ddepth,
	dx,
	dy,
	ksize = None,
	scale = None,
	delta = None,
	borderType = None
)
  • src : 입력 이미지
  • ddepth : 출력 이미지의 정밀도, 대부분 16bit이상의 정밀도를 갖는다. 그이유는 입력이미지가 8bit의 정밀도를 갖게될 시에 미분하면 오버플로가 발생할 수 있다.
  • dx, dy : xorder와 yorder로 미분의 차수를 결정한다. 일반적으로 0, 1, 2의 값을 사용하며 둘의 합이 1 이상이되어야한다. 0은 해당방향으론 미분을 하지않음을 나타낸다.
  • ksize : 커널의 크기로, 최소 3부터 최대 31까지 가능하다.
  • scale : 비율,
  • delta : 오프셋, 비율 & 오프셋은 출력이미지를 반환하기적 적용, 8bit형태 출력이미지를 통해 미분값을 시각적으로 확인할 때 사용
  • bordetType : 테두리 외삽법

dx = 1, dy = 0, ksize = 3인 경우는 3x3 소벨 수직 마스크 형태를 의미한다.

2. 샤르 필터

샤르필터는 소벨 미분의 단점을 보완했다.
소벨 미분은 커널의 크기가 작으면 노이즈의 영향으로 정확도가 떨어지는데, 정확히 3x3 소벨필터의 경우 기울기(Gradient)의 각도가 수평이나 수직에서 멀어질 수록 정확도가 떨어진다.
이 부정확성을 해결하기위해서 샤르 필터를 사용하며
샤르 필더는 소벨 필터보다 빠르고 정확하기 때문에
만약 소벨 필터 3x3을 써야한다면,, 샤르필터 3x3을 쓴다.
참고로 샤르필터는 3x3크기의 커널만 존재한다.

샤르 연산 함수

dst = cv2.Scharr(
	src,
	ddepth,
	dx,
	dy,
	scale = None,
	delta = None,
	borderType = None
)
  • 모두 소벨 미분과 똑같은 매개변수이다.
  • 차이는 샤르는 커널크기가 3x3으로 고정되어있어서, ksize가 없다.

3. 라플라시안

라플라시안은 2차 미분의 형태이다.
1차 미분은 주로 가장자리의 존재 여부를 알기위해 수행되고,
2차 미분은 가장자리가 어두운부분에서 발생한 것인지 밝은 부분에서 발생한 것인지 확인할 수 있다.
2차 미분 방식은 x축과 y축을 따라 2차 미분한 합을 의미 = 라플라시안은 x축, y축을 따라 2차 미분한 합을 의미한다.

높은 값으로 둘러싸인 픽셀이나 커널보다 작은 얼룩은 양수를 최대화 하며, 낮은 값으로 둘러싸인 픽셀이나 커널보다 큰 얼룩은 음수를 최대화한다.

라플라시안 연산 함수

dst = cv2.Laplacian(
	src,
	ddepth,
	ksize,
	scale = None,
	delta = None,
	borderType = None
)
  • 모두 기존 소벨 연산 함수에 쓰인 동일한 매개변수이고
  • ddepth(결과 이미지 정밀도)로는 cv2.CV_8U를 주로 사용한다.
    그 외에도 cv2.CV_16S, cv2.CV_64F 등이 있다. (정밀도가 높다)

라플라시안 연산 함수는 소벨 연산에 기반을 두고 있으므로,
소벨 연산 함수의 매개변수 의미와 활용방식이 동일하다.
차이점은 커널의 크기(ksize)가 소벨 미분 커널을 의미하며
2차 미분 계산을 위해 샘플링 하는 영역의 크기가 다르다.
ksize = 1인 경우 기본 라플라시안 마스크를 사용한다.

4. 캐니 엣지 (Canny)

라플라시안 필터방식을 캐니(사람)가 개선한 방식,
x와 y에 대해 1차미분을 한 후, 네 방향으로 미분을 하는 것이다.
네 방향으로 미분한 결과로 극댓값을 갖는 지점들이 가장자리가 되는 것이다.
성능이 어마무시하며 노이즈에 민감하지않아
강력한 가장자리 검출 알고리즘으로 알려져 있다.

캐니 엣지 동작 순서

  • 1 : 노이즈 제거를 위해 가우시안 필터로 흐림효과 적용
  • 2 : 기울기(Gradient) 값이 높은 지점을 검출 (소벨 마스크 적용)
  • 3 : 최댓값이 아닌 픽셀의 값을 0으로 변경(가장자리만 남기는 작업)
  • 4 : 히스테리시스 임계값 적용

캐니 엣지는 히스테리시스 임계값을 적용해 윤곽을 생성한다.
임계값은 상위, 하위 임계값 두개가 존재하며
픽셀이 상위 임계값보다 큰 기울기를 가지면 픽셀을 가장자리로 인식하고
픽셀이 하위 임계값보다 작은 기울기를 가지면 무시한다.
그러면 상위 임계값 보단 작은데 하위 임계값 보다 큰 경우는 ??
상위 임곗값에 연결된 경우만 가장자리 픽셀로 인식한다.
예를 들어 상위 임계값이 200, 하위 임계값이 100이면
230은 가장자리 픽셀로, 70은 무시하며, 130의 경우는 주변 인접한 픽셀들을 보고
상위임계값과 연결되어있는 형태이면 가장자리로 인식하는 것이다.

캐니 엣지 함수

dst = cv2.Canny(
	src,
	threshold1,
	threshold2,
	apertureSize = None,
	L2gradient = None
)
  • src : 8bit 단일 채널 이미지만 입력이미지로 활용가능
  • threshold1 : 하위 임계값
  • threshold2 : 상위 임계값
  • apertureSize : 소벨 연산자 마스크 크기 (소벨 마스크 크기)
  • L2gradient : l2-norm으로 방향성 그레이디언트를 정확하게 계산할 건지, 정확성은 떨어지지만 속도가 더 빠른 l1-norm으로 계산할지 결정한다. True = L2 / False = L1
    (아닐 수 있는 욘초 잡지식)

인공지능에서 그레이디언트(기울기)를 구하는 방식은 총 2가지
L2, L1방식이 있다.

캐니 엣지 활용

import cv2
import numpy as np

src = cv2.imread("trump_card.jpg", cv2.IMREAD_GRAYSCALE)

dst = cv2.Canny(src, 100, 200, apertureSize=3, L2gradient=True)

cv2.imshow("dst", dst)
cv2.imshow("src", src)

cv2.waitKey(0)
cv2.destroyAllWindows()

보면 가장자리를 엄청 뚜렷하게 정확히 검출하는 것을 볼 수 있다.


2. 윤곽선 검출

가장자리 검출은 입력이미지에서 가장자리만 검출하는 것이지만, 검출된 객체들의 세그먼트(Segment : 서로 다른 두 점을 연결하는 가장 짧은 선)구성 요소가 구분돼있지 않아 어떤 형태인지 알 수 없다. 즉, 이미지에서 가장자리 검출은 했지만 그 가장자리가 어떤 객체의 가장자리인지,, 서로 구분할 수 있는 요소가 없다는 것인데.
이를 윤곽선 검출에서 진행한다.
윤곽선 검출은 전처리로 가장자리로 검출된 픽셀들을 대상으로 세그멘테이션(이미지에서 픽셀을 분류한뒤 그룹화)작업을 해준다.
검출된 윤곽선은 형상의 분석  물체 감지 및 인식에 가장 효과적인 방법이다.

윤곽선 검출 과정에서는 검출하기 좋은 상태의 이미지를 갖고 진행하는 것이다.
노이즈제거가 필수이며, 전처리로 가장자리 검출을 진행해야된다.
그러고 나면 윤곽선 검색 방법  근사 방법을 선택하는 것이다.
윤곽선 검색 방법으로 윤곽점들의 세그먼테이션 방법(선을 이어주는 방식)을 선택할 수 있고,
근사 방법으로 모든 윤곽선에 대한 윤곽점을 반환할 수 도있다.
우리가 윤곽선에서 제일 중요하게 생각해야되는게 계층 구조의 형태이다.

계층 구조

윤곽선 계층구조는 세그먼테이션이 어떻게 분류되었는가에대한 정보를 담고있다.
즉, 물체안에 여러 가장자리 정보가 나온다면 물체의 세그먼테이션, 물체 안의 가장자리에대한 세그먼테이션들이 생겨난다.
물체안의 윤곽선들을 분류해서 필요한 윤곽선들만 쓸 수 있게 하는게 계층 구조이다.
어렵겠지만 이부분은 아래 설명을 더 보면 이해할 수 있을 것이다.

계층 구조는 기본적으로 트리(Tree) 구조의 형태이다.

위 그림에서 A 노드가 트리구조에서 최상위 노드인 루트 노트라 하며,
B와 C 노드의 부모 노드 = A노드라 할 수 있다.
그러면 A 노드한테 B와 C노드는 자식 노드라 한다.
자식노드가 없는 C노드의 경우에는 잎(leaf) 노드라 한다.
잎노드가 아닌 모든 노드는 내부 노드라고도 한다.

윤곽선 계층구조에서 계층단계별 인덱스의 값을 구분해, 다음 윤곽선, 이전 윤곽선, 자식 윤곽선, 부모 윤곽선을 확인할 수 있다.

노드 0의 다음 윤곽선은 같은 레벨(최외각 윤곽선)에 있는 노드 2가 된다.
이전 윤곽선은 존재하지않아 -1을 반환하며, 부모노드도 존재하지않아 -1을 반환, 자식 노드는 존재한다.
이런 식으로 같은 레벨에 있는 노드끼리는 서로 이전, 다음 윤곽선으로 이어져있고,
자식과 부모, 다음, 이전 윤곽선에 대해 존재유무에 따라 해당 노드번호 또는 -1을 반환한다.


윤곽선 검출

윤곽선 검출 함수의 중요 매개변수는 검색방법 과 근사방법이다.
검색방법은 윤곽선의 계층 구조에 영향을 미치며,
근사방법은 윤곽선들의 윤곽점 형태와 개수에 영향을 미친다.

윤곽선 검출 함수

contours, hierarchy = cv2.findContours(
	image,
	mode,
	method,
	offset = None
)
  • contours : 검출된 윤곽선 (반환값)
  • hierarchy : 계층 구조 (반환값)
  • image : 입력이미지
  • mode : 검색방법, 어떤 계층구조의 형태를 사용할 것인지 결정 (아래 flag 참조)
  • method : 근사방법, 윤곽점 표시 방법 설정 (아래 flag 참조)
  • offset : 반환된 윤곽점들의 좌푯값에 이동할 값을 설정한다. 관심영역에서 윤곽선을 검출하거나 다른이미지에 표시하고자할때 사용한다.

mode flag

  • cv2.RETR_EXTERNAL : 최외곽 윤곽선만 검색
  • cv2.RETR_LIST : 모든 윤곽선 검출, 계층 구조 형성하지않음(모든 윤곽선을 동일한 레벨로 간주)
  • cv2.RETR_CCOMP : 모든 윤곽선을 검출, 2단계 계층 구조로 구상화
  • cv2.RETR_TREE : 모든 윤곽선을 검출하고 트리구조로 구성

method flag

  • cv2.CHAIN_APPROX_NONE : 검출된 모든 윤곽점 반환
  • cv2.CHAIN_APPROX_SIMPLE : 수평,수직,대각선 부분을 압축해서 끝점만 반환
  • cv2.CHAIN_APPROX_TC89_KCOS : The-Chain 체인 근사 알고리즘 적용

윤곽선 그리기

다각형 그리기 함수를 활용해 윤곽선을 그린다.

윤곽선 그리기 함수

cv2.drawContours(
	image,
	contours,
	contourIdx,
	color,
	thickness = None,
	lineType = None,
	hierarchy = None,
	maxLevel = None,
	offset = None
)
  • image : 입력 이미지
  • contours : 윤곽선
  • contoursIdx : 윤곽선 번호, 설정하면 지정된 윤곽선만 그린다. 음수를 입력하면 모든 윤곽선을 그린다.
  • color : 색상
  • thickness : 두께
  • lineType : 선형 타입
  • hierarchy : 계층 구조, 윤곽선 검출 함수에서 반환된 계층 구조
  • maxLevel : 계증 구조 최대 레벨, 이미지에 그려질 윤곽선 계층 구조의 깊이 설정, 0으로 설정하면 최상위 레벨만 그려진다.
  • offset : 윤곽선 함수와 동일한 기능

윤곽선 검출 예제

import cv2
import numpy as np

src = cv2.imread("trump_card_1.jpg")
dst = src.copy()

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 230, 255, cv2.THRESH_BINARY)
morp = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)
image = cv2.bitwise_not(morp)

contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

cv2.drawContours(dst, contours, -1, (0,0,255), 3)
for i in range(len(contours)):
	cv2.putText(dst, str(i), tuple(contours[i][0][0]), cv2.FONT_HERSHEY_COMPLEX, 1.3, (255,0,0), 1)
	print(i, hierarchy[0][i])


cv2.imshow("dst", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

  1. 노이즈 제거를 위해 이진화(threshold) 적용
  2. 모폴로지 연산을 통해 스펙클 제거 (morphologyEx)
  3. 반전연산(bitwise_not)을 수행해서 이미지 최외각(네모 윤곽선)을 검출하지 않게 설정
  4. 그후 윤곽선 검출 및 윤곽선 그리기 함수를 적용 (findContours, drawContorus)
Comments