620 lines
20 KiB
Python
620 lines
20 KiB
Python
|
import os
|
||
|
|
||
|
from .. import util
|
||
|
from .. import resolvers
|
||
|
|
||
|
from ..base import Trimesh
|
||
|
from ..parent import Geometry
|
||
|
from ..points import PointCloud
|
||
|
from ..scene.scene import Scene, append_scenes
|
||
|
from ..constants import log_time, log
|
||
|
|
||
|
from . import misc
|
||
|
from .ply import _ply_loaders
|
||
|
from .stl import _stl_loaders
|
||
|
from .dae import _collada_loaders
|
||
|
from .obj import _obj_loaders
|
||
|
from .off import _off_loaders
|
||
|
from .misc import _misc_loaders
|
||
|
from .gltf import _gltf_loaders
|
||
|
from .assimp import _assimp_loaders
|
||
|
from .threemf import _three_loaders
|
||
|
from .openctm import _ctm_loaders
|
||
|
from .xml_based import _xml_loaders
|
||
|
from .binvox import _binvox_loaders
|
||
|
from .xyz import _xyz_loaders
|
||
|
|
||
|
|
||
|
try:
|
||
|
from ..path.exchange.load import load_path, path_formats
|
||
|
except BaseException as E:
|
||
|
# save a traceback to see why path didn't import
|
||
|
_path_exception = E
|
||
|
|
||
|
def load_path(*args, **kwargs):
|
||
|
"""
|
||
|
Dummy load path function that will raise an exception
|
||
|
on use. Import of path failed, probably because a
|
||
|
dependency is not installed.
|
||
|
|
||
|
Raises
|
||
|
----------
|
||
|
path_exception : BaseException
|
||
|
Whatever failed when we imported path
|
||
|
"""
|
||
|
raise _path_exception
|
||
|
|
||
|
def path_formats():
|
||
|
return []
|
||
|
|
||
|
|
||
|
def mesh_formats():
|
||
|
"""
|
||
|
Get a list of mesh formats
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
loaders : list
|
||
|
Extensions of available mesh loaders
|
||
|
i.e. 'stl', 'ply', etc.
|
||
|
"""
|
||
|
return list(mesh_loaders.keys())
|
||
|
|
||
|
|
||
|
def available_formats():
|
||
|
"""
|
||
|
Get a list of all available loaders
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
loaders : list
|
||
|
Extensions of available loaders
|
||
|
i.e. 'stl', 'ply', 'dxf', etc.
|
||
|
"""
|
||
|
loaders = mesh_formats()
|
||
|
loaders.extend(path_formats())
|
||
|
loaders.extend(compressed_loaders.keys())
|
||
|
return loaders
|
||
|
|
||
|
|
||
|
def load(file_obj,
|
||
|
file_type=None,
|
||
|
resolver=None,
|
||
|
force=None,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Load a mesh or vectorized path into objects like
|
||
|
Trimesh, Path2D, Path3D, Scene
|
||
|
|
||
|
Parameters
|
||
|
-----------
|
||
|
file_obj : str, or file- like object
|
||
|
The source of the data to be loadeded
|
||
|
file_type: str
|
||
|
What kind of file type do we have (eg: 'stl')
|
||
|
resolver : trimesh.visual.Resolver
|
||
|
Object to load referenced assets like materials and textures
|
||
|
force : None or str
|
||
|
For 'mesh': try to coerce scenes into a single mesh
|
||
|
For 'scene': try to coerce everything into a scene
|
||
|
kwargs : dict
|
||
|
Passed to geometry __init__
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
geometry : Trimesh, Path2D, Path3D, Scene
|
||
|
Loaded geometry as trimesh classes
|
||
|
"""
|
||
|
# check to see if we're trying to load something
|
||
|
# that is already a native trimesh Geometry subclass
|
||
|
if isinstance(file_obj, Geometry):
|
||
|
log.info('Load called on %s object, returning input',
|
||
|
file_obj.__class__.__name__)
|
||
|
return file_obj
|
||
|
|
||
|
# parse the file arguments into clean loadable form
|
||
|
(file_obj, # file- like object
|
||
|
file_type, # str, what kind of file
|
||
|
metadata, # dict, any metadata from file name
|
||
|
opened, # bool, did we open the file ourselves
|
||
|
resolver # object to load referenced resources
|
||
|
) = parse_file_args(file_obj=file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver)
|
||
|
|
||
|
try:
|
||
|
if isinstance(file_obj, dict):
|
||
|
# if we've been passed a dict treat it as kwargs
|
||
|
kwargs.update(file_obj)
|
||
|
loaded = load_kwargs(kwargs)
|
||
|
elif file_type in path_formats():
|
||
|
# path formats get loaded with path loader
|
||
|
loaded = load_path(file_obj,
|
||
|
file_type=file_type,
|
||
|
**kwargs)
|
||
|
elif file_type in mesh_loaders:
|
||
|
# mesh loaders use mesh loader
|
||
|
loaded = load_mesh(file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver,
|
||
|
**kwargs)
|
||
|
elif file_type in compressed_loaders:
|
||
|
# for archives, like ZIP files
|
||
|
loaded = load_compressed(file_obj,
|
||
|
file_type=file_type,
|
||
|
**kwargs)
|
||
|
elif file_type in voxel_loaders:
|
||
|
loaded = voxel_loaders[file_type](
|
||
|
file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver,
|
||
|
**kwargs)
|
||
|
else:
|
||
|
if file_type in ['svg', 'dxf']:
|
||
|
# call the dummy function to raise the import error
|
||
|
# this prevents the exception from being super opaque
|
||
|
load_path()
|
||
|
else:
|
||
|
raise ValueError('File type: %s not supported' %
|
||
|
file_type)
|
||
|
finally:
|
||
|
# close any opened files even if we crashed out
|
||
|
if opened:
|
||
|
file_obj.close()
|
||
|
|
||
|
# add load metadata ('file_name') to each loaded geometry
|
||
|
for i in util.make_sequence(loaded):
|
||
|
i.metadata.update(metadata)
|
||
|
|
||
|
# if we opened the file in this function ourselves from a
|
||
|
# file name clean up after ourselves by closing it
|
||
|
if opened:
|
||
|
file_obj.close()
|
||
|
|
||
|
# combine a scene into a single mesh
|
||
|
if force == 'mesh' and isinstance(loaded, Scene):
|
||
|
return util.concatenate(loaded.dump())
|
||
|
if force == 'scene' and not isinstance(loaded, Scene):
|
||
|
return Scene(loaded)
|
||
|
|
||
|
return loaded
|
||
|
|
||
|
|
||
|
@log_time
|
||
|
def load_mesh(file_obj,
|
||
|
file_type=None,
|
||
|
resolver=None,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Load a mesh file into a Trimesh object
|
||
|
|
||
|
Parameters
|
||
|
-----------
|
||
|
file_obj : str or file object
|
||
|
File name or file with mesh data
|
||
|
file_type : str or None
|
||
|
Which file type, e.g. 'stl'
|
||
|
kwargs : dict
|
||
|
Passed to Trimesh constructor
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
mesh : trimesh.Trimesh or trimesh.Scene
|
||
|
Loaded geometry data
|
||
|
"""
|
||
|
|
||
|
# parse the file arguments into clean loadable form
|
||
|
(file_obj, # file- like object
|
||
|
file_type, # str, what kind of file
|
||
|
metadata, # dict, any metadata from file name
|
||
|
opened, # bool, did we open the file ourselves
|
||
|
resolver # object to load referenced resources
|
||
|
) = parse_file_args(file_obj=file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver)
|
||
|
|
||
|
try:
|
||
|
# make sure we keep passed kwargs to loader
|
||
|
# but also make sure loader keys override passed keys
|
||
|
results = mesh_loaders[file_type](file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver,
|
||
|
**kwargs)
|
||
|
|
||
|
if util.is_file(file_obj):
|
||
|
file_obj.close()
|
||
|
|
||
|
if not isinstance(results, list):
|
||
|
results = [results]
|
||
|
|
||
|
loaded = []
|
||
|
for result in results:
|
||
|
kwargs.update(result)
|
||
|
loaded.append(load_kwargs(kwargs))
|
||
|
loaded[-1].metadata.update(metadata)
|
||
|
if len(loaded) == 1:
|
||
|
loaded = loaded[0]
|
||
|
# show the repr for loaded
|
||
|
log.debug('loaded {} using {}'.format(
|
||
|
str(loaded),
|
||
|
mesh_loaders[file_type].__name__))
|
||
|
finally:
|
||
|
# if we failed to load close file
|
||
|
if opened:
|
||
|
file_obj.close()
|
||
|
|
||
|
return loaded
|
||
|
|
||
|
|
||
|
def load_compressed(file_obj,
|
||
|
file_type=None,
|
||
|
resolver=None,
|
||
|
mixed=False,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Given a compressed archive load all the geometry that
|
||
|
we can from it.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
file_obj : open file-like object
|
||
|
Containing compressed data
|
||
|
file_type : str
|
||
|
Type of the archive file
|
||
|
mixed : bool
|
||
|
If False, for archives containing both 2D and 3D
|
||
|
data will only load the 3D data into the Scene.
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
scene : trimesh.Scene
|
||
|
Geometry loaded in to a Scene object
|
||
|
"""
|
||
|
|
||
|
# parse the file arguments into clean loadable form
|
||
|
(file_obj, # file- like object
|
||
|
file_type, # str, what kind of file
|
||
|
metadata, # dict, any metadata from file name
|
||
|
opened, # bool, did we open the file ourselves
|
||
|
resolver # object to load referenced resources
|
||
|
) = parse_file_args(file_obj=file_obj,
|
||
|
file_type=file_type,
|
||
|
resolver=resolver)
|
||
|
|
||
|
try:
|
||
|
# a dict of 'name' : file-like object
|
||
|
files = util.decompress(file_obj=file_obj,
|
||
|
file_type=file_type)
|
||
|
# store loaded geometries as a list
|
||
|
geometries = []
|
||
|
|
||
|
# so loaders can access textures/etc
|
||
|
resolver = resolvers.ZipResolver(files)
|
||
|
|
||
|
# try to save the files with meaningful metadata
|
||
|
if 'file_path' in metadata:
|
||
|
archive_name = metadata['file_path']
|
||
|
else:
|
||
|
archive_name = 'archive'
|
||
|
|
||
|
# populate our available formats
|
||
|
if mixed:
|
||
|
available = available_formats()
|
||
|
else:
|
||
|
# all types contained in ZIP archive
|
||
|
contains = set(util.split_extension(n).lower()
|
||
|
for n in files.keys())
|
||
|
# if there are no mesh formats available
|
||
|
if contains.isdisjoint(mesh_formats()):
|
||
|
available = path_formats()
|
||
|
else:
|
||
|
available = mesh_formats()
|
||
|
|
||
|
for name, data in files.items():
|
||
|
# only load formats that we support
|
||
|
compressed_type = util.split_extension(name).lower()
|
||
|
if compressed_type not in available:
|
||
|
# don't raise an exception, just try the next one
|
||
|
continue
|
||
|
# store the file name relative to the archive
|
||
|
metadata['file_name'] = (archive_name + '/' +
|
||
|
os.path.basename(name))
|
||
|
# load the individual geometry
|
||
|
loaded = load(file_obj=data,
|
||
|
file_type=compressed_type,
|
||
|
resolver=resolver,
|
||
|
metadata=metadata,
|
||
|
**kwargs)
|
||
|
|
||
|
# some loaders return multiple geometries
|
||
|
if util.is_sequence(loaded):
|
||
|
# if the loader has returned a list of meshes
|
||
|
geometries.extend(loaded)
|
||
|
else:
|
||
|
# if the loader has returned a single geometry
|
||
|
geometries.append(loaded)
|
||
|
|
||
|
finally:
|
||
|
# if we opened the file in this function
|
||
|
# clean up after ourselves
|
||
|
if opened:
|
||
|
file_obj.close()
|
||
|
|
||
|
# append meshes or scenes into a single Scene object
|
||
|
result = append_scenes(geometries)
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def load_remote(url, **kwargs):
|
||
|
"""
|
||
|
Load a mesh at a remote URL into a local trimesh object.
|
||
|
|
||
|
This must be called explicitly rather than automatically
|
||
|
from trimesh.load to ensure users don't accidentally make
|
||
|
network requests.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
url : string
|
||
|
URL containing mesh file
|
||
|
**kwargs : passed to `load`
|
||
|
"""
|
||
|
# import here to keep requirement soft
|
||
|
import requests
|
||
|
|
||
|
# download the mesh
|
||
|
response = requests.get(url)
|
||
|
# wrap as file object
|
||
|
file_obj = util.wrap_as_stream(response.content)
|
||
|
|
||
|
# so loaders can access textures/etc
|
||
|
resolver = resolvers.WebResolver(url)
|
||
|
|
||
|
# actually load
|
||
|
loaded = load(file_obj=file_obj,
|
||
|
file_type=url,
|
||
|
resolver=resolver,
|
||
|
**kwargs)
|
||
|
return loaded
|
||
|
|
||
|
|
||
|
def load_kwargs(*args, **kwargs):
|
||
|
"""
|
||
|
Load geometry from a properly formatted dict or kwargs
|
||
|
"""
|
||
|
def handle_scene():
|
||
|
"""
|
||
|
Load a scene from our kwargs:
|
||
|
|
||
|
class: Scene
|
||
|
geometry: dict, name: Trimesh kwargs
|
||
|
graph: list of dict, kwargs for scene.graph.update
|
||
|
base_frame: str, base frame of graph
|
||
|
"""
|
||
|
scene = Scene()
|
||
|
scene.geometry.update({k: load_kwargs(v) for
|
||
|
k, v in kwargs['geometry'].items()})
|
||
|
for k in kwargs['graph']:
|
||
|
if isinstance(k, dict):
|
||
|
scene.graph.update(**k)
|
||
|
elif util.is_sequence(k) and len(k) == 3:
|
||
|
scene.graph.update(k[1], k[0], **k[2])
|
||
|
if 'base_frame' in kwargs:
|
||
|
scene.graph.base_frame = kwargs['base_frame']
|
||
|
if 'metadata' in kwargs:
|
||
|
scene.metadata.update(kwargs['metadata'])
|
||
|
return scene
|
||
|
|
||
|
def handle_mesh():
|
||
|
"""
|
||
|
Handle the keyword arguments for a Trimesh object
|
||
|
"""
|
||
|
# if they've been serialized as a dict
|
||
|
if (isinstance(kwargs['vertices'], dict) or
|
||
|
isinstance(kwargs['faces'], dict)):
|
||
|
return Trimesh(**misc.load_dict(kwargs))
|
||
|
# otherwise just load that puppy
|
||
|
return Trimesh(**kwargs)
|
||
|
|
||
|
def handle_export():
|
||
|
"""
|
||
|
Handle an exported mesh.
|
||
|
"""
|
||
|
data, file_type = kwargs['data'], kwargs['file_type']
|
||
|
if not isinstance(data, dict):
|
||
|
data = util.wrap_as_stream(data)
|
||
|
k = mesh_loaders[file_type](data,
|
||
|
file_type=file_type)
|
||
|
return Trimesh(**k)
|
||
|
|
||
|
def handle_pointcloud():
|
||
|
return PointCloud(**kwargs)
|
||
|
|
||
|
# if we've been passed a single dict instead of kwargs
|
||
|
# substitute the dict for kwargs
|
||
|
if (len(kwargs) == 0 and
|
||
|
len(args) == 1 and
|
||
|
isinstance(args[0], dict)):
|
||
|
kwargs = args[0]
|
||
|
|
||
|
# (function, tuple of expected keys)
|
||
|
# order is important
|
||
|
handlers = (
|
||
|
(handle_scene, ('graph', 'geometry')),
|
||
|
(handle_mesh, ('vertices', 'faces')),
|
||
|
(handle_pointcloud, ('vertices',)),
|
||
|
(handle_export, ('file_type', 'data')))
|
||
|
|
||
|
# filter out keys with a value of None
|
||
|
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||
|
|
||
|
# loop through handler functions and expected key
|
||
|
for func, expected in handlers:
|
||
|
if all(i in kwargs for i in expected):
|
||
|
# all expected kwargs exist
|
||
|
handler = func
|
||
|
# exit the loop as we found one
|
||
|
break
|
||
|
else:
|
||
|
raise ValueError('unable to determine type!')
|
||
|
|
||
|
return handler()
|
||
|
|
||
|
|
||
|
def parse_file_args(file_obj,
|
||
|
file_type,
|
||
|
resolver=None,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Given a file_obj and a file_type try to magically convert
|
||
|
arguments to a file-like object and a lowercase string of
|
||
|
file type.
|
||
|
|
||
|
Parameters
|
||
|
-----------
|
||
|
file_obj : str
|
||
|
if string represents a file path, returns:
|
||
|
file_obj: an 'rb' opened file object of the path
|
||
|
file_type: the extension from the file path
|
||
|
|
||
|
if string is NOT a path, but has JSON-like special characters:
|
||
|
file_obj: the same string passed as file_obj
|
||
|
file_type: set to 'json'
|
||
|
|
||
|
if string is a valid-looking URL
|
||
|
file_obj: an open 'rb' file object with retrieved data
|
||
|
file_type: from the extension
|
||
|
|
||
|
if string is none of those:
|
||
|
raise ValueError as we can't do anything with input
|
||
|
|
||
|
if file like object:
|
||
|
ValueError will be raised if file_type is None
|
||
|
file_obj: same as input
|
||
|
file_type: same as input
|
||
|
|
||
|
if other object: like a shapely.geometry.Polygon, etc:
|
||
|
file_obj: same as input
|
||
|
file_type: if None initially, set to the class name
|
||
|
(in lower case), otherwise passed through
|
||
|
|
||
|
file_type : str
|
||
|
type of file and handled according to above
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
file_obj : file-like object
|
||
|
Contains data
|
||
|
file_type : str
|
||
|
Lower case of the type of file (eg 'stl', 'dae', etc)
|
||
|
metadata : dict
|
||
|
Any metadata gathered
|
||
|
opened : bool
|
||
|
Did we open the file or not
|
||
|
resolver : trimesh.visual.Resolver
|
||
|
Resolver to load other assets
|
||
|
"""
|
||
|
metadata = {}
|
||
|
opened = False
|
||
|
if ('metadata' in kwargs and
|
||
|
isinstance(kwargs['metadata'], dict)):
|
||
|
metadata.update(kwargs['metadata'])
|
||
|
|
||
|
if util.is_pathlib(file_obj):
|
||
|
# convert pathlib objects to string
|
||
|
file_obj = str(file_obj.absolute())
|
||
|
|
||
|
if util.is_file(file_obj) and file_type is None:
|
||
|
raise ValueError('file_type must be set for file objects!')
|
||
|
if util.is_string(file_obj):
|
||
|
try:
|
||
|
# os.path.isfile will return False incorrectly
|
||
|
# if we don't give it an absolute path
|
||
|
file_path = os.path.expanduser(file_obj)
|
||
|
file_path = os.path.abspath(file_path)
|
||
|
exists = os.path.isfile(file_path)
|
||
|
except BaseException:
|
||
|
exists = False
|
||
|
|
||
|
# file obj is a string which exists on filesystm
|
||
|
if exists:
|
||
|
# if not passed create a resolver to find other files
|
||
|
if resolver is None:
|
||
|
resolver = resolvers.FilePathResolver(file_path)
|
||
|
# save the file name and path to metadata
|
||
|
metadata['file_path'] = file_path
|
||
|
metadata['file_name'] = os.path.basename(file_obj)
|
||
|
# if file_obj is a path that exists use extension as file_type
|
||
|
if file_type is None:
|
||
|
file_type = util.split_extension(
|
||
|
file_path,
|
||
|
special=['tar.gz', 'tar.bz2'])
|
||
|
# actually open the file
|
||
|
file_obj = open(file_path, 'rb')
|
||
|
opened = True
|
||
|
else:
|
||
|
if '{' in file_obj:
|
||
|
# if a dict bracket is in the string, its probably a straight
|
||
|
# JSON
|
||
|
file_type = 'json'
|
||
|
elif 'https://' in file_obj or 'http://' in file_obj:
|
||
|
# we've been passed a URL, warn to use explicit function
|
||
|
# and don't do network calls via magical pipeline
|
||
|
raise ValueError(
|
||
|
'use load_remote to load URL: {}'.format(file_obj))
|
||
|
elif file_type is None:
|
||
|
raise ValueError('string is not a file: {}'.format(file_obj))
|
||
|
|
||
|
if file_type is None:
|
||
|
file_type = file_obj.__class__.__name__
|
||
|
|
||
|
if util.is_string(file_type) and '.' in file_type:
|
||
|
# if someone has passed the whole filename as the file_type
|
||
|
# use the file extension as the file_type
|
||
|
if 'file_path' not in metadata:
|
||
|
metadata['file_path'] = file_type
|
||
|
metadata['file_name'] = os.path.basename(file_type)
|
||
|
file_type = util.split_extension(file_type)
|
||
|
if resolver is None and os.path.exists(file_type):
|
||
|
resolver = resolvers.FilePathResolver(file_type)
|
||
|
|
||
|
# all our stored extensions reference in lower case
|
||
|
file_type = file_type.lower()
|
||
|
|
||
|
# if we still have no resolver try using file_obj name
|
||
|
if (resolver is None and
|
||
|
hasattr(file_obj, 'name') and
|
||
|
file_obj.name is not None and
|
||
|
len(file_obj.name) > 0):
|
||
|
resolver = resolvers.FilePathResolver(file_obj.name)
|
||
|
|
||
|
return file_obj, file_type, metadata, opened, resolver
|
||
|
|
||
|
|
||
|
# loader functions for compressed extensions
|
||
|
compressed_loaders = {'zip': load_compressed,
|
||
|
'tar.bz2': load_compressed,
|
||
|
'tar.gz': load_compressed}
|
||
|
|
||
|
# map file_type to loader function
|
||
|
mesh_loaders = {}
|
||
|
# assimp has a lot of loaders, but they are all quite slow
|
||
|
# load first and replace with native loaders where possible
|
||
|
mesh_loaders.update(_assimp_loaders)
|
||
|
mesh_loaders.update(_misc_loaders)
|
||
|
mesh_loaders.update(_stl_loaders)
|
||
|
mesh_loaders.update(_ctm_loaders)
|
||
|
mesh_loaders.update(_ply_loaders)
|
||
|
mesh_loaders.update(_xml_loaders)
|
||
|
mesh_loaders.update(_obj_loaders)
|
||
|
mesh_loaders.update(_off_loaders)
|
||
|
mesh_loaders.update(_collada_loaders)
|
||
|
mesh_loaders.update(_gltf_loaders)
|
||
|
mesh_loaders.update(_three_loaders)
|
||
|
mesh_loaders.update(_xyz_loaders)
|
||
|
|
||
|
# collect loaders which return voxel types
|
||
|
voxel_loaders = {}
|
||
|
voxel_loaders.update(_binvox_loaders)
|