Mosaic Geometry

Georeferenced EO data sets are often too huge to be stored as one single file and are therefore often tiled and provided in a gridded structure. Geometry-wise, such a structure was realised within the MosaicGeometry classes, which represent a well-defined collection of Tile objects, which are a light wrapper around a RasterGeometry. The basic MosaicGeometry sets the following requirements to its tiles:

  • a tile is represented by a RasterGeometry object, so only rectangular shapes are allowed

  • all tiles must have the same CRS

  • tiles are not allowed to intersect with each other

The neighbourhood relationship is stored in an adjacency matrix (https://en.wikipedia.org/wiki/Adjacency_matrix), which allows performant spatial queries beyond tile boundaries.

Tile

A Tile object is a slim wrapper around a RasterGeometry object. It stores some further attributes, which are relevant for interacting with a mosaic:

  • active: Flag indicating if a tile is “visible”/active or not.

  • mosaic_topology: A string defining the relation with the mosaic boundary. It can be “INNER”, “OUTER”, or “BOUNDARY”. If it is None, then this indicates that the tile is decoupled from the original mosaic, e.g. after an intersection with a geometry.

  • metadata: A dictionary storing some additional metadata information.

  • mask: This parameter is a place-holder for a pixel mask indicating valid and non-valid pixel coordinates. This parameter is internally used when accessing tiles through the mosaic containing labelled pixels depending on the relation with the mosaic boundary.

Initialisation

The constructor of MosaicGeometry expects the following arguments:

  • tiles: List of Tile (or other child classes) instances.

  • boundary: A polygon strictly defining the valid area of the mosaic, i.e. in which coordinates are allowed to exist. In the default case, the cascaded union of all tiles defines the boundary of the mosaic.

  • adjacency_matrix: A quadratic adjacency matrix defining the direct neighbourhood relationship. It should only contain booleans or ones/zeros.

  • name (optional): Name of the mosaic

  • description (optional): Verbal description of the raster geometry.

  • check_consistency (optional): If true, the requirements listed above are checked.

[2]:
from geospade.crs import SpatialRef
from geospade.raster import Tile
from geospade.raster import MosaicGeometry

# define spatial reference system
sref = SpatialRef(4326)
# define the tiles of the mosaic
n_rows = 50
n_cols = 50
geotrans = (5, 0.2, 0, 50, 0, -0.2)
tile_1 = Tile(n_rows, n_cols, sref, geotrans=geotrans, name="Tile 1", metadata={'test': True})
n_rows = 50
n_cols = 50
geotrans = (15, 0.2, 0, 50, 0, -0.2)
tile_2 = Tile(n_rows, n_cols, sref, geotrans=geotrans, name="Tile 2", active=False, metadata={'test': True})
n_rows = 50
n_cols = 50
geotrans = (25, 0.2, 0, 50, 0, -0.2)
tile_3 = Tile(n_rows, n_cols, sref, geotrans=geotrans, name="Tile 3", metadata={'test': False})
n_rows = 50
n_cols = 75
geotrans = (5, 0.2, 0, 40, 0, -0.2)
tile_4 = Tile(n_rows, n_cols, sref, geotrans=geotrans, name="Tile 4", metadata={'test': False})
n_rows = 50
n_cols = 75
geotrans = (20, 0.2, 0, 40, 0, -0.2)
tile_5 = Tile(n_rows, n_cols, sref, geotrans=geotrans, name="Tile 5", metadata={'test': False})

tiles = [tile_1, tile_2, tile_3, tile_4, tile_5]

# define other attributes of the mosaic
mosaic_name = "Mosaic 1"
mosaic_geom = MosaicGeometry(tiles, name=mosaic_name)

This is not the only way to create a mosaic geometry. Similar as for a raster geometry, you can create a mosaic geometry from a JSON-like definition, which contains the adjacency matrix and the mosaic properties in addition to the list of tiles:

[3]:
import json
json_str = """
{
    "name": "Mosaic 1",
    "description": "",
    "boundary": "POLYGON ((5 30,5 40,5 50,15 50,25 50,35 50,35 40,35 30,20 30,5 30))",
    "tiles": {
        "0": {
            "name": "Tile 1",
            "active": "true",
            "mosaic_topology": "INNER",
            "metadata": {},
            "number_of_rows": 50,
            "number_of_columns": 50,
            "spatial_reference": {
                "proj": "longlat",
                "datum": "WGS84",
                "no_defs": true
            },
            "geotransformation": [
                5,
                0.2,
                0,
                50,
                0,
                -0.2
            ],
            "pixel_origin": "ul",
            "description": ""
        },
        "1": {
            "name": "Tile 2",
            "active": "false",
            "mosaic_topology": "INNER",
            "metadata": {},
            "number_of_rows": 50,
            "number_of_columns": 50,
            "spatial_reference": {
                "proj": "longlat",
                "datum": "WGS84",
                "no_defs": true
            },
            "geotransformation": [
                15,
                0.2,
                0,
                50,
                0,
                -0.2
            ],
            "pixel_origin": "ul",
            "description": ""
        },
        "2": {
            "name": "Tile 3",
            "active": "true",
            "mosaic_topology": "INNER",
            "metadata": {},
            "number_of_rows": 50,
            "number_of_columns": 50,
            "spatial_reference": {
                "proj": "longlat",
                "datum": "WGS84",
                "no_defs": true
            },
            "geotransformation": [
                25,
                0.2,
                0,
                50,
                0,
                -0.2
            ],
            "pixel_origin": "ul",
            "description": ""
        },
        "3": {
            "name": "Tile 4",
            "active": "true",
            "mosaic_topology": "INNER",
            "metadata": {},
            "number_of_rows": 50,
            "number_of_columns": 75,
            "spatial_reference": {
                "proj": "longlat",
                "datum": "WGS84",
                "no_defs": true
            },
            "geotransformation": [
                5,
                0.2,
                0,
                40,
                0,
                -0.2
            ],
            "pixel_origin": "ul",
            "description": ""
        },
        "4": {
            "name": "Tile 5",
            "active": "true",
            "mosaic_topology": "INNER",
            "metadata": {},
            "number_of_rows": 50,
            "number_of_columns": 75,
            "spatial_reference": {
                "proj": "longlat",
                "datum": "WGS84",
                "no_defs": true
            },
            "geotransformation": [
                20,
                0.2,
                0,
                40,
                0,
                -0.2
            ],
            "pixel_origin": "ul",
            "description": ""
        }
    },
    "type": "irregular",
    "adjacency_matrix": [
        [
            false,
            true,
            false,
            true,
            false
        ],
        [
            true,
            false,
            true,
            true,
            true
        ],
        [
            false,
            true,
            false,
            false,
            true
        ],
        [
            true,
            true,
            false,
            false,
            true
        ],
        [
            false,
            true,
            true,
            true,
            false
        ]
    ]
}
"""
definition = json.loads(json_str)
mosaic_geom_def = MosaicGeometry.from_definition(definition)

Properties

A mosaic geometry shares some common attributes with a raster geometry:

[4]:
mosaic_geom.sref
[4]:
<geospade.crs.SpatialRef at 0x12bacdc0550>
[5]:
mosaic_geom.ori
[5]:
-0.0

In addition, you can access the tiles via

[6]:
mosaic_geom.tiles
[6]:
[<geospade.raster.Tile at 0x12bab7f4d68>,
 <geospade.raster.Tile at 0x12bb20b9080>,
 <geospade.raster.Tile at 0x12bb20b9048>,
 <geospade.raster.Tile at 0x12bb20b90b8>]

, and the tile names via

[7]:
mosaic_geom.tile_names
[7]:
['Tile 1', 'Tile 3', 'Tile 4', 'Tile 5']

Both properties allow access to the tiles set to active=True. If you want to retrieve all tiles disregarding their visibility status, use:

[8]:
mosaic_geom.all_tiles
[8]:
[<geospade.raster.Tile at 0x12bab7f4d68>,
 <geospade.raster.Tile at 0x12bb20a6f60>,
 <geospade.raster.Tile at 0x12bb20b9080>,
 <geospade.raster.Tile at 0x12bb20b9048>,
 <geospade.raster.Tile at 0x12bb20b90b8>]
[9]:
mosaic_geom.all_tile_names
[9]:
['Tile 1', 'Tile 2', 'Tile 3', 'Tile 4', 'Tile 5']

Plotting

A mosaic geometry offers a simple wrapper to plot each tile, with almost the same keywords as for the plot() function of a raster geometry. In addition, you can choose if you want to only show active tiles (active_only=True, default) and to plot the mosaic boundary (plot_boundary=True, default).

[10]:
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 12))
mosaic_geom.plot(label_tiles=True, alpha=0.8, extent=[0, 25, 40, 55], plot_boundary=True, active_only=True)
[10]:
<cartopy.mpl.geoaxes.GeoAxesSubplot at 0x12bb20b9da0>
../_images/notebooks_mosaic_geometry_16_1.png

