Skip to content

Commit f462abe

Browse files
authored
Merge pull request #59 from diffCheckOrg/test_suite_seg_reg
test suite complement to pc and meshes + segmentation and registration
2 parents 69ca054 + 788ceea commit f462abe

File tree

11 files changed

+277
-10
lines changed

11 files changed

+277
-10
lines changed

src/diffCheck/geometry/DFPointCloud.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ namespace diffCheck::geometry
116116
return cilantroPointCloud;
117117
}
118118

119-
std::vector<Eigen::Vector3d> DFPointCloud::ComputeBoundingBox()
119+
std::vector<Eigen::Vector3d> DFPointCloud::GetAxixAlignedBoundingBox()
120120
{
121121
auto O3DPointCloud = this->Cvt2O3DPointCloud();
122122
auto boundingBox = O3DPointCloud->GetAxisAlignedBoundingBox();

src/diffCheck/geometry/DFPointCloud.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ namespace diffCheck::geometry
5454
* @return std::vector<Eigen::Vector3d> A vector of two Eigen::Vector3d, with the first one being the minimum
5555
* point and the second one the maximum point of the bounding box.
5656
*/
57-
std::vector<Eigen::Vector3d> ComputeBoundingBox();
57+
std::vector<Eigen::Vector3d> GetAxixAlignedBoundingBox();
5858

5959
/**
6060
* @brief Estimate the normals of the point cloud by either knn or if the radius

src/diffCheck/registrations/DFRefinedRegistration.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace diffCheck::registrations
1313
int maxIteration,
1414
bool usePointToPlane)
1515
{
16-
std::vector<Eigen::Vector3d> minMax = source->ComputeBoundingBox();
16+
std::vector<Eigen::Vector3d> minMax = source->GetAxixAlignedBoundingBox();
1717

1818
std::shared_ptr<open3d::geometry::PointCloud> O3Dsource = source->Cvt2O3DPointCloud();
1919
std::shared_ptr<open3d::geometry::PointCloud> O3Dtarget = target->Cvt2O3DPointCloud();
@@ -65,7 +65,7 @@ namespace diffCheck::registrations
6565
double relativeFitness,
6666
double relativeRMSE)
6767
{
68-
std::vector<Eigen::Vector3d> minMax = source->ComputeBoundingBox();
68+
std::vector<Eigen::Vector3d> minMax = source->GetAxixAlignedBoundingBox();
6969

7070
std::shared_ptr<open3d::geometry::PointCloud> O3Dsource = source->Cvt2O3DPointCloud();
7171
std::shared_ptr<open3d::geometry::PointCloud> O3Dtarget = target->Cvt2O3DPointCloud();

src/diffCheckBindings.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
3131

3232
.def("compute_distance", &diffCheck::geometry::DFPointCloud::ComputeDistance,
3333
py::arg("target_cloud"))
34-
.def("compute_BoundingBox", &diffCheck::geometry::DFPointCloud::ComputeBoundingBox)
3534

3635
.def("voxel_downsample", &diffCheck::geometry::DFPointCloud::VoxelDownsample,
3736
py::arg("voxel_size"))
@@ -40,6 +39,9 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
4039
.def("downsample_by_size", &diffCheck::geometry::DFPointCloud::DownsampleBySize,
4140
py::arg("target_size"))
4241

42+
.def("apply_transformation", &diffCheck::geometry::DFPointCloud::ApplyTransformation,
43+
py::arg("transformation"))
44+
4345
.def("estimate_normals", &diffCheck::geometry::DFPointCloud::EstimateNormals,
4446
py::arg("use_cilantro_evaluator") = false,
4547
py::arg("knn") = 100,
@@ -52,6 +54,7 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
5254
.def("add_points", &diffCheck::geometry::DFPointCloud::AddPoints)
5355

5456
.def("get_tight_bounding_box", &diffCheck::geometry::DFPointCloud::GetTightBoundingBox)
57+
.def("get_axis_aligned_bounding_box", &diffCheck::geometry::DFPointCloud::GetAxixAlignedBoundingBox)
5558

5659
.def("get_num_points", &diffCheck::geometry::DFPointCloud::GetNumPoints)
5760
.def("get_num_colors", &diffCheck::geometry::DFPointCloud::GetNumColors)

tests/integration_tests/pybinds_tests/test_pybind_units.py

Lines changed: 253 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
57128
def 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+
118211
def 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

157406
if __name__ == "__main__":
158407
pytest.main()

tests/test_data/cube_mesh.ply

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:8a7804c4bfc0fbebb68ffb9a7d616c6dc21199d1e43a0b9434168ee47ce5a09e
3+
size 36134
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:0374e977d8f8a23e99eb19682d1429a572b8a1f4db297d06afe8aeafdf389db9
3+
size 120891
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:c5af80113153bfc7e52ba6d0966a020f4a0b96ef4021b2704ebdbedbef40994a
3+
size 1195348
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:878ac619b461a9e1385a74cdaab7e9a9e553221da8bf59c407c1130eb16faff5
3+
size 48291
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:94ca289b7e9eedeabae77b8ab764e95d3817877a060b44eb2375b048f576514c
3+
size 48291

0 commit comments

Comments
 (0)