Skip to content

ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries.#1585

Open
imikejackson wants to merge 16 commits intoBlueQuartzSoftware:developfrom
imikejackson:topic/image_io_rewrite
Open

ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries.#1585
imikejackson wants to merge 16 commits intoBlueQuartzSoftware:developfrom
imikejackson:topic/image_io_rewrite

Conversation

@imikejackson
Copy link
Copy Markdown
Contributor

Introduces three new SimplnxCore image I/O filters (ReadImageFilter, WriteImageFilter, ReadImageStackFilter) backed by a new format-agnostic IImageIO
abstraction layer, fixes a long-standing multi-component conversion bug in the legacy ITK reader, and consolidates the test data archives for both filter
families into two versioned _v3 archives.

New SimplnxCore filters

  • ReadImageFilter, WriteImageFilter, ReadImageStackFilter — full feature parity with their ITKImageReaderFilter / ITKImageWriterFilter /
    ITKImportImageStackFilter counterparts (origin/spacing overrides with pre/post-processed timing, voxel and physical cropping, data type conversion, XY/XZ/YZ
    plane slicing on write, flip transforms, grayscale conversion, resampling).

New IImageIO abstraction layer (simplnx/Utilities/ImageIO/)

  • Strategy pattern with StbImageIO (PNG/JPEG/BMP via stb) and TiffImageIO (TIFF via libtiff) backends selected by a CreateImageIO() extension-based factory.
  • Out-of-core safe: data is copied tuple-by-tuple into AbstractDataStore via operator[] — no raw pointers to DataArray internals. Write temp buffers are
    never larger than a single 2D slice.
  • Error messages from the underlying libraries are captured and returned through Result. libtiff errors are captured via a thread-local handler.

Upstream ITK reader bug fix

  • ITKImageProcessing/Common/ReadImageUtils.hpp — ConvertImageToDataStoreAsType() was iterating only pixelContainer->Size() elements, which is the pixel
    count, not the scalar element count. For multi-component images (e.g. RGB itk::Vector<uint8, 3>) this left two-thirds of the destination uninitialized. Fixed
    by multiplying by itk::NumericTraits::GetLength().

@imikejackson imikejackson requested a review from JDuffeyBQ April 10, 2026 16:24
imikejackson and others added 16 commits April 10, 2026 14:55
These new filters use libTiff and Stb libraries to read and write images.
The main utility classes IImageIO is placed into the simplnx library itself so any code can utilize it.
…skeleton

The skeleton references cxItkImageReaderFilter::OriginSpacingProcessingTiming
which is an ITK plugin type not available in SimplnxCore. Commented out until
ReadImageStackFilter implementation replaces it with a local enum.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a strategy-pattern image I/O layer under Utilities/ImageIO/ that
decouples image file reading/writing from simplnx data structures. Includes
ImageMetadata struct, IImageIO abstract interface, factory function for
backend selection by extension, and two concrete backends: StbImageIO
(PNG/JPEG/BMP via stb_image/stb_image_write) and TiffImageIO (TIFF via
libtiff with scanline and tiled image support).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add IImageIO, ImageMetadata, ImageIOFactory, StbImageIO, TiffImageIO
  to SIMPLNX_HDRS and SIMPLNX_SRCS in root CMakeLists.txt
- Move STB_IMAGE_IMPLEMENTATION from ReadImageFilter.cpp to StbImageIO.cpp
- Use uint16_t/uint32_t/int32_t for TIFF-interacting variables to avoid
  collision with libtiff's deprecated typedef names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReadImage algorithm reads single 2D images via IImageIO into DataArrays.
