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

        Parameters
        ----------
        parent : None, optional
            Top-level widget.
        """
        super().__init__(parent)
        self.setupUi(self)

        p1 = self.scatterPlot.addPlot()
        self.scatterPlotItem = ScatterPlotItem()
        p1.addItem(self.scatterPlotItem)
        p1.setLabel('left', 'Y', units='pixel')
        p1.setLabel('bottom', 'X', units='pixel')

        self.yHistogramItem = None
        self.xHistogramItem = None
        self.numBins = 40

        self.dataSize = None
        self.xData = None
        self.yData = None
        self.rollArray = False
        self.dataCounter = 0
        self.brushes = None
        self.brushColor = (159, 159, 159)
        self.penColor = (255, 255, 255)
        self.maxAlpha = 255
        self.minAlpha = 127
        self.histogramFillBrush = mkBrush(*self.brushColor, 200)
        self.pointBrush = mkBrush(*self.brushColor, self.maxAlpha)
        self.pointPen = mkPen(*self.penColor)
        self.scatterPlotItem.setBrush(self.pointBrush)
예제 #2
0
    def __init__(self, *args, **kwargs):
        self.centerplot = ScatterPlotItem(brush="r")
        self.centerplot.setZValue(100)

        super(CenterMarker, self).__init__(*args, **kwargs)

        self.addItem(self.centerplot)
        self.drawCenter()
예제 #3
0
 def scatter(self,
             x: np.ndarray,
             y: np.ndarray,
             label: Optional[str] = None,
             symbol: Optional[list] = None,
             **kwargs) -> None:
     kwargs = Axes._add_label_to_kwargs(label, kwargs)
     item = ScatterPlotItem(**kwargs)
     item.setData(x=x, y=y)
     self._raw.addItem(item)
예제 #4
0
파일: item.py 프로젝트: asciili/vnpy
    def __init__(self, manager: BarManager, am: ArrayManager):
        """"""
        ScatterPlotItem.__init__(self)
        # ExChartItem.__init__(self,manager)
        # super(TradeItem,self).__init__(manager, am)
        super(ExChartItem, self).__init__(manager, am)

        self.blue_pen: QtGui.QPen = pg.mkPen(color=(100, 100, 255), width=2)

        self.trades: Dict[int, Dict[str,
                                    TradeData]] = {}  # {ix:{tradeid:trade}}
예제 #5
0
class ScatterPlotterNode(Node):
    nodeName = 'ScatterPlotter'
    sigUpdatePlot = QtCore.Signal(object)

    def __init__(self, name):
        Node.__init__(self,
                      name,
                      terminals={
                          'x': {
                              'io': 'in'
                          },
                          'y': {
                              'io': 'in'
                          },
                          'color': {
                              'io': 'in'
                          },
                          'plotItem': {
                              'io': 'out'
                          }
                      })
        self.ScatterPlotItem = ScatterPlotItem(pen=None,
                                               symbol='o',
                                               size=2,
                                               brush=(255, 255, 255, 160))
        self.sigUpdatePlot.connect(self.updatePlot)

    def updatePlot(self, xyb):
        x, y, brs = xyb
        self.ScatterPlotItem.setData(x, y)
        if not brs is None:
            self.ScatterPlotItem.setBrush(brs)

    def process(self, x, y, color, display=True):
        if x is None or y is None:
            raise Exception('set proper inputs')
        brs = None
        if not color is None:
            brs = [
                fn.mkBrush(tuple(c)) for c in (color * 255).astype(np.int32)
            ]
        self.sigUpdatePlot.emit((x, y, brs))
        return {'plotItem': self.ScatterPlotItem}
예제 #6
0
 def plotCurveAndFit(self, xData, yData):
     # noinspection PyTypeChecker
     self.plotAttributes["curve"] = ScatterPlotItem(xData,
                                                    yData,
                                                    pen=1,
                                                    symbol='x',
                                                    size=5)
     self.plot.addItem(self.plotAttributes["curve"])
     self.plotFit(xData, yData,
                  self.devices["B"] + ' vs. ' + self.devices["A"])
예제 #7
0
class CenterMarker(QSpace):
    def __init__(self, *args, **kwargs):
        self.centerplot = ScatterPlotItem(brush="r")
        self.centerplot.setZValue(100)

        super(CenterMarker, self).__init__(*args, **kwargs)

        self.addItem(self.centerplot)
        self.drawCenter()

    def drawCenter(self):
        try:
            fit2d = self._geometry.getFit2D()
        except (TypeError, AttributeError):
            pass
        else:
            if self.imageItem.image is not None:
                if self.displaymode == DisplayMode.raw:
                    x = fit2d['centerX']
                    y = self._raw_image.shape[-2] - fit2d['centerY']
                    self.centerplot.setData(x=[x], y=[y])
                elif self.displaymode == DisplayMode.remesh:
                    self.centerplot.setData(x=[0], y=[0])

    def setGeometry(self, geometry):
        super(CenterMarker, self).setGeometry(geometry)
        self.drawCenter()
예제 #8
0
 def __init__(self, name):
     Node.__init__(self,
                   name,
                   terminals={
                       'x': {
                           'io': 'in'
                       },
                       'y': {
                           'io': 'in'
                       },
                       'color': {
                           'io': 'in'
                       },
                       'plotItem': {
                           'io': 'out'
                       }
                   })
     self.ScatterPlotItem = ScatterPlotItem(pen=None,
                                            symbol='o',
                                            size=2,
                                            brush=(255, 255, 255, 160))
     self.sigUpdatePlot.connect(self.updatePlot)
예제 #9
0
class CenterMarker(QSpace):
    def __init__(self, *args, **kwargs):
        self.centerplot = ScatterPlotItem(brush="r")
        self.centerplot.setZValue(100)

        super(CenterMarker, self).__init__(*args, **kwargs)

        self.addItem(self.centerplot)
        self.drawCenter()

    def drawCenter(self):
        try:
            fit2d = self._geometry.getFit2D()
        except (TypeError, AttributeError):
            pass
        else:
            x = fit2d['centerX']
            y = fit2d['centerY']
            self.centerplot.setData(x=[x], y=[y])

    def setGeometry(self, geometry):
        super(CenterMarker, self).setGeometry(geometry)
        self.drawCenter()
예제 #10
0
 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()
예제 #11
0
    def __init__(
        self,
        date_format="%Y-%m-%d %H:%M:%S",
        y_format="%0.4f",
        trigger_point_size=10,
    ):
        super(DataInspectorLine, self).__init__(angle=90, movable=True)
        self._labels = []
        self._plot_item = None

        self.y_format = y_format
        self.trigger_point_size = trigger_point_size
        self.date_format = date_format
        self._label_style = "background-color: #35393C;"
        self.sigPositionChanged.connect(self._inspect)
        self._highlights = ScatterPlotItem(
            pos=(),
            symbol="s",
            brush="35393C88",
            pxMode=True,
            size=trigger_point_size,
        )
        # hack to make the CurvesPropertiesTool ignore the highlight points
        self._highlights._UImodifiable = False
예제 #12
0
파일: masker.py 프로젝트: alecheckert/quot
class Masker(QDialog):
    """
    An interface for the user to draw masks on an image or movie.
    This includes:

        - draw masks on the raw image, either by placing discrete vertices
            or freestyle drawing
        - modify masks by adding / deleting / moving vertices
        - change between frames
        - export masks
        - apply masks to image files

    init
    ----
        image_path           :   str, path to an image file (e.g. ND2 or TIF)
        max_points_freestyle :  int, the maximum number of points to allow
                                for a freestyle mask. This limits memory
                                usage.
        parent               :  root QWidget 


    """
    def __init__(self,
                 image_path,
                 max_points_freestyle=40,
                 dialog_mode=False,
                 parent=None):

        super(Masker, self).__init__(parent=parent)
        self.image_path = image_path
        self.max_points_freestyle = max_points_freestyle
        self.dialog_mode = dialog_mode
        self.initData()
        self.initUI()

    def initData(self):
        """
        Load the images and related data.

        """
        # Create the image reader
        self.imageReader = ImageReader(self.image_path)

        # Get the first image
        self.image = self.imageReader.get_frame(0)

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

        """
        # Main layout
        L_master = QGridLayout(self)

        # An ImageView on the left to contain the subject of masking
        self.imageView = ImageView(parent=self)
        L_master.addWidget(self.imageView, 0, 0, 15, 2)
        self.imageView.setImage(self.image)

        # Override the default ImageView.imageItem.mouseClickEvent
        self.imageView.imageItem.mouseClickEvent = self._imageView_mouseClickEvent

        # Override the default ImageView.imageItem.mouseDoubleClickEvent
        self.imageView.imageItem.mouseDoubleClickEvent = \
            self._imageView_mouseDoubleClickEvent

        # All currently clicked points
        self.points = []

        # All current PolyLineROI objects
        self.polyLineROIs = []

        # ScatterPlotItem, to show current accumulated clicks
        self.scatterPlotItem = ScatterPlotItem()
        self.scatterPlotItem.setParentItem(self.imageView.imageItem)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Frame slider
        self.frame_slider = IntSlider(minimum=0,
                                      maximum=self.imageReader.n_frames - 1,
                                      interval=1,
                                      init_value=0,
                                      name='Frame',
                                      parent=self)
        L_master.addWidget(self.frame_slider,
                           0,
                           2,
                           1,
                           1,
                           alignment=widget_align)
        self.frame_slider.assign_callback(self.frame_slider_callback)

        # Button: create new ROI
        self.create_roi_mode = False
        self.B_create_ROI = QPushButton("Draw ROI", parent=self)
        self.B_create_ROI.clicked.connect(self.B_create_ROI_callback)
        L_master.addWidget(self.B_create_ROI,
                           10,
                           2,
                           1,
                           1,
                           alignment=widget_align)

        # Button: freestyle drawing to make ROI
        self.freestyle_mode = False
        self.B_freestyle = QPushButton("Freestyle", parent=self)
        self.B_freestyle.clicked.connect(self.B_freestyle_callback)
        L_master.addWidget(self.B_freestyle,
                           11,
                           2,
                           1,
                           1,
                           alignment=widget_align)

        # Pure dialog mode: no options to save or load masks, only
        # define and accept
        if self.dialog_mode:

            # Button: accept the current set of masks. This is only meaningful
            # when this class is being used as a dialog by other classes.
            self.B_accept = QPushButton("Accept masks", parent=self)
            self.B_accept.clicked.connect(self.B_accept_callback)
            L_master.addWidget(self.B_accept,
                               12,
                               2,
                               1,
                               1,
                               alignment=widget_align)

        # Non-dialog mode: full range of options
        else:

            # Button: apply these masks to the localizations in a file
            self.B_apply = QPushButton("Apply masks", parent=self)
            self.B_apply.clicked.connect(self.B_apply_callback)
            L_master.addWidget(self.B_apply,
                               12,
                               2,
                               1,
                               1,
                               alignment=widget_align)

            # Button: save masks to a file
            self.B_save = QPushButton("Save masks", parent=self)
            self.B_save.clicked.connect(self.B_save_callback)
            L_master.addWidget(self.B_save,
                               13,
                               2,
                               1,
                               1,
                               alignment=widget_align)

            # Button: load preexisting masks from a file
            self.B_load = QPushButton("Load masks", parent=self)
            self.B_load.clicked.connect(self.B_load_callback)
            L_master.addWidget(self.B_load,
                               14,
                               2,
                               1,
                               1,
                               alignment=widget_align)

        # Resize and launch
        self.resize(800, 600)
        self.show()

    ## CORE FUNCTIONS

    def update_image(self,
                     frame_index=None,
                     autoRange=False,
                     autoLevels=False,
                     autoHistogramRange=False):
        if frame_index is None:
            frame_index = self.frame_slider.value()
        self.image = self.imageReader.get_frame(frame_index)
        self.imageView.setImage(self.image,
                                autoRange=autoRange,
                                autoLevels=autoLevels,
                                autoHistogramRange=autoHistogramRange)

    def update_scatter(self):
        """
        Write the contents of self.points to self.scatterPlotItem.

        """
        self.scatterPlotItem.setData(
            pos=np.asarray(self.points),
            pxMode=False,
            symbol='o',
            pen={
                'color': '#FFFFFF',
                'width': 3.0
            },
            size=2.0,
            brush=None,
        )

    def clear_scatter(self):
        self.points = []
        self.scatterPlotItem.setData()

    def createPolyLineROI(self, points):
        p = PolyLineROI(self.points, closed=True, removable=True)
        self.polyLineROIs.append(p)
        self.imageView.view.addItem(self.polyLineROIs[-1])

        # If the user requests to remove this ROI, remove it
        self.polyLineROIs[-1].sigRemoveRequested.connect(self._remove_ROI)

    def getPoints(self, polyLineROI):
        """
        Return the set of points that make up a PolyLineROI as a 
        2D ndarray (shape (n_points, 2)).

        """
        state = polyLineROI.getState()
        return np.asarray([[p.x(), p.y()] for p in state['points']])

    def get_currdir(self):
        """
        Get the last directory that was accessed by the user. If 
        no directory was previously accessed, then this is just 
        the same directory as the image file.

        """
        if not hasattr(self, "_currdir"):
            self.set_currdir(self.image_path)
        return self._currdir

    def set_currdir(self, path):
        """
        Set the directory returned by self.get_currdir().

        args
        ----
            path            :   str, a file path or directory path. If 
                                a file path, its parent directory is used.

        """
        if os.path.isfile(path):
            self._currdir = os.path.split(os.path.abspath(path))[0]
        elif os.path.isdir(path):
            self._currdir = path

    def clear_polyLineROIs(self):
        """
        Destroy all current PolyLineROI objects.

        """
        for p in self.polyLineROIs[::-1]:
            self._remove_ROI(p)

    ## SIGNAL CALLBACKS

    def _imageView_mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            if self.imageView.imageItem.raiseContextMenu(ev):
                ev.accept()
        if ev.button() == QtCore.Qt.LeftButton and self.create_roi_mode:
            self.points.append(ev.pos())
            self.update_scatter()

    def _imageView_mouseDoubleClickEvent(self, ev):
        if self.create_roi_mode:
            self.createPolyLineROI(self.points)
            self.clear_scatter()
            self.create_roi_mode = False
            self.imageView.view.state['mouseEnabled'] = np.array([True, True])
        elif self.freestyle_mode:
            self.B_freestyle_callback()
        else:
            pass

    def _remove_ROI(self, polylineroi):
        polylineroi.sigRemoveRequested.disconnect(self._remove_ROI)
        self.polyLineROIs.remove(polylineroi)
        self.imageView.view.removeItem(polylineroi)
        del polylineroi

    ## WIDGET CALLBACKS

    def frame_slider_callback(self):
        """
        Callback for user changes to the frame slider, which changes 
        the current frame shown beneath the masks.

        """
        frame_index = self.frame_slider.value()
        self.update_image(frame_index=frame_index)

    def B_create_ROI_callback(self):
        """
        Callback for user selection of the "Draw ROI" button. Enter
        create ROI mode, which allows the user to place discrete vertices
        sequentially to build a mask.

        """
        self.create_roi_mode = True
        self.imageView.view.state['mouseEnabled'] = np.array([False, False])

    def B_freestyle_callback(self):
        """
        Callback for user selection of the "freestyle" button. Enter
        freestyle mode, which allows the user to draw a mask on the 
        image by dragging the mouse.

        """
        # End freestyle mode by creating a mask from the drawn points
        if self.freestyle_mode:
            self.freestyle_mode = False
            self.imageView.imageItem.setDrawKernel(kernel=None,
                                                   mask=None,
                                                   center=None)
            mask = (self.image == self.draw_val)
            self.points = get_ordered_mask_points(
                mask, max_points=self.max_points_freestyle)
            self.createPolyLineROI(self.points)
            self.frame_slider_callback()
            self.points = []
            self.B_freestyle.setText("Freestyle")

        # Start freestyle mode by enabling drawing on self.imageView.imageItem
        else:
            self.freestyle_mode = True
            self.draw_val = int(self.image.max() + 2)
            kernel = np.array([[self.draw_val]])
            self.imageView.imageItem.setDrawKernel(kernel=kernel,
                                                   mask=None,
                                                   center=(0, 0))
            self.B_freestyle.setText("Finish freestyle")

    def B_save_callback(self):
        """
        Save the current set of masks to a file. Every currently 
        defined mask is saved in a CSV-like format indexed by mask
        vertex. This CSV has the following columns:

            filename        :   str, the source filename
            mask_index      :   int, the index of this mask
            y               :   float, the y-coordinate of the vertex
            x               :   float, the x-coordinate of the vertex
            vertex          :   int, the index of this vertex in the
                                context of its mask

        The "vertex" always ascends from 0 to n-1, where n is the number
        of vertices in that mask.

        """
        # If no masks are defined, do nothing
        if len(self.polyLineROIs) == 0:
            return

        # Prompt the user to select an output filename
        out_path = getSaveFilePath(self,
                                   "Select output CSV",
                                   "{}_masks.csv".format(
                                       os.path.splitext(self.image_path)[0]),
                                   "CSV files (*.csv)",
                                   initialdir=self.get_currdir())

        # For each PolyLineROI mask, get the corresponding set of vertices
        point_arrays = [self.getPoints(p) for p in self.polyLineROIs]
        n_masks = len(point_arrays)

        # Get the total size of the output dataframe
        m = sum([arr.shape[0] for arr in point_arrays])

        # Format as a pandas.DataFrame
        df = pd.DataFrame(
            index=np.arange(m),
            columns=["filename", "mask_index", "y", "x", "vertex"])
        df["filename"] = self.image_path
        c = 0
        for mask_index, arr in enumerate(point_arrays):
            l = arr.shape[0]
            df.loc[c:c + l - 1, "mask_index"] = mask_index
            df.loc[c:c + l - 1, "y"] = arr[:, 0]
            df.loc[c:c + l - 1, "x"] = arr[:, 1]
            df.loc[c:c + l - 1, "vertex"] = np.arange(l)
            c += l

        # Save
        df.to_csv(out_path, index=False)

        # Save this directory as the last accessed
        self.set_currdir(out_path)

    def B_load_callback(self):
        """
        Load a set of masks from a file previously saved with this GUI. This
        erases any currently defined masks.

        """
        # Prompt the user to select a file
        path = getOpenFilePath(self,
                               "Select mask CSV",
                               "CSV and TIF files (*.csv *.tif *.tiff)",
                               initialdir=self.get_currdir())
        self.set_currdir(path)

        ext = os.path.splitext(path)[-1]

        # Open this file and check that it contains the necessary info
        if ext == ".csv":
            try:
                df = pd.read_csv(path)
                for c in ["filename", "mask_index", "y", "x", "vertex"]:
                    assert c in df.columns
            except:
                print("File {} not in the correct format; must be a CSV with " \
                    "the 'filename', 'mask_index', 'y', 'x', and 'vertex' " \
                    "columns".format(path))
                return

            # Warn the user if the image file path in this file doesn't
            # match the current image file path
            if df.loc[0, "filename"] != self.image_path:
                print("WARNING: original file path for this mask file is different " \
                    "than the current image file.\nOriginal: {}\nCurrent: {}".format(
                        df.loc[0, "filename"], self.image_path))
        elif ext in [".tif", ".tiff"]:

            # Load the mask image
            masks_im = tifffile.imread(path)
            assert len(masks_im.shape) == 2, "WARNING: RGB TIFs not supported"

            # Get the set of unique masks defined in this image
            n_points = 100
            mask_indices = [j for j in list(np.unique(masks_im)) if j != 0]
            dfs = []
            for mi in mask_indices:
                mask_im = masks_im == mi
                edges = get_edges(mask_im)
                points = get_ordered_mask_points(edges, max_points=n_points)
                df = pd.DataFrame(index=list(range(points.shape[0])),
                                  columns=["filename", "mask_index", "y", "x"])
                df["filename"] = path
                df["mask_index"] = mi
                df["y"] = points[:, 0]
                df["x"] = points[:, 1]
                df["vertex"] = list(range(points.shape[0]))
                dfs.append(df)
            df = pd.concat(dfs, axis=0, ignore_index=True)

        # Erase the current set of masks, if any
        self.clear_polyLineROIs()

        # Generate a PolyLineROI object for each of the loaded masks
        for mask_index, mask_frame in df.groupby("mask_index"):
            self.points = np.asarray(mask_frame[["y", "x"]])
            self.createPolyLineROI(self.points)
            self.points = []

    def B_apply_callback(self):
        """
        Callback for user selection of the "Apply masks" button. The
        user is prompted to select a file containing localizations,
        and the localizations in that file are classified according to 
        which mask they belong to.

        """
        # Prompt the user to select a file containing localizations
        path = getOpenFilePath(
            self,
            "Select localization file",
            "CSV files and *Tracked.mat files (*.csv *Tracked.mat)",
            initialdir=self.get_currdir())
        self.set_currdir(path)

        # Open this file and make sure it actually contains localizations
        ext = os.path.splitext(path)[-1]
        try:
            if ext == ".csv":
                locs = pd.read_csv(path)
                assert all([c in locs.columns for c in ['frame', 'y', 'x']])
            elif ext == ".mat":
                locs = tracked_mat_to_csv(path)
        except:
            print("Could not load file {}; must either be *.csv or " \
                "*Tracked.mat format".format(path))
            return

        # Prompt the user to input the mask column. This can be something
        # as simple as "mask_index", but the user may want something more
        # descriptive - like "nuclear_mask" or something. This becomes useful
        # when the user applies masks multiple times to the same file - for
        # instance, to label primary and secondary features (e.g. nuclei and
        # nucleoli) in an image.
        col = getTextInputs(["Output mask column name"], ["mask_index"],
                            title="Select output column name")[0]

        # Prompt the user to select a mode for the assignment of trajectories
        # to masks. This can either be "by_localization" (independent of the
        # trajectory to which each localization is assigned), "single_point"
        # (assign all localizations to a mask if a single localization is
        # inside the mask), or "all_points" (only assign all localizations to a
        # mask if all localizations are inside the mask)
        options = ["by_localization", "single_point", "all_points"]
        box = SingleComboBoxDialog(
            "Method by which trajectories are assigned to masks",
            options,
            init_value="single_point",
            title="Assignment mode",
            parent=self)
        box.exec_()
        if box.Accepted:
            mode = box.return_val
        else:
            print("Dialog not accepted")
            return

        # Assign each localization to one of the current masks
        point_sets = [self.getPoints(p) for p in self.polyLineROIs]
        locs[col] = apply_masks(point_sets, locs, mode=mode)

        # Save to the same file
        if ext == ".csv":
            out_path = path
        elif ext == ".mat":
            if "_Tracked.mat" in path:
                out_path = path.replace("_Tracked.mat", ".csv")
            elif "Tracked.mat" in path:
                out_path = path.replace("Tracked.mat", ".csv")
            else:
                out_path = "{}.csv".format(os.path.splitext(path)[0])
        locs.to_csv(out_path, index=False)

        # Save only the masked trajectories to a different file
        out_file_inside = "{}_in_mask.csv".format(
            os.path.splitext(out_path)[0])
        out_file_outside = "{}_outside_mask.csv".format(
            os.path.splitext(out_path)[0])
        locs[locs["mask_index"] > 0].to_csv(out_file_inside, index=False)
        locs[locs["mask_index"] == 0].to_csv(out_file_outside, index=False)

        # Show the result
        show_mask_assignments(point_sets,
                              locs,
                              mask_col=col,
                              max_points_scatter=5000)

    def B_accept_callback(self):
        """
        If this GUI is being used as a dialog, accept and return the currently
        defined set of masks.

        """
        self.return_val = [self.getPoints(p) for p in self.polyLineROIs]
        print(self.return_val)
        self.accept()
