Пример #1
0
class ConditionDialog(QDialog):
    """
    Create a new binary column on a set of localizations by 
    drawing a threshold on a 1D histogram of some attribute 
    for those localizations.

    For example, threshold only spots with low intensity (I0)
    or something.

    init
    ----
        locs        :   pandas.DataFrame
        parent      :   root QWidget

    """
    def __init__(self, locs, parent=None):
        super(ConditionDialog, self).__init__(parent=parent)
        self.locs = locs 
        self.initUI()

    def initUI(self):
        """
        Initialize user interface.

        """
        L = QGridLayout(self)
        self.resize(500, 300)

        widget_align = Qt.AlignLeft

        # Available columns for recondition: all numeric columns
        self.columns = list(filter(
            lambda c: self.locs[c].dtype in ['float64', 'float32', \
                'uint8', 'uint16', 'int64'],
            self.locs.columns))

        # For the default, choose `I0` if available; otherwise
        # choose the first column
        init_col = 'I0' if ('I0' in self.columns) else self.columns[0]
        self.load_col(init_col)

        # Main plot
        self.PlotWidget = PlotWidget(name="Create Boolean attribute")
        L.addWidget(self.PlotWidget, 0, 0, alignment=widget_align)

        # Histogram
        self.curve = self.PlotWidget.plot(self.bin_c, self.H, clickable=True)

        # User threshold, as a LinearRegionItem from pyqtgraph
        self.LinearRegion = LinearRegionItem([self.hmin, (self.hmax-self.hmin)*0.25+self.hmin])
        self.PlotWidget.addItem(self.LinearRegion)

        # Drop-down menu to select the column
        self.M_select_col = LabeledQComboBox(self.columns, "Attribute",
            init_value=init_col, parent=self)
        self.M_select_col.assign_callback(self.M_select_col_callback)
        L.addWidget(self.M_select_col, 1, 0, alignment=widget_align)

        # Accept the current threshold
        self.B_accept = QPushButton("Accept", parent=self)
        L.addWidget(self.B_accept, 2, 0, alignment=widget_align)
        self.B_accept.clicked.connect(self.B_accept_callback)

        self.update_histogram()

    def load_col(self, col):
        """
        Get data from a specific column in the locs dataframe.

        args
        ----
            col     :   str

        """
        self.data = np.asarray(self.locs[col])

        # Histogram limits
        self.hmin = self.data.min()
        self.hmax = np.percentile(self.data, 99.9)

        # Binning scheme
        n_bins = 5000
        bin_size = (self.hmax - self.hmin) / n_bins
        self.bin_edges = np.arange(self.hmin, self.hmax, bin_size)

        # Bin the data according to the binning scheme
        self.H, _edges = np.histogram(self.data, bins=self.bin_edges)
        self.bin_c = self.bin_edges[:-1] + (self.bin_edges[1]-self.bin_edges[0])/2.0

    def update_histogram(self):
        """
        Update the main histogram with data from a new column

        """
        self.PlotWidget.clear()
        self.curve = self.PlotWidget.plot(self.bin_c, self.H)
        self.curve.setPen('w')

        # Set default values for linear rect region
        self.LinearRegion.setRegion((self.hmin, np.percentile(self.data, 50)))
        self.PlotWidget.addItem(self.LinearRegion)
        self.curve.updateItems()

    def M_select_col_callback(self):
        """
        Select the current attribute to filter on.

        """
        col = self.M_select_col.currentText()
        self.load_col(col)
        self.update_histogram()

    def B_accept_callback(self):
        """
        Set the return value and exit from the dialog.

        """
        self.return_val = (
            self.LinearRegion.getRegion(),
            self.M_select_col.currentText(),
        )
        self.accept()
