Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #4
0
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
Beispiel #5
0
    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))
Beispiel #6
0
    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)
Beispiel #7
0
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)
Beispiel #8
0
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,
        )
Beispiel #9
0
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,
    )
Beispiel #10
0
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
Beispiel #11
0
    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))
Beispiel #12
0
    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)
Beispiel #13
0
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])
Beispiel #14
0
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
Beispiel #15
0
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