Repository: pyvista/fast-simplification Branch: main Commit: 05315c72f401 Files: 36 Total size: 119.9 KB Directory structure: gitextract_q5jmbpat/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── testing-and-deployment.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── doc/ │ ├── Makefile │ ├── _templates/ │ │ └── autosummary/ │ │ └── class.rst │ ├── api.rst │ ├── conf.py │ └── index.rst ├── examples/ │ ├── README.txt │ ├── replay.py │ └── simplify.py ├── fast_simplification/ │ ├── Replay.h │ ├── Simplify.h │ ├── __init__.py │ ├── _replay.pyx │ ├── _simplify.pyx │ ├── _version.py │ ├── fast_simplification.py │ ├── replay.py │ ├── simplify.py │ ├── utils.py │ ├── wrapper.h │ └── wrapper_replay.h ├── pyproject.toml ├── pytest.ini ├── requirements_docs.txt ├── requirements_test.txt ├── setup.py ├── tests/ │ ├── test_map_isolated_points.py │ ├── test_replay.py │ └── test_simplify.py └── tools/ └── audit_wheel.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: pip directory: / insecure-external-code-execution: allow schedule: interval: monthly open-pull-requests-limit: 100 labels: - maintenance - dependencies groups: pip: patterns: - '*' - package-ecosystem: github-actions directory: / schedule: interval: monthly open-pull-requests-limit: 100 labels: - maintenance - dependencies groups: actions: patterns: - '*' ================================================ FILE: .github/workflows/testing-and-deployment.yml ================================================ name: Build on: pull_request: push: tags: - v* branches: - main # disable concurrent runs concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # fail fast and early to avoid clogging GH Actions smoke_testing: runs-on: ubuntu-latest name: Source distribution testing steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.13' - name: Setup headless display uses: pyvista/setup-headless-display-action@v4 - name: Build and validate wheel run: | pip install build twine python -m build twine check dist/* - name: Install run: pip install dist/*.whl - name: Test run: | pip install -r requirements_test.txt cd tests && python -m pytest -v - name: Upload sdist uses: actions/upload-artifact@v7 with: path: ./dist/*.tar.gz name: fast-simplification-sdist docs_build: name: Build Documentation runs-on: ubuntu-latest needs: smoke_testing steps: - uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.13' - name: Setup headless display uses: pyvista/setup-headless-display-action@v4 - name: Install library run: pip install . - name: Build Documentation run: | pip install -r requirements_docs.txt make -C doc html - name: Deploy on tag uses: JamesIves/github-pages-deploy-action@v4 if: startsWith(github.ref, 'refs/tags/') with: token: ${{ secrets.GITHUB_TOKEN }} branch: gh-pages folder: doc/_build/html build_wheels: needs: smoke_testing name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest, macos-15-intel] steps: - uses: actions/checkout@v6 - name: Build wheels uses: pypa/cibuildwheel@v3.4.0 - name: List generated wheels run: ls ./wheelhouse/* - uses: actions/upload-artifact@v7 with: path: ./wheelhouse/*.whl name: fast-simplification-wheel-${{ matrix.os }} release: name: Release if: github.event_name == 'push' && contains(github.ref, 'refs/tags') needs: [build_wheels, docs_build] runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/fast-simplification permissions: id-token: write # this permission is mandatory for trusted publishing contents: write # required to create a release steps: - uses: actions/download-artifact@v8 - name: Flatten directory structure run: | mkdir -p dist/ find . -name '*.whl' -exec mv {} dist/ \; find . -name '*.tar.gz' -exec mv {} dist/ \; - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: generate_release_notes: true files: | ./**/*.whl ================================================ FILE: .gitignore ================================================ # Compiled source # ################### *.pyc *.pyd *.so *.o __pycache__/ # Pip *.egg-info # Cython generated files # fast_simplification/_simplify.cpp fast_simplification/_replay.cpp # documentation doc/_build/ doc/examples/ doc/_autosummary/ dist/ venv/ build/ # IDE .vscode/ ================================================ FILE: .pre-commit-config.yaml ================================================ # Integration with GitHub Actions # See https://pre-commit.ci/ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] exclude: ^(docs/|tests) - id: ruff-format - repo: https://github.com/pycqa/isort rev: 8.0.1 hooks: - id: isort - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell args: [--toml, pyproject.toml] additional_dependencies: [tomli] - repo: https://github.com/keewis/blackdoc rev: v0.4.6 hooks: - id: blackdoc files: \.py$ # - repo: https://github.com/pycqa/pydocstyle # rev: 6.1.1 # hooks: # - id: pydocstyle # additional_dependencies: [toml] # exclude: "tests/" - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-merge-conflict - id: debug-statements - id: trailing-whitespace - id: no-commit-to-branch args: [--branch, main] - id: requirements-txt-fixer - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.16.0 hooks: - id: pretty-format-toml args: [--autofix] - id: pretty-format-yaml args: [--autofix, --indent, '2'] # this validates our github workflow files - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.37.1 hooks: - id: check-github-workflows ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2017-2021 The PyVista Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include fast_simplification/*.h ================================================ FILE: README.rst ================================================ Python Fast-Quadric-Mesh-Simplification Wrapper =============================================== This is a python wrapping of the `Fast-Quadric-Mesh-Simplification Library `_. Having arrived at the same problem as the original author, but needing a Python library, this project seeks to extend the work of the original library while adding integration to Python and the `PyVista `_ project. For the full documentation visit: https://pyvista.github.io/fast-simplification/ .. image:: https://github.com/pyvista/fast-simplification/raw/main/doc/images/simplify_demo.png Installation ------------ Fast Simplification can be installed from PyPI using pip on Python >= 3.7:: pip install fast-simplification See the `Contributing `_ for more details regarding development or if the installation through pip doesn't work out. Basic Usage ----------- The basic interface is quite straightforward and can work directly with arrays of points and triangles: .. code:: python points = [[ 0.5, -0.5, 0.0], [ 0.0, -0.5, 0.0], [-0.5, -0.5, 0.0], [ 0.5, 0.0, 0.0], [ 0.0, 0.0, 0.0], [-0.5, 0.0, 0.0], [ 0.5, 0.5, 0.0], [ 0.0, 0.5, 0.0], [-0.5, 0.5, 0.0]] faces = [[0, 1, 3], [4, 3, 1], [1, 2, 4], [5, 4, 2], [3, 4, 6], [7, 6, 4], [4, 5, 7], [8, 7, 5]] points_out, faces_out = fast_simplification.simplify(points, faces, 0.5) Advanced Usage -------------- This library supports direct integration with VTK through PyVista to provide a simplistic interface to the library. As this library provides a 4-5x improvement to the VTK decimation algorithms. .. code:: python >>> from pyvista import examples >>> mesh = examples.download_nefertiti() >>> out = fast_simplification.simplify_mesh(mesh, target_reduction=0.9) Compare with built-in VTK/PyVista methods: >>> fas_sim = fast_simplification.simplify_mesh(mesh, target_reduction=0.9) >>> dec_std = mesh.decimate(0.9) # vtkQuadricDecimation >>> dec_pro = mesh.decimate_pro(0.9) # vtkDecimatePro >>> pv.set_plot_theme('document') >>> pl = pv.Plotter(shape=(2, 2), window_size=(1000, 1000)) >>> pl.add_text('Original', 'upper_right', color='w') >>> pl.add_mesh(mesh, show_edges=True) >>> pl.camera_position = cpos >>> pl.subplot(0, 1) >>> pl.add_text( ... 'Fast-Quadric-Mesh-Simplification\n~2.2 seconds', 'upper_right', color='w' ... ) >>> pl.add_mesh(fas_sim, show_edges=True) >>> pl.camera_position = cpos >>> pl.subplot(1, 0) >>> pl.add_mesh(dec_std, show_edges=True) >>> pl.add_text( ... 'vtkQuadricDecimation\n~9.5 seconds', 'upper_right', color='w' ... ) >>> pl.camera_position = cpos >>> pl.subplot(1, 1) >>> pl.add_mesh(dec_pro, show_edges=True) >>> pl.add_text( ... 'vtkDecimatePro\n11.4~ seconds', 'upper_right', color='w' ... ) >>> pl.camera_position = cpos >>> pl.show() Comparison to other libraries ----------------------------- The `pyfqmr `_ library wraps the same header file as this library and has similar capabilities. In this library, the decision was made to write the Cython layer on top of an additional C++ layer rather than directly interfacing with wrapper from Cython. This results in a mild performance improvement. Reusing the example above: .. code:: python Set up a timing function. >>> import pyfqmr >>> vertices = mesh.points >>> faces = mesh.faces.reshape(-1, 4)[:, 1:] >>> def time_pyfqmr(): ... mesh_simplifier = pyfqmr.Simplify() ... mesh_simplifier.setMesh(vertices, faces) ... mesh_simplifier.simplify_mesh( ... target_count=out.n_faces, aggressiveness=7, verbose=0 ... ) ... vertices_out, faces_out, normals_out = mesh_simplifier.getMesh() ... return vertices_out, faces_out, normals_out Now, time it and compare with the non-VTK API of this library: .. code:: python >>> timeit time_pyfqmr() 2.75 s ± 5.35 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) >>> timeit vout, fout = fast_simplification.simplify(vertices, faces, 0.9) 2.05 s ± 3.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Additionally, the ``fast-simplification`` library has direct plugins to the ``pyvista`` library, making it easy to read and write meshes: .. code:: python >>> import pyvista >>> import fast_simplification >>> mesh = pyvista.read('my_mesh.stl') >>> simple = fast_simplification.simplify_mesh(mesh) >>> simple.save('my_simple_mesh.stl') Since both libraries are based on the same core C++ code, feel free to use whichever gives you the best performance and interoperability. Replay decimation functionality ------------------------------- This library also provides an interface to keep track of the successive collapses that occur during the decimation process and to replay the decimation process. This can be useful for different applications, such as: * applying the same decimation to a collection of meshes that share the same topology * computing a correspondence map between the vertices of the original mesh and the vertices of the decimated mesh, to transfer field data from one to the other for example * replaying the decimation process with a smaller target reduction than the original one, faster than decimating the original mesh with the smaller target reduction To use this functionality, you need to set the ``return_collapses`` parameter to ``True`` when calling ``simplify``. This will return the successive collapses of the decimation process in addition to points and faces. .. code:: python >>> import fast_simplification >>> import pyvista >>> mesh = pyvista.Sphere() >>> points, faces = mesh.points, mesh.faces.reshape(-1, 4)[:, 1:] >>> points_out, faces_out, collapses = fast_simplification.simplify(points, faces, 0.9, return_collapses=True) Now you can call ``replay_simplification`` to replay the decimation process and obtain the mapping between the vertices of the original mesh and the vertices of the decimated mesh. .. code:: python >>> points_out, faces_out, indice_mapping = fast_simplification.replay_simplification(points, faces, collapses) >>> i = 3 >>> print(f'Vertex {i} of the original mesh is mapped to {indice_mapping[i]} of the decimated mesh') You can also use the ``replay_simplification`` function to replay the decimation process with a smaller target reduction than the original one. This is faster than decimating the original mesh with the smaller target reduction. To do so, you need to pass a subset of the collapses to the ``replay_simplification`` function. For example, to replay the decimation process with a target reduction of 50% the initial rate, you can run: .. code:: python >>> import numpy as np >>> collapses_half = collapses[:int(0.5 * len(collapses))] >>> points_out, faces_out, indice_mapping = fast_simplification.replay_simplification(points, faces, collapses_half) If you have a collection of meshes that share the same topology, you can apply the same decimation to all of them by calling ``replay_simplification`` with the same collapses for each mesh. This ensure that the decimated meshes will share the same topology. .. code:: python >>> import numpy as np >>> # Assume that you have a collection of meshes stored in a list meshes >>> _, _, collapses = fast_simplification.simplify(meshes[0].points, meshes[0].faces, ... 0.9, return_collapses=True) >>> decimated_meshes = [] >>> for mesh in meshes: ... points_out, faces_out, _ = fast_simplification.replay_simplification(mesh.points, mesh.faces, collapses) ... decimated_meshes.append(pyvista.PolyData(points_out, faces_out)) Contributing ------------ Contribute to this repository by forking this repository and installing in development mode with:: git clone https://github.com//fast-simplification pip install -e . pip install -r requirements_test.txt You can then add your feature or commit your bug fix and then run your unit testing with:: pytest Unit testing will automatically enforce minimum code coverage standards. Next, to ensure your code meets minimum code styling standards, run:: pip install pre-commit pre-commit run --all-files Finally, `create a pull request`_ from your fork and I'll be sure to review it. .. _create a pull request: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request ================================================ FILE: doc/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = pymeshfix SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: rm -rf $(BUILDDIR)/* rm -rf examples/ find . -type d -name "_autosummary" -exec rm -rf {} + ================================================ FILE: doc/_templates/autosummary/class.rst ================================================ {{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} {% block methods %} {% if methods %} .. rubric:: {{ _('Methods') }} .. autosummary:: :toctree: {% for item in methods %} {% if item != "__init__" %} {{ name }}.{{ item }} {% endif %} {%- endfor %} {% endif %} {% endblock %} {% block attributes %} {% if attributes %} .. rubric:: {{ _('Attributes') }} .. autosummary:: :toctree: {% for item in attributes %} {% if item.0 != item.upper().0 %} {{ name }}.{{ item }} {% endif %} {%- endfor %} {% endif %} {% endblock %} ================================================ FILE: doc/api.rst ================================================ API Reference ============= These are the three public methods that expose the fast-simplification API to Python. .. currentmodule:: fast_simplification .. autosummary:: :toctree: _autosummary simplify simplify_mesh replay_simplification ================================================ FILE: doc/conf.py ================================================ import datetime import os import numpy as np import pyvista from sphinx_gallery.sorting import FileNameSortKey from fast_simplification import __version__ # Manage errors pyvista.set_error_output_file("errors.txt") # Ensure that offscreen rendering is used for docs generation pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy # Preferred plotting style for documentation pyvista.set_plot_theme("document") pyvista.global_theme.window_size = np.array([1024, 768]) * 2 # Save figures in specified directory pyvista.FIGURE_PATH = os.path.abspath("./images/") if not os.path.exists(pyvista.FIGURE_PATH): os.makedirs(pyvista.FIGURE_PATH) pyvista.BUILDING_GALLERY = True # -- Project information ----------------------------------------------------- project = "fast-simplification" year = datetime.date.today().year copyright = f"2017-{year}, The PyVista Developers" author = "Alex Kaszynski" # The short X.Y version version = release = __version__ # -- General configuration --------------------------------------------------- html_logo = "./_static/pyvista_logo_sm.png" # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", # 'sphinx.ext.doctest', "sphinx.ext.autosummary", "notfound.extension", "sphinx_copybutton", "sphinx_gallery.gen_gallery", "sphinx.ext.extlinks", "numpydoc", ] numpydoc_show_class_members = False # numpydoc_class_members_toctree = False html_static_path = ["_static"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # Copy button customization --------------------------------------------------- # exclude traditional Python prompts from the copied code copybutton_prompt_text = r">>> ?|\.\.\. " copybutton_prompt_is_regexp = True # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" html_context = { # Enable the "Edit in GitHub link within the header of each page. "display_github": True, "github_user": "pyvista", "github_repo": "fast-simplification", "github_version": "master", "menu_links_name": "Getting Connected", "menu_links": [ ( ' Slack Community', "http://slack.pyvista.org", ), ( ' Support', "https://github.com/pyvista/pyvista-support", ), ( ' Source Code', "https://github.com/pyvista/pymeshfix", ), ], } html_theme_options = { "show_prev_next": False, "github_url": "https://github.com/pyvista/pymeshfix", } # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "PyMeshFix" # -- Options for LaTeX output ------------------------------------------------ latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "pymeshfix.tex", "PyMeshFix Documentation", "Alex Kaszynski", "manual", ), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "PyMeshFix", "PyMeshFix Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "PyMeshFix", "PyMeshFix Documentation", author, "PyMeshFix", "One line description of project.", "Miscellaneous", ), ] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "https://docs.python.org/": None, "https://docs.pyvista.org": None, } # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Sphinx Gallery Options sphinx_gallery_conf = { # path to your examples scripts "examples_dirs": [ "../examples/", ], # path where to save gallery generated examples "gallery_dirs": ["examples"], # pattern to search for example files "filename_pattern": r"\.py", # Remove the "Download all examples" button from the top level gallery "download_all_examples": False, # Sort gallery example by file name instead of number of lines (default) "within_subsection_order": FileNameSortKey, # directory where function granular galleries are stored "backreferences_dir": None, # Modules for which function level galleries are created. In "doc_module": "pymeshfix", "image_scrapers": (pyvista.Scraper(), "matplotlib"), "thumbnail_size": (350, 350), } # -- Custom 404 page notfound_no_urls_prefix = True ================================================ FILE: doc/index.rst ================================================ .. include:: ../README.rst .. toctree:: :hidden: self .. toctree:: :maxdepth: 2 :caption: Examples :hidden: examples/index .. toctree:: :maxdepth: 2 :caption: API Reference :hidden: api ================================================ FILE: examples/README.txt ================================================ Fast-simplification Examples ============================ The following examples demonstrate ``fast-simplification`` functionality using `pyvista `__. ================================================ FILE: examples/replay.py ================================================ """ Replay Decimation ----------------- This example shows how to replay a decimation sequence with replay. """ from time import time import numpy as np import pyvista as pv from pyvista import examples import fast_simplification # Ancillary function to convert triangles to padded faces def triangles_to_faces(triangles): tmp = 3 * np.ones((len(triangles), 4), dtype=triangles.dtype) tmp[:, 1:] = triangles return tmp.copy().reshape(-1) # mesh = examples.download_cow().triangulate().clean() # cpos = [(12.81184076782852, 0.2698100334791761, -10.82840852844307), # (-5.767085129340097, -0.45822321783537723, 6.935179459234972), # (-0.031039486276564762, 0.99948202301343, 0.008499174352116386)] # load an example mesh mesh = examples.download_louis_louvre() # nice camera angle cpos = [ (5.428820015861438, -10.151721995577468, 15.902198956656623), (1.4405146331169636, 2.897371104075222, 10.951469667556948), (-0.01001925846458282, 0.3520252491158569, 0.9359368773826251), ] points = mesh.points triangles = mesh.faces.reshape(-1, 4)[:, 1:] # Decimate the mesh with fast_simplification # and record the collapses start = time() dec_points, dec_triangles, collapses = fast_simplification.simplify( points, triangles, 0.995, return_collapses=True ) time_simplify = time() - start # Replay the decimation sequence and record the mapping between # the original points and the decimated points start = time() ( dec_points_replay, dec_triangles_replay, indice_mapping_replay, ) = fast_simplification.replay_simplification( points=points, triangles=triangles, collapses=collapses, ) time_replay_new = time() - start # Partially replay the decimation sequence (90% of the collapses are replayed) partial_collapses = collapses[0 : int(0.9 * len(collapses))] start = time() ( dec_points_replay2, dec_triangles_replay2, indice_mapping_replay2, ) = fast_simplification.replay_simplification( points=points, triangles=triangles, collapses=partial_collapses ) time_replay_new = time() - start # Randomly select two points on the original mesh np.random.seed(1) i, j = np.random.randint(0, len(mesh.points), 2) # Map the indices of the original points to the indices of the decimated points m_i = indice_mapping_replay[i] m_j = indice_mapping_replay[j] m_i2 = indice_mapping_replay2[i] m_j2 = indice_mapping_replay2[j] p = pv.Plotter(shape=(2, 2), theme=pv.themes.DocumentTheme()) p.subplot(0, 0) # Plot the original mesh with the two highlighted points p.add_mesh(mesh, show_edges=True, color="tan") p.add_points(mesh.points[i], color="red", point_size=10, render_points_as_spheres=True) p.add_points(mesh.points[j], color="blue", point_size=10, render_points_as_spheres=True) p.add_text( f"Original mesh, {mesh.points.shape[0]} vertices, {triangles.shape[0]} triangles", font_size=10, ) p.camera_position = cpos p.subplot(0, 1) # Plot the decimated mesh p.add_mesh( pv.PolyData(dec_points, faces=triangles_to_faces(dec_triangles)), show_edges=True, color="tan", ) p.add_text( f"Decimated mesh, {dec_points.shape[0]} vertices, {dec_triangles.shape[0]} triangles, took {time_simplify:.2f}s", font_size=10, ) p.camera_position = cpos p.subplot(1, 0) # Plot the mesh decimated with replay with the two highlighted points p.add_mesh( pv.PolyData(dec_points_replay, faces=triangles_to_faces(dec_triangles_replay)), show_edges=True, color="tan", ) p.add_points( dec_points_replay[m_i], color="red", point_size=10, render_points_as_spheres=True, ) p.add_points( dec_points_replay[m_j], color="blue", point_size=10, render_points_as_spheres=True, ) n_points, n_triangles = dec_points_replay.shape[0], dec_triangles_replay.shape[0] p.add_text( f"Replay, {n_points} vertices, {n_triangles} triangles, took {time_replay_new:.2f}s", font_size=10, ) p.camera_position = cpos p.subplot(1, 1) # Plot the mesh partially decimated with replay with the two highlighted points p.add_mesh( pv.PolyData(dec_points_replay2, faces=triangles_to_faces(dec_triangles_replay2)), show_edges=True, color="tan", ) p.add_points( dec_points_replay2[m_i2], color="red", point_size=10, render_points_as_spheres=True, ) p.add_points( dec_points_replay2[m_j2], color="blue", point_size=10, render_points_as_spheres=True, ) n_points, n_triangles = dec_points_replay2.shape[0], dec_triangles_replay2.shape[0] p.add_text( f"Partial replay, {n_points} vertices, {n_triangles} triangles, took {time_replay_new:.2f}s", font_size=10, ) p.camera_position = cpos p.show() ================================================ FILE: examples/simplify.py ================================================ """ Compare Decimation Methods -------------------------- This example compares various decimation methods """ import time import pyvista as pv from pyvista import examples import fast_simplification # load an example mesh mesh = examples.download_louis_louvre() # nice camera angle cpos = [ (6.264157141857314, -6.959267635766402, 11.71668951132694), (1.3291685457683413, 2.267162128740896, 12.263240938610595), (0.0023825740958850136, -0.05786378450796799, 0.9983216444528751), ] ############################################################################### # Compare decimation times reduction = 0.9 print("Approach Time Elapsed") tstart = time.time() fas_sim = fast_simplification.simplify_mesh(mesh, target_reduction=reduction) fast_sim_time = time.time() - tstart print(f"Fast Quadratic Simplification {fast_sim_time:8.4f} seconds") tstart = time.time() dec_std = mesh.decimate(reduction) dec_std_time = time.time() - tstart print(f"vtkQuadricDecimation {dec_std_time:8.4f} seconds") tstart = time.time() dec_pro = mesh.decimate_pro(reduction) dec_pro_time = time.time() - tstart print(f"vtkDecimatePro {dec_pro_time:8.4f} seconds") pl = pv.Plotter(shape=(2, 2), window_size=(1000, 1000), theme=pv.themes.DocumentTheme()) pl.add_text("Original", "upper_right", color="k") pl.add_mesh(mesh, show_edges=True) pl.camera_position = cpos pl.subplot(0, 1) pl.add_text( f"Fast-Quadric-Mesh-Simplification\n{fast_sim_time:8.4f} seconds", "upper_right", color="k", ) pl.add_mesh(fas_sim, show_edges=True) pl.camera_position = cpos pl.subplot(1, 0) pl.add_mesh(dec_std, show_edges=True) pl.add_text(f"vtkQuadricDecimation\n{dec_std_time:8.4f} seconds", "upper_right", color="k") pl.camera_position = cpos pl.subplot(1, 1) pl.add_mesh(dec_pro, show_edges=True) pl.add_text(f"vtkDecimatePro\n{dec_pro_time:8.4f} seconds", "upper_right", color="k") pl.camera_position = cpos pl.show() ================================================ FILE: fast_simplification/Replay.h ================================================ #include "Simplify.h" namespace Replay{ // Global Variables & Structures (same as for Simplify) enum Attributes { NONE, NORMAL = 2, TEXCOORD = 4, COLOR = 8 }; struct Triangle { int v[3];double err[4];int deleted,dirty,attr;vec3f n;vec3f uvs[3];int material; }; struct Vertex { vec3f p;int tstart,tcount;SymetricMatrix q;int border;}; struct Ref { int tid,tvertex; }; std::vector triangles; std::vector vertices; std::vector refs; std::string mtllib; std::vector materials; std::vector> collapses; // Helper functions double vertex_error(SymetricMatrix q, double x, double y, double z); double calculate_error(int id_v1, int id_v2, vec3f &p_result); void initialize_quadrics(); void replay_simplification() { // init for(int i=0; i vcount,vids; loopi(0,vertices.size()) vertices[i].border=0; loopi(0,vertices.size()) { Vertex &v=vertices[i]; vcount.clear(); vids.clear(); loopj(0,v.tcount) { int k=refs[v.tstart+j].tid; Triangle &t=triangles[k]; loopk(0,3) { int ofs=0,id=t.v[k]; while(ofs try to find best result vec3f p1=vertices[id_v1].p; vec3f p2=vertices[id_v2].p; vec3f p3=(p1+p2)/2; double error1 = vertex_error(q, p1.x,p1.y,p1.z); double error2 = vertex_error(q, p2.x,p2.y,p2.z); double error3 = vertex_error(q, p3.x,p3.y,p3.z); error = min(error1, min(error2, error3)); if (error1 == error) p_result=p1; if (error2 == error) p_result=p2; if (error3 == error) p_result=p3; } return error; } char *trimwhitespace(char *str) { char *end; // Trim leading space while(isspace((unsigned char)*str)) str++; if(*str == 0) // All spaces? return str; // Trim trailing space end = str + strlen(str) - 1; while(end > str && isspace((unsigned char)*end)) end--; // Write new null terminator *(end+1) = 0; return str; } //Option : Load OBJ void load_obj(const char* filename, bool process_uv=false){ vertices.clear(); triangles.clear(); // printf ( "Loading Objects %s ... \n",filename); FILE* fn; if(filename==NULL) return ; if((char)filename[0]==0) return ; if ((fn = fopen(filename, "rb")) == NULL) { printf ( "File %s not found!\n" ,filename ); return; } char line[1000]; memset ( line,0,1000 ); int vertex_cnt = 0; int material = -1; std::map material_map; std::vector uvs; std::vector > uvMap; while(fgets( line, 1000, fn ) != NULL) { Vertex v; vec3f uv; if (strncmp(line, "mtllib", 6) == 0) { mtllib = trimwhitespace(&line[7]); } if (strncmp(line, "usemtl", 6) == 0) { std::string usemtl = trimwhitespace(&line[7]); if (material_map.find(usemtl) == material_map.end()) { material_map[usemtl] = materials.size(); materials.push_back(usemtl); } material = material_map[usemtl]; } if ( line[0] == 'v' && line[1] == 't' ) { if ( line[2] == ' ' ) if(sscanf(line,"vt %lf %lf", &uv.x,&uv.y)==2) { uv.z = 0; uvs.push_back(uv); } else if(sscanf(line,"vt %lf %lf %lf", &uv.x,&uv.y,&uv.z)==3) { uvs.push_back(uv); } } else if ( line[0] == 'v' ) { if ( line[1] == ' ' ) if(sscanf(line,"v %lf %lf %lf", &v.p.x, &v.p.y, &v.p.z)==3) { vertices.push_back(v); } } int integers[9]; if ( line[0] == 'f' ) { Triangle t; bool tri_ok = false; bool has_uv = false; if(sscanf(line,"f %d %d %d", &integers[0],&integers[1],&integers[2])==3) { tri_ok = true; }else if(sscanf(line,"f %d// %d// %d//", &integers[0],&integers[1],&integers[2])==3) { tri_ok = true; }else if(sscanf(line,"f %d//%d %d//%d %d//%d", &integers[0],&integers[3], &integers[1],&integers[4], &integers[2],&integers[5])==6) { tri_ok = true; }else if(sscanf(line,"f %d/%d/%d %d/%d/%d %d/%d/%d", &integers[0],&integers[6],&integers[3], &integers[1],&integers[7],&integers[4], &integers[2],&integers[8],&integers[5])==9) { tri_ok = true; has_uv = true; }else // Add Support for v/vt only meshes if (sscanf(line, "f %d/%d %d/%d %d/%d", &integers[0], &integers[6], &integers[1], &integers[7], &integers[2], &integers[8]) == 6) { tri_ok = true; has_uv = true; } else { printf("unrecognized sequence\n"); printf("%s\n",line); while(1); } if ( tri_ok ) { t.v[0] = integers[0]-1-vertex_cnt; t.v[1] = integers[1]-1-vertex_cnt; t.v[2] = integers[2]-1-vertex_cnt; t.attr = 0; if ( process_uv && has_uv ) { std::vector indices; indices.push_back(integers[6]-1-vertex_cnt); indices.push_back(integers[7]-1-vertex_cnt); indices.push_back(integers[8]-1-vertex_cnt); uvMap.push_back(indices); t.attr |= TEXCOORD; } t.material = material; //geo.triangles.push_back ( tri ); triangles.push_back(t); //state_before = state; //state ='f'; } } } if ( process_uv && uvs.size() ) { loopi(0,triangles.size()) { loopj(0,3) triangles[i].uvs[j] = uvs[uvMap[i][j]]; } } fclose(fn); //printf("load_obj: vertices = %lu, triangles = %lu, uvs = %lu\n", vertices.size(), triangles.size(), uvs.size() ); } // load_obj() // Optional : Store as OBJ void write_obj(const char* filename) { FILE *file=fopen(filename, "w"); int cur_material = -1; bool has_uv = (triangles.size() && (triangles[0].attr & TEXCOORD) == TEXCOORD); if (!file) { printf("write_obj: can't write data file \"%s\".\n", filename); exit(0); } if (!mtllib.empty()) { fprintf(file, "mtllib %s\n", mtllib.c_str()); } loopi(0,vertices.size()) { //fprintf(file, "v %lf %lf %lf\n", vertices[i].p.x,vertices[i].p.y,vertices[i].p.z); fprintf(file, "v %g %g %g\n", vertices[i].p.x,vertices[i].p.y,vertices[i].p.z); //more compact: remove trailing zeros } if (has_uv) { loopi(0,triangles.size()) if(!triangles[i].deleted) { fprintf(file, "vt %g %g\n", triangles[i].uvs[0].x, triangles[i].uvs[0].y); fprintf(file, "vt %g %g\n", triangles[i].uvs[1].x, triangles[i].uvs[1].y); fprintf(file, "vt %g %g\n", triangles[i].uvs[2].x, triangles[i].uvs[2].y); } } int uv = 1; loopi(0,triangles.size()) if(!triangles[i].deleted) { if (triangles[i].material != cur_material) { cur_material = triangles[i].material; fprintf(file, "usemtl %s\n", materials[triangles[i].material].c_str()); } if (has_uv) { fprintf(file, "f %d/%d %d/%d %d/%d\n", triangles[i].v[0]+1, uv, triangles[i].v[1]+1, uv+1, triangles[i].v[2]+1, uv+2); uv += 3; } else { fprintf(file, "f %d %d %d\n", triangles[i].v[0]+1, triangles[i].v[1]+1, triangles[i].v[2]+1); } //fprintf(file, "f %d// %d// %d//\n", triangles[i].v[0]+1, triangles[i].v[1]+1, triangles[i].v[2]+1); //more compact: remove trailing zeros } fclose(file); } } ================================================ FILE: fast_simplification/Simplify.h ================================================ ///////////////////////////////////////////// // // Mesh Simplification Tutorial // // (C) by Sven Forstmann in 2014 // // License : MIT // http://opensource.org/licenses/MIT // //https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification // // 5/2016: Chris Rorden created minimal version for OSX/Linux/Windows compile #include //#include //#include //#include //#include #include //#include //#include #include #include #include #include #include // std::pair #include #include #include #include //FLT_EPSILON, DBL_EPSILON #define loopi(start_l,end_l) for ( int i=start_l;i1) input=1; return (double) acos ( input ); } inline double angle2( const vec3f& v , const vec3f& w ) { vec3f a = v , b= *this; double dot = a.x*b.x + a.y*b.y + a.z*b.z; double len = a.length() * b.length(); if(len==0)len=1; vec3f plane; plane.cross( b,w ); if ( plane.x * a.x + plane.y * a.y + plane.z * a.z > 0 ) return (double) -acos ( dot / len ); return (double) acos ( dot / len ); } inline vec3f rot_x( double a ) { double yy = cos ( a ) * y + sin ( a ) * z; double zz = cos ( a ) * z - sin ( a ) * y; y = yy; z = zz; return *this; } inline vec3f rot_y( double a ) { double xx = cos ( -a ) * x + sin ( -a ) * z; double zz = cos ( -a ) * z - sin ( -a ) * x; x = xx; z = zz; return *this; } inline void clamp( double min, double max ) { if (xmax) x=max; if (y>max) y=max; if (z>max) z=max; } inline vec3f rot_z( double a ) { double yy = cos ( a ) * y + sin ( a ) * x; double xx = cos ( a ) * x - sin ( a ) * y; y = yy; x = xx; return *this; } inline vec3f invert() { x=-x;y=-y;z=-z;return *this; } inline vec3f frac() { return vec3f( x-double(int(x)), y-double(int(y)), z-double(int(z)) ); } inline vec3f integer() { return vec3f( double(int(x)), double(int(y)), double(int(z)) ); } inline double length() const { return (double)sqrt(x*x + y*y + z*z); } inline vec3f normalize( double desired_length = 1 ) { double square = sqrt(x*x + y*y + z*z); /* if (square <= 0.00001f ) { x=1;y=0;z=0; return *this; }*/ //double len = desired_length / square; x/=square;y/=square;z/=square; return *this; } static vec3f normalize( vec3f a ); static void random_init(); static double random_double(); static vec3f random(); static int random_number; double random_double_01(double a){ double rnf=a*14.434252+a*364.2343+a*4213.45352+a*2341.43255+a*254341.43535+a*223454341.3523534245+23453.423412; int rni=((int)rnf)%100000; return double(rni)/(100000.0f-1.0f); } vec3f random01_fxyz(){ x=(double)random_double_01(x); y=(double)random_double_01(y); z=(double)random_double_01(z); return *this; } }; vec3f barycentric(const vec3f &p, const vec3f &a, const vec3f &b, const vec3f &c){ vec3f v0 = b-a; vec3f v1 = c-a; vec3f v2 = p-a; double d00 = v0.dot(v0); double d01 = v0.dot(v1); double d11 = v1.dot(v1); double d20 = v2.dot(v0); double d21 = v2.dot(v1); double denom = d00*d11-d01*d01; double v = (d11 * d20 - d01 * d21) / denom; double w = (d00 * d21 - d01 * d20) / denom; double u = 1.0 - v - w; return vec3f(u,v,w); } vec3f interpolate(const vec3f &p, const vec3f &a, const vec3f &b, const vec3f &c, const vec3f attrs[3]) { vec3f bary = barycentric(p,a,b,c); vec3f out = vec3f(0,0,0); out = out + attrs[0] * bary.x; out = out + attrs[1] * bary.y; out = out + attrs[2] * bary.z; return out; } double min(double v1, double v2) { return fmin(v1,v2); } class SymetricMatrix { public: // Constructor SymetricMatrix(double c=0) { loopi(0,10) m[i] = c; } SymetricMatrix( double m11, double m12, double m13, double m14, double m22, double m23, double m24, double m33, double m34, double m44) { m[0] = m11; m[1] = m12; m[2] = m13; m[3] = m14; m[4] = m22; m[5] = m23; m[6] = m24; m[7] = m33; m[8] = m34; m[9] = m44; } // Make plane SymetricMatrix(double a,double b,double c,double d) { m[0] = a*a; m[1] = a*b; m[2] = a*c; m[3] = a*d; m[4] = b*b; m[5] = b*c; m[6] = b*d; m[7 ] =c*c; m[8 ] = c*d; m[9 ] = d*d; } double operator[](int c) const { return m[c]; } // Determinant double det( int a11, int a12, int a13, int a21, int a22, int a23, int a31, int a32, int a33) { double det = m[a11]*m[a22]*m[a33] + m[a13]*m[a21]*m[a32] + m[a12]*m[a23]*m[a31] - m[a13]*m[a22]*m[a31] - m[a11]*m[a23]*m[a32]- m[a12]*m[a21]*m[a33]; return det; } const SymetricMatrix operator+(const SymetricMatrix& n) const { return SymetricMatrix( m[0]+n[0], m[1]+n[1], m[2]+n[2], m[3]+n[3], m[4]+n[4], m[5]+n[5], m[6]+n[6], m[ 7]+n[ 7], m[ 8]+n[8 ], m[ 9]+n[9 ]); } SymetricMatrix& operator+=(const SymetricMatrix& n) { m[0]+=n[0]; m[1]+=n[1]; m[2]+=n[2]; m[3]+=n[3]; m[4]+=n[4]; m[5]+=n[5]; m[6]+=n[6]; m[7]+=n[7]; m[8]+=n[8]; m[9]+=n[9]; return *this; } double m[10]; }; /////////////////////////////////////////// namespace Simplify { // Global Variables & Strctures enum Attributes { NONE, NORMAL = 2, TEXCOORD = 4, COLOR = 8 }; struct Triangle { int v[3];double err[4];int deleted,dirty,attr;vec3f n;vec3f uvs[3];int material; }; struct Vertex { vec3f p;int tstart,tcount;SymetricMatrix q;int border;}; struct Ref { int tid,tvertex; }; std::vector triangles; std::vector vertices; std::vector refs; std::string mtllib; std::vector materials; std::vector> collapses; // Helper functions double vertex_error(SymetricMatrix q, double x, double y, double z); double calculate_error(int id_v1, int id_v2, vec3f &p_result); bool flipped(vec3f p,int i0,int i1,Vertex &v0,Vertex &v1,std::vector &deleted); void update_uvs(int i0,const Vertex &v,const vec3f &p,std::vector &deleted); void update_triangles(int i0,Vertex &v,std::vector &deleted,int &deleted_triangles); void update_mesh(int iteration); void compact_mesh(); // // Main simplification function // // target_count : target nr. of triangles // agressiveness : sharpness to increase the threshold. // 5..8 are good numbers // more iterations yield higher quality // void simplify_mesh(int target_count, double agressiveness=7, bool verbose=false) { // init loopi(0,triangles.size()) { triangles[i].deleted=0; } // main iteration loop int deleted_triangles=0; std::vector deleted0,deleted1; int triangle_count=triangles.size(); //int iteration = 0; //loop(iteration,0,100) collapses.clear(); for (int iteration = 0; iteration < 100; iteration ++) { if(triangle_count-deleted_triangles<=target_count)break; // update mesh once in a while if(iteration%5==0) { update_mesh(iteration); } // clear dirty flag loopi(0,triangles.size()) triangles[i].dirty=0; // // All triangles with edges below the threshold will be removed // // The following numbers works well for most models. // If it does not, try to adjust the 3 parameters // double threshold = 0.000000001*pow(double(iteration+3),agressiveness); // target number of triangles reached ? Then break if ((verbose) && (iteration%5==0)) { printf("iteration %d - triangles %d threshold %g\n",iteration,triangle_count-deleted_triangles, threshold); } // remove vertices & mark deleted triangles loopi(0,triangles.size()) { Triangle &t=triangles[i]; if(t.err[3]>threshold) continue; if(t.deleted) continue; if(t.dirty) continue; loopj(0,3)if(t.err[j]({i0,i1})); int tcount=refs.size()-tstart; if(tcount<=v0.tcount) { // save ram if(tcount)memcpy(&refs[v0.tstart],&refs[tstart],tcount*sizeof(Ref)); } else // append v0.tstart=tstart; v0.tcount=tcount; break; } // done? if(triangle_count-deleted_triangles<=target_count)break; } } // clean up mesh compact_mesh(); } //simplify_mesh() void simplify_mesh_lossless(bool verbose=false) { // init loopi(0,triangles.size()) triangles[i].deleted=0; // main iteration loop int deleted_triangles=0; std::vector deleted0,deleted1; int triangle_count=triangles.size(); //int iteration = 0; //loop(iteration,0,100) collapses.clear(); for (int iteration = 0; iteration < 9999; iteration ++) { // update mesh constantly update_mesh(iteration); // clear dirty flag loopi(0,triangles.size()) triangles[i].dirty=0; // // All triangles with edges below the threshold will be removed // // The following numbers works well for most models. // If it does not, try to adjust the 3 parameters // double threshold = DBL_EPSILON; //1.0E-3 EPS; if (verbose) { printf("lossless iteration %d\n", iteration); } // remove vertices & mark deleted triangles loopi(0,triangles.size()) { Triangle &t=triangles[i]; if(t.err[3]>threshold) continue; if(t.deleted) continue; if(t.dirty) continue; loopj(0,3)if(t.err[j]({i0,i1})); int tcount=refs.size()-tstart; if(tcount<=v0.tcount) { // save ram if(tcount)memcpy(&refs[v0.tstart],&refs[tstart],tcount*sizeof(Ref)); } else // append v0.tstart=tstart; v0.tcount=tcount; break; } } if(deleted_triangles<=0)break; deleted_triangles=0; } //for each iteration // clean up mesh compact_mesh(); } //simplify_mesh_lossless() // Check if a triangle flips when this edge is removed bool flipped(vec3f p,int i0,int i1,Vertex &v0,Vertex &v1,std::vector &deleted) { loopk(0,v0.tcount) { Triangle &t=triangles[refs[v0.tstart+k].tid]; if(t.deleted)continue; int s=refs[v0.tstart+k].tvertex; int id1=t.v[(s+1)%3]; int id2=t.v[(s+2)%3]; if(id1==i1 || id2==i1) // delete ? { deleted[k]=1; continue; } vec3f d1 = vertices[id1].p-p; d1.normalize(); vec3f d2 = vertices[id2].p-p; d2.normalize(); if(fabs(d1.dot(d2))>0.999) return true; vec3f n; n.cross(d1,d2); n.normalize(); deleted[k]=0; if(n.dot(t.n)<0.2) return true; } return false; } // update_uvs void update_uvs(int i0,const Vertex &v,const vec3f &p,std::vector &deleted) { loopk(0,v.tcount) { Ref &r=refs[v.tstart+k]; Triangle &t=triangles[r.tid]; if(t.deleted)continue; if(deleted[k])continue; vec3f p1=vertices[t.v[0]].p; vec3f p2=vertices[t.v[1]].p; vec3f p3=vertices[t.v[2]].p; t.uvs[r.tvertex] = interpolate(p,p1,p2,p3,t.uvs); } } // Update triangle connections and edge error after a edge is collapsed void update_triangles(int i0,Vertex &v,std::vector &deleted,int &deleted_triangles) { vec3f p; loopk(0,v.tcount) { Ref &r=refs[v.tstart+k]; Triangle &t=triangles[r.tid]; if(t.deleted)continue; if(deleted[k]) { t.deleted=1; deleted_triangles++; continue; } t.v[r.tvertex]=i0; t.dirty=1; t.err[0]=calculate_error(t.v[0],t.v[1],p); t.err[1]=calculate_error(t.v[1],t.v[2],p); t.err[2]=calculate_error(t.v[2],t.v[0],p); t.err[3]=min(t.err[0],min(t.err[1],t.err[2])); refs.push_back(r); } } // compact triangles, compute edge error and build reference list void update_mesh(int iteration) { if(iteration>0) // compact triangles { int dst=0; loopi(0,triangles.size()) if(!triangles[i].deleted) { triangles[dst++]=triangles[i]; } triangles.resize(dst); } // // Init Quadrics by Plane & Edge Errors // // required at the beginning ( iteration == 0 ) // recomputing during the simplification is not required, // but mostly improves the result for closed meshes // if( iteration == 0 ) { loopi(0,vertices.size()) vertices[i].q=SymetricMatrix(0.0); loopi(0,vertices.size()) vertices[i].border=0; loopi(0,triangles.size()) { Triangle &t=triangles[i]; vec3f n,p[3]; loopj(0,3) p[j]=vertices[t.v[j]].p; n.cross(p[1]-p[0],p[2]-p[0]); n.normalize(); t.n=n; loopj(0,3) vertices[t.v[j]].q = vertices[t.v[j]].q+SymetricMatrix(n.x,n.y,n.z,-n.dot(p[0])); } loopi(0,triangles.size()) { // Calc Edge Error Triangle &t=triangles[i];vec3f p; loopj(0,3) t.err[j]=calculate_error(t.v[j],t.v[(j+1)%3],p); t.err[3]=min(t.err[0],min(t.err[1],t.err[2])); } } // Init Reference ID list loopi(0,vertices.size()) { vertices[i].tstart=0; vertices[i].tcount=0; } loopi(0,triangles.size()) { Triangle &t=triangles[i]; loopj(0,3) vertices[t.v[j]].tcount++; } int tstart=0; loopi(0,vertices.size()) { Vertex &v=vertices[i]; v.tstart=tstart; tstart+=v.tcount; v.tcount=0; } // Write References refs.resize(triangles.size()*3); loopi(0,triangles.size()) { Triangle &t=triangles[i]; loopj(0,3) { Vertex &v=vertices[t.v[j]]; refs[v.tstart+v.tcount].tid=i; refs[v.tstart+v.tcount].tvertex=j; v.tcount++; } } // Identify boundary : vertices[].border=0,1 if( iteration == 0 ) { std::vector vcount,vids; loopi(0,vertices.size()) vertices[i].border=0; loopi(0,vertices.size()) { Vertex &v=vertices[i]; vcount.clear(); vids.clear(); loopj(0,v.tcount) { int k=refs[v.tstart+j].tid; Triangle &t=triangles[k]; loopk(0,3) { int ofs=0,id=t.v[k]; while(ofs try to find best result vec3f p1=vertices[id_v1].p; vec3f p2=vertices[id_v2].p; vec3f p3=(p1+p2)/2; double error1 = vertex_error(q, p1.x,p1.y,p1.z); double error2 = vertex_error(q, p2.x,p2.y,p2.z); double error3 = vertex_error(q, p3.x,p3.y,p3.z); error = min(error1, min(error2, error3)); if (error1 == error) p_result=p1; if (error2 == error) p_result=p2; if (error3 == error) p_result=p3; } return error; } char *trimwhitespace(char *str) { char *end; // Trim leading space while(isspace((unsigned char)*str)) str++; if(*str == 0) // All spaces? return str; // Trim trailing space end = str + strlen(str) - 1; while(end > str && isspace((unsigned char)*end)) end--; // Write new null terminator *(end+1) = 0; return str; } //Option : Load OBJ void load_obj(const char* filename, bool process_uv=false){ vertices.clear(); triangles.clear(); // printf ( "Loading Objects %s ... \n",filename); FILE* fn; if(filename==NULL) return ; if((char)filename[0]==0) return ; if ((fn = fopen(filename, "rb")) == NULL) { printf ( "File %s not found!\n" ,filename ); return; } char line[1000]; memset ( line,0,1000 ); int vertex_cnt = 0; int material = -1; std::map material_map; std::vector uvs; std::vector > uvMap; while(fgets( line, 1000, fn ) != NULL) { Vertex v; vec3f uv; if (strncmp(line, "mtllib", 6) == 0) { mtllib = trimwhitespace(&line[7]); } if (strncmp(line, "usemtl", 6) == 0) { std::string usemtl = trimwhitespace(&line[7]); if (material_map.find(usemtl) == material_map.end()) { material_map[usemtl] = materials.size(); materials.push_back(usemtl); } material = material_map[usemtl]; } if ( line[0] == 'v' && line[1] == 't' ) { if ( line[2] == ' ' ) if(sscanf(line,"vt %lf %lf", &uv.x,&uv.y)==2) { uv.z = 0; uvs.push_back(uv); } else if(sscanf(line,"vt %lf %lf %lf", &uv.x,&uv.y,&uv.z)==3) { uvs.push_back(uv); } } else if ( line[0] == 'v' ) { if ( line[1] == ' ' ) if(sscanf(line,"v %lf %lf %lf", &v.p.x, &v.p.y, &v.p.z)==3) { vertices.push_back(v); } } int integers[9]; if ( line[0] == 'f' ) { Triangle t; bool tri_ok = false; bool has_uv = false; if(sscanf(line,"f %d %d %d", &integers[0],&integers[1],&integers[2])==3) { tri_ok = true; }else if(sscanf(line,"f %d// %d// %d//", &integers[0],&integers[1],&integers[2])==3) { tri_ok = true; }else if(sscanf(line,"f %d//%d %d//%d %d//%d", &integers[0],&integers[3], &integers[1],&integers[4], &integers[2],&integers[5])==6) { tri_ok = true; }else if(sscanf(line,"f %d/%d/%d %d/%d/%d %d/%d/%d", &integers[0],&integers[6],&integers[3], &integers[1],&integers[7],&integers[4], &integers[2],&integers[8],&integers[5])==9) { tri_ok = true; has_uv = true; }else // Add Support for v/vt only meshes if (sscanf(line, "f %d/%d %d/%d %d/%d", &integers[0], &integers[6], &integers[1], &integers[7], &integers[2], &integers[8]) == 6) { tri_ok = true; has_uv = true; } else { printf("unrecognized sequence\n"); printf("%s\n",line); while(1); } if ( tri_ok ) { t.v[0] = integers[0]-1-vertex_cnt; t.v[1] = integers[1]-1-vertex_cnt; t.v[2] = integers[2]-1-vertex_cnt; t.attr = 0; if ( process_uv && has_uv ) { std::vector indices; indices.push_back(integers[6]-1-vertex_cnt); indices.push_back(integers[7]-1-vertex_cnt); indices.push_back(integers[8]-1-vertex_cnt); uvMap.push_back(indices); t.attr |= TEXCOORD; } t.material = material; //geo.triangles.push_back ( tri ); triangles.push_back(t); //state_before = state; //state ='f'; } } } if ( process_uv && uvs.size() ) { loopi(0,triangles.size()) { loopj(0,3) triangles[i].uvs[j] = uvs[uvMap[i][j]]; } } fclose(fn); //printf("load_obj: vertices = %lu, triangles = %lu, uvs = %lu\n", vertices.size(), triangles.size(), uvs.size() ); } // load_obj() // Optional : Store as OBJ void write_obj(const char* filename) { FILE *file=fopen(filename, "w"); int cur_material = -1; bool has_uv = (triangles.size() && (triangles[0].attr & TEXCOORD) == TEXCOORD); if (!file) { printf("write_obj: can't write data file \"%s\".\n", filename); exit(0); } if (!mtllib.empty()) { fprintf(file, "mtllib %s\n", mtllib.c_str()); } loopi(0,vertices.size()) { //fprintf(file, "v %lf %lf %lf\n", vertices[i].p.x,vertices[i].p.y,vertices[i].p.z); fprintf(file, "v %g %g %g\n", vertices[i].p.x,vertices[i].p.y,vertices[i].p.z); //more compact: remove trailing zeros } if (has_uv) { loopi(0,triangles.size()) if(!triangles[i].deleted) { fprintf(file, "vt %g %g\n", triangles[i].uvs[0].x, triangles[i].uvs[0].y); fprintf(file, "vt %g %g\n", triangles[i].uvs[1].x, triangles[i].uvs[1].y); fprintf(file, "vt %g %g\n", triangles[i].uvs[2].x, triangles[i].uvs[2].y); } } int uv = 1; loopi(0,triangles.size()) if(!triangles[i].deleted) { if (triangles[i].material != cur_material) { cur_material = triangles[i].material; fprintf(file, "usemtl %s\n", materials[triangles[i].material].c_str()); } if (has_uv) { fprintf(file, "f %d/%d %d/%d %d/%d\n", triangles[i].v[0]+1, uv, triangles[i].v[1]+1, uv+1, triangles[i].v[2]+1, uv+2); uv += 3; } else { fprintf(file, "f %d %d %d\n", triangles[i].v[0]+1, triangles[i].v[1]+1, triangles[i].v[2]+1); } //fprintf(file, "f %d// %d// %d//\n", triangles[i].v[0]+1, triangles[i].v[1]+1, triangles[i].v[2]+1); //more compact: remove trailing zeros } fclose(file); } }; /////////////////////////////////////////// ================================================ FILE: fast_simplification/__init__.py ================================================ from ._version import __version__ # noqa: F401 from .replay import _map_isolated_points, replay_simplification # noqa: F401 from .simplify import simplify, simplify_mesh # noqa: F401 ================================================ FILE: fast_simplification/_replay.pyx ================================================ # cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True import numpy as np cimport numpy as np from libc.stdint cimport int64_t from libcpp cimport bool cdef extern from "wrapper_replay.h" namespace "Replay": void load_arrays_int32(const int, const int, const int, float*, int*, int*) void load_arrays_int64(const int, const int, const int, float*, int64_t*, int*) void replay_simplification() void get_points(float*) void get_triangles(int*) void get_collapses(int*) int get_faces_int32(int*) int get_faces_int32_no_padding(int*) int get_faces_int64(int64_t*) void write_obj(const char*) void load_obj(const char*, bool) int n_points() int n_triangles() int n_collapses() int load_triangles_from_vtk(const int, int*) void load_points(const int, float*) void load_collapses(const int, int*) def load_int32(int n_points, int n_faces, int n_collapses, float [:, ::1] points, int [:, ::1] faces, int [:, ::1] collapses): load_arrays_int32(n_points, n_faces, n_collapses, &points[0, 0], &faces[0, 0], &collapses[0, 0]) def load_int64( int n_points, int n_faces, int n_collapses, float [:, ::1] points, int64_t [:, ::1] faces, int [:, ::1] collapses ): load_arrays_int64(n_points, n_faces, n_collapses, &points[0, 0], &faces[0, 0], &collapses[0, 0]) # def simplify(int target_count, double aggressiveness=7, bool verbose=False): # simplify_mesh(target_count, aggressiveness, verbose) def replay(): replay_simplification() def save_obj(filename): py_byte_string = filename.encode('UTF-8') cdef char* c_filename = py_byte_string write_obj(c_filename) def read(filename): py_byte_string = filename.encode('UTF-8') cdef char* c_filename = py_byte_string load_obj(c_filename, False) def return_points(): cdef float [:, ::1] points = np.empty((n_points(), 3), np.float32) get_points(&points[0, 0]) return np.array(points) def return_triangles(): cdef int [:, ::1] triangles = np.empty((n_triangles(), 3), np.int32) get_triangles(&triangles[0, 0]) return np.array(triangles) def return_collapses(): cdef int [:, ::1] collapses = np.empty((n_collapses(), 2), np.int32) get_collapses(&collapses[0, 0]) return np.array(collapses) def return_faces_int32_no_padding(): """VTK formatted faces""" cdef int [::1] faces = np.empty(n_triangles()*3, np.int32) n_tri = get_faces_int32_no_padding(&faces[0]) return np.array(faces[:n_tri*3]) def return_faces_int32(): """VTK formatted faces""" cdef int [::1] faces = np.empty(n_triangles()*4, np.int32) n_tri = get_faces_int32(&faces[0]) return np.array(faces[:n_tri*4]) def return_faces_int64(): """VTK formatted faces""" cdef int64_t [::1] faces = np.empty(n_triangles()*4, np.int64) n_tri = get_faces_int64(&faces[0]) return np.array(faces[:n_tri*4]) def load_from_vtk(int n_points, float [:, ::1] points, int [::1] faces, int n_faces): result = load_triangles_from_vtk(n_faces, &faces[0]) if result: raise ValueError( "Input mesh ``mesh`` must consist of only triangles.\n" "Run ``.triangulate()`` to convert to an all triangle mesh." ) load_points(n_points, &points[0, 0]) def compute_indice_mapping(int[:, :] collapses, int n_points): ''' Compute the mapping from original indices to new indices after collapsing edges (pure python implementation with numpy) ''' # start with identity mapping indice_mapping = np.arange(n_points, dtype=int) # First round of mapping origin_indices = collapses[:, 1] indice_mapping[origin_indices] = collapses[:, 0] previous = np.zeros(len(indice_mapping)) while not np.array_equal(previous, indice_mapping): previous = indice_mapping.copy() indice_mapping[origin_indices] = indice_mapping[ indice_mapping[origin_indices] ] keep = np.setdiff1d( np.arange(n_points), collapses[:, 1] ) # Indices of the points that must be kept after decimation cdef int i = 0 cdef int j = 0 cdef int[:] application = np.zeros(n_points, dtype=np.int32) for i in range(n_points): if j == len(keep): break if i == keep[j]: application[i] = j j += 1 indice_mapping = np.array(application)[indice_mapping] return indice_mapping def clean_triangles_and_edges(int[:, :] mapped_triangles, bool clean_edges=False): """Return the edges and triangles of a mesh from mapped triangles Args: mapped_triangles (np.ndarray): Mapped triangles clean_edges (bool, optional): If True, remove duplicated edges. Returns: np.ndarray: Edges np.ndarray: Triangles """ cdef int i, j, k, l cdef int n_edges = 0 cdef int n_triangles = 0 cdef int N = len(mapped_triangles) cdef int[:, :] edges_with_rep = np.zeros((N, 2), dtype=np.int32) cdef int[:, :] triangles = np.zeros((N, 3), dtype=np.int32) for i in range(N): j = mapped_triangles[i, 0] k = mapped_triangles[i, 1] l = mapped_triangles[i, 2] if j != k and j != l and k != l: triangles[n_triangles, 0] = j triangles[n_triangles, 1] = k triangles[n_triangles, 2] = l n_triangles += 1 elif j != k: # j, k = np.sort([j, k]) edges_with_rep[n_edges, 0] = j edges_with_rep[n_edges, 1] = k n_edges += 1 elif j != l: # j, l = np.sort([j, l]) edges_with_rep[n_edges, 0] = j edges_with_rep[n_edges, 1] = l n_edges += 1 elif l != k: # l, k = np.sort([j, k]) edges_with_rep[n_edges, 0] = l edges_with_rep[n_edges, 1] = k n_edges += 1 if not clean_edges: return np.asarray(edges_with_rep)[:n_edges, :], np.asarray(triangles)[:n_triangles, :] cdef int[:, :] edges = np.zeros((n_edges, 2), dtype=np.int32) # Lexicographic sort cdef int[:] order = np.lexsort((np.asarray(edges_with_rep[:n_edges, 1]), np.asarray(edges_with_rep[:n_edges, 0]))) # Remove duplicates cdef int n_keep_edges = 1 edges[0, :] = edges_with_rep[order[0], :] print(f"n_edges : {n_edges}") for i in range(1, n_edges): if (edges_with_rep[order[i], 0] != edges_with_rep[order[i - 1], 0]) or (edges_with_rep[order[i], 1] != edges_with_rep[order[i - 1], 1]): edges[n_keep_edges, :] = edges_with_rep[order[i], :] n_keep_edges += 1 return np.asarray(edges)[:n_keep_edges, :], np.asarray(triangles)[:n_triangles, :] ================================================ FILE: fast_simplification/_simplify.pyx ================================================ # cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True import numpy as np cimport numpy as np from libc.stdint cimport int64_t from libcpp cimport bool cdef extern from "wrapper.h" namespace "Simplify": void load_arrays_int32(const int, const int, double*, int*) void load_arrays_int64(const int, const int, double*, int64_t*) void simplify_mesh(int, double aggressiveness, bool verbose) void simplify_mesh_lossless(bool) void get_points(double*) void get_triangles(int*) void get_collapses(int*) int get_faces_int32(int*) int get_faces_int32_no_padding(int*) int get_faces_int64(int64_t*) void write_obj(const char*) void load_obj(const char*, bool) int n_points() int n_triangles() int n_collapses() int load_triangles_from_vtk(const int, int*) void load_points(const int, double*) def load_int32(int n_points, int n_faces, double [:, ::1] points, int [:, ::1] faces): load_arrays_int32(n_points, n_faces, &points[0, 0], &faces[0, 0]) def load_int64( int n_points, int n_faces, double [:, ::1] points, int64_t [:, ::1] faces ): load_arrays_int64(n_points, n_faces, &points[0, 0], &faces[0, 0]) def simplify(int target_count, double aggressiveness=7, bool verbose=False): simplify_mesh(target_count, aggressiveness, verbose) def simplify_lossless(bool verbose=False): simplify_mesh_lossless(verbose) def save_obj(filename): py_byte_string = filename.encode('UTF-8') cdef char* c_filename = py_byte_string write_obj(c_filename) def read(filename): py_byte_string = filename.encode('UTF-8') cdef char* c_filename = py_byte_string load_obj(c_filename, False) def return_points(): cdef double [:, ::1] points = np.empty((n_points(), 3), np.float64) get_points(&points[0, 0]) return np.array(points) def return_triangles(): cdef int [:, ::1] triangles = np.empty((n_triangles(), 3), np.int32) get_triangles(&triangles[0, 0]) return np.array(triangles) def return_collapses(): cdef int [:, ::1] collapses = np.empty((n_collapses(), 2), np.int32) get_collapses(&collapses[0, 0]) return np.array(collapses) def return_faces_int32_no_padding(): """VTK formatted faces""" cdef int [::1] faces = np.empty(n_triangles()*3, np.int32) n_tri = get_faces_int32_no_padding(&faces[0]) return np.array(faces[:n_tri*3]) def return_faces_int32(): """VTK formatted faces""" cdef int [::1] faces = np.empty(n_triangles()*4, np.int32) n_tri = get_faces_int32(&faces[0]) return np.array(faces[:n_tri*4]) def return_faces_int64(): """VTK formatted faces""" cdef int64_t [::1] faces = np.empty(n_triangles()*4, np.int64) n_tri = get_faces_int64(&faces[0]) return np.array(faces[:n_tri*4]) def load_from_vtk(int n_points, double [:, ::1] points, int [::1] faces, int n_faces): result = load_triangles_from_vtk(n_faces, &faces[0]) if result: raise ValueError( "Input mesh ``mesh`` must consist of only triangles.\n" "Run ``.triangulate()`` to convert to an all triangle mesh." ) load_points(n_points, &points[0, 0]) ================================================ FILE: fast_simplification/_version.py ================================================ """fast_simplification version On the ``main`` branch, use 'dev0' to denote a development version. For example: version_info = 0, 27, 'dev0' """ version_info = 0, 2, "dev0" __version__ = ".".join(map(str, version_info)) ================================================ FILE: fast_simplification/fast_simplification.py ================================================ def simplify(): pass ================================================ FILE: fast_simplification/replay.py ================================================ import numpy as np from . import _replay from .utils import ascontiguous def _map_isolated_points(points, edges, triangles, return_outliers=False): r"""Map the isolated points to the triangles. (points, edges, triangles) represents a structure. The goal of this function is to compute a mapping array such that the points that are not in the triangles but are in the edges are merged into the points that are in the triangles, with respect to the edges. An example is given below. (1) / | \\ (0) | (2)-3 \ | / \\ (4) 6-9 | 5 8-7 In this example, the points 5, 3, 4, 7, 8, 9 are not connected to any triangle. The expected mapping is: 0 -> 0 1 -> 1 2 -> 2 3 -> 2 4 -> 4 5 -> 4 6 -> 2 7 -> 7 (7 cannot be merged into any point in the triangles) 8 -> 8 (8 cannot be merged into any point in the triangles) 9 -> 2 The output will be the mapping array and the merged points array. In this example, the mapping array is [0, 1, 2, 2, 4, 4, 2, 7, 8, 2] and the merged points array is [3, 5, 6, 9]. The points 7 and 8 are outliers. If return_outliers is True, the function will return the mapping array, the merged points array and the isolated points array. Else, the function will return the mapping array and the merged points array. Parameters ---------- points : sequence array of points edges : sequence array of edges triangles : sequence array of triangles return_outsider : bool if True, return the outliers Returns ------- np.ndarray mapping array np.ndarray merged points array """ n_points = points.shape[0] # The points to connect are the points that are not in the triangles # but are in the edges points_to_connect = np.intersect1d( np.setdiff1d(np.arange(n_points), np.unique(triangles)), np.unique(edges) ) # Start with the identity mapping mapping = np.arange(n_points, dtype=np.int64) # Remove edges that do not contains points to connect edges = edges[np.isin(edges, points_to_connect).any(axis=1)] n_edges = edges.shape[0] n_edges_old = 0 # Iterate until there is no more edges to collapse # or until a statiionary state is reached while n_edges > 0 and n_edges != n_edges_old: n_edges_old = n_edges # Edges that connect two points to connect # are kept for the next iteration keep = np.isin(edges, points_to_connect).all(axis=1) # Edges that connect a point to connect to a point # that is not to connect are merged connexions = edges[~keep] a = np.isin(connexions, points_to_connect) merged = connexions[np.where(a)] target = connexions[np.where(~a)] # Update the mapping array and the points to connect mapping[merged] = mapping[target] points_to_connect = np.setdiff1d(points_to_connect, merged) # Remove the edges that are merged edges = edges[keep] # Remove edges that do not contains points to connect edges = edges[np.isin(edges, points_to_connect).any(axis=1)] n_edges = edges.shape[0] # The points that have been merged are the ones # such that mapping[i] != i merged_points = np.where(mapping != np.arange(len(mapping)))[0] if return_outliers: isolated_points = points_to_connect return mapping, merged_points, isolated_points return mapping, merged_points @ascontiguous def replay_simplification(points, triangles, collapses): """Replay the decimation of a triangular mesh. Parameters ---------- points : sequence A ``(n, 3)`` array of points. May be a ``numpy.ndarray`` or a list of points. For efficiency, provide points as a float32 array. triangles : sequence A ``(n, 3)`` array of triangle indices. May be a ``numpy.ndarray`` or a list of triangle indices. For efficiency, provide points as a float32 array. collapses : sequence The collapses to replay. A ``(n, 2)`` numpy.ndarray of collapses. ``collapses[i] = [i0, i1]`` means that during the i-th collapse, the vertex ``i1`` was collapsed into the vertex ``i0``. Returns ------- np.ndarray Points array. np.ndarray Triangles array. np.ndarray indice_mapping array. A ``(n,)`` array of indices. ``indice_mapping[i] = j`` means that the vertex ``i`` of the original mesh was mapped to the vertex ``j`` of the decimated mesh. """ import numpy as np if not isinstance(points, np.ndarray): points = np.array(points, dtype=np.float32) if not isinstance(triangles, np.ndarray): triangles = np.array(triangles, dtype=np.int32) if points.ndim != 2: raise ValueError("``points`` array must be 2 dimensional") if points.shape[1] != 3: raise ValueError(f"Expected ``points`` array to be (n, 3), not {points.shape}") if triangles.ndim != 2: raise ValueError("``triangles`` array must be 2 dimensional") if triangles.shape[1] != 3: raise ValueError(f"Expected ``triangles`` array to be (n, 3), not {triangles.shape}") if not triangles.flags.c_contiguous: triangles = np.ascontiguousarray(triangles) if triangles.dtype == np.int32: load = _replay.load_int32 elif triangles.dtype == np.int64: load = _replay.load_int64 else: load = _replay.load_int32 triangles = triangles.astype(np.int32) # Collapse the points n_faces = triangles.shape[0] n_points = points.shape[0] load(n_points, n_faces, collapses.shape[0], points, triangles, collapses) _replay.replay() dec_points = _replay.return_points() # Compute the indice mapping indice_mapping = _replay.compute_indice_mapping(collapses, len(points)) # compute the new triangles # Apply the indice mapping to the triangles mapped_triangles = indice_mapping[triangles.copy()] # Extract the edges and the triangles # Edges can be repeated, but this is not a problem # and it is faster to do so dec_edges, dec_triangles = _replay.clean_triangles_and_edges(mapped_triangles) # Map the isolated points to the triangles mapping, points_to_merge, outliers = _map_isolated_points( dec_points, dec_edges, dec_triangles, return_outliers=True ) dec_triangles = mapping[dec_triangles] indice_mapping = mapping[indice_mapping] points_to_merge = np.union1d(points_to_merge, outliers) # Remove the isolated points # isolated_points = new_collapses[:, 1] points_to_merge = np.sort(points_to_merge)[::-1] mapping = np.arange(dec_points.shape[0]) for ip in points_to_merge: dec_points = np.delete(dec_points, ip, axis=0) mapping[ip:] -= 1 indice_mapping = mapping[indice_mapping] dec_triangles = mapping[dec_triangles] return dec_points, dec_triangles, indice_mapping ================================================ FILE: fast_simplification/simplify.py ================================================ """Simplification library.""" from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from . import _simplify from .utils import ascontiguous if TYPE_CHECKING: try: from pyvista.core.pointset import PolyData except ModuleNotFoundError: pass def _check_args(target_reduction, target_count, n_faces): """Check arguments.""" if target_reduction and target_count: raise ValueError("You may specify ``target_reduction`` or ``target_count``, but not both") if target_reduction is None and target_count is None: raise ValueError("You must specify ``target_reduction`` or ``target_count``") if target_reduction is not None: if target_reduction > 1 or target_reduction < 0: raise ValueError("``target_reduction`` must be between 0 and 1") target_count = (1 - target_reduction) * n_faces if target_count < 0: raise ValueError("``target_count`` must be greater than 0") if target_count > n_faces: raise ValueError(f"``target_count`` must be less than the number of faces {n_faces}") return int(target_count) @ascontiguous def simplify( points: NDArray[np.float64], triangles: NDArray[np.int32], target_reduction: float | None = None, target_count: int | None = None, agg: float = 7.0, verbose: bool = False, return_collapses: bool = False, lossless: bool = False, ) -> ( tuple[NDArray[np.float64], NDArray[np.int64]] | tuple[NDArray[np.float64], NDArray[np.int64], NDArray[np.int64]] ): """Simplify a triangular mesh. Parameters ---------- points : sequence[float | double] A ``(n, 3)`` array of points. May be a ``numpy.ndarray`` or a sequence of points. Internally converted to double precision. triangles : sequence A ``(n, 3)`` array of triangle indices. May be a ``numpy.ndarray`` or a list of triangle indices. target_reduction : float, optional Fraction of the original mesh to remove. If set to ``0.9``, this function will try to reduce the data set to 10% of its original size and will remove 90% of the input triangles. Use this parameter or ``target_count``. target_count : int, optional Target number of triangles to reduce mesh to. This may be used in place of ``target_reduction``, but both cannot be set. agg : float, default: 7.0 Controls how aggressively to decimate the mesh. A value of 10 will result in a fast decimation at the expense of mesh quality and shape. A value of 0 will attempt to preserve the original mesh geometry at the expense of time. Setting a low value may result in being unable to reach the ``target_reduction`` or ``target_count``. verbose : bool, optional Enable verbose output when simplifying the mesh. return_collapses : bool, optional If True, return the history of collapses as a ``(n_collapses, 2)`` array of indices. ``collapses[i] = [i0, i1]`` means that durint the i-th collapse, the vertex ``i1`` was collapsed into the vertex ``i0``. lossless : bool, optional If True, simplify the mesh losslessly. Returns ------- np.ndarray Points array. np.ndarray Triangles array. np.ndarray (optional) Collapses array. Examples -------- This basic example demonstrates how to decimate a simple planar mesh composed by 8 triangles. >>> import fast_simplification >>> points = [ ... [0.5, -0.5, 0.0], ... [0.0, -0.5, 0.0], ... [-0.5, -0.5, 0.0], ... [0.5, 0.0, 0.0], ... [0.0, 0.0, 0.0], ... [-0.5, 0.0, 0.0], ... [0.5, 0.5, 0.0], ... [0.0, 0.5, 0.0], ... [-0.5, 0.5, 0.0], ... ] >>> faces = [ ... [0, 1, 3], ... [4, 3, 1], ... [1, 2, 4], ... [5, 4, 2], ... [3, 4, 6], ... [7, 6, 4], ... [4, 5, 7], ... [8, 7, 5], ... ] >>> points_out, faces_out = fast_simplification.simplify(points, faces, 0.5) """ points = np.asarray(points, dtype=np.float64) if not isinstance(triangles, np.ndarray): triangles = np.array(triangles, dtype=np.int32) if points.ndim != 2: raise ValueError("``points`` array must be 2 dimensional") if points.shape[1] != 3: raise ValueError(f"Expected ``points`` array to be (n, 3), not {points.shape}") if triangles.ndim != 2: raise ValueError("``triangles`` array must be 2 dimensional") if triangles.shape[1] != 3: raise ValueError(f"Expected ``triangles`` array to be (n, 3), not {triangles.shape}") n_faces = triangles.shape[0] triangles = np.ascontiguousarray(triangles) if triangles.dtype == np.int32: load = _simplify.load_int32 elif triangles.dtype == np.int64: load = _simplify.load_int64 else: load = _simplify.load_int32 triangles = triangles.astype(np.int32) load( points.shape[0], n_faces, points, triangles, ) if lossless: _simplify.simplify_lossless(verbose) else: target_count = _check_args(target_reduction, target_count, n_faces) _simplify.simplify(target_count, agg, verbose) points = _simplify.return_points() faces = _simplify.return_faces_int32_no_padding().reshape(-1, 3) if return_collapses: return points, faces, _simplify.return_collapses() return points, faces def simplify_mesh( mesh: "PolyData", target_reduction: float | None = None, target_count: int | None = None, agg: float = 7.0, verbose: bool = False, ): """Simplify a pyvista mesh. Parameters ---------- mesh : pyvista.PolyData PyVista mesh. target_reduction : float Fraction of the original mesh to remove. If set to ``0.9``, this function will try to reduce the data set to 10% of its original size and will remove 90% of the input triangles. Use this parameter or ``target_count``. target_count : int, optional Target number of triangles to reduce mesh to. This may be used in place of ``target_reduction``, but both cannot be set. agg : float, default: 7.0 Controls how aggressively to decimate the mesh. A value of ``10.0`` will result in a fast decimation at the expense of mesh quality and shape. A value of ``0.0`` will attempt to preserve the original mesh geometry at the expense of time. Setting a low value may result in being unable to reach the ``target_reduction`` or ``target_count``. verbose : bool, optional Enable verbose output when simplifying the mesh. Returns ------- pyvista.PolyData Simplified mesh. The field data of the mesh will contain a field named ``fast_simplification_collapses`` that contains the history of collapses as a ``(n_collapses, 2)`` array of indices. ``collapses[i] = [i0, i1]`` means that during the i-th collapse, the vertex ``i1`` was collapsed into the vertex ``i0``. """ try: import pyvista as pv except ImportError: raise ImportError("Please install pyvista to use this feature with:\npip install pyvista") n_faces = mesh.n_cells _simplify.load_from_vtk( mesh.n_points, mesh.points.astype(np.float64, order="C", copy=False), mesh.faces.astype(np.int32, order="C", copy=False), n_faces, ) target_count = _check_args(target_reduction, target_count, n_faces) _simplify.simplify(target_count, agg, verbose) # return the correct datatype of the faces if pv._get_vtk_id_type() == np.int32: faces = _simplify.return_faces_int32() else: faces = _simplify.return_faces_int64() # construct mesh mesh = pv.PolyData(_simplify.return_points(), faces, deep=False) mesh.field_data["fast_simplification_collapses"] = _simplify.return_collapses() return mesh ================================================ FILE: fast_simplification/utils.py ================================================ """Utility functions for the fast_simplification package.""" import numpy as np def ascontiguous(func): """A decorator that ensure that all the numpy arrays passed to the function are contiguous in memory and if not, apply np.ascontinguous arrays. """ def wrapper(*args, **kwargs): args = list(args) for i, arg in enumerate(args): if isinstance(arg, np.ndarray): args[i] = np.ascontiguousarray(arg) for key, value in kwargs.items(): if isinstance(value, np.ndarray): kwargs[key] = np.ascontiguousarray(value) return func(*args, **kwargs) # Copy annotations wrapper.__annotations__ = func.__annotations__ return wrapper ================================================ FILE: fast_simplification/wrapper.h ================================================ // wrap simplify header file for integration with cython #include "Simplify.h" namespace Simplify{ // load triangles void load_points(const int n_points, double* points){ vertices.clear(); // load vertices for (int ii = 0; ii < n_points; ii ++){ Vertex v; v.p.x = points[0 + 3*ii]; v.p.y = points[1 + 3*ii]; v.p.z = points[2 + 3*ii]; vertices.push_back(v); } } // load triangles void load_triangles(const int n_tri, int* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // load triangles void load_triangles_int64(const int n_tri, int64_t* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // load triangles from vtk and deal with padding int load_triangles_from_vtk(const int n_tri, int* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; if (faces[4*ii] != 3){ return 1; } t.v[0] = faces[1 + 4*ii]; t.v[1] = faces[2 + 4*ii]; t.v[2] = faces[3 + 4*ii]; triangles.push_back(t); } return 0; } void load_arrays_int32(const int n_points, const int n_tri, double* points, int* faces){ load_points(n_points, points); load_triangles(n_tri, faces); } void load_arrays_int64(const int n_points, const int n_tri, double* points, int64_t* faces){ load_points(n_points, points); load_triangles_int64(n_tri, faces); } int n_points(){ return vertices.size(); } int n_triangles(){ return triangles.size(); } int n_collapses(){ return collapses.size(); } // load triangles void load_triangles(const int n_tri, int64_t* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // populate a contiguous array with the points in the vertices vector void get_points(double* points){ // load vertices int n_points = vertices.size(); for (int ii = 0; ii < n_points; ii ++){ points[0 + 3*ii] = vertices[ii].p.x; points[1 + 3*ii] = vertices[ii].p.y; points[2 + 3*ii] = vertices[ii].p.z; } } // populate a contiguous array with the points in the vertices vector void get_triangles(int* tri){ // load vertices int n_tri = triangles.size(); for (int ii = 0; ii < n_tri; ii ++){ tri[0 + 3*ii] = triangles[ii].v[0]; tri[1 + 3*ii] = triangles[ii].v[1]; tri[2 + 3*ii] = triangles[ii].v[2]; } } void get_collapses(int* coll){ // load vertices int n_collapse = collapses.size(); for (int ii = 0; ii < n_collapse; ii ++){ coll[0 + 2*ii] = collapses.at(ii).at(0); coll[1 + 2*ii] = collapses.at(ii).at(1); } } // populate a contiguous array with the points in the vertices vector int get_faces_int32(int32_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 3*jj] = 3; tri[1 + 3*jj] = triangles[ii].v[0]; tri[2 + 3*jj] = triangles[ii].v[1]; tri[3 + 3*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } // populate a contiguous array with the points in the vertices // vector without the vtk padding int get_faces_int32_no_padding(int32_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 3*jj] = triangles[ii].v[0]; tri[1 + 3*jj] = triangles[ii].v[1]; tri[2 + 3*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } // populate a contiguous array with the points in the vertices vector int get_faces_int64(int64_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 4*jj] = 3; tri[1 + 4*jj] = triangles[ii].v[0]; tri[2 + 4*jj] = triangles[ii].v[1]; tri[3 + 4*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } } ================================================ FILE: fast_simplification/wrapper_replay.h ================================================ // wrap simplify header file for integration with cython #include "Replay.h" namespace Replay{ // load collapses void load_collapses(const int n_coll, int* coll){ collapses.clear(); for (int ii = 0; ii < n_coll; ii ++){ std::vector c; c.push_back(coll[0 + 2*ii]); c.push_back(coll[1 + 2*ii]); collapses.push_back(c); } } // load points void load_points(const int n_points, float* points){ vertices.clear(); // load vertices for (int ii = 0; ii < n_points; ii ++){ Vertex v; v.p.x = points[0 + 3*ii]; v.p.y = points[1 + 3*ii]; v.p.z = points[2 + 3*ii]; vertices.push_back(v); } } // load triangles void load_triangles(const int n_tri, int* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // load triangles void load_triangles_int64(const int n_tri, int64_t* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // load triangles from vtk and deal with padding int load_triangles_from_vtk(const int n_tri, int* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; if (faces[4*ii] != 3){ return 1; } t.v[0] = faces[1 + 4*ii]; t.v[1] = faces[2 + 4*ii]; t.v[2] = faces[3 + 4*ii]; triangles.push_back(t); } return 0; } void load_arrays_int32(const int n_points, const int n_tri, const int n_coll, float* points, int* faces, int* collapses){ load_points(n_points, points); load_triangles(n_tri, faces); load_collapses(n_coll, collapses); } void load_arrays_int64(const int n_points, const int n_tri, const int n_coll, float* points, int64_t* faces, int* collapses){ load_points(n_points, points); load_triangles_int64(n_tri, faces); load_collapses(n_coll, collapses); } int n_points(){ return vertices.size(); } int n_triangles(){ return triangles.size(); } int n_collapses(){ return collapses.size(); } // load triangles void load_triangles(const int n_tri, int64_t* faces){ triangles.clear(); for (int ii = 0; ii < n_tri; ii ++){ Triangle t; t.attr = 0; t.material = -1; t.v[0] = faces[0 + 3*ii]; t.v[1] = faces[1 + 3*ii]; t.v[2] = faces[2 + 3*ii]; triangles.push_back(t); } } // populate a contiguous array with the points in the vertices vector void get_points(float* points){ // load vertices int n_points = vertices.size(); for (int ii = 0; ii < n_points; ii ++){ points[0 + 3*ii] = vertices[ii].p.x; points[1 + 3*ii] = vertices[ii].p.y; points[2 + 3*ii] = vertices[ii].p.z; } } // populate a contiguous array with the points in the vertices vector void get_triangles(int* tri){ // load vertices int n_tri = triangles.size(); for (int ii = 0; ii < n_tri; ii ++){ tri[0 + 3*ii] = triangles[ii].v[0]; tri[1 + 3*ii] = triangles[ii].v[1]; tri[2 + 3*ii] = triangles[ii].v[2]; } } void get_collapses(int* coll){ // load vertices int n_collapse = collapses.size(); for (int ii = 0; ii < n_collapse; ii ++){ coll[0 + 2*ii] = collapses.at(ii).at(0); coll[1 + 2*ii] = collapses.at(ii).at(1); } } // populate a contiguous array with the points in the vertices vector int get_faces_int32(int32_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 3*jj] = 3; tri[1 + 3*jj] = triangles[ii].v[0]; tri[2 + 3*jj] = triangles[ii].v[1]; tri[3 + 3*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } // populate a contiguous array with the points in the vertices // vector without the vtk padding int get_faces_int32_no_padding(int32_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 3*jj] = triangles[ii].v[0]; tri[1 + 3*jj] = triangles[ii].v[1]; tri[2 + 3*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } // populate a contiguous array with the points in the vertices vector int get_faces_int64(int64_t* tri){ // load vertices int n_tri = triangles.size(); int jj = 0; for (int ii = 0; ii < n_tri; ii ++){ if (!triangles[ii].deleted){ tri[0 + 4*jj] = 3; tri[1 + 4*jj] = triangles[ii].v[0]; tri[2 + 4*jj] = triangles[ii].v[1]; tri[3 + 4*jj] = triangles[ii].v[2]; jj += 1; } } return jj; } } ================================================ FILE: pyproject.toml ================================================ [build-system] build-backend = "setuptools.build_meta" requires = [ "cython>=3.0.0", "numpy>=2,<3", "setuptools>=45.0", "wheel>=0.37.0" ] [tool.cibuildwheel] archs = ["auto64"] # 64-bit only before-build = "pip install abi3audit" build = "cp310-* cp311-*" # 3.11+ are abi3 wheels skip = "*musllinux*" test-command = "pytest {project}/tests" test-requires = "pyvista pytest" [tool.cibuildwheel.linux] repair-wheel-command = [ "auditwheel repair -w {dest_dir} {wheel}", "bash tools/audit_wheel.sh {wheel}" ] [tool.cibuildwheel.macos] archs = ["native"] repair-wheel-command = [ "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}", "bash tools/audit_wheel.sh {wheel}" ] [tool.cibuildwheel.windows] before-build = "pip install delvewheel abi3audit" repair-wheel-command = [ "delvewheel repair -w {dest_dir} {wheel}", "bash tools/audit_wheel.sh {wheel}" ] [tool.codespell] ignore-words-list = 'THIRDPARTY' quiet-level = 3 skip = '*.pyc,*.txt,*.gif,*.png,*.jpg,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,./doc/build/*,./doc/images/*,./dist/*,*~,.hypothesis*,./doc/source/examples/*,*cover,*.dat,*.mac,build,fast_simplification/Simplify.h,PKG-INFO,*.mypy_cache/*,./docker/mapdl/*,./_unused/*' [tool.isort] default_section = "THIRDPARTY" force_sort_within_sections = true line_length = 100 profile = "black" skip_glob = ["__init__.py"] src_paths = ["doc", "fast_simplification", "tests"] [tool.pytest.ini_options] filterwarnings = [ # bogus numpy ABI warning (see numpy/#432) "ignore:.*numpy.dtype size changed.*:RuntimeWarning", "ignore:.*numpy.ufunc size changed.*:RuntimeWarning", "ignore:.*Distutils was imported before Setuptools*" ] junit_family = "legacy" [tool.ruff] line-length = 100 [tool.ruff.lint] ignore = [] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. select = ["E4", "E7", "E9", "F"] ================================================ FILE: pytest.ini ================================================ [pytest] junit_family=legacy filterwarnings = ignore::FutureWarning ignore::PendingDeprecationWarning ignore::DeprecationWarning # bogus numpy ABI warning (see numpy/#432) ignore:.*numpy.dtype size changed.*:RuntimeWarning ignore:.*numpy.ufunc size changed.*:RuntimeWarning ignore:.*Given trait value dtype "float64":UserWarning doctest_optionflags = NUMBER ELLIPSIS ================================================ FILE: requirements_docs.txt ================================================ numpydoc>=1.8.0 pydata-sphinx-theme pyvista Sphinx>=4.0.0 sphinx-copybutton sphinx-gallery>=0.8.1 sphinx-notfound-page>=0.3.0 ================================================ FILE: requirements_test.txt ================================================ pytest pyvista ================================================ FILE: setup.py ================================================ """Setup for fast-simplification.""" import builtins from io import open as io_open import os import platform import sys from setuptools import Extension, setup from setuptools.command.build_ext import build_ext as _build_ext from wheel.bdist_wheel import bdist_wheel filepath = os.path.dirname(__file__) # Define macros for cython macros = [] ext_kwargs = {} setup_kwargs = {"cmdclass": {}} if os.name == "nt": # windows extra_compile_args = ["/openmp", "/O2", "/w", "/GS"] elif os.name == "posix": # linux org mac os if sys.platform == "linux": extra_compile_args = ["-std=gnu++11", "-O3", "-w"] else: # probably mac os extra_compile_args = ["-std=c++11", "-O3", "-w"] else: raise OSError("Unsupported OS %s" % os.name) # Check if 64-bit if sys.maxsize > 2**32: macros.append(("IS64BITPLATFORM", None)) # https://github.com/joerick/python-abi3-package-sample/blob/main/setup.py class bdist_wheel_abi3(bdist_wheel): # noqa: D101 def get_tag(self): # noqa: D102 python, abi, plat = super().get_tag() if python.startswith("cp"): return "cp311", "abi3", plat return python, abi, plat if sys.version_info.minor >= 11 and platform.python_implementation() == "CPython": # Can create an abi3 wheel (typed memoryviews first available in 3.11)! macros.append(("Py_LIMITED_API", "0x030B0000")) ext_kwargs["py_limited_api"] = True setup_kwargs["cmdclass"]["bdist_wheel"] = bdist_wheel_abi3 # Get version from version info __version__ = None version_file = os.path.join(filepath, "fast_simplification", "_version.py") with io_open(version_file, mode="r") as fd: exec(fd.read()) # readme file readme_file = os.path.join(filepath, "README.rst") # for: the cc1plus: warning: command line option '-Wstrict-prototypes' class build_ext(_build_ext): def finalize_options(self): _build_ext.finalize_options(self) # prevent numpy from thinking it is still in its setup process: try: del builtins.__NUMPY_SETUP__ except AttributeError: pass import numpy self.include_dirs.append(numpy.get_include()) def build_extensions(self): try: self.compiler.compiler_so.remove("-Wstrict-prototypes") except (AttributeError, ValueError): pass _build_ext.build_extensions(self) setup_kwargs["cmdclass"]["build_ext"] = build_ext setup( name="fast_simplification", packages=["fast_simplification"], version=__version__, description="Wrapper around the Fast-Quadric-Mesh-Simplification library.", long_description=open(readme_file).read(), long_description_content_type="text/x-rst", author="Alex Kaszynski", author_email="akascap@gmail.com", license="MIT", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ], url="https://github.com/pyvista/fast-simplification", python_requires=">=3.9", # Build cython modules ext_modules=[ Extension( "fast_simplification._simplify", ["fast_simplification/_simplify.pyx"], language="c++", extra_compile_args=extra_compile_args, define_macros=macros, **ext_kwargs, ), Extension( "fast_simplification._replay", ["fast_simplification/_replay.pyx"], language="c++", extra_compile_args=extra_compile_args, define_macros=macros, **ext_kwargs, ), ], keywords="fast-simplification decimation", install_requires=["numpy"], **setup_kwargs, ) ================================================ FILE: tests/test_map_isolated_points.py ================================================ import numpy as np from fast_simplification import _map_isolated_points as map_isolated_points def test_map_isolated_points(): # Example 1 # # (1) # / | \ # (0) | (2)-3 # \ | / \ # (4) 6-9 # | # 5 8-7 points = np.random.rand(10, 3) edges = np.array( [ [0, 1], [0, 4], [1, 4], [1, 2], [2, 4], [2, 3], [2, 6], [6, 9], [4, 5], [8, 7], ], dtype=np.int64, ) triangles = np.array( [ [0, 1, 4], [1, 2, 4], ], dtype=np.int64, ) target_mapping = np.array([0, 1, 2, 2, 4, 4, 2, 7, 8, 2], dtype=np.int64) target_merged_points = np.array([3, 5, 6, 9], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping) assert np.allclose(merged_points, target_merged_points) # Example 2 # # (7)-(8) 3 # \ | # \ | # (0)-1-2-9-4-5-6 points = np.random.rand(10, 3) edges = np.array( [ [0, 1], [1, 2], [2, 9], [9, 4], [4, 5], [5, 6], ], dtype=np.int64, ) triangles = np.array( [ [0, 7, 8], ] ) target_mapping = np.array([0, 0, 0, 3, 0, 0, 0, 7, 8, 0], dtype=np.int64) target_merged_points = np.array([1, 2, 4, 5, 6, 9], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping) assert np.allclose(merged_points, target_merged_points) # Example 3 # # (1) # | \ # | \ # (2)-(0)-4-6 # | | # 3-5 points = np.random.rand(7, 3) edges = np.array([[0, 1], [1, 2], [2, 0], [0, 4], [4, 6], [5, 6], [3, 5]], dtype=np.int64) triangles = np.array( [ [0, 1, 2], ], dtype=np.int64, ) target_mapping = np.array([0, 1, 2, 0, 0, 0, 0], dtype=np.int64) target_merged_points = np.array([3, 4, 5, 6], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping) assert np.allclose(merged_points, target_merged_points) ## Example 4 # # (6) (7)-(8) # | \ | / # | \ | / # (5)-(0)-1-2-3-(4) # # Here the situation is ambiguous. Does 2 merge into 0 or 4 ? # We consider 2 -> 4 and 2 -> 0 as valid solutions. points = np.random.rand(9, 3) edges = np.array( [ [0, 1], [1, 2], [2, 3], [3, 4], ], dtype=np.int64, ) triangles = np.array([[0, 5, 6], [4, 7, 8]], dtype=np.int64) target_mapping1 = np.array([0, 0, 0, 4, 4, 5, 6, 7, 8], dtype=np.int64) target_mapping2 = np.array([0, 0, 4, 4, 4, 5, 6, 7, 8], dtype=np.int64) target_merged_points = np.array([1, 2, 3], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping1) or np.allclose(mapping, target_mapping2) assert np.allclose(merged_points, target_merged_points) ## Example 5 # # (1) (7)-(8)-9 # | \ | / # | \ | / # (2)-(3)-0 4-5-(6) points = np.random.rand(10, 3) edges = np.array( [ [0, 3], [1, 3], [2, 3], [1, 2], [4, 5], [5, 6], [8, 9], ], dtype=np.int64, ) triangles = np.array( [ [1, 2, 3], [6, 7, 8], ], dtype=np.int64, ) target_mapping = np.array([3, 1, 2, 3, 6, 6, 6, 7, 8, 8], dtype=np.int64) target_merged_points = np.array([0, 4, 5, 9], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping) assert np.allclose(merged_points, target_merged_points) ## Example 6 # # 0-1-2 points = np.random.rand(3, 3) edges = np.array( [ [0, 1], [1, 2], ], dtype=np.int64, ) triangles = np.array([[]], dtype=np.int64) target_mapping = np.array([0, 1, 2], dtype=np.int64) target_merged_points = np.array([], dtype=np.int64) mapping, merged_points = map_isolated_points(points, edges, triangles) assert np.allclose(mapping, target_mapping) assert np.allclose(merged_points, target_merged_points) ================================================ FILE: tests/test_replay.py ================================================ import numpy as np import pytest import fast_simplification try: import pyvista as pv has_vtk = True except ModuleNotFoundError: has_vtk = False skip_no_vtk = pytest.mark.skipif(not has_vtk, reason="Requires VTK") @pytest.fixture def mesh(): return pv.Sphere() def test_collapses_trivial(): # arrays from: # mesh = pv.Plane(i_resolution=2, j_resolution=2).triangulate() points = [ [0.5, -0.5, 0.0], [0.0, -0.5, 0.0], [-0.5, -0.5, 0.0], [0.5, 0.0, 0.0], [0.0, 0.0, 0.0], [-0.5, 0.0, 0.0], [0.5, 0.5, 0.0], [0.0, 0.5, 0.0], [-0.5, 0.5, 0.0], ] faces = [ [0, 1, 3], [4, 3, 1], [1, 2, 4], [5, 4, 2], [3, 4, 6], [7, 6, 4], [4, 5, 7], [8, 7, 5], ] with pytest.raises(ValueError, match="You must specify"): fast_simplification.simplify(points, faces) points_out, faces_out, collapses = fast_simplification.simplify( points, faces, 0.5, return_collapses=True ) ( replay_points, replay_faces, indice_mapping, ) = fast_simplification.replay_simplification(points, faces, collapses) assert np.allclose(points_out, replay_points) assert np.allclose(faces_out, replay_faces) @skip_no_vtk def test_collapses_sphere(mesh): points = mesh.points faces = mesh.faces.reshape(-1, 4)[:, 1:] reduction = 0.5 points_out, faces_out, collapses = fast_simplification.simplify( points, faces, reduction, return_collapses=True ) ( replay_points, replay_faces, indice_mapping, ) = fast_simplification.replay_simplification(points, faces, collapses) assert np.allclose(points_out, replay_points) assert np.allclose(faces_out, replay_faces) try: from pyvista import examples @pytest.fixture def louis(): return examples.download_louis_louvre() @pytest.fixture def human(): return examples.download_human() has_examples = True except: has_examples = False skip_no_examples = pytest.mark.skipif(not has_examples, reason="Requires pyvista.examples") @skip_no_examples @skip_no_vtk def test_collapses_louis(louis): points = louis.points faces = louis.faces.reshape(-1, 4)[:, 1:] reduction = 0.9 points_out, faces_out, collapses = fast_simplification.simplify( points, faces, reduction, return_collapses=True ) ( replay_points, replay_faces, indice_mapping, ) = fast_simplification.replay_simplification(points, faces, collapses) assert np.allclose(points_out, replay_points) assert np.allclose(faces_out, replay_faces) @skip_no_examples @skip_no_vtk def test_human(human): points = human.points faces = human.faces.reshape(-1, 4)[:, 1:] reduction = 0.9 points_out, faces_out, collapses = fast_simplification.simplify( points, faces, reduction, return_collapses=True ) ( replay_points, replay_faces, indice_mapping, ) = fast_simplification.replay_simplification(points, faces, collapses) assert np.allclose(points_out, replay_points) assert np.allclose(faces_out, replay_faces) ================================================ FILE: tests/test_simplify.py ================================================ import numpy as np import pytest import fast_simplification try: import pyvista as pv has_vtk = True except ModuleNotFoundError: has_vtk = False skip_no_vtk = pytest.mark.skipif(not has_vtk, reason="Requires VTK") @pytest.fixture def mesh(): return pv.Sphere() def test_simplify_trivial(): # arrays from: # mesh = pv.Plane(i_resolution=2, j_resolution=2).triangulate() points = [ [0.5, -0.5, 0.0], [0.0, -0.5, 0.0], [-0.5, -0.5, 0.0], [0.5, 0.0, 0.0], [0.0, 0.0, 0.0], [-0.5, 0.0, 0.0], [0.5, 0.5, 0.0], [0.0, 0.5, 0.0], [-0.5, 0.5, 0.0], ] faces = [ [0, 1, 3], [4, 3, 1], [1, 2, 4], [5, 4, 2], [3, 4, 6], [7, 6, 4], [4, 5, 7], [8, 7, 5], ] with pytest.raises(ValueError, match="You must specify"): fast_simplification.simplify(points, faces) points_out, faces_out = fast_simplification.simplify(points, faces, 0.5) assert points_out.shape[0] == 5 assert faces_out.shape[0] == 4 # Test with return_collapses=True # We check that the number of points after simplification is equal to the number of # points before simplification minus the number of collapses points_out, faces_out, collapses = fast_simplification.simplify( points, faces, 0.5, return_collapses=True ) n_points_before_simplification = len(points) n_points_after_simplification = len(points_out) n_collapses = len(collapses) assert n_points_after_simplification == n_points_before_simplification - n_collapses @skip_no_vtk def test_simplify_none(mesh): triangles = mesh._connectivity_array.reshape(-1, 3) reduction = 0 points, faces = fast_simplification.simplify(mesh.points, triangles, reduction) assert np.allclose(triangles, faces) assert np.allclose(mesh.points, points) @skip_no_vtk def test_simplify(mesh): triangles = mesh._connectivity_array.reshape(-1, 3) reduction = 0.5 points, faces, collapses = fast_simplification.simplify( mesh.points, triangles, reduction, return_collapses=True ) assert triangles.shape[0] * reduction == faces.shape[0] # We check that the number of points after simplification is equal to the number of # points before simplification minus the number of collapses n_points_before_simplification = mesh.points.shape[0] n_points_after_simplification = points.shape[0] n_collapses = collapses.shape[0] assert n_points_after_simplification == n_points_before_simplification - n_collapses assert points.dtype == np.float64 @skip_no_vtk def test_simplify_lossless(mesh): triangles = mesh._connectivity_array.reshape(-1, 3) reduction = 0.5 points, faces = fast_simplification.simplify(mesh.points, triangles, reduction, lossless=True) assert np.allclose(mesh.points, points) assert np.allclose(triangles, faces) @skip_no_vtk def test_simplify_agg(mesh): triangles = mesh._connectivity_array.reshape(-1, 3) reduction = 0.5 points, faces = fast_simplification.simplify( mesh.points, triangles, reduction, agg=0, ) assert triangles.shape[0] == faces.shape[0] reduction = 0.5 points, faces = fast_simplification.simplify( mesh.points, triangles, reduction, agg=1, ) # somewhere between the requested reduction and the original number of triangles assert triangles.shape[0] * reduction < faces.shape[0] < triangles.shape[0] @skip_no_vtk def test_simplify_mesh(mesh): reduction = 0.5 mesh_out = fast_simplification.simplify_mesh(mesh, reduction) assert mesh_out.n_cells == mesh.n_cells * reduction ================================================ FILE: tools/audit_wheel.sh ================================================ #!/bin/bash -eo pipefail set -x PY_MINOR=$(python -c "import sys; print(sys.version_info.minor)") if [ "$PY_MINOR" -lt 11 ]; then echo "Not checking abi3audit for Python $PY_MINOR < 3.11" exit 0 fi abi3audit --strict --report --verbose "$1"