Пример #2
0
class FindPeak(QWidget):
    def __init__(self, parent, x_data, y_data):
        super(FindPeak, self).__init__()
        self.parent = parent
        self.x = x_data
        self.y = y_data
        self.lower = np.min(x_data)
        self.upper = np.max(x_data)
        self.range = self.upper - self.lower
        self.renderWindow()
        self.initPlotView()
        self.drawCurve()
        self.setUpProcessUI()
        self.bindEvents()
        self.integral(x_data, y_data, self.lower, self.upper)

    def bindEvents(self):
        self.bindBoundEvent()
        self.bindAlgorithmEvent()
        self.bindFindEvent()

    def bindBoundEvent(self):
        def leftBoundEvent(x):
            self.lower = x
            upper = self.upper
            self.plotRegion.setRegion([x, upper])
            self.rightBound.setMinimum(x)
            self.peakCenter.setMinimum(x)
            self.peakCenter.setValue((x + upper) / 2)
            self.integral(self.x, self.y, x, upper)

        def rightBoundEvent(x):
            self.upper = x
            lower = self.lower
            self.plotRegion.setRegion([lower, x])
            self.leftBound.setMaximum(x)
            self.peakCenter.setMaximum(x)
            self.peakCenter.setValue((x + lower) / 2)
            self.integral(self.x, self.y, lower, x)

        def regionChangeEvent():
            lower, upper = self.plotRegion.getRegion()
            self.lower = lower
            self.upper = upper
            self.leftBound.setValue(lower)
            self.leftBound.setMaximum(upper)
            self.rightBound.setValue(upper)
            self.rightBound.setMinimum(lower)
            self.peakCenter.setMinimum(lower)
            self.peakCenter.setMaximum(upper)
            self.peakCenter.setValue((lower + upper) / 2)
            self.integral(self.x, self.y, lower, upper)

        self.leftBound.valueChanged.connect(leftBoundEvent)
        self.rightBound.valueChanged.connect(rightBoundEvent)
        self.plotRegion.sigRegionChanged.connect(regionChangeEvent)

    def bindAlgorithmEvent(self):
        def updateInput(a, b, c, d, e, f):
            self.peakWidth.setEnabled(a)
            self.detectDis.setEnabled(b)
            self.noisePrt.setEnabled(c)
            self.amplitude.setEnabled(d)
            self.threshold.setEnabled(e)
            self.findBtn.setEnabled(f)

        def changeAlgorithm(algorithm):
            if algorithm == "Extremum":
                updateInput(False, False, False, False, False, True)
                pass
            elif algorithm == "Matlab Like":
                updateInput(True, True, False, True, True, True)
                pass
            elif algorithm == "Gaussian":
                updateInput(False, False, False, False, False, False)
                pass
            elif algorithm == "Lorentzian":
                updateInput(False, False, False, False, False, False)
                pass
            elif algorithm == "Pseudo-Voigt":
                updateInput(False, False, False, False, False, False)
                pass
            elif algorithm == "Wavelet Transform":
                updateInput(True, True, True, False, False, False)
                pass

        self.algorithm.currentTextChanged.connect(changeAlgorithm)
        updateInput(False, False, False, False, False, True)

    def integral(self, x_data, y_data, lower, upper):
        idx = np.where((x_data >= lower) & (x_data <= upper))
        x = x_data[idx]
        y = y_data[idx]
        self.integralArea.setValue(simps(y, x))

    def bindFindEvent(self):
        x_data = self.x
        y_data = self.y

        def findPeak():
            region = np.where((x_data >= self.lower) & (x_data <= self.upper))
            sub_data = y_data[region]
            sub_region = x_data[region]
            algorithm = self.algorithm.currentText()
            shape = self.shape.currentText()
            if shape == "Peak":
                const = 1
            else:
                const = -1
            sub_data = sub_data * const
            if algorithm == "Extremum":
                peak = np.max(sub_data)
                idx = np.where(sub_data == peak)
                x = sub_region[idx][0]
                y = sub_data[idx][0] * const
                self.peakCenter.setValue(x)
                return self.renderPeakPoint([x, y])
            elif algorithm == "Matlab Like":
                indexes = find_peaks(
                    sub_data,
                    height=self.amplitude.value(),  #低于指定高度忽略
                    threshold=self.threshold.value(),  #相邻两点高度差
                    distance=self.detectDis.value(),  #两峰间距
                    width=self.peakWidth.value()  #峰宽
                )[0]
                if np.size(indexes) == 0:
                    return
                idx = np.where(sub_data == np.max(sub_data[indexes]))
                x = sub_region[idx][0]
                y = sub_data[idx][0] * const
                self.peakCenter.setValue(x)
                return self.renderPeakPoint([x, y])
            elif algorithm == "Wavelet Transform":
                indexes = find_peaks_cwt(
                    sub_data,
                    widths=self.peakWidth.value(),  #峰宽
                    max_distances=self.detectDis.value(),  #两峰间距
                    noise_perc=self.noisePrt.value())[0]
                if np.size(indexes) == 0:
                    return
                idx = np.where(sub_data == np.max(sub_data[indexes]))
                x = sub_region[idx][0]
                y = sub_data[idx][0] * const
                self.peakCenter.setValue(x)
                return self.renderPeakPoint([x, y])
                self.noisePrt
            pass

        self.findBtn.clicked.connect(findPeak)

    def renderPeakPoint(self, pos):
        self.peakPoint.clear()
        self.peakPoint.addPoints([{'pos': pos, 'data': 1}])

    def renderWindow(self):
        #边框结构
        self.setGeometry(80, 80, 800, 420)
        size = self.geometry()
        screen = QDesktopWidget().screenGeometry()
        posX = (screen.width() - size.width()) / 2
        posY = (screen.height() - size.height()) / 2
        self.move(posX, posY)
        #标题
        self.setWindowTitle('Find Peak')
        self.setWindowIcon(QIcon('resource/curve.ico'))
        #布局
        layout = QGridLayout()
        self.graphicsView = QGridLayout()
        layout.addLayout(self.graphicsView, 0, 0, 1, 1)

        self.Process_Box = QGroupBox()
        self.Process_Box.setMinimumSize(200, 420)
        self.Process_Box.setFlat(True)
        layout.addWidget(self.Process_Box, 0, 1, 1, 1)

        self.setLayout(layout)

    def setUpProcessUI(self):
        layout = QGridLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(10)
        self.Process_Box.setLayout(layout)

        layout.addWidget(QLabel(self.translate('Left Boundary')), 0, 0, 1, 1)
        layout.addWidget(QLabel(self.translate('Right Boundary')), 1, 0, 1, 1)
        layout.addWidget(QLabel(self.translate("Integral Area")), 2, 0, 1, 1)
        layout.addWidget(QLabel(self.translate('Peak Center')), 3, 0, 1, 1)
        layout.addWidget(QLabel(self.translate('Peak Shape')), 4, 0, 1, 1)
        layout.addWidget(QLabel(self.translate('Find Peak Algorithm')), 5, 0,
                         1, 1)
        layout.addWidget(QLabel(self.translate('Minimum Peak Width')), 6, 0, 1,
                         1)
        layout.addWidget(QLabel(self.translate('Minimum Detect Distance')), 7,
                         0, 1, 1)
        layout.addWidget(QLabel(self.translate('Noise Percent')), 8, 0, 1, 1)
        layout.addWidget(QLabel(self.translate("Minimum Amplitude")), 9, 0, 1,
                         1)
        layout.addWidget(QLabel(self.translate("Relative Threshold")), 10, 0,
                         1, 1)

        self.leftBound = SpinBox(lower=self.lower, dec=4, val=self.lower)
        self.rightBound = SpinBox(upper=self.upper, dec=4, val=self.upper)
        self.peakCenter = SpinBox(lower=self.lower, upper=self.upper, dec=4)
        self.peakWidth = SpinBox(lower=1, upper=10000, val=5)
        self.noisePrt = SpinBox(lower=0, upper=100, step=1, val=10)
        self.detectDis = SpinBox(lower=1, val=3)
        self.amplitude = SpinBox(lower=-1E5, upper=1E5, dec=4, val=-1)
        self.threshold = SpinBox(lower=0, upper=100, dec=4, val=0.001)
        self.integralArea = SpinBox(upper=1E8, dec=4)
        self.integralArea.setReadOnly(True)
        self.integralArea.setButtonSymbols(QAbstractSpinBox.NoButtons)

        self.shape = QComboBox()
        self.shape.addItems(["Peak", "Valley"])
        #self.shape.currentTextChanged.connect()

        self.algorithm = QComboBox()
        self.algorithm.addItems([
            'Extremum', 'Matlab Like', 'Wavelet Transform', 'Gaussian',
            'Lorentzian', 'Pseudo-Voigt'
        ])
        #self.algorithm.currentTextChanged.connect()
        #https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks_cwt.html
        layout.addWidget(self.leftBound, 0, 1, 1, 1)
        layout.addWidget(self.rightBound, 1, 1, 1, 1)
        layout.addWidget(self.integralArea, 2, 1, 1, 1)
        layout.addWidget(self.peakCenter, 3, 1, 1, 1)
        layout.addWidget(self.shape, 4, 1, 1, 1)
        layout.addWidget(self.algorithm, 5, 1, 1, 1)
        layout.addWidget(self.peakWidth, 6, 1, 1, 1)
        layout.addWidget(self.detectDis, 7, 1, 1, 1)
        layout.addWidget(self.noisePrt, 8, 1, 1, 1)
        layout.addWidget(self.amplitude, 9, 1, 1, 1)
        layout.addWidget(self.threshold, 10, 1, 1, 1)

        self.findBtn = QPushButton(self.translate('Find Peak'))
        layout.addWidget(self.findBtn, 11, 0, 1, 2)
        pass

    def initPlotView(self):
        self.plot = PlotWidget(enableAutoRange=True)
        self.plot.setXRange(self.lower - self.range * 0.05,
                            self.upper + self.range * 0.05)
        self.plotLegand = self.plot.addLegend()
        self.graphicsView.addWidget(self.plot)
        self.plotRegion = LinearRegionItem()
        self.plotRegion.setZValue(10)
        self.peakPoint = ScatterPlotItem(size=8,
                                         pen=mkPen(color='0000FF', width=2),
                                         symbol="+",
                                         brush=mkBrush(255, 255, 255, 240))
        self.plot.addItem(self.plotRegion, ignoreBounds=True)
        self.plot.addItem(self.peakPoint)
        self.setGraphViewStyle()

    def setGraphViewStyle(self):
        self.plot.setAutoVisible(y=True)
        self.plot.setBackground('#ffffff')
        self.plot.showGrid(x=True, y=True, alpha=0.25)
        self.plot.getAxis('bottom').setPen(color='#000000', width=1.5)
        self.plot.getAxis('left').setPen(color='#000000', width=1.5)
        self.plotRegion.setRegion([self.lower, self.upper])
        self.plotRegion.setBounds([self.lower, self.upper])

    def drawCurve(self):
        pen = mkPen(color='FF0000', width=2)
        self.plot.plot(self.x, self.y, pen=pen)
        self.plot.show()

    def translate(self, text):
        if self.parent:
            self.langText = self.parent.langText
        else:
            self.langText = load(open('SCN.translation', encoding='utf-8'))
        if text in self.langText:
            return self.langText[text]
        return text
