Click here to Skip to main content
Click here to Skip to main content

Canny Edge Detection in C#

By , 23 Apr 2012
 

Introduction

The purpose of edge detection in general is to significantly reduce the amount of data in an image, while preserving the structural properties to be used for further image processing. Several algorithms exists, and this worksheet focuses on a particular one developed by John F. Canny (JFC) in 1986. Even though it is quite old, it has become one of the standard edge detection methods and it is still used in research.

The aim of JFC was to develop an algorithm that is optimal with regards to the following criteria:

1. Detection: The probability of detecting real edge points should be maximized while the probability of falsely detecting non-edge points should be minimized. This corresponds to maximizing the signal-to-noise ratio.

2. Localization: The detected edges should be as close as possible to the real edges.

3. Number of responses: One real edge should not result in more than one detected edge (one can argue that this is implicitly included in the first requirement).

With Canny’s mathematical formulation of these criteria, Canny’s Edge Detector is optimal for a certain class of edges (known as step edges). A C# implementation of the algorithm is presented here.

Background

The readers are advised to do more research on canny edge detection method for detailed theory.

Using the code

The Canny Edge Detection Algorithm

The algorithm runs in 5 separate steps:

1. Smoothing: Blurring of the image to remove noise.

private void GenerateGaussianKernel(int N, float S ,out int Weight)
        {

            float Sigma = S ;
            float pi;
            pi = (float)Math.PI;
            int i, j;
            int SizeofKernel=N;
            
            float [,] Kernel = new float [N,N];
            GaussianKernel = new int [N,N];
            float[,] OP = new float[N, N];
            float D1,D2;


            D1= 1/(2*pi*Sigma*Sigma);
            D2= 2*Sigma*Sigma;
            
            float min=1000;

            for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
            {
               for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
                {
                    Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = ((1 / D1) * (float)Math.Exp(-(i * i + j * j) / D2));
                    if (Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] < min)
                        min = Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];

                 }
            }
            int mult = (int)(1 / min);
            int sum = 0;
            if ((min > 0) && (min < 1))
            {

                for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
                {
                    for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
                    {
                        Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (float)Math.Round(Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] * mult, 0);
                        GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (int)Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
                        sum = sum + GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
                    }

                }

            }
            else
            {
                sum = 0;
                for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
                {
                    for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
                    {
                        Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (float)Math.Round(Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] , 0);
                        GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (int)Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
                        sum = sum + GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
                    }

                }

            }
          //Normalizing kernel Weight
          Weight= sum;
         
         return;
        }

Following subroutine removes noise by Gaussian Filtering

private int[,] GaussianFilter(int[,] Data)
        {
            GenerateGaussianKernel(KernelSize, Sigma,out KernelWeight);

            int[,] Output = new int[Width, Height];
            int i, j,k,l;
            int Limit = KernelSize /2;

            float Sum=0;


 Output = Data; // Removes Unwanted Data Omission due to kernel bias while convolution

         
            for (i = Limit; i <= ((Width - 1) - Limit); i++)
            {
                for (j = Limit; j <= ((Height - 1) - Limit); j++)
                {
                    Sum = 0;
                    for (k = -Limit; k <= Limit; k++)
                    {

                        for (l = -Limit; l <= Limit; l++)
                        {
                            Sum = Sum + ((float)Data[i + k, j + l] * GaussianKernel [Limit + k, Limit + l]); 
                            
                        }
                    }
                    Output[i, j] = (int)(Math.Round(Sum/ (float)KernelWeight));
                }

            }


            return Output;
        }

2. Finding gradients: The edges should be marked where the gradients of the image haslarge magnitudes.

Sobel X and Y Masks are used to generate X & Y Gradients of Image; next function implements differentiation using sobel Filter Mask

