diff --git a/src/aws/osml/model_runner/tile_worker/variable_overlap_tiling_strategy.py b/src/aws/osml/model_runner/tile_worker/variable_overlap_tiling_strategy.py index 7df9d4b..15cccf7 100644 --- a/src/aws/osml/model_runner/tile_worker/variable_overlap_tiling_strategy.py +++ b/src/aws/osml/model_runner/tile_worker/variable_overlap_tiling_strategy.py @@ -224,6 +224,19 @@ def _group_features_by_overlap( def _calculate_overlap_for_full_tiles( full_image_size: ImageDimensions, fixed_tile_size: ImageDimensions, minimum_overlap: ImageDimensions ) -> ImageDimensions: + """ + Calculate the adjusted overlap for generating full tiles. + + This method adjusts the minimum overlap to ensure that tiles generated from the + full image will align properly, minimizing any remaining space at the edges. + + :param full_image_size: The dimensions of the full image (width, height) in pixels. + :param fixed_tile_size: The fixed dimensions of the tiles (width, height) in pixels. + :param minimum_overlap: The minimum overlap (width, height) between tiles in pixels. + + :return: The adjusted overlap (width, height) that should be applied to the tiles. + """ + def expand_overlap(image_dimension: int, tile_dimension: int, minimum_overlap: int) -> int: stride = tile_dimension - minimum_overlap num_tiles = ceildiv(image_dimension - minimum_overlap, stride) @@ -242,6 +255,21 @@ def expand_overlap(image_dimension: int, tile_dimension: int, minimum_overlap: i def _calculate_region_size_for_full_tiles( nominal_region_size: ImageDimensions, fixed_tile_size: ImageDimensions, minimum_overlap: ImageDimensions ) -> ImageDimensions: + """ + Calculate the adjusted region size to accommodate full tiles. + + This method adjusts the region size to ensure that tiles can be generated without + leaving any partial tiles. It considers the fixed tile size and the minimum overlap + between tiles. + + :param nominal_region_size: The nominal region size (width, height) in pixels. + :param fixed_tile_size: The fixed dimensions of the tiles (width, height) in pixels. + :param minimum_overlap: The minimum overlap (width, height) between tiles in pixels. + + :raises ValueError: If the requested overlap is greater than or equal to the tile size. + + :return: The adjusted region size (width, height) that accommodates full tiles. + """ if minimum_overlap[0] >= fixed_tile_size[0] or minimum_overlap[1] >= fixed_tile_size[1]: raise ValueError(f"Requested overlap {minimum_overlap} is invalid for tile size {fixed_tile_size}") diff --git a/test/aws/osml/model_runner/tile_worker/test_tiling_strategy.py b/test/aws/osml/model_runner/tile_worker/test_tiling_strategy.py index 9faaa95..f22e3f2 100644 --- a/test/aws/osml/model_runner/tile_worker/test_tiling_strategy.py +++ b/test/aws/osml/model_runner/tile_worker/test_tiling_strategy.py @@ -7,12 +7,18 @@ class TestTilingStrategy(TestCase): def test_chip_generator(self): + """ + Test that the chip generator correctly produces crops based on the given image size, + crop size, and overlap. Verifies multiple scenarios for different configurations. + """ from aws.osml.model_runner.tile_worker.tiling_strategy import generate_crops + # Test case 1: Image with partial overlap chip_list = [] for chip in generate_crops(((5, 10), (1024, 1024)), (300, 300), (44, 44)): chip_list.append(chip) + # Verify the total number of chips and their positions/sizes assert len(chip_list) == 16 assert chip_list[0] == ((5, 10), (300, 300)) assert chip_list[1] == ((5, 266), (300, 300)) @@ -20,10 +26,12 @@ def test_chip_generator(self): assert chip_list[12] == ((773, 10), (300, 256)) assert chip_list[15] == ((773, 778), (256, 256)) + # Test case 2: Image with no overlap, producing complete tiles chip_list = [] for chip in generate_crops(((0, 0), (5000, 2500)), (2048, 2048), (0, 0)): chip_list.append(chip) + # Verify the total number of chips and their positions/sizes assert len(chip_list) == 6 assert chip_list[0] == ((0, 0), (2048, 2048)) assert chip_list[1] == ((0, 2048), (2048, 2048)) @@ -32,13 +40,21 @@ def test_chip_generator(self): assert chip_list[4] == ((2048, 2048), (2048, 452)) assert chip_list[5] == ((2048, 4096), (904, 452)) + # Test case 3: Image with full overlap, large crop sizes chip_list = [] for chip in generate_crops(((150, 150), (5000, 5000)), (2048, 2048), (1024, 1024)): chip_list.append(chip) + # Verify the output is logically handled even without assert checks (e.g., no exceptions) + def test_invalid_chip_generator(self): + """ + Test that the chip generator raises an error for invalid overlap configurations. + Verifies scenarios where the overlap exceeds crop size, which is not allowed. + """ from aws.osml.model_runner.tile_worker.tiling_strategy import generate_crops + # Overlap values larger than crop dimensions should raise a ValueError with pytest.raises(ValueError): chip_list = [] for chip in generate_crops(((5, 10), (1024, 1024)), (300, 300), (301, 0)): diff --git a/test/aws/osml/model_runner/tile_worker/test_variable_overlap_tiling_strategy.py b/test/aws/osml/model_runner/tile_worker/test_variable_overlap_tiling_strategy.py index a72bed0..f6deaf8 100644 --- a/test/aws/osml/model_runner/tile_worker/test_variable_overlap_tiling_strategy.py +++ b/test/aws/osml/model_runner/tile_worker/test_variable_overlap_tiling_strategy.py @@ -6,18 +6,27 @@ class TestVariableOverlapTilingStrategy(TestCase): def test_compute_regions_full_image(self): + """ + Test that regions are correctly computed for a full-sized image + based on the specified nominal region size, tile size, and overlap. + """ from aws.osml.model_runner.tile_worker import VariableOverlapTilingStrategy tiling_strategy = VariableOverlapTilingStrategy() + # Define image and tiling parameters full_image_size = (25000, 12000) nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) - # Check a full image + # Compute regions for the full image regions = tiling_strategy.compute_regions(((0, 0), full_image_size), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 8 + + # Verify that all computed regions match expected results for r in regions: assert r in [ ((0, 0), (7580, 8048)), @@ -31,17 +40,26 @@ def test_compute_regions_full_image(self): ] def test_compute_regions_roi(self): + """ + Test that regions are correctly computed within a specific region of interest (ROI) + based on the specified nominal region size, tile size, and overlap. + """ from aws.osml.model_runner.tile_worker import VariableOverlapTilingStrategy tiling_strategy = VariableOverlapTilingStrategy() + # Define tiling parameters nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) - # Check regions generated from a subset of an image + # Compute regions within the ROI regions = tiling_strategy.compute_regions(((200, 8000), (17000, 11800)), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 6 + + # Verify that all computed regions match expected results for r in regions: assert r in [ ((200, 8000), (7322, 7948)), @@ -53,28 +71,40 @@ def test_compute_regions_roi(self): ] def test_compute_regions_tiny_image(self): + """ + Test that regions are correctly computed for a tiny image where the + nominal region size is larger than the image itself. + """ from aws.osml.model_runner.tile_worker import VariableOverlapTilingStrategy tiling_strategy = VariableOverlapTilingStrategy() + # Define tiling parameters nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) - # Check regions generated from an image that has a dimension smaller than the fixed tile size + # Compute regions for a tiny image regions = tiling_strategy.compute_regions(((0, 0), (12000, 2000)), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 2 + + # Verify that all computed regions match expected results for r in regions: assert r in [((0, 0), (8048, 2000)), ((0, 7904), (4096, 2000))] def test_compute_tiles(self): + """ + Test that tiles are correctly computed within specified regions based on tile size and overlap. + """ from aws.osml.model_runner.tile_worker import VariableOverlapTilingStrategy tiling_strategy = VariableOverlapTilingStrategy() overlap = (100, 100) tile_size = (4096, 4096) - # First full region + # Test full region tiling tiles = tiling_strategy.compute_tiles(((0, 0), (7580, 8048)), tile_size, overlap) assert len(tiles) == 4 for t in tiles: @@ -85,39 +115,44 @@ def test_compute_tiles(self): ((3952, 3484), (4096, 4096)), ] - # A region on the right edge of the image + # Test region on the right edge of the image tiles = tiling_strategy.compute_tiles(((0, 20904), (4096, 8048)), tile_size, overlap) assert len(tiles) == 2 for t in tiles: assert t in [((0, 20904), (4096, 4096)), ((3952, 20904), (4096, 4096))] - # A region on the bottom edge of the image + # Test region on the bottom edge of the image tiles = tiling_strategy.compute_tiles(((7904, 13936), (7580, 4096)), tile_size, overlap) assert len(tiles) == 2 for t in tiles: assert t in [((7904, 13936), (4096, 4096)), ((7904, 17420), (4096, 4096))] - # The bottom right corner region + # Test bottom right corner region tiles = tiling_strategy.compute_tiles(((7904, 20904), (4096, 4096)), tile_size, overlap) assert len(tiles) == 1 assert tiles[0] == ((7904, 20904), (4096, 4096)) def test_compute_tiles_tiny_region(self): + """ + Test that tiles are correctly computed for a small region that is smaller than the tile size. + """ from aws.osml.model_runner.tile_worker import VariableOverlapTilingStrategy tiling_strategy = VariableOverlapTilingStrategy() overlap = (100, 100) tile_size = (4096, 4096) - # If the requested region is smaller than the actual tile size then the full region - # will be turned into a single tile. This is an odd edge case that we don't expect - # to see much but we don't want to error out and will instead just fall back to - # a partial tile strategy. + # Compute tiles for a small region tiles = tiling_strategy.compute_tiles(((10, 50), (1024, 2048)), tile_size, overlap) + + # Verify that only a single tile is produced for the tiny region assert len(tiles) == 1 assert tiles[0] == ((10, 50), (1024, 2048)) def test_deconflict_features(self): + """ + Test that duplicate features are properly deconflicted based on specified rules. + """ from geojson import Feature from aws.osml.model_runner.inference import FeatureSelector @@ -125,30 +160,28 @@ def test_deconflict_features(self): tiling_strategy = VariableOverlapTilingStrategy() + # Define image and tiling parameters full_image_size = (25000, 12000) full_image_region = ((0, 0), full_image_size) nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) + # Sample feature inputs, including duplicates features = [ - # Duplicate features in overlap of two regions, keep 1 Feature(properties={"imageBBox": [20904, 7904, 20924, 7924]}), Feature(properties={"imageBBox": [20905, 7905, 20925, 7925]}), - # Duplicate features in overlap of two tiles in lower edge region, keep 1 Feature(properties={"imageBBox": [17500, 10000, 17510, 10010]}), Feature(properties={"imageBBox": [17500, 10000, 17510, 10010]}), - # Duplicate features in overlap of tiles in center of first region, keep 1 Feature(properties={"imageBBox": [4000, 4000, 4010, 4010]}), Feature(properties={"imageBBox": [4000, 4000, 4010, 4010]}), - # Duplicate features in overlap of tiles in center of second region, keep 1 Feature(properties={"imageBBox": [10000, 4000, 10010, 4010]}), Feature(properties={"imageBBox": [10000, 4000, 10010, 4010]}), - # Features duplicate but do not touch overlap regions, keep 2 Feature(properties={"imageBBox": [10, 10, 10, 10]}), Feature(properties={"imageBBox": [10, 10, 10, 10]}), ] + # Mock feature selector to deconflict overlapping features class DummyFeatureSelector(FeatureSelector): def select_features(self, features): if len(features) > 0: @@ -156,13 +189,16 @@ def select_features(self, features): return [] mock_feature_selector = Mock(wraps=DummyFeatureSelector()) + + # Deconflict features using the tiling strategy deduped_features = tiling_strategy.cleanup_duplicate_features( full_image_region, nominal_region_size, tile_size, overlap, features, mock_feature_selector ) - # Check to ensure we have the correct number of features returned and that the feature selector - # was called once for each overlapping group + # Verify the correct number of deconflicted features assert len(deduped_features) == 6 + + # Verify that the feature selector was called for each overlapping group assert len(mock_feature_selector.method_calls) == 4 diff --git a/test/aws/osml/model_runner/tile_worker/test_variable_tile_tiling_strategy.py b/test/aws/osml/model_runner/tile_worker/test_variable_tile_tiling_strategy.py index c6736eb..ad0e101 100644 --- a/test/aws/osml/model_runner/tile_worker/test_variable_tile_tiling_strategy.py +++ b/test/aws/osml/model_runner/tile_worker/test_variable_tile_tiling_strategy.py @@ -6,17 +6,27 @@ class TestVariableTileTilingStrategy(TestCase): def test_compute_regions(self): + """ + Test that regions are correctly computed for a full-sized image based on + the specified nominal region size, tile size, and overlap. + """ from aws.osml.model_runner.tile_worker import VariableTileTilingStrategy tiling_strategy = VariableTileTilingStrategy() + # Define image and tiling parameters full_image_size = (25000, 12000) nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) + # Compute regions regions = tiling_strategy.compute_regions(((0, 0), full_image_size), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 6 + + # Verify that all computed regions match expected results for r in regions: assert r in [ ((0, 0), (10000, 10000)), @@ -28,16 +38,26 @@ def test_compute_regions(self): ] def test_compute_regions_roi(self): + """ + Test that regions are correctly computed within a specific region of interest (ROI) + based on the specified nominal region size, tile size, and overlap. + """ from aws.osml.model_runner.tile_worker import VariableTileTilingStrategy tiling_strategy = VariableTileTilingStrategy() + # Define tiling parameters nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) + # Compute regions within the ROI regions = tiling_strategy.compute_regions(((200, 8000), (17000, 11800)), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 4 + + # Verify that all computed regions match expected results for r in regions: assert r in [ ((200, 8000), (10000, 10000)), @@ -47,27 +67,40 @@ def test_compute_regions_roi(self): ] def test_compute_regions_tiny_image(self): + """ + Test that regions are correctly computed for a tiny image where the + nominal region size is larger than the image itself. + """ from aws.osml.model_runner.tile_worker import VariableTileTilingStrategy tiling_strategy = VariableTileTilingStrategy() + # Define tiling parameters nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) + # Compute regions for a tiny image regions = tiling_strategy.compute_regions(((0, 0), (12000, 2000)), nominal_region_size, tile_size, overlap) + + # Verify the number of computed regions assert len(regions) == 2 + + # Verify that all computed regions match expected results for r in regions: assert r in [((0, 0), (10000, 2000)), ((0, 9900), (2100, 2000))] def test_compute_tiles(self): + """ + Test that tiles are correctly computed within specified regions based on tile size and overlap. + """ from aws.osml.model_runner.tile_worker import VariableTileTilingStrategy tiling_strategy = VariableTileTilingStrategy() overlap = (100, 100) tile_size = (4096, 4096) - # First full region + # Test full region tiling tiles = tiling_strategy.compute_tiles(((0, 0), (10000, 10000)), tile_size, overlap) assert len(tiles) == 9 for t in tiles: @@ -83,7 +116,7 @@ def test_compute_tiles(self): ((7992, 7992), (2008, 2008)), ] - # A region on the right edge of the image + # Test region on the right edge of the image tiles = tiling_strategy.compute_tiles(((0, 19800), (5200, 10000)), tile_size, overlap) assert len(tiles) == 6 for t in tiles: @@ -96,31 +129,39 @@ def test_compute_tiles(self): ((7992, 23796), (1204, 2008)), ] - # A region on the bottom edge of the image + # Test region on the bottom edge of the image tiles = tiling_strategy.compute_tiles(((9900, 9900), (10000, 2100)), tile_size, overlap) assert len(tiles) == 3 for t in tiles: assert t in [((9900, 9900), (4096, 2100)), ((9900, 13896), (4096, 2100)), ((9900, 17892), (2008, 2100))] - # The bottom right corner region + # Test bottom right corner region tiles = tiling_strategy.compute_tiles(((9900, 19800), (5200, 2100)), tile_size, overlap) assert len(tiles) == 2 for t in tiles: assert t in [((9900, 19800), (4096, 2100)), ((9900, 23796), (1204, 2100))] def test_compute_tiles_tiny_region(self): + """ + Test that tiles are correctly computed for a small region that is smaller than the tile size. + """ from aws.osml.model_runner.tile_worker import VariableTileTilingStrategy tiling_strategy = VariableTileTilingStrategy() overlap = (100, 100) tile_size = (4096, 4096) - # First full region + # Compute tiles for a small region tiles = tiling_strategy.compute_tiles(((10, 50), (1024, 2048)), tile_size, overlap) + + # Verify that only a single tile is produced for the tiny region assert len(tiles) == 1 assert tiles[0] == ((10, 50), (1024, 2048)) def test_deconflict_features(self): + """ + Test that duplicate features are properly deconflicted based on specified rules. + """ from geojson import Feature from aws.osml.model_runner.inference import FeatureSelector @@ -128,30 +169,28 @@ def test_deconflict_features(self): tiling_strategy = VariableTileTilingStrategy() + # Define image and tiling parameters full_image_size = (25000, 12000) full_image_region = ((0, 0), full_image_size) nominal_region_size = (10000, 10000) overlap = (100, 100) tile_size = (4096, 4096) + # Sample feature inputs, including duplicates features = [ - # Duplicate features in overlap of two regions, keep 1 Feature(properties={"imageBBox": [19804, 9904, 19824, 9924]}), Feature(properties={"imageBBox": [19805, 9905, 19825, 9925]}), - # Duplicate features in overlap of two tiles in lower edge region, keep 1 Feature(properties={"imageBBox": [13900, 11000, 17510, 13910]}), Feature(properties={"imageBBox": [13900, 11000, 17510, 13910]}), - # Duplicate features in overlap of tiles in center of first region, keep 1 Feature(properties={"imageBBox": [4000, 4000, 4010, 4010]}), Feature(properties={"imageBBox": [4000, 4000, 4010, 4010]}), - # Duplicate features in overlap of tiles in center of second region, keep 1 Feature(properties={"imageBBox": [16000, 4000, 16010, 4010]}), Feature(properties={"imageBBox": [16000, 4000, 16010, 4010]}), - # Features duplicate but do not touch overlap regions, keep 2 Feature(properties={"imageBBox": [10, 10, 10, 10]}), Feature(properties={"imageBBox": [10, 10, 10, 10]}), ] + # Mock feature selector to deconflict overlapping features class DummyFeatureSelector(FeatureSelector): def select_features(self, features): if len(features) > 0: @@ -159,15 +198,16 @@ def select_features(self, features): return [] mock_feature_selector = Mock(wraps=DummyFeatureSelector()) + + # Deconflict features using the tiling strategy deduped_features = tiling_strategy.cleanup_duplicate_features( full_image_region, nominal_region_size, tile_size, overlap, features, mock_feature_selector ) - print(deduped_features) - - # Check to ensure we have the correct number of features returned and that the feature selector - # was called once for each overlapping group + # Verify the correct number of deconflicted features assert len(deduped_features) == 6 + + # Verify that the feature selector was called for each overlapping group assert len(mock_feature_selector.method_calls) == 4