PERF: Hoist constant tensor-basis SVD out of the per-voxel DTI loop (backport of #6482)#6483
Conversation
DiffusionTensor3DReconstructionImageFilter constructed a vnl_svd of the constant m_TensorBasis once per voxel (~16.7M times for a 256^3 DWI) to solve the Stejskal-Tanner equations. The pseudo-inverse (the dual tensor basis) is the same for every voxel, so precompute it once in BeforeThreadedGenerateData and apply it per voxel as a matrix-vector product. vnl_svd::solve(X) is pinverse()*X, so the reconstructed tensors are unchanged. Removing the per-voxel SVD also frees the loop of the thread-unsafe netlib dsvdc call, so the stale single-thread warning is dropped.
The existing DiffusionTensor3DReconstructionImageFilter test only exercises the multi-image gradient path (AddGradientImage). Add a test that reconstructs the same data through the single-VectorImage path (SetGradientImage) and verifies it produces identical tensors, covering the previously-untested code path.
|
| Filename | Overview |
|---|---|
| Modules/Filtering/DiffusionTensorImage/include/itkDiffusionTensor3DReconstructionImageFilter.hxx | Core change: SVD pseudo-inverse moved to BeforeThreadedGenerateData; both voxel-loop paths updated consistently; m_BMatrix transpose ordering preserved. |
| Modules/Filtering/DiffusionTensorImage/include/itkDiffusionTensor3DReconstructionImageFilter.h | Adds m_TensorBasisInverse as a vnl_matrix private member with a clear comment; no API or public interface changes. |
| Modules/Filtering/DiffusionTensorImage/test/itkDiffusionTensor3DReconstructionSingleImageTest.cxx | New test covering the SetGradientImage path; compares multi-image vs single-image output with a 1e-8 tolerance and an anyNonZero guard to catch degenerate zero output. |
| Modules/Filtering/DiffusionTensorImage/test/CMakeLists.txt | Registers the new test source and adds an itk_add_test entry; no data dependencies needed since the test generates synthetic in-memory data. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant ITK as ITK Pipeline
participant BTGD as BeforeThreadedGenerateData
participant TGD as DynamicThreadedGenerateData (N threads)
ITK->>BTGD: call once before threading
BTGD->>BTGD: build m_BMatrix from gradient dirs
BTGD->>BTGD: set m_TensorBasis
BTGD->>BTGD: "m_TensorBasisInverse = vnl_svd(m_TensorBasis).pinverse()"
BTGD->>BTGD: m_BMatrix.inplace_transpose()
BTGD-->>ITK: done
ITK->>TGD: spawn N threads
par Thread 1
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
and Thread 2
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
and Thread N
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
end
TGD-->>ITK: output tensor image
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant ITK as ITK Pipeline
participant BTGD as BeforeThreadedGenerateData
participant TGD as DynamicThreadedGenerateData (N threads)
ITK->>BTGD: call once before threading
BTGD->>BTGD: build m_BMatrix from gradient dirs
BTGD->>BTGD: set m_TensorBasis
BTGD->>BTGD: "m_TensorBasisInverse = vnl_svd(m_TensorBasis).pinverse()"
BTGD->>BTGD: m_BMatrix.inplace_transpose()
BTGD-->>ITK: done
ITK->>TGD: spawn N threads
par Thread 1
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
and Thread 2
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
and Thread N
TGD->>TGD: "D = m_TensorBasisInverse x (m_BMatrix x B)"
end
TGD-->>ITK: output tensor image
Reviews (1): Last reviewed commit: "ENH: Add single-image-path test for DTI ..." | Re-trigger Greptile
Backport of #6482 to release-5.4.
DiffusionTensor3DReconstructionImageFilterconstructed avnl_svdof the constantm_TensorBasisonce per voxel (~16.7M times for a 256³ DWI). The pseudo-inverse (the dual tensor basis) is identical for every voxel, so this precomputes it once inBeforeThreadedGenerateDataand applies it per voxel as a matrix-vector product.vnl_svd::solve(X)ispinverse()·X, so the reconstructed tensors are unchanged; removing the per-voxel SVD also frees the loop of the thread-unsafe netlibdsvdccall.Verification
The change is identical to #6482 (main), which is validated by
itkDiffusionTensor3DReconstructionImageFilterTestplus a mutation check (corrupting the precomputed pseudo-inverse makes that test fail). The release-5.4 filter is structurally identical; the edited filter compiles and type-checks clean against ITK 5.x, and release-5.4 CI runs the same test.