def test_has_alpha(): """Check if rasters have alpha bands.""" with rasterio.open(S3_ALPHA_PATH) as src_dst: assert utils.has_alpha_band(src_dst) with rasterio.open(COG_DST) as src_dst: assert not utils.has_alpha_band(src_dst)
def export_raster_index(input, expression, output): with rasterio.open(input) as src: profile = src.profile profile.update( dtype=rasterio.float32, count=1, nodata=-9999 ) data = src.read().astype(np.float32) alpha_index = None if has_alpha_band(src): try: alpha_index = src.colorinterp.index(ColorInterp.alpha) except ValueError: pass bands_names = ["b{}".format(b) for b in tuple(set(re.findall(r"b(?P<bands>[0-9]{1,2})", expression)))] rgb = expression.split(",") arr = dict(zip(bands_names, data)) arr = np.array([np.nan_to_num(ne.evaluate(bloc.strip(), local_dict=arr)) for bloc in rgb]) # Set nodata values index_band = arr[0] if alpha_index is not None: index_band[data[alpha_index] == 0] = -9999 # Remove infinity values index_band[index_band>1e+30] = -9999 index_band[index_band<-1e+30] = -9999 with rasterio.open(output, 'w', **profile) as dst: dst.write(arr)
def test_has_mask(): """Should return True.""" with rasterio.open(S3_MASK_PATH) as src_dst: assert utils.has_mask_band(src_dst) assert not utils.has_alpha_band(src_dst) with rasterio.open(COG_DST) as src_dst: assert not utils.has_mask_band(src_dst)
def make_cogeo_legacy(src_path): """ Make src_path a Cloud Optimized GeoTIFF This implementation does not require GDAL >= 3.1 but sometimes (rarely) hangs for unknown reasons """ tmpfile = tempfile.mktemp('_cogeo.tif', dir=settings.MEDIA_TMP) swapfile = tempfile.mktemp('_cogeo_swap.tif', dir=settings.MEDIA_TMP) with rasterio.open(src_path) as dst: output_profile = dict(blockxsize=256, blockysize=256, driver='GTiff', tiled=True, compress=dst.profile.get('compress', 'deflate'), interleave='pixel') # Dataset Open option (see gdalwarp `-oo` option) config = dict( GDAL_NUM_THREADS="ALL_CPUS", GDAL_TIFF_INTERNAL_MASK=True, GDAL_TIFF_OVR_BLOCKSIZE="128", ) nodata = None if has_alpha_band(dst) and dst.meta['dtype'] == 'uint16': nodata = 0.0 # Hack to workaround https://github.com/cogeotiff/rio-cogeo/issues/112 cog_translate(dst, tmpfile, output_profile, nodata=nodata, config=config, in_memory=False, quiet=True, web_optimized=False) # web_optimized reduces the dimension of the raster, as well as reprojecting to EPSG:3857 # we want to keep resolution and projection at the tradeoff of slightly slower tile render speed if os.path.isfile(tmpfile): shutil.move(src_path, swapfile) # Move to swap location try: shutil.move(tmpfile, src_path) except IOError as e: logger.warning("Cannot move %s to %s: %s" % (tmpfile, src_path, str(e))) shutil.move(swapfile, src_path) # Attempt to restore raise e if os.path.isfile(swapfile): os.remove(swapfile) return True else: return False
def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", scale=1): """ Get a tile image """ task = self.get_and_check_task(request, pk) z = int(z) x = int(x) y = int(y) scale = int(scale) ext = "png" driver = "jpeg" if ext == "jpg" else ext indexes = None nodata = None formula = self.request.query_params.get('formula') bands = self.request.query_params.get('bands') rescale = self.request.query_params.get('rescale') color_map = self.request.query_params.get('color_map') hillshade = self.request.query_params.get('hillshade') if formula == '': formula = None if bands == '': bands = None if rescale == '': rescale = None if color_map == '': color_map = None if hillshade == '' or hillshade == '0': hillshade = None try: expr, _ = lookup_formula(formula, bands) except ValueError as e: raise exceptions.ValidationError(str(e)) if tile_type in ['dsm', 'dtm'] and rescale is None: rescale = "0,1000" if tile_type in ['dsm', 'dtm'] and color_map is None: color_map = "gray" if tile_type == 'orthophoto' and formula is not None: if color_map is None: color_map = "gray" if rescale is None: rescale = "-1,1" if nodata is not None: nodata = np.nan if nodata == "nan" else float(nodata) tilesize = scale * 256 url = get_raster_path(task, tile_type) if not os.path.isfile(url): raise exceptions.NotFound() with rasterio.open(url) as src: minzoom, maxzoom = get_zoom_safe(src) has_alpha = has_alpha_band(src) if z < minzoom - ZOOM_EXTRA_LEVELS or z > maxzoom + ZOOM_EXTRA_LEVELS: raise exceptions.NotFound() # Handle N-bands datasets for orthophotos (not plant health) if tile_type == 'orthophoto' and expr is None: ci = src.colorinterp # More than 4 bands? if len(ci) > 4: # Try to find RGBA band order if ColorInterp.red in ci and \ ColorInterp.green in ci and \ ColorInterp.blue in ci: indexes = ( ci.index(ColorInterp.red) + 1, ci.index(ColorInterp.green) + 1, ci.index(ColorInterp.blue) + 1, ) else: # Fallback to first three indexes = ( 1, 2, 3, ) elif has_alpha: indexes = non_alpha_indexes(src) resampling = "nearest" padding = 0 if tile_type in ["dsm", "dtm"]: resampling = "bilinear" padding = 16 try: if expr is not None: tile, mask = expression(url, x, y, z, expr=expr, tilesize=tilesize, nodata=nodata, tile_edge_padding=padding, resampling_method=resampling) else: tile, mask = main.tile(url, x, y, z, indexes=indexes, tilesize=tilesize, nodata=nodata, tile_edge_padding=padding, resampling_method=resampling) except TileOutsideBounds: raise exceptions.NotFound("Outside of bounds") if color_map: try: color_map = get_colormap(color_map, format="gdal") except FileNotFoundError: raise exceptions.ValidationError("Not a valid color_map value") intensity = None if hillshade is not None: try: hillshade = float(hillshade) if hillshade <= 0: hillshade = 1.0 except ValueError: raise exceptions.ValidationError("Invalid hillshade value") if tile.shape[0] != 1: raise exceptions.ValidationError( "Cannot compute hillshade of non-elevation raster (multiple bands found)" ) delta_scale = (maxzoom + ZOOM_EXTRA_LEVELS + 1 - z) * 4 dx = src.meta["transform"][0] * delta_scale dy = -src.meta["transform"][4] * delta_scale ls = LightSource(azdeg=315, altdeg=45) # Hillshading is not a local tile operation and # requires neighbor tiles to be rendered seamlessly elevation = get_elevation_tiles(tile[0], url, x, y, z, tilesize, nodata, resampling, padding) intensity = ls.hillshade(elevation, dx=dx, dy=dy, vert_exag=hillshade) intensity = intensity[tilesize:tilesize * 2, tilesize:tilesize * 2] rgb, rmask = rescale_tile(tile, mask, rescale=rescale) rgb = apply_colormap(rgb, color_map) if intensity is not None: # Quick check if rgb.shape[0] != 3: raise exceptions.ValidationError( "Cannot process tile: intensity image provided, but no RGB data was computed." ) intensity = intensity * 255.0 rgb = hsv_blend(rgb, intensity) options = img_profiles.get(driver, {}) return HttpResponse(array_to_image(rgb, rmask, img_format=driver, **options), content_type="image/{}".format(ext))
def get(self, request, pk=None, project_pk=None, tile_type=""): """ Get the metadata for this tasks's asset type """ task = self.get_and_check_task(request, pk) formula = self.request.query_params.get('formula') bands = self.request.query_params.get('bands') if formula == '': formula = None if bands == '': bands = None try: expr, hrange = lookup_formula(formula, bands) except ValueError as e: raise exceptions.ValidationError(str(e)) pmin, pmax = 2.0, 98.0 raster_path = get_raster_path(task, tile_type) if not os.path.isfile(raster_path): raise exceptions.NotFound() try: with rasterio.open(raster_path, "r") as src: band_count = src.meta['count'] if has_alpha_band(src): band_count -= 1 info = main.metadata(src, pmin=pmin, pmax=pmax, histogram_bins=255, histogram_range=hrange, expr=expr) except IndexError as e: # Caught when trying to get an invalid raster metadata raise exceptions.ValidationError( "Cannot retrieve raster metadata: %s" % str(e)) # Override min/max if hrange: for b in info['statistics']: info['statistics'][b]['min'] = hrange[0] info['statistics'][b]['max'] = hrange[1] cmap_labels = { "jet": "Jet", "terrain": "Terrain", "gist_earth": "Earth", "rdylgn": "RdYlGn", "rdylgn_r": "RdYlGn (Reverse)", "spectral": "Spectral", "spectral_r": "Spectral (Reverse)", "pastel1": "Pastel", } colormaps = [] algorithms = [] if tile_type in ['dsm', 'dtm']: colormaps = ['jet', 'terrain', 'gist_earth', 'pastel1'] elif formula and bands: colormaps = ['rdylgn', 'spectral', 'rdylgn_r', 'spectral_r'] algorithms = *get_algorithm_list(band_count), info['color_maps'] = [] info['algorithms'] = algorithms if colormaps: for cmap in colormaps: try: info['color_maps'].append({ 'key': cmap, 'color_map': get_colormap(cmap, format="gdal"), 'label': cmap_labels.get(cmap, cmap) }) except FileNotFoundError: raise exceptions.ValidationError( "Not a valid color_map value: %s" % cmap) del info['address'] info['name'] = task.name info['scheme'] = 'xyz' info['tiles'] = [ get_tile_url(task, tile_type, self.request.query_params) ] if info['maxzoom'] < info['minzoom']: info['maxzoom'] = info['minzoom'] info['maxzoom'] += ZOOM_EXTRA_LEVELS info['minzoom'] -= ZOOM_EXTRA_LEVELS return Response(info)
def assure_cogeo(src_path): """ Guarantee that the .tif passed as an argument is a Cloud Optimized GeoTIFF (cogeo) If the path is not a cogeo, it is destructively converted into a cogeo. If the file cannot be converted, the function does not change the file :param src_path: path to GeoTIFF (cogeo or not) :return: None """ if not os.path.isfile(src_path): logger.warning("Cannot validate cogeo: %s (file does not exist)" % src_path) return if valid_cogeo(src_path): return # Not a cogeo logger.info("Optimizing %s as Cloud Optimized GeoTIFF" % src_path) tmpfile = tempfile.mktemp('_cogeo.tif', dir=settings.MEDIA_TMP) swapfile = tempfile.mktemp('_cogeo_swap.tif', dir=settings.MEDIA_TMP) with rasterio.open(src_path) as dst: output_profile = dict(blockxsize=256, blockysize=256, driver='GTiff', tiled=True, compress=dst.profile.get('compress', 'deflate'), interleave='pixel') # Dataset Open option (see gdalwarp `-oo` option) config = dict( GDAL_NUM_THREADS="ALL_CPUS", GDAL_TIFF_INTERNAL_MASK=True, GDAL_TIFF_OVR_BLOCKSIZE="128", ) nodata = None if has_alpha_band(dst) and dst.meta['dtype'] == 'uint16': nodata = 0.0 # Hack to workaround https://github.com/cogeotiff/rio-cogeo/issues/112 cog_translate(dst, tmpfile, output_profile, nodata=nodata, config=config, in_memory=False, quiet=True, web_optimized=False) # web_optimized reduces the dimension of the raster, as well as reprojecting to EPSG:3857 # we want to keep resolution and projection at the tradeoff of slightly slower tile render speed if os.path.isfile(tmpfile): shutil.move(src_path, swapfile) # Move to swap location try: shutil.move(tmpfile, src_path) except IOError as e: logger.warning("Cannot move %s to %s: %s" % (tmpfile, src_path, str(e))) shutil.move(swapfile, src_path) # Attempt to restore raise e if os.path.isfile(swapfile): os.remove(swapfile)
def info(address: str) -> Dict: """ Return simple metadata about the file. Attributes ---------- address : str or PathLike object A dataset path or URL. Will be opened in "r" mode. Returns ------- out : dict. """ with rasterio.open(address) as src_dst: minzoom, maxzoom = get_zooms(src_dst) bounds = transform_bounds(src_dst.crs, constants.WGS84_CRS, *src_dst.bounds, densify_pts=21) center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom] def _get_descr(ix): """Return band description.""" name = src_dst.descriptions[ix - 1] if not name: name = "band{}".format(ix) return name band_descriptions = [(ix, _get_descr(ix)) for ix in src_dst.indexes] tags = [(ix, src_dst.tags(ix)) for ix in src_dst.indexes] other_meta = dict() if src_dst.scales[0] and src_dst.offsets[0]: other_meta.update(dict(scale=src_dst.scales[0])) other_meta.update(dict(offset=src_dst.offsets[0])) if has_alpha_band(src_dst): nodata_type = "Alpha" elif has_mask_band(src_dst): nodata_type = "Mask" elif src_dst.nodata is not None: nodata_type = "Nodata" else: nodata_type = "None" try: cmap = src_dst.colormap(1) other_meta.update(dict(colormap=cmap)) except ValueError: pass return dict( address=address, bounds=bounds, center=center, minzoom=minzoom, maxzoom=maxzoom, band_metadata=tags, band_descriptions=band_descriptions, dtype=src_dst.meta["dtype"], colorinterp=[ src_dst.colorinterp[ix - 1].name for ix in src_dst.indexes ], nodata_type=nodata_type, **other_meta, )
def metadata( src_dst: Union[DatasetReader, DatasetWriter, WarpedVRT], bounds: Optional[Tuple[float, float, float, float]] = None, indexes: Optional[Union[Sequence[int], int]] = None, max_size: int = 1024, bounds_crs: CRS = constants.WGS84_CRS, percentiles: Tuple[float, float] = (2.0, 98.0), hist_options: Dict = {}, **kwargs: Any, ) -> Dict: """ Retrieve metadata and statistics from an image. Attributes ---------- src_dst : rasterio.io.DatasetReader rasterio.io.DatasetReader object bounds : tuple, optional Bounding box coordinates from which to calculate image statistics. max_size : int `max_size` of the longest dimension, respecting bounds X/Y aspect ratio. indexes : list of ints or a single int, optional Band indexes. bounds_crs: CRS or str, optional Specify bounds coordinate reference system, default WGS84/EPSG4326. percentiles: tuple, optional Tuple of Min/Max percentiles to compute. Default is (2, 98). hist_options : dict, optional Options to forward to numpy.histogram function. kwargs : Any, optional Additional options to forward to part or preview Returns ------- dict """ if isinstance(indexes, int): indexes = (indexes, ) if indexes is None: indexes = non_alpha_indexes(src_dst) if indexes != src_dst.indexes: warnings.warn("Alpha band was removed from the output data array", AlphaBandWarning) if bounds: data, mask = part( src_dst, bounds, max_size=max_size, indexes=indexes, bounds_crs=bounds_crs, **kwargs, ) bounds = transform_bounds(bounds_crs, constants.WGS84_CRS, *bounds, densify_pts=21) else: data, mask = preview(src_dst, max_size=max_size, indexes=indexes, **kwargs) bounds = transform_bounds(src_dst.crs, constants.WGS84_CRS, *src_dst.bounds, densify_pts=21) data = numpy.ma.array(data) data.mask = mask == 0 statistics = { indexes[b]: raster_stats(data[b], percentiles=percentiles, **hist_options) for b in range(data.shape[0]) } def _get_descr(ix): """Return band description.""" name = src_dst.descriptions[ix - 1] if not name: name = "band{}".format(ix) return name band_descriptions = [(ix, _get_descr(ix)) for ix in indexes] tags = [(ix, src_dst.tags(ix)) for ix in indexes] other_meta = dict() if src_dst.scales[0] and src_dst.offsets[0]: other_meta.update(dict(scale=src_dst.scales[0])) other_meta.update(dict(offset=src_dst.offsets[0])) if has_alpha_band(src_dst): nodata_type = "Alpha" elif has_mask_band(src_dst): nodata_type = "Mask" elif src_dst.nodata is not None: nodata_type = "Nodata" else: nodata_type = "None" try: cmap = src_dst.colormap(1) other_meta.update(dict(colormap=cmap)) except ValueError: pass return dict( bounds=bounds, statistics=statistics, band_metadata=tags, band_descriptions=band_descriptions, dtype=src_dst.meta["dtype"], colorinterp=[src_dst.colorinterp[ix - 1].name for ix in indexes], nodata_type=nodata_type, **other_meta, )
def _read( src_dst: Union[DatasetReader, DatasetWriter, WarpedVRT], height: Optional[int] = None, width: Optional[int] = None, indexes: Optional[Union[Sequence[int], int]] = None, out_window: Optional[windows.Window] = None, # DEPRECATED window: Optional[windows.Window] = None, nodata: Optional[Union[float, int, str]] = None, resampling_method: Resampling = "nearest", force_binary_mask: bool = True, unscale: bool = False, vrt_options: Dict = {}, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """ Create WarpedVRT and read data and mask. Attributes ---------- src_dst: rasterio.io.DatasetReader rasterio.io.DatasetReader object height: int, optional Output height of the array. width: int, optional Output width of the array. indexes: list of ints or a single int, optional Band indexes out_window: rasterio.windows.Window, optional DEPRECATED Output window to read. window: rasterio.windows.Window, optional Window to read. nodata: int or float, optional resampling_method: str, optional Resampling algorithm. Default is "nearest". force_binary_mask: bool, optional If True, rio-tiler makes sure mask has only 0 or 255 values. Default is set to True. unscale: bool, optional If True, apply scale and offset to the data array. Default is set to False. vrt_options: dict, optional These will be passed to the rasterio.warp.WarpedVRT class. Returns ------- data : numpy ndarray mask: numpy array """ if isinstance(indexes, int): indexes = (indexes, ) # Deprecate out_window. if out_window is not None: warnings.warn("out_window will be removed in 2.0, use window", DeprecationWarning) if window is None: window = out_window vrt_params = dict(add_alpha=True, resampling=Resampling[resampling_method]) nodata = nodata if nodata is not None else src_dst.nodata if nodata is not None: vrt_params.update( dict(nodata=nodata, add_alpha=False, src_nodata=nodata)) if has_alpha_band(src_dst): vrt_params.update(dict(add_alpha=False)) if indexes is None: indexes = non_alpha_indexes(src_dst) if indexes != src_dst.indexes: warnings.warn("Alpha band was removed from the output data array", AlphaBandWarning) out_shape = (len(indexes), height, width) if height and width else None mask_out_shape = (height, width) if height and width else None resampling = Resampling[resampling_method] vrt_params.update(vrt_options) with WarpedVRT(src_dst, **vrt_params) as vrt: data = vrt.read( indexes=indexes, window=window, out_shape=out_shape, resampling=resampling, ) if ColorInterp.alpha in vrt.colorinterp: idx = vrt.colorinterp.index(ColorInterp.alpha) + 1 mask = vrt.read( indexes=idx, window=window, out_shape=mask_out_shape, resampling=resampling, out_dtype="uint8", ) else: mask = vrt.dataset_mask( window=window, out_shape=mask_out_shape, resampling=resampling, ) if force_binary_mask: mask = numpy.where(mask != 0, numpy.uint8(255), numpy.uint8(0)) if unscale: data = data.astype("float32", casting="unsafe") numpy.multiply(data, vrt.scales[0], out=data, casting="unsafe") numpy.add(data, vrt.offsets[0], out=data, casting="unsafe") return data, mask
def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", scale=1): """ Get a tile image """ task = self.get_and_check_task(request, pk) z = int(z) x = int(x) y = int(y) scale = int(scale) ext = "png" driver = "jpeg" if ext == "jpg" else ext indexes = None nodata = None rgb_tile = None formula = self.request.query_params.get('formula') bands = self.request.query_params.get('bands') rescale = self.request.query_params.get('rescale') color_map = self.request.query_params.get('color_map') hillshade = self.request.query_params.get('hillshade') boundaries_feature = self.request.query_params.get('boundaries') if boundaries_feature == '': boundaries_feature = None if boundaries_feature is not None: try: boundaries_feature = json.loads(boundaries_feature) except json.JSONDecodeError: raise exceptions.ValidationError( _("Invalid boundaries parameter")) if formula == '': formula = None if bands == '': bands = None if rescale == '': rescale = None if color_map == '': color_map = None if hillshade == '' or hillshade == '0': hillshade = None try: expr, _discard_ = lookup_formula(formula, bands) except ValueError as e: raise exceptions.ValidationError(str(e)) if tile_type in ['dsm', 'dtm'] and rescale is None: rescale = "0,1000" if tile_type == 'orthophoto' and rescale is None: rescale = "0,255" if tile_type in ['dsm', 'dtm'] and color_map is None: color_map = "gray" if tile_type == 'orthophoto' and formula is not None: if color_map is None: color_map = "gray" if rescale is None: rescale = "-1,1" if nodata is not None: nodata = np.nan if nodata == "nan" else float(nodata) tilesize = scale * 256 url = get_raster_path(task, tile_type) if not os.path.isfile(url): raise exceptions.NotFound() with COGReader(url) as src: if not src.tile_exists(z, x, y): raise exceptions.NotFound(_("Outside of bounds")) with COGReader(url) as src: minzoom, maxzoom = get_zoom_safe(src) has_alpha = has_alpha_band(src.dataset) if z < minzoom - ZOOM_EXTRA_LEVELS or z > maxzoom + ZOOM_EXTRA_LEVELS: raise exceptions.NotFound() if boundaries_feature is not None: try: boundaries_cutline = create_cutline( src.dataset, boundaries_feature, CRS.from_string('EPSG:4326')) except: raise exceptions.ValidationError(_("Invalid boundaries")) else: boundaries_cutline = None # Handle N-bands datasets for orthophotos (not plant health) if tile_type == 'orthophoto' and expr is None: ci = src.dataset.colorinterp # More than 4 bands? if len(ci) > 4: # Try to find RGBA band order if ColorInterp.red in ci and \ ColorInterp.green in ci and \ ColorInterp.blue in ci: indexes = ( ci.index(ColorInterp.red) + 1, ci.index(ColorInterp.green) + 1, ci.index(ColorInterp.blue) + 1, ) else: # Fallback to first three indexes = ( 1, 2, 3, ) elif has_alpha: indexes = non_alpha_indexes(src.dataset) # Workaround for https://github.com/OpenDroneMap/WebODM/issues/894 if nodata is None and tile_type == 'orthophoto': nodata = 0 resampling = "nearest" padding = 0 if tile_type in ["dsm", "dtm"]: resampling = "bilinear" padding = 16 try: with COGReader(url) as src: if expr is not None: if boundaries_cutline is not None: tile = src.tile( x, y, z, expression=expr, tilesize=tilesize, nodata=nodata, padding=padding, resampling_method=resampling, vrt_options={'cutline': boundaries_cutline}) else: tile = src.tile(x, y, z, expression=expr, tilesize=tilesize, nodata=nodata, padding=padding, resampling_method=resampling) else: if boundaries_cutline is not None: tile = src.tile( x, y, z, tilesize=tilesize, nodata=nodata, padding=padding, resampling_method=resampling, vrt_options={'cutline': boundaries_cutline}) else: tile = src.tile(x, y, z, indexes=indexes, tilesize=tilesize, nodata=nodata, padding=padding, resampling_method=resampling) except TileOutsideBounds: raise exceptions.NotFound(_("Outside of bounds")) if color_map: try: colormap.get(color_map) except InvalidColorMapName: raise exceptions.ValidationError( _("Not a valid color_map value")) intensity = None try: rescale_arr = list(map(float, rescale.split(","))) except ValueError: raise exceptions.ValidationError(_("Invalid rescale value")) options = img_profiles.get(driver, {}) if hillshade is not None: try: hillshade = float(hillshade) if hillshade <= 0: hillshade = 1.0 except ValueError: raise exceptions.ValidationError(_("Invalid hillshade value")) if tile.data.shape[0] != 1: raise exceptions.ValidationError( _("Cannot compute hillshade of non-elevation raster (multiple bands found)" )) delta_scale = (maxzoom + ZOOM_EXTRA_LEVELS + 1 - z) * 4 dx = src.dataset.meta["transform"][0] * delta_scale dy = -src.dataset.meta["transform"][4] * delta_scale ls = LightSource(azdeg=315, altdeg=45) # Hillshading is not a local tile operation and # requires neighbor tiles to be rendered seamlessly elevation = get_elevation_tiles(tile.data[0], url, x, y, z, tilesize, nodata, resampling, padding) intensity = ls.hillshade(elevation, dx=dx, dy=dy, vert_exag=hillshade) intensity = intensity[tilesize:tilesize * 2, tilesize:tilesize * 2] if intensity is not None: rgb = tile.post_process(in_range=(rescale_arr, )) if colormap: rgb, _discard_ = apply_cmap(rgb.data, colormap.get(color_map)) if rgb.data.shape[0] != 3: raise exceptions.ValidationError( _("Cannot process tile: intensity image provided, but no RGB data was computed." )) intensity = intensity * 255.0 rgb = hsv_blend(rgb, intensity) if rgb is not None: return HttpResponse(render(rgb, tile.mask, img_format=driver, **options), content_type="image/{}".format(ext)) if color_map is not None: return HttpResponse( tile.post_process(in_range=(rescale_arr, )).render( img_format=driver, colormap=colormap.get(color_map), **options), content_type="image/{}".format(ext)) return HttpResponse(tile.post_process(in_range=(rescale_arr, )).render( img_format=driver, **options), content_type="image/{}".format(ext))
def get(self, request, pk=None, project_pk=None, tile_type=""): """ Get the metadata for this tasks's asset type """ task = self.get_and_check_task(request, pk) formula = self.request.query_params.get('formula') bands = self.request.query_params.get('bands') defined_range = self.request.query_params.get('range') boundaries_feature = self.request.query_params.get('boundaries') if formula == '': formula = None if bands == '': bands = None if defined_range == '': defined_range = None if boundaries_feature == '': boundaries_feature = None if boundaries_feature is not None: boundaries_feature = json.loads(boundaries_feature) try: expr, hrange = lookup_formula(formula, bands) if defined_range is not None: new_range = tuple(map(float, defined_range.split(",")[:2])) #Validate rescaling range if hrange is not None and (new_range[0] < hrange[0] or new_range[1] > hrange[1]): pass else: hrange = new_range except ValueError as e: raise exceptions.ValidationError(str(e)) pmin, pmax = 2.0, 98.0 raster_path = get_raster_path(task, tile_type) if not os.path.isfile(raster_path): raise exceptions.NotFound() try: with COGReader(raster_path) as src: band_count = src.dataset.meta['count'] if boundaries_feature is not None: boundaries_cutline = create_cutline( src.dataset, boundaries_feature, CRS.from_string('EPSG:4326')) boundaries_bbox = featureBounds(boundaries_feature) else: boundaries_cutline = None boundaries_bbox = None if has_alpha_band(src.dataset): band_count -= 1 nodata = None # Workaround for https://github.com/OpenDroneMap/WebODM/issues/894 if tile_type == 'orthophoto': nodata = 0 histogram_options = {"bins": 255, "range": hrange} if expr is not None: if boundaries_cutline is not None: data, mask = src.preview( expression=expr, vrt_options={'cutline': boundaries_cutline}) else: data, mask = src.preview(expression=expr) data = numpy.ma.array(data) data.mask = mask == 0 stats = { str(b + 1): raster_stats(data[b], percentiles=(pmin, pmax), bins=255, range=hrange) for b in range(data.shape[0]) } stats = {b: ImageStatistics(**s) for b, s in stats.items()} metadata = RioMetadata(statistics=stats, **src.info().dict()) else: if (boundaries_cutline is not None) and (boundaries_bbox is not None): metadata = src.metadata( pmin=pmin, pmax=pmax, hist_options=histogram_options, nodata=nodata, bounds=boundaries_bbox, vrt_options={'cutline': boundaries_cutline}) else: metadata = src.metadata(pmin=pmin, pmax=pmax, hist_options=histogram_options, nodata=nodata) info = json.loads(metadata.json()) except IndexError as e: # Caught when trying to get an invalid raster metadata raise exceptions.ValidationError( "Cannot retrieve raster metadata: %s" % str(e)) # Override min/max if hrange: for b in info['statistics']: info['statistics'][b]['min'] = hrange[0] info['statistics'][b]['max'] = hrange[1] cmap_labels = { "viridis": "Viridis", "jet": "Jet", "terrain": "Terrain", "gist_earth": "Earth", "rdylgn": "RdYlGn", "rdylgn_r": "RdYlGn (Reverse)", "spectral": "Spectral", "spectral_r": "Spectral (Reverse)", "discrete_ndvi": "Contrast NDVI", "better_discrete_ndvi": "Custom NDVI Index", "rplumbo": "Rplumbo (Better NDVI)", "pastel1": "Pastel", } colormaps = [] algorithms = [] if tile_type in ['dsm', 'dtm']: colormaps = ['viridis', 'jet', 'terrain', 'gist_earth', 'pastel1'] elif formula and bands: colormaps = [ 'rdylgn', 'spectral', 'rdylgn_r', 'spectral_r', 'rplumbo', 'discrete_ndvi', 'better_discrete_ndvi' ] algorithms = *get_algorithm_list(band_count), info['color_maps'] = [] info['algorithms'] = algorithms if colormaps: for cmap in colormaps: try: info['color_maps'].append({ 'key': cmap, 'color_map': colormap.get(cmap).values(), 'label': cmap_labels.get(cmap, cmap) }) except FileNotFoundError: raise exceptions.ValidationError( "Not a valid color_map value: %s" % cmap) info['name'] = task.name info['scheme'] = 'xyz' info['tiles'] = [ get_tile_url(task, tile_type, self.request.query_params) ] if info['maxzoom'] < info['minzoom']: info['maxzoom'] = info['minzoom'] info['maxzoom'] += ZOOM_EXTRA_LEVELS info['minzoom'] -= ZOOM_EXTRA_LEVELS info['bounds'] = {'value': src.bounds, 'crs': src.dataset.crs} return Response(info)
def export_raster(input, output, **opts): epsg = opts.get('epsg') expression = opts.get('expression') export_format = opts.get('format') rescale = opts.get('rescale') color_map = opts.get('color_map') hillshade = opts.get('hillshade') asset_type = opts.get('asset_type') name = opts.get('name', 'raster') # KMZ specific dem = asset_type in ['dsm', 'dtm'] with COGReader(input) as ds_src: src = ds_src.dataset profile = src.meta.copy() # Output format driver = "GTiff" compress = None max_bands = 9999 with_alpha = True rgb = False indexes = src.indexes output_raster = output jpg_background = 255 # white # KMZ is special, we just export it as jpg with EPSG:4326 # and then call GDAL to tile/package it kmz = export_format == "kmz" if kmz: export_format = "jpg" epsg = 4326 path_base, _ = os.path.splitext(output) output_raster = path_base + ".jpg" jpg_background = 0 # black if export_format == "jpg": driver = "JPEG" profile.update(quality=90) band_count = 3 with_alpha = False rgb = True elif export_format == "png": driver = "PNG" band_count = 4 rgb = True elif export_format == "gtiff-rgb": compress = "JPEG" profile.update(jpeg_quality=90) band_count = 4 rgb = True else: compress = "DEFLATE" band_count = src.count if compress is not None: profile.update(compress=compress) profile.update(predictor=2 if compress == "DEFLATE" else 1) if rgb and rescale is None: # Compute min max nodata = None if asset_type == 'orthophoto': nodata = 0 md = ds_src.metadata(pmin=2.0, pmax=98.0, hist_options={"bins": 255}, nodata=nodata) rescale = [md['statistics']['1']['min'], md['statistics']['1']['max']] ci = src.colorinterp if rgb and expression is None: # More than 4 bands? if len(ci) > 4: # Try to find RGBA band order if ColorInterp.red in ci and \ ColorInterp.green in ci and \ ColorInterp.blue in ci and \ ColorInterp.alpha in ci: indexes = (ci.index(ColorInterp.red) + 1, ci.index(ColorInterp.green) + 1, ci.index(ColorInterp.blue) + 1, ci.index(ColorInterp.alpha) + 1) if ColorInterp.alpha in ci: mask = src.read(ci.index(ColorInterp.alpha) + 1) else: mask = src.dataset_mask() cmap = None if color_map: try: cmap = colormap.get(color_map) except InvalidColorMapName: logger.warning("Invalid colormap {}".format(color_map)) def process(arr, skip_rescale=False, skip_alpha=False, skip_type=False): if not skip_rescale and rescale is not None: arr = linear_rescale(arr, in_range=rescale) if not skip_alpha and not with_alpha: arr[mask==0] = jpg_background if not skip_type and rgb and arr.dtype != np.uint8: arr = arr.astype(np.uint8) return arr def update_rgb_colorinterp(dst): if with_alpha: dst.colorinterp = [ColorInterp.red, ColorInterp.green, ColorInterp.blue, ColorInterp.alpha] else: dst.colorinterp = [ColorInterp.red, ColorInterp.green, ColorInterp.blue] profile.update(driver=driver, count=band_count) if rgb: profile.update(dtype=rasterio.uint8) if dem and rgb and profile.get('nodata') is not None: profile.update(nodata=None) # Define write band function # Reprojection needed? if src.crs is not None and epsg is not None and src.crs.to_epsg() != epsg: dst_crs = "EPSG:{}".format(epsg) transform, width, height = calculate_default_transform( src.crs, dst_crs, src.width, src.height, *src.bounds) profile.update( crs=dst_crs, transform=transform, width=width, height=height ) def write_band(arr, dst, band_num): reproject(source=arr, destination=rasterio.band(dst, band_num), src_transform=src.transform, src_crs=src.crs, dst_transform=transform, dst_crs=dst_crs, resampling=Resampling.nearest) else: # No reprojection needed def write_band(arr, dst, band_num): dst.write(arr, band_num) if expression is not None: # Apply band math if rgb: profile.update(dtype=rasterio.uint8, count=band_count) else: profile.update(dtype=rasterio.float32, count=1, nodata=-9999) bands_names = ["b{}".format(b) for b in tuple(sorted(set(re.findall(r"b(?P<bands>[0-9]{1,2})", expression))))] rgb_expr = expression.split(",") indexes = tuple([int(b.replace("b", "")) for b in bands_names]) alpha_index = None if has_alpha_band(src): try: alpha_index = src.colorinterp.index(ColorInterp.alpha) + 1 indexes += (alpha_index, ) except ValueError: pass data = src.read(indexes=indexes, out_dtype=np.float32) arr = dict(zip(bands_names, data)) arr = np.array([np.nan_to_num(ne.evaluate(bloc.strip(), local_dict=arr)) for bloc in rgb_expr]) # Set nodata values index_band = arr[0] if alpha_index is not None: # -1 is the last band = alpha index_band[data[-1] == 0] = -9999 # Remove infinity values index_band[index_band>1e+30] = -9999 index_band[index_band<-1e+30] = -9999 # Make sure this is float32 arr = arr.astype(np.float32) with rasterio.open(output_raster, 'w', **profile) as dst: # Apply colormap? if rgb and cmap is not None: rgb_data, _ = apply_cmap(process(arr, skip_alpha=True), cmap) band_num = 1 for b in rgb_data: write_band(process(b, skip_rescale=True), dst, band_num) band_num += 1 if with_alpha: write_band(mask, dst, band_num) update_rgb_colorinterp(dst) else: # Raw write_band(process(arr)[0], dst, 1) elif dem: # Apply hillshading, colormaps to elevation with rasterio.open(output_raster, 'w', **profile) as dst: arr = src.read() intensity = None if hillshade is not None and hillshade > 0: delta_scale = (ZOOM_EXTRA_LEVELS + 1) * 4 dx = src.meta["transform"][0] * delta_scale dy = -src.meta["transform"][4] * delta_scale ls = LightSource(azdeg=315, altdeg=45) intensity = ls.hillshade(arr[0], dx=dx, dy=dy, vert_exag=hillshade) intensity = intensity * 255.0 # Apply colormap? if rgb and cmap is not None: rgb_data, _ = apply_cmap(process(arr, skip_alpha=True), cmap) if intensity is not None: rgb_data = hsv_blend(rgb_data, intensity) band_num = 1 for b in rgb_data: write_band(process(b, skip_rescale=True), dst, band_num) band_num += 1 if with_alpha: write_band(mask, dst, band_num) update_rgb_colorinterp(dst) else: # Raw write_band(process(arr)[0], dst, 1) else: # Copy bands as-is with rasterio.open(output_raster, 'w', **profile) as dst: band_num = 1 for idx in indexes: ci = src.colorinterp[idx - 1] arr = src.read(idx) if ci == ColorInterp.alpha: if with_alpha: write_band(arr, dst, band_num) band_num += 1 else: write_band(process(arr), dst, band_num) band_num += 1 new_ci = [src.colorinterp[idx - 1] for idx in indexes] if not with_alpha: new_ci = [ci for ci in new_ci if ci != ColorInterp.alpha] dst.colorinterp = new_ci if kmz: subprocess.check_output(["gdal_translate", "-of", "KMLSUPEROVERLAY", "-co", "Name={}".format(name), "-co", "FORMAT=JPEG", output_raster, output])
def get_area_stats( src, bounds, max_img_size=512, indexes=None, nodata=None, resampling_method="bilinear", bbox_crs="epsg:4326", histogram_bins=20, histogram_range=None, ): """ Read data and mask. Attributes ---------- srd_dst : rasterio.io.DatasetReader rasterio.io.DatasetReader object bounds : list bounds (left, bottom, right, top) tilesize : int Output image size indexes : list of ints or a single int, optional, (defaults: None) If `indexes` is a list, the result is a 3D array, but is a 2D array if it is a band index number. nodata: int or float, optional (defaults: None) resampling_method : str, optional (default: "bilinear") Resampling algorithm histogram_bins: int, optional Defines the number of equal-width histogram bins (default: 10). histogram_range: str, optional The lower and upper range of the bins. If not provided, range is simply the min and max of the array. Returns ------- out : array, int returns pixel value. """ if isinstance(indexes, int): indexes = [indexes] elif isinstance(indexes, tuple): indexes = list(indexes) with rasterio.open(src) as src_dst: bounds = transform_bounds(bbox_crs, src_dst.crs, *bounds, densify_pts=21) vrt_params = dict(add_alpha=True, resampling=Resampling[resampling_method]) indexes = indexes if indexes is not None else src_dst.indexes nodata = nodata if nodata is not None else src_dst.nodata def _get_descr(ix): """Return band description.""" name = src_dst.descriptions[ix - 1] if not name: name = "band{}".format(ix) return name band_descriptions = [(ix, _get_descr(ix)) for ix in indexes] vrt_transform, vrt_width, vrt_height = get_vrt_transform( src_dst, bounds, bounds_crs=src_dst.crs) vrt_params.update( dict(transform=vrt_transform, width=vrt_width, height=vrt_height)) width = round(vrt_width) if vrt_width < max_img_size else max_img_size height = round( vrt_height) if vrt_height < max_img_size else max_img_size out_shape = (len(indexes), width, height) if nodata is not None: vrt_params.update( dict(nodata=nodata, add_alpha=False, src_nodata=nodata)) if has_alpha_band(src_dst): vrt_params.update(dict(add_alpha=False)) with WarpedVRT(src_dst, **vrt_params) as vrt: arr = vrt.read(out_shape=out_shape, indexes=indexes, masked=True) if not arr.any(): return None, band_descriptions params = {} if histogram_bins: params.update(dict(bins=histogram_bins)) if histogram_range: params.update(dict(range=histogram_range)) stats = { indexes[b]: _stats(arr[b], **params) for b in range(arr.shape[0]) if vrt.colorinterp[b] != ColorInterp.alpha } return stats, band_descriptions
def read_cog_tile(src, bounds, tile_size, indexes=None, nodata=None, resampling_method="bilinear", tile_edge_padding=2): """ Read cloud-optimized geotiff tile. Notes ----- Modified from `rio-tiler <https://github.com/cogeotiff/rio-tiler>`_. License included below per terms of use. BSD 3-Clause License (c) 2017 Mapbox All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Arguments --------- src : rasterio.io.DatasetReader rasterio.io.DatasetReader object bounds : list Tile bounds (left, bottom, right, top) tile_size : list Output image size indexes : list of ints or a single int, optional, (defaults: None) If `indexes` is a list, the result is a 3D array, but is a 2D array if it is a band index number. nodata: int or float, optional (defaults: None) resampling_method : str, optional (default: "bilinear") Resampling algorithm tile_edge_padding : int, optional (default: 2) Padding to apply to each edge of the tile when retrieving data to assist in reducing resampling artefacts along edges. Returns ------- out : array, int returns pixel value. """ if isinstance(indexes, int): indexes = [indexes] elif isinstance(indexes, tuple): indexes = list(indexes) vrt_params = dict( add_alpha=True, crs='epsg:' + str(src.crs.to_epsg()), resampling=Resampling[resampling_method] ) vrt_transform, vrt_width, vrt_height = get_vrt_transform( src, bounds, bounds_crs='epsg:' + str(src.crs.to_epsg())) out_window = Window(col_off=0, row_off=0, width=vrt_width, height=vrt_height) if tile_edge_padding > 0 and not \ _requested_tile_aligned_with_internal_tile(src, bounds, tile_size): vrt_transform = vrt_transform * Affine.translation( -tile_edge_padding, -tile_edge_padding ) orig__vrt_height = vrt_height orig_vrt_width = vrt_width vrt_height = vrt_height + 2 * tile_edge_padding vrt_width = vrt_width + 2 * tile_edge_padding out_window = Window( col_off=tile_edge_padding, row_off=tile_edge_padding, width=orig_vrt_width, height=orig__vrt_height, ) vrt_params.update(dict(transform=vrt_transform, width=vrt_width, height=vrt_height)) indexes = indexes if indexes is not None else src.indexes out_shape = (len(indexes), tile_size[1], tile_size[0]) nodata = nodata if nodata is not None else src.nodata if nodata is not None: vrt_params.update(dict(nodata=nodata, add_alpha=False, src_nodata=nodata)) if has_alpha_band(src): vrt_params.update(dict(add_alpha=False)) with WarpedVRT(src, **vrt_params) as vrt: data = vrt.read( out_shape=out_shape, indexes=indexes, window=out_window, resampling=Resampling[resampling_method], ) mask = vrt.dataset_mask(out_shape=(tile_size[1], tile_size[0]), window=out_window) return data, mask, out_window, vrt_transform