Example #1
0
        /// <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);
            }
        }
Example #2
0
 /// <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)));
        }
Example #5
0
        /// <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));
        }