Skip to content

Commit 7f60df0

Browse files
Merge pull request #1324 from KrisThielemans/GEfixes
TOF fixes, mostly for GE HDF5 list-mode
2 parents 151c457 + ee2bc9a commit 7f60df0

24 files changed

+354
-228
lines changed

documentation/release_6.0.htm

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ <h2> Summary for end users (also to be read by developers)</h2>
4545

4646
<h3>Changes breaking backwards compatibility from a user-perspective</h3>
4747
<ul>
48-
<li> </li>
48+
<li>
49+
When parsing Interfile headers for projection data and the <tt>originating system</tt>
50+
is not recognised, the previous version of STIR tried to guess the scanner based on the
51+
number of views or rings. This was using very old scanners though, and could lead to
52+
confusion. These guesses have now been removed.
53+
</li>
4954
</ul>
5055

5156
<h3>Bug fixes</h3>
@@ -99,6 +104,28 @@ <h4>General</h4>
99104
</li>
100105
</ul>
101106
</li>
107+
<li>
108+
<ul>Interfile headers now use use the following keywords:
109+
<pre>
110+
number of radionuclides := 1
111+
radionuclide name[1] := ...
112+
radionuclide halflife (sec)[1] := ...
113+
radionuclide branching factor[1] := ...
114+
</pre>
115+
Previous versions of STIR used <tt>isotope name</tt>. This is still recognised
116+
if <tt>radionuclide name[1]</tt> is not present. Note that
117+
neither versions are confirming to the (very old) Interfile 4.0 proposal.
118+
</li>
119+
<li>
120+
Radionuclide information is read from Interfile and GE HDF5 headers.
121+
If the radionuclide name is recognised to the STIR database, its values for half-life etc
122+
are used, as opposed to what was recorded in the file (if anything).
123+
</li>
124+
<li>
125+
<tt>list_lm_events</tt> now has an additional option <tt>--event-bin</tt> which lists the bin
126+
assigned for the event (according to the "native" projection data, i.e. without any mashing).<br>
127+
In addition, the <tt>--event-LOR<tt> option now also works for SPECT (it was disabled by accident).
128+
</li>
102129
</ul>
103130

104131
<h4>Python (and MATLAB)</h4>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
!INTERFILE :=
2+
!imaging modality := PT
3+
name of data file := template.s
4+
originating system := GE Signa PET/MR
5+
!version of keys := STIR4.0
6+
!GENERAL DATA :=
7+
!GENERAL IMAGE DATA :=
8+
!type of data := PET
9+
imagedata byte order := LITTLEENDIAN
10+
!PET STUDY (General) :=
11+
!PET data type := Emission
12+
applied corrections := {None}
13+
!number format := float
14+
!number of bytes per pixel := 4
15+
number of dimensions := 5
16+
matrix axis label [5] := timing positions
17+
!matrix size [5] := 27
18+
matrix axis label [4] := segment
19+
!matrix size [4] := 45
20+
matrix axis label [3] := view
21+
!matrix size [3] := 224
22+
matrix axis label [2] := axial coordinate
23+
!matrix size [2] := { 1,5,9,13,17,21,25,29,33,37,41,45,49,53,57,61,65,69,73,77,81,85,89,85,81,77,73,69,65,61,57,53,49,45,41,37,33,29,25,21,17,13,9,5,1}
24+
matrix axis label [1] := tangential coordinate
25+
!matrix size [1] := 357
26+
27+
TOF mashing factor := 13
28+
minimum ring difference per segment := { -44,-43,-41,-39,-37,-35,-33,-31,-29,-27,-25,-23,-21,-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44}
29+
maximum ring difference per segment := { -44,-42,-40,-38,-36,-34,-32,-30,-28,-26,-24,-22,-20,-18,-16,-14,-12,-10,-8,-6,-4,-2,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,44}
30+
Scanner parameters:=
31+
Scanner type := GE Signa PET/MR
32+
Number of rings := 45
33+
Number of detectors per ring := 448
34+
Inner ring diameter (cm) := 62.36
35+
Average depth of interaction (cm) := 0.85
36+
Distance between rings (cm) := 0.556
37+
Default bin size (cm) := 0.201565
38+
View offset (degrees) := -5.23
39+
Maximum number of non-arc-corrected bins := 357
40+
Default number of arc-corrected bins := 331
41+
Energy resolution := 0.105
42+
Reference energy (in keV) := 511
43+
Number of blocks per bucket in transaxial direction := 4
44+
Number of blocks per bucket in axial direction := 5
45+
Number of crystals per block in axial direction := 9
46+
Number of crystals per block in transaxial direction := 4
47+
Number of detector layers := 1
48+
Number of crystals per singles unit in axial direction := 1
49+
Number of crystals per singles unit in transaxial direction := 1
50+
end scanner parameters:=
51+
effective central bin size (cm) := 0.224608
52+
number of time frames := 1
53+
start vertical bed position (mm) := 0
54+
start horizontal bed position (mm) := 0
55+
!END OF INTERFILE :=

