Introduction: C# Edge Detection
This project is a simple example of edge detection. This app takes an image, finds the edges using filters and strength of those filters, replaces all colors with one then spits out the coordinates for the edge.
I know there are apps and libraries to do this stuff but I just wanted to take a stab at it for fun. Know that this app is still being developed with the intention of possibly comparing a database of shapes to identify plants. Again just because.
The code and executable can be found at the end of this Instructable.
Step 1: The Filters Class
public class FilterMatrix { public static double[,] Laplacian3x3 { get { return new double[,] { { -1, -1, -1, }, { -1, 8, -1, }, { -1, -1, -1, }, }; } } public static double[,] Laplacian5x5 { get { return new double[,] { { -1, -1, -1, -1, -1, }, { -1, -1, -1, -1, -1, }, { -1, -1, 24, -1, -1, }, { -1, -1, -1, -1, -1, }, { -1, -1, -1, -1, -1 }, }; } } public static double[,] LaplacianOfGaussian { get { return new double[,] { { 0, 0, -1, 0, 0 }, { 0, -1, -2, -1, 0 }, { -1, -2, 16, -2, -1 }, { 0, -1, -2, -1, 0 }, { 0, 0, -1, 0, 0 }, }; } } public static double[,] Gaussian3x3 { get { return new double[,] { { 1, 2, 1, }, { 2, 4, 2, }, { 1, 2, 1, }, }; } } public static double[,] Gaussian5x5Type1 { get { return new double[,] { { 2, 04, 05, 04, 2 }, { 4, 09, 12, 09, 4 }, { 5, 12, 15, 12, 5 }, { 4, 09, 12, 09, 4 }, { 2, 04, 05, 04, 2 }, }; } } public static double[,] Gaussian5x5Type2 { get { return new double[,] { { 1, 4, 6, 4, 1 }, { 4, 16, 24, 16, 4 }, { 6, 24, 36, 24, 6 }, { 4, 16, 24, 16, 4 }, { 1, 4, 6, 4, 1 }, }; } } public static double[,] Sobel3x3Horizontal { get { return new double[,] { { -1, 0, 1, }, { -2, 0, 2, }, { -1, 0, 1, }, }; } } public static double[,] Sobel3x3Vertical { get { return new double[,] { { 1, 2, 1, }, { 0, 0, 0, }, { -1, -2, -1, }, }; } } public static double[,] Prewitt3x3Horizontal { get { return new double[,] { { -1, 0, 1, }, { -1, 0, 1, }, { -1, 0, 1, }, }; } } public static double[,] Prewitt3x3Vertical { get { return new double[,] { { 1, 1, 1, }, { 0, 0, 0, }, { -1, -1, -1, }, }; } } public static double[,] Kirsch3x3Horizontal { get { return new double[,] { { 5, 5, 5, }, { -3, 0, -3, }, { -3, -3, -3, }, }; } } public static double[,] Kirsch3x3Vertical { get { return new double[,] { { 5, -3, -3, }, { 5, 0, -3, }, { 5, -3, -3, }, }; } } }
Using the filters class
public void filter(string xfilter, string yfilter)
{ double[,] xFilterMatrix; double[,] yFilterMatrix; switch (xfilter) { case "Laplacian3x3": xFilterMatrix = FilterMatrix.Laplacian3x3; break; case "Laplacian5x5": xFilterMatrix = FilterMatrix.Laplacian5x5; break; case "LaplacianOfGaussian": xFilterMatrix = FilterMatrix.LaplacianOfGaussian; break; case "Gaussian3x3": xFilterMatrix = FilterMatrix.Gaussian3x3; break; case "Gaussian5x5Type1": xFilterMatrix = FilterMatrix.Gaussian5x5Type1; break; case "Gaussian5x5Type2": xFilterMatrix = FilterMatrix.Gaussian5x5Type2; break; case "Sobel3x3Horizontal": xFilterMatrix = FilterMatrix.Sobel3x3Horizontal; break; case "Sobel3x3Vertical": xFilterMatrix = FilterMatrix.Sobel3x3Vertical; break; case "Prewitt3x3Horizontal": xFilterMatrix = FilterMatrix.Prewitt3x3Horizontal; break; case "Prewitt3x3Vertical": xFilterMatrix = FilterMatrix.Prewitt3x3Vertical; break; case "Kirsch3x3Horizontal": xFilterMatrix = FilterMatrix.Kirsch3x3Horizontal; break; case "Kirsch3x3Vertical": xFilterMatrix = FilterMatrix.Kirsch3x3Vertical; break; default: xFilterMatrix = FilterMatrix.Laplacian3x3; break; } switch (yfilter) { case "Laplacian3x3": yFilterMatrix = FilterMatrix.Laplacian3x3; break; case "Laplacian5x5": yFilterMatrix = FilterMatrix.Laplacian5x5; break; case "LaplacianOfGaussian": yFilterMatrix = FilterMatrix.LaplacianOfGaussian; break; case "Gaussian3x3": yFilterMatrix = FilterMatrix.Gaussian3x3; break; case "Gaussian5x5Type1": yFilterMatrix = FilterMatrix.Gaussian5x5Type1; break; case "Gaussian5x5Type2": yFilterMatrix = FilterMatrix.Gaussian5x5Type2; break; case "Sobel3x3Horizontal": yFilterMatrix = FilterMatrix.Sobel3x3Horizontal; break; case "Sobel3x3Vertical": yFilterMatrix = FilterMatrix.Sobel3x3Vertical; break; case "Prewitt3x3Horizontal": yFilterMatrix = FilterMatrix.Prewitt3x3Horizontal; break; case "Prewitt3x3Vertical": yFilterMatrix = FilterMatrix.Prewitt3x3Vertical; break; case "Kirsch3x3Horizontal": yFilterMatrix = FilterMatrix.Kirsch3x3Horizontal; break; case "Kirsch3x3Vertical": yFilterMatrix = FilterMatrix.Kirsch3x3Vertical; break; default: yFilterMatrix = FilterMatrix.Laplacian3x3; break; }
Step 2: Applying the Filters
Excerpt 1 - Pixel by pixel scan
Bitmap newbitmap = new Bitmap(pictureBoxPreview.Image); BitmapData newbitmapData = new BitmapData(); newbitmapData = newbitmap.LockBits(new Rectangle(0, 0, newbitmap.Width, newbitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb); byte[] pixelbuff = new byte[newbitmapData.Stride * newbitmapData.Height]; byte[] resultbuff = new byte[newbitmapData.Stride * newbitmapData.Height]; Marshal.Copy(newbitmapData.Scan0, pixelbuff, 0, pixelbuff.Length); newbitmap.UnlockBits(newbitmapData); double blue = 0.0; double green = 0.0; double red = 0.0;
Excerpt 2 - Narrowing colors to Green using a scale to set the threshold
Notice that we are only keeping green out of the RGB
//blueTotal = Math.Sqrt((blueX * blueX) + (blueY * blueY)); blueTotal = 0; greenTotal = Math.Sqrt((greenX * greenX) + (greenY * greenY)); //redTotal = Math.Sqrt((redX * redX) + (redY * redY)); redTotal = 0; if (blueTotal > 255) { blueTotal = 255; } else if (blueTotal < 0) { blueTotal = 0; } if (greenTotal > 255) { greenTotal = 255; } else if (greenTotal < 0) { greenTotal = 0; } try { if (greenTotal < Convert.ToInt32(trackBarThreshold.Value)) { greenTotal = 0; } else { greenTotal = 255; } } catch (Exception) { throw; }
Step 3: Grab XY Coordinates
Going through the image pixel by pixel matching on color we get a list of X/Y coordinates that we can plot.
Excerpt 1
for (x = 0; x < width; x++) { for (y = 0; y < height; y++) { Color pixelColor = Color.FromArgb(bitmapIMG.GetPixel(x, y).ToArgb()); if (pixelColor.Name != "ff000000" && pixelColor.Name != "0") { coord = coord + x.ToString() + "," + y.ToString() + "|"; double newX = Convert.ToDouble(x); double newY = Convert.ToDouble(y); int angle = 110; //Rotate newX = newX * Math.Cos(angle) - newY * Math.Sin(angle); newY = newX * Math.Sin(angle) + newY * Math.Cos(angle); Image.coord imgCoord = new Image.coord(); imgCoord.x = newX; imgCoord.y = newY; coorArray.Add(imgCoord); coord = coord + newX.ToString() + "," + newY.ToString() + "|"; chart1.Series["plot"].Points.AddXY(newX, newY); } } }
Step 4: Rotating All Points on Plot
After you grab the x/y coordinates from the image post edge detection and filtering and plot them to a chart the image is upside down. To correct this we do some simple algebra. (will attach image of result soon)
Rotation
counterclockwise by angle
x = x * cos(theta) - y * sin(theta)
y = x * sin(theta) + y * cos(theta)
theta = the angle at which to rotate
In c# you would need to do this for each coordinate in your collection
for (int i = 0; i < topCoord.Length; i++) { string[] bottomCoord = topCoord[i].Split(','); double newX = Convert.ToDouble(bottomCoord[0]); double newY = Convert.ToDouble(bottomCoord[1]); int angle = 110; newX = newX * Math.Cos(angle) - newY * Math.Sin(angle); newY = newX * Math.Sin(angle) + newY * Math.Cos(angle); Image.coord imgCoord = new Image.coord(); imgCoord.x = newX; imgCoord.y = newY; coorArray.Add(imgCoord); coorArray2.Add(imgCoord); chart1.Series["plot"].Points.AddXY(newX, newY); }
Step 5: Cleaning Up Unused Points
In the images above you can see that there are a lot of stray points after the edge detection. I would like to remove those.
This is still in theory but I think I can remove un-needed points using a form of the following equation. Basically checking to see if a set of coordinates is connected to another, using a recursive function and creating arrays of connected pairs until finished keeping the largest set.
x1, y1
x2, y2
x1 == x2 & (y1 == y2 || y1 == (y2-1) || y1 == (y2+1)
y1 == y2 & x1 == x2 || x1 == (x2-1) || x1 == (x2+1)
Step 6: The Files
5/12/15 Updated code to correctly output x,y coordinates.
5/13/15 Corrected a few more issues with code
5/25/15 Used the code snippets to better display code and added the rotation implementation in c#
Attached is the sourcecode and executable. Latest code is published to GitHub https://github.com/MrRedBeard/EdgeDetection
Once the application is open the leaf image is pre-loaded. Just select the filters you want to use, change the threshold, then click Apply Filters.
Step 7: Notes
Arrays/Collections
If you're wonder why I have been using strings and split here's why. I have been trying to get an IList of a class to hold the coordinates but for whatever reason it errors out and quits without throwing an exception.
Future
I plan to separate the code out into functions and expand the classes.
Will be researching algorithms that will allow me to match shapes.
Develop scaling and rotation to best match shapes to those in database.
Database
I want to eventually create a database of shape data.
Collaboration
If anyone would like to help let me know.