motmot

A sophisticated mesh class for analysing 3D surfaces.

https://github.com/bwoodsend/motmot

Science Score: 26.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
    Found .zenodo.json file
  • DOI references
  • Academic publication links
  • Committers with academic emails
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (9.7%) to scientific vocabulary
Last synced: 11 months ago · JSON representation

Repository

A sophisticated mesh class for analysing 3D surfaces.

Basic Info
Statistics
  • Stars: 2
  • Watchers: 1
  • Forks: 0
  • Open Issues: 0
  • Releases: 4
Created over 5 years ago · Last pushed 12 months ago
Metadata Files
Readme Changelog Contributing License

README.md

Motmot

A sophisticated mesh class for analysing colourless Polygon meshes such as an STL file providing a seamless abstraction between raw vectors meshes or the more efficient vertices + faces (a.k.a vertices + polygons) form.

  • Free software: MIT license
  • Source code: https://github.com/bwoodsend/motmot/
  • Releases: https://pypi.org/project/motmot/
  • Documentation: https://bwoodsend.github.io/motmot/index.html

Related projects

This library focuses almost exclusively on analysing meshes. It it highly likely that you will need to supplement it with other libraries to read/write to a certain format or to simplify an existing mesh.

  • Mesh read/write:

    • numpy-stl: Reads and writes STL files. This is a dependency of motmot.
    • meshio: Reads and writes a multitude of mesh formats.
    • pymesh: Reads and writes STL and OBJ files.
  • Mesh analysis:

    • PyMesh: A highly sophisticated mesh library which unfortunately depends on some rather hairy C++ libraries, making it not generally installable. It's not even on PyPI.
    • trimesh: Another general purpose mesh library. This one is pure Python and focuses strictly on triangular and preferably closed meshes. It also brings a few readers and writers with it. This library is very powerful. It's quite likely that you'd be better off using it instead of motmot.
  • Mesh cleaning:

    • quadmeshsimplify: Decimate (collapse redundant or near redundant vertices in) meshes to make the filesize much smaller with negligible impact on quality.
    • Py_Fast-Quadric-Mesh-Simplification: Another mesh decimator. This one is much faster but not on PyPI (yet).

Usage

The basic API for motmot is modelled off that of numpy-stl with a few alterations.

Initialisation

Meshes can be :

  1. Constructed from scratch using a single vectors array. This array should be 3D with shape (n, k, 3) where:
* ``n`` is the number of polygons in the mesh,
* ``k`` is the number of corners each polygon has,
* ``3`` corresponds to having 3 axes. i.e. ``x``, ``y`` and ``z``.

```python
# vectors is a (n, 3, 3) numpy array.
triangle_mesh = Mesh(vectors)

# vectors is a (n, 4, 3) numpy array.
square_mesh = Mesh(vectors)
```
  1. Or using the more memory efficient vertices + faces form.

    ```python

    vertices is an array of points. It should contain no duplicates.

    faces is an integer array indicating which vertices are used to construct

    each polygon.

    mesh = Mesh(vertices, faces) ```

  2. Read from an STL file. This uses numpy-stl under the hood. Currently, STL is the only file format that motmot will read implicitly:

    python from motmot import Mesh mesh = Mesh("some-file.stl")

  3. Read from an lzma, gzip or bzip2 compressed STL file:

    ```python from motmot import Mesh

    An lzma compressed STL file. Create using xz some-file.stl in bash.

    mesh = Mesh("some-file.stl.xz")

    A gzip compressed STL file. Create using gzip some-file.stl in bash.

    mesh = Mesh("some-file.stl.gz")

    A bzip2 compressed STL file. Create using bzip2 some-file.stl in bash.

    mesh = Mesh("some-file.stl.bz2") ```

  4. Stream from any subclass of io.RawIOBase. From here you can read from arbitrary sources such as embedded files, streams, archives or other pseudo files. For example, the following reads an STL directly from a web request:

    ```python from urllib import request from motmot import Mesh

    Pull an STL file from the internet and load it without an intermediate

    temporary file.

    url = "https://raw.githubusercontent.com/bwoodsend/vtkplotlib/master/" \ "vtkplotlib/data/models/rabbit/rabbit.stl"

    with request.urlopen(url) as req: mesh = Mesh(req) ```

Vertices + Faces meshes vs Vectors meshes

There are two forms of mesh.

  1. A vectors mesh is essentially a list of polygons where each polygon is a list of points (its corners) and each point is an (x, y, z) triplet. This form is simple but wasteful because points which appear in multiple polygons are written multiple times which wastes memory and rendering time.

  2. A vertices+faces mesh takes all the unique points from a vectors mesh, calling them the vertices, then replaces each point in vectors with its index from vertices, calling this faces. Note that faces is often also known as facets or polygons.

Motmot makes the two forms interchangeable. Each of vectors, vertices and faces are all available as attributes on all meshes but, depending on how a mesh is constructed, vectors may be internally derived from vertices and faces or vice-versa.