examples/GE-Signa-PETMR/process_VQC_data.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ datadir=`cd "$datadir";pwd`
3737
mkdir output
3838
cd output
3939

40-
listmode="$datadir"/LST/LST_30501_PET_Scan_for_VQC_Verification/LIST0000uncompressed.BLF
40+
listmode="$datadir"/LST/LST_30501_PET_Scan_for_VQC_Verification/LIST0000.BLF
4141

4242
# make a frame definition file with 1 frame for all the data
4343
create_fdef_from_listmode.sh frames.fdef "$listmode"

src/IO/GEHDF5Wrapper.cxx

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
/*
1616
Copyright (C) 2017-2019, University of Leeds
1717
Copyright (C) 2018 University of Hull
18-
Copyright (C) 2018-2020, University College London
18+
Copyright (C) 2018-2021, 2024, University College London
1919
This file is part of STIR.
2020
2121
SPDX-License-Identifier: Apache-2.0
@@ -25,6 +25,8 @@
2525

2626
#include "stir/IO/GEHDF5Wrapper.h"
2727
#include "stir/IndexRange3D.h"
28+
#include "stir/Radionuclide.h"
29+
#include "stir/RadionuclideDB.h"
2830
#include "stir/is_null_ptr.h"
2931
#include "stir/info.h"
3032
#include "stir/error.h"
@@ -53,6 +55,15 @@ GEHDF5Wrapper::read_dataset_int32(const std::string& dataset_name)
5355
return tmp;
5456
}
5557

58+
59+
static float read_float(const H5::H5File& file, const std::string& dataset)
60+
{
61+
float tmp = 0.F;
62+
H5::DataSet ds = file.openDataSet(dataset.c_str());
63+
ds.read(&tmp, H5::PredType::NATIVE_FLOAT);
64+
return tmp;
65+
}
66+
5667
bool GEHDF5Wrapper::check_GE_signature(const std::string& filename)
5768
{
5869
try
@@ -89,34 +100,10 @@ bool GEHDF5Wrapper::check_GE_signature(H5::H5File& file)
89100
return false;
90101
}
91102

92-
// // Checks if input file is listfile.
93-
// AB: todo do we want this func? helps test from filename
94-
/*
95-
bool
96-
GEHDF5Wrapper::is_list_file(const std::string& filename)
97-
{
98-
99-
H5::H5File file;
100-
101-
if(!file.isHdf5(filename))
102-
error("File is not HDF5. Aborting");
103-
104-
file.openFile( filename, H5F_ACC_RDONLY );
105-
// All RDF files shoudl have this DataSet
106-
H5::DataSet dataset = file.openDataSet("/HeaderData/RDFConfiguration/isListFile");
107-
unsigned int is_list;
108-
dataset.read(&is_list, H5::PredType::STD_U32LE);
109-
return is_list;
110-
111-
}
112-
*/
113-
114-
115-
// AB todo: only valid for RDF9 (until they tell us otherwise)
116103
bool
117104
GEHDF5Wrapper::is_list_file() const
118105
{
119-
// have we already checked this?
106+
// have we already checked this? (note: initially set to false in check_file())
120107
if(is_list)
121108
return true;
122109

@@ -342,6 +329,13 @@ shared_ptr<Scanner> GEHDF5Wrapper::get_scanner_from_HDF5()
342329
str_radial_crystals_per_block.read(&num_transaxial_crystals_per_block, H5::PredType::NATIVE_UINT32);
343330
str_axial_crystals_per_block.read(&num_axial_crystals_per_block, H5::PredType::NATIVE_UINT32);
344331

332+
// TOF related
333+
const float timingResolutionInPico = read_float(file, "/HeaderData/SystemGeometry/timingResolutionInPico");
334+
const int posCoincidenceWindow = read_dataset_int32("/HeaderData/AcqParameters/EDCATParameters/posCoincidenceWindow");
335+
const int negCoincidenceWindow = read_dataset_int32("/HeaderData/AcqParameters/EDCATParameters/negCoincidenceWindow");
336+
const float coincTimingPrecisionInPico = read_float(file, "/HeaderData/AcqParameters/EDCATParameters/coincTimingPrecision") * 1000; // in nanoSecs in file
337+
const int num_tof_bins = posCoincidenceWindow + negCoincidenceWindow + 1;
338+
345339
int num_rings = num_axial_blocks_per_bucket*num_axial_crystals_per_block*axial_modules_per_system;
346340
int num_detectors_per_ring = num_transaxial_blocks_per_bucket*num_transaxial_crystals_per_block*radial_modules_per_system;
347341
float ring_spacing = detector_axial_size/num_rings;
@@ -375,6 +369,31 @@ shared_ptr<Scanner> GEHDF5Wrapper::get_scanner_from_HDF5()
375369
scanner_sptr->set_inner_ring_radius((effective_ring_diameter/2) - def_DOI);
376370
scanner_sptr->set_average_depth_of_interaction(def_DOI);
377371
}
372+
if (timingResolutionInPico > 0 // Signa files seem to have zero in this field
373+
&& (fabs(scanner_sptr->get_timing_resolution() - timingResolutionInPico) > .1F))
374+
{
375+
warning("GEHDF5Wrapper: default STIR timing resolution is "
376+
+ std::to_string(scanner_sptr->get_timing_resolution())
377+
+ ", while RDF says " + std::to_string(timingResolutionInPico)
378+
+ "\nWill adjust scanner info to fit with the RDF file");
379+
scanner_sptr->set_timing_resolution(timingResolutionInPico);
380+
}
381+
if (fabs(scanner_sptr->get_size_of_timing_pos() - coincTimingPrecisionInPico)> .1F)
382+
{
383+
warning("GEHDF5Wrapper: default STIR size of (unmashed) TOF bins is "
384+
+ std::to_string(scanner_sptr->get_size_of_timing_pos())
385+
+ ", while RDF says " + std::to_string(coincTimingPrecisionInPico)
386+
+ "\nWill adjust scanner info to fit with the RDF file");
387+
scanner_sptr->set_size_of_timing_poss(coincTimingPrecisionInPico);
388+
}
389+
if (std::abs(scanner_sptr->get_max_num_timing_poss() - num_tof_bins) > 0)
390+
{
391+
warning("GEHDF5Wrapper: default STIR number of (unmashed) TOF bins is "
392+
+ std::to_string(scanner_sptr->get_max_num_timing_poss())
393+
+ ", while RDF says " + std::to_string(num_tof_bins)
394+
+ "\nWill adjust scanner info to fit with the RDF file");
395+
scanner_sptr->set_max_num_timing_poss(num_tof_bins);
396+
}
378397
if (scanner_sptr->get_default_bin_size() <= 0.F)
379398
{
380399
warning("GEHDF5Wrapper: default bin-size is not set. This will create trouble for FBP etc");
@@ -401,7 +420,8 @@ void GEHDF5Wrapper::initialise_proj_data_info_from_HDF5()
401420
/* max_delta*/ scanner_sptr->get_num_rings()-1,
402421
/* num_views */ scanner_sptr->get_num_detectors_per_ring()/2,
403422
/* num_tangential_poss */ scanner_sptr->get_max_num_non_arccorrected_bins(),
404-
/* arc_corrected */ false
423+
/* arc_corrected */ false,
424+
this->is_list_file() ? 1 : 0 // TODO change when reading sinos as TOF
405425
);
406426
this->proj_data_info_sptr->
407427
set_bed_position_horizontal(this->read_dataset_int32("/HeaderData/AcqParameters/LandmarkParameters/absTableLongitude")/10.F); /* units in RDF are 0.1 mm */
@@ -475,6 +495,22 @@ void GEHDF5Wrapper::initialise_exam_info()
475495

