def test_get_windows(self): extent = Box(0, 0, 100, 100) windows = list(extent.get_windows(10, 10)) self.assertEqual(len(windows), 100) extent = Box(0, 0, 100, 100) windows = list(extent.get_windows(10, 5)) self.assertEqual(len(windows), 400) extent = Box(0, 0, 20, 20) windows = set( [window.tuple_format() for window in extent.get_windows(10, 10)]) expected_windows = [ Box.make_square(0, 0, 10), Box.make_square(10, 0, 10), Box.make_square(0, 10, 10), Box.make_square(10, 10, 10) ] expected_windows = set( [window.tuple_format() for window in expected_windows]) self.assertSetEqual(windows, expected_windows) extent = Box(10, 10, 20, 20) windows = set( [window.tuple_format() for window in extent.get_windows(6, 6)]) expected_windows = [ Box.make_square(10, 10, 6), Box.make_square(10, 16, 6), Box.make_square(16, 10, 6), Box.make_square(16, 16, 6) ] expected_windows = set( [window.tuple_format() for window in expected_windows]) self.assertSetEqual(windows, expected_windows)
def test_nonidentical_extents_and_resolutions(self): img_path_1 = data_file_path( 'multi_raster_source/const_100_600x600.tiff') img_path_2 = data_file_path('multi_raster_source/const_175_60x60.tiff') img_path_3 = data_file_path('multi_raster_source/const_250_6x6.tiff') source_1 = RasterioSourceConfig(uris=[img_path_1], channel_order=[0]) source_2 = RasterioSourceConfig(uris=[img_path_2], channel_order=[0]) source_3 = RasterioSourceConfig(uris=[img_path_3], channel_order=[0]) cfg = MultiRasterSourceConfig(raster_sources=[ SubRasterSourceConfig(raster_source=source_1, target_channels=[0]), SubRasterSourceConfig(raster_source=source_2, target_channels=[1]), SubRasterSourceConfig(raster_source=source_3, target_channels=[2]) ]) rs = cfg.build(tmp_dir=self.tmp_dir) with rs.activate(): for get_chip_fn in [rs._get_chip, rs.get_chip]: ch_1_only = get_chip_fn(Box(0, 0, 10, 10)) self.assertEqual(tuple(ch_1_only.reshape(-1, 3).mean(axis=0)), (100, 0, 0)) ch_2_only = get_chip_fn(Box(0, 600, 10, 600 + 10)) self.assertEqual(tuple(ch_2_only.reshape(-1, 3).mean(axis=0)), (0, 175, 0)) ch_3_only = get_chip_fn(Box(600, 0, 600 + 10, 10)) self.assertEqual(tuple(ch_3_only.reshape(-1, 3).mean(axis=0)), (0, 0, 250)) full_img = get_chip_fn(Box(0, 0, 600, 600)) self.assertEqual(set(np.unique(full_img[..., 0])), {100}) self.assertEqual(set(np.unique(full_img[..., 1])), {0, 175}) self.assertEqual(set(np.unique(full_img[..., 2])), {0, 250})
def get_extent(self): h, w = self.height, self.width if self.extent_crop is not None: skip_top, skip_left, skip_bottom, skip_right = self.extent_crop ymin, xmin = int(h * skip_top), int(w * skip_left) ymax, xmax = h - int(h * skip_bottom), w - int(w * skip_right) return Box(ymin, xmin, ymax, xmax) return Box(0, 0, h, w)
def test_make_buffer(self): buffer_size = 1 max_extent = Box.make_square(0, 0, 3) buffer_box = Box(0, 0, 3, 3) output_buffer_box = self.box.make_buffer(buffer_size, max_extent) self.assertEqual(output_buffer_box, buffer_box) buffer_size = 0.5 max_extent = Box.make_square(0, 0, 5) buffer_box = Box(0, 0, 3, 5) output_buffer_box = self.box.make_buffer(buffer_size, max_extent) self.assertEqual(output_buffer_box, buffer_box)
def read_labels(geojson, extent=None) -> ChipClassificationLabels: """Convert GeoJSON to ChipClassificationLabels. If the GeoJSON already contains a grid of cells, then it can be constructed in a straightforward manner without having to infer the class of cells. If extent is given, only labels that intersect with the extent are returned. Args: geojson: dict in normalized GeoJSON format (see VectorSource) extent: Box in pixel coords Returns: ChipClassificationLabels """ labels = ChipClassificationLabels() for f in geojson['features']: geom = shape(f['geometry']) (xmin, ymin, xmax, ymax) = geom.bounds cell = Box(ymin, xmin, ymax, xmax) if extent is not None and not cell.to_shapely().intersects( extent.to_shapely()): continue props = f['properties'] class_id = props['class_id'] scores = props.get('scores') labels.set_cell(cell, class_id, scores) return labels
def test_subscripting(self): box = Box(1, 2, 3, 4) self.assertEqual(box[0], 1) self.assertEqual(box[1], 2) self.assertEqual(box[2], 3) self.assertEqual(box[3], 4) self.assertRaises(IndexError, lambda: box[4])
def setUp(self): self.windows = [ Box.make_square(0, 0, 10), Box.make_square(0, 5, 10), Box.make_square(0, 10, 10) ] self.num_classes = 3 self.scores_left = make_random_scores(self.num_classes, 10, 10) self.scores_mid = make_random_scores(self.num_classes, 10, 10) self.scores_right = make_random_scores(self.num_classes, 10, 10) self.scores_left = self.scores_left.astype(np.float16) self.scores_mid = self.scores_mid.astype(np.float16) self.scores_right = self.scores_right.astype(np.float16) self.extent = Box(0, 0, 10, 20) self.labels = SemanticSegmentationSmoothLabels( extent=self.extent, num_classes=self.num_classes) self.labels[self.windows[0]] = self.scores_left self.labels[self.windows[1]] = self.scores_mid self.labels[self.windows[2]] = self.scores_right arr = np.zeros((self.num_classes, 10, 20), dtype=np.float16) arr[..., :10] += self.scores_left arr[..., 5:15] += self.scores_mid arr[..., 10:] += self.scores_right self.expected_scores = arr hits = np.zeros((10, 20), dtype=np.uint8) hits[..., :10] += 1 hits[..., 5:15] += 1 hits[..., 10:] += 1 self.expected_hits = hits
def test_make_eroded(self): max_extent = Box.make_square(0, 0, 10) box = Box(1, 1, 3, 4) buffer_size = erosion_size = 1 eroded_box = box.make_buffer(buffer_size, max_extent) \ .make_eroded(erosion_size) self.assertEqual(eroded_box, box)
def test_normalized_to_local(self): norm_npboxes = np.array([[0., 0., 0.2, 0.02], [0.2, 0.02, 0.4, 0.04]]) window = Box(0, 0, 10, 100) local_npboxes = ObjectDetectionLabels.normalized_to_local( norm_npboxes, window) expected_local_npboxes = np.array([[0., 0., 2., 2.], [2., 2., 4., 4.]]) np.testing.assert_array_equal(local_npboxes, expected_local_npboxes)
def test_fill_overflow(self): extent = Box(10, 10, 90, 90) window = Box(0, 0, 100, 100) arr = np.ones((100, 100), dtype=np.uint8) out = fill_overflow(extent, window, arr) mask = np.zeros_like(arr).astype(bool) mask[10:90, 10:90] = 1 self.assertTrue(np.all(out[mask] == 1)) self.assertTrue(np.all(out[~mask] == 0)) window = Box(0, 0, 80, 100) arr = np.ones((80, 100), dtype=np.uint8) out = fill_overflow(extent, window, arr) mask = np.zeros((80, 100), dtype=bool) mask[10:90, 10:90] = 1 self.assertTrue(np.all(out[mask] == 1)) self.assertTrue(np.all(out[~mask] == 0))
def test_build(self): self.assertIsInstance(SemanticSegmentationLabels.build(smooth=False), SemanticSegmentationDiscreteLabels) self.assertRaises( ValueError, lambda: SemanticSegmentationLabels.build(smooth=True)) self.assertRaises( ValueError, lambda: SemanticSegmentationLabels.build(smooth=True, extent=Box(0, 0, 10, 10))) self.assertIsInstance( SemanticSegmentationLabels.build(smooth=True, extent=Box(0, 0, 10, 10), num_classes=2), SemanticSegmentationSmoothLabels)
def test_make_random_square(self): window = Box(5, 5, 15, 15) size = 5 nb_tests = 100 for _ in range(nb_tests): box = window.make_random_square(size) self.assertEqual(box.get_width(), box.get_height()) self.assertEqual(box.get_width(), size) self.assertTrue(window.to_shapely().contains(box.to_shapely()))
def get_extent(self) -> Box: rs = self.raster_sources[0] extent = rs.get_extent() if self.extent_crop is not None: h, w = extent.get_height(), extent.get_width() skip_top, skip_left, skip_bottom, skip_right = self.extent_crop ymin, xmin = int(h * skip_top), int(w * skip_left) ymax, xmax = h - int(h * skip_bottom), w - int(w * skip_right) return Box(ymin, xmin, ymax, xmax) return extent
def test_enough_target_pixels_true(self): data = np.zeros((10, 10, 3), dtype=np.uint8) data[4:, 4:, :] = [1, 1, 1] raster_source = MockRasterSource([0, 1, 2], 3) raster_source.set_raster(data) rgb_class_map = ClassMap([ClassItem(id=1, color='#010101')]) label_source = SemanticSegmentationLabelSource( source=raster_source, rgb_class_map=rgb_class_map) with label_source.activate(): extent = Box(0, 0, 10, 10) self.assertTrue(label_source.enough_target_pixels(extent, 30, [1]))
def test_get_chip(self): # create a 3-channel raster from a 1-channel raster # using a ReclassTransformer to give each channel a different value # (100, 175, and 250 respectively) path = data_file_path('multi_raster_source/const_100_600x600.tiff') source_1 = RasterioSourceConfig(uris=[path], channel_order=[0]) source_2 = RasterioSourceConfig( uris=[path], channel_order=[0], transformers=[ReclassTransformerConfig(mapping={100: 175})]) source_3 = RasterioSourceConfig( uris=[path], channel_order=[0], transformers=[ReclassTransformerConfig(mapping={100: 250})]) cfg = MultiRasterSourceConfig(raster_sources=[ SubRasterSourceConfig(raster_source=source_1, target_channels=[0]), SubRasterSourceConfig(raster_source=source_2, target_channels=[1]), SubRasterSourceConfig(raster_source=source_3, target_channels=[2]) ], channel_order=[2, 1, 0], transformers=[ ReclassTransformerConfig(mapping={ 100: 10, 175: 17, 250: 25 }) ]) rs = cfg.build(tmp_dir=self.tmp_dir) with rs.activate(): window = Box(0, 0, 100, 100) # sub transformers and channel_order applied sub_chips = rs._get_sub_chips(window, raw=False) self.assertEqual(tuple(c.mean() for c in sub_chips), (100, 175, 250)) # sub transformers, channel_order, and transformer applied chip = rs.get_chip(window) self.assertEqual(tuple(chip.reshape(-1, 3).mean(axis=0)), (25, 17, 10)) # none of sub transformers, channel_order, and transformer applied sub_chips = rs._get_sub_chips(window, raw=True) self.assertEqual(tuple(c.mean() for c in sub_chips), (100, 100, 100)) chip = rs._get_chip(window) self.assertEqual(tuple(chip.reshape(-1, 3).mean(axis=0)), (100, 100, 100))
def test_get_with_aoi(self): aoi_polygons = [Box.make_square(5, 15, 2).to_shapely()] exp_label_arr = self.label_arr1.copy() mask = np.zeros(exp_label_arr.shape) mask[5:7, 5:7] = 1 exp_label_arr = exp_label_arr * mask labels = self.labels.filter_by_aoi(aoi_polygons) label_arr = labels.get_label_arr(self.windows[1]) np.testing.assert_array_equal(label_arr, exp_label_arr) # Set clip_extent clip_extent = Box(0, 0, 10, 18) label_arr = labels.get_label_arr(self.windows[1], clip_extent=clip_extent) np.testing.assert_array_equal(label_arr, exp_label_arr[:, 0:8])
def _get_shifted_window(self, window): do_shift = self.x_shift != 0.0 or self.y_shift != 0.0 if do_shift: ymin, xmin, ymax, xmax = window.tuple_format() width = window.get_width() height = window.get_height() # Transform image coordinates into world coordinates transform = self.image_dataset.transform xmin2, ymin2 = transform * (xmin, ymin) # Transform from world coordinates to WGS84 if self.crs != wgs84_proj4 and self.proj: lon, lat = pyproj.transform(self.proj, wgs84, xmin2, ymin2) else: lon, lat = xmin2, ymin2 # Shift. This is performed by computing the shifts in # meters to shifts in degrees. Those shifts are then # applied to the WGS84 coordinate. # # Courtesy of https://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters # noqa lat_radians = math.pi * lat / 180.0 dlon = Decimal(self.x_shift) / Decimal( meters_per_degree * math.cos(lat_radians)) dlat = Decimal(self.y_shift) / Decimal(meters_per_degree) lon = float(Decimal(lon) + dlon) lat = float(Decimal(lat) + dlat) # Transform from WGS84 to world coordinates if self.crs != wgs84_proj4 and self.proj: xmin3, ymin3 = pyproj.transform(wgs84, self.proj, lon, lat) xmin3 = int(round(xmin3)) ymin3 = int(round(ymin3)) else: xmin3, ymin3 = lon, lat # Trasnform from world coordinates back into image coordinates xmin4, ymin4 = ~transform * (xmin3, ymin3) window = Box(ymin4, xmin4, ymin4 + height, xmin4 + width) return window
def get_transformed_window(self, window: Box, inverse: bool = False) -> Box: """Apply the CRS transform to the window. Args: window (Box): A window in pixel coordinates. Returns: Box: A window in world coordinates. """ if inverse: tf = self.crs_transformer.map_to_pixel else: tf = self.crs_transformer.pixel_to_map ymin, xmin, ymax, xmax = window xmin, ymin = tf((xmin, ymin)) xmax, ymax = tf((xmax, ymax)) window = Box(ymin, xmin, ymax, xmax) return window
def from_geojson(geojson, extent=None): """Convert GeoJSON to ObjectDetectionLabels object. If extent is provided, filter out the boxes that lie "more than a little bit" outside the extent. Args: geojson: (dict) normalized GeoJSON (see VectorSource) extent: (Box) in pixel coords Returns: ObjectDetectionLabels """ boxes = [] class_ids = [] scores = [] for f in geojson['features']: geom = shape(f['geometry']) (xmin, ymin, xmax, ymax) = geom.bounds boxes.append(Box(ymin, xmin, ymax, xmax)) props = f['properties'] class_ids.append(props['class_id']) scores.append(props.get('score', 1.0)) if len(boxes): boxes = np.array([box.npbox_format() for box in boxes], dtype=float) class_ids = np.array(class_ids) scores = np.array(scores) labels = ObjectDetectionLabels(boxes, class_ids, scores=scores) else: labels = ObjectDetectionLabels.make_empty() if extent is not None: labels = ObjectDetectionLabels.get_overlapping(labels, extent, ioa_thresh=0.8, clip=True) return labels
def test_extent_crop_overflow(self): f = 1 / 10 arr = np.ones((100, 100), dtype=np.uint8) mask = np.zeros_like(arr).astype(bool) mask[10:90, 10:90] = 1 with NamedTemporaryFile('wb') as fp: uri = fp.name with rasterio.open(uri, 'w', driver='GTiff', height=100, width=100, count=1, dtype=np.uint8) as ds: ds.write_band(1, arr) cfg = RasterioSourceConfig(uris=[uri], extent_crop=(f, f, f, f)) rs = cfg.build(tmp_dir=self.tmp_dir) with rs.activate(): out = rs.get_chip(Box(0, 0, 100, 100))[..., 0] self.assertTrue(np.all(out[mask] == 1)) self.assertTrue(np.all(out[~mask] == 0))
def make_labels(self, class_ids): """Make 2x2 grid label store. Args: class_ids: 2x2 array of class_ids to use """ cell_size = 200 y_cells = 2 x_cells = 2 labels = ChipClassificationLabels() for yind in range(y_cells): for xind in range(x_cells): ymin = yind * cell_size xmin = xind * cell_size ymax = ymin + cell_size xmax = xmin + cell_size window = Box(ymin, xmin, ymax, xmax) class_id = class_ids[yind][xind] new_labels = ChipClassificationLabels() new_labels.set_cell(window, class_id) labels.extend(new_labels) return labels
def test_neq(self): other = Box(self.ymin + 1, self.xmin, self.ymax, self.xmax) self.assertTrue(self.box != other)
def test_int(self): other = Box( float(self.ymin) + 0.01, float(self.xmin), float(self.ymax), float(self.xmax)) self.assertTrue(other.to_int() == self.box)
def test_repr(self): box = Box(1, 2, 3, 4) self.assertEqual(box.__repr__(), 'Box(1, 2, 3, 4)')
def test_unpacking(self): box = Box(1, 2, 3, 4) ymin, xmin, ymax, xmax = box self.assertEqual((ymin, xmin, ymax, xmax), box.tuple_format())
def test_make_square(self): square = Box(0, 0, 10, 10) output_square = Box.make_square(0, 0, 10) self.assertEqual(output_square, square) self.assertEqual(output_square.get_width(), output_square.get_height())
def setUp(self): self.ymin = 0 self.xmin = 0 self.ymax = 2 self.xmax = 3 self.box = Box(self.ymin, self.xmin, self.ymax, self.xmax)
def get_extent(self): return Box(0, 0, self.height, self.width)
def generate_scene(task, tiff_path, labels_path, chip_size, chips_per_dimension): """Generate a synthetic object detection scene. Randomly generates a GeoTIFF with red and greed boxes denoting two classes and a corresponding label file. This is useful for generating synthetic scenes for testing purposes. """ class_map = ClassMap([ClassItem(1, 'car'), ClassItem(2, 'building')]) # make extent that's divisible by chip_size chip_size = chip_size ymax = chip_size * chips_per_dimension xmax = chip_size * chips_per_dimension extent = Box(0, 0, ymax, xmax) # make windows along grid windows = extent.get_windows(chip_size, chip_size) # for each window, make some random boxes within it and render to image nb_channels = 3 image = np.zeros((ymax, xmax, nb_channels)).astype(np.uint8) boxes = [] class_ids = [] for window in windows: # leave some windows blank if random.uniform(0, 1) > 0.3: # pick a random class class_id = random.randint(1, 2) box = window.make_random_square(50).to_int() boxes.append(box) class_ids.append(class_id) image[box.ymin:box.ymax, box.xmin:box.xmax, class_id - 1] = 255 # save image as geotiff centered in philly transform = from_origin(-75.163506, 39.952536, 0.000001, 0.000001) print('Generated {} boxes with {} different classes.'.format( len(boxes), len(set(class_ids)))) with rasterio.open(tiff_path, 'w', driver='GTiff', height=ymax, transform=transform, crs='EPSG:4326', compression=rasterio.enums.Compression.none, width=xmax, count=nb_channels, dtype='uint8') as dst: for channel_ind in range(0, nb_channels): dst.write(image[:, :, channel_ind], channel_ind + 1) if task == 'object_detection': # make OD labels and make boxes npboxes = Box.to_npboxes(boxes) class_ids = np.array(class_ids) labels = ObjectDetectionLabels(npboxes, class_ids) # save labels to geojson with rasterio.open(tiff_path) as image_dataset: crs_transformer = RasterioCRSTransformer(image_dataset) od_file = ObjectDetectionGeoJSONStore(labels_path, crs_transformer, class_map) od_file.save(labels) elif task == 'semantic_segmentation': label_image = np.zeros((ymax, xmax, 1)).astype(np.uint8) for box, class_id in zip(boxes, class_ids): label_image[box.ymin:box.ymax, box.xmin:box.xmax, 0] = class_id # save labels to raster with rasterio.open(labels_path, 'w', driver='GTiff', height=ymax, transform=transform, crs='EPSG:4326', compression=rasterio.enums.Compression.none, width=xmax, count=1, dtype='uint8') as dst: dst.write(label_image[:, :, 0], 1)
def _sample_window(self) -> Box: """Randomly sample a window with random size and location.""" h, w = self.sample_window_size() x, y = self.sample_window_loc(h, w) window = Box(y, x, y + h, x + w) return window