import numpy as np import h5py import torch import torch.nn.functional as F import random import json from tqdm import tqdm def chamfer_distance(points_pred, points_gt): x, y = points_pred, points_gt bs, num_points, points_dim = x.size() xx = torch.bmm(x, x.transpose(2, 1)) yy = torch.bmm(y, y.transpose(2, 1)) zz = torch.bmm(x, y.transpose(2, 1)) diag_ind = torch.arange(0, num_points).to(points_pred).long() rx = xx[:, diag_ind, diag_ind].unsqueeze(1).expand_as(xx) ry = yy[:, diag_ind, diag_ind].unsqueeze(1).expand_as(yy) P = rx.transpose(2, 1) + ry - 2 * zz return (P.min(1)[0].mean(dim=1) + P.min(2)[0].mean(dim=1)) def chamfer_distance1(pc1, pc2): # pairwise_dist: (N1, N2) pairwise_dist = torch.cdist(pc1, pc2, p=2) ** 2 # Euclidean distances min_dist_pc1_to_pc2 = pairwise_dist.min(dim=1)[0] # shape (N1,) min_dist_pc2_to_pc1 = pairwise_dist.min(dim=0)[0] # shape (N2,) chamfer = min_dist_pc1_to_pc2.mean() + min_dist_pc2_to_pc1.mean() return chamfer def points_to_voxel_grid(points, voxel_size, grid_min, grid_max): """Converts a point cloud to a voxel grid representation. Args: points (torch.Tensor): (N, 3) tensor of point coordinates. voxel_size (float): Size of each voxel. grid_min (torch.Tensor): (3,) tensor of minimum grid coordinates. grid_max (torch.Tensor): (3,) tensor of maximum grid coordinates. Returns: torch.Tensor: (Dx, Dy, Dz) boolean tensor representing the voxel grid. """ device = points.device grid_size = ((grid_max - grid_min) / voxel_size).round().int() grid = torch.zeros(*grid_size, dtype=torch.bool, device=device) valid_indices = ( (points >= grid_min).all(dim=1) & (points < grid_max).all(dim=1) ) valid_points = points[valid_indices] voxel_indices = ((valid_points - grid_min) / voxel_size).floor().long() # print(grid_size, voxel_indices.max(), valid_points.max()) # import pdb; pdb.set_trace(); grid[voxel_indices[:, 0], voxel_indices[:, 1], voxel_indices[:, 2]] = True return grid def volume_iou(grid1, grid2): """Calculates the Volume IoU between two voxel grids. Args: grid1 (torch.Tensor): (Dx, Dy, Dz) boolean tensor representing the first voxel grid. grid2 (torch.Tensor): (Dx, Dy, Dz) boolean tensor representing the second voxel grid. Returns: float: Volume IoU score. """ intersection = (grid1 & grid2).sum().float() union = (grid1 | grid2).sum().float() if union == 0: return 0.0 return intersection / union def evaluate_4d(pts_pred, pts_gt): N = pts_pred.shape[0] total_iou, total_cd = 0.0, 0.0 voxel_size = 0.1 grid_min = torch.tensor([-1.5, -1.5, -1.5]) grid_max = torch.tensor([1.5, 1.5, 1.5]) for i in range(N): cd = chamfer_distance1(pts_pred[i], pts_gt[i]) total_cd += cd grid1 = points_to_voxel_grid(pts_gt[i].reshape(-1, 3), voxel_size, grid_min, grid_max) grid2 = points_to_voxel_grid(pts_pred[i].reshape(-1, 3), voxel_size, grid_min, grid_max) iou = volume_iou(grid1, grid2) total_iou += iou total_iou /= N total_cd /= N total_mse = F.mse_loss(pts_pred, pts_gt) return total_iou, total_cd, total_mse def evaluate_test_4d(pts_pred_all, pts_gt_all): N = pts_pred_all.shape[0] print(f"Num of testing samples: {N}") total_iou, total_cd, total_mse = 0., 0., 0. single_iou, single_cd, single_mse = [], [], [] for i in tqdm(range(N)): seq_iou, seq_cd, seq_mse = evaluate_4d(pts_pred_all[i], pts_gt_all[i]) single_iou.append(seq_iou) single_cd.append(seq_cd) single_mse.append(seq_mse) total_iou += seq_iou total_cd += seq_cd total_mse += seq_mse return total_iou / N, total_cd / N, total_mse / N, single_iou, single_cd, single_mse if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('--split_lst', type=str, default='./outputs_v3_test_list.json') parser.add_argument('--pred_path', type=str, default='./outputs/dit_8layers_2048p_hf-objaverse-v1_24frames_pointembed_latent256_deform0.001_8gpus_v5/test_100_25steps') parser.add_argument('--save_html', action='store_true') parser.add_argument('--num_samples', type=int, default=100) args = parser.parse_args() # split_lst = json.load(open('/mnt/kostas-graid/datasets/chenwang/traj/ObjaverseXL_sketchfab/raw/hf-objaverse-v1/hf-objaverse-v1_valid_sim_list_v3_test.json')) split_lst = json.load(open(args.split_lst)) random.seed(0) random.shuffle(split_lst) split_lst = split_lst[:100] print(split_lst) pts_gt_all = [] pts_pred_all = [] gif_paths = [] import glob, os for i in range(len(split_lst)): model_metas = h5py.File(f'/mnt/kostas-graid/datasets/chenwang/traj/ObjaverseXL_sketchfab/raw/hf-objaverse-v1/outputs_v3/{split_lst[i]}') pts_gt = np.array(model_metas['x'])[1:48:2] pts_gt = (pts_gt - 5) / 2 pts_pred = np.load(f"{args.pred_path}/{i}_{torch.log10(torch.from_numpy(np.array(model_metas['E'])).float()):03f}_{np.array(model_metas['nu']):03f}.npy") pts_gt_all.append(pts_gt) pts_pred_all.append(pts_pred) pts_gt_all = np.stack(pts_gt_all, axis=0) pts_pred_all = np.stack(pts_pred_all, axis=0) print("Loaded all test samples") iou, cd, mse, single_iou, single_cd, single_mse = evaluate_test_4d(torch.from_numpy(pts_gt_all), torch.from_numpy(pts_pred_all)) print("IOU, CD, MSE:", iou, cd, mse) if args.save_html: sorted_indices = np.argsort(single_iou) pts_gt_all = pts_gt_all[sorted_indices] pts_pred_all = pts_pred_all[sorted_indices] single_iou = np.array(single_iou)[sorted_indices] single_cd = np.array(single_cd)[sorted_indices] single_mse = np.array(single_mse)[sorted_indices] gif_paths = [gif_paths[i] for i in sorted_indices] import html rows = [ "", "", "", " ", " GIF gallery", " ", "", "", ] # 4) one
per gif with caption for i, gif in enumerate(gif_paths): name = gif # full file name (incl. .gif) alt = html.escape(gif) # alt text sans extension rows.append( f"
" f"{alt}" f"

" f"Name: {html.escape(name)}
" f"IoU: {single_iou[i]:.4f}, Chamfer Distance: {single_cd[i]:.4f}, MSE: {single_mse[i]:.4f}" f"

" f"
" ) rows += ["", ""] with open('./outputs/dit_8layers_2048p_hf-objaverse-v1_24frames_pointembed_latent256_deform0.001_8gpus_v3_all/test_1000/visualize.html', 'w') as f: f.write('\n'.join(rows))