Ch 6. 비전 에이전트

wonnie1224·2023년 4월 27일
0

6.1 지능 에이전트로서 비전 에이전트

합리적 에이전트 개념(사람 = 항상 최적의 의사결정 하려 노력하는 주체) → 컴퓨터에 적용하면 “지능 에이전트”

  • 지능 에이전트 : 비전, 자연 언어 처리, 지식 표현, 학습, 추론 등 기능 종합적 발휘
  • 비전 에이전트 (vision agent) : 비전에 특화된 지능 에이전트

Untitled

  • 비전 프로그램 (2~5장의 프로그래밍 실습) : 환경과 상호작용 없음 - 비전 프로그램
  • 비전 에이전트 : 환경과 상호작용하는 과정 추가됨

+) 환경과 상호작용 - 인식한 결과에 따라 로봇 move / 자동차 브레이크 등 조작해야함

액추에이터(actuator) : 환경에 영향 미치는 물리적 장치

6.2 PyQt를 이용한 사용자 인터페이스

비전 프로그램 → 비전 에이전트 확장 : 그래픽 사용자 인터페이스(GUI: Graphic User Interface) 추가해야함

  • python GUI 프로그래밍 : tkinter / PyOt 모듈 사용

📌 PyQt 기초 프로그래밍

# 모듈 불러오기
from PyQt5.QtWidgets import *
import sys
import winsound # 삑 소리 내는데 사용

## PyQt로 GUI 제작하는 일 지원하는 클래스 선언
# 삑 소리 내주는 클래스
class BeepSound(QMainWindow):   # QMainWindow 클래스(윈도우 생성 및 관리하는 함수 제공하는 클래스) 상속 받음
    # BeepSound 클래스로 객체 생성 시 자동 실행되는 생성자 함수 정의
    def __init__(self) :
        super().__init__()
        # 윈도우 이름과 위치 지정
        self.setWindowTitle('삑 소리 내기')     # 윈도우의 제목 표시줄에 적힐 제목 설정
        self.setGeometry(200,200,500,100)   # (윈도우의 초기 위치 x좌표, y좌표, 윈도우의 너비, 높이)

       	# 4개 위젯 : 버튼 & 레이블 생성
        shortBeepButton=QPushButton('짧게 삑',self)
        longBeepButton=QPushButton('길게 삑',self)
        quitButton=QPushButton('나가기',self)
        self.label=QLabel('환영합니다!',self)   # 멤버 변수; shortBeepButton, longBeepButton 함수에서 접근 위해서
        
       	# 4개 위젯(버튼,레이블) 위치와 크기 지정 
        shortBeepButton.setGeometry(10,10,100,30)
        longBeepButton.setGeometry(110,10,100,30)
        quitButton.setGeometry(210,10,100,30)
        self.label.setGeometry(10,40,500,70)
        
        # 사용자가 버튼 클릭했을 때 수행할 콜백 함수 지정
        shortBeepButton.clicked.connect(self.shortBeepFunction)  # shortBeepButton 클릭시 shortBeepFunction 함수 실행
        longBeepButton.clicked.connect(self.longBeepFunction)         
        quitButton.clicked.connect(self.quitFunction)
       
    def shortBeepFunction(self):
        self.label.setText('주파수 1000으로 0.5초 동안 삑 소리를 냅니다.')  # 레이블 위젯에 지정한 텍스트 씀
        winsound.Beep(1000,500) # 소리 발생
        
    def longBeepFunction(self):
        self.label.setText('주파수 1000으로 3초 동안 삑 소리를 냅니다.')        
        winsound.Beep(1000,3000) 
                
    def quitFunction(self):
        self.close()
                
app=QApplication(sys.argv)  # PyQt 실행에 필요한 객체 생성
win=BeepSound()     # 생성자 함수 실행 -> 윈도우 생성, 위젯 4개, 콜백함수 등록
win.show()  # win에 해당하는 윈도우를 화면에 표시
app.exec_() # 무한 루프 돌아 프로그램 끝나는 것 방지 (없으면 win 객체인 윈도우 화면 띄우는 순간 프로그램 종료됨)

📌 OpenCV에 PyQt 붙여 프로그램 확장

💡 프로그램 패턴

  1. GUI 제작 지원하는 클래스 선언
    1. 생성자 함수 __init__ 설정 : 윈도우 제목&위치&크기 설정, 위젯(버튼,레이블 등) 생성, 버튼 클릭 시 수행할 콜백함수 지정
    2. 호출되는 콜백함수들 선언
  2. 프로그램의 메인 : PyOt 실행에 필요한 객체 생성

