Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions dali/operators/imgcodec/image_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@
// limitations under the License.

#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#if LIBTIFF_ENABLED
#include <tiffio.h>
#endif
#include "dali/core/call_at_exit.h"
#include "dali/core/mm/memory.h"
#include "dali/operators.h"
Expand Down Expand Up @@ -46,6 +53,51 @@ nvimgcodecStatus_t get_nvjpeg2k_extension_desc(nvimgcodecExtensionDesc_t *ext_de
namespace dali {
namespace imgcodec {

#if LIBTIFF_ENABLED

// GeoTIFF-specific TIFF tag IDs that libtiff does not recognize natively.
// When decoding GeoTIFF files, libtiff emits "Unknown field with tag X" warnings for these tags.
// The image data is decoded correctly; the tags only carry geographic metadata.
constexpr uint32_t kGeoTIFFTags[] = {
33550, // ModelPixelScaleTag
33922, // ModelTiepointTag
34264, // ModelTransformationTag
34735, // GeoKeyDirectoryTag
34736, // GeoDoubleParamsTag
34737, // GeoAsciiParamsTag
42112, // GDAL_METADATA
42113, // GDAL_NODATA
};

inline void SuppressGeoTIFFTagWarnings(const char *module, const char *fmt, va_list ap) {
if (strstr(fmt, "Unknown field with tag") != nullptr) {
va_list ap_copy;
va_copy(ap_copy, ap);
unsigned int tag = va_arg(ap_copy, unsigned int);
va_end(ap_copy);
for (auto geotiff_tag : kGeoTIFFTags) {
if (tag == geotiff_tag)
return;
}
}
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
// libtiff permits a null module name in some code paths
if (module)
std::cerr << module << ": " << buf << "\n";
else
std::cerr << buf << "\n";
}

// Declared inline so the static once_flag is shared across all translation units,
// guaranteeing TIFFSetWarningHandler is called exactly once per process.
inline void InstallGeoTIFFWarningFilter() {
static std::once_flag flag;
std::call_once(flag, [] { TIFFSetWarningHandler(SuppressGeoTIFFTagWarnings); });
}

#endif // LIBTIFF_ENABLED

template <typename Backend>
struct OutBackend {
using type = GPUBackend;
Expand Down Expand Up @@ -219,6 +271,9 @@ class ImageDecoder : public StatelessOperator<Backend> {


explicit ImageDecoder(const OpSpec &spec) : StatelessOperator<Backend>(spec) {
#if LIBTIFF_ENABLED
InstallGeoTIFFWarningFilter();
#endif
device_id_ = std::is_same<CPUBackend, Backend>::value ? CPU_ONLY_DEVICE_ID :
spec.GetArgument<int>("device_id");
format_ = spec.GetArgument<DALIImageType>("output_type");
Expand Down
107 changes: 105 additions & 2 deletions dali/test/python/decoder/test_image.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,15 +19,16 @@
import nvidia.dali.types as types
import os
import random
import struct
import tempfile
from nvidia.dali import pipeline_def

from nose2.tools import params
from nose_utils import assert_raises, SkipTest
from test_utils import compare_pipelines
from test_utils import get_dali_extra_path
from test_utils import to_array
from test_utils import get_nvjpeg_ver
from test_utils import to_array


def get_img_files(data_path, subdir="*", ext=None):
Expand Down Expand Up @@ -491,6 +492,108 @@ def pipe():
assert np.quantile(delta, 0.9) < 0.05, "Original and palette TIFF differ significantly"


def _create_geotiff(path, width=4, height=4):
"""Build a minimal GeoTIFF file from scratch with the five standard GeoTIFF metadata tags.

Returns the expected pixel array (HxW uint8) so callers can compare against decoded output.
The file is constructed with struct so no third-party TIFF library is required.
"""
# Image: row-major, values 0..(H*W-1), clipped to uint8
image_data = bytes(i % 256 for i in range(width * height))

# Extra tag payloads (stored after the IFD)
model_pixel_scale = struct.pack("<3d", 1.0, 1.0, 0.0) # ModelPixelScaleTag (3 doubles)
model_tiepoint = struct.pack("<6d", *([0.0] * 6)) # ModelTiepointTag (6 doubles)
geo_key_dir = struct.pack("<4H", 1, 1, 0, 0) # GeoKeyDirectoryTag (4 shorts)
geo_double = struct.pack("<d", 0.0) # GeoDoubleParamsTag (1 double)
geo_ascii = b"WGS 84|\x00" # GeoAsciiParamsTag (ASCII)

# Compute layout offsets
# 0..7: header
# 8..23: image data (16 bytes for 4x4)
# 24..: IFD
num_entries = 14
image_offset = 8
ifd_offset = image_offset + len(image_data)
entries_offset = ifd_offset + 2 # skip num_entries field
extra_start = entries_offset + num_entries * 12 + 4 # after entries + next-IFD pointer

off_mps = extra_start
off_mt = off_mps + len(model_pixel_scale)
off_gkd = off_mt + len(model_tiepoint)
off_gdp = off_gkd + len(geo_key_dir)
off_gas = off_gdp + len(geo_double)

SHORT, LONG, DOUBLE, ASCII = 3, 4, 12, 2

def ifd_entry(tag, ttype, count, val):
return struct.pack("<HHII", tag, ttype, count, val)

entries = b"".join(
[
ifd_entry(256, SHORT, 1, width), # ImageWidth
ifd_entry(257, SHORT, 1, height), # ImageLength
ifd_entry(258, SHORT, 1, 8), # BitsPerSample
ifd_entry(259, SHORT, 1, 1), # Compression = None
ifd_entry(262, SHORT, 1, 1), # PhotometricInterpretation
ifd_entry(273, LONG, 1, image_offset), # StripOffsets
ifd_entry(277, SHORT, 1, 1), # SamplesPerPixel
ifd_entry(278, SHORT, 1, height), # RowsPerStrip
ifd_entry(279, LONG, 1, len(image_data)), # StripByteCounts
ifd_entry(33550, DOUBLE, 3, off_mps), # ModelPixelScaleTag
ifd_entry(33922, DOUBLE, 6, off_mt), # ModelTiepointTag
ifd_entry(34735, SHORT, 4, off_gkd), # GeoKeyDirectoryTag
ifd_entry(34736, DOUBLE, 1, off_gdp), # GeoDoubleParamsTag
ifd_entry(34737, ASCII, len(geo_ascii), off_gas), # GeoAsciiParamsTag
]
)
assert len(entries) == num_entries * 12

tiff_bytes = (
struct.pack("<HHI", 0x4949, 42, ifd_offset) # header: LE + magic + IFD offset
+ image_data
+ struct.pack("<H", num_entries)
+ entries
+ struct.pack("<I", 0) # next IFD = none
+ model_pixel_scale
+ model_tiepoint
+ geo_key_dir
+ geo_double
+ geo_ascii
)
with open(path, "wb") as f:
f.write(tiff_bytes)

return np.frombuffer(image_data, dtype=np.uint8).reshape(height, width)


@params("cpu", "mixed")
def test_image_decoder_geotiff(device):
"""GeoTIFF files with standard geographic metadata tags must decode without errors.

GeoTIFF tags (ModelPixelScale, ModelTiepoint, GeoKeyDirectory, GeoDoubleParams,
GeoAsciiParams) are not natively known to libtiff, which emits "Unknown field with tag"
warnings for them. The fix installs a custom libtiff warning handler that suppresses
those specific warnings while passing through all others.
"""
with tempfile.TemporaryDirectory() as tmpdir:
geo_path = os.path.join(tmpdir, "geo.tif")
expected = _create_geotiff(geo_path)

@pipeline_def(batch_size=1, device_id=0, num_threads=1)
def geo_pipe(files):
encoded, _ = fn.readers.file(files=files)
decoded = fn.decoders.image(encoded, device=device, output_type=types.ANY_DATA)
return decoded

p = geo_pipe(files=[geo_path])
out = p.run()[0]
if device == "mixed":
out = out.as_cpu()
result = np.array(out[0]).squeeze()
np.testing.assert_array_equal(result, expected)


def test_image_decoder_crafted_tiny_files():
with tempfile.TemporaryDirectory() as tmpdir:
tiny_file_path = os.path.join(tmpdir, "tiny.img")
Expand Down