Skip to content
This repository has been archived by the owner on Nov 19, 2020. It is now read-only.

Question: Is there a Distance Transform algorithm in Accord.Net? #654

Open
fdncred opened this issue Jun 20, 2017 · 19 comments
Open

Question: Is there a Distance Transform algorithm in Accord.Net? #654

fdncred opened this issue Jun 20, 2017 · 19 comments

Comments

@fdncred
Copy link
Collaborator

fdncred commented Jun 20, 2017

I've grepped and looked through the code looking for a Distance Transform but I don't see one. Could you please clarify if there is one please?

This is what I'm looking for.
https://en.wikipedia.org/wiki/Distance_transform
https://homepages.inf.ed.ac.uk/rbf/HIPR2/distance.htm

@cesarsouza
Copy link
Member

Hi there,

Thanks for creating the issue. The framework doesn't offer an implementation for the (Euclidean) Distance transform yet, though I also agree that it would be very useful to have one.

At some point there was one file in the repository that was supposed to be implementing it, but it was not actually included in the build. It was a leftover from an incomplete implementation.

In any case, may I ask if this file implements the transform you need? https://github.com/accord-net/java/blob/master/Catalano.Android.Image/src/Catalano/Imaging/Filters/DistanceTransform.java

If yes, then it should be straightforward to add it to the project. Diego (the author of that code) is also a developer here in Accord.NET.

Regards,
Cesar

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 20, 2017

Diego's code seems reasonable. I like that it has other distance masks other than Euclidean.

@cesarsouza
Copy link
Member

@DiegoCatalano would you like to try passing this algorithm to C#? 😄

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 20, 2017

