Exemple #1
0
class Bp2DWidget(PlotWidget):

    def __init__(self):
        super(Bp2DWidget, self).__init__()
        # M.B. plot add to params
        self.setXRange(-60, 60)
        self.setYRange(-60, 60)
        self.img = ImageItem()
        self.addItem(self.img)
        _translate = QCoreApplication.translate
        self.setLabels(title=_translate("Bp2DWidget", "Beam pattern"),
                       left=_translate("Bp2DWidget", "Elevation, °"),
                       bottom=_translate("Bp2DWidget", "Azimuth, °"))
        self.setLogMode()
        colormap = ColorMap(*zip(*Gradients["bipolar"]["ticks"]))
        self.img.setLookupTable(colormap.getLookupTable())
        #gradient_legend = GradientLegend(10, 10)
        #self.addItem(gradient_legend)

    @pyqtSlot()
    def on_data_changed(self):
        sender = self.sender()
        self.img.setImage(np.rot90(sender.data, -1))
        self.img.setRect(self.__ensure_rect(np.shape(sender.data)))

    def __ensure_rect(self, shape):
        x_offset = 120. / (shape[0]-1) / 2
        y_offset = 120. / (shape[1]-1) / 2
        return QRectF(-60 - x_offset, 60 + y_offset, 120 + x_offset * 2, -120 - y_offset * 2)
class ColorMapPlotWidget(InteractivePlotWidget):
    """An InteractivePlotWidget optimised for plotting color(heat) maps.
    Uses the Matplotlib colormap given by *cmap* to color the map.

    Attributes
    ----------
    lookup_table : ndarray
        The lookup table generated from *cmap* to colour the image with
    num_contours : int
        The number of different colour levels to plot
    contour_spacing : int
        How closely spaced the colour levels are
    """
    def __init__(self, parent=None, cmap="jet"):
        self.lookup_table = matplotlib_lookup_table(cmap)
        self.num_contours = 5
        self.contour_spacing_dB = 5
        self.parent = parent
        super().__init__(parent=self.parent)

    def plot_colormap(self, x, y, z, num_contours=5, contour_spacing_dB=5):
        """Plot *x*, *y* and *z* on a colourmap, with colour intervals defined
        by *num_contours* at *contour_spacing_dB* intervals."""

        #self.PlotWidget.removeItem(self.z_img)

        self.x = x
        self.y = y
        self.z = z

        self.num_contours = num_contours
        self.contour_spacing_dB = contour_spacing_dB
        self.update_lowest_contour()

        # Set up axes:
        x_axis = self.PlotWidget.getAxis('bottom')
        y_axis = self.PlotWidget.getAxis('left')

        self.x_scale_fact = self.get_scale_fact(x)
        self.y_scale_fact = self.get_scale_fact(y)

        x_axis.setScale(self.x_scale_fact)
        y_axis.setScale(self.y_scale_fact)

        #self.autoRange()

        self.z_img = ImageItem(z.transpose())
        self.z_img.setLookupTable(self.lookup_table)
        self.z_img.setLevels([self.lowest_contour, self.highest_contour])

        self.PlotWidget.addItem(self.z_img)

        self.PlotWidget.autoRange()
        #self.PlotWidget.ViewBox.autoRange()

    def get_scale_fact(self, var):
        return var.max() / var.size

    def update_lowest_contour(self):
        """Find the lowest contour to plot, as determined by the number of
        contours and the contour spacing."""
        self.lowest_contour = self.z.max() - (self.num_contours *
                                              self.contour_spacing_dB)
        self.highest_contour = self.z.max()
