Package: pymartini
pymartiniA Cython port ofMartinifor fast RTIN terrain mesh generation, 2-3x
faster than Martini in Node. The only dependency is Numpy.A wireframe rendering of the Grand Canyon. The mesh is created usingpymartini, encoded usingquantized-mesh-encoder,
served on-demand usingdem-tiler, and rendered withdeck.gl.InstallWith pip:pip install pymartinior with Conda:conda install -c conda-forge pymartiniUsingExampleThe API is modeled after Martini.frompymartiniimportMartini# set up mesh generator for a certain 2^k+1 grid size# Usually either 257 or 513martini=Martini(257)# generate RTIN hierarchy from terrain data (an array of size^2 length)tile=martini.create_tile(terrain)# get a mesh (vertices and triangles indices) for a 10m errorvertices,triangles=tile.get_mesh(10)APITheMartiniclass andcreate_tileandget_meshmethods are a direct port
from the JS Martini library.Additionally I include two helper functions:decode_eleto decode a Mapbox
Terrain RGB or Terrarium PNG array to elevations; andrescale_positions, which
adds elevations to each vertex and optionally linearly rescales each vertex's XY
coordinates to a new bounding box.MartiniA class to instantiate constants needed for thecreate_tileandget_meshsteps. As noted in the benchmarks below, instantiating theMartiniclass is
the slowest of the three functions. If you're planning to create many meshes of
the same size, create oneMartiniclass and create many tiles from it.Argumentsgrid_size(int, default257): the grid size to use when generating the
mesh. Must be 2^k+1. If your source heightmap is 256x256 pixels, usegrid_size=257and backfill the border pixels.ReturnsReturns aMartiniinstance on which you can callcreate_tile.Martini.create_tileGenerate RTIN hierarchy from terrain data. This is faster than creating theMartiniinstance, but slower than creating a mesh for a given max error. If
you need to create many meshes with different errors for the same tile, you
should reuse aTileinstance.Argumentsterrain(numpyndarray): an array of dtypefloat32representing the
input heightmap. The array can either be flattened, of shape (2^k+1 * 2^k+1)
or a two-dimensional array of shape (2^k+1, 2^k+1). Note that for a 2D array
pymartini expects indices in (columns, rows) order, so you might need to
transpose your array first. Currently an error will be produced if the dtype
of your input array is notnp.float32.ReturnsReturns aTileinstance on which you can callget_mesh.Tile.get_meshGet a mesh for a given max error.Argumentsmax_error(float, default0): the maximum vertical error for each
triangle in the output mesh. For example if the units of the input heightmap
is meters, usingmax_error=5would mean that the mesh is continually refined
until every triangle approximates the surface of the heightmap within 5
meters.ReturnsReturns a tuple of (vertices,triangles).Each is a flat numpy array. Vertices represents the interleaved2Dcoordinates of each vertex, e.g.[x0, y0, x1, y1, ...]. If you need 3D
coordinates, you can use therescale_positionshelper function described
below.trianglesrepresentsindiceswithin theverticesarray. So[0, 1, 3, ...]would use the first, second, and fourth vertices within theverticesarray as a single triangle.decode_eleA helper function to decode a PNG terrain tile into elevations.Argumentspng(np.ndarray): Ndarray of elevations encoded in three channels,
representing red, green, and blue. Must be of shape (tile_size,tile_size,>=3) or (>=3,tile_size,tile_size), wheretile_sizeis usually 256
or 512encoding(str): Either 'mapbox' or 'terrarium', the two main RGB
encodings for elevation valuesbackfill(bool, defaultTrue): Whether to create an array of size
(tile_size + 1,tile_size + 1), backfilling the bottom and right edges. This is used
because Martini needs a grid of size2^n + 1Returns(np.ndarray) Array with decoded elevation values. IfbackfillisTrue,
returned shape is (tile_size + 1,tile_size + 1), otherwise returned shape
is (tile_size,tile_size), wheretile_sizeis the shape of the input
array.Examplefromimageioimportimreadfrompymartiniimportdecode_elepath='./test/data/fuji.png'fuji=imread(path)terrain=decode_ele(fuji,'mapbox')rescale_positionsA helper function to rescale theverticesoutput and add elevations. The
output is a numpy ndarray of the form[[x1, y1, z1], [x2, y2, z2], ...].Argumentsvertices: (np.array) vertices output from Martiniterrain: (np.ndarray) 2d heightmap array of elevations as output bydecode_ele. Expected to have shape (grid_size,grid_size).terrainis expected to be the exact same array passed toMartini.create_tile.If
you use a different or transposed array, the mesh will look weird. See#15. If you need to
transpose your array, do it before passing toMartini.create_tile.bounds: (List[float], defaultNone) linearly rescale position values to
this extent, expected to be [minx, miny, maxx, maxy]. If not provided, no
rescaling is doneflip_y: (bool, defaultFalse) Flip y coordinates. Can be useful when
original data source is a PNG, since the origin of a PNG is the top left.Examplefromimageioimportimreadfrompymartiniimportdecode_ele,Martini,rescale_positionspath='./test/data/terrarium.png'png=imread(path)terrain=decode_ele(png,'mapbox')martini=Martini(png.shape[0]+1)tile=martini.create_tile(terrain)vertices,triangles=tile.get_mesh(10)# Use mercantile to find the bounds in WGS84 of this tileimportmercantilebounds=mercantile.bounds(mercantile.Tile(385,803,11))# Rescale positions to WGS84rescaled=rescale_positions(vertices,terrain,bounds=bounds,flip_y=Truecolumn_row=True)MartiniorDelatin?Two popular algorithms for terrain mesh generation are the"Martini"algorithm, found in the JavaScriptmartinilibrary and this Pythonpymartinilibrary, and the"Delatin"algorithm, found in the
C++hmmlibrary, the Pythonpydelatinlibrary, and the JavaScriptdelatinlibrary.Which to use?For most purposes, usepydelatinoverpymartini. A good breakdown froma
Martini issue:Martini:Only works on square 2^n+1 x 2^n+1 grids.Generates a hierarchy of meshes (pick arbitrary detail after a single run)Optimized for meshing speed rather than quality.Delatin:Works on arbitrary raster grids.Generates a single mesh for a particular detail.Optimized for quality (as few triangles as possible for a given error).Correctnesspymartinipasses the (only) test case included in the original Martini JS
library. I also wrote a few extra conformance tests to compare output bypymartiniand Martini. I've found some small differences in float values at
the end of the second step.This second step,martini.create_tile(terrain), computes the maximum error of
every possible triangle and accumulates them. Thus, small float errors appear to
be magnified by the summation of errors into larger triangles. These errors
appear to be within1e-5of the JS output. I'm guessing that this variance is
greater than normal float rounding errors, due to this summation behavior.These differences are larger when using 512px tiles compared to 256px tiles,
which reinforces my hypothesis that the differences have something to do with
small low-level float or bitwise operations differences between Python and
JavaScript.If you'd like to explore this in more detail, look at theTile.update()inmartini.pyxand the corresponding Martini code.Type CheckingAs ofpymartini0.4.0, types are provided, which can be used with a checker
likemypy. If you wish to get the full
benefit, make sure toenable Numpy's mypy
plugin.BenchmarkPreparation steps are about 3x faster in Python than in Node; generating the
mesh is about 2x faster in Python than in Node.Pythongitclonehttps://github.com/kylebarron/pymartinicdpymartini
pipinstall'.[test]'pythonbench.pyinit tileset: 14.860ms
create tile: 5.862ms
mesh (max_error=30): 1.010ms
vertices: 9700.0, triangles: 19078.0
mesh 0: 18.350ms
mesh 1: 17.581ms
mesh 2: 15.245ms
mesh 3: 13.853ms
mesh 4: 11.284ms
mesh 5: 12.360ms
mesh 6: 8.293ms
mesh 7: 8.342ms
mesh 8: 7.166ms
mesh 9: 5.678ms
mesh 10: 5.886ms
mesh 11: 5.092ms
mesh 12: 3.732ms
mesh 13: 3.420ms
mesh 14: 3.524ms
mesh 15: 3.101ms
mesh 16: 2.892ms
mesh 17: 2.358ms
mesh 18: 2.250ms
mesh 19: 2.293ms
mesh 20: 2.281ms
20 meshes total: 155.559msJS (Node)gitclonehttps://github.com/mapbox/martinicdmartini
npminstall
node-resmbench.jsinit tileset: 54.293ms
create tile: 17.307ms
mesh: 6.230ms
vertices: 9704, triangles: 19086
mesh 0: 43.181ms
mesh 1: 33.102ms
mesh 2: 30.735ms
mesh 3: 25.935ms
mesh 4: 20.643ms
mesh 5: 17.511ms
mesh 6: 15.066ms
mesh 7: 13.334ms
mesh 8: 11.180ms
mesh 9: 9.651ms
mesh 10: 9.240ms
mesh 11: 10.996ms
mesh 12: 7.520ms
mesh 13: 6.617ms
mesh 14: 5.860ms
mesh 15: 5.693ms
mesh 16: 4.907ms
mesh 17: 4.469ms
mesh 18: 4.267ms
mesh 19: 4.267ms
mesh 20: 3.619ms
20 meshes total: 290.256msLicenseThis library is ported from Mapbox'sMartini, which is licensed under
the ISC License. My additions are licensed under the MIT license.ISC LicenseCopyright (c) 2019, MapboxPermission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.