Here's my naive approach to porting that function. It seems to work but I'm probably doing some things stupidly. :) But maybe it'll save you some time.

    public class DistanceTransform
    {

        /**
         * Distance metric.
         */
        public enum Distance
        {
            /**
             * Chessboard metric.
             */
            Chessboard,
            /**
             * Euclidean metric.
             */
            Euclidean,
            /**
             * Manhattan metric.
             */
            Manhattan
        };

        private Distance distance = Distance.Euclidean;

        private float[,] maskDistance = new float[3, 3] { 
            {1.4142f,1,1.4142f},
            {1,0,1},
            {1.4142f,1,1.4142f}
        };
        private float[,] image;

        /**
         * Get Distance metric.
         * @return Distance metric.
         */
        public Distance GetDistance()
        {
            return distance;
        }

        /**
         * Set Distance metric.
         * @param distance Distance metric.
         */
        public void SetDistance(Distance distance)
        {

            this.distance = distance;
            switch (distance)
            {
                case Distance.Chessboard:
                    this.maskDistance = new float[3,3]{
                        {1,1,1},
                        {1,0,1},
                        {1,1,1}
                        };
                    break;
                case Distance.Manhattan:
                    this.maskDistance = new float[3,3]{
                        {2,1,2},
                        {1,0,1},
                        {2,1,2}
                        };
                    break;
                case Distance.Euclidean:
                    this.maskDistance = new float[3,3]{
                        {1.4142135f,1,1.4142135f},
                        {1,0,1},
                        {1.4142135f,1,1.4142135f}
                        };
                    break;
            }
        }

        /**
         * Get Mask distance.
         * @return Mask distance.
         */
        public float[,] GetMaskDistance()
        {
            return maskDistance;
        }

        /**
         * Set Mask distance.
         * @param maskDistance Mask distance.
         */
        public void SetMaskDistance(float[,] maskDistance)
        {
            this.maskDistance = maskDistance;
        }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * Default distance: Euclidean.
         */
        public DistanceTransform() { }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * @param distance Distance metric.
         */
        public DistanceTransform(Distance distance)
        {
            SetDistance(distance);
        }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * @param maskDistance Distance mask.
         */
        public DistanceTransform(float[,] maskDistance)
        {
            this.maskDistance = maskDistance;
        }

        /**
         * Compute Distance Transform.
         * @param fastBitmap Image to be processed.
         * @return Distance map.
         */
        public float[,] Compute(UnmanagedImage fastBitmap)
        {
            if (fastBitmap.PixelFormat == PixelFormat.Format8bppIndexed)// isGrayscale())
            {
                int width = fastBitmap.Width;
                int height = fastBitmap.Height;

                image = new float[height,width];

                var colorLookup = Enum.GetValues(typeof(KnownColor))
                                       .Cast<KnownColor>()
                                       .Select(Color.FromKnownColor)
                                       .ToLookup(c => c.ToArgb());

                //Initialize Distance Map
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        var color = fastBitmap.GetPixel(j, i);
                        var pixelColors = colorLookup[color.ToArgb()];
                        bool matchingName = false;
                        foreach (var itemColor in pixelColors)
                        {
                            if (itemColor == Color.Black)
                                matchingName = true;
                        }
                        if (matchingName)
                        {
                            image[i,j] = 0;
                        }
                        else
                        {
                            image[i,j] = float.PositiveInfinity;
                        }
                    }
                }

                //Top -> Bottom - Left -> Right
                for (int i = 1; i < height - 1; i++)
                {
                    for (int j = 1; j < width - 1; j++)
                    {

                        if (image[i,j] > 0)
                        {
                            float d1 = maskDistance[1,0] + image[i,j - 1];
                            float d2 = maskDistance[0,0] + image[i - 1,j - 1];
                            float d3 = maskDistance[0,1] + image[i - 1,j];
                            float d4 = maskDistance[0,2] + image[i - 1,j + 1];
                            image[i,j] = Math.Min(d1, Math.Min(d2, Math.Min(d3, d4)));
                        }

                    }
                }

                //Bottom -> Top - Right -> Left
                for (int i = height - 2; i > 1; i--)
                {
                    for (int j = width - 3; j > 1; j--)
                    {

                        if (image[i,j] > 0)
                        {
                            float d1 = maskDistance[1,2] + image[i,j + 1];
                            float d2 = maskDistance[2,2] + image[i + 1,j + 1];
                            float d3 = maskDistance[2,1] + image[i + 1,j];
                            float d4 = maskDistance[2,0] + image[i + 1,j - 1];
                            image[i,j] = Math.Min(image[i,j], Math.Min(d1, Math.Min(d2, Math.Min(d3, d4))));
                        }

                    }
                }

                for (int i = 0; i < image.GetUpperBound(0); i++)
                {
                    for (int j = 0; j < image.GetUpperBound(1); j++)
                    {
                        if (image[i,j] == float.PositiveInfinity)
                        {
                            image[i,j] = 0;
                        }
                    }
                }

                return image;
            }
            else
            {
                throw new ArgumentException("Distance Transform only works in grayscale images.");
            }

        }

        /**
         * Convert Distance map to FastBitmap.
         * @return FastBitmap.
         */
        public UnmanagedImage ToFastBitmap()
        {

            int width = image.GetUpperBound(1);
            int height = image.GetUpperBound(0);

            double max = 0;
            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    if (image[i,j] > max)
                    {
                        max = image[i,j];
                    }
                }
            }

            //FastBitmap fb = new FastBitmap(width, height, FastBitmap.ColorSpace.Grayscale);
            using (Bitmap bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed))
            {
                UnmanagedImage fb = UnmanagedImage.FromManagedImage(bmp);

                if (max > 255)
                {
                    for (int i = 0; i < height; i++)
                    {
                        for (int j = 0; j < width; j++)
                        {
                            var col = (byte)Accord.Math.Tools.Scale(0, max, 0, 255, image[i, j]);
                            fb.SetPixel(j, i, col);
                        }
                    }
                }
                else
                {
                    for (int i = 0; i < height; i++)
                    {
                        for (int j = 0; j < width; j++)
                        {
                            var col = (byte)Accord.Math.Tools.Scale(0, max, 0, 255, image[i, j]);
                            fb.SetPixel(j, i, col);
                        }
                    }
                }

                return fb;
            }
        }
    }

@cesarsouza
Copy link
Member

Thanks @fdncred!

However, I think you really do not want to use the GetPixel/SetPixel functions in your loops. They will slow down your algorithm by multiple orders of magnitude.

But it surely helps as a starting point ;-) thanks!

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 21, 2017

@cesarsouza How about this one? I used your MatrixToImage and ImageToMatrix. Strange thing though, the output is different than with SetPixel().

SetPixel() Output
image

MatrixToImage Output
image

