Schoolwork/컴퓨터그래픽스 및 비젼

[Python] 특징 기술자 및 매칭 (Term Project)

FATKITTY 2022. 3. 2. 21:11
반응형

프로젝트 설명

▶ 목적

     특징 기술자와 특징기술자의 매칭에 기반한 인식, 추적을 구현한다.

 

▶ 내용

    1.  특징 기술자의 추출을 구현한다. SIFT, SURF 및 강의에서 언급되지 않은 특징 기술자를 사용할 수 있음.

    2.  특징 기술자의 매칭을 구현한다. 

    3.  매칭을 기반으로 인식을 구현한다.

    4.  물체인식을 이용하여 물체 추적을 구현한다.

    5.  세 가지 종류 이상의 모델 객체 들이 포함된 정지영상(Query image)

           A.  각 종류에 대하여 하나 또는 복수의 모델을 사용할 수 있음.

           B.  물체는 캐릭터, 로고, 책표지 등 제한 없음.

    6.  모델과 이외 방해 객체들이 움직이는 검색 동영상.

           A.  2분 이상의 길이

           B.  카메라의 줌, 방향은 고정됨.

           C.  모델 객체 들은 회전(2D 회전-프레임 평면에 수직한 축을 기준으로 회전),

                또는 축소/확대된 형태로 나타나며 프레임내에서 이동하거나 정지할 수 있다.

                또한, 영상 프레임에서 사라지거나 다시 출현할 수 있다. (프레임 내/외부 간에 이동할 수 있음)

    7.  모든 OpenCV 함수를 사용할 수 있음.

    8.  단, 프로젝트 전체적으로 충분한 양, 질적 난이도 및 문제 해결 노력이 보여질 수 있도록 하세요.

 

▶ 프로그램의 시나리오

    1.  모델에서 특징 기술자 추출

           A.  모델(정지)영상(Query image)에 모델 객체들이 나타나 있고

           B.  마우스 드랙으로 각 모델별로 사각형의 영역을 지정하여

           C.  각 모델의 특징 기술자들을 추출한다.

    2.  검색 동영상(Target image)에서 모델 객체를 아래와 같이 인식하고 추적한다.

           A.  현재 동영상에 나타난 모델 종류별로 객체들의 수를 영상의 우측 상단에 표시한다.

           B.  각 객체의 중심에 객체를 구별하는 마크(모델에 따라 다른 색, 모양, 문자를 사용)로 표시한다.

                객체들이 움직이면 마크도 따라서 움직이도록 함. 모델 객체 외에 방해 물체도 간간히 등장함.

                단, 물체들끼리 겹쳐져서 서로 가리는 경우는 없음.

           C.  위의 동작이 실시간으로 이루어져야 함.

 

▶ 계산 속도의 개선

    1.  최적화

           A.  특징점 검출, 특징 벡터추출, 매칭 등은 계산 복잡도가 높습니다. 이들 방법을 이용하여

                실시간 물체 인식, 추적을 하기 위해서는 계산 시간을 단축을 위한 최적화가 필요합니다.

           B.  예를 들면 모든 프레임을 검색하지 않고 10 프레임을 건너서 한 프레임을 검색할 수 도 있습니다.

                단, 실시간처럼 보일수만 있다는 전제하에

    2.  시험 환경에 대한 합리적 가정

           A.  실시간 인식, 추적이 가능하도록 환경 및 영상에 대한 적절한 가정을 할 수 있습니다.

           B.  예를 들면 화면에 나타난 물체의 수와 크기, 물체의 움직이는 속도 등에 제한을 둘 수 있습니다.

 

 

진행과정

개발환경

Python IDLE tool (version 3.7.xxxxxx)

pip version : opencv 3.4.2.16

 

만약 위 사진처럼 opencv-python 3.4.2.16 버전 설치가 안 된다면

높은 확률로 Python의 버전이 높아서 그럴 것이다.

Python 최신 버전(3.8 이상)에서는 opencv-python 3.4.2.16 설치가 안 된다.

지우고 3.6 ~ 3.7 버전으로 재설치하는걸 추천한다.

 

Image 준비

먼저 조건에 맞는 Query image와 Target image를 준비한다.

(난 직접 촬영해서 썼지만, 피피티로 흰 바탕에 캐릭터들이 왔다갔다하고 회전하는 영상 만든 팀도 있었음)

 

Query Image

Target Image

모델 객체를 제외하고 나머지 배경이 깨끗해야 인식도 잘 되고 편리하겠지만,

실전이라 생각하고 일단 시작해봤다.

 

시나리오

Query Image에서 검색할 객체 1가지를 골라서 사각형 영역에 맞도록 드래그한 후,

객체 이름을 설정해주고 엔터를 누르면 모델 객체가 생성, 저장된다.

이 과정을 3번 반복해서 객체 3가지를 저장한다.

그 후 img 창에서 space bar를 누르면 영상이 재생되며 매칭점 연산이 시작된다.

객체 3가지에 대해서 각각 빨강, 파랑, 초록색 사각형으로 영역이 표시된다.

그 옆에 각각의 객체 이름도 표시된다.

오른쪽 상단에는 영상에 나타나는 객체의 갯수를 표시한다.

영상이 끝나면 프로그램 종료.

 

코드의 전체 흐름

1.  Query Image에서 모델 객체 3가지를 드래그, 이름 설정, 저장

2.  영상 frame 캡쳐

3.  SIFT 연산자를 이용해 frame과 모델 객체 이미지들 간의 특징점 매치

4.  매칭된 특징점 중 좋은 특징점들만 각 물체의 good 배열에 저장

5.  good 특징점이 10개 넘어야 그 물체가 영상에 존재한다고 판단 후

물체의 객체 수+1, 사각형 영역 그려주고 객체 이름 보여줌

(good 특징점이 10개 이하면 없는 셈 취급)

6.  객체 3가지에 대해서 2~5를 영상 끝날 때까지 반복

 

주요 알고리즘

-  이미지의 특징점 매칭을 통한 Homography 분석

-  특징점 비교는 SIFT 기술자를 이용

-  각 객체에 대해서 좋은 특징점들만 선별

 

환경에 대한 합리적 가정

-  인식에 방해가 될 물체, 무늬 등이 너무 많으면 안 됨

-  인식할 물체는 3가지로 한정

 

Code

__author__ = "fatkitty"

import cv2
import numpy as np

value = []
blue = (255, 0, 0)
green = (0, 255, 0)
red = (0, 0, 255)
white = (255, 255, 255)

# set up text
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale = 2
thickness = 2

isDragging = False
x0, y0, w, h = -1, -1, -1, -1

def onMouse(event, x, y, flags, param):
    global isDragging, x0, y0, img, value
    
    if event == cv2.EVENT_LBUTTONDOWN:
        isDragging = True
        x0 = x
        y0 = y

    elif event == cv2.EVENT_MOUSEMOVE:
        if isDragging:
            img_draw = img.copy()
            cv2.rectangle(img_draw,(x0,y0),(x,y),blue,2)
            cv2.imshow('img',img_draw)

    elif event == cv2.EVENT_LBUTTONUP:
        if isDragging:
            isDragging = False
            w = x - x0
            h = y - y0

            if w > 0 and h > 0:
                img_draw = img.copy()
                cv2.rectangle(img_draw,(x0,y0),(x,y),red,2)
                cv2.imshow('img', img_draw)
                roi = img[y0:y0+h, x0:x0+w]
                val = input("모델 객체의 이름을 지정해주세요: ")
                value.append(val)
                cv2.imwrite(val+'.png',roi)
                cv2.imshow(val,roi)
                cv2.moveWindow(val,0,0)
                print("\n>> 객체는 총 3가지 지정해주세요.")
                print("지정 작업을 계속해주시거나,")
                print("객체 지정이 끝났다면 img 창에서 스페이스바를 눌러주세요.\n")
                
            else:
                cv2.imshow('img',img)
                print('드래그 방향은 왼쪽위->오른쪽아래\n')


