MainWindow.py 11 KB

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