Пример #3
0
class PlotBC(Smooth1DPlot):
    sigBackgroundChanged = pyqtSignal()

    def __init__(self, profile: BasicProfile, parent=None):
        super().__init__(profile, parent)
        self._status = BaseLineStatus.no_baseline
        self.baseline_plot = self.image_view.plot_item.plot()
        self._init_roi()
        self._baseline_setup_widget = BaseLineSetup(
            self, self._status, **self.profile.get_parameters())
        self.profile.sigDataUpdated.connect(self.update_plot)

    @property
    def y(self):
        if self._status == BaseLineStatus.baseline_subtracted and self.profile.baseline is not None:
            return self.profile.y - self.profile.baseline
        else:
            return self.profile.y

    def update_data(self, *args, **kwargs):
        self.profile.update_data(*args, **kwargs)

    def is_shown(self, shown: bool):
        self.profile.is_shown = shown

    def _init_toolbars(self):
        super()._init_toolbars()

        baseline_toolbar = BlackToolBar('Baseline Correction')
        self.addToolBar(baseline_toolbar)

        baseline_button = RoundedPushButton(parent=baseline_toolbar,
                                            icon=Icon('baseline'),
                                            radius=30)
        baseline_button.clicked.connect(self.open_baseline_setup)
        baseline_toolbar.addWidget(baseline_button)

    def _init_roi(self):
        self._roi = LinearRegionItem()
        self._roi.hide()
        self._roi.setBrush(QColor(255, 255, 255, 50))
        self.image_view.plot_item.addItem(self._roi)

    def open_baseline_setup(self):
        if self.y is None:
            return
        setup = self._baseline_setup_widget

        if self.profile.x_range is None:
            self._set_default_bounds()
        self._roi.setRegion(self.profile.x_range)

        if self.profile.baseline is None:
            self._set_status(BaseLineStatus.no_baseline)
        # elif self._status == BaseLineStatus.no_baseline:
        #     self._set_status(BaseLineStatus.baseline_subtracted)
        # else:
        #     self._set_status(self._status)

        # self.plot()

        setup.set_parameters(self.profile.get_parameters())

        setup.calculate_signal.connect(self._on_calculate_baseline)
        setup.subtract_signal.connect(self._on_subtracting_baseline)
        setup.restore_signal.connect(self._on_restoring_data)
        setup.close_signal.connect(self._on_closing_setup)
        setup.show()
        self._roi.show()

    # def show_baseline(self):
    #     if (self.profile.baseline is None or self._status == BaseLineStatus.baseline_calculated or
    #             self._status == BaseLineStatus.baseline_restored):
    #         return
    #     self._on_restoring_data()

    def update_plot(self):
        self.sigma_slider.set_value(self.profile.sigma, True)

        if self.profile.baseline is None:
            self.clear_baseline()
        else:
            self._set_status(BaseLineStatus.baseline_subtracted)
        self.plot()

    def plot_baseline(self):
        if self.profile.baseline is not None:
            self.baseline_plot.setData(self.profile.x,
                                       self.profile.baseline,
                                       pen=get_pen(width=4,
                                                   color='red',
                                                   style=Qt.DashDotLine))

    def _set_default_bounds(self):
        if self.x is None:
            self.profile.x_range = (0, 1)
        else:
            self.profile.x_range = (self.x.min(), self.x.max())

    def _update_bounds(self):
        self.profile.x_range = self._roi.getRegion()

    def _set_status(self, status: 'BaseLineStatus'):
        self._status = status
        self._baseline_setup_widget.set_status(status)

    def _on_calculate_baseline(self, params: dict):
        self.profile.set_parameters(**params)
        self._update_bounds()
        try:
            self.profile.update_baseline()
        except Exception as err:
            logger.exception(err)
            show_error(
                'Failed calculating baseline. Change roi region or parameters and try again.',
                error_title='Baseline calculation error')
            return
        self._set_status(BaseLineStatus.baseline_calculated)
        self.plot_baseline()

    def _on_subtracting_baseline(self):
        self.baseline_plot.clear()
        self._set_status(BaseLineStatus.baseline_subtracted)
        self.plot()

    def _on_restoring_data(self):
        self._set_status(BaseLineStatus.baseline_restored)
        self.plot_baseline()
        self.plot()

    def _on_closing_setup(self):
        self._baseline_setup_widget.hide()
        self._roi.hide()
        self.clear_baseline()
        if self._status != BaseLineStatus.baseline_subtracted:
            self._set_status(BaseLineStatus.no_baseline)
            self.profile.clear_baseline(clear_range=False)
        self.sigBackgroundChanged.emit()

    def clear_baseline(self):
        self.baseline_plot.clear()