# queryImage는 jpg, trainImage는 png형식이라 가정
# 추적할 객체는 3가지로 한정, 입력순
def vidTrack():
    global frame, value
    MIN_MATCH_COUNT = 10
    skip = 0
    count1, count2, count3 = 0, 0, 0
    
    cap = cv2.VideoCapture('./video.mp4')
    
    while(cap.isOpened()):
        ret, frame = cap.read()
        
        if ret:
            cap.set(cv2.CAP_PROP_POS_FRAMES, skip)
            _, frame = cap.read()
            frame = cv2.resize(frame,dsize=(0,0),fx=0.8,fy=0.8,interpolation=cv2.INTER_LINEAR)
            cv2.imshow('video',frame)
            skip += 8   # 최적화-프레임 건너뛰기

            # 원래 query 이미지와 모델 이미지들
            imgQuery = frame
            img1 = cv2.imread('./' +value[0]+'.png')
            img2 = cv2.imread('./' +value[1]+'.png')
            img3 = cv2.imread('./' +value[2]+'.png')

            # 모델 객체 이름과 그 객체의 갯수를 오른쪽 상단의 적당한 좌표에 표시하기 위한 전처리
            textsize1 = cv2.getTextSize(value[0], font, fontScale, thickness)[0]
            textsize2 = cv2.getTextSize(value[1], font, fontScale, thickness)[0]
            textsize3 = cv2.getTextSize(value[2], font, fontScale, thickness)[0]
            txtsize = [textsize1[0], textsize2[0], textsize3[0]]
            maxtxt = max(txtsize)

            counterX = int(imgQuery.shape[1])
            counterY = int(textsize1[1])
            item1 = (counterX - maxtxt, counterY)
            item2 = (counterX - maxtxt, counterY * 2)
            item3 = (counterX - maxtxt, counterY * 3)
            counter1 = (counterX - int(maxtxt/3), counterY)
            counter2 = (counterX - int(maxtxt/3), counterY * 2)
            counter3 = (counterX - int(maxtxt/3), counterY * 3)
            cv2.putText(frame,value[0],item1,font,1,white,2)
            cv2.putText(frame,value[1],item2,font,1,white,2)
            cv2.putText(frame,value[2],item3,font,1,white,2)

            # 특징점 추출, 가장 좋은 특징점들 선별 후 각 개체별로 homography 분석
            sift = cv2.xfeatures2d.SIFT_create()

            kpQ, desQ = sift.detectAndCompute(imgQuery,None)
            kp1, des1 = sift.detectAndCompute(img1,None)
            kp2, des2 = sift.detectAndCompute(img2,None)
            kp3, des3 = sift.detectAndCompute(img3,None)

            FLANN_INDEX_KDTREE = 0
            index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
            search_params = dict(checks = 50)
            flann = cv2.FlannBasedMatcher(index_params, search_params)
            matches1 = flann.knnMatch(desQ,des1,k=2)
            matches2 = flann.knnMatch(desQ,des2,k=2)
            matches3 = flann.knnMatch(desQ,des3,k=2)

            good1 = []
            good2 = []
            good3 = []
            for m,n in matches1:
                if m.distance < 0.4*n.distance:
                    good1.append(m)
            for m,n in matches2:
                if m.distance < 0.4*n.distance:
                    good2.append(m)
            for m,n in matches3:
                if m.distance < 0.4*n.distance:
                    good3.append(m)

            # 첫번째 객체
            if len(good1)>MIN_MATCH_COUNT:

                count1 = 1

                src_pts = np.float32([ kpQ[m.queryIdx].pt for m in good1 ]).reshape(-1,1,2)
                dst_pts = np.float32([ kp1[m.trainIdx].pt for m in good1 ]).reshape(-1,1,2)
                # x좌표와 y좌표 분리를 위한 reshape
                src_pts_xy = np.float32([ kpQ[m.queryIdx].pt for m in good1 ]).reshape(-1,1,1)
                
                M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
                matchesMask1 = mask.ravel().tolist()
                
                h,w,c = img1.shape
                # 매칭점의 평균 좌표값 구하기
                i, sumx1, sumy1 = 0, 0, 0
                while i < len(src_pts_xy):
                    if i%2 == 0:
                        sumx1 += src_pts_xy[i][0]
                    elif i%2 == 1:
                        sumy1 += src_pts_xy[i][0]
                    i += 1
                x1 = sumx1 / len(src_pts)
                y1 = sumy1 / len(src_pts)
                # 평균 좌표값을 무게중심으로 두는 직사각형 그리기
                pts = np.float32([ [x1-w/2,y1-h/2],[x1-w/2,y1+h/2],[x1+w/2,y1+h/2],[x1+w/2,y1-h/2] ]).reshape(-1,1,2)
                name_loc = (x1+w/2, y1+h/2)
                
                frame = cv2.polylines(frame,[np.int32(pts)],True,green,2)
                cv2.putText(frame,value[0],name_loc,font,1,green,2)
                cv2.putText(frame,str(count1),counter1,font,1,white,2)
                cv2.imshow('video',frame)
                
            elif len(good1)<=MIN_MATCH_COUNT:
                #print(value[0]+"의 매칭점이 충분하지 않습니다. - %d/%d" % (len(good1),MIN_MATCH_COUNT))
                matchesMask1 = None
                count1 = 0
                cv2.putText(frame,str(count1),counter1,font,1,white,2)

            # 두번째 객체
            if len(good2)>MIN_MATCH_COUNT:

                count2 = 1

                src_pts = np.float32([ kpQ[m.queryIdx].pt for m in good2 ]).reshape(-1,1,2)
                dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good2 ]).reshape(-1,1,2)
                
                src_pts_xy = np.float32([ kpQ[m.queryIdx].pt for m in good2 ]).reshape(-1,1,1)
                
                M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
                matchesMask2 = mask.ravel().tolist()
                
                h,w,c = img2.shape
                j, sumx2, sumy2 = 0, 0, 0
                while j < len(src_pts_xy):
                    if j%2 == 0:
                        sumx2 += src_pts_xy[j][0]
                    elif j%2 == 1:
                        sumy2 += src_pts_xy[j][0]
                    j += 1
                x2 = sumx2 / len(src_pts)
                y2 = sumy2 / len(src_pts)
                pts = np.float32([ [x2-w/2,y2-h/2],[x2-w/2,y2+h/2],[x2+w/2,y2+h/2],[x2+w/2,y2-h/2] ]).reshape(-1,1,2)
                name_loc = (x2+w/2, y2+h/2)
                
                frame = cv2.polylines(frame,[np.int32(pts)],True,red,2)
                cv2.putText(frame,value[1],name_loc,font,1,red,2)
                cv2.putText(frame,str(count2),counter2,font,1,white,2)
                cv2.imshow('video',frame)
                
            elif len(good2)<=MIN_MATCH_COUNT:
                #print(value[1]+"의 매칭점이 충분하지 않습니다. - %d/%d" % (len(good2),MIN_MATCH_COUNT))
                matchesMask2 = None
                count2 = 0
                cv2.putText(frame,str(count2),counter2,font,1,white,2)

            # 세번째 객체
            if len(good3) > MIN_MATCH_COUNT:

                count3 = 1

                src_pts = np.float32([ kpQ[m.queryIdx].pt for m in good3 ]).reshape(-1,1,2)
                dst_pts = np.float32([ kp3[m.trainIdx].pt for m in good3 ]).reshape(-1,1,2)
                
                src_pts_xy = np.float32([ kpQ[m.queryIdx].pt for m in good3 ]).reshape(-1,1,1)
                
                M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
                matchesMask3 = mask.ravel().tolist()
                
                h,w,c = img3.shape
                k, sumx3, sumy3 = 0, 0, 0
                while k < len(src_pts_xy):
                    if k%2 == 0:
                        sumx3 += src_pts_xy[k][0]
                    elif k%2 == 1:
                        sumy3 += src_pts_xy[k][0]
                    k += 1
                x3 = sumx3 / len(src_pts)
                y3 = sumy3 / len(src_pts)
                pts = np.float32([ [x3-w/2,y3-h/2],[x3-w/2,y3+h/2],[x3+w/2,y3+h/2],[x3+w/2,y3-h/2] ]).reshape(-1,1,2)
                name_loc = (x3+w/2, y3+h/2)
                
                frame = cv2.polylines(frame,[np.int32(pts)],True,blue,2)
                cv2.putText(frame,value[2],name_loc,font,1,blue,2)
                cv2.putText(frame,str(count3),counter3,font,1,white,2)
                cv2.imshow('video',frame)
                
            elif len(good3) <= MIN_MATCH_COUNT:
                #print(value[2]+"의 매칭점이 충분하지 않습니다. - %d/%d" % (len(good3),MIN_MATCH_COUNT))
                matchesMask3 = None
                count3 = 0
                cv2.putText(frame,str(count3),counter3,font,1,white,2)
                cv2.imshow('video',frame)
            
            
            key = cv2.waitKey(1)
            if key == 32:
                break
            elif key == ord('p'):
                cv2.waitKey(-1)

        else:
            break
    cap.release()
    cv2.destroyAllWindows()


