def __init__(self, name: str, *, start: float = None, end: float = None): from gui import PALMS from logic.databases.DatabaseHandler import Database track = Database.get().tracks[Database.get().main_track_label] super().__init__((start, end)) self.setBounds([start, end]) self.track = track self.start = start self.end = end self.mid = self.start + (self.end - self.start) / 2 self.name = name self.label = pg.TextItem(name) self.label.setFont( QFont("", PALMS.config['epoch_labels_font_size'], QFont.Bold)) self.label.setAnchor((0.5, 1)) # self.label.setColor(QColor('k')) try: label_y = EpochModeConfig.get().label_to_ypos[name] except: label_y = self.track.get_yrange_between(self.start, self.end)[0] self.label.setPos(self.mid, label_y) # self.sigRegionChangeFinished.connect(self.region_moved) EpochWindow._instance = weakref.ref(self)() qInfo('Window at [{:0.2f}; {:0.2f}] '.format(self.start, self.end))
def from_click(cls, name: str, *, click_x: float): """construct SinglePartition from single point: calculate its start/end considering other partitions""" from logic.databases.DatabaseHandler import Database from gui import PALMS track = Database.get().tracks[Database.get().main_track_label] initial_span = (track.maxX - track.minX) * 0.01 initial_span = PALMS.config['initial_partition_span_sec'] # NB: set initial span of just created partition left_bound, right_bound = Partitions.calculate_boundaries(click_x, click_x) left_bound = left_bound if left_bound is not None else track.minX right_bound = right_bound if right_bound is not None else track.maxX start = max([max([click_x - initial_span / 2, track.minX]), left_bound]) end = min([min([click_x + initial_span / 2, track.maxX]), right_bound]) return cls(name, start=start, end=end)
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 from_csv(cls, csv): from gui import PALMS settings_init = pd.read_csv(csv) db = Database.get() fiducials = [] for _, row_data in settings_init.iterrows(): assert all([k in row_data for k in PALMS.config['annotationConfig_columns']]) # NB: recover aConf.pinned_to changes from json # it is not needed as one can rewrite annotation config from AnnotationConfigDialog Save button # see also AnnotationConfigDialog where applied data is saved # try: # tmp_singleFiducialConfig = SingleFiducialConfig(row_data) # pinned_to_prev_state = PALMS.config['pinned_to_last_state'][tmp_singleFiducialConfig.name] # pinned_to_prev_state = pinned_to_prev_state.split() # if pinned_to_prev_state[0] in PALMS.config['pinned_to_options'] and \ # pinned_to_prev_state[1] in db.tracks_to_plot_initially: # row_data['pinned_to'] = " ".join(pinned_to_prev_state) # qInfo('Annotation Config updated with config.json data') # except Exception as e: # pass fiducials.append(SingleFiducialConfig(row_data)) aConf = AnnotationConfig.get() aConf.reset_fiducials_config(fiducials) try: # NB: nice to do it here, but Viewer object might still not be created from gui.viewer import Viewer Viewer.get().annotationConfig.aConf_to_table(aConf) Viewer.get().annotationConfig.reset_pinned_to_options_to_existing_views() except: pass return aConf
def move_left(): EpochWindow.hide() from logic.databases.DatabaseHandler import Database from gui.viewer import Viewer from gui.plot_area import PlotArea track = Database.get().tracks[Database.get().main_track_label] success = EpochModeConfig.CURRENT_WINDOW_IDX.decrease() if success: st, en, label = EpochModeConfig.get().get_window_data( EpochModeConfig.CURRENT_WINDOW_IDX.get()) EpochWindow(label, start=st, end=en) while EpochWindow.is_out_of_scope(): Viewer.get().shiftLeft() EpochWindow.show() PlotArea.get_main_view().renderer.plot_area.setFocus()
def select_main_view(self): try: all_views = [ v.track.label for v in self.main_window.selectedPanel.views ] self.selectRow(all_views.index(Database.get().main_track_label) ) # select the main signal again except: pass
def __init__(self, name: str, *, start: float = None, end: float = None): """ constructor when start/end are given explicitly. when created from mouse click (single point) see self.from_click """ from gui import PALMS from logic.databases.DatabaseHandler import Database track = Database.get().tracks[Database.get().main_track_label] left_bound, right_bound = Partitions.calculate_boundaries(start, end) left_bound = left_bound if left_bound is not None else track.minX right_bound = right_bound if right_bound is not None else track.maxX start = max([max([start, track.minX]), left_bound]) end = min([min([end, track.maxX]), right_bound]) super().__init__((start, end)) self.setBounds([left_bound, right_bound]) self.track = track self.start = start self.end = end self.mid = self.start + (self.end - self.start) / 2 self.name = name self.label = pg.TextItem(name) self.label.setFont(QFont("", PALMS.config['partition_labels_font_size'], QFont.Bold)) # self.label.setColor(QColor('k')) self.label.setAnchor((0.5, 1)) self.label.setPos(self.mid, self.track.get_yrange_between(self.start, self.end)[0]) Partitions.add(self) Partitions.update_all_bounds(self) # # update config with new partition name # from gui.viewer import PALMS # PALMS.config['default_partition_labels'] = list( # unique_everseen(PALMS.config['default_partition_labels'] + Partitions.unique_labels())) self.sigRegionChangeFinished.connect(self.region_moved) qInfo('Region {} [{:0.2f}; {:0.2f}] created'.format(self.name, self.start, self.end))
def toggleAllViewsExceptMain(self): # TODO: make a method to get all views over all panels from logic.databases.DatabaseHandler import Database all_views = [v for v in self.main_window.selectedPanel.views] if self.ALL_VIEWS_HIDDEN: for v in all_views: v.show = True self.showView(v) self.ALL_VIEWS_HIDDEN = False else: for v in all_views: if not v.track.label is Database.get().main_track_label: v.show = False self.hideView(v) self.ALL_VIEWS_HIDDEN = True self.display_panel.view_table.update_showCheckbox_state()
def get_main_view(): from gui.viewer import Viewer from logic.databases.DatabaseHandler import Database main_track_label = Database.get().main_track_label if Viewer.get() is None: return None for frame in Viewer.get().frames: all_views = [v for v in frame.displayPanel.panel.views] all_views_labels = [v.track.label for v in all_views] main_track_idx = all_views_labels.index( main_track_label ) if main_track_label in all_views_labels else None if main_track_idx is not None: break if main_track_idx is None: # in case main view is not created yet return None main_view = all_views[main_track_idx] return main_view
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 create_RRinterval_track(self): db = Database.get() to_HR = db.RR_interval_as_HR track_fs = db.tracks[db.main_track_label].fs track_ts = db.tracks[db.main_track_label].ts closest_int_fs = int(min([np.ceil(1 / min(np.diff(self.x))), track_fs])) fs = track_fs # maximum zooming is defined by the lowest sampling freq of all tracks rr_ts = np.arange(track_ts[0], track_ts[-1], 1 / fs) new_ts_idx = np.arange(bisect.bisect_right(rr_ts, self.x[1]), bisect.bisect_left(rr_ts, self.x[-1])) rr = np.zeros_like(rr_ts) from gui.tracking import Wave if to_HR: rr[new_ts_idx] = np.interp(rr_ts[new_ts_idx], self.x[1:], 60 / np.diff(self.x)) rr_int_wave = Wave(rr, fs, rr_ts, offset=0, label='HR(' + self.name + ')', unit='BPM') rr_int_wave.type = 'HR' else: rr[new_ts_idx] = np.interp(rr_ts[new_ts_idx], self.x[1:], np.diff(self.x)) rr_int_wave = Wave(rr, fs, rr_ts, offset=0, label='RR(' + self.name + ')', unit='sec') rr_int_wave.type = 'RR' return rr_int_wave
""" Copyright (c) 2020 Stichting imec Nederland (https://www.imec-int.com/en/imec-the-netherlands) @license GPL-3.0+ <http://spdx.org/licenses/GPL-3.0+> See COPYING, README. """ from pathlib import Path from logic.databases.DatabaseHandler import Database from utils.utils_general import resource_path DATABASE_MODULE_NAME = 'logic.databases' ALL_DATABASES = [c.__name__ for c in Database.__subclasses__()] ICON_PATH = resource_path(Path('config', 'icons', 'PALMS.png')) SHORTCUTS_PATH = resource_path(Path('config', 'shortcuts.json')) CONFIG_PATH = Path('config.json').absolute() # @formatter:off default_config = { 'panel_height': 300, 'yrange_margin': 0.1, 'partition_labels_font_size': 15, 'epoch_labels_font_size': 30, 'min_xzoom_factor': 4, 'autoscale_y': True, 'save_tracks': True, 'save_overwrite': True, "show_cursor": False, "show_xaxis_label": False, "autoplay_timer_interval": 800, "default_mode": "annotation" }
def __init__(self, *, winLen=10, labels: List[str] = None, keys: List[str] = None, default_label=None, description: List[str] = None): if labels is None: labels = ['low', 'good'] if keys is None: keys = list(np.arange(len(labels)).astype(str)) if description is None: description = labels if default_label is not None: if isinstance(default_label, str): if default_label not in labels: default_label = labels[0] qInfo( 'Given default_label is not in given labels. Changed to {} ' .format(default_label)) elif isinstance(default_label, int): if default_label <= len(labels): default_label = labels[default_label] else: default_label = labels[0] qInfo( 'Given default_label is not in given labels. Changed to {} ' .format(default_label)) else: default_label = EpochModeConfig.NONE_LABEL from logic.databases.DatabaseHandler import Database track = Database.get().tracks[Database.get().main_track_label] self.window_data = pd.DataFrame(columns={'start', 'end', 'label'}) self.visuals = [] self.label_to_ypos = {} self.labels_to_keys = {} self.keys_to_labels = {} self.labels_to_color = {} self.toggle_fiducials = True self.window_overlap = 0 # sec self.window_length = winLen # sec self.labels = labels self.default_label = default_label self.keys = keys self.description = description all_starts = np.arange(track.time[0], track.time[-1], self.window_length) all_ends = all_starts + self.window_length if all_ends[-1] > track.time[-1]: all_ends[-1] = track.time[-1] self.window_data['start'] = all_starts self.window_data['end'] = all_ends self.window_data['is_modified'] = 0 self.window_data['label'] = self.default_label self.test_epoch_config() # colors to be assigned to different labels, colors do not mean anything and can be counterintuitive as Good=Red, Bad=Green colors = cycle(['r', 'g', 'b', 'c', 'm', 'y', 'w']) for l, k in zip(self.labels, self.keys): self.labels_to_keys[l] = k self.keys_to_labels[k] = l self.labels_to_color[l] = next(colors) st, en, label = self.get_window_data( EpochModeConfig.CURRENT_WINDOW_IDX.get()) EpochWindow(label, start=st, end=en) self.redraw_epochs() EpochModeConfig._instance = weakref.ref(self)()
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 is_main_view_in_current_panel(self): from logic.databases.DatabaseHandler import Database main_track_label = Database.get().main_track_label return main_track_label in [ s.track.label for s in self.display_panel.panel.views ]
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