476496
TimeFrameDefinitions tm(tf);
477497
exam_info_sptr->set_time_frame_definitions(tm);
498+
499+
// radionuclide
500+
{
501+
auto rn_name_ds = file.openDataSet("/HeaderData/ExamData/radionuclideName");
502+
H5::StrType str_type(rn_name_ds);
503+
std::string rn_name;
504+
rn_name_ds.read(rn_name, str_type);
505+
RadionuclideDB radionuclide_db;
506+
Radionuclide radionuclide = radionuclide_db.get_radionuclide(exam_info_sptr->imaging_modality,rn_name);
507+
508+
const float positron_fraction = read_float(file, "/HeaderData/ExamData/positronFraction");
509+
const float half_life = read_float(file, "/HeaderData/ExamData/halfLife");
510+
if (radionuclide.get_half_life(false) < 0)
511+
radionuclide = Radionuclide(rn_name, 511.F, positron_fraction, half_life, exam_info_sptr->imaging_modality);
512+
exam_info_sptr->set_radionuclide(radionuclide);
513+
}
478514
}
479515

480516
Succeeded GEHDF5Wrapper::initialise_listmode_data()
@@ -700,33 +736,6 @@ Succeeded GEHDF5Wrapper::initialise_efficiency_factors()
700736
return Succeeded::yes;
701737
}
702738

