728x90

CAMShift ( Continuously Adaptive Mean Shift Algorithm ) 알고리즘은

Color Segment 방법의 MeanShift 알고리즘을 Streaming(효과,추적) 환경에서 사용하기 위해 개선한 것으로
탐색윈도우의 크기를 스스로 조정하는 기법을 사용하여 Mean-shift의 답점을 보강한다.

객체를 고속으로 추적하는데 사용되며 조도변화, 잡음이 많은 배경에서는 성능이 좋지않은 특징이 있다.

검출된 객체의 영역의 Hue 값의 분포를 이용하여 변화될 위치를 예측하고 탐지한 후
중심을 찾아 객체를 추적하게 된다.

이름에서 알수 있듯이 '연속적인 적응성 평균이동 알고리즘'이므로
mean-shift 를 사용하며 탐색윈도우 크기를 스스로 조정한다는 것이다.

CAMSHIFT 는 널리 알려진 meanshift algorithm의 변형으로 임의의 물체를 추출하기 위해
경험적 분포 (empirical distribution)에서 동작하는 탐색 알고리즘이다.



 Camshift 알고리즘은 먼저 meanshift를 적용하고, 윈도우의 크기를 다음 공식에 의해 갱신한다.

또한 이 윈도우에 가장 잘 맞는 타원을 계산한다. 다시 새롭게 크기가 조정된 윈도우와 이전 윈도우의 위치를 가지고 Meanshift를 적용한다. 이러한 과정은 원하는 정확도가 나올때까지 반복한다.

 

출처: https://techlog.gurucat.net/146 [하얀쿠아의 이것저것 만들기 Blog]

 


import cv2 as cv
import numpy as np
cap = cv.VideoCapture(0)
success, frame = cap.read()

x,y,w,h = cv.selectROI(frame)
track_window = (x, y, w, h)

roi = frame[y:y+h, x:x+w]
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = None
roi_hist = cv.calcHist([hsv_roi], [0], mask, [180], [0, 180])
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)

# Define the termination criteria:
# 10 iterations or convergence within 1-pixel radius.
term_crit = (cv.TERM_CRITERIA_COUNT | cv.TERM_CRITERIA_EPS, 10, 1)
success, frame = cap.read()
cv.flip(frame,1)
while success:
    # Perform back-projection of the HSV histogram onto the frame.
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    back_proj = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
    # Perform tracking with CamShift.
    rotated_rect, track_window = cv.CamShift(
    back_proj, track_window, term_crit)
    box_points = cv.boxPoints(rotated_rect)
    box_points = np.int0(box_points)
    cv.polylines(frame, [box_points], True, (255, 0, 0), 2)
    cv.imshow('back-projection', back_proj)
    cv.imshow('camshift', frame)
    k = cv.waitKey(1)
    if k == 27: # Escape
        break
    success, frame = cap.read()
cap.release()
cv.destroyAllWindows()

잘 인식하는 사진을 캡쳐해서 그렇지 배경에 조도변화랑 노이즈가 너무 많아서 조금만 움직여도 엉뚱한걸 인식한다 보완 할 점이 많아보인다 

728x90
728x90

by 다크 프로그래머 블로그 ( 시험 때 공부할 겸 그대로 퍼왔슴둥 )

darkpgmr.tistory.com/64

 

영상에서 물체를 추적하고자 할 때, 보통 가장 먼저 떠오르는게 mean-shift 방법일 것이다. 하지만 막상 mean-shift를 이용하여 영상 추적을 하려면 실제 어떻게 해야 하는지 막막한 경우가 많다. 가장 기본적인 방법임에도 불구하고 처음에는 헤매기 쉽다 (저도 그랬습니다 ^^).

 

1. Mean Shift 란?

 

시력이 매우 나쁜 사람을 산에다가 던져놓고 산 정상을 찾아오라고 하면 어떻게 될까? 당연히 눈이 보이는 한도 내에서는 가장 높은 쪽으로 걸음을 옮길 것이고, 이렇게 가다보면 산 정상에 다다를 수 있을 것이다. 이러한 탐색 방법을 힐 클라임(Hill Climb) 탐색 방법이라고 부르는데, Mean Shift도 Hill Climb 탐색 방법의 일종이다.

 

Mean Shift는 어떤 데이터 분포의 peak 또는 무게중심을 찾는 한 방법으로서, 현재 자신의 주변에서 가장 데이터가 밀집된 방향으로 이동한다. 그러다 보면 언젠가는 분포 중심을 찾을 수 있을 거라는 방법이다.

 

예를 들어, 2차원 평면상에 데이터들이 분포되어 있는데 Mean Shift를 이용하여 이 데이터들이 가장 밀집된 피크(peak) 점을 찾는다고 하자. Mean Shift의 탐색반경을 r이라 했을 때, Mean Shift 알고리즘은 다음과 같다.

  1. 현재 위치에서 반경 r 이내에 들어오는 데이터들을 구한다: (x1,y1), (x2,y2), ..., (xn,yn)
  2. 이들의 무게중심의 좌표 (∑xi/n, ∑yi/n)로 현재 위치를 이동시킨다.
  3. 1~2 과정을 위치변화가 거의 없을 때까지, 즉 수렴할 때까지 반복한다.

 

Mean Shift란 용어는 평균(mean)을 따라 이동(shift)시킨다는 뜻이다. Shift란 용어는 어떤 것이 현재 상태 그대로 위치만 변할 때 사용된다. 마이클 잭슨의 빌리지인~ 춤을 생각하기 바란다. 여기서는 크기가 고정된 탐색 윈도우를 이동시킨다는 의미이다.

 

 

2. Mean Shift 에 대해 좀 더 알아보기

 

앞서, Mean Shift는 Hill Climb 탐색 방법의 일종이라고 했는데, 비슷한 뜻으로 gradient descent search, blind search, greedy search 등이 있다. 이러한 류의 탐색 방법은 효과적이기는 하지만 local minimum에 빠지기 쉽다는 문제점이 있다. 예를 들어 앞의 산의 정상을 찾는 문제에서 던져진 사람은 바로 옆에 높은 산이 있더라도 처음 출발한 산의 정상만을 찾게 될 것이다. 만일 처음에 매우 완만한 구릉에서 출발했다면 그 구릉 꼭대기에서 멈춰버릴 것이고 그 옆의 진짜 높은 산 정상은 가지 못할 것이다.

 

Mean Shift와 같은 Hill Climb 방식은 global minimum을 찾지 못하며, 초기위치(출발위치)에 따라서 최종적으로 수렴하는 위치가 달라질 수 있다. 초기 위치는 문제에 따라 미리 주어지거나, 아니면 무작위로 임의의 위치에서 시작하거나, 혹은 전체 데이터의 평균 위치에서 출발하는 등의 방법이 있을 수 있다.

 

Mean Shift에서는 기본적으로 주변의 지역적인(local) 상황만 보고 진행방향을 결정하기 때문에, Mean Shift를 적용하기 위해서는 먼저 탐색반경(search radius)의 설정이 필요하다. 탐색반경을 너무 크게 잡으면 정확한 피크(peak) 위치를 찾지 못하게 되며, 반대로 너무 작게 잡으면 local minimum에 빠지기 쉬워진다.

 

Mean Shift의 가장 큰 단점 중의 하나는 탐색 윈도우(탐색 반경)의 크기를 정하는 것이 쉽지 않다는 것이다. 특히 영상 추적의 경우 대상 물체의 크기, 형태 변화에 따라 탐색 윈도우의 크기나 형태를 적절히 변경해 주어야 하는데 이게 적절히 변경되지 않으면 추적 성능에 많은 영향을 끼치게 된다.

 

Mean Shift는 물체추적(object tracking) 외에도 영상 세그멘테이션(segmentation), 데이터 클러스터링(clustering), 경계를 보존하는 영상 스무딩(smoothing) 등 다양한 활용을 갖는다. 구체적 활용 예 및 보다 심도깊은 이론적 내용에 대해서는 "Mean shift: a robust approach toward feature space analysis", PAMI 2002년도 논문을 참조하기 바란다.

 

 

3. Mean Shift를 이용한 영상 이동체 추적

 

영상추적을 하는 사람들은 Mean Shift를 하나의 추적방법인 것처럼 생각하지만, Mean Shift는 하나의 일반적인 방법론적 도구일 뿐이다. 따라서, Mean Shift를 가지고 영상 추적을 하는 것은 사실은 별개의 얘기이다.

 

Mean Shift를 이용한 영상추적 방법은 사실 Comaniciu, Ramesh, Meer, "Real-time tracking of non-rigid objects using mean shift", CVPR 2000 논문에 있는 방법을 지칭한다 (저자들은 mean-shift에 대해 특허도 걸어 놓았다). 여기서 설명하고자 하는 mean shift 추적 방법도 기본적으로는 이 논문에 있는 방법이다.

 

기본 아이디어는 아래 그림과 같이 추적하고자 하는 대상 물체에 대한 색상 히스토그램(histogram)과 현재 입력 영상의 히스토그램을 비교해서 가장 유사한 히스토그램을 갖는 윈도우 영역을 찾는 것이다.

 

그런데 이렇게 직접 히스토그램을 비교할 경우에는 모든 가능한 윈도우 위치에 대해 각각 히스토그램을 구하고 또 비교해야 하기 때문에 시간이 너무 오래 걸린다.

 

따라서 실제로는 아래 그림과 같이 histogram backprojection 기법과 mean shift를 결합한 방법을 사용하는 것이 일반적이다 (이렇게 해도 가장 유사한 히스토그램 영역을 찾을 수 있다는게 위 CVPR 2000 논문의 핵심 내용이며, 논문에 그 증명이 나와있다)

 

 

먼저, 영상에서 추적할 대상 영역이 정해지면 해당 윈도우 영역에 대해 히스토그램을 구하여 객체 모델로 저장한다. 이후 입력 영상이 들어오면 histogram backprojection을 이용해서 입력 영상의 픽셀값들을 확률값으로 변경시킨다. 이렇게 구한 확률값 분포에 대해 mean shift를 적용하여 물체의 위치를 찾는다. 만일 물체의 크기 변화까지 따라가고 싶은 경우에는 찾아진 위치에서 윈도우의 크기를 조절해 가면서 저장된 모델과 가장 히스토그램 유사도가 큰 스케일(scale)을 선택한다.

 

 

4. Mean Shift 추적 구현

 

[히스토그램 구하기]

색상 모델은 RGB, HSV, YCbCr 등 어느것을 써도 된다 (큰 차이가 없다). 또는 그레이(gray)를 사용하거나 HSV의 H(Hue)만 사용해도 된다. 히스토그램은 윈도우에 들어오는 픽셀들에 대해 각 색상별로 픽셀 개수를 센 다음에 확률적 해석을 위해 전체 픽셀수로 나눠주면 된다.

 

[히스토그램 백프로젝션(backprojection)]

현재 입력 영상에 있는 픽셀 색상값이 추적하고자 하는 객체 모델에 얼마나 많이 포함되어 있는 색인지를 수치화하는 과정이다. 모델 히스토그램을 Hm, 입력 이미지 I의 픽셀 x에서의 색상값을 I(x)라 하면 백프로젝션 값은 w(x) = Hm(I(x)) 와 같이 구할 수 있다. 그런데, 보통은 현재 입력 영상에 대한 히스토그램 H를 구한 후 w(x) = sqrt{Hm(I(x))/H(I(x))}와 같이 모델 히스토그램 값을 현재 영상 히스토그램 값으로 나누는 것이 일반적이다. 이렇게 값을 나누어 주면 모델에 포함된 색상값들이 그 비율에 관계없이 일정하게 높은 w값을 갖게 하는 효과가 있다. 예를 들어, 추적하고가 하는 물체가 빨간색 영역이 10%, 파란색 영역이 90%로 구성된 경우를 생각해 보기 바란다.

 

[Mean Shift 적용]

히스토그램 백프로젝션을 통해 얻은 w값들을 일종의 확률값처럼 생각하고 mean shift를 적용하는 것이다. 이전 영상 프레임(frame)에서의 물체의 위치를 초기 위치로 해서 다음과 같이 mean shift를 적용한다.

즉, w를 가중치(weight)로 해서 현재 윈도우(window)내에 있는 픽셀 좌표들의 가중평균(무게중심) 위치를 구하는 것이다. 이렇게 구한 xnew가 새로운 윈도우의 중심이 되도록 윈도우를 이동시킨 다음에 수렴할 때까지 이 과정을 반복하는 것이다. 식에 있는 K(ri)에서 K는 커널(kernel) 함수, ri는 현재 윈도우 중심에서 xi까지의 거리를 나타낸다. 커널함수는 배경의 영향을 줄이기 위한 목적으로 사용하는데, 윈도우 중심에서 가장 높은 값을 갖고 중심에서 멀어질수록 값이 작아지는 방사형의 symmetric 함수가 주로 사용된다. 아무래도 윈도우의 경계 부근에서는 배경 픽셀이 포함될 확률이 높기 때문에 경계로 갈수록 가중치를 낮춰주는 것이다. 실제 커널 함수로는 Epanechnikov 함수가 주로 사용된다.

아니면 그냥 윈도우에 내접하는 타원에 대해 타원 내부에 있으면 1, 외부면 0인 함수를 커널로 사용해도 된다.

 

[히스토그램 유사도 측정]

두 히스토그램 H1 = {p1,...,pn}, H2 = {q1,...,qn} 사이의 유사도는 보통 Bhattacharyya 계수(coefficient)를 이용해서 계산한다.

Bhattacharyya 계수는 두 히스토그램이 일치할 때 최대값 1, 상관성이 하나도 없으면 최소값 0을 갖는다.

 

[물체의 크기(scale) 결정]

일단 mean shift로 위치를 찾은 다음에 윈도우의 크기를 조금씩 변경시켜 보면서 모델 히스토그램과 현재 윈도우 영역에 대한 히스토그램을 비교한다. Bhattacharyya 계수가 가장 큰 경우를 찾으면 된다.

 

 

5. Mean Shift 샘플 동영상

 

직접 구현한 Mean Shift 추적기를 실제 영상추적에 적용한 샘플 동영상이다. 녹색이 배경과 잘 구분되기 때문에 초반에는 비교적 잘 추적하지만 후반부에는 같은 팀 선수들 사이로 여기저기 옮겨다니는 걸 볼 수 있다.

 

youtu.be/EI3wJDW_3wE

 

 

6. Mean Shift 추적에 대한 생각

 

Mean Shift 추적은 일단은 가장 기본적인 방법이기 때문에 영상 추적을 한다면 한번 정도는 직접 해 볼 필요가 있다. 하지만 성능은 그다지 기대하지 않는 것이 좋다. 추적하고자 하는 물체와 배경이 유사한 색상을 가지면 실패하기 쉽상이기 때문이다. 또한 물체의 크기가 변하는 경우도 큰 문제이다. Mean Shift 추적은 윈도우 크기가 잘못되면 성능이 형편없어진다.

 

그럼에도 불구하고 Mean Shift 추적은 대상의 형태가 변하는 경우에 유용하며, 가볍고 빠르기 때문에 나름 유용하다. 일반적인 실외환경이 아니라 제한된 환경 및 응용에서는 Mean Shift가 매우 강력한 방법이 될 수도 있다. 또는 Mean Shitf를 다른 추적 방법과 상호 보완적으로 결합하여 사용하는 것도 좋은 방법이 될 것이다.

 

아래 동영상은 유투브에서 발견한 것인데 mean shift가 성공적으로 적용되는 경우들을 보여준다. 후반부에 나오는 경찰 추격신이 꽤나 흥미롭다.

www.youtube.com/watch?v=RG5uV_h50b0&feature=emb_imp_woyt


<내 실습코드>

 

import cv2 as cv
cap = cv.VideoCapture(0)
# Capture several frames to a
success, frame = cap.read()
x,y,w,h = cv.selectROI(frame)
track_window = (x, y, w, h)
roi = frame[y:y+h, x:x+w]
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = None
roi_hist = cv.calcHist([hsv_roi], [0], mask, [180], [0, 180])
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
term_crit = (cv.TERM_CRITERIA_COUNT | cv.TERM_CRITERIA_EPS, 10, 1)
while success:
    # Perform back-projection of the HSV histogram onto the frame.
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    back_proj = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
    # Perform tracking with MeanShift.
    num_iters, track_window = cv.meanShift(back_proj, track_window, term_crit)
    # Draw the tracking window.
    x, y, w, h = track_window
    cv.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
    cv.imshow('back-projection', back_proj)
    cv.imshow('meanshift', frame)
    k = cv.waitKey(1)
    if k == 27: # Escape
        break
    success, frame = cap.read()
    cv.flip(frame, 1)
    # When everything done, release the capture