Tile Access

There are several options to access a tile within a mosaic geometry. First you can use brackets with a tile name to get a tile object from a mosaic.

[11]:
tile_by_brkts = mosaic_geom['Tile 1']

If you want to know which tile is covering a coordinate tuple, you can use

[12]:
tile_by_coords = mosaic_geom.xy2tile(5.25, 49.6)

Both tiles should be the same:

[13]:
tile_by_brkts == tile_by_coords
[13]:
True

There is also the possibility to get the direct neighbours of one tile:

[14]:
mosaic_geom.get_neighbouring_tiles('Tile 1')
[14]:
{'Tile 4': <geospade.raster.Tile at 0x12bb20b9048>}

As you can see above, all tile access methods (except the [] access with a tile name) return a dictionary with the tile names as keys and the tile objects as values.

You can also select tiles by specifying a geometry, which can be intersected with the mosaic as follows:

[15]:
from osgeo import osr
from osgeo import ogr
from shapely.geometry import Polygon

polygon = Polygon([(10, 47), (17, 41), (15, 35), (7, 33), (10, 47)])
ogr_geom = ogr.CreateGeometryFromWkt(polygon.wkt)
ogr_geom.AssignSpatialReference(sref.osr_sref)

sel_tiles = mosaic_geom.select_tiles_by_geom(ogr_geom, active_only=True, apply_mask=True)
sel_tiles
[15]:
{'Tile 1': <geospade.raster.Tile at 0x12bab7f4d68>,
 'Tile 4': <geospade.raster.Tile at 0x12bb20b9048>}

