Skip to content

Commit 5890d49

Browse files
dev dans master (#171)
* feat: offline mode for color * update las comparison (#168) * update las comparison * remove print * Update ChangeLog (#169) * bump version (#170) --------- Co-authored-by: Nathan Lenglet <nathan.lenglet@ign.fr> Co-authored-by: nlenglet-ign <58478624+nlenglet-ign@users.noreply.github.com>
1 parent 05d4409 commit 5890d49

File tree

10 files changed

+557
-60
lines changed

10 files changed

+557
-60
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ repos:
33
rev: 23.12.0
44
hooks:
55
- id: black
6-
language_version: python3.11
6+
language_version: python3.12
77
- repo: https://github.com/pycqa/flake8
88
rev: 6.1.0
99
hooks:

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.15.7
2+
- [update] Update las_comparison with tolerance
3+
- [update] Colorisation of Las with stream or files
4+
15
# 1.15.6
26
- [fix] fix use of tempory file in windows
37

pdaltools/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.15.6"
1+
__version__ = "1.15.7"
22

33

44
if __name__ == "__main__":

pdaltools/color.py

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: f
3131
return min_d, max_d
3232

3333

34-
def color(
34+
def color_from_stream(
3535
input_file: str,
3636
output_file: str,
3737
proj="",
@@ -156,28 +156,58 @@ def color(
156156
return tmp_ortho, tmp_ortho_irc
157157

158158

159-
def parse_args():
160-
parser = argparse.ArgumentParser("Colorize tool", formatter_class=argparse.RawTextHelpFormatter)
161-
parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
162-
parser.add_argument("--output", "-o", type=str, default="", help="Output file")
163-
parser.add_argument(
164-
"--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
159+
def color_from_files(
160+
input_file: str,
161+
output_file: str,
162+
rgb_image: str,
163+
irc_image: str,
164+
color_rvb_enabled=True,
165+
color_ir_enabled=True,
166+
veget_index_file="",
167+
vegetation_dim="Deviation",
168+
):
169+
pipeline = pdal.Reader.las(filename=input_file)
170+
171+
writer_extra_dims = "all"
172+
173+
if veget_index_file and veget_index_file != "":
174+
print(f"Remplissage du champ {vegetation_dim} à partir du fichier {veget_index_file}")
175+
pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions=f"{vegetation_dim}:1:256.0")
176+
writer_extra_dims = [f"{vegetation_dim}=ushort"]
177+
178+
# Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
179+
# which turns it to a 0 to 255*256 range.
180+
# It is kept this way because of other dependencies that have been tuned to fit this range
181+
if color_rvb_enabled:
182+
pipeline |= pdal.Filter.colorization(raster=rgb_image, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0")
183+
if color_ir_enabled:
184+
pipeline |= pdal.Filter.colorization(raster=irc_image, dimensions="Infrared:1:256.0")
185+
186+
pipeline |= pdal.Writer.las(
187+
filename=output_file, extra_dims=writer_extra_dims, minor_version="4", dataformat_id="8", forward="all"
165188
)
166-
parser.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
167-
parser.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
168-
parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
169-
parser.add_argument("--ir", action="store_true", help="Colorize IR")
170-
parser.add_argument(
171-
"--vegetation",
172-
type=str,
173-
default="",
174-
help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
189+
190+
print("Traitement du nuage de point")
191+
pipeline.execute()
192+
193+
194+
def argument_parser():
195+
parser = argparse.ArgumentParser("Colorize tool")
196+
subparsers = parser.add_subparsers(required=True)
197+
198+
# first command is 'from_stream'
199+
from_stream = subparsers.add_parser("from_stream", help="Images are downloaded from streams")
200+
from_stream.add_argument(
201+
"--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
175202
)
176-
parser.add_argument(
177-
"--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
203+
from_stream.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
204+
from_stream.add_argument("--rvb", action="store_true", help="Colorize RVB")
205+
from_stream.add_argument("--ir", action="store_true", help="Colorize IR")
206+
from_stream.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
207+
from_stream.add_argument(
208+
"--check-images", "-c", action="store_true", help="Check that downloaded image is not white"
178209
)
179-
parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
180-
parser.add_argument(
210+
from_stream.add_argument(
181211
"--stream-RGB",
182212
type=str,
183213
default="ORTHOIMAGERY.ORTHOPHOTOS",
@@ -186,27 +216,49 @@ def parse_args():
186216
for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
187217
for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""",
188218
)
189-
parser.add_argument(
219+
from_stream.add_argument(
190220
"--stream-IRC",
191221
type=str,
192222
default="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
193223
help="""WMS raster stream for IRC colorization. Default to ORTHOIMAGERY.ORTHOPHOTOS.IRC
194224
Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho""",
195225
)
196-
parser.add_argument(
226+
from_stream.add_argument(
197227
"--size-max-GPF",
198228
type=int,
199229
default=5000,
200230
help="Maximum edge size (in pixels) of downloaded images."
201231
" If input file needs more, several images are downloaded and merged.",
202232
)
233+
add_common_options(from_stream)
234+
from_stream.set_defaults(func=from_stream_func)
203235

204-
return parser.parse_args()
236+
# second command is 'from_files'
237+
from_files = subparsers.add_parser("from_files", help="Images are in directories from RGB/IRC")
238+
from_files.add_argument("--image_RGB", type=str, required=True, help="RGB image filepath")
239+
from_files.add_argument("--image_IRC", type=str, required=True, help="IRC image filepath")
240+
add_common_options(from_files)
241+
from_files.set_defaults(func=from_files_func)
205242

243+
return parser
206244

207-
if __name__ == "__main__":
208-
args = parse_args()
209-
color(
245+
246+
def add_common_options(parser):
247+
parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
248+
parser.add_argument("--output", "-o", type=str, default="", help="Output file")
249+
parser.add_argument(
250+
"--vegetation",
251+
type=str,
252+
default="",
253+
help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
254+
)
255+
parser.add_argument(
256+
"--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
257+
)
258+
259+
260+
def from_stream_func(args):
261+
color_from_stream(
210262
input_file=args.input,
211263
output_file=args.output,
212264
proj=args.proj,
@@ -221,3 +273,33 @@ def parse_args():
221273
stream_IRC=args.stream_IRC,
222274
size_max_gpf=args.size_max_GPF,
223275
)
276+
277+
278+
def from_files_func(args):
279+
if args.image_RGB and args.image_RGB != "":
280+
color_rvb_enabled = True
281+
else:
282+
color_rvb_enabled = False
283+
if args.image_IRC and args.image_IRC != "":
284+
color_irc_enabled = True
285+
else:
286+
color_irc_enabled = False
287+
288+
if not color_rvb_enabled and not color_irc_enabled:
289+
raise ValueError("At least one of --rvb or --ir must be provided")
290+
291+
color_from_files(
292+
input_file=args.input,
293+
output_file=args.output,
294+
rgb_image=args.image_RGB,
295+
irc_image=args.image_IRC,
296+
color_rvb_enabled=color_rvb_enabled,
297+
color_ir_enabled=color_irc_enabled,
298+
veget_index_file=args.vegetation,
299+
vegetation_dim=args.vegetation_dim,
300+
)
301+
302+
303+
if __name__ == "__main__":
304+
args = argument_parser.parse_args()
305+
args.func(args)

pdaltools/las_comparison.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import argparse
2-
from pathlib import Path
3-
from typing import Tuple
2+
from typing import Tuple, Dict, Optional
43

54
import laspy
65
import numpy as np
76

7+
from pathlib import Path
8+
89

9-
def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) -> Tuple[bool, int, float]:
10+
def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None, precision: Optional[Dict[str, float]] = None) -> Tuple[bool, int, float]:
1011
"""
1112
Compare specified dimensions between two LAS files.
1213
If no dimensions are specified, compares all available dimensions.
@@ -16,6 +17,8 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
1617
file1: Path to the first LAS file
1718
file2: Path to the second LAS file
1819
dimensions: List of dimension names to compare (optional)
20+
precision: Dictionary mapping dimension names to tolerance values for float comparison.
21+
If None or dimension not in dict, uses exact comparison (default: None)
1922
2023
Returns:
2124
bool: True if all specified dimensions are identical, False otherwise
@@ -59,20 +62,42 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
5962
# Compare each dimension
6063
for dim in dimensions:
6164
try:
65+
6266
# Get sorted dimension arrays
6367
dim1 = np.array(las1[dim])[sort_idx1]
6468
dim2 = np.array(las2[dim])[sort_idx2]
6569

70+
# Get precision for this dimension (if specified)
71+
dim_precision = None
72+
if precision is not None and dim in precision:
73+
dim_precision = precision[dim]
74+
6675
# Compare dimensions
67-
if not np.array_equal(dim1, dim2):
68-
# Find differences
69-
diff_indices = np.where(dim1 != dim2)[0]
70-
print(f"Found {len(diff_indices)} points with different {dim}:")
71-
for idx in diff_indices[:10]: # Show first 10 differences
72-
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
73-
if len(diff_indices) > 10:
74-
print(f"... and {len(diff_indices) - 10} more differences")
75-
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
76+
if dim_precision is not None:
77+
# Use tolerance-based comparison for floats
78+
are_equal = np.allclose(dim1, dim2, rtol=0, atol=dim_precision)
79+
if not are_equal:
80+
# Find differences
81+
diff_mask = ~np.isclose(dim1, dim2, rtol=0, atol=dim_precision)
82+
diff_indices = np.where(diff_mask)[0]
83+
print(f"Found {len(diff_indices)} points with different {dim} (tolerance={dim_precision}):")
84+
for idx in diff_indices[:10]: # Show first 10 differences
85+
diff_value = abs(dim1[idx] - dim2[idx])
86+
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}, diff={diff_value}")
87+
if len(diff_indices) > 10:
88+
print(f"... and {len(diff_indices) - 10} more differences")
89+
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
90+
else:
91+
# Exact comparison
92+
if not np.array_equal(dim1, dim2):
93+
# Find differences
94+
diff_indices = np.where(dim1 != dim2)[0]
95+
print(f"Found {len(diff_indices)} points with different {dim}:")
96+
for idx in diff_indices[:10]: # Show first 10 differences
97+
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
98+
if len(diff_indices) > 10:
99+
print(f"... and {len(diff_indices) - 10} more differences")
100+
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
76101

77102
except KeyError:
78103
print(f"Dimension '{dim}' not found in one or both files")
@@ -93,12 +118,32 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
93118

94119
# Update main function to use the new compare function
95120
def main():
96-
parser = argparse.ArgumentParser(description="Compare dimensions between two LAS files")
121+
parser = argparse.ArgumentParser(
122+
description="Compare dimensions between two LAS files",
123+
formatter_class=argparse.RawDescriptionHelpFormatter,
124+
epilog="""
125+
Examples:
126+
# Compare all dimensions with exact match
127+
python las_comparison.py file1.las file2.las
128+
129+
# Compare specific dimensions with precision per dimension
130+
python las_comparison.py file1.las file2.las --dimensions X Y Z --precision X=0.001 Y=0.001 Z=0.0001
131+
132+
# Compare all dimensions with precision for specific ones
133+
python las_comparison.py file1.las file2.las --precision X=0.001 Y=0.001
134+
"""
135+
)
97136
parser.add_argument("file1", type=str, help="Path to first LAS file")
98137
parser.add_argument("file2", type=str, help="Path to second LAS file")
99138
parser.add_argument(
100139
"--dimensions", nargs="*", help="List of dimensions to compare. If not specified, compares all dimensions."
101140
)
141+
parser.add_argument(
142+
"--precision", nargs="*", metavar="DIM=VAL",
143+
help="Tolerance for float comparison per dimension (format: DIMENSION=PRECISION). "
144+
"Example: --precision X=0.001 Y=0.001 Z=0.0001. "
145+
"Dimensions not specified will use exact comparison."
146+
)
102147

103148
args = parser.parse_args()
104149

@@ -109,7 +154,18 @@ def main():
109154
print("Error: One or both files do not exist")
110155
exit(1)
111156

112-
result = compare_las_dimensions(file1, file2, args.dimensions)
157+
# Parse precision dictionary from command line arguments
158+
precision_dict = None
159+
if args.precision:
160+
precision_dict = {}
161+
for prec_spec in args.precision:
162+
try:
163+
dim_name, prec_value = prec_spec.split('=', 1)
164+
precision_dict[dim_name] = float(prec_value)
165+
except ValueError:
166+
parser.error(f"Invalid precision format: '{prec_spec}'. Expected format: DIMENSION=PRECISION (e.g., X=0.001)")
167+
168+
result = compare_las_dimensions(file1, file2, args.dimensions, precision_dict)
113169
print(f"Dimensions comparison result: {'identical' if result[0] else 'different'}")
114170
return result
115171

test/data/color/test_data_irc.tif

187 KB
Binary file not shown.

test/data/color/test_data_rgb.tif

187 KB
Binary file not shown.

0 commit comments

Comments
 (0)