에지 검출 알고리즘은 아래의 명암 변화 특성 활용함
현실 세계의 영상 - 물체 경계에서 명암 변화가 계단모양이 아님
🔍 현실 세계를 반영하려면?
필터 u로 컨볼루션 2번 적용
⇒ [-1 -2 1] 필터 1개로 가능
에지에서
➡️ 에지 검출 : 1차 미분에서 봉우리 찾기 or 2차 미분에서 영교차 찾는 일
에지 강도 & 에지 방향
import cv2 as cv
img=cv.imread('soccer.jpg')
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
grad_x=cv.Sobel(gray,cv.CV_32F,1,0,ksize=3) # 소벨 연산자 적용
grad_y=cv.Sobel(gray,cv.CV_32F,0,1,ksize=3)
sobel_x=cv.convertScaleAbs(grad_x) # 절대값을 취해 양수 영상으로 변환
sobel_y=cv.convertScaleAbs(grad_y)
edge_strength=cv.addWeighted(sobel_x,0.5,sobel_y,0.5,0) # 에지 강도 계산
# 결과 영상 윈도우에 디스플레이
cv.imshow('Original',gray)
cv.imshow('sobelx',sobel_x)
cv.imshow('sobely',sobel_y)
cv.imshow('edge strength',edge_strength)
cv.waitKey()
cv.destroyAllWindows()
Sobel()
: Sobel(명암 영상으로 변환, 32비트 실수형, x방향 or y방향 연산자 결정, 그림 크기)convertScaleAbs()
함수 : 부호 없는 8비트형인 CV_8U(numpy의 uint8과 동일) 맵 만듦addWeighted(img1,a,img2,b,c)
: img1 x a + img2 x b + c 계산🌿 결과 영상
1) 에지 추적 시작 화소 설정 : 에지 강도가 T_high 이상인 에지 화소
⇒ 실제 에지일 가능성이 높은 곳에서 추적 시작하기 위해서임
2) T_low 임곗값 넘는 에지 대상으로 추적 진행
⇒ 추적 이력이 있는(= T_low 임곗값 넘는 에지) 이웃 가진 화소는 에지 강도 낮아도 실제 에지로 인정하는 전략!
💡 T_high : T_low의 2~3배로 설정 권고
import cv2 as cv
img=cv.imread('soccer.jpg') # 영상 읽기
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
canny1=cv.Canny(gray,50,150) # Tlow=50, Thigh=150으로 설정
canny2=cv.Canny(gray,100,200) # Tlow=100, Thigh=200으로 설정
cv.imshow('Original',gray)
cv.imshow('Canny1',canny1)
cv.imshow('Canny2',canny2)
cv.waitKey()
cv.destroyAllWindows()
명암 변화에만 의존해서
⇒ 물체 경계에 나타난 에지 & 그림자로 인해 발생한 가짜 에지 구분 불가능
⇒ 인접한 두 물체가 비슷한 명암 가져 명암 변화가 적으면 에지 발생 X
+) 사람 - 에지 검출 시 물체 모양 표현한 3차원 모델 & 눈에 비치는 2차원 겉모습 모델 동시 사용
에지맵 - 에지의 연결 관계가 명시적으로 표현 X
⇒ 에지 연결 ➡️ 경계선 ➡️ 직선으로 변환하면 물체 표현 / 인식에 유리
import cv2 as cv
import numpy as np
img=cv.imread('soccer.jpg') # 영상 읽기
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
canny=cv.Canny(gray,100,200)
contour,hierarchy=cv.findContours(canny,cv.RETR_LIST,cv.CHAIN_APPROX_NONE)
lcontour=[]
for i in range(len(contour)):
if contour[i].shape[0]>100: # 길이가 100보다 크면
lcontour.append(contour[i])
cv.drawContours(img,lcontour,-1,(0,255,0),3)
cv.imshow('Original with contours',img)
cv.imshow('Canny',canny)
cv.waitKey()
cv.destroyAllWindows()
🌿 코드 설명
1) cv.Canny()
: Canny 함수로 에지맵 구하기
2) cv.findContours(canny,cv.RETR_LIST,cv.CHAIN_APPROX_NONE)
: 경계선 찾아 contour 객체에 저장
cv.RETER_LIST
: 맨 바깥쪽 경계선만 찾는 방식cv.CHAIN_APPROX_NONE
: [그림 4-9]처럼 모든 점을 기록 cv.CHAIN_APPROX_SIMPLE
: 직선에 대 해서는 양 끝점만 기록 cv. CHAIN_APPROX_TC89_L1
or cv.CHAIN_APPROX_ TC89_KCOS
: Teh-Chin 알고리즘으로 굴곡이 심한 점을 찾아 그들만 기록3) for문 ; 길이가 50 이상인 경계선만 골라 Icontour 객체에 저장
findContours 함수 - 시작점부터 끝점까지 추적한 후, 역추적하여 시작점으로 돌아오도록 경계선을 표현
⇒ if문에서 길이 기준 100으로 설정 → 실제로는 길이가 50 이상인 경계선만 남김
4) drawContours(img,lcontour,-1,(0,255,0),3
) : 영상에 경계선 그림
이웃한 에지 연결하여 경계선 검출 ⇒ 같은 물체 구성하는 에지 자잘하게 끊겨 나타나는 경우 문제 발생
➡️ 허프 변환 적용 시 끊긴 에지 모아 선분 / 원 등 검출 가능!
현실엔 점 #개, 점들이 완벽히 일직선을 이루지 X
⇒ (a,b) 공간을 이산화하여 해결
ex)
(1) a와 b의 범위를 [-1000,1000]으로 설정
(2) 각각을 크기가 20인 구간 50개로 나누어 칸이 50×50=2500인 2차원 누적 배열 v 생성
(3) v를 0으로 초기화한 후 각각의 직선은 자신이 지나는 모든 칸에 1씩 투표
[그림 4-11] 가상의 누적 배열
직선 방정식 y=ax+b를 사용하면 기울기 a가 무한대인 경우 투표 불가능
⇒ 극좌표에서의 직선의 방정식 도입해서 해결
⇒ 양 끝점을 알아 내려면 비최대 억제 과정에서 극점을 형성한 화소를 찾아 가장 먼 곳에 있는 두 화소를 계산하는 과정 추가로 필요
ex) 평균X, 중앙값 : 아웃라이어를 배제 → 강인한 추정 기법
💡 에지 영상에서 선분을 추정하는 문제에 RANSAC 을 적용한 경우
1) 랜덤하게 두 점을 선택하고 두 점을 지나는 직선을 계산
2) 일정한 양의 오차 허용 → 직선에 일치하는 점의 개수 count
3) 개수가 임곗값 d를 넘지 못하면 가능성이 없다고 보고 버림
4) 1~3의 과정 반복 마치면 후보군에서 최적을 찾아 출력한다.
➡️ 반복 횟수 많을수록 진짜 직선 찾을 가능성 up, 시간은 # 소요
➡️ 난수 사용 → 매 실행마다 다른 결과 출력
물체의 경계를 지정하는 에지가 완벽하다면 영역 분할이 따로 필요 X
but 명암 변화가 낮은 곳에서 뚫려 폐곡선을 형성 못하는 경우 #
사람 - 의미 있는 단위로 분할하는 방식인 의미 분할 사용
여기선 의미 전혀 고려 X, 명암 또는 컬러의 변화만 보고 영역을 분할하는 고전적인 기법만 다룸
ex1) 오츄 이진화에서 여러 임곗값을 사용하도록 확장한 알고리즘
ex2) 군집화 알고리즘 : (r,g,b) 3개 값으로 표현된 화소를 샘플로 보고, 3차원 공간에서 클러스터링 수행하여 화소 각각에 클러스터 번호를 부여
⇒ 이렇게 구성된 영상에서 연결 요소를 찾아 영역으로 간주
영상을 아주 작은 영역으로 분할해 다른 알고리즘의 입력으로 사용
슈퍼 화소 알고리즘 - SLIC (Simple Linear Iterative Clustering) 알고리즘
1) 입력 영상 - k개 화소를 군집 중심으로 지정
화소를 가장 가까운 군집 중심에 할당 단계 & 군집 중심 갱신 단계 반복
2) 화소 할당 단계 : 화소 각각에 대해 주위 4개 군집 중심 ~ 자신까지 거리 계산 ⇒ 가장 유사한 군집 중심에 할당
3) 화소 할당이 끝나면 각 군집 중심은 자신에게 할당된 화소를 평균해 군집 중심을 갱신
4) 모든 군집 중심의 이동량의 평균 < 임계치 ⇒ 수렴했다고 판단하고 알고리즘 멈춤
import skimage
import numpy as np
import cv2 as cv
img=skimage.data.coffee()
cv.imshow('Coffee image',cv.cvtColor(img,cv.COLOR_RGB2BGR))
slic1=skimage.segmentation.slic(img,compactness=20,n_segments=600)
sp_img1=skimage.segmentation.mark_boundaries(img,slic1)
sp_img1=np.uint8(sp_img1*255.0)
slic2=skimage.segmentation.slic(img,compactness=40,n_segments=600)
sp_img2=skimage.segmentation.mark_boundaries(img,slic2)
sp_img2=np.uint8(sp_img2*255.0)
cv.imshow('Super pixels (compact 20)',cv.cvtColor(sp_img1,cv.COLOR_RGB2BGR))
cv.imshow('Super pixels (compact 40)',cv.cvtColor(sp_img2,cv.COLOR_RGB2BGR))
cv.waitKey()
cv.destroyAllWindows()
🌿 코드 설명
두 라이브러리(skimage, OpenCV) 모두 numpy 배열로 영상을 표현하기 때문에 호환 가능
cv.imshow('Coffee image',cv.cvtColor(img,cv.COLOR_RGB2BGR))
: skimage - RGB 순서 / OpenCV - BGR 순서로 저장 → cvtColor 함수로 BGR로 변환하여 출력
skimage.segmentation.slic(img,compactness=20,n_segments=600)
: skimage의 slic 함수로 슈퍼 화소 분할을 수행 결과를 slic1 객체에 저장
sp_img1=skimage.segmentation.mark_boundaries(img,slic1)
: slic1 객체의 분할 정보를 img 영상에 표시하고 결과를 sp_img1 객체에 저장
sp_img1=np.uint8(sp_img1*255.0)
: 0~1 사이의 실수로 표현된 sp_img1을 0~255 사이로 바꾸고 uint8 형으로 변환
이전의 분할 알고리즘 - 지역적 명암 변화만 고려
ex) 물체 색 & 배경색 비슷 → 경계 제대로 형성 X
: 지역적으로 아주 약한 색상 변화지만, 전역적으로 유리한 측면 있으면 그곳을 물체 경계로 간주
➡️ 영상 그래프 표현 후 분할을 최적화 문제로 해결
원래 영역 C → 2개의 영역 C1, C2로 분할했을 때 cut
cut : 영역 분할의 좋은 정도를 측정해주는 목적 함수
⇒ 작을 수록 분할 잘 된 거임
⇒ 영역을 자잘하게 분할하는 경향
ncut : cut을 정규화하여 영역 크기에 중립 되게 한 식
ncut 함수 - 목적 함수로 사용 → 분할을 최적화 문제로 해결 가능
ncut이 작을 수록 좋은 분할 ⇒ 최소화 문제임!
import skimage
import numpy as np
import cv2 as cv
import time
coffee=skimage.data.coffee() # coffee 영상을 읽어
start=time.time() # 분할하는 데 걸리는 시간을 측정해 출력
slic=skimage.segmentation.slic(coffee,compactness=20,n_segments=600,start_label=1)
g=skimage.future.graph.rag_mean_color(coffee,slic,mode='similarity')
ncut=skimage.future.graph.cut_normalized(slic,g) # 정규화 절단
print(coffee.shape,' Coffee 영상을 분할하는데 ',time.time()-start,'초 소요')
marking=skimage.segmentation.mark_boundaries(coffee,ncut) # 원래 영상인 coffee에 영역 분할 정보를 담은 ncut 맵을 이용하여 영역 경계를 표 시하고 marking 객체에 저장
ncut_coffee=np.uint8(marking*255.0) # 0~1 사이의 실수를 가진 marking을 0~255 사이의 uint8 형으로 변환
cv.imshow('Normalized cut',cv.cvtColor(ncut_coffee,cv.COLOR_RGB2BGR)) # RGB로 표현된 분할 영상을 BGR로 변환하여 윈도우에 디스플레이
cv.waitKey()
cv.destroyAllWindows()
slic 함수는 영상을 600개 슈퍼 화소로 분할해 slic1 객체에 저장한다.
rag_mean_color 함수 : 슈퍼 화소를 노드로 사용하고 'similarity'를 에지 가중치로 사용한 그래프를 구성하여 8 객체에 저장
cut_normalized 함수 : slic1과 g 객체 정보를 이용하여 정규화 절단을 수행하고 결과를 ncut 객체에 저장
ncut : 화소에 영역의 번호를 부여한 맵
능동 외곽선(active contour) 알고리즘 : 사용자가 물체 내부에 초기 곡선을 지정하면 곡선을 점점 확장하면서 물체 외곽선으로 접근하는 방법
스네이크를 구현 위한 곡선 표현 방법 ➡️ 스프라인 곡선(spline curve)
스네이크 곡선 g의 에너지 E = 영상 에너지 + 내부 에너지 + 도메인 에너지
➡️ 스네이크 알고리즘이 풀어야 하는 문제 : 에너지 최소로 하는 최적 곡선 찾는 최적화 문제
💡 Kass의 탐색 방법을 Williams가 개선한 알고리즘
Kass의 탐색 방법 : 사용자 지정한 초기 곡선 g0
g0에서 g1, g1~g2, …, gt~gt+1 찾아가는 과정을 수렴할 때까지 반복
GrabCut 알고리즘 : 사용자가 붓으로 물체와 배경을 초기 지정
1) 파란 화소 - 물체 & 빨간 화소 - 배경
⇒ 화소를 가지고 물체 히스토그램과 배경 히스토그램 만듦
2) 나머지 화소들 - 두 히스토그램과 유사성을 따져 물체일 확률 & 배경일 확률 추정
⇒ 물체 영역과 배경 영역을 갱신
3) 새로운 정보로 히스토그램을 다시 만들고 물체 영역과 배경 영역을 갱신 반복
⇒ 영역이 거의 변하지 않으면 수렴한 것으로 간주하고 멈춘다.
[ex 4-7] 사용자가 붓칠한 정보를 이용하여 GrabCut으로 물체를 분할
import cv2 as cv
import numpy as np
img=cv.imread('soccer.jpg') # 영상 읽기
img_show=np.copy(img) # 붓 칠을 디스플레이할 목적의 영상
mask=np.zeros((img.shape[0],img.shape[1]),np.uint8)
mask[:,:]=cv.GC_PR_BGD # 모든 화소를 배경일 것 같음으로 초기화
BrushSiz=9 # 붓의 크기
LColor,RColor=(255,0,0),(0,0,255) # 파란색(물체)과 빨간색(배경)
def painting(event,x,y,flags,param):
if event==cv.EVENT_LBUTTONDOWN:
cv.circle(img_show,(x,y),BrushSiz,LColor,-1) # 왼쪽 버튼 클릭하면 파란색
cv.circle(mask,(x,y),BrushSiz,cv.GC_FGD,-1)
elif event==cv.EVENT_RBUTTONDOWN:
cv.circle(img_show,(x,y),BrushSiz,RColor,-1) # 오른쪽 버튼 클릭하면 빨간색
cv.circle(mask,(x,y),BrushSiz,cv.GC_BGD,-1)
elif event==cv.EVENT_MOUSEMOVE and flags==cv.EVENT_FLAG_LBUTTON:
cv.circle(img_show,(x,y),BrushSiz,LColor,-1)# 왼쪽 버튼 클릭하고 이동하면 파란색
cv.circle(mask,(x,y),BrushSiz,cv.GC_FGD,-1)
elif event==cv.EVENT_MOUSEMOVE and flags==cv.EVENT_FLAG_RBUTTON:
cv.circle(img_show,(x,y),BrushSiz,RColor,-1) # 오른쪽 버튼 클릭하고 이동하면 빨간색
cv.circle(mask,(x,y),BrushSiz,cv.GC_BGD,-1)
cv.imshow('Painting',img_show)
cv.namedWindow('Painting')
cv.setMouseCallback('Painting',painting)
while(True): # 붓 칠을 끝내려면 'q' 키를 누름
if cv.waitKey(1)==ord('q'):
break
# 여기부터 GrabCut 적용하는 코드
background=np.zeros((1,65),np.float64) # 배경 히스토그램 0으로 초기화
foreground=np.zeros((1,65),np.float64) # 물체 히스토그램 0으로 초기화
cv.grabCut(img,mask,None,background,foreground,5,cv.GC_INIT_WITH_MASK)
mask2=np.where((mask==cv.GC_BGD)|(mask==cv.GC_PR_BGD),0,1).astype('uint8')
grab=img*mask2[:,:,np.newaxis]
cv.imshow('Grab cut image',grab)
cv.waitKey()
cv.destroyAllWindows()
1) 영상을 읽어 img 객체에 저장
2) 붓칠한 정보를 표시하는 데 쓸 img_ show 객체를 만듦
3) 사용자의 붓칠에 따라 물체인지 배경인지에 대한 정보를 기록할 배열 mask만듦
: 원본 영상과 크기가 같은 mask 배열을 생성 & 모든 화소를 배경일 것 같음(CV.GC_PR_BGD)으로 초기화
💡 OpenCV는 배경으로 판정된 화소 - cv. GCBGD, 물체로 판정된 화소 - cv. GC FGD, 배경일 것 같은 화소 - cv. GC_PR_BGD, 물체일 것 같은 화소 - cv.GC_PR_FGD로 표시
(해당 값은 순서대로 0, 1, 2, 3)
4) 사용자가 붓칠을 마치고 q를 누르면 37행으로 이동해 분할 시도
: grabCut 함수가 내부에서 사용할 배경 히스토그램 & 물체 히스토그램 생성 후 0으로 초기화
5) grabCut 함수 호출 → 실제 분할을 수행
cv.grabCut(img,mask,None,background,foreground,5,cv.GC_INIT_WITH_MASK)
6) 배경 / 배경일 것 같음 표시된 화소 : 0
물체 / 물체일 것 같음 표시된 화소 : 1
로 바꿔서 mask2 객체에 저장
7) 원본 영상 img x mask2 ⇒ 배경 화소 검게 바꿔서 grab 객체에 저장
8) grab를 윈도우에 디스플레이
1) 불변성 : 변환해도 특징 값 안 변함
2) 등변성 : 특징이 어떤 벼노한에 대해 따라 변함
➡️ 상황에 맞게 특징 선택 중요!
(y,x) : 영역 R에 속하는 화소
영역의 면적 & 중점
열 분산 : 화소들이 수직 방향으로 퍼진 정도
행 분산 : 화소들이 수평 방향으로 퍼진 정도