예제 #13
0
파일: masker.py 프로젝트: alecheckert/quot
    def initUI(self):
        """
        Initialize the user interface.

        """
        # Main layout
        L_master = QGridLayout(self)

        # An ImageView on the left to contain the subject of masking
        self.imageView = ImageView(parent=self)
        L_master.addWidget(self.imageView, 0, 0, 15, 2)
        self.imageView.setImage(self.image)

        # Override the default ImageView.imageItem.mouseClickEvent
        self.imageView.imageItem.mouseClickEvent = self._imageView_mouseClickEvent

        # Override the default ImageView.imageItem.mouseDoubleClickEvent
        self.imageView.imageItem.mouseDoubleClickEvent = \
            self._imageView_mouseDoubleClickEvent

        # All currently clicked points
        self.points = []

        # All current PolyLineROI objects
        self.polyLineROIs = []

        # ScatterPlotItem, to show current accumulated clicks
        self.scatterPlotItem = ScatterPlotItem()
        self.scatterPlotItem.setParentItem(self.imageView.imageItem)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Frame slider
        self.frame_slider = IntSlider(minimum=0,
                                      maximum=self.imageReader.n_frames - 1,
                                      interval=1,
                                      init_value=0,
                                      name='Frame',
                                      parent=self)
        L_master.addWidget(self.frame_slider,
                           0,
                           2,
                           1,
                           1,
                           alignment=widget_align)
        self.frame_slider.assign_callback(self.frame_slider_callback)

        # Button: create new ROI
        self.create_roi_mode = False
        self.B_create_ROI = QPushButton("Draw ROI", parent=self)
        self.B_create_ROI.clicked.connect(self.B_create_ROI_callback)
        L_master.addWidget(self.B_create_ROI,
                           10,
                           2,
                           1,
                           1,
                           alignment=widget_align)

        # Button: freestyle drawing to make ROI
        self.freestyle_mode = False
        self.B_freestyle = QPushButton("Freestyle", parent=self)
        self.B_freestyle.clicked.connect(self.B_freestyle_callback)
        L_master.addWidget(self.B_freestyle,
                           11,
                           2,
                           1,
                           1,
                           alignment=widget_align)

        # Pure dialog mode: no options to save or load masks, only
        # define and accept
        if self.dialog_mode:

            # Button: accept the current set of masks. This is only meaningful
            # when this class is being used as a dialog by other classes.
            self.B_accept = QPushButton("Accept masks", parent=self)
            self.B_accept.clicked.connect(self.B_accept_callback)
            L_master.addWidget(self.B_accept,
                               12,
                               2,
                               1,
                               1,
                               alignment=widget_align)

        # Non-dialog mode: full range of options
        else:

            # Button: apply these masks to the localizations in a file
            self.B_apply = QPushButton("Apply masks", parent=self)
            self.B_apply.clicked.connect(self.B_apply_callback)
            L_master.addWidget(self.B_apply,
                               12,
                               2,
                               1,
                               1,
                               alignment=widget_align)

            # Button: save masks to a file
            self.B_save = QPushButton("Save masks", parent=self)
            self.B_save.clicked.connect(self.B_save_callback)
            L_master.addWidget(self.B_save,
                               13,
                               2,
                               1,
                               1,
                               alignment=widget_align)

            # Button: load preexisting masks from a file
            self.B_load = QPushButton("Load masks", parent=self)
            self.B_load.clicked.connect(self.B_load_callback)
            L_master.addWidget(self.B_load,
                               14,
                               2,
                               1,
                               1,
                               alignment=widget_align)

        # Resize and launch
        self.resize(800, 600)
        self.show()
예제 #14
0
    def __init__(self, settings, server):
        super().__init__()

        self.server = server
        self.timer = QTimer()
        self.timer.timeout.connect(self.time_tick)

        self.settings = settings
        self.current_class = None
        self.current_students = []

        self.stack = QStackedLayout()
        self.setLayout(self.stack)

        # First page: connection instructions
        connect_widget = QWidget()
        connect_layout = QVBoxLayout(connect_widget)
        connect_layout.addStretch()
        connect_label = QLabel(
            "Open a world in Minecraft, open a terminal (press t), and type:",
            self)
        connect_layout.addWidget(connect_label)
        self.connect_command = f'/connect {server.get_ip()}:{PORT}'
        connect_command_box = QLineEdit(self.connect_command, self)
        connect_command_box.setReadOnly(True)
        connect_layout.addWidget(connect_command_box)
        connect_copy_button = QPushButton("Copy to Clipboard", self)
        connect_copy_button.clicked.connect(
            lambda: QApplication.clipboard().setText(self.connect_command))
        connect_layout.addWidget(connect_copy_button)
        connection_problems_button = QPushButton("Connection Problems?", self)
        connection_problems_button.clicked.connect(self.show_connection_help)
        connect_layout.addWidget(connection_problems_button)
        connect_layout.addStretch()
        self.stack.addWidget(connect_widget)

        # Main page
        main_col_widget = QWidget()
        columns = QHBoxLayout(main_col_widget)
        col_left = QVBoxLayout()
        col_mid = QVBoxLayout()
        col_right = QVBoxLayout()
        columns.addLayout(col_left)
        columns.addSpacing(10)
        columns.addLayout(col_mid)
        columns.addSpacing(10)
        columns.addLayout(col_right)
        self.stack.addWidget(main_col_widget)

        self.pause_button = self.setup_toggle_button(col_left,
                                                     self.server.pause_game,
                                                     'Un-pause',
                                                     self.server.unpause_game,
                                                     'Pause')
        button_size = self.pause_button.size()
        self.disable_chat_button = self.setup_toggle_button(
            col_left, self.server.disable_chat, 'Enable Chat',
            self.server.enable_chat, 'Disable Chat', button_size)
        self.allow_mobs_button = self.setup_toggle_button(
            col_left, self.server.disallow_mobs, 'Allow Mobs',
            self.server.allow_mobs, 'Disable Mobs', button_size)
        self.allow_destructiveobjects_button = self.setup_toggle_button(
            col_left, self.server.disallow_destructiveobjects,
            'Enable Destructive Items', self.server.allow_destructiveobjects,
            'Disable Destructive Items', button_size)
        self.allow_player_damage_button = self.setup_toggle_button(
            col_left, self.server.disallow_player_damage,
            'Enable Player Damage', self.server.allow_player_damage,
            'Disable Player Damage', button_size)
        self.allow_pvp_button = self.setup_toggle_button(
            col_left, self.server.disallow_pvp, 'Allow Player Fighting',
            self.server.allow_pvp, 'Disable Player Fighting', button_size)
        self.immutable_button = self.setup_toggle_button(
            col_left, self.server.immutable_world,
            'Enable World Modifications', self.server.mutable_world,
            'Disable World Modifications', button_size)
        self.weather_button = self.setup_toggle_button(
            col_left, self.server.perfect_weather, 'Disable Perfect Weather',
            self.server.imperfect_weather, 'Enable Perfect Weather',
            button_size)
        self.disable_potions_button = self.setup_toggle_button(
            col_left, self.server.disable_potions, 'Enable Potions',
            self.server.enable_potions, 'Disable Potions', button_size)

        # self.clear_potions_button = QPushButton('Clear All Potion Effects', self)
        # self.clear_potions_button.resize(button_size)
        # self.clear_potions_button.clicked.connect(lambda: self.server.clear_effects("@a"))
        # col_left.addWidget(self.clear_potions_button)

        self.teleport_button = QPushButton('Teleport Everyone to You', self)
        self.teleport_button.resize(button_size)
        self.teleport_button.clicked.connect(
            lambda: self.server.teleport_all_to("@s"))
        col_left.addWidget(self.teleport_button)

        self.disconnect_button = QPushButton('Disconnect', self)
        self.disconnect_button.resize(button_size)
        self.disconnect_button.clicked.connect(self.server.socket_disconnected)
        col_left.addWidget(self.disconnect_button)

        col_left.addStretch()

        self.version_label = QLabel(f'Version {VERSION}', self)
        self.version_label.setAlignment(Qt.AlignCenter)
        col_left.addWidget(self.version_label)

        self.feedback_button = QPushButton('Feedback/Bug report', self)
        self.feedback_button.resize(button_size)
        self.feedback_button.clicked.connect(
            lambda: QDesktopServices.openUrl(FEEDBACK_URL))
        col_left.addWidget(self.feedback_button)

        # Middle Column: Roll/Register
        self.classes_combo = QComboBox(self)
        class_names = self.settings.value("class_names", [])
        self.classes_combo.addItem("Select class")
        self.classes_combo.addItem("Add a class")
        self.classes_combo.addItem("Delete a class")
        self.classes_combo.addItems(class_names)
        self.classes_combo.currentTextChanged.connect(self.class_changed)
        col_mid.addWidget(self.classes_combo)

        self.users_table = QTableWidget(0, 2)
        self.users_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.users_table.setSizePolicy(
            QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding))
        self.users_table.setFixedWidth(140)
        self.users_table.verticalHeader().hide()
        header = self.users_table.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.Stretch)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.hide()
        col_mid.addWidget(self.users_table)

        self.class_edit_button = QPushButton("Edit Class", self)
        col_mid.addWidget(self.class_edit_button)
        self.class_edit_button.clicked.connect(self.edit_class)

        self.chat_box = QPlainTextEdit(
            f'Minecraft Education Chat Logs {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
            self)
        self.chat_box.setReadOnly(True)
        col_right.addWidget(self.chat_box)

        self.chat_input = QLineEdit(self)
        self.chat_input.setPlaceholderText("Type chat here; enter to send")
        self.chat_input.returnPressed.connect(self.chat_enter)
        col_right.addWidget(self.chat_input)

        self.chat_save = QPushButton("Save Chat Logs", self)
        self.chat_save.clicked.connect(self.save_chat)
        col_right.addWidget(self.chat_save)

        self.user_map = PlotWidget()
        self.map_item = ScatterPlotItem(size=10)
        self.user_map.addItem(self.map_item)
        self.user_map.getPlotItem().hideAxis('left')
        self.user_map.getPlotItem().hideAxis('bottom')
        self.map_item.scene().sigMouseMoved.connect(self.map_hover)
        map_viewbox = self.map_item.getViewBox()
        map_viewbox.menu = None
        col_right.addWidget(self.user_map)

        self.user_map_info = QLineEdit("Hover over a user", self)
        self.user_map_info.setReadOnly(True)
        col_right.addWidget(self.user_map_info)

        self.setGeometry(300, 300, 800, 600)
        self.setWindowTitle('MineClass')

        self.stack.setCurrentIndex(0)
        self.activate_buttons(False)
        self.show()

        if is_newer_version_available():
            QMessageBox.about(
                self, "Newer Version Available",
                f'A newer version of this program is available <a href="{GITHUB_DOWNLOAD_URL}">here</a>.'
            )

        if not self.settings.value("HasRunFirstTime", False):
            self.show_connection_help()
            self.settings.setValue("HasRunFirstTime", True)