private float[,] Differentiate(int[,] Data, int[,] Filter)
        {
            int i, j,k,l, Fh, Fw;

            Fw = Filter.GetLength(0);
            Fh = Filter.GetLength(1);
            float sum = 0;
            float[,] Output = new float[Width, Height];

            for (i = Fw / 2; i <= (Width - Fw / 2) - 1; i++)
            {
                for (j = Fh / 2; j <= (Height  - Fh / 2) - 1; j++)
                {
                  sum=0;
                   for(k=-Fw/2; k<=Fw/2; k++)
                   {
                       for(l=-Fh/2; l<=Fh/2; l++)
                       {
                          sum=sum + Data[i+k,j+l]*Filter[Fw/2+k,Fh/2+l];


                       }
                   }
                    Output[i,j]=sum;

                }

            }
            return Output;

        }

3. Non-maximum suppression: Only local maxima should be marked as edges.

We find gradient direction and using these direction we perform non maxima suppression (Read “Digital Image Processing- by R Gonzales-Pearson Education)

// Perform Non maximum suppression:
           // NonMax = Gradient;

            for (i = 0; i <= (Width - 1); i++)
            {
                for (j = 0; j <= (Height - 1); j++)
                {
                    NonMax[i, j] = Gradient[i, j];
                }
            }
         
            int Limit = KernelSize / 2;
            int r, c;
            float Tangent;
    

            for (i = Limit; i <= (Width - Limit) - 1; i++)
            {
                for (j = Limit; j <= (Height - Limit) - 1; j++)
                {

                    if (DerivativeX[i, j] == 0)
                        Tangent = 90F;
                    else
                        Tangent = (float)(Math.Atan(DerivativeY[i, j] / DerivativeX[i, j]) * 180 / Math.PI); //rad to degree



                    //Horizontal Edge
                    if (((-22.5 < Tangent) && (Tangent <= 22.5)) || ((157.5 < Tangent) && (Tangent <= -157.5)))
                    {
                        if ((Gradient[i, j] < Gradient[i, j + 1]) || (Gradient[i, j] < Gradient[i, j - 1]))
                            NonMax[i, j] = 0;
                    }


                    //Vertical Edge
                    if (((-112.5 < Tangent) && (Tangent <= -67.5)) || ((67.5 < Tangent) && (Tangent <= 112.5)))
                    {
                        if ((Gradient[i, j] < Gradient[i + 1, j]) || (Gradient[i, j] < Gradient[i - 1, j]))
                            NonMax[i, j] = 0;
                    }

                    //+45 Degree Edge
                    if (((-67.5 < Tangent) && (Tangent <= -22.5)) || ((112.5 < Tangent) && (Tangent <= 157.5)))
                    {
                        if ((Gradient[i, j] < Gradient[i + 1, j - 1]) || (Gradient[i, j] < Gradient[i - 1, j + 1]))
                            NonMax[i, j] = 0;
                    }

                    //-45 Degree Edge
                    if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((67.5 < Tangent) && (Tangent <= 22.5)))
                    {
                        if ((Gradient[i, j] < Gradient[i + 1, j + 1]) || (Gradient[i, j] < Gradient[i - 1, j - 1]))
                            NonMax[i, j] = 0;
                    }

                }
            }

4.Double thresholding: Potential edges are determined by thresholding.

5.Edge tracking by hysteresis: Final edges are determined by suppressing all edges that are not connected to a very certain (strong) edge.

private void HysterisisThresholding(int[,] Edges)
        {

            int i, j;
            int Limit= KernelSize/2;


            for (i = Limit; i <= (Width - 1) - Limit; i++)
                for (j = Limit; j <= (Height - 1) - Limit; j++)
                {
                    if (Edges[i, j] == 1)
                    {
                        EdgeMap[i, j] = 1;

                    }

                }

            for (i = Limit; i <= (Width - 1) - Limit; i++)
            {
                for (j = Limit; j <= (Height  - 1) - Limit; j++)
                {
                    if (Edges[i, j] == 1)
                    {
                        EdgeMap[i, j] = 1;
                        Travers(i, j);
                        VisitedMap[i, j] = 1;
                    }
                }
            }




            return;
        }

//Recursive Procedure 
private void Travers(int X, int Y)
        {
            

            if (VisitedMap[X, Y] == 1)
            {
                return;
            }

            //1
            if (EdgePoints[X + 1, Y] == 2)
            {
                EdgeMap[X + 1, Y] = 1;
                VisitedMap[X + 1, Y] = 1;
                Travers(X + 1, Y);
                return;
            }
            //2
            if (EdgePoints[X + 1, Y - 1] == 2)
            {
                EdgeMap[X + 1, Y - 1] = 1;
                VisitedMap[X + 1, Y - 1] = 1;
                Travers(X + 1, Y - 1);
                return;
            }

           //3

            if (EdgePoints[X, Y - 1] == 2)
            {
                EdgeMap[X , Y - 1] = 1;
                VisitedMap[X , Y - 1] = 1;
                Travers(X , Y - 1);
                return;
            }

           //4

            if (EdgePoints[X - 1, Y - 1] == 2)
            {
                EdgeMap[X - 1, Y - 1] = 1;
                VisitedMap[X - 1, Y - 1] = 1;
                Travers(X - 1, Y - 1);
                return;
            }
            //5
            if (EdgePoints[X - 1, Y] == 2)
            {
                EdgeMap[X - 1, Y ] = 1;
                VisitedMap[X - 1, Y ] = 1;
                Travers(X - 1, Y );
                return;
            }
            //6
            if (EdgePoints[X - 1, Y + 1] == 2)
            {
                EdgeMap[X - 1, Y + 1] = 1;
                VisitedMap[X - 1, Y + 1] = 1;
                Travers(X - 1, Y + 1);
                return;
            }
            //7
            if (EdgePoints[X, Y + 1] == 2)
            {
                EdgeMap[X , Y + 1] = 1;
                VisitedMap[X, Y + 1] = 1;
                Travers(X , Y + 1);
                return;
            }
            //8

            if (EdgePoints[X + 1, Y + 1] == 2)
            {
                EdgeMap[X + 1, Y + 1] = 1;
                VisitedMap[X + 1, Y + 1] = 1;
                Travers(X + 1, Y + 1);
                return;
            }


            //VisitedMap[X, Y] = 1;
            return;
        }
               
        //Canny Class Ends
    }

This is performed by a recursive function which performs double thresholding by two thresholds High Threshold(TH) and Low Threshold (TL) and 8-connectivity analysis

Points of Interest

Please refer to "Digital Image Processing" by Gonzalez & woods, Pearson Education

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Dr. Vinayak Ashok Bharadi
Unknown
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionWrong codememberchipu842 May '13 - 2:41 
Given the definition of D1 (D1= 1/(2*pi*Sigma*Sigma);), the following line in the GenerateGaussianKernel subroutine is wrong:
 
Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = ((1 / D1) * (float)Math.Exp(-(i * i + j * j) / D2));
 
(1/D1) should be replaced with D1
Questiondifficulty in project of image processingmemberMember 950879327 Oct '12 - 20:46 
how to get the edge detection of another image on the same mainform of canny edge detection?
QuestionWhat if I don't want to detect the edges of Cans?memberDewey29 May '12 - 13:15 
Just joking, but do fix the title, your code is in C, not C#.
QuestionPlease fix the topic of this articlememberFrancoishill24 Apr '12 - 20:53 
It should be Canny Edge Detection in C, not in C#.
AnswerRe: Please fix the topic of this articlememberJudah Himango3 Jan '13 - 11:06 
Maybe I'm missing something, but the code shown in the article and in the download, are C#.


My Messianic Jewish blog: Kineti L'Tziyon
My software blog: Debugger.Break()
Judah Himango


Questionabout subpixelmemberxiejiahe2 Apr '12 - 17:49 
do you have any idea to add subpixel accuracy in this code?
Questionthere is a bug in -45 degreememberxiejiahe2 Apr '12 - 17:46 
//-45 Degree Edge
                  if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((67.5 > Tangent) && (Tangent >

GeneralCanny edge detection in image recognition?memberamelia deo31 Mar '12 - 21:14 
currently,I'm interest in Canny Edge detection and I decided to apply Canny edge detection in my project. May I know whether Canny edge detection able use in image recognition? Hope to get your reply soon
GeneralMy vote of 5membermanoj kumar choubey28 Mar '12 - 0:38 
Nice
QuestionSobel mask used differs in implementationmemberADTC234510 Mar '12 - 15:25 
I was checking out the code (in the download) and I found that the Sobel masks used in DetectCannyEdges() are:
int[,] Dx = {{1,0,-1},
             {1,0,-1},
             {1,0,-1}};
 
int[,] Dy = {{1,1,1},
             {0,0,0},
             {-1,-1,-1}};
This differs from the Sobel masks provided in the included PDF file (which is amazing btw, you must write in the article that one can refer to the PDF to learn how Canny works):
      [ -1 0 1 ]        [  1  2  1 ]
KGX = [ -2 0 2 ]  KGY = [  0  0  0 ]
      [ -1 0 1 ]        [ -1 -2 -1 ]
I was wondering if this is by intention or a mistake. Could you explain why the difference if it is by intention?
 
PS: Section 3 (Non-maximum suppression) in your article appears broken. Perhaps the <pre> tag is malformed or something.
AnswerRe: Sobel mask used differs in implementationmemberJohn Ktejik31 Oct '12 - 15:28 
This one I am able to answer. One gives higher priority to horizontal and vertical lines (meaning they will appear slightly darker/thicker), the other treats all lines equally.
QuestionGradient Vector Flowmemberdavidalcon6 Mar '12 - 2:03 
Hi sir,

I'm solve to make an implementations to convert an image to binary image bassed in threshold. Now i need some class or namespace to implements Gradient Vector Flow algoritm in C#. Could you or anybody help me plesae?

Thanks you.
Questioncode canny - edge detectiomemberdavidalcon29 Feb '12 - 6:51 
Hi sir,
 
I'm trying to pass MATLAB code to C# code. In Matlab there are constructed many tools, but in C # does not. Your code is really useful, but in my case I need to convert image to binary image, based on threshold. In matlab is very easy because I use im2bw fuction to do it, but in C# i don´t know. Could you help me plesae?
 
Thanks you.
Questionimage processingmemberjayshri dhar28 Feb '12 - 4:27 
hello sir ,
I am doing image processing with C.. can u please tel me the equivalent C code for canny edge detection in C since i am very weak and a newbie in C.
thank u
Questioncode of cannymemberjayshri dhar1 Feb '12 - 6:06 
sir is it a c code or c++ code...? m doing it in linux and getting some error
AnswerRe: code of cannymemberDr. Vinayak Ashok Bharadi1 Feb '12 - 18:36 
Greetings,
This is a C# code, but with some minor modifications you can run in C,
 
Edit the main Canny Class file, convert the functions in C/C++ equivalent syntax
 
Regards,
Dr. Vinayak Ashok Bharadi
GeneralMy vote of 2memberKarl Sanford14 Jan '12 - 8:04 
This isn't what I would consider an article, this is a code dump. Articles should teach and explain, not be a repository for copy/paste programmers. If this is such a well known topic (as you indicated to others that expressed the same sentiment) then you should have no probelm explaining it Smile | :)
GeneralRe: My vote of 2memberDr. Vinayak Ashok Bharadi1 Feb '12 - 18:40 
I know that it is a code dump. and I sincerely feel sorry for that , but I just wanted to share the code only with the world, canny edge detection is a widely used function in Image Processing, In the literature you can find the theory. Thank you for your appreciation.
Best Regards,
 
Dr. Vinayak Ashok Bharadi
GeneralMy vote of 2memberAlexander M. Batishchev1 Dec '11 - 5:29 
You Travers() is awful.
GeneralRe: My vote of 2memberViktors Garkavijs3 Jul '12 - 16:21 
Yes, the Travers() should be minimized. There has already been a comment about copy-paste code. Why not show a better solution here? Would this one be better?
 
private void Travers(int X, int Y)
{
    if (VisitedMap[X, Y] == 1)
    {
        return;
    }
 
    for (int i = -1; i <= 1; i++)
    {
        for (int j = -1; j <= 1; j++)
        {
            if (i == 0 && j == 0)
            {
                continue;
            }
            else if (EdgePoints[X + i, Y + j] == 2)
            {
                 VisitedMap[X + i, Y + j] = EdgeMap[X + i, Y + j] = 1;
                 Travers(X + i, Y + j);
            }
        }
    }
}

GeneralMy vote of 5memberVHGN25 May '11 - 3:20 
I finnaly understand Sobel Opertor Smile | :) ) Thanks man Smile | :)
GeneralMy vote of 5memberbidhankaran25 Feb '11 - 6:05 
i searched several site but got ur one its ow some
GeneralExellentmemberdeepak_rai28 Jan '11 - 6:07 
My vote is 5
GeneralUnreachable codememberMartin Setnička23 Dec '10 - 11:57 
In Canny.cs on line 445:
if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((67.5 < Tangent) && (Tangent <= 22.5)))



Thank you very much for this article.
GeneralRe: Unreachable codememberVinayak Bharadi23 Dec '10 - 21:57 
Thank you for your suggestion. I will work on this.
Thanks,
Vinayak
GeneralMy vote of 5memberAlbinAbel22 Dec '10 - 21:13 
I like these kind of scientific development
GeneralMy vote of 3memberMember 243269620 Jul '10 - 3:36 
First: there's a lot of "divide by 2" in the code, but the fraction doesn't change inside the loop. Calculate the average once outside the loop and then use it.
Second, float might be a bad choice because double fits without promotion-demotion into the math registers.
 
I haven't tried it, but my guess is these simple changes would improve performance.
GeneralGaussian filter is separablememberPhil Atkin20 Jul '10 - 2:58 
The 2D Gaussian kernel [Math.Exp(-(i * i + j * j) / D2)] is separable into a horizontal pass and a vertical one (because the expression above can be written as Math.Exp(-(i*i)/D3) * Math.Exp(-(j*j)/D3) where D3 is sigma * root two).
This means that the 2D convolution can be performed by a horizontal filtering pass followed by a vertical filtering pass, which means that it becomes an O(2N) operation instead of O(N^2). This is an important speedup when N is more than (say)10.
Phil Atkin
Synoptics Ltd., Cambridge, UK

GeneralMy vote of 2mvpJosh Fischer14 Jul '10 - 4:31 
In response to the other posters, you say "The canny edge detection algorithm is very well known...". If it is well known, then there is no need to post it here on CP. If your goal was to simply make an open source implementation of the technique, then please post it on a site that specializes in code management (codeplex, github, etc).
The point of writing an article is to highlight things that might not be obvious in the code. Most of us don’t have time to go through code files line by line looking for comments; especially not when the code is just raw mathematics.
GeneralRe: My vote of 2memberInnowaze15 Jul '10 - 4:53 
Clearly, this article is informative and is of use to anyone interested in edge detection.Thumbs Up | :thumbsup: I don't see a reason why this article should not be on CP.
 
Smile | :)
 
~ Ram
GeneralRe: My vote of 2mvpJosh Fischer15 Jul '10 - 5:25 
The code is informative, not the article. I think you are missing the point I, and others, are trying to make. Any of us could post some code and say "Here is code that does Foo following the Bar algorythm", but that doesn't mean it qualifies as an article. As you've seen from the other posts, I'm not alone on this opinion. You need to explain the details better because 99.99% of us don't know anything about edge detection.
Josh Fischer

GeneralRe: My vote of 2memberRutvik Dave15 Jul '10 - 5:27 
I agree with you on not explaning the article... he should explain the Code/Algorithm...
 