I'm not sure which one is right.

    public class DistanceTransform
    {
        /**
         * Distance metric.
         */
        public enum Distance
        {
            /**
             * Chessboard metric.
             */
            Chessboard,
            /**
             * Euclidean metric.
             */
            Euclidean,
            /**
             * Manhattan metric.
             */
            Manhattan
        };

        private Distance distance = Distance.Euclidean;

        private float[,] maskDistance = new float[3, 3] 
        {
            {1.4142f,1,1.4142f},
            {1,0,1},
            {1.4142f,1,1.4142f}
        };
        private float[,] image;

        /**
         * Get Distance metric.
         * @return Distance metric.
         */
        public Distance GetDistance()
        {
            return distance;
        }

        /**
         * Set Distance metric.
         * @param distance Distance metric.
         */
        public void SetDistance(Distance distance)
        {

            this.distance = distance;
            switch (distance)
            {
                case Distance.Chessboard:
                    this.maskDistance = new float[3, 3]
                    {
                        {1,1,1},
                        {1,0,1},
                        {1,1,1}
                    };
                    break;
                case Distance.Manhattan:
                    this.maskDistance = new float[3, 3]
                    {
                        {2,1,2},
                        {1,0,1},
                        {2,1,2}
                    };
                    break;
                case Distance.Euclidean:
                    this.maskDistance = new float[3, 3]
                    {
                        {1.4142135f,1,1.4142135f},
                        {1,0,1},
                        {1.4142135f,1,1.4142135f}
                    };
                    break;
            }
        }

        /**
         * Get Mask distance.
         * @return Mask distance.
         */
        public float[,] GetMaskDistance()
        {
            return maskDistance;
        }

        /**
         * Set Mask distance.
         * @param maskDistance Mask distance.
         */
        public void SetMaskDistance(float[,] maskDistance)
        {
            this.maskDistance = maskDistance;
        }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * Default distance: Euclidean.
         */
        public DistanceTransform() { }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * @param distance Distance metric.
         */
        public DistanceTransform(Distance distance)
        {
            SetDistance(distance);
        }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * @param maskDistance Distance mask.
         */
        public DistanceTransform(float[,] maskDistance)
        {
            this.maskDistance = maskDistance;
        }

        /**
         * Compute Distance Transform.
         * @param fastBitmap Image to be processed.
         * @return Distance map.
         */
        public unsafe float[,] Compute(UnmanagedImage fastBitmap)
        {
            if (fastBitmap.PixelFormat == PixelFormat.Format8bppIndexed)// isGrayscale())
            {
                int width = fastBitmap.Width;
                int height = fastBitmap.Height;

                image = new float[height, width];

                Accord.Imaging.Converters.ImageToMatrix conv = new Accord.Imaging.Converters.ImageToMatrix(min: 0, max: 1);
                conv.Convert(fastBitmap, out image);

                //Top -> Bottom - Left -> Right
                for (int i = 1; i < height - 1; i++)
                {
                    for (int j = 1; j < width - 1; j++)
                    {
                        if (image[i, j] > 0)
                        {
                            float d1 = maskDistance[1, 0] + image[i, j - 1];
                            float d2 = maskDistance[0, 0] + image[i - 1, j - 1];
                            float d3 = maskDistance[0, 1] + image[i - 1, j];
                            float d4 = maskDistance[0, 2] + image[i - 1, j + 1];
                            image[i, j] = Math.Min(d1, Math.Min(d2, Math.Min(d3, d4)));
                        }
                    }
                }

                //Bottom -> Top - Right -> Left
                for (int i = height - 2; i > 1; i--)
                {
                    for (int j = width - 3; j > 1; j--)
                    {
                        if (image[i, j] > 0)
                        {
                            float d1 = maskDistance[1, 2] + image[i, j + 1];
                            float d2 = maskDistance[2, 2] + image[i + 1, j + 1];
                            float d3 = maskDistance[2, 1] + image[i + 1, j];
                            float d4 = maskDistance[2, 0] + image[i + 1, j - 1];
                            image[i, j] = Math.Min(image[i, j], Math.Min(d1, Math.Min(d2, Math.Min(d3, d4))));
                        }
                    }
                }

                for (int i = 0; i < image.GetUpperBound(0) + 1; i++)
                {
                    for (int j = 0; j < image.GetUpperBound(1) + 1; j++)
                    {
                        if (image[i, j] == 1)//float.PositiveInfinity)
                        {
                            image[i, j] = 0;
                        }
                    }
                }

                return image;
            }
            else
            {
                throw new ArgumentException("Distance Transform only works in grayscale images.");
            }
        }

        /**
         * Convert Distance map to FastBitmap.
         * @return FastBitmap.
         */
        public unsafe Bitmap ToBitmap()
        {

            int width = image.GetUpperBound(1) + 1;
            int height = image.GetUpperBound(0) + 1;

            double max = 0;
            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    if (image[i, j] > max)
                    {
                        max = image[i, j];
                    }
                }
            }

            UnmanagedImage fb = UnmanagedImage.Create(width, height, PixelFormat.Format8bppIndexed);
            Accord.Imaging.Converters.ImageToMatrix conv = new Accord.Imaging.Converters.ImageToMatrix(min: 0, max: 255);
            conv.Convert(fb, out byte[,] img);

            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    byte col = (byte)Vector.Scale(image[i, j], 0, max, 0, 255);
                    img[i, j] = col;
                }
            }

            Accord.Imaging.Converters.MatrixToImage m2i = new Accord.Imaging.Converters.MatrixToImage(min: 0, max: 255);
            m2i.Convert(img, out Bitmap output);

            return output;
        }
    }

@DiegoCatalano
Copy link
Contributor

Hello @fdncred,

The distance you used was a very old version. The new version, it's the same algorithm used in ImageJ.

I just tested and it is correct as this link below:
https://homepages.inf.ed.ac.uk/rbf/HIPR2/distance.htm

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 22, 2017

Thanks @DiegoCatalano. Do you have a C# version? If not, I'll try to port it.

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 22, 2017

I didn't change that much.

    /**
     * Distance Transform.
     * 
     * <p><li>Supported types: Grayscale.
     * <br><li>Coordinate System: Matrix.
     * 
     * @author Diego Catalano
     */
    public class DistanceTransform
    {

        /**
         * Distance.
         */
        public enum Distance
        {
            /**
             * Chessboard.
             */
            Chessboard,

            /**
             * Euclidean.
             */
            Euclidean,

            /**
             * Manhattan.
             */
            Manhattan,

            /**
             * Squared Euclidean.
             */
            SquaredEuclidean
        };

        private float[][] image;
        private float max = 0;
        private Accord.IntPoint ued;
        private Distance distance = Distance.Euclidean;

        /**
         * Get Maximum distance from transform.
         * @return Maximum distance.
         */
        public float GetMaximumDistance()
        {
            return max;
        }

        /**
         * Get the Ultimate eroded point.
         * @return UED.
         */
        public Accord.IntPoint GetUltimateErodedPoint()
        {
            return ued;
        }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * Default distance: Euclidean.
         */
        public DistanceTransform() { }

        /**
         * Initialize a new instance of the DistanceTransform class.
         * @param distance Distance.
         */
        public DistanceTransform(Distance distance)
        {
            this.distance = distance;
        }

        /**
         * Compute Distance Transform.
         * @param fastBitmap Image to be processed.
         * @return Distance map.
         */
        public float[][] Compute(UnmanagedImage fastBitmap)
        {
            if (fastBitmap.PixelFormat == PixelFormat.Format8bppIndexed)//fastBitmap.isGrayscale())
            {
                int width = fastBitmap.Width;
                int height = fastBitmap.Height;
                //byte[] bPixels = fastBitmap.getGrayData();
                Accord.Imaging.Converters.ImageToArray conv = new Accord.Imaging.Converters.ImageToArray(min: 0, max: 255);
                conv.Convert(fastBitmap, out float[] fbPixels);
                byte[] bPixels = fbPixels.Select(f => (byte)f).ToArray();

                float[] fPixels = new float[bPixels.Length];

                for (int i = 0; i < width * height; i++)
                    if (bPixels[i] != 0)
                        fPixels[i] = float.MaxValue;

                //int[][] pointBufs = new int[2][width];
                int[][] pointBufs = new int[2][];
                pointBufs[0] = new int[width];
                pointBufs[1] = new int[width];
                // pass 1 & 2: increasing y
                for (int x = 0; x < width; x++)
                {
                    pointBufs[0][x] = -1;
                    pointBufs[1][x] = -1;
                }
                for (int y = 0; y < height; y++)
                    EdmLine(bPixels, fPixels, pointBufs, width, y * width, y);

                //pass 3 & 4: decreasing y
                for (int x = 0; x < width; x++)
                {
                    pointBufs[0][x] = -1;
                    pointBufs[1][x] = -1;
                }
                for (int y = height - 1; y >= 0; y--)
                    EdmLine(bPixels, fPixels, pointBufs, width, y * width, y);

                //image = new float[height][width];
                image = new float[height][];
                int p = 0;

                if (distance == Distance.Euclidean)
                {
                    for (int i = 0; i < height; i++)
                    {
                        image[i] = new float[width];
                        for (int j = 0; j < width; j++)
                        {
                            if (fPixels[p] < 0f)
                                image[i][j] = 0;
                            else
                                image[i][j] = (float)Math.Sqrt(fPixels[p]);
                            if (image[i][j] > max)
                            {
                                max = image[i][j];
                                ued = new Accord.IntPoint(i, j);
                            }
                            p++;
                        }
                    }
                }
                else
                {
                    for (int i = 0; i < height; i++)
                    {
                        image[i] = new float[width];
                        for (int j = 0; j < width; j++)
                        {
                            if (fPixels[p] < 0f)
                                image[i][j] = 0;
                            else
                                image[i][j] = fPixels[p];
                            if (image[i][j] > max)
                            {
                                max = image[i][j];
                                ued = new Accord.IntPoint(i, j);
                            }
                            p++;
                        }
                    }
                }

                return image;
            }
            else
            {
                throw new ArgumentException("Distance Transform only works in grayscale images.");
            }

        }

        // Handle a line; two passes: left-to-right and right-to-left
        private void EdmLine(byte[] bPixels, float[] fPixels, int[][] pointBufs, int width, int offset, int y)
        {
            int[] points = pointBufs[0];        // the buffer for the left-to-right pass
            int pPrev = -1;
            int pDiag = -1;               // point at (-/+1, -/+1) to current one (-1,-1 in the first pass)
            int pNextDiag;
            int distSqr = Int32.MaxValue;    // this value is used only if edges are not background
            for (int x = 0; x < width; x++, offset++)
            {
                pNextDiag = points[x];
                if (bPixels[offset] == 0)
                {
                    points[x] = x | y << 16;      // remember coordinates as a candidate for nearest background point
                }
                else
                {                        // foreground pixel:
                    float dist2 = MinDist2(points, pPrev, pDiag, x, y, distSqr, distance);
                    if (fPixels[offset] > dist2) fPixels[offset] = dist2;
                }
                pPrev = points[x];
                pDiag = pNextDiag;
            }
            offset--; //now points to the last pixel in the line
            points = pointBufs[1];              // the buffer for the right-to-left pass. Low short contains x, high short y
            pPrev = -1;
            pDiag = -1;
            for (int x = width - 1; x >= 0; x--, offset--)
            {
                pNextDiag = points[x];
                if (bPixels[offset] == 0)
                {
                    points[x] = x | y << 16;      // remember coordinates as a candidate for nearest background point
                }
                else
                {                        // foreground pixel:
                    float dist2 = MinDist2(points, pPrev, pDiag, x, y, distSqr, distance);
                    if (fPixels[offset] > dist2) fPixels[offset] = dist2;
                }
                pPrev = points[x];
                pDiag = pNextDiag;
            }
        }

        private float MinDist2(int[] points, int pPrev, int pDiag, int x, int y, int distSqr, Distance distance)
        {
            int p0 = points[x];              // the nearest background point for the same x in the previous line
            int nearestPoint = p0;
            if (p0 != -1)
            {
                int x0 = p0 & 0xffff; int y0 = (p0 >> 16) & 0xffff;
                int dist1Sqr = CalcDistance(x, y, x0, y0, distance);
                if (dist1Sqr < distSqr)
                    distSqr = dist1Sqr;
            }
            if (pDiag != p0 && pDiag != -1)
            {
                int x1 = pDiag & 0xffff; int y1 = (pDiag >> 16) & 0xffff;
                int dist1Sqr = CalcDistance(x, y, x1, y1, distance);
                if (dist1Sqr < distSqr)
                {
                    nearestPoint = pDiag;
                    distSqr = dist1Sqr;
                }
            }
            if (pPrev != pDiag && pPrev != -1)
            {
                int x1 = pPrev & 0xffff; int y1 = (pPrev >> 16) & 0xffff;
                int dist1Sqr = CalcDistance(x, y, x1, y1, distance);
                if (dist1Sqr < distSqr)
                {
                    nearestPoint = pPrev;
                    distSqr = dist1Sqr;
                }
            }
            points[x] = nearestPoint;
            return (float)distSqr;
        }

        private int CalcDistance(int x, int y, int x0, int y0, Distance distance)
        {
            int v = 0;
            switch (distance)
            {
                case Distance.Euclidean:
                    v = (x - x0) * (x - x0) + (y - y0) * (y - y0);
                    break;
                case Distance.Manhattan:
                    v = Math.Abs(x - x0) + Math.Abs(y - y0);
                    break;
                case Distance.Chessboard:
                    v = Math.Max(Math.Abs(x - x0), Math.Abs(y - y0));
                    break;
                case Distance.SquaredEuclidean:
                    v = (x - x0) * (x - x0) + (y - y0) * (y - y0);
                    break;
            }
            return v;
        }

        /**
         * Convert Distance map to FastBitmap.
         * @return FastBitmap.
         */
        //public FastBitmap toFastBitmap()
        //{

        //    int width = image[0].length;
        //    int height = image.length;

        //    FastBitmap fb = new FastBitmap(width, height, FastBitmap.ColorSpace.Grayscale);

        //    if (max > 255)
        //    {
        //        for (int i = 0; i < height; i++)
        //        {
        //            for (int j = 0; j < width; j++)
        //            {
        //                fb.setGray(i, j, (int)Catalano.Math.Tools.Scale(0, max, 0, 255, image[i][j]));
        //            }
        //        }
        //    }
        //    else
        //    {
        //        for (int i = 0; i < height; i++)
        //        {
        //            for (int j = 0; j < width; j++)
        //            {
        //                fb.setGray(i, j, (int)image[i][j]);
        //            }
        //        }
        //    }

        //    return fb;
        //}

        /**
         * Convert Distance map to FastBitmap.
         * @return FastBitmap.
         */
        public unsafe Bitmap ToBitmap()
        {

            int width = image[0].Length;
            int height = image.Length;

            UnmanagedImage fb = UnmanagedImage.Create(width, height, PixelFormat.Format8bppIndexed);
            Accord.Imaging.Converters.ImageToMatrix conv = new Accord.Imaging.Converters.ImageToMatrix(min: 0, max: 255);
            conv.Convert(fb, out byte[,] img);

            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    byte col = (byte)Vector.Scale(image[i][j], 0, max, 0, 255);
                    img[i, j] = col;
                }
            }

            Accord.Imaging.Converters.MatrixToImage m2i = new Accord.Imaging.Converters.MatrixToImage(min: 0, max: 255);
            m2i.Convert(img, out Bitmap output);

            return output;
        }
    }

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 22, 2017

