Source code for blueprints.cache

import os
import sys
import struct
import xml.etree.ElementTree as xml
import numpy as np
import blueprints as blue
from collections import defaultdict
from itertools import count
from imageio import imread



[docs] class BaseCache(blue.UniqueThing, blue.CacheType): """ :class:`Caches <BaseCache>` are used to store large amounts of data. For blueprints to allow for the modification of this data, it has to be loaded to memory. If this data is part of a Thing which is copied multiple times, this would drastically increase the memory usage for redundant data. Caches solve this, by implementing a single source of data for multiple copies of a Thing. Thing classes that contain large amounts of data can simply outsource them to a cache (by linking the outsourced properties to the caches properties) while keeping small attributes that are modified frequently. To ensure data integrity, if a Things property which is stored in its :class:`Cache <BaseCache>` is modified, a new copy of the caches content is created replacing the old ``cache`` attribute of the Thing (unless only one Thing uses the Cache, in which case the Cache is modified directly). For example, consider the following scenario: .. code-block:: python :caption: Cache example — The large data attribute is outsourced to a Cache >>> one = SomeThing(data=open('large.file').read()) >>> one.cache SomeCache<None:2> >>> one.data is one.cache.data True >>> two = one.copy() >>> one.cache is two.cache True >>> one.name = 'one' >>> two.name = 'two' >>> one.cache is two.cache True >>> two.data = open('other.file').read() >>> one.cache is two.cache False """
[docs] def __init__(self, **kwargs): """ Parameters ---------- **kwargs Keyword arguments are passed to ``super().__init__``. """ self._DEPENDENCY_FLAGS = defaultdict(lambda: True) super().__init__(**kwargs)
def _flag_dependencies(self, attr): """ This method updates the flags for deriving attributes, notifying them that their value mus be updated. Parameters ---------- attr : str name of the attribute """ if hasattr(self, '_DEPENDENCIES') and hasattr(self, '_DEPENDENCY_FLAGS'): for dependency in self._DEPENDENCIES[attr]: self._DEPENDENCY_FLAGS[dependency] = False def _validate(self, attr): """ Indicates whether the attribute has to be updated. Parameters ---------- attr : str name of the attribute """ return not hasattr(self, '_DEPENDENCY_FLAGS') or self._DEPENDENCY_FLAGS[attr]
[docs] class MeshCache(blue.MeshCacheType, BaseCache): """ This class stores the mesh data which might be loaded from a file and is too large to be copied frequently. Attributes ---------- built : bool A flag indicating whether the MeshAssets has been built. This is necessary since multiple :class:`MeshAssets <blueprints.assets.MeshAsset>` might use the same :class:`MeshCache` in which case it should be built only once. centered : bool If ``True`` the vertecies are normalized such that their mean position is the reference frames origin. vertecies : list The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the spatial dimensions. texcoords : list The list of texture coordinates. texcoords_idx : dict A dictionary used to link texture coordinates with face vertecies. normals : list The normals used by mujoco, this attribute is derived and should not be used for modification, see :attr:`face_normals` or :attr:`vertex_normals` for this instead. face_normals : list Face normals are the normal vectors of faces. They specify the direction in which the face points. vertex_normals : list Vertex normals are the normal vectors of vertecies. They specify the direction in which the vertecies of a face point. Specifying vertex normals instead of face normals enables mujoco to renderer soft edges. normals_idx : TYPE A dictionary used to link normals with faces. faces : list This attribute is used to write to xml. It returns a flattened list of indecies that is written raw to xml. For a structured list of faces see :attr:`faces`. filename : str The user specified file name. """
[docs] @blue.restrict def __init__(self, vertecies: np.ndarray|list[np.ndarray|list[float|int]]|None = None, faces: list[np.ndarray|list[float|int]]|None = None, filename: str|None = None, centered: bool = False, **kwargs) -> None: """ Parameters ---------- vertecies : np.ndarray | list[np.ndarray | list[float | int]] | None, optional A list of all vertex positions. faces : list[np.ndarray | list[float | int]] | None, optional A list of all faces. A face consists of at least 3 indecies of vertecies. file : str | None, optional The filename from which the mesh data is loaded and to to which the mesh will be saved. The file is saved in a special directory such that no input file is overwritten by the output file. centered : bool, optional If ``True`` the vertecies are normalized such that their mean position is the reference frames origin. **kwargs Keyword arguments are passed to ``super().__init__``. """ self.centered = centered self.filename = filename self.texcoords = None self.texcoords_idx = None self.normals = None self.normals_idx = None self.face_normals = None self.vertex_normals = None self._DEPENDENCIES = {'vertecies': ('vertecies_minimum', 'vertecies_center', 'vertecies_maximum')} super().__init__(**kwargs) if filename is not None: self.load(filename) elif vertecies is not None: self.vertecies = vertecies self.faces = faces self._built = False if centered and hasattr(self, '_vertecies'): self.vertecies = self.vertecies - self.vertecies_center #blue.REGISTER.caches.append(self) if 'name' not in kwargs: self._name = None
[docs] @blue.restrict def copy(self, **kwargs) -> blue.CacheType: """ This method constructs a copy of the Cache with possible alterations to its attributes as specified in kwargs. Parameters ---------- **kwargs Keyword arguments are passed to the :meth:`__init__` to replace the attributes of the current Thing from which copy is called. Returns ------- blue.CacheType A new instance of the Cache """ mesh = super().copy(**kwargs) if 'name' not in kwargs: mesh._name = self._name return mesh
@blue.restrict def _build(self, dirname: str, **kwargs) -> None: """ This method is called by the Assets parent to construct the xml representations of the kinematic tree. Parameters ---------- dirname : str The directory name in which the mesh file is saved. **kwargs Dummy argument """ if not self._built: pathname, basename = os.path.split(self.filename) if not pathname.endswith(dirname): path = f'{dirname}/{self.ID}_{basename}' else: path = self.filename self.save(path) self._path = path self._built = True @blue.restrict @classmethod def _from_xml_element(cls, xml_element) -> blue.CacheType: """ This method reconstructs an MeshCache from an xml element. Parameters ---------- xml_element : xml.Element The xml element from which a MeshCache is reconstructed. Returns ------- blue.CacheType The reconstructed MeshCache. """ init_args, post_args, rest_args = cls._xml_element_args(xml_element) if 'vertex' in post_args: init_args['vertecies'] = post_args['vertex'] del post_args['vertex'] obj = object.__new__(cls) obj.__init__(**init_args) for key, val in post_args.items(): setattr(obj, key, val) return obj
[docs] @blue.restrict def load(self, filename: str) -> None: """ This methods loads the mesh data from a file. Parameters ---------- filename : str Possible file types are ``'.stl'`` binary or ascii and ``'.obj'`` ascii. """ with open(filename, 'rb') as file: data = file.read() if data.isascii(): self._load_ascii(filename, data.decode('ascii')) else: self._load_binary(filename, data)
@blue.restrict def _load_binary(self, filename: str, data: bytes) -> None: """ Helper function that routes the loading for difference file formats in binary. Parameters ---------- filename : str Possible file types are ``'.stl'``. data : bytes The data of the file to be parsed Raises ------ NotImplemented If the file format is not supported an error is raised. """ if filename.lower().endswith('.obj'): raise NotImplemented elif filename.lower().endswith('.stl'): self._load_STL_binary(data) else: raise NotImplemented @blue.restrict def _load_ascii(self, filename: str, data: str) -> None: """ Helper function that routes the loading for difference file formats in ascii. Parameters ---------- filename : str Possible file types are ``'.stl'`` and ``'.obj'``. data : str The data of the file to be parsed Raises ------ NotImplemented If the file format is not supported an error is raised. """ #return if filename.lower().endswith('.obj'): self._load_OBJ_ascii(data) elif filename.lower().endswith('.stl'): self._load_STL_ascii(data) else: raise NotImplemented @blue.restrict def _load_OBJ_ascii(self, data: str) -> None: """ Parses the data from an ascii obj file to the Cache. Parameters ---------- data : str The data of the file to be parsed """ # DATA CONTAINERS vertecies = [] faces = [] texcoords = [] face_normals = [] vertex_normals = [] # SORTING CONTAINERS all_face_normals = {} texcoords_idx = {} normals_idx = {} # READING LINES for line in data.split('\n'): # REMOVING COMMENTS line = line.split('#')[0] # HANDLING VERTECIES if line.startswith('v '): values = line[2:].strip().split(' ')[:3] vertecies.append(tuple(map(float, values))) elif line.startswith('vn '): values = line[3:].strip().split(' ')[:3] vertex_normals.append(np.array(values, dtype=np.float64)) elif line.startswith('vt '): values = line[3:].strip().split(' ')[:2] texcoords.append(tuple(map(float, values))) elif line.startswith('f '): vertex_idx = line[2:].strip().split(' ')#[:3] triangle_idx = [[vertex_idx[0], a, b] for a, b in zip(vertex_idx[1:], vertex_idx[2:])] for triangle in triangle_idx: if all(map(lambda x: x.count('//') == 1, triangle)): # GET VALUES values, normal_idx = zip(*(value.split('//') for value in triangle)) values = list(map(lambda x: int(x) - 1, values)) normal_idx = list(map(lambda x: int(x) - 1, normal_idx)) # SET INDECIES normals_idx[len(faces)] = normal_idx elif all(map(lambda x: x.count('/') == 2, triangle)): # GET VALUES values, tex_idx, normal_idx = zip(*(value.split('/') for value in triangle)) values = list(map(lambda x: int(x) - 1, values)) tex_idx = list(map(lambda x: int(x) - 1, tex_idx)) normal_idx = list(map(lambda x: int(x) - 1, normal_idx)) # SET INDECIES normals_idx[len(faces)] = normal_idx texcoords_idx[len(faces)] = tex_idx elif all(map(lambda x: x.count('/') == 1, triangle)): # GET VALUES values, tex_idx = zip(*(value.split('/') for value in triangle)) values = list(map(lambda x: int(x) - 1, values)) tex_idx = list(map(lambda x: int(x) - 1, tex_idx)) # SET INDECIES texcoords_idx[len(faces)] = tex_idx face = list(map(int, values)) face = [idx if idx != -2 else len(vertecies) for idx in face] faces.append(face) assert not texcoords_idx.values() or all(map(texcoords_idx.__contains__, range(len(faces)))) self.vertecies = vertecies self.faces = faces self.vertex_normals = vertex_normals or None self.texcoords = texcoords or None self.texcoords_idx = texcoords_idx or None self.normals_idx = normals_idx or None @blue.restrict def _load_STL_ascii(self, data: str) -> None: """ Parses the data from an ascii stl file to the Cache. Parameters ---------- data : str The data of the file to be parsed """ # CROP DATA data = data[data.find('\n'):].split('facet')[1::2] # INIT CONTAINERS faces = list() normals = list() vertecies = list() vertex_counter = count() vertex_table = defaultdict(lambda: next(vertex_counter)) # VECTOR CONSTRUCTION FUNCTION vector_tuple = lambda s: tuple(map(float, list(filter(lambda x: x != '', s.split(' ')))[:3])) for facet in data: # GET FACET SECTION lines = facet.split('\n') # GET VECTORS normal_line = lines[0].replace('normal', '').strip() vertex_0_line = lines[2].replace('vertex', '').strip() vertex_1_line = lines[3].replace('vertex', '').strip() vertex_2_line = lines[4].replace('vertex', '').strip() # CONSTRUCT VECTOR TUPLES normal_tuple = vector_tuple(normal_line) vertex_tuple_0 = vector_tuple(vertex_0_line) vertex_tuple_1 = vector_tuple(vertex_1_line) vertex_tuple_2 = vector_tuple(vertex_2_line) # CONSTRUCT NP.ARRAYS normal_array = np.array(normal_tuple) vertex_array_0 = np.array(vertex_tuple_0) vertex_array_1 = np.array(vertex_tuple_1) vertex_array_2 = np.array(vertex_tuple_2) # ADD NEW VERTECIES TO HASH TABLE if vertex_tuple_0 not in vertex_table: vertex_table[vertex_tuple_0] vertecies.append(vertex_array_0) if vertex_tuple_1 not in vertex_table: vertex_table[vertex_tuple_1] vertecies.append(vertex_array_1) if vertex_tuple_2 not in vertex_table: vertex_table[vertex_tuple_2] vertecies.append(vertex_array_2) # GET NORMAL FROM ORDER edge_array_0 = vertex_array_1 - vertex_array_0 edge_array_1 = vertex_array_2 - vertex_array_0 edge_cross = np.cross(edge_array_0, edge_array_1) edge_normal = edge_cross/np.linalg.norm(edge_cross) correlation = np.dot(edge_normal, normal_array) if correlation > 0: vertex_tuples = [vertex_tuple_0, vertex_tuple_1, vertex_tuple_2] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(normal_array) elif correlation < 0: vertex_tuples = [vertex_tuple_0, vertex_tuple_2, vertex_tuple_1] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(-normal_array) else: vertex_tuples = [vertex_tuple_0, vertex_tuple_2, vertex_tuple_1] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(normal_array) vertex_tuples = [vertex_tuple_0, vertex_tuple_1, vertex_tuple_2] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(-normal_array) # SET ATTRIBUTES self.vertecies = vertecies self.faces = faces self.face_normals = normals @blue.restrict def _load_STL_binary(self, data: bytes) -> None: """ Parses the data from an binary stl file to the Cache. Parameters ---------- data : bytes The data of the file to be parsed """ # CONVERSION FUNCTIONS bytes_to_float = lambda x: struct.unpack('f', x)[0] vector_tuple = lambda x: tuple(map(bytes_to_float, (x[:4], x[4:8], x[8:]))) # HEADER DATA header = data[:80] number = struct.unpack('I', data[80:84])[0] # TRTIANGLE DATA triangle_data = data[84:] faces = list() normals = list() vertecies = list() vertex_counter = count() vertex_table = defaultdict(lambda: next(vertex_counter)) for i in range(number): # GET TRIANGLE triangle = triangle_data[i*50:(i+1)*50] # CONSTRUCT VECTOR TUPLES normal_tuple = vector_tuple(triangle[:12]) vertex_tuple_0 = vector_tuple(triangle[12:24]) vertex_tuple_1 = vector_tuple(triangle[24:36]) vertex_tuple_2 = vector_tuple(triangle[36:48]) # CONSTRUCT NP.ARRAYS normal_array = np.array(normal_tuple) vertex_array_0 = np.array(vertex_tuple_0) vertex_array_1 = np.array(vertex_tuple_1) vertex_array_2 = np.array(vertex_tuple_2) # ADD NEW VERTECIES TO HASH TABLE if vertex_tuple_0 not in vertex_table: vertex_table[vertex_tuple_0] vertecies.append(vertex_array_0) if vertex_tuple_1 not in vertex_table: vertex_table[vertex_tuple_1] vertecies.append(vertex_array_1) if vertex_tuple_2 not in vertex_table: vertex_table[vertex_tuple_2] vertecies.append(vertex_array_2) # GET NORMAL FROM ORDER edge_array_0 = vertex_array_1 - vertex_array_0 edge_array_1 = vertex_array_2 - vertex_array_0 edge_cross = np.cross(edge_array_0, edge_array_1) edge_normal = edge_cross/np.linalg.norm(edge_cross) correlation = np.dot(edge_normal, normal_array) if correlation > 0: vertex_tuples = [vertex_tuple_0, vertex_tuple_1, vertex_tuple_2] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(normal_array) elif correlation < 0: vertex_tuples = [vertex_tuple_0, vertex_tuple_2, vertex_tuple_1] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(-normal_array) else: vertex_tuples = [vertex_tuple_0, vertex_tuple_2, vertex_tuple_1] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(normal_array) vertex_tuples = [vertex_tuple_0, vertex_tuple_1, vertex_tuple_2] face = list(map(vertex_table.__getitem__, vertex_tuples)) faces.append(face) normals.append(-normal_array) # SET ATTRIBUTES self.vertecies = vertecies self.faces = faces self.face_normals = normals
[docs] @blue.restrict def save(self, file: str) -> None: """ This method saves the Mesh data to a file. Parameters ---------- file : str The name to which the file is saved. Raises ------ NotImplemented If the file format is not supported an error is raised. """ if file.lower().endswith('.obj'): self._save_OBJ(file) elif file.lower().endswith('.stl'): self._save_STL(file) elif file.lower().endswith('.msh'): raise NotImplemented else: raise NotImplemented
@blue.restrict def _save_OBJ(self, filename: str|None) -> None: """ This method saves the Mesh data to an ascii obj file. Parameters ---------- filename : str | None The name to which the file is saved. """ lines = ['# Exported with microcosm AI blueprints\n', '\n# VERTECIES\n'] for v in self.vertecies: lines.append(f'v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n') if self.vertex_normals: lines.append('\n# NORMALS\n') for n in self.vertex_normals: lines.append(f'vn {n[0]:.6f} {n[1]:.6f} {n[2]:.6f}\n') if self.texcoords: lines.append('\n# TEXTURE COORDINATES\n') for t in self.texcoords: lines.append(f'vt {t[0]:.6f} {t[1]:.6f}\n') if self.faces: texcoords_idx = self.texcoords_idx normals_idx = self.normals_idx lines.append('\n# FACES\n') for i, face in enumerate(self.faces): has_tex = i in texcoords_idx has_nrm = i in normals_idx if has_tex and has_nrm: parts = ' '.join(f"{v + 1}/{t + 1}/{n + 1}" for v, t, n in zip(face, texcoords_idx[i], normals_idx[i])) elif has_tex: parts = ' '.join(f"{v + 1}/{t + 1}" for v, t in zip(face, texcoords_idx[i])) elif has_nrm: parts = ' '.join(f"{v + 1}//{n + 1}" for v, n in zip(face, normals_idx[i])) else: parts = ' '.join(f"{v + 1}" for v in face) lines.append(f'f {parts}\n') with open(filename, 'w') as file: file.writelines(lines) @blue.restrict def _save_STL(self, filename: str) -> None: """ This method saves the Mesh data to an binary stl file. Parameters ---------- filename : str The name to which the file is saved. """ # GET VERTECIES AND CONSTRUCT CONVERSION FUNCTION vertecies = self.vertecies float_to_bytes = lambda x: struct.pack('f', x) vector_bytes = lambda x: float_to_bytes(x[0]) + float_to_bytes(x[1]) + float_to_bytes(x[2]) # CREATE HEADER SOURCE = bytes('Saved from Blueprints, UNITS= m', encoding='ascii') HEADER = bytearray(84) HEADER[:len(SOURCE)] = SOURCE HEADER[80:] = struct.pack('I', len(self.faces)) with open(filename, 'wb') as file: file.write(HEADER) for normal, indecies in zip(self.face_normals, self.faces): vertex_a, vertex_b, vertex_c = map(vertecies.__getitem__, indecies) file.write(vector_bytes(normal)) file.write(vector_bytes(vertex_a)) file.write(vector_bytes(vertex_b)) file.write(vector_bytes(vertex_c)) file.write(bytes.fromhex('00') * 2) # MUJOCO PROPERTIES @property def file(self) -> str | None: """ Filename for Mujoco XML Returns ------- str """ return self.filename @property def vertex(self): """ Returns ------- np.ndarray This attribute is used to write construct the mujoco xml. It returns a flattened list of coordinates that is written raw to xml. For a structured list of vertecies see :attr:`vertecies`. """ return self._vertecies.flatten() @property def face(self) -> np.ndarray|None: """ Returns ------- np.ndarray | None This attribute is used to write construct the mujoco xml. It returns a flattened list of coordinates that is written raw to xml. For a structured list of faces see :attr:`faces`. """ return self._faces @face.setter @blue.restrict def face(self, face: list) -> None: """ This attribute is used to write construct the mujoco xml. It returns a flattened list of coordinates that is written raw to xml. For a structured list of faces see :attr:`faces`. Parameters ---------- face : list A flattened list of all face indecies. """ self.faces = face @property def texcoord(self) -> np.ndarray|None: """ Returns ------- np.ndarray | None This attribute is used to write to xml. It returns a flattened list of texture coordinates that is written raw to xml. For a structured list of texture coordinates see :attr:`texcoords`. """ return self._texcoords or None @texcoord.setter @blue.restrict def texcoord(self, texcoord: list) -> None: """ This attribute is used to write to xml. It returns a flattened list of texture coordinates that is written raw to xml. For a structured list of texture coordinates see :attr:`texcoords`. Parameters ---------- texcoord : list A flattened list of all texture coordinates. """ self.texcoords = texcoord @property def normal(self) -> np.ndarray|None: """ Returns ------- np.ndarray | None This attribute is used to write to xml. It returns a flattened list of face normals that is written raw to xml. For a structured list of face normals see :attr:`normals`. """ if self._face_normals is None: return None else: return np.concatenate(self._face_normals) @normal.setter @blue.restrict def normal(self, normal: list) -> None: """ This attribute is used to write to xml. It returns a flattened list of face normals that is written raw to xml. For a structured list of face normals see :attr:`normals`. Parameters ---------- normal : list A flattened list of all normals coordinates. """ self.face_normals = normal @property def size(self) -> np.ndarray: """ Returns ------- np.ndarray The size of the Mesh is computed as the difference of the minimal and maximal component for each axis on each vertex. """ return self.vertecies_max - self.vertecies_min @size.setter @blue.restrict def size(self, size: np.ndarray|list[int|float]) -> None: """ The size of the Mesh is defined as the difference of the minimal and maximal component for each axis on each vertex. Parameters ---------- size : np.ndarray | list[int | float] Setting this attribute results in a rescaling of all vertecies. Setting this attribute should only be done if the raw data must be modified directly, use :attr:`scale <blueprints.assets.MeshAsset.scale>` instead. """ vertecies = np.array(self.vertecies) vertecies = vertecies - self.vertecies_center scale = np.array(size) / self.size scale[np.isinf(scale)] = 0 vertecies = vertecies * scale self.vertecies = vertecies + self.vertecies_center # BLUEPRINTS PROPERTIES @property def vertecies(self) -> np.ndarray: """ Returns ------- np.ndarray The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the spatial dimensions. """ return self._vertecies @vertecies.setter @blue.restrict def vertecies(self, vertecies: np.ndarray|list[np.ndarray|list[float|int]]) -> None: """ Parameters ---------- vertecies : np.ndarray | list[np.ndarray | list[float | int]] The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the spatial dimensions. """ vertecies = np.array(vertecies) self._vertecies = vertecies self._flag_dependencies('vertecies') self._built = False @property def faces(self) -> list[np.ndarray]|None: """ Returns ------- list[np.ndarray] | None The list of faces. Each face is a np.ndarray with n indecies for each of the vertecies of the face. """ return self._faces #### if self._faces is None: return None else: return list(np.rollaxis(self._faces.reshape((-1, 3)), axis=0)) @faces.setter @blue.restrict def faces(self, faces: np.ndarray|list[np.ndarray|list[float|int]]|None) -> None: """ Parameters ---------- faces : np.ndarray | list[np.ndarray | list[float | int]] | None The list of faces. Each face is a np.ndarray with n indecies for each of the vertecies of the face. """ if isinstance(faces, np.ndarray): faces = list(np.rollaxis(faces, axis=0)) self._faces = faces self._built = False return ### if faces is not None: faces = np.array(faces, dtype=np.int32).flatten() self._faces = faces self._built = False @property def texcoords(self) -> list[np.ndarray]|None: """ Returns ------- list[np.ndarray] | None The list of texture coordinates. Each texture coordinate is a np.ndarray with coordinates in the range [0, 1] specifying the point on the texture which is mapped to the referencing face vertex. """ if self._texcoords is None: return None else: return list(np.rollaxis(self._texcoords.reshape((-1, 2)), axis=0)) @texcoords.setter @blue.restrict def texcoords(self, texcoords: np.ndarray|list[np.ndarray|list[float|int]]|None) -> None: """ Parameters ---------- texcoords : np.ndarray | list[np.ndarray | list[float | int]] | None The list of texture coordinates. Each texture coordinate is a np.ndarray with coordinates in the range [0, 1] specifying the point on the texture which is mapped to the referencing face vertex. """ if texcoords is not None: texcoords = np.array(texcoords, dtype=np.float32).flatten() self._texcoords = texcoords self._built = False @property def texcoords_idx(self) -> dict|None: """ Returns ------- dict | None The list of texture coordinate indecies for faces. """ return self._texcoords_idx @texcoords_idx.setter @blue.restrict def texcoords_idx(self, texcoords_idx: dict|None) -> None: """ Parameters ---------- texcoords_idx : dict | None The list of texture coordinate indecies for faces. """ self._texcoords_idx = texcoords_idx self._built = False @property def face_normals(self) -> list[np.ndarray]|None: """ Returns ------- list[np.ndarray] | None Face normals are the normal vectors of faces. They specify the direction in which the face points. """ if self._face_normals is None: if self._faces is not None: faces = self.vertecies[self.faces,:] edges = faces[:,:,1:] - faces[:,:,:1] cross = np.cross(edges[...,0], edges[...,1], axisa=1, axisb=1) norms = cross / np.linalg.norm(cross, axis=1)[:,None] self.face_normals = norms return self.face_normals else: return None else: return self._face_normals @face_normals.setter @blue.restrict def face_normals(self, face_normals: np.ndarray|list[np.ndarray|list[float|int]]|None) -> None: """ Parameters ---------- face_normals : np.ndarray | list[np.ndarray | list[float | int]] | None Face normals are the normal vectors of faces. They specify the direction in which the face points. """ if isinstance(face_normals, np.ndarray): face_normals = list(np.rollaxis(face_normals, axis=0)) self._face_normals = face_normals self._built = False @property def vertex_normals(self) -> list[np.ndarray]|None: """ Returns ------- list[np.ndarray] | None Vertex normals are the normal vectors of vertecies. They specify the direction in which the vertecies of a face point. Specifying vertex normals instead of face normals enables mujoco torenderer soft edges. """ if self._vertex_normals is None: return None else: return self._vertex_normals @vertex_normals.setter @blue.restrict def vertex_normals(self, vertex_normals: np.ndarray|list[np.ndarray|list[float|int]]|None) -> None: """ Parameters ---------- vertex_normals : np.ndarray | list[np.ndarray | list[float | int]] | None Vertex normals are the normal vectors of vertecies. They specify the direction in which the vertecies of a face point. Specifying vertex normals instead of face normals enables mujoco torenderer soft edges. """ if isinstance(vertex_normals, np.ndarray): vertex_normals = list(np.rollaxis(vertex_normals, axis=0)) self._vertex_normals = vertex_normals self._built = False @property def normals_idx(self) -> dict|None: """ The list of normals indecies for faces. Returns ------- dict | None """ return self._normals_idx @normals_idx.setter @blue.restrict def normals_idx(self, normals_idx: dict|None) -> None: """ Parameters ---------- normals_idx : dict | None The list of normals indecies for faces. """ self._normals_idx = normals_idx self._built = False @property def vertecies_min(self): """ The minimum for each axis component of all vertecies. Returns ------- np.ndarray """ if self._validate('vertecies_min'): self._vertecies_min = np.min(self.vertecies, axis=0) self._DEPENDENCY_FLAGS['vertecies_min'] = True return self._vertecies_min @property def vertecies_max(self): """ The maximum for each axis component of all vertecies. Returns ------- np.ndarray """ if self._validate('vertecies_max'): self._vertecies_max = np.max(self.vertecies, axis=0) self._DEPENDENCY_FLAGS['vertecies_max'] = True return self._vertecies_max @property def vertecies_center(self): """ The middle point for each axis component of all vertecies. Returns ------- np.ndarray """ return (self.vertecies_min + self.vertecies_max)/2
[docs] class HFieldCache(blue.HFieldCacheType, BaseCache):
[docs] def __init__(self, terrain: np.ndarray|list[np.ndarray|list[float|int]]|None = None, filename: str|None = None, **kwargs): self._built = False self._world = None super().__init__(**kwargs) self.filename = filename if filename is not None: self.load(filename) elif terrain is not None: self.terrain = terrain if 'name' not in kwargs: self._name = None
def __getitem__(self, key: tuple[slice|int]) -> np.ndarray|np.float32: """ Returns ------- np.ndarray | np.float32 The values from the selected index/slice of the height field. """ return self.terrain[key] def __setitem__(self, key: tuple[slice|int], value: int|float|list[int|float]|np.ndarray) -> None: """ Parameters ---------- key : tuple[slice | int] The indecies/slices of acces value : int|float|list[int|float]|np.ndarray The value to be assigned in the selected parts of the field. """ self.terrain[key] = np.array(value, dtype=np.float32)
[docs] @blue.restrict def copy(self, **kwargs) -> blue.CacheType: """ This method constructs a copy of the Cache with possible alterations to its attributes as specified in kwargs. Parameters ---------- **kwargs Keyword arguments are passed to the :meth:`__init__` to replace the attributes of the current Thing from which copy is called. Returns ------- blue.CacheType A new instance of the Cache """ mesh = super().copy(**kwargs) if 'name' not in kwargs: mesh._name = self._name return mesh
@blue.restrict def _build(self, dirname: str, **kwargs) -> None: """ This method is called by the Assets parent to construct the xml representations of the kinematic tree. Parameters ---------- dirname : str The directory name in which the hfield file is saved. **kwargs Dummy argument """ if not self._built: filename = self.filename or f'hfield.hf' pathname, basename = os.path.split(filename) if not pathname.endswith(dirname): path = f'{dirname}/{self.ID}_{basename}' else: path = filename self.save(path) self._path = path self._built = True @blue.restrict @classmethod def _from_xml_element(cls, xml_element) -> blue.CacheType: """ This method reconstructs an HFieldCache from an xml element. Parameters ---------- xml_element : xml.Element The xml element from which a HFieldCache is reconstructed. Returns ------- blue.CacheType The reconstructed HFieldCache. """ init_args, post_args, rest_args = cls._xml_element_args(xml_element) if 'terrain' in post_args: init_args['terrain'] = post_args['terrain'] del post_args['terrain'] if 'file' in rest_args: init_args['filename'] = rest_args['file'] obj = object.__new__(cls) obj.__init__(**init_args) for key, val in post_args.items(): setattr(obj, key, val) return obj
[docs] @blue.restrict def load(self, filename: str) -> None: """ This methods loads the mesh data from a file. Parameters ---------- filename : str Possible file types are ``'.stl'`` binary or ascii and ``'.obj'`` ascii. """ if filename.lower().endswith('.hf'): with open(filename, 'rb') as file: data = file.read() self._load_HF(data) elif filename.lower().endswith('.png'): self._load_PNG(filename) else: raise Exception('HFields are only implemented for PNG and HF.')
@blue.restrict def _load_HF(self, data: bytes) -> None: """ Parses the data from an binary stl file to the Cache. Parameters ---------- data : bytes The data of the file to be parsed """ # HEADER DATA nrows = struct.unpack('I', data[0:4])[0] ncols = struct.unpack('I', data[4:8])[0] # HEIGHT DATA (batch unpack all floats at once) n_floats = nrows * ncols heights = struct.unpack(f'{n_floats}f', data[8:8 + n_floats * 4]) # SET ATTRIBUTES self.terrain = np.array(heights, dtype=np.float32).reshape((nrows, ncols)) @blue.restrict def _load_PNG(self, filename: str) -> None: image = imread(filename) height = np.mean(image[:,:,:3], axis=2) self.terrain = height
[docs] @blue.restrict def save(self, filename: str) -> None: """ This method saves the Mesh data to a file. Parameters ---------- file : str The name to which the file is saved. Raises ------ NotImplemented If the file format is not supported an error is raised. """ if filename.lower().endswith('.hf'): self._save_HF(filename) else: raise NotImplemented
@blue.restrict def _save_HF(self, filename: str) -> None: """ This method saves the Mesh data to an binary stl file. Parameters ---------- filename : str The name to which the file is saved. """ # GET VERTECIES AND CONSTRUCT CONVERSION FUNCTION int_to_bytes = lambda x: struct.pack('I', x) float_to_bytes = lambda x: struct.pack('f', x) with open(filename, 'wb') as file: file.write(int_to_bytes(self.nrow)) file.write(int_to_bytes(self.ncol)) for height in self.terrain.reshape(-1): file.write(float_to_bytes(height)) # DERIVED PROPERTIES @property def elevation(self): """ Derived dummy property """ return self.terrain.flatten() @property def nrow(self) -> int: """ Number of rows in the terrain. Returns ------- int """ return int(self.terrain.shape[0]) @property def ncol(self) -> int: """ Number of collumns in the terrain. Returns ------- int """ return int(self.terrain.shape[1]) # BLUEPRINTS PROPERTIES @property def filename(self) -> str: """ The filename of the hfield data. Returns ------- str """ return self._filename @filename.setter @blue.restrict def filename(self, filename: str|None) -> None: self._filename = filename @property def terrain(self) -> np.ndarray: """ Returns ------- np.ndarray The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the spatial dimensions. """ return self._terrain @terrain.setter @blue.restrict def terrain(self, terrain: np.ndarray|list[np.ndarray|list[float|int]]) -> None: """ Parameters ---------- vertecies : np.ndarray | list[np.ndarray | list[float | int]] The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the spatial dimensions. """ self._terrain = np.array(terrain, dtype=np.float32) if self._world is not None: mj_hfield = self._world._mj_data.model.hfield(self._index) mj_hfield.data = self._terrain if self._world._viewer is not None: self._world._viewer.update_hfield(self._index) #self._world._mj_model.update_hfield(self._index) @property def file(self) -> str: """ Dummy variable for the default names of hfield data files. Returns ------- str """ if self._file is None: return f'hfield_{self.name}.hf' else: return self._file
CACHE_THINGS = {'mesh': MeshCache, 'hfield': HFieldCache}