Skip to content

Suppress spurious libtiff warnings when decoding GeoTIFF files#6336

Closed
shreyaskommuri wants to merge 4 commits intoNVIDIA:mainfrom
shreyaskommuri:fix/geotiff-libtiff-warnings
Closed

Suppress spurious libtiff warnings when decoding GeoTIFF files#6336
shreyaskommuri wants to merge 4 commits intoNVIDIA:mainfrom
shreyaskommuri:fix/geotiff-libtiff-warnings

Conversation

@shreyaskommuri
Copy link
Copy Markdown

@shreyaskommuri shreyaskommuri commented May 5, 2026

Summary

Suppress the `TIFFReadDirectory: Warning, Unknown field with tag X encountered` warnings that libtiff emits when decoding GeoTIFF files. GeoTIFF embeds geographic metadata using five well-known tags that libtiff does not recognise natively — the image data is decoded correctly, but the warnings pollute stderr on every decode.

Related Issue

Closes #6114

Changes

  • Install a custom `TIFFErrorHandler` via `TIFFSetWarningHandler` in the `ImageDecoder` constructor (gated on `LIBTIFF_ENABLED`). The handler silently drops warnings for the eight known GeoTIFF/GDAL tags and forwards all other libtiff warnings to stderr unchanged.
  • Use `std::call_once` so the handler is installed at most once per process regardless of how many `ImageDecoder` instances are created.
  • Added `_create_geotiff()` test helper that builds a valid 4×4 GeoTIFF from scratch using only `struct` (no third-party dependency), embedding all five GeoTIFF tags from the issue report.
  • Added `test_image_decoder_geotiff()` that decodes the synthetic GeoTIFF with both `cpu` and `mixed` backends and asserts pixel values are correct.

Type of Change

  • Code change (bug fix)

Verification

  • `make lint`
  • `qa/TL0_python-self-test-operators_1/test.sh`

The tags suppressed are:

Tag ID Name
33550 ModelPixelScaleTag
33922 ModelTiepointTag
34264 ModelTransformationTag
34735 GeoKeyDirectoryTag
34736 GeoDoubleParamsTag
34737 GeoAsciiParamsTag
42112 GDAL_METADATA
42113 GDAL_NODATA

AI Disclosure

  • AI-assisted — tool: Claude Code

Signed-off-by: shreyaskommuri shreyaskommuri@gmail.com

GeoTIFF files embed geographic metadata in five TIFF tags
(ModelPixelScaleTag 33550, ModelTiepointTag 33922,
GeoKeyDirectoryTag 34735, GeoDoubleParamsTag 34736,
GeoAsciiParamsTag 34737) plus two common GDAL tags (42112, 42113).
libtiff does not know these tags natively and emits
"Unknown field with tag X encountered" warnings for each one.

Install a custom TIFFErrorHandler in the ImageDecoder constructor
(guarded by LIBTIFF_ENABLED) that silently drops warnings for these
known GeoTIFF/GDAL tags and forwards all other warnings to stderr
unchanged.  The handler is installed once per process via
std::call_once.

Also add test_image_decoder_geotiff which builds a minimal GeoTIFF
from scratch (no third-party dependency) and decodes it with both
CPU and mixed backends, asserting correct pixel values.

Fixes: NVIDIA#6114
Signed-off-by: shreyaskommuri <shreyaskommuri@gmail.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR suppresses spurious TIFFReadDirectory: Warning, Unknown field with tag X messages that libtiff emits when decoding GeoTIFF files by installing a custom TIFFWarningHandler in the ImageDecoder constructor, filtering eight well-known GeoTIFF/GDAL tag IDs and forwarding all other warnings to stderr unchanged.

  • C++ handler (image_decoder.h): An inline SuppressGeoTIFFTagWarnings function is registered once per process via std::call_once inside InstallGeoTIFFWarningFilter. The null-module guard is present and va_copy is used correctly so the original va_list is intact for the vsnprintf fallthrough path.
  • Python test (test_image.py): A self-contained _create_geotiff helper builds a valid 4×4 GeoTIFF with struct (no third-party TIFF dependency) and test_image_decoder_geotiff verifies pixel-level correctness for both cpu and mixed devices.

Confidence Score: 4/5

Safe to merge with minor test coverage gaps. The warning handler logic is correct; the synthetic TIFF test helper is spec-compliant.

The warning filter is functionally correct — va_copy preserves the original va_list, the null-module guard is in place, and the inline/std::call_once pattern correctly shares the static once_flag across translation units. The main gaps are that three of the eight suppressed tags are not exercised by the test fixture, and the strstr coupling to libtiff's internal format string is undocumented. Neither is blocking.

The test helper in dali/test/python/decoder/test_image.py should embed the three missing tags (34264, 42112, 42113) to give the suppression logic full coverage.

Important Files Changed

