import copy
import numpy as np
import matplotlib as mpl
import matplotlib.cm as cm
from cis_interface import backwards
from cis_interface.serialize.DefaultSerialize import DefaultSerialize
[docs]class PlyDict(dict):
r"""Class for storing ply information.
Args:
vertices (list, optional): 3D positions of vertices comprising the
3D object. Defaults to [].
faces (list, optional): Indices of 3 or more vertices making up faces.
Defaults to [].
vertex_colors (list, optional): RGB values for each of the vertices.
If not provided, all vertices will be black. Defaults to [].
"""
def __init__(self, **kwargs):
list_keys = ['vertices', 'faces', 'vertex_colors']
for k in list_keys:
if kwargs.get(k, None) is None:
kwargs.setdefault(k, [])
super(PlyDict, self).__init__(**kwargs)
@property
def nvert(self):
r"""int: Number of vertices."""
return len(self['vertices'])
@property
def nface(self):
r"""int: Number of faces."""
return len(self['faces'])
@property
def bounds(self):
r"""tuple: Minimums and maximums of vertices."""
mins = 1e6 * np.ones(3, 'float')
maxs = -1e6 * np.ones(3, 'float')
for v in self['vertices']:
mins = np.minimum(mins, np.array(v))
maxs = np.maximum(maxs, np.array(v))
return mins, maxs
@property
def mesh(self):
r"""list: Vertices for each face in the structure."""
mesh = []
for f in self['faces']:
imesh = []
for i in range(3):
imesh += self['vertices'][f[i]]
mesh.append(imesh)
return mesh
[docs] @classmethod
def from_shape(cls, shape, d, conversion=1.0):
r"""Create a ply 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.
"""
iply = None
d.process(shape)
if d.result is not None:
iply = cls()
# Vertices
for p in d.result.pointList:
iply['vertices'].append([conversion * p.x,
conversion * p.y,
conversion * p.z])
# Colors
if d.result.colorPerVertex and d.result.colorList:
if d.result.isColorIndexListToDefault():
for c in d.result.colorList:
iply['vertex_colors'].append([c.red,
c.green,
c.blue])
else: # pragma: debug
raise Exception("Indexed vertex colors not supported.")
# elif not shape.appearance.isAmbientToDefault():
# c = shape.appearance.ambient
# icolor = [c.red, c.green, c.blue]
# iply['vertex_colors'] += [icolor for p in d.result.pointList]
# Material
if (shape.appearance.name != shape.appearance.DEFAULT_MATERIAL.name):
iply['material'] = shape.appearance.name
# Faces
for i3 in d.result.indexList:
iply['faces'].append([i3[0], i3[1], i3[2]])
return iply
[docs] @classmethod
def from_scene(cls, scene, d=None, conversion=1.0, default_rgb=None):
r"""Create a ply 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.
default_rgb (list, optional): Default color in RGB that should be
used for missing colors. Defaults to [0, 0, 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()
iply = cls.from_shape(shape, d, conversion=conversion)
if iply is not None:
out.append(iply, default_rgb=default_rgb)
d.clear()
return out
[docs] def to_scene(self, conversion=1.0, name=None):
r"""Create a PlantGL scene from a Ply 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):
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 = []
for v in self['vertices']:
xarr = conversion * np.array(v)
obj_points.append(pgl.Vector3(xarr[0], xarr[1], xarr[2]))
points = pgl.Point3Array(obj_points)
# Add indices
obj_indices = []
nind = None
index_class = pgl.Index3
array_class = pgl.Index3Array
smb_class = pgl.TriangleSet
for f in self['faces']:
if nind is None:
nind = len(f)
if nind == 3:
pass
else:
raise ValueError("No PlantGL class for faces with %d vertices."
% nind)
else:
if len(f) != nind:
raise ValueError("Faces do not all contain %d vertices." % nind)
f_int = [int(_f) for _f in f]
obj_indices.append(index_class(*f_int))
indices = array_class(obj_indices)
# Add colors
if self['vertex_colors']:
obj_colors = []
for c in self['vertex_colors']:
assert(len(c) == 3)
obj_colors.append(pgl.Color4(c[0], c[1], c[2], 1))
colors = pgl.Color4Array(obj_colors)
kwargs['colorList'] = colors
kwargs['colorPerVertex'] = True
args = (points, indices)
return smb_class, args, kwargs
[docs] def set_vertex_colors(self, default_rgb=None):
r"""Set the vertex colors to a default if they are not yet set.
Args:
default_rgb (list, optional): Default color in RGB that should be
used for missing colors. Defaults to [0, 0, 0].
"""
if len(self['vertex_colors']) == self.nvert:
return
if default_rgb is None:
default_rgb = [0, 0, 0]
self['vertex_colors'] = [default_rgb for _ in range(self.nvert)]
[docs] def append(self, solf, default_rgb=None):
r"""Append new ply information to this dictionary.
Args:
solf (PlyDict): Another ply to append to this one.
default_rgb (list, optional): Default color in RGB that should be
used for missing colors. Defaults to [0, 0, 0].
"""
do_colors = False
if self['vertex_colors'] or solf['vertex_colors']:
self.set_vertex_colors(default_rgb=default_rgb)
solf.set_vertex_colors(default_rgb=default_rgb)
do_colors = True
# Vertex fields
nvert = self.nvert
self['vertices'] += solf['vertices']
if do_colors:
self['vertex_colors'] += solf['vertex_colors']
# Face fields
for f in solf['faces']:
self['faces'].append([v + nvert for v in f])
[docs] def merge(self, ply_list, no_copy=False, default_rgb=None):
r"""Merge a list of ply dictionaries.
Args:
ply_list (list): Ply dictionaries.
no_copy (bool, optional): If True, the current dictionary will be
updated, otherwise a copy will be returned with the update.
Defaults to False.
default_rgb (list, optional): Default color in RGB that should be
used for missing colors. Defaults to [0, 0, 0].
Returns:
dict: Merged ply dictionary.
"""
if not isinstance(ply_list, list):
ply_list = [ply_list]
# Merge fields
if no_copy:
out = self
else:
out = copy.deepcopy(self)
for x in ply_list:
out.append(x, default_rgb=default_rgb)
return out
[docs] def apply_scalar_map(self, scalar_arr, color_map=None,
vmin=None, vmax=None, scaling='linear',
scale_by_area=False, no_copy=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: Ply with updated vertex colors.
"""
# Scale by area
if scale_by_area:
scalar_arr = copy.deepcopy(scalar_arr)
for i in range(len(self['faces'])):
f = self['faces'][i]
v0 = np.array(self['vertices'][f[0]])
v1 = np.array(self['vertices'][f[1]])
v2 = np.array(self['vertices'][f[2]])
a = np.sqrt(np.sum((v0 - v1)**2))
b = np.sqrt(np.sum((v1 - v2)**2))
c = np.sqrt(np.sum((v2 - v0)**2))
s = (a + b + c) / 2.0
area = np.sqrt(s * (s - a) * (s - b) * (s - c))
scalar_arr[i] = area * scalar_arr[i]
# Map vertices onto faces
vertex_scalar = [[] for x in self['vertices']]
for i in range(len(self['faces'])):
for v in self['faces'][i]:
vertex_scalar[v].append(scalar_arr[i])
for i in range(len(vertex_scalar)):
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()
# print(vmin, vmax)
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)
# Scale colors
vertex_colors = (255 * m.to_rgba(vertex_scalar)).astype('int')[:, :3].tolist()
if no_copy:
out = self
else:
out = copy.deepcopy(self)
out['vertex_colors'] = vertex_colors
return out
[docs]class PlySerialize(DefaultSerialize):
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'.
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.
"""
def __init__(self, *args, **kwargs):
self.write_header = kwargs.pop('write_header', True)
self.newline = backwards.bytes2unicode(kwargs.pop('newline', '\n'))
self.default_rgb = [0, 0, 0]
super(PlySerialize, self).__init__(*args, **kwargs)
@property
def serializer_type(self):
r"""int: Type of serializer."""
return 8
@property
def empty_msg(self):
r"""obj: Object indicating empty message."""
return backwards.unicode2bytes('')
[docs] def func_serialize(self, args):
r"""Serialize a message.
Args:
args (PlyDict): Dictionary of ply information.
Returns:
bytes, str: Serialized message.
"""
lines = []
if isinstance(args, dict):
args = PlyDict(**args)
nvert = args.nvert
nface = args.nface
# Header
if self.write_header:
lines += ['ply',
'format ascii 1.0',
'comment author cis_auto',
'comment File generated by cis_interface',
'element vertex %d' % nvert,
'property float x',
'property float y',
'property float z']
if args.get('vertex_colors', []):
lines += ['property uchar diffuse_red',
'property uchar diffuse_green',
'property uchar diffuse_blue']
lines += ['element face %d' % nface,
'property list uchar int vertex_indices',
'end_header']
# 3D objects
if args.get('vertex_colors', []):
for i in range(args.nvert):
v = args['vertices'][i]
c = args['vertex_colors'][i]
entry = tuple(list(v) + list(c))
lines.append('%6.4f %6.4f %6.4f %d %d %d' % entry)
else:
for i in range(args.nvert):
v = args['vertices'][i]
entry = tuple(list(v))
lines.append('%6.4f %6.4f %6.4f' % entry)
for f in args.get('faces', []):
nv = len(f)
iline = '%d' % nv
for v in f:
iline += ' %d' % v
lines.append(iline)
out = self.newline.join(lines) + self.newline
return backwards.unicode2bytes(out)
[docs] def func_deserialize(self, msg, nvert=None, nface=None,
do_vertex_colors=False):
r"""Deserialize a message.
Args:
msg (str, bytes): Message to be deserialized.
nvert (int, optional): Number of vertices expected if the ply
header is not in the message. Defaults to None.
nface (int, optional): Number of faces expected if the ply
header is not in the message. Defaults to None.
do_vertex_colors (bool, optional): If True the vertex color
will be contained in the vertex information if the ply
header is not in the message. Defaults to False.
Returns:
dict: Deserialized .ply information.
"""
if len(msg) == 0:
out = self.empty_msg
else:
lines = backwards.bytes2unicode(msg).split(self.newline)
# Split header and body
headline = 0
for i in range(len(lines)):
if 'end_header' in lines[i]:
headline = i + 1
break
if headline > 0:
element = None
for i in range(headline):
if lines[i].startswith('element'):
parts = lines[i].split()
element = parts[1]
if element == 'vertex':
nvert = int(parts[2])
elif element == 'face':
nface = int(parts[2])
elif element == 'vertex':
if 'green' in lines[i]:
do_vertex_colors = True
if (nvert is None) or (nface is None): # pragma: debug
raise RuntimeError("Could not locate element definitions.")
# Get 3D info
out = PlyDict()
i = headline
while out.nvert < nvert:
values = lines[i].split()
if len(values) > 0:
out['vertices'].append([x for x in map(float, values[:3])])
if do_vertex_colors:
iclr = self.default_rgb
if len(values) >= 6:
iclr = [x for x in map(int, values[3:])]
out['vertex_colors'].append(iclr)
i += 1
while out.nface < nface:
values = lines[i].split()
if len(values) > 0:
nv = int(values[0])
out['faces'].append([x for x in map(int, values[1:(nv + 1)])])
for x in out['faces'][-1]:
assert(x < out.nvert)
i += 1
return out