/// <summary> /// Converts a medical scan, and a set of binary masks, into a Dicom representation. /// The returned set of Dicom files will have files for all slices of the scan, and an RtStruct /// file containing the contours that were derived from the masks. The RtStruct file will be the first /// entry in the returned list of Dicom files. /// Use with extreme care - many Dicom elements have to be halluzinated here, and there's no /// guarantee that the resulting Dicom will be usable beyond what is needed in InnerEye. /// </summary> /// <param name="scan">The medical scan.</param> /// <param name="imageModality">The image modality through which the scan was acquired.</param> /// <param name="contours">A list of contours for individual anatomical structures, alongside /// name and rendering color.</param> /// <param name="seriesDescription">The value to use as the Dicom series description.</param> /// <param name="patientID">The patient ID that should be used in the Dicom files. If null, /// a randomly generated patient ID will be used.</param> /// <param name="studyInstanceID">The study ID that should be used in the Dicom files (DicomTag.StudyInstanceUID). If null, /// a randomly generated study ID will be used.</param> /// <param name="additionalDicomItems">Additional Dicom items that will be added to each of the slice datasets. This can /// be used to pass in additional information like manufacturer.</param> /// <returns></returns> public static List <DicomFileAndPath> ScanAndContoursToDicom(Volume3D <short> scan, ImageModality imageModality, IReadOnlyList <ContourRenderingInformation> contours, string seriesDescription = null, string patientID = null, string studyInstanceID = null, DicomDataset additionalDicomItems = null) { // To create the Dicom identifiers, write the volume to a set of Dicom files first, // then read back in. // When working with MR scans from the CNN models, it is quite common to see scans that have a large deviation from // the 1:1 aspect ratio. Relax that constraint a bit (default is 1.001) var scanFiles = ScanToDicomInMemory(scan, imageModality, seriesDescription, patientID, studyInstanceID, additionalDicomItems); var medicalVolume = MedicalVolumeFromDicom(scanFiles, maxPixelSizeRatioMR: 1.01); var dicomIdentifiers = medicalVolume.Identifiers; var volumeTransform = medicalVolume.Volume.Transform; medicalVolume = null; var rtFile = ContoursToDicomRtFile(contours, dicomIdentifiers, volumeTransform); var dicomFiles = new List <DicomFileAndPath> { rtFile }; dicomFiles.AddRange(scanFiles); return(dicomFiles); }
/// <summary> /// Saves a medical scan to a set of Dicom files. Each file is saved into a memory stream. /// The returned Dicom files have the Path property set to {sliceIndex}.dcm. /// Use with extreme care - many Dicom elements have to be halluzinated here, and there's no /// guarantee that the resulting Dicom will be usable beyond what is needed in InnerEye. /// </summary> /// <param name="scan">The medical scan.</param> /// <param name="imageModality">The image modality through which the scan was acquired.</param> /// <param name="seriesDescription">The series description that should be used in the Dicom files.</param> /// <param name="patientID">The patient ID that should be used in the Dicom files. If null, /// a randomly generated patient ID will be used.</param> /// <param name="studyInstanceID">The study ID that should be used in the Dicom files (DicomTag.StudyInstanceUID). If null, /// a randomly generated study ID will be used.</param> /// <param name="additionalDicomItems">Additional Dicom items that will be added to each of the slice datasets. This can /// be used to pass in additional information like manufacturer.</param> /// <returns></returns> public static List <DicomFileAndPath> ScanToDicomInMemory(Volume3D <short> scan, ImageModality imageModality, string seriesDescription = null, string patientID = null, string studyInstanceID = null, DicomDataset additionalDicomItems = null) { var scanAsDicomFiles = Convert(scan, imageModality, seriesDescription, patientID, studyInstanceID, additionalDicomItems).ToList(); var dicomFileAndPath = new List <DicomFileAndPath>(); for (var index = 0; index < scanAsDicomFiles.Count; index++) { var stream = new MemoryStream(); scanAsDicomFiles[index].Save(stream); stream.Seek(0, SeekOrigin.Begin); var dicomFile = DicomFileAndPath.SafeCreate(stream, $"{index}.dcm"); dicomFileAndPath.Add(dicomFile); } return(dicomFileAndPath); }
/// <summary> /// Converts a volume 3D into a collection of Dicom files (split by slice on the primary plane). /// This code writes the patient position as HFS (this might not be correct but was needed at some point to view the output). /// /// Note: This code has not been tested with MR data. It also assumes the Photometric Interpretation to be MONOCHROME2. /// Use with extreme care - many Dicom elements have to be halluzinated here, and there's no /// guarantee that the resulting Dicom will be usable beyond what is needed in InnerEye. /// </summary> /// <param name="volume">The volume to convert.</param> /// <param name="modality">The image modality.</param> /// <param name="seriesDescription">The value to use as the Dicom series description.</param> /// <param name="patientID">The patient ID that should be used in the Dicom files. If null, /// a randomly generated patient ID will be used.</param> /// <param name="studyInstanceID">The study ID that should be used in the Dicom files (DicomTag.StudyInstanceUID). If null, /// a randomly generated study ID will be used.</param> /// <param name="additionalDicomItems">Additional Dicom items that will be added to each of the slice datasets. This can /// be used to pass in additional information like manufacturer.</param> /// <returns>The collection of Dicom files that represents the Dicom image series.</returns> public static IEnumerable <DicomFile> Convert(Volume3D <short> volume, ImageModality modality, string seriesDescription = null, string patientID = null, string studyInstanceID = null, DicomDataset additionalDicomItems = null) { seriesDescription = seriesDescription ?? string.Empty; patientID = CreateUidIfEmpty(patientID); studyInstanceID = CreateUidIfEmpty(studyInstanceID); if (!IsValidDicomLongString(seriesDescription)) { throw new ArgumentException("The series description is not a valid Dicom Long String.", nameof(seriesDescription)); } if (!IsValidDicomLongString(patientID)) { throw new ArgumentException("The patient ID is not a valid Dicom Long String.", nameof(patientID)); } if (!IsValidDicomLongString(studyInstanceID)) { throw new ArgumentException("The study instance ID is not a valid Dicom Long String.", nameof(studyInstanceID)); } var spacingZ = volume.SpacingZ; var imageOrientationPatient = new decimal[6]; var directionColumn1 = volume.Direction.Column(0); var directionColumn2 = volume.Direction.Column(1); imageOrientationPatient[0] = (decimal)directionColumn1.X; imageOrientationPatient[1] = (decimal)directionColumn1.Y; imageOrientationPatient[2] = (decimal)directionColumn1.Z; imageOrientationPatient[3] = (decimal)directionColumn2.X; imageOrientationPatient[4] = (decimal)directionColumn2.Y; imageOrientationPatient[5] = (decimal)directionColumn2.Z; var frameOfReferenceUID = CreateUID().UID; var seriesUID = CreateUID().UID; var sopInstanceUIDs = new DicomUID[volume.DimZ]; // DicomUID.Generate() is not thread safe. We must create unique DicomUID's single threaded. // https://github.com/fo-dicom/fo-dicom/issues/546 for (var i = 0; i < sopInstanceUIDs.Length; i++) { sopInstanceUIDs[i] = CreateUID(); } var results = new DicomFile[volume.DimZ]; Parallel.For(0, volume.DimZ, i => { var sliceLocation = (i * spacingZ) + volume.Origin.Z; var imagePositionPatient = volume.Transform.DataToDicom.Transform(new Point3D(0, 0, i)); var dataset = new DicomDataset() { { DicomTag.ImageType, new[] { "DERIVED", "PRIMARY", "AXIAL" } }, { DicomTag.PatientPosition, "HFS" }, { new DicomOtherWord(DicomTag.PixelData, new MemoryByteBuffer(ExtractSliceAsByteArray(volume, i))) }, { new DicomUniqueIdentifier(DicomTag.SOPInstanceUID, sopInstanceUIDs[i]) }, { new DicomUniqueIdentifier(DicomTag.SeriesInstanceUID, seriesUID) }, { new DicomUniqueIdentifier(DicomTag.PatientID, patientID) }, { new DicomUniqueIdentifier(DicomTag.StudyInstanceUID, studyInstanceID) }, { new DicomUniqueIdentifier(DicomTag.FrameOfReferenceUID, frameOfReferenceUID) }, { new DicomLongString(DicomTag.SeriesDescription, seriesDescription) }, { new DicomUnsignedShort(DicomTag.Columns, (ushort)volume.DimX) }, { new DicomUnsignedShort(DicomTag.Rows, (ushort)volume.DimY) }, { new DicomDecimalString(DicomTag.PixelSpacing, (decimal)volume.SpacingY, (decimal)volume.SpacingX) }, // Note: Spacing X & Y are not the expected way around { new DicomDecimalString(DicomTag.ImagePositionPatient, (decimal)imagePositionPatient.X, (decimal)imagePositionPatient.Y, (decimal)imagePositionPatient.Z) }, { new DicomDecimalString(DicomTag.ImageOrientationPatient, imageOrientationPatient) }, { new DicomDecimalString(DicomTag.SliceLocation, (decimal)sliceLocation) }, { new DicomUnsignedShort(DicomTag.SamplesPerPixel, DicomSeriesInformationValidator.ExpectedSamplesPerPixel) }, { new DicomUnsignedShort(DicomTag.PixelRepresentation, 1) }, { new DicomUnsignedShort(DicomTag.BitsStored, DicomSeriesInformationValidator.ExpectedBitsAllocated) }, { new DicomUnsignedShort(DicomTag.BitsAllocated, DicomSeriesInformationValidator.ExpectedBitsAllocated) }, { new DicomUnsignedShort(DicomTag.HighBit, DicomSeriesInformationValidator.ExpectedBitsAllocated - 1) }, { new DicomCodeString(DicomTag.PhotometricInterpretation, DicomSeriesInformationValidator.ExpectedPhotometricInterpretation) } }; if (modality == ImageModality.CT) { dataset.Add(DicomTag.SOPClassUID, DicomUID.CTImageStorage); dataset.Add(DicomTag.Modality, ImageModality.CT.ToString()); dataset.Add(new DicomItem[] { new DicomDecimalString(DicomTag.RescaleIntercept, 0), new DicomDecimalString(DicomTag.RescaleSlope, 1), }); } else if (modality == ImageModality.MR) { dataset.Add(DicomTag.SOPClassUID, DicomUID.MRImageStorage); dataset.Add(DicomTag.Modality, ImageModality.MR.ToString()); } if (additionalDicomItems != null) { foreach (var item in additionalDicomItems.Clone()) { if (!dataset.Contains(item.Tag)) { dataset.Add(item); } } } results[i] = new DicomFile(dataset); }); return(results); }