@@ -27,6 +27,41 @@ def get_ply_cloud_roof_quarter_path():
2727 raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
2828 return ply_file_path
2929
30+ def get_ply_cloud_sphere_path ():
31+ base_test_data_dir = os .getenv ('DF_TEST_DATA_DIR' , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'test_data' )))
32+ ply_file_path = os .path .join (base_test_data_dir , "sphere_5kpts_with_normals.ply" )
33+ if not os .path .exists (ply_file_path ):
34+ raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
35+ return ply_file_path
36+
37+ def get_ply_cloud_bunny_path ():
38+ base_test_data_dir = os .getenv ('DF_TEST_DATA_DIR' , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'test_data' )))
39+ ply_file_path = os .path .join (base_test_data_dir , "stanford_bunny_50kpts_with_normals.ply" )
40+ if not os .path .exists (ply_file_path ):
41+ raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
42+ return ply_file_path
43+
44+ def get_ply_mesh_cube_path ():
45+ base_test_data_dir = os .getenv ('DF_TEST_DATA_DIR' , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'test_data' )))
46+ ply_file_path = os .path .join (base_test_data_dir , "cube_mesh.ply" )
47+ if not os .path .exists (ply_file_path ):
48+ raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
49+ return ply_file_path
50+
51+ def get_two_separate_planes_ply_path ():
52+ base_test_data_dir = os .getenv ('DF_TEST_DATA_DIR' , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'test_data' )))
53+ ply_file_path = os .path .join (base_test_data_dir , "two_separate_planes_with_normals.ply" )
54+ if not os .path .exists (ply_file_path ):
55+ raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
56+ return ply_file_path
57+
58+ def get_two_connected_planes_ply_path ():
59+ base_test_data_dir = os .getenv ('DF_TEST_DATA_DIR' , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'test_data' )))
60+ ply_file_path = os .path .join (base_test_data_dir , "two_connected_planes_with_normals.ply" )
61+ if not os .path .exists (ply_file_path ):
62+ raise FileNotFoundError (f"PLY file not found at: { ply_file_path } " )
63+ return ply_file_path
64+
3065#------------------------------------------------------------------------------
3166# dfb_geometry namespace
3267#------------------------------------------------------------------------------
@@ -54,6 +89,42 @@ def create_DFPointCloudSampleRoof():
5489 df_pcd .load_from_PLY (get_ply_cloud_roof_quarter_path ())
5590 yield df_pcd
5691
92+ @pytest .fixture
93+ def create_two_DFPointCloudSphere ():
94+ df_pcd_1 = dfb .dfb_geometry .DFPointCloud ()
95+ df_pcd_2 = dfb .dfb_geometry .DFPointCloud ()
96+ df_pcd_1 .load_from_PLY (get_ply_cloud_sphere_path ())
97+ df_pcd_2 .load_from_PLY (get_ply_cloud_sphere_path ())
98+ yield df_pcd_1 , df_pcd_2
99+
100+ @pytest .fixture
101+ def create_two_DFPointCloudBunny ():
102+ df_pcd_1 = dfb .dfb_geometry .DFPointCloud ()
103+ df_pcd_2 = dfb .dfb_geometry .DFPointCloud ()
104+ df_pcd_1 .load_from_PLY (get_ply_cloud_bunny_path ())
105+ df_pcd_2 .load_from_PLY (get_ply_cloud_bunny_path ())
106+ yield df_pcd_1 , df_pcd_2
107+
108+ @pytest .fixture
109+ def create_DFPointCloudTwoSeparatePlanes ():
110+ df_pcd = dfb .dfb_geometry .DFPointCloud ()
111+ df_pcd .load_from_PLY (get_two_separate_planes_ply_path ())
112+ yield df_pcd
113+
114+ @pytest .fixture
115+ def create_DFPointCloudTwoConnectedPlanes ():
116+ df_pcd = dfb .dfb_geometry .DFPointCloud ()
117+ df_pcd .load_from_PLY (get_two_connected_planes_ply_path ())
118+ yield df_pcd
119+
120+ @pytest .fixture
121+ def create_DFMeshCube ():
122+ df_mesh = dfb .dfb_geometry .DFMesh ()
123+ df_mesh .load_from_PLY (get_ply_mesh_cube_path ())
124+ yield df_mesh
125+
126+ # point cloud tests
127+
57128def test_DFPointCloud_properties (create_DFPointCloudSampleRoof ):
58129 pc = create_DFPointCloudSampleRoof
59130 assert pc .points .__len__ () == 7379 , "DFPointCloud should have 7379 points"
@@ -114,9 +185,82 @@ def test_DFPointCloud_get_tight_bounding_box(create_DFPointCloudSampleRoof):
114185 # round to the 3 decimal places
115186 assert round (obb [0 ][0 ], 3 ) == 0.196 , "The min x of the OBB should be 0.196"
116187
117- # TODO: to implement DFMesh tests
188+ def test_DFPointCloud_get_axis_aligned_bounding_box (create_DFPointCloudSampleRoof ):
189+ pc = create_DFPointCloudSampleRoof
190+ aabb = pc .get_axis_aligned_bounding_box ()
191+ # round to the 3 decimal places
192+ assert round (aabb [0 ][0 ], 3 ) == - 2.339 , "The min x of the AABB should be 0.196"
193+
194+ def test_DFPointCloud_compute_distance ():
195+ point_pc_1 = [(0 , 0 , 0 )]
196+ point_pc_2 = [(1 , 0 , 0 )]
197+ normal_pc_1 = [(0 , 0 , 1 )]
198+ normal_pc_2 = [(0 , 0 , 1 )]
199+ color_pc_1 = [(0 , 0 , 0 )]
200+ color_pc_2 = [(0 , 0 , 0 )]
201+
202+ pc_1 = dfb .dfb_geometry .DFPointCloud (point_pc_1 , normal_pc_1 , color_pc_1 )
203+ pc_2 = dfb .dfb_geometry .DFPointCloud (point_pc_2 , normal_pc_2 , color_pc_2 )
204+
205+ distance = pc_1 .compute_distance (pc_2 )[0 ]
206+
207+ assert distance == 1. , "The distance between the two points should be 1."
208+
209+ # mesh tests
210+
118211def test_DFMesh_init ():
119- pass
212+ mesh = dfb .dfb_geometry .DFMesh ()
213+ assert mesh is not None , "DFMesh should be initialized successfully"
214+
215+ def test_DFMesh_load_from_PLY (create_DFMeshCube ):
216+ mesh = create_DFMeshCube
217+ assert mesh .vertices .__len__ () == 726 , "DFMesh should have 726 vertices"
218+ assert mesh .faces .__len__ () == 1200 , "DFMesh should have 800 faces"
219+
220+ def test_DFMesh_properties ():
221+ vertices = [[0 , 0 , 0 ], [1 , 0 , 0 ], [1 , 1 , 0 ], [0 , 1 , 0 ], [0 , 1 , - 1 ], [0 , 0 , - 1 ]]
222+ faces = [[0 , 1 , 2 ], [0 , 2 , 3 ], [0 , 3 , 4 ], [0 , 4 , 5 ]]
223+ mesh = dfb .dfb_geometry .DFMesh (vertices , faces , [], [], [])
224+ assert mesh .get_num_vertices () == 6 , "get_num_vertices() should return 6"
225+ assert mesh .get_num_faces () == 4 , "get_num_faces() should return 4"
226+ assert isinstance (mesh .vertices , list ), "vertices should be a list"
227+ assert isinstance (mesh .faces , list ), "faces should be a list"
228+ assert isinstance (mesh .normals_face , list ), "normals_faces should be a list"
229+ assert isinstance (mesh .normals_vertex , list ), "normals_vertex should be a list"
230+ assert isinstance (mesh .colors_face , list ), "colors_faces should be a list"
231+ assert isinstance (mesh .colors_vertex , list ), "colors_vertex should be a list"
232+ assert len (mesh .vertices [0 ]) == 3 , "vertices should be a list of 3 coordinates"
233+ assert len (mesh .faces [0 ]) == 3 , "faces should be a list of 3 indexes"
234+
235+ def test_DFMesh_compute_distance ():
236+ vertices = [[0 , 0 , 0 ], [1 , 0 , 0 ], [0 , 1 , 0 ]]
237+ faces = [[0 , 1 , 2 ]]
238+ mesh = dfb .dfb_geometry .DFMesh (vertices , faces , [], [], [])
239+ point = [(0 , 0 , 1 )]
240+ normal = [(0 , 0 , 1 )]
241+ color = [(0 , 0 , 0 )]
242+ pc = dfb .dfb_geometry .DFPointCloud (point , normal , color )
243+ distance = mesh .compute_distance (pc )[0 ]
244+ assert distance == 1.0 , "The distance between the point and the mesh should be 1.0"
245+
246+ def test_DFMesh_sample_points (create_DFMeshCube ):
247+ mesh = create_DFMeshCube
248+ pc = mesh .sample_points_uniformly (1000 )
249+ assert pc .points .__len__ () == 1000 , "DFPointCloud should have 1000 points"
250+
251+ def test_DFMesh_compute_bounding_box (create_DFMeshCube ):
252+ mesh = create_DFMeshCube
253+ obb = mesh .get_tight_bounding_box ()
254+ assert obb [0 ][0 ] == 0 , "The x coordinate of the first corner of the OBB should be 0"
255+ assert obb [1 ][0 ] == 100 , "The y coordinate of the second corner of the OBB should be 100"
256+ assert obb [2 ][0 ] == 0 , "The y coordinate of the third corner of the OBB should be 0"
257+ assert obb [6 ][2 ] == 100 , "The z coordinate of the second to last corner of the OBB should be 100"
258+
259+ def test_DFMesh_getters (create_DFMeshCube ):
260+ mesh = create_DFMeshCube
261+ assert mesh .get_num_vertices () == 726 , "get_num_vertices() should return 726"
262+ assert mesh .get_num_faces () == 1200 , "get_num_faces() should return 1200"
263+
120264
121265#------------------------------------------------------------------------------
122266# dfb_transformation namespace
@@ -146,13 +290,118 @@ def test_DFTransform_read_write(create_DFPointCloudSampleRoof):
146290#------------------------------------------------------------------------------
147291# dfb_registrations namespace
148292#------------------------------------------------------------------------------
149- # TODO: to be implemented
293+ def test_DFRegistration_pure_translation (create_two_DFPointCloudSphere ):
294+
295+ def make_assertions (df_transformation_result ):
296+ assert df_transformation_result is not None , "DFRegistration should return a transformation matrix"
297+ assert abs (df_transformation_result .transformation_matrix [0 ][3 ] - 20 ) < 0.5 , "The translation in x should be around 20"
298+ assert abs (df_transformation_result .transformation_matrix [1 ][3 ] - 20 ) < 0.5 , "The translation in y should be around 20"
299+ assert abs (df_transformation_result .transformation_matrix [2 ][3 ] - 20 ) < 0.5 , "The translation in z should be around 20"
150300
301+ sphere_1 , sphere_2 = create_two_DFPointCloudSphere
302+
303+ t = dfb .dfb_transformation .DFTransformation ()
304+ t .transformation_matrix = [[1.0 , 0.0 , 0.0 , 20 ],
305+ [0.0 , 1.0 , 0.0 , 20 ],
306+ [0.0 , 0.0 , 1.0 , 20 ],
307+ [0.0 , 0.0 , 0.0 , 1.0 ]]
308+
309+ sphere_2 .apply_transformation (t )
310+
311+ df_transformation_result_o3dfgrfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DFastGlobalRegistrationFeatureMatching (sphere_1 , sphere_2 )
312+ df_transformation_result_o3drfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DRansacOnFeatureMatching (sphere_1 , sphere_2 )
313+ df_transformation_result_o3dicp = dfb .dfb_registrations .DFRefinedRegistration .O3DICP (sphere_1 , sphere_2 , max_correspondence_distance = 20 )
314+ df_transformation_result_o3dgicp = dfb .dfb_registrations .DFRefinedRegistration .O3DGeneralizedICP (sphere_1 , sphere_2 , max_correspondence_distance = 20 )
315+
316+ make_assertions (df_transformation_result_o3dfgrfm )
317+ make_assertions (df_transformation_result_o3drfm )
318+ make_assertions (df_transformation_result_o3dicp )
319+ make_assertions (df_transformation_result_o3dgicp )
320+
321+
322+ def test_DFRegistration_rotation_bunny (create_two_DFPointCloudBunny ):
323+
324+ def make_assertions (df_transformation_result ):
325+ assert df_transformation_result is not None , "DFRegistration should return a transformation matrix"
326+ assert abs (df_transformation_result .transformation_matrix [0 ][0 ] - 0.866 ) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
327+ assert abs (df_transformation_result .transformation_matrix [0 ][1 ]) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
328+ assert abs (df_transformation_result .transformation_matrix [0 ][2 ] - 0.5 ) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
329+
330+ bunny_1 , bunny_2 = create_two_DFPointCloudBunny
331+
332+ r = dfb .dfb_transformation .DFTransformation ()
333+ r .transformation_matrix = [[0.866 , 0.0 , 0.5 , 0.0 ],
334+ [0.0 , 1.0 , 0.0 , 0.0 ],
335+ [- 0.5 , 0.0 , 0.866 , 0.0 ],
336+ [0.0 , 0.0 , 0.0 , 1.0 ]] # 30 degree rotation around y-axis
337+ bunny_2 .apply_transformation (r )
338+
339+ df_transformation_result_o3dfgrfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DFastGlobalRegistrationFeatureMatching (bunny_1 , bunny_2 )
340+ df_transformation_result_o3drfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DRansacOnFeatureMatching (bunny_1 , bunny_2 )
341+ df_transformation_result_o3dicp = dfb .dfb_registrations .DFRefinedRegistration .O3DICP (bunny_1 , bunny_2 , max_correspondence_distance = 1.0 )
342+ df_transformation_result_o3dgicp = dfb .dfb_registrations .DFRefinedRegistration .O3DGeneralizedICP (bunny_1 , bunny_2 , max_correspondence_distance = 15.0 )
343+
344+ make_assertions (df_transformation_result_o3dfgrfm )
345+ make_assertions (df_transformation_result_o3drfm )
346+ make_assertions (df_transformation_result_o3dicp )
347+ make_assertions (df_transformation_result_o3dgicp )
348+
349+
350+ def test_DFRegistration_composite_bunny (create_two_DFPointCloudBunny ):
351+
352+ def make_assertions (df_transformation_result ):
353+ assert df_transformation_result is not None , "DFRegistration should return a transformation matrix"
354+ assert abs (df_transformation_result .transformation_matrix [0 ][3 ] - 0.1 ) < 0.02 , "The translation in x should be around -0.05"
355+ assert abs (df_transformation_result .transformation_matrix [1 ][3 ] - 0.1 ) < 0.02 , "The translation in y should be around -0.05"
356+ assert abs (df_transformation_result .transformation_matrix [2 ][3 ] - 0.1 ) < 0.02 , "The translation in z should be around 0.05"
357+ assert abs (df_transformation_result .transformation_matrix [0 ][0 ] - 0.866 ) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
358+ assert abs (df_transformation_result .transformation_matrix [0 ][1 ]) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
359+ assert abs (df_transformation_result .transformation_matrix [0 ][2 ] - 0.5 ) < 0.2 , "The rotation part of transformation matrix should be close to the transposed rotation matrix initially applied"
360+
361+ bunny_1 ,bunny_2 = create_two_DFPointCloudBunny
362+
363+ transform = dfb .dfb_transformation .DFTransformation ()
364+ transform .transformation_matrix = [[0.866 , 0.0 , 0.5 , 0.1 ],
365+ [0.0 , 1.0 , 0.0 , 0.1 ],
366+ [- 0.5 , 0.0 , 0.866 , 0.1 ],
367+ [0.0 , 0.0 , 0.0 , 1.0 ]] # 30 degree rotation around y-axis + translation
368+
369+ bunny_2 .apply_transformation (transform )
370+
371+ df_transformation_result_o3dfgrfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DFastGlobalRegistrationFeatureMatching (bunny_1 , bunny_2 )
372+ df_transformation_result_o3drfm = dfb .dfb_registrations .DFGlobalRegistrations .O3DRansacOnFeatureMatching (bunny_1 , bunny_2 )
373+ df_transformation_result_o3dicp = dfb .dfb_registrations .DFRefinedRegistration .O3DICP (bunny_1 , bunny_2 )
374+ df_transformation_result_o3dgicp = dfb .dfb_registrations .DFRefinedRegistration .O3DGeneralizedICP (bunny_1 , bunny_2 )
375+
376+ make_assertions (df_transformation_result_o3dfgrfm )
377+ make_assertions (df_transformation_result_o3drfm )
378+ make_assertions (df_transformation_result_o3dicp )
379+ make_assertions (df_transformation_result_o3dgicp )
380+
381+
151382#------------------------------------------------------------------------------
152383# dfb_segmentation namespace
153384#------------------------------------------------------------------------------
154- # TODO: to be implemented
155385
386+ def test_DFPlaneSegmentation_separate_plans (create_DFPointCloudTwoSeparatePlanes ):
387+ pc = create_DFPointCloudTwoSeparatePlanes
388+
389+ segments = dfb .dfb_segmentation .DFSegmentation .segment_by_normal (pc ,
390+ normal_threshold_degree = 5 ,
391+ min_cluster_size = 100 ,
392+ knn_neighborhood_size = 20 )
393+
394+ assert len (segments ) == 2 , "DFPlaneSegmentation should return 2 segments"
395+
396+ def test_DFPlaneSegmentation_connected_plans (create_DFPointCloudTwoConnectedPlanes ):
397+ pc = create_DFPointCloudTwoConnectedPlanes
398+
399+ segments = dfb .dfb_segmentation .DFSegmentation .segment_by_normal (pc ,
400+ normal_threshold_degree = 5 ,
401+ min_cluster_size = 100 ,
402+ knn_neighborhood_size = 20 )
403+
404+ assert len (segments ) == 2 , "DFPlaneSegmentation should return 2 segments"
156405
157406if __name__ == "__main__" :
158407 pytest .main ()
0 commit comments