cap.release()
cv.destroyAllWindows()

에어팟 프로를 잘 추적하는데 가끔씩 마스크를 지나갈 때 local minima에 빠져서 비슷한 픽셀값을 가진 마스크를 에어팟으로 인식하는 경우도 있다

728x90
728x90

해당 내용은  https://bskyvision.com/21 블로그 포스팅 내용이지만 시험기간 때 공부하려고 그대로 퍼왔습니다

문제가 있을시 바로 삭제하겠습니둥

 

SIFT (Scale-Invariant Feature Transform)은 이미지의 크기와 회전에 불변하는 특징을 추출하는 알고리즘입니다. 서로 다른 두 이미지에서 SIFT 특징을 각각 추출한 다음에 서로 가장 비슷한 특징끼리 매칭해주면 두 이미지에서 대응되는 부분을 찾을 수 있다는 것이 기본 원리입니다. 즉, 그림 1과 같이 크기와 회전은 다르지만 일치하는 내용을 갖고 이미지에서 동일한 물체를 찾아서 매칭해줄 수 있는 알고리즘입니다.

 

그림 1. SIFT로 이미지 매칭한 예

 

위에 두 이미지에서 동일한 부분인 책을 찾아서 매칭해준 것을 확인할 수 있습니다. 중요한 것은 이미지의 크기도 다르고, 책의 회전? 방향도 다르고, 다른 물체들에 조금씩 가려져 있는데도 일치되는 부분을 잘 찾아서 매칭해주었습니다. 이것이 바로 SIFT의 장점입니다. SIFT는 크기나 회전에 불변하는 특징을 찾아내줍니다. 위의 이미지들은 흑백이라 조금 보기 안 이쁘니 다른 예를 하나 더 살펴보겠습니다. 그림 2와 그림 3을 보시죠 (참고로 이 사진은 부산에 아는 형 결혼식 갔다가 찍은 사진입니다.) 그림 2에서는 왼쪽 아래에 있는 빨간 색 차를 매칭해봤습니다. 차 사진이 회전되었고, 좀 더 흐려졌음에도 잘 매칭되었음을 확인할 수 있습니다.  

그림 2. SIFT를 이용해서 두 이미지 안에 있는 빨간 색 차를 매칭

 

 

그림 3에서는 표지판을 매칭했는데, 두 포인트 정도 완전히 엇나간 것을 제외하고는 비교적 잘 매칭되었습니다.

 

그림 3. SIFT를 이용해서 두 이미지 안에 있는 표지판을 매칭

  

이런 식으로 작동하는 SIFT는 파노라마 영상을 만들 때도 사용됩니다. 파노라마 사진 만드는 알고리즘을 보진 않았지만 아마도 여러 장 조금 겹치게 찍은 사진에서 서로 겹치는 부분을 찾아서 옆에 붙이고 붙여주는 원리를 갖고 있지 않을까 싶네요. 그리고 SIFT는 두 장의 이미지가 좌우로 조금 다른 수평 디스패리티를 갖고 있는 스테레오 이미지를 매칭할 때도 사용될 수 있습니다. 

그렇다면, 도대체 SIFT 알고리즘은 어떤 순서를 갖고 있기에 회전과 크기 변화에도 robust하게 서로 매칭되는 부분을 잘 찾아낼 수 있는 것일까요? 절차를 하나씩 잘 살펴보겠습니다. 대략적으로 아래와 같은 순서로 진행됩니다.

 

1. "Scale space" 만들기

2. Difference of Gaussian (DoG) 연산

3. keypoint들 찾기

4. 나쁜 Keypoint들 제거하기

5. keypoint들에 방향 할당해주기

6. 최종적으로 SIFT 특징들 산출하기

 

자, 그러면 하나씩 살펴보도록 하겠습니다. 

 

 

1. "Scale space" 만들기 

 

