Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

I want to create a GraphicsPath and a list of Points to form the outline of the non-transparent area of a bitmap. If needed, I can guarantee that each image has only one solid collection of nontransparent pixels. So for example, I should be able to record the points either clockwise or counter-clockwise along the edge of the pixels and perform a full closed loop.

The speed of this algorithm is not important. However, the efficiency of the resulting points is semi-important if I can skip some points to reduce in a smaller and less complex GraphicsPath.

I will list my current code below which works perfectly with most images. However, some images which are more complex end up with paths which seem to connect in the wrong order. I think I know why this occurs, but I can't come up with a solution.

public static Point[] GetOutlinePoints(Bitmap image)
{
    List<Point> outlinePoints = new List<Point>();

    BitmapData bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

    byte[] originalBytes = new byte[image.Width * image.Height * 4];
    Marshal.Copy(bitmapData.Scan0, originalBytes, 0, originalBytes.Length);

    for (int x = 0; x < bitmapData.Width; x++)
    {
        for (int y = 0; y < bitmapData.Height; y++)
        {
            byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

            if (alpha != 0)
            {
                Point p = new Point(x, y);

                if (!ContainsPoint(outlinePoints, p))
                    outlinePoints.Add(p);

                break;
            }
        }
    }

    for (int y = 0; y < bitmapData.Height; y++)
    {
        for (int x = bitmapData.Width - 1; x >= 0; x--)
        {
            byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

            if (alpha != 0)
            {
                Point p = new Point(x, y);

                if (!ContainsPoint(outlinePoints, p))
                    outlinePoints.Add(p);

                break;
            }
        }
    }

    for (int x = bitmapData.Width - 1; x >= 0; x--)
    {
        for (int y = bitmapData.Height - 1; y >= 0; y--)
        {
            byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

            if (alpha != 0)
            {
                Point p = new Point(x, y);

                if (!ContainsPoint(outlinePoints, p))
                    outlinePoints.Add(p);

                break;
            }
        }
    }

    for (int y = bitmapData.Height - 1; y >= 0; y--)
    {
        for (int x = 0; x < bitmapData.Width; x++)
        {
            byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

            if (alpha != 0)
            {
                Point p = new Point(x, y);

                if (!ContainsPoint(outlinePoints, p))
                    outlinePoints.Add(p);

                break;
            }
        }
    }

    // Added to close the loop
    outlinePoints.Add(outlinePoints[0]);

    image.UnlockBits(bitmapData);

    return outlinePoints.ToArray();
}

public static bool ContainsPoint(IEnumerable<Point> points, Point value)
{
    foreach (Point p in points)
    {
        if (p == value)
            return true;
    }

    return false;
}

And when I turn the points into a path:

GraphicsPath outlinePath = new GraphicsPath();
outlinePath.AddLines(_outlinePoints);

Here's an example showing what I want. The red outline should be an array of points which can be made into a GraphicsPath in order to perform hit detection, draw an outline pen, and fill it with a brush.

Example of an Outline

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
242 views
Welcome To Ask or Share your Answers For Others

1 Answer

Like both of you desrcipt you just have to find the first non transparent point and afterward move along the none transparent pixels with a transparent neighbor.

Additionaly you'll have to save the point you've already visisted and how often you visited them or you'll end in same cases in an invinity loop. If the point doesn't have a neighbor which already was visited you must go back each point, in revered direction, until a unvisited point is available again.

That's it.


//CODE REMOVED - POST WAS TO LONG


EDIT 1

Modified code:


//CODE REMOVED - POST WAS TO LONG


EDIT 2

Now all regions are returned:


//CODE REMOVED - POST WAS TO LONG


EDIT 3

Changes:

  • Point.EMPTY was replaced by Point(-1,-1), or a non transparent pixel in the upper left corner causes an invinityloop
  • Check for borderpoint at the image border

class BorderFinder {

    int stride = 0;
    int[] visited = null;
    byte[] bytes = null;
    PointData borderdata = null;
    Size size = Size.Empty;
    bool outside = false;
    Point zeropoint = new Point(-1,-1);

    public List<Point[]> Find(Bitmap bmp, bool outside = true) {
        this.outside = outside;
        List<Point> border = new List<Point>();
        BitmapData bmpdata = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        stride = bmpdata.Stride;
        bytes = new byte[bmp.Width * bmp.Height * 4];
        size = bmp.Size;


        Marshal.Copy(bmpdata.Scan0, bytes, 0, bytes.Length);


        // Get all Borderpoint
        borderdata = getBorderData(bytes);

        bmp.UnlockBits(bmpdata);

        List<List<Point>> regions = new List<List<Point>>();

        //Loop until no more borderpoints are available
        while (borderdata.PointCount > 0) {
            List<Point> region = new List<Point>();

            //if valid is false the region doesn't close
            bool valid = true;

            //Find the first borderpoint from where whe start crawling
            Point startpos = getFirstPoint(borderdata);

            //we need this to know if and how often we already visted the point.
            //we somtime have to visit a point a second time because we have to go backward until a unvisted point is found again
            //for example if we go int a narrow 1px hole
            visited = new int[bmp.Size.Width * bmp.Size.Height];

            region.Add(startpos);

            //Find the next possible point
            Point current = getNextPoint(startpos);

            if (current != zeropoint) {
                visited[current.Y * bmp.Width + current.X]++;
                region.Add(current);
            }

            //May occure with just one transparent pixel without neighbors
            if (current == zeropoint)
                valid = false;

            //Loop until the area closed or colsing the area wasn't poosible
            while (!current.Equals(startpos) && valid) {
                var pos = current;
                //Check if the area was aready visited
                if (visited[current.Y * bmp.Width + current.X] < 2) {
                    current = getNextPoint(pos);
                    visited[pos.Y * bmp.Width + pos.X]++;
                    //If no possible point was found, search in reversed direction
                    if (current == zeropoint)
                        current = getNextPointBackwards(pos);
                } else { //If point was already visited, search in reversed direction
                    current = getNextPointBackwards(pos);
                }

                //No possible point was found. Closing isn't possible
                if (current == zeropoint) {
                    valid = false;
                    break;
                }

                visited[current.Y * bmp.Width + current.X]++;

                region.Add(current);
            }
            //Remove point from source borderdata
            foreach (var p in region) {
                borderdata.SetPoint(p.Y * bmp.Width + p.X, false);
            }
            //Add region if closing was possible
            if (valid)
                regions.Add(region);
        }

        //Checks if Region goes the same way back and trims it in this case
        foreach (var region in regions) {
            int duplicatedpos = -1;

            bool[] duplicatecheck = new bool[size.Width * size.Height];
            int length = region.Count;
            for (int i = 0; i < length; i++) {
                var p = region[i];
                if (duplicatecheck[p.Y * size.Width + p.X]) {
                    duplicatedpos = i - 1;
                    break;
                }
                duplicatecheck[p.Y * size.Width + p.X] = true;
            }

            if (duplicatedpos == -1)
                continue;

            if (duplicatedpos != ((region.Count - 1) / 2))
                continue;

            bool reversed = true;

            for (int i = 0; i < duplicatedpos; i++) {
                if (region[duplicatedpos - i - 1] != region[duplicatedpos + i + 1]) {
                    reversed = false;
                    break;
                }
            }

            if (!reversed)
                continue;

            region.RemoveRange(duplicatedpos + 1, region.Count - duplicatedpos - 1);
        }

        List<List<Point>> tempregions = new List<List<Point>>(regions);
        regions.Clear();

        bool connected = true;
        //Connects region if possible
        while (connected) {
            connected = false;
            foreach (var region in tempregions) {
                int connectionpos = -1;
                int connectionregion = -1;
                Point pointstart = region.First();
                Point pointend = region.Last();
                for (int ir = 0; ir < regions.Count; ir++) {
                    var otherregion = regions[ir];
                    if (region == otherregion)
                        continue;

                    for (int ip = 0; ip < otherregion.Count; ip++) {
                        var p = otherregion[ip];
                        if ((isConnected(pointstart, p) && isConnected(pointend, p)) ||
                            (isConnected(pointstart, p) && isConnected(pointstart, p))) {
                            connectionregion = ir;
                            connectionpos = ip;
                        }

                        if ((isConnected(pointend, p) && isConnected(pointend, p))) {
                            region.Reverse();
                            connectionregion = ir;
                            connectionpos = ip;
                        }
                    }

                }

                if (connectionpos == -1) {
                    regions.Add(region);
                } else {
                    regions[connectionregion].InsertRange(connectionpos, region);
                }

            }

            tempregions = new List<List<Point>>(regions);
            regions.Clear();
        }

        List<Point[]> returnregions = new List<Point[]>();

        foreach (var region in tempregions)
            returnregions.Add(region.ToArray());

        return returnregions;
    }

    private bool isConnected(Point p0, Point p1) {

        if (p0.X == p1.X && p0.Y - 1 == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y - 1 == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y - 1 == p1.Y)
            return true;

        return false;
    }

    private Point getNextPoint(Point pos) {
        if (pos.Y > 0) {
            int x = pos.X;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.Y > 0 && pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X < size.Width - 1 && pos.Y < size.Height - 1) {
            int x = pos.X + 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.Y < size.Height - 1) {
            int x = pos.X;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }


        if (pos.Y < size.Height - 1 && pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X > 0 && pos.Y > 0) {
            int x = pos.X - 1;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }


        return zeropoint;
    }

    private Point getNextPointBackwards(Point pos) {
        Point backpoint = zeropoint;

        int trys = 0;

        if (pos.X > 0 && pos.Y > 0) {
            int x = pos.X - 1;
            int y = pos.Y - 1;
            if (ValidPoint(x, y) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.X > 0) {
         

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...