Source code for cis_interface.serialize.ObjSerialize

import copy
from cis_interface import backwards
from cis_interface.serialize.PlySerialize import PlyDict, PlySerialize


[docs]class ObjDict(PlyDict): r"""Class for storing obj 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 or a tuple containing the indices for the position, texture coordinate, and normal for each vertex in the face. This information can also be provided in their own lists, but there must be an entry for every face. Defaults to []. vertex_colors (list, optional): RGB values for each of the vertices. If not provided, all vertices will be black. Defaults to []. material (str, optional): Material to use for faces. Defaults to None. normals (list, optional): 3D normals for vertices. Defaults to []. texcoords (list, optional): 3D texture coordinates for vertices. Defaults to []. face_texcoords (list, optional): Indices of texture coordinates for each vertex in the face. Entries of None are ignored. Defaults to []. face_normals (list, optional): Indices of normals for each vertex in the face. Entries of None are ignored. Defaults to []. """ def __init__(self, **kwargs): list_keys = ['vertices', 'faces', 'vertex_colors', 'normals', 'texcoords', 'face_texcoords', 'face_normals'] for k in list_keys: if kwargs.get(k, None) is None: kwargs.setdefault(k, []) kwargs.setdefault('material', None) super(ObjDict, self).__init__(**kwargs) self.update(**self.standardize())
[docs] def standardize(self, no_copy=False): r"""Put the dictionary in the standard format with face information split into separate fields. Args: no_copy (bool, optional): If True, the current dictionary will be updated. Otherwise a copy will be returned. Defaults to False. Returns: ObjDict: Standardized obj information. """ if no_copy: out = self else: out = copy.deepcopy(self) # Convert face tuples to lists face_keys = {'faces': 0, 'face_texcoords': 1, 'face_normals': 2} for k in ['face_texcoords', 'face_normals']: if not out.get(k, []): out[k] = [[None for v in f] for f in self['faces']] for i, f in enumerate(self['faces']): for k in ['face_texcoords', 'face_normals']: if out[k][i] is None: out[k][i] = [None for v in f] for j, v in enumerate(f): if issubclass(v.__class__, (int, float)): out['faces'][i][j] = v else: for k, kindex in face_keys.items(): out[k][i][j] = v[kindex] return out
[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. """ iobj = super(ObjDict, cls).from_shape(shape, d, conversion=conversion) if iobj is not None: # Texcoords if d.result.texCoordList: for t in d.result.texCoordList: # TODO: Should the coords be scaled? iobj['texcoords'].append([t.x, t.y]) if d.result.texCoordIndexList: for t in d.result.texCoordIndexList: if t[0] < len(iobj['texcoords']): iobj['face_texcoords'].append([t[0], t[1], t[2]]) else: iobj['face_texcoords'].append([None, None, None]) # Normals if d.result.normalList: for n in d.result.normalList: iobj['normals'].append([n.x, n.y, n.z]) if d.result.texCoordIndexList: for n in d.result.texCoordIndexList: if n[0] < len(iobj['normals']): iobj['face_normals'].append([n[0], n[1], n[2]]) else: iobj['face_normals'].append([None, None, None]) return iobj
[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 smb_class, args, kwargs = super(ObjDict, self).to_geom_args( conversion=conversion, name=name) index_class = pgl.Index3 array_class = pgl.Index3Array # Texture coords if self.get('texcoords', []): obj_texcoords = [] for t in self['texcoords']: obj_texcoords.append(pgl.Vector2(t[0], t[1])) kwargs['texCoordList'] = pgl.Point2Array(obj_texcoords) if self.get('face_texcoords', []): obj_ftexcoords = [] for t in self['face_texcoords']: if (t is not None) and (t[0] is not None): entry = [int(_t) for _t in t] else: entry = [len(self['texcoords']) for _ in range(3)] obj_ftexcoords.append(index_class(*entry)) kwargs['texCoordIndexList'] = array_class(obj_ftexcoords) # Normals if self.get('normals', []): obj_normals = [] for n in self['normals']: obj_normals.append(pgl.Vector3(n[0], n[1], n[2])) kwargs['normalList'] = pgl.Point3Array(obj_normals) if self.get('face_normals', []): obj_fnormals = [] for n in self['face_normals']: if (n is not None) and (n[0] is not None): entry = [int(_n) for _n in n] else: entry = [len(self['normals']) for _ in range(3)] obj_fnormals.append(index_class(*entry)) kwargs['normalIndexList'] = array_class(obj_fnormals) return smb_class, args, kwargs
[docs] def append(self, solf, default_rgb=None): r"""Append new ply information to this dictionary. Args: solf (ObjDict): 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]. """ super(ObjDict, self).append(solf, default_rgb=default_rgb) # Merge material using first in list material = None for x in [self, solf]: if x['material'] is not None: material = x['material'] break self['material'] = material # Merge vertex things for k in ['normals', 'texcoords']: fk = 'face_' + k nprev = len(self[k]) self[k] += solf[k] for f in solf[fk]: fnew = [] for v in f: if v is None: fnew.append(v) else: fnew.append(v + nprev) self[fk].append(fnew) return self
[docs]class ObjSerialize(PlySerialize): r"""Class for serializing/deserializing .obj file formats. Reader adapted from https://www.pygame.org/wiki/OBJFileLoader.""" @property def serializer_type(self): r"""int: Type of serializer.""" return 9
[docs] def func_serialize(self, args, zero_indexed=True): r"""Serialize a message. Args: args (ObjDict): Dictionary of obj information. zero_indexed (bool, optional): If True, the input indices are assumed to start at zero and they will be adjusted to start at one and conform with .obj format. If False, the input indices are assumed to start at one and they will not be adjusted. Defaults to True. Returns: bytes, str: Serialized message. """ lines = [] fkey_order = ['faces', 'face_texcoords', 'face_normals'] # Standardize if isinstance(args, dict): args = ObjDict(**args) sargs = args.standardize() # Header if self.write_header: lines += ['# Author cis_auto', '# Generated by cis_interface', ''] if sargs.get('material', None) is not None: lines.append('usemtl %s' % sargs['material']) if 'vertices' in sargs: if not sargs.get('vertex_colors', []): for v in sargs['vertices']: lines.append('v %6.4f %6.4f %6.4f' % tuple(v)) else: for i in range(len(sargs['vertices'])): line = 'v' line += ' %6.4f %6.4f %6.4f' % tuple(sargs['vertices'][i]) line += ' %d %d %d' % tuple(sargs['vertex_colors'][i]) lines.append(line) for v in sargs['normals']: lines.append('vn %6.4f %6.4f %6.4f' % tuple(v)) for v in sargs['texcoords']: lines.append('vt %6.4f %6.4f' % tuple(v)) # Faces add_ind = 0 if zero_indexed: add_ind = 1 for i in range(sargs.nface): iline = 'f' for j in range(len(sargs['faces'][i])): v = [sargs[k][i][j] for k in fkey_order] iline += ' %d/' % (v[0] + add_ind) if v[1] is not None: iline += '%d' % (v[1] + add_ind) iline += '/' if v[2] is not None: iline += '%d' % (v[2] + add_ind) lines.append(iline) out = self.newline.join(lines) + self.newline return backwards.unicode2bytes(out)
[docs] def func_deserialize(self, msg, zero_indexed=True): r"""Deserialize a message. Args: msg (str, bytes): Message to be deserialized. zero_indexed (bool, optional): If True, the parsed indices are adjusted to start at zero. If False, the indices will not be adjusted and will start at one as per .obj format. Defaults to True. Returns: ObjDict: Deserialized .obj information. The faces are zero indexed. """ if len(msg) == 0: out = self.empty_msg else: lines = backwards.bytes2unicode(msg).split(self.newline) out = ObjDict() nvert = 0 for line in lines: if line.startswith('#'): continue values = line.split() if not values: continue if values[0] == 'v': out['vertices'].append([x for x in map(float, values[1:4])]) iclr = self.default_rgb if len(values) == 7: if not out['vertex_colors']: out['vertex_colors'] = [self.default_rgb for _ in range(nvert)] iclr = [x for x in map(int, values[4:7])] if out['vertex_colors']: out['vertex_colors'].append(iclr) nvert += 1 elif values[0] == 'vn': out['normals'].append([x for x in map(float, values[1:4])]) elif values[0] == 'vt': out['texcoords'].append([x for x in map(float, values[1:3])]) elif values[0] in ('usemtl', 'usemat'): out['material'] = values[1] elif values[0] == 'f': sub_ind = 0 if zero_indexed: sub_ind = 1 face = [] texcoords = [] norms = [] for v in values[1:]: w = v.split('/') face.append(int(w[0]) - sub_ind) itexc = None inorm = None if len(w) >= 2 and len(w[1]) > 0: itexc = int(w[1]) - sub_ind texcoords.append(itexc) if len(w) >= 3 and len(w[2]) > 0: inorm = int(w[2]) - sub_ind norms.append(inorm) out['faces'].append(face) out['face_texcoords'].append(texcoords) out['face_normals'].append(norms) for x in out['faces'][-1]: assert(x <= (len(out['vertices']) - sub_ind)) out.standardize(no_copy=True) return out