I guess the goal isn't to match OpenCV's implementation of the DT but, unless I'm doing something wrong, this updated algorithm seems pretty far off of the DT image in this tutorial. http://docs.opencv.org/3.2.0/d2/dbd/tutorial_distance_transform.html

Before:
image

After:
image

@DiegoCatalano
Copy link
Contributor

DiegoCatalano commented Jun 23, 2017

@fdncred,

The error in your result was to determine the threshold, before executing the distance transform.

Look how the lines are connected.

image

I set the threshold to a value of 200, so I got those lines of separation from the cards.

image

Now we can apply the distance transform.

image

Apply threshold again with the value 100.

image

In the last, we need to implement a watershed with markers !!!

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 23, 2017

Thanks for explaining @DiegoCatalano. I'm able to reproduce your result.

@cesarsouza
Copy link
Member

Hi there,

I have just committed an implementation for the distance transform to the framework based on Diego's code. I might be able to include in the release I was planning to do this weekend.

Regards,
Cesar

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 26, 2017

@DiegoCatalano @cesarsouza I was going to ask whether there was watershed in Accord. Thanks for answering before I asked. LOL. This is my code snippet of trying to match OpenCV.

    //Load File into a Bitmap
    Bitmap CurBitmap = Accord.Imaging.Image.FromFile(sInputFileName).Clone(PixelFormat.Format24bppRgb);

    //Show loaded Image
    ImageBox.Show("Current Bitmap", CurBitmap);

    // This is for cards.png, change all white to black
    UnmanagedImage input = UnmanagedImage.FromManagedImage(CurBitmap);
    int width = input.Width;
    int height = input.Height;
    int pixelSize = System.Drawing.Image.GetPixelFormatSize(input.PixelFormat) / 8;
    int offset = input.Stride - input.Width * pixelSize;

    unsafe
    {
        byte* src = (byte*)input.ImageData.ToPointer();

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++, src += pixelSize)
            {
                // Change White to Black
                if (src[RGB.B] == 255 &&
                    src[RGB.G] == 255 &&
                    src[RGB.R] == 255)
                {
                    src[RGB.B] = 0;
                    src[RGB.G] = 0;
                    src[RGB.R] = 0;
                }
            }
            src += offset;
        }
    }

    // Show modified cards
    ImageBox.Show("Input Bitmap", input.ToManagedImage());

    int[,] kernel =
    {
        {+0, -1, +0},
        {-1, +4, -1},
        {+0, -1, +0}
    };
    Convolution lapConvolve = new Convolution(kernel);
    UnmanagedImage lap = lapConvolve.Apply(input);

    // Subtract images using laplacian convolution
    Subtract sub = new Subtract(lap);
    sub.ApplyInPlace(input);

    // Make Grayscale
    UnmanagedImage GrayCurBitmap = Grayscale.CommonAlgorithms.BT709.Apply(input);

    // Threshold 200 to match Diego's results
    Threshold thresh = new Threshold(200);
    thresh.ApplyInPlace(GrayCurBitmap);

    // Distance Transform
    DistanceTransform dt = new DistanceTransform(DistanceTransform.Distance.Euclidean);
    
    // Compute the DT
    float[][] dtFloat = dt.Compute(GrayCurBitmap);
                
    // Make a bitmap
    Bitmap dtBmp = dt.ToBitmap();

    // Apply Threshold to match OpenCV
    Threshold thresh2 = new Threshold(100);
    thresh2.ApplyInPlace(dtBmp);

    Dilatation3x3 dilate = new Dilatation3x3();
    dilate.ApplyInPlace(dtBmp);

    CurBitmap.Dispose();

