├── .gitattributes
├── LICENSE
├── MANIFEST.in
├── README.md
├── examples
├── CC0.txt
├── quatrefoil_voronoi.py
└── shield.py
├── lgpl-2.1.txt
├── meshlabxml
├── __init__.py
├── clean.py
├── color_names.py
├── color_names.txt
├── compute.py
├── create.py
├── delete.py
├── files.py
├── layers.py
├── mlx.py
├── mp_func.py
├── normals.py
├── remesh.py
├── sampling.py
├── select.py
├── smooth.py
├── subdivide.py
├── texture.py
├── transfer.py
├── transform.py
├── util.py
└── vert_color.py
├── models
├── bunny.txt
├── bunny_flat(1Z).ply
└── bunny_raw(-1250Y).ply
├── setup.cfg
├── setup.py
└── test
├── black.png
├── blue.png
├── cyan.png
├── green.png
├── magenta.png
├── red.png
├── white.png
└── yellow.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | #*.c text
7 | #*.h text
8 |
9 | # Declare files that will always have CRLF line endings on checkout.
10 | #*.sln text eol=crlf
11 |
12 | # Denote all files that are truly binary and should not be modified.
13 | *.png binary
14 | *.jpg binary
15 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include lgpl-2.1.txt
2 | include README.md
3 | include examples/CC0.txt
4 | include examples/shield.py
5 | include examples/quatrefoil_voronoi.py
6 | include meshlabxml/color_names.txt
7 | include models/bunny.txt
8 | include models/bunny_raw(-1250Y).ply
9 | include models/bunny_flat(1Z).ply
10 | recursive-exclude test *
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 | MLX, or **M**esh**L**ab**X**ML, is a Python (2.7 or 3) scripting interface to [MeshLab](http://www.meshlab.net/), the open source system for processing and editing 3D triangular meshes.
5 |
6 | Under the hood, MLX generates XML filter scripts that can then be executed headless with the meshlabserver executable or run in the MeshLab GUI. It can also parse some of MeshLab's output, such as the results of the measure_geometry and measure_topology functions.
7 |
8 | MLX is named after the .mlx file extension for MeshLab script files, however the name was already taken on PyPi (for an unrelated machine learning library), so it is formally registered under the longer name of MeshLabXML.
9 |
10 | ## Installation
11 |
12 | MLX can be installed via [PyPI](https://pypi.org/project/MeshLabXML/) and pip:
13 |
14 | pip install meshlabxml
15 |
16 | The released PyPI version may lag behind this git repository somewhat, so install from git if you want the latest and greatest. MLX may also be installed and run in other Python environments, such as [Blender](https://www.blender.org/). Note that Blender does not come with pip by default, however it can be easily installed using [get-pip](https://bootstrap.pypa.io/get-pip.py).
17 |
18 | ## Platforms & Versions
19 |
20 | *Platforms:* MLX should work anywhere that MeshLab will run, including Windows, Mac & Linux, although it is only routinely tested on 64 bit Windows.
21 |
22 | *Python:* MLX should work under Python 2.7 and 3.x, although it is only routinely tested on 64 bit >=3.5
23 |
24 | *MeshLab:* MLX is known to work on MeshLab versions 1.34BETA (64 bit Windows only) and 2016.12. As of the time of this writing, not all functions have been tested with 2016.12 yet, so please open an issue if you find a bug.
25 |
26 | *MLX version numbers:* PyPI releases are numbered by the year and month of release, e.g. 2017.9. A letter may be added on the end ("a", "b", "c", etc) if there is more than one release in a month.
27 |
28 | ## Filters & Functions
29 |
30 | MLX contains a fairly large subset of the filters available in MeshLab. Additional filters will be added over time, generally on an "as I need them" basis. If you need a filter that is not yet incorporated, please open an issue.
31 |
32 | Many of the functions below are a direct implementation of a MeshLab filter. Others are created from a combination of other functions, or implement new functionality using the [muparser](http://beltoforion.de/article.php?a=muparser) function filters.
33 |
34 | Documentation for most filters is available by using "help" within a Python shell, although there are many that still need to be documented. In addition, in many cases the documentation is taken directly from the MeshLab filter, which is not always sufficient to understand the function if you are not already familiar with how it works.
35 |
36 | *mlx* - functions to create and run scripts, determine inputs & outputs, etc.
37 | * FilterScript - Main class to create scripts
38 | * create_mlp
39 | * find_texture_files
40 | * default_output_mask
41 | * run
42 |
43 | *mlx.create* - functions that create a new mesh
44 | * grid
45 | * cube
46 | * cube_hires
47 | * cube_open_hires
48 | * cylinder
49 | * cylinder_open_hires
50 | * tube_hires
51 | * icosphere
52 | * half_sphere_hires
53 | * sphere_cap
54 | * plane_hires_edges
55 | * annulus
56 | * annulus_hires
57 | * torus
58 |
59 | *mlx.transform* - functions that transform, deform or morph mesh geometry
60 | * translate
61 | * translate2
62 | * rotate
63 | * rotate2
64 | * scale
65 | * scale2
66 | * freeze_matrix
67 | * function
68 | * function_cyl_co
69 | * wrap2cylinder
70 | * wrap2sphere
71 | * emboss_sphere
72 | * bend
73 | * deform2curve
74 |
75 | *mlx.select* - functions that work with selections
76 | * all
77 | * none
78 | * invert
79 | * border
80 | * grow
81 | * shrink
82 | * self_intersecting_face
83 | * nonmanifold_vert
84 | * nonmanifold_edge
85 | * small_parts
86 | * vert_quality
87 | * face_function
88 | * vert_function
89 | * cylindrical_vert
90 | * spherical_vert
91 |
92 | *mlx.delete* - functions that delete faces and/or vertices
93 | * nonmanifold_vert
94 | * nonmanifold_edge
95 | * small_parts
96 | * selected
97 | * faces_from_nonmanifold_edges
98 | * unreferenced_vert
99 | * duplicate_faces
100 | * duplicate_verts
101 | * zero_area_face
102 |
103 | *mlx.clean* - functions to clean and repair a mesh
104 | * merge_vert
105 | * close_holes
106 | * split_vert_on_nonmanifold_face
107 | * fix_folded_face
108 | * snap_mismatched_borders
109 |
110 | *mlx.layers* - functions that work with mesh layers
111 | * join
112 | * delete
113 | * rename
114 | * change
115 | * duplicate
116 | * split_parts
117 |
118 | *mlx.normals* - functions that work with normals
119 | * reorient
120 | * flip
121 | * fix
122 | * point_sets
123 |
124 | *mlx.remesh* - remeshing functions
125 | * simplify
126 | * uniform_resampling
127 | * hull
128 | * surface_poisson
129 | * surface_poisson_screened
130 | * curvature_flipping
131 | * voronoi
132 |
133 | *mlx.sampling* - sampling functions
134 | * hausdorff_distance
135 | * poisson_disk
136 | * mesh_element
137 | * clustered_vert
138 |
139 | *mlx.smooth* - smoothing functions
140 | * laplacian
141 | * hc_laplacian
142 | * taubin
143 | * twostep
144 | * depth
145 |
146 | *mlx.subdivide* - subdivision functions
147 | * loop
148 | * ls3loop
149 | * midpoint
150 | * butterfly
151 | * catmull_clark
152 |
153 | *mlx.texture* - functions that work with textures and UV mapping (parameterization)
154 | * flat_plane
155 | * per_triangle
156 | * voronoi
157 | * isometric
158 | * isometric_build_atlased_mesh
159 | * isometric_save
160 | * isometric_load
161 | * isometric_transfer
162 | * isometric_remesh
163 | * set_texture
164 | * project_rasters
165 | * param_texture_from_rasters
166 | * param_from_rasters
167 |
168 | *mlx.transfer* - functions to transfer attributes
169 | * tex2vc
170 | * vc2tex
171 | * fc2vc
172 | * vc2fc
173 | * mesh2fc
174 | * vert_attr_2_meshes
175 | * vert_attr2tex_2_meshes
176 | * tex2vc_2_meshes
177 |
178 | *mlx.compute* - functions that measure or perform a computation
179 | * section
180 | * measure_geometry
181 | * measure_topology
182 | * parse_geometry
183 | * parse_topology
184 |
185 | *mlx.vert_color* - functions that work with vertex colors
186 | * function
187 | * voronoi
188 | * cyclic_rainbow
189 |
190 | *mlx.mp_func* - functions to work with muparser filter functions, this is mostly a vector math library
191 | * muparser_ref
192 | * v_cross
193 | * v_dot
194 | * v_add
195 | * v_subtract
196 | * v_multiply
197 | * v_length
198 | * v_normalize
199 | * torus_knot
200 | * torus_knot_bbox
201 | * vert_attr
202 | * face_attr
203 | * vq_function
204 | * fq_function
205 |
206 | *mlx.files* - functions that operate directly on files, usually to measure them
207 | * measure_aabb
208 | * measure_section
209 | * measure_geometry
210 | * measure_topology
211 | * measure_all
212 | * measure_dimension
213 |
214 |
215 | ## Possible Workflow
216 |
217 | For production MLX can run completely headless, however while developing new scripts some visual feedback can be helpful.
218 |
219 | MLX does not have an integrated GUI like OpenSCAD or Blender, however you can simulate one by arranging several programs into a useful layout, such as shown below. This can be accomplished pretty easily in modern versions of Windows using the Windows key and the arrow keys.
220 |
221 | Generally you will want a text editor, a console and of course MeshLab itself. The general script development workflow may consist of the following steps
222 | 1. Write & edit script in text editor
223 | 2. Run script in console and view meshlabserver output
224 | 3. Load output in MeshLab. Use the "reload" button to reload the mesh after any changes are made.
225 | 4. Repeat ad nauseam
226 |
227 | 
228 |
229 | ## Examples
230 |
231 | Some simple examples are shown below. These assume that the meshlabserver executable is in your path. If it is not already in your path, you can add it to your path in your script using something similar to the following:
232 |
233 | import os
234 |
235 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab'
236 | os.environ['PATH'] = meshlabserver_path + os.pathsep + os.environ['PATH']
237 |
238 |
239 | Example #1: Create an orange cube and apply some transformations
240 |
241 | import meshlabxml as mlx
242 |
243 | orange_cube = mlx.FilterScript(file_out='orange_cube.ply', ml_version='2016.12')
244 | mlx.create.cube(orange_cube, size=[3.0, 4.0, 5.0], center=True, color='orange')
245 | mlx.transform.rotate(orange_cube, axis='x', angle=45)
246 | mlx.transform.rotate(orange_cube, axis='y', angle=45)
247 | mlx.transform.translate(orange_cube, value=[0, 5.0, 0])
248 | orange_cube.run_script()
249 |
250 | Output:
251 |
252 | 
253 |
254 |
255 | Example #2: Measure the built-in Stanford Bunny test model and print the results
256 |
257 | import meshlabxml as mlx
258 |
259 | aabb, geometry, topology = mlx.files.measure_all('bunny', ml_version='2016.12')
260 |
261 | Output:
262 |
263 | max = [103.817589, 87.032661, 191.203903]
264 | diagonal = 310.4472554416525
265 | size = [193.95133199999998, 149.001499, 191.203903]
266 | center = [6.841923000000001, 12.531911500000003, 95.6019515]
267 | min = [-90.133743, -61.968838, 0.0]
268 | volume_cm3 = 1486.804
269 | inertia_tensor = [[3170550016.0, -46370464.0, 987163904.0], [-46370464.0, 5072923136.0, -58690096.0], [987163904.0, -58690096.0, 3817212928.0]]
270 | area_cm2 = 901.506875
271 | center_of_mass = [1.747876, -2.226919, 64.788971]
272 | total_edge_length = 179819.484375
273 | volume_mm3 = 1486804.0
274 | principal_axes = [[0.809736, 0.001188, -0.586793], [0.581329, 0.134539, 0.802468], [-0.0799, 0.990908, -0.108251]]
275 | total_edge_length_incl_faux = 179819.484375
276 | area_mm2 = 90150.6875
277 | axis_momenta = [2455110656.0, 4522500608.0, 5083073024.0]
278 | genus = 0
279 | manifold = True
280 | non_manifold_E = 0
281 | vert_num = 32285
282 | boundry_edge_num = 0
283 | hole_num = 0
284 | unref_vert_num = 0
285 | edge_num = 96849
286 | face_num = 64566
287 | non_manifold_V = 0
288 | part_num = 1
289 |
290 | Check out the "examples" directory for more complex examples.
291 |
292 | ## Logo
293 |
294 | The MLX logo is a rainbow colored quatrefoil torus knot with Voronoi meshing. This model is created entirely using MLX; it's source code is included in the "examples" directory. This is a moderately complex script that should give you an idea of some of the things that MLX can do; it makes heavy use of the powerful muparser functions.
295 |
296 |
297 | ## Tips
298 |
299 | * MeshLabServer can be a bit unstable, especially on certain filters such as mlx.remesh.uniform_resampling. If you have many filters to run, it is better to break the project up into smaller scripts and run problematic filters or sequences independently.
300 | * It is not currently possible to measure a mesh and use the results in the same script; you will need to measure the mesh and input the results into another instance. For example, if you want to simplify a mesh based on a percentage of the number of faces, you would first need to measure the number of faces with mlx.compute.measure_topology, then input the results into mlx.remesh.simplify.
301 |
302 |
303 | ## Status
304 |
305 | MLX is still under heavy development and the API is not yet considered stable. Still, it is already quite useful, and is used for production purposes within our small company.
306 |
307 | ## License
308 |
309 | MLX is released under [LGPL version 2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html)
310 |
311 | Example code is released into the public domain, except the quatrefoil logo, which is LGPL.
312 |
313 | Any included models (such as the Stanford Bunny) are released under their own licenses.
314 |
315 | Much of the documentation for the filter functions is taken directly from MeshLab, and is under the MeshLab license [GPL version 3](https://www.gnu.org/licenses/gpl-3.0-standalone.html)
316 |
--------------------------------------------------------------------------------
/examples/CC0.txt:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/examples/quatrefoil_voronoi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """ MLX logo and example script
3 |
4 | Demonstrates how to use MLX as a design tool for a moderately complex project.
5 |
6 | Units: mm
7 |
8 | License:
9 | Copyright (C) 2017 by Tim Ayres, 3DLirious@gmail.com
10 |
11 | This program is free software; you can redistribute it and/or
12 | modify it under the terms of the GNU Lesser General Public
13 | License as published by the Free Software Foundation; either
14 | version 2.1 of the License, or (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 | Lesser General Public License for more details.
20 |
21 | You should have received a copy of the GNU Lesser General Public
22 | License along with this program; if not, write to the Free Software
23 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
24 | """
25 |
26 | import os
27 | import time
28 | import inspect
29 | import math
30 |
31 | import meshlabxml as mlx
32 |
33 | THIS_SCRIPTPATH = os.path.dirname(
34 | os.path.realpath(inspect.getsourcefile(lambda: 0)))
35 |
36 |
37 | def quatrefoil():
38 | """ Rainbow colored voronoi quatrefoil (3,4) torus knot """
39 | start_time = time.time()
40 |
41 | os.chdir(THIS_SCRIPTPATH)
42 | #ml_version = '1.3.4BETA'
43 | ml_version = '2016.12'
44 |
45 | # Add meshlabserver directory to OS PATH; omit this if it is already in
46 | # your PATH
47 | #meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab'
48 | #"""
49 | if ml_version == '1.3.4BETA':
50 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab'
51 | elif ml_version == '2016.12':
52 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab_2016_12'
53 | #"""
54 | os.environ['PATH'] = meshlabserver_path + os.pathsep + os.environ['PATH']
55 |
56 | # Cross section parameters
57 | length = math.radians(360)
58 | tube_size = [10, 10, length]
59 | segments = [64, 64, 720*2]
60 | inner_radius = 2.0
61 |
62 | # Sinusoidal deformation parameters
63 | amplitude = 4.2
64 | freq = 4
65 | phase = 270
66 | center = 'r'
67 | start_pt = 0
68 | increment = 'z-{}'.format(start_pt)
69 |
70 | # Cyclic rainbow color parameters
71 | c_start_pt = 0
72 | c_freq = 5
73 | c_phase_shift = 0 #90 #300
74 | c_phase = (0 + c_phase_shift, 120 + c_phase_shift, 240 + c_phase_shift, 0)
75 |
76 | # Voronoi surface parameters
77 | holes = [2, 2, 44] # Number of holes in each axis; x are sides, y is outside
78 | web_thickness = 0.5
79 | solid_radius = 5.0 # If the mesh is smaller than this radius the holes will be closed
80 | faces_surface = 50000
81 |
82 | # Voronoi solid parameters
83 | voxel = 0.5
84 | thickness = 2.5
85 | faces_solid = 200000
86 |
87 | # Scaling parameters
88 | size = 75 # desired max size of the curve
89 | curve_max_size = 2*(1 + 1.5) # the 1.5 s/b inner_radius, but am keepng current scaling
90 | scale = (size-2*(thickness + amplitude) - tube_size[1])/curve_max_size
91 |
92 | # File names
93 | file_color = 'quatrefoil_color.ply'
94 | file_voronoi_surf = 'quatrefoil_voronoi_surf.ply'
95 | file_voronoi_solid = 'quatrefoil_voronoi_solid.ply'
96 | file_voronoi_color = 'quatrefoil_voronoi_final.ply'
97 |
98 | # Create FilterScript objects for each step in the process
99 | quatrefoil_color = mlx.FilterScript(
100 | file_in=None, file_out=file_color, ml_version=ml_version)
101 | quatrefoil_voronoi_surf = mlx.FilterScript(
102 | file_in=file_color, file_out=file_voronoi_surf, ml_version=ml_version)
103 | quatrefoil_voronoi_solid = mlx.FilterScript(
104 | file_in=file_voronoi_surf, file_out=file_voronoi_solid,
105 | ml_version=ml_version)
106 | quatrefoil_voronoi_color = mlx.FilterScript(
107 | file_in=[file_color, file_voronoi_solid], file_out=file_voronoi_color,
108 | ml_version=ml_version)
109 |
110 |
111 | print('\n Create colored quatrefoil curve ...')
112 | mlx.create.cube_open_hires(
113 | quatrefoil_color, size=tube_size, x_segments=segments[0],
114 | y_segments=segments[1], z_segments=segments[2], center=True)
115 | mlx.transform.translate(quatrefoil_color, [0, 0, length/2])
116 |
117 | # Sinusoidal deformation
118 | r_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format(
119 | f=freq, i=increment, p=math.radians(phase), a=amplitude, c=center)
120 | mlx.transform.function_cyl_co(
121 | quatrefoil_color, r_func=r_func, theta_func='theta', z_func='z')
122 |
123 | # Save max radius in quality field so that we can save it with the file
124 | # for use in the next step
125 | max_radius = math.sqrt((tube_size[0]/2)**2+(tube_size[1]/2)**2) # at corners
126 | q_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format(
127 | f=freq, i=increment, p=math.radians(phase), a=amplitude, c=max_radius)
128 | mlx.mp_func.vq_function(quatrefoil_color, function=q_func)
129 |
130 | # Apply rainbow vertex colors
131 | mlx.vert_color.cyclic_rainbow(
132 | quatrefoil_color, direction='z', start_pt=c_start_pt, amplitude=255 / 2,
133 | center=255 / 2, freq=c_freq, phase=c_phase)
134 |
135 | # Deform mesh to quatrefoil curve. Merge vertices after, which
136 | # will weld the ends together so it becomes watertight
137 | quatrefoil_func = mlx.transform.deform2curve(
138 | quatrefoil_color,
139 | curve=mlx.mp_func.torus_knot('t', p=3, q=4, scale=scale,
140 | radius=inner_radius))
141 | mlx.clean.merge_vert(quatrefoil_color, threshold=0.0001)
142 |
143 | # Run script
144 | mlx.layers.delete_lower(quatrefoil_color)
145 | quatrefoil_color.run_script(output_mask='-m vc vq')
146 |
147 | print('\n Create Voronoi surface ...')
148 | # Move quality value into radius attribute
149 | mlx.mp_func.vert_attr(quatrefoil_voronoi_surf, name='radius', function='q')
150 |
151 | # Create seed vertices
152 | # For grid style holes, we will create a mesh similar to the original
153 | # but with fewer vertices.
154 | mlx.create.cube_open_hires(
155 | quatrefoil_voronoi_surf, size=tube_size, x_segments=holes[0]+1,
156 | y_segments=holes[1]+1, z_segments=holes[2]+1, center=True)
157 | mlx.select.all(quatrefoil_voronoi_surf, vert=False)
158 | mlx.delete.selected(quatrefoil_voronoi_surf, vert=False)
159 | mlx.select.cylindrical_vert(quatrefoil_voronoi_surf,
160 | radius=max_radius-0.0001, inside=False)
161 | mlx.transform.translate(quatrefoil_voronoi_surf, [0, 0, 20])
162 | mlx.delete.selected(quatrefoil_voronoi_surf, face=False)
163 |
164 | mlx.transform.function_cyl_co(quatrefoil_voronoi_surf, r_func=r_func,
165 | theta_func='theta', z_func='z')
166 | mlx.transform.vert_function(
167 | quatrefoil_voronoi_surf, x_func=quatrefoil_func[0],
168 | y_func=quatrefoil_func[1], z_func=quatrefoil_func[2])
169 |
170 | mlx.layers.change(quatrefoil_voronoi_surf, 0)
171 | mlx.vert_color.voronoi(quatrefoil_voronoi_surf)
172 |
173 | if quatrefoil_voronoi_surf.ml_version == '1.3.4BETA':
174 | sel_func = '(q <= {}) or ((radius)<={})'.format(web_thickness, solid_radius)
175 | else:
176 | sel_func = '(q <= {}) || ((radius)<={})'.format(web_thickness, solid_radius)
177 | mlx.select.vert_function(quatrefoil_voronoi_surf, function=sel_func)
178 | #mlx.select.face_function(quatrefoil_voronoi_surf, function='(vsel0 && vsel1 && vsel2)')
179 | mlx.select.invert(quatrefoil_voronoi_surf, face=False)
180 | mlx.delete.selected(quatrefoil_voronoi_surf, face=False)
181 |
182 | mlx.smooth.laplacian(quatrefoil_voronoi_surf, iterations=3)
183 | mlx.remesh.simplify(quatrefoil_voronoi_surf, texture=False, faces=faces_surface)
184 |
185 | mlx.layers.delete_lower(quatrefoil_voronoi_surf)
186 | #quatrefoil_voronoi_surf.save_to_file('temp_script.mlx')
187 | quatrefoil_voronoi_surf.run_script(script_file=None, output_mask='-m vc vq')
188 |
189 | print('\n Solidify Voronoi surface ...')
190 | mlx.remesh.uniform_resampling(quatrefoil_voronoi_solid, voxel=voxel,
191 | offset=thickness/2, thicken=True)
192 | mlx.layers.delete_lower(quatrefoil_voronoi_solid)
193 | quatrefoil_voronoi_solid.run_script()
194 |
195 | print('\n Clean up & transfer color to final model ...')
196 | # Clean up from uniform mesh resamplng
197 | mlx.delete.small_parts(quatrefoil_voronoi_color)
198 | mlx.delete.unreferenced_vert(quatrefoil_voronoi_color)
199 | mlx.delete.faces_from_nonmanifold_edges(quatrefoil_voronoi_color)
200 | mlx.clean.split_vert_on_nonmanifold_face(quatrefoil_voronoi_color)
201 | mlx.clean.close_holes(quatrefoil_voronoi_color)
202 |
203 | # Simplify (to improve triangulation quality), refine, & smooth
204 | mlx.remesh.simplify(quatrefoil_voronoi_color, texture=False, faces=faces_solid)
205 | mlx.subdivide.ls3loop(quatrefoil_voronoi_color, iterations=1)
206 | mlx.smooth.laplacian(quatrefoil_voronoi_color, iterations=3)
207 |
208 | # Transfer colors from original curve
209 | mlx.transfer.vert_attr_2_meshes(
210 | quatrefoil_voronoi_color, source_mesh=0, target_mesh=1, color=True,
211 | max_distance=7)
212 | mlx.layers.delete_lower(quatrefoil_voronoi_color)
213 | quatrefoil_voronoi_color.run_script(script_file=None)
214 | print(' done! Took %.1f sec' % (time.time() - start_time))
215 |
216 | return None
217 |
218 | if __name__ == '__main__':
219 | quatrefoil()
220 |
--------------------------------------------------------------------------------
/examples/shield.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """Example MeshLabXML script to create a heroic shield.
3 |
4 | MeshLab is typically used to process existing meshes; this script was
5 | created to demonstrate some of MeshLab's mesh creation features as well.
6 | It demonstrates combining python functionality (math and flow control)
7 | and MeshLab to create a parametric 3D model that's truly heroic!
8 |
9 | Note that the final model is composed of separate surfaces. It is not
10 | manifold and is e.g. not suitable for 3D printing; it's just a silly
11 | example.
12 |
13 | License:
14 | Written in 2016 by Tim Ayres 3DLirious@gmail.com
15 |
16 | To the extent possible under law, the author(s) have dedicated all
17 | copyright and related and neighboring rights to this software to the
18 | public domain worldwide. This software is distributed without any
19 | warranty.
20 |
21 | You should have received a copy of the CC0 Public Domain Dedication
22 | along with this software. If not, see
23 | .
24 |
25 | """
26 |
27 | import os
28 | import math
29 |
30 | import meshlabxml as mlx
31 |
32 | # Add meshlabserver directory to OS PATH; omit this if it is already in
33 | # your PATH
34 | MESHLABSERVER_PATH = 'C:\\Program Files\\VCG\\MeshLab'
35 | os.environ['PATH'] += os.pathsep + MESHLABSERVER_PATH
36 |
37 |
38 | def main():
39 | """Run main script"""
40 | # segments = number of segments to use for circles
41 | segments = 50
42 | # star_points = number of points (or sides) of the star
43 | star_points = 5
44 | # star_radius = radius of circle circumscribing the star
45 | star_radius = 2
46 | # ring_thickness = thickness of the colored rings
47 | ring_thickness = 1
48 | # sphere_radius = radius of sphere the shield will be deformed to
49 | sphere_radius = 2 * (star_radius + 3 * ring_thickness)
50 |
51 | # Star calculations:
52 | # Visually approximate a star by using multiple diamonds (i.e. scaled
53 | # squares) which overlap in the center. For the star calculations,
54 | # consider a central polygon with triangles attached to the edges, all
55 | # circumscribed by a circle.
56 | # polygon_radius = distance from center of circle to polygon edge midpoint
57 | polygon_radius = star_radius / \
58 | (1 + math.tan(math.radians(180 / star_points)) /
59 | math.tan(math.radians(90 / star_points)))
60 | # width = 1/2 width of polygon edge/outer triangle bottom
61 | width = polygon_radius * math.tan(math.radians(180 / star_points))
62 | # height = height of outer triangle
63 | height = width / math.tan(math.radians(90 / star_points))
64 |
65 | shield = mlx.FilterScript(file_out="shield.ply")
66 |
67 | # Create the colored front of the shield using several concentric
68 | # annuluses; combine them together and subdivide so we have more vertices
69 | # to give a smoother deformation later.
70 | mlx.create.annulus(shield, radius=star_radius, cir_segments=segments, color='blue')
71 | mlx.create.annulus(shield,
72 | radius1=star_radius + ring_thickness,
73 | radius2=star_radius,
74 | cir_segments=segments,
75 | color='red')
76 | mlx.create.annulus(shield,
77 | radius1=star_radius + 2 * ring_thickness,
78 | radius2=star_radius + ring_thickness,
79 | cir_segments=segments,
80 | color='white')
81 | mlx.create.annulus(shield,
82 | radius1=star_radius + 3 * ring_thickness,
83 | radius2=star_radius + 2 * ring_thickness,
84 | cir_segments=segments,
85 | color='red')
86 | mlx.layers.join(shield)
87 | mlx.subdivide.midpoint(shield, iterations=2)
88 |
89 | # Create the inside surface of the shield & translate down slightly so it
90 | # doesn't overlap the front.
91 | mlx.create.annulus(shield,
92 | radius1=star_radius + 3 * ring_thickness,
93 | cir_segments=segments,
94 | color='silver')
95 | mlx.transform.rotate(shield, axis='y', angle=180)
96 | mlx.transform.translate(shield, value=[0, 0, -0.005])
97 | mlx.subdivide.midpoint(shield, iterations=4)
98 |
99 | # Create a diamond for the center star. First create a plane, specifying
100 | # extra vertices to support the final deformation. The length from the
101 | # center of the plane to the corners should be 1 for ease of scaling, so
102 | # we use a side length of sqrt(2) (thanks Pythagoras!). Rotate the plane
103 | # by 45 degrees and scale it to stretch it out per the calculations above,
104 | # then translate it into place (including moving it up in z slightly so
105 | # that it doesn't overlap the shield front).
106 | mlx.create.grid(shield,
107 | size=math.sqrt(2),
108 | x_segments=10,
109 | y_segments=10,
110 | center=True,
111 | color='white')
112 | mlx.transform.rotate(shield, axis='z', angle=45)
113 | mlx.transform.scale(shield, value=[width, height, 1])
114 | mlx.transform.translate(shield, value=[0, polygon_radius, 0.001])
115 |
116 | # Duplicate the diamond and rotate the duplicates around, generating the
117 | # star.
118 | for _ in range(1, star_points):
119 | mlx.layers.duplicate(shield)
120 | mlx.transform.rotate(shield, axis='z', angle=360 / star_points)
121 |
122 | # Combine everything together and deform using a spherical function.
123 | mlx.layers.join(shield)
124 | mlx.transform.vert_function(shield,
125 | z_func='sqrt(%s-x^2-y^2)-%s+z' %
126 | (sphere_radius**2, sphere_radius))
127 |
128 | # Run the script using meshlabserver and generate the model
129 | shield.run_script()
130 | return None
131 |
132 | if __name__ == '__main__':
133 | main()
134 |
--------------------------------------------------------------------------------
/meshlabxml/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .mlx import *
3 |
4 | from . import clean
5 | from . import compute
6 | from . import create
7 | from . import delete
8 | from . import files
9 | from . import layers
10 | from . import normals
11 | from . import remesh
12 | from . import sampling
13 | from . import select
14 | from . import smooth
15 | from . import subdivide
16 | from . import texture
17 | from . import transfer
18 | from . import transform
19 | from . import util
20 | from . import vert_color
21 | from . import mp_func
22 |
23 | #from .color_names import color_name
24 |
--------------------------------------------------------------------------------
/meshlabxml/clean.py:
--------------------------------------------------------------------------------
1 | """MeshLabXML cleaning and repairing functions
2 |
3 | See select and delete modules for additional cleaning functions
4 | """
5 |
6 | from . import util
7 |
8 | def merge_vert(script, threshold=0.0):
9 | """ Merge together all the vertices that are nearer than the specified
10 | threshold. Like a unify duplicate vertices but with some tolerance.
11 |
12 | Args:
13 | script: the FilterScript object or script filename to write
14 | the filter to.
15 | threshold (float): Merging distance. All the vertices that are closer
16 | than this threshold are merged together. Use very small values,
17 | default is zero.
18 |
19 | Layer stack:
20 | No impacts
21 |
22 | MeshLab versions:
23 | 2016.12
24 | 1.3.4BETA
25 | """
26 | filter_xml = ''.join([
27 | ' \n',
28 | ' \n',
35 | ' \n'])
36 | util.write_filter(script, filter_xml)
37 | return None
38 |
39 |
40 | def close_holes(script, hole_max_edge=30, selected=False,
41 | sel_new_face=True, self_intersection=True):
42 | """ Close holes smaller than a given threshold
43 |
44 | Args:
45 | script: the FilterScript object or script filename to write
46 | the filter to.
47 | hole_max_edge (int): The size is expressed as number of edges composing
48 | the hole boundary.
49 | selected (bool): Only the holes with at least one of the boundary faces
50 | selected are closed.
51 | sel_new_face (bool): After closing a hole the faces that have been
52 | created are left selected. Any previous selection is lost. Useful
53 | for example for smoothing or subdividing the newly created holes.
54 | self_intersection (bool): When closing an holes it tries to prevent the
55 | creation of faces that intersect faces adjacent to the boundary of
56 | the hole. It is an heuristic, non intersecting hole filling can be
57 | NP-complete.
58 |
59 | Layer stack:
60 | No impacts
61 |
62 | MeshLab versions:
63 | 2016.12
64 | 1.3.4BETA
65 | """
66 | filter_xml = ''.join([
67 | ' \n',
68 | ' \n',
73 | ' \n',
78 | ' \n',
83 | ' \n',
88 | ' \n'])
89 | util.write_filter(script, filter_xml)
90 | return None
91 |
92 |
93 | def split_vert_on_nonmanifold_face(script, vert_displacement_ratio=0.0):
94 | """ Split non-manifold vertices until it becomes two-manifold.
95 |
96 | Args:
97 | script: the FilterScript object or script filename to write
98 | the filter to.
99 | vert_displacement_ratio (float): When a vertex is split it is moved
100 | along the average vector going from its position to the centroid
101 | of the FF connected faces sharing it.
102 |
103 | Layer stack:
104 | No impacts
105 |
106 | MeshLab versions:
107 | 2016.12
108 | 1.3.4BETA
109 | """
110 | if script.ml_version == '1.3.4BETA':
111 | filter_name = 'Split Vertexes Incident on Non Manifold Faces'
112 | else:
113 | filter_name = 'Repair non Manifold Vertices by splitting'
114 | filter_xml = ''.join([
115 | ' \n'.format(filter_name),
116 | ' \n',
121 | ' \n'])
122 | util.write_filter(script, filter_xml)
123 | return None
124 |
125 |
126 | def fix_folded_face(script):
127 | """ Delete all the single folded faces.
128 |
129 | A face is considered folded if its normal is opposite to all the adjacent
130 | faces. It is removed by flipping it against the face f adjacent along the
131 | edge e such that the vertex opposite to e fall inside f.
132 |
133 | Args:
134 | script: the FilterScript object or script filename to write
135 | the filter to.
136 |
137 | Layer stack:
138 | No impacts
139 |
140 | MeshLab versions:
141 | 2016.12
142 | 1.3.4BETA
143 | """
144 | filter_xml = ' \n'
145 | util.write_filter(script, filter_xml)
146 | return None
147 |
148 |
149 | def snap_mismatched_borders(script, edge_dist_ratio=0.01, unify_vert=True):
150 | """ Try to snap together adjacent borders that are slightly mismatched.
151 |
152 | This situation can happen on badly triangulated adjacent patches defined by
153 | high order surfaces. For each border vertex the filter snaps it onto the
154 | closest boundary edge only if it is closest of edge_legth*threshold. When
155 | vertex is snapped the corresponding face it split and a new vertex is
156 | created.
157 |
158 | Args:
159 | script: the FilterScript object or script filename to write
160 | the filter to.
161 | edge_dist_ratio (float): Collapse edge when the edge / distance ratio
162 | is greater than this value. E.g. for default value 1000 two
163 | straight border edges are collapsed if the central vertex dist from
164 | the straight line composed by the two edges less than a 1/1000 of
165 | the sum of the edges length. Larger values enforce that only
166 | vertexes very close to the line are removed.
167 | unify_vert (bool): If true the snap vertices are welded together.
168 |
169 | Layer stack:
170 | No impacts
171 |
172 | MeshLab versions:
173 | 2016.12
174 | 1.3.4BETA
175 | """
176 | filter_xml = ''.join([
177 | ' \n',
178 | ' \n',
183 | ' \n',
188 | ' \n'])
189 | util.write_filter(script, filter_xml)
190 | return None
191 |
--------------------------------------------------------------------------------
/meshlabxml/color_names.py:
--------------------------------------------------------------------------------
1 | """ Dictionary of the 140 HTML Color Names defined in CSS & SVG
2 | https://en.wikipedia.org/wiki/Web_colors#X11_color_names
3 | Format:
4 | key = color_name (lowercase str)
5 | value = r (int), g (int), b (int), hex (str)
6 | """
7 |
8 | color_name = {
9 | 'aliceblue': (240, 248, 255, 'f0f8ff'),
10 | 'antiquewhite': (250, 235, 215, 'faebd7'),
11 | 'aqua': (0, 255, 255, '00ffff'),
12 | 'aquamarine': (127, 255, 212, '7fffd4'),
13 | 'azure': (240, 255, 255, 'f0ffff'),
14 | 'beige': (245, 245, 220, 'f5f5dc'),
15 | 'bisque': (255, 228, 196, 'ffe4c4'),
16 | 'black': (0, 0, 0, '000000'),
17 | 'blanchedalmond': (255, 235, 205, 'ffebcd'),
18 | 'blue': (0, 0, 255, '0000ff'),
19 | 'blueviolet': (138, 43, 226, '8a2be2'),
20 | 'brown': (165, 42, 42, 'a52a2a'),
21 | 'burlywood': (222, 184, 135, 'deb887'),
22 | 'cadetblue': (95, 158, 160, '5f9ea0'),
23 | 'chartreuse': (127, 255, 0, '7fff00'),
24 | 'chocolate': (210, 105, 30, 'd2691e'),
25 | 'coral': (255, 127, 80, 'ff7f50'),
26 | 'cornflowerblue': (100, 149, 237, '6495ed'),
27 | 'cornsilk': (255, 248, 220, 'fff8dc'),
28 | 'crimson': (220, 20, 60, 'dc143c'),
29 | 'cyan': (0, 255, 255, '00ffff'),
30 | 'darkblue': (0, 0, 139, '00008b'),
31 | 'darkcyan': (0, 139, 139, '008b8b'),
32 | 'darkgoldenrod': (184, 134, 11, 'b8860b'),
33 | 'darkgray': (169, 169, 169, 'a9a9a9'),
34 | 'darkgreen': (0, 100, 0, '006400'),
35 | 'darkgrey': (169, 169, 169, 'a9a9a9'),
36 | 'darkkhaki': (189, 183, 107, 'bdb76b'),
37 | 'darkmagenta': (139, 0, 139, '8b008b'),
38 | 'darkolivegreen': (85, 107, 47, '556b2f'),
39 | 'darkorange': (255, 140, 0, 'ff8c00'),
40 | 'darkorchid': (153, 50, 204, '9932cc'),
41 | 'darkred': (139, 0, 0, '8b0000'),
42 | 'darksalmon': (233, 150, 122, 'e9967a'),
43 | 'darkseagreen': (143, 188, 143, '8fbc8f'),
44 | 'darkslateblue': (72, 61, 139, '483d8b'),
45 | 'darkslategray': (47, 79, 79, '2f4f4f'),
46 | 'darkslategrey': (47, 79, 79, '2f4f4f'),
47 | 'darkturquoise': (0, 206, 209, '00ced1'),
48 | 'darkviolet': (148, 0, 211, '9400d3'),
49 | 'deeppink': (255, 20, 147, 'ff1493'),
50 | 'deepskyblue': (0, 191, 255, '00bfff'),
51 | 'dimgray': (105, 105, 105, '696969'),
52 | 'dimgrey': (105, 105, 105, '696969'),
53 | 'dodgerblue': (30, 144, 255, '1e90ff'),
54 | 'firebrick': (178, 34, 34, 'b22222'),
55 | 'floralwhite': (255, 250, 240, 'fffaf0'),
56 | 'forestgreen': (34, 139, 34, '228b22'),
57 | 'fuchsia': (255, 0, 255, 'ff00ff'),
58 | 'gainsboro': (220, 220, 220, 'dcdcdc'),
59 | 'ghostwhite': (248, 248, 255, 'f8f8ff'),
60 | 'gold': (255, 215, 0, 'ffd700'),
61 | 'goldenrod': (218, 165, 32, 'daa520'),
62 | 'gray': (128, 128, 128, '808080'),
63 | 'green': (0, 128, 0, '008000'),
64 | 'greenyellow': (173, 255, 47, 'adff2f'),
65 | 'grey': (128, 128, 128, '808080'),
66 | 'honeydew': (240, 255, 240, 'f0fff0'),
67 | 'hotpink': (255, 105, 180, 'ff69b4'),
68 | 'indianred': (205, 92, 92, 'cd5c5c'),
69 | 'indigo': (75, 0, 130, '4b0082'),
70 | 'ivory': (255, 255, 240, 'fffff0'),
71 | 'khaki': (240, 230, 140, 'f0e68c'),
72 | 'lavender': (230, 230, 250, 'e6e6fa'),
73 | 'lavenderblush': (255, 240, 245, 'fff0f5'),
74 | 'lawngreen': (124, 252, 0, '7cfc00'),
75 | 'lemonchiffon': (255, 250, 205, 'fffacd'),
76 | 'lightblue': (173, 216, 230, 'add8e6'),
77 | 'lightcoral': (240, 128, 128, 'f08080'),
78 | 'lightcyan': (224, 255, 255, 'e0ffff'),
79 | 'lightgoldenrodyellow': (250, 250, 210, 'fafad2'),
80 | 'lightgray': (211, 211, 211, 'd3d3d3'),
81 | 'lightgreen': (144, 238, 144, '90ee90'),
82 | 'lightgrey': (211, 211, 211, 'd3d3d3'),
83 | 'lightpink': (255, 182, 193, 'ffb6c1'),
84 | 'lightsalmon': (255, 160, 122, 'ffa07a'),
85 | 'lightseagreen': (32, 178, 170, '20b2aa'),
86 | 'lightskyblue': (135, 206, 250, '87cefa'),
87 | 'lightslategray': (119, 136, 153, '778899'),
88 | 'lightslategrey': (119, 136, 153, '778899'),
89 | 'lightsteelblue': (176, 196, 222, 'b0c4de'),
90 | 'lightyellow': (255, 255, 224, 'ffffe0'),
91 | 'lime': (0, 255, 0, '00ff00'),
92 | 'limegreen': (50, 205, 50, '32cd32'),
93 | 'linen': (250, 240, 230, 'faf0e6'),
94 | 'magenta': (255, 0, 255, 'ff00ff'),
95 | 'maroon': (128, 0, 0, '800000'),
96 | 'mediumaquamarine': (102, 205, 170, '66cdaa'),
97 | 'mediumblue': (0, 0, 205, '0000cd'),
98 | 'mediumorchid': (186, 85, 211, 'ba55d3'),
99 | 'mediumpurple': (147, 112, 219, '9370db'),
100 | 'mediumseagreen': (60, 179, 113, '3cb371'),
101 | 'mediumslateblue': (123, 104, 238, '7b68ee'),
102 | 'mediumspringgreen': (0, 250, 154, '00fa9a'),
103 | 'mediumturquoise': (72, 209, 204, '48d1cc'),
104 | 'mediumvioletred': (199, 21, 133, 'c71585'),
105 | 'midnightblue': (25, 25, 112, '191970'),
106 | 'mintcream': (245, 255, 250, 'f5fffa'),
107 | 'mistyrose': (255, 228, 225, 'ffe4e1'),
108 | 'moccasin': (255, 228, 181, 'ffe4b5'),
109 | 'navajowhite': (255, 222, 173, 'ffdead'),
110 | 'navy': (0, 0, 128, '000080'),
111 | 'oldlace': (253, 245, 230, 'fdf5e6'),
112 | 'olive': (128, 128, 0, '808000'),
113 | 'olivedrab': (107, 142, 35, '6b8e23'),
114 | 'orange': (255, 165, 0, 'ffa500'),
115 | 'orangered': (255, 69, 0, 'ff4500'),
116 | 'orchid': (218, 112, 214, 'da70d6'),
117 | 'palegoldenrod': (238, 232, 170, 'eee8aa'),
118 | 'palegreen': (152, 251, 152, '98fb98'),
119 | 'paleturquoise': (175, 238, 238, 'afeeee'),
120 | 'palevioletred': (219, 112, 147, 'db7093'),
121 | 'papayawhip': (255, 239, 213, 'ffefd5'),
122 | 'peachpuff': (255, 218, 185, 'ffdab9'),
123 | 'peru': (205, 133, 63, 'cd853f'),
124 | 'pink': (255, 192, 203, 'ffc0cb'),
125 | 'plum': (221, 160, 221, 'dda0dd'),
126 | 'powderblue': (176, 224, 230, 'b0e0e6'),
127 | 'purple': (128, 0, 128, '800080'),
128 | 'red': (255, 0, 0, 'ff0000'),
129 | 'rosybrown': (188, 143, 143, 'bc8f8f'),
130 | 'royalblue': (65, 105, 225, '4169e1'),
131 | 'saddlebrown': (139, 69, 19, '8b4513'),
132 | 'salmon': (250, 128, 114, 'fa8072'),
133 | 'sandybrown': (244, 164, 96, 'f4a460'),
134 | 'seagreen': (46, 139, 87, '2e8b57'),
135 | 'seashell': (255, 245, 238, 'fff5ee'),
136 | 'sienna': (160, 82, 45, 'a0522d'),
137 | 'silver': (192, 192, 192, 'c0c0c0'),
138 | 'skyblue': (135, 206, 235, '87ceeb'),
139 | 'slateblue': (106, 90, 205, '6a5acd'),
140 | 'slategray': (112, 128, 144, '708090'),
141 | 'slategrey': (112, 128, 144, '708090'),
142 | 'snow': (255, 250, 250, 'fffafa'),
143 | 'springgreen': (0, 255, 127, '00ff7f'),
144 | 'steelblue': (70, 130, 180, '4682b4'),
145 | 'tan': (210, 180, 140, 'd2b48c'),
146 | 'teal': (0, 128, 128, '008080'),
147 | 'thistle': (216, 191, 216, 'd8bfd8'),
148 | 'tomato': (255, 99, 71, 'ff6347'),
149 | 'turquoise': (64, 224, 208, '40e0d0'),
150 | 'violet': (238, 130, 238, 'ee82ee'),
151 | 'wheat': (245, 222, 179, 'f5deb3'),
152 | 'white': (255, 255, 255, 'ffffff'),
153 | 'whitesmoke': (245, 245, 245, 'f5f5f5'),
154 | 'yellow': (255, 255, 0, 'ffff00'),
155 | 'yellowgreen': (154, 205, 50, '9acd32'),
156 | }
157 |
158 |
--------------------------------------------------------------------------------
/meshlabxml/color_names.txt:
--------------------------------------------------------------------------------
1 | # The 140 HTML Color Names defined in CSS & SVG
2 | # https://en.wikipedia.org/wiki/Web_colors#X11_color_names
3 | color_name hex r g b
4 | aliceblue #f0f8ff 240 248 255
5 | antiquewhite #faebd7 250 235 215
6 | aqua #00ffff 0 255 255
7 | aquamarine #7fffd4 127 255 212
8 | azure #f0ffff 240 255 255
9 | beige #f5f5dc 245 245 220
10 | bisque #ffe4c4 255 228 196
11 | black #000000 0 0 0
12 | blanchedalmond #ffebcd 255 235 205
13 | blue #0000ff 0 0 255
14 | blueviolet #8a2be2 138 43 226
15 | brown #a52a2a 165 42 42
16 | burlywood #deb887 222 184 135
17 | cadetblue #5f9ea0 95 158 160
18 | chartreuse #7fff00 127 255 0
19 | chocolate #d2691e 210 105 30
20 | coral #ff7f50 255 127 80
21 | cornflowerblue #6495ed 100 149 237
22 | cornsilk #fff8dc 255 248 220
23 | crimson #dc143c 220 20 60
24 | cyan #00ffff 0 255 255
25 | darkblue #00008b 0 0 139
26 | darkcyan #008b8b 0 139 139
27 | darkgoldenrod #b8860b 184 134 11
28 | darkgray #a9a9a9 169 169 169
29 | darkgreen #006400 0 100 0
30 | darkgrey #a9a9a9 169 169 169
31 | darkkhaki #bdb76b 189 183 107
32 | darkmagenta #8b008b 139 0 139
33 | darkolivegreen #556b2f 85 107 47
34 | darkorange #ff8c00 255 140 0
35 | darkorchid #9932cc 153 50 204
36 | darkred #8b0000 139 0 0
37 | darksalmon #e9967a 233 150 122
38 | darkseagreen #8fbc8f 143 188 143
39 | darkslateblue #483d8b 72 61 139
40 | darkslategray #2f4f4f 47 79 79
41 | darkslategrey #2f4f4f 47 79 79
42 | darkturquoise #00ced1 0 206 209
43 | darkviolet #9400d3 148 0 211
44 | deeppink #ff1493 255 20 147
45 | deepskyblue #00bfff 0 191 255
46 | dimgray #696969 105 105 105
47 | dimgrey #696969 105 105 105
48 | dodgerblue #1e90ff 30 144 255
49 | firebrick #b22222 178 34 34
50 | floralwhite #fffaf0 255 250 240
51 | forestgreen #228b22 34 139 34
52 | fuchsia #ff00ff 255 0 255
53 | gainsboro #dcdcdc 220 220 220
54 | ghostwhite #f8f8ff 248 248 255
55 | gold #ffd700 255 215 0
56 | goldenrod #daa520 218 165 32
57 | gray #808080 128 128 128
58 | green #008000 0 128 0
59 | greenyellow #adff2f 173 255 47
60 | grey #808080 128 128 128
61 | honeydew #f0fff0 240 255 240
62 | hotpink #ff69b4 255 105 180
63 | indianred #cd5c5c 205 92 92
64 | indigo #4b0082 75 0 130
65 | ivory #fffff0 255 255 240
66 | khaki #f0e68c 240 230 140
67 | lavender #e6e6fa 230 230 250
68 | lavenderblush #fff0f5 255 240 245
69 | lawngreen #7cfc00 124 252 0
70 | lemonchiffon #fffacd 255 250 205
71 | lightblue #add8e6 173 216 230
72 | lightcoral #f08080 240 128 128
73 | lightcyan #e0ffff 224 255 255
74 | lightgoldenrodyellow #fafad2 250 250 210
75 | lightgray #d3d3d3 211 211 211
76 | lightgreen #90ee90 144 238 144
77 | lightgrey #d3d3d3 211 211 211
78 | lightpink #ffb6c1 255 182 193
79 | lightsalmon #ffa07a 255 160 122
80 | lightseagreen #20b2aa 32 178 170
81 | lightskyblue #87cefa 135 206 250
82 | lightslategray #778899 119 136 153
83 | lightslategrey #778899 119 136 153
84 | lightsteelblue #b0c4de 176 196 222
85 | lightyellow #ffffe0 255 255 224
86 | lime #00ff00 0 255 0
87 | limegreen #32cd32 50 205 50
88 | linen #faf0e6 250 240 230
89 | magenta #ff00ff 255 0 255
90 | maroon #800000 128 0 0
91 | mediumaquamarine #66cdaa 102 205 170
92 | mediumblue #0000cd 0 0 205
93 | mediumorchid #ba55d3 186 85 211
94 | mediumpurple #9370db 147 112 219
95 | mediumseagreen #3cb371 60 179 113
96 | mediumslateblue #7b68ee 123 104 238
97 | mediumspringgreen #00fa9a 0 250 154
98 | mediumturquoise #48d1cc 72 209 204
99 | mediumvioletred #c71585 199 21 133
100 | midnightblue #191970 25 25 112
101 | mintcream #f5fffa 245 255 250
102 | mistyrose #ffe4e1 255 228 225
103 | moccasin #ffe4b5 255 228 181
104 | navajowhite #ffdead 255 222 173
105 | navy #000080 0 0 128
106 | oldlace #fdf5e6 253 245 230
107 | olive #808000 128 128 0
108 | olivedrab #6b8e23 107 142 35
109 | orange #ffa500 255 165 0
110 | orangered #ff4500 255 69 0
111 | orchid #da70d6 218 112 214
112 | palegoldenrod #eee8aa 238 232 170
113 | palegreen #98fb98 152 251 152
114 | paleturquoise #afeeee 175 238 238
115 | palevioletred #db7093 219 112 147
116 | papayawhip #ffefd5 255 239 213
117 | peachpuff #ffdab9 255 218 185
118 | peru #cd853f 205 133 63
119 | pink #ffc0cb 255 192 203
120 | plum #dda0dd 221 160 221
121 | powderblue #b0e0e6 176 224 230
122 | purple #800080 128 0 128
123 | red #ff0000 255 0 0
124 | rosybrown #bc8f8f 188 143 143
125 | royalblue #4169e1 65 105 225
126 | saddlebrown #8b4513 139 69 19
127 | salmon #fa8072 250 128 114
128 | sandybrown #f4a460 244 164 96
129 | seagreen #2e8b57 46 139 87
130 | seashell #fff5ee 255 245 238
131 | sienna #a0522d 160 82 45
132 | silver #c0c0c0 192 192 192
133 | skyblue #87ceeb 135 206 235
134 | slateblue #6a5acd 106 90 205
135 | slategray #708090 112 128 144
136 | slategrey #708090 112 128 144
137 | snow #fffafa 255 250 250
138 | springgreen #00ff7f 0 255 127
139 | steelblue #4682b4 70 130 180
140 | tan #d2b48c 210 180 140
141 | teal #008080 0 128 128
142 | thistle #d8bfd8 216 191 216
143 | tomato #ff6347 255 99 71
144 | turquoise #40e0d0 64 224 208
145 | violet #ee82ee 238 130 238
146 | wheat #f5deb3 245 222 179
147 | white #ffffff 255 255 255
148 | whitesmoke #f5f5f5 245 245 245
149 | yellow #ffff00 255 255 0
150 | yellowgreen #9acd32 154 205 50
151 |
--------------------------------------------------------------------------------
/meshlabxml/compute.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML measurement and computation functions """
2 |
3 | #from . import mlx.FilterScript
4 | import meshlabxml as mlx
5 | import re
6 | from . import util
7 |
8 | ml_version = '2020.09'
9 |
10 | def section(script, axis='z', offset=0.0, surface=False, custom_axis=None,
11 | planeref=2, split_surface_with_section=False, ml_version=ml_version):
12 | """ Compute the polyline representing a planar section (a slice) of a mesh.
13 |
14 | If the resulting polyline is closed the result can be filled with a
15 | triangular mesh representing the section.
16 |
17 | Args:
18 | script: the mlx.FilterScript object or script filename to write
19 | the filter to.
20 | axis (str): The slicing plane is perpendicular to this axis. Accepted
21 | values are 'x', 'y', or 'z'; any other input will be interpreted
22 | as a custom axis (although using 'custom' is recommended
23 | for clarity). Upper or lowercase values are accepted.
24 | offset (float): Specify an offset of the cross-plane. The offset
25 | corresponds to the distance along 'axis' from the point specified
26 | in 'planeref'.
27 | surface (bool): If True, in addition to a layer with the section
28 | polyline, also a layer with a triangulated version of the section
29 | polyline will be created. This only works if the section polyline
30 | is closed.
31 | split_surface_with_section (bool): If selected, it will create two layers
32 | with the portion of the mesh under and over the section plane. It
33 | requires manifoldness of the mesh.
34 | custom_axis (3 component list or tuple): Specify a custom axis as
35 | a 3 component vector (x, y, z); this is ignored unless 'axis' is
36 | set to 'custom'.
37 | planeref (int): Specify the reference from which the planes are
38 | shifted. Valid values are:
39 | 0 - Bounding box center
40 | 1 - Bounding box min
41 | 2 - Origin (default)
42 |
43 | Layer stack:
44 | Creates a new layer '{label}_sect_{axis_name}_{offset}', where
45 | 'axis_name' is one of [X, Y, Z, custom] and 'offest' is
46 | truncated 'offset'
47 | If surface is True, create a new layer '{label}_sect_{axis}_{offset}_mesh'
48 | Current layer is changed to the last (newly created) layer
49 |
50 | MeshLab versions:
51 | 2020.09
52 | 2016.12
53 | 1.3.4BETA
54 | """
55 | # Convert axis name into number
56 | if axis.lower() == 'x':
57 | axis_num = 0
58 | axis_name = 'X'
59 | elif axis.lower() == 'y':
60 | axis_num = 1
61 | axis_name = 'Y'
62 | elif axis.lower() == 'z':
63 | axis_num = 2
64 | axis_name = 'Z'
65 | else: # custom axis
66 | axis_num = 3
67 | axis_name = 'custom'
68 | if custom_axis is None:
69 | print('WARNING: a custom axis was selected, however',
70 | '"custom_axis" was not provided. Using default (Z).')
71 | if custom_axis is None:
72 | custom_axis = (0.0, 0.0, 1.0)
73 |
74 | filter_xml = ''.join([
75 | ' \n',
76 | ' \n',
86 | ' \n',
92 | ' \n',
97 | ' \n',
106 | ' \n',])
111 | if ml_version == '2020.09':
112 | filter_xml = ''.join([
113 | filter_xml,
114 | ' \n'])
119 | filter_xml = ''.join([filter_xml, ' \n'])
120 | util.write_filter(script, filter_xml)
121 | if isinstance(script, mlx.FilterScript):
122 | current_layer_label = script.layer_stack[script.current_layer()]
123 | script.add_layer('{}_sect_{}_{}'.format(current_layer_label, axis_name,
124 | int(offset)))
125 | if surface:
126 | script.add_layer('{}_sect_{}_{}_filled'.format(current_layer_label,
127 | axis_name, int(offset)))
128 | if split_surface_with_section:
129 | script.add_layer('{}_sect_{}_{}_under'.format(current_layer_label,
130 | axis_name, int(offset)))
131 | script.add_layer('{}_sect_{}_{}_over'.format(current_layer_label,
132 | axis_name, int(offset)))
133 | return None
134 |
135 |
136 | def measure_geometry(script):
137 | """ Compute a set of geometric measures of a mesh/pointcloud.
138 |
139 | Bounding box extents and diagonal, principal axis, thin shell barycenter
140 | (mesh only), vertex barycenter and quality-weighted barycenter (pointcloud
141 | only), surface area (mesh only), volume (closed mesh) and Inertia tensor
142 | Matrix (closed mesh).
143 |
144 | Args:
145 | script: the mlx.FilterScript object or script filename to write
146 | the filter to.
147 |
148 | Layer stack:
149 | No impacts
150 |
151 | MeshLab versions:
152 | 2016.12
153 | 1.3.4BETA
154 |
155 | Bugs:
156 | Bounding box extents not computed correctly for some volumes
157 | """
158 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12':
159 | filter_xml = ' \n'
160 | else:
161 | filter_xml = ' \n'
162 | util.write_filter(script, filter_xml)
163 | if isinstance(script, mlx.FilterScript):
164 | script.parse_geometry = True
165 | return None
166 |
167 |
168 | def measure_topology(script):
169 | """ Compute a set of topological measures over a mesh
170 |
171 | Args:
172 | script: the mlx.FilterScript object or script filename to write
173 | the filter to.
174 |
175 | Layer stack:
176 | No impacts
177 |
178 | MeshLab versions:
179 | 2016.12
180 | 1.3.4BETA
181 | """
182 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12':
183 | filter_xml = ' \n'
184 | else:
185 | filter_xml = ' \n'
186 | util.write_filter(script, filter_xml)
187 | if isinstance(script, mlx.FilterScript):
188 | script.parse_topology = True
189 | return None
190 |
191 |
192 | def parse_geometry(ml_log, log=None, ml_version='2016.12', print_output=False):
193 | """Parse the ml_log file generated by the measure_geometry function.
194 |
195 | Warnings: Not all keys may exist if mesh is not watertight or manifold
196 |
197 | Args:
198 | ml_log (str): MeshLab log file to parse
199 | log (str): filename to log output
200 | """
201 | # TODO: read more than one occurrence per file. Record in list.
202 | aabb = {}
203 | geometry = {'aabb':aabb}
204 | with open(ml_log) as fread:
205 | for line in fread:
206 | if 'Mesh Bounding Box min' in line: #2016.12
207 | geometry['aabb']['min'] = (line.split()[4:7])
208 | geometry['aabb']['min'] = [util.to_float(val) for val in geometry['aabb']['min']]
209 | if 'Mesh Bounding Box max' in line: #2016.12
210 | geometry['aabb']['max'] = (line.split()[4:7])
211 | geometry['aabb']['max'] = [util.to_float(val) for val in geometry['aabb']['max']]
212 | if 'Mesh Bounding Box Size' in line: #2016.12
213 | geometry['aabb']['size'] = (line.split()[4:7])
214 | geometry['aabb']['size'] = [util.to_float(val) for val in geometry['aabb']['size']]
215 | if 'Mesh Bounding Box Diag' in line: #2016.12
216 | geometry['aabb']['diagonal'] = util.to_float(line.split()[4])
217 | if 'Mesh Volume' in line:
218 | geometry['volume_mm3'] = util.to_float(line.split()[3])
219 | geometry['volume_cm3'] = geometry['volume_mm3'] * 0.001
220 | if 'Mesh Surface' in line:
221 | if ml_version == '1.3.4BETA':
222 | geometry['area_mm2'] = util.to_float(line.split()[3])
223 | else:
224 | geometry['area_mm2'] = util.to_float(line.split()[4])
225 | geometry['area_cm2'] = geometry['area_mm2'] * 0.01
226 | if 'Mesh Total Len of' in line:
227 | if 'including faux edges' in line:
228 | geometry['total_edge_length_incl_faux'] = util.to_float(
229 | line.split()[7])
230 | else:
231 | geometry['total_edge_length'] = util.to_float(
232 | line.split()[7])
233 | if 'Thin shell barycenter' in line:
234 | geometry['barycenter'] = (line.split()[3:6])
235 | geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']]
236 | if 'Thin shell (faces) barycenter' in line: #2016.12
237 | geometry['barycenter'] = (line.split()[4:7])
238 | geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']]
239 | if 'Vertices barycenter' in line: #2016.12
240 | geometry['vert_barycenter'] = (line.split()[2:5])
241 | geometry['vert_barycenter'] = [util.to_float(val) for val in geometry['vert_barycenter']]
242 | if 'Center of Mass' in line:
243 | geometry['center_of_mass'] = (line.split()[4:7])
244 | geometry['center_of_mass'] = [util.to_float(val) for val in geometry['center_of_mass']]
245 | if 'Inertia Tensor' in line:
246 | geometry['inertia_tensor'] = []
247 | for val in range(3):
248 | row = (next(fread, val).split()[1:4])
249 | row = [util.to_float(b) for b in row]
250 | geometry['inertia_tensor'].append(row)
251 | if 'Principal axes' in line:
252 | geometry['principal_axes'] = []
253 | for val in range(3):
254 | row = (next(fread, val).split()[1:4])
255 | row = [util.to_float(b) for b in row]
256 | geometry['principal_axes'].append(row)
257 | if 'axis momenta' in line:
258 | geometry['axis_momenta'] = (next(fread).split()[1:4])
259 | geometry['axis_momenta'] = [util.to_float(val) for val in geometry['axis_momenta']]
260 | break # stop after we find the first match
261 | geometry['aabb']['center'] = [geometry['aabb']['max'][0] - geometry['aabb']['size'][0]/2.0, geometry['aabb']['max'][1] - geometry['aabb']['size'][1]/2, geometry['aabb']['max'][2] - geometry['aabb']['size'][2]/2]
262 |
263 | for key, value in geometry.items():
264 | if log is not None:
265 | log_file = open(log, 'a')
266 | log_file.write('{:27} = {}\n'.format(key, value))
267 | log_file.close()
268 | elif print_output:
269 | print('{:27} = {}'.format(key, value))
270 | return geometry
271 |
272 |
273 | def parse_topology(ml_log, log=None, ml_version='1.3.4BETA', print_output=False):
274 | """Parse the ml_log file generated by the measure_topology function.
275 |
276 | Args:
277 | ml_log (str): MeshLab log file to parse
278 | log (str): filename to log output
279 |
280 | Returns:
281 | dict: dictionary with the following keys:
282 | vert_num (int): number of vertices
283 | edge_num (int): number of edges
284 | face_num (int): number of faces
285 | unref_vert_num (int): number or unreferenced vertices
286 | boundry_edge_num (int): number of boundary edges
287 | part_num (int): number of parts (components) in the mesh.
288 | manifold (bool): True if mesh is two-manifold, otherwise false.
289 | non_manifold_edge (int): number of non_manifold edges.
290 | non_manifold_vert (int): number of non-manifold verices
291 | genus (int or str): genus of the mesh, either a number or
292 | 'undefined' if the mesh is non-manifold.
293 | holes (int or str): number of holes in the mesh, either a number
294 | or 'undefined' if the mesh is non-manifold.
295 |
296 | """
297 | topology = {'manifold': True, 'non_manifold_E': 0, 'non_manifold_V': 0}
298 | with open(ml_log) as fread:
299 | for line in fread:
300 | if 'V:' in line:
301 | vert_edge_face = line.replace('V:', ' ').replace('E:', ' ').replace('F:', ' ').split()
302 | topology['vert_num'] = int(vert_edge_face[0])
303 | topology['edge_num'] = int(vert_edge_face[1])
304 | topology['face_num'] = int(vert_edge_face[2])
305 | if 'Unreferenced Vertices' in line:
306 | topology['unref_vert_num'] = int(line.split()[2])
307 | if 'Boundary Edges' in line:
308 | topology['boundry_edge_num'] = int(line.split()[2])
309 | if 'Mesh is composed by' in line:
310 | topology['part_num'] = int(line.split()[4])
311 | if 'non 2-manifold mesh' in line:
312 | topology['manifold'] = False
313 | if 'non two manifold edges' in line:
314 | topology['non_manifold_edge'] = int(line.split()[2])
315 | if 'non two manifold vertexes' in line:
316 | topology['non_manifold_vert'] = int(line.split()[2])
317 | if 'Genus is' in line: # undefined or int
318 | topology['genus'] = line.split()[2]
319 | if topology['genus'] != 'undefined':
320 | topology['genus'] = int(topology['genus'])
321 | if 'Mesh has' in line and 'holes' in line: # previously just searched on 'holes' but this collided with filenames
322 | topology['hole_num'] = line.split()[2]
323 | if topology['hole_num'] == 'a':
324 | topology['hole_num'] = 'undefined'
325 | else:
326 | topology['hole_num'] = int(topology['hole_num'])
327 | for key, value in topology.items():
328 | if log is not None:
329 | log_file = open(log, 'a')
330 | log_file.write('{:16} = {}\n'.format(key, value))
331 | log_file.close()
332 | elif print_output:
333 | print('{:16} = {}'.format(key, value))
334 |
335 | return topology
336 |
337 |
338 | def parse_hausdorff(ml_log, log=None, print_output=False):
339 | """Parse the ml_log file generated by the hausdorff_distance function.
340 |
341 | Args:
342 | ml_log (str): MeshLab log file to parse
343 | log (str): filename to log output
344 |
345 | Returns:
346 | dict: dictionary with the following keys:
347 | number_points (int): number of points in mesh
348 | min_distance (float): minimum hausdorff distance
349 | max_distance (float): maximum hausdorff distance
350 | mean_distance (float): mean hausdorff distance
351 | rms_distance (float): root mean square distance
352 |
353 | """
354 | hausdorff_distance = {"min_distance": 0.0,
355 | "max_distance": 0.0,
356 | "mean_distance": 0.0,
357 | "rms_distance": 0.0,
358 | "number_points": 0}
359 | with open(ml_log) as fread:
360 | result = fread.readlines()
361 | data = ""
362 |
363 | for idx, line in enumerate(result):
364 | m = re.match(r"\s*Sampled (\d+) pts.*", line)
365 | if m is not None:
366 | hausdorff_distance["number_points"] = int(m.group(1))
367 | if 'Hausdorff Distance computed' in line:
368 | data = result[idx + 2]
369 |
370 | m = re.match(r"\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)", data)
371 | hausdorff_distance["min_distance"] = float(m.group(1))
372 | hausdorff_distance["max_distance"] = float(m.group(2))
373 | hausdorff_distance["mean_distance"] = float(m.group(3))
374 | hausdorff_distance["rms_distance"] = float(m.group(4))
375 | for key, value in hausdorff_distance.items():
376 | if log is not None:
377 | log_file = open(log, 'a')
378 | log_file.write('{:16} = {}\n'.format(key, value))
379 | log_file.close()
380 | elif print_output:
381 | print('{:16} = {}'.format(key, value))
382 | return hausdorff_distance
383 |
--------------------------------------------------------------------------------
/meshlabxml/delete.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML deletion functions"""
2 |
3 | from . import util
4 | from . import select
5 |
6 | def nonmanifold_vert(script):
7 | """ Select & delete the non manifold vertices that do not belong to
8 | non manifold edges.
9 |
10 | For example two cones connected by their apex. Vertices incident on
11 | non manifold edges are ignored.
12 |
13 | Args:
14 | script: the FilterScript object or script filename to write
15 | the filter to.
16 |
17 | Layer stack:
18 | No impacts
19 |
20 | MeshLab versions:
21 | 2016.12
22 | 1.3.4BETA
23 | """
24 | select.nonmanifold_vert(script)
25 | selected(script, face=False)
26 | return None
27 |
28 |
29 | def nonmanifold_edge(script):
30 | """ Select & delete the faces and the vertices incident on
31 | non manifold edges (e.g. edges where more than two faces are incident).
32 |
33 | Note that this function selects the components that are related to
34 | non manifold edges. The case of non manifold vertices is specifically
35 | managed by nonmanifold_vert.
36 |
37 | Args:
38 | script: the FilterScript object or script filename to write
39 | the filter to.
40 |
41 | Layer stack:
42 | No impacts
43 |
44 | MeshLab versions:
45 | 2016.12
46 | 1.3.4BETA
47 | """
48 | select.nonmanifold_edge(script)
49 | selected(script, face=False)
50 | return None
51 |
52 |
53 | def small_parts(script, ratio=0.2, non_closed_only=False):
54 | """ Select & delete the small disconnected parts (components) of a mesh.
55 |
56 | Args:
57 | script: the FilterScript object or script filename to write
58 | the filter to.
59 | ratio (float): This ratio (between 0 and 1) defines the meaning of
60 | 'small' as the threshold ratio between the number of faces of the
61 | largest component and the other ones. A larger value will select
62 | more components.
63 | non_closed_only (bool): Select only non-closed components.
64 |
65 | Layer stack:
66 | No impacts
67 |
68 | MeshLab versions:
69 | 2016.12
70 | 1.3.4BETA
71 | """
72 | select.small_parts(script, ratio, non_closed_only)
73 | selected(script)
74 | return None
75 |
76 |
77 | def selected(script, face=True, vert=True):
78 | """ Delete selected vertices and/or faces
79 |
80 | Note: if the mesh has no faces (e.g. a point cloud) you must
81 | set face=False, or the vertices will not be deleted
82 |
83 | Args:
84 | script: the FilterScript object or script filename to write
85 | the filter to.
86 | face (bool): if True the selected faces will be deleted. If vert
87 | is also True, then all the vertices surrounded by those faces will
88 | also be deleted. Note that if no faces are selected (only vertices)
89 | then this filter will not do anything. For example, if you want to
90 | delete a point cloud selection, you must set this to False.
91 | vert (bool): if True the selected vertices will be deleted.
92 |
93 | Layer stack:
94 | No impacts
95 |
96 | MeshLab versions:
97 | 2016.12
98 | 1.3.4BETA
99 | """
100 | if face and vert:
101 | filter_xml = ' \n'
102 | elif face and not vert:
103 | filter_xml = ' \n'
104 | elif not face and vert:
105 | filter_xml = ' \n'
106 | util.write_filter(script, filter_xml)
107 | return None
108 |
109 |
110 | def faces_from_nonmanifold_edges(script):
111 | """ For each non manifold edge it iteratively deletes the smallest area
112 | face until it becomes two-manifold.
113 |
114 | Args:
115 | script: the FilterScript object or script filename to write
116 | the filter to.
117 |
118 | Layer stack:
119 | No impacts
120 |
121 | MeshLab versions:
122 | 2016.12
123 | 1.3.4BETA
124 | """
125 | if script.ml_version == '1.3.4BETA':
126 | filter_xml = ' \n'
127 | else:
128 | filter_xml = ' \n'
129 | util.write_filter(script, filter_xml)
130 | #unreferenced_vert(script)
131 | return None
132 |
133 |
134 | def unreferenced_vert(script):
135 | """ Check for every vertex on the mesh: if it is NOT referenced by a face,
136 | removes it.
137 |
138 | Args:
139 | script: the FilterScript object or script filename to write
140 | the filter to.
141 |
142 | Layer stack:
143 | No impacts
144 |
145 | MeshLab versions:
146 | 2016.12
147 | 1.3.4BETA
148 | """
149 | if script.ml_version == '1.3.4BETA':
150 | filter_xml = ' \n'
151 | else:
152 | filter_xml = ' \n'
153 | util.write_filter(script, filter_xml)
154 | return None
155 |
156 |
157 | def duplicate_faces(script):
158 | """ Delete all the duplicate faces.
159 |
160 | Two faces are considered equal if they are composed by the same set of
161 | vertices, regardless of the order of the vertices.
162 |
163 | Args:
164 | script: the FilterScript object or script filename to write
165 | the filter to.
166 |
167 | Layer stack:
168 | No impacts
169 |
170 | MeshLab versions:
171 | 2016.12
172 | 1.3.4BETA
173 | """
174 | filter_xml = ' \n'
175 | util.write_filter(script, filter_xml)
176 | return None
177 |
178 |
179 | def duplicate_verts(script):
180 | """ "Check for every vertex on the mesh: if there are two vertices with
181 | the same coordinates they are merged into a single one.
182 |
183 | Args:
184 | script: the FilterScript object or script filename to write
185 | the filter to.
186 |
187 | Layer stack:
188 | No impacts
189 |
190 | MeshLab versions:
191 | 2016.12
192 | 1.3.4BETA
193 | """
194 | if script.ml_version == '1.3.4BETA':
195 | filter_xml = ' \n'
196 | else:
197 | filter_xml = ' \n'
198 |
199 | util.write_filter(script, filter_xml)
200 | return None
201 |
202 |
203 | def zero_area_face(script):
204 | """ Remove null faces (the ones with area equal to zero)
205 |
206 | Args:
207 | script: the FilterScript object or script filename to write
208 | the filter to.
209 |
210 | Layer stack:
211 | No impacts
212 |
213 | MeshLab versions:
214 | 2016.12
215 | 1.3.4BETA
216 | """
217 | filter_xml = ' \n'
218 | util.write_filter(script, filter_xml)
219 | return None
220 |
--------------------------------------------------------------------------------
/meshlabxml/files.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML functions that operate on mesh files """
2 |
3 | import os
4 | import sys
5 | import math
6 |
7 | import meshlabxml as mlx
8 | from . import run
9 | from . import util
10 | from . import compute
11 | from . import transform
12 | from . import layers
13 |
14 | #ml_version = '1.3.4BETA'
15 | #ml_version = '2016.12'
16 | ml_version = '2020.09'
17 |
18 | def measure_aabb(fbasename=None, log=None, coord_system='CARTESIAN'):
19 | """ Measure the axis aligned bounding box (aabb) of a mesh
20 | in multiple coordinate systems.
21 |
22 | Args:
23 | fbasename (str): filename of input model
24 | log (str): filename of log file
25 | coord_system (enum in ['CARTESIAN', 'CYLINDRICAL']
26 | Coordinate system to use:
27 | 'CARTESIAN': lists contain [x, y, z]
28 | 'CYLINDRICAL': lists contain [r, theta, z]
29 | Returns:
30 | dict: dictionary with the following aabb properties
31 | min (3 element list): minimum values
32 | max (3 element list): maximum values
33 | center (3 element list): the center point
34 | size (3 element list): size of the aabb in each coordinate (max-min)
35 | diagonal (float): the diagonal of the aabb
36 | """
37 | # TODO: add center point, spherical coordinate system
38 | fext = os.path.splitext(fbasename)[1][1:].strip().lower()
39 | if fext != 'xyz':
40 | fin = 'TEMP3D_aabb.xyz'
41 | run(log=log, file_in=fbasename, file_out=fin, script=None)
42 | else:
43 | fin = fbasename
44 | fread = open(fin, 'r')
45 | aabb = {'min': [999999.0, 999999.0, 999999.0], 'max': [-999999.0, -999999.0, -999999.0]}
46 | for line in fread:
47 | x_co, y_co, z_co = line.split()
48 | x_co = util.to_float(x_co)
49 | y_co = util.to_float(y_co)
50 | z_co = util.to_float(z_co)
51 | if coord_system == 'CARTESIAN':
52 | if x_co < aabb['min'][0]:
53 | aabb['min'][0] = x_co
54 | if y_co < aabb['min'][1]:
55 | aabb['min'][1] = y_co
56 | if z_co < aabb['min'][2]:
57 | aabb['min'][2] = z_co
58 | if x_co > aabb['max'][0]:
59 | aabb['max'][0] = x_co
60 | if y_co > aabb['max'][1]:
61 | aabb['max'][1] = y_co
62 | if z_co > aabb['max'][2]:
63 | aabb['max'][2] = z_co
64 | elif coord_system == 'CYLINDRICAL':
65 | radius = math.sqrt(x_co**2 + y_co**2)
66 | theta = math.degrees(math.atan2(y_co, x_co))
67 | if radius < aabb['min'][0]:
68 | aabb['min'][0] = radius
69 | if theta < aabb['min'][1]:
70 | aabb['min'][1] = theta
71 | if z_co < aabb['min'][2]:
72 | aabb['min'][2] = z_co
73 | if radius > aabb['max'][0]:
74 | aabb['max'][0] = radius
75 | if theta > aabb['max'][1]:
76 | aabb['max'][1] = theta
77 | if z_co > aabb['max'][2]:
78 | aabb['max'][2] = z_co
79 | fread.close()
80 | try:
81 | aabb['center'] = [(aabb['max'][0] + aabb['min'][0]) / 2,
82 | (aabb['max'][1] + aabb['min'][1]) / 2,
83 | (aabb['max'][2] + aabb['min'][2]) / 2]
84 | aabb['size'] = [aabb['max'][0] - aabb['min'][0], aabb['max'][1] - aabb['min'][1],
85 | aabb['max'][2] - aabb['min'][2]]
86 | aabb['diagonal'] = math.sqrt(
87 | aabb['size'][0]**2 +
88 | aabb['size'][1]**2 +
89 | aabb['size'][2]**2)
90 | except UnboundLocalError:
91 | print('Error: aabb input file does not contain valid data. Exiting ...')
92 | sys.exit(1)
93 | for key, value in aabb.items():
94 | if log is None:
95 | print('{:10} = {}'.format(key, value))
96 | else:
97 | log_file = open(log, 'a')
98 | log_file.write('{:10} = {}\n'.format(key, value))
99 | log_file.close()
100 | """
101 | if log is not None:
102 | log_file = open(log, 'a')
103 | #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename)
104 | log_file.write('min = %s\n' % aabb['min'])
105 | log_file.write('max = %s\n' % aabb['max'])
106 | log_file.write('center = %s\n' % aabb['center'])
107 | log_file.write('size = %s\n' % aabb['size'])
108 | log_file.write('diagonal = %s\n\n' % aabb['diagonal'])
109 | log_file.close()
110 | # print(aabb)
111 | """
112 | return aabb
113 |
114 |
115 | def measure_section(fbasename=None, log=None, axis='z', offset=0.0,
116 | rotate_x_angle=None, ml_version=ml_version):
117 | """Measure a cross section of a mesh
118 |
119 | Perform a plane cut in one of the major axes (X, Y, Z). If you want to cut on
120 | a different plane you will need to rotate the model in place, perform the cut,
121 | and rotate it back.
122 |
123 | Args:
124 | fbasename (str): filename of input model
125 | log (str): filename of log file
126 | axis (str): axis perpendicular to the cutting plane, e.g. specify "z" to cut
127 | parallel to the XY plane.
128 | offset (float): amount to offset the cutting plane from the origin
129 | rotate_x_angle (float): degrees to rotate about the X axis. Useful for correcting "Up" direction: 90 to rotate Y to Z, and -90 to rotate Z to Y.
130 |
131 | Returns:
132 | dict: dictionary with the following keys for the aabb of the section:
133 | min (list): list of the x, y & z minimum values
134 | max (list): list of the x, y & z maximum values
135 | center (list): the x, y & z coordinates of the center of the aabb
136 | size (list): list of the x, y & z sizes (max - min)
137 | diagonal (float): the diagonal of the aabb
138 | """
139 | ml_script1_file = 'TEMP3D_measure_section.mlx'
140 | file_out = 'TEMP3D_sect_aabb.xyz'
141 |
142 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
143 | if rotate_x_angle is not None:
144 | transform.rotate(ml_script1, axis='x', angle=rotate_x_angle)
145 | compute.section(ml_script1, axis=axis, offset=offset)
146 | layers.delete_lower(ml_script1)
147 | ml_script1.save_to_file(ml_script1_file)
148 | ml_script1.run_script(log=log, script_file=ml_script1_file)
149 | aabb = measure_aabb(file_out, log)
150 | return aabb
151 |
152 |
153 | def polylinesort(fbasename=None, log=None):
154 | """Sort separate line segments in obj format into a continuous polyline or polylines.
155 | NOT FINISHED; DO NOT USE
156 |
157 | Also measures the length of each polyline
158 |
159 | Return polyline and polylineMeta (lengths)
160 |
161 | """
162 | fext = os.path.splitext(fbasename)[1][1:].strip().lower()
163 | if fext != 'obj':
164 | print('Input file must be obj. Exiting ...')
165 | sys.exit(1)
166 | fread = open(fbasename, 'r')
167 | first = True
168 | polyline_vertices = []
169 | line_segments = []
170 | for line in fread:
171 | element, x_co, y_co, z_co = line.split()
172 | if element == 'v':
173 | polyline_vertices.append(
174 | [util.to_float(x_co), util.to_float(y_co), util.to_float(z_co)])
175 | elif element == 'l':
176 | p1 = x_co
177 | p2 = y_co
178 | line_segments.append([int(p1), int(p2)])
179 |
180 | fread.close()
181 | if log is not None:
182 | log_file = open(log, 'a')
183 | #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename)
184 | """log_file.write('min = %s\n' % aabb['min'])
185 | log_file.write('max = %s\n' % aabb['max'])
186 | log_file.write('center = %s\n' % aabb['center'])
187 | log_file.write('size = %s\n' % aabb['size'])
188 | log_file.write('diagonal = %s\n' % aabb['diagonal'])"""
189 | log_file.close()
190 | # print(aabb)
191 | return None
192 |
193 |
194 | def measure_geometry(fbasename=None, log=None, ml_version=ml_version):
195 | """Measures mesh geometry, including aabb"""
196 | ml_script1_file = 'TEMP3D_measure_geometry.mlx'
197 | if ml_version == '1.3.4BETA':
198 | file_out = 'TEMP3D_aabb.xyz'
199 | else:
200 | file_out = None
201 |
202 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
203 | compute.measure_geometry(ml_script1)
204 | ml_script1.save_to_file(ml_script1_file)
205 | ml_script1.run_script(log=log, script_file=ml_script1_file)
206 | geometry = ml_script1.geometry
207 |
208 | if ml_version == '1.3.4BETA':
209 | if log is not None:
210 | log_file = open(log, 'a')
211 | log_file.write(
212 | '***Axis Aligned Bounding Results for file "%s":\n' %
213 | fbasename)
214 | log_file.close()
215 | aabb = measure_aabb(file_out, log)
216 | else:
217 | aabb = geometry['aabb']
218 | return aabb, geometry
219 |
220 |
221 | def measure_topology(fbasename=None, log=None, ml_version=ml_version):
222 | """Measures mesh topology
223 |
224 | Args:
225 | fbasename (str): input filename.
226 | log (str): filename to log output
227 |
228 | Returns:
229 | dict: dictionary with the following keys:
230 | vert_num (int): number of vertices
231 | edge_num (int): number of edges
232 | face_num (int): number of faces
233 | unref_vert_num (int): number or unreferenced vertices
234 | boundry_edge_num (int): number of boundary edges
235 | part_num (int): number of parts (components) in the mesh.
236 | manifold (bool): True if mesh is two-manifold, otherwise false.
237 | non_manifold_edge (int): number of non_manifold edges.
238 | non_manifold_vert (int): number of non-manifold verices
239 | genus (int or str): genus of the mesh, either a number or
240 | 'undefined' if the mesh is non-manifold.
241 | holes (int or str): number of holes in the mesh, either a number
242 | or 'undefined' if the mesh is non-manifold.
243 |
244 | """
245 | ml_script1_file = 'TEMP3D_measure_topology.mlx'
246 | ml_script1 = mlx.FilterScript(file_in=fbasename, ml_version=ml_version)
247 | compute.measure_topology(ml_script1)
248 | ml_script1.save_to_file(ml_script1_file)
249 | ml_script1.run_script(log=log, script_file=ml_script1_file)
250 | topology = ml_script1.topology
251 | return topology
252 |
253 |
254 | def measure_all(fbasename=None, log=None, ml_version=ml_version):
255 | """Measures mesh geometry, aabb and topology."""
256 | ml_script1_file = 'TEMP3D_measure_gAndT.mlx'
257 | if ml_version == '1.3.4BETA':
258 | file_out = 'TEMP3D_aabb.xyz'
259 | else:
260 | file_out = None
261 |
262 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
263 | compute.measure_geometry(ml_script1)
264 | compute.measure_topology(ml_script1)
265 | ml_script1.save_to_file(ml_script1_file)
266 | ml_script1.run_script(log=log, script_file=ml_script1_file)
267 | geometry = ml_script1.geometry
268 | topology = ml_script1.topology
269 |
270 | if ml_version == '1.3.4BETA':
271 | if log is not None:
272 | log_file = open(log, 'a')
273 | log_file.write(
274 | '***Axis Aligned Bounding Results for file "%s":\n' %
275 | fbasename)
276 | log_file.close()
277 | aabb = measure_aabb(file_out, log)
278 | else:
279 | aabb = geometry['aabb']
280 | return aabb, geometry, topology
281 |
282 |
283 | def measure_dimension(fbasename=None, log=None, axis1=None, offset1=0.0,
284 | axis2=None, offset2=0.0, ml_version=ml_version):
285 | """Measure a dimension of a mesh"""
286 | axis1 = axis1.lower()
287 | axis2 = axis2.lower()
288 | ml_script1_file = 'TEMP3D_measure_dimension.mlx'
289 | file_out = 'TEMP3D_measure_dimension.xyz'
290 |
291 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
292 | compute.section(ml_script1, axis1, offset1, surface=True)
293 | compute.section(ml_script1, axis2, offset2, surface=False)
294 | layers.delete_lower(ml_script1)
295 | ml_script1.save_to_file(ml_script1_file)
296 | ml_script1.run_script(log=log, script_file=ml_script1_file)
297 |
298 | for val in ('x', 'y', 'z'):
299 | if val not in (axis1, axis2):
300 | axis = val
301 | # ord: Get number that represents letter in ASCII
302 | # Here we find the offset from 'x' to determine the list reference
303 | # i.e. 0 for x, 1 for y, 2 for z
304 | axis_num = ord(axis) - ord('x')
305 | aabb = measure_aabb(file_out, log)
306 | dimension = {'min': aabb['min'][axis_num], 'max': aabb['max'][axis_num],
307 | 'length': aabb['size'][axis_num], 'axis': axis}
308 | if log is None:
309 | print('\nFor file "%s"' % fbasename)
310 | print('Dimension parallel to %s with %s=%s & %s=%s:' % (axis, axis1, offset1,
311 | axis2, offset2))
312 | print(' Min = %s, Max = %s, Total length = %s' % (dimension['min'],
313 | dimension['max'], dimension['length']))
314 | else:
315 | log_file = open(log, 'a')
316 | log_file.write('\nFor file "%s"\n' % fbasename)
317 | log_file.write('Dimension parallel to %s with %s=%s & %s=%s:\n' % (axis, axis1, offset1,
318 | axis2, offset2))
319 | log_file.write('min = %s\n' % dimension['min'])
320 | log_file.write('max = %s\n' % dimension['max'])
321 | log_file.write('Total length = %s\n' % dimension['length'])
322 | log_file.close()
323 | return dimension
324 |
--------------------------------------------------------------------------------
/meshlabxml/layers.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML layer functions """
2 |
3 | #from . import mlx.FilterScript
4 | #from meshlabxml import mlx.FilterScript
5 | import meshlabxml as mlx
6 | from . import util
7 |
8 | def join(script, merge_visible=True, merge_vert=False, delete_layer=True,
9 | keep_unreferenced_vert=False):
10 | """ Flatten all or only the visible layers into a single new mesh.
11 |
12 | Transformations are preserved. Existing layers can be optionally
13 | deleted.
14 |
15 | Args:
16 | script: the mlx.FilterScript object or script filename to write
17 | the filter to.
18 | merge_visible (bool): merge only visible layers
19 | merge_vert (bool): merge the vertices that are duplicated among
20 | different layers. Very useful when the layers are spliced portions
21 | of a single big mesh.
22 | delete_layer (bool): delete all the merged layers. If all layers are
23 | visible only a single layer will remain after the invocation of
24 | this filter.
25 | keep_unreferenced_vert (bool): Do not discard unreferenced vertices
26 | from source layers. Necessary for point-only layers.
27 |
28 | Layer stack:
29 | Creates a new layer "Merged Mesh"
30 | Changes current layer to the new layer
31 | Optionally deletes all other layers
32 |
33 | MeshLab versions:
34 | 2016.12
35 | 1.3.4BETA
36 |
37 | Bugs:
38 | UV textures: not currently preserved, however will be in a future
39 | release. https://github.com/cnr-isti-vclab/meshlab/issues/128
40 | merge_visible: it is not currently possible to change the layer
41 | visibility from meshlabserver, however this will be possible
42 | in the future https://github.com/cnr-isti-vclab/meshlab/issues/123
43 | """
44 | filter_xml = ''.join([
45 | ' \n',
46 | ' \n',
51 | ' \n',
56 | ' \n',
61 | ' \n',
66 | ' \n'])
67 | util.write_filter(script, filter_xml)
68 | if isinstance(script, mlx.FilterScript):
69 | script.add_layer('Merged Mesh')
70 | if delete_layer:
71 | # As it is not yet possible to change the layer visibility, all
72 | # layers will be deleted. This will be updated once layer
73 | # visibility is tracked.
74 | for i in range(script.last_layer()):
75 | script.del_layer(0)
76 | return None
77 |
78 |
79 | def delete(script, layer_num=None):
80 | """ Delete layer
81 |
82 | Args:
83 | script: the mlx.FilterScript object or script filename to write
84 | the filter to.
85 | layer_num (int): the number of the layer to delete. Default is the
86 | current layer. Not supported on the file base API.
87 |
88 | Layer stack:
89 | Deletes a layer
90 | will change current layer if deleted layer is lower in the stack
91 |
92 | MeshLab versions:
93 | 2016.12
94 | 1.3.4BETA
95 | """
96 | filter_xml = ' \n'
97 | if isinstance(script, mlx.FilterScript):
98 | if (layer_num is None) or (layer_num == script.current_layer()):
99 | util.write_filter(script, filter_xml)
100 | script.del_layer(script.current_layer())
101 | else:
102 | cur_layer = script.current_layer()
103 | change(script, layer_num)
104 | util.write_filter(script, filter_xml)
105 | if layer_num < script.current_layer():
106 | change(script, cur_layer - 1)
107 | else:
108 | change(script, cur_layer)
109 | script.del_layer(layer_num)
110 | else:
111 | util.write_filter(script, filter_xml)
112 | return None
113 |
114 |
115 | def rename(script, label='blank', layer_num=None):
116 | """ Rename layer label
117 |
118 | Can be useful for outputting mlp files, as the output file names use
119 | the labels.
120 |
121 | Args:
122 | script: the mlx.FilterScript object or script filename to write
123 | the filter to.
124 | label (str): new label for the mesh layer
125 | layer_num (int): layer number to rename. Default is the
126 | current layer. Not supported on the file base API.
127 |
128 | Layer stack:
129 | Renames a layer
130 |
131 | MeshLab versions:
132 | 2016.12
133 | 1.3.4BETA
134 | """
135 | filter_xml = ''.join([
136 | ' \n',
137 | ' \n',
142 | ' \n'])
143 | if isinstance(script, mlx.FilterScript):
144 | if (layer_num is None) or (layer_num == script.current_layer()):
145 | util.write_filter(script, filter_xml)
146 | script.layer_stack[script.current_layer()] = label
147 | else:
148 | cur_layer = script.current_layer()
149 | change(script, layer_num)
150 | util.write_filter(script, filter_xml)
151 | change(script, cur_layer)
152 | script.layer_stack[layer_num] = label
153 | else:
154 | util.write_filter(script, filter_xml)
155 | return None
156 |
157 |
158 | def change(script, layer_num=None):
159 | """ Change the current layer by specifying the new layer number.
160 |
161 | Args:
162 | script: the mlx.FilterScript object or script filename to write
163 | the filter to.
164 | layer_num (int): the number of the layer to change to. Default is the
165 | last layer if script is a mlx.FilterScript object; if script is a
166 | filename the default is the first layer.
167 |
168 | Layer stack:
169 | Modifies current layer
170 |
171 | MeshLab versions:
172 | 2016.12
173 | 1.3.4BETA
174 | """
175 | if layer_num is None:
176 | if isinstance(script, mlx.FilterScript):
177 | layer_num = script.last_layer()
178 | else:
179 | layer_num = 0
180 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12':
181 | filter_xml = ''.join([
182 | ' \n',
183 | ' \n',
188 | ' \n'])
189 | else:
190 | filter_xml = ''.join([
191 | ' \n',
192 | ' \n',
197 | ' \n'])
198 | util.write_filter(script, filter_xml)
199 | if isinstance(script, mlx.FilterScript):
200 | script.set_current_layer(layer_num)
201 | #script.layer_stack[len(self.layer_stack) - 1] = layer_num
202 | return None
203 |
204 |
205 | def duplicate(script, layer_num=None):
206 | """ Duplicate a layer.
207 |
208 | New layer label is '*_copy'.
209 |
210 | Args:
211 | script: the mlx.FilterScript object or script filename to write
212 | the filter to.
213 | layer_num (int): layer number to duplicate. Default is the
214 | current layer. Not supported on the file base API.
215 |
216 | Layer stack:
217 | Creates a new layer
218 | Changes current layer to the new layer
219 |
220 | MeshLab versions:
221 | 2016.12
222 | 1.3.4BETA
223 | """
224 | filter_xml = ' \n'
225 | if isinstance(script, mlx.FilterScript):
226 | if (layer_num is None) or (layer_num == script.current_layer()):
227 | util.write_filter(script, filter_xml)
228 | script.add_layer('{}_copy'.format(script.layer_stack[script.current_layer()]), True)
229 | else:
230 | change(script, layer_num)
231 | util.write_filter(script, filter_xml)
232 | script.add_layer('{}_copy'.format(script.layer_stack[layer_num]), True)
233 | else:
234 | util.write_filter(script, filter_xml)
235 | return None
236 |
237 |
238 | def split_parts(script, part_num=None, layer_num=None):
239 | """ Split current layer into many layers, one for each part (connected
240 | component)
241 |
242 | Mesh is split so that the largest part is the lowest named layer "CC 0"
243 | and the smallest part is the highest numbered "CC" layer.
244 |
245 | Args:
246 | script: the mlx.FilterScript object or script filename to write
247 | the filter to.
248 | part_num (int): the number of parts in the model. This is needed in
249 | order to properly create and manage the layer stack. Can be found
250 | with mlx.compute.measure_topology.
251 | layer_num (int): the number of the layer to split. Default is the
252 | current layer. Not supported on the file base API.
253 |
254 | Layer stack:
255 | Creates a new layer for each part named "CC 0", "CC 1", etc.
256 | Changes current layer to the last new layer
257 |
258 | MeshLab versions:
259 | 2016.12
260 | 1.3.4BETA
261 |
262 | Bugs:
263 | UV textures: not currently preserved, however will be in a future
264 | release. https://github.com/cnr-isti-vclab/meshlab/issues/127
265 | """
266 | filter_xml = ' \n'
267 | if isinstance(script, mlx.FilterScript):
268 | if (layer_num is not None) and (layer_num != script.current_layer()):
269 | change(script, layer_num)
270 | util.write_filter(script, filter_xml)
271 | if part_num is not None:
272 | for i in range(part_num):
273 | script.add_layer('CC {}'.format(i), True)
274 | else:
275 | script.add_layer('CC 0', True)
276 | print('Warning: the number of parts was not provided and cannot',
277 | 'be determined automatically. The layer stack is likely',
278 | 'incorrect!')
279 | else:
280 | util.write_filter(script, filter_xml)
281 | return None
282 |
283 | def delete_lower(script, layer_num=None):
284 | """ Delete all layers below the specified one.
285 |
286 | Useful for MeshLab ver 2016.12, which will only output layer 0.
287 | """
288 | if layer_num is None:
289 | layer_num = script.current_layer()
290 | if layer_num != 0:
291 | change(script, 0)
292 | for i in range(layer_num):
293 | delete(script, 0)
294 | return None
295 |
--------------------------------------------------------------------------------
/meshlabxml/mp_func.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML muparser functions """
2 |
3 | import math
4 | import re
5 |
6 | from . import FilterScript
7 | from . import util
8 |
9 |
10 | def muparser_ref():
11 | """Reference documentation for muparser.
12 |
13 | muparser is used by many internal MeshLab filters, specifically those
14 | where you can control parameters via a mathematical expression. Examples:
15 | transform.function
16 | select.vert_function
17 | vert_color.function
18 |
19 | The valid variables that can be used in an expression are given in the
20 | documentation for the individual functions. Generally, it's possible to use
21 | the following per-vertex variables in the expression:
22 |
23 | Variables (per vertex):
24 | x, y, z (coordinates)
25 | nx, ny, nz (normal)
26 | r, g, b, a (color)
27 | q (quality)
28 | rad
29 | vi (vertex index)
30 | vtu, vtv (texture coordinates)
31 | ti (texture index)
32 | vsel (is the vertex selected? 1 yes, 0 no)
33 | and all custom vertex attributes already defined by user.
34 |
35 | Below is a list of the predefined operators and functions within muparser.
36 |
37 | muparser homepage: http://beltoforion.de/article.php?a=muparser
38 | ml_version='1.3.4Beta' muparser version: 1.3.2
39 | ml_version='2016.12' muparser version: 2.2.5
40 |
41 | Built-in functions:
42 | Name Argc. Explanation
43 | ----------------------------
44 | sin 1 sine function
45 | cos 1 cosine function
46 | tan 1 tangens function
47 | asin 1 arcus sine function
48 | acos 1 arcus cosine function
49 | atan 1 arcus tangens function
50 | atan2 2 atan2(y, x)
51 | sinh 1 hyperbolic sine function
52 | cosh 1 hyperbolic cosine
53 | tanh 1 hyperbolic tangens function
54 | asinh 1 hyperbolic arcus sine function
55 | acosh 1 hyperbolic arcus tangens function
56 | atanh 1 hyperbolic arcur tangens function
57 | log2 1 logarithm to the base 2
58 | log10 1 logarithm to the base 10
59 | log 1 logarithm to the base 10
60 | ln 1 logarithm to base e (2.71828...)
61 | exp 1 e raised to the power of x
62 | sqrt 1 square root of a value
63 | sign 1 sign function -1 if x<0; 1 if x>0
64 | rint 1 round to nearest integer
65 | abs 1 absolute value
66 | min var. min of all arguments
67 | max var. max of all arguments
68 | sum var. sum of all arguments
69 | avg var. mean value of all arguments
70 |
71 | Built-in binary operators
72 | Operator Description Priority
73 | -----------------------------------------------
74 | = assignment -1
75 | && logical and* 1
76 | || logical or* 2
77 | <= less or equal 4
78 | >= greater or equal 4
79 | != not equal 4
80 | == equal 4
81 | > greater than 4
82 | < less than 4
83 | + addition 5
84 | - subtraction 5
85 | * multiplication 6
86 | / division 6
87 | ^ raise x to the power of y 7
88 |
89 | Built-in ternary operator
90 | ?: (Test ? Then_value : Otherwise_value)*
91 |
92 | *: Some operators in older muparser (ml_version='1.3.4Beta') are different:
93 | and logical and 1
94 | or logical or 2
95 | if ternary operator: if(Test, Then_value, Otherwise_value)
96 | atan2 not included; use mp_atan2 below
97 | """
98 | pass
99 |
100 |
101 | def mp_atan2(y, x):
102 | """muparser atan2 function
103 |
104 | Implements an atan2(y,x) function for older muparser versions (<2.1.0);
105 | atan2 was added as a built-in function in muparser 2.1.0
106 |
107 | Args:
108 | y (str): y argument of the atan2(y,x) function
109 | x (str): x argument of the atan2(y,x) function
110 |
111 | Returns:
112 | A muparser string that calculates atan2(y,x)
113 | """
114 | return 'if((x)>0, atan((y)/(x)), if(((x)<0) and ((y)>=0), atan((y)/(x))+pi, if(((x)<0) and ((y)<0), atan((y)/(x))-pi, if(((x)==0) and ((y)>0), pi/2, if(((x)==0) and ((y)<0), -pi/2, 0)))))'.replace(
115 | 'pi', str(math.pi)).replace('y', y).replace('x', x)
116 |
117 |
118 | def modulo(a,b):
119 | """ Modulo operator
120 | Example: modulo(angle,2*pi) to limit an angle to within 2pi radians
121 | """
122 | return '(a < 0) ? b - (a - (b * rint(a/b))) : a - (b * rint(a/b))'.replace('a', a).replace('b', b)
123 |
124 |
125 | def v_cross(u, v):
126 | """muparser cross product function
127 |
128 | Compute the cross product of two 3x1 vectors
129 |
130 | Args:
131 | u (list or tuple of 3 strings): first vector
132 | v (list or tuple of 3 strings): second vector
133 | Returns:
134 | A list containing a muparser string of the cross product
135 | """
136 | """
137 | i = u[1]*v[2] - u[2]*v[1]
138 | j = u[2]*v[0] - u[0]*v[2]
139 | k = u[0]*v[1] - u[1]*v[0]
140 | """
141 |
142 | i = '(({u1})*({v2}) - ({u2})*({v1}))'.format(u1=u[1], u2=u[2], v1=v[1], v2=v[2])
143 | j = '(({u2})*({v0}) - ({u0})*({v2}))'.format(u0=u[0], u2=u[2], v0=v[0], v2=v[2])
144 | k = '(({u0})*({v1}) - ({u1})*({v0}))'.format(u0=u[0], u1=u[1], v0=v[0], v1=v[1])
145 | return [i, j, k]
146 |
147 |
148 | def v_dot(v1, v2):
149 | for i, x in enumerate(v1):
150 | if i == 0:
151 | dot = '({})*({})'.format(v1[i], v2[i])
152 | else:
153 | dot += '+({})*({})'.format(v1[i], v2[i])
154 | dot = '(' + dot + ')'
155 | return dot
156 |
157 |
158 | def v_add(v1, v2):
159 | vector = []
160 | for i, x in enumerate(v1):
161 | vector.append('(({})+({}))'.format(v1[i], v2[i]))
162 | return vector
163 |
164 |
165 | def v_subtract(v1, v2):
166 | vector = []
167 | for i, x in enumerate(v1):
168 | vector.append('(({})-({}))'.format(v1[i], v2[i]))
169 | return vector
170 |
171 |
172 | def v_multiply(scalar, v1):
173 | """ Multiply vector by scalar"""
174 | vector = []
175 | for i, x in enumerate(v1):
176 | vector.append('(({})*({}))'.format(scalar, v1[i]))
177 | return vector
178 |
179 |
180 | def v_length(v1):
181 | for i, x in enumerate(v1):
182 | if i == 0:
183 | length = '({})^2'.format(v1[i])
184 | else:
185 | length += '+({})^2'.format(v1[i])
186 | length = 'sqrt(' + length + ')'
187 | return length
188 |
189 |
190 | def v_normalize(v1):
191 | vector = []
192 | length = v_length(v1)
193 | for i, x in enumerate(v1):
194 | vector.append('({})/({})'.format(v1[i], length))
195 | return vector
196 |
197 |
198 | def torus_knot(t, p=3, q=4, scale=1.0, radius=2.0):
199 | """ A tight (small inner crossings) (p,q) torus knot parametric curve
200 |
201 | Source (for trefoil): https://en.wikipedia.org/wiki/Trefoil_knot
202 |
203 | """
204 | return ['{scale}*(sin({t}) + ({radius})*sin({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius),
205 | '{scale}*(cos({t}) - ({radius})*cos({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius),
206 | '{scale}*(-sin({q}*({t})))'.format(t=t, q=q, scale=scale)]
207 |
208 |
209 | def torus_knot_bbox(scale=1.0, radius=2.0):
210 | """ Bounding box of the sprecified torus knot
211 |
212 | """
213 | return [2*scale*(1 + radius), 2*scale*(1 + radius), 2*scale]
214 |
215 |
216 | def vert_attr(script, name='radius', function='x^2 + y^2'):
217 | """ Add a new Per-Vertex scalar attribute to current mesh and fill it with
218 | the defined function.
219 |
220 | The specified name can be used in other filter functions.
221 |
222 | It's possible to use parenthesis, per-vertex variables and boolean operator:
223 | (, ), and, or, <, >, =
224 | It's possible to use the following per-vertex variables in the expression:
225 |
226 | Variables:
227 | x, y, z (coordinates)
228 | nx, ny, nz (normal)
229 | r, g, b, a (color)
230 | q (quality)
231 | rad
232 | vi (vertex index)
233 | ?vtu, vtv (texture coordinates)
234 | ?ti (texture index)
235 | ?vsel (is the vertex selected? 1 yes, 0 no)
236 | and all custom vertex attributes already defined by user.
237 |
238 | Args:
239 | script: the FilterScript object or script filename to write
240 | the filter] to.
241 | name (str): the name of new attribute. You can access attribute in
242 | other filters through this name.
243 | function (str): function to calculate custom attribute value for each
244 | vertex
245 |
246 | Layer stack:
247 | No impacts
248 |
249 | MeshLab versions:
250 | 2016.12
251 | 1.3.4BETA
252 | """
253 | filter_xml = ''.join([
254 | ' \n',
255 | ' \n',
260 | ' \n',
265 | ' \n'])
266 | util.write_filter(script, filter_xml)
267 | return None
268 |
269 |
270 | def face_attr(script, name='radiosity', function='fi'):
271 | """ Add a new Per-Face attribute to current mesh and fill it with
272 | the defined function..
273 |
274 | The specified name can be used in other filter functions.
275 |
276 | It's possible to use parenthesis, per-face variables and boolean operator:
277 | (, ), and, or, <, >, =
278 |
279 | It's possible to use per-face variables like attributes associated to the
280 | three vertices of every face.
281 |
282 | It's possible to use the following per-face variables in the expression:
283 |
284 | Variables:
285 | x0,y0,z0 (first vertex); x1,y1,z1 (second vertex); x2,y2,z2 (third vertex)
286 | nx0,ny0,nz0; nx1,ny1,nz1; nx2,ny2,nz2 (normals)
287 | r0,g0,b0 (color)
288 | q0,q1,q2 (quality)
289 | wtu0, wtv0; wtu1, wtv1; wtu2, wtv2 (per wedge tex coord)
290 | fi (face index)
291 | ?fsel (is the vertex selected? 1 yes, 0 no)
292 | fi (face index)
293 | and all custom face attributes already defined by user.
294 |
295 | Args:
296 | script: the FilterScript object or script filename to write
297 | the filter] to.
298 | name (str): the name of new attribute. You can access attribute in
299 | other filters through this name.
300 | function (str): function to calculate custom attribute value for each
301 | face
302 |
303 | Layer stack:
304 | No impacts
305 |
306 | MeshLab versions:
307 | 2016.12
308 | 1.3.4BETA
309 | """
310 | filter_xml = ''.join([
311 | ' \n',
312 | ' \n',
317 | ' \n',
322 | ' \n'])
323 | util.write_filter(script, filter_xml)
324 | return None
325 |
326 |
327 | def vq_function(script, function='vi', normalize=False, color=False):
328 | """:Quality function using muparser to generate new Quality for every vertex
It's possibile to use the following per-vertex variables in the expression:
x, y, z, nx, ny, nz (normal), r, g, b (color), q (quality), rad, vi,
and all custom vertex attributes already defined by user.
329 |
330 |
331 | function function to generate new Quality for every vertex
332 | normalize if checked normalize all quality values in range [0..1]
333 | color if checked map quality generated values into per-vertex color
334 |
335 | """
336 | filter_xml = ''.join([
337 | ' \n',
338 | ' \n',
343 | ' \n',
348 | ' \n',
353 | ' \n'])
354 | util.write_filter(script, filter_xml)
355 | return None
356 |
357 |
358 | def fq_function(script, function='x0+y0+z0', normalize=False, color=False):
359 | """ :Quality function using muparser to generate new Quality for every face
Insert three function each one for quality of the three vertex of a face
It's possibile to use per-face variables like attributes associated to the three vertex of every face.
x0,y0,z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex.
and also nx0,ny0,nz0 nx1,ny1,nz1 etc. for normals and r0,g0,b0 for color,q0,q1,q2 for quality.
360 |
361 | function function to generate new Quality for each face
362 | normalize if checked normalize all quality values in range [0..1]
363 | color if checked map quality generated values into per-vertex color
364 |
365 |
366 | """
367 | filter_xml = ''.join([
368 | ' \n',
369 | ' \n',
374 | ' \n',
379 | ' \n',
384 | ' \n'])
385 | util.write_filter(script, filter_xml)
386 | return None
387 |
--------------------------------------------------------------------------------
/meshlabxml/normals.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML functions for mesh normals """
2 |
3 | from . import util
4 |
5 | def reorient(script):
6 | """ Re-orient in a consistent way all the faces of the mesh.
7 |
8 | The filter visits a mesh face to face, reorienting any unvisited face so
9 | that it is coherent to the already visited faces. If the surface is
10 | orientable it will end with a consistent orientation of all the faces. If
11 | the surface is not orientable (e.g. it is non manifold or non orientable
12 | like a moebius strip) the filter will not build a consistent orientation
13 | simply because it is not possible. The filter can end up in a consistent
14 | orientation that can be exactly the opposite of the expected one; in that
15 | case simply invert the whole mesh orientation.
16 |
17 | Args:
18 | script: the FilterScript object or script filename to write
19 | the filter to.
20 |
21 | Layer stack:
22 | No impacts
23 |
24 | MeshLab versions:
25 | 2016.12
26 | 1.3.4BETA
27 | """
28 | filter_xml = ' \n'
29 | util.write_filter(script, filter_xml)
30 | return None
31 |
32 |
33 | def flip(script, force_flip=False, selected=False):
34 | """ Invert faces orientation, flipping the normals of the mesh.
35 |
36 | If requested, it tries to guess the right orientation; mainly it decides to
37 | flip all the faces if the minimum/maximum vertexes have not outward point
38 | normals for a few directions. Works well for single component watertight
39 | objects.
40 |
41 | Args:
42 | script: the FilterScript object or script filename to write
43 | the filter to.
44 | force_flip (bool): If selected, the normals will always be flipped;
45 | otherwise, the filter tries to set them outside.
46 | selected (bool): If selected, only selected faces will be affected.
47 |
48 | Layer stack:
49 | No impacts
50 |
51 | MeshLab versions:
52 | 2016.12
53 | 1.3.4BETA
54 | """
55 | filter_xml = ''.join([
56 | ' \n',
57 | ' \n',
62 | ' \n',
67 | ' \n'])
68 | util.write_filter(script, filter_xml)
69 | return None
70 |
71 |
72 | def fix(script):
73 | """ Will reorient normals & ensure they are oriented outwards
74 |
75 | Layer stack:
76 | No impacts
77 |
78 | MeshLab versions:
79 | 2016.12
80 | 1.3.4BETA
81 | """
82 | reorient(script)
83 | flip(script)
84 | return
85 |
86 |
87 | def point_sets(script, neighbors=10, smooth_iteration=0, flip=False,
88 | viewpoint_pos=(0.0, 0.0, 0.0)):
89 | """ Compute the normals of the vertices of a mesh without exploiting the
90 | triangle connectivity, useful for dataset with no faces.
91 |
92 | Args:
93 | script: the FilterScript object or script filename to write
94 | the filter to.
95 | neighbors (int): The number of neighbors used to estimate normals.
96 | smooth_iteration (int): The number of smoothing iteration done on the
97 | p used to estimate and propagate normals.
98 | flip (bool): Flip normals w.r.t. viewpoint. If the 'viewpoint' (i.e.
99 | scanner position) is known, it can be used to disambiguate normals
100 | orientation, so that all the normals will be oriented in the same
101 | direction.
102 | viewpoint_pos (single xyz point, tuple or list): Set the x, y, z
103 | coordinates of the viewpoint position.
104 |
105 | Layer stack:
106 | No impacts
107 |
108 | MeshLab versions:
109 | 2016.12
110 | 1.3.4BETA
111 | """
112 | filter_xml = ''.join([
113 | ' \n',
114 | ' \n',
119 | ' \n',
124 | ' \n',
129 | ' \n',
135 | ' \n'])
136 | util.write_filter(script, filter_xml)
137 | return None
138 |
--------------------------------------------------------------------------------
/meshlabxml/sampling.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML sampling functions """
2 |
3 | from . import FilterScript
4 | from . import util
5 |
6 |
7 | def hausdorff_distance(script, sampled_layer=1, target_layer=0,
8 | save_sample=False, sample_vert=True, sample_edge=True,
9 | sample_faux_edge=False, sample_face=True,
10 | sample_num=1000, maxdist=10):
11 | """ Compute the Hausdorff Distance between two meshes, sampling one of the
12 | two and finding for each sample the closest point over the other mesh.
13 |
14 | Args:
15 | script: the FilterScript object or script filename to write
16 | the filter to.
17 | sampled_layer (int): The mesh layer whose surface is sampled. For each
18 | sample we search the closest point on the target mesh layer.
19 | target_layer (int): The mesh that is sampled for the comparison.
20 | save_sample (bool): Save the position and distance of all the used
21 | samples on both the two surfaces, creating two new layers with two
22 | point clouds representing the used samples.
23 | sample_vert (bool): For the search of maxima it is useful to sample
24 | vertices and edges of the mesh with a greater care. It is quite
25 | probable that the farthest points falls along edges or on mesh
26 | vertexes, and with uniform montecarlo sampling approaches the
27 | probability of taking a sample over a vertex or an edge is
28 | theoretically null. On the other hand this kind of sampling could
29 | make the overall sampling distribution slightly biased and slightly
30 | affects the cumulative results.
31 | sample_edge (bool): see sample_vert
32 | sample_faux_edge (bool): see sample_vert
33 | sample_face (bool): see sample_vert
34 | sample_num (int): The desired number of samples. It can be smaller or
35 | larger than the mesh size, and according to the chosen sampling
36 | strategy it will try to adapt.
37 | maxdist (int): Sample points for which we do not find anything within
38 | this distance are rejected and not considered neither for averaging
39 | nor for max.
40 |
41 | Layer stack:
42 | If save_sample is True, two new layers are created: 'Hausdorff Closest
43 | Points' and 'Hausdorff Sample Point'; and the current layer is
44 | changed to the last newly created layer.
45 | If save_sample is False, no impacts
46 |
47 | MeshLab versions:
48 | 2016.12
49 | 1.3.4BETA
50 | """
51 | # MeshLab defaults:
52 | # sample_num = number of vertices
53 | # maxdist = 0.05 * AABB['diag'] #5% of AABB[diag]
54 | # maxdist_max = AABB['diag']
55 | maxdist_max = 2*maxdist
56 | # TODO: parse output (min, max, mean, etc.)
57 | filter_xml = ''.join([
58 | ' \n',
59 | ' \n',
64 | ' \n',
69 | ' \n',
74 | ' \n',
79 | ' \n',
84 | ' \n',
89 | ' \n',
95 | ' \n',
100 | ' \n',
108 | ' \n'])
109 | util.write_filter(script, filter_xml)
110 | if isinstance(script, FilterScript):
111 | script.parse_hausdorff = True
112 | if isinstance(script, FilterScript) and save_sample:
113 | script.add_layer('Hausdorff Closest Points')
114 | script.add_layer('Hausdorff Sample Point')
115 | return None
116 |
117 |
118 | def poisson_disk(script, sample_num=1000, radius=0.0,
119 | montecarlo_rate=20, save_montecarlo=False,
120 | approx_geodesic_dist=False, subsample=False, refine=False,
121 | refine_layer=0, best_sample=True, best_sample_pool=10,
122 | exact_num=False, radius_variance=1.0):
123 | """ Create a new layer populated with a point sampling of the current mesh.
124 |
125 | Samples are generated according to a Poisson-disk distribution, using the
126 | algorithm described in:
127 |
128 | 'Efficient and Flexible Sampling with Blue Noise Properties of Triangular Meshes'
129 | Massimiliano Corsini, Paolo Cignoni, Roberto Scopigno
130 | IEEE TVCG 2012
131 |
132 | Args:
133 | script: the FilterScript object or script filename to write
134 | the filter to.
135 | sample_num (int): The desired number of samples. The radius of the disk
136 | is calculated according to the sampling density.
137 | radius (float): If not zero this parameter overrides the previous
138 | parameter to allow exact radius specification.
139 | montecarlo_rate (int): The over-sampling rate that is used to generate
140 | the intial Monte Carlo samples (e.g. if this parameter is 'K' means
141 | that 'K * sample_num' points will be used). The generated
142 | Poisson-disk samples are a subset of these initial Monte Carlo
143 | samples. Larger numbers slow the process but make it a bit more
144 | accurate.
145 | save_montecarlo (bool): If True, it will generate an additional Layer
146 | with the Monte Carlo sampling that was pruned to build the Poisson
147 | distribution.
148 | approx_geodesic_dist (bool): If True Poisson-disk distances are
149 | computed using an approximate geodesic distance, e.g. an Euclidean
150 | distance weighted by a function of the difference between the
151 | normals of the two points.
152 | subsample (bool): If True the original vertices of the base mesh are
153 | used as base set of points. In this case the sample_num should be
154 | obviously much smaller than the original vertex number. Note that
155 | this option is very useful in the case you want to subsample a
156 | dense point cloud.
157 | refine (bool): If True the vertices of the refine_layer mesh layer are
158 | used as starting vertices, and they will be utterly refined by
159 | adding more and more points until possible.
160 | refine_layer (int): Used only if refine is True.
161 | best_sample (bool): If True it will use a simple heuristic for choosing
162 | the samples. At a small cost (it can slow the process a bit) it
163 | usually improves the maximality of the generated sampling.
164 | best_sample_pool (bool): Used only if best_sample is True. It controls
165 | the number of attempts that it makes to get the best sample. It is
166 | reasonable that it is smaller than the Monte Carlo oversampling
167 | factor.
168 | exact_num (bool): If True it will try to do a dicotomic search for the
169 | best Poisson-disk radius that will generate the requested number of
170 | samples with a tolerance of the 0.5%. Obviously it takes much
171 | longer.
172 | radius_variance (float): The radius of the disk is allowed to vary
173 | between r and r*var. If this parameter is 1 the sampling is the
174 | same as the Poisson-disk Sampling.
175 |
176 | Layer stack:
177 | Creates new layer 'Poisson-disk Samples'. Current layer is NOT changed
178 | to the new layer (see Bugs).
179 | If save_montecarlo is True, creates a new layer 'Montecarlo Samples'.
180 | Current layer is NOT changed to the new layer (see Bugs).
181 |
182 | MeshLab versions:
183 | 2016.12
184 | 1.3.4BETA
185 |
186 | Bugs:
187 | Current layer is NOT changed to the new layer, which is inconsistent
188 | with the majority of filters that create new layers.
189 | """
190 | filter_xml = ''.join([
191 | ' \n',
192 | ' \n',
197 | ' \n',
204 | ' \n',
209 | ' \n',
214 | ' \n',
219 | ' \n',
224 | ' \n',
229 | ' \n',
234 | ' \n',
239 | ' \n',
244 | ' \n',
249 | ' \n',
254 | ' \n'])
255 | util.write_filter(script, filter_xml)
256 | if isinstance(script, FilterScript):
257 | script.add_layer('Poisson-disk Samples')
258 | if save_montecarlo:
259 | script.add_layer('Montecarlo Samples')
260 | return None
261 |
262 |
263 | def mesh_element(script, sample_num=1000, element='VERT'):
264 | """ Create a new layer populated with a point sampling of the current mesh,
265 | at most one sample for each element of the mesh is created.
266 |
267 | Samples are taking in a uniform way, one for each element
268 | (vertex/edge/face); all the elements have the same probabilty of being
269 | choosen.
270 |
271 | Args:
272 | script: the FilterScript object or script filename to write
273 | the filter to.
274 | sample_num (int): The desired number of elements that must be chosen.
275 | Being a subsampling of the original elements if this number should
276 | not be larger than the number of elements of the original mesh.
277 | element (enum in ['VERT', 'EDGE', 'FACE']): Choose what mesh element
278 | will be used for the subsampling. At most one point sample will
279 | be added for each one of the chosen elements
280 |
281 | Layer stack:
282 | Creates new layer 'Sampled Mesh'. Current layer is changed to the new
283 | layer.
284 |
285 | MeshLab versions:
286 | 2016.12
287 | 1.3.4BETA
288 | """
289 | if element.lower() == 'vert':
290 | element_num = 0
291 | elif element.lower() == 'edge':
292 | element_num = 1
293 | elif element.lower() == 'face':
294 | element_num = 2
295 | filter_xml = ''.join([
296 | ' \n',
297 | ' \n',
306 | ' \n',
311 | ' \n'])
312 | util.write_filter(script, filter_xml)
313 | if isinstance(script, FilterScript):
314 | script.add_layer('Sampled Mesh')
315 | return None
316 |
317 |
318 | def clustered_vert(script, cell_size=1.0, strategy='AVERAGE', selected=False):
319 | """ "Create a new layer populated with a subsampling of the vertexes of the
320 | current mesh
321 |
322 | The subsampling is driven by a simple one-per-gridded cell strategy.
323 |
324 | Args:
325 | script: the FilterScript object or script filename to write
326 | the filter to.
327 | cell_size (float): The size of the cell of the clustering grid. Smaller the cell finer the resulting mesh. For obtaining a very coarse mesh use larger values.
328 | strategy (enum 'AVERAGE' or 'CENTER'): <b>Average</b>: for each cell we take the average of the sample falling into. The resulting point is a new point.<br><b>Closest to center</b>: for each cell we take the sample that is closest to the center of the cell. Choosen vertices are a subset of the original ones.
329 | selected (bool): If true only for the filter is applied only on the selected subset of the mesh.
330 |
331 | Layer stack:
332 | Creates new layer 'Cluster Samples'. Current layer is changed to the new
333 | layer.
334 |
335 | MeshLab versions:
336 | 2016.12
337 | 1.3.4BETA
338 | """
339 | if strategy.lower() == 'average':
340 | strategy_num = 0
341 | elif strategy.lower() == 'center':
342 | strategy_num = 1
343 |
344 | filter_xml = ''.join([
345 | ' \n',
346 | ' \n',
353 | ' \n',
361 | ' \n',
366 | ' \n'])
367 | util.write_filter(script, filter_xml)
368 | if isinstance(script, FilterScript):
369 | script.add_layer('Cluster Samples')
370 | return None
371 |
--------------------------------------------------------------------------------
/meshlabxml/select.py:
--------------------------------------------------------------------------------
1 | """MeshLabXML selection functions"""
2 |
3 | from . import util
4 |
5 |
6 | def all(script, face=True, vert=True):
7 | """ Select all the faces of the current mesh
8 |
9 | Args:
10 | script: the FilterScript object or script filename to write
11 | the filter to.
12 | faces (bool): If True the filter will select all the faces.
13 | verts (bool): If True the filter will select all the vertices.
14 |
15 | Layer stack:
16 | No impacts
17 |
18 | MeshLab versions:
19 | 2016.12
20 | 1.3.4BETA
21 | """
22 | filter_xml = ''.join([
23 | ' \n',
24 | ' \n',
29 | ' \n',
34 | ' \n'])
35 | util.write_filter(script, filter_xml)
36 | return None
37 |
38 |
39 | def none(script, face=True, vert=True):
40 | """ Clear the current set of selected faces
41 |
42 | Args:
43 | script: the FilterScript object or script filename to write
44 | the filter to.
45 | faces (bool): If True the filter will deselect all the faces.
46 | verts (bool): If True the filter will deselect all the vertices.
47 |
48 | Layer stack:
49 | No impacts
50 |
51 | MeshLab versions:
52 | 2016.12
53 | 1.3.4BETA
54 | """
55 | filter_xml = ''.join([
56 | ' \n',
57 | ' \n',
62 | ' \n',
67 | ' \n'])
68 | util.write_filter(script, filter_xml)
69 | return None
70 |
71 |
72 | def invert(script, face=True, vert=True):
73 | """ Invert the current set of selected faces
74 |
75 | Args:
76 | script: the FilterScript object or script filename to write
77 | the filter to.
78 | faces (bool): If True the filter will invert the selected faces.
79 | verts (bool): If True the filter will invert the selected vertices.
80 |
81 | Layer stack:
82 | No impacts
83 |
84 | MeshLab versions:
85 | 2016.12
86 | 1.3.4BETA
87 | """
88 | filter_xml = ''.join([
89 | ' \n',
90 | ' \n',
95 | ' \n',
100 | ' \n'])
101 | util.write_filter(script, filter_xml)
102 | return None
103 |
104 |
105 | def border(script):
106 | """ Select vertices and faces on the boundary
107 |
108 | Args:
109 | script: the FilterScript object or script filename to write
110 | the filter to.
111 |
112 | Layer stack:
113 | No impacts
114 |
115 | MeshLab versions:
116 | 2016.12
117 | 1.3.4BETA
118 | """
119 | filter_xml = ' \n'
120 | util.write_filter(script, filter_xml)
121 | return None
122 |
123 |
124 | def grow(script, iterations=1):
125 | """ Grow (dilate, expand) the current set of selected faces
126 |
127 | Args:
128 | script: the FilterScript object or script filename to write
129 | the filter to.
130 | iterations (int): the number of times to grow the selection.
131 |
132 | Layer stack:
133 | No impacts
134 |
135 | MeshLab versions:
136 | 2016.12
137 | 1.3.4BETA
138 | """
139 | filter_xml = ' \n'
140 | for _ in range(iterations):
141 | util.write_filter(script, filter_xml)
142 | return None
143 |
144 |
145 | def shrink(script, iterations=1):
146 | """ Shrink (erode, reduce) the current set of selected faces
147 |
148 | Args:
149 | script: the FilterScript object or script filename to write
150 | the filter to.
151 | iterations (int): the number of times to shrink the selection.
152 |
153 | Layer stack:
154 | No impacts
155 |
156 | MeshLab versions:
157 | 2016.12
158 | 1.3.4BETA
159 | """
160 | filter_xml = ' \n'
161 | for _ in range(iterations):
162 | util.write_filter(script, filter_xml)
163 | return None
164 |
165 |
166 | def self_intersecting_face(script):
167 | """ Select only self intersecting faces
168 |
169 | Args:
170 | script: the FilterScript object or script filename to write
171 | the filter to.
172 |
173 | Layer stack:
174 | No impacts
175 |
176 | MeshLab versions:
177 | 2016.12
178 | 1.3.4BETA
179 | """
180 | filter_xml = ' \n'
181 | util.write_filter(script, filter_xml)
182 | return None
183 |
184 |
185 | def nonmanifold_vert(script):
186 | """ Select the non manifold vertices that do not belong to non manifold
187 | edges.
188 |
189 | For example two cones connected by their apex. Vertices incident on
190 | non manifold edges are ignored.
191 |
192 | Args:
193 | script: the FilterScript object or script filename to write
194 | the filter to.
195 |
196 | Layer stack:
197 | No impacts
198 |
199 | MeshLab versions:
200 | 2016.12
201 | 1.3.4BETA
202 | """
203 | filter_xml = ' \n'
204 | util.write_filter(script, filter_xml)
205 | return None
206 |
207 |
208 | def nonmanifold_edge(script):
209 | """ Select the faces and the vertices incident on non manifold edges (e.g.
210 | edges where more than two faces are incident).
211 |
212 | Note that this function selects the components that are related to
213 | non manifold edges. The case of non manifold vertices is specifically
214 | managed by nonmanifold_vert.
215 |
216 | Args:
217 | script: the FilterScript object or script filename to write
218 | the filter to.
219 |
220 | Layer stack:
221 | No impacts
222 |
223 | MeshLab versions:
224 | 2016.12
225 | 1.3.4BETA
226 | """
227 | filter_xml = ' \n'
228 | util.write_filter(script, filter_xml)
229 | return None
230 |
231 |
232 | def small_parts(script, ratio=0.2, non_closed_only=False):
233 | """ Select the small disconnected parts (components) of a mesh.
234 |
235 | Args:
236 | script: the FilterScript object or script filename to write
237 | the filter to.
238 | ratio (float): This ratio (between 0 and 1) defines the meaning of
239 | 'small' as the threshold ratio between the number of faces of the
240 | largest component and the other ones. A larger value will select
241 | more components.
242 | non_closed_only (bool): Select only non-closed components.
243 |
244 | Layer stack:
245 | No impacts
246 |
247 | MeshLab versions:
248 | 2016.12
249 | 1.3.4BETA
250 | """
251 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12':
252 | filter_name = 'Small component selection'
253 | else:
254 | filter_name = 'Select small disconnected component'
255 | filter_xml = ''.join([
256 | ' \n'.format(filter_name),
257 | ' \n',
262 | ' \n',
267 | ' \n'])
268 | util.write_filter(script, filter_xml)
269 | return None
270 |
271 |
272 | def vert_quality(script, min_quality=0.0, max_quality=0.05, inclusive=True):
273 | """ Select all the faces and vertexes within the specified vertex quality
274 | range.
275 |
276 | Args:
277 | script: the FilterScript object or script filename to write
278 | the filter] to.
279 | min_quality (float): Minimum acceptable quality value.
280 | max_quality (float): Maximum acceptable quality value.
281 | inclusive (bool): If True only the faces with ALL the vertices within
282 | the specified range are selected. Otherwise any face with at least
283 | one vertex within the range is selected.
284 |
285 | Layer stack:
286 | No impacts
287 |
288 | MeshLab versions:
289 | 2016.12
290 | 1.3.4BETA
291 | """
292 | filter_xml = ''.join([
293 | ' \n',
294 | ' \n',
301 | ' \n',
308 | ' \n',
313 | ' \n'])
314 | util.write_filter(script, filter_xml)
315 | return None
316 |
317 |
318 | def face_function(script, function='(fi == 0)'):
319 | """Boolean function using muparser lib to perform face selection over
320 | current mesh.
321 |
322 | See help(mlx.muparser_ref) for muparser reference documentation.
323 |
324 | It's possible to use parenthesis, per-vertex variables and boolean operator:
325 | (, ), and, or, <, >, =
326 | It's possible to use per-face variables like attributes associated to the three
327 | vertices of every face.
328 |
329 | Variables (per face):
330 | x0, y0, z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex
331 | nx0, ny0, nz0, nx1, ny1, nz1, etc. for vertex normals
332 | r0, g0, b0, a0, etc. for vertex color
333 | q0, q1, q2 for quality
334 | wtu0, wtv0, wtu1, wtv1, wtu2, wtv2 (per wedge texture coordinates)
335 | ti for face texture index (>= ML2016.12)
336 | vsel0, vsel1, vsel2 for vertex selection (1 yes, 0 no) (>= ML2016.12)
337 | fr, fg, fb, fa for face color (>= ML2016.12)
338 | fq for face quality (>= ML2016.12)
339 | fnx, fny, fnz for face normal (>= ML2016.12)
340 | fsel face selection (1 yes, 0 no) (>= ML2016.12)
341 |
342 | Args:
343 | script: the FilterScript object or script filename to write
344 | the filter] to.
345 | function (str): a boolean function that will be evaluated in order
346 | to select a subset of faces.
347 |
348 | Layer stack:
349 | No impacts
350 |
351 | MeshLab versions:
352 | 2016.12
353 | 1.3.4BETA
354 | """
355 | filter_xml = ''.join([
356 | ' \n',
357 | ' \n',
362 | ' \n'])
363 | util.write_filter(script, filter_xml)
364 | return None
365 |
366 |
367 | def vert_function(script, function='(q < 0)', strict_face_select=True):
368 | """Boolean function using muparser lib to perform vertex selection over current mesh.
369 |
370 | See help(mlx.muparser_ref) for muparser reference documentation.
371 |
372 | It's possible to use parenthesis, per-vertex variables and boolean operator:
373 | (, ), and, or, <, >, =
374 | It's possible to use the following per-vertex variables in the expression:
375 |
376 | Variables:
377 | x, y, z (coordinates)
378 | nx, ny, nz (normal)
379 | r, g, b, a (color)
380 | q (quality)
381 | rad
382 | vi (vertex index)
383 | vtu, vtv (texture coordinates)
384 | ti (texture index)
385 | vsel (is the vertex selected? 1 yes, 0 no)
386 | and all custom vertex attributes already defined by user.
387 |
388 | Args:
389 | script: the FilterScript object or script filename to write
390 | the filter] to.
391 | function (str): a boolean function that will be evaluated in order
392 | to select a subset of vertices. Example: (y > 0) and (ny > 0)
393 | strict_face_select (bool): if True a face is selected if ALL its
394 | vertices are selected. If False a face is selected if at least
395 | one of its vertices is selected. ML v1.3.4BETA only; this is
396 | ignored in 2016.12. In 2016.12 only vertices are selected.
397 |
398 | Layer stack:
399 | No impacts
400 |
401 | MeshLab versions:
402 | 2016.12
403 | 1.3.4BETA
404 | """
405 | if script.ml_version == '1.3.4BETA':
406 | strict_select = ''.join([
407 | ' \n',
412 | ])
413 | else:
414 | strict_select = ''
415 |
416 | filter_xml = ''.join([
417 | ' \n',
418 | ' \n',
423 | strict_select,
424 | ' \n'])
425 | util.write_filter(script, filter_xml)
426 | return None
427 |
428 |
429 | def cylindrical_vert(script, radius=1.0, inside=True):
430 | """Select all vertices within a cylindrical radius
431 |
432 | Args:
433 | radius (float): radius of the sphere
434 | center_pt (3 coordinate tuple or list): center point of the sphere
435 |
436 | Layer stack:
437 | No impacts
438 |
439 | MeshLab versions:
440 | 2016.12
441 | 1.3.4BETA
442 | """
443 | if inside:
444 | function = 'sqrt(x^2+y^2)<={}'.format(radius)
445 | else:
446 | function = 'sqrt(x^2+y^2)>={}'.format(radius)
447 | vert_function(script, function=function)
448 | return None
449 |
450 |
451 | def spherical_vert(script, radius=1.0, center_pt=(0.0, 0.0, 0.0)):
452 | """Select all vertices within a spherical radius
453 |
454 | Args:
455 | radius (float): radius of the sphere
456 | center_pt (3 coordinate tuple or list): center point of the sphere
457 |
458 | Layer stack:
459 | No impacts
460 |
461 | MeshLab versions:
462 | 2016.12
463 | 1.3.4BETA
464 | """
465 | function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}'.format(
466 | center_pt[0], center_pt[1], center_pt[2], radius)
467 | vert_function(script, function=function)
468 | return None
469 |
--------------------------------------------------------------------------------
/meshlabxml/smooth.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML smoothing functions """
2 |
3 | from . import util
4 |
5 | def laplacian(script, iterations=1, boundary=True, cotangent_weight=True,
6 | selected=False):
7 | """ Laplacian smooth of the mesh: for each vertex it calculates the average
8 | position with nearest vertex
9 |
10 | Args:
11 | script: the FilterScript object or script filename to write
12 | the filter to.
13 | iterations (int): The number of times that the whole algorithm (normal
14 | smoothing + vertex fitting) is iterated.
15 | boundary (bool): If true the boundary edges are smoothed only by
16 | themselves (e.g. the polyline forming the boundary of the mesh is
17 | independently smoothed). Can reduce the shrinking on the border but
18 | can have strange effects on very small boundaries.
19 | cotangent_weight (bool): If True the cotangent weighting scheme is
20 | computed for the averaging of the position. Otherwise (False) the
21 | simpler umbrella scheme (1 if the edge is present) is used.
22 | selected (bool): If selected the filter is performed only on the
23 | selected faces
24 |
25 | Layer stack:
26 | No impacts
27 |
28 | MeshLab versions:
29 | 2016.12
30 | 1.3.4BETA
31 | """
32 | filter_xml = ''.join([
33 | ' \n',
34 | ' \n',
39 | ' \n',
44 | ' \n',
49 | ' \n',
54 | ' \n'])
55 | util.write_filter(script, filter_xml)
56 | return None
57 |
58 |
59 | def hc_laplacian(script):
60 | """ HC Laplacian Smoothing, extended version of Laplacian Smoothing, based
61 | on the paper of Vollmer, Mencl, and Muller
62 |
63 | Layer stack:
64 | No impacts
65 |
66 | MeshLab versions:
67 | 2016.12
68 | 1.3.4BETA
69 | """
70 | filter_xml = ' \n'
71 | util.write_filter(script, filter_xml)
72 | return None
73 |
74 |
75 | def taubin(script, iterations=10, t_lambda=0.5, t_mu=-0.53, selected=False):
76 | """ The lambda & mu Taubin smoothing, it make two steps of smoothing, forth
77 | and back, for each iteration.
78 |
79 | Based on:
80 | Gabriel Taubin
81 | "A signal processing approach to fair surface design"
82 | Siggraph 1995
83 |
84 | Args:
85 | script: the FilterScript object or script filename to write
86 | the filter to.
87 | iterations (int): The number of times that the taubin smoothing is
88 | iterated. Usually it requires a larger number of iteration than the
89 | classical laplacian.
90 | t_lambda (float): The lambda parameter of the Taubin Smoothing algorithm
91 | t_mu (float): The mu parameter of the Taubin Smoothing algorithm
92 | selected (bool): If selected the filter is performed only on the
93 | selected faces
94 |
95 | Layer stack:
96 | No impacts
97 |
98 | MeshLab versions:
99 | 2016.12
100 | 1.3.4BETA
101 | """
102 | filter_xml = ''.join([
103 | ' \n',
104 | ' \n',
109 | ' \n',
114 | ' \n',
119 | ' \n',
124 | ' \n'])
125 | util.write_filter(script, filter_xml)
126 | return None
127 |
128 |
129 | def twostep(script, iterations=3, angle_threshold=60, normal_steps=20, fit_steps=20,
130 | selected=False):
131 | """ Two Step Smoothing, a feature preserving/enhancing fairing filter.
132 |
133 | It is based on a Normal Smoothing step where similar normals are averaged
134 | together and a step where the vertexes are fitted on the new normals.
135 |
136 | Based on:
137 | A. Belyaev and Y. Ohtake,
138 | "A Comparison of Mesh Smoothing Methods"
139 | Proc. Israel-Korea Bi-National Conf. Geometric Modeling and Computer
140 | Graphics, pp. 83-87, 2003.
141 |
142 | Args:
143 | script: the FilterScript object or script filename to write
144 | the filter to.
145 | iterations (int): The number of times that the whole algorithm (normal
146 | smoothing + vertex fitting) is iterated.
147 | angle_threshold (float): Specify a threshold angle (0..90) for features
148 | that you want to be preserved. Features forming angles LARGER than
149 | the specified threshold will be preserved.
150 | 0 -> no smoothing
151 | 90 -> all faces will be smoothed
152 | normal_steps (int): Number of iterations of normal smoothing step. The
153 | larger the better and (the slower)
154 | fit_steps (int): Number of iterations of the vertex fitting procedure
155 | selected (bool): If selected the filter is performed only on the
156 | selected faces
157 |
158 | Layer stack:
159 | No impacts
160 |
161 | MeshLab versions:
162 | 2016.12
163 | 1.3.4BETA
164 | """
165 | filter_xml = ''.join([
166 | ' \n',
167 | ' \n',
172 | ' \n',
177 | ' \n',
182 | ' \n',
187 | ' \n',
192 | ' \n'])
193 | util.write_filter(script, filter_xml)
194 | return None
195 |
196 |
197 | def depth(script, iterations=3, viewpoint=(0, 0, 0), selected=False):
198 | """ A laplacian smooth that is constrained to move vertices only along the
199 | view direction.
200 |
201 | Args:
202 | script: the FilterScript object or script filename to write
203 | the filter to.
204 | iterations (int): The number of times that the whole algorithm (normal
205 | smoothing + vertex fitting) is iterated.
206 | viewpoint (vector tuple or list): The position of the view point that
207 | is used to get the constraint direction.
208 | selected (bool): If selected the filter is performed only on the
209 | selected faces
210 |
211 | Layer stack:
212 | No impacts
213 |
214 | MeshLab versions:
215 | 2016.12
216 | 1.3.4BETA
217 | """
218 | filter_xml = ''.join([
219 | ' \n',
220 | ' \n',
225 | ' \n',
232 | ' \n',
237 | ' \n'])
238 | util.write_filter(script, filter_xml)
239 | return None
240 |
--------------------------------------------------------------------------------
/meshlabxml/subdivide.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML subdivide functions """
2 |
3 | from . import util
4 |
5 | def loop(script, iterations=1, loop_weight=0, edge_threshold=0,
6 | selected=False):
7 | """ Apply Loop's Subdivision Surface algorithm.
8 |
9 | It is an approximant subdivision method and it works for every triangle
10 | and has rules for extraordinary vertices.
11 |
12 | Args:
13 | script: the FilterScript object or script filename to write
14 | the filter to.
15 | iterations (int): Number of times the model is subdivided.
16 | loop_weight (int): Change the weights used. Allow to optimize some
17 | behaviours in spite of others. Valid values are:
18 | 0 - Loop (default)
19 | 1 - Enhance regularity
20 | 2 - Enhance continuity
21 | edge_threshold (float): All the edges longer than this threshold will
22 | be refined. Setting this value to zero will force a uniform
23 | refinement.
24 | selected (bool): If selected the filter is performed only on the
25 | selected faces.
26 |
27 | Layer stack:
28 | No impacts
29 |
30 | MeshLab versions:
31 | 2016.12
32 | 1.3.4BETA
33 | """
34 | filter_xml = ''.join([
35 | ' \n',
36 | ' \n',
45 | ' \n',
50 | ' \n',
57 | ' \n',
62 | ' \n'])
63 | util.write_filter(script, filter_xml)
64 | return None
65 |
66 |
67 | def ls3loop(script, iterations=1, loop_weight=0, edge_threshold=0,
68 | selected=False):
69 | """ Apply LS3 Subdivision Surface algorithm using Loop's weights.
70 |
71 | This refinement method take normals into account.
72 | See: Boye', S. Guennebaud, G. & Schlick, C.
73 | "Least squares subdivision surfaces"
74 | Computer Graphics Forum, 2010.
75 |
76 | Alternatives weighting schemes are based on the paper:
77 | Barthe, L. & Kobbelt, L.
78 | "Subdivision scheme tuning around extraordinary vertices"
79 | Computer Aided Geometric Design, 2004, 21, 561-583.
80 |
81 | The current implementation of these schemes don't handle vertices of
82 | valence > 12
83 |
84 | Args:
85 | script: the FilterScript object or script filename to write
86 | the filter to.
87 | iterations (int): Number of times the model is subdivided.
88 | loop_weight (int): Change the weights used. Allow to optimize some
89 | behaviours in spite of others. Valid values are:
90 | 0 - Loop (default)
91 | 1 - Enhance regularity
92 | 2 - Enhance continuity
93 | edge_threshold (float): All the edges longer than this threshold will
94 | be refined. Setting this value to zero will force a uniform
95 | refinement.
96 | selected (bool): If selected the filter is performed only on the
97 | selected faces.
98 |
99 | Layer stack:
100 | No impacts
101 |
102 | MeshLab versions:
103 | 2016.12
104 | 1.3.4BETA
105 | """
106 | filter_xml = ''.join([
107 | ' \n',
108 | ' \n',
117 | ' \n',
122 | ' \n',
129 | ' \n',
134 | ' \n'])
135 | util.write_filter(script, filter_xml)
136 | return None
137 |
138 |
139 | def midpoint(script, iterations=1, edge_threshold=0, selected=False):
140 | """ Apply a plain subdivision scheme where every edge is split on its
141 | midpoint.
142 |
143 | Useful to uniformly refine a mesh substituting each triangle with four
144 | smaller triangles.
145 |
146 | Args:
147 | script: the FilterScript object or script filename to write
148 | the filter to.
149 | iterations (int): Number of times the model is subdivided.
150 | edge_threshold (float): All the edges longer than this threshold will
151 | be refined. Setting this value to zero will force a uniform
152 | refinement.
153 | selected (bool): If selected the filter is performed only on the
154 | selected faces.
155 |
156 | Layer stack:
157 | No impacts
158 |
159 | MeshLab versions:
160 | 2016.12
161 | 1.3.4BETA
162 | """
163 | filter_xml = ''.join([
164 | ' \n',
165 | ' \n',
170 | ' \n',
177 | ' \n',
182 | ' \n'])
183 | util.write_filter(script, filter_xml)
184 | return None
185 |
186 |
187 | def butterfly(script, iterations=1, edge_threshold=0, selected=False):
188 | """ Apply Butterfly Subdivision Surface algorithm.
189 |
190 | It is an interpolated method, defined on arbitrary triangular meshes.
191 | The scheme is known to be C1 but not C2 on regular meshes.
192 |
193 | Args:
194 | script: the FilterScript object or script filename to write
195 | the filter to.
196 | iterations (int): Number of times the model is subdivided.
197 | edge_threshold (float): All the edges longer than this threshold will
198 | be refined. Setting this value to zero will force a uniform
199 | refinement.
200 | selected (bool): If selected the filter is performed only on the
201 | selected faces.
202 |
203 | Layer stack:
204 | No impacts
205 |
206 | MeshLab versions:
207 | 2016.12
208 | 1.3.4BETA
209 | """
210 | filter_xml = ''.join([
211 | ' \n',
212 | ' \n',
217 | ' \n',
224 | ' \n',
229 | ' \n'])
230 | util.write_filter(script, filter_xml)
231 | return None
232 |
233 |
234 | def catmull_clark(script):
235 | """ Apply the Catmull-Clark Subdivision Surfaces.
236 |
237 | Note that position of the new vertices is simply linearly interpolated.
238 | If the mesh is triangle based (no faux edges) it generates a quad mesh,
239 | otherwise it honors the faux-edge bits.
240 |
241 | Args:
242 | script: the FilterScript object or script filename to write
243 | the filter to.
244 |
245 | Layer stack:
246 | No impacts
247 |
248 | MeshLab versions:
249 | 2016.12
250 | 1.3.4BETA
251 | """
252 | filter_xml = ' \n'
253 | util.write_filter(script, filter_xml)
254 | return None
255 |
--------------------------------------------------------------------------------
/meshlabxml/transfer.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML functions to transfer attributes """
2 |
3 | from . import util
4 |
5 |
6 | def tex2vc(script):
7 | """Transfer texture colors to vertex colors
8 |
9 | BUG: this does not work correctly if the file has multiple textures; it
10 | only uses one texture and remaps all of the UVs to that
11 | https://github.com/cnr-isti-vclab/meshlab/issues/124
12 | should be fixed in post 2016.12 release
13 |
14 | """
15 | filter_xml = ' \n'
16 | util.write_filter(script, filter_xml)
17 | return None
18 |
19 |
20 | def vc2tex(script, tex_name='TEMP3D_texture.png', tex_width=1024,
21 | tex_height=1024, overwrite_tex=False, assign_tex=False,
22 | fill_tex=True):
23 | """Transfer vertex colors to texture colors
24 |
25 | Args:
26 | script: the FilterScript object or script filename to write
27 | the filter to.
28 | tex_name (str): The texture file to be created
29 | tex_width (int): The texture width
30 | tex_height (int): The texture height
31 | overwrite_tex (bool): If current mesh has a texture will be overwritten (with provided texture dimension)
32 | assign_tex (bool): Assign the newly created texture
33 | fill_tex (bool): If enabled the unmapped texture space is colored using a pull push filling algorithm, if false is set to black
34 | """
35 | if script.ml_version == '1.3.4BETA':
36 | filter_name = 'Vertex Color to Texture'
37 | else:
38 | filter_name = 'Transfer: Vertex Color to Texture'
39 | filter_xml = ''.join([
40 | ' \n' % filter_name,
41 | ' \n',
46 | ' \n',
51 | ' \n',
56 | ' \n',
61 | ' \n',
66 | ' \n',
71 | ' \n'])
72 | util.write_filter(script, filter_xml)
73 | return None
74 |
75 |
76 | def fc2vc(script):
77 | """Transfer face colors to vertex colors
78 |
79 | Args:
80 | script: the FilterScript object or script filename to write
81 | the filter to.
82 | """
83 | filter_xml = ' \n'
84 | util.write_filter(script, filter_xml)
85 | return None
86 |
87 |
88 | def vc2fc(script):
89 | """Transfer vertex colors to face colors
90 |
91 | Args:
92 | script: the FilterScript object or script filename to write
93 | the filter to.
94 | """
95 | filter_xml = ' \n'
96 | util.write_filter(script, filter_xml)
97 | return None
98 |
99 |
100 | def mesh2fc(script, all_visible_layers=False):
101 | """Transfer mesh colors to face colors
102 |
103 | Args:
104 | script: the FilterScript object or script filename to write
105 | the filter to.
106 | all_visible_layers (bool): If true the color mapping is applied to all the meshes
107 | """
108 | filter_xml = ''.join([
109 | ' \n',
110 | ' \n',
115 | ' \n'])
116 | util.write_filter(script, filter_xml)
117 | return None
118 |
119 |
120 | def vert_attr_2_meshes(script, source_mesh=0, target_mesh=1,
121 | geometry=False, normal=False, color=True,
122 | quality=False, selection=False,
123 | quality_distance=False, max_distance=0.5):
124 | """Vertex Attribute Transfer (between 2 meshes)
125 |
126 | Transfer the chosen per-vertex attributes from one mesh to another. Useful to transfer attributes to different representations of the same object. For each vertex of the target mesh the closest point (not vertex!) on the source mesh is computed, and the requested interpolated attributes from that source point are copied into the target vertex.
127 |
128 | The algorithm assumes that the two meshes are reasonably similar and aligned.
129 |
130 | UpperBound: absolute value (not percentage)
131 |
132 | Args:
133 | script: the FilterScript object or script filename to write
134 | the filter to.
135 | source_mesh (int): The mesh that contains the source data that we want to transfer
136 | target_mesh (int): The mesh whose vertexes will receive the data from the source
137 | geometry (bool): If enabled, the position of each vertex of the target mesh will be snapped onto the corresponding closest point on the source mesh
138 | normal (bool): If enabled, the normal of each vertex of the target mesh will get the (interpolated) normal of the corresponding closest point on the source mesh
139 | color (bool): If enabled, the color of each vertex of the target mesh will become the color of the corresponding closest point on the source mesh
140 | quality (bool): If enabled, the quality of each vertex of the target mesh will become the quality of the corresponding closest point on the source mesh
141 | selection (bool): If enabled, each vertex of the target mesh will be selected if the corresponding closest point on the source mesh falls in a selected face
142 | quality_distance (bool): If enabled, we store the distance of the transferred value as in the vertex quality
143 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering attributes
144 |
145 | """
146 | filter_xml = ''.join([
147 | ' \n',
148 | ' \n',
153 | ' \n',
158 | ' \n',
163 | ' \n',
168 | ' \n',
173 | ' \n',
178 | ' \n',
183 | ' \n',
188 | ' \n',
195 | ' \n'])
196 | util.write_filter(script, filter_xml)
197 | return None
198 |
199 |
200 | def vert_attr2tex_2_meshes(script, source_mesh=0, target_mesh=1, attribute=0,
201 | max_distance=0.5, tex_name='TEMP3D_texture.png',
202 | tex_width=1024, tex_height=1024,
203 | overwrite_tex=True, assign_tex=False,
204 | fill_tex=True):
205 | """Transfer Vertex Attributes to Texture (between 2 meshes)
206 |
207 | Target mesh must be saved to disk or filter will fail
208 |
209 | Created texture seems to be created with absolute pathname. To set relative pathname,
210 | use mlx.texture.set_texture afterwards
211 |
212 | Args:
213 | script: the FilterScript object or script filename to write
214 | the filter to.
215 | source_mesh (int): The mesh that contains the source data that we want to transfer
216 | target_mesh (int): The mesh whose texture will be filled according to source mesh data
217 | attribute (int): Choose what attribute has to be transferred onto the target texture. You can choose between Per vertex attributes (color, normal, quality) or to transfer color information from source mesh texture
218 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering data
219 | tex_name (str): The texture file to be created
220 | tex_width (int): The texture width
221 | tex_height (int): The texture height
222 | overwrite_tex (bool): If target mesh has a texture will be overwritten (with provided texture dimension)
223 | assign_tex (bool): Assign the newly created texture to target mesh
224 | fill_tex (bool): If enabled the unmapped texture space is colored using a pull push filling algorithm, if false is set to black
225 |
226 | Layer stack:
227 | No impacts
228 |
229 | MeshLab versions:
230 | 2016.12
231 | 1.3.4BETA
232 | """
233 | if script.ml_version == '1.3.4BETA':
234 | filter_name = 'Transfer Vertex Attributes to Texture (between 2 meshes)'
235 | else:
236 | filter_name = 'Transfer: Vertex Attributes to Texture (1 or 2 meshes)'
237 | filter_xml = ''.join([
238 | ' \n'.format(filter_name),
239 | ' \n',
244 | ' \n',
249 | ' \n',
259 | ' \n',
266 | ' \n',
271 | ' \n',
276 | ' \n',
281 | ' \n',
286 | ' \n',
291 | ' \n',
296 | ' \n'])
297 | util.write_filter(script, filter_xml)
298 | return None
299 |
300 |
301 | def tex2vc_2_meshes(script, source_mesh=0, target_mesh=1, max_distance=0.5):
302 | """Transfer texture colors to vertex colors (between 2 meshes)
303 |
304 | Args:
305 | script: the FilterScript object or script filename to write
306 | the filter to.
307 | source_mesh (int): The mesh with associated texture that we want to sample from
308 | target_mesh (int): The mesh whose vertex color will be filled according to source mesh texture
309 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering color
310 |
311 | Layer stack:
312 | No impacts
313 |
314 | MeshLab versions:
315 | 2016.12
316 | 1.3.4BETA
317 | """
318 | if script.ml_version == '1.3.4BETA':
319 | filter_name = 'Texture to Vertex Color (between 2 meshes)'
320 | else:
321 | filter_name = 'Transfer: Texture to Vertex Color (1 or 2 meshes)'
322 | filter_xml = ''.join([
323 | ' \n'.format(filter_name),
324 | ' \n',
329 | ' \n',
334 | ' \n',
341 | ' \n'])
342 | util.write_filter(script, filter_xml)
343 | return None
344 |
--------------------------------------------------------------------------------
/meshlabxml/util.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML utility functions """
2 |
3 | import os
4 | import sys
5 | import inspect
6 | from glob import glob
7 |
8 | #from . import FilterScript
9 | import meshlabxml as mlx
10 |
11 | def is_number(num):
12 | """ Check if a variable is a number by trying to convert it to a float.
13 |
14 | """
15 | try:
16 | float(num)
17 | return True
18 | except:
19 | return False
20 |
21 |
22 | def to_float(num):
23 | """ Convert a variable to a float """
24 | try:
25 | float(num)
26 | return float(num)
27 | except ValueError:
28 | return float('NaN')
29 |
30 |
31 | def delete_all(filename):
32 | """ Delete all files in the current directory that match a pattern.
33 |
34 | Intended primarily for temp files, e.g. mlx.delete_all('TEMP3D*').
35 |
36 | """
37 | for fread in glob(filename):
38 | os.remove(fread)
39 |
40 |
41 | def color_values(color):
42 | """Read color_names.txt and find the red, green, and blue values
43 | for a named color.
44 | """
45 | # Get the directory where this script file is located:
46 | this_dir = os.path.dirname(
47 | os.path.realpath(
48 | inspect.getsourcefile(
49 | lambda: 0)))
50 | color_name_file = os.path.join(this_dir, 'color_names.txt')
51 | found = False
52 | for line in open(color_name_file, 'r'):
53 | line = line.rstrip()
54 | if color.lower() == line.split()[0]:
55 | #hex_color = line.split()[1]
56 | red = line.split()[2]
57 | green = line.split()[3]
58 | blue = line.split()[4]
59 | found = True
60 | break
61 | if not found:
62 | print('Color name "%s" not found, using default (white)' % color)
63 | red = 255
64 | green = 255
65 | blue = 255
66 | return red, green, blue
67 |
68 |
69 | def check_list(var, num_terms):
70 | """ Check if a variable is a list and is the correct length.
71 |
72 | If variable is not a list it will make it a list of the correct length with
73 | all terms identical.
74 | """
75 | if not isinstance(var, list):
76 | if isinstance(var, tuple):
77 | var = list(var)
78 | else:
79 | var = [var]
80 | for _ in range(1, num_terms):
81 | var.append(var[0])
82 | if len(var) != num_terms:
83 | print(
84 | '"%s" has the wrong number of terms; it needs %s. Exiting ...' %
85 | (var, num_terms))
86 | sys.exit(1)
87 | return var
88 |
89 |
90 | def make_list(var, num_terms=1):
91 | """ Make a variable a list if it is not already
92 |
93 | If variable is not a list it will make it a list of the correct length with
94 | all terms identical.
95 | """
96 | if not isinstance(var, list):
97 | if isinstance(var, tuple):
98 | var = list(var)
99 | else:
100 | var = [var]
101 | #if len(var) == 1:
102 | for _ in range(1, num_terms):
103 | var.append(var[0])
104 | return var
105 |
106 |
107 | def write_filter(script, filter_xml):
108 | """ Write filter to FilterScript object or filename
109 |
110 | Args:
111 | script (FilterScript object or filename str): the FilterScript object
112 | or script filename to write the filter to.
113 | filter_xml (str): the xml filter string
114 |
115 | """
116 | if isinstance(script, mlx.FilterScript):
117 | script.filters.append(filter_xml)
118 | elif isinstance(script, str):
119 | script_file = open(script, 'a')
120 | script_file.write(filter_xml)
121 | script_file.close()
122 | else:
123 | print(filter_xml)
124 | return None
125 |
126 |
127 | # Matrix Math
128 | def mat_transpose(matrix):
129 | """ Transpose 2D matrix
130 |
131 | Matrix must be a list of lists, i.e. [[1, 2], [3, 4]]
132 |
133 | """
134 | # Using nested list comprehension
135 | result = [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))]
136 | return result
137 |
138 |
139 | def matmul(matrix_a, matrix_b):
140 | """ Multiply two 2D matrices
141 |
142 | Matrix must be a list of lists, i.e. [[1, 2], [3, 4]]
143 |
144 | """
145 | cols_a = len(matrix_a[0])
146 | rows_b = len(matrix_b)
147 | if cols_a != rows_b:
148 | print("Error: Matrices cannot be multiplied, columns_a != rows_b")
149 | return
150 |
151 | # Using nested list comprehension
152 | result = [[sum(x*y for x, y in zip(matrix_a_row, matrix_b_col)) for matrix_b_col in zip(*matrix_b)] for matrix_a_row in matrix_a]
153 | return result
154 |
--------------------------------------------------------------------------------
/meshlabxml/vert_color.py:
--------------------------------------------------------------------------------
1 | """ MeshLabXML vertex color functions """
2 |
3 | import math
4 |
5 | from . import util
6 | from .color_names import color_name
7 |
8 | def function(script, red=255, green=255, blue=255, alpha=255, color=None):
9 | """Color function using muparser lib to generate new RGBA color for every
10 | vertex
11 |
12 | Red, Green, Blue and Alpha channels may be defined by specifying a function
13 | for each.
14 |
15 | See help(mlx.muparser_ref) for muparser reference documentation.
16 |
17 | It's possible to use the following per-vertex variables in the expression:
18 |
19 | Variables (per vertex):
20 | x, y, z (coordinates)
21 | nx, ny, nz (normal)
22 | r, g, b, a (color)
23 | q (quality)
24 | rad (radius)
25 | vi (vertex index)
26 | vtu, vtv (texture coordinates)
27 | ti (texture index)
28 | vsel (is the vertex selected? 1 yes, 0 no)
29 | and all custom vertex attributes already defined by user.
30 |
31 | Args:
32 | script: the FilterScript object or script filename to write
33 | the filter to.
34 | red (str [0, 255]): function to generate red component
35 | green (str [0, 255]): function to generate green component
36 | blue (str [0, 255]): function to generate blue component
37 | alpha (str [0, 255]): function to generate alpha component
38 | color (str): name of one of the 140 HTML Color Names defined
39 | in CSS & SVG.
40 | Ref: https://en.wikipedia.org/wiki/Web_colors#X11_color_names
41 | If not None this will override the per component variables.
42 |
43 | Layer stack:
44 | No impacts
45 |
46 | MeshLab versions:
47 | 2016.12
48 | 1.3.4BETA
49 | """
50 | # TODO: add options for HSV
51 | # https://www.cs.rit.edu/~ncs/color/t_convert.html
52 | if color is not None:
53 | red, green, blue, _ = color_name[color.lower()]
54 | filter_xml = ''.join([
55 | ' \n',
56 | ' \n',
61 | ' \n',
66 | ' \n',
71 | ' \n',
76 | ' \n'])
77 | util.write_filter(script, filter_xml)
78 | return None
79 |
80 |
81 | def voronoi(script, target_layer=0, source_layer=1, backward=True):
82 | """ Given a Mesh 'M' and a Pointset 'P', the filter projects each vertex of
83 | P over M and color M according to the geodesic distance from these
84 | projected points. Projection and coloring are done on a per vertex
85 | basis.
86 |
87 | Args:
88 | script: the FilterScript object or script filename to write
89 | the filter to.
90 | target_layer (int): The mesh layer whose surface is colored. For each
91 | vertex of this mesh we decide the color according to the following
92 | arguments.
93 | source_layer (int): The mesh layer whose vertexes are used as seed
94 | points for the color computation. These seeds point are projected
95 | onto the target_layer mesh.
96 | backward (bool): If True the mesh is colored according to the distance
97 | from the frontier of the voronoi diagram induced by the
98 | source_layer seeds.
99 |
100 | Layer stack:
101 | No impacts
102 |
103 | MeshLab versions:
104 | 2016.12
105 | 1.3.4BETA
106 | """
107 | filter_xml = ''.join([
108 | ' \n',
109 | ' \n',
114 | ' \n',
119 | ' \n',
124 | ' \n'])
125 | util.write_filter(script, filter_xml)
126 | return None
127 |
128 |
129 | def cyclic_rainbow(script, direction='sphere', start_pt=(0, 0, 0),
130 | amplitude=255 / 2, center=255 / 2, freq=0.8,
131 | phase=(0, 120, 240, 0), alpha=False):
132 | """ Color mesh vertices in a repeating sinusiodal rainbow pattern
133 |
134 | Sine wave follows the following equation for each color channel (RGBA):
135 | channel = sin(freq*increment + phase)*amplitude + center
136 |
137 | Args:
138 | script: the FilterScript object or script filename to write
139 | the filter to.
140 | direction (str) = the direction that the sine wave will travel; this
141 | and the start_pt determine the 'increment' of the sine function.
142 | Valid values are:
143 | 'sphere' - radiate sine wave outward from start_pt (default)
144 | 'x' - sine wave travels along the X axis
145 | 'y' - sine wave travels along the Y axis
146 | 'z' - sine wave travels along the Z axis
147 | or define the increment directly using a muparser function, e.g.
148 | '2x + y'. In this case start_pt will not be used; include it in
149 | the function directly.
150 | start_pt (3 coordinate tuple or list): start point of the sine wave. For a
151 | sphere this is the center of the sphere.
152 | amplitude (float [0, 255], single value or 4 term tuple or list): amplitude
153 | of the sine wave, with range between 0-255. If a single value is
154 | specified it will be used for all channels, otherwise specify each
155 | channel individually.
156 | center (float [0, 255], single value or 4 term tuple or list): center
157 | of the sine wave, with range between 0-255. If a single value is
158 | specified it will be used for all channels, otherwise specify each
159 | channel individually.
160 | freq (float, single value or 4 term tuple or list): frequency of the sine
161 | wave. If a single value is specified it will be used for all channels,
162 | otherwise specifiy each channel individually.
163 | phase (float [0, 360], single value or 4 term tuple or list): phase
164 | of the sine wave in degrees, with range between 0-360. If a single
165 | value is specified it will be used for all channels, otherwise specify
166 | each channel individually.
167 | alpha (bool): if False the alpha channel will be set to 255 (full opacity).
168 |
169 | Layer stack:
170 | No impacts
171 |
172 | MeshLab versions:
173 | 2016.12
174 | 1.3.4BETA
175 | """
176 | start_pt = util.make_list(start_pt, 3)
177 | amplitude = util.make_list(amplitude, 4)
178 | center = util.make_list(center, 4)
179 | freq = util.make_list(freq, 4)
180 | phase = util.make_list(phase, 4)
181 |
182 | if direction.lower() == 'sphere':
183 | increment = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)'.format(
184 | start_pt[0], start_pt[1], start_pt[2])
185 | elif direction.lower() == 'x':
186 | increment = 'x - {}'.format(start_pt[0])
187 | elif direction.lower() == 'y':
188 | increment = 'y - {}'.format(start_pt[1])
189 | elif direction.lower() == 'z':
190 | increment = 'z - {}'.format(start_pt[2])
191 | else:
192 | increment = direction
193 |
194 | red_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
195 | f=freq[0], i=increment, p=math.radians(phase[0]),
196 | a=amplitude[0], c=center[0])
197 | green_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
198 | f=freq[1], i=increment, p=math.radians(phase[1]),
199 | a=amplitude[1], c=center[1])
200 | blue_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
201 | f=freq[2], i=increment, p=math.radians(phase[2]),
202 | a=amplitude[2], c=center[2])
203 | if alpha:
204 | alpha_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
205 | f=freq[3], i=increment, p=math.radians(phase[3]),
206 | a=amplitude[3], c=center[3])
207 | else:
208 | alpha_func = 255
209 |
210 | function(script, red=red_func, green=green_func, blue=blue_func,
211 | alpha=alpha_func)
212 | return None
213 |
--------------------------------------------------------------------------------
/models/bunny.txt:
--------------------------------------------------------------------------------
1 | Stanford Bunny
2 |
3 | The Stanford Bunny model is courtesy of the Stanford University Computer
4 | Graphics Laboratory, and is included with MeshLabXML with permission. The
5 | original file can be found at http://graphics.stanford.edu/data/3Dscanrep/
6 | with additional information on the model's venerable history at
7 | http://www.cc.gatech.edu/~turk/bunny/bunny.html .
8 |
9 | Terms of use (retrieved from the first link above):
10 | Please be sure to acknowledge the source of the data and models you take
11 | from this repository. In each of the listings below, we have cited the
12 | source of the range data and reconstructed models. You are welcome to use
13 | the data and models for research purposes. You are also welcome to mirror
14 | or redistribute them for free. Finally, you may publish images made using
15 | these models, or the images on this web site, in a scholarly article or
16 | book - as long as credit is given to the Stanford Computer Graphics
17 | Laboratory. However, such models or images are not to be used for
18 | commercial purposes, nor should they appear in a product for sale (with
19 | the exception of scholarly journals or books), without our permission.
20 |
21 | Source: Stanford University Computer Graphics Laboratory
22 | Scanner: Cyberware 3030 MS
23 | Number of scans: 10
24 | Total size of scans: 362,272 points (about 725,000 triangles)
25 | Reconstruction: zipper
26 | Size of reconstruction: 35947 vertices, 69451 triangles
27 | Comments: contains 5 holes in the bottom
28 |
29 | Files:
30 | The following files are included with MeshLabXML. Note that the values in the
31 | parentheses in the filenames indicate metadata about the model, specifically
32 | scale factor (with negative values indicating an inverse scale factor) and
33 | "up" axis (either Y or Z).
34 |
35 | Filename: bunny_raw(-1250Y).ply
36 | MeshLabXML name: bunny_raw
37 | Description: model is identical to the original source, except it has been
38 | converted to a binary ply file to reduce file size.
39 |
40 | Filename: bunny_flat(1Z).ply
41 | MeshLabXML name: bunny
42 | Description: model has had the following changes applied:
43 | - Scaled to actual size in mm (see "Notes on units and scale" below)
44 | - Hole under chin has been filled
45 | - Unreferenced vertices have been deleted
46 | - Bottom has been sliced flat and placed on the XY plane
47 | - Model has been rotated to Z up
48 | The above changes result in a model which is watertight and manifold, e.g.
49 | suitable for 3D printing.
50 |
51 | Notes on units and scale:
52 | The units and scale of the original model are unknown. The principals
53 | involved (Marc Levoy and Greg Turk) were contacted however they no longer
54 | have this information available. Based on the reported size of the actual
55 | object (approximately 7.5 inches high and 8 inches long), units of mm and a
56 | scale factor of 1:1250 is assumed for the original model to approximate the
57 | correct size.
58 |
--------------------------------------------------------------------------------
/models/bunny_flat(1Z).ply:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/models/bunny_flat(1Z).ply
--------------------------------------------------------------------------------
/models/bunny_raw(-1250Y).ply:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/models/bunny_raw(-1250Y).ply
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | # read the contents of your README file
5 | import io
6 | from os import path
7 | this_directory = path.abspath(path.dirname(__file__))
8 | with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
9 | long_description = f.read()
10 |
11 | setup(name='MeshLabXML',
12 | version='2021.7',
13 | description='Create and run MeshLab XML scripts',
14 | long_description=long_description,
15 | long_description_content_type='text/markdown',
16 | url='https://github.com/3DLIRIOUS/MeshLabXML',
17 | author='3DLirious, LLC',
18 | author_email='3DLirious@gmail.com',
19 | license='LGPL-2.1',
20 | packages=['meshlabxml'],
21 | include_package_data=True)
22 |
--------------------------------------------------------------------------------
/test/black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/black.png
--------------------------------------------------------------------------------
/test/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/blue.png
--------------------------------------------------------------------------------
/test/cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/cyan.png
--------------------------------------------------------------------------------
/test/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/green.png
--------------------------------------------------------------------------------
/test/magenta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/magenta.png
--------------------------------------------------------------------------------
/test/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/red.png
--------------------------------------------------------------------------------
/test/white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/white.png
--------------------------------------------------------------------------------
/test/yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/yellow.png
--------------------------------------------------------------------------------