Source code for genesis.options.textures

import os
from typing import Optional, List, Union

import numpy as np
from PIL import Image

import genesis as gs
import genesis.utils.mesh as mu

from .options import Options

IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp", ".hdr", ".exr")
HDR_EXTENSIONS = (".hdr", ".exr")


[docs]class Texture(Options): """ Base class for Genesis's texture objects. Note ---- This class should *not* be instantiated directly. """ def __init__(self, **data): super().__init__(**data) def check_dim(self, dim): raise NotImplementedError def check_simplify(self): raise NotImplementedError def apply_cutoff(self, cutoff): raise NotImplementedError def is_black(self): raise NotImplementedError def requires_uv(self): raise NotImplementedError
[docs]class ColorTexture(Texture): """ A texture that consists of a single color. Parameters ---------- color : list of float A list of color values, stored as tuple, supporting any number of channels within the range [0.0, 1.0]. Default is (1.0, 1.0, 1.0). """ color: Union[float, List[float]] = (1.0, 1.0, 1.0) def __init__(self, **data): super().__init__(**data) if isinstance(self.color, float): self.color = (self.color,) else: self.color = tuple(self.color) # Use tuple to store image color since it is more efficient def check_dim(self, dim): if len(self.color) > dim: self.color, res = self.color[:dim], self.color[dim] return ColorTexture(color=res) return None def check_simplify(self): return self def apply_cutoff(self, cutoff): if cutoff is None: return self.color = tuple(1.0 if c >= cutoff else 0.0 for c in self.color) def is_black(self): return all(c < gs.EPS for c in self.color) def requires_uv(self): return False
[docs]class ImageTexture(Texture): """ A texture with a texture map (image). Parameters ---------- image_path : str, optional Path to the image file. image_array : np.ndarray, optional Image array. image_color : float or list of float, optional The factor that will be multiplied with the base color, stored as tuple. Default is None. encoding : str, optional The encoding way of the image. Possible values are ['srgb', 'linear']. Default is 'srgb'. - 'srgb': Encoding of some RGB images. - 'linear': All generic images, such as opacity, roughness and normal, should be encoded with 'linear'. """ image_path: Optional[str] = None image_array: Optional[np.ndarray] = None image_color: Optional[Union[float, List[float]]] = None encoding: str = "srgb" class Config: arbitrary_types_allowed = True def __init__(self, **data): super().__init__(**data) if not (self.image_path is None) ^ (self.image_array is None): gs.raise_exception("Please set either `image_path` or `image_array`.") if self.image_path is not None: input_image_path = self.image_path if not os.path.exists(self.image_path): self.image_path = os.path.join(gs.utils.get_assets_dir(), self.image_path) if not os.path.exists(self.image_path): gs.raise_exception( f"File not found in either current directory or assets directory: '{input_image_path}'." ) # Load image_path as actual image_array, unless for special texture images (e.g. `.hdr` and `.exr`) that are only supported by raytracers if self.image_path.endswith(HDR_EXTENSIONS): self.encoding = "linear" # .exr or .hdr images should be encoded with 'linear' if self.image_path.endswith((".exr")): self.image_path = mu.check_exr_compression(self.image_path) else: self.image_array = np.array(Image.open(self.image_path)) elif self.image_array is not None: if not isinstance(self.image_array, np.ndarray): gs.raise_exception("`image_array` needs to be a numpy array.") arr = self.image_array if arr.dtype != np.uint8: if np.issubdtype(arr.dtype, np.floating): if arr.max() <= 1.0: arr = (arr * 255.0).round() arr = np.clip(arr, 0.0, 255.0).astype(np.uint8) elif arr.dtype == np.bool_: arr = arr.astype(np.uint8) * 255 elif np.issubdtype(arr.dtype, np.integer): arr = np.clip(arr, 0, 255).astype(np.uint8) else: gs.raise_exception( f"Unsupported image dtype {arr.dtype}. " "Only uint8, integer, floating-point, or bool types are supported." ) self.image_array = arr # just calculate channel if self.image_array is None: # Using 'image_path' self._mean_color = np.array([1.0, 1.0, 1.0], dtype=np.float16) self._channel = 3 else: self._mean_color = (np.mean(self.image_array, axis=(0, 1), dtype=np.float32) / 255.0).astype(np.float16) self._channel = self.image_array.shape[2] if self.image_array.ndim == 3 else 1 # build image color if self.image_color is None: self.image_color = (1.0,) * self._channel else: if isinstance(self.image_color, float): self.image_color = (self.image_color,) * self._channel else: self.image_color = tuple(self.image_color[: self._channel]) self.encoding = self.encoding.lower() if self.encoding not in ["srgb", "linear"]: gs.raise_exception(f"Invalid image encoding: {self.encoding}.") def check_dim(self, dim): if self.image_array is not None: if self._channel > dim: self.image_array, res_array = self.image_array[:, :, :dim], self.image_array[:, :, dim] self.image_color, res_color = self.image_color[:dim], self.image_color[dim:] self._channel = dim return ImageTexture(image_array=res_array, image_color=res_color, encoding="linear").check_simplify() return None def check_simplify(self): if self.image_array is None: return self max_color = np.max(self.image_array, axis=(0, 1)) min_color = np.min(self.image_array, axis=(0, 1)) if np.all(min_color == max_color): return ColorTexture(color=max_color.reshape(-1) / 255.0 * self.image_color) else: return self def apply_cutoff(self, cutoff): if cutoff is None or self.image_array is None: # Cutoff does not apply on image file. return self.image_array = np.where(self.image_array >= 255.0 * cutoff, 255, 0).astype(np.uint8) def is_black(self): return all(c < gs.EPS for c in self.image_color) or np.max(self.image_array) == 0 def requires_uv(self): return True def mean_color(self): return self._mean_color def channel(self): return self._channel
class BatchTexture(Texture): """ A batch of textures for batch rendering. Parameters ---------- textures : List[Optional[Texture]] List of textures. """ textures: Optional[List[Optional[Texture]]] def __init__(self, **data): super().__init__(**data) @staticmethod def from_images( image_paths: Optional[List[str]] = None, image_folder: Optional[str] = None, image_arrays: Optional[List[np.ndarray]] = None, image_colors: Optional[Union[List[float], List[List[float]]]] = None, encoding: str = "srgb", ): """ Create a batch texture from images. Parameters ---------- image_paths : List[str], optional List of paths to the image files. image_folder : str, optional Path to the image folder. image_arrays : List[np.ndarray], optional List of image arrays. image_colors : List[Union[float, List[float]]], optional List of color factors that will be multiplied with the base color, stored as tuple. Default is None. encoding : str, optional The encoding way of the image. Possible values are ['srgb', 'linear']. Default is 'srgb'. - 'srgb': Encoding of some RGB images. - 'linear': All generic images, such as opacity, roughness and normal, should be encoded with 'linear'. """ image_sources = (image_paths, image_folder, image_arrays) if sum(x is not None for x in image_sources) != 1: gs.raise_exception("Please set exactly one of `image_paths`, `image_folder`, `image_arrays`.") image_textures = [] if image_folder is not None: input_image_folder = image_folder if not os.path.exists(image_folder): image_folder = os.path.join(gs.utils.get_assets_dir(), image_folder) if not os.path.exists(image_folder): gs.raise_exception( f"Directory not found in either current directory or assets directory: '{input_image_folder}'." ) image_paths = [ os.path.join(image_folder, image_path) for image_path in sorted(os.listdir(image_folder)) if image_path.lower().endswith(IMAGE_EXTENSIONS) ] num_images = len(image_paths) if image_paths is not None else len(image_arrays) if num_images == 0: gs.raise_exception("No images found.") if image_colors is not None: if isinstance(image_colors[0], float): # List[float] image_colors = [image_colors for _ in range(num_images)] else: # List[List[float]] if len(image_colors) != num_images: gs.raise_exception("The number of image colors must be the same as the number of images.") else: image_colors = [None] * num_images if image_paths is not None: for image_path, image_color in zip(image_paths, image_colors): image_textures.append(ImageTexture(image_path=image_path, image_color=image_color, encoding=encoding)) else: for image_array, image_color in zip(image_arrays, image_colors): image_textures.append(ImageTexture(image_array=image_array, image_color=image_color, encoding=encoding)) return BatchTexture(textures=image_textures) @staticmethod def from_colors( colors: List[List[float]], ): """ Create a batch texture from colors. Parameters ---------- colors : List[List[float]] List of colors. """ color_textures = [ColorTexture(color=color) for color in colors] return BatchTexture(textures=color_textures) def check_dim(self, dim): return BatchTexture(textures=[texture.check_dim(dim) for texture in self.textures]).check_simplify() def check_simplify(self): self.textures = [texture.check_simplify() if texture is not None else None for texture in self.textures] return self def apply_cutoff(self, cutoff): for texture in self.textures: if texture is not None: texture.apply_cutoff(cutoff) def is_black(self): return all(texture.is_black() if texture is not None else True for texture in self.textures) def requires_uv(self): return any(texture is not None and texture.requires_uv() for texture in self.textures) def merge(self, other: "BatchTexture"): self.textures.extend(other.textures)