def test_geographic_crop_with_resize(self): coords = mercantile.xy_bounds(*tiles[17]) raster = self.geographic_raster() vector = GeoVector(Polygon.from_bounds(*coords), crs=self.metric_crs).reproject(self.geographic_crs) cropped = raster.crop(vector, mercator_zoom_to_resolution[17]) x_ex_res, y_ex_res = convert_resolution_from_meters_to_deg( self.metric_affine[6], mercator_zoom_to_resolution[17]) self.assertAlmostEqual(cropped.affine[0], x_ex_res) self.assertAlmostEqual(abs(cropped.affine[4]), y_ex_res, 6)
def test_from_bounds(self): xmin, ymin, xmax, ymax = -180, -90, 180, 90 coords = [ (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] self.assertEqual( Polygon(coords), Polygon.from_bounds(xmin, ymin, xmax, ymax))
def test_featurecollection_schema_raises_error_for_heterogeneous_geometry_types(): fc = FeatureCollection.from_geovectors([ GeoVector(Polygon.from_bounds(0, 0, 1, 1)), GeoVector(Point(0, 0)) ]) with pytest.raises(FeatureCollectionIOError) as excinfo: fc.schema assert "Cannot generate a schema for a heterogeneous FeatureCollection. " in excinfo.exconly()
def test_crop_returns_full_resolution_as_default(self): coords = mercantile.xy_bounds(*tiles[17]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.metric_raster() _, win = raster._vector_to_raster_bounds(shape) cropped = raster.crop(shape) self.assertEqual( cropped.shape, (raster.num_bands, round(win.height), round(win.width))) self.assertEqual(cropped.affine[0], raster.affine[0])
def test_georaster_contains_geometry(): roi = GeoVector(Polygon.from_bounds(12.36, 42.05, 12.43, 42.10), WGS84_CRS).reproject(WEB_MERCATOR_CRS) resolution = 20.0 empty = GeoRaster2.empty_from_roi(roi, resolution) assert roi in empty assert roi.buffer(-1) in empty assert roi.buffer(1) not in empty
def make_mask_seg(image_file: str, label_file: str, field, width: int, height: int, drop_last: bool, outpath: str): if not Path(image_file).is_file(): raise ValueError('file {} not exits.'.format(image_file)) # TODO: Check the crs # read the file and distinguish the label_file is raster or vector try: label_src = rasterio.open(label_file) label_flag = 'raster' except rasterio.RasterioIOError: label_df = geopandas.read_file(label_file) # TODO: create spatial index to speed up the clip label_flag = 'vector' img_src = rasterio.open(image_file) rows = img_src.meta['height'] // height if drop_last else img_src.meta['height'] // height + 1 columns = img_src.meta['width'] // width if drop_last else img_src.meta['width'] // width + 1 for row in tqdm(range(rows)): for col in range(columns): # image outfile_image = os.path.join(outpath, Path(image_file).stem+'_'+str(row)+'_'+str(col)+Path(image_file).suffix) window = Window(col * width, row * height, width, height) patched_arr = img_src.read(window=window, boundless=True) kwargs = img_src.meta.copy() patched_transform = rasterio.windows.transform(window, img_src.transform) kwargs.update({ 'height': window.height, 'width': window.width, 'transform': patched_transform}) with rasterio.open(outfile_image, 'w', **kwargs) as dst: dst.write(patched_arr) # label outfile_label = Path(outfile_image).with_suffix('.png') if label_flag == 'raster': label_arr = label_src.read(window=window, boundless=True) else: bounds = rasterio.windows.bounds(window, img_src.transform) clipped_poly = geopandas.clip(label_df, Polygon.from_bounds(*bounds)) shapes = [(geom, value) for geom, value in zip(clipped_poly.geometry, clipped_poly[field])] label_arr = rasterize(shapes, out_shape=(width, height), default_value=0, transform=patched_transform) kwargs = img_src.meta.copy() kwargs.update({ 'driver': 'png', 'count': 1, 'height': window.height, 'width': window.width, 'transform': patched_transform, 'dtype': 'uint8' }) with rasterio.open(outfile_label, 'w', **kwargs) as dst: dst.write(label_arr, 1) img_src.close()
def _fillStack(self): min_coord = self.getMachineNearestCornerToExtruder(self._global_stack) transform_x = -int(round(min_coord[0] / abs(min_coord[0]))) transform_y = -int(round(min_coord[1] / abs(min_coord[1]))) machine_size = [ self._global_stack.getProperty("machine_width", "value"), self._global_stack.getProperty("machine_depth", "value") ] def flip_x(polygon): tm2 = [-1, 0, 0, 1, 0, 0] return affinity.affine_transform( affinity.translate(polygon, xoff=-machine_size[0]), tm2) def flip_y(polygon): tm2 = [1, 0, 0, -1, 0, 0] return affinity.affine_transform( affinity.translate(polygon, yoff=-machine_size[1]), tm2) if self._checkForCollisions(): self._node_stack = [] return node_list = [] for node in self._scene_node.getChildren(): if not issubclass(type(node), SceneNode): continue convex_hull = node.callDecoration("getConvexHull") if convex_hull: xmin = min(x for x, _ in convex_hull._points) xmax = max(x for x, _ in convex_hull._points) ymin = min(y for _, y in convex_hull._points) ymax = max(y for _, y in convex_hull._points) convex_hull_polygon = Polygon.from_bounds( xmin, ymin, xmax, ymax) if transform_x < 0: convex_hull_polygon = flip_x(convex_hull_polygon) if transform_y < 0: convex_hull_polygon = flip_y(convex_hull_polygon) node_list.append({ "node": node, "min_coord": [ convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1] ], }) node_list = sorted(node_list, key=lambda d: d["min_coord"]) self._node_stack = [d["node"] for d in node_list]
async def test_cog_tiler_tile(): async with COGTiler(INFILE) as cog: centroid = Polygon.from_bounds(*cog.bounds).centroid tile = await cog.tile( *mercantile.tile(centroid.x, centroid.y, cog.maxzoom), tilesize=256, resampling_method="bilinear" ) assert tile.data.shape == (3, 256, 256) assert tile.data.dtype == cog.profile["dtype"]
def test_crop_and_get_tile_do_the_same_when_image_is_populated(self): coords = mercantile.xy_bounds(*tiles[15]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.read_only_virtual_geo_raster() with NamedTemporaryFile(mode='w+b', suffix=".tif") as rf: raster.save(rf.name) raster = GeoRaster2.open(rf.name) tile15 = raster.get_tile(*tiles[15]) raster._populate_from_rasterio_object(read_image=True) cropped_15 = raster.crop(shape, MERCATOR_RESOLUTION_MAPPING[15]) self.assertEqual(tile15, cropped_15)
def test_rasterize_without_bounds(mock_rasterize): gv = GeoVector(Polygon.from_bounds(0, 0, 1, 1)) gv.rasterize(dest_resolution=0.1, fill_value=29) expected_shape = [gv.get_shape(gv.crs)] expected_bounds = gv.envelope.get_shape(gv.crs) mock_rasterize.assert_called_with(expected_shape, gv.crs, expected_bounds, 0.1, fill_value=29, dtype=None)
def centerline_points( in_lines: Path, distance: float = 0.0, transform: Transform = None) -> Dict[int, List[RiverPoint]]: """Generates points along each line feature at specified distances from the end as well as quarter and halfway Args: in_lines (Path): path of shapefile with features distance (float, optional): distance from ends to generate points. Defaults to 0.0. transform (Transform, optional): coordinate transformation. Defaults to None. Returns: [type]: [description] """ log = Logger('centerline_points') with get_shp_or_gpkg(in_lines) as in_lyr: out_group = {} ogr_extent = in_lyr.ogr_layer.GetExtent() extent = Polygon.from_bounds(ogr_extent[0], ogr_extent[2], ogr_extent[1], ogr_extent[3]) for feat, _counter, progbar in in_lyr.iterate_features( "Centerline points"): line = VectorBase.ogr2shapely(feat, transform) fid = feat.GetFID() out_points = [] # Attach the FID in case we need it later props = {'fid': fid} pts = [ line.interpolate(distance), line.interpolate(0.5, True), line.interpolate(-distance) ] if line.project(line.interpolate(0.25, True)) > distance: pts.append(line.interpolate(0.25, True)) pts.append(line.interpolate(-0.25, True)) for pt in pts: # Recall that interpolation can have multiple solutions due to pythagorean theorem # Throw away anything that's not inside our bounds if not extent.contains(pt): progbar.erase() log.warning('Point {} is outside of extent: {}'.format( pt.coords[0], ogr_extent)) out_points.append(RiverPoint(pt, properties=props)) out_group[int(fid)] = out_points feat = None return out_group
def test_crop_and_get_tile_do_the_same(self): coords = mercantile.xy_bounds(*tiles[15]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.read_only_virtual_geo_raster() with NamedTemporaryFile(mode='w+b', suffix=".tif") as rf: raster.save(rf.name) raster2 = GeoRaster2.open(rf.name) tile15 = raster2.get_tile(*tiles[15]) # load the image data raster2.image cropped15 = raster2.crop(shape, MERCATOR_RESOLUTION_MAPPING[15]) self.assertEqual(tile15, cropped15)
def test_crop_in_memory_and_off_memory_without_resizing_are_the_same(self): coords = mercantile.xy_bounds(*tiles[18]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.read_only_virtual_geo_raster() with NamedTemporaryFile(mode='w+b', suffix=".tif") as rf: raster.save(rf.name) raster2 = GeoRaster2.open(rf.name) off_memory_crop = raster2.crop(shape) # load the image data raster2.image in_memory_crop = raster2.crop(shape) self.assertEqual(off_memory_crop, in_memory_crop)
def _test_dict(): return { "a": 1, "b": { "a": "a", "b": datetime.datetime.now() }, "c": Polygon.from_bounds(0, 0, 10, 5), "d": Point(10, 20), "e": np.zeros([10]), "f": Modality.EL_IMAGE, }
def from_bounds_to_geojson(bounds, crs): try: in_proj = Proj(crs) out_proj = Proj(init='epsg:4326') xmin, ymin = transform(in_proj, out_proj, bounds['left'], bounds['bottom']) xmax, ymax = transform(in_proj, out_proj, bounds['right'], bounds['top']) wgs84_bounds = Polygon.from_bounds(xmin, ymin, xmax, ymax) return json.dumps(mapping(wgs84_bounds)) except RuntimeError: return ''
def test_crop_and_get_tile_do_the_same(self): coords = mercantile.xy_bounds(*tiles[15]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.metric_raster() with NamedTemporaryFile(mode='w+b', suffix=".tif") as rf: raster.save(rf.name) raster2 = GeoRaster2.open(rf.name) tile15 = raster2.get_tile(*tiles[15]) # load the image data raster2.image cropped15 = raster2.crop(shape, mercator_zoom_to_resolution[15]) self.assertEqual(tile15, cropped15)
def test_crop_and_get_tile_do_the_same_when_image_is_populated_first_for_low_zoom( self): coords = mercantile.xy_bounds(*tiles[11]) shape = GeoVector(Polygon.from_bounds(*coords), WEB_MERCATOR_CRS) raster = self.metric_raster() with NamedTemporaryFile(mode='w+b', suffix=".tif") as rf: raster.save(rf.name) raster = GeoRaster2.open(rf.name) raster._populate_from_rasterio_object(read_image=True) tile11 = raster.get_tile(*tiles[11]) cropped_11 = raster.crop(shape, mercator_zoom_to_resolution[11]) self.assertEqual(tile11, cropped_11)
def getBbox(self, srcfile, outfile): """ Creates a bounding box of polygon. Takes a polygon shp file as an input and creates a polygon shp file of bounding boxes for each of the polygon they represent. Bounding boxes will have the attributes of their respective pylogons. PARAMETER(S): : srcfile : The source polygon shapefile. : outfile : The name of the bounding box shapefile to be created. EXAMPLE(S): import bigeo bb = bigeo.BoundingBoxCreator() bb.getBbox('/home/polygon.shp', '/home/boundingbox.shp') """ self.srcfile = srcfile self.outfile = outfile with fiona.drivers(): logging.info("Reading file: " + self.srcfile) with fiona.open(self.srcfile) as src: self.meta = src.meta logging.info("Creating output file: " + self.outfile) with fiona.open(self.outfile, 'w', **self.meta) as dst: for f in src: logging.info("Creating bounds: " + str(fiona.bounds(f))) bbox = Polygon.from_bounds( fiona.bounds(f)[0], fiona.bounds(f)[1], fiona.bounds(f)[2], fiona.bounds(f)[3]) f['geometry'] = mapping(bbox) dst.write(f) logging.info( "Done creating bounds for all features. Writing to the specified output file." )
def _deserialize(self, value, att, obj): bbox = [float(i) for i in value.split(',')] if len(bbox) != 4: message = 'BBox should be in "Xmin, Ymin, Xmax, Ymax" format' raise ValidationError(message) elif bbox[2] <= bbox[0]: message = 'Xmax must be greater than Xmin' raise ValidationError(message) elif bbox[3] <= bbox[1]: message = 'Ymax must be greater than Ymin' raise ValidationError(message) geometry = Polygon.from_bounds(*bbox) return mapping(geometry)
def construct_gtiff(map_: Type[maps.Map], bbox: Tuple[float, float, float, float], zoom: int, path: Path, tiles_dir: Path, img_format: ImageFormat, projection: Optional[Proj] = None, *, printing=False) -> None: # language=rst """ Construct GeoTIFF file with `bbox` area for `map_` tiles from `tiles_dir` with `zoom` zoom-level :param map_: maps.Map subclass, which tiles will be downloaded :param bbox: bbox of area coordinates in `projection` reference system in from `(min_x, min_y, max_x, max_y)` :param zoom: zoom-level for tiles :param path: path for output GeoTIFF :param tiles_dir: path to directory that contains necessary tiles :param img_format: tiles images format :param projection: projection for output GeoTIFF :param printing: if `True`, will print info. Default -- `False` :return: """ if printing: print(f'Constructing GeoTiff to {path} ...') map_projection_bbox = bbox if projection is None else ( transform(projection, map_.projection, *bbox[:2]) + transform(projection, map_.projection, *bbox[2:])) corner_tiles = map_.get_corner_tiles(map_projection_bbox, zoom) with tempfile.NamedTemporaryFile(suffix='.tiff') as uncut_file: merge_in_gtiff(map_, corner_tiles, uncut_file.name, tiles_dir, img_format, projection) with rio.open(uncut_file.name) as img: meta = img.meta.copy() cropped_data, meta['transform'] = rio.mask.mask(img, [ Polygon.from_bounds(*bbox), ], crop=True) meta['height'], meta['width'] = cropped_data.shape[-2:] with rio.open(path, 'w', **meta) as file: for i in range(meta['count']): file.write(cropped_data[i], i + 1) if printing: print(f'done. {humanize.naturalsize(path.stat().st_size)}')
def _do_locate_reference_cell( image: Image, reference_area: int, scale: float = 0.1, ) -> Polygon: """Perform localization of reference cell""" # filter + binarize image_f = transform.rescale(image.data, scale) thresh = filters.threshold_multiotsu(image_f, classes=5)[0] image_t = image_f > thresh # find regions labeled = morphology.label(image_t) regions = measure.regionprops(labeled) # drop areas that are not approximately square regions = [ r for r in regions if np.abs(r.bbox[2] - r.bbox[0]) / np.abs(r.bbox[3] - r.bbox[1]) > 0.8 and np.abs(r.bbox[2] - r.bbox[0]) / np.abs(r.bbox[3] - r.bbox[1]) < 1.8 ] # convert to Polygon tmp: List[Polygon] = [] for r in regions: bbox = [int(r.bbox[i] * 1 / scale) for i in range(4)] y0, x0 = max(0, bbox[0]), max(0, bbox[1]) y1, x1 = ( min(image.shape[0], bbox[2]), min(image.shape[1], bbox[3]), ) box = Polygon.from_bounds(x0, y0, x1, y1) tmp.append(box) # drop boxes that intersect with others regions = [ r for r in tmp if not np.any([x.intersects(r) and x is not r for x in tmp]) ] if len(regions) == 0: return None # process regions area_dev = [np.abs((r.area - reference_area) / reference_area) for r in regions] min_dev_idx = np.argmin(area_dev) if area_dev[min_dev_idx] < 2.0: return affinity.scale(regions[min_dev_idx], xfact=0.5, yfact=0.5) else: return None
async def test_cog_read_single_band(create_cog_reader): infile = "https://async-cog-reader-test-data.s3.amazonaws.com/int16_deflate.tif" async with create_cog_reader(infile) as cog: assert cog.profile["count"] == 1 with rasterio.open(infile) as ds: _, zoom = get_zooms(ds) centroid = Polygon.from_bounds( *transform_bounds(cog.epsg, "EPSG:4326", *cog.bounds)).centroid tile = mercantile.tile(centroid.x, centroid.y, zoom) tile_native_bounds = transform_bounds("EPSG:4326", cog.epsg, *mercantile.bounds(tile)) arr = await cog.read(tile_native_bounds, (256, 256)) assert arr.shape == (256, 256) assert arr.dtype == cog.profile["dtype"]
def subdivide_geometry(geometry, max_points=100): """ Subdivides a geometry into pieces having no more than max_points points each """ q = deque([geometry]) while len(q) > 0: # Get next geometry to process from queue geom = q.pop() if count_points(geom) <= max_points: yield geom else: # Intersect geometry with each half of its bounding box, and add to work queue for b in split_bounds(*geom.bounds): q.append(geom.intersection(Polygon.from_bounds(*b)))
def test_merge_all_different_crs(crop, recwarn): roi = GeoVector( Polygon.from_bounds(-6321833, -3092272, -6319273, -3089712), WEB_MERCATOR_CRS) affine = Affine.translation(-57, -26) * Affine.scale(0.00083, -0.00083) expected_resolution = 10 expected_crs = WEB_MERCATOR_CRS # from memory raster_0 = make_test_raster(1, [1], height=1200, width=1200, affine=affine, crs=WGS84_CRS) result_0 = merge_all([raster_0], roi=roi, dest_resolution=expected_resolution, crs=expected_crs, crop=crop) assert (result_0.resolution() == expected_resolution) assert (result_0.crs == expected_crs) assert (result_0.footprint().envelope.almost_equals(roi.envelope, decimal=3)) # from file path = "/vsimem/raster_for_test.tif" result_0.save(path) raster_1 = GeoRaster2.open(path) result_1 = merge_all([raster_1], roi=roi, dest_resolution=expected_resolution, crs=expected_crs, crop=crop) assert (result_1.resolution() == expected_resolution) assert (result_1.crs == expected_crs) assert (result_1.footprint().envelope.almost_equals(roi.envelope, decimal=3)) assert (result_0 == result_1) # preserve the original resolution if dest_resolution is not provided raster_2 = make_test_raster(1, [1], height=1200, width=1200, affine=affine, crs=WGS84_CRS) result_2 = merge_all([raster_2], roi=roi, crs=expected_crs, crop=crop) assert pytest.approx(result_2.resolution()) == 97.9691
def from_bounds_to_geojson(bounds, crs): try: in_proj = Proj(crs) out_proj = Proj(init='epsg:4326') xmin, ymin = transform(in_proj, out_proj, bounds['left'], bounds['bottom']) xmax, ymax = transform(in_proj, out_proj, bounds['right'], bounds['top']) # Index creation fails for layers that # exceeds lat long limits in mongo wgs84_bounds = Polygon.from_bounds(clamp(xmin, -180.0, 180.0), clamp(ymin, -90.0, 90.0), clamp(xmax, -180.0, 180.0), clamp(ymax, -90.0, 90.0)) return mapping(wgs84_bounds) except RuntimeError: return ''
def project_with_annotations(project_with_classes, svs_small): pth_cls, *_ = project_with_classes.path_classes num_annotations = NUM_ANNOTATIONS with project_with_classes as qp: entry = qp.add_image( svs_small, image_type=QuPathImageType.BRIGHTFIELD_H_E ) for x in range(num_annotations): entry.hierarchy.add_annotation( roi=Polygon.from_bounds(0 + x, 0 + x, 1 + x, 1 + x), path_class=pth_cls, ) assert len(entry.hierarchy.annotations) == num_annotations yield project_with_classes
def test_crop_boundless_masked(bounds): raster_w_mask = GeoRaster2.open("tests/data/raster/rgb.tif") raster_wo_mask = GeoRaster2.open("tests/data/raster/rgb.jp2") roi = GeoVector(Polygon.from_bounds(*bounds), WEB_MERCATOR_CRS) assert (np.array_equal( raster_w_mask.crop(roi).image.mask, raster_wo_mask.crop(roi).image.mask)) feature = GeoFeature(roi, properties={'prop1': 1, 'prop2': 2}) assert (np.array_equal( raster_w_mask.crop(feature).image.mask, raster_wo_mask.crop(feature).image.mask)) collection = FeatureCollection([feature]) assert (np.array_equal( raster_w_mask.crop(collection).image.mask, raster_wo_mask.crop(collection).image.mask))
async def test_cog_tiler_tile(create_cog_reader): infile = "https://async-cog-reader-test-data.s3.amazonaws.com/webp_cog.tif" async with create_cog_reader(infile) as cog: tiler = COGTiler(cog) with rasterio.open(infile) as ds: _, zoom = get_zooms(ds) centroid = Polygon.from_bounds( *transform_bounds(cog.epsg, "EPSG:4326", *cog.bounds)).centroid tile = mercantile.tile(centroid.x, centroid.y, zoom) # tile_native_bounds = transform_bounds("EPSG:4326", cog.epsg, *mercantile.bounds(tile)) arr = await tiler.tile(tile.x, tile.y, tile.z, tile_size=256, resample_method=Image.BILINEAR) with cogeo_reader(infile) as ds: rio_tile_arr, rio_tile_mask = ds.tile(tile.x, tile.y, tile.z, tilesize=256, resampling_method="bilinear") if cog.is_masked: tile_arr = np.ma.getdata(arr) tile_mask = np.ma.getmask(arr) # Make sure image data is the same assert pytest.approx(tile_arr - rio_tile_arr, 1) == np.zeros(tile_arr.shape) # Make sure mask data is the same rio_mask_counts = np.unique(rio_tile_mask, return_counts=True) tile_mask_counts = np.unique(tile_mask, return_counts=True) assert len(rio_mask_counts[0]) == len(tile_mask_counts[0]) assert (rio_mask_counts[1][0] * cog.profile["count"] == tile_mask_counts[1][0]) else: assert pytest.approx(arr - rio_tile_arr, 1) == np.zeros(arr.shape)
def from_bounds(cls, xmin, ymin, xmax, ymax, crs=DEFAULT_CRS): """Creates GeoVector object from bounds. Parameters ---------- xmin, ymin, xmax, ymax : float Bounds of the GeoVector. Also (east, south, north, west). crs : ~rasterio.crs.CRS, dict Projection, default to :py:data:`telluric.constants.DEFAULT_CRS`. Examples -------- >>> from telluric import GeoVector >>> GeoVector.from_bounds(xmin=0, ymin=0, xmax=1, ymax=1) GeoVector(shape=POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), crs=CRS({'init': 'epsg:4326'})) >>> GeoVector.from_bounds(xmin=0, xmax=1, ymin=0, ymax=1) GeoVector(shape=POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)), crs=CRS({'init': 'epsg:4326'})) """ return cls(Polygon.from_bounds(xmin, ymin, xmax, ymax), crs)
def test_empty_from_roi(): roi = GeoVector(Polygon.from_bounds(12.36, 42.05, 12.43, 42.10), WGS84_CRS).reproject(WEB_MERCATOR_CRS) resolution = 20.0 band_names = ["a", "b", "c"] some_dtype = np.uint16 empty = GeoRaster2.empty_from_roi(roi, resolution, band_names, some_dtype) # Cannot compare approximate equality of polygons because the # topology might be different, see https://github.com/Toblerity/Shapely/issues/535 # (Getting the envelope seems to fix the problem) # Also, reprojecting the empty footprint to WGS84 permits using # a positive number of decimals (the relative error is of course # the same) assert empty.footprint().reproject(WGS84_CRS).envelope.almost_equals( roi.envelope, decimal=3) assert empty.resolution() == resolution assert empty.crs == roi.crs assert empty.band_names == band_names assert empty.dtype == some_dtype assert empty.affine.determinant == -1 * resolution * resolution