def main():
    global img
    src = cv2.imread('./image.jpg')
    img = cv2.resize(src,dsize=(0,0),fx=0.4,fy=0.4,interpolation=cv2.INTER_LINEAR)
    cv2.imshow('img',img)
    cv2.moveWindow('img',300,0)
    while True:
        cv2.setMouseCallback('img',onMouse)
        if cv2.waitKey(1) == 32:
            cv2.destroyAllWindows()
            break
    # 객체지정 작업이 끝났다면 영상을 불러와서 트래킹
    vidTrack()


if __name__ == "__main__":
    main()

코드가 조금 지저분한 감이 없지 않아 있다.

3가지의 객체마다 변수를 각각 선언하다 보니 변수만 한가득.

그래도 내 선에서는 이게 최선이었다. 😢

 

실행결과

 

 

최적화 노력

모든 프레임을 검색하지 않고, 프레임을 6번에 한번씩 보이게 하여 동영상 플레이 속도, 연산 속도를 높였다.

다만 6프레임마다 jpg형식으로 해당 프레임을 저장해서 연산을 하기 때문에,

한번 프로그램을 실행하고 난 뒤에는 폴더 내에 엄청나게 많은 jpg 이미지들이 쌓이게 된다.

→ 이미지로 저장하지 않고 프레임을 건너뛰며 연산을 할 수는 없을까?

