├── mcubes ├── version.py ├── src │ ├── pyarray_symbol.h │ ├── pywrapper.h │ ├── _mcubes.pyx │ ├── pywrapper.cpp │ ├── pyarraymodule.h │ ├── marchingcubes.h │ └── marchingcubes.cpp ├── __init__.py ├── exporter.py ├── numpy_smoothing.py └── smoothing.py ├── .codecov.yml ├── images ├── binary.jpg ├── smooth.jpg └── smoothing_overview.png ├── setup.cfg ├── pyproject.toml ├── MANIFEST.in ├── .github └── workflows │ ├── mcubes-ci.yml │ └── mcubes-deployment.yml ├── examples └── spheres.py ├── LICENSE ├── setup.py ├── test_mcubes.py ├── test_smoothing.py ├── .gitignore └── README.md /mcubes/version.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.1.6" 3 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | -------------------------------------------------------------------------------- /images/binary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmneila/PyMCubes/HEAD/images/binary.jpg -------------------------------------------------------------------------------- /images/smooth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmneila/PyMCubes/HEAD/images/smooth.jpg -------------------------------------------------------------------------------- /mcubes/src/pyarray_symbol.h: -------------------------------------------------------------------------------- 1 | 2 | #define PY_ARRAY_UNIQUE_SYMBOL mcubes_PyArray_API 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | per-file-ignores = __init__.py:F401 4 | -------------------------------------------------------------------------------- /images/smoothing_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmneila/PyMCubes/HEAD/images/smoothing_overview.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "Cython", "numpy~=2.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /mcubes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from ._mcubes import marching_cubes, marching_cubes_func 3 | from .exporter import export_mesh, export_obj, export_off 4 | from .smoothing import smooth, smooth_constrained, smooth_gaussian 5 | from .version import __version__ 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include mcubes/*.py 2 | exclude mcubes/numpy_smoothing.py 3 | 4 | include mcubes/src/*.h 5 | include mcubes/src/*.cpp 6 | include mcubes/src/_mcubes.pyx 7 | exclude mcubes/src/_mcubes.cpp 8 | 9 | include examples/*.py 10 | include images/binary.jpg 11 | include images/smooth.jpg 12 | include images/smoothing_overview.png 13 | 14 | include setup.py 15 | include README.rst 16 | include LICENSE 17 | -------------------------------------------------------------------------------- /mcubes/src/pywrapper.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _PYWRAPPER_H 3 | #define _PYWRAPPER_H 4 | 5 | #include 6 | #include "pyarraymodule.h" 7 | 8 | #include 9 | 10 | PyObject* marching_cubes(PyArrayObject* arr, double isovalue); 11 | PyObject* marching_cubes_func(PyObject* lower, PyObject* upper, 12 | int numx, int numy, int numz, PyObject* f, double isovalue); 13 | 14 | #endif // _PYWRAPPER_H 15 | -------------------------------------------------------------------------------- /.github/workflows/mcubes-ci.yml: -------------------------------------------------------------------------------- 1 | name: PyMCubes tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | name: Build and test 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.9", "3.10", "3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | - name: Install the package 26 | run: | 27 | python -m pip install -e . 28 | - name: Install dependencies for test 29 | run: | 30 | python -m pip install pycollada 31 | python -m pip install pytest>=7.2.0 pytest-cov>=4.0 codecov 32 | - name: Test and coverage 33 | run: | 34 | mkdir output 35 | python -m pytest --cov=mcubes --cov-report=xml 36 | codecov 37 | - name: Flake8 38 | run: | 39 | python -m pip install flake8 40 | flake8 . 41 | -------------------------------------------------------------------------------- /mcubes/src/_mcubes.pyx: -------------------------------------------------------------------------------- 1 | 2 | # distutils: language = c++ 3 | # cython: embedsignature = True 4 | 5 | # from libcpp.vector cimport vector 6 | import numpy as np 7 | 8 | # Define PY_ARRAY_UNIQUE_SYMBOL 9 | cdef extern from "pyarray_symbol.h": 10 | pass 11 | 12 | cimport numpy as np 13 | 14 | np.import_array() 15 | 16 | cdef extern from "pywrapper.h": 17 | cdef object c_marching_cubes "marching_cubes"(np.ndarray, double) except + 18 | cdef object c_marching_cubes_func "marching_cubes_func"(tuple, tuple, int, int, int, object, double) except + 19 | 20 | def marching_cubes(np.ndarray volume, float isovalue): 21 | 22 | verts, faces = c_marching_cubes(volume, isovalue) 23 | verts.shape = (-1, 3) 24 | faces.shape = (-1, 3) 25 | return verts, faces 26 | 27 | def marching_cubes_func(tuple lower, tuple upper, int numx, int numy, int numz, object f, double isovalue): 28 | 29 | if any(l_i >= u_i for l_i, u_i in zip(lower, upper)): 30 | raise ValueError("lower coordinates cannot be larger than upper coordinates") 31 | 32 | if numx < 2 or numy < 2 or numz < 2: 33 | raise ValueError("numx, numy, numz cannot be smaller than 2") 34 | 35 | verts, faces = c_marching_cubes_func(lower, upper, numx, numy, numz, f, isovalue) 36 | verts.shape = (-1, 3) 37 | faces.shape = (-1, 3) 38 | return verts, faces 39 | -------------------------------------------------------------------------------- /examples/spheres.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import mcubes 4 | 5 | print("Example 1: Isosurface in NumPy volume...") 6 | 7 | # Create a data volume 8 | X, Y, Z = np.mgrid[:100, :100, :100] 9 | u = (X-50)**2 + (Y-50)**2 + (Z-50)**2 - 25**2 10 | 11 | # Extract the 0-isosurface 12 | vertices1, triangles1 = mcubes.marching_cubes(u, 0) 13 | 14 | # Export the result to sphere.dae 15 | mcubes.export_mesh(vertices1, triangles1, "sphere1.dae", "MySphere") 16 | 17 | print("Done. Result saved in 'sphere1.dae'.") 18 | 19 | print("Example 2: Isosurface in Python function...") 20 | print("(this might take a while...)") 21 | 22 | 23 | # Create the volume 24 | def f(x, y, z): 25 | return x**2 + y**2 + z**2 26 | 27 | 28 | # Extract the 16-isosurface 29 | vertices2, triangles2 = mcubes.marching_cubes_func( 30 | (-10, -10, -10), # Lower bound 31 | (10, 10, 10), # Upper bound 32 | 100, 100, 100, # Number of samples in each dimension 33 | f, # Implicit function 34 | 16) # Isosurface value 35 | 36 | # Export the result to sphere2.dae 37 | mcubes.export_mesh(vertices2, triangles2, "sphere2.dae", "MySphere") 38 | print("Done. Result saved in 'sphere2.dae'.") 39 | 40 | try: 41 | print("Plotting mesh...") 42 | from mayavi import mlab 43 | mlab.triangular_mesh( 44 | vertices1[:, 0], vertices1[:, 1], vertices1[:, 2], 45 | triangles1) 46 | print("Done.") 47 | mlab.show() 48 | except ImportError: 49 | print("Could not import mayavi. Interactive demo not available.") 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015, P. M. Neila 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /.github/workflows/mcubes-deployment.yml: -------------------------------------------------------------------------------- 1 | name: PyMCubes deployment 2 | 3 | on: 4 | push: 5 | tags: 'v[0-9]+*' 6 | 7 | jobs: 8 | deploy-sdist: 9 | name: Deploy source distribution 10 | runs-on: ubuntu-latest 11 | env: 12 | TWINE_USERNAME: __token__ 13 | TWINE_PASSWORD: ${{ secrets.PYPI }} 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | - name: Install cibuildwheel 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install build 25 | - name: Build sdist 26 | run: python -m build --sdist 27 | - name: Deploy sdist 28 | run: | 29 | python3 -m pip install twine 30 | python3 -m twine upload --skip-existing dist/* 31 | 32 | deploy-wheels: 33 | name: Deploy wheels on ${{ matrix.os }} 34 | runs-on: ${{ matrix.os }} 35 | env: 36 | CIBW_ARCHS: "auto64" 37 | CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-*" 38 | CIBW_SKIP: "*musllinux* pp*-win* pp*-macosx* pp*" 39 | TWINE_USERNAME: __token__ 40 | TWINE_PASSWORD: ${{ secrets.PYPI }} 41 | 42 | strategy: 43 | matrix: 44 | os: [ubuntu-latest, windows-latest, macos-latest, macos-13] 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Set up Python 49 | uses: actions/setup-python@v4 50 | with: 51 | python-version: '3.9' 52 | - name: Install cibuildwheel 53 | run: | 54 | python -m pip install --upgrade pip 55 | python -m pip install cibuildwheel 56 | - name: Build wheels 57 | run: python3 -m cibuildwheel --output-dir wheelhouse 58 | - name: Deploy 59 | run: | 60 | python3 -m pip install twine 61 | python3 -m twine upload --skip-existing wheelhouse/*.whl 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import runpy 4 | 5 | from setuptools import setup 6 | 7 | from setuptools.extension import Extension 8 | 9 | from Cython.Build import cythonize 10 | import numpy 11 | 12 | 13 | __version__ = runpy.run_path("mcubes/version.py")["__version__"] 14 | 15 | 16 | def extensions(): 17 | 18 | numpy_include_dir = numpy.get_include() 19 | 20 | mcubes_module = Extension( 21 | "mcubes._mcubes", 22 | [ 23 | "mcubes/src/_mcubes.pyx", 24 | "mcubes/src/pywrapper.cpp", 25 | "mcubes/src/marchingcubes.cpp" 26 | ], 27 | language="c++", 28 | extra_compile_args=['-std=c++11', '-Wall'], 29 | include_dirs=[numpy_include_dir], 30 | depends=[ 31 | "mcubes/src/marchingcubes.h", 32 | "mcubes/src/pyarray_symbol.h", 33 | "mcubes/src/pyarraymodule.h", 34 | "mcubes/src/pywrapper.h" 35 | ], 36 | define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")], 37 | ) 38 | 39 | return cythonize( 40 | [mcubes_module], 41 | language_level="3" 42 | ) 43 | 44 | 45 | setup( 46 | name="PyMCubes", 47 | version=__version__, 48 | description="Marching cubes for Python", 49 | author="Pablo Márquez Neila", 50 | author_email="pablo.marquez@unibe.ch", 51 | url="https://github.com/pmneila/PyMCubes", 52 | license="BSD 3-clause", 53 | long_description=""" 54 | Marching cubes for Python 55 | """, 56 | classifiers=[ 57 | "Development Status :: 5 - Production/Stable", 58 | "Environment :: Console", 59 | "Intended Audience :: Developers", 60 | "Intended Audience :: Science/Research", 61 | "License :: OSI Approved :: BSD License", 62 | "Natural Language :: English", 63 | "Operating System :: OS Independent", 64 | "Programming Language :: C++", 65 | "Programming Language :: Python", 66 | "Topic :: Multimedia :: Graphics :: 3D Modeling", 67 | "Topic :: Scientific/Engineering :: Image Recognition", 68 | ], 69 | packages=["mcubes"], 70 | ext_modules=extensions(), 71 | install_requires=['numpy>=1.21', 'scipy>=1.0.0'], 72 | ) 73 | -------------------------------------------------------------------------------- /mcubes/exporter.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | 5 | def export_obj(vertices: np.ndarray, triangles: np.ndarray, filename: str, flip_normals: bool = False): 6 | """ 7 | Export a 3D mesh to a Wavefront (.obj) file. 8 | 9 | If `flip_normals` is True, reverses the order of the vertices in each face 10 | to flip the normals. Default is False. 11 | """ 12 | 13 | with open(filename, 'w') as fh: 14 | 15 | for v in vertices: 16 | fh.write("v {} {} {}\n".format(*v)) 17 | 18 | if not flip_normals: 19 | for f in triangles: 20 | fh.write("f {} {} {}\n".format(*(f + 1))) 21 | else: 22 | for f in triangles: 23 | fh.write("f {} {} {}\n".format(*(f[::-1] + 1))) 24 | 25 | 26 | def export_off(vertices: np.ndarray, triangles: np.ndarray, filename: str): 27 | """ 28 | Exports a mesh in the (.off) format. 29 | """ 30 | 31 | with open(filename, 'w') as fh: 32 | fh.write('OFF\n') 33 | fh.write('{} {} 0\n'.format(len(vertices), len(triangles))) 34 | 35 | for v in vertices: 36 | fh.write("{} {} {}\n".format(*v)) 37 | 38 | for f in triangles: 39 | fh.write("3 {} {} {}\n".format(*f)) 40 | 41 | 42 | def export_mesh(vertices: np.ndarray, triangles: np.ndarray, filename: str, mesh_name: str = "mcubes_mesh"): 43 | """ 44 | Exports a mesh in the COLLADA (.dae) format. 45 | 46 | Needs PyCollada (https://github.com/pycollada/pycollada). 47 | """ 48 | 49 | import collada 50 | 51 | mesh = collada.Collada() 52 | 53 | vert_src = collada.source.FloatSource("verts-array", vertices, ('X', 'Y', 'Z')) 54 | geom = collada.geometry.Geometry(mesh, "geometry0", mesh_name, [vert_src]) 55 | 56 | input_list = collada.source.InputList() 57 | input_list.addInput(0, 'VERTEX', "#verts-array") 58 | 59 | triset = geom.createTriangleSet(np.copy(triangles), input_list, "") 60 | geom.primitives.append(triset) 61 | mesh.geometries.append(geom) 62 | 63 | geomnode = collada.scene.GeometryNode(geom, []) 64 | node = collada.scene.Node(mesh_name, children=[geomnode]) 65 | 66 | myscene = collada.scene.Scene("mcubes_scene", [node]) 67 | mesh.scenes.append(myscene) 68 | mesh.scene = myscene 69 | 70 | mesh.write(filename) 71 | -------------------------------------------------------------------------------- /test_mcubes.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | import numpy as np 5 | from numpy.testing import assert_array_equal, assert_allclose 6 | 7 | import mcubes 8 | 9 | 10 | def test_empty(): 11 | 12 | levelset = np.zeros((50, 50, 50)) 13 | vertices, triangles = mcubes.marching_cubes(levelset, 0.5) 14 | 15 | assert len(vertices) == len(triangles) == 0 16 | 17 | 18 | def test_sphere(): 19 | x, y, z = np.mgrid[:100, :100, :100] 20 | u = (x - 50)**2 + (y - 50)**2 + (z - 50)**2 - 25**2 21 | 22 | def func(x, y, z): 23 | return (x - 50)**2 + (y - 50)**2 + (z - 50)**2 - 25**2 24 | 25 | vertices1, triangles1 = mcubes.marching_cubes(u, 0.0) 26 | vertices2, triangles2 = mcubes.marching_cubes_func( 27 | (0, 0, 0), 28 | (99, 99, 99), 29 | 100, 100, 100, 30 | func, 0.0 31 | ) 32 | 33 | assert_allclose(vertices1, vertices2) 34 | assert_array_equal(triangles1, triangles2) 35 | 36 | 37 | def test_no_duplicates(): 38 | def sphere(x, y, z): 39 | return np.sqrt((x - 4)**2 + (y - 4)**2 + (z - 4)**2) - 4 40 | 41 | vertices, _ = mcubes.marching_cubes_func((2, 2, 2), (9, 9, 9), 20, 20, 20, sphere, 0) 42 | 43 | assert len(vertices) == len(np.unique(vertices, axis=0)) 44 | 45 | 46 | def test_export(): 47 | 48 | u = np.zeros((10, 10, 10)) 49 | u[2:-2, 2:-2, 2:-2] = 1.0 50 | vertices, triangles = mcubes.marching_cubes(u, 0.5) 51 | 52 | mcubes.export_obj(vertices, triangles, "output/test.obj") 53 | mcubes.export_off(vertices, triangles, "output/test.off") 54 | mcubes.export_mesh(vertices, triangles, "output/test.dae") 55 | 56 | 57 | def test_invalid_input(): 58 | 59 | def func(x, y, z): 60 | return x**2 + y**2 + z**2 - 1 61 | 62 | mcubes.marching_cubes_func((-1.5, -1.5, -1.5), (1.5, 1.5, 1.5), 10, 10, 10, func, 0) 63 | 64 | with pytest.raises(ValueError): 65 | mcubes.marching_cubes_func((0, 0, 0), (0, 0, 0), 10, 10, 10, func, 0) 66 | 67 | with pytest.raises(ValueError): 68 | mcubes.marching_cubes_func((-1.5, -1.5, -1.5), (1.5, 1.5, 1.5), 1, 10, 10, func, 0) 69 | 70 | with pytest.raises(Exception): 71 | mcubes.marching_cubes_func((-1.5, -1.5), (1.5, 1.5, 1.5), 10, 10, 10, func, 0) 72 | 73 | with pytest.raises(Exception): 74 | mcubes.marching_cubes_func((-1.5, -1.5, -1.5), (1.5, 1.5), 10, 10, 10, func, 0) 75 | -------------------------------------------------------------------------------- /mcubes/numpy_smoothing.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from scipy import ndimage as ndi 4 | 5 | __all__ = [ 6 | 'numpy_smooth', 7 | ] 8 | 9 | FILTER = np.array([1, -2, 1], dtype=np.float64) 10 | 11 | JACOBI_R_2D = np.array([ 12 | [0, 0, 1, 0, 0], 13 | [0, 0, -4, 0, 0], 14 | [1, -4, 0, -4, 1], 15 | [0, 0, -4, 0, 0], 16 | [0, 0, 1, 0, 0] 17 | ], dtype=np.float64) 18 | JACOBI_D_2D = 1/12 19 | 20 | JACOBI_R_3D = np.array([ 21 | [[0, 0, 0, 0, 0], 22 | [0, 0, 0, 0, 0], 23 | [0, 0, 1, 0, 0], 24 | [0, 0, 0, 0, 0], 25 | [0, 0, 0, 0, 0]], 26 | [[0, 0, 0, 0, 0], 27 | [0, 0, 0, 0, 0], 28 | [0, 0, -4, 0, 0], 29 | [0, 0, 0, 0, 0], 30 | [0, 0, 0, 0, 0]], 31 | [[0, 0, 1, 0, 0], 32 | [0, 0, -4, 0, 0], 33 | [1, -4, 0, -4, 1], 34 | [0, 0, -4, 0, 0], 35 | [0, 0, 1, 0, 0]], 36 | [[0, 0, 0, 0, 0], 37 | [0, 0, 0, 0, 0], 38 | [0, 0, -4, 0, 0], 39 | [0, 0, 0, 0, 0], 40 | [0, 0, 0, 0, 0]], 41 | [[0, 0, 0, 0, 0], 42 | [0, 0, 0, 0, 0], 43 | [0, 0, 1, 0, 0], 44 | [0, 0, 0, 0, 0], 45 | [0, 0, 0, 0, 0]] 46 | ], dtype=np.float64) 47 | 48 | JACOBI_D_3D = 1/18 49 | 50 | 51 | def signed_distance_function(binary_arr: np.ndarray) -> np.ndarray: 52 | 53 | arr = np.where(binary_arr > 0, 1.0, 0.0) 54 | dist_func = ndi.distance_transform_edt 55 | distance = np.where( 56 | binary_arr, 57 | dist_func(arr) - 0.5, 58 | -dist_func(1 - arr) + 0.5 59 | ) 60 | return distance 61 | 62 | 63 | def energy(arr: np.ndarray) -> np.ndarray: 64 | 65 | darr2 = [ndi.convolve1d(arr, FILTER, axis=i)**2 for i in range(arr.ndim)] 66 | return np.sum(darr2) / 2 67 | 68 | 69 | def solve_jacobi( 70 | arr: np.ndarray, 71 | lower_bound: np.ndarray, 72 | upper_bound: np.ndarray, 73 | max_iters: int = 500, 74 | jacobi_weight: float = 0.5 75 | ) -> np.ndarray: 76 | 77 | jacobi_d = JACOBI_D_2D if arr.ndim == 2 else JACOBI_D_3D 78 | jacobi_r = JACOBI_R_2D if arr.ndim == 2 else JACOBI_R_3D 79 | 80 | for it in range(max_iters): 81 | # energy_it = torch.sum(diff_energy(arr) * arr[2:-2, 2:-2, 2:-2]) / 2 82 | energy_it = energy(arr) 83 | print("Energy in iteration {}: {:.4g}".format(it, energy_it)) 84 | 85 | r_arr = ndi.convolve(arr, jacobi_r, mode='nearest') 86 | arr_1 = - jacobi_d * r_arr 87 | arr = jacobi_weight * arr_1 + (1 - jacobi_weight) * arr 88 | 89 | arr = np.maximum(arr, lower_bound) 90 | arr = np.minimum(arr, upper_bound) 91 | 92 | return arr 93 | 94 | 95 | def numpy_smooth(binary_array: np.ndarray, max_iters: int = 500) -> np.ndarray: 96 | 97 | arr = signed_distance_function(binary_array) 98 | 99 | upper_bound = np.where(arr < 0, arr, np.inf) 100 | lower_bound = np.where(arr > 0, arr, -np.inf) 101 | 102 | upper_bound[np.abs(upper_bound) < 1] = 0 103 | lower_bound[np.abs(lower_bound) < 1] = 0 104 | 105 | return solve_jacobi(arr, lower_bound, upper_bound, max_iters) 106 | -------------------------------------------------------------------------------- /test_smoothing.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pytest 4 | 5 | import numpy as np 6 | 7 | import mcubes 8 | 9 | 10 | def test_sphere(): 11 | 12 | # Create sphere with radius 25 centered at (50, 50, 50) 13 | x, y, z = np.mgrid[:100, :100, :100] 14 | levelset = np.sqrt((x - 50)**2 + (y - 50)**2 + (z - 50)**2) - 25 15 | 16 | # vertices, triangles = mcubes.marching_cubes(levelset, 0) 17 | # mcubes.export_obj(vertices, triangles, 'sphere1.obj') 18 | 19 | binary_levelset = levelset > 0 20 | smoothed_levelset = mcubes.smooth( 21 | binary_levelset, 22 | method='constrained', 23 | max_iters=500, 24 | rel_tol=1e-4 25 | ) 26 | 27 | vertices, _ = mcubes.marching_cubes(smoothed_levelset, 0.0) 28 | 29 | # Check all vertices have same distance to (50, 50, 50) 30 | dist = np.sqrt(np.sum((vertices - [50, 50, 50])**2, axis=1)) 31 | 32 | assert dist.min() > 24.5 and dist.max() < 25.5 33 | assert np.all(np.abs(smoothed_levelset - levelset) < 1) 34 | assert np.all((smoothed_levelset > 0) == binary_levelset) 35 | 36 | 37 | def test_gaussian_smoothing(): 38 | 39 | # Create sphere with radius 25 centered at (50, 50, 50) 40 | x, y, z = np.mgrid[:100, :100, :100] 41 | levelset = np.sqrt((x - 50)**2 + (y - 50)**2 + (z - 50)**2) - 25 42 | 43 | binary_levelset = levelset > 0 44 | smoothed_levelset = mcubes.smooth( 45 | binary_levelset, 46 | method='gaussian', 47 | sigma=3 48 | ) 49 | 50 | vertices, _ = mcubes.marching_cubes(smoothed_levelset, 0.0) 51 | 52 | # Check all vertices have same distance to (50, 50, 50) 53 | dist = np.sqrt(np.sum((vertices - [50, 50, 50])**2, axis=1)) 54 | assert dist.min() > 24 and dist.max() < 25 55 | 56 | 57 | def test_wrong_ndim(): 58 | 59 | binary_levelset = np.random.uniform(size=(10)) < 0.5 60 | 61 | with pytest.raises(ValueError): 62 | mcubes.smooth( 63 | binary_levelset, 64 | method='constrained', 65 | max_iters=500, 66 | rel_tol=1e-4 67 | ) 68 | 69 | binary_levelset = np.random.uniform(size=(10, 10, 10, 10)) < 0.5 70 | 71 | with pytest.raises(ValueError): 72 | mcubes.smooth( 73 | binary_levelset, 74 | method='constrained', 75 | max_iters=500, 76 | rel_tol=1e-4 77 | ) 78 | 79 | 80 | def test_wrong_method(): 81 | 82 | with pytest.raises(ValueError): 83 | mcubes.smooth( 84 | np.zeros((10, 10), dtype=np.bool_), 85 | method='wrong', 86 | max_iters=500, 87 | rel_tol=1e-4 88 | ) 89 | 90 | 91 | def test_circle(): 92 | 93 | x, y = np.mgrid[:100, :100] 94 | levelset = np.sqrt((x - 50)**2 + (y - 50)**2) - 25 95 | binary_levelset = levelset > 0 96 | 97 | smoothed_levelset = mcubes.smooth( 98 | binary_levelset, 99 | max_iters=500, 100 | rel_tol=1e-4 101 | ) 102 | 103 | assert np.all(np.abs(smoothed_levelset - levelset) < 1) 104 | assert np.all((smoothed_levelset > 0) == binary_levelset) 105 | 106 | 107 | # if __name__ == '__main__': 108 | # # logging.basicConfig(level=logging.DEBUG) 109 | # test_circle() 110 | # test_sphere() 111 | # test_large_sphere() 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.dae 3 | _mcubes.cpp 4 | *.py[cod] 5 | .vscode 6 | .mypy_cache 7 | 8 | .vscode 9 | 10 | # Created by https://www.gitignore.io/api/osx,linux,python 11 | # Edit at https://www.gitignore.io/?templates=osx,linux,python 12 | 13 | ### Linux ### 14 | *~ 15 | 16 | # temporary files which can be created if a process still has a handle open of a deleted file 17 | .fuse_hidden* 18 | 19 | # KDE directory preferences 20 | .directory 21 | 22 | # Linux trash folder which might appear on any partition or disk 23 | .Trash-* 24 | 25 | # .nfs files are created when an open file is removed but is still being accessed 26 | .nfs* 27 | 28 | ### OSX ### 29 | # General 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | 56 | ### Python ### 57 | # Byte-compiled / optimized / DLL files 58 | __pycache__/ 59 | *.py[cod] 60 | *$py.class 61 | 62 | # C extensions 63 | *.so 64 | 65 | # Distribution / packaging 66 | .Python 67 | build/ 68 | develop-eggs/ 69 | dist/ 70 | downloads/ 71 | eggs/ 72 | .eggs/ 73 | lib/ 74 | lib64/ 75 | parts/ 76 | sdist/ 77 | var/ 78 | wheels/ 79 | pip-wheel-metadata/ 80 | share/python-wheels/ 81 | *.egg-info/ 82 | .installed.cfg 83 | *.egg 84 | MANIFEST 85 | 86 | # PyInstaller 87 | # Usually these files are written by a python script from a template 88 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 89 | *.manifest 90 | *.spec 91 | 92 | # Installer logs 93 | pip-log.txt 94 | pip-delete-this-directory.txt 95 | 96 | # Unit test / coverage reports 97 | htmlcov/ 98 | .tox/ 99 | .nox/ 100 | .coverage 101 | .coverage.* 102 | .cache 103 | nosetests.xml 104 | coverage.xml 105 | *.cover 106 | .hypothesis/ 107 | .pytest_cache/ 108 | 109 | # Translations 110 | *.mo 111 | *.pot 112 | 113 | # Scrapy stuff: 114 | .scrapy 115 | 116 | # Sphinx documentation 117 | docs/_build/ 118 | 119 | # PyBuilder 120 | target/ 121 | 122 | # pyenv 123 | .python-version 124 | 125 | # pipenv 126 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 127 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 128 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 129 | # install all needed dependencies. 130 | #Pipfile.lock 131 | 132 | # celery beat schedule file 133 | celerybeat-schedule 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # Mr Developer 146 | .mr.developer.cfg 147 | .project 148 | .pydevproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # End of https://www.gitignore.io/api/osx,linux,python 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMCubes 2 | 3 | `PyMCubes` is an implementation of the marching cubes algorithm to extract 4 | iso-surfaces from volumetric data. The volumetric data can be given as a 5 | three-dimensional `NumPy` array or as a Python function ``f(x, y, z)``. 6 | 7 | `PyMCubes` also provides functions to export the results of the marching cubes 8 | in a number of mesh file formats. 9 | 10 | ## Installation 11 | 12 | Use `pip`: 13 | ``` 14 | $ pip install --upgrade PyMCubes 15 | ``` 16 | 17 | ## Example 18 | 19 | The following example creates a `NumPy` volume with spherical iso-surfaces and 20 | extracts one of them (i.e., a sphere) with `mcubes.marching_cubes`. The result 21 | is exported to `sphere.dae`: 22 | 23 | ```Python 24 | >>> import numpy as np 25 | >>> import mcubes 26 | 27 | # Create a data volume (30 x 30 x 30) 28 | >>> X, Y, Z = np.mgrid[:30, :30, :30] 29 | >>> u = (X-15)**2 + (Y-15)**2 + (Z-15)**2 - 8**2 30 | 31 | # Extract the 0-isosurface 32 | >>> vertices, triangles = mcubes.marching_cubes(u, 0) 33 | 34 | # Export the result to sphere.dae 35 | >>> mcubes.export_mesh(vertices, triangles, "sphere.dae", "MySphere") 36 | ``` 37 | 38 | Alternatively, you can use a Python function to represent the volume instead of 39 | a `NumPy` array: 40 | 41 | ```Python 42 | >>> import numpy as np 43 | >>> import mcubes 44 | 45 | # Create the volume 46 | >>> f = lambda x, y, z: x**2 + y**2 + z**2 47 | 48 | # Extract the 16-isosurface 49 | >>> vertices, triangles = mcubes.marching_cubes_func((-10,-10,-10), (10,10,10), 50 | ... 100, 100, 100, f, 16) 51 | 52 | # Export the result to sphere.dae (requires PyCollada) 53 | >>> mcubes.export_mesh(vertices, triangles, "sphere.dae", "MySphere") 54 | 55 | # Or export to an OBJ file 56 | >>> mcubes.export_obj(vertices, triangles, 'sphere.obj') 57 | ``` 58 | 59 | Note that using a function to represent the volumetric data is **much** slower 60 | than using a `NumPy` array. 61 | 62 | ## Smoothing binary arrays 63 | 64 | ![Overview](images/smoothing_overview.png "Overview of mcubes.smooth") 65 | 66 | Many segmentation methods build binary masks to separate _inside_ and _outside_ 67 | areas of the segmented object. When passing these binary mask to the marching 68 | cubes algorithm the resulting mesh looks jagged. The following code shows an 69 | example with a binary array embedding a sphere. 70 | ```Python 71 | x, y, z = np.mgrid[:100, :100, :100] 72 | binary_sphere = (x - 50)**2 + (y - 50)**2 + (z - 50)**2 - 25**2 < 0 73 | 74 | # Extract the 0.5-levelset since the array is binary 75 | vertices, triangles = mcubes.marching_cubes(binary_sphere, 0.5) 76 | ``` 77 | ![Mesh of a binary embedding](images/binary.jpg "Marching cubes with a binary embedding") 78 | 79 | `PyMCubes` provides the function `mcubes.smooth` that takes a 2D or 3D binary 80 | embedding function and produces a smooth version of it. 81 | ```Python 82 | smoothed_sphere = mcubes.smooth(binary_sphere) 83 | 84 | # Extract the 0-levelset (the 0-levelset of the output of mcubes.smooth is the 85 | # smoothed version of the 0.5-levelset of the binary array). 86 | vertices, triangles = mcubes.marching_cubes(smoothed_sphere, 0) 87 | ``` 88 | ![Mesh of a smoothed embedding](images/smooth.jpg "Marching cubes after smoothing the binary embedding") 89 | 90 | `mcubes.smooth` builds a smooth embedding array with negative values in the 91 | areas where the binary embedding array is 0, and positive values in the areas 92 | where it is 1. In this way, `mcubes.smooth` keeps all the information from the 93 | original embedding function, including fine details and thin structures that 94 | are commonly eroded by other standard smoothing methods. 95 | -------------------------------------------------------------------------------- /mcubes/src/pywrapper.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "pywrapper.h" 3 | 4 | #include "marchingcubes.h" 5 | 6 | #include 7 | #include 8 | 9 | 10 | PyObject* marching_cubes_func(PyObject* lower, PyObject* upper, 11 | int numx, int numy, int numz, PyObject* pyfunc, double isovalue) 12 | { 13 | std::vector vertices; 14 | std::vector polygons; 15 | 16 | // Copy the lower and upper coordinates to a C array. 17 | std::array lower_; 18 | std::array upper_; 19 | for(int i=0; i<3; ++i) 20 | { 21 | PyObject* l = PySequence_GetItem(lower, i); 22 | if(l == NULL) 23 | throw std::runtime_error("len(lower) < 3"); 24 | PyObject* u = PySequence_GetItem(upper, i); 25 | if(u == NULL) 26 | { 27 | Py_DECREF(l); 28 | throw std::runtime_error("len(upper) < 3"); 29 | } 30 | 31 | lower_[i] = PyFloat_AsDouble(l); 32 | upper_[i] = PyFloat_AsDouble(u); 33 | 34 | Py_DECREF(l); 35 | Py_DECREF(u); 36 | if(lower_[i]==-1.0 || upper_[i]==-1.0) 37 | { 38 | if(PyErr_Occurred()) 39 | throw std::runtime_error("unknown error"); 40 | } 41 | } 42 | 43 | auto pyfunc_to_cfunc = [&](double x, double y, double z) -> double { 44 | PyObject* res = PyObject_CallFunction(pyfunc, "(d,d,d)", x, y, z); 45 | if(res == NULL) 46 | return 0.0; 47 | 48 | double result = PyFloat_AsDouble(res); 49 | Py_DECREF(res); 50 | return result; 51 | }; 52 | 53 | // Marching cubes. 54 | mc::marching_cubes(lower_, upper_, numx, numy, numz, pyfunc_to_cfunc, isovalue, vertices, polygons); 55 | 56 | // Copy the result to two Python ndarrays. 57 | npy_intp size_vertices = vertices.size(); 58 | npy_intp size_polygons = polygons.size(); 59 | PyArrayObject* verticesarr = reinterpret_cast(PyArray_SimpleNew(1, &size_vertices, NPY_DOUBLE)); 60 | PyArrayObject* polygonsarr = reinterpret_cast(PyArray_SimpleNew(1, &size_polygons, NPY_ULONG)); 61 | 62 | std::vector::const_iterator it = vertices.begin(); 63 | for(int i=0; it!=vertices.end(); ++i, ++it) 64 | *reinterpret_cast(PyArray_GETPTR1(verticesarr, i)) = *it; 65 | std::vector::const_iterator it2 = polygons.begin(); 66 | for(int i=0; it2!=polygons.end(); ++i, ++it2) 67 | *reinterpret_cast(PyArray_GETPTR1(polygonsarr, i)) = *it2; 68 | 69 | PyObject* res = Py_BuildValue("(O,O)", verticesarr, polygonsarr); 70 | Py_XDECREF(verticesarr); 71 | Py_XDECREF(polygonsarr); 72 | return res; 73 | } 74 | 75 | 76 | PyObject* marching_cubes(PyArrayObject* arr, double isovalue) 77 | { 78 | if(PyArray_NDIM(arr) != 3) 79 | throw std::runtime_error("Only three-dimensional arrays are supported."); 80 | 81 | // Prepare data. 82 | npy_intp* shape = PyArray_DIMS(arr); 83 | std::array lower{0, 0, 0}; 84 | std::array upper{shape[0]-1, shape[1]-1, shape[2]-1}; 85 | long numx = upper[0] - lower[0] + 1; 86 | long numy = upper[1] - lower[1] + 1; 87 | long numz = upper[2] - lower[2] + 1; 88 | std::vector vertices; 89 | std::vector polygons; 90 | 91 | auto pyarray_to_cfunc = [&](long x, long y, long z) -> double { 92 | const npy_intp c[3] = {x, y, z}; 93 | return PyArray_SafeGet(arr, c); 94 | }; 95 | 96 | // Marching cubes. 97 | mc::marching_cubes(lower, upper, numx, numy, numz, pyarray_to_cfunc, isovalue, 98 | vertices, polygons); 99 | 100 | // Copy the result to two Python ndarrays. 101 | npy_intp size_vertices = vertices.size(); 102 | npy_intp size_polygons = polygons.size(); 103 | PyArrayObject* verticesarr = reinterpret_cast(PyArray_SimpleNew(1, &size_vertices, NPY_DOUBLE)); 104 | PyArrayObject* polygonsarr = reinterpret_cast(PyArray_SimpleNew(1, &size_polygons, NPY_ULONG)); 105 | 106 | std::vector::const_iterator it = vertices.begin(); 107 | for(int i=0; it!=vertices.end(); ++i, ++it) 108 | *reinterpret_cast(PyArray_GETPTR1(verticesarr, i)) = *it; 109 | std::vector::const_iterator it2 = polygons.begin(); 110 | for(int i=0; it2!=polygons.end(); ++i, ++it2) 111 | *reinterpret_cast(PyArray_GETPTR1(polygonsarr, i)) = *it2; 112 | 113 | PyObject* res = Py_BuildValue("(O,O)", verticesarr, polygonsarr); 114 | Py_XDECREF(verticesarr); 115 | Py_XDECREF(polygonsarr); 116 | 117 | return res; 118 | } 119 | -------------------------------------------------------------------------------- /mcubes/src/pyarraymodule.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _EXTMODULE_H 3 | #define _EXTMODULE_H 4 | 5 | #include 6 | #include 7 | 8 | // #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION 9 | #define PY_ARRAY_UNIQUE_SYMBOL mcubes_PyArray_API 10 | #define NO_IMPORT_ARRAY 11 | #include "numpy/arrayobject.h" 12 | 13 | #include 14 | 15 | template 16 | struct numpy_typemap; 17 | 18 | #define define_numpy_type(ctype, dtype) \ 19 | template<> \ 20 | struct numpy_typemap \ 21 | {static const int type = dtype;}; 22 | 23 | define_numpy_type(bool, NPY_BOOL); 24 | define_numpy_type(char, NPY_BYTE); 25 | define_numpy_type(short, NPY_SHORT); 26 | define_numpy_type(int, NPY_INT); 27 | define_numpy_type(long, NPY_LONG); 28 | define_numpy_type(long long, NPY_LONGLONG); 29 | define_numpy_type(unsigned char, NPY_UBYTE); 30 | define_numpy_type(unsigned short, NPY_USHORT); 31 | define_numpy_type(unsigned int, NPY_UINT); 32 | define_numpy_type(unsigned long, NPY_ULONG); 33 | define_numpy_type(unsigned long long, NPY_ULONGLONG); 34 | define_numpy_type(float, NPY_FLOAT); 35 | define_numpy_type(double, NPY_DOUBLE); 36 | define_numpy_type(long double, NPY_LONGDOUBLE); 37 | define_numpy_type(std::complex, NPY_CFLOAT); 38 | define_numpy_type(std::complex, NPY_CDOUBLE); 39 | define_numpy_type(std::complex, NPY_CLONGDOUBLE); 40 | 41 | template 42 | T PyArray_SafeGet(const PyArrayObject* aobj, const npy_intp* indaux) 43 | { 44 | // HORROR. 45 | npy_intp* ind = const_cast(indaux); 46 | void* ptr = PyArray_GetPtr(const_cast(aobj), ind); 47 | switch(PyArray_TYPE(aobj)) 48 | { 49 | case NPY_BOOL: 50 | return static_cast(*reinterpret_cast(ptr)); 51 | case NPY_BYTE: 52 | return static_cast(*reinterpret_cast(ptr)); 53 | case NPY_SHORT: 54 | return static_cast(*reinterpret_cast(ptr)); 55 | case NPY_INT: 56 | return static_cast(*reinterpret_cast(ptr)); 57 | case NPY_LONG: 58 | return static_cast(*reinterpret_cast(ptr)); 59 | case NPY_LONGLONG: 60 | return static_cast(*reinterpret_cast(ptr)); 61 | case NPY_UBYTE: 62 | return static_cast(*reinterpret_cast(ptr)); 63 | case NPY_USHORT: 64 | return static_cast(*reinterpret_cast(ptr)); 65 | case NPY_UINT: 66 | return static_cast(*reinterpret_cast(ptr)); 67 | case NPY_ULONG: 68 | return static_cast(*reinterpret_cast(ptr)); 69 | case NPY_ULONGLONG: 70 | return static_cast(*reinterpret_cast(ptr)); 71 | case NPY_FLOAT: 72 | return static_cast(*reinterpret_cast(ptr)); 73 | case NPY_DOUBLE: 74 | return static_cast(*reinterpret_cast(ptr)); 75 | case NPY_LONGDOUBLE: 76 | return static_cast(*reinterpret_cast(ptr)); 77 | default: 78 | throw std::runtime_error("data type not supported"); 79 | } 80 | } 81 | 82 | template 83 | T PyArray_SafeSet(PyArrayObject* aobj, const npy_intp* indaux, const T& value) 84 | { 85 | // HORROR. 86 | npy_intp* ind = const_cast(indaux); 87 | void* ptr = PyArray_GetPtr(aobj, ind); 88 | switch(PyArray_TYPE(aobj)) 89 | { 90 | case NPY_BOOL: 91 | *reinterpret_cast(ptr) = static_cast(value); 92 | break; 93 | case NPY_BYTE: 94 | *reinterpret_cast(ptr) = static_cast(value); 95 | break; 96 | case NPY_SHORT: 97 | *reinterpret_cast(ptr) = static_cast(value); 98 | break; 99 | case NPY_INT: 100 | *reinterpret_cast(ptr) = static_cast(value); 101 | break; 102 | case NPY_LONG: 103 | *reinterpret_cast(ptr) = static_cast(value); 104 | break; 105 | case NPY_LONGLONG: 106 | *reinterpret_cast(ptr) = static_cast(value); 107 | break; 108 | case NPY_UBYTE: 109 | *reinterpret_cast(ptr) = static_cast(value); 110 | break; 111 | case NPY_USHORT: 112 | *reinterpret_cast(ptr) = static_cast(value); 113 | break; 114 | case NPY_UINT: 115 | *reinterpret_cast(ptr) = static_cast(value); 116 | break; 117 | case NPY_ULONG: 118 | *reinterpret_cast(ptr) = static_cast(value); 119 | break; 120 | case NPY_ULONGLONG: 121 | *reinterpret_cast(ptr) = static_cast(value); 122 | break; 123 | case NPY_FLOAT: 124 | *reinterpret_cast(ptr) = static_cast(value); 125 | break; 126 | case NPY_DOUBLE: 127 | *reinterpret_cast(ptr) = static_cast(value); 128 | break; 129 | case NPY_LONGDOUBLE: 130 | *reinterpret_cast(ptr) = static_cast(value); 131 | break; 132 | default: 133 | throw std::runtime_error("data type not supported"); 134 | } 135 | } 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /mcubes/src/marchingcubes.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _MARCHING_CUBES_H 3 | #define _MARCHING_CUBES_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace mc 10 | { 11 | 12 | extern int edge_table[256]; 13 | extern int triangle_table[256][16]; 14 | 15 | namespace private_ 16 | { 17 | 18 | double mc_isovalue_interpolation(double isovalue, double f1, double f2, 19 | double x1, double x2); 20 | size_t mc_add_vertex(double x1, double y1, double z1, double c2, 21 | int axis, double f1, double f2, double isovalue, std::vector* vertices); 22 | } 23 | 24 | template 25 | void marching_cubes(const vector3& lower, const vector3& upper, 26 | int numx, int numy, int numz, formula f, double isovalue, 27 | std::vector& vertices, std::vector& polygons) 28 | { 29 | using coord_type = typename vector3::value_type; 30 | using size_type = typename vector3::size_type; 31 | using namespace private_; 32 | 33 | // Some initial checks 34 | if(numx < 2 || numy < 2 || numz < 2) 35 | return; 36 | 37 | if(!std::equal(std::begin(lower), std::end(lower), std::begin(upper), 38 | [](double a, double b)->bool {return a <= b;})) 39 | return; 40 | 41 | // numx, numy and numz are the numbers of evaluations in each direction 42 | --numx; --numy; --numz; 43 | 44 | coord_type dx = (upper[0] - lower[0]) / static_cast(numx); 45 | coord_type dy = (upper[1] - lower[1]) / static_cast(numy); 46 | coord_type dz = (upper[2] - lower[2]) / static_cast(numz); 47 | 48 | const int num_shared_indices = 2 * (numy + 1) * (numz + 1); 49 | std::vector shared_indices_x(num_shared_indices); 50 | std::vector shared_indices_y(num_shared_indices); 51 | std::vector shared_indices_z(num_shared_indices); 52 | auto _offset = [&](size_t i, size_t j, size_t k){return i*(numy+1)*(numz+1) + j*(numz+1) + k;}; 53 | 54 | for(int i=0; i indices; 89 | if(edges & 0x040) 90 | { 91 | size_t index = mc_add_vertex(x_dx, y_dy, z_dz, x, 0, v[6], v[7], isovalue, &vertices); 92 | indices[6] = index; 93 | shared_indices_x[_offset(i_mod_2_inv, j+1, k+1)] = index; 94 | } 95 | if(edges & 0x020) 96 | { 97 | size_t index = mc_add_vertex(x_dx, y, z_dz, y_dy, 1, v[5], v[6], isovalue, &vertices); 98 | indices[5] = index; 99 | shared_indices_y[_offset(i_mod_2_inv, j+1, k+1)] = index; 100 | } 101 | if(edges & 0x400) 102 | { 103 | size_t index = mc_add_vertex(x_dx, y_dy, z, z_dz, 2, v[2], v[6], isovalue, &vertices); 104 | indices[10] = index; 105 | shared_indices_z[_offset(i_mod_2_inv, j+1, k+1)] = index; 106 | } 107 | 108 | if(edges & 0x001) 109 | { 110 | if(j == 0 && k == 0) 111 | { 112 | size_t index = mc_add_vertex(x, y, z, x_dx, 0, v[0], v[1], isovalue, &vertices); 113 | indices[0] = index; 114 | } 115 | else 116 | indices[0] = shared_indices_x[_offset(i_mod_2_inv, j, k)]; 117 | } 118 | if(edges & 0x002) 119 | { 120 | if(k == 0) 121 | { 122 | size_t index = mc_add_vertex(x_dx, y, z, y_dy, 1, v[1], v[2], isovalue, &vertices); 123 | indices[1] = index; 124 | shared_indices_y[_offset(i_mod_2_inv, j+1, k)] = index; 125 | } 126 | else 127 | indices[1] = shared_indices_y[_offset(i_mod_2_inv, j+1, k)]; 128 | } 129 | if(edges & 0x004) 130 | { 131 | if(k == 0) 132 | { 133 | size_t index = mc_add_vertex(x_dx, y_dy, z, x, 0, v[2], v[3], isovalue, &vertices); 134 | indices[2] = index; 135 | shared_indices_x[_offset(i_mod_2_inv, j+1, k)] = index; 136 | } 137 | else 138 | indices[2] = shared_indices_x[_offset(i_mod_2_inv, j+1, k)]; 139 | } 140 | if(edges & 0x008) 141 | { 142 | if(i == 0 && k == 0) 143 | { 144 | size_t index = mc_add_vertex(x, y_dy, z, y, 1, v[3], v[0], isovalue, &vertices); 145 | indices[3] = index; 146 | } 147 | else 148 | indices[3] = shared_indices_y[_offset(i_mod_2, j+1, k)]; 149 | } 150 | if(edges & 0x010) 151 | { 152 | if(j == 0) 153 | { 154 | size_t index = mc_add_vertex(x, y, z_dz, x_dx, 0, v[4], v[5], isovalue, &vertices); 155 | indices[4] = index; 156 | shared_indices_x[_offset(i_mod_2_inv, j, k+1)] = index; 157 | } 158 | else 159 | indices[4] = shared_indices_x[_offset(i_mod_2_inv, j, k+1)]; 160 | } 161 | if(edges & 0x080) 162 | { 163 | if(i == 0) 164 | { 165 | size_t index = mc_add_vertex(x, y_dy, z_dz, y, 1, v[7], v[4], isovalue, &vertices); 166 | indices[7] = index; 167 | shared_indices_y[_offset(i_mod_2, j+1, k+1)] = index; 168 | } 169 | else 170 | indices[7] = shared_indices_y[_offset(i_mod_2, j+1, k+1)]; 171 | } 172 | if(edges & 0x100) 173 | { 174 | if(i == 0 && j == 0) 175 | { 176 | size_t index = mc_add_vertex(x, y, z, z_dz, 2, v[0], v[4], isovalue, &vertices); 177 | indices[8] = index; 178 | } 179 | else 180 | indices[8] = shared_indices_z[_offset(i_mod_2, j, k+1)]; 181 | } 182 | if(edges & 0x200) 183 | { 184 | if(j == 0) 185 | { 186 | size_t index = mc_add_vertex(x_dx, y, z, z_dz, 2, v[1], v[5], isovalue, &vertices); 187 | indices[9] = index; 188 | shared_indices_z[_offset(i_mod_2_inv, j, k+1)] = index; 189 | } 190 | else 191 | indices[9] = shared_indices_z[_offset(i_mod_2_inv, j, k+1)]; 192 | } 193 | if(edges & 0x800) 194 | { 195 | if(i == 0) 196 | { 197 | size_t index = mc_add_vertex(x, y_dy, z, z_dz, 2, v[3], v[7], isovalue, &vertices); 198 | indices[11] = index; 199 | shared_indices_z[_offset(i_mod_2, j+1, k+1)] = index; 200 | } 201 | else 202 | indices[11] = shared_indices_z[_offset(i_mod_2, j+1, k+1)]; 203 | } 204 | 205 | int tri; 206 | int* triangle_table_ptr = triangle_table[cubeindex]; 207 | for(int m=0; tri = triangle_table_ptr[m], tri != -1; ++m) 208 | polygons.push_back(indices[tri]); 209 | } 210 | } 211 | } 212 | 213 | } 214 | 215 | } 216 | 217 | #endif // _MARCHING_CUBES_H 218 | -------------------------------------------------------------------------------- /mcubes/smoothing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Utilities for smoothing the 0.5 level-set of binary arrays. 5 | """ 6 | 7 | import logging 8 | from typing import Tuple 9 | 10 | import numpy as np 11 | from scipy import sparse 12 | from scipy import ndimage as ndi 13 | 14 | __all__ = [ 15 | 'smooth', 16 | 'smooth_constrained', 17 | 'smooth_gaussian', 18 | 'signed_distance_function' 19 | ] 20 | 21 | 22 | def _build_variable_indices(band: np.ndarray) -> np.ndarray: 23 | num_variables = np.count_nonzero(band) 24 | variable_indices = np.full(band.shape, -1, dtype=np.int32) 25 | variable_indices[band] = np.arange(num_variables) 26 | return variable_indices 27 | 28 | 29 | def _buildq3d(variable_indices: np.ndarray): 30 | """ 31 | Builds the filterq matrix for the given variables. 32 | """ 33 | 34 | num_variables = variable_indices.max() + 1 35 | filterq = sparse.lil_matrix((3*num_variables, num_variables)) 36 | 37 | # Pad variable_indices to simplify out-of-bounds accesses 38 | variable_indices = np.pad( 39 | variable_indices, 40 | [(0, 1), (0, 1), (0, 1)], 41 | mode='constant', 42 | constant_values=-1 43 | ) 44 | 45 | coords = np.nonzero(variable_indices >= 0) 46 | for count, (i, j, k) in enumerate(zip(*coords)): 47 | 48 | assert variable_indices[i, j, k] == count 49 | 50 | filterq[3*count, count] = -2 51 | neighbor = variable_indices[i-1, j, k] 52 | if neighbor >= 0: 53 | filterq[3*count, neighbor] = 1 54 | else: 55 | filterq[3*count, count] += 1 56 | 57 | neighbor = variable_indices[i+1, j, k] 58 | if neighbor >= 0: 59 | filterq[3*count, neighbor] = 1 60 | else: 61 | filterq[3*count, count] += 1 62 | 63 | filterq[3*count+1, count] = -2 64 | neighbor = variable_indices[i, j-1, k] 65 | if neighbor >= 0: 66 | filterq[3*count+1, neighbor] = 1 67 | else: 68 | filterq[3*count+1, count] += 1 69 | 70 | neighbor = variable_indices[i, j+1, k] 71 | if neighbor >= 0: 72 | filterq[3*count+1, neighbor] = 1 73 | else: 74 | filterq[3*count+1, count] += 1 75 | 76 | filterq[3*count+2, count] = -2 77 | neighbor = variable_indices[i, j, k-1] 78 | if neighbor >= 0: 79 | filterq[3*count+2, neighbor] = 1 80 | else: 81 | filterq[3*count+2, count] += 1 82 | 83 | neighbor = variable_indices[i, j, k+1] 84 | if neighbor >= 0: 85 | filterq[3*count+2, neighbor] = 1 86 | else: 87 | filterq[3*count+2, count] += 1 88 | 89 | filterq = filterq.tocsr() 90 | return filterq.T.dot(filterq) 91 | 92 | 93 | def _buildq2d(variable_indices: np.ndarray): 94 | """ 95 | Builds the filterq matrix for the given variables. 96 | 97 | Version for 2 dimensions. 98 | """ 99 | 100 | num_variables = variable_indices.max() + 1 101 | filterq = sparse.lil_matrix((3*num_variables, num_variables)) 102 | 103 | # Pad variable_indices to simplify out-of-bounds accesses 104 | variable_indices = np.pad( 105 | variable_indices, 106 | [(0, 1), (0, 1)], 107 | mode='constant', 108 | constant_values=-1 109 | ) 110 | 111 | coords = np.nonzero(variable_indices >= 0) 112 | for count, (i, j) in enumerate(zip(*coords)): 113 | 114 | assert variable_indices[i, j] == count 115 | 116 | filterq[2*count, count] = -2 117 | neighbor = variable_indices[i-1, j] 118 | if neighbor >= 0: 119 | filterq[2*count, neighbor] = 1 120 | else: 121 | filterq[2*count, count] += 1 122 | 123 | neighbor = variable_indices[i+1, j] 124 | if neighbor >= 0: 125 | filterq[2*count, neighbor] = 1 126 | else: 127 | filterq[2*count, count] += 1 128 | 129 | filterq[2*count+1, count] = -2 130 | neighbor = variable_indices[i, j-1] 131 | if neighbor >= 0: 132 | filterq[2*count+1, neighbor] = 1 133 | else: 134 | filterq[2*count+1, count] += 1 135 | 136 | neighbor = variable_indices[i, j+1] 137 | if neighbor >= 0: 138 | filterq[2*count+1, neighbor] = 1 139 | else: 140 | filterq[2*count+1, count] += 1 141 | 142 | filterq = filterq.tocsr() 143 | return filterq.T.dot(filterq) 144 | 145 | 146 | def _jacobi( 147 | filterq, 148 | x0: np.ndarray, 149 | lower_bound: np.ndarray, 150 | upper_bound: np.ndarray, 151 | max_iters: int = 10, 152 | rel_tol: float = 1e-6, 153 | weight: float = 0.5): 154 | """Jacobi method with constraints.""" 155 | 156 | jacobi_r = sparse.lil_matrix(filterq) 157 | shp = jacobi_r.shape 158 | jacobi_d = 1.0 / filterq.diagonal() 159 | jacobi_r.setdiag((0,) * shp[0]) 160 | jacobi_r = jacobi_r.tocsr() 161 | 162 | x = x0 163 | 164 | # We check the stopping criterion each 10 iterations 165 | check_each = 10 166 | cum_rel_tol = 1 - (1 - rel_tol)**check_each 167 | 168 | energy_now = np.dot(x, filterq.dot(x)) / 2 169 | logging.debug("Energy at iter %d: %.6g", 0, energy_now) 170 | for i in range(max_iters): 171 | 172 | x_1 = - jacobi_d * jacobi_r.dot(x) 173 | x = weight * x_1 + (1 - weight) * x 174 | 175 | # Constraints. 176 | x = np.maximum(x, lower_bound) 177 | x = np.minimum(x, upper_bound) 178 | 179 | # Stopping criterion 180 | if (i + 1) % check_each == 0: 181 | # Update energy 182 | energy_before = energy_now 183 | energy_now = np.dot(x, filterq.dot(x)) / 2 184 | 185 | logging.debug("Energy at iter %d: %.6g", i + 1, energy_now) 186 | 187 | # Check stopping criterion 188 | cum_rel_improvement = (energy_before - energy_now) / energy_before 189 | if cum_rel_improvement < cum_rel_tol: 190 | break 191 | 192 | return x 193 | 194 | 195 | def signed_distance_function( 196 | levelset: np.ndarray, 197 | band_radius: int 198 | ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 199 | """ 200 | Return the distance to the 0.5 levelset of a function, the mask of the 201 | border (i.e., the nearest cells to the 0.5 level-set) and the mask of the 202 | band (i.e., the cells of the function whose distance to the 0.5 level-set 203 | is less of equal to `band_radius`). 204 | """ 205 | 206 | binary_array = np.where(levelset > 0, True, False) 207 | 208 | # Compute the band and the border. 209 | dist_func = ndi.distance_transform_edt 210 | distance = np.where( 211 | binary_array, 212 | dist_func(binary_array) - 0.5, 213 | -dist_func(~binary_array) + 0.5 214 | ) 215 | border = np.abs(distance) < 1 216 | band = np.abs(distance) <= band_radius 217 | 218 | return distance, border, band 219 | 220 | 221 | def smooth_constrained( 222 | binary_array: np.ndarray, 223 | band_radius: int = 4, 224 | max_iters: int = 500, 225 | rel_tol: float = 1e-6 226 | ) -> np.ndarray: 227 | """ 228 | Implementation of the smoothing method from 229 | 230 | "Surface Extraction from Binary Volumes with Higher-Order Smoothness" 231 | Victor Lempitsky, CVPR10 232 | """ 233 | 234 | # # Compute the distance map, the border and the band. 235 | logging.info("Computing distance transform...") 236 | distance, _, band = signed_distance_function(binary_array, band_radius) 237 | 238 | variable_indices = _build_variable_indices(band) 239 | 240 | # Compute filterq. 241 | logging.info("Building matrix filterq...") 242 | if binary_array.ndim == 3: 243 | filterq = _buildq3d(variable_indices) 244 | elif binary_array.ndim == 2: 245 | filterq = _buildq2d(variable_indices) 246 | else: 247 | raise ValueError("binary_array.ndim not in [2, 3]") 248 | 249 | # Initialize the variables. 250 | res = np.asarray(distance, dtype=np.double) 251 | x = res[band] 252 | upper_bound = np.where(x < 0, x, np.inf) 253 | lower_bound = np.where(x > 0, x, -np.inf) 254 | 255 | upper_bound[np.abs(upper_bound) < 1] = 0 256 | lower_bound[np.abs(lower_bound) < 1] = 0 257 | 258 | # Solve. 259 | logging.info("Minimizing energy...") 260 | x = _jacobi( 261 | filterq=filterq, 262 | x0=x, 263 | lower_bound=lower_bound, 264 | upper_bound=upper_bound, 265 | max_iters=max_iters, 266 | rel_tol=rel_tol 267 | ) 268 | 269 | res[band] = x 270 | return res 271 | 272 | 273 | def smooth_gaussian(binary_array: np.ndarray, sigma: float = 3) -> np.ndarray: 274 | vol = np.float64(binary_array) - 0.5 275 | return ndi.gaussian_filter(vol, sigma=sigma) 276 | 277 | 278 | def smooth( 279 | binary_array: np.ndarray, 280 | method: str = 'auto', 281 | **kwargs 282 | ) -> np.ndarray: 283 | """ 284 | Smooths the 0.5 level-set of a binary array. Returns a floating-point 285 | array with a smoothed version of the original level-set in the 0 isovalue. 286 | 287 | This function can apply two different methods: 288 | 289 | - A constrained smoothing method which preserves details and fine 290 | structures, but it is slow and requires a large amount of memory. This 291 | method is recommended when the input array is small (smaller than 292 | (500, 500, 500)). 293 | - A Gaussian filter applied over the binary array. This method is fast, but 294 | not very precise, as it can destroy fine details. It is only recommended 295 | when the input array is large and the 0.5 level-set does not contain 296 | thin structures. 297 | 298 | Parameters 299 | ---------- 300 | binary_array : ndarray 301 | Input binary array with the 0.5 level-set to smooth. 302 | method : str, one of ['auto', 'gaussian', 'constrained'] 303 | Smoothing method. If 'auto' is given, the method will be automatically 304 | chosen based on the size of `binary_array`. 305 | 306 | Parameters for 'gaussian' 307 | ------------------------- 308 | sigma : float 309 | Size of the Gaussian filter (default 3). 310 | 311 | Parameters for 'constrained' 312 | ---------------------------- 313 | max_iters : positive integer 314 | Number of iterations of the constrained optimization method 315 | (default 500). 316 | rel_tol: float 317 | Relative tolerance as a stopping criterion (default 1e-6). 318 | 319 | Output 320 | ------ 321 | res : ndarray 322 | Floating-point array with a smoothed 0 level-set. 323 | """ 324 | 325 | binary_array = np.asarray(binary_array) 326 | 327 | if method == 'auto': 328 | if binary_array.size > 500**3: 329 | method = 'gaussian' 330 | else: 331 | method = 'constrained' 332 | 333 | if method == 'gaussian': 334 | return smooth_gaussian(binary_array, **kwargs) 335 | 336 | if method == 'constrained': 337 | return smooth_constrained(binary_array, **kwargs) 338 | 339 | raise ValueError("Unknown method '{}'".format(method)) 340 | -------------------------------------------------------------------------------- /mcubes/src/marchingcubes.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "marchingcubes.h" 3 | 4 | namespace mc 5 | { 6 | 7 | int edge_table[256] = 8 | { 9 | 0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, 10 | 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 11 | 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 12 | 0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 13 | 0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, 14 | 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 15 | 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c, 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 16 | 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 17 | 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, 18 | 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, 19 | 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 20 | 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460, 21 | 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0, 22 | 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230, 23 | 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190, 24 | 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000 25 | }; 26 | 27 | int triangle_table[256][16] = 28 | { 29 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 30 | {0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 31 | {0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 32 | {1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 33 | {1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 34 | {0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 35 | {9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 36 | {2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1}, 37 | {3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 38 | {0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 39 | {1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 40 | {1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1}, 41 | {3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 42 | {0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1}, 43 | {3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1}, 44 | {9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 45 | {4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 46 | {4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 47 | {0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 48 | {4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1}, 49 | {1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 50 | {3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1}, 51 | {9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, 52 | {2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1}, 53 | {8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 54 | {11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1}, 55 | {9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, 56 | {4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1}, 57 | {3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1}, 58 | {1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1}, 59 | {4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1}, 60 | {4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1}, 61 | {9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 62 | {9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 63 | {0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 64 | {8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1}, 65 | {1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 66 | {3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, 67 | {5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1}, 68 | {2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1}, 69 | {9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 70 | {0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, 71 | {0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, 72 | {2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1}, 73 | {10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1}, 74 | {4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1}, 75 | {5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1}, 76 | {5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1}, 77 | {9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 78 | {9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1}, 79 | {0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1}, 80 | {1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 81 | {9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1}, 82 | {10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1}, 83 | {8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1}, 84 | {2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1}, 85 | {7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1}, 86 | {9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1}, 87 | {2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1}, 88 | {11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1}, 89 | {9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1}, 90 | {5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1}, 91 | {11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1}, 92 | {11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 93 | {10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 94 | {0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 95 | {9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 96 | {1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, 97 | {1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 98 | {1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1}, 99 | {9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1}, 100 | {5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1}, 101 | {2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 102 | {11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, 103 | {0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, 104 | {5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1}, 105 | {6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1}, 106 | {0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1}, 107 | {3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1}, 108 | {6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1}, 109 | {5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 110 | {4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1}, 111 | {1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, 112 | {10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1}, 113 | {6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1}, 114 | {1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1}, 115 | {8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1}, 116 | {7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1}, 117 | {3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, 118 | {5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1}, 119 | {0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1}, 120 | {9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1}, 121 | {8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1}, 122 | {5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1}, 123 | {0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1}, 124 | {6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1}, 125 | {10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 126 | {4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1}, 127 | {10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1}, 128 | {8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1}, 129 | {1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1}, 130 | {3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1}, 131 | {0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 132 | {8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1}, 133 | {10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1}, 134 | {0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1}, 135 | {3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1}, 136 | {6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1}, 137 | {9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1}, 138 | {8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1}, 139 | {3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1}, 140 | {6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 141 | {7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1}, 142 | {0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1}, 143 | {10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1}, 144 | {10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1}, 145 | {1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1}, 146 | {2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1}, 147 | {7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1}, 148 | {7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 149 | {2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1}, 150 | {2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1}, 151 | {1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1}, 152 | {11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1}, 153 | {8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1}, 154 | {0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 155 | {7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1}, 156 | {7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 157 | {7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 158 | {3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 159 | {0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 160 | {8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, 161 | {10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 162 | {1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, 163 | {2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, 164 | {6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1}, 165 | {7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 166 | {7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1}, 167 | {2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1}, 168 | {1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1}, 169 | {10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1}, 170 | {10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1}, 171 | {0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1}, 172 | {7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1}, 173 | {6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 174 | {3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1}, 175 | {8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1}, 176 | {9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1}, 177 | {6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1}, 178 | {1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1}, 179 | {4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1}, 180 | {10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1}, 181 | {8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1}, 182 | {0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 183 | {1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1}, 184 | {1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1}, 185 | {8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1}, 186 | {10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1}, 187 | {4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1}, 188 | {10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 189 | {4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 190 | {0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, 191 | {5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, 192 | {11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1}, 193 | {9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, 194 | {6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1}, 195 | {7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1}, 196 | {3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1}, 197 | {7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1}, 198 | {9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1}, 199 | {3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1}, 200 | {6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1}, 201 | {9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1}, 202 | {1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1}, 203 | {4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1}, 204 | {7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1}, 205 | {6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1}, 206 | {3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1}, 207 | {0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1}, 208 | {6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1}, 209 | {1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1}, 210 | {0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1}, 211 | {11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1}, 212 | {6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1}, 213 | {5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1}, 214 | {9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1}, 215 | {1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1}, 216 | {1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 217 | {1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1}, 218 | {10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1}, 219 | {0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 220 | {10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 221 | {11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 222 | {11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1}, 223 | {5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1}, 224 | {10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1}, 225 | {11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1}, 226 | {0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1}, 227 | {9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1}, 228 | {7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1}, 229 | {2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1}, 230 | {8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1}, 231 | {9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1}, 232 | {9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1}, 233 | {1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 234 | {0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1}, 235 | {9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1}, 236 | {9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 237 | {5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1}, 238 | {5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1}, 239 | {0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1}, 240 | {10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1}, 241 | {2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1}, 242 | {0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1}, 243 | {0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1}, 244 | {9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 245 | {2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1}, 246 | {5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1}, 247 | {3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1}, 248 | {5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1}, 249 | {8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1}, 250 | {0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 251 | {8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1}, 252 | {9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 253 | {4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1}, 254 | {0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1}, 255 | {1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1}, 256 | {3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1}, 257 | {4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1}, 258 | {9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1}, 259 | {11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1}, 260 | {11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1}, 261 | {2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1}, 262 | {9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1}, 263 | {3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1}, 264 | {1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 265 | {4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1}, 266 | {4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1}, 267 | {4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 268 | {4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 269 | {9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 270 | {3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1}, 271 | {0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1}, 272 | {3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 273 | {1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1}, 274 | {3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1}, 275 | {0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 276 | {3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 277 | {2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1}, 278 | {9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 279 | {2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1}, 280 | {1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 281 | {1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 282 | {0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 283 | {0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 284 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1} 285 | }; 286 | 287 | namespace private_ 288 | { 289 | 290 | double mc_isovalue_interpolation(double isovalue, double f1, double f2, 291 | double x1, double x2) 292 | { 293 | if(f2==f1) 294 | return (x2+x1)/2; 295 | 296 | return (x2-x1)*(isovalue-f1)/(f2-f1) + x1; 297 | } 298 | 299 | size_t mc_add_vertex(double x1, double y1, double z1, double c2, 300 | int axis, double f1, double f2, double isovalue, std::vector* vertices) 301 | { 302 | size_t vertex_index = vertices->size() / 3; 303 | if(axis == 0) 304 | { 305 | double x = mc_isovalue_interpolation(isovalue, f1, f2, x1, c2); 306 | vertices->push_back(x); 307 | vertices->push_back(y1); 308 | vertices->push_back(z1); 309 | return vertex_index; 310 | } 311 | if(axis == 1) 312 | { 313 | double y = mc_isovalue_interpolation(isovalue, f1, f2, y1, c2); 314 | vertices->push_back(x1); 315 | vertices->push_back(y); 316 | vertices->push_back(z1); 317 | return vertex_index; 318 | } 319 | if(axis == 2) 320 | { 321 | double z = mc_isovalue_interpolation(isovalue, f1, f2, z1, c2); 322 | vertices->push_back(x1); 323 | vertices->push_back(y1); 324 | vertices->push_back(z); 325 | return vertex_index; 326 | } 327 | 328 | // This should not happen. 329 | return -1; 330 | } 331 | 332 | } 333 | 334 | } 335 | --------------------------------------------------------------------------------