Skip to content

Commit 34ff978

Browse files
committed
Add synthetic flag criterium on donor points
1 parent 4f626c8 commit 34ff978

File tree

6 files changed

+146
-41
lines changed

6 files changed

+146
-41
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# CHANGELOG
22

3+
- Possibilité d'ignorer les points synthétiques du fichier donneur (paramètre DONOR_USE_SYNTHETIC_POINTS dans le fichier de config)
4+
35
## 1.2.1
46
- Ajout de gdal dans l'image docker (fichiers manquant pour l'utiliser en ligne de commande)
57

configs/configs_patchwork.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ mount_points:
4848
CRS: 2154
4949

5050
DONOR_CLASS_LIST: [2, 22]
51+
DONOR_USE_SYNTHETIC_POINTS: false
52+
5153
RECIPIENT_CLASS_LIST: [2, 6, 9, 17]
5254

5355
TILE_SIZE: 1000

patchwork/patchwork.py

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,66 @@
1616

1717

1818
def get_selected_classes_points(
19-
config: DictConfig,
2019
tile_origin: Tuple[int, int],
2120
points_list: ScaleAwarePointRecord,
2221
class_list: list[int],
22+
use_synthetic_points: bool,
2323
fields_to_keep: list[str],
24+
patch_size: int,
25+
tile_size: int,
2426
) -> pd.DataFrame:
25-
"""get a list of points from a las, and return a ndarray of those point with the selected classification"""
26-
27+
"""get a list of points from a las, and return a pandas dataframe of those point with the selected classification
28+
29+
Args:
30+
tile_origin (Tuple[int, int]): _description_
31+
points_list (ScaleAwarePointRecord): _description_
32+
class_list (list[int]): _description_
33+
use_synthetic_points (bool): _description_
34+
fields_to_keep (list[str]): _description_
35+
patch_size (int): _description_
36+
tile_size (int): _description_
37+
38+
Raises:
39+
NotImplementedError: _description_
40+
41+
Returns:
42+
pd.DataFrame: _description_
43+
"""
2744
# we add automatically classification, so we remove it if it's in field_to_keep
2845
if c.CLASSIFICATION_STR in fields_to_keep:
2946
fields_to_keep.remove(c.CLASSIFICATION_STR)
3047

3148
table_fields_to_keep = [points_list[field] for field in fields_to_keep]
3249
table_field_necessary = [
33-
np.int32(points_list.x / config.PATCH_SIZE), # convert x into the coordinate of the patch
34-
np.int32(points_list.y / config.PATCH_SIZE), # convert y into the coordinate of the patch
50+
np.int32(points_list.x / patch_size), # convert x into the coordinate of the patch
51+
np.int32(points_list.y / patch_size), # convert y into the coordinate of the patch
3552
points_list.classification,
3653
]
54+
all_fields_list = [*fields_to_keep, c.PATCH_X_STR, c.PATCH_Y_STR, c.CLASSIFICATION_STR]
3755

3856
all_classes_points = np.array(table_fields_to_keep + table_field_necessary).transpose()
57+
df_points = pd.DataFrame(all_classes_points, columns=all_fields_list)
3958

40-
mask = np.zeros(len(all_classes_points), dtype=bool)
41-
for classification in class_list:
42-
mask = mask | (all_classes_points[:, -1] == classification)
43-
wanted_classes_points = all_classes_points[mask]
44-
all_fields_list = [*fields_to_keep, c.PATCH_X_STR, c.PATCH_Y_STR, c.CLASSIFICATION_STR]
45-
df_wanted_classes_points = pd.DataFrame(wanted_classes_points, columns=all_fields_list)
59+
# Filter points based on classification
60+
df_points = df_points[df_points.classification.isin(class_list)]
61+
62+
# Filter based on if the point is synthetic
63+
if not use_synthetic_points:
64+
if "synthetic" in fields_to_keep:
65+
df_points = df_points[np.logical_not(df_points.synthetic)]
66+
else:
67+
raise NotImplementedError(
68+
"'get_selected_classes_points' is asked to filter on synthetic flag, "
69+
"but this flag is not in fields to keep."
70+
)
4671

