Bruker 2dseq reader: ParaVision on-disk format conformance - #6761
Bruker 2dseq reader: ParaVision on-disk format conformance#6761gdevenyi wants to merge 10 commits into
Conversation
|
This work is built on https://github.com/gdevenyi/brkraw-legacy/blob/main/FILE_FORMAT.md which was constructed using an extensive AI deep dive into publicly available Bruker datasets, the Bruker Paravision manuals over multiple versions. |
|
@greptileai review this. |
|
350c966 to
bc6224d
Compare
|
Reviewed the six commits against the merge base. The parser work is solid — the RLE bound added in Please rebase on current if(NOT "ITKGoogleTest" IN_LIST ITK_MODULE_${itk-module}-Test_DEPENDS)
message(FATAL_ERROR "${itk-module} builds a GoogleTest driver: add ITKGoogleTest to its TEST_DEPENDS")
endif()This PR adds TEST_DEPENDS
ITKTestKernel
ITKIOMeta
+ ITKGoogleTestHardening checklistEvery item below is driven by a value read from
S1 — unvalidated array cardinality (pre-existing; please fix here)
const vnl_matrix<double> dirMatrix(&orient[0], 3, 3);
To be clear: this is not introduced by this PR — A checked accessor keeps it to one line per call site: // Cardinality comes from the file; verify before indexing.
std::vector<double>
GetParameterArray(const MetaDataDictionary & dict, const std::string & name, const SizeType minimumSize)
{
auto values = GetParameter<std::vector<double>>(dict, name);
if (values.size() < minimumSize)
{
itkGenericExceptionMacro("Bruker parameter " << name << " has " << values.size() << " values, need "
<< minimumSize);
}
return values;
}Then S2 —
|
|
Filed S1 (and the related S2-S5 input-validation gaps, all of which predate this PR) as #6813 against |
hjmjohnson
left a comment
There was a problem hiding this comment.
Please see the checklist of requested changes.
PV5.1 headers have two $$ lines, not three, which desynchronized the fixed-header parse. ParaVision 360 files add run-length encoded arrays (@n*(value)), $$ comments inside wrapped value blocks, commas inside <> strings, enum values stored as sized arrays, and scalar struct values on the parameter line. Parse records by their layout instead of assuming a fixed header and comma-splittable structs. Change-Id: I20430e5c667057d20920a9ad9b3c7a85163eb808
VisuCoreDataSlope and VisuCoreDataOffs may be absent, hold a single value applying to every frame, or hold one value per frame; indexing them per-frame read out of bounds when a single value was stored. Change-Id: I9ad4eebf2d9316a0557c93261cd9d2db6f178b36
2D datasets without an FG_SLICE frame group (FG_ISA parameter maps) are single slice; deriving the slice count from the identical per-frame positions gave a zero slice spacing. Orient the slice axis along the actual slice-position step so oblique and coronal stacks match their stored geometry. Change-Id: If0dce54d2d8ebd770e85801be8e9d889626521f5
Change-Id: I6caa1598d92f3a51dc4002520cb064aec3ae26bf
KWStyle reports "{ value };" initializers as an unnecessary
semicolon, failing ITKIOBrukerKWStyleTest.
Change-Id: Id2fa7f90374d2b9043413909640517530d524b2a
A crafted repetition count such as @2147483647*(2) in visu_pars drove a multi-GiB allocation while reading image information. Reject counts of more than nine digits and expansions past 64 MiB. Change-Id: I4c0c1e90045173c439febad47f9a40e225f87433
CreateGoogleTestDriver requires the module to list ITKGoogleTest in TEST_DEPENDS. Change-Id: I69efa152d5e2dc1d5e814e9ab433bf9b7cb4caa2
VisuCoreDataSlope and VisuCoreDataOffs are not frame-group dependent, so ParaVision writes no value, one value, or one per frame. Any other count is a corrupt file, not a broadcast. GTest output now goes to ITK_TEST_OUTPUT_DIR, which CTest cleans up. Change-Id: Ib0cb03a8b7e1030e5d874c4ef0c1fcd1013cfc94
Two frames of one slice give a position step whose direction is rounding noise. Requiring half the frame thickness keeps the slice axis unflipped unless a real inter-slice step was measured. Change-Id: I4107a7c1827db065be27f30c2e6f37f7642ed770
bc6224d to
7b28095
Compare
|
Rebased on current
S6 — why the strict check is safe
A parameter that does not depend on a frame group carries no value, one value, or The check sits in S7 — the guard, and why the sign is measured at allThe guard is yours, with one change. The reader takes the thickness through the existing On your wider point: you are right that the position progression cannot confirm a rule derived from Your guard narrows that measurement to steps large enough to mean something, which is the right I also checked the affected datasets against a separate reader. Sixty-seven datasets in a local
Please weigh that for what it is. The code is independent of ITK's, but it shares lineage with the Smaller notesDone:
Not done:
One note on the test fixture, because the file looks self-contradictory. It declares |
Change-Id: Ic151a5d7822cc391c82671722822f6c699254022 ImageIOBase::SizeType is signed, so comparing it against std::vector::size_type warns under -Wsign-compare.
Parse the JCAMP-DX forms ParaVision writes (PV5.1 headers, PV360 RLE and enum arrays, strings with commas), fix frame-scaling cardinality, and derive slice count and direction from frame groups. Adds synthetic PV360 GTests, no new external test data.
Defects fixed, per commit
##/three-$$header assumption went out of step on PV5.1 files, which write two$$lines, so the reader consumedVisuVersionas a header line. It could not read ParaVision 360 files at all:@N*(value)run-length encoded arrays,$$ @vis=comments inside wrapped value blocks, commas inside<>strings such as<Parameter maps T2 relaxation, bg: Otsu.>, enum values stored as sized arrays (( 1 )thendisk_normal_slice_order), and scalar struct values on the parameter line.VisuCoreDataSlopeandVisuCoreDataOffshold either one value for all frames or one value per frame. Per-frame indexing read out of bounds when the file stored a single value. Neither parameter can depend on a frame group, so the reader now rejects any other count instead of reusing element 0.FG_SLICE, such asFG_ISAparameter maps, hold a single slice. Taking the slice count from identical per-frame positions gave a zero slice spacing. The slice axis now follows the sign of the slice-position step along the orientation's third row, which generalizes the earlier coronal-only Y-component heuristic to oblique stacks. A step below half the frame thickness lies inside one slice and no longer flips the axis.Why the reader measures the slice axis instead of reading it from the orientation
The format notes linked in the first comment reconstruct
VisuCoreOrientationfromACQ_grad_matrixand theACQ_*_offsetvalues as[ −d_r ; −d_p ; ±d_s ]. The third row is the right-handed completion of the first two, not the direction in which the slice offsets grow. It therefore carries either sign, and the slice order has to come fromVisuCorePosition.VisuCoreDiskSliceOrdercannot supply it either. ParaVision writes that parameter for 3D frames, not for the 2D multi-slice case this code path handles.Test results
itkBruker2dseq_PV5.1_FSE_INT16andPV6.0_FLASH_*regression tests pass against unchanged baselines.Bruker2dseqImageIOGTests cover RLE arrays, wrapped strings with embedded commas, mid-value comments, broadcast scaling, frame-group reordering, a reversed slice axis, an out-of-range scaling count, an oversized and an over-long RLE count, and a slice step too small to carry a direction.AI assistance