본문 바로가기

프로젝트 이야기/드론을 이용한 식물 이상 탐지 시스템

드론을 이용한 식물 이상 탐지 시스템 - Mainwindow 구현

Mainwindow의 PyQt5 코드 구현에 대해 설명하도록 하겠습니다.

 

전체적인 Mainwindow의 모습입니다.

버튼 하나하나 코드를 어떻게 구현했는지 알아보도록 하겠습니다.

 

from PyQt5 import QtCore, QtGui, QtWidgets # 위젯 클래스들
from PyQt5.QtWidgets import * # QtWidgets 내부의 함수들
from PyQt5.QtCore import QThread, pyqtSignal # 쓰레드를 이용하기 위해 포함
from ui.mainwindow_ui import Ui_Mainwindow # Mainwindow ui 코드
from ui.connectwindow_ui import Ui_Connect # Connectwindow ui 코드
from ui.dbwindow_ui import Ui_DBwindow # DBwindow ui 코드
import os, sys, time
import shutil
import maps, server, vfs # 각 기능에 사용할 코드들

먼저 import 부분입니다.

주석처리에 기재된 것과 같이 필요한 부분들을 import 시켜 주었습니다.

os, sys, time, shutil은 각 기능을 위해 기본 함수들을 import 시켜줍니다.

 

각 Widget들에 사용할 UI는 이전 포스팅에 했던 방식으로 Qt Creator로 제작하여

pyuic5를 이용해 모두 .py 파일로 만들어 두었습니다.

 

class MainWindow(Ui_Mainwindow):
    # 실행시 초기화
    def __init__(self, w):
        self.temppath = "temp/"
        self.slicepath = "slice/"

        self.AnalysisThread = darknet()
        self.AnalysisThread.printLog.connect(self.logPrint)

        Ui_Mainwindow.__init__(self)
        self.setupUi(w)
        self.initButton.clicked.connect(self.InitData)
        self.recvButton.clicked.connect(self.DataReceive)
        self.seeButton.clicked.connect(self.SeeDatamap)
        self.analButton.clicked.connect(self.DataAnalysis)
        self.closeButton.clicked.connect(QtCore.QCoreApplication.instance().quit)

    # Cache Clear 버튼
    def InitData(self):
        self.logBrowser.append("Cache Initialize Start..."  )
        if os.path.isdir(self.temppath):
            filelist1 = os.listdir(self.temppath)

            for file in filelist1:
                os.remove(self.temppath + file)

        else:
            self.logBrowser.append("Cache Folder not exist!!")
            os.mkdir(self.temppath)
            self.logBrowser.append("Created Cache Folder!")

        if os.path.isdir(self.slicepath):
            filelist2 = os.listdir(self.slicepath)

            for file in filelist2:
                os.remove(self.slicepath + file)
        else:
            self.logBrowser.append("Cache Folder not exist!!")
            os.mkdir(self.slicepath)
            self.logBrowser.append("Created Cache Folder!")
        
        self.logBrowser.append("Cache Initialize Finished")

    # Data Analysis 버튼
    def DataAnalysis(self):
        if self.AnalysisThread.workingFlag == 0:
            self.AnalysisThread.start()
        else:
            self.logBrowser.append("Please wait work finish")

    # Data Receive 버튼
    def DataReceive(self):
        w2 = QtWidgets.QDialog()
        ui = ConnectWindow(w2)
        w2.exec_()

        if ui.successFlag == 1:
            self.logPrint("Data Receive Success!")
        elif ui.successFlag == 2:
            self.logPrint("Data Receive Fail...")

    # See Datamap 버튼
    def SeeDatamap(self):
        w3 = QtWidgets.QDialog()
        ui = DBWindow(w3)
        w3.exec_()

        if ui.url != "":
            self.htmlBrowser.load(QtCore.QUrl(ui.url))

    # 로그를 출력하기 위해 정의한 함수. Thread로부터 Signal을 받아 실행함.
    def logPrint(self, text):
        self.logBrowser.append(text)

Mainwindow 전체 코드입니다.

 

각 UI는 .ui파일을 변환해 만든 .py파일을 상속받아 구현할 수 있습니다.

class MainWindow(Ui_Mainwindow): 부분을 보면 Ui_Mainwindow를 상속받아 이용한 것을 확인할 수 있습니다.

 

mainwindow를 구현한 mainwindow_ui.py 파일을 보면

메인윈도우가 Ui_Mainwindow 라는 클래스로 생성된 것을 확인할 수 있습니다.

이 Ui_Mainwindow를 상속받아 클래스로 구현하면 UI를 실행시킬 수 있습니다.

 

    def __init__(self, w):
        # 각 경로를 저장할 변수
        self.temppath = "temp/"
        self.slicepath = "slice/"

        # 쓰레드 변수
        self.AnalysisThread = darknet()
        # 쓰레드 내의 변수와 로그 출력 변수 연결
        self.AnalysisThread.printLog.connect(self.logPrint)

        # 각 버튼들과 함수 기능 연결
        Ui_Mainwindow.__init__(self)
        self.setupUi(w)
        self.initButton.clicked.connect(self.InitData)
        self.recvButton.clicked.connect(self.DataReceive)
        self.seeButton.clicked.connect(self.SeeDatamap)
        self.analButton.clicked.connect(self.DataAnalysis)
        self.closeButton.clicked.connect(QtCore.QCoreApplication.instance().quit)

__init__ 함수입니다.

__init__ 함수는 클래스가 만들어 질 때, 즉 UI 윈도우가 처음 켜질 때 한 번 실행되는 부분입니다.

 

각 기능 처리에 필요한 경로를 담은 함수를 선언하고

데이터 분석을 실행할 Thread 변수를 만들어 줍니다.

로그 출력을 위해 Thread에서 신호를 주고받을 수 있는 변수도 선언해 줍니다.

버튼이 클릭되었을 때 실행되게 할 것이므로 버튼과 함수를 모두 clicked.connectI()로 연결해 줍니다.

 

다음으로는 각 버튼 구현에 대해 설명하겠습니다.

 

    def InitData(self): # 버튼을 클릭했을 시 실행되는 함수
        self.logBrowser.append("Cache Initialize Start..."  ) # 초기화 시작 메시지를 출력
        # 전송 파일이 저장될 폴더가 있으면
        if os.path.isdir(self.temppath):
            filelist1 = os.listdir(self.temppath)

            # 내용 모두 삭제
            for file in filelist1:
                os.remove(self.temppath + file)
                
        # 폴더가 없으면
        else:
            # 폴더 생성
            self.logBrowser.append("Cache Folder not exist!!")
            os.mkdir(self.temppath)
            self.logBrowser.append("Created Cache Folder!")

        # 전처리 된 파일이 저장될 폴더가 있으면
        if os.path.isdir(self.slicepath):
            filelist2 = os.listdir(self.slicepath)

            # 내용 모두 삭제
            for file in filelist2:
                os.remove(self.slicepath + file)
                
        # 폴더가 없으면
        else:
            # 폴더 생성
            self.logBrowser.append("Cache Folder not exist!!")
            os.mkdir(self.slicepath)
            self.logBrowser.append("Created Cache Folder!")
        
        self.logBrowser.append("Cache Initialize Finished" # 초기화 끝 메시지 출력

Cache Clear 버튼을 구현한 함수입니다.

보시다시피, 각 버튼이 눌렸을 때 실행되는 기능은 각각 함수로 구현해줄 수 있습니다.

버튼을 클릭하면 각 캐시 폴더가 있는지 검사하고 내용물을 모두 삭제해 초기화 해 줍니다.

 

    def DataAnalysis(self):
        # 작업중인지 검사, flag가 1이면 작업중, 1이면 작업중 아님
        if self.AnalysisThread.workingFlag == 0:
            # 이미 분석중이 아니라면 분석 시작
            self.AnalysisThread.start()
        else:
            # 이미 분석중이라면 메시지 출력
            self.logBrowser.append("Please wait work finish")

Data Analysis 버튼을 구현한 함수입니다.

버튼을 클릭하면 분석중인지 아닌지 검사를 한 후, 분석중이 아니라면 분석 쓰레드를 실행시킵니다.

PyQt5에서는 기본적으로 작업이 실행중일 땐 다른 작업을 실행할 수 없기 때문에

분석중에 지속적으로 로그가 출력되고, 분석중에도 다른 작업이 가능하도록 쓰레드로 구현했습니다.

 

PyQt5의 쓰레드는 QThread라는 클래스를 상속받아 간단하게 구현이 가능합니다.

밑은 데이터 분석 쓰레드 코드입니다.

class darknet(QThread):
    printLog = pyqtSignal(str)
    workingFlag = 0
    mapfunc = maps.datamap()

    def run(self):
        try:
            self.workingFlag = 1
            self.printLog.emit("Analysis Initializing...")
            sys.path.append(os.path.join(os.getcwd(),'darknet/python/'))
            sys.path.append(os.path.join(os.getcwd()))
            import darknet as dn
            import pdb

            dn.set_gpu(0)
            net = dn.load_net("darknet/cfg/yolov3.cfg".encode('utf-8'), "darknet/backup_corn/yolov3_34000.weights".encode('utf-8'), 0)
            meta = dn.load_meta("darknet/cfg/coco.data".encode('utf-8'))


            path = 'slice/'
            file_list = os.listdir(path)
            if ".DS_Store" in file_list:
                file_list.remove(".DS_Store")

            total_file_num = len(file_list)
            current_file_num = int()

            if os.path.isfile('detection_result.txt'):
                os.remove('detection_result.txt')
            f = open('detection_result.txt', 'a')
                
            self.printLog.emit("Start Analysis...")
            for file_name in file_list:
                self.printLog.emit("Analysing... " + str(current_file_num) + '/' + str(total_file_num))
                filepath = path + file_name
                r = dn.detect(net, meta, filepath.encode('utf-8'))

                current_file_num = current_file_num + 1
                print(str(current_file_num) + '///' + str(total_file_num))
                
                if r:
                    print(r)
                    print(file_name)
                    
                    
                    f.write(file_name)
                    f.write('\n')

            f.close()
            self.printLog.emit("Analysing... " + str(current_file_num) + '/' + str(total_file_num))

            self.printLog.emit("Detect Finished! Making datamap...")
            self.mapfunc.create_map()
            self.printLog.emit("Analysing Finished! Please check DB")
            self.workingFlag = 0
            print("darknet END")

        except OSError:
            self.printLog.emit("Darknet Faild to start!")
            self.printLog.emit("Darknet Reinitializing...")

            if os.path.isfile("darknet/libdarknet.so"):
                os.remove("darknet/libdarknet.so")
            if os.path.isfile("darknet/libdarknet.a"):
                os.remove("darknet/libdarknet.a")
            if os.path.isfile("darknet/darknet"):
                os.remove("darknet/darknet")
            if os.path.isdir("darknet/obj/"):
                shutil.rmtree("darknet/obj/")

            makepath = os.getcwd() + "/darknet"
            os.system("make -C " + makepath)

            self.printLog.emit("Darknet Reinitializing Finished")
            self.printLog.emit("Please try again Analysis")
            self.workingFlag = 0

Darknet의 Detector 예제 코드를 이식하여 만들었습니다.

분석 실행이 불가능할 경우 자동적으로 다시 make를 진행하여 실행 가능하도록 만들도록 작성했습니다.

또, 분석중 mainwindow에 로그를 출력할 수 있도록 만들었습니다.

 

Thread에서 Widget으로 신호를 보내고 싶을 땐 pyqtSignal을 이용해야 합니다.

여기에서는 문자열 하나를 신호로 보낼 수 있는 pyqtSignal 변수를 printLog라는 이름으로 만들었습니다.

__init__ 부분에서 self.AnalysisThread.printLog.connect(self.logPrint) 부분이 mainwindow와 Thread를 연결하는 부분입니다.

이후엔 emit 함수를 이용하여 pyqtSignal을 보낼 수 있습니다.

 

이런 방식으로 버튼을 클릭했을 시 실행되는 기능을 쓰레드로 구현하였습니다.

 

Darknet의 Detector를 이식할 때 발생하는 다양한 오류에 대한 처리는

이전 포스팅(https://dudgns7675.tistory.com/9)을 참고해 주세요.

 

분석이 완료되면 maps.datamap() 함수가 실행되어 결과데이터를 지도로 작성합니다.

maps 코드는 아래와 같이 구현되어 있습니다.

 

import folium
import base64
import os
from math import radians, cos, sin, asin, sqrt
from datetime import datetime
from PIL import Image

class datamap():
	def __init__(self):
		pass

	def create_map(self):
		templist = os.listdir(os.getcwd()+"/temp/")
		loc_filename = ""
		for name in templist:
			if ".txt" in name:
				loc_filename = name
		print(loc_filename)
		loc_data = open("temp/"+loc_filename, 'r')
		print("loc data open success")
		detected_data = open("detection_result.txt", "r")
		print("detected data open success")

		detected_idx_list = list()
		try:
			for d in detected_data:
				detected_idx_list.append(int(d.replace('\n','').split('.')[0]))
		except:
			pass

		print(detected_idx_list)

		idx_list = list()
		loc_list = list()
		for l in loc_data:
			idx_list.append(l.replace('\n','').split(':')[0])
			loc_list.append(l.replace('\n','').split(':')[1].split(','))

		for index in detected_idx_list:
			try:
				m = folium.Map(location=[float(loc_list[index][0]), float(loc_list[index][1])],
								zoom_start=17)
				break
			except:
				continue

		markerList = list()
		skipFlag = 0
		for loc in detected_idx_list:
			try:
				if ((loc_list[loc][0] == "NaN") | (loc_list[loc][1] == "NaN")):
					continue
				loc_list[loc][0]
				for marker in markerList:
					distance = datamap.calcDistance(marker[1],marker[0],float(loc_list[loc][1]),float(loc_list[loc][0]))
					if distance < 6.0:
						skipFlag = 1
						break
				if skipFlag == 1:
					skipFlag = 0
					continue
				tooltip = loc_list[loc][0] + ',' + loc_list[loc][1]
				image_name = 'slice/' + str(loc) + '.jpg'
				image = Image.open(image_name).resize((280,280)).save("temp.jpg")
				pic = base64.b64encode(open("temp.jpg",'rb').read()).decode()
				image_tag = '<img src="data:image/jpeg;base64,{}">'.format(pic)
				iframe = folium.IFrame(image_tag, width=300, height=300)
				popup = folium.Popup(iframe, max_width=1200)

				folium.Marker([float(loc_list[loc][0]), float(loc_list[loc][1])],
								popup=popup,
								tooltip=tooltip).add_to(m)

				markerList.append([float(loc_list[loc][0]), float(loc_list[loc][1])])
			except:
				continue

		savename = "DBResult/"+datamap.date()+".html"
		m.save(savename)
		os.remove("temp.jpg")

	def calcDistance(lon1, lat1, lon2, lat2):
	    """
	    Calculate the great circle distance between two points 
	    on the earth (specified in decimal degrees)
	    """
	    # convert decimal degrees to radians 
	    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
	    # haversine formula 
	    dlon = lon2 - lon1 
	    dlat = lat2 - lat1 
	    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
	    c = 2 * asin(sqrt(a)) 
	    km = 6367 * c
	    m = km * 1000
	    return m

	def date():
		now = datetime.now()
		day = "%s-%s-%s_%s:%s:%s" % (now.year, now.month, now.day, now.hour, now.minute, now.second)
		return day

folium을 이용하여 결과 데이터를 지도로 작성합니다.

folium에 관련된 정보는 이전 포스팅(https://dudgns7675.tistory.com/6)에 기재되어 있습니다.

 

지도에 마커를 생성하는 과정에서 마커가 너무 많이 생성되는 것을 방지하기 위하여

한 번 탐지되어 마커가 생성된 부분에서 6m이상 떨어져 있을 경우에만 새 마커가 생성되도록 작성하였습니다.

6m로 설정한 이유는 GPS의 오차범위 때문입니다.

미국 GPS에서 발표한 자료(https://www.gps.gov/systems/gps/performance/accuracy/)에 의하면 GPS는 평균적으로 4.9m의 오차범위를 가진다고 합니다. 이를 참고로 하여 새 마커가 생성되는 거리를 6m로 설정하였습니다.

 

각 마커간의 거리는 GPS 좌표에서 나타나는 위도와 경도를 이용하여 계산하였습니다.

 

이렇게 만들어진 데이터는 html 형식의 파일로 출력됩니다.

 

 

    def DataReceive(self):
        # 버튼이 클릭되면 새 위젯을 만들고 대기
        w2 = QtWidgets.QDialog()
        ui = ConnectWindow(w2)
        w2.exec_()

        # 새 위젯이 종료되면 결과에 따라 로그메시지 출력
        if ui.successFlag == 1:
            self.logPrint("Data Receive Success!")
        elif ui.successFlag == 2:
            self.logPrint("Data Receive Fail...")

Data Receive 버튼을 구현한 함수입니다.

버튼이 클릭되면 새 Widget을 하나 만들고, ConnectWindow를 상속받아 UI를 만들어 줍니다.

exec_() 함수를 사용하면 Widget이 실행되고, Widget이 종료될 때 까지 대기하게 됩니다.

exec_() 함수 아래 부분들은 Widget이 종료되면 실행됩니다.

여기에서는 connectwindow 창에서 데이터 전송 기능이 모두 실행되고 Widget이 종료되면

데이터 전송 상태를 판별해서 성공/실패 메시지를 출력하도록 만들었습니다.

 

    def SeeDatamap(self):
        # 버튼이 클릭되면 새 위젯을 만들고 대기
        w3 = QtWidgets.QDialog()
        ui = DBWindow(w3)
        w3.exec_()

        # 새 위젯이 종료되면 맵을 로드
        if ui.url != "":
            self.htmlBrowser.load(QtCore.QUrl(ui.url))

See Datamap 버튼을 구현한 함수입니다.

Data Receive 버튼과 같이 새 Widget을 만들어 dbwindow의 기능을 구현하고

dbwindow가 종료되면 처리된 데이터를 받아 mainwindow에 출력하도록 만들었습니다.

 

    def logPrint(self, text):
        self.logBrowser.append(text)

pyqtSignal을 전송받아 로그를 출력하기 위해 정의한 간단한 함수입니다.

text가 입력으로 들어오면 로그창에 그 text를 출력합니다.

 

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = QtWidgets.QMainWindow()
    ui = MainWindow(w)
    w.show()
    sys.exit(app.exec_())

마지막으로 코드를 직접 실행하면 Mainwindow가 실행되도록 main을 구성해 주었습니다.

파이썬은 C나 여타 다른 코드와 달리 main 함수를 구분하는 부분이 없기 때문에

__name__ 변수를 검사하여 main으로 실행된 것인지 판별합니다.

 

코드가 실행되면 Mainwindow가 실행되도록 구성했습니다.

물론 PyQt5의 기능을 이용하여 실행 파일로 릴리즈 하는 것도 가능은 합니다만

이럴 경우 Mac OS와 Ubuntu간의 호환성이 없어지므로 개발중에는 코드를 직접 실행하는 방식으로 작성을 했습니다.

 

 

이런 방식으로 메인 윈도우의 GUI가 모두 구현되었습니다.

다음으로는 각 Widget들, 데이터 전송 창과 데이터 열람 창의 구현에 대해 알아보겠습니다.