def __init__(self): super().__init__() # settings self.setBackground("#fff") self.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) self.setAntialiasing(True) # create custom view box view_box = ViewBox() view_box.setMouseEnabled(False, False) view_box.setLimits(xMin=0, yMin=0, minXRange=10, minYRange=100) view_box.setRange(xRange=(0, 400), yRange=(0, 5000)) view_box.enableAutoRange() # create natural axis items self.x_axis = NaturalAxis("bottom") self.x_axis.setLabel(QApplication.translate("NaturalPlotView", "Fence length"), "m") self.y_axis = NaturalAxis("left") self.y_axis.setLabel(QApplication.translate("NaturalPlotView", "Number of plants")) # create fence information text self.fenceItem = TextItem(border=pyqtgraph.mkPen(width=2, color="#555"), fill=pyqtgraph.mkBrush((255, 255, 255, 200))) # create tube information text self.tubeItem = TextItem(border=pyqtgraph.mkPen(width=2, color="#555"), fill=pyqtgraph.mkBrush((255, 255, 255, 200)), anchor=(1,1)) # create plot item with custom view box and natural axis items self.plotItem = PlotItem(viewBox=view_box, axisItems={"bottom" : self.x_axis, "left" : self.y_axis}, enableMenu=False) self.plotItem.setContentsMargins(5, 5, 12, 5) self.plotItem.hideButtons() self.plotItem.hide() self.setCentralWidget(self.plotItem) # connect actions view_box.sigResized.connect(self.updateTubeLegendPosition) # translate the plot item self.retranslateUi()
class HistogramLUTItem_overlay(GraphicsWidget): """ This is a graphicsWidget which provides controls for adjusting the display of an image. Includes: - Image histogram - Movable region over histogram to select black/white levels - Gradient editor to define color lookup table for single-channel images Parameters ---------- image : ImageItem or None If *image* is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. fillHistogram : bool By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. rgbHistogram : bool Sets whether the histogram is computed once over all channels of the image, or once per channel. levelMode : 'mono' or 'rgba' If 'mono', then only a single set of black/whilte level lines is drawn, and the levels apply to all channels in the image. If 'rgba', then one set of levels is drawn for each channel. """ sigLookupTableChanged = QtCore.Signal(object) sigLevelsChanged = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object) def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'): GraphicsWidget.__init__(self) self.overlay = False self.lut = None self.imageItem = lambda: None # fake a dead weakref self.levelMode = levelMode self.rgbHistogram = rgbHistogram self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) self.layout.setContentsMargins(1,1,1,1) self.layout.setSpacing(0) self.vb = ViewBox(parent=self) self.vb.setMaximumWidth(152) self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) self.gradient = GradientEditorItem() self.gradient.setOrientation('right') self.gradient.loadPreset('grey') self.regions = [ LinearRegionItem([0, 1], 'horizontal', swapMode='block'), LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r', brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)), LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g', brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)), LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b', brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)), LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w', brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))] for region in self.regions: region.setZValue(1000) self.vb.addItem(region) region.lines[0].addMarker('<|', 0.5) region.lines[1].addMarker('|>', 0.5) region.sigRegionChanged.connect(self.regionChanging) region.sigRegionChangeFinished.connect(self.regionChanged) self.region = self.regions[0] # for backward compatibility. self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) self.range = None self.gradient.setFlag(self.gradient.ItemStacksBehindParent) self.vb.setFlag(self.gradient.ItemStacksBehindParent) self.gradient.sigGradientChanged.connect(self.gradientChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged) add = QtGui.QPainter.CompositionMode_Plus self.plots = [ PlotCurveItem(pen=(200, 200, 200, 100)), # mono PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a ] self.plot = self.plots[0] # for backward compatibility. for plot in self.plots: plot.rotate(90) self.vb.addItem(plot) self.fillHistogram(fillHistogram) self._showRegions() self.vb.addItem(self.plot) self.autoHistogramRange() if image is not None: self.setImageItem(image) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] for i,plot in enumerate(self.plots): if fill: plot.setFillLevel(level) plot.setBrush(colors[i]) else: plot.setFillLevel(None) def paint(self, p, *args): if self.levelMode != 'mono': return pen = self.region.lines[0].pen rgn = self.getLevels() p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]: p.setPen(pen) p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft()) p.drawLine(p2 - Point(0, 5), gradRect.topLeft()) p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) def setHistogramRange(self, mn, mx, padding=0.1): """Set the Y range on the histogram plot. This disables auto-scaling.""" self.vb.enableAutoRange(self.vb.YAxis, False) self.vb.setYRange(mn, mx, padding) def autoHistogramRange(self): """Enable auto-scaling on the histogram plot.""" self.vb.enableAutoRange(self.vb.XYAxes) def setImageItem(self, img): """Set an ImageItem to have its levels and LUT automatically controlled by this HistogramLUTItem. """ self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result self.regionChanged() self.imageChanged(autoLevel=True) def viewRangeChanged(self): self.update() def gradientChanged(self): if self.imageItem() is not None: if self.gradient.isLookupTrivial(): self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8)) else: self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None self.sigLookupTableChanged.emit(self) def getLookupTable(self, img=None, n=None, alpha=None): """Return a lookup table from the color gradient defined by this HistogramLUTItem. """ if self.levelMode is not 'mono': return None if n is None: if img.dtype == np.uint8: n = 256 else: n = 512 if self.lut is None: self.lut = self.gradient.getLookupTable(n, alpha=alpha) return self.lut def regionChanged(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelChangeFinished.emit(self) def regionChanging(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): if self.imageItem() is None: return if self.levelMode == 'mono': for plt in self.plots[1:]: plt.setVisible(False) self.plots[0].setVisible(True) # plot one histogram for all image data profiler = debug.Profiler() h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return self.plot.setData(*h) profiler('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) profiler('set region') else: mn, mx = self.imageItem().levels self.region.setRegion([mn, mx]) else: # plot one histogram for each channel self.plots[0].setVisible(False) ch = self.imageItem().getHistogram(perChannel=True) if ch[0] is None: return for i in range(1, 5): if len(ch) >= i: h = ch[i-1] self.plots[i].setVisible(True) self.plots[i].setData(*h) if autoLevel: mn = h[0][0] mx = h[0][-1] self.region[i].setRegion([mn, mx]) else: # hide channels not present in image data self.plots[i].setVisible(False) # make sure we are displaying the correct number of channels self._showRegions() def getLevels(self): """Return the min and max levels. For rgba mode, this returns a list of the levels for each channel. """ if self.levelMode == 'mono': return self.region.getRegion() else: nch = self.imageItem().channels() if nch is None: nch = 3 return [r.getRegion() for r in self.regions[1:nch+1]] def setLevels(self, min=None, max=None, rgba=None): """Set the min/max (bright and dark) levels. Arguments may be *min* and *max* for single-channel data, or *rgba* = [(rmin, rmax), ...] for multi-channel data. """ if self.levelMode == 'mono': if min is None: min, max = rgba[0] assert None not in (min, max) self.region.setRegion((min, max)) else: if rgba is None: raise TypeError("Must specify rgba argument when levelMode != 'mono'.") for i, levels in enumerate(rgba): self.regions[i+1].setRegion(levels) def setLevelMode(self, mode): """ Set the method of controlling the image levels offered to the user. Options are 'mono' or 'rgba'. """ assert mode in ('mono', 'rgba') if mode == self.levelMode: return oldLevels = self.getLevels() self.levelMode = mode self._showRegions() # do our best to preserve old levels if mode == 'mono': levels = np.array(oldLevels).mean(axis=0) self.setLevels(*levels) else: levels = [oldLevels] * 4 self.setLevels(rgba=levels) # force this because calling self.setLevels might not set the imageItem # levels if there was no change to the region item self.imageItem().setLevels(self.getLevels()) self.imageChanged() self.update() def _showRegions(self): for i in range(len(self.regions)): self.regions[i].setVisible(False) if self.levelMode == 'rgba': imax = 4 if self.imageItem() is not None: # Only show rgb channels if connected image lacks alpha. nch = self.imageItem().channels() if nch is None: nch = 3 xdif = 1.0 / nch for i in range(1, nch+1): self.regions[i].setVisible(True) self.regions[i].setSpan((i-1) * xdif, i * xdif) self.gradient.hide() elif self.levelMode == 'mono': self.regions[0].setVisible(True) self.gradient.show() else: raise ValueError("Unknown level mode %r" % self.levelMode) def saveState(self): return { 'gradient': self.gradient.saveState(), 'levels': self.getLevels(), 'mode': self.levelMode, } def restoreState(self, state): self.setLevelMode(state['mode']) self.gradient.restoreState(state['gradient']) self.setLevels(*state['levels']) def setOverlay(self,state=False): self.overlay = state
class HistogramItem(GraphicsWidget): """ This is a graphicsWidget which provides controls for adjusting the display of an image. Includes: - Image histogram - Movable region over histogram to select black/white levels Parameters ---------- image : ImageItem or None If *image* is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. fillHistogram : bool By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. """ sigLevelsChanged = pyqtSignal(object) sigLevelChangeFinished = pyqtSignal(object) def __init__(self, image=None, fillHistogram=True, bounds: tuple = None): GraphicsWidget.__init__(self) self.imageItem = lambda: None # fake a dead weakref self.layout = QGraphicsGridLayout() self.setLayout(self.layout) self.layout.setContentsMargins(1, 1, 1, 1) self.layout.setSpacing(0) self.vb = ViewBox(parent=self) # self.vb.setMaximumHeight(152) # self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=True, y=False) self.region = LinearRegionItem([0, 1], 'vertical', swapMode='block', bounds=bounds) self.region.setZValue(1000) self.vb.addItem(self.region) self.region.lines[0].addMarker('<|', 0.5) self.region.lines[1].addMarker('|>', 0.5) self.region.sigRegionChanged.connect(self.regionChanging) self.region.sigRegionChangeFinished.connect(self.regionChanged) self.axis = AxisItem('bottom', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 1, 0) self.layout.addItem(self.vb, 0, 0) self.range = None self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.plot = PlotCurveItem(pen=(200, 200, 200, 100)) # self.plot.rotate(90) self.vb.addItem(self.plot) self.fillHistogram(fillHistogram) self._showRegions() self.autoHistogramRange() if image is not None: self.setImageItem(image) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): if fill: self.plot.setFillLevel(level) self.plot.setBrush(color) else: self.plot.setFillLevel(None) def paint(self, p, *args): rgn = self.getLevels() self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) def setHistogramRange(self, mn, mx, padding=0.1): """Set the Y range on the histogram plot. This disables auto-scaling.""" self.vb.enableAutoRange(self.vb.XAxis, False) self.vb.setYRange(mn, mx, padding) def autoHistogramRange(self): """Enable auto-scaling on the histogram plot.""" self.vb.enableAutoRange(self.vb.XYAxes) def setImageItem(self, img): """Set an ImageItem to have its levels and LUT automatically controlled by this HistogramLUTItem. """ self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) self.regionChanged() self.imageChanged(autoLevel=True) def viewRangeChanged(self): self.update() def regionChanged(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelChangeFinished.emit(self) def regionChanging(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False): if self.imageItem() is None: return self.plot.setVisible(True) # plot one histogram for all image data h = self.imageItem().getHistogram() if h[0] is None: return self.plot.setData(*h) if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) else: mn, mx = self.imageItem().levels self.region.setRegion([mn, mx]) def getLevels(self): """ Return the min and max levels. """ return self.region.getRegion() def setLevels(self, min=None, max=None): """ Set the min/max (bright and dark) levels. """ assert None not in (min, max) self.region.setRegion((min, max)) def _showRegions(self): self.region.setVisible(True) def saveState(self): return { 'levels': self.getLevels(), } def restoreState(self, state): self.setLevels(*state['levels'])
class DataWidget(QWidget): def __init__(self, parent, shared_data): super(DataWidget, self).__init__(parent) self.__shared_data = shared_data self.__shared_data.update_sync.emit() # Add the file selection controls self.__dir_picker_button = QPushButton() self.__dir_picker_button.setEnabled(True) self.__dir_picker_button.setText("Load data") self.__dir_picker_button.setIcon(self.style().standardIcon(QStyle.SP_DirIcon)) self.__dir_picker_button.setToolTip('Select the directory using the file explorer') self.__dir_picker_button.clicked.connect(self.__open_dir_picker) # Add the sync controls self.__sync_time_label = QLabel() self.__sync_time_label.setText('Enter the timecode (HH:mm:ss:zzz) : ') self.__sync_time_edit = QTimeEdit() self.__sync_time_edit.setDisplayFormat('HH:mm:ss:zzz') self.__sync_time_edit.setEnabled(False) self.__sync_time_button = QPushButton() self.__sync_time_button.setText('Sync data') self.__sync_time_button.setEnabled(False) self.__sync_time_button.clicked.connect(self.__sync_data) # Create the layout for the file controls dir_layout = QHBoxLayout() dir_layout.setContentsMargins(0, 0, 0, 0) dir_layout.addWidget(self.__dir_picker_button) dir_layout.addStretch(1) dir_layout.addWidget(self.__sync_time_label) dir_layout.addWidget(self.__sync_time_edit) dir_layout.addWidget(self.__sync_time_button) # Create the axis and their viewbox self.__x_axis_item = AxisItem('left') self.__y_axis_item = AxisItem('left') self.__z_axis_item = AxisItem('left') self.__x_axis_viewbox = ViewBox() self.__y_axis_viewbox = ViewBox() self.__z_axis_viewbox = ViewBox() # Create the widget which will display the data self.__graphic_view = GraphicsView(background="#ecf0f1") self.__graphic_layout = GraphicsLayout() self.__graphic_view.setCentralWidget(self.__graphic_layout) # Add the axis to the widget self.__graphic_layout.addItem(self.__x_axis_item, row=2, col=3, rowspan=1, colspan=1) self.__graphic_layout.addItem(self.__y_axis_item, row=2, col=2, rowspan=1, colspan=1) self.__graphic_layout.addItem(self.__z_axis_item, row=2, col=1, rowspan=1, colspan=1) self.__plot_item = PlotItem() self.__plot_item_viewbox = self.__plot_item.vb self.__graphic_layout.addItem(self.__plot_item, row=2, col=4, rowspan=1, colspan=1) self.__graphic_layout.scene().addItem(self.__x_axis_viewbox) self.__graphic_layout.scene().addItem(self.__y_axis_viewbox) self.__graphic_layout.scene().addItem(self.__z_axis_viewbox) self.__x_axis_item.linkToView(self.__x_axis_viewbox) self.__y_axis_item.linkToView(self.__y_axis_viewbox) self.__z_axis_item.linkToView(self.__z_axis_viewbox) self.__x_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__y_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__z_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__plot_item_viewbox.sigResized.connect(self.__update_views) self.__x_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) self.__y_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) self.__z_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) # Create the final layout self.__v_box = QVBoxLayout() self.__v_box.addLayout(dir_layout) self.__v_box.addWidget(self.__graphic_view) self.setLayout(self.__v_box) self.__restore_state() def __open_dir_picker(self): self.__shared_data.data_file_path = QFileDialog.getOpenFileUrl(self, 'Open the Hexoskin data directory', QDir.homePath())[0] if self.__shared_data.data_file_path is not None: try: self.__load_data() self.__add_selector_acc_gyr() self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') except FileNotFoundError: pass except UnicodeDecodeError: pass def __load_data(self): if self.__shared_data.data_file_path is not None: self.__shared_data.import_parameter() def __show_data(self, field1, field2, field3): if self.__shared_data.parameter is not None: # Generate the timecodes if needed if len(self.__shared_data.parameter['TIMECODE']) == 0: if self.__shared_data.sampling_rate is None: result = False while not result and result == 0: result = self.__show_sampling_rate_picker() self.__shared_data.add_timecode() self.__x_axis_viewbox.clear() self.__y_axis_viewbox.clear() self.__z_axis_viewbox.clear() # Show the 3 selected fields self.__x_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field1))), pen='#34495e')) self.__y_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field2))), pen='#9b59b6')) self.__z_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field3))), pen='#3498db')) self.__x_axis_item.setLabel(field1, color="#34495e") self.__y_axis_item.setLabel(field2, color="#9b59b6") self.__z_axis_item.setLabel(field3, color="#3498db") # Add the middle line and the bottom timecodes timecodes = self.__shared_data.parameter['TIMECODE'] middle = [0] * len(timecodes) self.__plot_item_viewbox.addItem(PlotCurveItem(middle, pen='#000000')) self.__plot_item.getAxis('bottom').setTicks( self.__generate_time_ticks(timecodes, self.__shared_data.sampling_rate)) # Enable the controls self.__sync_time_edit.setEnabled(True) self.__sync_time_button.setEnabled(True) self.__dir_picker_button.setEnabled(False) self.__update_views() def __update_views(self): self.__x_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) self.__y_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) self.__z_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) def __generate_time_ticks(self, timecodes, rate): ticks = list() steps = [rate * 30, rate * 15, rate * 5, rate] for step in steps: temp = list() i = step while i in range(len(timecodes)): temp.append((i, timecodes[i].strftime('%H:%M:%S:') + str(int(timecodes[i].microsecond / 1000)))) i += step ticks.append(temp) return ticks def __sync_data(self): self.__shared_data.data_sync = self.__sync_time_edit.text() self.__shared_data.update_sync.emit() def __show_sampling_rate_picker(self) -> bool: self.__shared_data.sampling_rate, result = QInputDialog.getInt(self, 'Set sampling rate value', 'Sampling rate') return result def __add_selector_acc_gyr(self): if 'GYR_X' in self.__shared_data.parameter.keys() or 'GYR_Y' in self.__shared_data.parameter.keys() \ or 'GYR_Z' in self.__shared_data.parameter.keys(): show_acc = QPushButton() show_acc.setText('Show Accelerometer Axis') show_acc.clicked.connect(self.__show_acc) show_gyr = QPushButton() show_gyr.setText('Show Gyroscope Axis') show_gyr.clicked.connect(self.__show_gyr) layout = QHBoxLayout() layout.addWidget(show_acc) layout.addWidget(show_gyr) layout.addStretch(1) self.__v_box.addLayout(layout) def __show_acc(self): self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') def __show_gyr(self): self.__show_data('GYR_X', 'GYR_Y', 'GYR_Z') def __restore_state(self): if self.__shared_data.parameter is not None: self.__add_selector_acc_gyr() self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') print('trigger reimport') if self.__shared_data.data_sync is not None: text_time = self.__shared_data.data_sync.split(':') time = QTime() time.setHMS(int(text_time[0]), int(text_time[1]), int(text_time[2]), int(text_time[3])) self.__sync_time_edit.setTime(time)