ex.6-2: OpenCV에 PyQt의 GUI 붙이기; 비디오에서 프레임 잡아 저장하기

from PyQt5.QtWidgets import *
import sys
import cv2 as cv
       
class Video(QMainWindow):
    def __init__(self) :
        super().__init__()
        self.setWindowTitle('비디오에서 프레임 수집')	# 윈도우 이름과 위치 지정
        self.setGeometry(200,200,500,100)

        videoButton=QPushButton('비디오 켜기',self)	# 버튼 생성
        captureButton=QPushButton('프레임 잡기',self)
        saveButton=QPushButton('프레임 저장',self)
        quitButton=QPushButton('나가기',self)
        
        videoButton.setGeometry(10,10,100,30)		# 버튼 위치와 크기 지정
        captureButton.setGeometry(110,10,100,30)
        saveButton.setGeometry(210,10,100,30)
        quitButton.setGeometry(310,10,100,30)
        
        videoButton.clicked.connect(self.videoFunction) # 콜백 함수 지정
        captureButton.clicked.connect(self.captureFunction)         
        saveButton.clicked.connect(self.saveFunction)
        quitButton.clicked.connect(self.quitFunction)

    # 웹 캠으로부터 비디오 입력 받아 윈도우에 디스플레이
    def videoFunction(self):
        self.cap=cv.VideoCapture(0,cv.CAP_DSHOW)	# 카메라와 연결 시도
        if not self.cap.isOpened(): self.close()    # 연결 안 되면 오류 메시지 출력 후 프로그램 종료
            
        while True: # 카메라 연결 성공하면
            ret,self.frame=self.cap.read()  # 비디오에서 프레임 획득 -> frame 변수에 저장
            if not ret: break            
            cv.imshow('video display',self.frame)   # 'video display'라는 윈도우에 표시
            cv.waitKey(1)
        
    def captureFunction(self):
        self.capturedFrame=self.frame   # 비디오 프레임을 저장한 frame(멤버변수여서 사용 가능)을 capturedFrame 변수에 저장
        cv.imshow('Captured Frame',self.capturedFrame)  # 캡쳐된 프레임을 윈도우에 디스플레이
        
    def saveFunction(self):				# 파일 저장
        fname=QFileDialog.getSaveFileName(self,'파일 저장','./')    # 사용자가 입력한 파일 이름 반환
        cv.imwrite(fname[0],self.capturedFrame) # capturedFrame에 저장해뒀던 프레임을 사용자가 지정한 파일명으로 저장
        
    def quitFunction(self):
        self.cap.release()				# 카메라와 연결을 끊음 (멤버변수 cap 사용)
        cv.destroyAllWindows()  # OpenCV가 연 모든 윈도우 닫음
        self.close()    # 프로그램 종료시킴
                
app=QApplication(sys.argv) 
win=Video() 
win.show()
app.exec_()

6.3 [비전 에이전트 1] 오림

📌 관심 물체를 분할

🌿 ex 6-3. GrabCut을 이용해 관심 물체 오리기

: 사용자와 상호작용하면서 GrabCut 반복 적용 → 사용자가 만족할 때까지 물체 영역 오려내는 일을 지원하는 비전 에이전트

import cv2 as cv 
import numpy as np
import sys
from PyQt5.QtWidgets import *

