def create_cutline( src_dst: Union[DatasetReader, DatasetWriter, WarpedVRT], geometry: Dict, geometry_crs: CRS = None, ) -> str: """ Create WKT Polygon Cutline for GDALWarpOptions. Ref: https://gdal.org/api/gdalwarp_cpp.html?highlight=vrt#_CPPv415GDALWarpOptions Args: src_dst (rasterio.io.DatasetReader or rasterio.io.DatasetWriter or rasterio.vrt.WarpedVRT): Rasterio dataset. geometry (dict): GeoJSON feature or GeoJSON geometry. By default the cordinates are considered to be in the dataset CRS. Use `geometry_crs` to set a specific CRS. geometry_crs (rasterio.crs.CRS, optional): Input geometry Coordinate Reference System Returns: str: WKT geometry in form of `POLYGON ((x y, x y, ...))) """ if "geometry" in geometry: geometry = geometry["geometry"] if not is_valid_geom(geometry): raise RioTilerError("Invalid geometry") geom_type = geometry["type"] if geom_type not in ["Polygon", "MultiPolygon"]: raise RioTilerError( "Invalid geometry type: {geom_type}. Should be Polygon or MultiPolygon" ) if geometry_crs: geometry = transform_geom(geometry_crs, src_dst.crs, geometry) polys = [] geom = ( [geometry["coordinates"]] if geom_type == "Polygon" else geometry["coordinates"] ) for p in geom: xs, ys = zip(*coords(p)) src_y, src_x = rowcol(src_dst.transform, xs, ys) src_x = [max(0, min(src_dst.width, x)) for x in src_x] src_y = [max(0, min(src_dst.height, y)) for y in src_y] poly = ", ".join([f"{x} {y}" for x, y in list(zip(src_x, src_y))]) polys.append(f"(({poly}))") str_poly = ",".join(polys) return ( f"POLYGON {str_poly}" if geom_type == "Polygon" else f"MULTIPOLYGON ({str_poly})" )
def __call__(self): with rasterio.open(in_fp) as src: if bidx > src.count: raise ValueError('bidx is out of range for raster') # Adjust transforms. transform = src.transform # Most of the time, we'll use the valid data mask. # We skip reading it if we're extracting every possible # feature (even invalid data features) from a band. msk = src.read_masks(bidx) img = src.read(bidx, masked=False) # Transform the raster bounds. bounds = src.bounds xs = [bounds[0], bounds[2]] ys = [bounds[1], bounds[3]] xs, ys = rasterio.warp.transform(src.crs, CRS({'init': 'epsg:4326'}), xs, ys) self._xs = xs self._ys = ys # Prepare keyword arguments for shapes(). kwargs = {'transform': transform} kwargs['mask'] = msk src_basename = os.path.basename(src.name) # Yield GeoJSON features. for i, (g, val) in enumerate( rasterio.features.shapes(img, **kwargs)): g = rasterio.warp.transform_geom(src.crs, 'EPSG:4326', g, antimeridian_cutting=True, precision=-1) xs, ys = zip(*coords(g)) yield { 'type': 'Feature', 'id': str(i), 'properties': { 'val': val, 'filename': src_basename, 'id': i }, 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g }
def reproject(shapes, bounds): with rasterio.Env(OGR_ENABLE_PARTIAL_REPROJECTION=True): for g, val in shapes: # TODO this produces garbage on the left edge of the world g = warp.transform_geom(bounds.crs, WGS84_CRS, g) xs, ys = zip(*coords(g)) yield { "type": "Feature", "properties": { "value": val }, "bbox": [min(xs), min(ys), max(xs), max(ys)], "geometry": g, }
def create_cutline(src_dst: DataSet, geometry: Dict, geometry_crs: CRS = None) -> str: """ Create WKT Polygon Cutline for GDALWarpOptions. Ref: https://gdal.org/api/gdalwarp_cpp.html?highlight=vrt#_CPPv415GDALWarpOptions Attributes ---------- src_dst: rasterio.io.DatasetReader rasterio.io.DatasetReader object geometry: dict GeoJSON feature or GeoJSON geometry geometry_crs: CRS or str, optional Specify bounds coordinate reference system, default is same as input dataset. Returns ------- wkt: str Cutline WKT geometry in form of `POLYGON ((x y, x y, ...))) """ if "geometry" in geometry: geometry = geometry["geometry"] geom_type = geometry["type"] if not geom_type == "Polygon": raise RioTilerError( "Invalid geometry type: {geom_type}. Should be Polygon") if geometry_crs: geometry = transform_geom(geometry_crs, src_dst.crs, geometry) xs, ys = zip(*coords(geometry)) src_y, src_x = rowcol(src_dst.transform, xs, ys) src_x = [max(0, min(src_dst.width, x)) for x in src_x] src_y = [max(0, min(src_dst.height, y)) for y in src_y] poly = ", ".join([f"{x} {y}" for x, y in list(zip(src_x, src_y))]) return f"POLYGON (({poly}))"
def dataset_features(src, bidx=None, sampling=1, band=True, as_mask=False, with_nodata=False, geographic=True, precision=-1): """Yield GeoJSON features for the dataset The geometries are polygons bounding contiguous regions of the same raster value. Parameters ---------- src: Rasterio Dataset bidx: int band index sampling: int (DEFAULT: 1) Inverse of the sampling fraction; a value of 10 decimates band: boolean (DEFAULT: True) extract features from a band (True) or a mask (False) as_mask: boolean (DEFAULT: False) Interpret band as a mask and output only one class of valid data shapes? with_nodata: boolean (DEFAULT: False) Include nodata regions? geographic: str (DEFAULT: True) Output shapes in EPSG:4326? Otherwise use the native CRS. precision: int (DEFAULT: -1) Decimal precision of coordinates. -1 for full float precision output Yields ------ GeoJSON-like Feature dictionaries for shapes found in the given band """ if bidx is not None and bidx > src.count: raise ValueError('bidx is out of range for raster') img = None msk = None # Adjust transforms. transform = src.transform if sampling > 1: # Determine the target shape (to decimate) shape = (int(math.ceil(src.height / sampling)), int(math.ceil(src.width / sampling))) # Calculate independent sampling factors x_sampling = src.width / shape[1] y_sampling = src.height / shape[0] # Decimation of the raster produces a georeferencing # shift that we correct with a translation. transform *= Affine.translation(src.width % x_sampling, src.height % y_sampling) # And follow by scaling. transform *= Affine.scale(x_sampling, y_sampling) # Most of the time, we'll use the valid data mask. # We skip reading it if we're extracting every possible # feature (even invalid data features) from a band. if not band or (band and not as_mask and not with_nodata): if sampling == 1: msk = src.read_masks(bidx) else: msk_shape = shape if bidx is None: msk = np.zeros((src.count, ) + msk_shape, 'uint8') else: msk = np.zeros(msk_shape, 'uint8') msk = src.read_masks(bidx, msk) if bidx is None: msk = np.logical_or.reduce(msk).astype('uint8') # Possibly overridden below. img = msk # Read the band data unless the --mask option is given. if band: if sampling == 1: img = src.read(bidx, masked=False) else: img = np.zeros(shape, dtype=src.dtypes[src.indexes.index(bidx)]) img = src.read(bidx, img, masked=False) # If as_mask option was given, convert the image # to a binary image. This reduces the number of shape # categories to 2 and likely reduces the number of # shapes. if as_mask: tmp = np.ones_like(img, 'uint8') * 255 tmp[img == 0] = 0 img = tmp if not with_nodata: msk = tmp # Prepare keyword arguments for shapes(). kwargs = {'transform': transform} if not with_nodata: kwargs['mask'] = msk src_basename = os.path.basename(src.name) # Yield GeoJSON features. for i, (g, val) in enumerate(rasterio.features.shapes(img, **kwargs)): if geographic: g = warp.transform_geom(src.crs, 'EPSG:4326', g, antimeridian_cutting=True, precision=precision) xs, ys = zip(*coords(g)) yield { 'type': 'Feature', 'id': "{0}:{1}".format(src_basename, i), 'properties': { 'val': val, 'filename': src_basename }, 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g }
def dataset_features( src, bidx=None, sampling=1, band=True, as_mask=False, with_nodata=False, geographic=True, precision=-1): """Yield GeoJSON features for the dataset The geometries are polygons bounding contiguous regions of the same raster value. Parameters ---------- src: Rasterio Dataset bidx: int band index sampling: int (DEFAULT: 1) Inverse of the sampling fraction; a value of 10 decimates band: boolean (DEFAULT: True) extract features from a band (True) or a mask (False) as_mask: boolean (DEFAULT: False) Interpret band as a mask and output only one class of valid data shapes? with_nodata: boolean (DEFAULT: False) Include nodata regions? geographic: str (DEFAULT: True) Output shapes in EPSG:4326? Otherwise use the native CRS. precision: int (DEFAULT: -1) Decimal precision of coordinates. -1 for full float precision output Yields ------ GeoJSON-like Feature dictionaries for shapes found in the given band """ if bidx is not None and bidx > src.count: raise ValueError('bidx is out of range for raster') img = None msk = None # Adjust transforms. transform = src.transform if sampling > 1: # Determine the target shape (to decimate) shape = (int(math.ceil(src.height / sampling)), int(math.ceil(src.width / sampling))) # Calculate independent sampling factors x_sampling = src.width / shape[1] y_sampling = src.height / shape[0] # Decimation of the raster produces a georeferencing # shift that we correct with a translation. transform *= Affine.translation( src.width % x_sampling, src.height % y_sampling) # And follow by scaling. transform *= Affine.scale(x_sampling, y_sampling) # Most of the time, we'll use the valid data mask. # We skip reading it if we're extracting every possible # feature (even invalid data features) from a band. if not band or (band and not as_mask and not with_nodata): if sampling == 1: msk = src.read_masks(bidx) else: msk_shape = shape if bidx is None: msk = np.zeros( (src.count,) + msk_shape, 'uint8') else: msk = np.zeros(msk_shape, 'uint8') msk = src.read_masks(bidx, msk) if bidx is None: msk = np.logical_or.reduce(msk).astype('uint8') # Possibly overridden below. img = msk # Read the band data unless the --mask option is given. if band: if sampling == 1: img = src.read(bidx, masked=False) else: img = np.zeros( shape, dtype=src.dtypes[src.indexes.index(bidx)]) img = src.read(bidx, img, masked=False) # If as_mask option was given, convert the image # to a binary image. This reduces the number of shape # categories to 2 and likely reduces the number of # shapes. if as_mask: tmp = np.ones_like(img, 'uint8') * 255 tmp[img == 0] = 0 img = tmp if not with_nodata: msk = tmp # Prepare keyword arguments for shapes(). kwargs = {'transform': transform} if not with_nodata: kwargs['mask'] = msk src_basename = os.path.basename(src.name) # Yield GeoJSON features. for i, (g, val) in enumerate( rasterio.features.shapes(img, **kwargs)): if geographic: g = warp.transform_geom( src.crs, 'EPSG:4326', g, antimeridian_cutting=True, precision=precision) xs, ys = zip(*coords(g)) yield { 'type': 'Feature', 'id': "{0}:{1}".format(src_basename, i), 'properties': { 'val': val, 'filename': src_basename }, 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g }
def tiles(ctx, zoom, input, identifier, seq): """ Lists TMS tiles at ZOOM level intersecting GeoJSON [west, south, east, north] bounding boxen, features, or collections read from stdin. Output is a JSON [x, y, z] array. Input may be a compact newline-delimited sequences of JSON or a pretty-printed ASCII RS-delimited sequence of JSON (like https://tools.ietf.org/html/rfc8142 and https://tools.ietf.org/html/rfc7159). Example: $ echo "[-105.05, 39.95, -105, 40]" | morecantiles tiles 12 Output: [852, 1550, 12] [852, 1551, 12] [853, 1550, 12] [853, 1551, 12] """ tms = morecantile.tms.get(identifier) for obj in normalize_source(input): if isinstance(obj, list): bbox = obj if len(bbox) == 2: bbox += bbox if len(bbox) != 4: raise click.BadParameter("{0}".format(bbox), param=input, param_hint="input") elif isinstance(obj, dict): if "bbox" in obj: bbox = obj["bbox"] else: box_xs = [] box_ys = [] for feat in obj.get("features", [obj]): lngs, lats = zip(*list(coords(feat))) box_xs.extend([min(lngs), max(lngs)]) box_ys.extend([min(lats), max(lats)]) bbox = min(box_xs), min(box_ys), max(box_xs), max(box_ys) west, south, east, north = bbox epsilon = 1.0e-10 if east != west and north != south: # 2D bbox # shrink the bounds a small amount so that # shapes/tiles round trip. west += epsilon south += epsilon east -= epsilon north -= epsilon for tile in tms.tiles(west, south, east, north, [zoom], truncate=False): vals = (tile.x, tile.y, zoom) output = json.dumps(vals) if seq: click.echo(u"\x1e") click.echo(output)
def __call__(self): with rasterio.open(input) as src: if bidx is not None and bidx > src.count: raise ValueError('bidx is out of range for raster') img = None msk = None # Adjust transforms. transform = src.transform if sampling > 1: # Determine the target shape (to decimate) shape = (int(math.ceil(src.height / sampling)), int(math.ceil(src.width / sampling))) # Calculate independent sampling factors x_sampling = src.width / shape[1] y_sampling = src.height / shape[0] # Decimation of the raster produces a georeferencing # shift that we correct with a translation. transform *= Affine.translation(src.width % x_sampling, src.height % y_sampling) # And follow by scaling. transform *= Affine.scale(x_sampling, y_sampling) # Most of the time, we'll use the valid data mask. # We skip reading it if we're extracting every possible # feature (even invalid data features) from a band. if not band or (band and not as_mask and not with_nodata): if sampling == 1: msk = src.read_masks(bidx) else: msk_shape = shape if bidx is None: msk = np.zeros((src.count, ) + msk_shape, 'uint8') else: msk = np.zeros(msk_shape, 'uint8') msk = src.read_masks(bidx, msk) if bidx is None: msk = np.logical_or.reduce(msk).astype('uint8') # Possibly overridden below. img = msk # Read the band data unless the --mask option is given. if band: if sampling == 1: img = src.read(bidx, masked=False) else: img = np.zeros( shape, dtype=src.dtypes[src.indexes.index(bidx)]) img = src.read(bidx, img, masked=False) # If --as-mask option was given, convert the image # to a binary image. This reduces the number of shape # categories to 2 and likely reduces the number of # shapes. if as_mask: tmp = np.ones_like(img, 'uint8') * 255 tmp[img == 0] = 0 img = tmp if not with_nodata: msk = tmp # Transform the raster bounds. bounds = src.bounds xs = [bounds[0], bounds[2]] ys = [bounds[1], bounds[3]] if projection == 'geographic': xs, ys = rasterio.warp.transform( src.crs, CRS({'init': 'epsg:4326'}), xs, ys) if precision >= 0: xs = [round(v, precision) for v in xs] ys = [round(v, precision) for v in ys] self._xs = xs self._ys = ys # Prepare keyword arguments for shapes(). kwargs = {'transform': transform} if not with_nodata: kwargs['mask'] = msk src_basename = os.path.basename(src.name) # Yield GeoJSON features. for i, (g, val) in enumerate( rasterio.features.shapes(img, **kwargs)): if projection == 'geographic': g = rasterio.warp.transform_geom( src.crs, 'EPSG:4326', g, antimeridian_cutting=True, precision=precision) xs, ys = zip(*coords(g)) yield { 'type': 'Feature', 'id': "{0}:{1}".format(src_basename, i), 'properties': { 'val': val, 'filename': src_basename }, 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g }
def __call__(self): with rasterio.open(input) as src: if bidx is not None and bidx > src.count: raise ValueError('bidx is out of range for raster') img = None msk = None # Adjust transforms. transform = src.transform if sampling > 1: # Decimation of the raster produces a georeferencing # shift that we correct with a translation. transform *= Affine.translation( src.width % sampling, src.height % sampling) # And follow by scaling. transform *= Affine.scale(float(sampling)) # Most of the time, we'll use the valid data mask. # We skip reading it if we're extracting every possible # feature (even invalid data features) from a band. if not band or (band and not as_mask and not with_nodata): if sampling == 1: msk = src.read_masks(bidx) else: msk_shape = ( src.height // sampling, src.width // sampling) if bidx is None: msk = np.zeros( (src.count,) + msk_shape, 'uint8') else: msk = np.zeros(msk_shape, 'uint8') msk = src.read_masks(bidx, msk) if bidx is None: msk = np.logical_or.reduce(msk).astype('uint8') # Possibly overridden below. img = msk # Read the band data unless the --mask option is given. if band: if sampling == 1: img = src.read(bidx, masked=False) else: img = np.zeros( (src.height // sampling, src.width // sampling), dtype=src.dtypes[src.indexes.index(bidx)]) img = src.read(bidx, img, masked=False) # If --as-mask option was given, convert the image # to a binary image. This reduces the number of shape # categories to 2 and likely reduces the number of # shapes. if as_mask: tmp = np.ones_like(img, 'uint8') * 255 tmp[img == 0] = 0 img = tmp if not with_nodata: msk = tmp # Transform the raster bounds. bounds = src.bounds xs = [bounds[0], bounds[2]] ys = [bounds[1], bounds[3]] if projection == 'geographic': xs, ys = rasterio.warp.transform( src.crs, CRS({'init': 'epsg:4326'}), xs, ys) if precision >= 0: xs = [round(v, precision) for v in xs] ys = [round(v, precision) for v in ys] self._xs = xs self._ys = ys # Prepare keyword arguments for shapes(). kwargs = {'transform': transform} if not with_nodata: kwargs['mask'] = msk src_basename = os.path.basename(src.name) # Yield GeoJSON features. for i, (g, val) in enumerate( rasterio.features.shapes(img, **kwargs)): if projection == 'geographic': g = rasterio.warp.transform_geom( src.crs, 'EPSG:4326', g, antimeridian_cutting=True, precision=precision) xs, ys = zip(*coords(g)) yield { 'type': 'Feature', 'id': "{0}:{1}".format(src_basename, i), 'properties': { 'val': val, 'filename': src_basename }, 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g }