public void FillPolygonTests7() { var dimX = 5; var dimY = 5; var expectedArray = new byte[] { 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, }; var volume2D = new Volume2D <byte>(expectedArray, 5, 5, 1, 1, new Point2D(), new Matrix2()); var extractContour = ExtractContours.PolygonsFilled(volume2D); var output1 = new byte[dimX * dimY]; var polygon = extractContour.First().Points.Select(x => CreatePoint(x.X + 0.00002, x.Y - 0.00001)).ToArray(); FillPolygon.Fill(polygon, output1, dimX, dimY, 0, 0, (byte)1); for (var i = 0; i < output1.Length; i++) { Assert.AreEqual(expectedArray[i], output1[i]); } }
public void FloodFillTest2(string filename) { var path = GetTestDataPath(filename); var image = new Bitmap(path); var mask = image.ToByteArray(); Assert.IsTrue(mask.Any(x => x == 0)); Assert.IsTrue(mask.Any(x => x == 1)); Assert.IsTrue(mask.Length == image.Width * image.Height); var actual = new Volume2D <byte>(mask, image.Width, image.Height, 1, 1, new Point2D(), new Matrix2()); var contoursFilled = actual.ContoursFilled(); var expected = actual.CreateSameSize <byte>(); expected.Fill(contoursFilled, (byte)1); var stopwatch = Stopwatch.StartNew(); FillPolygon.FloodFillHoles(actual.Array, expected.DimX, expected.DimY, 0, 0, 1, 0); stopwatch.Stop(); actual.SaveBrushVolumeToPng(@"C:\Temp\Actual.png"); expected.SaveBrushVolumeToPng(@"C:\Temp\Expected.png"); Assert.AreEqual(expected.Array, actual.Array, "Extracting filled contours and filling those should give the same result as flood filling holes."); var contoursWithHoles = actual.ContoursWithHoles(); var filledWithHoles = actual.CreateSameSize <byte>(); filledWithHoles.Fill(contoursWithHoles, (byte)1); Assert.AreEqual(actual.Array, filledWithHoles.Array, "Extracting contours with holes and filling those in should not change the mask"); }
/// <summary> /// Creates a PNG image file from the given volume. Specific voxel values in the volume are mapped /// to fixed colors in the PNG file, as per the given mapping. /// </summary> /// <param name="mask"></param> /// <param name="filePath"></param> /// <param name="voxelMapping"></param> public static void SaveVolumeToPng(this Volume2D <byte> mask, string filePath, IDictionary <byte, Color> voxelMapping, Color?defaultColor = null) { var width = mask.DimX; var height = mask.DimY; var image = new Bitmap(width, height); CreateFolderStructureIfNotExists(filePath); for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var maskValue = mask[x, y]; if (!voxelMapping.TryGetValue(maskValue, out var color)) { color = defaultColor ?? throw new ArgumentException($"The voxel-to-color mapping does not contain an entry for value {maskValue} found at point ({x}, {y}), and no default color is set.", nameof(voxelMapping)); } image.SetPixel(x, y, color); } } image.Save(filePath); }
/// <summary> /// Applies flood filling to all holes in the given volume. /// </summary> /// <param name="volume"></param> /// <param name="foregroundId"></param> /// <param name="backgroundId"></param> public static void FloodFillHoles( Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity, byte backgroundId = ModelConstants.MaskBackgroundIntensity) { FloodFillHoles(volume.Array, volume.DimX, volume.DimY, 0, 0, foregroundId, backgroundId); }
private static VoxelCounts FillNodePairsAndCount( ushort[] result, ushort fillValue, double epsilon, float[] nodeX, int y, int nodes, Volume2D <byte> countVolume, byte foregroundId) { uint foregroundCount = 0; uint otherCount = 0; var dimX = countVolume.DimX; var countArray = countVolume.Array; for (int i = 0; i < nodes; i += 2) { var first = nodeX[i]; var second = nodeX[i + 1]; var initRange = Math.Max(0, (int)Math.Ceiling(first - epsilon)); var endRange = (int)(second + epsilon); if (initRange >= dimX) { break; } if (endRange >= 0) { if (endRange >= dimX) { endRange = dimX - 1; } // Manually computing index, rather than relying on GetIndex, brings substantial speedup. var offsetY = dimX * y; for (int x = initRange; x <= endRange; x++) { var index = x + offsetY; if (result[index] != fillValue) { result[index] = fillValue; if (countArray[index] == foregroundId) { foregroundCount++; } else { otherCount++; } } } } } return(new VoxelCounts(foregroundCount, otherCount)); }
/// <summary> /// Modifies the present volume by filling all points that fall inside of the given contours, /// using the provided fill value. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="volume">The volume that should be modified.</param> /// <param name="contours">The contours that should be used for filling.</param> /// <param name="value">The value that should be used to fill all points that fall inside of /// any of the given contours.</param> public static void FillContours <T>(Volume2D <T> volume, IEnumerable <ContourPolygon> contours, T value) { Parallel.ForEach( contours, contour => { FillContour(volume, contour.ContourPoints, value); }); }
/// <summary> /// http://alienryderflex.com/polygon_fill/ /// </summary> /// <typeparam name="T"></typeparam> /// <param name="polygon"></param> /// <param name="fillVolume"></param> /// <param name="dimX"></param> /// <param name="dimY"></param> /// <param name="dimZ"></param> /// <param name="sliceZ"></param> /// <param name="fillValue"></param> /// <returns></returns> private static VoxelCounts FillPolygonAndCount( PointF[] polygon, ushort[] fillVolume, ushort fillValue, Volume2D <byte> countVolume, byte foregroundId) { var bounds = GetBoundingBox(polygon); const float epsilon = 0.01f; var length = polygon.Length; var nodeIntersections = new IntersectionXPoint[length * 2]; var nodeX = new float[length]; var polygonX = new float[length]; var polygonY = new float[length]; for (var index = 0; index < length; index++) { var point = polygon[index]; polygonX[index] = point.X; polygonY[index] = point.Y; } var voxelCounts = new VoxelCounts(); // Loop through the rows of the image. for (int y = 0; y < countVolume.DimY; y++) { float yPlusEpsilon = y + epsilon; float yMinusEpsilon = y - epsilon; if ((yPlusEpsilon < bounds.Top && yMinusEpsilon < bounds.Top) || (yPlusEpsilon > bounds.Bottom && yMinusEpsilon > bounds.Bottom)) { continue; } // Build a list of nodes, sorted int nodesBoth = FindIntersections(polygonX, polygonY, nodeIntersections, y, yPlusEpsilon, yMinusEpsilon); // Merge int nodes = MergeIntersections(nodeIntersections, nodeX, nodesBoth); // Fill the pixels between node pairs. voxelCounts += FillNodePairsAndCount( fillVolume, fillValue, epsilon, nodeX, y, nodes, countVolume, foregroundId); } return(voxelCounts); }
/// <summary> /// Creates a PNG image file from the given binary mask. Value 0 is plotted as white, /// value 1 as black. If the mask contains other values, an exception is thrown. /// </summary> /// <param name="brushVolume"></param> /// <param name="filePath"></param> public static void SaveBinaryMaskToPng(this Volume2D <byte> mask, string filePath) { var voxelMapping = new Dictionary <byte, Color> { { 0, Color.White }, { 1, Color.Black } }; SaveVolumeToPng(mask, filePath, voxelMapping); }
/// <summary> /// Fills all points that fall inside of a given polygon, and at the same time, /// aggregate statistics on what values are present at the filled pixel /// positions in a "count volume" that has the same size as the fill volume. /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="U"></typeparam> /// <param name="polygon"></param> /// <param name="fillVolume"></param> /// <param name="dimX"></param> /// <param name="dimY"></param> /// <param name="dimZ"></param> /// <param name="sliceZ"></param> /// <param name="fillValue"></param> /// <param name="countVolume"></param> /// <param name="foregroundId"></param> /// <returns></returns> public static VoxelCounts FillPolygonAndCount( PointInt[] polygon, ushort[] fillVolume, ushort fillValue, Volume2D <byte> countVolume, byte foregroundId) { polygon = polygon ?? throw new ArgumentNullException(nameof(polygon)); fillVolume = fillVolume ?? throw new ArgumentNullException(nameof(fillVolume)); countVolume = countVolume ?? throw new ArgumentNullException(nameof(countVolume)); if (polygon.Length == 0) { throw new ArgumentOutOfRangeException(nameof(polygon), "The polygon does not contain any points."); } if (fillVolume.Length != countVolume.Length) { throw new ArgumentException("The fill and the count volume must have the same size.", nameof(fillVolume)); } var pointsAsFloat = polygon.Select(point => new PointF(point.X, point.Y)).ToArray(); uint foregroundCount = 0; uint otherCount = 0; var countArray = countVolume.Array; var dimX = countVolume.DimX; foreach (var point in polygon) { // Manually computing index, rather than relying on GetIndex, brings substantial speedup. var index = point.X + dimX * point.Y; if (fillVolume[index] != fillValue) { fillVolume[index] = fillValue; if (countArray[index] == foregroundId) { foregroundCount++; } else { otherCount++; } } } var voxelCountsAtPoints = new VoxelCounts(foregroundCount, otherCount); var voxelCountsInside = FillPolygonAndCount( pointsAsFloat, fillVolume, fillValue, countVolume, foregroundId); return(voxelCountsAtPoints + voxelCountsInside); }
/// <summary> /// Takes an input volume and extracts the contours for all voxels that have the given /// foreground value. /// Contour extraction will take account of holes and inserts, up to the default nesting level. /// </summary> /// <param name="volume">The input volume.</param> /// <param name="foregroundId">The ID we are looking for when extracting contours.</param> /// <param name="smoothingType">The type of smoothing that should be applied when going from a /// point polygon to a contour.</param> /// <param name="maxNestingLevel">The maximum nesting level up to which polygons should be extracted. If set to /// 0, only the outermost polygons will be returned. If 1, the outermost polygons and the holes therein. /// If 2, the outermost polygon, the holes, and the foreground inside the holes.</param> /// <returns>The collection of contours.</returns> public static IReadOnlyList <ContourPolygon> ContoursWithHoles(Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity, ContourSmoothingType smoothingType = ContourSmoothingType.Small, int maxNestingLevel = DefaultMaxPolygonNestingLevel) { var polygonPoints = PolygonsWithHoles(volume, foregroundId, maxNestingLevel); return(polygonPoints .Select(x => new ContourPolygon(SmoothPolygon.Smooth(x, smoothingType), x.TotalPixels)) .ToList()); }
/// <summary> /// Creates volume of the same size as the argument, with pixel values equal to 1 for all /// pixels that have the given <paramref name="foregroundId"/>, and pixel value 0 for all others. /// </summary> /// <param name="volume"></param> /// <param name="foregroundId">The voxel value that should be considered foreground.</param> /// <returns></returns> private static Volume2D <byte> CreateBinaryVolume(Volume2D <byte> volume, byte foregroundId) { var binaryVolume = volume.CreateSameSize <byte>(); var binaryArray = binaryVolume.Array; var volumeArray = volume.Array; for (var index = 0; index < volumeArray.Length; index++) { binaryArray[index] = volumeArray[index] == foregroundId ? (byte)1 : (byte)0; } return(binaryVolume); }
/// <summary> /// Convert a volume to an image. Voxels with default value (usually 0) become white, everything /// else becomes black. /// </summary> /// <typeparam name="T">Volume data type.</typeparam> /// <param name="volume">Volume.</param> /// <returns>Image.</returns> public static Image ToImage <T>(this Volume2D <T> volume) { var result = new Bitmap(volume.DimX, volume.DimY); for (var y = 0; y < volume.DimY; y++) { for (var x = 0; x < volume.DimX; x++) { var colorValue = volume[x, y].Equals(default(T)) ? 255 : 0; result.SetPixel(x, y, Color.FromArgb(colorValue, colorValue, colorValue)); } } return(result); }
/// <summary> /// Takes an input volume and extracts the contours for all voxels that have the given /// foreground value. /// Contour extraction will not take account of holes, and hence only return the outermost /// contour around a region of interest. /// </summary> /// <param name="volume">The input volume.</param> /// <param name="foregroundId">The ID we are looking for when extracting contours.</param> /// <param name="smoothingType">The type of smoothing that should be applied when going from a /// point polygon to a contour.</param> /// <returns>The collection of contours.</returns> public static IReadOnlyList <ContourPolygon> ContoursFilled(Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity, ContourSmoothingType smoothingType = ContourSmoothingType.Small) { var polygonPoints = PolygonsFilled(volume, foregroundId); return(polygonPoints .Select(x => { var isCounterClockwise = false; var smoothedPoints = SmoothPolygon.SmoothPoints(x.Points, isCounterClockwise, smoothingType); return new ContourPolygon(smoothedPoints, x.VoxelCounts.Total); }) .ToList()); }
/// <summary> /// Creates the minimum 2D volume from the list of contours and the region of interest. /// </summary> /// <param name="contours">The contours by slice.</param> /// <param name="spacingX">The X-dimension pixel spacing.</param> /// <param name="spacingY">The Y-dimension pixel spacing.</param> /// <param name="origin">The patient position origin.</param> /// <param name="direction">The directional matrix.</param> /// <param name="region">The region of interest.</param> /// <returns>The minimum 2D volume.</returns> public static Volume2D <byte> CreateVolume2D(this IReadOnlyList <ContourPolygon> contours, double spacingX, double spacingY, Point2D origin, Matrix2 direction, Region2D <int> region) { // Convert every point to within the region var subContours = contours.Select(x => new ContourPolygon( x.ContourPoints.Select( point => new PointF(point.X - region.MinimumX, point.Y - region.MinimumY)).ToArray(), 0)).ToList(); // Create 2D volume var result = new Volume2D <byte>(region.MaximumX - region.MinimumX + 1, region.MaximumY - region.MinimumY + 1, spacingX, spacingY, origin, direction); result.Fill(subContours, ModelConstants.MaskForegroundIntensity); return(result); }
/// <summary> /// Creates a PNG image file from the given volume. Specific voxel values in the volume are mapped /// to fixed colors in the PNG file: /// Background (value 0) is plotted in Red /// Foreground (value 1) is Green /// Value 2 is Orange /// Value 3 is MediumAquamarine /// All other voxel values are plotted in Blue. /// </summary> /// <param name="brushVolume"></param> /// <param name="filePath"></param> public static void SaveBrushVolumeToPng(this Volume2D <byte> brushVolume, string filePath) { const byte fg = 1; const byte bg = 0; const byte bfg = 3; const byte bbg = 2; var voxelMapping = new Dictionary <byte, Color> { { fg, Color.Green }, { bfg, Color.MediumAquamarine }, { bg, Color.Red }, { bbg, Color.Orange } }; SaveVolumeToPng(brushVolume, filePath, voxelMapping, Color.Blue); }
/// <summary> /// Takes an input volume and extracts the contours for the regions where the voxel value is /// <paramref name="foregroundId"/>. Region are assumed to be filled: If there is a doughnut-shaped /// region in the volume, only the outer rim of that region is extracted. /// </summary> /// <param name="volume">The input volume.</param> /// <param name="foregroundId">The voxel value that should be considered foreground.</param> /// <returns>The collection of contours.</returns> public static IReadOnlyList <PolygonPoints> PolygonsFilled( Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity) { volume = volume ?? throw new ArgumentNullException(nameof(volume)); var binaryVolume = CreateBinaryVolume(volume, foregroundId); var foundPolygons = new ushort[volume.Length]; var polygons = ExtractPolygons( binaryVolume, foundPolygons, searchInsidePolygon: 0, isInnerPolygon: false, firstNewPolygon: 1); return(polygons.Values.ToList()); }
public void FillPolygonTestsRandomCheckTermination() { var dimX = 2000; int seed = Guid.NewGuid().GetHashCode(); Console.WriteLine($"Seed {seed}"); var random = new Random(seed); var byteArray = Enumerable.Range(0, dimX * dimX).Select(_ => (byte)random.Next(0, 2)).ToArray(); var volume2D = new Volume2D <byte>(byteArray, dimX, dimX, 1, 1, new Point2D(), new Matrix2()); var extractContour = ExtractContours.PolygonsFilled(volume2D); Console.WriteLine($"Contours count {extractContour.Count}"); Assert.IsTrue(extractContour.Count > 0); }
public static void SaveDistanceVolumeToPng(this Volume2D <float> distanceVolume, string filePath) { var width = distanceVolume.DimX; var height = distanceVolume.DimY; var image = new Bitmap(distanceVolume.DimX, distanceVolume.DimY); CreateFolderStructureIfNotExists(filePath); var minMax = MinMaxFloat(distanceVolume.Array); var minimum = minMax.Item1; var maximum = minMax.Item2; float extval = Math.Min(Math.Min(Math.Abs(minimum), maximum), 3000); if (minimum >= 0) { extval = maximum; } else if (maximum <= 0) { extval = Math.Abs(minimum); } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var currentDistanceValue = distanceVolume[x, y]; if (currentDistanceValue < -extval) { currentDistanceValue = -extval; } if (currentDistanceValue > extval) { currentDistanceValue = extval; } float alpha = (currentDistanceValue - (-extval)) / (2 * extval); float R, G, B; R = 255 * alpha; G = 255 * (1 - alpha); B = 255 * (float)(1 - Math.Abs(alpha - 0.5) * 2); Color color = Color.FromArgb(255, (byte)R, (byte)G, (byte)B); // Background (color intensity for red) if (currentDistanceValue < short.MinValue) { color = Color.Orange; } else if (currentDistanceValue > short.MaxValue) { color = Color.HotPink; } else if ((int)currentDistanceValue == 0) { color = Color.Yellow; } image.SetPixel(x, y, color); } } image.Save(filePath); }
/// <summary> /// Extracts a set of possibly nested polygons from a given binary mask. /// The top level extracted polygons are the outermost regions on the canvas where the voxel value is 1. /// Inside of each of these polygons can be further polygons that describe holes (doughnut shape, /// voxel value 0), which in turn can contain further polygons that have voxel value 1, etc. /// </summary> /// <param name="volume">The binary mask from which the polygons should be extracted. </param> /// <param name="foregroundId">The voxel value that should be considered foreground.</param> /// <param name="maxNestingLevel">The maximum nesting level up to which polygons should be extracted. If set to /// 0, only the outermost polygons will be returned. If 1, the outermost polygons and the holes therein. /// If 2, the outermost polygon, the holes, and the foreground inside the holes.</param> /// <param name="enableVerboseOutput">If true, print statistics about the found polygons to Trace.</param> /// <returns></returns> public static IReadOnlyList <InnerOuterPolygon> PolygonsWithHoles( Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity, int maxNestingLevel = DefaultMaxPolygonNestingLevel, bool enableVerboseOutput = false) { var contoursWithHoles = new Stack <ushort>(); void PushIfHolesPresent(KeyValuePair <ushort, PolygonPoints> p) { var backgroundVoxels = p.Value.VoxelCounts.Other; if (backgroundVoxels > 0) { contoursWithHoles.Push(p.Key); } } var binaryVolume = CreateBinaryVolume(volume, foregroundId); var foundPolygons = new ushort[volume.Length]; var searchInside = (ushort)0; var contours = ExtractPolygons( binaryVolume, foundPolygons, searchInside, isInnerPolygon: false, firstNewPolygon: 1); var result = new Dictionary <ushort, InnerOuterPolygon>(); foreach (var p in contours) { result.Add(p.Key, new InnerOuterPolygon(p.Value)); PushIfHolesPresent(p); if (enableVerboseOutput) { Trace.TraceInformation($"Polygon {p.Key} on canvas: {p.Value.Count} points on the outside. Contains {p.Value.VoxelCounts.Other} background voxels."); } } var remainingHoles = 0; uint remainingHoleVoxels = 0; while (contoursWithHoles.Count > 0) { var parentIndex = contoursWithHoles.Pop(); var parentWithHoles = contours[parentIndex]; if (parentWithHoles.NestingLevel < maxNestingLevel) { // Inside of the polygon just popped from the stack, search for holes or inserts: // What is background in the enclosing polygon is now foreground. var nextIndex = (ushort)(contours.Keys.Max() + 1); var searchInsidePolygon = parentIndex; var isInnerPolygon = !parentWithHoles.IsInnerContour; var holePolygons = ExtractPolygons( binaryVolume, foundPolygons, searchInsidePolygon, isInnerPolygon, nextIndex); foreach (var p in holePolygons) { p.Value.NestingLevel = parentWithHoles.NestingLevel + 1; contours.Add(p.Key, p.Value); if (p.Value.IsInnerContour) { result[parentIndex].AddInnerContour(p.Value); } else { result.Add(p.Key, new InnerOuterPolygon(p.Value)); } PushIfHolesPresent(p); if (enableVerboseOutput) { Trace.TraceInformation($"Polygon {p.Key} inside {p.Value.InsideOfPolygon} (nesting level {p.Value.NestingLevel}): {p.Value.Count} points on the outside. Contains {p.Value.VoxelCounts.Other} hole voxels."); } } } else { remainingHoles++; remainingHoleVoxels += parentWithHoles.VoxelCounts.Other; } } if (remainingHoles > 0 && enableVerboseOutput) { Trace.TraceWarning($"Capping at maximum nesting level was applied. There are still {remainingHoles} hole/insert regions, with a total of {remainingHoleVoxels} voxels."); } return(result.Values.ToList()); }
private static PointInt[] FindPolygon( Volume2D <byte> volume, ushort[] foundPolygons, ushort searchInsidePolygon, PointInt start, byte backgroundId, bool searchClockwise) { // Clockwise search starts at a point (x, y) where there is no foreground // on lines with smaller y. Next point can hence be in direction 0 // (neighbor x+1,y) or at directions larger than 0. // For counterclockwise search, we start with a point that we know is background, // and go to the line above (y-1) from that, where we are guaranteed to find foreground. // From that point, going in direction hits the point we know is background, and can // continue to search in clockwise direction. var nextSearchDirection = searchClockwise ? 0 : 2; var done = false; var current = start; var nextPoint = start; var contour = new List <PointInt>(); var dimX = volume.DimX; var dimY = volume.DimY; var neighbors = Delta.Length; var array = volume.Array; (int, PointInt) FindNextPoint(int currentX, int currentY, int deltaIndex) { for (var i = 0; i < 7; i++) { var delta = Delta[deltaIndex]; var x = currentX + delta.X; var y = currentY + delta.Y; // Manually computing index, rather than relying on GetIndex, brings substantial speedup. var index = x + y * dimX; if (x < 0 || x >= dimX || y < 0 || y >= dimY || array[index] == backgroundId || foundPolygons[index] != searchInsidePolygon) { deltaIndex = (deltaIndex + 1) % neighbors; } else { // found non-background pixel return(deltaIndex, new PointInt(x, y)); } } return(deltaIndex, new PointInt(currentX, currentY)); } while (!done) { contour.Add(current); (nextSearchDirection, nextPoint) = FindNextPoint( current.X, current.Y, nextSearchDirection); // Delta positions for search of the next neighbor are specified in clockwise order, // starting with the point (x+1,y). After finding a neighbor, reset those by going // "back" two increments. // Because of % 8, going back by 2 for clockwise search amounts to going forward by 6. nextSearchDirection = (nextSearchDirection + 6) % neighbors; // Terminate when we are back at the starting position. done = nextPoint == start; current = nextPoint; } return(contour.ToArray()); }
/// <summary> /// Fills the contour using high accuracy (point in polygon testing). /// </summary> /// <typeparam name="T">The volume type.</typeparam> /// <param name="volume">The volume.</param> /// <param name="contourPoints">The points that defines the contour we are filling.</param> /// <param name="region">The value we will mark in the volume when a point is within the contour.</param> /// <returns>The number of points filled.</returns> public static int FillContour <T>(Volume2D <T> volume, PointF[] contourPoints, T value) { return(Fill(contourPoints, volume.Array, volume.DimX, volume.DimY, 0, 0, value)); }
/// <summary> /// Modifies the present volume by filling all points that fall inside of the given contours, /// using the provided fill value. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="volume">The volume that should be modified.</param> /// <param name="contours">The contours that should be used for filling.</param> /// <param name="value">The value that should be used to fill all points that fall inside of /// any of the given contours.</param> public static void Fill <T>(this Volume2D <T> volume, IEnumerable <ContourPolygon> contours, T value) => FillPolygon.FillContours(volume, contours, value);
/// <summary> /// Extracts the contours around all voxel values in the volume that have the given foreground value. /// All other voxel values (zero and anything that is not the foreground value) is treated as background. /// Contour extraction will not take account of holes, and hence only return the outermost /// contour around a region of interest. /// </summary> /// <param name="volume"></param> /// <param name="foregroundId">The voxel value that should be used as foreground in the contour search.</param> /// <param name="smoothingType">The smoothing that should be applied when going from a point polygon to /// a contour.</param> /// <returns></returns> public static IReadOnlyList <ContourPolygon> ContoursFilled( this Volume2D <byte> volume, byte foregroundId = 1, ContourSmoothingType smoothingType = ContourSmoothingType.Small) => ExtractContours.ContoursFilled(volume, foregroundId, smoothingType);
/// <summary> /// Applies flood filling to all holes in the given volume. /// </summary> /// <param name="volume"></param> /// <param name="foregroundId"></param> /// <param name="backgroundId"></param> public static void FillHoles( this Volume2D <byte> volume, byte foregroundId = ModelConstants.MaskForegroundIntensity, byte backgroundId = ModelConstants.MaskBackgroundIntensity) => FillPolygon.FloodFillHoles(volume, foregroundId, backgroundId);
/// <summary> /// Extracts polygons by walking the edge of the foreground values of the worker volume. /// This method will return closed polygons. Also, the polygons will be filled (any holes removed). /// The <paramref name="foundPolygons"/> argument will be updated in place, by marking all voxels /// that are found to be inside of a polygon with the index of that polygon. /// The first new polygon that is found will be given the number supplied in <paramref name="firstNewPolygon"/> /// (must be 1 or higher) /// </summary> /// <param name="binaryVolume">The volume we are extracting polygons from. Must only contain values 0 and 1.</param> /// <param name="foundPolygons">A volume that contains the IDs for all polygons that have been found already. 0 means no polygon found.</param> /// <param name="searchInsidePolygon">For walking along edges, limit to the voxels that presently are assigned to /// the polygon given here.</param> /// <param name="isInnerPolygon">If true, the polygon will walk along boundaries around regions with /// voxel value 0 (background), but keep the polyon points on voxel values 1, counterclockwises.</param> /// <param name="firstNewPolygon">The ID for the first polygon that is found.</param> /// <returns>A collection of polygons and there respective sizes (number of foreground points in each polygon)</returns> private static Dictionary <ushort, PolygonPoints> ExtractPolygons( Volume2D <byte> binaryVolume, ushort[] foundPolygons, ushort searchInsidePolygon, bool isInnerPolygon, ushort firstNewPolygon) { if (binaryVolume == null) { throw new ArgumentNullException(nameof(binaryVolume)); } if (foundPolygons == null) { throw new ArgumentNullException(nameof(foundPolygons)); } if (firstNewPolygon < 1) { throw new ArgumentOutOfRangeException(nameof(firstNewPolygon), "Polygon index 0 is reserved for 'not assigned'"); } var foregroundId = isInnerPolygon ? (byte)0 : (byte)1; var polygons = new Dictionary <ushort, PolygonPoints>(); var dimX = binaryVolume.DimX; var dimY = binaryVolume.DimY; var volumeArray = binaryVolume.Array; for (var y = 0; y < dimY; y++) { // Manually computing index, rather than relying on GetIndex, brings substantial speedup. var offsetY = y * dimX; for (var x = 0; x < dimX; x++) { var pixelIndex = x + offsetY; // Starting point of a new polygon is where we see the desired foreground in the original // volume, and have either not found any polygon yet (searchInsidePolygon == 0) // or have found a polygon already and now search for the holes inside it. if (volumeArray[pixelIndex] == foregroundId && foundPolygons[pixelIndex] == searchInsidePolygon) { PointInt startPoint; if (isInnerPolygon) { Debug.Assert(y >= 1, "When searching for innner polygons (holes), expecting that there is foreground in the row above."); startPoint = new PointInt(x, y - 1); } else { startPoint = new PointInt(x, y); } VoxelCounts voxelCounts; PointInt[] contourPoints; if (isInnerPolygon) { var innerPoints = FindPolygon( binaryVolume, foundPolygons, searchInsidePolygon, new PointInt(x, y), backgroundId: 1, searchClockwise: true); voxelCounts = FillPolygon.FillPolygonAndCount( innerPoints, foundPolygons, firstNewPolygon, binaryVolume, foregroundId: 0); contourPoints = FindPolygon( binaryVolume, foundPolygons, searchInsidePolygon, startPoint, backgroundId: 0, searchClockwise: false); } else { contourPoints = FindPolygon( binaryVolume, foundPolygons, searchInsidePolygon, startPoint, backgroundId: 0, searchClockwise: true); voxelCounts = FillPolygon.FillPolygonAndCount( contourPoints, foundPolygons, firstNewPolygon, binaryVolume, foregroundId); } var polygon = new PolygonPoints( contourPoints, voxelCounts, searchInsidePolygon, isInside: isInnerPolygon, startPointMinimumY: startPoint); polygons.Add(firstNewPolygon, polygon); firstNewPolygon++; } } } return(polygons); }