class Visualizer(object):
    '''Visualizer class\n
    Base class for the simulation of the swarm behavior.
    Args:
        width (int): defines the width of the window
        height (int): defines the height of the window
    '''
    def __init__(self, width=800, height=800, show_axis=True):
        self.canvas = SceneCanvas(size=(width, height), position=(0,0), keys='interactive', title=self.__class__.__name__)
        self.view = self.canvas.central_widget.add_view()
        self.view.camera = 'turntable'
        if show_axis:
            self.axis = visuals.XYZAxis(parent=self.view.scene)
        self.canvas.show()

    def __bool__(self):
        return not self.canvas._closed

    def update(self):
        vispy.app.process_events()        
        self.canvas.update()
        time.sleep(0.01)

    def animation(self, t):
        self.update()
        return _screenshot((0,0,self.canvas.size[0],self.canvas.size[1]))[:,:,:3]
Beispiel #2
0
class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        box = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight, self)
        self.resize(500, 200)
        self.setLayout(box)

        self.canvas_0 = SceneCanvas(bgcolor='w')
        self.vb_0 = ViewBox(parent=self.canvas_0.scene, bgcolor='r')
        self.vb_0.camera.rect = -1, -1, 2, 2
        self.canvas_0.events.initialize.connect(self.on_init)
        self.canvas_0.events.resize.connect(partial(on_resize,
                                                    self.canvas_0,
                                                    self.vb_0))
        box.addWidget(self.canvas_0.native)

        # pass the context from the first canvas to the second
        self.canvas_1 = SceneCanvas(bgcolor='w', shared=self.canvas_0.context)
        self.vb_1 = ViewBox(parent=self.canvas_1.scene, bgcolor='b')
        self.vb_1.camera.rect = -1, -1, 2, 2
        self.canvas_1.events.resize.connect(partial(on_resize,
                                                    self.canvas_1,
                                                    self.vb_1))
        box.addWidget(self.canvas_1.native)

        self.tick_count = 0
        self.timer = Timer(interval=1., connect=self.on_timer, start=True)
        self.setWindowTitle('Shared contexts')
        self.show()

    def on_init(self, event):
        self.text = Text('Initialized', font_size=40.,
                         anchor_x='left', anchor_y='top',
                         parent=[self.vb_0.scene, self.vb_1.scene])

    def on_timer(self, event):
        self.tick_count += 1
        self.text.text = 'Tick #%s' % self.tick_count
        self.canvas_0.update()
        self.canvas_1.update()

    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_Escape:
            self.close()
        elif event.key() == QtCore.Qt.Key_F11:
            self.showNormal() if self.isFullScreen() else self.showFullScreen()
Beispiel #3
0
class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        box = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight, self)
        self.resize(500, 200)
        self.setLayout(box)

        self.canvas_0 = SceneCanvas(bgcolor='w')
        self.vb_0 = ViewBox(parent=self.canvas_0.scene, bgcolor='r')
        self.vb_0.camera.rect = -1, -1, 2, 2
        self.canvas_0.events.initialize.connect(self.on_init)
        self.canvas_0.events.resize.connect(
            partial(on_resize, self.canvas_0, self.vb_0))
        box.addWidget(self.canvas_0.native)

        # pass the context from the first canvas to the second
        self.canvas_1 = SceneCanvas(bgcolor='w', shared=self.canvas_0.context)
        self.vb_1 = ViewBox(parent=self.canvas_1.scene, bgcolor='b')
        self.vb_1.camera.rect = -1, -1, 2, 2
        self.canvas_1.events.resize.connect(
            partial(on_resize, self.canvas_1, self.vb_1))
        box.addWidget(self.canvas_1.native)

        self.tick_count = 0
        self.timer = Timer(interval=1., connect=self.on_timer, start=True)
        self.setWindowTitle('Shared contexts')
        self.show()

    def on_init(self, event):
        self.text = Text('Initialized',
                         font_size=40.,
                         anchor_x='left',
                         anchor_y='top',
                         parent=[self.vb_0.scene, self.vb_1.scene])

    def on_timer(self, event):
        self.tick_count += 1
        self.text.text = 'Tick #%s' % self.tick_count
        self.canvas_0.update()
        self.canvas_1.update()

    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_Escape:
            self.close()
        elif event.key() == QtCore.Qt.Key_F11:
            self.showNormal() if self.isFullScreen() else self.showFullScreen()
Beispiel #4
0
class VolumeView(BaseQtWidget):

    def __init__(self, _model: VolumeViewModel):

        self._model = _model
        self._model.register(self.update)

        self._canvas = SceneCanvas()

        self._viewbox = ViewBox(parent=self._canvas.scene)
        self._canvas.central_widget.add_widget(self._viewbox)
        self._viewbox.camera = ArcballCamera(fov=0)

        self._atlas_volume = Volume(self._model.atlas_volume, parent=self._viewbox.scene)  # , interpolation='nearest')
        self._atlas_volume.attach(filters.ColorFilter((1., .5, 0., 1.)))
        self._atlas_volume.set_gl_state('additive', depth_test=False)

        self._section_image = Image(parent=self._viewbox.scene, cmap='grays')
        self._section_image.attach(filters.ColorFilter((0., .5, 1., 1.)))
        self._section_image.set_gl_state('additive', depth_test=False)

        self._canvas.events.key_press.connect(lambda event: self._model.press_key(event.key.name))

    @property
    def qt_widget(self) -> QWidget:
        return self._canvas.native

    def update(self, changed: str) -> None:
        render_funs = {
            'atlas_volume': self._render_volume,
            'section_image': self._render_section,
            'section_transform': self._render_section_transform,
            'clim': self._render_section_clim,
            'sections': (lambda: ...),
        }
        render_funs[changed]()

    def _render_section_clim(self) -> None:
        self._section_image.clim = self._model.clim
        self._canvas.update()

    def _render_section_transform(self) -> None:
        self._section_image.transform = MatrixTransform(self._model.section_transform.T)
        self._canvas.update()

    def _render_section(self) -> None:
        self._section_image.set_data(self._model.section_image.T)
        self._canvas.update()

    def _render_volume(self) -> None:
        model = self._model
        self._atlas_volume.set_data(model.atlas_volume.swapaxes(0, 2), clim=model.volume_clim)
        self._viewbox.camera.center = model.camera_center
        self._viewbox.camera.scale_factor = model.camera_distance
        self._canvas.update()
Beispiel #5
0
class SwarmVisualizer(object):
    """docstring for SwarmVisualizer."""
    ARROW_SIZE = 20

    def __init__(self, width=600, height=600):
        self._canvas = SceneCanvas(size=(width, height),
                                   position=(0, 0),
                                   keys='interactive',
                                   title="ALife book " +
                                   self.__class__.__name__)
        self._view = self._canvas.central_widget.add_view()
        #self._view.camera = 'arcball'
        self._view.camera = 'turntable'
        self._axis = visuals.XYZAxis(parent=self._view.scene)
        self._arrows = None
        self._markers = None
        self._canvas.show()

    def update(self, position, direction):
        assert position.ndim is 2 and position.shape[1] in (2, 3)
        assert direction.ndim is 2 and direction.shape[1] in (2, 3)
        assert position.shape[0] == direction.shape[0]
        if self._arrows is None:
            self._arrows = visuals.Arrow(arrow_size=self.ARROW_SIZE,
                                         arrow_type='triangle_30',
                                         parent=self._view.scene)
        # arrow_coordinate[0::2] is position of arrow and
        # arrow_coordinate[1::2] is direction of tail (length is ignored)
        arrow_coordinate = np.repeat(position, 2, axis=0)
        arrow_coordinate[::2] -= direction
        self._arrows.set_data(arrows=arrow_coordinate.reshape((-1, 6)))
        self._canvas.update()
        vispy.app.process_events()

    def set_markers(self, position):
        assert position.ndim is 2 and position.shape[-1] in (2, 3)
        if self._markers is None:
            self._markers = visuals.Markers(parent=self._view.scene)
        self._markers.set_data(position, face_color=(1, 0, 0), size=20)
        self._canvas.update()
        vispy.app.process_events()

    def __bool__(self):
        return not self._canvas._closed
Beispiel #6
0
class AntSimulator(object):
    MIN_VELOCITY = 0.2
    MAX_VELOCITY = 0.8
    # MIN_VELOCITY = 0.128 # 0.0005 * self._FIELD_WIDTH #original on legacy program
    # MAX_VELOCITY = 0.256 # 0.001 * self._FIELD_WIDTH #original on legacy program
    MAX_ANGULAR_VELOCITY = 0.05 * np.pi
    SENSOR_NUM = 7
    AGENT_RADIUS = 12.8 # 0.05 * self._FIELD_WIDTH  #original on legacy program
    SENSOR_NOISE = 0.1

    def __init__(self, N, width=600, height=600, decay_rate=1.0, hormone_secretion=None):
        from PIL.Image import open as open_image
        # setup simulation
        self._N = N
        self._INITIAL_FIELD = np.array(open_image(path.join(ENV_MAP_PATH, 'envmap01.png'))).astype(np.float32) / 255.
        #self._INITIAL_FIELD = np.zeros(self._INITIAL_FIELD.shape)
        self._FIELD_WIDTH = self._INITIAL_FIELD.shape[1]
        self._FIELD_HEIGHT = self._INITIAL_FIELD.shape[0]
        self._FIELD_DECAY_RATE = decay_rate
        self._SECRATION = hormone_secretion
        sensor_th = np.linspace(0, 2*np.pi, self.SENSOR_NUM, endpoint=False)
        self._SENSOR_POSITION = self.AGENT_RADIUS * np.array([np.cos(sensor_th), np.sin(sensor_th)]).T
        self.reset()  # initialize all variables, position, velocity and field status

        # setup display
        self._canvas = SceneCanvas(size=(width, height), position=(0,0), keys='interactive', title="ALife book "+self.__class__.__name__)
        self._canvas.events.mouse_double_click.connect(self._on_mouse_double_click)
        self._view = self._canvas.central_widget.add_view()
        self._view.camera = PanZoomCamera((0, 0, self._FIELD_WIDTH, self._FIELD_HEIGHT), aspect=1)
        self._field_image = Image(self._field, interpolation='nearest', parent=self._view.scene, method='subdivide', clim=(0,1))
        self._agent_polygon = []
        for i in range(self._N):
            p = AntSimulator._generate_agent_visual_polygon(self.AGENT_RADIUS)
            p.parent = self._field_image
            self._agent_polygon.append(p)
        self._canvas.show()

    def reset(self, random_seed=None):
        np.random.seed(random_seed)
        self._field =  self._INITIAL_FIELD.copy()
        self._agents_pos = np.random.random((self._N, 2)).astype(np.float32) * self._FIELD_WIDTH
        self._agents_th = np.random.random(self._N).astype(np.float32) * np.pi * 2
        self._agents_vel = np.ones(self._N).astype(np.float32) * 0.001
        self._agents_ang_vel = (np.random.random(self._N).astype(np.float32) * 0.1 - 0.05) * np.pi
        self._agents_fitness = np.zeros(self._N)

    def get_sensor_data(self):
        sensor_data = np.empty((self._N, 7))
        for ai in range(self._N):
            th = self._agents_th[ai]
            rot_mat = np.array([[np.cos(th), -np.sin(th)],[np.sin(th), np.cos(th)]])
            for si, sensor_pos in enumerate(self._SENSOR_POSITION):
                sx, sy = rot_mat @ sensor_pos
                xi = int((sx + self._agents_pos[ai][0] + self._FIELD_WIDTH) % self._FIELD_WIDTH)
                yi = int((sy + self._agents_pos[ai][1] + self._FIELD_HEIGHT) % self._FIELD_HEIGHT)
                sensor_data[ai, si] = self._field[yi, xi] + np.random.randn() * self.SENSOR_NOISE
        return sensor_data

    def set_agent_color(self, index, color):
        self._agent_polygon[index].border_color = color

    def update(self, action):
        # action take 0-1 value
        v = action[:,0] * (self.MAX_VELOCITY - self.MIN_VELOCITY) + self.MIN_VELOCITY
        av = (action[:,1] - 0.5) * 2 * self.MAX_ANGULAR_VELOCITY
        self._agents_pos += (v * [np.cos(self._agents_th), np.sin(self._agents_th)]).T
        self._agents_th += av

        self._agents_pos[:,0] = (self._agents_pos[:,0] + self._FIELD_WIDTH) % self._FIELD_WIDTH
        self._agents_pos[:,1] = (self._agents_pos[:,1] + self._FIELD_HEIGHT) % self._FIELD_HEIGHT
        self._agents_th = (self._agents_th + 2.0 * np.pi) % (2.0 * np.pi)

        for x, y in self._agents_pos.astype(int):
            for i in range(-1, 2):
                for j in range(-1, 2):
                    xi = (x + i + self._FIELD_WIDTH) % self._FIELD_WIDTH
                    yi = (y + j + self._FIELD_HEIGHT) % self._FIELD_HEIGHT
                    self._agents_fitness += self._field[yi,xi]
                    if self._SECRATION is None:
                        #self._field[yi,xi] *= 0.5 # current running
                        self._field[yi,xi] *= 0.9 # sampledata_last2
                    else:
                        self._field[yi, xi] += self._SECRATION
        self._field.clip(0, 1)

        self._field *= self._FIELD_DECAY_RATE

        self._field_image.set_data(self._field)
        for polygon, (x, y), th in zip(self._agent_polygon, self._agents_pos, self._agents_th):
            polygon.transform.reset()
            polygon.transform.rotate(180 * th / np.pi, (0,0,1))
            polygon.transform.translate((x, y))

        self._canvas.update()
        app.process_events()

    def get_fitness(self):
        return self._agents_fitness

    def _on_mouse_double_click(self, event):
        self._view.camera.set_range(x = (0, self._FIELD_WIDTH), y = (0, self._FIELD_HEIGHT), margin=0)

    def __bool__(self):
        return not self._canvas._closed

    @staticmethod
    def _generate_agent_visual_polygon(radius):
        th = np.linspace(0, 2*np.pi, 16)
        points = np.c_[np.cos(th), np.sin(th)]
        points = np.r_[[[0,0]], points] * radius
        polygon = Polygon(points, color=(0, 0, 0, 0), border_color=(1, 0, 0), border_method='agg', border_width=2)
        #polygon = Polygon(points, color=(0, 0, 0, 0), border_color=(1, 0, 0))  # more fast, but agents visual is hard to seee
        polygon.transform = MatrixTransform()
        return polygon