# Orim 클래스 선언      
class Orim(QMainWindow):
    # 생성자 함수
    def __init__(self) :
        super().__init__()
        self.setWindowTitle('오림')
        self.setGeometry(200,200,700,200)
       
        # 버튼 7개 생성
        fileButton=QPushButton('파일',self)
        paintButton=QPushButton('페인팅',self)
        cutButton=QPushButton('오림',self)
        incButton=QPushButton('+',self)
        decButton=QPushButton('-',self)
        saveButton=QPushButton('저장',self)
        quitButton=QPushButton('나가기',self)

        # 버튼 위치, 크기 지정
        fileButton.setGeometry(10,10,100,30)
        paintButton.setGeometry(110,10,100,30)
        cutButton.setGeometry(210,10,100,30)
        incButton.setGeometry(310,10,50,30)
        decButton.setGeometry(360,10,50,30)
        saveButton.setGeometry(410,10,100,30)
        quitButton.setGeometry(510,10,100,30)
        
        # 콜백함수 등록
        fileButton.clicked.connect(self.fileOpenFunction)
        paintButton.clicked.connect(self.paintFunction) 
        cutButton.clicked.connect(self.cutFunction)    
        incButton.clicked.connect(self.incFunction)              
        decButton.clicked.connect(self.decFunction) 
        saveButton.clicked.connect(self.saveFunction)                         
        quitButton.clicked.connect(self.quitFunction)

        self.BrushSiz=5			# 페인팅 붓의 크기
        self.LColor,self.RColor=(255,0,0),(0,0,255) # 파란색 - 물체, 빨간색 - 배경
        
    def fileOpenFunction(self):
        fname=QFileDialog.getOpenFileName(self,'Open file','./')    # 폴더 브라우징하여 파일을 읽어옴
        self.img=cv.imread(fname[0])   # imread 함수로 영상 파일 읽어 img 객체에 저장
        if self.img is None: sys.exit('파일을 찾을 수 없습니다.')  
        
        self.img_show=np.copy(self.img)	# img 복사 -> 사용자가 색칠한 정보 표시용 영상 
        cv.imshow('Painting',self.img_show)
        
        self.mask=np.zeros((self.img.shape[0],self.img.shape[1]),np.uint8)  # 사용자가 색칠한 정보 저장할 mask 객체 생성
        self.mask[:,:]=cv.GC_PR_BGD	# 모든 화소를 배경일 것 같음으로 초기화
      
    def paintFunction(self):
        cv.setMouseCallback('Painting',self.painting)   # painting 함수를 'Painting' 윈도우의 콜백함수로 등록
        # => Painting이라는 윈도우에서 마우스 조작 일어나면 painting 함수 호출
        
    def painting(self,event,x,y,flags,param):
        if event==cv.EVENT_LBUTTONDOWN:   
            cv.circle(self.img_show,(x,y),self.BrushSiz,self.LColor,-1) # 왼쪽 버튼을 클릭하면 파란색
            cv.circle(self.mask,(x,y),self.BrushSiz,cv.GC_FGD,-1)
        elif event==cv.EVENT_RBUTTONDOWN: 
            cv.circle(self.img_show,(x,y),self.BrushSiz,self.RColor,-1) # 오른쪽 버튼을 클릭하면 빨간색
            cv.circle(self.mask,(x,y),self.BrushSiz,cv.GC_BGD,-1)
        elif event==cv.EVENT_MOUSEMOVE and flags==cv.EVENT_FLAG_LBUTTON:
            cv.circle(self.img_show,(x,y),self.BrushSiz,self.LColor,-1) # 왼쪽 버튼을 클릭하고 이동하면 파란색
            cv.circle(self.mask,(x,y),self.BrushSiz,cv.GC_FGD,-1)
        elif event==cv.EVENT_MOUSEMOVE and flags==cv.EVENT_FLAG_RBUTTON:
            cv.circle(self.img_show,(x,y),self.BrushSiz,self.RColor,-1) # 오른쪽 버튼을 클릭하고 이동하면 빨간색 
            cv.circle(self.mask,(x,y),self.BrushSiz,cv.GC_BGD,-1)
    
        cv.imshow('Painting',self.img_show)        
        
    def cutFunction(self):
        background=np.zeros((1,65),np.float64) 
        foreground=np.zeros((1,65),np.float64) 
        cv.grabCut(self.img,self.mask,None,background,foreground,5,cv.GC_INIT_WITH_MASK)
        mask2=np.where((self.mask==2)|(self.mask==0),0,1).astype('uint8')
        self.grabImg=self.img*mask2[:,:,np.newaxis]
        cv.imshow('Scissoring',self.grabImg) 
        
    def incFunction(self):
        self.BrushSiz=min(20,self.BrushSiz+1)   # 붓 크기 +1, 붓 크기 20 넘지 않도록 함
        
    def decFunction(self):
        self.BrushSiz=max(1,self.BrushSiz-1)    # 붓 크기 -1, 붓 크기가 1보다 작아지지 않게 함
        
    def saveFunction(self):
        fname=QFileDialog.getSaveFileName(self,'파일 저장','./')
        cv.imwrite(fname[0],self.grabImg)
                
    def quitFunction(self): # OpenCV가 연 모든 윈도우 닫고 프로그램 종료시킴
        cv.destroyAllWindows()        
        self.close()

# main               
app=QApplication(sys.argv) 
win=Orim() 
win.show()
app.exec_()

🌿 실행 결과

Untitled

Untitled

6.4 교통약자 보호구역 알림

📌 도로 영상에서 표지판 식별

🌿 ex 6-4: 교통약자 보호구역 알림 구현하기