4772
# "push" the points on the limit of the tile to the closest patch
48-
mask_points_on_max_x = df_wanted_classes_points[c.PATCH_X_STR] == tile_origin[0] + config.TILE_SIZE
49-
df_wanted_classes_points.loc[mask_points_on_max_x, c.PATCH_X_STR] = tile_origin[0] + config.TILE_SIZE - 1
50-
mask_points_on_max_y = df_wanted_classes_points[c.PATCH_Y_STR] == tile_origin[1]
51-
df_wanted_classes_points.loc[mask_points_on_max_y, c.PATCH_Y_STR] = tile_origin[1] - 1
73+
mask_points_on_max_x = df_points[c.PATCH_X_STR] == tile_origin[0] + tile_size
74+
df_points.loc[mask_points_on_max_x, c.PATCH_X_STR] = tile_origin[0] + tile_size - 1
75+
mask_points_on_max_y = df_points[c.PATCH_Y_STR] == tile_origin[1]
76+
df_points.loc[mask_points_on_max_y, c.PATCH_Y_STR] = tile_origin[1] - 1
5277

53-
return df_wanted_classes_points
78+
return df_points
5479

5580

5681
def get_type(new_column_size: int):
@@ -75,7 +100,13 @@ def get_complementary_points(
75100
recipient_points = recipient_file.read().points
76101

77102
df_recipient_points = get_selected_classes_points(
78-
config, tile_origin, recipient_points, config.RECIPIENT_CLASS_LIST, []
103+
tile_origin,
104+
recipient_points,
105+
config.RECIPIENT_CLASS_LIST,
106+
use_synthetic_points=True,
107+
fields_to_keep=[],
108+
patch_size=config.PATCH_SIZE,
109+
tile_size=config.TILE_SIZE,
79110
)
80111

81112
# set, for each patch of coordinate (patch_x, patch_y), the number of recipient point
@@ -100,7 +131,15 @@ def get_complementary_points(
100131

101132
donor_columns = get_field_from_header(donor_file)
102133
dfs_donor_points.append(
103-
get_selected_classes_points(config, tile_origin, donor_points, config.DONOR_CLASS_LIST, donor_columns)
134+
get_selected_classes_points(
135+
tile_origin,
136+
donor_points,
137+
config.DONOR_CLASS_LIST,
138+
config.DONOR_USE_SYNTHETIC_POINTS,
139+
donor_columns,
140+
patch_size=config.PATCH_SIZE,
141+
tile_size=config.TILE_SIZE,
142+
)
104143
)
105144

106145
if len(df_donor_info.index):

test/configs/config_test_mount_points.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ mount_points:
5050

5151
CRS: 2154
5252

53+
DONOR_USE_SYNTHETIC_POINTS: true
54+
5355
DONOR_CLASS_LIST: [2, 22]
5456
RECIPIENT_CLASS_LIST: [2, 6, 9, 17]
5557

62 KB
Binary file not shown.

test/test_patchwork.py

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
patchwork,
1919
)
2020

21-
RECIPIENT_TEST_DIR = "test/data/"
21+
TEST_DATA_DIR = "test/data/"
2222
RECIPIENT_TEST_NAME = "recipient_test.laz"
2323

2424
DONOR_CLASS_LIST = [2, 9]
@@ -29,37 +29,79 @@
2929
NEW_COLUMN = "virtual_column"
3030
NEW_COLUMN_SIZE = 8
3131
VALUE_ADDED_POINTS = 1
32+
TILE_SIZE = 1000
33+
PATCH_SIZE = 1
3234

3335
SHP_X_Y_TO_METER_FACTOR = 1000
3436

3537

3638
def test_get_field_from_header():
37-
with laspy.open(os.path.join(RECIPIENT_TEST_DIR, RECIPIENT_TEST_NAME)) as recipient_file:
39+
with laspy.open(os.path.join(TEST_DATA_DIR, RECIPIENT_TEST_NAME)) as recipient_file:
3840
recipient_fields_list = get_field_from_header(recipient_file)
3941
assert len(recipient_fields_list) == 18
4042
# check if all fields are lower case
4143
assert [field for field in recipient_fields_list if field != field.lower()] == []
4244

4345

44-
def test_get_selected_classes_points():
45-
with initialize(version_base="1.2", config_path="../configs"):
46-
config = compose(
47-
config_name="configs_patchwork.yaml",
48-
overrides=[
49-
f"filepath.RECIPIENT_DIRECTORY={RECIPIENT_TEST_DIR}",
50-
f"filepath.RECIPIENT_NAME={RECIPIENT_TEST_NAME}",
51-
f"RECIPIENT_CLASS_LIST={RECIPIENT_CLASS_LIST}",
52-
],
46+
@pytest.mark.parametrize(
47+
"las_path, class_list, fields_to_keep, use_synthetic",
48+
[
49+
# Keep all points
50+
(os.path.join(TEST_DATA_DIR, RECIPIENT_TEST_NAME), [1, 2, 3, 4, 5], ["synthetic", "intensity"], True),
51+
# Filter on class only
52+
(os.path.join(TEST_DATA_DIR, RECIPIENT_TEST_NAME), [2, 3], ["synthetic", "intensity"], True),
53+
# Filter out synthetic points
54+
(os.path.join(TEST_DATA_DIR, RECIPIENT_TEST_NAME), [2, 3], ["synthetic", "x"], False),
55+
],
56+
)
57+
def test_get_selected_classes_points(las_path, class_list, fields_to_keep, use_synthetic):
58+
tile_origin = get_tile_origin_using_header_info(las_path, TILE_SIZE)
59+
with laspy.open(las_path) as recipient_file:
60+
input_points = recipient_file.read().points
61+
df_output_points = get_selected_classes_points(
62+
tile_origin,
63+
input_points,
64+
class_list,
65+
fields_to_keep=fields_to_keep,
66+
use_synthetic_points=use_synthetic,
67+
patch_size=PATCH_SIZE,
68+
tile_size=TILE_SIZE,
5369
)
54-
recipient_path = os.path.join(config.filepath.RECIPIENT_DIRECTORY, config.filepath.RECIPIENT_NAME)
55-
tile_origin_recipient = get_tile_origin_using_header_info(recipient_path, config.TILE_SIZE)
56-
with laspy.open(recipient_path) as recipient_file:
57-
recipient_points = recipient_file.read().points
58-
df_recipient_points = get_selected_classes_points(
59-
config, tile_origin_recipient, recipient_points, config.RECIPIENT_CLASS_LIST, []
70+
assert len(df_output_points.index), "No points in output dataframe"
71+
classification = set(df_output_points[c.CLASSIFICATION_STR])
72+
assert classification.issubset(class_list)
73+
assert set(df_output_points.columns.values) == {
74+
*fields_to_keep,
75+
c.PATCH_X_STR,
76+
c.PATCH_Y_STR,
77+
c.CLASSIFICATION_STR,
78+
}
79+
if use_synthetic:
80+
assert len(df_output_points.index) == np.count_nonzero(
81+
np.isin(np.array(input_points.classification), class_list)
82+
)
83+
else:
84+
assert not np.any(df_output_points.synthetic)
85+
86+
87+
def test_get_selected_classes_points_raise_error():
88+
las_path = os.path.join(TEST_DATA_DIR, "recipient_with_synthetic_points.laz")
89+
class_list = [2, 3]
90+
fields_to_keep = []
91+
use_synthetic = False
92+
tile_origin = get_tile_origin_using_header_info(las_path, TILE_SIZE)
93+
with pytest.raises(NotImplementedError):
94+
with laspy.open(las_path) as las_path:
95+
input_points = las_path.read().points
96+
get_selected_classes_points(
97+
tile_origin,
98+
input_points,
99+
class_list,
100+
fields_to_keep=fields_to_keep,
101+
use_synthetic_points=use_synthetic,
102+
patch_size=PATCH_SIZE,
103+
tile_size=TILE_SIZE,
60104
)
61-
for classification in np.unique(df_recipient_points[c.CLASSIFICATION_STR]):
62-
assert classification in RECIPIENT_CLASS_LIST
63105

64106

65107
@pytest.mark.parametrize(
@@ -104,6 +146,7 @@ def test_get_complementary_points(donor_info_path, recipient_path, x, y, expecte
104146
f"DONOR_CLASS_LIST={DONOR_CLASS_LIST}",
105147
f"RECIPIENT_CLASS_LIST={RECIPIENT_CLASS_LIST}",
106148
f"+VIRTUAL_CLASS_TRANSLATION={VIRTUAL_CLASS_TRANSLATION}",
149+
"DONOR_USE_SYNTHETIC_POINTS=true",
107150
],
108151
)
109152
complementary_points = get_complementary_points(df_donor_info, recipient_path, (x, y), config)
@@ -157,6 +200,7 @@ def test_get_complementary_points_2_more_fields(tmp_path_factory):
157200
f"DONOR_CLASS_LIST={DONOR_CLASS_LIST}",
158201
f"RECIPIENT_CLASS_LIST={RECIPIENT_CLASS_LIST}",
159202
f"+VIRTUAL_CLASS_TRANSLATION={VIRTUAL_CLASS_TRANSLATION}",
203+
"DONOR_USE_SYNTHETIC_POINTS=true",
160204
],
161205
)
162206

@@ -193,7 +237,7 @@ def test_append_points(tmp_path_factory):
193237
config = compose(
194238
config_name="configs_patchwork.yaml",
195239
overrides=[
196-
f"filepath.RECIPIENT_DIRECTORY={RECIPIENT_TEST_DIR}",
240+
f"filepath.RECIPIENT_DIRECTORY={TEST_DATA_DIR}",
197241
f"filepath.RECIPIENT_NAME={RECIPIENT_TEST_NAME}",
198242
f"filepath.OUTPUT_DIR={tmp_file_dir}",
199243
f"filepath.OUTPUT_NAME={tmp_file_name}",
@@ -251,7 +295,7 @@ def test_append_points_new_column(tmp_path_factory):
251295
config = compose(
252296
config_name="configs_patchwork.yaml",
253297
overrides=[
254-
f"filepath.RECIPIENT_DIRECTORY={RECIPIENT_TEST_DIR}",
298+
f"filepath.RECIPIENT_DIRECTORY={TEST_DATA_DIR}",
255299
f"filepath.RECIPIENT_NAME={RECIPIENT_TEST_NAME}",
256300
f"filepath.OUTPUT_DIR={tmp_file_dir}",
257301
f"filepath.OUTPUT_NAME={tmp_file_name}",
@@ -326,6 +370,7 @@ def test_patchwork_default(tmp_path_factory, recipient_path, expected_nb_added_p
326370
f"DONOR_CLASS_LIST={DONOR_CLASS_LIST}",
327371
f"RECIPIENT_CLASS_LIST={RECIPIENT_CLASS_LIST}",
328372
f"+VIRTUAL_CLASS_TRANSLATION={VIRTUAL_CLASS_TRANSLATION}",
373+
"DONOR_USE_SYNTHETIC_POINTS=true",
329374
"NEW_COLUMN=null",
330375
],
331376
)
@@ -346,27 +391,40 @@ def test_patchwork_default(tmp_path_factory, recipient_path, expected_nb_added_p
346391

347392

348393
@pytest.mark.parametrize(
349-
"recipient_path, expected_nb_added_points",
394+
"recipient_path, donor_use_synthetic_points, expected_nb_added_points",
350395
# expected_nb_points value set after inspection of the initial result using qgis:
351396
# - there are points only inside the shapefile geometry
352397
# - when visualizing a grid, there seems to be no points in the cells where there is ground points in the
353398
# recipient laz
354399
[
355400
(
356401
"test/data/lidar_HD_decimated/Semis_2022_0673_6362_LA93_IGN69_decimated.laz",
402+
True,
357403
128675,
358404
), # One donor
405+
(
406+
"test/data/lidar_HD_decimated/Semis_2022_0673_6362_LA93_IGN69_decimated.laz",
407+
False,
408+
127961,
409+
), # One donor, no synthetic points
359410
(
360411
"test/data/lidar_HD_decimated/Semis_2022_0673_6363_LA93_IGN69_decimated.laz",
412+
True,
361413
149490,
362414
), # Two donors
415+
(
416+
"test/data/lidar_HD_decimated/Semis_2022_0673_6363_LA93_IGN69_decimated.laz",
417+
False,
418+
149340,
419+
), # Two donors, no synthetic points
363420
(
364421
"test/data/lidar_HD_decimated/Semis_2022_0674_6363_LA93_IGN69_decimated.laz",
422+
True,
365423
0,
366424
), # No donor
367425
],
368426
)
369-
def test_patchwork_with_origin(tmp_path_factory, recipient_path, expected_nb_added_points):
427+
def test_patchwork_with_origin(tmp_path_factory, recipient_path, donor_use_synthetic_points, expected_nb_added_points):
370428
input_shp_path = "test/data/shapefile_local/patchwork_geometries.shp"
371429
tmp_file_dir = tmp_path_factory.mktemp("data")
372430
tmp_output_las_name = "result_patchwork.laz"
@@ -386,6 +444,7 @@ def test_patchwork_with_origin(tmp_path_factory, recipient_path, expected_nb_add
386444
f"filepath.OUTPUT_INDICES_MAP_NAME={tmp_output_indices_map_name}",
387445
f"DONOR_CLASS_LIST={DONOR_CLASS_LIST}",
388446
f"RECIPIENT_CLASS_LIST={RECIPIENT_CLASS_LIST}",
447+
f"DONOR_USE_SYNTHETIC_POINTS={donor_use_synthetic_points}",
389448
"NEW_COLUMN='Origin'",
390449
],
391450
)
@@ -411,6 +470,7 @@ def test_patchwork_with_origin(tmp_path_factory, recipient_path, expected_nb_add
411470
@pytest.mark.parametrize(
412471
"input_shp_path, recipient_path, expected_nb_added_points",
413472
# Same tests as "test_patchwork_default", but with shapefiles that refer to paths in mounted stores
473+
# All tests keep synthetic points
414474
[
415475
(
416476
"test/data/shapefile_mounted_unix_path/patchwork_geometries.shp",

0 commit comments

Comments
 (0)