codestella
code change
97ec1af
raw
history blame
22.8 kB
# coding=utf-8
# Copyright 2021 The Google Research Authors.
#
# 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.
# Lint as: python3
"""Different datasets implementation plus a general port for all the datasets."""
INTERNAL = False # pylint: disable=g-statement-before-imports
import json
import os, time
from os import path
import queue
import threading
if not INTERNAL:
import cv2 # pylint: disable=g-import-not-at-top
import jax
import numpy as np
from PIL import Image
from nerf import utils
from nerf import clip_utils
def get_dataset(split, args, clip_model = None):
return dataset_dict[args.dataset](split, args, clip_model)
def convert_to_ndc(origins, directions, focal, w, h, near=1.):
"""Convert a set of rays to NDC coordinates."""
# Shift ray origins to near plane
t = -(near + origins[..., 2]) / directions[..., 2]
origins = origins + t[..., None] * directions
dx, dy, dz = tuple(np.moveaxis(directions, -1, 0))
ox, oy, oz = tuple(np.moveaxis(origins, -1, 0))
# Projection
o0 = -((2 * focal) / w) * (ox / oz)
o1 = -((2 * focal) / h) * (oy / oz)
o2 = 1 + 2 * near / oz
d0 = -((2 * focal) / w) * (dx / dz - ox / oz)
d1 = -((2 * focal) / h) * (dy / dz - oy / oz)
d2 = -2 * near / oz
origins = np.stack([o0, o1, o2], -1)
directions = np.stack([d0, d1, d2], -1)
return origins, directions
class Dataset(threading.Thread):
"""Dataset Base Class."""
def __init__(self, split, flags, clip_model):
super(Dataset, self).__init__()
self.queue = queue.Queue(3) # Set prefetch buffer to 3 batches.
self.daemon = True
self.use_pixel_centers = flags.use_pixel_centers
self.split = split
if split == "train":
self._train_init(flags, clip_model)
elif split == "test":
self._test_init(flags)
else:
raise ValueError(
"the split argument should be either \"train\" or \"test\", set"
"to {} here.".format(split))
self.batch_size = flags.batch_size // jax.process_count()
self.batching = flags.batching
self.render_path = flags.render_path
self.far = flags.far
self.near = flags.near
self.max_steps = flags.max_steps
self.start()
def __iter__(self):
return self
def __next__(self):
"""Get the next training batch or test example.
Returns:
batch: dict, has "pixels" and "rays".
"""
x = self.queue.get()
if self.split == "train":
return utils.shard(x)
else:
return utils.to_device(x)
def peek(self):
"""Peek at the next training batch or test example without dequeuing it.
Returns:
batch: dict, has "pixels" and "rays".
"""
x = self.queue.queue[0].copy() # Make a copy of the front of the queue.
if self.split == "train":
return utils.shard(x)
else:
return utils.to_device(x)
def run(self):
if self.split == "train":
next_func = self._next_train
else:
next_func = self._next_test
while True:
self.queue.put(next_func())
@property
def size(self):
return self.n_examples
def _train_init(self, flags, clip_model):
"""Initialize training."""
self._load_renderings(flags, clip_model)
self._generate_rays()
if flags.batching == "all_images":
# flatten the ray and image dimension together.
self.images = self.images.reshape([-1, 3])
self.rays = utils.namedtuple_map(lambda r: r.reshape([-1, r.shape[-1]]),
self.rays)
elif flags.batching == "single_image":
self.images = self.images.reshape([-1, self.resolution, 3])
self.rays = utils.namedtuple_map(
lambda r: r.reshape([-1, self.resolution, r.shape[-1]]), self.rays)
else:
raise NotImplementedError(
f"{flags.batching} batching strategy is not implemented.")
def _test_init(self, flags):
self._load_renderings(flags, clip_model = None)
self._generate_rays()
self.it = 0
def _next_train(self):
"""Sample next training batch."""
if self.batching == "all_images":
ray_indices = np.random.randint(0, self.rays[0].shape[0],
(self.batch_size,))
batch_pixels = self.images[ray_indices]
batch_rays = utils.namedtuple_map(lambda r: r[ray_indices], self.rays)
raise NotImplementedError("image_index not implemented for batching=all_images")
elif self.batching == "single_image":
image_index = np.random.randint(0, self.n_examples, ())
ray_indices = np.random.randint(0, self.rays[0][0].shape[0],
(self.batch_size,))
batch_pixels = self.images[image_index][ray_indices]
batch_rays = utils.namedtuple_map(lambda r: r[image_index][ray_indices],
self.rays)
else:
raise NotImplementedError(
f"{self.batching} batching strategy is not implemented.")
return {"pixels": batch_pixels, "rays": batch_rays, "image_index": image_index}
def _next_test(self):
"""Sample next test example."""
idx = self.it
self.it = (self.it + 1) % self.n_examples
if self.render_path:
return {"rays": utils.namedtuple_map(lambda r: r[idx], self.render_rays)}
else:
return {"pixels": self.images[idx],
"rays": utils.namedtuple_map(lambda r: r[idx], self.rays),
"image_index": idx}
# TODO(bydeng): Swap this function with a more flexible camera model.
def _generate_rays(self):
"""Generating rays for all images."""
pixel_center = 0.5 if self.use_pixel_centers else 0.0
x, y = np.meshgrid( # pylint: disable=unbalanced-tuple-unpacking
np.arange(self.w, dtype=np.float32) + pixel_center, # X-Axis (columns)
np.arange(self.h, dtype=np.float32) + pixel_center, # Y-Axis (rows)
indexing="xy")
camera_dirs = np.stack([(x - self.w * 0.5) / self.focal,
-(y - self.h * 0.5) / self.focal, -np.ones_like(x)],
axis=-1)
directions = ((camera_dirs[None, ..., None, :] *
self.camtoworlds[:, None, None, :3, :3]).sum(axis=-1))
origins = np.broadcast_to(self.camtoworlds[:, None, None, :3, -1],
directions.shape)
viewdirs = directions / np.linalg.norm(directions, axis=-1, keepdims=True)
self.rays = utils.Rays(
origins=origins, directions=directions, viewdirs=viewdirs)
def camtoworld_matrix_to_rays(self, camtoworld, downsample = 1):
""" render one instance of rays given a camera to world matrix (4, 4) """
pixel_center = 0.5 if self.use_pixel_centers else 0.0
# TODO @Alex: apply mesh downsampling here
x, y = np.meshgrid( # pylint: disable=unbalanced-tuple-unpacking
np.arange(self.w, step = downsample, dtype=np.float32) + pixel_center, # X-Axis (columns)
np.arange(self.h, step = downsample, dtype=np.float32) + pixel_center, # Y-Axis (rows)
indexing="xy")
camera_dirs = np.stack([(x - self.w * 0.5) / self.focal,
-(y - self.h * 0.5) / self.focal, -np.ones_like(x)],
axis=-1)
directions = (camera_dirs[..., None, :] * camtoworld[None, None, :3, :3]).sum(axis=-1)
origins = np.broadcast_to(camtoworld[None, None, :3, -1], directions.shape)
viewdirs = directions / np.linalg.norm(directions, axis=-1, keepdims=True)
return utils.Rays(origins=origins, directions=directions, viewdirs=viewdirs)
class Blender(Dataset):
"""Blender Dataset."""
def _load_renderings(self, flags, clip_model = None):
"""Load images from disk."""
if flags.render_path:
raise ValueError("render_path cannot be used for the blender dataset.")
cams, images, meta = self.load_files(flags.data_dir, self.split, flags.factor, flags.few_shot)
self.images = np.stack(images, axis=0)
if flags.white_bkgd:
self.images = (self.images[..., :3] * self.images[..., -1:] +
(1. - self.images[..., -1:]))
else:
self.images = self.images[..., :3]
self.h, self.w = self.images.shape[1:3]
self.resolution = self.h * self.w
self.camtoworlds = np.stack(cams, axis=0)
camera_angle_x = float(meta["camera_angle_x"])
self.focal = .5 * self.w / np.tan(.5 * camera_angle_x)
self.n_examples = self.images.shape[0]
self.dtype = flags.clip_output_dtype
if flags.use_semantic_loss and clip_model is not None:
embs = []
for img in self.images:
img = np.expand_dims(np.transpose(img,[2,0,1]), 0)
emb = clip_model.get_image_features(pixel_values = clip_utils.preprocess_for_CLIP(img))
embs.append( emb/np.linalg.norm(emb) )
self.embeddings = np.concatenate(embs, 0)
self.image_idx = np.arange(self.images.shape[0])
np.random.shuffle(self.image_idx)
self.image_idx = self.image_idx.tolist()
@staticmethod
def load_files(data_dir, split, factor, few_shot):
with utils.open_file(path.join(data_dir, "transforms_{}.json".format(split)), "r") as fp:
meta = json.load(fp)
images = []
cams = []
frames = np.arange(len(meta["frames"]))
if few_shot > 0 and split == 'train':
# np.random.seed(0)
# np.random.shuffle(frames)
frames = frames[:few_shot]
# if split == 'train':
# frames = [2,5,10,40,52,53,69,78,83,85,90,94,96,97]
for i in frames:
frame = meta["frames"][i]
fname = os.path.join(data_dir, frame["file_path"] + ".png")
with utils.open_file(fname, "rb") as imgin:
image = np.array(Image.open(imgin)).astype(np.float32) / 255.
if factor == 2:
[halfres_h, halfres_w] = [hw // 2 for hw in image.shape[:2]]
image = cv2.resize(image, (halfres_w, halfres_h),
interpolation=cv2.INTER_AREA)
elif factor == 4:
[halfres_h, halfres_w] = [hw // 4 for hw in image.shape[:2]]
image = cv2.resize(image, (halfres_w, halfres_h),
interpolation=cv2.INTER_AREA)
elif factor > 0:
raise ValueError("Blender dataset only supports factor=0 or 2 or 4, {} "
"set.".format(factor))
cams.append(np.array(frame["transform_matrix"], dtype=np.float32))
images.append(image)
print(f'No. of samples: {len(frames)}')
return cams, images, meta
def _next_train(self):
batch_dict = super(Blender, self)._next_train()
if self.batching == "single_image":
image_index = batch_dict.pop("image_index")
else:
raise NotImplementedError
return batch_dict
def get_clip_data(self):
if len(self.image_idx) == 0:
self.image_idx = np.arange(self.images.shape[0])
np.random.shuffle(self.image_idx)
self.image_idx = self.image_idx.tolist()
image_index = self.image_idx.pop()
batch_dict = {}
batch_dict["embedding"] = self.embeddings[image_index]
src_seed = int(time.time())
src_rng = jax.random.PRNGKey(src_seed)
src_camtoworld = np.array(clip_utils.random_pose(src_rng, (self.near, self.far)))
cx = np.random.randint(320, 480)
cy = np.random.randint(320, 480)
d = 140
random_rays = self.camtoworld_matrix_to_rays(src_camtoworld, downsample = 1)
random_rays = jax.tree_map(lambda x: x[cy-d:cy+d:4,cx-d:cx+d:4], random_rays)
w = random_rays[0].shape[0] - random_rays[0].shape[0]%jax.local_device_count()
random_rays = jax.tree_map(lambda x: x[:w,:w].reshape(-1,3), random_rays)
batch_dict["random_rays"] = utils.shard(random_rays)
if self.dtype == 'float16':
batch_dict = jax.tree_map(lambda x: x.astype(np.float16), batch_dict)
return batch_dict
class LLFF(Dataset):
"""LLFF Dataset."""
def _load_renderings(self, flags):
"""Load images from disk."""
# Load images.
imgdir_suffix = ""
if flags.factor > 0:
imgdir_suffix = "_{}".format(flags.factor)
factor = flags.factor
else:
factor = 1
imgdir = path.join(flags.data_dir, "images" + imgdir_suffix)
if not utils.file_exists(imgdir):
raise ValueError("Image folder {} doesn't exist.".format(imgdir))
imgfiles = [
path.join(imgdir, f)
for f in sorted(utils.listdir(imgdir))
if f.endswith("JPG") or f.endswith("jpg") or f.endswith("png")
]
images = []
for imgfile in imgfiles:
with utils.open_file(imgfile, "rb") as imgin:
image = np.array(Image.open(imgin), dtype=np.float32) / 255.
images.append(image)
images = np.stack(images, axis=-1)
# Load poses and bds.
with utils.open_file(path.join(flags.data_dir, "poses_bounds.npy"),
"rb") as fp:
poses_arr = np.load(fp)
poses = poses_arr[:, :-2].reshape([-1, 3, 5]).transpose([1, 2, 0])
bds = poses_arr[:, -2:].transpose([1, 0])
if poses.shape[-1] != images.shape[-1]:
raise RuntimeError("Mismatch between imgs {} and poses {}".format(
images.shape[-1], poses.shape[-1]))
# Update poses according to downsampling.
poses[:2, 4, :] = np.array(images.shape[:2]).reshape([2, 1])
poses[2, 4, :] = poses[2, 4, :] * 1. / factor
# Correct rotation matrix ordering and move variable dim to axis 0.
poses = np.concatenate(
[poses[:, 1:2, :], -poses[:, 0:1, :], poses[:, 2:, :]], 1)
poses = np.moveaxis(poses, -1, 0).astype(np.float32)
images = np.moveaxis(images, -1, 0)
bds = np.moveaxis(bds, -1, 0).astype(np.float32)
# Rescale according to a default bd factor.
scale = 1. / (bds.min() * .75)
poses[:, :3, 3] *= scale
bds *= scale
# Recenter poses.
poses = self._recenter_poses(poses)
# Generate a spiral/spherical ray path for rendering videos.
if flags.spherify:
poses = self._generate_spherical_poses(poses, bds)
self.spherify = True
else:
self.spherify = False
if not flags.spherify and self.split == "test":
self._generate_spiral_poses(poses, bds)
# Select the split.
i_test = np.arange(images.shape[0])[::flags.llffhold]
i_train = np.array(
[i for i in np.arange(int(images.shape[0])) if i not in i_test])
if self.split == "train":
indices = i_train
else:
indices = i_test
images = images[indices]
poses = poses[indices]
self.images = images
self.camtoworlds = poses[:, :3, :4]
self.focal = poses[0, -1, -1]
self.h, self.w = images.shape[1:3]
self.resolution = self.h * self.w
if flags.render_path:
self.n_examples = self.render_poses.shape[0]
else:
self.n_examples = images.shape[0]
def _generate_rays(self):
"""Generate normalized device coordinate rays for llff."""
if self.split == "test":
n_render_poses = self.render_poses.shape[0]
self.camtoworlds = np.concatenate([self.render_poses, self.camtoworlds],
axis=0)
super()._generate_rays()
if not self.spherify:
ndc_origins, ndc_directions = convert_to_ndc(self.rays.origins,
self.rays.directions,
self.focal, self.w, self.h)
self.rays = utils.Rays(
origins=ndc_origins,
directions=ndc_directions,
viewdirs=self.rays.viewdirs)
# Split poses from the dataset and generated poses
if self.split == "test":
self.camtoworlds = self.camtoworlds[n_render_poses:]
split = [np.split(r, [n_render_poses], 0) for r in self.rays]
split0, split1 = zip(*split)
self.render_rays = utils.Rays(*split0)
self.rays = utils.Rays(*split1)
def _recenter_poses(self, poses):
"""Recenter poses according to the original NeRF code."""
poses_ = poses.copy()
bottom = np.reshape([0, 0, 0, 1.], [1, 4])
c2w = self._poses_avg(poses)
c2w = np.concatenate([c2w[:3, :4], bottom], -2)
bottom = np.tile(np.reshape(bottom, [1, 1, 4]), [poses.shape[0], 1, 1])
poses = np.concatenate([poses[:, :3, :4], bottom], -2)
poses = np.linalg.inv(c2w) @ poses
poses_[:, :3, :4] = poses[:, :3, :4]
poses = poses_
return poses
def _poses_avg(self, poses):
"""Average poses according to the original NeRF code."""
hwf = poses[0, :3, -1:]
center = poses[:, :3, 3].mean(0)
vec2 = self._normalize(poses[:, :3, 2].sum(0))
up = poses[:, :3, 1].sum(0)
c2w = np.concatenate([self._viewmatrix(vec2, up, center), hwf], 1)
return c2w
def _viewmatrix(self, z, up, pos):
"""Construct lookat view matrix."""
vec2 = self._normalize(z)
vec1_avg = up
vec0 = self._normalize(np.cross(vec1_avg, vec2))
vec1 = self._normalize(np.cross(vec2, vec0))
m = np.stack([vec0, vec1, vec2, pos], 1)
return m
def _normalize(self, x):
"""Normalization helper function."""
return x / np.linalg.norm(x)
def _generate_spiral_poses(self, poses, bds):
"""Generate a spiral path for rendering."""
c2w = self._poses_avg(poses)
# Get average pose.
up = self._normalize(poses[:, :3, 1].sum(0))
# Find a reasonable "focus depth" for this dataset.
close_depth, inf_depth = bds.min() * .9, bds.max() * 5.
dt = .75
mean_dz = 1. / (((1. - dt) / close_depth + dt / inf_depth))
focal = mean_dz
# Get radii for spiral path.
tt = poses[:, :3, 3]
rads = np.percentile(np.abs(tt), 90, 0)
c2w_path = c2w
n_views = 120
n_rots = 2
# Generate poses for spiral path.
render_poses = []
rads = np.array(list(rads) + [1.])
hwf = c2w_path[:, 4:5]
zrate = .5
for theta in np.linspace(0., 2. * np.pi * n_rots, n_views + 1)[:-1]:
c = np.dot(c2w[:3, :4], (np.array(
[np.cos(theta), -np.sin(theta), -np.sin(theta * zrate), 1.]) * rads))
z = self._normalize(c - np.dot(c2w[:3, :4], np.array([0, 0, -focal, 1.])))
render_poses.append(np.concatenate([self._viewmatrix(z, up, c), hwf], 1))
self.render_poses = np.array(render_poses).astype(np.float32)[:, :3, :4]
def _generate_spherical_poses(self, poses, bds):
"""Generate a 360 degree spherical path for rendering."""
# pylint: disable=g-long-lambda
p34_to_44 = lambda p: np.concatenate([
p,
np.tile(np.reshape(np.eye(4)[-1, :], [1, 1, 4]), [p.shape[0], 1, 1])
], 1)
rays_d = poses[:, :3, 2:3]
rays_o = poses[:, :3, 3:4]
def min_line_dist(rays_o, rays_d):
a_i = np.eye(3) - rays_d * np.transpose(rays_d, [0, 2, 1])
b_i = -a_i @ rays_o
pt_mindist = np.squeeze(-np.linalg.inv(
(np.transpose(a_i, [0, 2, 1]) @ a_i).mean(0)) @ (b_i).mean(0))
return pt_mindist
pt_mindist = min_line_dist(rays_o, rays_d)
center = pt_mindist
up = (poses[:, :3, 3] - center).mean(0)
vec0 = self._normalize(up)
vec1 = self._normalize(np.cross([.1, .2, .3], vec0))
vec2 = self._normalize(np.cross(vec0, vec1))
pos = center
c2w = np.stack([vec1, vec2, vec0, pos], 1)
poses_reset = (
np.linalg.inv(p34_to_44(c2w[None])) @ p34_to_44(poses[:, :3, :4]))
rad = np.sqrt(np.mean(np.sum(np.square(poses_reset[:, :3, 3]), -1)))
sc = 1. / rad
poses_reset[:, :3, 3] *= sc
bds *= sc
rad *= sc
centroid = np.mean(poses_reset[:, :3, 3], 0)
zh = centroid[2]
radcircle = np.sqrt(rad ** 2 - zh ** 2)
new_poses = []
for th in np.linspace(0., 2. * np.pi, 120):
camorigin = np.array([radcircle * np.cos(th), radcircle * np.sin(th), zh])
up = np.array([0, 0, -1.])
vec2 = self._normalize(camorigin)
vec0 = self._normalize(np.cross(vec2, up))
vec1 = self._normalize(np.cross(vec2, vec0))
pos = camorigin
p = np.stack([vec0, vec1, vec2, pos], 1)
new_poses.append(p)
new_poses = np.stack(new_poses, 0)
new_poses = np.concatenate([
new_poses,
np.broadcast_to(poses[0, :3, -1:], new_poses[:, :3, -1:].shape)
], -1)
poses_reset = np.concatenate([
poses_reset[:, :3, :4],
np.broadcast_to(poses[0, :3, -1:], poses_reset[:, :3, -1:].shape)
], -1)
if self.split == "test":
self.render_poses = new_poses[:, :3, :4]
return poses_reset
dataset_dict = {"blender": Blender,
"llff": LLFF}