Skip to content

Commit 2ab134e

Browse files
authored
Merge pull request #42 from ArcanaFramework/fixed-get-dicom-tag
fixed get_dicom_tag to handle implicit VR
2 parents e95870b + adef968 commit 2ab134e

File tree

10 files changed

+298
-48
lines changed

10 files changed

+298
-48
lines changed

extras/fileformats/extras/medimage/raw.py

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,41 @@
22
from pathlib import Path
33
import pydicom
44
import sys
5+
from collections import Counter
56
from fileformats.core import SampleFileGenerator
67
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_listmode import (
78
get_data as get_pet_listmode_data,
89
)
910
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_countrate import (
1011
get_data as get_pet_countrate_data,
1112
)
13+
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_em_sino import (
14+
get_data as get_pet_sinogram_data,
15+
)
16+
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_calibration import (
17+
get_data as get_pet_calibration_data,
18+
)
19+
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_dynamics_sino import (
20+
get_data as get_pet_dynamics_sino_data,
21+
)
22+
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.pet_replay_param import (
23+
get_data as get_pet_replay_param_data,
24+
)
25+
from medimages4tests.dummy.raw.pet.siemens.biograph_vision.vr20b.petct_spl import (
26+
get_data as get_petct_spl_data,
27+
)
1228
from fileformats.core import extra_implementation, FileSet
1329
from fileformats.medimage.dicom import DicomImage
1430
from fileformats.medimage.raw import (
31+
Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData,
1532
Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData,
16-
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
1733
Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
18-
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData,
34+
Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
35+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
36+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
37+
Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
38+
Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
39+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
1940
)
2041
from fileformats.core.io import BinaryIOWindow
2142

@@ -49,9 +70,26 @@ def siemens_pet_raw_data_read_metadata(
4970
return DicomImage.pydicom_to_dict(dcm)
5071

5172

73+
@extra_implementation(Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData.load_pydicom)
74+
def siemens_pet_raw_data_load_pydicom(
75+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData,
76+
specific_tags: ty.Optional[TagListType] = None,
77+
**kwargs: ty.Any,
78+
) -> pydicom.Dataset:
79+
80+
with pet_raw_data.open() as f:
81+
window = BinaryIOWindow(
82+
f, # type: ignore[arg-type]
83+
pet_raw_data.dicom_header_offset,
84+
pet_raw_data.dcm_hdr_size_int_offset,
85+
)
86+
dcm = pydicom.dcmread(window, specific_tags=specific_tags)
87+
return dcm
88+
89+
5290
@extra_implementation(FileSet.read_metadata)
5391
def siemens_petct_raw_data_read_metadata(
54-
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData,
92+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
5593
specific_tags: ty.Optional[TagListType] = None,
5694
**kwargs: ty.Any,
5795
) -> ty.Mapping[str, ty.Any]:
@@ -65,6 +103,59 @@ def siemens_petct_raw_data_read_metadata(
65103
return DicomImage.pydicom_to_dict(dcm)
66104

67105

106+
@extra_implementation(Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData.load_pydicom)
107+
def siemens_petct_raw_data_load_pydicom(
108+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
109+
specific_tags: ty.Optional[TagListType] = None,
110+
**kwargs: ty.Any,
111+
) -> pydicom.Dataset:
112+
113+
with pet_raw_data.open() as f:
114+
window = BinaryIOWindow(
115+
f, # type: ignore[arg-type]
116+
*pet_raw_data.dicom_header_limits,
117+
)
118+
dcm = pydicom.dcmread(window, specific_tags=specific_tags)
119+
return dcm
120+
121+
122+
@extra_implementation(FileSet.read_metadata)
123+
def siemens_pet_dynamic_sinogram_series_read_metadata(
124+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
125+
specific_tags: ty.Optional[TagListType] = None,
126+
**kwargs: ty.Any,
127+
) -> ty.Mapping[str, ty.Any]:
128+
129+
# Collated DICOM headers across series
130+
collated: ty.Dict[str, ty.Any] = {}
131+
key_repeats: ty.Counter[str] = Counter()
132+
varying_keys = set()
133+
# We use the "contents" property implementation in TypeSet instead of the overload
134+
# in DicomCollection because we don't want the metadata to be read ahead of the
135+
# the `select_metadata` call below
136+
137+
for ptd in pet_raw_data.contents:
138+
for key, val in ptd.metadata.items():
139+
try:
140+
prev_val = collated[key]
141+
except KeyError:
142+
collated[
143+
key
144+
] = val # Insert initial value (should only happen on first iter)
145+
key_repeats.update([key])
146+
else:
147+
if key in varying_keys:
148+
collated[key].append(val)
149+
# Check whether the value is the same as the values in the previous
150+
# images in the series
151+
elif val != prev_val:
152+
collated[key] = [prev_val] * key_repeats[key] + [val]
153+
varying_keys.add(key)
154+
else:
155+
key_repeats.update([key])
156+
return collated
157+
158+
68159
@extra_implementation(FileSet.generate_sample_data)
69160
def siemens_pet_listmode_generate_sample_data(
70161
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
@@ -79,3 +170,43 @@ def siemens_pet_countrate_generate_sample_data(
79170
generator: SampleFileGenerator,
80171
) -> ty.List[Path]:
81172
return get_pet_countrate_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]
173+
174+
175+
@extra_implementation(FileSet.generate_sample_data)
176+
def siemens_pet_sinogram_generate_sample_data(
177+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
178+
generator: SampleFileGenerator,
179+
) -> ty.List[Path]:
180+
return get_pet_sinogram_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]
181+
182+
183+
@extra_implementation(FileSet.generate_sample_data)
184+
def siemens_pet_dynamics_sino_generate_sample_data(
185+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
186+
generator: SampleFileGenerator,
187+
) -> ty.List[Path]:
188+
return get_pet_dynamics_sino_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]
189+
190+
191+
@extra_implementation(FileSet.generate_sample_data)
192+
def siemens_pet_normalisation_generate_sample_data(
193+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
194+
generator: SampleFileGenerator,
195+
) -> ty.List[Path]:
196+
return get_pet_calibration_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]
197+
198+
199+
@extra_implementation(FileSet.generate_sample_data)
200+
def siemens_petct_spl_generate_sample_data(
201+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
202+
generator: SampleFileGenerator,
203+
) -> ty.List[Path]:
204+
return get_petct_spl_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]
205+
206+
207+
@extra_implementation(FileSet.generate_sample_data)
208+
def siemens_pet_parameterisation_generate_sample_data(
209+
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
210+
generator: SampleFileGenerator,
211+
) -> ty.List[Path]:
212+
return get_pet_replay_param_data(out_dir=generator.dest_dir) # type: ignore[no-any-return]

