Beispiel #1
0
        /// <summary>
        /// Analyse all DICOM files in the given folder and attempt to construct a volume for the given seriesUID
        /// </summary>
        /// <param name="pathFolder">The absolute path to the folder containing the DICOM files</param>
        /// <param name="seriesUID">The DICOM Series UID you wish to load</param>
        /// <param name="acceptanceTests">An implementation of IVolumeGeometricAcceptanceTest defining the geometric constraints of your application</param>
        /// <param name="loadStructuresIfExists">True if rt-structures identified in the folder and referencing seriesUID should be loaded</param>
        /// <param name="supportLossyCodecs">If you wish to accept lossy encodings of image pixel data</param>
        /// <returns></returns>
        public static async Task <VolumeLoaderResult> LoadDicomSeriesInFolderAsync(
            string pathFolder, string seriesUID, IVolumeGeometricAcceptanceTest acceptanceTests, bool loadStructuresIfExists = true, bool supportLossyCodecs = true)
        {
            var dfc = await DicomFileSystemSource.Build(pathFolder);

            var pSeriesUID = DicomUID.Parse(seriesUID);

            return(LoadDicomSeries(dfc, pSeriesUID, acceptanceTests, loadStructuresIfExists, supportLossyCodecs));
        }
Beispiel #2
0
        /// <summary>
        /// Analyse all DICOM files in the given folder and attempt to construct all volumes for CT and MR series therein.
        /// </summary>
        /// <param name="pathFolder">The absolute path to the folder containing the DICOM files</param>
        /// <param name="acceptanceTests">An implementation of IVolumeGeometricAcceptanceTest defining the geometric constraints of your application</param>
        /// <param name="loadStructuresIfExists">True if rt-structures identified in the folder and referencing a volume should be loaded</param>
        /// <param name="supportLossyCodecs">If you wish to accept lossy encodings of image pixel data</param>
        /// <returns>A list of volume loading results for the specified folder</returns>
        public static async Task <IList <VolumeLoaderResult> > LoadAllDicomSeriesInFolderAsync(
            string pathFolder, IVolumeGeometricAcceptanceTest acceptanceTests, bool loadStructuresIfExists = true, bool supportLossyCodecs = true)
        {
            var stopwatch = Stopwatch.StartNew();
            var dfc       = await DicomFileSystemSource.Build(pathFolder);

            stopwatch.Stop();
            Trace.TraceInformation($"Analysing folder structure took: {stopwatch.ElapsedMilliseconds} ms");

            return(LoadAllDicomSeries(dfc, acceptanceTests, loadStructuresIfExists, supportLossyCodecs));
        }
        /// <summary>
        /// Attempt to construct a 3-dimensional volume instance from the provided set of DICOM series files.
        /// </summary>
        /// <param name="dicomDatasets">The collection of DICOM datasets.</param>
        /// <param name="acceptanceTest">An implmentation of IVolumeGeometricAcceptanceTest expressing the geometric tollerances required by your application</param>
        /// <param name="supportLossyCodecs">true if it is appropriate for your application to support lossy pixel encodings</param>
        /// <returns>The created 3-dimensional volume.</returns>
        /// <exception cref="ArgumentNullException">The DICOM datasets or acceptance test is null.</exception>
        /// <exception cref="ArgumentException">A volume could not be formed from the provided DICOM series datasets.</exception>
        public static Volume3D <short> BuildVolume(
            IEnumerable <DicomDataset> dicomDatasets,
            IVolumeGeometricAcceptanceTest acceptanceTest,
            bool supportLossyCodecs)
        {
            dicomDatasets  = dicomDatasets ?? throw new ArgumentNullException(nameof(dicomDatasets));
            acceptanceTest = acceptanceTest ?? throw new ArgumentNullException(nameof(acceptanceTest));

            // 1. Construct the volume information: this requires a minimum set of DICOM tags in each dataset.
            var volumeInformation = VolumeInformation.Create(dicomDatasets);

            // 2. Now validate the volume based on the acceptance tests (will throw argument exception on failure).
            DicomSeriesInformationValidator.ValidateVolumeInformation(volumeInformation, acceptanceTest, supportLossyCodecs ? null : SupportedTransferSyntaxes);

            // 3. Now validated, lets extract the pixels as a short array.
            return(DicomSeriesImageReader.BuildVolume(volumeInformation));
        }