첫번째로 "Scale space" 만들기입니다. Scale space에 대한 개념은 아래 링크를 참고하세요.(http://bskyvision.com/144) 먼저 원본 이미지를 두 배로 크게 만든 다음에 점진적으로 블러되게 만듭니다. 그리고 원본 이미지를 점진적으로 블러되게 만듭니다. 그 다음에는 원본 이미지를 반으로 축소시킨 이미지에서 또한 점진적으로 블러된 이미지들을 만들어냅니다. 그리고 또 반으로 축소시킨 다음에 점진적으로 블러된 이미지들을 만들어냅니다. (원본 이미지를 일단 2배로 키우는 이유는 나중에 Difference of Gaussian (DoG) 이미지를 만들 때 같은 옥타브 내에서 인접한 2개의 블러 이미지를 활용해서 만들고, 또 그렇게 생성된 DoG 이미지들 중에서 인접한 세 개의 DoG 이미지를 활용해서 Keypoint들을 찾기 때문입니다.) 그림 4에 구현한 결과를 나타냈습니다. 

 

그림 4. Scale space 만든 결과

 

blurring, 즉 흐리게 만드는 것은 가우시안 연산자와 이미지를 컨볼루션 연산해줌으로 가능합니다. 공식으로 나타내면 아래와 같습니다. 

 

...(1)

 

여기서 L은 블러된 이미지이고, G는 가우시안 필터이고, I는 이미지입니다. x, y는 이미지 픽셀의 좌표값이고, 

는 "scale" 파라미터입니다. 이거 이름을 왜 scale 파라미터라고 했는지는 잘 모르겠지만, 아무튼 블러의 정도를 결정짓는 파라미터입니다. 값이 클수록 더 흐려집니다. 아래는 가우시안 필터의 공식입니다. 

 

 

...(2)

점차적으로 블러된 이미지를 얻기 위해서 

값을 k배씩 높여갔습니다. 처음 

값을 

으로, k 는 

로 설정했습니다. 2배로 키운 원본이미지가 처음에는 

의 값으로 블러되었다면, 그 다음에는 1이 되고, 그 다음에는 

, 또 그 다음에는 2의 값으로 블러가 되는 방식입니다. 점점 더 흐려진 결과를 볼 수 있습니다. 원본 이미지에서는 

에서 시작해서 k배씩 점차적으로 블러되게 해줍니다. 그리고 원본 이미지의 크기를 가로, 세로 각각 반씩 줄인 다음에는 

에서 시작해서 k배씩 점차적으로 블러되게 해줍니다. 같은 사이즈를 갖지만 다른 블러의 정도를 갖는 이미지가 각각 5장씩 존재하고, 총 4개의 그룹을 형성하게 됩니다. 여기서 같은 사이즈의 이미지들의 그룹을 octave라고 명칭합니다. 즉, 세로로 배열되어 있는 이미지들이 한 옥타브를 형성합니다. 아마도 우리가 어떤 물체를 볼 때 거리에 따라 명확하거나 흐리게 보이기도 하고, 크거나 작게 보이기도 하는 현상을 담으려고 이런 방법을 쓰는 것이 아닐까 싶습니다. 이 과정 덕분에 SIFT 특징은 이미지의 크기에 불변하는 특성을 갖게 됩니다. 

 

 

2. DoG 연산

 

전 단계에서 저희는 이미지의 scale space를 만들었습니다. 다양한 scale값으로 Gaussian 연산처리된 4 그룹의 옥타브, 총 20장의 이미지를 얻었습니다. 이제 Laplacian of Gaussian (LoG)를 사용하면 이미지 내에서 흥미로운 점들, 즉 keypoint들을 찾을 수 있습니다. LoG 연산자의 작동 원리를 간단히 말하자면, 먼저 이미지를 살짝 블러한 다음에 2차 미분을 계산합니다. 식으로 나타내면 다음과 같습니다.

 

...(3)

 

LoG 연산을 통해서 이미지 내에 있는 엣지들과 코너들이 두드러지게 됩니다. 이러한 엣지들과 코너들은 keypoint들을 찾는데 큰 도움을 줍니다. 하지만 LoG는 많은 연산을 요구하기 때문에, 비교적 간단하면서도 비슷한 성능을 낼 수 있는 Difference of Gaussian (DoG)로 대체합니다. LoG, DoG 모두 이미지에서 엣지 정보, 코너 정보를 도출할 때 널리 사용되는 방법들입니다. DoG는 매우 간단합니다. 전 단계에서 얻은 같은 옥타브 내에서 인접한 두 개의 블러 이미지들끼리 빼주면 됩니다. 그림 5는 그 과정을 설명해줍니다. 

그림 5. SIFT 내에서의 DoG 연산 [2]

 

이 과정을 실제 이미지로 구현한 결과는 그림 6에 있습니다. 

 

그림 6. DoG 연산을 실제로 구현한 결과

중간에 있는 DoG 이미지들 그냥 검은색 이미지 아닙니다. 컴퓨터 화면이 얼룩진건가 하시는 분들도 있을 텐데 자세히 봐보세요. 얼룩말의 형상이 보일 것입니다. 그래도 잘 안보이는 것 같아서, 정규화한 버전의 DoG 이미지들도 오른쪽에 추가했습니다. 이미지의 엣지 정보들이 잘 도출된 것을 확인할 수 있습니다. 이 과정이 모든 octave에서 동일하게 진행됩니다. 그러면 결과적으로 4개씩, 16장의 DoG이미지를 얻겠죠?? 

여기서 잠깐 LoG와 DoG에 대해 좀 더 깊이 살펴보고 지나가겠습니다. LoG를 DoG로 대체할 때 시간이 적게 걸린다는 것 외에도 장점이 더 있습니다. LoG는 scale 불변성을 위해 

로 라플라시안 연산자를 정규화(normalization)해줘야합니다. 즉, LoG연산자가 아래와 같이 scale-normalized LoG로 변합니다. 

 

 

...(4)

 

scale-normalized LoG의 극대값, 극소값은 매우 안정적으로 이미지 특징을 나타냅니다. 그래서 이 극대값, 극소값들은 keypoint의 후보가 되는 것입니다. 그러면 도대체 어떻게 LoG가 DoG로 대체될 수 있을까요? 이것을 증명하기 위해서 열 확산 방정식이 응용됩니다. 

 

...(5)

 

열 확산 방정식에 의해 이런 관계가 형성된다고 합니다. Gaussian을 

에 대해 미분한 것은 LoG에 

를 곱한 것과 같다는 의미죠. 이것은 미분함수의 성질을 이용하면 다음과 같이 전개될 수 있습니다.

 

...(6)

 

양변에 우변의 분모를 각각 곱해주면, 아래와 같은 식이 나오게 되죠.

 

...(7)

 

결국 다른 scale을 갖고 있는 Gaussian 이미지들끼리의 합, 즉 DoG는 scale-normalized LoG에 (k-1)을 곱한 것과 거의 같습니다. 따라서 DoG는 scale 불변성을 보장하는 

scale 정규화 과정을 자연스럽게 포함하게 됩니다. 그리고 곱해진 (k-1)은 극대값, 극소값을 찾는데는 아무런 영향을 주지 않기 때문에 무시해도 괜찮습니다. 암튼 이러한 방식으로 LoG는 DoG로 무리없이 잘 대체됩니다. (오히려 상당부분 이득을 보면서 대체됩니다.) 

이제 이 DoG 이미지들을 활용해서 흥미있는 keypoint들을 찾아낼 것입니다. 

 

 

3. keypoint들 찾기

 

이제 DoG 이미지들에서 keypoint들을 찾을 차례입니다. 먼저 DoG 이미지들 내에서 극대값, 극소값들의 대략적인 위치를 찾습니다. 그림 7에 극대값, 극소값의 위치를 찾는 방법이 설명되어 있습니다. 

 

그림 7. DoG이미지에서 극소값, 극대값 찾는 원리

한 픽셀에서의 극대값, 극소값을 결정할 때는 동일한 octave내의 세 장의 DoG 이미지가 필요합니다. 체크할 DoG이미지와 scale이 한 단계씩 크고 작은 DoG 이미지들이 필요합니다. 지금 체크할 픽셀 주변의 8개 픽셀과, scale이 한단계씩 다른 위 아래 두 DoG 이미지에서 체크하려고 하는 픽셀과 가까운 9개씩, 총 26개를 검사합니다. 만약 지금 체크하는 픽셀의 값이 26개의 이웃 픽셀값 중에 가장 작거나 가장 클 때 keypoint로 인정이 됩니다. 그러므로 4장의 DoG 이미지들 중에서 가장 위에 있고 가장 아래에 있는 DoG 이미지들에서는 극대값, 극소값을 찾을 수 없습니다. 이런 방식으로 DoG 이미지의 모든 픽셀에서 Keypoint들을 찾습니다. 좀 더 확실한 이해를 위해 이 과정을 실제 이미지로 구현해보겠습니다. 이번에도 octave1에서의 결과만 보이도록 하겠습니다. 그림 8을 확인하세요. 나머지 octave들에서도 동일한 연산이 이루어진다는 것을 잊지 마세요. 

 

그림 8. 극대값, 극소값 찾기

 

흰색 점들로 표현된 곳이 극대값, 극소값들입니다. 역시 잘 안보이지만 모니터의 각도를 좀 바꿔보면 잘 보이는 각도가 있을거에요. 암튼 이 점들이 일차적으로 keypoint들이 됩니다. 여기서 보면 두 장의 극값 이미지를 얻기 위해서는 4장의 DoG이미지가 필요하다는 것을 알 수 있습니다. 이를 위해서 scale space를 만들 때 5 단계의 가우시안 블러 이미지를 만든 것입니다. 5장의 블러 이미지에서 4장의 DoG 이미지가 나오고, 또 4장의 DoG 이미지에서 2장의 극값 이미지가 나오는 거죠. 네 그룹의 octave를 갖고 있으니, 8장의 극값 이미지를 얻게 됩니다. 그런데, 이렇게 얻은 극값들은 대략적인 것입니다. 왜냐하면, 그림 9에 설명한 것과 같이 진짜 극소값, 극대값들은 픽셀들 사이의 공간에 위치할 가능성이 많기 때문이죠. 

 

그림 9. 진짜 극값의 위치

 

그런데 우리는 이 진짜 극소값, 극대값들의 위치에 접근할 수 없습니다. 그래서 subpixel 위치를 수학적으로 찾아내야 합니다. subpixel은 이렇게 픽셀들 사이에 위치한 것을 의미하는 것 같습니다. 좀 더 정확한 극값들을 어떻게 찾아내는가 하면, 바로 테일러 2차 전개를 사용합니다. (여기 테일러 전개에 대한 이론적인 내용은 저도 잘 모르겠네요. 일단 대략적인 개념만 이해하고 넘어가셔도 좋습니다.) 

 

...(8)

 

여기서 D는 DoG이미지이고, x는

입니다. 이걸 x로 미분해서 0이 되게 하는 x의 값, 즉

이 극값의 위치가 됩니다. 계산하면 아래와 같이 구해진다고 합니다. 

 

...(9)

 

이 과정은 알고리즘이 좀 더 안정적인 성능을 낼 수 있게 도와줍니다. 

 

 

4. 나쁜 Keypoint들 제거하기

 

이제 전 단계에서 극값들로 찾은 keypoint들 중에서 활용가치가 떨어지는 것들은 제거해줘야 합니다. 두 가지 기준으로 제거해주는데요, 첫번째는 낮은 콘트라스트를 갖고 있는 것들을 제거해줍니다. 그 다음 엣지 위에 존재하는 것들을 제거해줍니다. 

먼저 낮은 콘트라스트의 keypoint들을 제거해주는 과정을 살펴보겠습니다. 간단히 DoG 이미지에서 keypoint들의 픽셀의 값이 특정 값(threshold)보다 작으면 제거해줍니다. 저희는 지금 subpixel 위치에서 keypoint들을 갖고 있기 때문에 이번에도 역시 테일러 전개를 활용해서 subpixel 위치에서의 픽셀값을 구합니다. 간단히 위에서 구한 

을 (8)에 대입해주면 됩니다. 그 결과는 (10)과 같습니다.

 

...(10)

 

이 값의 절대값이 특정 값보다 작으면 그 위치는 keypoint에서 제외됩니다. 

이제 두번째로 엣지 위에 존재하는 keypoint들을 제거해줍니다. 그 이유를 저는 다음과 같이 이해했습니다. DoG가 엣지를 찾아낼 때 매우 민감하게 찾아내기 때문에, 약간의 노이즈에도 반응할 수 있는 위험이 있습니다. 즉, 노이즈를 엣지로 찾아낼 수도 있다는 것입니다. 그래서 엣지 위에 있는 극값들을 keypoint로 사용하기에는 조금 위험성이 따릅니다. 따라서 좀 더 확실하고 안전한 코너점들만 keypoint로 남겨주는 것이죠. 이를 위해서 keypoint에서 수직, 수평 그레디언트를 계산해줍니다. 수평 방향으로는 픽셀 값이 어떻게 변화하는지, 수직 방향으로는 픽셀 값이 어떻게 변화하는지 변화량을 계산하는 것입니다. 만약 양 방향으로 변화가 거의 없다면 평탄한 지역(flat region)이라고 생각할 수 있습니다. 근데 수직 방향(or 수평 방향)으로는 변화가 큰데, 수평 방향(or 수직 방향)으로는 변화가 적으면 엣지(가장자리)라고 판단할 수 있습니다. 또 수직, 수평 방향 모두 변화가 크면 코너(모서리)라고 판단할 수 있구요. 설명이 부족하다면, 아래 그림10을 보세요.

 

그림 10. flat 지역, 엣지, 코너 설명

이 중에서 코너들이 좋은 keypoint들입니다. 따라서 우리의 목적은 코너에 위치한 keypoint들만 남기는 것입니다. Hessian Matrix를 활용하면 코너인지 아닌지 판별할 수 있다고 합니다. 이 두 제거 과정을 통해 그림 11에서 확인할 수 있듯이 keypoint의 갯수는 상당히 줄어듭니다. 흰 점들이 상당히 많이 줄어들었죠. 

 

그림 11. 나쁜 keypoint들 제거한 결과

이제는 이 keypoint들에게 방향을 할당해줄 차례입니다. 

 

5. keypoint들에 방향 할당해주기

 

전 단계에서 우리는 적당한 keypoint들을 찾았습니다. 이 keypoint들은 scale invariance(스케일 불변성)를 만족시킵니다. 이제는 keypoint들에 방향을 할당해줘서 rotation invariance(회전 불변성)를 갖게 하려고 합니다. 방법은 각 keypoint 주변의 그레디언트 방향과 크기를 모으는 것입니다. 그런 다음 가장 두드러지는 방향을 찾아내어 keypoint의 방향으로 할당해줍니다. 그림 12와 같이 하나의 keypoint 주변에 윈도우를 만들어준 다음 가우시안 블러링을 해줍니다. 이 keypoint의 scale값(블러의 정도를 결정해주는 파라미터)으로 가우시안 블러링을 해줍니다. 

 

그림 12. keypoint 주변에서 그레디언트 크기와 방향에 대한 값들 모으기위해 윈도우 설정 [1]

그 다음에 그 안에 있는 모든 픽셀들의 그레디언트 방향과 크기를 다음의 공식들을 이용해서 구합니다.

 

...(11)

 

그 결과는 그림 13과 같습니다. 

 

그림 13. 그레디언트 크기, 방향 계산 결과 [3]

여기서 그레디언트 크기에는 가우시안 가중함수를 이용해서 keypoint에 가까울수록 좀 더 큰 값을, 멀수록 좀 더 작은 값을 갖게 해줍니다. 이 과정은 그림 14에 있습니다. 

 

그림 14. 그레디언트 크기에 가우시안 가중함수를 곱해준 결과 [3]

이제 각 keypoint에 어떻게 방향을 할당해주는지 살펴보도록 하겠습니다. 먼저 36개의 bin을 가진 히스토그램을 만듭니다. 360도의 방향을 0~9도, 10~19도, ... , 350~359도로 10도씩 36개로 쪼갭니다. 그래서 만약 28도면 3번째 bin을 그레디언트 크기만큼 채우는 것입니다. 여기서 그레디언트 크기는 가중처리된 값입니다. 윈도우 내의 모든 픽셀에서 그레디언트 방향의 값을 이런 식으로 해당하는 bin에 채워줍니다. 그러면 그레디언트 방향에 대한 히스토그램이 완성됩니다. 그림 15를 보시죠. 

 

그림 15. keypoint 주변 그레디언트 방향 히스토그램 [1]

이런 히스토그램이 만들어집니다. 가장 높은 bin의 방향이 keypoint의 방향으로 할당됩니다. 또 여기서 만약 가장 높은 bin의 80% 이상의 높이를 갖는 bin이 있다면 그 방향도 keypoint의 방향으로 인정됩니다. 한 keypoint가 두 개의 keypoint로 분리되는 것입니다. 마찬가지로 만약 3개의 bin이 가장 높은 bin의 80% 이상의 높이를 갖는다면 총 4개의 keypoint로 분리됩니다. 이제 keypoint들에 방향을 할당해주는 작업이 완료되었습니다. 

 

 

6. 최종적으로 SIFT 특징들 산출하기

 

지금까지 keypoint들을 결정해왔습니다. 우리는 지금 keypoint들의 위치와 스케일과 방향을 알고 있습니다. 그리고 keypoint들은 스케일과 회전 불변성을 갖고 있습니다. 이제 이 keypoint들을 식별하기 위해 지문(fingerprint)과 같은 특별한 정보를 각각 부여해줘야 합니다. 각각의 keypoint의 특징을 128개의 숫자로 표현을 합니다. 이를 위해 keypoint 주변의 모양변화에 대한 경향을 파악합니다. keypoint 주변에 16x16 윈도우를 세팅하는데, 이 윈도우는 작은 16개의 4x4 윈도우로 구성됩니다. 그림 16을 확인하세요.

 

그림 16. 하나의 keypoint의 특징을 나타내는 128개의 숫자를 얻는 과정

 

16개의 작은 윈도우에 속한 픽셀들의 그레디언트 크기와 방향을 계산해줍니다. 전 단계에서 했던 것과 비슷하게 히스토그램을 만들어주는데, 이번에는 bin을 8개만 세팅합니다. 360도를 8개로 쪼갤 것입니다. 0~44, 45~89, 90~134, ... , 320~359로 나눠지겠죠? 역시 그레디언트 방향와 크기를 이용해서 bin들을 채웁니다. 결국 16개의 작은 윈도우들 모두 자신만의 히스토그램을 갖게 됩니다. 16개의 작은 윈도우마다 8개의 bin값들이 있으므로, 16 x 8을 해주면 128개의 숫자(feature vector)를 얻게 됩니다. 이것이 바로 keypoint의 지문이 되는 것입니다. 아직 회전 의존성과 밝기 의존성을 해결해야합니다. 128개의 숫자로 구성된 feature vector는 이미지가 회전하면 모든 그레디언트 방향은 변해버립니다. 회전 의존문제를 해결하기 위해 keypoint의 방향을 각각의 그레디언트 방향에서 빼줍니다. 그러면 각각의 그레디언트 방향은 keypoint의 방향에 상대적이게 됩니다.  (예를 들어 keypoint의 방향을 구한 것이 20-29도라면 24.5도를 keypoint 주변 16개의 4x4 윈도우의 방향에서 빼주면, 16개의 윈도우의 방향은 keypoint 방향에 상대적이게 됩니다.) 그리고 밝기 의존성을 해결해주기 위해서 정규화를 해줍니다. 이렇게 최종적으로 keypoint들에게 지문(fingerprint)을 할당해줬습니다. 이것이 바로 SIFT 특징입니다. 

 

 

그렇다면 SIFT로 이미지 매칭을 어떻게 하는 것일까요? 먼저 두 이미지에서 각각 keypoint들을 찾고 지문을 달아줬다면, 이 지문값들의 차이가 가장 작은 곳이 서로 매칭되는 위치인 것입니다.

728x90
728x90

Theory

이전 포스팅 에서 Harris Corner Detector에 대해서 다뤘다.

1994년도 후반, J. Shi and C. Tomasi 는 그들의 논문에서 Good Features to Track 라는 작은 수정을 했는데 이는 Harris Corner Detector에 비해서 더 좋은 성능을 나타내었다.

Harris Corner Detector 의 scoring function 은 다음과 같다.:

 

대신, Shi-Tomasi 가 제한한 함수는:

인데,  이것이 만약 threshold value 보다 크다면, 이를 코너라고 생각하는 것이다. 만약 우리가 이를

라는 공간 아래에 그린다면, Harris Corner Detector에서 보았던 공간 그림과 같을 것이다. 우리는 아래와 같은 이미지를 얻는다.

이 그림에서 당신은, λ1 과 λ2가 최소값 λmin 위일때만 , corner 라고 고려하면 된다. (green region).

Code

이 모든 정보를 통해 함수는 이미지의 코너를 찾는다. 품질 수준 이하의 모든 코너가 제외된다. 그런 다음 품질에 따라 내림차순으로 나머지 모서리를 정렬한다. 그런 다음 함수는 가장 강한 모서리를 가져 와서 최소 거리의 범위보다 가까운 모든 모서리를 버리고 N 개의 가장 강한 모서리를 반환한다.

 

이 함수는 Tracking에 더 적절하다고 하신다.

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('blox.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 
# (img,   max number of corners,   Quality level,   Min Euclidian distance between corners)

corners = cv2.goodFeaturesToTrack(gray,25,0.01,10) 
# img = float32 형식 그레이스케일 이미지
# max# = 검출할 최대 코너 수 
# Quality = 품질 수준 
# Min distance = 감지된 코너 사이의 최소 유클리드 거리 

corners = np.int0(corners) #정수형으로 바꿔준다
for i in corners:
    print('i:',i)
    x,y = i.ravel() # 2D -> 1D로 바꿔준다 
    print('x:{}, y:{}'.format(x,y))
    cv2.circle(img,(x,y),3,255,-1)
plt.imshow(img),plt.show()

 

728x90
728x90

1) 특징에 대해 이해하기 

 

- A, B 는 평평한 면이고 이 건 넓은 영역에 걸쳐져 있어서 찾기가 매우 어렵다.

- C, D는 빌딩의 엣지들이고 비슷한 위치를 찾을 수 있지만 정확한 위치는 추출하기 어렵다.

- E, F 는 어떤 빌딩의 코너다 이 코너는 찾기가 매우 쉽다  

-----> 그래서 앞으로 코너를 검출함으로써 특징을 탐지하고 추적하는 것을 공부 할 예정.

 

 - 어떻게 코너를 찾을 것인가?

-----> 직관적인 방법으로는 짧은 거리에서 최대한의 변화를 가진 이미지의 영역을 찾는다, 이를 Feature Detection이라고 한다


2) Harris Corner Detection

   2-1) cv2.cornerHarris (img, blockSize, ksize, k)   

  • img - 입력 이미지이고, 흑백스케일에 float32형태여야한다.
  • blockSize - 모서리 탐지를 위한 고려될 이웃 픽셀의 크기를 말한다.
  • ksize - 미분에 사용될 커널 사이즈
  • k - 방정식에서 나온 검출식에서의 k 

