7

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

Trevor Elliott
  • 11,292
  • 11
  • 63
  • 102
  • I've added an updated code yesterday. Have you checked it out? It's much shorter than other versions and seems to work as you wanted it to. – Lukasz M Mar 21 '12 at 16:48

3 Answers3

8

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.


See revision history for old versions.

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) {
            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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }


        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }


        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }


        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 (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        return backpoint;
    }

    private bool ValidPoint(int x, int y) {
        return (borderdata[y * size.Width + x]);
    }

    private bool HasNeighbor(int x, int y) {
        if (y > 0) {
            if (!borderdata[(y - 1) * size.Width + x]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (x < size.Width - 1) {
            if (!borderdata[y * size.Width + (x + 1)]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (y < size.Height - 1) {
            if (!borderdata[(y + 1) * size.Width + x]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (x > 0) {
            if (!borderdata[y * size.Width + (x - 1)]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        return false;
    }

    private Point getFirstPoint(PointData data) {
        Point startpos = zeropoint;
        for (int y = 0; y < size.Height; y++) {
            for (int x = 0; x < size.Width; x++) {
                if (data[y * size.Width + x]) {
                    startpos = new Point(x, y);
                    return startpos;
                }
            }
        }
        return startpos;
    }

    private PointData getBorderData(byte[] bytes) {

        PointData isborderpoint = new PointData(size.Height * size.Width);
        bool prevtrans = false;
        bool currenttrans = false;
        for (int y = 0; y < size.Height; y++) {
            prevtrans = false;
            for (int x = 0; x <= size.Width; x++) {
                if (x == size.Width) {
                    if (!prevtrans) {
                        isborderpoint.SetPoint(y * size.Width + x - 1, true);
                    }
                    continue;
                }
                currenttrans = bytes[y * stride + x * 4 + 3] == 0;
                if (x == 0 && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                if (prevtrans && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x - 1, true);
                if (!prevtrans && currenttrans && x != 0)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                prevtrans = currenttrans;
            }
        }
        for (int x = 0; x < size.Width; x++) {
            prevtrans = false;
            for (int y = 0; y <= size.Height; y++) {
                if (y == size.Height) {
                    if (!prevtrans) {
                        isborderpoint.SetPoint((y - 1) * size.Width + x, true);
                    }
                    continue;
                }
                currenttrans = bytes[y * stride + x * 4 + 3] == 0;
                if(y == 0 && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                if (prevtrans && !currenttrans)
                    isborderpoint.SetPoint((y - 1) * size.Width + x, true);
                if (!prevtrans && currenttrans && y != 0)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                prevtrans = currenttrans;
            }
        }
        return isborderpoint;
    }
}

class PointData {
    bool[] points = null;
    int validpoints = 0;
    public PointData(int length) {
        points = new bool[length];
    }

    public int PointCount {
        get {
            return validpoints;
        }
    }

    public void SetPoint(int pos, bool state) {
        if (points[pos] != state) {
            if (state)
                validpoints++;
            else
                validpoints--;
        }
        points[pos] = state;
    }
    public bool this[int pos] {
        get {
            return points[pos];
        }
    }


}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
Wowa
  • 1,791
  • 1
  • 14
  • 24
  • Hmm there were a few images that this method didn't work with but the other method did. Trying to figure out why. – Trevor Elliott Mar 20 '12 at 03:42
  • sorry, it was 01:00am and i was tired :) see my edited code. you will still have a little ugly border at narrow spaces. to fill them you have to go in all possible directions from a point and continue your path with the shortest line between the narrow space. – Wowa Mar 20 '12 at 12:57
  • now i changed the routine a little again. It now create all possible regions and combine neighbor regions to one. if you just want all outlineregions just add all regions to a graphicspath and check if the paths intersect. – Wowa Mar 20 '12 at 22:23
  • Awesome stuff. I'm still testing it and did notice one anomaly with an image. It produced only one array of Points (correctly) but for some reason 2 points were in the wrong order causing a line to form across the middle of the image. I think the image touches the edges of the bitmap so that might be why – Trevor Elliott Mar 21 '12 at 01:18
  • now everything should work fine (see EDIT). if you still find a picture which doesn't work, please upload it and i will test around :) – Wowa Mar 21 '12 at 14:18
  • Everything of mine seems to work beautifully. Excellent work! Thank you! – Trevor Elliott Mar 21 '12 at 16:00
  • Pretty good to go this long without a problem reported...I managed to find one. See [here](http://i907.photobucket.com/albums/ac273/ulatekh/toggle-guard-open.png) for an image that reproduces a bad result. – ulatekh Sep 03 '14 at 03:06
  • It still has a problem. If forexample the duck picture sample had a whole in the center, the algorithm would mess up – Pedrum Sep 20 '14 at 22:47
  • Do you have a copy/paste error? I see `isConnected(pointstart, p) && isConnected(pointstart, p)` and `(isConnected(pointend, p) && isConnected(pointend, p))` – ulatekh Sep 23 '19 at 21:01
3

I've modified GetOutlinePoints by adding a helper variable than verifies the position at which new points should be added.

The idea of the outline detection algorithm in your code is something like looking at the image while standing each of its edges and writing down all non-transparent pixels that are visible. It's ok, however, you always added pixels to the end of the collection, which caused issues. I've added a variable that remembers position of the last pixel visible from the previous edge and the current one and uses it to determine index where the new pixel should be added. I suppose it should work as long as the outline is continuous, but I suppose it is in your case.

I've tested it on several images and it seems to work correctly:

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);
        //find non-transparent pixels visible from the top of the image
        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;
                }
            }
        }

        //helper variable for storing position of the last pixel visible from both sides 
        //or last inserted pixel
        int? lastInsertedPosition = null;
        //find non-transparent pixels visible from the right side of the image
        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))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

        lastInsertedPosition = null;
        //find non-transparent pixels visible from the bottom of the image
        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))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

        lastInsertedPosition = null;
        //find non-transparent pixels visible from the left side of the image
        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))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

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

        image.UnlockBits(bitmapData);

        return outlinePoints.ToArray();
    }

