Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_get_report_dicom_with_invalid_file(tmp_path):
file_path.write_text("not a dicom")
with open(file_path, "rb") as f:
response = client.post(
"/report/process/dicom",
"/api/report/process/dicom",
files={"dcm_files": ("not_a_dicom.txt", f, "text/plain")},
data={"modality": "ASL"}
)
Expand Down
2 changes: 1 addition & 1 deletion package/src/pyaslreport/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from pyaslreport.enums.modaliy_enum import ModalityTypeValues
from pyaslreport.enums.modality_enum import ModalityTypeValues

__all__ = ["ModalityTypeValues"]
2 changes: 1 addition & 1 deletion package/src/pyaslreport/sequences/factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pyaslreport.enums.modaliy_enum import ModalityTypeValues
from pyaslreport.enums.modality_enum import ModalityTypeValues
from pyaslreport.sequences.ge.asl import GEBasicSinglePLD, GEMultiPLD
from pyaslreport.sequences.ge.dsc import GEDSCSequence
from pyaslreport.sequences.siemens.asl import SiemensBasicSinglePLD
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from pyaslreport.sequences.siemens.siemens_base import SiemensBaseSequence
from pyaslreport.utils import dicom_tags_utils as dcm_tags
from pydicom.tag import Tag

# Siemens uses standard DICOM InversionTime tag for PostLabelingDelay
SIEMENS_INVERSION_TIME = Tag(0x0018, 0x0082)

# TODO: LabelingDuration for Siemens should be extracted from the Phoenix protocol
# (private tag 0x0029,0x1020). For now, use the same tag address as GE_LABEL_DURATION
# since some Siemens sequences store it there.
SIEMENS_LABEL_DURATION = Tag(0x0043, 0x10A5)

class SiemensBasicSinglePLD(SiemensBaseSequence):
@classmethod
Expand All @@ -15,8 +24,8 @@ def extract_bids_metadata(self):
bids = self._extract_common_metadata()
bids.update(self._extract_siemens_common_metadata())
d = self.dicom_header
if dcm_tags.GE_LABEL_DURATION in d:
bids["LabelingDuration"] = d.get(dcm_tags.GE_LABEL_DURATION, None).value
if dcm_tags.GE_INVERSION_TIME in d:
bids["PostLabelingDelay"] = d.get(dcm_tags.GE_INVERSION_TIME, None).value
if SIEMENS_LABEL_DURATION in d:
bids["LabelingDuration"] = d.get(SIEMENS_LABEL_DURATION, None).value
if SIEMENS_INVERSION_TIME in d:
bids["PostLabelingDelay"] = d.get(SIEMENS_INVERSION_TIME, None).value
return bids
51 changes: 20 additions & 31 deletions package/src/pyaslreport/sequences/siemens/siemens_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,35 @@ def is_siemens_manufacturer(cls, dicom_header):
bool: True if manufacturer contains Siemens
"""
manufacturer = dicom_header.get(dcm_tags.MANUFACTURER, "").value.strip().upper()
return "SIEMENS" in manufacturer or "SIEMENS HEALTHCARE" in manufacturer or "SIEMENS HEALHINEERS" in manufacturer
return "SIEMENS" in manufacturer or "SIEMENS HEALTHCARE" in manufacturer or "SIEMENS HEALTHINEERS" in manufacturer


def _extract_siemens_common_metadata(self) -> dict:
d = self.dicom_header
bids = {}
# Direct GE-specific mappings
if dcm_tags.GE_ASSET_R_FACTOR in d:
bids["AssetRFactor"] = d.get(dcm_tags.GE_ASSET_R_FACTOR, None).value
if dcm_tags.GE_EFFECTIVE_ECHO_SPACING in d:
bids["EffectiveEchoSpacing"] = d.get(dcm_tags.GE_EFFECTIVE_ECHO_SPACING, None).value
if dcm_tags.GE_ACQUISITION_MATRIX in d:
bids["AcquisitionMatrix"] = d.get(dcm_tags.GE_ACQUISITION_MATRIX, None).value
if dcm_tags.GE_NUMBER_OF_EXCITATIONS in d:
bids["TotalAcquiredPairs"] = d.get(dcm_tags.GE_NUMBER_OF_EXCITATIONS, None).value

# Derived fields
# EffectiveEchoSpacing = EffectiveEchoSpacing * AssetRFactor * 1e-6
if dcm_tags.GE_EFFECTIVE_ECHO_SPACING in d and dcm_tags.GE_ASSET_R_FACTOR in d:
try:
eff_echo = float(d.get(dcm_tags.GE_EFFECTIVE_ECHO_SPACING, None).value)
asset = float(d.get(dcm_tags.GE_ASSET_R_FACTOR, None).value)
bids["EffectiveEchoSpacing"] = eff_echo * asset * 1e-6
except Exception:
pass
# Siemens-specific metadata extraction
if dcm_tags.SIEMENS_BANDWIDTH_PER_PIXEL_PHASE_ENCODING in d:
bids["BandwidthPerPixelPhaseEncode"] = d.get(dcm_tags.SIEMENS_BANDWIDTH_PER_PIXEL_PHASE_ENCODING, None).value

# TotalReadoutTime = (AcquisitionMatrix[0] - 1) * EffectiveEchoSpacing
if (
dcm_tags.GE_ACQUISITION_MATRIX in d and
isinstance(d.get(dcm_tags.GE_ACQUISITION_MATRIX, None).value, (list, tuple)) and
len(d.get(dcm_tags.GE_ACQUISITION_MATRIX, None).value) > 0 and
dcm_tags.GE_EFFECTIVE_ECHO_SPACING in bids
):
if dcm_tags.SIEMENS_ROWS in d and dcm_tags.SIEMENS_COLUMNS in d:
rows = d.get(dcm_tags.SIEMENS_ROWS, None).value
cols = d.get(dcm_tags.SIEMENS_COLUMNS, None).value
bids["AcquisitionMatrix"] = [rows, cols]

if dcm_tags.SIEMENS_INPLANE_PHASE_ENCODING_DIRECTION in d:
bids["InPlanePhaseEncodingDirection"] = d.get(dcm_tags.SIEMENS_INPLANE_PHASE_ENCODING_DIRECTION, None).value

# Derive EffectiveEchoSpacing and TotalReadoutTime from BandwidthPerPixelPhaseEncode
if dcm_tags.SIEMENS_BANDWIDTH_PER_PIXEL_PHASE_ENCODING in d and dcm_tags.SIEMENS_ROWS in d:
try:
acq_matrix = d.get(dcm_tags.GE_ACQUISITION_MATRIX, None).value[0]
eff_echo = bids["EffectiveEchoSpacing"]
bids["TotalReadoutTime"] = (acq_matrix - 1) * eff_echo
bw = float(d.get(dcm_tags.SIEMENS_BANDWIDTH_PER_PIXEL_PHASE_ENCODING, None).value)
rows = int(d.get(dcm_tags.SIEMENS_ROWS, None).value)
if bw > 0:
bids["EffectiveEchoSpacing"] = 1.0 / (bw * rows)
bids["TotalReadoutTime"] = (rows - 1) * bids["EffectiveEchoSpacing"]
except Exception:
pass

# MRAcquisitionType default is 3D if not present
if dcm_tags.MR_ACQUISITION_TYPE in d:
bids["MRAcquisitionType"] = d.get(dcm_tags.MR_ACQUISITION_TYPE, None).value
Expand Down