Source code for yggdrasil.serialize.PlySerialize

import copy
import numpy as np
from yggdrasil import constants, rapidjson
from yggdrasil.serialize.SerializeBase import SerializeBase


[docs]class GeometryBase: r"""Base class for extening rapidjson geometry classes."""
[docs] @classmethod def from_shape(cls, shape, d, conversion=1.0, _as_obj=False): # pragma: lpy r"""Create a geometry dictionary from a PlantGL shape and descritizer. Args: scene (openalea.plantgl.scene): Scene that should be descritized. d (openalea.plantgl.descritizer): Descritizer. conversion (float, optional): Conversion factor that should be applied to the vertex positions. Defaults to 1.0. """ d.process(shape) if d.result is not None: out = {} # Vertices for p in d.result.pointList: new_vert = {} for k in ['x', 'y', 'z']: new_vert[k] = conversion * getattr(p, k) out['vertices'].append(new_vert) # Colors if d.result.colorPerVertex and d.result.colorList: if d.result.isColorIndexListToDefault(): for i, c in enumerate(d.result.colorList): for k in ['red', 'green', 'blue']: out['vertices'][i][k] = getattr(c, k) else: # pragma: debug raise Exception("Indexed vertex colors not supported.") # elif not shape.appearance.isAmbientToDefault(): # c = shape.appearance.ambient # for k in ['red', 'green', 'blue']: # for v in out['vertices']: # v[k] = getattr(c, k) # Material if (shape.appearance.name != shape.appearance.DEFAULT_MATERIAL.name): out['material'] = shape.appearance.name # Faces if _as_obj: for i3 in d.result.indexList: out['faces'].append([{'vertex_index': i3[j]} for j in range(len(i3))]) else: for i3 in d.result.indexList: out['faces'].append({'vertex_index': [i3[j] for j in range(len(i3))]}) return cls.from_dict(out)
[docs] @classmethod def from_scene(cls, scene, d=None, conversion=1.0): # pragma: lpy r"""Create a geometry dictionary from a PlantGL scene and descritizer. Args: scene (openalea.plantgl.scene): Scene that should be descritized. d (openalea.plantgl.descritizer, optional): Descritizer. Defaults to openalea.plantgl.all.Tesselator. conversion (float, optional): Conversion factor that should be applied to the vertex positions. Defaults to 1.0. """ if d is None: from openalea.plantgl.all import Tesselator d = Tesselator() out = cls() for k, shapes in scene.todict().items(): for shape in shapes: d.clear() igeom = cls.from_shape(shape, d, conversion=conversion) if igeom is not None: out.append(igeom) d.clear() return out
[docs] def to_scene(self, conversion=1.0, name=None): # pragma: lpy r"""Create a PlantGL scene from a geometry dictionary. Args: conversion (float, optional): Conversion factor that should be applied to the vertices. Defaults to 1.0. name (str, optional): Name that should be given to the created PlantGL symbol. Defaults to None and is ignored. Returns: """ import openalea.plantgl.all as pgl smb_class, args, kwargs = self.to_geom_args(conversion=conversion, name=name) smb = smb_class(*args, **kwargs) if name is not None: smb.setName(name) if self.get('material', None) is not None: mat = pgl.Material(self['material']) shp = pgl.Shape(smb, mat) else: shp = pgl.Shape(smb) if name is not None: shp.setName(name) scn = pgl.Scene([shp]) return scn
[docs] def to_geom_args(self, conversion=1.0, name=None, _as_obj=False): # pragma: lpy r"""Get arguments for creating a PlantGL geometry. Args: conversion (float, optional): Conversion factor that should be applied to the vertices. Defaults to 1.0. name (str, optional): Name that should be given to the created PlantGL symbol. Defaults to None and is ignored. Returns: tuple: Class, arguments and keyword arguments for PlantGL geometry. """ import openalea.plantgl.all as pgl kwargs = dict() # Add vertices obj_points = [] obj_colors = [] for v in self.get('vertices', []): xarr = conversion * np.array([v[k] for k in ['x', 'y', 'z']]) obj_points.append(pgl.Vector3(np.float64(xarr[0]), np.float64(xarr[1]), np.float64(xarr[2]))) c = [v.get(k, None) for k in ['red', 'green', 'blue']] if None not in c: cast_type = int obj_colors.append(pgl.Color4(cast_type(c[0]), cast_type(c[1]), cast_type(c[2]), cast_type(1))) points = pgl.Point3Array(obj_points) if obj_colors: colors = pgl.Color4Array(obj_colors) kwargs['colorList'] = colors kwargs['colorPerVertex'] = True # Add indices obj_indices = [] index_class = pgl.Index array_class = pgl.IndexArray smb_class = pgl.FaceSet # index_class = pgl.Index3 # array_class = pgl.Index3Array # smb_class = pgl.TriangleSet for f in self.get('faces', []): if _as_obj: f_int = [int(_f['vertex_index']) for _f in f] else: f_int = [int(_f) for _f in f['vertex_index']] obj_indices.append(index_class(*f_int)) indices = array_class(obj_indices) args = (points, indices) return smb_class, args, kwargs
[docs] def apply_scalar_map(self, scalar_arr, color_map=None, vmin=None, vmax=None, scaling='linear', scale_by_area=False, no_copy=False, _as_obj=False): r"""Set the color of faces in a 3D object based on a scalar map. This creates a copy unless no_copy is True. Args: scalar_arr (arr): Scalar values that should be mapped to colors for each face. color_map (str, optional): The name of the color map that should be used. Defaults to 'plasma'. vmin (float, optional): Value that should map to the minimum of the colormap. Defaults to min(scalar_arr). vmax (float, optional): Value that should map to the maximum of the colormap. Defaults to max(scalar_arr). scaling (str, optional): Scaling that should be used to map the scalar array onto the colormap. Defaults to 'linear'. scale_by_area (bool, optional): If True, the elements of the scalar array will be multiplied by the area of the corresponding face. If True, vmin and vmax should be in terms of the scaled array. Defaults to False. no_copy (bool, optional): If True, the returned object will not be a copy. Defaults to False. Returns: dict: Geometry with updated vertex colors. """ from matplotlib import cm from matplotlib import colors as mpl_colors # Scale by area if scale_by_area: areas = np.array(self.areas) scalar_arr = scalar_arr * areas # Map vertices onto faces vertex_scalar = [[] for x in self['vertices']] for i in range(len(self.get('faces', []))): for v in self.get('faces', [])[i]['vertex_index']: vertex_scalar[v].append(scalar_arr[i]) for i in range(len(vertex_scalar)): if len(vertex_scalar[i]) == 0: vertex_scalar[i] = 0 else: vertex_scalar[i] = np.mean(vertex_scalar[i]) vertex_scalar = np.array(vertex_scalar) if scaling == 'log': vertex_scalar = np.ma.MaskedArray(vertex_scalar, vertex_scalar <= 0) # Get color scaling if color_map is None: # color_map = 'summer' color_map = 'plasma' if vmin is None: vmin = vertex_scalar.min() if vmax is None: vmax = vertex_scalar.max() # Scale colors if isinstance(vmin, np.ma.core.MaskedConstant): assert isinstance(vmax, np.ma.core.MaskedConstant) vertex_colors = np.zeros((len(vertex_scalar), 3), 'int') else: cmap = cm.get_cmap(color_map) if scaling == 'log': norm = mpl_colors.LogNorm(vmin=vmin, vmax=vmax) elif scaling == 'linear': norm = mpl_colors.Normalize(vmin=vmin, vmax=vmax) else: # pragma: debug raise Exception("Scaling must be 'linear' or 'log'.") m = cm.ScalarMappable(norm=norm, cmap=cmap) vertex_colors = (255 * m.to_rgba(vertex_scalar)).astype( 'int')[:, :3] if no_copy: out = self else: out = copy.deepcopy(self) out.add_colors("vertices", vertex_colors.tolist()) return out
[docs]class PlyDict(GeometryBase, rapidjson.geometry.Ply): r"""Enhanced dictionary class for storing Ply information.""" pass
[docs]class PlySerialize(SerializeBase): r"""Class for serializing/deserializing .ply file formats. Args: write_header (bool, optional): If True, headers will be added to serialized output. Defaults to True. newline (str, optional): String that should be used for new lines. Defaults to '\n'. prune_duplicates (bool, optional): If True, serialized meshes in array format will be pruned of duplicates when being normalized into a Ply object. If False, duplicates will not be pruned. Defaults to True. Attributes: write_header (bool): If True, headers will be added to serialized output. newline (str): String that should be used for new lines. default_rgb (list): Default color in RGB that should be used for missing colors. """ _seritype = 'ply' _schema_subtype_description = ( 'Serialize 3D structures using `Ply format ' '<http://paulbourke.net/dataformats/ply/>`_.') _schema_properties = { 'newline': {'type': 'string', 'default': constants.DEFAULT_NEWLINE_STR}, 'prune_duplicates': {'type': 'boolean', 'default': True}} default_datatype = {'type': 'ply'} file_extensions = ['.ply'] concats_as_str = False def __init__(self, *args, **kwargs): r"""Initialize immediately as default is only type.""" super(PlySerialize, self).__init__(*args, **kwargs) @property def initialized(self): r"""bool: True if the serializer has been initialized.""" return True
[docs] def func_serialize(self, args): r"""Serialize a message. Args: args: List of arguments to be formatted or numpy array to be serialized. Returns: bytes: Serialized message. """ assert isinstance(args, PlyDict) return str(args).encode("utf-8")
[docs] def func_deserialize(self, msg): r"""Deserialize a message. Args: msg (bytes): Message to be deserialized. Returns: obj: Deserialized message. """ return PlyDict(msg)
[docs] @classmethod def is_mesh(cls, args): r"""Check if an object is a 3D mesh with x, y, z for vertices in each face in rows. Args: args (object): Object to check. Returns: bool: True if object is a mesh, false otherwise. """ if isinstance(args, list): args = np.asarray(args) return (isinstance(args, np.ndarray) and ((args.ndim == 2 and (args.shape[1] // 3) > 0 and (args.shape[1] % 3) == 0) or (args.ndim == 1 and (len(args.dtype) // 3) > 0 and (len(args.dtype) % 3) == 0)))
[docs] def normalize(self, args): r"""Normalize a message to conform to the expected datatype. Args: args (object): Message arguments. Returns: object: Normalized message. """ if isinstance(args, PlyDict): return args elif self.is_mesh(args): return PlyDict.from_mesh( args, prune_duplicates=self.prune_duplicates) return PlyDict(super(PlySerialize, self).normalize(args))
[docs] @classmethod def concatenate(cls, objects, **kwargs): r"""Concatenate objects to get object that would be recieved if the concatenated serialization were deserialized. Args: objects (list): Objects to be concatenated. **kwargs: Additional keyword arguments are ignored. Returns: list: Set of objects that results from concatenating those provided. """ if len(objects) == 0: return [] total = type(objects[0])(objects[0]) out = total.merge(objects[1:], no_copy=True) return [out]
[docs] @classmethod def get_testing_options(cls, **kwargs): r"""Method to return a dictionary of testing options for this class. Returns: dict: Dictionary of variables to use for testing. """ out = super(PlySerialize, cls).get_testing_options() obj = PlyDict( {'vertices': [ {'x': float(0), 'y': float(0), 'z': float(0)}, {'x': float(0), 'y': float(0), 'z': float(1)}, {'x': float(0), 'y': float(1), 'z': float(1)}, {'x': float(1), 'y': float(1), 'z': float(2)}, {'x': float(1), 'y': float(1), 'z': float(1)}], 'faces': [ {'vertex_index': [int(0), int(1), int(2)]}, {'vertex_index': [int(1), int(2), int(3)]}, {'vertex_index': [int(0), int(1), int(2), int(3)]}], 'comments': ["author ygg_auto", "File generated by yggdrasil"]}) out.update(objects=[obj, obj], empty={}, invalid='hello', contents=(b'ply\n' + b'format ascii 1.0\n' + b'comment author ygg_auto\n' + b'comment File generated by yggdrasil\n' + b'element vertex 10\n' + b'property double x\n' + b'property double y\n' + b'property double z\n' + b'element face 6\nproperty list uchar int vertex_index\n' + b'end_header\n' + b'0 0 0\n' + b'0 0 1\n' + b'0 1 1\n' + b'1 1 2\n' + b'1 1 1\n' + b'0 0 0\n' + b'0 0 1\n' + b'0 1 1\n' + b'1 1 2\n' + b'1 1 1\n' + b'3 0 1 2\n' + b'3 1 2 3\n' + b'4 0 1 2 3\n' + b'3 5 6 7\n' + b'3 6 7 8\n' + b'4 5 6 7 8\n')) out['concatenate'] = [([], [])] return out