Skip to content

Commit e642c30

Browse files
authored
Merge pull request #167 from IGNF/feat_color_offline
Feat color offline
2 parents 3b7f425 + 6acdb14 commit e642c30

File tree

6 files changed

+183
-45
lines changed

6 files changed

+183
-45
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:

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)

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.

test/test_color.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
import pytest
88

99
from pdaltools import color
10+
from pdaltools.color import argument_parser
1011

1112
cwd = os.getcwd()
1213

1314
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
1415
TMPDIR = os.path.join(TEST_PATH, "tmp", "color")
1516

1617
INPUT_PATH = os.path.join(TEST_PATH, "data/test_noepsg_043500_629205_IGN69.laz")
18+
INPUT_PATH_TILE = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
19+
20+
RGB_IMAGE = os.path.join(TEST_PATH, "data/color/test_data_rgb.tif")
21+
IRC_IMAGE = os.path.join(TEST_PATH, "data/color/test_data_irc.tif")
22+
1723

1824
OUTPUT_FILE = os.path.join(TMPDIR, "Semis_2021_0435_6292_LA93_IGN69.colorized.las")
1925
EPSG = "2154"
@@ -33,12 +39,12 @@ def test_epsg_fail():
3339
RuntimeError,
3440
match="EPSG could not be inferred from metadata: No 'srs' key in metadata.",
3541
):
36-
color.color(INPUT_PATH, OUTPUT_FILE, "", 0.1, 15)
42+
color.color_from_stream(INPUT_PATH, OUTPUT_FILE, "", 0.1, 15)
3743

3844

3945
@pytest.mark.geopf
4046
def test_color_and_keeping_orthoimages():
41-
tmp_ortho, tmp_ortho_irc = color.color(INPUT_PATH, OUTPUT_FILE, EPSG, check_images=True)
47+
tmp_ortho, tmp_ortho_irc = color.color_from_stream(INPUT_PATH, OUTPUT_FILE, EPSG, check_images=True)
4248
assert Path(tmp_ortho.name).exists()
4349
assert Path(tmp_ortho_irc.name).exists()
4450

@@ -63,7 +69,7 @@ def test_color_narrow_cloud():
6369
input_path = os.path.join(TEST_PATH, "data/test_data_0436_6384_LA93_IGN69_single_point.laz")
6470
output_path = os.path.join(TMPDIR, "color_narrow_cloud_test_data_0436_6384_LA93_IGN69_single_point.colorized.laz")
6571
# Test that clouds that are smaller in width or height to 20cm are still colorized without an error.
66-
color.color(input_path, output_path, EPSG)
72+
color.color_from_stream(input_path, output_path, EPSG)
6773
with laspy.open(output_path, "r") as las:
6874
las_data = las.read()
6975
# Check all points are colored
@@ -75,10 +81,9 @@ def test_color_narrow_cloud():
7581

7682
@pytest.mark.geopf
7783
def test_color_standard_cloud():
78-
input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
7984
output_path = os.path.join(TMPDIR, "color_standard_cloud_test_data_77055_627760_LA93_IGN69.colorized.laz")
8085
# Test that clouds that are smaller in width or height to 20cm are still colorized without an error.
81-
color.color(input_path, output_path, EPSG)
86+
color.color_from_stream(INPUT_PATH_TILE, output_path, EPSG)
8287
with laspy.open(output_path, "r") as las:
8388
las_data = las.read()
8489
# Check all points are colored
@@ -92,7 +97,7 @@ def test_color_epsg_2975_forced():
9297
input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz")
9398
output_path = os.path.join(TMPDIR, "color_epsg_2975_forced_sample_lareunion_epsg2975.colorized.laz")
9499

95-
color.color(input_path, output_path, 2975)
100+
color.color_from_stream(input_path, output_path, 2975)
96101

97102

98103
# the test is not working, the image is not detected as white
@@ -104,7 +109,7 @@ def test_color_epsg_2975_forced():
104109
# output_path = os.path.join(TMPDIR, "sample_lareunion_epsg2975.colorized.white.laz")#
105110

106111
# with pytest.raises(ValueError) as excinfo:
107-
# color.color(input_path, output_path, check_images=True)
112+
# color.color_from_stream(input_path, output_path, check_images=True)
108113

109114
# assert "Downloaded image is white" in str(excinfo.value)
110115

@@ -114,18 +119,17 @@ def test_color_epsg_2975_detected():
114119
input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz")
115120
output_path = os.path.join(TMPDIR, "color_epsg_2975_detected_sample_lareunion_epsg2975.colorized.laz")
116121
# Test that clouds that are smaller in width or height to 20cm are still clorized without an error.
117-
color.color(input_path, output_path)
122+
color.color_from_stream(input_path, output_path)
118123

119124

