/// <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);
        }