From b45a3d3a810f7772788697693690a2f4049abc7b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 3 Sep 2024 20:56:25 +0200 Subject: [PATCH 1/5] GTiff: when reading a unrecognized value of color interpretation in the GDAL_METADATA, expose it in a COLOR_INTERPRETATION metadata item --- .../gcore/data/gtiff/unknown_colorinterp.tif | Bin 0 -> 872 bytes autotest/gcore/tiff_read.py | 12 ++++++++++++ frmts/gtiff/gtiffdataset_read.cpp | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 autotest/gcore/data/gtiff/unknown_colorinterp.tif diff --git a/autotest/gcore/data/gtiff/unknown_colorinterp.tif b/autotest/gcore/data/gtiff/unknown_colorinterp.tif new file mode 100644 index 0000000000000000000000000000000000000000..02466ea4db36cefb828939955828c3ae0e71e71a GIT binary patch literal 872 zcmZutF^f|{5Z;%BcpO0#jDlS8$W_|J6Tv2i3!X?Mmy`2ENf*`!%3hKqlgddUI*?`& zi!B!`U-^&}>AccTQu_=13zqurOAaEq%Q7D`-+c4!wA;7UmQw19Qc+PwK-ci14P02` zz$&hB;FUM<)u@#B{zRd=k-xsiLG#9%^9h`t4bHO-eECB~6~Vm+&U>k-GH?g@>#V3^ z;3@EYR!}>@-+W_2J4NzNEgp{o(4*7Q_|0&VjL&M#mpF}j=xuH{>i>$aCMnh3U!jY5k^g_( z|8=t!DnCAV@)&TG@9dINr4`14@?4=bpH)MBYd$Nhsw!jkk@~o+Dj~jjUCQIF-EQmP z?)|tPKR-E)-;Rb!ytmh#=E)J@ z%}lt4oFNp&2QeaI$0>_3;`uzwoFj)m60bwc(tOXHwE}elJq*bhjKH$c63A=T3k^R) zxkYBt$c4$)YO66)-~mO(AHoSm_eBandInterp = - GDALGetColorInterpretationByName(pszUnescapedValue); + if (EQUAL(pszUnescapedValue, "undefined")) + poBand->m_eBandInterp = GCI_Undefined; + else + { + poBand->m_eBandInterp = + GDALGetColorInterpretationByName( + pszUnescapedValue); + if (poBand->m_eBandInterp == GCI_Undefined) + { + poBand->m_oGTiffMDMD.SetMetadataItem( + "COLOR_INTERPRETATION", pszUnescapedValue); + } + } } else { From 6123db2635954a87251ee739c38c7e6d2cfe525b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 3 Sep 2024 18:57:23 +0200 Subject: [PATCH 2/5] Add new GCI_ constants in particular for InfraRed, and 'standardize' a band-level IMAGERY metadata domain Added values: * GCI_PanBand: Panchromatic band [0.40 - 1.00 um] * GCI_CoastalBand: Coastal band [0.40 - 0.45 um] * GCI_RedEdgeBand: Red-edge band [0.69 - 0.79 um] * GCI_NIRBand: Near-InfraRed (NIR) band [0.75 - 1.40 um] * GCI_SWIRBand: Short-Wavelength InfraRed (SWIR) band [1.40 - 3.00 um] * GCI_MWIRBand: Mid-Wavelength InfraRed (MWIR) band [3.00 - 8.00 um] * GCI_LWIRBand: Long-Wavelength InfraRed (LWIR) band [8.00 - 15 um] * GCI_TIRBand: Thermal InfraRed (TIR) band (MWIR or LWIR) [3 - 15 um] * GCI_OtherIRBand: Other infrared band [0.75 - 1000 um] For spectral bands, the wavelength ranges are indicative only, and may vary depending on sensors. STACIT driver enhanced to map STAC common_name to GCI_ constants gdalinfo -json output enhanced to output STAC common_name from GCI_ constants Items in the IMAGERY band-level metadata domain: - CENTRAL_WAVELENGTH_UM: Central Wavelength in micrometers. - FWHM_UM: Full-width half-maximum (FWHM) in micrometers. Filled by the SENTINEL2 and ENVI drivers (if corresponding metadata items are found in the ENVI header) --- MIGRATION_GUIDE.TXT | 3 + apps/gdal_translate_lib.cpp | 19 ++- apps/gdalinfo_lib.cpp | 28 +++-- autotest/gdrivers/envi.py | 117 ++++++++++++++++++ autotest/gdrivers/sentinel2.py | 21 ++-- autotest/gdrivers/stacit.py | 2 +- autotest/utilities/test_gdalinfo_lib.py | 11 ++ doc/source/programs/gdal_edit.rst | 6 +- doc/source/programs/gdal_translate.rst | 8 +- doc/source/spelling_wordlist.txt | 1 + doc/source/user/raster_data_model.rst | 52 +++++--- frmts/raw/envidataset.cpp | 44 +++++++ frmts/sentinel2/sentinel2dataset.cpp | 31 +++-- frmts/stacit/stacitdataset.cpp | 16 +-- frmts/vrt/data/gdalvrt.xsd | 9 ++ gcore/gdal.h | 37 +++++- gcore/gdal_misc.cpp | 107 +++++++++++++++- gcore/gdal_priv.h | 5 + swig/include/gdal.i | 3 + swig/include/gdalconst.i | 15 ++- .../gdal-utils/osgeo_utils/gdal_edit.py | 29 ++--- 21 files changed, 464 insertions(+), 100 deletions(-) diff --git a/MIGRATION_GUIDE.TXT b/MIGRATION_GUIDE.TXT index f1155642d493..146ed58ef8ff 100644 --- a/MIGRATION_GUIDE.TXT +++ b/MIGRATION_GUIDE.TXT @@ -18,6 +18,9 @@ MIGRATION GUIDE FROM GDAL 3.9 to GDAL 3.10 - Python bindings: Band.GetStatistics() and Band.ComputeStatistics() now return a None value in case of error (when exceptions are not enabled) +- New color interpretation (GCI_xxxx) items have been added to the GDALColorInterp + enumeration. Code testing color interpretation may need to be adapted. + MIGRATION GUIDE FROM GDAL 3.8 to GDAL 3.9 ----------------------------------------- diff --git a/apps/gdal_translate_lib.cpp b/apps/gdal_translate_lib.cpp index 79d903017568..fdd31db6289b 100644 --- a/apps/gdal_translate_lib.cpp +++ b/apps/gdal_translate_lib.cpp @@ -2737,18 +2737,11 @@ static void CopyBandInfo(GDALRasterBand *poSrcBand, GDALRasterBand *poDstBand, static int GetColorInterp(const char *pszStr) { - if (EQUAL(pszStr, "red")) - return GCI_RedBand; - if (EQUAL(pszStr, "green")) - return GCI_GreenBand; - if (EQUAL(pszStr, "blue")) - return GCI_BlueBand; - if (EQUAL(pszStr, "alpha")) - return GCI_AlphaBand; - if (EQUAL(pszStr, "gray") || EQUAL(pszStr, "grey")) - return GCI_GrayIndex; if (EQUAL(pszStr, "undefined")) return GCI_Undefined; + const int eInterp = GDALGetColorInterpretationByName(pszStr); + if (eInterp != GCI_Undefined) + return eInterp; CPLError(CE_Warning, CPLE_NotSupported, "Unsupported color interpretation: %s", pszStr); return -1; @@ -3078,7 +3071,8 @@ GDALTranslateOptionsGetParser(GDALTranslateOptions *psOptions, _("Add the indicated ground control point to the output dataset.")); argParser->add_argument("-colorinterp") - .metavar("{red|green|blue|alpha|gray|undefined},...") + .metavar("{red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|" + "swir|mwir|lwir|...},...") .action( [psOptions](const std::string &s) { @@ -3093,7 +3087,8 @@ GDALTranslateOptionsGetParser(GDALTranslateOptions *psOptions, argParser->add_argument("-colorinterp_X") .append() - .metavar("{red|green|blue|alpha|gray|undefined}") + .metavar("{red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|" + "swir|mwir|lwir|...}") .help(_("Override the color interpretation of band X.")); { diff --git a/apps/gdalinfo_lib.cpp b/apps/gdalinfo_lib.cpp index 293bdbbf66d2..e61ef39ff8e5 100644 --- a/apps/gdalinfo_lib.cpp +++ b/apps/gdalinfo_lib.cpp @@ -1128,25 +1128,21 @@ char *GDALInfo(GDALDatasetH hDataset, const GDALInfoOptions *psOptions) json_object_object_add(poStacEOBand, "name", poBandName); } - if (GDALGetDescription(hBand) != nullptr && - strlen(GDALGetDescription(hBand)) > 0) + const char *pszBandDesc = GDALGetDescription(hBand); + if (pszBandDesc != nullptr && strlen(pszBandDesc) > 0) { if (bJson) { - json_object *poBandDescription = - json_object_new_string(GDALGetDescription(hBand)); json_object_object_add(poBand, "description", - poBandDescription); + json_object_new_string(pszBandDesc)); - json_object *poStacBandDescription = - json_object_new_string(GDALGetDescription(hBand)); json_object_object_add(poStacEOBand, "description", - poStacBandDescription); + json_object_new_string(pszBandDesc)); } else { Concat(osStr, psOptions->bStdoutOutput, " Description = %s\n", - GDALGetDescription(hBand)); + pszBandDesc); } } else @@ -1161,6 +1157,17 @@ char *GDALInfo(GDALDatasetH hDataset, const GDALInfoOptions *psOptions) } } + if (bJson) + { + const char *pszCommonName = GDALGetSTACCommonNameFromColorInterp( + GDALGetRasterColorInterpretation(hBand)); + if (pszCommonName) + { + json_object_object_add(poStacEOBand, "common_name", + json_object_new_string(pszCommonName)); + } + } + { int bGotMin = FALSE; int bGotMax = FALSE; @@ -2269,6 +2276,9 @@ static void GDALInfoReportMetadata(const GDALInfoOptions *psOptions, GDALInfoPrintMetadata(psOptions, hObject, "RPC", "RPC Metadata", pszIndent, bJson, poMetadata, osStr); } + + GDALInfoPrintMetadata(psOptions, hObject, "IMAGERY", "Imagery", pszIndent, + bJson, poMetadata, osStr); } /************************************************************************/ diff --git a/autotest/gdrivers/envi.py b/autotest/gdrivers/envi.py index 9cc41b9f074e..23af13136c28 100755 --- a/autotest/gdrivers/envi.py +++ b/autotest/gdrivers/envi.py @@ -1056,3 +1056,120 @@ def test_envi_read_metadata_with_leading_space(): assert ds.GetRasterBand(1).GetMetadataItem("wavelength") == "3" ds = None gdal.GetDriverByName("ENVI").Delete("/vsimem/test.bin") + + +############################################################################### +# Test wavelength / fwhm + + +def test_envi_read_wavelength_fwhm_um(): + + gdal.FileFromMemBuffer( + "/vsimem/test.hdr", + """ENVI +samples = 1 +lines = 1 +bands = 3 +header offset = 0 +file type = ENVI Standard +data type = 1 +interleave = bip +sensor type = Unknown +byte order = 0 +wavelength units = um +wavelength = {3, 2, 1} +fwhm = {.3, .2, .1}""", + ) + gdal.FileFromMemBuffer("/vsimem/test.bin", "xyz") + + ds = gdal.Open("/vsimem/test.bin") + assert ( + ds.GetRasterBand(1).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "3.000" + ) + assert ds.GetRasterBand(1).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.300" + assert ( + ds.GetRasterBand(2).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "2.000" + ) + assert ds.GetRasterBand(2).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.200" + ds = None + gdal.GetDriverByName("ENVI").Delete("/vsimem/test.bin") + + +############################################################################### +# Test wavelength / fwhm + + +def test_envi_read_wavelength_fwhm_nm(): + + gdal.FileFromMemBuffer( + "/vsimem/test.hdr", + """ENVI +samples = 1 +lines = 1 +bands = 3 +header offset = 0 +file type = ENVI Standard +data type = 1 +interleave = bip +sensor type = Unknown +byte order = 0 +wavelength units = nm +wavelength = {3000, 2000, 1000} +fwhm = {300, 200, 100}""", + ) + gdal.FileFromMemBuffer("/vsimem/test.bin", "xyz") + + ds = gdal.Open("/vsimem/test.bin") + assert ( + ds.GetRasterBand(1).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "3.000" + ) + assert ds.GetRasterBand(1).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.300" + assert ( + ds.GetRasterBand(2).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "2.000" + ) + assert ds.GetRasterBand(2).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.200" + ds = None + gdal.GetDriverByName("ENVI").Delete("/vsimem/test.bin") + + +############################################################################### +# Test wavelength / fwhm + + +def test_envi_read_wavelength_fwhm_mm(): + + gdal.FileFromMemBuffer( + "/vsimem/test.hdr", + """ENVI +samples = 1 +lines = 1 +bands = 3 +header offset = 0 +file type = ENVI Standard +data type = 1 +interleave = bip +sensor type = Unknown +byte order = 0 +wavelength units = mm +wavelength = {0.003, 0.002, 0.001} +fwhm = {0.0003, 0.0002, 0.0001}""", + ) + gdal.FileFromMemBuffer("/vsimem/test.bin", "xyz") + + ds = gdal.Open("/vsimem/test.bin") + assert ( + ds.GetRasterBand(1).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "3.000" + ) + assert ds.GetRasterBand(1).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.300" + assert ( + ds.GetRasterBand(2).GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") + == "2.000" + ) + assert ds.GetRasterBand(2).GetMetadataItem("FWHM_UM", "IMAGERY") == "0.200" + ds = None + gdal.GetDriverByName("ENVI").Delete("/vsimem/test.bin") diff --git a/autotest/gdrivers/sentinel2.py b/autotest/gdrivers/sentinel2.py index 63cbb9e6b836..4d5901feda47 100755 --- a/autotest/gdrivers/sentinel2.py +++ b/autotest/gdrivers/sentinel2.py @@ -244,6 +244,9 @@ def test_sentinel2_l1c_2(): pprint.pprint(got_md) pytest.fail() + assert band.GetMetadataItem("CENTRAL_WAVELENGTH_UM", "IMAGERY") == "0.665" + assert band.GetMetadataItem("FWHM_UM", "IMAGERY") == "0.030" + assert band.GetColorInterpretation() == gdal.GCI_RedBand assert band.DataType == gdal.GDT_UInt16 @@ -252,7 +255,7 @@ def test_sentinel2_l1c_2(): band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -843,7 +846,7 @@ def test_sentinel2_l1c_tile_3(): band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -2618,7 +2621,7 @@ def test_sentinel2_l1c_safe_compact_2(): band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -2804,7 +2807,7 @@ def test_sentinel2_l1c_processing_baseline_5_09__1(): band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -2900,13 +2903,13 @@ def test_sentinel2_l1c_processing_baseline_5_09__2(): pprint.pprint(got_md) pytest.fail() - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_RedEdgeBand assert band.DataType == gdal.GDT_UInt16 band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -3035,7 +3038,7 @@ def test_sentinel2_l2a_processing_baseline_5_09__1(): band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { @@ -3158,13 +3161,13 @@ def test_sentinel2_l2a_processing_baseline_5_09__2(): pprint.pprint(got_md) pytest.fail() - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_RedEdgeBand assert band.DataType == gdal.GDT_UInt16 band = ds.GetRasterBand(4) - assert band.GetColorInterpretation() == gdal.GCI_Undefined + assert band.GetColorInterpretation() == gdal.GCI_NIRBand got_md = band.GetMetadata() expected_md = { diff --git a/autotest/gdrivers/stacit.py b/autotest/gdrivers/stacit.py index d08f8f8fda48..5f5dec2b7a90 100755 --- a/autotest/gdrivers/stacit.py +++ b/autotest/gdrivers/stacit.py @@ -162,7 +162,7 @@ def test_stacit_overlapping_sources(): # Check that the source covered by another one is not listed vrt = ds.GetMetadata("xml:VRT")[0] only_one_simple_source = """ - Gray + Coastal data/byte.tif 1 diff --git a/autotest/utilities/test_gdalinfo_lib.py b/autotest/utilities/test_gdalinfo_lib.py index 8c7cfd3ead44..995f6fa4b15d 100755 --- a/autotest/utilities/test_gdalinfo_lib.py +++ b/autotest/utilities/test_gdalinfo_lib.py @@ -341,3 +341,14 @@ def test_gdalinfo_lib_nomask(tmp_path): ret = gdal.Info(ds, format="json", showMask=False) assert "mask" not in ret["bands"][0] + + +############################################################################### + + +def test_gdalinfo_lib_json_stac_common_name(): + + ds = gdal.GetDriverByName("MEM").Create("", 1, 1) + ds.GetRasterBand(1).SetColorInterpretation(gdal.GCI_PanBand) + ret = gdal.Info(ds, options="-json") + assert ret["stac"]["eo:bands"][0]["common_name"] == "pan" diff --git a/doc/source/programs/gdal_edit.rst b/doc/source/programs/gdal_edit.rst index 983f315c478d..0fa57725523d 100644 --- a/doc/source/programs/gdal_edit.rst +++ b/doc/source/programs/gdal_edit.rst @@ -15,14 +15,14 @@ Synopsis .. code-block:: - gdal_edit [--help] [--help-general] [-ro] [-a_srs ] + gdal_edit [--help] [--help-general] [-ro] [-a_srs ] [-a_ullr ] [-a_ulurll ] [-tr ] [-unsetgt] [-unsetrpc] [-a_nodata ] [-unsetnodata] [-a_coord_epoch ] [-unsetepoch] [-unsetstats] [-stats] [-approx_stats] [-setstats ] [-scale ] [-offset ] [-units ] - [-colorinterp_ {red|green|blue|alpha|gray|undefined}]... + [-colorinterp_ {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...}]... [-gcp []]... [-unsetmd] [-oo =]... [-mo =]... @@ -172,7 +172,7 @@ It works only with raster formats that support update access to existing dataset .. versionadded:: 3.1 -.. option:: -colorinterp_ {red|green|blue|alpha|gray|undefined} +.. option:: -colorinterp_ {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...} Change the color interpretation of band X (where X is a valid band number, starting at 1). diff --git a/doc/source/programs/gdal_translate.rst b/doc/source/programs/gdal_translate.rst index 8c13b82d5e56..1b7d9b39041c 100644 --- a/doc/source/programs/gdal_translate.rst +++ b/doc/source/programs/gdal_translate.rst @@ -32,8 +32,8 @@ Synopsis [-a_gt ] [-a_scale ] [-a_offset ] [-nogcp] [-gcp []]... - |-colorinterp{_bn} {red|green|blue|alpha|gray|undefined}] - |-colorinterp {red|green|blue|alpha|gray|undefined},...] + |-colorinterp{_bn} {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...}] + |-colorinterp {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...},...] [-mo =]... [-dmo "DOMAIN:META-TAG=VALUE"]... [-q] [-sds] [-co =]... [-stats] [-norat] [-noxmp] [-oo =]... @@ -289,14 +289,14 @@ resampling, and rescaling pixels in the process. nodata value, this does not cause pixel values that are equal to that nodata value to be changed to the value specified with this option. -.. option:: -colorinterp_X +.. option:: -colorinterp_X Override the color interpretation of band X (where X is a valid band number, starting at 1) .. versionadded:: 2.3 -.. option:: -colorinterp {red|green|blue|alpha|gray|undefined},... +.. option:: -colorinterp {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...},... Override the color interpretation of all specified bands. For example -colorinterp red,green,blue,alpha for a 4 band output dataset. diff --git a/doc/source/spelling_wordlist.txt b/doc/source/spelling_wordlist.txt index 991c9919ec42..f4f90cad3f81 100644 --- a/doc/source/spelling_wordlist.txt +++ b/doc/source/spelling_wordlist.txt @@ -1392,6 +1392,7 @@ InfoFormat InfoOptions Informatisé Informix +InfraRed ing ini init diff --git a/doc/source/user/raster_data_model.rst b/doc/source/user/raster_data_model.rst index 35b80ae79ca6..0deac2fd404a 100644 --- a/doc/source/user/raster_data_model.rst +++ b/doc/source/user/raster_data_model.rst @@ -189,6 +189,13 @@ For satellite or aerial imagery the IMAGERY Domain may be present. It depends on - CLOUDCOVER: Cloud coverage. The value between 0 - 100 or 999 if not available - ACQUISITIONDATETIME: The image acquisition date time in UTC +Starting with GDAL 3.10, there also exists a raster band level IMAGERY metadata domain with the following items: + +- CENTRAL_WAVELENGTH_UM: Central Wavelength in micrometers. +- FWHM_UM: Full-width half-maximum (FWHM) in micrometers. + +At time of writing, they are set by the SENTINEL2 and ENVI drivers (if corresponding metadata items are found in the ENVI header) + xml: Domains ++++++++++++ @@ -228,20 +235,37 @@ A raster band has the following properties: - An optional raster unit name. For instance, this might indicate linear units for elevation data. - A color interpretation for the band. This is one of: - * GCI_Undefined: the default, nothing is known. - * GCI_GrayIndex: this is an independent gray-scale image - * GCI_PaletteIndex: this raster acts as an index into a color table - * GCI_RedBand: this raster is the red portion of an RGB or RGBA image - * GCI_GreenBand: this raster is the green portion of an RGB or RGBA image - * GCI_BlueBand: this raster is the blue portion of an RGB or RGBA image - * GCI_AlphaBand: this raster is the alpha portion of an RGBA image - * GCI_HueBand: this raster is the hue of an HLS image - * GCI_SaturationBand: this raster is the saturation of an HLS image - * GCI_LightnessBand: this raster is the lightness of an HLS image - * GCI_CyanBand: this band is the cyan portion of a CMY or CMYK image - * GCI_MagentaBand: this band is the magenta portion of a CMY or CMYK image - * GCI_YellowBand: this band is the yellow portion of a CMY or CMYK image - * GCI_BlackBand: this band is the black portion of a CMYK image. + * GCI_Undefined / "Undefined": default, nothing is known. + * GCI_GrayIndex / "Gray": independent gray-scale image + * GCI_PaletteIndex / "Palette": this raster acts as an index into a color table + * GCI_RedBand / "Red": red portion of an RGB or RGBA image, or red spectral band [0.62 - 0.69 um] + * GCI_GreenBand/ "Green": green portion of an RGB or RGBA image, or green spectral band [0.51 - 0.60 um] + * GCI_BlueBand / "Blue": blue portion of an RGB or RGBA image, or blue spectral band [0.45 - 0.53 um] + * GCI_AlphaBand / "Alpha": alpha portion of an RGBA image + * GCI_HueBand / "Hue": hue of a HLS image + * GCI_SaturationBand / "Saturation": saturation of a HLS image + * GCI_LightnessBand / "Lightness": lightness of a HLS image + * GCI_CyanBand / "Cyan": cyan portion of a CMY or CMYK image + * GCI_MagentaBand / "Magenta": magenta portion of a CMY or CMYK image + * GCI_YellowBand / "Yellow": yellow portion of a CMY or CMYK image, or yellow spectral band [0.58 - 0.62 um] + * GCI_BlackBand / "Black": black portion of a CMYK image. + + Below values have been added in GDAL 3.10: + + * GCI_PanBand / "Pan": Panchromatic band [0.40 - 1.00 um] + * GCI_CoastalBand / "Coastal": Coastal band [0.40 - 0.45 um] + * GCI_RedEdgeBand / "RedEdge": Red-edge band [0.69 - 0.79 um] + * GCI_NIRBand / "NIR": Near-InfraRed (NIR) band [0.75 - 1.40 um] + * GCI_SWIRBand / "SWIR": Short-Wavelength InfraRed (SWIR) band [1.40 - 3.00 um] + * GCI_MWIRBand / "MWIR": Mid-Wavelength InfraRed (MWIR) band [3.00 - 8.00 um] + * GCI_LWIRBand / "LWIR": Long-Wavelength InfraRed (LWIR) band [8.00 - 15 um] + * GCI_TIRBand / "TIR": Thermal InfraRed (TIR) band (MWIR or LWIR) [3 - 15 um] + * GCI_OtherIRBand / "OtherIR": Other infrared band [0.75 - 1000 um] + + For spectral bands, the wavelength ranges are indicative only, and may vary + depending on sensors. The ``CENTRAL_WAVELENGTH_UM`` and ``FWHM_UM`` metadata + items in the band ``IMAGERY`` metadata domain of the raster band, when present, will + give more accurate characteristics. - A color table, described in more detail later. - Knowledge of reduced resolution overviews (pyramids) if available. diff --git a/frmts/raw/envidataset.cpp b/frmts/raw/envidataset.cpp index 4dba3e5bc89f..13f732150404 100644 --- a/frmts/raw/envidataset.cpp +++ b/frmts/raw/envidataset.cpp @@ -2401,9 +2401,12 @@ ENVIDataset *ENVIDataset::Open(GDALOpenInfo *poOpenInfo, bool bFileSizeCheck) { char **papszBandNames = poDS->SplitList(pszBandNames); char **papszWL = poDS->SplitList(pszWaveLength); + const char *pszFWHM = poDS->m_aosHeader["fwhm"]; + char **papszFWHM = pszFWHM ? poDS->SplitList(pszFWHM) : nullptr; const char *pszWLUnits = nullptr; const int nWLCount = CSLCount(papszWL); + const int nFWHMCount = CSLCount(papszFWHM); if (papszWL) { // If WL information is present, process wavelength units. @@ -2460,6 +2463,29 @@ ENVIDataset *ENVIDataset::Open(GDALOpenInfo *poOpenInfo, bool bFileSizeCheck) CPLString osBandId = CPLSPrintf("Band_%i", i + 1); poDS->SetMetadataItem(osBandId, osBandName.c_str()); + const auto ConvertWaveLength = + [pszWLUnits](double dfVal) -> const char * + { + if (EQUAL(pszWLUnits, "Micrometers") || EQUAL(pszWLUnits, "um")) + { + return CPLSPrintf("%.3f", dfVal); + } + else if (EQUAL(pszWLUnits, "Nanometers") || + EQUAL(pszWLUnits, "nm")) + { + return CPLSPrintf("%.3f", dfVal / 1000); + } + else if (EQUAL(pszWLUnits, "Millimeters") || + EQUAL(pszWLUnits, "mm")) + { + return CPLSPrintf("%.3f", dfVal * 1000); + } + else + { + return nullptr; + } + }; + // Set wavelength metadata to band. if (papszWL && nWLCount > i) { @@ -2470,11 +2496,29 @@ ENVIDataset *ENVIDataset::Open(GDALOpenInfo *poOpenInfo, bool bFileSizeCheck) { poDS->GetRasterBand(i + 1)->SetMetadataItem( "wavelength_units", pszWLUnits); + + if (const char *pszVal = + ConvertWaveLength(CPLAtof(papszWL[i]))) + { + poDS->GetRasterBand(i + 1)->SetMetadataItem( + "CENTRAL_WAVELENGTH_UM", pszVal, "IMAGERY"); + } + } + } + + if (papszFWHM && nFWHMCount > i && pszWLUnits) + { + if (const char *pszVal = + ConvertWaveLength(CPLAtof(papszFWHM[i]))) + { + poDS->GetRasterBand(i + 1)->SetMetadataItem( + "FWHM_UM", pszVal, "IMAGERY"); } } } CSLDestroy(papszWL); CSLDestroy(papszBandNames); + CSLDestroy(papszFWHM); } // Apply "default bands" if we have it to set RGB color interpretation. diff --git a/frmts/sentinel2/sentinel2dataset.cpp b/frmts/sentinel2/sentinel2dataset.cpp index 2de45ee872f2..6be2744bbb4a 100644 --- a/frmts/sentinel2/sentinel2dataset.cpp +++ b/frmts/sentinel2/sentinel2dataset.cpp @@ -78,15 +78,23 @@ typedef struct GDALColorInterp eColorInterp; } SENTINEL2BandDescription; +/* clang-format off */ static const SENTINEL2BandDescription asBandDesc[] = { - {"B1", 60, 443, 20, GCI_Undefined}, {"B2", 10, 490, 65, GCI_BlueBand}, - {"B3", 10, 560, 35, GCI_GreenBand}, {"B4", 10, 665, 30, GCI_RedBand}, - {"B5", 20, 705, 15, GCI_Undefined}, {"B6", 20, 740, 15, GCI_Undefined}, - {"B7", 20, 783, 20, GCI_Undefined}, {"B8", 10, 842, 115, GCI_Undefined}, - {"B8A", 20, 865, 20, GCI_Undefined}, {"B9", 60, 945, 20, GCI_Undefined}, - {"B10", 60, 1375, 30, GCI_Undefined}, {"B11", 20, 1610, 90, GCI_Undefined}, - {"B12", 20, 2190, 180, GCI_Undefined}, + {"B1", 60, 443, 20, GCI_CoastalBand}, + {"B2", 10, 490, 65, GCI_BlueBand}, + {"B3", 10, 560, 35, GCI_GreenBand}, + {"B4", 10, 665, 30, GCI_RedBand}, + {"B5", 20, 705, 15, GCI_RedEdgeBand}, // rededge071 + {"B6", 20, 740, 15, GCI_RedEdgeBand}, // rededge075 + {"B7", 20, 783, 20, GCI_RedEdgeBand}, // rededge078 + {"B8", 10, 842, 115, GCI_NIRBand}, // nir + {"B8A", 20, 865, 20, GCI_NIRBand}, // nir08 + {"B9", 60, 945, 20, GCI_NIRBand}, // nir09 + {"B10", 60, 1375, 30, GCI_OtherIRBand}, // cirrus + {"B11", 20, 1610, 90, GCI_SWIRBand}, // swir16 + {"B12", 20, 2190, 180, GCI_SWIRBand}, // swir11 }; +/* clang-format on */ #define NB_BANDS (sizeof(asBandDesc) / sizeof(asBandDesc[0])) @@ -1971,6 +1979,15 @@ static void SENTINEL2SetBandMetadata(GDALRasterBand *poBand, poBand->SetMetadataItem("WAVELENGTH", CPLSPrintf("%d", psBandDesc->nWaveLength)); poBand->SetMetadataItem("WAVELENGTH_UNIT", "nm"); + + poBand->SetMetadataItem( + "CENTRAL_WAVELENGTH_UM", + CPLSPrintf("%.3f", double(psBandDesc->nWaveLength) / 1000), + "IMAGERY"); + poBand->SetMetadataItem( + "FWHM_UM", + CPLSPrintf("%.3f", double(psBandDesc->nBandWidth) / 1000), + "IMAGERY"); } else { diff --git a/frmts/stacit/stacitdataset.cpp b/frmts/stacit/stacitdataset.cpp index 043ba7845d9a..32a092e40934 100644 --- a/frmts/stacit/stacitdataset.cpp +++ b/frmts/stacit/stacitdataset.cpp @@ -602,17 +602,13 @@ bool STACITDataset::SetupDataset( if (!osBandName.empty()) poVRTBand->SetDescription(osBandName.c_str()); - if (eInterp != GCI_Undefined) + const auto osCommonName = eoBand["common_name"].ToString(); + if (!osCommonName.empty()) { - const auto osCommonName = eoBand["common_name"].ToString(); - if (osCommonName == "red") - poVRTBand->SetColorInterpretation(GCI_RedBand); - else if (osCommonName == "green") - poVRTBand->SetColorInterpretation(GCI_GreenBand); - else if (osCommonName == "blue") - poVRTBand->SetColorInterpretation(GCI_BlueBand); - else if (osCommonName == "alpha") - poVRTBand->SetColorInterpretation(GCI_AlphaBand); + const auto eInterpFromCommonName = + GDALGetColorInterpFromSTACCommonName(osCommonName.c_str()); + if (eInterpFromCommonName != GCI_Undefined) + poVRTBand->SetColorInterpretation(eInterpFromCommonName); } for (const auto &eoBandChild : eoBand.GetChildren()) diff --git a/frmts/vrt/data/gdalvrt.xsd b/frmts/vrt/data/gdalvrt.xsd index 9f9a91d7be63..2530ab929671 100644 --- a/frmts/vrt/data/gdalvrt.xsd +++ b/frmts/vrt/data/gdalvrt.xsd @@ -442,6 +442,15 @@ + + + + + + + + + diff --git a/gcore/gdal.h b/gcore/gdal.h index 565133372c0c..b40a88083546 100644 --- a/gcore/gdal.h +++ b/gcore/gdal.h @@ -223,27 +223,52 @@ typedef struct (s).bFloatingPointWindowValidity = FALSE; \ } while (0) -/*! Types of color interpretation for raster bands. */ +/** Types of color interpretation for raster bands. + * + * For spectral bands, the wavelength ranges are indicative only, and may vary + * depending on sensors. The CENTRAL_WAVELENGTH_UM and FWHM_UM metadata + * items in the IMAGERY metadata domain of the raster band, when present, will + * give more accurate characteristics. + */ typedef enum { /*! Undefined */ GCI_Undefined = 0, /*! Greyscale */ GCI_GrayIndex = 1, /*! Paletted (see associated color table) */ GCI_PaletteIndex = 2, - /*! Red band of RGBA image */ GCI_RedBand = 3, - /*! Green band of RGBA image */ GCI_GreenBand = 4, - /*! Blue band of RGBA image */ GCI_BlueBand = 5, + /*! Red band of RGBA image, or red spectral band [0.62 - 0.69 um]*/ + GCI_RedBand = 3, + /*! Green band of RGBA image, or green spectral band [0.51 - 0.60 um]*/ + GCI_GreenBand = 4, + /*! Blue band of RGBA image, or blue spectral band [0.45 - 0.53 um] */ + GCI_BlueBand = 5, /*! Alpha (0=transparent, 255=opaque) */ GCI_AlphaBand = 6, /*! Hue band of HLS image */ GCI_HueBand = 7, /*! Saturation band of HLS image */ GCI_SaturationBand = 8, /*! Lightness band of HLS image */ GCI_LightnessBand = 9, /*! Cyan band of CMYK image */ GCI_CyanBand = 10, /*! Magenta band of CMYK image */ GCI_MagentaBand = 11, - /*! Yellow band of CMYK image */ GCI_YellowBand = 12, + /*! Yellow band of CMYK image, or yellow spectral band [0.58 - 0.62 um] */ + GCI_YellowBand = 12, /*! Black band of CMYK image */ GCI_BlackBand = 13, /*! Y Luminance */ GCI_YCbCr_YBand = 14, /*! Cb Chroma */ GCI_YCbCr_CbBand = 15, /*! Cr Chroma */ GCI_YCbCr_CrBand = 16, - /*! Max current value (equals to GCI_YCbCr_CrBand currently) */ GCI_Max = 16 + /* GDAL 3.10 addition: begin */ + /*! Panchromatic band [0.40 - 1.00 um] */ GCI_PanBand = 17, + /*! Coastal band [0.40 - 0.45 um] */ GCI_CoastalBand = 18, + /*! Red-edge band [0.69 - 0.79 um] */ GCI_RedEdgeBand = 19, + /*! Near-InfraRed (NIR) band [0.75 - 1.40 um] */ GCI_NIRBand = 20, + /*! Short-Wavelength InfraRed (SWIR) band [1.40 - 3.00 um] */ GCI_SWIRBand = + 21, + /*! Mid-Wavelength InfraRed (MWIR) band [3.00 - 8.00 um] */ GCI_MWIRBand = + 22, + /*! Long-Wavelength InfraRed (LWIR) band [8.00 - 15 um] */ GCI_LWIRBand = + 23, + /*! Thermal InfraRed (TIR) band (MWIR or LWIR) [3 - 15 um] */ GCI_TIRBand = + 24, + /*! Other infrared band [0.75 - 1000 um] */ GCI_OtherIRBand = 25, + /* GDAL 3.10 addition: end */ + /*! Max current value (equals to GCI_OtherIRBand currently) */ GCI_Max = 25 } GDALColorInterp; const char CPL_DLL *GDALGetColorInterpretationName(GDALColorInterp); diff --git a/gcore/gdal_misc.cpp b/gcore/gdal_misc.cpp index 8daa761b33de..ce761dbf5bba 100644 --- a/gcore/gdal_misc.cpp +++ b/gcore/gdal_misc.cpp @@ -1181,7 +1181,7 @@ const char *GDALGetColorInterpretationName(GDALColorInterp eInterp) switch (eInterp) { case GCI_Undefined: - return "Undefined"; + break; case GCI_GrayIndex: return "Gray"; @@ -1231,9 +1231,34 @@ const char *GDALGetColorInterpretationName(GDALColorInterp eInterp) case GCI_YCbCr_CrBand: return "YCbCr_Cr"; - default: - return "Unknown"; + case GCI_PanBand: + return "Pan"; + + case GCI_CoastalBand: + return "Coastal"; + + case GCI_RedEdgeBand: + return "RedEdge"; + + case GCI_NIRBand: + return "NIR"; + + case GCI_SWIRBand: + return "SWIR"; + + case GCI_MWIRBand: + return "MWIR"; + + case GCI_LWIRBand: + return "LWIR"; + + case GCI_TIRBand: + return "TIR"; + + case GCI_OtherIRBand: + return "OtherIR"; } + return "Undefined"; } /************************************************************************/ @@ -1270,9 +1295,85 @@ GDALColorInterp GDALGetColorInterpretationByName(const char *pszName) } } + // Accept British English spelling + if (EQUAL(pszName, "grey")) + return GCI_GrayIndex; + return GCI_Undefined; } +/************************************************************************/ +/* GDALGetColorInterpFromSTACCommonName() */ +/************************************************************************/ + +static const struct +{ + const char *pszName; + GDALColorInterp eInterp; +} asSTACCommonNames[] = { + {"pan", GCI_PanBand}, + {"coastal", GCI_CoastalBand}, + {"blue", GCI_BlueBand}, + {"green", GCI_GreenBand}, + {"green05", GCI_GreenBand}, // no exact match + {"yellow", GCI_YellowBand}, + {"red", GCI_RedBand}, + {"rededge", GCI_RedEdgeBand}, + {"rededge071", GCI_RedEdgeBand}, // no exact match + {"rededge075", GCI_RedEdgeBand}, // no exact match + {"rededge078", GCI_RedEdgeBand}, // no exact match + {"nir", GCI_NIRBand}, + {"nir08", GCI_NIRBand}, // no exact match + {"nir09", GCI_NIRBand}, // no exact match + {"cirrus", GCI_NIRBand}, // no exact match + {nullptr, + GCI_SWIRBand}, // so that GDALGetSTACCommonNameFromColorInterp returns null on GCI_SWIRBand + {"swir16", GCI_SWIRBand}, // no exact match + {"swir22", GCI_SWIRBand}, // no exact match + {"lwir", GCI_LWIRBand}, + {"lwir11", GCI_LWIRBand}, // no exact match + {"lwir12", GCI_LWIRBand}, // no exact match +}; + +/** Get color interpreetation from STAC eo:common_name + * + * Cf https://github.com/stac-extensions/eo?tab=readme-ov-file#common-band-names + * + * @since GDAL 3.10 + */ +GDALColorInterp GDALGetColorInterpFromSTACCommonName(const char *pszName) +{ + + for (const auto &sAssoc : asSTACCommonNames) + { + if (sAssoc.pszName && EQUAL(pszName, sAssoc.pszName)) + return sAssoc.eInterp; + } + return GCI_Undefined; +} + +/************************************************************************/ +/* GDALGetSTACCommonNameFromColorInterp() */ +/************************************************************************/ + +/** Get STAC eo:common_name from GDAL color interpretation + * + * Cf https://github.com/stac-extensions/eo?tab=readme-ov-file#common-band-names + * + * @return nullptr if there is no match + * + * @since GDAL 3.10 + */ +const char *GDALGetSTACCommonNameFromColorInterp(GDALColorInterp eInterp) +{ + for (const auto &sAssoc : asSTACCommonNames) + { + if (eInterp == sAssoc.eInterp) + return sAssoc.pszName; + } + return nullptr; +} + /************************************************************************/ /* GDALGetRandomRasterSample() */ /************************************************************************/ diff --git a/gcore/gdal_priv.h b/gcore/gdal_priv.h index 421367884f0a..da9d7e5d5270 100644 --- a/gcore/gdal_priv.h +++ b/gcore/gdal_priv.h @@ -4564,6 +4564,11 @@ GDALRasterAttributeTable CPL_DLL *GDALCreateRasterAttributeTableFromMDArrays( const std::vector> &apoArrays, const std::vector &aeUsages); +GDALColorInterp CPL_DLL +GDALGetColorInterpFromSTACCommonName(const char *pszName); +const char CPL_DLL * +GDALGetSTACCommonNameFromColorInterp(GDALColorInterp eInterp); + // Macro used so that Identify and driver metadata methods in drivers built // as plugin can be duplicated in libgdal core and in the driver under different // names diff --git a/swig/include/gdal.i b/swig/include/gdal.i index 3799c74ed0f8..14ccffb67a60 100644 --- a/swig/include/gdal.i +++ b/swig/include/gdal.i @@ -369,6 +369,7 @@ $1; %rename (GetDataTypeByName) GDALGetDataTypeByName; %rename (DataTypeUnion) GDALDataTypeUnion; %rename (GetColorInterpretationName) GDALGetColorInterpretationName; +%rename (GetColorInterpretationByName) GDALGetColorInterpretationByName; %rename (GetPaletteInterpretationName) GDALGetPaletteInterpretationName; %rename (DecToDMS) GDALDecToDMS; %rename (PackedDMSToDec) GDALPackedDMSToDec; @@ -725,6 +726,8 @@ GDALDataType GDALDataTypeUnion( GDALDataType a, GDALDataType b ); const char *GDALGetColorInterpretationName( GDALColorInterp eColorInterp ); +GDALColorInterp GDALGetColorInterpretationByName( const char* pszColorInterpName ); + const char *GDALGetPaletteInterpretationName( GDALPaletteInterp ePaletteInterp ); #ifdef SWIGJAVA diff --git a/swig/include/gdalconst.i b/swig/include/gdalconst.i index 426b2b7b76e9..4d935cf22635 100644 --- a/swig/include/gdalconst.i +++ b/swig/include/gdalconst.i @@ -101,9 +101,18 @@ %constant GCI_MagentaBand = GCI_MagentaBand; %constant GCI_YellowBand = GCI_YellowBand; %constant GCI_BlackBand = GCI_BlackBand; -%constant GCI_YCbCr_YBand = GCI_YCbCr_YBand; -%constant GCI_YCbCr_CrBand = GCI_YCbCr_CrBand; -%constant GCI_YCbCr_CbBand = GCI_YCbCr_CbBand; +%constant GCI_YCbCr_YBand = GCI_YCbCr_YBand; +%constant GCI_YCbCr_CrBand = GCI_YCbCr_CrBand; +%constant GCI_YCbCr_CbBand = GCI_YCbCr_CbBand; +%constant GCI_PanBand = GCI_PanBand; +%constant GCI_CoastalBand = GCI_CoastalBand; +%constant GCI_RedEdgeBand = GCI_RedEdgeBand; +%constant GCI_NIRBand = GCI_NIRBand; +%constant GCI_SWIRBand = GCI_SWIRBand; +%constant GCI_MWIRBand = GCI_MWIRBand; +%constant GCI_LWIRBand = GCI_LWIRBand; +%constant GCI_TIRBand = GCI_TIRBand; +%constant GCI_OtherIRBand = GCI_OtherIRBand; // GDALResampleAlg diff --git a/swig/python/gdal-utils/osgeo_utils/gdal_edit.py b/swig/python/gdal-utils/osgeo_utils/gdal_edit.py index 50a401b58a7f..ebbfa0a75d39 100755 --- a/swig/python/gdal-utils/osgeo_utils/gdal_edit.py +++ b/swig/python/gdal-utils/osgeo_utils/gdal_edit.py @@ -50,7 +50,7 @@ def Usage(isError): " [-offset ] [-scale ] [-units ]", file=f ) print( - " [-colorinterp_ {red|green|blue|alpha|gray|undefined]]...", + " [-colorinterp_ {red|green|blue|alpha|gray|undefined|pan|coastal|rededge|nir|swir|mwir|lwir|...]]...", file=f, ) print(" [-a_coord_epoch ] [-unsetepoch]", file=f) @@ -251,26 +251,17 @@ def gdal_edit(argv): i = i + 1 elif argv[i].startswith("-colorinterp_") and i < len(argv) - 1: band = int(argv[i][len("-colorinterp_") :]) - val = argv[i + 1] - if val.lower() == "red": - val = gdal.GCI_RedBand - elif val.lower() == "green": - val = gdal.GCI_GreenBand - elif val.lower() == "blue": - val = gdal.GCI_BlueBand - elif val.lower() == "alpha": - val = gdal.GCI_AlphaBand - elif val.lower() == "gray" or val.lower() == "grey": - val = gdal.GCI_GrayIndex - elif val.lower() == "undefined": + val_str = argv[i + 1] + if val_str.lower() == "undefined": val = gdal.GCI_Undefined else: - print( - "Unsupported color interpretation %s.\n" % val - + "Only red, green, blue, alpha, gray, undefined are supported.\n", - file=sys.stderr, - ) - return Usage(isError=True) + val = gdal.GetColorInterpretationByName(val_str) + if val == gdal.GCI_Undefined: + print( + "Unsupported color interpretation %s.\n" % val_str, + file=sys.stderr, + ) + return Usage(isError=True) colorinterp[band] = val i = i + 1 elif argv[i][0] == "-": From f2eb85948cb149ce4504c22535516ce863796e93 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 4 Sep 2024 15:10:13 +0200 Subject: [PATCH 3/5] GTiff: make sure CreateCopy() copies the band IMAGERY metadata domain by default (unless COPY_SRC_MDD or SRC_MDD creation options are set) --- autotest/gcore/tiff_write.py | 54 ++++++++++++++++++++++++++++ frmts/gtiff/gtiffdataset_write.cpp | 57 +++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/autotest/gcore/tiff_write.py b/autotest/gcore/tiff_write.py index 28da66db9805..c7bcdef00130 100755 --- a/autotest/gcore/tiff_write.py +++ b/autotest/gcore/tiff_write.py @@ -11679,3 +11679,57 @@ def test_tiff_write_too_large_webp( filename = str(tmp_vsimem / "test.tif") with pytest.raises(Exception, match=expected_error_msg): gdal.GetDriverByName("GTiff").Create(filename, xsize, ysize, options=options) + + +############################################################################### +# Test writing/reading band IMAGERY metadata + + +def test_tiff_write_band_IMAGERY(tmp_vsimem): + + filename = str(tmp_vsimem / "test.tif") + with gdal.GetDriverByName("GTiff").Create(filename, 1, 1) as ds: + ds.GetRasterBand(1).SetMetadataItem("foo", "bar", "IMAGERY") + with gdal.Open(filename) as ds: + assert ds.GetRasterBand(1).GetMetadataDomainList() == ["IMAGERY"] + with gdal.Open(filename) as ds: + assert ds.GetRasterBand(1).GetMetadataItem("foo", "IMAGERY") == "bar" + with gdal.Open(filename) as ds: + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {"foo": "bar"} + + filename2 = str(tmp_vsimem / "test2.tif") + + with gdal.Open(filename) as ds: + gdal.GetDriverByName("GTiff").CreateCopy(filename2, ds) + with gdal.Open(filename2) as ds: + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {"foo": "bar"} + + with gdal.Open(filename) as ds: + gdal.GetDriverByName("GTiff").CreateCopy( + filename2, ds, options=["COPY_SRC_MDD=YES"] + ) + with gdal.Open(filename2) as ds: + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {"foo": "bar"} + + with gdal.Open(filename) as ds: + gdal.GetDriverByName("GTiff").CreateCopy( + filename2, ds, options=["COPY_SRC_MDD=NO"] + ) + with gdal.Open(filename2) as ds: + assert ds.GetRasterBand(1).GetMetadataDomainList() is None + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {} + + with gdal.Open(filename) as ds: + gdal.GetDriverByName("GTiff").CreateCopy( + filename2, ds, options=["SRC_MDD=not_existing"] + ) + with gdal.Open(filename2) as ds: + assert ds.GetRasterBand(1).GetMetadataDomainList() is None + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {} + + with gdal.Open(filename) as ds: + gdal.GetDriverByName("GTiff").CreateCopy( + filename2, ds, options=["SRC_MDD=not_existing", "SRC_MDD=IMAGERY"] + ) + with gdal.Open(filename2) as ds: + assert ds.GetRasterBand(1).GetMetadata_Dict("IMAGERY") == {"foo": "bar"} diff --git a/frmts/gtiff/gtiffdataset_write.cpp b/frmts/gtiff/gtiffdataset_write.cpp index 5d0e54776965..768837cc753c 100644 --- a/frmts/gtiff/gtiffdataset_write.cpp +++ b/frmts/gtiff/gtiffdataset_write.cpp @@ -3833,7 +3833,7 @@ static void WriteMDMetadata(GDALMultiDomainMetadata *poMDMD, TIFF *hTIFF, for (int iDomain = 0; papszDomainList && papszDomainList[iDomain]; ++iDomain) { - char **papszMD = poMDMD->GetMetadata(papszDomainList[iDomain]); + CSLConstList papszMD = poMDMD->GetMetadata(papszDomainList[iDomain]); bool bIsXML = false; if (EQUAL(papszDomainList[iDomain], "IMAGE_STRUCTURE") || @@ -4065,6 +4065,11 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, CPLXMLNode *psRoot = nullptr; CPLXMLNode *psTail = nullptr; + const char *pszCopySrcMDD = + CSLFetchNameValueDef(papszCreationOptions, "COPY_SRC_MDD", "AUTO"); + char **papszSrcMDD = + CSLFetchNameValueMultiple(papszCreationOptions, "SRC_MDD"); + if (bSrcIsGeoTIFF) { GTiffDataset *poSrcDSGTiff = cpl::down_cast(poSrcDS); @@ -4074,15 +4079,11 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, } else { - const char *pszCopySrcMDD = - CSLFetchNameValueDef(papszCreationOptions, "COPY_SRC_MDD", "AUTO"); - char **papszSrcMDD = - CSLFetchNameValueMultiple(papszCreationOptions, "SRC_MDD"); if (EQUAL(pszCopySrcMDD, "AUTO") || CPLTestBool(pszCopySrcMDD) || papszSrcMDD) { GDALMultiDomainMetadata l_oMDMD; - char **papszMD = poSrcDS->GetMetadata(); + CSLConstList papszMD = poSrcDS->GetMetadata(); if (CSLCount(papszMD) > 0 && (!papszSrcMDD || CSLFindString(papszSrcMDD, "") >= 0 || CSLFindString(papszSrcMDD, "_DEFAULT_") >= 0)) @@ -4094,7 +4095,7 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, papszSrcMDD) { char **papszDomainList = poSrcDS->GetMetadataDomainList(); - for (char **papszIter = papszDomainList; + for (CSLConstList papszIter = papszDomainList; papszIter && *papszIter; ++papszIter) { const char *pszDomain = *papszIter; @@ -4111,7 +4112,6 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, WriteMDMetadata(&l_oMDMD, l_hTIFF, &psRoot, &psTail, 0, eProfile); } - CSLDestroy(papszSrcMDD); } if (!bExcludeRPBandIMGFileWriting) @@ -4156,13 +4156,44 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, } else { - char **papszMD = poBand->GetMetadata(); + GDALMultiDomainMetadata l_oMDMD; + bool bOMDMDSet = false; - if (CSLCount(papszMD) > 0) + if (EQUAL(pszCopySrcMDD, "AUTO") && !papszSrcMDD) { - GDALMultiDomainMetadata l_oMDMD; - l_oMDMD.SetMetadata(papszMD); + for (const char *pszDomain : {"", "IMAGERY"}) + { + if (CSLConstList papszMD = poBand->GetMetadata(pszDomain)) + { + if (papszMD[0]) + { + bOMDMDSet = true; + l_oMDMD.SetMetadata(papszMD, pszDomain); + } + } + } + } + else if (CPLTestBool(pszCopySrcMDD) || papszSrcMDD) + { + char **papszDomainList = poBand->GetMetadataDomainList(); + for (const char *pszDomain : + cpl::Iterate(CSLConstList(papszDomainList))) + { + if (pszDomain[0] != 0 && + !EQUAL(pszDomain, "IMAGE_STRUCTURE") && + (!papszSrcMDD || + CSLFindString(papszSrcMDD, pszDomain) >= 0)) + { + bOMDMDSet = true; + l_oMDMD.SetMetadata(poBand->GetMetadata(pszDomain), + pszDomain); + } + } + CSLDestroy(papszDomainList); + } + if (bOMDMDSet) + { WriteMDMetadata(&l_oMDMD, l_hTIFF, &psRoot, &psTail, nBand, eProfile); } @@ -4234,6 +4265,8 @@ bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF, } } + CSLDestroy(papszSrcMDD); + const char *pszTilingSchemeName = CSLFetchNameValue(papszCreationOptions, "@TILING_SCHEME_NAME"); if (pszTilingSchemeName) From 804bc11a8da18b28270156342ff1dbcc8fccf1d2 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 4 Sep 2024 15:50:07 +0200 Subject: [PATCH 4/5] Doc: enhance doc about band IMAGERY metadata --- doc/source/spelling_wordlist.txt | 1 + doc/source/user/raster_data_model.rst | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/source/spelling_wordlist.txt b/doc/source/spelling_wordlist.txt index f4f90cad3f81..5227db582518 100644 --- a/doc/source/spelling_wordlist.txt +++ b/doc/source/spelling_wordlist.txt @@ -614,6 +614,7 @@ describedby deserialization deserialize deserialized +deserializing desirabled dest destructor diff --git a/doc/source/user/raster_data_model.rst b/doc/source/user/raster_data_model.rst index 0deac2fd404a..9278d28853c1 100644 --- a/doc/source/user/raster_data_model.rst +++ b/doc/source/user/raster_data_model.rst @@ -194,7 +194,15 @@ Starting with GDAL 3.10, there also exists a raster band level IMAGERY metadata - CENTRAL_WAVELENGTH_UM: Central Wavelength in micrometers. - FWHM_UM: Full-width half-maximum (FWHM) in micrometers. -At time of writing, they are set by the SENTINEL2 and ENVI drivers (if corresponding metadata items are found in the ENVI header) +Clients can get (resp. set) these metadata items with :cpp:func:`GDALRasterBand::GetMetadataItem()` +(resp. :cpp:func:`GDALRasterBand::SetMetadataItem()`).` + +They are specifically set by the :ref:`raster.sentinel2` and +:ref:`raster.envi` drivers (if corresponding metadata items are found in the ENVI header), +but may also be found in other drivers handling arbitrary GDAL metadata, such as +the one using the GDAL Persistent Auxiliary Mechanism (PAM / .aux.xml side car files) +or :ref:`raster.vrt` drivers. The :ref:`raster.gtiff` driver also supports serializing +and deserializing the band IMAGERY metadata domain in the ``GDAL_METADATA`` TIFF tag. xml: Domains ++++++++++++ From 66d2068c99d27b45a642f5bf44bbc547e656103b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 4 Sep 2024 18:26:53 +0200 Subject: [PATCH 5/5] GDALColorInterp: add GCI_SAR_[Ka|K|Ku|X|C|S|L|P_Band], as well as GCI_[IR|SAR]_[Start|End] as well as GCI_IR_Reserved[1|2|3|4] and GCI_SAR_Reserved_[1|2] --- autotest/gcore/basic_test.py | 10 +++ doc/source/spelling_wordlist.txt | 1 + doc/source/user/raster_data_model.rst | 11 ++++ frmts/vrt/data/gdalvrt.xsd | 8 +++ gcore/gdal.h | 89 ++++++++++++++++++++++++--- gcore/gdal_misc.cpp | 47 ++++++++++++++ swig/include/gdalconst.i | 16 +++++ 7 files changed, 175 insertions(+), 7 deletions(-) diff --git a/autotest/gcore/basic_test.py b/autotest/gcore/basic_test.py index 2719fc963dcc..7e496fea622b 100755 --- a/autotest/gcore/basic_test.py +++ b/autotest/gcore/basic_test.py @@ -990,3 +990,13 @@ def test_band_getitem(): with pytest.raises(IndexError): ds[5] + + +def test_colorinterp(): + + d = {} + for c in range(gdal.GCI_Max + 1): + name = gdal.GetColorInterpretationName(c) + assert name not in d + d[name] = c + assert gdal.GetColorInterpretationByName(name) == c diff --git a/doc/source/spelling_wordlist.txt b/doc/source/spelling_wordlist.txt index 5227db582518..8aec96480a48 100644 --- a/doc/source/spelling_wordlist.txt +++ b/doc/source/spelling_wordlist.txt @@ -1510,6 +1510,7 @@ KShortest kslikebase KSpread ktx +Ku Kubernetes Kumar Kuratomi diff --git a/doc/source/user/raster_data_model.rst b/doc/source/user/raster_data_model.rst index 9278d28853c1..03af38ace48b 100644 --- a/doc/source/user/raster_data_model.rst +++ b/doc/source/user/raster_data_model.rst @@ -269,12 +269,23 @@ A raster band has the following properties: * GCI_LWIRBand / "LWIR": Long-Wavelength InfraRed (LWIR) band [8.00 - 15 um] * GCI_TIRBand / "TIR": Thermal InfraRed (TIR) band (MWIR or LWIR) [3 - 15 um] * GCI_OtherIRBand / "OtherIR": Other infrared band [0.75 - 1000 um] + * GCI_SAR_Ka_Band / "SAR_Ka": Synthetic Aperture Radar (SAR) Ka band [0.8 - 1.1 cm / 27 - 40 GHz] + * GCI_SAR_K_Band / "SAR_K": Synthetic Aperture Radar (SAR) K band [1.1 - 1.7 cm / 18 - 27 GHz] + * GCI_SAR_Ku_Band / "SAR_Ku": Synthetic Aperture Radar (SAR) Ku band [1.7 - 2.4 cm / 12 - 18 GHz] + * GCI_SAR_X_Band / "SAR_X": Synthetic Aperture Radar (SAR) X band [2.4 - 3.8 cm / 8 - 12 GHz] + * GCI_SAR_C_Band / "SAR_C": Synthetic Aperture Radar (SAR) C band [3.8 - 7.5 cm / 4 - 8 GHz] + * GCI_SAR_S_Band / "SAR_S": Synthetic Aperture Radar (SAR) S band [7.5 - 15 cm / 2 - 4 GHz] + * GCI_SAR_L_Band / "SAR_L": Synthetic Aperture Radar (SAR) L band [15 - 30 cm / 1 - 2 GHz] + * GCI_SAR_P_Band / "SAR_P": Synthetic Aperture Radar (SAR) P band [30 - 100 cm / 0.3 - 1 GHz] For spectral bands, the wavelength ranges are indicative only, and may vary depending on sensors. The ``CENTRAL_WAVELENGTH_UM`` and ``FWHM_UM`` metadata items in the band ``IMAGERY`` metadata domain of the raster band, when present, will give more accurate characteristics. + Values belonging to the IR domain are in the [GCI_IR_Start, GCI_IR_End] range. + Values belonging to the SAR domain are in the [GCI_SAR_Start, GCI_SAR_End] range. + - A color table, described in more detail later. - Knowledge of reduced resolution overviews (pyramids) if available. diff --git a/frmts/vrt/data/gdalvrt.xsd b/frmts/vrt/data/gdalvrt.xsd index 2530ab929671..c2654337d99d 100644 --- a/frmts/vrt/data/gdalvrt.xsd +++ b/frmts/vrt/data/gdalvrt.xsd @@ -451,6 +451,14 @@ + + + + + + + + diff --git a/gcore/gdal.h b/gcore/gdal.h index b40a88083546..a6ff6d16ca88 100644 --- a/gcore/gdal.h +++ b/gcore/gdal.h @@ -223,12 +223,51 @@ typedef struct (s).bFloatingPointWindowValidity = FALSE; \ } while (0) +/** Value indicating the start of the range for color interpretations belonging + * to the InfraRed (IR) domain. All constants of the GDALColorInterp enumeration + * in the IR domain are in the [GCI_IR_Start, GCI_IR_End] range. + * + * @since 3.10 + */ +#define GCI_IR_Start 20 + +/** Value indicating the end of the range for color interpretations belonging + * to the InfraRed (IR) domain. All constants of the GDALColorInterp enumeration + * in the IR domain are in the [GCI_IR_Start, GCI_IR_End] range. + * + * @since 3.10 + */ +#define GCI_IR_End 29 + +/** Value indicating the start of the range for color interpretations belonging + * to the Synthetic Aperture Radar (SAR) domain. + * All constants of the GDALColorInterp enumeration + * in the SAR domain are in the [GCI_SAR_Start, GCI_SAR_End] range. + * + * @since 3.10 + */ +#define GCI_SAR_Start 30 + +/** Value indicating the end of the range for color interpretations belonging + * to the Synthetic Aperture Radar (SAR) domain. + * All constants of the GDALColorInterp enumeration + * in the SAR domain are in the [GCI_SAR_Start, GCI_SAR_End] range. + * + * @since 3.10 + */ +#define GCI_SAR_End 39 + /** Types of color interpretation for raster bands. * * For spectral bands, the wavelength ranges are indicative only, and may vary * depending on sensors. The CENTRAL_WAVELENGTH_UM and FWHM_UM metadata * items in the IMAGERY metadata domain of the raster band, when present, will * give more accurate characteristics. + * + * Values belonging to the IR domain are in the [GCI_IR_Start, GCI_IR_End] range. + * Values belonging to the SAR domain are in the [GCI_SAR_Start, GCI_SAR_End] range. + * + * Values between GCI_PanBand to GCI_SAR_Reserved_2 have been added in GDAL 3.10. */ typedef enum { @@ -253,22 +292,58 @@ typedef enum /*! Y Luminance */ GCI_YCbCr_YBand = 14, /*! Cb Chroma */ GCI_YCbCr_CbBand = 15, /*! Cr Chroma */ GCI_YCbCr_CrBand = 16, + /* GDAL 3.10 addition: begin */ /*! Panchromatic band [0.40 - 1.00 um] */ GCI_PanBand = 17, /*! Coastal band [0.40 - 0.45 um] */ GCI_CoastalBand = 18, /*! Red-edge band [0.69 - 0.79 um] */ GCI_RedEdgeBand = 19, - /*! Near-InfraRed (NIR) band [0.75 - 1.40 um] */ GCI_NIRBand = 20, + + /*! Near-InfraRed (NIR) band [0.75 - 1.40 um] */ GCI_NIRBand = + GCI_IR_Start + 0, /*! Short-Wavelength InfraRed (SWIR) band [1.40 - 3.00 um] */ GCI_SWIRBand = - 21, + GCI_IR_Start + 1, /*! Mid-Wavelength InfraRed (MWIR) band [3.00 - 8.00 um] */ GCI_MWIRBand = - 22, + GCI_IR_Start + 2, /*! Long-Wavelength InfraRed (LWIR) band [8.00 - 15 um] */ GCI_LWIRBand = - 23, + GCI_IR_Start + 3, /*! Thermal InfraRed (TIR) band (MWIR or LWIR) [3 - 15 um] */ GCI_TIRBand = - 24, - /*! Other infrared band [0.75 - 1000 um] */ GCI_OtherIRBand = 25, + GCI_IR_Start + 4, + /*! Other infrared band [0.75 - 1000 um] */ GCI_OtherIRBand = + GCI_IR_Start + 5, + /*! Reserved value. Do not set it ! */ + GCI_IR_Reserved_1 = GCI_IR_Start + 6, + /*! Reserved value. Do not set it ! */ + GCI_IR_Reserved_2 = GCI_IR_Start + 7, + /*! Reserved value. Do not set it ! */ + GCI_IR_Reserved_3 = GCI_IR_Start + 8, + /*! Reserved value. Do not set it ! */ + GCI_IR_Reserved_4 = GCI_IR_Start + 9, + + /*! Synthetic Aperture Radar (SAR) Ka band [0.8 - 1.1 cm / 27 - 40 GHz] */ + GCI_SAR_Ka_Band = GCI_SAR_Start + 0, + /*! Synthetic Aperture Radar (SAR) K band [1.1 - 1.7 cm / 18 - 27 GHz] */ + GCI_SAR_K_Band = GCI_SAR_Start + 1, + /*! Synthetic Aperture Radar (SAR) Ku band [1.7 - 2.4 cm / 12 - 18 GHz] */ + GCI_SAR_Ku_Band = GCI_SAR_Start + 2, + /*! Synthetic Aperture Radar (SAR) X band [2.4 - 3.8 cm / 8 - 12 GHz] */ + GCI_SAR_X_Band = GCI_SAR_Start + 3, + /*! Synthetic Aperture Radar (SAR) C band [3.8 - 7.5 cm / 4 - 8 GHz] */ + GCI_SAR_C_Band = GCI_SAR_Start + 4, + /*! Synthetic Aperture Radar (SAR) S band [7.5 - 15 cm / 2 - 4 GHz] */ + GCI_SAR_S_Band = GCI_SAR_Start + 5, + /*! Synthetic Aperture Radar (SAR) L band [15 - 30 cm / 1 - 2 GHz] */ + GCI_SAR_L_Band = GCI_SAR_Start + 6, + /*! Synthetic Aperture Radar (SAR) P band [30 - 100 cm / 0.3 - 1 GHz] */ + GCI_SAR_P_Band = GCI_SAR_Start + 7, + /*! Reserved value. Do not set it ! */ + GCI_SAR_Reserved_1 = GCI_SAR_Start + 8, + /*! Reserved value. Do not set it ! */ + GCI_SAR_Reserved_2 = GCI_SAR_Start + 9, + /* GDAL 3.10 addition: end */ - /*! Max current value (equals to GCI_OtherIRBand currently) */ GCI_Max = 25 + + /*! Max current value (equals to GCI_SAR_Reserved_2 currently) */ GCI_Max = + GCI_SAR_Reserved_2 } GDALColorInterp; const char CPL_DLL *GDALGetColorInterpretationName(GDALColorInterp); diff --git a/gcore/gdal_misc.cpp b/gcore/gdal_misc.cpp index ce761dbf5bba..50e1c65793db 100644 --- a/gcore/gdal_misc.cpp +++ b/gcore/gdal_misc.cpp @@ -1178,6 +1178,11 @@ const char *GDALGetPaletteInterpretationName(GDALPaletteInterp eInterp) const char *GDALGetColorInterpretationName(GDALColorInterp eInterp) { + static_assert(GCI_IR_Start == GCI_RedEdgeBand + 1); + static_assert(GCI_NIRBand == GCI_IR_Start); + static_assert(GCI_SAR_Start == GCI_IR_End + 1); + static_assert(GCI_Max == GCI_SAR_End); + switch (eInterp) { case GCI_Undefined: @@ -1257,6 +1262,48 @@ const char *GDALGetColorInterpretationName(GDALColorInterp eInterp) case GCI_OtherIRBand: return "OtherIR"; + + case GCI_IR_Reserved_1: + return "IR_Reserved_1"; + + case GCI_IR_Reserved_2: + return "IR_Reserved_2"; + + case GCI_IR_Reserved_3: + return "IR_Reserved_3"; + + case GCI_IR_Reserved_4: + return "IR_Reserved_4"; + + case GCI_SAR_Ka_Band: + return "SAR_Ka"; + + case GCI_SAR_K_Band: + return "SAR_K"; + + case GCI_SAR_Ku_Band: + return "SAR_Ku"; + + case GCI_SAR_X_Band: + return "SAR_X"; + + case GCI_SAR_C_Band: + return "SAR_C"; + + case GCI_SAR_S_Band: + return "SAR_S"; + + case GCI_SAR_L_Band: + return "SAR_L"; + + case GCI_SAR_P_Band: + return "SAR_P"; + + case GCI_SAR_Reserved_1: + return "SAR_Reserved_1"; + + case GCI_SAR_Reserved_2: + return "SAR_Reserved_2"; } return "Undefined"; } diff --git a/swig/include/gdalconst.i b/swig/include/gdalconst.i index 4d935cf22635..4a664f959e78 100644 --- a/swig/include/gdalconst.i +++ b/swig/include/gdalconst.i @@ -107,12 +107,28 @@ %constant GCI_PanBand = GCI_PanBand; %constant GCI_CoastalBand = GCI_CoastalBand; %constant GCI_RedEdgeBand = GCI_RedEdgeBand; + +%constant GCI_IR_Start = GCI_IR_Start; // just a marker, not an enumeration value %constant GCI_NIRBand = GCI_NIRBand; %constant GCI_SWIRBand = GCI_SWIRBand; %constant GCI_MWIRBand = GCI_MWIRBand; %constant GCI_LWIRBand = GCI_LWIRBand; %constant GCI_TIRBand = GCI_TIRBand; %constant GCI_OtherIRBand = GCI_OtherIRBand; +%constant GCI_IR_End = GCI_IR_End; // just a marker, not an enumeration value + +%constant GCI_SAR_Start = GCI_SAR_Start; // just a marker, not an enumeration value +%constant GCI_SAR_Ka_Band = GCI_SAR_Ka_Band; +%constant GCI_SAR_K_Band = GCI_SAR_K_Band; +%constant GCI_SAR_Ku_Band = GCI_SAR_Ku_Band; +%constant GCI_SAR_X_Band = GCI_SAR_X_Band; +%constant GCI_SAR_C_Band = GCI_SAR_C_Band; +%constant GCI_SAR_S_Band = GCI_SAR_S_Band; +%constant GCI_SAR_L_Band = GCI_SAR_L_Band; +%constant GCI_SAR_P_Band = GCI_SAR_P_Band; +%constant GCI_SAR_End = GCI_SAR_End; // just a marker, not an enumeration value + +%constant GCI_Max = GCI_Max; // GDALResampleAlg