class VideoPlayer(QWidget): def __init__(self, aPath, parent=None): super(VideoPlayer, self).__init__(parent) self.setAttribute(Qt.WA_NoSystemBackground, True) self.setAcceptDrops(True) self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.StreamPlayback) self.mediaPlayer.setVolume(80) self.videoWidget = QVideoWidget(self) self.videoWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.videoWidget.setMinimumSize(QSize(640, 360)) self.lbl = QLineEdit('00:00:00') self.lbl.setReadOnly(True) self.lbl.setFixedWidth(70) self.lbl.setUpdatesEnabled(True) self.lbl.setStyleSheet(stylesheet(self)) self.elbl = QLineEdit('00:00:00') self.elbl.setReadOnly(True) self.elbl.setFixedWidth(70) self.elbl.setUpdatesEnabled(True) self.elbl.setStyleSheet(stylesheet(self)) self.playButton = QPushButton() self.playButton.setEnabled(False) self.playButton.setFixedWidth(32) self.playButton.setStyleSheet("background-color: black") self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.playButton.clicked.connect(self.play) self.positionSlider = QSlider(Qt.Horizontal, self) self.positionSlider.setStyleSheet(stylesheet(self)) self.positionSlider.setRange(0, 100) self.positionSlider.sliderMoved.connect(self.setPosition) self.positionSlider.sliderMoved.connect(self.handleLabel) self.positionSlider.setSingleStep(2) self.positionSlider.setPageStep(20) self.positionSlider.setAttribute(Qt.WA_TranslucentBackground, True) self.clip = QApplication.clipboard() self.process = QProcess(self) self.process.readyRead.connect(self.dataReady) self.process.finished.connect(self.playFromURL) self.myurl = "" # channel list self.channelList = QListView(self) self.channelList.setMinimumSize(QSize(150, 0)) self.channelList.setMaximumSize(QSize(150, 4000)) self.channelList.setFrameShape(QFrame.Box) self.channelList.setObjectName("channelList") self.channelList.setStyleSheet("background-color: black; color: #585858;") self.channelList.setFocus() # for adding items to list must create a model self.model = QStandardItemModel() self.channelList.setModel(self.model) self.controlLayout = QHBoxLayout() self.controlLayout.setContentsMargins(5, 0, 5, 0) self.controlLayout.addWidget(self.playButton) self.controlLayout.addWidget(self.lbl) self.controlLayout.addWidget(self.positionSlider) self.controlLayout.addWidget(self.elbl) self.mainLayout = QHBoxLayout() # contains video and cotrol widgets to the left side self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.videoWidget) self.layout.addLayout(self.controlLayout) # adds channels list to the right self.mainLayout.addLayout(self.layout) self.mainLayout.addWidget(self.channelList) self.setLayout(self.mainLayout) self.myinfo = "©2020\nTIVOpy v1.0" self.widescreen = True #### shortcuts #### self.shortcut = QShortcut(QKeySequence("q"), self) self.shortcut.activated.connect(self.handleQuit) self.shortcut = QShortcut(QKeySequence("u"), self) self.shortcut.activated.connect(self.playFromURL) self.shortcut = QShortcut(QKeySequence(Qt.Key_Space), self) self.shortcut.activated.connect(self.play) self.shortcut = QShortcut(QKeySequence(Qt.Key_F), self) self.shortcut.activated.connect(self.handleFullscreen) self.shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self) self.shortcut.activated.connect(self.exitFullscreen) self.shortcut.activated.connect(self.handleFullscreen) self.shortcut = QShortcut(QKeySequence("i"), self) self.shortcut.activated.connect(self.handleInfo) self.shortcut = QShortcut(QKeySequence("s"), self) self.shortcut.activated.connect(self.toggleSlider) self.shortcut = QShortcut(QKeySequence(Qt.Key_Right), self) self.shortcut.activated.connect(self.forwardSlider) self.shortcut = QShortcut(QKeySequence(Qt.Key_Left), self) self.shortcut.activated.connect(self.backSlider) self.mediaPlayer.setVideoOutput(self.videoWidget) self.mediaPlayer.stateChanged.connect(self.mediaStateChanged) self.mediaPlayer.positionChanged.connect(self.positionChanged) self.mediaPlayer.positionChanged.connect(self.handleLabel) self.mediaPlayer.durationChanged.connect(self.durationChanged) self.mediaPlayer.error.connect(self.handleError) self.populateChannelList() self.selectChannel() self.initialPlay() def playFromURL(self): self.mediaPlayer.pause() self.myurl = self.clip.text() self.mediaPlayer.setMedia(QMediaContent(QUrl(self.myurl))) self.playButton.setEnabled(True) self.mediaPlayer.play() self.hideSlider() print(self.myurl) def dataReady(self): self.myurl = str(self.process.readAll(), encoding='utf8').rstrip() ### self.myurl = self.myurl.partition("\n")[0] print(self.myurl) self.clip.setText(self.myurl) self.playFromURL() def play(self): if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.mediaPlayer.pause() else: self.mediaPlayer.play() def mediaStateChanged(self, state): if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.playButton.setIcon( self.style().standardIcon(QStyle.SP_MediaPause)) else: self.playButton.setIcon( self.style().standardIcon(QStyle.SP_MediaPlay)) def positionChanged(self, position): self.positionSlider.setValue(position) def durationChanged(self, duration): self.positionSlider.setRange(0, duration) mtime = QTime(0, 0, 0, 0) mtime = mtime.addMSecs(self.mediaPlayer.duration()) self.elbl.setText(mtime.toString()) def setPosition(self, position): self.mediaPlayer.setPosition(position) def handleError(self): self.playButton.setEnabled(False) print("Error: ", self.mediaPlayer.errorString()) def handleQuit(self): self.mediaPlayer.stop() print("Goodbye ...") app.quit() def contextMenuRequested(self, point): menu = QMenu() actionURL = menu.addAction(QIcon.fromTheme("browser"), "URL from Clipboard (u)") menu.addSeparator() actionToggle = menu.addAction(QIcon.fromTheme("next"), "Show / Hide Channels (s)") actionFull = menu.addAction(QIcon.fromTheme("view-fullscreen"), "Fullscreen (f)") menu.addSeparator() actionInfo = menu.addAction(QIcon.fromTheme("help-about"), "About (i)") menu.addSeparator() actionQuit = menu.addAction(QIcon.fromTheme("application-exit"), "Exit (q)") actionQuit.triggered.connect(self.handleQuit) actionFull.triggered.connect(self.handleFullscreen) actionInfo.triggered.connect(self.handleInfo) actionToggle.triggered.connect(self.toggleSlider) actionURL.triggered.connect(self.playFromURL) menu.exec_(self.mapToGlobal(point)) def wheelEvent(self, event): mscale = event.angleDelta().y() / 13 self.mediaPlayer.setVolume(self.mediaPlayer.volume() + mscale) print("Volume: " + str(self.mediaPlayer.volume())) def mouseDoubleClickEvent(self, event): if event.buttons() == Qt.LeftButton: self.handleFullscreen() def handleFullscreen(self): if self.windowState() and Qt.WindowFullScreen: self.showNormal() else: self.showFullScreen() def exitFullscreen(self): self.showNormal() def handleInfo(self): QMessageBox.about(self, "About", self.myinfo) def toggleSlider(self): if self.positionSlider.isVisible(): self.hideSlider() else: self.showSlider() def hideSlider(self): self.channelList.hide() self.playButton.hide() self.lbl.hide() self.positionSlider.hide() self.elbl.hide() def showSlider(self): self.channelList.show() self.playButton.show() self.lbl.show() self.positionSlider.show() self.elbl.show() self.channelList.setFocus() def forwardSlider(self): self.mediaPlayer.setPosition(self.mediaPlayer.position() + 1000 * 60) def backSlider(self): self.mediaPlayer.setPosition(self.mediaPlayer.position() - 1000 * 60) def volumeUp(self): self.mediaPlayer.setVolume(self.mediaPlayer.volume() + 10) print("Volume: " + str(self.mediaPlayer.volume())) def volumeDown(self): self.mediaPlayer.setVolume(self.mediaPlayer.volume() - 10) print("Volume: " + str(self.mediaPlayer.volume())) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() elif event.mimeData().hasText(): event.accept() else: event.ignore() def dropEvent(self, event): print("drop") if event.mimeData().hasUrls(): url = event.mimeData().urls()[0].toString() print("url = ", url) self.mediaPlayer.stop() self.mediaPlayer.setMedia(QMediaContent(QUrl(url))) self.playButton.setEnabled(True) self.mediaPlayer.play() elif event.mimeData().hasText(): mydrop = event.mimeData().text() print("generic url = ", mydrop) self.mediaPlayer.setMedia(QMediaContent(QUrl(mydrop))) self.playButton.setEnabled(True) self.mediaPlayer.play() self.hideSlider() def loadFilm(self, f): self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(f))) self.playButton.setEnabled(True) self.mediaPlayer.play() def populateChannelList(self): # file must be in same directory as the script FILEPATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "canaletv.txt") # lines from file with "channel name" -- "link" channelArray = [] # split file by line and adding it to the array with open(FILEPATH) as f: for line in f: channelArray.append(line.rstrip()) # dictionary with key = channel name and value = link self.channelDict = dict(ch.split(" -- ") for ch in channelArray) for channel in self.channelDict.keys(): item = QStandardItem(channel) self.model.appendRow(item) def selectedItemBehavior(self, index): # gets the link for the selected channel and plays it itms = self.channelList.selectedIndexes() for it in itms: channel = it.data() link = self.channelDict[channel] self.mediaPlayer.setMedia(QMediaContent(QUrl(link))) self.play() def selectChannel(self): # selecting channel from sidebar calls selectedItemBehavior self.selModel = self.channelList.selectionModel() self.selModel.selectionChanged.connect(self.selectedItemBehavior) def initialPlay(self): # play somenting when app opens self.mediaPlayer.setMedia(QMediaContent(QUrl("https://vid.hls.protv.ro/proxhdn/proxhd_3_34/index.m3u8?1"))) self.play() def handleLabel(self): self.lbl.clear() mtime = QTime(0, 0, 0, 0) self.time = mtime.addMSecs(self.mediaPlayer.position()) self.lbl.setText(self.time.toString())
class IntracranialElectrodeLocator(QMainWindow): """Locate electrode contacts using a coregistered MRI and CT.""" _xy_idx = ( (1, 2), (0, 2), (0, 1), ) def __init__(self, info, trans, aligned_ct, subject=None, subjects_dir=None, groups=None, verbose=None): """GUI for locating intracranial electrodes. .. note:: Images will be displayed using orientation information obtained from the image header. Images will be resampled to dimensions [256, 256, 256] for display. """ # initialize QMainWindow class super(IntracranialElectrodeLocator, self).__init__() if not info.ch_names: raise ValueError('No channels found in `info` to locate') # store info for modification self._info = info self._seeg_idx = pick_types(self._info, meg=False, seeg=True) self._verbose = verbose # channel plotting default parameters self._ch_alpha = 0.5 self._radius = int(_CH_PLOT_SIZE // 100) # starting 1/100 of image # load imaging data self._subject_dir = op.join(subjects_dir, subject) self._load_image_data(aligned_ct) # initialize channel data self._ch_index = 0 # load data, apply trans self._head_mri_t = _get_trans(trans, 'head', 'mri')[0] self._mri_head_t = invert_transform(self._head_mri_t) # load channels, convert from m to mm self._chs = { name: apply_trans(self._head_mri_t, ch['loc'][:3]) * 1000 for name, ch in zip(info.ch_names, info['chs']) } self._ch_names = list(self._chs.keys()) # set current position if np.isnan(self._chs[self._ch_names[self._ch_index]]).any(): ras = [0., 0., 0.] else: ras = self._chs[self._ch_names[self._ch_index]] self._set_ras(ras, update_plots=False) self._group_channels(groups) # GUI design # Main plots: make one plot for each view; sagittal, coronal, axial plt_grid = QGridLayout() plts = [_make_slice_plot(), _make_slice_plot(), _make_slice_plot()] self._figs = [plts[0][1], plts[1][1], plts[2][1]] plt_grid.addWidget(plts[0][0], 0, 0) plt_grid.addWidget(plts[1][0], 0, 1) plt_grid.addWidget(plts[2][0], 1, 0) self._renderer = _get_renderer(name='IEEG Locator', size=(400, 400), bgcolor='w') plt_grid.addWidget(self._renderer.plotter) # Channel selector self._ch_list = QListView() self._ch_list.setSelectionMode(Qt.QAbstractItemView.SingleSelection) max_ch_name_len = max([len(name) for name in self._chs]) self._ch_list.setMinimumWidth(max_ch_name_len * _CH_MENU_WIDTH) self._ch_list.setMaximumWidth(max_ch_name_len * _CH_MENU_WIDTH) self._set_ch_names() # Plots self._plot_images() # Menus button_hbox = self._get_button_bar() slider_hbox = self._get_slider_bar() bottom_hbox = self._get_bottom_bar() # Add lines self._lines = dict() self._lines_2D = dict() for group in set(self._groups.values()): self._update_lines(group) # Put everything together plot_ch_hbox = QHBoxLayout() plot_ch_hbox.addLayout(plt_grid) plot_ch_hbox.addWidget(self._ch_list) main_vbox = QVBoxLayout() main_vbox.addLayout(button_hbox) main_vbox.addLayout(slider_hbox) main_vbox.addLayout(plot_ch_hbox) main_vbox.addLayout(bottom_hbox) central_widget = QWidget() central_widget.setLayout(main_vbox) self.setCentralWidget(central_widget) # ready for user self._move_cursors_to_pos() self._ch_list.setFocus() # always focus on list def _load_image_data(self, ct): """Get MRI and CT data to display and transforms to/from vox/RAS.""" # allows recon-all not to be finished (T1 made in a few minutes) mri_img = 'brain' if op.isfile( op.join(self._subject_dir, 'mri', 'brain.mgz')) else 'T1' self._mri_data, self._vox_ras_t = _load_image(op.join( self._subject_dir, 'mri', f'{mri_img}.mgz'), 'MRI Image', verbose=self._verbose) self._ras_vox_t = np.linalg.inv(self._vox_ras_t) self._voxel_sizes = np.array(self._mri_data.shape) # We need our extents to land the centers of each pixel on the voxel # number. This code assumes 1mm isotropic... img_delta = 0.5 self._img_extents = list([ -img_delta, self._voxel_sizes[idx[0]] - img_delta, -img_delta, self._voxel_sizes[idx[1]] - img_delta ] for idx in self._xy_idx) ch_deltas = list(img_delta * (self._voxel_sizes[ii] / _CH_PLOT_SIZE) for ii in range(3)) self._ch_extents = list([ -ch_delta, self._voxel_sizes[idx[0]] - ch_delta, -ch_delta, self._voxel_sizes[idx[1]] - ch_delta ] for idx, ch_delta in zip(self._xy_idx, ch_deltas)) # ready ct self._ct_data, vox_ras_t = _load_image(ct, 'CT', verbose=self._verbose) if self._mri_data.shape != self._ct_data.shape or \ not np.allclose(self._vox_ras_t, vox_ras_t, rtol=1e-6): raise ValueError('CT is not aligned to MRI, got ' f'CT shape={self._ct_data.shape}, ' f'MRI shape={self._mri_data.shape}, ' f'CT affine={vox_ras_t} and ' f'MRI affine={self._vox_ras_t}') self._ct_maxima = None # don't compute until turned on if op.exists(op.join(self._subject_dir, 'surf', 'lh.seghead')): self._head = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.seghead')) assert _frame_to_str[self._head['coord_frame']] == 'mri' else: warn('`seghead` not found, using marching cubes on CT for ' 'head plot, use :ref:`mne.bem.make_scalp_surfaces` ' 'to add the scalp surface instead of skull from the CT') self._head = None if op.exists(op.join(self._subject_dir, 'surf', 'lh.pial')): self._lh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.pial')) assert _frame_to_str[self._lh['coord_frame']] == 'mri' self._rh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'rh.pial')) assert _frame_to_str[self._rh['coord_frame']] == 'mri' else: warn('`pial` surface not found, skipping adding to 3D ' 'plot. This indicates the Freesurfer recon-all ' 'has not finished or has been modified and ' 'these files have been deleted.') self._lh = self._rh = None def _make_ch_image(self, axis, proj=False): """Make a plot to display the channel locations.""" # Make channel data higher resolution so it looks better. ch_image = np.zeros((_CH_PLOT_SIZE, _CH_PLOT_SIZE)) * np.nan vxyz = self._voxel_sizes def color_ch_radius(ch_image, xf, yf, group, radius): # Take the fraction across each dimension of the RAS # coordinates converted to xyz and put a circle in that # position in this larger resolution image ex, ey = np.round(np.array([xf, yf]) * _CH_PLOT_SIZE).astype(int) ii = np.arange(-radius, radius + 1) ii_sq = ii * ii idx = np.where(ii_sq + ii_sq[:, np.newaxis] < radius * radius) # negative y because y axis is inverted ch_image[-(ey + ii[idx[1]]), ex + ii[idx[0]]] = group return ch_image for name, ras in self._chs.items(): # move from middle-centered (half coords positive, half negative) # to bottom-left corner centered (all coords positive). if np.isnan(ras).any(): continue xyz = apply_trans(self._ras_vox_t, ras) # check if closest to that voxel dist = np.linalg.norm(xyz - self._current_slice) if proj or dist < self._radius: group = self._groups[name] r = self._radius if proj else \ self._radius - np.round(abs(dist)).astype(int) xf, yf = (xyz / vxyz)[list(self._xy_idx[axis])] ch_image = color_ch_radius(ch_image, xf, yf, group, r) return ch_image @verbose def _save_ch_coords(self, info=None, verbose=None): """Save the location of the electrode contacts.""" logger.info('Saving channel positions to `info`') if info is None: info = self._info with info._unlock(): for name, ch in zip(info.ch_names, info['chs']): ch['loc'][:3] = apply_trans(self._mri_head_t, self._chs[name] / 1000) # mm->m def _plot_images(self): """Use the MRI and CT to make plots.""" # Plot sagittal (0), coronal (1) or axial (2) view self._images = dict(ct=list(), chs=list(), ct_bounds=list(), cursor_v=list(), cursor_h=list()) ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) text_kwargs = dict(fontsize='medium', weight='bold', color='#66CCEE', family='monospace', ha='center', va='center', path_effects=[ patheffects.withStroke(linewidth=4, foreground="k", alpha=0.75) ]) xyz = apply_trans(self._ras_vox_t, self._ras) for axis in range(3): plot_x_idx, plot_y_idx = self._xy_idx[axis] fig = self._figs[axis] ax = fig.axes[0] ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T self._images['ct'].append( ax.imshow(ct_data, cmap='gray', aspect='auto', zorder=1, vmin=ct_min, vmax=ct_max)) img_extent = self._img_extents[axis] # x0, x1, y0, y1 w, h = np.diff(np.array(img_extent).reshape(2, 2), axis=1)[:, 0] self._images['ct_bounds'].append( Rectangle(img_extent[::2], w, h, edgecolor='w', facecolor='none', alpha=0.25, lw=0.5, zorder=1.5)) ax.add_patch(self._images['ct_bounds'][-1]) self._images['chs'].append( ax.imshow(self._make_ch_image(axis), aspect='auto', extent=self._ch_extents[axis], zorder=3, cmap=_CMAP, alpha=self._ch_alpha, vmin=0, vmax=_N_COLORS)) v_x = (xyz[plot_x_idx], ) * 2 v_y = img_extent[2:4] self._images['cursor_v'].append( ax.plot(v_x, v_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) h_y = (xyz[plot_y_idx], ) * 2 h_x = img_extent[0:2] self._images['cursor_h'].append( ax.plot(h_x, h_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) # label axes self._figs[axis].text(0.5, 0.05, _IMG_LABELS[axis][0], **text_kwargs) self._figs[axis].text(0.05, 0.5, _IMG_LABELS[axis][1], **text_kwargs) self._figs[axis].axes[0].axis(img_extent) self._figs[axis].canvas.mpl_connect('scroll_event', self._on_scroll) self._figs[axis].canvas.mpl_connect( 'button_release_event', partial(self._on_click, axis=axis)) # add head and brain in mm (convert from m) if self._head is None: logger.info('Using marching cubes on CT for the ' '3D visualization panel') rr, tris = _marching_cubes( np.where(self._ct_data < np.quantile(self._ct_data, 0.95), 0, 1), [1])[0] rr = apply_trans(self._vox_ras_t, rr) self._renderer.mesh(*rr.T, triangles=tris, color='gray', opacity=0.2, reset_camera=False, render=False) else: self._renderer.mesh(*self._head['rr'].T * 1000, triangles=self._head['tris'], color='gray', opacity=0.2, reset_camera=False, render=False) if self._lh is not None and self._rh is not None: self._renderer.mesh(*self._lh['rr'].T * 1000, triangles=self._lh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.mesh(*self._rh['rr'].T * 1000, triangles=self._rh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._3d_chs = dict() for name in self._chs: self._plot_3d_ch(name) self._renderer.set_camera(azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)) # update plots self._draw() self._renderer._update() def _update_camera(self, render=False): """Update the camera position.""" self._renderer.set_camera( # needs fix, distance moves when focal point updates distance=self._renderer.plotter.camera.distance * 0.9, focalpoint=tuple(self._ras), reset_camera=False) def _plot_3d_ch(self, name, render=False): """Plot a single 3D channel.""" if name in self._3d_chs: self._renderer.plotter.remove_actor(self._3d_chs.pop(name), render=False) if not any(np.isnan(self._chs[name])): self._3d_chs[name] = self._renderer.sphere( tuple(self._chs[name]), scale=1, color=_CMAP(self._groups[name])[:3], opacity=self._ch_alpha)[0] # The actor scale is managed differently than the glyph scale # in order not to recreate objects, we use the actor scale self._3d_chs[name].SetOrigin(self._chs[name]) self._3d_chs[name].SetScale(self._radius * _RADIUS_SCALAR) if render: self._renderer._update() def _get_button_bar(self): """Make a bar with buttons for user interactions.""" hbox = QHBoxLayout() help_button = QPushButton('Help') help_button.released.connect(self._show_help) hbox.addWidget(help_button) hbox.addStretch(8) hbox.addWidget(QLabel('Snap to Center')) self._snap_button = QPushButton('Off') self._snap_button.setMaximumWidth(25) # not too big hbox.addWidget(self._snap_button) self._snap_button.released.connect(self._toggle_snap) self._toggle_snap() # turn on to start hbox.addStretch(1) self._toggle_brain_button = QPushButton('Show Brain') self._toggle_brain_button.released.connect(self._toggle_show_brain) hbox.addWidget(self._toggle_brain_button) hbox.addStretch(1) mark_button = QPushButton('Mark') hbox.addWidget(mark_button) mark_button.released.connect(self._mark_ch) remove_button = QPushButton('Remove') hbox.addWidget(remove_button) remove_button.released.connect(self._remove_ch) self._group_selector = ComboBox() group_model = self._group_selector.model() for i in range(_N_COLORS): self._group_selector.addItem(' ') color = QtGui.QColor() color.setRgb(*(255 * np.array(_CMAP(i))).round().astype(int)) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) group_model.setData(group_model.index(i, 0), brush, QtCore.Qt.BackgroundRole) self._group_selector.clicked.connect(self._select_group) self._group_selector.currentIndexChanged.connect(self._select_group) hbox.addWidget(self._group_selector) # update background color for current selection self._update_group() return hbox def _get_slider_bar(self): """Make a bar with sliders on it.""" def make_label(name): label = QLabel(name) label.setAlignment(QtCore.Qt.AlignCenter) return label def make_slider(smin, smax, sval, sfun=None): slider = QSlider(QtCore.Qt.Horizontal) slider.setMinimum(int(round(smin))) slider.setMaximum(int(round(smax))) slider.setValue(int(round(sval))) slider.setTracking(False) # only update on release if sfun is not None: slider.valueChanged.connect(sfun) slider.keyPressEvent = self._key_press_event return slider slider_hbox = QHBoxLayout() ch_vbox = QVBoxLayout() ch_vbox.addWidget(make_label('ch alpha')) ch_vbox.addWidget(make_label('ch radius')) slider_hbox.addLayout(ch_vbox) ch_slider_vbox = QVBoxLayout() self._alpha_slider = make_slider(0, 100, self._ch_alpha * 100, self._update_ch_alpha) ch_plot_max = _CH_PLOT_SIZE // 50 # max 1 / 50 of plot size ch_slider_vbox.addWidget(self._alpha_slider) self._radius_slider = make_slider(0, ch_plot_max, self._radius, self._update_radius) ch_slider_vbox.addWidget(self._radius_slider) slider_hbox.addLayout(ch_slider_vbox) ct_vbox = QVBoxLayout() ct_vbox.addWidget(make_label('CT min')) ct_vbox.addWidget(make_label('CT max')) slider_hbox.addLayout(ct_vbox) ct_slider_vbox = QVBoxLayout() ct_min = int(round(np.nanmin(self._ct_data))) ct_max = int(round(np.nanmax(self._ct_data))) self._ct_min_slider = make_slider(ct_min, ct_max, ct_min, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_min_slider) self._ct_max_slider = make_slider(ct_min, ct_max, ct_max, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_max_slider) slider_hbox.addLayout(ct_slider_vbox) return slider_hbox def _get_bottom_bar(self): """Make a bar at the bottom with information in it.""" hbox = QHBoxLayout() hbox.addStretch(3) self._toggle_show_mip_button = QPushButton('Show Max Intensity Proj') self._toggle_show_mip_button.released.connect(self._toggle_show_mip) hbox.addWidget(self._toggle_show_mip_button) self._toggle_show_max_button = QPushButton('Show Maxima') self._toggle_show_max_button.released.connect(self._toggle_show_max) hbox.addWidget(self._toggle_show_max_button) self._intensity_label = QLabel('') # update later hbox.addWidget(self._intensity_label) VOX_label = QLabel('VOX =') self._VOX_textbox = QPlainTextEdit('') # update later self._VOX_textbox.setMaximumHeight(25) self._VOX_textbox.setMaximumWidth(125) self._VOX_textbox.focusOutEvent = self._update_VOX self._VOX_textbox.textChanged.connect(self._check_update_VOX) hbox.addWidget(VOX_label) hbox.addWidget(self._VOX_textbox) RAS_label = QLabel('RAS =') self._RAS_textbox = QPlainTextEdit('') # update later self._RAS_textbox.setMaximumHeight(25) self._RAS_textbox.setMaximumWidth(200) self._RAS_textbox.focusOutEvent = self._update_RAS self._RAS_textbox.textChanged.connect(self._check_update_RAS) hbox.addWidget(RAS_label) hbox.addWidget(self._RAS_textbox) self._update_moved() # update text now return hbox def _group_channels(self, groups): """Automatically find a group based on the name of the channel.""" if groups is not None: for name in self._ch_names: if name not in groups: raise ValueError(f'{name} not found in ``groups``') _validate_type(groups[name], (float, int), f'groups[{name}]') self.groups = groups else: i = 0 self._groups = dict() base_names = dict() for name in self._ch_names: # strip all numbers from the name base_name = ''.join([ letter for letter in name if not letter.isdigit() and letter != ' ' ]) if base_name in base_names: # look up group number by base name self._groups[name] = base_names[base_name] else: self._groups[name] = i base_names[base_name] = i i += 1 def _update_lines(self, group, only_2D=False): """Draw lines that connect the points in a group.""" if group in self._lines_2D: # remove existing 2D lines first for line in self._lines_2D[group]: line.remove() self._lines_2D.pop(group) if only_2D: # if not in projection, don't add 2D lines if self._toggle_show_mip_button.text() == \ 'Show Max Intensity Proj': return elif group in self._lines: # if updating 3D, remove first self._renderer.plotter.remove_actor(self._lines[group], render=False) pos = np.array([ self._chs[ch] for i, ch in enumerate(self._ch_names) if self._groups[ch] == group and i in self._seeg_idx and not np.isnan(self._chs[ch]).any() ]) if len(pos) < 2: # not enough points for line return # first, the insertion will be the point farthest from the origin # brains are a longer posterior-anterior, scale for this (80%) insert_idx = np.argmax( np.linalg.norm(pos * np.array([1, 0.8, 1]), axis=1)) # second, find the farthest point from the insertion target_idx = np.argmax(np.linalg.norm(pos[insert_idx] - pos, axis=1)) # third, make a unit vector and to add to the insertion for the bolt elec_v = pos[insert_idx] - pos[target_idx] elec_v /= np.linalg.norm(elec_v) if not only_2D: self._lines[group] = self._renderer.tube( [pos[target_idx]], [pos[insert_idx] + elec_v * _BOLT_SCALAR], radius=self._radius * _TUBE_SCALAR, color=_CMAP(group)[:3])[0] if self._toggle_show_mip_button.text() == 'Hide Max Intensity Proj': # add 2D lines on each slice plot if in max intensity projection target_vox = apply_trans(self._ras_vox_t, pos[target_idx]) insert_vox = apply_trans(self._ras_vox_t, pos[insert_idx] + elec_v * _BOLT_SCALAR) lines_2D = list() for axis in range(3): x, y = self._xy_idx[axis] lines_2D.append(self._figs[axis].axes[0].plot( [target_vox[x], insert_vox[x]], [target_vox[y], insert_vox[y]], color=_CMAP(group), linewidth=0.25, zorder=7)[0]) self._lines_2D[group] = lines_2D def _set_ch_names(self): """Add the channel names to the selector.""" self._ch_list_model = QtGui.QStandardItemModel(self._ch_list) for name in self._ch_names: self._ch_list_model.appendRow(QtGui.QStandardItem(name)) self._color_list_item(name=name) self._ch_list.setModel(self._ch_list_model) self._ch_list.clicked.connect(self._go_to_ch) self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._ch_list.keyPressEvent = self._key_press_event def _select_group(self): """Change the group label to the selection.""" group = self._group_selector.currentIndex() self._groups[self._ch_names[self._ch_index]] = group # color differently if found already self._color_list_item(self._ch_names[self._ch_index]) self._update_group() def _update_group(self): """Set background for closed group menu.""" group = self._group_selector.currentIndex() rgb = (255 * np.array(_CMAP(group))).round().astype(int) self._group_selector.setStyleSheet( 'background-color: rgb({:d},{:d},{:d})'.format(*rgb)) self._group_selector.update() def _on_scroll(self, event): """Process mouse scroll wheel event to zoom.""" self._zoom(event.step, draw=True) def _zoom(self, sign=1, draw=False): """Zoom in on the image.""" delta = _ZOOM_STEP_SIZE * sign for axis, fig in enumerate(self._figs): xmid = self._images['cursor_v'][axis].get_xdata()[0] ymid = self._images['cursor_h'][axis].get_ydata()[0] xmin, xmax = fig.axes[0].get_xlim() ymin, ymax = fig.axes[0].get_ylim() xwidth = (xmax - xmin) / 2 - delta ywidth = (ymax - ymin) / 2 - delta if xwidth <= 0 or ywidth <= 0: return fig.axes[0].set_xlim(xmid - xwidth, xmid + xwidth) fig.axes[0].set_ylim(ymid - ywidth, ymid + ywidth) if draw: self._figs[axis].canvas.draw() def _update_ch_selection(self): """Update which channel is selected.""" name = self._ch_names[self._ch_index] self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._group_selector.setCurrentIndex(self._groups[name]) self._update_group() if not np.isnan(self._chs[name]).any(): self._set_ras(self._chs[name]) self._update_camera(render=True) self._draw() def _go_to_ch(self, index): """Change current channel to the item selected.""" self._ch_index = index.row() self._update_ch_selection() @pyqtSlot() def _next_ch(self): """Increment the current channel selection index.""" self._ch_index = (self._ch_index + 1) % len(self._ch_names) self._update_ch_selection() @pyqtSlot() def _update_RAS(self, event): """Interpret user input to the RAS textbox.""" text = self._RAS_textbox.toPlainText() ras = self._convert_text(text, 'ras') if ras is not None: self._set_ras(ras) @pyqtSlot() def _update_VOX(self, event): """Interpret user input to the RAS textbox.""" text = self._VOX_textbox.toPlainText() ras = self._convert_text(text, 'vox') if ras is not None: self._set_ras(ras) def _convert_text(self, text, text_kind): text = text.replace('\n', '') vals = text.split(',') if len(vals) != 3: vals = text.split(' ') # spaces also okay as in freesurfer vals = [var.lstrip().rstrip() for var in vals] try: vals = np.array([float(var) for var in vals]).reshape(3) except Exception: self._update_moved() # resets RAS label return if text_kind == 'vox': vox = vals ras = apply_trans(self._vox_ras_t, vox) else: assert text_kind == 'ras' ras = vals vox = apply_trans(self._ras_vox_t, ras) wrong_size = any(var < 0 or var > n - 1 for var, n in zip(vox, self._voxel_sizes)) if wrong_size: self._update_moved() # resets RAS label return return ras @property def _ras(self): return self._ras_safe def _set_ras(self, ras, update_plots=True): ras = np.asarray(ras, dtype=float) assert ras.shape == (3, ) msg = ', '.join(f'{x:0.2f}' for x in ras) logger.debug(f'Trying RAS: ({msg}) mm') # clip to valid vox = apply_trans(self._ras_vox_t, ras) vox = np.array([ np.clip(d, 0, self._voxel_sizes[ii] - 1) for ii, d in enumerate(vox) ]) # transform back, make write-only self._ras_safe = apply_trans(self._vox_ras_t, vox) self._ras_safe.flags['WRITEABLE'] = False msg = ', '.join(f'{x:0.2f}' for x in self._ras_safe) logger.debug(f'Setting RAS: ({msg}) mm') if update_plots: self._move_cursors_to_pos() @property def _vox(self): return apply_trans(self._ras_vox_t, self._ras) @property def _current_slice(self): return self._vox.round().astype(int) @pyqtSlot() def _check_update_RAS(self): """Check whether the RAS textbox is done being edited.""" if '\n' in self._RAS_textbox.toPlainText(): self._update_RAS(event=None) self._ch_list.setFocus() # remove focus from text edit @pyqtSlot() def _check_update_VOX(self): """Check whether the VOX textbox is done being edited.""" if '\n' in self._VOX_textbox.toPlainText(): self._update_VOX(event=None) self._ch_list.setFocus() # remove focus from text edit def _color_list_item(self, name=None): """Color the item in the view list for easy id of marked channels.""" name = self._ch_names[self._ch_index] if name is None else name color = QtGui.QColor('white') if not np.isnan(self._chs[name]).any(): group = self._groups[name] color.setRgb(*[int(c * 255) for c in _CMAP(group)]) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.BackgroundRole) # color text black color = QtGui.QColor('black') brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.ForegroundRole) @pyqtSlot() def _toggle_snap(self): """Toggle snapping the contact location to the center of mass.""" if self._snap_button.text() == 'Off': self._snap_button.setText('On') self._snap_button.setStyleSheet("background-color: green") else: # text == 'On', turn off self._snap_button.setText('Off') self._snap_button.setStyleSheet("background-color: red") @pyqtSlot() def _mark_ch(self): """Mark the current channel as being located at the crosshair.""" name = self._ch_names[self._ch_index] if self._snap_button.text() == 'Off': self._chs[name][:] = self._ras else: shape = np.mean(self._mri_data.shape) # Freesurfer shape (256) voxels_max = int(4 / 3 * np.pi * (shape * self._radius / _CH_PLOT_SIZE)**3) neighbors = _voxel_neighbors(self._vox, self._ct_data, thresh=0.5, voxels_max=voxels_max, use_relative=True) self._chs[name][:] = apply_trans( # to surface RAS self._vox_ras_t, np.array(list(neighbors)).mean(axis=0)) self._color_list_item() self._update_lines(self._groups[name]) self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._save_ch_coords() self._next_ch() self._ch_list.setFocus() @pyqtSlot() def _remove_ch(self): """Remove the location data for the current channel.""" name = self._ch_names[self._ch_index] self._chs[name] *= np.nan self._color_list_item() self._save_ch_coords() self._update_lines(self._groups[name]) self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._next_ch() self._ch_list.setFocus() def _draw(self, axis=None): """Update the figures with a draw call.""" for axis in (range(3) if axis is None else [axis]): self._figs[axis].canvas.draw() def _update_ch_images(self, axis=None, draw=False): """Update the channel image(s).""" for axis in range(3) if axis is None else [axis]: self._images['chs'][axis].set_data(self._make_ch_image(axis)) if self._toggle_show_mip_button.text() == \ 'Hide Max Intensity Proj': self._images['mip_chs'][axis].set_data( self._make_ch_image(axis, proj=True)) if draw: self._draw(axis) def _update_ct_images(self, axis=None, draw=False): """Update the CT image(s).""" for axis in range(3) if axis is None else [axis]: ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T # Threshold the CT so only bright objects (electrodes) are visible ct_data[ct_data < self._ct_min_slider.value()] = np.nan ct_data[ct_data > self._ct_max_slider.value()] = np.nan self._images['ct'][axis].set_data(ct_data) if 'local_max' in self._images: ct_max_data = np.take(self._ct_maxima, self._current_slice[axis], axis=axis).T self._images['local_max'][axis].set_data(ct_max_data) if draw: self._draw(axis) def _update_mri_images(self, axis=None, draw=False): """Update the CT image(s).""" if 'mri' in self._images: for axis in range(3) if axis is None else [axis]: self._images['mri'][axis].set_data( np.take(self._mri_data, self._current_slice[axis], axis=axis).T) if draw: self._draw(axis) def _update_images(self, axis=None, draw=True): """Update CT and channel images when general changes happen.""" self._update_ct_images(axis=axis) self._update_ch_images(axis=axis) self._update_mri_images(axis=axis) if draw: self._draw(axis) def _update_ct_scale(self): """Update CT min slider value.""" new_min = self._ct_min_slider.value() new_max = self._ct_max_slider.value() # handle inversions self._ct_min_slider.setValue(min([new_min, new_max])) self._ct_max_slider.setValue(max([new_min, new_max])) self._update_ct_images(draw=True) def _update_radius(self): """Update channel plot radius.""" self._radius = np.round(self._radius_slider.value()).astype(int) if self._toggle_show_max_button.text() == 'Hide Maxima': self._update_ct_maxima() self._update_ct_images() else: self._ct_maxima = None # signals ct max is out-of-date self._update_ch_images(draw=True) for name, actor in self._3d_chs.items(): if not np.isnan(self._chs[name]).any(): actor.SetOrigin(self._chs[name]) actor.SetScale(self._radius * _RADIUS_SCALAR) self._renderer._update() self._ch_list.setFocus() # remove focus from 3d plotter def _update_ch_alpha(self): """Update channel plot alpha.""" self._ch_alpha = self._alpha_slider.value() / 100 for axis in range(3): self._images['chs'][axis].set_alpha(self._ch_alpha) self._draw() for actor in self._3d_chs.values(): actor.GetProperty().SetOpacity(self._ch_alpha) self._renderer._update() self._ch_list.setFocus() # remove focus from 3d plotter def _move_cursors_to_pos(self): """Move the cursors to a position.""" for axis in range(3): x, y = self._vox[list(self._xy_idx[axis])] self._images['cursor_v'][axis].set_xdata([x, x]) self._images['cursor_h'][axis].set_ydata([y, y]) self._zoom(0) # doesn't actually zoom just resets view to center self._update_images(draw=True) self._update_moved() def _show_help(self): """Show the help menu.""" QMessageBox.information( self, 'Help', "Help:\n'm': mark channel location\n" "'r': remove channel location\n" "'b': toggle viewing of brain in T1\n" "'+'/'-': zoom\nleft/right arrow: left/right\n" "up/down arrow: superior/inferior\n" "left angle bracket/right angle bracket: anterior/posterior") def _update_ct_maxima(self): """Compute the maximum voxels based on the current radius.""" self._ct_maxima = maximum_filter(self._ct_data, (self._radius, ) * 3) == self._ct_data self._ct_maxima[self._ct_data <= np.median(self._ct_data)] = \ False self._ct_maxima = np.where(self._ct_maxima, 1, np.nan) # transparent def _toggle_show_mip(self): """Toggle whether the maximum-intensity projection is shown.""" if self._toggle_show_mip_button.text() == 'Show Max Intensity Proj': self._toggle_show_mip_button.setText('Hide Max Intensity Proj') self._images['mip'] = list() self._images['mip_chs'] = list() ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) for axis in range(3): ct_mip_data = np.max(self._ct_data, axis=axis).T self._images['mip'].append(self._figs[axis].axes[0].imshow( ct_mip_data, cmap='gray', aspect='auto', vmin=ct_min, vmax=ct_max, zorder=5)) # add circles for each channel xs, ys, colors = list(), list(), list() for name, ras in self._chs.items(): xyz = self._vox xs.append(xyz[self._xy_idx[axis][0]]) ys.append(xyz[self._xy_idx[axis][1]]) colors.append(_CMAP(self._groups[name])) self._images['mip_chs'].append(self._figs[axis].axes[0].imshow( self._make_ch_image(axis, proj=True), aspect='auto', extent=self._ch_extents[axis], zorder=6, cmap=_CMAP, alpha=1, vmin=0, vmax=_N_COLORS)) for group in set(self._groups.values()): self._update_lines(group, only_2D=True) else: for img in self._images['mip'] + self._images['mip_chs']: img.remove() self._images.pop('mip') self._images.pop('mip_chs') self._toggle_show_mip_button.setText('Show Max Intensity Proj') for group in set(self._groups.values()): # remove lines self._update_lines(group, only_2D=True) self._draw() def _toggle_show_max(self): """Toggle whether to color local maxima differently.""" if self._toggle_show_max_button.text() == 'Show Maxima': self._toggle_show_max_button.setText('Hide Maxima') # happens on initiation or if the radius is changed with it off if self._ct_maxima is None: # otherwise don't recompute self._update_ct_maxima() self._images['local_max'] = list() for axis in range(3): ct_max_data = np.take(self._ct_maxima, self._current_slice[axis], axis=axis).T self._images['local_max'].append( self._figs[axis].axes[0].imshow(ct_max_data, cmap='autumn', aspect='auto', vmin=0, vmax=1, zorder=4)) else: for img in self._images['local_max']: img.remove() self._images.pop('local_max') self._toggle_show_max_button.setText('Show Maxima') self._draw() def _toggle_show_brain(self): """Toggle whether the brain/MRI is being shown.""" if 'mri' in self._images: for img in self._images['mri']: img.remove() self._images.pop('mri') self._toggle_brain_button.setText('Show Brain') else: self._images['mri'] = list() for axis in range(3): mri_data = np.take(self._mri_data, self._current_slice[axis], axis=axis).T self._images['mri'].append(self._figs[axis].axes[0].imshow( mri_data, cmap='hot', aspect='auto', alpha=0.25, zorder=2)) self._toggle_brain_button.setText('Hide Brain') self._draw() def _key_press_event(self, event): """Execute functions when the user presses a key.""" if event.key() == 'escape': self.close() if event.text() == 'h': self._show_help() if event.text() == 'm': self._mark_ch() if event.text() == 'r': self._remove_ch() if event.text() == 'b': self._toggle_show_brain() if event.text() in ('=', '+', '-'): self._zoom(sign=-2 * (event.text() == '-') + 1, draw=True) # Changing slices if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Comma, QtCore.Qt.Key_Period, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): ras = np.array(self._ras) if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): ras[2] += 2 * (event.key() == QtCore.Qt.Key_Up) - 1 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): ras[0] += 2 * (event.key() == QtCore.Qt.Key_Right) - 1 else: ras[1] += 2 * (event.key() == QtCore.Qt.Key_PageUp or event.key() == QtCore.Qt.Key_Period) - 1 self._set_ras(ras) def _on_click(self, event, axis): """Move to view on MRI and CT on click.""" if event.inaxes is self._figs[axis].axes[0]: # Data coordinates are voxel coordinates pos = (event.xdata, event.ydata) logger.info(f'Clicked {"XYZ"[axis]} ({axis}) axis at pos {pos}') xyz = self._vox xyz[list(self._xy_idx[axis])] = pos logger.debug(f'Using voxel {list(xyz)}') ras = apply_trans(self._vox_ras_t, xyz) self._set_ras(ras) def _update_moved(self): """Update when cursor position changes.""" self._RAS_textbox.setPlainText( '{:.2f}, {:.2f}, {:.2f}'.format(*self._ras)) self._VOX_textbox.setPlainText( '{:3d}, {:3d}, {:3d}'.format(*self._current_slice)) self._intensity_label.setText('intensity = {:.2f}'.format( self._ct_data[tuple(self._current_slice)])) @safe_event def closeEvent(self, event): """Clean up upon closing the window.""" self._renderer.plotter.close() self.close()
class IntracranialElectrodeLocator(QMainWindow): """Locate electrode contacts using a coregistered MRI and CT.""" def __init__(self, info, trans, aligned_ct, subject=None, subjects_dir=None, groups=None, verbose=None): """GUI for locating intracranial electrodes. .. note:: Images will be displayed using orientation information obtained from the image header. Images will be resampled to dimensions [256, 256, 256] for display. """ # initialize QMainWindow class super(IntracranialElectrodeLocator, self).__init__() if not info.ch_names: raise ValueError('No channels found in `info` to locate') # store info for modification self._info = info self._verbose = verbose # load imaging data self._subject_dir = _check_subject_dir(subject, subjects_dir) self._load_image_data(aligned_ct) self._ch_alpha = 0.5 self._radius = int(_CH_PLOT_SIZE // 100) # starting 1/200 of image # initialize channel data self._ch_index = 0 # load data, apply trans self._head_mri_t = _get_trans(trans, 'head', 'mri')[0] self._mri_head_t = invert_transform(self._head_mri_t) # load channels, convert from m to mm self._chs = {name: apply_trans(self._head_mri_t, ch['loc'][:3]) * 1000 for name, ch in zip(info.ch_names, info['chs'])} self._ch_names = list(self._chs.keys()) # set current position if np.isnan(self._chs[self._ch_names[self._ch_index]]).any(): self._ras = np.array([0., 0., 0.]) else: self._ras = self._chs[self._ch_names[self._ch_index]].copy() self._current_slice = apply_trans( self._ras_vox_t, self._ras).round().astype(int) self._group_channels(groups) # GUI design # Main plots: make one plot for each view; sagittal, coronal, axial plt_grid = QGridLayout() plts = [_make_slice_plot(), _make_slice_plot(), _make_slice_plot()] self._figs = [plts[0][1], plts[1][1], plts[2][1]] plt_grid.addWidget(plts[0][0], 0, 0) plt_grid.addWidget(plts[1][0], 0, 1) plt_grid.addWidget(plts[2][0], 1, 0) self._renderer = _get_renderer( name='IEEG Locator', size=(400, 400), bgcolor='w') # TODO: should eventually make sure the renderer here is actually # some PyVista(Qt) variant, not mayavi, otherwise the following # call will fail (hopefully it's rare that people who want to use this # have also set their MNE_3D_BACKEND=mayavi and/or don't have a working # pyvistaqt setup; also hopefully the refactoring to use the # Qt/notebook abstraction will make this easier, too): plt_grid.addWidget(self._renderer.plotter) # Channel selector self._ch_list = QListView() self._ch_list.setSelectionMode(Qt.QAbstractItemView.SingleSelection) self._ch_list.setMinimumWidth(150) self._set_ch_names() # Plots self._plot_images() # Menus button_hbox = self._get_button_bar() slider_hbox = self._get_slider_bar() bottom_hbox = self._get_bottom_bar() # Put everything together plot_ch_hbox = QHBoxLayout() plot_ch_hbox.addLayout(plt_grid) plot_ch_hbox.addWidget(self._ch_list) main_vbox = QVBoxLayout() main_vbox.addLayout(button_hbox) main_vbox.addLayout(slider_hbox) main_vbox.addLayout(plot_ch_hbox) main_vbox.addLayout(bottom_hbox) central_widget = QWidget() central_widget.setLayout(main_vbox) self.setCentralWidget(central_widget) # ready for user self._move_cursors_to_pos() self._ch_list.setFocus() # always focus on list def _load_image_data(self, ct): """Get MRI and CT data to display and transforms to/from vox/RAS.""" self._mri_data, self._vox_ras_t = _load_image( op.join(self._subject_dir, 'mri', 'brain.mgz'), 'MRI Image', verbose=self._verbose) self._ras_vox_t = np.linalg.inv(self._vox_ras_t) self._voxel_sizes = np.array(self._mri_data.shape) self._img_ranges = [[0, self._voxel_sizes[1], 0, self._voxel_sizes[2]], [0, self._voxel_sizes[0], 0, self._voxel_sizes[2]], [0, self._voxel_sizes[0], 0, self._voxel_sizes[1]]] # ready ct self._ct_data, vox_ras_t = _load_image(ct, 'CT', verbose=self._verbose) if self._mri_data.shape != self._ct_data.shape or \ not np.allclose(self._vox_ras_t, vox_ras_t, rtol=1e-6): raise ValueError('CT is not aligned to MRI, got ' f'CT shape={self._ct_data.shape}, ' f'MRI shape={self._mri_data.shape}, ' f'CT affine={vox_ras_t} and ' f'MRI affine={self._vox_ras_t}') if op.exists(op.join(self._subject_dir, 'surf', 'lh.seghead')): self._head = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.seghead')) assert _frame_to_str[self._head['coord_frame']] == 'mri' else: warn('`seghead` not found, skipping head plot, see ' ':ref:`mne.bem.make_scalp_surfaces` to add the head') self._head = None if op.exists(op.join(self._subject_dir, 'surf', 'lh.pial')): self._lh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.pial')) assert _frame_to_str[self._lh['coord_frame']] == 'mri' self._rh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'rh.pial')) assert _frame_to_str[self._rh['coord_frame']] == 'mri' else: warn('`pial` surface not found, skipping adding to 3D ' 'plot. This indicates the Freesurfer recon-all ' 'has been modified and these files have been deleted.') self._lh = self._rh = None def _make_ch_image(self, axis): """Make a plot to display the channel locations.""" # Make channel data higher resolution so it looks better. ch_image = np.zeros((_CH_PLOT_SIZE, _CH_PLOT_SIZE)) * np.nan vx, vy, vz = self._voxel_sizes def color_ch_radius(ch_image, xf, yf, group, radius): # Take the fraction across each dimension of the RAS # coordinates converted to xyz and put a circle in that # position in this larger resolution image ex, ey = np.round(np.array([xf, yf]) * _CH_PLOT_SIZE).astype(int) for i in range(-radius, radius + 1): for j in range(-radius, radius + 1): if (i**2 + j**2)**0.5 < radius: # negative y because y axis is inverted ch_image[-(ey + i), ex + j] = group return ch_image for name, ras in self._chs.items(): # move from middle-centered (half coords positive, half negative) # to bottom-left corner centered (all coords positive). if np.isnan(ras).any(): continue xyz = apply_trans(self._ras_vox_t, ras) # check if closest to that voxel dist = np.linalg.norm(xyz - self._current_slice) if dist < self._radius: x, y, z = xyz group = self._groups[name] r = self._radius - np.round(abs(dist)).astype(int) if axis == 0: ch_image = color_ch_radius( ch_image, y / vy, z / vz, group, r) elif axis == 1: ch_image = color_ch_radius( ch_image, x / vx, z / vx, group, r) elif axis == 2: ch_image = color_ch_radius( ch_image, x / vx, y / vy, group, r) return ch_image @verbose def _save_ch_coords(self, info=None, verbose=None): """Save the location of the electrode contacts.""" logger.info('Saving channel positions to `info`') if info is None: info = self._info for name, ch in zip(info.ch_names, info['chs']): ch['loc'][:3] = apply_trans( self._mri_head_t, self._chs[name] / 1000) # mm->m def _plot_images(self): """Use the MRI and CT to make plots.""" # Plot sagittal (0), coronal (1) or axial (2) view self._images = dict(ct=list(), chs=list(), cursor=list(), cursor2=list()) ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) text_kwargs = dict(fontsize=3, color='#66CCEE', family='monospace', weight='bold', ha='center', va='center') xyz = apply_trans(self._ras_vox_t, self._ras) for axis in range(3): ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T self._images['ct'].append(self._figs[axis].axes[0].imshow( ct_data, cmap='gray', aspect='auto', vmin=ct_min, vmax=ct_max)) self._images['chs'].append( self._figs[axis].axes[0].imshow( self._make_ch_image(axis), aspect='auto', extent=self._img_ranges[axis], cmap=_CMAP, alpha=self._ch_alpha, vmin=0, vmax=_N_COLORS)) self._images['cursor'].append( self._figs[axis].axes[0].plot( (xyz[axis], xyz[axis]), (0, self._voxel_sizes[axis]), color=[0, 1, 0], linewidth=1, alpha=0.5)[0]) self._images['cursor2'].append( self._figs[axis].axes[0].plot( (0, self._voxel_sizes[axis]), (xyz[axis], xyz[axis]), color=[0, 1, 0], linewidth=1, alpha=0.5)[0]) # label axes self._figs[axis].text(0.5, 0.05, _IMG_LABELS[axis][0], **text_kwargs) self._figs[axis].text(0.05, 0.5, _IMG_LABELS[axis][1], **text_kwargs) self._figs[axis].axes[0].axis(self._img_ranges[axis]) self._figs[axis].canvas.mpl_connect( 'scroll_event', self._on_scroll) self._figs[axis].canvas.mpl_connect( 'button_release_event', partial(self._on_click, axis)) # add head and brain in mm (convert from m) if self._head is not None: self._renderer.mesh( *self._head['rr'].T * 1000, triangles=self._head['tris'], color='gray', opacity=0.2, reset_camera=False, render=False) if self._lh is not None and self._rh is not None: self._renderer.mesh( *self._lh['rr'].T * 1000, triangles=self._lh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.mesh( *self._rh['rr'].T * 1000, triangles=self._rh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._3d_chs = dict() self._plot_3d_ch_pos() self._renderer.set_camera(azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)) # update plots self._draw() self._renderer._update() def _scale_radius(self): """Scale the radius to mm.""" shape = np.mean(self._ct_data.shape) # this is Freesurfer shape (256) scale = np.diag(self._ras_vox_t)[:3].mean() return scale * self._radius * (shape / _CH_PLOT_SIZE) def _update_camera(self, render=False): """Update the camera position.""" self._renderer.set_camera( # needs fix, distance moves when focal point updates distance=self._renderer.plotter.camera.distance * 0.9, focalpoint=tuple(self._ras), reset_camera=False) def _plot_3d_ch(self, name, render=False): """Plot a single 3D channel.""" if name in self._3d_chs: self._renderer.plotter.remove_actor(self._3d_chs.pop(name)) if not any(np.isnan(self._chs[name])): radius = self._scale_radius() self._3d_chs[name] = self._renderer.sphere( tuple(self._chs[name]), scale=radius * 3, color=_UNIQUE_COLORS[self._groups[name] % _N_COLORS], opacity=self._ch_alpha)[0] if render: self._renderer._update() def _plot_3d_ch_pos(self, render=False): for name in self._chs: self._plot_3d_ch(name) if render: self._renderer._update() def _get_button_bar(self): """Make a bar with buttons for user interactions.""" hbox = QHBoxLayout() help_button = QPushButton('Help') help_button.released.connect(self._show_help) hbox.addWidget(help_button) hbox.addStretch(8) hbox.addWidget(QLabel('Snap to Center')) self._snap_button = QPushButton('Off') self._snap_button.setMaximumWidth(25) # not too big hbox.addWidget(self._snap_button) self._snap_button.released.connect(self._toggle_snap) self._toggle_snap() # turn on to start hbox.addStretch(1) self._toggle_brain_button = QPushButton('Show Brain') self._toggle_brain_button.released.connect(self._toggle_show_brain) hbox.addWidget(self._toggle_brain_button) hbox.addStretch(1) mark_button = QPushButton('Mark') hbox.addWidget(mark_button) mark_button.released.connect(self._mark_ch) remove_button = QPushButton('Remove') hbox.addWidget(remove_button) remove_button.released.connect(self._remove_ch) self._group_selector = ComboBox() group_model = self._group_selector.model() for i in range(_N_COLORS): self._group_selector.addItem(' ') color = QtGui.QColor() color.setRgb(*(255 * np.array(_UNIQUE_COLORS[i % _N_COLORS]) ).round().astype(int)) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) group_model.setData(group_model.index(i, 0), brush, QtCore.Qt.BackgroundRole) self._group_selector.clicked.connect(self._select_group) self._group_selector.currentIndexChanged.connect( self._select_group) hbox.addWidget(self._group_selector) # update background color for current selection self._update_group() return hbox def _get_slider_bar(self): """Make a bar with sliders on it.""" def make_label(name): label = QLabel(name) label.setAlignment(QtCore.Qt.AlignCenter) return label def make_slider(smin, smax, sval, sfun=None): slider = QSlider(QtCore.Qt.Horizontal) slider.setMinimum(int(round(smin))) slider.setMaximum(int(round(smax))) slider.setValue(int(round(sval))) slider.setTracking(False) # only update on release if sfun is not None: slider.valueChanged.connect(sfun) slider.keyPressEvent = self._key_press_event return slider slider_hbox = QHBoxLayout() ch_vbox = QVBoxLayout() ch_vbox.addWidget(make_label('ch alpha')) ch_vbox.addWidget(make_label('ch radius')) slider_hbox.addLayout(ch_vbox) ch_slider_vbox = QVBoxLayout() self._alpha_slider = make_slider(0, 100, self._ch_alpha * 100, self._update_ch_alpha) ch_plot_max = _CH_PLOT_SIZE // 50 # max 1 / 50 of plot size ch_slider_vbox.addWidget(self._alpha_slider) self._radius_slider = make_slider(0, ch_plot_max, self._radius, self._update_radius) ch_slider_vbox.addWidget(self._radius_slider) slider_hbox.addLayout(ch_slider_vbox) ct_vbox = QVBoxLayout() ct_vbox.addWidget(make_label('CT min')) ct_vbox.addWidget(make_label('CT max')) slider_hbox.addLayout(ct_vbox) ct_slider_vbox = QVBoxLayout() ct_min = int(round(np.nanmin(self._ct_data))) ct_max = int(round(np.nanmax(self._ct_data))) self._ct_min_slider = make_slider( ct_min, ct_max, ct_min, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_min_slider) self._ct_max_slider = make_slider( ct_min, ct_max, ct_max, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_max_slider) slider_hbox.addLayout(ct_slider_vbox) return slider_hbox def _get_bottom_bar(self): """Make a bar at the bottom with information in it.""" hbox = QHBoxLayout() hbox.addStretch(10) self._intensity_label = QLabel('') # update later hbox.addWidget(self._intensity_label) RAS_label = QLabel('RAS =') self._RAS_textbox = QPlainTextEdit('') # update later self._RAS_textbox.setMaximumHeight(25) self._RAS_textbox.setMaximumWidth(200) self._RAS_textbox.focusOutEvent = self._update_RAS self._RAS_textbox.textChanged.connect(self._check_update_RAS) hbox.addWidget(RAS_label) hbox.addWidget(self._RAS_textbox) self._update_moved() # update text now return hbox def _group_channels(self, groups): """Automatically find a group based on the name of the channel.""" if groups is not None: for name in self._ch_names: if name not in groups: raise ValueError(f'{name} not found in ``groups``') _validate_type(groups[name], (float, int), f'groups[{name}]') self.groups = groups else: i = 0 self._groups = dict() base_names = dict() for name in self._ch_names: # strip all numbers from the name base_name = ''.join([letter for letter in name if not letter.isdigit() and letter != ' ']) if base_name in base_names: # look up group number by base name self._groups[name] = base_names[base_name] else: self._groups[name] = i base_names[base_name] = i i += 1 def _set_ch_names(self): """Add the channel names to the selector.""" self._ch_list_model = QtGui.QStandardItemModel(self._ch_list) for name in self._ch_names: self._ch_list_model.appendRow(QtGui.QStandardItem(name)) self._color_list_item(name=name) self._ch_list.setModel(self._ch_list_model) self._ch_list.clicked.connect(self._go_to_ch) self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._ch_list.keyPressEvent = self._key_press_event def _select_group(self): """Change the group label to the selection.""" group = self._group_selector.currentIndex() self._groups[self._ch_names[self._ch_index]] = group # color differently if found already self._color_list_item(self._ch_names[self._ch_index]) self._update_group() def _update_group(self): """Set background for closed group menu.""" group = self._group_selector.currentIndex() rgb = (255 * np.array(_UNIQUE_COLORS[group % _N_COLORS]) ).round().astype(int) self._group_selector.setStyleSheet( 'background-color: rgb({:d},{:d},{:d})'.format(*rgb)) self._group_selector.update() def _on_scroll(self, event): """Process mouse scroll wheel event to zoom.""" self._zoom(event.step, draw=True) def _zoom(self, sign=1, draw=False): """Zoom in on the image.""" delta = _ZOOM_STEP_SIZE * sign for axis, fig in enumerate(self._figs): xmid = self._images['cursor'][axis].get_xdata()[0] ymid = self._images['cursor2'][axis].get_ydata()[0] xmin, xmax = fig.axes[0].get_xlim() ymin, ymax = fig.axes[0].get_ylim() xwidth = (xmax - xmin) / 2 - delta ywidth = (ymax - ymin) / 2 - delta if xwidth <= 0 or ywidth <= 0: return fig.axes[0].set_xlim(xmid - xwidth, xmid + xwidth) fig.axes[0].set_ylim(ymid - ywidth, ymid + ywidth) self._images['cursor'][axis].set_ydata([ymin, ymax]) self._images['cursor2'][axis].set_xdata([xmin, xmax]) if draw: self._figs[axis].canvas.draw() def _update_ch_selection(self): """Update which channel is selected.""" name = self._ch_names[self._ch_index] self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._group_selector.setCurrentIndex(self._groups[name]) self._update_group() if not np.isnan(self._chs[name]).any(): self._ras[:] = self._chs[name] self._move_cursors_to_pos() self._update_camera(render=True) self._draw() def _go_to_ch(self, index): """Change current channel to the item selected.""" self._ch_index = index.row() self._update_ch_selection() @pyqtSlot() def _next_ch(self): """Increment the current channel selection index.""" self._ch_index = (self._ch_index + 1) % len(self._ch_names) self._update_ch_selection() @pyqtSlot() def _update_RAS(self, event): """Interpret user input to the RAS textbox.""" text = self._RAS_textbox.toPlainText().replace('\n', '') ras = text.split(',') if len(ras) != 3: ras = text.split(' ') # spaces also okay as in freesurfer ras = [var.lstrip().rstrip() for var in ras] if len(ras) != 3: self._update_moved() # resets RAS label return all_float = all([all([dig.isdigit() or dig in ('-', '.') for dig in var]) for var in ras]) if not all_float: self._update_moved() # resets RAS label return ras = np.array([float(var) for var in ras]) xyz = apply_trans(self._ras_vox_t, ras) wrong_size = any([var < 0 or var > n for var, n in zip(xyz, self._voxel_sizes)]) if wrong_size: self._update_moved() # resets RAS label return # valid RAS position, update and move self._ras = ras self._move_cursors_to_pos() @pyqtSlot() def _check_update_RAS(self): """Check whether the RAS textbox is done being edited.""" if '\n' in self._RAS_textbox.toPlainText(): self._update_RAS(event=None) self._ch_list.setFocus() # remove focus from text edit def _color_list_item(self, name=None): """Color the item in the view list for easy id of marked channels.""" name = self._ch_names[self._ch_index] if name is None else name color = QtGui.QColor('white') if not np.isnan(self._chs[name]).any(): group = self._groups[name] color.setRgb(*[int(c * 255) for c in _UNIQUE_COLORS[int(group) % _N_COLORS]]) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.BackgroundRole) # color text black color = QtGui.QColor('black') brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.ForegroundRole) @pyqtSlot() def _toggle_snap(self): """Toggle snapping the contact location to the center of mass.""" if self._snap_button.text() == 'Off': self._snap_button.setText('On') self._snap_button.setStyleSheet("background-color: green") else: # text == 'On', turn off self._snap_button.setText('Off') self._snap_button.setStyleSheet("background-color: red") @pyqtSlot() def _mark_ch(self): """Mark the current channel as being located at the crosshair.""" name = self._ch_names[self._ch_index] if self._snap_button.text() == 'Off': self._chs[name][:] = self._ras else: coord = apply_trans(self._ras_vox_t, self._ras.copy()) shape = np.mean(self._mri_data.shape) # Freesurfer shape (256) voxels_max = int( 4 / 3 * np.pi * (shape * self._radius / _CH_PLOT_SIZE)**3) neighbors = _voxel_neighbors( coord, self._ct_data, thresh=0.5, voxels_max=voxels_max, use_relative=True) self._chs[name][:] = apply_trans( # to surface RAS self._vox_ras_t, np.array(list(neighbors)).mean(axis=0)) self._color_list_item() self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._save_ch_coords() self._next_ch() self._ch_list.setFocus() @pyqtSlot() def _remove_ch(self): """Remove the location data for the current channel.""" name = self._ch_names[self._ch_index] self._chs[name] *= np.nan self._color_list_item() self._save_ch_coords() self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._next_ch() self._ch_list.setFocus() def _draw(self, axis=None): """Update the figures with a draw call.""" for axis in (range(3) if axis is None else [axis]): self._figs[axis].canvas.draw() def _update_ch_images(self, axis=None, draw=False): """Update the channel image(s).""" for axis in range(3) if axis is None else [axis]: self._images['chs'][axis].set_data( self._make_ch_image(axis)) if draw: self._draw(axis) def _update_ct_images(self, axis=None, draw=False): """Update the CT image(s).""" for axis in range(3) if axis is None else [axis]: ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T # Threshold the CT so only bright objects (electrodes) are visible ct_data[ct_data < self._ct_min_slider.value()] = np.nan ct_data[ct_data > self._ct_max_slider.value()] = np.nan self._images['ct'][axis].set_data(ct_data) if draw: self._draw(axis) def _update_mri_images(self, axis=None, draw=False): """Update the CT image(s).""" if 'mri' in self._images: for axis in range(3) if axis is None else [axis]: self._images['mri'][axis].set_data( np.take(self._mri_data, self._current_slice[axis], axis=axis).T) if draw: self._draw(axis) def _update_images(self, axis=None, draw=True): """Update CT and channel images when general changes happen.""" self._update_ct_images(axis=axis) self._update_ch_images(axis=axis) self._update_mri_images(axis=axis) if draw: self._draw(axis) def _update_ct_scale(self): """Update CT min slider value.""" new_min = self._ct_min_slider.value() new_max = self._ct_max_slider.value() # handle inversions self._ct_min_slider.setValue(min([new_min, new_max])) self._ct_max_slider.setValue(max([new_min, new_max])) self._update_ct_images(draw=True) def _update_radius(self): """Update channel plot radius.""" self._radius = np.round(self._radius_slider.value()).astype(int) self._update_ch_images(draw=True) self._plot_3d_ch_pos(render=True) self._ch_list.setFocus() # remove focus from 3d plotter def _update_ch_alpha(self): """Update channel plot alpha.""" self._ch_alpha = self._alpha_slider.value() / 100 for axis in range(3): self._images['chs'][axis].set_alpha(self._ch_alpha) self._draw() self._plot_3d_ch_pos(render=True) self._ch_list.setFocus() # remove focus from 3d plotter def _get_click_pos(self, axis, x, y): """Get which axis was clicked and where.""" fx, fy = self._figs[axis].transFigure.inverted().transform((x, y)) xmin, xmax = self._figs[axis].axes[0].get_xlim() ymin, ymax = self._figs[axis].axes[0].get_ylim() return (fx * (xmax - xmin) + xmin, fy * (ymax - ymin) + ymin) def _move_cursors_to_pos(self): """Move the cursors to a position.""" x, y, z = apply_trans(self._ras_vox_t, self._ras) self._current_slice = np.array([x, y, z]).round().astype(int) self._move_cursor_to(0, x=y, y=z) self._move_cursor_to(1, x=x, y=z) self._move_cursor_to(2, x=x, y=y) self._zoom(0) # doesn't actually zoom just resets view to center self._update_images(draw=True) self._update_moved() def _move_cursor_to(self, axis, x, y): """Move the cursors to a position for a given subplot.""" self._images['cursor2'][axis].set_ydata([y, y]) self._images['cursor'][axis].set_xdata([x, x]) def _show_help(self): """Show the help menu.""" QMessageBox.information( self, 'Help', "Help:\n'm': mark channel location\n" "'r': remove channel location\n" "'b': toggle viewing of brain in T1\n" "'+'/'-': zoom\nleft/right arrow: left/right\n" "up/down arrow: superior/inferior\n" "page up/page down arrow: anterior/posterior") def _toggle_show_brain(self): """Toggle whether the brain/MRI is being shown.""" if 'mri' in self._images: for img in self._images['mri']: img.remove() self._images.pop('mri') self._toggle_brain_button.setText('Show Brain') else: self._images['mri'] = list() for axis in range(3): mri_data = np.take(self._mri_data, self._current_slice[axis], axis=axis).T self._images['mri'].append(self._figs[axis].axes[0].imshow( mri_data, cmap='hot', aspect='auto', alpha=0.25)) self._toggle_brain_button.setText('Hide Brain') self._draw() def _key_press_event(self, event): """Execute functions when the user presses a key.""" if event.key() == 'escape': self.close() if event.text() == 'h': self._show_help() if event.text() == 'm': self._mark_ch() if event.text() == 'r': self._remove_ch() if event.text() == 'b': self._toggle_show_brain() if event.text() in ('=', '+', '-'): self._zoom(sign=-2 * (event.text() == '-') + 1, draw=True) # Changing slices if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): self._ras[2] += 2 * (event.key() == QtCore.Qt.Key_Up) - 1 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): self._ras[0] += 2 * (event.key() == QtCore.Qt.Key_Right) - 1 elif event.key() in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): self._ras[1] += 2 * (event.key() == QtCore.Qt.Key_PageUp) - 1 self._move_cursors_to_pos() def _on_click(self, axis, event): """Move to view on MRI and CT on click.""" # Transform coordinates to figure coordinates pos = self._get_click_pos(axis, event.x, event.y) logger.info(f'Clicked axis {axis} at pos {pos}') if axis is not None and pos is not None: xyz = apply_trans(self._ras_vox_t, self._ras) if axis == 0: xyz[[1, 2]] = pos elif axis == 1: xyz[[0, 2]] = pos elif axis == 2: xyz[[0, 1]] = pos self._ras = apply_trans(self._vox_ras_t, xyz) self._move_cursors_to_pos() def _update_moved(self): """Update when cursor position changes.""" self._RAS_textbox.setPlainText('{:.2f}, {:.2f}, {:.2f}'.format( *self._ras)) self._intensity_label.setText('intensity = {:.2f}'.format( self._ct_data[tuple(self._current_slice)]))
class ToDoList(QWidget): def __init__(self, parent, component_def): super(ToDoList, self).__init__(parent) self.component = component_def self.layout = QVBoxLayout() self.list = QListView() self.list.setWordWrap(True) self.list.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.list.verticalScrollBar().setSingleStep(5) self.form = QVBoxLayout() self.buttons = QHBoxLayout() self.add = QPushButton("Add") self.add.clicked.connect(self.action_add) self.label = QLineEdit() self.label.setPlaceholderText("new task name") self.label.returnPressed.connect(self.add.click) self.priority = QComboBox() self.priority.insertItem(0, "Low") self.priority.insertItem(1, "Medium") self.priority.insertItem(2, "High") self.priority.insertItem(3, "Critical") self.priority.setCurrentIndex(1) self.stack() def stack(self): self.layout.addWidget(self.list) self.form.addWidget(self.label) self.buttons.addWidget(self.priority) self.buttons.addWidget(self.add) self.form.addLayout(self.buttons) self.layout.addLayout(self.form) self.setLayout(self.layout) def load(self, folder): model = Database().get_task_model(folder) self.list.setModel(model) #self.list.clicked[QModelIndex].connect(self.item_check) #def item_check(self, index): # item = self.list.model().itemFromIndex(index) # if item.checkState() == QtCore.Qt.Checked: def action_add(self): if self.label.text() != "": item = ToDoItem() item.label = self.label.text() item.date = datetime.datetime.now().strftime("%Y%m%d%H%M%S") item.priority = Priority(self.priority.currentIndex()) item.setSelectable(True) item.setEditable(False) item.setCheckable(True) idx = self.get_index_of_priority_to_insert(item.priority) self.list.model().insertRow(idx, item) self.label.setText("") def get_index_of_priority_to_insert(self, priority): p = int(priority) c = self.list.model().rowCount() # print("inserted priority = %d" % p) for idx in range(0, c): item = self.list.model().item(idx) if int(item.priority) <= p: return idx return c def save(self, folder): model = self.list.model() result = False for index in range(model.rowCount()): item = model.item(index) if item.id == 0: item.id = Database().insert_task(folder, item) result = True else: if item.checkState() == QtCore.Qt.Checked: Database().check_task(item.id) result = True self.tidy_up() return result def tidy_up(self): go_on = self.list.model().rowCount() > 0 i = 0 while go_on: if self.list.model().item(i).checkState() == QtCore.Qt.Checked: self.list.model().removeRow(i) else: i += 1 go_on = i < self.list.model().rowCount() def focus(self): self.list.setFocus()