Initial snapshot: hand motion pipeline (Dyn-HaMR + dex-retargeting + SPIDER)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 14:13:49 +08:00
commit 7e4ef6f98b
7002 changed files with 1452249 additions and 0 deletions
@@ -0,0 +1,58 @@
import torch
import torchvision.transforms as transforms
import numpy as np
import torch.nn.functional as F
class RGBDAugmentor:
""" perform augmentation on RGB-D video """
def __init__(self, crop_size):
self.crop_size = crop_size
self.augcolor = transforms.Compose([
transforms.ToPILImage(),
transforms.ColorJitter(brightness=0.25, contrast=0.25, saturation=0.25, hue=0.4/3.14),
transforms.RandomGrayscale(p=0.1),
transforms.ToTensor()])
self.max_scale = 0.25
def spatial_transform(self, images, depths, poses, intrinsics):
""" cropping and resizing """
ht, wd = images.shape[2:]
max_scale = self.max_scale
min_scale = np.log2(np.maximum(
(self.crop_size[0] + 1) / float(ht),
(self.crop_size[1] + 1) / float(wd)))
scale = 2 ** np.random.uniform(min_scale, max_scale)
intrinsics = scale * intrinsics
depths = depths.unsqueeze(dim=1)
images = F.interpolate(images, scale_factor=scale, mode='bilinear',
align_corners=False, recompute_scale_factor=True)
depths = F.interpolate(depths, scale_factor=scale, recompute_scale_factor=True)
# always perform center crop (TODO: try non-center crops)
y0 = (images.shape[2] - self.crop_size[0]) // 2
x0 = (images.shape[3] - self.crop_size[1]) // 2
intrinsics = intrinsics - torch.tensor([0.0, 0.0, x0, y0])
images = images[:, :, y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
depths = depths[:, :, y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
depths = depths.squeeze(dim=1)
return images, poses, depths, intrinsics
def color_transform(self, images):
""" color jittering """
num, ch, ht, wd = images.shape
images = images.permute(1, 2, 3, 0).reshape(ch, ht, wd*num)
images = 255 * self.augcolor(images[[2,1,0]] / 255.0)
return images[[2,1,0]].reshape(ch, ht, wd, num).permute(3,0,1,2).contiguous()
def __call__(self, images, poses, depths, intrinsics):
images = self.color_transform(images)
return self.spatial_transform(images, depths, poses, intrinsics)
@@ -0,0 +1,157 @@
import numpy as np
import torch
import torch.utils.data as data
import torch.nn.functional as F
import csv
import os
import cv2
import math
import random
import json
import pickle
import os.path as osp
from .augmentation import RGBDAugmentor
from .rgbd_utils import *
class RGBDDataset(data.Dataset):
def __init__(self, name, datapath, n_frames=4, crop_size=[384,512], fmin=8.0, fmax=75.0, do_aug=True):
""" Base class for RGBD dataset """
self.aug = None
self.root = datapath
self.name = name
self.n_frames = n_frames
self.fmin = fmin # exclude very easy examples
self.fmax = fmax # exclude very hard examples
if do_aug:
self.aug = RGBDAugmentor(crop_size=crop_size)
# building dataset is expensive, cache so only needs to be performed once
cur_path = osp.dirname(osp.abspath(__file__))
if not os.path.isdir(osp.join(cur_path, 'cache')):
os.mkdir(osp.join(cur_path, 'cache'))
cache_path = osp.join(cur_path, 'cache', '{}.pickle'.format(self.name))
if osp.isfile(cache_path):
scene_info = pickle.load(open(cache_path, 'rb'))[0]
else:
scene_info = self._build_dataset()
with open(cache_path, 'wb') as cachefile:
pickle.dump((scene_info,), cachefile)
self.scene_info = scene_info
self._build_dataset_index()
def _build_dataset_index(self):
self.dataset_index = []
for scene in self.scene_info:
if not self.__class__.is_test_scene(scene):
graph = self.scene_info[scene]['graph']
for i in graph:
if len(graph[i][0]) > self.n_frames:
self.dataset_index.append((scene, i))
else:
print("Reserving {} for validation".format(scene))
@staticmethod
def image_read(image_file):
return cv2.imread(image_file)
@staticmethod
def depth_read(depth_file):
return np.load(depth_file)
def build_frame_graph(self, poses, depths, intrinsics, f=16, max_flow=256):
""" compute optical flow distance between all pairs of frames """
def read_disp(fn):
depth = self.__class__.depth_read(fn)[f//2::f, f//2::f]
depth[depth < 0.01] = np.mean(depth)
return 1.0 / depth
poses = np.array(poses)
intrinsics = np.array(intrinsics) / f
disps = np.stack(list(map(read_disp, depths)), 0)
d = f * compute_distance_matrix_flow(poses, disps, intrinsics)
# uncomment for nice visualization
# import matplotlib.pyplot as plt
# plt.imshow(d)
# plt.show()
graph = {}
for i in range(d.shape[0]):
j, = np.where(d[i] < max_flow)
graph[i] = (j, d[i,j])
return graph
def __getitem__(self, index):
""" return training video """
index = index % len(self.dataset_index)
scene_id, ix = self.dataset_index[index]
frame_graph = self.scene_info[scene_id]['graph']
images_list = self.scene_info[scene_id]['images']
depths_list = self.scene_info[scene_id]['depths']
poses_list = self.scene_info[scene_id]['poses']
intrinsics_list = self.scene_info[scene_id]['intrinsics']
inds = [ ix ]
while len(inds) < self.n_frames:
# get other frames within flow threshold
k = (frame_graph[ix][1] > self.fmin) & (frame_graph[ix][1] < self.fmax)
frames = frame_graph[ix][0][k]
# prefer frames forward in time
if np.count_nonzero(frames[frames > ix]):
ix = np.random.choice(frames[frames > ix])
elif np.count_nonzero(frames):
ix = np.random.choice(frames)
inds += [ ix ]
images, depths, poses, intrinsics = [], [], [], []
for i in inds:
images.append(self.__class__.image_read(images_list[i]))
depths.append(self.__class__.depth_read(depths_list[i]))
poses.append(poses_list[i])
intrinsics.append(intrinsics_list[i])
images = np.stack(images).astype(np.float32)
depths = np.stack(depths).astype(np.float32)
poses = np.stack(poses).astype(np.float32)
intrinsics = np.stack(intrinsics).astype(np.float32)
images = torch.from_numpy(images).float()
images = images.permute(0, 3, 1, 2)
disps = torch.from_numpy(1.0 / depths)
poses = torch.from_numpy(poses)
intrinsics = torch.from_numpy(intrinsics)
if self.aug is not None:
images, poses, disps, intrinsics = \
self.aug(images, poses, disps, intrinsics)
# scale scene
if len(disps[disps>0.01]) > 0:
s = disps[disps>0.01].mean()
disps = disps / s
poses[...,:3] *= s
return images, poses, disps, intrinsics
def __len__(self):
return len(self.dataset_index)
def __imul__(self, x):
self.dataset_index *= x
return self
@@ -0,0 +1,82 @@
import pickle
import os
import os.path as osp
# RGBD-Dataset
from .tartan import TartanAir
from .stream import ImageStream
from .stream import StereoStream
from .stream import RGBDStream
# streaming datasets for inference
from .tartan import TartanAirStream
from .tartan import TartanAirTestStream
def dataset_factory(dataset_list, **kwargs):
""" create a combined dataset """
from torch.utils.data import ConcatDataset
dataset_map = { 'tartan': (TartanAir, ) }
db_list = []
for key in dataset_list:
# cache datasets for faster future loading
db = dataset_map[key][0](**kwargs)
print("Dataset {} has {} images".format(key, len(db)))
db_list.append(db)
return ConcatDataset(db_list)
def create_datastream(dataset_path, **kwargs):
""" create data_loader to stream images 1 by 1 """
from torch.utils.data import DataLoader
if osp.isfile(osp.join(dataset_path, 'calibration.txt')):
db = ETH3DStream(dataset_path, **kwargs)
elif osp.isdir(osp.join(dataset_path, 'image_left')):
db = TartanAirStream(dataset_path, **kwargs)
elif osp.isfile(osp.join(dataset_path, 'rgb.txt')):
db = TUMStream(dataset_path, **kwargs)
elif osp.isdir(osp.join(dataset_path, 'mav0')):
db = EurocStream(dataset_path, **kwargs)
elif osp.isfile(osp.join(dataset_path, 'calib.txt')):
db = KITTIStream(dataset_path, **kwargs)
else:
# db = TartanAirStream(dataset_path, **kwargs)
db = TartanAirTestStream(dataset_path, **kwargs)
stream = DataLoader(db, shuffle=False, batch_size=1, num_workers=4)
return stream
def create_imagestream(dataset_path, **kwargs):
""" create data_loader to stream images 1 by 1 """
from torch.utils.data import DataLoader
db = ImageStream(dataset_path, **kwargs)
return DataLoader(db, shuffle=False, batch_size=1, num_workers=4)
def create_stereostream(dataset_path, **kwargs):
""" create data_loader to stream images 1 by 1 """
from torch.utils.data import DataLoader
db = StereoStream(dataset_path, **kwargs)
return DataLoader(db, shuffle=False, batch_size=1, num_workers=4)
def create_rgbdstream(dataset_path, **kwargs):
""" create data_loader to stream images 1 by 1 """
from torch.utils.data import DataLoader
db = RGBDStream(dataset_path, **kwargs)
return DataLoader(db, shuffle=False, batch_size=1, num_workers=4)
@@ -0,0 +1,190 @@
import numpy as np
import os.path as osp
import torch
from lietorch import SE3
import geom.projective_ops as pops
from scipy.spatial.transform import Rotation
def parse_list(filepath, skiprows=0):
""" read list data """
data = np.loadtxt(filepath, delimiter=' ', dtype=np.unicode_, skiprows=skiprows)
return data
def associate_frames(tstamp_image, tstamp_depth, tstamp_pose, max_dt=1.0):
""" pair images, depths, and poses """
associations = []
for i, t in enumerate(tstamp_image):
if tstamp_pose is None:
j = np.argmin(np.abs(tstamp_depth - t))
if (np.abs(tstamp_depth[j] - t) < max_dt):
associations.append((i, j))
else:
j = np.argmin(np.abs(tstamp_depth - t))
k = np.argmin(np.abs(tstamp_pose - t))
if (np.abs(tstamp_depth[j] - t) < max_dt) and \
(np.abs(tstamp_pose[k] - t) < max_dt):
associations.append((i, j, k))
return associations
def loadtum(datapath, frame_rate=-1):
""" read video data in tum-rgbd format """
if osp.isfile(osp.join(datapath, 'groundtruth.txt')):
pose_list = osp.join(datapath, 'groundtruth.txt')
elif osp.isfile(osp.join(datapath, 'pose.txt')):
pose_list = osp.join(datapath, 'pose.txt')
else:
return None, None, None, None
image_list = osp.join(datapath, 'rgb.txt')
depth_list = osp.join(datapath, 'depth.txt')
calib_path = osp.join(datapath, 'calibration.txt')
intrinsic = None
if osp.isfile(calib_path):
intrinsic = np.loadtxt(calib_path, delimiter=' ')
intrinsic = intrinsic.astype(np.float64)
image_data = parse_list(image_list)
depth_data = parse_list(depth_list)
pose_data = parse_list(pose_list, skiprows=1)
pose_vecs = pose_data[:,1:].astype(np.float64)
tstamp_image = image_data[:,0].astype(np.float64)
tstamp_depth = depth_data[:,0].astype(np.float64)
tstamp_pose = pose_data[:,0].astype(np.float64)
associations = associate_frames(tstamp_image, tstamp_depth, tstamp_pose)
# print(len(tstamp_image))
# print(len(associations))
indicies = range(len(associations))[::5]
# indicies = [ 0 ]
# for i in range(1, len(associations)):
# t0 = tstamp_image[associations[indicies[-1]][0]]
# t1 = tstamp_image[associations[i][0]]
# if t1 - t0 > 1.0 / frame_rate:
# indicies += [ i ]
images, poses, depths, intrinsics, tstamps = [], [], [], [], []
for ix in indicies:
(i, j, k) = associations[ix]
images += [ osp.join(datapath, image_data[i,1]) ]
depths += [ osp.join(datapath, depth_data[j,1]) ]
poses += [ pose_vecs[k] ]
tstamps += [ tstamp_image[i] ]
if intrinsic is not None:
intrinsics += [ intrinsic ]
return images, depths, poses, intrinsics, tstamps
def all_pairs_distance_matrix(poses, beta=2.5):
""" compute distance matrix between all pairs of poses """
poses = np.array(poses, dtype=np.float32)
poses[:,:3] *= beta # scale to balence rot + trans
poses = SE3(torch.from_numpy(poses))
r = (poses[:,None].inv() * poses[None,:]).log()
return r.norm(dim=-1).cpu().numpy()
def pose_matrix_to_quaternion(pose):
""" convert 4x4 pose matrix to (t, q) """
q = Rotation.from_matrix(pose[:3, :3]).as_quat()
return np.concatenate([pose[:3, 3], q], axis=0)
def compute_distance_matrix_flow(poses, disps, intrinsics):
""" compute flow magnitude between all pairs of frames """
if not isinstance(poses, SE3):
poses = torch.from_numpy(poses).float().cuda()[None]
poses = SE3(poses).inv()
disps = torch.from_numpy(disps).float().cuda()[None]
intrinsics = torch.from_numpy(intrinsics).float().cuda()[None]
N = poses.shape[1]
ii, jj = torch.meshgrid(torch.arange(N), torch.arange(N))
ii = ii.reshape(-1).cuda()
jj = jj.reshape(-1).cuda()
MAX_FLOW = 100.0
matrix = np.zeros((N, N), dtype=np.float32)
s = 2048
for i in range(0, ii.shape[0], s):
flow1, val1 = pops.induced_flow(poses, disps, intrinsics, ii[i:i+s], jj[i:i+s])
flow2, val2 = pops.induced_flow(poses, disps, intrinsics, jj[i:i+s], ii[i:i+s])
flow = torch.stack([flow1, flow2], dim=2)
val = torch.stack([val1, val2], dim=2)
mag = flow.norm(dim=-1).clamp(max=MAX_FLOW)
mag = mag.view(mag.shape[1], -1)
val = val.view(val.shape[1], -1)
mag = (mag * val).mean(-1) / val.mean(-1)
mag[val.mean(-1) < 0.7] = np.inf
i1 = ii[i:i+s].cpu().numpy()
j1 = jj[i:i+s].cpu().numpy()
matrix[i1, j1] = mag.cpu().numpy()
return matrix
def compute_distance_matrix_flow2(poses, disps, intrinsics, beta=0.4):
""" compute flow magnitude between all pairs of frames """
# if not isinstance(poses, SE3):
# poses = torch.from_numpy(poses).float().cuda()[None]
# poses = SE3(poses).inv()
# disps = torch.from_numpy(disps).float().cuda()[None]
# intrinsics = torch.from_numpy(intrinsics).float().cuda()[None]
N = poses.shape[1]
ii, jj = torch.meshgrid(torch.arange(N), torch.arange(N))
ii = ii.reshape(-1)
jj = jj.reshape(-1)
MAX_FLOW = 128.0
matrix = np.zeros((N, N), dtype=np.float32)
s = 2048
for i in range(0, ii.shape[0], s):
flow1a, val1a = pops.induced_flow(poses, disps, intrinsics, ii[i:i+s], jj[i:i+s], tonly=True)
flow1b, val1b = pops.induced_flow(poses, disps, intrinsics, ii[i:i+s], jj[i:i+s])
flow2a, val2a = pops.induced_flow(poses, disps, intrinsics, jj[i:i+s], ii[i:i+s], tonly=True)
flow2b, val2b = pops.induced_flow(poses, disps, intrinsics, ii[i:i+s], jj[i:i+s])
flow1 = flow1a + beta * flow1b
val1 = val1a * val2b
flow2 = flow2a + beta * flow2b
val2 = val2a * val2b
flow = torch.stack([flow1, flow2], dim=2)
val = torch.stack([val1, val2], dim=2)
mag = flow.norm(dim=-1).clamp(max=MAX_FLOW)
mag = mag.view(mag.shape[1], -1)
val = val.view(val.shape[1], -1)
mag = (mag * val).mean(-1) / val.mean(-1)
mag[val.mean(-1) < 0.8] = np.inf
i1 = ii[i:i+s].cpu().numpy()
j1 = jj[i:i+s].cpu().numpy()
matrix[i1, j1] = mag.cpu().numpy()
return matrix
@@ -0,0 +1,234 @@
import numpy as np
import torch
import torch.utils.data as data
import torch.nn.functional as F
import csv
import os
import cv2
import math
import random
import json
import pickle
import os.path as osp
from .rgbd_utils import *
class RGBDStream(data.Dataset):
def __init__(self, datapath, frame_rate=-1, image_size=[384,512], crop_size=[0,0]):
self.datapath = datapath
self.frame_rate = frame_rate
self.image_size = image_size
self.crop_size = crop_size
self._build_dataset_index()
@staticmethod
def image_read(image_file):
return cv2.imread(image_file)
@staticmethod
def depth_read(depth_file):
return np.load(depth_file)
def __len__(self):
return len(self.images)
def __getitem__(self, index):
""" return training video """
image = self.__class__.image_read(self.images[index])
image = torch.from_numpy(image).float()
image = image.permute(2, 0, 1)
try:
tstamp = self.tstamps[index]
except:
tstamp = index
pose = torch.from_numpy(self.poses[index]).float()
intrinsic = torch.from_numpy(self.intrinsics[index]).float()
# resize image
sx = self.image_size[1] / image.shape[2]
sy = self.image_size[0] / image.shape[1]
image = F.interpolate(image[None], self.image_size, mode='bilinear', align_corners=False)[0]
fx, fy, cx, cy = intrinsic.unbind(dim=0)
fx, cx = sx * fx, sx * cx
fy, cy = sy * fy, sy * cy
# crop image
if self.crop_size[0] > 0:
cy = cy - self.crop_size[0]
image = image[:,self.crop_size[0]:-self.crop_size[0],:]
if self.crop_size[1] > 0:
cx = cx - self.crop_size[1]
image = image[:,:,self.crop_size[1]:-self.crop_size[1]]
intrinsic = torch.stack([fx, fy, cx, cy])
return tstamp, image, pose, intrinsic
class ImageStream(data.Dataset):
def __init__(self, datapath, intrinsics, rate=1, image_size=[384,512]):
rgb_list = osp.join(datapath, 'rgb.txt')
if os.path.isfile(rgb_list):
rgb_list = np.loadtxt(rgb_list, delimiter=' ', dtype=np.unicode_)
self.timestamps = rgb_list[:,0].astype(np.float)
self.images = [os.path.join(datapath, x) for x in rgb_list[:,1]]
self.images = self.images[::rate]
self.timestamps = self.timestamps[::rate]
else:
import glob
self.images = sorted(glob.glob(osp.join(datapath, '*.jpg'))) + sorted(glob.glob(osp.join(datapath, '*.png')))
self.images = self.images[::rate]
self.intrinsics = intrinsics
self.image_size = image_size
def __len__(self):
return len(self.images)
@staticmethod
def image_read(imfile):
return cv2.imread(imfile)
def __getitem__(self, index):
""" return training video """
image = self.__class__.image_read(self.images[index])
try:
tstamp = self.timestamps[index]
except:
tstamp = index
ht0, wd0 = image.shape[:2]
ht1, wd1 = self.image_size
intrinsics = torch.as_tensor(self.intrinsics)
intrinsics[0] *= wd1 / wd0
intrinsics[1] *= ht1 / ht0
intrinsics[2] *= wd1 / wd0
intrinsics[3] *= ht1 / ht0
# resize image
ikwargs = {'mode': 'bilinear', 'align_corners': True}
image = torch.from_numpy(image).float().permute(2, 0, 1)
image = F.interpolate(image[None], self.image_size, **ikwargs)[0]
return tstamp, image, intrinsics
class StereoStream(data.Dataset):
def __init__(self, datapath, intrinsics, rate=1, image_size=[384,512],
map_left=None, map_right=None, left_root='image_left', right_root='image_right'):
import glob
self.intrinsics = intrinsics
self.image_size = image_size
imgs = sorted(glob.glob(osp.join(datapath, left_root, '*.png')))[::rate]
self.images_l = []
self.images_r = []
self.tstamps = []
for img_l in imgs:
img_r = img_l.replace(left_root, right_root)
if os.path.isfile(img_r):
t = np.float(img_l.split('/')[-1].replace('.png', ''))
self.tstamps.append(t)
self.images_l += [ img_l ]
self.images_r += [ img_r ]
self.map_left = map_left
self.map_right = map_right
def __len__(self):
return len(self.images_l)
@staticmethod
def image_read(imfile, imap=None):
image = cv2.imread(imfile)
if imap is not None:
image = cv2.remap(image, imap[0], imap[1], interpolation=cv2.INTER_LINEAR)
return image
def __getitem__(self, index):
""" return training video """
tstamp = self.tstamps[index]
image_l = self.__class__.image_read(self.images_l[index], self.map_left)
image_r = self.__class__.image_read(self.images_r[index], self.map_right)
ht0, wd0 = image_l.shape[:2]
ht1, wd1 = self.image_size
intrinsics = torch.as_tensor(self.intrinsics)
intrinsics[0] *= wd1 / wd0
intrinsics[1] *= ht1 / ht0
intrinsics[2] *= wd1 / wd0
intrinsics[3] *= ht1 / ht0
image_l = torch.from_numpy(image_l).float().permute(2, 0, 1)
image_r = torch.from_numpy(image_r).float().permute(2, 0, 1)
# resize image
ikwargs = {'mode': 'bilinear', 'align_corners': True}
image_l = F.interpolate(image_l[None], self.image_size, **ikwargs)[0]
image_r = F.interpolate(image_r[None], self.image_size, **ikwargs)[0]
return tstamp, image_l, image_r, intrinsics
# class RGBDStream(data.Dataset):
# def __init__(self, datapath, intrinsics=None, rate=1, image_size=[384,512]):
# assoc_file = osp.join(datapath, 'associated.txt')
# assoc_list = np.loadtxt(assoc_file, delimiter=' ', dtype=np.unicode_)
# self.intrinsics = intrinsics
# self.image_size = image_size
# self.timestamps = assoc_list[:,0].astype(np.float)[::rate]
# self.images = [os.path.join(datapath, x) for x in assoc_list[:,1]][::rate]
# self.depths = [os.path.join(datapath, x) for x in assoc_list[:,3]][::rate]
# def __len__(self):
# return len(self.images)
# @staticmethod
# def image_read(imfile):
# return cv2.imread(imfile)
# @staticmethod
# def depth_read(depth_file):
# depth = cv2.imread(depth_file, cv2.IMREAD_ANYDEPTH)
# return depth.astype(np.float32) / 5000.0
# def __getitem__(self, index):
# """ return training video """
# tstamp = self.timestamps[index]
# image = self.__class__.image_read(self.images[index])
# depth = self.__class__.depth_read(self.depths[index])
# ht0, wd0 = image.shape[:2]
# ht1, wd1 = self.image_size
# intrinsics = torch.as_tensor(self.intrinsics)
# intrinsics[0] *= wd1 / wd0
# intrinsics[1] *= ht1 / ht0
# intrinsics[2] *= wd1 / wd0
# intrinsics[3] *= ht1 / ht0
# # resize image
# ikwargs = {'mode': 'bilinear', 'align_corners': True}
# image = torch.from_numpy(image).float().permute(2, 0, 1)
# image = F.interpolate(image[None], self.image_size, **ikwargs)[0]
# depth = torch.from_numpy(depth).float()[None,None]
# depth = F.interpolate(depth, self.image_size, mode='nearest').squeeze()
# return tstamp, image, depth, intrinsics
@@ -0,0 +1,138 @@
import numpy as np
import torch
import glob
import cv2
import os
import os.path as osp
from lietorch import SE3
from .base import RGBDDataset
from .stream import RGBDStream
cur_path = osp.dirname(osp.abspath(__file__))
test_split = osp.join(cur_path, 'tartan_test.txt')
test_split = open(test_split).read().split()
class TartanAir(RGBDDataset):
# scale depths to balance rot & trans
DEPTH_SCALE = 5.0
def __init__(self, mode='training', **kwargs):
self.mode = mode
self.n_frames = 2
super(TartanAir, self).__init__(name='TartanAir', **kwargs)
@staticmethod
def is_test_scene(scene):
# print(scene, any(x in scene for x in test_split))
return any(x in scene for x in test_split)
def _build_dataset(self):
from tqdm import tqdm
print("Building TartanAir dataset")
scene_info = {}
scenes = glob.glob(osp.join(self.root, '*/*/*/*'))
for scene in tqdm(sorted(scenes)):
images = sorted(glob.glob(osp.join(scene, 'image_left/*.png')))
depths = sorted(glob.glob(osp.join(scene, 'depth_left/*.npy')))
poses = np.loadtxt(osp.join(scene, 'pose_left.txt'), delimiter=' ')
poses = poses[:, [1, 2, 0, 4, 5, 3, 6]]
poses[:,:3] /= TartanAir.DEPTH_SCALE
intrinsics = [TartanAir.calib_read()] * len(images)
# graph of co-visible frames based on flow
graph = self.build_frame_graph(poses, depths, intrinsics)
scene = '/'.join(scene.split('/'))
scene_info[scene] = {'images': images, 'depths': depths,
'poses': poses, 'intrinsics': intrinsics, 'graph': graph}
return scene_info
@staticmethod
def calib_read():
return np.array([320.0, 320.0, 320.0, 240.0])
@staticmethod
def image_read(image_file):
return cv2.imread(image_file)
@staticmethod
def depth_read(depth_file):
depth = np.load(depth_file) / TartanAir.DEPTH_SCALE
depth[depth==np.nan] = 1.0
depth[depth==np.inf] = 1.0
return depth
class TartanAirStream(RGBDStream):
def __init__(self, datapath, **kwargs):
super(TartanAirStream, self).__init__(datapath=datapath, **kwargs)
def _build_dataset_index(self):
""" build list of images, poses, depths, and intrinsics """
self.root = 'datasets/TartanAir'
scene = osp.join(self.root, self.datapath)
image_glob = osp.join(scene, 'image_left/*.png')
images = sorted(glob.glob(image_glob))
poses = np.loadtxt(osp.join(scene, 'pose_left.txt'), delimiter=' ')
poses = poses[:, [1, 2, 0, 4, 5, 3, 6]]
poses = SE3(torch.as_tensor(poses))
poses = poses[[0]].inv() * poses
poses = poses.data.cpu().numpy()
intrinsic = self.calib_read(self.datapath)
intrinsics = np.tile(intrinsic[None], (len(images), 1))
self.images = images[::int(self.frame_rate)]
self.poses = poses[::int(self.frame_rate)]
self.intrinsics = intrinsics[::int(self.frame_rate)]
@staticmethod
def calib_read(datapath):
return np.array([320.0, 320.0, 320.0, 240.0])
@staticmethod
def image_read(image_file):
return cv2.imread(image_file)
class TartanAirTestStream(RGBDStream):
def __init__(self, datapath, **kwargs):
super(TartanAirTestStream, self).__init__(datapath=datapath, **kwargs)
def _build_dataset_index(self):
""" build list of images, poses, depths, and intrinsics """
self.root = 'datasets/mono'
image_glob = osp.join(self.root, self.datapath, '*.png')
images = sorted(glob.glob(image_glob))
poses = np.loadtxt(osp.join(self.root, 'mono_gt', self.datapath + '.txt'), delimiter=' ')
poses = poses[:, [1, 2, 0, 4, 5, 3, 6]]
poses = SE3(torch.as_tensor(poses))
poses = poses[[0]].inv() * poses
poses = poses.data.cpu().numpy()
intrinsic = self.calib_read(self.datapath)
intrinsics = np.tile(intrinsic[None], (len(images), 1))
self.images = images[::int(self.frame_rate)]
self.poses = poses[::int(self.frame_rate)]
self.intrinsics = intrinsics[::int(self.frame_rate)]
@staticmethod
def calib_read(datapath):
return np.array([320.0, 320.0, 320.0, 240.0])
@staticmethod
def image_read(image_file):
return cv2.imread(image_file)
@@ -0,0 +1,32 @@
abandonedfactory/abandonedfactory/Easy/P011
abandonedfactory/abandonedfactory/Hard/P011
abandonedfactory_night/abandonedfactory_night/Easy/P013
abandonedfactory_night/abandonedfactory_night/Hard/P014
amusement/amusement/Easy/P008
amusement/amusement/Hard/P007
carwelding/carwelding/Easy/P007
endofworld/endofworld/Easy/P009
gascola/gascola/Easy/P008
gascola/gascola/Hard/P009
hospital/hospital/Easy/P036
hospital/hospital/Hard/P049
japanesealley/japanesealley/Easy/P007
japanesealley/japanesealley/Hard/P005
neighborhood/neighborhood/Easy/P021
neighborhood/neighborhood/Hard/P017
ocean/ocean/Easy/P013
ocean/ocean/Hard/P009
office2/office2/Easy/P011
office2/office2/Hard/P010
office/office/Hard/P007
oldtown/oldtown/Easy/P007
oldtown/oldtown/Hard/P008
seasidetown/seasidetown/Easy/P009
seasonsforest/seasonsforest/Easy/P011
seasonsforest/seasonsforest/Hard/P006
seasonsforest_winter/seasonsforest_winter/Easy/P009
seasonsforest_winter/seasonsforest_winter/Hard/P018
soulcity/soulcity/Easy/P012
soulcity/soulcity/Hard/P009
westerndesert/westerndesert/Easy/P013
westerndesert/westerndesert/Hard/P007
@@ -0,0 +1,193 @@
import numpy as np
import torch
import lietorch
import droid_backends
from torch.multiprocessing import Process, Queue, Lock, Value
from collections import OrderedDict
from droid_net import cvx_upsample
import geom.projective_ops as pops
class DepthVideo:
def __init__(self, image_size=[480, 640], buffer=1024, stereo=False, device="cuda:0"):
# current keyframe count
self.counter = Value('i', 0)
self.ready = Value('i', 0)
self.ht = ht = image_size[0]
self.wd = wd = image_size[1]
### state attributes ###
self.tstamp = torch.zeros(buffer, device="cuda", dtype=torch.float).share_memory_()
self.images = torch.zeros(buffer, 3, ht, wd, device="cuda", dtype=torch.uint8)
self.dirty = torch.zeros(buffer, device="cuda", dtype=torch.bool).share_memory_()
self.red = torch.zeros(buffer, device="cuda", dtype=torch.bool).share_memory_()
self.poses = torch.zeros(buffer, 7, device="cuda", dtype=torch.float).share_memory_()
self.disps = torch.ones(buffer, ht//8, wd//8, device="cuda", dtype=torch.float).share_memory_()
self.disps_sens = torch.zeros(buffer, ht//8, wd//8, device="cuda", dtype=torch.float).share_memory_()
self.disps_up = torch.zeros(buffer, ht, wd, device="cuda", dtype=torch.float).share_memory_()
self.intrinsics = torch.zeros(buffer, 4, device="cuda", dtype=torch.float).share_memory_()
self.stereo = stereo
c = 1 if not self.stereo else 2
### feature attributes ###
self.fmaps = torch.zeros(buffer, c, 128, ht//8, wd//8, dtype=torch.half, device="cuda").share_memory_()
self.nets = torch.zeros(buffer, 128, ht//8, wd//8, dtype=torch.half, device="cuda").share_memory_()
self.inps = torch.zeros(buffer, 128, ht//8, wd//8, dtype=torch.half, device="cuda").share_memory_()
# initialize poses to identity transformation
self.poses[:] = torch.as_tensor([0, 0, 0, 0, 0, 0, 1], dtype=torch.float, device="cuda")
def get_lock(self):
return self.counter.get_lock()
def __item_setter(self, index, item):
if isinstance(index, int) and index >= self.counter.value:
self.counter.value = index + 1
elif isinstance(index, torch.Tensor) and index.max().item() > self.counter.value:
self.counter.value = index.max().item() + 1
# self.dirty[index] = True
self.tstamp[index] = item[0]
self.images[index] = item[1]
if item[2] is not None:
self.poses[index] = item[2]
if item[3] is not None:
self.disps[index] = item[3]
if item[4] is not None:
depth = item[4][3::8,3::8]
self.disps_sens[index] = torch.where(depth>0, 1.0/depth, depth)
if item[5] is not None:
self.intrinsics[index] = item[5]
if len(item) > 6:
self.fmaps[index] = item[6]
if len(item) > 7:
self.nets[index] = item[7]
if len(item) > 8:
self.inps[index] = item[8]
def __setitem__(self, index, item):
with self.get_lock():
self.__item_setter(index, item)
def __getitem__(self, index):
""" index the depth video """
with self.get_lock():
# support negative indexing
if isinstance(index, int) and index < 0:
index = self.counter.value + index
item = (
self.poses[index],
self.disps[index],
self.intrinsics[index],
self.fmaps[index],
self.nets[index],
self.inps[index])
return item
def append(self, *item):
with self.get_lock():
self.__item_setter(self.counter.value, item)
### geometric operations ###
@staticmethod
def format_indicies(ii, jj):
""" to device, long, {-1} """
if not isinstance(ii, torch.Tensor):
ii = torch.as_tensor(ii)
if not isinstance(jj, torch.Tensor):
jj = torch.as_tensor(jj)
ii = ii.to(device="cuda", dtype=torch.long).reshape(-1)
jj = jj.to(device="cuda", dtype=torch.long).reshape(-1)
return ii, jj
def upsample(self, ix, mask):
""" upsample disparity """
disps_up = cvx_upsample(self.disps[ix].unsqueeze(-1), mask)
self.disps_up[ix] = disps_up.squeeze()
def normalize(self):
""" normalize depth and poses """
with self.get_lock():
s = self.disps[:self.counter.value].mean()
self.disps[:self.counter.value] /= s
self.poses[:self.counter.value,:3] *= s
self.dirty[:self.counter.value] = True
def reproject(self, ii, jj):
""" project points from ii -> jj """
ii, jj = DepthVideo.format_indicies(ii, jj)
Gs = lietorch.SE3(self.poses[None])
coords, valid_mask = \
pops.projective_transform(Gs, self.disps[None], self.intrinsics[None], ii, jj)
return coords, valid_mask
def distance(self, ii=None, jj=None, beta=0.3, bidirectional=True):
""" frame distance metric """
return_matrix = False
if ii is None:
return_matrix = True
N = self.counter.value
ii, jj = torch.meshgrid(torch.arange(N), torch.arange(N))
ii, jj = DepthVideo.format_indicies(ii, jj)
if bidirectional:
poses = self.poses[:self.counter.value].clone()
d1 = droid_backends.frame_distance(
poses, self.disps, self.intrinsics[0], ii, jj, beta)
d2 = droid_backends.frame_distance(
poses, self.disps, self.intrinsics[0], jj, ii, beta)
d = .5 * (d1 + d2)
else:
d = droid_backends.frame_distance(
self.poses, self.disps, self.intrinsics[0], ii, jj, beta)
if return_matrix:
return d.reshape(N, N)
return d
def ba(self, target, weight, eta, ii, jj, t0=1, t1=None, itrs=2, lm=1e-4, ep=0.1, motion_only=False):
""" dense bundle adjustment (DBA) """
with self.get_lock():
# [t0, t1] window of bundle adjustment optimization
if t1 is None:
t1 = max(ii.max().item(), jj.max().item()) + 1
droid_backends.ba(self.poses, self.disps, self.intrinsics[0], self.disps_sens,
target, weight, eta, ii, jj, t0, t1, itrs, lm, ep, motion_only)
self.disps.clamp_(min=0.001)
@@ -0,0 +1,89 @@
import torch
import lietorch
import numpy as np
from droid_net import DroidNet
from depth_video import DepthVideo
from motion_filter import MotionFilter
from droid_frontend import DroidFrontend
from droid_backend import DroidBackend
from trajectory_filler import PoseTrajectoryFiller
from collections import OrderedDict
from torch.multiprocessing import Process
class Droid:
def __init__(self, args):
super(Droid, self).__init__()
self.load_weights(args.weights)
self.args = args
self.disable_vis = args.disable_vis
# store images, depth, poses, intrinsics (shared between processes)
self.video = DepthVideo(args.image_size, args.buffer, stereo=args.stereo)
# filter incoming frames so that there is enough motion
self.filterx = MotionFilter(self.net, self.video, thresh=args.filter_thresh)
# frontend process
self.frontend = DroidFrontend(self.net, self.video, self.args)
# backend process
self.backend = DroidBackend(self.net, self.video, self.args)
# visualizer
if not self.disable_vis:
from visualization import droid_visualization
self.visualizer = Process(target=droid_visualization, args=(self.video,))
self.visualizer.start()
# post processor - fill in poses for non-keyframes
self.traj_filler = PoseTrajectoryFiller(self.net, self.video)
def load_weights(self, weights):
""" load trained model weights """
print(weights)
self.net = DroidNet()
state_dict = OrderedDict([
(k.replace("module.", ""), v) for (k, v) in torch.load(weights).items()])
state_dict["update.weight.2.weight"] = state_dict["update.weight.2.weight"][:2]
state_dict["update.weight.2.bias"] = state_dict["update.weight.2.bias"][:2]
state_dict["update.delta.2.weight"] = state_dict["update.delta.2.weight"][:2]
state_dict["update.delta.2.bias"] = state_dict["update.delta.2.bias"][:2]
self.net.load_state_dict(state_dict)
self.net.to("cuda:0").eval()
def track(self, tstamp, image, depth=None, intrinsics=None):
""" main thread - update map """
with torch.no_grad():
# check there is enough motion
self.filterx.track(tstamp, image, depth, intrinsics)
# local bundle adjustment
self.frontend()
# global bundle adjustment
# self.backend()
def terminate(self, stream=None):
""" terminate the visualization process, return poses [t, q] """
del self.frontend
torch.cuda.empty_cache()
print("#" * 32)
self.backend(7)
torch.cuda.empty_cache()
print("#" * 32)
self.backend(12)
camera_trajectory = self.traj_filler(stream)
return camera_trajectory.inv().data.cpu().numpy()
@@ -0,0 +1,41 @@
import torch
import lietorch
import numpy as np
from lietorch import SE3
from factor_graph import FactorGraph
class DroidBackend:
def __init__(self, net, video, args):
self.video = video
self.update_op = net.update
# global optimization window
self.t0 = 0
self.t1 = 0
self.upsample = args.upsample
self.beta = args.beta
self.backend_thresh = args.backend_thresh
self.backend_radius = args.backend_radius
self.backend_nms = args.backend_nms
@torch.no_grad()
def __call__(self, steps=12):
""" main update """
t = self.video.counter.value
if not self.video.stereo and not torch.any(self.video.disps_sens):
self.video.normalize()
graph = FactorGraph(self.video, self.update_op, corr_impl="alt", max_factors=16*t, upsample=self.upsample)
graph.add_proximity_factors(rad=self.backend_radius,
nms=self.backend_nms,
thresh=self.backend_thresh,
beta=self.beta)
graph.update_lowmem(steps=steps)
graph.clear_edges()
self.video.dirty[:t] = True
@@ -0,0 +1,119 @@
import torch
import lietorch
import numpy as np
from lietorch import SE3
from factor_graph import FactorGraph
class DroidFrontend:
def __init__(self, net, video, args):
self.video = video
self.update_op = net.update
self.graph = FactorGraph(video, net.update, max_factors=48, upsample=args.upsample)
# local optimization window
self.t0 = 0
self.t1 = 0
# frontent variables
self.is_initialized = False
self.count = 0
self.max_age = 25
self.iters1 = 4
self.iters2 = 2
self.warmup = args.warmup
self.beta = args.beta
self.frontend_nms = args.frontend_nms
self.keyframe_thresh = args.keyframe_thresh
self.frontend_window = args.frontend_window
self.frontend_thresh = args.frontend_thresh
self.frontend_radius = args.frontend_radius
def __update(self):
""" add edges, perform update """
self.count += 1
self.t1 += 1
if self.graph.corr is not None:
self.graph.rm_factors(self.graph.age > self.max_age, store=True)
self.graph.add_proximity_factors(self.t1-5, max(self.t1-self.frontend_window, 0),
rad=self.frontend_radius, nms=self.frontend_nms, thresh=self.frontend_thresh, beta=self.beta, remove=True)
self.video.disps[self.t1-1] = torch.where(self.video.disps_sens[self.t1-1] > 0,
self.video.disps_sens[self.t1-1], self.video.disps[self.t1-1])
for itr in range(self.iters1):
self.graph.update(None, None, use_inactive=True)
# set initial pose for next frame
poses = SE3(self.video.poses)
d = self.video.distance([self.t1-3], [self.t1-2], beta=self.beta, bidirectional=True)
if d.item() < self.keyframe_thresh:
self.graph.rm_keyframe(self.t1 - 2)
with self.video.get_lock():
self.video.counter.value -= 1
self.t1 -= 1
else:
for itr in range(self.iters2):
self.graph.update(None, None, use_inactive=True)
# set pose for next itration
self.video.poses[self.t1] = self.video.poses[self.t1-1]
self.video.disps[self.t1] = self.video.disps[self.t1-1].mean()
# update visualization
self.video.dirty[self.graph.ii.min():self.t1] = True
def __initialize(self):
""" initialize the SLAM system """
self.t0 = 0
self.t1 = self.video.counter.value
self.graph.add_neighborhood_factors(self.t0, self.t1, r=3)
for itr in range(8):
self.graph.update(1, use_inactive=True)
self.graph.add_proximity_factors(0, 0, rad=2, nms=2, thresh=self.frontend_thresh, remove=False)
for itr in range(8):
self.graph.update(1, use_inactive=True)
# self.video.normalize()
self.video.poses[self.t1] = self.video.poses[self.t1-1].clone()
self.video.disps[self.t1] = self.video.disps[self.t1-4:self.t1].mean()
# initialization complete
self.is_initialized = True
self.last_pose = self.video.poses[self.t1-1].clone()
self.last_disp = self.video.disps[self.t1-1].clone()
self.last_time = self.video.tstamp[self.t1-1].clone()
with self.video.get_lock():
self.video.ready.value = 1
self.video.dirty[:self.t1] = True
self.graph.rm_factors(self.graph.ii < self.warmup-4, store=True)
def __call__(self):
""" main update """
# do initialization
if not self.is_initialized and self.video.counter.value == self.warmup:
self.__initialize()
# do update
elif self.is_initialized and self.t1 < self.video.counter.value:
self.__update()
@@ -0,0 +1,222 @@
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import OrderedDict
from modules.extractor import BasicEncoder
from modules.corr import CorrBlock
from modules.gru import ConvGRU
from modules.clipping import GradientClip
from lietorch import SE3
from geom.ba import BA
import geom.projective_ops as pops
from geom.graph_utils import graph_to_edge_list, keyframe_indicies
from torch_scatter import scatter_mean
def cvx_upsample(data, mask):
""" upsample pixel-wise transformation field """
batch, ht, wd, dim = data.shape
data = data.permute(0, 3, 1, 2)
mask = mask.view(batch, 1, 9, 8, 8, ht, wd)
mask = torch.softmax(mask, dim=2)
up_data = F.unfold(data, [3,3], padding=1)
up_data = up_data.view(batch, dim, 9, 1, 1, ht, wd)
up_data = torch.sum(mask * up_data, dim=2)
up_data = up_data.permute(0, 4, 2, 5, 3, 1)
up_data = up_data.reshape(batch, 8*ht, 8*wd, dim)
return up_data
def upsample_disp(disp, mask):
batch, num, ht, wd = disp.shape
disp = disp.view(batch*num, ht, wd, 1)
mask = mask.view(batch*num, -1, ht, wd)
return cvx_upsample(disp, mask).view(batch, num, 8*ht, 8*wd)
class GraphAgg(nn.Module):
def __init__(self):
super(GraphAgg, self).__init__()
self.conv1 = nn.Conv2d(128, 128, 3, padding=1)
self.conv2 = nn.Conv2d(128, 128, 3, padding=1)
self.relu = nn.ReLU(inplace=True)
self.eta = nn.Sequential(
nn.Conv2d(128, 1, 3, padding=1),
GradientClip(),
nn.Softplus())
self.upmask = nn.Sequential(
nn.Conv2d(128, 8*8*9, 1, padding=0))
def forward(self, net, ii):
batch, num, ch, ht, wd = net.shape
net = net.view(batch*num, ch, ht, wd)
_, ix = torch.unique(ii, return_inverse=True)
net = self.relu(self.conv1(net))
net = net.view(batch, num, 128, ht, wd)
net = scatter_mean(net, ix, dim=1)
net = net.view(-1, 128, ht, wd)
net = self.relu(self.conv2(net))
eta = self.eta(net).view(batch, -1, ht, wd)
upmask = self.upmask(net).view(batch, -1, 8*8*9, ht, wd)
return .01 * eta, upmask
class UpdateModule(nn.Module):
def __init__(self):
super(UpdateModule, self).__init__()
cor_planes = 4 * (2*3 + 1)**2
self.corr_encoder = nn.Sequential(
nn.Conv2d(cor_planes, 128, 1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU(inplace=True))
self.flow_encoder = nn.Sequential(
nn.Conv2d(4, 128, 7, padding=3),
nn.ReLU(inplace=True),
nn.Conv2d(128, 64, 3, padding=1),
nn.ReLU(inplace=True))
self.weight = nn.Sequential(
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 2, 3, padding=1),
GradientClip(),
nn.Sigmoid())
self.delta = nn.Sequential(
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 2, 3, padding=1),
GradientClip())
self.gru = ConvGRU(128, 128+128+64)
self.agg = GraphAgg()
def forward(self, net, inp, corr, flow=None, ii=None, jj=None):
""" RaftSLAM update operator """
batch, num, ch, ht, wd = net.shape
if flow is None:
flow = torch.zeros(batch, num, 4, ht, wd, device=net.device)
output_dim = (batch, num, -1, ht, wd)
net = net.view(batch*num, -1, ht, wd)
inp = inp.view(batch*num, -1, ht, wd)
corr = corr.view(batch*num, -1, ht, wd)
flow = flow.view(batch*num, -1, ht, wd)
corr = self.corr_encoder(corr)
flow = self.flow_encoder(flow)
net = self.gru(net, inp, corr, flow)
### update variables ###
delta = self.delta(net).view(*output_dim)
weight = self.weight(net).view(*output_dim)
delta = delta.permute(0,1,3,4,2)[...,:2].contiguous()
weight = weight.permute(0,1,3,4,2)[...,:2].contiguous()
net = net.view(*output_dim)
if ii is not None:
eta, upmask = self.agg(net, ii.to(net.device))
return net, delta, weight, eta, upmask
else:
return net, delta, weight
class DroidNet(nn.Module):
def __init__(self):
super(DroidNet, self).__init__()
self.fnet = BasicEncoder(output_dim=128, norm_fn='instance')
self.cnet = BasicEncoder(output_dim=256, norm_fn='none')
self.update = UpdateModule()
def extract_features(self, images):
""" run feeature extraction networks """
# normalize images
images = images[:, :, [2,1,0]] / 255.0
mean = torch.as_tensor([0.485, 0.456, 0.406], device=images.device)
std = torch.as_tensor([0.229, 0.224, 0.225], device=images.device)
images = images.sub_(mean[:, None, None]).div_(std[:, None, None])
fmaps = self.fnet(images)
net = self.cnet(images)
net, inp = net.split([128,128], dim=2)
net = torch.tanh(net)
inp = torch.relu(inp)
return fmaps, net, inp
def forward(self, Gs, images, disps, intrinsics, graph=None, num_steps=12, fixedp=2):
""" Estimates SE3 or Sim3 between pair of frames """
u = keyframe_indicies(graph)
ii, jj, kk = graph_to_edge_list(graph)
ii = ii.to(device=images.device, dtype=torch.long)
jj = jj.to(device=images.device, dtype=torch.long)
fmaps, net, inp = self.extract_features(images)
net, inp = net[:,ii], inp[:,ii]
corr_fn = CorrBlock(fmaps[:,ii], fmaps[:,jj], num_levels=4, radius=3)
ht, wd = images.shape[-2:]
coords0 = pops.coords_grid(ht//8, wd//8, device=images.device)
coords1, _ = pops.projective_transform(Gs, disps, intrinsics, ii, jj)
target = coords1.clone()
Gs_list, disp_list, residual_list = [], [], []
for step in range(num_steps):
Gs = Gs.detach()
disps = disps.detach()
coords1 = coords1.detach()
target = target.detach()
# extract motion features
corr = corr_fn(coords1)
resd = target - coords1
flow = coords1 - coords0
motion = torch.cat([flow, resd], dim=-1)
motion = motion.permute(0,1,4,2,3).clamp(-64.0, 64.0)
net, delta, weight, eta, upmask = \
self.update(net, inp, corr, motion, ii, jj)
target = coords1 + delta
for i in range(2):
Gs, disps = BA(target, weight, eta, Gs, disps, intrinsics, ii, jj, fixedp=2)
coords1, valid_mask = pops.projective_transform(Gs, disps, intrinsics, ii, jj)
residual = (target - coords1)
Gs_list.append(Gs)
disp_list.append(upsample_disp(disps, upmask))
residual_list.append(valid_mask * residual)
return Gs_list, disp_list, residual_list
@@ -0,0 +1,375 @@
import torch
import lietorch
import numpy as np
import matplotlib.pyplot as plt
from lietorch import SE3
from modules.corr import CorrBlock, AltCorrBlock
import geom.projective_ops as pops
class FactorGraph:
def __init__(self, video, update_op, device="cuda:0", corr_impl="volume", max_factors=-1, upsample=False):
self.video = video
self.update_op = update_op
self.device = device
self.max_factors = max_factors
self.corr_impl = corr_impl
self.upsample = upsample
# operator at 1/8 resolution
self.ht = ht = video.ht // 8
self.wd = wd = video.wd // 8
self.coords0 = pops.coords_grid(ht, wd, device=device)
self.ii = torch.as_tensor([], dtype=torch.long, device=device)
self.jj = torch.as_tensor([], dtype=torch.long, device=device)
self.age = torch.as_tensor([], dtype=torch.long, device=device)
self.corr, self.net, self.inp = None, None, None
self.damping = 1e-6 * torch.ones_like(self.video.disps)
self.target = torch.zeros([1, 0, ht, wd, 2], device=device, dtype=torch.float)
self.weight = torch.zeros([1, 0, ht, wd, 2], device=device, dtype=torch.float)
# inactive factors
self.ii_inac = torch.as_tensor([], dtype=torch.long, device=device)
self.jj_inac = torch.as_tensor([], dtype=torch.long, device=device)
self.ii_bad = torch.as_tensor([], dtype=torch.long, device=device)
self.jj_bad = torch.as_tensor([], dtype=torch.long, device=device)
self.target_inac = torch.zeros([1, 0, ht, wd, 2], device=device, dtype=torch.float)
self.weight_inac = torch.zeros([1, 0, ht, wd, 2], device=device, dtype=torch.float)
def __filter_repeated_edges(self, ii, jj):
""" remove duplicate edges """
keep = torch.zeros(ii.shape[0], dtype=torch.bool, device=ii.device)
eset = set(
[(i.item(), j.item()) for i, j in zip(self.ii, self.jj)] +
[(i.item(), j.item()) for i, j in zip(self.ii_inac, self.jj_inac)])
for k, (i, j) in enumerate(zip(ii, jj)):
keep[k] = (i.item(), j.item()) not in eset
return ii[keep], jj[keep]
def print_edges(self):
ii = self.ii.cpu().numpy()
jj = self.jj.cpu().numpy()
ix = np.argsort(ii)
ii = ii[ix]
jj = jj[ix]
w = torch.mean(self.weight, dim=[0,2,3,4]).cpu().numpy()
w = w[ix]
for e in zip(ii, jj, w):
print(e)
print()
def filter_edges(self):
""" remove bad edges """
conf = torch.mean(self.weight, dim=[0,2,3,4])
mask = (torch.abs(self.ii-self.jj) > 2) & (conf < 0.001)
self.ii_bad = torch.cat([self.ii_bad, self.ii[mask]])
self.jj_bad = torch.cat([self.jj_bad, self.jj[mask]])
self.rm_factors(mask, store=False)
def clear_edges(self):
self.rm_factors(self.ii >= 0)
self.net = None
self.inp = None
@torch.cuda.amp.autocast(enabled=True)
def add_factors(self, ii, jj, remove=False):
""" add edges to factor graph """
if not isinstance(ii, torch.Tensor):
ii = torch.as_tensor(ii, dtype=torch.long, device=self.device)
if not isinstance(jj, torch.Tensor):
jj = torch.as_tensor(jj, dtype=torch.long, device=self.device)
# remove duplicate edges
ii, jj = self.__filter_repeated_edges(ii, jj)
if ii.shape[0] == 0:
return
# place limit on number of factors
if self.max_factors > 0 and self.ii.shape[0] + ii.shape[0] > self.max_factors \
and self.corr is not None and remove:
ix = torch.arange(len(self.age))[torch.argsort(self.age).cpu()]
self.rm_factors(ix >= self.max_factors - ii.shape[0], store=True)
net = self.video.nets[ii].to(self.device).unsqueeze(0)
# correlation volume for new edges
if self.corr_impl == "volume":
c = (ii == jj).long()
fmap1 = self.video.fmaps[ii,0].to(self.device).unsqueeze(0)
fmap2 = self.video.fmaps[jj,c].to(self.device).unsqueeze(0)
corr = CorrBlock(fmap1, fmap2)
self.corr = corr if self.corr is None else self.corr.cat(corr)
inp = self.video.inps[ii].to(self.device).unsqueeze(0)
self.inp = inp if self.inp is None else torch.cat([self.inp, inp], 1)
with torch.cuda.amp.autocast(enabled=False):
target, _ = self.video.reproject(ii, jj)
weight = torch.zeros_like(target)
self.ii = torch.cat([self.ii, ii], 0)
self.jj = torch.cat([self.jj, jj], 0)
self.age = torch.cat([self.age, torch.zeros_like(ii)], 0)
# reprojection factors
self.net = net if self.net is None else torch.cat([self.net, net], 1)
self.target = torch.cat([self.target, target], 1)
self.weight = torch.cat([self.weight, weight], 1)
@torch.cuda.amp.autocast(enabled=True)
def rm_factors(self, mask, store=False):
""" drop edges from factor graph """
# store estimated factors
if store:
self.ii_inac = torch.cat([self.ii_inac, self.ii[mask]], 0)
self.jj_inac = torch.cat([self.jj_inac, self.jj[mask]], 0)
self.target_inac = torch.cat([self.target_inac, self.target[:,mask]], 1)
self.weight_inac = torch.cat([self.weight_inac, self.weight[:,mask]], 1)
self.ii = self.ii[~mask]
self.jj = self.jj[~mask]
self.age = self.age[~mask]
if self.corr_impl == "volume":
self.corr = self.corr[~mask]
if self.net is not None:
self.net = self.net[:,~mask]
if self.inp is not None:
self.inp = self.inp[:,~mask]
self.target = self.target[:,~mask]
self.weight = self.weight[:,~mask]
@torch.cuda.amp.autocast(enabled=True)
def rm_keyframe(self, ix):
""" drop edges from factor graph """
with self.video.get_lock():
self.video.images[ix] = self.video.images[ix+1]
self.video.poses[ix] = self.video.poses[ix+1]
self.video.disps[ix] = self.video.disps[ix+1]
self.video.disps_sens[ix] = self.video.disps_sens[ix+1]
self.video.intrinsics[ix] = self.video.intrinsics[ix+1]
self.video.nets[ix] = self.video.nets[ix+1]
self.video.inps[ix] = self.video.inps[ix+1]
self.video.fmaps[ix] = self.video.fmaps[ix+1]
m = (self.ii_inac == ix) | (self.jj_inac == ix)
self.ii_inac[self.ii_inac >= ix] -= 1
self.jj_inac[self.jj_inac >= ix] -= 1
if torch.any(m):
self.ii_inac = self.ii_inac[~m]
self.jj_inac = self.jj_inac[~m]
self.target_inac = self.target_inac[:,~m]
self.weight_inac = self.weight_inac[:,~m]
m = (self.ii == ix) | (self.jj == ix)
self.ii[self.ii >= ix] -= 1
self.jj[self.jj >= ix] -= 1
self.rm_factors(m, store=False)
@torch.cuda.amp.autocast(enabled=True)
def update(self, t0=None, t1=None, itrs=2, use_inactive=False, EP=1e-7, motion_only=False):
""" run update operator on factor graph """
# motion features
with torch.cuda.amp.autocast(enabled=False):
coords1, mask = self.video.reproject(self.ii, self.jj)
motn = torch.cat([coords1 - self.coords0, self.target - coords1], dim=-1)
motn = motn.permute(0,1,4,2,3).clamp(-64.0, 64.0)
# correlation features
corr = self.corr(coords1)
self.net, delta, weight, damping, upmask = \
self.update_op(self.net, self.inp, corr, motn, self.ii, self.jj)
if t0 is None:
t0 = max(1, self.ii.min().item()+1)
with torch.cuda.amp.autocast(enabled=False):
self.target = coords1 + delta.to(dtype=torch.float)
self.weight = weight.to(dtype=torch.float)
ht, wd = self.coords0.shape[0:2]
self.damping[torch.unique(self.ii)] = damping
if use_inactive:
m = (self.ii_inac >= t0 - 3) & (self.jj_inac >= t0 - 3)
ii = torch.cat([self.ii_inac[m], self.ii], 0)
jj = torch.cat([self.jj_inac[m], self.jj], 0)
target = torch.cat([self.target_inac[:,m], self.target], 1)
weight = torch.cat([self.weight_inac[:,m], self.weight], 1)
else:
ii, jj, target, weight = self.ii, self.jj, self.target, self.weight
damping = .2 * self.damping[torch.unique(ii)].contiguous() + EP
target = target.view(-1, ht, wd, 2).permute(0,3,1,2).contiguous()
weight = weight.view(-1, ht, wd, 2).permute(0,3,1,2).contiguous()
# dense bundle adjustment
self.video.ba(target, weight, damping, ii, jj, t0, t1,
itrs=itrs, lm=1e-4, ep=0.1, motion_only=motion_only)
if self.upsample:
self.video.upsample(torch.unique(self.ii), upmask)
self.age += 1
@torch.cuda.amp.autocast(enabled=False)
def update_lowmem(self, t0=None, t1=None, itrs=2, use_inactive=False, EP=1e-7, steps=8):
""" run update operator on factor graph - reduced memory implementation """
# alternate corr implementation
t = self.video.counter.value
num, rig, ch, ht, wd = self.video.fmaps.shape
corr_op = AltCorrBlock(self.video.fmaps.view(1, num*rig, ch, ht, wd))
for step in range(steps):
print("Global BA Iteration #{}".format(step+1))
with torch.cuda.amp.autocast(enabled=False):
coords1, mask = self.video.reproject(self.ii, self.jj)
motn = torch.cat([coords1 - self.coords0, self.target - coords1], dim=-1)
motn = motn.permute(0,1,4,2,3).clamp(-64.0, 64.0)
s = 8
for i in range(0, self.jj.max()+1, s):
v = (self.ii >= i) & (self.ii < i + s)
iis = self.ii[v]
jjs = self.jj[v]
ht, wd = self.coords0.shape[0:2]
corr1 = corr_op(coords1[:,v], rig * iis, rig * jjs + (iis == jjs).long())
with torch.cuda.amp.autocast(enabled=True):
net, delta, weight, damping, upmask = \
self.update_op(self.net[:,v], self.video.inps[None,iis], corr1, motn[:,v], iis, jjs)
if self.upsample:
self.video.upsample(torch.unique(iis), upmask)
self.net[:,v] = net
self.target[:,v] = coords1[:,v] + delta.float()
self.weight[:,v] = weight.float()
self.damping[torch.unique(iis)] = damping
damping = .2 * self.damping[torch.unique(self.ii)].contiguous() + EP
target = self.target.view(-1, ht, wd, 2).permute(0,3,1,2).contiguous()
weight = self.weight.view(-1, ht, wd, 2).permute(0,3,1,2).contiguous()
# dense bundle adjustment
self.video.ba(target, weight, damping, self.ii, self.jj, 1, t,
itrs=itrs, lm=1e-5, ep=1e-2, motion_only=False)
self.video.dirty[:t] = True
def add_neighborhood_factors(self, t0, t1, r=3):
""" add edges between neighboring frames within radius r """
ii, jj = torch.meshgrid(torch.arange(t0,t1), torch.arange(t0,t1))
ii = ii.reshape(-1).to(dtype=torch.long, device=self.device)
jj = jj.reshape(-1).to(dtype=torch.long, device=self.device)
c = 1 if self.video.stereo else 0
keep = ((ii - jj).abs() > c) & ((ii - jj).abs() <= r)
self.add_factors(ii[keep], jj[keep])
def add_proximity_factors(self, t0=0, t1=0, rad=2, nms=2, beta=0.25, thresh=16.0, remove=False):
""" add edges to the factor graph based on distance """
t = self.video.counter.value
ix = torch.arange(t0, t)
jx = torch.arange(t1, t)
ii, jj = torch.meshgrid(ix, jx)
ii = ii.reshape(-1)
jj = jj.reshape(-1)
d = self.video.distance(ii, jj, beta=beta)
d[ii - rad < jj] = np.inf
d[d > 100] = np.inf
ii1 = torch.cat([self.ii, self.ii_bad, self.ii_inac], 0)
jj1 = torch.cat([self.jj, self.jj_bad, self.jj_inac], 0)
for i, j in zip(ii1.cpu().numpy(), jj1.cpu().numpy()):
for di in range(-nms, nms+1):
for dj in range(-nms, nms+1):
if abs(di) + abs(dj) <= max(min(abs(i-j)-2, nms), 0):
i1 = i + di
j1 = j + dj
if (t0 <= i1 < t) and (t1 <= j1 < t):
d[(i1-t0)*(t-t1) + (j1-t1)] = np.inf
es = []
for i in range(t0, t):
if self.video.stereo:
es.append((i, i))
d[(i-t0)*(t-t1) + (i-t1)] = np.inf
for j in range(max(i-rad-1,0), i):
es.append((i,j))
es.append((j,i))
d[(i-t0)*(t-t1) + (j-t1)] = np.inf
ix = torch.argsort(d)
for k in ix:
if d[k].item() > thresh:
continue
if len(es) > self.max_factors:
break
i = ii[k]
j = jj[k]
# bidirectional
es.append((i, j))
es.append((j, i))
for di in range(-nms, nms+1):
for dj in range(-nms, nms+1):
if abs(di) + abs(dj) <= max(min(abs(i-j)-2, nms), 0):
i1 = i + di
j1 = j + dj
if (t0 <= i1 < t) and (t1 <= j1 < t):
d[(i1-t0)*(t-t1) + (j1-t1)] = np.inf
ii, jj = torch.as_tensor(es, device=self.device).unbind(dim=-1)
self.add_factors(ii, jj, remove)
@@ -0,0 +1,158 @@
import lietorch
import torch
import torch.nn.functional as F
from .chol import block_solve, schur_solve
import geom.projective_ops as pops
from torch_scatter import scatter_sum
# utility functions for scattering ops
def safe_scatter_add_mat(A, ii, jj, n, m):
v = (ii >= 0) & (jj >= 0) & (ii < n) & (jj < m)
return scatter_sum(A[:,v], ii[v]*m + jj[v], dim=1, dim_size=n*m)
def safe_scatter_add_vec(b, ii, n):
v = (ii >= 0) & (ii < n)
return scatter_sum(b[:,v], ii[v], dim=1, dim_size=n)
# apply retraction operator to inv-depth maps
def disp_retr(disps, dz, ii):
ii = ii.to(device=dz.device)
return disps + scatter_sum(dz, ii, dim=1, dim_size=disps.shape[1])
# apply retraction operator to poses
def pose_retr(poses, dx, ii):
ii = ii.to(device=dx.device)
return poses.retr(scatter_sum(dx, ii, dim=1, dim_size=poses.shape[1]))
def BA(target, weight, eta, poses, disps, intrinsics, ii, jj, fixedp=1, rig=1):
""" Full Bundle Adjustment """
B, P, ht, wd = disps.shape
N = ii.shape[0]
D = poses.manifold_dim
### 1: commpute jacobians and residuals ###
coords, valid, (Ji, Jj, Jz) = pops.projective_transform(
poses, disps, intrinsics, ii, jj, jacobian=True)
r = (target - coords).view(B, N, -1, 1)
w = .001 * (valid * weight).view(B, N, -1, 1)
### 2: construct linear system ###
Ji = Ji.reshape(B, N, -1, D)
Jj = Jj.reshape(B, N, -1, D)
wJiT = (w * Ji).transpose(2,3)
wJjT = (w * Jj).transpose(2,3)
Jz = Jz.reshape(B, N, ht*wd, -1)
Hii = torch.matmul(wJiT, Ji)
Hij = torch.matmul(wJiT, Jj)
Hji = torch.matmul(wJjT, Ji)
Hjj = torch.matmul(wJjT, Jj)
vi = torch.matmul(wJiT, r).squeeze(-1)
vj = torch.matmul(wJjT, r).squeeze(-1)
Ei = (wJiT.view(B,N,D,ht*wd,-1) * Jz[:,:,None]).sum(dim=-1)
Ej = (wJjT.view(B,N,D,ht*wd,-1) * Jz[:,:,None]).sum(dim=-1)
w = w.view(B, N, ht*wd, -1)
r = r.view(B, N, ht*wd, -1)
wk = torch.sum(w*r*Jz, dim=-1)
Ck = torch.sum(w*Jz*Jz, dim=-1)
kx, kk = torch.unique(ii, return_inverse=True)
M = kx.shape[0]
# only optimize keyframe poses
P = P // rig - fixedp
ii = ii // rig - fixedp
jj = jj // rig - fixedp
H = safe_scatter_add_mat(Hii, ii, ii, P, P) + \
safe_scatter_add_mat(Hij, ii, jj, P, P) + \
safe_scatter_add_mat(Hji, jj, ii, P, P) + \
safe_scatter_add_mat(Hjj, jj, jj, P, P)
E = safe_scatter_add_mat(Ei, ii, kk, P, M) + \
safe_scatter_add_mat(Ej, jj, kk, P, M)
v = safe_scatter_add_vec(vi, ii, P) + \
safe_scatter_add_vec(vj, jj, P)
C = safe_scatter_add_vec(Ck, kk, M)
w = safe_scatter_add_vec(wk, kk, M)
C = C + eta.view(*C.shape) + 1e-7
H = H.view(B, P, P, D, D)
E = E.view(B, P, M, D, ht*wd)
### 3: solve the system ###
dx, dz = schur_solve(H, E, C, v, w)
### 4: apply retraction ###
poses = pose_retr(poses, dx, torch.arange(P) + fixedp)
disps = disp_retr(disps, dz.view(B,-1,ht,wd), kx)
disps = torch.where(disps > 10, torch.zeros_like(disps), disps)
disps = disps.clamp(min=0.0)
return poses, disps
def MoBA(target, weight, eta, poses, disps, intrinsics, ii, jj, fixedp=1, rig=1):
""" Motion only bundle adjustment """
B, P, ht, wd = disps.shape
N = ii.shape[0]
D = poses.manifold_dim
### 1: commpute jacobians and residuals ###
coords, valid, (Ji, Jj, Jz) = pops.projective_transform(
poses, disps, intrinsics, ii, jj, jacobian=True)
r = (target - coords).view(B, N, -1, 1)
w = .001 * (valid * weight).view(B, N, -1, 1)
### 2: construct linear system ###
Ji = Ji.reshape(B, N, -1, D)
Jj = Jj.reshape(B, N, -1, D)
wJiT = (w * Ji).transpose(2,3)
wJjT = (w * Jj).transpose(2,3)
Hii = torch.matmul(wJiT, Ji)
Hij = torch.matmul(wJiT, Jj)
Hji = torch.matmul(wJjT, Ji)
Hjj = torch.matmul(wJjT, Jj)
vi = torch.matmul(wJiT, r).squeeze(-1)
vj = torch.matmul(wJjT, r).squeeze(-1)
# only optimize keyframe poses
P = P // rig - fixedp
ii = ii // rig - fixedp
jj = jj // rig - fixedp
H = safe_scatter_add_mat(Hii, ii, ii, P, P) + \
safe_scatter_add_mat(Hij, ii, jj, P, P) + \
safe_scatter_add_mat(Hji, jj, ii, P, P) + \
safe_scatter_add_mat(Hjj, jj, jj, P, P)
v = safe_scatter_add_vec(vi, ii, P) + \
safe_scatter_add_vec(vj, jj, P)
H = H.view(B, P, P, D, D)
### 3: solve the system ###
dx = block_solve(H, v)
### 4: apply retraction ###
poses = pose_retr(poses, dx, torch.arange(P) + fixedp)
return poses
@@ -0,0 +1,73 @@
import torch
import torch.nn.functional as F
import geom.projective_ops as pops
class CholeskySolver(torch.autograd.Function):
@staticmethod
def forward(ctx, H, b):
# don't crash training if cholesky decomp fails
try:
U = torch.linalg.cholesky(H)
xs = torch.cholesky_solve(b, U)
ctx.save_for_backward(U, xs)
ctx.failed = False
except Exception as e:
print(e)
ctx.failed = True
xs = torch.zeros_like(b)
return xs
@staticmethod
def backward(ctx, grad_x):
if ctx.failed:
return None, None
U, xs = ctx.saved_tensors
dz = torch.cholesky_solve(grad_x, U)
dH = -torch.matmul(xs, dz.transpose(-1,-2))
return dH, dz
def block_solve(H, b, ep=0.1, lm=0.0001):
""" solve normal equations """
B, N, _, D, _ = H.shape
I = torch.eye(D).to(H.device)
H = H + (ep + lm*H) * I
H = H.permute(0,1,3,2,4)
H = H.reshape(B, N*D, N*D)
b = b.reshape(B, N*D, 1)
x = CholeskySolver.apply(H,b)
return x.reshape(B, N, D)
def schur_solve(H, E, C, v, w, ep=0.1, lm=0.0001, sless=False):
""" solve using shur complement """
B, P, M, D, HW = E.shape
H = H.permute(0,1,3,2,4).reshape(B, P*D, P*D)
E = E.permute(0,1,3,2,4).reshape(B, P*D, M*HW)
Q = (1.0 / C).view(B, M*HW, 1)
# damping
I = torch.eye(P*D).to(H.device)
H = H + (ep + lm*H) * I
v = v.reshape(B, P*D, 1)
w = w.reshape(B, M*HW, 1)
Et = E.transpose(1,2)
S = H - torch.matmul(E, Q*Et)
v = v - torch.matmul(E, Q*w)
dx = CholeskySolver.apply(S, v)
if sless:
return dx.reshape(B, P, D)
dz = Q * (w - Et @ dx)
dx = dx.reshape(B, P, D)
dz = dz.reshape(B, M, HW)
return dx, dz
@@ -0,0 +1,113 @@
import torch
import numpy as np
from collections import OrderedDict
import lietorch
from data_readers.rgbd_utils import compute_distance_matrix_flow, compute_distance_matrix_flow2
def graph_to_edge_list(graph):
ii, jj, kk = [], [], []
for s, u in enumerate(graph):
for v in graph[u]:
ii.append(u)
jj.append(v)
kk.append(s)
ii = torch.as_tensor(ii)
jj = torch.as_tensor(jj)
kk = torch.as_tensor(kk)
return ii, jj, kk
def keyframe_indicies(graph):
return torch.as_tensor([u for u in graph])
def meshgrid(m, n, device='cuda'):
ii, jj = torch.meshgrid(torch.arange(m), torch.arange(n))
return ii.reshape(-1).to(device), jj.reshape(-1).to(device)
def neighbourhood_graph(n, r):
ii, jj = meshgrid(n, n)
d = (ii - jj).abs()
keep = (d >= 1) & (d <= r)
return ii[keep], jj[keep]
def build_frame_graph(poses, disps, intrinsics, num=16, thresh=24.0, r=2):
""" construct a frame graph between co-visible frames """
N = poses.shape[1]
poses = poses[0].cpu().numpy()
disps = disps[0][:,3::8,3::8].cpu().numpy()
intrinsics = intrinsics[0].cpu().numpy() / 8.0
d = compute_distance_matrix_flow(poses, disps, intrinsics)
count = 0
graph = OrderedDict()
for i in range(N):
graph[i] = []
d[i,i] = np.inf
for j in range(i-r, i+r+1):
if 0 <= j < N and i != j:
graph[i].append(j)
d[i,j] = np.inf
count += 1
while count < num:
ix = np.argmin(d)
i, j = ix // N, ix % N
if d[i,j] < thresh:
graph[i].append(j)
d[i,j] = np.inf
count += 1
else:
break
return graph
def build_frame_graph_v2(poses, disps, intrinsics, num=16, thresh=24.0, r=2):
""" construct a frame graph between co-visible frames """
N = poses.shape[1]
# poses = poses[0].cpu().numpy()
# disps = disps[0].cpu().numpy()
# intrinsics = intrinsics[0].cpu().numpy()
d = compute_distance_matrix_flow2(poses, disps, intrinsics)
# import matplotlib.pyplot as plt
# plt.imshow(d)
# plt.show()
count = 0
graph = OrderedDict()
for i in range(N):
graph[i] = []
d[i,i] = np.inf
for j in range(i-r, i+r+1):
if 0 <= j < N and i != j:
graph[i].append(j)
d[i,j] = np.inf
count += 1
while 1:
ix = np.argmin(d)
i, j = ix // N, ix % N
if d[i,j] < thresh:
graph[i].append(j)
for i1 in range(i-1, i+2):
for j1 in range(j-1, j+2):
if 0 <= i1 < N and 0 <= j1 < N:
d[i1, j1] = np.inf
count += 1
else:
break
return graph
@@ -0,0 +1,118 @@
from collections import OrderedDict
import numpy as np
import torch
from lietorch import SO3, SE3, Sim3
from .graph_utils import graph_to_edge_list
from .projective_ops import projective_transform
def pose_metrics(dE):
""" Translation/Rotation/Scaling metrics from Sim3 """
t, q, s = dE.data.split([3, 4, 1], -1)
ang = SO3(q).log().norm(dim=-1)
# convert radians to degrees
r_err = (180 / np.pi) * ang
t_err = t.norm(dim=-1)
s_err = (s - 1.0).abs()
return r_err, t_err, s_err
def fit_scale(Ps, Gs):
b = Ps.shape[0]
t1 = Ps.data[...,:3].detach().reshape(b, -1)
t2 = Gs.data[...,:3].detach().reshape(b, -1)
s = (t1*t2).sum(-1) / ((t2*t2).sum(-1) + 1e-8)
return s
def geodesic_loss(Ps, Gs, graph, gamma=0.9, do_scale=True):
""" Loss function for training network """
# relative pose
ii, jj, kk = graph_to_edge_list(graph)
dP = Ps[:,jj] * Ps[:,ii].inv()
n = len(Gs)
geodesic_loss = 0.0
for i in range(n):
w = gamma ** (n - i - 1)
dG = Gs[i][:,jj] * Gs[i][:,ii].inv()
if do_scale:
s = fit_scale(dP, dG)
dG = dG.scale(s[:,None])
# pose error
d = (dG * dP.inv()).log()
if isinstance(dG, SE3):
tau, phi = d.split([3,3], dim=-1)
geodesic_loss += w * (
tau.norm(dim=-1).mean() +
phi.norm(dim=-1).mean())
elif isinstance(dG, Sim3):
tau, phi, sig = d.split([3,3,1], dim=-1)
geodesic_loss += w * (
tau.norm(dim=-1).mean() +
phi.norm(dim=-1).mean() +
0.05 * sig.norm(dim=-1).mean())
dE = Sim3(dG * dP.inv()).detach()
r_err, t_err, s_err = pose_metrics(dE)
metrics = {
'rot_error': r_err.mean().item(),
'tr_error': t_err.mean().item(),
'bad_rot': (r_err < .1).float().mean().item(),
'bad_tr': (t_err < .01).float().mean().item(),
}
return geodesic_loss, metrics
def residual_loss(residuals, gamma=0.9):
""" loss on system residuals """
residual_loss = 0.0
n = len(residuals)
for i in range(n):
w = gamma ** (n - i - 1)
residual_loss += w * residuals[i].abs().mean()
return residual_loss, {'residual': residual_loss.item()}
def flow_loss(Ps, disps, poses_est, disps_est, intrinsics, graph, gamma=0.9):
""" optical flow loss """
N = Ps.shape[1]
graph = OrderedDict()
for i in range(N):
graph[i] = [j for j in range(N) if abs(i-j)==1]
ii, jj, kk = graph_to_edge_list(graph)
coords0, val0 = projective_transform(Ps, disps, intrinsics, ii, jj)
val0 = val0 * (disps[:,ii] > 0).float().unsqueeze(dim=-1)
n = len(poses_est)
flow_loss = 0.0
for i in range(n):
w = gamma ** (n - i - 1)
coords1, val1 = projective_transform(poses_est[i], disps_est[i], intrinsics, ii, jj)
v = (val0 * val1).squeeze(dim=-1)
epe = v * (coords1 - coords0).norm(dim=-1)
flow_loss += w * epe.mean()
epe = epe.reshape(-1)[v.reshape(-1) > 0.5]
metrics = {
'f_error': epe.mean().item(),
'1px': (epe<1.0).float().mean().item(),
}
return flow_loss, metrics
@@ -0,0 +1,139 @@
import torch
import torch.nn.functional as F
from lietorch import SE3, Sim3
MIN_DEPTH = 0.2
def extract_intrinsics(intrinsics):
return intrinsics[...,None,None,:].unbind(dim=-1)
def coords_grid(ht, wd, **kwargs):
y, x = torch.meshgrid(
torch.arange(ht).to(**kwargs).float(),
torch.arange(wd).to(**kwargs).float())
return torch.stack([x, y], dim=-1)
def iproj(disps, intrinsics, jacobian=False):
""" pinhole camera inverse projection """
ht, wd = disps.shape[2:]
fx, fy, cx, cy = extract_intrinsics(intrinsics)
y, x = torch.meshgrid(
torch.arange(ht).to(disps.device).float(),
torch.arange(wd).to(disps.device).float())
i = torch.ones_like(disps)
X = (x - cx) / fx
Y = (y - cy) / fy
pts = torch.stack([X, Y, i, disps], dim=-1)
if jacobian:
J = torch.zeros_like(pts)
J[...,-1] = 1.0
return pts, J
return pts, None
def proj(Xs, intrinsics, jacobian=False, return_depth=False):
""" pinhole camera projection """
fx, fy, cx, cy = extract_intrinsics(intrinsics)
X, Y, Z, D = Xs.unbind(dim=-1)
Z = torch.where(Z < 0.5*MIN_DEPTH, torch.ones_like(Z), Z)
d = 1.0 / Z
x = fx * (X * d) + cx
y = fy * (Y * d) + cy
if return_depth:
coords = torch.stack([x, y, D*d], dim=-1)
else:
coords = torch.stack([x, y], dim=-1)
if jacobian:
B, N, H, W = d.shape
o = torch.zeros_like(d)
proj_jac = torch.stack([
fx*d, o, -fx*X*d*d, o,
o, fy*d, -fy*Y*d*d, o,
# o, o, -D*d*d, d,
], dim=-1).view(B, N, H, W, 2, 4)
return coords, proj_jac
return coords, None
def actp(Gij, X0, jacobian=False):
""" action on point cloud """
X1 = Gij[:,:,None,None] * X0
if jacobian:
X, Y, Z, d = X1.unbind(dim=-1)
o = torch.zeros_like(d)
B, N, H, W = d.shape
if isinstance(Gij, SE3):
Ja = torch.stack([
d, o, o, o, Z, -Y,
o, d, o, -Z, o, X,
o, o, d, Y, -X, o,
o, o, o, o, o, o,
], dim=-1).view(B, N, H, W, 4, 6)
elif isinstance(Gij, Sim3):
Ja = torch.stack([
d, o, o, o, Z, -Y, X,
o, d, o, -Z, o, X, Y,
o, o, d, Y, -X, o, Z,
o, o, o, o, o, o, o
], dim=-1).view(B, N, H, W, 4, 7)
return X1, Ja
return X1, None
def projective_transform(poses, depths, intrinsics, ii, jj, jacobian=False, return_depth=False):
""" map points from ii->jj """
# inverse project (pinhole)
X0, Jz = iproj(depths[:,ii], intrinsics[:,ii], jacobian=jacobian)
# transform
Gij = poses[:,jj] * poses[:,ii].inv()
Gij.data[:,ii==jj] = torch.as_tensor([-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], device="cuda")
X1, Ja = actp(Gij, X0, jacobian=jacobian)
# project (pinhole)
x1, Jp = proj(X1, intrinsics[:,jj], jacobian=jacobian, return_depth=return_depth)
# exclude points too close to camera
valid = ((X1[...,2] > MIN_DEPTH) & (X0[...,2] > MIN_DEPTH)).float()
valid = valid.unsqueeze(-1)
if jacobian:
# Ji transforms according to dual adjoint
Jj = torch.matmul(Jp, Ja)
Ji = -Gij[:,:,None,None,None].adjT(Jj)
Jz = Gij[:,:,None,None] * Jz
Jz = torch.matmul(Jp, Jz.unsqueeze(-1))
return x1, valid, (Ji, Jj, Jz)
return x1, valid
def induced_flow(poses, disps, intrinsics, ii, jj):
""" optical flow induced by camera motion """
ht, wd = disps.shape[2:]
y, x = torch.meshgrid(
torch.arange(ht).to(disps.device).float(),
torch.arange(wd).to(disps.device).float())
coords0 = torch.stack([x, y], dim=-1)
coords1, valid = projective_transform(poses, disps, intrinsics, ii, jj, False)
return coords1[...,:2] - coords0, valid
@@ -0,0 +1,54 @@
import torch
from torch.utils.tensorboard import SummaryWriter
SUM_FREQ = 100
class Logger:
def __init__(self, name, scheduler):
self.total_steps = 0
self.running_loss = {}
self.writer = None
self.name = name
self.scheduler = scheduler
def _print_training_status(self):
if self.writer is None:
self.writer = SummaryWriter('runs/%s' % self.name)
print([k for k in self.running_loss])
lr = self.scheduler.get_lr().pop()
metrics_data = [self.running_loss[k]/SUM_FREQ for k in self.running_loss.keys()]
training_str = "[{:6d}, {:10.7f}] ".format(self.total_steps+1, lr)
metrics_str = ("{:10.4f}, "*len(metrics_data)).format(*metrics_data)
# print the training status
print(training_str + metrics_str)
for key in self.running_loss:
val = self.running_loss[key] / SUM_FREQ
self.writer.add_scalar(key, val, self.total_steps)
self.running_loss[key] = 0.0
def push(self, metrics):
for key in metrics:
if key not in self.running_loss:
self.running_loss[key] = 0.0
self.running_loss[key] += metrics[key]
if self.total_steps % SUM_FREQ == SUM_FREQ-1:
self._print_training_status()
self.running_loss = {}
self.total_steps += 1
def write_dict(self, results):
for key in results:
self.writer.add_scalar(key, results[key], self.total_steps)
def close(self):
self.writer.close()
@@ -0,0 +1,24 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
GRAD_CLIP = .01
class GradClip(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
return x
@staticmethod
def backward(ctx, grad_x):
o = torch.zeros_like(grad_x)
grad_x = torch.where(grad_x.abs()>GRAD_CLIP, o, grad_x)
grad_x = torch.where(torch.isnan(grad_x), o, grad_x)
return grad_x
class GradientClip(nn.Module):
def __init__(self):
super(GradientClip, self).__init__()
def forward(self, x):
return GradClip.apply(x)
@@ -0,0 +1,140 @@
import torch
import torch.nn.functional as F
import droid_backends
class CorrSampler(torch.autograd.Function):
@staticmethod
def forward(ctx, volume, coords, radius):
ctx.save_for_backward(volume,coords)
ctx.radius = radius
corr, = droid_backends.corr_index_forward(volume, coords, radius)
return corr
@staticmethod
def backward(ctx, grad_output):
volume, coords = ctx.saved_tensors
grad_output = grad_output.contiguous()
grad_volume, = droid_backends.corr_index_backward(volume, coords, grad_output, ctx.radius)
return grad_volume, None, None
class CorrBlock:
def __init__(self, fmap1, fmap2, num_levels=4, radius=3):
self.num_levels = num_levels
self.radius = radius
self.corr_pyramid = []
# all pairs correlation
corr = CorrBlock.corr(fmap1, fmap2)
batch, num, h1, w1, h2, w2 = corr.shape
corr = corr.reshape(batch*num*h1*w1, 1, h2, w2)
for i in range(self.num_levels):
self.corr_pyramid.append(
corr.view(batch*num, h1, w1, h2//2**i, w2//2**i))
corr = F.avg_pool2d(corr, 2, stride=2)
def __call__(self, coords):
out_pyramid = []
batch, num, ht, wd, _ = coords.shape
coords = coords.permute(0,1,4,2,3)
coords = coords.contiguous().view(batch*num, 2, ht, wd)
for i in range(self.num_levels):
corr = CorrSampler.apply(self.corr_pyramid[i], coords/2**i, self.radius)
out_pyramid.append(corr.view(batch, num, -1, ht, wd))
return torch.cat(out_pyramid, dim=2)
def cat(self, other):
for i in range(self.num_levels):
self.corr_pyramid[i] = torch.cat([self.corr_pyramid[i], other.corr_pyramid[i]], 0)
return self
def __getitem__(self, index):
for i in range(self.num_levels):
self.corr_pyramid[i] = self.corr_pyramid[i][index]
return self
@staticmethod
def corr(fmap1, fmap2):
""" all-pairs correlation """
batch, num, dim, ht, wd = fmap1.shape
fmap1 = fmap1.reshape(batch*num, dim, ht*wd) / 4.0
fmap2 = fmap2.reshape(batch*num, dim, ht*wd) / 4.0
corr = torch.matmul(fmap1.transpose(1,2), fmap2)
return corr.view(batch, num, ht, wd, ht, wd)
class CorrLayer(torch.autograd.Function):
@staticmethod
def forward(ctx, fmap1, fmap2, coords, r):
ctx.r = r
ctx.save_for_backward(fmap1, fmap2, coords)
corr, = droid_backends.altcorr_forward(fmap1, fmap2, coords, ctx.r)
return corr
@staticmethod
def backward(ctx, grad_corr):
fmap1, fmap2, coords = ctx.saved_tensors
grad_corr = grad_corr.contiguous()
fmap1_grad, fmap2_grad, coords_grad = \
droid_backends.altcorr_backward(fmap1, fmap2, coords, grad_corr, ctx.r)
return fmap1_grad, fmap2_grad, coords_grad, None
class AltCorrBlock:
def __init__(self, fmaps, num_levels=4, radius=3):
self.num_levels = num_levels
self.radius = radius
B, N, C, H, W = fmaps.shape
fmaps = fmaps.view(B*N, C, H, W) / 4.0
self.pyramid = []
for i in range(self.num_levels):
sz = (B, N, H//2**i, W//2**i, C)
fmap_lvl = fmaps.permute(0, 2, 3, 1).contiguous()
self.pyramid.append(fmap_lvl.view(*sz))
fmaps = F.avg_pool2d(fmaps, 2, stride=2)
def corr_fn(self, coords, ii, jj):
B, N, H, W, S, _ = coords.shape
coords = coords.permute(0, 1, 4, 2, 3, 5)
corr_list = []
for i in range(self.num_levels):
r = self.radius
fmap1_i = self.pyramid[0][:, ii]
fmap2_i = self.pyramid[i][:, jj]
coords_i = (coords / 2**i).reshape(B*N, S, H, W, 2).contiguous()
fmap1_i = fmap1_i.reshape((B*N,) + fmap1_i.shape[2:])
fmap2_i = fmap2_i.reshape((B*N,) + fmap2_i.shape[2:])
corr = CorrLayer.apply(fmap1_i.float(), fmap2_i.float(), coords_i, self.radius)
corr = corr.view(B, N, S, -1, H, W).permute(0, 1, 3, 4, 5, 2)
corr_list.append(corr)
corr = torch.cat(corr_list, dim=2)
return corr
def __call__(self, coords, ii, jj):
squeeze_output = False
if len(coords.shape) == 5:
coords = coords.unsqueeze(dim=-2)
squeeze_output = True
corr = self.corr_fn(coords, ii, jj)
if squeeze_output:
corr = corr.squeeze(dim=-1)
return corr.contiguous()
@@ -0,0 +1,198 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class ResidualBlock(nn.Module):
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1)
self.relu = nn.ReLU(inplace=True)
num_groups = planes // 8
if norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
if not stride == 1:
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
elif norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(planes)
self.norm2 = nn.BatchNorm2d(planes)
if not stride == 1:
self.norm3 = nn.BatchNorm2d(planes)
elif norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(planes)
self.norm2 = nn.InstanceNorm2d(planes)
if not stride == 1:
self.norm3 = nn.InstanceNorm2d(planes)
elif norm_fn == 'none':
self.norm1 = nn.Sequential()
self.norm2 = nn.Sequential()
if not stride == 1:
self.norm3 = nn.Sequential()
if stride == 1:
self.downsample = None
else:
self.downsample = nn.Sequential(
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm3)
def forward(self, x):
y = x
y = self.relu(self.norm1(self.conv1(y)))
y = self.relu(self.norm2(self.conv2(y)))
if self.downsample is not None:
x = self.downsample(x)
return self.relu(x+y)
class BottleneckBlock(nn.Module):
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
super(BottleneckBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes//4, kernel_size=1, padding=0)
self.conv2 = nn.Conv2d(planes//4, planes//4, kernel_size=3, padding=1, stride=stride)
self.conv3 = nn.Conv2d(planes//4, planes, kernel_size=1, padding=0)
self.relu = nn.ReLU(inplace=True)
num_groups = planes // 8
if norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
if not stride == 1:
self.norm4 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
elif norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(planes//4)
self.norm2 = nn.BatchNorm2d(planes//4)
self.norm3 = nn.BatchNorm2d(planes)
if not stride == 1:
self.norm4 = nn.BatchNorm2d(planes)
elif norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(planes//4)
self.norm2 = nn.InstanceNorm2d(planes//4)
self.norm3 = nn.InstanceNorm2d(planes)
if not stride == 1:
self.norm4 = nn.InstanceNorm2d(planes)
elif norm_fn == 'none':
self.norm1 = nn.Sequential()
self.norm2 = nn.Sequential()
self.norm3 = nn.Sequential()
if not stride == 1:
self.norm4 = nn.Sequential()
if stride == 1:
self.downsample = None
else:
self.downsample = nn.Sequential(
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm4)
def forward(self, x):
y = x
y = self.relu(self.norm1(self.conv1(y)))
y = self.relu(self.norm2(self.conv2(y)))
y = self.relu(self.norm3(self.conv3(y)))
if self.downsample is not None:
x = self.downsample(x)
return self.relu(x+y)
DIM=32
class BasicEncoder(nn.Module):
def __init__(self, output_dim=128, norm_fn='batch', dropout=0.0, multidim=False):
super(BasicEncoder, self).__init__()
self.norm_fn = norm_fn
self.multidim = multidim
if self.norm_fn == 'group':
self.norm1 = nn.GroupNorm(num_groups=8, num_channels=DIM)
elif self.norm_fn == 'batch':
self.norm1 = nn.BatchNorm2d(DIM)
elif self.norm_fn == 'instance':
self.norm1 = nn.InstanceNorm2d(DIM)
elif self.norm_fn == 'none':
self.norm1 = nn.Sequential()
self.conv1 = nn.Conv2d(3, DIM, kernel_size=7, stride=2, padding=3)
self.relu1 = nn.ReLU(inplace=True)
self.in_planes = DIM
self.layer1 = self._make_layer(DIM, stride=1)
self.layer2 = self._make_layer(2*DIM, stride=2)
self.layer3 = self._make_layer(4*DIM, stride=2)
# output convolution
self.conv2 = nn.Conv2d(4*DIM, output_dim, kernel_size=1)
if self.multidim:
self.layer4 = self._make_layer(256, stride=2)
self.layer5 = self._make_layer(512, stride=2)
self.in_planes = 256
self.layer6 = self._make_layer(256, stride=1)
self.in_planes = 128
self.layer7 = self._make_layer(128, stride=1)
self.up1 = nn.Conv2d(512, 256, 1)
self.up2 = nn.Conv2d(256, 128, 1)
self.conv3 = nn.Conv2d(128, output_dim, kernel_size=1)
if dropout > 0:
self.dropout = nn.Dropout2d(p=dropout)
else:
self.dropout = None
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.InstanceNorm2d, nn.GroupNorm)):
if m.weight is not None:
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def _make_layer(self, dim, stride=1):
layer1 = ResidualBlock(self.in_planes, dim, self.norm_fn, stride=stride)
layer2 = ResidualBlock(dim, dim, self.norm_fn, stride=1)
layers = (layer1, layer2)
self.in_planes = dim
return nn.Sequential(*layers)
def forward(self, x):
b, n, c1, h1, w1 = x.shape
x = x.view(b*n, c1, h1, w1)
x = self.conv1(x)
x = self.norm1(x)
x = self.relu1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.conv2(x)
_, c2, h2, w2 = x.shape
return x.view(b, n, c2, h2, w2)
@@ -0,0 +1,34 @@
import torch
import torch.nn as nn
class ConvGRU(nn.Module):
def __init__(self, h_planes=128, i_planes=128):
super(ConvGRU, self).__init__()
self.do_checkpoint = False
self.convz = nn.Conv2d(h_planes+i_planes, h_planes, 3, padding=1)
self.convr = nn.Conv2d(h_planes+i_planes, h_planes, 3, padding=1)
self.convq = nn.Conv2d(h_planes+i_planes, h_planes, 3, padding=1)
self.w = nn.Conv2d(h_planes, h_planes, 1, padding=0)
self.convz_glo = nn.Conv2d(h_planes, h_planes, 1, padding=0)
self.convr_glo = nn.Conv2d(h_planes, h_planes, 1, padding=0)
self.convq_glo = nn.Conv2d(h_planes, h_planes, 1, padding=0)
def forward(self, net, *inputs):
inp = torch.cat(inputs, dim=1)
net_inp = torch.cat([net, inp], dim=1)
b, c, h, w = net.shape
glo = torch.sigmoid(self.w(net)) * net
glo = glo.view(b, c, h*w).mean(-1).view(b, c, 1, 1)
z = torch.sigmoid(self.convz(net_inp) + self.convz_glo(glo))
r = torch.sigmoid(self.convr(net_inp) + self.convr_glo(glo))
q = torch.tanh(self.convq(torch.cat([r*net, inp], dim=1)) + self.convq_glo(glo))
net = (1-z) * net + z * q
return net
@@ -0,0 +1,159 @@
import cv2
import torch
import lietorch
from collections import OrderedDict
from droid_net import DroidNet
import geom.projective_ops as pops
from modules.corr import CorrBlock
class MotionFilter:
""" This class is used to filter incoming frames and extract features """
def __init__(self, net, video, thresh=2.5, device="cuda:0"):
# split net modules
self.cnet = net.cnet
self.fnet = net.fnet
self.update = net.update
self.video = video
self.thresh = thresh
self.device = device
self.count = 0
# mean, std for image normalization
self.MEAN = torch.as_tensor([0.485, 0.456, 0.406], device=self.device)[:, None, None]
self.STDV = torch.as_tensor([0.229, 0.224, 0.225], device=self.device)[:, None, None]
@torch.cuda.amp.autocast(enabled=True)
def __context_encoder(self, image):
""" context features """
net, inp = self.cnet(image).split([128,128], dim=2)
return net.tanh().squeeze(0), inp.relu().squeeze(0)
@torch.cuda.amp.autocast(enabled=True)
def __feature_encoder(self, image):
""" features for correlation volume """
return self.fnet(image).squeeze(0)
@torch.cuda.amp.autocast(enabled=True)
@torch.no_grad()
def track(self, tstamp, image, depth=None, intrinsics=None):
""" main update operation - run on every frame in video """
Id = lietorch.SE3.Identity(1,).data.squeeze()
ht = image.shape[-2] // 8
wd = image.shape[-1] // 8
# normalize images
inputs = image[None, :, [2,1,0]].to(self.device) / 255.0
inputs = inputs.sub_(self.MEAN).div_(self.STDV)
# extract features
gmap = self.__feature_encoder(inputs)
### always add first frame to the depth video ###
if self.video.counter.value == 0:
net, inp = self.__context_encoder(inputs[:,[0]])
self.net, self.inp, self.fmap = net, inp, gmap
self.video.append(tstamp, image[0], Id, 1.0, depth, intrinsics / 8.0, gmap, net[0,0], inp[0,0])
### only add new frame if there is enough motion ###
else:
# index correlation volume
coords0 = pops.coords_grid(ht, wd, device=self.device)[None,None]
corr = CorrBlock(self.fmap[None,[0]], gmap[None,[0]])(coords0)
# approximate flow magnitude using 1 update iteration
_, delta, weight = self.update(self.net[None], self.inp[None], corr)
# check motion magnitue / add new frame to video
if delta.norm(dim=-1).mean().item() > self.thresh:
self.count = 0
net, inp = self.__context_encoder(inputs[:,[0]])
self.net, self.inp, self.fmap = net, inp, gmap
self.video.append(tstamp, image[0], None, None, depth, intrinsics / 8.0, gmap, net[0], inp[0])
else:
self.count += 1
# class MotionFilter:
# """ This class is used to filter incoming frames and extract features """
# def __init__(self, net, video, thresh=2.5, device="cuda:0"):
# # split net modules
# self.cnet = net.cnet
# self.fnet = net.fnet
# self.update = net.update
# self.video = video
# self.thresh = thresh
# self.device = device
# self.count = 0
# # mean, std for image normalization
# self.MEAN = torch.as_tensor([0.485, 0.456, 0.406], device=self.device)[:, None, None]
# self.STDV = torch.as_tensor([0.229, 0.224, 0.225], device=self.device)[:, None, None]
# @torch.cuda.amp.autocast(enabled=True)
# def __context_encoder(self, image):
# """ context features """
# x = self.cnet(image)
# net, inp = self.cnet(image).split([128,128], dim=2)
# return net.tanh().squeeze(0), inp.relu().squeeze(0)
# @torch.cuda.amp.autocast(enabled=True)
# def __feature_encoder(self, image):
# """ features for correlation volume """
# return self.fnet(image).squeeze(0)
# @torch.cuda.amp.autocast(enabled=True)
# @torch.no_grad()
# def track(self, tstamp, image, depth=None, intrinsics=None):
# """ main update operation - run on every frame in video """
# Id = lietorch.SE3.Identity(1,).data.squeeze()
# ht = image.shape[-2] // 8
# wd = image.shape[-1] // 8
# # normalize images
# inputs = image[None, None, [2,1,0]].to(self.device) / 255.0
# inputs = inputs.sub_(self.MEAN).div_(self.STDV)
# # extract features
# gmap = self.__feature_encoder(inputs)
# ### always add first frame to the depth video ###
# if self.video.counter.value == 0:
# net, inp = self.__context_encoder(inputs)
# self.net, self.inp, self.fmap = net, inp, gmap
# self.video.append(tstamp, image, Id, 1.0, intrinsics / 8.0, gmap[0], net[0], inp[0])
# ### only add new frame if there is enough motion ###
# else:
# # index correlation volume
# coords0 = pops.coords_grid(ht, wd, device=self.device)[None,None]
# corr = CorrBlock(self.fmap[None], gmap[None])(coords0)
# # approximate flow magnitude using 1 update iteration
# _, delta, weight = self.update(self.net[None], self.inp[None], corr)
# # check motion magnitue / add new frame to video
# if delta.norm(dim=-1).mean().item() > self.thresh:
# self.count = 0
# net, inp = self.__context_encoder(inputs)
# self.net, self.inp, self.fmap = net, inp, gmap
# self.video.append(tstamp, image, None, None, intrinsics / 8.0, gmap[0], net[0], inp[0])
# else:
# self.count += 1
@@ -0,0 +1,104 @@
import cv2
import torch
import lietorch
from lietorch import SE3
from collections import OrderedDict
from factor_graph import FactorGraph
from droid_net import DroidNet
import geom.projective_ops as pops
class PoseTrajectoryFiller:
""" This class is used to fill in non-keyframe poses """
def __init__(self, net, video, device="cuda:0"):
# split net modules
self.cnet = net.cnet
self.fnet = net.fnet
self.update = net.update
self.count = 0
self.video = video
self.device = device
# mean, std for image normalization
self.MEAN = torch.as_tensor([0.485, 0.456, 0.406], device=self.device)[:, None, None]
self.STDV = torch.as_tensor([0.229, 0.224, 0.225], device=self.device)[:, None, None]
@torch.cuda.amp.autocast(enabled=True)
def __feature_encoder(self, image):
""" features for correlation volume """
return self.fnet(image)
def __fill(self, tstamps, images, intrinsics):
""" fill operator """
tt = torch.as_tensor(tstamps, device="cuda")
images = torch.stack(images, 0)
intrinsics = torch.stack(intrinsics, 0)
inputs = images[:,:,[2,1,0]].to(self.device) / 255.0
### linear pose interpolation ###
N = self.video.counter.value
M = len(tstamps)
ts = self.video.tstamp[:N]
Ps = SE3(self.video.poses[:N])
t0 = torch.as_tensor([ts[ts<=t].shape[0] - 1 for t in tstamps])
t1 = torch.where(t0<N-1, t0+1, t0)
dt = ts[t1] - ts[t0] + 1e-3
dP = Ps[t1] * Ps[t0].inv()
v = dP.log() / dt.unsqueeze(-1)
w = v * (tt - ts[t0]).unsqueeze(-1)
Gs = SE3.exp(w) * Ps[t0]
# extract features (no need for context features)
inputs = inputs.sub_(self.MEAN).div_(self.STDV)
fmap = self.__feature_encoder(inputs)
self.video.counter.value += M
self.video[N:N+M] = (tt, images[:,0], Gs.data, 1, None, intrinsics / 8.0, fmap)
graph = FactorGraph(self.video, self.update)
graph.add_factors(t0.cuda(), torch.arange(N, N+M).cuda())
graph.add_factors(t1.cuda(), torch.arange(N, N+M).cuda())
for itr in range(6):
graph.update(N, N+M, motion_only=True)
Gs = SE3(self.video.poses[N:N+M].clone())
self.video.counter.value -= M
return [ Gs ]
@torch.no_grad()
def __call__(self, image_stream):
""" fill in poses of non-keyframe images """
# store all camera poses
pose_list = []
tstamps = []
images = []
intrinsics = []
for (tstamp, image, intrinsic) in image_stream:
tstamps.append(tstamp)
images.append(image)
intrinsics.append(intrinsic)
if len(tstamps) == 16:
pose_list += self.__fill(tstamps, images, intrinsics)
tstamps, images, intrinsics = [], [], []
if len(tstamps) > 0:
pose_list += self.__fill(tstamps, images, intrinsics)
# stitch pose segments together
return lietorch.cat(pose_list, 0)
@@ -0,0 +1,154 @@
import torch
import cv2
import lietorch
import droid_backends
import time
import argparse
import numpy as np
import open3d as o3d
from lietorch import SE3
import geom.projective_ops as pops
CAM_POINTS = np.array([
[ 0, 0, 0],
[-1, -1, 1.5],
[ 1, -1, 1.5],
[ 1, 1, 1.5],
[-1, 1, 1.5],
[-0.5, 1, 1.5],
[ 0.5, 1, 1.5],
[ 0, 1.2, 1.5]])
CAM_LINES = np.array([
[1,2], [2,3], [3,4], [4,1], [1,0], [0,2], [3,0], [0,4], [5,7], [7,6]])
def white_balance(img):
# from https://stackoverflow.com/questions/46390779/automatic-white-balancing-with-grayworld-assumption
result = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
avg_a = np.average(result[:, :, 1])
avg_b = np.average(result[:, :, 2])
result[:, :, 1] = result[:, :, 1] - ((avg_a - 128) * (result[:, :, 0] / 255.0) * 1.1)
result[:, :, 2] = result[:, :, 2] - ((avg_b - 128) * (result[:, :, 0] / 255.0) * 1.1)
result = cv2.cvtColor(result, cv2.COLOR_LAB2BGR)
return result
def create_camera_actor(g, scale=0.05):
""" build open3d camera polydata """
camera_actor = o3d.geometry.LineSet(
points=o3d.utility.Vector3dVector(scale * CAM_POINTS),
lines=o3d.utility.Vector2iVector(CAM_LINES))
color = (g * 1.0, 0.5 * (1-g), 0.9 * (1-g))
camera_actor.paint_uniform_color(color)
return camera_actor
def create_point_actor(points, colors):
""" open3d point cloud from numpy array """
point_cloud = o3d.geometry.PointCloud()
point_cloud.points = o3d.utility.Vector3dVector(points)
point_cloud.colors = o3d.utility.Vector3dVector(colors)
return point_cloud
def droid_visualization(video, device="cuda:0"):
""" DROID visualization frontend """
torch.cuda.set_device(device)
droid_visualization.video = video
droid_visualization.cameras = {}
droid_visualization.points = {}
droid_visualization.warmup = 8
droid_visualization.scale = 1.0
droid_visualization.ix = 0
droid_visualization.filter_thresh = 0.005
def increase_filter(vis):
droid_visualization.filter_thresh *= 2
with droid_visualization.video.get_lock():
droid_visualization.video.dirty[:droid_visualization.video.counter.value] = True
def decrease_filter(vis):
droid_visualization.filter_thresh *= 0.5
with droid_visualization.video.get_lock():
droid_visualization.video.dirty[:droid_visualization.video.counter.value] = True
def animation_callback(vis):
cam = vis.get_view_control().convert_to_pinhole_camera_parameters()
with torch.no_grad():
with video.get_lock():
t = video.counter.value
dirty_index, = torch.where(video.dirty.clone())
dirty_index = dirty_index
if len(dirty_index) == 0:
return
video.dirty[dirty_index] = False
# convert poses to 4x4 matrix
poses = torch.index_select(video.poses, 0, dirty_index)
disps = torch.index_select(video.disps, 0, dirty_index)
Ps = SE3(poses).inv().matrix().cpu().numpy()
images = torch.index_select(video.images, 0, dirty_index)
images = images.cpu()[:,[2,1,0],3::8,3::8].permute(0,2,3,1) / 255.0
points = droid_backends.iproj(SE3(poses).inv().data, disps, video.intrinsics[0]).cpu()
thresh = droid_visualization.filter_thresh * torch.ones_like(disps.mean(dim=[1,2]))
count = droid_backends.depth_filter(
video.poses, video.disps, video.intrinsics[0], dirty_index, thresh)
count = count.cpu()
disps = disps.cpu()
masks = ((count >= 2) & (disps > .5*disps.mean(dim=[1,2], keepdim=True)))
for i in range(len(dirty_index)):
pose = Ps[i]
ix = dirty_index[i].item()
if ix in droid_visualization.cameras:
vis.remove_geometry(droid_visualization.cameras[ix])
del droid_visualization.cameras[ix]
if ix in droid_visualization.points:
vis.remove_geometry(droid_visualization.points[ix])
del droid_visualization.points[ix]
### add camera actor ###
cam_actor = create_camera_actor(True)
cam_actor.transform(pose)
vis.add_geometry(cam_actor)
droid_visualization.cameras[ix] = cam_actor
mask = masks[i].reshape(-1)
pts = points[i].reshape(-1, 3)[mask].cpu().numpy()
clr = images[i].reshape(-1, 3)[mask].cpu().numpy()
## add point actor ###
point_actor = create_point_actor(pts, clr)
vis.add_geometry(point_actor)
droid_visualization.points[ix] = point_actor
# hack to allow interacting with vizualization during inference
if len(droid_visualization.cameras) >= droid_visualization.warmup:
cam = vis.get_view_control().convert_from_pinhole_camera_parameters(cam)
droid_visualization.ix += 1
vis.poll_events()
vis.update_renderer()
### create Open3D visualization ###
vis = o3d.visualization.VisualizerWithKeyCallback()
vis.register_animation_callback(animation_callback)
vis.register_key_callback(ord("S"), increase_filter)
vis.register_key_callback(ord("A"), decrease_filter)
vis.create_window(height=540, width=960)
vis.get_render_option().load_from_json("misc/renderoption.json")
vis.run()
vis.destroy_window()