MainWindow.py 11 KB

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