Ejemplo n.º 1
0
    def set_channels(self,
                     data_arrays,
                     shape=None,
                     cell_width=None,
                     cell_height=None,
                     origin_x=None,
                     origin_y=None):
        assert (shape or data_arrays
                is not None), "`data` or `shape` must be provided"
        if cell_width is not None:
            self.cell_width = cell_width
        if cell_height:
            self.cell_height = cell_height  # Note: cell_height is usually negative
        if origin_x:
            self.origin_x = origin_x
        if origin_y:
            self.origin_y = origin_y
        self.shape = self._compute_shape(shape, data_arrays)
        assert None not in (self.cell_width, self.cell_height, self.origin_x,
                            self.origin_y, self.shape)
        # how many of the higher resolution channel tiles (smaller geographic area) make
        # up a low resolution channel tile
        self._channel_factors = tuple(
            self.shape[0] / float(chn.shape[0]) if chn is not None else 1.
            for chn in data_arrays)
        self._lowest_factor = max(self._channel_factors)
        self._lowest_rez = Resolution(
            abs(self.cell_height * self._lowest_factor),
            abs(self.cell_width * self._lowest_factor))

        # Where does this image lie in this lonely world
        self.calc = TileCalculator(self.name,
                                   self.shape,
                                   Point(x=self.origin_x, y=self.origin_y),
                                   Resolution(dy=abs(self.cell_height),
                                              dx=abs(self.cell_width)),
                                   self.tile_shape,
                                   self.texture_shape,
                                   wrap_lon=self.wrap_lon)

        # Reset texture state, if we change things to know which texture
        # don't need to be updated then this can be removed/changed
        self.texture_state.reset()
        self._need_texture_upload = True
        self._need_vertex_update = True
        # Reset the tiling logic to force a retile
        # even though we might be looking at the exact same spot
        self._latest_tile_box = None
Ejemplo n.º 2
0
    def _init_geo_parameters(self, origin_x, origin_y, cell_width, cell_height,
                             projection, texture_shape, tile_shape, wrap_lon,
                             shape, data):
        self._viewable_mesh_mask = None
        self._ref1 = None
        self._ref2 = None

        self.origin_x = origin_x
        self.origin_y = origin_y
        self.cell_width = cell_width
        self.cell_height = cell_height  # Note: cell_height is usually negative
        self.texture_shape = texture_shape
        self.tile_shape = tile_shape
        self.num_tex_tiles = self.texture_shape[0] * self.texture_shape[1]
        self._stride = (0, 0)
        self._latest_tile_box = None
        self.wrap_lon = wrap_lon
        self._tiles = {}
        assert (shape
                or data is not None), "`data` or `shape` must be provided"
        self.shape = shape or data.shape
        self.ndim = len(self.shape) or data.ndim

        # Where does this image lie in this lonely world
        self.calc = TileCalculator(
            self.name,
            self.shape,
            Point(x=self.origin_x, y=self.origin_y),
            Resolution(dy=abs(self.cell_height), dx=abs(self.cell_width)),
            self.tile_shape,
            self.texture_shape,
            wrap_lon=self.wrap_lon,
            projection=projection,
        )
        # What tiles have we used and can we use
        self.texture_state = TextureTileState(self.num_tex_tiles)
Ejemplo n.º 3
0
def test_visible_tiles(tc_params, vg, etiles, stride, exp):
    """Test returned box of tiles to draw is correct given a visible world geometry and sampling."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.visible_tiles(vg, stride, etiles)
    assert res == exp
Ejemplo n.º 4
0
def test_calc_texture_coordinates(tc_params, ti, fr, ofr, tl, exp):
    """Test texture coordinates for a given tile are correct."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.calc_texture_coordinates(ti, fr, ofr, tl)
    assert np.array_equal(res, exp)
Ejemplo n.º 5
0
def test_calc_overview_stride(tc_params, ims, exp):
    """Test calculated stride is correct given a valid image."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.calc_overview_stride(ims)
    assert res == exp
Ejemplo n.º 6
0
def test_calc_stride(tc_params, v, t, exp):
    """Test calculated stride value is correct given world geometry and sampling."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.calc_stride(v, t)
    assert res == exp