Beispiel #7
0
class QtViewer(QSplitter):
    with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f:
        raw_stylesheet = f.read()

    def __init__(self, viewer):
        super().__init__()

        self.pool = QThreadPool()

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True)

        self.viewer = viewer
        self.dims = QtDims(self.viewer.dims)
        self.controls = QtControls(self.viewer)
        self.layers = QtLayerList(self.viewer.layers)
        self.layerButtons = QtLayerButtons(self.viewer)
        self.viewerButtons = QtViewerButtons(self.viewer)
        self.console = QtConsole({'viewer': self.viewer})

        layerList = QWidget()
        layerList.setObjectName('layerList')
        layerListLayout = QVBoxLayout()
        layerListLayout.addWidget(self.layerButtons)
        layerListLayout.addWidget(self.layers)
        layerListLayout.addWidget(self.viewerButtons)
        layerListLayout.setContentsMargins(8, 4, 8, 6)
        layerList.setLayout(layerListLayout)
        self.dockLayerList = QtViewerDockWidget(
            self,
            layerList,
            name='layer list',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockLayerControls = QtViewerDockWidget(
            self,
            self.controls,
            name='layer controls',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockConsole = QtViewerDockWidget(
            self,
            self.console,
            name='console',
            area='bottom',
            allowed_areas=['top', 'bottom'],
            shortcut='Ctrl+Shift+C',
        )
        self.dockConsole.setVisible(False)
        self.dockLayerControls.visibilityChanged.connect(self._constrain_width)
        self.dockLayerList.setMaximumWidth(258)
        self.dockLayerList.setMinimumWidth(258)

        self.aboutKeybindings = QtAboutKeybindings(self.viewer)
        self.aboutKeybindings.hide()

        # This dictionary holds the corresponding vispy visual for each layer
        self.layer_to_visual = {}

        if self.console.shell is not None:
            self.viewerButtons.consoleButton.clicked.connect(
                lambda: self.toggle_console())
        else:
            self.viewerButtons.consoleButton.setEnabled(False)

        self.canvas = SceneCanvas(keys=None, vsync=True)
        self.canvas.events.ignore_callback_errors = False
        self.canvas.events.draw.connect(self.dims.enable_play)
        self.canvas.native.setMinimumSize(QSize(200, 200))
        self.canvas.context.set_depth_func('lequal')

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)
        self.canvas.connect(self.on_draw)

        self.view = self.canvas.central_widget.add_view()
        self._update_camera()

        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(10, 22, 10, 2)
        main_layout.addWidget(self.canvas.native)
        main_layout.addWidget(self.dims)
        main_layout.setSpacing(10)
        main_widget.setLayout(main_layout)

        self.setOrientation(Qt.Vertical)
        self.addWidget(main_widget)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'disabled':
            QCursor(
                QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)),
            'cross':
            Qt.CrossCursor,
            'forbidden':
            Qt.ForbiddenCursor,
            'pointing':
            Qt.PointingHandCursor,
            'standard':
            QCursor(),
        }

        self._update_palette(viewer.palette)

        self._key_release_generators = {}

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(
            lambda event: self._update_palette(event.palette))
        self.viewer.layers.events.reordered.connect(self._reorder_layers)
        self.viewer.layers.events.added.connect(self._add_layer)
        self.viewer.layers.events.removed.connect(self._remove_layer)
        self.viewer.dims.events.camera.connect(
            lambda event: self._update_camera())
        # stop any animations whenever the layers change
        self.viewer.events.layers_change.connect(lambda x: self.dims.stop())

        self.setAcceptDrops(True)

    def _constrain_width(self, event):
        # allow the layer controls to be wider, only if floated
        if self.dockLayerControls.isFloating():
            self.controls.setMaximumWidth(700)
        else:
            self.controls.setMaximumWidth(220)

    def _add_layer(self, event):
        """When a layer is added, set its parent and order."""
        layers = event.source
        layer = event.item
        vispy_layer = create_vispy_visual(layer)
        vispy_layer.camera = self.view.camera
        vispy_layer.node.parent = self.view.scene
        vispy_layer.order = len(layers)
        self.layer_to_visual[layer] = vispy_layer

    def _remove_layer(self, event):
        """When a layer is removed, remove its parent."""
        layer = event.item
        vispy_layer = self.layer_to_visual[layer]
        vispy_layer.node.transforms = ChainTransform()
        vispy_layer.node.parent = None
        del self.layer_to_visual[layer]

    def _reorder_layers(self, event):
        """When the list is reordered, propagate changes to draw order."""
        for i, layer in enumerate(self.viewer.layers):
            vispy_layer = self.layer_to_visual[layer]
            vispy_layer.order = i
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_camera(self):
        if self.viewer.dims.ndisplay == 3:
            # Set a 3D camera
            if not isinstance(self.view.camera, ArcballCamera):
                self.view.camera = ArcballCamera(name="ArcballCamera", fov=0)
                # flip y-axis to have correct alignment
                # self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()
        else:
            # Set 2D camera
            if not isinstance(self.view.camera, PanZoomCamera):
                self.view.camera = PanZoomCamera(aspect=1,
                                                 name="PanZoomCamera")
                # flip y-axis to have correct alignment
                self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()

    def screenshot(self):
        """Take currently displayed screen and convert to an image array.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        img = self.canvas.native.grabFramebuffer()
        return QImg2array(img)

    def _open_images(self):
        """Add image files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select image(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if (filenames != []) and (filenames is not None):
            self._add_files(filenames)

    def _open_folder(self):
        """Add a folder of files from the menubar."""
        folder = QFileDialog.getExistingDirectory(
            parent=self,
            caption='Select folder...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if folder not in {'', None}:
            self._add_files([folder])

    def _add_files(self, filenames):
        """Add an image layer to the viewer.

        If multiple images are selected, they are stacked along the 0th
        axis.

        Parameters
        -------
        filenames : list
            List of filenames to be opened
        """
        if len(filenames) > 0:
            self.viewer.add_image(path=filenames)
            self._last_visited_dir = os.path.dirname(filenames[0])

    def _on_interactive(self, event):
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        cursor = self.viewer.cursor
        size = self.viewer.cursor_size
        if cursor == 'square':
            if size < 10 or size > 300:
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(
                    QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight(
                        size))
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        if isinstance(self.view.camera, ArcballCamera):
            quat = self.view.camera._quaternion.create_from_axis_angle(
                *event.quaternion)
            self.view.camera._quaternion = quat
            self.view.camera.center = event.center
            self.view.camera.scale_factor = event.scale_factor
        else:
            # Assumes default camera has the same properties as PanZoomCamera
            self.view.camera.rect = event.rect

    def _update_palette(self, palette):
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet, **palette)
        self.console.style_sheet = themed_stylesheet
        self.console.syntax_style = palette['syntax_style']
        bracket_color = QtGui.QColor(*str_to_rgb(palette['highlight']))
        self.console._bracket_matcher.format.setBackground(bracket_color)
        self.setStyleSheet(themed_stylesheet)
        self.aboutKeybindings.setStyleSheet(themed_stylesheet)
        self.canvas.bgcolor = palette['canvas']

    def toggle_console(self):
        """Toggle console visible and not visible."""
        viz = not self.dockConsole.isVisible()
        # modulate visibility at the dock widget level as console is docakable
        self.dockConsole.setVisible(viz)
        if self.dockConsole.isFloating():
            self.dockConsole.setFloating(True)

        self.viewerButtons.consoleButton.setProperty(
            'expanded', self.dockConsole.isVisible())
        self.viewerButtons.consoleButton.style().unpolish(
            self.viewerButtons.consoleButton)
        self.viewerButtons.consoleButton.style().polish(
            self.viewerButtons.consoleButton)

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.
        """
        if event.pos is None:
            return

        event = ReadOnlyWrapper(event)
        mouse_press_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_press(event)
            mouse_press_callbacks(layer, event)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.
        """
        if event.pos is None:
            return

        mouse_move_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_move(event)
            mouse_move_callbacks(layer, event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.
        """
        mouse_release_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_release(event)
            mouse_release_callbacks(layer, event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.
        """
        if (event.native is not None and event.native.isAutoRepeat()
                and event.key.name not in ['Up', 'Down', 'Left', 'Right'
                                           ]) or event.key is None:
            # pass is no key is present or if key is held down, unless the
            # key being held down is one of the navigation keys
            return

        comb = components_to_key_combo(event.key.name, event.modifiers)

        layer = self.viewer.active_layer

        if layer is not None and comb in layer.keymap:
            parent = layer
        elif comb in self.viewer.keymap:
            parent = self.viewer
        else:
            return

        func = parent.keymap[comb]
        gen = func(parent)

        if inspect.isgeneratorfunction(func):
            try:
                next(gen)
            except StopIteration:  # only one statement
                pass
            else:
                self._key_release_generators[event.key] = gen

    def on_key_release(self, event):
        """Called whenever key released in canvas.
        """
        try:
            next(self._key_release_generators[event.key])
        except (KeyError, StopIteration):
            pass

    def on_draw(self, event):
        """Called whenever drawn in canvas. Called for all layers, not just top
        """
        for visual in self.layer_to_visual.values():
            visual.on_draw(event)

    def keyPressEvent(self, event):
        self.canvas._backend._keyEvent(self.canvas.events.key_press, event)
        event.accept()

    def keyReleaseEvent(self, event):
        self.canvas._backend._keyEvent(self.canvas.events.key_release, event)
        event.accept()

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop."""
        filenames = []
        for url in event.mimeData().urls():
            if url.isLocalFile():
                filenames.append(url.toLocalFile())
            else:
                filenames.append(url.toString())
        self._add_files(filenames)

    def closeEvent(self, event):
        if self.pool.activeThreadCount() > 0:
            self.pool.clear()
        event.accept()

    def shutdown(self):
        self.pool.clear()
        self.canvas.close()
        self.console.shutdown()
Beispiel #8
0
class VolumeView(BaseQtView):
    def __init__(self):

        self._canvas = SceneCanvas()

        self._viewbox = ViewBox(parent=self._canvas.scene)
        self._canvas.central_widget.add_widget(self._viewbox)
        self._viewbox.camera = TurntableCamera(fov=0,
                                               azimuth=0,
                                               elevation=90,
                                               distance=1000)

        self._atlas_volume = Volume(
            array([[[1, 2]]]) * 1000,
            parent=self._viewbox.scene)  #, interpolation='nearest')
        self._atlas_volume.attach(filters.ColorFilter((1., .5, 0., 1.)))
        self._atlas_volume.set_gl_state('additive', depth_test=False)

        self._section_image = Image(parent=self._viewbox.scene, cmap='grays')
        self._section_image.attach(filters.ColorFilter((0., .5, 1., 1.)))
        self._section_image.set_gl_state('additive', depth_test=False)

        self._canvas.events.key_press.connect(
            self._handle_vispy_key_press_events)

    # View Code
    @property
    def qt_widget(self) -> QWidget:
        return self._canvas.native

    def on_atlas_update(self, volume: ndarray, transform: ndarray):
        self._atlas_volume.set_data(volume,
                                    clim=(np.min(volume), np.max(volume)))
        self._atlas_volume.transform = MatrixTransform(transform.T)
        self._viewbox.camera.center = (0, 0, 0)
        self._viewbox.camera.scale_factor = transform[0, 0] * volume.shape[0]
        self._canvas.update()

    def on_section_loaded(self, image: ndarray, transform: ndarray):
        self._section_image.set_data(image)
        self._section_image.clim = np.min(image), np.max(image)
        if transform is not None:
            self._section_image.transform = MatrixTransform(transform.T)
        self._canvas.update()

    def on_channel_select(self, image: ndarray, channel: int):
        self._section_image.set_data(image)
        self._section_image.clim = np.min(image), np.max(image)
        self._canvas.update()

    def on_section_moved(self, transform: ndarray, atlas_slice_image: ndarray):
        self._section_image.transform = MatrixTransform(transform.T)
        self._canvas.update()

    # Controller Code

    def _handle_vispy_key_press_events(self, event: KeyEvent) -> None:
        """Router: Calls AppCommands functions based on the event that's given."""

        key_commands = {
            '1': lambda: self.select_channel(1),
            '2': lambda: self.select_channel(2),
            '3': lambda: self.select_channel(3),
            '4': lambda: self.select_channel(4),
            'W': lambda: self.move_section(z=30),
            'S': lambda: self.move_section(z=-30),
            'A': lambda: self.move_section(x=-30),
            'D': lambda: self.move_section(x=30),
            'Q': lambda: self.move_section(y=-30),
            'E': lambda: self.move_section(y=30),
            'I': lambda: self.move_section(rz=3),
            'K': lambda: self.move_section(rz=-3),
            'J': lambda: self.move_section(rx=-3),
            'L': lambda: self.move_section(rx=3),
            'U': lambda: self.move_section(ry=-3),
            'O': lambda: self.move_section(ry=3),
            'Escape': use_app().quit,
        }
        if command := key_commands.get(event.key.name):
            command()
Beispiel #9
0
class SliceView(BaseQtView):

    def __init__(self):

        self._canvas = SceneCanvas()

        self._viewbox = ViewBox(parent=self._canvas.scene)
        self._canvas.central_widget.add_widget(self._viewbox)

        self._viewbox.camera = TurntableCamera(
            interactive=False,
            fov=0,  # Makes it an ortho camera.
            azimuth=0,
            elevation=-90,
        )

        self._reference_slice = Image(cmap='grays', parent=self._viewbox.scene)
        self._reference_slice.attach(ColorFilter((1., .5, 0., 1.)))
        self._reference_slice.set_gl_state('additive', depth_test=False)

        self._slice = Image(cmap='grays', parent=self._viewbox.scene)
        self._slice.attach(ColorFilter((0., .5, 1., 1.)))
        self._slice.set_gl_state('additive', depth_test=False)

        self._canvas.events.mouse_press.connect(self._vispy_mouse_event)
        self._canvas.events.mouse_move.connect(self._vispy_mouse_event)
        self._canvas.events.mouse_release.connect(self._vispy_mouse_event)
        self._canvas.events.mouse_wheel.connect(self._vispy_mouse_event)

    @property
    def qt_widget(self) -> QWidget:
        return self._canvas.native

    def on_section_loaded(self, image: ndarray, transform: ndarray) -> None:
        self.update_slice_image(image=image)

    def on_channel_select(self, image: ndarray, channel: int) -> None:
        self.update_slice_image(image=image)

    def on_section_moved(self, transform: ndarray, atlas_slice_image: ndarray) -> None:
        self.update_ref_slice_image(image=atlas_slice_image)

    def update_slice_image(self, image: ndarray):
        self._slice.set_data(image)
        self._slice.clim = np.min(image), np.max(image)
        self._viewbox.camera.center = image.shape[1] / 2, image.shape[0] / 2, 0.
        self._viewbox.camera.scale_factor = image.shape[1]
        self._canvas.update()

    def update_ref_slice_image(self, image: ndarray):
        self._reference_slice.set_data(image)
        self._reference_slice.clim = (np.min(image), np.max(image))  if np.max(image) - np.min(image) > 0 else (0, 1)
        self._canvas.update()

    def _vispy_mouse_event(self, event: SceneMouseEvent) -> None:
        if event.type == 'mouse_press':
            event.handled = True

        elif event.type == 'mouse_move':
            if event.press_event is None:
                return
            x1, y1 = event.last_event.pos
            x2, y2 = event.pos
            if event.button == 1:  # Left Mouse Button
                self._on_left_mouse_drag(x1=x1, y1=y1, x2=x2, y2=y2)
            elif event.button == 2:  # Right Mouse Button
                self._on_right_mouse_drag(x1=x1, y1=y1, x2=x2, y2=y2)

        elif event.type == 'mouse_wheel':
            self._on_mousewheel_move(increment=int(event.delta[1]))


    def _on_left_mouse_drag(self, x1: int, y1: int, x2: int, y2: int):
        x_amp = abs(x2 - x1)
        y_amp = abs(y2 - y1)
        x_dir = ((x2 > x1) * 2) - 1
        y_dir = ((y2 > y1) * 2) - 1
        scale = 4.
        x_slice_offset = x_amp * x_dir * scale
        y_slice_offset = y_amp * y_dir * scale
        self.move_section(x=x_slice_offset, y=y_slice_offset)
        self.get_coord_data(i=0, j=0)  # todo: replace with mouse highlighting

    def _on_right_mouse_drag(self, x1: int, y1: int, x2: int, y2: int):
        x_amp = abs(x2 - x1)
        x_dir = ((x2 > x1) * 2) - 1
        scale = .1
        x_slice_offset = x_amp * x_dir * scale
        self.move_section(ry=x_slice_offset)

    def _on_mousewheel_move(self, increment: int):
        self.move_section(z=10 * increment)

    def move_section(self, x=0., y=0., z=0., rx=0., ry=0., rz=0.) -> None:
        raise NotImplementedError("Wire up to MoveSectionCommand to use this.")

    def get_coord_data(self, i: int, j: int):
        raise NotImplementedError("Wire up to GetPixelRegistrationDataCommand to use this.")
Beispiel #10
0
class QtViewer(QSplitter):
    """Qt view for the napari Viewer model.

    Parameters
    ----------
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.

    Attributes
    ----------
    canvas : vispy.scene.SceneCanvas
        Canvas for rendering the current view.
    console : QtConsole
        iPython console terminal integrated into the napari GUI.
    controls : QtControls
        Qt view for GUI controls.
    dims : napari.qt_dims.QtDims
        Dimension sliders; Qt View for Dims model.
    dockConsole : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    aboutKeybindings : QtAboutKeybindings
        Key bindings for the 'About' Qt dialog.
    dockLayerControls : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    dockLayerList : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    layerButtons : QtLayerButtons
        Button controls for napari layers.
    layers : QtLayerList
        Qt view for LayerList controls.
    layer_to_visual : dict
        Dictionary mapping napari layers with their corresponding vispy_layers.
    view : vispy scene widget
        View displayed by vispy canvas. Adds a vispy ViewBox as a child widget.
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.
    viewerButtons : QtViewerButtons
        Button controls for the napari viewer.
    """

    raw_stylesheet = get_stylesheet()

    def __init__(self, viewer):
        super().__init__()
        self.setAttribute(Qt.WA_DeleteOnClose)

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True)

        self.viewer = viewer
        self.dims = QtDims(self.viewer.dims)
        self.controls = QtControls(self.viewer)
        self.layers = QtLayerList(self.viewer.layers)
        self.layerButtons = QtLayerButtons(self.viewer)
        self.viewerButtons = QtViewerButtons(self.viewer)
        self._console = None

        layerList = QWidget()
        layerList.setObjectName('layerList')
        layerListLayout = QVBoxLayout()
        layerListLayout.addWidget(self.layerButtons)
        layerListLayout.addWidget(self.layers)
        layerListLayout.addWidget(self.viewerButtons)
        layerListLayout.setContentsMargins(8, 4, 8, 6)
        layerList.setLayout(layerListLayout)
        self.dockLayerList = QtViewerDockWidget(
            self,
            layerList,
            name='layer list',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockLayerControls = QtViewerDockWidget(
            self,
            self.controls,
            name='layer controls',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockConsole = QtViewerDockWidget(
            self,
            QWidget(),
            name='console',
            area='bottom',
            allowed_areas=['top', 'bottom'],
            shortcut='Ctrl+Shift+C',
        )
        self.dockConsole.setVisible(False)
        # because the console is loaded lazily in the @getter, this line just
        # gets (or creates) the console when the dock console is made visible.
        self.dockConsole.visibilityChanged.connect(lambda visible: self.console
                                                   if visible else None)
        self.dockLayerControls.visibilityChanged.connect(self._constrain_width)
        self.dockLayerList.setMaximumWidth(258)
        self.dockLayerList.setMinimumWidth(258)

        # This dictionary holds the corresponding vispy visual for each layer
        self.layer_to_visual = {}
        self.viewerButtons.consoleButton.clicked.connect(
            self.toggle_console_visibility)

        self.canvas = SceneCanvas(keys=None, vsync=True, parent=self)
        self.canvas.events.ignore_callback_errors = False
        self.canvas.events.draw.connect(self.dims.enable_play)
        self.canvas.native.setMinimumSize(QSize(200, 200))
        self.canvas.context.set_depth_func('lequal')

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)

        self.view = self.canvas.central_widget.add_view()
        self._update_camera()

        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(10, 22, 10, 2)
        main_layout.addWidget(self.canvas.native)
        main_layout.addWidget(self.dims)
        main_layout.setSpacing(10)
        main_widget.setLayout(main_layout)

        self.setOrientation(Qt.Vertical)
        self.addWidget(main_widget)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'cross': Qt.CrossCursor,
            'forbidden': Qt.ForbiddenCursor,
            'pointing': Qt.PointingHandCursor,
            'standard': QCursor(),
        }

        self._update_palette()

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(self._update_palette)
        self.viewer.layers.events.reordered.connect(self._reorder_layers)
        self.viewer.layers.events.added.connect(self._add_layer)
        self.viewer.layers.events.removed.connect(self._remove_layer)
        self.viewer.dims.events.camera.connect(
            lambda event: self._update_camera())
        # stop any animations whenever the layers change
        self.viewer.events.layers_change.connect(lambda x: self.dims.stop())

        self.setAcceptDrops(True)

    @property
    def console(self):
        """QtConsole: iPython console terminal integrated into the napari GUI.
        """
        if self._console is None:
            from .qt_console import QtConsole

            self.console = QtConsole({'viewer': self.viewer})
        return self._console

    @console.setter
    def console(self, console):
        self._console = console
        self.dockConsole.widget = console
        self._update_palette()

    def _constrain_width(self, event):
        """Allow the layer controls to be wider, only if floated.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if self.dockLayerControls.isFloating():
            self.controls.setMaximumWidth(700)
        else:
            self.controls.setMaximumWidth(220)

    def _add_layer(self, event):
        """When a layer is added, set its parent and order.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        layers = event.source
        layer = event.item
        vispy_layer = create_vispy_visual(layer)
        vispy_layer.node.parent = self.view.scene
        vispy_layer.order = len(layers)
        self.canvas.connect(vispy_layer.on_draw)
        self.layer_to_visual[layer] = vispy_layer

    def _remove_layer(self, event):
        """When a layer is removed, remove its parent.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        layer = event.item
        vispy_layer = self.layer_to_visual[layer]
        self.canvas.events.draw.disconnect(vispy_layer.on_draw)
        vispy_layer.node.transforms = ChainTransform()
        vispy_layer.node.parent = None
        del vispy_layer

    def _reorder_layers(self, event):
        """When the list is reordered, propagate changes to draw order.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        for i, layer in enumerate(self.viewer.layers):
            vispy_layer = self.layer_to_visual[layer]
            vispy_layer.order = i
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_camera(self):
        """Update the viewer camera."""
        if self.viewer.dims.ndisplay == 3:
            # Set a 3D camera
            if not isinstance(self.view.camera, ArcballCamera):
                self.view.camera = ArcballCamera(name="ArcballCamera", fov=0)
                # flip y-axis to have correct alignment
                # self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()
        else:
            # Set 2D camera
            if not isinstance(self.view.camera, PanZoomCamera):
                self.view.camera = PanZoomCamera(aspect=1,
                                                 name="PanZoomCamera")
                # flip y-axis to have correct alignment
                self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()

    def _save_layers_dialog(self, selected=False):
        """Save layers (all or selected) to disk, using ``LayerList.save()``.

        Parameters
        ----------
        selected : bool
            If True, only layers that are selected in the viewer will be saved.
            By default, all layers are saved.
        """
        msg = ''
        if not len(self.viewer.layers):
            msg = "There are no layers in the viewer to save"
        elif selected and not len(self.viewer.layers.selected):
            msg = ('Please select one or more layers to save,'
                   '\nor use "Save all layers..."')
        if msg:
            QMessageBox.warning(self, "Nothing to save", msg, QMessageBox.Ok)
            return

        filename, _ = QFileDialog.getSaveFileName(
            parent=self,
            caption=f'Save {"selected" if selected else "all"} layers',
            directory=self._last_visited_dir,  # home dir by default
        )
        if filename:
            self.viewer.layers.save(filename, selected=selected)

    def screenshot(self, path=None):
        """Take currently displayed screen and convert to an image array.

        Parmeters
        ---------
        path : str
            Filename for saving screenshot image.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        img = self.canvas.native.grabFramebuffer()
        if path is not None:
            imsave(path, QImg2array(img))  # scikit-image imsave method
        return QImg2array(img)

    def _screenshot_dialog(self):
        """Save screenshot of current display, default .png"""
        filename, _ = QFileDialog.getSaveFileName(
            parent=self,
            caption='Save screenshot',
            directory=self._last_visited_dir,  # home dir by default
            filter=
            "Image files (*.png *.bmp *.gif *.tif *.tiff)",  # first one used by default
            # jpg and jpeg not included as they don't support an alpha channel
        )
        if (filename != '') and (filename is not None):
            # double check that an appropriate extension has been added as the
            # filter option does not always add an extension on linux and windows
            # see https://bugreports.qt.io/browse/QTBUG-27186
            image_extensions = ('.bmp', '.gif', '.png', '.tif', '.tiff')
            if not filename.endswith(image_extensions):
                filename = filename + '.png'
            self.screenshot(path=filename)

    def _open_files_dialog(self):
        """Add files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select file(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if (filenames != []) and (filenames is not None):
            self.viewer.open(filenames)

    def _open_files_dialog_as_stack_dialog(self):
        """Add files as a stack, from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select files...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if (filenames != []) and (filenames is not None):
            self.viewer.open(filenames, stack=True)

    def _open_folder_dialog(self):
        """Add a folder of files from the menubar."""
        folder = QFileDialog.getExistingDirectory(
            parent=self,
            caption='Select folder...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if folder not in {'', None}:
            self.viewer.open([folder])

    def _on_interactive(self, event):
        """Link interactive attributes of view and viewer.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        """Set the appearance of the mouse cursor.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        cursor = self.viewer.cursor
        if cursor == 'square':
            size = self.viewer.cursor_size
            # make sure the square fits within the current canvas
            if size < 8 or size > (
                    min(*self.viewer.window.qt_viewer.canvas.size) - 4):
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(square_pixmap(size))
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        """Reset view of the rendered scene.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if isinstance(self.view.camera, ArcballCamera):
            quat = self.view.camera._quaternion.create_from_axis_angle(
                *event.quaternion)
            self.view.camera._quaternion = quat
            self.view.camera.center = event.center
            self.view.camera.scale_factor = event.scale_factor
        else:
            # Assumes default camera has the same properties as PanZoomCamera
            self.view.camera.rect = event.rect

    def _update_palette(self, event=None):
        """Update the napari GUI theme."""
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet,
                                     **self.viewer.palette)
        if self._console is not None:
            self.console._update_palette(self.viewer.palette,
                                         themed_stylesheet)
        self.setStyleSheet(themed_stylesheet)
        self.canvas.bgcolor = self.viewer.palette['canvas']

    def toggle_console_visibility(self, event=None):
        """Toggle console visible and not visible.

        Imports the console the first time it is requested.
        """
        # force instantiation of console if not already instantiated
        _ = self.console

        viz = not self.dockConsole.isVisible()
        # modulate visibility at the dock widget level as console is docakable
        self.dockConsole.setVisible(viz)
        if self.dockConsole.isFloating():
            self.dockConsole.setFloating(True)

        self.viewerButtons.consoleButton.setProperty(
            'expanded', self.dockConsole.isVisible())
        self.viewerButtons.consoleButton.style().unpolish(
            self.viewerButtons.consoleButton)
        self.viewerButtons.consoleButton.style().polish(
            self.viewerButtons.consoleButton)

    def show_key_bindings_dialog(self, event=None):
        dialog = QtAboutKeyBindings(self.viewer, parent=self)
        dialog.show()

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.pos is None:
            return

        event = ReadOnlyWrapper(event)
        mouse_press_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # update cursor position in visual and layer
            visual = self.layer_to_visual[layer]
            visual._position = list(event.pos)
            layer.position = visual._transform_position(visual._position)
            mouse_press_callbacks(layer, event)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.pos is None:
            return

        mouse_move_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # update cursor position in visual and layer
            visual = self.layer_to_visual[layer]
            visual._position = list(event.pos)
            layer.position = visual._transform_position(visual._position)
            mouse_move_callbacks(layer, event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.pos is None:
            return

        mouse_release_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # update cursor position in visual and layer
            visual = self.layer_to_visual[layer]
            visual._position = list(event.pos)
            layer.position = visual._transform_position(visual._position)
            mouse_release_callbacks(layer, event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if (event.native is not None and event.native.isAutoRepeat()
                and event.key.name not in ['Up', 'Down', 'Left', 'Right'
                                           ]) or event.key is None:
            # pass if no key is present or if key is held down, unless the
            # key being held down is one of the navigation keys
            # this helps for scrolling, etc.
            return

        combo = components_to_key_combo(event.key.name, event.modifiers)
        self.viewer.press_key(combo)

    def on_key_release(self, event):
        """Called whenever key released in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.key is None:
            return
        combo = components_to_key_combo(event.key.name, event.modifiers)
        self.viewer.release_key(combo)

    def keyPressEvent(self, event):
        """Called whenever a key is pressed.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.canvas._backend._keyEvent(self.canvas.events.key_press, event)
        event.accept()

    def keyReleaseEvent(self, event):
        """Called whenever a key is released.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.canvas._backend._keyEvent(self.canvas.events.key_release, event)
        event.accept()

    def dragEnterEvent(self, event):
        """Ignore event if not dragging & dropping a file or URL to open.

        Using event.ignore() here allows the event to pass through the
        parent widget to its child widget, otherwise the parent widget
        would catch the event and not pass it on to the child widget.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        shift_down = QGuiApplication.keyboardModifiers() & Qt.ShiftModifier
        filenames = []
        for url in event.mimeData().urls():
            if url.isLocalFile():
                filenames.append(url.toLocalFile())
            else:
                filenames.append(url.toString())
        self.viewer.open(filenames, stack=bool(shift_down))

    def closeEvent(self, event):
        """Cleanup and close.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        # if the viewer.QtDims object is playing an axis, we need to terminate
        # the AnimationThread before close, otherwise it will cauyse a segFault
        # or Abort trap. (calling stop() when no animation is occuring is also
        # not a problem)
        self.dims.stop()
        self.canvas.native.deleteLater()
        if self._console is not None:
            self.console.close()
        self.dockConsole.deleteLater()
        event.accept()
Beispiel #11
0
class QtViewer(QSplitter):
    with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f:
        raw_stylesheet = f.read()

    def __init__(self, viewer):
        super().__init__()

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True)

        self.viewer = viewer
        self.dims = QtDims(self.viewer.dims)
        self.controls = QtControls(self.viewer)
        self.layers = QtLayerList(self.viewer.layers)
        self.layerButtons = QtLayerButtons(self.viewer)
        self.viewerButtons = QtViewerButtons(self.viewer)
        self.console = QtConsole({'viewer': self.viewer})

        # This dictionary holds the corresponding vispy visual for each layer
        self.layer_to_visual = {}

        if self.console.shell is not None:
            self.console.style().unpolish(self.console)
            self.console.style().polish(self.console)
            self.console.hide()
            self.viewerButtons.consoleButton.clicked.connect(
                lambda: self._toggle_console())
        else:
            self.viewerButtons.consoleButton.setEnabled(False)

        self.canvas = SceneCanvas(keys=None, vsync=True)
        self.canvas.native.setMinimumSize(QSize(200, 200))
        self.canvas.context.set_depth_func('lequal')

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)
        self.canvas.connect(self.on_draw)

        self.view = self.canvas.central_widget.add_view()
        self._update_camera()

        main_widget = QWidget()
        main_layout = QGridLayout()
        main_layout.setContentsMargins(15, 20, 15, 10)
        main_layout.addWidget(self.canvas.native, 0, 1, 3, 1)
        main_layout.addWidget(self.dims, 3, 1)
        main_layout.addWidget(self.controls, 0, 0)
        main_layout.addWidget(self.layerButtons, 1, 0)
        main_layout.addWidget(self.layers, 2, 0)
        main_layout.addWidget(self.viewerButtons, 3, 0)
        main_layout.setColumnStretch(1, 1)
        main_layout.setSpacing(10)
        main_widget.setLayout(main_layout)

        self.setOrientation(Qt.Vertical)
        self.addWidget(main_widget)
        if self.console.shell is not None:
            self.addWidget(self.console)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'disabled':
            QCursor(
                QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)),
            'cross':
            Qt.CrossCursor,
            'forbidden':
            Qt.ForbiddenCursor,
            'pointing':
            Qt.PointingHandCursor,
            'standard':
            QCursor(),
        }

        self._update_palette(viewer.palette)

        self._key_release_generators = {}

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(
            lambda event: self._update_palette(event.palette))
        self.viewer.layers.events.reordered.connect(self._reorder_layers)
        self.viewer.layers.events.added.connect(self._add_layer)
        self.viewer.layers.events.removed.connect(self._remove_layer)
        self.viewer.dims.events.camera.connect(
            lambda event: self._update_camera())

        self.setAcceptDrops(True)

    def _add_layer(self, event):
        """When a layer is added, set its parent and order."""
        layers = event.source
        layer = event.item
        vispy_layer = create_vispy_visual(layer)
        vispy_layer.camera = self.view.camera
        vispy_layer.node.parent = self.view.scene
        vispy_layer.order = len(layers)
        self.layer_to_visual[layer] = vispy_layer

    def _remove_layer(self, event):
        """When a layer is removed, remove its parent."""
        layer = event.item
        vispy_layer = self.layer_to_visual[layer]
        vispy_layer.node.transforms = ChainTransform()
        vispy_layer.node.parent = None
        del self.layer_to_visual[layer]

    def _reorder_layers(self, event):
        """When the list is reordered, propagate changes to draw order."""
        for i, layer in enumerate(self.viewer.layers):
            vispy_layer = self.layer_to_visual[layer]
            vispy_layer.order = i
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_camera(self):
        if self.viewer.dims.ndisplay == 3:
            # Set a 3D camera
            if not isinstance(self.view.camera, ArcballCamera):
                self.view.camera = ArcballCamera(name="ArcballCamera")
                # flip y-axis to have correct alignment
                # self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()
        else:
            # Set 2D camera
            if not isinstance(self.view.camera, PanZoomCamera):
                self.view.camera = PanZoomCamera(aspect=1,
                                                 name="PanZoomCamera")
                # flip y-axis to have correct alignment
                self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()

    def screenshot(self):
        """Take currently displayed screen and convert to an image array.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        img = self.canvas.native.grabFramebuffer()
        b = img.constBits()
        h, w, c = img.height(), img.width(), 4

        # As vispy doesn't use qtpy we need to reconcile the differences
        # between the `QImage` API for `PySide2` and `PyQt5` on how to convert
        # a QImage to a numpy array.
        if API_NAME == 'PySide2':
            arr = np.array(b).reshape(h, w, c)
        else:
            b.setsize(h * w * c)
            arr = np.frombuffer(b, np.uint8).reshape(h, w, c)

        # Format of QImage is ARGB32_Premultiplied, but color channels are
        # reversed.
        arr = arr[:, :, [2, 1, 0, 3]]
        return arr

    def _open_images(self):
        """Add image files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select image(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        self._add_files(filenames)

    def _add_files(self, filenames):
        """Add an image layer to the viewer.

        Whether the image is rgb is determined by
        :func:`napari.util.misc.is_rgb`.

        If multiple images are selected, they are stacked along the 0th
        axis.

        Parameters
        -------
        filenames : list
            List of filenames to be opened
        """
        if len(filenames) > 0:
            image = io.magic_read(filenames)
            self.viewer.add_image(image, rgb=is_rgb(image.shape))
            self._last_visited_dir = os.path.dirname(filenames[0])

    def _on_interactive(self, event):
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        cursor = self.viewer.cursor
        size = self.viewer.cursor_size
        if cursor == 'square':
            if size < 10 or size > 300:
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(
                    QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight(
                        size))
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        if isinstance(self.view.camera, ArcballCamera):
            self.view.camera._quaternion = self.view.camera._quaternion.create_from_axis_angle(
                *event.quaternion)
            self.view.camera.center = event.center
            self.view.camera.scale_factor = event.scale_factor
        else:
            # Assumes default camera has the same properties as PanZoomCamera
            self.view.camera.rect = event.rect

    def _update_palette(self, palette):
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet, **palette)
        self.console.style_sheet = themed_stylesheet
        self.console.syntax_style = palette['syntax_style']
        self.setStyleSheet(themed_stylesheet)
        self.canvas.bgcolor = palette['canvas']

    def _toggle_console(self):
        """Toggle console visible and not visible."""
        self.console.setVisible(not self.console.isVisible())
        self.viewerButtons.consoleButton.setProperty('expanded',
                                                     self.console.isVisible())
        self.viewerButtons.consoleButton.style().unpolish(
            self.viewerButtons.consoleButton)
        self.viewerButtons.consoleButton.style().polish(
            self.viewerButtons.consoleButton)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            self.layer_to_visual[layer].on_mouse_move(event)

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            self.layer_to_visual[layer].on_mouse_press(event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            self.layer_to_visual[layer].on_mouse_release(event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.
        """
        if (event.native.isAutoRepeat() and event.key.name
                not in ['Up', 'Down', 'Left', 'Right']) or event.key is None:
            # pass is no key is present or if key is held down, unless the
            # key being held down is one of the navigation keys
            return

        comb = components_to_key_combo(event.key.name, event.modifiers)

        layer = self.viewer.active_layer

        if layer is not None and comb in layer.keymap:
            parent = layer
        elif comb in self.viewer.keymap:
            parent = self.viewer
        else:
            return

        func = parent.keymap[comb]
        gen = func(parent)

        if inspect.isgeneratorfunction(func):
            try:
                next(gen)
            except StopIteration:  # only one statement
                pass
            else:
                self._key_release_generators[event.key] = gen

    def on_key_release(self, event):
        """Called whenever key released in canvas.
        """
        try:
            next(self._key_release_generators[event.key])
        except (KeyError, StopIteration):
            pass

    def on_draw(self, event):
        """Called whenever drawn in canvas. Called for all layers, not just top
        """
        for layer in self.viewer.layers:
            self.layer_to_visual[layer].on_draw(event)

    def keyPressEvent(self, event):
        self.canvas._backend._keyEvent(self.canvas.events.key_press, event)
        event.accept()

    def keyReleaseEvent(self, event):
        self.canvas._backend._keyEvent(self.canvas.events.key_release, event)
        event.accept()

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop."""
        filenames = []
        for url in event.mimeData().urls():
            path = url.toString()
            if os.path.isfile(path):
                filenames.append(path)
            elif os.path.isdir(path) and not path.endswith('.zarr'):
                filenames = filenames + list(glob(os.path.join(path, '*')))
            else:
                filenames.append(path)
        self._add_files(filenames)
Beispiel #12
0
class QtViewer(QSplitter):
    with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f:
        raw_stylesheet = f.read()

    def __init__(self, viewer):
        super().__init__()

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True
        )

        self.viewer = viewer

        self.canvas = SceneCanvas(keys=None, vsync=True)
        self.canvas.native.setMinimumSize(QSize(100, 100))

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)
        self.canvas.connect(self.on_draw)

        self.view = self.canvas.central_widget.add_view()

        # Set 2D camera (the camera will scale to the contents in the scene)
        self.view.camera = PanZoomCamera(aspect=1)
        # flip y-axis to have correct aligment
        self.view.camera.flip = (0, 1, 0)
        self.view.camera.set_range()
        self.view.camera.viewbox_key_event = viewbox_key_event

        # TO DO: Remove
        self.viewer._view = self.view

        center = QWidget()
        center_layout = QVBoxLayout()
        center_layout.setContentsMargins(15, 20, 15, 10)
        center_layout.addWidget(self.canvas.native)
        self.dims = QtDims(self.viewer.dims)
        center_layout.addWidget(self.dims)
        center.setLayout(center_layout)

        # Add controls, center, and layerlist
        self.control_panel = QtControls(viewer)
        self.addWidget(self.control_panel)
        self.addWidget(center)

        right = QWidget()
        right_layout = QVBoxLayout()
        self.layers = QtLayerList(self.viewer.layers)
        right_layout.addWidget(self.layers)
        self.buttons = QtLayersButtons(viewer)
        right_layout.addWidget(self.buttons)
        right.setLayout(right_layout)
        right.setMinimumSize(QSize(308, 250))

        self.addWidget(right)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'disabled': QCursor(
                QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)
            ),
            'cross': Qt.CrossCursor,
            'forbidden': Qt.ForbiddenCursor,
            'pointing': Qt.PointingHandCursor,
            'standard': QCursor(),
        }

        self._update_palette(viewer.palette)

        self._key_release_generators = {}

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(
            lambda event: self._update_palette(event.palette)
        )
        self.viewer.layers.events.reordered.connect(self._update_canvas)

        self.setAcceptDrops(True)

    def screenshot(self, region=None, size=None, bgcolor=None):
        """Render the scene to an offscreen buffer and return the image array.

        Parameters
        ----------
        region : tuple | None
            Specifies the region of the canvas to render. Format is
            (x, y, w, h). By default, the entire canvas is rendered.
        size : tuple | None
            Specifies the size of the image array to return. If no size is
            given, then the size of the *region* is used, multiplied by the
            pixel scaling factor of the canvas (see `pixel_scale`). This
            argument allows the scene to be rendered at resolutions different
            from the native canvas resolution.
        bgcolor : instance of Color | None
            The background color to use.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        return self.canvas.render(region, size, bgcolor)

    def _open_images(self):
        """Adds image files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select image(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        self._add_files(filenames)

    def _add_files(self, filenames):
        """Adds an image layer to the viewer.

        Whether the image is multichannel is determined by
        :func:`napari.util.misc.is_multichannel`.

        If multiple images are selected, they are stacked along the 0th
        axis.

        Parameters
        -------
        filenames : list
            List of filenames to be opened
        """
        if len(filenames) > 0:
            image = read(filenames)
            self.viewer.add_image(
                image, multichannel=is_multichannel(image.shape)
            )
            self._last_visited_dir = os.path.dirname(filenames[0])

    def _on_interactive(self, event):
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        cursor = self.viewer.cursor
        size = self.viewer.cursor_size
        if cursor == 'square':
            if size < 10 or size > 300:
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(
                    QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight(
                        size
                    )
                )
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        self.view.camera.rect = event.viewbox

    def _update_canvas(self, event):
        """Clears draw order and refreshes canvas. Usefeul for when layers are
        reoredered.
        """
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_palette(self, palette):
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet, **palette)
        self.setStyleSheet(themed_stylesheet)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_move(event)

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_press(event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_release(event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.
        """
        if event.native.isAutoRepeat() or event.key is None:
            return

        comb = components_to_key_combo(event.key.name, event.modifiers)

        layer = self.viewer.active_layer

        # TODO: remove me once keybinding system converted
        if layer is not None:
            layer.on_key_press(event)

        if layer is not None and comb in layer.keymap:
            parent = layer
        elif comb in self.viewer.keymap:
            parent = self.viewer
        else:
            return

        func = parent.keymap[comb]
        gen = func(parent)

        if inspect.isgeneratorfunction(func):
            try:
                next(gen)
            except StopIteration:  # only one statement
                pass
            else:
                self._key_release_generators[event.key] = gen

    def on_key_release(self, event):
        """Called whenever key released in canvas.
        """
        try:
            next(self._key_release_generators[event.key])
        except (KeyError, StopIteration):
            pass

        # TODO: remove me once keybinding system converted
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_key_release(event)

    def on_draw(self, event):
        """Called whenever drawn in canvas. Called for all layers, not just top
        """
        for layer in self.viewer.layers:
            layer.on_draw(event)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop."""
        filenames = []
        for url in event.mimeData().urls():
            path = url.toString()
            if os.path.isfile(path):
                filenames.append(path)
            elif os.path.isdir(path):
                filenames = filenames + list(glob(os.path.join(path, '*')))
            else:
                filenames.append(path)
        self._add_files(filenames)
Beispiel #13
0
class ScannetVis(QWidget):
    """Class that creates and handles a visualizer for a pointcloud"""
    def __init__(self,
                 scan,
                 rgb_names,
                 depth_names,
                 pose_names,
                 offset=0,
                 skip_im=10,
                 mesh_plot=True,
                 parent=None):
        super(ScannetVis, self).__init__(parent=parent)

        self.scan = scan
        self.rgb_names = rgb_names
        self.depth_names = depth_names
        self.pose_names = pose_names
        self.offset = offset
        self.offset_prev = offset
        self.skip_im = skip_im
        self.mesh_plot = mesh_plot

        self.keyboard_inputs = None
        self.total = len(self.rgb_names)

        self.checkBox_list = []
        self.checkBox_with_3D = []

        self.reset()
        self.initUI()
        self.update_scan()

    def initUI(self):
        self.setStyleSheet("background-color: white;")
        self.principalLayout = QHBoxLayout(self)
        ''' left left Frame : RGB with yolact & depth frame '''
        self.left2Frame = QFrame(self)
        self.left2Frame.setFrameShape(QFrame.StyledPanel)
        self.left2Frame.setFrameShadow(QFrame.Raised)
        self.vertical2Layout = QVBoxLayout(self.left2Frame)
        # self.vertical2Layout.setSpacing(0)
        self.principalLayout.addWidget(self.left2Frame)

        # self.vertical2_1Layout = QVBoxLayout(self.left2Frame)
        # self.vertical2Layout.addWidget(self.left2Frame)
        # add rgb depth
        self.img_canvas.create_native()
        self.img_canvas.native.setMinimumSize(320, 480)
        self.vertical2Layout.addWidget(self.img_canvas.native)
        ''' left Frame : 3D reconstructed Scene '''
        self.leftFrame = QFrame(self)
        self.leftFrame.setFrameShape(QFrame.StyledPanel)
        self.leftFrame.setFrameShadow(QFrame.Raised)
        self.verticalLayout = QVBoxLayout(self.leftFrame)
        # self.verticalLayout.setSpacing(0)
        self.principalLayout.addWidget(self.leftFrame)

        self.canvas.create_native()
        self.canvas.native.setMinimumSize(640, 480)
        self.verticalLayout.addWidget(self.canvas.native)
        ''' left center Frame : 3D Scene graph'''
        self.SGFrame = QFrame(self)
        self.SGFrame.setFrameShape(QFrame.StyledPanel)
        self.SGFrame.setFrameShadow(QFrame.Raised)
        # self.verticalSGLayout = QVBoxLayout(self.SGFrame)
        # self.verticalLayout.setSpacing(0)
        self.principalLayout.addWidget(self.SGFrame)

        self.scene_graph_canvas.create_native()
        self.scene_graph_canvas.native.setMinimumSize(640, 480)
        self.verticalLayout.addWidget(self.scene_graph_canvas.native)
        ''' center Frame : control pannel '''
        self.keyFrame = QFrame(self)
        self.keyFrame.setFrameShape(QFrame.StyledPanel)
        self.keyFrame.setFrameShadow(QFrame.Raised)
        self.keysverticalLayout = QVBoxLayout(self.keyFrame)

        self.label1 = QLabel(
            "To navigate: "
            "\n   n: next (next scan) "
            "\n   s: start (start processing sequential rgb-d images)"
            "\n   p: pause (pause processing)"
            "\n   q: quit (exit program)"
            "\n\n To control 3D view: "
            "\n   LMB: orbits the view around its center point"
            "\n   RMB or scroll: change scale_factor (i.e. zoom level)"
            "\n   SHIFT + LMB: translate the center point"
            "\n   SHIFT + RMB: change FOV")
        self.label2 = QLabel("To find specific objects in 3D Space : ")
        # self.keysverticalLayout.addWidget(self.label1)
        # self.keysverticalLayout.addWidget(self.label2)
        self.vertical2Layout.addWidget(self.label1)
        self.vertical2Layout.addWidget(self.label2)

        self.le = QLineEdit(self)
        self.vertical2Layout.addWidget(self.le)

        self.spb = QPushButton('search', self)
        self.vertical2Layout.addWidget(self.spb)
        self.spb.clicked.connect(self.search_button_click)

        self.cpb = QPushButton('clear', self)
        self.vertical2Layout.addWidget(self.cpb)
        self.cpb.clicked.connect(self.clear_button_click)

        self.verticalLayoutR = QVBoxLayout()
        self.verticalLayoutR.addWidget(self.keyFrame)
        self.verticalLayoutR.setContentsMargins(0, 0, 0, 0)
        self.verticalLayoutR.setSpacing(0)
        self.principalLayout.addLayout(self.verticalLayoutR)
        ''' Right Frame : result images of searched objects '''
        self.rightFrame = QFrame(self)
        self.rightFrame.setFrameShape(QFrame.StyledPanel)
        self.rightFrame.setFrameShadow(QFrame.Raised)
        self.verticalLayoutRight = QVBoxLayout(self.rightFrame)
        self.verticalLayoutRight.setContentsMargins(0, 0, 0, 0)
        self.verticalLayoutRight.setSpacing(0)
        self.principalLayout.addWidget(self.rightFrame)

        self.setLayout(self.principalLayout)
        self.setWindowTitle('Searching objects')
        self.setGeometry(300, 300, 300, 200)
        self.show()

    def reset(self):
        """ Reset. """
        # last key press (it should have a mutex, but visualization is not
        # safety critical, so let's do things wrong)
        self.action = "no"  # no, next, back, quit are the possibilities
        ''' 3D points cloud or mesh SceneCanvas '''
        # new canvas prepared for visualizing data
        self.canvas = SceneCanvas(keys='interactive', show=True)
        # interface (n next, b back, q quit, very simple)
        self.canvas.events.key_press.connect(self.key_press)
        self.canvas.events.draw.connect(self.draw)
        # grid
        self.grid = self.canvas.central_widget.add_grid()

        # add point cloud views
        self.scan_view = vispy.scene.widgets.ViewBox(border_color='white',
                                                     parent=self.canvas.scene)
        self.grid.add_widget(self.scan_view, 0, 0)

        # Camera location settings
        self.scene_cam = vispy.scene.cameras.BaseCamera()
        # self.scene_cam.center = (-10, -10, 10)
        # self.scan_view.add(self.scene_cam)
        # self.scene_cam.pre_transform.set_range()

        canvas2 = vispy.app.Canvas()
        w = QMainWindow()
        widget = QWidget()
        w.setCentralWidget(widget)
        widget.setLayout(QVBoxLayout())
        widget.layout().addWidget(canvas2.native)
        widget.layout().addWidget(QPushButton())
        w.show()

        self.scan_vis = visuals.Mesh()
        self.scan_vis_mean = visuals.Line()
        self.scan_vis_cam = visuals.Line()
        self.scan_bbox_3d = visuals.Line()
        self.label_vis = visuals.Text()

        self.scan_view.add(self.scan_vis)
        self.scan_view.add(self.scan_vis_mean)
        self.scan_view.add(self.scan_vis_cam)
        self.scan_view.add(self.scan_bbox_3d)
        self.scan_view.add(self.label_vis)

        self.scan_view.camera = 'arcball'
        self.tr = self.scan_vis.transforms.get_transform(map_from='visual',
                                                         map_to='canvas')
        # self.scan_view.camera = self.scene_cam
        # self.scan_view.camera = 'arcball' , 'turntable'
        # self.scan_view.camera.transform.rotate(90, (0,1,0))

        visuals.XYZAxis(parent=self.scan_view.scene)
        ''' 2D images SceneCanvas '''
        # img canvas size
        self.canvas_W = 320
        self.canvas_H = 280
        self.multiplier = 2
        ''' new canvas for RGB & Depth img '''
        self.img_canvas = SceneCanvas(keys='interactive',
                                      show=True,
                                      size=(self.canvas_W,
                                            self.canvas_H * self.multiplier))
        self.img_grid = self.img_canvas.central_widget.add_grid()
        # interface (n next, s start, p pause, q quit, )
        self.img_canvas.events.key_press.connect(self.key_press)
        self.img_canvas.events.draw.connect(self.draw)

        # add rgb views
        self.rgb_img_raw_view = vispy.scene.widgets.ViewBox(
            border_color='white', parent=self.img_canvas.scene)
        self.img_grid.add_widget(self.rgb_img_raw_view, 0, 0)
        self.rgb_img_raw_vis = visuals.Image(cmap='viridis')
        self.rgb_img_raw_view.add(self.rgb_img_raw_vis)

        # add a view for the depth
        self.depth_img_view = vispy.scene.widgets.ViewBox(
            border_color='white', parent=self.img_canvas.scene)
        self.img_grid.add_widget(self.depth_img_view, 1, 0)
        self.depth_img_vis = visuals.Image(cmap='viridis')
        self.depth_img_view.add(self.depth_img_vis)
        ''' new canvas for 3D scene graph img '''
        self.scene_graph_canvas = SceneCanvas(keys='interactive',
                                              show=True,
                                              size=(640, 480))
        self.scene_graph_grid = self.scene_graph_canvas.central_widget.add_grid(
        )
        self.scene_graph_canvas.events.key_press.connect(self.key_press)
        self.scene_graph_canvas.events.draw.connect(self.draw)

        # add a view for 3D scene graphs
        self.scene_graph_view = vispy.scene.widgets.ViewBox(
            border_color='white', parent=self.scene_graph_canvas.scene)
        self.scene_graph_grid.add_widget(self.scene_graph_view, 0, 0)
        self.scene_graph_vis = visuals.Image(cmap='viridis')
        self.scene_graph_view.add(self.scene_graph_vis)

    def get_mpl_colormap(self, cmap_name):
        cmap = plt.get_cmap(cmap_name)

        # Initialize the matplotlib color map
        sm = plt.cm.ScalarMappable(cmap=cmap)

        # Obtain linear color range
        color_range = sm.to_rgba(np.linspace(0, 1, 256), bytes=True)[:, 2::-1]

        return color_range.reshape(256, 3).astype(np.float32) / 255.0

    def update_yolact(self):
        title = "scan " + str(self.offset)

        # draw color & depth image
        self.img_canvas.title = title

        _, _, _, _ = self.scan.open_scan(self.rgb_names[self.offset],
                                         self.depth_names[self.offset],
                                         self.pose_names[self.offset],
                                         self.offset,
                                         recon=False)

        text_str = 'Frame %d ' % (self.offset)
        font_face = cv2.FONT_HERSHEY_DUPLEX
        font_scale = 0.6
        font_thickness = 1
        text_w, text_h = cv2.getTextSize(text_str, font_face, font_scale,
                                         font_thickness)[0]
        masked_img = self.scan.masked_img.copy()
        masked_img = cv2.resize(masked_img, (320, 240),
                                interpolation=cv2.INTER_AREA)

        x1, y1 = 0, 0
        text_pt = (x1, y1 + 15)
        text_color = [255, 255, 255]
        color = [0, 0, 0]
        cv2.rectangle(masked_img, (x1, y1), (x1 + text_w, y1 + text_h + 4),
                      color, -1)
        cv2.putText(masked_img, text_str, text_pt, font_face, font_scale,
                    text_color, font_thickness, cv2.LINE_AA)

        self.rgb_img_raw_vis.set_data(masked_img)
        self.rgb_img_raw_vis.update()

        depth_img = cv2.resize(self.scan.depth_im.copy(), (320, 240),
                               interpolation=cv2.INTER_AREA)
        self.depth_img_vis.set_data(depth_img)
        self.depth_img_vis.update()

    def update_3d_recon(self):
        title = "scan " + str(self.offset)
        if (self.offset % self.skip_im == 0):
            start_time = time.time()
            verts, faces, norms, colors = self.scan.open_scan(
                self.rgb_names[self.offset],
                self.depth_names[self.offset],
                self.pose_names[self.offset],
                self.offset,
                recon=True)
            self.verts, self.faces, self.norms, self.colors = verts, faces, norms, colors

            self.canvas.title = title
            self.scan_vis.set_data(vertices=verts,
                                   faces=faces,
                                   vertex_colors=colors / 255.)
            self.scan_vis.update()

            #if self.scan.num_dets_to_consider > 0 and not self.scan.use_gpu:
            if self.scan.num_dets_to_consider > 0 and self.scan.tsdf_vol.debug_same_node_detector:
                self.mean_pose = np.array(self.scan.tsdf_vol.mask_centers)
                self.scan_vis_mean.set_data(self.mean_pose,
                                            color='red',
                                            width=3,
                                            connect='strip')
                self.scan_vis_mean.update()

                # find object's position and visualize
                self.label_vis.text = self.scan.tsdf_vol.class_label
                self.label_vis.pos = self.mean_pose
                self.label_vis.font_size = int(40)

            self.cam_frustum = np.array(self.scan.tsdf_vol.cam_frustum)
            self.scan_vis_cam.set_data(self.cam_frustum,
                                       color='blue',
                                       width=3,
                                       connect=self.scan.tsdf_vol.cam_connect)
            self.scan_vis_cam.update()
            if ('camera' in self.label_vis.text):
                self.label_vis.text.pop()
                self.label_vis.pos = self.label_vis.pos[:-1, :]
            self.label_vis.text += self.scan.tsdf_vol.cam_label
            self.label_vis.pos = np.append(self.label_vis.pos,
                                           self.scan.tsdf_vol.cam_centers,
                                           axis=0)

            # Draw Scene graph images
            generated_scene_graph_file = os.path.join(
                self.scan.tsdf_vol.scene_graph_path,
                'scene_graph' + str(self.offset) + '.png')
            if os.path.exists(generated_scene_graph_file):
                print('Draw scene graph{}'.format(self.offset))
                sg_img = cv2.cvtColor(cv2.imread(generated_scene_graph_file),
                                      cv2.COLOR_BGR2RGB)
                self.sg_img = cv2.resize(sg_img, (640, 480),
                                         interpolation=cv2.INTER_AREA)
                self.scene_graph_vis.set_data(self.sg_img)
                self.scene_graph_vis.update()

            print("--- %s seconds of %d to %d images---" %
                  (time.time() - start_time, self.offset - self.skip_im + 1,
                   self.offset))
            print("--- fps : {} ---".format(self.skip_im /
                                            (time.time() - start_time)))

    def update_scan(self):
        # update_yolact images
        self.update_yolact()

        # Reconstruct 3D Scene and detect same nodes or not
        self.update_3d_recon()

    def update_seq_scan(self):
        if self.canvas.events.key_press.blocked():
            self.canvas.events.key_press.unblock()
        if self.img_canvas.events.key_press.blocked():
            self.img_canvas.events.key_press.unblock()
        if self.scene_graph_canvas.events.key_press.blocked():
            self.scene_graph_canvas.events.key_press.unblock()
        if (self.start):
            self.offset += 1
            self.update_yolact()
            self.update_3d_recon()

            self.canvas.scene.update()
            self.img_canvas.scene.update()
            self.scene_graph_canvas.update()
            self.canvas.on_draw(None)
            self.img_canvas.on_draw(None)
            self.scene_graph_canvas.on_draw(None)

    # interface
    def key_press(self, event):
        self.keyboard_inputs = event.key
        if event.key == 'N':
            self.offset += 1
            if self.offset >= self.total:
                self.offset = 0
            self.update_scan()

        elif event.key == 'S':
            # Start to process RGB-D sequences
            self.start = True
            self.timer1 = vispy.app.Timer(0.033,
                                          connect=self.on_timer1,
                                          start=True)
            self.timer2 = vispy.app.Timer(0.033,
                                          connect=self.on_timer2,
                                          start=True)

        elif event.key == 'P':
            # Pause to process RGB sequences
            self.start = False

        elif event.key == 'U':
            # test when updated draw function
            self.canvas.scene.update()
            self.img_canvas.scene.update()
            self.scene_graph_canvas.update()

        elif event.key == 'Q' or event.key == 'Escape':
            self.destroy()

    def on_timer1(self, event):
        # self.update_seq_scan()
        if (self.start):
            self.offset += 1
            self.update_yolact()

    def on_timer2(self, event):
        if (self.start):
            # self.offset += 1
            self.update_3d_recon()

    def search_button_click(self):
        print('searching object : {}'.format(self.le.text()))
        objects_dict = self.scan.tsdf_vol.node_data

        is_obj_exist = []

        self.clear_searched_items(self.verticalLayoutRight)

        for key, val in objects_dict.items():
            if (val['class'] == self.le.text()):
                print('find {}'.format(self.le.text()))

                thumbnail_path = os.path.join(
                    self.scan.tsdf_vol.bbox_path,
                    'thumbnail_' + str(key) + '_' +
                    str(int(objects_dict[str(key)]['detection_cnt'] / 2)) +
                    '.png')
                cv2_img = cv2.cvtColor(cv2.imread(thumbnail_path),
                                       cv2.COLOR_BGR2RGB)
                image = QImage(cv2_img.data, cv2_img.shape[1],
                               cv2_img.shape[0], cv2_img.strides[0],
                               QImage.Format_RGB888)
                image_frame = QLabel()
                image_frame.setPixmap(QPixmap.fromImage(image))
                self.verticalLayoutRight.addWidget(image_frame)

                checkBox = QCheckBox(val['class'] + str(key))
                self.checkBox_list += [[checkBox, val['class'], str(key)]]

                scan_bbox_3d = visuals.Line()
                self.checkBox_with_3D += [scan_bbox_3d]
                self.scan_view.add(scan_bbox_3d)

                checkBox.stateChanged.connect(self.checkBoxState)

                # searched_obj = QLabel(val['class'] + str(key))
                self.verticalLayoutRight.addWidget(checkBox)
                is_obj_exist += [True]

        if (not is_obj_exist):
            searched_obj = QLabel("Nothing was found!")
            self.verticalLayoutRight.addWidget(searched_obj)
        else:
            searched_obj = QLabel(
                "Check box if you want to find objects in 3D Scene.")
            self.verticalLayoutRight.addWidget(searched_obj)

    def clear_button_click(self):
        print('clear previous searched object')
        self.clear_searched_items(self.verticalLayoutRight)

    def clear_searched_items(self, layout):
        # reset rearching results widget
        while layout.count() > 0:
            item = layout.takeAt(0)
            if not item:
                continue

            w = item.widget()
            if w:
                w.deleteLater()

        # reset visuals.Line for 3D BBox of searched objects
        for i, check in enumerate(self.checkBox_list):
            self.checkBox_with_3D[i].parent = None
            self.checkBox_with_3D[i] = visuals.Line()
            self.scan_view.add(self.checkBox_with_3D[i])

        self.checkBox_list = []
        self.checkBox_with_3D = []

    def checkBoxState(self):
        # checkBox_list is composed of [QcheckBox, class_name, class_3D_ID]
        for i, check in enumerate(self.checkBox_list):
            if check[0].isChecked() == True:
                print('checked!!!')
                # Find 3D BBox in 3D Scene Canvas\
                bbox_3d = np.array(self.scan.tsdf_vol.bbox_3ds[check[2]])
                bbox_connect = np.array([[0, 1], [1, 2], [2, 3], [3,
                                                                  0], [4, 5],
                                         [5, 6], [6, 7], [7, 4], [0, 4],
                                         [1, 5], [2, 6], [3, 7]])
                self.checkBox_with_3D[i].set_data(bbox_3d,
                                                  color='green',
                                                  width=3,
                                                  connect=bbox_connect)
            else:
                self.checkBox_with_3D[i].parent = None
                self.checkBox_with_3D[i] = visuals.Line()
                self.scan_view.add(self.checkBox_with_3D[i])

    def draw(self, event):
        # print('draw states!!')
        # print('event key: {}'.format(self.keyboard_inputs))
        if self.canvas.events.key_press.blocked():
            self.canvas.events.key_press.unblock()
        if self.img_canvas.events.key_press.blocked():
            self.img_canvas.events.key_press.unblock()
        if self.scene_graph_canvas.events.key_press.blocked():
            self.scene_graph_canvas.events.key_press.unblock()

        if self.keyboard_inputs == 'P':
            # Pause to process RGB sequences
            self.start = False
        # if self.keyboard_inputs == 'S':
        #     self.update_seq_scan()

    def destroy(self):
        # destroy the visualization
        self.canvas.close()
        self.img_canvas.close()
        self.scene_graph_canvas.close()
        vispy.app.quit()

    def run(self):
        vispy.app.use_app(backend_name="PyQt5", call_reuse=True)
        vispy.app.run()
Beispiel #14
0
class SliceView(BaseQtWidget):
    def __init__(self, _model: SliceViewModel):
        self._model = _model
        self._model.register(self.update)

        self._canvas = SceneCanvas()

        self._viewbox = ViewBox(parent=self._canvas.scene)
        self._canvas.central_widget.add_widget(self._viewbox)

        self._viewbox.camera = TurntableCamera(
            interactive=False,
            fov=0,  # Makes it an ortho camera.
            azimuth=0,
            elevation=-90,
        )

        self._reference_slice = Image(cmap='grays', parent=self._viewbox.scene)
        self._reference_slice.attach(ColorFilter((1., .5, 0., 1.)))
        self._reference_slice.set_gl_state('additive', depth_test=False)

        self._slice = Image(cmap='grays', parent=self._viewbox.scene)
        self._slice.attach(ColorFilter((0., .5, 1., 1.)))
        self._slice.set_gl_state('additive', depth_test=False)

        self._canvas.events.mouse_press.connect(self.mouse_press)
        self._canvas.events.mouse_move.connect(self.mouse_move)
        self._canvas.events.mouse_wheel.connect(self.mouse_wheel)

    @property
    def qt_widget(self) -> QWidget:
        return self._canvas.native

    def mouse_press(self, event: SceneMouseEvent) -> None:
        event.handled = True

    def mouse_move(self, event: SceneMouseEvent) -> None:
        if event.press_event is None:
            return
        x1, y1 = event.last_event.pos
        x2, y2 = event.pos
        if event.button == 1:  # Left Mouse Button
            self._model.on_left_mouse_drag(x1=x1, x2=x2, y1=y1, y2=y2)
        elif event.button == 2:  # Right Mouse Button
            self._model.on_right_mouse_drag(x1=x1, y1=y1, x2=x2, y2=y2)

    def mouse_wheel(self, event: SceneMouseEvent):
        self._model.on_mousewheel_move(increment=int(event.delta[1]))

    def update(self, changed: str) -> None:
        render_funs = {
            'section_image': self._render_section_image,
            'clim': self._render_section_clim,
            'atlas_image': self._render_atlas_image,
        }
        render_funs[changed]()

    def _render_atlas_image(self):
        image = self._model.atlas_image
        self._reference_slice.set_data(image)
        self._reference_slice.clim = (
            np.min(image),
            np.max(image)) if np.max(image) - np.min(image) > 0 else (0, 1)
        self._canvas.update()

    def _render_section_clim(self):
        self._slice.clim = self._model.clim
        self._canvas.update()

    def _render_section_image(self):
        image = self._model.section_image
        self._slice.set_data(image)
        self._viewbox.camera.center = image.shape[1] / 2, image.shape[0] / 2, 0.
        self._viewbox.camera.scale_factor = image.shape[1]
        self._canvas.update()
Beispiel #15
0
class AtlasSectionView(BaseQtWidget):
    def __init__(self, _model: AtlasSectionViewModel):
        self._model = _model
        self._model.register(self.update)

        self._canvas = SceneCanvas()

        self._viewbox = ViewBox(parent=self._canvas.scene)
        self._canvas.central_widget.add_widget(self._viewbox)

        self._viewbox.camera = TurntableCamera(
            interactive=False,
            fov=0,  # Makes it an ortho camera.
            azimuth=0,
            elevation=90,
        )

        self._slice = Image(cmap='grays', parent=self._viewbox.scene)
        self._slice.transform = MatrixTransform()
        self._slice.set_data(self._model.atlas_section_image)
        self._slice.clim = self._model.clim
        self._viewbox.camera.center = self._model.camera_center
        self._viewbox.camera.scale_factor = self._viewbox.camera.scale_factor

        self._vertical_line = InfiniteLine(pos=0,
                                           vertical=True,
                                           parent=self._viewbox.scene)
        self._horizontal_line = InfiniteLine(pos=0,
                                             vertical=False,
                                             parent=self._viewbox.scene)

        self._canvas.events.mouse_press.connect(self.mouse_press)
        self._canvas.events.mouse_move.connect(self.mouse_move)

        self._vertical_line.set_data(color=self._model.vertical_line_color)
        self._horizontal_line.set_data(color=self._model.horizontal_line_color)

    @property
    def qt_widget(self) -> QWidget:
        return self._canvas.native

    def update(self, changed: str):
        render_funs = {
            'atlas_section_image': self._render_image,
            'plane': (lambda: None),
            'camera_scale': self._render_camera_scale,
            'camera_center': self._render_camera_center,
            'section_scale': self._render_section_scale,
            'horizontal_line_pos': self._render_horizontal_line,
            'vertical_line_pos': self._render_vertical_line,
        }
        render_funs[changed]()

    def mouse_press(self, event: SceneMouseEvent) -> None:
        event.handled = True

        # transform coordinates from canvas to image
        tr = self._canvas.scene.node_transform(self._viewbox.scene)
        assert len(event.pos) == 2
        x, y, _, _ = tr.map(event.pos)

        if event.button == 1:
            self._model.click_left_mouse_button(x=x, y=y)

    def mouse_move(self, event: SceneMouseEvent) -> None:
        if event.press_event is None:
            return

        # transform coordinates from canvas to image
        tr = self._canvas.scene.node_transform(self._viewbox.scene)
        x1, y1, _, _ = tr.map(event.last_event.pos)
        x2, y2, _, _ = tr.map(event.pos)

        if event.button == 1:  # Left Mouse Button
            self._model.drag_left_mouse(x1=int(x1),
                                        x2=int(x2),
                                        y1=int(y1),
                                        y2=int(y2))

    def _render_horizontal_line(self):
        self._vertical_line.set_data(pos=self._model.horizontal_line_pos)

    def _render_vertical_line(self):
        self._horizontal_line.set_data(pos=self._model.vertical_line_pos)

    def _render_image(self):
        self._slice.set_data(self._model.atlas_section_image)
        self._slice.clim = self._model.clim
        self._canvas.update()

    def _render_camera_scale(self):
        self._viewbox.camera.scale_factor = self._model.camera_scale
        self._canvas.update()

    def _render_camera_center(self):
        self._viewbox.camera.center = self._model.camera_center
        self._canvas.update()

    def _render_section_scale(self):
        self._slice.transform.reset()
        self._slice.transform.scale(self._model.section_scale)
        self._canvas.update()
Beispiel #16
0
class QtViewer(QSplitter):
    """Qt view for the napari Viewer model.

    Parameters
    ----------
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.

    Attributes
    ----------
    canvas : vispy.scene.SceneCanvas
        Canvas for rendering the current view.
    console : QtConsole
        iPython console terminal integrated into the napari GUI.
    controls : QtControls
        Qt view for GUI controls.
    dims : napari.qt_dims.QtDims
        Dimension sliders; Qt View for Dims model.
    dockConsole : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    aboutKeybindings : QtAboutKeybindings
        Key bindings for the 'About' Qt dialog.
    dockLayerControls : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    dockLayerList : QtViewerDockWidget
        QWidget wrapped in a QDockWidget with forwarded viewer events.
    layerButtons : QtLayerButtons
        Button controls for napari layers.
    layers : QtLayerList
        Qt view for LayerList controls.
    layer_to_visual : dict
        Dictionary mapping napari layers with their corresponding vispy_layers.
    pool : qtpy.QtCore.QThreadPool
        Pool of worker threads.
    view : vispy scene widget
        View displayed by vispy canvas. Adds a vispy ViewBox as a child widget.
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.
    viewerButtons : QtViewerButtons
        Button controls for the napari viewer.
    """

    raw_stylesheet = combine_stylesheets()

    def __init__(self, viewer):
        super().__init__()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.pool = QThreadPool()

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True)

        self.viewer = viewer
        self.dims = QtDims(self.viewer.dims)
        self.controls = QtControls(self.viewer)
        self.layers = QtLayerList(self.viewer.layers)
        self.layerButtons = QtLayerButtons(self.viewer)
        self.viewerButtons = QtViewerButtons(self.viewer)
        self.console = QtConsole({'viewer': self.viewer})

        layerList = QWidget()
        layerList.setObjectName('layerList')
        layerListLayout = QVBoxLayout()
        layerListLayout.addWidget(self.layerButtons)
        layerListLayout.addWidget(self.layers)
        layerListLayout.addWidget(self.viewerButtons)
        layerListLayout.setContentsMargins(8, 4, 8, 6)
        layerList.setLayout(layerListLayout)
        self.dockLayerList = QtViewerDockWidget(
            self,
            layerList,
            name='layer list',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockLayerControls = QtViewerDockWidget(
            self,
            self.controls,
            name='layer controls',
            area='left',
            allowed_areas=['left', 'right'],
        )
        self.dockConsole = QtViewerDockWidget(
            self,
            self.console,
            name='console',
            area='bottom',
            allowed_areas=['top', 'bottom'],
            shortcut='Ctrl+Shift+C',
        )
        self.dockConsole.setVisible(False)
        self.dockLayerControls.visibilityChanged.connect(self._constrain_width)
        self.dockLayerList.setMaximumWidth(258)
        self.dockLayerList.setMinimumWidth(258)

        # This dictionary holds the corresponding vispy visual for each layer
        self.layer_to_visual = {}

        if self.console.shell is not None:
            self.viewerButtons.consoleButton.clicked.connect(
                lambda: self.toggle_console())
        else:
            self.viewerButtons.consoleButton.setEnabled(False)

        self.canvas = SceneCanvas(keys=None, vsync=True, parent=self)
        self.canvas.events.ignore_callback_errors = False
        self.canvas.events.draw.connect(self.dims.enable_play)
        self.canvas.native.setMinimumSize(QSize(200, 200))
        self.canvas.context.set_depth_func('lequal')

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)
        self.canvas.connect(self.on_draw)

        self.view = self.canvas.central_widget.add_view()
        self._update_camera()

        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(10, 22, 10, 2)
        main_layout.addWidget(self.canvas.native)
        main_layout.addWidget(self.dims)
        main_layout.setSpacing(10)
        main_widget.setLayout(main_layout)

        self.setOrientation(Qt.Vertical)
        self.addWidget(main_widget)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'disabled':
            QCursor(
                QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)),
            'cross':
            Qt.CrossCursor,
            'forbidden':
            Qt.ForbiddenCursor,
            'pointing':
            Qt.PointingHandCursor,
            'standard':
            QCursor(),
        }

        self._update_palette(viewer.palette)

        self._key_release_generators = {}

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(
            lambda event: self._update_palette(event.palette))
        self.viewer.layers.events.reordered.connect(self._reorder_layers)
        self.viewer.layers.events.added.connect(self._add_layer)
        self.viewer.layers.events.removed.connect(self._remove_layer)
        self.viewer.dims.events.camera.connect(
            lambda event: self._update_camera())
        # stop any animations whenever the layers change
        self.viewer.events.layers_change.connect(lambda x: self.dims.stop())

        self.setAcceptDrops(True)

    def _constrain_width(self, event):
        """Allow the layer controls to be wider, only if floated.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if self.dockLayerControls.isFloating():
            self.controls.setMaximumWidth(700)
        else:
            self.controls.setMaximumWidth(220)

    def _add_layer(self, event):
        """When a layer is added, set its parent and order.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        layers = event.source
        layer = event.item
        vispy_layer = create_vispy_visual(layer)
        vispy_layer.camera = self.view.camera
        vispy_layer.node.parent = self.view.scene
        vispy_layer.order = len(layers)
        self.layer_to_visual[layer] = vispy_layer

    def _remove_layer(self, event):
        """When a layer is removed, remove its parent.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        layer = event.item
        vispy_layer = self.layer_to_visual[layer]
        vispy_layer.node.transforms = ChainTransform()
        vispy_layer.node.parent = None
        del self.layer_to_visual[layer]

    def _reorder_layers(self, event):
        """When the list is reordered, propagate changes to draw order.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        for i, layer in enumerate(self.viewer.layers):
            vispy_layer = self.layer_to_visual[layer]
            vispy_layer.order = i
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_camera(self):
        """Update the viewer camera."""
        if self.viewer.dims.ndisplay == 3:
            # Set a 3D camera
            if not isinstance(self.view.camera, ArcballCamera):
                self.view.camera = ArcballCamera(name="ArcballCamera", fov=0)
                # flip y-axis to have correct alignment
                # self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()
        else:
            # Set 2D camera
            if not isinstance(self.view.camera, PanZoomCamera):
                self.view.camera = PanZoomCamera(aspect=1,
                                                 name="PanZoomCamera")
                # flip y-axis to have correct alignment
                self.view.camera.flip = (0, 1, 0)

                self.view.camera.viewbox_key_event = viewbox_key_event
                self.viewer.reset_view()

    def screenshot(self, path=None):
        """Take currently displayed screen and convert to an image array.

        Parmeters
        ---------
        path : str
            Filename for saving screenshot image.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        img = self.canvas.native.grabFramebuffer()
        if path is not None:
            imsave(path, QImg2array(img))  # scikit-image imsave method
        return QImg2array(img)

    def _save_screenshot(self):
        """Save screenshot of current display, default .png"""
        filename, _ = QFileDialog.getSaveFileName(
            parent=self,
            caption='',
            directory=self._last_visited_dir,  # home dir by default
            filter=
            "Image files (*.png *.bmp *.gif *.tif *.tiff)",  # first one used by default
            # jpg and jpeg not included as they don't support an alpha channel
        )
        if (filename != '') and (filename is not None):
            # double check that an appropriate extension has been added as the filter
            # filter option does not always add an extension on linux and windows
            # see https://bugreports.qt.io/browse/QTBUG-27186
            image_extensions = ('.bmp', '.gif', '.png', '.tif', '.tiff')
            if not filename.endswith(image_extensions):
                filename = filename + '.png'
            self.screenshot(path=filename)

    def _open_images(self):
        """Add image files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select image(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if (filenames != []) and (filenames is not None):
            self._add_files(filenames)

    def _open_folder(self):
        """Add a folder of files from the menubar."""
        folder = QFileDialog.getExistingDirectory(
            parent=self,
            caption='Select folder...',
            directory=self._last_visited_dir,  # home dir by default
        )
        if folder not in {'', None}:
            self._add_files([folder])

    def _add_files(self, filenames):
        """Add an image layer to the viewer.

        If multiple images are selected, they are stacked along the 0th
        axis.

        Parameters
        -------
        filenames : list
            List of filenames to be opened
        """
        if len(filenames) > 0:
            self.viewer.add_image(path=filenames)
            self._last_visited_dir = os.path.dirname(filenames[0])

    def _on_interactive(self, event):
        """Link interactive attributes of view and viewer.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        """Set the appearance of the mouse cursor.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        cursor = self.viewer.cursor
        size = self.viewer.cursor_size
        if cursor == 'square':
            if size < 10 or size > 300:
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(
                    QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight(
                        size))
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        """Reset view of the rendered scene.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if isinstance(self.view.camera, ArcballCamera):
            quat = self.view.camera._quaternion.create_from_axis_angle(
                *event.quaternion)
            self.view.camera._quaternion = quat
            self.view.camera.center = event.center
            self.view.camera.scale_factor = event.scale_factor
        else:
            # Assumes default camera has the same properties as PanZoomCamera
            self.view.camera.rect = event.rect

    def _update_palette(self, palette):
        """Update the napari GUI theme.

        Parameters
        ----------
        palette : dict of str: str
            Color palette with which to style the viewer.
            Property of napari.components.viewer_model.ViewerModel
        """
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet, **palette)
        self.console.style_sheet = themed_stylesheet
        self.console.syntax_style = palette['syntax_style']
        bracket_color = QtGui.QColor(*str_to_rgb(palette['highlight']))
        self.console._bracket_matcher.format.setBackground(bracket_color)
        self.setStyleSheet(themed_stylesheet)
        self.canvas.bgcolor = palette['canvas']

    def toggle_console(self):
        """Toggle console visible and not visible."""
        viz = not self.dockConsole.isVisible()
        # modulate visibility at the dock widget level as console is docakable
        self.dockConsole.setVisible(viz)
        if self.dockConsole.isFloating():
            self.dockConsole.setFloating(True)

        self.viewerButtons.consoleButton.setProperty(
            'expanded', self.dockConsole.isVisible())
        self.viewerButtons.consoleButton.style().unpolish(
            self.viewerButtons.consoleButton)
        self.viewerButtons.consoleButton.style().polish(
            self.viewerButtons.consoleButton)

    def show_keybindings_dialog(self, event=None):
        dialog = QtAboutKeybindings(self.viewer, parent=self)
        dialog.show()

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.pos is None:
            return

        event = ReadOnlyWrapper(event)
        mouse_press_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_press(event)
            mouse_press_callbacks(layer, event)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.pos is None:
            return

        mouse_move_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_move(event)
            mouse_move_callbacks(layer, event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        mouse_release_callbacks(self.viewer, event)

        layer = self.viewer.active_layer
        if layer is not None:
            # Line bellow needed until layer mouse callbacks are refactored
            self.layer_to_visual[layer].on_mouse_release(event)
            mouse_release_callbacks(layer, event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if (event.native is not None and event.native.isAutoRepeat()
                and event.key.name not in ['Up', 'Down', 'Left', 'Right'
                                           ]) or event.key is None:
            # pass is no key is present or if key is held down, unless the
            # key being held down is one of the navigation keys
            return

        comb = components_to_key_combo(event.key.name, event.modifiers)

        layer = self.viewer.active_layer

        if layer is not None and comb in layer.keymap:
            parent = layer
        elif comb in self.viewer.keymap:
            parent = self.viewer
        else:
            return

        func = parent.keymap[comb]
        gen = func(parent)

        if inspect.isgenerator(gen):
            try:
                next(gen)
            except StopIteration:  # only one statement
                pass
            else:
                self._key_release_generators[event.key] = gen

    def on_key_release(self, event):
        """Called whenever key released in canvas.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        try:
            next(self._key_release_generators[event.key])
        except (KeyError, StopIteration):
            pass

    def on_draw(self, event):
        """Called whenever drawn in canvas. Called for all layers, not just top

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        for visual in self.layer_to_visual.values():
            visual.on_draw(event)

    def keyPressEvent(self, event):
        """Called whenever a key is pressed.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.canvas._backend._keyEvent(self.canvas.events.key_press, event)
        event.accept()

    def keyReleaseEvent(self, event):
        """Called whenever a key is released.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self.canvas._backend._keyEvent(self.canvas.events.key_release, event)
        event.accept()

    def dragEnterEvent(self, event):
        """Ignore event if not dragging & dropping a file or URL to open.

        Using event.ignore() here allows the event to pass through the
        parent widget to its child widget, otherwise the parent widget
        would catch the event and not pass it on to the child widget.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        filenames = []
        for url in event.mimeData().urls():
            if url.isLocalFile():
                filenames.append(url.toLocalFile())
            else:
                filenames.append(url.toString())
        self._add_files(filenames)

    def closeEvent(self, event):
        """Clear pool of worker threads and close.

        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        # if the viewer.QtDims object is playing an axis, we need to terminate
        # the AnimationThread before close, otherwise it will cauyse a segFault
        # or Abort trap. (calling stop() when no animation is occuring is also
        # not a problem)
        self.dims.stop()
        self.canvas.native.deleteLater()
        self.console.close()
        self.dockConsole.deleteLater()
        if not self.pool.waitForDone(10000):
            raise TimeoutError("Timed out waiting for QtViewer.pool to finish")
        event.accept()
Beispiel #17
0
class QtViewer(QSplitter):
    with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f:
        raw_stylesheet = f.read()

    def __init__(self, viewer):
        super().__init__()

        QCoreApplication.setAttribute(
            Qt.AA_UseStyleSheetPropagationInWidgetStyles, True
        )

        self.viewer = viewer
        self.dims = QtDims(self.viewer.dims)
        self.controls = QtControls(self.viewer)
        self.layers = QtLayerList(self.viewer.layers)
        self.buttons = QtLayersButtons(self.viewer)
        self.console = QtConsole({'viewer': self.viewer})

        if self.console.shell is not None:
            self.console.style().unpolish(self.console)
            self.console.style().polish(self.console)
            self.console.hide()
            self.buttons.consoleButton.clicked.connect(
                lambda: self._toggle_console()
            )
        else:
            self.buttons.consoleButton.setEnabled(False)

        self.canvas = SceneCanvas(keys=None, vsync=True)
        self.canvas.native.setMinimumSize(QSize(200, 200))

        self.canvas.connect(self.on_mouse_move)
        self.canvas.connect(self.on_mouse_press)
        self.canvas.connect(self.on_mouse_release)
        self.canvas.connect(self.on_key_press)
        self.canvas.connect(self.on_key_release)
        self.canvas.connect(self.on_draw)

        self.view = self.canvas.central_widget.add_view()
        self._update_camera()

        center = QWidget()
        center_layout = QVBoxLayout()
        center_layout.setContentsMargins(15, 20, 15, 10)
        center_layout.addWidget(self.canvas.native)
        center_layout.addWidget(self.dims)
        center.setLayout(center_layout)

        right = QWidget()
        right_layout = QVBoxLayout()
        right_layout.addWidget(self.layers)
        right_layout.addWidget(self.buttons)
        right.setLayout(right_layout)
        right.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)

        left = self.controls

        top = QWidget()
        top_layout = QHBoxLayout()
        top_layout.addWidget(left)
        top_layout.addWidget(center)
        top_layout.addWidget(right)
        top.setLayout(top_layout)

        self.setOrientation(Qt.Vertical)
        self.addWidget(top)

        if self.console.shell is not None:
            self.addWidget(self.console)

        self._last_visited_dir = str(Path.home())

        self._cursors = {
            'disabled': QCursor(
                QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)
            ),
            'cross': Qt.CrossCursor,
            'forbidden': Qt.ForbiddenCursor,
            'pointing': Qt.PointingHandCursor,
            'standard': QCursor(),
        }

        self._update_palette(viewer.palette)

        self._key_release_generators = {}

        self.viewer.events.interactive.connect(self._on_interactive)
        self.viewer.events.cursor.connect(self._on_cursor)
        self.viewer.events.reset_view.connect(self._on_reset_view)
        self.viewer.events.palette.connect(
            lambda event: self._update_palette(event.palette)
        )
        self.viewer.layers.events.reordered.connect(self._update_canvas)
        self.viewer.dims.events.display.connect(
            lambda event: self._update_camera()
        )

        self.setAcceptDrops(True)

    def _update_camera(self):
        if np.sum(self.viewer.dims.display) == 3:
            # Set a 3D camera
            self.view.camera = ArcballCamera(name="ArcballCamera")
            # flip y-axis to have correct alignment
            self.view.camera.flip = (0, 1, 0)
            min_shape, max_shape = self.viewer._calc_bbox()
            centroid = np.add(max_shape, min_shape) / 2
            size = np.subtract(max_shape, min_shape)
            # Scale the camera to the contents in the scene
            if len(centroid) > 0:
                centroid = centroid[-3:]
                self.view.camera.center = centroid[::-1]
                self.view.camera.scale_factor = 1.5 * np.mean(size[-3:])
        elif np.sum(self.viewer.dims.display) == 2:
            # Set 2D camera
            self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera")
            # flip y-axis to have correct alignment
            self.view.camera.flip = (0, 1, 0)
            # Scale the camera to the contents in the scene
            self.view.camera.set_range()
        else:
            raise ValueError(
                "Invalid display flags set in dimensions {}".format(
                    self.viewer.dims.display
                )
            )

        self.view.camera.viewbox_key_event = viewbox_key_event
        # TO DO: Remove
        self.viewer._view = self.view

    def screenshot(self):
        """Take currently displayed screen and convert to an image array.

        Returns
        -------
        image : array
            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the
            upper-left corner of the rendered region.
        """
        img = self.canvas.native.grabFramebuffer()
        b = img.constBits()
        h, w, c = img.height(), img.width(), 4

        # As vispy doesn't use qtpy we need to reconcile the differences
        # between the `QImage` API for `PySide2` and `PyQt5` on how to convert
        # a QImage to a numpy array.
        if API_NAME == 'PySide2':
            arr = np.array(b).reshape(h, w, c)
        else:
            b.setsize(h * w * c)
            arr = np.frombuffer(b, np.uint8).reshape(h, w, c)

        # Format of QImage is ARGB32_Premultiplied, but color channels are
        # reversed.
        arr = arr[:, :, [2, 1, 0, 3]]
        return arr

    def _open_images(self):
        """Adds image files from the menubar."""
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=self,
            caption='Select image(s)...',
            directory=self._last_visited_dir,  # home dir by default
        )
        self._add_files(filenames)

    def _add_files(self, filenames):
        """Adds an image layer to the viewer.

        Whether the image is multichannel is determined by
        :func:`napari.util.misc.is_multichannel`.

        If multiple images are selected, they are stacked along the 0th
        axis.

        Parameters
        -------
        filenames : list
            List of filenames to be opened
        """
        if len(filenames) > 0:
            image = read(filenames)
            self.viewer.add_image(
                image, multichannel=is_multichannel(image.shape)
            )
            self._last_visited_dir = os.path.dirname(filenames[0])

    def _on_interactive(self, event):
        self.view.interactive = self.viewer.interactive

    def _on_cursor(self, event):
        cursor = self.viewer.cursor
        size = self.viewer.cursor_size
        if cursor == 'square':
            if size < 10 or size > 300:
                q_cursor = self._cursors['cross']
            else:
                q_cursor = QCursor(
                    QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight(
                        size
                    )
                )
        else:
            q_cursor = self._cursors[cursor]
        self.canvas.native.setCursor(q_cursor)

    def _on_reset_view(self, event):
        self.view.camera.rect = event.viewbox

    def _update_canvas(self, event):
        """Clears draw order and refreshes canvas. Usefeul for when layers are
        reoredered.
        """
        self.canvas._draw_order.clear()
        self.canvas.update()

    def _update_palette(self, palette):
        # template and apply the primary stylesheet
        themed_stylesheet = template(self.raw_stylesheet, **palette)
        self.console.style_sheet = themed_stylesheet
        self.console.syntax_style = palette['syntax_style']
        self.setStyleSheet(themed_stylesheet)

    def _toggle_console(self):
        """Toggle console visible and not visible."""
        self.console.setVisible(not self.console.isVisible())
        self.buttons.consoleButton.setProperty(
            'expanded', self.console.isVisible()
        )
        self.buttons.consoleButton.style().unpolish(self.buttons.consoleButton)
        self.buttons.consoleButton.style().polish(self.buttons.consoleButton)

    def on_mouse_move(self, event):
        """Called whenever mouse moves over canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_move(event)

    def on_mouse_press(self, event):
        """Called whenever mouse pressed in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_press(event)

    def on_mouse_release(self, event):
        """Called whenever mouse released in canvas.
        """
        layer = self.viewer.active_layer
        if layer is not None:
            layer.on_mouse_release(event)

    def on_key_press(self, event):
        """Called whenever key pressed in canvas.
        """
        if event.native.isAutoRepeat() or event.key is None:
            return

        comb = components_to_key_combo(event.key.name, event.modifiers)

        layer = self.viewer.active_layer

        if layer is not None and comb in layer.keymap:
            parent = layer
        elif comb in self.viewer.keymap:
            parent = self.viewer
        else:
            return

        func = parent.keymap[comb]
        gen = func(parent)

        if inspect.isgeneratorfunction(func):
            try:
                next(gen)
            except StopIteration:  # only one statement
                pass
            else:
                self._key_release_generators[event.key] = gen

    def on_key_release(self, event):
        """Called whenever key released in canvas.
        """
        try:
            next(self._key_release_generators[event.key])
        except (KeyError, StopIteration):
            pass

    def on_draw(self, event):
        """Called whenever drawn in canvas. Called for all layers, not just top
        """
        for layer in self.viewer.layers:
            layer.on_draw(event)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Add local files and web URLS with drag and drop."""
        filenames = []
        for url in event.mimeData().urls():
            path = url.toString()
            if os.path.isfile(path):
                filenames.append(path)
            elif os.path.isdir(path):
                filenames = filenames + list(glob(os.path.join(path, '*')))
            else:
                filenames.append(path)
        self._add_files(filenames)
Beispiel #18
0
class RigidAlignmentWidget(QWidget, OnActivateMixin):
    def __init__(self, model: Model):
        QWidget.__init__(self)
        self.model = model
        self.fixed_url = None
        self.level = None
        self.fixed_volume = None
        self.moving_url = None
        self.moving_volume = None
        self.canvas_shape = None
        self.view = None

        top_layout = QVBoxLayout()
        self.setLayout(top_layout)
        splitter = QSplitter(QtCore.Qt.Horizontal)
        top_layout.addWidget(splitter)
        left_widget = QWidget()
        splitter.addWidget(left_widget)
        left_layout = QVBoxLayout()
        left_widget.setLayout(left_layout)
        #
        # ------ Center coordinates
        #
        center_group_box = QGroupBox("Center coordinates")
        left_layout.addWidget(center_group_box)
        center_layout = QVBoxLayout()
        center_group_box.setLayout(center_layout)
        hlayout = QHBoxLayout()
        center_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("X:"))
        self.center_x_spin_box = QSpinBox()
        model.center_x.bind_spin_box(self.center_x_spin_box)
        model.center_x.register_callback("centering", self.apply_translation)
        hlayout.addWidget(self.center_x_spin_box)

        hlayout = QHBoxLayout()
        center_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Y:"))
        self.center_y_spin_box = QSpinBox()
        model.center_y.bind_spin_box(self.center_y_spin_box)
        model.center_y.register_callback("centering", self.apply_translation)
        hlayout.addWidget(self.center_y_spin_box)

        hlayout = QHBoxLayout()
        center_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Z:"))
        self.center_z_spin_box = QSpinBox()
        model.center_z.bind_spin_box(self.center_z_spin_box)
        model.center_z.register_callback("centering", self.apply_translation)
        hlayout.addWidget(self.center_z_spin_box)
        center_button = QPushButton("Set center")
        center_layout.addWidget(center_button)
        center_button.clicked.connect(self.set_center)
        #
        # ------ Offsets
        #
        offset_group_box = QGroupBox("Offsets")
        left_layout.addWidget(offset_group_box)
        offset_layout = QVBoxLayout()
        offset_group_box.setLayout(offset_layout)
        hlayout = QHBoxLayout()
        offset_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("X:"))
        self.offset_x_spin_box = QSpinBox()
        model.offset_x.bind_spin_box(self.offset_x_spin_box)
        model.offset_x.register_callback(uuid.uuid4(), self.apply_translation)
        hlayout.addWidget(self.offset_x_spin_box)

        hlayout = QHBoxLayout()
        offset_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Y:"))
        self.offset_y_spin_box = QSpinBox()
        model.offset_y.bind_spin_box(self.offset_y_spin_box)
        model.offset_y.register_callback(uuid.uuid4(), self.apply_translation)
        hlayout.addWidget(self.offset_y_spin_box)

        hlayout = QHBoxLayout()
        offset_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Z:"))
        self.offset_z_spin_box = QSpinBox()
        model.offset_z.bind_spin_box(self.offset_z_spin_box)
        model.offset_z.register_callback(uuid.uuid4(), self.apply_translation)
        hlayout.addWidget(self.offset_z_spin_box)
        #
        # ------ Angles
        #
        angle_group_box = QGroupBox("Rotation angles (-180° to 180°)")
        left_layout.addWidget(angle_group_box)
        angle_layout = QVBoxLayout()
        angle_group_box.setLayout(angle_layout)
        hlayout = QHBoxLayout()
        angle_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("X:"))
        self.angle_x_spin_box = QDoubleSpinBox()
        self.angle_x_spin_box.setRange(-180, 180)
        model.angle_x.bind_double_spin_box(self.angle_x_spin_box)
        model.angle_x.register_callback("rotation", self.apply_translation)
        hlayout.addWidget(self.angle_x_spin_box)

        hlayout = QHBoxLayout()
        angle_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Y:"))
        self.angle_y_spin_box = QDoubleSpinBox()
        self.angle_y_spin_box.setRange(-180, 180)
        model.angle_y.bind_double_spin_box(self.angle_y_spin_box)
        model.angle_y.register_callback("rotation", self.apply_translation)
        hlayout.addWidget(self.angle_y_spin_box)

        hlayout = QHBoxLayout()
        angle_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Z:"))
        self.angle_z_spin_box = QDoubleSpinBox()
        self.angle_z_spin_box.setRange(-180, 180)
        model.angle_z.bind_double_spin_box(self.angle_z_spin_box)
        model.angle_z.register_callback("rotation", self.apply_translation)
        hlayout.addWidget(self.angle_z_spin_box)
        #
        # Display parameters
        #
        display_group_box = QGroupBox("Display")
        left_layout.addWidget(display_group_box)
        dgb_layout = QVBoxLayout()
        display_group_box.setLayout(dgb_layout)
        hlayout = QHBoxLayout()
        dgb_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Fixed threshold (0 to 1)"))
        fixed_display_threshold_widget = QDoubleSpinBox()
        hlayout.addWidget(fixed_display_threshold_widget)
        fixed_display_threshold_widget.setRange(0, 1)
        self.model.fixed_display_threshold.bind_double_spin_box(
            fixed_display_threshold_widget)
        self.model.fixed_display_threshold.register_callback(
            "try_to_draw", self.try_to_draw)
        hlayout = QHBoxLayout()
        dgb_layout.addLayout(hlayout)
        hlayout.addWidget(QLabel("Moving threshold (0 to 1)"))
        moving_display_threshold_widget = QDoubleSpinBox()
        hlayout.addWidget(moving_display_threshold_widget)
        moving_display_threshold_widget.setRange(0, 1)
        self.model.moving_display_threshold.bind_double_spin_box(
            moving_display_threshold_widget)
        self.model.moving_display_threshold.register_callback(
            "try_to_draw", self.try_to_draw)

        self.scene = SceneCanvas(keys='interactive')
        splitter.addWidget(self.scene.native)
        self.view = self.scene.central_widget.add_view()

    def on_activated(self):
        self.try_to_draw()

    def set_center(self, *args):
        if not os.path.exists(self.model.fixed_precomputed_path.get()):
            return False
        fixed_url = pathlib.Path(
            self.model.fixed_precomputed_path.get()).as_uri()
        try:
            fixed_shape = \
                ArrayReader(fixed_url, format="blockfs", level=1).shape
        except:
            return False
        self.model.center_x.set(fixed_shape[2] // 2)
        self.model.center_y.set(fixed_shape[1] // 2)
        self.model.center_z.set(fixed_shape[0] // 2)

    def try_to_draw(self, *args):
        if not fixed_neuroglancer_path_is_valid(self.model):
            return
        if not moving_neuroglancer_path_is_valid(self.model):
            return
        fixed_url = fixed_neuroglancer_url(self.model)
        moving_url = moving_neuroglancer_url(self.model)
        canvas_shape = (self.scene.native.contentsRect().height(),
                        self.scene.native.contentsRect().width())
        if fixed_url != self.fixed_url or \
                self.canvas_shape != canvas_shape:
            # We have to update the fixed volume
            fixed_array = None
            fixed_shape = \
                ArrayReader(fixed_url, format="blockfs", level=1).shape
            self.center_x_spin_box.setMinimum(0)
            self.center_x_spin_box.setMaximum(fixed_shape[2])
            self.offset_x_spin_box.setMinimum(-fixed_shape[2])
            self.offset_x_spin_box.setMaximum(fixed_shape[2])
            self.center_y_spin_box.setMinimum(0)
            self.center_y_spin_box.setMaximum(fixed_shape[1])
            self.offset_y_spin_box.setMinimum(-fixed_shape[1])
            self.offset_y_spin_box.setMaximum(fixed_shape[1])
            self.center_z_spin_box.setMinimum(0)
            self.center_z_spin_box.setMaximum(fixed_shape[0])
            self.offset_z_spin_box.setMinimum(-fixed_shape[0])
            self.offset_z_spin_box.setMaximum(fixed_shape[0])
            for level_idx in range(0, 6):
                level = 2**level_idx
                try:
                    test_fixed_array = ArrayReader(fixed_url,
                                                   format="blockfs",
                                                   level=level)
                    test_shape = test_fixed_array.shape
                    fixed_array = test_fixed_array
                    self.level = level
                    if test_shape[1] < canvas_shape[0] // 2 or \
                       test_shape[2] < canvas_shape[1] // 2:
                        break
                except:
                    break
            if not fixed_array:
                return
            self.fixed_volume = fixed_array[0:test_shape[0], 0:test_shape[1],
                                            0:test_shape[2]]
            self.fixed_volume = (
                np.clip(self.fixed_volume.astype(np.float32), 100, 1000) /
                1000 * 255).astype(np.uint8)
            self.fixed_url = fixed_url
            need_to_do_moving = True
        else:
            need_to_do_moving = False
        if need_to_do_moving or moving_url != self.moving_url:
            try:
                moving_array = ArrayReader(moving_url,
                                           format="blockfs",
                                           level=self.level)
                moving_shape = moving_array.shape
                self.moving_volume = \
                    moving_array[0:moving_shape[0], 0:moving_shape[1],
                                 0:moving_shape[2]]
                self.moving_volume = (
                    np.clip(self.moving_volume.astype(np.float32), 100, 1000) /
                    1000 * 255).astype(np.uint8)
                self.moving_url = moving_url
            except:
                return
        self.draw_scene()

    def apply_translation(self, *args):
        if self.view and self.level:
            rmatrix = np.eye(4)
            r = rotation_matrix((self.model.angle_z.get() * np.pi / 180,
                                 self.model.angle_y.get() * np.pi / 180,
                                 self.model.angle_x.get() * np.pi / 180))

            rmatrix[:3, :3] = r[::-1, ::-1]
            cmatrix = np.array([
                self.model.center_x.get() / self.level,
                self.model.center_y.get() / self.level,
                self.model.center_z.get() / self.level
            ])
            tmatrix = np.array([
                self.model.offset_x.get() / self.level,
                self.model.offset_y.get() / self.level,
                self.model.offset_z.get() / self.level
            ])
            #
            # The last of 4 columns of the affine transform is the
            # translation. The translation is the rotated center minus the
            # offset plus the center, not rotated.
            #
            rmatrix[3, :3] = rmatrix[:3, :3].dot(tmatrix - cmatrix) + cmatrix
            rotate_transform = MatrixTransform(rmatrix)
            self.translation_frame.transform = rotate_transform
            self.center_frame.transform = MatrixTransform(
                np.array([[1, 0, 0, cmatrix[0]], [0, 1, 0, cmatrix[1]],
                          [0, 0, 1, cmatrix[2]], [0, 0, 0, 1]]))
            self.scene.update()

    def draw_scene(self):
        self.fixed_frame = scene.node.Node(self.view.scene)
        self.fixed_frame.transform = STTransform(
            translate=(-self.moving_volume.shape[2] // 2,
                       -self.moving_volume.shape[1] // 2, -500))
        fixed_volume = scene.visuals.Volume(
            self.fixed_volume,
            parent=self.fixed_frame,
            threshold=self.model.fixed_display_threshold.get(),
            emulate_texture=False)
        fixed_volume.cmap = TranslucentFixedColormap()
        fixed_volume.method = VOLUME_RENDERING_METHOD
        #
        # The transformation is done as follows:
        #
        # The translation frame handles the offset
        #
        # The centering frame picks the center of the moving frame
        #
        # The rotation frame rotates about the center
        #
        # The uncentering frame readjusts the coordinates so that 0, 0 is
        # placed away from the center.
        #
        self.translation_frame = scene.node.Node(self.fixed_frame)

        moving_volume = scene.visuals.Volume(
            self.moving_volume,
            parent=self.translation_frame,
            threshold=self.model.moving_display_threshold.get(),
            emulate_texture=False)
        moving_volume.cmap = TranslucentMovingColormap()
        moving_volume.method = VOLUME_RENDERING_METHOD
        self.camera = scene.cameras.TurntableCamera(
            parent=self.view.scene,
            fov=60.,
            elevation=self.fixed_volume.shape[2] // 2,
            name="Turntable")
        self.view.camera = self.camera
        self.center_frame = scene.node.Node(parent=self.view)
        self.axis = scene.visuals.XYZAxis(parent=self.center_frame)
        axis_t = STTransform(scale=(50, 50, 50, 1))
        self.axis.transform = axis_t.as_matrix()
        self.apply_translation()
        self.scene.events.mouse_move.connect(self.on_mouse_move)

    def on_mouse_move(self, event):
        if event.button == 1 and event.is_dragging:
            self.axis.transform.reset()

            self.axis.transform.rotate(self.camera.roll, (0, 0, 1))
            self.axis.transform.rotate(self.camera.elevation, (1, 0, 0))
            self.axis.transform.rotate(self.camera.azimuth, (0, 1, 0))
            self.axis.transform.translate(
                (self.model.center_x.get() / self.level,
                 self.model.center_y.get() / self.level,
                 self.model.center_z.get() / self.level))
            self.axis.transform.scale((50, 50, 0.001))
            #axis.transform.translate((50., 50.))
            self.axis.update()