Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Image Processing for Dummies with C# and GDI+ Part 3 - Edge Detection Filters

0.00/5 (No votes)
31 Mar 2002 9  
The third in a series of articles which will build an image processing library in C# and GDI+

Sample Image

Introduction

Welcome back. This is probably goign to be the last in this series for a while, I want to focus on some other things to learn some more C#, and come back to this when I have some more time.

Overview

This article will focus on one of the most common image processing tasks, detecting edges.  We will look at a number of ways to do this, and also look at one use for such information, an edge enhance filter.  We will start with what we know from the last article, using convolution filters to detect edges.

Convolution Filters - Sobel, Prewitt and Kirsh

We will use three different convolution masks to detect edges, named presumably after their inventors.  In each case, we apply a horizontal version of the filter to one bitmap, a vertical version to another, and the formula pixel = sqrt(pixel1 * pixel1 + pixel2 * pixel2) to merge them together.  Hopefully you're familiar enough with the previous articles to know what the code would look like to do this.  The convolution masks look like this:

Sobell
1 2 1
0 0 0
-1 -2 -1 /1+0
Prewitt
1 1 1
0 0 0
-1 -1 -1 /1+0
Kirsh
5 5 5
-3 -3 -3
-3 -3 -3 /1+0

These filters perform the horizontal edge detect, rotating them 90 degrees gives us the vertical, and then the merge takes place.

How do they work ?

Edge detection filters work essentially by looking for contrast in an image.  This can be done a number of different ways, the convolution filters do it by applying a negative weight on one edge, and a positive on the other.  This has the net effect of trending towards zero if the values are the same, and trending upwards as contrast exists.  This is precisely how our emboss filter worked, and using an offset of 127 would again make these filters look similar to our previous embossing filter.  The following examples follow the different filter types in the same order as the filters above.  The images have a tooltip if you want to be sure which is which.  These three filters also allow specification of a threshold.  Any value below this threshold will be clamped to it.  For the test I have kept the threshold at 0.

My kids    Sobel Edge Detection

Prewitt Edge Detection    Kirsh Edge Detection

Horizontal and Vertical Edge Detection

To perform an edge detection operation in just the horizontal or vertical planes, we can again use a convolution method.  However, rather than use our framework for 3x3 filters, we are better off writing the code from scratch so that our filter ( which will be a Prewitt filter ) will be either very wide, or very high.  I've chosen 7 as a good umber, our horizontal filter is 7x3 and our vertical filter is 3x7.  The code is not dissimilar enough from what we've already done to warrant showing it to you especially, but it's there if you want to have a look.  Following is the result first of our horizontal filter, and then the vertical one.

Horizontal Edge Detection Vertical Edge Detection

There's more to life than convolution

Convolution filters can do some cool stuff, and if you did a search online, you'd be forgiven for thinking that they are behind all image processing.  However, it's probably more true that the sort of filters you see in Photoshop as especially written to directly do what a convolution filter can only imitate.  I'd again point to the Photoshop embossing filter with it's range of options as evidence of this.

The problem with convolution for edge detection is not so much that the process is unsatisfactory, as much as unnecessarily expensive.  I'm going to cover two more methods of edge detection, which both involve us iterating through the image directly and doing a number of compares on neighbouring pixels, but which treat the resultant values differently to a convolution filter.

Homogenity Edge Detection

If we are to perceive an edge in an image, it follows that there is a change in colour between two objects, for an edge to be apparent.  To put it another way, if we were to take a pixel and store as it's value the greatest difference between it's starting value and the values of it's eight neighbours, we would come up with black where the pixels are the same, and trend towards white the harder the colour difference was.  We would detect the edges in the image.  Furthermore, if we allowed a threshold to be set, and set values below this to 0, we could eliminate soft edges to whatever degree we desires.  The code to do this is followed by an example at threshold 0 and one at threshold 127.