모서리가 모든 방향의 Variation 의 강도가 큰 이미지의 영역이다.

 

먼저 , 기본적으로(u,v) 변위(위치변화)에 대한 강도의 변화를 찾는다.

 

모든방향에 대해서 이는 아래와 같이 표현될 수 있다.

위 식은 테일러급수 확장에 의해 아래 식이 된다.

 

위와 같은 식을 행렬곱으로 나타낸다면, 아래 식이 된다 

위 식을 다시 정리하면  M은 아래 식과 같은데 Ix, Iy는 이미지의 x, y 방향으로의 미분값인데 sobel()필터 를 써서 쉽게 구할 수 있다.

최종적으로 얻어온 위치변화에 대한 강도 변화값은 다음과 같다

이렇게 얻어진 식을 가지고 이미지가 코너를 포함하고 있는지 아닌지에 대해 결정한다

(람다값은 행렬의 고유값을 나타낸다.)

 

- |R| 값이 작을경우 λ1, λ2 값이 둘 모두 작으므로  해당 영역은 평면이다.

- |R|<0 일 경우 λ1또는 λ2값 둘 중 하나가 압도적으로 큼으로 해당 영역은 edge다.

- |R|값이 매우 클 때 λ1, λ2 값 둘 모두 크다는 의미 이므로  λ1~ λ2 영역은 코너다!

import numpy as np
import cv2 
img = cv2.imread('checkerboard.jpg')
# img = cv2.imread('blox.jpg')

#cornerHarris()에 들어갈 입력 이미지는 GrayScale에 float32형태여야 한다.
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)
dst = cv2.cornerHarris(gray,2,3,0.04) #입력 영상, 
# img - 입력 이미지이고, 흑백스케일에 float32형태여야한다.
# blockSize - 모서리 탐지를 위한 고려될 이웃 픽셀의 크기를 말한다.
# ksize - 미분에 사용될 커널 사이즈
# k - 방정식에서 나온 검출식에서의 k 

#result is dilated for marking the corners, not important
dst = cv2.dilate(dst,None) 

# Threshold for an optimal value, it may vary depending on the image.
img[dst>0.01*dst.max()]=[0,0,255] #이미지에서 dst값이 최대 dst픽셀값 x 0.01보다 큰 부분은  빨간색으로 칠한다. 
cv2.imshow('dst',img)
if cv2.waitKey(0) & 0xff == 27:
    cv2.destroyAllWindows()

 

728x90
728x90


낮은 대역의 주파수를 가진 부분만 영상에 표출해준다 LPF를 만든것.


2. 템플릿 매칭 


cv2.matchTemplate 메소드를 사용했을 때 출력되는 영상의 크기와 모양. 합성곱 연산을 한번 한것과 같다


오차제곱합 기법을 사용할 땐 최소값을, 나머지는 최대값을 찾는 방향으로 템플릿 매칭을 해야함.

이 연산을 해서 나온 값으로 cv2.minMaxLoc(위 연산 결과 대입, mask=None) 해주면

 -> 최소,최대값, 최소,최대 좌표 4개 출력  (min_val,  max_val, min_loc,  max_loc) 이렇게 네가지 값이 나온다



1. 오차제곱합은 픽셀이 가장 어두운 부분을 찾아내는 것이고

2. corss corelation 방법은 가장 하얀 최대값을 찾는 것인데 로고가 grayscale에서 하얀색은매우 높은 값을 지니기에 인식을 잘못 했지만 정규화 한 후에 해결되었고

3. 상관계수 방법은 밝기에 대해 보정을 하고 나서 상호연관연산을 하므로 연산량은 제일 많지만 결과값이 제일 잘나옴



1. ROI 지정


<상관 계수 방법>

상관계수 방법에선 제일 밝은부분이 추출되는걸 확인


<Cross Corelation>

밝기에 대한 보정이 이뤄지지 않아서 조명이 밝은 부분을 동전으로 인식하는 오류 발생


<오차제곱합법>

정확히 들어맞는 부분이 가장 어둡게 나오므로 가장 어두운픽셀을 골라서 검출

728x90
728x90
# Task1 : 히스토그램 계산 
# cv2.calcHist([images], [channels], mask(None으로하자), [histSize], [ranges], hist=None, accumulate=None)

# 1-1) ROI지정, 히스토그램 계산
roi = cv2.imread('coin.jpg')
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # HSV평면으로 이동(색,채도,밝기)(180,256,256)
hist_roi = cv2.calcHist([hsv_roi], [0,2],None, [180, 256], [0,180 ,0,256]) # H, V영역 히스토그램 계산
# -----> 이건 뒤늦게 찾은 오류인데 ROI의 H S영역이 아니고 H V영역을 히스토그램 뽑아서 타겟의 H,S에다 역투영 시켰는데 객체가 너무 잘뽑힘 

# 1-2) target지정, 히스토그램 계산, 역투영
# cv2.calcBackProject([images], [channels], [hist], [ranges], scale(=1로해야 크기 유지), dst=None)

src = cv2.imread('coins.jpg')
target = cv2.resize(src, (600,400),cv2.INTER_AREA)#사진이 너무 커서 줄임
hsvt = cv2.cvtColor(target, cv2.COLOR_BGR2HSV) #객체 따낼 사진도 hsv평면으로 옮기고
hist_tar = cv2.calcHist([hsvt], [0,1], None, [180, 256], [0,180 ,0,256]) # 타겟의 히스토그램을 구함
dst = cv2.calcBackProject([hsvt], [0,1], hist_roi, [0,180 ,0,256], 1) #dst는 타겟 히스토그램에다가  아까 구한 roi의 히스토그램을 역투영시킴
cv2.imshow('dst',dst)

# 1-3) 동전을 이진화시키기 전에 조명에 의한 노이즈를 제거하기 위해 필터처리
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5)) # 5x5사이즈 동그란 커널 가져와서
cv2.filter2D(dst, -1,disc, dst) # 필터처리 src, ddepth(-1해야 크기유지), kernel, dst=None, anchor=None, delta=None, borderType=None
cv2.imshow('filtered_dst',dst)

# 1-4) 영상 이진화, bitwise연산
_, thersh = cv2.threshold(dst, 35,255,0) # 트랙바로 찾은 제일 이진화가 잘된 값을 이용
thersh = cv2.merge((thersh,thersh,thersh)) # target은 3채널 영상이니까 mask도 3채널로 만들어줌
res = cv2.bitwise_and(target,thersh) # 마스크 부분에 대해서 bitwise and연산 한걸 res로 받아줌
result = np.vstack((target,thersh,res)) #이건 그냥 영상 세개를 이어붙이는 방법
cv2.imwrite('Program01.jpg',result)# 이건 과제물 저장용

# # 적절값 찾기 : 임계값이 몇일 때 가장 잘 나오는지 직접 확인하기 위해 트랙바 사용
# def on_trackbar(pos):
#     pos = cv2.getTrackbarPos('th','thresh') #트랙바이름, 윈도우이름
#     _, thersh = cv2.threshold(dst, pos,255,0,cv2.THRESH_BINARY)

#     cv2.imshow('thersh', thersh)

cv2.imshow('target',target)
cv2.imshow('hsv_roi',hsv_roi)
cv2.imshow('hist_tar',hist_tar)
cv2.imshow('thersh',thersh)
cv2.imshow('res',res)

# # cv2.namedWindow('thresh')
# # cv2.createTrackbar('th','thresh',0,255,on_trackbar)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

 

다음 코드의 결과다

위에 주석에 달아놓긴 했는데 실수로 ROI의 H , V영역의 히스토그램을 뽑아서 Target의 H ,S 영역에다 역투영 시켰는데 특정 색상에 대한 객체 검출이 너무너무너무 잘됐다

 

이걸 원래대로 ROI의 H, S영역을  뽑아서 target의 H, S영역에다 역투영시키면

개판이다 ㅋㅋㅋㅋㅋ 우측 상단 res가  결과물인데 별의 별 색을 다 뽑아낸다 실수가 만들어낸 역작인듯 나중에 알아보고 누가 이걸로 논문 안썼으면 내가써야지

 


# Task2 : Descrete Fourier Transform
# 1-1)마름모 이미지 생성
img = np.zeros((600,600),np.uint8)
img[150:450, 150:450]=255
rot = cv2.getRotationMatrix2D((300,300),45,1)
rotated_img = cv2.warpAffine(img, rot,(0,0))

# 1-2) 마름모 DFT
# np.fft.fft2(영상,--->이부분은 안씀 잘 s=None, axes=( -2, -1), norm=None)
f1 = np.fft.fft2(rotated_img) 
print(f1)
# np.fft.fftshift(f1, axes=None) 이거는  중심점을 가운데로 옮겨주는 메소드
fshift1 = np.fft.fftshift(f1)
magnitude_spectrum1 = 20*np.log(np.abs(fshift1)) #20log(|값|) 으로 해서 dB 뽑아줌 

# 2-1) 원 이미지 생성
cir = img.copy() *0
cv2.circle(cir, (300, 300), 10, 255, -1, cv2.LINE_AA)

# 2_2)원 DFT
f2 = np.fft.fft2(cir)
fshift2 = np.fft.fftshift(f2)
magnitude_spectrum2 = 20*np.log(np.abs(fshift2))

# 3) 작성
plt.subplot(221), plt.imshow(rotated_img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(222), plt.imshow(magnitude_spectrum1, cmap='gray')
plt.title('Magnitude_Spectrum_RECT'), plt.xticks([]), plt.yticks([])
plt.subplot(223), plt.imshow(cir, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(224), plt.imshow(magnitude_spectrum2, cmap='gray')
plt.title('Magnitude_Spectrum_CIR'), plt.xticks([]), plt.yticks([])
plt.show()

푸리에변환은 수학이 약해서 잘 모르지만 시간영역을 주파수영역으로 바꾸는 선형변환으로 알고있는데 

영상처리에 이렇게 쓰일줄은 상상도 못했다 덜덜덜덜 나중에 필요하면 다시 공부해봐야겠다

728x90
728x90
# Task 1 : 이미지 피라미드
# cv2.pyrUp(src, dst=None, dstsize=None, borderType=None)
# cv2.pyrDown(src, dst=None, dstsize=None, borderType=None)
src = cv2.imread('apple.jpg')
dstUp = cv2.pyrUp(src)
dstDown = cv2.pyrDown(src)

cv2.imshow('src',src)
cv2.imshow('dstUp',dstUp)
cv2.imshow('dstDown',dstDown)
cv2.waitKey()
cv2.destroyAllWindows()


# Task2 : 윤곽선 검출
# cv2.findContours(image, mode, method, contours=None, hierarchy=None, offset=None)

src = cv2.imread('hand.jpg') #3채널 영상 받아서
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 그레이스케일 따로 만들어주고
_, th = cv2.threshold(src_gray, 127,255, cv2.THRESH_BINARY) # 그거 이진화시켜서 마스크 딴다음에
contours, hierarchy = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # 윤곽선 ,계층 뽑아줌
dst = src.copy() # 출력용 영상 하나 복사해주고 

for cnt in contours:
    #윤곽선 그리기 
    # cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None)
    cv2.drawContours(dst, cnt, -1, (255,0,0), 1, cv2.LINE_AA) #파란색
    
    #면적 따오기 
    # cv2.arcLength(cnt, closed=T/F)
    area = cv2.arcLength(cnt, True)
    print(area)

    #외곽선 근사화시켜서 모양 잡기
    # cv2.approxPolyDP(cnt, epsilon, closed, approxCurve=None)
    epsilon = 0.02 * area # 면적x0.02가 국룰이라고 함 
    approx = cv2.approxPolyDP(cnt, epsilon, True)
    print(len(approx))    
    cv2.drawContours(dst, [approx], 0,(0,255,0),1,cv2.LINE_AA) #초록색

    # 무게중심 따와서 그리기
    # cv2.moments(cnt, binaryImage=None)
    # cv2.circle(도화지, 중심점, 반지름, 색, 두께, 라인타입)
    moment = cv2.moments(cnt)
    cx = int(moment['m10']/moment['m00']) # x중심좌표
    cy = int(moment['m01']/moment['m00']) # y중심좌표
    cv2.circle(dst, (cx,cy), 5,(0,0,0), -1, 3)#무게중심 그리기 #검은색

    #바운딩박스 그리기
    # cv2.boundingRect(cnt) -> x,y, w,h
    # cv2.rectangle(도화지, 시작점, 끝점, 색, 두께, 라인타입)
    x, y, w, h = cv2.boundingRect(cnt)
    cv2.rectangle(dst, (x,y), (x+w, y+h), (0,0,255), 1, cv2.LINE_AA) # 빨간색 얇은거

    # 객체 모양에 맞게 회전된 박스 그리기----이건 그냥 외우자 
    rect = cv2.minAreaRect(cnt)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    cv2.drawContours(dst, [box], 0,(0,255,255),2)

    # convexHull 끝에쪽 테두리 잇기
    # cv2.convexHull(points, hull=None, clockwise=None, returnPoints=None(convexity defects 구할 때 False))
    hull =cv2.convexHull(cnt)
    cv2.drawContours(dst, [hull],0,(255,0,255),4) # 보라색

    #convexity defect 이건 convexhull 그리는거랑 같이 쓸 수가 없음
    # cv2.convexityDefects(contour, convexhull, convexityDefects=None)

    hull = cv2.convexHull(cnt, returnPoints=False)
    defect = cv2.convexityDefects(cnt, hull)
    for i in range (defect.shape[0]): # defect[0]번째개수만큼 점이 있는데
        s ,e ,f, d = defect[i,0] # 시작, 끝, 중심, 안쓰는값 은i,0번째임
        start = tuple(cnt[s][0]) # 시작점은 [s][0]
        end = tuple(cnt[e][0]) #끝점은[e][0]
        far = tuple(cnt[f][0]) #중심점은[f][0]

        cv2.line(dst, start, end ,[0,255,0], 5) # 선 초록색
        cv2.circle(dst, far, 5, [0,0,255],-1) # 점 빨간색

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

728x90
728x90
# Task 1 : 이미지 이진화
# cv2.threshold(src, thresh, max, type, dst=None)

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

_, bin = cv2.threshold(src, 127, 255, cv2.THRESH_BINARY)
_, bin_inv = cv2.threshold(src, 127, 255, cv2.THRESH_BINARY_INV)
_, trunc = cv2.threshold(src, 127, 255, cv2.THRESH_TRUNC)
_, tozero = cv2.threshold(src, 127, 255, cv2.THRESH_TOZERO)
_, tozero_inv = cv2.threshold(src, 127, 255, cv2.THRESH_TOZERO_INV)
_, otsu = cv2.threshold(src, 127, 255, cv2.THRESH_OTSU)
#창 한꺼번에 띄우기용 이름,영상 배열
images = [bin, bin_inv, trunc, tozero, tozero_inv,otsu]
images_name = ['bin', 'bin_inv', 'trunc', 'tozero', 'tozero_inv','otsu']
#창 한꺼번에 띄우기
for i,j in zip(images_name, images):

    cv2.imshow(i,j)

cv2.waitKey()
cv2.destroyAllWindows()


#Task 2 : Adaptivethresholding
# cv2.adaptiveThreshold((gray)src, maxval, adaptiveMethod(cv2.ADAPTIVE_THRESH_GAUSSIAN,MEAN_C), thresholdType, blockSize, 블록내평균값에서 뺄값)

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

mean = cv2.adaptiveThreshold(src,255,cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15,2)
gauss = cv2.adaptiveThreshold(src,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15,2)

cv2.imshow('src',src)
cv2.imshow('mean',mean)
cv2.imshow('gauss',gauss)

cv2.waitKey()
cv2.destroyAllWindows()



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


Task3 : 필터링- 2D convolution
cv2.filter2D(src, ddepth(출력영상데이터타입-1로 ㄱ), kernel, anchor(고정점 위치, (-1,-1)이면 중앙을 고정점으로 사용), delta(추가적으로 더할 값), borderType, dst)

src = cv2.imread('lena.jpg')

# 평균값 필터 방법  1.
kernel = np.ones((5,5), np.float32)/25 #Averaging필터 생성
dst = cv2.filter2D(src, -1, kernel)
cv2.imshow('src',src)
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()


#평균값 필터 방법 2
cv2.blur(src,(5,5))
cv2.imshow('src',src)
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()


# Task4 : 가우시안필터링
# cv2.GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)

src = cv2.imread('lena.jpg')
dst = cv2.GaussianBlur(src, (0,0), 3,None,3)
cv2.imshow('src',src)
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()


# Task5 : 중간값 필터링 
# cv2.medianBlur(src, ksize, dst=None)

src = cv2.imread('noise.bmp')
dst = cv2.medianBlur(src,5)
cv2.imshow('src',src)
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()


#Task6 : 양방향필터(엣지를 살리며 필터링)
# cv2.bilateralFilter(src, d(-1줘), sigmaColor(색공간표준편차), sigmaSpace(좌표공간표준편차), dst=None, borderType=None)

src = cv2.imread('lena.jpg')
dst = cv2.bilateralFilter(src,-1, 10,5)
cv2.imshow('src',src)
cv2.imshow('dst',dst)
cv2.waitKey()
cv2.destroyAllWindows()


# Task7 : 모폴로지 침식과 팽창 한꺼번에 총정리
# cv2.erode(src, kernel, dst=None, anchor=None, iterations=None, borderType=None, borderValue=None)
# cv2.dilate(src, kernel, dst=None, anchor=None, iterations=None, borderType=None, borderValue=None)
# cv2.morphologyEx(src, op, kernel, dst=None, anchor=None, iterations=None, borderType=None, borderValue=None)
# cv2.getStructuringElement(shape, ksize, anchor=None) #shape 에다가 cv2.MORPH_RECT , CROSS, ELLIPSE쓰면  그런 모양의 커널이 생김

src = cv2.imread('j_letter.png')
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

dilation = cv2.dilate(src, kernel)
erosion = cv2.erode(src, kernel)
opening = cv2.morphologyEx(src, cv2.MORPH_OPEN,kernel) # 열기 -> 침식팽창(하얀 점들이 사라짐)
closing = cv2.morphologyEx(src, cv2.MORPH_CLOSE,kernel)# 닫기 - > 팽창침식 (검은 노이즈가 사라짐)

cv2.imshow('src',src)
cv2.imshow('dilation',dilation)
cv2.imshow('erosion',erosion)
cv2.imshow('opening',opening)
cv2.imshow('closing',closing)

cv2.waitKey()
cv2.destroyAllWindows()


# Task 8 이미지 Gradient 
# cv2.Sobel(src, ddepth(-1), dx, dy, dst=None, ksize=None, scale=None, delta=None, borderType=None)
# cv2.Laplacian(src, ddepth(-1), dst=None, ksize=None, scale=None, delta=None, borderType=None)

src = cv2.imread('sudoku copy.jpg')
dx = cv2.Sobel(src, -1, 2, 0, (3,3))
dy = cv2.Sobel(src, -1, 0, 2, (3,3))
laplacian = cv2.Laplacian(src,-1)
h, w  = src.shape[:2]


cv2.imshow('src',src)
cv2.imshow('dx',dx)
cv2.imshow('dy',dy)
cv2.imshow('laplacian',laplacian)


cv2.waitKey()
cv2.destroyAllWindows()


# Task 9 캐니엣지검출
# 1. 5X5가우시안필터로 노이즈감소
# 2. 그래디언트 강도 탐지
# 3. 비최대 억제
# 4. Hysteresis Thresholding ====> threshold1 ~~ threshold2 사이의 미분값만 엣지로 검출함 
# cv2.Canny(image, threshold1, threshold2, edges=None, apertureSize=None, L2gradient=None)

src = cv2.imread('sudoku copy.jpg', cv2.IMREAD_GRAYSCALE)
th = cv2.adaptiveThreshold(src, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))
th = cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel)
edge = cv2.Canny(th, 100,200)
cv2.imshow('src',src)
cv2.imshow('th',th)
cv2.imshow('edge',edge)
cv2.waitKey()
cv2.destroyAllWindows()

728x90
728x90
# Task1 : Resizing
src = cv2.imread('Lena.jpg')
resize(src, dsize, dst=None, fx=None, fy=None, interpolation=None)
dst = cv2.resize(src,None,fx=1.3,fy=1.3, interpolation= cv2.INTER_CUBIC)
cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()


# Task2 : Transformation
# cv2.warpAffine(영상, 2x3실수행렬, 결과영상크기(0,0)는원본, 출력영상, 보간법, bordermode, bordervalue)
src = cv2.imread('Lena.jpg')

M = np.array([[1, 0, 100],
              [0, 1, 50]], dtype = np.float32) 

dst = cv2.warpAffine(src, M ,(0,0), borderMode=cv2.BORDER_REFLECT)

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


 

# Task 3 : affine 변환 점 세개로 하기
# cv2.warpAffine(영상, 2x3실수행렬, 결과영상크기(0,0)는원본, 출력영상, 보간법, bordermode, bordervalue)
# cv2.getAffineTransform(3개의 원본 좌표, 3개의 결과 좌표)

src = cv2.imread('Lena.jpg')
h, w = src.shape[:2]

#시작점, 끝점, 실수형 행렬로 만들기 
start = np.array([[0,0],[0,h],[w,0]],dtype=np.float32)
end = np.array([[30,30],[300,500],[w+50,0]], dtype=np.float32)

#어파인변환 행렬 따오기 
M = cv2.getAffineTransform(start,end)
dst = cv2.warpAffine(src, M, (0,0)) # 어파인변환

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


# Task 4 : perspective 변환 점 4개로 하기 
# cv2.warpPerspective(영상, 2x3실수행렬, 결과영상크기(0,0)는원본, 출력영상, 보간법, bordermode, bordervalue)
# cv2.getPerspectiveTransform(시작점, 끝점)

src = cv2.imread('st_changhak02.JPG')
h, w = src.shape[:2]
#4점짜리 실수행렬 구하기
start = np.array([[60,281],[881,165],[1228,268],[334,618]],dtype=np.float32)
end = np.array([[0,0],[1200,0],[1200,700],[0,700]], dtype=np.float32)
#getPerspective로 변환행렬 따오기
M = cv2.getPerspectiveTransform(start, end)
#투시변환 
dst = cv2.warpPerspective(src, M ,(0,0),flags = cv2.INTER_CUBIC)

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


# Task 5 : 영상의 회전
# cv2.getRotationMatrix2D(center, angle, sclae)

src = cv2.imread('Lena.jpg')
h, w = src.shape[:2]
#회전행렬따오기
M = cv2.getRotationMatrix2D((w//2, h//2), 45,1)
dst = cv2.warpAffine(src, M, (0,0),borderMode=cv2.BORDER_REFLECT)

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

728x90

+ Recent posts