def test_transform_geom_gdal22(): """Enabling `antimeridian_cutting` has no effect on GDAL 2.2.0 or newer where antimeridian cutting is always enabled. This could produce unexpected geometries, so an exception is raised. """ geom = {"type": "Point", "coordinates": [0, 0]} with pytest.raises(GDALVersionError): transform_geom("EPSG:4326", "EPSG:3857", geom, antimeridian_cutting=False)
def test_transform_geom(): geom = { 'type': 'Polygon', 'coordinates': ( ((798842.3090855901, 6569056.500655151), (756688.2826828464, 6412397.888771972), (755571.0617232556, 6408461.009397383), (677605.2284582685, 6425600.39266733), (677605.2284582683, 6425600.392667332), (670873.3791649605, 6427248.603432341), (664882.1106069803, 6407585.48425362), (663675.8662823177, 6403676.990080649), (485120.71963574126, 6449787.167760638), (485065.55660851026, 6449802.826920689), (485957.03982722526, 6452708.625101285), (487541.24541826674, 6457883.292107048), (531008.5797472061, 6605816.560367976), (530943.7197027118, 6605834.9333479265), (531888.5010308184, 6608940.750411527), (533299.5981959199, 6613962.642851984), (533403.6388841148, 6613933.172096095), (576345.6064638699, 6761983.708069147), (577649.6721159086, 6766698.137844516), (578600.3589008929, 6770143.99782289), (578679.4732294685, 6770121.638265098), (655836.640492081, 6749376.357102599), (659913.0791150068, 6764770.1314677475), (661105.8478791204, 6769515.168134831), (661929.4670843681, 6772800.8565198565), (661929.4670843673, 6772800.856519875), (661975.1582566603, 6772983.354777632), (662054.7979028501, 6772962.86384242), (841909.6014891531, 6731793.200435557), (840726.455490463, 6727039.8672589315), (798842.3090855901, 6569056.500655151)), ) } result = transform_geom('EPSG:3373', 'EPSG:4326', geom) assert result['type'] == 'Polygon' assert len(result['coordinates']) == 1 result = transform_geom( 'EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True) assert result['type'] == 'MultiPolygon' assert len(result['coordinates']) == 2 result = transform_geom( 'EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True, antimeridian_offset=0) assert result['type'] == 'MultiPolygon' assert len(result['coordinates']) == 2 result = transform_geom('EPSG:3373', 'EPSG:4326', geom, precision=1) assert int(result['coordinates'][0][0][0] * 10) == -1778
def test_transform_geom_gdal22(): """Enabling `antimeridian_cutting` has no effect on GDAL 2.2.0 or newer where antimeridian cutting is always enabled. This could produce unexpected geometries, so an exception is raised. """ geom = { 'type': 'Point', 'coordinates': [0, 0] } with pytest.raises(GDALBehaviorChangeException): transform_geom( 'EPSG:4326', 'EPSG:3857', geom, antimeridian_cutting=False)
def test_transform_geom_polygon_offset(polygon_3373): geom = polygon_3373 result = transform_geom( "EPSG:3373", "EPSG:4326", geom, antimeridian_cutting=True, antimeridian_offset=0 ) assert result["type"] == "MultiPolygon" assert len(result["coordinates"]) == 2
def test_transform_geom_linearring_precision(polygon_3373): ring = polygon_3373["coordinates"][0] geom = {"type": "LinearRing", "coordinates": ring} result = transform_geom( "EPSG:3373", "EPSG:4326", geom, precision=1, antimeridian_cutting=True ) assert all(round(x, 1) == x for x in flatten_coords(result["coordinates"]))
def test_transform_geom_linestring_precision_z(polygon_3373): ring = polygon_3373['coordinates'][0] x, y = zip(*ring) ring = list(zip(x, y, [0.0 for i in range(len(x))])) geom = {'type': 'LineString', 'coordinates': ring} result = transform_geom('EPSG:3373', 'EPSG:3373', geom, precision=1) assert int(result['coordinates'][0][0] * 10) == 7988423 assert int(result['coordinates'][0][2] * 10) == 0
def test_transform_geom_linestring_precision_z(polygon_3373): ring = polygon_3373["coordinates"][0] x, y = zip(*ring) ring = list(zip(x, y, [0.0 for i in range(len(x))])) geom = {"type": "LineString", "coordinates": ring} result = transform_geom("EPSG:3373", "EPSG:3373", geom, precision=1) assert int(result["coordinates"][0][0] * 10) == 7988423 assert int(result["coordinates"][0][2] * 10) == 0
def test_issue_1446(): """Confirm resolution of #1446""" g = transform_geom( CRS.from_epsg(4326), CRS.from_epsg(32610), {"type": "Point", "coordinates": (-122.51403808499907, 38.06106733107932)}, ) assert round(g["coordinates"][0], 1) == 542630.9 assert round(g["coordinates"][1], 1) == 4212702.1
def test_transform_geom_polygon_offset(polygon_3373): geom = polygon_3373 result = transform_geom( 'EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True, antimeridian_offset=0) assert result['type'] == 'MultiPolygon' assert len(result['coordinates']) == 2
def geojson_tile_burn(tile, features, srid, ts, burn_value=1): """Burn tile with GeoJSON features.""" crs = (CRS.from_epsg(srid), CRS.from_epsg(3857)) shapes = ((transform_geom(*crs, feature["geometry"]), burn_value) for feature in features) try: return rasterize(shapes, out_shape=ts, transform=from_bounds(*tile_bbox(tile, mercator=True), *ts)) except: return None
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 water_mask(case_study, crs, transform, width, height): """Water mask from OSM database (water bodies + seas & oceans).""" db = osm.OSMDatabase(DB_NAME, DB_USER, DB_PASS, DB_HOST) polygons = db.water(case_study.aoi_latlon) geoms = [feature['geometry'] for feature in polygons] geoms = [transform_geom(WGS84, crs, geom) for geom in geoms] if len(geoms) == 0: return np.zeros(shape=(height, width), dtype=np.bool) water = rasterize( shapes=geoms, out_shape=(height, width), transform=transform, dtype=np.uint8).astype(np.bool) db.connection.close() return water
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 coordinates 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 test_transform_geom_wrap(): geom = { 'type': 'Polygon', 'coordinates': (((798842.3090855901, 6569056.500655151), (756688.2826828464, 6412397.888771972), (755571.0617232556, 6408461.009397383), (677605.2284582685, 6425600.39266733), (677605.2284582683, 6425600.392667332), (670873.3791649605, 6427248.603432341), (664882.1106069803, 6407585.48425362), (663675.8662823177, 6403676.990080649), (485120.71963574126, 6449787.167760638), (485065.55660851026, 6449802.826920689), (485957.03982722526, 6452708.625101285), (487541.24541826674, 6457883.292107048), (531008.5797472061, 6605816.560367976), (530943.7197027118, 6605834.9333479265), (531888.5010308184, 6608940.750411527), (533299.5981959199, 6613962.642851984), (533403.6388841148, 6613933.172096095), (576345.6064638699, 6761983.708069147), (577649.6721159086, 6766698.137844516), (578600.3589008929, 6770143.99782289), (578679.4732294685, 6770121.638265098), (655836.640492081, 6749376.357102599), (659913.0791150068, 6764770.1314677475), (661105.8478791204, 6769515.168134831), (661929.4670843681, 6772800.8565198565), (661929.4670843673, 6772800.856519875), (661975.1582566603, 6772983.354777632), (662054.7979028501, 6772962.86384242), (841909.6014891531, 6731793.200435557), (840726.455490463, 6727039.8672589315), (798842.3090855901, 6569056.500655151)), ) } result = transform_geom('EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True) assert result['type'] == 'MultiPolygon' assert len(result['coordinates']) == 2
def _add_zones(self, source, name, uid, species, path, zone_field): with fiona.open(path, 'r') as shp: for feature in shp: try: zone_id = feature['properties'][zone_field] except KeyError: zone_id = feature['properties'][zone_field.lower()] if zone_id is None: continue zone_id = int(zone_id) if zone_id == 0: continue if hasattr(name, '__call__'): object_id = feature['properties'].get('OBJECTID') zone_name = name(zone_id, object_id) else: zone_name = name.format(zone_id=zone_id, species=SPECIES_NAMES.get(species)) geometry = transform_geom(shp.crs, {'init': 'EPSG:4326'}, feature['geometry']) if feature['geometry']['type'] == 'MultiPolygon': geometry['coordinates'] = itertools.chain(*geometry['coordinates']) polygon = Polygon(*[LinearRing(x) for x in geometry['coordinates']]) uid_suffix = 0 while True: zone_uid = uid.format(zone_id=zone_id) if uid_suffix > 0: zone_uid += '_{}'.format(uid_suffix) try: with transaction.atomic(): SeedZone.objects.create( source=source, name=zone_name, species=species, zone_id=zone_id, zone_uid=zone_uid, polygon=polygon ) break except IntegrityError: if uid_suffix > 10: raise uid_suffix += 1
def crop_to_box(image, gps_bbox): ''' This differs from crop to shape in that: 1 - the bbox coords are WGS84 lat lon (GPS) 2 - we want the result to be a 'square' image (not a diamond) (we're assuming the image is more-or-less aligned N/S) ''' # for some reason the gps_bbox cannot be a shapely object; it has to be a geojson dict crs_bbox = transform_geom('WGS84', image.crs, mapping(gps_bbox)) pixel_bbox = [image.index(*xy) for xy in crs_bbox['coordinates'][0]] xs, ys = zip(*pixel_bbox) # maybe we should do something smarter here to ensure we always have data? # like take the miniumum of the maximal values, etc. pixel_bbox = box(min(xs), min(ys), max(xs), max(ys)) crs_bbox = Polygon([image.xy(*xy) for xy in pixel_bbox.exterior.coords]) return crop_to_shape(image, crs_bbox)
def __enter__(self): """Support using with Context Managers.""" self.scene_params = s2_sceneid_parser(self.sceneid) prefix = self._prefix.format(**self.scene_params) self.tileInfo = json.loads( get_object(self._hostname, f"{prefix}/tileInfo.json", request_pays=True)) input_geom = self.tileInfo["tileDataGeometry"] input_crs = CRS.from_user_input( input_geom["crs"]["properties"]["name"]) self.datageom = transform_geom(input_crs, constants.WGS84_CRS, input_geom) self.bounds = featureBounds(self.datageom) return self
def grid(ovr_level: int = Query(...)): """return geojson.""" options = {"OVERVIEW_LEVEL": ovr_level - 1} if ovr_level else {} with rasterio.open(self.src_path, **options) as src_dst: feats = [] for _, window in list(src_dst.block_windows(1)): geom = bbox_to_feature(src_dst.window_bounds(window)) geom = transform_geom( src_dst.crs, WGS84_CRS, geom.geometry.dict(), ) feats.append( {"type": "Feature", "geometry": geom, "properties": {}} ) grids = {"type": "FeatureCollection", "features": feats} return grids
def rasterize_geojson(geojson, template_path, out_path, crs=None, save_geojson=False): """ Creates transition spatial multipliers from a GeoJSON dictionary or list of dictionaries. :param geojson: GeoJSON-formatted dictionary. :param template_path: Path to the template raster to constrain the shapes to. :param out_path: Path to the outputted raster with burned shapes into it. :param crs: CRS of the input geometry. Default is EPSG:4326. :param save_geojson: If True, save a copy of the rasterized GeoJSON next to the file. """ if crs is None: crs = {'init': 'EPSG:4326'} # Handle single geometries as well as lists if type(geojson) is dict: geojson = [geojson] if save_geojson: ext = '.{}'.format(out_path.split('.')[-1]) json_path = out_path.replace(ext, '.json') with open(json_path, 'w') as f: json.dump(geojson, f) with rasterio.open(template_path, 'r') as template: if not os.path.exists(os.path.dirname(out_path)): os.makedirs(os.path.dirname(out_path)) with rasterio.open(out_path, 'w', **template.meta.copy()) as dest: image = features.rasterize( ( (transform_geom(crs, template.crs, g), 255.0 ) # Todo, should value be 255 or 100? for g in [f['geometry'] for f in geojson]), out_shape=template.shape, transform=template.transform, dtype='float64') image[numpy.where( image == 0 )] = 1.0 # Where a polygon wasn't drawn, set multiplier to 1.0 dest.write(image, 1)
def get_zones(self): for file in self.config.files: src_filename = os.path.join(self.dir, file) with fiona.open(src_filename) as shp: reproject = True if shp.crs and "init" in shp.crs and shp.crs["init"].lower( ) == "epsg:4326": reproject = False else: warnings.warn( "{} is not in WGS84 coordinates, it will be reprojected on the fly, which may be slow!" .format(src_filename), UserWarning, ) for feature in Bar(f"Processing {self.name}", max=len(shp)).iter(shp): geometry = feature["geometry"] if reproject: geometry = transform_geom(shp.crs, {"init": "EPSG:4326"}, geometry) if feature["geometry"]["type"] == "MultiPolygon": polygon = MultiPolygon(*[ Polygon(*[LinearRing(x) for x in g]) for g in geometry["coordinates"] ]) else: polygon = Polygon( *[LinearRing(x) for x in geometry["coordinates"]]) info = self.config.get_zone_info(feature, file) if info is not None: yield polygon, info else: raise ValueError( "Zone info is not valid for input feature", feature["properties"])
def geojson_parse_geometry(zoom, srid, feature_map, geometry, buffer): if buffer: geometry = transform_geom(CRS.from_epsg(srid), CRS.from_epsg(3857), geometry) # be sure to be planar geometry = mapping(shape(geometry).buffer(buffer)) srid = 3857 if geometry["type"] == "Polygon": feature_map = geojson_parse_polygon(zoom, srid, feature_map, geometry) elif geometry["type"] == "MultiPolygon": for polygon in geometry["coordinates"]: feature_map = geojson_parse_polygon(zoom, srid, feature_map, { "type": "Polygon", "coordinates": polygon }) return feature_map
def handle(self, name, file, *args, **options): name = name[0] file = file[0] if Region.objects.filter(name__iexact=name).exists(): message = ( 'WARNING: This will replace an existing region with the same name: {}. Do you want to continue? [y/n]' ).format(name) if input(message).lower() not in {'y', 'yes'}: return temp_dir = None try: if file.endswith('.zip'): temp_dir = mkdtemp() with ZipFile(file) as zf: zf.extractall(temp_dir) try: file = glob.glob(os.path.join(temp_dir, '*.shp'))[0] except IndexError: raise ValueError('No shapefile in zip archive') polygons = [] with fiona.open(file, 'r') as shp: for feature in shp: geometry = transform_geom(shp.crs, {'init': 'EPSG:4326'}, feature['geometry']) polygons.append(Polygon(*[LinearRing(x) for x in geometry['coordinates']])) with transaction.atomic(): Region.objects.filter(name__iexact=name).delete() Region.objects.create(name=name, polygons=MultiPolygon(polygons)) finally: if temp_dir is not None: try: shutil.rmtree(temp_dir) except OSError: pass
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 test_issue_1446_b(): """Confirm that lines aren't thrown as reported in #1446""" src_crs = CRS.from_epsg(4326) dst_crs = CRS( { "proj": "sinu", "lon_0": 350.85607029556, "x_0": 0, "y_0": 0, "a": 3396190, "b": 3396190, "units": "m", "no_defs": True, } ) collection = json.load(open("tests/data/issue1446.geojson")) geoms = {f["properties"]["fid"]: f["geometry"] for f in collection["features"]} transformed_geoms = { k: transform_geom(src_crs, dst_crs, g) for k, g in geoms.items() } # Before the fix, this geometry was thrown eastward of 0.0. It should be between -350 and -250. assert all([-350 < x < -150 for x, y in transformed_geoms[183519]["coordinates"]])
def transform(shape, source_crs, destination_crs=None, src_affine=None, dst_affine=None): """Transforms shape from one CRS to another. Parameters ---------- shape : shapely.geometry.base.BaseGeometry Shape to transform. source_crs : dict or str Source CRS in the form of key/value pairs or proj4 string. destination_crs : dict or str, optional Destination CRS, EPSG:4326 if not given. src_affine: Affine, optional. input shape in relative to this affine dst_affine: Affine, optional. output shape in relative to this affine Returns ------- shapely.geometry.base.BaseGeometry Transformed shape. """ if destination_crs is None: destination_crs = WGS84_CRS if src_affine is not None: shape = ops.transform(lambda r, q: ~src_affine * (r, q), shape) if source_crs != destination_crs: shape = make_shape(transform_geom(source_crs, destination_crs, shape)) if dst_affine is not None: shape = ops.transform(lambda r, q: dst_affine * (r, q), shape) return shape
def test_issue_1446_b(): """Confirm that lines aren't thrown as reported in #1446""" src_crs = CRS({"init": "epsg:4326"}) dst_crs = CRS( { "proj": "sinu", "lon_0": 350.85607029556, "x_0": 0, "y_0": 0, "a": 3396190, "b": 3396190, "units": "m", "no_defs": True, } ) collection = json.load(open("tests/data/issue1446.geojson")) geoms = {f["properties"]["fid"]: f["geometry"] for f in collection["features"]} transformed_geoms = { k: transform_geom(src_crs, dst_crs, g) for k, g in geoms.items() } # Before the fix, this geometry was thrown eastward of 0.0. It should be between -350 and -250. assert all([-350 < x < -150 for x, y in transformed_geoms[183519]["coordinates"]])
def geojson(self, crs=_GEOJSON_EPSG_4326_STRING): """ Return this Tile's geometry as GeoJSON Parameters ---------- crs : rasterio.crs.CRS Coordinate reference system of output. Defaults to EPSG:4326 per GeoJSON standard (RFC7946). If ``None``, will return geometries in Tile's CRS Returns ------- dict This tile's geometry and crs represented as GeoJSON References ---------- .. [1] https://tools.ietf.org/html/rfc7946#page-12 """ gj = shapely.geometry.mapping(self.bbox) if crs is not None: from rasterio.warp import transform_geom crs_ = convert.to_crs(crs) if crs_ != self.crs: gj = transform_geom(self.crs, crs_, gj) else: logger.debug('Not reprojecting GeoJSON since output CRS ' 'is the same as the Tile CRS') return { 'type': 'Feature', 'properties': { 'horizontal': self.horizontal, 'vertical': self.vertical }, 'geometry': gj }
def reproject_features(features, src_crs, dst_crs): """Reproject a list of GeoJSON-like features. Parameters ---------- features : iterable of dict An iterable of GeoJSON-like features. src_crs : CRS Source CRS. dst_crs : CRS Target CRS. Returns ------- out_features : iterable of dict Iterable of reprojected GeoJSON-like features. """ out_features = [] for feature in features: out_feature = feature.copy() out_geom = transform_geom(src_crs, dst_crs, feature['geometry']) out_feature['geometry'] = out_geom out_features.append(out_feature) return out_features
def test_transform_geom_polygon_precision(polygon_3373): geom = polygon_3373 result = transform_geom( "EPSG:3373", "EPSG:4326", geom, precision=1, antimeridian_cutting=True ) assert all(round(x, 1) == x for x in flatten_coords(result["coordinates"]))
def test_transform_geom_linestring_precision_iso(polygon_3373): ring = polygon_3373["coordinates"][0] geom = {"type": "LineString", "coordinates": ring} result = transform_geom("EPSG:3373", "EPSG:3373", geom, precision=1) assert int(result["coordinates"][0][0] * 10) == 7988423
from tilezilla import geoutils, products, stores, tilespec # Get tilespec, find product, retrieve intersecting tiles weld_conus = tilespec.TILESPECS['WELD_CONUS'] product = products.ESPALandsat('../tests/data/LT50120312002300-SC20151009172149/') tile = list(weld_conus.bounds_to_tiles(product.bounding_box(weld_conus.crs)))[0] crs.to_string(weld_conus.crs) weld_conus.crs geoutils.bounds_to_polygon(tile.bounds) # TODO: geom_geojson looks a little off warp.transform_geom('EPSG:4326', 'EPSG:5070', json.dumps(tile.geom_geojson)) # Init the data store # TODO: switch on some storage format metadata configuration values store = stores.GeoTIFFStore('tests/data/stores/GeoTIFF', tile, product) # TODO: allow override driver metadata options (section "creation_options:") store.meta_options # TODO: user input / configuration to define desired variables to_store = _util.multiple_filter( [b.long_name for b in product.bands], ('.*surface reflectance.*', '.*brightness temperature.*', '^cfmask_band$',), True) to_store
def test_transform_geom_multipolygon(polygon_3373): geom = { 'type': 'MultiPolygon', 'coordinates': [polygon_3373['coordinates']]} result = transform_geom('EPSG:3373', 'EPSG:4326', geom, precision=1) assert int(result['coordinates'][0][0][0][0] * 10) == -1778
def test_transform_geom_polygon(polygon_3373): geom = polygon_3373 result = transform_geom('EPSG:3373', 'EPSG:4326', geom) assert result['type'] == 'Polygon' assert len(result['coordinates']) == 1
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 test_transform_geom_linestring_precision_iso(polygon_3373): ring = polygon_3373['coordinates'][0] geom = {'type': 'LineString', 'coordinates': ring} result = transform_geom('EPSG:3373', 'EPSG:3373', geom, precision=1) assert int(result['coordinates'][0][0] * 10) == 7988423
def test_transform_geom_multipolygon(polygon_3373): geom = {"type": "MultiPolygon", "coordinates": [polygon_3373["coordinates"]]} result = transform_geom("EPSG:3373", "EPSG:4326", geom, precision=1) assert all(round(x, 1) == x for x in flatten_coords(result["coordinates"]))
def _compute_image_stats_chunked( dataset: 'DatasetReader') -> Optional[Dict[str, Any]]: """Compute statistics for the given rasterio dataset by looping over chunks.""" from rasterio import features, warp, windows from shapely import geometry total_count = valid_data_count = 0 tdigest = TDigest() sstats = SummaryStats() convex_hull = geometry.Polygon() block_windows = [w for _, w in dataset.block_windows(1)] for w in block_windows: with warnings.catch_warnings(): warnings.filterwarnings('ignore', message='invalid value encountered.*') block_data = dataset.read(1, window=w, masked=True) total_count += int(block_data.size) valid_data = block_data.compressed() if valid_data.size == 0: continue valid_data_count += int(valid_data.size) if np.any(block_data.mask): hull_candidates = RasterDriver._hull_candidate_mask( ~block_data.mask) hull_shapes = [ geometry.shape(s) for s, _ in features.shapes( np.ones(hull_candidates.shape, 'uint8'), mask=hull_candidates, transform=windows.transform(w, dataset.transform)) ] else: w, s, e, n = windows.bounds(w, dataset.transform) hull_shapes = [ geometry.Polygon([(w, s), (e, s), (e, n), (w, n)]) ] convex_hull = geometry.MultiPolygon([convex_hull, *hull_shapes]).convex_hull tdigest.update(valid_data) sstats.update(valid_data) if sstats.count() == 0: return None convex_hull_wgs = warp.transform_geom(dataset.crs, 'epsg:4326', geometry.mapping(convex_hull)) return { 'valid_percentage': valid_data_count / total_count * 100, 'range': (sstats.min(), sstats.max()), 'mean': sstats.mean(), 'stdev': sstats.std(), 'percentiles': tdigest.quantile(np.arange(0.01, 1, 0.01)), 'convex_hull': convex_hull_wgs }
def mask( input, output, variable, like, netcdf3, all_touched, invert, zip): """ Create a NetCDF mask from a shapefile. Values are equivalent to a numpy mask: 0 for unmasked areas, and 1 for masked areas. Template NetCDF dataset must have a valid projection defined or be inferred from dimensions (e.g., lat / long) """ with Dataset(like) as template_ds: template_varname = data_variables(template_ds).keys()[0] template_variable = template_ds.variables[template_varname] template_crs = get_crs(template_ds, template_varname) if template_crs: template_crs = CRS.from_string(template_crs) elif is_geographic(template_ds, template_varname): template_crs = CRS({'init': 'EPSG:4326'}) else: raise click.UsageError('template dataset must have a valid projection defined') spatial_dimensions = template_variable.dimensions[-2:] mask_shape = template_variable.shape[-2:] template_y_name, template_x_name = spatial_dimensions coords = SpatialCoordinateVariables.from_dataset( template_ds, x_name=template_x_name, y_name=template_y_name, projection=Proj(**template_crs.to_dict()) ) with fiona.open(input, 'r') as shp: transform_required = CRS(shp.crs) != template_crs # Project bbox for filtering bbox = coords.bbox if transform_required: bbox = bbox.project(Proj(**shp.crs), edge_points=21) geometries = [] for f in shp.filter(bbox=bbox.as_list()): geom = f['geometry'] if transform_required: geom = transform_geom(shp.crs, template_crs, geom) geometries.append(geom) click.echo('Converting {0} features to mask'.format(len(geometries))) if invert: fill_value = 0 default_value = 1 else: fill_value = 1 default_value = 0 with rasterio.Env(): # Rasterize features to 0, leaving background as 1 mask = rasterize( geometries, out_shape=mask_shape, transform=coords.affine, all_touched=all_touched, fill=fill_value, default_value=default_value, dtype=numpy.uint8 ) format = 'NETCDF3_CLASSIC' if netcdf3 else 'NETCDF4' dtype = 'int8' if netcdf3 else 'uint8' with Dataset(output, 'w', format=format) as out: coords.add_to_dataset(out, template_x_name, template_y_name) out_var = out.createVariable(variable, dtype, dimensions=spatial_dimensions, zlib=zip, fill_value=get_fill_value(dtype)) out_var[:] = mask
def test_transform_geom_polygon_precision(polygon_3373): geom = polygon_3373 result = transform_geom('EPSG:3373', 'EPSG:4326', geom, precision=1) assert int(result['coordinates'][0][0][0] * 10) == -1778
def test_transform_geom_polygon_cutting(polygon_3373): geom = polygon_3373 result = transform_geom( 'EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True) assert result['type'] == 'MultiPolygon' assert len(result['coordinates']) == 2
def test_transform_geom_array(polygon_3373): geom = [polygon_3373 for _ in range(10)] result = transform_geom("EPSG:3373", "EPSG:4326", geom, precision=1) assert isinstance(result, list) assert len(result) == 10
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 zones( input, output, variable, attribute, like, netcdf3, zip): """ Create zones in a NetCDF from features in a shapefile. This is intended to be used as input to zonal statistics functions; it is not intended as a direct replacement for rasterizing geometries into NetCDF. Only handles < 65,535 features for now. If --attribute is provided, any features that do not have this will not be assigned to zones. A values lookup will be used to store values. The zones are indices of the unique values encountered when extracting features. The original values are stored in an additional variable with the name of the zones variable plus '_values'. Template NetCDF dataset must have a valid projection defined or be inferred from dimensions (e.g., lat / long). """ with Dataset(like) as template_ds: template_varname = list(data_variables(template_ds).keys())[0] template_variable = template_ds.variables[template_varname] template_crs = get_crs(template_ds, template_varname) if template_crs: template_crs = CRS.from_string(template_crs) elif is_geographic(template_ds, template_varname): template_crs = CRS({'init': 'EPSG:4326'}) else: raise click.UsageError('template dataset must have a valid projection defined') spatial_dimensions = template_variable.dimensions[-2:] out_shape = template_variable.shape[-2:] template_y_name, template_x_name = spatial_dimensions coords = SpatialCoordinateVariables.from_dataset( template_ds, x_name=template_x_name, y_name=template_y_name, projection=Proj(**template_crs.to_dict()) ) with fiona.open(input, 'r') as shp: if attribute: if not attribute in shp.meta['schema']['properties']: raise click.BadParameter('{0} not found in dataset'.format(attribute), param='--attribute', param_hint='--attribute') att_dtype = shp.meta['schema']['properties'][attribute].split(':')[0] if not att_dtype in ('int', 'str'): raise click.BadParameter('integer or string attribute required'.format(attribute), param='--attribute', param_hint='--attribute') transform_required = CRS(shp.crs) != template_crs geometries = [] values = set() values_lookup = {} # Project bbox for filtering bbox = coords.bbox if transform_required: bbox = bbox.project(Proj(**shp.crs), edge_points=21) index = 0 for f in shp.filter(bbox=bbox.as_list()): value = f['properties'].get(attribute) if attribute else int(f['id']) if value is not None: geom = f['geometry'] if transform_required: geom = transform_geom(shp.crs, template_crs, geom) geometries.append((geom, index)) if not value in values: values.add(value) values_lookup[index] = value index += 1 # Otherwise, these will not be rasterized num_geometries = len(geometries) # Save a slot at the end for nodata if num_geometries < 255: dtype = numpy.dtype('uint8') elif num_geometries < 65535: dtype = numpy.dtype('uint16') else: raise click.UsageError('Too many features to rasterize: {0}, Exceptioning...'.format(num_geometries)) fill_value = get_fill_value(dtype) click.echo('Rasterizing {0} features into zones'.format(num_geometries)) with rasterio.Env(): zones = rasterize( geometries, out_shape=out_shape, transform=coords.affine, all_touched=False, # True produces undesirable results for adjacent polygons fill=fill_value, dtype=dtype ) format = 'NETCDF4' out_dtype = dtype if netcdf3: format = 'NETCDF3_CLASSIC' if dtype == numpy.uint8: out_dtype = numpy.dtype('int16') elif dtype == numpy.uint16: out_dtype = numpy.dtype('int32') # Have to convert fill_value to mask since we changed data type zones = numpy.ma.masked_array(zones, mask=(zones == fill_value)) with Dataset(output, 'w', format=format) as out: values_varname = '{0}_values'.format(variable) coords.add_to_dataset(out, template_x_name, template_y_name) out_var = out.createVariable(variable, out_dtype, dimensions=spatial_dimensions, zlib=zip, fill_value=get_fill_value(out_dtype)) out_var.setncattr('values', values_varname) out_var[:] = zones out_values = numpy.array([values_lookup[k] for k in range(0, len(values_lookup))]) if netcdf3 and out_values.dtype == numpy.int64: out_values = out_values.astype('int32') out.createDimension(values_varname, len(out_values)) values_var = out.createVariable(values_varname, out_values.dtype, dimensions=(values_varname, ), zlib=zip) values_var[:] = out_values
easting_list = [] northing_list = [] for i in highest_east: easting_list.append(i) for i in highest_north: northing_list.append(i) buffer_coordinates = generate_coordinates( easting_list, northing_list) # Warp the coordinates warped_elevation_coordinates = warp.transform_geom( {'init': 'EPSG:27700'}, elevation.crs, {"type": "Polygon", "coordinates": [buffer_coordinates]}) # create an 3d array containing the elevation data masked to the buffer zone elevation_mask, out_transform = mask.mask(elevation, [warped_elevation_coordinates], crop=False) # Search for the highest point in the buffer zone highest_point = np.amax(elevation_mask) # Extract the index of the highest point in pixel coordinates z, highest_east, highest_north = np.where(highest_point == elevation_mask) # Isolate the first value from the list
def test_transform_geom_multipolygon(polygon_3373): geom = { 'type': 'MultiPolygon', 'coordinates': [polygon_3373['coordinates']]} result = transform_geom('EPSG:3373', 'EPSG:4326', geom, precision=1) assert all(round(x, 1) == x for x in flatten_coords(result['coordinates']))
def test_transform_geom_dst_crs_none(): with pytest.raises(CRSError): transform_geom(WGS84_crs, None, None)
def test_transform_geom_linestring_precision(polygon_3373): ring = polygon_3373['coordinates'][0] geom = {'type': 'LineString', 'coordinates': ring} result = transform_geom('EPSG:3373', 'EPSG:4326', geom, precision=1, antimeridian_cutting=True) assert all(round(x, 1) == x for x in flatten_coords(result['coordinates']))
def test_transform_geom_polygon_cutting(polygon_3373): geom = polygon_3373 result = transform_geom("EPSG:3373", "EPSG:4326", geom, antimeridian_cutting=True) assert result["type"] == "MultiPolygon" assert len(result["coordinates"]) == 2
def transform(feat): dst_crs = 'epsg:4326' if projection == 'geographic' else crs geom = transform_geom(crs, dst_crs, feat['geometry'], precision=precision) feat['geometry'] = geom return feat