extras/fileformats/extras/medimage/tests/test_deidentify.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ def test_nifti_deidentify():
3636
assert nifti.hash_files() == deidentified.hash_files()
3737

3838

39-
@pytest.mark.xfail(reason="Need to generate more realistic Siemens raw pet data")
4039
def test_raw_pet_data_deidentify():
4140
raw_pet = Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram.sample()
4241
with pytest.raises(FileFormatsExtrasError):

extras/fileformats/extras/medimage/tests/test_generators.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pytest
21
from fileformats.medimage import (
32
NiftiGzX,
43
NiftiGzXBvec,
@@ -7,6 +6,11 @@
76
Fmri,
87
Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
98
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
9+
Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
10+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
11+
Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
12+
Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
13+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
1014
)
1115

1216

@@ -25,13 +29,36 @@ def test_dmri_generator():
2529
assert len(img.dims()) == 4
2630

2731

28-
@pytest.mark.xfail(reason="Need to generate more realistic Siemens raw pet data")
2932
def test_siemens_pet_listmode_generator():
3033
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode.sample()
3134
assert img.metadata["PatientName"] == "FirstName^LastName"
3235

3336

34-
@pytest.mark.xfail(reason="Need to generate more realistic Siemens raw pet data")
3537
def test_siemens_pet_countrate_generator():
3638
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate.sample()
3739
assert img.metadata["PatientName"] == "FirstName^LastName"
40+
41+
42+
def test_siemens_pet_sinogram_generator():
43+
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram.sample()
44+
assert img.metadata["PatientName"] == "FirstName^LastName"
45+
46+
47+
def test_siemens_pet_dynamics_sino_generator():
48+
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries.sample()
49+
assert img.metadata["PatientName"] == "FirstName^LastName"
50+
51+
52+
def test_siemens_pet_normalisation_generator():
53+
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation.sample()
54+
assert img.metadata["PatientName"] == "FirstName^LastName"
55+
56+
57+
def test_siemens_pet_petct_spl_generator():
58+
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData.sample()
59+
assert img.metadata["PatientName"] == "FirstName^LastName"
60+
61+
62+
def test_siemens_pet_parameterisation_generator():
63+
img = Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation.sample()
64+
assert img.metadata["PatientName"] == "FirstName^LastName"