```python import numpy as np from motmot import Mesh

Define the 8 vertices in a cuboid.

vertices = np.array([ [0., 0., 0.], [3., 0., 0.], [0., 5., 0.], [3., 5., 0.], [0., 0., 9.], [3., 0., 9.], [0., 5., 9.], [3., 5., 9.], ])

Define the 6 sides of a cube or cuboid.

faces = np.array([ # Draw a square using vertices[6], vertices[2], vertices[0] and vertices[4] [6, 2, 0, 4], # Draw a square using vertices[0], vertices[1], vertices[5] and vertices[4] [0, 1, 5, 4], # And so on... [0, 2, 3, 1], [5, 1, 3, 7], [3, 2, 6, 7], [4, 5, 7, 6], ])

Construct a vertices+faces mesh.

mesh = Mesh(vertices, faces)

This attribute is set to True to signify that this was originally a faces mesh.

mesh.isfacesmesh

Although vectors can still be derived automatically.

mesh.vectors

Construct a vectors mesh.

mesh = Mesh(vertices[faces])

This attribute is set to False to signify that this was originally a vectors

mesh.

mesh.isfacesmesh

But vertices and faces can still be derived automatically.

mesh.vertices, mesh.faces ```

Mesh properties

This is just a brief summary of what is available. See the corresponding entry in the the API reference for more information on each property.

```python

Outward normal to each polygon:

mesh.normals array([[-45., 0., 0.], [ -0., -27., -0.], [ -0., -0., -15.], [ 45., 0., -0.], [ -0., 27., 0.], [ 0., 0., 15.]])

Normalised (magnitude of 1.0) outward normal to each polygon:

mesh.units array([[-1., 0., 0.], [ 0., -1., 0.], [ 0., 0., -1.], [ 1., 0., 0.], [ 0., 1., 0.], [ 0., 0., 1.]])

Area of each polygon.

mesh.areas array([45., 27., 15., 45., 27., 15.])

Total surface area (just a sum of mesh.areas).

mesh.area 174.0

The number of times each vertex is used (which admittedly

isn't particularly interesting for a cuboid):

mesh.vertex_counts array([3, 3, 3, 3, 3, 3, 3, 3], dtype=int32)

A mapping of which other vertices each vertex is directly connect to.

mesh.vertexmap RaggedArray.fromnested([ [1, 7, 3], # vertices[0] connects to vertices[[1, 7, 3]]. [2, 6, 0], # vertices[1] connects to vertices[[2, 6, 0]]. [4, 1, 3], # and so on... [5, 0, 2], [5, 6, 2], [4, 7, 3], [1, 4, 7], [0, 5, 6], ])

Because this mesh's vertices appear the same number of times,

this example slightly trivialises the problem. Consider instead

a mesh with only the first three faces. Not all vertices have

the same number of neighbours.

mesh[:3].vertexmap RaggedArray.fromnested([ [1, 3], [2, 6, 0], [4, 1, 3], [0, 2, 5], [5, 2, 6], [3, 4], [4, 1], ])

If you prefer to use raw vertices rather than vertex IDs then

use the connected_vertices() method.

mesh.connected_vertices(mesh.vertices[0]) array([[0., 5., 0.], [3., 5., 9.], [0., 0., 9.]])

Similarly, polygon_map maps every polygon to each of its neighbours.

Read the first line of the following as *polygon 0 shares an edge each with

polygons 4, 2, 1 and 5*.

mesh.polygon_map array([[4, 2, 1, 5], [2, 3, 5, 0], [0, 4, 3, 1], [1, 2, 4, 5], [2, 0, 5, 3], [1, 3, 4, 0]]) ```

Vertex Lookup

motmot leverages two libraries for looking up vertices.

Exact lookup

It is easy to convert vertex IDs to real vertices. Simply pass them as indices to mesh.vertices.

```python

ids = [0, 4, 5, 2] points = mesh.vertices[ids] points array([[0., 0., 0.], [0., 0., 9.], [3., 0., 9.], [0., 5., 0.]]) ```

Go the other way by indexing the vertex_table attribute.

```python

mesh.vertex_table[points] array([0, 4, 5, 2], dtype=int64) ```

Some things to be aware of:

  • The dtype of the points queried must match mesh.dtype.

  • As with regular floats in a regular Python dict, even the smallest deviation will cause lookup to fail.

    ```python

    mesh.vertextable[[3., 0., 9.]] 5 mesh.vertextable[[3., 0, 9.00000000001]] KeyError: 'key = array([3., 0., 9.]) is not in this table.' ```

Approximate lookup

To find nearest points, motmot uses a KDTree. The API here is very shallow and it is quite likely that you may wish to create and use KDTrees directly rather than use motmot's methods.

A KDTree, fitted to mesh.centers (the middle of each polygon), is found at the mesh.kdtree attribute.

Given a set of points defined as: python points = np.array([[2., 3.5, 4.2], [2.3, 4.2, 1.1]], mesh.dtype)

Find the nearest point on the mesh surface to each point:

```python

mesh.closest_point(points) array([[3. , 3.5, 4.2], [2.3, 4.2, 0. ]]) `` Or to restrict the output to onlymesh.centers` without interpolating between them:

```python

mesh.closest_point(points, interpolate=False) array([[3. , 2.5, 4.5], [1.5, 2.5, 0. ]]) ```

For anything else, use mesh.kdtree directly.

Laziness

A motmot.Mesh lazy loads its properties using a backport of @functools.cached_property. This allows them to be calculated when only you need them so that no time is ever wasted calculating something which you do not use. Take for example, mesh.normals. Nothing is calculated on mesh = Mesh(vertices, faces) so that if the normals are never used then they are never calculated. Accessing the attribute mesh.normals initialises and returns them making mesh.normals look like a regular attribute on the outside. The value is cached so that the calculation never runs more than once. i.e. mesh.normals is mesh.normals.

Caches should be reset after a mesh is modified. Most of this is done automatically. Mesh modifier methods such as rotate(), translate() or crop(in_place=True) will all invalidate affected caches themselves. Similarly, setting any of the vertices, faces or vectors attributes will reset all caches. Writing in place to those arrays (e.g. mesh.vectors[:] = x) however is undetectable to motmot. Call mesh.reset() after doing an in place modification.

Owner

  • Name: Brénainn Woodsend
  • Login: bwoodsend
  • Kind: user
  • Location: Scotland

Software engineer and/or maths graduate. Specialises in Python with some C/C++. Currently mixing AI into dentistry. Name is pronounced Bren-an Woods-end.

GitHub Events

Total
  • Delete event: 1
  • Push event: 3
  • Create event: 1
Last Year
  • Delete event: 1
  • Push event: 3
  • Create event: 1

Committers

Last synced: almost 3 years ago

All Time
  • Total Commits: 99
  • Total Committers: 1
  • Avg Commits per committer: 99.0
  • Development Distribution Score (DDS): 0.0
Past Year
  • Commits: 0
  • Committers: 0
  • Avg Commits per committer: 0.0
  • Development Distribution Score (DDS): 0.0
Top Committers
Name Email Commits
bwoodsend b****d@g****m 99

Issues and Pull Requests

Last synced: 12 months ago

All Time
  • Total issues: 0
  • Total pull requests: 0
  • Average time to close issues: N/A
  • Average time to close pull requests: N/A
  • Total issue authors: 0
  • Total pull request authors: 0
  • Average comments per issue: 0
  • Average comments per pull request: 0
  • Merged pull requests: 0
  • Bot issues: 0
  • Bot pull requests: 0
Past Year
  • Issues: 0
  • Pull requests: 0
  • Average time to close issues: N/A
  • Average time to close pull requests: N/A
  • Issue authors: 0
  • Pull request authors: 0
  • Average comments per issue: 0
  • Average comments per pull request: 0
  • Merged pull requests: 0
  • Bot issues: 0
  • Bot pull requests: 0
Top Authors
Issue Authors
Pull Request Authors
Top Labels
Issue Labels
Pull Request Labels

Packages

  • Total packages: 1
  • Total downloads:
    • pypi 107 last-month
  • Total dependent packages: 0
  • Total dependent repositories: 1
  • Total versions: 5
  • Total maintainers: 1
pypi.org: motmot

A sophisticated mesh class for analysing 3D surfaces.

  • Versions: 5
  • Dependent Packages: 0
  • Dependent Repositories: 1
  • Downloads: 107 Last month
Rankings
Dependent packages count: 10.0%
Dependent repos count: 21.7%
Average: 24.3%
Stargazers count: 27.8%
Forks count: 29.8%
Downloads: 32.3%
Maintainers (1)
Last synced: 12 months ago

Dependencies

.github/workflows/build-wheels.yml actions
  • actions/checkout v4 composite
  • actions/setup-python v5 composite
  • actions/upload-artifact v3 composite
  • bwoodsend/setup-winlibs-action v1 composite
.github/workflows/github-pages.yml actions
  • actions/checkout v4 composite
  • actions/setup-python v5 composite
  • peaceiris/actions-gh-pages v3 composite
.github/workflows/test.yml actions
  • actions/checkout v4 composite
  • actions/setup-python v5 composite
  • bwoodsend/setup-winlibs-action v1 composite
Dockerfile docker
  • quay.io/pypa/${BASE} latest build
docs/requirements.txt pypi
  • Sphinx ==4.4.0
  • docutils ==0.16
  • m2r2 ==0.3.2
  • sphinx-autodoc-typehints ==1.11.1
  • sphinx-copybutton ==0.3.3
  • sphinx-rtd-theme ==0.5.0
  • sphinxcontrib-applehelp ==1.0.2
  • sphinxcontrib-devhelp ==1.0.2
  • sphinxcontrib-htmlhelp ==2.0.0
  • sphinxcontrib-jsmath ==1.0.1
  • sphinxcontrib-qthelp ==1.0.3
  • sphinxcontrib-serializinghtml ==1.1.5
  • sphinxcontrib-websupport ==1.1.0
pyproject.toml pypi
requirements-dev.txt pypi
setup.py pypi