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.
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:
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.
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.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: