6
0

MainWindow.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import json
  2. import os
  3. from PyQt5 import uic, QtWidgets, QtCore, QtGui
  4. from . import AboutDialog
  5. from .LabelDialog import LabelDialog
  6. from .NewProjectWizard import NewProjectWizard
  7. from ..project import Project
  8. from ..utils import Video
  9. class MainWindow:
  10. # Properties
  11. def _set_project(self, project):
  12. self.__project = project
  13. if project is not None:
  14. project._update_ui_fn = lambda: self._update_ui()
  15. self._update_ui()
  16. def _get_project(self):
  17. return self.__project
  18. _project = property(fget=_get_project, fset=_set_project)
  19. def _has_project(self):
  20. if self.__project is not None:
  21. return True
  22. else:
  23. return False
  24. def _set_predictions(self, predictions):
  25. self.__predictions = predictions
  26. self._update_prediction_ui()
  27. def _get_predictions(self):
  28. return self.__predictions
  29. _predictions = property(fget=_get_predictions, fset=_set_predictions)
  30. def _project_needs_saving(self):
  31. return True
  32. # Constructor
  33. def __init__(self, **kwargs):
  34. # initialize window
  35. self.ui = QtWidgets.QMainWindow(**kwargs)
  36. # set object properties
  37. self.__project = None
  38. self.__predictions = []
  39. # load ui from corresponding file
  40. spath = os.path.dirname(__file__)
  41. uic.loadUi(os.path.join(spath, 'MainWindow.ui'), self.ui)
  42. # prepare some more ui stuff
  43. self.ui.statusLabel = QtWidgets.QLabel(self.ui)
  44. self.ui.statusBar.addPermanentWidget(self.ui.statusLabel)
  45. self.ui.timerThread = QtCore.QThread(self.ui)
  46. self.ui.timer = QtCore.QTimer()
  47. self.ui.timer.setInterval(2000)
  48. self.ui.timer.moveToThread(self.ui.timerThread)
  49. # connect actions
  50. # File
  51. self.ui.actionNew.triggered.connect(self._project_new)
  52. self.ui.actionOpen.triggered.connect(self.project_open)
  53. self.ui.actionClose.triggered.connect(self._project_try_closing)
  54. self.ui.actionSave.triggered.connect(self._project_save)
  55. self.ui.actionQuit.triggered.connect(self._file_quit)
  56. # Labeling
  57. self.ui.actionStart_Labeling_Session.triggered.connect(self._start_labeling_session)
  58. self.ui.startLabelingSessionButton.clicked.connect(self._start_labeling_session)
  59. # Help
  60. self.ui.actionAbout.triggered.connect(self._help_about)
  61. self.ui.actionPredict_Images.triggered.connect(self.predict_via_dialog)
  62. self.ui.predictButton.clicked.connect(self.predict_via_dialog)
  63. self.ui.imageThumbnailGallery.currentRowChanged.connect(self._prediction_row_changed)
  64. self._update_ui()
  65. self._update_prediction_ui()
  66. # TODO delete me
  67. # self._project = Project('test-project')
  68. def _update_ui(self):
  69. # Status text
  70. if self._has_project():
  71. status_text = "OK"
  72. if self._project.last_error is not None:
  73. status_text = self._project.last_error
  74. else:
  75. status_text = "No project loaded."
  76. self.ui.pipelineStatusLabel.setText(status_text)
  77. # Action availability
  78. project_only_actions = [self.ui.actionSave,
  79. self.ui.actionSave_As,
  80. self.ui.actionClose,
  81. self.ui.actionImport_Labeled_Faces,
  82. self.ui.actionImport_Labeled_Images,
  83. self.ui.startLabelingSessionButton,
  84. self.ui.actionStart_Labeling_Session,
  85. self.ui.actionManage_Individuals,
  86. self.ui.actionPredict_Images,
  87. self.ui.predictButton,
  88. self.ui.actionUpdate_Model,
  89. self.ui.updateButton,
  90. self.ui.actionValidate_Model,
  91. self.ui.clearPredictionsButton,
  92. self.ui.exportCSVButton]
  93. for action in project_only_actions:
  94. action.setEnabled(self._has_project())
  95. def show(self):
  96. return self.ui.show()
  97. #####################
  98. # Project lifecycle #
  99. #####################
  100. def _project_try_closing(self):
  101. if self._has_project():
  102. if self._project_needs_saving():
  103. # Ask user
  104. box = QtWidgets.QMessageBox()
  105. box.setText('The current project has unsaved changes. Would you like to save them?')
  106. box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel)
  107. retval = box.exec_()
  108. if retval == QtWidgets.QMessageBox.Yes:
  109. self._project.save()
  110. if self._project.last_error is not None:
  111. error_string = self._project.last_error
  112. QtWidgets.QMessageBox.warning(self.ui, 'Error', 'Error while saving (see log for details): %s'
  113. % error_string)
  114. self._project.reset_error()
  115. return False
  116. elif retval == QtWidgets.QMessageBox.No:
  117. pass
  118. elif retval == QtWidgets.QMessageBox.Cancel:
  119. return False
  120. self._project.close()
  121. self._project = None
  122. return True
  123. else:
  124. return True
  125. def _project_new(self):
  126. retval = self._project_try_closing()
  127. if retval:
  128. w = NewProjectWizard(self.ui)
  129. w_retval = w.exec_()
  130. if w_retval:
  131. self._project = Project(w.project_root, w.project_config)
  132. def project_open(self, path=False):
  133. """
  134. opens a project by either using the optional parameter path or
  135. or the value the user enters using the ui file dialog
  136. :param path: path to project.json
  137. """
  138. # check if there is an open project with unsaved changes
  139. retval = self._project_try_closing()
  140. if not retval:
  141. return
  142. # copy path to selection or question user if not given
  143. if path is not False:
  144. selection = path
  145. else:
  146. selection, _ = QtWidgets.QFileDialog.getOpenFileName(self.ui, 'Select Project Folder', filter='project.json')
  147. # check if path exists and open project
  148. if len(selection) > 0 and os.path.exists(selection):
  149. self._project = Project(selection)
  150. if self._project.last_error is not None:
  151. error_string = self._project.last_error
  152. try:
  153. QtWidgets.QMessageBox.warning(self.ui, 'Error', 'Error while opening (see log for details): %s'
  154. % error_string)
  155. self._project.close()
  156. except:
  157. pass
  158. self._project = None
  159. elif len(selection) > 0: # Project file does not exist
  160. QtWidgets.QMessageBox.warning(self.ui, 'Error', 'Could not find project file: %s' % selection)
  161. def _project_save(self):
  162. self._project.save()
  163. if self._project.last_error is not None:
  164. error_string = self._project.last_error
  165. QtWidgets.QMessageBox.warning(self.ui, 'Error', 'Error while saving (see log for details): %s'
  166. % error_string)
  167. self._project.reset_error()
  168. def _file_quit(self):
  169. # TODO: warning if there are any unsaved changes
  170. self.close()
  171. def _start_labeling_session(self):
  172. labeling = LabelDialog(self._predictions)
  173. labeling.exec_()
  174. def _help_about(self):
  175. about = AboutDialog()
  176. about.exec_()
  177. #####################################
  178. # Prediction actions and management #
  179. #####################################
  180. def predict_via_dialog(self, selection=False):
  181. """
  182. loads images from given paths or ui file dialog
  183. :param selection: array of image paths
  184. :return:
  185. """
  186. # valid image and video extensions
  187. image_extensions = ['.jpg', '.png', '.jpeg']
  188. video_extensions = ['.mp4', '.avi', '.mkv', '.mpg', '.mpeg']
  189. # ask user for input
  190. if selection is False:
  191. name_list = ' '.join(['*' + extension for extension in image_extensions + video_extensions])
  192. extensions = f'All supported files ({name_list})'
  193. selection, _ = QtWidgets.QFileDialog.getOpenFileNames(self.ui, 'Select Files To Add', filter=extensions)
  194. # load jobs
  195. jobs = []
  196. for filename in selection:
  197. _, extension = os.path.splitext(filename)
  198. extension = extension.lower()
  199. if extension in image_extensions:
  200. jobs.append({
  201. 'filename': filename,
  202. 'filetype': 'image',
  203. 'jobs': ['detect-faces']
  204. })
  205. elif extension in video_extensions:
  206. jobs.append({
  207. 'filename': filename,
  208. 'filetype': 'image',
  209. 'jobs': ['detect-faces']
  210. })
  211. progress_dialog = QtWidgets.QProgressDialog('Executing prediction jobs...', 'Abort', 0, 1000, self.ui)
  212. progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
  213. def callback(progress):
  214. progress_dialog.setValue(int(progress * 1000))
  215. for i in range(10):
  216. QtWidgets.QApplication.processEvents()
  217. self._project.execute(jobs, callback=callback)
  218. progress_dialog.close()
  219. self._predictions += jobs
  220. def _update_prediction_ui(self):
  221. # TODO implement better sync
  222. self.ui.imageThumbnailGallery.clear()
  223. for prediction in self._predictions:
  224. filename = prediction['filename']
  225. if prediction['filetype'] == 'image':
  226. icon = QtGui.QIcon(filename)
  227. item = QtWidgets.QListWidgetItem(icon, os.path.basename(filename))
  228. else:
  229. item = QtWidgets.QListWidgetItem(os.path.basename(filename))
  230. self.ui.imageThumbnailGallery.addItem(item)
  231. self._prediction_row_changed(self.ui.imageThumbnailGallery.currentRow())
  232. if len(self._predictions) == 0:
  233. self._prediction_row_changed(-1)
  234. def _prediction_row_changed(self, row):
  235. if row in range(len(self._predictions)):
  236. prediction = self._predictions[row]
  237. self._setup_video_slider(prediction)
  238. self._display_prediction(prediction)
  239. else:
  240. self._setup_video_slider({'filetype': 'empty'})
  241. self._display_prediction({'filetype': 'empty'})
  242. def _setup_video_slider(self, prediction):
  243. if prediction['filetype'] == 'video':
  244. video_cap = Video(prediction['filename'])
  245. if video_cap.last_error is None:
  246. self.ui.frameNumber.setDecMode()
  247. self.ui.frameNumber.display(0)
  248. self.ui.frameNumber.setDigitCount(5)
  249. self.ui.frameNumber.show()
  250. self.ui.frameSlider.show()
  251. self.ui.frameSlider.setValue(0)
  252. self.ui.frameSlider.setMinimum(0)
  253. self.ui.frameSlider.setMaximum(video_cap.framecount - 1)
  254. self.ui.frameSlider.valueChanged.connect(
  255. lambda value, _prediction=prediction:
  256. [self._display_prediction(dict(frame=value, cap=video_cap, **_prediction)),
  257. self.ui.frameNumber.display(value)])
  258. else:
  259. self._disable_video_slider()
  260. else:
  261. self._disable_video_slider()
  262. def _disable_video_slider(self):
  263. self.ui.frameSlider.setValue(0)
  264. try:
  265. self.ui.frameSlider.valueChanged.disconnect()
  266. except:
  267. pass
  268. self.ui.frameSlider.hide()
  269. self.ui.frameNumber.hide()
  270. def _display_prediction(self, prediction):
  271. self.ui.annotatedImageView.display(prediction)
  272. filtered_dict = {i: prediction[i] for i in prediction if i != 'cap'}
  273. if 'cap' in prediction.keys():
  274. filtered_dict['cap'] = '[VideoCapture]'
  275. self.ui.predictionDebugText.setPlainText(json.dumps(filtered_dict, indent=2, skipkeys=True))