Update: This algorithm won't work correctly for images that have outline parts not "visible" from any of the edges. See comments for suggested solutions. I'll try to post a code snippet later.

Update II

I've prepared another algorithm as described in my comments. It just crawls around the object and saves the outline.

First, it finds the first pixel of the outline using a method from the previous algorithm. Then, it looks throught the neighbouring pixels in clockwise order, find the first one that is transparent and then continues browsing, but looking for a non-transparent one. The first non-transparent pixel found is the next one in the outline. The loop continues until it goes around the whole object and gets back to the starting pixel.

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

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

        Point currentP = new Point(0, 0);
        Point firstP = new Point(0, 0);

        byte[] originalBytes = new byte[image.Width * image.Height * 4];
        Marshal.Copy(bitmapData.Scan0, originalBytes, 0, originalBytes.Length);
        //find non-transparent pixels visible from the top of the image
        for (int x = 0; x < bitmapData.Width && outlinePoints.Count == 0; x++)
        {
            for (int y = 0; y < bitmapData.Height && outlinePoints.Count == 0; y++)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

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

                    outlinePoints.Add(p);
                    currentP = p;
                    firstP = p;

                    break;
                }
            }
        }

        Point[] neighbourPoints = new Point[] { new Point(-1, -1), new Point(0, -1), new Point(1, -1), 
                                                new Point(1, 0), new Point(1, 1), new Point(0, 1), 
                                                new Point(-1, 1), new Point(-1, 0) };

        //crawl around the object and look for the next pixel of the outline
        do
        {
            bool transparentNeighbourFound = false;
            bool nextPixelFound = false;
            int i;
            //searching is done in clockwise order
            for (i = 0; (i < neighbourPoints.Length * 2) && !nextPixelFound; ++i)
            {
                int neighbourPosition = i % neighbourPoints.Length;

                int x = currentP.X + neighbourPoints[neighbourPosition].X;
                int y = currentP.Y + neighbourPoints[neighbourPosition].Y;

                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                //a transparent pixel has to be found first
                if (!transparentNeighbourFound)
                {
                    if (alpha == 0)
                    {
                        transparentNeighbourFound = true;
                    }
                }
                else //after a transparent pixel is found, a next non-transparent one is the next pixel of the outline
                {
                    if (alpha != 0)
                    {
                        Point p = new Point(x, y);

                        currentP = p;
                        outlinePoints.Add(p);
                        nextPixelFound = true;
                    }
                }
            }
        } while (currentP != firstP);

        image.UnlockBits(bitmapData);

        return outlinePoints.ToArray();
    }