<기능>

  1. <표지판 등록> 버튼 : 3종류의 표지판 영상 읽어 등록
  2. <도로 영상 불러옴> 버튼 : 사용자가 도로 영상 선택
  3. <인식> 버튼 : 표지판 영상 인식 후 결과 보여줌
import cv2 as cv
import numpy as np
from PyQt5.QtWidgets import *
import sys
import winsound

class TrafficWeak(QMainWindow):
    def __init__(self):
        super().__init__()
        # 윈도우 제목,위치,크기 설정
        self.setWindowTitle('교통약자 보호')
        self.setGeometry(200,200,700,200)
       
        # 버튼 4개, 레이블 1개 생성
        signButton=QPushButton('표지판 등록',self)
        roadButton=QPushButton('도로 영상 불러옴',self)
        recognitionButton=QPushButton('인식',self)
        quitButton=QPushButton('나가기',self)
        self.label=QLabel('환영합니다!',self)
        
        # 버튼 클릭 시 수행할 콜백 함수 지정
        signButton.setGeometry(10,10,100,30)
        roadButton.setGeometry(110,10,100,30)
        recognitionButton.setGeometry(210,10,100,30)
        quitButton.setGeometry(510,10,100,30)
        self.label.setGeometry(10,40,600,170)
        
        signButton.clicked.connect(self.signFunction)
        roadButton.clicked.connect(self.roadFunction) 
        recognitionButton.clicked.connect(self.recognitionFunction)        
        quitButton.clicked.connect(self.quitFunction)

        self.signFiles=[['child.png','어린이'],['elder.png','노인'],['disabled.png','장애인']]	# 표지판 모델 영상 이름 설정
        self.signImgs=[]				# 표지판 모델 영상 저장할 signImgs 객체 생성

    # 콜백함수 선언     
    def signFunction(self):
        self.label.clear()  # 레이블 지움
        self.label.setText('교통약자 번호판을 등록합니다.') # 메시지 출력
        
        for fname,_ in self.signFiles:  # signFiles 리스트에 들어있는 요소 각각에 대해 파일 이름을 fname에 담음
            self.signImgs.append(cv.imread(fname))  # 이름이 fname인 영상 파일 읽어 signImgs에 추가
            cv.imshow(fname,self.signImgs[-1])      # signImgs의 맨 뒤에 있는(= 방금 추가한 영상)을 fname이라는 제목 표시줄 가진 윈도우에 디스플레이

    def roadFunction(self):
        if self.signImgs==[]: # signImgs가 비었을 때(= 사용자가 표지판 등록 안 하고 '도로 영상 불러옴' 버튼 클릭했을 때)
            self.label.setText('먼저 번호판을 등록하세요.') # 메시지 표시
        else:   # 표지판 제대로 등록해둔 경우
            fname=QFileDialog.getOpenFileName(self,'파일 읽기','./')    # 사용자가 폴더 브라우징하면서 도로 영상 선택할 수 있게 함
            self.roadImg=cv.imread(fname[0])    # 영상 파일 읽어 roadImg 객체에 저장
            if self.roadImg is None: sys.exit('파일을 찾을 수 없습니다.')  
    
            cv.imshow('Road scene',self.roadImg)  # 읽은 영상을 윈도우에 디스플레이
        
    def recognitionFunction(self):
        if self.roadImg is None:    # 사용자가 도로 영상 입력 안 하고 '인식' 버튼 클릭했을 때
            self.label.setText('먼저 도로 영상을 입력하세요.')
        else:   # 도로 영상 제대로 입력해둔 경우
            sift=cv.SIFT_create()   # SIFT 검출하는 데 사용할 객체 생성

            ## signImgs에 담겨있는 3장의 표지판 모델 영상에서 특징점 & 기술자 차례로 추출하여 KD에 저장
            KD=[] # 여러 표지판 영상의 키포인트와 기술자 저장
            for img in self.signImgs: 
                gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
                KD.append(sift.detectAndCompute(gray,None))
                
            grayRoad=cv.cvtColor(self.roadImg,cv.COLOR_BGR2GRAY) # 명암으로 변환
            road_kp,road_des=sift.detectAndCompute(grayRoad,None) # 키포인트와 기술자 추출
            
            ## 여러 표지판 영상의 good match를 저장 => GD : 표지판 영상 3장 각각에 대해 좋은 매칭 리스트 가지게 됨  
            matcher=cv.DescriptorMatcher_create(cv.DescriptorMatcher_FLANNBASED)    # FLANN 기반 매칭해주는 matcher 객체 생성
            GM=[]   # 3장의 모델 영상이랑 매칭한 결과 저장할 GM 객체 생성
            for sign_kp,sign_des in KD: # 표지판 모델 영상에서 추출해둔 특징점 & 기술자를 KD에서 하나씩 꺼내서 sign_kp, sign_des에 저장하고 반복문 실행
                knn_match=matcher.knnMatch(sign_des,road_des,2) # sign_des & road_des 매칭하여 특징점마다 최근접 이웃 2개 찾음
                T=0.7
                # 좋은 매칭 골라 good_match에 저장
                good_match=[]
                for nearest1,nearest2 in knn_match:
                    if (nearest1.distance/nearest2.distance)<T:
                        good_match.append(nearest1)
                GM.append(good_match) # good_match를 GD에 추가       
            
            best=GM.index(max(GM,key=len)) # 표지판 영상 3장의 매칭 리스트 중 max 찾음 => 매칭 쌍 개수가 최대인 번호판 찾기
            
            if len(GM[best])<4:	# 최선의 번호판이 매칭 쌍 4개 미만이면 인식 실패로 간주
                self.label.setText('표지판이 없습니다.')  
            else:			# 인식에 성공한 경우(호모그래피 찾아 영상에 표시)
                sign_kp=KD[best][0] # 표지판 영상의 특징점
                good_match=GM[best] # 매칭 쌍 정보 저장
            
                points1=np.float32([sign_kp[gm.queryIdx].pt for gm in good_match])
                points2=np.float32([road_kp[gm.trainIdx].pt for gm in good_match])
                
                H,_=cv.findHomography(points1,points2,cv.RANSAC)
                
                h1,w1=self.signImgs[best].shape[0],self.signImgs[best].shape[1] # 번호판 영상의 크기
                h2,w2=self.roadImg.shape[0],self.roadImg.shape[1] # 도로 영상의 크기
                
                box1=np.float32([[0,0],[0,h1-1],[w1-1,h1-1],[w1-1,0]]).reshape(4,1,2)
                box2=cv.perspectiveTransform(box1,H)
                
                self.roadImg=cv.polylines(self.roadImg,[np.int32(box2)],True,(0,255,0),4)
                
                img_match=np.empty((max(h1,h2),w1+w2,3),dtype=np.uint8)
                cv.drawMatches(self.signImgs[best],sign_kp,self.roadImg,road_kp,good_match,img_match,flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
                cv.imshow('Matches and Homography',img_match)
                
                self.label.setText(self.signFiles[best][1]+' 보호구역입니다. 30km로 서행하세요.')   # best가 0:어린이, 1:노인, 2:장애인                 
                winsound.Beep(3000,500) # 비프음 발생         
                      
    def quitFunction(self): # '나가기' 버튼 클릭 시 실행
        cv.destroyAllWindows()        
        self.close()
                
app=QApplication(sys.argv) 
win=TrafficWeak() 
win.show()
app.exec_()

🌿 프로그램의 한계점

  • 정지 영상을 불러들여 표지판 인식함
  • 환경과 상호작용 강화 위해 동영상에서 인식 가능하도록 확장해야 함
    • ex) 자동차 블랙박스, 휴대폰 실시간 동영상 등