Beispiel #4
0
        /// <summary>
        /// Attempt to load all volume for all CT and MR image series within the given DicomFolderContents
        /// </summary>
        /// <param name="dfc">A pre-built description of DICOM contents within a particular folder</param>
        /// <param name="acceptanceTests">An implementation of IVolumeGeometricAcceptanceTest defining the geometric constraints of your application</param>
        /// <param name="loadStructuresIfExists">True if rt-structures identified in the folder and referencing a volume should be loaded</param>
        /// <param name="supportLossyCodecs">If you wish to accept lossy encodings of image pixel data</param>
        /// <returns></returns>
        public static IList <VolumeLoaderResult> LoadAllDicomSeries(
            DicomFolderContents dfc, IVolumeGeometricAcceptanceTest acceptanceTests, bool loadStructuresIfExists, bool supportLossyCodecs)
        {
            var stopwatch = Stopwatch.StartNew();

            var resultList = new List <VolumeLoaderResult>();

            foreach (var s in dfc.Series)
            {
                if (s.SeriesUID != null)
                {
                    resultList.Add(LoadDicomSeries(dfc, s.SeriesUID, acceptanceTests, loadStructuresIfExists, supportLossyCodecs));
                }
            }

            stopwatch.Stop();
            Trace.TraceInformation($"Reading all DICOM series took: {stopwatch.ElapsedMilliseconds} ms");
            return(resultList);
        }
Beispiel #5
0
        /// <summary>
        /// Validates the provided volume information in accordance with the provided volume geometric acceptance test and
        /// that every slice in the volume is valid.
        /// This will check:
        ///     1. Validates each slice using the validate slice information method (and will use the supported transfer syntaxes if provided).
        ///     2. Grid conformance of the volume.
        ///     3. Slice spacing conformance for each slice.
        ///     4. Executes the propose method of the acceptance test.
        /// </summary>
        /// <param name="volumeInformation">The volume information.</param>
        /// <param name="volumeGeometricAcceptanceTest">The volume geometric acceptance test.</param>
        /// <param name="supportedTransferSyntaxes">The supported transfer syntaxes or null if we do not want to check against this.</param>
        /// <exception cref="ArgumentNullException">The volume information or acceptance test is null.</exception>
        /// <exception cref="ArgumentException">An acceptance test did not pass.</exception>
        public static void ValidateVolumeInformation(
            VolumeInformation volumeInformation,
            IVolumeGeometricAcceptanceTest volumeGeometricAcceptanceTest,
            IReadOnlyCollection <DicomTransferSyntax> supportedTransferSyntaxes = null)
        {
            volumeInformation             = volumeInformation ?? throw new ArgumentNullException(nameof(volumeInformation));
            volumeGeometricAcceptanceTest = volumeGeometricAcceptanceTest ?? throw new ArgumentNullException(nameof(volumeGeometricAcceptanceTest));

            // 1. Validate each slice.
            for (var i = 0; i < volumeInformation.Depth; i++)
            {
                // Validate the DICOM tags of each slice.
                ValidateSliceInformation(volumeInformation.GetSliceInformation(i), supportedTransferSyntaxes);

                if (i > 0)
                {
                    // Validate the slice information is consistent across slices using the first slice as a reference.
                    ValidateSliceInformation(volumeInformation.GetSliceInformation(i), volumeInformation.GetSliceInformation(0));
                }
            }

            // 2. + 3. Check the slice and grid conformance of the volume information.
            CheckGridConformance(volumeInformation, volumeGeometricAcceptanceTest);
            CheckSliceSpacingConformance(volumeInformation, volumeGeometricAcceptanceTest);

            // 4. Run acceptance testing.
            var acceptanceErrorMessage = string.Empty;

            if (!volumeGeometricAcceptanceTest.Propose(
                    volumeInformation.SopClass,
                    volumeInformation.Origin,
                    volumeInformation.Direction,
                    new Point3D(volumeInformation.VoxelWidthInMillimeters, volumeInformation.VoxelHeightInMillimeters, volumeInformation.Depth),
                    out acceptanceErrorMessage))
            {
                throw new ArgumentException(acceptanceErrorMessage, nameof(volumeInformation));
            }
        }
