Skip to content

Commit

Permalink
gdal2tiles: added support for JPEG output
Browse files Browse the repository at this point in the history
Fixes #6703
  • Loading branch information
zdila authored and rouault committed Apr 17, 2024
1 parent 88b0dd8 commit b9e984a
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 10 deletions.
141 changes: 141 additions & 0 deletions autotest/pyscripts/test_gdal2tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,144 @@ def test_gdal2tiles_excluded_values(script_path, tmp_path):
(12 + 22 + 42) // 3,
255,
)


@pytest.mark.require_driver("JPEG")
@pytest.mark.parametrize(
"resampling, expected_stats_z0, expected_stats_z1",
(
(
"average",
[
[0.0, 255.0, 62.789886474609375, 71.57543623020909],
[0.0, 255.0, 62.98188781738281, 70.54545410356597],
[0.0, 255.0, 77.94142150878906, 56.07427114858068],
],
[
[0.0, 255.0, 63.620819091796875, 68.38688881060699],
[0.0, 255.0, 63.620819091796875, 68.38688881060699],
[0.0, 255.0, 87.09403991699219, 53.07665243601322],
],
),
(
"antialias",
[
[0.0, 255.0, 62.66636657714844, 71.70766144632985],
[0.0, 255.0, 62.91070556640625, 70.705889259777],
[0.0, 255.0, 77.78370666503906, 56.251290816620596],
],
[
[0.0, 255.0, 63.61163330078125, 68.49625328462534],
[0.0, 255.0, 63.61163330078125, 68.49625328462534],
[0.0, 255.0, 87.04747009277344, 53.1751939061486],
],
),
),
)
def test_gdal2tiles_py_jpeg_3band_input(
script_path, tmp_path, resampling, expected_stats_z0, expected_stats_z1
):

if resampling == "antialias" and not pil_available():
pytest.skip("'antialias' resampling is not available")

out_dir_jpeg = str(tmp_path / "out_gdal2tiles_smallworld_jpeg")

test_py_scripts.run_py_script_as_external_script(
script_path,
"gdal2tiles",
"-q -z 0-1 -r "
+ resampling
+ " --tiledriver=JPEG "
+ test_py_scripts.get_data_path("gdrivers")
+ f"small_world.tif {out_dir_jpeg}",
)

ds = gdal.Open(f"{out_dir_jpeg}/0/0/0.jpg")
got_stats_0 = [
ds.GetRasterBand(i + 1).ComputeStatistics(approx_ok=0)
for i in range(ds.RasterCount)
]

ds = gdal.Open(f"{out_dir_jpeg}/1/0/0.jpg")
got_stats_1 = [
ds.GetRasterBand(i + 1).ComputeStatistics(approx_ok=0)
for i in range(ds.RasterCount)
]

for i in range(ds.RasterCount):
assert got_stats_0[i] == pytest.approx(expected_stats_z0[i], rel=0.05), (
i,
got_stats_0,
got_stats_1,
)

for i in range(ds.RasterCount):
assert got_stats_1[i] == pytest.approx(expected_stats_z1[i], rel=0.05), (
i,
got_stats_0,
got_stats_1,
)


@pytest.mark.require_driver("JPEG")
@pytest.mark.parametrize(
"resampling, expected_stats_z14, expected_stats_z13",
(
(
(
"average",
[[0.0, 255.0, 44.11726379394531, 61.766206763153946]],
[[0.0, 255.0, 11.057342529296875, 36.182401045647644]],
),
(
"antialias",
[[0.0, 255.0, 43.9254150390625, 61.58666064861184]],
[[0.0, 255.0, 11.013427734375, 36.12022842174338]],
),
)
),
)
def test_gdal2tiles_py_jpeg_1band_input(
script_path, tmp_path, resampling, expected_stats_z14, expected_stats_z13
):

if resampling == "antialias" and not pil_available():
pytest.skip("'antialias' resampling is not available")

out_dir_jpeg = str(tmp_path / "out_gdal2tiles_byte_jpeg")

test_py_scripts.run_py_script_as_external_script(
script_path,
"gdal2tiles",
"-q -z 13-14 -r "
+ resampling
+ " --tiledriver=JPEG "
+ test_py_scripts.get_data_path("gcore")
+ f"byte.tif {out_dir_jpeg}",
)

ds = gdal.Open(f"{out_dir_jpeg}/14/2838/9833.jpg")
got_stats_14 = [
ds.GetRasterBand(i + 1).ComputeStatistics(approx_ok=0)
for i in range(ds.RasterCount)
]

ds = gdal.Open(f"{out_dir_jpeg}/13/1419/4916.jpg")
got_stats_13 = [
ds.GetRasterBand(i + 1).ComputeStatistics(approx_ok=0)
for i in range(ds.RasterCount)
]