6.5 [비전 에이전트 3] 파노라마 영상 제작

📌 SIFT를 이용한 영상 봉합

🌿 기능

  1. <영상 수집> 버튼 : 웹 캠으로 영상 수집
  2. <영상 수집> 버튼 : 수집한 영상 확인
  3. <봉합> 버튼 : 수집한 영상 봉합 → 파노라마 영상 제작
  4. <저장> 버튼 : 파노라마 영상 저장
  • ) 영상 수집 마치기 전엔 나머지 버튼들 비활성해야 함

⇒ 결과 : 수집한 영상들 봉합한 파노라마 영상 보여줌

from PyQt5.QtWidgets import *
import cv2 as cv
import numpy as np
import winsound
import sys

# Panorama 클래스 선언
class Panorama(QMainWindow) :
    def __init__(self) :
        super().__init__()
        self.setWindowTitle('파노라마 영상')
        self.setGeometry(200,200,700,200)
        
        # 버튼 5개, 레이블 1개 생성
        collectButton=QPushButton('영상 수집',self)
        self.showButton=QPushButton('영상 보기',self) 
        self.stitchButton=QPushButton('봉합',self) 
        self.saveButton=QPushButton('저장',self)
        quitButton=QPushButton('나가기',self)
        self.label=QLabel('환영합니다!',self)
        
        collectButton.setGeometry(10,25,100,30)
        self.showButton.setGeometry(110,25,100,30) 
        self.stitchButton.setGeometry(210,25,100,30) 
        self.saveButton.setGeometry(310,25,100,30)
        quitButton.setGeometry(450,25,100,30) 
        self.label.setGeometry(10,70,600,170)

        # 나머지 3개 버튼 비활성화
        self.showButton.setEnabled(False) 
        self.stitchButton.setEnabled(False) 
        self.saveButton.setEnabled(False)
        
        # 버튼 클릭 시 수행할 콜백함수 등록
        collectButton.clicked.connect(self.collectFunction)
        self.showButton.clicked.connect(self.showFunction)       
        self.stitchButton.clicked.connect(self.stitchFunction) 
        self.saveButton.clicked.connect(self.saveFunction)   
        quitButton.clicked.connect(self.quitFunction)        

    # <영상 수집> 버튼 클릭 시 실행
    def collectFunction(self):
        # <영상 보기>,<봉합>,<저장> 버튼 비활성화 : 원래 프로그램 시작될 때 비활성화되지만, 사용자가 다시 시도하는 경우 대비
        self.showButton.setEnabled(False) 
        self.stitchButton.setEnabled(False) 
        self.saveButton.setEnabled(False)
        self.label.setText('c를 여러 번 눌러 수집하고 끝나면 q를 눌러 비디오를 끕니다.')
        
        self.cap=cv.VideoCapture(0,cv.CAP_DSHOW)
        if not self.cap.isOpened(): sys.exit('카메라 연결 실패')
        
        self.imgs=[]   
        while True:
            ret,frame=self.cap.read()  
            if not ret: break
            
            cv.imshow('video display', frame)
            
            key=cv.waitKey(1)
            if key==ord('c'):   # c 누르면            
                self.imgs.append(frame)	# 영상 저장
            elif key==ord('q'): # q 누르면
                self.cap.release() # 비디오 연결 끊고
                cv.destroyWindow('video display')  # 연결된 윈도우 닫음              
                break 
        
        if len(self.imgs)>=2:		# 수집한 영상이 2장 이상이면 나머지 3개 버튼 활성화
            self.showButton.setEnabled(True) 
            self.stitchButton.setEnabled(True) 
            self.saveButton.setEnabled(True)        

    # <영상 보기> 버튼 클릭 시 실행                
    def showFunction(self):
        self.label.setText('수집된 영상은 '+str(len(self.imgs))+'장 입니다.')   # 수집된 영상 수 레이블에 표시
        stack=cv.resize(self.imgs[0],dsize=(0,0),fx=0.25,fy=0.25)   # 수집된 영상 0.25배 축소
        for i in range(1,len(self.imgs)):   # 수집한 영상들 가로로 이어붙임
            stack=np.hstack((stack,cv.resize(self.imgs[i],dsize=(0,0),fx=0.25,fy=0.25))) 
        cv.imshow('Image collection',stack) # 이어붙인 영상 디스플레이        

    # <봉합> 버튼 클릭 시 실행    
    def stitchFunction(self):
        stitcher=cv.Stitcher_create()   # 영상 봉합에 쓸 stitcher 객체 생성
        status,self.img_stitched=stitcher.stitch(self.imgs) # stitch 함수로 봉합 시도, stitch(수집한 영상 객체)
        if status==cv.STITCHER_OK:  # 봉합 성공
            cv.imshow('Image stitched panorama',self.img_stitched)  # 새 윈도우에 파노라마 영상 디스플레이  
        else:   # 봉합 실패
            winsound.Beep(3000,500) # 비프음 출력       
            self.label.setText('파노라마 제작에 실패했습니다. 다시 시도하세요.')    

    # <저장> 버튼 클릭 시 실행        
    def saveFunction(self):
        fname=QFileDialog.getSaveFileName(self,'파일 저장','./')
        cv.imwrite(fname[0],self.img_stitched)

    # <나가기> 버튼 클릭 시 실행    
    def quitFunction(self): 
        self.cap.release() 
        cv.destroyAllWindows()  
        self.close()

