Example #1
0
        /// <summary>
        /// Compares two instances of Volume3D, and throws an exception if they don't match exactly. If they don't match,
        /// the first 20 differences are printed out on the console, as well as statistics about the distinct values contained
        /// in the two volumes.
        /// </summary>
        /// <param name="expected">The expected volume.</param>
        /// <param name="actual">The actual volume.</param>
        /// <param name="loggingMessage">A string prefix for the messages created in Asserts (usually a file name pointing to the source</param>
        /// <param name="logHistogramOnMatch">Whether to print logs if volumes match</param>
        /// of the discrepancy)</param>
        public static void AssertVolumesMatch <T>(
            Volume3D <T> expected,
            Volume3D <T> actual,
            string loggingPrefix = "") where T : IEquatable <T>
        {
            AssertVolumeSizeAndSpacingMatches(expected, actual, loggingPrefix);
            var numDifferences = 0;
            var maxDifferences = 20;

            for (var index = 0; index < expected.Array.Length; index++)
            {
                var e = expected[index];
                var a = actual[index];
                if (!e.Equals(a) && numDifferences < maxDifferences)
                {
                    numDifferences++;
                    Console.WriteLine($"Difference at index {index}: Expected {e}, actual {a}");
                    if (numDifferences >= maxDifferences)
                    {
                        Console.WriteLine($"Stopping at {maxDifferences} differences");
                    }
                }
            }
            if (numDifferences > 0)
            {
                Assert.Fail($"{loggingPrefix}: Volumes are different. Console has detailed diff.");
            }
        }
 /// <summary>
 /// Creates a new instance of the class, from a binary mask. The contour is extracted from the mask
 /// using the default settings: Background 0, foreground 1, axial slices.
 /// </summary>
 /// <param name="name">The name of the anatomical structure that is represented by the contour.</param>
 /// <param name="color">The color that should be used to render the contour.</param>
 /// <param name="mask">The binary mask that represents the anatomical structure.</param>
 /// <exception cref="ArgumentNullException">The contour name or mask was null.</exception>
 public ContourRenderingInformation(string name, RGBColor color, Volume3D <byte> mask)
 {
     Name    = name ?? throw new ArgumentNullException(nameof(name));
     Color   = color;
     mask    = mask ?? throw new ArgumentNullException(nameof(mask));
     Contour = ExtractContours.ContoursWithHolesPerSlice(mask);
 }
Example #3
0
        /// <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);
        }
Example #4
0
        public void FloodFillTest3D()
        {
            var actual = new Volume3D <byte>(3, 3, 3);

            // 0 X 0
            // X 0 X
            // 0 X 0
            for (int i = 0; i < 3; i++)
            {
                actual[1, 0, i] = 1;
                actual[0, 1, i] = 1;
                actual[2, 1, i] = 1;
                actual[1, 2, i] = 1;
            }

            var expected = actual.Copy();

            expected[1, 1, 0] = 1;
            expected[1, 1, 1] = 1;
            expected[1, 1, 2] = 1;

            actual.FillHoles();

            CollectionAssert.AreEqual(expected.Array, actual.Array);
        }
Example #5
0
        public void FloodFillTest3()
        {
            var actual = new Volume3D <byte>(3, 3, 3);

            // X X X
            // X 0 X
            // X X X
            for (var i = 0; i < actual.Length; i++)
            {
                actual[i] = 1;
            }

            actual[1, 1, 0] = 0;
            actual[1, 1, 1] = 0;
            actual[1, 1, 2] = 0;

            var expected = actual.Copy();

            expected[1, 1, 0] = 1;
            expected[1, 1, 1] = 1;
            expected[1, 1, 2] = 1;

            actual.FillHoles();

            CollectionAssert.AreEqual(expected.Array, actual.Array);
        }
        private static Volume3D <byte> ToVolume3D(
            this ContoursPerSlice contours,
            double spacingX,
            double spacingY,
            double spacingZ,
            Point3D origin,
            Matrix3 direction,
            Region3D <int> roi)
        {
            ContoursPerSlice subContours = new ContoursPerSlice(
                contours.Where(x => x.Value != null).Select(
                    contour =>
                    new KeyValuePair <int, IReadOnlyList <ContourPolygon> >(
                        contour.Key - roi.MinimumZ,
                        contour.Value.Select(x =>
                                             new ContourPolygon(
                                                 x.ContourPoints.Select(
                                                     point => new PointF(point.X - roi.MinimumX, point.Y - roi.MinimumY)).ToArray(), 0))
                        .ToList())).ToDictionary(x => x.Key, y => y.Value));

            var result = new Volume3D <byte>(roi.MaximumX - roi.MinimumX + 1, roi.MaximumY - roi.MinimumY + 1, roi.MaximumZ - roi.MinimumZ + 1, spacingX, spacingY, spacingZ, origin, direction);

            result.Fill(subContours, ModelConstants.MaskForegroundIntensity);

            return(result);
        }
        /// <summary>
        /// Converts a medical scan, and a set of binary masks, into a Dicom representation.
        /// The returned set of Dicom files will have files for all slices of the scan, and an RtStruct
        /// file containing the contours that were derived from the masks. The RtStruct file will be the first
        /// entry in the returned list of Dicom files.
        /// Use with extreme care - many Dicom elements have to be halluzinated here, and there's no
        /// guarantee that the resulting Dicom will be usable beyond what is needed in InnerEye.
        /// </summary>
        /// <param name="scan">The medical scan.</param>
        /// <param name="imageModality">The image modality through which the scan was acquired.</param>
        /// <param name="contours">A list of contours for individual anatomical structures, alongside
        /// name and rendering color.</param>
        /// <param name="seriesDescription">The value to use as the Dicom series description.</param>
        /// <param name="patientID">The patient ID that should be used in the Dicom files. If null,
        /// a randomly generated patient ID will be used.</param>
        /// <param name="studyInstanceID">The study ID that should be used in the Dicom files (DicomTag.StudyInstanceUID). If null,
        /// a randomly generated study ID will be used.</param>
        /// <param name="additionalDicomItems">Additional Dicom items that will be added to each of the slice datasets. This can
        /// be used to pass in additional information like manufacturer.</param>
        /// <returns></returns>
        public static List <DicomFileAndPath> ScanAndContoursToDicom(Volume3D <short> scan,
                                                                     ImageModality imageModality,
                                                                     IReadOnlyList <ContourRenderingInformation> contours,
                                                                     string seriesDescription          = null,
                                                                     string patientID                  = null,
                                                                     string studyInstanceID            = null,
                                                                     DicomDataset additionalDicomItems = null)
        {
            // To create the Dicom identifiers, write the volume to a set of Dicom files first,
            // then read back in.
            // When working with MR scans from the CNN models, it is quite common to see scans that have a large deviation from
            // the 1:1 aspect ratio. Relax that constraint a bit (default is 1.001)
            var scanFiles        = ScanToDicomInMemory(scan, imageModality, seriesDescription, patientID, studyInstanceID, additionalDicomItems);
            var medicalVolume    = MedicalVolumeFromDicom(scanFiles, maxPixelSizeRatioMR: 1.01);
            var dicomIdentifiers = medicalVolume.Identifiers;
            var volumeTransform  = medicalVolume.Volume.Transform;

            medicalVolume = null;
            var rtFile     = ContoursToDicomRtFile(contours, dicomIdentifiers, volumeTransform);
            var dicomFiles = new List <DicomFileAndPath> {
                rtFile
            };

            dicomFiles.AddRange(scanFiles);
            return(dicomFiles);
        }
        private static List <DicomRTContourItem> GetContoursForAllSlices(Volume3D <byte> segmentationMask)
        {
            var listOfPointsPerSlice = segmentationMask.ContoursWithHolesPerSlice();

            var resultList = new List <DicomRTContourItem>();

            foreach (var sliceContours in listOfPointsPerSlice)
            {
                foreach (var contour in sliceContours.Value)
                {
                    var dataToDicom = segmentationMask.Transform.DataToDicom;

                    var allpoints = contour.ContourPoints.SelectMany(
                        p => (dataToDicom * new Point3D(p.X, p.Y, sliceContours.Key)).Data).ToArray();

                    if (allpoints.Length > 0)
                    {
                        var contourImageSeq = new List <DicomRTContourImageItem>();
                        var dicomContour    = new DicomRTContourItem(
                            allpoints,
                            contour.Length,
                            DicomExtensions.ClosedPlanarString,
                            contourImageSeq);
                        resultList.Add(dicomContour);
                    }
                }
            }

            return(resultList);
        }
Example #9
0
        /// <summary>
        /// Writes a slice of unsigned data to the provided volume at the specified index.
        /// Note: This method is unsafe and only uses checking when creating a short value out of each voxel (to check for overflows).
        /// </summary>
        /// <param name="data">The uncompressed unsigned pixel data.</param>
        /// <param name="volume">The volume to write the slice into.</param>
        /// <param name="sliceIndex">The index of the slice to write.</param>
        /// <param name="highBit">The high bit value for reading the pixel information.</param>
        /// <param name="rescaleIntercept">The rescale intercept of the pixel data.</param>
        /// <param name="rescaleSlope">The rescale slope of the pixel data.</param>
        private static unsafe void WriteUnsignedSlice(
            byte[] data,
            Volume3D <short> volume,
            uint sliceIndex,
            int highBit,
            double rescaleIntercept,
            double rescaleSlope)
        {
            // Construct a binary mask such that all bit positions to the right of highbit and highbit
            // are masked in, and all bit positions to the left are masked out.
            var mask = (2 << highBit) - 1;

            fixed(short *volumePointer = volume.Array)
            fixed(byte *dataPtr = data)
            {
                var slicePointer = volumePointer + volume.DimXY * sliceIndex;
                var dataPointer  = dataPtr;

                for (var y = 0; y < volume.DimY; y++)
                {
                    for (var x = 0; x < volume.DimX; x++, dataPointer += 2, slicePointer++)
                    {
                        var value = (ushort)((*dataPointer | *(dataPointer + 1) << 8) & mask);

                        // Force checked so out-of-range values will cause overflow exception.
                        checked
                        {
                            *slicePointer = (short)Math.Round(rescaleSlope * value + rescaleIntercept);
                        }
                    }
                }
            }
        }
Example #10
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);
            }
        }
        private static ContoursPerSlice GenerateContoursPerSlice(
            Volume3D <byte> volume,
            bool fillContours,
            byte foregroundId,
            SliceType sliceType,
            bool filterEmptyContours,
            Region3D <int> regionOfInterest,
            ContourSmoothingType axialSmoothingType)
        {
            var region = regionOfInterest ?? new Region3D <int>(0, 0, 0, volume.DimX - 1, volume.DimY - 1, volume.DimZ - 1);

            int startPoint;
            int endPoint;

            // Only smooth the output on the axial slices
            var smoothingType = axialSmoothingType;

            switch (sliceType)
            {
            case SliceType.Axial:
                startPoint = region.MinimumZ;
                endPoint   = region.MaximumZ;
                break;

            case SliceType.Coronal:
                startPoint    = region.MinimumY;
                endPoint      = region.MaximumY;
                smoothingType = ContourSmoothingType.None;
                break;

            case SliceType.Sagittal:
                startPoint    = region.MinimumX;
                endPoint      = region.MaximumX;
                smoothingType = ContourSmoothingType.None;
                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(sliceType), sliceType, null);
            }

            var numberOfSlices  = endPoint - startPoint + 1;
            var arrayOfContours = new Tuple <int, IReadOnlyList <ContourPolygon> > [numberOfSlices];

            for (var i = 0; i < arrayOfContours.Length; i++)
            {
                var z        = startPoint + i;
                var volume2D = ExtractSlice.Slice(volume, sliceType, z);
                var contours = fillContours ?
                               ContoursFilled(volume2D, foregroundId, smoothingType):
                               ContoursWithHoles(volume2D, foregroundId, smoothingType);

                arrayOfContours[i] = Tuple.Create(z, contours);
            }

            return(new ContoursPerSlice(
                       arrayOfContours
                       .Where(x => !filterEmptyContours || x.Item2.Count > 0)
                       .ToDictionary(x => x.Item1, x => x.Item2)));
        }
 /// <summary>
 /// Extracts contours from all slices of the given volume, searching for the given foreground value.
 /// Contour extraction will take holes into account.
 /// </summary>
 /// <param name="volume"></param>
 /// <param name="foregroundId">The voxel value that should be used in the contour search as foreground.</param>
 /// <param name="axialSmoothingType">The smoothing that should be applied when going from a point polygon to
 /// contours. This will only affect axial slice, for other slice types no smoothing will be applied.
 /// <param name="sliceType">The type of slice that should be used for contour extraction.</param>
 /// <param name="filterEmptyContours">If true, contours with no points are not extracted.</param>
 /// <param name="regionOfInterest"></param>
 /// <returns></returns>
 public static ContoursPerSlice ContoursWithHolesPerSlice(
     this Volume3D <byte> volume,
     byte foregroundId                       = ModelConstants.MaskForegroundIntensity,
     SliceType sliceType                     = SliceType.Axial,
     bool filterEmptyContours                = true,
     Region3D <int> regionOfInterest         = null,
     ContourSmoothingType axialSmoothingType = ContourSmoothingType.Small)
 => ExtractContours.ContoursWithHolesPerSlice(volume, foregroundId, sliceType, filterEmptyContours, regionOfInterest, axialSmoothingType);
        public void Setup()
        {
            sourceVolume = MedIO.LoadNiftiAsByte(TestNiftiSegmentationLocation);
            Trace.TraceInformation($"Loaded NIFTI from {TestNiftiSegmentationLocation}");

            Assert.AreEqual(NumValidLabels, FillHoles.Length);
            Assert.AreEqual(NumValidLabels, StructureColors.Length);
            Assert.AreEqual(NumValidLabels, StructureNames.Length);
        }
 /// <summary>
 /// Creates a volume that has the same spacing and coordinate system as the reference volume,
 /// and fills all points that fall inside of the contours in the present object with the
 /// default foreground value. The returned volume has its size determined by the given region of interest.
 /// Contour points are transformed using the region of interest.
 /// </summary>
 /// <typeparam name="T"></typeparam>
 /// <param name="contours">The contours to use for filling.</param>
 /// <param name="refVolume3D">The reference volume to copy spacing and coordinate system from.</param>
 /// <param name="regionOfInterest"></param>
 /// <returns></returns>
 public static Volume3D <byte> ToVolume3D <T>(this ContoursPerSlice contours, Volume3D <T> refVolume3D, Region3D <int> regionOfInterest)
 {
     return(contours.ToVolume3D(
                refVolume3D.SpacingX,
                refVolume3D.SpacingY,
                refVolume3D.SpacingZ,
                refVolume3D.Origin,
                refVolume3D.Direction,
                regionOfInterest));
 }
Example #15
0
 /// <summary>
 /// Applies flood filling to all holes in all Z slices of the given volume.
 /// </summary>
 /// <param name="volume"></param>
 /// <param name="foregroundId"></param>
 /// <param name="backgroundId"></param>
 public static void FloodFillHoles(
     Volume3D <byte> volume,
     byte foregroundId = ModelConstants.MaskForegroundIntensity,
     byte backgroundId = ModelConstants.MaskBackgroundIntensity)
 {
     Parallel.For(0, volume.DimZ, sliceZ =>
     {
         FloodFillHoles(volume.Array, volume.DimX, volume.DimY, volume.DimZ, sliceZ, foregroundId, backgroundId);
     });
 }
Example #16
0
 /// <summary>
 /// Modifies the present volume by filling all points that fall inside of the given contours,
 /// using the provided fill value. Contours are filled on axial slices.
 /// </summary>
 /// <typeparam name="T"></typeparam>
 /// <param name="volume">The volume that should be modified.</param>
 /// <param name="contours">The contours per axial slice.</param>
 /// <param name="value">The value that should be used to fill all points that fall inside of
 /// the given contours.</param>
 public static void FillContours <T>(Volume3D <T> volume, ContoursPerSlice contours, T value)
 {
     foreach (var contourPerSlice in contours)
     {
         foreach (var contour in contourPerSlice.Value)
         {
             Fill(contour.ContourPoints, volume.Array, volume.DimX, volume.DimY, volume.DimZ, contourPerSlice.Key, value);
         }
     }
 }
 /// <summary>
 /// Creates a volume that has the same size, spacing, and coordinate system as the reference volume,
 /// and fills all points that fall inside of the contours in the present object with the default foreground value.
 /// </summary>
 /// <typeparam name="T"></typeparam>
 /// <param name="contours">The contours to use for filling.</param>
 /// <param name="refVolume3D">The reference volume to copy spacing and coordinate system from.</param>
 /// <returns></returns>
 public static Volume3D <byte> ToVolume3D <T>(this ContoursPerSlice contours, Volume3D <T> refVolume3D)
 {
     return(contours.ToVolume3D(
                refVolume3D.SpacingX,
                refVolume3D.SpacingY,
                refVolume3D.SpacingZ,
                refVolume3D.Origin,
                refVolume3D.Direction,
                refVolume3D.GetFullRegion()));
 }
Example #18
0
 /// <summary>
 /// Compares two instances of Volume3D, and throws an exception if they have different size or spacing.
 /// </summary>
 /// <param name="expected">The expected volume.</param>
 /// <param name="actual">The actual volume.</param>
 /// <param name="loggingMessage">A string prefix for the messages created in Asserts (usually a file name pointing to the source
 /// of the discrepancy)</param>
 public static void AssertVolumeSizeAndSpacingMatches <T>(Volume3D <T> expected, Volume3D <T> actual, string loggingPrefix = "") where T : IEquatable <T>
 {
     Assert.AreEqual(expected.DimX, actual.DimX, $"{loggingPrefix}: X dimension must match");
     Assert.AreEqual(expected.DimY, actual.DimY, $"{loggingPrefix}: Y dimension must match");
     Assert.AreEqual(expected.DimZ, actual.DimZ, $"{loggingPrefix}: Z dimension must match");
     Assert.AreEqual(expected.SpacingX, actual.SpacingX, 1e-5, $"{loggingPrefix}: X spacing must match");
     Assert.AreEqual(expected.SpacingY, actual.SpacingY, 1e-5, $"{loggingPrefix}: Y spacing must match");
     Assert.AreEqual(expected.SpacingZ, actual.SpacingZ, 1e-5, $"{loggingPrefix}: Z spacing must match");
     Assert.AreEqual(expected.Array.Length, actual.Array.Length, $"{loggingPrefix}: The volumes have a different size");
 }
 /// <summary>
 /// Extracts contours from all slices of the given volume, searching for the given foreground value.
 /// Contour extraction will take holes into account.
 /// </summary>
 /// <param name="volume"></param>
 /// <param name="foregroundId">The voxel value that should be used in the contour search as foreground.</param>
 /// <param name="axialSmoothingType">The smoothing that should be applied when going from a point polygon to
 /// contours. This will only affect axial slice, for other slice types no smoothing will be applied.
 /// <param name="sliceType">The type of slice that should be used for contour extraction.</param>
 /// <param name="filterEmptyContours">If true, contours with no points are not extracted.</param>
 /// <param name="regionOfInterest"></param>
 /// <returns></returns>
 public static ContoursPerSlice ContoursFilledPerSlice(
     Volume3D <byte> volume,
     byte foregroundId                       = ModelConstants.MaskForegroundIntensity,
     SliceType sliceType                     = SliceType.Axial,
     bool filterEmptyContours                = true,
     Region3D <int> regionOfInterest         = null,
     ContourSmoothingType axialSmoothingType = ContourSmoothingType.Small)
 {
     return(GenerateContoursPerSlice(volume, true, foregroundId, sliceType, filterEmptyContours, regionOfInterest, axialSmoothingType));
 }
Example #20
0
        /// <summary>
        /// Extracts a slice of a chosen type (orientation) from the present volume, and returns it as a
        /// <see cref="Volume2D"/> instance of correct size. Note that these slices are NOT extracted
        /// according to the patient-centric coordinate system, but in the coordinate system of the volume
        /// alone.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="volume"></param>
        /// <param name="sliceType"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public static Volume2D <T> Slice <T>(Volume3D <T> volume, SliceType sliceType, int index)
        {
            var result = AllocateSlice <T, T>(volume, sliceType);

            if (result != null)
            {
                Extract(volume, sliceType, index, result.Array);
            }

            return(result);
        }
        /// <summary>
        /// Extracts axial contours for the foreground values in the given volume. After extracting the contours,
        /// a check is conducted if the contours truthfully represent the actual volume. This will throw exceptions
        /// for example if the incoming volume has "doughnut shape" structures.
        /// </summary>
        /// <param name="volume">The mask volume to extract contours from.</param>
        /// <param name="maxAbsoluteDifference">The maximum allowed difference in foreground voxels when going
        /// from mask to contours to mask.</param>
        /// <param name="maxRelativeDifference">The maximum allowed relative in foreground voxels (true - rendered)/true
        /// when going from mask to contours to mask.</param>
        /// <returns></returns>
        public static ContoursPerSlice ExtractContoursAndCheck(this Volume3D <byte> volume,
                                                               int?maxAbsoluteDifference    = 10,
                                                               double?maxRelativeDifference = 0.15
                                                               )
        {
            var contours = ExtractContours.ContoursWithHolesPerSlice(
                volume,
                foregroundId: ModelConstants.MaskForegroundIntensity,
                sliceType: SliceType.Axial,
                filterEmptyContours: true,
                regionOfInterest: null,
                axialSmoothingType: ContourSmoothingType.Small);
            var slice = new byte[volume.DimX * volume.DimY];

            void ClearSlice()
            {
                for (var index = 0; index < slice.Length; index++)
                {
                    slice[index] = ModelConstants.MaskBackgroundIntensity;
                }
            }

            foreach (var contourPerSlice in contours)
            {
                var indexZ  = contourPerSlice.Key;
                var offsetZ = indexZ * volume.DimXY;
                ClearSlice();
                foreach (var contour in contourPerSlice.Value)
                {
                    FillPolygon.Fill(contour.ContourPoints, slice,
                                     volume.DimX, volume.DimY, 1, 0, ModelConstants.MaskForegroundIntensity);
                }
                var true1     = 0;
                var rendered1 = 0;
                for (var index = 0; index < slice.Length; index++)
                {
                    if (volume[offsetZ + index] == ModelConstants.MaskForegroundIntensity)
                    {
                        true1++;
                    }
                    if (slice[index] == ModelConstants.MaskForegroundIntensity)
                    {
                        rendered1++;
                    }
                }
                CheckContourRendering(true1, rendered1, maxAbsoluteDifference, maxRelativeDifference, $"Slice z={indexZ}");
            }
            ;

            return(contours);
        }
        private int CountAndValidateVoxelsInLabel(Volume3D <byte> volume)
        {
            int voxelcount = 0;

            for (var i = 0; i < volume.Length; i++)
            {
                voxelcount += volume[i];
                if (volume[i] > 1)
                {
                    Assert.Fail("Invalid data in converted image. Should only be ones or zeroes");
                }
            }
            return(voxelcount);
        }
Example #23
0
        /// <summary>
        /// Construct MedicalVolume from Dicom images
        /// </summary>
        /// <param name="volume"></param>
        /// <param name="identifiers"></param>
        /// <param name="filePaths"></param>
        /// <param name="rtStruct"></param>
        public MedicalVolume(
            Volume3D <short> volume,
            IReadOnlyList <DicomIdentifiers> identifiers,
            IReadOnlyList <string> filePaths,
            RadiotherapyStruct rtStruct)
        {
            Debug.Assert(volume != null);
            Debug.Assert(identifiers != null);
            Debug.Assert(filePaths != null);
            Debug.Assert(rtStruct != null);

            Identifiers = identifiers;
            Volume      = volume;
            FilePaths   = filePaths;
            Struct      = rtStruct;
        }
Example #24
0
        /// <summary>
        /// Extracts a slice of a given type (orientation) from the present volume, and writes it to
        /// the provided array in <paramref name="outVolume"/>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="volume"></param>
        /// <param name="sliceType"></param>
        /// <param name="sliceIndex"></param>
        /// <param name="outVolume"></param>
        /// <param name="skip"></param>
        private static void Extract <T>(Volume3D <T> volume, SliceType sliceType, int sliceIndex, T[] outVolume, int skip = 1)
        {
            switch (sliceType)
            {
            case SliceType.Axial:
                if (sliceIndex < volume.DimZ && outVolume.Length == volume.DimXY * skip)
                {
                    Parallel.For(0, volume.DimY, y =>
                    {
                        for (var x = 0; x < volume.DimX; x++)
                        {
                            outVolume[(x + y * volume.DimX) * skip] = volume[(sliceIndex * volume.DimY + y) * volume.DimX + x];
                        }
                    });
                }

                break;

            case SliceType.Coronal:
                if (sliceIndex < volume.DimY && outVolume.Length == volume.DimZ * volume.DimX * skip)
                {
                    Parallel.For(0, volume.DimZ, z =>
                    {
                        for (var x = 0; x < volume.DimX; x++)
                        {
                            outVolume[(x + z * volume.DimX) * skip] = volume[(z * volume.DimY + sliceIndex) * volume.DimX + x];
                        }
                    });
                }

                break;

            case SliceType.Sagittal:
                if (sliceIndex < volume.DimX && outVolume.Length == volume.DimY * volume.DimZ * skip)
                {
                    Parallel.For(0, volume.DimZ, z =>
                    {
                        for (var y = 0; y < volume.DimY; y++)
                        {
                            outVolume[(y + z * volume.DimY) * skip] = volume[(z * volume.DimY + y) * volume.DimX + sliceIndex];
                        }
                    });
                }

                break;
            }
        }
Example #25
0
        /// <summary>
        /// Converts a multi-label map to a set of binary volumes. If a voxel has value v in the
        /// input image, v >= 1, then the (v-1)th result volume will have set that voxel to 1.
        /// Put another way: The i.th result volume will have voxels non-zero wherever the input
        /// volume had value (i+1).
        /// </summary>
        /// <param name="image">A multi-label input volume.</param>
        /// <param name="numOutputMasks">The number of result volumes that will be generated.
        /// This value must be at least equal to the maximum voxel value in the input volume.</param>
        /// <returns></returns>
        public static Volume3D <byte>[] MultiLabelMapping(Volume3D <byte> image, int numOutputMasks)
        {
            var result = new Volume3D <byte> [numOutputMasks];

            for (var i = 0; i < numOutputMasks; i++)
            {
                result[i] = image.CreateSameSize <byte>();
            }

            for (var i = 0; i < image.Length; ++i)
            {
                if (image[i] != 0 && image[i] <= numOutputMasks)
                {
                    result[image[i] - 1][i] = 1;
                }
            }

            return(result);
        }
        /// <summary>
        /// Saves a medical scan to a set of Dicom files. Each file is saved into a memory stream.
        /// The returned Dicom files have the Path property set to {sliceIndex}.dcm.
        /// Use with extreme care - many Dicom elements have to be halluzinated here, and there's no
        /// guarantee that the resulting Dicom will be usable beyond what is needed in InnerEye.
        /// </summary>
        /// <param name="scan">The medical scan.</param>
        /// <param name="imageModality">The image modality through which the scan was acquired.</param>
        /// <param name="seriesDescription">The series description that should be used in the Dicom files.</param>
        /// <param name="patientID">The patient ID that should be used in the Dicom files. If null,
        /// a randomly generated patient ID will be used.</param>
        /// <param name="studyInstanceID">The study ID that should be used in the Dicom files (DicomTag.StudyInstanceUID). If null,
        /// a randomly generated study ID will be used.</param>
        /// <param name="additionalDicomItems">Additional Dicom items that will be added to each of the slice datasets. This can
        /// be used to pass in additional information like manufacturer.</param>
        /// <returns></returns>
        public static List <DicomFileAndPath> ScanToDicomInMemory(Volume3D <short> scan,
                                                                  ImageModality imageModality,
                                                                  string seriesDescription          = null,
                                                                  string patientID                  = null,
                                                                  string studyInstanceID            = null,
                                                                  DicomDataset additionalDicomItems = null)
        {
            var scanAsDicomFiles = Convert(scan, imageModality, seriesDescription, patientID, studyInstanceID, additionalDicomItems).ToList();
            var dicomFileAndPath = new List <DicomFileAndPath>();

            for (var index = 0; index < scanAsDicomFiles.Count; index++)
            {
                var stream = new MemoryStream();
                scanAsDicomFiles[index].Save(stream);
                stream.Seek(0, SeekOrigin.Begin);
                var dicomFile = DicomFileAndPath.SafeCreate(stream, $"{index}.dcm");
                dicomFileAndPath.Add(dicomFile);
            }
            return(dicomFileAndPath);
        }
        /// <summary>
        /// Extracts a slice from the X/Y plane as a byte array.
        /// </summary>
        /// <param name="volume">The volume to extra the slice from.</param>
        /// <param name="sliceIndex">Index of the slice.</param>
        /// <returns>The extracted X/Y slice as a byte array.</returns>
        private static byte[] ExtractSliceAsByteArray(Volume3D <short> volume, int sliceIndex)
        {
            if (sliceIndex < 0 || sliceIndex >= volume.DimZ)
            {
                throw new ArgumentException(nameof(sliceIndex));
            }

            var result      = new byte[volume.DimXY * 2];
            var resultIndex = 0;

            for (var i = sliceIndex * volume.DimXY; i < (sliceIndex + 1) * volume.DimXY; i++)
            {
                var bytes = BitConverter.GetBytes(volume[i]);

                result[resultIndex++] = bytes[0];
                result[resultIndex++] = bytes[1];
            }

            return(result);
        }
        public static Region3D <int> Dilate <T>(this Region3D <int> region, Volume3D <T> volume, double mmDilationX, double mmDilationY, double mmDilationZ)
        {
            if (region.IsEmpty())
            {
                return(region.Clone());
            }
            var dilatedMinimumX = region.MinimumX - (int)Math.Ceiling(mmDilationX / volume.SpacingX);
            var dilatedMaximumX = region.MaximumX + (int)Math.Ceiling(mmDilationX / volume.SpacingX);

            var dilatedMinimumY = region.MinimumY - (int)Math.Ceiling(mmDilationY / volume.SpacingY);
            var dilatedMaximumY = region.MaximumY + (int)Math.Ceiling(mmDilationY / volume.SpacingY);

            var dilatedMinimumZ = region.MinimumZ - (int)Math.Ceiling(mmDilationZ / volume.SpacingZ);
            var dilatedMaximumZ = region.MaximumZ + (int)Math.Ceiling(mmDilationZ / volume.SpacingZ);

            return(new Region3D <int>(
                       dilatedMinimumX < 0 ? 0 : dilatedMinimumX,
                       dilatedMinimumY < 0 ? 0 : dilatedMinimumY,
                       dilatedMinimumZ < 0 ? 0 : dilatedMinimumZ,
                       dilatedMaximumX >= volume.DimX ? volume.DimX - 1 : dilatedMaximumX,
                       dilatedMaximumY >= volume.DimY ? volume.DimY - 1 : dilatedMaximumY,
                       dilatedMaximumZ >= volume.DimZ ? volume.DimZ - 1 : dilatedMaximumZ));
        }
Example #29
0
        /// <summary>
        /// Writes a slice of signed data to the provided volume at the specified index.
        /// Note: This method is unsafe and only uses checking when creating a short value out of each voxel (to check for overflows).
        /// </summary>
        /// <param name="data">The uncompressed signed pixel data.</param>
        /// <param name="volume">The volume to write the slice into.</param>
        /// <param name="sliceIndex">The index of the slice to write.</param>
        /// <param name="highBit">The high bit value for reading the pixel information.</param>
        /// <param name="rescaleIntercept">The rescale intercept of the pixel data.</param>
        /// <param name="rescaleSlope">The rescale slope of the pixel data.</param>
        private static unsafe void WriteSignedSlice(
            byte[] data,
            Volume3D <short> volume,
            uint sliceIndex,
            int highBit,
            double rescaleIntercept,
            double rescaleSlope)
        {
            fixed(short *volumePointer = volume.Array)
            fixed(byte *dataPtr = data)
            {
                var slicePointer = volumePointer + volume.DimXY * sliceIndex;
                var dataPointer  = dataPtr;

                for (var y = 0; y < volume.DimY; y++)
                {
                    for (var x = 0; x < volume.DimX; x++, dataPointer += 2, slicePointer++)
                    {
                        short value;

                        // Force unchecked so conversions won't cause overflow exceptions regardless of project settings.
                        unchecked
                        {
                            var bits = (ushort)(*dataPointer | *(dataPointer + 1) << 8);
                            value = (short)(bits << (15 - highBit));  // mask
                            value = (short)(value >> (15 - highBit)); // sign extend
                        }

                        // Force checked so out-of-range values will cause overflow exception.
                        checked
                        {
                            *slicePointer = (short)Math.Round(rescaleSlope * value + rescaleIntercept);
                        }
                    }
                }
            }
        }
        public void LoadAndSaveMedicalVolumeTest()
        {
            var directory       = TestData.GetFullImagesPath("sample_dicom");
            var acceptanceTests = new StrictGeometricAcceptanceTest(string.Empty, string.Empty);
            var medicalVolume   = MedIO.LoadAllDicomSeriesInFolderAsync(directory, acceptanceTests).Result.First().Volume;

            Directory.CreateDirectory(_tempFolder);
            Console.WriteLine($"Directory created {_tempFolder}");

            Volume3D <byte>[] contourVolumes = new Volume3D <byte> [medicalVolume.Struct.Contours.Count];

            for (int i = 0; i < medicalVolume.Struct.Contours.Count; i++)
            {
                contourVolumes[i] = new Volume3D <byte>(
                    medicalVolume.Volume.DimX,
                    medicalVolume.Volume.DimY,
                    medicalVolume.Volume.DimZ,
                    medicalVolume.Volume.SpacingX,
                    medicalVolume.Volume.SpacingY,
                    medicalVolume.Volume.SpacingZ,
                    medicalVolume.Volume.Origin,
                    medicalVolume.Volume.Direction);

                contourVolumes[i].Fill(medicalVolume.Struct.Contours[i].Contours, (byte)1);
            }

            // Calculate contours based on masks
            var rtContours = new List <RadiotherapyContour>();

            for (int i = 0; i < medicalVolume.Struct.Contours.Count; i++)
            {
                var contour = medicalVolume.Struct.Contours[i];

                var contourForAllSlices = GetContoursForAllSlices(contourVolumes[i]);
                var rtcontour           = new DicomRTContour(
                    contour.DicomRtContour.ReferencedRoiNumber,
                    contour.DicomRtContour.RGBColor,
                    contourForAllSlices);
                DicomRTStructureSetROI rtROIstructure = new DicomRTStructureSetROI(
                    contour.StructureSetRoi.RoiNumber,
                    contour.StructureSetRoi.RoiName,
                    string.Empty,
                    ERoiGenerationAlgorithm.Semiautomatic);
                DicomRTObservation observation = new DicomRTObservation(
                    contour.DicomRtObservation.ReferencedRoiNumber, new DicomPersonNameConverter("Left^Richard^^Dr"), ROIInterpretedType.EXTERNAL);

                rtContours.Add(new RadiotherapyContour(rtcontour, rtROIstructure, observation));
            }

            var rtStructureSet = new RadiotherapyStruct(
                medicalVolume.Struct.StructureSet,
                medicalVolume.Struct.Patient,
                medicalVolume.Struct.Equipment,
                medicalVolume.Struct.Study,
                medicalVolume.Struct.RTSeries,
                rtContours);

            MedIO.SaveMedicalVolumeAsync(_tempFolder, new MedicalVolume(
                                             medicalVolume.Volume,
                                             medicalVolume.Identifiers,
                                             medicalVolume.FilePaths,
                                             rtStructureSet)).Wait();

            var medicalVolume2 = MedIO.LoadAllDicomSeriesInFolderAsync(_tempFolder, acceptanceTests).Result.First().Volume;

            foreach (var radiotherapyContour in medicalVolume2.Struct.Contours.Where(x => x.DicomRtContour.DicomRtContourItems.First().GeometricType == "CLOSED_PLANAR"))
            {
                var savedContour =
                    medicalVolume2.Struct.Contours.First(x => x.StructureSetRoi.RoiName == radiotherapyContour.StructureSetRoi.RoiName);

                foreach (var contour in radiotherapyContour.Contours)
                {
                    Assert.AreEqual(radiotherapyContour.DicomRtObservation.ROIInterpretedType, ROIInterpretedType.EXTERNAL);
                    for (int i = 0; i < contour.Value.Count; i++)
                    {
                        if (!contour.Value[i].Equals(savedContour.Contours.ContoursForSlice(contour.Key)[i]))
                        {
                            Console.WriteLine(radiotherapyContour.StructureSetRoi.RoiName);
                            Assert.Fail();
                        }
                    }
                }
            }
        }