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