app=QApplication(sys.argv) 
win=Panorama() 
win.show()
app.exec_()

🌿 실행 결과

  1. <영상 수집> 이외 3개의 버튼 비활성화 상태

Untitled

  1. 2장 이상 영상 수집 후

Untitled

Untitled

음….??..ㅠㅠ

6.6 [비전 에이전트 4] 특수 효과

📌 특수 효과의 원리

  • stylization : 카툰 효과, 디폴트값 O
  • pencilSketch : 연필 스케치 효과, 디폴트값 O
  • oilPainting : 유화 효과, 디폴트값 X

💡 함수 매개변수 확인

Untitled

Untitled

  • 에지 보존 필터 : 물체 경계의 명암 대비 유지하면서 다른 부분만 흐릿하게 만듦
  • 양방향 필터(bilateral filter) : 가장 # 쓰이는 에지 보존 필
    • 필터 u → 가우시안 필터로 대체

Untitled

➡️ 양방향 필터 식

➡️ 양방향 필터 식

Untitled

2개의 가우시안 함수

  1. gs_(i) : i=0 일 때 가장 큼, i가 0에서 멀어질수록 작아짐

  2. g_r(f(x)-f(x+i)

    • 매개변수 : (필터 씌워진 현재 화소 값) - (필터 씌워진 이웃 화소 값)
    • 두 화소 값 차이 클수록 g_r 작아짐
    • g_r로 인해 i에 해당하는 화소는 중앙 화소값과 차이 클수록 가중치 낮아짐

    ⇒ 물체 경계에서 서로 다른 물체에 속한 화소는 서로에게 영향력 낮아짐 → 에지 잘 보존

stylization & pencilSketch 매개변수

  • sigma_s : g_s의 표준편차, 클수록 영상 흐릿하게 만드는 효과 up
  • sigma_r : g_r의 표준편차, 작을수록 에지 보존 효과 up

📌 정지 영상의 특수 효과

🌿 사용자 인터페이스

  1. <사진 읽기> 버튼 : 폴더에서 사진 영상 선택, 읽어오기
  2. <엠보싱>, <카툰>, <연필 스케치>, <유화> 버튼 : 4가지 특수 효과 지원
  3. <저장하기> 버튼 : 특수 효과 처리된 영상 저장, 콤보박스를 이용해 어떤 특수 효과를 저장할지 선택 가능
import cv2 as cv
import numpy as np
from PyQt5.QtWidgets import *
import sys    
    
class SpecialEffect(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('사진 특수 효과')
        self.setGeometry(200,200,800,200)
       
        # 버튼 7개,레이블 1개,콤보박스 1개 생성
        pictureButton=QPushButton('사진 읽기',self)
        embossButton=QPushButton('엠보싱',self)
        cartoonButton=QPushButton('카툰',self)
        sketchButton=QPushButton('연필 스케치',self)
        oilButton=QPushButton('유화',self)
        saveButton=QPushButton('저장하기',self) 
        self.pickCombo=QComboBox(self)     
        self.pickCombo.addItems(['엠보싱','카툰','연필 스케치(명암)','연필 스케치(컬러)','유화'])
        quitButton=QPushButton('나가기',self)        
        self.label=QLabel('환영합니다!',self)
        
        # 버튼, 레이블, 콤보박스 위젯의 위치와 크기를 지정
        pictureButton.setGeometry(10,10,100,30)
        embossButton.setGeometry(110,10,100,30)
        cartoonButton.setGeometry(210,10,100,30)
        sketchButton.setGeometry(310,10,100,30)
        oilButton.setGeometry(410,10,100,30)  
        saveButton.setGeometry(510,10,100,30)
        self.pickCombo.setGeometry(510,40,110,30)                  
        quitButton.setGeometry(620,10,100,30)
        self.label.setGeometry(10,40,500,170)
        
        # 버튼을 클릭했을 때 수행할 콜백 함수 등록
        pictureButton.clicked.connect(self.pictureOpenFunction)
        embossButton.clicked.connect(self.embossFunction) 
        cartoonButton.clicked.connect(self.cartoonFunction)
        sketchButton.clicked.connect(self.sketchFunction)
        oilButton.clicked.connect(self.oilFunction) 
        saveButton.clicked.connect(self.saveFunction)    
        quitButton.clicked.connect(self.quitFunction)

    # <사진 읽기> 버튼 클릭 시 실행
    def pictureOpenFunction(self):
        # 폴더에서 사진 파일을 브라우징하여 선택하고 읽어오기
        fname=QFileDialog.getOpenFileName(self,'사진 읽기','./')
        self.img=cv.imread(fname[0])    
        if self.img is None: sys.exit('파일을 찾을 수 없습니다.')  
        
        cv.imshow('Painting',self.img)         

    # <엠보싱> 버튼 클릭 시 실행
    def embossFunction(self):
        femboss=np.array([[-1.0, 0.0, 0.0],[0.0, 0.0, 0.0],[0.0, 0.0, 1.0]])    # 엠보싱 필터 정의
        # 명암 영상으로 변환하 고 컨볼루션을 적용
        gray=cv.cvtColor(self.img,cv.COLOR_BGR2GRAY)    
        gray16=np.int16(gray)
        self.emboss=np.uint8(np.clip(cv.filter2D(gray16,-1,femboss)+128,0,255))
        
        cv.imshow('Emboss',self.emboss)
    
    # <카툰> 버튼 클릭 시 실행
    def cartoonFunction(self):
        self.cartoon=cv.stylization(self.img,sigma_s=60,sigma_r=0.45)   # 매개변수는 기본값으로 설정
        cv.imshow('Cartoon',self.cartoon) 
    
    # <연필 스케치> 버튼 클릭 시 실행
    def sketchFunction(self):
        self.sketch_gray,self.sketch_color=cv.pencilSketch(self.img,sigma_s=60,sigma_r=0.07,shade_factor=0.02)
        cv.imshow('Pencil sketch(gray)',self.sketch_gray)
        cv.imshow('Pencil sketch(color)',self.sketch_color)

    # <유화> 버튼 클릭 시 실행
    def oilFunction(self):
        self.oil=cv.xphoto.oilPainting(self.img,10,1,cv.COLOR_BGR2Lab)
        cv.imshow('Oil painting',self.oil) 
                           
    def saveFunction(self):      
        fname=QFileDialog.getSaveFileName(self,'파일 저장','./')
        
        i=self.pickCombo.currentIndex()
        if i==0: cv.imwrite(fname[0],self.emboss)
        elif i==1: cv.imwrite(fname[0],self.cartoon)
        elif i==2: cv.imwrite(fname[0],self.sketch_gray)
        elif i==3: cv.imwrite(fname[0],self.sketch_color)
        elif i==4: cv.imwrite(fname[0],self.oil)
                        
    def quitFunction(self):
        cv.destroyAllWindows()        
        self.close()
                
app=QApplication(sys.argv) 
win=SpecialEffect() 
win.show()
app.exec_()

Untitled

📌 비디오 영상에 특수 효과 처리

import cv2 as cv
import numpy as np
from PyQt5.QtWidgets import *
import sys
    
class VideoSpecialEffect(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('비디오 특수 효과')
        self.setGeometry(200,200,400,100)
       
        videoButton=QPushButton('비디오 시작',self)
        self.pickCombo=QComboBox(self)     
        self.pickCombo.addItems(['엠보싱','카툰','연필 스케치(명암)','연필 스케치(컬러)','유화'])
        quitButton=QPushButton('나가기',self)        
        
        videoButton.setGeometry(10,10,140,30)
        self.pickCombo.setGeometry(150,10,110,30)                  
        quitButton.setGeometry(280,10,100,30)
        
        videoButton.clicked.connect(self.videoSpecialEffectFunction) 
        quitButton.clicked.connect(self.quitFunction)
        
    def videoSpecialEffectFunction(self):             
        self.cap=cv.VideoCapture(0,cv.CAP_DSHOW) 
        if not self.cap.isOpened(): sys.exit('카메라 연결 실패')
        
        while True:
            ret,frame=self.cap.read()  
            if not ret: break

            pick_effect=self.pickCombo.currentIndex()        
            if pick_effect==0:
                femboss=np.array([[-1.0, 0.0, 0.0],[0.0, 0.0, 0.0],[0.0, 0.0, 1.0]])
                gray=cv.cvtColor(frame,cv.COLOR_BGR2GRAY)    
                gray16=np.int16(gray)
                special_img=np.uint8(np.clip(cv.filter2D(gray16,-1,femboss)+128,0,255))
            elif pick_effect==1:
                special_img=cv.stylization(frame,sigma_s=60,sigma_r=0.45)
            elif pick_effect==2:    
                special_img,_=cv.pencilSketch(frame,sigma_s=60,sigma_r=0.07,shade_factor=0.02)
            elif pick_effect==3:    
                _,special_img=cv.pencilSketch(frame,sigma_s=60,sigma_r=0.07,shade_factor=0.02)
            elif pick_effect==4:    
                special_img=cv.xphoto.oilPainting(frame,10,1,cv.COLOR_BGR2Lab)
                
            cv.imshow('Special effect',special_img)              
            cv.waitKey(1) 
                                        
    def quitFunction(self):
        self.cap.release()
        cv.destroyAllWindows()        
        self.close()
                
app=QApplication(sys.argv) 
win=VideoSpecialEffect() 
win.show()
app.exec_()
profile
안녕하세요😊 컴퓨터비전을 공부하고 있습니다 🙌

0개의 댓글