{"id":18174816,"url":"https://github.com/kylebarron/pymartini","last_synced_at":"2025-04-10T01:08:02.815Z","repository":{"id":45604209,"uuid":"266947888","full_name":"kylebarron/pymartini","owner":"kylebarron","description":"A Cython port of Martini for fast RTIN terrain mesh generation","archived":false,"fork":false,"pushed_at":"2024-09-03T21:19:12.000Z","size":1829,"stargazers_count":90,"open_issues_count":9,"forks_count":9,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-10T01:07:49.387Z","etag":null,"topics":["cython","elevation","heightmap","mapbox","martini","mesh","mesh-generation","numpy","terrain"],"latest_commit_sha":null,"homepage":"https://kylebarron.dev/quantized-mesh-encoder","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kylebarron.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-26T04:55:59.000Z","updated_at":"2025-03-20T11:18:46.000Z","dependencies_parsed_at":"2022-09-11T15:41:37.439Z","dependency_job_id":null,"html_url":"https://github.com/kylebarron/pymartini","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebarron%2Fpymartini","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebarron%2Fpymartini/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebarron%2Fpymartini/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebarron%2Fpymartini/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kylebarron","download_url":"https://codeload.github.com/kylebarron/pymartini/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248137888,"owners_count":21053775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cython","elevation","heightmap","mapbox","martini","mesh","mesh-generation","numpy","terrain"],"created_at":"2024-11-02T16:07:50.953Z","updated_at":"2025-04-10T01:08:02.791Z","avatar_url":"https://github.com/kylebarron.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# pymartini\n\nA Cython port of [Martini][martini] for fast RTIN terrain mesh generation, 2-3x\nfaster than Martini in Node. The only dependency is Numpy.\n\n[![][image_url]][example]\n\n[image_url]: https://raw.githubusercontent.com/kylebarron/pymartini/master/assets/grca_wireframe.jpg\n[example]: https://kylebarron.dev/quantized-mesh-encoder\n\nA wireframe rendering of the Grand Canyon. The mesh is created using\n`pymartini`, encoded using [`quantized-mesh-encoder`][quantized-mesh-encoder],\nserved on-demand using [`dem-tiler`][dem-tiler], and rendered with\n[deck.gl](https://deck.gl).\n\n[quantized-mesh-encoder]: https://github.com/kylebarron/quantized-mesh-encoder\n[dem-tiler]: https://github.com/kylebarron/dem-tiler\n\n## Install\n\nWith pip:\n\n```\npip install pymartini\n```\n\nor with Conda:\n\n```\nconda install -c conda-forge pymartini\n```\n\n## Using\n\n### Example\n\nThe API is modeled after Martini.\n\n```py\nfrom pymartini import Martini\n\n# set up mesh generator for a certain 2^k+1 grid size\n# Usually either 257 or 513\nmartini = Martini(257)\n\n# generate RTIN hierarchy from terrain data (an array of size^2 length)\ntile = martini.create_tile(terrain)\n\n# get a mesh (vertices and triangles indices) for a 10m error\nvertices, triangles = tile.get_mesh(10)\n```\n\n### API\n\nThe `Martini` class and `create_tile` and `get_mesh` methods are a direct port\nfrom the JS Martini library.\n\nAdditionally I include two helper functions: `decode_ele` to decode a Mapbox\nTerrain RGB or Terrarium PNG array to elevations; and `rescale_positions`, which\nadds elevations to each vertex and optionally linearly rescales each vertex's XY\ncoordinates to a new bounding box.\n\n#### `Martini`\n\nA class to instantiate constants needed for the `create_tile` and `get_mesh`\nsteps. As noted in the benchmarks below, instantiating the `Martini` class is\nthe slowest of the three functions. If you're planning to create many meshes of\nthe same size, create one `Martini` class and create many tiles from it.\n\n##### Arguments\n\n- `grid_size` (`int`, default `257`): the grid size to use when generating the\n  mesh. Must be 2^k+1. If your source heightmap is 256x256 pixels, use\n  `grid_size=257` and backfill the border pixels.\n\n##### Returns\n\nReturns a `Martini` instance on which you can call `create_tile`.\n\n#### `Martini.create_tile`\n\nGenerate RTIN hierarchy from terrain data. This is faster than creating the\n`Martini` instance, but slower than creating a mesh for a given max error. If\nyou need to create many meshes with different errors for the same tile, you\nshould reuse a `Tile` instance.\n\n##### Arguments\n\n- `terrain` (numpy `ndarray`): an array of dtype `float32` representing the\n  input heightmap. The array can either be flattened, of shape (2^k+1 \\* 2^k+1)\n  or a two-dimensional array of shape (2^k+1, 2^k+1). Note that for a 2D array\n  pymartini expects indices in (columns, rows) order, so you might need to\n  transpose your array first. Currently an error will be produced if the dtype\n  of your input array is not `np.float32`.\n\n##### Returns\n\nReturns a `Tile` instance on which you can call `get_mesh`.\n\n#### `Tile.get_mesh`\n\nGet a mesh for a given max error.\n\n##### Arguments\n\n- `max_error` (`float`, default `0`): the maximum vertical error for each\n  triangle in the output mesh. For example if the units of the input heightmap\n  is meters, using `max_error=5` would mean that the mesh is continually refined\n  until every triangle approximates the surface of the heightmap within 5\n  meters.\n\n##### Returns\n\nReturns a tuple of (`vertices`, `triangles`).\n\nEach is a flat numpy array. Vertices represents the interleaved **2D**\ncoordinates of each vertex, e.g. `[x0, y0, x1, y1, ...]`. If you need 3D\ncoordinates, you can use the `rescale_positions` helper function described\nbelow.\n\n`triangles` represents _indices_ within the `vertices` array. So `[0, 1, 3, ...]` would use the first, second, and fourth vertices within the `vertices`\narray as a single triangle.\n\n#### `decode_ele`\n\nA helper function to decode a PNG terrain tile into elevations.\n\n##### Arguments\n\n- `png` (`np.ndarray`): Ndarray of elevations encoded in three channels,\n  representing red, green, and blue. Must be of shape (`tile_size`, `tile_size`,\n  `\u003e=3`) or (`\u003e=3`, `tile_size`, `tile_size`), where `tile_size` is usually 256\n  or 512\n- `encoding` (`str`): Either 'mapbox' or 'terrarium', the two main RGB\n  encodings for elevation values\n- `backfill` (`bool`, default `True`): Whether to create an array of size\n  (`tile_size + 1`, `tile_size + 1`), backfilling the bottom and right edges. This is used\n  because Martini needs a grid of size `2^n + 1`\n\n##### Returns\n\n- (`np.ndarray`) Array with decoded elevation values. If `backfill` is `True`,\n  returned shape is (`tile_size + 1`, `tile_size + 1`), otherwise returned shape\n  is (`tile_size`, `tile_size`), where `tile_size` is the shape of the input\n  array.\n\n##### Example\n\n```py\nfrom imageio import imread\nfrom pymartini import decode_ele\n\npath = './test/data/fuji.png'\nfuji = imread(path)\nterrain = decode_ele(fuji, 'mapbox')\n```\n\n#### `rescale_positions`\n\nA helper function to rescale the `vertices` output and add elevations. The\noutput is a numpy ndarray of the form `[[x1, y1, z1], [x2, y2, z2], ...]`.\n\n##### Arguments\n\n- `vertices`: (`np.array`) vertices output from Martini\n- `terrain`: (`np.ndarray`) 2d heightmap array of elevations as output by\n  `decode_ele`. Expected to have shape (`grid_size`, `grid_size`). **`terrain`\n  is expected to be the exact same array passed to `Martini.create_tile`.** If\n  you use a different or transposed array, the mesh will look weird. See\n  [#15](https://github.com/kylebarron/pymartini/issues/15). If you need to\n  transpose your array, do it before passing to `Martini.create_tile`.\n- `bounds`: (`List[float]`, default `None`) linearly rescale position values to\n  this extent, expected to be [minx, miny, maxx, maxy]. If not provided, no\n  rescaling is done\n- `flip_y`: (`bool`, default `False`) Flip y coordinates. Can be useful when\n  original data source is a PNG, since the origin of a PNG is the top left.\n\n##### Example\n\n```py\nfrom imageio import imread\nfrom pymartini import decode_ele, Martini, rescale_positions\n\npath = './test/data/terrarium.png'\npng = imread(path)\nterrain = decode_ele(png, 'mapbox')\nmartini = Martini(png.shape[0] + 1)\ntile = martini.create_tile(terrain)\nvertices, triangles = tile.get_mesh(10)\n\n# Use mercantile to find the bounds in WGS84 of this tile\nimport mercantile\nbounds = mercantile.bounds(mercantile.Tile(385, 803, 11))\n\n# Rescale positions to WGS84\nrescaled = rescale_positions(\n    vertices,\n    terrain,\n    bounds=bounds,\n    flip_y=True\n    column_row=True\n)\n```\n\n## `Martini` or `Delatin`?\n\nTwo popular algorithms for terrain mesh generation are the **\"Martini\"**\nalgorithm, found in the JavaScript [`martini`][martini] library and this Python\n`pymartini` library, and the **\"Delatin\"** algorithm, found in the\nC++ [`hmm`][hmm] library, the Python [`pydelatin`][pydelatin] library, and the JavaScript\n[`delatin`][delatin] library.\n\nWhich to use?\n\nFor most purposes, use `pydelatin` over `pymartini`. A good breakdown from [a\nMartini issue][martini_desc_issue]:\n\n\u003e Martini:\n\u003e\n\u003e - Only works on square 2^n+1 x 2^n+1 grids.\n\u003e - Generates a hierarchy of meshes (pick arbitrary detail after a single run)\n\u003e - Optimized for meshing speed rather than quality.\n\u003e\n\u003e Delatin:\n\u003e\n\u003e - Works on arbitrary raster grids.\n\u003e - Generates a single mesh for a particular detail.\n\u003e - Optimized for quality (as few triangles as possible for a given error).\n\n[hmm]: https://github.com/fogleman/hmm\n[pydelatin]: https://github.com/kylebarron/pydelatin\n[delatin]: https://github.com/mapbox/delatin\n[martini_desc_issue]: https://github.com/mapbox/martini/issues/15#issuecomment-700475731\n\n## Correctness\n\n`pymartini` passes the (only) test case included in the original Martini JS\nlibrary. I also wrote a few extra conformance tests to compare output by\n`pymartini` and Martini. I've found some small differences in float values at\nthe end of the second step.\n\nThis second step, `martini.create_tile(terrain)`, computes the maximum error of\nevery possible triangle and accumulates them. Thus, small float errors appear to\nbe magnified by the summation of errors into larger triangles. These errors\nappear to be within `1e-5` of the JS output. I'm guessing that this variance is\ngreater than normal float rounding errors, due to this summation behavior.\n\nThese differences are larger when using 512px tiles compared to 256px tiles,\nwhich reinforces my hypothesis that the differences have something to do with\nsmall low-level float or bitwise operations differences between Python and\nJavaScript.\n\nIf you'd like to explore this in more detail, look at the `Tile.update()` in\n`martini.pyx` and the corresponding Martini code.\n\n## Type Checking\n\nAs of `pymartini` 0.4.0, types are provided, which can be used with a checker\nlike [`mypy`](https://mypy.readthedocs.io/). If you wish to get the full\nbenefit, make sure to [enable Numpy's mypy\nplugin](https://numpy.org/devdocs/reference/typing.html#examples).\n\n## Benchmark\n\nPreparation steps are about 3x faster in Python than in Node; generating the\nmesh is about 2x faster in Python than in Node.\n\n### Python\n\n```bash\ngit clone https://github.com/kylebarron/pymartini\ncd pymartini\npip install '.[test]'\npython bench.py\n```\n\n```\ninit tileset: 14.860ms\ncreate tile: 5.862ms\nmesh (max_error=30): 1.010ms\nvertices: 9700.0, triangles: 19078.0\nmesh 0: 18.350ms\nmesh 1: 17.581ms\nmesh 2: 15.245ms\nmesh 3: 13.853ms\nmesh 4: 11.284ms\nmesh 5: 12.360ms\nmesh 6: 8.293ms\nmesh 7: 8.342ms\nmesh 8: 7.166ms\nmesh 9: 5.678ms\nmesh 10: 5.886ms\nmesh 11: 5.092ms\nmesh 12: 3.732ms\nmesh 13: 3.420ms\nmesh 14: 3.524ms\nmesh 15: 3.101ms\nmesh 16: 2.892ms\nmesh 17: 2.358ms\nmesh 18: 2.250ms\nmesh 19: 2.293ms\nmesh 20: 2.281ms\n20 meshes total: 155.559ms\n```\n\n### JS (Node)\n\n```bash\ngit clone https://github.com/mapbox/martini\ncd martini\nnpm install\nnode -r esm bench.js\n```\n\n```\ninit tileset: 54.293ms\ncreate tile: 17.307ms\nmesh: 6.230ms\nvertices: 9704, triangles: 19086\nmesh 0: 43.181ms\nmesh 1: 33.102ms\nmesh 2: 30.735ms\nmesh 3: 25.935ms\nmesh 4: 20.643ms\nmesh 5: 17.511ms\nmesh 6: 15.066ms\nmesh 7: 13.334ms\nmesh 8: 11.180ms\nmesh 9: 9.651ms\nmesh 10: 9.240ms\nmesh 11: 10.996ms\nmesh 12: 7.520ms\nmesh 13: 6.617ms\nmesh 14: 5.860ms\nmesh 15: 5.693ms\nmesh 16: 4.907ms\nmesh 17: 4.469ms\nmesh 18: 4.267ms\nmesh 19: 4.267ms\nmesh 20: 3.619ms\n20 meshes total: 290.256ms\n```\n\n## License\n\nThis library is ported from Mapbox's [Martini][martini], which is licensed under\nthe ISC License. My additions are licensed under the MIT license.\n\nISC License\n\nCopyright (c) 2019, Mapbox\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n\n[martini]: https://github.com/mapbox/martini\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylebarron%2Fpymartini","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkylebarron%2Fpymartini","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylebarron%2Fpymartini/lists"}