main_shell.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import copy
  2. from iltis.Main import Main as ILTISMain
  3. from iltis.io.IOtools import save_tstack
  4. from iltis.Objects.Data_Object import Data_Object, Metadata_Object
  5. from iltis.Widgets.Options_Control_Widget import SingleValueWidget
  6. from iltis.Objects.ROIs_Object import myPolyLineROI, myCircleROI
  7. from PyQt5.QtCore import pyqtSlot
  8. from PyQt5.QtGui import QFont
  9. from PyQt5.QtWidgets import QLabel, QMessageBox
  10. import numpy as np
  11. import pandas as pd
  12. from .save_area_file_dialog import SaveAreaFileDialog, SaveCircleROIsFileDialog, SaveAllROIsFileDialog
  13. from .orphan_functions import convert_iltisROI2VIEWROI
  14. from view.python_core.rois.roi_io import ILTISTextROIFileIO
  15. import logging
  16. import pathlib as pl
  17. class ILTISMainShell(ILTISMain):
  18. def __init__(self, verbose=False):
  19. super().__init__(verbose)
  20. menu_bar = self.MainWindow.menuBar()
  21. # otherwise, Qt will try to integrate into the native menubar on MAC, which causes issues
  22. menu_bar.setNativeMenuBar(False)
  23. view_menu = menu_bar.addMenu("&VIEW-Related")
  24. self.import_action_quick = view_menu.addAction("Quickly import all data from VIEW")
  25. self.import_action = view_menu.addAction("Selectively import data from VIEW")
  26. self.save_cirle_rois_action = view_menu.addAction("Save circle ROIS as .roi for VIEW")
  27. self.save_cirle_rois_action.triggered.connect(self.spawn_save_circle_rois_dialog)
  28. self.save_cirle_rois_action.setEnabled(False)
  29. self.save_rois_action = view_menu.addAction("Save all ROI types as .roi for VIEW")
  30. self.save_rois_action.triggered.connect(self.spawn_save_all_rois_dialog)
  31. self.save_rois_action.setEnabled(False)
  32. self.save_area_action = view_menu.addAction("Save Polygon ROIs as AREA for VIEW")
  33. self.save_area_action.triggered.connect(self.spawn_save_area_dialog)
  34. self.save_area_action.setEnabled(False)
  35. self.quick_save_area_action = view_menu.addAction("Quick save AREA for VIEW (using selected data and ROIs)")
  36. self.quick_save_area_action.triggered.connect(self.quick_save_area)
  37. self.quick_save_area_action.setEnabled(False)
  38. self.dialogs = []
  39. self.metadata = None
  40. # this is used to reset ILTIs from VIEW if required
  41. @pyqtSlot(name="reset")
  42. def reset(self, restore_options=True):
  43. # save some options for restoration
  44. # some options in the tab "Preprocessing" change the data, hence are not saved and restored
  45. # when loading new data
  46. roi_options_to_save = ["view", "ROI", "export"]
  47. try:
  48. roi_options = {x: copy.copy(getattr(self.Options, x)) for x in roi_options_to_save}
  49. # if the Options object is initialized but default options have not yet been loaded
  50. except AttributeError as ae:
  51. roi_options = {}
  52. # clear ILTIS data and history
  53. self.ROIs.reset()
  54. self.Signals.resetSignal.emit()
  55. self.Options.__init__(self)
  56. if restore_options:
  57. # restore options; placed here because (1) load_default_options() uses Data.nFrames and Metadata.paths
  58. # (2) since load_default_options() need to be called before initing Option_Control (see below)
  59. [setattr(self.Options, k, v) for k, v in roi_options.items()]
  60. try:
  61. self.MainWindow.roi_type_widget.layout().itemAt(1).widget().set_value(roi_options["ROI"]["type"])
  62. except KeyError as ke:
  63. pass
  64. # parts of iltis.Objects.IO_Object.IO_Object.load_data
  65. # Data object creation
  66. self.Data = Data_Object()
  67. self.Data.Metadata = Metadata_Object(self.Data)
  68. self.metadata = None
  69. self.MainWindow.ToolBar.setEnabled(False)
  70. # disable some actions from menubar
  71. self.save_cirle_rois_action.setEnabled(False)
  72. self.save_rois_action.setEnabled(False)
  73. self.save_area_action.setEnabled(False)
  74. self.quick_save_area_action.setEnabled(False)
  75. self.MainWindow.roi_type_widget.setEnabled(False)
  76. return roi_options
  77. @pyqtSlot(list, list, pd.DataFrame, int, tuple, tuple, int, name="import data")
  78. def import_data(self, raw_data_list, signal_list, metadata, n_frames, stim_onset, stim_offset, default_radius):
  79. """
  80. Writes raw data, (df/f) signal data and trials names into the data structures iltis.
  81. In principle replicates iltis.Objects.IO_Object.IO_Object.init_data for writing raw data
  82. and initializing assoicated UI. Then writes df/f signal and calls
  83. iltis.Widgets.MainWindow_Widget.MainWindow_Widget.toggle_dFF
  84. :param raw_data_list: iterable of numpy.ndarrays of dimension 3
  85. :param signal_list: iterable of numpy.ndarray of dimension 3, same size as raw_data_list
  86. :param metadata: pandas.Dataframe, indices are unique label for each measurement, columns are
  87. metadata, must have the columns "Label to use", the entries of which will be used as data labels in ILTIS.
  88. Columns must also contain flags of the subgroup "paths". Must contain a column "Raw Data File"
  89. which contains the full paths of the raw data files
  90. :param n_frames: int, maximium number of frames among all raw data in raw_data_list
  91. :param stim_onset: list, of stimulus onsets as frame numbers
  92. :param stim_offset: list, of stimulus offsets as frame numbers
  93. :param default_radius: int, default radius of ROIs
  94. """
  95. assert len(raw_data_list) == len(signal_list) == metadata.shape[0]
  96. assert "Label to use" in metadata.columns
  97. assert all(type(x) is np.ndarray for x in raw_data_list)
  98. assert all(len(x.shape) == 3 for x in raw_data_list)
  99. assert all(type(x) is np.ndarray for x in signal_list)
  100. assert all(len(x.shape) == 3 for x in signal_list)
  101. for raw_data, sig_data in zip(raw_data_list[1:], signal_list[1:]):
  102. if not (raw_data.shape == raw_data_list[0].shape == sig_data.shape == signal_list[0].shape):
  103. QMessageBox.critical(
  104. self.MainWindow, "Error importing data",
  105. "Data being imported have different sizes (X, Y and/or Z). Please have a look at the columns"
  106. "'No. of pixels along X', 'No. of pixels along Y' and 'No. of frames' of the table shown "
  107. "when choosing data for import"
  108. )
  109. return
  110. self.write_status("[working] Importing data from VIEW to ILTIS")
  111. old_roi_options = self.reset(restore_options=False)
  112. self.metadata = metadata
  113. n_trials = len(raw_data_list)
  114. # set raw data,
  115. # view.idl_translation_core.ViewLoadData.load_pst
  116. self.Data.raw = np.concatenate([x[:, :, :, np.newaxis] for x in raw_data_list], axis=3)
  117. # dFF needs to be a float to avoid problems with pyqtgraph, see Issue 56 of VIEW
  118. self.Data.dFF = np.zeros_like(self.Data.raw, dtype="float32")
  119. # set some metadata
  120. self.Data.Metadata.paths = metadata["Raw File Name"].values
  121. self.Data.nFrames = raw_data_list
  122. # set inferred data, in principle replicates iltis.Objects.Data_Object.Data_Object.infer
  123. self.Data.nTrials = n_trials
  124. self.Data.nFrames = n_frames
  125. # instead of file names, set trial labels directly
  126. self.Data.Metadata.trial_labels = metadata["Label to use"].values.tolist()
  127. # restore options; placed here because (1) load_default_options() uses Data.nFrames and Metadata.paths
  128. # (2) since load_default_options() need to be called before initing Option_Control (see below)
  129. self.Options.load_default_options()
  130. [setattr(self.Options, k, v) for k, v in old_roi_options.items() if len(v)]
  131. try:
  132. getattr(self.Options, "ROI")["diameter"] = 2 * default_radius + 1
  133. self.MainWindow.roi_type_widget.layout().itemAt(1).widget().set_value(old_roi_options["ROI"]["type"])
  134. except KeyError as ke:
  135. pass
  136. # set cwd to IDLOutput
  137. random_metadata = metadata.iloc[0]
  138. op_dir_first_dataset = random_metadata["STG_OdorReportPath"]
  139. self.cwd = op_dir_first_dataset
  140. self.Options.general['cwd'] = op_dir_first_dataset
  141. # set data_path and roi_path
  142. self.data_path = random_metadata["STG_Datapath"]
  143. self.roi_path = random_metadata["STG_OdormaskPath"]
  144. # set stimuli parameters
  145. self.add_stim_options(stim_onset, stim_offset)
  146. # initialize and update GUI elements
  147. self.MainWindow.Options_Control.init_UI()
  148. self.Signals.initDataSignal.emit()
  149. self.Signals.updateSignal.emit()
  150. # replace nans if any in signals with lowest value of signal
  151. for x in signal_list:
  152. x[np.isnan(x)] = np.nanmin(x)
  153. # set dFF
  154. self.Data.dFF = np.concatenate([x[:, :, :, np.newaxis] for x in signal_list], axis=3)
  155. self.Options.flags["dFF_was_calc"] = True
  156. # - for all display triggers, whatever the state, set it to True and toggle it.
  157. self.MainWindow.ToolBar.setEnabled(True)
  158. # -- list of tuples to store flag name and trigger
  159. flag_action_names = [
  160. ('show_dFF', 'toggledFFAction'),
  161. ('use_global_levels', 'toggleGlobalLevels'),
  162. ('show_avg', 'toggleAvgAction'),
  163. ('show_monochrome', 'toggleMonochromeAction')
  164. ]
  165. for flag_name, action_name in flag_action_names:
  166. self.Options.view[flag_name] = True
  167. action = getattr(self.MainWindow, action_name)
  168. action.setChecked(True)
  169. action.trigger()
  170. # reset levels of dFF signal once set
  171. self.Data_Display.LUT_Controlers.reset_levels(which="dFF")
  172. # enable all mouse based interactions in the Data_Display_Widget
  173. self.MainWindow.Data_Display.enable_interaction()
  174. # add a warning about signal calculation in ILTIS/transfer from VIEW
  175. qfont = QFont()
  176. qfont.setBold(True)
  177. qlabel = QLabel("Warning: The following options will only be used when data is loaded using Open->load data.\n"
  178. "They are not used when data is imported from VIEW using VIEW-Related->Import data from VIEW.\n"
  179. "In this case, deltaF/F is not calculated in ILTIS, but initialized with the signal data "
  180. "calculated in VIEW.")
  181. qlabel.setFont(qfont)
  182. fake_field = SingleValueWidget(parent=self.MainWindow.Options_Control, dict_name="preprocessing",
  183. param_name="fake", dtype='S')
  184. fake_field.setText("Please take care!")
  185. fake_field.setReadOnly(True)
  186. self.MainWindow.Options_Control.widget(1).layout().insertRow(1, qlabel, fake_field)
  187. # enable other VIEW-related actions
  188. self.save_area_action.setEnabled(True)
  189. self.save_rois_action.setEnabled(True)
  190. self.save_cirle_rois_action.setEnabled(True)
  191. self.quick_save_area_action.setEnabled(True)
  192. self.MainWindow.roi_type_widget.setEnabled(True)
  193. # done, write status and return
  194. self.write_status("[success] Importing data from VIEW to ILTIS")
  195. def write_status(self, msg):
  196. self.MainWindow.statusBar().showMessage(msg)
  197. logging.info(msg)
  198. def add_stim_options(self, stim_onset, stim_offset):
  199. if len(stim_onset) != len(stim_offset) or len(stim_onset) == 0:
  200. return
  201. else:
  202. stim_onset_offsets = [x for x in zip(stim_onset, stim_offset) if x[0] is not None and x[1] is not None]
  203. stim_times = np.array(stim_onset_offsets, dtype=float)
  204. self.Options.preprocessing["nStimuli"] = len(stim_onset_offsets)
  205. self.Options.preprocessing["stimuli"] = stim_times
  206. def get_roi_and_selected_by_type(self, roi_types=None):
  207. if roi_types is None:
  208. roi_types = []
  209. roi_labels = []
  210. roi_labels_selected = []
  211. for roi in self.ROIs.ROI_list:
  212. if type(roi) in roi_types or roi_types == []:
  213. roi_labels.append(roi.label)
  214. if roi.active:
  215. roi_labels_selected.append(roi.label)
  216. return roi_labels, roi_labels_selected
  217. def get_selected_data_labels(self):
  218. data_selector = self.MainWindow.Front_Control_Panel.Data_Selector
  219. selected_data_labels = [data_selector.item(x.row(), 0).text()
  220. for x in data_selector.selectionModel().selectedRows()]
  221. return selected_data_labels
  222. @pyqtSlot(name="save circle roi_labels for VIEW")
  223. def spawn_save_circle_rois_dialog(self):
  224. circle_roi_labels, circle_roi_labels_selected = self.get_roi_and_selected_by_type([myCircleROI])
  225. selected_data_labels = self.get_selected_data_labels()
  226. save_circle_rois_dialog = SaveCircleROIsFileDialog(metadata=self.metadata, data_selected=selected_data_labels,
  227. circle_roi_labels=circle_roi_labels,
  228. circle_rois_selected=circle_roi_labels_selected)
  229. save_circle_rois_dialog.return_choices_signal.connect(self.save_coors_for_VIEW)
  230. self.dialogs.append(save_circle_rois_dialog)
  231. save_circle_rois_dialog.show()
  232. @pyqtSlot(name="save roi_labels for VIEW")
  233. def spawn_save_all_rois_dialog(self):
  234. roi_labels, roi_labels_selected = self.get_roi_and_selected_by_type([myCircleROI, myPolyLineROI])
  235. selected_data_labels = self.get_selected_data_labels()
  236. save_all_rois_dialog = SaveAllROIsFileDialog(metadata=self.metadata, data_selected=selected_data_labels,
  237. roi_labels=roi_labels,
  238. roi_labels_selected=roi_labels_selected)
  239. save_all_rois_dialog.return_choices_signal.connect(self.save_coors_for_VIEW)
  240. self.dialogs.append(save_all_rois_dialog)
  241. save_all_rois_dialog.show()
  242. @pyqtSlot(name="save area for VIEW")
  243. def spawn_save_area_dialog(self):
  244. poly_roi_labels, poly_roi_labels_selected = self.get_roi_and_selected_by_type([myPolyLineROI])
  245. selected_data_labels = self.get_selected_data_labels()
  246. save_area_dialog = SaveAreaFileDialog(metadata=self.metadata, data_selected=selected_data_labels,
  247. poly_roi_labels=poly_roi_labels,
  248. poly_rois_selected=poly_roi_labels_selected)
  249. save_area_dialog.return_choices_signal.connect(self.save_area_for_VIEW)
  250. self.dialogs.append(save_area_dialog)
  251. save_area_dialog.show()
  252. @pyqtSlot(list, str, name="save area for VIEW")
  253. def save_area_for_VIEW(self, roi_labels, filename):
  254. self.write_status("[working] Writing AREA file for VIEW")
  255. extraction_mask = np.zeros((self.Data.raw.shape[0],
  256. self.Data.raw.shape[1],
  257. len(roi_labels)), dtype='bool')
  258. rois_chosen = [x for x in self.ROIs.ROI_list if x.label in roi_labels and type(x) == myPolyLineROI]
  259. roi_data = []
  260. for i, ROI in enumerate(rois_chosen):
  261. if ROI.label in roi_labels:
  262. mask, inds = self.ROIs.get_ROI_mask(ROI)
  263. extraction_mask[mask, i] = 1
  264. roi_data.append(convert_iltisROI2VIEWROI(ROI))
  265. pl.Path(filename).parent.mkdir(exist_ok=True)
  266. ILTISTextROIFileIO.write(f"{filename}.roi", roi_data)
  267. save_tstack(extraction_mask, filename)
  268. QMessageBox.information(self.MainWindow, "AREA File saved!", f"to\n{filename}\nusing ROIs {roi_labels}")
  269. self.write_status("[success] Writing AREA file for VIEW")
  270. @pyqtSlot(list, str, name="save COORs for VIEW")
  271. def save_coors_for_VIEW(self, roi_labels, filename):
  272. self.write_status("[working] Writing COORs file for VIEW")
  273. rois_chosen = [x for x in self.ROIs.ROI_list if x.label in roi_labels]
  274. roi_datas = []
  275. for roi in rois_chosen:
  276. roi_data = convert_iltisROI2VIEWROI(roi)
  277. roi_datas.append(roi_data)
  278. ILTISTextROIFileIO.write(filename, roi_datas)
  279. QMessageBox.information(self.MainWindow, "Coor File saved!", f"to\n{filename}\nusing ROIs {roi_labels}")
  280. self.write_status("[success] Writing COORs file for VIEW")
  281. @pyqtSlot(name="quick save area")
  282. def quick_save_area(self):
  283. self.write_status("[working] Writing COORs file for VIEW")
  284. selected_data_labels = self.get_selected_data_labels()
  285. mask = self.metadata["Label to use"].apply(lambda x: x in selected_data_labels)
  286. metadata_selected_data = self.metadata.loc[mask, :]
  287. animals_deduplicated = metadata_selected_data['STG_ReportTag'].unique()
  288. if animals_deduplicated.shape[0] == 1:
  289. current_metadata_row = metadata_selected_data.iloc[0]
  290. filename = \
  291. str(pl.Path(current_metadata_row["STG_OdorAreaPath"]) / f"{animals_deduplicated[0]}.area.tif")
  292. _, selected_roi_labels = self.get_roi_and_selected_by_type()
  293. if len(selected_roi_labels) == 0:
  294. QMessageBox.critical(
  295. self.MainWindow, "No ROIs selected!",
  296. "Please select one or more ROIs on the right column and try again!"
  297. )
  298. self.save_area_for_VIEW(roi_labels=selected_roi_labels, filename=filename)
  299. else:
  300. QMessageBox.critical(
  301. self.MainWindow, "No data selected!",
  302. "Please select one imaging data on the right column and try again!"
  303. )