ReadImageFilter provides preflight with geometry/array creation and
origin/spacing override support. Data type conversion with ratio scaling.
Removes direct Stb/TIFF dependencies from SimplnxCore CMakeLists since
those are now handled by the ImageIO abstraction layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WriteImage extracts 2D slices along XY/XZ/YZ planes from 3D
ImageGeometry and writes individual image files via IImageIO.
Uses AtomicFile for safe writes. Temp buffers are one slice only.
WriteImageFilter validates parameters and delegates to algorithm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reads numbered image sequences into 3D ImageGeometry via IImageIO.
Uses ReadImageFilter as sub-filter for per-slice reading. Supports
resampling, grayscale conversion, flip transforms, origin/spacing
overrides, and Z-slice cropping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests mirror the ITK test patterns for ReadImageFilter, WriteImageFilter,
and ReadImageStackFilter. Use same test data archives as the ITK tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConvertImageToDataStoreAsType in ReadImageUtils.hpp was only converting
the first channel of multi-component (vector) images (e.g. RGB, RGBA)
because std::transform was bounded by pixelContainer->Size(), which
returns the number of pixels rather than the number of underlying
scalar elements. For an N-component image this caused (N-1)/N of the
destination buffer to be left uninitialized (zeros).

Multiply pixelContainer->Size() by the number of components per pixel
(derived from sizeof(PixelT)/sizeof(T)) so all channels are scaled.

The itk_image_reader_test exemplar archive was regenerated with the
correct conversion for the DataType_Conversion,
Interaction_Crop_DataType, and Interaction_Crop_OriginSpacing_Preprocessed_DataType
datasets. It is now published as itk_image_reader_test_v2.tar.gz with
an updated SHA512. Both ITKImageReaderTest and ReadImageTest reference
the v2 archive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Applies voxel and physical bounds cropping to 2D images. Uses
CropImageGeomFilter in preflight to compute cropped dimensions,
origin, and spacing. Execute extracts the cropped subvolume from
the temp buffer before copying to DataStore. Respects pre/post-
processed timing for origin/spacing overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ReadImageStackFilter preflight creates the resampled image geometry at
a temporary "_resampled" path in the main DataStructure and renames it back
to the original path via a deferred action that runs AFTER executeImpl
completes. Similarly, when grayscale conversion is enabled, the preflight
creates the output array with a "grayscale_" prefix and renames it back via
a deferred action.

The algorithm was writing to the ORIGINAL (un-resampled, un-prefixed) paths
during execute, which pointed to stale arrays that still had the original
un-resampled dimensions and/or RGB component count. This caused dimension
mismatch errors (resample tests) and component count mismatch errors
(grayscale tests) when copying slice data into the destination.

Match the ITK version of ReadImageStack by updating destImageGeomPath to
the "_resampled" path after running the resample sub-filter, and by using
the "grayscale_" prefixed array name for the destination path when
grayscale conversion is requested.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConvertImageToDataStoreAsType in ReadImageUtils.hpp was only converting
the first 1/N of the scalar buffer for multi-component (vector) images
like RGB or RGBA. The std::transform range used pixelContainer->Size(),
which returns the number of pixels in the container, not the number of
underlying scalar elements. For an itk::Vector<uint8, 3> image, Size()
returned width*height while the raw buffer cast to uint8* actually held
width*height*3 elements, leaving two-thirds of the destination buffer
uninitialized.

Multiply by itk::NumericTraits<PixelT>::GetLength() (the canonical ITK
component count) so every scalar in the buffer is converted.

This breaks the DataType_Conversion, Interaction_Crop_DataType, and
Interaction_All test exemplars in itk_image_reader_test.tar.gz, which
were generated by the buggy code. The exemplar archive must be
regenerated before those tests will pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change TestFileSentinel construction in all ten ITKImageReaderFilter
test cases to pass decompressFiles=false and removeTemp=false. This
matches the pattern already used by the new SimplnxCore ReadImageFilter
tests: the sentinel no longer decompresses the archive on construction
(which would overwrite a freshly-regenerated exemplar with the stale
archive contents) and no longer deletes the extracted directory on
teardown. This allows the regeneration pipeline and both test suites
to iterate against the same exemplar without interference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@imikejackson imikejackson force-pushed the topic/image_io_rewrite branch from 40317cd to 3724e79 Compare April 10, 2026 18:55
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.

1 participant