/// <summary> /// Builds a 3-dimensional volume from the provided volume information. /// This method will parallelise voxel extraction per slice. /// </summary> /// <param name="volumeInformation">The volume information.</param> /// <param name="maxDegreeOfParallelism">The maximum degrees of parallelism when extracting voxel data from the DICOM datasets.</param> /// <returns>The 3-dimensional volume.</returns> /// <exception cref="ArgumentNullException">The provided volume information was null.</exception> /// <exception cref="InvalidOperationException">The decoded DICOM pixel data was not the expected length.</exception> public static Volume3D <short> BuildVolume( VolumeInformation volumeInformation, uint maxDegreeOfParallelism = 100) { volumeInformation = volumeInformation ?? throw new ArgumentNullException(nameof(volumeInformation)); // Allocate the array for reading the volume data. var result = new Volume3D <short>( (int)volumeInformation.Width, (int)volumeInformation.Height, (int)volumeInformation.Depth, volumeInformation.VoxelWidthInMillimeters, volumeInformation.VoxelHeightInMillimeters, volumeInformation.VoxelDepthInMillimeters, volumeInformation.Origin, volumeInformation.Direction); Parallel.For( 0, volumeInformation.Depth, new ParallelOptions() { MaxDegreeOfParallelism = (int)maxDegreeOfParallelism }, i => WriteSlice(result, volumeInformation.GetSliceInformation((int)i), (uint)i)); return(result); }
/// <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> /// 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)); } } } } }