chores: rebase commits

This commit is contained in:
MrTornado24
2023-12-13 00:17:53 +08:00
commit 50ecd13a88
177 changed files with 45954 additions and 0 deletions

36
threestudio/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
__modules__ = {}
def register(name):
def decorator(cls):
__modules__[name] = cls
return cls
return decorator
def find(name):
return __modules__[name]
### 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

View File

@@ -0,0 +1 @@
from . import image, uncond

351
threestudio/data/image.py Normal file
View File

@@ -0,0 +1,351 @@
import bisect
import math
import os
from dataclasses import dataclass, field
import cv2
import numpy as np
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset, IterableDataset
import threestudio
from threestudio import register
from threestudio.data.uncond import (
RandomCameraDataModuleConfig,
RandomCameraDataset,
RandomCameraIterableDataset,
)
from threestudio.utils.base import Updateable
from threestudio.utils.config import parse_structured
from threestudio.utils.misc import get_rank
from threestudio.utils.ops import (
get_mvp_matrix,
get_projection_matrix,
get_ray_directions,
get_rays,
)
from threestudio.utils.typing import *
@dataclass
class SingleImageDataModuleConfig:
# height and width should be Union[int, List[int]]
# but OmegaConf does not support Union of containers
height: Any = 96
width: Any = 96
resolution_milestones: List[int] = field(default_factory=lambda: [])
default_elevation_deg: float = 0.0
default_azimuth_deg: float = -180.0
default_camera_distance: float = 1.2
default_fovy_deg: float = 60.0
image_path: str = ""
use_random_camera: bool = True
random_camera: dict = field(default_factory=dict)
rays_noise_scale: float = 2e-3
batch_size: int = 1
requires_depth: bool = False
requires_normal: bool = False
rays_d_normalize: bool = True
use_mixed_camera_config: bool = False
class SingleImageDataBase:
def setup(self, cfg, split):
self.split = split
self.rank = get_rank()
self.cfg: SingleImageDataModuleConfig = cfg
if self.cfg.use_random_camera:
random_camera_cfg = parse_structured(
RandomCameraDataModuleConfig, self.cfg.get("random_camera", {})
)
# FIXME:
if self.cfg.use_mixed_camera_config:
if self.rank % 2 == 0:
random_camera_cfg.camera_distance_range=[self.cfg.default_camera_distance, self.cfg.default_camera_distance]
random_camera_cfg.fovy_range=[self.cfg.default_fovy_deg, self.cfg.default_fovy_deg]
self.fixed_camera_intrinsic = True
else:
self.fixed_camera_intrinsic = False
if split == "train":
self.random_pose_generator = RandomCameraIterableDataset(
random_camera_cfg
)
else:
self.random_pose_generator = RandomCameraDataset(
random_camera_cfg, split
)
elevation_deg = torch.FloatTensor([self.cfg.default_elevation_deg])
azimuth_deg = torch.FloatTensor([self.cfg.default_azimuth_deg])
camera_distance = torch.FloatTensor([self.cfg.default_camera_distance])
elevation = elevation_deg * math.pi / 180
azimuth = azimuth_deg * math.pi / 180
camera_position: Float[Tensor, "1 3"] = torch.stack(
[
camera_distance * torch.cos(elevation) * torch.cos(azimuth),
camera_distance * torch.cos(elevation) * torch.sin(azimuth),
camera_distance * torch.sin(elevation),
],
dim=-1,
)
center: Float[Tensor, "1 3"] = torch.zeros_like(camera_position)
up: Float[Tensor, "1 3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32)[None]
light_position: Float[Tensor, "1 3"] = camera_position
lookat: Float[Tensor, "1 3"] = F.normalize(center - camera_position, dim=-1)
right: Float[Tensor, "1 3"] = F.normalize(torch.cross(lookat, up), dim=-1)
up = F.normalize(torch.cross(right, lookat), dim=-1)
self.c2w: Float[Tensor, "1 3 4"] = torch.cat(
[torch.stack([right, up, -lookat], dim=-1), camera_position[:, :, None]],
dim=-1,
)
self.c2w4x4: Float[Tensor, "B 4 4"] = torch.cat(
[self.c2w, torch.zeros_like(self.c2w[:, :1])], dim=1
)
self.c2w4x4[:, 3, 3] = 1.0
self.camera_position = camera_position
self.light_position = light_position
self.elevation_deg, self.azimuth_deg = elevation_deg, azimuth_deg
self.camera_distance = camera_distance
self.fovy = torch.deg2rad(torch.FloatTensor([self.cfg.default_fovy_deg]))
self.heights: List[int] = (
[self.cfg.height] if isinstance(self.cfg.height, int) else self.cfg.height
)
self.widths: List[int] = (
[self.cfg.width] if isinstance(self.cfg.width, int) else self.cfg.width
)
assert len(self.heights) == len(self.widths)
self.resolution_milestones: List[int]
if len(self.heights) == 1 and len(self.widths) == 1:
if len(self.cfg.resolution_milestones) > 0:
threestudio.warn(
"Ignoring resolution_milestones since height and width are not changing"
)
self.resolution_milestones = [-1]
else:
assert len(self.heights) == len(self.cfg.resolution_milestones) + 1
self.resolution_milestones = [-1] + self.cfg.resolution_milestones
self.directions_unit_focals = [
get_ray_directions(H=height, W=width, focal=1.0)
for (height, width) in zip(self.heights, self.widths)
]
self.focal_lengths = [
0.5 * height / torch.tan(0.5 * self.fovy) for height in self.heights
]
self.height: int = self.heights[0]
self.width: int = self.widths[0]
self.directions_unit_focal = self.directions_unit_focals[0]
self.focal_length = self.focal_lengths[0]
self.set_rays()
self.load_images()
self.prev_height = self.height
def set_rays(self):
# get directions by dividing directions_unit_focal by focal length
directions: Float[Tensor, "1 H W 3"] = self.directions_unit_focal[None]
directions[:, :, :, :2] = directions[:, :, :, :2] / self.focal_length
rays_o, rays_d = get_rays(
directions,
self.c2w,
keepdim=True,
noise_scale=self.cfg.rays_noise_scale,
normalize=self.cfg.rays_d_normalize,
)
proj_mtx: Float[Tensor, "4 4"] = get_projection_matrix(
self.fovy, self.width / self.height, 0.01, 100.0
) # FIXME: hard-coded near and far
mvp_mtx: Float[Tensor, "4 4"] = get_mvp_matrix(self.c2w, proj_mtx)
self.rays_o, self.rays_d = rays_o, rays_d
self.mvp_mtx = mvp_mtx
def load_images(self):
# load image
assert os.path.exists(
self.cfg.image_path
), f"Could not find image {self.cfg.image_path}!"
rgba = cv2.cvtColor(
cv2.imread(self.cfg.image_path, cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGBA
)
rgba = (
cv2.resize(
rgba, (self.width, self.height), interpolation=cv2.INTER_AREA
).astype(np.float32)
/ 255.0
)
rgb = rgba[..., :3]
self.rgb: Float[Tensor, "1 H W 3"] = (
torch.from_numpy(rgb).unsqueeze(0).contiguous().to(self.rank)
)
self.mask: Float[Tensor, "1 H W 1"] = (
torch.from_numpy(rgba[..., 3:] > 0.5).unsqueeze(0).to(self.rank)
)
print(
f"[INFO] single image dataset: load image {self.cfg.image_path} {self.rgb.shape}"
)
# load depth
if self.cfg.requires_depth:
depth_path = self.cfg.image_path.replace("_rgba.png", "_depth.png")
assert os.path.exists(depth_path)
depth = cv2.imread(depth_path, cv2.IMREAD_UNCHANGED)
depth = cv2.resize(
depth, (self.width, self.height), interpolation=cv2.INTER_AREA
)
self.depth: Float[Tensor, "1 H W 1"] = (
torch.from_numpy(depth.astype(np.float32) / 255.0)
.unsqueeze(0)
.to(self.rank)
)
print(
f"[INFO] single image dataset: load depth {depth_path} {self.depth.shape}"
)
else:
self.depth = None
# load normal
if self.cfg.requires_normal:
normal_path = self.cfg.image_path.replace("_rgba.png", "_normal.png")
assert os.path.exists(normal_path)
normal = cv2.imread(normal_path, cv2.IMREAD_UNCHANGED)
normal = cv2.resize(
normal, (self.width, self.height), interpolation=cv2.INTER_AREA
)
self.normal: Float[Tensor, "1 H W 3"] = (
torch.from_numpy(normal.astype(np.float32) / 255.0)
.unsqueeze(0)
.to(self.rank)
)
print(
f"[INFO] single image dataset: load normal {normal_path} {self.normal.shape}"
)
else:
self.normal = None
def get_all_images(self):
return self.rgb
def update_step_(self, epoch: int, global_step: int, on_load_weights: bool = False):
size_ind = bisect.bisect_right(self.resolution_milestones, global_step) - 1
self.height = self.heights[size_ind]
if self.height == self.prev_height:
return
self.prev_height = self.height
self.width = self.widths[size_ind]
self.directions_unit_focal = self.directions_unit_focals[size_ind]
self.focal_length = self.focal_lengths[size_ind]
threestudio.debug(f"Training height: {self.height}, width: {self.width}")
self.set_rays()
self.load_images()
class SingleImageIterableDataset(IterableDataset, SingleImageDataBase, Updateable):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.setup(cfg, split)
def collate(self, batch) -> Dict[str, Any]:
batch = {
"rays_o": self.rays_o,
"rays_d": self.rays_d,
"mvp_mtx": self.mvp_mtx,
"camera_positions": self.camera_position,
"light_positions": self.light_position,
"elevation": self.elevation_deg,
"azimuth": self.azimuth_deg,
"camera_distances": self.camera_distance,
"rgb": self.rgb,
"ref_depth": self.depth,
"ref_normal": self.normal,
"mask": self.mask,
"height": self.cfg.height,
"width": self.cfg.width,
"c2w": self.c2w,
"c2w4x4": self.c2w4x4,
}
if self.cfg.use_random_camera:
batch["random_camera"] = self.random_pose_generator.collate(None)
return batch
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
self.update_step_(epoch, global_step, on_load_weights)
self.random_pose_generator.update_step(epoch, global_step, on_load_weights)
def __iter__(self):
while True:
yield {}
class SingleImageDataset(Dataset, SingleImageDataBase):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.setup(cfg, split)
def __len__(self):
return len(self.random_pose_generator)
def __getitem__(self, index):
batch = self.random_pose_generator[index]
batch.update(
{
"height": self.random_pose_generator.cfg.eval_height,
"width": self.random_pose_generator.cfg.eval_width,
"mvp_mtx_ref": self.mvp_mtx[0],
"c2w_ref": self.c2w4x4,
}
)
return batch
@register("single-image-datamodule")
class SingleImageDataModule(pl.LightningDataModule):
cfg: SingleImageDataModuleConfig
def __init__(self, cfg: Optional[Union[dict, DictConfig]] = None) -> None:
super().__init__()
self.cfg = parse_structured(SingleImageDataModuleConfig, cfg)
def setup(self, stage=None) -> None:
if stage in [None, "fit"]:
self.train_dataset = SingleImageIterableDataset(self.cfg, "train")
if stage in [None, "fit", "validate"]:
self.val_dataset = SingleImageDataset(self.cfg, "val")
if stage in [None, "test", "predict"]:
self.test_dataset = SingleImageDataset(self.cfg, "test")
def prepare_data(self):
pass
def general_loader(self, dataset, batch_size, collate_fn=None) -> DataLoader:
return DataLoader(
dataset, num_workers=0, batch_size=batch_size, collate_fn=collate_fn
)
def train_dataloader(self) -> DataLoader:
return self.general_loader(
self.train_dataset,
batch_size=self.cfg.batch_size,
collate_fn=self.train_dataset.collate,
)
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)

351
threestudio/data/images.py Normal file
View File

@@ -0,0 +1,351 @@
import bisect
import math
import os
from dataclasses import dataclass, field
import cv2
import numpy as np
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset, IterableDataset
import threestudio
from threestudio import register
from threestudio.data.uncond import (
RandomCameraDataModuleConfig,
RandomCameraDataset,
RandomCameraIterableDataset,
)
from threestudio.utils.base import Updateable
from threestudio.utils.config import parse_structured
from threestudio.utils.misc import get_rank
from threestudio.utils.ops import (
get_mvp_matrix,
get_projection_matrix,
get_ray_directions,
get_rays,
)
from threestudio.utils.typing import *
@dataclass
class SingleImageDataModuleConfig:
# height and width should be Union[int, List[int]]
# but OmegaConf does not support Union of containers
height: Any = 96
width: Any = 96
resolution_milestones: List[int] = field(default_factory=lambda: [])
default_elevation_deg: float = 0.0
default_azimuth_deg: float = -180.0
default_camera_distance: float = 1.2
default_fovy_deg: float = 60.0
image_path: str = ""
use_random_camera: bool = True
random_camera: dict = field(default_factory=dict)
rays_noise_scale: float = 2e-3
batch_size: int = 1
requires_depth: bool = False
requires_normal: bool = False
rays_d_normalize: bool = True
use_mixed_camera_config: bool = False
class SingleImageDataBase:
def setup(self, cfg, split):
self.split = split
self.rank = get_rank()
self.cfg: SingleImageDataModuleConfig = cfg
if self.cfg.use_random_camera:
random_camera_cfg = parse_structured(
RandomCameraDataModuleConfig, self.cfg.get("random_camera", {})
)
# FIXME:
if self.cfg.use_mixed_camera_config:
if self.rank % 2 == 0:
random_camera_cfg.camera_distance_range=[self.cfg.default_camera_distance, self.cfg.default_camera_distance]
random_camera_cfg.fovy_range=[self.cfg.default_fovy_deg, self.cfg.default_fovy_deg]
self.fixed_camera_intrinsic = True
else:
self.fixed_camera_intrinsic = False
if split == "train":
self.random_pose_generator = RandomCameraIterableDataset(
random_camera_cfg
)
else:
self.random_pose_generator = RandomCameraDataset(
random_camera_cfg, split
)
elevation_deg = torch.FloatTensor([self.cfg.default_elevation_deg])
azimuth_deg = torch.FloatTensor([self.cfg.default_azimuth_deg])
camera_distance = torch.FloatTensor([self.cfg.default_camera_distance])
elevation = elevation_deg * math.pi / 180
azimuth = azimuth_deg * math.pi / 180
camera_position: Float[Tensor, "1 3"] = torch.stack(
[
camera_distance * torch.cos(elevation) * torch.cos(azimuth),
camera_distance * torch.cos(elevation) * torch.sin(azimuth),
camera_distance * torch.sin(elevation),
],
dim=-1,
)
center: Float[Tensor, "1 3"] = torch.zeros_like(camera_position)
up: Float[Tensor, "1 3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32)[None]
light_position: Float[Tensor, "1 3"] = camera_position
lookat: Float[Tensor, "1 3"] = F.normalize(center - camera_position, dim=-1)
right: Float[Tensor, "1 3"] = F.normalize(torch.cross(lookat, up), dim=-1)
up = F.normalize(torch.cross(right, lookat), dim=-1)
self.c2w: Float[Tensor, "1 3 4"] = torch.cat(
[torch.stack([right, up, -lookat], dim=-1), camera_position[:, :, None]],
dim=-1,
)
self.c2w4x4: Float[Tensor, "B 4 4"] = torch.cat(
[self.c2w, torch.zeros_like(self.c2w[:, :1])], dim=1
)
self.c2w4x4[:, 3, 3] = 1.0
self.camera_position = camera_position
self.light_position = light_position
self.elevation_deg, self.azimuth_deg = elevation_deg, azimuth_deg
self.camera_distance = camera_distance
self.fovy = torch.deg2rad(torch.FloatTensor([self.cfg.default_fovy_deg]))
self.heights: List[int] = (
[self.cfg.height] if isinstance(self.cfg.height, int) else self.cfg.height
)
self.widths: List[int] = (
[self.cfg.width] if isinstance(self.cfg.width, int) else self.cfg.width
)
assert len(self.heights) == len(self.widths)
self.resolution_milestones: List[int]
if len(self.heights) == 1 and len(self.widths) == 1:
if len(self.cfg.resolution_milestones) > 0:
threestudio.warn(
"Ignoring resolution_milestones since height and width are not changing"
)
self.resolution_milestones = [-1]
else:
assert len(self.heights) == len(self.cfg.resolution_milestones) + 1
self.resolution_milestones = [-1] + self.cfg.resolution_milestones
self.directions_unit_focals = [
get_ray_directions(H=height, W=width, focal=1.0)
for (height, width) in zip(self.heights, self.widths)
]
self.focal_lengths = [
0.5 * height / torch.tan(0.5 * self.fovy) for height in self.heights
]
self.height: int = self.heights[0]
self.width: int = self.widths[0]
self.directions_unit_focal = self.directions_unit_focals[0]
self.focal_length = self.focal_lengths[0]
self.set_rays()
self.load_images()
self.prev_height = self.height
def set_rays(self):
# get directions by dividing directions_unit_focal by focal length
directions: Float[Tensor, "1 H W 3"] = self.directions_unit_focal[None]
directions[:, :, :, :2] = directions[:, :, :, :2] / self.focal_length
rays_o, rays_d = get_rays(
directions,
self.c2w,
keepdim=True,
noise_scale=self.cfg.rays_noise_scale,
normalize=self.cfg.rays_d_normalize,
)
proj_mtx: Float[Tensor, "4 4"] = get_projection_matrix(
self.fovy, self.width / self.height, 0.01, 100.0
) # FIXME: hard-coded near and far
mvp_mtx: Float[Tensor, "4 4"] = get_mvp_matrix(self.c2w, proj_mtx)
self.rays_o, self.rays_d = rays_o, rays_d
self.mvp_mtx = mvp_mtx
def load_images(self):
# load image
assert os.path.exists(
self.cfg.image_path
), f"Could not find image {self.cfg.image_path}!"
rgba = cv2.cvtColor(
cv2.imread(self.cfg.image_path, cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGBA
)
rgba = (
cv2.resize(
rgba, (self.width, self.height), interpolation=cv2.INTER_AREA
).astype(np.float32)
/ 255.0
)
rgb = rgba[..., :3]
self.rgb: Float[Tensor, "1 H W 3"] = (
torch.from_numpy(rgb).unsqueeze(0).contiguous().to(self.rank)
)
self.mask: Float[Tensor, "1 H W 1"] = (
torch.from_numpy(rgba[..., 3:] > 0.5).unsqueeze(0).to(self.rank)
)
print(
f"[INFO] single image dataset: load image {self.cfg.image_path} {self.rgb.shape}"
)
# load depth
if self.cfg.requires_depth:
depth_path = self.cfg.image_path.replace("_rgba.png", "_depth.png")
assert os.path.exists(depth_path)
depth = cv2.imread(depth_path, cv2.IMREAD_UNCHANGED)
depth = cv2.resize(
depth, (self.width, self.height), interpolation=cv2.INTER_AREA
)
self.depth: Float[Tensor, "1 H W 1"] = (
torch.from_numpy(depth.astype(np.float32) / 255.0)
.unsqueeze(0)
.to(self.rank)
)
print(
f"[INFO] single image dataset: load depth {depth_path} {self.depth.shape}"
)
else:
self.depth = None
# load normal
if self.cfg.requires_normal:
normal_path = self.cfg.image_path.replace("_rgba.png", "_normal.png")
assert os.path.exists(normal_path)
normal = cv2.imread(normal_path, cv2.IMREAD_UNCHANGED)
normal = cv2.resize(
normal, (self.width, self.height), interpolation=cv2.INTER_AREA
)
self.normal: Float[Tensor, "1 H W 3"] = (
torch.from_numpy(normal.astype(np.float32) / 255.0)
.unsqueeze(0)
.to(self.rank)
)
print(
f"[INFO] single image dataset: load normal {normal_path} {self.normal.shape}"
)
else:
self.normal = None
def get_all_images(self):
return self.rgb
def update_step_(self, epoch: int, global_step: int, on_load_weights: bool = False):
size_ind = bisect.bisect_right(self.resolution_milestones, global_step) - 1
self.height = self.heights[size_ind]
if self.height == self.prev_height:
return
self.prev_height = self.height
self.width = self.widths[size_ind]
self.directions_unit_focal = self.directions_unit_focals[size_ind]
self.focal_length = self.focal_lengths[size_ind]
threestudio.debug(f"Training height: {self.height}, width: {self.width}")
self.set_rays()
self.load_images()
class SingleImageIterableDataset(IterableDataset, SingleImageDataBase, Updateable):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.setup(cfg, split)
def collate(self, batch) -> Dict[str, Any]:
batch = {
"rays_o": self.rays_o,
"rays_d": self.rays_d,
"mvp_mtx": self.mvp_mtx,
"camera_positions": self.camera_position,
"light_positions": self.light_position,
"elevation": self.elevation_deg,
"azimuth": self.azimuth_deg,
"camera_distances": self.camera_distance,
"rgb": self.rgb,
"ref_depth": self.depth,
"ref_normal": self.normal,
"mask": self.mask,
"height": self.cfg.height,
"width": self.cfg.width,
"c2w": self.c2w,
"c2w4x4": self.c2w4x4,
}
if self.cfg.use_random_camera:
batch["random_camera"] = self.random_pose_generator.collate(None)
return batch
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
self.update_step_(epoch, global_step, on_load_weights)
self.random_pose_generator.update_step(epoch, global_step, on_load_weights)
def __iter__(self):
while True:
yield {}
class SingleImageDataset(Dataset, SingleImageDataBase):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.setup(cfg, split)
def __len__(self):
return len(self.random_pose_generator)
def __getitem__(self, index):
batch = self.random_pose_generator[index]
batch.update(
{
"height": self.random_pose_generator.cfg.eval_height,
"width": self.random_pose_generator.cfg.eval_width,
"mvp_mtx_ref": self.mvp_mtx[0],
"c2w_ref": self.c2w4x4,
}
)
return batch
@register("single-image-datamodule")
class SingleImageDataModule(pl.LightningDataModule):
cfg: SingleImageDataModuleConfig
def __init__(self, cfg: Optional[Union[dict, DictConfig]] = None) -> None:
super().__init__()
self.cfg = parse_structured(SingleImageDataModuleConfig, cfg)
def setup(self, stage=None) -> None:
if stage in [None, "fit"]:
self.train_dataset = SingleImageIterableDataset(self.cfg, "train")
if stage in [None, "fit", "validate"]:
self.val_dataset = SingleImageDataset(self.cfg, "val")
if stage in [None, "test", "predict"]:
self.test_dataset = SingleImageDataset(self.cfg, "test")
def prepare_data(self):
pass
def general_loader(self, dataset, batch_size, collate_fn=None) -> DataLoader:
return DataLoader(
dataset, num_workers=0, batch_size=batch_size, collate_fn=collate_fn
)
def train_dataloader(self) -> DataLoader:
return self.general_loader(
self.train_dataset,
batch_size=self.cfg.batch_size,
collate_fn=self.train_dataset.collate,
)
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)

518
threestudio/data/uncond.py Normal file
View File

@@ -0,0 +1,518 @@
import bisect
import math
import random
from dataclasses import dataclass, field
import numpy as np
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset, IterableDataset
import threestudio
from threestudio import register
from threestudio.utils.base import Updateable
from threestudio.utils.config import parse_structured
from threestudio.utils.misc import get_device
from threestudio.utils.ops import (
get_full_projection_matrix,
get_mvp_matrix,
get_projection_matrix,
get_ray_directions,
get_rays,
)
from threestudio.utils.typing import *
@dataclass
class RandomCameraDataModuleConfig:
# height, width, and batch_size should be Union[int, List[int]]
# but OmegaConf does not support Union of containers
height: Any = 64
width: Any = 64
batch_size: Any = 1
resolution_milestones: List[int] = field(default_factory=lambda: [])
eval_height: int = 512
eval_width: int = 512
eval_batch_size: int = 1
n_val_views: int = 1
n_test_views: int = 120
elevation_range: Tuple[float, float] = (-10, 90)
azimuth_range: Tuple[float, float] = (-180, 180)
camera_distance_range: Tuple[float, float] = (1, 1.5)
fovy_range: Tuple[float, float] = (
40,
70,
) # in degrees, in vertical direction (along height)
camera_perturb: float = 0.1
center_perturb: float = 0.2
up_perturb: float = 0.02
light_position_perturb: float = 1.0
light_distance_range: Tuple[float, float] = (0.8, 1.5)
eval_elevation_deg: float = 15.0
eval_camera_distance: float = 1.5
eval_fovy_deg: float = 70.0
light_sample_strategy: str = "dreamfusion"
batch_uniform_azimuth: bool = True
progressive_until: int = 0 # progressive ranges for elevation, azimuth, r, fovy
rays_d_normalize: bool = True
class RandomCameraIterableDataset(IterableDataset, Updateable):
def __init__(self, cfg: Any) -> None:
super().__init__()
self.cfg: RandomCameraDataModuleConfig = cfg
self.heights: List[int] = (
[self.cfg.height] if isinstance(self.cfg.height, int) else self.cfg.height
)
self.widths: List[int] = (
[self.cfg.width] if isinstance(self.cfg.width, int) else self.cfg.width
)
self.batch_sizes: List[int] = (
[self.cfg.batch_size]
if isinstance(self.cfg.batch_size, int)
else self.cfg.batch_size
)
assert len(self.heights) == len(self.widths) == len(self.batch_sizes)
self.resolution_milestones: List[int]
if (
len(self.heights) == 1
and len(self.widths) == 1
and len(self.batch_sizes) == 1
):
if len(self.cfg.resolution_milestones) > 0:
threestudio.warn(
"Ignoring resolution_milestones since height and width are not changing"
)
self.resolution_milestones = [-1]
else:
assert len(self.heights) == len(self.cfg.resolution_milestones) + 1
self.resolution_milestones = [-1] + self.cfg.resolution_milestones
self.directions_unit_focals = [
get_ray_directions(H=height, W=width, focal=1.0)
for (height, width) in zip(self.heights, self.widths)
]
self.height: int = self.heights[0]
self.width: int = self.widths[0]
self.batch_size: int = self.batch_sizes[0]
self.directions_unit_focal = self.directions_unit_focals[0]
self.elevation_range = self.cfg.elevation_range
self.azimuth_range = self.cfg.azimuth_range
self.camera_distance_range = self.cfg.camera_distance_range
self.fovy_range = self.cfg.fovy_range
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
size_ind = bisect.bisect_right(self.resolution_milestones, global_step) - 1
self.height = self.heights[size_ind]
self.width = self.widths[size_ind]
self.batch_size = self.batch_sizes[size_ind]
self.directions_unit_focal = self.directions_unit_focals[size_ind]
threestudio.debug(
f"Training height: {self.height}, width: {self.width}, batch_size: {self.batch_size}"
)
# progressive view
self.progressive_view(global_step)
def __iter__(self):
while True:
yield {}
def progressive_view(self, global_step):
r = min(1.0, global_step / (self.cfg.progressive_until + 1))
self.elevation_range = [
(1 - r) * self.cfg.eval_elevation_deg + r * self.cfg.elevation_range[0],
(1 - r) * self.cfg.eval_elevation_deg + r * self.cfg.elevation_range[1],
]
self.azimuth_range = [
(1 - r) * 0.0 + r * self.cfg.azimuth_range[0],
(1 - r) * 0.0 + r * self.cfg.azimuth_range[1],
]
# self.camera_distance_range = [
# (1 - r) * self.cfg.eval_camera_distance
# + r * self.cfg.camera_distance_range[0],
# (1 - r) * self.cfg.eval_camera_distance
# + r * self.cfg.camera_distance_range[1],
# ]
# self.fovy_range = [
# (1 - r) * self.cfg.eval_fovy_deg + r * self.cfg.fovy_range[0],
# (1 - r) * self.cfg.eval_fovy_deg + r * self.cfg.fovy_range[1],
# ]
def collate(self, batch) -> Dict[str, Any]:
# sample elevation angles
elevation_deg: Float[Tensor, "B"]
elevation: Float[Tensor, "B"]
if random.random() < 0.5:
# sample elevation angles uniformly with a probability 0.5 (biased towards poles)
elevation_deg = (
torch.rand(self.batch_size)
* (self.elevation_range[1] - self.elevation_range[0])
+ self.elevation_range[0]
)
elevation = elevation_deg * math.pi / 180
else:
# otherwise sample uniformly on sphere
elevation_range_percent = [
self.elevation_range[0] / 180.0 * math.pi,
self.elevation_range[1] / 180.0 * math.pi,
]
# inverse transform sampling
elevation = torch.asin(
(
torch.rand(self.batch_size)
* (
math.sin(elevation_range_percent[1])
- math.sin(elevation_range_percent[0])
)
+ math.sin(elevation_range_percent[0])
)
)
elevation_deg = elevation / math.pi * 180.0
# sample azimuth angles from a uniform distribution bounded by azimuth_range
azimuth_deg: Float[Tensor, "B"]
if self.cfg.batch_uniform_azimuth:
# ensures sampled azimuth angles in a batch cover the whole range
azimuth_deg = (
torch.rand(self.batch_size) + torch.arange(self.batch_size)
) / self.batch_size * (
self.azimuth_range[1] - self.azimuth_range[0]
) + self.azimuth_range[
0
]
else:
# simple random sampling
azimuth_deg = (
torch.rand(self.batch_size)
* (self.azimuth_range[1] - self.azimuth_range[0])
+ self.azimuth_range[0]
)
azimuth = azimuth_deg * math.pi / 180
# sample distances from a uniform distribution bounded by distance_range
camera_distances: Float[Tensor, "B"] = (
torch.rand(self.batch_size)
* (self.camera_distance_range[1] - self.camera_distance_range[0])
+ self.camera_distance_range[0]
)
# convert spherical coordinates to cartesian coordinates
# right hand coordinate system, x back, y right, z up
# elevation in (-90, 90), azimuth from +x to +y in (-180, 180)
camera_positions: Float[Tensor, "B 3"] = torch.stack(
[
camera_distances * torch.cos(elevation) * torch.cos(azimuth),
camera_distances * torch.cos(elevation) * torch.sin(azimuth),
camera_distances * torch.sin(elevation),
],
dim=-1,
)
# default scene center at origin
center: Float[Tensor, "B 3"] = torch.zeros_like(camera_positions)
# default camera up direction as +z
up: Float[Tensor, "B 3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32)[
None, :
].repeat(self.batch_size, 1)
# sample camera perturbations from a uniform distribution [-camera_perturb, camera_perturb]
camera_perturb: Float[Tensor, "B 3"] = (
torch.rand(self.batch_size, 3) * 2 * self.cfg.camera_perturb
- self.cfg.camera_perturb
)
camera_positions = camera_positions + camera_perturb
# sample center perturbations from a normal distribution with mean 0 and std center_perturb
center_perturb: Float[Tensor, "B 3"] = (
torch.randn(self.batch_size, 3) * self.cfg.center_perturb
)
center = center + center_perturb
# sample up perturbations from a normal distribution with mean 0 and std up_perturb
up_perturb: Float[Tensor, "B 3"] = (
torch.randn(self.batch_size, 3) * self.cfg.up_perturb
)
up = up + up_perturb
# sample fovs from a uniform distribution bounded by fov_range
fovy_deg: Float[Tensor, "B"] = (
torch.rand(self.batch_size) * (self.fovy_range[1] - self.fovy_range[0])
+ self.fovy_range[0]
)
fovy = fovy_deg * math.pi / 180
# sample light distance from a uniform distribution bounded by light_distance_range
light_distances: Float[Tensor, "B"] = (
torch.rand(self.batch_size)
* (self.cfg.light_distance_range[1] - self.cfg.light_distance_range[0])
+ self.cfg.light_distance_range[0]
)
if self.cfg.light_sample_strategy == "dreamfusion":
# sample light direction from a normal distribution with mean camera_position and std light_position_perturb
light_direction: Float[Tensor, "B 3"] = F.normalize(
camera_positions
+ torch.randn(self.batch_size, 3) * self.cfg.light_position_perturb,
dim=-1,
)
# get light position by scaling light direction by light distance
light_positions: Float[Tensor, "B 3"] = (
light_direction * light_distances[:, None]
)
elif self.cfg.light_sample_strategy == "magic3d":
# sample light direction within restricted angle range (pi/3)
local_z = F.normalize(camera_positions, dim=-1)
local_x = F.normalize(
torch.stack(
[local_z[:, 1], -local_z[:, 0], torch.zeros_like(local_z[:, 0])],
dim=-1,
),
dim=-1,
)
local_y = F.normalize(torch.cross(local_z, local_x, dim=-1), dim=-1)
rot = torch.stack([local_x, local_y, local_z], dim=-1)
light_azimuth = (
torch.rand(self.batch_size) * math.pi * 2 - math.pi
) # [-pi, pi]
light_elevation = (
torch.rand(self.batch_size) * math.pi / 3 + math.pi / 6
) # [pi/6, pi/2]
light_positions_local = torch.stack(
[
light_distances
* torch.cos(light_elevation)
* torch.cos(light_azimuth),
light_distances
* torch.cos(light_elevation)
* torch.sin(light_azimuth),
light_distances * torch.sin(light_elevation),
],
dim=-1,
)
light_positions = (rot @ light_positions_local[:, :, None])[:, :, 0]
else:
raise ValueError(
f"Unknown light sample strategy: {self.cfg.light_sample_strategy}"
)
lookat: Float[Tensor, "B 3"] = F.normalize(center - camera_positions, dim=-1)
right: Float[Tensor, "B 3"] = F.normalize(torch.cross(lookat, up), dim=-1)
up = F.normalize(torch.cross(right, lookat), dim=-1)
c2w3x4: Float[Tensor, "B 3 4"] = torch.cat(
[torch.stack([right, up, -lookat], dim=-1), camera_positions[:, :, None]],
dim=-1,
)
c2w: Float[Tensor, "B 4 4"] = torch.cat(
[c2w3x4, torch.zeros_like(c2w3x4[:, :1])], dim=1
)
c2w[:, 3, 3] = 1.0
# get directions by dividing directions_unit_focal by focal length
focal_length: Float[Tensor, "B"] = 0.5 * self.height / torch.tan(0.5 * fovy)
directions: Float[Tensor, "B H W 3"] = self.directions_unit_focal[
None, :, :, :
].repeat(self.batch_size, 1, 1, 1)
directions[:, :, :, :2] = (
directions[:, :, :, :2] / focal_length[:, None, None, None]
)
# Importance note: the returned rays_d MUST be normalized!
rays_o, rays_d = get_rays(
directions, c2w, keepdim=True, normalize=self.cfg.rays_d_normalize
)
self.proj_mtx: Float[Tensor, "B 4 4"] = get_projection_matrix(
fovy, self.width / self.height, 0.1, 1000.0
) # FIXME: hard-coded near and far
mvp_mtx: Float[Tensor, "B 4 4"] = get_mvp_matrix(c2w, self.proj_mtx)
self.fovy = fovy
return {
"rays_o": rays_o,
"rays_d": rays_d,
"mvp_mtx": mvp_mtx,
"camera_positions": camera_positions,
"c2w": c2w,
"light_positions": light_positions,
"elevation": elevation_deg,
"azimuth": azimuth_deg,
"camera_distances": camera_distances,
"height": self.height,
"width": self.width,
"fovy": self.fovy,
"proj_mtx": self.proj_mtx,
}
class RandomCameraDataset(Dataset):
def __init__(self, cfg: Any, split: str) -> None:
super().__init__()
self.cfg: RandomCameraDataModuleConfig = cfg
self.split = split
if split == "val":
self.n_views = self.cfg.n_val_views
else:
self.n_views = self.cfg.n_test_views
azimuth_deg: Float[Tensor, "B"]
if self.split == "val":
# make sure the first and last view are not the same
azimuth_deg = torch.linspace(0, 360.0, self.n_views + 1)[: self.n_views]
else:
azimuth_deg = torch.linspace(0, 360.0, self.n_views)
elevation_deg: Float[Tensor, "B"] = torch.full_like(
azimuth_deg, self.cfg.eval_elevation_deg
)
camera_distances: Float[Tensor, "B"] = torch.full_like(
elevation_deg, self.cfg.eval_camera_distance
)
elevation = elevation_deg * math.pi / 180
azimuth = azimuth_deg * math.pi / 180
# convert spherical coordinates to cartesian coordinates
# right hand coordinate system, x back, y right, z up
# elevation in (-90, 90), azimuth from +x to +y in (-180, 180)
camera_positions: Float[Tensor, "B 3"] = torch.stack(
[
camera_distances * torch.cos(elevation) * torch.cos(azimuth),
camera_distances * torch.cos(elevation) * torch.sin(azimuth),
camera_distances * torch.sin(elevation),
],
dim=-1,
)
# default scene center at origin
center: Float[Tensor, "B 3"] = torch.zeros_like(camera_positions)
# default camera up direction as +z
up: Float[Tensor, "B 3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32)[
None, :
].repeat(self.cfg.eval_batch_size, 1)
fovy_deg: Float[Tensor, "B"] = torch.full_like(
elevation_deg, self.cfg.eval_fovy_deg
)
fovy = fovy_deg * math.pi / 180
light_positions: Float[Tensor, "B 3"] = camera_positions
lookat: Float[Tensor, "B 3"] = F.normalize(center - camera_positions, dim=-1)
right: Float[Tensor, "B 3"] = F.normalize(torch.cross(lookat, up), dim=-1)
up = F.normalize(torch.cross(right, lookat), dim=-1)
c2w3x4: Float[Tensor, "B 3 4"] = torch.cat(
[torch.stack([right, up, -lookat], dim=-1), camera_positions[:, :, None]],
dim=-1,
)
c2w: Float[Tensor, "B 4 4"] = torch.cat(
[c2w3x4, torch.zeros_like(c2w3x4[:, :1])], dim=1
)
c2w[:, 3, 3] = 1.0
# get directions by dividing directions_unit_focal by focal length
focal_length: Float[Tensor, "B"] = (
0.5 * self.cfg.eval_height / torch.tan(0.5 * fovy)
)
directions_unit_focal = get_ray_directions(
H=self.cfg.eval_height, W=self.cfg.eval_width, focal=1.0
)
directions: Float[Tensor, "B H W 3"] = directions_unit_focal[
None, :, :, :
].repeat(self.n_views, 1, 1, 1)
directions[:, :, :, :2] = (
directions[:, :, :, :2] / focal_length[:, None, None, None]
)
rays_o, rays_d = get_rays(
directions, c2w, keepdim=True, normalize=self.cfg.rays_d_normalize
)
self.proj_mtx: Float[Tensor, "B 4 4"] = get_projection_matrix(
fovy, self.cfg.eval_width / self.cfg.eval_height, 0.1, 1000.0
) # FIXME: hard-coded near and far
mvp_mtx: Float[Tensor, "B 4 4"] = get_mvp_matrix(c2w, self.proj_mtx)
self.rays_o, self.rays_d = rays_o, rays_d
self.mvp_mtx = mvp_mtx
self.c2w = c2w
self.camera_positions = camera_positions
self.light_positions = light_positions
self.elevation, self.azimuth = elevation, azimuth
self.elevation_deg, self.azimuth_deg = elevation_deg, azimuth_deg
self.camera_distances = camera_distances
self.fovy = fovy
def __len__(self):
return self.n_views
def __getitem__(self, index):
return {
"index": index,
"rays_o": self.rays_o[index],
"rays_d": self.rays_d[index],
"mvp_mtx": self.mvp_mtx[index],
"c2w": self.c2w[index],
"camera_positions": self.camera_positions[index],
"light_positions": self.light_positions[index],
"elevation": self.elevation_deg[index],
"azimuth": self.azimuth_deg[index],
"camera_distances": self.camera_distances[index],
"height": self.cfg.eval_height,
"width": self.cfg.eval_width,
"fovy": self.fovy[index],
"proj_mtx": self.proj_mtx[index],
}
def collate(self, batch):
batch = torch.utils.data.default_collate(batch)
batch.update({"height": self.cfg.eval_height, "width": self.cfg.eval_width})
return batch
@register("random-camera-datamodule")
class RandomCameraDataModule(pl.LightningDataModule):
cfg: RandomCameraDataModuleConfig
def __init__(self, cfg: Optional[Union[dict, DictConfig]] = None) -> None:
super().__init__()
self.cfg = parse_structured(RandomCameraDataModuleConfig, cfg)
def setup(self, stage=None) -> None:
if stage in [None, "fit"]:
self.train_dataset = RandomCameraIterableDataset(self.cfg)
if stage in [None, "fit", "validate"]:
self.val_dataset = RandomCameraDataset(self.cfg, "val")
if stage in [None, "test", "predict"]:
self.test_dataset = RandomCameraDataset(self.cfg, "test")
def prepare_data(self):
pass
def general_loader(self, dataset, batch_size, collate_fn=None) -> DataLoader:
return DataLoader(
dataset,
# very important to disable multi-processing if you want to change self attributes at runtime!
# (for example setting self.width and self.height in update_step)
num_workers=0, # type: ignore
batch_size=batch_size,
collate_fn=collate_fn,
)
def train_dataloader(self) -> DataLoader:
return self.general_loader(
self.train_dataset, batch_size=None, collate_fn=self.train_dataset.collate
)
def val_dataloader(self) -> DataLoader:
return self.general_loader(
self.val_dataset, batch_size=1, collate_fn=self.val_dataset.collate
)
# return self.general_loader(self.train_dataset, batch_size=None, collate_fn=self.train_dataset.collate)
def test_dataloader(self) -> DataLoader:
return self.general_loader(
self.test_dataset, batch_size=1, collate_fn=self.test_dataset.collate
)
def predict_dataloader(self) -> DataLoader:
return self.general_loader(
self.test_dataset, batch_size=1, collate_fn=self.test_dataset.collate
)

View File

@@ -0,0 +1,9 @@
from . import (
background,
exporters,
geometry,
guidance,
materials,
prompt_processors,
renderers,
)

View File

@@ -0,0 +1,6 @@
from . import (
base,
neural_environment_map_background,
solid_color_background,
textured_background,
)

View File

@@ -0,0 +1,24 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.utils.base import BaseModule
from threestudio.utils.typing import *
class BaseBackground(BaseModule):
@dataclass
class Config(BaseModule.Config):
pass
cfg: Config
def configure(self):
pass
def forward(self, dirs: Float[Tensor, "B H W 3"]) -> Float[Tensor, "B H W Nc"]:
raise NotImplementedError

View File

@@ -0,0 +1,71 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
@threestudio.register("neural-environment-map-background")
class NeuralEnvironmentMapBackground(BaseBackground):
@dataclass
class Config(BaseBackground.Config):
n_output_dims: int = 3
color_activation: str = "sigmoid"
dir_encoding_config: dict = field(
default_factory=lambda: {"otype": "SphericalHarmonics", "degree": 3}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "VanillaMLP",
"activation": "ReLU",
"n_neurons": 16,
"n_hidden_layers": 2,
}
)
random_aug: bool = False
random_aug_prob: float = 0.5
eval_color: Optional[Tuple[float, float, float]] = None
# multi-view diffusion
share_aug_bg: bool = False
cfg: Config
def configure(self) -> None:
self.encoding = get_encoding(3, self.cfg.dir_encoding_config)
self.network = get_mlp(
self.encoding.n_output_dims,
self.cfg.n_output_dims,
self.cfg.mlp_network_config,
)
def forward(self, dirs: Float[Tensor, "B H W 3"]) -> Float[Tensor, "B H W Nc"]:
if not self.training and self.cfg.eval_color is not None:
return torch.ones(*dirs.shape[:-1], self.cfg.n_output_dims).to(
dirs
) * torch.as_tensor(self.cfg.eval_color).to(dirs)
# viewdirs must be normalized before passing to this function
dirs = (dirs + 1.0) / 2.0 # (-1, 1) => (0, 1)
dirs_embd = self.encoding(dirs.view(-1, 3))
color = self.network(dirs_embd).view(*dirs.shape[:-1], self.cfg.n_output_dims)
color = get_activation(self.cfg.color_activation)(color)
if (
self.training
and self.cfg.random_aug
and random.random() < self.cfg.random_aug_prob
):
# use random background color with probability random_aug_prob
n_color = 1 if self.cfg.share_aug_bg else dirs.shape[0]
color = color * 0 + ( # prevent checking for unused parameters in DDP
torch.rand(n_color, 1, 1, self.cfg.n_output_dims)
.to(dirs)
.expand(*dirs.shape[:-1], -1)
)
return color

View File

@@ -0,0 +1,51 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.utils.typing import *
@threestudio.register("solid-color-background")
class SolidColorBackground(BaseBackground):
@dataclass
class Config(BaseBackground.Config):
n_output_dims: int = 3
color: Tuple = (1.0, 1.0, 1.0)
learned: bool = False
random_aug: bool = False
random_aug_prob: float = 0.5
cfg: Config
def configure(self) -> None:
self.env_color: Float[Tensor, "Nc"]
if self.cfg.learned:
self.env_color = nn.Parameter(
torch.as_tensor(self.cfg.color, dtype=torch.float32)
)
else:
self.register_buffer(
"env_color", torch.as_tensor(self.cfg.color, dtype=torch.float32)
)
def forward(self, dirs: Float[Tensor, "B H W 3"]) -> Float[Tensor, "B H W Nc"]:
color = torch.ones(*dirs.shape[:-1], self.cfg.n_output_dims).to(
dirs
) * self.env_color.to(dirs)
if (
self.training
and self.cfg.random_aug
and random.random() < self.cfg.random_aug_prob
):
# use random background color with probability random_aug_prob
color = color * 0 + ( # prevent checking for unused parameters in DDP
torch.rand(dirs.shape[0], 1, 1, self.cfg.n_output_dims)
.to(dirs)
.expand(*dirs.shape[:-1], -1)
)
return color

View File

@@ -0,0 +1,54 @@
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
@threestudio.register("textured-background")
class TexturedBackground(BaseBackground):
@dataclass
class Config(BaseBackground.Config):
n_output_dims: int = 3
height: int = 64
width: int = 64
color_activation: str = "sigmoid"
cfg: Config
def configure(self) -> None:
self.texture = nn.Parameter(
torch.randn((1, self.cfg.n_output_dims, self.cfg.height, self.cfg.width))
)
def spherical_xyz_to_uv(self, dirs: Float[Tensor, "*B 3"]) -> Float[Tensor, "*B 2"]:
x, y, z = dirs[..., 0], dirs[..., 1], dirs[..., 2]
xy = (x**2 + y**2) ** 0.5
u = torch.atan2(xy, z) / torch.pi
v = torch.atan2(y, x) / (torch.pi * 2) + 0.5
uv = torch.stack([u, v], -1)
return uv
def forward(self, dirs: Float[Tensor, "*B 3"]) -> Float[Tensor, "*B Nc"]:
dirs_shape = dirs.shape[:-1]
uv = self.spherical_xyz_to_uv(dirs.reshape(-1, dirs.shape[-1]))
uv = 2 * uv - 1 # rescale to [-1, 1] for grid_sample
uv = uv.reshape(1, -1, 1, 2)
color = (
F.grid_sample(
self.texture,
uv,
mode="bilinear",
padding_mode="reflection",
align_corners=False,
)
.reshape(self.cfg.n_output_dims, -1)
.T.reshape(*dirs_shape, self.cfg.n_output_dims)
)
color = get_activation(self.cfg.color_activation)(color)
return color

View File

@@ -0,0 +1,118 @@
from typing import Callable, List, Optional, Tuple
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
import torch
from nerfacc.data_specs import RayIntervals
from nerfacc.estimators.base import AbstractEstimator
from nerfacc.pdf import importance_sampling, searchsorted
from nerfacc.volrend import render_transmittance_from_density
from torch import Tensor
class ImportanceEstimator(AbstractEstimator):
def __init__(
self,
) -> None:
super().__init__()
@torch.no_grad()
def sampling(
self,
prop_sigma_fns: List[Callable],
prop_samples: List[int],
num_samples: int,
# rendering options
n_rays: int,
near_plane: float,
far_plane: float,
sampling_type: Literal["uniform", "lindisp"] = "uniform",
# training options
stratified: bool = False,
requires_grad: bool = False,
) -> Tuple[Tensor, Tensor]:
"""Sampling with CDFs from proposal networks.
Args:
prop_sigma_fns: Proposal network evaluate functions. It should be a list
of functions that take in samples {t_starts (n_rays, n_samples),
t_ends (n_rays, n_samples)} and returns the post-activation densities
(n_rays, n_samples).
prop_samples: Number of samples to draw from each proposal network. Should
be the same length as `prop_sigma_fns`.
num_samples: Number of samples to draw in the end.
n_rays: Number of rays.
near_plane: Near plane.
far_plane: Far plane.
sampling_type: Sampling type. Either "uniform" or "lindisp". Default to
"lindisp".
stratified: Whether to use stratified sampling. Default to `False`.
Returns:
A tuple of {Tensor, Tensor}:
- **t_starts**: The starts of the samples. Shape (n_rays, num_samples).
- **t_ends**: The ends of the samples. Shape (n_rays, num_samples).
"""
assert len(prop_sigma_fns) == len(prop_samples), (
"The number of proposal networks and the number of samples "
"should be the same."
)
cdfs = torch.cat(
[
torch.zeros((n_rays, 1), device=self.device),
torch.ones((n_rays, 1), device=self.device),
],
dim=-1,
)
intervals = RayIntervals(vals=cdfs)
for level_fn, level_samples in zip(prop_sigma_fns, prop_samples):
intervals, _ = importance_sampling(
intervals, cdfs, level_samples, stratified
)
t_vals = _transform_stot(
sampling_type, intervals.vals, near_plane, far_plane
)
t_starts = t_vals[..., :-1]
t_ends = t_vals[..., 1:]
with torch.set_grad_enabled(requires_grad):
sigmas = level_fn(t_starts, t_ends)
assert sigmas.shape == t_starts.shape
trans, _ = render_transmittance_from_density(t_starts, t_ends, sigmas)
cdfs = 1.0 - torch.cat([trans, torch.zeros_like(trans[:, :1])], dim=-1)
intervals, _ = importance_sampling(intervals, cdfs, num_samples, stratified)
t_vals_fine = _transform_stot(
sampling_type, intervals.vals, near_plane, far_plane
)
t_vals = torch.cat([t_vals, t_vals_fine], dim=-1)
t_vals, _ = torch.sort(t_vals, dim=-1)
t_starts_ = t_vals[..., :-1]
t_ends_ = t_vals[..., 1:]
return t_starts_, t_ends_
def _transform_stot(
transform_type: Literal["uniform", "lindisp"],
s_vals: torch.Tensor,
t_min: torch.Tensor,
t_max: torch.Tensor,
) -> torch.Tensor:
if transform_type == "uniform":
_contract_fn, _icontract_fn = lambda x: x, lambda x: x
elif transform_type == "lindisp":
_contract_fn, _icontract_fn = lambda x: 1 / x, lambda x: 1 / x
else:
raise ValueError(f"Unknown transform_type: {transform_type}")
s_min, s_max = _contract_fn(t_min), _contract_fn(t_max)
icontract_fn = lambda s: _icontract_fn(s * s_max + (1 - s) * s_min)
return icontract_fn(s_vals)

View File

@@ -0,0 +1 @@
from . import base, mesh_exporter

View File

@@ -0,0 +1,59 @@
from dataclasses import dataclass
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.utils.base import BaseObject
from threestudio.utils.typing import *
@dataclass
class ExporterOutput:
save_name: str
save_type: str
params: Dict[str, Any]
class Exporter(BaseObject):
@dataclass
class Config(BaseObject.Config):
save_video: bool = False
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
@dataclass
class SubModules:
geometry: BaseImplicitGeometry
material: BaseMaterial
background: BaseBackground
self.sub_modules = SubModules(geometry, material, background)
@property
def geometry(self) -> BaseImplicitGeometry:
return self.sub_modules.geometry
@property
def material(self) -> BaseMaterial:
return self.sub_modules.material
@property
def background(self) -> BaseBackground:
return self.sub_modules.background
def __call__(self, *args, **kwargs) -> List[ExporterOutput]:
raise NotImplementedError
@threestudio.register("dummy-exporter")
class DummyExporter(Exporter):
def __call__(self, *args, **kwargs) -> List[ExporterOutput]:
# DummyExporter does not export anything
return []

View File

@@ -0,0 +1,175 @@
from dataclasses import dataclass, field
import cv2
import numpy as np
import torch
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.exporters.base import Exporter, ExporterOutput
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.mesh import Mesh
from threestudio.utils.rasterize import NVDiffRasterizerContext
from threestudio.utils.typing import *
@threestudio.register("mesh-exporter")
class MeshExporter(Exporter):
@dataclass
class Config(Exporter.Config):
fmt: str = "obj-mtl" # in ['obj-mtl', 'obj'], TODO: fbx
save_name: str = "model"
save_normal: bool = False
save_uv: bool = True
save_texture: bool = True
texture_size: int = 1024
texture_format: str = "jpg"
xatlas_chart_options: dict = field(default_factory=dict)
xatlas_pack_options: dict = field(default_factory=dict)
context_type: str = "gl"
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
super().configure(geometry, material, background)
self.ctx = NVDiffRasterizerContext(self.cfg.context_type, self.device)
def __call__(self) -> List[ExporterOutput]:
mesh: Mesh = self.geometry.isosurface()
if self.cfg.fmt == "obj-mtl":
return self.export_obj_with_mtl(mesh)
elif self.cfg.fmt == "obj":
return self.export_obj(mesh)
else:
raise ValueError(f"Unsupported mesh export format: {self.cfg.fmt}")
def export_obj_with_mtl(self, mesh: Mesh) -> List[ExporterOutput]:
params = {
"mesh": mesh,
"save_mat": True,
"save_normal": self.cfg.save_normal,
"save_uv": self.cfg.save_uv,
"save_vertex_color": False,
"map_Kd": None, # Base Color
"map_Ks": None, # Specular
"map_Bump": None, # Normal
# ref: https://en.wikipedia.org/wiki/Wavefront_.obj_file#Physically-based_Rendering
"map_Pm": None, # Metallic
"map_Pr": None, # Roughness
"map_format": self.cfg.texture_format,
}
if self.cfg.save_uv:
mesh.unwrap_uv(self.cfg.xatlas_chart_options, self.cfg.xatlas_pack_options)
if self.cfg.save_texture:
threestudio.info("Exporting textures ...")
assert self.cfg.save_uv, "save_uv must be True when save_texture is True"
# clip space transform
uv_clip = mesh.v_tex * 2.0 - 1.0
# pad to four component coordinate
uv_clip4 = torch.cat(
(
uv_clip,
torch.zeros_like(uv_clip[..., 0:1]),
torch.ones_like(uv_clip[..., 0:1]),
),
dim=-1,
)
# rasterize
rast, _ = self.ctx.rasterize_one(
uv_clip4, mesh.t_tex_idx, (self.cfg.texture_size, self.cfg.texture_size)
)
hole_mask = ~(rast[:, :, 3] > 0)
def uv_padding(image):
uv_padding_size = self.cfg.xatlas_pack_options.get("padding", 2)
inpaint_image = (
cv2.inpaint(
(image.detach().cpu().numpy() * 255).astype(np.uint8),
(hole_mask.detach().cpu().numpy() * 255).astype(np.uint8),
uv_padding_size,
cv2.INPAINT_TELEA,
)
/ 255.0
)
return torch.from_numpy(inpaint_image).to(image)
# Interpolate world space position
gb_pos, _ = self.ctx.interpolate_one(
mesh.v_pos, rast[None, ...], mesh.t_pos_idx
)
gb_pos = gb_pos[0]
# Sample out textures from MLP
geo_out = self.geometry.export(points=gb_pos)
mat_out = self.material.export(points=gb_pos, **geo_out)
threestudio.info(
"Perform UV padding on texture maps to avoid seams, may take a while ..."
)
if "albedo" in mat_out:
params["map_Kd"] = uv_padding(mat_out["albedo"])
else:
threestudio.warn(
"save_texture is True but no albedo texture found, using default white texture"
)
if "metallic" in mat_out:
params["map_Pm"] = uv_padding(mat_out["metallic"])
if "roughness" in mat_out:
params["map_Pr"] = uv_padding(mat_out["roughness"])
if "bump" in mat_out:
params["map_Bump"] = uv_padding(mat_out["bump"])
# TODO: map_Ks
return [
ExporterOutput(
save_name=f"{self.cfg.save_name}.obj", save_type="obj", params=params
)
]
def export_obj(self, mesh: Mesh) -> List[ExporterOutput]:
params = {
"mesh": mesh,
"save_mat": False,
"save_normal": self.cfg.save_normal,
"save_uv": self.cfg.save_uv,
"save_vertex_color": False,
"map_Kd": None, # Base Color
"map_Ks": None, # Specular
"map_Bump": None, # Normal
# ref: https://en.wikipedia.org/wiki/Wavefront_.obj_file#Physically-based_Rendering
"map_Pm": None, # Metallic
"map_Pr": None, # Roughness
"map_format": self.cfg.texture_format,
}
if self.cfg.save_uv:
mesh.unwrap_uv(self.cfg.xatlas_chart_options, self.cfg.xatlas_pack_options)
if self.cfg.save_texture:
threestudio.info("Exporting textures ...")
geo_out = self.geometry.export(points=mesh.v_pos)
mat_out = self.material.export(points=mesh.v_pos, **geo_out)
if "albedo" in mat_out:
mesh.set_vertex_color(mat_out["albedo"])
params["save_vertex_color"] = True
else:
threestudio.warn(
"save_texture is True but no albedo texture found, not saving vertex color"
)
return [
ExporterOutput(
save_name=f"{self.cfg.save_name}.obj", save_type="obj", params=params
)
]

View File

@@ -0,0 +1,8 @@
from . import (
base,
custom_mesh,
implicit_sdf,
implicit_volume,
tetrahedra_sdf_grid,
volume_grid,
)

View File

@@ -0,0 +1,209 @@
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.isosurface import (
IsosurfaceHelper,
MarchingCubeCPUHelper,
MarchingTetrahedraHelper,
)
from threestudio.models.mesh import Mesh
from threestudio.utils.base import BaseModule
from threestudio.utils.ops import chunk_batch, scale_tensor
from threestudio.utils.typing import *
def contract_to_unisphere(
x: Float[Tensor, "... 3"], bbox: Float[Tensor, "2 3"], unbounded: bool = False
) -> Float[Tensor, "... 3"]:
if unbounded:
x = scale_tensor(x, bbox, (0, 1))
x = x * 2 - 1 # aabb is at [-1, 1]
mag = x.norm(dim=-1, keepdim=True)
mask = mag.squeeze(-1) > 1
x[mask] = (2 - 1 / mag[mask]) * (x[mask] / mag[mask])
x = x / 4 + 0.5 # [-inf, inf] is at [0, 1]
else:
x = scale_tensor(x, bbox, (0, 1))
return x
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()
threestudio.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:
threestudio.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])
threestudio.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,
),
)

View File

@@ -0,0 +1,178 @@
import os
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.geometry.base import (
BaseExplicitGeometry,
BaseGeometry,
contract_to_unisphere,
)
from threestudio.models.mesh import Mesh
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import scale_tensor
from threestudio.utils.typing import *
@threestudio.register("custom-mesh")
class CustomMesh(BaseExplicitGeometry):
@dataclass
class Config(BaseExplicitGeometry.Config):
n_input_dims: int = 3
n_feature_dims: int = 3
pos_encoding_config: dict = field(
default_factory=lambda: {
"otype": "HashGrid",
"n_levels": 16,
"n_features_per_level": 2,
"log2_hashmap_size": 19,
"base_resolution": 16,
"per_level_scale": 1.447269237440378,
}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "VanillaMLP",
"activation": "ReLU",
"output_activation": "none",
"n_neurons": 64,
"n_hidden_layers": 1,
}
)
shape_init: str = ""
shape_init_params: Optional[Any] = None
shape_init_mesh_up: str = "+z"
shape_init_mesh_front: str = "+x"
cfg: Config
def configure(self) -> None:
super().configure()
self.encoding = get_encoding(
self.cfg.n_input_dims, self.cfg.pos_encoding_config
)
self.feature_network = get_mlp(
self.encoding.n_output_dims,
self.cfg.n_feature_dims,
self.cfg.mlp_network_config,
)
# Initialize custom mesh
if self.cfg.shape_init.startswith("mesh:"):
assert isinstance(self.cfg.shape_init_params, float)
mesh_path = self.cfg.shape_init[5:]
if not os.path.exists(mesh_path):
raise ValueError(f"Mesh file {mesh_path} does not exist.")
import trimesh
scene = trimesh.load(mesh_path)
if isinstance(scene, trimesh.Trimesh):
mesh = scene
elif isinstance(scene, trimesh.scene.Scene):
mesh = trimesh.Trimesh()
for obj in scene.geometry.values():
mesh = trimesh.util.concatenate([mesh, obj])
else:
raise ValueError(f"Unknown mesh type at {mesh_path}.")
# move to center
centroid = mesh.vertices.mean(0)
mesh.vertices = mesh.vertices - centroid
# align to up-z and front-x
dirs = ["+x", "+y", "+z", "-x", "-y", "-z"]
dir2vec = {
"+x": np.array([1, 0, 0]),
"+y": np.array([0, 1, 0]),
"+z": np.array([0, 0, 1]),
"-x": np.array([-1, 0, 0]),
"-y": np.array([0, -1, 0]),
"-z": np.array([0, 0, -1]),
}
if (
self.cfg.shape_init_mesh_up not in dirs
or self.cfg.shape_init_mesh_front not in dirs
):
raise ValueError(
f"shape_init_mesh_up and shape_init_mesh_front must be one of {dirs}."
)
if self.cfg.shape_init_mesh_up[1] == self.cfg.shape_init_mesh_front[1]:
raise ValueError(
"shape_init_mesh_up and shape_init_mesh_front must be orthogonal."
)
z_, x_ = (
dir2vec[self.cfg.shape_init_mesh_up],
dir2vec[self.cfg.shape_init_mesh_front],
)
y_ = np.cross(z_, x_)
std2mesh = np.stack([x_, y_, z_], axis=0).T
mesh2std = np.linalg.inv(std2mesh)
# scaling
scale = np.abs(mesh.vertices).max()
mesh.vertices = mesh.vertices / scale * self.cfg.shape_init_params
mesh.vertices = np.dot(mesh2std, mesh.vertices.T).T
v_pos = torch.tensor(mesh.vertices, dtype=torch.float32).to(self.device)
t_pos_idx = torch.tensor(mesh.faces, dtype=torch.int64).to(self.device)
self.mesh = Mesh(v_pos=v_pos, t_pos_idx=t_pos_idx)
self.register_buffer(
"v_buffer",
v_pos,
)
self.register_buffer(
"t_buffer",
t_pos_idx,
)
else:
raise ValueError(
f"Unknown shape initialization type: {self.cfg.shape_init}"
)
print(self.mesh.v_pos.device)
def isosurface(self) -> Mesh:
if hasattr(self, "mesh"):
return self.mesh
elif hasattr(self, "v_buffer"):
self.mesh = Mesh(v_pos=self.v_buffer, t_pos_idx=self.t_buffer)
return self.mesh
else:
raise ValueError(f"custom mesh is not initialized")
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
assert (
output_normal == False
), f"Normal output is not supported for {self.__class__.__name__}"
points_unscaled = points # points in the original scale
points = contract_to_unisphere(points, self.bbox) # points normalized to (0, 1)
enc = self.encoding(points.view(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
return {"features": features}
def export(self, points: Float[Tensor, "*N Di"], **kwargs) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if self.cfg.n_feature_dims == 0:
return out
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox)
enc = self.encoding(points.reshape(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
out.update(
{
"features": features,
}
)
return out

View File

@@ -0,0 +1,413 @@
import os
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.geometry.base import BaseImplicitGeometry, contract_to_unisphere
from threestudio.models.mesh import Mesh
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.misc import broadcast, get_rank
from threestudio.utils.typing import *
@threestudio.register("implicit-sdf")
class ImplicitSDF(BaseImplicitGeometry):
@dataclass
class Config(BaseImplicitGeometry.Config):
n_input_dims: int = 3
n_feature_dims: int = 3
pos_encoding_config: dict = field(
default_factory=lambda: {
"otype": "HashGrid",
"n_levels": 16,
"n_features_per_level": 2,
"log2_hashmap_size": 19,
"base_resolution": 16,
"per_level_scale": 1.447269237440378,
}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "VanillaMLP",
"activation": "ReLU",
"output_activation": "none",
"n_neurons": 64,
"n_hidden_layers": 1,
}
)
normal_type: Optional[
str
] = "finite_difference" # in ['pred', 'finite_difference', 'finite_difference_laplacian']
finite_difference_normal_eps: Union[
float, str
] = 0.01 # in [float, "progressive"]
shape_init: Optional[str] = None
shape_init_params: Optional[Any] = None
shape_init_mesh_up: str = "+z"
shape_init_mesh_front: str = "+x"
force_shape_init: bool = False
sdf_bias: Union[float, str] = 0.0
sdf_bias_params: Optional[Any] = None
# no need to removal outlier for SDF
isosurface_remove_outliers: bool = False
cfg: Config
def configure(self) -> None:
super().configure()
self.encoding = get_encoding(
self.cfg.n_input_dims, self.cfg.pos_encoding_config
)
self.sdf_network = get_mlp(
self.encoding.n_output_dims, 1, self.cfg.mlp_network_config
)
if self.cfg.n_feature_dims > 0:
self.feature_network = get_mlp(
self.encoding.n_output_dims,
self.cfg.n_feature_dims,
self.cfg.mlp_network_config,
)
if self.cfg.normal_type == "pred":
self.normal_network = get_mlp(
self.encoding.n_output_dims, 3, self.cfg.mlp_network_config
)
if self.cfg.isosurface_deformable_grid:
assert (
self.cfg.isosurface_method == "mt"
), "isosurface_deformable_grid only works with mt"
self.deformation_network = get_mlp(
self.encoding.n_output_dims, 3, self.cfg.mlp_network_config
)
self.finite_difference_normal_eps: Optional[float] = None
def initialize_shape(self) -> None:
if self.cfg.shape_init is None and not self.cfg.force_shape_init:
return
# do not initialize shape if weights are provided
if self.cfg.weights is not None and not self.cfg.force_shape_init:
return
if self.cfg.sdf_bias != 0.0:
threestudio.warn(
"shape_init and sdf_bias are both specified, which may lead to unexpected results."
)
get_gt_sdf: Callable[[Float[Tensor, "N 3"]], Float[Tensor, "N 1"]]
assert isinstance(self.cfg.shape_init, str)
if self.cfg.shape_init == "ellipsoid":
assert (
isinstance(self.cfg.shape_init_params, Sized)
and len(self.cfg.shape_init_params) == 3
)
size = torch.as_tensor(self.cfg.shape_init_params).to(self.device)
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
return ((points_rand / size) ** 2).sum(
dim=-1, keepdim=True
).sqrt() - 1.0 # pseudo signed distance of an ellipsoid
get_gt_sdf = func
elif self.cfg.shape_init == "sphere":
assert isinstance(self.cfg.shape_init_params, float)
radius = self.cfg.shape_init_params
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
return (points_rand**2).sum(dim=-1, keepdim=True).sqrt() - radius
get_gt_sdf = func
elif self.cfg.shape_init.startswith("mesh:"):
assert isinstance(self.cfg.shape_init_params, float)
mesh_path = self.cfg.shape_init[5:]
if not os.path.exists(mesh_path):
raise ValueError(f"Mesh file {mesh_path} does not exist.")
import trimesh
scene = trimesh.load(mesh_path)
if isinstance(scene, trimesh.Trimesh):
mesh = scene
elif isinstance(scene, trimesh.scene.Scene):
mesh = trimesh.Trimesh()
for obj in scene.geometry.values():
mesh = trimesh.util.concatenate([mesh, obj])
else:
raise ValueError(f"Unknown mesh type at {mesh_path}.")
# move to center
centroid = mesh.vertices.mean(0)
mesh.vertices = mesh.vertices - centroid
# align to up-z and front-x
dirs = ["+x", "+y", "+z", "-x", "-y", "-z"]
dir2vec = {
"+x": np.array([1, 0, 0]),
"+y": np.array([0, 1, 0]),
"+z": np.array([0, 0, 1]),
"-x": np.array([-1, 0, 0]),
"-y": np.array([0, -1, 0]),
"-z": np.array([0, 0, -1]),
}
if (
self.cfg.shape_init_mesh_up not in dirs
or self.cfg.shape_init_mesh_front not in dirs
):
raise ValueError(
f"shape_init_mesh_up and shape_init_mesh_front must be one of {dirs}."
)
if self.cfg.shape_init_mesh_up[1] == self.cfg.shape_init_mesh_front[1]:
raise ValueError(
"shape_init_mesh_up and shape_init_mesh_front must be orthogonal."
)
z_, x_ = (
dir2vec[self.cfg.shape_init_mesh_up],
dir2vec[self.cfg.shape_init_mesh_front],
)
y_ = np.cross(z_, x_)
std2mesh = np.stack([x_, y_, z_], axis=0).T
mesh2std = np.linalg.inv(std2mesh)
# scaling
scale = np.abs(mesh.vertices).max()
mesh.vertices = mesh.vertices / scale * self.cfg.shape_init_params
mesh.vertices = np.dot(mesh2std, mesh.vertices.T).T
from pysdf import SDF
sdf = SDF(mesh.vertices, mesh.faces)
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
# add a negative signed here
# as in pysdf the inside of the shape has positive signed distance
return torch.from_numpy(-sdf(points_rand.cpu().numpy())).to(
points_rand
)[..., None]
get_gt_sdf = func
else:
raise ValueError(
f"Unknown shape initialization type: {self.cfg.shape_init}"
)
# Initialize SDF to a given shape when no weights are provided or force_shape_init is True
optim = torch.optim.Adam(self.parameters(), lr=1e-3)
from tqdm import tqdm
for _ in tqdm(
range(1000),
desc=f"Initializing SDF to a(n) {self.cfg.shape_init}:",
disable=get_rank() != 0,
):
points_rand = (
torch.rand((10000, 3), dtype=torch.float32).to(self.device) * 2.0 - 1.0
)
sdf_gt = get_gt_sdf(points_rand)
sdf_pred = self.forward_sdf(points_rand)
loss = F.mse_loss(sdf_pred, sdf_gt)
optim.zero_grad()
loss.backward()
optim.step()
# explicit broadcast to ensure param consistency across ranks
for param in self.parameters():
broadcast(param, src=0)
def get_shifted_sdf(
self, points: Float[Tensor, "*N Di"], sdf: Float[Tensor, "*N 1"]
) -> Float[Tensor, "*N 1"]:
sdf_bias: Union[float, Float[Tensor, "*N 1"]]
if self.cfg.sdf_bias == "ellipsoid":
assert (
isinstance(self.cfg.sdf_bias_params, Sized)
and len(self.cfg.sdf_bias_params) == 3
)
size = torch.as_tensor(self.cfg.sdf_bias_params).to(points)
sdf_bias = ((points / size) ** 2).sum(
dim=-1, keepdim=True
).sqrt() - 1.0 # pseudo signed distance of an ellipsoid
elif self.cfg.sdf_bias == "sphere":
assert isinstance(self.cfg.sdf_bias_params, float)
radius = self.cfg.sdf_bias_params
sdf_bias = (points**2).sum(dim=-1, keepdim=True).sqrt() - radius
elif isinstance(self.cfg.sdf_bias, float):
sdf_bias = self.cfg.sdf_bias
else:
raise ValueError(f"Unknown sdf bias {self.cfg.sdf_bias}")
return sdf + sdf_bias
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
grad_enabled = torch.is_grad_enabled()
if output_normal and self.cfg.normal_type == "analytic":
torch.set_grad_enabled(True)
points.requires_grad_(True)
points_unscaled = points # points in the original scale
points = contract_to_unisphere(
points, self.bbox, self.unbounded
) # points normalized to (0, 1)
enc = self.encoding(points.view(-1, self.cfg.n_input_dims))
sdf = self.sdf_network(enc).view(*points.shape[:-1], 1)
sdf = self.get_shifted_sdf(points_unscaled, sdf)
output = {"sdf": sdf}
if self.cfg.n_feature_dims > 0:
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
output.update({"features": features})
if output_normal:
if (
self.cfg.normal_type == "finite_difference"
or self.cfg.normal_type == "finite_difference_laplacian"
):
assert self.finite_difference_normal_eps is not None
eps: float = self.finite_difference_normal_eps
if self.cfg.normal_type == "finite_difference_laplacian":
offsets: Float[Tensor, "6 3"] = torch.as_tensor(
[
[eps, 0.0, 0.0],
[-eps, 0.0, 0.0],
[0.0, eps, 0.0],
[0.0, -eps, 0.0],
[0.0, 0.0, eps],
[0.0, 0.0, -eps],
]
).to(points_unscaled)
points_offset: Float[Tensor, "... 6 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
sdf_offset: Float[Tensor, "... 6 1"] = self.forward_sdf(
points_offset
)
sdf_grad = (
0.5
* (sdf_offset[..., 0::2, 0] - sdf_offset[..., 1::2, 0])
/ eps
)
else:
offsets: Float[Tensor, "3 3"] = torch.as_tensor(
[[eps, 0.0, 0.0], [0.0, eps, 0.0], [0.0, 0.0, eps]]
).to(points_unscaled)
points_offset: Float[Tensor, "... 3 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
sdf_offset: Float[Tensor, "... 3 1"] = self.forward_sdf(
points_offset
)
sdf_grad = (sdf_offset[..., 0::1, 0] - sdf) / eps
normal = F.normalize(sdf_grad, dim=-1)
elif self.cfg.normal_type == "pred":
normal = self.normal_network(enc).view(*points.shape[:-1], 3)
normal = F.normalize(normal, dim=-1)
sdf_grad = normal
elif self.cfg.normal_type == "analytic":
sdf_grad = -torch.autograd.grad(
sdf,
points_unscaled,
grad_outputs=torch.ones_like(sdf),
create_graph=True,
)[0]
normal = F.normalize(sdf_grad, dim=-1)
if not grad_enabled:
sdf_grad = sdf_grad.detach()
normal = normal.detach()
else:
raise AttributeError(f"Unknown normal type {self.cfg.normal_type}")
output.update(
{"normal": normal, "shading_normal": normal, "sdf_grad": sdf_grad}
)
return output
def forward_sdf(self, points: Float[Tensor, "*N Di"]) -> Float[Tensor, "*N 1"]:
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
sdf = self.sdf_network(
self.encoding(points.reshape(-1, self.cfg.n_input_dims))
).reshape(*points.shape[:-1], 1)
sdf = self.get_shifted_sdf(points_unscaled, sdf)
return sdf
def forward_field(
self, points: Float[Tensor, "*N Di"]
) -> Tuple[Float[Tensor, "*N 1"], Optional[Float[Tensor, "*N 3"]]]:
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
enc = self.encoding(points.reshape(-1, self.cfg.n_input_dims))
sdf = self.sdf_network(enc).reshape(*points.shape[:-1], 1)
sdf = self.get_shifted_sdf(points_unscaled, sdf)
deformation: Optional[Float[Tensor, "*N 3"]] = None
if self.cfg.isosurface_deformable_grid:
deformation = self.deformation_network(enc).reshape(*points.shape[:-1], 3)
return sdf, deformation
def forward_level(
self, field: Float[Tensor, "*N 1"], threshold: float
) -> Float[Tensor, "*N 1"]:
return field - threshold
def export(self, points: Float[Tensor, "*N Di"], **kwargs) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if self.cfg.n_feature_dims == 0:
return out
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
enc = self.encoding(points.reshape(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
out.update(
{
"features": features,
}
)
return out
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
if (
self.cfg.normal_type == "finite_difference"
or self.cfg.normal_type == "finite_difference_laplacian"
):
if isinstance(self.cfg.finite_difference_normal_eps, float):
self.finite_difference_normal_eps = (
self.cfg.finite_difference_normal_eps
)
elif self.cfg.finite_difference_normal_eps == "progressive":
# progressive finite difference eps from Neuralangelo
# https://arxiv.org/abs/2306.03092
hg_conf: Any = self.cfg.pos_encoding_config
assert (
hg_conf.otype == "ProgressiveBandHashGrid"
), "finite_difference_normal_eps=progressive only works with ProgressiveBandHashGrid"
current_level = min(
hg_conf.start_level
+ max(global_step - hg_conf.start_step, 0) // hg_conf.update_steps,
hg_conf.n_levels,
)
grid_res = hg_conf.base_resolution * hg_conf.per_level_scale ** (
current_level - 1
)
grid_size = 2 * self.cfg.radius / grid_res
if grid_size != self.finite_difference_normal_eps:
threestudio.info(
f"Update finite_difference_normal_eps to {grid_size}"
)
self.finite_difference_normal_eps = grid_size
else:
raise ValueError(
f"Unknown finite_difference_normal_eps={self.cfg.finite_difference_normal_eps}"
)

View File

@@ -0,0 +1,325 @@
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.geometry.base import (
BaseGeometry,
BaseImplicitGeometry,
contract_to_unisphere,
)
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
@threestudio.register("implicit-volume")
class ImplicitVolume(BaseImplicitGeometry):
@dataclass
class Config(BaseImplicitGeometry.Config):
n_input_dims: int = 3
n_feature_dims: int = 3
density_activation: Optional[str] = "softplus"
density_bias: Union[float, str] = "blob_magic3d"
density_blob_scale: float = 10.0
density_blob_std: float = 0.5
pos_encoding_config: dict = field(
default_factory=lambda: {
"otype": "HashGrid",
"n_levels": 16,
"n_features_per_level": 2,
"log2_hashmap_size": 19,
"base_resolution": 16,
"per_level_scale": 1.447269237440378,
}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "VanillaMLP",
"activation": "ReLU",
"output_activation": "none",
"n_neurons": 64,
"n_hidden_layers": 1,
}
)
normal_type: Optional[
str
] = "finite_difference" # in ['pred', 'finite_difference', 'finite_difference_laplacian']
finite_difference_normal_eps: Union[
float, str
] = 0.01 # in [float, "progressive"]
# automatically determine the threshold
isosurface_threshold: Union[float, str] = 25.0
# 4D Gaussian Annealing
anneal_density_blob_std_config: Optional[dict] = None
cfg: Config
def configure(self) -> None:
super().configure()
self.encoding = get_encoding(
self.cfg.n_input_dims, self.cfg.pos_encoding_config
)
self.density_network = get_mlp(
self.encoding.n_output_dims, 1, self.cfg.mlp_network_config
)
if self.cfg.n_feature_dims > 0:
self.feature_network = get_mlp(
self.encoding.n_output_dims,
self.cfg.n_feature_dims,
self.cfg.mlp_network_config,
)
if self.cfg.normal_type == "pred":
self.normal_network = get_mlp(
self.encoding.n_output_dims, 3, self.cfg.mlp_network_config
)
self.finite_difference_normal_eps: Optional[float] = None
def get_activated_density(
self, points: Float[Tensor, "*N Di"], density: Float[Tensor, "*N 1"]
) -> Tuple[Float[Tensor, "*N 1"], Float[Tensor, "*N 1"]]:
density_bias: Union[float, Float[Tensor, "*N 1"]]
if self.cfg.density_bias == "blob_dreamfusion":
# pre-activation density bias
density_bias = (
self.cfg.density_blob_scale
* torch.exp(
-0.5 * (points**2).sum(dim=-1) / self.cfg.density_blob_std**2
)[..., None]
)
elif self.cfg.density_bias == "blob_magic3d":
# pre-activation density bias
density_bias = (
self.cfg.density_blob_scale
* (
1
- torch.sqrt((points**2).sum(dim=-1)) / self.cfg.density_blob_std
)[..., None]
)
elif isinstance(self.cfg.density_bias, float):
density_bias = self.cfg.density_bias
else:
raise ValueError(f"Unknown density bias {self.cfg.density_bias}")
raw_density: Float[Tensor, "*N 1"] = density + density_bias
density = get_activation(self.cfg.density_activation)(raw_density)
return raw_density, density
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
grad_enabled = torch.is_grad_enabled()
if output_normal and self.cfg.normal_type == "analytic":
torch.set_grad_enabled(True)
points.requires_grad_(True)
points_unscaled = points # points in the original scale
points = contract_to_unisphere(
points, self.bbox, self.unbounded
) # points normalized to (0, 1)
enc = self.encoding(points.view(-1, self.cfg.n_input_dims))
density = self.density_network(enc).view(*points.shape[:-1], 1)
raw_density, density = self.get_activated_density(points_unscaled, density)
output = {
"density": density,
}
if self.cfg.n_feature_dims > 0:
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
output.update({"features": features})
if output_normal:
if (
self.cfg.normal_type == "finite_difference"
or self.cfg.normal_type == "finite_difference_laplacian"
):
# TODO: use raw density
assert self.finite_difference_normal_eps is not None
eps: float = self.finite_difference_normal_eps
if self.cfg.normal_type == "finite_difference_laplacian":
offsets: Float[Tensor, "6 3"] = torch.as_tensor(
[
[eps, 0.0, 0.0],
[-eps, 0.0, 0.0],
[0.0, eps, 0.0],
[0.0, -eps, 0.0],
[0.0, 0.0, eps],
[0.0, 0.0, -eps],
]
).to(points_unscaled)
points_offset: Float[Tensor, "... 6 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
density_offset: Float[Tensor, "... 6 1"] = self.forward_density(
points_offset
)
normal = (
-0.5
* (density_offset[..., 0::2, 0] - density_offset[..., 1::2, 0])
/ eps
)
else:
offsets: Float[Tensor, "3 3"] = torch.as_tensor(
[[eps, 0.0, 0.0], [0.0, eps, 0.0], [0.0, 0.0, eps]]
).to(points_unscaled)
points_offset: Float[Tensor, "... 3 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
density_offset: Float[Tensor, "... 3 1"] = self.forward_density(
points_offset
)
normal = -(density_offset[..., 0::1, 0] - density) / eps
normal = F.normalize(normal, dim=-1)
elif self.cfg.normal_type == "pred":
normal = self.normal_network(enc).view(*points.shape[:-1], 3)
normal = F.normalize(normal, dim=-1)
elif self.cfg.normal_type == "analytic":
normal = -torch.autograd.grad(
density,
points_unscaled,
grad_outputs=torch.ones_like(density),
create_graph=True,
)[0]
normal = F.normalize(normal, dim=-1)
if not grad_enabled:
normal = normal.detach()
else:
raise AttributeError(f"Unknown normal type {self.cfg.normal_type}")
output.update({"normal": normal, "shading_normal": normal})
torch.set_grad_enabled(grad_enabled)
return output
def forward_density(self, points: Float[Tensor, "*N Di"]) -> Float[Tensor, "*N 1"]:
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
density = self.density_network(
self.encoding(points.reshape(-1, self.cfg.n_input_dims))
).reshape(*points.shape[:-1], 1)
_, density = self.get_activated_density(points_unscaled, density)
return density
def forward_field(
self, points: Float[Tensor, "*N Di"]
) -> Tuple[Float[Tensor, "*N 1"], Optional[Float[Tensor, "*N 3"]]]:
if self.cfg.isosurface_deformable_grid:
threestudio.warn(
f"{self.__class__.__name__} does not support isosurface_deformable_grid. Ignoring."
)
density = self.forward_density(points)
return density, None
def forward_level(
self, field: Float[Tensor, "*N 1"], threshold: float
) -> Float[Tensor, "*N 1"]:
return -(field - threshold)
def export(self, points: Float[Tensor, "*N Di"], **kwargs) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if self.cfg.n_feature_dims == 0:
return out
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
enc = self.encoding(points.reshape(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
out.update(
{
"features": features,
}
)
return out
@staticmethod
@torch.no_grad()
def create_from(
other: BaseGeometry,
cfg: Optional[Union[dict, DictConfig]] = None,
copy_net: bool = True,
**kwargs,
) -> "ImplicitVolume":
if isinstance(other, ImplicitVolume):
instance = ImplicitVolume(cfg, **kwargs)
instance.encoding.load_state_dict(other.encoding.state_dict())
instance.density_network.load_state_dict(other.density_network.state_dict())
if copy_net:
if (
instance.cfg.n_feature_dims > 0
and other.cfg.n_feature_dims == instance.cfg.n_feature_dims
):
instance.feature_network.load_state_dict(
other.feature_network.state_dict()
)
if (
instance.cfg.normal_type == "pred"
and other.cfg.normal_type == "pred"
):
instance.normal_network.load_state_dict(
other.normal_network.state_dict()
)
return instance
else:
raise TypeError(
f"Cannot create {ImplicitVolume.__name__} from {other.__class__.__name__}"
)
# FIXME: use progressive normal eps
def update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
) -> None:
if self.cfg.anneal_density_blob_std_config is not None:
min_step = self.cfg.anneal_density_blob_std_config.min_anneal_step
max_step = self.cfg.anneal_density_blob_std_config.max_anneal_step
if global_step >= min_step and global_step <= max_step:
end_val = self.cfg.anneal_density_blob_std_config.end_val
start_val = self.cfg.anneal_density_blob_std_config.start_val
self.density_blob_std = start_val + (global_step - min_step) * (
end_val - start_val
) / (max_step - min_step)
if (
self.cfg.normal_type == "finite_difference"
or self.cfg.normal_type == "finite_difference_laplacian"
):
if isinstance(self.cfg.finite_difference_normal_eps, float):
self.finite_difference_normal_eps = (
self.cfg.finite_difference_normal_eps
)
elif self.cfg.finite_difference_normal_eps == "progressive":
# progressive finite difference eps from Neuralangelo
# https://arxiv.org/abs/2306.03092
hg_conf: Any = self.cfg.pos_encoding_config
assert (
hg_conf.otype == "ProgressiveBandHashGrid"
), "finite_difference_normal_eps=progressive only works with ProgressiveBandHashGrid"
current_level = min(
hg_conf.start_level
+ max(global_step - hg_conf.start_step, 0) // hg_conf.update_steps,
hg_conf.n_levels,
)
grid_res = hg_conf.base_resolution * hg_conf.per_level_scale ** (
current_level - 1
)
grid_size = 2 * self.cfg.radius / grid_res
if grid_size != self.finite_difference_normal_eps:
threestudio.info(
f"Update finite_difference_normal_eps to {grid_size}"
)
self.finite_difference_normal_eps = grid_size
else:
raise ValueError(
f"Unknown finite_difference_normal_eps={self.cfg.finite_difference_normal_eps}"
)

View File

@@ -0,0 +1,369 @@
import os
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.geometry.base import (
BaseExplicitGeometry,
BaseGeometry,
contract_to_unisphere,
)
from threestudio.models.geometry.implicit_sdf import ImplicitSDF
from threestudio.models.geometry.implicit_volume import ImplicitVolume
from threestudio.models.isosurface import MarchingTetrahedraHelper
from threestudio.models.mesh import Mesh
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.misc import broadcast
from threestudio.utils.ops import scale_tensor
from threestudio.utils.typing import *
@threestudio.register("tetrahedra-sdf-grid")
class TetrahedraSDFGrid(BaseExplicitGeometry):
@dataclass
class Config(BaseExplicitGeometry.Config):
isosurface_resolution: int = 128
isosurface_deformable_grid: bool = True
isosurface_remove_outliers: bool = False
isosurface_outlier_n_faces_threshold: Union[int, float] = 0.01
n_input_dims: int = 3
n_feature_dims: int = 3
pos_encoding_config: dict = field(
default_factory=lambda: {
"otype": "HashGrid",
"n_levels": 16,
"n_features_per_level": 2,
"log2_hashmap_size": 19,
"base_resolution": 16,
"per_level_scale": 1.447269237440378,
}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "VanillaMLP",
"activation": "ReLU",
"output_activation": "none",
"n_neurons": 64,
"n_hidden_layers": 1,
}
)
shape_init: Optional[str] = None
shape_init_params: Optional[Any] = None
shape_init_mesh_up: str = "+z"
shape_init_mesh_front: str = "+x"
force_shape_init: bool = False
geometry_only: bool = False
fix_geometry: bool = False
cfg: Config
def configure(self) -> None:
super().configure()
# this should be saved to state_dict, register as buffer
self.isosurface_bbox: Float[Tensor, "2 3"]
self.register_buffer("isosurface_bbox", self.bbox.clone())
self.isosurface_helper = MarchingTetrahedraHelper(
self.cfg.isosurface_resolution,
f"load/tets/{self.cfg.isosurface_resolution}_tets.npz",
)
self.sdf: Float[Tensor, "Nv 1"]
self.deformation: Optional[Float[Tensor, "Nv 3"]]
if not self.cfg.fix_geometry:
self.register_parameter(
"sdf",
nn.Parameter(
torch.zeros(
(self.isosurface_helper.grid_vertices.shape[0], 1),
dtype=torch.float32,
)
),
)
if self.cfg.isosurface_deformable_grid:
self.register_parameter(
"deformation",
nn.Parameter(
torch.zeros_like(self.isosurface_helper.grid_vertices)
),
)
else:
self.deformation = None
else:
self.register_buffer(
"sdf",
torch.zeros(
(self.isosurface_helper.grid_vertices.shape[0], 1),
dtype=torch.float32,
),
)
if self.cfg.isosurface_deformable_grid:
self.register_buffer(
"deformation",
torch.zeros_like(self.isosurface_helper.grid_vertices),
)
else:
self.deformation = None
if not self.cfg.geometry_only:
self.encoding = get_encoding(
self.cfg.n_input_dims, self.cfg.pos_encoding_config
)
self.feature_network = get_mlp(
self.encoding.n_output_dims,
self.cfg.n_feature_dims,
self.cfg.mlp_network_config,
)
self.mesh: Optional[Mesh] = None
def initialize_shape(self) -> None:
if self.cfg.shape_init is None and not self.cfg.force_shape_init:
return
# do not initialize shape if weights are provided
if self.cfg.weights is not None and not self.cfg.force_shape_init:
return
get_gt_sdf: Callable[[Float[Tensor, "N 3"]], Float[Tensor, "N 1"]]
assert isinstance(self.cfg.shape_init, str)
if self.cfg.shape_init == "ellipsoid":
assert (
isinstance(self.cfg.shape_init_params, Sized)
and len(self.cfg.shape_init_params) == 3
)
size = torch.as_tensor(self.cfg.shape_init_params).to(self.device)
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
return ((points_rand / size) ** 2).sum(
dim=-1, keepdim=True
).sqrt() - 1.0 # pseudo signed distance of an ellipsoid
get_gt_sdf = func
elif self.cfg.shape_init == "sphere":
assert isinstance(self.cfg.shape_init_params, float)
radius = self.cfg.shape_init_params
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
return (points_rand**2).sum(dim=-1, keepdim=True).sqrt() - radius
get_gt_sdf = func
elif self.cfg.shape_init.startswith("mesh:"):
assert isinstance(self.cfg.shape_init_params, float)
mesh_path = self.cfg.shape_init[5:]
if not os.path.exists(mesh_path):
raise ValueError(f"Mesh file {mesh_path} does not exist.")
import trimesh
mesh = trimesh.load(mesh_path)
# move to center
centroid = mesh.vertices.mean(0)
mesh.vertices = mesh.vertices - centroid
# align to up-z and front-x
dirs = ["+x", "+y", "+z", "-x", "-y", "-z"]
dir2vec = {
"+x": np.array([1, 0, 0]),
"+y": np.array([0, 1, 0]),
"+z": np.array([0, 0, 1]),
"-x": np.array([-1, 0, 0]),
"-y": np.array([0, -1, 0]),
"-z": np.array([0, 0, -1]),
}
if (
self.cfg.shape_init_mesh_up not in dirs
or self.cfg.shape_init_mesh_front not in dirs
):
raise ValueError(
f"shape_init_mesh_up and shape_init_mesh_front must be one of {dirs}."
)
if self.cfg.shape_init_mesh_up[1] == self.cfg.shape_init_mesh_front[1]:
raise ValueError(
"shape_init_mesh_up and shape_init_mesh_front must be orthogonal."
)
z_, x_ = (
dir2vec[self.cfg.shape_init_mesh_up],
dir2vec[self.cfg.shape_init_mesh_front],
)
y_ = np.cross(z_, x_)
std2mesh = np.stack([x_, y_, z_], axis=0).T
mesh2std = np.linalg.inv(std2mesh)
# scaling
scale = np.abs(mesh.vertices).max()
mesh.vertices = mesh.vertices / scale * self.cfg.shape_init_params
mesh.vertices = np.dot(mesh2std, mesh.vertices.T).T
from pysdf import SDF
sdf = SDF(mesh.vertices, mesh.faces)
def func(points_rand: Float[Tensor, "N 3"]) -> Float[Tensor, "N 1"]:
# add a negative signed here
# as in pysdf the inside of the shape has positive signed distance
return torch.from_numpy(-sdf(points_rand.cpu().numpy())).to(
points_rand
)[..., None]
get_gt_sdf = func
else:
raise ValueError(
f"Unknown shape initialization type: {self.cfg.shape_init}"
)
sdf_gt = get_gt_sdf(
scale_tensor(
self.isosurface_helper.grid_vertices,
self.isosurface_helper.points_range,
self.isosurface_bbox,
)
)
self.sdf.data = sdf_gt
# explicit broadcast to ensure param consistency across ranks
for param in self.parameters():
broadcast(param, src=0)
def isosurface(self) -> Mesh:
# return cached mesh if fix_geometry is True to save computation
if self.cfg.fix_geometry and self.mesh is not None:
return self.mesh
mesh = self.isosurface_helper(self.sdf, self.deformation)
mesh.v_pos = scale_tensor(
mesh.v_pos, self.isosurface_helper.points_range, self.isosurface_bbox
)
if self.cfg.isosurface_remove_outliers:
mesh = mesh.remove_outlier(self.cfg.isosurface_outlier_n_faces_threshold)
self.mesh = mesh
return mesh
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
if self.cfg.geometry_only:
return {}
assert (
output_normal == False
), f"Normal output is not supported for {self.__class__.__name__}"
points_unscaled = points # points in the original scale
points = contract_to_unisphere(points, self.bbox) # points normalized to (0, 1)
enc = self.encoding(points.view(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
return {"features": features}
@staticmethod
@torch.no_grad()
def create_from(
other: BaseGeometry,
cfg: Optional[Union[dict, DictConfig]] = None,
copy_net: bool = True,
**kwargs,
) -> "TetrahedraSDFGrid":
if isinstance(other, TetrahedraSDFGrid):
instance = TetrahedraSDFGrid(cfg, **kwargs)
assert instance.cfg.isosurface_resolution == other.cfg.isosurface_resolution
instance.isosurface_bbox = other.isosurface_bbox.clone()
instance.sdf.data = other.sdf.data.clone()
if (
instance.cfg.isosurface_deformable_grid
and other.cfg.isosurface_deformable_grid
):
assert (
instance.deformation is not None and other.deformation is not None
)
instance.deformation.data = other.deformation.data.clone()
if (
not instance.cfg.geometry_only
and not other.cfg.geometry_only
and copy_net
):
instance.encoding.load_state_dict(other.encoding.state_dict())
instance.feature_network.load_state_dict(
other.feature_network.state_dict()
)
return instance
elif isinstance(other, ImplicitVolume):
instance = TetrahedraSDFGrid(cfg, **kwargs)
if other.cfg.isosurface_method != "mt":
other.cfg.isosurface_method = "mt"
threestudio.warn(
f"Override isosurface_method of the source geometry to 'mt'"
)
if other.cfg.isosurface_resolution != instance.cfg.isosurface_resolution:
other.cfg.isosurface_resolution = instance.cfg.isosurface_resolution
threestudio.warn(
f"Override isosurface_resolution of the source geometry to {instance.cfg.isosurface_resolution}"
)
mesh = other.isosurface()
instance.isosurface_bbox = mesh.extras["bbox"]
instance.sdf.data = (
mesh.extras["grid_level"].to(instance.sdf.data).clamp(-1, 1)
)
if not instance.cfg.geometry_only and copy_net:
instance.encoding.load_state_dict(other.encoding.state_dict())
instance.feature_network.load_state_dict(
other.feature_network.state_dict()
)
return instance
elif isinstance(other, ImplicitSDF):
instance = TetrahedraSDFGrid(cfg, **kwargs)
if other.cfg.isosurface_method != "mt":
other.cfg.isosurface_method = "mt"
threestudio.warn(
f"Override isosurface_method of the source geometry to 'mt'"
)
if other.cfg.isosurface_resolution != instance.cfg.isosurface_resolution:
other.cfg.isosurface_resolution = instance.cfg.isosurface_resolution
threestudio.warn(
f"Override isosurface_resolution of the source geometry to {instance.cfg.isosurface_resolution}"
)
mesh = other.isosurface()
instance.isosurface_bbox = mesh.extras["bbox"]
instance.sdf.data = mesh.extras["grid_level"].to(instance.sdf.data)
if (
instance.cfg.isosurface_deformable_grid
and other.cfg.isosurface_deformable_grid
):
assert instance.deformation is not None
instance.deformation.data = mesh.extras["grid_deformation"].to(
instance.deformation.data
)
if not instance.cfg.geometry_only and copy_net:
instance.encoding.load_state_dict(other.encoding.state_dict())
instance.feature_network.load_state_dict(
other.feature_network.state_dict()
)
return instance
else:
raise TypeError(
f"Cannot create {TetrahedraSDFGrid.__name__} from {other.__class__.__name__}"
)
def export(self, points: Float[Tensor, "*N Di"], **kwargs) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if self.cfg.geometry_only or self.cfg.n_feature_dims == 0:
return out
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox)
enc = self.encoding(points.reshape(-1, self.cfg.n_input_dims))
features = self.feature_network(enc).view(
*points.shape[:-1], self.cfg.n_feature_dims
)
out.update(
{
"features": features,
}
)
return out

View File

@@ -0,0 +1,190 @@
from dataclasses import dataclass, field
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.geometry.base import BaseImplicitGeometry, contract_to_unisphere
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
@threestudio.register("volume-grid")
class VolumeGrid(BaseImplicitGeometry):
@dataclass
class Config(BaseImplicitGeometry.Config):
grid_size: Tuple[int, int, int] = field(default_factory=lambda: (100, 100, 100))
n_feature_dims: int = 3
density_activation: Optional[str] = "softplus"
density_bias: Union[float, str] = "blob"
density_blob_scale: float = 5.0
density_blob_std: float = 0.5
normal_type: Optional[
str
] = "finite_difference" # in ['pred', 'finite_difference', 'finite_difference_laplacian']
# automatically determine the threshold
isosurface_threshold: Union[float, str] = "auto"
cfg: Config
def configure(self) -> None:
super().configure()
self.grid_size = self.cfg.grid_size
self.grid = nn.Parameter(
torch.zeros(1, self.cfg.n_feature_dims + 1, *self.grid_size)
)
if self.cfg.density_bias == "blob":
self.register_buffer("density_scale", torch.tensor(0.0))
else:
self.density_scale = nn.Parameter(torch.tensor(0.0))
if self.cfg.normal_type == "pred":
self.normal_grid = nn.Parameter(torch.zeros(1, 3, *self.grid_size))
def get_density_bias(self, points: Float[Tensor, "*N Di"]):
if self.cfg.density_bias == "blob":
# density_bias: Float[Tensor, "*N 1"] = self.cfg.density_blob_scale * torch.exp(-0.5 * (points ** 2).sum(dim=-1) / self.cfg.density_blob_std ** 2)[...,None]
density_bias: Float[Tensor, "*N 1"] = (
self.cfg.density_blob_scale
* (
1
- torch.sqrt((points.detach() ** 2).sum(dim=-1))
/ self.cfg.density_blob_std
)[..., None]
)
return density_bias
elif isinstance(self.cfg.density_bias, float):
return self.cfg.density_bias
else:
raise AttributeError(f"Unknown density bias {self.cfg.density_bias}")
def get_trilinear_feature(
self, points: Float[Tensor, "*N Di"], grid: Float[Tensor, "1 Df G1 G2 G3"]
) -> Float[Tensor, "*N Df"]:
points_shape = points.shape[:-1]
df = grid.shape[1]
di = points.shape[-1]
out = F.grid_sample(
grid, points.view(1, 1, 1, -1, di), align_corners=False, mode="bilinear"
)
out = out.reshape(df, -1).T.reshape(*points_shape, df)
return out
def forward(
self, points: Float[Tensor, "*N Di"], output_normal: bool = False
) -> Dict[str, Float[Tensor, "..."]]:
points_unscaled = points # points in the original scale
points = contract_to_unisphere(
points, self.bbox, self.unbounded
) # points normalized to (0, 1)
points = points * 2 - 1 # convert to [-1, 1] for grid sample
out = self.get_trilinear_feature(points, self.grid)
density, features = out[..., 0:1], out[..., 1:]
density = density * torch.exp(self.density_scale) # exp scaling in DreamFusion
# breakpoint()
density = get_activation(self.cfg.density_activation)(
density + self.get_density_bias(points_unscaled)
)
output = {
"density": density,
"features": features,
}
if output_normal:
if (
self.cfg.normal_type == "finite_difference"
or self.cfg.normal_type == "finite_difference_laplacian"
):
eps = 1.0e-3
if self.cfg.normal_type == "finite_difference_laplacian":
offsets: Float[Tensor, "6 3"] = torch.as_tensor(
[
[eps, 0.0, 0.0],
[-eps, 0.0, 0.0],
[0.0, eps, 0.0],
[0.0, -eps, 0.0],
[0.0, 0.0, eps],
[0.0, 0.0, -eps],
]
).to(points_unscaled)
points_offset: Float[Tensor, "... 6 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
density_offset: Float[Tensor, "... 6 1"] = self.forward_density(
points_offset
)
normal = (
-0.5
* (density_offset[..., 0::2, 0] - density_offset[..., 1::2, 0])
/ eps
)
else:
offsets: Float[Tensor, "3 3"] = torch.as_tensor(
[[eps, 0.0, 0.0], [0.0, eps, 0.0], [0.0, 0.0, eps]]
).to(points_unscaled)
points_offset: Float[Tensor, "... 3 3"] = (
points_unscaled[..., None, :] + offsets
).clamp(-self.cfg.radius, self.cfg.radius)
density_offset: Float[Tensor, "... 3 1"] = self.forward_density(
points_offset
)
normal = -(density_offset[..., 0::1, 0] - density) / eps
normal = F.normalize(normal, dim=-1)
elif self.cfg.normal_type == "pred":
normal = self.get_trilinear_feature(points, self.normal_grid)
normal = F.normalize(normal, dim=-1)
else:
raise AttributeError(f"Unknown normal type {self.cfg.normal_type}")
output.update({"normal": normal, "shading_normal": normal})
return output
def forward_density(self, points: Float[Tensor, "*N Di"]) -> Float[Tensor, "*N 1"]:
points_unscaled = points
points = contract_to_unisphere(points_unscaled, self.bbox, self.unbounded)
points = points * 2 - 1 # convert to [-1, 1] for grid sample
out = self.get_trilinear_feature(points, self.grid)
density = out[..., 0:1]
density = density * torch.exp(self.density_scale)
density = get_activation(self.cfg.density_activation)(
density + self.get_density_bias(points_unscaled)
)
return density
def forward_field(
self, points: Float[Tensor, "*N Di"]
) -> Tuple[Float[Tensor, "*N 1"], Optional[Float[Tensor, "*N 3"]]]:
if self.cfg.isosurface_deformable_grid:
threestudio.warn(
f"{self.__class__.__name__} does not support isosurface_deformable_grid. Ignoring."
)
density = self.forward_density(points)
return density, None
def forward_level(
self, field: Float[Tensor, "*N 1"], threshold: float
) -> Float[Tensor, "*N 1"]:
return -(field - threshold)
def export(self, points: Float[Tensor, "*N Di"], **kwargs) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if self.cfg.n_feature_dims == 0:
return out
points_unscaled = points
points = contract_to_unisphere(points, self.bbox, self.unbounded)
points = points * 2 - 1 # convert to [-1, 1] for grid sample
features = self.get_trilinear_feature(points, self.grid)[..., 1:]
out.update(
{
"features": features,
}
)
return out

View File

@@ -0,0 +1,13 @@
from . import (
controlnet_guidance,
controlnet_reg_guidance,
deep_floyd_guidance,
stable_diffusion_guidance,
stable_diffusion_unified_guidance,
stable_diffusion_vsd_guidance,
stable_diffusion_bsd_guidance,
stable_zero123_guidance,
zero123_guidance,
zero123_unified_guidance,
clip_guidance,
)

View File

@@ -0,0 +1,84 @@
from dataclasses import dataclass
import torch
import torch.nn.functional as F
import torchvision.transforms as T
import clip
import threestudio
from threestudio.utils.base import BaseObject
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.typing import *
@threestudio.register("clip-guidance")
class CLIPGuidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
cache_dir: Optional[str] = None
pretrained_model_name_or_path: str = "ViT-B/16"
view_dependent_prompting: bool = True
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading CLIP ...")
self.clip_model, self.clip_preprocess = clip.load(
self.cfg.pretrained_model_name_or_path,
device=self.device,
jit=False,
download_root=self.cfg.cache_dir
)
self.aug = T.Compose([
T.Resize((224, 224)),
T.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)),
])
threestudio.info(f"Loaded CLIP!")
@torch.cuda.amp.autocast(enabled=False)
def get_embedding(self, input_value, is_text=True):
if is_text:
value = clip.tokenize(input_value).to(self.device)
z = self.clip_model.encode_text(value)
else:
input_value = self.aug(input_value)
z = self.clip_model.encode_image(input_value)
return z / z.norm(dim=-1, keepdim=True)
def get_loss(self, image_z, clip_z, loss_type='similarity_score', use_mean=True):
if loss_type == 'similarity_score':
loss = -((image_z * clip_z).sum(-1))
elif loss_type == 'spherical_dist':
image_z, clip_z = F.normalize(image_z, dim=-1), F.normalize(clip_z, dim=-1)
loss = ((image_z - clip_z).norm(dim=-1).div(2).arcsin().pow(2).mul(2))
else:
raise NotImplementedError
return loss.mean() if use_mean else loss
def __call__(
self,
pred_rgb: Float[Tensor, "B H W C"],
gt_rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
embedding_type: str = 'both',
loss_type: Optional[str] = 'similarity_score',
**kwargs,
):
clip_text_loss, clip_img_loss = 0, 0
if embedding_type in ('both', 'text'):
text_embeddings = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
).chunk(2)[0]
clip_text_loss = self.get_loss(self.get_embedding(pred_rgb, is_text=False), text_embeddings, loss_type=loss_type)
if embedding_type in ('both', 'img'):
clip_img_loss = self.get_loss(self.get_embedding(pred_rgb, is_text=False), self.get_embedding(gt_rgb, is_text=False), loss_type=loss_type)
return clip_text_loss + clip_img_loss

View File

@@ -0,0 +1,517 @@
import os
from dataclasses import dataclass
import cv2
import numpy as np
import torch
import torch.nn.functional as F
from controlnet_aux import CannyDetector, NormalBaeDetector
from diffusers import ControlNetModel, DDIMScheduler, StableDiffusionControlNetPipeline
from diffusers.utils.import_utils import is_xformers_available
from tqdm import tqdm
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, parse_version
from threestudio.utils.perceptual import PerceptualLoss
from threestudio.utils.typing import *
@threestudio.register("stable-diffusion-controlnet-guidance")
class ControlNetGuidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
cache_dir: Optional[str] = None
pretrained_model_name_or_path: str = "SG161222/Realistic_Vision_V2.0"
ddim_scheduler_name_or_path: str = "runwayml/stable-diffusion-v1-5"
control_type: str = "normal" # normal/canny
enable_memory_efficient_attention: bool = False
enable_sequential_cpu_offload: bool = False
enable_attention_slicing: bool = False
enable_channels_last_format: bool = False
guidance_scale: float = 7.5
condition_scale: float = 1.5
grad_clip: Optional[Any] = None
half_precision_weights: bool = True
fixed_size: int = -1
min_step_percent: float = 0.02
max_step_percent: float = 0.98
diffusion_steps: int = 20
use_sds: bool = False
use_du: bool = False
per_du_step: int = 10
start_du_step: int = 1000
cache_du: bool = False
# Canny threshold
canny_lower_bound: int = 50
canny_upper_bound: int = 100
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading ControlNet ...")
controlnet_name_or_path: str
if self.cfg.control_type in ("normal", "input_normal"):
controlnet_name_or_path = "lllyasviel/control_v11p_sd15_normalbae"
elif self.cfg.control_type == "canny":
controlnet_name_or_path = "lllyasviel/control_v11p_sd15_canny"
self.weights_dtype = (
torch.float16 if self.cfg.half_precision_weights else torch.float32
)
pipe_kwargs = {
"safety_checker": None,
"feature_extractor": None,
"requires_safety_checker": False,
"torch_dtype": self.weights_dtype,
"cache_dir": self.cfg.cache_dir,
}
controlnet = ControlNetModel.from_pretrained(
controlnet_name_or_path,
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
)
self.pipe = StableDiffusionControlNetPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, controlnet=controlnet, **pipe_kwargs
).to(self.device)
self.scheduler = DDIMScheduler.from_pretrained(
self.cfg.ddim_scheduler_name_or_path,
subfolder="scheduler",
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
)
self.scheduler.set_timesteps(self.cfg.diffusion_steps)
if self.cfg.enable_memory_efficient_attention:
if parse_version(torch.__version__) >= parse_version("2"):
threestudio.info(
"PyTorch2.0 uses memory efficient attention by default."
)
elif not is_xformers_available():
threestudio.warn(
"xformers is not available, memory efficient attention is not enabled."
)
else:
self.pipe.enable_xformers_memory_efficient_attention()
if self.cfg.enable_sequential_cpu_offload:
self.pipe.enable_sequential_cpu_offload()
if self.cfg.enable_attention_slicing:
self.pipe.enable_attention_slicing(1)
if self.cfg.enable_channels_last_format:
self.pipe.unet.to(memory_format=torch.channels_last)
# Create model
self.vae = self.pipe.vae.eval()
self.unet = self.pipe.unet.eval()
self.controlnet = self.pipe.controlnet.eval()
if self.cfg.control_type == "normal":
self.preprocessor = NormalBaeDetector.from_pretrained(
"lllyasviel/Annotators"
)
self.preprocessor.model.to(self.device)
elif self.cfg.control_type == "canny":
self.preprocessor = CannyDetector()
for p in self.vae.parameters():
p.requires_grad_(False)
for p in self.unet.parameters():
p.requires_grad_(False)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.set_min_max_steps() # set to default value
self.alphas: Float[Tensor, "..."] = self.scheduler.alphas_cumprod.to(
self.device
)
self.grad_clip_val: Optional[float] = None
if self.cfg.use_du:
if self.cfg.cache_du:
self.edit_frames = {}
self.perceptual_loss = PerceptualLoss().eval().to(self.device)
threestudio.info(f"Loaded ControlNet!")
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def forward_controlnet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
image_cond: Float[Tensor, "..."],
condition_scale: float,
encoder_hidden_states: Float[Tensor, "..."],
) -> Float[Tensor, "..."]:
return self.controlnet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
controlnet_cond=image_cond.to(self.weights_dtype),
conditioning_scale=condition_scale,
return_dict=False,
)
@torch.cuda.amp.autocast(enabled=False)
def forward_control_unet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
cross_attention_kwargs,
down_block_additional_residuals,
mid_block_additional_residual,
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
return self.unet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
cross_attention_kwargs=cross_attention_kwargs,
down_block_additional_residuals=down_block_additional_residuals,
mid_block_additional_residual=mid_block_additional_residual,
).sample.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def encode_images(
self, imgs: Float[Tensor, "B 3 H W"]
) -> Float[Tensor, "B 4 DH DW"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
posterior = self.vae.encode(imgs.to(self.weights_dtype)).latent_dist
latents = posterior.sample() * self.vae.config.scaling_factor
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def encode_cond_images(
self, imgs: Float[Tensor, "B 3 H W"]
) -> Float[Tensor, "B 4 DH DW"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
posterior = self.vae.encode(imgs.to(self.weights_dtype)).latent_dist
latents = posterior.mode()
uncond_image_latents = torch.zeros_like(latents)
latents = torch.cat([latents, latents, uncond_image_latents], dim=0)
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def decode_latents(
self, latents: Float[Tensor, "B 4 DH DW"]
) -> Float[Tensor, "B 3 H W"]:
input_dtype = latents.dtype
latents = 1 / self.vae.config.scaling_factor * latents
image = self.vae.decode(latents.to(self.weights_dtype)).sample
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
def edit_latents(
self,
text_embeddings: Float[Tensor, "BB 77 768"],
latents: Float[Tensor, "B 4 DH DW"],
image_cond: Float[Tensor, "B 3 H W"],
t: Int[Tensor, "B"],
mask = None
) -> Float[Tensor, "B 4 DH DW"]:
self.scheduler.config.num_train_timesteps = t.item()
self.scheduler.set_timesteps(self.cfg.diffusion_steps)
if mask is not None:
mask = F.interpolate(mask, (latents.shape[-2], latents.shape[-1]), mode='bilinear')
with torch.no_grad():
# add noise
noise = torch.randn_like(latents)
latents = self.scheduler.add_noise(latents, noise, t) # type: ignore
# sections of code used from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py
threestudio.debug("Start editing...")
for i, t in enumerate(self.scheduler.timesteps):
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# pred noise
latent_model_input = torch.cat([latents] * 2)
(
down_block_res_samples,
mid_block_res_sample,
) = self.forward_controlnet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
image_cond=image_cond,
condition_scale=self.cfg.condition_scale,
)
noise_pred = self.forward_control_unet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
cross_attention_kwargs=None,
down_block_additional_residuals=down_block_res_samples,
mid_block_additional_residual=mid_block_res_sample,
)
# perform classifier-free guidance
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
if mask is not None:
noise_pred = mask * noise_pred + (1 - mask) * noise
# get previous sample, continue loop
latents = self.scheduler.step(noise_pred, t, latents).prev_sample
threestudio.debug("Editing finished.")
return latents
def prepare_image_cond(self, cond_rgb: Float[Tensor, "B H W C"]):
if self.cfg.control_type == "normal":
cond_rgb = (
(cond_rgb[0].detach().cpu().numpy() * 255).astype(np.uint8).copy()
)
detected_map = self.preprocessor(cond_rgb)
control = (
torch.from_numpy(np.array(detected_map)).float().to(self.device) / 255.0
)
control = control.unsqueeze(0)
control = control.permute(0, 3, 1, 2)
elif self.cfg.control_type == "canny":
cond_rgb = (
(cond_rgb[0].detach().cpu().numpy() * 255).astype(np.uint8).copy()
)
blurred_img = cv2.blur(cond_rgb, ksize=(5, 5))
detected_map = self.preprocessor(
blurred_img, self.cfg.canny_lower_bound, self.cfg.canny_upper_bound
)
control = (
torch.from_numpy(np.array(detected_map)).float().to(self.device) / 255.0
)
# control = control.unsqueeze(-1).repeat(1, 1, 3)
control = control.unsqueeze(0)
control = control.permute(0, 3, 1, 2)
elif self.cfg.control_type == "input_normal":
cond_rgb[..., 0] = (
1 - cond_rgb[..., 0]
) # Flip the sign on the x-axis to match bae system
control = cond_rgb.permute(0, 3, 1, 2)
else:
raise ValueError(f"Unknown control type: {self.cfg.control_type}")
return control
def compute_grad_sds(
self,
text_embeddings: Float[Tensor, "BB 77 768"],
latents: Float[Tensor, "B 4 DH DW"],
image_cond: Float[Tensor, "B 3 H W"],
t: Int[Tensor, "B"],
):
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# pred noise
latent_model_input = torch.cat([latents_noisy] * 2)
down_block_res_samples, mid_block_res_sample = self.forward_controlnet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
image_cond=image_cond,
condition_scale=self.cfg.condition_scale,
)
noise_pred = self.forward_control_unet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
cross_attention_kwargs=None,
down_block_additional_residuals=down_block_res_samples,
mid_block_additional_residual=mid_block_res_sample,
)
# perform classifier-free guidance
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
w = (1 - self.alphas[t]).view(-1, 1, 1, 1)
grad = w * (noise_pred - noise)
return grad
def compute_grad_du(
self,
latents: Float[Tensor, "B 4 H W"],
rgb_BCHW_HW8: Float[Tensor, "B 3 RH RW"],
cond_feature: Float[Tensor, "B 3 RH RW"],
cond_rgb: Float[Tensor, "B H W 3"],
text_embeddings: Float[Tensor, "BB 77 768"],
mask = None,
**kwargs,
):
batch_size, _, RH, RW = cond_feature.shape
assert batch_size == 1
origin_gt_rgb = F.interpolate(
cond_rgb.permute(0, 3, 1, 2), (RH, RW), mode="bilinear"
).permute(0, 2, 3, 1)
need_diffusion = (
self.global_step % self.cfg.per_du_step == 0
and self.global_step > self.cfg.start_du_step
)
if self.cfg.cache_du:
if torch.is_tensor(kwargs["index"]):
batch_index = kwargs["index"].item()
else:
batch_index = kwargs["index"]
if (
not (batch_index in self.edit_frames)
) and self.global_step > self.cfg.start_du_step:
need_diffusion = True
need_loss = self.cfg.cache_du or need_diffusion
guidance_out = {}
if need_diffusion:
t = torch.randint(
self.min_step,
self.max_step,
[1],
dtype=torch.long,
device=self.device,
)
print("t:", t)
edit_latents = self.edit_latents(text_embeddings, latents, cond_feature, t, mask)
edit_images = self.decode_latents(edit_latents)
edit_images = F.interpolate(
edit_images, (RH, RW), mode="bilinear"
).permute(0, 2, 3, 1)
self.edit_images = edit_images
if self.cfg.cache_du:
self.edit_frames[batch_index] = edit_images.detach().cpu()
if need_loss:
if self.cfg.cache_du:
if batch_index in self.edit_frames:
gt_rgb = self.edit_frames[batch_index].to(cond_feature.device)
else:
gt_rgb = origin_gt_rgb
else:
gt_rgb = edit_images
import cv2
import numpy as np
temp = (edit_images.detach().cpu()[0].numpy() * 255).astype(np.uint8)
cv2.imwrite(".threestudio_cache/test.jpg", temp[:, :, ::-1])
guidance_out.update(
{
"loss_l1": torch.nn.functional.l1_loss(
rgb_BCHW_HW8, gt_rgb.permute(0, 3, 1, 2), reduction="sum"
),
"loss_p": self.perceptual_loss(
rgb_BCHW_HW8.contiguous(),
gt_rgb.permute(0, 3, 1, 2).contiguous(),
).sum(),
}
)
return guidance_out
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
cond_rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
mask = None,
**kwargs,
):
batch_size, H, W, _ = rgb.shape
assert batch_size == 1
assert rgb.shape[:-1] == cond_rgb.shape[:-1]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
if mask is not None: mask = mask.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 DH DW"]
if self.cfg.fixed_size > 0:
RH, RW = self.cfg.fixed_size, self.cfg.fixed_size
else:
RH, RW = H // 8 * 8, W // 8 * 8
rgb_BCHW_HW8 = F.interpolate(
rgb_BCHW, (RH, RW), mode="bilinear", align_corners=False
)
latents = self.encode_images(rgb_BCHW_HW8)
image_cond = self.prepare_image_cond(cond_rgb)
image_cond = F.interpolate(
image_cond, (RH, RW), mode="bilinear", align_corners=False
)
temp = torch.zeros(1).to(rgb.device)
azimuth = kwargs.get("azimuth", temp)
camera_distance = kwargs.get("camera_distance", temp)
view_dependent_prompt = kwargs.get("view_dependent_prompt", False)
text_embeddings = prompt_utils.get_text_embeddings(temp, azimuth, camera_distance, view_dependent_prompt) # FIXME: change to view-conditioned prompt
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
guidance_out = {}
if self.cfg.use_sds:
grad = self.compute_grad_sds(text_embeddings, latents, image_cond, t)
grad = torch.nan_to_num(grad)
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
target = (latents - grad).detach()
loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out.update(
{
"loss_sds": loss_sds,
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
)
if self.cfg.use_du:
grad = self.compute_grad_du(
latents, rgb_BCHW_HW8, image_cond, cond_rgb, text_embeddings, mask, **kwargs
)
guidance_out.update(grad)
return guidance_out
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)
self.global_step = global_step

View File

@@ -0,0 +1,454 @@
import os
from dataclasses import dataclass
import cv2
import numpy as np
import torch
import torch.nn.functional as F
from controlnet_aux import CannyDetector, NormalBaeDetector
from diffusers import ControlNetModel, DDIMScheduler, StableDiffusionControlNetPipeline, DPMSolverMultistepScheduler
from diffusers.utils.import_utils import is_xformers_available
from tqdm import tqdm
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, parse_version
from threestudio.utils.typing import *
@threestudio.register("stable-diffusion-controlnet-reg-guidance")
class ControlNetGuidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
cache_dir: Optional[str] = None
local_files_only: Optional[bool] = False
pretrained_model_name_or_path: str = "SG161222/Realistic_Vision_V2.0"
ddim_scheduler_name_or_path: str = "runwayml/stable-diffusion-v1-5"
control_type: str = "normal" # normal/canny
enable_memory_efficient_attention: bool = False
enable_sequential_cpu_offload: bool = False
enable_attention_slicing: bool = False
enable_channels_last_format: bool = False
guidance_scale: float = 7.5
condition_scale: float = 1.5
grad_clip: Optional[Any] = None
half_precision_weights: bool = True
min_step_percent: float = 0.02
max_step_percent: float = 0.98
diffusion_steps: int = 20
use_sds: bool = False
# Canny threshold
canny_lower_bound: int = 50
canny_upper_bound: int = 100
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading ControlNet ...")
self.weights_dtype = torch.float16 if self.cfg.half_precision_weights else torch.float32
self.preprocessor, controlnet_name_or_path = self.get_preprocessor_and_controlnet()
pipe_kwargs = self.configure_pipeline()
self.load_models(pipe_kwargs, controlnet_name_or_path)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.scheduler.set_timesteps(self.cfg.diffusion_steps)
self.pipe.scheduler = DPMSolverMultistepScheduler.from_config(self.pipe.scheduler.config)
self.scheduler = self.pipe.scheduler
self.check_memory_efficiency_conditions()
self.set_min_max_steps()
self.alphas = self.scheduler.alphas_cumprod.to(self.device)
self.grad_clip_val = None
threestudio.info(f"Loaded ControlNet!")
def get_preprocessor_and_controlnet(self):
if self.cfg.control_type in ("normal", "input_normal"):
if self.cfg.pretrained_model_name_or_path == "SG161222/Realistic_Vision_V2.0":
controlnet_name_or_path = "lllyasviel/control_v11p_sd15_normalbae"
else:
controlnet_name_or_path = "thibaud/controlnet-sd21-normalbae-diffusers"
preprocessor = NormalBaeDetector.from_pretrained("lllyasviel/Annotators", cache_dir=self.cfg.cache_dir)
preprocessor.model.to(self.device)
elif self.cfg.control_type == "canny" or self.cfg.control_type == "canny2":
controlnet_name_or_path = self.get_canny_controlnet()
preprocessor = CannyDetector()
else:
raise ValueError(f"Unknown control type: {self.cfg.control_type}")
return preprocessor, controlnet_name_or_path
def get_canny_controlnet(self):
if self.cfg.control_type == "canny":
return "lllyasviel/control_v11p_sd15_canny"
elif self.cfg.control_type == "canny2":
return "thepowefuldeez/sd21-controlnet-canny"
def configure_pipeline(self):
return {
"safety_checker": None,
"feature_extractor": None,
"requires_safety_checker": False,
"torch_dtype": self.weights_dtype,
"cache_dir": self.cfg.cache_dir,
"local_files_only": self.cfg.local_files_only
}
def load_models(self, pipe_kwargs, controlnet_name_or_path):
controlnet = ControlNetModel.from_pretrained(
controlnet_name_or_path,
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
local_files_only=self.cfg.local_files_only
)
self.pipe = StableDiffusionControlNetPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, controlnet=controlnet, **pipe_kwargs
).to(self.device)
self.scheduler = DDIMScheduler.from_pretrained(
self.cfg.ddim_scheduler_name_or_path,
subfolder="scheduler",
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
local_files_only=self.cfg.local_files_only
)
self.vae = self.pipe.vae.eval()
self.unet = self.pipe.unet.eval()
self.controlnet = self.pipe.controlnet.eval()
def check_memory_efficiency_conditions(self):
if self.cfg.enable_memory_efficient_attention:
self.memory_efficiency_status()
if self.cfg.enable_sequential_cpu_offload:
self.pipe.enable_sequential_cpu_offload()
if self.cfg.enable_attention_slicing:
self.pipe.enable_attention_slicing(1)
if self.cfg.enable_channels_last_format:
self.pipe.unet.to(memory_format=torch.channels_last)
def memory_efficiency_status(self):
if parse_version(torch.__version__) >= parse_version("2"):
threestudio.info("PyTorch2.0 uses memory efficient attention by default.")
elif not is_xformers_available():
threestudio.warn("xformers is not available, memory efficient attention is not enabled.")
else:
self.pipe.enable_xformers_memory_efficient_attention()
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def forward_controlnet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
image_cond: Float[Tensor, "..."],
condition_scale: float,
encoder_hidden_states: Float[Tensor, "..."],
) -> Float[Tensor, "..."]:
return self.controlnet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
controlnet_cond=image_cond.to(self.weights_dtype),
conditioning_scale=condition_scale,
return_dict=False,
)
@torch.cuda.amp.autocast(enabled=False)
def forward_control_unet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
cross_attention_kwargs,
down_block_additional_residuals,
mid_block_additional_residual,
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
return self.unet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
cross_attention_kwargs=cross_attention_kwargs,
down_block_additional_residuals=down_block_additional_residuals,
mid_block_additional_residual=mid_block_additional_residual,
).sample.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def encode_images(
self, imgs: Float[Tensor, "B 3 512 512"]
) -> Float[Tensor, "B 4 64 64"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
posterior = self.vae.encode(imgs.to(self.weights_dtype)).latent_dist
latents = posterior.sample() * self.vae.config.scaling_factor
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def encode_cond_images(
self, imgs: Float[Tensor, "B 3 512 512"]
) -> Float[Tensor, "B 4 64 64"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
posterior = self.vae.encode(imgs.to(self.weights_dtype)).latent_dist
latents = posterior.mode()
uncond_image_latents = torch.zeros_like(latents)
latents = torch.cat([latents, latents, uncond_image_latents], dim=0)
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def decode_latents(
self,
latents: Float[Tensor, "B 4 H W"],
latent_height: int = 64,
latent_width: int = 64,
) -> Float[Tensor, "B 3 512 512"]:
input_dtype = latents.dtype
latents = F.interpolate(
latents, (latent_height, latent_width), mode="bilinear", align_corners=False
)
latents = 1 / self.vae.config.scaling_factor * latents
image = self.vae.decode(latents.to(self.weights_dtype)).sample
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
def edit_latents(
self,
text_embeddings: Float[Tensor, "BB 77 768"],
latents: Float[Tensor, "B 4 64 64"],
image_cond: Float[Tensor, "B 3 512 512"],
t: Int[Tensor, "B"],
mask=None
) -> Float[Tensor, "B 4 64 64"]:
batch_size = t.shape[0]
self.scheduler.set_timesteps(num_inference_steps=self.cfg.diffusion_steps)
init_timestep = max(1, min(int(self.cfg.diffusion_steps * t[0].item() / self.num_train_timesteps), self.cfg.diffusion_steps))
t_start = max(self.cfg.diffusion_steps - init_timestep, 0)
latent_timestep = self.scheduler.timesteps[t_start : t_start + 1].repeat(batch_size)
B, _, DH, DW = latents.shape
origin_latents = latents.clone()
if mask is not None:
mask = F.interpolate(mask, (DH, DW), mode="bilinear", antialias=True)
with torch.no_grad():
# sections of code used from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py
noise = torch.randn_like(latents)
latents = self.scheduler.add_noise(latents, noise, latent_timestep) # type: ignore
threestudio.debug("Start editing...")
for i, step in enumerate(range(t_start, self.cfg.diffusion_steps)):
timestep = self.scheduler.timesteps[step]
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# pred noise
latent_model_input = torch.cat([latents] * 2)
(
down_block_res_samples,
mid_block_res_sample,
) = self.forward_controlnet(
latent_model_input,
timestep,
encoder_hidden_states=text_embeddings,
image_cond=image_cond,
condition_scale=self.cfg.condition_scale,
)
noise_pred = self.forward_control_unet(
latent_model_input,
timestep,
encoder_hidden_states=text_embeddings,
cross_attention_kwargs=None,
down_block_additional_residuals=down_block_res_samples,
mid_block_additional_residual=mid_block_res_sample,
)
# perform classifier-free guidance
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
if mask is not None:
noise_pred = noise_pred * mask + (1-mask) * noise
latents = self.scheduler.step(noise_pred, timestep, latents).prev_sample
threestudio.debug("Editing finished.")
return latents
def prepare_image_cond(self, cond_rgb: Float[Tensor, "B H W C"]):
if self.cfg.control_type == "normal":
cond_rgb = (
(cond_rgb[0].detach().cpu().numpy() * 255).astype(np.uint8).copy()
)
detected_map = self.preprocessor(cond_rgb)
control = (
torch.from_numpy(np.array(detected_map)).float().to(self.device) / 255.0
)
control = control.unsqueeze(0)
control = control.permute(0, 3, 1, 2)
elif self.cfg.control_type == "canny" or self.cfg.control_type == "canny2":
cond_rgb = (
(cond_rgb[0].detach().cpu().numpy() * 255).astype(np.uint8).copy()
)
blurred_img = cv2.blur(cond_rgb, ksize=(5, 5))
detected_map = self.preprocessor(
blurred_img, self.cfg.canny_lower_bound, self.cfg.canny_upper_bound
)
control = (
torch.from_numpy(np.array(detected_map)).float().to(self.device) / 255.0
)
control = control.unsqueeze(-1).repeat(1, 1, 3)
control = control.unsqueeze(0)
control = control.permute(0, 3, 1, 2)
elif self.cfg.control_type == "input_normal":
cond_rgb[..., 0] = (
1 - cond_rgb[..., 0]
) # Flip the sign on the x-axis to match bae system
control = cond_rgb.permute(0, 3, 1, 2)
else:
raise ValueError(f"Unknown control type: {self.cfg.control_type}")
return F.interpolate(control, (512, 512), mode="bilinear", align_corners=False)
def compute_grad_sds(
self,
text_embeddings: Float[Tensor, "BB 77 768"],
latents: Float[Tensor, "B 4 64 64"],
image_cond: Float[Tensor, "B 3 512 512"],
t: Int[Tensor, "B"],
):
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# pred noise
latent_model_input = torch.cat([latents_noisy] * 2)
down_block_res_samples, mid_block_res_sample = self.forward_controlnet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
image_cond=image_cond,
condition_scale=self.cfg.condition_scale,
)
noise_pred = self.forward_control_unet(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
cross_attention_kwargs=None,
down_block_additional_residuals=down_block_res_samples,
mid_block_additional_residual=mid_block_res_sample,
)
# perform classifier-free guidance
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
w = (1 - self.alphas[t]).view(-1, 1, 1, 1)
grad = w * (noise_pred - noise)
return grad
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
cond_rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
mask: Float[Tensor, "B H W C"],
**kwargs,
):
batch_size, H, W, _ = rgb.shape
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 64 64"]
rgb_BCHW_512 = F.interpolate(
rgb_BCHW, (512, 512), mode="bilinear", align_corners=False
)
latents = self.encode_images(rgb_BCHW_512)
image_cond = self.prepare_image_cond(cond_rgb)
temp = torch.zeros(1).to(rgb.device)
text_embeddings = prompt_utils.get_text_embeddings(temp, temp, temp, False)
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
if self.cfg.use_sds:
grad = self.compute_grad_sds(text_embeddings, latents, image_cond, t)
grad = torch.nan_to_num(grad)
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
target = (latents - grad).detach()
loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
return {
"loss_sds": loss_sds,
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
else:
if mask is not None: mask = mask.permute(0, 3, 1, 2)
edit_latents = self.edit_latents(text_embeddings, latents, image_cond, t, mask)
edit_images = self.decode_latents(edit_latents)
edit_images = F.interpolate(edit_images, (H, W), mode="bilinear")
return {"edit_images": edit_images.permute(0, 2, 3, 1),
"edit_latents": edit_latents}
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)
if __name__ == "__main__":
from threestudio.utils.config import ExperimentConfig, load_config
from threestudio.utils.typing import Optional
cfg = load_config("configs/experimental/controlnet-normal.yaml")
guidance = threestudio.find(cfg.system.guidance_type)(cfg.system.guidance)
prompt_processor = threestudio.find(cfg.system.prompt_processor_type)(
cfg.system.prompt_processor
)
rgb_image = cv2.imread("assets/face.jpg")[:, :, ::-1].copy() / 255
rgb_image = cv2.resize(rgb_image, (512, 512))
rgb_image = torch.FloatTensor(rgb_image).unsqueeze(0).to(guidance.device)
prompt_utils = prompt_processor()
guidance_out = guidance(rgb_image, rgb_image, prompt_utils)
edit_image = (
(guidance_out["edit_images"][0].detach().cpu().clip(0, 1).numpy() * 255)
.astype(np.uint8)[:, :, ::-1]
.copy()
)
os.makedirs(".threestudio_cache", exist_ok=True)
cv2.imwrite(".threestudio_cache/edit_image.jpg", edit_image)

View File

@@ -0,0 +1,582 @@
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers import IFPipeline, DDPMScheduler
from diffusers.utils.import_utils import is_xformers_available
from tqdm import tqdm
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, parse_version
from threestudio.utils.ops import perpendicular_component
from threestudio.utils.typing import *
@threestudio.register("deep-floyd-guidance")
class DeepFloydGuidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
cache_dir: Optional[str] = None
local_files_only: Optional[bool] = False
pretrained_model_name_or_path: str = "DeepFloyd/IF-I-XL-v1.0"
# FIXME: xformers error
enable_memory_efficient_attention: bool = False
enable_sequential_cpu_offload: bool = False
enable_attention_slicing: bool = False
enable_channels_last_format: bool = True
guidance_scale: float = 20.0
grad_clip: Optional[
Any
] = None # field(default_factory=lambda: [0, 2.0, 8.0, 1000])
time_prior: Optional[Any] = None # [w1,w2,s1,s2]
half_precision_weights: bool = True
min_step_percent: float = 0.02
max_step_percent: float = 0.98
weighting_strategy: str = "sds"
view_dependent_prompting: bool = True
"""Maximum number of batch items to evaluate guidance for (for debugging) and to save on disk. -1 means save all items."""
max_items_eval: int = 4
lora_weights_path: Optional[str] = None
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading Deep Floyd ...")
self.weights_dtype = (
torch.float16 if self.cfg.half_precision_weights else torch.float32
)
# Create model
self.pipe = IFPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path,
text_encoder=None,
safety_checker=None,
watermarker=None,
feature_extractor=None,
requires_safety_checker=False,
variant="fp16" if self.cfg.half_precision_weights else None,
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
local_files_only=self.cfg.local_files_only
).to(self.device)
# Load lora weights
if self.cfg.lora_weights_path is not None:
self.pipe.load_lora_weights(self.cfg.lora_weights_path)
self.pipe.scheduler = self.pipe.scheduler.__class__.from_config(self.pipe.scheduler.config, variance_type="fixed_small")
if self.cfg.enable_memory_efficient_attention:
if parse_version(torch.__version__) >= parse_version("2"):
threestudio.info(
"PyTorch2.0 uses memory efficient attention by default."
)
elif not is_xformers_available():
threestudio.warn(
"xformers is not available, memory efficient attention is not enabled."
)
else:
threestudio.warn(
f"Use DeepFloyd with xformers may raise error, see https://github.com/deep-floyd/IF/issues/52 to track this problem."
)
self.pipe.enable_xformers_memory_efficient_attention()
if self.cfg.enable_sequential_cpu_offload:
self.pipe.enable_sequential_cpu_offload()
if self.cfg.enable_attention_slicing:
self.pipe.enable_attention_slicing(1)
if self.cfg.enable_channels_last_format:
self.pipe.unet.to(memory_format=torch.channels_last)
self.unet = self.pipe.unet.eval()
for p in self.unet.parameters():
p.requires_grad_(False)
self.scheduler = self.pipe.scheduler
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.set_min_max_steps() # set to default value
if self.cfg.time_prior is not None:
m1, m2, s1, s2 = self.cfg.time_prior
weights = torch.cat(
(
torch.exp(
-((torch.arange(self.num_train_timesteps, m1, -1) - m1) ** 2)
/ (2 * s1**2)
),
torch.ones(m1 - m2 + 1),
torch.exp(
-((torch.arange(m2 - 1, 0, -1) - m2) ** 2) / (2 * s2**2)
),
)
)
weights = weights / torch.sum(weights)
self.time_prior_acc_weights = torch.cumsum(weights, dim=0)
self.alphas: Float[Tensor, "..."] = self.scheduler.alphas_cumprod.to(
self.device
)
self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(
self.device
)
self.grad_clip_val: Optional[float] = None
threestudio.info(f"Loaded Deep Floyd!")
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def forward_unet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
return self.unet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
).sample.to(input_dtype)
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
current_step_ratio=None,
mask: Float[Tensor, "B H W 1"] = None,
rgb_as_latents=False,
guidance_eval=False,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
if mask is not None:
mask = mask.permute(0, 3, 1, 2)
mask = F.interpolate(
mask, (64, 64), mode="bilinear", align_corners=False
)
assert rgb_as_latents == False, f"No latent space in {self.__class__.__name__}"
rgb_BCHW = rgb_BCHW * 2.0 - 1.0 # scale to [-1, 1] to match the diffusion range
latents = F.interpolate(
rgb_BCHW, (64, 64), mode="bilinear", align_corners=False
)
if self.cfg.time_prior is not None:
time_index = torch.where(
(self.time_prior_acc_weights - current_step_ratio) > 0
)[0][0]
if time_index == 0 or torch.abs(
self.time_prior_acc_weights[time_index] - current_step_ratio
) < torch.abs(
self.time_prior_acc_weights[time_index - 1] - current_step_ratio
):
t = self.num_train_timesteps - time_index
else:
t = self.num_train_timesteps - time_index + 1
t = torch.clip(t, self.min_step, self.max_step + 1)
t = torch.full((batch_size,), t, dtype=torch.long, device=self.device)
else:
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
if prompt_utils.use_perp_neg:
(
text_embeddings,
neg_guidance_weights,
) = prompt_utils.get_text_embeddings_perp_neg(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
with torch.no_grad():
noise = torch.randn_like(latents)
latents_noisy = self.scheduler.add_noise(latents, noise, t)
if mask is not None:
latents_noisy = (1 - mask) * latents + mask * latents_noisy
latent_model_input = torch.cat([latents_noisy] * 4, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 4),
encoder_hidden_states=text_embeddings,
) # (4B, 6, 64, 64)
noise_pred_text, _ = noise_pred[:batch_size].split(3, dim=1)
noise_pred_uncond, _ = noise_pred[batch_size : batch_size * 2].split(
3, dim=1
)
noise_pred_neg, _ = noise_pred[batch_size * 2 :].split(3, dim=1)
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
neg_guidance_weights = None
text_embeddings = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
if mask is not None:
latents_noisy = (1 - mask) * latents + mask * latents_noisy
# pred noise
latent_model_input = torch.cat([latents_noisy] * 2, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 2),
encoder_hidden_states=text_embeddings,
) # (2B, 6, 64, 64)
# perform guidance (high scale from paper!)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred_text, predicted_variance = noise_pred_text.split(3, dim=1)
noise_pred_uncond, _ = noise_pred_uncond.split(3, dim=1)
noise_pred = noise_pred_text + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
"""
# thresholding, experimental
if self.cfg.thresholding:
assert batch_size == 1
noise_pred = torch.cat([noise_pred, predicted_variance], dim=1)
noise_pred = custom_ddpm_step(self.scheduler,
noise_pred, int(t.item()), latents_noisy, **self.pipe.prepare_extra_step_kwargs(None, 0.0)
)
"""
if self.cfg.weighting_strategy == "sds":
# w(t), sigma_t^2
w = (1 - self.alphas[t]).view(-1, 1, 1, 1)
elif self.cfg.weighting_strategy == "uniform":
w = 1
elif self.cfg.weighting_strategy == "fantasia3d":
w = (self.alphas[t] ** 0.5 * (1 - self.alphas[t])).view(-1, 1, 1, 1)
else:
raise ValueError(
f"Unknown weighting strategy: {self.cfg.weighting_strategy}"
)
grad = w * (noise_pred - noise)
grad = torch.nan_to_num(grad)
# clip grad for stable training?
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# loss = SpecifyGradient.apply(latents, grad)
# SpecifyGradient is not straghtforward, use a reparameterization trick instead
target = (latents - grad).detach()
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
loss_sd = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sd,
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
# # FIXME: Visualize inpainting results
# self.scheduler.set_timesteps(20)
# latents = latents_noisy
# for t in tqdm(self.scheduler.timesteps):
# # pred noise
# noise_pred = self.get_noise_pred(
# latents, t, text_embeddings, prompt_utils.use_perp_neg, None
# )
# # get prev latent
# prev_latents = latents
# latents = self.scheduler.step(noise_pred, t, latents)["prev_sample"]
# if mask is not None:
# latents = (1 - mask) * prev_latents + mask * latents
# denoised_img = (latents / 2 + 0.5).permute(0, 2, 3, 1)
# guidance_out.update(
# {"denoised_img": denoised_img}
# )
if guidance_eval:
guidance_eval_utils = {
"use_perp_neg": prompt_utils.use_perp_neg,
"neg_guidance_weights": neg_guidance_weights,
"text_embeddings": text_embeddings,
"t_orig": t,
"latents_noisy": latents_noisy,
"noise_pred": torch.cat([noise_pred, predicted_variance], dim=1),
}
guidance_eval_out = self.guidance_eval(**guidance_eval_utils)
texts = []
for n, e, a, c in zip(
guidance_eval_out["noise_levels"], elevation, azimuth, camera_distances
):
texts.append(
f"n{n:.02f}\ne{e.item():.01f}\na{a.item():.01f}\nc{c.item():.02f}"
)
guidance_eval_out.update({"texts": texts})
guidance_out.update({"eval": guidance_eval_out})
return guidance_out
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_noise_pred(
self,
latents_noisy,
t,
text_embeddings,
use_perp_neg=False,
neg_guidance_weights=None,
):
batch_size = latents_noisy.shape[0]
if use_perp_neg:
latent_model_input = torch.cat([latents_noisy] * 4, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t.reshape(1)] * 4).to(self.device),
encoder_hidden_states=text_embeddings,
) # (4B, 6, 64, 64)
noise_pred_text, _ = noise_pred[:batch_size].split(3, dim=1)
noise_pred_uncond, _ = noise_pred[batch_size : batch_size * 2].split(
3, dim=1
)
noise_pred_neg, _ = noise_pred[batch_size * 2 :].split(3, dim=1)
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
latent_model_input = torch.cat([latents_noisy] * 2, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t.reshape(1)] * 2).to(self.device),
encoder_hidden_states=text_embeddings,
) # (2B, 6, 64, 64)
# perform guidance (high scale from paper!)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred_text, predicted_variance = noise_pred_text.split(3, dim=1)
noise_pred_uncond, _ = noise_pred_uncond.split(3, dim=1)
noise_pred = noise_pred_text + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
return torch.cat([noise_pred, predicted_variance], dim=1)
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def guidance_eval(
self,
t_orig,
text_embeddings,
latents_noisy,
noise_pred,
use_perp_neg=False,
neg_guidance_weights=None,
):
# use only 50 timesteps, and find nearest of those to t
self.scheduler.set_timesteps(50)
self.scheduler.timesteps_gpu = self.scheduler.timesteps.to(self.device)
bs = (
min(self.cfg.max_items_eval, latents_noisy.shape[0])
if self.cfg.max_items_eval > 0
else latents_noisy.shape[0]
) # batch size
large_enough_idxs = self.scheduler.timesteps_gpu.expand([bs, -1]) > t_orig[
:bs
].unsqueeze(
-1
) # sized [bs,50] > [bs,1]
idxs = torch.min(large_enough_idxs, dim=1)[1]
t = self.scheduler.timesteps_gpu[idxs]
fracs = list((t / self.scheduler.config.num_train_timesteps).cpu().numpy())
imgs_noisy = (latents_noisy[:bs] / 2 + 0.5).permute(0, 2, 3, 1)
# get prev latent
latents_1step = []
pred_1orig = []
for b in range(bs):
step_output = self.scheduler.step(
noise_pred[b : b + 1], t[b], latents_noisy[b : b + 1]
)
latents_1step.append(step_output["prev_sample"])
pred_1orig.append(step_output["pred_original_sample"])
latents_1step = torch.cat(latents_1step)
pred_1orig = torch.cat(pred_1orig)
imgs_1step = (latents_1step / 2 + 0.5).permute(0, 2, 3, 1)
imgs_1orig = (pred_1orig / 2 + 0.5).permute(0, 2, 3, 1)
latents_final = []
for b, i in enumerate(idxs):
latents = latents_1step[b : b + 1]
text_emb = (
text_embeddings[
[b, b + len(idxs), b + 2 * len(idxs), b + 3 * len(idxs)], ...
]
if use_perp_neg
else text_embeddings[[b, b + len(idxs)], ...]
)
neg_guid = neg_guidance_weights[b : b + 1] if use_perp_neg else None
for t in tqdm(self.scheduler.timesteps[i + 1 :], leave=False):
# pred noise
noise_pred = self.get_noise_pred(
latents, t, text_emb, use_perp_neg, neg_guid
)
# get prev latent
latents = self.scheduler.step(noise_pred, t, latents)["prev_sample"]
latents_final.append(latents)
latents_final = torch.cat(latents_final)
imgs_final = (latents_final / 2 + 0.5).permute(0, 2, 3, 1)
return {
"bs": bs,
"noise_levels": fracs,
"imgs_noisy": imgs_noisy,
"imgs_1step": imgs_1step,
"imgs_1orig": imgs_1orig,
"imgs_final": imgs_final,
}
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)
"""
# used by thresholding, experimental
def custom_ddpm_step(ddpm, model_output: torch.FloatTensor, timestep: int, sample: torch.FloatTensor, generator=None, return_dict: bool = True):
self = ddpm
t = timestep
prev_t = self.previous_timestep(t)
if model_output.shape[1] == sample.shape[1] * 2 and self.variance_type in ["learned", "learned_range"]:
model_output, predicted_variance = torch.split(model_output, sample.shape[1], dim=1)
else:
predicted_variance = None
# 1. compute alphas, betas
alpha_prod_t = self.alphas_cumprod[t].item()
alpha_prod_t_prev = self.alphas_cumprod[prev_t].item() if prev_t >= 0 else 1.0
beta_prod_t = 1 - alpha_prod_t
beta_prod_t_prev = 1 - alpha_prod_t_prev
current_alpha_t = alpha_prod_t / alpha_prod_t_prev
current_beta_t = 1 - current_alpha_t
# 2. compute predicted original sample from predicted noise also called
# "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf
if self.config.prediction_type == "epsilon":
pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5)
elif self.config.prediction_type == "sample":
pred_original_sample = model_output
elif self.config.prediction_type == "v_prediction":
pred_original_sample = (alpha_prod_t**0.5) * sample - (beta_prod_t**0.5) * model_output
else:
raise ValueError(
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample` or"
" `v_prediction` for the DDPMScheduler."
)
# 3. Clip or threshold "predicted x_0"
if self.config.thresholding:
pred_original_sample = self._threshold_sample(pred_original_sample)
elif self.config.clip_sample:
pred_original_sample = pred_original_sample.clamp(
-self.config.clip_sample_range, self.config.clip_sample_range
)
noise_thresholded = (sample - (alpha_prod_t ** 0.5) * pred_original_sample) / (beta_prod_t ** 0.5)
return noise_thresholded
"""
if __name__ == '__main__':
from threestudio.utils.config import load_config
import pytorch_lightning as pl
import numpy as np
import os
import cv2
cfg = load_config("configs/debugging/deepfloyd.yaml")
guidance = threestudio.find(cfg.system.guidance_type)(cfg.system.guidance)
prompt_processor = threestudio.find(cfg.system.prompt_processor_type)(cfg.system.prompt_processor)
prompt_utils = prompt_processor()
temp = torch.zeros(1).to(guidance.device)
# rgb_image = guidance.sample(prompt_utils, temp, temp, temp, seed=cfg.seed)
# rgb_image = (rgb_image[0].detach().cpu().clip(0, 1).numpy()*255).astype(np.uint8)[:, :, ::-1].copy()
# os.makedirs('.threestudio_cache', exist_ok=True)
# cv2.imwrite('.threestudio_cache/diffusion_image.jpg', rgb_image)
### inpaint
rgb_image = cv2.imread("assets/test.jpg")[:, :, ::-1].copy() / 255
mask_image = cv2.imread("assets/mask.png")[:, :, :1].copy() / 255
rgb_image = cv2.resize(rgb_image, (512, 512))
mask_image = cv2.resize(mask_image, (512, 512)).reshape(512, 512, 1)
rgb_image = torch.FloatTensor(rgb_image).unsqueeze(0).to(guidance.device)
mask_image = torch.FloatTensor(mask_image).unsqueeze(0).to(guidance.device)
guidance_out = guidance(rgb_image, prompt_utils, temp, temp, temp, mask=mask_image)
edit_image = (
(guidance_out["denoised_img"][0].detach().cpu().clip(0, 1).numpy() * 255)
.astype(np.uint8)[:, :, ::-1]
.copy()
)
os.makedirs(".threestudio_cache", exist_ok=True)
cv2.imwrite(".threestudio_cache/edit_image.jpg", edit_image)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,632 @@
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers import DDIMScheduler, DDPMScheduler, StableDiffusionPipeline
from diffusers.utils.import_utils import is_xformers_available
from tqdm import tqdm
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, cleanup, parse_version
from threestudio.utils.ops import perpendicular_component
from threestudio.utils.typing import *
@threestudio.register("stable-diffusion-guidance")
class StableDiffusionGuidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
cache_dir: Optional[str] = None
local_files_only: Optional[bool] = False
pretrained_model_name_or_path: str = "runwayml/stable-diffusion-v1-5"
enable_memory_efficient_attention: bool = False
enable_sequential_cpu_offload: bool = False
enable_attention_slicing: bool = False
enable_channels_last_format: bool = False
guidance_scale: float = 100.0
grad_clip: Optional[
Any
] = None # field(default_factory=lambda: [0, 2.0, 8.0, 1000])
time_prior: Optional[Any] = None # [w1,w2,s1,s2]
half_precision_weights: bool = True
min_step_percent: float = 0.02
max_step_percent: float = 0.98
max_step_percent_annealed: float = 0.5
anneal_start_step: Optional[int] = None
use_sjc: bool = False
var_red: bool = True
weighting_strategy: str = "sds"
token_merging: bool = False
token_merging_params: Optional[dict] = field(default_factory=dict)
view_dependent_prompting: bool = True
"""Maximum number of batch items to evaluate guidance for (for debugging) and to save on disk. -1 means save all items."""
max_items_eval: int = 4
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading Stable Diffusion ...")
self.weights_dtype = (
torch.float16 if self.cfg.half_precision_weights else torch.float32
)
pipe_kwargs = {
"tokenizer": None,
"safety_checker": None,
"feature_extractor": None,
"requires_safety_checker": False,
"torch_dtype": self.weights_dtype,
"cache_dir": self.cfg.cache_dir,
"local_files_only": self.cfg.local_files_only
}
self.pipe = StableDiffusionPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path,
**pipe_kwargs,
).to(self.device)
if self.cfg.enable_memory_efficient_attention:
if parse_version(torch.__version__) >= parse_version("2"):
threestudio.info(
"PyTorch2.0 uses memory efficient attention by default."
)
elif not is_xformers_available():
threestudio.warn(
"xformers is not available, memory efficient attention is not enabled."
)
else:
self.pipe.enable_xformers_memory_efficient_attention()
if self.cfg.enable_sequential_cpu_offload:
self.pipe.enable_sequential_cpu_offload()
if self.cfg.enable_attention_slicing:
self.pipe.enable_attention_slicing(1)
if self.cfg.enable_channels_last_format:
self.pipe.unet.to(memory_format=torch.channels_last)
del self.pipe.text_encoder
cleanup()
# Create model
self.vae = self.pipe.vae.eval()
self.unet = self.pipe.unet.eval()
for p in self.vae.parameters():
p.requires_grad_(False)
for p in self.unet.parameters():
p.requires_grad_(False)
if self.cfg.token_merging:
import tomesd
tomesd.apply_patch(self.unet, **self.cfg.token_merging_params)
if self.cfg.use_sjc:
# score jacobian chaining use DDPM
self.scheduler = DDPMScheduler.from_pretrained(
self.cfg.pretrained_model_name_or_path,
subfolder="scheduler",
torch_dtype=self.weights_dtype,
beta_start=0.00085,
beta_end=0.0120,
beta_schedule="scaled_linear",
cache_dir=self.cfg.cache_dir,
)
else:
self.scheduler = DDIMScheduler.from_pretrained(
self.cfg.pretrained_model_name_or_path,
subfolder="scheduler",
torch_dtype=self.weights_dtype,
cache_dir=self.cfg.cache_dir,
local_files_only=self.cfg.local_files_only,
)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.set_min_max_steps() # set to default value
if self.cfg.time_prior is not None:
m1, m2, s1, s2 = self.cfg.time_prior
weights = torch.cat(
(
torch.exp(
-((torch.arange(self.num_train_timesteps, m1, -1) - m1) ** 2)
/ (2 * s1**2)
),
torch.ones(m1 - m2 + 1),
torch.exp(
-((torch.arange(m2 - 1, 0, -1) - m2) ** 2) / (2 * s2**2)
),
)
)
weights = weights / torch.sum(weights)
self.time_prior_acc_weights = torch.cumsum(weights, dim=0)
self.alphas: Float[Tensor, "..."] = self.scheduler.alphas_cumprod.to(
self.device
)
if self.cfg.use_sjc:
# score jacobian chaining need mu
self.us: Float[Tensor, "..."] = torch.sqrt((1 - self.alphas) / self.alphas)
self.grad_clip_val: Optional[float] = None
threestudio.info(f"Loaded Stable Diffusion!")
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def forward_unet(
self,
latents: Float[Tensor, "..."],
t: Float[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
return self.unet(
latents.to(self.weights_dtype),
t.to(self.weights_dtype),
encoder_hidden_states=encoder_hidden_states.to(self.weights_dtype),
).sample.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def encode_images(
self, imgs: Float[Tensor, "B 3 512 512"]
) -> Float[Tensor, "B 4 64 64"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
posterior = self.vae.encode(imgs.to(self.weights_dtype)).latent_dist
latents = posterior.sample() * self.vae.config.scaling_factor
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def decode_latents(
self,
latents: Float[Tensor, "B 4 H W"],
latent_height: int = 64,
latent_width: int = 64,
) -> Float[Tensor, "B 3 512 512"]:
input_dtype = latents.dtype
latents = F.interpolate(
latents, (latent_height, latent_width), mode="bilinear", align_corners=False
)
latents = 1 / self.vae.config.scaling_factor * latents
image = self.vae.decode(latents.to(self.weights_dtype)).sample
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
def compute_grad_sds(
self,
latents: Float[Tensor, "B 4 64 64"],
t: Int[Tensor, "B"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
):
batch_size = elevation.shape[0]
if prompt_utils.use_perp_neg:
(
text_embeddings,
neg_guidance_weights,
) = prompt_utils.get_text_embeddings_perp_neg(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
with torch.no_grad():
noise = torch.randn_like(latents)
latents_noisy = self.scheduler.add_noise(latents, noise, t)
latent_model_input = torch.cat([latents_noisy] * 4, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 4),
encoder_hidden_states=text_embeddings,
) # (4B, 3, 64, 64)
noise_pred_text = noise_pred[:batch_size]
noise_pred_uncond = noise_pred[batch_size : batch_size * 2]
noise_pred_neg = noise_pred[batch_size * 2 :]
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
neg_guidance_weights = None
text_embeddings = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# pred noise
latent_model_input = torch.cat([latents_noisy] * 2, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 2),
encoder_hidden_states=text_embeddings,
)
# perform guidance (high scale from paper!)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_text + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
if self.cfg.weighting_strategy == "sds":
# w(t), sigma_t^2
w = (1 - self.alphas[t]).view(-1, 1, 1, 1)
elif self.cfg.weighting_strategy == "uniform":
w = 1
elif self.cfg.weighting_strategy == "fantasia3d":
w = (self.alphas[t] ** 0.5 * (1 - self.alphas[t])).view(-1, 1, 1, 1)
else:
raise ValueError(
f"Unknown weighting strategy: {self.cfg.weighting_strategy}"
)
grad = w * (noise_pred - noise)
guidance_eval_utils = {
"use_perp_neg": prompt_utils.use_perp_neg,
"neg_guidance_weights": neg_guidance_weights,
"text_embeddings": text_embeddings,
"t_orig": t,
"latents_noisy": latents_noisy,
"noise_pred": noise_pred,
}
return grad, guidance_eval_utils
def compute_grad_sjc(
self,
latents: Float[Tensor, "B 4 64 64"],
t: Int[Tensor, "B"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
):
batch_size = elevation.shape[0]
sigma = self.us[t]
sigma = sigma.view(-1, 1, 1, 1)
if prompt_utils.use_perp_neg:
(
text_embeddings,
neg_guidance_weights,
) = prompt_utils.get_text_embeddings_perp_neg(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
with torch.no_grad():
noise = torch.randn_like(latents)
y = latents
zs = y + sigma * noise
scaled_zs = zs / torch.sqrt(1 + sigma**2)
# pred noise
latent_model_input = torch.cat([scaled_zs] * 4, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 4),
encoder_hidden_states=text_embeddings,
) # (4B, 3, 64, 64)
noise_pred_text = noise_pred[:batch_size]
noise_pred_uncond = noise_pred[batch_size : batch_size * 2]
noise_pred_neg = noise_pred[batch_size * 2 :]
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
neg_guidance_weights = None
text_embeddings = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
y = latents
zs = y + sigma * noise
scaled_zs = zs / torch.sqrt(1 + sigma**2)
# pred noise
latent_model_input = torch.cat([scaled_zs] * 2, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t] * 2),
encoder_hidden_states=text_embeddings,
)
# perform guidance (high scale from paper!)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_text + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
Ds = zs - sigma * noise_pred
if self.cfg.var_red:
grad = -(Ds - y) / sigma
else:
grad = -(Ds - zs) / sigma
guidance_eval_utils = {
"use_perp_neg": prompt_utils.use_perp_neg,
"neg_guidance_weights": neg_guidance_weights,
"text_embeddings": text_embeddings,
"t_orig": t,
"latents_noisy": scaled_zs,
"noise_pred": noise_pred,
}
return grad, guidance_eval_utils
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
rgb_as_latents=False,
guidance_eval=False,
current_step_ratio=None,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 64 64"]
if rgb_as_latents:
latents = F.interpolate(
rgb_BCHW, (64, 64), mode="bilinear", align_corners=False
)
else:
rgb_BCHW_512 = F.interpolate(
rgb_BCHW, (512, 512), mode="bilinear", align_corners=False
)
# encode image into latents with vae
latents = self.encode_images(rgb_BCHW_512)
if self.cfg.time_prior is not None:
time_index = torch.where(
(self.time_prior_acc_weights - current_step_ratio) > 0
)[0][0]
if time_index == 0 or torch.abs(
self.time_prior_acc_weights[time_index] - current_step_ratio
) < torch.abs(
self.time_prior_acc_weights[time_index - 1] - current_step_ratio
):
t = self.num_train_timesteps - time_index
else:
t = self.num_train_timesteps - time_index + 1
t = torch.clip(t, self.min_step, self.max_step + 1)
t = torch.full((batch_size,), t, dtype=torch.long, device=self.device)
else:
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
if self.cfg.use_sjc:
grad, guidance_eval_utils = self.compute_grad_sjc(
latents, t, prompt_utils, elevation, azimuth, camera_distances
)
else:
grad, guidance_eval_utils = self.compute_grad_sds(
latents, t, prompt_utils, elevation, azimuth, camera_distances
)
grad = torch.nan_to_num(grad)
# clip grad for stable training?
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# loss = SpecifyGradient.apply(latents, grad)
# SpecifyGradient is not straghtforward, use a reparameterization trick instead
target = (latents - grad).detach()
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sds,
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
if guidance_eval:
guidance_eval_out = self.guidance_eval(**guidance_eval_utils)
texts = []
for n, e, a, c in zip(
guidance_eval_out["noise_levels"], elevation, azimuth, camera_distances
):
texts.append(
f"n{n:.02f}\ne{e.item():.01f}\na{a.item():.01f}\nc{c.item():.02f}"
)
guidance_eval_out.update({"texts": texts})
guidance_out.update({"eval": guidance_eval_out})
return guidance_out
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_noise_pred(
self,
latents_noisy,
t,
text_embeddings,
use_perp_neg=False,
neg_guidance_weights=None,
):
batch_size = latents_noisy.shape[0]
if use_perp_neg:
# pred noise
latent_model_input = torch.cat([latents_noisy] * 4, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t.reshape(1)] * 4).to(self.device),
encoder_hidden_states=text_embeddings,
) # (4B, 3, 64, 64)
noise_pred_text = noise_pred[:batch_size]
noise_pred_uncond = noise_pred[batch_size : batch_size * 2]
noise_pred_neg = noise_pred[batch_size * 2 :]
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
# pred noise
latent_model_input = torch.cat([latents_noisy] * 2, dim=0)
noise_pred = self.forward_unet(
latent_model_input,
torch.cat([t.reshape(1)] * 2).to(self.device),
encoder_hidden_states=text_embeddings,
)
# perform guidance (high scale from paper!)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_text + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
return noise_pred
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def guidance_eval(
self,
t_orig,
text_embeddings,
latents_noisy,
noise_pred,
use_perp_neg=False,
neg_guidance_weights=None,
):
# use only 50 timesteps, and find nearest of those to t
self.scheduler.set_timesteps(50)
self.scheduler.timesteps_gpu = self.scheduler.timesteps.to(self.device)
bs = (
min(self.cfg.max_items_eval, latents_noisy.shape[0])
if self.cfg.max_items_eval > 0
else latents_noisy.shape[0]
) # batch size
large_enough_idxs = self.scheduler.timesteps_gpu.expand([bs, -1]) > t_orig[
:bs
].unsqueeze(
-1
) # sized [bs,50] > [bs,1]
idxs = torch.min(large_enough_idxs, dim=1)[1]
t = self.scheduler.timesteps_gpu[idxs]
fracs = list((t / self.scheduler.config.num_train_timesteps).cpu().numpy())
imgs_noisy = self.decode_latents(latents_noisy[:bs]).permute(0, 2, 3, 1)
# get prev latent
latents_1step = []
pred_1orig = []
for b in range(bs):
step_output = self.scheduler.step(
noise_pred[b : b + 1], t[b], latents_noisy[b : b + 1], eta=1
)
latents_1step.append(step_output["prev_sample"])
pred_1orig.append(step_output["pred_original_sample"])
latents_1step = torch.cat(latents_1step)
pred_1orig = torch.cat(pred_1orig)
imgs_1step = self.decode_latents(latents_1step).permute(0, 2, 3, 1)
imgs_1orig = self.decode_latents(pred_1orig).permute(0, 2, 3, 1)
latents_final = []
for b, i in enumerate(idxs):
latents = latents_1step[b : b + 1]
text_emb = (
text_embeddings[
[b, b + len(idxs), b + 2 * len(idxs), b + 3 * len(idxs)], ...
]
if use_perp_neg
else text_embeddings[[b, b + len(idxs)], ...]
)
neg_guid = neg_guidance_weights[b : b + 1] if use_perp_neg else None
for t in tqdm(self.scheduler.timesteps[i + 1 :], leave=False):
# pred noise
noise_pred = self.get_noise_pred(
latents, t, text_emb, use_perp_neg, neg_guid
)
# get prev latent
latents = self.scheduler.step(noise_pred, t, latents, eta=1)[
"prev_sample"
]
latents_final.append(latents)
latents_final = torch.cat(latents_final)
imgs_final = self.decode_latents(latents_final).permute(0, 2, 3, 1)
return {
"bs": bs,
"noise_levels": fracs,
"imgs_noisy": imgs_noisy,
"imgs_1step": imgs_1step,
"imgs_1orig": imgs_1orig,
"imgs_final": imgs_final,
}
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)

View File

@@ -0,0 +1,729 @@
import random
from contextlib import contextmanager
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers import (
AutoencoderKL,
ControlNetModel,
DDPMScheduler,
DPMSolverSinglestepScheduler,
StableDiffusionPipeline,
UNet2DConditionModel,
)
from diffusers.loaders import AttnProcsLayers
from diffusers.models.attention_processor import LoRAAttnProcessor
from diffusers.models.embeddings import TimestepEmbedding
from diffusers.utils.import_utils import is_xformers_available
from tqdm import tqdm
import threestudio
from threestudio.models.networks import ToDTypeWrapper
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseModule
from threestudio.utils.misc import C, cleanup, enable_gradient, parse_version
from threestudio.utils.ops import perpendicular_component
from threestudio.utils.typing import *
@threestudio.register("stable-diffusion-unified-guidance")
class StableDiffusionUnifiedGuidance(BaseModule):
@dataclass
class Config(BaseModule.Config):
cache_dir: Optional[str] = None
local_files_only: Optional[bool] = False
# guidance type, in ["sds", "vsd"]
guidance_type: str = "sds"
pretrained_model_name_or_path: str = "runwayml/stable-diffusion-v1-5"
guidance_scale: float = 100.0
weighting_strategy: str = "dreamfusion"
view_dependent_prompting: bool = True
min_step_percent: Any = 0.02
max_step_percent: Any = 0.98
grad_clip: Optional[Any] = None
return_rgb_1step_orig: bool = False
return_rgb_multistep_orig: bool = False
n_rgb_multistep_orig_steps: int = 4
# TODO
# controlnet
controlnet_model_name_or_path: Optional[str] = None
preprocessor: Optional[str] = None
control_scale: float = 1.0
# TODO
# lora
lora_model_name_or_path: Optional[str] = None
# efficiency-related configurations
half_precision_weights: bool = True
enable_memory_efficient_attention: bool = False
enable_sequential_cpu_offload: bool = False
enable_attention_slicing: bool = False
enable_channels_last_format: bool = False
token_merging: bool = False
token_merging_params: Optional[dict] = field(default_factory=dict)
# VSD configurations, only used when guidance_type is "vsd"
vsd_phi_model_name_or_path: Optional[str] = None
vsd_guidance_scale_phi: float = 1.0
vsd_use_lora: bool = True
vsd_lora_cfg_training: bool = False
vsd_lora_n_timestamp_samples: int = 1
vsd_use_camera_condition: bool = True
# camera condition type, in ["extrinsics", "mvp", "spherical"]
vsd_camera_condition_type: Optional[str] = "extrinsics"
cfg: Config
def configure(self) -> None:
self.min_step: Optional[int] = None
self.max_step: Optional[int] = None
self.grad_clip_val: Optional[float] = None
@dataclass
class NonTrainableModules:
pipe: StableDiffusionPipeline
pipe_phi: Optional[StableDiffusionPipeline] = None
controlnet: Optional[ControlNetModel] = None
self.weights_dtype = (
torch.float16 if self.cfg.half_precision_weights else torch.float32
)
threestudio.info(f"Loading Stable Diffusion ...")
pipe_kwargs = {
"tokenizer": None,
"safety_checker": None,
"feature_extractor": None,
"requires_safety_checker": False,
"torch_dtype": self.weights_dtype,
"cache_dir": self.cfg.cache_dir,
"local_files_only": self.cfg.local_files_only,
}
pipe = StableDiffusionPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path,
**pipe_kwargs,
).to(self.device)
self.prepare_pipe(pipe)
self.configure_pipe_token_merging(pipe)
# phi network for VSD
# introduce two trainable modules:
# - self.camera_embedding
# - self.lora_layers
pipe_phi = None
# if the phi network shares the same unet with the pretrain network
# we need to pass additional cross attention kwargs to the unet
self.vsd_share_model = (
self.cfg.guidance_type == "vsd"
and self.cfg.vsd_phi_model_name_or_path is None
)
if self.cfg.guidance_type == "vsd":
if self.cfg.vsd_phi_model_name_or_path is None:
pipe_phi = pipe
else:
pipe_phi = StableDiffusionPipeline.from_pretrained(
self.cfg.vsd_phi_model_name_or_path,
**pipe_kwargs,
).to(self.device)
self.prepare_pipe(pipe_phi)
self.configure_pipe_token_merging(pipe_phi)
# set up camera embedding
if self.cfg.vsd_use_camera_condition:
if self.cfg.vsd_camera_condition_type in ["extrinsics", "mvp"]:
self.camera_embedding_dim = 16
elif self.cfg.vsd_camera_condition_type == "spherical":
self.camera_embedding_dim = 4
else:
raise ValueError("Invalid camera condition type!")
# FIXME: hard-coded output dim
self.camera_embedding = ToDTypeWrapper(
TimestepEmbedding(self.camera_embedding_dim, 1280),
self.weights_dtype,
).to(self.device)
pipe_phi.unet.class_embedding = self.camera_embedding
if self.cfg.vsd_use_lora:
# set up LoRA layers
lora_attn_procs = {}
for name in pipe_phi.unet.attn_processors.keys():
cross_attention_dim = (
None
if name.endswith("attn1.processor")
else pipe_phi.unet.config.cross_attention_dim
)
if name.startswith("mid_block"):
hidden_size = pipe_phi.unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(
reversed(pipe_phi.unet.config.block_out_channels)
)[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = pipe_phi.unet.config.block_out_channels[block_id]
lora_attn_procs[name] = LoRAAttnProcessor(
hidden_size=hidden_size, cross_attention_dim=cross_attention_dim
)
pipe_phi.unet.set_attn_processor(lora_attn_procs)
self.lora_layers = AttnProcsLayers(pipe_phi.unet.attn_processors).to(
self.device
)
self.lora_layers._load_state_dict_pre_hooks.clear()
self.lora_layers._state_dict_hooks.clear()
threestudio.info(f"Loaded Stable Diffusion!")
# controlnet
controlnet = None
if self.cfg.controlnet_model_name_or_path is not None:
threestudio.info(f"Loading ControlNet ...")
controlnet = ControlNetModel.from_pretrained(
self.cfg.controlnet_model_name_or_path,
torch_dtype=self.weights_dtype,
).to(self.device)
controlnet.eval()
enable_gradient(controlnet, enabled=False)
threestudio.info(f"Loaded ControlNet!")
self.scheduler = DDPMScheduler.from_config(pipe.scheduler.config)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
# q(z_t|x) = N(alpha_t x, sigma_t^2 I)
# in DDPM, alpha_t = sqrt(alphas_cumprod_t), sigma_t^2 = 1 - alphas_cumprod_t
self.alphas_cumprod: Float[Tensor, "T"] = self.scheduler.alphas_cumprod.to(
self.device
)
self.alphas: Float[Tensor, "T"] = self.alphas_cumprod**0.5
self.sigmas: Float[Tensor, "T"] = (1 - self.alphas_cumprod) ** 0.5
# log SNR
self.lambdas: Float[Tensor, "T"] = self.sigmas / self.alphas
self._non_trainable_modules = NonTrainableModules(
pipe=pipe,
pipe_phi=pipe_phi,
controlnet=controlnet,
)
@property
def pipe(self) -> StableDiffusionPipeline:
return self._non_trainable_modules.pipe
@property
def pipe_phi(self) -> StableDiffusionPipeline:
if self._non_trainable_modules.pipe_phi is None:
raise RuntimeError("phi model is not available.")
return self._non_trainable_modules.pipe_phi
@property
def controlnet(self) -> ControlNetModel:
if self._non_trainable_modules.controlnet is None:
raise RuntimeError("ControlNet model is not available.")
return self._non_trainable_modules.controlnet
def prepare_pipe(self, pipe: StableDiffusionPipeline):
if self.cfg.enable_memory_efficient_attention:
if parse_version(torch.__version__) >= parse_version("2"):
threestudio.info(
"PyTorch2.0 uses memory efficient attention by default."
)
elif not is_xformers_available():
threestudio.warn(
"xformers is not available, memory efficient attention is not enabled."
)
else:
pipe.enable_xformers_memory_efficient_attention()
if self.cfg.enable_sequential_cpu_offload:
pipe.enable_sequential_cpu_offload()
if self.cfg.enable_attention_slicing:
pipe.enable_attention_slicing(1)
if self.cfg.enable_channels_last_format:
pipe.unet.to(memory_format=torch.channels_last)
# FIXME: pipe.__call__ requires text_encoder.dtype
# pipe.text_encoder.to("meta")
cleanup()
pipe.vae.eval()
pipe.unet.eval()
enable_gradient(pipe.vae, enabled=False)
enable_gradient(pipe.unet, enabled=False)
# disable progress bar
pipe.set_progress_bar_config(disable=True)
def configure_pipe_token_merging(self, pipe: StableDiffusionPipeline):
if self.cfg.token_merging:
import tomesd
tomesd.apply_patch(pipe.unet, **self.cfg.token_merging_params)
@torch.cuda.amp.autocast(enabled=False)
def forward_unet(
self,
unet: UNet2DConditionModel,
latents: Float[Tensor, "..."],
t: Int[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
class_labels: Optional[Float[Tensor, "..."]] = None,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
down_block_additional_residuals: Optional[Float[Tensor, "..."]] = None,
mid_block_additional_residual: Optional[Float[Tensor, "..."]] = None,
velocity_to_epsilon: bool = False,
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
pred = unet(
latents.to(unet.dtype),
t.to(unet.dtype),
encoder_hidden_states=encoder_hidden_states.to(unet.dtype),
class_labels=class_labels,
cross_attention_kwargs=cross_attention_kwargs,
down_block_additional_residuals=down_block_additional_residuals,
mid_block_additional_residual=mid_block_additional_residual,
).sample
if velocity_to_epsilon:
pred = latents * self.sigmas[t].view(-1, 1, 1, 1) + pred * self.alphas[
t
].view(-1, 1, 1, 1)
return pred.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def vae_encode(
self, vae: AutoencoderKL, imgs: Float[Tensor, "B 3 H W"], mode=False
) -> Float[Tensor, "B 4 Hl Wl"]:
# expect input in [-1, 1]
input_dtype = imgs.dtype
posterior = vae.encode(imgs.to(vae.dtype)).latent_dist
if mode:
latents = posterior.mode()
else:
latents = posterior.sample()
latents = latents * vae.config.scaling_factor
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def vae_decode(
self, vae: AutoencoderKL, latents: Float[Tensor, "B 4 Hl Wl"]
) -> Float[Tensor, "B 3 H W"]:
# output in [0, 1]
input_dtype = latents.dtype
latents = 1 / vae.config.scaling_factor * latents
image = vae.decode(latents.to(vae.dtype)).sample
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
@contextmanager
def disable_unet_class_embedding(self, unet: UNet2DConditionModel):
class_embedding = unet.class_embedding
try:
unet.class_embedding = None
yield unet
finally:
unet.class_embedding = class_embedding
@contextmanager
def set_scheduler(
self, pipe: StableDiffusionPipeline, scheduler_class: Any, **kwargs
):
scheduler_orig = pipe.scheduler
pipe.scheduler = scheduler_class.from_config(scheduler_orig.config, **kwargs)
yield pipe
pipe.scheduler = scheduler_orig
def get_eps_pretrain(
self,
latents_noisy: Float[Tensor, "B 4 Hl Wl"],
t: Int[Tensor, "B"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
) -> Float[Tensor, "B 4 Hl Wl"]:
batch_size = latents_noisy.shape[0]
if prompt_utils.use_perp_neg:
(
text_embeddings,
neg_guidance_weights,
) = prompt_utils.get_text_embeddings_perp_neg(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
with torch.no_grad():
with self.disable_unet_class_embedding(self.pipe.unet) as unet:
noise_pred = self.forward_unet(
unet,
torch.cat([latents_noisy] * 4, dim=0),
torch.cat([t] * 4, dim=0),
encoder_hidden_states=text_embeddings,
cross_attention_kwargs={"scale": 0.0}
if self.vsd_share_model
else None,
velocity_to_epsilon=self.pipe.scheduler.config.prediction_type
== "v_prediction",
) # (4B, 3, Hl, Wl)
noise_pred_text = noise_pred[:batch_size]
noise_pred_uncond = noise_pred[batch_size : batch_size * 2]
noise_pred_neg = noise_pred[batch_size * 2 :]
e_pos = noise_pred_text - noise_pred_uncond
accum_grad = 0
n_negative_prompts = neg_guidance_weights.shape[-1]
for i in range(n_negative_prompts):
e_i_neg = noise_pred_neg[i::n_negative_prompts] - noise_pred_uncond
accum_grad += neg_guidance_weights[:, i].view(
-1, 1, 1, 1
) * perpendicular_component(e_i_neg, e_pos)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
e_pos + accum_grad
)
else:
text_embeddings = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, self.cfg.view_dependent_prompting
)
with torch.no_grad():
with self.disable_unet_class_embedding(self.pipe.unet) as unet:
noise_pred = self.forward_unet(
unet,
torch.cat([latents_noisy] * 2, dim=0),
torch.cat([t] * 2, dim=0),
encoder_hidden_states=text_embeddings,
cross_attention_kwargs={"scale": 0.0}
if self.vsd_share_model
else None,
velocity_to_epsilon=self.pipe.scheduler.config.prediction_type
== "v_prediction",
)
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
return noise_pred
def get_eps_phi(
self,
latents_noisy: Float[Tensor, "B 4 Hl Wl"],
t: Int[Tensor, "B"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
camera_condition: Float[Tensor, "B ..."],
) -> Float[Tensor, "B 4 Hl Wl"]:
batch_size = latents_noisy.shape[0]
# not using view-dependent prompting in LoRA
text_embeddings, _ = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, view_dependent_prompting=False
).chunk(2)
with torch.no_grad():
noise_pred = self.forward_unet(
self.pipe_phi.unet,
torch.cat([latents_noisy] * 2, dim=0),
torch.cat([t] * 2, dim=0),
encoder_hidden_states=torch.cat([text_embeddings] * 2, dim=0),
class_labels=torch.cat(
[
camera_condition.view(batch_size, -1),
torch.zeros_like(camera_condition.view(batch_size, -1)),
],
dim=0,
)
if self.cfg.vsd_use_camera_condition
else None,
cross_attention_kwargs={"scale": 1.0},
velocity_to_epsilon=self.pipe_phi.scheduler.config.prediction_type
== "v_prediction",
)
noise_pred_camera, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.vsd_guidance_scale_phi * (
noise_pred_camera - noise_pred_uncond
)
return noise_pred
def train_phi(
self,
latents: Float[Tensor, "B 4 Hl Wl"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
camera_condition: Float[Tensor, "B ..."],
):
B = latents.shape[0]
latents = latents.detach().repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1, 1, 1
)
num_train_timesteps = self.pipe_phi.scheduler.config.num_train_timesteps
t = torch.randint(
int(num_train_timesteps * 0.0),
int(num_train_timesteps * 1.0),
[B * self.cfg.vsd_lora_n_timestamp_samples],
dtype=torch.long,
device=self.device,
)
noise = torch.randn_like(latents)
latents_noisy = self.pipe_phi.scheduler.add_noise(latents, noise, t)
if self.pipe_phi.scheduler.config.prediction_type == "epsilon":
target = noise
elif self.pipe_phi.scheduler.prediction_type == "v_prediction":
target = self.pipe_phi.scheduler.get_velocity(latents, noise, t)
else:
raise ValueError(
f"Unknown prediction type {self.pipe_phi.scheduler.prediction_type}"
)
# not using view-dependent prompting in LoRA
text_embeddings, _ = prompt_utils.get_text_embeddings(
elevation, azimuth, camera_distances, view_dependent_prompting=False
).chunk(2)
if (
self.cfg.vsd_use_camera_condition
and self.cfg.vsd_lora_cfg_training
and random.random() < 0.1
):
camera_condition = torch.zeros_like(camera_condition)
noise_pred = self.forward_unet(
self.pipe_phi.unet,
latents_noisy,
t,
encoder_hidden_states=text_embeddings.repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1, 1
),
class_labels=camera_condition.view(B, -1).repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1
)
if self.cfg.vsd_use_camera_condition
else None,
cross_attention_kwargs={"scale": 1.0},
)
return F.mse_loss(noise_pred.float(), target.float(), reduction="mean")
def forward(
self,
rgb: Float[Tensor, "B H W C"],
prompt_utils: PromptProcessorOutput,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
mvp_mtx: Float[Tensor, "B 4 4"],
c2w: Float[Tensor, "B 4 4"],
rgb_as_latents=False,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 Hl Wl"]
if rgb_as_latents:
# treat input rgb as latents
# input rgb should be in range [-1, 1]
latents = F.interpolate(
rgb_BCHW, (64, 64), mode="bilinear", align_corners=False
)
else:
# treat input rgb as rgb
# input rgb should be in range [0, 1]
rgb_BCHW = F.interpolate(
rgb_BCHW, (512, 512), mode="bilinear", align_corners=False
)
# encode image into latents with vae
latents = self.vae_encode(self.pipe.vae, rgb_BCHW * 2.0 - 1.0)
# sample timestep
# use the same timestep for each batch
assert self.min_step is not None and self.max_step is not None
t = torch.randint(
self.min_step,
self.max_step + 1,
[1],
dtype=torch.long,
device=self.device,
).repeat(batch_size)
# sample noise
noise = torch.randn_like(latents)
latents_noisy = self.scheduler.add_noise(latents, noise, t)
eps_pretrain = self.get_eps_pretrain(
latents_noisy, t, prompt_utils, elevation, azimuth, camera_distances
)
latents_1step_orig = (
1
/ self.alphas[t].view(-1, 1, 1, 1)
* (latents_noisy - self.sigmas[t].view(-1, 1, 1, 1) * eps_pretrain)
).detach()
if self.cfg.guidance_type == "sds":
eps_phi = noise
elif self.cfg.guidance_type == "vsd":
if self.cfg.vsd_camera_condition_type == "extrinsics":
camera_condition = c2w
elif self.cfg.vsd_camera_condition_type == "mvp":
camera_condition = mvp_mtx
elif self.cfg.vsd_camera_condition_type == "spherical":
camera_condition = torch.stack(
[
torch.deg2rad(elevation),
torch.sin(torch.deg2rad(azimuth)),
torch.cos(torch.deg2rad(azimuth)),
camera_distances,
],
dim=-1,
)
else:
raise ValueError(
f"Unknown camera_condition_type {self.cfg.vsd_camera_condition_type}"
)
eps_phi = self.get_eps_phi(
latents_noisy,
t,
prompt_utils,
elevation,
azimuth,
camera_distances,
camera_condition,
)
loss_train_phi = self.train_phi(
latents,
prompt_utils,
elevation,
azimuth,
camera_distances,
camera_condition,
)
if self.cfg.weighting_strategy == "dreamfusion":
w = (1.0 - self.alphas[t]).view(-1, 1, 1, 1)
elif self.cfg.weighting_strategy == "uniform":
w = 1.0
elif self.cfg.weighting_strategy == "fantasia3d":
w = (self.alphas[t] ** 0.5 * (1 - self.alphas[t])).view(-1, 1, 1, 1)
else:
raise ValueError(
f"Unknown weighting strategy: {self.cfg.weighting_strategy}"
)
grad = w * (eps_pretrain - eps_phi)
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# reparameterization trick:
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
target = (latents - grad).detach()
loss_sd = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sd,
"grad_norm": grad.norm(),
"timesteps": t,
"min_step": self.min_step,
"max_step": self.max_step,
"latents": latents,
"latents_1step_orig": latents_1step_orig,
"rgb": rgb_BCHW.permute(0, 2, 3, 1),
"weights": w,
"lambdas": self.lambdas[t],
}
if self.cfg.return_rgb_1step_orig:
with torch.no_grad():
rgb_1step_orig = self.vae_decode(
self.pipe.vae, latents_1step_orig
).permute(0, 2, 3, 1)
guidance_out.update({"rgb_1step_orig": rgb_1step_orig})
if self.cfg.return_rgb_multistep_orig:
with self.set_scheduler(
self.pipe,
DPMSolverSinglestepScheduler,
solver_order=1,
num_train_timesteps=int(t[0]),
) as pipe:
text_embeddings = prompt_utils.get_text_embeddings(
elevation,
azimuth,
camera_distances,
self.cfg.view_dependent_prompting,
)
text_embeddings_cond, text_embeddings_uncond = text_embeddings.chunk(2)
with torch.cuda.amp.autocast(enabled=False):
latents_multistep_orig = pipe(
num_inference_steps=self.cfg.n_rgb_multistep_orig_steps,
guidance_scale=self.cfg.guidance_scale,
eta=1.0,
latents=latents_noisy.to(pipe.unet.dtype),
prompt_embeds=text_embeddings_cond.to(pipe.unet.dtype),
negative_prompt_embeds=text_embeddings_uncond.to(
pipe.unet.dtype
),
cross_attention_kwargs={"scale": 0.0}
if self.vsd_share_model
else None,
output_type="latent",
).images.to(latents.dtype)
with torch.no_grad():
rgb_multistep_orig = self.vae_decode(
self.pipe.vae, latents_multistep_orig
)
guidance_out.update(
{
"latents_multistep_orig": latents_multistep_orig,
"rgb_multistep_orig": rgb_multistep_orig.permute(0, 2, 3, 1),
}
)
if self.cfg.guidance_type == "vsd":
guidance_out.update(
{
"loss_train_phi": loss_train_phi,
}
)
return guidance_out
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.min_step = int(
self.num_train_timesteps * C(self.cfg.min_step_percent, epoch, global_step)
)
self.max_step = int(
self.num_train_timesteps * C(self.cfg.max_step_percent, epoch, global_step)
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,340 @@
import importlib
import os
from dataclasses import dataclass, field
import cv2
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers import DDIMScheduler, DDPMScheduler, StableDiffusionPipeline
from diffusers.utils.import_utils import is_xformers_available
from omegaconf import OmegaConf
from tqdm import tqdm
import threestudio
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, parse_version
from threestudio.utils.typing import *
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
def instantiate_from_config(config):
if not "target" in config:
if config == "__is_first_stage__":
return None
elif config == "__is_unconditional__":
return None
raise KeyError("Expected key `target` to instantiate.")
return get_obj_from_str(config["target"])(**config.get("params", dict()))
# load model
def load_model_from_config(config, ckpt, device, vram_O=True, verbose=False):
pl_sd = torch.load(ckpt, map_location="cpu")
if "global_step" in pl_sd and verbose:
print(f'[INFO] Global Step: {pl_sd["global_step"]}')
sd = pl_sd["state_dict"]
model = instantiate_from_config(config.model)
m, u = model.load_state_dict(sd, strict=False)
if len(m) > 0 and verbose:
print("[INFO] missing keys: \n", m)
if len(u) > 0 and verbose:
print("[INFO] unexpected keys: \n", u)
# manually load ema and delete it to save GPU memory
if model.use_ema:
if verbose:
print("[INFO] loading EMA...")
model.model_ema.copy_to(model.model)
del model.model_ema
if vram_O:
# we don't need decoder
del model.first_stage_model.decoder
torch.cuda.empty_cache()
model.eval().to(device)
return model
@threestudio.register("stable-zero123-guidance")
class StableZero123Guidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
pretrained_model_name_or_path: str = "load/zero123/stable-zero123.ckpt"
pretrained_config: str = "load/zero123/sd-objaverse-finetune-c_concat-256.yaml"
vram_O: bool = True
cond_image_path: str = "load/images/hamburger_rgba.png"
cond_elevation_deg: float = 0.0
cond_azimuth_deg: float = 0.0
cond_camera_distance: float = 1.2
guidance_scale: float = 5.0
grad_clip: Optional[
Any
] = None # field(default_factory=lambda: [0, 2.0, 8.0, 1000])
half_precision_weights: bool = False
min_step_percent: float = 0.02
max_step_percent: float = 0.98
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading Stable Zero123 ...")
self.config = OmegaConf.load(self.cfg.pretrained_config)
# TODO: seems it cannot load into fp16...
self.weights_dtype = torch.float32
self.model = load_model_from_config(
self.config,
self.cfg.pretrained_model_name_or_path,
device=self.device,
vram_O=self.cfg.vram_O,
)
for p in self.model.parameters():
p.requires_grad_(False)
# timesteps: use diffuser for convenience... hope it's alright.
self.num_train_timesteps = self.config.model.params.timesteps
self.scheduler = DDIMScheduler(
self.num_train_timesteps,
self.config.model.params.linear_start,
self.config.model.params.linear_end,
beta_schedule="scaled_linear",
clip_sample=False,
set_alpha_to_one=False,
steps_offset=1,
)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.set_min_max_steps() # set to default value
self.alphas: Float[Tensor, "..."] = self.scheduler.alphas_cumprod.to(
self.device
)
self.grad_clip_val: Optional[float] = None
self.prepare_embeddings(self.cfg.cond_image_path)
threestudio.info(f"Loaded Stable Zero123!")
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def prepare_embeddings(self, image_path: str) -> None:
# load cond image for zero123
assert os.path.exists(image_path)
rgba = cv2.cvtColor(
cv2.imread(image_path, cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGBA
)
rgba = (
cv2.resize(rgba, (256, 256), interpolation=cv2.INTER_AREA).astype(
np.float32
)
/ 255.0
)
rgb = rgba[..., :3] * rgba[..., 3:] + (1 - rgba[..., 3:])
self.rgb_256: Float[Tensor, "1 3 H W"] = (
torch.from_numpy(rgb)
.unsqueeze(0)
.permute(0, 3, 1, 2)
.contiguous()
.to(self.device)
)
self.c_crossattn, self.c_concat = self.get_img_embeds(self.rgb_256)
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_img_embeds(
self,
img: Float[Tensor, "B 3 256 256"],
) -> Tuple[Float[Tensor, "B 1 768"], Float[Tensor, "B 4 32 32"]]:
img = img * 2.0 - 1.0
c_crossattn = self.model.get_learned_conditioning(img.to(self.weights_dtype))
c_concat = self.model.encode_first_stage(img.to(self.weights_dtype)).mode()
return c_crossattn, c_concat
@torch.cuda.amp.autocast(enabled=False)
def encode_images(
self, imgs: Float[Tensor, "B 3 256 256"]
) -> Float[Tensor, "B 4 32 32"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
latents = self.model.get_first_stage_encoding(
self.model.encode_first_stage(imgs.to(self.weights_dtype))
)
return latents.to(input_dtype) # [B, 4, 32, 32] Latent space image
@torch.cuda.amp.autocast(enabled=False)
def decode_latents(
self,
latents: Float[Tensor, "B 4 H W"],
) -> Float[Tensor, "B 3 512 512"]:
input_dtype = latents.dtype
image = self.model.decode_first_stage(latents)
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_cond(
self,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
c_crossattn=None,
c_concat=None,
**kwargs,
) -> dict:
T = torch.stack(
[
torch.deg2rad(
(90 - elevation) - (90 - self.cfg.cond_elevation_deg)
), # Zero123 polar is 90-elevation
torch.sin(torch.deg2rad(azimuth - self.cfg.cond_azimuth_deg)),
torch.cos(torch.deg2rad(azimuth - self.cfg.cond_azimuth_deg)),
torch.deg2rad(
90 - torch.full_like(elevation, self.cfg.cond_elevation_deg)
),
],
dim=-1,
)[:, None, :].to(self.device)
cond = {}
clip_emb = self.model.cc_projection(
torch.cat(
[
(self.c_crossattn if c_crossattn is None else c_crossattn).repeat(
len(T), 1, 1
),
T,
],
dim=-1,
)
)
cond["c_crossattn"] = [
torch.cat([torch.zeros_like(clip_emb).to(self.device), clip_emb], dim=0)
]
cond["c_concat"] = [
torch.cat(
[
torch.zeros_like(self.c_concat)
.repeat(len(T), 1, 1, 1)
.to(self.device),
(self.c_concat if c_concat is None else c_concat).repeat(
len(T), 1, 1, 1
),
],
dim=0,
)
]
return cond
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
rgb_as_latents=False,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 64 64"]
if rgb_as_latents:
latents = (
F.interpolate(rgb_BCHW, (32, 32), mode="bilinear", align_corners=False)
* 2
- 1
)
else:
rgb_BCHW_512 = F.interpolate(
rgb_BCHW, (256, 256), mode="bilinear", align_corners=False
)
# encode image into latents with vae
latents = self.encode_images(rgb_BCHW_512)
cond = self.get_cond(elevation, azimuth, camera_distances)
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# pred noise
x_in = torch.cat([latents_noisy] * 2)
t_in = torch.cat([t] * 2)
noise_pred = self.model.apply_model(x_in, t_in, cond)
# perform guidance
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_cond - noise_pred_uncond
)
w = (1 - self.alphas[t]).reshape(-1, 1, 1, 1)
grad = w * (noise_pred - noise)
grad = torch.nan_to_num(grad)
# clip grad for stable training?
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# loss = SpecifyGradient.apply(latents, grad)
# SpecifyGradient is not straghtforward, use a reparameterization trick instead
target = (latents - grad).detach()
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sds,
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
return guidance_out
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)

View File

@@ -0,0 +1,528 @@
import importlib
import os
from dataclasses import dataclass, field
import cv2
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers import DDIMScheduler, DDPMScheduler, StableDiffusionPipeline
from diffusers.utils.import_utils import is_xformers_available
from omegaconf import OmegaConf
from tqdm import tqdm
import threestudio
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import C, parse_version
from threestudio.utils.typing import *
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
def instantiate_from_config(config):
if not "target" in config:
if config == "__is_first_stage__":
return None
elif config == "__is_unconditional__":
return None
raise KeyError("Expected key `target` to instantiate.")
return get_obj_from_str(config["target"])(**config.get("params", dict()))
# load model
def load_model_from_config(config, ckpt, device, vram_O=True, verbose=False):
pl_sd = torch.load(ckpt, map_location="cpu")
if "global_step" in pl_sd and verbose:
print(f'[INFO] Global Step: {pl_sd["global_step"]}')
sd = pl_sd["state_dict"]
model = instantiate_from_config(config.model)
m, u = model.load_state_dict(sd, strict=False)
if len(m) > 0 and verbose:
print("[INFO] missing keys: \n", m)
if len(u) > 0 and verbose:
print("[INFO] unexpected keys: \n", u)
# manually load ema and delete it to save GPU memory
if model.use_ema:
if verbose:
print("[INFO] loading EMA...")
model.model_ema.copy_to(model.model)
del model.model_ema
if vram_O:
# we don't need decoder
del model.first_stage_model.decoder
torch.cuda.empty_cache()
model.eval().to(device)
return model
@threestudio.register("zero123-guidance")
class Zero123Guidance(BaseObject):
@dataclass
class Config(BaseObject.Config):
pretrained_model_name_or_path: str = "load/zero123/105000.ckpt"
pretrained_config: str = "load/zero123/sd-objaverse-finetune-c_concat-256.yaml"
vram_O: bool = True
cond_image_path: str = "load/images/hamburger_rgba.png"
cond_elevation_deg: float = 0.0
cond_azimuth_deg: float = 0.0
cond_camera_distance: float = 1.2
guidance_scale: float = 5.0
grad_clip: Optional[
Any
] = None # field(default_factory=lambda: [0, 2.0, 8.0, 1000])
half_precision_weights: bool = False
min_step_percent: float = 0.02
max_step_percent: float = 0.98
"""Maximum number of batch items to evaluate guidance for (for debugging) and to save on disk. -1 means save all items."""
max_items_eval: int = 4
cfg: Config
def configure(self) -> None:
threestudio.info(f"Loading Zero123 ...")
self.config = OmegaConf.load(self.cfg.pretrained_config)
# TODO: seems it cannot load into fp16...
self.weights_dtype = torch.float32
self.model = load_model_from_config(
self.config,
self.cfg.pretrained_model_name_or_path,
device=self.device,
vram_O=self.cfg.vram_O,
)
for p in self.model.parameters():
p.requires_grad_(False)
# timesteps: use diffuser for convenience... hope it's alright.
self.num_train_timesteps = self.config.model.params.timesteps
self.scheduler = DDIMScheduler(
self.num_train_timesteps,
self.config.model.params.linear_start,
self.config.model.params.linear_end,
beta_schedule="scaled_linear",
clip_sample=False,
set_alpha_to_one=False,
steps_offset=1,
)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
self.set_min_max_steps() # set to default value
self.alphas: Float[Tensor, "..."] = self.scheduler.alphas_cumprod.to(
self.device
)
self.grad_clip_val: Optional[float] = None
self.prepare_embeddings(self.cfg.cond_image_path)
threestudio.info(f"Loaded Zero123!")
@torch.cuda.amp.autocast(enabled=False)
def set_min_max_steps(self, min_step_percent=0.02, max_step_percent=0.98):
self.min_step = int(self.num_train_timesteps * min_step_percent)
self.max_step = int(self.num_train_timesteps * max_step_percent)
@torch.cuda.amp.autocast(enabled=False)
def prepare_embeddings(self, image_path: str) -> None:
# load cond image for zero123
assert os.path.exists(image_path)
rgba = cv2.cvtColor(
cv2.imread(image_path, cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGBA
)
rgba = (
cv2.resize(rgba, (256, 256), interpolation=cv2.INTER_AREA).astype(
np.float32
)
/ 255.0
)
rgb = rgba[..., :3] * rgba[..., 3:] + (1 - rgba[..., 3:])
self.rgb_256: Float[Tensor, "1 3 H W"] = (
torch.from_numpy(rgb)
.unsqueeze(0)
.permute(0, 3, 1, 2)
.contiguous()
.to(self.device)
)
self.c_crossattn, self.c_concat = self.get_img_embeds(self.rgb_256)
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_img_embeds(
self,
img: Float[Tensor, "B 3 256 256"],
) -> Tuple[Float[Tensor, "B 1 768"], Float[Tensor, "B 4 32 32"]]:
img = img * 2.0 - 1.0
c_crossattn = self.model.get_learned_conditioning(img.to(self.weights_dtype))
c_concat = self.model.encode_first_stage(img.to(self.weights_dtype)).mode()
return c_crossattn, c_concat
@torch.cuda.amp.autocast(enabled=False)
def encode_images(
self, imgs: Float[Tensor, "B 3 256 256"]
) -> Float[Tensor, "B 4 32 32"]:
input_dtype = imgs.dtype
imgs = imgs * 2.0 - 1.0
latents = self.model.get_first_stage_encoding(
self.model.encode_first_stage(imgs.to(self.weights_dtype))
)
return latents.to(input_dtype) # [B, 4, 32, 32] Latent space image
@torch.cuda.amp.autocast(enabled=False)
def decode_latents(
self,
latents: Float[Tensor, "B 4 H W"],
) -> Float[Tensor, "B 3 512 512"]:
input_dtype = latents.dtype
image = self.model.decode_first_stage(latents)
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def get_cond(
self,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
c_crossattn=None,
c_concat=None,
**kwargs,
) -> dict:
T = torch.stack(
[
torch.deg2rad(
(90 - elevation) - (90 - self.cfg.cond_elevation_deg)
), # Zero123 polar is 90-elevation
torch.sin(torch.deg2rad(azimuth - self.cfg.cond_azimuth_deg)),
torch.cos(torch.deg2rad(azimuth - self.cfg.cond_azimuth_deg)),
camera_distances - self.cfg.cond_camera_distance,
],
dim=-1,
)[:, None, :].to(self.device)
cond = {}
clip_emb = self.model.cc_projection(
torch.cat(
[
(self.c_crossattn if c_crossattn is None else c_crossattn).repeat(
len(T), 1, 1
),
T,
],
dim=-1,
)
)
cond["c_crossattn"] = [
torch.cat([torch.zeros_like(clip_emb).to(self.device), clip_emb], dim=0)
]
cond["c_concat"] = [
torch.cat(
[
torch.zeros_like(self.c_concat)
.repeat(len(T), 1, 1, 1)
.to(self.device),
(self.c_concat if c_concat is None else c_concat).repeat(
len(T), 1, 1, 1
),
],
dim=0,
)
]
return cond
def __call__(
self,
rgb: Float[Tensor, "B H W C"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
rgb_as_latents=False,
guidance_eval=False,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 64 64"]
if rgb_as_latents:
latents = (
F.interpolate(rgb_BCHW, (32, 32), mode="bilinear", align_corners=False)
* 2
- 1
)
else:
rgb_BCHW_512 = F.interpolate(
rgb_BCHW, (256, 256), mode="bilinear", align_corners=False
)
# encode image into latents with vae
latents = self.encode_images(rgb_BCHW_512)
cond = self.get_cond(elevation, azimuth, camera_distances)
# timestep ~ U(0.02, 0.98) to avoid very high/low noise level
t = torch.randint(
self.min_step,
self.max_step + 1,
[batch_size],
dtype=torch.long,
device=self.device,
)
# predict the noise residual with unet, NO grad!
with torch.no_grad():
# add noise
noise = torch.randn_like(latents) # TODO: use torch generator
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# pred noise
x_in = torch.cat([latents_noisy] * 2)
t_in = torch.cat([t] * 2)
noise_pred = self.model.apply_model(x_in, t_in, cond)
# perform guidance
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_cond - noise_pred_uncond
)
w = (1 - self.alphas[t]).reshape(-1, 1, 1, 1)
grad = w * (noise_pred - noise)
grad = torch.nan_to_num(grad)
# clip grad for stable training?
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# loss = SpecifyGradient.apply(latents, grad)
# SpecifyGradient is not straghtforward, use a reparameterization trick instead
target = (latents - grad).detach()
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sds, # loss_sds
"grad_norm": grad.norm(),
"min_step": self.min_step,
"max_step": self.max_step,
}
if guidance_eval:
guidance_eval_utils = {
"cond": cond,
"t_orig": t,
"latents_noisy": latents_noisy,
"noise_pred": noise_pred,
}
guidance_eval_out = self.guidance_eval(**guidance_eval_utils)
texts = []
for n, e, a, c in zip(
guidance_eval_out["noise_levels"], elevation, azimuth, camera_distances
):
texts.append(
f"n{n:.02f}\ne{e.item():.01f}\na{a.item():.01f}\nc{c.item():.02f}"
)
guidance_eval_out.update({"texts": texts})
guidance_out.update({"eval": guidance_eval_out})
return guidance_out
@torch.cuda.amp.autocast(enabled=False)
@torch.no_grad()
def guidance_eval(self, cond, t_orig, latents_noisy, noise_pred):
# use only 50 timesteps, and find nearest of those to t
self.scheduler.set_timesteps(50)
self.scheduler.timesteps_gpu = self.scheduler.timesteps.to(self.device)
bs = (
min(self.cfg.max_items_eval, latents_noisy.shape[0])
if self.cfg.max_items_eval > 0
else latents_noisy.shape[0]
) # batch size
large_enough_idxs = self.scheduler.timesteps_gpu.expand([bs, -1]) > t_orig[
:bs
].unsqueeze(
-1
) # sized [bs,50] > [bs,1]
idxs = torch.min(large_enough_idxs, dim=1)[1]
t = self.scheduler.timesteps_gpu[idxs]
fracs = list((t / self.scheduler.config.num_train_timesteps).cpu().numpy())
imgs_noisy = self.decode_latents(latents_noisy[:bs]).permute(0, 2, 3, 1)
# get prev latent
latents_1step = []
pred_1orig = []
for b in range(bs):
step_output = self.scheduler.step(
noise_pred[b : b + 1], t[b], latents_noisy[b : b + 1], eta=1
)
latents_1step.append(step_output["prev_sample"])
pred_1orig.append(step_output["pred_original_sample"])
latents_1step = torch.cat(latents_1step)
pred_1orig = torch.cat(pred_1orig)
imgs_1step = self.decode_latents(latents_1step).permute(0, 2, 3, 1)
imgs_1orig = self.decode_latents(pred_1orig).permute(0, 2, 3, 1)
latents_final = []
for b, i in enumerate(idxs):
latents = latents_1step[b : b + 1]
c = {
"c_crossattn": [cond["c_crossattn"][0][[b, b + len(idxs)], ...]],
"c_concat": [cond["c_concat"][0][[b, b + len(idxs)], ...]],
}
for t in tqdm(self.scheduler.timesteps[i + 1 :], leave=False):
# pred noise
x_in = torch.cat([latents] * 2)
t_in = torch.cat([t.reshape(1)] * 2).to(self.device)
noise_pred = self.model.apply_model(x_in, t_in, c)
# perform guidance
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_cond - noise_pred_uncond
)
# get prev latent
latents = self.scheduler.step(noise_pred, t, latents, eta=1)[
"prev_sample"
]
latents_final.append(latents)
latents_final = torch.cat(latents_final)
imgs_final = self.decode_latents(latents_final).permute(0, 2, 3, 1)
return {
"bs": bs,
"noise_levels": fracs,
"imgs_noisy": imgs_noisy,
"imgs_1step": imgs_1step,
"imgs_1orig": imgs_1orig,
"imgs_final": imgs_final,
}
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.set_min_max_steps(
min_step_percent=C(self.cfg.min_step_percent, epoch, global_step),
max_step_percent=C(self.cfg.max_step_percent, epoch, global_step),
)
# verification - requires `vram_O = False` in load_model_from_config
@torch.no_grad()
def generate(
self,
image, # image tensor [1, 3, H, W] in [0, 1]
elevation=0,
azimuth=0,
camera_distances=0, # new view params
c_crossattn=None,
c_concat=None,
scale=3,
ddim_steps=50,
post_process=True,
ddim_eta=1,
):
if c_crossattn is None:
c_crossattn, c_concat = self.get_img_embeds(image)
cond = self.get_cond(
elevation, azimuth, camera_distances, c_crossattn, c_concat
)
imgs = self.gen_from_cond(cond, scale, ddim_steps, post_process, ddim_eta)
return imgs
# verification - requires `vram_O = False` in load_model_from_config
@torch.no_grad()
def gen_from_cond(
self,
cond,
scale=3,
ddim_steps=50,
post_process=True,
ddim_eta=1,
):
# produce latents loop
B = cond["c_crossattn"][0].shape[0] // 2
latents = torch.randn((B, 4, 32, 32), device=self.device)
self.scheduler.set_timesteps(ddim_steps)
for t in self.scheduler.timesteps:
x_in = torch.cat([latents] * 2)
t_in = torch.cat([t.reshape(1).repeat(B)] * 2).to(self.device)
noise_pred = self.model.apply_model(x_in, t_in, cond)
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + scale * (
noise_pred_cond - noise_pred_uncond
)
latents = self.scheduler.step(noise_pred, t, latents, eta=ddim_eta)[
"prev_sample"
]
imgs = self.decode_latents(latents)
imgs = imgs.cpu().numpy().transpose(0, 2, 3, 1) if post_process else imgs
return imgs
if __name__ == '__main__':
from threestudio.utils.config import load_config
import pytorch_lightning as pl
import numpy as np
import os
import cv2
cfg = load_config("configs/experimental/zero123.yaml")
guidance = threestudio.find(cfg.system.guidance_type)(cfg.system.guidance)
elevations = [0, 20, -20]
azimuths = [45, 90, 135, -45, -90]
radius = torch.tensor([3.8]).to(guidance.device)
outdir = ".threestudio_cache/saiyan"
os.makedirs(outdir, exist_ok=True)
# rgb_image = (rgb_image[0].detach().cpu().clip(0, 1).numpy()*255).astype(np.uint8)[:, :, ::-1].copy()
# os.makedirs('.threestudio_cache', exist_ok=True)
# cv2.imwrite('.threestudio_cache/diffusion_image.jpg', rgb_image)
rgb_image = cv2.imread(cfg.system.guidance.cond_image_path)[:, :, ::-1].copy() / 255
rgb_image = cv2.resize(rgb_image, (256, 256))
rgb_image = torch.FloatTensor(rgb_image).unsqueeze(0).to(guidance.device).permute(0,3,1,2)
for elevation in elevations:
for azimuth in azimuths:
output1 = guidance.generate(
rgb_image,
torch.tensor([elevation]).to(guidance.device),
torch.tensor([azimuth]).to(guidance.device),
radius,
c_crossattn=guidance.c_crossattn,
c_concat=guidance.c_concat
)
from torchvision.utils import save_image
save_image(torch.tensor(output1).float().permute(0,3,1,2), f"{outdir}/result_e_{elevation}_a_{azimuth}.png", normalize=True, value_range=(0,1))

View File

@@ -0,0 +1,721 @@
import os
import random
import sys
from contextlib import contextmanager
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms.functional as TF
from diffusers import (
AutoencoderKL,
DDPMScheduler,
DPMSolverSinglestepScheduler,
UNet2DConditionModel,
)
from diffusers.loaders import AttnProcsLayers
from diffusers.models.attention_processor import LoRAAttnProcessor
from diffusers.models.embeddings import TimestepEmbedding
from PIL import Image
from tqdm import tqdm
import threestudio
from extern.zero123 import Zero123Pipeline
from threestudio.models.networks import ToDTypeWrapper
from threestudio.models.prompt_processors.base import PromptProcessorOutput
from threestudio.utils.base import BaseModule
from threestudio.utils.misc import C, cleanup, enable_gradient, parse_version
from threestudio.utils.typing import *
@threestudio.register("zero123-unified-guidance")
class Zero123UnifiedGuidance(BaseModule):
@dataclass
class Config(BaseModule.Config):
cache_dir: Optional[str] = None
local_files_only: Optional[bool] = False
# guidance type, in ["sds", "vsd"]
guidance_type: str = "sds"
pretrained_model_name_or_path: str = "bennyguo/zero123-diffusers"
guidance_scale: float = 5.0
weighting_strategy: str = "dreamfusion"
min_step_percent: Any = 0.02
max_step_percent: Any = 0.98
grad_clip: Optional[Any] = None
return_rgb_1step_orig: bool = False
return_rgb_multistep_orig: bool = False
n_rgb_multistep_orig_steps: int = 4
cond_image_path: str = ""
cond_elevation_deg: float = 0.0
cond_azimuth_deg: float = 0.0
cond_camera_distance: float = 1.2
# efficiency-related configurations
half_precision_weights: bool = True
# VSD configurations, only used when guidance_type is "vsd"
vsd_phi_model_name_or_path: Optional[str] = None
vsd_guidance_scale_phi: float = 1.0
vsd_use_lora: bool = True
vsd_lora_cfg_training: bool = False
vsd_lora_n_timestamp_samples: int = 1
vsd_use_camera_condition: bool = True
# camera condition type, in ["extrinsics", "mvp", "spherical"]
vsd_camera_condition_type: Optional[str] = "extrinsics"
cfg: Config
def configure(self) -> None:
self.min_step: Optional[int] = None
self.max_step: Optional[int] = None
self.grad_clip_val: Optional[float] = None
@dataclass
class NonTrainableModules:
pipe: Zero123Pipeline
pipe_phi: Optional[Zero123Pipeline] = None
self.weights_dtype = (
torch.float16 if self.cfg.half_precision_weights else torch.float32
)
threestudio.info(f"Loading Zero123 ...")
# need to make sure the pipeline file is in path
sys.path.append("extern/")
pipe_kwargs = {
"safety_checker": None,
"requires_safety_checker": False,
"variant": "fp16" if self.cfg.half_precision_weights else None,
"torch_dtype": self.weights_dtype,
"cache_dir": self.cfg.cache_dir,
"local_files_only": self.cfg.local_files_only,
}
pipe = Zero123Pipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path,
**pipe_kwargs,
).to(self.device)
self.prepare_pipe(pipe)
# phi network for VSD
# introduce two trainable modules:
# - self.camera_embedding
# - self.lora_layers
pipe_phi = None
# if the phi network shares the same unet with the pretrain network
# we need to pass additional cross attention kwargs to the unet
self.vsd_share_model = (
self.cfg.guidance_type == "vsd"
and self.cfg.vsd_phi_model_name_or_path is None
)
if self.cfg.guidance_type == "vsd":
if self.cfg.vsd_phi_model_name_or_path is None:
pipe_phi = pipe
else:
pipe_phi = Zero123Pipeline.from_pretrained(
self.cfg.vsd_phi_model_name_or_path,
**pipe_kwargs,
).to(self.device)
self.prepare_pipe(pipe_phi)
# set up camera embedding
if self.cfg.vsd_use_camera_condition:
if self.cfg.vsd_camera_condition_type in ["extrinsics", "mvp"]:
self.camera_embedding_dim = 16
elif self.cfg.vsd_camera_condition_type == "spherical":
self.camera_embedding_dim = 4
else:
raise ValueError("Invalid camera condition type!")
# FIXME: hard-coded output dim
self.camera_embedding = ToDTypeWrapper(
TimestepEmbedding(self.camera_embedding_dim, 1280),
self.weights_dtype,
).to(self.device)
pipe_phi.unet.class_embedding = self.camera_embedding
if self.cfg.vsd_use_lora:
# set up LoRA layers
lora_attn_procs = {}
for name in pipe_phi.unet.attn_processors.keys():
cross_attention_dim = (
None
if name.endswith("attn1.processor")
else pipe_phi.unet.config.cross_attention_dim
)
if name.startswith("mid_block"):
hidden_size = pipe_phi.unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(
reversed(pipe_phi.unet.config.block_out_channels)
)[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = pipe_phi.unet.config.block_out_channels[block_id]
lora_attn_procs[name] = LoRAAttnProcessor(
hidden_size=hidden_size, cross_attention_dim=cross_attention_dim
)
pipe_phi.unet.set_attn_processor(lora_attn_procs)
self.lora_layers = AttnProcsLayers(pipe_phi.unet.attn_processors).to(
self.device
)
self.lora_layers._load_state_dict_pre_hooks.clear()
self.lora_layers._state_dict_hooks.clear()
threestudio.info(f"Loaded Stable Diffusion!")
self.scheduler = DDPMScheduler.from_config(pipe.scheduler.config)
self.num_train_timesteps = self.scheduler.config.num_train_timesteps
# q(z_t|x) = N(alpha_t x, sigma_t^2 I)
# in DDPM, alpha_t = sqrt(alphas_cumprod_t), sigma_t^2 = 1 - alphas_cumprod_t
self.alphas_cumprod: Float[Tensor, "T"] = self.scheduler.alphas_cumprod.to(
self.device
)
self.alphas: Float[Tensor, "T"] = self.alphas_cumprod**0.5
self.sigmas: Float[Tensor, "T"] = (1 - self.alphas_cumprod) ** 0.5
# log SNR
self.lambdas: Float[Tensor, "T"] = self.sigmas / self.alphas
self._non_trainable_modules = NonTrainableModules(
pipe=pipe,
pipe_phi=pipe_phi,
)
# self.clip_image_embeddings and self.image_latents
self.prepare_image_embeddings()
@property
def pipe(self) -> Zero123Pipeline:
return self._non_trainable_modules.pipe
@property
def pipe_phi(self) -> Zero123Pipeline:
if self._non_trainable_modules.pipe_phi is None:
raise RuntimeError("phi model is not available.")
return self._non_trainable_modules.pipe_phi
def prepare_pipe(self, pipe: Zero123Pipeline):
cleanup()
pipe.image_encoder.eval()
pipe.vae.eval()
pipe.unet.eval()
pipe.clip_camera_projection.eval()
enable_gradient(pipe.image_encoder, enabled=False)
enable_gradient(pipe.vae, enabled=False)
enable_gradient(pipe.unet, enabled=False)
enable_gradient(pipe.clip_camera_projection, enabled=False)
# disable progress bar
pipe.set_progress_bar_config(disable=True)
def prepare_image_embeddings(self) -> None:
if not os.path.exists(self.cfg.cond_image_path):
raise RuntimeError(
f"Condition image not found at {self.cfg.cond_image_path}"
)
image = Image.open(self.cfg.cond_image_path).convert("RGBA").resize((256, 256))
image = (
TF.to_tensor(image)
.unsqueeze(0)
.to(device=self.device, dtype=self.weights_dtype)
)
# rgba -> rgb, apply white background
image = image[:, :3] * image[:, 3:4] + (1 - image[:, 3:4])
with torch.no_grad():
self.clip_image_embeddings: Float[
Tensor, "1 1 D"
] = self.extract_clip_image_embeddings(image)
# encoded latents should be multiplied with vae.config.scaling_factor
# but zero123 was not trained this way
self.image_latents: Float[Tensor, "1 4 Hl Wl"] = (
self.vae_encode(self.pipe.vae, image * 2.0 - 1.0, mode=True)
/ self.pipe.vae.config.scaling_factor
)
def extract_clip_image_embeddings(
self, images: Float[Tensor, "B 3 H W"]
) -> Float[Tensor, "B 1 D"]:
# expect images in [0, 1]
images_pil = [TF.to_pil_image(image) for image in images]
images_processed = self.pipe.feature_extractor(
images=images_pil, return_tensors="pt"
).pixel_values.to(device=self.device, dtype=self.weights_dtype)
clip_image_embeddings = self.pipe.image_encoder(images_processed).image_embeds
return clip_image_embeddings.to(images.dtype)
def get_image_camera_embeddings(
self,
elevation_deg: Float[Tensor, "B"],
azimuth_deg: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
) -> Float[Tensor, "B 1 D"]:
batch_size = elevation_deg.shape[0]
camera_embeddings: Float[Tensor, "B 1 4"] = torch.stack(
[
torch.deg2rad(self.cfg.cond_elevation_deg - elevation_deg),
torch.sin(torch.deg2rad(azimuth_deg - self.cfg.cond_azimuth_deg)),
torch.cos(torch.deg2rad(azimuth_deg - self.cfg.cond_azimuth_deg)),
camera_distances - self.cfg.cond_camera_distance,
],
dim=-1,
)[:, None, :]
image_camera_embeddings = self.pipe.clip_camera_projection(
torch.cat(
[
self.clip_image_embeddings.repeat(batch_size, 1, 1),
camera_embeddings,
],
dim=-1,
).to(self.weights_dtype)
)
return image_camera_embeddings
@torch.cuda.amp.autocast(enabled=False)
def forward_unet(
self,
unet: UNet2DConditionModel,
latents: Float[Tensor, "..."],
t: Int[Tensor, "..."],
encoder_hidden_states: Float[Tensor, "..."],
class_labels: Optional[Float[Tensor, "..."]] = None,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
down_block_additional_residuals: Optional[Float[Tensor, "..."]] = None,
mid_block_additional_residual: Optional[Float[Tensor, "..."]] = None,
velocity_to_epsilon: bool = False,
) -> Float[Tensor, "..."]:
input_dtype = latents.dtype
pred = unet(
latents.to(unet.dtype),
t.to(unet.dtype),
encoder_hidden_states=encoder_hidden_states.to(unet.dtype),
class_labels=class_labels,
cross_attention_kwargs=cross_attention_kwargs,
down_block_additional_residuals=down_block_additional_residuals,
mid_block_additional_residual=mid_block_additional_residual,
).sample
if velocity_to_epsilon:
pred = latents * self.sigmas[t].view(-1, 1, 1, 1) + pred * self.alphas[
t
].view(-1, 1, 1, 1)
return pred.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def vae_encode(
self, vae: AutoencoderKL, imgs: Float[Tensor, "B 3 H W"], mode=False
) -> Float[Tensor, "B 4 Hl Wl"]:
# expect input in [-1, 1]
input_dtype = imgs.dtype
posterior = vae.encode(imgs.to(vae.dtype)).latent_dist
if mode:
latents = posterior.mode()
else:
latents = posterior.sample()
latents = latents * vae.config.scaling_factor
return latents.to(input_dtype)
@torch.cuda.amp.autocast(enabled=False)
def vae_decode(
self, vae: AutoencoderKL, latents: Float[Tensor, "B 4 Hl Wl"]
) -> Float[Tensor, "B 3 H W"]:
# output in [0, 1]
input_dtype = latents.dtype
latents = 1 / vae.config.scaling_factor * latents
image = vae.decode(latents.to(vae.dtype)).sample
image = (image * 0.5 + 0.5).clamp(0, 1)
return image.to(input_dtype)
@contextmanager
def disable_unet_class_embedding(self, unet: UNet2DConditionModel):
class_embedding = unet.class_embedding
try:
unet.class_embedding = None
yield unet
finally:
unet.class_embedding = class_embedding
@contextmanager
def set_scheduler(self, pipe: Zero123Pipeline, scheduler_class: Any, **kwargs):
scheduler_orig = pipe.scheduler
pipe.scheduler = scheduler_class.from_config(scheduler_orig.config, **kwargs)
yield pipe
pipe.scheduler = scheduler_orig
def get_eps_pretrain(
self,
latents_noisy: Float[Tensor, "B 4 Hl Wl"],
t: Int[Tensor, "B"],
image_camera_embeddings: Float[Tensor, "B 1 D"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
) -> Float[Tensor, "B 4 Hl Wl"]:
batch_size = latents_noisy.shape[0]
with torch.no_grad():
with self.disable_unet_class_embedding(self.pipe.unet) as unet:
noise_pred = self.forward_unet(
unet,
torch.cat(
[
torch.cat([latents_noisy] * 2, dim=0),
torch.cat(
[
self.image_latents.repeat(batch_size, 1, 1, 1),
torch.zeros_like(self.image_latents).repeat(
batch_size, 1, 1, 1
),
],
dim=0,
),
],
dim=1,
),
torch.cat([t] * 2, dim=0),
encoder_hidden_states=torch.cat(
[
image_camera_embeddings,
torch.zeros_like(image_camera_embeddings),
],
dim=0,
),
cross_attention_kwargs={"scale": 0.0}
if self.vsd_share_model
else None,
velocity_to_epsilon=self.pipe.scheduler.config.prediction_type
== "v_prediction",
)
noise_pred_image, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.guidance_scale * (
noise_pred_image - noise_pred_uncond
)
return noise_pred
def get_eps_phi(
self,
latents_noisy: Float[Tensor, "B 4 Hl Wl"],
t: Int[Tensor, "B"],
image_camera_embeddings: Float[Tensor, "B 1 D"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
camera_condition: Float[Tensor, "B ..."],
) -> Float[Tensor, "B 4 Hl Wl"]:
batch_size = latents_noisy.shape[0]
with torch.no_grad():
noise_pred = self.forward_unet(
self.pipe_phi.unet,
torch.cat(
[
torch.cat([latents_noisy] * 2, dim=0),
torch.cat(
[self.image_latents.repeat(batch_size, 1, 1, 1)] * 2,
dim=0,
),
],
dim=1,
),
torch.cat([t] * 2, dim=0),
encoder_hidden_states=torch.cat([image_camera_embeddings] * 2, dim=0),
class_labels=torch.cat(
[
camera_condition.view(batch_size, -1),
torch.zeros_like(camera_condition.view(batch_size, -1)),
],
dim=0,
)
if self.cfg.vsd_use_camera_condition
else None,
cross_attention_kwargs={"scale": 1.0},
velocity_to_epsilon=self.pipe_phi.scheduler.config.prediction_type
== "v_prediction",
)
noise_pred_camera, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.cfg.vsd_guidance_scale_phi * (
noise_pred_camera - noise_pred_uncond
)
return noise_pred
def train_phi(
self,
latents: Float[Tensor, "B 4 Hl Wl"],
image_camera_embeddings: Float[Tensor, "B 1 D"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
camera_condition: Float[Tensor, "B ..."],
):
B = latents.shape[0]
latents = latents.detach().repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1, 1, 1
)
num_train_timesteps = self.pipe_phi.scheduler.config.num_train_timesteps
t = torch.randint(
int(num_train_timesteps * 0.0),
int(num_train_timesteps * 1.0),
[B * self.cfg.vsd_lora_n_timestamp_samples],
dtype=torch.long,
device=self.device,
)
noise = torch.randn_like(latents)
latents_noisy = self.pipe_phi.scheduler.add_noise(latents, noise, t)
if self.pipe_phi.scheduler.config.prediction_type == "epsilon":
target = noise
elif self.pipe_phi.scheduler.prediction_type == "v_prediction":
target = self.pipe_phi.scheduler.get_velocity(latents, noise, t)
else:
raise ValueError(
f"Unknown prediction type {self.pipe_phi.scheduler.prediction_type}"
)
if (
self.cfg.vsd_use_camera_condition
and self.cfg.vsd_lora_cfg_training
and random.random() < 0.1
):
camera_condition = torch.zeros_like(camera_condition)
noise_pred = self.forward_unet(
self.pipe_phi.unet,
torch.cat([latents_noisy, self.image_latents.repeat(B, 1, 1, 1)], dim=1),
t,
encoder_hidden_states=image_camera_embeddings.repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1, 1
),
class_labels=camera_condition.view(B, -1).repeat(
self.cfg.vsd_lora_n_timestamp_samples, 1
)
if self.cfg.vsd_use_camera_condition
else None,
cross_attention_kwargs={"scale": 1.0},
)
return F.mse_loss(noise_pred.float(), target.float(), reduction="mean")
def forward(
self,
rgb: Float[Tensor, "B H W C"],
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
mvp_mtx: Float[Tensor, "B 4 4"],
c2w: Float[Tensor, "B 4 4"],
rgb_as_latents=False,
**kwargs,
):
batch_size = rgb.shape[0]
rgb_BCHW = rgb.permute(0, 3, 1, 2)
latents: Float[Tensor, "B 4 32 32"]
if rgb_as_latents:
# treat input rgb as latents
# input rgb should be in range [-1, 1]
latents = F.interpolate(
rgb_BCHW, (32, 32), mode="bilinear", align_corners=False
)
else:
# treat input rgb as rgb
# input rgb should be in range [0, 1]
rgb_BCHW = F.interpolate(
rgb_BCHW, (256, 256), mode="bilinear", align_corners=False
)
# encode image into latents with vae
latents = self.vae_encode(self.pipe.vae, rgb_BCHW * 2.0 - 1.0)
# sample timestep
# use the same timestep for each batch
assert self.min_step is not None and self.max_step is not None
t = torch.randint(
self.min_step,
self.max_step + 1,
[1],
dtype=torch.long,
device=self.device,
).repeat(batch_size)
# sample noise
noise = torch.randn_like(latents)
latents_noisy = self.scheduler.add_noise(latents, noise, t)
# image-camera feature condition
image_camera_embeddings = self.get_image_camera_embeddings(
elevation, azimuth, camera_distances
)
eps_pretrain = self.get_eps_pretrain(
latents_noisy,
t,
image_camera_embeddings,
elevation,
azimuth,
camera_distances,
)
latents_1step_orig = (
1
/ self.alphas[t].view(-1, 1, 1, 1)
* (latents_noisy - self.sigmas[t].view(-1, 1, 1, 1) * eps_pretrain)
).detach()
if self.cfg.guidance_type == "sds":
eps_phi = noise
elif self.cfg.guidance_type == "vsd":
if self.cfg.vsd_camera_condition_type == "extrinsics":
camera_condition = c2w
elif self.cfg.vsd_camera_condition_type == "mvp":
camera_condition = mvp_mtx
elif self.cfg.vsd_camera_condition_type == "spherical":
camera_condition = torch.stack(
[
torch.deg2rad(elevation),
torch.sin(torch.deg2rad(azimuth)),
torch.cos(torch.deg2rad(azimuth)),
camera_distances,
],
dim=-1,
)
else:
raise ValueError(
f"Unknown camera_condition_type {self.cfg.vsd_camera_condition_type}"
)
eps_phi = self.get_eps_phi(
latents_noisy,
t,
image_camera_embeddings,
elevation,
azimuth,
camera_distances,
camera_condition,
)
loss_train_phi = self.train_phi(
latents,
image_camera_embeddings,
elevation,
azimuth,
camera_distances,
camera_condition,
)
if self.cfg.weighting_strategy == "dreamfusion":
w = (1.0 - self.alphas[t]).view(-1, 1, 1, 1)
elif self.cfg.weighting_strategy == "uniform":
w = 1.0
elif self.cfg.weighting_strategy == "fantasia3d":
w = (self.alphas[t] ** 0.5 * (1 - self.alphas[t])).view(-1, 1, 1, 1)
else:
raise ValueError(
f"Unknown weighting strategy: {self.cfg.weighting_strategy}"
)
grad = w * (eps_pretrain - eps_phi)
if self.grad_clip_val is not None:
grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)
# reparameterization trick:
# d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
target = (latents - grad).detach()
loss_sd = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size
guidance_out = {
"loss_sd": loss_sd,
"grad_norm": grad.norm(),
"timesteps": t,
"min_step": self.min_step,
"max_step": self.max_step,
"latents": latents,
"latents_1step_orig": latents_1step_orig,
"rgb": rgb_BCHW.permute(0, 2, 3, 1),
"weights": w,
"lambdas": self.lambdas[t],
}
if self.cfg.return_rgb_1step_orig:
with torch.no_grad():
rgb_1step_orig = self.vae_decode(
self.pipe.vae, latents_1step_orig
).permute(0, 2, 3, 1)
guidance_out.update({"rgb_1step_orig": rgb_1step_orig})
if self.cfg.return_rgb_multistep_orig:
with self.set_scheduler(
self.pipe,
DPMSolverSinglestepScheduler,
solver_order=1,
num_train_timesteps=int(t[0]),
) as pipe:
with torch.cuda.amp.autocast(enabled=False):
latents_multistep_orig = pipe(
num_inference_steps=self.cfg.n_rgb_multistep_orig_steps,
guidance_scale=self.cfg.guidance_scale,
eta=1.0,
latents=latents_noisy.to(pipe.unet.dtype),
image_camera_embeddings=image_camera_embeddings.to(
pipe.unet.dtype
),
image_latents=self.image_latents.repeat(batch_size, 1, 1, 1).to(
pipe.unet.dtype
),
cross_attention_kwargs={"scale": 0.0}
if self.vsd_share_model
else None,
output_type="latent",
).images.to(latents.dtype)
with torch.no_grad():
rgb_multistep_orig = self.vae_decode(
self.pipe.vae, latents_multistep_orig
)
guidance_out.update(
{
"latents_multistep_orig": latents_multistep_orig,
"rgb_multistep_orig": rgb_multistep_orig.permute(0, 2, 3, 1),
}
)
if self.cfg.guidance_type == "vsd":
guidance_out.update(
{
"loss_train_phi": loss_train_phi,
}
)
return guidance_out
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
# clip grad for stable training as demonstrated in
# Debiasing Scores and Prompts of 2D Diffusion for Robust Text-to-3D Generation
# http://arxiv.org/abs/2303.15413
if self.cfg.grad_clip is not None:
self.grad_clip_val = C(self.cfg.grad_clip, epoch, global_step)
self.min_step = int(
self.num_train_timesteps * C(self.cfg.min_step_percent, epoch, global_step)
)
self.max_step = int(
self.num_train_timesteps * C(self.cfg.max_step_percent, epoch, global_step)
)

View File

@@ -0,0 +1,253 @@
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.mesh import Mesh
from threestudio.utils.typing import *
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:
threestudio.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

View File

@@ -0,0 +1,9 @@
from . import (
base,
diffuse_with_point_light_material,
hybrid_rgb_latent_material,
neural_radiance_material,
no_material,
pbr_material,
sd_latent_adapter_material,
)

View File

@@ -0,0 +1,29 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.utils.base import BaseModule
from threestudio.utils.typing import *
class BaseMaterial(BaseModule):
@dataclass
class Config(BaseModule.Config):
pass
cfg: Config
requires_normal: bool = False
requires_tangent: bool = False
def configure(self):
pass
def forward(self, *args, **kwargs) -> Float[Tensor, "*B 3"]:
raise NotImplementedError
def export(self, *args, **kwargs) -> Dict[str, Any]:
return {}

View File

@@ -0,0 +1,120 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.utils.ops import dot, get_activation
from threestudio.utils.typing import *
@threestudio.register("diffuse-with-point-light-material")
class DiffuseWithPointLightMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
ambient_light_color: Tuple[float, float, float] = (0.1, 0.1, 0.1)
diffuse_light_color: Tuple[float, float, float] = (0.9, 0.9, 0.9)
ambient_only_steps: int = 1000
diffuse_prob: float = 0.75
textureless_prob: float = 0.5
albedo_activation: str = "sigmoid"
soft_shading: bool = False
cfg: Config
def configure(self) -> None:
self.requires_normal = True
self.ambient_light_color: Float[Tensor, "3"]
self.register_buffer(
"ambient_light_color",
torch.as_tensor(self.cfg.ambient_light_color, dtype=torch.float32),
)
self.diffuse_light_color: Float[Tensor, "3"]
self.register_buffer(
"diffuse_light_color",
torch.as_tensor(self.cfg.diffuse_light_color, dtype=torch.float32),
)
self.ambient_only = False
def forward(
self,
features: Float[Tensor, "B ... Nf"],
positions: Float[Tensor, "B ... 3"],
shading_normal: Float[Tensor, "B ... 3"],
light_positions: Float[Tensor, "B ... 3"],
ambient_ratio: Optional[float] = None,
shading: Optional[str] = None,
**kwargs,
) -> Float[Tensor, "B ... 3"]:
albedo = get_activation(self.cfg.albedo_activation)(features[..., :3])
if ambient_ratio is not None:
# if ambient ratio is specified, use it
diffuse_light_color = (1 - ambient_ratio) * torch.ones_like(
self.diffuse_light_color
)
ambient_light_color = ambient_ratio * torch.ones_like(
self.ambient_light_color
)
elif self.training and self.cfg.soft_shading:
# otherwise if in training and soft shading is enabled, random a ambient ratio
diffuse_light_color = torch.full_like(
self.diffuse_light_color, random.random()
)
ambient_light_color = 1.0 - diffuse_light_color
else:
# otherwise use the default fixed values
diffuse_light_color = self.diffuse_light_color
ambient_light_color = self.ambient_light_color
light_directions: Float[Tensor, "B ... 3"] = F.normalize(
light_positions - positions, dim=-1
)
diffuse_light: Float[Tensor, "B ... 3"] = (
dot(shading_normal, light_directions).clamp(min=0.0) * diffuse_light_color
)
textureless_color = diffuse_light + ambient_light_color
# clamp albedo to [0, 1] to compute shading
color = albedo.clamp(0.0, 1.0) * textureless_color
if shading is None:
if self.training:
# adopt the same type of augmentation for the whole batch
if self.ambient_only or random.random() > self.cfg.diffuse_prob:
shading = "albedo"
elif random.random() < self.cfg.textureless_prob:
shading = "textureless"
else:
shading = "diffuse"
else:
if self.ambient_only:
shading = "albedo"
else:
# return shaded color by default in evaluation
shading = "diffuse"
# multiply by 0 to prevent checking for unused parameters in DDP
if shading == "albedo":
return albedo + textureless_color * 0
elif shading == "textureless":
return albedo * 0 + textureless_color
elif shading == "diffuse":
return color
else:
raise ValueError(f"Unknown shading type {shading}")
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
if global_step < self.cfg.ambient_only_steps:
self.ambient_only = True
else:
self.ambient_only = False
def export(self, features: Float[Tensor, "*N Nf"], **kwargs) -> Dict[str, Any]:
albedo = get_activation(self.cfg.albedo_activation)(features[..., :3]).clamp(
0.0, 1.0
)
return {"albedo": albedo}

View File

@@ -0,0 +1,36 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import dot, get_activation
from threestudio.utils.typing import *
@threestudio.register("hybrid-rgb-latent-material")
class HybridRGBLatentMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
n_output_dims: int = 3
color_activation: str = "sigmoid"
requires_normal: bool = True
cfg: Config
def configure(self) -> None:
self.requires_normal = self.cfg.requires_normal
def forward(
self, features: Float[Tensor, "B ... Nf"], **kwargs
) -> Float[Tensor, "B ... Nc"]:
assert (
features.shape[-1] == self.cfg.n_output_dims
), f"Expected {self.cfg.n_output_dims} output dims, only got {features.shape[-1]} dims input."
color = features
color[..., :3] = get_activation(self.cfg.color_activation)(color[..., :3])
return color

View File

@@ -0,0 +1,54 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import dot, get_activation
from threestudio.utils.typing import *
@threestudio.register("neural-radiance-material")
class NeuralRadianceMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
input_feature_dims: int = 8
color_activation: str = "sigmoid"
dir_encoding_config: dict = field(
default_factory=lambda: {"otype": "SphericalHarmonics", "degree": 3}
)
mlp_network_config: dict = field(
default_factory=lambda: {
"otype": "FullyFusedMLP",
"activation": "ReLU",
"n_neurons": 16,
"n_hidden_layers": 2,
}
)
cfg: Config
def configure(self) -> None:
self.encoding = get_encoding(3, self.cfg.dir_encoding_config)
self.n_input_dims = self.cfg.input_feature_dims + self.encoding.n_output_dims # type: ignore
self.network = get_mlp(self.n_input_dims, 3, self.cfg.mlp_network_config)
def forward(
self,
features: Float[Tensor, "*B Nf"],
viewdirs: Float[Tensor, "*B 3"],
**kwargs,
) -> Float[Tensor, "*B 3"]:
# viewdirs and normals must be normalized before passing to this function
viewdirs = (viewdirs + 1.0) / 2.0 # (-1, 1) => (0, 1)
viewdirs_embd = self.encoding(viewdirs.view(-1, 3))
network_inp = torch.cat(
[features.view(-1, features.shape[-1]), viewdirs_embd], dim=-1
)
color = self.network(network_inp).view(*features.shape[:-1], 3)
color = get_activation(self.cfg.color_activation)(color)
return color

View File

@@ -0,0 +1,63 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.networks import get_encoding, get_mlp
from threestudio.utils.ops import dot, get_activation
from threestudio.utils.typing import *
@threestudio.register("no-material")
class NoMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
n_output_dims: int = 3
color_activation: str = "sigmoid"
input_feature_dims: Optional[int] = None
mlp_network_config: Optional[dict] = None
requires_normal: bool = False
cfg: Config
def configure(self) -> None:
self.use_network = False
if (
self.cfg.input_feature_dims is not None
and self.cfg.mlp_network_config is not None
):
self.network = get_mlp(
self.cfg.input_feature_dims,
self.cfg.n_output_dims,
self.cfg.mlp_network_config,
)
self.use_network = True
self.requires_normal = self.cfg.requires_normal
def forward(
self, features: Float[Tensor, "B ... Nf"], **kwargs
) -> Float[Tensor, "B ... Nc"]:
if not self.use_network:
assert (
features.shape[-1] == self.cfg.n_output_dims
), f"Expected {self.cfg.n_output_dims} output dims, only got {features.shape[-1]} dims input."
color = get_activation(self.cfg.color_activation)(features)
else:
color = self.network(features.view(-1, features.shape[-1])).view(
*features.shape[:-1], self.cfg.n_output_dims
)
color = get_activation(self.cfg.color_activation)(color)
return color
def export(self, features: Float[Tensor, "*N Nf"], **kwargs) -> Dict[str, Any]:
color = self(features, **kwargs).clamp(0, 1)
assert color.shape[-1] >= 3, "Output color must have at least 3 channels"
if color.shape[-1] > 3:
threestudio.warn(
"Output color has >3 channels, treating the first 3 as RGB"
)
return {"albedo": color[..., :3]}

View File

@@ -0,0 +1,143 @@
import random
from dataclasses import dataclass, field
import envlight
import numpy as np
import nvdiffrast.torch as dr
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
@threestudio.register("pbr-material")
class PBRMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
material_activation: str = "sigmoid"
environment_texture: str = "load/lights/mud_road_puresky_1k.hdr"
environment_scale: float = 2.0
min_metallic: float = 0.0
max_metallic: float = 0.9
min_roughness: float = 0.08
max_roughness: float = 0.9
use_bump: bool = True
cfg: Config
def configure(self) -> None:
self.requires_normal = True
self.requires_tangent = self.cfg.use_bump
self.light = envlight.EnvLight(
self.cfg.environment_texture, scale=self.cfg.environment_scale
)
FG_LUT = torch.from_numpy(
np.fromfile("load/lights/bsdf_256_256.bin", dtype=np.float32).reshape(
1, 256, 256, 2
)
)
self.register_buffer("FG_LUT", FG_LUT)
def forward(
self,
features: Float[Tensor, "*B Nf"],
viewdirs: Float[Tensor, "*B 3"],
shading_normal: Float[Tensor, "B ... 3"],
tangent: Optional[Float[Tensor, "B ... 3"]] = None,
**kwargs,
) -> Float[Tensor, "*B 3"]:
prefix_shape = features.shape[:-1]
material: Float[Tensor, "*B Nf"] = get_activation(self.cfg.material_activation)(
features
)
albedo = material[..., :3]
metallic = (
material[..., 3:4] * (self.cfg.max_metallic - self.cfg.min_metallic)
+ self.cfg.min_metallic
)
roughness = (
material[..., 4:5] * (self.cfg.max_roughness - self.cfg.min_roughness)
+ self.cfg.min_roughness
)
if self.cfg.use_bump:
assert tangent is not None
# perturb_normal is a delta to the initialization [0, 0, 1]
perturb_normal = (material[..., 5:8] * 2 - 1) + torch.tensor(
[0, 0, 1], dtype=material.dtype, device=material.device
)
perturb_normal = F.normalize(perturb_normal.clamp(-1, 1), dim=-1)
# apply normal perturbation in tangent space
bitangent = F.normalize(torch.cross(tangent, shading_normal), dim=-1)
shading_normal = (
tangent * perturb_normal[..., 0:1]
- bitangent * perturb_normal[..., 1:2]
+ shading_normal * perturb_normal[..., 2:3]
)
shading_normal = F.normalize(shading_normal, dim=-1)
v = -viewdirs
n_dot_v = (shading_normal * v).sum(-1, keepdim=True)
reflective = n_dot_v * shading_normal * 2 - v
diffuse_albedo = (1 - metallic) * albedo
fg_uv = torch.cat([n_dot_v, roughness], -1).clamp(0, 1)
fg = dr.texture(
self.FG_LUT,
fg_uv.reshape(1, -1, 1, 2).contiguous(),
filter_mode="linear",
boundary_mode="clamp",
).reshape(*prefix_shape, 2)
F0 = (1 - metallic) * 0.04 + metallic * albedo
specular_albedo = F0 * fg[:, 0:1] + fg[:, 1:2]
diffuse_light = self.light(shading_normal)
specular_light = self.light(reflective, roughness)
color = diffuse_albedo * diffuse_light + specular_albedo * specular_light
color = color.clamp(0.0, 1.0)
return color
def export(self, features: Float[Tensor, "*N Nf"], **kwargs) -> Dict[str, Any]:
material: Float[Tensor, "*N Nf"] = get_activation(self.cfg.material_activation)(
features
)
albedo = material[..., :3]
metallic = (
material[..., 3:4] * (self.cfg.max_metallic - self.cfg.min_metallic)
+ self.cfg.min_metallic
)
roughness = (
material[..., 4:5] * (self.cfg.max_roughness - self.cfg.min_roughness)
+ self.cfg.min_roughness
)
out = {
"albedo": albedo,
"metallic": metallic,
"roughness": roughness,
}
if self.cfg.use_bump:
perturb_normal = (material[..., 5:8] * 2 - 1) + torch.tensor(
[0, 0, 1], dtype=material.dtype, device=material.device
)
perturb_normal = F.normalize(perturb_normal.clamp(-1, 1), dim=-1)
perturb_normal = (perturb_normal + 1) / 2
out.update(
{
"bump": perturb_normal,
}
)
return out

View File

@@ -0,0 +1,42 @@
import random
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.materials.base import BaseMaterial
from threestudio.utils.typing import *
@threestudio.register("sd-latent-adapter-material")
class StableDiffusionLatentAdapterMaterial(BaseMaterial):
@dataclass
class Config(BaseMaterial.Config):
pass
cfg: Config
def configure(self) -> None:
adapter = nn.Parameter(
torch.as_tensor(
[
# R G B
[0.298, 0.207, 0.208], # L1
[0.187, 0.286, 0.173], # L2
[-0.158, 0.189, 0.264], # L3
[-0.184, -0.271, -0.473], # L4
]
)
)
self.register_parameter("adapter", adapter)
def forward(
self, features: Float[Tensor, "B ... 4"], **kwargs
) -> Float[Tensor, "B ... 3"]:
assert features.shape[-1] == 4
color = features @ self.adapter
color = (color + 1) / 2
color = color.clamp(0.0, 1.0)
return color

309
threestudio/models/mesh.py Normal file
View File

@@ -0,0 +1,309 @@
from __future__ import annotations
import numpy as np
import torch
import torch.nn.functional as F
import threestudio
from threestudio.utils.ops import dot
from threestudio.utils.typing import *
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) -> None:
self.extras[k] = v
def remove_outlier(self, outlier_n_faces_threshold: Union[int, float]) -> Mesh:
if self.requires_grad:
threestudio.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
threestudio.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
threestudio.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
threestudio.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
threestudio.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)
# 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 = {}
):
threestudio.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)
atlas.generate(co, po)
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

View File

@@ -0,0 +1,411 @@
import math
import tinycudann as tcnn
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.utils.base import Updateable
from threestudio.utils.config import config_to_primitive
from threestudio.utils.misc import get_rank
from threestudio.utils.ops import get_activation
from threestudio.utils.typing import *
class ProgressiveBandFrequency(nn.Module, Updateable):
def __init__(self, in_channels: int, config: dict):
super().__init__()
self.N_freqs = config["n_frequencies"]
self.in_channels, self.n_input_dims = in_channels, in_channels
self.funcs = [torch.sin, torch.cos]
self.freq_bands = 2 ** torch.linspace(0, self.N_freqs - 1, self.N_freqs)
self.n_output_dims = self.in_channels * (len(self.funcs) * self.N_freqs)
self.n_masking_step = config.get("n_masking_step", 0)
self.update_step(
None, None
) # mask should be updated at the beginning each step
def forward(self, x):
out = []
for freq, mask in zip(self.freq_bands, self.mask):
for func in self.funcs:
out += [func(freq * x) * mask]
return torch.cat(out, -1)
def update_step(self, epoch, global_step, on_load_weights=False):
if self.n_masking_step <= 0 or global_step is None:
self.mask = torch.ones(self.N_freqs, dtype=torch.float32)
else:
self.mask = (
1.0
- torch.cos(
math.pi
* (
global_step / self.n_masking_step * self.N_freqs
- torch.arange(0, self.N_freqs)
).clamp(0, 1)
)
) / 2.0
threestudio.debug(
f"Update mask: {global_step}/{self.n_masking_step} {self.mask}"
)
class TCNNEncoding(nn.Module):
def __init__(self, in_channels, config, dtype=torch.float32) -> None:
super().__init__()
self.n_input_dims = in_channels
with torch.cuda.device(get_rank()):
self.encoding = tcnn.Encoding(in_channels, config, dtype=dtype)
self.n_output_dims = self.encoding.n_output_dims
def forward(self, x):
return self.encoding(x)
# 4D implicit decomposition of space and time (4D-fy)
class TCNNEncodingSpatialTime(nn.Module):
def __init__(
self, in_channels, config, dtype=torch.float32, init_time_zero=False
) -> None:
super().__init__()
self.n_input_dims = in_channels
config["otype"] = "HashGrid"
self.num_frames = 1 # config["num_frames"]
self.static = config["static"]
self.cfg = config_to_primitive(config)
self.cfg_time = self.cfg
self.n_key_frames = config.get("n_key_frames", 1)
with torch.cuda.device(get_rank()):
self.encoding = tcnn.Encoding(self.n_input_dims, self.cfg, dtype=dtype)
self.encoding_time = tcnn.Encoding(
self.n_input_dims + 1, self.cfg_time, dtype=dtype
)
self.n_output_dims = self.encoding.n_output_dims
self.frame_time = None
if self.static:
self.set_temp_param_grad(requires_grad=False)
self.use_key_frame = config.get("use_key_frame", False)
self.is_video = True
self.update_occ_grid = False
def set_temp_param_grad(self, requires_grad=False):
self.set_param_grad(self.encoding_time, requires_grad=requires_grad)
def set_param_grad(self, param_list, requires_grad=False):
if isinstance(param_list, nn.Parameter):
param_list.requires_grad = requires_grad
else:
for param in param_list.parameters():
param.requires_grad = requires_grad
def forward(self, x):
# TODO frame_time only supports batch_size == 1 cases
if self.update_occ_grid and not isinstance(self.frame_time, float):
frame_time = self.frame_time
else:
if (self.static or not self.training) and self.frame_time is None:
frame_time = torch.zeros(
(self.num_frames, 1), device=x.device, dtype=x.dtype
).expand(x.shape[0], 1)
else:
if self.frame_time is None:
frame_time = 0.0
else:
frame_time = self.frame_time
frame_time = (
torch.ones((self.num_frames, 1), device=x.device, dtype=x.dtype)
* frame_time
).expand(x.shape[0], 1)
frame_time = frame_time.view(-1, 1)
enc_space = self.encoding(x)
x_frame_time = torch.cat((x, frame_time), 1)
enc_space_time = self.encoding_time(x_frame_time)
enc = enc_space + enc_space_time
return enc
class ProgressiveBandHashGrid(nn.Module, Updateable):
def __init__(self, in_channels, config, dtype=torch.float32):
super().__init__()
self.n_input_dims = in_channels
encoding_config = config.copy()
encoding_config["otype"] = "Grid"
encoding_config["type"] = "Hash"
with torch.cuda.device(get_rank()):
self.encoding = tcnn.Encoding(in_channels, encoding_config, dtype=dtype)
self.n_output_dims = self.encoding.n_output_dims
self.n_level = config["n_levels"]
self.n_features_per_level = config["n_features_per_level"]
self.start_level, self.start_step, self.update_steps = (
config["start_level"],
config["start_step"],
config["update_steps"],
)
self.current_level = self.start_level
self.mask = torch.zeros(
self.n_level * self.n_features_per_level,
dtype=torch.float32,
device=get_rank(),
)
def forward(self, x):
enc = self.encoding(x)
enc = enc * self.mask
return enc
def update_step(self, epoch, global_step, on_load_weights=False):
current_level = min(
self.start_level
+ max(global_step - self.start_step, 0) // self.update_steps,
self.n_level,
)
if current_level > self.current_level:
threestudio.debug(f"Update current level to {current_level}")
self.current_level = current_level
self.mask[: self.current_level * self.n_features_per_level] = 1.0
class CompositeEncoding(nn.Module, Updateable):
def __init__(self, encoding, include_xyz=False, xyz_scale=2.0, xyz_offset=-1.0):
super(CompositeEncoding, self).__init__()
self.encoding = encoding
self.include_xyz, self.xyz_scale, self.xyz_offset = (
include_xyz,
xyz_scale,
xyz_offset,
)
self.n_output_dims = (
int(self.include_xyz) * self.encoding.n_input_dims
+ self.encoding.n_output_dims
)
def forward(self, x, *args):
return (
self.encoding(x, *args)
if not self.include_xyz
else torch.cat(
[x * self.xyz_scale + self.xyz_offset, self.encoding(x, *args)], dim=-1
)
)
def get_encoding(n_input_dims: int, config) -> nn.Module:
# input suppose to be range [0, 1]
encoding: nn.Module
if config.otype == "ProgressiveBandFrequency":
encoding = ProgressiveBandFrequency(n_input_dims, config_to_primitive(config))
elif config.otype == "ProgressiveBandHashGrid":
encoding = ProgressiveBandHashGrid(n_input_dims, config_to_primitive(config))
elif config.otype == "HashGridSpatialTime":
encoding = TCNNEncodingSpatialTime(n_input_dims, config) # 4D-fy encoding
else:
encoding = TCNNEncoding(n_input_dims, config_to_primitive(config))
encoding = CompositeEncoding(
encoding,
include_xyz=config.get("include_xyz", False),
xyz_scale=2.0,
xyz_offset=-1.0,
) # FIXME: hard coded
return encoding
class VanillaMLP(nn.Module):
def __init__(self, dim_in: int, dim_out: int, config: dict):
super().__init__()
self.n_neurons, self.n_hidden_layers = (
config["n_neurons"],
config["n_hidden_layers"],
)
layers = [
self.make_linear(dim_in, self.n_neurons, is_first=True, is_last=False),
self.make_activation(),
]
for i in range(self.n_hidden_layers - 1):
layers += [
self.make_linear(
self.n_neurons, self.n_neurons, is_first=False, is_last=False
),
self.make_activation(),
]
layers += [
self.make_linear(self.n_neurons, dim_out, is_first=False, is_last=True)
]
self.layers = nn.Sequential(*layers)
self.output_activation = get_activation(config.get("output_activation", None))
def forward(self, x):
# disable autocast
# strange that the parameters will have empty gradients if autocast is enabled in AMP
with torch.cuda.amp.autocast(enabled=False):
x = self.layers(x)
x = self.output_activation(x)
return x
def make_linear(self, dim_in, dim_out, is_first, is_last):
layer = nn.Linear(dim_in, dim_out, bias=False)
return layer
def make_activation(self):
return nn.ReLU(inplace=True)
class SphereInitVanillaMLP(nn.Module):
def __init__(self, dim_in, dim_out, config):
super().__init__()
self.n_neurons, self.n_hidden_layers = (
config["n_neurons"],
config["n_hidden_layers"],
)
self.sphere_init, self.weight_norm = True, True
self.sphere_init_radius = config["sphere_init_radius"]
self.sphere_init_inside_out = config["inside_out"]
self.layers = [
self.make_linear(dim_in, self.n_neurons, is_first=True, is_last=False),
self.make_activation(),
]
for i in range(self.n_hidden_layers - 1):
self.layers += [
self.make_linear(
self.n_neurons, self.n_neurons, is_first=False, is_last=False
),
self.make_activation(),
]
self.layers += [
self.make_linear(self.n_neurons, dim_out, is_first=False, is_last=True)
]
self.layers = nn.Sequential(*self.layers)
self.output_activation = get_activation(config.get("output_activation", None))
def forward(self, x):
# disable autocast
# strange that the parameters will have empty gradients if autocast is enabled in AMP
with torch.cuda.amp.autocast(enabled=False):
x = self.layers(x)
x = self.output_activation(x)
return x
def make_linear(self, dim_in, dim_out, is_first, is_last):
layer = nn.Linear(dim_in, dim_out, bias=True)
if is_last:
if not self.sphere_init_inside_out:
torch.nn.init.constant_(layer.bias, -self.sphere_init_radius)
torch.nn.init.normal_(
layer.weight,
mean=math.sqrt(math.pi) / math.sqrt(dim_in),
std=0.0001,
)
else:
torch.nn.init.constant_(layer.bias, self.sphere_init_radius)
torch.nn.init.normal_(
layer.weight,
mean=-math.sqrt(math.pi) / math.sqrt(dim_in),
std=0.0001,
)
elif is_first:
torch.nn.init.constant_(layer.bias, 0.0)
torch.nn.init.constant_(layer.weight[:, 3:], 0.0)
torch.nn.init.normal_(
layer.weight[:, :3], 0.0, math.sqrt(2) / math.sqrt(dim_out)
)
else:
torch.nn.init.constant_(layer.bias, 0.0)
torch.nn.init.normal_(layer.weight, 0.0, math.sqrt(2) / math.sqrt(dim_out))
if self.weight_norm:
layer = nn.utils.weight_norm(layer)
return layer
def make_activation(self):
return nn.Softplus(beta=100)
class TCNNNetwork(nn.Module):
def __init__(self, dim_in: int, dim_out: int, config: dict) -> None:
super().__init__()
with torch.cuda.device(get_rank()):
self.network = tcnn.Network(dim_in, dim_out, config)
def forward(self, x):
return self.network(x).float() # transform to float32
def get_mlp(n_input_dims, n_output_dims, config) -> nn.Module:
network: nn.Module
if config.otype == "VanillaMLP":
network = VanillaMLP(n_input_dims, n_output_dims, config_to_primitive(config))
elif config.otype == "SphereInitVanillaMLP":
network = SphereInitVanillaMLP(
n_input_dims, n_output_dims, config_to_primitive(config)
)
else:
assert (
config.get("sphere_init", False) is False
), "sphere_init=True only supported by VanillaMLP"
network = TCNNNetwork(n_input_dims, n_output_dims, config_to_primitive(config))
return network
class NetworkWithInputEncoding(nn.Module, Updateable):
def __init__(self, encoding, network):
super().__init__()
self.encoding, self.network = encoding, network
def forward(self, x):
return self.network(self.encoding(x))
class TCNNNetworkWithInputEncoding(nn.Module):
def __init__(
self,
n_input_dims: int,
n_output_dims: int,
encoding_config: dict,
network_config: dict,
) -> None:
super().__init__()
with torch.cuda.device(get_rank()):
self.network_with_input_encoding = tcnn.NetworkWithInputEncoding(
n_input_dims=n_input_dims,
n_output_dims=n_output_dims,
encoding_config=encoding_config,
network_config=network_config,
)
def forward(self, x):
return self.network_with_input_encoding(x).float() # transform to float32
def create_network_with_input_encoding(
n_input_dims: int, n_output_dims: int, encoding_config, network_config
) -> nn.Module:
# input suppose to be range [0, 1]
network_with_input_encoding: nn.Module
if encoding_config.otype in [
"VanillaFrequency",
"ProgressiveBandHashGrid",
] or network_config.otype in ["VanillaMLP", "SphereInitVanillaMLP"]:
encoding = get_encoding(n_input_dims, encoding_config)
network = get_mlp(encoding.n_output_dims, n_output_dims, network_config)
network_with_input_encoding = NetworkWithInputEncoding(encoding, network)
else:
network_with_input_encoding = TCNNNetworkWithInputEncoding(
n_input_dims=n_input_dims,
n_output_dims=n_output_dims,
encoding_config=config_to_primitive(encoding_config),
network_config=config_to_primitive(network_config),
)
return network_with_input_encoding
class ToDTypeWrapper(nn.Module):
def __init__(self, module: nn.Module, dtype: torch.dtype):
super().__init__()
self.module = module
self.dtype = dtype
def forward(self, x: Float[Tensor, "..."]) -> Float[Tensor, "..."]:
return self.module(x).to(self.dtype)

View File

@@ -0,0 +1,7 @@
from . import (
base,
deepfloyd_prompt_processor,
dummy_prompt_processor,
stable_diffusion_prompt_processor,
clip_prompt_processor,
)

View File

@@ -0,0 +1,517 @@
import json
import os
from dataclasses import dataclass, field
import torch
import torch.multiprocessing as mp
import torch.nn as nn
import torch.nn.functional as F
from pytorch_lightning.utilities.rank_zero import rank_zero_only
from transformers import AutoTokenizer, BertForMaskedLM
import threestudio
from threestudio.utils.base import BaseObject
from threestudio.utils.misc import barrier, cleanup, get_rank
from threestudio.utils.ops import shifted_cosine_decay, shifted_expotional_decay
from threestudio.utils.typing import *
def hash_prompt(model: str, prompt: str) -> str:
import hashlib
identifier = f"{model}-{prompt}"
return hashlib.md5(identifier.encode()).hexdigest()
@dataclass
class DirectionConfig:
name: str
prompt: Callable[[str], str]
negative_prompt: Callable[[str], str]
condition: Callable[
[Float[Tensor, "B"], Float[Tensor, "B"], Float[Tensor, "B"]],
Float[Tensor, "B"],
]
@dataclass
class PromptProcessorOutput:
text_embeddings: Float[Tensor, "N Nf"]
uncond_text_embeddings: Float[Tensor, "N Nf"]
text_embeddings_vd: Float[Tensor, "Nv N Nf"]
uncond_text_embeddings_vd: Float[Tensor, "Nv N Nf"]
directions: List[DirectionConfig]
direction2idx: Dict[str, int]
use_perp_neg: bool
perp_neg_f_sb: Tuple[float, float, float]
perp_neg_f_fsb: Tuple[float, float, float]
perp_neg_f_fs: Tuple[float, float, float]
perp_neg_f_sf: Tuple[float, float, float]
def get_text_embeddings(
self,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
view_dependent_prompting: bool = True,
) -> Float[Tensor, "BB N Nf"]:
batch_size = elevation.shape[0]
if view_dependent_prompting:
# Get direction
direction_idx = torch.zeros_like(elevation, dtype=torch.long)
for d in self.directions:
direction_idx[
d.condition(elevation, azimuth, camera_distances)
] = self.direction2idx[d.name]
# Get text embeddings
text_embeddings = self.text_embeddings_vd[direction_idx] # type: ignore
uncond_text_embeddings = self.uncond_text_embeddings_vd[direction_idx] # type: ignore
else:
text_embeddings = self.text_embeddings.expand(batch_size, -1, -1) # type: ignore
uncond_text_embeddings = self.uncond_text_embeddings.expand( # type: ignore
batch_size, -1, -1
)
# IMPORTANT: we return (cond, uncond), which is in different order than other implementations!
return torch.cat([text_embeddings, uncond_text_embeddings], dim=0)
def get_text_embeddings_perp_neg(
self,
elevation: Float[Tensor, "B"],
azimuth: Float[Tensor, "B"],
camera_distances: Float[Tensor, "B"],
view_dependent_prompting: bool = True,
) -> Tuple[Float[Tensor, "BBBB N Nf"], Float[Tensor, "B 2"]]:
assert (
view_dependent_prompting
), "Perp-Neg only works with view-dependent prompting"
batch_size = elevation.shape[0]
direction_idx = torch.zeros_like(elevation, dtype=torch.long)
for d in self.directions:
direction_idx[
d.condition(elevation, azimuth, camera_distances)
] = self.direction2idx[d.name]
# 0 - side view
# 1 - front view
# 2 - back view
# 3 - overhead view
pos_text_embeddings = []
neg_text_embeddings = []
neg_guidance_weights = []
uncond_text_embeddings = []
side_emb = self.text_embeddings_vd[0]
front_emb = self.text_embeddings_vd[1]
back_emb = self.text_embeddings_vd[2]
overhead_emb = self.text_embeddings_vd[3]
for idx, ele, azi, dis in zip(
direction_idx, elevation, azimuth, camera_distances
):
azi = shift_azimuth_deg(azi) # to (-180, 180)
uncond_text_embeddings.append(
self.uncond_text_embeddings_vd[idx]
) # should be ""
if idx.item() == 3: # overhead view
pos_text_embeddings.append(overhead_emb) # side view
# dummy
neg_text_embeddings += [
self.uncond_text_embeddings_vd[idx],
self.uncond_text_embeddings_vd[idx],
]
neg_guidance_weights += [0.0, 0.0]
else: # interpolating views
if torch.abs(azi) < 90:
# front-side interpolation
# 0 - complete side, 1 - complete front
r_inter = 1 - torch.abs(azi) / 90
pos_text_embeddings.append(
r_inter * front_emb + (1 - r_inter) * side_emb
)
neg_text_embeddings += [front_emb, side_emb]
neg_guidance_weights += [
-shifted_expotional_decay(*self.perp_neg_f_fs, r_inter),
-shifted_expotional_decay(*self.perp_neg_f_sf, 1 - r_inter),
]
else:
# side-back interpolation
# 0 - complete back, 1 - complete side
r_inter = 2.0 - torch.abs(azi) / 90
pos_text_embeddings.append(
r_inter * side_emb + (1 - r_inter) * back_emb
)
neg_text_embeddings += [side_emb, front_emb]
neg_guidance_weights += [
-shifted_expotional_decay(*self.perp_neg_f_sb, r_inter),
-shifted_expotional_decay(*self.perp_neg_f_fsb, r_inter),
]
text_embeddings = torch.cat(
[
torch.stack(pos_text_embeddings, dim=0),
torch.stack(uncond_text_embeddings, dim=0),
torch.stack(neg_text_embeddings, dim=0),
],
dim=0,
)
return text_embeddings, torch.as_tensor(
neg_guidance_weights, device=elevation.device
).reshape(batch_size, 2)
def shift_azimuth_deg(azimuth: Float[Tensor, "..."]) -> Float[Tensor, "..."]:
# shift azimuth angle (in degrees), to [-180, 180]
return (azimuth + 180) % 360 - 180
class PromptProcessor(BaseObject):
@dataclass
class Config(BaseObject.Config):
prompt: str = "a hamburger"
# manually assigned view-dependent prompts
prompt_front: Optional[str] = None
prompt_side: Optional[str] = None
prompt_back: Optional[str] = None
prompt_overhead: Optional[str] = None
negative_prompt: str = ""
pretrained_model_name_or_path: str = "runwayml/stable-diffusion-v1-5"
overhead_threshold: float = 60.0
front_threshold: float = 45.0
back_threshold: float = 45.0
view_dependent_prompt_front: bool = False
use_cache: bool = True
spawn: bool = True
# perp neg
use_perp_neg: bool = False
# a*e(-b*r) + c
# a * e(-b) + c = 0
perp_neg_f_sb: Tuple[float, float, float] = (1, 0.5, -0.606)
perp_neg_f_fsb: Tuple[float, float, float] = (1, 0.5, +0.967)
perp_neg_f_fs: Tuple[float, float, float] = (
4,
0.5,
-2.426,
) # f_fs(1) = 0, a, b > 0
perp_neg_f_sf: Tuple[float, float, float] = (4, 0.5, -2.426)
# prompt debiasing
use_prompt_debiasing: bool = False
pretrained_model_name_or_path_prompt_debiasing: str = "bert-base-uncased"
# index of words that can potentially be removed
prompt_debiasing_mask_ids: Optional[List[int]] = None
cfg: Config
@rank_zero_only
def configure_text_encoder(self) -> None:
raise NotImplementedError
@rank_zero_only
def destroy_text_encoder(self) -> None:
raise NotImplementedError
def configure(self) -> None:
self._cache_dir = ".threestudio_cache/text_embeddings" # FIXME: hard-coded path
# view-dependent text embeddings
self.directions: List[DirectionConfig]
if self.cfg.view_dependent_prompt_front:
self.directions = [
DirectionConfig(
"side",
lambda s: f"side view of {s}",
lambda s: s,
lambda ele, azi, dis: torch.ones_like(ele, dtype=torch.bool),
),
DirectionConfig(
"front",
lambda s: f"front view of {s}",
lambda s: s,
lambda ele, azi, dis: (
shift_azimuth_deg(azi) > -self.cfg.front_threshold
)
& (shift_azimuth_deg(azi) < self.cfg.front_threshold),
),
DirectionConfig(
"back",
lambda s: f"backside view of {s}",
lambda s: s,
lambda ele, azi, dis: (
shift_azimuth_deg(azi) > 180 - self.cfg.back_threshold
)
| (shift_azimuth_deg(azi) < -180 + self.cfg.back_threshold),
),
DirectionConfig(
"overhead",
lambda s: f"overhead view of {s}",
lambda s: s,
lambda ele, azi, dis: ele > self.cfg.overhead_threshold,
),
]
else:
self.directions = [
DirectionConfig(
"side",
lambda s: f"{s}, side view",
lambda s: s,
lambda ele, azi, dis: torch.ones_like(ele, dtype=torch.bool),
),
DirectionConfig(
"front",
lambda s: f"{s}, front view",
lambda s: s,
lambda ele, azi, dis: (
shift_azimuth_deg(azi) > -self.cfg.front_threshold
)
& (shift_azimuth_deg(azi) < self.cfg.front_threshold),
),
DirectionConfig(
"back",
lambda s: f"{s}, back view",
lambda s: s,
lambda ele, azi, dis: (
shift_azimuth_deg(azi) > 180 - self.cfg.back_threshold
)
| (shift_azimuth_deg(azi) < -180 + self.cfg.back_threshold),
),
DirectionConfig(
"overhead",
lambda s: f"{s}, overhead view",
lambda s: s,
lambda ele, azi, dis: ele > self.cfg.overhead_threshold,
),
]
self.direction2idx = {d.name: i for i, d in enumerate(self.directions)}
with open(os.path.join("load/prompt_library.json"), "r") as f:
self.prompt_library = json.load(f)
# use provided prompt or find prompt in library
self.prompt = self.preprocess_prompt(self.cfg.prompt)
# use provided negative prompt
self.negative_prompt = self.cfg.negative_prompt
threestudio.info(
f"Using prompt [{self.prompt}] and negative prompt [{self.negative_prompt}]"
)
# view-dependent prompting
if self.cfg.use_prompt_debiasing:
assert (
self.cfg.prompt_side is None
and self.cfg.prompt_back is None
and self.cfg.prompt_overhead is None
), "Do not manually assign prompt_side, prompt_back or prompt_overhead when using prompt debiasing"
prompts = self.get_debiased_prompt(self.prompt)
self.prompts_vd = [
d.prompt(prompt) for d, prompt in zip(self.directions, prompts)
]
else:
self.prompts_vd = [
self.cfg.get(f"prompt_{d.name}", None) or d.prompt(self.prompt) # type: ignore
for d in self.directions
]
prompts_vd_display = " ".join(
[
f"[{d.name}]:[{prompt}]"
for prompt, d in zip(self.prompts_vd, self.directions)
]
)
threestudio.info(f"Using view-dependent prompts {prompts_vd_display}")
self.negative_prompts_vd = [
d.negative_prompt(self.negative_prompt) for d in self.directions
]
self.prepare_text_embeddings()
self.load_text_embeddings()
@staticmethod
def spawn_func(pretrained_model_name_or_path, prompts, cache_dir, device):
raise NotImplementedError
@rank_zero_only
def prepare_text_embeddings(self):
os.makedirs(self._cache_dir, exist_ok=True)
all_prompts = (
[self.prompt]
+ [self.negative_prompt]
+ self.prompts_vd
+ self.negative_prompts_vd
)
prompts_to_process = []
for prompt in all_prompts:
if self.cfg.use_cache:
# some text embeddings are already in cache
# do not process them
cache_path = os.path.join(
self._cache_dir,
f"{hash_prompt(self.cfg.pretrained_model_name_or_path, prompt)}.pt",
)
if os.path.exists(cache_path):
threestudio.debug(
f"Text embeddings for model {self.cfg.pretrained_model_name_or_path} and prompt [{prompt}] are already in cache, skip processing."
)
continue
prompts_to_process.append(prompt)
if len(prompts_to_process) > 0:
if self.cfg.spawn:
ctx = mp.get_context("spawn")
subprocess = ctx.Process(
target=self.spawn_func,
args=(
self.cfg.pretrained_model_name_or_path,
prompts_to_process,
self._cache_dir,
self.device
),
)
subprocess.start()
subprocess.join()
else:
self.spawn_func(
self.cfg.pretrained_model_name_or_path,
prompts_to_process,
self._cache_dir,
self.device
)
cleanup()
def load_text_embeddings(self):
# synchronize, to ensure the text embeddings have been computed and saved to cache
barrier()
self.text_embeddings = self.load_from_cache(self.prompt)[None, ...]
self.uncond_text_embeddings = self.load_from_cache(self.negative_prompt)[
None, ...
]
self.text_embeddings_vd = torch.stack(
[self.load_from_cache(prompt) for prompt in self.prompts_vd], dim=0
)
self.uncond_text_embeddings_vd = torch.stack(
[self.load_from_cache(prompt) for prompt in self.negative_prompts_vd], dim=0
)
threestudio.debug(f"Loaded text embeddings.")
def load_from_cache(self, prompt):
cache_path = os.path.join(
self._cache_dir,
f"{hash_prompt(self.cfg.pretrained_model_name_or_path, prompt)}.pt",
)
if not os.path.exists(cache_path):
raise FileNotFoundError(
f"Text embedding file {cache_path} for model {self.cfg.pretrained_model_name_or_path} and prompt [{prompt}] not found."
)
return torch.load(cache_path, map_location=self.device)
def preprocess_prompt(self, prompt: str) -> str:
if prompt.startswith("lib:"):
# find matches in the library
candidate = None
keywords = prompt[4:].lower().split("_")
for prompt in self.prompt_library["dreamfusion"]:
if all([k in prompt.lower() for k in keywords]):
if candidate is not None:
raise ValueError(
f"Multiple prompts matched with keywords {keywords} in library"
)
candidate = prompt
if candidate is None:
raise ValueError(
f"Cannot find prompt with keywords {keywords} in library"
)
threestudio.info("Find matched prompt in library: " + candidate)
return candidate
else:
return prompt
def get_text_embeddings(
self, prompt: Union[str, List[str]], negative_prompt: Union[str, List[str]]
) -> Tuple[Float[Tensor, "B ..."], Float[Tensor, "B ..."]]:
raise NotImplementedError
def get_debiased_prompt(self, prompt: str) -> List[str]:
os.environ["TOKENIZERS_PARALLELISM"] = "false"
tokenizer = AutoTokenizer.from_pretrained(
self.cfg.pretrained_model_name_or_path_prompt_debiasing
)
model = BertForMaskedLM.from_pretrained(
self.cfg.pretrained_model_name_or_path_prompt_debiasing
)
views = [d.name for d in self.directions]
view_ids = tokenizer(" ".join(views), return_tensors="pt").input_ids[0]
view_ids = view_ids[1:5]
def modulate(prompt):
prompt_vd = f"This image is depicting a [MASK] view of {prompt}"
tokens = tokenizer(
prompt_vd,
padding="max_length",
truncation=True,
add_special_tokens=True,
return_tensors="pt",
)
mask_idx = torch.where(tokens.input_ids == tokenizer.mask_token_id)[1]
logits = model(**tokens).logits
logits = F.softmax(logits[0, mask_idx], dim=-1)
logits = logits[0, view_ids]
probes = logits / logits.sum()
return probes
prompts = [prompt.split(" ") for _ in range(4)]
full_probe = modulate(prompt)
n_words = len(prompt.split(" "))
prompt_debiasing_mask_ids = (
self.cfg.prompt_debiasing_mask_ids
if self.cfg.prompt_debiasing_mask_ids is not None
else list(range(n_words))
)
words_to_debias = [prompt.split(" ")[idx] for idx in prompt_debiasing_mask_ids]
threestudio.info(f"Words that can potentially be removed: {words_to_debias}")
for idx in prompt_debiasing_mask_ids:
words = prompt.split(" ")
prompt_ = " ".join(words[:idx] + words[(idx + 1) :])
part_probe = modulate(prompt_)
pmi = full_probe / torch.lerp(part_probe, full_probe, 0.5)
for i in range(pmi.shape[0]):
if pmi[i].item() < 0.95:
prompts[i][idx] = ""
debiased_prompts = [" ".join([word for word in p if word]) for p in prompts]
for d, debiased_prompt in zip(views, debiased_prompts):
threestudio.info(f"Debiased prompt of the {d} view is [{debiased_prompt}]")
del tokenizer, model
cleanup()
return debiased_prompts
def __call__(self) -> PromptProcessorOutput:
return PromptProcessorOutput(
text_embeddings=self.text_embeddings,
uncond_text_embeddings=self.uncond_text_embeddings,
text_embeddings_vd=self.text_embeddings_vd,
uncond_text_embeddings_vd=self.uncond_text_embeddings_vd,
directions=self.directions,
direction2idx=self.direction2idx,
use_perp_neg=self.cfg.use_perp_neg,
perp_neg_f_sb=self.cfg.perp_neg_f_sb,
perp_neg_f_fsb=self.cfg.perp_neg_f_fsb,
perp_neg_f_fs=self.cfg.perp_neg_f_fs,
perp_neg_f_sf=self.cfg.perp_neg_f_sf,
)

View File

@@ -0,0 +1,44 @@
import json
import os
from dataclasses import dataclass
import clip
import torch
import torch
import torch.nn as nn
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessor, hash_prompt
from threestudio.utils.misc import cleanup
from threestudio.utils.typing import *
@threestudio.register("clip-prompt-processor")
class ClipPromptProcessor(PromptProcessor):
@dataclass
class Config(PromptProcessor.Config):
pass
cfg: Config
@staticmethod
def spawn_func(pretrained_model_name_or_path, prompts, cache_dir, device):
os.environ["TOKENIZERS_PARALLELISM"] = "false"
clip_model, _ = clip.load(pretrained_model_name_or_path, jit=False)
with torch.no_grad():
tokens = clip.tokenize(
prompts,
).to(device)
text_embeddings = clip_model.encode_text(tokens)
text_embeddings = text_embeddings / text_embeddings.norm(dim=-1, keepdim=True)
for prompt, embedding in zip(prompts, text_embeddings):
torch.save(
embedding,
os.path.join(
cache_dir,
f"{hash_prompt(pretrained_model_name_or_path, prompt)}.pt",
),
)
del clip_model

View File

@@ -0,0 +1,98 @@
import json
import os
from dataclasses import dataclass
import torch
import torch.nn as nn
from diffusers import IFPipeline
from transformers import T5EncoderModel, T5Tokenizer
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessor, hash_prompt
from threestudio.utils.misc import cleanup
from threestudio.utils.typing import *
@threestudio.register("deep-floyd-prompt-processor")
class DeepFloydPromptProcessor(PromptProcessor):
@dataclass
class Config(PromptProcessor.Config):
pretrained_model_name_or_path: str = "DeepFloyd/IF-I-XL-v1.0"
cfg: Config
### these functions are unused, kept for debugging ###
def configure_text_encoder(self) -> None:
os.environ["TOKENIZERS_PARALLELISM"] = "false"
self.text_encoder = T5EncoderModel.from_pretrained(
self.cfg.pretrained_model_name_or_path,
subfolder="text_encoder",
load_in_8bit=True,
variant="8bit",
device_map="auto",
) # FIXME: behavior of auto device map in multi-GPU training
self.pipe = IFPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path,
text_encoder=self.text_encoder, # pass the previously instantiated 8bit text encoder
unet=None,
)
def destroy_text_encoder(self) -> None:
del self.text_encoder
del self.pipe
cleanup()
def get_text_embeddings(
self, prompt: Union[str, List[str]], negative_prompt: Union[str, List[str]]
) -> Tuple[Float[Tensor, "B 77 4096"], Float[Tensor, "B 77 4096"]]:
text_embeddings, uncond_text_embeddings = self.pipe.encode_prompt(
prompt=prompt, negative_prompt=negative_prompt, device=self.device
)
return text_embeddings, uncond_text_embeddings
###
@staticmethod
def spawn_func(pretrained_model_name_or_path, prompts, cache_dir, device):
max_length = 77
tokenizer = T5Tokenizer.from_pretrained(
pretrained_model_name_or_path,
subfolder="tokenizer",
local_files_only=True
)
text_encoder = T5EncoderModel.from_pretrained(
pretrained_model_name_or_path,
subfolder="text_encoder",
torch_dtype=torch.float16, # suppress warning
load_in_8bit=True,
variant="8bit",
device_map="auto",
local_files_only=True
)
with torch.no_grad():
text_inputs = tokenizer(
prompts,
padding="max_length",
max_length=max_length,
truncation=True,
add_special_tokens=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids
attention_mask = text_inputs.attention_mask
text_embeddings = text_encoder(
text_input_ids.to(text_encoder.device),
attention_mask=attention_mask.to(text_encoder.device),
)
text_embeddings = text_embeddings[0]
for prompt, embedding in zip(prompts, text_embeddings):
torch.save(
embedding,
os.path.join(
cache_dir,
f"{hash_prompt(pretrained_model_name_or_path, prompt)}.pt",
),
)
del text_encoder

View File

@@ -0,0 +1,18 @@
import json
import os
from dataclasses import dataclass
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessor, hash_prompt
from threestudio.utils.misc import cleanup
from threestudio.utils.typing import *
@threestudio.register("dummy-prompt-processor")
class DummyPromptProcessor(PromptProcessor):
@dataclass
class Config(PromptProcessor.Config):
pretrained_model_name_or_path: str = ""
prompt: str = ""
cfg: Config

View File

@@ -0,0 +1,136 @@
import json
import os
from dataclasses import dataclass
import torch
import torch.nn as nn
from transformers import AutoTokenizer, CLIPTextModel
import threestudio
from threestudio.models.prompt_processors.base import PromptProcessor, hash_prompt
from threestudio.utils.misc import cleanup
from threestudio.utils.typing import *
@threestudio.register("stable-diffusion-prompt-processor")
class StableDiffusionPromptProcessor(PromptProcessor):
@dataclass
class Config(PromptProcessor.Config):
pass
cfg: Config
### these functions are unused, kept for debugging ###
def configure_text_encoder(self) -> None:
self.tokenizer = AutoTokenizer.from_pretrained(
self.cfg.pretrained_model_name_or_path, subfolder="tokenizer"
)
os.environ["TOKENIZERS_PARALLELISM"] = "false"
self.text_encoder = CLIPTextModel.from_pretrained(
self.cfg.pretrained_model_name_or_path, subfolder="text_encoder"
).to(self.device)
for p in self.text_encoder.parameters():
p.requires_grad_(False)
def destroy_text_encoder(self) -> None:
del self.tokenizer
del self.text_encoder
cleanup()
def get_text_embeddings(
self, prompt: Union[str, List[str]], negative_prompt: Union[str, List[str]]
) -> Tuple[Float[Tensor, "B 77 768"], Float[Tensor, "B 77 768"]]:
if isinstance(prompt, str):
prompt = [prompt]
if isinstance(negative_prompt, str):
negative_prompt = [negative_prompt]
# Tokenize text and get embeddings
tokens = self.tokenizer(
prompt,
padding="max_length",
max_length=self.tokenizer.model_max_length,
return_tensors="pt",
)
uncond_tokens = self.tokenizer(
negative_prompt,
padding="max_length",
max_length=self.tokenizer.model_max_length,
return_tensors="pt",
)
with torch.no_grad():
text_embeddings = self.text_encoder(tokens.input_ids.to(self.device))[0]
uncond_text_embeddings = self.text_encoder(
uncond_tokens.input_ids.to(self.device)
)[0]
return text_embeddings, uncond_text_embeddings
###
@staticmethod
def spawn_func(pretrained_model_name_or_path, prompts, cache_dir, device):
os.environ["TOKENIZERS_PARALLELISM"] = "false"
tokenizer = AutoTokenizer.from_pretrained(
pretrained_model_name_or_path,
subfolder="tokenizer",
local_files_only=True,
)
text_encoder = CLIPTextModel.from_pretrained(
pretrained_model_name_or_path,
subfolder="text_encoder",
device_map="auto",
local_files_only=True,
)
with torch.no_grad():
tokens = tokenizer(
prompts,
padding="max_length",
max_length=tokenizer.model_max_length,
return_tensors="pt",
)
text_embeddings = text_encoder(tokens.input_ids.to(text_encoder.device))[0]
for prompt, embedding in zip(prompts, text_embeddings):
torch.save(
embedding,
os.path.join(
cache_dir,
f"{hash_prompt(pretrained_model_name_or_path, prompt)}.pt",
),
)
del text_encoder
from transformers.models.clip import CLIPTextModel, CLIPTokenizer
def add_tokens_to_model(learned_embeds_path, text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer, override_token: Optional[Union[str, dict]] = None) -> None:
r"""Adds tokens to the tokenizer and text encoder of a model."""
learned_embeds = torch.load(learned_embeds_path, map_location='cpu')
# Loop over learned embeddings
new_tokens = []
for token, embedding in learned_embeds.items():
embedding = embedding.to(text_encoder.get_input_embeddings().weight.dtype)
if override_token is not None:
token = override_token if isinstance(override_token, str) else override_token[token]
# Add the token to the tokenizer
num_added_tokens = tokenizer.add_tokens(token)
if num_added_tokens == 0:
raise ValueError((f"The tokenizer already contains the token {token}. Please pass a "
"different `token` that is not already in the tokenizer."))
# Resize the token embeddings
text_encoder.resize_token_embeddings(len(tokenizer))
# Get the id for the token and assign the embeds
token_id = tokenizer.convert_tokens_to_ids(token)
text_encoder.get_input_embeddings().weight.data[token_id] = embedding
new_tokens.append(token)
print(f'Added {len(new_tokens)} tokens to tokenizer and text embedding: {new_tokens}')

View File

@@ -0,0 +1,9 @@
from . import (
base,
deferred_volume_renderer,
gan_volume_renderer,
nerf_volume_renderer,
neus_volume_renderer,
nvdiff_rasterizer,
patch_renderer,
)

View File

@@ -0,0 +1,80 @@
from dataclasses import dataclass
import nerfacc
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.utils.base import BaseModule
from threestudio.utils.typing import *
class Renderer(BaseModule):
@dataclass
class Config(BaseModule.Config):
radius: float = 1.0
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
# keep references to submodules using namedtuple, avoid being registered as modules
@dataclass
class SubModules:
geometry: BaseImplicitGeometry
material: BaseMaterial
background: BaseBackground
self.sub_modules = SubModules(geometry, material, background)
# set up bounding box
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,
),
)
def forward(self, *args, **kwargs) -> Dict[str, Any]:
raise NotImplementedError
@property
def geometry(self) -> BaseImplicitGeometry:
return self.sub_modules.geometry
@property
def material(self) -> BaseMaterial:
return self.sub_modules.material
@property
def background(self) -> BaseBackground:
return self.sub_modules.background
def set_geometry(self, geometry: BaseImplicitGeometry) -> None:
self.sub_modules.geometry = geometry
def set_material(self, material: BaseMaterial) -> None:
self.sub_modules.material = material
def set_background(self, background: BaseBackground) -> None:
self.sub_modules.background = background
class VolumeRenderer(Renderer):
pass
class Rasterizer(Renderer):
pass

View File

@@ -0,0 +1,11 @@
from dataclasses import dataclass
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.renderers.base import VolumeRenderer
class DeferredVolumeRenderer(VolumeRenderer):
pass

View File

@@ -0,0 +1,159 @@
from dataclasses import dataclass
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.renderers.base import VolumeRenderer
from threestudio.utils.GAN.discriminator import NLayerDiscriminator, weights_init
from threestudio.utils.GAN.distribution import DiagonalGaussianDistribution
from threestudio.utils.GAN.mobilenet import MobileNetV3 as GlobalEncoder
from threestudio.utils.GAN.vae import Decoder as Generator
from threestudio.utils.GAN.vae import Encoder as LocalEncoder
from threestudio.utils.typing import *
@threestudio.register("gan-volume-renderer")
class GANVolumeRenderer(VolumeRenderer):
@dataclass
class Config(VolumeRenderer.Config):
base_renderer_type: str = ""
base_renderer: Optional[VolumeRenderer.Config] = None
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
self.base_renderer = threestudio.find(self.cfg.base_renderer_type)(
self.cfg.base_renderer,
geometry=geometry,
material=material,
background=background,
)
self.ch_mult = [1, 2, 4]
self.generator = Generator(
ch=64,
out_ch=3,
ch_mult=self.ch_mult,
num_res_blocks=1,
attn_resolutions=[],
dropout=0.0,
resamp_with_conv=True,
in_channels=7,
resolution=512,
z_channels=4,
)
self.local_encoder = LocalEncoder(
ch=32,
out_ch=3,
ch_mult=self.ch_mult,
num_res_blocks=1,
attn_resolutions=[],
dropout=0.0,
resamp_with_conv=True,
in_channels=3,
resolution=512,
z_channels=4,
)
self.global_encoder = GlobalEncoder(n_class=64)
self.discriminator = NLayerDiscriminator(
input_nc=3, n_layers=3, use_actnorm=False, ndf=64
).apply(weights_init)
def forward(
self,
rays_o: Float[Tensor, "B H W 3"],
rays_d: Float[Tensor, "B H W 3"],
light_positions: Float[Tensor, "B 3"],
bg_color: Optional[Tensor] = None,
gt_rgb: Float[Tensor, "B H W 3"] = None,
multi_level_guidance: Bool = False,
**kwargs
) -> Dict[str, Float[Tensor, "..."]]:
B, H, W, _ = rays_o.shape
if gt_rgb is not None and multi_level_guidance:
generator_level = torch.randint(0, 3, (1,)).item()
interval_x = torch.randint(0, 8, (1,)).item()
interval_y = torch.randint(0, 8, (1,)).item()
int_rays_o = rays_o[:, interval_y::8, interval_x::8]
int_rays_d = rays_d[:, interval_y::8, interval_x::8]
out = self.base_renderer(
int_rays_o, int_rays_d, light_positions, bg_color, **kwargs
)
comp_int_rgb = out["comp_rgb"][..., :3]
comp_gt_rgb = gt_rgb[:, interval_y::8, interval_x::8]
else:
generator_level = 0
scale_ratio = 2 ** (len(self.ch_mult) - 1)
rays_o = torch.nn.functional.interpolate(
rays_o.permute(0, 3, 1, 2),
(H // scale_ratio, W // scale_ratio),
mode="bilinear",
).permute(0, 2, 3, 1)
rays_d = torch.nn.functional.interpolate(
rays_d.permute(0, 3, 1, 2),
(H // scale_ratio, W // scale_ratio),
mode="bilinear",
).permute(0, 2, 3, 1)
out = self.base_renderer(rays_o, rays_d, light_positions, bg_color, **kwargs)
comp_rgb = out["comp_rgb"][..., :3]
latent = out["comp_rgb"][..., 3:]
out["comp_lr_rgb"] = comp_rgb.clone()
posterior = DiagonalGaussianDistribution(latent.permute(0, 3, 1, 2))
if multi_level_guidance:
z_map = posterior.sample()
else:
z_map = posterior.mode()
lr_rgb = comp_rgb.permute(0, 3, 1, 2)
if generator_level == 0:
g_code_rgb = self.global_encoder(F.interpolate(lr_rgb, (224, 224)))
comp_gan_rgb = self.generator(torch.cat([lr_rgb, z_map], dim=1), g_code_rgb)
elif generator_level == 1:
g_code_rgb = self.global_encoder(
F.interpolate(gt_rgb.permute(0, 3, 1, 2), (224, 224))
)
comp_gan_rgb = self.generator(torch.cat([lr_rgb, z_map], dim=1), g_code_rgb)
elif generator_level == 2:
g_code_rgb = self.global_encoder(
F.interpolate(gt_rgb.permute(0, 3, 1, 2), (224, 224))
)
l_code_rgb = self.local_encoder(gt_rgb.permute(0, 3, 1, 2))
posterior = DiagonalGaussianDistribution(l_code_rgb)
z_map = posterior.sample()
comp_gan_rgb = self.generator(torch.cat([lr_rgb, z_map], dim=1), g_code_rgb)
comp_rgb = F.interpolate(comp_rgb.permute(0, 3, 1, 2), (H, W), mode="bilinear")
comp_gan_rgb = F.interpolate(comp_gan_rgb, (H, W), mode="bilinear")
out.update(
{
"posterior": posterior,
"comp_gan_rgb": comp_gan_rgb.permute(0, 2, 3, 1),
"comp_rgb": comp_rgb.permute(0, 2, 3, 1),
"generator_level": generator_level,
}
)
if gt_rgb is not None and multi_level_guidance:
out.update({"comp_int_rgb": comp_int_rgb, "comp_gt_rgb": comp_gt_rgb})
return out
def update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
) -> None:
self.base_renderer.update_step(epoch, global_step, on_load_weights)
def train(self, mode=True):
return self.base_renderer.train(mode)
def eval(self):
return self.base_renderer.eval()

View File

@@ -0,0 +1,462 @@
from dataclasses import dataclass, field
from functools import partial
import nerfacc
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.estimators import ImportanceEstimator
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.networks import create_network_with_input_encoding
from threestudio.models.renderers.base import VolumeRenderer
from threestudio.systems.utils import parse_optimizer, parse_scheduler_to_instance
from threestudio.utils.ops import chunk_batch, get_activation, validate_empty_rays
from threestudio.utils.typing import *
@threestudio.register("nerf-volume-renderer")
class NeRFVolumeRenderer(VolumeRenderer):
@dataclass
class Config(VolumeRenderer.Config):
num_samples_per_ray: int = 512
eval_chunk_size: int = 160000
randomized: bool = True
near_plane: float = 0.0
far_plane: float = 1e10
return_comp_normal: bool = False
return_normal_perturb: bool = False
# in ["occgrid", "proposal", "importance"]
estimator: str = "occgrid"
# for occgrid
grid_prune: bool = True
prune_alpha_threshold: bool = True
# for proposal
proposal_network_config: Optional[dict] = None
prop_optimizer_config: Optional[dict] = None
prop_scheduler_config: Optional[dict] = None
num_samples_per_ray_proposal: int = 64
# for importance
num_samples_per_ray_importance: int = 64
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
super().configure(geometry, material, background)
if self.cfg.estimator == "occgrid":
self.estimator = nerfacc.OccGridEstimator(
roi_aabb=self.bbox.view(-1), resolution=32, levels=1
)
if not self.cfg.grid_prune:
self.estimator.occs.fill_(True)
self.estimator.binaries.fill_(True)
self.render_step_size = (
1.732 * 2 * self.cfg.radius / self.cfg.num_samples_per_ray
)
self.randomized = self.cfg.randomized
elif self.cfg.estimator == "importance":
self.estimator = ImportanceEstimator()
elif self.cfg.estimator == "proposal":
self.prop_net = create_network_with_input_encoding(
**self.cfg.proposal_network_config
)
self.prop_optim = parse_optimizer(
self.cfg.prop_optimizer_config, self.prop_net
)
self.prop_scheduler = (
parse_scheduler_to_instance(
self.cfg.prop_scheduler_config, self.prop_optim
)
if self.cfg.prop_scheduler_config is not None
else None
)
self.estimator = nerfacc.PropNetEstimator(
self.prop_optim, self.prop_scheduler
)
def get_proposal_requires_grad_fn(
target: float = 5.0, num_steps: int = 1000
):
schedule = lambda s: min(s / num_steps, 1.0) * target
steps_since_last_grad = 0
def proposal_requires_grad_fn(step: int) -> bool:
nonlocal steps_since_last_grad
target_steps_since_last_grad = schedule(step)
requires_grad = steps_since_last_grad > target_steps_since_last_grad
if requires_grad:
steps_since_last_grad = 0
steps_since_last_grad += 1
return requires_grad
return proposal_requires_grad_fn
self.proposal_requires_grad_fn = get_proposal_requires_grad_fn()
self.randomized = self.cfg.randomized
else:
raise NotImplementedError(
"Unknown estimator, should be one of ['occgrid', 'proposal', 'importance']."
)
# for proposal
self.vars_in_forward = {}
def forward(
self,
rays_o: Float[Tensor, "B H W 3"],
rays_d: Float[Tensor, "B H W 3"],
light_positions: Float[Tensor, "B 3"],
bg_color: Optional[Tensor] = None,
**kwargs
) -> Dict[str, Float[Tensor, "..."]]:
batch_size, height, width = rays_o.shape[:3]
rays_o_flatten: Float[Tensor, "Nr 3"] = rays_o.reshape(-1, 3)
rays_d_flatten: Float[Tensor, "Nr 3"] = rays_d.reshape(-1, 3)
light_positions_flatten: Float[Tensor, "Nr 3"] = (
light_positions.reshape(-1, 1, 1, 3)
.expand(-1, height, width, -1)
.reshape(-1, 3)
)
n_rays = rays_o_flatten.shape[0]
if self.cfg.estimator == "occgrid":
if not self.cfg.grid_prune:
with torch.no_grad():
ray_indices, t_starts_, t_ends_ = self.estimator.sampling(
rays_o_flatten,
rays_d_flatten,
sigma_fn=None,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
render_step_size=self.render_step_size,
alpha_thre=0.0,
stratified=self.randomized,
cone_angle=0.0,
early_stop_eps=0,
)
else:
def sigma_fn(t_starts, t_ends, ray_indices):
t_starts, t_ends = t_starts[..., None], t_ends[..., None]
t_origins = rays_o_flatten[ray_indices]
t_positions = (t_starts + t_ends) / 2.0
t_dirs = rays_d_flatten[ray_indices]
positions = t_origins + t_dirs * t_positions
if self.training:
sigma = self.geometry.forward_density(positions)[..., 0]
else:
sigma = chunk_batch(
self.geometry.forward_density,
self.cfg.eval_chunk_size,
positions,
)[..., 0]
return sigma
with torch.no_grad():
ray_indices, t_starts_, t_ends_ = self.estimator.sampling(
rays_o_flatten,
rays_d_flatten,
sigma_fn=sigma_fn if self.cfg.prune_alpha_threshold else None,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
render_step_size=self.render_step_size,
alpha_thre=0.01 if self.cfg.prune_alpha_threshold else 0.0,
stratified=self.randomized,
cone_angle=0.0,
)
elif self.cfg.estimator == "proposal":
def prop_sigma_fn(
t_starts: Float[Tensor, "Nr Ns"],
t_ends: Float[Tensor, "Nr Ns"],
proposal_network,
):
t_origins: Float[Tensor, "Nr 1 3"] = rays_o_flatten.unsqueeze(-2)
t_dirs: Float[Tensor, "Nr 1 3"] = rays_d_flatten.unsqueeze(-2)
positions: Float[Tensor, "Nr Ns 3"] = (
t_origins + t_dirs * (t_starts + t_ends)[..., None] / 2.0
)
aabb_min, aabb_max = self.bbox[0], self.bbox[1]
positions = (positions - aabb_min) / (aabb_max - aabb_min)
selector = ((positions > 0.0) & (positions < 1.0)).all(dim=-1)
density_before_activation = (
proposal_network(positions.view(-1, 3))
.view(*positions.shape[:-1], 1)
.to(positions)
)
density: Float[Tensor, "Nr Ns 1"] = (
get_activation("shifted_trunc_exp")(density_before_activation)
* selector[..., None]
)
return density.squeeze(-1)
t_starts_, t_ends_ = self.estimator.sampling(
prop_sigma_fns=[partial(prop_sigma_fn, proposal_network=self.prop_net)],
prop_samples=[self.cfg.num_samples_per_ray_proposal],
num_samples=self.cfg.num_samples_per_ray,
n_rays=n_rays,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
sampling_type="uniform",
stratified=self.randomized,
requires_grad=self.vars_in_forward["requires_grad"],
)
ray_indices = (
torch.arange(n_rays, device=rays_o_flatten.device)
.unsqueeze(-1)
.expand(-1, t_starts_.shape[1])
)
ray_indices = ray_indices.flatten()
t_starts_ = t_starts_.flatten()
t_ends_ = t_ends_.flatten()
elif self.cfg.estimator == "importance":
def prop_sigma_fn(
t_starts: Float[Tensor, "Nr Ns"],
t_ends: Float[Tensor, "Nr Ns"],
proposal_network,
):
t_origins: Float[Tensor, "Nr 1 3"] = rays_o_flatten.unsqueeze(-2)
t_dirs: Float[Tensor, "Nr 1 3"] = rays_d_flatten.unsqueeze(-2)
positions: Float[Tensor, "Nr Ns 3"] = (
t_origins + t_dirs * (t_starts + t_ends)[..., None] / 2.0
)
with torch.no_grad():
geo_out = chunk_batch(
proposal_network,
self.cfg.eval_chunk_size,
positions.reshape(-1, 3),
output_normal=False,
)
density = geo_out["density"]
return density.reshape(positions.shape[:2])
t_starts_, t_ends_ = self.estimator.sampling(
prop_sigma_fns=[partial(prop_sigma_fn, proposal_network=self.geometry)],
prop_samples=[self.cfg.num_samples_per_ray_importance],
num_samples=self.cfg.num_samples_per_ray,
n_rays=n_rays,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
sampling_type="uniform",
stratified=self.randomized,
)
ray_indices = (
torch.arange(n_rays, device=rays_o_flatten.device)
.unsqueeze(-1)
.expand(-1, t_starts_.shape[1])
)
ray_indices = ray_indices.flatten()
t_starts_ = t_starts_.flatten()
t_ends_ = t_ends_.flatten()
else:
raise NotImplementedError
ray_indices, t_starts_, t_ends_ = validate_empty_rays(
ray_indices, t_starts_, t_ends_
)
ray_indices = ray_indices.long()
t_starts, t_ends = t_starts_[..., None], t_ends_[..., None]
t_origins = rays_o_flatten[ray_indices]
t_dirs = rays_d_flatten[ray_indices]
t_light_positions = light_positions_flatten[ray_indices]
t_positions = (t_starts + t_ends) / 2.0
positions = t_origins + t_dirs * t_positions
t_intervals = t_ends - t_starts
if self.training:
geo_out = self.geometry(
positions, output_normal=self.material.requires_normal
)
rgb_fg_all = self.material(
viewdirs=t_dirs,
positions=positions,
light_positions=t_light_positions,
**geo_out,
**kwargs
)
comp_rgb_bg = self.background(dirs=rays_d)
else:
geo_out = chunk_batch(
self.geometry,
self.cfg.eval_chunk_size,
positions,
output_normal=self.material.requires_normal,
)
rgb_fg_all = chunk_batch(
self.material,
self.cfg.eval_chunk_size,
viewdirs=t_dirs,
positions=positions,
light_positions=t_light_positions,
**geo_out
)
comp_rgb_bg = chunk_batch(
self.background, self.cfg.eval_chunk_size, dirs=rays_d
)
weights: Float[Tensor, "Nr 1"]
weights_, trans_, _ = nerfacc.render_weight_from_density(
t_starts[..., 0],
t_ends[..., 0],
geo_out["density"][..., 0],
ray_indices=ray_indices,
n_rays=n_rays,
)
if self.training and self.cfg.estimator == "proposal":
self.vars_in_forward["trans"] = trans_.reshape(n_rays, -1)
weights = weights_[..., None]
opacity: Float[Tensor, "Nr 1"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=None, ray_indices=ray_indices, n_rays=n_rays
)
depth: Float[Tensor, "Nr 1"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=t_positions, ray_indices=ray_indices, n_rays=n_rays
)
comp_rgb_fg: Float[Tensor, "Nr Nc"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=rgb_fg_all, ray_indices=ray_indices, n_rays=n_rays
)
# populate depth and opacity to each point
t_depth = depth[ray_indices]
z_variance = nerfacc.accumulate_along_rays(
weights[..., 0],
values=(t_positions - t_depth) ** 2,
ray_indices=ray_indices,
n_rays=n_rays,
)
if bg_color is None:
bg_color = comp_rgb_bg
else:
if bg_color.shape[:-1] == (batch_size,):
# e.g. constant random color used for Zero123
# [bs,3] -> [bs, 1, 1, 3]):
bg_color = bg_color.unsqueeze(1).unsqueeze(1)
# -> [bs, height, width, 3]):
bg_color = bg_color.expand(-1, height, width, -1)
if bg_color.shape[:-1] == (batch_size, height, width):
bg_color = bg_color.reshape(batch_size * height * width, -1)
comp_rgb = comp_rgb_fg + bg_color * (1.0 - opacity)
out = {
"comp_rgb": comp_rgb.view(batch_size, height, width, -1),
"comp_rgb_fg": comp_rgb_fg.view(batch_size, height, width, -1),
"comp_rgb_bg": comp_rgb_bg.view(batch_size, height, width, -1),
"opacity": opacity.view(batch_size, height, width, 1),
"depth": depth.view(batch_size, height, width, 1),
"z_variance": z_variance.view(batch_size, height, width, 1),
}
if self.training:
out.update(
{
"weights": weights,
"t_points": t_positions,
"t_intervals": t_intervals,
"t_dirs": t_dirs,
"ray_indices": ray_indices,
"points": positions,
**geo_out,
}
)
if "normal" in geo_out:
if self.cfg.return_comp_normal:
comp_normal: Float[Tensor, "Nr 3"] = nerfacc.accumulate_along_rays(
weights[..., 0],
values=geo_out["normal"],
ray_indices=ray_indices,
n_rays=n_rays,
)
comp_normal = F.normalize(comp_normal, dim=-1)
comp_normal = (
(comp_normal + 1.0) / 2.0 * opacity
) # for visualization
out.update(
{
"comp_normal": comp_normal.view(
batch_size, height, width, 3
),
}
)
if self.cfg.return_normal_perturb:
normal_perturb = self.geometry(
positions + torch.randn_like(positions) * 1e-2,
output_normal=self.material.requires_normal,
)["normal"]
out.update({"normal_perturb": normal_perturb})
else:
if "normal" in geo_out:
comp_normal = nerfacc.accumulate_along_rays(
weights[..., 0],
values=geo_out["normal"],
ray_indices=ray_indices,
n_rays=n_rays,
)
comp_normal = F.normalize(comp_normal, dim=-1)
comp_normal = (comp_normal + 1.0) / 2.0 * opacity # for visualization
out.update(
{
"comp_normal": comp_normal.view(batch_size, height, width, 3),
}
)
return out
def update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
) -> None:
if self.cfg.estimator == "occgrid":
if self.cfg.grid_prune:
def occ_eval_fn(x):
density = self.geometry.forward_density(x)
# approximate for 1 - torch.exp(-density * self.render_step_size) based on taylor series
return density * self.render_step_size
if self.training and not on_load_weights:
self.estimator.update_every_n_steps(
step=global_step, occ_eval_fn=occ_eval_fn
)
elif self.cfg.estimator == "proposal":
if self.training:
requires_grad = self.proposal_requires_grad_fn(global_step)
self.vars_in_forward["requires_grad"] = requires_grad
else:
self.vars_in_forward["requires_grad"] = False
def update_step_end(self, epoch: int, global_step: int) -> None:
if self.cfg.estimator == "proposal" and self.training:
self.estimator.update_every_n_steps(
self.vars_in_forward["trans"],
self.vars_in_forward["requires_grad"],
loss_scaler=1.0,
)
def train(self, mode=True):
self.randomized = mode and self.cfg.randomized
if self.cfg.estimator == "proposal":
self.prop_net.train()
return super().train(mode=mode)
def eval(self):
self.randomized = False
if self.cfg.estimator == "proposal":
self.prop_net.eval()
return super().eval()

View File

@@ -0,0 +1,390 @@
from dataclasses import dataclass
from functools import partial
import nerfacc
import torch
import torch.nn as nn
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.estimators import ImportanceEstimator
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.renderers.base import VolumeRenderer
from threestudio.utils.ops import chunk_batch, validate_empty_rays
from threestudio.utils.typing import *
def volsdf_density(sdf, inv_std):
inv_std = inv_std.clamp(0.0, 80.0)
beta = 1 / inv_std
alpha = inv_std
return alpha * (0.5 + 0.5 * sdf.sign() * torch.expm1(-sdf.abs() / beta))
class LearnedVariance(nn.Module):
def __init__(self, init_val):
super(LearnedVariance, self).__init__()
self.register_parameter("_inv_std", nn.Parameter(torch.tensor(init_val)))
@property
def inv_std(self):
val = torch.exp(self._inv_std * 10.0)
return val
def forward(self, x):
return torch.ones_like(x) * self.inv_std.clamp(1.0e-6, 1.0e6)
@threestudio.register("neus-volume-renderer")
class NeuSVolumeRenderer(VolumeRenderer):
@dataclass
class Config(VolumeRenderer.Config):
num_samples_per_ray: int = 512
randomized: bool = True
eval_chunk_size: int = 160000
learned_variance_init: float = 0.3
cos_anneal_end_steps: int = 0
use_volsdf: bool = False
near_plane: float = 0.0
far_plane: float = 1e10
# in ['occgrid', 'importance']
estimator: str = "occgrid"
# for occgrid
grid_prune: bool = True
prune_alpha_threshold: bool = True
# for importance
num_samples_per_ray_importance: int = 64
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
super().configure(geometry, material, background)
self.variance = LearnedVariance(self.cfg.learned_variance_init)
if self.cfg.estimator == "occgrid":
self.estimator = nerfacc.OccGridEstimator(
roi_aabb=self.bbox.view(-1), resolution=32, levels=1
)
if not self.cfg.grid_prune:
self.estimator.occs.fill_(True)
self.estimator.binaries.fill_(True)
self.render_step_size = (
1.732 * 2 * self.cfg.radius / self.cfg.num_samples_per_ray
)
self.randomized = self.cfg.randomized
elif self.cfg.estimator == "importance":
self.estimator = ImportanceEstimator()
else:
raise NotImplementedError(
"unknown estimator, should be in ['occgrid', 'importance']"
)
self.cos_anneal_ratio = 1.0
def get_alpha(self, sdf, normal, dirs, dists):
inv_std = self.variance(sdf)
if self.cfg.use_volsdf:
alpha = torch.abs(dists.detach()) * volsdf_density(sdf, inv_std)
else:
true_cos = (dirs * normal).sum(-1, keepdim=True)
# "cos_anneal_ratio" grows from 0 to 1 in the beginning training iterations. The anneal strategy below makes
# the cos value "not dead" at the beginning training iterations, for better convergence.
iter_cos = -(
F.relu(-true_cos * 0.5 + 0.5) * (1.0 - self.cos_anneal_ratio)
+ F.relu(-true_cos) * self.cos_anneal_ratio
) # always non-positive
# Estimate signed distances at section points
estimated_next_sdf = sdf + iter_cos * dists * 0.5
estimated_prev_sdf = sdf - iter_cos * dists * 0.5
prev_cdf = torch.sigmoid(estimated_prev_sdf * inv_std)
next_cdf = torch.sigmoid(estimated_next_sdf * inv_std)
p = prev_cdf - next_cdf
c = prev_cdf
alpha = ((p + 1e-5) / (c + 1e-5)).clip(0.0, 1.0)
return alpha
def forward(
self,
rays_o: Float[Tensor, "B H W 3"],
rays_d: Float[Tensor, "B H W 3"],
light_positions: Float[Tensor, "B 3"],
bg_color: Optional[Tensor] = None,
**kwargs
) -> Dict[str, Float[Tensor, "..."]]:
batch_size, height, width = rays_o.shape[:3]
rays_o_flatten: Float[Tensor, "Nr 3"] = rays_o.reshape(-1, 3)
rays_d_flatten: Float[Tensor, "Nr 3"] = rays_d.reshape(-1, 3)
light_positions_flatten: Float[Tensor, "Nr 3"] = (
light_positions.reshape(-1, 1, 1, 3)
.expand(-1, height, width, -1)
.reshape(-1, 3)
)
n_rays = rays_o_flatten.shape[0]
if self.cfg.estimator == "occgrid":
def alpha_fn(t_starts, t_ends, ray_indices):
t_starts, t_ends = t_starts[..., None], t_ends[..., None]
t_origins = rays_o_flatten[ray_indices]
t_positions = (t_starts + t_ends) / 2.0
t_dirs = rays_d_flatten[ray_indices]
positions = t_origins + t_dirs * t_positions
if self.training:
sdf = self.geometry.forward_sdf(positions)[..., 0]
else:
sdf = chunk_batch(
self.geometry.forward_sdf,
self.cfg.eval_chunk_size,
positions,
)[..., 0]
inv_std = self.variance(sdf)
if self.cfg.use_volsdf:
alpha = self.render_step_size * volsdf_density(sdf, inv_std)
else:
estimated_next_sdf = sdf - self.render_step_size * 0.5
estimated_prev_sdf = sdf + self.render_step_size * 0.5
prev_cdf = torch.sigmoid(estimated_prev_sdf * inv_std)
next_cdf = torch.sigmoid(estimated_next_sdf * inv_std)
p = prev_cdf - next_cdf
c = prev_cdf
alpha = ((p + 1e-5) / (c + 1e-5)).clip(0.0, 1.0)
return alpha
if not self.cfg.grid_prune:
with torch.no_grad():
ray_indices, t_starts_, t_ends_ = self.estimator.sampling(
rays_o_flatten,
rays_d_flatten,
alpha_fn=None,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
render_step_size=self.render_step_size,
alpha_thre=0.0,
stratified=self.randomized,
cone_angle=0.0,
early_stop_eps=0,
)
else:
with torch.no_grad():
ray_indices, t_starts_, t_ends_ = self.estimator.sampling(
rays_o_flatten,
rays_d_flatten,
alpha_fn=alpha_fn if self.cfg.prune_alpha_threshold else None,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
render_step_size=self.render_step_size,
alpha_thre=0.01 if self.cfg.prune_alpha_threshold else 0.0,
stratified=self.randomized,
cone_angle=0.0,
)
elif self.cfg.estimator == "importance":
def prop_sigma_fn(
t_starts: Float[Tensor, "Nr Ns"],
t_ends: Float[Tensor, "Nr Ns"],
proposal_network,
):
if self.cfg.use_volsdf:
t_origins: Float[Tensor, "Nr 1 3"] = rays_o_flatten.unsqueeze(-2)
t_dirs: Float[Tensor, "Nr 1 3"] = rays_d_flatten.unsqueeze(-2)
positions: Float[Tensor, "Nr Ns 3"] = (
t_origins + t_dirs * (t_starts + t_ends)[..., None] / 2.0
)
with torch.no_grad():
geo_out = chunk_batch(
proposal_network,
self.cfg.eval_chunk_size,
positions.reshape(-1, 3),
output_normal=False,
)
inv_std = self.variance(geo_out["sdf"])
density = volsdf_density(geo_out["sdf"], inv_std)
return density.reshape(positions.shape[:2])
else:
raise ValueError(
"Currently only VolSDF supports importance sampling."
)
t_starts_, t_ends_ = self.estimator.sampling(
prop_sigma_fns=[partial(prop_sigma_fn, proposal_network=self.geometry)],
prop_samples=[self.cfg.num_samples_per_ray_importance],
num_samples=self.cfg.num_samples_per_ray,
n_rays=n_rays,
near_plane=self.cfg.near_plane,
far_plane=self.cfg.far_plane,
sampling_type="uniform",
stratified=self.randomized,
)
ray_indices = (
torch.arange(n_rays, device=rays_o_flatten.device)
.unsqueeze(-1)
.expand(-1, t_starts_.shape[1])
)
ray_indices = ray_indices.flatten()
t_starts_ = t_starts_.flatten()
t_ends_ = t_ends_.flatten()
else:
raise NotImplementedError
ray_indices, t_starts_, t_ends_ = validate_empty_rays(
ray_indices, t_starts_, t_ends_
)
ray_indices = ray_indices.long()
t_starts, t_ends = t_starts_[..., None], t_ends_[..., None]
t_origins = rays_o_flatten[ray_indices]
t_dirs = rays_d_flatten[ray_indices]
t_light_positions = light_positions_flatten[ray_indices]
t_positions = (t_starts + t_ends) / 2.0
positions = t_origins + t_dirs * t_positions
t_intervals = t_ends - t_starts
if self.training:
geo_out = self.geometry(positions, output_normal=True)
rgb_fg_all = self.material(
viewdirs=t_dirs,
positions=positions,
light_positions=t_light_positions,
**geo_out,
**kwargs
)
comp_rgb_bg = self.background(dirs=rays_d)
else:
geo_out = chunk_batch(
self.geometry,
self.cfg.eval_chunk_size,
positions,
output_normal=True,
)
rgb_fg_all = chunk_batch(
self.material,
self.cfg.eval_chunk_size,
viewdirs=t_dirs,
positions=positions,
light_positions=t_light_positions,
**geo_out
)
comp_rgb_bg = chunk_batch(
self.background, self.cfg.eval_chunk_size, dirs=rays_d
)
# grad or normal?
alpha: Float[Tensor, "Nr 1"] = self.get_alpha(
geo_out["sdf"], geo_out["normal"], t_dirs, t_intervals
)
weights: Float[Tensor, "Nr 1"]
weights_, _ = nerfacc.render_weight_from_alpha(
alpha[..., 0],
ray_indices=ray_indices,
n_rays=n_rays,
)
weights = weights_[..., None]
opacity: Float[Tensor, "Nr 1"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=None, ray_indices=ray_indices, n_rays=n_rays
)
depth: Float[Tensor, "Nr 1"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=t_positions, ray_indices=ray_indices, n_rays=n_rays
)
comp_rgb_fg: Float[Tensor, "Nr Nc"] = nerfacc.accumulate_along_rays(
weights[..., 0], values=rgb_fg_all, ray_indices=ray_indices, n_rays=n_rays
)
if bg_color is None:
bg_color = comp_rgb_bg
if bg_color.shape[:-1] == (batch_size, height, width):
bg_color = bg_color.reshape(batch_size * height * width, -1)
comp_rgb = comp_rgb_fg + bg_color * (1.0 - opacity)
out = {
"comp_rgb": comp_rgb.view(batch_size, height, width, -1),
"comp_rgb_fg": comp_rgb_fg.view(batch_size, height, width, -1),
"comp_rgb_bg": comp_rgb_bg.view(batch_size, height, width, -1),
"opacity": opacity.view(batch_size, height, width, 1),
"depth": depth.view(batch_size, height, width, 1),
}
if self.training:
out.update(
{
"weights": weights,
"t_points": t_positions,
"t_intervals": t_intervals,
"t_dirs": t_dirs,
"ray_indices": ray_indices,
"points": positions,
**geo_out,
}
)
else:
if "normal" in geo_out:
comp_normal: Float[Tensor, "Nr 3"] = nerfacc.accumulate_along_rays(
weights[..., 0],
values=geo_out["normal"],
ray_indices=ray_indices,
n_rays=n_rays,
)
comp_normal = F.normalize(comp_normal, dim=-1)
comp_normal = (comp_normal + 1.0) / 2.0 * opacity # for visualization
out.update(
{
"comp_normal": comp_normal.view(batch_size, height, width, 3),
}
)
out.update({"inv_std": self.variance.inv_std})
return out
def update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
) -> None:
self.cos_anneal_ratio = (
1.0
if self.cfg.cos_anneal_end_steps == 0
else min(1.0, global_step / self.cfg.cos_anneal_end_steps)
)
if self.cfg.estimator == "occgrid":
if self.cfg.grid_prune:
def occ_eval_fn(x):
sdf = self.geometry.forward_sdf(x)
inv_std = self.variance(sdf)
if self.cfg.use_volsdf:
alpha = self.render_step_size * volsdf_density(sdf, inv_std)
else:
estimated_next_sdf = sdf - self.render_step_size * 0.5
estimated_prev_sdf = sdf + self.render_step_size * 0.5
prev_cdf = torch.sigmoid(estimated_prev_sdf * inv_std)
next_cdf = torch.sigmoid(estimated_next_sdf * inv_std)
p = prev_cdf - next_cdf
c = prev_cdf
alpha = ((p + 1e-5) / (c + 1e-5)).clip(0.0, 1.0)
return alpha
if self.training and not on_load_weights:
self.estimator.update_every_n_steps(
step=global_step, occ_eval_fn=occ_eval_fn
)
def train(self, mode=True):
self.randomized = mode and self.cfg.randomized
return super().train(mode=mode)
def eval(self):
self.randomized = False
return super().eval()

View File

@@ -0,0 +1,188 @@
from dataclasses import dataclass
import nerfacc
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.renderers.base import Rasterizer, VolumeRenderer
from threestudio.utils.misc import get_device
from threestudio.utils.rasterize import NVDiffRasterizerContext
from threestudio.utils.typing import *
@threestudio.register("nvdiff-rasterizer")
class NVDiffRasterizer(Rasterizer):
@dataclass
class Config(VolumeRenderer.Config):
context_type: str = "gl"
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
super().configure(geometry, material, background)
self.ctx = NVDiffRasterizerContext(self.cfg.context_type, get_device())
def forward(
self,
mvp_mtx: Float[Tensor, "B 4 4"],
camera_positions: Float[Tensor, "B 3"],
light_positions: Float[Tensor, "B 3"],
height: int,
width: int,
render_rgb: bool = True,
render_mask: bool = False,
**kwargs
) -> Dict[str, Any]:
batch_size = mvp_mtx.shape[0]
mesh = self.geometry.isosurface()
v_pos_clip: Float[Tensor, "B Nv 4"] = self.ctx.vertex_transform(
mesh.v_pos, mvp_mtx
)
rast, _ = self.ctx.rasterize(v_pos_clip, mesh.t_pos_idx, (height, width))
mask = rast[..., 3:] > 0
mask_aa = self.ctx.antialias(mask.float(), rast, v_pos_clip, mesh.t_pos_idx)
out = {"opacity": mask_aa, "mesh": mesh}
if render_mask:
# get front-view visibility mask
with torch.no_grad():
mvp_mtx_ref = kwargs["mvp_mtx_ref"] # FIXME
v_pos_clip_front: Float[Tensor, "B Nv 4"] = self.ctx.vertex_transform(
mesh.v_pos, mvp_mtx_ref
)
rast_front, _ = self.ctx.rasterize(v_pos_clip_front, mesh.t_pos_idx, (height, width))
mask_front = rast_front[..., 3:]
mask_front = mask_front[mask_front > 0] - 1.
faces_vis = mesh.t_pos_idx[mask_front.long()]
mesh._v_rgb = torch.zeros(mesh.v_pos.shape[0], 1).to(mesh.v_pos)
mesh._v_rgb[faces_vis[:,0]] = 1.
mesh._v_rgb[faces_vis[:,1]] = 1.
mesh._v_rgb[faces_vis[:,2]] = 1.
mask_vis, _ = self.ctx.interpolate_one(mesh._v_rgb, rast, mesh.t_pos_idx)
mask_vis = mask_vis > 0.
# from torchvision.utils import save_image
# save_image(mask_vis.permute(0,3,1,2).float(), "debug.png")
out.update({"mask": 1.0 - mask_vis.float()})
# FIXME: paste texture back to mesh
# import cv2
# import imageio
# import numpy as np
# gt_rgb = imageio.imread("load/images/tiger_nurse_rgba.png")/255.
# gt_rgb = cv2.resize(gt_rgb[:,:,:3],(512, 512))
# gt_rgb = torch.Tensor(gt_rgb[None,...]).permute(0,3,1,2).to(v_pos_clip_front)
# # align to up-z and front-x
# dir2vec = {
# "+x": np.array([1, 0, 0]),
# "+y": np.array([0, 1, 0]),
# "+z": np.array([0, 0, 1]),
# "-x": np.array([-1, 0, 0]),
# "-y": np.array([0, -1, 0]),
# "-z": np.array([0, 0, -1]),
# }
# z_, x_ = (
# dir2vec["-y"],
# dir2vec["-z"],
# )
# y_ = np.cross(z_, x_)
# std2mesh = np.stack([x_, y_, z_], axis=0).T
# v_pos_ = (torch.mm(torch.tensor(std2mesh).to(mesh.v_pos), mesh.v_pos.T).T) * 2
# print(v_pos_.min(), v_pos_.max())
# mesh._v_rgb=F.grid_sample(gt_rgb, v_pos_[None, None][..., :2], mode="nearest").permute(3,1,0,2).squeeze(-1).squeeze(-1).contiguous()
# rgb_vis, _ = self.ctx.interpolate_one(mesh._v_rgb, rast, mesh.t_pos_idx)
# rgb_vis_aa = self.ctx.antialias(
# rgb_vis, rast, v_pos_clip, mesh.t_pos_idx
# )
# from torchvision.utils import save_image
# save_image(rgb_vis_aa.permute(0,3,1,2), "debug.png")
gb_normal, _ = self.ctx.interpolate_one(mesh.v_nrm, rast, mesh.t_pos_idx)
gb_normal = F.normalize(gb_normal, dim=-1)
gb_normal_aa = torch.lerp(
torch.zeros_like(gb_normal), (gb_normal + 1.0) / 2.0, mask.float()
)
gb_normal_aa = self.ctx.antialias(
gb_normal_aa, rast, v_pos_clip, mesh.t_pos_idx
)
out.update({"comp_normal": gb_normal_aa}) # in [0, 1]
# Compute normal in view space.
# TODO: make is clear whether to compute this.
w2c = kwargs["c2w"][:, :3, :3].inverse()
gb_normal_viewspace = torch.einsum("bij,bhwj->bhwi", w2c, gb_normal)
gb_normal_viewspace = F.normalize(gb_normal_viewspace, dim=-1)
bg_normal = torch.zeros_like(gb_normal_viewspace)
bg_normal[..., 2] = 1
gb_normal_viewspace_aa = torch.lerp(
(bg_normal + 1.0) / 2.0,
(gb_normal_viewspace + 1.0) / 2.0,
mask.float(),
).contiguous()
gb_normal_viewspace_aa = self.ctx.antialias(
gb_normal_viewspace_aa, rast, v_pos_clip, mesh.t_pos_idx
)
out.update({"comp_normal_viewspace": gb_normal_viewspace_aa})
# TODO: make it clear whether to compute the normal, now we compute it in all cases
# consider using: require_normal_computation = render_normal or (render_rgb and material.requires_normal)
# or
# render_normal = render_normal or (render_rgb and material.requires_normal)
if render_rgb:
selector = mask[..., 0]
gb_pos, _ = self.ctx.interpolate_one(mesh.v_pos, rast, mesh.t_pos_idx)
gb_viewdirs = F.normalize(
gb_pos - camera_positions[:, None, None, :], dim=-1
)
gb_light_positions = light_positions[:, None, None, :].expand(
-1, height, width, -1
)
positions = gb_pos[selector]
geo_out = self.geometry(positions, output_normal=False)
extra_geo_info = {}
if self.material.requires_normal:
extra_geo_info["shading_normal"] = gb_normal[selector]
if self.material.requires_tangent:
gb_tangent, _ = self.ctx.interpolate_one(
mesh.v_tng, rast, mesh.t_pos_idx
)
gb_tangent = F.normalize(gb_tangent, dim=-1)
extra_geo_info["tangent"] = gb_tangent[selector]
rgb_fg = self.material(
viewdirs=gb_viewdirs[selector],
positions=positions,
light_positions=gb_light_positions[selector],
**extra_geo_info,
**geo_out
)
gb_rgb_fg = torch.zeros(batch_size, height, width, 3).to(rgb_fg)
gb_rgb_fg[selector] = rgb_fg
gb_rgb_bg = self.background(dirs=gb_viewdirs)
gb_rgb = torch.lerp(gb_rgb_bg, gb_rgb_fg, mask.float())
gb_rgb_aa = self.ctx.antialias(gb_rgb, rast, v_pos_clip, mesh.t_pos_idx)
out.update({"comp_rgb": gb_rgb_aa, "comp_rgb_bg": gb_rgb_bg})
return out

View File

@@ -0,0 +1,106 @@
from dataclasses import dataclass
import torch
import torch.nn.functional as F
import threestudio
from threestudio.models.background.base import BaseBackground
from threestudio.models.geometry.base import BaseImplicitGeometry
from threestudio.models.materials.base import BaseMaterial
from threestudio.models.renderers.base import VolumeRenderer
from threestudio.utils.typing import *
@threestudio.register("patch-renderer")
class PatchRenderer(VolumeRenderer):
@dataclass
class Config(VolumeRenderer.Config):
patch_size: int = 128
base_renderer_type: str = ""
base_renderer: Optional[VolumeRenderer.Config] = None
global_detach: bool = False
global_downsample: int = 4
cfg: Config
def configure(
self,
geometry: BaseImplicitGeometry,
material: BaseMaterial,
background: BaseBackground,
) -> None:
self.base_renderer = threestudio.find(self.cfg.base_renderer_type)(
self.cfg.base_renderer,
geometry=geometry,
material=material,
background=background,
)
def forward(
self,
rays_o: Float[Tensor, "B H W 3"],
rays_d: Float[Tensor, "B H W 3"],
light_positions: Float[Tensor, "B 3"],
bg_color: Optional[Tensor] = None,
**kwargs
) -> Dict[str, Float[Tensor, "..."]]:
B, H, W, _ = rays_o.shape
if self.base_renderer.training:
downsample = self.cfg.global_downsample
global_rays_o = torch.nn.functional.interpolate(
rays_o.permute(0, 3, 1, 2),
(H // downsample, W // downsample),
mode="bilinear",
).permute(0, 2, 3, 1)
global_rays_d = torch.nn.functional.interpolate(
rays_d.permute(0, 3, 1, 2),
(H // downsample, W // downsample),
mode="bilinear",
).permute(0, 2, 3, 1)
out_global = self.base_renderer(
global_rays_o, global_rays_d, light_positions, bg_color, **kwargs
)
PS = self.cfg.patch_size
patch_x = torch.randint(0, W - PS, (1,)).item()
patch_y = torch.randint(0, H - PS, (1,)).item()
patch_rays_o = rays_o[:, patch_y : patch_y + PS, patch_x : patch_x + PS]
patch_rays_d = rays_d[:, patch_y : patch_y + PS, patch_x : patch_x + PS]
out = self.base_renderer(
patch_rays_o, patch_rays_d, light_positions, bg_color, **kwargs
)
valid_patch_key = []
for key in out:
if torch.is_tensor(out[key]):
if len(out[key].shape) == len(out["comp_rgb"].shape):
if out[key][..., 0].shape == out["comp_rgb"][..., 0].shape:
valid_patch_key.append(key)
for key in valid_patch_key:
out_global[key] = F.interpolate(
out_global[key].permute(0, 3, 1, 2), (H, W), mode="bilinear"
).permute(0, 2, 3, 1)
if self.cfg.global_detach:
out_global[key] = out_global[key].detach()
out_global[key][
:, patch_y : patch_y + PS, patch_x : patch_x + PS
] = out[key]
out = out_global
else:
out = self.base_renderer(
rays_o, rays_d, light_positions, bg_color, **kwargs
)
return out
def update_step(
self, epoch: int, global_step: int, on_load_weights: bool = False
) -> None:
self.base_renderer.update_step(epoch, global_step, on_load_weights)
def train(self, mode=True):
return self.base_renderer.train(mode)
def eval(self):
return self.base_renderer.eval()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
import argparse
import os
from subprocess import run, CalledProcessError
import cv2
import glob
import numpy as np
import pytorch_lightning as pl
import torch
from tqdm import tqdm
from torchvision.utils import save_image
from threestudio.scripts.generate_mv_datasets import generate_mv_dataset
from threestudio.utils.config import load_config
import threestudio
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="path to config file")
parser.add_argument("--action", default="both", help="action to perform", choices=["gen_data", "dreambooth", "both""])
args, extras = parser.parse_known_args()
return args, extras
def main(args, extras):
cfg = load_config(args.config, cli_args=extras, n_gpus=1)
if args.action == "gen_data" or args.action == "both":
# Generate multi-view dataset
generate_mv_dataset(cfg)
if args.action == "dreambooth" or args.action == "both":
# Run DreamBooth.
command = f'accelerate launch threestudio/scripts/train_dreambooth.py \
--pretrained_model_name_or_path="{cfg.custom_import.dreambooth.model_name}" \
--instance_data_dir="{cfg.custom_import.dreambooth.instance_dir}" \
--output_dir="{cfg.custom_import.dreambooth.output_dir}"\
--instance_prompt="{cfg.custom_import.dreambooth.prompt_dreambooth}" \
--resolution=512 \
--train_batch_size=2 \
--gradient_accumulation_steps=1 \
--learning_rate=1e-6 \
--lr_scheduler="constant" \
--lr_warmup_steps=0 \
--max_train_steps=1000'
os.system(command)
if __name__ == "__main__":
args, extras = parse_args()
main(args, extras)

View File

@@ -0,0 +1,92 @@
from diffusers import DiffusionPipeline
from diffusers.utils import pt_to_pil
import torch
import os
import glob
import json
import argparse
import numpy as np
from tqdm import tqdm
SAVE_FOLDER = "./load/images_dreamfusion"
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--rank", default=0, type=int, help="# of GPU")
parser.add_argument("--prompt",required=True, type=str)
args = parser.parse_args()
# stage 1
stage_1 = DiffusionPipeline.from_pretrained(
"DeepFloyd/IF-I-XL-v1.0",
variant="fp16",
torch_dtype=torch.float16,
local_files_only=True
)
stage_1.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_1.enable_model_cpu_offload()
# stage 2
stage_2 = DiffusionPipeline.from_pretrained(
"DeepFloyd/IF-II-L-v1.0",
text_encoder=None,
variant="fp16",
torch_dtype=torch.float16,
local_files_only=True
)
# stage_2.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_2.enable_model_cpu_offload()
# stage 3
# safety_modules = {"feature_extractor": stage_1.feature_extractor, "safety_checker": stage_1.safety_checker, "watermarker": stage_1.watermarker}
safety_modules = None
stage_3 = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-x4-upscaler",
torch_dtype=torch.float16,
local_files_only=True
)
stage_3.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_3.enable_model_cpu_offload()
# # load prompt library
# with open(os.path.join("load/prompt_library.json"), "r") as f:
# prompt_library = json.load(f)
# n_prompts = len(prompt_library["dreamfusion"])
# n_prompts_per_rank = int(np.ceil(n_prompts / 8))
# for prompt in tqdm(prompt_library["dreamfusion"][args.rank * n_prompts_per_rank : (args.rank + 1) * n_prompts_per_rank]):
prompt = args.prompt
print("Prompt:", prompt)
save_folder = os.path.join(SAVE_FOLDER, prompt)
os.makedirs(save_folder, exist_ok=True)
# if len(glob.glob(f"{save_folder}/*.png")) >= 30:
# continue
# enhance prompt
prompt = prompt + ", 8k uhd, dslr, soft lighting, high quality, film grain, Fujifilm XT3, hyperrealistic, intricate details, ultra-realistic, award-winning"
prompt_embeds, negative_embeds = stage_1.encode_prompt(prompt)
for _ in tqdm(range(30)):
seed = np.random.randint(low=0, high=10000000, size=1)[0]
generator = torch.manual_seed(seed)
### Stage 1
image = stage_1(prompt_embeds=prompt_embeds, negative_prompt_embeds=negative_embeds, generator=generator, output_type="pt").images
# pt_to_pil(image)[0].save("./if_stage_I.png")
### Stage 2
image = stage_2(
image=image, prompt_embeds=prompt_embeds, negative_prompt_embeds=negative_embeds, generator=generator, output_type="pt"
).images
# pt_to_pil(image)[0].save("./if_stage_II.png")
### Stage 3
image = stage_3(prompt=prompt, image=(image.float() * 0.5 + 0.5), generator=generator, noise_level=100).images
image[0].save(f"{save_folder}/img_{seed:08d}.png")

View File

@@ -0,0 +1,90 @@
from diffusers import DiffusionPipeline
from diffusers.utils import pt_to_pil
import torch
import os
import glob
import json
import argparse
import numpy as np
from tqdm import tqdm
SAVE_FOLDER = "./load/images_dreamfusion"
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--rank", default=0, type=int, help="# of GPU")
args = parser.parse_args()
# stage 1
stage_1 = DiffusionPipeline.from_pretrained(
"DeepFloyd/IF-I-XL-v1.0",
variant="fp16",
torch_dtype=torch.float16,
local_files_only=True
)
stage_1.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_1.enable_model_cpu_offload()
# stage 2
stage_2 = DiffusionPipeline.from_pretrained(
"DeepFloyd/IF-II-L-v1.0",
text_encoder=None,
variant="fp16",
torch_dtype=torch.float16,
local_files_only=True
)
# stage_2.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_2.enable_model_cpu_offload()
# stage 3
# safety_modules = {"feature_extractor": stage_1.feature_extractor, "safety_checker": stage_1.safety_checker, "watermarker": stage_1.watermarker}
safety_modules = None
stage_3 = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-x4-upscaler",
torch_dtype=torch.float16,
local_files_only=True
)
stage_3.enable_xformers_memory_efficient_attention() # remove line if torch.__version__ >= 2.0.0
stage_3.enable_model_cpu_offload()
# load prompt library
with open(os.path.join("load/prompt_library.json"), "r") as f:
prompt_library = json.load(f)
n_prompts = len(prompt_library["dreamfusion"])
n_prompts_per_rank = int(np.ceil(n_prompts / 8))
for prompt in tqdm(prompt_library["dreamfusion"][args.rank * n_prompts_per_rank : (args.rank + 1) * n_prompts_per_rank]):
print("Prompt:", prompt)
save_folder = os.path.join(SAVE_FOLDER, prompt)
os.makedirs(save_folder, exist_ok=True)
if len(glob.glob(f"{save_folder}/*.png")) >= 30:
continue
# enhance prompt
prompt = prompt + ", 8k uhd, dslr, soft lighting, high quality, film grain, Fujifilm XT3, hyperrealistic, intricate details, ultra-realistic, award-winning"
prompt_embeds, negative_embeds = stage_1.encode_prompt(prompt)
for _ in tqdm(range(30)):
seed = np.random.randint(low=0, high=10000000, size=1)[0]
generator = torch.manual_seed(seed)
### Stage 1
image = stage_1(prompt_embeds=prompt_embeds, negative_prompt_embeds=negative_embeds, generator=generator, output_type="pt").images
# pt_to_pil(image)[0].save("./if_stage_I.png")
### Stage 2
image = stage_2(
image=image, prompt_embeds=prompt_embeds, negative_prompt_embeds=negative_embeds, generator=generator, output_type="pt"
).images
# pt_to_pil(image)[0].save("./if_stage_II.png")
### Stage 3
image = stage_3(prompt=prompt, image=(image.float() * 0.5 + 0.5), generator=generator, noise_level=100).images
image[0].save(f"{save_folder}/img_{seed:08d}.png")

View File

@@ -0,0 +1,95 @@
import os
import cv2
import glob
import torch
import argparse
import numpy as np
from tqdm import tqdm
import pytorch_lightning as pl
from torchvision.utils import save_image
from subprocess import run, CalledProcessError
from threestudio.utils.config import load_config
import threestudio
# Constants
AZIMUTH_FACTOR = 360
IMAGE_SIZE = (512, 512)
def copy_file(source, destination):
try:
command = ['cp', source, destination]
result = run(command, capture_output=True, text=True)
result.check_returncode()
except CalledProcessError as e:
print(f'Error: {e.output}')
def prepare_images(cfg):
rgb_list = sorted(glob.glob(os.path.join(cfg.data.render_image_path, "*.png")))
rgb_list.sort(key=lambda file: int(os.path.splitext(os.path.basename(file))[0]))
n_rgbs = len(rgb_list)
n_samples = cfg.data.n_samples
os.makedirs(cfg.data.save_path, exist_ok=True)
copy_file(cfg.data.ref_image_path, f"{cfg.data.save_path}/ref_0.0.png")
sampled_indices = np.linspace(0, len(rgb_list)-1, n_samples, dtype=int)
rgb_samples = [rgb_list[index] for index in sampled_indices]
return rgb_samples
def process_images(rgb_samples, cfg, guidance, prompt_utils):
n_rgbs = 120
for rgb_name in tqdm(rgb_samples):
rgb_idx = int(os.path.basename(rgb_name).split(".")[0])
rgb = cv2.imread(rgb_name)[:, :, :3][:, :, ::-1].copy() / 255.0
H, W = rgb.shape[0:2]
rgb_image, mask_image = rgb[:, :H], rgb[:, -H:, :1]
rgb_image = cv2.resize(rgb_image, IMAGE_SIZE)
rgb_image = torch.FloatTensor(rgb_image).unsqueeze(0).to(guidance.device)
mask_image = cv2.resize(mask_image, IMAGE_SIZE).reshape(IMAGE_SIZE[0], IMAGE_SIZE[1], 1)
mask_image = torch.FloatTensor(mask_image).unsqueeze(0).to(guidance.device)
temp = torch.zeros(1).to(guidance.device)
azimuth = torch.tensor([rgb_idx/n_rgbs * AZIMUTH_FACTOR]).to(guidance.device)
camera_distance = torch.tensor([cfg.data.default_camera_distance]).to(guidance.device)
if cfg.data.view_dependent_noise:
guidance.min_step_percent = 0. + (rgb_idx/n_rgbs) * (cfg.system.guidance.min_step_percent)
guidance.max_step_percent = 0. + (rgb_idx/n_rgbs) * (cfg.system.guidance.max_step_percent)
denoised_image = process_guidance(cfg, guidance, prompt_utils, rgb_image, azimuth, temp, camera_distance, mask_image)
save_image(denoised_image.permute(0,3,1,2), f"{cfg.data.save_path}/img_{azimuth[0]}.png", normalize=True, value_range=(0, 1))
copy_file(rgb_name.replace("png", "npy"), f"{cfg.data.save_path}/img_{azimuth[0]}.npy")
if rgb_idx == 0:
copy_file(rgb_name.replace("png", "npy"), f"{cfg.data.save_path}/ref_{azimuth[0]}.npy")
def process_guidance(cfg, guidance, prompt_utils, rgb_image, azimuth, temp, camera_distance, mask_image):
if cfg.data.azimuth_range[0] < azimuth < cfg.data.azimuth_range[1]:
return guidance.sample_img2img(
rgb_image, prompt_utils, temp,
azimuth, camera_distance, seed=0, mask=mask_image
)["edit_image"]
else:
return rgb_image
def generate_mv_dataset(cfg):
guidance = threestudio.find(cfg.system.guidance_type)(cfg.system.guidance)
prompt_processor = threestudio.find(cfg.system.prompt_processor_type)(cfg.system.prompt_processor)
prompt_utils = prompt_processor()
guidance.update_step(epoch=0, global_step=0)
rgb_samples = prepare_images(cfg)
print(rgb_samples)
process_images(rgb_samples, cfg, guidance, prompt_utils)

View File

@@ -0,0 +1,84 @@
import os
import argparse
from PIL import Image
import torch
from diffusers import DiffusionPipeline, EulerAncestralDiscreteScheduler, StableDiffusionUpscalePipeline
def load_model(superres):
mv_model = DiffusionPipeline.from_pretrained(
"sudo-ai/zero123plus-v1.1", custom_pipeline="sudo-ai/zero123plus-pipeline",
torch_dtype=torch.float16, cache_dir="load/checkpoints/huggingface/hub", local_files_only=True,
)
mv_model.scheduler = EulerAncestralDiscreteScheduler.from_config(
mv_model.scheduler.config, timestep_spacing='trailing', cache_dir="load/checkpoints/huggingface/hub", local_files_only=True,
)
if superres:
superres_model = StableDiffusionUpscalePipeline.from_pretrained(
"stabilityai/stable-diffusion-x4-upscaler", revision="fp16",
torch_dtype=torch.float16, cache_dir="load/checkpoints/huggingface/hub", local_files_only=True,
)
else:
superres_model = None
return mv_model, superres_model
def superres_4x(image, model, prompt):
low_res_img = image.resize((256, 256))
model.to('cuda:1')
result = model(prompt=prompt, image=low_res_img).images[0]
return result
def img_to_mv(image_path, model):
cond = Image.open(image_path)
model.to('cuda:1')
result = model(cond, num_inference_steps=75).images[0]
return result
def crop_save_image_to_2x3_grid(image, args, model):
save_path = args.save_path
width, height = image.size
grid_width = width//2
grid_height = height//3
images = []
for i in range(3):
for j in range(2):
left = j * grid_width
upper = i * grid_height
right = (j+1) * grid_width
lower = (i+1) * grid_height
cropped_image = image.crop((left, upper, right, lower))
if args.superres:
cropped_image = superres_4x(cropped_image, model, args.prompt)
images.append(cropped_image)
for idx, img in enumerate(images):
img.save(os.path.join(save_path, f'cropped_{idx}.jpg'))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--image_path', type=str, help="path to image (png, jpeg, etc.)")
parser.add_argument('--save_path', type=str, help="path to save output images")
parser.add_argument('--prompt', type=str, help="prompt to use for superres")
parser.add_argument('--superres', action='store_true', help="whether to use superres")
args = parser.parse_args()
print(args.superres)
os.makedirs(args.save_path, exist_ok=True)
os.system(f"cp '{args.image_path}' '{args.save_path}'")
mv_model, superres_model = load_model(args.superres)
images = img_to_mv(args.image_path, mv_model)
crop_save_image_to_2x3_grid(images, args, superres_model)
# Example usage:
# python threestudio/scripts/img_to_mv.py --image_path 'mushroom.png' --save_path '.cache/temp' --prompt 'a photo of mushroom' --superres

View File

@@ -0,0 +1,77 @@
# make_training_vid("outputs/zero123/64_teddy_rgba.png@20230627-195615", frames_per_vid=30, fps=20, max_iters=200)
import argparse
import glob
import os
import imageio
import numpy as np
from PIL import Image, ImageDraw
from tqdm import tqdm
def draw_text_in_image(img, texts):
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)
return np.asarray(img)
def make_training_vid(exp, frames_per_vid=1, fps=3, max_iters=None, max_vids=None):
# exp = "/admin/home-vikram/git/threestudio/outputs/zero123/64_teddy_rgba.png@20230627-195615"
files = glob.glob(os.path.join(exp, "save", "*.mp4"))
if os.path.join(exp, "save", "training_vid.mp4") in files:
files.remove(os.path.join(exp, "save", "training_vid.mp4"))
its = [int(os.path.basename(file).split("-")[0].split("it")[-1]) for file in files]
it_sort = np.argsort(its)
files = list(np.array(files)[it_sort])
its = list(np.array(its)[it_sort])
max_vids = max_iters // its[0] if max_iters is not None else max_vids
files, its = files[:max_vids], its[:max_vids]
frames, i = [], 0
for it, file in tqdm(zip(its, files), total=len(files)):
vid = imageio.mimread(file)
for _ in range(frames_per_vid):
frame = vid[i % len(vid)]
frame = draw_text_in_image(frame, [str(it)])
frames.append(frame)
i += 1
# Save
imageio.mimwrite(os.path.join(exp, "save", "training_vid.mp4"), frames, fps=fps)
def join(file1, file2, name):
# file1 = "/admin/home-vikram/git/threestudio/outputs/zero123/OLD_64_dragon2_rgba.png@20230629-023028/save/it200-val.mp4"
# file2 = "/admin/home-vikram/git/threestudio/outputs/zero123/64_dragon2_rgba.png@20230628-152734/save/it200-val.mp4"
vid1 = imageio.mimread(file1)
vid2 = imageio.mimread(file2)
frames = []
for f1, f2 in zip(vid1, vid2):
frames.append(
np.concatenate([f1[:, : f1.shape[0]], f2[:, : f2.shape[0]]], axis=1)
)
imageio.mimwrite(name, frames)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--exp", help="directory of experiment")
parser.add_argument(
"--frames_per_vid", type=int, default=1, help="# of frames from each val vid"
)
parser.add_argument("--fps", type=int, help="max # of iters to save")
parser.add_argument("--max_iters", type=int, help="max # of iters to save")
parser.add_argument(
"--max_vids",
type=int,
help="max # of val videos to save. Will be overridden by max_iters",
)
args = parser.parse_args()
make_training_vid(
args.exp, args.frames_per_vid, args.fps, args.max_iters, args.max_vids
)

View File

@@ -0,0 +1,459 @@
# * evaluate use laion/CLIP-ViT-H-14-laion2B-s32B-b79K
# best open source clip so far: laion/CLIP-ViT-bigG-14-laion2B-39B-b160k
# code adapted from NeuralLift-360
import torch
import torch.nn as nn
import os
import torchvision.transforms as T
import torchvision.transforms.functional as TF
import matplotlib.pyplot as plt
# import clip
from transformers import CLIPFeatureExtractor, CLIPModel, CLIPTokenizer, CLIPProcessor
from torchvision import transforms
import numpy as np
import torch.nn.functional as F
from tqdm import tqdm
import cv2
from PIL import Image
# import torchvision.transforms as transforms
import glob
from skimage.metrics import peak_signal_noise_ratio as compare_psnr
import lpips
from os.path import join as osp
import argparse
import pandas as pd
class CLIP(nn.Module):
def __init__(self,
device,
clip_name='laion/CLIP-ViT-bigG-14-laion2B-39B-b160k',
size=224): #'laion/CLIP-ViT-B-32-laion2B-s34B-b79K'):
super().__init__()
self.size = size
self.device = f"cuda:{device}"
clip_name = clip_name
self.feature_extractor = CLIPFeatureExtractor.from_pretrained(
clip_name)
self.clip_model = CLIPModel.from_pretrained(clip_name).to(self.device)
self.tokenizer = CLIPTokenizer.from_pretrained(
'openai/clip-vit-base-patch32')
self.normalize = transforms.Normalize(
mean=self.feature_extractor.image_mean,
std=self.feature_extractor.image_std)
self.resize = transforms.Resize(224)
self.to_tensor = transforms.ToTensor()
# image augmentation
self.aug = T.Compose([
T.Resize((224, 224)),
T.Normalize((0.48145466, 0.4578275, 0.40821073),
(0.26862954, 0.26130258, 0.27577711)),
])
# * recommend to use this function for evaluation
@torch.no_grad()
def score_gt(self, ref_img_path, novel_views):
# assert len(novel_views) == 100
clip_scores = []
for novel in novel_views:
clip_scores.append(self.score_from_path(ref_img_path, [novel]))
return np.mean(clip_scores)
# * recommend to use this function for evaluation
# def score_gt(self, ref_paths, novel_paths):
# clip_scores = []
# for img1_path, img2_path in zip(ref_paths, novel_paths):
# clip_scores.append(self.score_from_path(img1_path, img2_path))
# return np.mean(clip_scores)
def similarity(self, image1_features: torch.Tensor,
image2_features: torch.Tensor) -> float:
with torch.no_grad(), torch.cuda.amp.autocast():
y = image1_features.T.view(image1_features.T.shape[1],
image1_features.T.shape[0])
similarity = torch.matmul(y, image2_features.T)
# print(similarity)
return similarity[0][0].item()
def get_img_embeds(self, img):
if img.shape[0] == 4:
img = img[:3, :, :]
img = self.aug(img).to(self.device)
img = img.unsqueeze(0) # b,c,h,w
# plt.imshow(img.cpu().squeeze(0).permute(1, 2, 0).numpy())
# plt.show()
# print(img)
image_z = self.clip_model.get_image_features(img)
image_z = image_z / image_z.norm(dim=-1,
keepdim=True) # normalize features
return image_z
def score_from_feature(self, img1, img2):
img1_feature, img2_feature = self.get_img_embeds(
img1), self.get_img_embeds(img2)
# for debug
return self.similarity(img1_feature, img2_feature)
def read_img_list(self, img_list):
size = self.size
images = []
# white_background = np.ones((size, size, 3), dtype=np.uint8) * 255
for img_path in img_list:
img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
# print(img_path)
if img.shape[2] == 4: # Handle BGRA images
alpha = img[:, :, 3] # Extract alpha channel
img = cv2.cvtColor(img,cv2.COLOR_BGRA2RGB) # Convert BGRA to BGR
img[np.where(alpha == 0)] = [
255, 255, 255
] # Set transparent pixels to white
else: # Handle other image formats like JPG and PNG
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
# plt.imshow(img)
# plt.show()
images.append(img)
images = np.stack(images, axis=0)
# images[np.where(images == 0)] = 255 # Set black pixels to white
# images = np.where(images == 0, white_background, images) # Set transparent pixels to white
# images = images.astype(np.float32)
return images
def score_from_path(self, img1_path, img2_path):
img1, img2 = self.read_img_list(img1_path), self.read_img_list(img2_path)
img1 = np.squeeze(img1)
img2 = np.squeeze(img2)
# plt.imshow(img1)
# plt.show()
# plt.imshow(img2)
# plt.show()
img1, img2 = self.to_tensor(img1), self.to_tensor(img2)
# print("img1 to tensor ",img1)
return self.score_from_feature(img1, img2)
def numpy_to_torch(images):
images = images * 2.0 - 1.0
images = torch.from_numpy(images.transpose((0, 3, 1, 2))).float()
return images.cuda()
class LPIPSMeter:
def __init__(self,
net='alex',
device=None,
size=224): # or we can use 'alex', 'vgg' as network
self.size = size
self.net = net
self.results = []
self.device = device if device is not None else torch.device(
'cuda' if torch.cuda.is_available() else 'cpu')
self.fn = lpips.LPIPS(net=net).eval().to(self.device)
def measure(self):
return np.mean(self.results)
def report(self):
return f'LPIPS ({self.net}) = {self.measure():.6f}'
def read_img_list(self, img_list):
size = self.size
images = []
white_background = np.ones((size, size, 3), dtype=np.uint8) * 255
for img_path in img_list:
img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
if img.shape[2] == 4: # Handle BGRA images
alpha = img[:, :, 3] # Extract alpha channel
img = cv2.cvtColor(img,
cv2.COLOR_BGRA2BGR) # Convert BGRA to BGR
img = cv2.cvtColor(img,
cv2.COLOR_BGR2RGB) # Convert BGR to RGB
img[np.where(alpha == 0)] = [
255, 255, 255
] # Set transparent pixels to white
else: # Handle other image formats like JPG and PNG
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
images.append(img)
images = np.stack(images, axis=0)
# images[np.where(images == 0)] = 255 # Set black pixels to white
# images = np.where(images == 0, white_background, images) # Set transparent pixels to white
images = images.astype(np.float32) / 255.0
return images
# * recommend to use this function for evaluation
@torch.no_grad()
def score_gt(self, ref_paths, novel_paths):
self.results = []
for path0, path1 in zip(ref_paths, novel_paths):
# Load images
# img0 = lpips.im2tensor(lpips.load_image(path0)).cuda() # RGB image from [-1,1]
# img1 = lpips.im2tensor(lpips.load_image(path1)).cuda()
img0, img1 = self.read_img_list([path0]), self.read_img_list(
[path1])
img0, img1 = numpy_to_torch(img0), numpy_to_torch(img1)
# print(img0.shape,img1.shape)
img0 = F.interpolate(img0,
size=(self.size, self.size),
mode='area')
img1 = F.interpolate(img1,
size=(self.size, self.size),
mode='area')
# for debug vis
# plt.imshow(img0.cpu().squeeze(0).permute(1, 2, 0).numpy())
# plt.show()
# plt.imshow(img1.cpu().squeeze(0).permute(1, 2, 0).numpy())
# plt.show()
# equivalent to cv2.resize(rgba, (w, h), interpolation=cv2.INTER_AREA
# print(img0.shape,img1.shape)
self.results.append(self.fn.forward(img0, img1).cpu().numpy())
return self.measure()
class PSNRMeter:
def __init__(self, size=800):
self.results = []
self.size = size
def read_img_list(self, img_list):
size = self.size
images = []
white_background = np.ones((size, size, 3), dtype=np.uint8) * 255
for img_path in img_list:
img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
if img.shape[2] == 4: # Handle BGRA images
alpha = img[:, :, 3] # Extract alpha channel
img = cv2.cvtColor(img,
cv2.COLOR_BGRA2BGR) # Convert BGRA to BGR
img = cv2.cvtColor(img,
cv2.COLOR_BGR2RGB) # Convert BGR to RGB
img[np.where(alpha == 0)] = [
255, 255, 255
] # Set transparent pixels to white
else: # Handle other image formats like JPG and PNG
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
images.append(img)
images = np.stack(images, axis=0)
# images[np.where(images == 0)] = 255 # Set black pixels to white
# images = np.where(images == 0, white_background, images) # Set transparent pixels to white
images = images.astype(np.float32) / 255.0
# print(images.shape)
return images
def update(self, preds, truths):
# print(preds.shape)
psnr_values = []
# For each pair of images in the batches
for img1, img2 in zip(preds, truths):
# Compute the PSNR and add it to the list
# print(img1.shape,img2.shape)
# for debug
# plt.imshow(img1)
# plt.show()
# plt.imshow(img2)
# plt.show()
psnr = compare_psnr(
img1, img2,
data_range=1.0) # assuming your images are scaled to [0,1]
# print(f"temp psnr {psnr}")
psnr_values.append(psnr)
# Convert the list of PSNR values to a numpy array
self.results = psnr_values
def measure(self):
return np.mean(self.results)
def report(self):
return f'PSNR = {self.measure():.6f}'
# * recommend to use this function for evaluation
def score_gt(self, ref_paths, novel_paths):
self.results = []
# [B, N, 3] or [B, H, W, 3], range[0, 1]
preds = self.read_img_list(ref_paths)
truths = self.read_img_list(novel_paths)
self.update(preds, truths)
return self.measure()
all_inputs = 'data'
nerf_dataset = os.listdir(osp(all_inputs, 'nerf4'))
realfusion_dataset = os.listdir(osp(all_inputs, 'realfusion15'))
meta_examples = {
'nerf4': nerf_dataset,
'realfusion15': realfusion_dataset,
}
all_datasets = meta_examples.keys()
# organization 1
def deprecated_score_from_method_for_dataset(my_scorer,
method,
dataset,
input,
output,
score_type='clip',
): # psnr, lpips
# print("\n\n\n")
# print(f"______{method}___{dataset}___{score_type}_________")
scores = {}
final_res = 0
examples = meta_examples[dataset]
for i in range(len(examples)):
# compare entire folder for clip
if score_type == 'clip':
novel_view = osp(pred_path, examples[i], 'colors')
# compare first image for other metrics
else:
if method == '3d_fuse': method = '3d_fuse_0'
novel_view = list(
glob.glob(
osp(pred_path, examples[i], 'colors',
'step_0000*')))[0]
score_i = my_scorer.score_gt(
[], [novel_view])
scores[examples[i]] = score_i
final_res += score_i
# print(scores, " Avg : ", final_res / len(examples))
# print("``````````````````````")
return scores
# results organization 2
def score_from_method_for_dataset(my_scorer,
input_path,
pred_path,
score_type='clip',
rgb_name='lambertian',
result_folder='results/images',
first_str='*0000*'
): # psnr, lpips
scores = {}
final_res = 0
examples = os.listdir(input_path)
for i in range(len(examples)):
# ref path
ref_path = osp(input_path, examples[i], 'rgba.png')
# compare entire folder for clip
if score_type == 'clip':
novel_view = glob.glob(osp(pred_path,'*'+examples[i]+'*', result_folder, f'*{rgb_name}*'))
print(f'[INOF] {score_type} loss for example {examples[i]} between 1 GT and {len(novel_view)} predictions')
# compare first image for other metrics
else:
novel_view = glob.glob(osp(pred_path, '*'+examples[i]+'*/', result_folder, f'{first_str}{rgb_name}*'))
print(f'[INOF] {score_type} loss for example {examples[i]} between {ref_path} and {novel_view}')
# breakpoint()
score_i = my_scorer.score_gt([ref_path], novel_view)
scores[examples[i]] = score_i
final_res += score_i
avg_score = final_res / len(examples)
scores['average'] = avg_score
return scores
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Script to accept three string arguments")
parser.add_argument("--input_path",
default=all_inputs,
help="Specify the input path")
parser.add_argument("--pred_pattern",
default="out/magic123*",
help="Specify the pattern of predition paths")
parser.add_argument("--results_folder",
default="results/images",
help="where are the results under each pred_path")
parser.add_argument("--rgb_name",
default="lambertian",
help="the postfix of the image")
parser.add_argument("--first_str",
default="*0000*",
help="the str to indicate the first view")
parser.add_argument("--datasets",
default=all_datasets,
nargs='*',
help="Specify the output path")
parser.add_argument("--device",
type=int,
default=0,
help="Specify the GPU device to be used")
parser.add_argument("--save_dir", type=str, default='all_metrics/results')
args = parser.parse_args()
clip_scorer = CLIP(args.device)
lpips_scorer = LPIPSMeter()
psnr_scorer = PSNRMeter()
os.makedirs(args.save_dir, exist_ok=True)
for dataset in args.datasets:
input_path = osp(args.input_path, dataset)
# assume the pred_path is organized as: pred_path/methods/dataset
pred_pattern = osp(args.pred_pattern, dataset)
pred_paths = glob.glob(pred_pattern)
print(f"[INFO] Following the pattern {pred_pattern}, find {len(pred_paths)} pred_paths: \n", pred_paths)
if len(pred_paths) == 0:
raise IOError
for pred_path in pred_paths:
if not os.path.exists(pred_path):
print(f'[WARN] prediction does not exit for {pred_path}')
else:
print(f'[INFO] evaluate {pred_path}')
results_dict = {}
results_dict['clip'] = score_from_method_for_dataset(
clip_scorer, input_path, pred_path, 'clip',
result_folder=args.results_folder, rgb_name=args.rgb_name, first_str=args.first_str)
results_dict['psnr'] = score_from_method_for_dataset(
psnr_scorer, input_path, pred_path, 'psnr',
result_folder=args.results_folder, rgb_name=args.rgb_name, first_str=args.first_str)
results_dict['lpips'] = score_from_method_for_dataset(
lpips_scorer, input_path, pred_path, 'lpips',
result_folder=args.results_folder, rgb_name=args.rgb_name, first_str=args.first_str)
df = pd.DataFrame(results_dict)
method = pred_path.split('/')[-2]
print(osp(pred_path, args.results_folder))
results_str = '_'.join(args.results_folder.split('/'))
print(method+'-'+results_str)
print(df)
df.to_csv(f"{args.save_dir}/{method}-{results_str}-{dataset}.csv")

View File

@@ -0,0 +1,36 @@
import subprocess
prompt_list = [
"a delicious hamburger",
"A DSLR photo of a roast turkey on a platter",
"A high quality photo of a dragon",
"A DSLR photo of a bald eagle",
"A bunch of blue rose, highly detailed",
"A 3D model of an adorable cottage with a thatched roof",
"A high quality photo of a furry corgi",
"A DSLR photo of a panda",
"a DSLR photo of a cat lying on its side batting at a ball of yarn",
"a beautiful dress made out of fruit, on a mannequin. Studio lighting, high quality, high resolution",
"a DSLR photo of a corgi wearing a beret and holding a baguette, standing up on two hind legs",
"a zoomed out DSLR photo of a stack of pancakes",
"a zoomed out DSLR photo of a baby bunny sitting on top of a stack of pancakes",
]
negative_prompt = "oversaturated color, ugly, tiling, low quality, noise, ugly pattern"
gpu_id = 0
max_steps = 10
val_check = 1
out_name = "gsgen_baseline"
for prompt in prompt_list:
print(f"Running model on device {gpu_id}: ", prompt)
command = [
"python", "launch.py",
"--config", "configs/gaussian_splatting.yaml",
"--train",
f"system.prompt_processor.prompt={prompt}",
f"system.prompt_processor.negative_prompt={negative_prompt}",
f"name={out_name}",
"--gpu", f"{gpu_id}"
]
subprocess.run(command)

View File

@@ -0,0 +1,13 @@
NAME="dragon2"
# Phase 1 - 64x64
python launch.py --config configs/zero123.yaml --train --gpu 7 data.image_path=./load/images/${NAME}_rgba.png use_timestamp=False name=${NAME} tag=Phase1 # system.freq.guidance_eval=0 system.loggers.wandb.enable=false system.loggers.wandb.project="zero123" system.loggers.wandb.name=${NAME}_Phase1
# Phase 1.5 - 512 refine
python launch.py --config configs/zero123-geometry.yaml --train --gpu 4 data.image_path=./load/images/${NAME}_rgba.png system.geometry_convert_from=./outputs/${NAME}/Phase1/ckpts/last.ckpt use_timestamp=False name=${NAME} tag=Phase1p5 # system.freq.guidance_eval=0 system.loggers.wandb.enable=false system.loggers.wandb.project="zero123" system.loggers.wandb.name=${NAME}_Phase1p5
# Phase 2 - dreamfusion
python launch.py --config configs/experimental/imagecondition_zero123nerf.yaml --train --gpu 5 data.image_path=./load/images/${NAME}_rgba.png system.prompt_processor.prompt="A 3D model of a friendly dragon" system.weights="/admin/home-vikram/git/threestudio/outputs/${NAME}/Phase1/ckpts/last.ckpt" name=${NAME} tag=Phase2 # system.freq.guidance_eval=0 system.loggers.wandb.enable=false system.loggers.wandb.project="zero123" system.loggers.wandb.name=${NAME}_Phase2
# Phase 2 - SDF + dreamfusion
python launch.py --config configs/experimental/imagecondition_zero123nerf_refine.yaml --train --gpu 5 data.image_path=./load/images/${NAME}_rgba.png system.prompt_processor.prompt="A 3D model of a friendly dragon" system.geometry_convert_from="/admin/home-vikram/git/threestudio/outputs/${NAME}/Phase1/ckpts/last.ckpt" name=${NAME} tag=Phase2_refine # system.freq.guidance_eval=0 system.loggers.wandb.enable=false system.loggers.wandb.project="zero123" system.loggers.wandb.name=${NAME}_Phase2_refine

View File

@@ -0,0 +1,23 @@
# with standard zero123
threestudio/scripts/run_zero123_phase.sh 6 anya_front 105000 0
# with zero123XL (not released yet!)
threestudio/scripts/run_zero123_phase.sh 1 anya_front XL_20230604 0
threestudio/scripts/run_zero123_phase.sh 2 baby_phoenix_on_ice XL_20230604 20
threestudio/scripts/run_zero123_phase.sh 3 beach_house_1 XL_20230604 50
threestudio/scripts/run_zero123_phase.sh 4 bollywood_actress XL_20230604 0
threestudio/scripts/run_zero123_phase.sh 5 beach_house_2 XL_20230604 30
threestudio/scripts/run_zero123_phase.sh 6 hamburger XL_20230604 10
threestudio/scripts/run_zero123_phase.sh 7 cactus XL_20230604 8
threestudio/scripts/run_zero123_phase.sh 0 catstatue XL_20230604 50
threestudio/scripts/run_zero123_phase.sh 1 church_ruins XL_20230604 0
threestudio/scripts/run_zero123_phase.sh 2 firekeeper XL_20230604 10
threestudio/scripts/run_zero123_phase.sh 3 futuristic_car XL_20230604 20
threestudio/scripts/run_zero123_phase.sh 4 mona_lisa XL_20230604 10
threestudio/scripts/run_zero123_phase.sh 5 teddy XL_20230604 20
# set guidance_eval to 0, to greatly speed up training
threestudio/scripts/run_zero123_phase.sh 7 anya_front XL_20230604 0 system.freq.guidance_eval=0
# disable wandb for faster training (or if you don't want to use it)
threestudio/scripts/run_zero123_phase.sh 7 anya_front XL_20230604 0 system.loggers.wandb.enable=false system.freq.guidance_eval=0

View File

@@ -0,0 +1,25 @@
NAME="dragon2"
# Phase 1 - 64x64
python launch.py --config configs/zero123_64.yaml --train --gpu 7 system.loggers.wandb.enable=false system.loggers.wandb.project="voletiv-anya-new" system.loggers.wandb.name=${NAME} data.image_path=./load/images/${NAME}_rgba.png system.freq.guidance_eval=0 system.guidance.pretrained_model_name_or_path="./load/zero123/XL_20230604.ckpt" use_timestamp=False name=${NAME} tag="Phase1_64"
# python threestudio/scripts/make_training_vid.py --exp /admin/home-vikram/git/threestudio/outputs/zero123/64_dragon2_rgba.png@20230628-152734 --frames_per_vid 30 --fps 20 --max_iters 200
# # Phase 1.5 - 512
# python launch.py --config configs/zero123_512.yaml --train --gpu 5 system.loggers.wandb.enable=true system.loggers.wandb.project="voletiv-zero123XL-demo" system.loggers.wandb.name="robot_512_drel_n_XL_SAMEgeom" data.image_path=./load/images/robot_rgba.png system.freq.guidance_eval=0 system.guidance.pretrained_model_name_or_path="./load/zero123/XL_20230604.ckpt" tag='${data.random_camera.height}_${rmspace:${basename:${data.image_path}},_}_XL_SAMEgeom' system.weights="/admin/home-vikram/git/threestudio/outputs/zero123/[64, 128]_robot_rgba.png_OLD@20230630-052314/ckpts/last.ckpt"
# Phase 1.5 - 512 refine
python launch.py --config configs/zero123-geometry.yaml --train --gpu 4 system.loggers.wandb.enable=false system.loggers.wandb.project="voletiv-zero123XL-demo" system.loggers.wandb.name="robot_512_drel_n_XL_SAMEg" system.freq.guidance_eval=0 data.image_path=./load/images/${NAME}_rgba.png system.geometry_convert_from=./outputs/${NAME}/Phase1_64/ckpts/last.ckpt use_timestamp=False name=${NAME} tag="Phase2_512geom"
# Phase 2 - dreamfusion
python launch.py --config configs/experimental/imagecondition_zero123nerf.yaml --train --gpu 5 system.loggers.wandb.enable=false system.loggers.wandb.project="voletiv-zero123XL-demo" system.loggers.wandb.name="robot_512_drel_n_XL_SAMEw" tag='${data.random_camera.height}_${rmspace:${basename:${data.image_path}},_}_XL_Phase2' system.freq.guidance_eval=0 data.image_path=./load/images/robot_rgba.png system.prompt_processor.prompt="A DSLR 3D photo of a cute anime schoolgirl stands proudly with her arms in the air, pink hair ( unreal engine 5 trending on Artstation Ghibli 4k )" system.weights="/admin/home-vikram/git/threestudio/outputs/zero123/[64, 128]_robot_rgba.png_OLD@20230630-052314/ckpts/last.ckpt"
python launch.py --config configs/experimental/imagecondition_zero123nerf_refine.yaml --train --gpu 5 system.loggers.wandb.enable=false system.loggers.wandb.project="voletiv-zero123XL-demo" system.loggers.wandb.name="robot_512_drel_n_XL_SAMEw" tag='${data.random_camera.height}_${rmspace:${basename:${data.image_path}},_}_XL_Phase2_refine' system.freq.guidance_eval=0 data.image_path=./load/images/robot_rgba.png system.prompt_processor.prompt="A 3D model of a friendly dragon" system.geometry_convert_from="/admin/home-vikram/git/threestudio/outputs/zero123/[64, 128, 256]_dragon2_rgba.png_XL_REPEAT@20230705-023531/ckpts/last.ckpt"
# A DSLR 3D photo of a cute anime schoolgirl stands proudly with her arms in the air, pink hair ( unreal engine 5 trending on Artstation Ghibli 4k )"
# "/admin/home-vikram/git/threestudio/outputs/zero123/[64, 128]_robot_rgba.png_OLD@20230630-052314/ckpts/last.ckpt"
# Adds zero123_512-refine.yaml
# Adds resolution_milestones to image.py
# guidance_eval gets max batch_size 4
# Introduces random_bg in solid_color_bg

View File

@@ -0,0 +1,14 @@
GPU_ID=$1 # e.g. 0
IMAGE_PREFIX=$2 # e.g. "anya_front"
ZERO123_PREFIX=$3 # e.g. "XL_20230604"
ELEVATION=$4 # e.g. 0
REST=${@:5:99} # e.g. "system.guidance.min_step_percent=0.1 system.guidance.max_step_percent=0.9"
# change this config if you don't use wandb or want to speed up training
python launch.py --config configs/zero123.yaml --train --gpu $GPU_ID system.loggers.wandb.enable=true system.loggers.wandb.project="claforte-noise_atten" \
system.loggers.wandb.name="${IMAGE_PREFIX}_zero123_${ZERO123_PREFIX}...fov20_${REST}" \
data.image_path=./load/images/${IMAGE_PREFIX}_rgba.png system.freq.guidance_eval=37 \
system.guidance.pretrained_model_name_or_path="./load/zero123/${ZERO123_PREFIX}.ckpt" \
system.guidance.cond_elevation_deg=$ELEVATION \
${REST}

View File

@@ -0,0 +1,5 @@
# Reconstruct Anya using latest Zero123XL, in <2000 steps.
python launch.py --config configs/zero123.yaml --train --gpu 0 system.loggers.wandb.enable=true system.loggers.wandb.project="voletiv-anya-new" system.loggers.wandb.name="claforte_params" data.image_path=./load/images/anya_front_rgba.png system.freq.ref_or_zero123="accumulate" system.freq.guidance_eval=13 system.guidance.pretrained_model_name_or_path="./load/zero123/XL_20230604.ckpt"
# PHASE 2
python launch.py --config configs/experimental/imagecondition_zero123nerf.yaml --train --gpu 0 system.prompt_processor.prompt="A DSLR 3D photo of a cute anime schoolgirl stands proudly with her arms in the air, pink hair ( unreal engine 5 trending on Artstation Ghibli 4k )" system.weights=outputs/zero123/128_anya_front_rgba.png@20230623-145711/ckpts/last.ckpt system.freq.guidance_eval=13 system.loggers.wandb.enable=true system.loggers.wandb.project="voletiv-anya-new" data.image_path=./load/images/anya_front_rgba.png system.loggers.wandb.name="anya" data.random_camera.progressive_until=500

View File

@@ -0,0 +1,54 @@
from diffusers import StableDiffusionPipeline, DDIMScheduler
import torch
# model_id = "load/checkpoints/sd_21_base_mushroom_vd_prompt"
# model_id = "load/checkpoints/sd_base_mushroom"
model_id = ".cache/checkpoints/sd_21_base_rabbit"
# scheduler = DDIMScheduler()
pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda")
guidance_scale = 7.5
prompt = "a sks rabbit, front view"
image = pipe(prompt, num_inference_steps=50, guidance_scale=guidance_scale).images[0]
image.save("debug.png")
# import os
# import cv2
# import glob
# import torch
# import argparse
# import numpy as np
# from tqdm import tqdm
# import pytorch_lightning as pl
# from torchvision.utils import save_image
# import threestudio
# from threestudio.utils.config import load_config
# if __name__ == "__main__":
# parser = argparse.ArgumentParser()
# parser.add_argument("--config", required=True, help="path to config file")
# parser.add_argument("--view_dependent_noise", action="store_true", help="use view depdendent noise strength")
# args, extras = parser.parse_known_args()
# cfg = load_config(args.config, cli_args=extras, n_gpus=1)
# guidance = threestudio.find(cfg.system.guidance_type)(cfg.system.guidance)
# prompt_processor = threestudio.find(cfg.system.prompt_processor_type)(cfg.system.prompt_processor)
# prompt_utils = prompt_processor()
# guidance.update_step(epoch=0, global_step=0)
# elevation, azimuth = torch.zeros(1).cuda(), torch.zeros(1).cuda()
# camera_distances = torch.tensor([3.0]).cuda()
# c2w = torch.zeros(4,4).cuda()
# a = guidance.sample(prompt_utils, elevation, azimuth, camera_distances) # sample_lora
# from torchvision.utils import save_image
# save_image(a.permute(0,3,1,2), "debug.png", normalize=True, value_range=(0,1))
# python threestudio/scripts/test_dreambooth.py --config configs/experimental/stablediffusion.yaml system.prompt_processor.prompt="a sks mushroom growing on a log" \
# system.guidance.pretrained_model_name_or_path_lora="load/checkpoints/sd_21_base_mushroom_camera_condition"

View File

@@ -0,0 +1,25 @@
import torch
from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler
# model_base = "stabilityai/stable-diffusion-2-1-base"
# pipe = DiffusionPipeline.from_pretrained(model_base, torch_dtype=torch.float16, cache_dir=CACHE_DIR, local_files_only=True)
# pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config, cache_dir=CACHE_DIR, local_files_only=True)
# lora_model_path = "load/checkpoints/sd_21_base_bear_dreambooth_lora"
# pipe.unet.load_attn_procs(lora_model_path)
# pipe.to("cuda")
# image = pipe("A picture of a sks bear in the sky", num_inference_steps=50, guidance_scale=7.5).images[0]
# image.save("bear_dreambooth_lora.png")
pipe = DiffusionPipeline.from_pretrained("DeepFloyd/IF-I-XL-v1.0", local_files_only=True, safety_checker=None)
pipe.load_lora_weights("if_dreambooth_mushroom")
pipe.scheduler = pipe.scheduler.__class__.from_config(pipe.scheduler.config, variance_type="fixed_small")
pipe.to("cuda:7")
image = pipe("A photo of a sks mushroom, front view", num_inference_steps=50, guidance_scale=7.5).images[0]
image.save("mushroom_dreambooth_lora.png")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,927 @@
# coding=utf-8
# Copyright 2023 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.
"""Fine-tuning script for Stable Diffusion for text2image with support for LoRA."""
import argparse
import logging
import math
import os
import random
import shutil
from pathlib import Path
import datasets
import numpy as np
import torch
import torch.nn.functional as F
import torch.utils.checkpoint
import transformers
from accelerate import Accelerator
from accelerate.logging import get_logger
from accelerate.utils import ProjectConfiguration, set_seed
from datasets import load_dataset
from huggingface_hub import create_repo, upload_folder
from packaging import version
from torchvision import transforms
from tqdm.auto import tqdm
from transformers import CLIPTextModel, CLIPTokenizer
import diffusers
from diffusers import AutoencoderKL, DDPMScheduler, DiffusionPipeline, UNet2DConditionModel
from diffusers.loaders import AttnProcsLayers
from diffusers.models.attention_processor import LoRAAttnProcessor
from diffusers.optimization import get_scheduler
from diffusers.training_utils import compute_snr
from diffusers.utils import check_min_version, is_wandb_available
from diffusers.utils.import_utils import is_xformers_available
# Will error if the minimal version of diffusers is not installed. Remove at your own risks.
check_min_version("0.24.0.dev0")
logger = get_logger(__name__, log_level="INFO")
def save_model_card(repo_id: str, images=None, base_model=str, dataset_name=str, repo_folder=None):
img_str = ""
for i, image in enumerate(images):
image.save(os.path.join(repo_folder, f"image_{i}.png"))
img_str += f"![img_{i}](./image_{i}.png)\n"
yaml = f"""
---
license: creativeml-openrail-m
base_model: {base_model}
tags:
- stable-diffusion
- stable-diffusion-diffusers
- text-to-image
- diffusers
- lora
inference: true
---
"""
model_card = f"""
# LoRA text2image fine-tuning - {repo_id}
These are LoRA adaption weights for {base_model}. The weights were fine-tuned on the {dataset_name} dataset. You can find some example images in the following. \n
{img_str}
"""
with open(os.path.join(repo_folder, "README.md"), "w") as f:
f.write(yaml + model_card)
def parse_args():
parser = argparse.ArgumentParser(description="Simple example of a training script.")
parser.add_argument(
"--pretrained_model_name_or_path",
type=str,
default=None,
required=True,
help="Path to pretrained model or model identifier from huggingface.co/models.",
)
parser.add_argument(
"--revision",
type=str,
default=None,
required=False,
help="Revision of pretrained model identifier from huggingface.co/models.",
)
parser.add_argument(
"--dataset_name",
type=str,
default=None,
help=(
"The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private,"
" dataset). It can also be a path pointing to a local copy of a dataset in your filesystem,"
" or to a folder containing files that 🤗 Datasets can understand."
),
)
parser.add_argument(
"--dataset_config_name",
type=str,
default=None,
help="The config of the Dataset, leave as None if there's only one config.",
)
parser.add_argument(
"--train_data_dir",
type=str,
default=None,
help=(
"A folder containing the training data. Folder contents must follow the structure described in"
" https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file"
" must exist to provide the captions for the images. Ignored if `dataset_name` is specified."
),
)
parser.add_argument(
"--image_column", type=str, default="image", help="The column of the dataset containing an image."
)
parser.add_argument(
"--caption_column",
type=str,
default="text",
help="The column of the dataset containing a caption or a list of captions.",
)
parser.add_argument(
"--validation_prompt", type=str, default=None, help="A prompt that is sampled during training for inference."
)
parser.add_argument(
"--num_validation_images",
type=int,
default=4,
help="Number of images that should be generated during validation with `validation_prompt`.",
)
parser.add_argument(
"--validation_epochs",
type=int,
default=1,
help=(
"Run fine-tuning validation every X epochs. The validation process consists of running the prompt"
" `args.validation_prompt` multiple times: `args.num_validation_images`."
),
)
parser.add_argument(
"--max_train_samples",
type=int,
default=None,
help=(
"For debugging purposes or quicker training, truncate the number of training examples to this "
"value if set."
),
)
parser.add_argument(
"--output_dir",
type=str,
default="sd-model-finetuned-lora",
help="The output directory where the model predictions and checkpoints will be written.",
)
parser.add_argument(
"--cache_dir",
type=str,
default=None,
help="The directory where the downloaded models and datasets will be stored.",
)
parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.")
parser.add_argument(
"--resolution",
type=int,
default=512,
help=(
"The resolution for input images, all the images in the train/validation dataset will be resized to this"
" resolution"
),
)
parser.add_argument(
"--center_crop",
default=False,
action="store_true",
help=(
"Whether to center crop the input images to the resolution. If not set, the images will be randomly"
" cropped. The images will be resized to the resolution first before cropping."
),
)
parser.add_argument(
"--random_flip",
action="store_true",
help="whether to randomly flip images horizontally",
)
parser.add_argument(
"--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader."
)
parser.add_argument("--num_train_epochs", type=int, default=100)
parser.add_argument(
"--max_train_steps",
type=int,
default=None,
help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
)
parser.add_argument(
"--gradient_accumulation_steps",
type=int,
default=1,
help="Number of updates steps to accumulate before performing a backward/update pass.",
)
parser.add_argument(
"--gradient_checkpointing",
action="store_true",
help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.",
)
parser.add_argument(
"--learning_rate",
type=float,
default=1e-4,
help="Initial learning rate (after the potential warmup period) to use.",
)
parser.add_argument(
"--scale_lr",
action="store_true",
default=False,
help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.",
)
parser.add_argument(
"--lr_scheduler",
type=str,
default="constant",
help=(
'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",'
' "constant", "constant_with_warmup"]'
),
)
parser.add_argument(
"--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler."
)
parser.add_argument(
"--snr_gamma",
type=float,
default=None,
help="SNR weighting gamma to be used if rebalancing the loss. Recommended value is 5.0. "
"More details here: https://arxiv.org/abs/2303.09556.",
)
parser.add_argument(
"--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes."
)
parser.add_argument(
"--allow_tf32",
action="store_true",
help=(
"Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see"
" https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices"
),
)
parser.add_argument(
"--dataloader_num_workers",
type=int,
default=0,
help=(
"Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process."
),
)
parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.")
parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.")
parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.")
parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer")
parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.")
parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.")
parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.")
parser.add_argument(
"--prediction_type",
type=str,
default=None,
help="The prediction_type that shall be used for training. Choose between 'epsilon' or 'v_prediction' or leave `None`. If left to `None` the default prediction type of the scheduler: `noise_scheduler.config.prediciton_type` is chosen.",
)
parser.add_argument(
"--hub_model_id",
type=str,
default=None,
help="The name of the repository to keep in sync with the local `output_dir`.",
)
parser.add_argument(
"--logging_dir",
type=str,
default="logs",
help=(
"[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to"
" *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***."
),
)
parser.add_argument(
"--mixed_precision",
type=str,
default=None,
choices=["no", "fp16", "bf16"],
help=(
"Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >="
" 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the"
" flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config."
),
)
parser.add_argument(
"--report_to",
type=str,
default="tensorboard",
help=(
'The integration to report the results and logs to. Supported platforms are `"tensorboard"`'
' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.'
),
)
parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank")
parser.add_argument(
"--checkpointing_steps",
type=int,
default=500,
help=(
"Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming"
" training using `--resume_from_checkpoint`."
),
)
parser.add_argument(
"--checkpoints_total_limit",
type=int,
default=None,
help=("Max number of checkpoints to store."),
)
parser.add_argument(
"--resume_from_checkpoint",
type=str,
default=None,
help=(
"Whether training should be resumed from a previous checkpoint. Use a path saved by"
' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.'
),
)
parser.add_argument(
"--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers."
)
parser.add_argument("--noise_offset", type=float, default=0, help="The scale of noise offset.")
parser.add_argument(
"--rank",
type=int,
default=4,
help=("The dimension of the LoRA update matrices."),
)
args = parser.parse_args()
env_local_rank = int(os.environ.get("LOCAL_RANK", -1))
if env_local_rank != -1 and env_local_rank != args.local_rank:
args.local_rank = env_local_rank
# Sanity checks
if args.dataset_name is None and args.train_data_dir is None:
raise ValueError("Need either a dataset name or a training folder.")
return args
DATASET_NAME_MAPPING = {
"lambdalabs/pokemon-blip-captions": ("image", "text"),
}
def main():
args = parse_args()
logging_dir = Path(args.output_dir, args.logging_dir)
accelerator_project_config = ProjectConfiguration(project_dir=args.output_dir, logging_dir=logging_dir)
accelerator = Accelerator(
gradient_accumulation_steps=args.gradient_accumulation_steps,
mixed_precision=args.mixed_precision,
log_with=args.report_to,
project_config=accelerator_project_config,
)
if args.report_to == "wandb":
if not is_wandb_available():
raise ImportError("Make sure to install wandb if you want to use it for logging during training.")
import wandb
# Make one log on every process with the configuration for debugging.
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
level=logging.INFO,
)
logger.info(accelerator.state, main_process_only=False)
if accelerator.is_local_main_process:
datasets.utils.logging.set_verbosity_warning()
transformers.utils.logging.set_verbosity_warning()
diffusers.utils.logging.set_verbosity_info()
else:
datasets.utils.logging.set_verbosity_error()
transformers.utils.logging.set_verbosity_error()
diffusers.utils.logging.set_verbosity_error()
# If passed along, set the training seed now.
if args.seed is not None:
set_seed(args.seed)
# Handle the repository creation
if accelerator.is_main_process:
if args.output_dir is not None:
os.makedirs(args.output_dir, exist_ok=True)
if args.push_to_hub:
repo_id = create_repo(
repo_id=args.hub_model_id or Path(args.output_dir).name, exist_ok=True, token=args.hub_token
).repo_id
# Load scheduler, tokenizer and models.
noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler")
tokenizer = CLIPTokenizer.from_pretrained(
args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision
)
text_encoder = CLIPTextModel.from_pretrained(
args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision
)
vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision)
unet = UNet2DConditionModel.from_pretrained(
args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision
)
# freeze parameters of models to save more memory
unet.requires_grad_(False)
vae.requires_grad_(False)
text_encoder.requires_grad_(False)
# For mixed precision training we cast all non-trainable weigths (vae, non-lora text_encoder and non-lora unet) to half-precision
# as these weights are only used for inference, keeping weights in full precision is not required.
weight_dtype = torch.float32
if accelerator.mixed_precision == "fp16":
weight_dtype = torch.float16
elif accelerator.mixed_precision == "bf16":
weight_dtype = torch.bfloat16
# Move unet, vae and text_encoder to device and cast to weight_dtype
unet.to(accelerator.device, dtype=weight_dtype)
vae.to(accelerator.device, dtype=weight_dtype)
text_encoder.to(accelerator.device, dtype=weight_dtype)
# now we will add new LoRA weights to the attention layers
# It's important to realize here how many attention weights will be added and of which sizes
# The sizes of the attention layers consist only of two different variables:
# 1) - the "hidden_size", which is increased according to `unet.config.block_out_channels`.
# 2) - the "cross attention size", which is set to `unet.config.cross_attention_dim`.
# Let's first see how many attention processors we will have to set.
# For Stable Diffusion, it should be equal to:
# - down blocks (2x attention layers) * (2x transformer layers) * (3x down blocks) = 12
# - mid blocks (2x attention layers) * (1x transformer layers) * (1x mid blocks) = 2
# - up blocks (2x attention layers) * (3x transformer layers) * (3x down blocks) = 18
# => 32 layers
# Set correct lora layers
lora_attn_procs = {}
for name in unet.attn_processors.keys():
cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim
if name.startswith("mid_block"):
hidden_size = unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = unet.config.block_out_channels[block_id]
lora_attn_procs[name] = LoRAAttnProcessor(
hidden_size=hidden_size,
cross_attention_dim=cross_attention_dim,
rank=args.rank,
)
unet.set_attn_processor(lora_attn_procs)
if args.enable_xformers_memory_efficient_attention:
if is_xformers_available():
import xformers
xformers_version = version.parse(xformers.__version__)
if xformers_version == version.parse("0.0.16"):
logger.warn(
"xFormers 0.0.16 cannot be used for training in some GPUs. If you observe problems during training, please update xFormers to at least 0.0.17. See https://huggingface.co/docs/diffusers/main/en/optimization/xformers for more details."
)
unet.enable_xformers_memory_efficient_attention()
else:
raise ValueError("xformers is not available. Make sure it is installed correctly")
lora_layers = AttnProcsLayers(unet.attn_processors)
# Enable TF32 for faster training on Ampere GPUs,
# cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
if args.allow_tf32:
torch.backends.cuda.matmul.allow_tf32 = True
if args.scale_lr:
args.learning_rate = (
args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes
)
# Initialize the optimizer
if args.use_8bit_adam:
try:
import bitsandbytes as bnb
except ImportError:
raise ImportError(
"Please install bitsandbytes to use 8-bit Adam. You can do so by running `pip install bitsandbytes`"
)
optimizer_cls = bnb.optim.AdamW8bit
else:
optimizer_cls = torch.optim.AdamW
optimizer = optimizer_cls(
lora_layers.parameters(),
lr=args.learning_rate,
betas=(args.adam_beta1, args.adam_beta2),
weight_decay=args.adam_weight_decay,
eps=args.adam_epsilon,
)
# Get the datasets: you can either provide your own training and evaluation files (see below)
# or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub).
# In distributed training, the load_dataset function guarantees that only one local process can concurrently
# download the dataset.
if args.dataset_name is not None:
# Downloading and loading a dataset from the hub.
dataset = load_dataset(
args.dataset_name,
args.dataset_config_name,
cache_dir=args.cache_dir,
data_dir=args.train_data_dir,
)
else:
data_files = {}
if args.train_data_dir is not None:
data_files["train"] = os.path.join(args.train_data_dir, "**")
dataset = load_dataset(
"imagefolder",
data_files=data_files,
cache_dir=args.cache_dir,
)
# See more about loading custom images at
# https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder
# Preprocessing the datasets.
# We need to tokenize inputs and targets.
column_names = dataset["train"].column_names
# 6. Get the column names for input/target.
dataset_columns = DATASET_NAME_MAPPING.get(args.dataset_name, None)
if args.image_column is None:
image_column = dataset_columns[0] if dataset_columns is not None else column_names[0]
else:
image_column = args.image_column
if image_column not in column_names:
raise ValueError(
f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}"
)
if args.caption_column is None:
caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1]
else:
caption_column = args.caption_column
if caption_column not in column_names:
raise ValueError(
f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}"
)
# Preprocessing the datasets.
# We need to tokenize input captions and transform the images.
def tokenize_captions(examples, is_train=True):
captions = []
for caption in examples[caption_column]:
if isinstance(caption, str):
captions.append(caption)
elif isinstance(caption, (list, np.ndarray)):
# take a random caption if there are multiple
captions.append(random.choice(caption) if is_train else caption[0])
else:
raise ValueError(
f"Caption column `{caption_column}` should contain either strings or lists of strings."
)
inputs = tokenizer(
captions, max_length=tokenizer.model_max_length, padding="max_length", truncation=True, return_tensors="pt"
)
return inputs.input_ids
# Preprocessing the datasets.
train_transforms = transforms.Compose(
[
transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR),
transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution),
transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]),
]
)
def preprocess_train(examples):
images = [image.convert("RGB") for image in examples[image_column]]
examples["pixel_values"] = [train_transforms(image) for image in images]
examples["input_ids"] = tokenize_captions(examples)
return examples
with accelerator.main_process_first():
if args.max_train_samples is not None:
dataset["train"] = dataset["train"].shuffle(seed=args.seed).select(range(args.max_train_samples))
# Set the training transforms
train_dataset = dataset["train"].with_transform(preprocess_train)
def collate_fn(examples):
pixel_values = torch.stack([example["pixel_values"] for example in examples])
pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
input_ids = torch.stack([example["input_ids"] for example in examples])
return {"pixel_values": pixel_values, "input_ids": input_ids}
# DataLoaders creation:
train_dataloader = torch.utils.data.DataLoader(
train_dataset,
shuffle=True,
collate_fn=collate_fn,
batch_size=args.train_batch_size,
num_workers=args.dataloader_num_workers,
)
# Scheduler and math around the number of training steps.
overrode_max_train_steps = False
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
if args.max_train_steps is None:
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
overrode_max_train_steps = True
lr_scheduler = get_scheduler(
args.lr_scheduler,
optimizer=optimizer,
num_warmup_steps=args.lr_warmup_steps * accelerator.num_processes,
num_training_steps=args.max_train_steps * accelerator.num_processes,
)
# Prepare everything with our `accelerator`.
lora_layers, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
lora_layers, optimizer, train_dataloader, lr_scheduler
)
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
if overrode_max_train_steps:
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
# Afterwards we recalculate our number of training epochs
args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
# We need to initialize the trackers we use, and also store our configuration.
# The trackers initializes automatically on the main process.
if accelerator.is_main_process:
accelerator.init_trackers("text2image-fine-tune", config=vars(args))
# Train!
total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps
logger.info("***** Running training *****")
logger.info(f" Num examples = {len(train_dataset)}")
logger.info(f" Num Epochs = {args.num_train_epochs}")
logger.info(f" Instantaneous batch size per device = {args.train_batch_size}")
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}")
logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
logger.info(f" Total optimization steps = {args.max_train_steps}")
global_step = 0
first_epoch = 0
# Potentially load in the weights and states from a previous save
if args.resume_from_checkpoint:
if args.resume_from_checkpoint != "latest":
path = os.path.basename(args.resume_from_checkpoint)
else:
# Get the most recent checkpoint
dirs = os.listdir(args.output_dir)
dirs = [d for d in dirs if d.startswith("checkpoint")]
dirs = sorted(dirs, key=lambda x: int(x.split("-")[1]))
path = dirs[-1] if len(dirs) > 0 else None
if path is None:
accelerator.print(
f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run."
)
args.resume_from_checkpoint = None
initial_global_step = 0
else:
accelerator.print(f"Resuming from checkpoint {path}")
accelerator.load_state(os.path.join(args.output_dir, path))
global_step = int(path.split("-")[1])
initial_global_step = global_step
first_epoch = global_step // num_update_steps_per_epoch
else:
initial_global_step = 0
progress_bar = tqdm(
range(0, args.max_train_steps),
initial=initial_global_step,
desc="Steps",
# Only show the progress bar once on each machine.
disable=not accelerator.is_local_main_process,
)
for epoch in range(first_epoch, args.num_train_epochs):
unet.train()
train_loss = 0.0
for step, batch in enumerate(train_dataloader):
with accelerator.accumulate(unet):
# Convert images to latent space
latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample()
latents = latents * vae.config.scaling_factor
# Sample noise that we'll add to the latents
noise = torch.randn_like(latents)
if args.noise_offset:
# https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += args.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
bsz = latents.shape[0]
# Sample a random timestep for each image
timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device)
timesteps = timesteps.long()
# Add noise to the latents according to the noise magnitude at each timestep
# (this is the forward diffusion process)
noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)
# Get the text embedding for conditioning
encoder_hidden_states = text_encoder(batch["input_ids"])[0]
# Get the target for loss depending on the prediction type
if args.prediction_type is not None:
# set prediction_type of scheduler if defined
noise_scheduler.register_to_config(prediction_type=args.prediction_type)
if noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif noise_scheduler.config.prediction_type == "v_prediction":
target = noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}")
# Predict the noise residual and compute loss
model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample
if args.snr_gamma is None:
loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
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(noise_scheduler, timesteps)
if noise_scheduler.config.prediction_type == "v_prediction":
# Velocity objective requires that we add one to SNR values before we divide by them.
snr = snr + 1
mse_loss_weights = (
torch.stack([snr, args.snr_gamma * torch.ones_like(timesteps)], dim=1).min(dim=1)[0] / snr
)
loss = F.mse_loss(model_pred.float(), target.float(), reduction="none")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
# Gather the losses across all processes for logging (if we use distributed training).
avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean()
train_loss += avg_loss.item() / args.gradient_accumulation_steps
# Backpropagate
accelerator.backward(loss)
if accelerator.sync_gradients:
params_to_clip = lora_layers.parameters()
accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
progress_bar.update(1)
global_step += 1
accelerator.log({"train_loss": train_loss}, step=global_step)
train_loss = 0.0
if global_step % args.checkpointing_steps == 0:
if accelerator.is_main_process:
# _before_ saving state, check if this save would set us over the `checkpoints_total_limit`
if args.checkpoints_total_limit is not None:
checkpoints = os.listdir(args.output_dir)
checkpoints = [d for d in checkpoints if d.startswith("checkpoint")]
checkpoints = sorted(checkpoints, key=lambda x: int(x.split("-")[1]))
# before we save the new checkpoint, we need to have at _most_ `checkpoints_total_limit - 1` checkpoints
if len(checkpoints) >= args.checkpoints_total_limit:
num_to_remove = len(checkpoints) - args.checkpoints_total_limit + 1
removing_checkpoints = checkpoints[0:num_to_remove]
logger.info(
f"{len(checkpoints)} checkpoints already exist, removing {len(removing_checkpoints)} checkpoints"
)
logger.info(f"removing checkpoints: {', '.join(removing_checkpoints)}")
for removing_checkpoint in removing_checkpoints:
removing_checkpoint = os.path.join(args.output_dir, removing_checkpoint)
shutil.rmtree(removing_checkpoint)
save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(save_path)
logger.info(f"Saved state to {save_path}")
logs = {"step_loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]}
progress_bar.set_postfix(**logs)
if global_step >= args.max_train_steps:
break
if accelerator.is_main_process:
if args.validation_prompt is not None and epoch % args.validation_epochs == 0:
logger.info(
f"Running validation... \n Generating {args.num_validation_images} images with prompt:"
f" {args.validation_prompt}."
)
# create pipeline
pipeline = DiffusionPipeline.from_pretrained(
args.pretrained_model_name_or_path,
unet=accelerator.unwrap_model(unet),
revision=args.revision,
torch_dtype=weight_dtype,
)
pipeline = pipeline.to(accelerator.device)
pipeline.set_progress_bar_config(disable=True)
# run inference
generator = torch.Generator(device=accelerator.device)
if args.seed is not None:
generator = generator.manual_seed(args.seed)
images = []
for _ in range(args.num_validation_images):
images.append(
pipeline(args.validation_prompt, num_inference_steps=30, generator=generator).images[0]
)
for tracker in accelerator.trackers:
if tracker.name == "tensorboard":
np_images = np.stack([np.asarray(img) for img in images])
tracker.writer.add_images("validation", np_images, epoch, dataformats="NHWC")
if tracker.name == "wandb":
tracker.log(
{
"validation": [
wandb.Image(image, caption=f"{i}: {args.validation_prompt}")
for i, image in enumerate(images)
]
}
)
del pipeline
torch.cuda.empty_cache()
# Save the lora layers
accelerator.wait_for_everyone()
if accelerator.is_main_process:
unet = unet.to(torch.float32)
unet.save_attn_procs(args.output_dir)
if args.push_to_hub:
save_model_card(
repo_id,
images=images,
base_model=args.pretrained_model_name_or_path,
dataset_name=args.dataset_name,
repo_folder=args.output_dir,
)
upload_folder(
repo_id=repo_id,
folder_path=args.output_dir,
commit_message="End of training",
ignore_patterns=["step_*", "epoch_*"],
)
# Final inference
# Load previous pipeline
pipeline = DiffusionPipeline.from_pretrained(
args.pretrained_model_name_or_path, revision=args.revision, torch_dtype=weight_dtype
)
pipeline = pipeline.to(accelerator.device)
# load attention processors
pipeline.unet.load_attn_procs(args.output_dir)
# run inference
generator = torch.Generator(device=accelerator.device)
if args.seed is not None:
generator = generator.manual_seed(args.seed)
images = []
for _ in range(args.num_validation_images):
images.append(pipeline(args.validation_prompt, num_inference_steps=30, generator=generator).images[0])
if accelerator.is_main_process:
for tracker in accelerator.trackers:
if len(images) != 0:
if tracker.name == "tensorboard":
np_images = np.stack([np.asarray(img) for img in images])
tracker.writer.add_images("test", np_images, epoch, dataformats="NHWC")
if tracker.name == "wandb":
tracker.log(
{
"test": [
wandb.Image(image, caption=f"{i}: {args.validation_prompt}")
for i, image in enumerate(images)
]
}
)
accelerator.end_training()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
from . import dreamcraft3d, zero123

396
threestudio/systems/base.py Normal file
View File

@@ -0,0 +1,396 @@
import os
from dataclasses import dataclass, field
import pytorch_lightning as pl
import torch.nn.functional as F
import threestudio
from threestudio.models.exporters.base import Exporter, ExporterOutput
from threestudio.systems.utils import parse_optimizer, parse_scheduler
from threestudio.utils.base import (
Updateable,
update_end_if_possible,
update_if_possible,
)
from threestudio.utils.config import parse_structured
from threestudio.utils.misc import C, cleanup, get_device, load_module_weights, find_last_path
from threestudio.utils.saving import SaverMixin
from threestudio.utils.typing import *
class BaseSystem(pl.LightningModule, Updateable, SaverMixin):
@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
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
class BaseLift3DSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
geometry_type: str = ""
geometry: dict = field(default_factory=dict)
geometry_convert_from: Optional[str] = None
geometry_convert_inherit_texture: bool = False
# used to override configurations of the previous geometry being converted from,
# for example isosurface_threshold
geometry_convert_override: dict = field(default_factory=dict)
material_type: str = ""
material: dict = field(default_factory=dict)
background_type: str = ""
background: dict = field(default_factory=dict)
renderer_type: str = ""
renderer: dict = field(default_factory=dict)
guidance_type: str = ""
guidance: dict = field(default_factory=dict)
prompt_processor_type: str = ""
prompt_processor: dict = field(default_factory=dict)
# geometry export configurations, no need to specify in training
exporter_type: str = "mesh-exporter"
exporter: dict = field(default_factory=dict)
cfg: Config
def configure(self) -> None:
self.cfg.geometry_convert_from = find_last_path(self.cfg.geometry_convert_from)
self.cfg.weights = find_last_path(self.cfg.weights)
if (
self.cfg.geometry_convert_from # from_coarse must be specified
and not self.cfg.weights # not initialized from coarse when weights are specified
and not self.resumed # not initialized from coarse when resumed from checkpoints
):
threestudio.info("Initializing geometry from a given checkpoint ...")
from threestudio.utils.config import load_config, parse_structured
prev_cfg = load_config(
os.path.join(
os.path.dirname(self.cfg.geometry_convert_from),
"../configs/parsed.yaml",
)
) # TODO: hard-coded relative path
prev_system_cfg: BaseLift3DSystem.Config = parse_structured(
self.Config, prev_cfg.system
)
prev_geometry_cfg = prev_system_cfg.geometry
prev_geometry_cfg.update(self.cfg.geometry_convert_override)
prev_geometry = threestudio.find(prev_system_cfg.geometry_type)(
prev_geometry_cfg
)
state_dict, epoch, global_step = load_module_weights(
self.cfg.geometry_convert_from,
module_name="geometry",
map_location="cpu",
)
prev_geometry.load_state_dict(state_dict, strict=False)
# restore step-dependent states
prev_geometry.do_update_step(epoch, global_step, on_load_weights=True)
# convert from coarse stage geometry
prev_geometry = prev_geometry.to(get_device())
self.geometry = threestudio.find(self.cfg.geometry_type).create_from(
prev_geometry,
self.cfg.geometry,
copy_net=self.cfg.geometry_convert_inherit_texture,
)
del prev_geometry
cleanup()
else:
self.geometry = threestudio.find(self.cfg.geometry_type)(self.cfg.geometry)
self.material = threestudio.find(self.cfg.material_type)(self.cfg.material)
self.background = threestudio.find(self.cfg.background_type)(
self.cfg.background
)
self.renderer = threestudio.find(self.cfg.renderer_type)(
self.cfg.renderer,
geometry=self.geometry,
material=self.material,
background=self.background,
)
def on_fit_start(self) -> None:
if self._save_dir is not None:
threestudio.info(f"Validation results will be saved to {self._save_dir}")
else:
threestudio.warn(
f"Saving directory not set for the system, visualization results will not be saved"
)
def on_test_end(self) -> None:
if self._save_dir is not None:
threestudio.info(f"Test results saved to {self._save_dir}")
def on_predict_start(self) -> None:
self.exporter: Exporter = threestudio.find(self.cfg.exporter_type)(
self.cfg.exporter,
geometry=self.geometry,
material=self.material,
background=self.background,
)
def predict_step(self, batch, batch_idx):
if self.exporter.cfg.save_video:
self.test_step(batch, batch_idx)
def on_predict_epoch_end(self) -> None:
if self.exporter.cfg.save_video:
self.on_test_epoch_end()
exporter_output: List[ExporterOutput] = self.exporter()
for out in exporter_output:
save_func_name = f"save_{out.save_type}"
if not hasattr(self, save_func_name):
raise ValueError(f"{save_func_name} not supported by the SaverMixin")
save_func = getattr(self, save_func_name)
save_func(f"it{self.true_global_step}-export/{out.save_name}", **out.params)
def on_predict_end(self) -> None:
if self._save_dir is not None:
threestudio.info(f"Export assets saved to {self._save_dir}")
def guidance_evaluation_save(self, comp_rgb, guidance_eval_out):
B, size = comp_rgb.shape[:2]
resize = lambda x: F.interpolate(
x.permute(0, 3, 1, 2), (size, size), mode="bilinear", align_corners=False
).permute(0, 2, 3, 1)
filename = f"it{self.true_global_step}-train.png"
def merge12(x):
return x.reshape(-1, *x.shape[2:])
self.save_image_grid(
filename,
[
{
"type": "rgb",
"img": merge12(comp_rgb),
"kwargs": {"data_format": "HWC"},
},
]
+ (
[
{
"type": "rgb",
"img": merge12(resize(guidance_eval_out["imgs_noisy"])),
"kwargs": {"data_format": "HWC"},
}
]
)
+ (
[
{
"type": "rgb",
"img": merge12(resize(guidance_eval_out["imgs_1step"])),
"kwargs": {"data_format": "HWC"},
}
]
)
+ (
[
{
"type": "rgb",
"img": merge12(resize(guidance_eval_out["imgs_1orig"])),
"kwargs": {"data_format": "HWC"},
}
]
)
+ (
[
{
"type": "rgb",
"img": merge12(resize(guidance_eval_out["imgs_final"])),
"kwargs": {"data_format": "HWC"},
}
]
),
name="train_step",
step=self.true_global_step,
texts=guidance_eval_out["texts"],
)

View File

@@ -0,0 +1,608 @@
import os
import random
import shutil
from dataclasses import dataclass, field
import cv2
import clip
import torch
import shutil
import numpy as np
import torch.nn.functional as F
from torchmetrics import PearsonCorrCoef
import threestudio
from threestudio.systems.base import BaseLift3DSystem
from threestudio.utils.ops import binary_cross_entropy, dot
from threestudio.utils.typing import *
from threestudio.utils.misc import get_rank, get_device, load_module_weights
from threestudio.utils.perceptual import PerceptualLoss
@threestudio.register("dreamcraft3d-system")
class ImageConditionDreamFusion(BaseLift3DSystem):
@dataclass
class Config(BaseLift3DSystem.Config):
# in ['coarse', 'geometry', 'texture'].
# Note that in the paper we consolidate 'coarse' and 'geometry' into a single phase called 'geometry-sculpting'.
stage: str = "coarse"
freq: dict = field(default_factory=dict)
guidance_3d_type: str = ""
guidance_3d: dict = field(default_factory=dict)
use_mixed_camera_config: bool = False
control_guidance_type: str = ""
control_guidance: dict = field(default_factory=dict)
control_prompt_processor_type: str = ""
control_prompt_processor: dict = field(default_factory=dict)
visualize_samples: bool = False
cfg: Config
def configure(self):
# create geometry, material, background, renderer
super().configure()
self.guidance = threestudio.find(self.cfg.guidance_type)(self.cfg.guidance)
if self.cfg.guidance_3d_type != "":
self.guidance_3d = threestudio.find(self.cfg.guidance_3d_type)(
self.cfg.guidance_3d
)
else:
self.guidance_3d = None
self.prompt_processor = threestudio.find(self.cfg.prompt_processor_type)(
self.cfg.prompt_processor
)
self.prompt_utils = self.prompt_processor()
p_config = {}
self.perceptual_loss = threestudio.find("perceptual-loss")(p_config)
if not (self.cfg.control_guidance_type == ""):
self.control_guidance = threestudio.find(self.cfg.control_guidance_type)(self.cfg.control_guidance)
self.control_prompt_processor = threestudio.find(self.cfg.control_prompt_processor_type)(
self.cfg.control_prompt_processor
)
self.control_prompt_utils = self.control_prompt_processor()
def forward(self, batch: Dict[str, Any]) -> Dict[str, Any]:
if self.cfg.stage == "texture":
render_out = self.renderer(**batch, render_mask=True)
else:
render_out = self.renderer(**batch)
return {
**render_out,
}
def on_fit_start(self) -> None:
super().on_fit_start()
# visualize all training images
all_images = self.trainer.datamodule.train_dataloader().dataset.get_all_images()
self.save_image_grid(
"all_training_images.png",
[
{"type": "rgb", "img": image, "kwargs": {"data_format": "HWC"}}
for image in all_images
],
name="on_fit_start",
step=self.true_global_step,
)
self.pearson = PearsonCorrCoef().to(self.device)
def training_substep(self, batch, batch_idx, guidance: str, render_type="rgb"):
"""
Args:
guidance: one of "ref" (reference image supervision), "guidance"
"""
gt_mask = batch["mask"]
gt_rgb = batch["rgb"]
gt_depth = batch["ref_depth"]
gt_normal = batch["ref_normal"]
mvp_mtx_ref = batch["mvp_mtx"]
c2w_ref = batch["c2w4x4"]
if guidance == "guidance":
batch = batch["random_camera"]
# Support rendering visibility mask
batch["mvp_mtx_ref"] = mvp_mtx_ref
batch["c2w_ref"] = c2w_ref
out = self(batch)
loss_prefix = f"loss_{guidance}_"
loss_terms = {}
def set_loss(name, value):
loss_terms[f"{loss_prefix}{name}"] = value
guidance_eval = (
guidance == "guidance"
and self.cfg.freq.guidance_eval > 0
and self.true_global_step % self.cfg.freq.guidance_eval == 0
)
prompt_utils = self.prompt_processor()
if guidance == "ref":
if render_type == "rgb":
# color loss. Use l2 loss in coarse and geometry satge; use l1 loss in texture stage.
if self.C(self.cfg.loss.lambda_rgb) > 0:
gt_rgb = gt_rgb * gt_mask.float() + out["comp_rgb_bg"] * (
1 - gt_mask.float()
)
pred_rgb = out["comp_rgb"]
if self.cfg.stage in ["coarse", "geometry"]:
set_loss("rgb", F.mse_loss(gt_rgb, pred_rgb))
else:
if self.cfg.stage == "texture":
grow_mask = F.max_pool2d(1 - gt_mask.float().permute(0, 3, 1, 2), (9, 9), 1, 4)
grow_mask = (1 - grow_mask).permute(0, 2, 3, 1)
set_loss("rgb", F.l1_loss(gt_rgb*grow_mask, pred_rgb*grow_mask))
else:
set_loss("rgb", F.l1_loss(gt_rgb, pred_rgb))
# mask loss
if self.C(self.cfg.loss.lambda_mask) > 0:
set_loss("mask", F.mse_loss(gt_mask.float(), out["opacity"]))
# mask binary cross loss
if self.C(self.cfg.loss.lambda_mask_binary) > 0:
set_loss("mask_binary", F.binary_cross_entropy(
out["opacity"].clamp(1.0e-5, 1.0 - 1.0e-5),
batch["mask"].float(),))
# depth loss
if self.C(self.cfg.loss.lambda_depth) > 0:
valid_gt_depth = batch["ref_depth"][gt_mask.squeeze(-1)].unsqueeze(1)
valid_pred_depth = out["depth"][gt_mask].unsqueeze(1)
with torch.no_grad():
A = torch.cat(
[valid_gt_depth, torch.ones_like(valid_gt_depth)], dim=-1
) # [B, 2]
X = torch.linalg.lstsq(A, valid_pred_depth).solution # [2, 1]
valid_gt_depth = A @ X # [B, 1]
set_loss("depth", F.mse_loss(valid_gt_depth, valid_pred_depth))
# relative depth loss
if self.C(self.cfg.loss.lambda_depth_rel) > 0:
valid_gt_depth = batch["ref_depth"][gt_mask.squeeze(-1)] # [B,]
valid_pred_depth = out["depth"][gt_mask] # [B,]
set_loss(
"depth_rel", 1 - self.pearson(valid_pred_depth, valid_gt_depth)
)
# normal loss
if self.C(self.cfg.loss.lambda_normal) > 0:
valid_gt_normal = (
1 - 2 * gt_normal[gt_mask.squeeze(-1)]
) # [B, 3]
# FIXME: reverse x axis
pred_normal = out["comp_normal_viewspace"]
pred_normal[..., 0] = 1 - pred_normal[..., 0]
valid_pred_normal = (
2 * pred_normal[gt_mask.squeeze(-1)] - 1
) # [B, 3]
set_loss(
"normal",
1 - F.cosine_similarity(valid_pred_normal, valid_gt_normal).mean(),
)
elif guidance == "guidance" and self.true_global_step > self.cfg.freq.no_diff_steps:
if self.cfg.stage == "geometry" and render_type == "normal":
guidance_inp = out["comp_normal"]
else:
guidance_inp = out["comp_rgb"]
guidance_out = self.guidance(
guidance_inp,
prompt_utils,
**batch,
rgb_as_latents=False,
guidance_eval=guidance_eval,
mask=out["mask"] if "mask" in out else None,
)
for name, value in guidance_out.items():
self.log(f"train/{name}", value)
if name.startswith("loss_"):
set_loss(name.split("_")[-1], value)
if self.guidance_3d is not None:
# FIXME: use mixed camera config
if not self.cfg.use_mixed_camera_config or get_rank() % 2 == 0:
guidance_3d_out = self.guidance_3d(
out["comp_rgb"],
**batch,
rgb_as_latents=False,
guidance_eval=guidance_eval,
)
for name, value in guidance_3d_out.items():
if not (isinstance(value, torch.Tensor) and len(value.shape) > 0):
self.log(f"train/{name}_3d", value)
if name.startswith("loss_"):
set_loss("3d_"+name.split("_")[-1], value)
# set_loss("3d_sd", guidance_out["loss_sd"])
# Regularization
if self.C(self.cfg.loss.lambda_normal_smooth) > 0:
if "comp_normal" not in out:
raise ValueError(
"comp_normal is required for 2D normal smooth loss, no comp_normal is found in the output."
)
normal = out["comp_normal"]
set_loss(
"normal_smooth",
(normal[:, 1:, :, :] - normal[:, :-1, :, :]).square().mean()
+ (normal[:, :, 1:, :] - normal[:, :, :-1, :]).square().mean(),
)
if self.C(self.cfg.loss.lambda_3d_normal_smooth) > 0:
if "normal" not in out:
raise ValueError(
"Normal is required for normal smooth loss, no normal is found in the output."
)
if "normal_perturb" not in out:
raise ValueError(
"normal_perturb is required for normal smooth loss, no normal_perturb is found in the output."
)
normals = out["normal"]
normals_perturb = out["normal_perturb"]
set_loss("3d_normal_smooth", (normals - normals_perturb).abs().mean())
if self.cfg.stage == "coarse":
if self.C(self.cfg.loss.lambda_orient) > 0:
if "normal" not in out:
raise ValueError(
"Normal is required for orientation loss, no normal is found in the output."
)
set_loss(
"orient",
(
out["weights"].detach()
* dot(out["normal"], out["t_dirs"]).clamp_min(0.0) ** 2
).sum()
/ (out["opacity"] > 0).sum(),
)
if guidance != "ref" and self.C(self.cfg.loss.lambda_sparsity) > 0:
set_loss("sparsity", (out["opacity"] ** 2 + 0.01).sqrt().mean())
if self.C(self.cfg.loss.lambda_opaque) > 0:
opacity_clamped = out["opacity"].clamp(1.0e-3, 1.0 - 1.0e-3)
set_loss(
"opaque", binary_cross_entropy(opacity_clamped, opacity_clamped)
)
if "lambda_eikonal" in self.cfg.loss and self.C(self.cfg.loss.lambda_eikonal) > 0:
if "sdf_grad" not in out:
raise ValueError(
"SDF grad is required for eikonal loss, no normal is found in the output."
)
set_loss(
"eikonal", (
(torch.linalg.norm(out["sdf_grad"], ord=2, dim=-1) - 1.0) ** 2
).mean()
)
if "lambda_z_variance"in self.cfg.loss and self.C(self.cfg.loss.lambda_z_variance) > 0:
# z variance loss proposed in HiFA: http://arxiv.org/abs/2305.18766
# helps reduce floaters and produce solid geometry
loss_z_variance = out["z_variance"][out["opacity"] > 0.5].mean()
set_loss("z_variance", loss_z_variance)
elif self.cfg.stage == "geometry":
if self.C(self.cfg.loss.lambda_normal_consistency) > 0:
set_loss("normal_consistency", out["mesh"].normal_consistency())
if self.C(self.cfg.loss.lambda_laplacian_smoothness) > 0:
set_loss("laplacian_smoothness", out["mesh"].laplacian())
elif self.cfg.stage == "texture":
if self.C(self.cfg.loss.lambda_reg) > 0 and guidance == "guidance" and self.true_global_step % 5 == 0:
rgb = out["comp_rgb"]
rgb = F.interpolate(rgb.permute(0, 3, 1, 2), (512, 512), mode='bilinear').permute(0, 2, 3, 1)
control_prompt_utils = self.control_prompt_processor()
with torch.no_grad():
control_dict = self.control_guidance(
rgb=rgb,
cond_rgb=rgb,
prompt_utils=control_prompt_utils,
mask=out["mask"] if "mask" in out else None,
)
edit_images = control_dict["edit_images"]
temp = (edit_images.detach().cpu()[0].numpy() * 255).astype(np.uint8)
cv2.imwrite(".threestudio_cache/control_debug.jpg", temp[:, :, ::-1])
loss_reg = (rgb.shape[1] // 8) * (rgb.shape[2] // 8) * self.perceptual_loss(edit_images.permute(0, 3, 1, 2), rgb.permute(0, 3, 1, 2)).mean()
set_loss("reg", loss_reg)
else:
raise ValueError(f"Unknown stage {self.cfg.stage}")
loss = 0.0
for name, value in loss_terms.items():
self.log(f"train/{name}", value)
if name.startswith(loss_prefix):
loss_weighted = value * self.C(
self.cfg.loss[name.replace(loss_prefix, "lambda_")]
)
self.log(f"train/{name}_w", loss_weighted)
loss += loss_weighted
for name, value in self.cfg.loss.items():
self.log(f"train_params/{name}", self.C(value))
self.log(f"train/loss_{guidance}", loss)
if guidance_eval:
self.guidance_evaluation_save(
out["comp_rgb"].detach()[: guidance_out["eval"]["bs"]],
guidance_out["eval"],
)
return {"loss": loss}
def training_step(self, batch, batch_idx):
if self.cfg.freq.ref_or_guidance == "accumulate":
do_ref = True
do_guidance = True
elif self.cfg.freq.ref_or_guidance == "alternate":
do_ref = (
self.true_global_step < self.cfg.freq.ref_only_steps
or self.true_global_step % self.cfg.freq.n_ref == 0
)
do_guidance = not do_ref
if hasattr(self.guidance.cfg, "only_pretrain_step"):
if (self.guidance.cfg.only_pretrain_step > 0) and (self.global_step % self.guidance.cfg.only_pretrain_step) < (self.guidance.cfg.only_pretrain_step // 5):
do_guidance = True
do_ref = False
if self.cfg.stage == "geometry":
render_type = "rgb" if self.true_global_step % self.cfg.freq.n_rgb == 0 else "normal"
else:
render_type = "rgb"
total_loss = 0.0
if do_guidance:
out = self.training_substep(batch, batch_idx, guidance="guidance", render_type=render_type)
total_loss += out["loss"]
if do_ref:
out = self.training_substep(batch, batch_idx, guidance="ref", render_type=render_type)
total_loss += out["loss"]
self.log("train/loss", total_loss, prog_bar=True)
# sch = self.lr_schedulers()
# sch.step()
return {"loss": total_loss}
def validation_step(self, batch, batch_idx):
out = self(batch)
self.save_image_grid(
f"it{self.true_global_step}-val/{batch['index'][0]}.png",
(
[
{
"type": "rgb",
"img": batch["rgb"][0],
"kwargs": {"data_format": "HWC"},
}
]
if "rgb" in batch
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_rgb"][0],
"kwargs": {"data_format": "HWC"},
},
]
if "comp_rgb" in out
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_normal"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal" in out
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_normal_viewspace"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal_viewspace" in out
else []
)
+ (
[
{
"type": "grayscale",
"img": out["depth"][0],
"kwargs": {}
}
]
if "depth" in out
else []
)
+ [
{
"type": "grayscale",
"img": out["opacity"][0, :, :, 0],
"kwargs": {"cmap": None, "data_range": (0, 1)},
},
],
name="validation_step",
step=self.true_global_step,
)
if self.cfg.stage=="texture" and self.cfg.visualize_samples:
self.save_image_grid(
f"it{self.true_global_step}-{batch['index'][0]}-sample.png",
[
{
"type": "rgb",
"img": self.guidance.sample(
self.prompt_utils, **batch, seed=self.global_step
)[0],
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": self.guidance.sample_lora(self.prompt_utils, **batch)[0],
"kwargs": {"data_format": "HWC"},
},
],
name="validation_step_samples",
step=self.true_global_step,
)
def on_validation_epoch_end(self):
filestem = f"it{self.true_global_step}-val"
try:
self.save_img_sequence(
filestem,
filestem,
"(\d+)\.png",
save_format="mp4",
fps=30,
name="validation_epoch_end",
step=self.true_global_step,
)
shutil.rmtree(
os.path.join(self.get_save_dir(), f"it{self.true_global_step}-val")
)
except:
pass
def test_step(self, batch, batch_idx):
out = self(batch)
self.save_image_grid(
f"it{self.true_global_step}-test/{batch['index'][0]}.png",
(
[
{
"type": "rgb",
"img": batch["rgb"][0],
"kwargs": {"data_format": "HWC"},
}
]
if "rgb" in batch
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_rgb"][0],
"kwargs": {"data_format": "HWC"},
},
]
if "comp_rgb" in out
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_normal"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal" in out
else []
)
+ (
[
{
"type": "rgb",
"img": out["comp_normal_viewspace"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal_viewspace" in out
else []
)
+ (
[
{
"type": "grayscale", "img": out["depth"][0], "kwargs": {}
}
]
if "depth" in out
else []
)
+ [
{
"type": "grayscale",
"img": out["opacity"][0, :, :, 0],
"kwargs": {"cmap": None, "data_range": (0, 1)},
},
]
+ (
[
{
"type": "grayscale", "img": out["opacity_vis"][0, :, :, 0],
"kwargs": {"cmap": None, "data_range": (0, 1)}
}
]
if "opacity_vis" in out
else []
)
,
name="test_step",
step=self.true_global_step,
)
# FIXME: save camera extrinsics
c2w = batch["c2w"]
save_path = os.path.join(self.get_save_dir(), f"it{self.true_global_step}-test/{batch['index'][0]}.npy")
np.save(save_path, c2w.detach().cpu().numpy()[0])
def on_test_epoch_end(self):
self.save_img_sequence(
f"it{self.true_global_step}-test",
f"it{self.true_global_step}-test",
"(\d+)\.png",
save_format="mp4",
fps=30,
name="test",
step=self.true_global_step,
)
def on_before_optimizer_step(self, optimizer) -> None:
# print("on_before_opt enter")
# for n, p in self.geometry.named_parameters():
# if p.grad is None:
# print(n)
# print("on_before_opt exit")
pass
def on_load_checkpoint(self, checkpoint):
for k in list(checkpoint['state_dict'].keys()):
if k.startswith("guidance."):
return
guidance_state_dict = {"guidance."+k : v for (k,v) in self.guidance.state_dict().items()}
checkpoint['state_dict'] = {**checkpoint['state_dict'], **guidance_state_dict}
return
def on_save_checkpoint(self, checkpoint):
for k in list(checkpoint['state_dict'].keys()):
if k.startswith("guidance."):
checkpoint['state_dict'].pop(k)
return

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 threestudio
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()
]
threestudio.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 threestudio.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

View File

@@ -0,0 +1,390 @@
import os
import random
import shutil
from dataclasses import dataclass, field
import torch
import torch.nn.functional as F
from PIL import Image, ImageDraw
from torchmetrics import PearsonCorrCoef
import threestudio
from threestudio.systems.base import BaseLift3DSystem
from threestudio.utils.ops import binary_cross_entropy, dot
from threestudio.utils.typing import *
@threestudio.register("zero123-system")
class Zero123(BaseLift3DSystem):
@dataclass
class Config(BaseLift3DSystem.Config):
freq: dict = field(default_factory=dict)
refinement: bool = False
ambient_ratio_min: float = 0.5
cfg: Config
def configure(self):
# create geometry, material, background, renderer
super().configure()
def forward(self, batch: Dict[str, Any]) -> Dict[str, Any]:
render_out = self.renderer(**batch)
return {
**render_out,
}
def on_fit_start(self) -> None:
super().on_fit_start()
# no prompt processor
self.guidance = threestudio.find(self.cfg.guidance_type)(self.cfg.guidance)
# visualize all training images
all_images = self.trainer.datamodule.train_dataloader().dataset.get_all_images()
self.save_image_grid(
"all_training_images.png",
[
{"type": "rgb", "img": image, "kwargs": {"data_format": "HWC"}}
for image in all_images
],
name="on_fit_start",
step=self.true_global_step,
)
self.pearson = PearsonCorrCoef().to(self.device)
def training_substep(self, batch, batch_idx, guidance: str):
"""
Args:
guidance: one of "ref" (reference image supervision), "zero123"
"""
if guidance == "ref":
# bg_color = torch.rand_like(batch['rays_o'])
ambient_ratio = 1.0
shading = "diffuse"
batch["shading"] = shading
elif guidance == "zero123":
batch = batch["random_camera"]
ambient_ratio = (
self.cfg.ambient_ratio_min
+ (1 - self.cfg.ambient_ratio_min) * random.random()
)
batch["bg_color"] = None
batch["ambient_ratio"] = ambient_ratio
out = self(batch)
loss_prefix = f"loss_{guidance}_"
loss_terms = {}
def set_loss(name, value):
loss_terms[f"{loss_prefix}{name}"] = value
guidance_eval = (
guidance == "zero123"
and self.cfg.freq.guidance_eval > 0
and self.true_global_step % self.cfg.freq.guidance_eval == 0
)
if guidance == "ref":
gt_mask = batch["mask"]
gt_rgb = batch["rgb"]
# color loss
gt_rgb = gt_rgb * gt_mask.float() + out["comp_rgb_bg"] * (
1 - gt_mask.float()
)
set_loss("rgb", F.mse_loss(gt_rgb, out["comp_rgb"]))
# mask loss
set_loss("mask", F.mse_loss(gt_mask.float(), out["opacity"]))
# depth loss
if self.C(self.cfg.loss.lambda_depth) > 0:
valid_gt_depth = batch["ref_depth"][gt_mask.squeeze(-1)].unsqueeze(1)
valid_pred_depth = out["depth"][gt_mask].unsqueeze(1)
with torch.no_grad():
A = torch.cat(
[valid_gt_depth, torch.ones_like(valid_gt_depth)], dim=-1
) # [B, 2]
X = torch.linalg.lstsq(A, valid_pred_depth).solution # [2, 1]
valid_gt_depth = A @ X # [B, 1]
set_loss("depth", F.mse_loss(valid_gt_depth, valid_pred_depth))
# relative depth loss
if self.C(self.cfg.loss.lambda_depth_rel) > 0:
valid_gt_depth = batch["ref_depth"][gt_mask.squeeze(-1)] # [B,]
valid_pred_depth = out["depth"][gt_mask] # [B,]
set_loss(
"depth_rel", 1 - self.pearson(valid_pred_depth, valid_gt_depth)
)
# normal loss
if self.C(self.cfg.loss.lambda_normal) > 0:
valid_gt_normal = (
1 - 2 * batch["ref_normal"][gt_mask.squeeze(-1)]
) # [B, 3]
valid_pred_normal = (
2 * out["comp_normal"][gt_mask.squeeze(-1)] - 1
) # [B, 3]
set_loss(
"normal",
1 - F.cosine_similarity(valid_pred_normal, valid_gt_normal).mean(),
)
elif guidance == "zero123":
# zero123
guidance_out = self.guidance(
out["comp_rgb"],
**batch,
rgb_as_latents=False,
guidance_eval=guidance_eval,
)
# claforte: TODO: rename the loss_terms keys
set_loss("sds", guidance_out["loss_sds"])
if self.C(self.cfg.loss.lambda_normal_smooth) > 0:
if "comp_normal" not in out:
raise ValueError(
"comp_normal is required for 2D normal smooth loss, no comp_normal is found in the output."
)
normal = out["comp_normal"]
set_loss(
"normal_smooth",
(normal[:, 1:, :, :] - normal[:, :-1, :, :]).square().mean()
+ (normal[:, :, 1:, :] - normal[:, :, :-1, :]).square().mean(),
)
if self.C(self.cfg.loss.lambda_3d_normal_smooth) > 0:
if "normal" not in out:
raise ValueError(
"Normal is required for normal smooth loss, no normal is found in the output."
)
if "normal_perturb" not in out:
raise ValueError(
"normal_perturb is required for normal smooth loss, no normal_perturb is found in the output."
)
normals = out["normal"]
normals_perturb = out["normal_perturb"]
set_loss("3d_normal_smooth", (normals - normals_perturb).abs().mean())
if not self.cfg.refinement:
if self.C(self.cfg.loss.lambda_orient) > 0:
if "normal" not in out:
raise ValueError(
"Normal is required for orientation loss, no normal is found in the output."
)
set_loss(
"orient",
(
out["weights"].detach()
* dot(out["normal"], out["t_dirs"]).clamp_min(0.0) ** 2
).sum()
/ (out["opacity"] > 0).sum(),
)
if guidance != "ref" and self.C(self.cfg.loss.lambda_sparsity) > 0:
set_loss("sparsity", (out["opacity"] ** 2 + 0.01).sqrt().mean())
if self.C(self.cfg.loss.lambda_opaque) > 0:
opacity_clamped = out["opacity"].clamp(1.0e-3, 1.0 - 1.0e-3)
set_loss(
"opaque", binary_cross_entropy(opacity_clamped, opacity_clamped)
)
else:
if self.C(self.cfg.loss.lambda_normal_consistency) > 0:
set_loss("normal_consistency", out["mesh"].normal_consistency())
if self.C(self.cfg.loss.lambda_laplacian_smoothness) > 0:
set_loss("laplacian_smoothness", out["mesh"].laplacian())
loss = 0.0
for name, value in loss_terms.items():
self.log(f"train/{name}", value)
if name.startswith(loss_prefix):
loss_weighted = value * self.C(
self.cfg.loss[name.replace(loss_prefix, "lambda_")]
)
self.log(f"train/{name}_w", loss_weighted)
loss += loss_weighted
for name, value in self.cfg.loss.items():
self.log(f"train_params/{name}", self.C(value))
self.log(f"train/loss_{guidance}", loss)
if guidance_eval:
self.guidance_evaluation_save(
out["comp_rgb"].detach()[: guidance_out["eval"]["bs"]],
guidance_out["eval"],
)
return {"loss": loss}
def training_step(self, batch, batch_idx):
if self.cfg.freq.get("ref_or_zero123", "accumulate") == "accumulate":
do_ref = True
do_zero123 = True
elif self.cfg.freq.get("ref_or_zero123", "accumulate") == "alternate":
do_ref = (
self.true_global_step < self.cfg.freq.ref_only_steps
or self.true_global_step % self.cfg.freq.n_ref == 0
)
do_zero123 = not do_ref
total_loss = 0.0
if do_zero123:
out = self.training_substep(batch, batch_idx, guidance="zero123")
total_loss += out["loss"]
if do_ref:
out = self.training_substep(batch, batch_idx, guidance="ref")
total_loss += out["loss"]
self.log("train/loss", total_loss, prog_bar=True)
# sch = self.lr_schedulers()
# sch.step()
return {"loss": total_loss}
def validation_step(self, batch, batch_idx):
out = self(batch)
self.save_image_grid(
f"it{self.true_global_step}-val/{batch['index'][0]}.png",
(
[
{
"type": "rgb",
"img": batch["rgb"][0],
"kwargs": {"data_format": "HWC"},
}
]
if "rgb" in batch
else []
)
+ [
{
"type": "rgb",
"img": out["comp_rgb"][0],
"kwargs": {"data_format": "HWC"},
},
]
+ (
[
{
"type": "rgb",
"img": out["comp_normal"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal" in out
else []
)
+ (
[
{
"type": "grayscale",
"img": out["depth"][0],
"kwargs": {},
}
]
if "depth" in out
else []
)
+ [
{
"type": "grayscale",
"img": out["opacity"][0, :, :, 0],
"kwargs": {"cmap": None, "data_range": (0, 1)},
},
],
# claforte: TODO: don't hardcode the frame numbers to record... read them from cfg instead.
name=f"validation_step_batchidx_{batch_idx}"
if batch_idx in [0, 7, 15, 23, 29]
else None,
step=self.true_global_step,
)
def on_validation_epoch_end(self):
filestem = f"it{self.true_global_step}-val"
self.save_img_sequence(
filestem,
filestem,
"(\d+)\.png",
save_format="mp4",
fps=30,
name="validation_epoch_end",
step=self.true_global_step,
)
shutil.rmtree(
os.path.join(self.get_save_dir(), f"it{self.true_global_step}-val")
)
def test_step(self, batch, batch_idx):
out = self(batch)
self.save_image_grid(
f"it{self.true_global_step}-test/{batch['index'][0]}.png",
(
[
{
"type": "rgb",
"img": batch["rgb"][0],
"kwargs": {"data_format": "HWC"},
}
]
if "rgb" in batch
else []
)
+ [
{
"type": "rgb",
"img": out["comp_rgb"][0],
"kwargs": {"data_format": "HWC"},
},
]
+ (
[
{
"type": "rgb",
"img": out["comp_normal"][0],
"kwargs": {"data_format": "HWC", "data_range": (0, 1)},
}
]
if "comp_normal" in out
else []
)
+ (
[
{
"type": "grayscale",
"img": out["depth"][0],
"kwargs": {},
}
]
if "depth" in out
else []
)
+ [
{
"type": "grayscale",
"img": out["opacity"][0, :, :, 0],
"kwargs": {"cmap": None, "data_range": (0, 1)},
},
],
name="test_step",
step=self.true_global_step,
)
def on_test_epoch_end(self):
self.save_img_sequence(
f"it{self.true_global_step}-test",
f"it{self.true_global_step}-test",
"(\d+)\.png",
save_format="mp4",
fps=30,
name="test",
step=self.true_global_step,
)
shutil.rmtree(
os.path.join(self.get_save_dir(), f"it{self.true_global_step}-test")
)

View File

@@ -0,0 +1,278 @@
import math
from inspect import isfunction
import torch
import torch.nn.functional as F
from einops import rearrange, repeat
from torch import einsum, nn
from threestudio.utils.GAN.network_util import checkpoint
def exists(val):
return val is not None
def uniq(arr):
return {el: True for el in arr}.keys()
def default(val, d):
if exists(val):
return val
return d() if isfunction(d) else d
def max_neg_value(t):
return -torch.finfo(t.dtype).max
def init_(tensor):
dim = tensor.shape[-1]
std = 1 / math.sqrt(dim)
tensor.uniform_(-std, std)
return tensor
# feedforward
class GEGLU(nn.Module):
def __init__(self, dim_in, dim_out):
super().__init__()
self.proj = nn.Linear(dim_in, dim_out * 2)
def forward(self, x):
x, gate = self.proj(x).chunk(2, dim=-1)
return x * F.gelu(gate)
class FeedForward(nn.Module):
def __init__(self, dim, dim_out=None, mult=4, glu=False, dropout=0.0):
super().__init__()
inner_dim = int(dim * mult)
dim_out = default(dim_out, dim)
project_in = (
nn.Sequential(nn.Linear(dim, inner_dim), nn.GELU())
if not glu
else GEGLU(dim, inner_dim)
)
self.net = nn.Sequential(
project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out)
)
def forward(self, x):
return self.net(x)
def zero_module(module):
"""
Zero out the parameters of a module and return it.
"""
for p in module.parameters():
p.detach().zero_()
return module
def Normalize(in_channels):
return torch.nn.GroupNorm(
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
)
class LinearAttention(nn.Module):
def __init__(self, dim, heads=4, dim_head=32):
super().__init__()
self.heads = heads
hidden_dim = dim_head * heads
self.to_qkv = nn.Conv2d(dim, hidden_dim * 3, 1, bias=False)
self.to_out = nn.Conv2d(hidden_dim, dim, 1)
def forward(self, x):
b, c, h, w = x.shape
qkv = self.to_qkv(x)
q, k, v = rearrange(
qkv, "b (qkv heads c) h w -> qkv b heads c (h w)", heads=self.heads, qkv=3
)
k = k.softmax(dim=-1)
context = torch.einsum("bhdn,bhen->bhde", k, v)
out = torch.einsum("bhde,bhdn->bhen", context, q)
out = rearrange(
out, "b heads c (h w) -> b (heads c) h w", heads=self.heads, h=h, w=w
)
return self.to_out(out)
class SpatialSelfAttention(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.in_channels = in_channels
self.norm = Normalize(in_channels)
self.q = torch.nn.Conv2d(
in_channels, in_channels, kernel_size=1, stride=1, padding=0
)
self.k = torch.nn.Conv2d(
in_channels, in_channels, kernel_size=1, stride=1, padding=0
)
self.v = torch.nn.Conv2d(
in_channels, in_channels, kernel_size=1, stride=1, padding=0
)
self.proj_out = torch.nn.Conv2d(
in_channels, in_channels, kernel_size=1, stride=1, padding=0
)
def forward(self, x):
h_ = x
h_ = self.norm(h_)
q = self.q(h_)
k = self.k(h_)
v = self.v(h_)
# compute attention
b, c, h, w = q.shape
q = rearrange(q, "b c h w -> b (h w) c")
k = rearrange(k, "b c h w -> b c (h w)")
w_ = torch.einsum("bij,bjk->bik", q, k)
w_ = w_ * (int(c) ** (-0.5))
w_ = torch.nn.functional.softmax(w_, dim=2)
# attend to values
v = rearrange(v, "b c h w -> b c (h w)")
w_ = rearrange(w_, "b i j -> b j i")
h_ = torch.einsum("bij,bjk->bik", v, w_)
h_ = rearrange(h_, "b c (h w) -> b c h w", h=h)
h_ = self.proj_out(h_)
return x + h_
class CrossAttention(nn.Module):
def __init__(self, query_dim, context_dim=None, heads=8, dim_head=64, dropout=0.0):
super().__init__()
inner_dim = dim_head * heads
context_dim = default(context_dim, query_dim)
self.scale = dim_head**-0.5
self.heads = heads
self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
self.to_v = nn.Linear(context_dim, inner_dim, bias=False)
self.to_out = nn.Sequential(
nn.Linear(inner_dim, query_dim), nn.Dropout(dropout)
)
def forward(self, x, context=None, mask=None):
h = self.heads
q = self.to_q(x)
context = default(context, x)
k = self.to_k(context)
v = self.to_v(context)
q, k, v = map(lambda t: rearrange(t, "b n (h d) -> (b h) n d", h=h), (q, k, v))
sim = einsum("b i d, b j d -> b i j", q, k) * self.scale
if exists(mask):
mask = rearrange(mask, "b ... -> b (...)")
max_neg_value = -torch.finfo(sim.dtype).max
mask = repeat(mask, "b j -> (b h) () j", h=h)
sim.masked_fill_(~mask, max_neg_value)
# attention, what we cannot get enough of
attn = sim.softmax(dim=-1)
out = einsum("b i j, b j d -> b i d", attn, v)
out = rearrange(out, "(b h) n d -> b n (h d)", h=h)
return self.to_out(out)
class BasicTransformerBlock(nn.Module):
def __init__(
self,
dim,
n_heads,
d_head,
dropout=0.0,
context_dim=None,
gated_ff=True,
checkpoint=True,
):
super().__init__()
self.attn1 = CrossAttention(
query_dim=dim, heads=n_heads, dim_head=d_head, dropout=dropout
) # is a self-attention
self.ff = FeedForward(dim, dropout=dropout, glu=gated_ff)
self.attn2 = CrossAttention(
query_dim=dim,
context_dim=context_dim,
heads=n_heads,
dim_head=d_head,
dropout=dropout,
) # is self-attn if context is none
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.norm3 = nn.LayerNorm(dim)
self.checkpoint = checkpoint
def forward(self, x, context=None):
return checkpoint(
self._forward, (x, context), self.parameters(), self.checkpoint
)
def _forward(self, x, context=None):
x = self.attn1(self.norm1(x)) + x
x = self.attn2(self.norm2(x), context=context) + x
x = self.ff(self.norm3(x)) + x
return x
class SpatialTransformer(nn.Module):
"""
Transformer block for image-like data.
First, project the input (aka embedding)
and reshape to b, t, d.
Then apply standard transformer action.
Finally, reshape to image
"""
def __init__(
self, in_channels, n_heads, d_head, depth=1, dropout=0.0, context_dim=None
):
super().__init__()
self.in_channels = in_channels
inner_dim = n_heads * d_head
self.norm = Normalize(in_channels)
self.proj_in = nn.Conv2d(
in_channels, inner_dim, kernel_size=1, stride=1, padding=0
)
self.transformer_blocks = nn.ModuleList(
[
BasicTransformerBlock(
inner_dim, n_heads, d_head, dropout=dropout, context_dim=context_dim
)
for d in range(depth)
]
)
self.proj_out = zero_module(
nn.Conv2d(inner_dim, in_channels, kernel_size=1, stride=1, padding=0)
)
def forward(self, x, context=None):
# note: if no context is given, cross-attention defaults to self-attention
b, c, h, w = x.shape
x_in = x
x = self.norm(x)
x = self.proj_in(x)
x = rearrange(x, "b c h w -> b (h w) c")
for block in self.transformer_blocks:
x = block(x, context=context)
x = rearrange(x, "b (h w) c -> b c h w", h=h, w=w)
x = self.proj_out(x)
return x + x_in

View File

@@ -0,0 +1,217 @@
import functools
import torch
import torch.nn as nn
def count_params(model):
total_params = sum(p.numel() for p in model.parameters())
return total_params
class ActNorm(nn.Module):
def __init__(
self, num_features, logdet=False, affine=True, allow_reverse_init=False
):
assert affine
super().__init__()
self.logdet = logdet
self.loc = nn.Parameter(torch.zeros(1, num_features, 1, 1))
self.scale = nn.Parameter(torch.ones(1, num_features, 1, 1))
self.allow_reverse_init = allow_reverse_init
self.register_buffer("initialized", torch.tensor(0, dtype=torch.uint8))
def initialize(self, input):
with torch.no_grad():
flatten = input.permute(1, 0, 2, 3).contiguous().view(input.shape[1], -1)
mean = (
flatten.mean(1)
.unsqueeze(1)
.unsqueeze(2)
.unsqueeze(3)
.permute(1, 0, 2, 3)
)
std = (
flatten.std(1)
.unsqueeze(1)
.unsqueeze(2)
.unsqueeze(3)
.permute(1, 0, 2, 3)
)
self.loc.data.copy_(-mean)
self.scale.data.copy_(1 / (std + 1e-6))
def forward(self, input, reverse=False):
if reverse:
return self.reverse(input)
if len(input.shape) == 2:
input = input[:, :, None, None]
squeeze = True
else:
squeeze = False
_, _, height, width = input.shape
if self.training and self.initialized.item() == 0:
self.initialize(input)
self.initialized.fill_(1)
h = self.scale * (input + self.loc)
if squeeze:
h = h.squeeze(-1).squeeze(-1)
if self.logdet:
log_abs = torch.log(torch.abs(self.scale))
logdet = height * width * torch.sum(log_abs)
logdet = logdet * torch.ones(input.shape[0]).to(input)
return h, logdet
return h
def reverse(self, output):
if self.training and self.initialized.item() == 0:
if not self.allow_reverse_init:
raise RuntimeError(
"Initializing ActNorm in reverse direction is "
"disabled by default. Use allow_reverse_init=True to enable."
)
else:
self.initialize(output)
self.initialized.fill_(1)
if len(output.shape) == 2:
output = output[:, :, None, None]
squeeze = True
else:
squeeze = False
h = output / self.scale - self.loc
if squeeze:
h = h.squeeze(-1).squeeze(-1)
return h
class AbstractEncoder(nn.Module):
def __init__(self):
super().__init__()
def encode(self, *args, **kwargs):
raise NotImplementedError
class Labelator(AbstractEncoder):
"""Net2Net Interface for Class-Conditional Model"""
def __init__(self, n_classes, quantize_interface=True):
super().__init__()
self.n_classes = n_classes
self.quantize_interface = quantize_interface
def encode(self, c):
c = c[:, None]
if self.quantize_interface:
return c, None, [None, None, c.long()]
return c
class SOSProvider(AbstractEncoder):
# for unconditional training
def __init__(self, sos_token, quantize_interface=True):
super().__init__()
self.sos_token = sos_token
self.quantize_interface = quantize_interface
def encode(self, x):
# get batch size from data and replicate sos_token
c = torch.ones(x.shape[0], 1) * self.sos_token
c = c.long().to(x.device)
if self.quantize_interface:
return c, None, [None, None, c]
return c
def weights_init(m):
classname = m.__class__.__name__
if classname.find("Conv") != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find("BatchNorm") != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
class NLayerDiscriminator(nn.Module):
"""Defines a PatchGAN discriminator as in Pix2Pix
--> see https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/master/models/networks.py
"""
def __init__(self, input_nc=3, ndf=64, n_layers=3, use_actnorm=False):
"""Construct a PatchGAN discriminator
Parameters:
input_nc (int) -- the number of channels in input images
ndf (int) -- the number of filters in the last conv layer
n_layers (int) -- the number of conv layers in the discriminator
norm_layer -- normalization layer
"""
super(NLayerDiscriminator, self).__init__()
if not use_actnorm:
norm_layer = nn.BatchNorm2d
else:
norm_layer = ActNorm
if (
type(norm_layer) == functools.partial
): # no need to use bias as BatchNorm2d has affine parameters
use_bias = norm_layer.func != nn.BatchNorm2d
else:
use_bias = norm_layer != nn.BatchNorm2d
kw = 4
padw = 1
sequence = [
nn.Conv2d(input_nc, ndf, kernel_size=kw, stride=2, padding=padw),
nn.LeakyReLU(0.2, True),
]
nf_mult = 1
nf_mult_prev = 1
for n in range(1, n_layers): # gradually increase the number of filters
nf_mult_prev = nf_mult
nf_mult = min(2**n, 8)
sequence += [
nn.Conv2d(
ndf * nf_mult_prev,
ndf * nf_mult,
kernel_size=kw,
stride=2,
padding=padw,
bias=use_bias,
),
norm_layer(ndf * nf_mult),
nn.LeakyReLU(0.2, True),
]
nf_mult_prev = nf_mult
nf_mult = min(2**n_layers, 8)
sequence += [
nn.Conv2d(
ndf * nf_mult_prev,
ndf * nf_mult,
kernel_size=kw,
stride=1,
padding=padw,
bias=use_bias,
),
norm_layer(ndf * nf_mult),
nn.LeakyReLU(0.2, True),
]
sequence += [
nn.Conv2d(ndf * nf_mult, 1, kernel_size=kw, stride=1, padding=padw)
] # output 1 channel prediction map
self.main = nn.Sequential(*sequence)
def forward(self, input):
"""Standard forward."""
return self.main(input)

View File

@@ -0,0 +1,102 @@
import numpy as np
import torch
class AbstractDistribution:
def sample(self):
raise NotImplementedError()
def mode(self):
raise NotImplementedError()
class DiracDistribution(AbstractDistribution):
def __init__(self, value):
self.value = value
def sample(self):
return self.value
def mode(self):
return self.value
class DiagonalGaussianDistribution(object):
def __init__(self, parameters, deterministic=False):
self.parameters = parameters
self.mean, self.logvar = torch.chunk(parameters, 2, dim=1)
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).to(
device=self.parameters.device
)
def sample(self):
x = self.mean + self.std * torch.randn(self.mean.shape).to(
device=self.parameters.device
)
return x
def kl(self, other=None):
if self.deterministic:
return torch.Tensor([0.0])
else:
if other is None:
return 0.5 * torch.sum(
torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
dim=[1, 2, 3],
)
else:
return 0.5 * torch.sum(
torch.pow(self.mean - other.mean, 2) / other.var
+ self.var / other.var
- 1.0
- self.logvar
+ other.logvar,
dim=[1, 2, 3],
)
def nll(self, sample, dims=[1, 2, 3]):
if self.deterministic:
return torch.Tensor([0.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
def normal_kl(mean1, logvar1, mean2, logvar2):
"""
source: https://github.com/openai/guided-diffusion/blob/27c20a8fab9cb472df5d6bdd6c8d11c8f430b924/guided_diffusion/losses.py#L12
Compute the KL divergence between two gaussians.
Shapes are automatically broadcasted, so batches can be compared to
scalars, among other use cases.
"""
tensor = None
for obj in (mean1, logvar1, mean2, logvar2):
if isinstance(obj, torch.Tensor):
tensor = obj
break
assert tensor is not None, "at least one argument must be a Tensor"
# Force variances to be Tensors. Broadcasting helps convert scalars to
# Tensors, but it does not work for torch.exp().
logvar1, logvar2 = [
x if isinstance(x, torch.Tensor) else torch.tensor(x).to(tensor)
for x in (logvar1, logvar2)
]
return 0.5 * (
-1.0
+ logvar2
- logvar1
+ torch.exp(logvar1 - logvar2)
+ ((mean1 - mean2) ** 2) * torch.exp(-logvar2)
)

View File

@@ -0,0 +1,35 @@
import torch
import torch.nn.functional as F
def generator_loss(discriminator, inputs, reconstructions, cond=None):
if cond is None:
logits_fake = discriminator(reconstructions.contiguous())
else:
logits_fake = discriminator(
torch.cat((reconstructions.contiguous(), cond), dim=1)
)
g_loss = -torch.mean(logits_fake)
return g_loss
def hinge_d_loss(logits_real, logits_fake):
loss_real = torch.mean(F.relu(1.0 - logits_real))
loss_fake = torch.mean(F.relu(1.0 + logits_fake))
d_loss = 0.5 * (loss_real + loss_fake)
return d_loss
def discriminator_loss(discriminator, inputs, reconstructions, cond=None):
if cond is None:
logits_real = discriminator(inputs.contiguous().detach())
logits_fake = discriminator(reconstructions.contiguous().detach())
else:
logits_real = discriminator(
torch.cat((inputs.contiguous().detach(), cond), dim=1)
)
logits_fake = discriminator(
torch.cat((reconstructions.contiguous().detach(), cond), dim=1)
)
d_loss = hinge_d_loss(logits_real, logits_fake).mean()
return d_loss

View File

@@ -0,0 +1,254 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
__all__ = ["MobileNetV3", "mobilenetv3"]
def conv_bn(
inp,
oup,
stride,
conv_layer=nn.Conv2d,
norm_layer=nn.BatchNorm2d,
nlin_layer=nn.ReLU,
):
return nn.Sequential(
conv_layer(inp, oup, 3, stride, 1, bias=False),
norm_layer(oup),
nlin_layer(inplace=True),
)
def conv_1x1_bn(
inp, oup, conv_layer=nn.Conv2d, norm_layer=nn.BatchNorm2d, nlin_layer=nn.ReLU
):
return nn.Sequential(
conv_layer(inp, oup, 1, 1, 0, bias=False),
norm_layer(oup),
nlin_layer(inplace=True),
)
class Hswish(nn.Module):
def __init__(self, inplace=True):
super(Hswish, self).__init__()
self.inplace = inplace
def forward(self, x):
return x * F.relu6(x + 3.0, inplace=self.inplace) / 6.0
class Hsigmoid(nn.Module):
def __init__(self, inplace=True):
super(Hsigmoid, self).__init__()
self.inplace = inplace
def forward(self, x):
return F.relu6(x + 3.0, inplace=self.inplace) / 6.0
class SEModule(nn.Module):
def __init__(self, channel, reduction=4):
super(SEModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
Hsigmoid()
# nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y.expand_as(x)
class Identity(nn.Module):
def __init__(self, channel):
super(Identity, self).__init__()
def forward(self, x):
return x
def make_divisible(x, divisible_by=8):
import numpy as np
return int(np.ceil(x * 1.0 / divisible_by) * divisible_by)
class MobileBottleneck(nn.Module):
def __init__(self, inp, oup, kernel, stride, exp, se=False, nl="RE"):
super(MobileBottleneck, self).__init__()
assert stride in [1, 2]
assert kernel in [3, 5]
padding = (kernel - 1) // 2
self.use_res_connect = stride == 1 and inp == oup
conv_layer = nn.Conv2d
norm_layer = nn.BatchNorm2d
if nl == "RE":
nlin_layer = nn.ReLU # or ReLU6
elif nl == "HS":
nlin_layer = Hswish
else:
raise NotImplementedError
if se:
SELayer = SEModule
else:
SELayer = Identity
self.conv = nn.Sequential(
# pw
conv_layer(inp, exp, 1, 1, 0, bias=False),
norm_layer(exp),
nlin_layer(inplace=True),
# dw
conv_layer(exp, exp, kernel, stride, padding, groups=exp, bias=False),
norm_layer(exp),
SELayer(exp),
nlin_layer(inplace=True),
# pw-linear
conv_layer(exp, oup, 1, 1, 0, bias=False),
norm_layer(oup),
)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV3(nn.Module):
def __init__(
self, n_class=1000, input_size=224, dropout=0.0, mode="small", width_mult=1.0
):
super(MobileNetV3, self).__init__()
input_channel = 16
last_channel = 1280
if mode == "large":
# refer to Table 1 in paper
mobile_setting = [
# k, exp, c, se, nl, s,
[3, 16, 16, False, "RE", 1],
[3, 64, 24, False, "RE", 2],
[3, 72, 24, False, "RE", 1],
[5, 72, 40, True, "RE", 2],
[5, 120, 40, True, "RE", 1],
[5, 120, 40, True, "RE", 1],
[3, 240, 80, False, "HS", 2],
[3, 200, 80, False, "HS", 1],
[3, 184, 80, False, "HS", 1],
[3, 184, 80, False, "HS", 1],
[3, 480, 112, True, "HS", 1],
[3, 672, 112, True, "HS", 1],
[5, 672, 160, True, "HS", 2],
[5, 960, 160, True, "HS", 1],
[5, 960, 160, True, "HS", 1],
]
elif mode == "small":
# refer to Table 2 in paper
mobile_setting = [
# k, exp, c, se, nl, s,
[3, 16, 16, True, "RE", 2],
[3, 72, 24, False, "RE", 2],
[3, 88, 24, False, "RE", 1],
[5, 96, 40, True, "HS", 2],
[5, 240, 40, True, "HS", 1],
[5, 240, 40, True, "HS", 1],
[5, 120, 48, True, "HS", 1],
[5, 144, 48, True, "HS", 1],
[5, 288, 96, True, "HS", 2],
[5, 576, 96, True, "HS", 1],
[5, 576, 96, True, "HS", 1],
]
else:
raise NotImplementedError
# building first layer
assert input_size % 32 == 0
last_channel = (
make_divisible(last_channel * width_mult)
if width_mult > 1.0
else last_channel
)
self.features = [conv_bn(3, input_channel, 2, nlin_layer=Hswish)]
self.classifier = []
# building mobile blocks
for k, exp, c, se, nl, s in mobile_setting:
output_channel = make_divisible(c * width_mult)
exp_channel = make_divisible(exp * width_mult)
self.features.append(
MobileBottleneck(
input_channel, output_channel, k, s, exp_channel, se, nl
)
)
input_channel = output_channel
# building last several layers
if mode == "large":
last_conv = make_divisible(960 * width_mult)
self.features.append(
conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish)
)
self.features.append(nn.AdaptiveAvgPool2d(1))
self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0))
self.features.append(Hswish(inplace=True))
elif mode == "small":
last_conv = make_divisible(576 * width_mult)
self.features.append(
conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish)
)
# self.features.append(SEModule(last_conv)) # refer to paper Table2, but I think this is a mistake
self.features.append(nn.AdaptiveAvgPool2d(1))
self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0))
self.features.append(Hswish(inplace=True))
else:
raise NotImplementedError
# make it nn.Sequential
self.features = nn.Sequential(*self.features)
# building classifier
self.classifier = nn.Sequential(
nn.Dropout(p=dropout), # refer to paper section 6
nn.Linear(last_channel, n_class),
)
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = x.mean(3).mean(2)
x = self.classifier(x)
return x
def _initialize_weights(self):
# weight initialization
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out")
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.BatchNorm2d):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None:
nn.init.zeros_(m.bias)
def mobilenetv3(pretrained=False, **kwargs):
model = MobileNetV3(**kwargs)
if pretrained:
state_dict = torch.load("mobilenetv3_small_67.4.pth.tar")
model.load_state_dict(state_dict, strict=True)
# raise NotImplementedError
return model

View File

@@ -0,0 +1,296 @@
# adopted from
# https://github.com/openai/improved-diffusion/blob/main/improved_diffusion/gaussian_diffusion.py
# and
# https://github.com/lucidrains/denoising-diffusion-pytorch/blob/7706bdfc6f527f58d33f84b7b522e61e6e3164b3/denoising_diffusion_pytorch/denoising_diffusion_pytorch.py
# and
# https://github.com/openai/guided-diffusion/blob/0ba878e517b276c45d1195eb29f6f5f72659a05b/guided_diffusion/nn.py
#
# thanks!
import math
import os
import numpy as np
import torch
import torch.nn as nn
from einops import repeat
from threestudio.utils.GAN.util import instantiate_from_config
def make_beta_schedule(
schedule, n_timestep, linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3
):
if schedule == "linear":
betas = (
torch.linspace(
linear_start**0.5, linear_end**0.5, n_timestep, dtype=torch.float64
)
** 2
)
elif schedule == "cosine":
timesteps = (
torch.arange(n_timestep + 1, dtype=torch.float64) / n_timestep + cosine_s
)
alphas = timesteps / (1 + cosine_s) * np.pi / 2
alphas = torch.cos(alphas).pow(2)
alphas = alphas / alphas[0]
betas = 1 - alphas[1:] / alphas[:-1]
betas = np.clip(betas, a_min=0, a_max=0.999)
elif schedule == "sqrt_linear":
betas = torch.linspace(
linear_start, linear_end, n_timestep, dtype=torch.float64
)
elif schedule == "sqrt":
betas = (
torch.linspace(linear_start, linear_end, n_timestep, dtype=torch.float64)
** 0.5
)
else:
raise ValueError(f"schedule '{schedule}' unknown.")
return betas.numpy()
def make_ddim_timesteps(
ddim_discr_method, num_ddim_timesteps, num_ddpm_timesteps, verbose=True
):
if ddim_discr_method == "uniform":
c = num_ddpm_timesteps // num_ddim_timesteps
ddim_timesteps = np.asarray(list(range(0, num_ddpm_timesteps, c)))
elif ddim_discr_method == "quad":
ddim_timesteps = (
(np.linspace(0, np.sqrt(num_ddpm_timesteps * 0.8), num_ddim_timesteps)) ** 2
).astype(int)
else:
raise NotImplementedError(
f'There is no ddim discretization method called "{ddim_discr_method}"'
)
# assert ddim_timesteps.shape[0] == num_ddim_timesteps
# add one to get the final alpha values right (the ones from first scale to data during sampling)
steps_out = ddim_timesteps + 1
if verbose:
print(f"Selected timesteps for ddim sampler: {steps_out}")
return steps_out
def make_ddim_sampling_parameters(alphacums, ddim_timesteps, eta, verbose=True):
# select alphas for computing the variance schedule
alphas = alphacums[ddim_timesteps]
alphas_prev = np.asarray([alphacums[0]] + alphacums[ddim_timesteps[:-1]].tolist())
# according the the formula provided in https://arxiv.org/abs/2010.02502
sigmas = eta * np.sqrt(
(1 - alphas_prev) / (1 - alphas) * (1 - alphas / alphas_prev)
)
if verbose:
print(
f"Selected alphas for ddim sampler: a_t: {alphas}; a_(t-1): {alphas_prev}"
)
print(
f"For the chosen value of eta, which is {eta}, "
f"this results in the following sigma_t schedule for ddim sampler {sigmas}"
)
return sigmas, alphas, alphas_prev
def betas_for_alpha_bar(num_diffusion_timesteps, alpha_bar, max_beta=0.999):
"""
Create a beta schedule that discretizes the given alpha_t_bar function,
which defines the cumulative product of (1-beta) over time from t = [0,1].
:param num_diffusion_timesteps: the number of betas to produce.
:param alpha_bar: a lambda that takes an argument t from 0 to 1 and
produces the cumulative product of (1-beta) up to that
part of the diffusion process.
:param max_beta: the maximum beta to use; use values lower than 1 to
prevent singularities.
"""
betas = []
for i in range(num_diffusion_timesteps):
t1 = i / num_diffusion_timesteps
t2 = (i + 1) / num_diffusion_timesteps
betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta))
return np.array(betas)
def extract_into_tensor(a, t, x_shape):
b, *_ = t.shape
out = a.gather(-1, t)
return out.reshape(b, *((1,) * (len(x_shape) - 1)))
def checkpoint(func, inputs, params, flag):
"""
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.
"""
if flag:
args = tuple(inputs) + tuple(params)
return CheckpointFunction.apply(func, len(inputs), *args)
else:
return func(*inputs)
class CheckpointFunction(torch.autograd.Function):
@staticmethod
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
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
def timestep_embedding(timesteps, dim, max_period=10000, repeat_only=False):
"""
Create sinusoidal timestep embeddings.
:param timesteps: a 1-D Tensor of N indices, one per batch element.
These may be fractional.
:param dim: the dimension of the output.
:param max_period: controls the minimum frequency of the embeddings.
:return: an [N x dim] Tensor of positional embeddings.
"""
if not repeat_only:
half = dim // 2
freqs = torch.exp(
-math.log(max_period)
* torch.arange(start=0, end=half, dtype=torch.float32)
/ half
).to(device=timesteps.device)
args = timesteps[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat(
[embedding, torch.zeros_like(embedding[:, :1])], dim=-1
)
else:
embedding = repeat(timesteps, "b -> b d", d=dim)
return embedding
def zero_module(module):
"""
Zero out the parameters of a module and return it.
"""
for p in module.parameters():
p.detach().zero_()
return module
def scale_module(module, scale):
"""
Scale the parameters of a module and return it.
"""
for p in module.parameters():
p.detach().mul_(scale)
return module
def mean_flat(tensor):
"""
Take the mean over all non-batch dimensions.
"""
return tensor.mean(dim=list(range(1, len(tensor.shape))))
def normalization(channels):
"""
Make a standard normalization layer.
:param channels: number of input channels.
:return: an nn.Module for normalization.
"""
return GroupNorm32(32, channels)
# PyTorch 1.7 has SiLU, but we support PyTorch 1.5.
class SiLU(nn.Module):
def forward(self, x):
return x * torch.sigmoid(x)
class GroupNorm32(nn.GroupNorm):
def forward(self, x):
return super().forward(x.float()).type(x.dtype)
def conv_nd(dims, *args, **kwargs):
"""
Create a 1D, 2D, or 3D convolution module.
"""
if dims == 1:
return nn.Conv1d(*args, **kwargs)
elif dims == 2:
return nn.Conv2d(*args, **kwargs)
elif dims == 3:
return nn.Conv3d(*args, **kwargs)
raise ValueError(f"unsupported dimensions: {dims}")
def linear(*args, **kwargs):
"""
Create a linear module.
"""
return nn.Linear(*args, **kwargs)
def avg_pool_nd(dims, *args, **kwargs):
"""
Create a 1D, 2D, or 3D average pooling module.
"""
if dims == 1:
return nn.AvgPool1d(*args, **kwargs)
elif dims == 2:
return nn.AvgPool2d(*args, **kwargs)
elif dims == 3:
return nn.AvgPool3d(*args, **kwargs)
raise ValueError(f"unsupported dimensions: {dims}")
class HybridConditioner(nn.Module):
def __init__(self, c_concat_config, c_crossattn_config):
super().__init__()
self.concat_conditioner = instantiate_from_config(c_concat_config)
self.crossattn_conditioner = instantiate_from_config(c_crossattn_config)
def forward(self, c_concat, c_crossattn):
c_concat = self.concat_conditioner(c_concat)
c_crossattn = self.crossattn_conditioner(c_crossattn)
return {"c_concat": [c_concat], "c_crossattn": [c_crossattn]}
def noise_like(shape, device, repeat=False):
repeat_noise = lambda: torch.randn((1, *shape[1:]), device=device).repeat(
shape[0], *((1,) * (len(shape) - 1))
)
noise = lambda: torch.randn(shape, device=device)
return repeat_noise() if repeat else noise()

View File

@@ -0,0 +1,401 @@
"""
Copyright (C) 2019 NVIDIA Corporation. Ting-Chun Wang, Ming-Yu Liu, Jun-Yan Zhu.
BSD License. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
import functools
import numpy as np
import torch
import torch.nn as nn
from torch.autograd import Variable
###############################################################################
# Functions
###############################################################################
def weights_init(m):
classname = m.__class__.__name__
if classname.find("Conv") != -1:
m.weight.data.normal_(0.0, 0.02)
elif classname.find("BatchNorm2d") != -1:
m.weight.data.normal_(1.0, 0.02)
m.bias.data.fill_(0)
def get_norm_layer(norm_type="instance"):
if norm_type == "batch":
norm_layer = functools.partial(nn.BatchNorm2d, affine=True)
elif norm_type == "instance":
norm_layer = functools.partial(nn.InstanceNorm2d, affine=False)
else:
raise NotImplementedError("normalization layer [%s] is not found" % norm_type)
return norm_layer
def define_G(
input_nc,
output_nc,
ngf,
netG,
n_downsample_global=3,
n_blocks_global=9,
n_local_enhancers=1,
n_blocks_local=3,
norm="instance",
gpu_ids=[],
last_op=nn.Tanh(),
):
norm_layer = get_norm_layer(norm_type=norm)
if netG == "global":
netG = GlobalGenerator(
input_nc,
output_nc,
ngf,
n_downsample_global,
n_blocks_global,
norm_layer,
last_op=last_op,
)
elif netG == "local":
netG = LocalEnhancer(
input_nc,
output_nc,
ngf,
n_downsample_global,
n_blocks_global,
n_local_enhancers,
n_blocks_local,
norm_layer,
)
elif netG == "encoder":
netG = Encoder(input_nc, output_nc, ngf, n_downsample_global, norm_layer)
else:
raise ("generator not implemented!")
# print(netG)
if len(gpu_ids) > 0:
assert torch.cuda.is_available()
netG.cuda(gpu_ids[0])
netG.apply(weights_init)
return netG
def print_network(net):
if isinstance(net, list):
net = net[0]
num_params = 0
for param in net.parameters():
num_params += param.numel()
print(net)
print("Total number of parameters: %d" % num_params)
##############################################################################
# Generator
##############################################################################
class LocalEnhancer(nn.Module):
def __init__(
self,
input_nc,
output_nc,
ngf=32,
n_downsample_global=3,
n_blocks_global=9,
n_local_enhancers=1,
n_blocks_local=3,
norm_layer=nn.BatchNorm2d,
padding_type="reflect",
):
super(LocalEnhancer, self).__init__()
self.n_local_enhancers = n_local_enhancers
###### global generator model #####
ngf_global = ngf * (2**n_local_enhancers)
model_global = GlobalGenerator(
input_nc,
output_nc,
ngf_global,
n_downsample_global,
n_blocks_global,
norm_layer,
).model
model_global = [
model_global[i] for i in range(len(model_global) - 3)
] # get rid of final convolution layers
self.model = nn.Sequential(*model_global)
###### local enhancer layers #####
for n in range(1, n_local_enhancers + 1):
### downsample
ngf_global = ngf * (2 ** (n_local_enhancers - n))
model_downsample = [
nn.ReflectionPad2d(3),
nn.Conv2d(input_nc, ngf_global, kernel_size=7, padding=0),
norm_layer(ngf_global),
nn.ReLU(True),
nn.Conv2d(
ngf_global, ngf_global * 2, kernel_size=3, stride=2, padding=1
),
norm_layer(ngf_global * 2),
nn.ReLU(True),
]
### residual blocks
model_upsample = []
for i in range(n_blocks_local):
model_upsample += [
ResnetBlock(
ngf_global * 2, padding_type=padding_type, norm_layer=norm_layer
)
]
### upsample
model_upsample += [
nn.ConvTranspose2d(
ngf_global * 2,
ngf_global,
kernel_size=3,
stride=2,
padding=1,
output_padding=1,
),
norm_layer(ngf_global),
nn.ReLU(True),
]
### final convolution
if n == n_local_enhancers:
model_upsample += [
nn.ReflectionPad2d(3),
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0),
nn.Tanh(),
]
setattr(self, "model" + str(n) + "_1", nn.Sequential(*model_downsample))
setattr(self, "model" + str(n) + "_2", nn.Sequential(*model_upsample))
self.downsample = nn.AvgPool2d(
3, stride=2, padding=[1, 1], count_include_pad=False
)
def forward(self, input):
### create input pyramid
input_downsampled = [input]
for i in range(self.n_local_enhancers):
input_downsampled.append(self.downsample(input_downsampled[-1]))
### output at coarest level
output_prev = self.model(input_downsampled[-1])
### build up one layer at a time
for n_local_enhancers in range(1, self.n_local_enhancers + 1):
model_downsample = getattr(self, "model" + str(n_local_enhancers) + "_1")
model_upsample = getattr(self, "model" + str(n_local_enhancers) + "_2")
input_i = input_downsampled[self.n_local_enhancers - n_local_enhancers]
output_prev = model_upsample(model_downsample(input_i) + output_prev)
return output_prev
class NormalNet(nn.Module):
def __init__(
self,
name="normalnet",
input_nc=3,
output_nc=3,
ngf=64,
n_downsampling=4,
n_blocks=9,
norm_layer=nn.BatchNorm2d,
padding_type="reflect",
last_op=nn.Sigmoid(),
):
assert n_blocks >= 0
super(NormalNet, self).__init__()
self.name = name
activation = nn.ReLU(True)
model = [
nn.ReflectionPad2d(3),
nn.Conv2d(input_nc, ngf, kernel_size=7, padding=0),
nn.BatchNorm2d(ngf),
activation,
]
### downsample
for i in range(n_downsampling):
mult = 2**i
model += [
nn.Conv2d(
ngf * mult, ngf * mult * 2, kernel_size=3, stride=2, padding=1
),
nn.BatchNorm2d(ngf * mult * 2),
activation,
]
### resnet blocks
mult = 2**n_downsampling
for i in range(n_blocks):
model += [
ResnetBlock(
ngf * mult,
padding_type=padding_type,
activation=activation,
norm_layer=norm_layer,
)
]
### upsample
for i in range(n_downsampling):
mult = 2 ** (n_downsampling - i)
model += [
nn.Upsample(scale_factor=2),
nn.Conv2d(
ngf * mult, int(ngf * mult / 2), kernel_size=3, stride=1, padding=1
),
nn.BatchNorm2d(int(ngf * mult / 2)),
activation,
]
model += [
nn.ReflectionPad2d(3),
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0),
]
if last_op is not None:
model += [last_op]
self.model = nn.Sequential(*model)
def forward(self, in_x, label=None):
res_list = []
return self.model(in_x)
# Define a resnet block
class ResnetBlock(nn.Module):
def __init__(
self, dim, padding_type, norm_layer, activation=nn.ReLU(True), use_dropout=False
):
super(ResnetBlock, self).__init__()
self.conv_block = self.build_conv_block(
dim, padding_type, norm_layer, activation, use_dropout
)
def build_conv_block(self, dim, padding_type, norm_layer, activation, use_dropout):
conv_block = []
p = 0
if padding_type == "reflect":
conv_block += [nn.ReflectionPad2d(1)]
elif padding_type == "replicate":
conv_block += [nn.ReplicationPad2d(1)]
elif padding_type == "zero":
p = 1
else:
raise NotImplementedError("padding [%s] is not implemented" % padding_type)
conv_block += [
nn.Conv2d(dim, dim, kernel_size=3, padding=p),
nn.BatchNorm2d(dim),
activation,
]
if use_dropout:
conv_block += [nn.Dropout(0.5)]
p = 0
if padding_type == "reflect":
conv_block += [nn.ReflectionPad2d(1)]
elif padding_type == "replicate":
conv_block += [nn.ReplicationPad2d(1)]
elif padding_type == "zero":
p = 1
else:
raise NotImplementedError("padding [%s] is not implemented" % padding_type)
conv_block += [
nn.Conv2d(dim, dim, kernel_size=3, padding=p),
nn.BatchNorm2d(dim),
]
return nn.Sequential(*conv_block)
def forward(self, x):
out = x + self.conv_block(x)
return out
class Encoder(nn.Module):
def __init__(
self, input_nc, output_nc, ngf=32, n_downsampling=4, norm_layer=nn.BatchNorm2d
):
super(Encoder, self).__init__()
self.output_nc = output_nc
model = [
nn.ReflectionPad2d(3),
nn.Conv2d(input_nc, ngf, kernel_size=7, padding=0),
norm_layer(ngf),
nn.ReLU(True),
]
### downsample
for i in range(n_downsampling):
mult = 2**i
model += [
nn.Conv2d(
ngf * mult, ngf * mult * 2, kernel_size=3, stride=2, padding=1
),
norm_layer(ngf * mult * 2),
nn.ReLU(True),
]
### upsample
for i in range(n_downsampling):
mult = 2 ** (n_downsampling - i)
model += [
nn.ConvTranspose2d(
ngf * mult,
int(ngf * mult / 2),
kernel_size=3,
stride=2,
padding=1,
output_padding=1,
),
norm_layer(int(ngf * mult / 2)),
nn.ReLU(True),
]
model += [
nn.ReflectionPad2d(3),
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0),
nn.Tanh(),
]
self.model = nn.Sequential(*model)
def forward(self, input, inst):
outputs = self.model(input)
# instance-wise average pooling
outputs_mean = outputs.clone()
inst_list = np.unique(inst.cpu().numpy().astype(int))
for i in inst_list:
for b in range(input.size()[0]):
indices = (inst[b : b + 1] == int(i)).nonzero() # n x 4
for j in range(self.output_nc):
output_ins = outputs[
indices[:, 0] + b,
indices[:, 1] + j,
indices[:, 2],
indices[:, 3],
]
mean_feat = torch.mean(output_ins).expand_as(output_ins)
outputs_mean[
indices[:, 0] + b,
indices[:, 1] + j,
indices[:, 2],
indices[:, 3],
] = mean_feat
return outputs_mean

View File

@@ -0,0 +1,208 @@
import importlib
import multiprocessing as mp
from collections import abc
from functools import partial
from inspect import isfunction
from queue import Queue
from threading import Thread
import numpy as np
import torch
from einops import rearrange
from PIL import Image, ImageDraw, ImageFont
def log_txt_as_img(wh, xc, size=10):
# wh a tuple of (width, height)
# xc a list of captions to plot
b = len(xc)
txts = list()
for bi in range(b):
txt = Image.new("RGB", wh, color="white")
draw = ImageDraw.Draw(txt)
font = ImageFont.truetype("data/DejaVuSans.ttf", size=size)
nc = int(40 * (wh[0] / 256))
lines = "\n".join(
xc[bi][start : start + nc] for start in range(0, len(xc[bi]), nc)
)
try:
draw.text((0, 0), lines, fill="black", font=font)
except UnicodeEncodeError:
print("Cant encode string for logging. Skipping.")
txt = np.array(txt).transpose(2, 0, 1) / 127.5 - 1.0
txts.append(txt)
txts = np.stack(txts)
txts = torch.tensor(txts)
return txts
def ismap(x):
if not isinstance(x, torch.Tensor):
return False
return (len(x.shape) == 4) and (x.shape[1] > 3)
def isimage(x):
if not isinstance(x, torch.Tensor):
return False
return (len(x.shape) == 4) and (x.shape[1] == 3 or x.shape[1] == 1)
def exists(x):
return x is not None
def default(val, d):
if exists(val):
return val
return d() if isfunction(d) else d
def mean_flat(tensor):
"""
https://github.com/openai/guided-diffusion/blob/27c20a8fab9cb472df5d6bdd6c8d11c8f430b924/guided_diffusion/nn.py#L86
Take the mean over all non-batch dimensions.
"""
return tensor.mean(dim=list(range(1, len(tensor.shape))))
def count_params(model, verbose=False):
total_params = sum(p.numel() for p in model.parameters())
if verbose:
print(f"{model.__class__.__name__} has {total_params * 1.e-6:.2f} M params.")
return total_params
def instantiate_from_config(config):
if not "target" in config:
if config == "__is_first_stage__":
return None
elif config == "__is_unconditional__":
return None
raise KeyError("Expected key `target` to instantiate.")
return get_obj_from_str(config["target"])(**config.get("params", dict()))
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
def _do_parallel_data_prefetch(func, Q, data, idx, idx_to_fn=False):
# create dummy dataset instance
# run prefetching
if idx_to_fn:
res = func(data, worker_id=idx)
else:
res = func(data)
Q.put([idx, res])
Q.put("Done")
def parallel_data_prefetch(
func: callable,
data,
n_proc,
target_data_type="ndarray",
cpu_intensive=True,
use_worker_id=False,
):
# if target_data_type not in ["ndarray", "list"]:
# raise ValueError(
# "Data, which is passed to parallel_data_prefetch has to be either of type list or ndarray."
# )
if isinstance(data, np.ndarray) and target_data_type == "list":
raise ValueError("list expected but function got ndarray.")
elif isinstance(data, abc.Iterable):
if isinstance(data, dict):
print(
f'WARNING:"data" argument passed to parallel_data_prefetch is a dict: Using only its values and disregarding keys.'
)
data = list(data.values())
if target_data_type == "ndarray":
data = np.asarray(data)
else:
data = list(data)
else:
raise TypeError(
f"The data, that shall be processed parallel has to be either an np.ndarray or an Iterable, but is actually {type(data)}."
)
if cpu_intensive:
Q = mp.Queue(1000)
proc = mp.Process
else:
Q = Queue(1000)
proc = Thread
# spawn processes
if target_data_type == "ndarray":
arguments = [
[func, Q, part, i, use_worker_id]
for i, part in enumerate(np.array_split(data, n_proc))
]
else:
step = (
int(len(data) / n_proc + 1)
if len(data) % n_proc != 0
else int(len(data) / n_proc)
)
arguments = [
[func, Q, part, i, use_worker_id]
for i, part in enumerate(
[data[i : i + step] for i in range(0, len(data), step)]
)
]
processes = []
for i in range(n_proc):
p = proc(target=_do_parallel_data_prefetch, args=arguments[i])
processes += [p]
# start processes
print(f"Start prefetching...")
import time
start = time.time()
gather_res = [[] for _ in range(n_proc)]
try:
for p in processes:
p.start()
k = 0
while k < n_proc:
# get result
res = Q.get()
if res == "Done":
k += 1
else:
gather_res[res[0]] = res[1]
except Exception as e:
print("Exception: ", e)
for p in processes:
p.terminate()
raise e
finally:
for p in processes:
p.join()
print(f"Prefetching complete. [{time.time() - start} sec.]")
if target_data_type == "ndarray":
if not isinstance(gather_res[0], np.ndarray):
return np.concatenate([np.asarray(r) for r in gather_res], axis=0)
# order outputs
return np.concatenate(gather_res, axis=0)
elif target_data_type == "list":
out = []
for r in gather_res:
out.extend(r)
return out
else:
return gather_res

1028
threestudio/utils/GAN/vae.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
from . import base

118
threestudio/utils/base.py Normal file
View File

@@ -0,0 +1,118 @@
from dataclasses import dataclass
import torch
import torch.nn as nn
from threestudio.utils.config import parse_structured
from threestudio.utils.misc import get_device, load_module_weights
from threestudio.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

View File

@@ -0,0 +1,156 @@
import os
import shutil
import subprocess
import pytorch_lightning
from threestudio.utils.config import dump_config
from threestudio.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 ...")

131
threestudio/utils/config.py Normal file
View File

@@ -0,0 +1,131 @@
import os
from dataclasses import dataclass, field
from datetime import datetime
from omegaconf import OmegaConf
import threestudio
from threestudio.utils.typing import *
# ============ Register OmegaConf Recolvers ============= #
OmegaConf.register_new_resolver(
"calc_exp_lr_decay_rate", lambda factor, n: factor ** (1.0 / n)
)
OmegaConf.register_new_resolver("add", lambda a, b: a + b)
OmegaConf.register_new_resolver("sub", lambda a, b: a - b)
OmegaConf.register_new_resolver("mul", lambda a, b: a * b)
OmegaConf.register_new_resolver("div", lambda a, b: a / b)
OmegaConf.register_new_resolver("idiv", lambda a, b: a // b)
OmegaConf.register_new_resolver("basename", lambda p: os.path.basename(p))
OmegaConf.register_new_resolver("rmspace", lambda s, sub: s.replace(" ", sub))
OmegaConf.register_new_resolver("tuple2", lambda s: [float(s), float(s)])
OmegaConf.register_new_resolver("gt0", lambda s: s > 0)
OmegaConf.register_new_resolver("cmaxgt0", lambda s: C_max(s) > 0)
OmegaConf.register_new_resolver("not", lambda s: not s)
OmegaConf.register_new_resolver(
"cmaxgt0orcmaxgt0", lambda a, b: C_max(a) > 0 or C_max(b) > 0
)
# ======================================================= #
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"
# import custom extension
custom_import: Tuple[str] = ()
### 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:
threestudio.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

924
threestudio/utils/dpt.py Normal file
View File

@@ -0,0 +1,924 @@
import math
import types
import torch
import torch.nn as nn
import torch.nn.functional as F
import timm
class BaseModel(torch.nn.Module):
def load(self, path):
"""Load model from file.
Args:
path (str): file path
"""
parameters = torch.load(path, map_location=torch.device('cpu'))
if "optimizer" in parameters:
parameters = parameters["model"]
self.load_state_dict(parameters)
def unflatten_with_named_tensor(input, dim, sizes):
"""Workaround for unflattening with named tensor."""
# tracer acts up with unflatten. See https://github.com/pytorch/pytorch/issues/49538
new_shape = list(input.shape)[:dim] + list(sizes) + list(input.shape)[dim+1:]
return input.view(*new_shape)
class Slice(nn.Module):
def __init__(self, start_index=1):
super(Slice, self).__init__()
self.start_index = start_index
def forward(self, x):
return x[:, self.start_index :]
class AddReadout(nn.Module):
def __init__(self, start_index=1):
super(AddReadout, self).__init__()
self.start_index = start_index
def forward(self, x):
if self.start_index == 2:
readout = (x[:, 0] + x[:, 1]) / 2
else:
readout = x[:, 0]
return x[:, self.start_index :] + readout.unsqueeze(1)
class ProjectReadout(nn.Module):
def __init__(self, in_features, start_index=1):
super(ProjectReadout, self).__init__()
self.start_index = start_index
self.project = nn.Sequential(nn.Linear(2 * in_features, in_features), nn.GELU())
def forward(self, x):
readout = x[:, 0].unsqueeze(1).expand_as(x[:, self.start_index :])
features = torch.cat((x[:, self.start_index :], readout), -1)
return self.project(features)
class Transpose(nn.Module):
def __init__(self, dim0, dim1):
super(Transpose, self).__init__()
self.dim0 = dim0
self.dim1 = dim1
def forward(self, x):
x = x.transpose(self.dim0, self.dim1)
return x
def forward_vit(pretrained, x):
b, c, h, w = x.shape
glob = pretrained.model.forward_flex(x)
layer_1 = pretrained.activations["1"]
layer_2 = pretrained.activations["2"]
layer_3 = pretrained.activations["3"]
layer_4 = pretrained.activations["4"]
layer_1 = pretrained.act_postprocess1[0:2](layer_1)
layer_2 = pretrained.act_postprocess2[0:2](layer_2)
layer_3 = pretrained.act_postprocess3[0:2](layer_3)
layer_4 = pretrained.act_postprocess4[0:2](layer_4)
unflattened_dim = 2
unflattened_size = (
int(torch.div(h, pretrained.model.patch_size[1], rounding_mode='floor')),
int(torch.div(w, pretrained.model.patch_size[0], rounding_mode='floor')),
)
unflatten = nn.Sequential(nn.Unflatten(unflattened_dim, unflattened_size))
if layer_1.ndim == 3:
layer_1 = unflatten(layer_1)
if layer_2.ndim == 3:
layer_2 = unflatten(layer_2)
if layer_3.ndim == 3:
layer_3 = unflatten_with_named_tensor(layer_3, unflattened_dim, unflattened_size)
if layer_4.ndim == 3:
layer_4 = unflatten_with_named_tensor(layer_4, unflattened_dim, unflattened_size)
layer_1 = pretrained.act_postprocess1[3 : len(pretrained.act_postprocess1)](layer_1)
layer_2 = pretrained.act_postprocess2[3 : len(pretrained.act_postprocess2)](layer_2)
layer_3 = pretrained.act_postprocess3[3 : len(pretrained.act_postprocess3)](layer_3)
layer_4 = pretrained.act_postprocess4[3 : len(pretrained.act_postprocess4)](layer_4)
return layer_1, layer_2, layer_3, layer_4
def _resize_pos_embed(self, posemb, gs_h, gs_w):
posemb_tok, posemb_grid = (
posemb[:, : self.start_index],
posemb[0, self.start_index :],
)
gs_old = int(math.sqrt(posemb_grid.shape[0]))
posemb_grid = posemb_grid.reshape(1, gs_old, gs_old, -1).permute(0, 3, 1, 2)
posemb_grid = F.interpolate(posemb_grid, size=(gs_h, gs_w), mode="bilinear")
posemb_grid = posemb_grid.permute(0, 2, 3, 1).reshape(1, gs_h * gs_w, -1)
posemb = torch.cat([posemb_tok, posemb_grid], dim=1)
return posemb
def forward_flex(self, x):
b, c, h, w = x.shape
pos_embed = self._resize_pos_embed(
self.pos_embed, torch.div(h, self.patch_size[1], rounding_mode='floor'), torch.div(w, self.patch_size[0], rounding_mode='floor')
)
B = x.shape[0]
if hasattr(self.patch_embed, "backbone"):
x = self.patch_embed.backbone(x)
if isinstance(x, (list, tuple)):
x = x[-1] # last feature if backbone outputs list/tuple of features
x = self.patch_embed.proj(x).flatten(2).transpose(1, 2)
if getattr(self, "dist_token", None) is not None:
cls_tokens = self.cls_token.expand(
B, -1, -1
) # stole cls_tokens impl from Phil Wang, thanks
dist_token = self.dist_token.expand(B, -1, -1)
x = torch.cat((cls_tokens, dist_token, x), dim=1)
else:
cls_tokens = self.cls_token.expand(
B, -1, -1
) # stole cls_tokens impl from Phil Wang, thanks
x = torch.cat((cls_tokens, x), dim=1)
x = x + pos_embed
x = self.pos_drop(x)
for blk in self.blocks:
x = blk(x)
x = self.norm(x)
return x
activations = {}
def get_activation(name):
def hook(model, input, output):
activations[name] = output
return hook
def get_readout_oper(vit_features, features, use_readout, start_index=1):
if use_readout == "ignore":
readout_oper = [Slice(start_index)] * len(features)
elif use_readout == "add":
readout_oper = [AddReadout(start_index)] * len(features)
elif use_readout == "project":
readout_oper = [
ProjectReadout(vit_features, start_index) for out_feat in features
]
else:
assert (
False
), "wrong operation for readout token, use_readout can be 'ignore', 'add', or 'project'"
return readout_oper
def _make_vit_b16_backbone(
model,
features=[96, 192, 384, 768],
size=[384, 384],
hooks=[2, 5, 8, 11],
vit_features=768,
use_readout="ignore",
start_index=1,
):
pretrained = nn.Module()
pretrained.model = model
pretrained.model.blocks[hooks[0]].register_forward_hook(get_activation("1"))
pretrained.model.blocks[hooks[1]].register_forward_hook(get_activation("2"))
pretrained.model.blocks[hooks[2]].register_forward_hook(get_activation("3"))
pretrained.model.blocks[hooks[3]].register_forward_hook(get_activation("4"))
pretrained.activations = activations
readout_oper = get_readout_oper(vit_features, features, use_readout, start_index)
# 32, 48, 136, 384
pretrained.act_postprocess1 = nn.Sequential(
readout_oper[0],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[0],
kernel_size=1,
stride=1,
padding=0,
),
nn.ConvTranspose2d(
in_channels=features[0],
out_channels=features[0],
kernel_size=4,
stride=4,
padding=0,
bias=True,
dilation=1,
groups=1,
),
)
pretrained.act_postprocess2 = nn.Sequential(
readout_oper[1],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[1],
kernel_size=1,
stride=1,
padding=0,
),
nn.ConvTranspose2d(
in_channels=features[1],
out_channels=features[1],
kernel_size=2,
stride=2,
padding=0,
bias=True,
dilation=1,
groups=1,
),
)
pretrained.act_postprocess3 = nn.Sequential(
readout_oper[2],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[2],
kernel_size=1,
stride=1,
padding=0,
),
)
pretrained.act_postprocess4 = nn.Sequential(
readout_oper[3],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[3],
kernel_size=1,
stride=1,
padding=0,
),
nn.Conv2d(
in_channels=features[3],
out_channels=features[3],
kernel_size=3,
stride=2,
padding=1,
),
)
pretrained.model.start_index = start_index
pretrained.model.patch_size = [16, 16]
# We inject this function into the VisionTransformer instances so that
# we can use it with interpolated position embeddings without modifying the library source.
pretrained.model.forward_flex = types.MethodType(forward_flex, pretrained.model)
pretrained.model._resize_pos_embed = types.MethodType(
_resize_pos_embed, pretrained.model
)
return pretrained
def _make_pretrained_vitl16_384(pretrained, use_readout="ignore", hooks=None):
model = timm.create_model("vit_large_patch16_384", pretrained=pretrained)
hooks = [5, 11, 17, 23] if hooks == None else hooks
return _make_vit_b16_backbone(
model,
features=[256, 512, 1024, 1024],
hooks=hooks,
vit_features=1024,
use_readout=use_readout,
)
def _make_pretrained_vitb16_384(pretrained, use_readout="ignore", hooks=None):
model = timm.create_model("vit_base_patch16_384", pretrained=pretrained)
hooks = [2, 5, 8, 11] if hooks == None else hooks
return _make_vit_b16_backbone(
model, features=[96, 192, 384, 768], hooks=hooks, use_readout=use_readout
)
def _make_pretrained_deitb16_384(pretrained, use_readout="ignore", hooks=None):
model = timm.create_model("vit_deit_base_patch16_384", pretrained=pretrained)
hooks = [2, 5, 8, 11] if hooks == None else hooks
return _make_vit_b16_backbone(
model, features=[96, 192, 384, 768], hooks=hooks, use_readout=use_readout
)
def _make_pretrained_deitb16_distil_384(pretrained, use_readout="ignore", hooks=None):
model = timm.create_model(
"vit_deit_base_distilled_patch16_384", pretrained=pretrained
)
hooks = [2, 5, 8, 11] if hooks == None else hooks
return _make_vit_b16_backbone(
model,
features=[96, 192, 384, 768],
hooks=hooks,
use_readout=use_readout,
start_index=2,
)
def _make_vit_b_rn50_backbone(
model,
features=[256, 512, 768, 768],
size=[384, 384],
hooks=[0, 1, 8, 11],
vit_features=768,
use_vit_only=False,
use_readout="ignore",
start_index=1,
):
pretrained = nn.Module()
pretrained.model = model
if use_vit_only == True:
pretrained.model.blocks[hooks[0]].register_forward_hook(get_activation("1"))
pretrained.model.blocks[hooks[1]].register_forward_hook(get_activation("2"))
else:
pretrained.model.patch_embed.backbone.stages[0].register_forward_hook(
get_activation("1")
)
pretrained.model.patch_embed.backbone.stages[1].register_forward_hook(
get_activation("2")
)
pretrained.model.blocks[hooks[2]].register_forward_hook(get_activation("3"))
pretrained.model.blocks[hooks[3]].register_forward_hook(get_activation("4"))
pretrained.activations = activations
readout_oper = get_readout_oper(vit_features, features, use_readout, start_index)
if use_vit_only == True:
pretrained.act_postprocess1 = nn.Sequential(
readout_oper[0],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[0],
kernel_size=1,
stride=1,
padding=0,
),
nn.ConvTranspose2d(
in_channels=features[0],
out_channels=features[0],
kernel_size=4,
stride=4,
padding=0,
bias=True,
dilation=1,
groups=1,
),
)
pretrained.act_postprocess2 = nn.Sequential(
readout_oper[1],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[1],
kernel_size=1,
stride=1,
padding=0,
),
nn.ConvTranspose2d(
in_channels=features[1],
out_channels=features[1],
kernel_size=2,
stride=2,
padding=0,
bias=True,
dilation=1,
groups=1,
),
)
else:
pretrained.act_postprocess1 = nn.Sequential(
nn.Identity(), nn.Identity(), nn.Identity()
)
pretrained.act_postprocess2 = nn.Sequential(
nn.Identity(), nn.Identity(), nn.Identity()
)
pretrained.act_postprocess3 = nn.Sequential(
readout_oper[2],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[2],
kernel_size=1,
stride=1,
padding=0,
),
)
pretrained.act_postprocess4 = nn.Sequential(
readout_oper[3],
Transpose(1, 2),
nn.Unflatten(2, torch.Size([size[0] // 16, size[1] // 16])),
nn.Conv2d(
in_channels=vit_features,
out_channels=features[3],
kernel_size=1,
stride=1,
padding=0,
),
nn.Conv2d(
in_channels=features[3],
out_channels=features[3],
kernel_size=3,
stride=2,
padding=1,
),
)
pretrained.model.start_index = start_index
pretrained.model.patch_size = [16, 16]
# We inject this function into the VisionTransformer instances so that
# we can use it with interpolated position embeddings without modifying the library source.
pretrained.model.forward_flex = types.MethodType(forward_flex, pretrained.model)
# We inject this function into the VisionTransformer instances so that
# we can use it with interpolated position embeddings without modifying the library source.
pretrained.model._resize_pos_embed = types.MethodType(
_resize_pos_embed, pretrained.model
)
return pretrained
def _make_pretrained_vitb_rn50_384(
pretrained, use_readout="ignore", hooks=None, use_vit_only=False
):
model = timm.create_model("vit_base_resnet50_384", pretrained=pretrained)
hooks = [0, 1, 8, 11] if hooks == None else hooks
return _make_vit_b_rn50_backbone(
model,
features=[256, 512, 768, 768],
size=[384, 384],
hooks=hooks,
use_vit_only=use_vit_only,
use_readout=use_readout,
)
def _make_encoder(backbone, features, use_pretrained, groups=1, expand=False, exportable=True, hooks=None, use_vit_only=False, use_readout="ignore",):
if backbone == "vitl16_384":
pretrained = _make_pretrained_vitl16_384(
use_pretrained, hooks=hooks, use_readout=use_readout
)
scratch = _make_scratch(
[256, 512, 1024, 1024], features, groups=groups, expand=expand
) # ViT-L/16 - 85.0% Top1 (backbone)
elif backbone == "vitb_rn50_384":
pretrained = _make_pretrained_vitb_rn50_384(
use_pretrained,
hooks=hooks,
use_vit_only=use_vit_only,
use_readout=use_readout,
)
scratch = _make_scratch(
[256, 512, 768, 768], features, groups=groups, expand=expand
) # ViT-H/16 - 85.0% Top1 (backbone)
elif backbone == "vitb16_384":
pretrained = _make_pretrained_vitb16_384(
use_pretrained, hooks=hooks, use_readout=use_readout
)
scratch = _make_scratch(
[96, 192, 384, 768], features, groups=groups, expand=expand
) # ViT-B/16 - 84.6% Top1 (backbone)
elif backbone == "resnext101_wsl":
pretrained = _make_pretrained_resnext101_wsl(use_pretrained)
scratch = _make_scratch([256, 512, 1024, 2048], features, groups=groups, expand=expand) # efficientnet_lite3
elif backbone == "efficientnet_lite3":
pretrained = _make_pretrained_efficientnet_lite3(use_pretrained, exportable=exportable)
scratch = _make_scratch([32, 48, 136, 384], features, groups=groups, expand=expand) # efficientnet_lite3
else:
print(f"Backbone '{backbone}' not implemented")
assert False
return pretrained, scratch
def _make_scratch(in_shape, out_shape, groups=1, expand=False):
scratch = nn.Module()
out_shape1 = out_shape
out_shape2 = out_shape
out_shape3 = out_shape
out_shape4 = out_shape
if expand==True:
out_shape1 = out_shape
out_shape2 = out_shape*2
out_shape3 = out_shape*4
out_shape4 = out_shape*8
scratch.layer1_rn = nn.Conv2d(
in_shape[0], out_shape1, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
scratch.layer2_rn = nn.Conv2d(
in_shape[1], out_shape2, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
scratch.layer3_rn = nn.Conv2d(
in_shape[2], out_shape3, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
scratch.layer4_rn = nn.Conv2d(
in_shape[3], out_shape4, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
return scratch
def _make_pretrained_efficientnet_lite3(use_pretrained, exportable=False):
efficientnet = torch.hub.load(
"rwightman/gen-efficientnet-pytorch",
"tf_efficientnet_lite3",
pretrained=use_pretrained,
exportable=exportable
)
return _make_efficientnet_backbone(efficientnet)
def _make_efficientnet_backbone(effnet):
pretrained = nn.Module()
pretrained.layer1 = nn.Sequential(
effnet.conv_stem, effnet.bn1, effnet.act1, *effnet.blocks[0:2]
)
pretrained.layer2 = nn.Sequential(*effnet.blocks[2:3])
pretrained.layer3 = nn.Sequential(*effnet.blocks[3:5])
pretrained.layer4 = nn.Sequential(*effnet.blocks[5:9])
return pretrained
def _make_resnet_backbone(resnet):
pretrained = nn.Module()
pretrained.layer1 = nn.Sequential(
resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool, resnet.layer1
)
pretrained.layer2 = resnet.layer2
pretrained.layer3 = resnet.layer3
pretrained.layer4 = resnet.layer4
return pretrained
def _make_pretrained_resnext101_wsl(use_pretrained):
resnet = torch.hub.load("facebookresearch/WSL-Images", "resnext101_32x8d_wsl")
return _make_resnet_backbone(resnet)
class Interpolate(nn.Module):
"""Interpolation module.
"""
def __init__(self, scale_factor, mode, align_corners=False):
"""Init.
Args:
scale_factor (float): scaling
mode (str): interpolation mode
"""
super(Interpolate, self).__init__()
self.interp = nn.functional.interpolate
self.scale_factor = scale_factor
self.mode = mode
self.align_corners = align_corners
def forward(self, x):
"""Forward pass.
Args:
x (tensor): input
Returns:
tensor: interpolated data
"""
x = self.interp(
x, scale_factor=self.scale_factor, mode=self.mode, align_corners=self.align_corners
)
return x
class ResidualConvUnit(nn.Module):
"""Residual convolution module.
"""
def __init__(self, features):
"""Init.
Args:
features (int): number of features
"""
super().__init__()
self.conv1 = nn.Conv2d(
features, features, kernel_size=3, stride=1, padding=1, bias=True
)
self.conv2 = nn.Conv2d(
features, features, kernel_size=3, stride=1, padding=1, bias=True
)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
"""Forward pass.
Args:
x (tensor): input
Returns:
tensor: output
"""
out = self.relu(x)
out = self.conv1(out)
out = self.relu(out)
out = self.conv2(out)
return out + x
class FeatureFusionBlock(nn.Module):
"""Feature fusion block.
"""
def __init__(self, features):
"""Init.
Args:
features (int): number of features
"""
super(FeatureFusionBlock, self).__init__()
self.resConfUnit1 = ResidualConvUnit(features)
self.resConfUnit2 = ResidualConvUnit(features)
def forward(self, *xs):
"""Forward pass.
Returns:
tensor: output
"""
output = xs[0]
if len(xs) == 2:
output += self.resConfUnit1(xs[1])
output = self.resConfUnit2(output)
output = nn.functional.interpolate(
output, scale_factor=2, mode="bilinear", align_corners=True
)
return output
class ResidualConvUnit_custom(nn.Module):
"""Residual convolution module.
"""
def __init__(self, features, activation, bn):
"""Init.
Args:
features (int): number of features
"""
super().__init__()
self.bn = bn
self.groups=1
self.conv1 = nn.Conv2d(
features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups
)
self.conv2 = nn.Conv2d(
features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups
)
if self.bn==True:
self.bn1 = nn.BatchNorm2d(features)
self.bn2 = nn.BatchNorm2d(features)
self.activation = activation
self.skip_add = nn.quantized.FloatFunctional()
def forward(self, x):
"""Forward pass.
Args:
x (tensor): input
Returns:
tensor: output
"""
out = self.activation(x)
out = self.conv1(out)
if self.bn==True:
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
if self.bn==True:
out = self.bn2(out)
if self.groups > 1:
out = self.conv_merge(out)
return self.skip_add.add(out, x)
# return out + x
class FeatureFusionBlock_custom(nn.Module):
"""Feature fusion block.
"""
def __init__(self, features, activation, deconv=False, bn=False, expand=False, align_corners=True):
"""Init.
Args:
features (int): number of features
"""
super(FeatureFusionBlock_custom, self).__init__()
self.deconv = deconv
self.align_corners = align_corners
self.groups=1
self.expand = expand
out_features = features
if self.expand==True:
out_features = features//2
self.out_conv = nn.Conv2d(features, out_features, kernel_size=1, stride=1, padding=0, bias=True, groups=1)
self.resConfUnit1 = ResidualConvUnit_custom(features, activation, bn)
self.resConfUnit2 = ResidualConvUnit_custom(features, activation, bn)
self.skip_add = nn.quantized.FloatFunctional()
def forward(self, *xs):
"""Forward pass.
Returns:
tensor: output
"""
output = xs[0]
if len(xs) == 2:
res = self.resConfUnit1(xs[1])
output = self.skip_add.add(output, res)
# output += res
output = self.resConfUnit2(output)
output = nn.functional.interpolate(
output, scale_factor=2, mode="bilinear", align_corners=self.align_corners
)
output = self.out_conv(output)
return output
def _make_fusion_block(features, use_bn):
return FeatureFusionBlock_custom(
features,
nn.ReLU(False),
deconv=False,
bn=use_bn,
expand=False,
align_corners=True,
)
class DPT(BaseModel):
def __init__(
self,
head,
features=256,
backbone="vitb_rn50_384",
readout="project",
channels_last=False,
use_bn=False,
):
super(DPT, self).__init__()
self.channels_last = channels_last
hooks = {
"vitb_rn50_384": [0, 1, 8, 11],
"vitb16_384": [2, 5, 8, 11],
"vitl16_384": [5, 11, 17, 23],
}
# Instantiate backbone and reassemble blocks
self.pretrained, self.scratch = _make_encoder(
backbone,
features,
True, # Set to true of you want to train from scratch, uses ImageNet weights
groups=1,
expand=False,
exportable=False,
hooks=hooks[backbone],
use_readout=readout,
)
self.scratch.refinenet1 = _make_fusion_block(features, use_bn)
self.scratch.refinenet2 = _make_fusion_block(features, use_bn)
self.scratch.refinenet3 = _make_fusion_block(features, use_bn)
self.scratch.refinenet4 = _make_fusion_block(features, use_bn)
self.scratch.output_conv = head
def forward(self, x):
if self.channels_last == True:
x.contiguous(memory_format=torch.channels_last)
layer_1, layer_2, layer_3, layer_4 = forward_vit(self.pretrained, x)
layer_1_rn = self.scratch.layer1_rn(layer_1)
layer_2_rn = self.scratch.layer2_rn(layer_2)
layer_3_rn = self.scratch.layer3_rn(layer_3)
layer_4_rn = self.scratch.layer4_rn(layer_4)
path_4 = self.scratch.refinenet4(layer_4_rn)
path_3 = self.scratch.refinenet3(path_4, layer_3_rn)
path_2 = self.scratch.refinenet2(path_3, layer_2_rn)
path_1 = self.scratch.refinenet1(path_2, layer_1_rn)
out = self.scratch.output_conv(path_1)
return out
class DPTDepthModel(DPT):
def __init__(self, path=None, non_negative=True, num_channels=1, **kwargs):
features = kwargs["features"] if "features" in kwargs else 256
head = nn.Sequential(
nn.Conv2d(features, features // 2, kernel_size=3, stride=1, padding=1),
Interpolate(scale_factor=2, mode="bilinear", align_corners=True),
nn.Conv2d(features // 2, 32, kernel_size=3, stride=1, padding=1),
nn.ReLU(True),
nn.Conv2d(32, num_channels, kernel_size=1, stride=1, padding=0),
nn.ReLU(True) if non_negative else nn.Identity(),
nn.Identity(),
)
super().__init__(head, **kwargs)
if path is not None:
self.load(path)
def forward(self, x):
return super().forward(x).squeeze(dim=1)

View File

@@ -0,0 +1 @@
from .lpips import LPIPS

View File

@@ -0,0 +1,123 @@
"""Stripped version of https://github.com/richzhang/PerceptualSimilarity/tree/master/models"""
import torch
import torch.nn as nn
from torchvision import models
from collections import namedtuple
from threestudio.utils.lpips.utils import get_ckpt_path
class LPIPS(nn.Module):
# Learned perceptual metric
def __init__(self, use_dropout=True):
super().__init__()
self.scaling_layer = ScalingLayer()
self.chns = [64, 128, 256, 512, 512] # vg16 features
self.net = vgg16(pretrained=True, requires_grad=False)
self.lin0 = NetLinLayer(self.chns[0], use_dropout=use_dropout)
self.lin1 = NetLinLayer(self.chns[1], use_dropout=use_dropout)
self.lin2 = NetLinLayer(self.chns[2], use_dropout=use_dropout)
self.lin3 = NetLinLayer(self.chns[3], use_dropout=use_dropout)
self.lin4 = NetLinLayer(self.chns[4], use_dropout=use_dropout)
self.load_from_pretrained()
for param in self.parameters():
param.requires_grad = False
def load_from_pretrained(self, name="vgg_lpips"):
ckpt = get_ckpt_path(name, "threestudio/utils/lpips")
self.load_state_dict(torch.load(ckpt, map_location=torch.device("cpu")), strict=False)
print("loaded pretrained LPIPS loss from {}".format(ckpt))
@classmethod
def from_pretrained(cls, name="vgg_lpips"):
if name != "vgg_lpips":
raise NotImplementedError
model = cls()
ckpt = get_ckpt_path(name)
model.load_state_dict(torch.load(ckpt, map_location=torch.device("cpu")), strict=False)
return model
def forward(self, input, target):
in0_input, in1_input = (self.scaling_layer(input), self.scaling_layer(target))
outs0, outs1 = self.net(in0_input), self.net(in1_input)
feats0, feats1, diffs = {}, {}, {}
lins = [self.lin0, self.lin1, self.lin2, self.lin3, self.lin4]
for kk in range(len(self.chns)):
feats0[kk], feats1[kk] = normalize_tensor(outs0[kk]), normalize_tensor(outs1[kk])
diffs[kk] = (feats0[kk] - feats1[kk]) ** 2
res = [spatial_average(lins[kk].model(diffs[kk]), keepdim=True) for kk in range(len(self.chns))]
val = res[0]
for l in range(1, len(self.chns)):
val += res[l]
return val
class ScalingLayer(nn.Module):
def __init__(self):
super(ScalingLayer, self).__init__()
self.register_buffer('shift', torch.Tensor([-.030, -.088, -.188])[None, :, None, None])
self.register_buffer('scale', torch.Tensor([.458, .448, .450])[None, :, None, None])
def forward(self, inp):
return (inp - self.shift) / self.scale
class NetLinLayer(nn.Module):
""" A single linear layer which does a 1x1 conv """
def __init__(self, chn_in, chn_out=1, use_dropout=False):
super(NetLinLayer, self).__init__()
layers = [nn.Dropout(), ] if (use_dropout) else []
layers += [nn.Conv2d(chn_in, chn_out, 1, stride=1, padding=0, bias=False), ]
self.model = nn.Sequential(*layers)
class vgg16(torch.nn.Module):
def __init__(self, requires_grad=False, pretrained=True):
super(vgg16, self).__init__()
vgg_pretrained_features = models.vgg16(pretrained=pretrained).features
self.slice1 = torch.nn.Sequential()
self.slice2 = torch.nn.Sequential()
self.slice3 = torch.nn.Sequential()
self.slice4 = torch.nn.Sequential()
self.slice5 = torch.nn.Sequential()
self.N_slices = 5
for x in range(4):
self.slice1.add_module(str(x), vgg_pretrained_features[x])
for x in range(4, 9):
self.slice2.add_module(str(x), vgg_pretrained_features[x])
for x in range(9, 16):
self.slice3.add_module(str(x), vgg_pretrained_features[x])
for x in range(16, 23):
self.slice4.add_module(str(x), vgg_pretrained_features[x])
for x in range(23, 30):
self.slice5.add_module(str(x), vgg_pretrained_features[x])
if not requires_grad:
for param in self.parameters():
param.requires_grad = False
def forward(self, X):
h = self.slice1(X)
h_relu1_2 = h
h = self.slice2(h)
h_relu2_2 = h
h = self.slice3(h)
h_relu3_3 = h
h = self.slice4(h)
h_relu4_3 = h
h = self.slice5(h)
h_relu5_3 = h
vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3', 'relu5_3'])
out = vgg_outputs(h_relu1_2, h_relu2_2, h_relu3_3, h_relu4_3, h_relu5_3)
return out
def normalize_tensor(x,eps=1e-10):
norm_factor = torch.sqrt(torch.sum(x**2,dim=1,keepdim=True))
return x/(norm_factor+eps)
def spatial_average(x, keepdim=True):
return x.mean([2,3],keepdim=keepdim)

View File

@@ -0,0 +1,157 @@
import os, hashlib
import requests
from tqdm import tqdm
URL_MAP = {
"vgg_lpips": "https://heibox.uni-heidelberg.de/f/607503859c864bc1b30b/?dl=1"
}
CKPT_MAP = {
"vgg_lpips": "vgg.pth"
}
MD5_MAP = {
"vgg_lpips": "d507d7349b931f0638a25a48a722f98a"
}
def download(url, local_path, chunk_size=1024):
os.makedirs(os.path.split(local_path)[0], exist_ok=True)
with requests.get(url, stream=True) as r:
total_size = int(r.headers.get("content-length", 0))
with tqdm(total=total_size, unit="B", unit_scale=True) as pbar:
with open(local_path, "wb") as f:
for data in r.iter_content(chunk_size=chunk_size):
if data:
f.write(data)
pbar.update(chunk_size)
def md5_hash(path):
with open(path, "rb") as f:
content = f.read()
return hashlib.md5(content).hexdigest()
def get_ckpt_path(name, root, check=False):
assert name in URL_MAP
path = os.path.join(root, CKPT_MAP[name])
if not os.path.exists(path) or (check and not md5_hash(path) == MD5_MAP[name]):
print("Downloading {} model from {} to {}".format(name, URL_MAP[name], path))
download(URL_MAP[name], path)
md5 = md5_hash(path)
assert md5 == MD5_MAP[name], md5
return path
class KeyNotFoundError(Exception):
def __init__(self, cause, keys=None, visited=None):
self.cause = cause
self.keys = keys
self.visited = visited
messages = list()
if keys is not None:
messages.append("Key not found: {}".format(keys))
if visited is not None:
messages.append("Visited: {}".format(visited))
messages.append("Cause:\n{}".format(cause))
message = "\n".join(messages)
super().__init__(message)
def retrieve(
list_or_dict, key, splitval="/", default=None, expand=True, pass_success=False
):
"""Given a nested list or dict return the desired value at key expanding
callable nodes if necessary and :attr:`expand` is ``True``. The expansion
is done in-place.
Parameters
----------
list_or_dict : list or dict
Possibly nested list or dictionary.
key : str
key/to/value, path like string describing all keys necessary to
consider to get to the desired value. List indices can also be
passed here.
splitval : str
String that defines the delimiter between keys of the
different depth levels in `key`.
default : obj
Value returned if :attr:`key` is not found.
expand : bool
Whether to expand callable nodes on the path or not.
Returns
-------
The desired value or if :attr:`default` is not ``None`` and the
:attr:`key` is not found returns ``default``.
Raises
------
Exception if ``key`` not in ``list_or_dict`` and :attr:`default` is
``None``.
"""
keys = key.split(splitval)
success = True
try:
visited = []
parent = None
last_key = None
for key in keys:
if callable(list_or_dict):
if not expand:
raise KeyNotFoundError(
ValueError(
"Trying to get past callable node with expand=False."
),
keys=keys,
visited=visited,
)
list_or_dict = list_or_dict()
parent[last_key] = list_or_dict
last_key = key
parent = list_or_dict
try:
if isinstance(list_or_dict, dict):
list_or_dict = list_or_dict[key]
else:
list_or_dict = list_or_dict[int(key)]
except (KeyError, IndexError, ValueError) as e:
raise KeyNotFoundError(e, keys=keys, visited=visited)
visited += [key]
# final expansion of retrieved value
if expand and callable(list_or_dict):
list_or_dict = list_or_dict()
parent[last_key] = list_or_dict
except KeyNotFoundError as e:
if default is None:
raise e
else:
list_or_dict = default
success = False
if not pass_success:
return list_or_dict
else:
return list_or_dict, success
if __name__ == "__main__":
config = {"keya": "a",
"keyb": "b",
"keyc":
{"cc1": 1,
"cc2": 2,
}
}
from omegaconf import OmegaConf
config = OmegaConf.create(config)
print(config)
retrieve(config, "keya")

Some files were not shown because too many files have changed in this diff Show More