Beispiel #6
0
        /// <summary>
        /// Checks the grid conformance of the provided volume information based on the provided geometric acceptance test.
        /// </summary>
        /// <param name="volumeInformation">The volume information.</param>
        /// <param name="acceptanceTest">The acceptance test.</param>
        /// <exception cref="ArgumentNullException">The volume information or acceptance test was null.</exception>
        /// <exception cref="ArgumentException">The series did not conform to a regular grid.</exception>
        private static void CheckGridConformance(VolumeInformation volumeInformation, IVolumeGeometricAcceptanceTest acceptanceTest)
        {
            volumeInformation = volumeInformation ?? throw new ArgumentNullException(nameof(volumeInformation));
            acceptanceTest    = acceptanceTest ?? throw new ArgumentNullException(nameof(acceptanceTest));

            var scales = Matrix3.Diag(
                volumeInformation.VoxelWidthInMillimeters,
                volumeInformation.VoxelHeightInMillimeters,
                volumeInformation.VoxelDepthInMillimeters);

            if (volumeInformation.Depth != volumeInformation.Depth)
            {
                throw new ArgumentException("Mismatch between depth and number of slices.", nameof(volumeInformation));
            }

            for (int z = 0; z < volumeInformation.Depth; z++)
            {
                var sliceInformation = volumeInformation.GetSliceInformation(z);
                var sliceScales      = Matrix3.Diag(sliceInformation.VoxelWidthInMillimeters, sliceInformation.VoxelHeightInMillimeters, 0);
                var sliceOrientation = Matrix3.FromColumns(sliceInformation.Direction.Column(0), sliceInformation.Direction.Column(1), new Point3D(0, 0, 0));

                // Check corners of each slice only
                for (uint y = 0; y < sliceInformation.Height; y += sliceInformation.Height - 1)
                {
                    for (uint x = 0; x < sliceInformation.Width; x += sliceInformation.Width - 1)
                    {
                        var point = new Point3D(x, y, z);
                        var patientCoordViaSliceFrame = sliceOrientation * sliceScales * point + sliceInformation.Origin;
                        var patientCoordViaGridFrame  = volumeInformation.Direction * scales * point + volumeInformation.Origin;

                        if (!acceptanceTest.AcceptPositionError(volumeInformation.SopClass, patientCoordViaSliceFrame, patientCoordViaGridFrame))
                        {
                            throw new ArgumentException("The series did not conform to a regular grid.", nameof(volumeInformation));
                        }
                    }
                }
            }
        }
Beispiel #7
0
        /// <summary>
        /// Checks the slice spacing conformance based on the acceptance test.
        /// </summary>
        /// <param name="volumeInformation">The volume information.</param>
        /// <param name="acceptanceTest">The acceptance test.</param>
        /// <exception cref="ArgumentNullException">The volume information or acceptance test was null.</exception>
        /// <exception cref="ArgumentException">The acceptance test did not pass for a slice.</exception>
        private static void CheckSliceSpacingConformance(VolumeInformation volumeInformation, IVolumeGeometricAcceptanceTest acceptanceTest)
        {
            for (var i = 1; i < volumeInformation.Depth; i++)
            {
                var spacing = volumeInformation.GetSliceInformation(i).SlicePosition - volumeInformation.GetSliceInformation(i - 1).SlicePosition;

                if (!acceptanceTest.AcceptSliceSpacingError(volumeInformation.SopClass, spacing, volumeInformation.VoxelDepthInMillimeters))
                {
                    throw new ArgumentException($"The spacing between slice {i - 1} and {i} was inconsistent.");
                }
            }
        }