Пример #4
0
class GraphicWidgetLogic(Ui_GraphicWindow):
    def __init__(self, GraphicWidgetLogic):
        Ui_GraphicWindow.__init__(self)
        self.FS = 48000
        self.x = 0
        self.y = 0
        self.freq = 0
        self.amp = 0
        self.flag = "PURE"

    # se inicializa el sistema de visualizado avanzado
    def initializeBinds(self):
        # añadir plots

        self.pushButtonPlay.clicked.connect(self.play)
        self.pushButton.clicked.connect(self.showFFT)
        setConfigOption('leftButtonPan', False)
        self.x = 0
        self.y = 0

        self.zoomedPlot = self.graphicsView.addPlot(row=1, col=0)
        self.fullPlot = self.graphicsView.addPlot(row=2, col=0)

        self.graphicsView.setBackground(background="w")
        # self.zoomedPlot.vb.setBackgroundColor("w")
        # self.fullPlot.vb.setBackgroundColor("w")
        self.penB = mkPen('b')
        self.penR = mkPen('r')
        self.region = LinearRegionItem()
        self.region.setZValue(10)

        self.vb = self.zoomedPlot.vb
        self.region.setRegion([1000, 2000])

        self.fullPlot.addItem(self.region, ignoreBounds=True)
        # pg.dbg()
        self.zoomedPlot.setAutoVisible(y=True)

        self.vLine = InfiniteLine(angle=90, movable=False)
        self.hLine = InfiniteLine(angle=0, movable=False)
        self.zoomedPlot.addItem(self.vLine, ignoreBounds=True)
        self.zoomedPlot.addItem(self.hLine, ignoreBounds=True)

        # signal para capturar evento de raton
        # proxy = SignalProxy(self.zoomedPlot.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved)

        self.zoomedPlot.scene().sigMouseMoved.connect(self.mouseMoved)
        self.region.sigRegionChanged.connect(self.update)

        self.zoomedPlot.sigRangeChanged.connect(self.updateRegion)

    def play(self):
        sd.play(self.y, self.FS, blocking=True)

    def showFFT(self):
        self.FFTwindow = QtWidgets.QWidget()
        self.ui = GraphicWidgetLogicSpectrumLogic(self)
        self.ui.setupUi(self.FFTwindow)
        self.ui.initializeBinds()

        self.ui.PlotFFT(self.x, self.y, self.flag, self.amp)

        self.FFTwindow.activateWindow()
        self.FFTwindow.show()
        self.FFTwindow.raise_()

    def updateRegion(self, window, viewRange):
        rgn = viewRange[0]
        self.region.setRegion(rgn)

    def update(self):
        self.region.setZValue(10)
        minX, maxX = self.region.getRegion()
        self.zoomedPlot.setXRange(minX, maxX, padding=0)

    def mouseMoved(self, evt):
        pos = Point(evt.x(), evt.y())
        if self.zoomedPlot.sceneBoundingRect().contains(pos):
            mousePoint = self.vb.mapSceneToView(pos)
            index = float(evt.x())
            #   if index > 0 :
            # self.dataLabel.setText("<span style='font-size: 12pt'>x=%0.1f,   <span style='color: red'>y1=%0.1f</span>,   <span style='color: green'>y2=%0.1f</span>" % int(index), self.x[index], self.y[index])
            self.vLine.setPos(mousePoint.x())
            self.hLine.setPos(mousePoint.y())

    def PlotSin(self, amp, freq, phase, flag="PURE"):
        print(self.flag)
        self.flag = flag
        self.amp = amp
        self.freq = freq

        self.x = arange(0, 1, 1 / self.FS)
        self.y = (amp / 10) * sin(2 * pi * freq * self.x + (phase * pi))

        self.fullPlot.plot(self.x, self.y, pen=self.penR)
        self.zoomedPlot.plot(self.x, self.y, pen=self.penB)

    def plotSawtooth(self, amp, freq):
        self.amp = amp
        self.freq = freq
        self.Fs: int = 44100
        self.x = arange(0, 1, 1 / self.Fs)
        self.y = (amp / 10) * sawtooth(2 * pi * freq * self.x)
        self.fullPlot.plot(self.x, self.y, pen=self.penR)
        self.zoomedPlot.plot(self.x, self.y, pen=self.penB)

    def plotSquare(self, amp, freq):
        self.amp = amp
        self.freq = freq
        self.Fs: int = 44100
        self.x = arange(0, 1, 1 / self.Fs)
        self.y = (amp / 10) * square(2 * pi * freq * self.x)
        self.fullPlot.plot(self.x, self.y, pen=self.penR)
        self.zoomedPlot.plot(self.x, self.y, pen=self.penB)

    def plotGpulse(self, amp, freq):
        self.amp = amp
        self.freq = freq
        self.Fs: int = 44100
        self.x = arange(0, 1, 1 / self.Fs)
        self.y = (amp / 10) * gausspulse((self.x, freq))
        self.fullPlot.plot(self.x, self.y, pen=self.penR)
        self.zoomedPlot.plot(self.x, self.y, pen=self.penB)

    def plotHarmonic(self, x_array, y_array):
        self.x = x_array
        self.y = y_array
        self.fullPlot.plot(x_array, y_array, pen=self.penR)
        self.zoomedPlot.plot(x_array, y_array, pen=self.penB)