fileformats/medimage/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@
4141
PetCountRate,
4242
PetNormalisation,
4343
Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData,
44+
Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData,
4445
Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
4546
Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
47+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram,
48+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
4649
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
4750
Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
48-
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData,
51+
Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
52+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
4953
)
5054
from .surface import Gifti
5155
from .contents.imaging.modality import (
@@ -159,11 +163,15 @@
159163
"PetCountRate",
160164
"PetNormalisation",
161165
"Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData",
166+
"Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData",
162167
"Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode",
163168
"Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram",
169+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram",
170+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries",
164171
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate",
165172
"Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation",
166-
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData",
173+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation",
174+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData",
167175
"Gifti",
168176
"ImagingModality",
169177
"CombinedModalities",

fileformats/medimage/dicom.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,19 @@ def get_dicom_tag(
195195
element = int.from_bytes(tag_bytes[2:], "little")
196196
tag = (group, element)
197197

198-
vr = file_stream.read(2).decode()
199-
200-
if vr in {"OB", "OW", "OF", "SQ", "UT", "UN"}:
201-
file_stream.read(2) # reserved
202-
length = int.from_bytes(file_stream.read(4), "little")
198+
vr_bytes = file_stream.read(2)
199+
# If the VR bytes are not purely alphabetic, assume implicit VR.
200+
if vr_bytes.decode(errors="ignore").strip().isalpha():
201+
vr = vr_bytes.decode()
202+
if vr in {"OB", "OW", "OF", "SQ", "UT", "UN"}:
203+
file_stream.read(2) # reserved
204+
length = int.from_bytes(file_stream.read(4), "little")
205+
else:
206+
length = int.from_bytes(file_stream.read(2), "little")
203207
else:
204-
length = int.from_bytes(file_stream.read(2), "little")
205-
208+
# Implicit VR: rewind the 2 bytes and read a 4-byte length
209+
file_stream.seek(-2, 1)
210+
length = int.from_bytes(file_stream.read(4), "little")
206211
value = file_stream.read(length)
207212

208213
if tag == target_tag:

fileformats/medimage/raw/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData,
1313
Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
1414
Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
15+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram,
16+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
1517
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
1618
Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
17-
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData,
19+
Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
20+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
1821
)
1922

2023
__all__ = [
@@ -29,7 +32,10 @@
2932
"Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData",
3033
"Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode",
3134
"Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram",
35+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram",
36+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries",
3237
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate",
3338
"Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation",
34-
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData",
39+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation",
40+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData",
3541
]

fileformats/medimage/raw/pet/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData,
1111
Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode,
1212
Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram,
13+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram,
14+
Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries,
1315
Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate,
1416
Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation,
15-
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData,
17+
Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation,
18+
Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData,
1619
)
1720

1821

@@ -26,7 +29,10 @@
2629
"Vnd_Siemens_Biograph128Vision_Vr20b_LargePetRawData",
2730
"Vnd_Siemens_Biograph128Vision_Vr20b_PetListMode",
2831
"Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram",
32+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogram",
33+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetDynamicSinogramSeries",
2934
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate",
3035
"Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation",
31-
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtRawData",
36+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetParameterisation",
37+
"Vnd_Siemens_Biograph128Vision_Vr20b_PetCtSplRawData",
3238
]

fileformats/medimage/raw/pet/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ class PetCountRate(PetRawData):
4040

4141
class PetNormalisation(PetRawData):
4242
"normalisation scan or the current cross calibration factor"
43+
44+
45+
class PetPhysio(PetRawData):
46+
"physiological data (e.g. ECG, respiration) for motion correction"
47+
48+
49+
class PetParameterisation(PetRawData):
50+
"physiological data (e.g. ECG, respiration) for motion correction"

0 commit comments

Comments
 (0)