Filename Overview
dali/operators/imgcodec/image_decoder.h Adds an inline GeoTIFF warning filter installed via TIFFSetWarningHandler in the ImageDecoder constructor. The null-module guard and va_copy usage are correct; the strstr coupling to libtiff's internal format string is a minor fragility.
dali/test/python/decoder/test_image.py Adds _create_geotiff helper and test_image_decoder_geotiff test. The synthetic TIFF construction is TIFF-spec-correct. Three of the eight suppressed tags (34264, 42112, 42113) are absent from the test fixture, leaving those suppression paths untested.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[libtiff emits warning\nmodule, fmt, va_list ap] --> B{strstr fmt\n'Unknown field with tag'?}
    B -- No --> F[vsnprintf buf fmt ap\nwrite to stderr]
    B -- Yes --> C[va_copy ap → ap_copy\nva_arg ap_copy → tag id]
    C --> D{tag in kGeoTIFFTags?\n33550 33922 34264\n34735 34736 34737\n42112 42113}
    D -- Yes --> E[Return — warning suppressed]
    D -- No --> F
    F --> G[module != null?]
    G -- Yes --> H[cerr: module + buf]
    G -- No --> I[cerr: buf]
Loading

Reviews (5): Last reviewed commit: "Remove unused get_gpu_num import (fixes ..." | Re-trigger Greptile

Comment thread dali/operators/imgcodec/image_decoder.h Outdated
Comment on lines +84 to +86
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
std::cerr << module << ": " << buf << "\n";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [Bug] Null pointer undefined behaviour when module is null. The TIFFWarningHandler contract permits a null module argument — libtiff calls TIFFWarning(NULL, fmt, ...) in several places. Writing std::cerr << module with a null const char* is undefined behaviour that manifests as a crash or garbled output on most platforms. Guard before the fallthrough write.

Suggested change
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
std::cerr << module << ": " << buf << "\n";
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
if (module)
std::cerr << module << ": " << buf << "\n";
else
std::cerr << buf << "\n";