120125
def test_color_vegetation_only():
121-
"""Test the color() function with only vegetation"""
122-
input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
126+
"""Test the color_from_stream() function with only vegetation"""
123127
output_path = os.path.join(TMPDIR, "test_color_vegetation.colorized.las")
124128
vegetation_path = os.path.join(TEST_PATH, "data/mock_vegetation.tif")
125129

126130
# Test with all parameters explicitly defined
127-
color.color(
128-
input_file=input_path,
131+
color.color_from_stream(
132+
input_file=INPUT_PATH_TILE,
129133
output_file=output_path,
130134
proj="2154", # EPSG:2154 (Lambert 93)
131135
color_rvb_enabled=False, # RGB enabled
@@ -153,14 +157,13 @@ def test_color_vegetation_only():
153157

154158
@pytest.mark.geopf
155159
def test_color_with_all_parameters():
156-
"""Test the color() function with all parameters specified"""
157-
input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
160+
"""Test the color_from_stream() function with all parameters specified"""
158161
output_path = os.path.join(TMPDIR, "test_color_all_params.colorized.las")
159162
vegetation_path = os.path.join(TEST_PATH, "data/mock_vegetation.tif")
160163

161164
# Test with all parameters explicitly defined
162-
tmp_ortho, tmp_ortho_irc = color.color(
163-
input_file=input_path,
165+
tmp_ortho, tmp_ortho_irc = color.color_from_stream(
166+
input_file=INPUT_PATH_TILE,
164167
output_file=output_path,
165168
proj="2154", # EPSG:2154 (Lambert 93)
166169
pixel_per_meter=2.0, # custom resolution
@@ -196,3 +199,56 @@ def test_color_with_all_parameters():
196199
# Verify that the vegetation dimension is present
197200
assert "vegetation_dim" in las_data.point_format.dimension_names, "Vegetation dimension should be present"
198201
assert not np.all(las_data.vegetation_dim == 0), "Vegetation dimension should not be empty"
202+
203+
204+
def test_color_from_files():
205+
output_path = os.path.join(TMPDIR, "color_standard_cloud_files_test_data_77055_627760_LA93_IGN69.colorized.laz")
206+
207+
color.color_from_files(INPUT_PATH_TILE, output_path, RGB_IMAGE, IRC_IMAGE)
208+
209+
assert os.path.exists(output_path)
210+
211+
with laspy.open(output_path, "r") as las:
212+
las_data = las.read()
213+
214+
# Verify that all points have been colorized (no 0 values)
215+
las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
216+
assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
217+
assert not np.any(las_data.nir == 0), "No point should have missing NIR"
218+
219+
220+
@pytest.mark.geopf
221+
def test_main_from_stream():
222+
output_file = os.path.join(TMPDIR, "main_from_stream", "output_main_from_stream.laz")
223+
os.makedirs(os.path.dirname(output_file))
224+
cmd = f"from_stream -i {INPUT_PATH_TILE} -o {output_file} -p {EPSG} --rvb --ir".split()
225+
args = argument_parser().parse_args(cmd)
226+
args.func(args)
227+
228+
assert os.path.exists(output_file)
229+
230+
with laspy.open(output_file, "r") as las:
231+
las_data = las.read()
232+
233+
# Verify that all points have been colorized (no 0 values)
234+
las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
235+
assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
236+
assert not np.any(las_data.nir == 0), "No point should have missing NIR"
237+
238+
239+
def test_main_from_files():
240+
output_file = os.path.join(TMPDIR, "main_from_files", "output_main_from_files.laz")
241+
os.makedirs(os.path.dirname(output_file))
242+
cmd = f"from_files -i {INPUT_PATH_TILE} -o {output_file} --image_RGB {RGB_IMAGE} --image_IRC {IRC_IMAGE}".split()
243+
args = argument_parser().parse_args(cmd)
244+
args.func(args)
245+
246+
assert os.path.exists(output_file)
247+
248+
with laspy.open(output_file, "r") as las:
249+
las_data = las.read()
250+
251+
# Verify that all points have been colorized (no 0 values)
252+
las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
253+
assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
254+
assert not np.any(las_data.nir == 0), "No point should have missing NIR"

test/test_unlock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import laspy
55
import pytest
66

7-
from pdaltools.color import color
7+
from pdaltools.color import color_from_stream
88
from pdaltools.las_info import las_info_metadata
99
from pdaltools.unlock_file import copy_and_hack_decorator, unlock_file
1010

@@ -39,7 +39,7 @@ def test_copy_and_hack_decorator_color():
3939
LAS_FILE = os.path.join(TMPDIR, "test_pdalfail_0643_6319_LA93_IGN69.las")
4040

4141
# Color works only when an epsg is present in the header or as a parameter
42-
color(LAZ_FILE, LAS_FILE, "2154", 1)
42+
color_from_stream(LAZ_FILE, LAS_FILE, "2154", 1)
4343

4444
las = laspy.read(LAS_FILE)
4545
print(las.header)

0 commit comments

Comments
 (0)