Пример #5
0
class HistogramItem(GraphicsWidget):
    """
    This is a graphicsWidget which provides controls for adjusting the display of an image.

    Includes:

    - Image histogram 
    - Movable region over histogram to select black/white levels

    Parameters
    ----------
    image : ImageItem or None
        If *image* is provided, then the control will be automatically linked to
        the image and changes to the control will be immediately reflected in
        the image's appearance.
    fillHistogram : bool
        By default, the histogram is rendered with a fill.
        For performance, set *fillHistogram* = False.
    """

    sigLevelsChanged = pyqtSignal(object)
    sigLevelChangeFinished = pyqtSignal(object)

    def __init__(self, image=None, fillHistogram=True, bounds: tuple = None):
        GraphicsWidget.__init__(self)
        self.imageItem = lambda: None  # fake a dead weakref

        self.layout = QGraphicsGridLayout()
        self.setLayout(self.layout)
        self.layout.setContentsMargins(1, 1, 1, 1)
        self.layout.setSpacing(0)
        self.vb = ViewBox(parent=self)
        # self.vb.setMaximumHeight(152)
        # self.vb.setMinimumWidth(45)
        self.vb.setMouseEnabled(x=True, y=False)

        self.region = LinearRegionItem([0, 1], 'vertical', swapMode='block', bounds=bounds)
        self.region.setZValue(1000)
        self.vb.addItem(self.region)
        self.region.lines[0].addMarker('<|', 0.5)
        self.region.lines[1].addMarker('|>', 0.5)
        self.region.sigRegionChanged.connect(self.regionChanging)
        self.region.sigRegionChangeFinished.connect(self.regionChanged)

        self.axis = AxisItem('bottom', linkView=self.vb, maxTickLength=-10, parent=self)
        self.layout.addItem(self.axis, 1, 0)
        self.layout.addItem(self.vb, 0, 0)
        self.range = None
        self.vb.sigRangeChanged.connect(self.viewRangeChanged)

        self.plot = PlotCurveItem(pen=(200, 200, 200, 100))
        # self.plot.rotate(90)
        self.vb.addItem(self.plot)

        self.fillHistogram(fillHistogram)
        self._showRegions()

        self.autoHistogramRange()

        if image is not None:
            self.setImageItem(image)

    def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
        if fill:
            self.plot.setFillLevel(level)
            self.plot.setBrush(color)
        else:
            self.plot.setFillLevel(None)


    def paint(self, p, *args):
        rgn = self.getLevels()
        self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
        self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))


    def setHistogramRange(self, mn, mx, padding=0.1):
        """Set the Y range on the histogram plot. This disables auto-scaling."""
        self.vb.enableAutoRange(self.vb.XAxis, False)
        self.vb.setYRange(mn, mx, padding)

    def autoHistogramRange(self):
        """Enable auto-scaling on the histogram plot."""
        self.vb.enableAutoRange(self.vb.XYAxes)

    def setImageItem(self, img):
        """Set an ImageItem to have its levels and LUT automatically controlled
        by this HistogramLUTItem.
        """
        self.imageItem = weakref.ref(img)
        img.sigImageChanged.connect(self.imageChanged)
        self.regionChanged()
        self.imageChanged(autoLevel=True)

    def viewRangeChanged(self):
        self.update()

    def regionChanged(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelChangeFinished.emit(self)

    def regionChanging(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelsChanged.emit(self)
        self.update()

    def imageChanged(self, autoLevel=False):
        if self.imageItem() is None:
            return

        self.plot.setVisible(True)
        # plot one histogram for all image data
        h = self.imageItem().getHistogram()
        if h[0] is None:
            return
        self.plot.setData(*h)
        if autoLevel:
            mn = h[0][0]
            mx = h[0][-1]
            self.region.setRegion([mn, mx])
        else:
            mn, mx = self.imageItem().levels
            self.region.setRegion([mn, mx])

    def getLevels(self):
        """
        Return the min and max levels.
        """
        return self.region.getRegion()

    def setLevels(self, min=None, max=None):
        """
        Set the min/max (bright and dark) levels.
        """
        assert None not in (min, max)
        self.region.setRegion((min, max))

    def _showRegions(self):
        self.region.setVisible(True)

    def saveState(self):
        return {
            'levels': self.getLevels(),
        }

    def restoreState(self, state):
        self.setLevels(*state['levels'])