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