@fdncred
Copy link
Collaborator Author

fdncred commented Jun 30, 2017

Awesome! I'll have to try it. Thanks!

@fdncred
Copy link
Collaborator Author

fdncred commented Jul 6, 2017

I am able to get a little closer to the OpenCV tutorial output but not all the way there. It appears that BinaryWatershed() works a bit different than the OpenCV version. The OpenCV version takes a seed array of approximately where the markers will be.

This output doesn't quite match the OpenCV Step 7 output. But I can see that it's similar.

image

This is the bit of code that got me to this point.

    Accord.Imaging.Filters.DistanceTransform newDT = new Accord.Imaging.Filters.DistanceTransform();
    Bitmap dtBmp = newDT.Apply(GrayCurBitmap).ToManagedImage();

    Threshold thresh2 = new Threshold(100);
    thresh2.ApplyInPlace(dtBmp);
    //ImageBox.Show("DT Thresh Bitmap", dtBmp);

    Dilatation3x3 dilate = new Dilatation3x3();
    dilate.ApplyInPlace(dtBmp);
    //ImageBox.Show("DT Thresh Dilate Bitmap", dtBmp);

    Accord.Imaging.Filters.BinaryWatershed bw = new BinaryWatershed(1.0f, DistanceTransformMethod.Euclidean);
    Bitmap wtrBmp = bw.Apply(dtBmp);

    GrayscaleToRGB toRGB = new GrayscaleToRGB();
    wtrBmp = toRGB.Apply(wtrBmp);

    PointsMarker marker = new PointsMarker(Color.Red, 5);
    marker.Points = bw.MaxPoints;
    Bitmap marked = marker.Apply(wtrBmp);

I have no clue how to get the step 8 output with Accord. I'm not even sure I understand how the OpenCV makes the card shaped contours.

@cesarsouza
Copy link
Member

Well, now I understand what @DiegoCatalano said some posts ago: it would be necessary to implement a full watershed algorithm with markers in the framework. The current implementation only goes until the step where the foreground/background markers (and their maxima) are found, and does not conclude the final step of the watershed algorithm, where the objects would have finally been segmented into their pixelwise classes.

This issue will remain open until the complete version of the watershed algorithm can be implemented.

Anyways, thanks a lot for the investigation @fdncred: I hope that this issue can be resolved soon!

Regards,
Cesar

@cesarsouza
Copy link
Member

If anyone would like to work on this issue, the framework is almost there with a full watershed implementation. As explained above, the only step missing now is the segmentation after the borders and markers have been found.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants