ez_wsi_dicomweb/ml_toolkit/dicom_builder.py (148 lines of code) (raw):

# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Utility class for building Basic Text DICOM Structured Reports.""" from __future__ import absolute_import from __future__ import division from __future__ import print_function import datetime import os from typing import Any, Mapping import uuid from ez_wsi_dicomweb.ml_toolkit import dicom_json from ez_wsi_dicomweb.ml_toolkit import tag_values from ez_wsi_dicomweb.ml_toolkit import tags import numpy as np # Default UUID prefix used to generate UIDs of DICOM objects created by this # module. DEFAULT_UUID_PREFIX = '1.3.6.1.4.1.11129.5.3' # DICOM header preamble is 128-byte long. _PREAMBLE_LENGTH = 128 # Little Endian Transfer Syntax. _IMPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2' _EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1' # Accepted character set. _ISO_CHARACTER_SET = 'ISO_IR 192' class DicomBuilder(object): """Can be used to create DICOM objects.""" def __init__(self, uid_prefix: str = DEFAULT_UUID_PREFIX) -> None: """Inits DicomBuilder with passed args. Args: uid_prefix: UID string that is used as the prefix for all generated UIDs. """ self._uid_prefix = uid_prefix def BuildJsonSR( self, report_text: str, metadata_json: Mapping[str, Any] ) -> dicom_json.ObjectWithBulkData: """Builds and returns a Basic Text DICOM JSON Structured Report instance. This function will create a new DICOM series. Args: report_text: Text string to use for the Basic Text DICOM SR. metadata_json: Dict of tags (including study-level information) to add. Returns: DICOM JSON Object containing the Structured Report. """ # Dicom StowJsonRs expects a list with DICOM JSON as elements. # Add study level tags to the SR. dataset = dict(metadata_json) series_uid = self.GenerateUID() instance_uid = self.GenerateUID() dicom_json.Insert( dataset, tags.SOP_CLASS_UID, tag_values.BASIC_TEXT_SR_CUID ) dicom_json.Insert(dataset, tags.MODALITY, tag_values.SR_MODALITY) dicom_json.Insert(dataset, tags.SERIES_INSTANCE_UID, series_uid) dicom_json.Insert(dataset, tags.SPECIFIC_CHARACTER_SET, _ISO_CHARACTER_SET) dicom_json.Insert(dataset, tags.SOP_INSTANCE_UID, instance_uid) content_dataset = {} dicom_json.Insert(content_dataset, tags.RELATIONSHIP_TYPE, 'CONTAINS') dicom_json.Insert(content_dataset, tags.VALUE_TYPE, 'TEXT') dicom_json.Insert(content_dataset, tags.TEXT_VALUE, report_text) dicom_json.Insert(dataset, tags.CONTENT_SEQUENCE, content_dataset) dicom_json.Insert( dataset, tags.TRANSFER_SYNTAX_UID, _IMPLICIT_VR_LITTLE_ENDIAN ) dicom_json.Insert( dataset, tags.MEDIA_STORAGE_SOP_CLASS_UID, tag_values.BASIC_TEXT_SR_CUID ) dicom_json.Insert( dataset, tags.MEDIA_STORAGE_SOP_INSTANCE_UID, instance_uid ) return dicom_json.ObjectWithBulkData(dataset, []) def BuildJsonSC( self, image_array: np.ndarray, metadata_json: Mapping[str, Any], series_uid: str, ) -> dicom_json.ObjectWithBulkData: """Builds and returns a DICOM Secondary Capture. Args: image_array: Image array (RGB) to embed in DICOM instance. metadata_json: Dict of tags (including study-level information) to add. series_uid: UID of the series to create the SC in. Returns: DICOM JSON Object containing JSON and bulk data of the Secondary Capture. """ # Copy over any study and instance level tags. instance_uid = self.GenerateUID() metadata_json = dict(metadata_json) dicom_json.Insert( metadata_json, tags.SOP_CLASS_UID, tag_values.SECONDARY_CAPTURE_CUID ) dicom_json.Insert(metadata_json, tags.MODALITY, tag_values.OT_MODALITY) dicom_json.Insert(metadata_json, tags.SERIES_INSTANCE_UID, series_uid) dicom_json.Insert( metadata_json, tags.SPECIFIC_CHARACTER_SET, _ISO_CHARACTER_SET ) dicom_json.Insert(metadata_json, tags.SOP_INSTANCE_UID, instance_uid) dicom_json.Insert( metadata_json, tags.TRANSFER_SYNTAX_UID, _IMPLICIT_VR_LITTLE_ENDIAN ) dicom_json.Insert( metadata_json, tags.MEDIA_STORAGE_SOP_CLASS_UID, tag_values.SECONDARY_CAPTURE_CUID, ) dicom_json.Insert( metadata_json, tags.MEDIA_STORAGE_SOP_INSTANCE_UID, instance_uid ) # Assures URI is unique. study_uid = dicom_json.GetValue(metadata_json, tags.STUDY_INSTANCE_UID) uri = '{}/{}/{}'.format(study_uid, series_uid, instance_uid) metadata_json[tags.PIXEL_DATA.number] = { 'vr': tags.PIXEL_DATA.vr, 'BulkDataURI': uri, } dicom_json.Insert(metadata_json, tags.PHOTOMETRIC_INTERPRETATION, 'RGB') dicom_json.Insert(metadata_json, tags.SAMPLES_PER_PIXEL, 3) # Indicates we store pixel data as R1,G1,B1,R2,G2,B2... dicom_json.Insert(metadata_json, tags.PLANAR_CONFIGURATION, 0) dicom_json.Insert(metadata_json, tags.ROWS, image_array.shape[0]) dicom_json.Insert(metadata_json, tags.COLUMNS, image_array.shape[1]) dicom_json.Insert(metadata_json, tags.BITS_ALLOCATED, 8) dicom_json.Insert(metadata_json, tags.BITS_STORED, 8) dicom_json.Insert(metadata_json, tags.HIGH_BIT, 7) dicom_json.Insert(metadata_json, tags.PIXEL_REPRESENTATION, 0) bulkdata = dicom_json.DicomBulkData( uri=uri, data=image_array.tobytes(), content_type='application/octet-stream', ) return dicom_json.ObjectWithBulkData(metadata_json, [bulkdata]) def BuildJsonInstanceFromPng( self, image: bytes, sop_class_uid: str ) -> dicom_json.ObjectWithBulkData: """Builds and returns a DICOM instance from a PNG. This function will create a new DICOM study and series. Converts all incoming images to grayscale. Args: image: Image bytes of DICOM instance. sop_class_uid: UID of the SOP class for DICOM instance. Returns: DICOM JSON Object containing JSON and bulk data of the Secondary Capture. """ study_uid = self.GenerateUID() series_uid = self.GenerateUID() instance_uid = self.GenerateUID() metadata_json = {} dicom_json.Insert(metadata_json, tags.PLANAR_CONFIGURATION, 0) # Converts colored images to grayscale. dicom_json.Insert( metadata_json, tags.PHOTOMETRIC_INTERPRETATION, 'MONOCHROME2' ) dicom_json.Insert(metadata_json, tags.SOP_CLASS_UID, sop_class_uid) dicom_json.Insert(metadata_json, tags.STUDY_INSTANCE_UID, study_uid) dicom_json.Insert(metadata_json, tags.SERIES_INSTANCE_UID, series_uid) dicom_json.Insert( metadata_json, tags.SPECIFIC_CHARACTER_SET, _ISO_CHARACTER_SET ) dicom_json.Insert(metadata_json, tags.SOP_INSTANCE_UID, instance_uid) dicom_json.Insert( metadata_json, tags.TRANSFER_SYNTAX_UID, _EXPLICIT_VR_LITTLE_ENDIAN ) dicom_json.Insert( metadata_json, tags.MEDIA_STORAGE_SOP_CLASS_UID, sop_class_uid ) dicom_json.Insert( metadata_json, tags.MEDIA_STORAGE_SOP_INSTANCE_UID, instance_uid ) # Assures URI is unique. uri = '{}/{}/{}'.format(study_uid, series_uid, instance_uid) metadata_json[tags.PIXEL_DATA.number] = { 'vr': tags.PIXEL_DATA.vr, 'BulkDataURI': uri, } bulkdata = dicom_json.DicomBulkData( uri=uri, data=image, content_type='image/png; transfer-syntax=""' ) return dicom_json.ObjectWithBulkData(metadata_json, [bulkdata]) def GenerateUID(self) -> str: """Generates a random DICOM UUID with the prefix DicomBuilder was constructed with. Returns: Unique UID starting with |self._uid_prefix|. """ # Generates a unique UID using the Process ID, Host ID and current time. # Uses as a period as the separator and combines the generated UUID with the # |self._uid_prefix| prefix. # Example: 1.3.6.1.4.1.11129.5.3.268914880332007.160162.47.376673 uuid_components = [ self._uid_prefix, uuid.getnode(), abs(os.getpid()), datetime.datetime.today().second, datetime.datetime.today().microsecond, ] generated_uuid = '.'.join( str(uuid_component) for uuid_component in uuid_components ) return generated_uuid def UIDStartsWith(uid: str, prefix: str) -> bool: """Determines if the uid starts with |prefix|. Args: uid: Text string representing a UID. prefix: Text string representing the prefix of the UID which is being tested. Returns: True if |uid| starts with |prefix| false otherwise. """ return uid.find(prefix + '.') == 0 def IsToolkitUID(uid: str) -> bool: """Determines if the uid was generated by the toolkit. Args: uid: Text string representing a UID. Returns: True if |uid| starts with |DEFAULT_UUID_PREFIX| false otherwise. """ return UIDStartsWith(uid, DEFAULT_UUID_PREFIX)