| """Phase 5: Material & Texture Module. |
| |
| Generates: |
| - PBR materials (albedo, metallic, roughness, normal) |
| - Texture baking from multi-view images |
| - Lighting estimation for relightable scenes |
| """ |
|
|
| import os |
| from typing import Dict, List, Optional, Tuple, Union |
|
|
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from PIL import Image |
|
|
|
|
| class MaterialTextureModule(nn.Module): |
| """Generate PBR materials and bake textures onto meshes.""" |
| |
| def __init__( |
| self, |
| model_size: str = "L", |
| device: str = "cuda", |
| dtype: torch.dtype = torch.float16, |
| use_pbr: bool = True, |
| cache_dir: Optional[str] = None, |
| ): |
| super().__init__() |
| self.model_size = model_size |
| self.device = device |
| self.dtype = dtype |
| self.use_pbr = use_pbr |
| self.cache_dir = cache_dir |
| |
| |
| self._material_model = None |
| |
| |
| self.material_priors = { |
| "wall": {"albedo": [0.9, 0.9, 0.9], "metallic": 0.0, "roughness": 0.8}, |
| "floor_wood": {"albedo": [0.6, 0.4, 0.2], "metallic": 0.0, "roughness": 0.6}, |
| "floor_tile": {"albedo": [0.8, 0.8, 0.8], "metallic": 0.1, "roughness": 0.3}, |
| "floor_carpet": {"albedo": [0.5, 0.3, 0.2], "metallic": 0.0, "roughness": 0.9}, |
| "ceiling": {"albedo": [0.95, 0.95, 0.95], "metallic": 0.0, "roughness": 0.9}, |
| "furniture_wood": {"albedo": [0.5, 0.3, 0.15], "metallic": 0.0, "roughness": 0.5}, |
| "furniture_fabric": {"albedo": [0.6, 0.5, 0.4], "metallic": 0.0, "roughness": 0.8}, |
| "furniture_leather": {"albedo": [0.4, 0.2, 0.1], "metallic": 0.1, "roughness": 0.4}, |
| "furniture_metal": {"albedo": [0.7, 0.7, 0.7], "metallic": 0.9, "roughness": 0.2}, |
| "furniture_plastic": {"albedo": [0.8, 0.8, 0.8], "metallic": 0.0, "roughness": 0.3}, |
| "furniture_glass": {"albedo": [0.9, 0.9, 0.9], "metallic": 0.0, "roughness": 0.05}, |
| "default": {"albedo": [0.7, 0.7, 0.7], "metallic": 0.0, "roughness": 0.5}, |
| } |
| |
| def generate_room_materials( |
| self, |
| room_shell_mesh: "trimesh.Trimesh", |
| image: Image.Image, |
| semantic_seg: np.ndarray, |
| ) -> "trimesh.Trimesh": |
| """ |
| Generate materials for room shell (walls, floor, ceiling). |
| |
| Uses semantic segmentation to determine material types |
| and input image for color extraction. |
| """ |
| if not self.use_pbr: |
| return room_shell_mesh |
| |
| |
| img_np = np.array(image) |
| |
| |
| floor_region = semantic_seg == 1 |
| ceiling_region = semantic_seg == 2 |
| wall_regions = (semantic_seg == 3) | (semantic_seg == 4) |
| |
| |
| floor_color = self._extract_dominant_color(img_np, floor_region) |
| ceiling_color = self._extract_dominant_color(img_np, ceiling_region) |
| wall_color = self._extract_dominant_color(img_np, wall_regions) |
| |
| |
| floor_mat = self._create_material("floor_wood", color=floor_color) |
| ceiling_mat = self._create_material("ceiling", color=ceiling_color) |
| wall_mat = self._create_material("wall", color=wall_color) |
| |
| |
| |
| |
| room_shell_mesh.materials = { |
| "floor": floor_mat, |
| "ceiling": ceiling_mat, |
| "walls": wall_mat, |
| } |
| |
| return room_shell_mesh |
| |
| def generate_object_materials( |
| self, |
| object_mesh: "trimesh.Trimesh", |
| multiviews: List[Image.Image], |
| object_info: Dict, |
| ) -> Tuple["trimesh.Trimesh", List[Dict]]: |
| """ |
| Generate PBR materials for a furniture object. |
| |
| Uses multi-view images to bake texture and infer material properties. |
| """ |
| if not self.use_pbr: |
| return object_mesh, [] |
| |
| class_name = object_info.get("class_name", "furniture") |
| |
| |
| material_type = self._infer_material_type(class_name, multiviews[0]) |
| |
| |
| colors = [self._extract_dominant_color(np.array(mv), np.ones((mv.size[1], mv.size[0]), dtype=bool)) |
| for mv in multiviews] |
| avg_color = np.mean(colors, axis=0) |
| |
| |
| material = self._create_material(material_type, color=avg_color) |
| |
| |
| texture = self._bake_texture(object_mesh, multiviews, material) |
| |
| |
| if texture is not None: |
| object_mesh.visual = object_mesh.visual.to_texture() |
| |
| object_mesh.material_override = material |
| |
| materials = [material] |
| |
| return object_mesh, materials |
| |
| def estimate_lighting( |
| self, |
| image: Image.Image, |
| ) -> Dict: |
| """ |
| Estimate scene lighting from input image. |
| |
| Returns: |
| { |
| "environment_map": HDR environment map (placeholder), |
| "key_light_direction": [x, y, z], |
| "key_light_intensity": float, |
| "fill_light_intensity": float, |
| "ambient_intensity": float, |
| "color_temperature": float, # Kelvin |
| } |
| """ |
| img_np = np.array(image) |
| |
| |
| |
| |
| |
| brightness = img_np.mean() |
| |
| |
| avg_color = img_np.mean(axis=(0, 1)) |
| |
| |
| color_temp = 6500 |
| if avg_color[2] > avg_color[0] * 1.2: |
| color_temp = 8000 |
| elif avg_color[0] > avg_color[2] * 1.2: |
| color_temp = 3000 |
| |
| |
| |
| light_dir = np.array([0.3, 0.8, 0.2]) |
| light_dir = light_dir / np.linalg.norm(light_dir) |
| |
| return { |
| "environment_map": None, |
| "key_light_direction": light_dir.tolist(), |
| "key_light_intensity": float(brightness / 255.0 * 2.0), |
| "fill_light_intensity": float(brightness / 255.0 * 0.5), |
| "ambient_intensity": float(brightness / 255.0 * 0.3), |
| "color_temperature": float(color_temp), |
| } |
| |
| def _extract_dominant_color( |
| self, |
| image: np.ndarray, |
| mask: np.ndarray, |
| ) -> np.ndarray: |
| """Extract dominant color from image region.""" |
| if mask.sum() == 0: |
| return np.array([0.7, 0.7, 0.7]) |
| |
| masked_pixels = image[mask] |
| |
| |
| dominant_color = np.median(masked_pixels, axis=0) / 255.0 |
| |
| return dominant_color |
| |
| def _create_material( |
| self, |
| material_type: str, |
| color: Optional[np.ndarray] = None, |
| ) -> Dict: |
| """Create PBR material from type and color.""" |
| prior = self.material_priors.get(material_type, self.material_priors["default"]) |
| |
| if color is not None: |
| albedo = color.tolist() |
| else: |
| albedo = prior["albedo"] |
| |
| return { |
| "type": material_type, |
| "albedo": albedo, |
| "metallic": prior["metallic"], |
| "roughness": prior["roughness"], |
| "normal_scale": 1.0, |
| "ao_scale": 1.0, |
| |
| "albedo_map": None, |
| "metallic_map": None, |
| "roughness_map": None, |
| "normal_map": None, |
| "ao_map": None, |
| } |
| |
| def _infer_material_type( |
| self, |
| class_name: str, |
| image: Image.Image, |
| ) -> str: |
| """Infer material type from object class and visual appearance.""" |
| class_lower = class_name.lower() |
| |
| |
| material_map = { |
| "sofa": "furniture_fabric", |
| "chair": "furniture_fabric", |
| "table": "furniture_wood", |
| "coffee_table": "furniture_wood", |
| "bed": "furniture_fabric", |
| "desk": "furniture_wood", |
| "bookshelf": "furniture_wood", |
| "lamp": "furniture_metal", |
| "wardrobe": "furniture_wood", |
| "tv_stand": "furniture_wood", |
| "rug": "floor_carpet", |
| } |
| |
| return material_map.get(class_lower, "furniture_wood") |
| |
| def _bake_texture( |
| self, |
| mesh: "trimesh.Trimesh", |
| multiviews: List[Image.Image], |
| material: Dict, |
| ) -> Optional[Image.Image]: |
| """ |
| Bake multi-view images into a unified UV texture. |
| |
| Uses visibility-aware projection to handle occlusions. |
| """ |
| |
| |
| |
| if multiviews: |
| return multiviews[0] |
| return None |
|
|