Exemple #3
0
class SliceableGraphicsView(GraphicsView, SlicingView):
    sigToggleHorizontalSlice = Signal(bool)
    sigToggleVerticalSlice = Signal(bool)
    sigToggleDepthSlice = Signal(bool)
    sigMakePrimary = Signal(object, object)
    sigCrosshairMoved = Signal()

    SUPPORTED_NDIM = 2

    def __init__(self, slice_direction, parent=None, xlink=None, ylink=None):
        super(SliceableGraphicsView, self).__init__(parent=parent)

        self.slice_direction = slice_direction

        self.setContentsMargins(0, 0, 0, 0)

        # Add axes
        self.view = SliceableAxes(slice_direction)
        self.view.axes["left"]["item"].setZValue(10)
        self.view.axes["top"]["item"].setZValue(10)
        self.setCentralItem(self.view)

        for sig in [
                'sigToggleVerticalSlice', 'sigToggleHorizontalSlice',
                'sigToggleDepthSlice', 'sigMakePrimary'
        ]:
            if hasattr(self.view, sig):
                getattr(self.view, sig).connect(getattr(self, sig))

        # Add imageitem
        self.image_item = ImageItem(axisOrder='row-major')
        self.image_item.setOpts()
        self.view.addItem(self.image_item)

        # add crosshair
        self.crosshair = BetterCrosshairROI((0, 0),
                                            parent=self.view,
                                            resizable=False)
        self.crosshair.sigMoved.connect(self.sigCrosshairMoved)
        self.view.getViewBox().addItem(self.crosshair)

        # find top-level parent NDImageView
        while not isinstance(parent, NDImageView):
            parent = parent.parent()

        # Initialize lut, levels
        self.image_item.setLevels(parent.levels, update=True)
        self.image_item.setLookupTable(parent.lut, update=True)

        # Link axes
        if ylink:
            self.view.vb.setYLink(ylink)
        if xlink:
            self.view.vb.setXLink(xlink)

    def setData(self, data):
        # Constrain squareness when units match
        is_square = data.dims[-2].split('(')[-1] == data.dims[-1].split(
            '(')[-1]
        self.view.vb.setAspectLocked(is_square)

        xvals = data.coords[data.dims[-1]]
        yvals = data.coords[data.dims[-2]]
        xmin = float(xvals.min())
        xmax = float(xvals.max())
        ymin = float(yvals.min())
        ymax = float(yvals.max())

        # Position the image according to coords
        shape = data.shape
        a = [(0, shape[-2]), (shape[-1], shape[-2]), (shape[-1], 0), (0, 0)]

        # b = [(ymin, xmax), (ymax, xmax), (ymax, xmin), (ymin, xmin)]
        if self.slice_direction in ['horizontal', 'depth']:
            b = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
        elif self.slice_direction == 'vertical':
            b = [(xmax, ymax), (xmin, ymax), (xmin, ymin), (xmax, ymin)]

        quad1 = QPolygonF()
        quad2 = QPolygonF()
        for p, q in zip(a, b):
            quad1.append(QPointF(*p))
            quad2.append(QPointF(*q))

        transform = QTransform()
        QTransform.quadToQuad(quad1, quad2, transform)

        # Bind coords from the xarray to the timeline axis
        # super(SliceableGraphicsView, self).setImage(img, autoRange, autoLevels, levels, axes, np.asarray(img.coords[img.dims[0]]), pos, scale, transform, autoHistogramRange, levelMode)
        self.image_item.setImage(np.asarray(data), autoLevels=False)
        self.image_item.setTransform(transform)

        # Label the image axes
        self.view.setLabel('left', data.dims[-2])
        self.view.setLabel('bottom', data.dims[-1])

    def resetCrosshair(self):
        transform = self.image_item.viewTransform()
        new_pos = transform.map(self.image_item.boundingRect().center())
        self.crosshair.setPos(new_pos)
        # self.crosshair.sigMoved.emit(new_pos)

    def updateImage(self, autoHistogramRange=True):
        ## Redraw image on screen
        if self.image is None:
            return

        image = self.getProcessedImage()

        if autoHistogramRange:
            self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)

        # Transpose image into order expected by ImageItem
        if self.imageItem.axisOrder == 'col-major':
            axorder = ['t', 'x', 'y', 'c']
        else:
            axorder = ['t', 'y', 'x', 'c']
        axorder = [
            self.axes[ax] for ax in axorder if self.axes[ax] is not None
        ]
        ax_swap = [image.dims[ax_index] for ax_index in axorder]
        image = image.transpose(*ax_swap)

        # Select time index
        if self.axes['t'] is not None:
            self.ui.roiPlot.show()
            image = image[self.currentIndex]

        self.imageItem.updateImage(np.asarray(image))

    def updateCrosshair(self, x, y):
        self.crosshair.setPos(x, y)

    def quickMinMax(self, data):
        """
        Estimate the min/max values of *data* by subsampling. MODIFIED TO USE THE 99TH PERCENTILE instead of max.
        """
        if data is None:
            return 0, 0

        sl = slice(None, None, max(1, int(data.size // 1e6)))
        data = np.asarray(data[sl])

        levels = (np.nanmin(data),
                  np.nanpercentile(
                      np.where(data < np.nanmax(data), data, np.nanmin(data)),
                      99))

        return [levels]
Exemple #4
0
class ESR_plot(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.initGUI()

    def initGUI(self):
        self.plot = PlotWidget()
        self.sensorplot = PlotWidget()

        self.schemeplot = PlotWidget()
        self.scheme_plot = UTILS_QT.myplot(self.schemeplot,xlabel = ['time', 's'], ylabel =['',''],logmode=False)
        self.psp = UTILS_QT.pulses_scheme_plot(self.scheme_plot)


        date_axis = TimeAxisItem(orientation='bottom')
        #date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation = 'bottom')
        self.sensorplot = PlotWidget(axisItems = {'bottom': date_axis})


        win = GraphicsLayoutWidget()
        win2 = PlotWidget()



        self.view = win.addViewBox(border = 'w', invertY = True)
        self.view.setAspectLocked(True)
        self.img = ImageItem()
        self.plotaxes = win2.getPlotItem()
        #self.view.addItem(self.img)
        #self.view.addItem(self.plotaxes)
        #self.view.


        data = np.random.normal(size=(1, 600, 600), loc=1024, scale=64).astype(np.uint16)
        self.img.setImage(data[0])
        self.plotaxes.getViewBox().addItem(self.img)


        # colormap
        pos = np.array([0., 1., 0.5, 0.25, 0.75])
        #pos2 = np.array([1.0,0.75,0.5,0.25,0.])
        pos2 = np.array([0.,0.25,0.5,0.75,1.0])
        color2 = np.array([[255,242,15,255], [245,124,15,255],[170,69,16,255],[91,50,0,255],[0,0,0,255]],dtype=np.ubyte)
        color = np.array([[0,255,255,255], [255,255,0,255], [0,0,0,255], (0, 0, 255, 255), (255, 0, 0, 255)], dtype=np.ubyte)
        cmap = pg.ColorMap(pos2, color2)
        lut = cmap.getLookupTable(0.0, 1.0, 256)
        self.img.setLookupTable(lut)
        #self.img.setLevels([-50,1])

        self.tw = QtGui.QTabWidget()
        self.tw.addTab(win2,'ESR data')
        self.tw.addTab(self.sensorplot,'B field')
        self.tw.addTab(self.schemeplot,'Scheme')

        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.plot)
        #layout.addWidget(win2)
        layout.addWidget(self.tw)

        self.setLayout(layout)

        self.p1 = self.plot.getPlotItem()
        self.p2 = self.plot.getPlotItem()

        self.ps = self.sensorplot.getPlotItem()
        #self.p1.addLegend()
        self.p1data = self.p1.plot([0],pen = 'r')
        self.p2data = self.p1.plot([0],pen = 'g')
        self.psdata = self.ps.plot([],pen = 'w')
        self.ps.setLabel('left','Magnetic field', 'uT')

        self.vLine5 = pg.InfiniteLine(angle=90, movable=True)
        self.vLine6 = pg.InfiniteLine(angle=90, movable=True)
        self.plotaxes.addItem(self.vLine5, ignoreBounds=True)
        self.plotaxes.addItem(self.vLine6, ignoreBounds=True)
Exemple #5
0
class FilterPreviews(GraphicsLayoutWidget):
    image_before: ImageItem
    image_after: ImageItem
    image_diff: ImageItem
    histogram_before: Optional[PlotItem]
    histogram_after: Optional[PlotItem]
    histogram: Optional[PlotItem]

    def __init__(self, parent=None, **kwargs):
        super(FilterPreviews, self).__init__(parent, **kwargs)

        widget_location = self.mapToGlobal(QPoint(self.width() / 2, 0))
        # allow the widget to take up to 80% of the desktop's height
        self.ALLOWED_HEIGHT: QRect = QGuiApplication.screenAt(widget_location).availableGeometry().height() * 0.8

        self.before_histogram_data = None
        self.after_histogram_data = None
        self.histogram = None
        self.before_histogram = None
        self.after_histogram = None
        self.combined_histograms = True
        self.histogram_legend_visible = True

        self.addLabel("Image before")
        self.addLabel("Image after")
        self.addLabel("Image difference")
        self.nextRow()

        self.image_before, self.image_before_vb, self.image_before_hist = self.image_in_vb(name="before")
        self.image_after, self.image_after_vb, self.image_after_hist = self.image_in_vb(name="after")
        self.image_difference, self.image_difference_vb, self.image_difference_hist = self.image_in_vb(
            name="difference")

        self.image_after_overlay = ImageItem()
        self.image_after_overlay.setZValue(10)
        self.image_after_vb.addItem(self.image_after_overlay)

        # Ensure images resize equally
        self.image_layout: GraphicsLayout = self.addLayout(colspan=6)
        self.image_layout.addItem(self.image_before_vb, 0, 0)
        self.image_layout.addItem(self.image_before_hist, 0, 1)
        self.image_layout.addItem(self.image_after_vb, 0, 2)
        self.image_layout.addItem(self.image_after_hist, 0, 3)
        self.image_layout.addItem(self.image_difference_vb, 0, 4)
        self.image_layout.addItem(self.image_difference_hist, 0, 5)
        self.nextRow()

        before_details = self.addLabel("")
        after_details = self.addLabel("")
        difference_details = self.addLabel("")

        self.display_formatted_detail = {
            self.image_before: lambda val: before_details.setText(f"Before: {val:.6f}"),
            self.image_after: lambda val: after_details.setText(f"After: {val:.6f}"),
            self.image_difference: lambda val: difference_details.setText(f"Difference: {val:.6f}"),
        }

        for img in self.image_before, self.image_after, self.image_difference:
            img.hoverEvent = lambda ev: self.mouse_over(ev)

        self.init_histogram()

    def resizeEvent(self, ev: QResizeEvent):
        if ev is not None:
            size = ev.size()
            self.image_layout.setFixedHeight(min(size.height() * 0.7, self.ALLOWED_HEIGHT))
        super().resizeEvent(ev)

    def image_in_vb(self, name=None):
        im = ImageItem()
        vb = ViewBox(invertY=True, lockAspect=True, name=name)
        vb.addItem(im)
        hist = HistogramLUTItem(im)
        return im, vb, hist

    def clear_items(self):
        self.image_before.clear()
        self.image_after.clear()
        self.image_difference.clear()
        self.image_after_overlay.clear()

    def init_histogram(self):
        self.histogram = self.addPlot(row=histogram_coords["combined"].row,
                                      col=histogram_coords["combined"].col,
                                      labels=histogram_axes_labels,
                                      lockAspect=True,
                                      colspan=3)
        self.addLabel("Pixel values", row=label_coords["combined"].row, col=label_coords["combined"].col)

        self.legend = self.histogram.addLegend()

    def update_histogram_data(self):
        # Plot any histogram that has data, and add a legend if both exist
        before_data = self.image_before.getHistogram()
        after_data = self.image_after.getHistogram()
        if _data_valid_for_histogram(before_data):
            if self.combined_histograms:
                before_plot = self.histogram.plot(*before_data, pen=before_pen, clear=True)
                self.legend.addItem(before_plot, "Before")
            else:
                self.before_histogram.plot(*before_data, pen=before_pen, clear=True)
        if _data_valid_for_histogram(after_data):
            if self.combined_histograms:
                after_plot = self.histogram.plot(*after_data, pen=after_pen)
                self.legend.addItem(after_plot, "After")
            else:
                self.after_histogram.plot(*after_data, pen=after_pen, clear=True)

    def init_separate_histograms(self):
        hc = histogram_coords
        self.before_histogram = self.addPlot(row=hc["before"].row,
                                             col=hc["before"].col,
                                             labels=histogram_axes_labels,
                                             lockAspect=True)
        self.after_histogram = self.addPlot(row=hc["after"].row,
                                            col=hc["after"].col,
                                            labels=histogram_axes_labels,
                                            lockAspect=True)
        lc = label_coords
        self.addLabel("Pixel values before", row=lc["before"].row, col=lc["before"].col)
        self.addLabel("Pixel values after", row=lc["after"].row, col=lc["after"].col)
        if _data_valid_for_histogram(self.before_histogram_data):
            self.before_histogram.plot(*self.before_histogram_data, pen=before_pen)
        if _data_valid_for_histogram(self.after_histogram_data):
            self.after_histogram.plot(*self.after_histogram_data, pen=after_pen)

    def delete_histograms(self):
        coords = set(c for c in histogram_coords.values())
        histograms = (self.getItem(*coord) for coord in coords)
        for histogram in filter(lambda h: h is not None, histograms):
            self.removeItem(histogram)
        self.histogram = None
        self.before_histogram = None
        self.after_histogram = None

    def delete_histogram_labels(self):
        coords = set(c for c in label_coords.values())
        labels = (self.getItem(*coord) for coord in coords)
        for label in filter(lambda h: h is not None, labels):
            self.removeItem(label)

    @property
    def histogram_legend(self) -> Optional[LegendItem]:
        if self.histogram and self.histogram.legend:
            return self.histogram.legend
        return None

    def mouse_over(self, ev):
        # Ignore events triggered by leaving window or right clicking
        if ev.exit:
            return
        pos = CloseEnoughPoint(ev.pos())
        for img in self.image_before, self.image_after, self.image_difference:
            if img.image is not None and pos.x < img.image.shape[0] and pos.y < img.image.shape[1]:
                pixel_value = img.image[pos.y, pos.x]
                self.display_formatted_detail[img](pixel_value)

    def link_all_views(self):
        for view1, view2 in [[self.image_before_vb, self.image_after_vb],
                             [self.image_after_vb, self.image_difference_vb],
                             [self.image_after_hist.vb, self.image_before_hist.vb]]:
            view1.linkView(ViewBox.XAxis, view2)
            view1.linkView(ViewBox.YAxis, view2)

    def unlink_all_views(self):
        for view in self.image_before_vb, self.image_after_vb, self.image_after_hist.vb:
            view.linkView(ViewBox.XAxis, None)
            view.linkView(ViewBox.YAxis, None)

    def add_difference_overlay(self, diff):
        diff = -diff
        diff[diff > 0.0] = 1.0
        pos = np.array([0, 1])
        color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte)
        map = ColorMap(pos, color)
        self.image_after_overlay.setOpacity(1)
        self.image_after_overlay.setImage(diff)
        lut = map.getLookupTable(0, 1, 2)
        self.image_after_overlay.setLookupTable(lut)

    def hide_difference_overlay(self):
        self.image_after_overlay.setOpacity(0)

    def auto_range(self):
        # This will cause the previews to all show by just causing autorange on self.image_before_vb
        self.image_before_vb.autoRange()
Exemple #6
0
class PsdWaterfallPlotWidget(GraphicsLayoutWidget):

    """This class manages and displays the power spectrum distribution (PSD)
       data in a waterfall plot.

    Attributes
    ----------
    arraySize : int
        The size of the data array to display.
    boundingRect : QtCore.QRectF
        The actual coordinate space base on frequency and time of acquisition.
    data : numpy.ndarray
        The 2D array for the PSD data.
    image : pyqtgraph.ImageItem
        The instance of the image item for display.
    timeScale : float
        The total time for the buffer to accumulate at the ROI FPS.
    """

    def __init__(self, parent=None):
        """Initialize the class.

        Parameters
        ----------
        parent : None, optional
            Top-level widget.
        """
        super().__init__(parent)
        self.plot = self.addPlot()
        self.plot.invertY()

        self.image = ImageItem()
        self.image.setOpts(axisOrder='row-major')
        self.plot.addItem(self.image)

        self.data = None
        self.arraySize = None
        self.boundingRect = None
        self.timeScale = None
        self.colorMap = 'viridis'
        self.image.setLookupTable(getLutFromColorMap(self.colorMap))

    def clearPlot(self):
        """Reset all data and clear the plot.
        """
        self.data = None
        self.boundingRect = None
        self.image.clear()

    def getConfiguration(self):
        """Get the current plot configuration.

        Returns
        -------
        int, str
            The set of current configuration parameters.
        """
        return self.arraySize, self.colorMap

    def setConfiguration(self, config):
        """Set the new parameters into the widget.

        Parameters
        ----------
        config : `config.PsdPlotConfig`
            The new parameters to apply.
        """
        numBins = config.numWaterfallBins
        if self.arraySize != numBins:
            self.arraySize = numBins
            # Invalidate data
            self.data = None
            self.boundingRect = None

        colorMap = config.waterfallColorMap
        if self.colorMap != colorMap:
            self.colorMap = colorMap
            self.image.setLookupTable(getLutFromColorMap(self.colorMap))

    def setTimeScale(self, timeScale):
        """Update the stored timescale and invalidate data and bounding rect.

        Parameters
        ----------
        timeScale : float
            The new timescale.
        """
        self.timeScale = timeScale
        self.data = None
        self.boundingRect = None

    def setup(self, arraySize, timeScale, axisLabel):
        """Setup the widget with the array size.

        Parameters
        ----------
        arraySize : int
            The size fo the data array to display in terms of history.
        timeScale : float
            The total time for the buffer to accumulate at the ROI FPS.
        axisLabel : str
            Label for particular centroid coordinate.
        """
        self.arraySize = arraySize
        self.timeScale = timeScale
        self.plot.setLabel('bottom', '{} {}'.format(axisLabel, HTML_NU), units='Hz')
        self.plot.setLabel('left', 'Time', units='s')

    def updatePlot(self, psd, freqs):
        """Update the current plot with the given data.

        Parameters
        ----------
        psd : numpy.array
            The PSD data of a given centroid coordinate.
        freqs : numpy.array
            The frequency array associated with the PSD data.
        """
        if self.data is None:
            self.data = np.zeros((self.arraySize, psd.size))
        else:
            self.data[1:, ...] = self.data[:-1, ...]
        self.data[0, ...] = np.log(psd)

        self.image.setImage(self.data)
        if self.boundingRect is None:
            self.boundingRect = QtCore.QRectF(0, 0, freqs[-1], self.arraySize * self.timeScale)
            self.image.setRect(self.boundingRect)
Exemple #7
0
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap,
                            ReadingOrder):
    """
    A SpectrogramView with support for Channels and more from PyDM.

    If there is no :attr:`channelWidth` it is possible to define the width of
    the image with the :attr:`width` property.

    The :attr:`normalizeData` property defines if the colors of the images are
    relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
    the minimum and maximum values of the image.

    Use the :attr:`newImageSignal` to hook up to a signal that is emitted when
    a new image is rendered in the widget.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    image_channel : str, optional
        The channel to be used by the widget for the image data.
    xaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Clike), and to set the xaxis values
    yaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Fortranlike), and to set the yaxis values
    background : QColor, optional
        QColor to set the background color of the GraphicsView
    """

    Q_ENUMS(PyDMColorMap)
    Q_ENUMS(ReadingOrder)

    color_maps = cmaps

    def __init__(self,
                 parent=None,
                 image_channel=None,
                 xaxis_channel=None,
                 yaxis_channel=None,
                 roioffsetx_channel=None,
                 roioffsety_channel=None,
                 roiwidth_channel=None,
                 roiheight_channel=None,
                 title='',
                 background='w',
                 image_width=0,
                 image_height=0):
        """Initialize widget."""
        GraphicsLayoutWidget.__init__(self, parent)
        PyDMWidget.__init__(self)
        self.thread = None
        self._imagechannel = None
        self._xaxischannel = None
        self._yaxischannel = None
        self._roioffsetxchannel = None
        self._roioffsetychannel = None
        self._roiwidthchannel = None
        self._roiheightchannel = None
        self._channels = 7 * [
            None,
        ]
        self.image_waveform = np.zeros(0)
        self._image_width = image_width if not xaxis_channel else 0
        self._image_height = image_height if not yaxis_channel else 0
        self._roi_offsetx = 0
        self._roi_offsety = 0
        self._roi_width = 0
        self._roi_height = 0
        self._normalize_data = False
        self._auto_downsample = True
        self._last_yaxis_data = None
        self._last_xaxis_data = None
        self._auto_colorbar_lims = True
        self.format_tooltip = '{0:.4g}, {1:.4g}'

        # ViewBox and imageItem.
        self._view = ViewBox()
        self._image_item = ImageItem()
        self._view.addItem(self._image_item)

        # ROI
        self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
        self.ROIColor = QColor('red')
        pen = mkPen()
        pen.setColor(QColor('transparent'))
        pen.setWidth(1)
        self.ROICurve.setPen(pen)
        self._view.addItem(self.ROICurve)

        # Axis.
        self.xaxis = AxisItem('bottom')
        self.xaxis.setPen(QColor(0, 0, 0))
        if not xaxis_channel:
            self.xaxis.setVisible(False)
        self.yaxis = AxisItem('left')
        self.yaxis.setPen(QColor(0, 0, 0))
        if not yaxis_channel:
            self.yaxis.setVisible(False)

        # Colorbar legend.
        self.colorbar = _GradientLegend()

        # Title.
        start_row = 0
        if title:
            self.title = LabelItem(text=title, color='#000000')
            self.addItem(self.title, 0, 0, 1, 3)
            start_row = 1

        # Set layout.
        self.addItem(self._view, start_row, 1)
        self.addItem(self.yaxis, start_row, 0)
        self.addItem(self.colorbar, start_row, 2)
        self.addItem(self.xaxis, start_row + 1, 1)
        self.setBackground(background)
        self.ci.layout.setColumnSpacing(0, 0)
        self.ci.layout.setRowSpacing(start_row, 0)

        # Set color map limits.
        self.cm_min = 0.0
        self.cm_max = 255.0

        # Set default reading order of numpy array data to Clike.
        self._reading_order = ReadingOrder.Clike

        # Make a right-click menu for changing the color map.
        self.cm_group = QActionGroup(self)
        self.cmap_for_action = {}
        for cm in self.color_maps:
            action = self.cm_group.addAction(cmap_names[cm])
            action.setCheckable(True)
            self.cmap_for_action[action] = cm

        # Set the default colormap.
        self._cm_colors = None
        self.colorMap = PyDMColorMap.Inferno

        # Setup the redraw timer.
        self.needs_redraw = False
        self.redraw_timer = QTimer(self)
        self.redraw_timer.timeout.connect(self.redrawImage)
        self._redraw_rate = 30
        self.maxRedrawRate = self._redraw_rate
        self.newImageSignal = self._image_item.sigImageChanged

        # Set Channels.
        self.imageChannel = image_channel
        self.xAxisChannel = xaxis_channel
        self.yAxisChannel = yaxis_channel
        self.ROIOffsetXChannel = roioffsetx_channel
        self.ROIOffsetYChannel = roioffsety_channel
        self.ROIWidthChannel = roiwidth_channel
        self.ROIHeightChannel = roiheight_channel

    # --- Context menu ---
    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu.

        It will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by
            `assemble_tools_menu`.
        """
        self.menu = ViewBoxMenu(self._view)
        cm_menu = self.menu.addMenu("Color Map")
        for act in self.cmap_for_action.keys():
            cm_menu.addAction(act)
        cm_menu.triggered.connect(self._changeColorMap)
        return self.menu

    # --- Colormap methods ---
    def _changeColorMap(self, action):
        """
        Method invoked by the colormap Action Menu.

        Changes the current colormap used to render the image.

        Parameters
        ----------
        action : QAction
        """
        self.colorMap = self.cmap_for_action[action]

    @Property(float)
    def colorMapMin(self):
        """
        Minimum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_min

    @colorMapMin.setter
    @Slot(float)
    def colorMapMin(self, new_min):
        """
        Set the minimum value for the colormap.

        Parameters
        ----------
        new_min : float
        """
        if self.cm_min != new_min:
            self.cm_min = new_min
            if self.cm_min > self.cm_max:
                self.cm_max = self.cm_min

    @Property(float)
    def colorMapMax(self):
        """
        Maximum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_max

    @colorMapMax.setter
    @Slot(float)
    def colorMapMax(self, new_max):
        """
        Set the maximum value for the colormap.

        Parameters
        ----------
        new_max : float
        """
        if self.cm_max != new_max:
            self.cm_max = new_max
            if self.cm_max < self.cm_min:
                self.cm_min = self.cm_max

    def setColorMapLimits(self, mn, mx):
        """
        Set the limit values for the colormap.

        Parameters
        ----------
        mn : int
            The lower limit
        mx : int
            The upper limit
        """
        if mn >= mx:
            return
        self.cm_max = mx
        self.cm_min = mn

    @Property(PyDMColorMap)
    def colorMap(self):
        """
        Return the color map used by the SpectrogramView.

        Returns
        -------
        PyDMColorMap
        """
        return self._colormap

    @colorMap.setter
    def colorMap(self, new_cmap):
        """
        Set the color map used by the SpectrogramView.

        Parameters
        -------
        new_cmap : PyDMColorMap
        """
        self._colormap = new_cmap
        self._cm_colors = self.color_maps[new_cmap]
        self.setColorMap()
        for action in self.cm_group.actions():
            if self.cmap_for_action[action] == self._colormap:
                action.setChecked(True)
            else:
                action.setChecked(False)

    def setColorMap(self, cmap=None):
        """
        Update the image colormap.

        Parameters
        ----------
        cmap : ColorMap
        """
        if not cmap:
            if not self._cm_colors.any():
                return
            # Take default values
            pos = np.linspace(0.0, 1.0, num=len(self._cm_colors))
            cmap = ColorMap(pos, self._cm_colors)
        self._view.setBackgroundColor(cmap.map(0))
        lut = cmap.getLookupTable(0.0, 1.0, alpha=False)
        self.colorbar.setIntColorScale(colors=lut)
        self._image_item.setLookupTable(lut)

    # --- Connection Slots ---
    @Slot(bool)
    def image_connection_state_changed(self, conn):
        """
        Callback invoked when the Image Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        if conn:
            self.redraw_timer.start()
        else:
            self.redraw_timer.stop()

    @Slot(bool)
    def yaxis_connection_state_changed(self, connected):
        """
        Callback invoked when the TimeAxis Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        self._timeaxis_connected = connected

    @Slot(bool)
    def roioffsetx_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetX Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsetx = 0

    @Slot(bool)
    def roioffsety_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetY Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsety = 0

    @Slot(bool)
    def roiwidth_connection_state_changed(self, conn):
        """
        Run when the ROIWidth Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_width = 0

    @Slot(bool)
    def roiheight_connection_state_changed(self, conn):
        """
        Run when the ROIHeight Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_height = 0

    # --- Value Slots ---
    @Slot(np.ndarray)
    def image_value_changed(self, new_image):
        """
        Callback invoked when the Image Channel value is changed.

        We try to do as little as possible in this method, because it
        gets called every time the image channel updates, which might
        be extremely often.  Basically just store the data, and set
        a flag requesting that the image be redrawn.

        Parameters
        ----------
        new_image : np.ndarray
            The new image data.  This can be a flat 1D array, or a 2D array.
        """
        if new_image is None or new_image.size == 0:
            return
        logging.debug("SpectrogramView Received New Image: Needs Redraw->True")
        self.image_waveform = new_image
        self.needs_redraw = True
        if not self._image_height and self._image_width:
            self._image_height = new_image.size / self._image_width
        elif not self._image_width and self._image_height:
            self._image_width = new_image.size / self._image_height

    @Slot(np.ndarray)
    @Slot(float)
    def xaxis_value_changed(self, new_array):
        """
        Callback invoked when the Image Width Channel value is changed.

        Parameters
        ----------
        new_array : np.ndarray
            The new x axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_xaxis_data = new_array
        if self._reading_order == self.Clike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(np.ndarray)
    @Slot(float)
    def yaxis_value_changed(self, new_array):
        """
        Callback invoked when the TimeAxis Channel value is changed.

        Parameters
        ----------
        new_array : np.array
            The new y axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_yaxis_data = new_array
        if self._reading_order == self.Fortranlike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(int)
    def roioffsetx_value_changed(self, new_offset):
        """
        Run when the ROIOffsetX Channel value changes.

        Parameters
        ----------
        new_offsetx : int
            The new image ROI horizontal offset

        """
        if new_offset is None:
            return
        self._roi_offsetx = new_offset
        self.redrawROI()

    @Slot(int)
    def roioffsety_value_changed(self, new_offset):
        """
        Run when the ROIOffsetY Channel value changes.

        Parameters
        ----------
        new_offsety : int
            The new image ROI vertical offset

        """
        if new_offset is None:
            return
        self._roi_offsety = new_offset
        self.redrawROI()

    @Slot(int)
    def roiwidth_value_changed(self, new_width):
        """
        Run when the ROIWidth Channel value changes.

        Parameters
        ----------
        new_width : int
            The new image ROI width

        """
        if new_width is None:
            return
        self._roi_width = int(new_width)
        self.redrawROI()

    @Slot(int)
    def roiheight_value_changed(self, new_height):
        """
        Run when the ROIHeight Channel value changes.

        Parameters
        ----------
        new_height : int
            The new image ROI height

        """
        if new_height is None:
            return
        self._roi_height = int(new_height)
        self.redrawROI()

    # --- Image update methods ---
    def process_image(self, image):
        """
        Boilerplate method.

        To be used by applications in order to add calculations and also modify
        the image before it is displayed at the widget.

        .. warning::
           This code runs in a separated QThread so it **MUST** not try to
           write to QWidgets.

        Parameters
        ----------
        image : np.ndarray
            The Image Data as a 2D numpy array

        Returns
        -------
        np.ndarray
            The Image Data as a 2D numpy array after processing.
        """
        return image

    def redrawImage(self):
        """
        Set the image data into the ImageItem, if needed.

        If necessary, reshape the image to 2D first.
        """
        if self.thread is not None and not self.thread.isFinished():
            logger.warning(
                "Image processing has taken longer than the refresh rate.")
            return
        self.thread = SpectrogramUpdateThread(self)
        self.thread.updateSignal.connect(self._updateDisplay)
        logging.debug("SpectrogramView RedrawImage Thread Launched")
        self.thread.start()

    @Slot(list)
    def _updateDisplay(self, data):
        logging.debug("SpectrogramView Update Display with new image")

        # Update axis
        if self._last_xaxis_data is not None:
            szx = self._last_xaxis_data.size
            xMin = self._last_xaxis_data.min()
            xMax = self._last_xaxis_data.max()
        else:
            szx = self.imageWidth if self.readingOrder == self.Clike \
                else self.imageHeight
            xMin = 0
            xMax = szx

        if self._last_yaxis_data is not None:
            szy = self._last_yaxis_data.size
            yMin = self._last_yaxis_data.min()
            yMax = self._last_yaxis_data.max()
        else:
            szy = self.imageHeight if self.readingOrder == self.Clike \
                else self.imageWidth
            yMin = 0
            yMax = szy

        self.xaxis.setRange(xMin, xMax)
        self.yaxis.setRange(yMin, yMax)
        self._view.setLimits(xMin=0,
                             xMax=szx,
                             yMin=0,
                             yMax=szy,
                             minXRange=szx,
                             maxXRange=szx,
                             minYRange=szy,
                             maxYRange=szy)

        # Update image
        if self.autoSetColorbarLims:
            self.colorbar.setLimits(data)
        mini, maxi = data[0], data[1]
        img = data[2]
        self._image_item.setLevels([mini, maxi])
        self._image_item.setImage(img,
                                  autoLevels=False,
                                  autoDownsample=self.autoDownsample)

    # ROI update methods
    def redrawROI(self):
        startx = self._roi_offsetx
        endx = self._roi_offsetx + self._roi_width
        starty = self._roi_offsety
        endy = self._roi_offsety + self._roi_height
        self.ROICurve.setData([startx, startx, endx, endx, startx],
                              [starty, endy, endy, starty, starty])

    def showROI(self, show):
        """Set ROI visibility."""
        pen = mkPen()
        if show:
            pen.setColor(self.ROIColor)
        else:
            pen.setColor(QColor('transparent'))
        self.ROICurve.setPen(pen)

    # --- Properties ---
    @Property(bool)
    def autoDownsample(self):
        """
        Return if we should or not apply the autoDownsample option.

        Return
        ------
        bool
        """
        return self._auto_downsample

    @autoDownsample.setter
    def autoDownsample(self, new_value):
        """
        Whether we should or not apply the autoDownsample option.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_downsample:
            self._auto_downsample = new_value

    @Property(bool)
    def autoSetColorbarLims(self):
        """
        Return if we should or not auto set colorbar limits.

        Return
        ------
        bool
        """
        return self._auto_colorbar_lims

    @autoSetColorbarLims.setter
    def autoSetColorbarLims(self, new_value):
        """
        Whether we should or not auto set colorbar limits.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_colorbar_lims:
            self._auto_colorbar_lims = new_value

    @Property(int)
    def imageWidth(self):
        """
        Return the width of the image.

        Return
        ------
        int
        """
        return self._image_width

    @imageWidth.setter
    def imageWidth(self, new_width):
        """
        Set the width of the image.

        Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`.

        Parameters
        ----------
        new_width: int
        """
        boo = self._image_width != int(new_width)
        boo &= not self._xaxischannel
        boo &= not self._yaxischannel
        if boo:
            self._image_width = int(new_width)

    @Property(int)
    def imageHeight(self):
        """
        Return the height of the image.

        Return
        ------
        int
        """
        return self._image_height

    @Property(int)
    def ROIOffsetX(self):
        """
        Return the ROI offset in X axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsetx

    @ROIOffsetX.setter
    def ROIOffsetX(self, new_offset):
        """
        Set the ROI offset in X axis in pixels.

        Can be overridden by :attr:`ROIOffsetXChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsetx != int(new_offset)
        boo &= not self._roioffsetxchannel
        if boo:
            self._roi_offsetx = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIOffsetY(self):
        """
        Return the ROI offset in Y axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsety

    @ROIOffsetY.setter
    def ROIOffsetY(self, new_offset):
        """
        Set the ROI offset in Y axis in pixels.

        Can be overridden by :attr:`ROIOffsetYChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsety != int(new_offset)
        boo &= not self._roioffsetychannel
        if boo:
            self._roi_offsety = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIWidth(self):
        """
        Return the ROI width in pixels.

        Return
        ------
        int
        """
        return self._roi_width

    @ROIWidth.setter
    def ROIWidth(self, new_width):
        """
        Set the ROI width in pixels.

        Can be overridden by :attr:`ROIWidthChannel`.

        Parameters
        ----------
        new_width: int
        """
        if new_width is None:
            return
        boo = self._roi_width != int(new_width)
        boo &= not self._roiwidthchannel
        if boo:
            self._roi_width = int(new_width)
            self.redrawROI()

    @Property(int)
    def ROIHeight(self):
        """
        Return the ROI height in pixels.

        Return
        ------
        int
        """
        return self._roi_height

    @ROIHeight.setter
    def ROIHeight(self, new_height):
        """
        Set the ROI height in pixels.

        Can be overridden by :attr:`ROIHeightChannel`.

        Parameters
        ----------
        new_height: int
        """
        if new_height is None:
            return
        boo = self._roi_height != int(new_height)
        boo &= not self._roiheightchannel
        if boo:
            self._roi_height = int(new_height)
            self.redrawROI()

    @Property(bool)
    def normalizeData(self):
        """
        Return True if the colors are relative to data maximum and minimum.

        Returns
        -------
        bool
        """
        return self._normalize_data

    @normalizeData.setter
    @Slot(bool)
    def normalizeData(self, new_norm):
        """
        Define if the colors are relative to minimum and maximum of the data.

        Parameters
        ----------
        new_norm: bool
        """
        if self._normalize_data != new_norm:
            self._normalize_data = new_norm

    @Property(ReadingOrder)
    def readingOrder(self):
        """
        Return the reading order of the :attr:`imageChannel` array.

        Returns
        -------
        ReadingOrder
        """
        return self._reading_order

    @readingOrder.setter
    def readingOrder(self, order):
        """
        Set reading order of the :attr:`imageChannel` array.

        Parameters
        ----------
        order: ReadingOrder
        """
        if self._reading_order != order:
            self._reading_order = order

        if order == self.Clike:
            if self._last_xaxis_data is not None:
                self._image_width = self._last_xaxis_data.size
            if self._last_yaxis_data is not None:
                self._image_height = self._last_yaxis_data.size
        elif order == self.Fortranlike:
            if self._last_yaxis_data is not None:
                self._image_width = self._last_yaxis_data.size
            if self._last_xaxis_data is not None:
                self._image_height = self._last_xaxis_data.size

    @Property(int)
    def maxRedrawRate(self):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Returns
        -------
        int
        """
        return self._redraw_rate

    @maxRedrawRate.setter
    def maxRedrawRate(self, redraw_rate):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Parameters
        -------
        redraw_rate : int
        """
        self._redraw_rate = redraw_rate
        self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))

    # --- Events rederivations ---
    def keyPressEvent(self, ev):
        """Handle keypress events."""
        return

    def mouseMoveEvent(self, ev):
        if not self._image_item.width() or not self._image_item.height():
            super().mouseMoveEvent(ev)
            return
        pos = ev.pos()
        posaux = self._image_item.mapFromDevice(ev.pos())
        if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \
                posaux.y() < 0 or posaux.y() >= self._image_item.height():
            super().mouseMoveEvent(ev)
            return

        pos_scene = self._view.mapSceneToView(pos)
        x = round(pos_scene.x())
        y = round(pos_scene.y())

        if self.xAxisChannel and self._last_xaxis_data is not None:
            maxx = len(self._last_xaxis_data) - 1
            x = x if x < maxx else maxx
            valx = self._last_xaxis_data[x]
        else:
            valx = x

        if self.yAxisChannel and self._last_yaxis_data is not None:
            maxy = len(self._last_yaxis_data) - 1
            y = y if y < maxy else maxy
            valy = self._last_yaxis_data[y]
        else:
            valy = y

        txt = self.format_tooltip.format(valx, valy)
        QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(),
                          5000)
        super().mouseMoveEvent(ev)

    # --- Channels ---
    @Property(str)
    def imageChannel(self):
        """
        The channel address in use for the image data .

        Returns
        -------
        str
            Channel address
        """
        if self._imagechannel:
            return str(self._imagechannel.address)
        else:
            return ''

    @imageChannel.setter
    def imageChannel(self, value):
        """
        The channel address in use for the image data .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._imagechannel != value:
            # Disconnect old channel
            if self._imagechannel:
                self._imagechannel.disconnect()
            # Create and connect new channel
            self._imagechannel = PyDMChannel(
                address=value,
                connection_slot=self.image_connection_state_changed,
                value_slot=self.image_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[0] = self._imagechannel
            self._imagechannel.connect()

    @Property(str)
    def xAxisChannel(self):
        """
        The channel address in use for the x-axis of image.

        Returns
        -------
        str
            Channel address
        """
        if self._xaxischannel:
            return str(self._xaxischannel.address)
        else:
            return ''

    @xAxisChannel.setter
    def xAxisChannel(self, value):
        """
        The channel address in use for the x-axis of image.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._xaxischannel != value:
            # Disconnect old channel
            if self._xaxischannel:
                self._xaxischannel.disconnect()
            # Create and connect new channel
            self._xaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.connectionStateChanged,
                value_slot=self.xaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[1] = self._xaxischannel
            self._xaxischannel.connect()

    @Property(str)
    def yAxisChannel(self):
        """
        The channel address in use for the time axis.

        Returns
        -------
        str
            Channel address
        """
        if self._yaxischannel:
            return str(self._yaxischannel.address)
        else:
            return ''

    @yAxisChannel.setter
    def yAxisChannel(self, value):
        """
        The channel address in use for the time axis.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._yaxischannel != value:
            # Disconnect old channel
            if self._yaxischannel:
                self._yaxischannel.disconnect()
            # Create and connect new channel
            self._yaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.yaxis_connection_state_changed,
                value_slot=self.yaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[2] = self._yaxischannel
            self._yaxischannel.connect()

    @Property(str)
    def ROIOffsetXChannel(self):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetxchannel:
            return str(self._roioffsetxchannel.address)
        else:
            return ''

    @ROIOffsetXChannel.setter
    def ROIOffsetXChannel(self, value):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetxchannel != value:
            # Disconnect old channel
            if self._roioffsetxchannel:
                self._roioffsetxchannel.disconnect()
            # Create and connect new channel
            self._roioffsetxchannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsetx_connection_state_changed,
                value_slot=self.roioffsetx_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[3] = self._roioffsetxchannel
            self._roioffsetxchannel.connect()

    @Property(str)
    def ROIOffsetYChannel(self):
        """
        Return the channel address in use for the image ROI vertical offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetychannel:
            return str(self._roioffsetychannel.address)
        else:
            return ''

    @ROIOffsetYChannel.setter
    def ROIOffsetYChannel(self, value):
        """
        Return the channel address in use for the image ROI vertical offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetychannel != value:
            # Disconnect old channel
            if self._roioffsetychannel:
                self._roioffsetychannel.disconnect()
            # Create and connect new channel
            self._roioffsetychannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsety_connection_state_changed,
                value_slot=self.roioffsety_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[4] = self._roioffsetychannel
            self._roioffsetychannel.connect()

    @Property(str)
    def ROIWidthChannel(self):
        """
        Return the channel address in use for the image ROI width.

        Returns
        -------
        str
            Channel address

        """
        if self._roiwidthchannel:
            return str(self._roiwidthchannel.address)
        else:
            return ''

    @ROIWidthChannel.setter
    def ROIWidthChannel(self, value):
        """
        Return the channel address in use for the image ROI width.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiwidthchannel != value:
            # Disconnect old channel
            if self._roiwidthchannel:
                self._roiwidthchannel.disconnect()
            # Create and connect new channel
            self._roiwidthchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiwidth_connection_state_changed,
                value_slot=self.roiwidth_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[5] = self._roiwidthchannel
            self._roiwidthchannel.connect()

    @Property(str)
    def ROIHeightChannel(self):
        """
        Return the channel address in use for the image ROI height.

        Returns
        -------
        str
            Channel address

        """
        if self._roiheightchannel:
            return str(self._roiheightchannel.address)
        else:
            return ''

    @ROIHeightChannel.setter
    def ROIHeightChannel(self, value):
        """
        Return the channel address in use for the image ROI height.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiheightchannel != value:
            # Disconnect old channel
            if self._roiheightchannel:
                self._roiheightchannel.disconnect()
            # Create and connect new channel
            self._roiheightchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiheight_connection_state_changed,
                value_slot=self.roiheight_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[6] = self._roiheightchannel
            self._roiheightchannel.connect()

    def channels(self):
        """
        Return the channels being used for this Widget.

        Returns
        -------
        channels : list
            List of PyDMChannel objects
        """
        return self._channels

    def channels_for_tools(self):
        """Return channels for tools."""
        return [self._imagechannel]
class FilterPreviews(GraphicsLayoutWidget):
    image_before: ImageItem
    image_after: ImageItem
    image_diff: ImageItem
    histogram_before: Optional[PlotItem]
    histogram_after: Optional[PlotItem]
    histogram: Optional[PlotItem]

    def __init__(self, parent=None, **kwargs):
        super(FilterPreviews, self).__init__(parent, **kwargs)
        self.before_histogram_data = None
        self.after_histogram_data = None
        self.histogram = None
        self.before_histogram = None
        self.after_histogram = None

        self.combined_histograms = True
        self.histogram_legend_visible = True

        self.addLabel("Image before")
        self.addLabel("Image after")
        self.addLabel("Image difference")
        self.nextRow()

        self.image_before, self.image_before_vb = self.image_in_vb(
            name="before")
        self.image_after, self.image_after_vb = self.image_in_vb(name="after")
        self.image_difference, self.image_difference_vb = self.image_in_vb(
            name="difference")

        self.image_after_overlay = ImageItem()
        self.image_after_overlay.setZValue(10)
        self.image_after_vb.addItem(self.image_after_overlay)

        # Ensure images resize equally
        image_layout = self.addLayout(colspan=3)
        image_layout.addItem(self.image_before_vb, 0, 0)
        image_layout.addItem(self.image_after_vb, 0, 1)
        image_layout.addItem(self.image_difference_vb, 0, 2)
        self.nextRow()

        before_details = self.addLabel("")
        after_details = self.addLabel("")
        difference_details = self.addLabel("")

        self.display_formatted_detail = {
            self.image_before:
            lambda val: before_details.setText(f"Before: {val:.6f}"),
            self.image_after:
            lambda val: after_details.setText(f"After: {val:.6f}"),
            self.image_difference:
            lambda val: difference_details.setText(f"Difference: {val:.6f}"),
        }

        for img in self.image_before, self.image_after, self.image_difference:
            img.hoverEvent = lambda ev: self.mouse_over(ev)

    def image_in_vb(self, name=None):
        im = ImageItem()
        im.setAutoDownsample(False)
        vb = ViewBox(invertY=True, lockAspect=True, name=name)
        vb.addItem(im)
        return im, vb

    def clear_items(self):
        self.image_before.clear()
        self.image_after.clear()
        self.image_difference.clear()
        self.image_after_overlay.clear()
        self.delete_histograms()

    # There seems to be a bug with pyqtgraph.PlotDataItem.setData not forcing a redraw.
    # We work around this by redrawing everything completely every time, which is unreasonably fast anyway.
    def redraw_histograms(self):
        self.delete_histograms()
        self.delete_histogram_labels()

        if self.combined_histograms:
            self.draw_combined_histogram()
        else:
            self.draw_separate_histograms()

    def delete_histograms(self):
        coords = set(c for c in histogram_coords.values())
        histograms = (self.getItem(*coord) for coord in coords)
        for histogram in filter(lambda h: h is not None, histograms):
            self.removeItem(histogram)
        self.histogram = None
        self.before_histogram = None
        self.after_histogram = None
        self.diff_histogram = None

    def delete_histogram_labels(self):
        coords = set(c for c in label_coords.values())
        labels = (self.getItem(*coord) for coord in coords)
        for label in filter(lambda h: h is not None, labels):
            self.removeItem(label)

    def draw_combined_histogram(self):
        self.histogram = self.addPlot(row=histogram_coords["combined"].row,
                                      col=histogram_coords["combined"].col,
                                      labels=histogram_axes_labels,
                                      lockAspect=True,
                                      colspan=3)
        self.addLabel("Pixel values",
                      row=label_coords["combined"].row,
                      col=label_coords["combined"].col)

        legend = self.histogram.addLegend()
        # Plot any histogram that has data, and add a legend if both exist
        if _data_valid_for_histogram(self.before_histogram_data):
            before_plot = self.histogram.plot(*self.before_histogram_data,
                                              pen=before_pen)
            legend.addItem(before_plot, "Before")
        if _data_valid_for_histogram(self.after_histogram_data):
            after_plot = self.histogram.plot(*self.after_histogram_data,
                                             pen=after_pen)
            legend.addItem(after_plot, "After")

    def draw_separate_histograms(self):
        hc = histogram_coords
        self.before_histogram = self.addPlot(row=hc["before"].row,
                                             col=hc["before"].col,
                                             labels=histogram_axes_labels,
                                             lockAspect=True)
        self.after_histogram = self.addPlot(row=hc["after"].row,
                                            col=hc["after"].col,
                                            labels=histogram_axes_labels,
                                            lockAspect=True)
        lc = label_coords
        self.addLabel("Pixel values before",
                      row=lc["before"].row,
                      col=lc["before"].col)
        self.addLabel("Pixel values after",
                      row=lc["after"].row,
                      col=lc["after"].col)
        if _data_valid_for_histogram(self.before_histogram_data):
            self.before_histogram.plot(*self.before_histogram_data,
                                       pen=before_pen)
        if _data_valid_for_histogram(self.after_histogram_data):
            self.after_histogram.plot(*self.after_histogram_data,
                                      pen=after_pen)

    def set_before_histogram(self, data: Tuple[ndarray]):
        self.before_histogram_data = data
        self.redraw_histograms()

    def set_after_histogram(self, data: Tuple[ndarray]):
        self.after_histogram_data = data
        self.redraw_histograms()

    @property
    def histogram_legend(self) -> Optional[LegendItem]:
        if self.histogram and self.histogram.legend:
            return self.histogram.legend
        return None

    def mouse_over(self, ev):
        # Ignore events triggered by leaving window or right clicking
        if ev.exit:
            return
        pos = CloseEnoughPoint(ev.pos())
        for img in self.image_before, self.image_after, self.image_difference:
            if img.image is not None and pos.x < img.image.shape[
                    0] and pos.y < img.image.shape[1]:
                pixel_value = img.image[pos.y, pos.x]
                self.display_formatted_detail[img](pixel_value)

    def link_all_views(self):
        for view1, view2 in zip(
            [self.image_before_vb, self.image_after_vb],
            [self.image_after_vb, self.image_difference_vb]):
            view1.linkView(ViewBox.XAxis, view2)
            view1.linkView(ViewBox.YAxis, view2)

    def unlink_all_views(self):
        for view in self.image_before_vb, self.image_after_vb, self.image_difference_vb:
            view.linkView(ViewBox.XAxis, None)
            view.linkView(ViewBox.YAxis, None)

    def add_difference_overlay(self, diff):
        diff = -diff
        diff[diff > 0.0] = 1.0
        pos = np.array([0, 1])
        color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte)
        map = ColorMap(pos, color)
        self.image_after_overlay.setOpacity(1)
        self.image_after_overlay.setImage(diff)
        lut = map.getLookupTable(0, 1, 2)
        self.image_after_overlay.setLookupTable(lut)

    def hide_difference_overlay(self):
        self.image_after_overlay.setOpacity(0)
Exemple #9
0
class Plot2DWidget(PlotWidget):
    def __init__(self, model_wrapper, statusbar, parent=None):
        """
        The Plot2DWidget is responsible for rendering the 2D chromatogram data.
        :param model_wrapper: the wrapper of the model.
        :param parent: the parent of this Widget.
        """
        super().__init__(parent=parent)

        self.listener = Plot2DListener(self, model_wrapper, statusbar)
        """ The listener for the 2D plot """
        self.img = ImageItem()
        """ The image of the chromatogram"""
        self.wrapper_temp = model_wrapper  # TEMPORARY TODO What is this for?
        """A temporary reference to the wrapper?"""

        # Add the image to the plot.
        self.addItem(self.img)

        # Disable right click context menu.
        self.getPlotItem().setMenuEnabled(False)
        self.getPlotItem().getAxis('bottom').enableAutoSIPrefix(False)
        self.getPlotItem().getAxis('left').enableAutoSIPrefix(False)

        model_wrapper.add_observer(self, self.notify)

        # call notify to draw the model. NOTE: The if statement isn't nesessary, it checks in notify if there is
        # a model or not.
        self.notify('model', model_wrapper.model)

    def refresh_x_period(self, x_period):
        if x_period == 0:
            self.getPlotItem().getAxis('bottom').setScale(1)
        else:
            self.getPlotItem().getAxis('bottom').setScale(
                x_period / self.wrapper_temp.model.get_width())

    def refresh_y_period(self, y_period):
        if y_period == 0:
            self.getPlotItem().getAxis('left').setScale(1)
        else:
            self.getPlotItem().getAxis('left').setScale(
                y_period / self.wrapper_temp.model.get_height())

    def refresh_x_unit(self, x_unit):
        if x_unit is TimeUnit.NONE:
            self.getPlotItem().getAxis('bottom').setLabel(units="")
        else:
            self.getPlotItem().getAxis('bottom').setLabel(
                units=x_unit.name.lower())

    def refresh_y_unit(self, y_unit):
        if y_unit is TimeUnit.NONE:
            self.getPlotItem().getAxis('left').setLabel(units="")
        else:
            self.getPlotItem().getAxis('left').setLabel(
                units=y_unit.name.lower())

    def notify(self, name, value):
        """
        Updates the image rendered to match the model.
        :return: None
        """

        if name == 'newIntegration':
            self.addItem(value.selector.roi)
            value.selector.set_viewport(self.img)
        elif name == 'removeIntegration':
            self.removeItem(value.selector.roi)
        elif name in {'model', 'model.viewTransformed'}:
            if value is None or value.get_2d_chromatogram_data() is None:
                self.img.clear()
            else:
                self.img.setImage(value.get_2d_chromatogram_data().clip(
                    value.lower_bound, value.upper_bound),
                                  lut=value.palette)

                self.refresh_x_period(
                    self.wrapper_temp.get_preference(ScaleEnum.X_PERIOD))
                self.refresh_y_period(
                    self.wrapper_temp.get_preference(ScaleEnum.Y_PERIOD))
                self.refresh_x_unit(
                    self.wrapper_temp.get_preference(ScaleEnum.X_UNIT))
                self.refresh_y_unit(
                    self.wrapper_temp.get_preference(ScaleEnum.Y_UNIT))
        elif name == 'model.palette':
            self.img.setLookupTable(value.palette)
        elif name == 'model.lower_bound' or name == 'model.upper_bound':
            self.img.setImage(value.get_2d_chromatogram_data().clip(
                value.lower_bound, value.upper_bound),
                              lut=value.palette)
        elif name == ScaleEnum.X_UNIT.name:
            self.refresh_x_unit(value)
        elif name == ScaleEnum.Y_UNIT.name:
            self.refresh_y_unit(value)
        elif name == ScaleEnum.X_PERIOD.name:
            self.refresh_x_period(value)
        elif name == ScaleEnum.Y_PERIOD.name:
            self.refresh_y_period(value)
class astraPlotWidget(QWidget):

    twissplotLayout = [
        {
            'name': 'sigma_x',
            'range': [0, 1],
            'scale': 1e3
        },
        {
            'name': 'sigma_y',
            'range': [0, 1],
            'scale': 1e3
        },
        {
            'name': 'kinetic_energy',
            'range': [0, 250],
            'scale': 1e-6
        },
        'next_row',
        {
            'name': 'sigma_p',
            'range': [0, 0.015],
            'scale': 1e6
        },
        {
            'name': 'sigma_z',
            'range': [0, 0.6],
            'scale': 1e3
        },
        {
            'name': 'enx',
            'range': [0.5, 1.5],
            'scale': 1e6
        },
        'next_row',
        {
            'name': 'eny',
            'range': [0.5, 1.5],
            'scale': 1e6
        },
        {
            'name': 'beta_x',
            'range': [0, 150],
            'scale': 1
        },
        {
            'name': 'beta_y',
            'range': [0, 150],
            'scale': 1
        },
    ]

    def __init__(self, directory='.', **kwargs):
        super(astraPlotWidget, self).__init__(**kwargs)
        self.beam = raf.beam()
        self.twiss = rtf.twiss()
        self.directory = directory
        ''' twissPlotWidget '''
        self.twissPlotView = GraphicsView(useOpenGL=True)
        self.twissPlotWidget = GraphicsLayout()
        self.twissPlotView.setCentralItem(self.twissPlotWidget)

        self.latticePlotData = imageio.imread('lattice_plot.png')
        self.latticePlots = {}
        self.twissPlots = {}
        i = -1
        for entry in self.twissplotLayout:
            if entry == 'next_row':
                self.twissPlotWidget.nextRow()
            else:
                i += 1
                p = self.twissPlotWidget.addPlot(title=entry['name'])
                p.showGrid(x=True, y=True)
                vb = p.vb
                vb.setYRange(*entry['range'])
                latticePlot = ImageItem(self.latticePlotData)
                latticePlot.setOpts(axisOrder='row-major')
                vb.addItem(latticePlot)
                latticePlot.setZValue(-1)  # make sure this image is on top
                # latticePlot.setOpacity(0.5)
                self.twissPlots[entry['name']] = p.plot(
                    pen=mkPen('b', width=3))
                self.latticePlots[p.vb] = latticePlot
                p.vb.sigRangeChanged.connect(self.scaleLattice)
        ''' beamPlotWidget '''
        self.beamPlotWidget = QWidget()
        self.beamPlotLayout = QVBoxLayout()
        self.item = ImageItem()
        self.beamPlotWidget.setLayout(self.beamPlotLayout)
        self.beamPlotView = ImageView(imageItem=self.item)
        self.rainbow = rainbow()
        self.item.setLookupTable(self.rainbow)
        self.item.setLevels([0, 1])
        # self.beamPlotWidgetGraphicsLayout = GraphicsLayout()
        # p = self.beamPlotWidgetGraphicsLayout.addPlot(title='beam')
        # p.showGrid(x=True, y=True)
        # self.beamPlot = p.plot(pen=None, symbol='+')
        # self.beamPlotView.setCentralItem(self.beamPlotWidgetGraphicsLayout)
        self.beamPlotXAxisCombo = QComboBox()
        self.beamPlotXAxisCombo.addItems(
            ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma'])
        self.beamPlotYAxisCombo = QComboBox()
        self.beamPlotYAxisCombo.addItems(
            ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma'])
        self.beamPlotNumberBins = QSpinBox()
        self.beamPlotNumberBins.setRange(10, 500)
        self.beamPlotNumberBins.setSingleStep(10)
        self.histogramBins = 100
        self.beamPlotNumberBins.setValue(self.histogramBins)
        self.beamPlotAxisWidget = QWidget()
        self.beamPlotAxisLayout = QHBoxLayout()
        self.beamPlotAxisWidget.setLayout(self.beamPlotAxisLayout)
        self.beamPlotAxisLayout.addWidget(self.beamPlotXAxisCombo)
        self.beamPlotAxisLayout.addWidget(self.beamPlotYAxisCombo)
        self.beamPlotAxisLayout.addWidget(self.beamPlotNumberBins)
        self.beamPlotXAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.beamPlotYAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.beamPlotNumberBins.valueChanged.connect(self.plotDataBeam)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.beamPlotLayout.addWidget(self.beamPlotAxisWidget)
        self.beamPlotLayout.addWidget(self.beamPlotView)
        ''' slicePlotWidget '''
        self.sliceParams = [
            {
                'name': 'slice_normalized_horizontal_emittance',
                'units': 'm-rad',
                'text': 'enx'
            },
            {
                'name': 'slice_normalized_vertical_emittance',
                'units': 'm-rad',
                'text': 'eny'
            },
            {
                'name': 'slice_peak_current',
                'units': 'A',
                'text': 'PeakI'
            },
            {
                'name': 'slice_relative_momentum_spread',
                'units': '%',
                'text': 'sigma-p'
            },
        ]
        self.slicePlotWidget = QWidget()
        self.slicePlotLayout = QVBoxLayout()
        self.slicePlotWidget.setLayout(self.slicePlotLayout)
        # self.slicePlotView = GraphicsView(useOpenGL=True)
        self.slicePlotWidgetGraphicsLayout = GraphicsLayoutWidget()
        # self.slicePlots = {}
        self.slicePlotCheckbox = {}
        self.curve = {}
        self.sliceaxis = {}
        self.slicePlotCheckboxWidget = QWidget()
        self.slicePlotCheckboxLayout = QVBoxLayout()
        self.slicePlotCheckboxWidget.setLayout(self.slicePlotCheckboxLayout)
        self.slicePlot = self.slicePlotWidgetGraphicsLayout.addPlot(
            title='Slice', row=0, col=50)
        self.slicePlot.showAxis('left', False)
        self.slicePlot.showGrid(x=True, y=True)
        i = -1
        colors = ['b', 'r', 'g', 'k']
        for param in self.sliceParams:
            i += 1
            axis = AxisItem("left")
            labelStyle = {'color': '#' + colorStr(mkColor(colors[i]))[0:-2]}
            axis.setLabel(text=param['text'],
                          units=param['units'],
                          **labelStyle)
            viewbox = ViewBox()
            axis.linkToView(viewbox)
            viewbox.setXLink(self.slicePlot.vb)
            self.sliceaxis[param['name']] = [axis, viewbox]
            self.curve[param['name']] = PlotDataItem(pen=colors[i], symbol='+')
            viewbox.addItem(self.curve[param['name']])
            col = self.findFirstEmptyColumnInGraphicsLayout()
            self.slicePlotWidgetGraphicsLayout.ci.addItem(axis,
                                                          row=0,
                                                          col=col,
                                                          rowspan=1,
                                                          colspan=1)
            self.slicePlotWidgetGraphicsLayout.ci.addItem(viewbox,
                                                          row=0,
                                                          col=50)
            p.showGrid(x=True, y=True)
            # self.slicePlots[param] = self.slicePlot.plot(pen=colors[i], symbol='+')
            self.slicePlotCheckbox[param['name']] = QCheckBox(param['text'])
            self.slicePlotCheckboxLayout.addWidget(
                self.slicePlotCheckbox[param['name']])
            self.slicePlotCheckbox[param['name']].stateChanged.connect(
                self.plotDataSlice)
        # self.slicePlotView.setCentralItem(self.slicePlotWidgetGraphicsLayout)
        self.slicePlotSliceWidthWidget = QSpinBox()
        self.slicePlotSliceWidthWidget.setMaximum(1000)
        self.slicePlotSliceWidthWidget.setValue(100)
        self.slicePlotSliceWidthWidget.setSingleStep(10)
        self.slicePlotSliceWidthWidget.setSuffix("fs")
        self.slicePlotSliceWidthWidget.setSpecialValueText('Automatic')
        self.slicePlotAxisWidget = QWidget()
        self.slicePlotAxisLayout = QHBoxLayout()
        self.slicePlotAxisWidget.setLayout(self.slicePlotAxisLayout)
        self.slicePlotAxisLayout.addWidget(self.slicePlotCheckboxWidget)
        self.slicePlotAxisLayout.addWidget(self.slicePlotSliceWidthWidget)
        # self.slicePlotXAxisCombo.currentIndexChanged.connect(self.plotDataSlice)
        self.slicePlotSliceWidthWidget.valueChanged.connect(
            self.changeSliceLength)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.slicePlotLayout.addWidget(self.slicePlotAxisWidget)
        self.slicePlotLayout.addWidget(self.slicePlotWidgetGraphicsLayout)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        self.tabWidget = QTabWidget()

        self.folderButton = QPushButton('Select Directory')
        self.folderLineEdit = QLineEdit()
        self.folderLineEdit.setReadOnly(True)
        self.folderLineEdit.setText(self.directory)
        self.reloadButton = QPushButton()
        self.reloadButton.setIcon(qApp.style().standardIcon(
            QStyle.SP_BrowserReload))
        self.folderWidget = QGroupBox()
        self.folderLayout = QHBoxLayout()
        self.folderLayout.addWidget(self.folderButton)
        self.folderLayout.addWidget(self.folderLineEdit)
        self.folderLayout.addWidget(self.reloadButton)
        self.folderWidget.setLayout(self.folderLayout)
        self.folderWidget.setMaximumWidth(800)
        self.reloadButton.clicked.connect(
            lambda: self.changeDirectory(self.directory))
        self.folderButton.clicked.connect(self.changeDirectory)

        self.fileSelector = QComboBox()
        self.fileSelector.currentIndexChanged.connect(self.updateScreenCombo)
        self.screenSelector = QComboBox()
        self.screenSelector.currentIndexChanged.connect(self.changeScreen)
        self.beamWidget = QGroupBox()
        self.beamLayout = QHBoxLayout()
        self.beamLayout.addWidget(self.fileSelector)
        self.beamLayout.addWidget(self.screenSelector)
        self.beamWidget.setLayout(self.beamLayout)
        self.beamWidget.setMaximumWidth(800)
        self.beamWidget.setVisible(False)

        self.folderBeamWidget = QWidget()
        self.folderBeamLayout = QHBoxLayout()
        self.folderBeamLayout.setAlignment(Qt.AlignLeft)
        self.folderBeamWidget.setLayout(self.folderBeamLayout)
        self.folderBeamLayout.addWidget(self.folderWidget)
        self.folderBeamLayout.addWidget(self.beamWidget)

        self.tabWidget.addTab(self.twissPlotView, 'Twiss Plots')
        self.tabWidget.addTab(self.beamPlotWidget, 'Beam Plots')
        self.tabWidget.addTab(self.slicePlotWidget, 'Slice Beam Plots')
        self.tabWidget.currentChanged.connect(self.changeTab)
        self.layout.addWidget(self.folderBeamWidget)
        self.layout.addWidget(self.tabWidget)

        self.plotType = 'Twiss'
        self.changeDirectory(self.directory)

    def findFirstEmptyColumnInGraphicsLayout(self):
        rowsfilled = self.slicePlotWidgetGraphicsLayout.ci.rows.get(0,
                                                                    {}).keys()
        for i in range(49):
            if not i in rowsfilled:
                return i

    def changeTab(self, i):
        if self.tabWidget.tabText(i) == 'Beam Plots':
            self.plotType = 'Beam'
            self.beamWidget.setVisible(True)
        elif self.tabWidget.tabText(i) == 'Slice Beam Plots':
            self.plotType = 'Slice'
            self.beamWidget.setVisible(True)
        else:
            self.plotType = 'Twiss'
            self.beamWidget.setVisible(False)
        self.loadDataFile()

    def changeDirectory(self, directory=None):
        if directory == None or directory == False:
            self.directory = str(
                QFileDialog.getExistingDirectory(self, "Select Directory",
                                                 self.directory,
                                                 QFileDialog.ShowDirsOnly))
        else:
            self.directory = directory
        self.folderLineEdit.setText(self.directory)
        self.currentFileText = self.fileSelector.currentText()
        self.currentScreenText = self.screenSelector.currentText()
        self.getScreenFiles()
        self.updateFileCombo()
        self.updateScreenCombo()
        self.loadDataFile()

    def getScreenFiles(self):
        self.screenpositions = {}
        files = glob.glob(self.directory + '/*.????.???')
        filenames = [
            '.'.join(os.path.basename(f).split('.')[:-2]) for f in files
        ]
        print 'filenames = ', filenames
        runnumber = [os.path.basename(f).split('.')[-1] for f in files]
        for f, r in list(set(zip(filenames, runnumber))):
            files = glob.glob(self.directory + '/' + f + '.????.???')
            screenpositions = [
                re.search(f + '\.(\d\d\d\d)\.\d\d\d', s).group(1)
                for s in files
            ]
            print 'screenpositions = ', screenpositions
            self.screenpositions[f] = {
                'screenpositions': sorted(screenpositions),
                'run': r
            }

    def updateFileCombo(self):
        self.fileSelector.clear()
        i = -1
        screenfirstpos = []
        for f in self.screenpositions:
            screenfirstpos.append(
                [f, min(self.screenpositions[f]['screenpositions'])])
        screenfirstpos = np.array(screenfirstpos)
        sortedscreennames = screenfirstpos[np.argsort(
            np.array(screenfirstpos)[:, 1])]
        print 'sortedscreennames = ', sortedscreennames
        for f in sortedscreennames:
            self.fileSelector.addItem(f[0])
            i += 1
            if f[0] == self.currentFileText:
                self.fileSelector.setCurrentIndex(i)

    def changeScreen(self, i):
        run = self.screenpositions[str(self.fileSelector.currentText())]['run']
        self.beamFileName = str(self.fileSelector.currentText()) + '.' + str(
            self.screenSelector.currentText()) + '.' + str(run)
        # print 'beamFileName = ', self.beamFileName
        self.loadDataFile()

    def updateScreenCombo(self):
        self.screenSelector.clear()
        i = -1
        for s in self.screenpositions[str(
                self.fileSelector.currentText())]['screenpositions']:
            self.screenSelector.addItem(s)
            i += 1
            if s == self.currentScreenText:
                self.screenSelector.setCurrentIndex(i)

    def loadDataFile(self):
        if self.plotType == 'Twiss':
            files = sorted(glob.glob(self.directory + "/*Xemit*"))
            self.twiss.read_astra_emit_files(files)
            self.plotDataTwiss()
        elif self.plotType == 'Beam' or self.plotType == 'Slice':
            if hasattr(
                    self,
                    'beamFileName') and os.path.isfile(self.directory + '/' +
                                                       self.beamFileName):
                # starttime = time.time()
                self.beam.read_astra_beam_file(self.directory + '/' +
                                               self.beamFileName)
                # print 'reading file took ', time.time()-starttime, 's'
                # print 'Read file: ', self.beamFileName
                if self.plotType == 'Beam':
                    self.plotDataBeam()
                else:
                    self.beam.bin_time()
                    self.plotDataSlice()

    def plotDataTwiss(self):
        for entry in self.twissplotLayout:
            if entry == 'next_row':
                pass
            else:
                x = self.twiss['z']
                y = self.twiss[entry['name']] * entry['scale']
                xy = np.transpose(np.array([x, y]))
                x, y = np.transpose(xy[np.argsort(xy[:, 0])])
                self.twissPlots[entry['name']].setData(x=x,
                                                       y=y,
                                                       pen=mkPen('b', width=3))

    def plotDataBeam(self):
        self.histogramBins = self.beamPlotNumberBins.value()
        x = getattr(self.beam, str(self.beamPlotXAxisCombo.currentText()))
        y = getattr(self.beam, str(self.beamPlotYAxisCombo.currentText()))
        h, xedges, yedges = np.histogram2d(x,
                                           y,
                                           self.histogramBins,
                                           normed=True)
        x0 = xedges[0]
        y0 = yedges[0]
        xscale = (xedges[-1] - xedges[0]) / len(xedges)
        yscale = (yedges[-1] - yedges[0]) / len(yedges)
        self.item.setImage(h)
        self.item.setLookupTable(self.rainbow)
        # self.item.setLevels([0,1])

    def changeSliceLength(self):
        self.beam.slice_length = self.slicePlotSliceWidthWidget.value() * 1e-15
        self.beam.bin_time()
        self.plotDataSlice()

    def plotDataSlice(self):
        for param in self.sliceParams:
            if self.slicePlotCheckbox[param['name']].isChecked():
                x = self.beam.slice_bins
                self.slicePlot.setRange(xRange=[min(x), max(x)])
                # self.plot.setRange(xRange=[-0.5,1.5])
                y = getattr(self.beam, param['name'])
                self.curve[param['name']].setData(x=x, y=y)
                self.sliceaxis[param['name']][0].setVisible(True)
                # currentrange = self.sliceaxis[param['name']][0].range
                # print 'currentrange = ', currentrange
                # self.sliceaxis[param['name']][0].setRange(0, currentrange[1])
            else:
                # pass
                self.curve[param['name']].setData(x=[], y=[])
                self.sliceaxis[param['name']][0].setVisible(False)
            self.sliceaxis[param['name']][1].autoRange()
            currentrange = self.sliceaxis[param['name']][1].viewRange()
            self.sliceaxis[param['name']][1].setYRange(0, currentrange[1][1])

    def scaleLattice(self, vb, range):
        yrange = range[1]
        scaleY = 0.05 * abs(yrange[1] - yrange[0])
        rect = QRectF(0, yrange[0] + 2 * scaleY, 49.2778, 4 * scaleY)
        self.latticePlots[vb].setRect(rect)
class FilterPreviews(GraphicsLayoutWidget):
    image_before: ImageItem
    image_after: ImageItem
    image_diff: ImageItem
    histogram: Optional[PlotItem]

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        widget_location = self.mapToGlobal(QPoint(self.width() // 2, 0))
        # allow the widget to take up to 80% of the desktop's height
        if QGuiApplication.screenAt(widget_location) is not None:
            screen_height = QGuiApplication.screenAt(
                widget_location).availableGeometry().height()
        else:
            screen_height = max(
                QGuiApplication.primaryScreen().availableGeometry().height(),
                600)
            LOG.info(
                "Unable to detect current screen. Setting screen height to %s"
                % screen_height)
        self.ALLOWED_HEIGHT: QRect = screen_height * 0.8

        self.histogram = None

        self.addLabel("Image before")
        self.addLabel("Image after")
        self.addLabel("Image difference")
        self.nextRow()

        self.imageview_before = MIMiniImageView(name="before")
        self.imageview_after = MIMiniImageView(name="after")
        self.imageview_difference = MIMiniImageView(name="difference")
        self.all_imageviews = [
            self.imageview_before, self.imageview_after,
            self.imageview_difference
        ]
        MIMiniImageView.set_siblings(self.all_imageviews, axis=True)
        MIMiniImageView.set_siblings(
            [self.imageview_before, self.imageview_after], hist=True)

        self.image_before, self.image_before_vb, self.image_before_hist = self.imageview_before.get_parts(
        )
        self.image_after, self.image_after_vb, self.image_after_hist = self.imageview_after.get_parts(
        )
        self.image_difference, self.image_difference_vb, self.image_difference_hist = \
            self.imageview_difference.get_parts()

        self.all_histograms = [
            self.image_before_hist, self.image_after_hist,
            self.image_difference_hist
        ]

        self.image_diff_overlay = ImageItem()
        self.image_diff_overlay.setZValue(10)
        self.image_after_vb.addItem(self.image_diff_overlay)

        # Ensure images resize equally
        self.image_layout: GraphicsLayout = self.addLayout(colspan=3)

        self.image_layout.addItem(self.imageview_before)
        self.image_layout.addItem(self.imageview_after)
        self.image_layout.addItem(self.imageview_difference)
        self.nextRow()

        self.init_histogram()

        # Work around for https://github.com/mantidproject/mantidimaging/issues/565
        self.scene().contextMenu = [
            item for item in self.scene().contextMenu
            if "export" not in item.text().lower()
        ]

        self.auto_colour_actions = []
        self._add_auto_colour_action(self.image_before_hist, self.image_before)
        self._add_auto_colour_action(self.image_after_hist, self.image_after)
        self._add_auto_colour_action(self.image_difference_hist,
                                     self.image_difference)

        self.imageview_before.link_sibling_axis()

        self.imageview_before.enable_nan_check()
        self.imageview_after.enable_nan_check()

    def resizeEvent(self, ev: QResizeEvent):
        if ev is not None and isinstance(self.histogram, PlotItem):
            size = ev.size()
            self.histogram.setFixedHeight(
                min(size.height() * 0.7, self.ALLOWED_HEIGHT) * 0.25)
        super().resizeEvent(ev)

    def clear_items(self, clear_before: bool = True):
        if clear_before:
            self.imageview_before.clear()
        self.imageview_after.clear()
        self.imageview_difference.clear()
        self.image_diff_overlay.clear()

    def init_histogram(self):
        self.histogram = self.addPlot(row=histogram_coords.row,
                                      col=histogram_coords.col,
                                      labels=histogram_axes_labels,
                                      lockAspect=True,
                                      colspan=3)
        self.addLabel("Pixel values",
                      row=label_coords.row,
                      col=label_coords.col)

        self.legend = self.histogram.addLegend()
        self.legend.setOffset((0, 1))

    def update_histogram_data(self):
        # Plot any histogram that has data, and add a legend if both exist
        before_data = self.imageview_before.image_item.getHistogram()
        after_data = self.imageview_after.image_item.getHistogram()
        if _data_valid_for_histogram(before_data):
            before_plot = self.histogram.plot(*before_data,
                                              pen=before_pen,
                                              clear=True)
            self.legend.addItem(before_plot, "Before")

        if _data_valid_for_histogram(after_data):
            after_plot = self.histogram.plot(*after_data, pen=after_pen)
            self.legend.addItem(after_plot, "After")

    @property
    def histogram_legend(self) -> Optional[LegendItem]:
        if self.histogram and self.histogram.legend:
            return self.histogram.legend
        return None

    def link_all_views(self):
        self.imageview_before.link_sibling_axis()

    def unlink_all_views(self):
        self.imageview_before.unlink_sibling_axis()

    def add_difference_overlay(self, diff, nan_change):
        diff = np.absolute(diff)
        diff[diff > OVERLAY_THRESHOLD] = 1.0
        diff[nan_change] = 1.0
        pos = np.array([0, 1])
        color = np.array([[0, 0, 0, 0], OVERLAY_COLOUR_DIFFERENCE],
                         dtype=np.ubyte)
        map = ColorMap(pos, color)
        self.image_diff_overlay.setVisible(True)
        self.image_diff_overlay.setImage(diff)
        lut = map.getLookupTable(0, 1, 2)
        self.image_diff_overlay.setLookupTable(lut)

    def add_negative_overlay(self):
        self.imageview_after.enable_nonpositive_check()

    def hide_difference_overlay(self):
        self.image_diff_overlay.setVisible(False)

    def hide_negative_overlay(self):
        self.imageview_after.enable_nonpositive_check(False)

    def auto_range(self):
        # This will cause the previews to all show by just causing autorange on self.image_before_vb
        self.image_before_vb.autoRange()

    def record_histogram_regions(self):
        self.before_region = self.image_before_hist.region.getRegion()
        self.diff_region = self.image_difference_hist.region.getRegion()
        self.after_region = self.image_after_hist.region.getRegion()

    def restore_histogram_regions(self):
        self.image_before_hist.region.setRegion(self.before_region)
        self.image_difference_hist.region.setRegion(self.diff_region)
        self.image_after_hist.region.setRegion(self.after_region)

    def link_before_after_histogram_scales(self, create_link: bool):
        """
        Connects or disconnects the scales of the before/after histograms.
        :param create_link: Whether the link should be created or removed.
        """
        if create_link:
            self.imageview_after.link_sibling_histogram()
        else:
            self.imageview_after.unlink_sibling_histogram()

    def set_histogram_log_scale(self):
        """
        Sets the y-values of the before and after histogram plots to a log scale.
        """
        set_histogram_log_scale(self.image_before_hist)
        set_histogram_log_scale(self.image_after_hist)

    def _add_auto_colour_action(self, histogram: HistogramLUTItem,
                                image: ImageItem):
        """
        Adds an "Auto" action to the histogram right-click menu.
        :param histogram: The HistogramLUTItem
        :param image: The ImageItem to have the Jenks/Otsu algorithm performed on it.
        """
        self.auto_colour_actions.append(QAction("Auto"))
        self.auto_colour_actions[-1].triggered.connect(
            lambda: self._on_change_colour_palette(histogram, image))

        action = histogram.gradient.menu.actions()[12]
        histogram.gradient.menu.insertAction(action,
                                             self.auto_colour_actions[-1])
        histogram.gradient.menu.insertSeparator(self.auto_colour_actions[-1])

    def _on_change_colour_palette(self, main_histogram: HistogramLUTItem,
                                  image: ImageItem):
        """
        Creates a Palette Changer window when the "Auto" option has been selected.
        :param main_histogram: The HistogramLUTItem.
        :param image: The ImageItem.
        """
        other_histograms = self.all_histograms[:]
        other_histograms.remove(main_histogram)
        change_colour_palette = PaletteChangerView(self, main_histogram,
                                                   image.image,
                                                   other_histograms)
        change_colour_palette.show()
class ImageViewer(GraphicsView):
    """ImageViewer class for Paint4Brains.

    This class contains the implementation of a series of functions required for the GUI of the Paint4Brains project.

    Args:
        brain (class): BrainData class for Paint4Brains 
        parent (class): Base or parent class
    """
    def __init__(self, brain, parent=None):
        super(ImageViewer, self).__init__(parent=parent)

        # Inputting data
        self.brain = brain

        # Creating viewing box to see data
        self.view = ModViewBox()
        self.setCentralItem(self.view)

        # Making Images out of data
        self.over_img = ImageItem(
            self.brain.current_label_data_slice,
            autoDownSmaple=False,
            opacity=1,
            compositionMode=QtGui.QPainter.CompositionMode_Plus)
        self.mid_img = ImageItem(
            np.zeros(self.brain.current_other_labels_data_slice.shape),
            autoDownSmaple=False,
            opacity=0.7,
            compositionMode=QtGui.QPainter.CompositionMode_Plus)
        self.img = ImageItem(
            self.brain.current_data_slice,
            autoDownsample=False,
            compositionMode=QtGui.QPainter.CompositionMode_SourceOver,
            levels=(0., 1.))

        # Colouring the labelled data
        lut = np.array([[0, 0, 0, 0], [250, 0, 0, 255]])
        self.over_img.setLookupTable(lut)
        self.over_img.setLevels([0, 1])

        # Maybe the visualization lecture was not that useless...
        self.colours = [[166, 206, 27], [31, 120, 180], [178, 223, 138],
                        [51, 160, 44], [251, 154, 153], [227, 26, 28],
                        [253, 191, 111], [255, 127, 0], [202, 178, 214],
                        [106, 61, 154], [255, 255, 153], [177, 89, 40]]

        self.update_colormap()

        # Adding the images to the viewing box and setting it to drawing mode (if there is labeled data)
        self.view.addItem(self.img)
        self.view.addItem(self.mid_img)
        self.view.addItem(self.over_img)

        self.select_mode = False
        self.see_all_labels = False

        if self.brain.label_filename is not None:
            self.enable_drawing()
            self.update_colormap()
            self.refresh_image()

        self.dropbox = SelectLabel(self)
        self.bonus = BonusBrush()
        self.bonus.buttn.clicked.connect(self.new_brush)

    def refresh_image(self):
        """Image Refresher

        Sets the images displayed by the Image viewer to the current data slices.
        It will only show all the labels if the self.see_all_labels parameters is True.
        """
        #Creating slice copy
        slice = self.brain.current_data_slice
        new_slice = np.clip(
            np.log2(1 + slice.astype(float)) * self.brain.intensity, 0,
            self.brain.scale)
        self.img.setImage(new_slice, levels=(0., 1.))

        self.over_img.setImage(self.brain.current_label_data_slice,
                               autoLevels=False)
        if self.see_all_labels:
            self.mid_img.setImage(self.brain.current_other_labels_data_slice,
                                  autoLevels=False)
        else:
            self.mid_img.setImage(np.zeros(
                self.brain.current_other_labels_data_slice.shape),
                                  autoLevels=False)

    def recenter(self):
        """Brain Recenter 

        Recenter the brain into the middle of the image viewer.
        The implementation may seem weird, but this is a predefined action by PyQt5.
        """
        self.view.menu.actions()[0].trigger()

    def update_colormap(self):
        """Label Colormap Update

        Updates the colormap to account for the current number of distinct labels.
        There are only 12 distinct colours (not including the "invisible" colour)
        """
        num = len(self.brain.different_labels) + 1
        self.mid_img.setLookupTable(
            np.array([[0, 0, 0]] +
                     int(num / len(self.colours) + 1) * self.colours)[:num])
        self.mid_img.setLevels([0, np.max(self.brain.different_labels)])

    def enable_drawing(self):
        """Activates drawing mode

        The default pen is a voxel in size.
        """
        self.over_img.setDrawKernel(dot, mask=dot, center=(0, 0), mode='add')
        self.view.drawing = True

    def disable_drawing(self):
        """Deactivates drawing mode

        It does this by deactivating the drawing kernel and setting the value of the drawing parameter in the modified view box to False.
        """
        if self.view.drawing:
            self.over_img.drawKernel = None
            self.view.drawing = False
            self.view.state["mouseMode"] = 3
        else:
            self.enable_drawing()

    def edit_button1(self):
        """Sets the drawing mode to DOT

        This is basically a square of one voxel in size with value one.
        For all the editing buttons the matrix used to edit is defined at the top of the file
        """
        self.view.drawing = True
        self.over_img.setDrawKernel(dot, mask=dot, center=(0, 0), mode='add')

    def edit_button2(self):
        """Sets the drawing mode to RUBBER

        This is basically a square of one voxel in size with value one.
        For all the editing buttons the matrix used to edit is defined at the top of the file
        Removes the label from voxels.
        """
        self.view.drawing = True
        self.over_img.setDrawKernel(rubber,
                                    mask=rubber,
                                    center=(0, 0),
                                    mode='add')

    def edit_button3(self):
        """Sets the drawing mode to BRUSH (or cross)

        This sets the paintbrush to a cross of 3x3 voxels in size.
        For all the editing buttons the matrix used to edit is defined at the top of the file
        """
        self.view.drawing = True
        self.over_img.setDrawKernel(cross,
                                    mask=cross,
                                    center=(1, 1),
                                    mode='add')

    def bonus_brush(self):
        """Allows the user to design their own brush

        This opens a window in which the user can design a brush by using the pen and rubber.
        """
        self.enable_drawing()
        self.bonus.setVisible(True)

    def new_brush(self):
        """Sets the drawing mode to the user designed BRUSH (or bonus_brush)

        This sets the paintbrush to a user defined brush.
        """
        self.enable_drawing()
        cent = len(self.bonus.pen) // 2
        self.over_img.setDrawKernel(self.bonus.pen,
                                    mask=self.bonus.pen,
                                    center=(cent, cent),
                                    mode='add')

    def select_label(self):
        """Select label of interest

        Allows the user to select the location of the label to be edited next.
        Will only have effect if there are multiple labels from which to select
        The bulk of the implementation for this method is in the modified mouseReleasEevent method
        """
        if self.brain.multiple_labels:
            self.over_img.drawKernel = None
            self.select_mode = True

    def view_back_labels(self):
        """Toggle all/single label.

        Switch that determines whether all segmented areas are visible or just one.
        If see_all_labels was False, it makes all labels visible.
        If it was True it makes all labels except the one the user is currently editing invisible.
        """
        self.see_all_labels = not self.see_all_labels
        self.refresh_image()

    def next_label(self):
        """Label forward scroll

        Brings the next label in the list to be edited
        Lets you iterate through all existing labels
        """
        if self.brain.multiple_labels:
            new_index = np.where(self.brain.different_labels ==
                                 self.brain.current_label)[0][0] + 1
            if new_index < len(self.brain.different_labels):
                self.brain.current_label = self.brain.different_labels[
                    new_index]
            else:
                self.brain.current_label = self.brain.different_labels[1]
            self.refresh_image()
            self.dropbox.update_box()

    def previous_label(self):
        """Label backward scroll

        Brings the previous label in the list to be edited
        Lets you iterate through all existing labels
        """
        if self.brain.multiple_labels:
            old_index = np.where(
                self.brain.different_labels == self.brain.current_label)[0][0]
            if old_index != 1:
                self.brain.current_label = self.brain.different_labels[
                    old_index - 1]
            else:
                self.brain.current_label = self.brain.different_labels[-1]
            self.refresh_image()
            self.dropbox.update_box()

    def undo_previous_edit(self):
        """Undo function

        This function reverts the previous user action and refreshes the image.
        """
        current = self.brain.current_edit
        if current > 1:
            self.brain.label_data = self.brain.edit_history[current - 2][0]
            self.brain.other_labels_data = self.brain.edit_history[current -
                                                                   2][1]
            self.brain.current_edit = self.brain.current_edit - 1
            self.refresh_image()

    def redo_previous_edit(self):
        """Redo function

        This function re-does a previously reverted actions.
        """

        current = self.brain.current_edit
        if current < len(self.brain.edit_history):
            self.brain.label_data = self.brain.edit_history[current][0]
            self.brain.other_labels_data = self.brain.edit_history[current][1]
            self.brain.current_edit = self.brain.current_edit + 1
            self.refresh_image()

    def mouseReleaseEvent(self, ev):
        """Mouse event tracker

        This function keeps track of the actions performed by the mouse, while taking the selcted mode into account.
        If when select_mode is activated, the left button is released on a previously labeled area, then the pen is set to that label. Otherwise, everything should work as normal (the default)
        Now when you release the left button it assumes an edit has been made and stores it into the BrainData.

        Args:
            ev: signal emitted when user releases a mouse button.
        """
        if self.select_mode:
            if ev.button() == Qt.LeftButton:
                pos = ev.pos()
                mouse_x = int(self.img.mapFromScene(pos).x())
                mouse_y = int(self.img.mapFromScene(pos).y())
                location = self.brain.position_as_voxel(mouse_x, mouse_y)
                within = 0 < location[0] < self.brain.shape[0] and 0 < location[1] < self.brain.shape[1] and 0 < \
                    location[2] < self.brain.shape[2]
                if within:
                    label = self.brain.other_labels_data[location]
                    if label > 0:
                        self.brain.current_label = self.brain.other_labels_data[
                            location]
                        self.select_mode = False
                        self.refresh_image()
                        self.enable_drawing()
                        self.dropbox.update_box()
        super(ImageViewer, self).mouseReleaseEvent(ev)
        if self.view.drawing and ev.button() == Qt.LeftButton:
            self.brain.store_edit()

    def wheelEvent(self, ev):
        """ Overwriting the wheel functionality.

        If you scroll it will move along slices.
        If you scroll while holding the Ctrl button, it will zoom in and out

        Args:
            ev: signal emitted when user releases scrolls the wheel.
        """

        if ev.modifiers() == Qt.ControlModifier:
            super(ImageViewer, self).wheelEvent(ev)
        else:
            if ev.angleDelta().y(
            ) > 0 and self.brain.i < self.brain.shape[self.brain.section] - 1:
                self.brain.i = self.brain.i + 1
                self.refresh_image()
            elif ev.angleDelta().y() < 0 < self.brain.i:
                self.brain.i = self.brain.i - 1
                self.refresh_image()