Besides specifying if only active tiles should be considered (active_only=True, default), you can set apply_mask, which generates valid pixel mask for each tile (apply_mask=True, default).

[16]:
sel_tiles['Tile 1'].mask
[16]:
array([[1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       ...,
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.]])

A very similar function goes one step further, and intersects the tiles with a given geometry, thus decoupling them from the mosaic.

[17]:
intsct_tiles = mosaic_geom.slice_tiles_by_geom(ogr_geom, active_only=True)
intsct_tiles
[17]:
{0: <geospade.raster.Tile at 0x12bb307b5f8>,
 1: <geospade.raster.Tile at 0x12bb307b668>}
[18]:
x_coords, y_coords = polygon.exterior.xy
plt.figure(figsize=(12, 12))
ax = mosaic_geom.plot(label_tiles=True, alpha=0.8, extent=[0, 25, 40, 55], plot_boundary=True, active_only=True)
ax = intsct_tiles[0].plot(ax, edgewidth=4, alpha=0.8, facecolor='green')
ax = intsct_tiles[1].plot(ax, edgewidth=4, alpha=0.8, facecolor='green')
ax.plot(x_coords, y_coords, color='orange', linewidth=4)
[18]:
[<matplotlib.lines.Line2D at 0x12bb2d6ba20>]
../_images/notebooks_mosaic_geometry_31_1.png

Subsetting

Instead of retrieving single tiles from a spatial query, you can also change the active tiles in a mosaic. This can be done very similar as before, i.e. by intersecting the original mosaic geometry with another geometry.

[19]:
mosaic_geom.slice_by_geom(ogr_geom)

plt.figure(figsize=(12, 12))
ax = mosaic_geom.plot(label_tiles=True, edgewidth=1, alpha=0.8, extent=[0, 25, 40, 55])
ax.plot(x_coords, y_coords, color='orange', linewidth=4)
[19]:
[<matplotlib.lines.Line2D at 0x12bb2da5e48>]
../_images/notebooks_mosaic_geometry_33_1.png