public static bool EdgeDetectHomogenity(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 

    // pixel and it's eight neighbours. The threshold allows softer edges to 

    // be forced down to black, use 0 to negate it's effect.

    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.

    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                     ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for(int y=1;y<b.Height-1;++y)
        {
            p += 3;
            p2 += 3;

            for(int x=3; x < nWidth-3; ++x )
            {
                nPixelMax = Math.Abs(p2[0] - (p2+stride-3)[0]);
                nPixel = Math.Abs(p2[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 + stride + 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride + 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                if (nPixelMax < nThreshold) nPixelMax = 0;

                p[0] = (byte) nPixelMax;

                ++ p;
                ++ p2;
            }

            p += 3 + nOffset;
            p2 += 3 + nOffset;
        }
    }

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;
    
}

Homogenousl Edge Detection, 0 threshold Homogenousl Edge Detection, 127 threshold

Difference Edge Detection

The difference edge detection works in a similar way, but it detects the difference between pairs of pixel around the pixel we are setting.  It works out the highest value from the difference of the four pairs of pixels that can be used to form a line through the middle pixel.  The threshold works the same as the homogenity filter.  Again, here is the code, followed by two examples, one with no threshold, one with a threshold of 127.

public static bool EdgeDetectDifference(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 

    // pixel and it's eight neighbours. The threshold allows softer edges 

    // to be forced down to black, use 0 to negate it's effect.

    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.

    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                     ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for(int y=1;y<b.Height-1;++y)
        {
            p += 3;
            p2 += 3;

            for(int x=3; x < nWidth-3; ++x )
            {
                nPixelMax = Math.Abs((p2 - stride + 3)[0] - (p2+stride-3)[0]);
                nPixel = Math.Abs((p2 + stride + 3)[0] - (p2 - stride - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 - stride)[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2+3)[0] - (p2 - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                if (nPixelMax < nThreshold) nPixelMax = 0;

                p[0] = (byte) nPixelMax;

                ++ p;
                ++ p2;
            }

            p += 3 + nOffset;
            p2 += 3 + nOffset;
        }
    }

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;
    
}

Difference Edge Detection, 0 threshold Difference Edge Detection, 0 threshold

Edge Enhancement

One thing we can use edge detection for is to enhance edges in an image.  The concept is simple - we apply an edge filter, but we only store the value we derive if it is greater than the value already present.  Therefore if we find an edge, we will brighten it.  The end result is a filter which fattens the outline of objects within it.  We again apply a threshold, so that we can control how harsh an edge must be before we enhance it.  Again, I am going to give you the code, and an example of edge enhancement with values of 0 and 127, but because the result is a bit harder to see, I'll also give you the original image next to each for comparison.  Don't worry, your browser cached the starting image, so it won't slow the page down :-)

public static bool EdgeEnhance(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 

    // nPixel and it's eight neighbours. The threshold allows softer 

    // edges to be forced down to black, use 0 to negate it's effect.

    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.

    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                    ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for (int y = 1; y < b.Height-1; ++y)
        {
            p += 3;
            p2 += 3;

            for (int x = 3; x < nWidth-3; ++x)
            {
                nPixelMax = Math.Abs((p2 - stride + 3)[0] - (p2 + stride - 3)[0]);

                nPixel = Math.Abs((p2 + stride + 3)[0] - (p2 - stride - 3)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 - stride)[0] - (p2 + stride)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 + 3)[0] - (p2 - 3)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                if (nPixelMax > nThreshold && nPixelMax > p[0])
                    p[0] = (byte) Math.Max(p[0], nPixelMax);

                ++ p;
                ++ p2;            
            }

            p += nOffset + 3;
            p2 += nOffset + 3;
        }
    }    

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;

My Kids Enhanced with a threshold of 0

My Kids Enhanced with a threshold of 127

I have to say the effect here is somewhat muted - I can't readily see it in the images but it is very apparent when I have the program open and can swap between them.

I hope you've fond this article useful, I have a lot more to say on the subject of image processing, but it's not going to be for a little while, as I have other articles I want to get done and also projects I need to undertake to increase my skillset in areas pertaining to my work.  But, to quote my favourite actor, 'I'll be back'.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here