Merge pull request #234 from MrForExample/dev

Add Craftman
This commit is contained in:
Mr. For Example
2024-07-23 15:15:29 -06:00
committed by GitHub
51 changed files with 10487 additions and 20 deletions
+1 -3
View File
@@ -161,13 +161,11 @@ cython_debug/
# Ignore test outputs
outputs/
output/
# Ignore folders only used for build dependencies
_Pre_Builds/_Build_Dependencies
_Pre_Builds/_Build_Wheels
_Pre_Builds/_Libs
models/
output/
.idea
+143
View File
@@ -0,0 +1,143 @@
name: michelangelo-image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-170k
description: ''
tag: michelangelo-aligned-autoencoder+n4096+noise0.0+pfeat3+zeroemb0.0+normembFalse+lr5e-05+qkvbiasFalse+nfreq8+ln_postTrue
seed: 0
use_timestamp: true
timestamp: ''
exp_root_dir: outputs
exp_dir: outputs/michelangelo-image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-170k
trial_name: michelangelo-aligned-autoencoder+n4096+noise0.0+pfeat3+zeroemb0.0+normembFalse+lr5e-05+qkvbiasFalse+nfreq8+ln_postTrue
trial_dir: outputs/michelangelo-image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-170k/michelangelo-aligned-autoencoder+n4096+noise0.0+pfeat3+zeroemb0.0+normembFalse+lr5e-05+qkvbiasFalse+nfreq8+ln_postTrue
n_gpus: 8
resume: ./ckpts/3DNativeGeneration/michelangelo-image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-170k.ckpt
data_type: objaverse-datamodule
data:
root_dir: data/objaverse_clean/cap3d_high_quality_170k_images
data_type: occupancy
n_samples: 4096
noise_sigma: 0.0
load_supervision: false
supervision_type: occupancy
n_supervision: 10000
load_image: true
image_data_path: data/objaverse_clean/raw_data/images/cap3d_high_quality_170k
image_type: mvrgb
idx:
- 0
- 4
- 8
- 12
- 16
n_views: 4
load_caption: false
rotate_points: false
batch_size: 32
num_workers: 16
system_type: shape-diffusion-system
system:
val_samples_json: val_data/mv_images/val_samples_rgb_mvimage.json
z_scale_factor: 1.0
guidance_scale: 7.5
num_inference_steps: 50
eta: 0.0
shape_model_type: michelangelo-aligned-autoencoder
shape_model:
num_latents: 256
embed_dim: 64
point_feats: 3
out_dim: 1
num_freqs: 8
include_pi: false
heads: 12
width: 768
num_encoder_layers: 8
num_decoder_layers: 16
use_ln_post: true
init_scale: 0.25
qkv_bias: false
use_flash: true
use_checkpoint: true
condition_model_type: clip-embedder
condition_model:
pretrained_model_name_or_path: openai/clip-vit-large-patch14
encode_camera: true
camera_embeds_dim: 32
n_views: 4
empty_embeds_ratio: 0.1
normalize_embeds: false
zero_uncond_embeds: true
denoiser_model_type: simple-denoiser
denoiser_model:
input_channels: 64
output_channels: 64
n_ctx: 256
width: 768
layers: 6
heads: 12
context_dim: 1024
init_scale: 1.0
skip_ln: true
use_checkpoint: true
noise_scheduler_type: diffusers.schedulers.DDPMScheduler
noise_scheduler:
num_train_timesteps: 1000
beta_start: 0.00085
beta_end: 0.012
beta_schedule: scaled_linear
variance_type: fixed_small
clip_sample: false
denoise_scheduler_type: diffusers.schedulers.DDIMScheduler
denoise_scheduler:
num_train_timesteps: 1000
beta_start: 0.00085
beta_end: 0.012
beta_schedule: scaled_linear
clip_sample: false
set_alpha_to_one: false
steps_offset: 1
loggers:
wandb:
enable: false
project: JiangXin
name: text-to-shape-diffusion+michelangelo-image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-170k+michelangelo-aligned-autoencoder+n4096+noise0.0+pfeat3+zeroemb0.0+normembFalse+lr5e-05+qkvbiasFalse+nfreq8+ln_postTrue
loss:
loss_type: mse
lambda_diffusion: 1.0
optimizer:
name: AdamW
args:
lr: 5.0e-05
betas:
- 0.9
- 0.99
eps: 1.0e-06
scheduler:
name: SequentialLR
interval: step
schedulers:
- name: LinearLR
interval: step
args:
start_factor: 1.0e-06
end_factor: 1.0
total_iters: 5000
- name: CosineAnnealingLR
interval: step
args:
T_max: 5000
eta_min: 0.0
milestones:
- 5000
trainer:
num_nodes: 2
max_epochs: 100000
log_every_n_steps: 5
num_sanity_val_steps: 1
check_val_every_n_epoch: 3
enable_progress_bar: true
precision: 16-mixed
strategy: ddp_find_unused_parameters_true
checkpoint:
save_last: true
save_top_k: -1
every_n_train_steps: 5000
@@ -194,7 +194,7 @@ class Mesh:
v1 = self.v_pos[i1, :]
v2 = self.v_pos[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0)
face_normals = torch.cross(v1 - v0, v2 - v0, dim=-1)
# Splat face normals to vertices
v_nrm = torch.zeros_like(self.v_pos)
@@ -48,7 +48,7 @@ def compute_vertex_normal(v_pos, t_pos_idx):
v1 = v_pos[i1, :]
v2 = v_pos[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0)
face_normals = torch.cross(v1 - v0, v2 - v0, dim=-1)
# Splat face normals to vertices
v_nrm = torch.zeros_like(v_pos)
@@ -350,9 +350,9 @@ def flip_edges(
e1 = v[:,1]
cl = v[:,2]
cr = v[:,3]
n = torch.cross(e1,cl) + torch.cross(cr,e1) #sum of old normal vectors
flip.logical_and_(torch.sum(n*torch.cross(cr,cl),dim=-1)>0) #first new face
flip.logical_and_(torch.sum(n*torch.cross(cl-e1,cr-e1),dim=-1)>0) #second new face
n = torch.cross(e1,cl,dim=-1) + torch.cross(cr,e1,dim=-1) #sum of old normal vectors
flip.logical_and_(torch.sum(n*torch.cross(cr,cl,dim=-1),dim=-1)>0) #first new face
flip.logical_and_(torch.sum(n*torch.cross(cl-e1,cr-e1,dim=-1),dim=-1)>0) #second new face
flip_edges_neighbors = edges_neighbors[flip] #E",4
flip_edge_to_face = edge_to_face[candidates,:,0][flip] #E",2
+52
View File
@@ -0,0 +1,52 @@
import importlib
__modules__ = {}
def register(name):
def decorator(cls):
if name in __modules__:
raise ValueError(
f"Module {name} already exists! Names of extensions conflict!"
)
else:
__modules__[name] = cls
return cls
return decorator
def find(name):
if name in __modules__:
return __modules__[name]
else:
try:
module_string = ".".join(name.split(".")[:-1])
cls_name = name.split(".")[-1]
module = importlib.import_module(module_string, package=None)
return getattr(module, cls_name)
except Exception as e:
raise ValueError(f"Module {name} not found!")
### grammar sugar for logging utilities ###
import logging
logger = logging.getLogger("pytorch_lightning")
from pytorch_lightning.utilities.rank_zero import (
rank_zero_debug,
rank_zero_info,
rank_zero_only,
)
debug = rank_zero_debug
info = rank_zero_info
@rank_zero_only
def warn(*args, **kwargs):
logger.warn(*args, **kwargs)
from . import data, models, systems
@@ -0,0 +1,3 @@
from . import (
objaverse
)
+311
View File
@@ -0,0 +1,311 @@
import math
import os
import json
from dataclasses import dataclass, field
import random
import imageio
import numpy as np
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from PIL import Image
from transformers import CLIPImageProcessor, CLIPTokenizer
from craftsman import register
from craftsman.utils.base import Updateable
from craftsman.utils.config import parse_structured
from craftsman.utils.typing import *
def rot2eul(R):
beta = -np.arcsin(R[2,0])
alpha = np.arctan2(R[2,1]/np.cos(beta),R[2,2]/np.cos(beta))
gamma = np.arctan2(R[1,0]/np.cos(beta),R[0,0]/np.cos(beta))
return np.array((alpha, beta, gamma))
def eul2rot(theta) :
R = np.array([[np.cos(theta[1])*np.cos(theta[2]), np.sin(theta[0])*np.sin(theta[1])*np.cos(theta[2]) - np.sin(theta[2])*np.cos(theta[0]), np.sin(theta[1])*np.cos(theta[0])*np.cos(theta[2]) + np.sin(theta[0])*np.sin(theta[2])],
[np.sin(theta[2])*np.cos(theta[1]), np.sin(theta[0])*np.sin(theta[1])*np.sin(theta[2]) + np.cos(theta[0])*np.cos(theta[2]), np.sin(theta[1])*np.sin(theta[2])*np.cos(theta[0]) - np.sin(theta[0])*np.cos(theta[2])],
[-np.sin(theta[1]), np.sin(theta[0])*np.cos(theta[1]), np.cos(theta[0])*np.cos(theta[1])]])
return R
@dataclass
class ObjaverseDataModuleConfig:
root_dir: str = None
data_type: str = "occupancy" # occupancy or sdf
n_samples: int = 4096 # number of points in input point cloud
scale: float = 1.0 # scale of the input point cloud and target supervision
noise_sigma: float = 0.0 # noise level of the input point cloud
load_supervision: bool = True # whether to load supervision
supervision_type: str = "occupancy" # occupancy, sdf, tsdf, tsdf_w_surface
n_supervision: int = 10000 # number of points in supervision
load_image: bool = False # whether to load images
image_data_path: str = "" # path to the image data
image_type: str = "rgb" # rgb, normal
background_color: Tuple[float, float, float] = field(
default_factory=lambda: (1.0, 1.0, 1.0)
)
idx: Optional[List[int]] = None # index of the image to load
n_views: int = 1 # number of views
rotate_points: bool = False # whether to rotate the input point cloud and the supervision
load_caption: bool = False # whether to load captions
caption_type: str = "text" # text, clip_embeds
tokenizer_pretrained_model_name_or_path: str = ""
batch_size: int = 32
num_workers: int = 0
class ObjaverseDataset(Dataset):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.cfg: ObjaverseDataModuleConfig = cfg
self.split = split
self.uids = json.load(open(f'{cfg.root_dir}/{split}.json'))
print(f"Loaded {len(self.uids)} {split} uids")
if self.cfg.load_caption:
self.tokenizer = CLIPTokenizer.from_pretrained(self.cfg.tokenizer_pretrained_model_name_or_path)
self.background_color = torch.as_tensor(self.cfg.background_color)
self.distance = 1.0
self.camera_embedding = torch.as_tensor([
[[1, 0, 0, 0],
[0, 0, -1, -self.distance],
[0, 1, 0, 0],
[0, 0, 0, 1]], # front to back
[[0, 0, 1, self.distance],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]], # right to left
[[-1, 0, 0, 0],
[0, 0, 1, self.distance],
[0, 1, 0, 0],
[0, 0, 0, 1]], # back to front
[[0, 0, -1, -self.distance],
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]], # left to right
], dtype=torch.float32)
if self.cfg.n_views != 1:
assert self.cfg.n_views == self.camera_embedding.shape[0]
def __len__(self):
return len(self.uids)
def _load_shape(self, index: int) -> Dict[str, Any]:
if self.cfg.data_type == "occupancy":
# for input point cloud
pointcloud = np.load(f'{self.cfg.root_dir}/{self.uids[index]}/pointcloud.npz')
surface = np.asarray(pointcloud['points']) * 2 # range from -1 to 1
normal = np.asarray(pointcloud['normals'])
surface = np.concatenate([surface, normal], axis=1)
elif self.cfg.data_type == "sdf":
data = np.load(f'{self.cfg.root_dir}/{self.uids[index]}.npz')
# for input point cloud
surface = data["surface"]
else:
raise NotImplementedError(f"Data type {self.cfg.data_type} not implemented")
# random sampling
rng = np.random.default_rng()
ind = rng.choice(surface.shape[0], self.cfg.n_samples, replace=False)
surface = surface[ind]
# rescale data
surface[:, :3] = surface[:, :3] * self.cfg.scale # target scale
# add noise to input point cloud
surface[:, :3] += (np.random.rand(surface.shape[0], 3) * 2 - 1) * self.cfg.noise_sigma
ret = {
"uid": self.uids[index].split('/')[-1],
"surface": surface.astype(np.float32),
}
return ret
def _load_shape_supervision(self, index: int) -> Dict[str, Any]:
# for supervision
ret = {}
if self.cfg.data_type == "occupancy":
points = np.load(f'{self.cfg.root_dir}/{self.uids[index]}/points.npz')
rand_points = np.asarray(points['points']) * 2 # range from -1.1 to 1.1
occupancies = np.asarray(points['occupancies'])
occupancies = np.unpackbits(occupancies)
elif self.cfg.data_type == "sdf":
data = np.load(f'{self.cfg.root_dir}/{self.uids[index]}.npz')
rand_points = data['rand_points']
sdfs = data['sdfs']
else:
raise NotImplementedError(f"Data type {self.cfg.data_type} not implemented")
# random sampling
rng = np.random.default_rng()
ind = rng.choice(rand_points.shape[0], self.cfg.n_supervision, replace=False)
rand_points = rand_points[ind]
rand_points = rand_points * self.cfg.scale
ret["rand_points"] = rand_points.astype(np.float32)
if self.cfg.data_type == "occupancy":
assert self.cfg.supervision_type == "occupancy", "Only occupancy supervision is supported for occupancy data"
occupancies = occupancies[ind]
ret["occupancies"] = occupancies.astype(np.float32)
elif self.cfg.data_type == "sdf":
if self.cfg.supervision_type == "sdf":
ret["sdf"] = sdfs[ind].flatten().astype(np.float32)
elif self.cfg.supervision_type == "occupancy":
ret["occupancies"] = np.where(sdfs[ind].flatten() < 1e-3, 0, 1).astype(np.float32)
else:
raise NotImplementedError(f"Supervision type {self.cfg.supervision_type} not implemented")
return ret
def _load_image(self, index: int) -> Dict[str, Any]:
def _load_single_image(img_path):
img = torch.from_numpy(
np.asarray(
Image.fromarray(imageio.v2.imread(img_path))
.convert("RGBA")
)
/ 255.0
).float()
mask: Float[Tensor, "H W 1"] = img[:, :, -1:]
image: Float[Tensor, "H W 3"] = img[:, :, :3] * mask + self.background_color[
None, None, :
] * (1 - mask)
return image
ret = {}
if self.cfg.image_type == "rgb" or self.cfg.image_type == "normal":
assert self.cfg.n_views == 1, "Only single view is supported for single image"
sel_idx = random.choice(self.cfg.idx)
ret["sel_image_idx"] = sel_idx
if self.cfg.image_type == "rgb":
img_path = f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f"/{sel_idx}.png"
elif self.cfg.image_type == "normal":
img_path = f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f"/{sel_idx}_normal.png"
ret["image"] = _load_single_image(img_path)
ret["c2w"] = self.camera_embedding[sel_idx % 4]
elif self.cfg.image_type == "mvrgb" or self.cfg.image_type == "mvnormal":
sel_idx = random.choice(self.cfg.idx)
ret["sel_image_idx"] = sel_idx
mvimages = []
for i in range(self.cfg.n_views):
if self.cfg.image_type == "mvrgb":
img_path = f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f"/{sel_idx+i}.png"
elif self.cfg.image_type == "mvnormal":
img_path = f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f"/{sel_idx+i}_normal.png"
mvimages.append(_load_single_image(img_path))
ret["mvimages"] = torch.stack(mvimages)
ret["c2ws"] = self.camera_embedding
else:
raise NotImplementedError(f"Image type {self.cfg.image_type} not implemented")
return ret
def _load_caption(self, index: int, drop_text_embed: bool = False) -> Dict[str, Any]:
ret = {}
if self.cfg.caption_type == "text":
caption = eval(json.load(open(f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f'/annotation.json')))
texts = [v for k, v in caption.items()]
sel_idx = random.randint(0, len(texts) - 1)
ret["sel_caption_idx"] = sel_idx
ret['text_input_ids'] = self.tokenizer(
texts[sel_idx] if not drop_text_embed else "",
max_length=self.tokenizer.model_max_length,
padding="max_length",
truncation=True,
return_tensors="pt"
).input_ids.detach()
else:
raise NotImplementedError(f"Caption type {self.cfg.caption_type} not implemented")
return ret
def get_data(self, index):
# load shape
ret = self._load_shape(index)
# load supervision for shape
if self.cfg.load_supervision:
ret.update(self._load_shape_supervision(index))
# load image
if self.cfg.load_image:
ret.update(self._load_image(index))
# load the rotation of the object and rotate the camera
rots = np.load(f'{self.cfg.image_data_path}/' + "/".join(self.uids[index].split('/')[-2:]) + f'/rots.npy')[ret['sel_image_idx']].astype(np.float32)
rots = torch.tensor(rots[:3, :3], dtype=torch.float32)
if "c2ws" in ret.keys():
ret["c2ws"][:, :3, :3] = torch.matmul(rots, ret["c2ws"][:, :3, :3])
ret["c2ws"][:, :3, 3] = torch.matmul(rots, ret["c2ws"][:, :3, 3].unsqueeze(-1)).squeeze(-1)
elif "c2w" in ret.keys():
ret["c2w"][:3, :3] = torch.matmul(rots, ret["c2w"][:3, :3])
ret["c2w"][:3, 3] = torch.matmul(rots, ret["c2w"][:3, 3].unsqueeze(-1)).squeeze(-1)
# load caption
if self.cfg.load_caption:
ret.update(self._load_caption(index))
return ret
def __getitem__(self, index):
try:
return self.get_data(index)
except Exception as e:
print(f"Error in {self.uids[index]}: {e}")
return self.__getitem__(np.random.randint(len(self)))
def collate(self, batch):
batch = torch.utils.data.default_collate(batch)
return batch
@register("objaverse-datamodule")
class ObjaverseDataModule(pl.LightningDataModule):
cfg: ObjaverseDataModuleConfig
def __init__(self, cfg: Optional[Union[dict, DictConfig]] = None) -> None:
super().__init__()
self.cfg = parse_structured(ObjaverseDataModuleConfig, cfg)
def setup(self, stage=None) -> None:
if stage in [None, "fit"]:
self.train_dataset = ObjaverseDataset(self.cfg, "train")
if stage in [None, "fit", "validate"]:
self.val_dataset = ObjaverseDataset(self.cfg, "val")
if stage in [None, "test", "predict"]:
self.test_dataset = ObjaverseDataset(self.cfg, "test")
def prepare_data(self):
pass
def general_loader(self, dataset, batch_size, collate_fn=None, num_workers=0) -> DataLoader:
return DataLoader(
dataset, batch_size=batch_size, collate_fn=collate_fn, num_workers=num_workers
)
def train_dataloader(self) -> DataLoader:
return self.general_loader(
self.train_dataset,
batch_size=self.cfg.batch_size,
collate_fn=self.train_dataset.collate,
num_workers=self.cfg.num_workers
)
def val_dataloader(self) -> DataLoader:
return self.general_loader(self.val_dataset, batch_size=1)
def test_dataloader(self) -> DataLoader:
return self.general_loader(self.test_dataset, batch_size=1)
def predict_dataloader(self) -> DataLoader:
return self.general_loader(self.test_dataset, batch_size=1)
@@ -0,0 +1,5 @@
from . import (
autoencoders,
conditional_encoders,
denoisers,
)
@@ -0,0 +1,3 @@
from . import (
michelangelo_autoencoder,
)
@@ -0,0 +1,403 @@
from dataclasses import dataclass
import math
import torch
import torch.nn as nn
from einops import repeat, rearrange
from transformers import CLIPModel
import craftsman
from craftsman.models.transformers.perceiver_1d import Perceiver
from craftsman.models.transformers.attention import ResidualCrossAttentionBlock
from craftsman.utils.checkpoint import checkpoint
from craftsman.utils.base import BaseModule
from craftsman.utils.typing import *
from .utils import AutoEncoder, FourierEmbedder, get_embedder
class PerceiverCrossAttentionEncoder(nn.Module):
def __init__(self,
use_downsample: bool,
num_latents: int,
embedder: FourierEmbedder,
point_feats: int,
embed_point_feats: bool,
width: int,
heads: int,
layers: int,
init_scale: float = 0.25,
qkv_bias: bool = True,
use_ln_post: bool = False,
use_flash: bool = False,
use_checkpoint: bool = False):
super().__init__()
self.use_checkpoint = use_checkpoint
self.num_latents = num_latents
self.use_downsample = use_downsample
self.embed_point_feats = embed_point_feats
if not self.use_downsample:
self.query = nn.Parameter(torch.randn((num_latents, width)) * 0.02)
self.embedder = embedder
if self.embed_point_feats:
self.input_proj = nn.Linear(self.embedder.out_dim * 2, width)
else:
self.input_proj = nn.Linear(self.embedder.out_dim + point_feats, width)
self.cross_attn = ResidualCrossAttentionBlock(
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash,
)
self.self_attn = Perceiver(
n_ctx=num_latents,
width=width,
layers=layers,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash,
use_checkpoint=False
)
if use_ln_post:
self.ln_post = nn.LayerNorm(width)
else:
self.ln_post = None
def _forward(self, pc, feats):
"""
Args:
pc (torch.FloatTensor): [B, N, 3]
feats (torch.FloatTensor or None): [B, N, C]
Returns:
"""
bs, N, D = pc.shape
data = self.embedder(pc)
if feats is not None:
if self.embed_point_feats:
feats = self.embedder(feats)
data = torch.cat([data, feats], dim=-1)
data = self.input_proj(data)
if self.use_downsample:
###### fps
from torch_cluster import fps
flattened = pc.view(bs*N, D)
batch = torch.arange(bs).to(pc.device)
batch = torch.repeat_interleave(batch, N)
pos = flattened
ratio = 1.0 * self.num_latents / N
idx = fps(pos, batch, ratio=ratio)
query = data.view(bs*N, -1)[idx].view(bs, -1, data.shape[-1])
else:
query = self.query
query = repeat(query, "m c -> b m c", b=bs)
latents = self.cross_attn(query, data)
latents = self.self_attn(latents)
if self.ln_post is not None:
latents = self.ln_post(latents)
return latents
def forward(self, pc: torch.FloatTensor, feats: Optional[torch.FloatTensor] = None):
"""
Args:
pc (torch.FloatTensor): [B, N, 3]
feats (torch.FloatTensor or None): [B, N, C]
Returns:
dict
"""
return checkpoint(self._forward, (pc, feats), self.parameters(), self.use_checkpoint)
class PerceiverCrossAttentionDecoder(nn.Module):
def __init__(self,
num_latents: int,
out_dim: int,
embedder: FourierEmbedder,
width: int,
heads: int,
init_scale: float = 0.25,
qkv_bias: bool = True,
use_flash: bool = False,
use_checkpoint: bool = False):
super().__init__()
self.use_checkpoint = use_checkpoint
self.embedder = embedder
self.query_proj = nn.Linear(self.embedder.out_dim, width)
self.cross_attn_decoder = ResidualCrossAttentionBlock(
n_data=num_latents,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash
)
self.ln_post = nn.LayerNorm(width)
self.output_proj = nn.Linear(width, out_dim)
def _forward(self, queries: torch.FloatTensor, latents: torch.FloatTensor):
queries = self.query_proj(self.embedder(queries))
x = self.cross_attn_decoder(queries, latents)
x = self.ln_post(x)
x = self.output_proj(x)
return x
def forward(self, queries: torch.FloatTensor, latents: torch.FloatTensor):
return checkpoint(self._forward, (queries, latents), self.parameters(), self.use_checkpoint)
@craftsman.register("michelangelo-autoencoder")
class MichelangeloAutoencoder(AutoEncoder):
r"""
A VAE model for encoding shapes into latents and decoding latent representations into shapes.
"""
@dataclass
class Config(BaseModule.Config):
pretrained_model_name_or_path: str = ""
use_downsample: bool = False
num_latents: int = 256
point_feats: int = 0
embed_point_feats: bool = False
out_dim: int = 1
embed_dim: int = 64
embed_type: str = "fourier"
num_freqs: int = 8
include_pi: bool = True
width: int = 768
heads: int = 12
num_encoder_layers: int = 8
num_decoder_layers: int = 16
init_scale: float = 0.25
qkv_bias: bool = True
use_ln_post: bool = False
use_flash: bool = False
use_checkpoint: bool = True
cfg: Config
def configure(self) -> None:
super().configure()
self.embedder = get_embedder(embed_type=self.cfg.embed_type, num_freqs=self.cfg.num_freqs, include_pi=self.cfg.include_pi)
# encoder
self.cfg.init_scale = self.cfg.init_scale * math.sqrt(1.0 / self.cfg.width)
self.encoder = PerceiverCrossAttentionEncoder(
use_downsample=self.cfg.use_downsample,
embedder=self.embedder,
num_latents=self.cfg.num_latents,
point_feats=self.cfg.point_feats,
embed_point_feats=self.cfg.embed_point_feats,
width=self.cfg.width,
heads=self.cfg.heads,
layers=self.cfg.num_encoder_layers,
init_scale=self.cfg.init_scale,
qkv_bias=self.cfg.qkv_bias,
use_ln_post=self.cfg.use_ln_post,
use_flash=self.cfg.use_flash,
use_checkpoint=self.cfg.use_checkpoint
)
if self.cfg.embed_dim > 0:
# VAE embed
self.pre_kl = nn.Linear(self.cfg.width, self.cfg.embed_dim * 2)
self.post_kl = nn.Linear(self.cfg.embed_dim, self.cfg.width)
self.latent_shape = (self.cfg.num_latents, self.cfg.embed_dim)
else:
self.latent_shape = (self.cfg.num_latents, self.cfg.width)
self.transformer = Perceiver(
n_ctx=self.cfg.num_latents,
width=self.cfg.width,
layers=self.cfg.num_decoder_layers,
heads=self.cfg.heads,
init_scale=self.cfg.init_scale,
qkv_bias=self.cfg.qkv_bias,
use_flash=self.cfg.use_flash,
use_checkpoint=self.cfg.use_checkpoint
)
# decoder
self.decoder = PerceiverCrossAttentionDecoder(
embedder=self.embedder,
out_dim=self.cfg.out_dim,
num_latents=self.cfg.num_latents,
width=self.cfg.width,
heads=self.cfg.heads,
init_scale=self.cfg.init_scale,
qkv_bias=self.cfg.qkv_bias,
use_flash=self.cfg.use_flash,
use_checkpoint=self.cfg.use_checkpoint
)
if self.cfg.pretrained_model_name_or_path != "":
print(f"Loading pretrained model from {self.cfg.pretrained_model_name_or_path}")
pretrained_ckpt = torch.load(self.cfg.pretrained_model_name_or_path, map_location="cpu")
if 'state_dict' in pretrained_ckpt:
_pretrained_ckpt = {}
for k, v in pretrained_ckpt['state_dict'].items():
if k.startswith('shape_model.'):
_pretrained_ckpt[k.replace('shape_model.', '')] = v
pretrained_ckpt = _pretrained_ckpt
self.load_state_dict(pretrained_ckpt, strict=True)
def encode(self,
surface: torch.FloatTensor,
sample_posterior: bool = True):
"""
Args:
surface (torch.FloatTensor): [B, N, 3+C]
sample_posterior (bool):
Returns:
shape_latents (torch.FloatTensor): [B, num_latents, width]
kl_embed (torch.FloatTensor): [B, num_latents, embed_dim]
posterior (DiagonalGaussianDistribution or None):
"""
assert surface.shape[-1] == 3 + self.cfg.point_feats, f"\
Expected {3 + self.cfg.point_feats} channels, got {surface.shape[-1]}"
pc, feats = surface[..., :3], surface[..., 3:] # B, n_samples, 3
shape_latents = self.encoder(pc, feats) # B, num_latents, width
kl_embed, posterior = self.encode_kl_embed(shape_latents, sample_posterior) # B, num_latents, embed_dim
return shape_latents, kl_embed, posterior
def decode(self,
latents: torch.FloatTensor):
"""
Args:
latents (torch.FloatTensor): [B, embed_dim]
Returns:
latents (torch.FloatTensor): [B, embed_dim]
"""
latents = self.post_kl(latents) # [B, num_latents, embed_dim] -> [B, num_latents, width]
return self.transformer(latents)
def query(self,
queries: torch.FloatTensor,
latents: torch.FloatTensor):
"""
Args:
queries (torch.FloatTensor): [B, N, 3]
latents (torch.FloatTensor): [B, embed_dim]
Returns:
logits (torch.FloatTensor): [B, N], occupancy logits
"""
logits = self.decoder(queries, latents).squeeze(-1)
return logits
@craftsman.register("michelangelo-aligned-autoencoder")
class MichelangeloAlignedAutoencoder(MichelangeloAutoencoder):
r"""
A VAE model for encoding shapes into latents and decoding latent representations into shapes.
"""
@dataclass
class Config(MichelangeloAutoencoder.Config):
clip_model_version: Optional[str] = None
cfg: Config
def configure(self) -> None:
if self.cfg.clip_model_version is not None:
self.clip_model: CLIPModel = CLIPModel.from_pretrained(self.cfg.clip_model_version)
self.projection = nn.Parameter(torch.empty(self.cfg.width, self.clip_model.projection_dim))
self.logit_scale = torch.exp(self.clip_model.logit_scale.data)
nn.init.normal_(self.projection, std=self.clip_model.projection_dim ** -0.5)
else:
self.projection = nn.Parameter(torch.empty(self.cfg.width, 768))
nn.init.normal_(self.projection, std=768 ** -0.5)
self.cfg.num_latents = self.cfg.num_latents + 1
super().configure()
def encode(self,
surface: torch.FloatTensor,
sample_posterior: bool = True):
"""
Args:
surface (torch.FloatTensor): [B, N, 3+C]
sample_posterior (bool):
Returns:
latents (torch.FloatTensor)
posterior (DiagonalGaussianDistribution or None):
"""
assert surface.shape[-1] == 3 + self.cfg.point_feats, f"\
Expected {3 + self.cfg.point_feats} channels, got {surface.shape[-1]}"
pc, feats = surface[..., :3], surface[..., 3:] # B, n_samples, 3
shape_latents = self.encoder(pc, feats) # B, num_latents, width
shape_embeds = shape_latents[:, 0] # B, width
shape_latents = shape_latents[:, 1:] # B, num_latents-1, width
kl_embed, posterior = self.encode_kl_embed(shape_latents, sample_posterior) # B, num_latents, embed_dim
shape_embeds = shape_embeds @ self.projection
return shape_embeds, kl_embed, posterior
def forward(self,
surface: torch.FloatTensor,
queries: torch.FloatTensor,
sample_posterior: bool = True):
"""
Args:
surface (torch.FloatTensor): [B, N, 3+C]
queries (torch.FloatTensor): [B, P, 3]
sample_posterior (bool):
Returns:
shape_embeds (torch.FloatTensor): [B, width]
latents (torch.FloatTensor): [B, num_latents, embed_dim]
posterior (DiagonalGaussianDistribution or None).
logits (torch.FloatTensor): [B, P]
"""
shape_embeds, kl_embed, posterior = self.encode(surface, sample_posterior=sample_posterior)
latents = self.decode(kl_embed) # [B, num_latents - 1, width]
logits = self.query(queries, latents) # [B,]
return shape_embeds, latents, posterior, logits
@@ -0,0 +1,302 @@
from dataclasses import dataclass
import torch
import torch.nn as nn
from torch import distributed as tdist
from torch.nn import functional as F
import math
import mcubes
import numpy as np
from einops import repeat, rearrange
from skimage import measure
from craftsman.utils.base import BaseModule
from craftsman.utils.typing import *
from craftsman.utils.misc import get_world_size
from craftsman.utils.ops import generate_dense_grid_points
VALID_EMBED_TYPES = ["identity", "fourier", "hashgrid", "sphere_harmonic", "triplane_fourier"]
class FourierEmbedder(nn.Module):
def __init__(self,
num_freqs: int = 6,
logspace: bool = True,
input_dim: int = 3,
include_input: bool = True,
include_pi: bool = True) -> None:
super().__init__()
if logspace:
frequencies = 2.0 ** torch.arange(
num_freqs,
dtype=torch.float32
)
else:
frequencies = torch.linspace(
1.0,
2.0 ** (num_freqs - 1),
num_freqs,
dtype=torch.float32
)
if include_pi:
frequencies *= torch.pi
self.register_buffer("frequencies", frequencies, persistent=False)
self.include_input = include_input
self.num_freqs = num_freqs
self.out_dim = self.get_dims(input_dim)
def get_dims(self, input_dim):
temp = 1 if self.include_input or self.num_freqs == 0 else 0
out_dim = input_dim * (self.num_freqs * 2 + temp)
return out_dim
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.num_freqs > 0:
embed = (x[..., None].contiguous() * self.frequencies).view(*x.shape[:-1], -1)
if self.include_input:
return torch.cat((x, embed.sin(), embed.cos()), dim=-1)
else:
return torch.cat((embed.sin(), embed.cos()), dim=-1)
else:
return x
class LearnedFourierEmbedder(nn.Module):
def __init__(self, input_dim, dim):
super().__init__()
assert (dim % 2) == 0
half_dim = dim // 2
per_channel_dim = half_dim // input_dim
self.weights = nn.Parameter(torch.randn(per_channel_dim))
self.out_dim = self.get_dims(input_dim)
def forward(self, x):
# [b, t, c, 1] * [1, d] = [b, t, c, d] -> [b, t, c * d]
freqs = (x[..., None] * self.weights[None] * 2 * np.pi).view(*x.shape[:-1], -1)
fouriered = torch.cat((x, freqs.sin(), freqs.cos()), dim=-1)
return fouriered
def get_dims(self, input_dim):
return input_dim * (self.weights.shape[0] * 2 + 1)
class Sine(nn.Module):
def __init__(self, w0 = 1.):
super().__init__()
self.w0 = w0
def forward(self, x):
return torch.sin(self.w0 * x)
class Siren(nn.Module):
def __init__(
self,
in_dim,
out_dim,
w0 = 1.,
c = 6.,
is_first = False,
use_bias = True,
activation = None,
dropout = 0.
):
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
self.is_first = is_first
weight = torch.zeros(out_dim, in_dim)
bias = torch.zeros(out_dim) if use_bias else None
self.init_(weight, bias, c = c, w0 = w0)
self.weight = nn.Parameter(weight)
self.bias = nn.Parameter(bias) if use_bias else None
self.activation = Sine(w0) if activation is None else activation
self.dropout = nn.Dropout(dropout)
def init_(self, weight, bias, c, w0):
dim = self.in_dim
w_std = (1 / dim) if self.is_first else (math.sqrt(c / dim) / w0)
weight.uniform_(-w_std, w_std)
if bias is not None:
bias.uniform_(-w_std, w_std)
def forward(self, x):
out = F.linear(x, self.weight, self.bias)
out = self.activation(out)
out = self.dropout(out)
return out
def get_embedder(embed_type="fourier", num_freqs=-1, input_dim=3, include_pi=True):
if embed_type == "identity" or (embed_type == "fourier" and num_freqs == -1):
return nn.Identity(), input_dim
elif embed_type == "fourier":
embedder_obj = FourierEmbedder(num_freqs=num_freqs, include_pi=include_pi)
elif embed_type == "learned_fourier":
embedder_obj = LearnedFourierEmbedder(in_channels=input_dim, dim=num_freqs)
elif embed_type == "siren":
embedder_obj = Siren(in_dim=input_dim, out_dim=num_freqs * input_dim * 2 + input_dim)
elif embed_type == "hashgrid":
raise NotImplementedError
elif embed_type == "sphere_harmonic":
raise NotImplementedError
else:
raise ValueError(f"{embed_type} is not valid. Currently only supprts {VALID_EMBED_TYPES}")
return embedder_obj
###################### AutoEncoder
class AutoEncoder(BaseModule):
@dataclass
class Config(BaseModule.Config):
pretrained_model_name_or_path: str = ""
num_latents: int = 256
embed_dim: int = 64
width: int = 768
cfg: Config
def configure(self) -> None:
super().configure()
def encode(self, x: torch.FloatTensor) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
raise NotImplementedError
def decode(self, z: torch.FloatTensor) -> torch.FloatTensor:
raise NotImplementedError
def encode_kl_embed(self, latents: torch.FloatTensor, sample_posterior: bool = True):
posterior = None
if self.cfg.embed_dim > 0:
moments = self.pre_kl(latents)
posterior = DiagonalGaussianDistribution(moments, feat_dim=-1)
if sample_posterior:
kl_embed = posterior.sample()
else:
kl_embed = posterior.mode()
else:
kl_embed = latents
return kl_embed, posterior
def forward(self,
surface: torch.FloatTensor,
queries: torch.FloatTensor,
sample_posterior: bool = True):
shape_latents, kl_embed, posterior = self.encode(surface, sample_posterior=sample_posterior)
latents = self.decode(kl_embed) # [B, num_latents, width]
logits = self.query(queries, latents) # [B,]
return shape_latents, latents, posterior, logits
def query(self, queries: torch.FloatTensor, latents: torch.FloatTensor) -> torch.FloatTensor:
raise NotImplementedError
@torch.no_grad()
def extract_geometry(self,
latents: torch.FloatTensor,
bounds: Union[Tuple[float], List[float], float] = (-1.05, -1.05, -1.05, 1.05, 1.05, 1.05),
grids_resolution: int = 256,
num_chunks: int = 10000,
):
if isinstance(bounds, float):
bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
bbox_min = np.array(bounds[0:3])
bbox_max = np.array(bounds[3:6])
bbox_size = bbox_max - bbox_min
xyz_samples, grid_size, length = generate_dense_grid_points(
bbox_min=bbox_min,
bbox_max=bbox_max,
grids_resolution=grids_resolution,
indexing="ij"
)
xyz_samples = torch.FloatTensor(xyz_samples)
batch_size = latents.shape[0]
batch_logits = []
for start in range(0, xyz_samples.shape[0], num_chunks):
queries = xyz_samples[start: start + num_chunks, :].to(latents)
batch_queries = repeat(queries, "p c -> b p c", b=batch_size)
logits = self.query(batch_queries, latents)
batch_logits.append(logits.cpu())
grid_logits = torch.cat(batch_logits, dim=1).view((batch_size, grid_size[0], grid_size[1], grid_size[2])).float().numpy()
mesh_v_f = []
has_surface = np.zeros((batch_size,), dtype=np.bool_)
for i in range(batch_size):
try:
vertices, faces, normals, _ = measure.marching_cubes(grid_logits[i], 0, method="lewiner")
# vertices, faces = mcubes.marching_cubes(grid_logits[i], 0)
vertices = vertices / grid_size * bbox_size + bbox_min
faces = faces[:, [2, 1, 0]]
mesh_v_f.append((vertices.astype(np.float32), np.ascontiguousarray(faces)))
has_surface[i] = True
except:
mesh_v_f.append((None, None))
has_surface[i] = False
return mesh_v_f, has_surface
class DiagonalGaussianDistribution(object):
def __init__(self, parameters: Union[torch.Tensor, List[torch.Tensor]], deterministic=False, feat_dim=1):
self.feat_dim = feat_dim
self.parameters = parameters
if isinstance(parameters, list):
self.mean = parameters[0]
self.logvar = parameters[1]
else:
self.mean, self.logvar = torch.chunk(parameters, 2, dim=feat_dim)
self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
self.deterministic = deterministic
self.std = torch.exp(0.5 * self.logvar)
self.var = torch.exp(self.logvar)
if self.deterministic:
self.var = self.std = torch.zeros_like(self.mean)
def sample(self):
x = self.mean + self.std * torch.randn_like(self.mean)
return x
def kl(self, other=None, dims=(1, 2)):
if self.deterministic:
return torch.Tensor([0.])
else:
if other is None:
return 0.5 * torch.mean(torch.pow(self.mean, 2)
+ self.var - 1.0 - self.logvar,
dim=dims)
else:
return 0.5 * torch.mean(
torch.pow(self.mean - other.mean, 2) / other.var
+ self.var / other.var - 1.0 - self.logvar + other.logvar,
dim=dims)
def nll(self, sample, dims=(1, 2)):
if self.deterministic:
return torch.Tensor([0.])
logtwopi = np.log(2.0 * np.pi)
return 0.5 * torch.sum(
logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
dim=dims)
def mode(self):
return self.mean
@@ -0,0 +1,3 @@
from . import (
clip_encoder,
)
@@ -0,0 +1,138 @@
import random
import torch
import torch.nn as nn
import numpy as np
from PIL import Image
from dataclasses import dataclass
from torchvision.transforms import Normalize
from torchvision.transforms import InterpolationMode
from torchvision.transforms.transforms import _interpolation_modes_from_int
from transformers import CLIPModel, CLIPTokenizer, CLIPImageProcessor
from transformers.utils import ModelOutput
from typing import Iterable, Optional, Union, List
from craftsman.utils.base import BaseModule
from craftsman.utils.typing import *
ImageType = Union[np.ndarray, torch.Tensor, Image.Image]
class BaseEmbedder(BaseModule):
@dataclass
class Config(BaseModule.Config):
pretrained_model_name_or_path: Optional[str] = None # the pretrained model name or path
encode_camera: bool = False # whether to encode camera
camera_embeds_type: str = "sincos" # the type of camera embeds
camera_embeds_dim: Optional[int] = None # the dimension of camera embeds
n_views: int = 1 # the number of views
empty_embeds_ratio: float = 0.1 # the ratio of empty embeds
zero_uncond_embeds: bool = True
normalize_embeds: bool = False # whether to normalize the embeds
cfg: Config
def configure(self) -> None:
super().configure()
if self.cfg.encode_camera:
self.distance = 1.0
self.register_buffer(
"cameras",
torch.as_tensor([
[[1, 0, 0, 0],
[0, 0, -1, -self.distance],
[0, 1, 0, 0],
[0, 0, 0, 1]], # front to back
[[0, 0, 1, self.distance],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]], # right to left
[[-1, 0, 0, 0],
[0, 0, 1, self.distance],
[0, 1, 0, 0],
[0, 0, 0, 1]], # back to front
[[0, 0, -1, -self.distance],
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]], # left to right
], dtype=torch.float32),
)
def encode_image(self, images: Iterable[Optional[ImageType]], camera_embeds: Optional[torch.Tensor] = None, **kwargs) -> torch.FloatTensor:
pass
def encode_text(self, texts: List[str], **kwargs) -> torch.FloatTensor:
pass
def encode_camera(self, c2ws: torch.Tensor):
if self.cfg.camera_embeds_type == "sincos":
assert c2ws.shape[-1] == 4 and c2ws.shape[-2] == 4, f"Invalid c2ws shape: {c2ws.shape}"
c2ws = c2ws.view(-1, 16)
return torch.cat([torch.sin(c2ws), torch.cos(c2ws)], dim=-1)
else:
raise NotImplementedError(f"Unknown camera_embeds_type: {self.cfg.camera_embeds_type}")
def post_process_embeds(self, text_embeds, visual_embeds):
bs = text_embeds.shape[0] if text_embeds is not None else visual_embeds.shape[0]
if self.cfg.normalize_embeds:
# post-process the text/visual embeds
if text_embeds is not None:
text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)
if visual_embeds is not None:
visual_embeds = visual_embeds / visual_embeds.norm(dim=-1, keepdim=True)
assert text_embeds is not None or visual_embeds is not None
# return text_embeds, visual_embeds
if text_embeds is not None and visual_embeds is not None:
return torch.cat([text_embeds, visual_embeds], dim=1)
elif text_embeds is not None:
return text_embeds
else:
return visual_embeds
def forward(self, batch):
bs = batch["surface"].shape[0]
text_embeds, visual_embeds = None, None
if random.random() < self.cfg.empty_embeds_ratio:
if "text_input_ids" in batch or "text_embeds" in batch:
if self.empty_text_embeds is None:
if not self.cfg.zero_uncond_embeds:
self.empty_text_embeds = self.encode_text([""]).detach() # [1, 77, 768]
text_embeds = self.empty_text_embeds.repeat(bs, 1, 1)
if "image" in batch or "image_embeds" in batch:
visual_embeds = self.empty_image_embeds.repeat(bs, 1, 1)
elif "mvimages" in batch or "mvimage_embeds" in batch:
visual_embeds = self.empty_image_embeds.unsqueeze(1).repeat(bs, 1, 1, 1)
else:
# for text inputs
if "text_input_ids" in batch:
text_embeds = self.encode_text(batch["text_input_ids"])
# for visual inputs
if "image" in batch:
if self.cfg.encode_camera:
visual_embeds = self.encode_image(batch["image"], cameras=batch["c2w"])
else:
visual_embeds = self.encode_image(batch["image"])
elif "mvimages" in batch:
n_views = batch["mvimages"].shape[1]
if self.cfg.encode_camera:
visual_embeds = self.encode_image(
batch["mvimages"].view(-1, *batch["mvimages"].shape[-3:]), \
cameras=batch["c2ws"]).view(bs, n_views, *self.empty_image_embeds.shape[-2:])
else:
visual_embeds = self.encode_image(
batch["mvimages"].view(-1, *batch["mvimages"].shape[-3:])).view(bs, n_views, *self.empty_image_embeds.shape[-2:])
return self.post_process_embeds(text_embeds, visual_embeds)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,385 @@
# coding=utf-8
# Copyright 2023 Meta AI and The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Reference:
# * transformers/models/dinov2/modeling_dinov2.py
# * https://github.com/facebookresearch/DiT/blob/main/models.py#L101
# * https://github.com/3DTopia/OpenLRM/tree/main/openlrm/models/encoders/dinov2
""" PyTorch CLIP model."""
from typing import Dict, List, Optional, Set, Tuple, Union
import torch
import torch.nn as nn
from .modeling_clip import (
CLIPConfig,
CLIPTextConfig,
CLIPVisionConfig,
CLIPEncoderLayer,
CLIPTextTransformer,
CLIPVisionTransformer,
CLIPModel,
CLIPVisionEmbeddings,
CLIPVisionModel,
CLIPOutput,
BaseModelOutput,
BaseModelOutputWithPooling
)
class ModLN(nn.Module):
def __init__(self, inner_dim: int, mod_dim: int = 32):
super().__init__()
self.mlp = nn.Sequential(
nn.SiLU(),
nn.Linear(mod_dim, inner_dim * 2),
)
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.zeros_(m.weight)
nn.init.zeros_(m.bias)
def forward(self, x:torch.Tensor, condition:torch.Tensor):
'''
x: [N, M, C_in], M: num of tokens
condition: [N, C_mod]
'''
shift, scale = self.mlp(condition).unsqueeze(1).chunk(2, dim=-1)
return x * (1 + scale) + shift
class ConditionalCLIPVisionConfig(CLIPVisionConfig):
def __init__(self, modulation_dim: int = 32, *args, **kwargs):
super().__init__(*args, **kwargs)
self.modulation_dim = modulation_dim
class ConditionalCLIPEncoderLayer(CLIPEncoderLayer):
"""This corresponds to the Block class in the original implementation."""
def __init__(self, config: ConditionalCLIPVisionConfig) -> None:
super().__init__(config)
self.mod_norm1 = ModLN(config.hidden_size, config.modulation_dim)
self.mod_norm2 = ModLN(config.hidden_size, config.modulation_dim)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor,
causal_attention_mask: torch.Tensor,
condition: Optional[torch.Tensor] = None,
output_attentions: bool = False,
) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]:
residual = hidden_states
hidden_states = self.mod_norm1(self.layer_norm1(hidden_states), condition)
hidden_states, attn_weights = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
causal_attention_mask=causal_attention_mask,
output_attentions=output_attentions,
)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.mod_norm2(self.layer_norm2(hidden_states), condition)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
outputs = (hidden_states,)
if output_attentions:
outputs += (attn_weights,)
return outputs
class ConditionalCLIPEncoder(nn.Module):
def __init__(self, config: CLIPConfig) -> None:
super().__init__()
self.config = config
self.layers = nn.ModuleList([ConditionalCLIPEncoderLayer(config) for _ in range(config.num_hidden_layers)])
self.gradient_checkpointing = False
def forward(
self,
inputs_embeds,
attention_mask: Optional[torch.Tensor] = None,
causal_attention_mask: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
condition: Optional[torch.Tensor] = None,
return_dict: Optional[bool] = None,
) -> Union[tuple, BaseModelOutput]:
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
encoder_states = () if output_hidden_states else None
all_attentions = () if output_attentions else None
hidden_states = inputs_embeds
for idx, encoder_layer in enumerate(self.layers):
if output_hidden_states:
encoder_states = encoder_states + (hidden_states,)
if self.gradient_checkpointing and self.training:
layer_outputs = self._gradient_checkpointing_func(
encoder_layer.__call__,
hidden_states,
attention_mask,
causal_attention_mask,
condition=condition,
output_attentions=output_attentions,
)
else:
layer_outputs = encoder_layer(
hidden_states,
attention_mask,
causal_attention_mask,
condition=condition,
output_attentions=output_attentions,
)
hidden_states = layer_outputs[0]
if output_attentions:
all_attentions = all_attentions + (layer_outputs[1],)
if output_hidden_states:
encoder_states = encoder_states + (hidden_states,)
if not return_dict:
return tuple(v for v in [hidden_states, encoder_states, all_attentions] if v is not None)
return BaseModelOutput(
last_hidden_state=hidden_states, hidden_states=encoder_states, attentions=all_attentions
)
class ConditionalCLIPVisionTransformer(CLIPVisionTransformer):
def __init__(self, config: ConditionalCLIPVisionConfig):
super().__init__(config)
self.config = config
embed_dim = config.hidden_size
self.embeddings = CLIPVisionEmbeddings(config)
self.pre_layrnorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
self.encoder = ConditionalCLIPEncoder(config)
self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
def forward(
self,
pixel_values: Optional[torch.FloatTensor] = None,
condition: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple, BaseModelOutputWithPooling]:
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
if pixel_values is None:
raise ValueError("You have to specify pixel_values")
hidden_states = self.embeddings(pixel_values)
hidden_states = self.pre_layrnorm(hidden_states)
encoder_outputs = self.encoder(
inputs_embeds=hidden_states,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
condition=condition,
return_dict=return_dict,
)
last_hidden_state = encoder_outputs[0]
pooled_output = last_hidden_state[:, 0, :]
pooled_output = self.post_layernorm(pooled_output)
if not return_dict:
return (last_hidden_state, pooled_output) + encoder_outputs[1:]
return BaseModelOutputWithPooling(
last_hidden_state=last_hidden_state,
pooler_output=pooled_output,
hidden_states=encoder_outputs.hidden_states,
attentions=encoder_outputs.attentions,
)
class ConditionalCLIPVisionModel(CLIPVisionModel):
config_class = ConditionalCLIPVisionConfig
def __init__(self, config: ConditionalCLIPVisionConfig):
super().__init__(config)
self.vision_model = ConditionalCLIPVisionTransformer(config)
# Initialize weights and apply final processing
self.post_init()
def forward(
self,
pixel_values: Optional[torch.FloatTensor] = None,
condition: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple, BaseModelOutputWithPooling]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
return self.vision_model(
pixel_values=pixel_values,
condition=condition,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
class ConditionalCLIPModel(CLIPModel):
config_class = CLIPConfig
def __init__(self, config: CLIPConfig):
super().__init__(config)
if not isinstance(config.text_config, CLIPTextConfig):
raise ValueError(
"config.text_config is expected to be of type CLIPTextConfig but is of type"
f" {type(config.text_config)}."
)
if not isinstance(config.vision_config, CLIPVisionConfig):
raise ValueError(
"config.vision_config is expected to be of type CLIPVisionConfig but is of type"
f" {type(config.vision_config)}."
)
text_config = config.text_config
vision_config = config.vision_config
self.projection_dim = config.projection_dim
self.text_embed_dim = text_config.hidden_size
self.vision_embed_dim = vision_config.hidden_size
self.text_model = CLIPTextTransformer(text_config)
self.vision_model = ConditionalCLIPVisionTransformer(vision_config)
self.visual_projection = nn.Linear(self.vision_embed_dim, self.projection_dim, bias=False)
self.text_projection = nn.Linear(self.text_embed_dim, self.projection_dim, bias=False)
self.logit_scale = nn.Parameter(torch.tensor(self.config.logit_scale_init_value))
# Initialize weights and apply final processing
self.post_init()
def get_image_features(
self,
pixel_values: Optional[torch.FloatTensor] = None,
condition: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> torch.FloatTensor:
# Use CLIP model's config for some fields (if specified) instead of those of vision & text components.
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
vision_outputs = self.vision_model(
pixel_values=pixel_values,
condition=condition,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
pooled_output = vision_outputs[1] # pooled_output
image_features = self.visual_projection(pooled_output)
return image_features
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
pixel_values: Optional[torch.FloatTensor] = None,
condition: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
return_loss: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple, CLIPOutput]:
# Use CLIP model's config for some fields (if specified) instead of those of vision & text components.
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
vision_outputs = self.vision_model(
pixel_values=pixel_values,
condition=condition,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
text_outputs = self.text_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
image_embeds = vision_outputs[1]
image_embeds = self.visual_projection(image_embeds)
text_embeds = text_outputs[1]
text_embeds = self.text_projection(text_embeds)
# normalized features
image_embeds = image_embeds / image_embeds.norm(p=2, dim=-1, keepdim=True)
text_embeds = text_embeds / text_embeds.norm(p=2, dim=-1, keepdim=True)
# cosine similarity as logits
logit_scale = self.logit_scale.exp()
logits_per_text = torch.matmul(text_embeds, image_embeds.t()) * logit_scale
logits_per_image = logits_per_text.t()
loss = None
if return_loss:
loss = clip_loss(logits_per_text)
if not return_dict:
output = (logits_per_image, logits_per_text, text_embeds, image_embeds, text_outputs, vision_outputs)
return ((loss,) + output) if loss is not None else output
return CLIPOutput(
loss=loss,
logits_per_image=logits_per_image,
logits_per_text=logits_per_text,
text_embeds=text_embeds,
image_embeds=image_embeds,
text_model_output=text_outputs,
vision_model_output=vision_outputs,
)
@@ -0,0 +1,172 @@
import random
import torch
from torch import nn
import numpy as np
from PIL import Image
from einops import rearrange
from dataclasses import dataclass
from torchvision.transforms import Normalize
from torchvision.transforms import InterpolationMode
from torchvision.transforms.transforms import _interpolation_modes_from_int
from torchvision import transforms
from transformers import CLIPTokenizer, CLIPImageProcessor
from transformers.utils import ModelOutput
from typing import Iterable, Optional, Union, List
import craftsman
from craftsman.utils.typing import *
from .clip.modeling_clip import CLIPModel
from .clip.modeling_conditional_clip import ConditionalCLIPModel
from .base import BaseEmbedder, ImageType
@dataclass
class CLIPEmbedOutput(ModelOutput):
last_hidden_state: torch.FloatTensor = None
pooler_output: torch.FloatTensor = None
embeds: torch.FloatTensor = None
@craftsman.register("clip-embedder")
class CLIPEmbedder(BaseEmbedder):
@dataclass
class Config(BaseEmbedder.Config):
freeze_modulation: bool = False
config_path: str = ''
cfg: Config
def configure(self) -> None:
super().configure()
# Load the CLIP model and processor
if not self.cfg.encode_camera:
self.model: CLIPModel = CLIPModel.from_pretrained(self.cfg.pretrained_model_name_or_path)
else:
if self.cfg.pretrained_model_name_or_path == '':
assert self.cfg.config_path is not None, "The config path should be provided"
conditional_clip_config = ConditionalCLIPModel.config_class.from_json_file(self.cfg.config_path)
conditional_clip_config.vision_config.modulation_dim = self.cfg.camera_embeds_dim
self.model: CLIPModel = ConditionalCLIPModel(conditional_clip_config)
else:
conditional_clip_config = ConditionalCLIPModel.config_class.from_pretrained(
self.cfg.pretrained_model_name_or_path,
)
conditional_clip_config.vision_config.modulation_dim = self.cfg.camera_embeds_dim
self.model: CLIPModel = ConditionalCLIPModel.from_pretrained(
self.cfg.pretrained_model_name_or_path,
vision_config=conditional_clip_config.vision_config
)
self.tokenizer = None
self.image_preprocess = CLIPImageProcessor()
self.transform = transforms.Compose(
[
transforms.Resize(224, transforms.InterpolationMode.BICUBIC, antialias=True),
transforms.CenterCrop(224), # crop a (224, 224) square
transforms.Normalize(
mean=[0.48145466, 0.4578275, 0.40821073],
std=[0.26862954, 0.26130258, 0.27577711],
),
]
)
self.logit_scale = self.model.logit_scale.exp()
if self.cfg.zero_uncond_embeds:
self.empty_text_embeds = torch.zeros((1, 77, 768)).detach()
self.empty_image_embeds = torch.zeros((self.cfg.n_views, 257, 1024)).detach()
else:
try:
self.empty_text_embeds = self.encode_text([""]).detach() # [1, 77, 768]
except:
self.empty_text_embeds = None
if self.cfg.encode_camera:
self.empty_image_embeds = self.encode_image(torch.zeros(self.cfg.n_views, 224, 224, 3), self.cameras[:self.cfg.n_views]).detach()
else:
self.empty_image_embeds = self.encode_image(torch.zeros(self.cfg.n_views, 224, 224, 3)).detach()
# Freeze the model parameters
self.model.eval()
for k, p in self.model.named_parameters():
ks = k.split('.')
if 'mod_norm1' in ks or 'mod_norm2' in ks and not self.cfg.freeze_modulation:
p.requires_grad_(True)
else:
p.requires_grad_(False)
def encode_image(self, images: Iterable[Optional[ImageType]], cameras: Optional[torch.Tensor] = None, force_none_camera_embeds: bool = False, return_dict: bool = False, **kwargs) -> torch.FloatTensor:
camera_embeds = None
if isinstance(images, (np.ndarray, torch.Tensor)): # for training process
assert images.min() >= 0.0 and images.max() <= 1.0, "The pixel values should be in the range of [0, 1]"
do_rescale = False
if self.cfg.encode_camera:
assert cameras is not None, "The cameras should be provided"
camera_embeds = self.encode_camera(cameras)
pixel_values = self.transform(images.permute(0, 3, 1, 2))
else: # for inference process
do_rescale = True
if self.cfg.encode_camera:
if cameras is None:
bs = len(images) // self.cfg.n_views
cameras = self.cameras[:self.cfg.n_views].repeat(bs, 1, 1).to(self.model.device)
camera_embeds = self.encode_camera(cameras)
pixel_values = self.image_preprocess.preprocess(images, return_tensors='pt', do_rescale=do_rescale).pixel_values
if force_none_camera_embeds:
camera_embeds = None
packed = False
if pixel_values.ndim == 4:
packed = True
pixel_values = pixel_values.unsqueeze(1)
if camera_embeds is not None:
camera_embeds = camera_embeds.unsqueeze(1)
if self.cfg.encode_camera and camera_embeds is not None:
vision_outputs = self.model.vision_model(
pixel_values=rearrange(pixel_values.to(self.model.device), "B N C H W -> (B N) C H W"),
condition=rearrange(camera_embeds, "B N C -> (B N) C")
)
else:
vision_outputs = self.model.vision_model(
pixel_values=rearrange(pixel_values.to(self.model.device), "B N C H W -> (B N) C H W"),
)
if return_dict:
pooler_output = vision_outputs[1] # pooled_output
image_features = self.model.visual_projection(pooler_output)
return CLIPEmbedOutput(
last_hidden_state=vision_outputs.last_hidden_state,
pooler_output=pooler_output,
embeds=image_features
)
else:
return vision_outputs.last_hidden_state
@torch.no_grad()
def encode_text(self, text_inputs: torch.Tensor, return_dict: bool = False) -> torch.FloatTensor:
if self.tokenizer is None:
self.tokenizer = CLIPTokenizer.from_pretrained(self.cfg.pretrained_model_name_or_path)
if isinstance(text_inputs, list):
text_inputs = self.tokenizer(
text_inputs,
max_length=self.tokenizer.model_max_length,
padding="max_length",
return_tensors="pt"
).input_ids
text_outputs = self.model.text_model(input_ids=text_inputs.to(self.model.device))
pooler_output = text_outputs[1] # pooled_output
text_features = self.model.text_projection(pooler_output)
if return_dict:
return CLIPEmbedOutput(
last_hidden_state=text_outputs.last_hidden_state,
pooler_output=pooler_output,
embeds=text_features
)
else:
return text_outputs.last_hidden_state
@@ -0,0 +1,3 @@
from . import (
simple_denoiser,
)
@@ -0,0 +1,191 @@
from dataclasses import dataclass
import torch
import torch.nn as nn
from typing import Optional
from diffusers.models.embeddings import Timesteps
import math
import craftsman
from craftsman.models.transformers.attention import ResidualAttentionBlock
from craftsman.models.transformers.utils import init_linear, MLP
from craftsman.utils.base import BaseModule
class UNetDiffusionTransformer(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
layers: int,
heads: int,
init_scale: float = 0.25,
qkv_bias: bool = False,
skip_ln: bool = False,
use_checkpoint: bool = False
):
super().__init__()
self.n_ctx = n_ctx
self.width = width
self.layers = layers
self.encoder = nn.ModuleList()
for _ in range(layers):
resblock = ResidualAttentionBlock(
n_ctx=n_ctx,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_checkpoint=use_checkpoint
)
self.encoder.append(resblock)
self.middle_block = ResidualAttentionBlock(
n_ctx=n_ctx,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_checkpoint=use_checkpoint
)
self.decoder = nn.ModuleList()
for _ in range(layers):
resblock = ResidualAttentionBlock(
n_ctx=n_ctx,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_checkpoint=use_checkpoint
)
linear = nn.Linear(width * 2, width)
init_linear(linear, init_scale)
layer_norm = nn.LayerNorm(width) if skip_ln else None
self.decoder.append(nn.ModuleList([resblock, linear, layer_norm]))
def forward(self, x: torch.Tensor):
enc_outputs = []
for block in self.encoder:
x = block(x)
enc_outputs.append(x)
x = self.middle_block(x)
for i, (resblock, linear, layer_norm) in enumerate(self.decoder):
x = torch.cat([enc_outputs.pop(), x], dim=-1)
x = linear(x)
if layer_norm is not None:
x = layer_norm(x)
x = resblock(x)
return x
@craftsman.register("simple-denoiser")
class SimpleDenoiser(BaseModule):
@dataclass
class Config(BaseModule.Config):
pretrained_model_name_or_path: Optional[str] = None
input_channels: int = 32
output_channels: int = 32
n_ctx: int = 512
width: int = 768
layers: int = 6
heads: int = 12
context_dim: int = 1024
context_ln: bool = True
skip_ln: bool = False
init_scale: float = 0.25
flip_sin_to_cos: bool = False
use_checkpoint: bool = False
cfg: Config
def configure(self) -> None:
super().configure()
init_scale = self.cfg.init_scale * math.sqrt(1.0 / self.cfg.width)
self.backbone = UNetDiffusionTransformer(
n_ctx=self.cfg.n_ctx,
width=self.cfg.width,
layers=self.cfg.layers,
heads=self.cfg.heads,
skip_ln=self.cfg.skip_ln,
init_scale=init_scale,
use_checkpoint=self.cfg.use_checkpoint
)
self.ln_post = nn.LayerNorm(self.cfg.width)
self.input_proj = nn.Linear(self.cfg.input_channels, self.cfg.width)
self.output_proj = nn.Linear(self.cfg.width, self.cfg.output_channels)
# timestep embedding
self.time_embed = Timesteps(self.cfg.width, flip_sin_to_cos=self.cfg.flip_sin_to_cos, downscale_freq_shift=0)
self.time_proj = MLP(width=self.cfg.width, init_scale=init_scale)
if self.cfg.context_ln:
self.context_embed = nn.Sequential(
nn.LayerNorm(self.cfg.context_dim),
nn.Linear(self.cfg.context_dim, self.cfg.width),
)
else:
self.context_embed = nn.Linear(self.cfg.context_dim, self.cfg.width)
if self.cfg.pretrained_model_name_or_path:
pretrained_ckpt = torch.load(self.cfg.pretrained_model_name_or_path, map_location="cpu")
_pretrained_ckpt = {}
for k, v in pretrained_ckpt.items():
if k.startswith('denoiser_model.'):
_pretrained_ckpt[k.replace('denoiser_model.', '')] = v
pretrained_ckpt = _pretrained_ckpt
if 'state_dict' in pretrained_ckpt:
_pretrained_ckpt = {}
for k, v in pretrained_ckpt['state_dict'].items():
if k.startswith('denoiser_model.'):
_pretrained_ckpt[k.replace('denoiser_model.', '')] = v
pretrained_ckpt = _pretrained_ckpt
self.load_state_dict(pretrained_ckpt, strict=True)
def forward(self,
model_input: torch.FloatTensor,
timestep: torch.LongTensor,
context: torch.FloatTensor):
r"""
Args:
model_input (torch.FloatTensor): [bs, n_data, c]
timestep (torch.LongTensor): [bs,]
context (torch.FloatTensor): [bs, context_tokens, c]
Returns:
sample (torch.FloatTensor): [bs, n_data, c]
"""
_, n_data, _ = model_input.shape
# 1. time
t_emb = self.time_proj(self.time_embed(timestep)).unsqueeze(dim=1)
# 2. conditions projector
context = self.context_embed(context)
# 3. denoiser
x = self.input_proj(model_input)
x = torch.cat([t_emb, context, x], dim=1)
x = self.backbone(x)
x = self.ln_post(x)
x = x[:, -n_data:] # B, n_data, width
sample = self.output_proj(x) # B, n_data, embed_dim
return sample
@@ -0,0 +1,3 @@
from . import (
base
)
@@ -0,0 +1,194 @@
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import craftsman
from .utils import (
Mesh,
IsosurfaceHelper,
MarchingCubeCPUHelper,
MarchingTetrahedraHelper,
)
from craftsman.utils.base import BaseModule
from craftsman.utils.ops import chunk_batch, scale_tensor
from craftsman.utils.typing import *
class BaseGeometry(BaseModule):
@dataclass
class Config(BaseModule.Config):
pass
cfg: Config
@staticmethod
def create_from(
other: "BaseGeometry", cfg: Optional[Union[dict, DictConfig]] = None, **kwargs
) -> "BaseGeometry":
raise TypeError(
f"Cannot create {BaseGeometry.__name__} from {other.__class__.__name__}"
)
def export(self, *args, **kwargs) -> Dict[str, Any]:
return {}
class BaseImplicitGeometry(BaseGeometry):
@dataclass
class Config(BaseGeometry.Config):
radius: float = 1.0
isosurface: bool = True
isosurface_method: str = "mt"
isosurface_resolution: int = 128
isosurface_threshold: Union[float, str] = 0.0
isosurface_chunk: int = 0
isosurface_coarse_to_fine: bool = True
isosurface_deformable_grid: bool = False
isosurface_remove_outliers: bool = True
isosurface_outlier_n_faces_threshold: Union[int, float] = 0.01
cfg: Config
def configure(self) -> None:
self.bbox: Float[Tensor, "2 3"]
self.register_buffer(
"bbox",
torch.as_tensor(
[
[-self.cfg.radius, -self.cfg.radius, -self.cfg.radius],
[self.cfg.radius, self.cfg.radius, self.cfg.radius],
],
dtype=torch.float32,
),
)
self.isosurface_helper: Optional[IsosurfaceHelper] = None
self.unbounded: bool = False
def _initilize_isosurface_helper(self):
if self.cfg.isosurface and self.isosurface_helper is None:
if self.cfg.isosurface_method == "mc-cpu":
self.isosurface_helper = MarchingCubeCPUHelper(
self.cfg.isosurface_resolution
).to(self.device)
elif self.cfg.isosurface_method == "mt":
self.isosurface_helper = MarchingTetrahedraHelper(
self.cfg.isosurface_resolution,
f"load/tets/{self.cfg.isosurface_resolution}_tets.npz",
).to(self.device)
else:
raise AttributeError(
"Unknown isosurface method {self.cfg.isosurface_method}"
)
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
raise NotImplementedError
def forward_field(
self, points: Float[Tensor, "*N Di"]
) -> Tuple[Float[Tensor, "*N 1"], Optional[Float[Tensor, "*N 3"]]]:
# return the value of the implicit field, could be density / signed distance
# also return a deformation field if the grid vertices can be optimized
raise NotImplementedError
def forward_level(
self, field: Float[Tensor, "*N 1"], threshold: float
) -> Float[Tensor, "*N 1"]:
# return the value of the implicit field, where the zero level set represents the surface
raise NotImplementedError
def _isosurface(self, bbox: Float[Tensor, "2 3"], fine_stage: bool = False) -> Mesh:
def batch_func(x):
# scale to bbox as the input vertices are in [0, 1]
field, deformation = self.forward_field(
scale_tensor(
x.to(bbox.device), self.isosurface_helper.points_range, bbox
),
)
field = field.to(
x.device
) # move to the same device as the input (could be CPU)
if deformation is not None:
deformation = deformation.to(x.device)
return field, deformation
assert self.isosurface_helper is not None
field, deformation = chunk_batch(
batch_func,
self.cfg.isosurface_chunk,
self.isosurface_helper.grid_vertices,
)
threshold: float
if isinstance(self.cfg.isosurface_threshold, float):
threshold = self.cfg.isosurface_threshold
elif self.cfg.isosurface_threshold == "auto":
eps = 1.0e-5
threshold = field[field > eps].mean().item()
craftsman.info(
f"Automatically determined isosurface threshold: {threshold}"
)
else:
raise TypeError(
f"Unknown isosurface_threshold {self.cfg.isosurface_threshold}"
)
level = self.forward_level(field, threshold)
mesh: Mesh = self.isosurface_helper(level, deformation=deformation)
mesh.v_pos = scale_tensor(
mesh.v_pos, self.isosurface_helper.points_range, bbox
) # scale to bbox as the grid vertices are in [0, 1]
mesh.add_extra("bbox", bbox)
if self.cfg.isosurface_remove_outliers:
# remove outliers components with small number of faces
# only enabled when the mesh is not differentiable
mesh = mesh.remove_outlier(self.cfg.isosurface_outlier_n_faces_threshold)
return mesh
def isosurface(self) -> Mesh:
if not self.cfg.isosurface:
raise NotImplementedError(
"Isosurface is not enabled in the current configuration"
)
self._initilize_isosurface_helper()
if self.cfg.isosurface_coarse_to_fine:
craftsman.debug("First run isosurface to get a tight bounding box ...")
with torch.no_grad():
mesh_coarse = self._isosurface(self.bbox)
vmin, vmax = mesh_coarse.v_pos.amin(dim=0), mesh_coarse.v_pos.amax(dim=0)
vmin_ = (vmin - (vmax - vmin) * 0.1).max(self.bbox[0])
vmax_ = (vmax + (vmax - vmin) * 0.1).min(self.bbox[1])
craftsman.debug("Run isosurface again with the tight bounding box ...")
mesh = self._isosurface(torch.stack([vmin_, vmax_], dim=0), fine_stage=True)
else:
mesh = self._isosurface(self.bbox)
return mesh
class BaseExplicitGeometry(BaseGeometry):
@dataclass
class Config(BaseGeometry.Config):
radius: float = 1.0
cfg: Config
def configure(self) -> None:
self.bbox: Float[Tensor, "2 3"]
self.register_buffer(
"bbox",
torch.as_tensor(
[
[-self.cfg.radius, -self.cfg.radius, -self.cfg.radius],
[self.cfg.radius, self.cfg.radius, self.cfg.radius],
],
dtype=torch.float32,
),
)
@@ -0,0 +1,559 @@
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import craftsman
from craftsman.utils.typing import *
def dot(x, y):
return torch.sum(x * y, -1, keepdim=True)
class Mesh:
def __init__(
self, v_pos: Float[Tensor, "Nv 3"], t_pos_idx: Integer[Tensor, "Nf 3"], **kwargs
) -> None:
self.v_pos: Float[Tensor, "Nv 3"] = v_pos
self.t_pos_idx: Integer[Tensor, "Nf 3"] = t_pos_idx
self._v_nrm: Optional[Float[Tensor, "Nv 3"]] = None
self._v_tng: Optional[Float[Tensor, "Nv 3"]] = None
self._v_tex: Optional[Float[Tensor, "Nt 3"]] = None
self._t_tex_idx: Optional[Float[Tensor, "Nf 3"]] = None
self._v_rgb: Optional[Float[Tensor, "Nv 3"]] = None
self._edges: Optional[Integer[Tensor, "Ne 2"]] = None
self.extras: Dict[str, Any] = {}
for k, v in kwargs.items():
self.add_extra(k, v)
def add_extra(self, k, v):
self.extras[k] = v
def remove_outlier(self, outlier_n_faces_threshold: Union[int, float]):
if self.requires_grad:
craftsman.debug("Mesh is differentiable, not removing outliers")
return self
# use trimesh to first split the mesh into connected components
# then remove the components with less than n_face_threshold faces
import trimesh
# construct a trimesh object
mesh = trimesh.Trimesh(
vertices=self.v_pos.detach().cpu().numpy(),
faces=self.t_pos_idx.detach().cpu().numpy(),
)
# split the mesh into connected components
components = mesh.split(only_watertight=False)
# log the number of faces in each component
craftsman.debug(
"Mesh has {} components, with faces: {}".format(
len(components), [c.faces.shape[0] for c in components]
)
)
n_faces_threshold: int
if isinstance(outlier_n_faces_threshold, float):
# set the threshold to the number of faces in the largest component multiplied by outlier_n_faces_threshold
n_faces_threshold = int(
max([c.faces.shape[0] for c in components]) * outlier_n_faces_threshold
)
else:
# set the threshold directly to outlier_n_faces_threshold
n_faces_threshold = outlier_n_faces_threshold
# log the threshold
craftsman.debug(
"Removing components with less than {} faces".format(n_faces_threshold)
)
# remove the components with less than n_face_threshold faces
components = [c for c in components if c.faces.shape[0] >= n_faces_threshold]
# log the number of faces in each component after removing outliers
craftsman.debug(
"Mesh has {} components after removing outliers, with faces: {}".format(
len(components), [c.faces.shape[0] for c in components]
)
)
# merge the components
mesh = trimesh.util.concatenate(components)
# convert back to our mesh format
v_pos = torch.from_numpy(mesh.vertices).to(self.v_pos)
t_pos_idx = torch.from_numpy(mesh.faces).to(self.t_pos_idx)
clean_mesh = Mesh(v_pos, t_pos_idx)
# keep the extras unchanged
if len(self.extras) > 0:
clean_mesh.extras = self.extras
craftsman.debug(
f"The following extra attributes are inherited from the original mesh unchanged: {list(self.extras.keys())}"
)
return clean_mesh
@property
def requires_grad(self):
return self.v_pos.requires_grad
@property
def v_nrm(self):
if self._v_nrm is None:
self._v_nrm = self._compute_vertex_normal()
return self._v_nrm
@property
def v_tng(self):
if self._v_tng is None:
self._v_tng = self._compute_vertex_tangent()
return self._v_tng
@property
def v_tex(self):
if self._v_tex is None:
self._v_tex, self._t_tex_idx = self._unwrap_uv()
return self._v_tex
@property
def t_tex_idx(self):
if self._t_tex_idx is None:
self._v_tex, self._t_tex_idx = self._unwrap_uv()
return self._t_tex_idx
@property
def v_rgb(self):
return self._v_rgb
@property
def edges(self):
if self._edges is None:
self._edges = self._compute_edges()
return self._edges
def _compute_vertex_normal(self):
i0 = self.t_pos_idx[:, 0]
i1 = self.t_pos_idx[:, 1]
i2 = self.t_pos_idx[:, 2]
v0 = self.v_pos[i0, :]
v1 = self.v_pos[i1, :]
v2 = self.v_pos[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0, dim=-1)
# Splat face normals to vertices
v_nrm = torch.zeros_like(self.v_pos)
v_nrm.scatter_add_(0, i0[:, None].repeat(1, 3), face_normals)
v_nrm.scatter_add_(0, i1[:, None].repeat(1, 3), face_normals)
v_nrm.scatter_add_(0, i2[:, None].repeat(1, 3), face_normals)
# Normalize, replace zero (degenerated) normals with some default value
v_nrm = torch.where(
dot(v_nrm, v_nrm) > 1e-20, v_nrm, torch.as_tensor([0.0, 0.0, 1.0]).to(v_nrm)
)
v_nrm = F.normalize(v_nrm, dim=1)
if torch.is_anomaly_enabled():
assert torch.all(torch.isfinite(v_nrm))
return v_nrm
def _compute_vertex_tangent(self):
vn_idx = [None] * 3
pos = [None] * 3
tex = [None] * 3
for i in range(0, 3):
pos[i] = self.v_pos[self.t_pos_idx[:, i]]
tex[i] = self.v_tex[self.t_tex_idx[:, i]]
# t_nrm_idx is always the same as t_pos_idx
vn_idx[i] = self.t_pos_idx[:, i]
tangents = torch.zeros_like(self.v_nrm)
tansum = torch.zeros_like(self.v_nrm)
# Compute tangent space for each triangle
uve1 = tex[1] - tex[0]
uve2 = tex[2] - tex[0]
pe1 = pos[1] - pos[0]
pe2 = pos[2] - pos[0]
nom = pe1 * uve2[..., 1:2] - pe2 * uve1[..., 1:2]
denom = uve1[..., 0:1] * uve2[..., 1:2] - uve1[..., 1:2] * uve2[..., 0:1]
# Avoid division by zero for degenerated texture coordinates
tang = nom / torch.where(
denom > 0.0, torch.clamp(denom, min=1e-6), torch.clamp(denom, max=-1e-6)
)
# Update all 3 vertices
for i in range(0, 3):
idx = vn_idx[i][:, None].repeat(1, 3)
tangents.scatter_add_(0, idx, tang) # tangents[n_i] = tangents[n_i] + tang
tansum.scatter_add_(
0, idx, torch.ones_like(tang)
) # tansum[n_i] = tansum[n_i] + 1
tangents = tangents / tansum
# Normalize and make sure tangent is perpendicular to normal
tangents = F.normalize(tangents, dim=1)
tangents = F.normalize(tangents - dot(tangents, self.v_nrm) * self.v_nrm)
if torch.is_anomaly_enabled():
assert torch.all(torch.isfinite(tangents))
return tangents
def _unwrap_uv(
self, xatlas_chart_options: dict = {}, xatlas_pack_options: dict = {}
):
craftsman.info("Using xatlas to perform UV unwrapping, may take a while ...")
import xatlas
atlas = xatlas.Atlas()
atlas.add_mesh(
self.v_pos.detach().cpu().numpy(),
self.t_pos_idx.cpu().numpy(),
)
co = xatlas.ChartOptions()
po = xatlas.PackOptions()
for k, v in xatlas_chart_options.items():
setattr(co, k, v)
for k, v in xatlas_pack_options.items():
setattr(po, k, v)
setattr(co, 'max_cost', 2.0)
setattr(po, 'resolution', 4096)
atlas.generate(co, po, verbose=True)
vmapping, indices, uvs = atlas.get_mesh(0)
vmapping = (
torch.from_numpy(
vmapping.astype(np.uint64, casting="same_kind").view(np.int64)
)
.to(self.v_pos.device)
.long()
)
uvs = torch.from_numpy(uvs).to(self.v_pos.device).float()
indices = (
torch.from_numpy(
indices.astype(np.uint64, casting="same_kind").view(np.int64)
)
.to(self.v_pos.device)
.long()
)
return uvs, indices
def unwrap_uv(
self, xatlas_chart_options: dict = {}, xatlas_pack_options: dict = {}
):
self._v_tex, self._t_tex_idx = self._unwrap_uv(
xatlas_chart_options, xatlas_pack_options
)
def set_vertex_color(self, v_rgb):
assert v_rgb.shape[0] == self.v_pos.shape[0]
self._v_rgb = v_rgb
def _compute_edges(self):
# Compute edges
edges = torch.cat(
[
self.t_pos_idx[:, [0, 1]],
self.t_pos_idx[:, [1, 2]],
self.t_pos_idx[:, [2, 0]],
],
dim=0,
)
edges = edges.sort()[0]
edges = torch.unique(edges, dim=0)
return edges
def normal_consistency(self) -> Float[Tensor, ""]:
edge_nrm: Float[Tensor, "Ne 2 3"] = self.v_nrm[self.edges]
nc = (
1.0 - torch.cosine_similarity(edge_nrm[:, 0], edge_nrm[:, 1], dim=-1)
).mean()
return nc
def _laplacian_uniform(self):
# from stable-dreamfusion
# https://github.com/ashawkey/stable-dreamfusion/blob/8fb3613e9e4cd1ded1066b46e80ca801dfb9fd06/nerf/renderer.py#L224
verts, faces = self.v_pos, self.t_pos_idx
V = verts.shape[0]
F = faces.shape[0]
# Neighbor indices
ii = faces[:, [1, 2, 0]].flatten()
jj = faces[:, [2, 0, 1]].flatten()
adj = torch.stack([torch.cat([ii, jj]), torch.cat([jj, ii])], dim=0).unique(
dim=1
)
adj_values = torch.ones(adj.shape[1]).to(verts)
# Diagonal indices
diag_idx = adj[0]
# Build the sparse matrix
idx = torch.cat((adj, torch.stack((diag_idx, diag_idx), dim=0)), dim=1)
values = torch.cat((-adj_values, adj_values))
# The coalesce operation sums the duplicate indices, resulting in the
# correct diagonal
return torch.sparse_coo_tensor(idx, values, (V, V)).coalesce()
def laplacian(self) -> Float[Tensor, ""]:
with torch.no_grad():
L = self._laplacian_uniform()
loss = L.mm(self.v_pos)
loss = loss.norm(dim=1)
loss = loss.mean()
return loss
class IsosurfaceHelper(nn.Module):
points_range: Tuple[float, float] = (0, 1)
@property
def grid_vertices(self) -> Float[Tensor, "N 3"]:
raise NotImplementedError
class MarchingCubeCPUHelper(IsosurfaceHelper):
def __init__(self, resolution: int) -> None:
super().__init__()
self.resolution = resolution
import mcubes
self.mc_func: Callable = mcubes.marching_cubes
self._grid_vertices: Optional[Float[Tensor, "N3 3"]] = None
self._dummy: Float[Tensor, "..."]
self.register_buffer(
"_dummy", torch.zeros(0, dtype=torch.float32), persistent=False
)
@property
def grid_vertices(self) -> Float[Tensor, "N3 3"]:
if self._grid_vertices is None:
# keep the vertices on CPU so that we can support very large resolution
x, y, z = (
torch.linspace(*self.points_range, self.resolution),
torch.linspace(*self.points_range, self.resolution),
torch.linspace(*self.points_range, self.resolution),
)
x, y, z = torch.meshgrid(x, y, z, indexing="ij")
verts = torch.cat(
[x.reshape(-1, 1), y.reshape(-1, 1), z.reshape(-1, 1)], dim=-1
).reshape(-1, 3)
self._grid_vertices = verts
return self._grid_vertices
def forward(
self,
level: Float[Tensor, "N3 1"],
deformation: Optional[Float[Tensor, "N3 3"]] = None,
) -> Mesh:
if deformation is not None:
craftsman.warn(
f"{self.__class__.__name__} does not support deformation. Ignoring."
)
level = -level.view(self.resolution, self.resolution, self.resolution)
v_pos, t_pos_idx = self.mc_func(
level.detach().cpu().numpy(), 0.0
) # transform to numpy
v_pos, t_pos_idx = (
torch.from_numpy(v_pos).float().to(self._dummy.device),
torch.from_numpy(t_pos_idx.astype(np.int64)).long().to(self._dummy.device),
) # transform back to torch tensor on CUDA
v_pos = v_pos / (self.resolution - 1.0)
return Mesh(v_pos=v_pos, t_pos_idx=t_pos_idx)
class MarchingTetrahedraHelper(IsosurfaceHelper):
def __init__(self, resolution: int, tets_path: str):
super().__init__()
self.resolution = resolution
self.tets_path = tets_path
self.triangle_table: Float[Tensor, "..."]
self.register_buffer(
"triangle_table",
torch.as_tensor(
[
[-1, -1, -1, -1, -1, -1],
[1, 0, 2, -1, -1, -1],
[4, 0, 3, -1, -1, -1],
[1, 4, 2, 1, 3, 4],
[3, 1, 5, -1, -1, -1],
[2, 3, 0, 2, 5, 3],
[1, 4, 0, 1, 5, 4],
[4, 2, 5, -1, -1, -1],
[4, 5, 2, -1, -1, -1],
[4, 1, 0, 4, 5, 1],
[3, 2, 0, 3, 5, 2],
[1, 3, 5, -1, -1, -1],
[4, 1, 2, 4, 3, 1],
[3, 0, 4, -1, -1, -1],
[2, 0, 1, -1, -1, -1],
[-1, -1, -1, -1, -1, -1],
],
dtype=torch.long,
),
persistent=False,
)
self.num_triangles_table: Integer[Tensor, "..."]
self.register_buffer(
"num_triangles_table",
torch.as_tensor(
[0, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 0], dtype=torch.long
),
persistent=False,
)
self.base_tet_edges: Integer[Tensor, "..."]
self.register_buffer(
"base_tet_edges",
torch.as_tensor([0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3], dtype=torch.long),
persistent=False,
)
tets = np.load(self.tets_path)
self._grid_vertices: Float[Tensor, "..."]
self.register_buffer(
"_grid_vertices",
torch.from_numpy(tets["vertices"]).float(),
persistent=False,
)
self.indices: Integer[Tensor, "..."]
self.register_buffer(
"indices", torch.from_numpy(tets["indices"]).long(), persistent=False
)
self._all_edges: Optional[Integer[Tensor, "Ne 2"]] = None
def normalize_grid_deformation(
self, grid_vertex_offsets: Float[Tensor, "Nv 3"]
) -> Float[Tensor, "Nv 3"]:
return (
(self.points_range[1] - self.points_range[0])
/ (self.resolution) # half tet size is approximately 1 / self.resolution
* torch.tanh(grid_vertex_offsets)
) # FIXME: hard-coded activation
@property
def grid_vertices(self) -> Float[Tensor, "Nv 3"]:
return self._grid_vertices
@property
def all_edges(self) -> Integer[Tensor, "Ne 2"]:
if self._all_edges is None:
# compute edges on GPU, or it would be VERY SLOW (basically due to the unique operation)
edges = torch.tensor(
[0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3],
dtype=torch.long,
device=self.indices.device,
)
_all_edges = self.indices[:, edges].reshape(-1, 2)
_all_edges_sorted = torch.sort(_all_edges, dim=1)[0]
_all_edges = torch.unique(_all_edges_sorted, dim=0)
self._all_edges = _all_edges
return self._all_edges
def sort_edges(self, edges_ex2):
with torch.no_grad():
order = (edges_ex2[:, 0] > edges_ex2[:, 1]).long()
order = order.unsqueeze(dim=1)
a = torch.gather(input=edges_ex2, index=order, dim=1)
b = torch.gather(input=edges_ex2, index=1 - order, dim=1)
return torch.stack([a, b], -1)
def _forward(self, pos_nx3, sdf_n, tet_fx4):
with torch.no_grad():
occ_n = sdf_n > 0
occ_fx4 = occ_n[tet_fx4.reshape(-1)].reshape(-1, 4)
occ_sum = torch.sum(occ_fx4, -1)
valid_tets = (occ_sum > 0) & (occ_sum < 4)
occ_sum = occ_sum[valid_tets]
# find all vertices
all_edges = tet_fx4[valid_tets][:, self.base_tet_edges].reshape(-1, 2)
all_edges = self.sort_edges(all_edges)
unique_edges, idx_map = torch.unique(all_edges, dim=0, return_inverse=True)
unique_edges = unique_edges.long()
mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
mapping = (
torch.ones(
(unique_edges.shape[0]), dtype=torch.long, device=pos_nx3.device
)
* -1
)
mapping[mask_edges] = torch.arange(
mask_edges.sum(), dtype=torch.long, device=pos_nx3.device
)
idx_map = mapping[idx_map] # map edges to verts
interp_v = unique_edges[mask_edges]
edges_to_interp = pos_nx3[interp_v.reshape(-1)].reshape(-1, 2, 3)
edges_to_interp_sdf = sdf_n[interp_v.reshape(-1)].reshape(-1, 2, 1)
edges_to_interp_sdf[:, -1] *= -1
denominator = edges_to_interp_sdf.sum(1, keepdim=True)
edges_to_interp_sdf = torch.flip(edges_to_interp_sdf, [1]) / denominator
verts = (edges_to_interp * edges_to_interp_sdf).sum(1)
idx_map = idx_map.reshape(-1, 6)
v_id = torch.pow(2, torch.arange(4, dtype=torch.long, device=pos_nx3.device))
tetindex = (occ_fx4[valid_tets] * v_id.unsqueeze(0)).sum(-1)
num_triangles = self.num_triangles_table[tetindex]
# Generate triangle indices
faces = torch.cat(
(
torch.gather(
input=idx_map[num_triangles == 1],
dim=1,
index=self.triangle_table[tetindex[num_triangles == 1]][:, :3],
).reshape(-1, 3),
torch.gather(
input=idx_map[num_triangles == 2],
dim=1,
index=self.triangle_table[tetindex[num_triangles == 2]][:, :6],
).reshape(-1, 3),
),
dim=0,
)
return verts, faces
def forward(
self,
level: Float[Tensor, "N3 1"],
deformation: Optional[Float[Tensor, "N3 3"]] = None,
) -> Mesh:
if deformation is not None:
grid_vertices = self.grid_vertices + self.normalize_grid_deformation(
deformation
)
else:
grid_vertices = self.grid_vertices
v_pos, t_pos_idx = self._forward(grid_vertices, level, self.indices)
mesh = Mesh(
v_pos=v_pos,
t_pos_idx=t_pos_idx,
# extras
grid_vertices=grid_vertices,
tet_edges=self.all_edges,
grid_level=level,
grid_deformation=deformation,
)
return mesh
@@ -0,0 +1,207 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from craftsman.utils.typing import *
from craftsman.utils.checkpoint import checkpoint
from .utils import init_linear, MLP
class MultiheadAttention(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
heads: int,
init_scale: float,
qkv_bias: bool,
use_flash: bool = False
):
super().__init__()
self.n_ctx = n_ctx
self.width = width
self.heads = heads
self.c_qkv = nn.Linear(width, width * 3, bias=qkv_bias)
self.c_proj = nn.Linear(width, width)
self.attention = QKVMultiheadAttention(heads=heads, n_ctx=n_ctx, use_flash=use_flash)
init_linear(self.c_qkv, init_scale)
init_linear(self.c_proj, init_scale)
def forward(self, x):
x = self.c_qkv(x)
x = checkpoint(self.attention, (x,), (), True)
x = self.c_proj(x)
return x
class QKVMultiheadAttention(nn.Module):
def __init__(self, *, heads: int, n_ctx: int, use_flash: bool = False):
super().__init__()
self.heads = heads
self.n_ctx = n_ctx
self.use_flash = use_flash
def forward(self, qkv):
bs, n_ctx, width = qkv.shape
attn_ch = width // self.heads // 3
scale = 1 / math.sqrt(math.sqrt(attn_ch))
qkv = qkv.view(bs, n_ctx, self.heads, -1)
q, k, v = torch.split(qkv, attn_ch, dim=-1)
if self.use_flash:
q = q.permute(0, 2, 1, 3)
k = k.permute(0, 2, 1, 3)
v = v.permute(0, 2, 1, 3)
out = F.scaled_dot_product_attention(q, k, v).permute(0, 2, 1, 3).reshape(bs, n_ctx, -1)
else:
weight = torch.einsum(
"bthc,bshc->bhts", q * scale, k * scale
) # More stable with f16 than dividing afterwards
wdtype = weight.dtype
weight = torch.softmax(weight.float(), dim=-1).type(wdtype)
out = torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1)
return out
class ResidualAttentionBlock(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
heads: int,
init_scale: float = 1.0,
qkv_bias: bool = True,
use_flash: bool = False,
use_checkpoint: bool = False
):
super().__init__()
self.use_checkpoint = use_checkpoint
self.attn = MultiheadAttention(
n_ctx=n_ctx,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash
)
self.ln_1 = nn.LayerNorm(width)
self.mlp = MLP(width=width, init_scale=init_scale)
self.ln_2 = nn.LayerNorm(width)
def _forward(self, x: torch.Tensor):
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x
def forward(self, x: torch.Tensor):
return checkpoint(self._forward, (x,), self.parameters(), self.use_checkpoint)
class MultiheadCrossAttention(nn.Module):
def __init__(
self,
*,
width: int,
heads: int,
init_scale: float,
qkv_bias: bool = True,
use_flash: bool = False,
n_data: Optional[int] = None,
data_width: Optional[int] = None,
):
super().__init__()
self.n_data = n_data
self.width = width
self.heads = heads
self.data_width = width if data_width is None else data_width
self.c_q = nn.Linear(width, width, bias=qkv_bias)
self.c_kv = nn.Linear(self.data_width, width * 2, bias=qkv_bias)
self.c_proj = nn.Linear(width, width)
self.attention = QKVMultiheadCrossAttention(
heads=heads, n_data=n_data, use_flash=use_flash
)
init_linear(self.c_q, init_scale)
init_linear(self.c_kv, init_scale)
init_linear(self.c_proj, init_scale)
def forward(self, x, data):
x = self.c_q(x)
data = self.c_kv(data)
x = checkpoint(self.attention, (x, data), (), True)
x = self.c_proj(x)
return x
class QKVMultiheadCrossAttention(nn.Module):
def __init__(self, *, heads: int, use_flash: bool = False, n_data: Optional[int] = None):
super().__init__()
self.heads = heads
self.n_data = n_data
self.use_flash = use_flash
def forward(self, q, kv):
_, n_ctx, _ = q.shape
bs, n_data, width = kv.shape
attn_ch = width // self.heads // 2
scale = 1 / math.sqrt(math.sqrt(attn_ch))
q = q.view(bs, n_ctx, self.heads, -1)
kv = kv.view(bs, n_data, self.heads, -1)
k, v = torch.split(kv, attn_ch, dim=-1)
if self.use_flash:
q = q.permute(0, 2, 1, 3)
k = k.permute(0, 2, 1, 3)
v = v.permute(0, 2, 1, 3)
out = F.scaled_dot_product_attention(q, k, v).permute(0, 2, 1, 3).reshape(bs, n_ctx, -1)
else:
weight = torch.einsum(
"bthc,bshc->bhts", q * scale, k * scale
) # More stable with f16 than dividing afterwards
wdtype = weight.dtype
weight = torch.softmax(weight.float(), dim=-1).type(wdtype)
out = torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1)
return out
class ResidualCrossAttentionBlock(nn.Module):
def __init__(
self,
*,
n_data: Optional[int] = None,
width: int,
heads: int,
data_width: Optional[int] = None,
init_scale: float = 0.25,
qkv_bias: bool = True,
use_flash: bool = False
):
super().__init__()
if data_width is None:
data_width = width
self.attn = MultiheadCrossAttention(
n_data=n_data,
width=width,
heads=heads,
data_width=data_width,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash,
)
self.ln_1 = nn.LayerNorm(width)
self.ln_2 = nn.LayerNorm(data_width)
self.mlp = MLP(width=width, init_scale=init_scale)
self.ln_3 = nn.LayerNorm(width)
def forward(self, x: torch.Tensor, data: torch.Tensor):
x = x + self.attn(self.ln_1(x), self.ln_2(data))
x = x + self.mlp(self.ln_3(x))
return x
@@ -0,0 +1,48 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from craftsman.utils.typing import *
from craftsman.utils.checkpoint import checkpoint
from .utils import init_linear
from .attention import ResidualAttentionBlock
class Perceiver(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
layers: int,
heads: int,
init_scale: float = 0.25,
qkv_bias: bool = True,
use_flash: bool = False,
use_checkpoint: bool = False
):
super().__init__()
self.n_ctx = n_ctx
self.width = width
self.layers = layers
self.resblocks = nn.ModuleList(
[
ResidualAttentionBlock(
n_ctx=n_ctx,
width=width,
heads=heads,
init_scale=init_scale,
qkv_bias=qkv_bias,
use_flash=use_flash,
use_checkpoint=use_checkpoint
)
for _ in range(layers)
]
)
def forward(self, x: torch.Tensor):
for block in self.resblocks:
x = block(x)
return x
@@ -0,0 +1,21 @@
import torch.nn as nn
def init_linear(l, stddev):
nn.init.normal_(l.weight, std=stddev)
if l.bias is not None:
nn.init.constant_(l.bias, 0.0)
class MLP(nn.Module):
def __init__(self, *,
width: int,
init_scale: float):
super().__init__()
self.width = width
self.c_fc = nn.Linear(width, width * 4)
self.c_proj = nn.Linear(width * 4, width)
self.gelu = nn.GELU()
init_linear(self.c_fc, init_scale)
init_linear(self.c_proj, init_scale)
def forward(self, x):
return self.c_proj(self.gelu(self.c_fc(x)))
@@ -0,0 +1,4 @@
from . import (
shape_autoencoder,
shape_diffusion,
)
+209
View File
@@ -0,0 +1,209 @@
import os
from dataclasses import dataclass, field
import pytorch_lightning as pl
import torch.nn.functional as F
from craftsman.utils.base import (
Updateable,
update_end_if_possible,
update_if_possible,
)
from craftsman.utils.scheduler import parse_optimizer, parse_scheduler
from craftsman.utils.config import parse_structured
from craftsman.utils.misc import C, cleanup, get_device, load_module_weights
#from craftsman.utils.saving import SaverMixin
from craftsman.utils.typing import *
#class BaseSystem(pl.LightningModule, Updateable, SaverMixin):
class BaseSystem(pl.LightningModule, Updateable):
@dataclass
class Config:
loggers: dict = field(default_factory=dict)
loss: dict = field(default_factory=dict)
optimizer: dict = field(default_factory=dict)
scheduler: Optional[dict] = None
weights: Optional[str] = None
weights_ignore_modules: Optional[List[str]] = None
cleanup_after_validation_step: bool = False
cleanup_after_test_step: bool = False
pretrained_model_path: Optional[str] = None
strict_load: bool = True
cfg: Config
def __init__(self, cfg, resumed=False) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
self._save_dir: Optional[str] = None
self._resumed: bool = resumed
self._resumed_eval: bool = False
self._resumed_eval_status: dict = {"global_step": 0, "current_epoch": 0}
#if "loggers" in cfg:
# self.create_loggers(cfg.loggers)
self.configure()
if self.cfg.weights is not None:
self.load_weights(self.cfg.weights, self.cfg.weights_ignore_modules)
self.post_configure()
def load_weights(self, weights: str, ignore_modules: Optional[List[str]] = None):
state_dict, epoch, global_step = load_module_weights(
weights, ignore_modules=ignore_modules, map_location="cpu"
)
self.load_state_dict(state_dict, strict=False)
# restore step-dependent states
self.do_update_step(epoch, global_step, on_load_weights=True)
def set_resume_status(self, current_epoch: int, global_step: int):
# restore correct epoch and global step in eval
self._resumed_eval = True
self._resumed_eval_status["current_epoch"] = current_epoch
self._resumed_eval_status["global_step"] = global_step
@property
def resumed(self):
# whether from resumed checkpoint
return self._resumed
@property
def true_global_step(self):
if self._resumed_eval:
return self._resumed_eval_status["global_step"]
else:
return self.global_step
@property
def true_current_epoch(self):
if self._resumed_eval:
return self._resumed_eval_status["current_epoch"]
else:
return self.current_epoch
def configure(self) -> None:
pass
def post_configure(self) -> None:
"""
executed after weights are loaded
"""
pass
def C(self, value: Any) -> float:
return C(value, self.true_current_epoch, self.true_global_step)
def configure_optimizers(self):
optim = parse_optimizer(self.cfg.optimizer, self)
ret = {
"optimizer": optim,
}
if self.cfg.scheduler is not None:
ret.update(
{
"lr_scheduler": parse_scheduler(self.cfg.scheduler, optim),
}
)
return ret
def training_step(self, batch, batch_idx):
raise NotImplementedError
def validation_step(self, batch, batch_idx):
raise NotImplementedError
def on_train_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.train_dataloader.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
def on_validation_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.val_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_validation_step:
# cleanup to save vram
cleanup()
def on_validation_epoch_end(self):
raise NotImplementedError
def test_step(self, batch, batch_idx):
raise NotImplementedError
def on_test_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.test_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_test_step:
# cleanup to save vram
cleanup()
def on_test_epoch_end(self):
pass
def predict_step(self, batch, batch_idx):
raise NotImplementedError
def on_predict_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.predict_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_test_step:
# cleanup to save vram
cleanup()
def on_predict_epoch_end(self):
pass
def preprocess_data(self, batch, stage):
pass
"""
Implementing on_after_batch_transfer of DataModule does the same.
But on_after_batch_transfer does not support DP.
"""
def on_train_batch_start(self, batch, batch_idx, unused=0):
self.preprocess_data(batch, "train")
self.dataset = self.trainer.train_dataloader.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_validation_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "validation")
self.dataset = self.trainer.val_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_test_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "test")
self.dataset = self.trainer.test_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_predict_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "predict")
self.dataset = self.trainer.predict_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
pass
def on_before_optimizer_step(self, optimizer):
"""
# some gradient-related debugging goes here, example:
from lightning.pytorch.utilities import grad_norm
norms = grad_norm(self.geometry, norm_type=2)
print(norms)
"""
pass
@@ -0,0 +1,137 @@
from dataclasses import dataclass, field
import numpy as np
import torch
from skimage import measure
from einops import repeat, rearrange
import craftsman
from craftsman.systems.base import BaseSystem
from craftsman.utils.ops import generate_dense_grid_points
from craftsman.utils.typing import *
from craftsman.utils.misc import get_rank
@craftsman.register("shape-autoencoder-system")
class ShapeAutoEncoderSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
shape_model_type: str = None
shape_model: dict = field(default_factory=dict)
sample_posterior: bool = True
cfg: Config
def configure(self):
super().configure()
self.shape_model = craftsman.find(self.cfg.shape_model_type)(self.cfg.shape_model)
def forward(self, batch: Dict[str, Any]) -> Dict[str, Any]:
if "xyz" in batch:
if "sdf" in batch:
bs = batch["sdf"].shape[0]
rand_points = torch.cat([batch["xyz"].view(bs, -1, 3), batch["patch_xyz"].view(bs, -1, 3)], dim=1)
target = torch.cat([batch["sdf"].view(bs, -1, 1), batch["patch_sdf"].view(bs, -1, 1)], dim=1).squeeze(-1)
criteria = torch.nn.MSELoss()
elif "occupancy" in batch:
bs = batch["occupancy"].shape[0]
rand_points = torch.cat([batch["xyz"].view(bs, -1, 3), batch["patch_xyz"].view(bs, -1, 3)], dim=1)
target = torch.cat([batch["occupancy"].view(bs, -1, 1), batch["patch_occupancy"].view(bs, -1, 1)], dim=1).squeeze(-1)
criteria = torch.nn.BCEWithLogitsLoss()
else:
raise NotImplementedError
else:
rand_points = batch["rand_points"]
if "sdf" in batch:
target = batch["sdf"]
criteria = torch.nn.MSELoss()
elif "occupancies" in batch:
target = batch["occupancies"]
criteria = torch.nn.BCEWithLogitsLoss()
else:
raise NotImplementedError
# forward pass
_, latents, posterior, logits = self.shape_model(
batch["surface"][..., :3 + self.cfg.shape_model.point_feats],
rand_points,
sample_posterior=self.cfg.sample_posterior
)
if self.cfg.sample_posterior:
loss_kl = posterior.kl()
loss_kl = torch.sum(loss_kl) / loss_kl.shape[0]
return {
"loss_logits": criteria(logits, target).mean(),
"loss_kl": loss_kl,
"logits": logits,
"target": target,
"latents": latents,
}
else:
return {
"loss_logits": criteria(logits, target).mean(),
"latents": latents,
"logits": logits,
}
def training_step(self, batch, batch_idx):
"""
Description:
Args:
batch:
batch_idx:
Returns:
loss:
"""
out = self(batch)
loss = 0.
for name, value in out.items():
if name.startswith("loss_"):
self.log(f"train/{name}", value)
loss += value * self.C(self.cfg.loss[name.replace("loss_", "lambda_")])
for name, value in self.cfg.loss.items():
self.log(f"train_params/{name}", self.C(value))
return {"loss": loss}
@torch.no_grad()
def validation_step(self, batch, batch_idx):
self.eval()
out = self(batch)
# self.save_state_dict("latest-weights", self.state_dict())
mesh_v_f, has_surface = self.shape_model.extract_geometry(out["latents"])
self.save_mesh(
f"it{self.true_global_step}/{batch['uid'][0]}.obj",
mesh_v_f[0][0], mesh_v_f[0][1]
)
threshold = 0
outputs = out["logits"]
labels = out["target"]
pred = torch.zeros_like(outputs)
pred[outputs>=threshold] = 1
accuracy = (pred==labels).float().sum(dim=1) / labels.shape[1]
accuracy = accuracy.mean()
intersection = (pred * labels).sum(dim=1)
union = (pred + labels).gt(0).sum(dim=1)
iou = intersection * 1.0 / union + 1e-5
iou = iou.mean()
self.log("val/accuracy", accuracy)
self.log("val/iou", iou)
torch.cuda.empty_cache()
return {"val/loss": out["loss_logits"], "val/accuracy": accuracy, "val/iou": iou}
def on_validation_epoch_end(self):
pass
@@ -0,0 +1,396 @@
from dataclasses import dataclass, field
import numpy as np
import json
import copy
import torch
import torch.nn.functional as F
from skimage import measure
from einops import repeat
from tqdm import tqdm
from PIL import Image
from diffusers import (
DDPMScheduler,
DDIMScheduler,
UniPCMultistepScheduler,
KarrasVeScheduler,
DPMSolverMultistepScheduler
)
import craftsman
from craftsman.systems.base import BaseSystem
from craftsman.utils.ops import generate_dense_grid_points
from craftsman.utils.misc import get_rank
from craftsman.utils.typing import *
import comfy.utils
def compute_snr(noise_scheduler, timesteps):
"""
Computes SNR as per
https://github.com/TiankaiHang/Min-SNR-Diffusion-Training/blob/521b624bd70c67cee4bdf49225915f5945a872e3/guided_diffusion/gaussian_diffusion.py#L847-L849
"""
alphas_cumprod = noise_scheduler.alphas_cumprod
sqrt_alphas_cumprod = alphas_cumprod**0.5
sqrt_one_minus_alphas_cumprod = (1.0 - alphas_cumprod) ** 0.5
# Expand the tensors.
# Adapted from https://github.com/TiankaiHang/Min-SNR-Diffusion-Training/blob/521b624bd70c67cee4bdf49225915f5945a872e3/guided_diffusion/gaussian_diffusion.py#L1026
sqrt_alphas_cumprod = sqrt_alphas_cumprod.to(device=timesteps.device)[timesteps].float()
while len(sqrt_alphas_cumprod.shape) < len(timesteps.shape):
sqrt_alphas_cumprod = sqrt_alphas_cumprod[..., None]
alpha = sqrt_alphas_cumprod.expand(timesteps.shape)
sqrt_one_minus_alphas_cumprod = sqrt_one_minus_alphas_cumprod.to(device=timesteps.device)[timesteps].float()
while len(sqrt_one_minus_alphas_cumprod.shape) < len(timesteps.shape):
sqrt_one_minus_alphas_cumprod = sqrt_one_minus_alphas_cumprod[..., None]
sigma = sqrt_one_minus_alphas_cumprod.expand(timesteps.shape)
# Compute SNR.
snr = (alpha / sigma) ** 2
return snr
def ddim_sample(ddim_scheduler: DDIMScheduler,
diffusion_model: torch.nn.Module,
shape: Union[List[int], Tuple[int]],
cond: torch.FloatTensor,
steps: int,
eta: float = 0.0,
guidance_scale: float = 3.0,
do_classifier_free_guidance: bool = True,
generator: Optional[torch.Generator] = None,
device: torch.device = "cuda:0",
disable_prog: bool = True):
assert steps > 0, f"{steps} must > 0."
# init latents
bsz = cond.shape[0]
if do_classifier_free_guidance:
bsz = bsz // 2
latents = torch.randn(
(bsz, *shape),
generator=generator,
device=cond.device,
dtype=cond.dtype,
)
# scale the initial noise by the standard deviation required by the scheduler
latents = latents * ddim_scheduler.init_noise_sigma
# set timesteps
ddim_scheduler.set_timesteps(steps)
timesteps = ddim_scheduler.timesteps.to(device)
# prepare extra kwargs for the scheduler step, since not all schedulers have the same signature
# eta (η) is only used with the DDIMScheduler, and between [0, 1]
extra_step_kwargs = {
# "eta": eta,
"generator": generator
}
# reverse
comfy_pbar = comfy.utils.ProgressBar(steps)
for i, t in enumerate(tqdm(timesteps, disable=disable_prog, desc="DDIM Sampling:", leave=False)):
# expand the latents if we are doing classifier free guidance
latent_model_input = (
torch.cat([latents] * 2)
if do_classifier_free_guidance
else latents
)
# predict the noise residual
timestep_tensor = torch.tensor([t], dtype=torch.long, device=device)
timestep_tensor = timestep_tensor.expand(latent_model_input.shape[0])
noise_pred = diffusion_model.forward(latent_model_input, timestep_tensor, cond)
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (
noise_pred_text - noise_pred_uncond
)
# compute the previous noisy sample x_t -> x_t-1
latents = ddim_scheduler.step(
noise_pred, t, latents, **extra_step_kwargs
).prev_sample
comfy_pbar.update_absolute(i + 1)
yield latents, t
@craftsman.register("shape-diffusion-system")
class ShapeDiffusionSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
val_samples_json: str = None
z_scale_factor: float = 1.0
guidance_scale: float = 7.5
num_inference_steps: int = 50
eta: float = 0.0
snr_gamma: float = 5.0
# shape vae model
shape_model_type: str = None
shape_model: dict = field(default_factory=dict)
# condition model
condition_model_type: str = None
condition_model: dict = field(default_factory=dict)
# diffusion model
denoiser_model_type: str = None
denoiser_model: dict = field(default_factory=dict)
# noise scheduler
noise_scheduler_type: str = None
noise_scheduler: dict = field(default_factory=dict)
# denoise scheduler
denoise_scheduler_type: str = None
denoise_scheduler: dict = field(default_factory=dict)
cfg: Config
def configure(self):
super().configure()
self.shape_model = craftsman.find(self.cfg.shape_model_type)(self.cfg.shape_model)
self.shape_model.eval()
self.shape_model.requires_grad_(False)
self.condition = craftsman.find(self.cfg.condition_model_type)(self.cfg.condition_model)
self.denoiser_model = craftsman.find(self.cfg.denoiser_model_type)(self.cfg.denoiser_model)
self.noise_scheduler = craftsman.find(self.cfg.noise_scheduler_type)(**self.cfg.noise_scheduler)
self.denoise_scheduler = craftsman.find(self.cfg.denoise_scheduler_type)(**self.cfg.denoise_scheduler)
self.z_scale_factor = self.cfg.z_scale_factor
def forward(self, batch: Dict[str, Any]) -> Dict[str, Any]:
# encode shape latents
shape_embeds, kl_embed, posterior = self.shape_model.encode(
batch["surface"][..., :3 + self.cfg.shape_model.point_feats],
sample_posterior=True
)
latents = kl_embed * self.z_scale_factor
cond_latents = self.condition(batch)
cond_latents = cond_latents.to(latents).view(latents.shape[0], -1, cond_latents.shape[-1])
# Sample noise that we"ll add to the latents
# [batch_size, n_token, latent_dim]
noise = torch.randn_like(latents).to(latents)
bs = latents.shape[0]
# Sample a random timestep for each motion
timesteps = torch.randint(
0,
self.noise_scheduler.config.num_train_timesteps,
(bs,),
device=latents.device,
)
# import pdb; pdb.set_trace()
timesteps = timesteps.long()
# Add noise to the latents according to the noise magnitude at each timestep
# x_t
noisy_z = self.noise_scheduler.add_noise(latents, noise, timesteps)
# diffusion model forward
noise_pred = self.denoiser_model(noisy_z, timesteps, cond_latents)
# compute loss
if self.noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif self.noise_scheduler.config.prediction_type == "v_prediction":
target = self.noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(f"Prediction Type: {self.noise_scheduler.prediction_type} not supported.")
if self.cfg.snr_gamma == 0:
if self.cfg.loss.loss_type == "l1":
loss = F.l1_loss(noise_pred, target, reduction="mean")
elif self.cfg.loss.loss_type in ["mse", "l2"]:
loss = F.mse_loss(noise_pred, target, reduction="mean")
else:
raise ValueError(f"Loss Type: {self.cfg.loss.loss_type} not supported.")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
# Since we predict the noise instead of x_0, the original formulation is slightly changed.
# This is discussed in Section 4.2 of the same paper.
snr = compute_snr(self.noise_scheduler, timesteps)
mse_loss_weights = torch.stack([snr, self.cfg.snr_gamma * torch.ones_like(timesteps)], dim=1).min(
dim=1
)[0]
if self.noise_scheduler.config.prediction_type == "epsilon":
mse_loss_weights = mse_loss_weights / snr
elif noise_scheduler.config.prediction_type == "v_prediction":
mse_loss_weights = mse_loss_weights / (snr + 1)
if self.cfg.loss.loss_type == "l1":
loss = F.l1_loss(noise_pred, target, reduction="none")
elif self.cfg.loss.loss_type in ["mse", "l2"]:
loss = F.mse_loss(noise_pred, target, reduction="none")
else:
raise ValueError(f"Loss Type: {self.cfg.loss.loss_type} not supported.")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
return {
"loss_diffusion": loss,
"latents": latents,
"x_0": noisy_z,
"noise": noise,
"noise_pred": noise_pred,
"timesteps": timesteps,
}
def training_step(self, batch, batch_idx):
out = self(batch)
loss = 0.
for name, value in out.items():
if name.startswith("loss_"):
self.log(f"train/{name}", value)
loss += value * self.C(self.cfg.loss[name.replace("loss_", "lambda_")])
for name, value in self.cfg.loss.items():
if name.startswith("lambda_"):
self.log(f"train_params/{name}", self.C(value))
return {"loss": loss}
@torch.no_grad()
def validation_step(self, batch, batch_idx):
self.eval()
if get_rank() == 0:
sample_inputs = json.loads(open(self.cfg.val_samples_json).read()) # condition
sample_inputs_ = copy.deepcopy(sample_inputs)
sample_outputs = self.sample(sample_inputs) # list
for i, sample_output in enumerate(sample_outputs):
mesh_v_f, has_surface = self.shape_model.extract_geometry(sample_output, octree_depth=7)
for j in range(len(mesh_v_f)):
if "text" in sample_inputs_ and "image" in sample_inputs_:
name = sample_inputs_["image"][j].split("/")[-1].replace(".png", "")
elif "text" in sample_inputs_ and "mvimage" in sample_inputs_:
name = sample_inputs_["mvimages"][j][0].split("/")[-2].replace(".png", "")
elif "text" in sample_inputs_:
name = sample_inputs_["text"][j].replace(" ", "_")
elif "image" in sample_inputs_:
name = sample_inputs_["image"][j].split("/")[-1].replace(".png", "")
elif "mvimages" in sample_inputs_:
name = sample_inputs_["mvimages"][j][0].split("/")[-2].replace(".png", "")
self.save_mesh(
f"it{self.true_global_step}/{name}_{i}.obj",
mesh_v_f[j][0], mesh_v_f[j][1]
)
out = self(batch)
if self.global_step == 0:
latents = self.shape_model.decode(out["latents"])
mesh_v_f, has_surface = self.shape_model.extract_geometry(latents)
self.save_mesh(
f"it{self.true_global_step}/{batch['uid'][0]}_{batch['sel_idx'][0] if 'sel_idx' in batch.keys() else 0}.obj",
mesh_v_f[0][0], mesh_v_f[0][1]
)
# exit()
torch.cuda.empty_cache()
return {"val/loss": out["loss_diffusion"]}
@torch.no_grad()
def sample(self,
sample_inputs: Dict[str, Union[torch.FloatTensor, List[str]]],
sample_times: int = 1,
steps: Optional[int] = None,
guidance_scale: Optional[float] = None,
eta: float = 0.0,
return_intermediates: bool = False,
camera_embeds: Optional[torch.Tensor] = None,
seed: Optional[int] = None,
**kwargs):
if steps is None:
steps = self.cfg.num_inference_steps
if guidance_scale is None:
guidance_scale = self.cfg.guidance_scale
do_classifier_free_guidance = guidance_scale > 0
# conditional encode
if "image" in sample_inputs:
sample_inputs["image"] = [Image.open(img) for img in sample_inputs["image"]]
cond = self.condition.encode_image(sample_inputs["image"])
if do_classifier_free_guidance:
un_cond = self.condition.empty_image_embeds.repeat(len(sample_inputs["image"]), 1, 1).to(cond)
cond = torch.cat([un_cond, cond], dim=0)
elif "mvimages" in sample_inputs: # by default 4 views
bs = len(sample_inputs["mvimages"])
cond = []
for image in sample_inputs["mvimages"]:
if isinstance(image, list) and isinstance(image[0], str):
sample_inputs["image"] = [Image.open(img) for img in image] # List[PIL]
else:
sample_inputs["image"] = image
cond += [self.condition.encode_image(sample_inputs["image"])]
cond = torch.stack(cond, dim=0)# tensor shape 为[len(sample_inputs["mvimages"], 4*(num_latents+1), context_dim]
if do_classifier_free_guidance:
un_cond = self.condition.empty_image_embeds.unsqueeze(0).repeat(len(sample_inputs["mvimages"]), cond.shape[1] // self.condition.cfg.n_views, 1, 1).to(cond) # shape 为[len(sample_inputs["mvimages"], 4*(num_latents+1), context_dim]
cond = torch.cat([un_cond, cond], dim=0).view(bs * 2, -1, cond[0].shape[-1])
else:
raise NotImplementedError("Only text, image or mvimages condition is supported.")
outputs = []
latents = None
if seed != None:
generator = torch.Generator(device="cuda").manual_seed(seed)
else:
generator = None
if not return_intermediates:
for _ in range(sample_times):
sample_loop = ddim_sample(
self.denoise_scheduler,
self.denoiser_model.eval(),
shape=self.shape_model.latent_shape,
cond=cond,
steps=steps,
guidance_scale=guidance_scale,
do_classifier_free_guidance=do_classifier_free_guidance,
device=self.device,
eta=eta,
disable_prog=False,
generator= generator
)
for sample, t in sample_loop:
latents = sample
outputs.append(self.shape_model.decode(latents / self.z_scale_factor, **kwargs))
else:
sample_loop = ddim_sample(
self.denoise_scheduler,
self.denoiser_model.eval(),
shape=self.shape_model.latent_shape,
cond=cond,
steps=steps,
guidance_scale=guidance_scale,
do_classifier_free_guidance=do_classifier_free_guidance,
device=self.device,
eta=eta,
disable_prog=False,
generator= generator
)
iter_size = steps // sample_times
i = 0
for sample, t in sample_loop:
latents = sample
if i % iter_size == 0 or i == steps - 1:
outputs.append(self.shape_model.decode(latents / self.z_scale_factor, **kwargs))
i += 1
return outputs
def on_validation_epoch_end(self):
pass
@@ -0,0 +1 @@
from . import base
+118
View File
@@ -0,0 +1,118 @@
from dataclasses import dataclass
import torch
import torch.nn as nn
from craftsman.utils.config import parse_structured
from craftsman.utils.misc import get_device, load_module_weights
from craftsman.utils.typing import *
class Configurable:
@dataclass
class Config:
pass
def __init__(self, cfg: Optional[dict] = None) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
class Updateable:
def do_update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
):
for attr in self.__dir__():
if attr.startswith("_"):
continue
try:
module = getattr(self, attr)
except:
continue # ignore attributes like property, which can't be retrived using getattr?
if isinstance(module, Updateable):
module.do_update_step(
epoch, global_step, on_load_weights=on_load_weights
)
self.update_step(epoch, global_step, on_load_weights=on_load_weights)
def do_update_step_end(self, epoch: int, global_step: int):
for attr in self.__dir__():
if attr.startswith("_"):
continue
try:
module = getattr(self, attr)
except:
continue # ignore attributes like property, which can't be retrived using getattr?
if isinstance(module, Updateable):
module.do_update_step_end(epoch, global_step)
self.update_step_end(epoch, global_step)
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# override this method to implement custom update logic
# if on_load_weights is True, you should be careful doing things related to model evaluations,
# as the models and tensors are not guarenteed to be on the same device
pass
def update_step_end(self, epoch: int, global_step: int):
pass
def update_if_possible(module: Any, epoch: int, global_step: int) -> None:
if isinstance(module, Updateable):
module.do_update_step(epoch, global_step)
def update_end_if_possible(module: Any, epoch: int, global_step: int) -> None:
if isinstance(module, Updateable):
module.do_update_step_end(epoch, global_step)
class BaseObject(Updateable):
@dataclass
class Config:
pass
cfg: Config # add this to every subclass of BaseObject to enable static type checking
def __init__(
self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs
) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
self.device = get_device()
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs) -> None:
pass
class BaseModule(nn.Module, Updateable):
@dataclass
class Config:
weights: Optional[str] = None
cfg: Config # add this to every subclass of BaseModule to enable static type checking
def __init__(
self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs
) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
self.device = get_device()
self.configure(*args, **kwargs)
if self.cfg.weights is not None:
# format: path/to/weights:module_name
weights_path, module_name = self.cfg.weights.split(":")
state_dict, epoch, global_step = load_module_weights(
weights_path, module_name=module_name, map_location="cpu"
)
self.load_state_dict(state_dict)
self.do_update_step(
epoch, global_step, on_load_weights=True
) # restore states
# dummy tensor to indicate model state
self._dummy: Float[Tensor, "..."]
self.register_buffer("_dummy", torch.zeros(0).float(), persistent=False)
def configure(self, *args, **kwargs) -> None:
pass
+156
View File
@@ -0,0 +1,156 @@
import os
import shutil
import subprocess
import pytorch_lightning
from craftsman.utils.config import dump_config
from craftsman.utils.misc import parse_version
if parse_version(pytorch_lightning.__version__) > parse_version("1.8"):
from pytorch_lightning.callbacks import Callback
else:
from pytorch_lightning.callbacks.base import Callback
from pytorch_lightning.callbacks.progress import TQDMProgressBar
from pytorch_lightning.utilities.rank_zero import rank_zero_only, rank_zero_warn
class VersionedCallback(Callback):
def __init__(self, save_root, version=None, use_version=True):
self.save_root = save_root
self._version = version
self.use_version = use_version
@property
def version(self) -> int:
"""Get the experiment version.
Returns:
The experiment version if specified else the next version.
"""
if self._version is None:
self._version = self._get_next_version()
return self._version
def _get_next_version(self):
existing_versions = []
if os.path.isdir(self.save_root):
for f in os.listdir(self.save_root):
bn = os.path.basename(f)
if bn.startswith("version_"):
dir_ver = os.path.splitext(bn)[0].split("_")[1].replace("/", "")
existing_versions.append(int(dir_ver))
if len(existing_versions) == 0:
return 0
return max(existing_versions) + 1
@property
def savedir(self):
if not self.use_version:
return self.save_root
return os.path.join(
self.save_root,
self.version
if isinstance(self.version, str)
else f"version_{self.version}",
)
class CodeSnapshotCallback(VersionedCallback):
def __init__(self, save_root, version=None, use_version=True):
super().__init__(save_root, version, use_version)
def get_file_list(self):
return [
b.decode()
for b in set(
subprocess.check_output(
'git ls-files -- ":!:load/*"', shell=True
).splitlines()
)
| set( # hard code, TODO: use config to exclude folders or files
subprocess.check_output(
"git ls-files --others --exclude-standard", shell=True
).splitlines()
)
]
@rank_zero_only
def save_code_snapshot(self):
os.makedirs(self.savedir, exist_ok=True)
for f in self.get_file_list():
if not os.path.exists(f) or os.path.isdir(f):
continue
os.makedirs(os.path.join(self.savedir, os.path.dirname(f)), exist_ok=True)
shutil.copyfile(f, os.path.join(self.savedir, f))
def on_fit_start(self, trainer, pl_module):
try:
self.save_code_snapshot()
except:
rank_zero_warn(
"Code snapshot is not saved. Please make sure you have git installed and are in a git repository."
)
class ConfigSnapshotCallback(VersionedCallback):
def __init__(self, config_path, config, save_root, version=None, use_version=True):
super().__init__(save_root, version, use_version)
self.config_path = config_path
self.config = config
@rank_zero_only
def save_config_snapshot(self):
os.makedirs(self.savedir, exist_ok=True)
dump_config(os.path.join(self.savedir, "parsed.yaml"), self.config)
shutil.copyfile(self.config_path, os.path.join(self.savedir, "raw.yaml"))
def on_fit_start(self, trainer, pl_module):
self.save_config_snapshot()
class CustomProgressBar(TQDMProgressBar):
def get_metrics(self, *args, **kwargs):
# don't show the version number
items = super().get_metrics(*args, **kwargs)
items.pop("v_num", None)
return items
class ProgressCallback(Callback):
def __init__(self, save_path):
super().__init__()
self.save_path = save_path
self._file_handle = None
@property
def file_handle(self):
if self._file_handle is None:
self._file_handle = open(self.save_path, "w")
return self._file_handle
@rank_zero_only
def write(self, msg: str) -> None:
self.file_handle.seek(0)
self.file_handle.truncate()
self.file_handle.write(msg)
self.file_handle.flush()
@rank_zero_only
def on_train_batch_end(self, trainer, pl_module, *args, **kwargs):
self.write(
f"Generation progress: {pl_module.true_global_step / trainer.max_steps * 100:.2f}%"
)
@rank_zero_only
def on_validation_start(self, trainer, pl_module):
self.write(f"Rendering validation image ...")
@rank_zero_only
def on_test_start(self, trainer, pl_module):
self.write(f"Rendering video ...")
@rank_zero_only
def on_predict_start(self, trainer, pl_module):
self.write(f"Exporting mesh assets ...")
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""
Adapted from: https://github.com/openai/guided-diffusion/blob/22e0df8183507e13a7813f8d38d51b072ca1e67c/guided_diffusion/nn.py#L124
"""
import torch
from craftsman.utils.typing import *
def checkpoint(
func: Callable[..., Union[torch.Tensor, Sequence[torch.Tensor]]],
inputs: Sequence[torch.Tensor],
params: Iterable[torch.Tensor],
flag: bool,
use_deepspeed: bool = False
):
"""
Evaluate a function without caching intermediate activations, allowing for
reduced memory at the expense of extra compute in the backward pass.
:param func: the function to evaluate.
:param inputs: the argument sequence to pass to `func`.
:param params: a sequence of parameters `func` depends on but does not
explicitly take as arguments.
:param flag: if False, disable gradient checkpointing.
:param use_deepspeed: if True, use deepspeed
"""
if flag:
if use_deepspeed:
import deepspeed
return deepspeed.checkpointing.checkpoint(func, *inputs)
args = tuple(inputs) + tuple(params)
return CheckpointFunction.apply(func, len(inputs), *args)
else:
return func(*inputs)
class CheckpointFunction(torch.autograd.Function):
@staticmethod
@torch.cuda.amp.custom_fwd
def forward(ctx, run_function, length, *args):
ctx.run_function = run_function
ctx.input_tensors = list(args[:length])
ctx.input_params = list(args[length:])
with torch.no_grad():
output_tensors = ctx.run_function(*ctx.input_tensors)
return output_tensors
@staticmethod
@torch.cuda.amp.custom_bwd
def backward(ctx, *output_grads):
ctx.input_tensors = [x.detach().requires_grad_(True) for x in ctx.input_tensors]
with torch.enable_grad():
# Fixes a bug where the first op in run_function modifies the
# Tensor storage in place, which is not allowed for detach()'d
# Tensors.
shallow_copies = [x.view_as(x) for x in ctx.input_tensors]
output_tensors = ctx.run_function(*shallow_copies)
input_grads = torch.autograd.grad(
output_tensors,
ctx.input_tensors + ctx.input_params,
output_grads,
allow_unused=True,
)
del ctx.input_tensors
del ctx.input_params
del output_tensors
return (None, None) + input_grads
+128
View File
@@ -0,0 +1,128 @@
import os
from dataclasses import dataclass, field
from datetime import datetime
from omegaconf import OmegaConf
import craftsman
from craftsman.utils.typing import *
# ============ Register OmegaConf Recolvers ============= #
OmegaConf.register_new_resolver(
"calc_exp_lr_decay_rate", lambda factor, n: factor ** (1.0 / n), replace=True
)
OmegaConf.register_new_resolver("add", lambda a, b: a + b, replace=True)
OmegaConf.register_new_resolver("sub", lambda a, b: a - b, replace=True)
OmegaConf.register_new_resolver("mul", lambda a, b: a * b, replace=True)
OmegaConf.register_new_resolver("div", lambda a, b: a / b, replace=True)
OmegaConf.register_new_resolver("idiv", lambda a, b: a // b, replace=True)
OmegaConf.register_new_resolver("basename", lambda p: os.path.basename(p), replace=True)
OmegaConf.register_new_resolver("rmspace", lambda s, sub: str(s).replace(" ", sub), replace=True)
OmegaConf.register_new_resolver("tuple2", lambda s: [float(s), float(s)], replace=True)
OmegaConf.register_new_resolver("gt0", lambda s: s > 0, replace=True)
OmegaConf.register_new_resolver("cmaxgt0", lambda s: C_max(s) > 0, replace=True)
OmegaConf.register_new_resolver("not", lambda s: not s, replace=True)
OmegaConf.register_new_resolver(
"cmaxgt0orcmaxgt0", lambda a, b: C_max(a) > 0 or C_max(b) > 0, replace=True
)
# ======================================================= #
def C_max(value: Any) -> float:
if isinstance(value, int) or isinstance(value, float):
pass
else:
value = config_to_primitive(value)
if not isinstance(value, list):
raise TypeError("Scalar specification only supports list, got", type(value))
if len(value) >= 6:
max_value = value[2]
for i in range(4, len(value), 2):
max_value = max(max_value, value[i])
value = [value[0], value[1], max_value, value[3]]
if len(value) == 3:
value = [0] + value
assert len(value) == 4
start_step, start_value, end_value, end_step = value
value = max(start_value, end_value)
return value
@dataclass
class ExperimentConfig:
name: str = "default"
description: str = ""
tag: str = ""
seed: int = 0
use_timestamp: bool = True
timestamp: Optional[str] = None
exp_root_dir: str = "outputs"
### these shouldn't be set manually
exp_dir: str = "outputs/default"
trial_name: str = "exp"
trial_dir: str = "outputs/default/exp"
n_gpus: int = 1
###
resume: Optional[str] = None
data_type: str = ""
data: dict = field(default_factory=dict)
system_type: str = ""
system: dict = field(default_factory=dict)
# accept pytorch-lightning trainer parameters
# see https://lightning.ai/docs/pytorch/stable/common/trainer.html#trainer-class-api
trainer: dict = field(default_factory=dict)
# accept pytorch-lightning checkpoint callback parameters
# see https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.ModelCheckpoint.html#modelcheckpoint
checkpoint: dict = field(default_factory=dict)
def __post_init__(self):
if not self.tag and not self.use_timestamp:
raise ValueError("Either tag is specified or use_timestamp is True.")
self.trial_name = self.tag
# if resume from an existing config, self.timestamp should not be None
if self.timestamp is None:
self.timestamp = ""
if self.use_timestamp:
if self.n_gpus > 1:
craftsman.warn(
"Timestamp is disabled when using multiple GPUs, please make sure you have a unique tag."
)
else:
self.timestamp = datetime.now().strftime("@%Y%m%d-%H%M%S")
self.trial_name += self.timestamp
self.exp_dir = os.path.join(self.exp_root_dir, self.name)
self.trial_dir = os.path.join(self.exp_dir, self.trial_name)
os.makedirs(self.trial_dir, exist_ok=True)
def load_config(*yamls: str, cli_args: list = [], from_string=False, **kwargs) -> Any:
if from_string:
yaml_confs = [OmegaConf.create(s) for s in yamls]
else:
yaml_confs = [OmegaConf.load(f) for f in yamls]
cli_conf = OmegaConf.from_cli(cli_args)
cfg = OmegaConf.merge(*yaml_confs, cli_conf, kwargs)
OmegaConf.resolve(cfg)
assert isinstance(cfg, DictConfig)
scfg = parse_structured(ExperimentConfig, cfg)
return scfg
def config_to_primitive(config, resolve: bool = True) -> Any:
return OmegaConf.to_container(config, resolve=resolve)
def dump_config(path: str, config) -> None:
with open(path, "w") as fp:
OmegaConf.save(config=config, f=fp)
def parse_structured(fields: Any, cfg: Optional[Union[dict, DictConfig]] = None) -> Any:
scfg = OmegaConf.structured(fields(**cfg))
return scfg
+168
View File
@@ -0,0 +1,168 @@
import gc
import os
import re
import torch
import torch.distributed as dist
from packaging import version
from craftsman.utils.config import config_to_primitive
from craftsman.utils.typing import *
def parse_version(ver: str):
return version.parse(ver)
def get_rank():
# SLURM_PROCID can be set even if SLURM is not managing the multiprocessing,
# therefore LOCAL_RANK needs to be checked first
rank_keys = ("RANK", "LOCAL_RANK", "SLURM_PROCID", "JSM_NAMESPACE_RANK")
for key in rank_keys:
rank = os.environ.get(key)
if rank is not None:
return int(rank)
return 0
def get_world_size():
world_size_keys = ("WORLD_SIZE", "SLURM_NTASKS", "JSM_NAMESPACE_SIZE")
for key in world_size_keys:
world_size = os.environ.get(key)
if world_size is not None:
return int(world_size)
return 1
def get_device():
return torch.device(f"cuda:{get_rank()}")
def load_module_weights(
path, module_name=None, ignore_modules=None, map_location=None
) -> Tuple[dict, int, int]:
if module_name is not None and ignore_modules is not None:
raise ValueError("module_name and ignore_modules cannot be both set")
if map_location is None:
map_location = get_device()
ckpt = torch.load(path, map_location=map_location)
state_dict = ckpt["state_dict"]
state_dict_to_load = state_dict
if ignore_modules is not None:
state_dict_to_load = {}
for k, v in state_dict.items():
ignore = any(
[k.startswith(ignore_module + ".") for ignore_module in ignore_modules]
)
if ignore:
continue
state_dict_to_load[k] = v
if module_name is not None:
state_dict_to_load = {}
for k, v in state_dict.items():
m = re.match(rf"^{module_name}\.(.*)$", k)
if m is None:
continue
state_dict_to_load[m.group(1)] = v
return state_dict_to_load, ckpt["epoch"], ckpt["global_step"]
def C(value: Any, epoch: int, global_step: int) -> float:
if isinstance(value, int) or isinstance(value, float):
pass
else:
value = config_to_primitive(value)
if not isinstance(value, list):
raise TypeError("Scalar specification only supports list, got", type(value))
if len(value) == 3:
value = [0] + value
assert len(value) == 4
start_step, start_value, end_value, end_step = value
if isinstance(end_step, int):
current_step = global_step
value = start_value + (end_value - start_value) * max(
min(1.0, (current_step - start_step) / (end_step - start_step)), 0.0
)
elif isinstance(end_step, float):
current_step = epoch
value = start_value + (end_value - start_value) * max(
min(1.0, (current_step - start_step) / (end_step - start_step)), 0.0
)
return value
def cleanup():
gc.collect()
torch.cuda.empty_cache()
tcnn.free_temporary_memory()
def finish_with_cleanup(func: Callable):
def wrapper(*args, **kwargs):
out = func(*args, **kwargs)
cleanup()
return out
return wrapper
def _distributed_available():
return torch.distributed.is_available() and torch.distributed.is_initialized()
def barrier():
if not _distributed_available():
return
else:
torch.distributed.barrier()
def broadcast(tensor, src=0):
if not _distributed_available():
return tensor
else:
torch.distributed.broadcast(tensor, src=src)
return tensor
def enable_gradient(model, enabled: bool = True) -> None:
for param in model.parameters():
param.requires_grad_(enabled)
def all_gather_batch(tensors):
"""
Performs all_gather operation on the provided tensors.
"""
# Queue the gathered tensors
world_size = get_world_size()
# There is no need for reduction in the single-proc case
if world_size == 1:
if isinstance(tensors, list):
return tensors
return tensors
if not isinstance(tensors, list):
is_list = False
tensors = [tensors]
else:
is_list = True
output_tensor = []
tensor_list = []
for tensor in tensors:
tensor_all = [torch.ones_like(tensor) for _ in range(world_size)]
dist.all_gather(
tensor_all,
tensor,
async_op=False # performance opt
)
tensor_list.append(tensor_all)
for tensor_all in tensor_list:
output_tensor.append(torch.cat(tensor_all, dim=0))
if not is_list:
return output_tensor[0]
return output_tensor
+167
View File
@@ -0,0 +1,167 @@
from collections import defaultdict
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from craftsman.utils.typing import *
def dot(x, y):
return torch.sum(x * y, -1, keepdim=True)
def reflect(x, n):
return 2 * dot(x, n) * n - x
ValidScale = Union[Tuple[float, float], Num[Tensor, "2 D"]]
def scale_tensor(
dat: Num[Tensor, "... D"], inp_scale: ValidScale, tgt_scale: ValidScale
):
if inp_scale is None:
inp_scale = (0, 1)
if tgt_scale is None:
tgt_scale = (0, 1)
if isinstance(tgt_scale, Tensor):
assert dat.shape[-1] == tgt_scale.shape[-1]
dat = (dat - inp_scale[0]) / (inp_scale[1] - inp_scale[0])
dat = dat * (tgt_scale[1] - tgt_scale[0]) + tgt_scale[0]
return dat
def chunk_batch(func: Callable, chunk_size: int, *args, **kwargs) -> Any:
if chunk_size <= 0:
return func(*args, **kwargs)
B = None
for arg in list(args) + list(kwargs.values()):
if isinstance(arg, torch.Tensor):
B = arg.shape[0]
break
assert (
B is not None
), "No tensor found in args or kwargs, cannot determine batch size."
out = defaultdict(list)
out_type = None
# max(1, B) to support B == 0
for i in range(0, max(1, B), chunk_size):
out_chunk = func(
*[
arg[i : i + chunk_size] if isinstance(arg, torch.Tensor) else arg
for arg in args
],
**{
k: arg[i : i + chunk_size] if isinstance(arg, torch.Tensor) else arg
for k, arg in kwargs.items()
},
)
if out_chunk is None:
continue
out_type = type(out_chunk)
if isinstance(out_chunk, torch.Tensor):
out_chunk = {0: out_chunk}
elif isinstance(out_chunk, tuple) or isinstance(out_chunk, list):
chunk_length = len(out_chunk)
out_chunk = {i: chunk for i, chunk in enumerate(out_chunk)}
elif isinstance(out_chunk, dict):
pass
else:
print(
f"Return value of func must be in type [torch.Tensor, list, tuple, dict], get {type(out_chunk)}."
)
exit(1)
for k, v in out_chunk.items():
v = v if torch.is_grad_enabled() else v.detach()
out[k].append(v)
if out_type is None:
return None
out_merged: Dict[Any, Optional[torch.Tensor]] = {}
for k, v in out.items():
if all([vv is None for vv in v]):
# allow None in return value
out_merged[k] = None
elif all([isinstance(vv, torch.Tensor) for vv in v]):
out_merged[k] = torch.cat(v, dim=0)
else:
raise TypeError(
f"Unsupported types in return value of func: {[type(vv) for vv in v if not isinstance(vv, torch.Tensor)]}"
)
if out_type is torch.Tensor:
return out_merged[0]
elif out_type in [tuple, list]:
return out_type([out_merged[i] for i in range(chunk_length)])
elif out_type is dict:
return out_merged
def randn_tensor(
shape: Union[Tuple, List],
generator: Optional[Union[List["torch.Generator"], "torch.Generator"]] = None,
device: Optional["torch.device"] = None,
dtype: Optional["torch.dtype"] = None,
layout: Optional["torch.layout"] = None,
):
"""A helper function to create random tensors on the desired `device` with the desired `dtype`. When
passing a list of generators, you can seed each batch size individually. If CPU generators are passed, the tensor
is always created on the CPU.
"""
# device on which tensor is created defaults to device
rand_device = device
batch_size = shape[0]
layout = layout or torch.strided
device = device or torch.device("cpu")
if generator is not None:
gen_device_type = generator.device.type if not isinstance(generator, list) else generator[0].device.type
if gen_device_type != device.type and gen_device_type == "cpu":
rand_device = "cpu"
if device != "mps":
logger.info(
f"The passed generator was created on 'cpu' even though a tensor on {device} was expected."
f" Tensors will be created on 'cpu' and then moved to {device}. Note that one can probably"
f" slighly speed up this function by passing a generator that was created on the {device} device."
)
elif gen_device_type != device.type and gen_device_type == "cuda":
raise ValueError(f"Cannot generate a {device} tensor from a generator of type {gen_device_type}.")
# make sure generator list of length 1 is treated like a non-list
if isinstance(generator, list) and len(generator) == 1:
generator = generator[0]
if isinstance(generator, list):
shape = (1,) + shape[1:]
latents = [
torch.randn(shape, generator=generator[i], device=rand_device, dtype=dtype, layout=layout)
for i in range(batch_size)
]
latents = torch.cat(latents, dim=0).to(device)
else:
latents = torch.randn(shape, generator=generator, device=rand_device, dtype=dtype, layout=layout).to(device)
return latents
def generate_dense_grid_points(
bbox_min: np.ndarray,
bbox_max: np.ndarray,
grids_resolution: int,
indexing: str = "ij"
):
length = bbox_max - bbox_min
num_cells = grids_resolution
x = np.linspace(bbox_min[0], bbox_max[0], int(num_cells) + 1, dtype=np.float32)
y = np.linspace(bbox_min[1], bbox_max[1], int(num_cells) + 1, dtype=np.float32)
z = np.linspace(bbox_min[2], bbox_max[2], int(num_cells) + 1, dtype=np.float32)
[xs, ys, zs] = np.meshgrid(x, y, z, indexing=indexing)
xyz = np.stack((xs, ys, zs), axis=-1)
xyz = xyz.reshape(-1, 3)
grid_size = [int(num_cells) + 1, int(num_cells) + 1, int(num_cells) + 1]
return xyz, grid_size, length
+653
View File
@@ -0,0 +1,653 @@
import json
import os
import re
import shutil
import cv2
import imageio
import matplotlib.pyplot as plt
import numpy as np
import torch
import trimesh
from matplotlib import cm
from matplotlib.colors import LinearSegmentedColormap
from PIL import Image, ImageDraw
from pytorch_lightning.loggers import WandbLogger
from craftsman.models.geometry.utils import Mesh
from craftsman.utils.typing import *
class SaverMixin:
import wandb
_save_dir: Optional[str] = None
_wandb_logger: Optional[WandbLogger] = None
def set_save_dir(self, save_dir: str):
self._save_dir = save_dir
def get_save_dir(self):
if self._save_dir is None:
raise ValueError("Save dir is not set")
return self._save_dir
def convert_data(self, data):
if data is None:
return None
elif isinstance(data, np.ndarray):
return data
elif isinstance(data, torch.Tensor):
return data.detach().cpu().numpy()
elif isinstance(data, list):
return [self.convert_data(d) for d in data]
elif isinstance(data, dict):
return {k: self.convert_data(v) for k, v in data.items()}
else:
raise TypeError(
"Data must be in type numpy.ndarray, torch.Tensor, list or dict, getting",
type(data),
)
def get_save_path(self, filename):
save_path = os.path.join(self.get_save_dir(), filename)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
return save_path
def create_loggers(self, cfg_loggers: DictConfig) -> None:
if "wandb" in cfg_loggers.keys() and cfg_loggers.wandb.enable:
self._wandb_logger = WandbLogger(
project=cfg_loggers.wandb.project, name=cfg_loggers.wandb.name
)
def get_loggers(self) -> List:
if self._wandb_logger:
return [self._wandb_logger]
else:
return []
DEFAULT_RGB_KWARGS = {"data_format": "HWC", "data_range": (0, 1)}
DEFAULT_UV_KWARGS = {
"data_format": "HWC",
"data_range": (0, 1),
"cmap": "checkerboard",
}
DEFAULT_GRAYSCALE_KWARGS = {"data_range": None, "cmap": "jet"}
DEFAULT_GRID_KWARGS = {"align": "max"}
def get_rgb_image_(self, img, data_format, data_range, rgba=False):
img = self.convert_data(img)
assert data_format in ["CHW", "HWC"]
if data_format == "CHW":
img = img.transpose(1, 2, 0)
if img.dtype != np.uint8:
img = img.clip(min=data_range[0], max=data_range[1])
img = (
(img - data_range[0]) / (data_range[1] - data_range[0]) * 255.0
).astype(np.uint8)
nc = 4 if rgba else 3
imgs = [img[..., start : start + nc] for start in range(0, img.shape[-1], nc)]
imgs = [
img_
if img_.shape[-1] == nc
else np.concatenate(
[
img_,
np.zeros(
(img_.shape[0], img_.shape[1], nc - img_.shape[2]),
dtype=img_.dtype,
),
],
axis=-1,
)
for img_ in imgs
]
img = np.concatenate(imgs, axis=1)
if rgba:
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
else:
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
return img
def _save_rgb_image(
self,
filename,
img,
data_format,
data_range,
name: Optional[str] = None,
step: Optional[int] = None,
):
img = self.get_rgb_image_(img, data_format, data_range)
cv2.imwrite(filename, img)
if name and self._wandb_logger:
wandb.log(
{
name: wandb.Image(self.get_save_path(filename)),
"trainer/global_step": step,
}
)
def save_rgb_image(
self,
filename,
img,
data_format=DEFAULT_RGB_KWARGS["data_format"],
data_range=DEFAULT_RGB_KWARGS["data_range"],
name: Optional[str] = None,
step: Optional[int] = None,
) -> str:
save_path = self.get_save_path(filename)
self._save_rgb_image(save_path, img, data_format, data_range, name, step)
return save_path
def get_uv_image_(self, img, data_format, data_range, cmap):
img = self.convert_data(img)
assert data_format in ["CHW", "HWC"]
if data_format == "CHW":
img = img.transpose(1, 2, 0)
img = img.clip(min=data_range[0], max=data_range[1])
img = (img - data_range[0]) / (data_range[1] - data_range[0])
assert cmap in ["checkerboard", "color"]
if cmap == "checkerboard":
n_grid = 64
mask = (img * n_grid).astype(int)
mask = (mask[..., 0] + mask[..., 1]) % 2 == 0
img = np.ones((img.shape[0], img.shape[1], 3), dtype=np.uint8) * 255
img[mask] = np.array([255, 0, 255], dtype=np.uint8)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
elif cmap == "color":
img_ = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
img_[..., 0] = (img[..., 0] * 255).astype(np.uint8)
img_[..., 1] = (img[..., 1] * 255).astype(np.uint8)
img_ = cv2.cvtColor(img_, cv2.COLOR_RGB2BGR)
img = img_
return img
def save_uv_image(
self,
filename,
img,
data_format=DEFAULT_UV_KWARGS["data_format"],
data_range=DEFAULT_UV_KWARGS["data_range"],
cmap=DEFAULT_UV_KWARGS["cmap"],
) -> str:
save_path = self.get_save_path(filename)
img = self.get_uv_image_(img, data_format, data_range, cmap)
cv2.imwrite(save_path, img)
return save_path
def get_grayscale_image_(self, img, data_range, cmap):
img = self.convert_data(img)
img = np.nan_to_num(img)
if data_range is None:
img = (img - img.min()) / (img.max() - img.min())
else:
img = img.clip(data_range[0], data_range[1])
img = (img - data_range[0]) / (data_range[1] - data_range[0])
assert cmap in [None, "jet", "magma", "spectral"]
if cmap == None:
img = (img * 255.0).astype(np.uint8)
img = np.repeat(img[..., None], 3, axis=2)
elif cmap == "jet":
img = (img * 255.0).astype(np.uint8)
img = cv2.applyColorMap(img, cv2.COLORMAP_JET)
elif cmap == "magma":
img = 1.0 - img
base = cm.get_cmap("magma")
num_bins = 256
colormap = LinearSegmentedColormap.from_list(
f"{base.name}{num_bins}", base(np.linspace(0, 1, num_bins)), num_bins
)(np.linspace(0, 1, num_bins))[:, :3]
a = np.floor(img * 255.0)
b = (a + 1).clip(max=255.0)
f = img * 255.0 - a
a = a.astype(np.uint16).clip(0, 255)
b = b.astype(np.uint16).clip(0, 255)
img = colormap[a] + (colormap[b] - colormap[a]) * f[..., None]
img = (img * 255.0).astype(np.uint8)
elif cmap == "spectral":
colormap = plt.get_cmap("Spectral")
def blend_rgba(image):
image = image[..., :3] * image[..., -1:] + (
1.0 - image[..., -1:]
) # blend A to RGB
return image
img = colormap(img)
img = blend_rgba(img)
img = (img * 255).astype(np.uint8)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
return img
def _save_grayscale_image(
self,
filename,
img,
data_range,
cmap,
name: Optional[str] = None,
step: Optional[int] = None,
):
img = self.get_grayscale_image_(img, data_range, cmap)
cv2.imwrite(filename, img)
if name and self._wandb_logger:
wandb.log(
{
name: wandb.Image(self.get_save_path(filename)),
"trainer/global_step": step,
}
)
def save_grayscale_image(
self,
filename,
img,
data_range=DEFAULT_GRAYSCALE_KWARGS["data_range"],
cmap=DEFAULT_GRAYSCALE_KWARGS["cmap"],
name: Optional[str] = None,
step: Optional[int] = None,
) -> str:
save_path = self.get_save_path(filename)
self._save_grayscale_image(save_path, img, data_range, cmap, name, step)
return save_path
def get_image_grid_(self, imgs, align):
if isinstance(imgs[0], list):
return np.concatenate(
[self.get_image_grid_(row, align) for row in imgs], axis=0
)
cols = []
for col in imgs:
assert col["type"] in ["rgb", "uv", "grayscale"]
if col["type"] == "rgb":
rgb_kwargs = self.DEFAULT_RGB_KWARGS.copy()
rgb_kwargs.update(col["kwargs"])
cols.append(self.get_rgb_image_(col["img"], **rgb_kwargs))
elif col["type"] == "uv":
uv_kwargs = self.DEFAULT_UV_KWARGS.copy()
uv_kwargs.update(col["kwargs"])
cols.append(self.get_uv_image_(col["img"], **uv_kwargs))
elif col["type"] == "grayscale":
grayscale_kwargs = self.DEFAULT_GRAYSCALE_KWARGS.copy()
grayscale_kwargs.update(col["kwargs"])
cols.append(self.get_grayscale_image_(col["img"], **grayscale_kwargs))
if align == "max":
h = max([col.shape[0] for col in cols])
w = max([col.shape[1] for col in cols])
elif align == "min":
h = min([col.shape[0] for col in cols])
w = min([col.shape[1] for col in cols])
elif isinstance(align, int):
h = align
w = align
elif (
isinstance(align, tuple)
and isinstance(align[0], int)
and isinstance(align[1], int)
):
h, w = align
else:
raise ValueError(
f"Unsupported image grid align: {align}, should be min, max, int or (int, int)"
)
for i in range(len(cols)):
if cols[i].shape[0] != h or cols[i].shape[1] != w:
cols[i] = cv2.resize(cols[i], (w, h), interpolation=cv2.INTER_LINEAR)
return np.concatenate(cols, axis=1)
def save_image_grid(
self,
filename,
imgs,
align=DEFAULT_GRID_KWARGS["align"],
name: Optional[str] = None,
step: Optional[int] = None,
texts: Optional[List[float]] = None,
):
save_path = self.get_save_path(filename)
img = self.get_image_grid_(imgs, align=align)
if texts is not None:
img = Image.fromarray(img)
draw = ImageDraw.Draw(img)
black, white = (0, 0, 0), (255, 255, 255)
for i, text in enumerate(texts):
draw.text((2, (img.size[1] // len(texts)) * i + 1), f"{text}", white)
draw.text((0, (img.size[1] // len(texts)) * i + 1), f"{text}", white)
draw.text((2, (img.size[1] // len(texts)) * i - 1), f"{text}", white)
draw.text((0, (img.size[1] // len(texts)) * i - 1), f"{text}", white)
draw.text((1, (img.size[1] // len(texts)) * i), f"{text}", black)
img = np.asarray(img)
cv2.imwrite(save_path, img)
if name and self._wandb_logger:
wandb.log({name: wandb.Image(save_path), "trainer/global_step": step})
return save_path
def save_image(self, filename, img) -> str:
save_path = self.get_save_path(filename)
img = self.convert_data(img)
assert img.dtype == np.uint8 or img.dtype == np.uint16
if img.ndim == 3 and img.shape[-1] == 3:
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
elif img.ndim == 3 and img.shape[-1] == 4:
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
cv2.imwrite(save_path, img)
return save_path
def save_cubemap(self, filename, img, data_range=(0, 1), rgba=False) -> str:
save_path = self.get_save_path(filename)
img = self.convert_data(img)
assert img.ndim == 4 and img.shape[0] == 6 and img.shape[1] == img.shape[2]
imgs_full = []
for start in range(0, img.shape[-1], 3):
img_ = img[..., start : start + 3]
img_ = np.stack(
[
self.get_rgb_image_(img_[i], "HWC", data_range, rgba=rgba)
for i in range(img_.shape[0])
],
axis=0,
)
size = img_.shape[1]
placeholder = np.zeros((size, size, 3), dtype=np.float32)
img_full = np.concatenate(
[
np.concatenate(
[placeholder, img_[2], placeholder, placeholder], axis=1
),
np.concatenate([img_[1], img_[4], img_[0], img_[5]], axis=1),
np.concatenate(
[placeholder, img_[3], placeholder, placeholder], axis=1
),
],
axis=0,
)
imgs_full.append(img_full)
imgs_full = np.concatenate(imgs_full, axis=1)
cv2.imwrite(save_path, imgs_full)
return save_path
def save_data(self, filename, data) -> str:
data = self.convert_data(data)
if isinstance(data, dict):
if not filename.endswith(".npz"):
filename += ".npz"
save_path = self.get_save_path(filename)
np.savez(save_path, **data)
else:
if not filename.endswith(".npy"):
filename += ".npy"
save_path = self.get_save_path(filename)
np.save(save_path, data)
return save_path
def save_state_dict(self, filename, data) -> str:
save_path = self.get_save_path(filename)
torch.save(data, save_path)
return save_path
def save_img_sequence(
self,
filename,
img_dir,
matcher,
save_format="mp4",
fps=30,
name: Optional[str] = None,
step: Optional[int] = None,
) -> str:
assert save_format in ["gif", "mp4"]
if not filename.endswith(save_format):
filename += f".{save_format}"
save_path = self.get_save_path(filename)
matcher = re.compile(matcher)
img_dir = os.path.join(self.get_save_dir(), img_dir)
imgs = []
for f in os.listdir(img_dir):
if matcher.search(f):
imgs.append(f)
imgs = sorted(imgs, key=lambda f: int(matcher.search(f).groups()[0]))
imgs = [cv2.imread(os.path.join(img_dir, f)) for f in imgs]
if save_format == "gif":
imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs]
imageio.mimsave(save_path, imgs, fps=fps, palettesize=256)
elif save_format == "mp4":
imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs]
imageio.mimsave(save_path, imgs, fps=fps)
if name and self._wandb_logger:
wandb.log(
{
name: wandb.Video(save_path, format="mp4"),
"trainer/global_step": step,
}
)
return save_path
def save_mesh(self, filename, v_pos, t_pos_idx, v_tex=None, t_tex_idx=None) -> str:
save_path = self.get_save_path(filename)
v_pos = self.convert_data(v_pos)
t_pos_idx = self.convert_data(t_pos_idx)
mesh = trimesh.Trimesh(vertices=v_pos, faces=t_pos_idx)
mesh.export(save_path)
return save_path
def save_obj(
self,
filename: str,
mesh: Mesh,
save_mat: bool = False,
save_normal: bool = False,
save_uv: bool = False,
save_vertex_color: bool = False,
map_Kd: Optional[Float[Tensor, "H W 3"]] = None,
map_Ks: Optional[Float[Tensor, "H W 3"]] = None,
map_Bump: Optional[Float[Tensor, "H W 3"]] = None,
map_Pm: Optional[Float[Tensor, "H W 1"]] = None,
map_Pr: Optional[Float[Tensor, "H W 1"]] = None,
map_format: str = "jpg",
) -> List[str]:
save_paths: List[str] = []
if not filename.endswith(".obj"):
filename += ".obj"
v_pos, t_pos_idx = self.convert_data(mesh.v_pos), self.convert_data(
mesh.t_pos_idx
)
v_nrm, v_tex, t_tex_idx, v_rgb = None, None, None, None
if save_normal:
v_nrm = self.convert_data(mesh.v_nrm)
if save_uv:
v_tex, t_tex_idx = self.convert_data(mesh.v_tex), self.convert_data(
mesh.t_tex_idx
)
if save_vertex_color:
v_rgb = self.convert_data(mesh.v_rgb)
matname, mtllib = None, None
if save_mat:
matname = "default"
mtl_filename = filename.replace(".obj", ".mtl")
mtllib = os.path.basename(mtl_filename)
mtl_save_paths = self._save_mtl(
mtl_filename,
matname,
map_Kd=self.convert_data(map_Kd),
map_Ks=self.convert_data(map_Ks),
map_Bump=self.convert_data(map_Bump),
map_Pm=self.convert_data(map_Pm),
map_Pr=self.convert_data(map_Pr),
map_format=map_format,
)
save_paths += mtl_save_paths
obj_save_path = self._save_obj(
filename,
v_pos,
t_pos_idx,
v_nrm=v_nrm,
v_tex=v_tex,
t_tex_idx=t_tex_idx,
v_rgb=v_rgb,
matname=matname,
mtllib=mtllib,
)
save_paths.append(obj_save_path)
return save_paths
def _save_obj(
self,
filename,
v_pos,
t_pos_idx,
v_nrm=None,
v_tex=None,
t_tex_idx=None,
v_rgb=None,
matname=None,
mtllib=None,
) -> str:
obj_str = ""
if matname is not None:
obj_str += f"mtllib {mtllib}\n"
obj_str += f"g object\n"
obj_str += f"usemtl {matname}\n"
for i in range(len(v_pos)):
obj_str += f"v {v_pos[i][0]} {v_pos[i][1]} {v_pos[i][2]}"
if v_rgb is not None:
obj_str += f" {v_rgb[i][0]} {v_rgb[i][1]} {v_rgb[i][2]}"
obj_str += "\n"
if v_nrm is not None:
for v in v_nrm:
obj_str += f"vn {v[0]} {v[1]} {v[2]}\n"
if v_tex is not None:
for v in v_tex:
obj_str += f"vt {v[0]} {1.0 - v[1]}\n"
for i in range(len(t_pos_idx)):
obj_str += "f"
for j in range(3):
obj_str += f" {t_pos_idx[i][j] + 1}/"
if v_tex is not None:
obj_str += f"{t_tex_idx[i][j] + 1}"
obj_str += "/"
if v_nrm is not None:
obj_str += f"{t_pos_idx[i][j] + 1}"
obj_str += "\n"
save_path = self.get_save_path(filename)
with open(save_path, "w") as f:
f.write(obj_str)
return save_path
def _save_mtl(
self,
filename,
matname,
Ka=(0.0, 0.0, 0.0),
Kd=(1.0, 1.0, 1.0),
Ks=(0.0, 0.0, 0.0),
map_Kd=None,
map_Ks=None,
map_Bump=None,
map_Pm=None,
map_Pr=None,
map_format="jpg",
step: Optional[int] = None,
) -> List[str]:
mtl_save_path = self.get_save_path(filename)
save_paths = [mtl_save_path]
mtl_str = f"newmtl {matname}\n"
mtl_str += f"Ka {Ka[0]} {Ka[1]} {Ka[2]}\n"
if map_Kd is not None:
map_Kd_save_path = os.path.join(
os.path.dirname(mtl_save_path), f"texture_kd.{map_format}"
)
mtl_str += f"map_Kd texture_kd.{map_format}\n"
self._save_rgb_image(
map_Kd_save_path,
map_Kd,
data_format="HWC",
data_range=(0, 1),
name=f"{matname}_Kd",
step=step,
)
save_paths.append(map_Kd_save_path)
else:
mtl_str += f"Kd {Kd[0]} {Kd[1]} {Kd[2]}\n"
if map_Ks is not None:
map_Ks_save_path = os.path.join(
os.path.dirname(mtl_save_path), f"texture_ks.{map_format}"
)
mtl_str += f"map_Ks texture_ks.{map_format}\n"
self._save_rgb_image(
map_Ks_save_path,
map_Ks,
data_format="HWC",
data_range=(0, 1),
name=f"{matname}_Ks",
step=step,
)
save_paths.append(map_Ks_save_path)
else:
mtl_str += f"Ks {Ks[0]} {Ks[1]} {Ks[2]}\n"
if map_Bump is not None:
map_Bump_save_path = os.path.join(
os.path.dirname(mtl_save_path), f"texture_nrm.{map_format}"
)
mtl_str += f"map_Bump texture_nrm.{map_format}\n"
self._save_rgb_image(
map_Bump_save_path,
map_Bump,
data_format="HWC",
data_range=(0, 1),
name=f"{matname}_Bump",
step=step,
)
save_paths.append(map_Bump_save_path)
if map_Pm is not None:
map_Pm_save_path = os.path.join(
os.path.dirname(mtl_save_path), f"texture_metallic.{map_format}"
)
mtl_str += f"map_Pm texture_metallic.{map_format}\n"
self._save_grayscale_image(
map_Pm_save_path,
map_Pm,
data_range=(0, 1),
cmap=None,
name=f"{matname}_refl",
step=step,
)
save_paths.append(map_Pm_save_path)
if map_Pr is not None:
map_Pr_save_path = os.path.join(
os.path.dirname(mtl_save_path), f"texture_roughness.{map_format}"
)
mtl_str += f"map_Pr texture_roughness.{map_format}\n"
self._save_grayscale_image(
map_Pr_save_path,
map_Pr,
data_range=(0, 1),
cmap=None,
name=f"{matname}_Ns",
step=step,
)
save_paths.append(map_Pr_save_path)
with open(self.get_save_path(filename), "w") as f:
f.write(mtl_str)
return save_paths
def save_file(self, filename, src_path) -> str:
save_path = self.get_save_path(filename)
shutil.copyfile(src_path, save_path)
return save_path
def save_json(self, filename, payload) -> str:
save_path = self.get_save_path(filename)
with open(save_path, "w") as f:
f.write(json.dumps(payload))
return save_path
+104
View File
@@ -0,0 +1,104 @@
import sys
import warnings
from bisect import bisect_right
import torch
import torch.nn as nn
from torch.optim import lr_scheduler
import craftsman
def get_scheduler(name):
if hasattr(lr_scheduler, name):
return getattr(lr_scheduler, name)
else:
raise NotImplementedError
def getattr_recursive(m, attr):
for name in attr.split("."):
m = getattr(m, name)
return m
def get_parameters(model, name):
module = getattr_recursive(model, name)
if isinstance(module, nn.Module):
return module.parameters()
elif isinstance(module, nn.Parameter):
return module
return []
def parse_optimizer(config, model):
if hasattr(config, "params"):
params = [
{"params": get_parameters(model, name), "name": name, **args}
for name, args in config.params.items()
]
craftsman.debug(f"Specify optimizer params: {config.params}")
else:
params = model.parameters()
if config.name in ["FusedAdam"]:
import apex
optim = getattr(apex.optimizers, config.name)(params, **config.args)
elif config.name in ["Adan"]:
from craftsman.systems import optimizers
optim = getattr(optimizers, config.name)(params, **config.args)
else:
optim = getattr(torch.optim, config.name)(params, **config.args)
return optim
def parse_scheduler_to_instance(config, optimizer):
if config.name == "ChainedScheduler":
schedulers = [
parse_scheduler_to_instance(conf, optimizer) for conf in config.schedulers
]
scheduler = lr_scheduler.ChainedScheduler(schedulers)
elif config.name == "Sequential":
schedulers = [
parse_scheduler_to_instance(conf, optimizer) for conf in config.schedulers
]
scheduler = lr_scheduler.SequentialLR(
optimizer, schedulers, milestones=config.milestones
)
else:
scheduler = getattr(lr_scheduler, config.name)(optimizer, **config.args)
return scheduler
def parse_scheduler(config, optimizer):
interval = config.get("interval", "epoch")
assert interval in ["epoch", "step"]
if config.name == "SequentialLR":
scheduler = {
"scheduler": lr_scheduler.SequentialLR(
optimizer,
[
parse_scheduler(conf, optimizer)["scheduler"]
for conf in config.schedulers
],
milestones=config.milestones,
),
"interval": interval,
}
elif config.name == "ChainedScheduler":
scheduler = {
"scheduler": lr_scheduler.ChainedScheduler(
[
parse_scheduler(conf, optimizer)["scheduler"]
for conf in config.schedulers
]
),
"interval": interval,
}
else:
scheduler = {
"scheduler": get_scheduler(config.name)(optimizer, **config.args),
"interval": interval,
}
return scheduler
+41
View File
@@ -0,0 +1,41 @@
"""
This module contains type annotations for the project, using
1. Python type hints (https://docs.python.org/3/library/typing.html) for Python objects
2. jaxtyping (https://github.com/google/jaxtyping/blob/main/API.md) for PyTorch tensors
Two types of typing checking can be used:
1. Static type checking with mypy (install with pip and enabled as the default linter in VSCode)
2. Runtime type checking with typeguard (install with pip and triggered at runtime, mainly for tensor dtype and shape checking)
"""
# Basic types
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Literal,
NamedTuple,
NewType,
Optional,
Sized,
Tuple,
Type,
TypeVar,
Union,
Sequence,
)
# Tensor dtype
# for jaxtyping usage, see https://github.com/google/jaxtyping/blob/main/API.md
from jaxtyping import Bool, Complex, Float, Inexact, Int, Integer, Num, Shaped, UInt
# Config type
from omegaconf import DictConfig
# PyTorch Tensor type
from torch import Tensor
# Runtime type checking decorator
from typeguard import typechecked as typechecker
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
@@ -0,0 +1,43 @@
import numpy as np
import matplotlib.pyplot as plt
# Helper functions
def get_colors(inp, colormap="viridis", normalize=True, vmin=None, vmax=None):
colormap = plt.cm.get_cmap(colormap)
if normalize:
vmin = np.min(inp)
vmax = np.max(inp)
norm = plt.Normalize(vmin, vmax)
return colormap(norm(inp))[:, :3]
def gen_checkers(n_checkers_x, n_checkers_y, width=256, height=256):
# tex dims need to be power of two.
array = np.ones((width, height, 3), dtype='float32')
# width in texels of each checker
checker_w = width / n_checkers_x
checker_h = height / n_checkers_y
for y in range(height):
for x in range(width):
color_key = int(x / checker_w) + int(y / checker_h)
if color_key % 2 == 0:
array[x, y, :] = [1., 0.874, 0.0]
else:
array[x, y, :] = [0., 0., 0.]
return array
def gen_circle(width=256, height=256):
xx, yy = np.mgrid[:width, :height]
circle = (xx - width / 2 + 0.5) ** 2 + (yy - height / 2 + 0.5) ** 2
array = np.ones((width, height, 4), dtype='float32')
array[:, :, 0] = (circle <= width)
array[:, :, 1] = (circle <= width)
array[:, :, 2] = (circle <= width)
array[:, :, 3] = circle <= width
return array
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
import io
import base64
import numpy as np
from PIL import Image
def to_html_frame(content):
html_frame = f"""
<html>
<body>
{content}
</body>
</html>
"""
return html_frame
def to_single_row_table(caption: str, content: str):
table_html = f"""
<table border = "1">
<caption>{caption}</caption>
<tr>
<td>{content}</td>
</tr>
</table>
"""
return table_html
def to_image_embed_tag(image: np.ndarray):
# Convert np.ndarray to bytes
img = Image.fromarray(image)
raw_bytes = io.BytesIO()
img.save(raw_bytes, "PNG")
# Encode bytes to base64
image_base64 = base64.b64encode(raw_bytes.getvalue()).decode("utf-8")
image_tag = f"""
<img src="data:image/png;base64,{image_base64}" alt="Embedded Image">
"""
return image_tag
@@ -0,0 +1,537 @@
import numpy as np
from ipywidgets import embed
import pythreejs as p3s
import uuid
from .color_util import get_colors, gen_circle, gen_checkers
EMBED_URL = "https://cdn.jsdelivr.net/npm/@jupyter-widgets/html-manager@1.0.1/dist/embed-amd.js"
class PyThreeJSViewer(object):
def __init__(self, settings, render_mode="WEBSITE"):
self.render_mode = render_mode
self.__update_settings(settings)
self._light = p3s.DirectionalLight(color='white', position=[0, 0, 1], intensity=0.6)
self._light2 = p3s.AmbientLight(intensity=0.5)
self._cam = p3s.PerspectiveCamera(position=[0, 0, 1], lookAt=[0, 0, 0], fov=self.__s["fov"],
aspect=self.__s["width"] / self.__s["height"], children=[self._light])
self._orbit = p3s.OrbitControls(controlling=self._cam)
self._scene = p3s.Scene(children=[self._cam, self._light2], background=self.__s["background"]) # "#4c4c80"
self._renderer = p3s.Renderer(camera=self._cam, scene=self._scene, controls=[self._orbit],
width=self.__s["width"], height=self.__s["height"],
antialias=self.__s["antialias"])
self.__objects = {}
self.__cnt = 0
def jupyter_mode(self):
self.render_mode = "JUPYTER"
def offline(self):
self.render_mode = "OFFLINE"
def website(self):
self.render_mode = "WEBSITE"
def __get_shading(self, shading):
shad = {"flat": True, "wireframe": False, "wire_width": 0.03, "wire_color": "black",
"side": 'DoubleSide', "colormap": "viridis", "normalize": [None, None],
"bbox": False, "roughness": 0.5, "metalness": 0.25, "reflectivity": 1.0,
"line_width": 1.0, "line_color": "black",
"point_color": "red", "point_size": 0.01, "point_shape": "circle",
"text_color": "red"
}
for k in shading:
shad[k] = shading[k]
return shad
def __update_settings(self, settings={}):
sett = {"width": 600, "height": 600, "antialias": True, "scale": 1.5, "background": "#ffffff",
"fov": 30}
for k in settings:
sett[k] = settings[k]
self.__s = sett
def __add_object(self, obj, parent=None):
if not parent: # Object is added to global scene and objects dict
self.__objects[self.__cnt] = obj
self.__cnt += 1
self._scene.add(obj["mesh"])
else: # Object is added to parent object and NOT to objects dict
parent.add(obj["mesh"])
self.__update_view()
if self.render_mode == "JUPYTER":
return self.__cnt - 1
elif self.render_mode == "WEBSITE":
return self
def __add_line_geometry(self, lines, shading, obj=None):
lines = lines.astype("float32", copy=False)
mi = np.min(lines, axis=0)
ma = np.max(lines, axis=0)
geometry = p3s.LineSegmentsGeometry(positions=lines.reshape((-1, 2, 3)))
material = p3s.LineMaterial(linewidth=shading["line_width"], color=shading["line_color"])
# , vertexColors='VertexColors'),
lines = p3s.LineSegments2(geometry=geometry, material=material) # type='LinePieces')
line_obj = {"geometry": geometry, "mesh": lines, "material": material,
"max": ma, "min": mi, "type": "Lines", "wireframe": None}
if obj:
return self.__add_object(line_obj, obj), line_obj
else:
return self.__add_object(line_obj)
def __update_view(self):
if len(self.__objects) == 0:
return
ma = np.zeros((len(self.__objects), 3))
mi = np.zeros((len(self.__objects), 3))
for r, obj in enumerate(self.__objects):
ma[r] = self.__objects[obj]["max"]
mi[r] = self.__objects[obj]["min"]
ma = np.max(ma, axis=0)
mi = np.min(mi, axis=0)
diag = np.linalg.norm(ma - mi)
mean = ((ma - mi) / 2 + mi).tolist()
scale = self.__s["scale"] * (diag)
self._orbit.target = mean
self._cam.lookAt(mean)
self._cam.position = [mean[0], mean[1], mean[2] + scale]
self._light.position = [mean[0], mean[1], mean[2] + scale]
self._orbit.exec_three_obj_method('update')
self._cam.exec_three_obj_method('updateProjectionMatrix')
def __get_bbox(self, v):
m = np.min(v, axis=0)
M = np.max(v, axis=0)
# Corners of the bounding box
v_box = np.array([[m[0], m[1], m[2]], [M[0], m[1], m[2]], [M[0], M[1], m[2]], [m[0], M[1], m[2]],
[m[0], m[1], M[2]], [M[0], m[1], M[2]], [M[0], M[1], M[2]], [m[0], M[1], M[2]]])
f_box = np.array([[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [7, 3]], dtype=np.uint32)
return v_box, f_box
def __get_colors(self, v, f, c, sh):
coloring = "VertexColors"
if type(c) == np.ndarray and c.size == 3: # Single color
colors = np.ones_like(v)
colors[:, 0] = c[0]
colors[:, 1] = c[1]
colors[:, 2] = c[2]
# print("Single colors")
elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[1] == 3: # Color values for
if c.shape[0] == f.shape[0]: # faces
colors = np.hstack([c, c, c]).reshape((-1, 3))
coloring = "FaceColors"
# print("Face color values")
elif c.shape[0] == v.shape[0]: # vertices
colors = c
# print("Vertex color values")
else: # Wrong size, fallback
print("Invalid color array given! Supported are numpy arrays.", type(c))
colors = np.ones_like(v)
colors[:, 0] = 1.0
colors[:, 1] = 0.874
colors[:, 2] = 0.0
elif type(c) == np.ndarray and c.size == f.shape[0]: # Function values for faces
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
cc = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
# print(cc.shape)
colors = np.hstack([cc, cc, cc]).reshape((-1, 3))
coloring = "FaceColors"
# print("Face function values")
elif type(c) == np.ndarray and c.size == v.shape[0]: # Function values for vertices
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
# print("Vertex function values")
else:
colors = np.ones_like(v)
# colors[:, 0] = 1.0
# colors[:, 1] = 0.874
# colors[:, 2] = 0.0
colors[:, 0] = 1
colors[:, 1] = 1
colors[:, 2] = 1
# No color
if c is not None:
print("Invalid color array given! Supported are numpy arrays.", type(c))
return colors, coloring
def __get_point_colors(self, v, c, sh):
v_color = True
if c is None: # No color given, use global color
# conv = mpl.colors.ColorConverter()
colors = sh["point_color"] # np.array(conv.to_rgb(sh["point_color"]))
v_color = False
elif isinstance(c, str): # No color given, use global color
# conv = mpl.colors.ColorConverter()
colors = c # np.array(conv.to_rgb(c))
v_color = False
elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] == 3:
# Point color
colors = c.astype("float32", copy=False)
elif isinstance(c, np.ndarray) and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] != 3:
# Function values for vertices, but the colors are features
c_norm = np.linalg.norm(c, ord=2, axis=-1)
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c_norm, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
colors = colors.astype("float32", copy=False)
elif type(c) == np.ndarray and c.size == v.shape[0]: # Function color
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
colors = colors.astype("float32", copy=False)
# print("Vertex function values")
else:
print("Invalid color array given! Supported are numpy arrays.", type(c))
colors = sh["point_color"]
v_color = False
return colors, v_color
def add_mesh(self, v, f, c=None, uv=None, n=None, shading={}, texture_data=None, **kwargs):
shading.update(kwargs)
sh = self.__get_shading(shading)
mesh_obj = {}
# it is a tet
if v.shape[1] == 3 and f.shape[1] == 4:
f_tmp = np.ndarray([f.shape[0] * 4, 3], dtype=f.dtype)
for i in range(f.shape[0]):
f_tmp[i * 4 + 0] = np.array([f[i][1], f[i][0], f[i][2]])
f_tmp[i * 4 + 1] = np.array([f[i][0], f[i][1], f[i][3]])
f_tmp[i * 4 + 2] = np.array([f[i][1], f[i][2], f[i][3]])
f_tmp[i * 4 + 3] = np.array([f[i][2], f[i][0], f[i][3]])
f = f_tmp
if v.shape[1] == 2:
v = np.append(v, np.zeros([v.shape[0], 1]), 1)
# Type adjustment vertices
v = v.astype("float32", copy=False)
# Color setup
colors, coloring = self.__get_colors(v, f, c, sh)
# Type adjustment faces and colors
c = colors.astype("float32", copy=False)
# Material and geometry setup
ba_dict = {"color": p3s.BufferAttribute(c)}
if coloring == "FaceColors":
verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
for ii in range(f.shape[0]):
# print(ii*3, f[ii])
verts[ii * 3] = v[f[ii, 0]]
verts[ii * 3 + 1] = v[f[ii, 1]]
verts[ii * 3 + 2] = v[f[ii, 2]]
v = verts
else:
f = f.astype("uint32", copy=False).ravel()
ba_dict["index"] = p3s.BufferAttribute(f, normalized=False)
ba_dict["position"] = p3s.BufferAttribute(v, normalized=False)
if uv is not None:
uv = (uv - np.min(uv)) / (np.max(uv) - np.min(uv))
if texture_data is None:
texture_data = gen_checkers(20, 20)
tex = p3s.DataTexture(data=texture_data, format="RGBFormat", type="FloatType")
material = p3s.MeshStandardMaterial(map=tex, reflectivity=sh["reflectivity"], side=sh["side"],
roughness=sh["roughness"], metalness=sh["metalness"],
flatShading=sh["flat"],
polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
ba_dict["uv"] = p3s.BufferAttribute(uv.astype("float32", copy=False))
else:
material = p3s.MeshStandardMaterial(vertexColors=coloring, reflectivity=sh["reflectivity"],
side=sh["side"], roughness=sh["roughness"], metalness=sh["metalness"],
flatShading=sh["flat"],
polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
if type(n) != type(None) and coloring == "VertexColors": # TODO: properly handle normals for FaceColors as well
ba_dict["normal"] = p3s.BufferAttribute(n.astype("float32", copy=False), normalized=True)
geometry = p3s.BufferGeometry(attributes=ba_dict)
if coloring == "VertexColors" and type(n) == type(None):
geometry.exec_three_obj_method('computeVertexNormals')
elif coloring == "FaceColors" and type(n) == type(None):
geometry.exec_three_obj_method('computeFaceNormals')
# Mesh setup
mesh = p3s.Mesh(geometry=geometry, material=material)
# Wireframe setup
mesh_obj["wireframe"] = None
if sh["wireframe"]:
wf_geometry = p3s.WireframeGeometry(mesh.geometry) # WireframeGeometry
wf_material = p3s.LineBasicMaterial(color=sh["wire_color"], linewidth=sh["wire_width"])
wireframe = p3s.LineSegments(wf_geometry, wf_material)
mesh.add(wireframe)
mesh_obj["wireframe"] = wireframe
# Bounding box setup
if sh["bbox"]:
v_box, f_box = self.__get_bbox(v)
_, bbox = self.add_edges(v_box, f_box, sh, mesh)
mesh_obj["bbox"] = [bbox, v_box, f_box]
# Object setup
mesh_obj["max"] = np.max(v, axis=0)
mesh_obj["min"] = np.min(v, axis=0)
mesh_obj["geometry"] = geometry
mesh_obj["mesh"] = mesh
mesh_obj["material"] = material
mesh_obj["type"] = "Mesh"
mesh_obj["shading"] = sh
mesh_obj["coloring"] = coloring
mesh_obj["arrays"] = [v, f, c] # TODO replays with proper storage or remove if not needed
return self.__add_object(mesh_obj)
def add_lines(self, beginning, ending, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if len(beginning.shape) == 1:
if len(beginning) == 2:
beginning = np.array([[beginning[0], beginning[1], 0]])
else:
if beginning.shape[1] == 2:
beginning = np.append(
beginning, np.zeros([beginning.shape[0], 1]), 1)
if len(ending.shape) == 1:
if len(ending) == 2:
ending = np.array([[ending[0], ending[1], 0]])
else:
if ending.shape[1] == 2:
ending = np.append(
ending, np.zeros([ending.shape[0], 1]), 1)
sh = self.__get_shading(shading)
lines = np.hstack([beginning, ending])
lines = lines.reshape((-1, 3))
return self.__add_line_geometry(lines, sh, obj)
def add_edges(self, vertices, edges, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if vertices.shape[1] == 2:
vertices = np.append(
vertices, np.zeros([vertices.shape[0], 1]), 1)
sh = self.__get_shading(shading)
lines = np.zeros((edges.size, 3))
cnt = 0
for e in edges:
lines[cnt, :] = vertices[e[0]]
lines[cnt + 1, :] = vertices[e[1]]
cnt += 2
return self.__add_line_geometry(lines, sh, obj)
def add_points(self, points, c=None, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if len(points.shape) == 1:
if len(points) == 2:
points = np.array([[points[0], points[1], 0]])
else:
if points.shape[1] == 2:
points = np.append(
points, np.zeros([points.shape[0], 1]), 1)
sh = self.__get_shading(shading)
points = points.astype("float32", copy=False)
mi = np.min(points, axis=0)
ma = np.max(points, axis=0)
g_attributes = {"position": p3s.BufferAttribute(points, normalized=False)}
m_attributes = {"size": sh["point_size"]}
if sh["point_shape"] == "circle": # Plot circles
tex = p3s.DataTexture(data=gen_circle(16, 16), format="RGBAFormat", type="FloatType")
m_attributes["map"] = tex
m_attributes["alphaTest"] = 0.5
m_attributes["transparency"] = True
else: # Plot squares
pass
colors, v_colors = self.__get_point_colors(points, c, sh)
if v_colors: # Colors per point
m_attributes["vertexColors"] = 'VertexColors'
g_attributes["color"] = p3s.BufferAttribute(colors, normalized=False)
else: # Colors for all points
m_attributes["color"] = colors
material = p3s.PointsMaterial(**m_attributes)
geometry = p3s.BufferGeometry(attributes=g_attributes)
points = p3s.Points(geometry=geometry, material=material)
point_obj = {"geometry": geometry, "mesh": points, "material": material,
"max": ma, "min": mi, "type": "Points", "wireframe": None}
if obj:
return self.__add_object(point_obj, obj), point_obj
else:
return self.__add_object(point_obj)
def remove_object(self, obj_id):
if obj_id not in self.__objects:
print("Invalid object id. Valid ids are: ", list(self.__objects.keys()))
return
self._scene.remove(self.__objects[obj_id]["mesh"])
del self.__objects[obj_id]
self.__update_view()
def reset(self):
for obj_id in list(self.__objects.keys()).copy():
self._scene.remove(self.__objects[obj_id]["mesh"])
del self.__objects[obj_id]
self.__update_view()
def update_object(self, oid=0, vertices=None, colors=None, faces=None):
obj = self.__objects[oid]
if type(vertices) != type(None):
if obj["coloring"] == "FaceColors":
f = obj["arrays"][1]
verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
for ii in range(f.shape[0]):
# print(ii*3, f[ii])
verts[ii * 3] = vertices[f[ii, 0]]
verts[ii * 3 + 1] = vertices[f[ii, 1]]
verts[ii * 3 + 2] = vertices[f[ii, 2]]
v = verts
else:
v = vertices.astype("float32", copy=False)
obj["geometry"].attributes["position"].array = v
# self.wireframe.attributes["position"].array = v # Wireframe updates?
obj["geometry"].attributes["position"].needsUpdate = True
# obj["geometry"].exec_three_obj_method('computeVertexNormals')
if type(colors) != type(None):
colors, coloring = self.__get_colors(obj["arrays"][0], obj["arrays"][1], colors, obj["shading"])
colors = colors.astype("float32", copy=False)
obj["geometry"].attributes["color"].array = colors
obj["geometry"].attributes["color"].needsUpdate = True
if type(faces) != type(None):
if obj["coloring"] == "FaceColors":
print("Face updates are currently only possible in vertex color mode.")
return
f = faces.astype("uint32", copy=False).ravel()
print(obj["geometry"].attributes)
obj["geometry"].attributes["index"].array = f
# self.wireframe.attributes["position"].array = v # Wireframe updates?
obj["geometry"].attributes["index"].needsUpdate = True
# obj["geometry"].exec_three_obj_method('computeVertexNormals')
# self.mesh.geometry.verticesNeedUpdate = True
# self.mesh.geometry.elementsNeedUpdate = True
# self.update()
if self.render_mode == "WEBSITE":
return self
# def update(self):
# self.mesh.exec_three_obj_method('update')
# self.orbit.exec_three_obj_method('update')
# self.cam.exec_three_obj_method('updateProjectionMatrix')
# self.scene.exec_three_obj_method('update')
def add_text(self, text, shading={}, **kwargs):
shading.update(kwargs)
sh = self.__get_shading(shading)
tt = p3s.TextTexture(string=text, color=sh["text_color"])
sm = p3s.SpriteMaterial(map=tt)
text = p3s.Sprite(material=sm, scaleToTexture=True)
self._scene.add(text)
# def add_widget(self, widget, callback):
# self.widgets.append(widget)
# widget.observe(callback, names='value')
# def add_dropdown(self, options, default, desc, cb):
# widget = widgets.Dropdown(options=options, value=default, description=desc)
# self.__widgets.append(widget)
# widget.observe(cb, names="value")
# display(widget)
# def add_button(self, text, cb):
# button = widgets.Button(description=text)
# self.__widgets.append(button)
# button.on_click(cb)
# display(button)
def to_html(self, imports=True, html_frame=True):
# Bake positions (fixes centering bug in offline rendering)
if len(self.__objects) == 0:
return
ma = np.zeros((len(self.__objects), 3))
mi = np.zeros((len(self.__objects), 3))
for r, obj in enumerate(self.__objects):
ma[r] = self.__objects[obj]["max"]
mi[r] = self.__objects[obj]["min"]
ma = np.max(ma, axis=0)
mi = np.min(mi, axis=0)
diag = np.linalg.norm(ma - mi)
mean = (ma - mi) / 2 + mi
for r, obj in enumerate(self.__objects):
v = self.__objects[obj]["geometry"].attributes["position"].array
v -= mean
v += np.array([0.0, .9, 0.0]) #! to move the obj to the center of window
scale = self.__s["scale"] * (diag)
self._orbit.target = [0.0, 0.0, 0.0]
self._cam.lookAt([0.0, 0.0, 0.0])
# self._cam.position = [0.0, 0.0, scale]
self._cam.position = [0.0, 0.5, scale * 1.3] #! show four complete meshes in the window
self._light.position = [0.0, 0.0, scale]
state = embed.dependency_state(self._renderer)
# Somehow these entries are missing when the state is exported in python.
# Exporting from the GUI works, so we are inserting the missing entries.
for k in state:
if state[k]["model_name"] == "OrbitControlsModel":
state[k]["state"]["maxAzimuthAngle"] = "inf"
state[k]["state"]["maxDistance"] = "inf"
state[k]["state"]["maxZoom"] = "inf"
state[k]["state"]["minAzimuthAngle"] = "-inf"
tpl = embed.load_requirejs_template
if not imports:
embed.load_requirejs_template = ""
s = embed.embed_snippet(self._renderer, state=state, embed_url=EMBED_URL)
# s = embed.embed_snippet(self.__w, state=state)
embed.load_requirejs_template = tpl
if html_frame:
s = "<html>\n<body>\n" + s + "\n</body>\n</html>"
# Revert changes
for r, obj in enumerate(self.__objects):
v = self.__objects[obj]["geometry"].attributes["position"].array
v += mean
self.__update_view()
return s
def save(self, filename=""):
if filename == "":
uid = str(uuid.uuid4()) + ".html"
else:
filename = filename.replace(".html", "")
uid = filename + '.html'
with open(uid, "w") as f:
f.write(self.to_html())
print("Plot saved to file %s." % uid)
@@ -116,7 +116,7 @@ class DiffRastRenderer(nn.Module):
i0, i1, i2 = self.mesh.f[:, 0].long(), self.mesh.f[:, 1].long(), self.mesh.f[:, 2].long()
v0, v1, v2 = v[i0, :], v[i1, :], v[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0)
face_normals = torch.cross(v1 - v0, v2 - v0, dim=-1)
face_normals = safe_normalize(face_normals)
vn = torch.zeros_like(v)
@@ -0,0 +1,488 @@
{
"last_node_id": 34,
"last_link_id": 53,
"nodes": [
{
"id": 20,
"type": "InvertMask",
"pos": [
341,
400
],
"size": {
"0": 210,
"1": 26
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 26
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
28
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "InvertMask"
}
},
{
"id": 1,
"type": "LoadImage",
"pos": [
-191,
320
],
"size": {
"0": 473.3881530761719,
"1": 545.7767944335938
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
2
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [
26
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"Mario_Cat.png",
"image"
]
},
{
"id": 3,
"type": "[Comfy3D] CRM Images MVDiffusion Model",
"pos": [
600,
300
],
"size": {
"0": 506.4000244140625,
"1": 262
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "crm_mvdiffusion_sampler",
"type": "CRM_MVDIFFUSION_SAMPLER",
"link": 48
},
{
"name": "reference_image",
"type": "IMAGE",
"link": 2
},
{
"name": "reference_mask",
"type": "MASK",
"link": 28
}
],
"outputs": [
{
"name": "multiview_images",
"type": "IMAGE",
"links": [
39,
47
],
"shape": 3,
"slot_index": 0
},
{
"name": "orbit_camposes",
"type": "ORBIT_CAMPOSES",
"links": [],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "[Comfy3D] CRM Images MVDiffusion Model"
},
"widgets_values": [
"3D assets",
"uniform low no texture ugly, boring, bad anatomy, blurry, pixelated, obscure, unnatural colors, poor lighting, dull, and unclear.",
1234,
"fixed",
5.5,
50
]
},
{
"id": 32,
"type": "[Comfy3D] Load CRM MVDiffusion Model",
"pos": [
-157,
163
],
"size": {
"0": 435.45733642578125,
"1": 83.6087875366211
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "crm_mvdiffusion_sampler",
"type": "CRM_MVDIFFUSION_SAMPLER",
"links": [
48
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Load CRM MVDiffusion Model"
},
"widgets_values": [
"pixel-diffusion.pth",
"sd_v2_base_ipmv_zero_SNR.yaml"
]
},
{
"id": 33,
"type": "[Comfy3D] Craftsman Shape Diffusion Model",
"pos": [
1501,
283
],
"size": {
"0": 315,
"1": 174
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "craftsman_model",
"type": "CRAFTSMAN_MODEL",
"link": 51
},
{
"name": "multiview_images",
"type": "IMAGE",
"link": 52
}
],
"outputs": [
{
"name": "mesh",
"type": "MESH",
"links": [
53
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Craftsman Shape Diffusion Model"
},
"widgets_values": [
0,
"fixed",
5,
50,
256
]
},
{
"id": 34,
"type": "[Comfy3D] Load Craftsman Shape Diffusion Model",
"pos": [
807,
169
],
"size": {
"0": 642.2877807617188,
"1": 60.448020935058594
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "craftsman_model",
"type": "CRAFTSMAN_MODEL",
"links": [
51
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Load Craftsman Shape Diffusion Model"
},
"widgets_values": [
"image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-aligned-vae/model.ckpt"
]
},
{
"id": 25,
"type": "[Comfy3D] Preview 3DMesh",
"pos": [
2218,
287
],
"size": [
600,
500
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "mesh_file_path",
"type": "STRING",
"link": 35,
"widget": {
"name": "mesh_file_path"
}
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Preview 3DMesh"
},
"widgets_values": [
"",
null
]
},
{
"id": 27,
"type": "GetImagesFromBatchIndexed",
"pos": [
1187,
300
],
"size": {
"0": 255.99082946777344,
"1": 76.0000228881836
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 39
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
52
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "GetImagesFromBatchIndexed"
},
"widgets_values": [
"5, 0, 2, 3"
]
},
{
"id": 31,
"type": "PreviewImage",
"pos": [
1184,
515
],
"size": [
741.4351814481588,
158.14768246691654
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 47
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 24,
"type": "[Comfy3D] Save 3D Mesh",
"pos": [
1861,
284
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": 53
}
],
"outputs": [
{
"name": "save_path",
"type": "STRING",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Save 3D Mesh"
},
"widgets_values": [
"Craftsman_test\\Mesh_Cat_CRM.obj"
]
}
],
"links": [
[
2,
1,
0,
3,
1,
"IMAGE"
],
[
26,
1,
1,
20,
0,
"MASK"
],
[
28,
20,
0,
3,
2,
"MASK"
],
[
35,
24,
0,
25,
0,
"STRING"
],
[
39,
3,
0,
27,
0,
"IMAGE"
],
[
47,
3,
0,
31,
0,
"IMAGE"
],
[
48,
32,
0,
3,
0,
"CRM_MVDIFFUSION_SAMPLER"
],
[
51,
34,
0,
33,
0,
"CRAFTSMAN_MODEL"
],
[
52,
27,
0,
33,
1,
"IMAGE"
],
[
53,
33,
0,
24,
0,
"MESH"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9090909090909091,
"offset": [
-816.3016446237854,
-1.7073104334998879
]
}
},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
+117 -8
View File
@@ -53,7 +53,7 @@ from GaussianSplatting.main_3DGS_renderer import GaussianSplattingRenderer
from NeRF.Instant_NGP import InstantNGP
from TriplaneGaussian.triplane_gaussian_transformers import TGS
from TriplaneGaussian.utils.config import ExperimentConfig, load_config as load_config_tgs
from TriplaneGaussian.utils.config import ExperimentConfig as ExperimentConfigTGS, load_config as load_config_tgs
from TriplaneGaussian.data import CustomImageOrbitDataset
from TriplaneGaussian.utils.misc import todevice, get_device
from LGM.core.options import config_defaults
@@ -80,6 +80,9 @@ from Unique3D.mesh_reconstruction.recon import reconstruct_stage1
from Unique3D.mesh_reconstruction.refine import run_mesh_refine
from CharacterGen.character_inference import Inference2D_API, Inference3D_API
from CharacterGen.Stage_3D.lrm.utils.config import load_config as load_config_cg3d
import craftsman
from craftsman.systems.base import BaseSystem
from craftsman.utils.config import ExperimentConfig as ExperimentConfigCraftsman, load_config as load_config_craftsman
from .shared_utils.image_utils import (
prepare_torch_img, torch_imgs_to_pils, troch_image_dilate,
@@ -1325,7 +1328,7 @@ class Load_Triplane_Gaussian_Transformers:
device = get_device()
cfg: ExperimentConfig = load_config_tgs(self.config_path_abs)
cfg: ExperimentConfigTGS = load_config_tgs(self.config_path_abs)
ckpt_path = resume_or_download_model_from_hf(self.checkpoints_dir_abs, self.default_repo_id, model_name, self.__class__.__name__)
@@ -1362,7 +1365,7 @@ class Triplane_Gaussian_Transformers:
CATEGORY = "Comfy3D/Algorithm"
def run_TGS(self, reference_image, reference_mask, tgs_model, cam_dist):
cfg: ExperimentConfig = load_config_tgs(self.config_path_abs)
cfg: ExperimentConfigTGS = load_config_tgs(self.config_path_abs)
cfg.data.cond_camera_distance = cam_dist
cfg.data.eval_camera_distance = cam_dist
@@ -1874,8 +1877,6 @@ class TripoSR:
@torch.no_grad()
def run_TSR(self, tsr_model, reference_image, reference_mask, geometry_extract_resolution, marching_cude_threshold):
cstr(f"[{self.__class__.__name__}] Running TripoSR...").msg.print()
mesh = None
image = reference_image[0]
@@ -2274,6 +2275,8 @@ class Load_InstantMesh_Reconstruction_Model:
if is_flexicubes:
lrm_model.init_flexicubes_geometry(DEVICE, fovy=30.0)
lrm_model = lrm_model.eval()
cstr(f"[{self.__class__.__name__}] loaded model ckpt from {ckpt_path}").msg.print()
return (lrm_model, )
@@ -2624,6 +2627,8 @@ class Load_Unique3D_Custom_UNet:
configurable_unet.unet.to(DEVICE, dtype=WEIGHT_DTYPE)
pipe.unet = configurable_unet.unet
cstr(f"[{self.__class__.__name__}] loaded unet ckpt from {checkpoint_path}").msg.print()
return (pipe, )
class Unique3D_MVDiffusion_Model:
@@ -2873,6 +2878,8 @@ class Load_CharacterGen_MVDiffusion_Model:
snapshot_download(repo_id=self.default_repo_id, local_dir=self.checkpoints_dir_abs, force_download=force_download, repo_type="model", ignore_patterns=["*.json", "*.py"])
# Load pre-trained models
character_mv_gen_pipe = Inference2D_API(checkpoint_root_path=self.checkpoints_dir_abs, **OmegaConf.load(self.config_root_path_abs))
cstr(f"[{self.__class__.__name__}] loaded model ckpt from {self.checkpoints_dir_abs}").msg.print()
return (character_mv_gen_pipe,)
class CharacterGen_MVDiffusion_Model:
@@ -2972,6 +2979,8 @@ class Load_CharacterGen_Reconstruction_Model:
snapshot_download(repo_id=self.default_repo_id, local_dir=self.checkpoints_dir_abs, force_download=force_download, repo_type="model", ignore_patterns=["*.json", "*.py"])
# Load pre-trained models
character_lrm_pipe = Inference3D_API(checkpoint_root_path=self.checkpoints_dir_abs, cfg=load_config_cg3d(self.config_root_path_abs))
cstr(f"[{self.__class__.__name__}] loaded model ckpt from {self.checkpoints_dir_abs}").msg.print()
return (character_lrm_pipe,)
class CharacterGen_Reconstruction_Model:
@@ -3006,6 +3015,106 @@ class CharacterGen_Reconstruction_Model:
mesh.auto_uv()
return (mesh,)
class Load_Craftsman_Shape_Diffusion_Model:
checkpoints_dir = "Craftsman"
default_repo_id = "wyysf/CraftsMan"
default_ckpt_name = "image-to-shape-diffusion/clip-mvrgb-modln-l256-e64-ne8-nd16-nl6-aligned-vae/model.ckpt"
config_path = "Craftsman_config.yaml"
@classmethod
def INPUT_TYPES(cls):
cls.checkpoints_dir_abs = os.path.join(CKPT_ROOT_PATH, cls.checkpoints_dir)
cls.config_root_path_abs = os.path.join(CONFIG_ROOT_PATH, cls.config_path)
all_models_names = get_list_filenames(cls.checkpoints_dir_abs, SUPPORTED_CHECKPOINTS_EXTENSIONS, recursive=True)
if cls.default_ckpt_name not in all_models_names:
all_models_names += [cls.default_ckpt_name]
return {
"required": {
"model_name": (all_models_names, ),
},
}
RETURN_TYPES = (
"CRAFTSMAN_MODEL",
)
RETURN_NAMES = (
"craftsman_model",
)
FUNCTION = "load_model"
CATEGORY = "Comfy3D/Import|Export"
def load_model(self, model_name):
ckpt_path = resume_or_download_model_from_hf(self.checkpoints_dir_abs, self.default_repo_id, model_name, self.__class__.__name__)
cfg: ExperimentConfigCraftsman
cfg = load_config_craftsman(self.config_root_path_abs)
craftsman_model: BaseSystem = craftsman.find(cfg.system_type)(
cfg.system,
)
craftsman_model.load_state_dict(torch.load(ckpt_path, map_location=torch.device('cpu'))['state_dict'])
craftsman_model = craftsman_model.to(DEVICE).eval()
cstr(f"[{self.__class__.__name__}] loaded model ckpt from {self.checkpoints_dir_abs}").msg.print()
return (craftsman_model,)
class Craftsman_Shape_Diffusion_Model:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"craftsman_model": ("CRAFTSMAN_MODEL", ),
"multiview_images": ("IMAGE",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"guidance_scale": ("FLOAT", {"default": 5.0, "min": 0.0, "step": 0.01}),
"num_inference_steps": ("INT", {"default": 50, "min": 1}),
"marching_cude_grids_resolution": ("INT", {"default": 256, "min": 1, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = (
"MESH",
)
RETURN_NAMES = (
"mesh",
)
FUNCTION = "run_model"
CATEGORY = "Comfy3D/Algorithm"
@torch.no_grad()
def run_model(self, craftsman_model, multiview_images, seed, guidance_scale, num_inference_steps, marching_cude_grids_resolution):
pil_mv_image_list = torch_imgs_to_pils(multiview_images)
sample_inputs = {"mvimages": [pil_mv_image_list]} # view order: front, right, back, left
latents = craftsman_model.sample(
sample_inputs,
sample_times=1,
steps=num_inference_steps,
guidance_scale=guidance_scale,
return_intermediates=False,
seed=seed
)[0]
cstr(f"[{self.__class__.__name__}] Starting to extract mesh...").msg.print()
# decode the latents to mesh
box_v = 1.1
mesh_outputs, _ = craftsman_model.shape_model.extract_geometry(
latents,
bounds=[-box_v, -box_v, -box_v, box_v, box_v, box_v],
grids_resolution=marching_cude_grids_resolution
)
vertices, faces = torch.from_numpy(mesh_outputs[0][0]).to(DEVICE), torch.from_numpy(mesh_outputs[0][1]).to(torch.int64).to(DEVICE)
mesh = Mesh(v=vertices, f=faces, device=DEVICE)
mesh.auto_normal()
mesh.auto_uv()
return (mesh,)
ORBITPOSE_PRESET = ["Custom", "CRM(6)", "Zero123Plus(6)", "Wonder3D(6)", "Era3D(6)", "MVDream(4)", "Unique3D(4)", "CharacterGen(4)"]
@@ -3040,7 +3149,7 @@ class OrbitPoses_JK:
RETURN_NAMES = ("orbit_lists", "orbit_camposes",)
FUNCTION = "get_orbit_poses"
CATEGORY = icons.get("JK/3D")
CATEGORY = "Comfy3D/Preprocessor"
def get_orbit_poses(self, orbitpose_preset, azimuths, elevations, radius, center):
@@ -3092,7 +3201,7 @@ class OrbitLists_to_OrbitPoses_JK:
RETURN_NAMES = ("orbit_camposes",)
FUNCTION = "convert_orbit_poses"
CATEGORY = icons.get("JK/3D")
CATEGORY = "Comfy3D/Preprocessor"
def convert_orbit_poses(self, orbit_lists):
@@ -3119,7 +3228,7 @@ class OrbitPoses_to_OrbitLists_JK:
RETURN_NAMES = ("orbit_lists",)
FUNCTION = "convert_orbit_poses"
CATEGORY = icons.get("JK/3D")
CATEGORY = "Comfy3D/Preprocessor"
def convert_orbit_poses(self, orbit_camposes):
+21 -3
View File
@@ -44,10 +44,28 @@ def parse_save_filename(save_path, output_directory, supported_extensions, class
return None
# Get all files in a folder, if extension_filter is provided then only reture the files with extensions in extension_filter
def get_list_filenames(directory, extension_filter=None):
def get_list_filenames(directory, extension_filter=None, recursive=False):
"""
Recursively finds files with specified extensions in a directory and returns relative paths.
Args:
directory (str): The directory path to search.
extension_filter (list): List of file extensions (e.g., ['.txt', '.csv']).
Returns:
list: List of relative file paths matching the specified extensions.
"""
if exists(directory):
return [f for f in listdir(directory) if isfile(join(directory, f)) and (extension_filter is None or f.lower().endswith(extension_filter))]
if recursive:
result = []
for root, _, files in os.walk(directory):
for item in files:
if extension_filter is None or os.path.splitext(item)[1].lower() in extension_filter:
relative_path = os.path.relpath(os.path.join(root, item), directory)
result.append(relative_path)
return result
else:
return [f for f in listdir(directory) if isfile(join(directory, f)) and (extension_filter is None or f.lower().endswith(extension_filter))]
else:
return []