703-
float GEHDF5Wrapper::get_coincidence_time_window() const
704-
{
705-
if(!is_list_file() && !is_sino_file())
706-
error("The file provided is not list or sino data. Aborting");
707-
708-
H5::DataSet ds_coincTimingPrecision = file.openDataSet("/HeaderData/AcqParameters/EDCATParameters/coincTimingPrecision");
709-
H5::DataSet ds_posCoincidenceWindow = file.openDataSet("/HeaderData/AcqParameters/EDCATParameters/posCoincidenceWindow");
710-
float coincTimingPrecision = 0;
711-
int posCoincidenceWindow = 0;
712-
ds_coincTimingPrecision.read(&coincTimingPrecision,H5::PredType::NATIVE_FLOAT);
713-
ds_posCoincidenceWindow.read(&posCoincidenceWindow,H5::PredType::NATIVE_INT32);
714-
715-
return (2*posCoincidenceWindow+1) *coincTimingPrecision*1e-9;
716-
}
717-
718-
float GEHDF5Wrapper::get_halflife() const
719-
{
720-
if(!is_list_file() && !is_sino_file())
721-
error("The file provided is not list or sino data. Aborting");
722-
723-
H5::DataSet ds_halflife = file.openDataSet("/HeaderData/ExamData/halfLife");
724-
float halflife = 0;
725-
ds_halflife.read(&halflife,H5::PredType::NATIVE_FLOAT);
726-
return halflife;
727-
}
728-
729-
730739
// Developed for listmode access
731740
Succeeded GEHDF5Wrapper::read_list_data( char* output,
732741
const std::streampos offset, const hsize_t size) const

0 commit comments

Comments
 (0)