12
12
# GNU General Public License for more details.
13
13
14
14
import os
15
+ import io
15
16
16
- import multiprocessing as mp
17
17
import trimesh as tm
18
18
19
- from pathlib import Path
20
19
from typing import Union , Iterable , Optional , Dict , Any
21
20
from typing_extensions import Literal
21
+ from urllib3 import HTTPResponse
22
22
23
23
from .. import config , utils , core
24
24
from . import base
25
25
26
26
# Set up logging
27
27
logger = config .get_logger (__name__ )
28
28
29
+ # Mesh files can have all sort of extensions
30
+ DEFAULT_FMT = "{name}.{file_ext}"
31
+
32
+ # Mesh extensions supported by trimesh
33
+ MESH_LOAD_EXT = tuple (tm .exchange .load .mesh_loaders .keys ())
34
+ MESH_WRITE_EXT = tuple (tm .exchange .export ._mesh_exporters .keys ())
35
+
36
+
37
+ class MeshReader (base .BaseReader ):
38
+ def __init__ (
39
+ self ,
40
+ output : str ,
41
+ fmt : str = DEFAULT_FMT ,
42
+ attrs : Optional [Dict [str , Any ]] = None ,
43
+ ):
44
+ super ().__init__ (
45
+ fmt = fmt ,
46
+ attrs = attrs ,
47
+ file_ext = MESH_LOAD_EXT ,
48
+ name_fallback = "MESH" ,
49
+ read_binary = True ,
50
+ )
51
+ self .output = output
52
+
53
+ def format_output (self , x ):
54
+ # This function replaces the BaseReader.format_output()
55
+ # This is to avoid trying to convert multiple (image, header) to NeuronList
56
+ if self .output == "trimesh" :
57
+ return x
58
+ elif x :
59
+ return core .NeuronList (x )
60
+ else :
61
+ return core .NeuronList ([])
62
+
63
+ @base .handle_errors
64
+ def read_buffer (
65
+ self , f , attrs : Optional [Dict [str , Any ]] = None
66
+ ) -> Union [tm .Trimesh , "core.Volume" , "core.MeshNeuron" ]:
67
+ """Read buffer into mesh.
68
+
69
+ Parameters
70
+ ----------
71
+ f : IO
72
+ Readable buffer (must be bytes).
73
+ attrs : dict | None
74
+ Arbitrary attributes to include in the neurons.
75
+
76
+ Returns
77
+ -------
78
+ Trimesh | MeshNeuron | Volume
79
+
80
+ """
81
+ if isinstance (f , HTTPResponse ):
82
+ f = io .StringIO (f .content )
83
+
84
+ if isinstance (f , bytes ):
85
+ f = io .BytesIO (f )
86
+
87
+ # We need to tell trimesh what file type we are reading
88
+ if "file" not in attrs :
89
+ raise KeyError (
90
+ f'Unable to parse file type. "file" not in attributes: { attrs } '
91
+ )
92
+
93
+ file_type = attrs ["file" ].split ("." )[- 1 ]
94
+
95
+ mesh = tm .load_mesh (f , file_type = file_type )
96
+
97
+ if self .output == "trimesh" :
98
+ return mesh
99
+ elif self .output == "volume" :
100
+ return core .Volume (mesh .vertices , mesh .faces , ** attrs )
101
+
102
+ # Turn into a MeshNeuron
103
+ n = core .MeshNeuron (mesh )
104
+
105
+ # Try adding properties one-by-one. If one fails, we'll keep track of it
106
+ # in the `.meta` attribute
107
+ meta = {}
108
+ for k , v in attrs .items ():
109
+ try :
110
+ n ._register_attr (k , v )
111
+ except (AttributeError , ValueError , TypeError ):
112
+ meta [k ] = v
113
+
114
+ if meta :
115
+ n .meta = meta
116
+
117
+ return n
29
118
30
- def read_mesh ( f : Union [ str , Iterable ],
31
- include_subdirs : bool = False ,
32
- parallel : Union [bool , int ] = 'auto' ,
33
- output : Union [ Literal [ 'neuron' ] ,
34
- Literal [ 'volume' ] ,
35
- Literal [' trimesh' ]] = ' neuron' ,
36
- errors : Union [ Literal [' raise' ] ,
37
- Literal [ 'log' ] ,
38
- Literal [ 'ignore' ]] = 'log' ,
39
- limit : Optional [ int ] = None ,
40
- ** kwargs ) -> ' core.NeuronObject' :
41
- """Create Neuron/List from mesh .
119
+
120
+ def read_mesh (
121
+ f : Union [str , Iterable ] ,
122
+ include_subdirs : bool = False ,
123
+ parallel : Union [ bool , int ] = "auto" ,
124
+ output : Union [ Literal [ "neuron" ], Literal [ "volume" ], Literal [" trimesh" ]] = " neuron" ,
125
+ errors : Literal [" raise" , "log" , "ignore" ] = "raise" ,
126
+ limit : Optional [ int ] = None ,
127
+ fmt : str = "{name}." ,
128
+ ** kwargs ,
129
+ ) -> " core.NeuronObject" :
130
+ """Load mesh file into Neuron/List.
42
131
43
132
This is a thin wrapper around `trimesh.load_mesh` which supports most
44
- common formats (obj, ply, stl, etc.).
133
+ commonly used formats (obj, ply, stl, etc.).
45
134
46
135
Parameters
47
136
----------
48
137
f : str | iterable
49
- Filename(s) or folder. If folder must include file
50
- extension (e.g. `my/dir/*.ply`).
138
+ Filename(s) or folder. If folder should include file
139
+ extension (e.g. `my/dir/*.ply`) otherwise all
140
+ mesh files in the folder will be read.
51
141
include_subdirs : bool, optional
52
142
If True and `f` is a folder, will also search
53
143
subdirectories for meshes.
@@ -59,9 +149,10 @@ def read_mesh(f: Union[str, Iterable],
59
149
neurons. Integer will be interpreted as the number of
60
150
cores (otherwise defaults to `os.cpu_count() - 2`).
61
151
output : "neuron" | "volume" | "trimesh"
62
- Determines function's output. See Returns.
152
+ Determines function's output - see ` Returns` .
63
153
errors : "raise" | "log" | "ignore"
64
- If "log" or "ignore", errors will not be raised.
154
+ If "log" or "ignore", errors will not be raised and the
155
+ mesh will be skipped. Can result in empty output.
65
156
limit : int | str | slice | list, optional
66
157
When reading from a folder or archive you can use this parameter to
67
158
restrict the which files read:
@@ -81,19 +172,24 @@ def read_mesh(f: Union[str, Iterable],
81
172
82
173
Returns
83
174
-------
84
- navis. MeshNeuron
175
+ MeshNeuron
85
176
If `output="neuron"` (default).
86
- navis. Volume
177
+ Volume
87
178
If `output="volume"`.
88
- trimesh. Trimesh
89
- If `output=' trimesh' `.
90
- navis. NeuronList
179
+ Trimesh
180
+ If `output=" trimesh" `.
181
+ NeuronList
91
182
If `output="neuron"` and import has multiple meshes
92
183
will return NeuronList of MeshNeurons.
93
184
list
94
185
If `output!="neuron"` and import has multiple meshes
95
186
will return list of Volumes or Trimesh.
96
187
188
+ See Also
189
+ --------
190
+ [`navis.read_precomputed`][]
191
+ Read meshes and skeletons from Neuroglancer's precomputed format.
192
+
97
193
Examples
98
194
--------
99
195
@@ -114,101 +210,19 @@ def read_mesh(f: Union[str, Iterable],
114
210
>>> nl = navis.read_mesh('mesh.obj', output='volume') # doctest: +SKIP
115
211
116
212
"""
117
- utils .eval_param (output , name = 'output' ,
118
- allowed_values = ('neuron' , 'volume' , 'trimesh' ))
119
-
120
- # If is directory, compile list of filenames
121
- if isinstance (f , str ) and '*' in f :
122
- f , ext = f .split ('*' )
123
- f = Path (f ).expanduser ()
124
-
125
- if not f .is_dir ():
126
- raise ValueError (f'{ f } does not appear to exist' )
127
-
128
- if not include_subdirs :
129
- f = list (f .glob (f'*{ ext } ' ))
130
- else :
131
- f = list (f .rglob (f'*{ ext } ' ))
132
-
133
- if limit :
134
- f = f [:limit ]
135
-
136
- if utils .is_iterable (f ):
137
- # Do not use if there is only a small batch to import
138
- if isinstance (parallel , str ) and parallel .lower () == 'auto' :
139
- if len (f ) < 100 :
140
- parallel = False
141
-
142
- if parallel :
143
- # Do not swap this as `isinstance(True, int)` returns `True`
144
- if isinstance (parallel , (bool , str )):
145
- n_cores = os .cpu_count () - 2
146
- else :
147
- n_cores = int (parallel )
148
-
149
- with mp .Pool (processes = n_cores ) as pool :
150
- results = pool .imap (_worker_wrapper , [dict (f = x ,
151
- output = output ,
152
- errors = errors ,
153
- include_subdirs = include_subdirs ,
154
- parallel = False ) for x in f ],
155
- chunksize = 1 )
156
-
157
- res = list (config .tqdm (results ,
158
- desc = 'Importing' ,
159
- total = len (f ),
160
- disable = config .pbar_hide ,
161
- leave = config .pbar_leave ))
162
-
163
- else :
164
- # If not parallel just import the good 'ole way: sequentially
165
- res = [read_mesh (x ,
166
- include_subdirs = include_subdirs ,
167
- output = output ,
168
- errors = errors ,
169
- parallel = parallel ,
170
- ** kwargs )
171
- for x in config .tqdm (f , desc = 'Importing' ,
172
- disable = config .pbar_hide ,
173
- leave = config .pbar_leave )]
174
-
175
- if output == 'neuron' :
176
- return core .NeuronList ([r for r in res if r ])
177
-
178
- return res
179
-
180
- try :
181
- # Open the file
182
- fname = '.' .join (os .path .basename (f ).split ('.' )[:- 1 ])
183
- mesh = tm .load_mesh (f )
184
-
185
- if output == 'trimesh' :
186
- return mesh
187
-
188
- attrs = {'name' : fname , 'origin' : f }
189
- attrs .update (kwargs )
190
- if output == 'volume' :
191
- return core .Volume (mesh .vertices , mesh .faces , ** attrs )
192
- else :
193
- return core .MeshNeuron (mesh , ** attrs )
194
- except BaseException as e :
195
- msg = f'Error reading file { fname } .'
196
- if errors == 'raise' :
197
- raise ImportError (msg ) from e
198
- elif errors == 'log' :
199
- logger .error (f'{ msg } : { e } ' )
200
- return
201
-
213
+ utils .eval_param (
214
+ output , name = "output" , allowed_values = ("neuron" , "volume" , "trimesh" )
215
+ )
202
216
203
- def _worker_wrapper (kwargs ):
204
- """Helper for importing meshes using multiple processes."""
205
- return read_mesh (** kwargs )
217
+ reader = MeshReader (fmt = fmt , output = output , errors = errors , attrs = kwargs )
218
+ return reader .read_any (f , include_subdirs , parallel , limit = limit )
206
219
207
220
208
- def write_mesh (x : Union ['core.NeuronList' , 'core.MeshNeuron' , 'core.Volume' , 'tm.Trimesh' ],
209
- filepath : Optional [str ] = None ,
210
- filetype : str = None ,
211
- ) -> None :
221
+ def write_mesh (
222
+ x : Union ["core.NeuronList" , "core.MeshNeuron" , "core.Volume" , "tm.Trimesh" ],
223
+ filepath : Optional [str ] = None ,
224
+ filetype : str = None ,
225
+ ) -> None :
212
226
"""Export meshes (MeshNeurons, Volumes, Trimeshes) to disk.
213
227
214
228
Under the hood this is using trimesh to export meshes.
@@ -264,41 +278,44 @@ def write_mesh(x: Union['core.NeuronList', 'core.MeshNeuron', 'core.Volume', 'tm
264
278
>>> navis.write_mesh(nl, tmp_dir / 'meshes.zip', filetype='obj')
265
279
266
280
"""
267
- ALLOWED_FILETYPES = ('stl' , 'ply' , 'obj' )
268
281
if filetype is not None :
269
- utils .eval_param (filetype , name = ' filetype' , allowed_values = ALLOWED_FILETYPES )
282
+ utils .eval_param (filetype , name = " filetype" , allowed_values = MESH_WRITE_EXT )
270
283
else :
271
284
# See if we can get filetype from filepath
272
285
if filepath is not None :
273
- for f in ALLOWED_FILETYPES :
274
- if str (filepath ).endswith (f' .{ f } ' ):
286
+ for f in MESH_WRITE_EXT :
287
+ if str (filepath ).endswith (f" .{ f } " ):
275
288
filetype = f
276
289
break
277
290
278
291
if not filetype :
279
- raise ValueError ('Must provide mesh type either explicitly via '
280
- '`filetype` variable or implicitly via the '
281
- 'file extension in `filepath`' )
292
+ raise ValueError (
293
+ "Must provide mesh type either explicitly via "
294
+ "`filetype` variable or implicitly via the "
295
+ "file extension in `filepath`"
296
+ )
282
297
283
- writer = base .Writer (_write_mesh , ext = f' .{ filetype } ' )
298
+ writer = base .Writer (_write_mesh , ext = f" .{ filetype } " )
284
299
285
- return writer .write_any (x ,
286
- filepath = filepath )
300
+ return writer .write_any (x , filepath = filepath )
287
301
288
302
289
- def _write_mesh (x : Union ['core.MeshNeuron' , 'core.Volume' , 'tm.Trimesh' ],
290
- filepath : Optional [str ] = None ) -> None :
303
+ def _write_mesh (
304
+ x : Union ["core.MeshNeuron" , "core.Volume" , "tm.Trimesh" ],
305
+ filepath : Optional [str ] = None ,
306
+ ) -> None :
291
307
"""Write single mesh to disk."""
292
308
if filepath and os .path .isdir (filepath ):
293
309
if isinstance (x , core .MeshNeuron ):
294
310
if not x .id :
295
- raise ValueError ('Neuron(s) must have an ID when destination '
296
- 'is a folder' )
297
- filepath = os .path .join (filepath , f'{ x .id } ' )
311
+ raise ValueError (
312
+ "Neuron(s) must have an ID when destination " "is a folder"
313
+ )
314
+ filepath = os .path .join (filepath , f"{ x .id } " )
298
315
elif isinstance (x , core .Volume ):
299
- filepath = os .path .join (filepath , f' { x .name } ' )
316
+ filepath = os .path .join (filepath , f" { x .name } " )
300
317
else :
301
- raise ValueError (f' Unable to generate filename for { type (x )} ' )
318
+ raise ValueError (f" Unable to generate filename for { type (x )} " )
302
319
303
320
if isinstance (x , core .MeshNeuron ):
304
321
mesh = x .trimesh
0 commit comments