Spaces:
Paused
Paused
import numpy as np | |
import os, imageio | |
import torch | |
import scipy | |
from tqdm import tqdm | |
########## Slightly modified version of LLFF data loading code | |
########## see https://github.com/Fyusion/LLFF for original | |
def imread(f): | |
if f.endswith('png'): | |
return imageio.imread(f, ignoregamma=True) | |
else: | |
return imageio.imread(f) | |
def depthread(path): | |
with open(path, "rb") as fid: | |
width, height, channels = np.genfromtxt(fid, delimiter="&", max_rows=1, | |
usecols=(0, 1, 2), dtype=int) | |
fid.seek(0) | |
num_delimiter = 0 | |
byte = fid.read(1) | |
while True: | |
if byte == b"&": | |
num_delimiter += 1 | |
if num_delimiter >= 3: | |
break | |
byte = fid.read(1) | |
array = np.fromfile(fid, np.float32) | |
array = array.reshape((width, height, channels), order="F") | |
return np.transpose(array, (1, 0, 2)).squeeze() | |
def _minify(basedir, factors=[], resolutions=[]): | |
needtoload = False | |
for r in factors: | |
imgdir = os.path.join(basedir, 'images_{}'.format(r)) | |
if not os.path.exists(imgdir): | |
needtoload = True | |
for r in resolutions: | |
imgdir = os.path.join(basedir, 'images_{}x{}'.format(r[1], r[0])) | |
if not os.path.exists(imgdir): | |
needtoload = True | |
if not needtoload: | |
return | |
from shutil import copy | |
from subprocess import check_output | |
imgdir = os.path.join(basedir, 'images') | |
imgs = [os.path.join(imgdir, f) for f in sorted(os.listdir(imgdir))] | |
imgs = [f for f in imgs if any([f.endswith(ex) for ex in ['JPG', 'jpg', 'png', 'jpeg', 'PNG']])] | |
imgdir_orig = imgdir | |
wd = os.getcwd() | |
for r in factors + resolutions: | |
if isinstance(r, int): | |
name = 'images_{}'.format(r) | |
resizearg = '{}%'.format(100./r) | |
else: | |
name = 'images_{}x{}'.format(r[1], r[0]) | |
resizearg = '{}x{}'.format(r[1], r[0]) | |
imgdir = os.path.join(basedir, name) | |
if os.path.exists(imgdir): | |
continue | |
print('Minifying', r, basedir) | |
os.makedirs(imgdir) | |
check_output('cp {}/* {}'.format(imgdir_orig, imgdir), shell=True) | |
ext = imgs[0].split('.')[-1] | |
args = ' '.join(['mogrify', '-resize', resizearg, '-format', 'png', '*.{}'.format(ext)]) | |
print(args) | |
os.chdir(imgdir) | |
check_output(args, shell=True) | |
os.chdir(wd) | |
if ext != 'png': | |
check_output('rm {}/*.{}'.format(imgdir, ext), shell=True) | |
print('Removed duplicates') | |
print('Done') | |
def _load_data(basedir, factor=None, width=None, height=None, load_imgs=True, load_depths=False, args=None): | |
poses_arr = np.load(os.path.join(basedir, 'poses_bounds.npy')) | |
if poses_arr.shape[1] == 17: | |
poses = poses_arr[:, :-2].reshape([-1, 3, 5]).transpose([1,2,0]) | |
elif poses_arr.shape[1] == 14: | |
poses = poses_arr[:, :-2].reshape([-1, 3, 4]).transpose([1,2,0]) | |
else: | |
raise NotImplementedError | |
bds = poses_arr[:, -2:].transpose([1,0]) | |
img0 = [os.path.join(basedir, 'images', f) for f in sorted(os.listdir(os.path.join(basedir, 'images'))) \ | |
if f.endswith('JPG') or f.endswith('jpg') or f.endswith('png')][0] | |
sh = imageio.imread(img0).shape | |
sfx = '' | |
if height is not None and width is not None: | |
_minify(basedir, resolutions=[[height, width]]) | |
sfx = '_{}x{}'.format(width, height) | |
elif factor is not None and factor != 1: | |
sfx = '_{}'.format(factor) | |
_minify(basedir, factors=[factor]) | |
factor = factor | |
elif height is not None: | |
factor = sh[0] / float(height) | |
width = int(sh[1] / factor) | |
_minify(basedir, resolutions=[[height, width]]) | |
sfx = '_{}x{}'.format(width, height) | |
elif width is not None: | |
factor = sh[1] / float(width) | |
height = int(sh[0] / factor) | |
_minify(basedir, resolutions=[[height, width]]) | |
sfx = '_{}x{}'.format(width, height) | |
else: | |
factor = 1 | |
imgdir = os.path.join(basedir, 'images' + sfx) | |
print(f'Loading images from {imgdir}') | |
if not os.path.exists(imgdir): | |
print( imgdir, 'does not exist, returning' ) | |
return | |
imgfiles = [os.path.join(imgdir, f) for f in sorted(os.listdir(imgdir)) if f.endswith('JPG') or f.endswith('jpg') or f.endswith('png')] | |
if poses.shape[-1] != len(imgfiles): | |
print() | |
print( 'Mismatch between imgs {} and poses {} !!!!'.format(len(imgfiles), poses.shape[-1]) ) | |
names = set(name[:-4] for name in np.load(os.path.join(basedir, 'poses_names.npy'))) | |
assert len(names) == poses.shape[-1] | |
print('Below failed files are skip due to SfM failure:') | |
new_imgfiles = [] | |
for i in imgfiles: | |
fname = os.path.split(i)[1][:-4] | |
if fname in names: | |
new_imgfiles.append(i) | |
else: | |
print('==>', i) | |
imgfiles = new_imgfiles | |
if len(imgfiles) < 3: | |
print('Too few images...') | |
import sys; sys.exit() | |
sh = imageio.imread(imgfiles[0]).shape | |
if poses.shape[1] == 4: | |
poses = np.concatenate([poses, np.zeros_like(poses[:,[0]])], 1) | |
poses[2, 4, :] = np.load(os.path.join(basedir, 'hwf_cxcy.npy'))[2] | |
poses[:2, 4, :] = np.array(sh[:2]).reshape([2, 1]) | |
poses[2, 4, :] = poses[2, 4, :] * 1./factor | |
if not load_imgs: | |
return poses, bds | |
imgs = imgs = [imread(f)[...,:3]/255. for f in tqdm(imgfiles)] | |
imgs = np.stack(imgs, -1) | |
height, width = imgs.shape[:2] | |
print('Loaded image data', imgs.shape, poses[:,-1,0]) | |
if not load_depths: | |
return poses, bds, imgs | |
depthdir = os.path.join(basedir, 'stereo', 'depth_maps') | |
assert os.path.exists(depthdir), f'Dir not found: {depthdir}' | |
depthfiles = [os.path.join(depthdir, f) for f in sorted(os.listdir(depthdir)) if f.endswith('.geometric.bin')] | |
assert poses.shape[-1] == len(depthfiles), 'Mismatch between imgs {} and poses {} !!!!'.format(len(depthfiles), poses.shape[-1]) | |
depths = [depthread(f) for f in depthfiles] | |
depths = np.stack(depths, -1) | |
print('Loaded depth data', depths.shape) | |
return poses, bds, imgs, depths | |
def normalize(x): | |
return x / np.linalg.norm(x) | |
def viewmatrix(z, up, pos): | |
vec2 = normalize(z) | |
vec1_avg = up | |
vec0 = normalize(np.cross(vec1_avg, vec2)) | |
vec1 = normalize(np.cross(vec2, vec0)) | |
m = np.stack([vec0, vec1, vec2, pos], 1) | |
return m | |
def ptstocam(pts, c2w): | |
tt = np.matmul(c2w[:3,:3].T, (pts-c2w[:3,3])[...,np.newaxis])[...,0] | |
return tt | |
def poses_avg(poses): | |
hwf = poses[0, :3, -1:] | |
center = poses[:, :3, 3].mean(0) | |
vec2 = normalize(poses[:, :3, 2].sum(0)) | |
up = poses[:, :3, 1].sum(0) | |
c2w = np.concatenate([viewmatrix(vec2, up, center), hwf], 1) | |
return c2w | |
def render_path_spiral(c2w, up, rads, focal, zdelta, zrate, rots, N): | |
render_poses = [] | |
rads = np.array(list(rads) + [1.]) | |
hwf = c2w[:,4:5] | |
for theta in np.linspace(0., 2. * np.pi * rots, N+1)[:-1]: | |
c = np.dot(c2w[:3,:4], np.array([np.cos(theta), -np.sin(theta), -np.sin(theta*zrate)*zdelta, 1.]) * rads) | |
z = normalize(c - np.dot(c2w[:3,:4], np.array([0,0,-focal, 1.]))) | |
render_poses.append(np.concatenate([viewmatrix(z, up, c), hwf], 1)) | |
return render_poses | |
def recenter_poses(poses): | |
poses_ = poses+0 | |
bottom = np.reshape([0,0,0,1.], [1,4]) | |
c2w = 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 rerotate_poses(poses): # [B, 3, 5] | |
poses = np.copy(poses) | |
centroid = poses[:,:3,3].mean(0) | |
poses[:,:3,3] = poses[:,:3,3] - centroid | |
# Find the minimum pca vector with minimum eigen value | |
x = poses[:,:,3] | |
mu = x.mean(0) | |
cov = np.cov((x-mu).T) | |
ev , eig = np.linalg.eig(cov) | |
cams_up = eig[:,np.argmin(ev)] # [3, ] | |
if cams_up[1] < 0: | |
cams_up = -cams_up | |
# Find rotation matrix that align cams_up with [0,1,0] | |
R = scipy.spatial.transform.Rotation.align_vectors( | |
[[0,1,0]], cams_up[None])[0].as_matrix() | |
# Apply rotation and add back the centroid position | |
poses[:,:3,:3] = R @ poses[:,:3,:3] | |
poses[:,:3,[3]] = R @ poses[:,:3,[3]] | |
poses[:,:3,3] = poses[:,:3,3] + centroid | |
return poses | |
##################### | |
def spherify_poses(poses, bds, depths): | |
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 = normalize(up) | |
vec1 = normalize(np.cross([.1,.2,.3], vec0)) | |
vec2 = 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]) | |
radius = np.sqrt(np.mean(np.sum(np.square(poses_reset[:,:3,3]), -1))) | |
sc = 1./radius | |
poses_reset[:,:3,3] *= sc | |
bds *= sc | |
radius *= sc | |
depths *= sc | |
poses_reset = np.concatenate([poses_reset[:,:3,:4], np.broadcast_to(poses[0,:3,-1:], poses_reset[:,:3,-1:].shape)], -1) | |
return poses_reset, radius, bds, depths | |
def load_llff_data(basedir, factor=8, width=None, height=None, | |
recenter=True, rerotate=True, | |
bd_factor=.75, spherify=False, path_zflat=False, load_depths=False, | |
movie_render_kwargs={}, args=None): | |
poses, bds, imgs = _load_data(basedir, factor=factor, width=width, height=height, | |
load_depths=load_depths, args=args) # factor=8 downsamples original imgs by 8x | |
print('Loaded', basedir, bds.min(), bds.max()) | |
if load_depths: | |
depths = depths[0] | |
else: | |
depths = 0 | |
# 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) | |
imgs = np.moveaxis(imgs, -1, 0).astype(np.float32) | |
images = imgs | |
bds = np.moveaxis(bds, -1, 0).astype(np.float32) | |
# Rescale if bd_factor is provided | |
if bds.min() < 0 and bd_factor is not None: | |
print('Found negative z values from SfM sparse points!?') | |
print('Please try bd_factor=None') | |
import sys; sys.exit() | |
sc = 1. if bd_factor is None else 1./(bds.min() * bd_factor) | |
poses[:,:3,3] *= sc # [B, 3, 5] | |
bds *= sc | |
depths *= sc | |
if recenter: | |
poses = recenter_poses(poses) # [B, 3, 5] | |
if spherify: | |
poses, radius, bds, depths = spherify_poses(poses, bds, depths) # [B, 3, 5] | |
if rerotate: | |
poses = rerotate_poses(poses) # [B, 3, 5] | |
### generate spiral poses for rendering fly-through movie | |
centroid = poses[:,:3,3].mean(0) | |
radcircle = movie_render_kwargs.get('scale_r', 1) * np.linalg.norm(poses[:,:3,3] - centroid, axis=-1).mean() | |
centroid[0] += movie_render_kwargs.get('shift_x', 0) | |
centroid[1] += movie_render_kwargs.get('shift_y', 0) | |
centroid[2] += movie_render_kwargs.get('shift_z', 0) | |
new_up_rad = movie_render_kwargs.get('pitch_deg', 0) * np.pi / 180 | |
target_y = radcircle * np.tan(new_up_rad) | |
render_poses = [] | |
for th in np.linspace(0., 2.*np.pi, 200): | |
camorigin = np.array([radcircle * np.cos(th), 0, radcircle * np.sin(th)]) | |
if movie_render_kwargs.get('flip_up', False): | |
up = np.array([0,1.,0]) | |
else: | |
up = np.array([0,-1.,0]) | |
vec2 = normalize(camorigin) | |
vec0 = normalize(np.cross(vec2, up)) | |
vec1 = normalize(np.cross(vec2, vec0)) | |
pos = camorigin + centroid | |
# rotate to align with new pitch rotation | |
lookat = -vec2 | |
lookat[1] = target_y | |
lookat = normalize(lookat) | |
vec2 = -lookat | |
vec1 = normalize(np.cross(vec2, vec0)) | |
p = np.stack([vec0, vec1, vec2, pos], 1) | |
render_poses.append(p) | |
render_poses = np.stack(render_poses, 0) | |
render_poses = np.concatenate([render_poses, np.broadcast_to(poses[0,:3,-1:], render_poses[:,:3,-1:].shape)], -1) | |
else: | |
c2w = poses_avg(poses) | |
print('recentered', c2w.shape) | |
print(c2w[:3,:4]) | |
## Get spiral | |
# Get average pose | |
up = 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 * movie_render_kwargs.get('scale_f', 1) | |
# Get radii for spiral path | |
zdelta = movie_render_kwargs.get('zdelta', 0.5) | |
zrate = movie_render_kwargs.get('zrate', 1.0) | |
tt = poses[:,:3,3] # ptstocam(poses[:3,3,:].T, c2w).T | |
rads = np.percentile(np.abs(tt), 90, 0) * movie_render_kwargs.get('scale_r', 1) | |
c2w_path = c2w | |
N_views = 120 | |
N_rots = movie_render_kwargs.get('N_rots', 1) | |
if path_zflat: | |
# zloc = np.percentile(tt, 10, 0)[2] | |
zloc = -close_depth * .1 | |
c2w_path[:3,3] = c2w_path[:3,3] + zloc * c2w_path[:3,2] | |
rads[2] = 0. | |
N_rots = 1 | |
N_views/=2 | |
# Generate poses for spiral path | |
render_poses = render_path_spiral(c2w_path, up, rads, focal, zdelta, zrate=zrate, rots=N_rots, N=N_views) | |
render_poses = torch.Tensor(render_poses) | |
c2w = poses_avg(poses) | |
print('Data:') | |
print(poses.shape, images.shape, bds.shape) | |
dists = np.sum(np.square(c2w[:3,3] - poses[:,:3,3]), -1) | |
i_test = np.argmin(dists) | |
print('HOLDOUT view is', i_test) | |
images = images.astype(np.float32) | |
poses = poses.astype(np.float32) | |
return images, depths, poses, bds, render_poses, i_test | |