for i in range(ds.RasterCount):
assert got_stats_14[i] == pytest.approx(expected_stats_z14[i], rel=0.05), (
i,
got_stats_14,
got_stats_13,
)
for i in range(ds.RasterCount):
assert got_stats_13[i] == pytest.approx(expected_stats_z13[i], rel=0.05), (
i,
got_stats_14,
got_stats_13,
)
19 changes: 16 additions & 3 deletions doc/source/programs/gdal2tiles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Synopsis
[-e] [-a nodata] [-v] [-q] [-h] [-k] [-n] [-u <url>]
[-w <webviewer>] [-t <title>] [-c <copyright>]
[--processes=<NB_PROCESSES>] [--mpi] [--xyz]
[--tilesize=<PIXELS>] [--tmscompatible]
[--tilesize=<PIXELS>] --tiledriver=<DRIVER> [--tmscompatible]
[--excluded-values=<EXCLUDED_VALUES>] [--excluded-values-pct-threshold=<EXCLUDED_VALUES_PCT_THRESHOLD>]
[-g <googlekey] [-b <bingkey>] <input_file> [<output_dir>] [<COMMON_OPTIONS>]
Expand Down Expand Up @@ -139,8 +139,8 @@ can publish a picture without proper georeferencing too.
.. option:: --tiledriver=<DRIVER>

Which output driver to use for the tiles, determines the file format of the tiles.
Currently PNG and WEBP are supported. Default is PNG.
Additional configuration for the WEBP driver are documented below.
Currently PNG, WEBP and JPEG (JPEG added in GDAL 3.9) are supported. Default is PNG.
Additional configuration for the WEBP and JPEG drivers are documented below.

.. versionadded:: 3.6

Expand Down Expand Up @@ -266,6 +266,19 @@ The following configuration options are available to further customize the webp
GDAL :ref:`WEBP driver <raster.webp>` documentation can be consulted


JPEG options
+++++++++++++

JPEG tiledriver support is new to GDAL 3.9. It is enabled by using --tiledriver=JPEG.

Note that JPEG does not support transparency, hence edge tiles will display black
pixels in areas not covered by the source raster.

The following configuration options are available to further customize the webp output:

.. option:: ---jpeg-quality=JPEG_QUALITY

QUALITY is a integer between 1-100. Default is 75.


Examples
Expand Down
116 changes: 109 additions & 7 deletions swig/python/gdal-utils/osgeo_utils/gdal2tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,12 @@ def scale_query_to_tile(dsquery, dstile, options, tilefilename=""):
array[:, :, i] = gdalarray.BandReadAsArray(
dsquery.GetRasterBand(i + 1), 0, 0, querysize, querysize
)
im = Image.fromarray(array, "RGBA") # Always four bands
if options.tiledriver == "JPEG" and tilebands == 2:
im = Image.fromarray(array[:, :, 0], "L")
elif options.tiledriver == "JPEG" and tilebands == 4:
im = Image.fromarray(array[:, :, 0:3], "RGB")
else:
im = Image.fromarray(array, "RGBA")
im1 = im.resize((tile_size, tile_size), Image.LANCZOS)
if os.path.exists(tilefilename):
im0 = Image.open(tilefilename)
Expand All @@ -931,6 +936,8 @@ def scale_query_to_tile(dsquery, dstile, options, tilefilename=""):
params["lossless"] = True
else:
params["quality"] = options.webp_quality
elif options.tiledriver == "JPEG":
params["quality"] = options.jpeg_quality
im1.save(tilefilename, options.tiledriver, **params)

else:
Expand Down Expand Up @@ -1314,6 +1321,8 @@ def _get_creation_options(options):
copts = ["LOSSLESS=True"]
else:
copts = ["QUALITY=" + str(options.webp_quality)]
elif options.tiledriver == "JPEG":
copts = ["QUALITY=" + str(options.jpeg_quality)]
return copts


Expand Down Expand Up @@ -1430,7 +1439,12 @@ def create_base_tile(tile_job_info: "TileJobInfo", tile_detail: "TileDetail") ->
if options.resampling != "antialias":
# Write a copy of tile to png/jpg
out_drv.CreateCopy(
tilefilename, dstile, strict=0, options=_get_creation_options(options)
tilefilename,
dstile
if tile_job_info.tile_driver != "JPEG"
else remove_alpha_band(dstile),
strict=0,
options=_get_creation_options(options),
)

# Remove useless side car file
Expand Down Expand Up @@ -1465,6 +1479,38 @@ def create_base_tile(tile_job_info: "TileJobInfo", tile_detail: "TileDetail") ->
)


def remove_alpha_band(src_ds):
if (
src_ds.GetRasterBand(src_ds.RasterCount).GetColorInterpretation()
!= gdal.GCI_AlphaBand
):
return src_ds

new_band_count = src_ds.RasterCount - 1

dst_ds = gdal.GetDriverByName("MEM").Create(
"",
src_ds.RasterXSize,
src_ds.RasterYSize,
new_band_count,
src_ds.GetRasterBand(1).DataType,
)

