def _bm_load_ply(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int): f = StringIO() save_ply(f, verts, faces, decimal_places) s = f.getvalue() # Recreate stream so it's unaffected by how it was created. return lambda: load_ply(StringIO(s))
def test_simple_save(self): verts = torch.tensor( [[0, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0], [1, 2, 0]], dtype=torch.float32) faces = torch.tensor([[0, 1, 2], [0, 3, 4]]) for filetype in BytesIO, TemporaryFile: lengths = {} for ascii in [True, False]: file = filetype() save_ply(file, verts=verts, faces=faces, ascii=ascii) lengths[ascii] = file.tell() file.seek(0) verts2, faces2 = load_ply(file) self.assertClose(verts, verts2) self.assertClose(faces, faces2) file.seek(0) if ascii: file.read().decode("ascii") else: with self.assertRaises(UnicodeDecodeError): file.read().decode("ascii") if filetype is TemporaryFile: file.close() self.assertLess(lengths[False], lengths[True], "ascii should be longer")
def bm_load_simple_ply_with_init(V: int, F: int): verts = torch.tensor([[0.1, 0.2, 0.3]]).expand(V, 3) faces = torch.tensor([[0, 1, 2]], dtype=torch.int64).expand(F, 3) ply_file = StringIO() save_ply(ply_file, verts=verts, faces=faces) ply = ply_file.getvalue() # Recreate stream so it's unaffected by how it was created. return lambda: load_ply(StringIO(ply))
def test_normals_save(self): verts = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=torch.float32) faces = torch.tensor([[0, 1, 2], [0, 2, 3]]) normals = torch.tensor([[0, 1, 0], [1, 0, 0], [0, 0, 1], [1, 0, 0]], dtype=torch.float32) file = StringIO() save_ply(file, verts=verts, faces=faces, verts_normals=normals) file.close()
def test_simple_save(self): verts = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=torch.float32) faces = torch.tensor([[0, 1, 2], [0, 3, 4]]) file = StringIO() save_ply(file, verts=verts, faces=faces) file.seek(0) verts2, faces2 = load_ply(file) self.assertClose(verts, verts2) self.assertClose(faces, faces2)
def test_save_ply_invalid_indices(self): message_regex = "Faces have invalid indices" verts = torch.FloatTensor([[0.1, 0.2, 0.3]]) faces = torch.LongTensor([[0, 1, 2]]) with self.assertWarnsRegex(UserWarning, message_regex): save_ply(StringIO(), verts, faces) faces = torch.LongTensor([[-1, 0, 1]]) with self.assertWarnsRegex(UserWarning, message_regex): save_ply(StringIO(), verts, faces)
def _test_save_load(self, verts, faces): f = StringIO() save_ply(f, verts, faces) f.seek(0) # raise Exception(f.getvalue()) expected_verts, expected_faces = verts, faces if not len(expected_verts): # Always compare with a (V, 3) tensor expected_verts = torch.zeros(size=(0, 3), dtype=torch.float32) if not len(expected_faces): # Always compare with an (F, 3) tensor expected_faces = torch.zeros(size=(0, 3), dtype=torch.int64) actual_verts, actual_faces = load_ply(f) self.assertClose(expected_verts, actual_verts) self.assertClose(expected_faces, actual_faces)
def load_ply_bm(V: int, F: int): verts = torch.tensor([[0.1, 0.2, 0.3]]).expand(V, 3) faces = torch.tensor([[0, 1, 2]], dtype=torch.int64).expand(F, 3) ply_file = StringIO() save_ply(ply_file, verts=verts, faces=faces) ply = ply_file.getvalue() # Recreate stream so it's unaffected by how it was created. def load_mesh(): ply_file = StringIO(ply) verts, faces = load_ply(ply_file) return load_mesh
def test_save_ply_invalid_shapes(self): # Invalid vertices shape with self.assertRaises(ValueError) as error: verts = torch.FloatTensor([[0.1, 0.2, 0.3, 0.4]]) # (V, 4) faces = torch.LongTensor([[0, 1, 2]]) save_ply(StringIO(), verts, faces) expected_message = "Argument 'verts' should either be empty or of shape (num_verts, 3)." self.assertTrue(expected_message, error.exception) # Invalid faces shape with self.assertRaises(ValueError) as error: verts = torch.FloatTensor([[0.1, 0.2, 0.3]]) faces = torch.LongTensor([[0, 1, 2, 3]]) # (F, 4) save_ply(StringIO(), verts, faces) expected_message = "Argument 'faces' should either be empty or of shape (num_faces, 3)." self.assertTrue(expected_message, error.exception)
def _bm_save_ply(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int): return lambda: save_ply( BytesIO(), verts=verts, faces=faces, ascii=True, decimal_places=decimal_places, )
def _bm_save_ply(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int): return lambda: save_ply( StringIO(), verts, faces, decimal_places=decimal_places)
def save_mesh(): file = StringIO() save_ply(file, verts_list, faces_list, 2)
def test_pcl_normals(self, batch_size=3, num_points=300, neighborhood_size=50): """ Tests the normal estimation on a spherical point cloud, where we know the ground truth normals. """ device = torch.device("cuda:0") # run several times for different random point clouds for run_idx in range(3): # either use tensors or Pointclouds as input for use_pointclouds in (True, False): # get a spherical point cloud pcl, normals_gt = TestPCLNormals.init_spherical_pcl( num_points=num_points, batch_size=batch_size, device=device, use_pointclouds=use_pointclouds, ) if use_pointclouds: normals_gt = pcl.normals_padded() num_pcl_points = pcl.num_points_per_cloud() else: num_pcl_points = [pcl.shape[1]] * batch_size # check for both disambiguation options for disambiguate_directions in (True, False): ( curvatures, local_coord_frames, ) = estimate_pointcloud_local_coord_frames( pcl, neighborhood_size=neighborhood_size, disambiguate_directions=disambiguate_directions, ) # estimate the normals normals = estimate_pointcloud_normals( pcl, neighborhood_size=neighborhood_size, disambiguate_directions=disambiguate_directions, ) # TODO: temporarily disabled if use_pointclouds: # test that the class method gives the same output normals_pcl = pcl.estimate_normals( neighborhood_size=neighborhood_size, disambiguate_directions=disambiguate_directions, assign_to_self=True, ) normals_from_pcl = pcl.normals_padded() for nrm, nrm_from_pcl, nrm_pcl, np in zip( normals, normals_from_pcl, normals_pcl, num_pcl_points): self.assertClose(nrm[:np], nrm_pcl[:np], atol=1e-5) self.assertClose(nrm[:np], nrm_from_pcl[:np], atol=1e-5) # check that local coord frames give the same normal # as normals for nrm, lcoord, np in zip(normals, local_coord_frames, num_pcl_points): self.assertClose(nrm[:np], lcoord[:np, :, 0], atol=1e-5) # dotp between normals and normals_gt normal_parallel = (normals_gt * normals).sum(2) # check that normals are on average # parallel to the expected ones for normp, np in zip(normal_parallel, num_pcl_points): abs_parallel = normp[:np].abs() avg_parallel = abs_parallel.mean() std_parallel = abs_parallel.std() self.assertClose(avg_parallel, torch.ones_like(avg_parallel), atol=1e-2) self.assertClose(std_parallel, torch.zeros_like(std_parallel), atol=1e-2) if disambiguate_directions: # check that 95% of normal dot products # have the same sign for normp, np in zip(normal_parallel, num_pcl_points): n_pos = (normp[:np] > 0).sum() self.assertTrue((n_pos > np * 0.95) or (n_pos < np * 0.05)) if DEBUG and run_idx == 0 and not use_pointclouds: import os from pytorch3d.io.ply_io import save_ply # export to .ply outdir = "/tmp/pt3d_pcl_normals_test/" os.makedirs(outdir, exist_ok=True) plyfile = os.path.join( outdir, f"pcl_disamb={disambiguate_directions}.ply") print( f"Storing point cloud with normals to {plyfile}.") pcl_idx = 0 save_ply( plyfile, pcl[pcl_idx].cpu(), faces=None, verts_normals=normals[pcl_idx].cpu(), )
def bm_save_simple_ply_with_init(V: int, F: int): verts_list = torch.tensor(V * [[0.11, 0.22, 0.33]]).view(-1, 3) faces_list = torch.tensor(F * [[0, 1, 2]]).view(-1, 3) return lambda: save_ply( StringIO(), verts_list, faces_list, decimal_places=2)