From 624533986beb123b59087c2eae90d2569965ae78 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 23 Dec 2024 02:35:11 +0100 Subject: [PATCH 1/7] GTiff/COG: add support for INTERLEAVE=TILE; COG: add support for INTERLEAVE=BAND Fixes #10859 Up to now the COG driver only produced INTERLEAVE=PIXEL / PlanarConfiguration=Contiguous files where values for all bands are in the same tile. This PR add supports for INTERLEAVE=BAND where the tiles of band 1 are placed first, followed by the ones of band 2, etc. It also introduces a INTERLEAVE=TILE mode, which is similar to the BIL (Band Interleave by Line), but generalize to tiles. That is you put first tile (x,y)=(0,0) of band 1, then tile (0, 0) of band 2, ... tile (0, 0) of band N, tile (1, 0) of band 1, ... tile (1, 0) of band N, etc. Both modes can be useful for hyper-spectral datasets for example. --- autotest/gcore/cog.py | 298 ++++++++++ autotest/gcore/test_driver_metadata.py | 1 + autotest/gcore/tiff_write.py | 52 ++ doc/source/drivers/raster/cog.rst | 126 ++++- doc/source/drivers/raster/gtiff.rst | 94 +++- frmts/gtiff/cogdriver.cpp | 31 +- frmts/gtiff/geotiff.cpp | 5 +- frmts/gtiff/gtiffdataset.cpp | 23 +- frmts/gtiff/gtiffdataset.h | 8 +- frmts/gtiff/gtiffdataset_read.cpp | 525 +++++++++++++++++- frmts/gtiff/gtiffdataset_write.cpp | 508 ++++++++++------- frmts/gtiff/gtiffrasterband.cpp | 19 +- frmts/gtiff/gtiffrasterband.h | 4 - frmts/gtiff/gtiffrasterband_read.cpp | 482 ---------------- port/cpl_known_config_options.h | 2 +- .../validate_cloud_optimized_geotiff.py | 139 ++++- 16 files changed, 1586 insertions(+), 731 deletions(-) diff --git a/autotest/gcore/cog.py b/autotest/gcore/cog.py index 8056e244caed..4b6e4b6a6aba 100755 --- a/autotest/gcore/cog.py +++ b/autotest/gcore/cog.py @@ -18,6 +18,7 @@ import gdaltest import pytest +import webserver from test_py_scripts import samples_path from osgeo import gdal, osr @@ -1990,3 +1991,300 @@ def test_cog_preserve_ALPHA_PREMULTIPLIED_on_copy(tmp_vsimem): ds.GetRasterBand(4).GetMetadataItem("ALPHA", "IMAGE_STRUCTURE") == "PREMULTIPLIED" ) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + _check_cog(out_filename) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_band(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=BAND", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "BAND" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + _check_cog(out_filename) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_with_mask(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Translate( + "", "data/stefan_full_rgba.tif", options="-f MEM -b 1 -b 2 -b 3 -mask 4" + ), + options=["INTERLEAVE=TILE", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 12603, + 58561, + 36064, + ] + assert ds.GetRasterBand(1).GetMaskBand().Checksum() == 22499 + + _check_cog(out_filename) + + # Check that the tiles are in the expected order in the file + last_offset = 0 + for y in range(2): + for x in range(2): + for band in range(3): + offset = int( + ds.GetRasterBand(band + 1).GetMetadataItem( + f"BLOCK_OFFSET_{x}_{y}", "TIFF" + ) + ) + assert offset > last_offset + last_offset = offset + offset = int( + ds.GetRasterBand(1) + .GetMaskBand() + .GetMetadataItem(f"BLOCK_OFFSET_{x}_{y}", "TIFF") + ) + assert offset > last_offset + last_offset = offset + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_with_mask_and_ovr(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + out2_filename = str(tmp_vsimem / "out2.tif") + + ds = gdal.Translate( + out_filename, + "data/stefan_full_rgba.tif", + options="-b 1 -b 2 -b 3 -mask 4 -outsize 1024 0", + ) + ds.BuildOverviews("NEAR", [2]) + ds.Close() + + ds = gdal.Open(out_filename) + expected_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + expected_md += [ds.GetRasterBand(1).GetMaskBand().Checksum()] + expected_ovr_md = [ + ds.GetRasterBand(band + 1).GetOverview(0).Checksum() for band in range(3) + ] + expected_ovr_md += [ds.GetRasterBand(1).GetMaskBand().GetOverview(0).Checksum()] + + gdal.GetDriverByName("COG").CreateCopy( + out2_filename, + ds, + options=["INTERLEAVE=TILE", "OVERVIEW_RESAMPLING=NEAREST"], + ) + + ds = gdal.Open(out2_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + _check_cog(out2_filename) + + got_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + got_md += [ds.GetRasterBand(1).GetMaskBand().Checksum()] + assert got_md == expected_md + got_ovr_md = [ + ds.GetRasterBand(band + 1).GetOverview(0).Checksum() for band in range(3) + ] + got_ovr_md += [ds.GetRasterBand(1).GetMaskBand().GetOverview(0).Checksum()] + assert got_ovr_md == expected_ovr_md + + +############################################################################### +# Check that our reading of a COG with /vsicurl is efficient + + +@pytest.mark.require_curl() +@pytest.mark.skipif( + not check_libtiff_internal_or_at_least(4, 0, 11), + reason="libtiff >= 4.0.11 required", +) +@pytest.mark.parametrize("INTERLEAVE", ["BAND", "TILE"]) +def test_cog_interleave_tile_or_band_vsicurl(tmp_vsimem, INTERLEAVE): + + gdal.VSICurlClearCache() + + webserver_process = None + webserver_port = 0 + + (webserver_process, webserver_port) = webserver.launch( + handler=webserver.DispatcherHttpHandler + ) + if webserver_port == 0: + pytest.skip() + + in_filename = str(tmp_vsimem / "in.tif") + cog_filename = str(tmp_vsimem / "cog.tif") + + ds = gdal.Translate( + in_filename, + "data/stefan_full_rgba.tif", + options="-b 1 -b 2 -b 3 -mask 4 -outsize 1024 0", + ) + ds.BuildOverviews("NEAR", [2]) + ds.Close() + + src_ds = gdal.Open(in_filename) + gdal.GetDriverByName("COG").CreateCopy( + cog_filename, + src_ds, + options=["INTERLEAVE=" + INTERLEAVE, "OVERVIEW_RESAMPLING=NEAREST"], + ) + + def extract(offset, size): + f = gdal.VSIFOpenL(cog_filename, "rb") + gdal.VSIFSeekL(f, offset, 0) + data = gdal.VSIFReadL(size, 1, f) + gdal.VSIFCloseL(f) + return data + + try: + filesize = gdal.VSIStatL(cog_filename).size + + handler = webserver.SequentialHandler() + handler.add("HEAD", "/cog.tif", 200, {"Content-Length": "%d" % filesize}) + handler.add( + "GET", + "/cog.tif", + 206, + {"Content-Length": "16384"}, + extract(0, 16384), + expected_headers={"Range": "bytes=0-16383"}, + ) + with webserver.install_http_handler(handler): + ds = gdal.Open("/vsicurl/http://localhost:%d/cog.tif" % webserver_port) + + handler = webserver.SequentialHandler() + + def method(request): + # sys.stderr.write('%s\n' % str(request.headers)) + + if request.headers["Range"].startswith("bytes="): + rng = request.headers["Range"][len("bytes=") :] + assert len(rng.split("-")) == 2 + start = int(rng.split("-")[0]) + end = int(rng.split("-")[1]) + + request.protocol_version = "HTTP/1.1" + request.send_response(206) + request.send_header("Content-type", "application/octet-stream") + request.send_header( + "Content-Range", "bytes %d-%d/%d" % (start, end, filesize) + ) + request.send_header("Content-Length", end - start + 1) + request.send_header("Connection", "close") + request.end_headers() + + request.wfile.write(extract(start, end - start + 1)) + + handler.add("GET", "/cog.tif", custom_method=method) + with webserver.install_http_handler(handler): + ret = ds.ReadRaster() + assert ret == src_ds.ReadRaster() + + finally: + webserver.server_stop(webserver_process, webserver_port) + + gdal.VSICurlClearCache() + + +############################################################################### + + +@pytest.mark.require_creation_option("COG", "JPEG") +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_jpeg(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + gdal.GetDriverByName("GTiff").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=BAND", "COMPRESS=JPEG"], + ) + with gdal.Open(out_filename) as ds: + expected_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "COMPRESS=JPEG"], + ) + with gdal.Open(out_filename) as ds: + got_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + + assert got_md == expected_md + + +############################################################################### + + +@pytest.mark.require_creation_option("COG", "WEBP") +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_webp_error(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with pytest.raises( + Exception, match="COMPRESS=WEBP only supported for INTERLEAVE=PIXEL" + ): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "COMPRESS=WEBP"], + ) diff --git a/autotest/gcore/test_driver_metadata.py b/autotest/gcore/test_driver_metadata.py index d36628be4055..b8907c47e938 100644 --- a/autotest/gcore/test_driver_metadata.py +++ b/autotest/gcore/test_driver_metadata.py @@ -130,6 +130,7 @@ + diff --git a/autotest/gcore/tiff_write.py b/autotest/gcore/tiff_write.py index 25f58a827f9c..c589fdf6b200 100755 --- a/autotest/gcore/tiff_write.py +++ b/autotest/gcore/tiff_write.py @@ -11988,3 +11988,55 @@ def test_tiff_write_warn_ignore_predictor_option(tmp_vsimem): out_filename, 1, 1, options=["PREDICTOR=2"] ) assert "PREDICTOR option is ignored" in gdal.GetLastErrorMsg() + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +@pytest.mark.parametrize("COPY_SRC_OVERVIEWS", ["YES", "NO"]) +def test_tiff_write_interleave_tile(tmp_vsimem, COPY_SRC_OVERVIEWS): + out_filename = str(tmp_vsimem / "out.tif") + with pytest.raises( + Exception, match="INTERLEAVE=TILE is only supported for CreateCopy" + ): + gdal.GetDriverByName("GTiff").Create( + out_filename, 1, 1, 2, options=["INTERLEAVE=TILE"] + ) + + ds = gdal.GetDriverByName("GTiff").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=[ + "INTERLEAVE=TILE", + "TILED=YES", + "BLOCKXSIZE=32", + "BLOCKYSIZE=32", + "COPY_SRC_OVERVIEWS=" + COPY_SRC_OVERVIEWS, + ], + ) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + ds.Close() + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + # Check that the tiles are in the expected order in the file + last_offset = 0 + for y in range(2): + for x in range(2): + for band in range(3): + offset = int( + ds.GetRasterBand(band + 1).GetMetadataItem( + f"BLOCK_OFFSET_{x}_{y}", "TIFF" + ) + ) + assert offset > last_offset + last_offset = offset diff --git a/doc/source/drivers/raster/cog.rst b/doc/source/drivers/raster/cog.rst index c46bac1f5703..a84a592725d6 100644 --- a/doc/source/drivers/raster/cog.rst +++ b/doc/source/drivers/raster/cog.rst @@ -43,6 +43,105 @@ General creation options Sets the tile width and height in pixels. Must be divisible by 16. +- .. co:: INTERLEAVE + :choices: BAND, PIXEL, TILE + :since: 3.11 + + Set the interleaving to use + + * ``PIXEL``: for each spatial block, one TIFF tile/strip gathering values for + all bands is used . This matches the ``contiguous`` planar configuration in + TIFF terminology. + This is also known as a ``BIP (Band Interleaved Per Pixel)`` organization. + Such method is the default, and may be slightly less + efficient than BAND interleaving for some purposes, but some applications + only support pixel interleaved TIFF files. On the other hand, image-based + compression methods may perform better using PIXEL interleaving. For JPEG + PHOTOMETRIC=YCbCr, pixel interleaving is required. It is also required for + WebP compression. + + Prior to GDAL 3.11, INTERLEAVE=PIXEL was the only possible interleaving + output by the COG driver. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_new_strip_or_tile() + end_for + end_for + end_for + end_for + + + * ``BAND``: for each spatial block, one TIFF tile/strip is used for each band. + This matches the contiguous ``separate`` configuration in TIFF terminology. + This is also known as a ``BSQ (Band SeQuential)`` organization. + + In addition to that, when using CreateCopy(), data for the first band is + written first, followed by data for the second band, etc. + The pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + + + * ``TILE`` (added in 3.11): this is a sort of + compromise between PIXEL and BAND, using the ``separate`` configuration, + but where data for a same spatial block is written for all bands, before + the data of the next spatial block. + When the block height is 1, this is also known as a + ``BIL (Band Interleaved per Line)`` organization. + + Such a layout may be useful for writing hyperspectral datasets (several + hundred of bands), to get a compromise between efficient spatial query, + and partial band selection. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for band in 0 ... numberBands -1: + start_new_strip_or_tile() + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + end_for + + + Starting with GDAL 3.5, when copying from a source dataset with multiple bands + which advertises a INTERLEAVE metadata item, if the INTERLEAVE creation option + is not specified, the source dataset INTERLEAVE will be automatically taken + into account, unless the COMPRESS creation option is specified. + - .. co:: COMPRESS :choices: NONE, LZW, JPEG, DEFLATE, ZSTD, WEBP, LERC, LERC_DEFLATE, LERC_ZSTD, LZMA :default: LZW @@ -56,7 +155,7 @@ General creation options files (seen as UInt16 bands with NBITS=12). For the COG driver, JPEG compression for 3 or 4-band images automatically selects the PHOTOMETRIC=YCBCR colorspace with a 4:2:2 subsampling of the Y,Cb,Cr - components. + components with the default INTERLEAVE=PIXEL. For a input dataset (single-band or 3-band), plus an alpha band, the alpha band will be converted as a 1-bit DEFLATE compressed mask. @@ -65,6 +164,10 @@ General creation options * ``ZSTD`` is available when using internal libtiff and if GDAL built against libzstd >=1.0, or if built against external libtiff with zstd support. + * ``WEBP`` is available when using internal libtiff and if GDAL built against + libwebp, or if built against external libtiff with WebP support. + It can only be used with the default INTERLEAVE=PIXEL. + * ``LERC`` is available when using internal libtiff. * ``LERC_ZSTD`` is available when ``LERC`` and ``ZSTD`` are available. @@ -559,11 +662,18 @@ line). mask.TileByteCounts[i], but none of them actually need to be read) * trailer of mask data (4 bytes) + This is only written if INTERLEAVE=PIXEL. + +- ``INTERLEAVE=BAND`` or ``INTERLEAVE=TILE``: (GDAL >= 3.11) + Reflects the value of the INTERLEAVE creation option. + Omission implies INTERLEAVE=PIXEL. + + .. note:: The content of the header ghost area can be retrieved by getting the ``GDAL_STRUCTURAL_METADATA`` metadata item of the ``TIFF`` metadata domain - on the datasett object (with GetMetadataItem()) + on the dataset object (with GetMetadataItem()) .. _cog.tile_data_leader_trailer: @@ -576,7 +686,8 @@ that follows it. This leader is *ghost* in the sense that the TileOffsets[] array does not point to it, but points to the real payload. Hence the offset of the leader is TileOffsets[i]-4. -An optimized reader seeing the ``BLOCK_LEADER=SIZE_AS_UINT4`` metadata item will thus look for TileOffset[i] +For INTERLEAVE=PIXEL or INTERLEAVE=TILE, an optimized reader seeing the +``BLOCK_LEADER=SIZE_AS_UINT4`` metadata item will thus look for TileOffset[i] and TileOffset[i+1] to deduce it must fetch the data starting at offset=TileOffset[i] - 4 and of size=TileOffset[i+1]-TileOffset[i]+4. It then checks the 4 first bytes to see if the size in this leader marker is @@ -587,6 +698,9 @@ MASK_INTERLEAVED_WITH_IMAGERY=YES, then the tile size indicated in the leader will be < TileOffset[i+1]-TileOffset[i] since the data for the mask will follow the imagery data (see MASK_INTERLEAVED_WITH_IMAGERY=YES) +For INTERLEAVE=BAND, the above paragraph applies but the successor of tile i +is not tile i+1, but tile i+nTilesPerBand. + Each tile data is immediately followed by a trailer, consisting of the repetition of the last 4 bytes of the payload of the tile data. The size of this trailer is *not* included in the TileByteCounts[] array. The purpose of this trailer is forces @@ -613,3 +727,9 @@ See Also - If your source dataset is an internally tiled geotiff with the desired georeferencing and compression, using `cogger `__ (possibly along with gdaladdo to create overviews) will be much faster than the COG driver. + + +.. below is an allow-list for spelling checker. + +.. spelling:word-list:: + nTilesPerBand diff --git a/doc/source/drivers/raster/gtiff.rst b/doc/source/drivers/raster/gtiff.rst index 31f2d4a46ece..c10a625852cc 100644 --- a/doc/source/drivers/raster/gtiff.rst +++ b/doc/source/drivers/raster/gtiff.rst @@ -423,13 +423,95 @@ This driver supports the following creation options: RPC information is available. - .. co:: INTERLEAVE - :choices: BAND, PIXEL + :choices: BAND, PIXEL, TILE + + Set the interleaving to use + + * ``PIXEL``: for each spatial block, one TIFF tile/strip gathering values for + all bands is used . This matches the ``contiguous`` planar configuration in + TIFF terminology. + This is also known as a ``BIP (Band Interleaved Per Pixel)`` organization. + Such method is the default, and may be slightly less + efficient than BAND interleaving for some purposes, but some applications + only support pixel interleaved TIFF files. On the other hand, image-based + compression methods may perform better using PIXEL interleaving. For JPEG + PHOTOMETRIC=YCbCr, pixel interleaving is required. It is also required for + WebP compression. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_new_strip_or_tile() + end_for + end_for + end_for + end_for + + + * ``BAND``: for each spatial block, one TIFF tile/strip is used for each band. + This matches the contiguous ``separate`` configuration in TIFF terminology. + This is also known as a ``BSQ (Band SeQuential)`` organization. + + In addition to that, when using CreateCopy(), data for the first band is + written first, followed by data for the second band, etc. + The pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + + + * ``TILE`` (added in 3.11, only for CreateCopy()): this is a sort of + compromise between PIXEL and BAND, using the ``separate`` configuration, + but where data for a same spatial block is written for all bands, before + the data of the next spatial block. + When the block height is 1, this is also known as a + ``BIL (Band Interleaved per Line)`` organization. + + Such a layout may be useful for writing hyperspectral datasets (several + hundred of bands), to get a compromise between efficient spatial query, + and partial band selection. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for band in 0 ... numberBands -1: + start_new_strip_or_tile() + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + end_for + - By default TIFF files with pixel - interleaving (PLANARCONFIG_CONTIG in TIFF terminology) are created. - These are slightly less efficient than BAND interleaving for some - purposes, but some applications only support pixel interleaved TIFF - files. Starting with GDAL 3.5, when copying from a source dataset with multiple bands which advertises a INTERLEAVE metadata item, if the INTERLEAVE creation option is not specified, the source dataset INTERLEAVE will be automatically taken diff --git a/frmts/gtiff/cogdriver.cpp b/frmts/gtiff/cogdriver.cpp index 310204a64127..73ced0419555 100644 --- a/frmts/gtiff/cogdriver.cpp +++ b/frmts/gtiff/cogdriver.cpp @@ -763,6 +763,21 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, return nullptr; } + const CPLString osCompress = CSLFetchNameValueDef( + papszOptions, "COMPRESS", gbHasLZW ? "LZW" : "NONE"); + + const char *pszInterleave = + CSLFetchNameValueDef(papszOptions, "INTERLEAVE", "PIXEL"); + if (EQUAL(osCompress, "WEBP")) + { + if (!EQUAL(pszInterleave, "PIXEL")) + { + CPLError(CE_Failure, CPLE_NotSupported, + "COMPRESS=WEBP only supported for INTERLEAVE=PIXEL"); + return nullptr; + } + } + CPLConfigOptionSetter oSetterReportDirtyBlockFlushing( "GDAL_REPORT_DIRTY_BLOCK_FLUSHING", "NO", true); @@ -877,9 +892,7 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, } } - CPLString osCompress = CSLFetchNameValueDef(papszOptions, "COMPRESS", - gbHasLZW ? "LZW" : "NONE"); - if (EQUAL(osCompress, "JPEG") && + if (EQUAL(osCompress, "JPEG") && EQUAL(pszInterleave, "PIXEL") && (poCurDS->GetRasterCount() == 2 || poCurDS->GetRasterCount() == 4) && poCurDS->GetRasterBand(poCurDS->GetRasterCount()) ->GetColorInterpretation() == GCI_AlphaBand) @@ -1198,7 +1211,7 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, if (EQUAL(osCompress, "JPEG")) { aosOptions.SetNameValue("JPEG_QUALITY", pszQuality); - if (nBands == 3) + if (nBands == 3 && EQUAL(pszInterleave, "PIXEL")) aosOptions.SetNameValue("PHOTOMETRIC", "YCBCR"); } else if (EQUAL(osCompress, "WEBP")) @@ -1316,7 +1329,8 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, } std::unique_ptr poPhotometricSetter; - if (nBands == 3 && EQUAL(pszOverviewCompress, "JPEG")) + if (nBands == 3 && EQUAL(pszOverviewCompress, "JPEG") && + EQUAL(pszInterleave, "PIXEL")) { poPhotometricSetter.reset( new CPLConfigOptionSetter("PHOTOMETRIC_OVERVIEW", "YCBCR", true)); @@ -1347,6 +1361,8 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, aosOptions.AddNameValue("SRC_MDD", *papszSrcMDDIter); CSLDestroy(papszSrcMDD); + aosOptions.SetNameValue("INTERLEAVE", pszInterleave); + CPLDebug("COG", "Generating final product: start"); auto poRet = poGTiffDrv->CreateCopy(pszFilename, poCurDS, false, aosOptions.List(), @@ -1525,6 +1541,11 @@ void GDALCOGDriver::InitializeCreationOptionList() "(16)'/>" " " " " "