gt = src_ds.GetGeoTransform(can_return_null=True)
if gt:
dst_ds.SetGeoTransform(gt)
srs = src_ds.GetSpatialRef()
if srs:
dst_ds.SetSpatialRef(srs)

for i in range(1, new_band_count + 1):
src_band = src_ds.GetRasterBand(i)
dst_band = dst_ds.GetRasterBand(i)
dst_band.WriteArray(src_band.ReadAsArray())

return dst_ds


def create_overview_tile(
base_tz: int,
base_tiles: List[Tuple[int, int]],
Expand Down Expand Up @@ -1542,7 +1588,35 @@ def create_overview_tile(
else:
tileposy = 0

if dsquerytile.RasterCount == tilebands - 1:
if (
tile_job_info.tile_driver == "JPEG"
and dsquerytile.RasterCount == 3
and tilebands == 2
):
# Input is RGB with R=G=B. Add An alpha band
tmp_ds = mem_driver.Create(
"", dsquerytile.RasterXSize, dsquerytile.RasterYSize, 2
)
tmp_ds.GetRasterBand(1).WriteRaster(
0,
0,
tile_job_info.tile_size,
tile_job_info.tile_size,
dsquerytile.GetRasterBand(1).ReadRaster(),
)
mask = bytearray(
[255] * (tile_job_info.tile_size * tile_job_info.tile_size)
)
tmp_ds.GetRasterBand(2).WriteRaster(
0,
0,
tile_job_info.tile_size,
tile_job_info.tile_size,
mask,
)
tmp_ds.GetRasterBand(2).SetColorInterpretation(gdal.GCI_AlphaBand)
dsquerytile = tmp_ds
elif dsquerytile.RasterCount == tilebands - 1:
# assume that the alpha band is missing and add it
tmp_ds = mem_driver.CreateCopy("", dsquerytile, 0)
tmp_ds.AddBand()
Expand All @@ -1559,7 +1633,10 @@ def create_overview_tile(
)
dsquerytile = tmp_ds
elif dsquerytile.RasterCount != tilebands:
raise Exception("Unexpected number of bands in base tile")
raise Exception(
"Unexpected number of bands in base tile. Got %d, expected %d"
% (dsquerytile.RasterCount, tilebands)
)

base_data = dsquerytile.ReadRaster(
0, 0, tile_job_info.tile_size, tile_job_info.tile_size
Expand All @@ -1584,7 +1661,12 @@ def create_overview_tile(
if options.resampling != "antialias":
# Write a copy of tile to png/jpg
out_driver.CreateCopy(
tilefilename, dstile, strict=0, options=_get_creation_options(options)
tilefilename,
dstile
if tile_job_info.tile_driver != "JPEG"
else remove_alpha_band(dstile),
strict=0,
options=_get_creation_options(options),
)
# Remove useless side car file
aux_xml = tilefilename + ".aux.xml"
Expand Down Expand Up @@ -1772,7 +1854,7 @@ def optparse_init() -> optparse.OptionParser:
p.add_option(
"--tiledriver",
dest="tiledriver",
choices=["PNG", "WEBP"],
choices=["PNG", "WEBP", "JPEG"],
default="PNG",
type="choice",
help="which tile driver to use for the tiles",
Expand Down Expand Up @@ -1883,6 +1965,17 @@ def optparse_init() -> optparse.OptionParser:
)
p.add_option_group(g)

# Jpeg options
g = optparse.OptionGroup(p, "JPEG options", "Options for JPEG tiledriver")
g.add_option(
"--jpeg-quality",
dest="jpeg_quality",
type=int,
default=75,
help="quality of jpeg image, integer between 1 and 100, default is 75",
)
p.add_option_group(g)

p.set_defaults(
verbose=False,
profile="mercator",
Expand Down Expand Up @@ -1996,6 +2089,13 @@ def options_post_processing(
if options.webp_quality <= 0 or options.webp_quality > 100:
exit_with_error("webp_quality should be in the range [1-100]")
options.webp_quality = int(options.webp_quality)
elif options.tiledriver == "JPEG":
if gdal.GetDriverByName(options.tiledriver) is None:
exit_with_error("JPEG driver is not available")

if options.jpeg_quality <= 0 or options.jpeg_quality > 100:
exit_with_error("jpeg_quality should be in the range [1-100]")
options.jpeg_quality = int(options.jpeg_quality)

# Output the results
if options.verbose:
Expand Down Expand Up @@ -2113,8 +2213,10 @@ def __init__(self, input_file: str, output_folder: str, options: Options) -> Non
self.tiledriver = options.tiledriver
if options.tiledriver == "PNG":
self.tileext = "png"
else:
elif options.tiledriver == "WEBP":
self.tileext = "webp"
else:
self.tileext = "jpg"
if options.mpi:
makedirs(output_folder)
self.tmp_dir = tempfile.mkdtemp(dir=output_folder)
Expand Down

0 comments on commit b9e984a

Please sign in to comment.