Download extend3d.py from junaid-simamdigital/Extend3D: direct link, hf CLI and curl.
- Browser
- Download file 33.2 kB
-
https://huggingface.co/spaces/junaid-simamdigital/Extend3D/resolve/main/extend3d.py
- Command line
-
hf download hf://spaces/junaid-simamdigital/Extend3D/extend3d.py
-
curl -L -o extend3d.py https://huggingface.co/spaces/junaid-simamdigital/Extend3D/resolve/main/extend3d.py
33.2 kB
| import os | |
| import json | |
| import numpy as np | |
| from PIL import Image | |
| from typing import List | |
| from tqdm import tqdm, trange | |
| os.environ['SPCONV_ALGO'] = 'native' | |
| import torch | |
| from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity | |
| from torchmetrics.image.ssim import StructuralSimilarityIndexMeasure | |
| from trellis.pipelines.base import Pipeline | |
| from trellis.pipelines import TrellisImageTo3DPipeline | |
| from trellis.models import SparseStructureFlowModel, SparseStructureEncoder, SparseStructureDecoder | |
| from trellis.modules.sparse.basic import sparse_cat, sparse_unbind, SparseTensor | |
| from trellis.utils import render_utils | |
| from trellis.representations.mesh import MeshExtractResult | |
| from trellis.representations.mesh.utils_cube import sparse_cube2verts | |
| from huggingface_hub import hf_hub_download | |
| from safetensors.torch import load_file | |
| from utils import * | |
| class Extend3D(Pipeline): | |
| # ----------------------------------------------------------------------- | |
| # Construction | |
| # ----------------------------------------------------------------------- | |
| def __init__(self, ckpt_path: str, device: str = 'cpu'): | |
| super().__init__() | |
| # Load the base Trellis pipeline | |
| self.pipeline = TrellisImageTo3DPipeline.from_pretrained(ckpt_path) | |
| self.pipeline.to(device) | |
| self.models = self.pipeline.models | |
| # Replace the sparse-structure encoder with a higher-capacity checkpoint | |
| config_path = hf_hub_download(repo_id=ckpt_path, | |
| filename='ckpts/ss_enc_conv3d_16l8_fp16.json') | |
| model_path = hf_hub_download(repo_id=ckpt_path, | |
| filename='ckpts/ss_enc_conv3d_16l8_fp16.safetensors') | |
| with open(config_path, 'r') as f: | |
| model_config = json.load(f) | |
| state_dict = load_file(model_path) | |
| encoder = SparseStructureEncoder(**model_config['args']) | |
| encoder.load_state_dict(state_dict) | |
| self.models['sparse_structure_encoder'] = encoder.to(device) | |
| # Perceptual metrics used for SLAT optimization loss (frozen, no gradients needed) | |
| self.lpips = LearnedPerceptualImagePatchSimilarity(normalize=True, net_type='squeeze').to(device) | |
| self.ssim = StructuralSimilarityIndexMeasure(data_range=1.0).to(device) | |
| self.lpips.requires_grad_(False) | |
| self.ssim.requires_grad_(False) | |
| # SLAT normalization constants (frozen; gradients must not flow through them) | |
| self.std = torch.tensor(self.pipeline.slat_normalization['std'])[None].to(device) | |
| self.mean = torch.tensor(self.pipeline.slat_normalization['mean'])[None].to(device) | |
| self.std.requires_grad_(False) | |
| self.mean.requires_grad_(False) | |
| # ----------------------------------------------------------------------- | |
| # Device management | |
| # ----------------------------------------------------------------------- | |
| def to(self, device) -> "Extend3D": | |
| self.pipeline.to(device) | |
| self.models['sparse_structure_encoder'] = self.models['sparse_structure_encoder'].to(device) | |
| self.lpips = self.lpips.to(device) | |
| self.ssim = self.ssim.to(device) | |
| self.std = self.std.to(device) | |
| self.mean = self.mean.to(device) | |
| return self | |
| def cuda(self) -> "Extend3D": | |
| return self.to(torch.device('cuda')) | |
| def cpu(self) -> "Extend3D": | |
| return self.to(torch.device('cpu')) | |
| def from_pretrained(ckpt_path: str, device: str = 'cpu') -> "Extend3D": | |
| return Extend3D(ckpt_path, device=device) | |
| # ----------------------------------------------------------------------- | |
| # Preprocessing | |
| # ----------------------------------------------------------------------- | |
| def preprocess(image: Image.Image) -> Image.Image: | |
| return image.resize((1024, 1024), Image.Resampling.LANCZOS) | |
| # ----------------------------------------------------------------------- | |
| # Conditioning | |
| # ----------------------------------------------------------------------- | |
| def get_cond( | |
| self, | |
| image: Image.Image, | |
| pointmap_info: PointmapInfo = None, | |
| width: int = 2, | |
| length: int = 2, | |
| div: int = 2, | |
| ) -> List[List[dict]]: | |
| """Compute per-patch image conditioning for the flow model.""" | |
| if pointmap_info is None: | |
| pointmap_info = PointmapInfo(image, device=self.device) | |
| patches = pointmap_info.divide_image(width, length, div) | |
| return [ | |
| [self.pipeline.get_cond([self.preprocess(patch)]) for patch in row] | |
| for row in patches | |
| ] | |
| # ----------------------------------------------------------------------- | |
| # Stage 1: Sparse structure sampling | |
| # ----------------------------------------------------------------------- | |
| def sample_sparse_structure( | |
| self, | |
| image: Image.Image, | |
| pointmap_info: PointmapInfo = None, | |
| optim: bool = True, | |
| width: int = 2, | |
| length: int = 2, | |
| div: int = 2, | |
| iterations: int = 3, | |
| steps: int = 25, | |
| rescale_t: float = 3.0, | |
| t_noise: float = 0.6, | |
| t_start: float = 0.8, | |
| cfg_strength: float = 7.5, | |
| alpha: float = 5.0, | |
| batch_size: int = 1, | |
| progress_callback=None, | |
| ) -> torch.Tensor: | |
| """ | |
| Sample occupied voxel coordinates via iterative flow-matching. | |
| Returns: | |
| coords: int32 tensor of shape [N, 4] (batch, y, x, z). | |
| """ | |
| if pointmap_info is None: | |
| pointmap_info = PointmapInfo(image, device=self.device) | |
| flow_model: SparseStructureFlowModel = self.models['sparse_structure_flow_model'] | |
| encoder: SparseStructureEncoder = self.models['sparse_structure_encoder'] | |
| decoder: SparseStructureDecoder = self.models['sparse_structure_decoder'] | |
| sampler = self.pipeline.sparse_structure_sampler | |
| cfg_interval = self.pipeline.sparse_structure_sampler_params['cfg_interval'] | |
| for p in decoder.parameters(): | |
| p.requires_grad_(False) | |
| sigma_min = sampler.sigma_min | |
| reso = flow_model.resolution | |
| # Build point cloud from the pointmap info | |
| pc = torch.tensor(pointmap_info.point_cloud(), dtype=torch.float32) | |
| pc[:, 2] *= max(width, length) | |
| # Encode initial voxel from the point cloud | |
| voxel = pointcloud_to_voxel(pc, (4 * reso * length, 4 * reso * width, 4 * reso)) | |
| voxel = voxel.permute(0, 1, 3, 2, 4).float().to(self.device) | |
| encoded_voxel = encoder(voxel) | |
| pc = pc.to(self.device) | |
| _, t_pairs = schedule(steps, rescale_t, start=t_start) | |
| views = get_views(width, length, reso, div) | |
| # Latent tensor and accumulation buffers | |
| latent = torch.randn(1, flow_model.in_channels, reso * width, reso * length, reso, | |
| device=self.device) | |
| count = torch.zeros_like(latent) | |
| value = torch.zeros_like(latent) | |
| global_cond = self.get_cond(image, pointmap_info, 1, 1, 1)[0][0] | |
| cond = self.get_cond(image, pointmap_info, width, length, div) | |
| total_steps = iterations * len(t_pairs) | |
| global_step = 0 | |
| iter_range = trange(iterations, position=0) if progress_callback is None else range(iterations) | |
| for it in iter_range: | |
| # Noise the latent to t_noise at the start of each iteration | |
| latent = diffuse(encoded_voxel, torch.tensor(t_noise, device=self.device), sigma_min) | |
| latent = latent.detach() | |
| step_iter = (tqdm(t_pairs, desc="Sparse Structure Sampling", position=1) | |
| if progress_callback is None else t_pairs) | |
| for t, t_prev in step_iter: | |
| cosine_factor = 0.5 * (1 + torch.cos(torch.pi * (1 - torch.tensor(t)))) | |
| c = cosine_factor ** alpha | |
| with torch.no_grad(): | |
| # --- 1. Overlapping patch-wise flow --- | |
| count.zero_() | |
| value.zero_() | |
| local_latents, patch_conds, patch_neg_conds, patch_views = [], [], [], [] | |
| for view in views: | |
| i, j, y0, y1, x0, x1 = view | |
| patch_views.append(view) | |
| local_latents.append(latent[:, :, y0:y1, x0:x1, :].contiguous()) | |
| patch_cond = cond[i][j] | |
| patch_conds.append(patch_cond['cond']) | |
| patch_neg_conds.append(patch_cond['neg_cond']) | |
| for start in range(0, len(local_latents), batch_size): | |
| end = min(start + batch_size, len(local_latents)) | |
| out = sampler.sample_once( | |
| flow_model, | |
| torch.cat(local_latents[start:end], dim=0), | |
| t, t_prev, | |
| cond=torch.cat(patch_conds[start:end], dim=0), | |
| neg_cond=torch.cat(patch_neg_conds[start:end], dim=0), | |
| cfg_strength=cfg_strength, | |
| cfg_interval=cfg_interval, | |
| ) | |
| for view, pred_v in zip(patch_views[start:end], out.pred_v): | |
| _, _, y0, y1, x0, x1 = view | |
| count[:, :, y0:y1, x0:x1, :] += 1 | |
| value[:, :, y0:y1, x0:x1, :] += pred_v | |
| local_pred_v = torch.where(count > 0, value / count, latent) | |
| # --- 2. Dilated sampling (global structure) --- | |
| count.zero_() | |
| value.zero_() | |
| dilated_samples = dilated_sampling(reso, width, length) | |
| dilated_latents = [] | |
| dilated_conds = [] | |
| dilated_neg_conds = [] | |
| for sample in dilated_samples: | |
| sample_latent = (latent[:, :, sample[:, 0], sample[:, 1], :] | |
| .view(1, flow_model.in_channels, reso, reso, reso)) | |
| dilated_latents.append(sample_latent) | |
| dilated_conds.append(global_cond['cond']) | |
| dilated_neg_conds.append(global_cond['neg_cond']) | |
| for start in range(0, len(dilated_latents), batch_size): | |
| end = min(start + batch_size, len(dilated_latents)) | |
| out = sampler.sample_once( | |
| flow_model, | |
| torch.cat(dilated_latents[start:end], dim=0), | |
| t, t_prev, | |
| cond=torch.cat(dilated_conds[start:end], dim=0), | |
| neg_cond=torch.cat(dilated_neg_conds[start:end], dim=0), | |
| cfg_strength=cfg_strength, | |
| cfg_interval=cfg_interval, | |
| ) | |
| for sample, pred_v in zip(dilated_samples[start:end], out.pred_v): | |
| count[:, :, sample[:, 0], sample[:, 1], :] += 1 | |
| value[:, :, sample[:, 0], sample[:, 1], :] += pred_v.view( | |
| 1, flow_model.in_channels, reso * reso, reso | |
| ) | |
| global_pred_v = torch.where(count > 0, value / count, latent) | |
| # Blend local and global velocity predictions | |
| v = local_pred_v * (1 - c) + global_pred_v * c | |
| v = v.detach() | |
| # Enable grad so that Adam can optimize v as a leaf variable | |
| v.requires_grad_() | |
| v.retain_grad() | |
| optimizer = torch.optim.Adam([v], lr=0.1) | |
| if optim and t < 0.7: | |
| for _ in range(20): | |
| optimizer.zero_grad() | |
| pred_latent = (1 - sigma_min) * latent - (sigma_min + (1 - sigma_min) * t) * v | |
| decoded_latent = decoder(pred_latent) | |
| loss = sparse_structure_loss(pc, decoded_latent.permute(0, 1, 3, 2, 4)) | |
| loss.backward() | |
| optimizer.step() | |
| # Euler step | |
| latent = (latent - (t - t_prev) * v).detach() | |
| if progress_callback is not None: | |
| global_step += 1 | |
| progress_callback( | |
| global_step / total_steps, | |
| f"Sparse Structure: iter {it + 1}/{iterations}, step {global_step}/{total_steps}", | |
| ) | |
| # Re-encode the decoded voxel for the next iteration | |
| voxel = (decoder(latent) > 0).float() | |
| encoded_voxel = encoder(voxel) | |
| coords = torch.argwhere(decoder(latent) > 0)[:, [0, 2, 3, 4]].int() | |
| return coords | |
| # ----------------------------------------------------------------------- | |
| # Stage 2: Structured latent (SLAT) sampling | |
| # ----------------------------------------------------------------------- | |
| def sample_slat( | |
| self, | |
| image: Image.Image, | |
| coords: torch.Tensor, | |
| pointmap_info: PointmapInfo = None, | |
| optim: bool = True, | |
| width: int = 2, | |
| length: int = 2, | |
| div: int = 2, | |
| steps: int = 25, | |
| rescale_t: float = 3.0, | |
| cfg_strength: float = 3.0, | |
| batch_size: int = 1, | |
| progress_callback=None, | |
| ) -> SparseTensor: | |
| """ | |
| Sample per-voxel latent features (SLAT) via flow-matching. | |
| Returns: | |
| slat: SparseTensor with denormalized latent features. | |
| """ | |
| if pointmap_info is None: | |
| pointmap_info = PointmapInfo(image, device=self.device) | |
| # Prepare reference image tensor for perceptual optimization loss | |
| resized_image = image.resize((512, 512)) | |
| tensor_image = (torch.from_numpy(np.array(resized_image)) | |
| .permute(2, 0, 1).float() / 255.0).to(self.device) | |
| intrinsic = torch.tensor(pointmap_info.camera_intrinsic(), dtype=torch.float32).to(self.device) | |
| extrinsic = torch.tensor(pointmap_info.camera_extrinsic(), dtype=torch.float32).to(self.device) | |
| flow_model = self.models['slat_flow_model'] | |
| sampler = self.pipeline.slat_sampler | |
| cfg_interval = self.pipeline.slat_sampler_params['cfg_interval'] | |
| cond = self.get_cond(image, pointmap_info, width, length, div) | |
| sigma_min = sampler.sigma_min | |
| reso = flow_model.resolution | |
| latent_feats = torch.randn(coords.shape[0], flow_model.in_channels, device=self.device) | |
| # Pre-compute where each voxel coordinate falls in the overlapping patch grid | |
| views = get_views(width, length, reso, div) | |
| valid_views = [] | |
| patch_indices = [] | |
| for i, j, y0, y1, x0, x1 in views: | |
| idx = torch.where( | |
| (coords[:, 1] >= y0) & (coords[:, 1] < y1) & | |
| (coords[:, 2] >= x0) & (coords[:, 2] < x1) | |
| )[0] | |
| if len(idx) > 0: | |
| valid_views.append((i, j, y0, y1, x0, x1)) | |
| patch_indices.append(idx) | |
| count = torch.zeros(coords.shape[0], flow_model.in_channels, device=self.device) | |
| value = torch.zeros(coords.shape[0], flow_model.in_channels, device=self.device) | |
| _, t_pairs = schedule(steps, rescale_t) | |
| total_steps = len(t_pairs) | |
| step_iter = (tqdm(t_pairs, desc="Structured Latent Sampling") | |
| if progress_callback is None else t_pairs) | |
| for slat_step, (t, t_prev) in enumerate(step_iter, start=1): | |
| with torch.no_grad(): | |
| count.zero_() | |
| value.zero_() | |
| patch_latents = [] | |
| patch_conds = [] | |
| for view, patch_index in zip(valid_views, patch_indices): | |
| i, j, y0, y1, x0, x1 = view | |
| patch_conds.append(cond[i][j]) | |
| patch_coords_local = coords[patch_index].clone() | |
| patch_coords_local[:, 1] -= y0 | |
| patch_coords_local[:, 2] -= x0 | |
| patch_latents.append(SparseTensor( | |
| feats=latent_feats[patch_index].contiguous(), | |
| coords=patch_coords_local, | |
| )) | |
| for start in range(0, len(patch_latents), batch_size): | |
| end = min(start + batch_size, len(patch_latents)) | |
| conds_chunk = patch_conds[start:end] | |
| batched_cond = { | |
| k: torch.cat([d[k] for d in conds_chunk], dim=0) | |
| for k in conds_chunk[0].keys() | |
| } | |
| outs = sampler.sample_once( | |
| flow_model, | |
| sparse_cat(patch_latents[start:end]), | |
| t, t_prev, | |
| cfg_strength=cfg_strength, | |
| cfg_interval=cfg_interval, | |
| **batched_cond, | |
| ) | |
| for out, pidx in zip(sparse_unbind(outs.pred_v, dim=0), patch_indices[start:end]): | |
| count[pidx, :] += 1 | |
| value[pidx, :] += out.feats | |
| v_feats = torch.where(count > 0, value / count, latent_feats).detach() | |
| # Enable grad for leaf-variable optimization | |
| v_feats.requires_grad_() | |
| optimizer = torch.optim.Adam([v_feats], lr=0.3) | |
| if optim and t < 0.8: | |
| for _ in range(20): | |
| optimizer.zero_grad() | |
| pred_feats = (1 - sigma_min) * latent_feats - (sigma_min + (1 - sigma_min) * t) * v_feats | |
| pred_slat = SparseTensor(feats=pred_feats, coords=coords) * self.std + self.mean | |
| rendered = render_utils.render_frames_torch( | |
| self.decode_slat(pred_slat, width, length, formats=['gaussian'])['gaussian'][0], | |
| [extrinsic], [intrinsic], | |
| {'resolution': 512, 'bg_color': (0, 0, 0)}, | |
| verbose=False, | |
| )['color'][0].permute(2, 1, 0) | |
| loss = (self.lpips(rendered.unsqueeze(0), tensor_image.unsqueeze(0)) | |
| - self.ssim(rendered.unsqueeze(0), tensor_image.unsqueeze(0))) | |
| loss.backward() | |
| optimizer.step() | |
| # Euler step; detach to free the computation graph | |
| latent_feats = (latent_feats - (t - t_prev) * v_feats).detach() | |
| if progress_callback is not None: | |
| progress_callback(slat_step / total_steps, | |
| f"SLAT Sampling: step {slat_step}/{total_steps}") | |
| slat = SparseTensor(feats=latent_feats, coords=coords) | |
| return slat * self.std + self.mean | |
| # ----------------------------------------------------------------------- | |
| # Stage 3: Decode SLAT → Gaussians and/or mesh | |
| # ----------------------------------------------------------------------- | |
| def decode_slat( | |
| self, | |
| slat: SparseTensor, | |
| width: int, | |
| length: int, | |
| formats: list[str] = ['gaussian', 'mesh'], | |
| ) -> dict: | |
| """Decode a structured latent into Gaussian splats and/or a triangle mesh.""" | |
| ret = {} | |
| feats = slat.feats | |
| coords = slat.coords | |
| reso = self.models['slat_flow_model'].resolution | |
| scale = max(width, length) | |
| # ------------------------------------------------------------------- | |
| # Mesh decoding | |
| # ------------------------------------------------------------------- | |
| if 'mesh' in formats: | |
| mesh_decoder = self.pipeline.models['slat_decoder_mesh'] | |
| sf2m = mesh_decoder.mesh_extractor # SparseFeatures2Mesh | |
| # Global high-res grid dimensions (4× upsampling from SLAT resolution) | |
| up_res = mesh_decoder.resolution * 4 | |
| res_y, res_x, res_z = width * up_res, length * up_res, up_res | |
| # Accumulate high-res sparse features across overlapping patches with cosine blending | |
| C = sf2m.feats_channels | |
| global_sum = torch.zeros(res_y, res_x, res_z, C, device=self.device) | |
| global_count = torch.zeros(res_y, res_x, res_z, 1, device=self.device) | |
| for _, _, y_start, y_end, x_start, x_end in get_views(width, length, reso, 4): | |
| patch_index = torch.where( | |
| (coords[:, 1] >= y_start) & (coords[:, 1] < y_end) & | |
| (coords[:, 2] >= x_start) & (coords[:, 2] < x_end) | |
| )[0] | |
| if len(patch_index) == 0: | |
| continue | |
| patch_coords = coords[patch_index].clone() | |
| patch_coords[:, 1] -= y_start | |
| patch_coords[:, 2] -= x_start | |
| patch_latent = SparseTensor( | |
| feats=feats[patch_index].contiguous(), | |
| coords=patch_coords, | |
| ) | |
| patch_hr = mesh_decoder.forward_features(patch_latent) | |
| # Cosine spatial weight: 1 at patch center, 0 at edges | |
| hr_coords = patch_hr.coords[:, 1:].clone() # [N, 3] | |
| patch_size = float(4 * reso) | |
| cos_w = (torch.cos(torch.pi * (hr_coords[:, 0].float() / patch_size - 0.5)) | |
| * torch.cos(torch.pi * (hr_coords[:, 1].float() / patch_size - 0.5)) | |
| ).unsqueeze(1) # [N, 1] | |
| # Shift to global coordinates | |
| hr_coords[:, 0] = (hr_coords[:, 0] + 4 * y_start).clamp(0, res_y - 1) | |
| hr_coords[:, 1] = (hr_coords[:, 1] + 4 * x_start).clamp(0, res_x - 1) | |
| hr_coords[:, 2] = hr_coords[:, 2].clamp(0, res_z - 1) | |
| gy, gx, gz = hr_coords[:, 0], hr_coords[:, 1], hr_coords[:, 2] | |
| global_sum [gy, gx, gz] += patch_hr.feats * cos_w | |
| global_count[gy, gx, gz] += cos_w | |
| # Average overlapping regions | |
| occupied = global_count[..., 0] > 0 | |
| global_sum[occupied] /= global_count[occupied] | |
| if occupied.any(): | |
| occ_coords = torch.argwhere(occupied) | |
| occ_feats = global_sum[occ_coords[:, 0], occ_coords[:, 1], occ_coords[:, 2]] | |
| # Extract per-cube SDF, deformation, color, and FlexiCubes weights | |
| sdf = sf2m.get_layout(occ_feats, 'sdf') + sf2m.sdf_bias # [N, 8, 1] | |
| deform = sf2m.get_layout(occ_feats, 'deform') # [N, 8, 3] | |
| color = sf2m.get_layout(occ_feats, 'color') # [N, 8, 6] or None | |
| weights = sf2m.get_layout(occ_feats, 'weights') # [N, 21] | |
| v_attrs_cat = (torch.cat([sdf, deform, color], dim=-1) | |
| if sf2m.use_color else torch.cat([sdf, deform], dim=-1)) | |
| # Merge cube corners into unique vertices | |
| v_pos, v_attrs, _ = sparse_cube2verts(occ_coords, v_attrs_cat, training=False) | |
| # Build flat dense vertex attribute array for the global grid | |
| res_vy, res_vx, res_vz = res_y + 1, res_x + 1, res_z + 1 | |
| v_attrs_d = torch.zeros(res_vy * res_vx * res_vz, v_attrs.shape[-1], device=self.device) | |
| v_attrs_d[:, 0] = 1.0 # SDF default: outside surface | |
| vert_ids = v_pos[:, 0] * res_vx * res_vz + v_pos[:, 1] * res_vz + v_pos[:, 2] | |
| v_attrs_d[vert_ids] = v_attrs | |
| sdf_d = v_attrs_d[:, 0] | |
| deform_d = v_attrs_d[:, 1:4] | |
| colors_d = v_attrs_d[:, 4:] if sf2m.use_color else None | |
| # Build flat dense cube weight array | |
| weights_d = torch.zeros(res_y * res_x * res_z, weights.shape[-1], device=self.device) | |
| cube_ids = occ_coords[:, 0] * res_x * res_z + occ_coords[:, 1] * res_z + occ_coords[:, 2] | |
| weights_d[cube_ids] = weights | |
| # Regular vertex position grid [V, 3], normalized to world space | |
| ay, ax, az = (torch.arange(r, device=self.device, dtype=torch.float) | |
| for r in (res_vy, res_vx, res_vz)) | |
| gy, gx, gz = torch.meshgrid(ay, ax, az, indexing='ij') | |
| reg_v = torch.stack([gy.flatten(), gx.flatten(), gz.flatten()], dim=1) | |
| # Normalize to Gaussian world coordinate convention: | |
| # y, x : [-0.5, 0.5] (centered) | |
| # z : [0, 1/scale] (not centered) | |
| norm_val = scale * up_res | |
| norm_t = torch.tensor([norm_val, norm_val, norm_val], device=self.device, dtype=torch.float) | |
| offset_t = torch.tensor([0.5, 0.5, 0.0], device=self.device, dtype=torch.float) | |
| x_nx3 = reg_v / norm_t - offset_t + (1 - 1e-8) / (norm_t * 2) * torch.tanh(deform_d) | |
| # Global cube → 8 corner vertex index table [C_total, 8] | |
| cy, cx, cz = (torch.arange(r, device=self.device) for r in (res_y, res_x, res_z)) | |
| gy, gx, gz = torch.meshgrid(cy, cx, cz, indexing='ij') | |
| cc = torch.tensor( | |
| [[0,0,0],[1,0,0],[0,1,0],[1,1,0],[0,0,1],[1,0,1],[0,1,1],[1,1,1]], | |
| dtype=torch.long, device=self.device, | |
| ) | |
| reg_c = ((gy.flatten().unsqueeze(1) + cc[:, 0]) * res_vx * res_vz | |
| + (gx.flatten().unsqueeze(1) + cc[:, 1]) * res_vz | |
| + (gz.flatten().unsqueeze(1) + cc[:, 2])) # [C, 8] | |
| # Single FlexiCubes call on the full global SDF | |
| vertices, faces, _, colors = sf2m.mesh_extractor( | |
| voxelgrid_vertices=x_nx3, | |
| scalar_field=sdf_d, | |
| cube_idx=reg_c, | |
| resolution=[res_y, res_x, res_z], | |
| beta=weights_d[:, :12], | |
| alpha=weights_d[:, 12:20], | |
| gamma_f=weights_d[:, 20], | |
| voxelgrid_colors=colors_d, | |
| training=False, | |
| ) | |
| ret['mesh'] = [MeshExtractResult( | |
| vertices=vertices, | |
| faces=faces, | |
| vertex_attrs=colors, | |
| res=max(res_y, res_x, res_z), | |
| )] | |
| else: | |
| ret['mesh'] = [] | |
| # ------------------------------------------------------------------- | |
| # Gaussian decoding | |
| # ------------------------------------------------------------------- | |
| if 'gaussian' in formats: | |
| gs_decoder = self.pipeline.models['slat_decoder_gs'] | |
| # Decode each patch and collect Gaussian lists per batch element | |
| all_patch_lists: list | None = None | |
| for i in range(width): | |
| for j in range(length): | |
| y0, y1 = i * reso, (i + 1) * reso | |
| x0, x1 = j * reso, (j + 1) * reso | |
| patch_index = torch.where( | |
| (coords[:, 1] >= y0) & (coords[:, 1] < y1) & | |
| (coords[:, 2] >= x0) & (coords[:, 2] < x1) | |
| )[0] | |
| if len(patch_index) == 0: | |
| continue | |
| patch_coords = coords[patch_index].clone() | |
| patch_coords[:, 1] -= y0 | |
| patch_coords[:, 2] -= x0 | |
| patch_latent = SparseTensor( | |
| feats=feats[patch_index].contiguous(), | |
| coords=patch_coords, | |
| ) | |
| patch_gaussians = gs_decoder(patch_latent) | |
| # Translate Gaussians to their world-space tile position | |
| offset = torch.tensor([[i + 0.5, j + 0.5, 0.5]], device=self.device) | |
| for g in patch_gaussians: | |
| g._xyz = g._xyz + offset | |
| if all_patch_lists is None: | |
| all_patch_lists = [[g] for g in patch_gaussians] | |
| else: | |
| for k, g in enumerate(patch_gaussians): | |
| all_patch_lists[k].append(g) | |
| # Concatenate all patches into a single Gaussian set per batch element | |
| merged_gaussians = [] | |
| for gs_list in all_patch_lists: | |
| g0 = gs_list[0] | |
| if len(gs_list) > 1: | |
| g0._features_dc = torch.cat([g._features_dc for g in gs_list], dim=0) | |
| g0._opacity = torch.cat([g._opacity for g in gs_list], dim=0) | |
| g0._rotation = torch.cat([g._rotation for g in gs_list], dim=0) | |
| g0._scaling = torch.cat([g._scaling for g in gs_list], dim=0) | |
| g0._xyz = torch.cat([g._xyz for g in gs_list], dim=0) | |
| merged_gaussians.append(g0) | |
| # Filter Gaussians with overly large kernels (outliers) | |
| for g in merged_gaussians: | |
| scale_norm = torch.sum(g.get_scaling ** 2, dim=1) ** 0.5 | |
| keep = torch.where(scale_norm < 0.03)[0] | |
| g._features_dc = g._features_dc[keep] | |
| g._opacity = g._opacity[keep] | |
| g._rotation = g._rotation[keep] | |
| g._scaling = g._scaling[keep] | |
| g._xyz = g._xyz[keep] | |
| # Normalize to world-space coordinate convention | |
| eps = 1e-4 | |
| center_offset = torch.tensor([[0.5, 0.5, 0.0]], device=self.device) | |
| for g in merged_gaussians: | |
| g.from_xyz(g.get_xyz / scale) | |
| g._xyz -= center_offset | |
| g.mininum_kernel_size /= scale | |
| g.from_scaling(torch.max( | |
| g.get_scaling / scale, | |
| torch.tensor(g.mininum_kernel_size * (1 + eps), device=self.device), | |
| )) | |
| ret['gaussian'] = merged_gaussians | |
| return ret | |
| # ----------------------------------------------------------------------- | |
| # Full pipeline | |
| # ----------------------------------------------------------------------- | |
| def run( | |
| self, | |
| image: Image.Image, | |
| width: int = 2, | |
| length: int = 2, | |
| div: int = 2, | |
| ss_optim: bool = True, | |
| ss_iterations: int = 3, | |
| ss_steps: int = 25, | |
| ss_rescale_t: float = 3.0, | |
| ss_t_noise: float = 0.6, | |
| ss_t_start: float = 0.8, | |
| ss_cfg_strength: float = 7.5, | |
| ss_alpha: float = 5.0, | |
| ss_batch_size: int = 1, | |
| slat_optim: bool = True, | |
| slat_steps: int = 25, | |
| slat_rescale_t: float = 3.0, | |
| slat_cfg_strength: float = 3.0, | |
| slat_batch_size: int = 1, | |
| formats: list = ['gaussian', 'mesh'], | |
| return_pointmap: bool = False, | |
| progress_callback=None, | |
| ) -> dict: | |
| """Run the full Extend3D pipeline: SS sampling → SLAT sampling → decode.""" | |
| pointmap_info = PointmapInfoMoGe(image, device=self.device) | |
| coords = self.sample_sparse_structure( | |
| image, pointmap_info, ss_optim, width, length, div, | |
| iterations=ss_iterations, | |
| steps=ss_steps, | |
| rescale_t=ss_rescale_t, | |
| t_noise=ss_t_noise, | |
| t_start=ss_t_start, | |
| cfg_strength=ss_cfg_strength, | |
| alpha=ss_alpha, | |
| batch_size=ss_batch_size, | |
| progress_callback=progress_callback, | |
| ).detach() | |
| slat = self.sample_slat( | |
| image, coords, pointmap_info, slat_optim, | |
| width, length, div, | |
| steps=slat_steps, | |
| rescale_t=slat_rescale_t, | |
| cfg_strength=slat_cfg_strength, | |
| batch_size=slat_batch_size, | |
| progress_callback=progress_callback, | |
| ) | |
| with torch.no_grad(): | |
| decoded = self.decode_slat(slat, width, length, formats=formats) | |
| if return_pointmap: | |
| return decoded, pointmap_info | |
| return decoded | |