Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ repos:
rev: 23.12.0
hooks:
- id: black
language_version: python3.11
language_version: python3.12
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.15.7
- [update] Update las_comparison with tolerance
- [update] Colorisation of Las with stream or files

# 1.15.6
- [fix] fix use of tempory file in windows

Expand Down
2 changes: 1 addition & 1 deletion pdaltools/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.15.6"
__version__ = "1.15.7"


if __name__ == "__main__":
Expand Down
134 changes: 108 additions & 26 deletions pdaltools/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: f
return min_d, max_d


def color(
def color_from_stream(
input_file: str,
output_file: str,
proj="",
Expand Down Expand Up @@ -156,28 +156,58 @@ def color(
return tmp_ortho, tmp_ortho_irc


def parse_args():
parser = argparse.ArgumentParser("Colorize tool", formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
parser.add_argument("--output", "-o", type=str, default="", help="Output file")
parser.add_argument(
"--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
def color_from_files(
input_file: str,
output_file: str,
rgb_image: str,
irc_image: str,
color_rvb_enabled=True,
color_ir_enabled=True,
veget_index_file="",
vegetation_dim="Deviation",
):
pipeline = pdal.Reader.las(filename=input_file)

writer_extra_dims = "all"

if veget_index_file and veget_index_file != "":
print(f"Remplissage du champ {vegetation_dim} à partir du fichier {veget_index_file}")
pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions=f"{vegetation_dim}:1:256.0")
writer_extra_dims = [f"{vegetation_dim}=ushort"]

# Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
# which turns it to a 0 to 255*256 range.
# It is kept this way because of other dependencies that have been tuned to fit this range
if color_rvb_enabled:
pipeline |= pdal.Filter.colorization(raster=rgb_image, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0")
if color_ir_enabled:
pipeline |= pdal.Filter.colorization(raster=irc_image, dimensions="Infrared:1:256.0")

pipeline |= pdal.Writer.las(
filename=output_file, extra_dims=writer_extra_dims, minor_version="4", dataformat_id="8", forward="all"
)
parser.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
parser.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
parser.add_argument("--ir", action="store_true", help="Colorize IR")
parser.add_argument(
"--vegetation",
type=str,
default="",
help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",

print("Traitement du nuage de point")
pipeline.execute()


def argument_parser():
parser = argparse.ArgumentParser("Colorize tool")
subparsers = parser.add_subparsers(required=True)

# first command is 'from_stream'
from_stream = subparsers.add_parser("from_stream", help="Images are downloaded from streams")
from_stream.add_argument(
"--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
)
parser.add_argument(
"--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
from_stream.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
from_stream.add_argument("--rvb", action="store_true", help="Colorize RVB")
from_stream.add_argument("--ir", action="store_true", help="Colorize IR")
from_stream.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
from_stream.add_argument(
"--check-images", "-c", action="store_true", help="Check that downloaded image is not white"
)
parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
parser.add_argument(
from_stream.add_argument(
"--stream-RGB",
type=str,
default="ORTHOIMAGERY.ORTHOPHOTOS",
Expand All @@ -186,27 +216,49 @@ def parse_args():
for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""",
)
parser.add_argument(
from_stream.add_argument(
"--stream-IRC",
type=str,
default="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
help="""WMS raster stream for IRC colorization. Default to ORTHOIMAGERY.ORTHOPHOTOS.IRC
Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho""",
)
parser.add_argument(
from_stream.add_argument(
"--size-max-GPF",
type=int,
default=5000,
help="Maximum edge size (in pixels) of downloaded images."
" If input file needs more, several images are downloaded and merged.",
)
add_common_options(from_stream)
from_stream.set_defaults(func=from_stream_func)

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

return parser

if __name__ == "__main__":
args = parse_args()
color(

def add_common_options(parser):
parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
parser.add_argument("--output", "-o", type=str, default="", help="Output file")
parser.add_argument(
"--vegetation",
type=str,
default="",
help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
)
parser.add_argument(
"--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
)


def from_stream_func(args):
color_from_stream(
input_file=args.input,
output_file=args.output,
proj=args.proj,
Expand All @@ -221,3 +273,33 @@ def parse_args():
stream_IRC=args.stream_IRC,
size_max_gpf=args.size_max_GPF,
)


def from_files_func(args):
if args.image_RGB and args.image_RGB != "":
color_rvb_enabled = True
else:
color_rvb_enabled = False
if args.image_IRC and args.image_IRC != "":
color_irc_enabled = True
else:
color_irc_enabled = False

if not color_rvb_enabled and not color_irc_enabled:
raise ValueError("At least one of --rvb or --ir must be provided")

color_from_files(
input_file=args.input,
output_file=args.output,
rgb_image=args.image_RGB,
irc_image=args.image_IRC,
color_rvb_enabled=color_rvb_enabled,
color_ir_enabled=color_irc_enabled,
veget_index_file=args.vegetation,
vegetation_dim=args.vegetation_dim,
)


if __name__ == "__main__":
args = argument_parser.parse_args()
args.func(args)
84 changes: 70 additions & 14 deletions pdaltools/las_comparison.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import argparse
from pathlib import Path
from typing import Tuple
from typing import Tuple, Dict, Optional

import laspy
import numpy as np

from pathlib import Path


def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) -> Tuple[bool, int, float]:
def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None, precision: Optional[Dict[str, float]] = None) -> Tuple[bool, int, float]:
"""
Compare specified dimensions between two LAS files.
If no dimensions are specified, compares all available dimensions.
Expand All @@ -16,6 +17,8 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
file1: Path to the first LAS file
file2: Path to the second LAS file
dimensions: List of dimension names to compare (optional)
precision: Dictionary mapping dimension names to tolerance values for float comparison.
If None or dimension not in dict, uses exact comparison (default: None)

Returns:
bool: True if all specified dimensions are identical, False otherwise
Expand Down Expand Up @@ -59,20 +62,42 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
# Compare each dimension
for dim in dimensions:
try:

# Get sorted dimension arrays
dim1 = np.array(las1[dim])[sort_idx1]
dim2 = np.array(las2[dim])[sort_idx2]

# Get precision for this dimension (if specified)
dim_precision = None
if precision is not None and dim in precision:
dim_precision = precision[dim]

# Compare dimensions
if not np.array_equal(dim1, dim2):
# Find differences
diff_indices = np.where(dim1 != dim2)[0]
print(f"Found {len(diff_indices)} points with different {dim}:")
for idx in diff_indices[:10]: # Show first 10 differences
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
if len(diff_indices) > 10:
print(f"... and {len(diff_indices) - 10} more differences")
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
if dim_precision is not None:
# Use tolerance-based comparison for floats
are_equal = np.allclose(dim1, dim2, rtol=0, atol=dim_precision)
if not are_equal:
# Find differences
diff_mask = ~np.isclose(dim1, dim2, rtol=0, atol=dim_precision)
diff_indices = np.where(diff_mask)[0]
print(f"Found {len(diff_indices)} points with different {dim} (tolerance={dim_precision}):")
for idx in diff_indices[:10]: # Show first 10 differences
diff_value = abs(dim1[idx] - dim2[idx])
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}, diff={diff_value}")
if len(diff_indices) > 10:
print(f"... and {len(diff_indices) - 10} more differences")
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
else:
# Exact comparison
if not np.array_equal(dim1, dim2):
# Find differences
diff_indices = np.where(dim1 != dim2)[0]
print(f"Found {len(diff_indices)} points with different {dim}:")
for idx in diff_indices[:10]: # Show first 10 differences
print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
if len(diff_indices) > 10:
print(f"... and {len(diff_indices) - 10} more differences")
return False, len(diff_indices), 100 * len(diff_indices) / len(las1)

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

# Update main function to use the new compare function
def main():
parser = argparse.ArgumentParser(description="Compare dimensions between two LAS files")
parser = argparse.ArgumentParser(
description="Compare dimensions between two LAS files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Compare all dimensions with exact match
python las_comparison.py file1.las file2.las

# Compare specific dimensions with precision per dimension
python las_comparison.py file1.las file2.las --dimensions X Y Z --precision X=0.001 Y=0.001 Z=0.0001

# Compare all dimensions with precision for specific ones
python las_comparison.py file1.las file2.las --precision X=0.001 Y=0.001
"""
)
parser.add_argument("file1", type=str, help="Path to first LAS file")
parser.add_argument("file2", type=str, help="Path to second LAS file")
parser.add_argument(
"--dimensions", nargs="*", help="List of dimensions to compare. If not specified, compares all dimensions."
)
parser.add_argument(
"--precision", nargs="*", metavar="DIM=VAL",
help="Tolerance for float comparison per dimension (format: DIMENSION=PRECISION). "
"Example: --precision X=0.001 Y=0.001 Z=0.0001. "
"Dimensions not specified will use exact comparison."
)

args = parser.parse_args()

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

result = compare_las_dimensions(file1, file2, args.dimensions)
# Parse precision dictionary from command line arguments
precision_dict = None
if args.precision:
precision_dict = {}
for prec_spec in args.precision:
try:
dim_name, prec_value = prec_spec.split('=', 1)
precision_dict[dim_name] = float(prec_value)
except ValueError:
parser.error(f"Invalid precision format: '{prec_spec}'. Expected format: DIMENSION=PRECISION (e.g., X=0.001)")

result = compare_las_dimensions(file1, file2, args.dimensions, precision_dict)
print(f"Dimensions comparison result: {'identical' if result[0] else 'different'}")
return result

Expand Down
Binary file added test/data/color/test_data_irc.tif
Binary file not shown.
Binary file added test/data/color/test_data_rgb.tif
Binary file not shown.
Loading
Loading