Skip to content

Commit fc07d80

Browse files
authored
Enhance some attributes in the text SR writer (#346)
* Added support of matching instance level tags, and update liver seg Signed-off-by: M Q <mingmelvinq@nvidia.com> * Fix Flake8 complaint Signed-off-by: M Q <mingmelvinq@nvidia.com> * Still enable publish intermediate nii files. Signed-off-by: M Q <mingmelvinq@nvidia.com> * Corrections per review comments Signed-off-by: M Q <mingmelvinq@nvidia.com> * Enhance attribute settings Signed-off-by: M Q <mingmelvinq@nvidia.com> * Fixed styling complaints Signed-off-by: M Q <mingmelvinq@nvidia.com> * Fix MyPy complaint Signed-off-by: M Q <mingmelvinq@nvidia.com> * Use content sequence for section of text Signed-off-by: M Q <mingmelvinq@nvidia.com> Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent aa68276 commit fc07d80

File tree

1 file changed

+66
-29
lines changed

1 file changed

+66
-29
lines changed

monai/deploy/operators/dicom_text_sr_writer_operator.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
import logging
1414
from pathlib import Path
1515
from random import randint
16-
from typing import Dict, List, Text, Union
16+
from typing import Dict, List, Optional, Text, Union
1717

1818
from monai.deploy.utils.importutil import optional_import
19-
from monai.deploy.utils.version import get_sdk_semver
2019

2120
dcmread, _ = optional_import("pydicom", name="dcmread")
2221
dcmwrite, _ = optional_import("pydicom.filewriter", name="dcmwrite")
@@ -31,6 +30,7 @@
3130
from monai.deploy.core.domain.dicom_series import DICOMSeries
3231
from monai.deploy.core.domain.dicom_series_selection import StudySelectedSeries
3332
from monai.deploy.exceptions import ItemNotExistsError
33+
from monai.deploy.utils.version import get_sdk_semver
3434

3535

3636
# Utility classes considered to be moved into Domain module
@@ -69,19 +69,20 @@ def __init__(
6969
manufacturer: str = "MONAI Deploy",
7070
manufacturer_model: str = "MONAI Deploy App SDK",
7171
series_number: str = "0000",
72-
software_version_number: str = "0.2",
72+
software_version_number: str = "",
7373
):
7474

7575
self.manufacturer = manufacturer if isinstance(manufacturer, str) else ""
7676
self.manufacturer_model = manufacturer_model if isinstance(manufacturer_model, str) else ""
7777
self.series_number = series_number if isinstance(series_number, str) else ""
78-
try:
79-
version_str = get_sdk_semver() # SDK Version
80-
except Exception:
81-
version_str = "0.2" # Fall back to the initial version
82-
self.software_version_number = (
83-
software_version_number if isinstance(software_version_number, str) else version_str
84-
)
78+
if software_version_number:
79+
self.software_version_number = software_version_number
80+
else:
81+
try:
82+
version_str = get_sdk_semver() # SDK Version
83+
except Exception:
84+
version_str = "" # Fall back to the initial version
85+
self.software_version_number = version_str
8586

8687

8788
# The SR writer operator class
@@ -129,12 +130,17 @@ def __init__(
129130
# "OT" for PDF
130131
# "SR" for Structured Report.
131132
# Media Storage SOP Class UID, e.g.,
133+
# "1.2.840.10008.5.1.4.1.1.88.11" for Basic Text SR Storage
132134
# "1.2.840.10008.5.1.4.1.1.104.1" for Encapsulated PDF Storage,
133135
# "1.2.840.10008.5.1.4.1.1.88.34" for Comprehensive 3D SR IOD
134136
# "1.2.840.10008.5.1.4.1.1.66.4" for Segmentation Storage
135137
self.modality_type = "SR"
136-
self.sop_class_uid = "1.2.840.10008.5.1.4.1.1.88.34"
137-
self.implementation_version_name = "MONAI Deploy App SDK 0.2"
138+
self.sop_class_uid = "1.2.840.10008.5.1.4.1.1.88.11"
139+
# Equipment version may be different from contributing equipment version
140+
try:
141+
self.software_version_number = get_sdk_semver() # SDK Version
142+
except Exception:
143+
self.software_version_number = ""
138144
self.operators_name = f"AI Algorithm {self.model_info.name}"
139145

140146
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
@@ -188,7 +194,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
188194
# Now ready to starting writing the DICOM instance
189195
self.write(result_text, dicom_series, output_dir)
190196

191-
def write(self, content_text, dicom_series: Union[DICOMSeries, None], output_dir: Path):
197+
def write(self, content_text, dicom_series: Optional[DICOMSeries], output_dir: Path):
192198
"""Writes DICOM object
193199
194200
Args:
@@ -220,17 +226,43 @@ def write(self, content_text, dicom_series: Union[DICOMSeries, None], output_dir
220226
# imaging procedure analyzed, not the algorithm used.
221227

222228
# Use text value for example
223-
ds.ValueType = "TEXT"
229+
ds.ValueType = "CONTAINER"
224230

225231
# ConceptNameCode Sequence
226232
seq_concept_name_code = Sequence()
233+
ds.ConceptNameCodeSequence = seq_concept_name_code
234+
235+
# Concept Name Code Sequence: Concept Name Code
227236
ds_concept_name_code = Dataset()
228237
ds_concept_name_code.CodeValue = "18748-4"
229238
ds_concept_name_code.CodingSchemeDesignator = "LN"
230239
ds_concept_name_code.CodeMeaning = "Diagnostic Imaging Report"
231240
seq_concept_name_code.append(ds_concept_name_code)
232-
ds.ConceptNameCodeSequence = seq_concept_name_code
233-
ds.TextValue = content_text # self._content_to_string(content_file)
241+
242+
ds.ContinuityOfContent = "SEPARATE"
243+
244+
# Content Sequence
245+
content_sequence = Sequence()
246+
ds.ContentSequence = content_sequence
247+
248+
# Content Sequence: Content 1
249+
content1 = Dataset()
250+
content1.RelationshipType = "CONTAINS"
251+
content1.ValueType = "TEXT"
252+
253+
# Concept Name Code Sequence
254+
concept_name_code_sequence = Sequence()
255+
content1.ConceptNameCodeSequence = concept_name_code_sequence
256+
257+
# Concept Name Code Sequence: Concept Name Code 1
258+
concept_name_code1 = Dataset()
259+
concept_name_code1.CodeValue = "111412" # or 111413 "Overall Assessment"
260+
concept_name_code1.CodingSchemeDesignator = "DCM"
261+
concept_name_code1.CodeMeaning = "Narrative Summary" # or 111413 'Overall Assessment'
262+
concept_name_code_sequence.append(concept_name_code1)
263+
264+
content1.TextValue = content_text # The actual report content text
265+
content_sequence.append(content1)
234266

235267
# For now, only allow str Keywords and str value
236268
if self.custom_tags:
@@ -262,12 +294,12 @@ def save_dcm_file(data_set, file_path: Path, validate_readable: bool = True):
262294
# TODO: The following function can be moved into Domain module as it's common.
263295
@staticmethod
264296
def write_common_modules(
265-
dicom_series: Union[DICOMSeries, None],
297+
dicom_series: Optional[DICOMSeries],
266298
copy_tags: bool,
267299
modality_type: str,
268300
sop_class_uid: str,
269-
model_info: Union[ModelInfo, None] = None,
270-
equipment_info: Union[EquipmentInfo, None] = None,
301+
model_info: Optional[ModelInfo] = None,
302+
equipment_info: Optional[EquipmentInfo] = None,
271303
):
272304
"""Writes DICOM object common modules with or without a reference DCIOM Series
273305
@@ -304,6 +336,7 @@ def write_common_modules(
304336
dt_now = datetime.datetime.now()
305337
date_now_dcm = dt_now.strftime("%Y%m%d")
306338
time_now_dcm = dt_now.strftime("%H%M%S")
339+
offset_from_utc = dt_now.astimezone().isoformat()[-6:].replace(":", "") # '2022-09-27T22:36:20.143857-07:00'
307340

308341
# Generate UIDs and descriptions
309342
my_sop_instance_uid = generate_uid()
@@ -321,7 +354,7 @@ def write_common_modules(
321354
file_meta.MediaStorageSOPInstanceUID = my_sop_instance_uid
322355
file_meta.TransferSyntaxUID = ImplicitVRLittleEndian # 1.2.840.10008.1.2, Little Endian Implicit VR
323356
file_meta.ImplementationClassUID = "1.2.40.0.13.1.1.1" # Made up. Not registered.
324-
file_meta.ImplementationVersionName = "MONAI Deploy App SDK 0.2"
357+
file_meta.ImplementationVersionName = equipment_info.software_version_number if equipment_info else ""
325358

326359
# Write modules to data set
327360
ds = Dataset()
@@ -335,6 +368,7 @@ def write_common_modules(
335368
# Use current time for now, but could potentially use the actual inference start time.
336369
ds.ContentDate = date_now_dcm
337370
ds.ContentTime = time_now_dcm
371+
ds.TimezoneOffsetFromUTC = offset_from_utc
338372

339373
# The date and time that the original generation of the data in the document started.
340374
ds.AcquisitionDateTime = date_now_dcm + time_now_dcm # Result has just been created.
@@ -360,9 +394,9 @@ def write_common_modules(
360394
# Equipment Module, mandatory
361395
if equipment_info:
362396
ds.Manufacturer = equipment_info.manufacturer
363-
ds.ManufacturerModel = equipment_info.manufacturer_model
397+
ds.ManufacturerModelName = equipment_info.manufacturer_model
364398
ds.SeriesNumber = equipment_info.series_number
365-
ds.SoftwareVersionNumber = equipment_info.software_version_number
399+
ds.SoftwareVersions = equipment_info.software_version_number
366400

367401
# SOP Common Module, mandatory
368402
ds.InstanceCreationDate = date_now_dcm
@@ -408,12 +442,12 @@ def write_common_modules(
408442
# '(121014, DCM, “Device Observer Manufacturer")'
409443
ds_contributing_equipment.Manufacturer = model_info.creator
410444
# u'(121015, DCM, “Device Observer Model Name")'
411-
ds_contributing_equipment.ManufacturerModel = model_info.name
445+
ds_contributing_equipment.ManufacturerModelName = model_info.name
412446
# u'(111003, DCM, “Algorithm Version")'
413-
ds_contributing_equipment.SoftwareVersionNumber = model_info.version
447+
ds_contributing_equipment.SoftwareVersions = model_info.version
414448
ds_contributing_equipment.DeviceUID = model_info.uid # u'(121012, DCM, “Device Observer UID")'
415449
seq_contributing_equipment.append(ds_contributing_equipment)
416-
ds.ContributingequipmentSequence = seq_contributing_equipment
450+
ds.ContributingEquipmentSequence = seq_contributing_equipment
417451

418452
logging.debug("DICOM common modules written:\n{}".format(ds))
419453

@@ -435,15 +469,18 @@ def test():
435469
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
436470

437471
current_file_dir = Path(__file__).parent.resolve()
438-
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm")
472+
data_path = current_file_dir.joinpath("../../../inputs/livertumor_ct/dcm/1-CT_series_liver_tumor_from_nii014")
439473
out_path = current_file_dir.joinpath("../../../examples/output_sr_op")
440-
test_report_text = "Dummy AI classification resutls."
441-
test_copy_tags = True
474+
test_report_text = "Tumors detected in Liver using MONAI Liver Tumor Seg model."
475+
test_copy_tags = False
442476

443477
loader = DICOMDataLoaderOperator()
444478
series_selector = DICOMSeriesSelectorOperator()
445479
sr_writer = DICOMTextSRWriterOperator(
446-
copy_tags=test_copy_tags, model_info=None, custom_tags={"SeriesDescription": "New AI Series"}
480+
copy_tags=test_copy_tags,
481+
model_info=None,
482+
equipment_info=EquipmentInfo(software_version_number="0.4"),
483+
custom_tags={"SeriesDescription": "Textual report from AI algorithm. Not for clinical use."},
447484
)
448485

449486
# Testing with the main entry functions

0 commit comments

Comments
 (0)