mirror of
https://github.com/deepseek-ai/DreamCraft3D
synced 2025-06-26 18:25:49 +00:00
chores: rebase commits
This commit is contained in:
36
threestudio/__init__.py
Normal file
36
threestudio/__init__.py
Normal 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
|
||||
1
threestudio/data/__init__.py
Normal file
1
threestudio/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import image, uncond
|
||||
351
threestudio/data/image.py
Normal file
351
threestudio/data/image.py
Normal 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
351
threestudio/data/images.py
Normal 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
518
threestudio/data/uncond.py
Normal 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
|
||||
)
|
||||
9
threestudio/models/__init__.py
Normal file
9
threestudio/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from . import (
|
||||
background,
|
||||
exporters,
|
||||
geometry,
|
||||
guidance,
|
||||
materials,
|
||||
prompt_processors,
|
||||
renderers,
|
||||
)
|
||||
6
threestudio/models/background/__init__.py
Normal file
6
threestudio/models/background/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from . import (
|
||||
base,
|
||||
neural_environment_map_background,
|
||||
solid_color_background,
|
||||
textured_background,
|
||||
)
|
||||
24
threestudio/models/background/base.py
Normal file
24
threestudio/models/background/base.py
Normal 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
|
||||
@@ -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
|
||||
51
threestudio/models/background/solid_color_background.py
Normal file
51
threestudio/models/background/solid_color_background.py
Normal 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
|
||||
54
threestudio/models/background/textured_background.py
Normal file
54
threestudio/models/background/textured_background.py
Normal 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
|
||||
118
threestudio/models/estimators.py
Normal file
118
threestudio/models/estimators.py
Normal 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)
|
||||
1
threestudio/models/exporters/__init__.py
Normal file
1
threestudio/models/exporters/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import base, mesh_exporter
|
||||
59
threestudio/models/exporters/base.py
Normal file
59
threestudio/models/exporters/base.py
Normal 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 []
|
||||
175
threestudio/models/exporters/mesh_exporter.py
Normal file
175
threestudio/models/exporters/mesh_exporter.py
Normal 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
|
||||
)
|
||||
]
|
||||
8
threestudio/models/geometry/__init__.py
Normal file
8
threestudio/models/geometry/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from . import (
|
||||
base,
|
||||
custom_mesh,
|
||||
implicit_sdf,
|
||||
implicit_volume,
|
||||
tetrahedra_sdf_grid,
|
||||
volume_grid,
|
||||
)
|
||||
209
threestudio/models/geometry/base.py
Normal file
209
threestudio/models/geometry/base.py
Normal 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,
|
||||
),
|
||||
)
|
||||
178
threestudio/models/geometry/custom_mesh.py
Normal file
178
threestudio/models/geometry/custom_mesh.py
Normal 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
|
||||
413
threestudio/models/geometry/implicit_sdf.py
Normal file
413
threestudio/models/geometry/implicit_sdf.py
Normal 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}"
|
||||
)
|
||||
325
threestudio/models/geometry/implicit_volume.py
Normal file
325
threestudio/models/geometry/implicit_volume.py
Normal 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}"
|
||||
)
|
||||
369
threestudio/models/geometry/tetrahedra_sdf_grid.py
Normal file
369
threestudio/models/geometry/tetrahedra_sdf_grid.py
Normal 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
|
||||
190
threestudio/models/geometry/volume_grid.py
Normal file
190
threestudio/models/geometry/volume_grid.py
Normal 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
|
||||
13
threestudio/models/guidance/__init__.py
Normal file
13
threestudio/models/guidance/__init__.py
Normal 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,
|
||||
)
|
||||
84
threestudio/models/guidance/clip_guidance.py
Normal file
84
threestudio/models/guidance/clip_guidance.py
Normal 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
|
||||
517
threestudio/models/guidance/controlnet_guidance.py
Normal file
517
threestudio/models/guidance/controlnet_guidance.py
Normal 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
|
||||
454
threestudio/models/guidance/controlnet_reg_guidance.py
Normal file
454
threestudio/models/guidance/controlnet_reg_guidance.py
Normal 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)
|
||||
582
threestudio/models/guidance/deep_floyd_guidance.py
Normal file
582
threestudio/models/guidance/deep_floyd_guidance.py
Normal 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)
|
||||
1134
threestudio/models/guidance/stable_diffusion_bsd_guidance.py
Normal file
1134
threestudio/models/guidance/stable_diffusion_bsd_guidance.py
Normal file
File diff suppressed because it is too large
Load Diff
632
threestudio/models/guidance/stable_diffusion_guidance.py
Normal file
632
threestudio/models/guidance/stable_diffusion_guidance.py
Normal 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),
|
||||
)
|
||||
729
threestudio/models/guidance/stable_diffusion_unified_guidance.py
Normal file
729
threestudio/models/guidance/stable_diffusion_unified_guidance.py
Normal 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)
|
||||
)
|
||||
1003
threestudio/models/guidance/stable_diffusion_vsd_guidance.py
Normal file
1003
threestudio/models/guidance/stable_diffusion_vsd_guidance.py
Normal file
File diff suppressed because it is too large
Load Diff
340
threestudio/models/guidance/stable_zero123_guidance.py
Normal file
340
threestudio/models/guidance/stable_zero123_guidance.py
Normal 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),
|
||||
)
|
||||
528
threestudio/models/guidance/zero123_guidance.py
Normal file
528
threestudio/models/guidance/zero123_guidance.py
Normal 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))
|
||||
|
||||
721
threestudio/models/guidance/zero123_unified_guidance.py
Normal file
721
threestudio/models/guidance/zero123_unified_guidance.py
Normal 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)
|
||||
)
|
||||
253
threestudio/models/isosurface.py
Normal file
253
threestudio/models/isosurface.py
Normal 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
|
||||
9
threestudio/models/materials/__init__.py
Normal file
9
threestudio/models/materials/__init__.py
Normal 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,
|
||||
)
|
||||
29
threestudio/models/materials/base.py
Normal file
29
threestudio/models/materials/base.py
Normal 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 {}
|
||||
@@ -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}
|
||||
36
threestudio/models/materials/hybrid_rgb_latent_material.py
Normal file
36
threestudio/models/materials/hybrid_rgb_latent_material.py
Normal 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
|
||||
54
threestudio/models/materials/neural_radiance_material.py
Normal file
54
threestudio/models/materials/neural_radiance_material.py
Normal 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
|
||||
63
threestudio/models/materials/no_material.py
Normal file
63
threestudio/models/materials/no_material.py
Normal 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]}
|
||||
143
threestudio/models/materials/pbr_material.py
Normal file
143
threestudio/models/materials/pbr_material.py
Normal 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
|
||||
42
threestudio/models/materials/sd_latent_adapter_material.py
Normal file
42
threestudio/models/materials/sd_latent_adapter_material.py
Normal 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
309
threestudio/models/mesh.py
Normal 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
|
||||
411
threestudio/models/networks.py
Normal file
411
threestudio/models/networks.py
Normal 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)
|
||||
7
threestudio/models/prompt_processors/__init__.py
Normal file
7
threestudio/models/prompt_processors/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from . import (
|
||||
base,
|
||||
deepfloyd_prompt_processor,
|
||||
dummy_prompt_processor,
|
||||
stable_diffusion_prompt_processor,
|
||||
clip_prompt_processor,
|
||||
)
|
||||
517
threestudio/models/prompt_processors/base.py
Normal file
517
threestudio/models/prompt_processors/base.py
Normal 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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}')
|
||||
9
threestudio/models/renderers/__init__.py
Normal file
9
threestudio/models/renderers/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from . import (
|
||||
base,
|
||||
deferred_volume_renderer,
|
||||
gan_volume_renderer,
|
||||
nerf_volume_renderer,
|
||||
neus_volume_renderer,
|
||||
nvdiff_rasterizer,
|
||||
patch_renderer,
|
||||
)
|
||||
80
threestudio/models/renderers/base.py
Normal file
80
threestudio/models/renderers/base.py
Normal 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
|
||||
11
threestudio/models/renderers/deferred_volume_renderer.py
Normal file
11
threestudio/models/renderers/deferred_volume_renderer.py
Normal 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
|
||||
159
threestudio/models/renderers/gan_volume_renderer.py
Normal file
159
threestudio/models/renderers/gan_volume_renderer.py
Normal 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()
|
||||
462
threestudio/models/renderers/nerf_volume_renderer.py
Normal file
462
threestudio/models/renderers/nerf_volume_renderer.py
Normal 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()
|
||||
390
threestudio/models/renderers/neus_volume_renderer.py
Normal file
390
threestudio/models/renderers/neus_volume_renderer.py
Normal 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()
|
||||
188
threestudio/models/renderers/nvdiff_rasterizer.py
Normal file
188
threestudio/models/renderers/nvdiff_rasterizer.py
Normal 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
|
||||
106
threestudio/models/renderers/patch_renderer.py
Normal file
106
threestudio/models/renderers/patch_renderer.py
Normal 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()
|
||||
1025
threestudio/scripts/convert_zero123_to_diffusers.py
Normal file
1025
threestudio/scripts/convert_zero123_to_diffusers.py
Normal file
File diff suppressed because it is too large
Load Diff
53
threestudio/scripts/dreamcraft3d_dreambooth.py
Normal file
53
threestudio/scripts/dreamcraft3d_dreambooth.py
Normal 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)
|
||||
92
threestudio/scripts/generate_images_if.py
Normal file
92
threestudio/scripts/generate_images_if.py
Normal 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")
|
||||
90
threestudio/scripts/generate_images_if_prompt_library.py
Normal file
90
threestudio/scripts/generate_images_if_prompt_library.py
Normal 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")
|
||||
95
threestudio/scripts/generate_mv_datasets.py
Normal file
95
threestudio/scripts/generate_mv_datasets.py
Normal 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)
|
||||
|
||||
84
threestudio/scripts/img_to_mv.py
Normal file
84
threestudio/scripts/img_to_mv.py
Normal 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
|
||||
77
threestudio/scripts/make_training_vid.py
Normal file
77
threestudio/scripts/make_training_vid.py
Normal 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
|
||||
)
|
||||
459
threestudio/scripts/metric_utils.py
Normal file
459
threestudio/scripts/metric_utils.py
Normal 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")
|
||||
36
threestudio/scripts/run_gaussian.sh
Executable file
36
threestudio/scripts/run_gaussian.sh
Executable 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)
|
||||
|
||||
13
threestudio/scripts/run_zero123.py
Normal file
13
threestudio/scripts/run_zero123.py
Normal 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
|
||||
23
threestudio/scripts/run_zero123_comparison.sh
Executable file
23
threestudio/scripts/run_zero123_comparison.sh
Executable 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
|
||||
25
threestudio/scripts/run_zero123_demo.sh
Normal file
25
threestudio/scripts/run_zero123_demo.sh
Normal 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
|
||||
14
threestudio/scripts/run_zero123_phase.sh
Executable file
14
threestudio/scripts/run_zero123_phase.sh
Executable 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}
|
||||
5
threestudio/scripts/run_zero123_phase2.sh
Normal file
5
threestudio/scripts/run_zero123_phase2.sh
Normal 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
|
||||
54
threestudio/scripts/test_dreambooth.py
Normal file
54
threestudio/scripts/test_dreambooth.py
Normal 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"
|
||||
25
threestudio/scripts/test_dreambooth_lora.py
Normal file
25
threestudio/scripts/test_dreambooth_lora.py
Normal 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")
|
||||
1500
threestudio/scripts/train_dreambooth.py
Normal file
1500
threestudio/scripts/train_dreambooth.py
Normal file
File diff suppressed because it is too large
Load Diff
1480
threestudio/scripts/train_dreambooth_lora.py
Normal file
1480
threestudio/scripts/train_dreambooth_lora.py
Normal file
File diff suppressed because it is too large
Load Diff
927
threestudio/scripts/train_text_to_image_lora.py
Normal file
927
threestudio/scripts/train_text_to_image_lora.py
Normal 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"\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()
|
||||
1
threestudio/systems/__init__.py
Normal file
1
threestudio/systems/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import dreamcraft3d, zero123
|
||||
396
threestudio/systems/base.py
Normal file
396
threestudio/systems/base.py
Normal 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"],
|
||||
)
|
||||
608
threestudio/systems/dreamcraft3d.py
Normal file
608
threestudio/systems/dreamcraft3d.py
Normal 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
|
||||
104
threestudio/systems/utils.py
Normal file
104
threestudio/systems/utils.py
Normal 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
|
||||
390
threestudio/systems/zero123.py
Normal file
390
threestudio/systems/zero123.py
Normal 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")
|
||||
)
|
||||
278
threestudio/utils/GAN/attention.py
Normal file
278
threestudio/utils/GAN/attention.py
Normal 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
|
||||
217
threestudio/utils/GAN/discriminator.py
Normal file
217
threestudio/utils/GAN/discriminator.py
Normal 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)
|
||||
102
threestudio/utils/GAN/distribution.py
Normal file
102
threestudio/utils/GAN/distribution.py
Normal 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)
|
||||
)
|
||||
35
threestudio/utils/GAN/loss.py
Normal file
35
threestudio/utils/GAN/loss.py
Normal 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
|
||||
254
threestudio/utils/GAN/mobilenet.py
Normal file
254
threestudio/utils/GAN/mobilenet.py
Normal 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
|
||||
296
threestudio/utils/GAN/network_util.py
Normal file
296
threestudio/utils/GAN/network_util.py
Normal 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()
|
||||
401
threestudio/utils/GAN/normalunet.py
Normal file
401
threestudio/utils/GAN/normalunet.py
Normal 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
|
||||
208
threestudio/utils/GAN/util.py
Normal file
208
threestudio/utils/GAN/util.py
Normal 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
1028
threestudio/utils/GAN/vae.py
Normal file
File diff suppressed because it is too large
Load Diff
1
threestudio/utils/__init__.py
Normal file
1
threestudio/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import base
|
||||
118
threestudio/utils/base.py
Normal file
118
threestudio/utils/base.py
Normal 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
|
||||
156
threestudio/utils/callbacks.py
Normal file
156
threestudio/utils/callbacks.py
Normal 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
131
threestudio/utils/config.py
Normal 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
924
threestudio/utils/dpt.py
Normal 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)
|
||||
1
threestudio/utils/lpips/__init__.py
Normal file
1
threestudio/utils/lpips/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .lpips import LPIPS
|
||||
123
threestudio/utils/lpips/lpips.py
Normal file
123
threestudio/utils/lpips/lpips.py
Normal 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)
|
||||
|
||||
157
threestudio/utils/lpips/utils.py
Normal file
157
threestudio/utils/lpips/utils.py
Normal 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
Reference in New Issue
Block a user