1313import logging
1414from pathlib import Path
1515from random import randint
16- from typing import Dict , List , Text , Union
16+ from typing import Dict , List , Optional , Text , Union
1717
1818from monai .deploy .utils .importutil import optional_import
19- from monai .deploy .utils .version import get_sdk_semver
2019
2120dcmread , _ = optional_import ("pydicom" , name = "dcmread" )
2221dcmwrite , _ = optional_import ("pydicom.filewriter" , name = "dcmwrite" )
3130from monai .deploy .core .domain .dicom_series import DICOMSeries
3231from monai .deploy .core .domain .dicom_series_selection import StudySelectedSeries
3332from 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