But I dont agree on everything else... why you want to discorage new member, who is trying to start contributing on something ?
 

(Atleast he is not like me sucking up everything, not sharing anything... Smile | :) )

GeneralRe: My vote of 2mvpJosh Fischer15 Jul '10 - 7:48 
I'm not trying to discourage him, in fact I'm trying to do the opposite. The value of CP, IMHO, is that people take the time to explain their topic. Sometimes they are very simple things for newbies, other times they are complex topics which only a small percentage of people care about (this article may qualify as the latter). Either way, you should be able to read the article without having to look at code and/or comments to get a good idea of what's going on.
It may seem counterintuitive, but the fact that I'm commenting on this article shows I am being encouraging. I want more explanation because it looks very interesting and I want to understand the code/article better.
So Vinayak Bharadi, good start, but please look at some of the other popular aritlces on CP to get a feel for what most people are expecting.
Josh Fischer

GeneralRe: My vote of 2memberHaBiX31 Jan '12 - 21:55 
I didnt know about canny edge algorithm.. and if you actually read the article, you get an idea on how it works.
GeneralRe: My vote of 2memberVinayak Bharadi31 Jan '12 - 22:09 
Hello,
Greetings,
Canny Edge detection is a well known algorithm for edge detection. I wrote this article to present the code, as canny edge detection is commonly required for image processing and C# does not has native library for it. The detail about canny algo can be found on the web and numerous details are available. Thank you all for reading the article, any changes suggested can be considered.
Regards,
Dr. Vinayak Ashok Bharadi
GeneralSource code link is brokenmemberTony Bermudez13 Jul '10 - 15:07 
The source code link is broken.
GeneralRe: Source code link is brokenmemberVinayak Bharadi13 Jul '10 - 18:38 
Dear Sir,
The Source code link is updated. It is working and i have tested it.
I am sorry for the error.
Regards,
Vinayak Bharadi
GeneralMy vote of 2memberjohannesnestler13 Jul '10 - 5:28 
interesting topic but just a code dump. Please update the article and explain the algorithm and your thoughts during the implementation.
GeneralRe: My vote of 2memberVinayak Bharadi13 Jul '10 - 18:42 
Dear Sir,
The source code contains the details of canny edge detection algorithm. The canny edge detcetion algorithm is very well known image processing technique and it is lengthy to explain. The C# version was not available. I have tried to implement the best I could, so that this can help others. The technical details are available in the notes provided in source code folder please refer it.
The Source code link is updated. It is working and i have tested it.
I am sorry for the error.
Regards,
Vinayak Bharadi
GeneralMy vote of 1memberdigital man13 Jul '10 - 1:36 
Code dump: you need to explain the code and concepts. Read the article guidelines and see what other authors are doing.
GeneralRe: My vote of 1memberVinayak Bharadi13 Jul '10 - 18:43 
Dear Sir,
The source code contains the details of canny edge detection algorithm. The canny edge detcetion algorithm is very well known image processing technique and it is lengthy to explain. The C# version was not available. I have tried to implement the best I could, so that this can help others. The technical details are available in the notes provided in source code folder please refer it.
The Source code link is updated. It is working and i have tested it.
I am sorry for the error.
Regards,
Vinayak Bharadi
GeneralRe: My vote of 1membermark merrens24 Apr '12 - 5:31 
It doesn't matter how clever your code is, simply dumping it on the page does not make it an article. You need less code and more explanation and down-voting me (you or your friends) will not change my opinion. This is not an article and, as such, deserved the one vote because, clearly, you had not bothered to see what a successful article looks like and emulate it and source code notes are not enough.
"If you think it's expensive to hire a professional to do the job, wait until you hire an amateur." Red Adair.
nils illegitimus carborundum
 
me, me, me

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 24 Apr 2012
Article Copyright 2010 by Dr. Vinayak Ashok Bharadi
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid