class Traces(QGraphicsView): """Main widget that contains the recordings to be plotted. Attributes ---------- parent : instance of QMainWindow the main window. config : instance of ConfigTraces settings for this widget y_scrollbar_value : int position of the vertical scrollbar data : instance of ChanTime filtered and reref'ed data chan : list of str list of channels (labels and channel group) chan_pos : list of int y-position of each channel (based on value at 0) chan_scale : list of float scaling factor for each channel time_pos : list of QPointF we need to keep track of the position of time label during creation sel_chan : int index of self.chan of the first selected channel sel_xy : tuple of 2 floats x and y position of the first selected point scene : instance of QGraphicsScene the main scene. idx_label : list of instance of QGraphicsSimpleTextItem the channel labels on the y-axis idx_time : list of instance of QGraphicsSimpleTextItem the time labels on the x-axis idx_sel : instance of QGraphicsRectItem the rectangle showing the selection (both for selection and event) idx_info : instance of QGraphicsSimpleTextItem the rectangle showing the selection idx_markers : list of QGraphicsRectItem list of markers in the dataset idx_annot : list of QGraphicsRectItem list of user-made annotations """ def __init__(self, parent): super().__init__() self.parent = parent self.config = ConfigTraces(self.parent.overview.update_position) self.y_scrollbar_value = 0 self.data = None self.chan = [] self.chan_pos = [] # used later to find out which channel we're using self.chan_scale = [] self.time_pos = [] self.sel_chan = None self.sel_xy = (None, None) self.scene = None self.idx_label = [] self.idx_time = [] self.idx_sel = None self.idx_info = None self.idx_markers = [] self.idx_annot = [] self.create_action() def create_action(self): """Create actions associated with this widget.""" actions = {} act = QAction(QIcon(ICON['step_prev']), 'Previous Step', self) act.setShortcut(QKeySequence.MoveToPreviousChar) act.triggered.connect(self.step_prev) actions['step_prev'] = act act = QAction(QIcon(ICON['step_next']), 'Next Step', self) act.setShortcut(QKeySequence.MoveToNextChar) act.triggered.connect(self.step_next) actions['step_next'] = act act = QAction(QIcon(ICON['page_prev']), 'Previous Page', self) act.setShortcut(QKeySequence.MoveToPreviousPage) act.triggered.connect(self.page_prev) actions['page_prev'] = act act = QAction(QIcon(ICON['page_next']), 'Next Page', self) act.setShortcut(QKeySequence.MoveToNextPage) act.triggered.connect(self.page_next) actions['page_next'] = act act = QAction(QIcon(ICON['zoomprev']), 'Wider Time Window', self) act.setShortcut(QKeySequence.ZoomIn) act.triggered.connect(self.X_more) actions['X_more'] = act act = QAction(QIcon(ICON['zoomnext']), 'Narrower Time Window', self) act.setShortcut(QKeySequence.ZoomOut) act.triggered.connect(self.X_less) actions['X_less'] = act act = QAction(QIcon(ICON['zoomin']), 'Larger Amplitude', self) act.setShortcut(QKeySequence.MoveToPreviousLine) act.triggered.connect(self.Y_more) actions['Y_less'] = act act = QAction(QIcon(ICON['zoomout']), 'Smaller Amplitude', self) act.setShortcut(QKeySequence.MoveToNextLine) act.triggered.connect(self.Y_less) actions['Y_more'] = act act = QAction(QIcon(ICON['ydist_more']), 'Larger Y Distance', self) act.triggered.connect(self.Y_wider) actions['Y_wider'] = act act = QAction(QIcon(ICON['ydist_less']), 'Smaller Y Distance', self) act.triggered.connect(self.Y_tighter) actions['Y_tighter'] = act act = QAction(QIcon(ICON['chronometer']), '6 Hours Earlier', self) act.triggered.connect(partial(self.add_time, -6 * 60 * 60)) actions['addtime_-6h'] = act act = QAction(QIcon(ICON['chronometer']), '1 Hour Earlier', self) act.triggered.connect(partial(self.add_time, -60 * 60)) actions['addtime_-1h'] = act act = QAction(QIcon(ICON['chronometer']), '10 Minutes Earlier', self) act.triggered.connect(partial(self.add_time, -10 * 60)) actions['addtime_-10min'] = act act = QAction(QIcon(ICON['chronometer']), '10 Minutes Later', self) act.triggered.connect(partial(self.add_time, 10 * 60)) actions['addtime_10min'] = act act = QAction(QIcon(ICON['chronometer']), '1 Hour Later', self) act.triggered.connect(partial(self.add_time, 60 * 60)) actions['addtime_1h'] = act act = QAction(QIcon(ICON['chronometer']), '6 Hours Later', self) act.triggered.connect(partial(self.add_time, 6 * 60 * 60)) actions['addtime_6h'] = act self.action = actions def read_data(self): """Read the data to plot.""" window_start = self.parent.value('window_start') window_end = window_start + self.parent.value('window_length') dataset = self.parent.info.dataset groups = self.parent.channels.groups chan_to_read = [] for one_grp in groups: chan_to_read.extend(one_grp['chan_to_plot'] + one_grp['ref_chan']) if not chan_to_read: return data = dataset.read_data(chan=chan_to_read, begtime=window_start, endtime=window_end) max_s_freq = self.parent.value('max_s_freq') if data.s_freq > max_s_freq: q = int(data.s_freq / max_s_freq) lg.debug('Decimate (no low-pass filter) at ' + str(q)) data.data[0] = data.data[0][:, slice(None, None, q)] data.axis['time'][0] = data.axis['time'][0][slice(None, None, q)] data.s_freq = int(data.s_freq / q) self.data = _create_data_to_plot(data, self.parent.channels.groups) def display(self): """Display the recordings.""" if self.data is None: return if self.scene is not None: self.y_scrollbar_value = self.verticalScrollBar().value() self.scene.clear() self.create_chan_labels() self.create_time_labels() window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') time_height = max([x.boundingRect().height() for x in self.idx_time]) label_width = window_length * self.parent.value('label_ratio') scene_height = (len(self.idx_label) * self.parent.value('y_distance') + time_height) self.scene = QGraphicsScene(window_start - label_width, 0, window_length + label_width, scene_height) self.setScene(self.scene) self.idx_markers = [] self.idx_annot = [] self.add_chan_labels() self.add_time_labels() self.add_traces() self.display_grid() self.display_markers() self.display_annotations() self.resizeEvent(None) self.verticalScrollBar().setValue(self.y_scrollbar_value) self.parent.info.display_view() self.parent.overview.display_current() def create_chan_labels(self): """Create the channel labels, but don't plot them yet. Notes ----- It's necessary to have the width of the labels, so that we can adjust the main scene. """ self.idx_label = [] for one_grp in self.parent.channels.groups: for one_label in one_grp['chan_to_plot']: item = QGraphicsSimpleTextItem(one_label) item.setBrush(QBrush(QColor(one_grp['color']))) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.idx_label.append(item) def create_time_labels(self): """Create the time labels, but don't plot them yet. Notes ----- It's necessary to have the height of the time labels, so that we can adjust the main scene. Not very robust, because it uses seconds as integers. """ min_time = int(floor(min(self.data.axis['time'][0]))) max_time = int(ceil(max(self.data.axis['time'][0]))) n_time_labels = self.parent.value('n_time_labels') self.idx_time = [] self.time_pos = [] for one_time in linspace(min_time, max_time, n_time_labels): x_label = (self.data.start_time + timedelta(seconds=one_time)).strftime('%H:%M:%S') item = QGraphicsSimpleTextItem(x_label) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.idx_time.append(item) self.time_pos.append(QPointF(one_time, len(self.idx_label) * self.parent.value('y_distance'))) def add_chan_labels(self): """Add channel labels on the left.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') label_width = window_length * self.parent.value('label_ratio') for row, one_label_item in enumerate(self.idx_label): self.scene.addItem(one_label_item) one_label_item.setPos(window_start - label_width, self.parent.value('y_distance') * row + self.parent.value('y_distance') / 2) def add_time_labels(self): """Add time labels at the bottom.""" for text, pos in zip(self.idx_time, self.time_pos): self.scene.addItem(text) text.setPos(pos) def add_traces(self): """Add traces based on self.data.""" y_distance = self.parent.value('y_distance') self.chan = [] self.chan_pos = [] self.chan_scale = [] row = 0 for one_grp in self.parent.channels.groups: for one_chan in one_grp['chan_to_plot']: # channel name chan_name = one_chan + ' (' + one_grp['name'] + ')' # trace dat = (self.data(trial=0, chan=chan_name) * self.parent.value('y_scale')) dat *= -1 # flip data, upside down path = self.scene.addPath(Path(self.data.axis['time'][0], dat)) path.setPen(QPen(QColor(one_grp['color']), LINE_WIDTH)) # adjust position chan_pos = y_distance * row + y_distance / 2 path.setPos(0, chan_pos) row += 1 self.chan.append(chan_name) self.chan_scale.append(one_grp['scale']) self.chan_pos.append(chan_pos) def display_grid(self): """Display grid on x-axis and y-axis.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length if self.parent.value('grid_x'): x_tick = self.parent.value('grid_xtick') x_ticks = arange(window_start, window_end + x_tick, x_tick) for x in x_ticks: x_pos = [x, x] y_pos = [0, self.parent.value('y_distance') * len(self.idx_label)] path = self.scene.addPath(Path(x_pos, y_pos)) path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) if self.parent.value('grid_y'): for one_label_item in self.idx_label: x_pos = [window_start, window_end] y_pos = [one_label_item.y(), one_label_item.y()] path = self.scene.addPath(Path(x_pos, y_pos)) path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) def display_markers(self): """Add markers on top of first plot.""" for item in self.idx_markers: self.scene.removeItem(item) self.idx_markers = [] window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length y_distance = self.parent.value('y_distance') markers = [] if self.parent.info.markers is not None: if self.parent.value('marker_show'): markers = self.parent.info.markers for mrk in markers: if window_start <= mrk['end'] and window_end >= mrk['start']: mrk_start = max((mrk['start'], window_start)) mrk_end = min((mrk['end'], window_end)) color = QColor(self.parent.value('marker_color')) item = QGraphicsRectItem(mrk_start, 0, mrk_end - mrk_start, len(self.idx_label) * y_distance) item.setPen(color) item.setBrush(color) item.setZValue(-9) self.scene.addItem(item) item = TextItem_with_BG(color.darker(200)) item.setText(mrk['name']) item.setPos(mrk['start'], len(self.idx_label) * self.parent.value('y_distance')) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) item.setRotation(-90) self.scene.addItem(item) self.idx_markers.append(item) def display_annotations(self): """Mark all the bookmarks/events, on top of first plot.""" for item in self.idx_annot: self.scene.removeItem(item) self.idx_annot = [] window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length y_distance = self.parent.value('y_distance') raw_chan_name = list(map(take_raw_name, self.chan)) bookmarks = [] events = [] if self.parent.notes.annot is not None: if self.parent.value('annot_show'): bookmarks = self.parent.notes.annot.get_bookmarks() events = self.parent.notes.get_selected_events((window_start, window_end)) annotations = bookmarks + events for annot in annotations: if window_start <= annot['end'] and window_end >= annot['start']: mrk_start = max((annot['start'], window_start)) mrk_end = min((annot['end'], window_end)) if annot in bookmarks: color = QColor(self.parent.value('annot_bookmark_color')) if annot in events: color = convert_name_to_color(annot['name']) if annot['chan'] == ['']: h_annot = len(self.idx_label) * y_distance y_annot = (0, ) item = TextItem_with_BG(color.darker(200)) item.setText(annot['name']) item.setPos(annot['start'], len(self.idx_label) * y_distance) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) item.setRotation(-90) self.scene.addItem(item) self.idx_annot.append(item) zvalue = -8 else: h_annot = y_distance # find indices of channels with annotations chan_idx_in_mrk = in1d(raw_chan_name, annot['chan']) y_annot = asarray(self.chan_pos)[chan_idx_in_mrk] y_annot -= y_distance / 2 zvalue = -7 for y in y_annot: item = QGraphicsRectItem(mrk_start, y, mrk_end - mrk_start, h_annot) item.setPen(color) item.setBrush(color) item.setZValue(zvalue) self.scene.addItem(item) self.idx_annot.append(item) def step_prev(self): """Go to the previous step.""" window_start = (self.parent.value('window_start') - self.parent.value('window_length') / self.parent.value('window_step')) self.parent.overview.update_position(window_start) def step_next(self): """Go to the next step.""" window_start = (self.parent.value('window_start') + self.parent.value('window_length') / self.parent.value('window_step')) self.parent.overview.update_position(window_start) def page_prev(self): """Go to the previous page.""" window_start = (self.parent.value('window_start') - self.parent.value('window_length')) self.parent.overview.update_position(window_start) def page_next(self): """Go to the next page.""" window_start = (self.parent.value('window_start') + self.parent.value('window_length')) self.parent.overview.update_position(window_start) def add_time(self, extra_time): """Go to the predefined time forward.""" window_start = self.parent.value('window_start') + extra_time self.parent.overview.update_position(window_start) def X_more(self): """Zoom in on the x-axis.""" self.parent.value('window_length', self.parent.value('window_length') * 2) self.parent.overview.update_position() def X_less(self): """Zoom out on the x-axis.""" self.parent.value('window_length', self.parent.value('window_length') / 2) self.parent.overview.update_position() def X_length(self, new_window_length): """Use presets for length of the window.""" self.parent.value('window_length', new_window_length) self.parent.overview.update_position() def Y_more(self): """Increase the amplitude.""" self.parent.value('y_scale', self.parent.value('y_scale') * 2) self.parent.traces.display() def Y_less(self): """Decrease the amplitude.""" self.parent.value('y_scale', self.parent.value('y_scale') / 2) self.parent.traces.display() def Y_ampl(self, new_y_scale): """Make amplitude on Y axis using predefined values""" self.parent.value('y_scale', new_y_scale) self.parent.traces.display() def Y_wider(self): """Increase the distance of the lines.""" self.parent.value('y_distance', self.parent.value('y_distance') * 1.4) self.parent.traces.display() def Y_tighter(self): """Decrease the distance of the lines.""" self.parent.value('y_distance', self.parent.value('y_distance') / 1.4) self.parent.traces.display() def Y_dist(self, new_y_distance): """Use preset values for the distance between lines.""" self.parent.value('y_distance', new_y_distance) self.parent.traces.display() def mousePressEvent(self, event): """Create a marker or start selection Parameters ---------- event : instance of QtCore.QEvent it contains the position that was clicked. """ if not self.scene: return xy_scene = self.mapToScene(event.pos()) chan_idx = argmin(abs(asarray(self.chan_pos) - xy_scene.y())) self.sel_chan = chan_idx self.sel_xy = (xy_scene.x(), xy_scene.y()) chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() if not (chk_marker or chk_event): channame = self.chan[self.sel_chan] + ' in selected window' self.parent.spectrum.show_channame(channame) def mouseMoveEvent(self, event): """When normal selection, update power spectrum with current selection. Otherwise, show the range of the new marker. """ if not self.scene: return if self.idx_sel in self.scene.items(): self.scene.removeItem(self.idx_sel) self.idx_sel = None chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() if chk_marker or chk_event: xy_scene = self.mapToScene(event.pos()) y_distance = self.parent.value('y_distance') pos = QRectF(self.sel_xy[0], 0, xy_scene.x() - self.sel_xy[0], len(self.idx_label) * y_distance) item = QGraphicsRectItem(pos.normalized()) item.setPen(NoPen) if chk_marker: color = QColor(self.parent.value('annot_bookmark_color')) elif chk_event: eventtype = self.parent.notes.idx_eventtype.currentText() color = convert_name_to_color(eventtype) item.setBrush(QBrush(color.lighter(115))) item.setZValue(-10) self.scene.addItem(item) self.idx_sel = item return xy_scene = self.mapToScene(event.pos()) pos = QRectF(self.sel_xy[0], self.sel_xy[1], xy_scene.x() - self.sel_xy[0], xy_scene.y() - self.sel_xy[1]) self.idx_sel = QGraphicsRectItem(pos.normalized()) self.idx_sel.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH)) self.scene.addItem(self.idx_sel) if self.idx_info in self.scene.items(): self.scene.removeItem(self.idx_info) duration = '{0:0.2f}s'.format(abs(xy_scene.x() - self.sel_xy[0])) # get y-size, based on scaling too y = abs(xy_scene.y() - self.sel_xy[1]) scale = self.parent.value('y_scale') * self.chan_scale[self.sel_chan] height = '{0:0.3f}uV'.format(y / scale) item = TextItem_with_BG() item.setText(duration + ' ' + height) item.setPos(self.sel_xy[0], self.sel_xy[1]) self.scene.addItem(item) self.idx_info = item trial = 0 time = self.parent.traces.data.axis['time'][trial] beg_win = min((self.sel_xy[0], xy_scene.x())) end_win = max((self.sel_xy[0], xy_scene.x())) time_of_interest = time[(time >= beg_win) & (time < end_win)] if len(time_of_interest) > MINIMUM_N_SAMPLES: data = self.parent.traces.data(trial=trial, chan=self.chan[self.sel_chan], time=time_of_interest) n_data = len(data) n_pad = (power(2, ceil(log2(n_data))) - n_data) / 2 data = pad(data, (int(ceil(n_pad)), int(floor(n_pad))), 'constant') self.parent.spectrum.display(data) def mouseReleaseEvent(self, event): """Create a new event or marker, or show the previous power spectrum """ if not self.scene: return chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() if chk_marker or chk_event: x_in_scene = self.mapToScene(event.pos()).x() # it can happen that selection is empty (f.e. double-click) if self.sel_xy[0] is not None: # max resolution = sampling frequency # in case there is no data s_freq = self.parent.info.dataset.header['s_freq'] at_s_freq = lambda x: round(x * s_freq) / s_freq start = at_s_freq(self.sel_xy[0]) end = at_s_freq(x_in_scene) if abs(end - start) < self.parent.value('min_marker_dur'): end = start if start <= end: time = (start, end) else: time = (end, start) if chk_marker: self.parent.notes.add_bookmark(time) elif chk_event: eventtype = self.parent.notes.idx_eventtype.currentText() self.parent.notes.add_event(eventtype, time) else: # normal selection if self.idx_info in self.scene.items(): self.scene.removeItem(self.idx_info) self.idx_info = None # restore spectrum self.parent.spectrum.update() self.parent.spectrum.display_window() # general garbage collection self.sel_chan = None self.sel_xy = (None, None) if self.idx_sel in self.scene.items(): self.scene.removeItem(self.idx_sel) self.idx_sel = None def resizeEvent(self, event): """Resize scene so that it fits the whole widget. Parameters ---------- event : instance of QtCore.QEvent not important Notes ----- This function overwrites Qt function, therefore the non-standard name. Argument also depends on Qt. The function is used to change the scale of view, so that the scene fits the whole scene. There are two problems that I could not fix: 1) how to give the width of the label in absolute width, 2) how to strech scene just enough that it doesn't trigger a scrollbar. However, it's pretty good as it is now. """ if self.scene is not None: ratio = self.width() / (self.scene.width() * 1.1) self.resetTransform() self.scale(ratio, 1) def reset(self): self.y_scrollbar_value = 0 self.data = None self.chan = [] self.chan_pos = [] self.chan_scale = [] self.sel_chan = None self.sel_xy = (None, None) if self.scene is not None: self.scene.clear() self.scene = None self.idx_sel = None self.idx_info = None self.idx_label = [] self.idx_time = [] self.time_pos = []
class Spectrum(QWidget): """Plot the power spectrum for a specified channel. Attributes ---------- parent : instance of QMainWindow the main window. x_limit : tuple or list 2 values specifying the limit on x-axis y_limit : tuple or list 2 values specifying the limit on y-axis log : bool log-transform the data or not idx_chan : instance of QComboBox the element with the list of channel names. idx_x_min : instance of QLineEdit value with min x value idx_x_max : instance of QLineEdit value with max x value idx_y_min : instance of QLineEdit value with min y value idx_y_max : instance of QLineEdit value with max y value idx_log : instance of QCheckBox widget that defines if log should be used or not idx_fig : instance of QGraphicsView the view with the power spectrum scene : instance of QGraphicsScene the scene with GraphicsItems Notes ----- If data contains NaN, it doesn't create any spectrum (feature or bug?). """ def __init__(self, parent): super().__init__() self.parent = parent self.config = ConfigSpectrum(self.display_window) self.selected_chan = None self.idx_chan = None self.idx_fig = None self.scene = None self.create() def create(self): """Create empty scene for power spectrum.""" self.idx_chan = QComboBox() self.idx_chan.activated.connect(self.display_window) self.idx_fig = QGraphicsView(self) self.idx_fig.scale(1, -1) layout = QVBoxLayout() layout.addWidget(self.idx_chan) layout.addWidget(self.idx_fig) self.setLayout(layout) self.resizeEvent(None) def show_channame(self, chan_name): self.selected_chan = self.idx_chan.currentIndex() self.idx_chan.clear() self.idx_chan.addItem(chan_name) self.idx_chan.setCurrentIndex(0) def update(self): """Add channel names to the combobox.""" self.idx_chan.clear() for chan_name in self.parent.traces.chan: self.idx_chan.addItem(chan_name) if self.selected_chan is not None: self.idx_chan.setCurrentIndex(self.selected_chan) self.selected_chan = None def display_window(self): """Read the channel name from QComboBox and plot its spectrum. This function is necessary it reads the data and it sends it to self.display. When the user selects a smaller chunk of data from the visible traces, then we don't need to call this function. """ if self.idx_chan.count() == 0: self.update() chan_name = self.idx_chan.currentText() lg.debug('Power spectrum for channel ' + chan_name) if chan_name: trial = 0 data = self.parent.traces.data(trial=trial, chan=chan_name) self.display(data) else: self.scene.clear() def display(self, data): """Make graphicsitem for spectrum figure. Parameters ---------- data : ndarray 1D vector containing the data only This function can be called by self.display_window (which reads the data for the selected channel) or by the mouse-events functions in traces (which read chunks of data from the user-made selection). """ value = self.config.value self.scene = QGraphicsScene(value['x_min'], value['y_min'], value['x_max'] - value['x_min'], value['y_max'] - value['y_min']) self.idx_fig.setScene(self.scene) self.add_grid() self.resizeEvent(None) s_freq = self.parent.traces.data.s_freq f, Pxx = welch(data, fs=s_freq, nperseg=int(min( (s_freq, len(data))))) # force int freq_limit = (value['x_min'] <= f) & (f <= value['x_max']) if self.config.value['log']: Pxx_to_plot = log(Pxx[freq_limit]) else: Pxx_to_plot = Pxx[freq_limit] self.scene.addPath(Path(f[freq_limit], Pxx_to_plot), QPen(QColor(LINE_COLOR), LINE_WIDTH)) def add_grid(self): """Add axis and ticks to figure. Notes ----- I know that visvis and pyqtgraphs can do this in much simpler way, but those packages create too large a padding around the figure and this is pretty fast. """ value = self.config.value # X-AXIS # x-bottom self.scene.addLine(value['x_min'], value['y_min'], value['x_min'], value['y_max'], QPen(QColor(LINE_COLOR), LINE_WIDTH)) # at y = 0, dashed self.scene.addLine(value['x_min'], 0, value['x_max'], 0, QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DashLine)) # ticks on y-axis y_high = int(floor(value['y_max'])) y_low = int(ceil(value['y_min'])) x_length = (value['x_max'] - value['x_min']) / value['x_tick'] for y in range(y_low, y_high): self.scene.addLine(value['x_min'], y, value['x_min'] + x_length, y, QPen(QColor(LINE_COLOR), LINE_WIDTH)) # Y-AXIS # left axis self.scene.addLine(value['x_min'], value['y_min'], value['x_max'], value['y_min'], QPen(QColor(LINE_COLOR), LINE_WIDTH)) # larger ticks on x-axis every 10 Hz x_high = int(floor(value['x_max'])) x_low = int(ceil(value['x_min'])) y_length = (value['y_max'] - value['y_min']) / value['y_tick'] for x in range(x_low, x_high, 10): self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length, QPen(QColor(LINE_COLOR), LINE_WIDTH)) # smaller ticks on x-axis every 10 Hz y_length = (value['y_max'] - value['y_min']) / value['y_tick'] / 2 for x in range(x_low, x_high, 5): self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length, QPen(QColor(LINE_COLOR), LINE_WIDTH)) def resizeEvent(self, event): """Fit the whole scene in view. Parameters ---------- event : instance of Qt.Event not important """ value = self.config.value self.idx_fig.fitInView(value['x_min'], value['y_min'], value['x_max'] - value['x_min'], value['y_max'] - value['y_min']) def reset(self): """Reset widget as new""" self.idx_chan.clear() if self.scene is not None: self.scene.clear() self.scene = None
class Traces(QGraphicsView): """Main widget that contains the recordings to be plotted. Attributes ---------- parent : instance of QMainWindow the main window. config : instance of ConfigTraces settings for this widget y_scrollbar_value : int position of the vertical scrollbar data : instance of ChanTime filtered and reref'ed data chan : list of str list of channels (labels and channel group) chan_pos : list of int y-position of each channel (based on value at 0) chan_scale : list of float scaling factor for each channel time_pos : list of QPointF we need to keep track of the position of time label during creation sel_chan : int index of self.chan of the first selected channel sel_xy : tuple of 2 floats x and y position of the first selected point scene : instance of QGraphicsScene the main scene. idx_label : list of instance of QGraphicsSimpleTextItem the channel labels on the y-axis idx_time : list of instance of QGraphicsSimpleTextItem the time labels on the x-axis idx_sel : instance of QGraphicsRectItem the rectangle showing the selection (both for selection and event) idx_info : instance of QGraphicsSimpleTextItem the rectangle showing the selection idx_markers : list of QGraphicsRectItem list of markers in the dataset idx_annot : list of QGraphicsRectItem list of user-made annotations """ def __init__(self, parent): super().__init__() self.parent = parent self.config = ConfigTraces(self.parent.overview.update_position) self.y_scrollbar_value = 0 self.data = None self.chan = [] self.chan_pos = [] # used later to find out which channel we're using self.chan_scale = [] self.time_pos = [] self.sel_chan = None self.sel_xy = (None, None) self.scene = None self.idx_label = [] self.idx_time = [] self.idx_sel = None self.idx_info = None self.idx_markers = [] self.idx_annot = [] self.idx_annot_labels = [] self.cross_chan_mrk = True self.highlight = None self.event_sel = None self.current_event = None self.current_event_row = None self.current_etype = None self.deselect = None self.ready = True self.create_action() def create_action(self): """Create actions associated with this widget.""" actions = {} act = QAction(QIcon(ICON['step_prev']), 'Previous Step', self) act.setShortcut('[') act.triggered.connect(self.step_prev) actions['step_prev'] = act act = QAction(QIcon(ICON['step_next']), 'Next Step', self) act.setShortcut(']') act.triggered.connect(self.step_next) actions['step_next'] = act act = QAction(QIcon(ICON['page_prev']), 'Previous Page', self) act.setShortcut(QKeySequence.MoveToPreviousChar) act.triggered.connect(self.page_prev) actions['page_prev'] = act act = QAction(QIcon(ICON['page_next']), 'Next Page', self) act.setShortcut(QKeySequence.MoveToNextChar) act.triggered.connect(self.page_next) actions['page_next'] = act act = QAction('Go to Epoch', self) act.setShortcut(QKeySequence.FindNext) act.triggered.connect(self.go_to_epoch) actions['go_to_epoch'] = act act = QAction('Line Up with Epoch', self) act.setShortcut('F4') act.triggered.connect(self.line_up_with_epoch) actions['line_up_with_epoch'] = act act = QAction(QIcon(ICON['zoomprev']), 'Wider Time Window', self) act.setShortcut(QKeySequence.ZoomIn) act.triggered.connect(self.X_more) actions['X_more'] = act act = QAction(QIcon(ICON['zoomnext']), 'Narrower Time Window', self) act.setShortcut(QKeySequence.ZoomOut) act.triggered.connect(self.X_less) actions['X_less'] = act act = QAction(QIcon(ICON['zoomin']), 'Larger Scaling', self) act.setShortcut(QKeySequence.MoveToPreviousLine) act.triggered.connect(self.Y_more) actions['Y_less'] = act act = QAction(QIcon(ICON['zoomout']), 'Smaller Scaling', self) act.setShortcut(QKeySequence.MoveToNextLine) act.triggered.connect(self.Y_less) actions['Y_more'] = act act = QAction(QIcon(ICON['ydist_more']), 'Larger Y Distance', self) act.triggered.connect(self.Y_wider) actions['Y_wider'] = act act = QAction(QIcon(ICON['ydist_less']), 'Smaller Y Distance', self) act.triggered.connect(self.Y_tighter) actions['Y_tighter'] = act act = QAction(QIcon(ICON['chronometer']), '6 Hours Earlier', self) act.triggered.connect(partial(self.add_time, -6 * 60 * 60)) actions['addtime_-6h'] = act act = QAction(QIcon(ICON['chronometer']), '1 Hour Earlier', self) act.triggered.connect(partial(self.add_time, -60 * 60)) actions['addtime_-1h'] = act act = QAction(QIcon(ICON['chronometer']), '10 Minutes Earlier', self) act.triggered.connect(partial(self.add_time, -10 * 60)) actions['addtime_-10min'] = act act = QAction(QIcon(ICON['chronometer']), '10 Minutes Later', self) act.triggered.connect(partial(self.add_time, 10 * 60)) actions['addtime_10min'] = act act = QAction(QIcon(ICON['chronometer']), '1 Hour Later', self) act.triggered.connect(partial(self.add_time, 60 * 60)) actions['addtime_1h'] = act act = QAction(QIcon(ICON['chronometer']), '6 Hours Later', self) act.triggered.connect(partial(self.add_time, 6 * 60 * 60)) actions['addtime_6h'] = act act = QAction('Go to Next Event', self) act.setShortcut('s') act.triggered.connect(self.next_event) actions['next_event'] = act act = QAction('Delete Event and Go to Next', self) act.setShortcut('d') act.triggered.connect(partial(self.next_event, True)) actions['del_and_next_event'] = act act = QAction('Next Event of Same Type', self) act.setCheckable(True) act.setChecked(True) actions['next_of_same_type'] = act act = QAction('Change Event Type', self) act.setShortcut('e') act.triggered.connect(self.change_event_type) actions['change_event_type'] = act act = QAction('Centre Window Around Event', self) act.setCheckable(True) act.setChecked(True) actions['centre_event'] = act act = QAction('Full-length Markers', self) act.setCheckable(True) act.setChecked(True) act.triggered.connect(self.display_annotations) actions['cross_chan_mrk'] = act # Misc act = QAction('Export to svg...', self) act.triggered.connect(partial(export_graphics, MAIN=self.parent)) actions['export_svg'] = act self.action = actions def read_data(self): """Read the data to plot.""" window_start = self.parent.value('window_start') window_end = window_start + self.parent.value('window_length') dataset = self.parent.info.dataset groups = self.parent.channels.groups chan_to_read = [] for one_grp in groups: chan_to_read.extend(one_grp['chan_to_plot'] + one_grp['ref_chan']) if not chan_to_read: return lg.debug( f'Reading data from dataset: begtime={window_start:10.3f}, endtime={window_end:10.3f}, {len(chan_to_read)} channels' ) data = dataset.read_data(chan=chan_to_read, begtime=window_start, endtime=window_end) max_s_freq = self.parent.value('max_s_freq') if data.s_freq > max_s_freq: q = int(data.s_freq / max_s_freq) lg.debug('Decimate (no low-pass filter) at ' + str(q)) data.data[0] = data.data[0][:, slice(None, None, q)] data.axis['time'][0] = data.axis['time'][0][slice(None, None, q)] data.s_freq = int(data.s_freq / q) self.data = _create_data_to_plot(data, self.parent.channels.groups) def display(self): """Display the recordings.""" if self.data is None: return if self.scene is not None: self.y_scrollbar_value = self.verticalScrollBar().value() self.scene.clear() self.create_chan_labels() self.create_time_labels() window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') time_height = max([x.boundingRect().height() for x in self.idx_time]) label_width = window_length * self.parent.value('label_ratio') scene_height = (len(self.idx_label) * self.parent.value('y_distance') + time_height) self.scene = QGraphicsScene(window_start - label_width, 0, window_length + label_width, scene_height) self.setScene(self.scene) self.idx_markers = [] self.idx_annot = [] self.idx_annot_labels = [] self.add_chan_labels() self.add_time_labels() self.add_traces() self.display_grid() self.display_markers() self.display_annotations() self.resizeEvent(None) self.verticalScrollBar().setValue(self.y_scrollbar_value) self.parent.info.display_view() self.parent.overview.display_current() def create_chan_labels(self): """Create the channel labels, but don't plot them yet. Notes ----- It's necessary to have the width of the labels, so that we can adjust the main scene. """ self.idx_label = [] for one_grp in self.parent.channels.groups: for one_label in one_grp['chan_to_plot']: item = QGraphicsSimpleTextItem(one_label) item.setBrush(QBrush(QColor(one_grp['color']))) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.idx_label.append(item) def create_time_labels(self): """Create the time labels, but don't plot them yet. Notes ----- It's necessary to have the height of the time labels, so that we can adjust the main scene. Not very robust, because it uses seconds as integers. """ min_time = int(floor(min(self.data.axis['time'][0]))) max_time = int(ceil(max(self.data.axis['time'][0]))) n_time_labels = self.parent.value('n_time_labels') self.idx_time = [] self.time_pos = [] for one_time in linspace(min_time, max_time, n_time_labels): x_label = (self.data.start_time + timedelta(seconds=one_time)).strftime('%H:%M:%S') item = QGraphicsSimpleTextItem(x_label) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.idx_time.append(item) self.time_pos.append( QPointF(one_time, len(self.idx_label) * self.parent.value('y_distance'))) def add_chan_labels(self): """Add channel labels on the left.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') label_width = window_length * self.parent.value('label_ratio') for row, one_label_item in enumerate(self.idx_label): self.scene.addItem(one_label_item) one_label_item.setPos( window_start - label_width, self.parent.value('y_distance') * row + self.parent.value('y_distance') / 2) def add_time_labels(self): """Add time labels at the bottom.""" for text, pos in zip(self.idx_time, self.time_pos): self.scene.addItem(text) text.setPos(pos) def add_traces(self): """Add traces based on self.data.""" y_distance = self.parent.value('y_distance') self.chan = [] self.chan_pos = [] self.chan_scale = [] row = 0 for one_grp in self.parent.channels.groups: for one_chan in one_grp['chan_to_plot']: # channel name chan_name = one_chan + ' (' + one_grp['name'] + ')' # trace dat = (self.data(trial=0, chan=chan_name) * self.parent.value('y_scale')) dat *= -1 # flip data, upside down (because y grows downward) path = self.scene.addPath(Path(self.data.axis['time'][0], dat)) path.setPen(QPen(QColor(one_grp['color']), LINE_WIDTH)) # adjust position chan_pos = y_distance * row + y_distance / 2 path.setPos(0, chan_pos) row += 1 self.chan.append(chan_name) self.chan_scale.append(one_grp['scale']) self.chan_pos.append(chan_pos) def display_grid(self): """Display grid on x-axis and y-axis.""" window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length if self.parent.value('grid_x'): x_tick = self.parent.value('grid_xtick') x_ticks = arange(window_start, window_end + x_tick, x_tick) for x in x_ticks: x_pos = [x, x] y_pos = [ 0, self.parent.value('y_distance') * len(self.idx_label) ] path = self.scene.addPath(Path(x_pos, y_pos)) path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) if self.parent.value('grid_y'): y_tick = (self.parent.value('grid_ytick') * self.parent.value('y_scale')) for one_label_item in self.idx_label: x_pos = [window_start, window_end] y = one_label_item.y() y_pos_0 = [y, y] path_0 = self.scene.addPath(Path(x_pos, y_pos_0)) path_0.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) y_up = one_label_item.y() + y_tick y_pos_up = [y_up, y_up] path_up = self.scene.addPath(Path(x_pos, y_pos_up)) path_up.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) y_down = one_label_item.y() - y_tick y_pos_down = [y_down, y_down] path_down = self.scene.addPath(Path(x_pos, y_pos_down)) path_down.setPen( QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine)) def display_markers(self): """Add markers on top of first plot.""" for item in self.idx_markers: self.scene.removeItem(item) self.idx_markers = [] window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length y_distance = self.parent.value('y_distance') markers = [] if self.parent.info.markers is not None: if self.parent.value('marker_show'): markers = self.parent.info.markers for mrk in markers: if window_start <= mrk['end'] and window_end >= mrk['start']: mrk_start = max((mrk['start'], window_start)) mrk_end = min((mrk['end'], window_end)) color = QColor(self.parent.value('marker_color')) h_annot = len(self.idx_label) * y_distance mrk_dur = amax((mrk_end - mrk_start, self.parent.value('min_marker_display_dur'))) item = RectMarker(mrk_start, 0, mrk_dur, h_annot, zvalue=-9, color=color) self.scene.addItem(item) item = TextItem_with_BG(color.darker(200)) item.setText(str(mrk['name'])) item.setPos( mrk['start'], len(self.idx_label) * self.parent.value('y_distance')) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) item.setRotation(-90) self.scene.addItem(item) self.idx_markers.append(item) def display_annotations(self): """Mark all the bookmarks/events, on top of first plot.""" for item in self.idx_annot: self.scene.removeItem(item) self.idx_annot = [] for item in self.idx_annot_labels: self.scene.removeItem(item) self.idx_annot_labels = [] self.highlight = None window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length y_distance = self.parent.value('y_distance') bookmarks = [] events = [] if self.parent.notes.annot is not None: if self.parent.value('annot_show'): bookmarks = self.parent.notes.annot.get_bookmarks() events = self.parent.notes.get_selected_events( (window_start, window_end)) annotations = bookmarks + events for annot in annotations: if window_start <= annot['end'] and window_end >= annot['start']: mrk_start = max((annot['start'], window_start)) mrk_end = min((annot['end'], window_end)) if annot in bookmarks: color = QColor(self.parent.value('annot_bookmark_color')) if annot in events: color = convert_name_to_color(annot['name']) if logical_or(annot['chan'] == [''], self.action['cross_chan_mrk'].isChecked()): h_annot = len(self.idx_label) * y_distance item = TextItem_with_BG(color.darker(200)) item.setText(annot['name']) item.setPos(annot['start'], len(self.idx_label) * y_distance) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) item.setRotation(-90) self.scene.addItem(item) self.idx_annot_labels.append(item) mrk_dur = amax( (mrk_end - mrk_start, self.parent.value('min_marker_display_dur'))) item = RectMarker(mrk_start, 0, mrk_dur, h_annot, zvalue=-8, color=color.lighter(120)) self.scene.addItem(item) self.idx_annot.append(item) if annot['chan'] != ['']: # find indices of channels with annotations chan_idx_in_mrk = in1d(self.chan, annot['chan']) y_annot = asarray(self.chan_pos)[chan_idx_in_mrk] y_annot -= y_distance / 2 mrk_dur = amax( (mrk_end - mrk_start, self.parent.value('min_marker_display_dur'))) for y in y_annot: item = RectMarker(mrk_start, y, mrk_dur, y_distance, zvalue=-7, color=color) self.scene.addItem(item) self.idx_annot.append(item) def step_prev(self): """Go to the previous step.""" window_start = around( self.parent.value('window_start') - self.parent.value('window_length') / self.parent.value('window_step'), 2) if window_start < 0: return self.parent.overview.update_position(window_start) def step_next(self): """Go to the next step.""" window_start = around( self.parent.value('window_start') + self.parent.value('window_length') / self.parent.value('window_step'), 2) self.parent.overview.update_position(window_start) def page_prev(self): """Go to the previous page.""" window_start = (self.parent.value('window_start') - self.parent.value('window_length')) if window_start < 0: return self.parent.overview.update_position(window_start) def page_next(self): """Go to the next page.""" window_start = (self.parent.value('window_start') + self.parent.value('window_length')) self.parent.overview.update_position(window_start) def go_to_epoch(self, checked=False, test_text_str=None): """Go to any window""" if test_text_str is not None: time_str = test_text_str ok = True else: time_str, ok = QInputDialog.getText( self, 'Go To Epoch', 'Enter start time of the ' 'epoch,\nin seconds ("1560") ' 'or\nas absolute time ' '("22:30")') if not ok: return try: rec_start_time = self.parent.info.dataset.header['start_time'] window_start = _convert_timestr_to_seconds(time_str, rec_start_time) except ValueError as err: error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error moving to epoch') error_dialog.showMessage(str(err)) if test_text_str is None: error_dialog.exec() self.parent.statusBar().showMessage(str(err)) return self.parent.overview.update_position(window_start) def line_up_with_epoch(self): """Go to the start of the present epoch.""" if self.parent.notes.annot is None: # TODO: remove if buttons are disabled error_dialog = QErrorMessage() error_dialog.setWindowTitle('Error moving to epoch') error_dialog.showMessage('No score file loaded') error_dialog.exec() return new_window_start = self.parent.notes.annot.get_epoch_start( self.parent.value('window_start')) self.parent.overview.update_position(new_window_start) def add_time(self, extra_time): """Go to the predefined time forward.""" window_start = self.parent.value('window_start') + extra_time self.parent.overview.update_position(window_start) def X_more(self): """Zoom out on the x-axis.""" new_length = self.parent.value('window_length') * 2 self.parent.value('window_length', new_length) new_start = self.parent.value('window_start') - new_length / 4 self.parent.value('window_start', new_start) self.parent.overview.update_position() def X_less(self): """Zoom in on the x-axis.""" new_length = self.parent.value('window_length') / 2 self.parent.value('window_length', new_length) new_start = self.parent.value('window_start') + new_length / 2 self.parent.value('window_start', new_start) self.parent.overview.update_position() def X_length(self, new_window_length): """Use presets for length of the window.""" self.parent.value('window_length', new_window_length) self.parent.overview.update_position() def Y_more(self): """Increase the scaling.""" self.parent.value('y_scale', self.parent.value('y_scale') * 2) self.parent.traces.display() def Y_less(self): """Decrease the scaling.""" self.parent.value('y_scale', self.parent.value('y_scale') / 2) self.parent.traces.display() def Y_ampl(self, new_y_scale): """Make scaling on Y axis using predefined values""" self.parent.value('y_scale', new_y_scale) self.parent.traces.display() def Y_wider(self): """Increase the distance of the lines.""" self.parent.value('y_distance', self.parent.value('y_distance') * 1.4) self.parent.traces.display() def Y_tighter(self): """Decrease the distance of the lines.""" self.parent.value('y_distance', self.parent.value('y_distance') / 1.4) self.parent.traces.display() def Y_dist(self, new_y_distance): """Use preset values for the distance between lines.""" self.parent.value('y_distance', new_y_distance) self.parent.traces.display() def mousePressEvent(self, event): """Create a marker or start selection Parameters ---------- event : instance of QtCore.QEvent it contains the position that was clicked. """ if not self.scene: return if self.event_sel or self.current_event: self.parent.notes.idx_eventtype.setCurrentText(self.current_etype) self.current_etype = None self.current_event = None self.deselect = True self.event_sel = None self.current_event_row = None self.scene.removeItem(self.highlight) self.highlight = None self.parent.statusBar().showMessage('') return self.ready = False self.event_sel = None xy_scene = self.mapToScene(event.pos()) chan_idx = argmin(abs(asarray(self.chan_pos) - xy_scene.y())) self.sel_chan = chan_idx self.sel_xy = (xy_scene.x(), xy_scene.y()) chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() if not (chk_marker or chk_event): channame = self.chan[self.sel_chan] + ' in selected window' self.parent.spectrum.show_channame(channame) # Make annotations clickable else: for annot in self.idx_annot: if annot.contains(xy_scene): self.highlight_event(annot) if chk_event: row = self.parent.notes.find_row( annot.marker.x(), annot.marker.x() + annot.marker.width()) self.parent.notes.idx_annot_list.setCurrentCell(row, 0) break self.ready = True def mouseMoveEvent(self, event): """When normal selection, update power spectrum with current selection. Otherwise, show the range of the new marker. """ if not self.scene: return if self.event_sel or self.deselect: return if self.sel_xy[0] is None or self.sel_xy[1] is None: return if self.idx_sel in self.scene.items(): self.scene.removeItem(self.idx_sel) self.idx_sel = None chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() if chk_marker or chk_event: xy_scene = self.mapToScene(event.pos()) y_distance = self.parent.value('y_distance') pos = QRectF(self.sel_xy[0], 0, xy_scene.x() - self.sel_xy[0], len(self.idx_label) * y_distance) item = QGraphicsRectItem(pos.normalized()) item.setPen(NoPen) if chk_marker: color = QColor(self.parent.value('annot_bookmark_color')) elif chk_event: eventtype = self.parent.notes.idx_eventtype.currentText() color = convert_name_to_color(eventtype) item.setBrush(QBrush(color.lighter(115))) item.setZValue(-10) self.scene.addItem(item) self.idx_sel = item return xy_scene = self.mapToScene(event.pos()) pos = QRectF(self.sel_xy[0], self.sel_xy[1], xy_scene.x() - self.sel_xy[0], xy_scene.y() - self.sel_xy[1]) self.idx_sel = QGraphicsRectItem(pos.normalized()) self.idx_sel.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH)) self.scene.addItem(self.idx_sel) if self.idx_info in self.scene.items(): self.scene.removeItem(self.idx_info) duration = '{0:0.3f}s'.format(abs(xy_scene.x() - self.sel_xy[0])) # get y-size, based on scaling too y = abs(xy_scene.y() - self.sel_xy[1]) scale = self.parent.value('y_scale') * self.chan_scale[self.sel_chan] height = '{0:0.3f}uV'.format(y / scale) item = TextItem_with_BG() item.setText(duration + ' ' + height) item.setPos(self.sel_xy[0], self.sel_xy[1]) self.scene.addItem(item) self.idx_info = item trial = 0 time = self.parent.traces.data.axis['time'][trial] beg_win = min((self.sel_xy[0], xy_scene.x())) end_win = max((self.sel_xy[0], xy_scene.x())) time_of_interest = time[(time >= beg_win) & (time < end_win)] if len(time_of_interest) > MINIMUM_N_SAMPLES: data = self.parent.traces.data(trial=trial, chan=self.chan[self.sel_chan], time=time_of_interest) n_data = len(data) n_pad = (power(2, ceil(log2(n_data))) - n_data) / 2 data = pad(data, (int(ceil(n_pad)), int(floor(n_pad))), 'constant') self.parent.spectrum.display(data) def mouseReleaseEvent(self, event): """Create a new event or marker, or show the previous power spectrum """ if not self.scene: return if self.event_sel: return if self.deselect: self.deselect = False return if not self.ready: return chk_marker = self.parent.notes.action['new_bookmark'].isChecked() chk_event = self.parent.notes.action['new_event'].isChecked() y_distance = self.parent.value('y_distance') if chk_marker or chk_event: x_in_scene = self.mapToScene(event.pos()).x() y_in_scene = self.mapToScene(event.pos()).y() # it can happen that selection is empty (f.e. double-click) if self.sel_xy[0] is not None: # max resolution = sampling frequency # in case there is no data s_freq = self.parent.info.dataset.header['s_freq'] at_s_freq = lambda x: round(x * s_freq) / s_freq start = at_s_freq(self.sel_xy[0]) end = at_s_freq(x_in_scene) if abs(end - start) < self.parent.value('min_marker_dur'): end = start if start <= end: time = (start, end) else: time = (end, start) if chk_marker: self.parent.notes.add_bookmark(time) elif chk_event and start != end: eventtype = self.parent.notes.idx_eventtype.currentText() # if dragged across > 1.5 chan, event is marked on all chan if abs(y_in_scene - self.sel_xy[1]) > 1.5 * y_distance: chan = '' else: chan_idx = int(floor(self.sel_xy[1] / y_distance)) chan = self.chan[chan_idx] self.parent.notes.add_event(eventtype, time, chan) else: # normal selection if self.idx_info in self.scene.items(): self.scene.removeItem(self.idx_info) self.idx_info = None # restore spectrum self.parent.spectrum.update() self.parent.spectrum.display_window() # general garbage collection self.sel_chan = None self.sel_xy = (None, None) if self.idx_sel in self.scene.items(): self.scene.removeItem(self.idx_sel) self.idx_sel = None def keyPressEvent(self, event): chk_event = self.parent.notes.action['new_event'].isChecked() chk_book = self.parent.notes.action['new_bookmark'].isChecked() if not ((chk_event or chk_book) and self.event_sel): return annot = self.event_sel highlight = self.highlight annot_start = annot.marker.x() annot_end = annot_start + annot.marker.width() if type(event) == QKeyEvent and (event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace): if chk_event: self.parent.notes.remove_event(time=(annot_start, annot_end)) elif chk_book: self.parent.notes.remove_bookmark(time=(annot_start, annot_end)) self.scene.removeItem(highlight) msg = 'Deleted event from {} to {}'.format(annot_start, annot_end) self.parent.statusBar().showMessage(msg) self.event_sel = None self.highlight = None self.parent.notes.idx_eventtype.setCurrentText(self.current_etype) self.current_etype = None self.current_event = None self.display_annotations def highlight_event(self, annot): """Highlight an annotation on the trace. Parameters ---------- annot : intance of wonambi.widgets.utils.RectMarker existing annotation """ beg = annot.marker.x() end = beg + annot.marker.width() window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') events = self.parent.notes.get_selected_events( (window_start, window_start + window_length)) ev = [x for x in events if (x['start'] == annot.marker.x() or \ x['end'] == annot.marker.y())] if ev: annot_name = ev[0]['name'] msg = "Event of type '{}' from {} to {}".format( annot_name, beg, end) self.current_etype = self.parent.notes.idx_eventtype.currentText() self.parent.notes.idx_eventtype.setCurrentText(annot_name) self.current_event = ev[0] else: msg = "Marker from {} to {}".format(beg, end) self.parent.statusBar().showMessage(msg) highlight = self.highlight = RectMarker(annot.marker.x(), annot.marker.y(), annot.marker.width(), annot.marker.height(), zvalue=-5, color=QColor(255, 255, 51)) self.scene.addItem(highlight) self.event_sel = annot def next_event(self, delete=False): """Go to next event.""" if delete: msg = "Delete this event? This cannot be undone." msgbox = QMessageBox(QMessageBox.Question, 'Delete event', msg) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgbox.setDefaultButton(QMessageBox.Yes) response = msgbox.exec_() if response == QMessageBox.No: return event_sel = self.event_sel if event_sel is None: return notes = self.parent.notes if not self.current_event_row: row = notes.find_row( event_sel.marker.x(), event_sel.marker.x() + event_sel.marker.width()) else: row = self.current_event_row same_type = self.action['next_of_same_type'].isChecked() if same_type: target = notes.idx_annot_list.item(row, 2).text() if delete: notes.delete_row() msg = 'Deleted event from {} to {}.'.format( event_sel.marker.x(), event_sel.marker.x() + event_sel.marker.width()) self.parent.statusBar().showMessage(msg) row -= 1 if row + 1 == notes.idx_annot_list.rowCount(): return if not same_type: next_row = row + 1 else: next_row = None types = notes.idx_annot_list.property('name')[row + 1:] for i, ty in enumerate(types): if ty == target: next_row = row + 1 + i break if next_row is None: return self.current_event_row = next_row notes.go_to_marker(next_row, 0, 'annot') notes.idx_annot_list.setCurrentCell(next_row, 0) def change_event_type(self): """Action: change highlighted event's type by cycling through event type list.""" if self.current_event is None: return hl_params = self.highlight.params self.scene.removeItem(self.highlight) ev = self.current_event new_name = self.parent.notes.change_event_type(name=ev['name'], time=(ev['start'], ev['end']), chan=ev['chan']) msg = "Event from {} to {} changed type from '{}' to '{}'".format( ev['start'], ev['end'], ev['name'], new_name) ev['name'] = new_name self.current_event = ev self.current_etype = new_name #self.event_sel = True self.parent.notes.idx_eventtype.setCurrentText(new_name) self.parent.statusBar().showMessage(msg) self.display_annotations() self.highlight = RectMarker(*hl_params) self.scene.addItem(self.highlight) def resizeEvent(self, event): """Resize scene so that it fits the whole widget. Parameters ---------- event : instance of QtCore.QEvent not important Notes ----- This function overwrites Qt function, therefore the non-standard name. Argument also depends on Qt. The function is used to change the scale of view, so that the scene fits the whole scene. There are two problems that I could not fix: 1) how to give the width of the label in absolute width, 2) how to strech scene just enough that it doesn't trigger a scrollbar. However, it's pretty good as it is now. """ if self.scene is not None: ratio = self.width() / (self.scene.width() * 1.1) self.resetTransform() self.scale(ratio, 1) def reset(self): self.y_scrollbar_value = 0 self.data = None self.chan = [] self.chan_pos = [] self.chan_scale = [] self.sel_chan = None self.sel_xy = (None, None) if self.scene is not None: self.scene.clear() self.scene = None self.idx_sel = None self.idx_info = None self.idx_label = [] self.idx_time = [] self.time_pos = []
class Screenshot(QGraphicsView): """ Main Class """ screen_shot_grabed = pyqtSignal(QImage) widget_closed = pyqtSignal() def __init__(self, flags=constant.DEFAULT, parent=None): """ flags: binary flags. see the flags in the constant.py """ super().__init__(parent) # Init self.penColorNow = QColor(PENCOLOR) self.penSizeNow = PENSIZE self.fontNow = QFont('Sans') self.clipboard = QApplication.clipboard() self.drawListResult = [ ] # draw list that sure to be drew, [action, coord] self.drawListProcess = None # the process to the result self.selectedArea = QRect( ) # a QRect instance which stands for the selected area self.selectedAreaRaw = QRect() self.mousePosition = MousePosition.OUTSIDE_AREA # mouse position self.screenPixel = None self.textRect = None self.mousePressed = False self.action = ACTION_SELECT self.mousePoint = self.cursor().pos() self.startX, self.startY = 0, 0 # the point where you start self.endX, self.endY = 0, 0 # the point where you end self.pointPath = QPainterPath( ) # the point mouse passes, used by draw free line self.itemsToRemove = [ ] # the items that should not draw on screenshot picture self.textPosition = None # result self.target_img = None # Init window self.getscreenshot() self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) self.setMouseTracking(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setContentsMargins(0, 0, 0, 0) self.setStyleSheet("QGraphicsView { border-style: none; }") self.tooBar = MyToolBar(flags, self) self.tooBar.trigger.connect(self.changeAction) self.penSetBar = None if flags & constant.RECT or flags & constant.ELLIPSE or flags & constant.LINE or flags & constant.FREEPEN \ or flags & constant.ARROW or flags & constant.TEXT: self.penSetBar = PenSetWidget(self) self.penSetBar.penSizeTrigger.connect(self.changePenSize) self.penSetBar.penColorTrigger.connect(self.changePenColor) self.penSetBar.fontChangeTrigger.connect(self.changeFont) self.textInput = TextInput(self) self.textInput.inputChanged.connect(self.textChange) self.textInput.cancelPressed.connect(self.cancelInput) self.textInput.okPressed.connect(self.okInput) self.graphicsScene = QGraphicsScene(0, 0, self.screenPixel.width(), self.screenPixel.height()) self.show() self.setScene(self.graphicsScene) self.windowHandle().setScreen(QGuiApplication.screenAt(QCursor.pos())) self.scale = self.get_scale() # self.setFixedSize(self.screenPixel.width(), self.screenPixel.height()) self.setGeometry(QGuiApplication.screenAt(QCursor.pos()).geometry()) self.showFullScreen() self.redraw() QShortcut(QKeySequence('ctrl+s'), self).activated.connect(self.saveScreenshot) QShortcut(QKeySequence('esc'), self).activated.connect(self.close) @staticmethod def take_screenshot(flags): loop = QEventLoop() screen_shot = Screenshot(flags) screen_shot.show() screen_shot.widget_closed.connect(loop.quit) loop.exec() img = screen_shot.target_img return img def getscreenshot(self): screen = QGuiApplication.screenAt(QCursor.pos()) self.screenPixel = screen.grabWindow(0) def mousePressEvent(self, event): """ :type event: QMouseEvent :param event: :return: """ if event.button() != Qt.LeftButton: return if self.action is None: self.action = ACTION_SELECT self.startX, self.startY = event.x(), event.y() if self.action == ACTION_SELECT: if self.mousePosition == MousePosition.OUTSIDE_AREA: self.mousePressed = True self.selectedArea = QRect() self.selectedArea.setTopLeft(QPoint(event.x(), event.y())) self.selectedArea.setBottomRight(QPoint(event.x(), event.y())) self.redraw() elif self.mousePosition == MousePosition.INSIDE_AREA: self.mousePressed = True else: pass elif self.action == ACTION_MOVE_SELECTED: if self.mousePosition == MousePosition.OUTSIDE_AREA: self.action = ACTION_SELECT self.selectedArea = QRect() self.selectedArea.setTopLeft(QPoint(event.x(), event.y())) self.selectedArea.setBottomRight(QPoint(event.x(), event.y())) self.redraw() self.mousePressed = True elif self.action in DRAW_ACTION: self.mousePressed = True if self.action == ACTION_FREEPEN: self.pointPath = QPainterPath() self.pointPath.moveTo(QPoint(event.x(), event.y())) elif self.action == ACTION_TEXT: if self.textPosition is None: self.textPosition = QPoint(event.x(), event.y()) self.textRect = None self.redraw() def mouseMoveEvent(self, event: QMouseEvent): """ :type event: QMouseEvent :param event: :return: """ self.mousePoint = QPoint(event.globalPos().x(), event.globalPos().y()) if self.action is None: self.action = ACTION_SELECT if not self.mousePressed: point = QPoint(event.x(), event.y()) self.detectMousePosition(point) self.setCursorStyle() self.redraw() else: self.endX, self.endY = event.x(), event.y() # if self.mousePosition != OUTSIDE_AREA: # self.action = ACTION_MOVE_SELECTED if self.action == ACTION_SELECT: self.selectedArea.setBottomRight(QPoint(event.x(), event.y())) self.redraw() elif self.action == ACTION_MOVE_SELECTED: self.selectedArea = QRect(self.selectedAreaRaw) if self.mousePosition == MousePosition.INSIDE_AREA: moveToX = event.x() - self.startX + self.selectedArea.left( ) moveToY = event.y() - self.startY + self.selectedArea.top() if 0 <= moveToX <= self.screenPixel.width( ) - 1 - self.selectedArea.width(): self.selectedArea.moveLeft(moveToX) if 0 <= moveToY <= self.screenPixel.height( ) - 1 - self.selectedArea.height(): self.selectedArea.moveTop(moveToY) self.selectedArea = self.selectedArea.normalized() self.selectedAreaRaw = QRect(self.selectedArea) self.startX, self.startY = event.x(), event.y() self.redraw() elif self.mousePosition == MousePosition.ON_THE_LEFT_SIDE: moveToX = event.x() - self.startX + self.selectedArea.left( ) if moveToX <= self.selectedArea.right(): self.selectedArea.setLeft(moveToX) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_RIGHT_SIDE: moveToX = event.x( ) - self.startX + self.selectedArea.right() self.selectedArea.setRight(moveToX) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_UP_SIDE: moveToY = event.y() - self.startY + self.selectedArea.top() self.selectedArea.setTop(moveToY) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_DOWN_SIDE: moveToY = event.y( ) - self.startY + self.selectedArea.bottom() self.selectedArea.setBottom(moveToY) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_TOP_LEFT_CORNER: moveToX = event.x() - self.startX + self.selectedArea.left( ) moveToY = event.y() - self.startY + self.selectedArea.top() self.selectedArea.setTopLeft(QPoint(moveToX, moveToY)) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_BOTTOM_RIGHT_CORNER: moveToX = event.x( ) - self.startX + self.selectedArea.right() moveToY = event.y( ) - self.startY + self.selectedArea.bottom() self.selectedArea.setBottomRight(QPoint(moveToX, moveToY)) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_TOP_RIGHT_CORNER: moveToX = event.x( ) - self.startX + self.selectedArea.right() moveToY = event.y() - self.startY + self.selectedArea.top() self.selectedArea.setTopRight(QPoint(moveToX, moveToY)) self.selectedArea = self.selectedArea.normalized() self.redraw() elif self.mousePosition == MousePosition.ON_THE_BOTTOM_LEFT_CORNER: moveToX = event.x() - self.startX + self.selectedArea.left( ) moveToY = event.y( ) - self.startY + self.selectedArea.bottom() self.selectedArea.setBottomLeft(QPoint(moveToX, moveToY)) self.redraw() else: pass elif self.action == ACTION_RECT: self.drawRect(self.startX, self.startY, event.x(), event.y(), False) self.redraw() pass elif self.action == ACTION_ELLIPSE: self.drawEllipse(self.startX, self.startY, event.x(), event.y(), False) self.redraw() elif self.action == ACTION_ARROW: self.drawArrow(self.startX, self.startY, event.x(), event.y(), False) self.redraw() elif self.action == ACTION_LINE: self.drawLine(self.startX, self.startY, event.x(), event.y(), False) self.redraw() elif self.action == ACTION_FREEPEN: y1, y2 = event.x(), event.y() rect = self.selectedArea.normalized() if y1 <= rect.left(): y1 = rect.left() elif y1 >= rect.right(): y1 = rect.right() if y2 <= rect.top(): y2 = rect.top() elif y2 >= rect.bottom(): y2 = rect.bottom() self.pointPath.lineTo(y1, y2) self.drawFreeLine(self.pointPath, False) self.redraw() def mouseReleaseEvent(self, event): """ :type event: QMouseEvent :param event: :return: """ if event.button() != Qt.LeftButton: return if self.mousePressed: self.mousePressed = False self.endX, self.endY = event.x(), event.y() if self.action == ACTION_SELECT: self.selectedArea.setBottomRight(QPoint(event.x(), event.y())) self.selectedAreaRaw = QRect(self.selectedArea) self.action = ACTION_MOVE_SELECTED self.redraw() elif self.action == ACTION_MOVE_SELECTED: self.selectedAreaRaw = QRect(self.selectedArea) self.redraw() # self.action = None elif self.action == ACTION_RECT: self.drawRect(self.startX, self.startY, event.x(), event.y(), True) self.redraw() elif self.action == ACTION_ELLIPSE: self.drawEllipse(self.startX, self.startY, event.x(), event.y(), True) self.redraw() elif self.action == ACTION_ARROW: self.drawArrow(self.startX, self.startY, event.x(), event.y(), True) self.redraw() elif self.action == ACTION_LINE: self.drawLine(self.startX, self.startY, event.x(), event.y(), True) self.redraw() elif self.action == ACTION_FREEPEN: self.drawFreeLine(self.pointPath, True) self.redraw() def detectMousePosition(self, point): """ :type point: QPoint :param point: the mouse position you want to check :return: """ if self.selectedArea == QRect(): self.mousePosition = MousePosition.OUTSIDE_AREA return if self.selectedArea.left() - ERRORRANGE <= point.x( ) <= self.selectedArea.left() and (self.selectedArea.top() - ERRORRANGE <= point.y() <= self.selectedArea.top()): self.mousePosition = MousePosition.ON_THE_TOP_LEFT_CORNER elif self.selectedArea.right() <= point.x() <= self.selectedArea.right( ) + ERRORRANGE and (self.selectedArea.top() - ERRORRANGE <= point.y() <= self.selectedArea.top()): self.mousePosition = MousePosition.ON_THE_TOP_RIGHT_CORNER elif self.selectedArea.left() - ERRORRANGE <= point.x( ) <= self.selectedArea.left() and ( self.selectedArea.bottom() <= point.y() <= self.selectedArea.bottom() + ERRORRANGE): self.mousePosition = MousePosition.ON_THE_BOTTOM_LEFT_CORNER elif self.selectedArea.right() <= point.x() <= self.selectedArea.right( ) + ERRORRANGE and (self.selectedArea.bottom() <= point.y() <= self.selectedArea.bottom() + ERRORRANGE): self.mousePosition = MousePosition.ON_THE_BOTTOM_RIGHT_CORNER elif -ERRORRANGE <= point.x() - self.selectedArea.left() <= 0 and ( self.selectedArea.topLeft().y() < point.y() < self.selectedArea.bottomLeft().y()): self.mousePosition = MousePosition.ON_THE_LEFT_SIDE elif 0 <= point.x() - self.selectedArea.right() <= ERRORRANGE and ( self.selectedArea.topRight().y() < point.y() < self.selectedArea.bottomRight().y()): self.mousePosition = MousePosition.ON_THE_RIGHT_SIDE elif -ERRORRANGE <= point.y() - self.selectedArea.top() <= 0 and ( self.selectedArea.topLeft().x() < point.x() < self.selectedArea.topRight().x()): self.mousePosition = MousePosition.ON_THE_UP_SIDE elif 0 <= point.y() - self.selectedArea.bottom() <= ERRORRANGE and ( self.selectedArea.bottomLeft().x() < point.x() < self.selectedArea.bottomRight().x()): self.mousePosition = MousePosition.ON_THE_DOWN_SIDE elif not self.selectedArea.contains(point): self.mousePosition = MousePosition.OUTSIDE_AREA else: self.mousePosition = MousePosition.INSIDE_AREA def setCursorStyle(self): if self.action in DRAW_ACTION: self.setCursor(Qt.CrossCursor) return if self.mousePosition == MousePosition.ON_THE_LEFT_SIDE or \ self.mousePosition == MousePosition.ON_THE_RIGHT_SIDE: self.setCursor(Qt.SizeHorCursor) elif self.mousePosition == MousePosition.ON_THE_UP_SIDE or \ self.mousePosition == MousePosition.ON_THE_DOWN_SIDE: self.setCursor(Qt.SizeVerCursor) elif self.mousePosition == MousePosition.ON_THE_TOP_LEFT_CORNER or \ self.mousePosition == MousePosition.ON_THE_BOTTOM_RIGHT_CORNER: self.setCursor(Qt.SizeFDiagCursor) elif self.mousePosition == MousePosition.ON_THE_TOP_RIGHT_CORNER or \ self.mousePosition == MousePosition.ON_THE_BOTTOM_LEFT_CORNER: self.setCursor(Qt.SizeBDiagCursor) elif self.mousePosition == MousePosition.OUTSIDE_AREA: self.setCursor(Qt.ArrowCursor) elif self.mousePosition == MousePosition.INSIDE_AREA: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) pass def drawMagnifier(self): # First, calculate the magnifier position due to the mouse position watchAreaWidth = 16 watchAreaHeight = 16 watchAreaPixmap = QPixmap() cursor_pos = self.mousePoint watchArea = QRect( QPoint(cursor_pos.x() - watchAreaWidth / 2, cursor_pos.y() - watchAreaHeight / 2), QPoint(cursor_pos.x() + watchAreaWidth / 2, cursor_pos.y() + watchAreaHeight / 2)) if watchArea.left() < 0: watchArea.moveLeft(0) watchArea.moveRight(watchAreaWidth) if self.mousePoint.x() + watchAreaWidth / 2 >= self.screenPixel.width( ): watchArea.moveRight(self.screenPixel.width() - 1) watchArea.moveLeft(watchArea.right() - watchAreaWidth) if self.mousePoint.y() - watchAreaHeight / 2 < 0: watchArea.moveTop(0) watchArea.moveBottom(watchAreaHeight) if self.mousePoint.y( ) + watchAreaHeight / 2 >= self.screenPixel.height(): watchArea.moveBottom(self.screenPixel.height() - 1) watchArea.moveTop(watchArea.bottom() - watchAreaHeight) # tricks to solve the hidpi impact on QCursor.pos() watchArea.setTopLeft( QPoint(watchArea.topLeft().x() * self.scale, watchArea.topLeft().y() * self.scale)) watchArea.setBottomRight( QPoint(watchArea.bottomRight().x() * self.scale, watchArea.bottomRight().y() * self.scale)) watchAreaPixmap = self.screenPixel.copy(watchArea) # second, calculate the magnifier area magnifierAreaWidth = watchAreaWidth * 10 magnifierAreaHeight = watchAreaHeight * 10 fontAreaHeight = 40 cursorSize = 24 magnifierArea = QRectF( QPoint(QCursor.pos().x() + cursorSize, QCursor.pos().y() + cursorSize), QPoint(QCursor.pos().x() + cursorSize + magnifierAreaWidth, QCursor.pos().y() + cursorSize + magnifierAreaHeight)) if magnifierArea.right() >= self.screenPixel.width(): magnifierArea.moveLeft(QCursor.pos().x() - magnifierAreaWidth - cursorSize / 2) if magnifierArea.bottom() + fontAreaHeight >= self.screenPixel.height( ): magnifierArea.moveTop(QCursor.pos().y() - magnifierAreaHeight - cursorSize / 2 - fontAreaHeight) # third, draw the watch area to magnifier area watchAreaScaled = watchAreaPixmap.scaled( QSize(magnifierAreaWidth * self.scale, magnifierAreaHeight * self.scale)) magnifierPixmap = self.graphicsScene.addPixmap(watchAreaScaled) magnifierPixmap.setOffset(magnifierArea.topLeft()) # then draw lines and text self.graphicsScene.addRect(QRectF(magnifierArea), QPen(QColor(255, 255, 255), 2)) self.graphicsScene.addLine( QLineF(QPointF(magnifierArea.center().x(), magnifierArea.top()), QPointF(magnifierArea.center().x(), magnifierArea.bottom())), QPen(QColor(0, 255, 255), 2)) self.graphicsScene.addLine( QLineF(QPointF(magnifierArea.left(), magnifierArea.center().y()), QPointF(magnifierArea.right(), magnifierArea.center().y())), QPen(QColor(0, 255, 255), 2)) # get the rgb of mouse point pointRgb = QColor(self.screenPixel.toImage().pixel(self.mousePoint)) # draw information self.graphicsScene.addRect( QRectF( magnifierArea.bottomLeft(), magnifierArea.bottomRight() + QPoint(0, fontAreaHeight + 30)), Qt.black, QBrush(Qt.black)) rgbInfo = self.graphicsScene.addSimpleText( ' Rgb: ({0}, {1}, {2})'.format(pointRgb.red(), pointRgb.green(), pointRgb.blue())) rgbInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 5)) rgbInfo.setPen(QPen(QColor(255, 255, 255), 2)) rect = self.selectedArea.normalized() sizeInfo = self.graphicsScene.addSimpleText(' Size: {0} x {1}'.format( rect.width() * self.scale, rect.height() * self.scale)) sizeInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 15) + QPoint(0, fontAreaHeight / 2)) sizeInfo.setPen(QPen(QColor(255, 255, 255), 2)) def get_scale(self): return self.devicePixelRatio() def saveScreenshot(self, clipboard=False, fileName='screenshot.png', picType='png'): fullWindow = QRect(0, 0, self.width() - 1, self.height() - 1) selected = QRect(self.selectedArea) if selected.left() < 0: selected.setLeft(0) if selected.right() >= self.width(): selected.setRight(self.width() - 1) if selected.top() < 0: selected.setTop(0) if selected.bottom() >= self.height(): selected.setBottom(self.height() - 1) source = (fullWindow & selected) source.setTopLeft( QPoint(source.topLeft().x() * self.scale, source.topLeft().y() * self.scale)) source.setBottomRight( QPoint(source.bottomRight().x() * self.scale, source.bottomRight().y() * self.scale)) image = self.screenPixel.copy(source) image.setDevicePixelRatio(1) if clipboard: QGuiApplication.clipboard().setImage(QImage(image), QClipboard.Clipboard) else: image.save(fileName, picType, 10) self.target_img = image self.screen_shot_grabed.emit(QImage(image)) def redraw(self): self.graphicsScene.clear() # draw screenshot self.graphicsScene.addPixmap(self.screenPixel) # prepare for drawing selected area rect = QRectF(self.selectedArea) rect = rect.normalized() topLeftPoint = rect.topLeft() topRightPoint = rect.topRight() bottomLeftPoint = rect.bottomLeft() bottomRightPoint = rect.bottomRight() topMiddlePoint = (topLeftPoint + topRightPoint) / 2 leftMiddlePoint = (topLeftPoint + bottomLeftPoint) / 2 bottomMiddlePoint = (bottomLeftPoint + bottomRightPoint) / 2 rightMiddlePoint = (topRightPoint + bottomRightPoint) / 2 # draw the picture mask mask = QColor(0, 0, 0, 155) if self.selectedArea == QRect(): self.graphicsScene.addRect(0, 0, self.screenPixel.width(), self.screenPixel.height(), QPen(Qt.NoPen), mask) else: self.graphicsScene.addRect(0, 0, self.screenPixel.width(), topRightPoint.y(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect(0, topLeftPoint.y(), topLeftPoint.x(), rect.height(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect( topRightPoint.x(), topRightPoint.y(), self.screenPixel.width() - topRightPoint.x(), rect.height(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect( 0, bottomLeftPoint.y(), self.screenPixel.width(), self.screenPixel.height() - bottomLeftPoint.y(), QPen(Qt.NoPen), mask) # draw the toolBar if self.action != ACTION_SELECT: spacing = 5 # show the toolbar first, then move it to the correct position # because the width of it may be wrong if this is the first time it shows self.tooBar.show() dest = QPointF(rect.bottomRight() - QPointF(self.tooBar.width(), 0) - QPointF(spacing, -spacing)) if dest.x() < spacing: dest.setX(spacing) pen_set_bar_height = self.penSetBar.height( ) if self.penSetBar is not None else 0 if dest.y() + self.tooBar.height( ) + pen_set_bar_height >= self.height(): if rect.top() - self.tooBar.height( ) - pen_set_bar_height < spacing: dest.setY(rect.top() + spacing) else: dest.setY(rect.top() - self.tooBar.height() - pen_set_bar_height - spacing) self.tooBar.move(dest.toPoint()) if self.penSetBar is not None: self.penSetBar.show() self.penSetBar.move(dest.toPoint() + QPoint(0, self.tooBar.height() + spacing)) if self.action == ACTION_TEXT: self.penSetBar.showFontWidget() else: self.penSetBar.showPenWidget() else: self.tooBar.hide() if self.penSetBar is not None: self.penSetBar.hide() # draw the list for step in self.drawListResult: self.drawOneStep(step) if self.drawListProcess is not None: self.drawOneStep(self.drawListProcess) if self.action != ACTION_TEXT: self.drawListProcess = None if self.selectedArea != QRect(): self.itemsToRemove = [] # draw the selected rectangle pen = QPen(QColor(0, 255, 255), 2) self.itemsToRemove.append(self.graphicsScene.addRect(rect, pen)) # draw the drag point radius = QPoint(3, 3) brush = QBrush(QColor(0, 255, 255)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topLeftPoint - radius, topLeftPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topMiddlePoint - radius, topMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topRightPoint - radius, topRightPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(leftMiddlePoint - radius, leftMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(rightMiddlePoint - radius, rightMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomLeftPoint - radius, bottomLeftPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomMiddlePoint - radius, bottomMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomRightPoint - radius, bottomRightPoint + radius), pen, brush)) # draw the textedit if self.textPosition is not None: textSpacing = 50 position = QPoint() if self.textPosition.x() + self.textInput.width( ) >= self.screenPixel.width(): position.setX(self.textPosition.x() - self.textInput.width()) else: position.setX(self.textPosition.x()) if self.textRect is not None: if self.textPosition.y() + self.textInput.height( ) + self.textRect.height() >= self.screenPixel.height(): position.setY(self.textPosition.y() - self.textInput.height() - self.textRect.height()) else: position.setY(self.textPosition.y() + self.textRect.height()) else: if self.textPosition.y() + self.textInput.height( ) >= self.screenPixel.height(): position.setY(self.textPosition.y() - self.textInput.height()) else: position.setY(self.textPosition.y()) self.textInput.move(position) self.textInput.show() # self.textInput.getFocus() # draw the magnifier if self.action == ACTION_SELECT: self.drawMagnifier() if self.mousePressed: self.drawSizeInfo() if self.action == ACTION_MOVE_SELECTED: self.drawSizeInfo() # deal with every step in drawList def drawOneStep(self, step): """ :type step: tuple """ if step[0] == ACTION_RECT: self.graphicsScene.addRect( QRectF(QPointF(step[1], step[2]), QPointF(step[3], step[4])), step[5]) elif step[0] == ACTION_ELLIPSE: self.graphicsScene.addEllipse( QRectF(QPointF(step[1], step[2]), QPointF(step[3], step[4])), step[5]) elif step[0] == ACTION_ARROW: arrow = QPolygonF() linex = float(step[1] - step[3]) liney = float(step[2] - step[4]) line = sqrt(pow(linex, 2) + pow(liney, 2)) # in case to divided by 0 if line == 0: return sinAngel = liney / line cosAngel = linex / line # sideLength is the length of bottom side of the body of an arrow # arrowSize is the size of the head of an arrow, left and right # sides' size is arrowSize, and the bottom side's size is arrowSize / 2 sideLength = step[5].width() arrowSize = 8 bottomSize = arrowSize / 2 tmpPoint = QPointF(step[3] + arrowSize * sideLength * cosAngel, step[4] + arrowSize * sideLength * sinAngel) point1 = QPointF(step[1] + sideLength * sinAngel, step[2] - sideLength * cosAngel) point2 = QPointF(step[1] - sideLength * sinAngel, step[2] + sideLength * cosAngel) point3 = QPointF(tmpPoint.x() - sideLength * sinAngel, tmpPoint.y() + sideLength * cosAngel) point4 = QPointF(tmpPoint.x() - bottomSize * sideLength * sinAngel, tmpPoint.y() + bottomSize * sideLength * cosAngel) point5 = QPointF(step[3], step[4]) point6 = QPointF(tmpPoint.x() + bottomSize * sideLength * sinAngel, tmpPoint.y() - bottomSize * sideLength * cosAngel) point7 = QPointF(tmpPoint.x() + sideLength * sinAngel, tmpPoint.y() - sideLength * cosAngel) arrow.append(point1) arrow.append(point2) arrow.append(point3) arrow.append(point4) arrow.append(point5) arrow.append(point6) arrow.append(point7) arrow.append(point1) self.graphicsScene.addPolygon(arrow, step[5], step[6]) elif step[0] == ACTION_LINE: self.graphicsScene.addLine( QLineF(QPointF(step[1], step[2]), QPointF(step[3], step[4])), step[5]) elif step[0] == ACTION_FREEPEN: self.graphicsScene.addPath(step[1], step[2]) elif step[0] == ACTION_TEXT: textAdd = self.graphicsScene.addSimpleText(step[1], step[2]) textAdd.setPos(step[3]) textAdd.setBrush(QBrush(step[4])) self.textRect = textAdd.boundingRect() # draw the size information on the top left corner def drawSizeInfo(self): sizeInfoAreaWidth = 200 sizeInfoAreaHeight = 30 spacing = 5 rect = self.selectedArea.normalized() sizeInfoArea = QRect(rect.left(), rect.top() - spacing - sizeInfoAreaHeight, sizeInfoAreaWidth, sizeInfoAreaHeight) if sizeInfoArea.top() < 0: sizeInfoArea.moveTopLeft(rect.topLeft() + QPoint(spacing, spacing)) if sizeInfoArea.right() >= self.screenPixel.width(): sizeInfoArea.moveTopLeft(rect.topLeft() - QPoint(spacing, spacing) - QPoint(sizeInfoAreaWidth, 0)) if sizeInfoArea.left() < spacing: sizeInfoArea.moveLeft(spacing) if sizeInfoArea.top() < spacing: sizeInfoArea.moveTop(spacing) self.itemsToRemove.append( self.graphicsScene.addRect(QRectF(sizeInfoArea), Qt.white, QBrush(Qt.black))) sizeInfo = self.graphicsScene.addSimpleText(' {0} x {1}'.format( rect.width() * self.scale, rect.height() * self.scale)) sizeInfo.setPos(sizeInfoArea.topLeft() + QPoint(0, 2)) sizeInfo.setPen(QPen(QColor(255, 255, 255), 2)) self.itemsToRemove.append(sizeInfo) def drawRect(self, x1, x2, y1, y2, result): rect = self.selectedArea.normalized() tmpRect = QRect(QPoint(x1, x2), QPoint(y1, y2)).normalized() resultRect = rect & tmpRect tmp = [ ACTION_RECT, resultRect.topLeft().x(), resultRect.topLeft().y(), resultRect.bottomRight().x(), resultRect.bottomRight().y(), QPen(QColor(self.penColorNow), int(self.penSizeNow)) ] if result: self.drawListResult.append(tmp) else: self.drawListProcess = tmp def drawEllipse(self, x1, x2, y1, y2, result): rect = self.selectedArea.normalized() tmpRect = QRect(QPoint(x1, x2), QPoint(y1, y2)).normalized() resultRect = rect & tmpRect tmp = [ ACTION_ELLIPSE, resultRect.topLeft().x(), resultRect.topLeft().y(), resultRect.bottomRight().x(), resultRect.bottomRight().y(), QPen(QColor(self.penColorNow), int(self.penSizeNow)) ] if result: self.drawListResult.append(tmp) else: self.drawListProcess = tmp def drawArrow(self, x1, x2, y1, y2, result): rect = self.selectedArea.normalized() if y1 <= rect.left(): y1 = rect.left() elif y1 >= rect.right(): y1 = rect.right() if y2 <= rect.top(): y2 = rect.top() elif y2 >= rect.bottom(): y2 = rect.bottom() tmp = [ ACTION_ARROW, x1, x2, y1, y2, QPen(QColor(self.penColorNow), int(self.penSizeNow)), QBrush(QColor(self.penColorNow)) ] if result: self.drawListResult.append(tmp) else: self.drawListProcess = tmp def drawLine(self, x1, x2, y1, y2, result): rect = self.selectedArea.normalized() if y1 <= rect.left(): y1 = rect.left() elif y1 >= rect.right(): y1 = rect.right() if y2 <= rect.top(): y2 = rect.top() elif y2 >= rect.bottom(): y2 = rect.bottom() tmp = [ ACTION_LINE, x1, x2, y1, y2, QPen(QColor(self.penColorNow), int(self.penSizeNow)) ] if result: self.drawListResult.append(tmp) else: self.drawListProcess = tmp def drawFreeLine(self, pointPath, result): tmp = [ ACTION_FREEPEN, QPainterPath(pointPath), QPen(QColor(self.penColorNow), int(self.penSizeNow)) ] if result: self.drawListResult.append(tmp) else: self.drawListProcess = tmp def textChange(self): if self.textPosition is None: return self.text = self.textInput.getText() self.drawListProcess = [ ACTION_TEXT, str(self.text), QFont(self.fontNow), QPoint(self.textPosition), QColor(self.penColorNow) ] self.redraw() def undoOperation(self): if len(self.drawListResult) == 0: self.action = ACTION_SELECT self.selectedArea = QRect() self.selectedAreaRaw = QRect() self.tooBar.hide() if self.penSetBar is not None: self.penSetBar.hide() else: self.drawListResult.pop() self.redraw() def saveOperation(self): filename = QFileDialog.getSaveFileName(self, 'Save file', './screenshot.png', '*.png;;*.jpg') if len(filename[0]) == 0: return else: self.saveScreenshot(False, filename[0], filename[1][2:]) self.close() def close(self): self.widget_closed.emit() super().close() self.tooBar.close() if self.penSetBar is not None: self.penSetBar.close() def saveToClipboard(self): QApplication.clipboard().setText('Test in save function') self.saveScreenshot(True) self.close() # slots def changeAction(self, nextAction): QApplication.clipboard().setText('Test in changeAction function') if nextAction == ACTION_UNDO: self.undoOperation() elif nextAction == ACTION_SAVE: self.saveOperation() elif nextAction == ACTION_CANCEL: self.close() elif nextAction == ACTION_SURE: self.saveToClipboard() else: self.action = nextAction self.setFocus() def changePenSize(self, nextPenSize): self.penSizeNow = nextPenSize def changePenColor(self, nextPenColor): self.penColorNow = nextPenColor def cancelInput(self): self.drawListProcess = None self.textPosition = None self.textRect = None self.textInput.hide() self.textInput.clearText() self.redraw() def okInput(self): self.text = self.textInput.getText() self.drawListResult.append([ ACTION_TEXT, str(self.text), QFont(self.fontNow), QPoint(self.textPosition), QColor(self.penColorNow) ]) self.textPosition = None self.textRect = None self.textInput.hide() self.textInput.clearText() self.redraw() def changeFont(self, font): self.fontNow = font
class imwin(QGraphicsView): #Subclass QLabel for interaction w/ QPixmap def __init__(self, parent=None): super(imwin, self).__init__(parent) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.pixmap = None self._lastpos = None self._thispos = None self.delta = QtCore.QPointF(0, 0) self.nm = None self.measuring_length = False self.measuring_widths = False self.measuring_area = False self.measuring_angle = False self._zoom = 1 self.newPos = None self.oldPos = None self.factor = 1.0 self.numwidths = None self.widthNames = [] #initialize as empty list self.d = {} #dictionary for line items #self.k = 0 #initialize counter so lines turn yellow self.L = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.scene.realline = None self.scene.testline = None self.setMouseTracking(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) self.setRenderHint(QtGui.QPainter.Antialiasing, True) self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, True) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setInteractive(False) def keyPressEvent(self, event): #shift modifier for panning if event.key() == QtCore.Qt.Key_Shift: pos = QtGui.QCursor.pos() self.oldPos = self.mapToScene(self.mapFromGlobal(pos)) def mouseMoveEvent(self, event): data = self.mapToScene(event.pos()) rules = [ self.measuring_length, self.measuring_angle, self.measuring_area ] modifiers = QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ShiftModifier and self.oldPos: QApplication.setOverrideCursor(QtCore.Qt.PointingHandCursor) self.newPos = data delta = self.newPos - self.oldPos self.translate(delta.x(), delta.y()) elif (any(rules) or self.measuring_widths): QApplication.setOverrideCursor( QtCore.Qt.CrossCursor) #change cursor else: QApplication.setOverrideCursor( QtCore.Qt.ArrowCursor) #change cursor #dragging line if self._thispos and any(rules): if self.measuring_length: self.parent().statusbar.showMessage( 'Click to place next point... double click to finish') if self.measuring_area: self.parent().statusbar.showMessage( 'Click to place next point... close polygon to finish') if self.measuring_angle: self.parent().statusbar.showMessage( 'Click point to define vector') end = QtCore.QPointF(data) #self.mapToScene(event.pos())) start = self._thispos if self.measuring_angle and self._lastpos: start = self._thispos if self.scene.testline: #remove old line self.scene.removeItem(self.scene.testline) self.scene.testline = False if self.measuring_area and self.line_count > 2: intersect, xi, yi, k = self.A.checkIntersect( data.x(), data.y()) if self.scene.area_ellipseItem: #remove existing intersect self.scene.removeItem(self.scene.area_ellipseItem) self.scene.area_ellipseItem = False if self.scene.polyItem: self.scene.removeItem(self.scene.polyItem) self.scene.polyItem = False if intersect: #indicate intersect point p = QtCore.QPointF(xi, yi) self.scene.area_ellipseItem = QGraphicsEllipseItem( 0, 0, 10, 10) self.scene.area_ellipseItem.setPos(p.x() - 10 / 2, p.y() - 10 / 2) self.scene.area_ellipseItem.setBrush( QtGui.QBrush(QtCore.Qt.blue, style=QtCore.Qt.SolidPattern)) self.scene.area_ellipseItem.setFlag( QGraphicsItem.ItemIgnoresTransformations, False ) #size stays small, but doesnt translate if set to false self.scene.addItem(self.scene.area_ellipseItem) #shade polygon region points = [ QtCore.QPointF(x, y) for x, y in zip(self.A.x[k:], self.A.y[k:]) ] points.append(QtCore.QPointF(xi, yi)) self.scene.polyItem = QGraphicsPolygonItem( QtGui.QPolygonF(points)) self.scene.polyItem.setBrush( QtGui.QBrush(QtGui.QColor(255, 255, 255, 127))) self.scene.addItem(self.scene.polyItem) self.scene.testline = QGraphicsLineItem(QtCore.QLineF(start, end)) self.scene.addItem(self.scene.testline) def mouseDoubleClickEvent(self, event): def qpt2pt(x, y): Q = self.mapFromScene(self.mapToScene(x, y)) return Q.x(), Q.y() #only delete lines if bezier fit if self.measuring_length and self.parent().bezier.isChecked() and (len( np.vstack((self.L.x, self.L.y)).T) > 2): self.parent().statusbar.showMessage('Length measurement complete.') #Remove most recent items drawn (exact lines) nl = self.line_count for k, i in enumerate(self.scene.items()): if k < nl: self.scene.removeItem(i) #set item to false? if self._lastpos and self.measuring_length: # catmull roms spline instead? # https://codeplea.com/introduction-to-splines if (self.parent().bezier.isChecked()) and (len( np.vstack((self.L.x, self.L.y)).T) > 2): nt = 2000 #max(1000, self.numwidths * 50) #num of interpolating points # https://gist.github.com/Alquimista/1274149 # https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html def bernstein(i, n, t): return comb(n, i) * t**(n - i) * (1 - t)**i def bezier_rational(points, nt): n = len(points) xp = np.array([p[0] for p in points]) yp = np.array([p[1] for p in points]) t = np.linspace(0.0, 1.0, nt) #Bezier curve B = np.array([bernstein(i, n - 1, t) for i in range(0, n)]) xb = np.dot(xp, B)[::-1] yb = np.dot(yp, B)[::-1] #Analytic gradient for bezier curve Qx = n * np.diff(xp) Qy = n * np.diff(yp) Bq = np.array( [bernstein(i, n - 2, t) for i in range(0, n - 1)]) dxb = np.dot(Qx, Bq)[::-1] dyb = np.dot(Qy, Bq)[::-1] m = np.vstack((dxb, dyb)) m *= (1 / np.linalg.norm(m, axis=0)) return xb, yb, m points = np.vstack((self.L.x, self.L.y)).T self.xs, self.ys, self.m = bezier_rational(points, nt) pts = np.array(list(map(qpt2pt, self.xs, self.ys))) x, y = pts[:, 0], pts[:, 1] #integrate for length self.l = np.cumsum(np.hypot(np.gradient(x), np.gradient(y))) #self.measurements[-1] = self.l[-1] #self.lengths[-1] = self.l[-1] #draw cubic line to interpolated points for i in range(1, nt - 1): start = self.mapFromScene( self.mapToScene(self.xs[i - 1], self.ys[i - 1])) #+ self.pos() mid = self.mapFromScene( self.mapToScene(self.xs[i], self.ys[i])) #+ self.pos() end = self.mapFromScene( self.mapToScene(self.xs[i + 1], self.ys[i + 1])) # + self.pos() path = QtGui.QPainterPath(start) path.cubicTo(start, mid, end) self.scene.addPath(path) if (not self.parent().bezier.isChecked()) or (len( np.vstack((self.L.x, self.L.y)).T) <= 2): pts = np.array(list(map(qpt2pt, self.L.x, self.L.y))) x, y = pts[:, 0], pts[:, 1] self.l = np.cumsum(np.hypot(np.diff(x), np.diff(y))) #integrate for length #self.measurements[-1] = self.l[-1] self.lengths[-1] = self.l[-1] self.lengths.extend([np.nan]) self.widths.append([]) self.widthNames.append([]) #self.measurements.extend([np.nan]) QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor) #change cursor if self.parent().bezier.isChecked(): self.parent().widthsButton.setEnabled(True) self.parent().lengthButton.setChecked(False) self.parent().angleButton.setChecked(False) self.measuring_length = False self.measuring_angle = False self._thispos = False def polyClose(self): #make into hot key not button if self.measuring_area: if self.line_count > 2: #cant make polygon w/ two lines self.measuring_area = False A = self.A.calcArea() self.areaValues = np.append(self.areaValues, A) #add area values #draw permanent polygon points = [ QtCore.QPointF(x, y) for x, y in zip(self.A.x, self.A.y) ] self.scene.polyItem2 = QGraphicsPolygonItem( QtGui.QPolygonF(points)) self.scene.polyItem2.setBrush( QtGui.QBrush(QtGui.QColor(255, 255, 255, 127))) if self.scene.polyItem: self.scene.removeItem( self.scene.polyItem) #remove mouseover polygon self.scene.polyItem = False #remove mouseover polygon self.scene.removeItem(self.scene.testline) self.scene.testline = False self.scene.addItem(self.scene.polyItem2) #shade in polygon self.parent().statusbar.showMessage( 'Polygon area measurement completed') self.parent().areaButton.setChecked(False) self.parent().bezier.setEnabled( True) #make bezier fit available again else: print("cannot draw polygon with fewer than three vertices") def measure_widths(self): def qpt2pt(x, y): Q = self.mapFromScene(self.mapToScene(x, y)) return Q.x(), Q.y() self.measuring_widths = True self.parent().widthsButton.setChecked(True) self.k = 0 self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) #preallocate custom widths #number of possible measurements per segment (length + #widths) #self.measurements = np.empty((0, self.iw.nm + 1), int) * np.nan self.numwidths = int(self.parent().subWin.numwidths.text()) #self.measurements[-1] = np.append( self.l[-1], np.zeros(self.numwidths-1)*np.nan ) #preallocate measurements self.widths[-1] = np.empty(self.numwidths - 1, dtype='float') #preallocate measurements self.widthNames[-1] = [ '{0:2.2f}% Width'.format(100 * f / self.numwidths) for f in np.arange(1, self.numwidths) ] self.nspines = 2 * (self.numwidths - 1) self.parent().statusbar.showMessage( 'Click point along spines to make width measurements perpindicular to the length segment' ) #get pts for width drawing bins = np.linspace(0, self.l[-1], self.numwidths + 1) inds = np.digitize(self.l, bins) __, self.inddec = np.unique(inds, return_index=True) pts = np.array(list(map(qpt2pt, self.xs, self.ys))) x, y = pts[:, 0], pts[:, 1] self.xp, self.yp = x[self.inddec], y[self.inddec] self.slopes = self.m[:, self.inddec] #Identify width spine points self.xsw = x[inds] self.ysw = y[inds] #Draw Widths for k, (x, y) in enumerate(zip(self.xp[1:-1], self.yp[1:-1])): x1, y1 = x, y L = self.pixmap_fit.width() H = self.pixmap_fit.height() v = self.slopes[:, k + 1] vx = v[1] vy = -v[0] t0 = np.hypot(L, H) t2 = 0 #intersect: rectangle for offset in ([0, 0], [L, H]): for ev in ([1, 0], [0, 1]): A = np.matrix([[vx, ev[0]], [vy, ev[1]]]) b = np.array([offset[0] - x1, offset[1] - y1]) T = np.linalg.solve(A, b)[0] t0 = min(T, t0, key=abs) #find nearest intersection to bounds #Find 2nd furthest intersection within bounds bounds = np.array([(L - x1) / vx, (H - y1) / vy, -x1 / vx, -y1 / vy]) t2 = max(-t0, np.sign(-t0) * np.partition(bounds, -2)[-2], key=abs) x0 = x1 + t0 * vx y0 = y1 + t0 * vy x2 = x1 + t2 * vx y2 = y1 + t2 * vy for l, (x, y) in enumerate(zip([x0, x2], [y0, y2])): start = QtCore.QPointF(x1, y1) end = QtCore.QPointF(x, y) self.scene.interpLine = QGraphicsLineItem( QtCore.QLineF(start, end)) self.d["{}".format(2 * k + l)] = self.scene.interpLine self.scene.addItem(self.scene.interpLine) if k == 0 and l == 0: self.scene.interpLine.setPen( QtGui.QPen(QtGui.QColor('yellow'))) def mousePressEvent(self, event): #http://pyqt.sourceforge.net/Docs/PyQt4/qgraphicsscenemouseevent.html #https://stackoverflow.com/questions/21197658/how-to-get-pixel-on-qgraphicspixmapitem-on-a-qgraphicsview-from-a-mouse-click data = self.mapToScene(event.pos()) #draw piecewise lines for non-width measurements rules = [ self.measuring_length, self.measuring_angle, self.measuring_area ] if self.scene.testline and self._thispos and any(rules): start = self._thispos end = QtCore.QPointF(data) if self._lastpos and self.measuring_angle: a = self._lastpos - self._thispos b = data - self._thispos a = np.array([a.x(), a.y()]) b = np.array([b.x(), b.y()]) self.measuring_angle = False t = np.arccos( np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) t *= 180 / np.pi #convert to degrees self.T.update(t) self.angleValues = np.append(self.angleValues, t) self.parent().statusbar.showMessage( 'Angle measurement complete') self.parent().angleButton.setChecked(False) self.parent().bezier.setEnabled(True) self.scene.realline = QGraphicsLineItem(QtCore.QLineF(start, end)) self.scene.addItem(self.scene.realline) #Collect piecewise line start/end points self._lastpos = self._thispos # save old position value self._thispos = QtCore.QPointF(data) # update current position if self.measuring_length: self.L.update(data.x(), data.y()) # update total length self.line_count += 1 elif self.measuring_area: self.line_count += 1 intersect = False if self.line_count > 2: #cant make polygon w/ two lines intersect, xi, yi, k = self.A.checkIntersect( data.x(), data.y()) self.parent().areaButton.setEnabled(True) if intersect: self.measuring_area = False self.A.update(xi, yi) #update with intersect point self.A.x, self.A.y = self.A.x[k:], self.A.y[ k:] #only use points after intersection A = self.A.calcArea() self.areaValues = np.append(self.areaValues, A) #add area values #draw permanent polygon points = [ QtCore.QPointF(x, y) for x, y in zip(self.A.x, self.A.y) ] self.scene.polyItem2 = QGraphicsPolygonItem( QtGui.QPolygonF(points)) self.scene.polyItem2.setBrush( QtGui.QBrush(QtGui.QColor(255, 255, 255, 127))) self.scene.removeItem( self.scene.polyItem) #remove mouseover polygon self.scene.polyItem = False #remove mouseover polygon self.scene.addItem(self.scene.polyItem2) #shade in polygon self.parent().statusbar.showMessage( 'Polygon area measurement completed') self.parent().areaButton.setChecked(False) self.parent().bezier.setEnabled( True) #make bezier fit available again QApplication.setOverrideCursor( QtCore.Qt.ArrowCursor) #change cursor else: self.A.update(data.x(), data.y()) #update with click point #https://stackoverflow.com/questions/30898846/qgraphicsview-items-not-being-placed-where-they-should-be if self.measuring_widths: #measure widths, snap to spines k = int(self.k / 2) + 1 #same origin for spine on either side x0, y0 = self.xp[k], self.yp[k] x1, y1 = data.x(), data.y() #perpindicular slopes vx = self.slopes[:, k][1] vy = -self.slopes[:, k][0] A = np.matrix([[vx, -vy], [vy, vx]]) b = np.array([x1 - x0, y1 - y0]) t = np.linalg.solve(A, b) xi = x0 + t[0] * vx yi = y0 + t[0] * vy self.W.update(xi, yi) p = QtCore.QPointF(xi, yi) s = 10 #dot size self.scene.ellipseItem = QGraphicsEllipseItem(0, 0, s, s) self.scene.ellipseItem.setPos(p.x() - s / 2, p.y() - s / 2) self.scene.ellipseItem.setBrush( QtGui.QBrush(QtCore.Qt.red, style=QtCore.Qt.SolidPattern)) self.scene.ellipseItem.setFlag( QGraphicsItem.ItemIgnoresTransformations, False) #size stays small, but doesnt translate if false self.scene.addItem(self.scene.ellipseItem) self.k += 1 if self.k < self.nspines: self.d[str(self.k)].setPen(QtGui.QPen( QtGui.QColor('yellow'))) #Highlight next spine if self.k == self.nspines: self.parent().statusbar.showMessage( 'Width measurements complete') self.measuring_widths = False self.parent().widthsButton.setEnabled(False) self.parent().widthsButton.setChecked(False) self.parent().bezier.setEnabled(True) width = np.sqrt( (self.W.x[1::2] - self.W.x[0::2])**2 + (self.W.y[1::2] - self.W.y[0::2])**2) #calculate widths #self.measurements[-1,1:] = width #update most recent row w/ length measurement #self.measurements[-1][1:] = width self.widths[-1] = width #MouseWheel Zoom def wheelEvent(self, event): #https://stackoverflow.com/questions/35508711/how-to-enable-pan-and-zoom-in-a-qgraphicsview #transform coordinates correctly #https://stackoverflow.com/questions/20942586/controlling-the-pan-to-anchor-a-point-when-zooming-into-an-image #https://stackoverflow.com/questions/41226194/pyqt4-pixel-information-on-rotated-image zoomInFactor = 1.05 zoomOutFactor = 1 / zoomInFactor self.setTransformationAnchor(QGraphicsView.NoAnchor) self.setResizeAnchor(QGraphicsView.NoAnchor) oldPos = self.mapToScene(event.pos()) #Zoom #https://quick-geek.github.io/answers/885796/index.html #y component for mouse with two wheels if event.angleDelta().y() > 0: zoomFactor = zoomInFactor else: zoomFactor = zoomOutFactor self.scale(zoomFactor, zoomFactor) newPos = self.mapToScene(event.pos()) #Get the new position delta = newPos - oldPos self.translate(delta.x(), delta.y()) #Move scene to old position
class NodesEditor(QDockWidget): nodeCreator = {} ##dict() def __init__(self, text, parent: QWidget = None, flags=Qt.WindowFlags()): super(NodesEditor, self).__init__(text, parent, flags) self.nodesViewArea = QGraphicsView(self) self.nodesScene = QGraphicsScene(self) self.setWidget(self.nodesViewArea) self.nodesViewArea.setAcceptDrops(True) self.nodesViewArea.setDragMode(QGraphicsView.RubberBandDrag) self.nodesViewArea.setScene(self.nodesScene) self.nodesScene.installEventFilter(self) self.nodes = [] self.connectFrom = None self.connectTo = None self.connectItem = None self.idconnect = -1 def addNode(self, node: Node): self.nodes.append(node) self.InitNodeItems(node) return node def DeleteNode(self, node: Node): #if node is None: # print("null delete") # return self.DeleteConnections(node) idn = -1 for i in range(0, len(self.nodes)): if node == self.nodes[i]: idn = i self.nodesScene.removeItem(node.item) node.item = 0 if idn >= 0: del self.nodes[idn] def DeleteConnections(self, node: Node): for i in range(0, len(self.nodes)): inputs = self.nodes[i].inputs for j in range(0, len(inputs)): if inputs[j].node == node or self.nodes[i] == node: if inputs[j].connector is not None: self.nodesScene.removeItem(inputs[j].connector) inputs[j].connector = None inputs[j].node = None inputs[j].out = 0 def DeleteConnection(self, item: QGraphicsItem): for i in range(0, len(self.nodes)): inputs = self.nodes[i].inputs for j in range(0, len(inputs)): if inputs[j].connector == item: if inputs[j].connector is not None: self.nodesScene.removeItem(inputs[j].connector) inputs[j].connector = None inputs[j].node = None inputs[j].out = 0 def GetNodeItem(self, item: QGraphicsItem): node = None if item is not None: if item.parentItem() is not None: item = item.parentItem() for i in range(0, len(self.nodes)): if self.nodes[i].item == item: node = self.nodes[i] return node def GetNode(self, pos: QPointF): item = self.nodesScene.itemAt(pos, self.nodesViewArea.transform()) node = None if item is not None: if item.parentItem() is not None: item = item.parentItem() for i in range(0, len(self.nodes)): if self.nodes[i].item == item: node = self.nodes[i] return node def eventFilter(self, obj, event): if event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Delete: lists = self.nodesScene.selectedItems() for i in range(0, len(lists)): if lists[i].type() == 2: #QGraphicsPathItem.Type: self.DeleteConnection(lists[i]) if lists[i].type() == 3: #QGraphicsRectItem.Type self.DeleteNode(self.GetNodeItem(lists[i])) self.clearConnection() self.updateConnectors() if event.type() == QEvent.GraphicsSceneMousePress and ( self.connectTo or self.connectFrom): if (event.buttons() & Qt.LeftButton) != 0: p = event.scenePos() node = self.GetNode(p) if node is not None: if self.connectTo is not None: ido = node.GetOutputId(p) if ido >= 0: self.connectTo.inputs[self.idconnect].node = node self.connectTo.inputs[self.idconnect].out = ido if self.connectFrom is not None: idi = node.GetInputId(p) if idi >= 0: node.inputs[idi].node = self.connectFrom node.inputs[idi].out = self.idconnect self.clearConnection() self.updateConnectors() if event.type() == QEvent.GraphicsSceneContextMenu: p = event.scenePos() node = self.GetNode(p) self.clearConnection() if node is not None: idi = node.GetInputId(p) ido = node.GetOutputId(p) if idi >= 0: self.connectTo = node self.idconnect = idi elif ido >= 0: self.connectFrom = node self.idconnect = ido if idi >= 0 or ido >= 0: return True if event.type() == QEvent.GraphicsSceneDragEnter or event.type( ) == QEvent.GraphicsSceneDragMove or event.type( ) == QEvent.GraphicsSceneDrop: event.acceptProposedAction() if event.type() == QEvent.GraphicsSceneDrop: bytearray = event.mimeData().data( event.mimeData().formats()[0]) data_items = self.decode_data(bytearray) text = data_items[0][Qt.DisplayRole].value() if text in NodesEditor.nodeCreator: node = NodesEditor.nodeCreator[text]() self.addNode(node).setPos(event.scenePos()) return True if event.type() == QEvent.GraphicsSceneMouseMove: if (event.buttons() & Qt.LeftButton) != 0: self.updateConnectors() if self.connectTo is not None or self.connectFrom is not None: p1 = QPointF(0, 0) p2 = QPointF(0, 0) if self.connectFrom is not None: p1 = event.scenePos() p2 = self.connectFrom.GetOutputPoint(self.idconnect) if self.connectTo is not None: p1 = self.connectTo.GetInputPoint(self.idconnect) p2 = event.scenePos() self.connectItem = self.updateConnector( self.connectItem, p1, p2, False) return QDockWidget.eventFilter(self, obj, event) def decode_data(self, bytearray): data = [] item = {} ds = QDataStream(bytearray) while not ds.atEnd(): row = ds.readInt32() column = ds.readInt32() map_items = ds.readInt32() for i in range(map_items): key = ds.readInt32() value = QVariant() ds >> value item[Qt.ItemDataRole(key)] = value data.append(item) return data def InitNodeItems(self, node: Node): wr = node.windowRect winRect = self.nodesScene.addRect(wr, QPen(), QBrush(QColor(0xFFFFFFFF))) winRect.setFlag(QGraphicsItem.ItemIsMovable) winRect.setFlag(QGraphicsItem.ItemIsSelectable) titleRect = self.nodesScene.addRect(wr.x(), wr.y(), wr.width(), 20) titleRect.setParentItem(winRect) title = self.nodesScene.addText(node.GetTitle()) title.setParentItem(titleRect) node.item = winRect node.ItemsInit(self.nodesScene) winRect.setRect(node.windowRect) def updateConnectors(self): for i in range(0, len(self.nodes)): c = self.nodes[i].inputs if len(c) <= 0: continue for j in range(0, len(c)): if c[j].node is not None: p1 = self.nodes[i].GetInputPoint(j) p2 = c[j].node.GetOutputPoint(c[j].out) c[j].connector = self.updateConnector( c[j].connector, p1, p2) elif (c[j].connector != None): self.nodesScene.removeItem(c[j].connector) c[j].connector = None def updateConnector(self, item: QGraphicsPathItem, p1: QPointF, p2: QPointF, select: bool = True): path = QPainterPath(p1) path.quadTo(p1 + QPointF(-15, 0), (p1 + p2) * 0.5) path.quadTo(p2 + QPointF(15, 0), p2) if item is None: item = self.nodesScene.addPath(path) else: item.setPath(path) item.setZValue(-1) if select: item.setFlag(QGraphicsItem.ItemIsSelectable) return item def clearConnection(self): self.connectFrom = None self.connectTo = None if self.connectItem is not None: self.nodesScene.removeItem(self.connectItem) self.connectItem = None def indexOfNode(self, node): for i in range(0, len(self.nodes)): if self.nodes[i] == node: return i return -1 def SeveToFile(self, path): #print(path) f = QFile(path) if f.open(QIODevice.WriteOnly): stream = QTextStream(f) stream.setCodec("UTF-8") doc = QDomDocument() xmlInstruct = doc.createProcessingInstruction( "xml", "version=\"1\" encoding=\"UTF-8\"") doc.appendChild(xmlInstruct) mainEl = doc.createElement("Nodes") doc.appendChild(mainEl) for i in range(0, len(self.nodes)): objectEl = doc.createElement("Node") mainEl.appendChild(objectEl) idoEl = doc.createElement("Id") objectEl.appendChild(idoEl) idoEltext = doc.createTextNode(str(i)) idoEl.appendChild(idoEltext) typeEl = doc.createElement("Type") objectEl.appendChild(typeEl) nameEltext = doc.createTextNode(type(self.nodes[i]).__name__) typeEl.appendChild(nameEltext) posEl = doc.createElement("Pos") objectEl.appendChild(posEl) pos = self.nodes[i].item.scenePos() posEltext = doc.createTextNode( str(pos.x()) + "," + str(pos.y())) posEl.appendChild(posEltext) liksEl = doc.createElement("Links") objectEl.appendChild(liksEl) inputs = self.nodes[i].inputs for j in range(0, len(inputs)): if inputs[j].node is not None: linkEl = doc.createElement("Link") liksEl.appendChild(linkEl) idEl = doc.createElement("Id") linkEl.appendChild(idEl) idEltext = doc.createTextNode(str(j)) idEl.appendChild(idEltext) nodeEl = doc.createElement("Node") linkEl.appendChild(nodeEl) idnEltext = doc.createTextNode( str(self.indexOfNode(inputs[j].node))) nodeEl.appendChild(idnEltext) outEl = doc.createElement("Out") linkEl.appendChild(outEl) outEltext = doc.createTextNode(str(inputs[j].out)) outEl.appendChild(outEltext) doc.save(stream, 4) f.close() return True return False def LoadFromFile(self, path): #print(path) self.nodes = [] self.nodesScene.clear() file = QFile(path) if not file.open(QIODevice.ReadOnly): return False doc = QDomDocument() if not doc.setContent(file): file.close() return False file.close() docElem = doc.documentElement() n = docElem.firstChild() while not n.isNull(): en = n.toElement() #print(en.tagName()) if en.tagName() == "Node": np = n.firstChild() typeN = "" pos = QPointF(0, 0) links = None while not np.isNull(): enp = np.toElement() if enp.tagName() == "Type": typeN = enp.text() if enp.tagName() == "Pos": post = enp.text().split(",") pos = QPointF(float(post[0]), float(post[1])) if enp.tagName() == "Links": links = np np = np.nextSibling() if typeN in NodesEditor.nodeCreator: node = NodesEditor.nodeCreator[typeN]() self.addNode(node).setPos(pos) if links is not None: nl = links.firstChild() while not nl.isNull(): npl = nl.firstChild() idl = -1 idn = -1 out = -1 while not npl.isNull(): enpl = npl.toElement() if enpl.tagName() == "Id": idl = int(enp.text()) if enpl.tagName() == "Node": idn = int(enp.text()) if enpl.tagName() == "Out": out = int(enp.text()) npl = npl.nextSibling() if idl >= 0: node.inputs[idl].node = self.nodes[idn] node.inputs[idl].out = out nl = nl.nextSibling() n = n.nextSibling() self.updateConnectors() return True
class TableViewWidget(QGraphicsView): g_table_view = None g_detect_size = 200 g_detect_text = "position" #g_detect_text = "quality" #g_detect_text = "none" g_rplidar_remanence = False g_rplidar_plot_life_ms = 1000 def __init__(self, parent = None, ihm_type='pc'): super(TableViewWidget, self).__init__(parent) if ihm_type=='pc': #self.setFixedSize(900,600) self.setFixedSize(960,660) elif ihm_type=='pc-mini': #self.setFixedSize(600,400) self.setFixedSize(640,440) else: #self.setFixedSize(225,150) self.setFixedSize(240,165) #self.setSceneRect(QRectF(0,-1500,2000,3000)) self.setSceneRect(QRectF(-100,-1600,2200,3200)) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._robots = {} self._waypoints = [] redium = QColor.fromCmykF(0,1,1,0.1) greenium = QColor.fromCmykF(0.7,0,0.9,0) blueium = QColor.fromCmykF(0.9,0.4,0,0) goldenium = QColor('white') yellow = QColor.fromCmykF(0,0.25,1,0) purple = QColor.fromCmykF(0.5,0.9,0,0.05) background = QColor(40,40,40) darker = QColor(20,20,20) # big_robot_poly = QPolygonF([ # QPointF(-135,-151), # QPointF(60,-151), # QPointF(170,-91), # QPointF(170,-45), # QPointF(111,-40), # QPointF(111,40), # QPointF(170,45), # QPointF(170,91), # QPointF(60,151), # QPointF(-135,151) # ]) little_robot_poly = QPolygonF([ QPointF( 50, 0), QPointF( 100, 85), QPointF( 65, 115), QPointF( -65, 115), QPointF(-100, 85), QPointF(-100, -85), QPointF( -65,-115), QPointF( 65,-115), QPointF( 100, -85) ]) #self._scene = QGraphicsScene(QRectF(0,-1500,2000,3000)) self._scene = QGraphicsScene(QRectF(-100,-1600,2200,3200)) # self._big_robot = self._scene.addPolygon(big_robot_poly, QPen(), QBrush(QColor('red'))) # self._big_robot.setZValue(1) #self._robots['little'] = Robot(self._scene) self._little_robot = self._scene.addPolygon(little_robot_poly, QPen(), QBrush(QColor('red'))) self._little_robot.setZValue(1) #self._friend_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('green'))) self._friend_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white'))) self._friend_robot.setZValue(1) self._friend_robot.setPos(-1 * 1000, -1 * 1000) if os.name == 'nt': self._friend_robot_text = self._scene.addText("0123456", QFont("Calibri",80)); else: self._friend_robot_text = self._scene.addText("0123456", QFont("System",40)); self._friend_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40) self._friend_robot_text.setRotation(-90) self._friend_robot_text.setTransform(QTransform(1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0)) self._friend_robot_text.setZValue(1) #self._adv1_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('white'))) self._adv1_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white'))) self._adv1_robot.setZValue(1) self._adv1_robot.setPos(-1 * 1000, -1 * 1000) if os.name == 'nt': self._adv1_robot_text = self._scene.addText("0", QFont("Calibri",80)); else: self._adv1_robot_text = self._scene.addText("0", QFont("System",40)); self._adv1_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40) self._adv1_robot_text.setRotation(-90) self._adv1_robot_text.setTransform(QTransform(1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0)) self._adv1_robot_text.setZValue(1) #self._adv2_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('blue'))) self._adv2_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white'))) self._adv2_robot.setZValue(1) self._adv2_robot.setPos(-1 * 1000, -1 * 1000) if os.name == 'nt': self._adv2_robot_text = self._scene.addText("0", QFont("Calibri",80)); else: self._adv2_robot_text = self._scene.addText("0", QFont("System",40)); self._adv2_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40) self._adv2_robot_text.setRotation(-90) self._adv2_robot_text.setTransform(QTransform(1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0)) self._adv2_robot_text.setZValue(1) self.setScene(self._scene) self.rotate(90) if ihm_type=='pc': self.scale(0.3, -0.3) elif ihm_type=='pc-mini': self.scale(0.2, -0.2) else: self.scale(0.075, -0.075) #self._scene.addRect(QRectF(0,-1500,2000,3000),QPen(), QBrush(background)) f=open("widgets/table_2020_600x400.png","rb") my_buff=f.read() test_img_pixmap2 = QPixmap() test_img_pixmap2.loadFromData(my_buff) #self.setPixmap(test_img_pixmap2) self._bg_img = QGraphicsPixmapItem(test_img_pixmap2) self._bg_img.setTransform(QTransform(1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.2)) self._bg_img.setRotation(-90) self._bg_img.setPos(0, -1500) self._scene.addItem(self._bg_img); # Scenario 2020 #Port principal "bleu" self._scene.addRect(QRectF(500,-1120,570,20),QPen(), QBrush(blueium)) self._scene.addRect(QRectF(500,-1500,30,400),QPen(), QBrush(greenium)) self._scene.addRect(QRectF(1070,-1500,30,400),QPen(), QBrush(redium)) #Port secondaire "bleu" self._scene.addRect(QRectF(1700,150,20,300),QPen(), QBrush(blueium)) self._scene.addRect(QRectF(1700,150,300,100),QPen(), QBrush(greenium)) self._scene.addRect(QRectF(1700,350,300,100),QPen(), QBrush(redium)) #Bouees cote "bleu" self._scene.addEllipse(QRectF(1200-35,-1200-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1080-35,-1050-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(510-35,-1050-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(400-35,-1200-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(100-35,-830-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(400-35,-550-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(800-35,-400-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1200-35,-230-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1650-35,-435-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1650-35,-165-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1955-35,-495-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1955-35,-105-35,70,70),QPen(), QBrush(greenium)) #Port principal "jaune" self._scene.addRect(QRectF(500,1100,570,20),QPen(), QBrush(yellow)) self._scene.addRect(QRectF(500,1100,30,400),QPen(), QBrush(redium)) self._scene.addRect(QRectF(1070,1100,30,400),QPen(), QBrush(greenium)) #Port secondaire "jaune" self._scene.addRect(QRectF(1700,-450,20,300),QPen(), QBrush(yellow)) self._scene.addRect(QRectF(1700,-450,300,100),QPen(), QBrush(greenium)) self._scene.addRect(QRectF(1700,-250,300,100),QPen(), QBrush(redium)) #Bouees cote "jaune" self._scene.addEllipse(QRectF(1200-35,1200-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1080-35,1050-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(510-35,1050-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(400-35,1200-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(100-35,830-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(400-35,550-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(800-35,400-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1200-35,230-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1650-35,435-35,70,70),QPen(), QBrush(redium)) self._scene.addEllipse(QRectF(1650-35,165-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1955-35,495-35,70,70),QPen(), QBrush(greenium)) self._scene.addEllipse(QRectF(1955-35,105-35,70,70),QPen(), QBrush(redium)) #dbg_plt_sz = 3 #self._scene.addEllipse(1000 - dbg_plt_sz, 0 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white'))) self._points = [] #self.setSceneRect(QRectF(0,-150,200,300)) self._traj_segm_l = [] self._debug_edit_mode = False self._debug_edit_point_l = [] # self._big_robot_x = 0 # self._big_robot_y = 0 self._little_robot_x = 0 self._little_robot_y = 0 self.last_plot_ts = 0 self.plot_graph_l = [] self._plot_items = [] TableViewWidget.g_table_view = self def set_strategy(self, strategy): greenium = QColor.fromCmykF(0.7,0,0.9,0) #greenium.setAlphaF(0.2) for id_, pos in strategy['strategy']['map']['waypoints'].items(): wp = self._scene.addEllipse(QRectF(pos[0]-10,pos[1]-10,20,20),QPen(), QBrush(greenium)) self._waypoints.append(wp) for id_, pose in strategy['strategy']['map']['poses'].items(): p = strategy['strategy']['map']['waypoints'][pose[0]] path = QPainterPath() cos_ = math.cos(pose[1] * math.pi / 180) sin_ = math.sin(pose[1] * math.pi / 180) l = 40 w = 20 path.moveTo(p[0] + l * cos_, p[1] + l * sin_) path.lineTo(p[0] -l * cos_ + w * sin_, p[1] - l * sin_ - w * cos_) path.lineTo(p[0] -l * cos_ - w * sin_, p[1] - l * sin_ + w * cos_) path.closeSubpath() itm = self._scene.addPath(path, QPen(), QBrush(greenium)) for id_, area in strategy['strategy']['map']['areas'].items(): path = QPainterPath() v = area['vertices'][0] path.moveTo(v[0], v[1]) for v in area['vertices'][1:]: path.lineTo(v[0], v[1]) path.closeSubpath() itm = self._scene.addPath(path, QPen(), QBrush(greenium)) self._waypoints.append(wp) def add_points(self, points): for p in points: pt = self._scene.addEllipse(p[0]-10, p[1]-10, 20, 20, QPen(), QBrush(QColor('grey'))) pt.setZValue(1) self._points.append((pt, p)) def sizeHint(self): return QSize(600,400) def set_client(self, client): self._client = client self._client.propulsion_telemetry.connect(self.update_telemetry) self._client.rplidar_plot.connect(self.update_plots) self._client.rplidar_robot_detection.connect(self.update_other_robots) def update_telemetry(self, telemetry): # self._big_robot.setPos(telemetry.x * 1000, telemetry.y * 1000) # self._big_robot.setRotation(telemetry.yaw * 180 / math.pi) # self._big_robot_x = telemetry.x * 1000 # self._big_robot_y = telemetry.y * 1000 self._little_robot.setPos(telemetry.pose.position.x * 1000, telemetry.pose.position.y * 1000) self._little_robot.setRotation(telemetry.pose.yaw * 180 / math.pi) self._little_robot_x = telemetry.pose.position.x * 1000 self._little_robot_y = telemetry.pose.position.y * 1000 def update_plots(self, my_plot): dbg_plt_sz = 1 for i in self._plot_items: self._scene.removeItem(i) self._plot_items = [] #self.last_plot_ts = my_plot.timestamp for pt in my_plot.points: itm = self._scene.addEllipse(pt.x * 1000 - dbg_plt_sz, pt.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('red')),4), QBrush(QColor('red'))) self._plot_items.append(itm) return my_plot_ellipse = self._scene.addEllipse(my_plot.x * 1000 - dbg_plt_sz, my_plot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('red')),4), QBrush(QColor('red'))) self.plot_graph_l.append((my_plot,my_plot_ellipse)) for rec in self.plot_graph_l: if (self.last_plot_ts-rec[0].timestamp>TableViewWidget.g_rplidar_plot_life_ms): rec_ellipse = rec[1] self._scene.removeItem(rec_ellipse) self.plot_graph_l.remove(rec) def update_other_robots(self, other_robot): dbg_plt_sz = 3 if (other_robot.id == 0): self._friend_robot.setPos(other_robot.x * 1000, other_robot.y * 1000) if (TableViewWidget.g_detect_text == "quality"): self._friend_robot_text.setPlainText("%d"%other_robot.samples) elif (TableViewWidget.g_detect_text == "position"): self._friend_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000)) else: self._friend_robot_text.setPlainText("") self._friend_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40) if TableViewWidget.g_rplidar_remanence: self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white'))) elif (other_robot.id == 1): self._adv1_robot.setPos(other_robot.x * 1000, other_robot.y * 1000) if (TableViewWidget.g_detect_text == "quality"): self._adv1_robot_text.setPlainText("%d"%other_robot.samples) elif (TableViewWidget.g_detect_text == "position"): self._adv1_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000)) else: self._adv1_robot_text.setPlainText("") self._adv1_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40) if TableViewWidget.g_rplidar_remanence: self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white'))) elif (other_robot.id == 2): self._adv2_robot.setPos(other_robot.x * 1000, other_robot.y * 1000) if (TableViewWidget.g_detect_text == "quality"): self._adv2_robot_text.setPlainText("%d"%other_robot.samples) elif (TableViewWidget.g_detect_text == "position"): self._adv2_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000)) else: self._adv2_robot_text.setPlainText("") self._adv2_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40) if TableViewWidget.g_rplidar_remanence: self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white'))) def debug_set_start(self, _new_x, _new_y): self.debug_start_x = _new_x self.debug_start_y = _new_y self.debug_cur_x = _new_x self.debug_cur_y = _new_y if self._debug_edit_mode: self._debug_edit_point_l.append((_new_x,_new_y)) def debug_line_to(self, _new_x, _new_y): my_segm = self._scene.addLine(self.debug_cur_x, self.debug_cur_y, _new_x, _new_y, QPen(QColor(255,255,255))); self._traj_segm_l.append(my_segm) self.debug_cur_x = _new_x self.debug_cur_y = _new_y def debug_clear_lines(self): for l in self._traj_segm_l: self._scene.removeItem(l) self._traj_segm_l = [] def debug_start_edit(self, _new_x, _new_y): self.debug_clear_lines() self._debug_edit_mode = True self.debug_start_x = _new_x self.debug_start_y = _new_y self.debug_cur_x = _new_x self.debug_cur_y = _new_y self._debug_edit_point_l = [(_new_x,_new_y)] def debug_start_edit_rel(self): self.debug_clear_lines() self._debug_edit_mode = True self.debug_start_x = self._little_robot_x self.debug_start_y = self._little_robot_y self.debug_cur_x = self._little_robot_x self.debug_cur_y = self._little_robot_y self._debug_edit_point_l = [(self._little_robot_x,self._little_robot_y)] def debug_stop_edit(self): self._debug_edit_mode = False return self._debug_edit_point_l def mousePressEvent(self, event): print ("pix:<{},{}>".format(event.x(),event.y())) #realY = 3000.0*(event.x()-450.0)/900.0 #realX = 2000.0*(event.y())/600.0 realY = 3200.0*(event.x()-480.0)/960.0 realX = 2200.0*(event.y()-30.0)/660.0 print ("real:<{},{}>".format(realX,realY)) if self._debug_edit_mode: self._debug_edit_point_l.append((realX,realY)) self.debug_line_to(realX, realY)
class Spectrum(QWidget): """Plot the power spectrum for a specified channel. Attributes ---------- parent : instance of QMainWindow the main window. x_limit : tuple or list 2 values specifying the limit on x-axis y_limit : tuple or list 2 values specifying the limit on y-axis log : bool log-transform the data or not idx_chan : instance of QComboBox the element with the list of channel names. idx_x_min : instance of QLineEdit value with min x value idx_x_max : instance of QLineEdit value with max x value idx_y_min : instance of QLineEdit value with min y value idx_y_max : instance of QLineEdit value with max y value idx_log : instance of QCheckBox widget that defines if log should be used or not idx_fig : instance of QGraphicsView the view with the power spectrum scene : instance of QGraphicsScene the scene with GraphicsItems Notes ----- If data contains NaN, it doesn't create any spectrum (feature or bug?). """ def __init__(self, parent): super().__init__() self.parent = parent self.config = ConfigSpectrum(self.display_window) self.selected_chan = None self.idx_chan = None self.idx_fig = None self.scene = None self.create() def create(self): """Create empty scene for power spectrum.""" self.idx_chan = QComboBox() self.idx_chan.activated.connect(self.display_window) self.idx_fig = QGraphicsView(self) self.idx_fig.scale(1, -1) layout = QVBoxLayout() layout.addWidget(self.idx_chan) layout.addWidget(self.idx_fig) self.setLayout(layout) self.resizeEvent(None) def show_channame(self, chan_name): self.selected_chan = self.idx_chan.currentIndex() self.idx_chan.clear() self.idx_chan.addItem(chan_name) self.idx_chan.setCurrentIndex(0) def update(self): """Add channel names to the combobox.""" self.idx_chan.clear() for chan_name in self.parent.traces.chan: self.idx_chan.addItem(chan_name) if self.selected_chan is not None: self.idx_chan.setCurrentIndex(self.selected_chan) self.selected_chan = None def display_window(self): """Read the channel name from QComboBox and plot its spectrum. This function is necessary it reads the data and it sends it to self.display. When the user selects a smaller chunk of data from the visible traces, then we don't need to call this function. """ if self.idx_chan.count() == 0: self.update() chan_name = self.idx_chan.currentText() lg.info('Power spectrum for channel ' + chan_name) if chan_name: trial = 0 data = self.parent.traces.data(trial=trial, chan=chan_name) self.display(data) else: self.scene.clear() def display(self, data): """Make graphicsitem for spectrum figure. Parameters ---------- data : ndarray 1D vector containing the data only This function can be called by self.display_window (which reads the data for the selected channel) or by the mouse-events functions in traces (which read chunks of data from the user-made selection). """ value = self.config.value self.scene = QGraphicsScene(value['x_min'], value['y_min'], value['x_max'] - value['x_min'], value['y_max'] - value['y_min']) self.idx_fig.setScene(self.scene) self.add_grid() self.resizeEvent(None) s_freq = self.parent.traces.data.s_freq f, Pxx = welch(data, fs=s_freq, nperseg=int(min((s_freq, len(data))))) # force int freq_limit = (value['x_min'] <= f) & (f <= value['x_max']) if self.config.value['log']: Pxx_to_plot = log(Pxx[freq_limit]) else: Pxx_to_plot = Pxx[freq_limit] self.scene.addPath(Path(f[freq_limit], Pxx_to_plot), QPen(QColor(LINE_COLOR), LINE_WIDTH)) def add_grid(self): """Add axis and ticks to figure. Notes ----- I know that visvis and pyqtgraphs can do this in much simpler way, but those packages create too large a padding around the figure and this is pretty fast. """ value = self.config.value # X-AXIS # x-bottom self.scene.addLine(value['x_min'], value['y_min'], value['x_min'], value['y_max'], QPen(QColor(LINE_COLOR), LINE_WIDTH)) # at y = 0, dashed self.scene.addLine(value['x_min'], 0, value['x_max'], 0, QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DashLine)) # ticks on y-axis y_high = int(floor(value['y_max'])) y_low = int(ceil(value['y_min'])) x_length = (value['x_max'] - value['x_min']) / value['x_tick'] for y in range(y_low, y_high): self.scene.addLine(value['x_min'], y, value['x_min'] + x_length, y, QPen(QColor(LINE_COLOR), LINE_WIDTH)) # Y-AXIS # left axis self.scene.addLine(value['x_min'], value['y_min'], value['x_max'], value['y_min'], QPen(QColor(LINE_COLOR), LINE_WIDTH)) # larger ticks on x-axis every 10 Hz x_high = int(floor(value['x_max'])) x_low = int(ceil(value['x_min'])) y_length = (value['y_max'] - value['y_min']) / value['y_tick'] for x in range(x_low, x_high, 10): self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length, QPen(QColor(LINE_COLOR), LINE_WIDTH)) # smaller ticks on x-axis every 10 Hz y_length = (value['y_max'] - value['y_min']) / value['y_tick'] / 2 for x in range(x_low, x_high, 5): self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length, QPen(QColor(LINE_COLOR), LINE_WIDTH)) def resizeEvent(self, event): """Fit the whole scene in view. Parameters ---------- event : instance of Qt.Event not important """ value = self.config.value self.idx_fig.fitInView(value['x_min'], value['y_min'], value['x_max'] - value['x_min'], value['y_max'] - value['y_min']) def reset(self): """Reset widget as new""" self.idx_chan.clear() if self.scene is not None: self.scene.clear() self.scene = None
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.ui = ui_main_window.Ui_MainWindow() self.ui.setupUi(self) self.cities = cities self.autoplay = False self._zoom = 1 self.genetic = None self.paths = [] self.last = None self.scene = QGraphicsScene() self.ui.graphicsView.setScene(self.scene) self.ui.popSize.setValue(20) self.ui.algorithm.addItem('ant', AntColony) self.ui.algorithm.addItem('genetic', Genetic) self.ui.algorithm.addItem('solver', Test) self.ui.playBtn.clicked.connect(self.on_play_click) self.ui.nextBtn.clicked.connect(self.next) self.ui.restartBtn.clicked.connect(self.restart) self.ui.showCur.stateChanged.connect(self.redraw) self.timer = QTimer() self.timer.timeout.connect(self.gen_next) self.timer.start(1) for city in self.cities: self.scene.addEllipse(city.x, city.y, 5, 5, QPen(), QBrush(QColor(255, 0, 0))) self.scene.addSimpleText(city.name).setPos(city.x + 5, city.y + 5) self.start() def restart(self): self.start() self.autoplay = True def start(self): try: seed = int(self.ui.seed.text()) except ValueError as e: seed = -1 if seed <= 0: seed = random.randint(0, 1000) #seed = 100 print(seed) random.seed(seed) np.random.seed(seed) self.clear_paths() self.ui.distance.setText("") clazz = self.ui.algorithm.currentData() self.genetic = clazz(self.cities, int(self.ui.popSize.text())).run({}) def on_play_click(self, play): self.autoplay = play def gen_next(self): if self.autoplay: self.next() @QtCore.pyqtSlot() def next(self): try: trajectories = next(self.genetic) except StopIteration: self.ui.playBtn.setChecked(False) self.autoplay = False return self.ui.distance.setText(f"{trajectories[0].distance:.2f} units") self.last = trajectories self.draw_trajectories(trajectories) def redraw(self): self.draw_trajectories(self.last) def draw_trajectories(self, trajectories): self.clear_paths() pens = [QPen(QColor(255, 0, 0)), QPen(QColor(0, 0, 0))] for i, trajectory in enumerate(trajectories): if i != 0 and not self.ui.showCur.isChecked(): break path = QPainterPath() path.moveTo(self.cities[trajectory.path[0]].x, self.cities[trajectory.path[0]].y) for hop in trajectory.path: city = self.cities[hop] path.lineTo(city.x, city.y) path.lineTo(self.cities[trajectory.path[0]].x, self.cities[trajectory.path[0]].y) self.paths.append(self.scene.addPath(path, pen=pens[i])) def clear_paths(self): for path in self.paths: self.scene.removeItem(path) self.paths = []
class imwin(QGraphicsView): #Subclass QLabel for interaction w/ QPixmap def __init__(self, parent=None): super(imwin, self).__init__(parent) QApplication.setOverrideCursor( QtCore.Qt.CrossCursor) #change cursor self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) #self.bezier_fit = True self.pixmap = None self._lastpos = None self._thispos = None self.delta = QtCore.QPointF(0, 0) self.nm = None self.measuring_custom = False self.measuring_length = False self.measuring_widths = False self.measuring_angle = False self._zoom = 1 self.newPos = None self.oldPos = None self.factor = 1.0 self.numwidths = None self.widthnames = [] #self.lengths = [] #self.widths = [] self.d = {} #dictionary for line items #self.k = 0 #initialize counter so lines turn yellow self.A = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.scene.realline = None self.scene.testline = None self.setMouseTracking(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) self.setRenderHint(QtGui.QPainter.Antialiasing, True) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setInteractive(False) #self.setFrameShape(QtGui.QFrame.NoFrame) def keyPressEvent(self, event): #shift modifier for panning if event.key() == QtCore.Qt.Key_Shift: pos = QtGui.QCursor.pos() self.oldPos = self.mapToScene(self.mapFromGlobal(pos)) def mouseMoveEvent(self, event): #Shift to pan modifiers = QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ShiftModifier and self.oldPos: self.newPos = self.mapToScene(event.pos()) delta = self.newPos - self.oldPos self.translate(delta.x(), delta.y()) #dragging line elif self._thispos and ( self.measuring_length or self.measuring_angle): #only on mouse press if self.measuring_length: self.parent().statusBar.showMessage( 'Click to place next point... double click to finish') if self.measuring_angle: self.parent().statusBar.showMessage( 'Click point to define vector') end = QtCore.QPointF(self.mapToScene(event.pos())) start = self._thispos if self.measuring_angle and self._lastpos: start = self._lastpos if self.scene.testline: #remove old line self.scene.removeItem(self.scene.testline) self.scene.testline = QGraphicsLineItem( QtCore.QLineF(start, end)) self.scene.addItem(self.scene.testline) def mouseDoubleClickEvent(self, event): def qpt2pt(x, y): Q = self.mapFromScene(self.mapToScene(x, y)) return Q.x(), Q.y() #only delete lines if bezier fit if self.measuring_length and self.parent().bezier.isChecked(): self.parent().statusBar.showMessage( 'Length measurement complete.') #Remove most recent items drawn (exact lines) nl = self.line_count for k, i in enumerate(self.scene.items()): if k < nl: self.scene.removeItem(i) if self._lastpos and not (self.measuring_widths or self.measuring_angle): #catmull roms spline instead? #https://codeplea.com/introduction-to-splines n = max(1000, self.numwidths * 50) #num of interpolating points if self.parent().bezier.isChecked(): #https://gist.github.com/Alquimista/1274149 def bernstein_poly(i, n, t): return comb(n, i) * (t**(n - i)) * (1 - t)**i points = np.vstack((self.A.x, self.A.y)).T def bezier_curve(points, nTimes=n): nPoints = len(points) xPoints = np.array([p[0] for p in points]) yPoints = np.array([p[1] for p in points]) t = np.linspace(0.0, 1.0, nTimes) polynomial_array = np.array([ bernstein_poly(i, nPoints - 1, t) for i in range(0, nPoints) ]) xvals = np.dot(xPoints, polynomial_array)[::-1] yvals = np.dot(yPoints, polynomial_array)[::-1] slopes = np.gradient(yvals) / np.gradient( xvals) #change with analytic gradient return xvals, yvals, slopes self.xs, self.ys, slopes = bezier_curve(points, nTimes=n) pts = np.array(list(map(qpt2pt, self.xs, self.ys))) x, y = pts[:, 0], pts[:, 1] self.l = np.cumsum(np.hypot( np.gradient(x), np.gradient(y))) #integrate for length add = np.concatenate( (self.l[-1], np.empty(self.nm) * np.nan), axis=None ) #add length and row of nans for possible widths add = np.expand_dims(add, axis=0) self.measurements = np.append(self.measurements, add, axis=0) #get pts for width drawing bins = np.linspace(0, self.l[-1], self.numwidths + 1) inds = np.digitize(self.l, bins) __, self.inddec = np.unique(inds, return_index=True) self.xp, self.yp = x[self.inddec], y[self.inddec] self.m = slopes[self.inddec] #Identify width spine points self.xsw = x[inds] self.ysw = y[inds] for i in range(1, n - 1): start = self.mapFromScene( self.mapToScene(self.xs[i - 1], self.ys[i - 1])) #+ self.pos() mid = self.mapFromScene( self.mapToScene(self.xs[i], self.ys[i])) #+ self.pos() end = self.mapFromScene( self.mapToScene(self.xs[i + 1], self.ys[i + 1])) # + self.pos() path = QtGui.QPainterPath(start) path.cubicTo(start, mid, end) self.scene.addPath(path) if not self.parent().bezier.isChecked(): pts = np.array(list(map(qpt2pt, self.A.x, self.A.y))) x, y = pts[:, 0], pts[:, 1] self.l = np.cumsum(np.hypot( np.diff(x), np.diff(y))) #integrate for length add = np.concatenate( (self.l[-1], np.empty(self.nm) * np.nan), axis=None ) #add length and row of nans for possible widths add = np.expand_dims(add, axis=0) self.measurements = np.append(self.measurements, add, axis=0) self.measuring_angle = False self.measuring_length = False self._thispos = False def measure_widths(self): self.measuring_widths = True self.k = 0 self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) #preallocate custom widths self.nspines = 2 * (self.numwidths - 1) self.parent().statusBar.showMessage( 'Click point along spines to make width measurements perpindicular to the length segment' ) #Draw widths for k, m in enumerate(self.m[1:-1]): #only middle widths x1, y1 = self.xp[k + 1], self.yp[k + 1] x2 = self.pixmap_fit.width() # - x1 y2 = -(1 / m) * (x2 - x1) + y1 y0 = self.pixmap_fit.height() # - y1 x0 = -m * (y0 - y1) + x1 #use larger distance if np.hypot((x1 - x0), (y1 - y0)) > np.hypot((x1 - x2), (y1 - y2)): x2 = x1 + (x1 - x0) y2 = y1 + (y1 - y0) else: x0 = x1 + (x1 - x2) y0 = y1 + (y1 - y2) # Limit spines to size of image...I am sure there is a cleaner way to do this if y2 > self.pixmap_fit.height(): y2 = self.pixmap_fit.height() x2 = -m * (y2 - y1) + x1 elif y2 < 0: y2 = 0 x2 = -m * (y2 - y1) + x1 if x0 > self.pixmap_fit.width(): x0 = self.pixmap_fit.width() y0 = -(1 / m) * (x0 - x1) + y1 elif x0 < 0: x0 = 0 y0 = -(1 / m) * (x0 - x1) + y1 # Limit spines to size of image...I am sure there is a cleaner way to do this if y0 > self.pixmap_fit.height(): y0 = self.pixmap_fit.height() x0 = -m * (y0 - y1) + x1 elif y0 < 0: y0 = 0 x0 = -m * (y0 - y1) + x1 if x2 > self.pixmap_fit.width(): x2 = self.pixmap_fit.width() y2 = -(1 / m) * (x2 - x1) + y1 elif x2 < 0: x2 = 0 y2 = -(1 / m) * (x2 - x1) + y1 for l, (x, y) in enumerate(zip([x0, x2], [y0, y2])): start = QtCore.QPointF(x1, y1) end = QtCore.QPointF(x, y) self.scene.interpLine = QGraphicsLineItem( QtCore.QLineF(start, end)) self.d["{}".format(2 * k + l)] = self.scene.interpLine self.scene.addItem(self.scene.interpLine) if k == 0 and l == 0: self.scene.interpLine.setPen( QtGui.QPen(QtGui.QColor('yellow'))) def mousePressEvent(self, event): #http://pyqt.sourceforge.net/Docs/PyQt4/qgraphicsscenemouseevent.html data = self.mapToScene(event.pos()) #https://stackoverflow.com/questions/21197658/how-to-get-pixel-on-qgraphicspixmapitem-on-a-qgraphicsview-from-a-mouse-click #draw piecewise lines if self.scene.testline and ( self.measuring_length or self.measuring_angle) and self._thispos: start = self._thispos end = QtCore.QPointF(data) if self._lastpos and self.measuring_angle: start = self._lastpos self.measuring_angle = False a = np.array([data.x() - start.x(), data.y() - start.y()]) b = np.array([ self._thispos.x() - start.x(), self._thispos.y() - start.y() ]) t = np.arccos( np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) t *= 180 / np.pi #convert to degrees self.T.update(t) self.parent().statusBar.showMessage( 'Angle measurement complete') self.scene.realline = QGraphicsLineItem( QtCore.QLineF(start, end)) #self.scene.realline.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.scene.addItem(self.scene.realline) #Collect piecewise line start/end points if self.measuring_angle: self._lastpos = self._thispos # save old position value self._thispos = QtCore.QPointF(data) # update current position if self.measuring_length: self._lastpos = self._thispos # save old position value self._thispos = QtCore.QPointF(data) # update current position self.A.update(data.x(), data.y()) # update total length self.line_count += 1 #https://stackoverflow.com/questions/30898846/qgraphicsview-items-not-being-placed-where-they-should-be if self.measuring_widths: #measure widths, snap to spines #y2 = m*(x2-x0)+y0 #y2 = mp*(x2-x1)+y1 #solves this system of equations in Ax=b form k = int(self.k / 2) + 1 #same origin for spine on either side x0, y0 = self.xp[k], self.yp[k] x1, y1 = data.x(), data.y() m = self.m[k] #tangent to whale length curve mp = -1 / self.m[k] #perpindicular to whale length curve A = np.matrix([[mp, -1], [m, -1]]) b = np.array([-y0 + mp * x0, -y1 + m * x1]) x = np.linalg.solve(A, b) p = QtCore.QPointF(x[0], x[1]) self.W.update(data.x(), data.y()) s = 10 #dot size self.scene.ellipseItem = QGraphicsEllipseItem(0, 0, s, s) self.scene.ellipseItem.setPos(p.x() - s / 2, p.y() - s / 2) self.scene.ellipseItem.setBrush( QtGui.QBrush(QtCore.Qt.red, style=QtCore.Qt.SolidPattern)) self.scene.ellipseItem.setFlag( QGraphicsItem.ItemIgnoresTransformations, False) #size stays small, but doesnt translate if false self.scene.addItem(self.scene.ellipseItem) self.k += 1 if self.k < self.nspines: self.d[str(self.k)].setPen( QtGui.QPen( QtGui.QColor('yellow'))) #Highlight next spine if self.k == self.nspines: self.parent().statusBar.showMessage( 'Width measurements complete') self.measuring_widths = False width = np.sqrt((self.W.x[1::2] - self.W.x[0::2])**2 + (self.W.y[1::2] - self.W.y[0::2])** 2) #calculate widths self.measurements[ -1, 1:] = width #update most recent row w/ length measurement #MouseWheel Zoom def wheelEvent(self, event): #https://stackoverflow.com/questions/35508711/how-to-enable-pan-and-zoom-in-a-qgraphicsview #transform coordinates correctly #https://stackoverflow.com/questions/20942586/controlling-the-pan-to-anchor-a-point-when-zooming-into-an-image #https://stackoverflow.com/questions/41226194/pyqt4-pixel-information-on-rotated-image zoomInFactor = 1.05 zoomOutFactor = 1 / zoomInFactor self.setTransformationAnchor(QGraphicsView.NoAnchor) self.setResizeAnchor(QGraphicsView.NoAnchor) oldPos = self.mapToScene(event.pos()) #Zoom #y component for mouse with two wheels #https://quick-geek.github.io/answers/885796/index.html if event.angleDelta().y() > 0: zoomFactor = zoomInFactor else: zoomFactor = zoomOutFactor self.scale(zoomFactor, zoomFactor) newPos = self.mapToScene(event.pos()) #Get the new position delta = newPos - oldPos self.translate(delta.x(), delta.y()) #Move scene to old position