Skip to content

Integration of TileJSON with MicroJSON

Purpose

This specification outlines how to use TileJSON to integrate tiled MicroJSON data, both in json form as well as binary form. It provides examples of how TileJSON can be used to specify the tiling scheme and zoom levels for MicroJSON data and its binary equievalent. It is based on the TileJSON 3.0.0 specification, but extends it by recommending additional properties to enable integration of MicroJSON data and fit purposes of microscopy imaging. The recommendations provided here are not intrinsic to the original TileJSON specification but have been tailored to suit the needs of microscopy metadata annotation and integration with MicroJSON. However, all suggestions are designed to maintain compatibility with the original TileJSON specification.

Background of TileJSON

TileJSON is a widely-used format in mapping applications for specifying tilesets. Developed to streamline the integration of different map layers, TileJSON is essential for ensuring consistency across mapping platforms. It describes tilesets through a JSON object, detailing properties like tile URLs, zoom levels, and spatial coverage.

TileJSON for MicroJSON Object Structure

  • tilejson: Specifies the version of the TileJSON spec being used. Required for all TileJSON objects.
  • name: The name of the tileset. Optional but recommended.
  • description: Provide a brief description of the tileset. Optional but recommended.
  • version: The version of the tileset. Optional but recommended.
  • attribution: A link to the data source or other attribution information, e.g. organisational origin. Optional but recommended.
  • tiles: Required. The URL pattern for accessing the vector tiles. The urlbase/{zlvl}/{t}/{c}/{z}/{x}/{y} is the recommended default naming pattern for the tiles, in this order, where urlbase is the base URL (e.g. http://example.com/tiles), {zlvl} is the zoom level, {t} is the tileset timestamp, {c} is the channel, {z} is the z coordinate, and {x} and {y} are the x and y coordinates, respectively. If not using a timestamp, channel, or z coordinate, these can be omitted. The zoom level should always be first.
  • minzoom and maxzoom: Defines the range of zoom levels for which the tiles are available.
  • bounds: Optional. Specifies the geometrical bounds included in the tileset. Specified as an array of minimum four numbers in the order [minX, minY, maxX, maxY], but may include up to a further six numbers for a total of ten, [minT, minC, minZ, minX, minY, maxT, maxC, maxZ, maxX, maxY], where minT is the minimum tileset timestamp, minC is the minimum channel, minZ is the minimum z coordinate, minX and minY are the minimum x and y coordinates, maxT is the maximum tileset timestamp, maxC is the maximum channel, maxZ is the maximum z coordinate, and maxX and maxY are the maximum x and y coordinates.
  • center: Optional. Indicates the center and suggested default view of the tileset. Minimum of three numbers in the order [x, y, zoom], but may include up to a further three numbers for a total of six, [t,c,z,x,y,zoom], where t is the tileset timestamp, c is the channel, z is the z coordinate, x and y are the x and y coordinates, and zoom is the zoom level. Zoom level should be last.
  • vector_layers: Required. Describes each layer within the vector tiles, and has the following structure:

  • id: Required. A unique identifier for the layer. Required for each layer.

  • fields: Required. A list of fields (attributes) and their data types. For MicroJSON, this can either be an empty list, or a simple datatype indicator, that is either of String, Number, or Bool. Complex data types, such as arrays or objects are not allowed. Required for each layer.
  • fieldranges: Optional. A dictionary of field names and their ranges. For example, {"label": [0,100], "channel": [0,10]}. Optional.
  • fieldenums: Optional. A dictionary of field names and their possible values. For example, {"plate": ["A1", "A2", "B1", "B2"], "image": ["image1.tif", "image2.tif", "image3.tif"]}. Optional.
  • fielddescriptions: Optional. A dictionary of field names and their descriptions. For example, {"plate": "Well plate identifier", "image": "Image filename", "label": "Label identifier", "channel": "Channel identifier"}. Optional.
  • description: Optional. A brief description of the layer.
  • minzoom and maxzoom: Optional. The range of zoom levels at which the layer is visible.
  • fillzoom: Optional. An integer specifying the zoom level from which to generate overzoomed tiles.
  • legend: Optional. Contains a legend to be displayed with the tileset.

The following fields of TileJSON may be used if the use case requires it, and are included here for completeness:

  • scheme: The tiling scheme of the tileset.
  • grids: The URL pattern for accessing grid data.
  • data: Optional. The URL pattern for accessing data. Used for GeoJSON originally, which in this specification is replaced by MicroJSON and used in the tiles field.
  • template: Optional. Contains a mustache template to be used to format data from grids for interaction.

Pydantic Model for TileJSON for MicroJSON

TileJSON

Bases: RootModel

The root object of a TileJSON file.

Source code in src/microjson/tilemodel.py
78
79
80
class TileJSON(RootModel):
    """ The root object of a TileJSON file."""
    root: TileModel

TileModel

Bases: BaseModel

A TileJSON object.

Parameters:

Name Type Description Default
tilejson str

The TileJSON version.

required
tiles List[Union[Path, AnyUrl]]

The list of tile URLs.

required
name Optional[str]

The name of the tileset.

required
description Optional[str]

The description of the tileset.

required
version Optional[str]

The version of the tileset.

required
attribution Optional[str]

The attribution of the tileset.

required
template Optional[str]

The template of the tileset.

required
legend Optional[str]

The legend of the tileset.

required
scheme Optional[str]

The scheme of the tileset.

required
grids Optional[Union[Path, AnyUrl]]

The grids of the tileset.

required
data Optional[Union[Path, AnyUrl]]

The data of the tileset.

required
minzoom Optional[int]

The minimum zoom level of the tileset.

required
maxzoom Optional[int]

The maximum zoom level of the tileset.

required
bounds Optional[conlist(float, min_length=4, max_length=10)]

The bounds of the tileset.

required
center Optional[conlist(float, min_length=3, max_length=6)]

The center of the tileset.

required
fillzoom Optional[int]

The fill zoom level of the tileset.

required
vector_layers List[TileLayer]

The vector layers of the tileset.

required
Source code in src/microjson/tilemodel.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class TileModel(BaseModel):
    """ A TileJSON object.

    Args:
        tilejson (str): The TileJSON version.
        tiles (List[Union[Path, AnyUrl]]): The list of tile URLs.
        name (Optional[str]): The name of the tileset.
        description (Optional[str]): The description of the tileset.
        version (Optional[str]): The version of the tileset.
        attribution (Optional[str]): The attribution of the tileset.
        template (Optional[str]): The template of the tileset.
        legend (Optional[str]): The legend of the tileset.
        scheme (Optional[str]): The scheme of the tileset.
        grids (Optional[Union[Path, AnyUrl]]): The grids of the tileset.
        data (Optional[Union[Path, AnyUrl]]): The data of the tileset.
        minzoom (Optional[int]): The minimum zoom level of the tileset.
        maxzoom (Optional[int]): The maximum zoom level of the tileset.
        bounds (Optional[conlist(float, min_length=4, max_length=10)]): The bounds of the tileset.
        center (Optional[conlist(float, min_length=3, max_length=6)]): The center of the tileset.
        fillzoom (Optional[int]): The fill zoom level of the tileset.
        vector_layers (List[TileLayer]): The vector layers of the tileset.

    """

    tilejson: str
    tiles: List[Union[Path, AnyUrl]]
    name: Optional[str] = None
    description: Optional[str] = None
    version: Optional[str] = None
    attribution: Optional[str] = None
    template: Optional[str] = None
    legend: Optional[str] = None
    scheme: Optional[str] = None
    grids: Optional[Union[Path, AnyUrl]] = None
    data: Optional[Union[Path, AnyUrl]] = None
    minzoom: Optional[int] = 0
    maxzoom: Optional[int] = 22
    bounds: Optional[conlist(  # type: ignore
        float,
        min_length=4,
        max_length=10)] = None
    center: Optional[conlist(  # type: ignore
        float,
        min_length=3,
        max_length=6)] = None
    fillzoom: Optional[int] = None
    vector_layers: List[TileLayer]

TileLayer

Bases: BaseModel

A vector layer in a TileJSON file.

Parameters:

Name Type Description Default
id str

The unique identifier for the layer.

required
fields Union[None, Dict[str, str]]

The fields in the layer.

required
minzoom Optional[int]

The minimum zoom level for the layer.

required
maxzoom Optional[int]

The maximum zoom level for the layer.

required
description Optional[str]

A description of the layer.

required
fieldranges Optional[Dict[str, List[Union[int, float, str]]]]

The ranges of the fields.

required
fieldenums Optional[Dict[str, List[str]]

The enums of the fields.

required
fielddescriptions Optional[Dict[str, str]]

The descriptions of the fields.

required
Source code in src/microjson/tilemodel.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class TileLayer(BaseModel):
    """ A vector layer in a TileJSON file.

    Args:
        id (str): The unique identifier for the layer.
        fields (Union[None, Dict[str, str]]): The fields in the layer.
        minzoom (Optional[int]): The minimum zoom level for the layer.
        maxzoom (Optional[int]): The maximum zoom level for the layer.
        description (Optional[str]): A description of the layer.
        fieldranges (Optional[Dict[str, List[Union[int, float, str]]]]): The ranges of the fields.
        fieldenums (Optional[Dict[str, List[str]]): The enums of the fields.
        fielddescriptions (Optional[Dict[str, str]]): The descriptions of the fields.
    """
    id: str
    fields: Union[None, Dict[str, str]] = None
    minzoom: Optional[int] = 0
    maxzoom: Optional[int] = 22
    description: Optional[str] = None
    fieldranges: Optional[Dict[str, List[Union[int, float, str]]]] = None
    fieldenums: Optional[Dict[str, List[str]]] = None
    fielddescriptions: Optional[Dict[str, str]] = None

General tiling requirements

This specification is designed to be compatible with the Vector Tile Specification and the TileJSON 3.0.0 specification. The Vector Tile Specification specifically requires vector tiles to be agnostic of the global coordinate system, and thus followingly each tile has a relative coordinate system, which instead is defined in the TileJSON. Our ambitions in general are to follow the same principles.

One difference is that we here recommend that the file ending for binary tiles is .pbf instead of .mvt to avoid confusion with the Mapbox Vector Tile format. The binary tiles should be encoded in the Protobuf format as defined in the Vector Tile Specification.

MicroJSON2vt

The MicroJSON2vt module is a helper module that can be used to convert MicroJSON objects to vector tiles. It is designed to be used in conjunction with the TileJSON for MicroJSON specification, and can be used to generate vector tiles from MicroJSON objects. The module is designed to be compatible with the Vector Tile Specification, and can be used to generate vector tiles in the intermediate vector tile JSON-format, which then, using vt2pbf may be transformed into protobuf. The module is included in the microjson package, and its wrapper function can be imported using the following code:

from microjson import microjson2vt

The module:

MicroJsonVt class, which is the main class for generating vector tiles from MicroJSON data

Source code in src/microjson/microjson2vt/microjson2vt.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
class MicroJsonVt:
    """
    MicroJsonVt class, which is the main class for generating vector tiles
    from MicroJSON data
    """
    def __init__(self, data, options, log_level=logging.INFO):
        """
        Constructor for MicroJsonVt class

        Args:
            data (dict): The data to be converted to vector tiles
            options (dict): The options to be used for generating vector tiles
            log_level (int): The logging level to be used
        """
        logging.basicConfig(
            level=log_level, format='%(asctime)s %(levelname)s %(message)s')
        options = self.options = extend(get_default_options(), options)

        logging.debug('preprocess data start')

        if options.get('maxZoom') < 0 or options.get('maxZoom') > 24:
            raise Exception('maxZoom should be in the 0-24 range')
        if options.get(
            'promoteId', None) is not None and options.get(
                'generateId', False):
            raise Exception(
                'promoteId and generateId cannot be used together.')

        # projects and adds simplification info
        # Create a new instance of a CartesianProjector

        features = convert(data, options)

        # tiles and tile_coords are part of the public API
        self.tiles = {}
        self.tile_coords = []

        logging.debug('preprocess data end')
        logging.debug(
            f'index: maxZoom: {options.get("indexMaxZoom")}, maxPoints: {options.get("indexMaxPoints")}')
        self.stats = {}
        self.total = 0

        # wraps features (ie extreme west and extreme east)
        # features = wrap(features, options)

        # start slicing from the top tile down
        if len(features) > 0:
            self.split_tile(features, 0, 0, 0)

        if len(features) > 0:
            logging.debug(
                f'features: {self.tiles[0].get("numFeatures")}, points: {self.tiles[0].get("numPoints")}')
            stop = datetime.now()
        logging.debug(f'generate tiles end: {stop}')
        logging.debug('tiles generated:', self.total, self.stats)

    # splits features from a parent tile to sub-tiles.
    # z, x, and y are the coordinates of the parent tile
    # cz, cx, and cy are the coordinates of the target tile
    #
    # If no target tile is specified, splitting stops when we reach the maximum
    # zoom or the number of points is low as specified in the options.

    def split_tile(self, features, z, x, y, cz=None, cx=None, cy=None):
        """
        Splits features from a parent tile to sub-tiles.

        Args:
            features (list): The features to be split
            z (int): The zoom level of the parent tile
            x (int): The x coordinate of the parent tile
            y (int): The y coordinate of the parent tile
            cz (int): The zoom level of the target tile
            cx (int): The x coordinate of the target tile
            cy (int): The y coordinate of the target tile
        """
        stack = [features, z, x, y]
        options = self.options
        # avoid recursion by using a processing queue
        while len(stack) > 0:
            y = stack.pop()
            x = stack.pop()
            z = stack.pop()
            features = stack.pop()

            z2 = 1 << z
            id_ = to_Id(z, x, y)
            tile = self.tiles.get(id_, None)

            if tile is None:
                logging.debug('creation start')

                self.tiles[id_] = create_tile(features, z, x, y, options)
                tile = self.tiles[id_]
                self.tile_coords.append({'z': z, 'x': x, 'y': y})

                logging.debug(
                    f'tile z{z}-{x}-{y} (features: {tile.get("numFeatures")}, points: {tile.get("numPoints")}, simplified: {tile.get("numSimplified")})')
                logging.debug('creation end')
                key = f'z{z}'
                self.stats[key] = self.stats.get(key, 0) + 1
                self.total += 1

            # save reference to original geometry in tile so that we can drill
            # down later if we stop now
            tile['source'] = features

            # if it's the first-pass tiling
            if cz is None:
                # stop tiling if we reached max zoom, or if the tile is too
                # simple
                if z == options.get(
                    'indexMaxZoom') or tile.get(
                        'numPoints') <= options.get('indexMaxPoints'):
                    continue  # if a drilldown to a specific tile
            elif z == options.get('maxZoom') or z == cz:
                # stop tiling if we reached base zoom or our target tile zoom
                continue
            elif cz is not None:
                # stop tiling if it's not an ancestor of the target tile
                zoomSteps = cz - z
                if x != (cx >> zoomSteps) or y != (cy >> zoomSteps):
                    continue

            # if we slice further down, no need to keep source geometry
            tile['source'] = None

            if not features or len(features) == 0:
                continue

            logging.debug('clipping start')

            # values we'll use for clipping
            k1 = 0.5 * options.get('buffer') / options.get('extent')
            k2 = 0.5 - k1
            k3 = 0.5 + k1
            k4 = 1 + k1

            tl = None
            bl = None
            tr = None
            br = None

            left = clip(features, z2, x - k1, x + k3, 0,
                        tile['minX'], tile['maxX'], options)
            right = clip(features, z2, x + k2, x + k4, 0,
                         tile['minX'], tile['maxX'], options)
            features = None

            if left is not None:
                tl = clip(left, z2, y - k1, y + k3, 1,
                          tile['minY'], tile['maxY'], options)
                bl = clip(left, z2, y + k2, y + k4, 1,
                          tile['minY'], tile['maxY'], options)
                left = None

            if right is not None:
                tr = clip(right, z2, y - k1, y + k3, 1,
                          tile['minY'], tile['maxY'], options)
                br = clip(right, z2, y + k2, y + k4, 1,
                          tile['minY'], tile['maxY'], options)
                right = None

            logging.debug('clipping ended')

            stack.append(tl if tl is not None else [])
            stack.append(z + 1)
            stack.append(x * 2)
            stack.append(y * 2)

            stack.append(bl if bl is not None else [])
            stack.append(z + 1)
            stack.append(x * 2)
            stack.append(y * 2 + 1)

            stack.append(tr if tr is not None else [])
            stack.append(z + 1)
            stack.append(x * 2 + 1)
            stack.append(y * 2)

            stack.append(br if br is not None else [])
            stack.append(z + 1)
            stack.append(x * 2 + 1)
            stack.append(y * 2 + 1)

    def get_tile(self, z, x, y):
        z = int(z)
        x = int(x)
        y = int(y)

        options = self.options
        extent = options.get('extent')

        if z < 0 or z > 24:
            return None

        z2 = 1 << z
        x = (x + z2) & (z2 - 1)  # wrap tile x coordinate

        id_ = to_Id(z, x, y)
        current_tile = self.tiles.get(id_, None)
        if current_tile is not None:
            return transform_tile(self.tiles[id_], extent)

        logging.debug(f'drilling down to z{z}-{x}-{y}')

        z0 = z
        x0 = x
        y0 = y
        parent = None

        while parent is None and z0 > 0:
            z0 -= 1
            x0 = x0 >> 1
            y0 = y0 >> 1
            parent = self.tiles.get(to_Id(z0, x0, y0), None)

        if parent is None or parent.get('source', None) is None:
            return None

        # if we found a parent tile containing the original geometry, we can 
        # drill down from it
        logging.debug(f'found parent tile z{z0}-{x0}-{y0}')
        logging.debug('drilling down start')

        self.split_tile(parent.get('source'), z0, x0, y0, z, x, y)

        logging.debug('drilling down end')

        transformed = transform_tile(
            self.tiles[id_], extent) if self.tiles.get(
                id_, None) is not None else None
        return transformed

__init__(data, options, log_level=logging.INFO)

Constructor for MicroJsonVt class

Parameters:

Name Type Description Default
data dict

The data to be converted to vector tiles

required
options dict

The options to be used for generating vector tiles

required
log_level int

The logging level to be used

INFO
Source code in src/microjson/microjson2vt/microjson2vt.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(self, data, options, log_level=logging.INFO):
    """
    Constructor for MicroJsonVt class

    Args:
        data (dict): The data to be converted to vector tiles
        options (dict): The options to be used for generating vector tiles
        log_level (int): The logging level to be used
    """
    logging.basicConfig(
        level=log_level, format='%(asctime)s %(levelname)s %(message)s')
    options = self.options = extend(get_default_options(), options)

    logging.debug('preprocess data start')

    if options.get('maxZoom') < 0 or options.get('maxZoom') > 24:
        raise Exception('maxZoom should be in the 0-24 range')
    if options.get(
        'promoteId', None) is not None and options.get(
            'generateId', False):
        raise Exception(
            'promoteId and generateId cannot be used together.')

    # projects and adds simplification info
    # Create a new instance of a CartesianProjector

    features = convert(data, options)

    # tiles and tile_coords are part of the public API
    self.tiles = {}
    self.tile_coords = []

    logging.debug('preprocess data end')
    logging.debug(
        f'index: maxZoom: {options.get("indexMaxZoom")}, maxPoints: {options.get("indexMaxPoints")}')
    self.stats = {}
    self.total = 0

    # wraps features (ie extreme west and extreme east)
    # features = wrap(features, options)

    # start slicing from the top tile down
    if len(features) > 0:
        self.split_tile(features, 0, 0, 0)

    if len(features) > 0:
        logging.debug(
            f'features: {self.tiles[0].get("numFeatures")}, points: {self.tiles[0].get("numPoints")}')
        stop = datetime.now()
    logging.debug(f'generate tiles end: {stop}')
    logging.debug('tiles generated:', self.total, self.stats)

split_tile(features, z, x, y, cz=None, cx=None, cy=None)

Splits features from a parent tile to sub-tiles.

Parameters:

Name Type Description Default
features list

The features to be split

required
z int

The zoom level of the parent tile

required
x int

The x coordinate of the parent tile

required
y int

The y coordinate of the parent tile

required
cz int

The zoom level of the target tile

None
cx int

The x coordinate of the target tile

None
cy int

The y coordinate of the target tile

None
Source code in src/microjson/microjson2vt/microjson2vt.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def split_tile(self, features, z, x, y, cz=None, cx=None, cy=None):
    """
    Splits features from a parent tile to sub-tiles.

    Args:
        features (list): The features to be split
        z (int): The zoom level of the parent tile
        x (int): The x coordinate of the parent tile
        y (int): The y coordinate of the parent tile
        cz (int): The zoom level of the target tile
        cx (int): The x coordinate of the target tile
        cy (int): The y coordinate of the target tile
    """
    stack = [features, z, x, y]
    options = self.options
    # avoid recursion by using a processing queue
    while len(stack) > 0:
        y = stack.pop()
        x = stack.pop()
        z = stack.pop()
        features = stack.pop()

        z2 = 1 << z
        id_ = to_Id(z, x, y)
        tile = self.tiles.get(id_, None)

        if tile is None:
            logging.debug('creation start')

            self.tiles[id_] = create_tile(features, z, x, y, options)
            tile = self.tiles[id_]
            self.tile_coords.append({'z': z, 'x': x, 'y': y})

            logging.debug(
                f'tile z{z}-{x}-{y} (features: {tile.get("numFeatures")}, points: {tile.get("numPoints")}, simplified: {tile.get("numSimplified")})')
            logging.debug('creation end')
            key = f'z{z}'
            self.stats[key] = self.stats.get(key, 0) + 1
            self.total += 1

        # save reference to original geometry in tile so that we can drill
        # down later if we stop now
        tile['source'] = features

        # if it's the first-pass tiling
        if cz is None:
            # stop tiling if we reached max zoom, or if the tile is too
            # simple
            if z == options.get(
                'indexMaxZoom') or tile.get(
                    'numPoints') <= options.get('indexMaxPoints'):
                continue  # if a drilldown to a specific tile
        elif z == options.get('maxZoom') or z == cz:
            # stop tiling if we reached base zoom or our target tile zoom
            continue
        elif cz is not None:
            # stop tiling if it's not an ancestor of the target tile
            zoomSteps = cz - z
            if x != (cx >> zoomSteps) or y != (cy >> zoomSteps):
                continue

        # if we slice further down, no need to keep source geometry
        tile['source'] = None

        if not features or len(features) == 0:
            continue

        logging.debug('clipping start')

        # values we'll use for clipping
        k1 = 0.5 * options.get('buffer') / options.get('extent')
        k2 = 0.5 - k1
        k3 = 0.5 + k1
        k4 = 1 + k1

        tl = None
        bl = None
        tr = None
        br = None

        left = clip(features, z2, x - k1, x + k3, 0,
                    tile['minX'], tile['maxX'], options)
        right = clip(features, z2, x + k2, x + k4, 0,
                     tile['minX'], tile['maxX'], options)
        features = None

        if left is not None:
            tl = clip(left, z2, y - k1, y + k3, 1,
                      tile['minY'], tile['maxY'], options)
            bl = clip(left, z2, y + k2, y + k4, 1,
                      tile['minY'], tile['maxY'], options)
            left = None

        if right is not None:
            tr = clip(right, z2, y - k1, y + k3, 1,
                      tile['minY'], tile['maxY'], options)
            br = clip(right, z2, y + k2, y + k4, 1,
                      tile['minY'], tile['maxY'], options)
            right = None

        logging.debug('clipping ended')

        stack.append(tl if tl is not None else [])
        stack.append(z + 1)
        stack.append(x * 2)
        stack.append(y * 2)

        stack.append(bl if bl is not None else [])
        stack.append(z + 1)
        stack.append(x * 2)
        stack.append(y * 2 + 1)

        stack.append(tr if tr is not None else [])
        stack.append(z + 1)
        stack.append(x * 2 + 1)
        stack.append(y * 2)

        stack.append(br if br is not None else [])
        stack.append(z + 1)
        stack.append(x * 2 + 1)
        stack.append(y * 2 + 1)

TileHandler module

The TileHandler module is a helper module that can be used to generate binary tiles from a large MicroJSON file, my utilizing both microjson2vt and vt2pbf.

TileHandler

Class to handle the generation of tiles from MicroJSON data

Source code in src/microjson/tilecut.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class TileHandler:
    """
    Class to handle the generation of tiles from MicroJSON data
    """
    tile_json: TileModel
    pbf: bool
    id_counter: int
    id_set: set

    def __init__(self, tileobj: TileModel, pbf: bool = False):
        """
        Initialize the TileHandler with a TileJSON configuration and optional
        PBF flag

        Args:
        tileobj (TileModel): TileJSON configuration
        pbf (bool): Flag to indicate whether to encode the tiles in PBF

        """
        # read the tilejson file to string
        self.tile_json = tileobj
        self.pbf = pbf
        self.id_counter = 0
        self.id_set = set()

    def microjson2tiles(self,
                        microjson_data_path: Union[str, Path],
                        validate: bool = False
                        ) -> List[str]:
        """
        Generate tiles in form of JSON or PBF files from MicroJSON data.

        Args:
            microjson_data_path (Union[str, Path]): Path to the MicroJSON data file
            validate (bool): Flag to indicate whether to validate the MicroJSON data

        Returns:
            List[str]: List of paths to the generated tiles
        """
        def save_tile(tile_data, z, x, y, tiles_path_template):
            """
            Save a single tile to a file based on the template path.

            Args:
                tile_data: The tile data to save
                z: The zoom level of the tile
                x: The x coordinate of the tile
                y: The y coordinate of the tile
                tiles_path_template: The template path for the tiles

            Returns:
                str: The path to the saved tile
            """
            # Format the path template with actual tile coordinates
            tile_path = str(tiles_path_template).format(z=z, x=x, y=y)
            os.makedirs(os.path.dirname(tile_path), exist_ok=True)

            # Save the tile data (this assumes tile_data is already in the
            # correct format, e.g., PBF or JSON)
            with open(
                tile_path,
                'wb' if tile_path.endswith('.pbf') else 'w') as f:
                f.write(tile_data)

            # return the path to the saved tile
            return tile_path

        def convert_id_to_int(data) -> dict:
            """
            Convert all id fields in the data to integers

            Args:
                data: The data to convert

            Returns:
                dict: The data with all id fields converted to integers
            """


            # check if data is a list
            if isinstance(data, list):
                for item in data:
                    convert_id_to_int(item)
                return data
            # check if data is a dict
            elif isinstance(data, dict):
                for key, value in data.items():
                    if key == 'id':
                        if value is None:
                            data[key] = self.id_counter
                            self.id_counter += 1
                        else:
                            data[key] = int(value)
                        while data[key] in self.id_set:
                            self.id_counter += 1
                            data[key] = self.id_counter
                        self.id_set.add(data[key])
                    if isinstance(value, dict):
                        convert_id_to_int(value)
                    if isinstance(value, list):
                        for item in value:
                            convert_id_to_int(item)
                return data
            else:
                return int(data)

        # Load the MicroJSON data
        with open(microjson_data_path, 'r') as file:
            microjson_data = json.load(file)

        # Validate the MicroJSON data
        if validate:
            try:
                MicroJSON.model_validate(microjson_data)
            except ValidationError as e:
                logger.error(f"MicroJSON data validation failed: {e}")
                return []

        # TODO currently only supports one tile layer
        # calculate maxzoom and minzoom from layer and global tilejson

        maxzoom = min(self.tile_json.maxzoom, 
                      self.tile_json.vector_layers[0].maxzoom)
        minzoom = max(self.tile_json.minzoom,
                      self.tile_json.vector_layers[0].minzoom)

        # Options for geojson2vt from TileJSON
        options = {
            'maxZoom': maxzoom,  # max zoom in the final tileset
            'indexMaxZoom': self.tile_json.maxzoom,  # max zoom in the initial tile index
            'indexMaxPoints': 0,  # max number of points per tile, set to 0 for no restriction
            'bounds': self.tile_json.bounds
        }

        # Convert GeoJSON to intermediate vector tiles
        tile_index = microjson2vt(microjson_data, options)

        # Placeholder for the tile paths
        generated_tiles = []

        # get tilepath from tilejson self.tile_json.tiles
        # extract the folder from the filepath
        tilefolder = Path(self.tile_json.tiles[0]).parent


        for tileno in tile_index.tiles:
            atile = tile_index.tiles[tileno]
            x, y, z = atile["x"], atile["y"], atile["z"]
            # if z is less than minzoom, or greater than maxzoom, skip the tile
            if z < minzoom or z > maxzoom:
                continue
            tile_data = tile_index.get_tile(z, x, y)

            # convert all id to int, as there is a bug in the geojson2vt
            # library
            tile_data = convert_id_to_int(tile_data)

            # add name to the tile_data
            tile_data["name"] = "tile"
            if self.pbf:
                # Using mapbox_vector_tile to encode tile data to PBF
                encoded_data = vt2pbf(tile_data)
            else:
                encoded_data = json.dumps(tile_data)

            generated_tiles.append(save_tile(
                encoded_data, z, x, y, self.tile_json.tiles[0]))

        return generated_tiles

__init__(tileobj, pbf=False)

Initialize the TileHandler with a TileJSON configuration and optional PBF flag

Args: tileobj (TileModel): TileJSON configuration pbf (bool): Flag to indicate whether to encode the tiles in PBF

Source code in src/microjson/tilecut.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def __init__(self, tileobj: TileModel, pbf: bool = False):
    """
    Initialize the TileHandler with a TileJSON configuration and optional
    PBF flag

    Args:
    tileobj (TileModel): TileJSON configuration
    pbf (bool): Flag to indicate whether to encode the tiles in PBF

    """
    # read the tilejson file to string
    self.tile_json = tileobj
    self.pbf = pbf
    self.id_counter = 0
    self.id_set = set()

microjson2tiles(microjson_data_path, validate=False)

Generate tiles in form of JSON or PBF files from MicroJSON data.

Parameters:

Name Type Description Default
microjson_data_path Union[str, Path]

Path to the MicroJSON data file

required
validate bool

Flag to indicate whether to validate the MicroJSON data

False

Returns:

Type Description
List[str]

List[str]: List of paths to the generated tiles

Source code in src/microjson/tilecut.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def microjson2tiles(self,
                    microjson_data_path: Union[str, Path],
                    validate: bool = False
                    ) -> List[str]:
    """
    Generate tiles in form of JSON or PBF files from MicroJSON data.

    Args:
        microjson_data_path (Union[str, Path]): Path to the MicroJSON data file
        validate (bool): Flag to indicate whether to validate the MicroJSON data

    Returns:
        List[str]: List of paths to the generated tiles
    """
    def save_tile(tile_data, z, x, y, tiles_path_template):
        """
        Save a single tile to a file based on the template path.

        Args:
            tile_data: The tile data to save
            z: The zoom level of the tile
            x: The x coordinate of the tile
            y: The y coordinate of the tile
            tiles_path_template: The template path for the tiles

        Returns:
            str: The path to the saved tile
        """
        # Format the path template with actual tile coordinates
        tile_path = str(tiles_path_template).format(z=z, x=x, y=y)
        os.makedirs(os.path.dirname(tile_path), exist_ok=True)

        # Save the tile data (this assumes tile_data is already in the
        # correct format, e.g., PBF or JSON)
        with open(
            tile_path,
            'wb' if tile_path.endswith('.pbf') else 'w') as f:
            f.write(tile_data)

        # return the path to the saved tile
        return tile_path

    def convert_id_to_int(data) -> dict:
        """
        Convert all id fields in the data to integers

        Args:
            data: The data to convert

        Returns:
            dict: The data with all id fields converted to integers
        """


        # check if data is a list
        if isinstance(data, list):
            for item in data:
                convert_id_to_int(item)
            return data
        # check if data is a dict
        elif isinstance(data, dict):
            for key, value in data.items():
                if key == 'id':
                    if value is None:
                        data[key] = self.id_counter
                        self.id_counter += 1
                    else:
                        data[key] = int(value)
                    while data[key] in self.id_set:
                        self.id_counter += 1
                        data[key] = self.id_counter
                    self.id_set.add(data[key])
                if isinstance(value, dict):
                    convert_id_to_int(value)
                if isinstance(value, list):
                    for item in value:
                        convert_id_to_int(item)
            return data
        else:
            return int(data)

    # Load the MicroJSON data
    with open(microjson_data_path, 'r') as file:
        microjson_data = json.load(file)

    # Validate the MicroJSON data
    if validate:
        try:
            MicroJSON.model_validate(microjson_data)
        except ValidationError as e:
            logger.error(f"MicroJSON data validation failed: {e}")
            return []

    # TODO currently only supports one tile layer
    # calculate maxzoom and minzoom from layer and global tilejson

    maxzoom = min(self.tile_json.maxzoom, 
                  self.tile_json.vector_layers[0].maxzoom)
    minzoom = max(self.tile_json.minzoom,
                  self.tile_json.vector_layers[0].minzoom)

    # Options for geojson2vt from TileJSON
    options = {
        'maxZoom': maxzoom,  # max zoom in the final tileset
        'indexMaxZoom': self.tile_json.maxzoom,  # max zoom in the initial tile index
        'indexMaxPoints': 0,  # max number of points per tile, set to 0 for no restriction
        'bounds': self.tile_json.bounds
    }

    # Convert GeoJSON to intermediate vector tiles
    tile_index = microjson2vt(microjson_data, options)

    # Placeholder for the tile paths
    generated_tiles = []

    # get tilepath from tilejson self.tile_json.tiles
    # extract the folder from the filepath
    tilefolder = Path(self.tile_json.tiles[0]).parent


    for tileno in tile_index.tiles:
        atile = tile_index.tiles[tileno]
        x, y, z = atile["x"], atile["y"], atile["z"]
        # if z is less than minzoom, or greater than maxzoom, skip the tile
        if z < minzoom or z > maxzoom:
            continue
        tile_data = tile_index.get_tile(z, x, y)

        # convert all id to int, as there is a bug in the geojson2vt
        # library
        tile_data = convert_id_to_int(tile_data)

        # add name to the tile_data
        tile_data["name"] = "tile"
        if self.pbf:
            # Using mapbox_vector_tile to encode tile data to PBF
            encoded_data = vt2pbf(tile_data)
        else:
            encoded_data = json.dumps(tile_data)

        generated_tiles.append(save_tile(
            encoded_data, z, x, y, self.tile_json.tiles[0]))

    return generated_tiles

getbounds(microjson_file)

Get the max and min bounds for coordinates of the MicroJSON file

Parameters:

Name Type Description Default
microjson_file str

Path to the MicroJSON file

required

Returns:

Type Description
List[float]

List[float]: List of the bounds [minx, miny, maxx, maxy]

Source code in src/microjson/tilecut.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def getbounds(microjson_file: str) -> List[float]:
    """
    Get the max and min bounds for coordinates of the MicroJSON file

    Args:
        microjson_file (str): Path to the MicroJSON file

    Returns:
        List[float]: List of the bounds [minx, miny, maxx, maxy]
    """
    with open(microjson_file, 'r') as file:
        data = json.load(file)

    # get the bounds
    minx = miny = float('inf')
    maxx = maxy = float('-inf')
    if 'features' in data:
        for feature in data['features']:
            if 'geometry' in feature:
                if feature['geometry']['type'] == 'Polygon':
                    for ring in feature['geometry']['coordinates']:
                        for coord in ring:
                            minx = min(minx, coord[0])
                            miny = min(miny, coord[1])
                            maxx = max(maxx, coord[0])
                            maxy = max(maxy, coord[1])
                if feature['geometry']['type'] == 'MultiPolygon':
                    for polygon in feature['geometry']['coordinates']:
                        for ring in polygon:
                            for coord in ring:
                                minx = min(minx, coord[0])
                                miny = min(miny, coord[1])
                                maxx = max(maxx, coord[0])
                                maxy = max(maxy, coord[1])
    return [minx, miny, maxx, maxy]

TileJSON for MicroJSON example with Vector Layers

The below example illustrates a TileJSON for a MicroJSON tileset multiple layers of detail. The tileset has a single vector layer, image_layer id of vector_layers, which contains a single vector layer describing images. The fields property of the this layer specifies the attributes of the layer, including the data types of the attributes. The tiles property specifies the URL pattern for accessing the vector tiles, which in this case is a 2D data set (no channels, time or z-axis) with zoom level.

This file is located in the examples/tiles directory of the repository, and is named tiled_example.json. It has a corresponding MicroJSON file for each tile, located in the examples/tiles/tiled_example directory of the repository. The MicroJSON files are organized according to the tiling scheme, with the directory structure zlvl/x/y.json where zlvl is the zoom level, x is the x coordinate, and y is the y coordinate. The MicroJSON files contain the MicroJSON objects for the corresponding tiles, and are named according to the tiling scheme. For example, the MicroJSON object for the tile at zoom level 1, tile at (0,1) in the tiling scheme is located at examples/tiles/tiled_example/1/0/1.json. Examples for MicroJSON objects at zoom levels 0, 1, and 2 are provided below.

{
    {
    "tilejson": "3.0.0",
    "name": "2D Data Example",
    "description": "A tileset showing 2D data with multiple layers of detail.",
    "version": "1.0.0",
    "attribution": "<a href='http://example.com'>Example</a>",
    "tiles": [
        "http://example.com/tiled_example/{zlvl}/{x}/{y}.json"
    ],
    "minzoom": 0,
    "maxzoom": 10,
    "bounds": [0, 0, 24000, 24000],
    "center": [12000, 12000, 0],
    "vector_layers": [
        {
            "id": "Tile_layer",
            "description": "Tile layer",
            "minzoom": 0,
            "maxzoom": 10,
            "fields": {
              "plate": "String",
              "image": "String",
              "label": "Number",
              "channel": "Number"
            }
        }
    ],
    "fillzoom": 3
}

Tiled binary TileJSON

In addition to json format, tiles may be encoded in a binary protobuf format. Below follows a similar example to the one above, but with binary tiles. The tiles property specifies the URL pattern for accessing the binary tiles, which in this case is a 2D data set (no channels, time or z-axis) with zoom level. The fillzoom property specifies the zoom level from which to generate overzoomed tiles, which in this case starts at level 3, after the last specified layer.

{
    "tilejson": "3.0.0",
    "name": "2D Data Example",
    "description": "A tileset showing 2D data with multiple layers of detail.",
    "version": "1.0.0",
    "attribution": "<a href='http://example.com'>Example</a>",
    "tiles": [
        "http://example.com/tiled_example/{zlvl}/{x}/{y}.pbf"
    ],
    "minzoom": 0,
    "maxzoom": 10,
    "bounds": [0, 0, 24000, 24000],
    "center": [12000, 12000, 0],
    "vector_layers": [
        {
            "id": "tile_layer",
            "description": "Tile layer",
            "minzoom": 0,
            "maxzoom": 10,
            "fields": {
              "plate": "String",
              "image": "String",
              "label": "Number",
              "channel": "Number",
            },
            "fieldranges": {
              "label": [0,100],
              "channel": [0,10]
            },
            "fieldenums": {
              "plate": ["A1", "A2", "B1", "B2"],
              "image": ["image1.tif", "image2.tif", "image3.tif"],
            }
            "fielddescriptions": {
              "plate": "Well plate identifier",
              "image": "Image filename",
              "label": "Label identifier",
              "channel": "Channel identifier"
            }
        }
    ],
    "fillzoom": 3
}

Tiled binary example

The examples folder contains an example of how to generate binary tiles from one large MicroJSON file. It uses a helper module that generates a large random polygon grid, as could be expected in an imaging setting, using typical imaging coordinates. It is also included below for reference.

Example of creating binary tiles from a large MicroJSON file

Source code in src/microjson/examples/tiling.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def main():
    # clear the tiles directory
    os.system("rm -rf tiles")

    # create the tiles directory
    os.makedirs("tiles")

    # Define your vector layers with appropriate settings
    vector_layers = [
        TileLayer(
            id="polygon-layer",
            fields={"id": "String", "polytype": "String"},
            minzoom=0,
            maxzoom=10,
            description="Layer containing polygon data",
            fieldranges= {
                "id": [1, 99999999]
            },
            fieldenums= {
                "polytype": ["Type1", "Type2", "Type3", "Type4"]
            },
        )
    ]

    # Create a microjson file with random polygons
    GRID_SIZE = 50000
    CELL_SIZE = 100
    MIN_VERTICES = 10
    MAX_VERTICES = 100
    meta_types = {
        "id": "str",
        "num_vertices": "int",
    }
    meta_values_options = {
        "polytype": ["Type1", "Type2", "Type3", "Type4"]
    }
    microjson_data_path = "tiles/microjson_data.json"
    generate_polygons(
        GRID_SIZE,
        CELL_SIZE,
        MIN_VERTICES,
        MAX_VERTICES,
        meta_types,
        meta_values_options,
        microjson_data_path
    )

    # get bounds
    maxbounds = getbounds(microjson_data_path)
    # set min to 0
    maxbounds[0] = 0
    maxbounds[1] = 0

    center = [0,
              (maxbounds[0] + maxbounds[2]) / 2,
              (maxbounds[1] + maxbounds[3]) / 2]

    # Instantiate TileModel with your settings
    tile_model = TileModel(
        tilejson="3.0.0",
        tiles=[Path("tiles/{z}/{x}/{y}.pbf")],  # Local path or URL
        name="Example Tile Layer",
        description="A TileJSON example incorporating MicroJSON data",
        version="1.0.0",
        attribution="Polus AI",
        minzoom=0,
        maxzoom=10,
        bounds=maxbounds,
        center=center,
        vector_layers=vector_layers
    )

    # Create the root model with your TileModel instance
    tileobj = TileJSON(root=tile_model)

    # export to tilejson
    os.makedirs("tiles", exist_ok=True)
    with open("tiles/metadata.json", "w") as f:
        f.write(tileobj.model_dump_json(indent=2))

    # Initialize the TileHandler
    handler = TileHandler(tile_model, pbf=True)
    handler.microjson2tiles(microjson_data_path, validate=False)

Tiled MicroJSON Example

Level 0

The following is an example of a MicroJSON object at zoom level 0, tile at (0,0) in the tiling scheme. Example URL: http://example.com/tiles/0/0/0.json

{
    "tilejson": "3.0.0",
    "name": "2D Data Example",
    "description": "A tileset showing 2D data with multiple layers of detail.",
    "version": "1.0.0",
    "attribution": "<a href='http://example.com'>Example</a>",
    "tiles": [
        "http://example.com/tiled_example/{zlvl}/{x}/{y}.json"
    ],
    "minzoom": 0,
    "maxzoom": 10,
    "bounds": [0, 0, 24000, 24000],
    "center": [12000, 12000, 0],
    "format": "json",
    "vector_layers": [
        {
            "id": "image_layer",
            "description": "Image layer",
            "minzoom": 0,
            "maxzoom": 10,
            "fields": {
              "plate": "String",
              "image": "String",
              "label": "Number",
              "channel": "Number"
            }
        }
    ],
    "fillzoom": 3
}

Level 1

The following is an example of a MicroJSON object at zoom level 1, tile at (0,1) in the tiling scheme. Example URL: http://example.com/tiles/1/0/1.json

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              0,
              10000
            ],
            [
              10000,
              10000
            ],
            [
              10000,
              20000
            ],
            [
              0,
              20000
            ],
            [
              0,
              10000
            ]
          ]
        ]
      },
      "properties": {
        "label": 3
      }
    }
  ],
  "multiscale": {
    "axes": [
      {
        "name": "x",
        "type": "space",
        "unit": "micrometer",
        "description": "x-axis"
      },
      {
        "name": "y",
        "type": "space",
        "unit": "micrometer",
        "description": "y-axis"
      }
    ]
  },
  "properties": {
    "plate": "label",
    "image": "x00_y01_p01_c1.ome.tif",
    "channel": 1.0
  }
}

Level 2

The following is an example of a MicroJSON object at zoom level 2, tile at (1,1) in the tiling scheme. Example URL: http://example.com/tiles/2/1/3.json

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              5000,
              15000
            ],
            [
              10000,
              15000
            ],
            [
              10000,
              20000
            ],
            [
              5000,
              20000
            ],
            [
              5000,
              15000
            ]
          ]
        ]
      },
      "properties": {
        "label": 13
      }
    }
  ],
  "multiscale": {
    "axes": [
      {
        "name": "x",
        "type": "space",
        "unit": "micrometer",
        "description": "x-axis"
      },
      {
        "name": "y",
        "type": "space",
        "unit": "micrometer",
        "description": "y-axis"
      }
    ]
  },
  "properties": {
    "plate": "label",
    "image": "x00_y01_p01_c1.ome.tif",
    "channel": 1.0
  }
}