수정본 : VideoCapture 후 특정 frame에 대해 set 후 read 하는 과정을 반복하면 저장과정 없이 프레임 건너뛰기 가능.

그리고 조금 더 빠른 연산을 위해 8프레임 건너뛰기로 수정함.

 

결과 고찰

전체적으로 입력 영상에 대해 특징점 매칭이 잘 되고, 그에 따라 객체 구별 마킹도 잘 된다.

다만 객체의 개수 counter 기능은 구현하지 못 했고,

빛,그림자 등 때문에 생기는 튐 현상은 해결하지 못 했다.

수정본 :

- 튐 현상 해결

튐 현상이라 부르기도 민망하게, 처음 코드는

임의의 매칭점 하나를 잡은 다음에 그것을 중심으로 직사각형을 그리는 식이었다.

당연히 불안정하게 계속 튈 수 밖에 없다.

그래서 모든 매칭점들의 평균값으로 구성된 좌표를 중심으로 직사각형을 그리도록 수정했다.

확실히 수정본이 안정감 있게 잘 마킹된다.

- counter 기능 구현

구현은 했지만 사실 조금 찝찝한 부분.

만약에 똑같은 물체가 두 개 이상 있을 때에도 잘 돌아갈지, count가 잘 될지 의구심이 들었다.

아마도 안 돌아갈 거 같다.

근데 더이상은 힘들어서 일단 여기서 마무리.

 

참고자료

http://www.gisdeveloper.co.kr/?p=6832

https://hyongdoc.tistory.com/355

 

 

  ❤와 댓글은 큰 힘이 됩니다. 감사합니다 :-)  

반응형