I modified GetOutlinePoints by adding a helper variable, which checks the position at which new points should be added.
The idea of โโthe contour detection algorithm in your code is something like searching for an image standing on each of its edges and recording all the opaque pixels that are visible. This is normal, however, you always added pixels to the end of the collection, which caused problems. I added a variable that remembers the position of the last pixel visible from the previous edge and the current one, and uses it to determine the index at which the new pixel should be added. I assume that it should work as long as the contour is continuous, but I suppose this is in your case.
I tested it on multiple 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 will not work correctly for images in which the contour parts are not "visible" from any of the edges. See comments on proposed solutions. I will try to post a code snippet later.
Update II
I prepared another algorithm as described in my comments. It just walks around the object and maintains the outline.
First, he finds the first pixel of the path using the method from the previous algorithm. Then it scans adjacent pixels clockwise, finds the first transparent one, and then continues to view, but searches for an opaque one. The first opaque pixel is found next in the path. The cycle continues until it bypasses the entire object and returns to the original 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 works. An IF object does NOT end at the edge of the image (there should be a transparent space between the object and each of the edges).
This can be easily done if you simply make the image one line larger on each side before passing it to the GetOutlinePointsNEW method.