Skip to content
Merged
49 changes: 49 additions & 0 deletions Documentation/docs/migration_guides/itk_6_migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,52 @@ variable declared as `FilterType::OutputIterator` and then passed where an
siblings in the iterator hierarchy, not subclasses, so such uses do not convert
implicitly. Code that only iterates (`GoToBegin`/`IsAtEnd`/`Get`/`Set`) needs no
change.

## `GDCMSeriesFileNames` reimplemented on `gdcm::Scanner`/`IPPSorter`

`itk::GDCMSeriesFileNames` no longer uses the GDCM-deprecated
`gdcm::SerieHelper`. It now enumerates with `gdcm::Directory`, groups series
with `gdcm::Scanner` (replicating `SerieHelper::CreateUniqueSeriesIdentifier`),
and orders slices geometrically with `gdcm::IPPSorter`. The public API is
unchanged; consumers compile without modification.

### What you need to do

- Nothing for the common case: single-series directories enumerate, group, and
order as before.
- If you relied on file ordering for **duplicate-`ImagePositionPatient`** or
**gantry-tilted** acquisitions, re-check it. `gdcm::IPPSorter` is strict and
fails to sort those; on failure an exception is thrown by default. Call
`SetFailOnAmbiguousOrdering(false)` to instead accept the legacy
`SerieHelper` heuristics (Instance Number when unique, else lexicographic
filename order) — a non-standards-conforming fallback retained only for
determinism and backward compatibility; do not trust its output.
First-class support is tracked in
[#6468](https://github.com/InsightSoftwareConsortium/ITK/issues/6468).
- Tags passed to `AddSeriesRestriction` now refine the series identifier
(sub-dividing a `SeriesInstanceUID`, the documented intent, e.g.
`"0008|0021"`) instead of `SerieHelper`'s largely-inert file-restriction
list. Malformed tags are warned-and-ignored rather than honored.
- `SetLoadSequences`/`SetLoadPrivateTags` have no effect with the
`gdcm::Scanner` backend (the scan reads only the grouping/ordering tags).
They are retained for source compatibility but no longer alter enumeration.
- Non-image DICOM objects (structured reports, RTSTRUCT, DICOMDIR,
presentation states — anything without Rows `(0028,0010)`) are excluded
from enumeration, matching `SerieHelper`'s `ImageReader`-based acceptance.
- `GetUseSeriesDetails()` now defaults to `false`, consistent with the
documented opt-in ("you may want to try calling `SetUseSeriesDetails(true)`")
and with DICOM series identity: by default series are grouped by their raw
`SeriesInstanceUID` `(0020,000e)`, and `GetFileNames(<raw UID>)` matches.
Grouping behavior is unchanged — the previous backend never applied the
detail tags until `SetUseSeriesDetails(true)` was called, even though the
getter misreported `true`. Only code that read `GetUseSeriesDetails()`
before calling the setter sees a different (now truthful) value.

### Concerns

- Repeated `GetSeriesUIDs`/`GetFileNames`/`GetInputFileNames` calls no longer
re-scan the directory (lazy parse cached by a `TimeStamp`); a directory
mutated on disk between calls without an intervening `Modified()` is not
re-read.
- The vendored `gdcm::SerieHelper` is untouched (GDCM still uses it
internally); it may be removed once upstream GDCM drops it.
83 changes: 62 additions & 21 deletions Modules/IO/GDCM/include/itkGDCMSeriesFileNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,11 @@
#include "itkProcessObject.h"
#include "itkObjectFactory.h"
#include "itkMacro.h"
#include <map>
#include <utility>
#include <vector>
#include "ITKIOGDCMExport.h"

// forward declaration, to remove compile dependency on GDCM library
namespace gdcm
{
class SerieHelper;
}

namespace itk
{
/**
Expand All @@ -42,10 +38,11 @@ namespace itk
*
* 1. Extract Image Orientation & Image Position from DICOM images, and then
* calculate the ordering based on the 3D coordinate of the slice.
* 2. If for some reason this information is not found or failed, another
* strategy is used: the ordering is based on 'Instance Number'.
* 3. If this strategy also failed, then the filenames are ordered by
* lexicographical order.
* 2. If the geometric ordering fails (duplicate positions, inconsistent
* orientation), an exception is thrown by default; see
* FailOnAmbiguousOrdering. When FailOnAmbiguousOrdering is false, the
* ordering falls back to 'Instance Number' when unique, else to
* lexicographic filename order.
*
* If multiple volumes are being grouped as a single series for your
* DICOM objects, you may want to try calling SetUseSeriesDetails(true)
Expand Down Expand Up @@ -148,7 +145,9 @@ class ITKIOGDCM_EXPORT GDCMSeriesFileNames : public ProcessObject

/** Use additional series information such as ProtocolName
* and SeriesName to identify when a single SeriesUID contains
* multiple 3D volumes - as can occur with perfusion and DTI imaging
* multiple 3D volumes - as can occur with perfusion and DTI imaging.
* Off by default: series are identified by their SeriesInstanceUID
* (0020,000e) alone, per the DICOM information model.
*/
void
SetUseSeriesDetails(bool useSeriesDetails);
Expand All @@ -166,25 +165,38 @@ class ITKIOGDCM_EXPORT GDCMSeriesFileNames : public ProcessObject
/** Add more restriction on the selection of a Series. This follows the same
* approach as SetUseSeriesDetails, but allows a user to add even more DICOM
* tags to take into account for subrefining a set of DICOM files into multiple
* series. The tag format is "group|element" of a DICOM tag.
* series. The tag format is "group|element" of a DICOM tag. Call order
* relative to SetUseSeriesDetails does not matter.
* \warning UseSeriesDetails needs to be set to true.
*/
void
AddSeriesRestriction(const std::string & tag);

/** Parse any sequences in the DICOM file. Defaults to false
* to skip sequences. This makes loading DICOM files faster when
* sequences are not needed.
/** Throw an exception when a series cannot be ordered geometrically by
* gdcm::IPPSorter (duplicate ImagePositionPatient, inconsistent
* orientation). When false, fall back to the legacy SerieHelper
* heuristics: Instance Number when unique, else lexicographic filename
* order. The fallback is not DICOM-standards conforming and its output
* should not be trusted; it exists only for backward compatibility. */
/** @ITKStartGrouping */
itkSetMacro(FailOnAmbiguousOrdering, bool);
itkGetConstMacro(FailOnAmbiguousOrdering, bool);
itkBooleanMacro(FailOnAmbiguousOrdering);
/** @ITKEndGrouping */

/** No effect with the gdcm::Scanner backend (retained for source
* compatibility). Series enumeration reads only the grouping and
* ordering tags, so sequences are never parsed during the scan.
*/
/** @ITKStartGrouping */
itkSetMacro(LoadSequences, bool);
itkGetConstMacro(LoadSequences, bool);
itkBooleanMacro(LoadSequences);
/** @ITKEndGrouping */

/** Parse any private tags in the DICOM file. Defaults to false
* to skip private tags. This makes loading DICOM files faster when
* private tags are not needed.
/** No effect with the gdcm::Scanner backend (retained for source
* compatibility). Series enumeration reads only the grouping and
* ordering tags, so private tags are never parsed during the scan.
*/
/** @ITKStartGrouping */
itkSetMacro(LoadPrivateTags, bool);
Expand All @@ -208,13 +220,42 @@ class ITKIOGDCM_EXPORT GDCMSeriesFileNames : public ProcessObject
FileNamesContainerType m_InputFileNames{};
FileNamesContainerType m_OutputFileNames{};

/** Internal structure to order series from one directory */
std::unique_ptr<gdcm::SerieHelper> m_SerieHelper;
/** Parse the input directory into the per-series file-name map. */
void
BuildSeriesMap();

/** Files of one series; ordered lazily on the first GetFileNames call. */
struct SeriesEntry
{
FileNamesContainerType Files{};
bool Ordered{ false };
};

/** Order one series geometrically, or apply the legacy fallback / throw
* per FailOnAmbiguousOrdering. */
void
OrderSeries(SeriesEntry & entry);

/** File names per distinct series identifier. */
std::map<std::string, SeriesEntry> m_SeriesFiles{};

/** Instance Number (0020,0013) per scanned file, for the legacy fallback. */
std::map<std::string, std::string> m_InstanceNumbers{};

/** User (group,element) tags from AddSeriesRestriction, appended to the
* series identifier (after the default detail tags) when UseSeriesDetails
* is enabled. */
std::vector<std::pair<unsigned short, unsigned short>> m_UserRefineTags{};

/** Modified time of the last directory parse; the cache is rebuilt only
* when the object has been Modified() since. */
TimeStamp m_CacheBuildTime{};

/** Internal structure to keep the list of series UIDs */
SeriesUIDContainerType m_SeriesUIDs{};

bool m_UseSeriesDetails = true;
bool m_UseSeriesDetails = false;
bool m_FailOnAmbiguousOrdering = true;
bool m_Recursive = false;
bool m_LoadSequences = false;
bool m_LoadPrivateTags = false;
Expand Down
Loading
Loading