Ejemplo n.º 7
0
def test_calc_tile_fraction(tc_params, tiy, tix, s, exp):
    """Test calculated fractional components of the specified tile are correct."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.calc_tile_fraction(tiy, tix, s)
    assert res == exp
Ejemplo n.º 8
0
def test_calc_tile_slice(tc_params, tiy, tix, s, exp, monkeypatch):
    """Test appropriate slice is returned given image data."""
    tile_calc = TileCalculator(*tc_params)
    res = tile_calc.calc_tile_slice(tiy, tix, s)
    assert res == exp
Ejemplo n.º 9
0
class SIFTMultiChannelTiledGeolocatedMixin(SIFTTiledGeolocatedMixin):
    def _normalize_data(self, data_arrays):
        if not isinstance(data_arrays, (list, tuple)):
            return super()._normalize_data(data_arrays)

        new_data = []
        for data in data_arrays:
            new_data.append(super()._normalize_data(data))
        return new_data

    def _init_geo_parameters(self, origin_x, origin_y, cell_width, cell_height,
                             projection, texture_shape, tile_shape, wrap_lon,
                             shape, data_arrays):
        if shape is None:
            shape = self._compute_shape(shape, data_arrays)
        ndim = len(shape) or [x for x in data_arrays if x is not None][0].ndim
        data = ArrayProxy(ndim, shape)
        super()._init_geo_parameters(
            origin_x,
            origin_y,
            cell_width,
            cell_height,
            projection,
            texture_shape,
            tile_shape,
            wrap_lon,
            shape,
            data,
        )

        self.set_channels(
            data_arrays,
            shape=shape,
            cell_width=cell_width,
            cell_height=cell_height,
            origin_x=origin_x,
            origin_y=origin_y,
        )

    def set_channels(self,
                     data_arrays,
                     shape=None,
                     cell_width=None,
                     cell_height=None,
                     origin_x=None,
                     origin_y=None):
        assert (shape or data_arrays
                is not None), "`data` or `shape` must be provided"
        if cell_width is not None:
            self.cell_width = cell_width
        if cell_height:
            self.cell_height = cell_height  # Note: cell_height is usually negative
        if origin_x:
            self.origin_x = origin_x
        if origin_y:
            self.origin_y = origin_y
        self.shape = self._compute_shape(shape, data_arrays)
        assert None not in (self.cell_width, self.cell_height, self.origin_x,
                            self.origin_y, self.shape)
        # how many of the higher resolution channel tiles (smaller geographic area) make
        # up a low resolution channel tile
        self._channel_factors = tuple(
            self.shape[0] / float(chn.shape[0]) if chn is not None else 1.
            for chn in data_arrays)
        self._lowest_factor = max(self._channel_factors)
        self._lowest_rez = Resolution(
            abs(self.cell_height * self._lowest_factor),
            abs(self.cell_width * self._lowest_factor))

        # Where does this image lie in this lonely world
        self.calc = TileCalculator(self.name,
                                   self.shape,
                                   Point(x=self.origin_x, y=self.origin_y),
                                   Resolution(dy=abs(self.cell_height),
                                              dx=abs(self.cell_width)),
                                   self.tile_shape,
                                   self.texture_shape,
                                   wrap_lon=self.wrap_lon)

        # Reset texture state, if we change things to know which texture
        # don't need to be updated then this can be removed/changed
        self.texture_state.reset()
        self._need_texture_upload = True
        self._need_vertex_update = True
        # Reset the tiling logic to force a retile
        # even though we might be looking at the exact same spot
        self._latest_tile_box = None

    @staticmethod
    def _compute_shape(shape, data_arrays):
        return shape or max(data.shape
                            for data in data_arrays if data is not None)

    def _get_stride(self, view_box):
        s = self.calc.calc_stride(view_box, texture=self._lowest_rez)
        return Point(np.int64(s[0] * self._lowest_factor),
                     np.int64(s[1] * self._lowest_factor))

    def _slice_texture_tile(self, data_arrays, y_slice, x_slice):
        new_data = []
        for data in data_arrays:
            if data is not None:
                # explicitly ask for the parent class of MultiBandTextureAtlas2D
                data = super()._slice_texture_tile(data, y_slice, x_slice)
            new_data.append(data)
        return new_data
Ejemplo n.º 10
0
class SIFTTiledGeolocatedMixin:
    def __init__(self,
                 data,
                 *area_params,
                 tile_shape=(DEFAULT_TILE_HEIGHT, DEFAULT_TILE_WIDTH),
                 texture_shape=(DEFAULT_TEXTURE_HEIGHT, DEFAULT_TEXTURE_WIDTH),
                 wrap_lon=False,
                 projection=DEFAULT_PROJECTION,
                 **visual_kwargs):
        origin_x, origin_y, cell_width, cell_height = area_params
        if visual_kwargs.get("method", "subdivide") != "subdivide":
            raise ValueError("Only 'subdivide' drawing method is supported.")
        visual_kwargs["method"] = "subdivide"
        if "grid" in visual_kwargs:
            raise ValueError(
                "The 'grid' keyword argument is not supported with the tiled mixin."
            )

        # visual nodes already have names, so be careful
        if not hasattr(self, "name"):
            self.name = visual_kwargs.pop("name", None)

        self._init_geo_parameters(
            origin_x,
            origin_y,
            cell_width,
            cell_height,
            projection,
            texture_shape,
            tile_shape,
            wrap_lon,
            visual_kwargs.get('shape'),
            data,
        )

        # Call the init of the Visual
        super().__init__(data, **visual_kwargs)

    def _init_geo_parameters(self, origin_x, origin_y, cell_width, cell_height,
                             projection, texture_shape, tile_shape, wrap_lon,
                             shape, data):
        self._viewable_mesh_mask = None
        self._ref1 = None
        self._ref2 = None

        self.origin_x = origin_x
        self.origin_y = origin_y
        self.cell_width = cell_width
        self.cell_height = cell_height  # Note: cell_height is usually negative
        self.texture_shape = texture_shape
        self.tile_shape = tile_shape
        self.num_tex_tiles = self.texture_shape[0] * self.texture_shape[1]
        self._stride = (0, 0)
        self._latest_tile_box = None
        self.wrap_lon = wrap_lon
        self._tiles = {}
        assert (shape
                or data is not None), "`data` or `shape` must be provided"
        self.shape = shape or data.shape
        self.ndim = len(self.shape) or data.ndim

        # Where does this image lie in this lonely world
        self.calc = TileCalculator(
            self.name,
            self.shape,
            Point(x=self.origin_x, y=self.origin_y),
            Resolution(dy=abs(self.cell_height), dx=abs(self.cell_width)),
            self.tile_shape,
            self.texture_shape,
            wrap_lon=self.wrap_lon,
            projection=projection,
        )
        # What tiles have we used and can we use
        self.texture_state = TextureTileState(self.num_tex_tiles)

    def _normalize_data(self, data):
        if data is not None and data.dtype == np.float64:
            data = data.astype(np.float32)
        return data

    def _build_texture_tiles(self, data, stride, tile_box: Box):
        """Prepare and organize strided data in to individual tiles with associated information.
        """
        data = self._normalize_data(data)

        LOG.debug("Uploading texture data for %d tiles (%r)",
                  (tile_box.bottom - tile_box.top) *
                  (tile_box.right - tile_box.left), tile_box)
        # Tiles start at upper-left so go from top to bottom
        tiles_info = []
        for tiy in range(tile_box.top, tile_box.bottom):
            for tix in range(tile_box.left, tile_box.right):
                already_in = (stride, tiy, tix) in self.texture_state
                # Update the age if already in there
                # Assume that texture_state does not change from the main thread if this is run in another
                tex_tile_idx = self.texture_state.add_tile((stride, tiy, tix))
                if already_in:
                    # FIXME: we should make a list/set of the tiles we need to add before this
                    continue

                # Assume we were given a total image worth of this stride
                y_slice, x_slice = self.calc.calc_tile_slice(tiy, tix, stride)
                tile_data = self._slice_texture_tile(data, y_slice, x_slice)
                tiles_info.append((stride, tiy, tix, tex_tile_idx, tile_data))

        return tiles_info

    def _slice_texture_tile(self, data, y_slice, x_slice):
        # force a copy of the data from the content array (provided by the workspace)
        # to a vispy-compatible contiguous float array
        # this can be a potentially time-expensive operation since content array is
        # often huge and always memory-mapped, so paging may occur
        # we don't want this paging deferred until we're back in the GUI thread pushing data to OpenGL!
        return np.array(data[y_slice, x_slice], dtype=np.float32)

    def _set_texture_tiles(self, tiles_info):
        for tile_info in tiles_info:
            stride, tiy, tix, tex_tile_idx, data = tile_info
            self._texture.set_tile_data(tex_tile_idx, data)

    def _build_vertex_tiles(self, preferred_stride, tile_box: Box):
        """Rebuild the vertex buffers used for rendering the image when using
        the subdivide method.

        SIFT Note: Copied from 0.5.0dev original ImageVisual class
        """
        total_num_tiles = (tile_box.bottom - tile_box.top) * (tile_box.right -
                                                              tile_box.left)

        if total_num_tiles <= 0:
            # we aren't looking at this image
            # FIXME: What's the correct way to stop drawing here
            raise RuntimeError(
                "View calculations determined a negative number of tiles are visible"
            )
        elif total_num_tiles > self.num_tex_tiles:
            LOG.warning(
                "Current view sees more tiles than can be held in the GPU")
            # We continue on, showing as many tiles as we can

        tex_coords = np.empty(
            (6 * total_num_tiles * (TESS_LEVEL * TESS_LEVEL), 2),
            dtype=np.float32)
        vertices = np.empty(
            (6 * total_num_tiles * (TESS_LEVEL * TESS_LEVEL), 2),
            dtype=np.float32)

        # What tile are we currently describing out of all the tiles being viewed
        used_tile_idx = -1
        LOG.debug("Building vertex data for %d tiles (%r)", total_num_tiles,
                  tile_box)
        tl = TESS_LEVEL * TESS_LEVEL
        # Tiles start at upper-left so go from top to bottom
        for tiy in range(tile_box.top, tile_box.bottom):
            for tix in range(tile_box.left, tile_box.right):
                # Update the index here because we have multiple exit/continuation points
                used_tile_idx += 1

                # Check if the tile we want to draw is actually in the GPU
                # if not (atlas too small?) fill with zeros and keep going
                if (preferred_stride, tiy, tix) not in self.texture_state:
                    # THIS SHOULD NEVER HAPPEN IF TEXTURE BUILDING IS DONE CORRECTLY AND THE ATLAS IS BIG ENOUGH
                    tile_start = TESS_LEVEL * TESS_LEVEL * used_tile_idx * 6
                    tile_end = TESS_LEVEL * TESS_LEVEL * (used_tile_idx +
                                                          1) * 6
                    tex_coords[tile_start:tile_end, :] = 0
                    vertices[tile_start:tile_end, :] = 0
                    continue

                # we should have already loaded the texture data in to the GPU so get the index of that texture
                tex_tile_idx = self.texture_state[(preferred_stride, tiy, tix)]
                factor_rez, offset_rez = self.calc.calc_tile_fraction(
                    tiy, tix, preferred_stride)
                tex_coords[tl * used_tile_idx * 6: tl * (used_tile_idx + 1) * 6, :] = \
                    self.calc.calc_texture_coordinates(tex_tile_idx, factor_rez, offset_rez,
                                                       tessellation_level=TESS_LEVEL)
                vertices[tl * used_tile_idx * 6:tl * (used_tile_idx + 1) *
                         6, :] = self.calc.calc_vertex_coordinates(
                             tiy,
                             tix,
                             preferred_stride[0],
                             preferred_stride[1],
                             factor_rez,
                             offset_rez,
                             tessellation_level=TESS_LEVEL)

        return vertices, tex_coords

    def _set_vertex_tiles(self, vertices, tex_coords):
        self._subdiv_position.set_data(vertices.astype('float32'))
        self._subdiv_texcoord.set_data(tex_coords.astype('float32'))

    def determine_reference_points(self):
        # Image points transformed to canvas coordinates
        img_cmesh = self.transforms.get_transform().map(self.calc.image_mesh)
        # Mask any points that are really far off screen (can't be transformed)
        valid_mask = (np.abs(img_cmesh[:, 0]) < CANVAS_EPSILON) & (np.abs(
            img_cmesh[:, 1]) < CANVAS_EPSILON)
        # The image mesh projected to canvas coordinates (valid only)
        img_cmesh = img_cmesh[valid_mask]
        # The image mesh of only valid "viewable" projected coordinates
        img_vbox = self.calc.image_mesh[valid_mask]

        if not img_cmesh[:, 0].size or not img_cmesh[:, 1].size:
            self._viewable_mesh_mask = None
            self._ref1, self._ref2 = None, None
            return

        x_cmin, x_cmax = img_cmesh[:, 0].min(), img_cmesh[:, 0].max()
        y_cmin, y_cmax = img_cmesh[:, 1].min(), img_cmesh[:, 1].max()
        center_x = (x_cmax - x_cmin) / 2. + x_cmin
        center_y = (y_cmax - y_cmin) / 2. + y_cmin
        dist = img_cmesh.copy()
        dist[:, 0] = center_x - img_cmesh[:, 0]
        dist[:, 1] = center_y - img_cmesh[:, 1]
        self._viewable_mesh_mask = valid_mask
        self._ref1, self._ref2 = get_reference_points(dist, img_vbox)

    def get_view_box(self):
        """Calculate shown portion of image and image units per pixel

        This method utilizes a precomputed "mesh" of relatively evenly
        spaced points over the entire image space. This mesh is transformed
        to the canvas space (-1 to 1 user-viewed space) to figure out which
        portions of the image are currently being viewed and which portions
        can actually be projected on the viewed projection.

        While the result of the chosen method may not always be completely
        accurate, it should work for all possible viewing cases.
        """
        if self._viewable_mesh_mask is None or self.canvas.size[
                0] == 0 or self.canvas.size[1] == 0:
            raise ValueError("Image '%s' is not viewable in this projection" %
                             (self.name, ))

        # Image points transformed to canvas coordinates
        img_cmesh = self.transforms.get_transform().map(self.calc.image_mesh)
        # The image mesh projected to canvas coordinates (valid only)
        img_cmesh = img_cmesh[self._viewable_mesh_mask]
        # The image mesh of only valid "viewable" projected coordinates
        img_vbox = self.calc.image_mesh[self._viewable_mesh_mask]

        ref_idx_1, ref_idx_2 = get_reference_points(img_cmesh, img_vbox)
        dx, dy = calc_pixel_size(img_cmesh[(self._ref1, self._ref2), :],
                                 img_vbox[(self._ref1, self._ref2), :],
                                 self.canvas.size)
        view_extents = self.calc.calc_view_extents(img_cmesh[ref_idx_1],
                                                   img_vbox[ref_idx_1],
                                                   self.canvas.size, dx, dy)
        return ViewBox(*view_extents, dx=dx, dy=dy)

    def _get_stride(self, view_box):
        return self.calc.calc_stride(view_box)

    def assess(self):
        """Determine if a retile is needed.

        Tell workspace we will be needed
        """
        try:
            view_box = self.get_view_box()
            preferred_stride = self._get_stride(view_box)
            tile_box = self.calc.visible_tiles(view_box,
                                               stride=preferred_stride,
                                               extra_tiles_box=Box(1, 1, 1, 1))
        except ValueError as e:
            # If image is outside of canvas, then an exception will be raised
            LOG.warning(
                "Could not determine viewable image area for '{}': {}".format(
                    self.name, e))
            return False, self._stride, self._latest_tile_box

        num_tiles = (tile_box.bottom - tile_box.top) * (tile_box.right -
                                                        tile_box.left)
        LOG.debug(
            "Assessment: Prefer '%s' have '%s', was looking at %r, now looking at %r",
            preferred_stride, self._stride, self._latest_tile_box, tile_box)

        # If we zoomed out or we panned
        need_retile = (num_tiles > 0) and (preferred_stride != self._stride or
                                           self._latest_tile_box != tile_box)

        return need_retile, preferred_stride, tile_box

    def retile(self, data, preferred_stride, tile_box):
        """Get data from workspace and retile/retexture as needed.
        """
        tiles_info = self._build_texture_tiles(data, preferred_stride,
                                               tile_box)
        vertices, tex_coords = self._build_vertex_tiles(
            preferred_stride, tile_box)
        return tiles_info, vertices, tex_coords

    def set_retiled(self, preferred_stride, tile_box, tiles_info, vertices,
                    tex_coords):
        self._set_texture_tiles(tiles_info)
        self._set_vertex_tiles(vertices, tex_coords)

        # don't update here, the caller will do that
        # Store the most recent level of detail that we've done
        self._stride = preferred_stride
        self._latest_tile_box = tile_box