최성환

box labeling변경 및 inference time 측정 코드 추가

1 +# USAGE
2 +# python detect_mask_video.py
3 +
4 +# import the necessary packages
5 +from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
6 +from tensorflow.keras.preprocessing.image import img_to_array
7 +from tensorflow.keras.models import load_model
8 +import numpy as np
9 +import argparse
10 +import os
11 +import cv2
12 +import sys
13 +from PyQt5 import QtCore
14 +from PyQt5 import QtWidgets
15 +from PyQt5 import QtGui
16 +from PyQt5 import QtTest
17 +import pyaudio
18 +import wave
19 +import requests
20 +import time
21 +
22 +#Audio
23 +# Record Audio의 startRecording 메서드에서 input_device_index는 기기마다 다름.
24 +FORMAT = pyaudio.paInt16
25 +CHANNELS = 1
26 +RATE = 16000
27 +CHUNK = 1024
28 +MAX_RECORD_SECONDS = 30
29 +WAVE_OUTPUT_FILENAME = "saved_voice\\audiofile\\file.wav"
30 +WAVE_ENROLL_FILENAME = "saved_voice\\enrollfile\\file.wav"
31 +#URL
32 +URL = 'http://163.180.146.68:7777/{}'
33 +#SpeakerRecognition
34 +THRESHOLD = 0.8
35 +SPEAKER_ID = 'NA'
36 +
37 +class ShowVideo(QtCore.QObject):
38 + flag_detect_mask = True
39 + run_video = True
40 +
41 + camera = cv2.VideoCapture(0) # 연결된 영상장치 index, 기본은 0
42 +
43 + ret, image = camera.read() # 2개의 값 리턴, 첫 번째는 프레임 읽음여부, 두 번째는 프레임 자체
44 + height, width = image.shape[:2]
45 +
46 + VideoSignal1 = QtCore.pyqtSignal(QtGui.QImage) # VideoSignal1이라는 사용자 정의 시그널 생성
47 +
48 + def __init__(self, parent=None):
49 + super(ShowVideo, self).__init__(parent)
50 +
51 + @QtCore.pyqtSlot()
52 + def startVideo(self, faceNet, maskNet):
53 + global image
54 +
55 + run_video = True
56 + self.flag_detect_mask = True
57 + while run_video:
58 + ret, image = self.camera.read()
59 +
60 + # detect faces in the frame and determine if they are wearing a
61 + # face mask or not
62 + QtWidgets.QApplication.processEvents()
63 + if self.flag_detect_mask:
64 + (locs, preds) = detect_and_predict_mask(image, faceNet, maskNet)
65 +
66 + # QtWidgets.QApplication.processEvents()
67 + # if self.flag_detect_mask:
68 + frame = image
69 + # loop over the detected face locations and their corresponding
70 + # locations
71 + for (box, pred) in zip(locs, preds):
72 + # unpack the bounding box and predictions
73 + (startX, startY, endX, endY) = box
74 + (mask, withoutMask) = pred
75 +
76 + # determine the class label and color we'll use to draw
77 + # the bounding box and text
78 + label = "Mask" if mask > withoutMask else "No Mask" # 박스 상단 출력 string
79 + color = (0, 255, 0) if label == "Mask" else (0, 0, 255)
80 +
81 + # include the probability in the label
82 + label = "{}: {:.2f}%".format(label, max(mask, withoutMask) * 100)
83 +
84 + # display the label and bounding box rectangle on the output
85 + # frame
86 + cv2.putText(frame, label, (startX, startY - 10), # label에 string들어감
87 + cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2)
88 + cv2.rectangle(frame, (startX, startY), (endX, endY), color, 2)
89 + image = frame
90 + ###
91 + color_swapped_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
92 +
93 + qt_image1 = QtGui.QImage(color_swapped_image.data,
94 + self.width,
95 + self.height,
96 + color_swapped_image.strides[0],
97 + QtGui.QImage.Format_RGB888)
98 + self.VideoSignal1.emit(qt_image1)
99 +
100 + loop = QtCore.QEventLoop()
101 + QtCore.QTimer.singleShot(25, loop.quit) # 25 ms
102 + loop.exec_()
103 +
104 + @QtCore.pyqtSlot()
105 + def maskdetectionoff(self):
106 + self.flag_detect_mask = False
107 +
108 +
109 +class ImageViewer(QtWidgets.QWidget):
110 + def __init__(self, parent=None):
111 + super(ImageViewer, self).__init__(parent)
112 + self.image = QtGui.QImage()
113 + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
114 +
115 + def paintEvent(self, event):
116 + painter = QtGui.QPainter(self)
117 + painter.drawImage(0, 0, self.image)
118 + self.image = QtGui.QImage()
119 +
120 + def initUI(self):
121 + self.setWindowTitle('Webcam')
122 +
123 + @QtCore.pyqtSlot(QtGui.QImage)
124 + def setImage(self, image):
125 + if image.isNull():
126 + print("Viewer Dropped frame!")
127 +
128 + self.image = image
129 + if image.size() != self.size():
130 + self.setFixedSize(image.size())
131 + self.update()
132 +
133 +
134 +def detect_and_predict_mask(frame, faceNet, maskNet):
135 + # grab the dimensions of the frame and then construct a blob
136 + # from it
137 + (h, w) = frame.shape[:2]
138 + blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300),
139 + (104.0, 177.0, 123.0))
140 +
141 + # pass the blob through the network and obtain the face detections
142 + faceNet.setInput(blob)
143 + detections = faceNet.forward()
144 +
145 + # initialize our list of faces, their corresponding locations,
146 + # and the list of predictions from our face mask network
147 + faces = []
148 + locs = []
149 + preds = []
150 +
151 + # loop over the detections
152 + for i in range(0, detections.shape[2]):
153 + # extract the confidence (i.e., probability) associated with
154 + # the detection
155 + confidence = detections[0, 0, i, 2]
156 +
157 + # filter out weak detections by ensuring the confidence is
158 + # greater than the minimum confidence
159 + if confidence > args["confidence"]:
160 + # compute the (x, y)-coordinates of the bounding box for
161 + # the object
162 + box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
163 + (startX, startY, endX, endY) = box.astype("int")
164 +
165 + # ensure the bounding boxes fall within the dimensions of
166 + # the frame
167 + (startX, startY) = (max(0, startX), max(0, startY))
168 + (endX, endY) = (min(w - 1, endX), min(h - 1, endY))
169 +
170 + # extract the face ROI, convert it from BGR to RGB channel
171 + # ordering, resize it to 224x224, and preprocess it
172 + face = frame[startY:endY, startX:endX]
173 + face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
174 + face = cv2.resize(face, (224, 224))
175 + face = img_to_array(face)
176 + face = preprocess_input(face)
177 +
178 + # add the face and bounding boxes to their respective
179 + # lists
180 + faces.append(face)
181 + locs.append((startX, startY, endX, endY))
182 +
183 + # only make a predictions if at least one face was detected
184 + if len(faces) > 0:
185 + # for faster inference we'll make batch predictions on *all*
186 + # faces at the same time rather than one-by-one predictions
187 + # in the above `for` loop
188 + faces = np.array(faces, dtype="float32")
189 + preds = maskNet.predict(faces, batch_size=32)
190 +
191 + # return a 2-tuple of the face locations and their corresponding
192 + # locations
193 + return (locs, preds)
194 +
195 +
196 +class SpeakerRecognition(QtWidgets.QWidget):
197 + verification_url = URL.format('verification')
198 + identification_url = URL.format('identification')
199 + enrollment_url = URL.format('enroll')
200 + speaker_id = ''
201 +
202 + def __init__(self, parent=None):
203 + super(SpeakerRecognition, self).__init__(parent)
204 + self.initUI()
205 +
206 + def initUI(self):
207 + self.label_1_1 = QtWidgets.QLabel('Result Message: ', self)
208 + self.label_1_2 = QtWidgets.QLabel('', self)
209 + self.push_button5 =QtWidgets.QPushButton('Authenticate', self)
210 + self.push_button5.clicked.connect(self.doAction)
211 +
212 + self.dialog_button = QtWidgets.QPushButton('화자 ID 입력:', self)
213 + self.dialog_button.clicked.connect(self.showDialog)
214 + self.le = QtWidgets.QLineEdit(self)
215 +
216 + self.register_button = QtWidgets.QPushButton('Register new voice', self)
217 + self.register_button.clicked.connect(self.switch_enrollment)
218 +
219 + def verification(self, speaker):
220 + try:
221 + with open(WAVE_OUTPUT_FILENAME, 'rb') as file_opened:
222 + files = {'file': file_opened}
223 + data = {'enroll_speaker': speaker}
224 + r = requests.post(self.verification_url, files=files, data=data)
225 + print(r.text)
226 + return r.text
227 + except FileNotFoundError:
228 + return False
229 +
230 + def identification(self):
231 + try:
232 + with open(WAVE_OUTPUT_FILENAME, 'rb') as file_opened:
233 + files = {'file': file_opened}
234 + r = requests.post(self.identification_url, files=files)
235 + print(r.text)
236 + return r.text
237 + except FileNotFoundError:
238 + return False
239 +
240 + def recognition(self):
241 + speaker = self.identification()
242 + if speaker == False:
243 + print('Record voice first!')
244 + return False
245 +
246 + percentage = self.verification(speaker)
247 + print(speaker, percentage)
248 +
249 + if float(percentage) >= THRESHOLD:
250 + result = '승인! 등록된 화자입니다.(ID: {})'.format(speaker)
251 + #result = speaker
252 + else:
253 + result = '등록되지 않은 화자입니다!'
254 + return result
255 +
256 + @QtCore.pyqtSlot()
257 + def doAction(self):
258 + start = time.time()
259 + recog = self.recognition()
260 + print("Inference time : ", time.time() - start)
261 + if recog == False:
262 + self.label_1_2.setText('Voice not recorded, record voice first!')
263 + else:
264 + self.label_1_2.setText(recog)
265 +
266 + def enrollment(self, speaker_id):
267 + try:
268 + if speaker_id == '':
269 + return 0
270 + with open(WAVE_ENROLL_FILENAME, 'rb') as file_opened:
271 + files = {'file': file_opened}
272 + data = {'enroll_speaker': speaker_id}
273 + r = requests.post(self.enrollment_url, files=files, data=data)
274 + print(r.text)
275 + return r.text
276 + except FileNotFoundError:
277 + return 1
278 +
279 + def switch_enrollment(self):
280 + enroll = self.enrollment(self.speaker_id)
281 + if enroll == 1:
282 + self.label_1_2.setText('Voice not recorded, record voice first!')
283 + elif enroll == 0:
284 + self.label_1_2.setText('No speaker ID input!')
285 + else:
286 + self.label_1_2.setText("""New speaker registered!('%s')""" % self.speaker_id)
287 + self.speaker_id = ''
288 + self.le.setText(self.speaker_id)
289 +
290 + def showDialog(self):
291 + text, ok = QtWidgets.QInputDialog.getText(self, '화자 등록',
292 + '등록할 화자 ID(Unique 값)을 입력하십시오:')
293 + if ok:
294 + self.le.setText(str(text))
295 + self.speaker_id = str(text)
296 +
297 +class RecordAudio(QtCore.QObject):
298 + isrecording = False
299 + frames = []
300 +
301 + def __init__(self, parent=None):
302 + super(RecordAudio, self).__init__(parent)
303 +
304 + @QtCore.pyqtSlot()
305 + def startRecording(self):
306 + # start Recording
307 + self.audio = pyaudio.PyAudio()
308 + self.stream = self.audio.open(format=pyaudio.paInt16,
309 + channels=CHANNELS,
310 + rate=RATE,
311 + input=True,
312 + input_device_index=1, # 기기마다 마이크 인덱스 다름
313 + frames_per_buffer=CHUNK)
314 + self.isrecording = True
315 + print("recording...")
316 +
317 + # frames = []
318 + self.frames.clear()
319 +
320 + for i in range(0, int(RATE / CHUNK * MAX_RECORD_SECONDS)):
321 + QtWidgets.QApplication.processEvents()
322 + if self.isrecording:
323 + data = self.stream.read(CHUNK)
324 + self.frames.append(data)
325 + else:
326 + print("Stopped recording")
327 + break
328 + print("finished recording")
329 +
330 + # stop Recording
331 + self.stream.stop_stream()
332 + self.stream.close()
333 + self.audio.terminate()
334 + waveFile = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
335 + waveFile.setnchannels(CHANNELS)
336 + waveFile.setsampwidth(self.audio.get_sample_size(FORMAT))
337 + waveFile.setframerate(RATE)
338 + waveFile.writeframes(b''.join(self.frames))
339 + waveFile.close()
340 + self.frames.clear()
341 +
342 + def stopRecording(self):
343 + print("stop called")
344 + self.isrecording = False
345 +
346 + def switch(self):
347 + if self.isrecording:
348 + QtTest.QTest.qWait(1 * 1000)
349 + self.stopRecording()
350 + else:
351 + self.startRecording()
352 +
353 +class RecordAudio_enroll(QtCore.QObject):
354 + isrecording = False
355 + frames = []
356 +
357 + def __init__(self, parent=None):
358 + super(RecordAudio_enroll, self).__init__(parent)
359 +
360 + @QtCore.pyqtSlot()
361 + def startRecording(self):
362 + # start Recording
363 + self.audio = pyaudio.PyAudio()
364 + self.stream = self.audio.open(format=pyaudio.paInt16,
365 + channels=CHANNELS,
366 + rate=RATE,
367 + input=True,
368 + input_device_index=1, # 기기마다 마이크 인덱스 다름
369 + frames_per_buffer=CHUNK)
370 + self.isrecording = True
371 + print("recording...")
372 +
373 + # frames = []
374 + self.frames.clear()
375 +
376 + for i in range(0, int(RATE / CHUNK * MAX_RECORD_SECONDS)):
377 + QtWidgets.QApplication.processEvents()
378 + if self.isrecording:
379 + data = self.stream.read(CHUNK)
380 + self.frames.append(data)
381 + else:
382 + print("Stopped recording")
383 + break
384 + print("finished recording")
385 +
386 + # stop Recording
387 + self.stream.stop_stream()
388 + self.stream.close()
389 + self.audio.terminate()
390 + waveFile = wave.open(WAVE_ENROLL_FILENAME, 'wb')
391 + waveFile.setnchannels(CHANNELS)
392 + waveFile.setsampwidth(self.audio.get_sample_size(FORMAT))
393 + waveFile.setframerate(RATE)
394 + waveFile.writeframes(b''.join(self.frames))
395 + waveFile.close()
396 + self.frames.clear()
397 +
398 + def stopRecording(self):
399 + print("stop called")
400 + self.isrecording = False
401 +
402 + def switch(self):
403 + if self.isrecording:
404 + QtTest.QTest.qWait(1 * 1000)
405 + self.stopRecording()
406 + else:
407 + self.startRecording()
408 +
409 +class RecordViewer(QtWidgets.QWidget):
410 + def __init__(self, parent=None):
411 + super(RecordViewer, self).__init__(parent)
412 + self.initUI()
413 +
414 + def initUI(self):
415 + self.pbar = QtWidgets.QProgressBar(self)
416 + self.pbar.setFixedWidth(400)
417 + self.pbar.setMaximum(MAX_RECORD_SECONDS)
418 + self.pbar.setAlignment(QtCore.Qt.AlignCenter)
419 +
420 + self.push_button3 = QtWidgets.QPushButton('Start Audio Record', self)
421 + self.push_button3.clicked.connect(self.doAction)
422 +
423 + self.timer = QtCore.QBasicTimer()
424 + self.step = 0
425 +
426 + def timerEvent(self, e):
427 + if self.step >= MAX_RECORD_SECONDS:
428 + self.timer.stop()
429 + self.push_button3.setText("Restart")
430 + return
431 + self.step = self.step + 1
432 + self.pbar.setValue(self.step)
433 + self.pbar.setFormat("%d sec" % self.step)
434 +
435 + @QtCore.pyqtSlot()
436 + def doAction(self):
437 + if self.timer.isActive():
438 + self.timer.stop()
439 + self.push_button3.setText("Restart")
440 + else:
441 + self.pbar.reset()
442 + self.step = 0
443 + self.timer.start(1000, self) # 1000/1000초마다 timer실행
444 + self.push_button3.setText("Stop")
445 +
446 +
447 +if __name__ == '__main__':
448 + # construct the argument parser and parse the arguments
449 + ap = argparse.ArgumentParser()
450 + ap.add_argument("-f", "--face", type=str, default="face_detector",
451 + help="path to face detector model directory")
452 + ap.add_argument("-m", "--model", type=str, default="mask_detector.model",
453 + help="path to trained face mask detector model")
454 + ap.add_argument("-c", "--confidence", type=float, default=0.5,
455 + help="minimum probability to filter weak detections")
456 + args = vars(ap.parse_args())
457 +
458 + # load our serialized face detector model from disk
459 + print("[INFO] loading face detector model...")
460 + prototxtPath = os.path.sep.join([args["face"], "deploy.prototxt"])
461 + weightsPath = os.path.sep.join([args["face"],
462 + "res10_300x300_ssd_iter_140000.caffemodel"])
463 + faceNet = cv2.dnn.readNet(prototxtPath, weightsPath)
464 +
465 + # load the face mask detector model from disk
466 + print("[INFO] loading face mask detector model...")
467 + maskNet = load_model(args["model"])
468 +
469 + app = QtWidgets.QApplication(sys.argv) # app 생성
470 +
471 + thread = QtCore.QThread()
472 + thread.start()
473 + vid = ShowVideo()
474 + vid.moveToThread(thread)
475 +
476 + thread2 = QtCore.QThread()
477 + thread2.start()
478 + aud = RecordViewer()
479 + aud.moveToThread(thread2)
480 +
481 + thread3 = QtCore.QThread()
482 + thread3.start()
483 + mic = RecordAudio_enroll()
484 + mic.moveToThread(thread3)
485 +
486 + thread4 = QtCore.QThread()
487 + thread4.start()
488 + sr = SpeakerRecognition()
489 + sr.moveToThread(thread4)
490 +
491 + thread5 = QtCore.QThread()
492 + thread5.start()
493 + aud2 = RecordViewer()
494 + aud2.moveToThread(thread5)
495 +
496 + thread6 = QtCore.QThread()
497 + thread6.start()
498 + mic2 = RecordAudio()
499 + mic2.moveToThread(thread6)
500 +
501 + thread7 = QtCore.QThread()
502 + thread7.start()
503 + sr2 = SpeakerRecognition()
504 + sr2.moveToThread(thread7)
505 +
506 +
507 + image_viewer1 = ImageViewer()
508 +
509 + vid.VideoSignal1.connect(image_viewer1.setImage)
510 +
511 + push_button1 = QtWidgets.QPushButton('Start Mask Detection')
512 + push_button2 = QtWidgets.QPushButton('Mask Detection Off')
513 + push_button4 = QtWidgets.QPushButton('Close')
514 +
515 + push_button1.clicked.connect(lambda: vid.startVideo(faceNet, maskNet))
516 + push_button2.clicked.connect(vid.maskdetectionoff)
517 + aud.push_button3.clicked.connect(mic.switch)
518 + push_button4.clicked.connect(sys.exit)
519 + aud2.push_button3.clicked.connect(mic2.switch)
520 +
521 + empty_label = QtWidgets.QLabel()
522 + empty_label.setText('')
523 +
524 + L_groupBox = QtWidgets.QGroupBox("Mask Detection")
525 + LR_layout = QtWidgets.QVBoxLayout()
526 + LR_layout.addWidget(push_button1)
527 + LR_layout.addWidget(push_button2)
528 + LR_layout.addStretch(1)
529 +
530 + L_horizontal_layout1 = QtWidgets.QHBoxLayout()
531 + L_horizontal_layout1.addWidget(image_viewer1)
532 + L_horizontal_layout1.addLayout(LR_layout)
533 + L_groupBox.setLayout(L_horizontal_layout1)
534 +
535 + RU_groupBox = QtWidgets.QGroupBox("Speaker Registration")
536 + pbar_layout = QtWidgets.QHBoxLayout()
537 + pbar_layout.addWidget(aud.pbar)
538 + pbar_layout.addStretch(1)
539 + ##
540 + dialog_layout = QtWidgets.QHBoxLayout()
541 + dialog_layout.addWidget(sr2.dialog_button)
542 + dialog_layout.addWidget(sr2.le)
543 + dialog_layout.addStretch(1)
544 +
545 + register_layout = QtWidgets.QHBoxLayout()
546 + register_layout.addWidget(sr2.register_button)
547 +
548 + result_1_layout = QtWidgets.QHBoxLayout()
549 + result_1_layout.addWidget(sr2.label_1_1)
550 + result_1_layout.addWidget(sr2.label_1_2)
551 + result_1_layout.addStretch(1)
552 + ##
553 + RL_label1 = QtWidgets.QLabel()
554 + RL_label1.setText("Max Record Time: 30 sec")
555 + RL_label2 = QtWidgets.QLabel()
556 + RL_label2.setText("Press Start/Restart to begin recording")
557 +
558 + RL_layout = QtWidgets.QVBoxLayout()
559 + RL_layout.addLayout(pbar_layout)
560 + RL_layout.addWidget(RL_label1)
561 + RL_layout.addWidget(RL_label2)
562 + RL_layout.addLayout(dialog_layout)
563 + RL_layout.addLayout(result_1_layout)
564 + RL_layout.addStretch(1)
565 +
566 + push_button3_layout = QtWidgets.QHBoxLayout()
567 + push_button3_layout.addWidget(aud.push_button3)
568 + # push_button3_layout.addStretch(1)
569 +
570 + # close_layout = QtWidgets.QHBoxLayout()
571 + # close_layout.addWidget(push_button4)
572 +
573 + RR_layout = QtWidgets.QVBoxLayout()
574 + RR_layout.addLayout(push_button3_layout)
575 + RR_layout.addWidget(empty_label)
576 + RR_layout.addWidget(empty_label)
577 + RR_layout.addLayout(register_layout)
578 + RR_layout.addStretch(1)
579 + # RR_layout.addLayout(close_layout)
580 +
581 + R_horizontal_layout2 = QtWidgets.QHBoxLayout()
582 + R_horizontal_layout2.addLayout(RL_layout)
583 + R_horizontal_layout2.addLayout(RR_layout)
584 + RU_groupBox.setLayout(R_horizontal_layout2)
585 +
586 +
587 + RD_groupBox = QtWidgets.QGroupBox("Speaker Recognition")
588 +###
589 + pbar_2_layout = QtWidgets.QHBoxLayout()
590 + pbar_2_layout.addWidget(aud2.pbar)
591 + pbar_2_layout.addStretch(1)
592 +
593 + RDL_label1 = QtWidgets.QLabel()
594 + RDL_label1.setText("Max Record Time: 30 sec")
595 + RDL_label2 = QtWidgets.QLabel()
596 + RDL_label2.setText("Press Start/Restart to begin recording")
597 +
598 + push_button3_2_layout = QtWidgets.QHBoxLayout()
599 + push_button3_2_layout.addWidget(aud2.push_button3)
600 +###
601 + result_2_layout = QtWidgets.QHBoxLayout()
602 + result_2_layout.addWidget(sr.label_1_1)
603 + result_2_layout.addWidget(sr.label_1_2)
604 + result_2_layout.addStretch(1)
605 +
606 + RDL_layout = QtWidgets.QVBoxLayout()
607 + RDL_layout.addLayout(pbar_2_layout)
608 + RDL_layout.addWidget(RDL_label1)
609 + RDL_layout.addWidget(RDL_label2)
610 + RDL_layout.addWidget(empty_label)
611 + RDL_layout.addLayout(result_2_layout)
612 + RDL_layout.addStretch(1)
613 +
614 + push_button5_layout = QtWidgets.QHBoxLayout()
615 + push_button5_layout.addWidget(sr.push_button5)
616 +
617 + close_layout = QtWidgets.QHBoxLayout()
618 + close_layout.addWidget(push_button4)
619 +
620 + RDR_layout = QtWidgets.QVBoxLayout()
621 + RDR_layout.addLayout(push_button3_2_layout)
622 + RDR_layout.addWidget(empty_label)
623 + RDR_layout.addWidget(empty_label)
624 + RDR_layout.addWidget(empty_label)
625 + RDR_layout.addLayout(push_button5_layout)
626 + RDR_layout.addStretch(1)
627 + RDR_layout.addLayout(close_layout)
628 +
629 + RD_horizontal_layout = QtWidgets.QHBoxLayout()
630 + RD_horizontal_layout.addLayout(RDL_layout)
631 + RD_horizontal_layout.addLayout(RDR_layout)
632 + RD_groupBox.setLayout(RD_horizontal_layout)
633 +
634 + R_layout = QtWidgets.QVBoxLayout()
635 + R_layout.addWidget(RU_groupBox)
636 + R_layout.addWidget(RD_groupBox)
637 +
638 + layout = QtWidgets.QHBoxLayout()
639 + layout.addWidget(L_groupBox)
640 + layout.addLayout(R_layout)
641 +
642 + layout_widget = QtWidgets.QWidget()
643 + layout_widget.setLayout(layout)
644 +
645 + main_window = QtWidgets.QMainWindow()
646 + main_window.setGeometry(150, 150, 500, 500) # test
647 + main_window.setCentralWidget(layout_widget)
648 + main_window.setWindowTitle('마스크 디텍션 및 화자 식별을 통한 입출입 시스템') # main window 제목
649 + main_window.show()
650 + sys.exit(app.exec_()) # 프로그램 대기상태 유지, 무한루프