Comment thread dali/operators/imgcodec/image_decoder.h Outdated
Comment on lines +89 to +92
void InstallGeoTIFFWarningFilter() {
static std::once_flag flag;
std::call_once(flag, [] { TIFFSetWarningHandler(SuppressGeoTIFFTagWarnings); });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [Question] std::call_once guarantees "at most once per std::once_flag instance." Because InstallGeoTIFFWarningFilter lives in an anonymous namespace inside a header, each translation unit that includes this header gets its own copy of the function and its own static once_flag. If two .cc files include image_decoder.h, both will call TIFFSetWarningHandler (once per TU), overwriting each other's handler. The last-written handler wins and is still functionally correct (since all copies are identical), but the "install at most once per process" comment overstates the guarantee. Consider defining the flag and the call in a single .cc file with external linkage, or accepting the current behaviour and updating the comment.

Comment thread dali/test/python/decoder/test_image.py Outdated
Comment on lines +577 to +590
for device in ["cpu", "mixed"]:

@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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [Bug] The "mixed" device path requires CUDA and will fail (or hang) on CPU-only CI runners. The rest of the test file uses SkipTest (imported at the top) or nose_utils guards to gate GPU-dependent paths. Without a skip guard here, this test will produce a hard failure rather than a clean skip on CPU-only machines. Consider wrapping or splitting the "mixed" iteration with a GPU-availability check, for example using test_utils.get_gpu_num() or calling SkipTest when no CUDA device is present.

- Guard against null module name in SuppressGeoTIFFTagWarnings
  (libtiff permits a null module argument in some code paths)
- Clarify comment on once_flag scope: each TU gets its own copy
  because the function lives in an anonymous namespace in a header;
  behaviour is still correct since all copies install the same handler
- Skip the mixed-backend path in test_image_decoder_geotiff on
  CPU-only runners (guard with get_gpu_num() > 0)
- Update copyright year to 2026

Signed-off-by: shreyaskommuri <shreyaskommuri@gmail.com>
Comment thread dali/test/python/decoder/test_image.py Outdated
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.
"""
import struct
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be moved to the top of the file?

Comment thread dali/test/python/decoder/test_image.py Outdated

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
warnings for them. The fix installs a custom libtiff warning handler that suppresses
warnings for them. The fix installs a custom libtiff warning handler that suppresses

Comment thread dali/test/python/decoder/test_image.py Outdated
if get_gpu_num() > 0:
devices.append("mixed")

for device in devices:
Copy link
Copy Markdown
Contributor

@JanuszL JanuszL May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make it a test param (although it would create the test file for each param but it should not impose big overhead):

@params(["cpu", "gpu"])
def test_image_decoder_geotiff(device):

Comment thread dali/test/python/decoder/test_image.py Outdated
Comment on lines +578 to +580
devices = ["cpu"]
if get_gpu_num() > 0:
devices.append("mixed")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
devices = ["cpu"]
if get_gpu_num() > 0:
devices.append("mixed")
if devices == "cpu" and get_gpu_num() <= 0:
raise SkipTest()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I don't think test can run without GPU in general, so this check is not needed as other test will fail anyway.

@JanuszL JanuszL self-assigned this May 6, 2026
@JanuszL
Copy link
Copy Markdown
Contributor

JanuszL commented May 6, 2026

Hi @shreyaskommuri,

Thank you for your contribution! Please review my comments at your convenience. Also please run lint & black on touched files. It seems there are things to improve (see this and this).

- Remove anonymous namespace from image_decoder.h (cpplint build/namespaces)
- Mark SuppressGeoTIFFTagWarnings and InstallGeoTIFFWarningFilter inline,
  which also makes the once_flag truly process-wide across TUs
- Move import struct to top of test_image.py
- Apply black formatting to _create_geotiff
- Fix double-space in docstring
- Refactor test_image_decoder_geotiff to use @params

Signed-off-by: shreyaskommuri <shreyaskommuri@gmail.com>
@shreyaskommuri
Copy link
Copy Markdown
Author

Thanks for the review @JanuszL! I've addressed all the feedback in the latest commit:

  • Moved import struct to the top of the file
  • Fixed the double-space in the docstring
  • Refactored test_image_decoder_geotiff to use @params("cpu", "mixed") (used "mixed" to match the existing convention in this file)
  • Fixed the black formatting issues in _create_geotiff
  • Fixed the C++ lint error: removed the anonymous namespace and marked the two helper functions inline, which also makes the once_flag truly process-wide across translation units (no longer just per-TU)

Skipped the GPU skip-test guard per your note that it's not needed.

@shreyaskommuri
Copy link
Copy Markdown
Author

Fix flake8 F401: removed the unused get_gpu_num import that was left behind when the test was refactored to use @params instead of a manual GPU check. That was the only thing blocking the Python lint job.

The import was left over after refactoring the geotiff test to use @params
instead of a manual GPU check.

Signed-off-by: shreyaskommuri <shreyaskommuri@gmail.com>
@shreyaskommuri shreyaskommuri force-pushed the fix/geotiff-libtiff-warnings branch from 58e1ea3 to d619ac6 Compare May 6, 2026 17:21
@jantonguirao
Copy link
Copy Markdown
Contributor

Hi @shreyaskommuri — thank you so much for digging into this and putting together such a clean fix! The synthetic GeoTIFF helper and the targeted tag allowlist are great, and we really appreciate the careful write-up.

Before we merge, though, we'd like to ask you to redirect this contribution: the right home for the filter is nvImageCodec, not DALI.

The "Unknown field with tag X" warnings originate from libtiff, and libtiff is invoked from inside the nvImageCodec libtiff extension (extensions/libtiff/libtiff_decoder.cpp) — DALI doesn't call libtiff directly. Installing TIFFSetWarningHandler in DALI's ImageDecoder works, but it's a layering workaround: it only helps DALI users, leaves every other nvImageCodec consumer with the same noise, and routes a global libtiff hook through the wrong layer.

Putting it in the nvImageCodec libtiff extension fixes the warning at its source for everyone, and DALI picks it up automatically through its nvImageCodec dependency.

Would you be willing to open the PR against nvImageCodec instead? 👉 https://github.com/NVIDIA/nvImageCodec

The natural spot is extensions/libtiff/ — most likely the warning handler in error_handling.h and the one-time std::call_once install from libtiff_ext.cpp (LibtiffImgCodecsExtension::libtiffExtensionCreate). Your test approach (struct-built GeoTIFF + pixel comparison) translates directly to test/python/ against nvimgcodec.Decoder. If you submit it there, it would be considered for the next nvImageCodec release, after which DALI would inherit the fix on its next bump.

Thanks again for the contribution — looking forward to seeing this land in the right place!

@shreyaskommuri
Copy link
Copy Markdown
Author

Hi @jantonguirao — I've opened the fix in nvImageCodec as requested: NVIDIA/nvImageCodec#52

The implementation matches your suggestion exactly:

  • SuppressGeoTIFFTagWarnings + InstallGeoTIFFWarningFilter in extensions/libtiff/error_handling.h
  • Called via std::call_once from LibtiffImgCodecsExtension::libtiffExtensionCreate in libtiff_ext.cpp
  • Synthetic GeoTIFF test (all 8 tags, struct-only, pixel comparison) in test/python/test_decode_tiff.py

All review feedback from this PR (null-module guard, inline once_flag, missing tags 34264/42112/42113 in test, formatting) has been incorporated into the nvImageCodec port. Happy to close this PR once the nvImageCodec one lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for GeoTIFF images

3 participants