From bf917a72a03af997a75a626153da50bdf7757ef5 Mon Sep 17 00:00:00 2001 From: GLiegard Date: Fri, 21 Jun 2024 15:34:34 +0200 Subject: [PATCH 1/2] color: detect white images. choose stream for RGB and IRC colorization --- CHANGELOG.md | 3 + pdaltools/color.py | 66 +++++++++++++++++---- script/test/test_color_fail_white_image.sh | 19 ++++++ script/test/test_download_reunion.sh | 19 ++++++ test/data/image/colored.tif | Bin 0 -> 30505 bytes test/data/image/white.tif | Bin 0 -> 187989 bytes test/test_color.py | 33 +++++++++-- 7 files changed, 121 insertions(+), 19 deletions(-) create mode 100755 script/test/test_color_fail_white_image.sh create mode 100755 script/test/test_download_reunion.sh create mode 100644 test/data/image/colored.tif create mode 100644 test/data/image/white.tif diff --git a/CHANGELOG.md b/CHANGELOG.md index c60f655..aa0d7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +- color: choose streams for RGB colorization, and IRC colorization (doc https://geoservices.ign.fr/services-web-experts-ortho) +- color: detect white images. + # 1.5.2 - refactor tool to propagate header infos from one pipeline to another to use it by itself diff --git a/pdaltools/color.py b/pdaltools/color.py index d751e29..618e6ad 100644 --- a/pdaltools/color.py +++ b/pdaltools/color.py @@ -3,8 +3,10 @@ import time from math import ceil +import numpy as np import pdal import requests +from osgeo import gdal_array import pdaltools.las_info as las_info from pdaltools.unlock_file import copy_and_hack_decorator @@ -58,7 +60,15 @@ def newfn(*args, **kwargs): return decorator -def download_image_from_geoplateforme(proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile, timeout): +def is_image_white(filename: str): + raster_array = gdal_array.LoadFile(filename) + band_is_white = [np.all(band == 255) for band in raster_array] + return np.all(band_is_white) + + +def download_image_from_geoplateforme( + proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile, timeout, check_images +): # Give single-point clouds a width/height of at least one pixel to have valid BBOX and SIZE if minx == maxx: maxx = minx + 1 / pixel_per_meter @@ -88,6 +98,9 @@ def download_image_from_geoplateforme(proj, layer, minx, miny, maxx, maxy, pixel print(f"Ecriture du fichier: {outfile}") open(outfile, "wb").write(req.content) + if check_images and is_image_white(outfile): + raise ValueError(f"Downloaded image is white, with stream: {layer}") + @copy_and_hack_decorator def color( @@ -99,6 +112,9 @@ def color( color_rvb_enabled=True, color_ir_enabled=True, veget_index_file="", + check_images=False, + stream_RGB="ORTHOIMAGERY.ORTHOPHOTOS", + stream_IRC="ORTHOIMAGERY.ORTHOPHOTOS.IRC", ): metadata = las_info.las_info_metadata(input_file) minx, maxx, miny, maxy = las_info.get_bounds_from_header_info(metadata) @@ -122,8 +138,9 @@ def color( if color_rvb_enabled: tmp_ortho = tempfile.NamedTemporaryFile() download_image_from_geoplateforme_retrying( - proj, "ORTHOIMAGERY.ORTHOPHOTOS", minx, miny, maxx, maxy, pixel_per_meter, tmp_ortho.name, timeout_second + proj, stream_RGB, minx, miny, maxx, maxy, pixel_per_meter, tmp_ortho.name, timeout_second, check_images ) + pipeline |= pdal.Filter.colorization( raster=tmp_ortho.name, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0" ) @@ -132,16 +149,9 @@ def color( if color_ir_enabled: tmp_ortho_irc = tempfile.NamedTemporaryFile() download_image_from_geoplateforme_retrying( - proj, - "ORTHOIMAGERY.ORTHOPHOTOS.IRC", - minx, - miny, - maxx, - maxy, - pixel_per_meter, - tmp_ortho_irc.name, - timeout_second, + proj, stream_IRC, minx, miny, maxx, maxy, pixel_per_meter, tmp_ortho_irc.name, timeout_second, check_images ) + pipeline |= pdal.Filter.colorization(raster=tmp_ortho_irc.name, dimensions="Infrared:1:256.0") pipeline |= pdal.Writer.las( @@ -158,7 +168,7 @@ def color( def parse_args(): - parser = argparse.ArgumentParser("Colorize tool") + 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( @@ -171,9 +181,39 @@ def parse_args(): parser.add_argument( "--vegetation", type=str, default="", help="Vegetation file, value will be stored in Deviation field" ) + parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white") + parser.add_argument( + "--stream-RGB", + type=str, + default="ORTHOIMAGERY.ORTHOPHOTOS", + help="""WMS raster stream for RGB colorization: +default stream (ORTHOIMAGERY.ORTHOPHOTOS) let the server choose the resolution +for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS +for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""", + ) + parser.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""", + ) + return parser.parse_args() if __name__ == "__main__": args = parse_args() - color(args.input, args.output, args.proj, args.resolution, args.timeout, args.rvb, args.ir, args.vegetation) + color( + input_file=args.input, + output_file=args.output, + proj=args.proj, + pixel_per_meter=args.resolution, + timeout_second=args.timeout, + color_rvb_enabled=args.rvb, + color_ir_enabled=args.ir, + veget_index_file=args.vegetation, + check_images=args.check_images, + stream_RGB=args.stream_RGB, + stream_IRC=args.stream_IRC, + ) diff --git a/script/test/test_color_fail_white_image.sh b/script/test/test_color_fail_white_image.sh new file mode 100755 index 0000000..e2d46d2 --- /dev/null +++ b/script/test/test_color_fail_white_image.sh @@ -0,0 +1,19 @@ +mkdir tmp + +echo +echo First test should faild with white image error. +echo + +python -m pdaltools.color \ +-i ./test/data/sample_lareunion_epsg2975.laz \ +-o ./tmp/output.tif \ +--rvb -c + +echo +echo Second test should succeed, because we use RGB stream of 20 cm resolution. +echo + +python -m pdaltools.color \ +-i ./test/data/sample_lareunion_epsg2975.laz \ +-o ./tmp/output.tif \ +--rvb -c --stream-RGB HR.ORTHOIMAGERY.ORTHOPHOTOS diff --git a/script/test/test_download_reunion.sh b/script/test/test_download_reunion.sh new file mode 100755 index 0000000..93c639b --- /dev/null +++ b/script/test/test_download_reunion.sh @@ -0,0 +1,19 @@ +mkdir tmp + +# test flux sur la reunion, flux 20cm: HR.ORTHOIMAGERY.ORTHOPHOTOS => ok +curl -o tmp/reunion1_20cm_ok.tif "https://data.geopf.fr/wms-r/wms?LAYERS=HR.ORTHOIMAGERY.ORTHOPHOTOS&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7654000,377100,7654100&WIDTH=500&HEIGHT=500" + +# test flux sur la reunion, flux 50cm: ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO => ok +curl -o tmp/reunion1_50cm_ok.tif "https://data.geopf.fr/wms-r/wms?LAYERS=ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7654000,377100,7654100&WIDTH=500&HEIGHT=500" + +# test flux sur la reunion, flux qui choisit: ORTHOIMAGERY.ORTHOPHOTOS => timeout +curl -o tmp/reunion1_choix_timeout.txt "https://data.geopf.fr/wms-r/wms?LAYERS=ORTHOIMAGERY.ORTHOPHOTOS&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7654000,377100,7654100&WIDTH=500&HEIGHT=500" + +# test flux sur la reunion, flux qui choisit: ORTHOIMAGERY.ORTHOPHOTOS => blanc +curl -o tmp/reunion2_choix_blanc.tif "https://data.geopf.fr/wms-r/wms?LAYERS=ORTHOIMAGERY.ORTHOPHOTOS&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7655950,377050,7655999.99&WIDTH=250&HEIGHT=250" + +# test flux sur la reunion, flux 20cm: HR.ORTHOIMAGERY.ORTHOPHOTOS => ok +curl -o tmp/reunion2_20cm_ok.tif "https://data.geopf.fr/wms-r/wms?LAYERS=HR.ORTHOIMAGERY.ORTHOPHOTOS&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7655950,377050,7655999.99&WIDTH=250&HEIGHT=250" + +# test flux sur la reunion, flux 50cm: ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO => ok +curl -o tmp/reunion2_50cm_ok.tif "https://data.geopf.fr/wms-r/wms?LAYERS=ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES=&CRS=EPSG:2975&BBOX=377000,7655950,377050,7655999.99&WIDTH=250&HEIGHT=250" \ No newline at end of file diff --git a/test/data/image/colored.tif b/test/data/image/colored.tif new file mode 100644 index 0000000000000000000000000000000000000000..5318a41f938ba94888a1f2637f97a303f85d704d GIT binary patch literal 30505 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 Q5nb()D1A{^G)JZX0kPiZrT_o{ literal 0 HcmV?d00001 diff --git a/test/data/image/white.tif b/test/data/image/white.tif new file mode 100644 index 0000000000000000000000000000000000000000..ea532f7229a3dd37c0282b34a5bb194adf8c12f8 GIT binary patch literal 187989 zcmeIuF>6y%6bJD0-ismDP_bycxn!}9q7+?X29b(Gp{q`=egFr-qJ`k1tBQm81#Fjs ztCL;C47vWC zce%T)`<{LeYu#pjs`h`)FV58S{x_>f=B(BF?x!}k>&*Arv;1gW=6}fg;qEwwnO}0h znRl_3xtcTg=Hob)^+DGA^EOUo{kgwHu2FfKyi*e|p7-l;y773neB;^S^moUuKhK; zl1&Usu1}7S_vZaSj!HJMR5IVQWEZDP<|Y2Nc5Ytnk9T&@mivEPxih=>^7Z8H&u`Of zTie&KT)4b(>3%FM0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNCfp9}l~ DYXbJF literal 0 HcmV?d00001 diff --git a/test/test_color.py b/test/test_color.py index 4e2c33b..42af206 100644 --- a/test/test_color.py +++ b/test/test_color.py @@ -47,7 +47,7 @@ def test_epsg_fail(): @pytest.mark.geopf def test_color_and_keeping_orthoimages(): - tmp_ortho, tmp_ortho_irc = color.color(INPUT_PATH, OUTPUT_FILE, epsg) + tmp_ortho, tmp_ortho_irc = color.color(INPUT_PATH, OUTPUT_FILE, epsg, check_images=True) assert Path(tmp_ortho.name).exists() assert Path(tmp_ortho_irc.name).exists() @@ -63,7 +63,7 @@ def test_color_narrow_cloud(): @pytest.mark.geopf def test_download_image_ok(): tif_output = os.path.join(TMPDIR, "download_image.tif") - color.download_image_from_geoplateforme(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, tif_output, 15) + color.download_image_from_geoplateforme(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, tif_output, 15, True) @pytest.mark.geopf @@ -74,6 +74,27 @@ def test_color_epsg_2975_forced(): color.color(input_path, output_path, 2975) +def test_is_image_white_true(): + input_path = os.path.join(TEST_PATH, "data/image/white.tif") + assert color.is_image_white(input_path), "This image should be detected as white" + + +def test_is_image_white_false(): + input_path = os.path.join(TEST_PATH, "data/image/colored.tif") + assert not color.is_image_white(input_path), "This image should NOT be detected as white" + + +@pytest.mark.geopf +def test_color_raise_for_white_image(): + input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz") + output_path = os.path.join(TMPDIR, "sample_lareunion_epsg2975.colorized.white.laz") + + with pytest.raises(ValueError) as excinfo: + color.color(input_path, output_path, check_images=True) + + assert "Downloaded image is white" in str(excinfo.value) + + @pytest.mark.geopf def test_color_epsg_2975_detected(): input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz") @@ -86,14 +107,14 @@ def test_color_epsg_2975_detected(): def test_download_image_raise1(): retry_download = color.retry(2, 5)(color.download_image_from_geoplateforme) with pytest.raises(requests.exceptions.HTTPError): - retry_download(epsg, "MAUVAISE_COUCHE", minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15) + retry_download(epsg, "MAUVAISE_COUCHE", minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15, True) @pytest.mark.geopf def test_download_image_raise2(): retry_download = color.retry(2, 5)(color.download_image_from_geoplateforme) with pytest.raises(requests.exceptions.HTTPError): - retry_download("9001", layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15) + retry_download("9001", layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15, True) def test_retry_on_server_error(): @@ -101,7 +122,7 @@ def test_retry_on_server_error(): mock.get(requests_mock.ANY, status_code=502, reason="Bad Gateway") with pytest.raises(requests.exceptions.HTTPError): retry_download = color.retry(2, 1, 2)(color.download_image_from_geoplateforme) - retry_download(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15) + retry_download(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15, True) history = mock.request_history assert len(history) == 3 @@ -111,7 +132,7 @@ def test_retry_on_connection_error(): mock.get(requests_mock.ANY, exc=requests.exceptions.ConnectionError) with pytest.raises(requests.exceptions.ConnectionError): retry_download = color.retry(2, 1)(color.download_image_from_geoplateforme) - retry_download(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15) + retry_download(epsg, layer, minx, miny, maxx, maxy, pixel_per_meter, OUTPUT_FILE, 15, True) history = mock.request_history assert len(history) == 3 From 2acd0d60695a4e892f87edc539ceb8939860cb57 Mon Sep 17 00:00:00 2001 From: GLiegard Date: Fri, 21 Jun 2024 15:36:36 +0200 Subject: [PATCH 2/2] version 1.6.0 --- CHANGELOG.md | 1 + pdaltools/_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0d7ba..04278b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +# 1.6.0 - color: choose streams for RGB colorization, and IRC colorization (doc https://geoservices.ign.fr/services-web-experts-ortho) - color: detect white images. diff --git a/pdaltools/_version.py b/pdaltools/_version.py index aa04392..6a32fb0 100644 --- a/pdaltools/_version.py +++ b/pdaltools/_version.py @@ -1,4 +1,4 @@ -__version__ = "1.5.2" +__version__ = "1.6.0" if __name__ == "__main__":