Another option is to filter the tiles by metadata entries, e.g. we want to only work with tiles, where the metadata attribute 'test' equals to True.

[20]:
mosaic_geom.filter_tile_metadata({'test': True})

plt.figure(figsize=(12, 12))
mosaic_geom.plot(label_tiles=True, edgewidth=1, alpha=0.8, extent=[0, 25, 40, 55])
[20]:
<cartopy.mpl.geoaxes.GeoAxesSubplot at 0x12bb2f85da0>
../_images/notebooks_mosaic_geometry_35_1.png

Regular Mosaic Geometry

With respect to MosaicGeometry, RegularMosaicGeometry sets some further requirements to its tiles:

  • same pixel size

  • same shape

This structure paves the path for more performant spatial queries and solving neighbourhood relationships. Such relationships are also encoded in an adjacency matrix, but this time not in a binary format. The dimensions of the matrix refer to the dimensions of the mosaic (e.g. number of tiles in S and W direction) and its entries to a tile name.

An additional classmethod from_rectangular_definition can help to create a regular mosaic geometry. In the following example we also make use of the boundary parameter (same for MosaicGeometry) to exemplify its interaction with the tiles.

[21]:
from geospade.raster import RegularMosaicGeometry

mosaic_ul_x = 5
mosaic_ul_y = 50
mosaic_rows = 2
mosaic_cols = 3
x_tile_size = 10.
y_tile_size = 10.
geotrans = (mosaic_ul_x, 0.2, 0., mosaic_ul_y, 0., -0.2)
mosaic_boundary = Polygon(((14, 41), (21, 48), (31, 46), (27, 32), (5, 31), (14, 41)))
mosaic_boundary = ogr.CreateGeometryFromWkt(mosaic_boundary.wkt)
mosaic_boundary.AssignSpatialReference(sref.osr_sref)

reg_mosaic_geom = RegularMosaicGeometry.from_rectangular_definition(mosaic_rows, mosaic_cols, x_tile_size,
                                                                    y_tile_size, sref,
                                                                    boundary=mosaic_boundary,
                                                                    geotrans=geotrans)

plt.figure(figsize=(12, 12))
reg_mosaic_geom.plot(label_tiles=True, edgewidth=1, alpha=0.8, extent=[0, 25, 40, 55])
[21]:
<cartopy.mpl.geoaxes.GeoAxesSubplot at 0x12bb3669518>
../_images/notebooks_mosaic_geometry_37_1.png

A regular mosaic geometry has the same properties as a basic mosaic geometry, except an additional shape property.

[22]:
reg_mosaic_geom.shape
[22]:
(2, 3)

None of the tiles is now fully inside or outside the mosaic, i.e. they all share the relation “BOUNDARY”.

[23]:
reg_mosaic_geom['S001W001'].mosaic_topology
[23]:
'BOUNDARY'

This means, if we access a tile by specifying coordinates being outside the mosaic boundary, we get None as a return value:

[24]:
reg_mosaic_geom.xy2tile(6, 50)

whereas for coordinates being inside we get

[25]:
reg_mosaic_geom.xy2tile(14, 41).name
[25]:
'S000W000'

Moreover, the masks of the “BOUNDARY” tiles now directly represent the mosaic boundary at the specified mosaic pixel sampling:

[26]:
reg_mosaic_geom['S001W001'].mask
[26]:
array([[1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
[27]:
plt.figure(figsize=(12, 12))
ax = reg_mosaic_geom.plot(label_tiles=True, edgewidth=1, alpha=0.8, extent=[0, 25, 40, 55])
for tile_name in reg_mosaic_geom.tile_names:
    tile = reg_mosaic_geom[tile_name]
    outer_boundary_extent = tile.outer_boundary_extent
    extent = (outer_boundary_extent[0], outer_boundary_extent[2], outer_boundary_extent[3], outer_boundary_extent[1])
    ax.imshow(tile.mask, extent=extent)
../_images/notebooks_mosaic_geometry_48_0.png