def visible_tiles(self, visible_geom, stride=None, extra_tiles_box=None) -> Box: """ given a visible world geometry and sampling, return (sampling-state, [Box-of-tiles-to-draw]) sampling state is WELLSAMPLED/OVERSAMPLED/UNDERSAMPLED returned Box should be iterated per standard start:stop style tiles are specified as (iy,ix) integer pairs extra_box value says how many extra tiles to include around each edge """ if stride is None: stride = Point(1, 1) if extra_tiles_box is None: extra_tiles_box = Box(0, 0, 0, 0) v = visible_geom e = extra_tiles_box return visible_tiles( float(self.pixel_rez[0]), float(self.pixel_rez[1]), float(self.tile_size[0]), float(self.tile_size[1]), float(self.image_center[0]), float(self.image_center[1]), int(self.image_shape[0]), int(self.image_shape[1]), int(self.tile_shape[0]), int(self.tile_shape[1]), float(v[0]), float(v[1]), float(v[2]), float(v[3]), float(v[4]), float(v[5]), int(stride[0]), int(stride[1]), int(e[0]), int(e[1]), int(e[2]), int(e[3]))
def init_overview(self, data_arrays): """Create and add a low resolution version of the data that is always shown behind the higher resolution image tiles. """ # FUTURE: Actually use this data attribute. For now let the base # think there is data (not None) self._data = ArrayProxy(self.ndim, self.shape) self.overview_info = nfo = {} y_slice, x_slice = self.calc.overview_stride # Update kwargs to reflect the new spatial resolution of the overview image nfo["cell_width"] = self.cell_width * x_slice.step nfo["cell_height"] = self.cell_height * y_slice.step # Tell the texture state that we are adding a tile that should never expire and should always exist nfo["texture_tile_index"] = ttile_idx = self.texture_state.add_tile((0, 0, 0), expires=False) for idx, data in enumerate(data_arrays): if data is not None: _y_slice, _x_slice = self.calc.calc_overview_stride(image_shape=data.shape) overview_data = data[_y_slice, _x_slice] else: overview_data = None self._textures[idx].set_tile_data(ttile_idx, self._normalize_data(overview_data)) # Handle wrapping around the anti-meridian so there is a -180/180 continuous image num_tiles = 1 if not self.wrap_lon else 2 tl = TESS_LEVEL * TESS_LEVEL nfo["texture_coordinates"] = np.empty((6 * num_tiles * tl, 2), dtype=np.float32) nfo["vertex_coordinates"] = np.empty((6 * num_tiles * tl, 2), dtype=np.float32) factor_rez, offset_rez = self.calc.calc_tile_fraction( 0, 0, Point(np.int64(y_slice.step), np.int64(x_slice.step))) nfo["texture_coordinates"][:6 * tl, :2] = self.calc.calc_texture_coordinates(ttile_idx, factor_rez, offset_rez, tessellation_level=TESS_LEVEL) nfo["vertex_coordinates"][:6 * tl, :2] = self.calc.calc_vertex_coordinates(0, 0, y_slice.step, x_slice.step, factor_rez, offset_rez, tessellation_level=TESS_LEVEL) self._set_vertex_tiles(nfo["vertex_coordinates"], nfo["texture_coordinates"])
def calc_stride(v_dx, v_dy, t_dx, t_dy, overview_stride_y, overview_stride_x): # screen dy,dx in world distance per pixel # world distance per pixel for our data # compute texture pixels per screen pixels tsy = min(overview_stride_y, max(1, np.ceil(v_dy * PREFERRED_SCREEN_TO_TEXTURE_RATIO / t_dy))) tsx = min(overview_stride_x, max(1, np.ceil(v_dx * PREFERRED_SCREEN_TO_TEXTURE_RATIO / t_dx))) return Point(np.int64(tsy), np.int64(tsx))
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
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)
monkeypatch.setattr('uwsift.view.tile_calculator.calc_stride', calc_stride.py_func) monkeypatch.setattr('uwsift.view.tile_calculator.calc_overview_stride', calc_overview_stride.py_func) monkeypatch.setattr( 'uwsift.view.tile_calculator.calc_vertex_coordinates', calc_vertex_coordinates.py_func) monkeypatch.setattr( 'uwsift.view.tile_calculator.calc_texture_coordinates', calc_texture_coordinates.py_func) @pytest.mark.parametrize("tc_params,vg,etiles,stride,exp", [([ "test", (500, 500), Point(500000, -500000), Resolution(200, 200), (50, 50) ], ViewBox(200000, -300000, 500000, -6000, 500, 400), (1, 1, 1, 1), (2, 2), Box(bottom=3, left=7, top=-2, right=3))]) 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 @pytest.mark.parametrize( "cp,ip,cs,exp", [([[10.0, 10.0], [20.0, 20.0]], [[10.0, 10.0], [20.0, 20.0]], (1, 1), (2.0, 2.0))])
def __init__(self, name, image_shape, ul_origin, pixel_rez, tile_shape=(DEFAULT_TILE_HEIGHT, DEFAULT_TILE_WIDTH), texture_shape=(DEFAULT_TEXTURE_HEIGHT, DEFAULT_TEXTURE_WIDTH), projection=DEFAULT_PROJECTION, wrap_lon=False): """Initialize numbers used by multiple calculations. Args: name (str): the 'name' of the tile, typically the path of the file it represents image_shape (int, int): (height, width) in pixels ul_origin (float, float): (y, x) in world coords specifies upper-left coordinate of the image pixel_rez (float, float): (dy, dx) in world coords per pixel ascending from corner [0,0], as measured near zero_point tile_shape (int, int): the pixel dimensions (h:int, w:int) of the GPU tiling we want to use texture_shape (int, int): the size of the texture being used (h, w) in number of tiles Notes: - Tiling is aligned to pixels, not world - World coordinates are eqm such that 0,0 matches 0°N 0°E, going north/south +-90° and west/east +-180° - Data coordinates are pixels with b l or b r corner being 0,0 """ super(TileCalculator, self).__init__() self.name = name self.image_shape = Point(np.int64(image_shape[0]), np.int64(image_shape[1])) self.ul_origin = Point(*ul_origin) self.pixel_rez = Resolution(np.float64(pixel_rez[0]), np.float64(pixel_rez[1])) self.tile_shape = Point(np.int64(tile_shape[0]), np.int64(tile_shape[1])) # in units of tiles: self.texture_shape = texture_shape # in units of data elements (float32): self.texture_size = (self.texture_shape[0] * self.tile_shape[0], self.texture_shape[1] * self.tile_shape[1]) self.image_tiles_avail = (self.image_shape[0] / self.tile_shape[0], self.image_shape[1] / self.tile_shape[1]) self.wrap_lon = wrap_lon self.proj = Proj(projection) self.image_extents_box = e = Box( bottom=np.float64(self.ul_origin[0] - self.image_shape[0] * self.pixel_rez.dy), top=np.float64(self.ul_origin[0]), left=np.float64(self.ul_origin[1]), right=np.float64(self.ul_origin[1] + self.image_shape[1] * self.pixel_rez.dx), ) # Array of points across the image space to be used as an estimate of image coverage # Used when checking if the image is viewable on the current canvas's projection self.image_mesh = np.meshgrid( np.linspace(e.left, e.right, IMAGE_MESH_SIZE), np.linspace(e.bottom, e.top, IMAGE_MESH_SIZE)) self.image_mesh = np.column_stack(( self.image_mesh[0].ravel(), self.image_mesh[1].ravel(), )) self.image_center = Point( self.ul_origin.y - self.image_shape[0] / 2. * self.pixel_rez.dy, self.ul_origin.x + self.image_shape[1] / 2. * self.pixel_rez.dx) # size of tile in image projection self.tile_size = Resolution(self.pixel_rez.dy * self.tile_shape[0], self.pixel_rez.dx * self.tile_shape[1]) self.overview_stride = self.calc_overview_stride()
def visible_tiles(z_dy, z_dx, tile_size_dy, tile_size_dx, image_center_y, image_center_x, image_shape_y, image_shape_x, tile_shape_y, tile_shape_x, v_bottom, v_left, v_top, v_right, v_dy, v_dx, stride_y, stride_x, x_bottom, x_left, x_top, x_right): tile_size = Resolution(tile_size_dy * stride_y, tile_size_dx * stride_x) # should be the upper-left corner of the tile centered on the center of the image to = Point(image_center_y + tile_size.dy / 2., image_center_x - tile_size.dx / 2.) # tile origin # number of data pixels between view edge and originpoint pv = Box(bottom=(v_bottom - to.y) / -(z_dy * stride_y), top=(v_top - to.y) / -(z_dy * stride_y), left=(v_left - to.x) / (z_dx * stride_x), right=(v_right - to.x) / (z_dx * stride_x)) th = tile_shape_y tw = tile_shape_x # first tile we'll need is (tiy0, tix0) # floor to make sure we get the upper-left of the theoretical tile tiy0 = np.floor(pv.top / th) tix0 = np.floor(pv.left / tw) # number of tiles wide and high we'll absolutely need # add 0.5 and ceil to make sure we include all possible tiles # NOTE: output r and b values are exclusive, l and t are inclusive nth = np.ceil((pv.bottom - tiy0 * th) / th + 0.5) ntw = np.ceil((pv.right - tix0 * tw) / tw + 0.5) # now add the extras if x_bottom > 0: nth += int(x_bottom) if x_left > 0: tix0 -= int(x_left) ntw += int(x_left) if x_top > 0: tiy0 -= int(x_top) nth += int(x_top) if x_right > 0: ntw += int(x_right) # Total number of tiles in this image at this stride (could be fractional) ath, atw = max_tiles_available(image_shape_y, image_shape_x, tile_shape_y, tile_shape_x, stride_y, stride_x) # truncate to the available tiles hw = atw / 2. hh = ath / 2. # center tile is half pixel off because we want center of the center # tile to be at the center of the image if tix0 < -hw + 0.5: ntw += hw - 0.5 + tix0 tix0 = -hw + 0.5 if tiy0 < -hh + 0.5: nth += hh - 0.5 + tiy0 tiy0 = -hh + 0.5 # add 0.5 to include the "end of the tile" since the r and b are exclusive if tix0 + ntw > hw + 0.5: ntw = hw + 0.5 - tix0 if tiy0 + nth > hh + 0.5: nth = hh + 0.5 - tiy0 tilebox = Box( bottom=np.int64(np.ceil(tiy0 + nth)), left=np.int64(np.floor(tix0)), top=np.int64(np.floor(tiy0)), right=np.int64(np.ceil(tix0 + ntw)), ) return tilebox
def __init__(self, data, origin_x, origin_y, cell_width, cell_height, shape=None, tile_shape=(DEFAULT_TILE_HEIGHT, DEFAULT_TILE_WIDTH), texture_shape=(DEFAULT_TEXTURE_HEIGHT, DEFAULT_TEXTURE_WIDTH), wrap_lon=False, projection=DEFAULT_PROJECTION, cmap='viridis', method='tiled', clim='auto', gamma=1., interpolation='nearest', **kwargs): if method != 'tiled': raise ValueError("Only 'tiled' method is currently supported") method = 'subdivide' grid = (1, 1) # visual nodes already have names, so be careful if not hasattr(self, "name"): self.name = kwargs.get("name", None) 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) # Current stride is None when we are showing the overview 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) # load 'float packed rgba8' interpolation kernel # to load float interpolation kernel use # `load_spatial_filters(packed=False)` kernel, self._interpolation_names = load_spatial_filters() self._kerneltex = Texture2D(kernel, interpolation='nearest') # The unpacking can be debugged by changing "spatial-filters.frag" # to have the "unpack" function just return the .r component. That # combined with using the below as the _kerneltex allows debugging # of the pipeline # self._kerneltex = Texture2D(kernel, interpolation='linear', # internalformat='r32f') # create interpolation shader functions for available # interpolations fun = [Function(_interpolation_template % n) for n in self._interpolation_names] self._interpolation_names = [n.lower() for n in self._interpolation_names] self._interpolation_fun = dict(zip(self._interpolation_names, fun)) self._interpolation_names.sort() self._interpolation_names = tuple(self._interpolation_names) # overwrite "nearest" and "bilinear" spatial-filters # with "hardware" interpolation _data_lookup_fn self._interpolation_fun['nearest'] = Function(_texture_lookup) self._interpolation_fun['bilinear'] = Function(_texture_lookup) if interpolation not in self._interpolation_names: raise ValueError("interpolation must be one of %s" % ', '.join(self._interpolation_names)) self._interpolation = interpolation # check texture interpolation if self._interpolation == 'bilinear': texture_interpolation = 'linear' else: texture_interpolation = 'nearest' self._method = method self._grid = grid self._need_texture_upload = True self._need_vertex_update = True self._need_colortransform_update = True self._need_interpolation_update = True self._texture = TextureAtlas2D(self.texture_shape, tile_shape=self.tile_shape, interpolation=texture_interpolation, format="LUMINANCE", internalformat="R32F", ) self._subdiv_position = VertexBuffer() self._subdiv_texcoord = VertexBuffer() # impostor quad covers entire viewport vertices = np.array([[-1, -1], [1, -1], [1, 1], [-1, -1], [1, 1], [-1, 1]], dtype=np.float32) self._impostor_coords = VertexBuffer(vertices) self._null_tr = NullTransform() self._init_view(self) super(ImageVisual, self).__init__(vcode=VERT_SHADER, fcode=FRAG_SHADER) self.set_gl_state('translucent', cull_face=False) self._draw_mode = 'triangles' # define _data_lookup_fn as None, will be setup in # self._build_interpolation() self._data_lookup_fn = None self.gamma = gamma self.clim = clim if clim != 'auto' else (np.nanmin(data), np.nanmax(data)) self._texture_LUT = None self.cmap = cmap self.overview_info = None self.init_overview(data) # self.transform = PROJ4Transform(projection, inverse=True) self.freeze()
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))