Skip to content

Commit

Permalink
HEIF and AVIF: add read-only GeoHEIF support (#11333)
Browse files Browse the repository at this point in the history
Adds read-only support for the draft OGC 24-038 "GeoHEIF" geo-referencing method for HEIF related drivers. This is being developed for OGC Testbed 20.

See http://t20-gimi-ogc-006ced09c39758532bebec9d5488fca26bed7198a2d3b1481b.pages.ogc.org/documents/D010/document.html for a WIP draft.

libheif and libavif have slightly different ways to get properties, but the base parsing code is shared.

This will work with libheif from 1.19.0. For libavif, it'll require master branch. There is a slightly modified CI build to check that.

Co-authored-by: Even Rouault <[email protected]>
Co-authored-by: Kai Pastor <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent 9bc4894 commit b914d9b
Show file tree
Hide file tree
Showing 20 changed files with 905 additions and 3 deletions.
13 changes: 11 additions & 2 deletions .github/workflows/ubuntu_24.04/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ RUN apt-get update && \
g++ \
git \
gpsbabel \
libavif-dev \
libblosc-dev \
libboost-dev \
libcairo2-dev \
Expand Down Expand Up @@ -73,7 +72,17 @@ RUN apt-get update && \
wget \
zip

# temporary libheif build
# libavif development build
RUN apt-get install -y --allow-unauthenticated libaom-dev libyuv-dev
RUN git clone --depth 1 https://github.com/AOMediaCodec/libavif.git libavif-git && \
cd libavif-git && \
mkdir build && \
cd build && \
cmake -DAVIF_CODEC_AOM=SYSTEM .. && \
make -j$(nproc) && \
make install

# libheif development build
RUN apt-get install -y --allow-unauthenticated libaom-dev libbrotli-dev libde265-dev libx265-dev
RUN git clone --depth 1 https://github.com/strukturag/libheif.git libheif-git && \
cd libheif-git && \
Expand Down
98 changes: 98 additions & 0 deletions autotest/gdrivers/avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def has_avif_encoder():
return drv is not None and drv.GetMetadataItem("DMD_CREATIONOPTIONLIST") is not None


def _has_geoheif_support():
drv = gdal.GetDriverByName("AVIF")
return drv and drv.GetMetadataItem("SUPPORTS_GEOHEIF", "AVIF")


def test_avif_subdatasets(tmp_path):

filename = str(tmp_path / "out.avif")
Expand Down Expand Up @@ -255,3 +260,96 @@ def test_avif_creation_errors(tmp_vsimem):
src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
with pytest.raises(Exception, match="Cannot create file /i_do/not/exist.avif"):
gdal.GetDriverByName("AVIF").CreateCopy("/i_do/not/exist.avif", src_ds)


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libavif does not support opaque properties like geoheif",
)
def test_avif_geoheif_wkt2():
ds = gdal.Open("data/heif/geo_small.avif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 128
assert ds.RasterYSize == 76
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691000.0, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691000.0, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libavif does not support opaque properties like geoheif",
)
def test_avif_geoheif_uri():
ds = gdal.Open("data/heif/geo_crsu.avif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 256
assert ds.RasterYSize == 64
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691051.2, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691051.2, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libavif does not support opaque properties like geoheif",
)
def test_avif_geoheif_curie():
ds = gdal.Open("data/heif/geo_curi.avif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 256
assert ds.RasterYSize == 64
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691051.2, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691051.2, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)
28 changes: 28 additions & 0 deletions autotest/gdrivers/avif_heif.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ def test_avif_heif():
)


def _has_geoheif_support():
drv = gdal.GetDriverByName("HEIF")
return drv and drv.GetMetadataItem("SUPPORTS_GEOHEIF", "HEIF")


if __name__ == "__main__":

os.environ["GDAL_SKIP"] = "AVIF"
Expand All @@ -45,3 +50,26 @@ def test_avif_heif():
gdal.UseExceptions()
ds = gdal.Open("data/avif/byte.avif")
assert ds.GetRasterBand(1).Checksum() == 4672

if _has_geoheif_support():
ds = gdal.Open("data/heif/geo_small.avif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 128
assert ds.RasterYSize == 76
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691000.0, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
print()
print("GCPs from avif_heif: ", ds.GetGCPCount())
print()
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691000.0, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)
Binary file added autotest/gdrivers/data/heif/geo_crsu.avif
Binary file not shown.
Binary file added autotest/gdrivers/data/heif/geo_crsu.heif
Binary file not shown.
Binary file added autotest/gdrivers/data/heif/geo_curi.avif
Binary file not shown.
Binary file added autotest/gdrivers/data/heif/geo_curi.heif
Binary file not shown.
Binary file added autotest/gdrivers/data/heif/geo_small.avif
Binary file not shown.
Binary file added autotest/gdrivers/data/heif/geo_wkt2.heif
Binary file not shown.
103 changes: 103 additions & 0 deletions autotest/gdrivers/heif.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ def _has_tiling_support():
return drv and drv.GetMetadataItem("SUPPORTS_TILES", "HEIF")


def _has_avif_decoding_support():
drv = gdal.GetDriverByName("HEIF")
return drv and drv.GetMetadataItem("SUPPORTS_AVIF", "HEIF")


def _has_hevc_decoding_support():
drv = gdal.GetDriverByName("HEIF")
return drv and drv.GetMetadataItem("SUPPORTS_HEVC", "HEIF")
Expand All @@ -57,6 +62,11 @@ def _has_read_write_support_for(format):
)


def _has_geoheif_support():
drv = gdal.GetDriverByName("HEIF")
return drv and drv.GetMetadataItem("SUPPORTS_GEOHEIF", "HEIF")


@pytest.mark.parametrize("endianness", ["big_endian", "little_endian"])
def test_heif_exif_endian(endianness):
if not _has_hevc_decoding_support():
Expand Down Expand Up @@ -617,3 +627,96 @@ def test_heif_create_copy_defaults(tmp_path):
result_ds = gdal.Open(tempfile)

assert result_ds


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libheif does not support opaque properties like geoheif",
)
def test_heif_geoheif_wkt2():
ds = gdal.Open("data/heif/geo_wkt2.heif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 256
assert ds.RasterYSize == 64
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691051.2, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691051.2, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libheif does not support opaque properties like geoheif",
)
def test_heif_geoheif_uri():
ds = gdal.Open("data/heif/geo_crsu.heif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 256
assert ds.RasterYSize == 64
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691051.2, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691051.2, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)


@pytest.mark.skipif(
not _has_geoheif_support(),
reason="this libheif does not support opaque properties like geoheif",
)
def test_heif_geoheif_curie():
ds = gdal.Open("data/heif/geo_curi.heif")
assert ds
assert ds.RasterCount == 3
assert ds.RasterXSize == 256
assert ds.RasterYSize == 64
assert ds.GetMetadataItem("NAME", "DESCRIPTION_en-AU") == "Copyright Statement"
assert (
ds.GetMetadataItem("DESCRIPTION", "DESCRIPTION_en-AU")
== 'CCBY "Jacobs Group (Australia) Pty Ltd and Australian Capital Territory"'
)
assert ds.GetMetadataItem("TAGS", "DESCRIPTION_en-AU") == "copyright"
assert ds.GetGeoTransform() is not None
assert ds.GetGeoTransform() == pytest.approx(
[691051.2, 0.1, 0.0, 6090000.0, 0.0, -0.1]
)
assert ds.GetGCPCount() == 1
gcp = ds.GetGCPs()[0]
assert (
gcp.GCPPixel == pytest.approx(0, abs=1e-5)
and gcp.GCPLine == pytest.approx(0, abs=1e-5)
and gcp.GCPX == pytest.approx(691051.2, abs=1e-5)
and gcp.GCPY == pytest.approx(6090000.0, abs=1e-5)
and gcp.GCPZ == pytest.approx(0, abs=1e-5)
)
21 changes: 21 additions & 0 deletions cmake/helpers/CheckDependentLibrariesAVIF.cmake
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
define_find_package2(AVIF avif/avif.h avif PKGCONFIG_NAME libavif)
gdal_check_package(AVIF "AVIF" CAN_DISABLE)

# Check if libavif supports opaque properties
include(CheckCXXSourceCompiles)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_INCLUDES "${AVIF_INCLUDE_DIRS}")
check_cxx_source_compiles(
"
#include <avif/avif.h>
int main()
{
offsetof(avifImage, numProperties);
return 0;
}
"
AVIF_HAS_OPAQUE_PROPERTIES
)
cmake_pop_check_state()

if (AVIF_HAS_OPAQUE_PROPERTIES)
set_property(TARGET AVIF::AVIF APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS "AVIF_HAS_OPAQUE_PROPERTIES")
endif ()
11 changes: 11 additions & 0 deletions doc/source/drivers/raster/avif.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Driver capabilities
.. supports_createcopy
.. supports_georeferencing::

Color Profile Metadata
----------------------

Expand All @@ -41,6 +43,15 @@ metadata in the COLOR_PROFILE domain:

- SOURCE_ICC_PROFILE (Base64 encoded ICC profile embedded in file.)

Georeferencing
--------------

AVIF provides experimental read-only support for GeoHEIF internal referencing,
based on the draft OGC 24-038 design. There is no support for writing
at this time.

This requires at least libavif 1.2.0 (currently unreleased).

Creation options
----------------

Expand Down
10 changes: 10 additions & 0 deletions doc/source/drivers/raster/heif.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ Driver capabilities

.. supports_createcopy::

.. supports_georeferencing::

Georeferencing
--------------

HEIF provides experimental read-only support for GeoHEIF internal referencing,
based on the draft OGC 24-038 design. There is no support for writing
at this time.

This requires at least libheif 1.19.0.

Built hints on Windows
----------------------
Expand Down
Loading

0 comments on commit b914d9b

Please sign in to comment.