/// <summary> /// Writes a slice into the 3-dimensional volume based on the slice information and slice index provided. /// </summary> /// <param name="volume">The 3-dimensional volume.</param> /// <param name="sliceInformation">The slice information.</param> /// <param name="sliceIndex">The slice index the slice information relates to.</param> /// <exception cref="ArgumentException">The provided slice index was outside the volume bounds.</exception> /// <exception cref="InvalidOperationException">The decoded DICOM pixel data was not the expected length.</exception> private static unsafe void WriteSlice(Volume3D <short> volume, SliceInformation sliceInformation, uint sliceIndex) { // Check the provided slice index exists in the volume bounds. if (sliceIndex >= volume.DimZ) { throw new ArgumentException("Attempt to write slice outside the volume.", nameof(sliceIndex)); } var data = GetUncompressedPixelData(sliceInformation.DicomDataset); // Checks the uncompressed data is the correct length. if (data.Length < sizeof(short) * volume.DimXY) { throw new InvalidOperationException($"The decoded DICOM pixel data has insufficient length. Actual: {data.Length} Required: {sizeof(short) * volume.DimXY}"); } if (sliceInformation.SignedPixelRepresentation) { WriteSignedSlice(data, volume, sliceIndex, (int)sliceInformation.HighBit, sliceInformation.RescaleIntercept, sliceInformation.RescaleSlope); } else { WriteUnsignedSlice(data, volume, sliceIndex, (int)sliceInformation.HighBit, sliceInformation.RescaleIntercept, sliceInformation.RescaleSlope); } }
/// <summary> /// Validates that all slices in the provided array matches the reference slice on the following properties: /// 1. SOP Class /// 2. Width /// 3. Height /// 4. Voxel Width /// 5. Voxel Height /// 6. Rescale slope /// 7. Rescale intercept. /// </summary> /// <param name="sliceInformation">The slice information to validate against the reference slice.</param> /// <param name="referenceSlice">The reference slice to match the properties against.</param> /// <exception cref="ArgumentException">A conformance check did not pass between slices.</exception> private static void ValidateSliceInformation(SliceInformation sliceInformation, SliceInformation referenceSlice) { ThrowArgumentExeceptionIfNotEquals(referenceSlice.SopClass, sliceInformation.SopClass, $"Slice at position '{sliceInformation.SlicePosition}' has an inconsistent SOP class."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.Width, sliceInformation.Width, $"Slice at position '{sliceInformation.SlicePosition}' has an inconsistent width."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.Height, sliceInformation.Height, $"Slice at position '{sliceInformation.SlicePosition}' has an inconsistent height."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.VoxelWidthInMillimeters, sliceInformation.VoxelWidthInMillimeters, $"Slice at position '{sliceInformation.SlicePosition}' has an inconsistent voxel width (spacing X)."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.VoxelHeightInMillimeters, sliceInformation.VoxelHeightInMillimeters, $"Slice at position '{sliceInformation.SlicePosition}' has an inconsistent voxel height (spacing Y)."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.RescaleSlope, sliceInformation.RescaleSlope, $"Slice at position '{sliceInformation.RescaleSlope}' has an inconsistent rescale slope."); ThrowArgumentExeceptionIfNotEquals(referenceSlice.RescaleIntercept, sliceInformation.RescaleIntercept, $"Slice at position '{sliceInformation.RescaleIntercept}' has an inconsistent rescale intercept."); }
public void TestVolumeInformationCreateTest() { ushort highBit = 16; var expectedDimX = 54; var expectedDimY = 64; var expectedDimZ = 50; var expectedSpacingX = 3; var expectedSpacingY = 5; var expectedSpacingZ = 4; var expectedOrigin = new Point3D(1, 2, 3); var dicomDatasets = CreateValidDicomDatasetVolume( expectedDimX, expectedDimY, expectedDimZ, expectedSpacingX, expectedSpacingY, expectedSpacingZ, expectedOrigin, DicomUID.CTImageStorage, highBit); var volumeInformation = VolumeInformation.Create(dicomDatasets); Assert.AreEqual(expectedDimX, volumeInformation.Width); Assert.AreEqual(expectedDimY, volumeInformation.Height); Assert.AreEqual(expectedDimZ, volumeInformation.Depth); Assert.AreEqual(expectedSpacingX, volumeInformation.VoxelWidthInMillimeters); Assert.AreEqual(expectedSpacingY, volumeInformation.VoxelHeightInMillimeters); Assert.AreEqual(expectedSpacingZ, volumeInformation.VoxelDepthInMillimeters); Assert.AreEqual(0, volumeInformation.RescaleIntercept); Assert.AreEqual(1, volumeInformation.RescaleSlope); Assert.AreEqual(highBit, volumeInformation.HighBit); Assert.AreEqual(true, volumeInformation.SignedPixelRepresentation); Assert.AreEqual(expectedOrigin.X, volumeInformation.Origin.X); Assert.AreEqual(expectedOrigin.Y, volumeInformation.Origin.Y); Assert.AreEqual(expectedOrigin.Z, volumeInformation.Origin.Z); Assert.AreEqual(Matrix3.CreateIdentity(), volumeInformation.Direction); for (var i = 0; i < dicomDatasets.Length; i++) { Assert.AreEqual(i * expectedSpacingZ + expectedOrigin.Z, volumeInformation.GetSliceInformation(i).SlicePosition); } // Exception testing. dicomDatasets[dicomDatasets.Length - 1].AddOrUpdate(new DicomDecimalString(DicomTag.PixelSpacing, new decimal[] { 0, 0 })); Assert.Throws <ArgumentException>(() => VolumeInformation.Create(dicomDatasets.Take(1))); var sliceInformation = new SliceInformation[1]; Assert.Throws <ArgumentException>(() => VolumeInformation.Create(sliceInformation)); sliceInformation = null; Assert.Throws <ArgumentNullException>(() => VolumeInformation.Create(sliceInformation)); dicomDatasets = null; Assert.Throws <ArgumentNullException>(() => VolumeInformation.Create(dicomDatasets)); }
public void TestSliceInformationValidation() { ushort highBit = 15; var dicomDataset = CreateValidDicomDatasetSlice(5, 5, 1, 1, new Point3D(), DicomUID.CTImageStorage, highBit); // Valid DICOM slice. DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset), new[] { dicomDataset.InternalTransferSyntax }); // Invalid supported transfer syntax Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset), new[] { DicomTransferSyntax.DeflatedExplicitVRLittleEndian })); // Add LUT Sequence dicomDataset.Add(new DicomSequence(DicomTag.ModalityLUTSequence, new DicomDataset[0])); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); // Remove LUT Sequence and set bits allocated to not 16 dicomDataset.Remove(DicomTag.ModalityLUTSequence); DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset)); dicomDataset.AddOrUpdate(new DicomUnsignedShort(DicomTag.BitsAllocated, 12)); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); // Set bits allocated to 16, and updated photometric interpation to not MONOCHROME2 dicomDataset.AddOrUpdate(new DicomUnsignedShort(DicomTag.BitsAllocated, 16)); DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset)); dicomDataset.AddOrUpdate(new DicomCodeString(DicomTag.PhotometricInterpretation, "INVALID")); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); // Set photometric interpation to MONOCHROME2 and change expected samples per pixel dicomDataset.AddOrUpdate(new DicomCodeString(DicomTag.PhotometricInterpretation, DicomSeriesInformationValidator.ExpectedPhotometricInterpretation)); DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset)); dicomDataset.AddOrUpdate(new DicomUnsignedShort(DicomTag.SamplesPerPixel, DicomSeriesInformationValidator.ExpectedSamplesPerPixel + 1)); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); // Set samples per pixel to 1 and change the modality to not CT dicomDataset.AddOrUpdate(new DicomUnsignedShort(DicomTag.SamplesPerPixel, DicomSeriesInformationValidator.ExpectedSamplesPerPixel)); DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset)); dicomDataset.AddOrUpdate(DicomTag.Modality, DicomConstants.MRModality); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); // Set modality to CT and change the bits stored to highbit + 2 dicomDataset.AddOrUpdate(DicomTag.Modality, DicomConstants.CTModality); DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset)); dicomDataset.AddOrUpdate(new DicomUnsignedShort(DicomTag.BitsStored, (ushort)(highBit + 2))); Assert.Throws <ArgumentException>(() => DicomSeriesInformationValidator.ValidateSliceInformation(SliceInformation.Create(dicomDataset))); }
/// <summary> /// Validates the slice information. This method will check: /// 1. The dataset has a supported transfer syntax. /// 2. Does not have a 'ModalityLUTSequence' attribute in the dataset. /// 3. Has 16 bits allocated. /// 4. Has 'MONOCHROME2' as the photometric interpretation. /// 5. Only has 1 sample (channel) per pixel. /// 6. Validates the SOP class of the dataset is CT or MR and has the correct matching 'Modality' attribute. /// 7. Validates the 'BitsStored' attribute is 1 + the 'HighBit' attribute. /// </summary> /// <param name="dataset">The dataset to build the slice information from.</param> /// <param name="supportedTransferSyntaxes">The list of supported transfer syntaxes or empty if you do not wish to validate against this.</param> /// <exception cref="ArgumentNullException">The provided slice information was null.</exception> /// <exception cref="ArgumentException">The provided DICOM dataset did not have a required attribute or was not of the correct value.</exception> public static void ValidateSliceInformation( SliceInformation sliceInformation, IReadOnlyCollection <DicomTransferSyntax> supportedTransferSyntaxes = null) { sliceInformation = sliceInformation ?? throw new ArgumentNullException(nameof(sliceInformation)); var dataset = sliceInformation.DicomDataset; // 1. If supported transfer syntaxes have been provided, validate the dataset has one of these. if (supportedTransferSyntaxes != null && !supportedTransferSyntaxes.Contains(dataset.InternalTransferSyntax)) { throw new ArgumentException($"The DICOM dataset has an unsupported storage transfer syntax type {dataset.InternalTransferSyntax}. Expected: {string.Join("/ ", supportedTransferSyntaxes.Select(x => x.ToString()))}"); } // 2. A dataset should not have the 'ModalityLUTSequence' tag. if (dataset.Contains(DicomTag.ModalityLUTSequence)) { throw new ArgumentException("The DICOM dataset should not have the 'ModalityLUTSequence' attribute.", nameof(dataset)); } // 3. Check if the dataset has the 'BitsAllocated' tag and is set to 16. ThrowArgumentExeceptionIfNotEquals(ExpectedBitsAllocated, dataset.GetRequiredDicomAttribute <int>(DicomTag.BitsAllocated), "The DICOM dataset has an unsupported value for the 'BitsAllocated' attribute."); // 4. A dataset should have 'MONOCHROME2' for the photometric interpretation (i.e. is gray-scale). ThrowArgumentExeceptionIfNotEquals(ExpectedPhotometricInterpretation, DicomExtensions.DicomTrim(dataset.GetRequiredDicomAttribute <string>(DicomTag.PhotometricInterpretation)), "The DICOM dataset has an unsupported value for the 'PhotometricInterpretation' attribute."); // 5. A slice should only have one sample per pixel (1 channel at each pixel). ThrowArgumentExeceptionIfNotEquals(ExpectedSamplesPerPixel, dataset.GetRequiredDicomAttribute <int>(DicomTag.SamplesPerPixel), "The DICOM dataset has an unsupported value for the 'SamplesPerPixel' attribute."); // 6. Validate the SOP class of the slice is either MR/ CT and has the correct matching 'Modality' attribute. var modality = dataset.GetRequiredDicomAttribute <string>(DicomTag.Modality); if (sliceInformation.SopClass == DicomUID.CTImageStorage) { ThrowArgumentExeceptionIfNotEquals(DicomConstants.CTModality, modality, "The DICOM dataset has an 'CTImageStorage' SOP class but does not have CT for the 'Modality' attribute."); } if (sliceInformation.SopClass == DicomUID.MRImageStorage) { ThrowArgumentExeceptionIfNotEquals(DicomConstants.MRModality, modality, "The DICOM dataset has an 'MRImageStorage' SOP class but does not have MR for the 'Modality' attribute."); } // 7. Check the high bit against the bit stored. ThrowArgumentExeceptionIfNotEquals((int)sliceInformation.HighBit + 1, dataset.GetRequiredDicomAttribute <int>(DicomTag.BitsStored), $"The DICOM dataset has an unsupported value for the 'BitsStored' attribute. High bit value: {sliceInformation.HighBit}."); }
public void TestSliceInformationCreateTest() { ushort highBit = 16; var expectedDimX = 54; var expectedDimY = 64; var expectedSpacingX = 3; var expectedSpacingY = 5; var expectedOrigin = new Point3D(1, 2, 3); // Create a CT DICOM dataset. var dicomDataset = CreateDicomDatasetSlice(expectedDimX, expectedDimY, expectedSpacingX, expectedSpacingY, expectedOrigin, DicomUID.CTImageStorage, highBit); var sliceInformation = SliceInformation.Create(dicomDataset); Assert.AreEqual(expectedDimX, sliceInformation.Width); Assert.AreEqual(expectedDimY, sliceInformation.Height); Assert.AreEqual(expectedSpacingX, sliceInformation.VoxelWidthInMillimeters); Assert.AreEqual(expectedSpacingY, sliceInformation.VoxelHeightInMillimeters); Assert.AreEqual(3, sliceInformation.SlicePosition); Assert.AreEqual(0, sliceInformation.RescaleIntercept); Assert.AreEqual(1, sliceInformation.RescaleSlope); Assert.AreEqual(highBit, sliceInformation.HighBit); Assert.AreEqual(true, sliceInformation.SignedPixelRepresentation); Assert.AreEqual(DicomUID.CTImageStorage, sliceInformation.SopClass); Assert.AreEqual(expectedOrigin.X, sliceInformation.Origin.X); Assert.AreEqual(expectedOrigin.Y, sliceInformation.Origin.Y); Assert.AreEqual(expectedOrigin.Z, sliceInformation.Origin.Z); Assert.AreEqual(Matrix3.CreateIdentity(), sliceInformation.Direction); Assert.AreEqual(dicomDataset, sliceInformation.DicomDataset); // Test null argument exception Assert.Throws <ArgumentNullException>(() => SliceInformation.Create(null)); // Remove the columns property dicomDataset.AddOrUpdate(new DicomDecimalString(DicomTag.PixelSpacing, expectedSpacingY, expectedSpacingX)); dicomDataset.Remove(DicomTag.Columns); Assert.Throws <ArgumentException>(() => SliceInformation.Create(dicomDataset)); }