PR #1585 added a new format-agnostic image-IO subsystem (IImageIO + StbImageIO / TiffImageIO backends) and three new SimplnxCore filters (ReadImageFilter, WriteImageFilter, ReadImageStackFilter). Two automated review passes (cross-cutting code quality + design validation) flagged a number of items that were intentionally deferred from that PR to keep the diff focused and to land the merge sooner. This issue tracks them.
Each item links back to the relevant comment thread on #1585 for the original rationale.
Correctness / API discipline
Performance
Architecture / API
Defensive
Documentation
Source
PR #1585 added a new format-agnostic image-IO subsystem (
IImageIO+StbImageIO/TiffImageIObackends) and three new SimplnxCore filters (ReadImageFilter,WriteImageFilter,ReadImageStackFilter). Two automated review passes (cross-cutting code quality + design validation) flagged a number of items that were intentionally deferred from that PR to keep the diff focused and to land the merge sooner. This issue tracks them.Each item links back to the relevant comment thread on #1585 for the original rationale.
Correctness / API discipline
Replace
throw std::runtime_errorinGetDataTypeSizeand theImageIOUtilitieschoice helpers withResult<T>.simplnx/Common/TypesUtility.hpp(GetDataTypeSize) andsimplnx/Utilities/ImageIO/ImageIOUtilities.hpp(ChoiceToImageDataType,ImageDataTypeToChoice) currently throw on unexpected enum values. The codebase convention isResult<T>/MakeErrorResult. The throws are unreachable in practice today (callers come from validatedChoicesParameterindexes / knownDataTypeenum values), but a future caller from an unvalidated path would crash any pipeline runner that does not catch. Should be done as a coordinated pass that also touches the existingGetNumericTypeSizefor consistency.TiffOpenOptionsfirst-error vs last-error semantics.simplnx/Utilities/ImageIO/TiffImageIO.cpp— when libtiff produces multiple errors during a single operation, the per-handle error handler currently keeps only the last one. The first error is usually the most diagnostic ("unknown tag 700" → "cannot read scanline N" — the first one is the actual cause). Change to first-write-wins.Add a focused regression test for the ITK multi-component
ConvertImageToDataStoreAsTypefix.PR ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries. #1585 fixed a bug in
ITKImageProcessing/Common/ReadImageUtils.hppwhere multi-component pixel images were getting only their first 1/N scalars filled in (pixelContainer->Size()returns the pixel count, not the scalar element count). Add a test that reads a 3-channel RGB TIFF with type conversion touint16and asserts every channel of every pixel is populated, not just the first 1/3.Performance
In-core
memcpyfast path onCopyPixelDataFunctor/ExtractSliceFunctor.Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cppandWriteImage.cppcurrently walk every pixel throughAbstractDataStore::operator[]/setValue— a virtual call per scalar component. For a 4096×4096 RGBA image that is ~67M virtual calls per slice. When (a) no cropping, (b)srcType == destType, and (c) the destination is in-core, the whole row (or the whole image) can be a singlestd::memcpy. Depends on the planned bulkcopyIntoBuffer/copyFromBufferAPI onAbstractDataStore— file a separate issue for that API if it doesn't already exist.Tiled-TIFF chunked read.
simplnx/Utilities/ImageIO/TiffImageIO.cppreadPixelDatafalls back toTIFFReadRGBAImageOrientedfor tiled TIFFs and allocatesstd::vector<uint32_t>(width * height)(1 GiB for a 16384² image). Switch toTIFFReadRGBAStrip/TIFFReadRGBATileto read in chunks. At minimum, reject upfront with a clear error if the implied scratch buffer exceeds some threshold rather than failing insidestd::vector::vectorwithstd::bad_alloc.Per-slice sub-filter construction in
ReadImageStack.Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cppconstructs a freshReadImageFilter,Arguments, and scratchDataStructureper slice (and anotherResampleImageGeomFilter/ConvertColorToGrayScaleFilterper slice when those modes are on). For a 1000-slice stack that is 1000 sub-filter setups. Hoist theArgumentsoutside the slice loop and onlyinsertOrAssignthe per-slice file path.Skip per-slice double-preflight in
ReadImageStack.ReadImageFilter::executere-runs preflight, which itself opens the file to read metadata. Each slice file is therefore opened twice. Consider an algorithm-level entry point (ReadImage::operator()directly) the stack code can use without going through the filter wrapper.FlipAboutXAxisrow-swap viastd::swap_ranges.Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cppdoes element-by-element scalar swap withgetValue/operator[].FlipAboutYAxisalready uses iterator-based row buffer copies;FlipAboutXAxisshould swap whole rows viastd::swap_rangeson the iterators. Same algorithmic complexity, real constant-factor savings.Throttled progress messaging in slice loops.
Both
Algorithms/ReadImageStack.cppandAlgorithms/WriteImage.cppfmt::formata per-slice "Importing/Writing X/Y" message every iteration. For thousands of slices this is wasted work behind a probably-no-op handler. UseThrottledMessengerper theprogress-messagingskill.Streaming
ReadImageinstead of whole-image scratch buffer.Algorithms/ReadImage.cppallocatesstd::vector<uint8> tempBufferof one image's worth of bytes — bounded for typical inputs but a 65535×65535 RGBA float32 image is 64 GiB. Add a scanline-streaming code path that materializes one row at a time when the source format supports it (libtiff scanline reads do).IImageIO::beginStream() / writeRow() / endStream()extension.Algorithms/WriteImage.cppper-slice scratch buffer is correct and bounded today, but a streaming write API would let TIFF and other row-oriented formats skip the intermediate slice buffer entirely. Only worth doing if profiling shows the slice buffer matters.Architecture / API
Format registry instead of an
if/elsechain inCreateImageIO.simplnx/Utilities/ImageIO/ImageIOFactory.cpphard-codes the supported extension list inside anif/else. Same list is duplicated inWriteImageFilter::parameters()(currentlyExtensionListType{}— empty). Add astatic const std::map<std::string, std::function<std::unique_ptr<IImageIO>()>>plus aGetSupportedReadExtensions()/GetSupportedWriteExtensions()helper that the filter parameter UI can call. Important once the third backend lands.Populate
WriteImageFilterextension dropdown from the same source asCreateImageIO.Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cppdeclaresExtensionListType{}(empty) for the outputFileSystemPathParameter— the user can type any extension and onlypreflightImplvalidates. Tied to the previous registry item.Drop
SIMPLNXCORE_EXPORTfromBuildReadImageFilterArgs.Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hppexports a sub-filter argument helper across the DLL boundary. If only the algorithms call it, drop the export and put it in an anonymous namespace.Decouple
ReadImageStackalgorithm fromReadImageFilterheader.Algorithms/ReadImageStack.cppcurrently includesSimplnxCore/Filters/ReadImageFilter.hppto construct aReadImageFilterper slice. If we expose an algorithm-level entry point onReadImage(see the double-preflight item above), the algorithm file can depend only on data + IO and not on a sibling filter header.ConvertColorToGrayScaleFilterin-place mode.Algorithms/ReadImageStack.cppdoes an awkward "convert to grayscale → get array namedgray<original>→ delete the original → rename the gray one back" dance becauseConvertColorToGrayScaleFilterdoes not support in-place. Long-term fix is on the grayscale filter.Defensive
WriteImageFilter::preflightImplshould validatetotalDigits >= 1.Negative or zero
totalDigitsis currently passed straight toCreateIndexStringwhich throws anfmt::formatexception out of preflight. Bounds-check in preflight andMakeErrorResultinstead.fs::create_directoriesusestd::error_codeoverload.Algorithms/WriteImage.cppfs::create_directoriesfailure currently produces a generic "Error creating output directory" message. Thestd::error_codeoverload would let us surface the OS-level reason.Surface override conflicts in
ReadImageFilter::preflightImpl.When the file specifies an origin/spacing and the user also overrides one in the filter, emit a
PreflightValuewarning ("file specifies origin X but you are overriding to Y") so the conflict is visible up front. Currently silent.TIFFGetField(XPOSITION/YPOSITION)partial-presence.simplnx/Utilities/ImageIO/TiffImageIO.cppreadMetadatasetshasOrigin = trueif either X or Y position tag is present; if only X is present,yPositionstays0.0fand is silently written intometadata.origin[1]. Either require both, or comment the partial-presence behavior.Documentation
Filter docs need a "use this filter for these formats" section.
Both the new
ReadImageFilter/WriteImageFilter/ReadImageStackFilterand the existing ITK-based equivalents are now equally findable in the filter palette (after PR ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries. #1585 dropped the per-format tags from the ITK reader). Add a "Use this for PNG/JPEG/BMP/TIFF; use ITK for everything else" note to both filter pairs' documentation so users know which to reach for first.Replace placeholder doc comments on
ReadImageFilter.hppandReadImageStackFilter.hpp.Both
/** @brief This filter will .... */comments are placeholders. Replace with a one-sentence description of what the filter does.Source