예제 #15
0
class MCClassroom(QWidget):
    def __init__(self, settings, server):
        super().__init__()

        self.server = server
        self.timer = QTimer()
        self.timer.timeout.connect(self.time_tick)

        self.settings = settings
        self.current_class = None
        self.current_students = []

        self.stack = QStackedLayout()
        self.setLayout(self.stack)

        # First page: connection instructions
        connect_widget = QWidget()
        connect_layout = QVBoxLayout(connect_widget)
        connect_layout.addStretch()
        connect_label = QLabel(
            "Open a world in Minecraft, open a terminal (press t), and type:",
            self)
        connect_layout.addWidget(connect_label)
        self.connect_command = f'/connect {server.get_ip()}:{PORT}'
        connect_command_box = QLineEdit(self.connect_command, self)
        connect_command_box.setReadOnly(True)
        connect_layout.addWidget(connect_command_box)
        connect_copy_button = QPushButton("Copy to Clipboard", self)
        connect_copy_button.clicked.connect(
            lambda: QApplication.clipboard().setText(self.connect_command))
        connect_layout.addWidget(connect_copy_button)
        connection_problems_button = QPushButton("Connection Problems?", self)
        connection_problems_button.clicked.connect(self.show_connection_help)
        connect_layout.addWidget(connection_problems_button)
        connect_layout.addStretch()
        self.stack.addWidget(connect_widget)

        # Main page
        main_col_widget = QWidget()
        columns = QHBoxLayout(main_col_widget)
        col_left = QVBoxLayout()
        col_mid = QVBoxLayout()
        col_right = QVBoxLayout()
        columns.addLayout(col_left)
        columns.addSpacing(10)
        columns.addLayout(col_mid)
        columns.addSpacing(10)
        columns.addLayout(col_right)
        self.stack.addWidget(main_col_widget)

        self.pause_button = self.setup_toggle_button(col_left,
                                                     self.server.pause_game,
                                                     'Un-pause',
                                                     self.server.unpause_game,
                                                     'Pause')
        button_size = self.pause_button.size()
        self.disable_chat_button = self.setup_toggle_button(
            col_left, self.server.disable_chat, 'Enable Chat',
            self.server.enable_chat, 'Disable Chat', button_size)
        self.allow_mobs_button = self.setup_toggle_button(
            col_left, self.server.disallow_mobs, 'Allow Mobs',
            self.server.allow_mobs, 'Disable Mobs', button_size)
        self.allow_destructiveobjects_button = self.setup_toggle_button(
            col_left, self.server.disallow_destructiveobjects,
            'Enable Destructive Items', self.server.allow_destructiveobjects,
            'Disable Destructive Items', button_size)
        self.allow_player_damage_button = self.setup_toggle_button(
            col_left, self.server.disallow_player_damage,
            'Enable Player Damage', self.server.allow_player_damage,
            'Disable Player Damage', button_size)
        self.allow_pvp_button = self.setup_toggle_button(
            col_left, self.server.disallow_pvp, 'Allow Player Fighting',
            self.server.allow_pvp, 'Disable Player Fighting', button_size)
        self.immutable_button = self.setup_toggle_button(
            col_left, self.server.immutable_world,
            'Enable World Modifications', self.server.mutable_world,
            'Disable World Modifications', button_size)
        self.weather_button = self.setup_toggle_button(
            col_left, self.server.perfect_weather, 'Disable Perfect Weather',
            self.server.imperfect_weather, 'Enable Perfect Weather',
            button_size)
        self.disable_potions_button = self.setup_toggle_button(
            col_left, self.server.disable_potions, 'Enable Potions',
            self.server.enable_potions, 'Disable Potions', button_size)

        # self.clear_potions_button = QPushButton('Clear All Potion Effects', self)
        # self.clear_potions_button.resize(button_size)
        # self.clear_potions_button.clicked.connect(lambda: self.server.clear_effects("@a"))
        # col_left.addWidget(self.clear_potions_button)

        self.teleport_button = QPushButton('Teleport Everyone to You', self)
        self.teleport_button.resize(button_size)
        self.teleport_button.clicked.connect(
            lambda: self.server.teleport_all_to("@s"))
        col_left.addWidget(self.teleport_button)

        self.disconnect_button = QPushButton('Disconnect', self)
        self.disconnect_button.resize(button_size)
        self.disconnect_button.clicked.connect(self.server.socket_disconnected)
        col_left.addWidget(self.disconnect_button)

        col_left.addStretch()

        self.version_label = QLabel(f'Version {VERSION}', self)
        self.version_label.setAlignment(Qt.AlignCenter)
        col_left.addWidget(self.version_label)

        self.feedback_button = QPushButton('Feedback/Bug report', self)
        self.feedback_button.resize(button_size)
        self.feedback_button.clicked.connect(
            lambda: QDesktopServices.openUrl(FEEDBACK_URL))
        col_left.addWidget(self.feedback_button)

        # Middle Column: Roll/Register
        self.classes_combo = QComboBox(self)
        class_names = self.settings.value("class_names", [])
        self.classes_combo.addItem("Select class")
        self.classes_combo.addItem("Add a class")
        self.classes_combo.addItem("Delete a class")
        self.classes_combo.addItems(class_names)
        self.classes_combo.currentTextChanged.connect(self.class_changed)
        col_mid.addWidget(self.classes_combo)

        self.users_table = QTableWidget(0, 2)
        self.users_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.users_table.setSizePolicy(
            QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding))
        self.users_table.setFixedWidth(140)
        self.users_table.verticalHeader().hide()
        header = self.users_table.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.Stretch)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.hide()
        col_mid.addWidget(self.users_table)

        self.class_edit_button = QPushButton("Edit Class", self)
        col_mid.addWidget(self.class_edit_button)
        self.class_edit_button.clicked.connect(self.edit_class)

        self.chat_box = QPlainTextEdit(
            f'Minecraft Education Chat Logs {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
            self)
        self.chat_box.setReadOnly(True)
        col_right.addWidget(self.chat_box)

        self.chat_input = QLineEdit(self)
        self.chat_input.setPlaceholderText("Type chat here; enter to send")
        self.chat_input.returnPressed.connect(self.chat_enter)
        col_right.addWidget(self.chat_input)

        self.chat_save = QPushButton("Save Chat Logs", self)
        self.chat_save.clicked.connect(self.save_chat)
        col_right.addWidget(self.chat_save)

        self.user_map = PlotWidget()
        self.map_item = ScatterPlotItem(size=10)
        self.user_map.addItem(self.map_item)
        self.user_map.getPlotItem().hideAxis('left')
        self.user_map.getPlotItem().hideAxis('bottom')
        self.map_item.scene().sigMouseMoved.connect(self.map_hover)
        map_viewbox = self.map_item.getViewBox()
        map_viewbox.menu = None
        col_right.addWidget(self.user_map)

        self.user_map_info = QLineEdit("Hover over a user", self)
        self.user_map_info.setReadOnly(True)
        col_right.addWidget(self.user_map_info)

        self.setGeometry(300, 300, 800, 600)
        self.setWindowTitle('MineClass')

        self.stack.setCurrentIndex(0)
        self.activate_buttons(False)
        self.show()

        if is_newer_version_available():
            QMessageBox.about(
                self, "Newer Version Available",
                f'A newer version of this program is available <a href="{GITHUB_DOWNLOAD_URL}">here</a>.'
            )

        if not self.settings.value("HasRunFirstTime", False):
            self.show_connection_help()
            self.settings.setValue("HasRunFirstTime", True)

    def show_connection_help(self):
        QMessageBox.about(
            self, "Connection Help",
            '''Before using (or if Minecraft has been newly installed), go to Settings->Profile and disable "Require Encrypted Websockets"\n\n
Sometimes you'll need to attempt connecting twice (use the up arrow in the Minecraft terminal to access history'''
        )

    def save_chat(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "Save Chat Logs",
            "",
            "Text Files (*.txt);;All Files (*)",
            options=options)
        if file_name:
            with open(file_name, "w") as f:
                f.write(self.chat_box.toPlainText())

    def chat_enter(self):
        server.send_chat(self.chat_input.text())
        self.update_chat_box("Teacher", self.chat_input.text(), "chat")
        self.chat_input.clear()

    def map_hover(self, pos):
        act_pos = self.map_item.mapFromScene(pos)
        points = self.map_item.pointsAt(act_pos)

        text = ""
        for p in points:
            text += f'{p.data()}: ({round(p.pos()[0])}, {round(p.pos()[1])}), '
        self.user_map_info.setText(text)

    def time_tick(self):
        self.server.get_users()

    def start_timer(self):
        self.time_tick()
        self.timer.start(10000)

    def stop_timer(self):
        self.timer.stop()

    def update_chat_box(self, sender, message, message_type, receiver=None):
        t = datetime.datetime.now().strftime("%H:%M:%S")
        if message_type == "chat":
            out = f'{t} <{sender}> {message}'
        elif message_type == "tell":
            out = f'{t} <{sender} whispers to {receiver}> {message}'
        self.chat_box.appendPlainText(out)

    def activate_buttons(self, activate):
        self.pause_button.setDisabled(not activate)

    def setup_toggle_button(self,
                            parent,
                            checked_action,
                            checked_text,
                            unchecked_action,
                            unchecked_text,
                            size=None):
        button = QPushButton(unchecked_text, self)
        button.resize(button.sizeHint() if size is None else size)
        button.setCheckable(True)
        parent.addWidget(button)

        def toggle_button_clicked(checked_status):
            if checked_status:
                checked_action()
                button.setText(checked_text)
            else:
                unchecked_action()
                button.setText(unchecked_text)

        button.toggled.connect(toggle_button_clicked)
        return button

    def get_students_from_grid(self):
        try:
            return [
                self.users_table.item(i, 0).text()
                for i in range(self.users_table.rowCount())
            ]
        except AttributeError:
            return []

    def edit_class(self):
        selection = self.classes_combo.currentText()
        if selection in ("Select class", "Add a class"):
            QMessageBox.about(self, "Error",
                              "Please select (or create) a class first")
            return

        current_list = self.get_students_from_grid()
        new_list, ok_pressed = QInputDialog().getMultiLineText(
            self,
            "Edit Class List",
            "Add or remove students from this class",
            text="\n".join(current_list))
        if ok_pressed:
            students = [i for i in new_list.split("\n")
                        if i]  # list comprehension to remove empty strings
            self.current_students = students
            self.settings.setValue(f'classes/{self.current_class}', students)
            self.load_users()

    def class_changed(self):
        selection = self.classes_combo.currentText()
        # if selection != "Select class":
        #     self.classes_combo.removeItem(self.classes_combo.findText("Select class"))
        if selection == "Add a class":
            new_class, ok_pressed = QInputDialog.getText(
                self, 'Class Name', 'Enter the Class Name or Code')
            if ok_pressed:
                classes = self.settings.value('class_names', [])
                if new_class in classes:
                    print('Class already exists; ignoring')
                else:
                    classes.append(new_class)
                    self.settings.setValue('class_names', classes)
                    self.classes_combo.addItem(new_class)
                self.classes_combo.setCurrentIndex(
                    self.classes_combo.findText(new_class))
        elif selection == "Select class":
            pass
        elif selection == "Delete a class":
            current_classes = self.settings.value('class_names', [])
            if current_classes:
                class_to_delete, ok_pressed = QInputDialog.getItem(
                    self, 'Delete Class', 'Select which class to delete',
                    current_classes, 0, False)
                if ok_pressed and class_to_delete:
                    current_classes.remove(class_to_delete)
                    self.settings.setValue('class_names', current_classes)
                    self.settings.remove(f'classes/{class_to_delete}')
                    self.classes_combo.removeItem(
                        self.classes_combo.findText(class_to_delete))
                    #TODO delete stdents from table
                    self.current_class = None
                    self.current_students = []
            else:
                QMessageBox.information(self, "No Classes!",
                                        "No Class to delete!")
            self.classes_combo.setCurrentIndex(0)
        else:
            self.current_class = selection
            self.current_students = self.settings.value(
                f'classes/{selection}', [])
            self.load_users()

    def load_users(self):
        if len(self.current_students) != self.users_table.rowCount():
            self.users_table.setRowCount(len(self.current_students))
        for i, user in enumerate(self.current_students):
            self.users_table.setItem(i, 0, QTableWidgetItem(user))
        self.users_table.sortItems(0, QtCore.Qt.AscendingOrder)
        self.users_table.sortItems(1, QtCore.Qt.DescendingOrder)

    def update_users_from_mc(self, users):
        table_user_count = self.users_table.rowCount()
        for i in range(table_user_count):
            current_table_user = self.users_table.item(i, 0).text()
            if current_table_user in users:
                tick = QTableWidgetItem("✓")
                tick.setTextAlignment(QtCore.Qt.AlignCenter)
                tick.setBackground(QColor(QtCore.Qt.green))
                self.users_table.setItem(i, 1, tick)
                users.remove(current_table_user)
            else:
                cross = QTableWidgetItem("✗")
                cross.setTextAlignment(QtCore.Qt.AlignCenter)
                cross.setBackground(QColor(QtCore.Qt.red))
                self.users_table.setItem(i, 1, cross)

        #handle users from server but not in table
        self.users_table.setRowCount(table_user_count + len(users))
        for i, user in enumerate(users):
            self.users_table.setItem(i + table_user_count, 0,
                                     QTableWidgetItem(user))

        self.users_table.sortItems(0, QtCore.Qt.AscendingOrder)
        self.users_table.sortItems(1, QtCore.Qt.DescendingOrder)
        self.users_table.resizeRowsToContents()

    def update_map(self, users):
        data = [
            {
                'pos': (int(u['position']['x']), int(u['position']['z'])),
                'data': u['name'],
                'brush': mkBrush(
                    'g'
                ),  #mkBrush("r" if u['name'] == self.server.self_name else "g"),
                'symbol': ("s" if u['name'] == self.server.self_name else "o"),
            } for u in users.values()
        ]
        self.map_item.setData(data)
예제 #16
0
    def __init__(self, imodel, iview, geom, parent=None):
        QObject.__init__(self, parent)
        self.imodel = imodel
        self.dmodel = imodel.model
        self.iview = iview
        self.iview.ui.menuBtn.hide()
        self.iview.ui.roiBtn.hide()

        # Thanks CXIVIEW, Anton & Valerio
        pos = np.array([0.0, 0.5, 1.0])
        color = np.array(
            [[255, 255, 255, 255], [128, 128, 128, 255], [0, 0, 0, 255]],
            dtype=np.ubyte)
        new_color_map = ColorMap(pos, color)
        self.iview.ui.histogram.gradient.setColorMap(new_color_map)

        self.lastrow = -1
        self.curFileName = None
        self.curFile = None
        self.canDraw = self.dmodel.canSaveLst()
        self.checkEvent = "event" in self.dmodel.cols
        self.yxmap = None
        self.slab_shape = None
        self.img_shape = None
        self.geom_coffset = 0
        self.geom_pixsize = None
        self.im_out = None
        self.resolutionLambda = None
        self.pixRadiusToRes = None
        self.hkl = None
        self.hklLookup = None
        self.imodel.dataChanged.connect(self.draw)

        self.iview.view.menu.addSeparator()
        openGeomAct = self.iview.view.menu.addAction("Load CrystFEL Geometry")
        openGeomAct.triggered.connect(self.openGeom)

        peakmenu = self.iview.view.menu.addMenu("Peaks")
        self.peakActionGroup = QActionGroup(self)
        self.peakActionGroup.setExclusive(True)
        self.peakActionGroup.triggered.connect(self.drawPeaks)

        peakNone = peakmenu.addAction("None")
        peakNone.setCheckable(True)
        peakNone.setChecked(True)
        peakNone.setData(0)
        self.peakActionGroup.addAction(peakNone)

        peakCXI = peakmenu.addAction("CXI")
        peakCXI.setCheckable(True)
        peakCXI.setEnabled(self.dmodel.canSaveLst())
        peakCXI.setData(1)
        self.peakActionGroup.addAction(peakCXI)

        peakStream = peakmenu.addAction("Stream")
        peakStream.setCheckable(True)
        peakStream.setEnabled(self.dmodel.hasStreamPeaks())
        peakStream.setData(2)
        self.peakActionGroup.addAction(peakStream)
        self.peakCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.peakCanvas)

        reflectionmenu = self.iview.view.menu.addMenu("Reflections")
        self.reflectionActionGroup = QActionGroup(self)
        self.reflectionActionGroup.setExclusive(True)
        self.reflectionActionGroup.triggered.connect(self.drawReflections)

        refNone = reflectionmenu.addAction("None")
        refNone.setCheckable(True)
        refNone.setChecked(True)
        refNone.setData(0)
        self.reflectionActionGroup.addAction(refNone)

        refStream = reflectionmenu.addAction("Stream")
        refStream.setCheckable(True)
        refStream.setEnabled(self.dmodel.hasStreamReflections())
        refStream.setData(1)
        self.reflectionActionGroup.addAction(refStream)
        self.reflectionCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.reflectionCanvas)

        self.drawResRingsAct = self.iview.view.menu.addAction(
            "Resolution Rings")
        self.drawResRingsAct.setCheckable(True)
        self.drawResRingsAct.setChecked(False)
        self.drawResRingsAct.triggered.connect(self.drawResRings)
        self.drawResRingsAct.setEnabled(self.yxmap is not None)
        self.resolutionRingsCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionRingsCanvas)
        self.resRingsTextItems = []
        for x in self.dmodel.cfg.viewerResolutionRingsAngstroms:
            self.resRingsTextItems.append(TextItem('', anchor=(0.5, 0.8)))
            self.iview.getView().addItem(self.resRingsTextItems[-1])

        self.drawResolutionLimitAct = self.iview.view.menu.addAction(
            "Resolution Limit Ring")
        self.drawResolutionLimitAct.setCheckable(True)
        self.drawResolutionLimitAct.setChecked(False)
        self.drawResolutionLimitAct.triggered.connect(self.drawResLimitRing)
        self.drawResolutionLimitAct.setEnabled(
            self.yxmap is not None and 'reslim' in self.dmodel.cols)
        self.resolutionLimitCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionLimitCanvas)

        if geom is not None:
            self.loadGeom(geom)

        self.toolTipsAct = self.iview.view.menu.addAction(
            "Show Position in Tool Tip")
        self.toolTipsAct.setCheckable(True)
        self.toolTipsAct.setChecked(True)
        self.iview.scene.sigMouseMoved.connect(self.mouseMove)

        self.draw()
예제 #17
0
    def initUI(self):

        # Master layout
        self.win = QWidget()
        L_master = QGridLayout(self.win)

        # Left subwindow, for widgets
        win_left = QWidget(self.win)
        L_left = QGridLayout(win_left)
        L_master.addWidget(win_left, 0, 0, 1, 3)

        # Right subwindow, for images
        win_right = QWidget(self.win)
        L_right = QGridLayout(win_right)
        L_master.addWidget(win_right, 0, 3, 1, 1)

        # Add four LabeledImageViews to this window
        liv_labels = ['(1) Raw', '(2) Filtered', '(3) Convolved', '(4) Binary']
        self.LIVs = [LabeledImageView(parent=win_left, label=liv_labels[j]) \
            for j in range(4)]
        for j in range(4):
            L_left.addWidget(self.LIVs[j], j % 2, j // 2)
            self.LIVs[j].setImage(self.images[j])

        # Link the views between the ImageViews
        for j in range(1, 4):
            self.LIVs[j].ImageView.view.setXLink(self.LIVs[0].ImageView.view)
            self.LIVs[j].ImageView.view.setYLink(self.LIVs[0].ImageView.view)

        # Set up three ScatterPlotItems for each of the first three ImageViews,
        # for overlaying detections as symbols
        self.SPIs = [ScatterPlotItem() for j in range(3)]
        for i in range(3):
            self.SPIs[i].setParentItem(self.LIVs[i].ImageView.imageItem)

        ## WIDGETS

        widget_align = Qt.AlignTop
        slider_min_width = 125

        # Frame slider
        self.frame_slider = IntSlider(minimum=self.start_frame,
                                      maximum=self.stop_frame,
                                      interval=1,
                                      name='Frame',
                                      init_value=0,
                                      min_width=slider_min_width,
                                      parent=win_right)
        L_right.addWidget(self.frame_slider, 0, 0, alignment=widget_align)
        self.frame_slider.assign_callback(self.frame_slider_callback)

        # Select filtering method
        filter_methods = keys_to_str(FILTER_SLIDER_CONFIG.keys())
        self.M_filter = LabeledQComboBox(filter_methods,
                                         "Filter method",
                                         init_value="identity",
                                         parent=win_right)
        L_right.addWidget(self.M_filter, 1, 0, alignment=widget_align)
        self.M_filter.assign_callback(self.M_filter_callback)

        # Select filtering chunk size
        chunk_size_choices = [
            str(i) for i in [2, 5, 10, 15, 20, 30, 40, 60, 80, 100, 200]
        ]
        self.M_chunk_size = LabeledQComboBox(chunk_size_choices, "Chunk size")
        L_right.addWidget(self.M_chunk_size, 2, 0, alignment=widget_align)
        self.M_chunk_size.assign_callback(self.M_chunk_size_callback)
        self.M_chunk_size.setCurrentText(str(self.ChunkFilter.chunk_size))

        # Three semantically-flexible filtering FloatSliders
        self.filter_sliders = [
            FloatSlider(parent=win_right, min_width=slider_min_width)
            for j in range(3)
        ]
        for i in range(len(self.filter_sliders)):
            L_right.addWidget(self.filter_sliders[i],
                              i + 3,
                              0,
                              alignment=widget_align)
            self.filter_sliders[i].assign_callback(
                getattr(self, "filter_slider_%d_callback" % i))
            self.filter_sliders[i].hide()

        # Button to change ROI
        self.B_change_roi = QPushButton("Change ROI", win_right)
        L_right.addWidget(self.B_change_roi, 6, 0, alignment=widget_align)
        self.B_change_roi.clicked.connect(self.B_change_roi_callback)

        # Button to change frame range
        self.B_change_frame_range = QPushButton("Change frame range",
                                                win_right)
        L_right.addWidget(self.B_change_frame_range,
                          7,
                          0,
                          alignment=widget_align)
        self.B_change_frame_range.clicked.connect(
            self.B_change_frame_range_callback)

        # Select detection method
        detect_methods = keys_to_str(DETECT_SLIDER_CONFIG.keys())
        self.M_detect = LabeledQComboBox(detect_methods,
                                         "Detect method",
                                         init_value="llr",
                                         parent=win_right)
        L_right.addWidget(self.M_detect, 0, 1, alignment=widget_align)

        # Set the initial detection method and arguments
        init_detect_method = 'llr'
        self.M_detect.setCurrentText(init_detect_method)
        self.detect_kwargs = {DETECT_SLIDER_CONFIG[init_detect_method][i]['name']: \
            DETECT_SLIDER_CONFIG[init_detect_method][i]['init_value'] \
                for i in DETECT_SLIDER_CONFIG[init_detect_method].keys()}
        self.detect_kwargs['method'] = init_detect_method
        self.M_detect.assign_callback(self.M_detect_callback)

        # Four semantically-flexible detection FloatSliders
        self.detect_sliders = [
            FloatSlider(parent=win_right, min_width=slider_min_width)
            for j in range(4)
        ]
        for i in range(len(self.detect_sliders)):
            L_right.addWidget(self.detect_sliders[i],
                              i + 1,
                              1,
                              alignment=widget_align)
            self.detect_sliders[i].assign_callback(
                getattr(self, "detect_slider_%d_callback" % i))

        # Autoscale the detection threshold
        self.B_auto_threshold = QPushButton("Rescale threshold",
                                            parent=win_right)
        L_right.addWidget(self.B_auto_threshold, 6, 1, alignment=widget_align)
        self.B_auto_threshold.clicked.connect(self.B_auto_threshold_callback)

        # Overlay detections as symbols
        self.B_spot_overlay_state = False
        self.B_spot_overlay = QPushButton("Overlay detections",
                                          parent=win_right)
        L_right.addWidget(self.B_spot_overlay, 7, 1, alignment=widget_align)
        self.B_spot_overlay.clicked.connect(self.B_spot_overlay_callback)

        # Change the symbol used for spot overlays
        symbol_choices = ['+', 'o', 'open +']
        self.M_symbol = LabeledQComboBox(symbol_choices,
                                         "Spot symbol",
                                         init_value='o',
                                         parent=win_right)
        L_right.addWidget(self.M_symbol, 5, 1, alignment=widget_align)
        self.M_symbol.assign_callback(self.M_symbol_callback)

        # Save result to file
        self.B_save = QPushButton("Save settings", parent=win_right)
        L_right.addWidget(self.B_save, 8, 1, alignment=widget_align)
        self.B_save.clicked.connect(self.B_save_callback)

        # Show individual spots
        self.B_show_spots = QPushButton("Show individual spots",
                                        parent=win_right)
        L_right.addWidget(self.B_show_spots, 9, 1, alignment=widget_align)
        self.B_show_spots.clicked.connect(self.B_show_spots_callback)

        ## EMPTY WIDGETS
        for j in range(8, 24):
            _ql = QLabel(win_right)
            L_right.addWidget(_ql, j, 0, alignment=widget_align)

        ## KEYBOARD SHORTCUTS - tab right/left through frames
        self.left_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Left),
                                       self.win)
        self.right_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Right),
                                        self.win)
        self.left_shortcut.activated.connect(self.tab_prev_frame)
        self.right_shortcut.activated.connect(self.tab_next_frame)

        ## INITIALIZATION
        self.change_detect_method(init_detect_method)
        self.filter()
        self.detect()
        self.auto_threshold()
        self.update_images(1,
                           2,
                           3,
                           autoRange=True,
                           autoLevels=True,
                           autoHistogramRange=True)

        # Resize GUI
        self.win.resize(self.gui_width, self.gui_height)

        # Show the main window
        self.win.show()
예제 #18
0
    def setupLayout(self):
        self.layout = QHBoxLayout(self)
        self.splitter = QSplitter(Qt.Horizontal)

        self.settingswidget = QWidget(self)
        self.settingswidget.setMaximumWidth(400)
        self.settingslayout = QVBoxLayout()

        self.percolationWidget = PercolationWidget()

        self.graph_type_combobox = QComboBox(self)
        self.graph_type_combobox.addItems(self.graph_names)
        self.graph_type_combobox.currentIndexChanged.connect(
            self.handleGraphTypeChanged)
        self.graph_type_combobox.setFocusPolicy(Qt.NoFocus)
        self.settingslayout.addWidget(self.graph_type_combobox)

        self.p_slider_layout = QHBoxLayout()
        self.settingslayout.addLayout(self.p_slider_layout)

        self.p_slider_layout.addWidget(QLabel("Set p value: ", self))

        self.p_slider_label = QLabel(self)

        self.p_slider = QSlider(Qt.Horizontal, self)
        self.p_slider.valueChanged.connect(self.handlePSliderChange)

        self.p_slider_layout.addWidget(self.p_slider)
        self.p_slider_layout.addWidget(self.p_slider_label)

        self.N_slider_layout = QHBoxLayout()
        self.settingslayout.addLayout(self.N_slider_layout)

        self.N_slider_layout.addWidget(QLabel("Set size (N): ", self))

        self.N_slider_label = QLabel(self)

        self.N_slider = QSlider(Qt.Horizontal, self)
        self.N_slider.valueChanged.connect(self.handleNSliderChange)

        self.N_slider_layout.addWidget(self.N_slider)
        self.N_slider_layout.addWidget(self.N_slider_label)

        # wether to refresh edges each time or keep edges to add to/remove from
        self.refresh_edges_checkbox = QCheckBox(
            "Refresh Edges instead of Adding/Removing", self)
        self.refresh_edges_checkbox.stateChanged.connect(
            self.handleRefreshCheckboxChange)

        self.settingslayout.addWidget(self.refresh_edges_checkbox)

        self.settingslayout.addSpacerItem(
            QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))

        self.plot_widget = PlotWidget(name="clustersize by p")
        self.settingslayout.addWidget(self.plot_widget)
        self.plot_widget.setLabel('left', 'maximum cluster size', units='%')
        self.plot_widget.setLabel('bottom', 'p', units='')
        self.plot_widget.setXRange(0, 1)
        self.plot_widget.setYRange(0, 1)
        self.plot = ScatterPlotItem()
        self.plot_widget.addItem(self.plot)

        self.plot_clear_button = QPushButton("Clear plot")
        self.plot_clear_button.clicked.connect(self.handlePlotClearButton)
        self.settingslayout.addWidget(self.plot_clear_button)

        self.settingswidget.setLayout(self.settingslayout)
        self.settingswidget.setMinimumSize(50, 50)

        self.splitter.addWidget(self.settingswidget)
        self.splitter.addWidget(self.percolationWidget)

        self.layout.addWidget(self.splitter)

        self.percolationWidget.graphUpdated.connect(self.handleGraphUpdated)

        self.setWindowTitle('Percolation Model')

        self.setFocus()
예제 #19
0
class AttributeViewer(QWidget):
    """
    Show a scatter plot of two attributes from a set of localizations.

    init
    ----
        locs_path       :   str, path to a CSV with localization 
                            data
        max_spots       :   int, the maximum number of spots to 
                            plot. It's often too costly to render
                            more than 30000 spots or so
        gui_size        :   int
        parent          :   root QWidget

    """
    def __init__(self, locs_path, max_spots=10000, gui_size=600, parent=None):

        super(AttributeViewer, self).__init__(parent=parent)
        self.locs_path = locs_path
        self.max_spots = max_spots
        self.gui_size = gui_size

        self.initData()
        self.initUI()

    def initData(self):
        """
        Load the spots data.

        """
        # Load the locs dataframe
        self.locs = pd.read_csv(self.locs_path).sort_values(by='frame')

        # If too many spots, truncate the dataframe
        if len(self.locs) > self.max_spots:
            print("Truncating dataframe to %d spots" % self.max_spots)
            self.locs = self.locs[:self.max_spots]
        else:
            print("Plotting %d spots" % len(self.locs))

        # List of available attributes to display
        dtypes = [
            'float64', 'float', 'int64', 'int32', 'uint16', 'uint8', 'int'
        ]
        self.parameters = [c for c in self.locs.columns if \
            self.locs[c].dtype in dtypes]

        # Generate colormap
        self.n_colors = 1000
        self.cmap = cm.get_cmap('viridis', self.n_colors)
        self.cmap_hex = np.array([mpl_colors.rgb2hex(self.cmap(i)[:3]) \
            for i in range(self.n_colors)])

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

        """
        # Main window
        self.win = QWidget()
        L = QGridLayout(self.win)
        self.win.resize(self.gui_size * 1.8, self.gui_size)

        # Two subwindows: one on the left for the scatter plot
        # and one on the right for widgets
        self.win_left = QWidget(self.win)
        self.win_right = QWidget(self.win)
        L.addWidget(self.win_left, 0, 0, 1, 3)
        L.addWidget(self.win_right, 0, 3, 1, 1)
        L_left = QGridLayout(self.win_left)
        L_right = QGridLayout(self.win_right)

        # GraphicsLayoutWidget, to organize scatter plot and
        # associated items
        self.graphicsLayoutWidget = GraphicsLayoutWidget(parent=self.win_left)

        # PlotItem, to contain the ScatterPlotItem
        self.plotItem = self.graphicsLayoutWidget.addPlot()
        L_left.addWidget(self.graphicsLayoutWidget, 0, 0)

        # ScatterPlotItem, core data display
        self.scatterPlotItem = ScatterPlotItem(symbol='o',
                                               brush=None,
                                               pxMode=True,
                                               pen={
                                                   'color': '#FFFFFF',
                                                   'width': 4.0
                                               },
                                               size=4.0)
        self.plotItem.addItem(self.scatterPlotItem)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Select parameter to map to the x-axis
        self.M_par_0 = LabeledQComboBox(self.parameters,
                                        "x-parameter",
                                        init_value="x",
                                        parent=self.win_right)
        L_right.addWidget(self.M_par_0, 0, 0, alignment=widget_align)
        self.M_par_0.assign_callback(self.M_par_callback)

        # Select parameter to map to the y-axis
        self.M_par_1 = LabeledQComboBox(self.parameters,
                                        "y-parameter",
                                        init_value="y",
                                        parent=self.win_right)
        L_right.addWidget(self.M_par_1, 1, 0, alignment=widget_align)
        self.M_par_1.assign_callback(self.M_par_callback)

        # Select which attribute to color the localizations by
        options = self.parameters + ["density"]
        self.M_color_by = LabeledQComboBox(options,
                                           "Color by",
                                           init_value="density",
                                           parent=self.win_right)
        L_right.addWidget(self.M_color_by, 0, 1, alignment=widget_align)
        self.M_color_by.assign_callback(self.M_color_by_callback)

        # Select the size of the window to use when computing
        # localization density
        window_size_options = [
            str(j)
            for j in [3, 5, 7, 9, 11, 13, 15, 19, 23, 31, 41, 61, 81, 101]
        ]
        self.M_density_window = LabeledQComboBox(window_size_options,
                                                 "Density window",
                                                 init_value="7",
                                                 parent=self.win_right)
        L_right.addWidget(self.M_density_window, 1, 1, alignment=widget_align)
        self.M_density_window.assign_callback(self.M_density_window_callback)

        # Button to induce a simpler representation that can handle
        # more spots
        self.simple_mode = False
        self.B_simple = QPushButton("Simple scatter", parent=self.win_right)
        L_right.addWidget(self.B_simple, 2, 0, alignment=widget_align)
        self.B_simple.clicked.connect(self.B_simple_callback)

        # Button to toggle log color scaling
        self.log_scale_mode = True
        self.B_log = QPushButton("Log color scale", parent=self.win_right)
        self.B_log.clicked.connect(self.B_log_callback)
        L_right.addWidget(self.B_log, 2, 1, alignment=widget_align)

        # Empty widgets to manipulate the layout
        n_rows = 15
        for j in range(3, n_rows):
            q = QWidget(self.win_right)
            L_right.addWidget(q, j, 0)

        # Show the main window
        self.update_scatter()
        self.win.show()

    ## CORE FUNCTIONS

    def get_pars(self):
        """
        Get the current attributes mapped to the x and y axes
        of the scatter plot.

        returns
        -------
            (str: x attribute, str: y attribute)

        """
        return self.M_par_0.currentText(), self.M_par_1.currentText()

    def update_scatter(self, rescale=True):
        """
        Update the scatter plot.

        args
        ----
            rescale     :   change axis limits

        """
        cx, cy = self.get_pars()

        # In the special case of plotting x vs y, make sure
        # that the aspect ratio is right
        if ((cx == 'x') and (cy == 'y')) or ((cx == 'y') and (cy == 'x')):
            self.plotItem.setAspectLocked(lock=True)
        else:
            self.plotItem.setAspectLocked(lock=False)

        # Update the scatter plots
        if self.simple_mode:
            self.scatter_simple(cx, cy)
        else:
            self.scatter_color(cx, cy)

        # Set axis labels
        labelStyle = {'font-size': '18pt'}
        self.plotItem.setLabel('bottom', text=cx, **labelStyle)
        self.plotItem.setLabel('left', text=cy, **labelStyle)

        # Change axis limits
        if rescale:
            self.plotItem.autoBtnClicked()

    def scatter_color(self, cx, cy):
        """
        Load the current set of data into a new scatter plot,
        coloring by the current "color by" attribute.

        args
        ----
            cx, cy      :   str, columns in self.locs

        """
        self.scatterPlotItem.setData(size=4.0, brush=None)
        color_attrib = self.M_color_by.currentText()
        if color_attrib == 'density':
            densities, spots = self.make_density(cx, cy)
        else:
            spots = self.make_attrib_colors(color_attrib)
        self.scatterPlotItem.setData(spots=spots)

    def scatter_simple(self, cx, cy):
        """
        A simpler way of representing the data, using black
        and white only.

        args
        ----
            cx, cy  :   str, columns in self.locs

        """
        self.scatterPlotItem.setData(self.locs[cx],
                                     self.locs[cy],
                                     pen=pg.mkPen(None),
                                     brush=pg.mkBrush(255, 255, 255, 10),
                                     size=10)

    def make_density(self, c0, c1):
        """
        Get the normalized density of points along the (c0, c1)
        axis in the set of localizations.

        args
        ----
            c0, c1      :   str, columns in self.locs

        returns
        -------
            (
                1D ndarray, the density of each point;
                dict, argument to pass to ScatterPlotItem.setData
            )

        """
        # Get the current density window size
        w = int(self.M_density_window.currentText())

        # Format the desired two attributes as ndarray for
        # fast indexing
        data = np.asarray(self.locs[[c0, c1]])

        # Each of the attributes could have very different
        # magnitudes, so scale the distances relative to the
        # difference between the 5th and 95th percentiles for
        # each attribute.

        # Special case: x and y get the same scaling
        if ((c0 == 'x') and (c1 == 'y')) or ((c0 == 'y') and (c1 == 'x')):
            ybinsize = 1.0
            xbinsize = 1.0
        else:
            norm = 400.0
            ybinsize = (np.percentile(self.locs[c0], 95) - \
                np.percentile(self.locs[c0], 5)) / norm
            xbinsize = (np.percentile(self.locs[c1], 95) - \
                np.percentile(self.locs[c1], 5)) / norm

        # Create the binning scheme from these bin sizes
        ybinedges = np.arange(np.percentile(self.locs[c0], 0.1),
                              np.percentile(self.locs[c0], 99.9), ybinsize)
        xbinedges = np.arange(np.percentile(self.locs[c1], 0.1),
                              np.percentile(self.locs[c1], 99.9), xbinsize)

        # Bin the data
        H, _yedges, _xedges = np.histogram2d(self.locs[c0],
                                             self.locs[c1],
                                             bins=(ybinedges, xbinedges))

        # Count the number of points in the neighborhood
        # of each point
        density = uniform_filter(H, w)

        # Digitize the data according to the binning scheme
        # X_int = ((data - np.array([ybinedges.min(), xbinedges.min()])) / \
        #     np.array([ybinsize, xbinsize])).astype('int64')
        # data_y_int = X_int[:,0]
        # data_x_int = X_int[:,1]
        data_y_int = np.digitize(data[:, 0], ybinedges)
        data_x_int = np.digitize(data[:, 1], xbinedges)

        # Determine which data points fall within the binning
        # scheme
        inside = (data_y_int>=0) & (data_x_int>=0) & \
            (data_y_int<(len(ybinedges)-1)) & \
            (data_x_int<(len(xbinedges)-1))
        data_y_int_inside = data_y_int[inside]
        data_x_int_inside = data_x_int[inside]

        # Retrieve the densities for each point
        point_densities = np.empty(data.shape[0], dtype=density.dtype)
        point_densities[inside] = density[data_y_int_inside, data_x_int_inside]

        # Set points outside the binning scheme to density 1
        point_densities[~inside] = 1.0

        # Rescale a little
        point_densities = point_densities * (w**2)
        point_densities[point_densities <= 0] = 0.001

        # Log-scale
        if self.log_scale_mode:
            point_densities = np.log(point_densities) / np.log(2.0)
            point_densities[point_densities < 0.0] = 0.0

        # Rescale the point densities into color indices
        R = (point_densities * (self.n_colors - 1) /
             point_densities.max()).astype(np.int64)
        R[~inside] = 0
        spot_colors = self.cmap_hex[R]

        # Format the plotting spot dict
        spots = [{'pos': data[i,:], 'pen': {'color': spot_colors[i], 'width': 3.0}} \
            for i in range(data.shape[0])]

        return point_densities, spots

    def make_attrib_colors(self, attrib):
        """
        Generate a spot format in which the color of each spot
        is keyed to a particular attribute, with appropriate
        rescaling for the different magnitudes of each attribute.

        args
        ----
            attrib  :   str, a column in self.locs

        returns
        -------
            dict, parameters to pass to ScatterPlotItem.setData

        """
        # Current axis attributes
        c0, c1 = self.get_pars()

        # Sort by ascending attribute
        self.locs = self.locs.sort_values(by=attrib)
        XY = np.asarray(self.locs[[c0, c1]])
        Z = np.asarray(self.locs[attrib])

        # Filter out unsanitary values
        sanitary = (~np.isnan(Z)) & (~np.isinf(Z))
        if (~sanitary).any():
            print("Filtering out unsanitary values in %s" % attrib)
        XY = XY[sanitary, :]
        Z = Z[sanitary]

        # Log scale
        if self.log_scale_mode:
            Z[Z <= 0.0] = 0.001
            Z = np.log(Z) / np.log(2.0)

        # Bin localizations by their relative value in
        # the color attribute
        norm = 400.0
        try:
            binsize = (np.percentile(Z, 95) - np.percentile(Z, 5)) / norm
            binedges = np.arange(np.percentile(Z, 0.01),
                                 np.percentile(Z, 99.9), binsize)
            Z[Z < binedges.min()] = binedges.min()
            Z[Z >= binedges.max()] = binedges.max() - binsize
            assignments = np.digitize(Z, bins=binedges).astype('float64')
        except ValueError:
            print("Can't generate a color scale for %s; " \
                "try turning off log scale" % attrib)
            return []

        # Scale into the available color indices
        spot_colors = self.cmap_hex[((assignments * (self.n_colors-1)) / \
            assignments.max()).astype(np.int64)]

        # Generate the argument to ScatterPlotItem.setData
        spots = [{'pos': XY[i,:], 'pen': {'color': spot_colors[i], 'width': 3.0}} \
            for i in range(XY.shape[0])]
        return spots

    ## WIDGET CALLBACKS

    def M_par_callback(self):
        """
        Change the attribute mapped to the axes.

        """
        self.update_scatter(rescale=True)

    def M_color_by_callback(self):
        """
        Change which attribute is used to generate the color
        scheme for the localizations.

        """
        self.update_scatter(rescale=False)

    def M_density_window_callback(self):
        """
        Change the size of the window used to determine the local
        density of each localization in the current plane.

        """
        if self.M_color_by.currentText() == "density":
            self.update_scatter(rescale=False)

    def B_simple_callback(self):
        """
        Change into simple plotting mode, which uses less memory
        to render the plot but doesn't support color.

        """
        self.simple_mode = not self.simple_mode
        self.update_scatter(rescale=False)

    def B_log_callback(self):
        """
        Toggle between log and linear scales for the color map.

        """
        self.log_scale_mode = not self.log_scale_mode
        print("Log scale: ", self.log_scale_mode)
        self.update_scatter(rescale=False)
예제 #20
0
 def handlePlotClearButton(self):
     self.plot = ScatterPlotItem()
     self.plot_widget.clear()
     self.plot_widget.addItem(self.plot)
     self.plot_widget.update()
예제 #21
0
class DataInspectorLine(InfiniteLine):
    """
    DataInspectorLine provides a moveable vertical line item that shows labels
    containing the coordinates of the points of existing curves it touches.
    It provides a method to attach it to a PlotItem.

    .. todo:: for now it only works on the main viewbox.
    """

    # TODO: support more than 1 viewbox (e.g. y2axis).
    # TODO: modify anchor of labels so that they are plotted on the left if
    #       they do not fit in the view

    def __init__(
        self,
        date_format="%Y-%m-%d %H:%M:%S",
        y_format="%0.4f",
        trigger_point_size=10,
    ):
        super(DataInspectorLine, self).__init__(angle=90, movable=True)
        self._labels = []
        self._plot_item = None

        self.y_format = y_format
        self.trigger_point_size = trigger_point_size
        self.date_format = date_format
        self._label_style = "background-color: #35393C;"
        self.sigPositionChanged.connect(self._inspect)
        self._highlights = ScatterPlotItem(
            pos=(),
            symbol="s",
            brush="35393C88",
            pxMode=True,
            size=trigger_point_size,
        )
        # hack to make the CurvesPropertiesTool ignore the highlight points
        self._highlights._UImodifiable = False

    def _inspect(self):
        """
        Slot to re inspector line movemethe mouse move event, and perform
        the action on the plot.

        :param evt: mouse event
        """
        xpos = self.pos().x()
        x_px_size, _ = self.getViewBox().viewPixelSize()

        self._removeLabels()
        points = []
        # iterate over the existing curves
        for c in self._plot_item.curves:
            if c is self._highlights:
                continue
            if c.xData is not None:
                # find the index of the closest point of this curve
                adiff = numpy.abs(c.xData - xpos)
                idx = numpy.argmin(adiff)
                # only add a label if the line touches the symbol
                tolerance = 0.5 * max(1, c.opts["symbolSize"]) * x_px_size
                if adiff[idx] < tolerance:
                    points.append((c.xData[idx], c.yData[idx]))

        self._createLabels(points)

    def _createLabels(self, points):
        for x, y in points:
            # create label at x,y
            _x = self._getXValue(x)
            _y = self._getYValue(y)
            text_item = TextItem()
            text_item.setPos(x, y)
            text_item.setHtml(("<div style='{}'> " + "<span><b>x=</b>{} " +
                               "<span><b>y=</b>{}</span> " + "</div>").format(
                                   self._label_style, _x, _y))
            self._labels.append(text_item)
            self._plot_item.addItem(text_item, ignoreBounds=True)
        # Add "highlight" marker at each point
        self._highlights.setData(pos=points)

    def _getXValue(self, x):
        """
        Helper method converting x value to time if necessary

        :param x: current x value
        :return: time or normal x value (depends of the x axis type)
        """
        x_axis = self._plot_item.getAxis("bottom")
        if isinstance(x_axis, DateAxisItem):
            return self._timestampToDateTime(x)
        else:
            return x

    def _getYValue(self, y):
        return str(self.y_format % y)

    def _timestampToDateTime(self, timestamp):
        """
        Method used to caste the timestamp from the curve to date
        in proper format (%Y-%m-%d %H:%M:%S)

        :param timestamp: selected timestamp from curve
        """
        return datetime.utcfromtimestamp(timestamp).strftime(self.date_format)

    def _removeLabels(self):
        # remove existing texts
        for item in self._labels:
            self.getViewBox().removeItem(item)
        self._labels = []
        # remove existing highlights
        self._highlights.setData(pos=())

    def attachToPlotItem(self, plot_item):
        """
        Method to attach :class:`DataInspectorLine` to the plot

        :param plot: to attach
        """
        self._plot_item = plot_item
        self._plot_item.addItem(self, ignoreBounds=True)
        self._plot_item.addItem(self._highlights, ignoreBounds=True)

    def dettach(self):
        """
        Method use to detach the class:`DataInspectorLine` from the plot
        """
        self._removeLabels()
        self._plot_item.removeItem(self._highlights)
        self._plot_item.removeItem(self)
        self._plot_item = None
class CentroidScatterPlotWidget(QWidget, Ui_ScatterPlot):
    """This class handles managing the centroid scatter plot and the histogram
       projections for both x and y coordinates.

    Attributes
    ----------
    brushColor : tuple
        The common color for all brushes in the widget.
    brushes : list
        A set of brushes with varying alpha.
    dataCounter : int
        Number of times data arrays have been appended to up until array size.
    dataSize : int
        The requested size of the data arrays.
    histogramFillBrush : QtGui.QBrush
        Brush used to fill the projection histograms.
    maxAlpha : int
        The maximum transparency value for a brush
    minAlpha : int
        The minimum transparency value for a brush.
    numBins : int
        Number of histogram bins for the projections.
    penColor : tuple
        The outline color for all the points.
    pointBrush : QtGui.QBrush
        The brush for the scatter plot points.
    pointPen : QtGui.QPen
        The outline pen for the scatter plot points.
    rollArray : bool
        Flag as to when to start rolling the data arrays of centroid values.
    scatterPlotItem : pyqtgraph.ScatterPlotItem
        Instance of the centroid scatter plot.
    xData : numpy.array
        Container for the x coordinate of the centroid.
    xHistogramItem : pyqtgraph.PlotItem
        Instance of the x coordinate histogram projection.
    yData : numpy.array
        Container for the y coordinate of the centroid.
    yHistogramItem : pyqtgraph.PlotItem
        Instance of the y coordinate histogram projection.
    """
    def __init__(self, parent=None):
        """Initialize the class.

        Parameters
        ----------
        parent : None, optional
            Top-level widget.
        """
        super().__init__(parent)
        self.setupUi(self)

        p1 = self.scatterPlot.addPlot()
        self.scatterPlotItem = ScatterPlotItem()
        p1.addItem(self.scatterPlotItem)
        p1.setLabel('left', 'Y', units='pixel')
        p1.setLabel('bottom', 'X', units='pixel')

        self.yHistogramItem = None
        self.xHistogramItem = None
        self.numBins = 40

        self.dataSize = None
        self.xData = None
        self.yData = None
        self.rollArray = False
        self.dataCounter = 0
        self.brushes = None
        self.brushColor = (159, 159, 159)
        self.penColor = (255, 255, 255)
        self.maxAlpha = 255
        self.minAlpha = 127
        self.histogramFillBrush = mkBrush(*self.brushColor, 200)
        self.pointBrush = mkBrush(*self.brushColor, self.maxAlpha)
        self.pointPen = mkPen(*self.penColor)
        self.scatterPlotItem.setBrush(self.pointBrush)

    def clearPlot(self):
        """Reset all data and clear the plot.
        """
        self.rollArray = False
        self.dataCounter = 0
        self.xData = np.array([])
        self.yData = np.array([])
        self.scatterPlotItem.setData(self.xData, self.yData)
        self.xHistogramItem.setData([], [], stepMode=False)
        self.yHistogramItem.setData([], [], stepMode=False)
        self.scatterPlotItem.getViewBox().setRange(xRange=(0, 1),
                                                   yRange=(0, 1),
                                                   disableAutoRange=False)
        self.xHistogramItem.getViewBox().setRange(xRange=(0, 1),
                                                  yRange=(0, 1),
                                                  disableAutoRange=False)
        self.yHistogramItem.getViewBox().setRange(xRange=(0, 1),
                                                  yRange=(0, 1),
                                                  disableAutoRange=False)

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

        Returns
        -------
        int
            The set of current configuration parameters.
        """
        return self.numBins

    def makeBrushes(self):
        """Make brushes for spots with differnet alpha factors.
        """
        self.brushes = []
        deltaAlpha = self.maxAlpha - self.minAlpha
        slope = deltaAlpha / (self.dataSize - 1)
        for i in range(self.dataSize):
            alpha = slope * i + self.minAlpha
            self.brushes.append(mkBrush(*self.brushColor, int(alpha)))
            #c = int(alpha)
            #self.brushes.append(mkBrush(c, c, c, self.maxAlpha))

    def setArraySize(self, arraySize):
        """Update the stored array size and adjust arrays.

        Parameters
        ----------
        arraySize : int
            The new array size to use.
        """
        self.dataSize = arraySize
        self.xData = np.array([])
        self.yData = np.array([])
        self.makeBrushes()
        self.rollArray = False

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

        Parameters
        ----------
        config : `config.CentroidPlotConfig`
            The new parameters to apply.
        """
        self.numBins = config.numHistogramBins

    def setup(self, arraySize):
        """Provide information for setting up the plot.

        Parameters
        ----------
        arraySize : int
            The size for the plot data arrays.
        """
        self.dataSize = arraySize
        self.xData = np.array([])
        self.yData = np.array([])
        self.makeBrushes()
        p1 = self.yHistogram.addPlot()
        self.yHistogramItem = p1.plot(self.yData, self.yData)
        self.yHistogramItem.setRotation(90)
        p2 = self.xHistogram.addPlot()
        self.xHistogramItem = p2.plot(self.xData, self.xData)

    def updateData(self, centroidX, centroidY):
        """Update the data arrays with a new centroid coordinate pair.

        Parameters
        ----------
        centroidX : float
            The current x coordinate of the centroid to plot.
        centroidY : float
            The current y coordinate of the centroid to plot.
        """
        if self.rollArray:
            self.xData[:-1] = self.xData[1:]
            self.xData[-1] = centroidX
            self.yData[:-1] = self.yData[1:]
            self.yData[-1] = centroidY
        else:
            # This does create copies of arrays, so watch performance.
            self.xData = np.append(self.xData, centroidX)
            self.yData = np.append(self.yData, centroidY)

        if self.dataCounter < self.dataSize:
            self.dataCounter += 1
            if self.dataCounter == self.dataSize:
                self.rollArray = True

    def showPlot(self):
        """Show the scatter and histogram plots.
        """
        self.scatterPlotItem.setData(self.xData,
                                     self.yData,
                                     pen=self.pointPen,
                                     brush=self.brushes)

        xy, xx = np.histogram(self.xData,
                              bins=np.linspace(np.min(self.xData),
                                               np.max(self.xData),
                                               self.numBins))
        self.xHistogramItem.setData(xx,
                                    xy,
                                    stepMode=True,
                                    fillLevel=0,
                                    fillBrush=self.histogramFillBrush)
        yy, yx = np.histogram(self.yData,
                              bins=np.linspace(np.min(self.yData),
                                               np.max(self.yData),
                                               self.numBins))
        # Flip due to rotated plot
        yy *= -1
        self.yHistogramItem.setData(yx,
                                    yy,
                                    stepMode=True,
                                    fillLevel=0,
                                    fillBrush=self.histogramFillBrush)
예제 #23
0
class AppWidget(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.graph_types = [Lattice_2d, Triangles_2d, Honeycomb_2d, Lattice_3d]
        self.graph_names = [
            "2D-Grid", "2D-Triangles", "2D-Honeycomb", "3D-Grid"
        ]

        self.setupLayout()
        self.setSettings()

    def setSettings(self):
        self.graph_type_combobox.setCurrentIndex(0)
        self.p_slider.setValue(100)
        self.N_slider.setValue(5)
        self.refresh_edges_checkbox.setCheckState(Qt.Checked)

    def setupLayout(self):
        self.layout = QHBoxLayout(self)
        self.splitter = QSplitter(Qt.Horizontal)

        self.settingswidget = QWidget(self)
        self.settingswidget.setMaximumWidth(400)
        self.settingslayout = QVBoxLayout()

        self.percolationWidget = PercolationWidget()

        self.graph_type_combobox = QComboBox(self)
        self.graph_type_combobox.addItems(self.graph_names)
        self.graph_type_combobox.currentIndexChanged.connect(
            self.handleGraphTypeChanged)
        self.graph_type_combobox.setFocusPolicy(Qt.NoFocus)
        self.settingslayout.addWidget(self.graph_type_combobox)

        self.p_slider_layout = QHBoxLayout()
        self.settingslayout.addLayout(self.p_slider_layout)

        self.p_slider_layout.addWidget(QLabel("Set p value: ", self))

        self.p_slider_label = QLabel(self)

        self.p_slider = QSlider(Qt.Horizontal, self)
        self.p_slider.valueChanged.connect(self.handlePSliderChange)

        self.p_slider_layout.addWidget(self.p_slider)
        self.p_slider_layout.addWidget(self.p_slider_label)

        self.N_slider_layout = QHBoxLayout()
        self.settingslayout.addLayout(self.N_slider_layout)

        self.N_slider_layout.addWidget(QLabel("Set size (N): ", self))

        self.N_slider_label = QLabel(self)

        self.N_slider = QSlider(Qt.Horizontal, self)
        self.N_slider.valueChanged.connect(self.handleNSliderChange)

        self.N_slider_layout.addWidget(self.N_slider)
        self.N_slider_layout.addWidget(self.N_slider_label)

        # wether to refresh edges each time or keep edges to add to/remove from
        self.refresh_edges_checkbox = QCheckBox(
            "Refresh Edges instead of Adding/Removing", self)
        self.refresh_edges_checkbox.stateChanged.connect(
            self.handleRefreshCheckboxChange)

        self.settingslayout.addWidget(self.refresh_edges_checkbox)

        self.settingslayout.addSpacerItem(
            QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))

        self.plot_widget = PlotWidget(name="clustersize by p")
        self.settingslayout.addWidget(self.plot_widget)
        self.plot_widget.setLabel('left', 'maximum cluster size', units='%')
        self.plot_widget.setLabel('bottom', 'p', units='')
        self.plot_widget.setXRange(0, 1)
        self.plot_widget.setYRange(0, 1)
        self.plot = ScatterPlotItem()
        self.plot_widget.addItem(self.plot)

        self.plot_clear_button = QPushButton("Clear plot")
        self.plot_clear_button.clicked.connect(self.handlePlotClearButton)
        self.settingslayout.addWidget(self.plot_clear_button)

        self.settingswidget.setLayout(self.settingslayout)
        self.settingswidget.setMinimumSize(50, 50)

        self.splitter.addWidget(self.settingswidget)
        self.splitter.addWidget(self.percolationWidget)

        self.layout.addWidget(self.splitter)

        self.percolationWidget.graphUpdated.connect(self.handleGraphUpdated)

        self.setWindowTitle('Percolation Model')

        self.setFocus()

    def handleGraphTypeChanged(self, index):
        if (self.graph_types[index].dim == 3):
            self.N_slider.setValue(10)
        self.graph = self.graph_types[index](self.N_slider.value())
        self.percolationWidget.setGraph(self.graph)
        self.setFocus()

    def handlePSliderChange(self, value):
        self.percolationWidget.setPValue(value)
        self.p_slider_label.setText(str(value) + " %")

    def handleNSliderChange(self, value):
        self.graph = self.graph_types[self.graph_type_combobox.currentIndex()](
            value)
        self.percolationWidget.setGraph(self.graph)
        self.N_slider_label.setText(str(value))

    def handleRefreshCheckboxChange(self, value):
        self.percolationWidget.setRefresh(value == Qt.Checked)

    def handleGraphUpdated(self, p, clustersize):
        self.plot.addPoints(x=[p], y=[clustersize])

    def handlePlotClearButton(self):
        self.plot = ScatterPlotItem()
        self.plot_widget.clear()
        self.plot_widget.addItem(self.plot)
        self.plot_widget.update()

    def keyPressEvent(self, event):
        if event.key() in [
                Qt.Key_W, Qt.Key_A, Qt.Key_S, Qt.Key_D, Qt.Key_Q, Qt.Key_E,
                Qt.Key_Plus, Qt.Key_Minus
        ]:
            self.percolationWidget.graphwidget.keyPressEvent(event)
        elif event.key() == Qt.Key_Tab:
            self.setFocus()
        else:
            QWidget.keyPressEvent(self, event)
예제 #24
0
    def initUI(self):
        """
        Initialize the user interface.

        """
        # Main window
        self.win = QWidget()
        L = QGridLayout(self.win)
        self.win.resize(self.gui_size * 1.8, self.gui_size)

        # Two subwindows: one on the left for the scatter plot
        # and one on the right for widgets
        self.win_left = QWidget(self.win)
        self.win_right = QWidget(self.win)
        L.addWidget(self.win_left, 0, 0, 1, 3)
        L.addWidget(self.win_right, 0, 3, 1, 1)
        L_left = QGridLayout(self.win_left)
        L_right = QGridLayout(self.win_right)

        # GraphicsLayoutWidget, to organize scatter plot and
        # associated items
        self.graphicsLayoutWidget = GraphicsLayoutWidget(parent=self.win_left)

        # PlotItem, to contain the ScatterPlotItem
        self.plotItem = self.graphicsLayoutWidget.addPlot()
        L_left.addWidget(self.graphicsLayoutWidget, 0, 0)

        # ScatterPlotItem, core data display
        self.scatterPlotItem = ScatterPlotItem(symbol='o',
                                               brush=None,
                                               pxMode=True,
                                               pen={
                                                   'color': '#FFFFFF',
                                                   'width': 4.0
                                               },
                                               size=4.0)
        self.plotItem.addItem(self.scatterPlotItem)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Select parameter to map to the x-axis
        self.M_par_0 = LabeledQComboBox(self.parameters,
                                        "x-parameter",
                                        init_value="x",
                                        parent=self.win_right)
        L_right.addWidget(self.M_par_0, 0, 0, alignment=widget_align)
        self.M_par_0.assign_callback(self.M_par_callback)

        # Select parameter to map to the y-axis
        self.M_par_1 = LabeledQComboBox(self.parameters,
                                        "y-parameter",
                                        init_value="y",
                                        parent=self.win_right)
        L_right.addWidget(self.M_par_1, 1, 0, alignment=widget_align)
        self.M_par_1.assign_callback(self.M_par_callback)

        # Select which attribute to color the localizations by
        options = self.parameters + ["density"]
        self.M_color_by = LabeledQComboBox(options,
                                           "Color by",
                                           init_value="density",
                                           parent=self.win_right)
        L_right.addWidget(self.M_color_by, 0, 1, alignment=widget_align)
        self.M_color_by.assign_callback(self.M_color_by_callback)

        # Select the size of the window to use when computing
        # localization density
        window_size_options = [
            str(j)
            for j in [3, 5, 7, 9, 11, 13, 15, 19, 23, 31, 41, 61, 81, 101]
        ]
        self.M_density_window = LabeledQComboBox(window_size_options,
                                                 "Density window",
                                                 init_value="7",
                                                 parent=self.win_right)
        L_right.addWidget(self.M_density_window, 1, 1, alignment=widget_align)
        self.M_density_window.assign_callback(self.M_density_window_callback)

        # Button to induce a simpler representation that can handle
        # more spots
        self.simple_mode = False
        self.B_simple = QPushButton("Simple scatter", parent=self.win_right)
        L_right.addWidget(self.B_simple, 2, 0, alignment=widget_align)
        self.B_simple.clicked.connect(self.B_simple_callback)

        # Button to toggle log color scaling
        self.log_scale_mode = True
        self.B_log = QPushButton("Log color scale", parent=self.win_right)
        self.B_log.clicked.connect(self.B_log_callback)
        L_right.addWidget(self.B_log, 2, 1, alignment=widget_align)

        # Empty widgets to manipulate the layout
        n_rows = 15
        for j in range(3, n_rows):
            q = QWidget(self.win_right)
            L_right.addWidget(q, j, 0)

        # Show the main window
        self.update_scatter()
        self.win.show()
예제 #25
0
    def __init__(self, manager: BarManager):
        """"""
        ScatterPlotItem.__init__(self)
        super(CandleItem,self).__init__(manager)

        self.orders : Dict[int,Dict[str,OrderData]] = {} # {ix:{orderid:order}}
예제 #26
0
class CrystfelImage(QObject):
    def __init__(self, imodel, iview, geom, parent=None):
        QObject.__init__(self, parent)
        self.imodel = imodel
        self.dmodel = imodel.model
        self.iview = iview
        self.iview.ui.menuBtn.hide()
        self.iview.ui.roiBtn.hide()

        # Thanks CXIVIEW, Anton & Valerio
        pos = np.array([0.0, 0.5, 1.0])
        color = np.array(
            [[255, 255, 255, 255], [128, 128, 128, 255], [0, 0, 0, 255]],
            dtype=np.ubyte)
        new_color_map = ColorMap(pos, color)
        self.iview.ui.histogram.gradient.setColorMap(new_color_map)

        self.lastrow = -1
        self.curFileName = None
        self.curFile = None
        self.canDraw = self.dmodel.canSaveLst()
        self.checkEvent = "event" in self.dmodel.cols
        self.yxmap = None
        self.slab_shape = None
        self.img_shape = None
        self.geom_coffset = 0
        self.geom_pixsize = None
        self.im_out = None
        self.resolutionLambda = None
        self.pixRadiusToRes = None
        self.hkl = None
        self.hklLookup = None
        self.imodel.dataChanged.connect(self.draw)

        self.iview.view.menu.addSeparator()
        openGeomAct = self.iview.view.menu.addAction("Load CrystFEL Geometry")
        openGeomAct.triggered.connect(self.openGeom)

        peakmenu = self.iview.view.menu.addMenu("Peaks")
        self.peakActionGroup = QActionGroup(self)
        self.peakActionGroup.setExclusive(True)
        self.peakActionGroup.triggered.connect(self.drawPeaks)

        peakNone = peakmenu.addAction("None")
        peakNone.setCheckable(True)
        peakNone.setChecked(True)
        peakNone.setData(0)
        self.peakActionGroup.addAction(peakNone)

        peakCXI = peakmenu.addAction("CXI")
        peakCXI.setCheckable(True)
        peakCXI.setEnabled(self.dmodel.canSaveLst())
        peakCXI.setData(1)
        self.peakActionGroup.addAction(peakCXI)

        peakStream = peakmenu.addAction("Stream")
        peakStream.setCheckable(True)
        peakStream.setEnabled(self.dmodel.hasStreamPeaks())
        peakStream.setData(2)
        self.peakActionGroup.addAction(peakStream)
        self.peakCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.peakCanvas)

        reflectionmenu = self.iview.view.menu.addMenu("Reflections")
        self.reflectionActionGroup = QActionGroup(self)
        self.reflectionActionGroup.setExclusive(True)
        self.reflectionActionGroup.triggered.connect(self.drawReflections)

        refNone = reflectionmenu.addAction("None")
        refNone.setCheckable(True)
        refNone.setChecked(True)
        refNone.setData(0)
        self.reflectionActionGroup.addAction(refNone)

        refStream = reflectionmenu.addAction("Stream")
        refStream.setCheckable(True)
        refStream.setEnabled(self.dmodel.hasStreamReflections())
        refStream.setData(1)
        self.reflectionActionGroup.addAction(refStream)
        self.reflectionCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.reflectionCanvas)

        self.drawResRingsAct = self.iview.view.menu.addAction(
            "Resolution Rings")
        self.drawResRingsAct.setCheckable(True)
        self.drawResRingsAct.setChecked(False)
        self.drawResRingsAct.triggered.connect(self.drawResRings)
        self.drawResRingsAct.setEnabled(self.yxmap is not None)
        self.resolutionRingsCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionRingsCanvas)
        self.resRingsTextItems = []
        for x in self.dmodel.cfg.viewerResolutionRingsAngstroms:
            self.resRingsTextItems.append(TextItem('', anchor=(0.5, 0.8)))
            self.iview.getView().addItem(self.resRingsTextItems[-1])

        self.drawResolutionLimitAct = self.iview.view.menu.addAction(
            "Resolution Limit Ring")
        self.drawResolutionLimitAct.setCheckable(True)
        self.drawResolutionLimitAct.setChecked(False)
        self.drawResolutionLimitAct.triggered.connect(self.drawResLimitRing)
        self.drawResolutionLimitAct.setEnabled(
            self.yxmap is not None and 'reslim' in self.dmodel.cols)
        self.resolutionLimitCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionLimitCanvas)

        if geom is not None:
            self.loadGeom(geom)

        self.toolTipsAct = self.iview.view.menu.addAction(
            "Show Position in Tool Tip")
        self.toolTipsAct.setCheckable(True)
        self.toolTipsAct.setChecked(True)
        self.iview.scene.sigMouseMoved.connect(self.mouseMove)

        self.draw()

    def draw(self):
        if not self.canDraw or self.imodel.currow == self.lastrow:
            return

        # Load the image
        image = self.image()
        if image is None:
            return  # Problem with file, don't try to load anything else

        # Image was loaded, so update what we've currently got drawn
        self.lastrow = self.imodel.currow

        # Apply geometry
        if self.yxmap is None:
            image = np.transpose(image)
        else:
            image = cfel_geom.apply_geometry_from_pixel_maps(
                image, self.yxmap, self.im_out)
        self.iview.setImage(image, autoLevels=False, autoRange=False)

        # CXI View Scaling
        bottom, top = histogram_clip_levels(
            image.ravel(), self.dmodel.cfg.cxiviewHistClipLevelValue)
        self.iview.setLevels(bottom, top)
        self.iview.getHistogramWidget().setHistogramRange(
            min(bottom, self.dmodel.cfg.cxiviewHistMin),
            max(top, self.dmodel.cfg.cxiviewHistMax),
            padding=self.dmodel.cfg.cxiviewHistPadding)

        self.calcResLambda()
        self.drawPeaks()
        self.drawReflections()
        self.drawResRings()
        self.drawResLimitRing()

    def loadGeom(self, filename):
        self.yxmap, self.slab_shape, self.img_shape = cfel_geom.pixel_maps_for_image_view(
            filename)
        self.im_out = np.zeros(self.img_shape, dtype=np.dtype(float))
        self.geom_coffset = cfel_geom.coffset_from_geometry_file(filename)
        self.geom_pixsize = 1 / cfel_geom.res_from_geometry_file(filename)
        self.drawResRingsAct.setEnabled(True)
        self.drawResolutionLimitAct.setEnabled('reslim' in self.dmodel.cols)
        self.lastrow = -1
        self.draw()

    def openGeom(self):
        name = QFileDialog.getOpenFileName(
            self.iview,
            'Select CrystFEL Geomery File (.geom)',
            filter='*.geom')
        if qt5:
            if name:
                self.loadGeom(name[0])
        elif name is not None and len(name):
            self.loadGeom(name)

    def mouseMove(self, pos):
        mapped = self.iview.getView().mapSceneToView(pos)
        txt = ""
        if self.toolTipsAct.isChecked() and self.iview.image is not None \
                                        and mapped.x() >= 0 and mapped.x() < self.iview.image.shape[0] \
                                        and mapped.y() >= 0 and mapped.y() < self.iview.image.shape[1]:
            if self.img_shape is not None:
                x = mapped.x() - self.img_shape[0] / 2
                y = self.img_shape[1] / 2 - mapped.y()
            else:
                x = mapped.x()
                y = mapped.y()
            txt = "x: %i, y: %i" % (x, y)
            if self.iview.image is not None:
                txt += " value: %.2f" % self.iview.image[int(mapped.x()),
                                                         int(mapped.y())]
            if self.pixRadiusToRes is not None:
                txt += " resolution: %.2f" % self.pixRadiusToRes(
                    np.sqrt(x**2 + y**2))
            if self.hklLookup is not None:
                res = self.hklLookup.query([mapped.x(), mapped.y()], k=1)
                if len(res
                       ) > 0 and res[0] < self.dmodel.cfg.viewerReflectionSize:
                    txt += " hkl: %s" % self.hkl[res[1]]
        self.iview.setToolTip(txt)

    # Sub functions called by draw, not meant to be called externally
    def fromMaybeEvent(self, paths):
        r = None
        for path in paths:
            if path in self.curFile:
                if self.checkEvent:
                    e = self.dmodel.data["event"][self.imodel.currow]
                    if e == -1:  # No event
                        r = self.curFile[path]
                    else:
                        r = self.curFile[path][e]
                else:  # No events
                    r = self.curFile[path]
                break
        return r

    def image(self):
        image = None
        ifile = self.dmodel.value("ifile", self.imodel.currow, False)
        if ifile != self.curFileName:
            if self.curFile is not None:
                self.curFile.close()
            self.curFileName = ifile
            self.curFile = h5py.File(ifile, 'r')
        image = self.fromMaybeEvent(self.dmodel.cfg.imageH5paths)
        return image

    def cxipeaks(self):
        # Since this function should ideally only be called internally from draw,
        # after image has been successfully generated, assume we have correct and valid ifile.
        px = self.fromMaybeEvent(self.dmodel.cfg.peakXH5paths)
        if px is None:
            px = []
            py = []
        else:
            if len(px.shape) > 1:
                # Data is two dimension, assume x is first column and y second
                py = px[:, 1]
                px = px[:, 0]
            else:
                py = self.fromMaybeEvent(self.dmodel.cfg.peakYH5paths)
                if py is None:
                    px = []
                    py = []
        return px, py

    def drawPeaks(self):
        px = []
        py = []
        if self.peakActionGroup.checkedAction().data() == 1:  # H5 Peaks
            px, py = self.cxipeaks()
        elif self.peakActionGroup.checkedAction().data() == 2:  # Stream peaks
            px, py = self.dmodel.streamPeaks(self.imodel.currow)

        if self.yxmap is not None and len(px):
            px = np.array(px, dtype=np.dtype(int))
            py = np.array(py, dtype=np.dtype(int))
            slab = py * self.slab_shape[1] + px
            px = self.yxmap[0][slab]
            py = self.yxmap[1][slab]
        self.peakCanvas.setData(px,py,symbol=self.dmodel.cfg.viewerPeakSymbol,\
            size=self.dmodel.cfg.viewerPeakSize,pen=\
            mkPen(self.dmodel.cfg.viewerPeakColor,width=self.dmodel.cfg.viewerPeakPenWidth),\
            brush=(0,0,0,0),pxMode=False)

    def drawReflections(self):
        px = []
        py = []
        self.hkl = None
        self.hklLookup = None
        if self.reflectionActionGroup.checkedAction().data() == 1:  # Stream
            px, py, self.hkl = self.dmodel.streamReflections(
                self.imodel.currow)

        if self.yxmap is not None and len(px):
            px = np.array(px, dtype=np.dtype(int))
            py = np.array(py, dtype=np.dtype(int))
            slab = py * self.slab_shape[1] + px
            px = self.yxmap[0][slab]
            py = self.yxmap[1][slab]
        self.reflectionCanvas.setData(px,py,symbol=self.dmodel.cfg.viewerReflectionSymbol,\
            size=self.dmodel.cfg.viewerReflectionSize,pen=\
            mkPen(self.dmodel.cfg.viewerReflectionColor,width=self.dmodel.cfg.viewerReflectionPenWidth),\
            brush=(0,0,0,0),pxMode=False)
        if self.hkl is not None:
            self.hklLookup = scipy.spatial.cKDTree(np.dstack((px, py))[0])

    def calcResLambda(self):
        self.resolutionLambda = None

        photon_ev = None
        if "phoen" in self.dmodel.cols:
            photon_ev = self.dmodel.data["phoen"][self.imodel.currow]
        if photon_ev is None or photon_ev <= 0:
            photon_ev = self.fromMaybeEvent(
                self.dmodel.cfg.viewerPhotonEvH5Paths)

        clen = None
        if "aclen" in self.dmodel.cols:
            clen = self.dmodel.data["aclen"][
                self.imodel.currow]  # Value is corrected with coffset
        if clen is None:
            clen = self.fromMaybeEvent(
                self.dmodel.cfg.viewerCameraLengthH5Paths)
            if clen is not None:  # Uncorrected, correct here
                clen += self.geom_coffset

        # Need valid photon energy, camera length, and geometry (yxmap)
        if photon_ev is None or photon_ev <= 0 or clen is None or self.yxmap is None:
            return  # Not enough info

        lmbd = scipy.constants.h * scipy.constants.c / (scipy.constants.e *
                                                        photon_ev)
        self.resolutionLambda=lambda r : (2.0/self.geom_pixsize)*(clen)*np.tan(2.0*np.arcsin(lmbd / \
                    (2.0 * r *1e-10)))
        self.pixRadiusToRes = lambda r: 1e10 * lmbd / (2.0 * np.sin(
            0.5 * np.arctan(r * self.geom_pixsize / clen)))

    def drawResRings(self):
        if not self.drawResRingsAct.isChecked(
        ) or self.resolutionLambda is None:
            self.resolutionRingsCanvas.setData([], [])
            for ti in self.resRingsTextItems:
                ti.setText('')
        else:
            ring_sizes = self.resolutionLambda(
                np.array(self.dmodel.cfg.viewerResolutionRingsAngstroms))
            self.resolutionRingsCanvas.setData([self.img_shape[0]/2]*len(ring_sizes), [self.img_shape[1]/2]*len(ring_sizes),symbol='o',\
                size=ring_sizes,pen=mkPen(self.dmodel.cfg.viewerResRingColor, width=self.dmodel.cfg.viewerResRingWidth),\
                brush=(0,0,0,0),pxMode=False)
            for i, ti in enumerate(self.resRingsTextItems):
                ti.setText("%.1f A" %
                           self.dmodel.cfg.viewerResolutionRingsAngstroms[i],
                           color=self.dmodel.cfg.viewerResRingColor)
                ti.setPos(self.img_shape[0] / 2,
                          self.img_shape[1] / 2 + ring_sizes[i] / 2)

    def drawResLimitRing(self):
        if not self.drawResolutionLimitAct.isChecked(
        ) or self.resolutionLambda is None:
            self.resolutionLimitCanvas.setData([], [])
        else:
            r = self.dmodel.data['reslim'][self.imodel.currow]
            if r <= 0:
                self.resolutionLimitCanvas.setData([], [])
            else:
                r = 10 / r
                self.resolutionLimitCanvas.setData([self.img_shape[0]/2], [self.img_shape[1]/2],symbol='o',\
                    size=self.resolutionLambda(r),\
                    pen=mkPen(self.dmodel.cfg.viewerResLimitRingColor, width=self.dmodel.cfg.viewerResLimitRingWidth),\
                    brush=(0,0,0,0),pxMode=False)
예제 #27
0
    def initUI(self):
        """
        Initialize the user interface.

        """
        # Main window
        self.win = QWidget()

        # Figure out the GUI size
        self.AR = float(self.imageReader.width) / self.imageReader.height
        self.win.resize(self.gui_size*1.5, self.gui_size)
        L_main = QGridLayout(self.win)

        # A subwindow on the left for widgets, and a subwindow
        # on the right for the image
        self.win_right = QWidget(self.win)
        self.win_left = QWidget(self.win)
        L_right = QGridLayout(self.win_right)
        L_left = QGridLayout(self.win_left)
        L_main.addWidget(self.win_right, 0, 1, 1, 3)
        L_main.addWidget(self.win_left, 0, 0, 1, 1)

        ## IMAGES / OVERLAYS

        # ImageView, for rendering image
        self.imageView = ImageView(parent=self.win_right)
        L_right.addWidget(self.imageView, 0, 0)

        # ScatterPlotItem, for overlaying localizations
        self.scatterPlotItem = ScatterPlotItem()
        self.scatterPlotItem.setParentItem(self.imageView.imageItem)

        # GraphItem, for overlaying trajectory histories when
        # desired
        self.graphItem = GraphItem()
        self.graphItem.setParentItem(self.imageView.imageItem)

        # # Make spots clickable
        self.lastClicked = []
        self.scatterPlotItem.sigClicked.connect(self.spot_clicked)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Frame slider
        self.frame_slider = IntSlider(minimum=0, interval=1, 
            maximum=self.imageReader.n_frames-1, init_value=self.start_frame,
            name='Frame', parent=self.win_left)
        L_left.addWidget(self.frame_slider, 0, 0, rowSpan=2, 
            alignment=widget_align)
        self.frame_slider.assign_callback(self.frame_slider_callback)

        # Button to toggle spot overlays
        self.B_overlay_state = True 
        self.B_overlay = QPushButton("Overlay spots", parent=self.win_left)
        self.B_overlay.clicked.connect(self.B_overlay_callback)
        L_left.addWidget(self.B_overlay, 1, 0, alignment=widget_align)

        # Button to toggle trajectory trails
        self.B_overlay_trails_state = False 
        self.B_overlay_trails = QPushButton("Overlay histories", parent=self.win_left)
        self.B_overlay_trails.clicked.connect(self.B_overlay_trails_callback)
        L_left.addWidget(self.B_overlay_trails, 1, 1, alignment=widget_align)
        self.B_overlay_trails.stackUnder(self.B_overlay)

        # Menu to select current overlay symbol
        symbol_options = keys_to_str(symbol_sizes.keys())
        self.M_symbol = LabeledQComboBox(symbol_options, "Overlay symbol",
            init_value="o", parent=self.win_left)
        self.M_symbol.assign_callback(self.M_symbol_callback)
        L_left.addWidget(self.M_symbol, 2, 0, alignment=widget_align)

        # Menu to select how the spots are colored
        color_by_options = ["None", "Trajectory", "Quantitative attribute", "Boolean attribute"]
        self.M_color_by = LabeledQComboBox(color_by_options, 
            "Color spots by", init_value="None", parent=self.win_left)
        self.M_color_by.assign_callback(self.M_color_by_callback)
        L_left.addWidget(self.M_color_by, 3, 0, alignment=widget_align)

        # Create a new binary spot condition
        self.B_create_condition = QPushButton("Create Boolean attribute", 
            parent=self.win_left)
        self.B_create_condition.clicked.connect(self.B_create_condition_callback)
        L_left.addWidget(self.B_create_condition, 4, 0, alignment=widget_align)

        # Select the binary column that determines color when "condition"
        # is selected as the color-by attribute
        condition_options = [c for c in self.locs.columns if self.locs[c].dtype == 'bool']
        self.M_condition = LabeledQComboBox(condition_options, "Boolean attribute",
            init_value="None", parent=self.win_left)
        L_left.addWidget(self.M_condition, 3, 1, alignment=widget_align)
        self.M_condition.assign_callback(self.M_condition_callback)

        # Compare spots in a separate window that shows a grid of spots
        self.B_compare_spots = QPushButton("Compare spots", parent=self)
        L_left.addWidget(self.B_compare_spots, 2, 1, alignment=widget_align)
        self.B_compare_spots.clicked.connect(self.B_compare_spot_callback)

        # Some placeholder widgets, for better formatting
        for j in range(6, 18):
            q = QLabel(parent=self.win_left)
            L_left.addWidget(q, j, 0, alignment=widget_align)


        ## KEYBOARD SHORTCUTS - tab right/left through frames
        self.left_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Left), self.win)
        self.right_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Right), self.win)
        self.left_shortcut.activated.connect(self.tab_prev_frame)
        self.right_shortcut.activated.connect(self.tab_next_frame)


        ## DISPLAY
        self.update_image(autoRange=True, autoLevels=True, autoHistogramRange=True)
        self.overlay_spots()
        if "trajectory" in self.locs.columns:
            self.M_color_by.setCurrentText("Trajectory")
            self.M_color_by_callback()
        self.win.show()
예제 #28
0
    def render(self, intent):
        if not self.canvas_widget:
            bases_names = getattr(intent, 'mixins', tuple()) or tuple()
            bases = map(
                lambda name: plugin_manager.type_mapping['PlotMixinPlugin'][
                    name], bases_names)
            self.canvas_widget = type('PlotViewBlend',
                                      (*bases, PlotIntentCanvasBlend), {})()
            self.layout().addWidget(self.canvas_widget)
            self.canvas_widget.plotItem.addLegend()

        items = []

        if isinstance(intent, (PlotIntent, ErrorBarIntent, ScatterIntent)):
            x = intent.x
            if intent.x is not None:
                x = np.asarray(intent.x).squeeze()

            ys = np.asarray(intent.y).squeeze()
            if ys.ndim == 1:
                ys = [ys]
                multicurves = False
            else:
                multicurves = True

            symbol = intent.kwargs.get("symbol", None)

            for i in range(len(ys)):
                name = intent.name
                if multicurves:
                    name += f' {i + 1}'

                if isinstance(intent, ScatterIntent):
                    item = ScatterPlotItem(x=x,
                                           y=ys[i],
                                           name=name,
                                           symbol=symbol)
                    self.canvas_widget.plotItem.addItem(item)
                elif isinstance(intent, (PlotIntent, ErrorBarIntent)):
                    item = self.canvas_widget.plot(x=x,
                                                   y=ys[i],
                                                   name=name,
                                                   symbol=symbol)
                items.append(item)

            # Use most recent intent's log mode for the canvas's log mode
            x_log_mode = intent.kwargs.get(
                "xLogMode",
                self.canvas_widget.plotItem.getAxis("bottom").logMode)
            y_log_mode = intent.kwargs.get(
                "yLogMode",
                self.canvas_widget.plotItem.getAxis("left").logMode)
            self.canvas_widget.plotItem.setLogMode(x=x_log_mode, y=y_log_mode)
            self.canvas_widget.setLabels(**intent.labels)

        if isinstance(intent, ErrorBarIntent):
            kwargs = intent.kwargs.copy()
            for key, value in kwargs.items():
                if isinstance(value, DataArray):
                    kwargs[key] = np.asanyarray(value).squeeze()
            erroritem = ErrorBarItem(x=np.asarray(intent.x).squeeze(),
                                     y=np.asarray(intent.y).squeeze(),
                                     **kwargs)
            self.canvas_widget.plotItem.addItem(erroritem)

            items.append(erroritem)

        elif isinstance(intent, BarIntent):
            kwargs = intent.kwargs.copy()
            for key, value in kwargs.items():
                if isinstance(value, DataArray):
                    kwargs[key] = np.asanyarray(value).squeeze()
            if intent.x is not None:
                kwargs['x'] = intent.x
            baritem = pg.BarGraphItem(**kwargs)
            self.canvas_widget.plotItem.addItem(baritem)

            items.append(baritem)

        self.intent_to_items[intent] = items
        self.colorize()
        return items
예제 #29
0
파일: item.py 프로젝트: asciili/vnpy
    def __init__(self, manager: BarManager, am: ArrayManager):
        """"""
        ScatterPlotItem.__init__(self)
        super(ExChartItem, self).__init__(manager, am)

        self.orders: Dict[int, Dict[str, Order]] = {}  # {ix:{orderid:order}}
예제 #30
0
class SpotViewer(QWidget):
    """
    Overlay localizations or trajectories onto a raw movie.
    Useful for QC.

    The user sees a single image window with the current
    frame and some overlaid localizations.

    Each localization is either colored identically or 
    according to a color scheme. Available color schemes are

        - color each localization by its trajectory index 
            (same color for locs in the same trajectory)

        - color each localization by one of its numerical
            attributes (e.g. intensity, BG, H_det, etc.)

        - color each localization one of two colors according
            to its value for a boolean attribute.

    SpotViewer provides the user a way to create new boolean
    attributes by thresholding attributes, via the "Create
    Boolean attribute" button.

    args
    ----
        image_path          :   str, path to image file (e.g.
                                *.nd2, *.tif, or *.tiff)
        locs_path           :   str, path to CSV with localizations
        gui_size            :   int
        start_frame         :   int, the initial frame shown
        parent              :   root QWidget

    """
    def __init__(self, image_path, locs_path, gui_size=800,
        start_frame=0, parent=None):
        super(SpotViewer, self).__init__(parent=parent)
        self.image_path = image_path 
        self.locs_path = locs_path
        self.gui_size = gui_size 
        self.start_frame = start_frame

        self.initData()
        self.initUI()

    def initData(self):
        """
        Load the datasets for this SpotViewer instance.

        """
        # Make sure the required paths exist
        assert os.path.isfile(self.image_path)
        assert os.path.isfile(self.locs_path)
        assert os.path.splitext(self.image_path)[1] in \
            ['.nd2', '.tif', '.tiff'], \
            "Image file must be one of (*.nd2 *.tif *.tiff)"

        # Make an image file reader
        self.imageReader = ImageReader(self.image_path)

        # Load the set of localizations
        self.locs = pd.read_csv(self.locs_path)

        # Some additional options if these localizations are 
        # assigned a trajectory
        if 'trajectory' in self.locs.columns:

            # Assign the traj_len, if it doesn't already exist
            self.locs = traj_length(self.locs)

            # Keep an array of localizations as an ndarray, for
            # fast indexing when overlaying trajectory histories
            self.tracks = np.asarray(
                self.locs.loc[
                    self.locs['traj_len']>1, 
                    ['frame', 'trajectory', 'y', 'x']
                ].sort_values(by=['trajectory', 'frame'])
            )

            # Adjust for off-by-1/2 plot pixel indexing
            self.tracks[:,2:] = self.tracks[:,2:] + 0.5

        # Generate the set of colors
        self.generate_color_schemes()

        # Assign condition colors (initially all negative)
        self.assign_condition_colors('y')

        # Load the first round of spots
        self.load_image(self.start_frame)

        # Assign each trajectory a color
        self.assign_traj_colors()

        # Assign self.locs_curr, the current set of spots
        self.load_spots(self.start_frame)

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

        """
        # Main window
        self.win = QWidget()

        # Figure out the GUI size
        self.AR = float(self.imageReader.width) / self.imageReader.height
        self.win.resize(self.gui_size*1.5, self.gui_size)
        L_main = QGridLayout(self.win)

        # A subwindow on the left for widgets, and a subwindow
        # on the right for the image
        self.win_right = QWidget(self.win)
        self.win_left = QWidget(self.win)
        L_right = QGridLayout(self.win_right)
        L_left = QGridLayout(self.win_left)
        L_main.addWidget(self.win_right, 0, 1, 1, 3)
        L_main.addWidget(self.win_left, 0, 0, 1, 1)

        ## IMAGES / OVERLAYS

        # ImageView, for rendering image
        self.imageView = ImageView(parent=self.win_right)
        L_right.addWidget(self.imageView, 0, 0)

        # ScatterPlotItem, for overlaying localizations
        self.scatterPlotItem = ScatterPlotItem()
        self.scatterPlotItem.setParentItem(self.imageView.imageItem)

        # GraphItem, for overlaying trajectory histories when
        # desired
        self.graphItem = GraphItem()
        self.graphItem.setParentItem(self.imageView.imageItem)

        # # Make spots clickable
        self.lastClicked = []
        self.scatterPlotItem.sigClicked.connect(self.spot_clicked)

        ## WIDGETS
        widget_align = Qt.AlignTop

        # Frame slider
        self.frame_slider = IntSlider(minimum=0, interval=1, 
            maximum=self.imageReader.n_frames-1, init_value=self.start_frame,
            name='Frame', parent=self.win_left)
        L_left.addWidget(self.frame_slider, 0, 0, rowSpan=2, 
            alignment=widget_align)
        self.frame_slider.assign_callback(self.frame_slider_callback)

        # Button to toggle spot overlays
        self.B_overlay_state = True 
        self.B_overlay = QPushButton("Overlay spots", parent=self.win_left)
        self.B_overlay.clicked.connect(self.B_overlay_callback)
        L_left.addWidget(self.B_overlay, 1, 0, alignment=widget_align)

        # Button to toggle trajectory trails
        self.B_overlay_trails_state = False 
        self.B_overlay_trails = QPushButton("Overlay histories", parent=self.win_left)
        self.B_overlay_trails.clicked.connect(self.B_overlay_trails_callback)
        L_left.addWidget(self.B_overlay_trails, 1, 1, alignment=widget_align)
        self.B_overlay_trails.stackUnder(self.B_overlay)

        # Menu to select current overlay symbol
        symbol_options = keys_to_str(symbol_sizes.keys())
        self.M_symbol = LabeledQComboBox(symbol_options, "Overlay symbol",
            init_value="o", parent=self.win_left)
        self.M_symbol.assign_callback(self.M_symbol_callback)
        L_left.addWidget(self.M_symbol, 2, 0, alignment=widget_align)

        # Menu to select how the spots are colored
        color_by_options = ["None", "Trajectory", "Quantitative attribute", "Boolean attribute"]
        self.M_color_by = LabeledQComboBox(color_by_options, 
            "Color spots by", init_value="None", parent=self.win_left)
        self.M_color_by.assign_callback(self.M_color_by_callback)
        L_left.addWidget(self.M_color_by, 3, 0, alignment=widget_align)

        # Create a new binary spot condition
        self.B_create_condition = QPushButton("Create Boolean attribute", 
            parent=self.win_left)
        self.B_create_condition.clicked.connect(self.B_create_condition_callback)
        L_left.addWidget(self.B_create_condition, 4, 0, alignment=widget_align)

        # Select the binary column that determines color when "condition"
        # is selected as the color-by attribute
        condition_options = [c for c in self.locs.columns if self.locs[c].dtype == 'bool']
        self.M_condition = LabeledQComboBox(condition_options, "Boolean attribute",
            init_value="None", parent=self.win_left)
        L_left.addWidget(self.M_condition, 3, 1, alignment=widget_align)
        self.M_condition.assign_callback(self.M_condition_callback)

        # Compare spots in a separate window that shows a grid of spots
        self.B_compare_spots = QPushButton("Compare spots", parent=self)
        L_left.addWidget(self.B_compare_spots, 2, 1, alignment=widget_align)
        self.B_compare_spots.clicked.connect(self.B_compare_spot_callback)

        # Some placeholder widgets, for better formatting
        for j in range(6, 18):
            q = QLabel(parent=self.win_left)
            L_left.addWidget(q, j, 0, alignment=widget_align)


        ## KEYBOARD SHORTCUTS - tab right/left through frames
        self.left_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Left), self.win)
        self.right_shortcut = QShortcut(QKeySequence(QtGui_Qt.Key_Right), self.win)
        self.left_shortcut.activated.connect(self.tab_prev_frame)
        self.right_shortcut.activated.connect(self.tab_next_frame)


        ## DISPLAY
        self.update_image(autoRange=True, autoLevels=True, autoHistogramRange=True)
        self.overlay_spots()
        if "trajectory" in self.locs.columns:
            self.M_color_by.setCurrentText("Trajectory")
            self.M_color_by_callback()
        self.win.show()

    ## CORE FUNCTIONS

    def update_image(self, autoRange=False, autoLevels=False, 
        autoHistogramRange=False):
        """
        Update self.imageView with the current value of self.image
        and change the scatter plots if necessary.

        """
        # Update the image view
        self.imageView.setImage(self.image, autoRange=autoRange, 
            autoLevels=autoLevels, autoHistogramRange=autoHistogramRange)

    def load_spots(self, frame_index=None):
        """
        Load a new set of localizations.

        """
        if frame_index is None:
            frame_index = self.frame_slider.value()

        self.locs_curr = self.locs.loc[self.locs['frame']==frame_index, :]
        self.locs_pos = np.asarray(self.locs_curr[['y', 'x']])
        self.locs_data = self.locs_curr.to_dict(orient='records')

    def load_image(self, frame_index):
        """
        Load a new image.

        """
        self.image = self.imageReader.get_frame(frame_index)

    def change_frame(self, frame_index):
        """
        Load a new frame with a new set of spots.

        """
        # Load the new image
        self.load_image(frame_index)

        # Load the new set of spots
        self.load_spots(frame_index)

    def tab_next_frame(self):
        """
        Load the next frame.

        """
        next_idx = int(self.frame_slider.value())
        if next_idx < self.frame_slider.maximum:
            next_idx += 1
        self.frame_slider.setValue(next_idx)

    def tab_prev_frame(self):
        """
        Load the previous frame.

        """
        prev_idx = int(self.frame_slider.value())
        if prev_idx > self.frame_slider.minimum:
            prev_idx -= 1
        self.frame_slider.setValue(prev_idx)

    def generate_color_spot_dict(self, color_col):
        """
        From the current set of spots, generate an argument
        suitable to update the scatter plot while coloring 
        each localization by its value of *color_col*. Usually
        *color_col* is either "traj_color" or "attrib_color".

        """
        symbol = self.M_symbol.currentText()
        return [{'pos': tuple(self.locs_pos[i,:]+0.5), 'size': symbol_sizes[symbol],
            'symbol': symbol, 'pen': {'color': self.locs_data[i][color_col], "width": pen_width},
            'brush': None} for i in range(self.locs_pos.shape[0])]

    def overlay_spots(self):
        """
        Overlay the current set of spots onto the current image.

        """
        if self.B_overlay_state:

            # Get the current overlay symbol
            symbol = self.M_symbol.currentText()

            # Get the current coloring scheme
            color_by = self.M_color_by.currentText()

            # Overlay the spots
            if color_by == "None":
                self.scatterPlotItem.setData(pos=self.locs_pos+0.5,
                    data=self.locs_data, symbol=symbol, 
                    size=symbol_sizes[symbol], **overlay_params)
            elif color_by == "Trajectory":
                self.scatterPlotItem.setData(
                    spots=self.generate_color_spot_dict("traj_color"),
                    data=self.locs_data
                )
            elif color_by == "Quantitative attribute": 
                self.scatterPlotItem.setData(
                    spots=self.generate_color_spot_dict("attrib_color"),
                    data=self.locs_data,
                )
            elif color_by == "Boolean attribute":
                self.scatterPlotItem.setData(
                    spots=self.generate_color_spot_dict("condition_color"),
                    data=self.locs_data,
                )
        else:
            self.scatterPlotItem.setData([])

        # Also overlay the trajectory histories, if desired
        if self.B_overlay_trails_state:
            self.overlay_trails()

    def generate_color_schemes(self):
        """
        Generate two color schemes for this dataframe.

        """
        self.n_colors = 133

        # Color scheme 1: viridis
        cmap = cm.get_cmap("viridis", self.n_colors)
        self.colors_0 = np.array(
            [mpl_colors.rgb2hex(cmap(i)[:3]) for i in range(self.n_colors)]
        )

        # Color scheme 2: inferno
        cmap = cm.get_cmap("cool", self.n_colors)
        self.colors_1 = np.array(
            [mpl_colors.rgb2hex(cmap(i)[:3]) for i in range(self.n_colors)]
        )

    def assign_traj_colors(self):
        """
        Assign the "traj_color" column of the dataframe, which assign
        each trajectory a different hex color.

        """
        # If the dataframe does not have a "trajectory" column, then
        # all of the spots get the same color
        if not "trajectory" in self.locs.columns:
            print("assign_traj_colors: `trajectory` column not found in dataframe")
            self.locs["traj_color"] = MASTER_COLOR 

        else:
            self.locs["traj_color"] = self.colors_0[
                (self.locs["trajectory"]*9277) % self.n_colors]

            # If a spot is not assigned to a trajectory, its color is white
            unassigned = self.locs["trajectory"] == -1 
            self.locs.loc[unassigned, "traj_color"] = "#FFFFFF"

    def assign_attribute_colors(self, attrib):
        """
        Assign the "color" column of the dataframe, which determines
        how individual spots are colored.

        """
        # Get all of the numerical values for this attribute
        x = np.asarray(self.locs[attrib].astype("float64"))
        sanitary = (~np.isinf(x)) & (~np.isnan(x))
        
        # Min and max values for the color map
        cmin = np.percentile(x, 1)
        cmax = np.percentile(x, 95)

        # Divide up the range into bins for each color
        bins = np.linspace(cmin, cmax, self.n_colors-1)

        # Assign each localization to one bin on the basis
        # of its attribute
        assignments = np.zeros(x.shape[0], dtype='int64')
        assignments[sanitary] = np.digitize(x[sanitary], bins=bins)

        # Generate the color indices
        self.locs["attrib_color"] = self.colors_1[assignments]

        # Unsanitary inputs are colored white
        self.locs.loc[~sanitary, "attrib_color"] = "#FFFFFF"

    def assign_condition_colors(self, condition_col):
        """
        Assign the "condition_color" of the dataframe which is 
        keyed to the truth value of *condition_col*.

        """
        indices = self.locs[condition_col].astype('int64')
        self.locs["condition_color"] = condition_colors[0]
        self.locs.loc[self.locs[condition_col].astype('bool'), 
            "condition_color"] = condition_colors[1]

    def overlay_trails(self):
        """
        For each trajectory running in the current frame, overlay
        that trajectory on the current image.

        Relies on an updated self.locs_curr.

        """
        if len(self.locs_curr) == 0:
            self.graphItem.setData()

        elif "trajectory" in self.locs.columns and \
            self.B_overlay_trails_state:

            # Get the current frame index
            frame_index = self.frame_slider.value()

            # Get all spots before or at the current frame
            T = self.tracks[self.tracks[:,0]<=frame_index, :]

            # Find the minimum trajectory index present in this frame
            t = T[T[:,0]==frame_index, 1].min()

            # Get all trajectories that coincide with this frame
            T = T[np.where(T[:,1]==t)[0][0]:,:]

            # Figure out which points correspond to the same
            # trajectory
            indicator = (T[1:,1] - T[:-1,1]) == 0
            connect_indices = np.where(indicator)[0]
            adj = np.asarray([connect_indices, connect_indices+1]).T 

            # Overlay
            self.graphItem.setData(pos=T[:,2:4], adj=adj,
                pen={'color': MASTER_COLOR, 'width': pen_width}, symbol=None,
                brush=None, pxMode=False)
        else:
            self.graphItem.setData()


    ## WIDGET CALLBACKS

    def spot_clicked(self, plot, points):
        """
        Respond to a spot being selected by the user: change
        its color and print all of its associated information
        to the terminal.

        """
        for p in self.lastClicked:
            p.resetPen()

        for p in points:
            print("Spot:")
            print(format_dict(p.data()))
            p.setPen('w', width=3)
        self.lastClicked = points 

    def frame_slider_callback(self):
        """
        Respond to a user change in the frame slider.

        """
        # Get the new frame
        frame_index = self.frame_slider.value()

        # Load a new image and set of localizations
        self.change_frame(frame_index)

        # Update the image
        self.update_image()

        # Update the scatter plot
        self.overlay_spots()

    def B_overlay_callback(self):
        """
        Toggle the spot overlay.

        """
        self.B_overlay_state = not self.B_overlay_state 
        self.overlay_spots()

    def M_color_by_callback(self):
        """
        Change the way that the spots are colored: either None 
        (all the same color), "trajectory", or "attribute".

        """
        color_by = self.M_color_by.currentText()
        if color_by == "Quantitative attribute":

            # Prompt the user to select an attribute to color by
            options = [c for c in self.locs.columns if \
                self.locs[c].dtype in ["int64", "float64", "uint8", \
                    "uint16", "float32", "uint32", "int32"]]
            ex = SingleComboBoxDialog("Attribute", options, 
                init_value="I0", title="Choose attribute color by",
                parent=self)
            ex.exec_()

            # Dialog accepted
            if ex.result() == 1:
                attrib = ex.return_val 

            # Dialog rejected; default to I0
            else:
                print("rejected; defaulting to I0")
                attrib = "I0"

            # Generate the color indices
            self.assign_attribute_colors(attrib)

            # Update the current set of localizations
            self.load_spots(self.frame_slider.value())

        elif color_by == "Boolean attribute":
            self.assign_attribute_colors(self.M_condition.currentText())
            self.overlay_spots()

        else:
            pass 

        # Update the scatter plot
        self.overlay_spots()

    def M_symbol_callback(self):
        """
        Change the spot overlay symbol.

        """
        self.overlay_spots()

    def B_overlay_trails_callback(self):
        """
        Show the history of each trajectory on the raw image.

        """
        self.B_overlay_trails_state = not self.B_overlay_trails_state 

        # Do nothing if these are not trajectories
        if not "trajectory" in self.locs.columns:
            return 

        # Else overlay the trajectory histories
        self.overlay_trails()

    def B_create_condition_callback(self):
        """
        Launch a sub-GUI to generate a Boolean attribute on 
        from one of the spot columns.

        """
        # Prompt the user to create a condition
        ex = ConditionDialog(self.locs, parent=self)
        ex.exec()

        # Accepted - unpack result
        if ex.result() == 1:
            bounds, col = ex.return_val
            l_bound, u_bound = bounds 

            # Add this as an option 
            condition_name = '%s_condition' % col 
            if condition_name not in self.locs.columns:
                self.M_condition.QComboBox.addItems([condition_name])

            # Create a new binary column on the dataframe
            self.locs[condition_name] = np.logical_and(
                self.locs[col]>=l_bound,
                self.locs[col]<=u_bound,
            )
            print("%d/%d localizations in condition %s" % (
                self.locs[condition_name].sum(),
                len(self.locs),
                condition_name
            ))

            # Update plots, assuming that the user wants to see the 
            # result immediately
            self.M_condition.QComboBox.setCurrentText(condition_name)
            self.M_color_by.QComboBox.setCurrentText("Boolean attribute")
            self.assign_condition_colors(condition_name)
            self.load_spots()
            self.overlay_spots()

    def M_condition_callback(self):
        """
        Select the current Boolean attribute to use for determining
        color when using the "Boolean attribute" option in M_color_by.

        """
        # Get the new condition name
        c = self.M_condition.currentText()

        # Assign each localization a color depending on its value
        # with this value
        self.assign_condition_colors(c)

        # Update the plots
        self.load_spots()
        self.overlay_spots()

    def B_compare_spot_callback(self):
        """
        Launch a subwindow that shows a grid of spots in the current
        frame and subsequent frames.

        This callback has two behaviors, depending on the current 
        state of self.M_color_by. If we're color by a Boolean attribute,
        then launch a ImageSubpositionCompare window. Else launch a 
        ImageSubpositionWindow. The former compares two sets of spots
        side-by-side, while the second is just a single grid of spots.

        """
        if self.M_color_by.currentText() == "Boolean attribute":

            # Size of image grid
            N = 8

            # Get the current Boolean attribute column
            col = self.M_condition.currentText()

            # Get as many spots as we can to fill this image grid
            frame_index = self.frame_slider.value()
            positions_false = []
            positions_true = []
            images = []
            while frame_index<self.imageReader.n_frames and \
                (sum([i.shape[0] for i in positions_false])<N**2 or 
                    (sum([i.shape[0] for i in positions_true])<N**2)):

                im = self.imageReader.get_frame(frame_index)
                frame_locs = self.locs.loc[
                    self.locs['frame']==frame_index,
                    ['y', 'x', col],
                ]
                frame_locs_true = np.asarray(frame_locs.loc[
                    frame_locs[col], ['y', 'x']]).astype(np.int64)
                frame_locs_false = np.asarray(frame_locs.loc[
                    ~frame_locs[col], ['y', 'x']]).astype(np.int64)
                images.append(im)
                positions_true.append(frame_locs_true)
                positions_false.append(frame_locs_false)

                frame_index += 1

            # Launch a ImageSubpositionCompare instance
            ex = ImageSubpositionCompare(images, positions_true, positions_false, 
                w=15, N=N, colors=condition_colors, parent=self)

        else:
            # Size of image grid
            N = 10

            # Get as many spots as we can to fill this image grid
            frame_index = self.frame_slider.value()
            positions = []
            images = []
            while sum([i.shape[0] for i in positions])<N**2 and \
                frame_index<self.imageReader.n_frames:

                im = self.imageReader.get_frame(frame_index)
                frame_locs = np.asarray(self.locs.loc[
                    self.locs['frame']==frame_index, ['y', 'x']]).astype(np.int64)
                images.append(im)
                positions.append(frame_locs)
                frame_index += 1

            # Launch an ImageSubpositionWindow
            ex = ImageSubpositionWindow(images, positions,
                w=15, N=N, parent=self)