One thing to remember is that it does work IF object does NOT ends at the edge of the image (there has to be a transparent space between the object and each of the edges).

This can be easily done if you just make the image one line larger at each side before passing it to the GetOutlinePointsNEW method.

Lukasz M
  • 5,635
  • 2
  • 22
  • 29
  • Yes this seems to work perfectly with all of my 50 or so images. – Trevor Elliott Mar 19 '12 at 19:36
  • You hit the nail on the head with that. I knew I had to insert those odd points but I had no idea how to calculate where. I have to wait 22 hours to award your bounty. – Trevor Elliott Mar 19 '12 at 19:42
  • I'm glad it helped :). I'll look more into this code later, because I think I'm not sure if it works correctly in some special cases. I'll investigate this later. Also, let me know if you find any bug in this code. – Lukasz M Mar 19 '12 at 19:45
  • Ok, I've found it. In some images (like in the duck image that you've posted), there are some points that are NOT "visible from any side" of the image. In those case the algorithm won't work, but I suppose it would require choosing another algorithm to fix it. I'll investigate it and write about it if I find any modification to make it work in those cases as well. – Lukasz M Mar 19 '12 at 20:13
  • I think I may have a solution by modifying your code (I haven't tested it yet), but I would like you to confirm if you need it or if the current version satisfies your requirements. – Lukasz M Mar 19 '12 at 20:29
  • That makes sense. None of my images are like that, but I might have some like that in the future. I guess the best way to do it would be to "crawl" around the edge and look at a group of 9 pixels at a time to find the path. – Trevor Elliott Mar 19 '12 at 20:29
  • Certainly any suggestions you have to allow photos like that ducky image work would be welcome. – Trevor Elliott Mar 19 '12 at 20:53
  • You could for example find one pixel (just like you find the first one now) and check neighbouring pixels for a non transparent (i.e. in order: top, top-right, right, bottom-right bottom, bottom-left, left, top-left) and select the first non-transparent as the next one in the outline. I suppose that this would simulate something like crawling around the object. – Lukasz M Mar 19 '12 at 21:31
  • Sorry, I haven't noticed you described the same in your comment :P. What I can suggest it to find a transparent and a neighbouring non-transparent pixel from this group and use this to detect the next outline pixel from the nine neighbours. – Lukasz M Mar 19 '12 at 21:49
  • I've posted a quite simple crawling algorithm that seems to work correctly. Look at my updated answer. – Lukasz M Mar 20 '12 at 21:27
  • I tested it and it seems to work perfectly for a single collection of pixels (as long as I give 1px of room around the edge). And it's worth noting that it's much faster than Wowa's solution so it's probably better for run-time calculations. – Trevor Elliott Mar 21 '12 at 17:22
  • However Wowa's solution goes above and beyond what I expected with detecting pockets of transparency for hollow objects, or multiple collections of pixels inside a single image. So for example if I have an image of a donut, I can detect if they click through the hole in the center, and I can fill a brush over the donut without also filling the inner hole for realistic effects like highlighting an image when it is selected. – Trevor Elliott Mar 21 '12 at 17:24
  • I suppose my version of this algorithm can be further modified to work with multiple separate areas with non-transparent pixels. Anyway, I'm glad that you've got a solution you liked :). – Lukasz M Mar 21 '12 at 21:17
1

The scenario by the poster was one I just encountered. After applying the GetOutlinePointsNEW method above, I encountered an index issue when the non-transparent pixel is at the edge of the image causing the crawl to be outside the bounds of the image. Below is a managed code update that treats the out of bounds pixels as non-transparent when performing the crawl.

    static public Point[] crawlerPoints = new Point[] { new Point(-1, -1), new Point(0, -1), new Point(1, -1),
                                            new Point(1, 0), new Point(1, 1), new Point(0, 1),
                                            new Point(-1, 1), new Point(-1, 0) };

    private BitmapData _bitmapData;
    private byte[] _originalBytes;
    private Bitmap _bitmap;  //this is loaded from an image passed in during processing

    public Point[] GetOutlinePoints()
    {
        List<Point> outlinePoints = new List<Point>();
        _originalBytes = new byte[_bitmap.Width * _bitmap.Height * 4];
        _bitmapData = _bitmap.LockBits(new Rectangle(0, 0, _bitmap.Width, _bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        Marshal.Copy(_bitmapData.Scan0, _originalBytes, 0, _originalBytes.Length);
        GetFirstNonTransparentPoint(outlinePoints);
        if (outlinePoints.Count > 0) { GetNonTransparentPoints(outlinePoints); }
        _bitmap.UnlockBits(_bitmapData);

        return outlinePoints.ToArray();
    }
    private void GetFirstNonTransparentPoint(List<Point> outlinePoints)
    {
        Point firstPoint = new Point(0, 0);

        for (int x = 0; x < _bitmapData.Width; x++)
        {
            for (int y = 0; y < _bitmapData.Height; y++)
            {
                if (!IsPointTransparent(x, y))
                {
                    firstPoint = new Point(x, y);
                    outlinePoints.Add(firstPoint);
                    break;
                }
            }
            if (outlinePoints.Count > 0) { break; }
        }            
    }
    private void GetNonTransparentPoints(List<Point> outlinePoints)
    {
        Point currentPoint = outlinePoints[0];
        do   //Crawl counter clock-wise around the current point
        {
            bool firstTransparentNeighbourFound = false;
            bool nextPixelFound = false;
            for (int i = 0; (i < ApplicationVariables.crawlerPoints.Length * 2) && !nextPixelFound; ++i)
            {
                int crawlPosition = i % ApplicationVariables.crawlerPoints.Length;
                if (!firstTransparentNeighbourFound) { firstTransparentNeighbourFound = IsCrawlPointTransparent(crawlPosition, currentPoint); }
                else
                {
                    if (!IsCrawlPointTransparent(crawlPosition, currentPoint))
                    {
                        outlinePoints.Add(new Point(currentPoint.X + ApplicationVariables.crawlerPoints[crawlPosition].X, currentPoint.Y + ApplicationVariables.crawlerPoints[crawlPosition].Y));
                        currentPoint = outlinePoints[outlinePoints.Count - 1];
                        nextPixelFound = true;
                    }
                }
            }
        } while (currentPoint != outlinePoints[0]);
    }

    private bool IsCrawlPointTransparent(int crawlPosition, Point currentPoint)
    {
        int x = currentPoint.X + ApplicationVariables.crawlerPoints[crawlPosition].X;
        int y = currentPoint.Y + ApplicationVariables.crawlerPoints[crawlPosition].Y;

        if (IsCrawlInBounds(x, y)) { return IsPointTransparent(x, y); }
        return true;
    }
    private bool IsCrawlInBounds(int x, int y)
    {
        return ((x >= 0 & x < _bitmapData.Width) && (y >= 0 & y < _bitmapData.Height));
    }
    private bool IsPointTransparent(int x, int y)
    {
        return _originalBytes[(y * _bitmapData.Stride) + (4 * x) + 3] == 0;
    }
Galactic
  • 400
  • 4
  • 14