Example #1
0
        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]);
            }
        }
Example #2
0
        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");
        }
Example #3
0
        /// <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);
        }
Example #4
0
 /// <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);
 }
Example #5
0
        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));
        }
Example #6
0
 /// <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);
     });
 }
Example #7
0
        /// <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);
        }
Example #8
0
        /// <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);
        }
Example #9
0
        /// <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);
        }
Example #12
0
        /// <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);
        }
Example #15
0
        /// <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());
        }
Example #17
0
        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);
        }
Example #18
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());
        }
Example #21
0
 /// <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);
        }