Beispiel #8
0
        /// <summary>
        /// Attempt to load a volume from the given SeriesUID for the given DicomFolderContents
        /// </summary>
        /// <param name="dfc">A pre-built description of DICOM contents within a particular folder</param>
        /// <param name="seriesUID">The DICOM seriesUID you wish to construct a volume for</param>
        /// <param name="acceptanceTests">An implementation of IVolumeGeometricAcceptanceTest defining the geometric constraints of your application</param>
        /// <param name="loadStructuresIfExists">True if rt-structures identified in the folder and referencing seriesUID should be loaded</param>
        /// <param name="supportLossyCodecs">If you wish to accept lossy encodings of image pixel data</param>
        /// <returns></returns>
        private static VolumeLoaderResult LoadDicomSeries(
            DicomFolderContents dfc, DicomUID seriesUID, IVolumeGeometricAcceptanceTest acceptanceTests, bool loadStructuresIfExists, bool supportLossyCodecs)
        {
            try
            {
                var dicomSeriesContent = dfc.Series.FirstOrDefault((s) => s.SeriesUID == seriesUID);

                var warnings = new List <string>();
                RadiotherapyStruct rtStruct = null;

                if (dicomSeriesContent != null)
                {
                    var volumeData = DicomSeriesReader.BuildVolume(dicomSeriesContent.Content.Select(x => x.File.Dataset), acceptanceTests, supportLossyCodecs);

                    if (volumeData != null && loadStructuresIfExists)
                    {
                        var rtStructData = dfc.RTStructs.FirstOrDefault(rt => rt.SeriesUID == seriesUID);
                        if (rtStructData != null)
                        {
                            if (rtStructData.Content.Count == 1)
                            {
                                var rtStructAndWarnings = RtStructReader.LoadContours(
                                    rtStructData.Content.First().File.Dataset,
                                    volumeData.Transform.DicomToData,
                                    seriesUID.UID,
                                    null,
                                    false);

                                rtStruct = rtStructAndWarnings.Item1;

                                var warning = rtStructAndWarnings.Item2;

                                if (!string.IsNullOrEmpty(warning))
                                {
                                    warnings.Add(warning);
                                }
                            }
                            else if (rtStructData.Content.Count > 1)
                            {
                                warnings.Add("There is more than 1 RT STRUCT referencing this series - skipping structure set load");
                            }
                        }
                    }
                    var dicomIdentifiers = dicomSeriesContent.Content.Select((v) => DicomIdentifiers.ReadDicomIdentifiers(v.File.Dataset)).ToArray();

                    if (rtStruct == null)
                    {
                        rtStruct = RadiotherapyStruct.CreateDefault(dicomIdentifiers);
                    }

                    var result = new MedicalVolume(
                        volumeData,
                        dicomIdentifiers,
                        dicomSeriesContent.Content.Select((d) => d.Path).ToArray(),
                        rtStruct);

                    return(new VolumeLoaderResult(seriesUID.UID, result, null, warnings));
                }
                throw new Exception("Could not find that series");
            }
            catch (Exception oops)
            {
                return(new VolumeLoaderResult(seriesUID.UID, null, oops, new List <string>()));
            }
        }
Beispiel #9
0
        /// <summary>
        /// Expects path to point to a folder containing exactly 1 volume.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="acceptanceTests"></param>
        /// <returns></returns>
        public static async Task <MedicalVolume> LoadSingleDicomSeriesAsync(string path, IVolumeGeometricAcceptanceTest acceptanceTests)
        {
            var attributes = File.GetAttributes(path);

            if ((attributes & FileAttributes.Directory) != FileAttributes.Directory)
            {
                throw new ArgumentException("Folder path was expected.");
            }

            var results = await LoadAllDicomSeriesInFolderAsync(path, acceptanceTests);

            if (results.Count != 1)
            {
                throw new Exception("Folder contained multiple series.");
            }

            if (results[0].Error != null)
            {
                throw new Exception("Error loading DICOM series.", results[0].Error);
            }

            return(results[0].Volume);
        }