def _get_matfile_object(self, fullpath: pathlib.Path): try: # MATLAB 7.3 file needs to be loaded as HDF5 [install HDF5 on your pc from hdfgroup.org] return h5py.File(fullpath, 'r') except OSError as e1: # MATLAB < 7.3 try: return loadmat(fullpath.as_posix(), struct_as_record=False, squeeze_me=True) except Exception as e2: Dialog().warningMessage( 'Loading {}.mat with h5py.File() failed\r\n'.format(fullpath.stem) + str( e1) + '\r\n' + 'Install HDF5 on your pc from hdfgroup.org\r\n' + 'Now attempting to use loadmat()\r\n') Dialog().warningMessage('Sorry, unsuccessful...\r\n' + str(e2))
def load(self, fullpath): try: hf = self._get_matfile_object(fullpath) assert all(s in hf.keys() for s in ['annotations', 'partitions']), r'h5 must have {} groups'.format( ['annotations', 'partitions']) assert all([s in hf['partitions'] for s in ['label', 'start', 'end']]), r'h5.partitions must have {} groups'.format( ['label', 'start', 'end']) labels = hf['partitions/label'] labels = [n.decode('ascii', 'ignore') for n in labels] start = np.array(hf['partitions/start']) end = np.array(hf['partitions/end']) assert len(labels) == start.size & start.size == end.size, 'Every partition should have label, start and end' Partitions.add_all(labels, start, end) from logic.operation_mode.annotation import AnnotationConfig assert all([s in AnnotationConfig.all_fiducials() for s in hf['annotations']]), 'All h5.annotations must be in {} groups'.format( AnnotationConfig.all_fiducials()) for f_name in hf['annotations'].keys(): self._set_annotation_from_time(f_name, np.array(hf['annotations/' + f_name + '/ts'])) from gui.viewer import Viewer try: # when loaded during initialization Viewer.get().selectedDisplayPanel.plot_area.redraw_fiducials() # to update and show loaded data except: pass try: # can be removed after thorough testing if 'epoch' in hf.keys(): assert all( [f in hf['epoch'] for f in ['start', 'end', 'is_modified', 'label', 'all_labels', 'keys', 'default_label']]), 'Loaded file contains incorrect epoch mode data' labels = [n.decode('ascii', 'ignore') for n in hf['epoch/label']] epoch_data = pd.DataFrame( {'start': hf['epoch/start'], 'end': hf['epoch/end'], 'is_modified': hf['epoch/is_modified'], 'label': labels}) keys = [n.decode('ascii', 'ignore') for n in hf['epoch/keys']] all_labels = [n.decode('ascii', 'ignore') for n in hf['epoch/all_labels']] description = [n.decode('ascii', 'ignore') for n in hf['epoch/description']] default_label = hf['epoch/default_label'][0] NONE_LABEL = hf['epoch/NONE_LABEL'][0] EpochModeConfig.load_from_hdf5(epoch_data, keys, all_labels, default_label, NONE_LABEL, description=description) except Exception as e: Dialog().warningMessage('Epoch mode data cannot be loaded\r\n' + 'The error was:\r\n' + str(e)) except Exception as e: Dialog().warningMessage('Loading existing annotations failed\r\n' + 'The error was:\r\n' + str(e))
def onclick_select(self): try: fn = [ pathlib.Path(self.db.DATAPATH, item.text()) for item in self.listWidget_files.selectedItems() ] if len(fn) > 1: Dialog().warningMessage( 'Multiple file selection not implemented') self.selected_files = fn global selected_files selected_files = fn self.accept() except Exception as e: Dialog().warningMessage(str(e))
def set_annotation_data(self): # NB: used to set initial guesses for annotations, otherwise, one has to start annotation from scratch # one can use here simple findpeaks() type algos, or more signal-specific python algorigthms # also possible to run an algo beforehand (e.g. in Matlab), store the results and load them here # NB: OPTIONAL!!! Load existing annotation if an .h5 file with the same name found in self.existing_annotation_folder (be careful with self.outputfile_prefix) existing_annotation_file = pathlib.Path( self.existing_annotations_folder, self.fullpath.stem + '.h5') existing_annotation_file_with_prefix = pathlib.Path( self.existing_annotations_folder, self.outputfile_prefix + self.fullpath.stem + '.h5') existing_annotation_files = self.get_annotation_file( self.fullpath.stem) if existing_annotation_files is not None: latest_file_idx = np.argmax( [os.path.getmtime(f) for f in existing_annotation_files]) try: self.load(existing_annotation_files[latest_file_idx]) qInfo('Loading annotations from {}'.format( existing_annotation_file_with_prefix)) except Exception as e: Dialog().warningMessage( 'Loading annotations from {} failed\r\n'.format( existing_annotation_file_with_prefix) + str(e)) else: # # NB: 1. Find\fetch preliminary annotation data ecg = self.tracks[self.main_track_label].value fs = self.tracks[self.main_track_label].fs try: qrs_detector = PanTomkinsQRSDetector(ecg, fs, verbose=True, log_data=False, plot_data=False, show_plot=False) # # NB: 2. Use inherited functions to assign annotations to the main signal # # all annotation labels should be also in the provided AnnotationConfig file # # User can use _set_annotation_from_time or _set_annotation_from_idx self._set_annotation_from_idx('rpeak', qrs_detector.qrs_peaks_indices) except Exception as e: Dialog().warningMessage( 'Failed to use beat detector\r\n' 'Currently you do not have any initial annotations loaded, but\r\n' 'You can fix the issue, or implement another way in set_annotation_data()' )
def set_annotation_data(self): # NB: used to set initial guesses for annotations, otherwise, one has to start annotation from scratch # one can use here simple findpeaks() type algos, or more signal-specific python algorithms # also possible to run an algo beforehand (e.g. in Matlab), store the results and load them here # NB: OPTIONAL!!! Load existing annotation if an .h5 file with the same name found in self.existing_annotation_folder (be careful with self.outputfile_prefix) existing_annotation_file = pathlib.Path(self.existing_annotations_folder, self.fullpath.stem + '.h5') if self.annotation_exists(existing_annotation_file.stem): try: self.load(existing_annotation_file) qInfo('Loading annotations from {}'.format(existing_annotation_file)) except Exception as e: Dialog().warningMessage('Loading annotations from {} failed\r\n'.format(existing_annotation_file) + str(e)) else: # # NB: 1. Find\fetch preliminary annotation data f = self._get_matfile_object(self.fullpath) offset = np.concatenate(np.array(f['/data/ppg/ts']))[0] # offset to start at time=0 as signals themselves peak = np.concatenate(np.array(f['/data/annotations/ppg/peak/timestamps'])) - offset foot = np.concatenate(np.array(f['/data/annotations/ppg/foot/timestamps'])) - offset # # NB: 2. Use inherited functions to assign annotations to the main signal # # all annotation labels should be also in the provided AnnotationConfig file # # User can use _set_annotation_from_time or _set_annotation_from_idx self._set_annotation_from_time('peak', peak) self._set_annotation_from_time('foot', foot)
def clear_annotations_in_this_partition(p: SinglePartition): """ batch removing annotations which fall within a partition. good to have if it is needed to clean large artifact region from spurious fiducials """ try: from logic.operation_mode.annotation import AnnotationConfig from gui.viewer import Viewer aConf = AnnotationConfig.get() for f in aConf.fiducials: ann = f.annotation remove_idx = np.arange(bisect.bisect_right(ann.x, p.start), bisect.bisect_left(ann.x, p.end)) nn = max(remove_idx.shape) result = QtWidgets.QMessageBox.question(Viewer.get(), "Confirm Delete Annotations...", "Are you sure you want to delete {nn} {name} annotations ?".format(nn=nn, name=ann.name), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if result == QtWidgets.QMessageBox.Yes: ann.x = np.delete(ann.x, remove_idx) ann.y = np.delete(ann.y, remove_idx) ann.idx = np.delete(ann.idx, remove_idx) Viewer.get().selectedDisplayPanel.plot_area.redraw_fiducials() Partitions.update_all_bounds() except Exception as e: Dialog().warningMessage('Deleting annotations failed with\r\n' + str(e))
def test_database_setup(self): """ place all asserts\tests in one place :return: bool """ # check type of the data (floats) and dimensions assert self.output_folder.is_dir(), 'self.output_folder is not a directory' assert self.existing_annotations_folder.is_dir(), 'self.existing_annotations_folder is not a directory' assert self.DATAPATH.is_dir(), 'self.DATAPATH is not a directory' assert self.tracks is not None, 'self.tracks is None' assert self.annotation_config_file is not None, 'self.annotation_config_file is None' assert self.main_track_label is not None, 'self.main_track_label is None' assert len(self.track_labels) == len(set(self.track_labels)),'not all track labels are unique' assert self.main_track_label in self.track_labels, 'self.main_track_label is not in self.track_labels' assert self.main_track_label in self.tracks_to_plot_initially, 'self.main_track_label not in self.tracks_to_plot_initially' assert all([tp in self.track_labels for tp in self.tracks_to_plot_initially]), 'not all self.tracks_to_plot_initially are in self.track_labels' assert self.main_track_label in self.track_labels, 'self.main_track_label not in self.track_labels' assert not any([' ' in l for l in self.track_labels]),'all self.track_labels should be one word, no spaces' # make the main track label to always go first for plotting, otherwise errors will appear for other views, when the main view is not created yet main_track_idx = np.argwhere([self.main_track_label == l for l in self.tracks_to_plot_initially])[0][0] if not main_track_idx == 0: tmp = self.tracks_to_plot_initially[0] self.tracks_to_plot_initially[0] = self.main_track_label self.tracks_to_plot_initially[main_track_idx] = tmp l = [l.get_time()[-1] for l in self.tracks.values()] x = np.reshape(l, (len(l), 1)) x = np.concatenate(x - x.transpose()) # all differences between track durations if max(abs(x)) > 1: Dialog().warningMessage('Some of the tracks differ in their duration more than {} seconds\r\n'.format(max(abs(x))) + 'You can continue working if this is expected.') # assert max(abs(x)) < 0.5 # tracks should be the same duration (0.5 sec difference allowed), otherwise smth is wrong pass
def zoomChanged(self): try: # this is to avoid redrawing if it is already zoomed out to max newRange = self.main_window.selectedView.renderer.vb.targetRange( )[0] viewRange = self.main_window.selectedView.renderer.vb.viewRange( )[0] # newRange = self.main_vb.viewRange()[0] if newRange[-1] >= Database.get().get_longest_track_duration( ) and newRange[0] <= 0: # TODO what if offset not 0 if self.FLAG_full_zoom_out: return else: self.FLAG_full_zoom_out = True else: self.FLAG_full_zoom_out = False except: pass if self.main_vb.geometry(): try: pixel_width = self.main_vb.viewPixelSize()[0] self.main_vb.setLimits(xMin=-pixel_width) for vb in self.vbs.values(): vb.setLimits(xMin=-pixel_width) self.redraw_fiducials() if self.display_panel is not None and len( self.display_panel.panel.views) > 0: self.setYRange() EpochModeConfig.get().redraw_epochs() except Exception as e: Dialog().warningMessage( 'Exception occured\r\n' 'Using more than one frame may have caused this!\r\n' + str(e))
def get(cls): """ method allows to get the only Database instance from anywhere in the code by running Database.get() :return: object of one of the classes in this module """ if Database._instance is not None: return Database._instance else: Dialog().warningMessage('Databse not initialized yet\r\n return None') return None
def save(self, **kwargs): # NB: save annotation data. By default annotations and partitions are saved as .h5 file. # All tracks can be saved too (see Settings in the menu bar). # One can also define custom save protocol here # self.output_folder = self.fullpath.parent # to save in the same location # self.output_folder = get_project_root() # to save in project root/near the executable try: self.output_folder = self.fullpath.parent super().save(filename=self.fullpath.stem, **kwargs) except Exception as e: Dialog().warningMessage('Save crashed with: \r\n' + str(e))
def add_all(labels: List[str], start: np.ndarray, end: np.ndarray): """ batch adding partitions, e.g. from loaded annotations file """ try: Partitions.delete_all() assert len(labels) == start.size & start.size == end.size, 'Every loaded partition should have label, start and end' for l, s, e in zip(labels, start, end): SinglePartition(l, start=s, end=e) except Exception as e: Dialog().warningMessage('Partitions cannot be loaded\r\n' + str(e))
def set_annotation_data(self): # NB: used to set initial guesses for annotations, otherwise, one has to start annotation from scratch # one can use here simple findpeaks() type algos, or more signal-specific python algorithms # also possible to run an algo beforehand (e.g. in Matlab), store the results and load them here # NB: OPTIONAL!!! Load existing annotation if an .h5 file with the same name found in self.existing_annotation_folder (be careful with self.outputfile_prefix) existing_annotation_file = pathlib.Path( self.existing_annotations_folder, self.fullpath.stem + '.h5') if self.annotation_exists(existing_annotation_file.stem): try: self.load(existing_annotation_file) qInfo('Loading annotations from {}'.format( existing_annotation_file)) except Exception as e: Dialog().warningMessage( 'Loading annotations from {} failed\r\n'.format( existing_annotation_file) + str(e)) else: # NB: 1. Find\fetch preliminary annotation data amp = self.tracks[self.main_track_label].value mpd = self.tracks[self.main_track_label].fs * 2 idx_peak = detect_peaks(amp, mph=np.median(amp), mpd=mpd, valley=False, show=False, kpsh=False) idx_valley = detect_peaks(amp, mph=np.median(amp), mpd=mpd, valley=True, show=False, kpsh=False) idx_upstroke = detect_peaks(np.diff(amp), mph=np.median(np.diff(amp)), mpd=mpd, valley=False, show=False, kpsh=False) idx_downstroke = detect_peaks(np.diff(amp), mph=np.median(np.diff(amp)), mpd=mpd, valley=True, show=False, kpsh=False) # NB: 2. Use inherited functions to assign annotations to the main signal # all annotation labels should be also in the provided AnnotationConfig file # User can use _set_annotation_from_time or _set_annotation_from_idx self._set_annotation_from_idx('peak', idx_peak) self._set_annotation_from_idx('valley', idx_valley) self._set_annotation_from_idx('upstroke', idx_upstroke) self._set_annotation_from_idx('downstroke', idx_downstroke)
def to_csv(filename: str): d = {'label': Partitions.all_labels(), 'start': Partitions.all_startpoints(), 'end': Partitions.all_endpoints()} df = dict_to_df_with_nans(d) try: df.to_csv(filename + '.csv', index=False) except OSError as e: try: xl = win32com.client.Dispatch("Excel.Application") xl.Quit() # quit excel, as if user hit the close button/clicked file->exit. # xl.ActiveWorkBook.Close() # close the active workbook df.to_csv(filename + '.csv', index=False) except Exception as e: Dialog().warningMessage('Save crashed with:\r\n' + str(e))
def to_csv(filename: str): data = EpochModeConfig.get() try: data.window_data.to_csv(filename + '.csv', index=False) except OSError as e: try: xl = win32com.client.Dispatch("Excel.Application") xl.Quit( ) # quit excel, as if user hit the close button/clicked file->exit. # xl.ActiveWorkBook.Close() # close the active workbook data.window_data.to_csv(filename + '.csv', index=False) except Exception as e: Dialog().warningMessage( 'Save of epoch mode annotations crashed with:\r\n' + str(e))
def find_files(self): if not os.path.isdir(self.db.DATAPATH): Dialog().warningMessage( 'database path is incorrect. No files can be found.') tmp = '**/*.' + self.db.filetype if self.db.file_template is None else self.db.file_template for filename in Path(self.db.DATAPATH).glob(tmp): item = QtWidgets.QListWidgetItem() item.setFont(QtGui.QFont('Arial', 16)) item.setText(filename.as_posix().replace( self.db.DATAPATH.as_posix() + '/', '')) if self.db.annotation_exists(pathlib.Path(item.text()).stem): item.setForeground(QtGui.QColor(QtCore.Qt.darkBlue)) item.setBackground(QtGui.QColor(QtCore.Qt.lightGray)) else: item.setForeground(QtGui.QColor(QtCore.Qt.blue)) self.listWidget_files.addItem(item) self.listWidget_files.setMinimumWidth( self.listWidget_files.sizeHintForColumn(0)) if self.listWidget_files.count() > 0: self.listWidget_files.setCurrentRow(0) self.listWidget_files.setFocus()
def split_pinned_to(self): try: from gui.viewer import PALMS if self.pinned_to in PALMS.config['pinned_to_options']: self.pinned_to = self.pinned_to + ' ' + Database.get().main_track_label pinned_to_split = self.pinned_to.split() # TODO: make it more generic and foolproof, now it is assumed that pinned_to consists of two words: peak\valley + track_label assert_text = '{} pinned_to setting must consist of two words:\r\n' \ '"peak" or "valley" + track_label from the database;\r\n' \ 'Current pinned_to value is {}\r\n' \ 'Check your AnnotationConfig file {} and run the tool again'.format(self.name, self.pinned_to, Database.get().annotation_config_file.stem) assert len(pinned_to_split) == 2, assert_text assert pinned_to_split[0] in PALMS.config['pinned_to_options'], assert_text self.pinned_to_location = pinned_to_split[0] self.pinned_to_track_label = pinned_to_split[1] except Exception as e: from utils.utils_gui import Dialog Dialog().warningMessage('Error in split_pinned_to():\r\n' + str(e)) raise ValueError
def load_annotation_config_from_csv(self, fileName=None): fn = fileName or self.fileName if not fn: fn, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load AnnotationConfig", get_project_root().as_posix(), " (*.csv);; All Files (*)", options=QtWidgets.QFileDialog.Options()) if fn: from gui import PALMS with open(fn, "r") as csv: settings_init = pd.read_csv(csv) fiducials = [] for _, row_data in settings_init.iterrows(): try: assert all([k in row_data for k in PALMS.config['annotationConfig_columns']]) except: Dialog().warningMessage('Columns in chosen {file} file do not match aConfColumns from config.py. Try again! ' ''.format(file=fn)) return fiducials.append(SingleFiducialConfig(row_data)) aConf = AnnotationConfig.get() aConf.reset_fiducials_config(fiducials) self.show()
def initialize_epoch_mode_settings_from_csv(csv): if csv is None: EpochModeConfig() qInfo( 'Epoch mode init settings not given. Starting with default values' ) try: csv_data = pd.read_csv(csv) winLen = int(csv_data.winLen.values[0]) labels = list(csv_data.labels.values) description = list(csv_data.description.values) try: if not all(isinstance(elem, str) for elem in description): description = labels except Exception as e: description = labels keys = list(csv_data['keys'].values) for i, k in enumerate(keys): if isinstance(k, (int, np.integer)): keys[i] = str(k) default_label = csv_data.default_label.values[0] if not isinstance(default_label, str) and np.isnan(default_label): default_label = None EpochModeConfig(winLen=winLen, labels=labels, keys=keys, default_label=default_label, description=description) except Exception as e: Dialog().warningMessage( 'Epoch mode settings processing failed with\r\n' + str(e) + '\r\nInitializing with defaults') EpochModeConfig()
def onclick_find(self): Dialog().warningMessage('Custom file choice not implemented')
def save(self, **kwargs): # TODO: popup warning when rewriting existing files try: from gui.viewer import Viewer filename = kwargs.get('filename', self.fullpath.stem) try: filename = self.outputfile_prefix + filename except Exception as e: qInfo('Output file prefix could not be added') fullpath = pathlib.Path(self.output_folder, filename + '.h5') OVERWRITE = kwargs.get('OVERWRITE', Viewer.get().settings_menu.save_overwrite_action.isChecked()) if fullpath.is_file(): # don't overwrite files if not OVERWRITE: path, filename = os.path.split(fullpath) filename = os.path.splitext(filename)[0] newfilename = filename + '_' + strftime("%Y_%m_%d_%H_%M_%S", gmtime()) + fullpath.suffix fullpath = pathlib.Path(path, newfilename) qInfo('Existing file found. Not overwriting') else: qInfo('Existing file OVERWRITTEN!') from logic.operation_mode.annotation import AnnotationConfig aConf = AnnotationConfig.get() hf = h5py.File(fullpath, 'w') group_annotations = hf.create_group('annotations') for f_idx, f in enumerate(aConf.fiducials): group_annotations.create_group(f.name) group_annotations.create_dataset(f.name + '/ts', data=f.annotation.x) group_annotations.create_dataset(f.name + '/idx', data=f.annotation.idx) group_annotations.create_dataset(f.name + '/amp', data=f.annotation.y) group_partitions = hf.create_group('partitions') asciiList = [n.encode("ascii", "ignore") for n in Partitions.all_labels()] group_partitions.create_dataset('label', data=asciiList) group_partitions.create_dataset('start', data=Partitions.all_startpoints()) group_partitions.create_dataset('end', data=Partitions.all_endpoints()) group_epoch = hf.create_group('epoch') group_epoch.create_dataset('start', data=EpochModeConfig.get().window_data['start'].values) group_epoch.create_dataset('end', data=EpochModeConfig.get().window_data['end'].values) group_epoch.create_dataset('is_modified', data=EpochModeConfig.get().window_data['is_modified'].values) asciiList = [n.encode("ascii", "ignore") for n in EpochModeConfig.get().window_data['label'].values] group_epoch.create_dataset('label', data=asciiList) asciiList = [n.encode("ascii", "ignore") for n in EpochModeConfig.get().keys] group_epoch.create_dataset('keys', data=asciiList) asciiList = [n.encode("ascii", "ignore") for n in EpochModeConfig.get().labels] group_epoch.create_dataset('all_labels', data=asciiList) asciiList = [n.encode("ascii", "ignore") for n in EpochModeConfig.get().description] group_epoch.create_dataset('description', data=asciiList) dt = h5py.special_dtype(vlen=str) group_epoch.create_dataset('default_label', (1,), dtype=dt) group_epoch['default_label'][:] = EpochModeConfig.get().default_label group_epoch.create_dataset('NONE_LABEL', (1,), dtype=dt) group_epoch['NONE_LABEL'][:] = EpochModeConfig.get().NONE_LABEL group_meta = hf.create_group('meta') dt = h5py.special_dtype(vlen=str) group_meta.create_dataset('timestamp', (1,), dtype=dt) group_meta['timestamp'][:] = strftime("%Y_%m_%d_%H_%M_%S", gmtime()) group_meta.create_dataset('filename', (1,), dtype=dt) group_meta['filename'][:] = self.fullpath.stem group_meta.create_dataset('filepath', (1,), dtype=dt) group_meta['filepath'][:] = self.fullpath.parent.as_posix() group_meta.create_dataset('main_track_label', (1,), dtype=dt) group_meta['main_track_label'][:] = self.main_track_label if Viewer.get().settings_menu.save_tracks_action.isChecked(): group_tracks = hf.create_group('tracks') for label, track in Database.get().tracks.items(): group_tracks.create_dataset(label + '/ts', data=track.ts) group_tracks.create_dataset(label + '/amp', data=track.value) group_tracks.create_dataset(label + '/offset', data=track.offset) group_tracks.create_dataset(label + '/fs', data=track.fs) hf.close() qInfo('{} saved'.format(fullpath.as_posix())) except Exception as e: try: hf.close() except: pass self._save_as_csv(filename=self.fullpath.stem, save_idx=False) Dialog().warningMessage('Default save crashed\r\n' + e.__repr__() + '\r\nSaved using deprecated method, as CSV files.')
def viewPopup(self, point): if isinstance(self.sender(), QtWidgets.QHeaderView): row = self.sender().logicalIndexAt(point) elif isinstance(self.sender(), QtWidgets.QLabel): row = self.rowFromWidget(self.sender()) else: logging.error(f'do not know how to handle getting index of ', f'{self.sender()} object') self.main_window.application.viewer.status( f'do not know how to handle getting index of ', f'{self.sender()} object') raise TypeError view = self.panel.views[row] menu = QtWidgets.QMenu(self.verticalHeader()) menu.clear() move_menu = menu.addMenu('&Move View') link_menu = menu.addMenu("&Link Track") copy_menu = menu.addMenu("Copy View") derive_menu = menu.addMenu("Add derived tracks") linkAction = QtWidgets.QAction('Create Link in this Panel', self) linkAction.triggered.connect( partial(self.display_panel.linkTrack, view, self.main_window.model.panels.index(self.panel))) link_menu.addAction(linkAction) link_menu.addSeparator() link_menu.setEnabled(False) copyAction = QtWidgets.QAction("Duplicate View in this Panel", self) copyAction.triggered.connect( partial(self.display_panel.copyView, view, self.main_window.model.panels.index(self.panel))) copy_menu.addAction(copyAction) copy_menu.addSeparator() copy_menu.setEnabled(False) addDerivativeAction = QtWidgets.QAction('1st derivative', self) addDerivativeAction.triggered.connect( partial(self.display_panel.addDerivative, view, self.main_window.model.panels.index(self.panel), 1)) # TODO: derivative filters GUI derive_menu.addAction(addDerivativeAction) addDerivativeAction = QtWidgets.QAction('2nd derivative', self) addDerivativeAction.triggered.connect( partial(self.display_panel.addDerivative, view, self.main_window.model.panels.index(self.panel), 2)) derive_menu.addAction(addDerivativeAction) menu.addSeparator() try: addRRintervalMenu = menu.addMenu('RR-intevals') from logic.operation_mode.annotation import AnnotationConfig RRintervalMenu_actions = [] for f in AnnotationConfig.all_fiducials(): f_idx = AnnotationConfig.get().find_idx_by_name(f) if AnnotationConfig.get( ).fiducials[f_idx].annotation.x.size > 2: action = QtWidgets.QAction(f, self, checkable=False, enabled=True) action.triggered.connect( partial( self.main_window.application.add_view_from_track, AnnotationConfig.get().fiducials[f_idx].annotation. create_RRinterval_track(), self.main_window.model.panels.index(self.panel))) RRintervalMenu_actions.append(action) addRRintervalMenu.addAction(action) except Exception as e: Dialog().warningMessage('Creating RR interval plot failed\r\n' 'The error was:\r\n' + str(e)) # TODO: load another track # TODO: move existing view db = Database.get() add_menu = menu.addMenu("Add tracks from DB") add_menu.setEnabled(False) if db is not None and db.ntracks() > 0: add_menu.setEnabled(True) for label, track in db.tracks.items(): plot_signal_action = QtWidgets.QAction(label, self) plot_signal_action.triggered.connect( partial(self.main_window.application.add_view_from_track, track, self.main_window.model.panels.index(self.panel))) add_menu.addAction(plot_signal_action) if label not in self.allViewsTrackLabels(): plot_signal_action.setEnabled(True) else: plot_signal_action.setEnabled(False) menu.addSeparator() process_action = QtWidgets.QAction('Processor Config', self) process_action.triggered.connect( partial(self.main_window.application.viewer.processorConfig.show, self.selectedView())) menu.addAction(process_action) process_action.setEnabled(False) menu.addSeparator() invertAction = QtWidgets.QAction('&Invert Track', self) invertAction.triggered.connect(self.main_window.invertView) invertAction.setEnabled(False) menu.addAction(invertAction) menu.addSeparator() deleteAction = QtWidgets.QAction('&Delete Track', self) deleteAction.triggered.connect(self.main_window.guiDelView) menu.addAction(deleteAction) for index, panel in enumerate(self.main_window.model.panels): if panel is self.panel: continue linkAction = QtWidgets.QAction(f'Link to Panel {index + 1}', self) linkAction.triggered.connect( partial(self.display_panel.linkTrack, view, index)) link_menu.addAction(linkAction) moveAction = QtWidgets.QAction(f'Move To Panel {index + 1}', self) moveAction.triggered.connect( partial(self.display_panel.moveView, view, index)) move_menu.addAction(moveAction) copyAction = QtWidgets.QAction(f'Copy to Panel {index + 1}', self) copyAction.triggered.connect( partial(self.display_panel.copyView, view, index)) copy_menu.addAction(copyAction) moveAction = QtWidgets.QAction(f'Move to New Panel', self) moveAction.triggered.connect( partial(self.display_panel.moveView, view, -1)) linkAction = QtWidgets.QAction(f'Link to New Panel', self) linkAction.triggered.connect( partial(self.display_panel.linkTrack, view, -1)) copyAction = QtWidgets.QAction(f'Copy to New Panel', self) copyAction.triggered.connect( partial(self.display_panel.copyView, view, -1)) move_menu.addSeparator() link_menu.addSeparator() copy_menu.addSeparator() move_menu.addAction(moveAction) link_menu.addAction(linkAction) copy_menu.addAction(copyAction) menu.popup(QtGui.QCursor.pos())
def mousePressEvent(self, event: QtGui.QMouseEvent): """ The following only has effect if the main track is selected in the track table view on the right Annotation mode: **LeftMouseButton** places a new fiducial. By default it is the first fiducial as set in the annotationConfig, unless before that a keyboard button was pressed corresponding to another fiducial or "sticky fiducial" mode is on. **RightMouseButton** removes the nearest fiducial (default), unless "sticky fiducial" mode is on. Keyboard has no effect Partition mode: **CTRL + LeftMouseButton** creates a new partition which takes 2% of the whole track duration or less to avoid overlapping partiotions; **CTRL + RightMouseButton** removes partition under the click **SHIFT + LeftMouseButton** to drag partition borders or move the partition. Repositioning is possible within the limits of neighboring partition. Moving the partition fully inside another one or reducing its size to 1 sample deletes the partition. #NOTE: creating partition of desired size directly by click and drag might not be that convenient and harder to implement Epoch mode: # NB: when adding\removing mouse click operations also adapt self.mouseReleaseEvent() and self.mouseMoveEvent() """ def which_fiducial_to_add(last_keypress_event_key): from gui.viewer import Viewer # first check if 'sticky' fiducial option is enabled for one of the fiducials sticky_fiducial = [ item.isChecked() for item in Viewer.get().annotation_menu.sticky_fiducial_menu.actions() ] if any(sticky_fiducial): idx = np.argwhere(sticky_fiducial)[0] return AnnotationConfig.get().find_idx_by_name( Viewer.get().annotation_menu.sticky_fiducial_menu.actions( )[idx[0]].text()) # if 'sticky' fiducial option is off, check what key was pressed the last default_fiducial_idx = 0 # default: first fiducial if last_keypress_event_key is not None: qInfo('Last pressed: ' + str(last_keypress_event_key[1])) for fiducial_idx, f in enumerate( AnnotationConfig.get().fiducials): if f.key.lower() == last_keypress_event_key[1].lower(): return fiducial_idx return default_fiducial_idx else: return default_fiducial_idx def which_fiducial_to_delete(click_x): from gui.viewer import Viewer # first check if 'sticky' fiducial option is enabled for one of the fiducials sticky_fiducial = [ item.isChecked() for item in Viewer.get().annotation_menu.sticky_fiducial_menu.actions() ] if any(sticky_fiducial): idx = np.argwhere(sticky_fiducial)[0] return AnnotationConfig.get().find_idx_by_name( Viewer.get().annotation_menu.sticky_fiducial_menu.actions( )[idx[0]].text()) dist = np.inf fiducial_name, fiducial_idx = None, None for f_idx, f in enumerate(AnnotationConfig.get().fiducials): if f.annotation.x.size > 0: closest_idx, _, _ = find_closest(f.annotation.x, np.array([click_x])) dist_new = abs(click_x - f.annotation.x[closest_idx]) if dist_new < dist: dist = dist_new fiducial_name, fiducial_idx = f.name, f_idx if not dist == np.inf: return fiducial_idx try: from gui.viewer import Viewer Viewer.get().selectFrame(self.display_panel.parent( )) # first select the frame where the click was made if Mode.is_epoch_mode(): EpochModeConfig.get().process_mouseclick(event) return vb = self.vbs[self.selected_view()] click_x = vb.mapSceneToView(event.pos()).x() if self.selected_view().track.label is Database.get( ).main_track_label: # TODO: do this check properly and uniformly everywhere if Mode.is_annotation_mode(): if AnnotationConfig.get().is_valid(): if event.button( ) == QtCore.Qt.LeftButton: # Left click to mark fiducial_idx = which_fiducial_to_add( self.last_keypress_event_key) AnnotationConfig.get().fiducials[ fiducial_idx].annotation.signal_annotate.emit( click_x) elif event.button( ) == pg.QtCore.Qt.RightButton: # right click to delete fiducial_idx = which_fiducial_to_delete(click_x) if fiducial_idx is not None: AnnotationConfig.get().fiducials[ fiducial_idx].annotation.signal_delete_annotation.emit( click_x) else: qInfo('No annotation found to be deleted') self.last_keypress_event_key = None # need to press extra key every time to annotate secondary fiducial elif Mode.is_partition_mode(): if event.button() == QtCore.Qt.LeftButton: if event.modifiers() == QtCore.Qt.ControlModifier: self.create_new_partition(event) else: super().mousePressEvent(event) qInfo( 'CTRL+Left: create region; SHIFT+Left: move region' ) # event.accept() elif event.button() == QtCore.Qt.RightButton: if event.modifiers() == QtCore.Qt.ControlModifier: p = Partitions.find_partition_by_point(click_x) if p is not None: p.region_deleted() # event.accept() elif event.modifiers() == QtCore.Qt.ShiftModifier: p = Partitions.find_partition_by_point(click_x) if p is not None: self.partition_context_menu(event) else: qInfo( 'No partition found...CTRL+Right: delete region; SHIFT+Right: region context menu' ) else: super().mousePressEvent(event) qInfo( 'CTRL+Right: delete region; SHIFT+Right: region context menu' ) else: super().mousePressEvent(event) elif Mode.mode == Modes.browse: super().mousePressEvent(event) else: if not self.is_main_view_in_current_panel( ): # click on a panel, without the main track Dialog().warningMessage( 'Selected display panel does not contain the main track ({}).\r\n' .format(Database.get().main_track_label) + 'Try clicking on another display panel') else: # click on a correct panel, but the main track is not selected Dialog().warningMessage( 'Selected signal is not the main one.\r\n' 'Click on the ' '{}' ' track in the control area on the right.'.format( Database.get().main_track_label)) return except Exception as e: Dialog().warningMessage('Mouse click processing failed\r\n' 'What unusual did you do?\r\n' + str(e)) return
def save(self, **kwargs): try: super().save(filename=self.fullpath.stem) except Exception as e: Dialog().warningMessage('Save crashed with: \r\n' + str(e), **kwargs)