Posts Tagged 'Feature extraction'



C# How to: Stained Glass Image Filter

Article Purpose

This article serves to provides a detailed discussion and implementation of a Stained Glass Image Filter. Primary topics explored include: Creating , Pixel Coordinate distance calculations implementing , and methods. In addition, this article explores Gradient Based implementing thresholds.

Zurich: Block Size 15, Factor 4, Euclidean

Zurich Block Size 15 Factor 4 Euclidean

Sample Source Code

This article is accompanied by a sample source code Visual Studio project which is available for download .

Using the Sample Application

This article’s accompanying sample source code includes a based sample application. The sample application provides an implementation of the concepts explored by this article. Concepts discussed can be easily replicated and tested by using the sample application.

Source/input files can be specified from the local system when clicking the Load Image button. Additionally users also have the option to save resulting filtered by clicking the Save Image button.

The sample application through its user interface allows a user to specify several filter configuration options. Two main categories of configuration options have been defined as Block Properties and Edge Properties.

Block Properties relate to  the process of rendering  . The following configuration options have been implemented:

  • Block Size – During the process of rendering a regions or blocks of equal shape and size have to be defined. These uniform regions/blocks form the basis of rendering uniquely shaped regions later on. The Block Size option determines the width and height of an individual region/block. Larger values result in larger non-uniform regions being rendered. Smaller values in return result in smaller non-uniform regions being rendered.
  • Distance Factor – The Distance Factor option determines the extent to which a pixel’s containing region will be calculated. Possible values range from 1 to 4 inclusive. A Distance Factor value of 4 equates to precise calculation of a pixel’s containing region, whereas a value of 1 results in containing regions often registering pixels that should be part of a neighbouring region. Values closer to 4 result in more varied region shapes. Values closer to 1 result in regions being rendered having more of a uniform shape/pattern.
  • Distance Formula – The distance between a pixel’s coordinates and a region’s outline determines whether that pixel should be considered part of a region. The sample application implements three different methods of calculating pixel distance: , and methods. Each result in region shapes being rendered differently.

Salzburg: Block Size 20, Factor 1, Chebyshev, Edge Threshold 2 

Saltzburg Block Size 20 Factor 1 Chebyshev Edge Threshold 2

Edge Properties relate to the implementation of Image Gradient Based Edge Detection. is an optional filter and can be enabled/disabled through the user interface, The implementation of serves to highlight/outline regions rendered as part of a . The configuration options implemented are:

  • Highlight Edges – Boolean value indicating whether or not should be applied
  • Threshold – In calculating a threshold value determines if a pixel forms part of an edge. Higher threshold values result in less being expressed. Lower threshold values result in more being expressed.
  • Colour – If a pixel has been determined as forming part of an , the resulting pixel colour will be determined by the colour value specified by the user.

The following image is a screenshot of the Stained Glass Image Filter sample application in action:

Stained Glass Image Filter Sample Application 

Locarno: Block Size 10, Factor 4, Euclidean

Locarno Block Size 10 Factor 4 Euclidean

Stained Glass

The Stained Glass Image Filter detailed in this article operates on the basis of implementing modifications upon a specified sample/input , producing resulting which resemble the appearance of stained glass artwork.

A common variant of stained glass artwork comes in the form of several individual pieces of coloured glass being combined in order to create an . The sample source code employs a similar  method of combining what appears to be non-uniform puzzle pieces. The following list provides a broad overview of the steps involved in applying a Stained Glass Image Filter:

  1. Render a Voronoi Diagram – Through rendering a the resulting will be divided into a number of regions. Each region being intended to represent an individual glass puzzle piece. The following section of this article provides a detailed discussion on rendering .
  2. Assign each Pixel to a Voronoi Diagram Region – Each pixel forming part of the source/input should be iterated. Whilst iterating pixels determine the region to which a pixel should be associated. A pixel should be associated to the region whose border has been determined the nearest to the pixel. In a following section of this article a detailed discussion regarding Pixel Coordinate Distance Calculations can be found.
  3. Determine each Region’s Colour Mean – Each region will only express a single colour value. A region’s colour equates to the average colour as expressed by all the pixels forming part of a region. Once the average colour value of a region has been determined every pixel forming part of that region should be set to the average colour.
  4. Implement Edge Detection – If the user configuration option indicates that should be implemented, apply Gradient Based Edge Detection. This method of has been discussed in detailed in a following section of this article.

Bad Ragaz: Block Size 10, Factor 1, Manhattan 

Bad Ragaz Block Size 10 Factor 1 Manhattan

Voronoi Diagrams

represent a fairly uncomplicated concept. In contrast, the implementation of prove somewhat more of a challenge. From we gain the following :

In mathematics, a Voronoi diagram is a way of dividing space into a number of regions. A set of points (called seeds, sites, or generators) is specified beforehand and for each seed there will be a corresponding region consisting of all points closer to that seed than to any other. The regions are called Voronoi cells. It is dual to the Delaunay triangulation.

In this article are generated resulting in regions expressing random shapes. Although region shapes are randomly generated, the parameters or ranges within which random values are selected are fixed/constant. The steps required in generating a can be detailed as follows:

  1. Define fixed size square regions – By making use of the user specified Block/Region Size value, group pixels together into square regions.
  2. Determine a Seed Value for Random number generation – Determine the sum total of pixel colour components of all the pixels forming part of a square region. The colour sum total value should be used as a seed value when generating random numbers in the next step.
  3. Determine a Random XY coordinate within each square region – Generate two random numbers, specifying each region’s coordinate boundaries as minimum and maximum boundaries in generating random numbers. Keep record of every new randomly generated XY-Coordinate value.
  4. Associate Pixels and Regions – A pixel should be associated to the Random Coordinate point nearest to that pixel. Determine the Random Coordinate nearest to each pixel in the source/input image. The method implemented in calculating coordinate distance depends on the configuration value specified by the user.
  5. Set Region Colours – Each pixel forming part of the same region should be set to the same colour. The colour assigned to a region’s pixels will be determined by the average colour value of the region’s pixels.

The following image illustrates an example consisting of 10 regions:

2Ddim-L2norm-10site

Port Edward: Block Size 10, Factor 1, Chebyshev, Edge Threshold 2

Port Edward Block Size 10 Factor 1 Chebyshev Edge Threshold 2

Calculating Pixel Coordinate Distances

The sample source code provides three different coordinate distance calculation methods. The supported methods are: , and . A pixel’s nearest randomly generated coordinate depends on the distance between that pixel and the random coordinate. Each method of calculating distance in most instances would be likely to produce different output values, which in turn influences the region to which a pixel will be associated.

The most common method of distance calculation, , has been described by as follows:

In mathematics, the Euclidean distance or Euclidean metric is the "ordinary" distance between two points that one would measure with a ruler, and is given by the Pythagorean formula. By using this formula as distance, Euclidean space (or even any inner product space) becomes a metric space. The associated norm is called the Euclidean norm. Older literature refers to the metric as Pythagorean metric.

When calculating the algorithm implemented can be expressed as follows:

Euclidean Distance Algorithm

Zurich: Block Size 10, Factor 1, Euclidean

Zurich Block Size 10 Factor 1 Euclidean

As an alternative to calculating , the sample source code also implements calculation. Often calculation will be referred to as , or . From we gain the following :

Taxicab geometry, considered by Hermann Minkowski in the 19th century, is a form of geometry in which the usual distance function or metric of Euclidean geometry is replaced by a new metric in which the distance between two points is the sum of the absolute differences of their coordinates. The taxicab metric is also known as rectilinear distance, L1 distance or \ell_1 norm (see Lp space), city block distance, Manhattan distance, or Manhattan length, with corresponding variations in the name of the geometry.[1] The latter names allude to the grid layout of most streets on the island of Manhattan, which causes the shortest path a car could take between two intersections in the borough to have length equal to the intersections’ distance in taxicab geometry

When calculating the algorithm implemented can be expressed as follows:

Manhattan Distance Algorithm

Port Edward: Block Size 10, Factor 4, Euclidean

Port Edward Block Size 10 Factor 4 Euclidean

, a distance algorithm resembling the way in which a King Chess piece may move on a chess board. The following we gain from :

In mathematics, Chebyshev distance (or Tchebychev distance), Maximum metric, or L∞ metric[1] is a metric defined on a vector space where the distance between two vectors is the greatest of their differences along any coordinate dimension.[2] It is named after Pafnuty Chebyshev.

It is also known as chessboard distance, since in the game of chess the minimum number of moves needed by a king to go from one square on a chessboard to another equals the Chebyshev distance between the centers of the squares, if the squares have side length one, as represented in 2-D spatial coordinates with axes aligned to the edges of the board.[3] For example, the Chebyshev distance between f6 and e2 equals 4.

When calculating the algorithm implemented can be expressed as follows:

Chebyshev Distance Algorithm

Salzburg: Block Size 20, Factor 1, Chebyshev

Salzburg Block Size 20 Factor 1 Chebyshev

Gradient Based Edge Detection

Various methods of can easily be implemented in C#. Each method of provides a set of benefits, usually weighed against a set of trade-offs. In this article and the accompanying sample source code the Gradient Based Edge Detection method has been implement.

Take into regard that every region within the rendered will only express a single colour, although most regions differ in the single colour they express. Once all pixels have been associated to a region and all pixel colour values have been updated the resulting defines mostly clearly distinguishable  colour gradients. A method of performs efficiently at detecting . The edges detected are defined between different regions.

An can be considered as a difference in colour intensity relating to a specific direction. Only once all tasks related to applying the Stained Glass Filter have been completed should the Gradient Based Edge Detection be applied. The steps involved in applying Gradient Based Edge Detection can be described as follows:

  1. Iterate each pixel – Each pixel forming part of a source/input image should be iterated.
  2. Determine Horizontal and Vertical Gradients – Calculate the colour value difference between the currently iterated pixel’s left and right neighbour pixel as well as the top and bottom neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  3. Determine Horizontal Gradient – Calculate the colour value difference between the currently iterated pixel’s left and right neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  4. Determine Vertical Gradient – Calculate the colour value difference between the currently iterated pixel’s top and bottom neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  5. Determine Diagonal Gradients – Calculate the colour value difference between the currently iterated pixel’s North-Western and South-Eastern neighbour pixel as well as the North-Eastern and South-Western neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  6. Determine NW-SE Gradient – Calculate the colour value difference between the currently iterated pixel’s North-Western and South-Eastern neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  7. Determine NE-SW Gradient  – Calculate the colour value difference between the currently iterated pixel’s North-Eastern and South-Western neighbour pixel.
  8. Determine and set result pixel value – If any of the six gradients calculated exceeded the specified threshold value set the related pixel in the resulting image to the Edge Colour specified by the user, if not, set the related pixel equal to the source pixel colour value.

Zurich: Block Size 10, Factor 4, Chebyshev

Zurich Block Size 10 Factor 4 Chebyshev 

Implementing a Stained Glass Image Filter

The sample source code defines two helper classes, both implemented when applying the Stained Glass Image Filter. The Pixel class represents a single pixel in terms of an XY-Coordinate and Red, Green and Blue values. The definition as follows:

public class Pixel  
{
    private int xOffset = 0; 
    public int XOffset 
    {
        get { return xOffset; } set { xOffset = value; } 
    }

private int yOffset = 0; public int YOffset { get { return yOffset; } set { yOffset = value; } }
private byte blue = 0; public byte Blue { get { return blue; } set { blue = value; } }
private byte green = 0; public byte Green { get { return green; } set { green = value; } }
private byte red = 0; public byte Red { get { return red; } set { red = value; } } }

Zurich: Block Size 10, Factor 1, Chebyshev, Edge Threshold 1

Zurich Block Size 10 Factor 1 Chebyshev Edge Threshold 1

The VoronoiPoint class serves as method of recording randomly generated coordinates and referencing a region’s associated pixels. The definition as follows:

public class VoronoiPoint 
{
    private int xOffset = 0; 
    public int XOffset 
    {
        get  { return xOffset; } set { xOffset = value; }
    }

private int yOffset = 0; public int YOffset { get { return yOffset; } set { yOffset = value; } }
private int blueTotal = 0; public int BlueTotal { get { return blueTotal; } set { blueTotal = value; } }
private int greenTotal = 0; public int GreenTotal { get {return greenTotal; } set { greenTotal = value; } }
private int redTotal = 0; public int RedTotal { get { return redTotal; } set { redTotal = value; } }
public void CalculateAverages() { if (pixelCollection.Count > 0) { blueAverage = blueTotal / pixelCollection.Count; greenAverage = greenTotal / pixelCollection.Count; redAverage = redTotal / pixelCollection.Count; } }
private int blueAverage = 0; public int BlueAverage { get { return blueAverage; } }
private int greenAverage = 0; public int GreenAverage { get { return greenAverage; } }
private int redAverage = 0; public int RedAverage { get { return redAverage; } }
private List<Pixel> pixelCollection = new List<Pixel>(); public List<Pixel> PixelCollection { get { return pixelCollection; } }
public void AddPixel(Pixel pixel) { blueTotal += pixel.Blue; greenTotal += pixel.Green; redTotal += pixel.Red;
pixelCollection.Add(pixel); } }

Zurich: Block Size 20, Factor 1, Euclidean, Edge Threshold 1

Zurich Block Size 20 Factor 1 Euclidean Edge Threshold 1

From the perspective of a filter implementation code base the only requirement comes in the form of having to invoke the StainedGlassColorFilter , no additional work is required from external code consumers. The StainedGlassColorFilter method has been defined as an targeting the class. The StainedGlassColorFilter method definition as follows:

public static Bitmap StainedGlassColorFilter(this Bitmap sourceBitmap,  
                                             int blockSize, double blockFactor, 
                                             DistanceFormulaType distanceType, 
                                             bool highlightEdges,  
                                             byte edgeThreshold, Color edgeColor) 
{
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle(0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height];
byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length);
sourceBitmap.UnlockBits(sourceData);
int neighbourHoodTotal = 0; int sourceOffset = 0; int resultOffset = 0; int currentPixelDistance = 0; int nearestPixelDistance = 0; int nearesttPointIndex = 0;
Random randomizer = new Random();
List<VoronoiPoint> randomPointList = new List<VoronoiPoint>();
for (int row = 0; row < sourceBitmap.Height - blockSize; row += blockSize) { for (int col = 0; col < sourceBitmap.Width - blockSize; col += blockSize) { sourceOffset = row * sourceData.Stride + col * 4;
neighbourHoodTotal = 0;
for (int y = 0; y < blockSize; y++) { for (int x = 0; x < blockSize; x++) { resultOffset = sourceOffset + y * sourceData.Stride + x * 4; neighbourHoodTotal += pixelBuffer[resultOffset]; neighbourHoodTotal += pixelBuffer[resultOffset + 1]; neighbourHoodTotal += pixelBuffer[resultOffset + 2]; } }
randomizer = new Random(neighbourHoodTotal);
VoronoiPoint randomPoint = new VoronoiPoint(); randomPoint.XOffset = randomizer.Next(0, blockSize) + col; randomPoint.YOffset = randomizer.Next(0, blockSize) + row;
randomPointList.Add(randomPoint); } }
int rowOffset = 0; int colOffset = 0;
for (int bufferOffset = 0; bufferOffset < pixelBuffer.Length - 4; bufferOffset += 4) { rowOffset = bufferOffset / sourceData.Stride; colOffset = (bufferOffset % sourceData.Stride) / 4;
currentPixelDistance = 0; nearestPixelDistance = blockSize * 4; nearesttPointIndex = 0;
List<VoronoiPoint> pointSubset = new List<VoronoiPoint>();
pointSubset.AddRange(from t in randomPointList where rowOffset >= t.YOffset - blockSize * 2 && rowOffset <= t.YOffset + blockSize * 2 select t);
for (int k = 0; k < pointSubset.Count; k++) { if (distanceType == DistanceFormulaType.Euclidean) { currentPixelDistance = CalculateDistanceEuclidean(pointSubset[k].XOffset, colOffset, pointSubset[k].YOffset, rowOffset); } else if (distanceType == DistanceFormulaType.Manhattan) { currentPixelDistance = CalculateDistanceManhattan(pointSubset[k].XOffset, colOffset, pointSubset[k].YOffset, rowOffset); } else if (distanceType == DistanceFormulaType.Chebyshev) { currentPixelDistance = CalculateDistanceChebyshev(pointSubset[k].XOffset, colOffset, pointSubset[k].YOffset, rowOffset); } if (currentPixelDistance <= nearestPixelDistance) { nearestPixelDistance = currentPixelDistance; nearesttPointIndex = k; if (nearestPixelDistance <= blockSize / blockFactor) { break; } } }
Pixel tmpPixel = new Pixel (); tmpPixel.XOffset = colOffset; tmpPixel.YOffset = rowOffset; tmpPixel.Blue = pixelBuffer[bufferOffset]; tmpPixel.Green = pixelBuffer[bufferOffset + 1]; tmpPixel.Red = pixelBuffer[bufferOffset + 2];
pointSubset[nearesttPointIndex].AddPixel(tmpPixel); }
for (int k = 0; k < randomPointList.Count; k++) { randomPointList[k].CalculateAverages();
for (int i = 0; i < randomPointList[k].PixelCollection.Count; i++) { resultOffset = randomPointList[k].PixelCollection[i].YOffset * sourceData.Stride + randomPointList[k].PixelCollection[i].XOffset * 4;
resultBuffer[resultOffset] = (byte)randomPointList[k].BlueAverage; resultBuffer[resultOffset + 1] = (byte)randomPointList[k].GreenAverage; resultBuffer[resultOffset + 2] = (byte)randomPointList[k].RedAverage;
resultBuffer[resultOffset + 3] = 255; } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length);
resultBitmap.UnlockBits(resultData);
if (highlightEdges == true ) { resultBitmap = resultBitmap.GradientBasedEdgeDetectionFilter(edgeColor, edgeThreshold); }
return resultBitmap; }

Locarno: Block Size 10, Factor 4, Euclidean, Edge Threshold 1

Locarno Block Size 10 Factor 4 Euclidean Edge Threshold 1

Implementing Pixel Coordinate Distance Calculations

As mentioned earlier, this article and the accompanying sample source code support coordinate distance calculations through three different calculation methods, namely , and . The method of distance calculation implemented depends on the configuration option specified by the user.

The CalculateDistanceEuclidean method calculates distance implementing the Calculation method. In order to aid faster execution this method will calculate the square root of a specific value only once. Once a square root has been calculated the result is kept in memory. The following code snippet lists the definition of the CalculateDistanceEuclidean method:

private static Dictionary <int,int> squareRoots = new Dictionary<int,int>(); 

private static int CalculateDistanceEuclidean(int x1, int x2, int y1, int y2) { int square = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
if(squareRoots.ContainsKey(square) == false) { squareRoots.Add(square, (int)Math.Sqrt(square)); }
return squareRoots[square]; }

The two other methods of calculating distance are implemented through the CalculateDistanceManhattan and CalculateDistanceChebyshev methods. The definition as follows:

private static int CalculateDistanceManhattan(int x1, int x2, int y1, int y2) 
{
    return Math.Abs(x1 - x2) + Math.Abs(y1 - y2); 
}

private static int CalculateDistanceChebyshev(int x1, int x2, int y1, int y2) { return Math.Max(Math.Abs(x1 - x2), Math.Abs(y1 - y2)); }

Bad Ragaz: Block Size 12, Factor 1, Chebyshev

Bad Ragaz Block Size 12 Factor 1 Chebyshev

Implementing Gradient Based Edge Detection

Did you notice the very last step performed by the StainedGlassColorFilter method involves implementing Gradient Based Edge Detection, depending on whether had been specified by the user.

The following code snippet provides the implementation of the GradientBasedEdgeDetectionFilter extension method:

public static Bitmap GradientBasedEdgeDetectionFilter( 
                this Bitmap sourceBitmap, 
                Color edgeColour, 
                byte threshold = 0) 
{
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle (0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height]; byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length); Marshal.Copy(sourceData.Scan0, resultBuffer, 0, resultBuffer.Length);
sourceBitmap.UnlockBits(sourceData);
int sourceOffset = 0, gradientValue = 0; bool exceedsThreshold = false;
for(int offsetY = 1; offsetY < sourceBitmap.Height - 1; offsetY++) { for(int offsetX = 1; offsetX < sourceBitmap.Width - 1; offsetX++) { sourceOffset = offsetY * sourceData.Stride + offsetX * 4; gradientValue = 0; exceedsThreshold = true;
// Horizontal Gradient CheckThreshold(pixelBuffer, sourceOffset - 4, sourceOffset + 4, ref gradientValue, threshold, 2); // Vertical Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride, sourceOffset + sourceData.Stride, ref gradientValue, threshold, 2);
if (exceedsThreshold == false) { gradientValue = 0;
// Horizontal Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - 4, sourceOffset + 4, ref gradientValue, threshold);
if (exceedsThreshold == false) { gradientValue = 0;
// Vertical Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride, sourceOffset + sourceData.Stride, ref gradientValue, threshold);
if (exceedsThreshold == false) { gradientValue = 0;
// Diagonal Gradient : NW-SE CheckThreshold(pixelBuffer, sourceOffset - 4 - sourceData.Stride, sourceOffset + 4 + sourceData.Stride, ref gradientValue, threshold, 2); // Diagonal Gradient : NE-SW exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride + 4, sourceOffset - 4 + sourceData.Stride, ref gradientValue, threshold, 2);
if (exceedsThreshold == false) { gradientValue = 0;
// Diagonal Gradient : NW-SE exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - 4 - sourceData.Stride, sourceOffset + 4 + sourceData.Stride, ref gradientValue, threshold);
if (exceedsThreshold == false) { gradientValue = 0;
// Diagonal Gradient : NE-SW exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride + 4, sourceOffset + sourceData.Stride - 4, ref gradientValue, threshold); } } } } }
if (exceedsThreshold == true) { resultBuffer[sourceOffset] = edgeColour.B; resultBuffer[sourceOffset + 1] = edgeColour.G; resultBuffer[sourceOffset + 2] = edgeColour.R; }
resultBuffer[sourceOffset + 3] = 255; } }
Bitmap resultBitmap = new Bitmap (sourceBitmap.Width, sourceBitmap.Height); BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode .WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length); resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Zurich: Block Size 15, Factor 1, Manhattan, Edge Threshold 1

Zurich Block Size 15 Factor 1 Manhattan Edge Threshold 1

Sample Images

This article features a rendered graphic illustrating an example which has been released into the public domain by its author, Augochy at the wikipedia project. This applies worldwide. The original can be downloaded from .

All of the photos that appear in this article were taken by myself. Photos listed under Zurich, Locarno and Bad Ragaz were shot in Switzerland. The photo listed as Salzburg had been shot in Austria and the photo listed under Port Edward had been shot in South Africa. In order to fully realize the extent to which had been modified the following section details the original photos.

Zurich, Switzerland

Zurich, Switzerland

Salzburg, Austria

Salzburg, Austria

Locarno, Switzerland

Locarno, Switzerland

Bad Ragaz, Switzerland

Bad Ragaz, Switzerland

Port Edward, South Africa

Port Edward, South Africa

Zurich, Switzerland

Zurich, Switzerland

Zurich, Switzerland

Zurich, Switzerland

Zurich, Switzerland

Zurich, Switzerland

Bad Ragaz, Switzerland

Bad Ragaz, Switzerland

Related Articles and Feedback

Feedback and questions are always encouraged. If you know of an alternative implementation or have ideas on a more efficient implementation please share in the comments section.

I’ve published a number of articles related to imaging and images of which you can find URL links here:

Advertisement

C# How to: Oil Painting and Cartoon Filter

Article Purpose

This article illustrates and provides a discussion and implementation of Oil Painting Filters and related Image Cartoon Filters.

Sunflower: Oil Painting, Filter 5, Levels 30, Cartoon Threshold 30

Sunflower Oil Painting Filter 5 Levels 30 Cartoon Threshold 30

Sample Source Code

This article is accompanied by a sample source code Visual Studio project which is available for download .

Using the Sample Application

A sample application accompanies this article. The sample application creates a visual implementation of the concepts discussed throughout this article. Source/input can be selected from the local system and if desired filter result images can be saved to the local file system.

The two main types of functionality exposed by the sample application can be described as Image Oil Painting Filters and Image Cartoon Filters. The user interface provides the following user input options:

  • Filter Size – The number of neighbouring pixels used in calculating each individual pixel value in regards to an Oil Painting Filter. Higher Filter sizes relate to a more intense Oil Painting Filter being applied. Lower Filter sizes relate to less intense Oil Painting Filters being applied.
  • Intensity Levels – Represents the number of Intensity Levels implemented when applying an Oil Painting Filter. Higher values result in a broader range of colour intensities forming part of the result . Lower values will reduce the range of colour intensities forming part of the result .
  • Cartoon Filter – A Boolean value indicating whether or not in addition to an Oil Painting Filter if a Cartoon Filter should also be applied.
  • Threshold – Only applicable when applying a Cartoon Filter. This option represents the threshold value implemented in determining whether a pixel forms part of an . Lower Values result in more being highlighted. Higher values result in less being highlighted.

The following image is screenshot of the Oil Painting Cartoon Filter sample application in action:

OilPaintingCartoonFilter_SampleApplication

Rose: Oil Painting, Filter 15, Levels 10

Rose Oil Painting Filter 15 Levels 10

Image Oil Painting Filter

The Image Oil Painting Filter consists of two main components: colour gradients and pixel colour intensities. As implied by the title when implementing this resulting are similar in appearance to of Oil Paintings. Result express a lesser degree of detail when compared to source/input . This filter also tends to output which appear to have smaller colour ranges.

Four steps are required when implementing an Oil Painting Filter, indicated as follows:

  1. Iterate each pixel – Every pixel forming part of the source/input should be iterated. When iterating a pixel determine the neighbouring pixel values based on the specified filter size/filter range.
  2. Calculate Colour Intensity -  Determine the Colour Intensity of each pixel being iterated and that of the neighbouring pixels. The neighbouring pixels included should extend to a range determined by the Filter Size specified. The calculated value should be reduced in order to match a value ranging from zero to the number of Intensity Levels specified.
  3. Determine maximum neighbourhood colour intensity – When calculating the colour intensities of a pixel neighbourhood determine the maximum intensity value. In addition, record the occurrence of each intensity level and sum each of the Red, Green and Blue pixel colour component values equating to the same intensity level.
  4. Assign the result pixel – The value assigned to the corresponding pixel in the resulting equates to the pixel colour sum total, where those pixels expressed the same intensity level. The sum total should be averaged by dividing the colour sum total by the intensity level occurrence.

Roses: Oil Painting, Filter 11, Levels 60, Cartoon Threshold 80

Roses Oil Painting Filter 11 Levels 60 Cartoon Threshold 80

When calculating colour intensity reduced to fit the number of levels specified  the algorithm implemented can be expressed as follows:

Colour Intensity Level Algorithm

In the algorithm listed above the variables implemented can be explained as follows:

  • I – Intensity: The calculated intensity value.
  • R – Red: The value of a pixel’s Red colour component.
  • G – Green: The value of a pixel’s Green colour component.
  • B – Blue: The value of a pixel’s Blue colour component.
  • l – Number of intensity levels: The maximum number of intensity levels specified.

Rose: Oil Painting, Filter 15, Levels 30

Rose Oil Painting Filter 15 Levels 30

Cartoon Filter implementing Edge Detection

A Cartoon Filter effect can be achieved by combining an Image Oil Painting filter and an Edge Detection Filter. The Oil Painting filter has the effect of creating more gradual colour gradients, in other words reducing edge intensity.

The steps required in implementing a Cartoon filter can be listed as follows:

  1. Apply Oil Painting filter – Applying an Oil Painting Filter creates the perception of result having been painted by hand.
  2. Implement Edge Detection – Using the original source/input create a new binary detailing .
  3. Overlay edges on Oil Painting image – Iterate each pixel forming part of the edge detected . If the pixel being iterated forms part of an edge, the related pixel in the Oil Painting filtered should be set to black. Because the edge detected was created as a binary , a pixel forms part of an edge should that pixel equate to white.

Daisy: Oil Painting, Filter 7, Levels 30, Cartoon Threshold 40 

Daisy Oil Painting Filter 7 Levels 30 Cartoon Threshold 40

In the sample source code has been implemented through Gradient Based Edge Detection. This method of compares the difference in colour gradients between a pixel’s neighbouring pixels. A pixel forms part of an edge if the difference in neighbouring pixel colour values exceeds a specified threshold value. The steps involved in Gradient Based Edge Detection as follows:

  1. Iterate each pixel – Each pixel forming part of a source/input should be iterated.
  2. Determine Horizontal and Vertical Gradients – Calculate the colour value difference between the currently iterated pixel’s left and right neighbour pixel as well as the top and bottom neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  3. Determine Horizontal Gradient – Calculate the colour value difference between the currently iterated pixel’s left and right neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  4. Determine Vertical Gradient – Calculate the colour value difference between the currently iterated pixel’s top and bottom neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  5. Determine Diagonal Gradients – Calculate the colour value difference between the currently iterated pixel’s North-Western and South-Eastern neighbour pixel as well as the North-Eastern and South-Western neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  6. Determine NW-SE Gradient – Calculate the colour value difference between the currently iterated pixel’s North-Western and South-Eastern neighbour pixel. If the gradient exceeds the specified threshold continue to step 8.
  7. Determine NE-SW Gradient  – Calculate the colour value difference between the currently iterated pixel’s North-Eastern and South-Western neighbour pixel.
  8. Determine and set result pixel value – If any of the six gradients calculated exceeded the specified threshold value set the related pixel in the resulting image to white, if not, set the related pixel to black.

Rose: Oil Painting, Filter 9, Levels 30

Rose Oil Painting Filter 9 Levels 30

Implementing an Oil Painting Filter

The sample source code defines the OilPaintFilter method, an targeting the class. method determines the maximum colour intensity from a pixel’s neighbouring pixels. The definition detailed as follows:

public static Bitmap OilPaintFilter(this Bitmap sourceBitmap, 
                                       int levels, 
                                       int filterSize) 
{
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle(0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height];
byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length);
sourceBitmap.UnlockBits(sourceData);
int[] intensityBin = new int [levels]; int[] blueBin = new int [levels]; int[] greenBin = new int [levels]; int[] redBin = new int [levels];
levels = levels - 1;
int filterOffset = (filterSize - 1) / 2; int byteOffset = 0; int calcOffset = 0; int currentIntensity = 0; int maxIntensity = 0; int maxIndex = 0;
double blue = 0; double green = 0; double red = 0;
for (int offsetY = filterOffset; offsetY < sourceBitmap.Height - filterOffset; offsetY++) { for (int offsetX = filterOffset; offsetX < sourceBitmap.Width - filterOffset; offsetX++) { blue = green = red = 0;
currentIntensity = maxIntensity = maxIndex = 0;
intensityBin = new int[levels + 1]; blueBin = new int[levels + 1]; greenBin = new int[levels + 1]; redBin = new int[levels + 1];
byteOffset = offsetY * sourceData.Stride + offsetX * 4;
for (int filterY = -filterOffset; filterY <= filterOffset; filterY++) { for (int filterX = -filterOffset; filterX <= filterOffset; filterX++) { calcOffset = byteOffset + (filterX * 4) + (filterY * sourceData.Stride);
currentIntensity = (int )Math.Round(((double) (pixelBuffer[calcOffset] + pixelBuffer[calcOffset + 1] + pixelBuffer[calcOffset + 2]) / 3.0 * (levels)) / 255.0);
intensityBin[currentIntensity] += 1; blueBin[currentIntensity] += pixelBuffer[calcOffset]; greenBin[currentIntensity] += pixelBuffer[calcOffset + 1]; redBin[currentIntensity] += pixelBuffer[calcOffset + 2];
if (intensityBin[currentIntensity] > maxIntensity) { maxIntensity = intensityBin[currentIntensity]; maxIndex = currentIntensity; } } }
blue = blueBin[maxIndex] / maxIntensity; green = greenBin[maxIndex] / maxIntensity; red = redBin[maxIndex] / maxIntensity;
resultBuffer[byteOffset] = ClipByte(blue); resultBuffer[byteOffset + 1] = ClipByte(green); resultBuffer[byteOffset + 2] = ClipByte(red); resultBuffer[byteOffset + 3] = 255; } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length);
resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Rose: Oil Painting, Filter 7, Levels 20, Cartoon Threshold 20

Rose Oil Painting Filter 7 Levels 20 Cartoon Threshold 20

Implementing a Cartoon Filter using Edge Detection

The sample source code defines the CheckThreshold method. The purpose of this method to determine the difference in colour between two pixels. In addition this method compares the colour difference and the specified threshold value. The following code snippet provides the implementation:

private static bool CheckThreshold(byte[] pixelBuffer,  
                                   int offset1, int offset2,  
                                   ref int gradientValue,  
                                   byte threshold,  
                                   int divideBy = 1) 
{ 
    gradientValue += 
    Math.Abs(pixelBuffer[offset1] - 
    pixelBuffer[offset2]) / divideBy; 

gradientValue += Math.Abs(pixelBuffer[offset1 + 1] - pixelBuffer[offset2 + 1]) / divideBy;
gradientValue += Math.Abs(pixelBuffer[offset1 + 2] - pixelBuffer[offset2 + 2]) / divideBy;
return (gradientValue >= threshold); }

Rose: Oil Painting, Filter 13, Levels 15

Rose Oil Painting Filter 13 Levels 15

The GradientBasedEdgeDetectionFilter method has been defined as an targeting the class. This method iterates each pixel forming part of the source/input . Whilst iterating pixels the GradientBasedEdgeDetectionFilter determines if the colour gradients in various directions exceeds the specified threshold value. A pixel is considered as part of an edge if a colour gradient exceeds the threshold value. The implementation as follows:

public static Bitmap GradientBasedEdgeDetectionFilter( 
                        this Bitmap sourceBitmap, 
                        byte threshold = 0) 
{ 
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle (0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height]; byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length); sourceBitmap.UnlockBits(sourceData);
int sourceOffset = 0, gradientValue = 0; bool exceedsThreshold = false;
for(int offsetY = 1; offsetY < sourceBitmap.Height - 1; offsetY++) { for(int offsetX = 1; offsetX < sourceBitmap.Width - 1; offsetX++) { sourceOffset = offsetY * sourceData.Stride + offsetX * 4; gradientValue = 0; exceedsThreshold = true ;
// Horizontal Gradient CheckThreshold(pixelBuffer, sourceOffset - 4, sourceOffset + 4, ref gradientValue, threshold, 2); // Vertical Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride, sourceOffset + sourceData.Stride, ref gradientValue, threshold, 2);
if (exceedsThreshold == false ) { gradientValue = 0;
// Horizontal Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - 4, sourceOffset + 4, ref gradientValue, threshold);
if (exceedsThreshold == false ) { gradientValue = 0; // Vertical Gradient exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride, sourceOffset + sourceData.Stride, ref gradientValue, threshold);
if (exceedsThreshold == false ) { gradientValue = 0; // Diagonal Gradient : NW-SE CheckThreshold(pixelBuffer, sourceOffset - 4 - sourceData.Stride, sourceOffset + 4 + sourceData.Stride, ref gradientValue, threshold, 2); // Diagonal Gradient : NE-SW exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride + 4, sourceOffset - 4 + sourceData.Stride, ref gradientValue, threshold, 2);
if (exceedsThreshold == false ) { gradientValue = 0; // Diagonal Gradient : NW-SE exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - 4 - sourceData.Stride, sourceOffset + 4 + sourceData.Stride, ref gradientValue, threshold);
if (exceedsThreshold == false ) { gradientValue = 0; // Diagonal Gradient : NE-SW exceedsThreshold = CheckThreshold(pixelBuffer, sourceOffset - sourceData.Stride + 4, sourceOffset + sourceData.Stride - 4, ref gradientValue, threshold); } } } } }
resultBuffer[sourceOffset] = (byte)(exceedsThreshold ? 255 : 0); resultBuffer[sourceOffset + 1] = resultBuffer[sourceOffset]; resultBuffer[sourceOffset + 2] = resultBuffer[sourceOffset]; resultBuffer[sourceOffset + 3] = 255; } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length); resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Rose: Oil Painting, Filter 7, Levels 20, Cartoon Threshold 20

Rose Oil Painting Filter 7 Levels 20 Cartoon Threshold 20

The CartoonFilter serves to combine generated by the OilPaintFilter and GradientBasedEdgeDetectionFilter methods. The CartoonFilter method being defined as an targets the class. In this method pixels detected as forming part of an edge are set to black in Oil Painting filtered . The definition as follows:

public static Bitmap CartoonFilter(this Bitmap sourceBitmap,
                                       int levels, 
                                       int filterSize, 
                                       byte threshold) 
{
    Bitmap paintFilterImage =  
           sourceBitmap.OilPaintFilter(levels, filterSize);

Bitmap edgeDetectImage = sourceBitmap.GradientBasedEdgeDetectionFilter(threshold);
BitmapData paintData = paintFilterImage.LockBits(new Rectangle (0, 0, paintFilterImage.Width, paintFilterImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] paintPixelBuffer = new byte[paintData.Stride * paintData.Height];
Marshal.Copy(paintData.Scan0, paintPixelBuffer, 0, paintPixelBuffer.Length);
paintFilterImage.UnlockBits(paintData);
BitmapData edgeData = edgeDetectImage.LockBits(new Rectangle (0, 0, edgeDetectImage.Width, edgeDetectImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] edgePixelBuffer = new byte[edgeData.Stride * edgeData.Height];
Marshal.Copy(edgeData.Scan0, edgePixelBuffer, 0, edgePixelBuffer.Length);
edgeDetectImage.UnlockBits(edgeData);
byte[] resultBuffer = new byte [edgeData.Stride * edgeData.Height];
for(int k = 0; k + 4 < paintPixelBuffer.Length; k += 4) { if (edgePixelBuffer[k] == 255 || edgePixelBuffer[k + 1] == 255 || edgePixelBuffer[k + 2] == 255) { resultBuffer[k] = 0; resultBuffer[k + 1] = 0; resultBuffer[k + 2] = 0; resultBuffer[k + 3] = 255; } else { resultBuffer[k] = paintPixelBuffer[k]; resultBuffer[k + 1] = paintPixelBuffer[k + 1]; resultBuffer[k + 2] = paintPixelBuffer[k + 2]; resultBuffer[k + 3] = 255; } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length);
resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Rose: Oil Painting, Filter 9, Levels 25, Cartoon Threshold 25

Rose Oil Painting Filter 9 Levels 25 Cartoon Threshold 25

Sample Images

This article features a number of sample images. All featured images have been licensed allowing for reproduction. The following image files feature a sample images:

Related Articles and Feedback

Feedback and questions are always encouraged. If you know of an alternative implementation or have ideas on a more efficient implementation please share in the comments section.

I’ve published a number of articles related to imaging and images of which you can find URL links here:

C# How to: Compass Edge Detection

Article Purpose

This article’s objective is to illustrate concepts relating to Compass . The methods implemented in this article include: , , Scharr, and Isotropic.

Wasp: Scharr 3 x 3 x 8

Wasp Scharr 3 x 3 x 8

Sample Source Code

This article is accompanied by a sample source code Visual Studio project which is available for download here.

Using the Sample Application

The sample source code accompanying this article includes a based sample application. When using the sample application users are able to load source/input from and save result to the local system. The user interface provides a which contains the supported methods of Compass Edge Detection. Selecting an item from the results in the related Compass Edge Detection method being applied to the current source/input . Supported methods are:

  • Prewitt3x3x4 – 3×3 in 4 compass directions
  • Prewitt3x3x8 – 3×3 in 8 compass directions
  • Prewitt5x5x4 – 5×5 in 4 compass directions
  • Sobel3x3x4 – 3×3 in 4 compass directions
  • Sobel3x3x8 – 3×3 in 8 compass directions
  • Sobel5x5x4 – 5×5 in 4 compass directions
  • Scharr3x3x4 – 3×3 Scharr in 4 compass directions
  • Scharr3x3x8 – 3×3 Scharr in 8 compass directions
  • Scharr5x5x4 – 5×5 Scharr in 4 compass directions
  • Kirsch3x3x4 – 3×3 in 4 compass directions
  • Kirsch3x3x8 – 3×3 in 8 compass directions
  • Isotropic3x3x4 – 3×3 Isotropic in 4 compass directions
  • Isotropic3x3x8 – 3×3 Isotropic in 8 compass directions

The following image is a screenshot of the Compass Edge Detection Sample Application in action:

Compass Edge Detection Sample Application

Bee: Isotropic 3 x 3 x 8

Bee Isotropic 3 x 3 x 8

Compass Edge Detection Overview

Compass Edge Detection as a concept title can be explained through the implementation of compass directions. Compass Edge Detection can be implemented through , using multiple , each suited to detecting edges in a specific direction. Often the edge directions implemented are:

  • North
  • North East
  • East
  • South East
  • South
  • South West
  • West
  • North West

Each of the compass directions listed above differ by 45 degrees. Applying a rotation of 45 degrees to an existing direction specific results in a new suited to detecting edges in the next compass direction.

Various can be implemented in Compass Edge Detection. This article and accompanying sample source code implements the following types:

Prey Mantis: Sobel 3 x 3 x 8

Prey Mantis Sobel 3 x 3 x 8

The steps required when implementing Compass Edge Detection can be described as follows:

  1. Determine the compass kernels. When an   suited to a specific direction is known, the suited to the 7 remaining compass directions can be calculated. Rotating a by 45 degrees around a central axis equates to the suited to the next compass direction. As an example, if the suited to detect edges in a northerly direction were to be rotated clockwise by 45 degrees around a central axis the result would be an suited to edges in a North Easterly direction.
  2. Iterate source image pixels. Every pixel forming part of the source/input should be iterated, implementing using each of the compass .
  3. Determine the most responsive kernel convolution. After having applied each compass to the pixel currently being iterated, the most responsive compass determines the output value. In other words, after having applied eight times on the same pixel using each compass direction the output value should be set to the highest value calculated.
  4. Validate and set output result. Ensure that the highest value returned from does not equate to less than 0 or more than 255. Should a value be less than zero the result should be assigned as zero. In a similar fashion, should a value exceed 255 the result should be assigned as 255.

Prewitt Compass Kernels

Prewitt Compass Kernels

LadyBug: Prewitt 3 x 3 x 8

LadyBug Prewitt 3 x 3 x 8

Rotating Convolution Kernels

can be rotated by implementing a . Repeatedly rotating by 45 degrees results in calculating 8 , each suited to a different direction. The algorithm implemented when performing a can be expressed as follows:

Rotate Horizontal Algorithm

Rotate Horizontal Algorithm

Rotate Vertical Algorithm

Rotate Vertical Algorithm

I’ve published an in-depth article on rotation available here:  

Butterfly: Sobel 3 x 3 x 8

Butterfly Sobel 3 x 3 x 8

Implementing Kernel Rotation

The sample source code defines the RotateMatrix method. This method accepts as parameter a single , defined as a two dimensional array of type double. In addition the method also expects as a parameter the degree to which the specified should be rotated. The definition as follows:

public static double[, ,] RotateMatrix(double[,] baseKernel,  
                                             double degrees) 
{
    double[, ,] kernel = new double[(int )(360 / degrees),  
        baseKernel.GetLength(0), baseKernel.GetLength(1)]; 

int xOffset = baseKernel.GetLength(1) / 2; int yOffset = baseKernel.GetLength(0) / 2;
for (int y = 0; y < baseKernel.GetLength(0); y++) { for (int x = 0; x < baseKernel.GetLength(1); x++) { for (int compass = 0; compass < kernel.GetLength(0); compass++) { double radians = compass * degrees * Math.PI / 180.0;
int resultX = (int)(Math.Round((x - xOffset) * Math.Cos(radians) - (y - yOffset) * Math.Sin(radians)) + xOffset);
int resultY = (int )(Math.Round((x - xOffset) * Math.Sin(radians) + (y - yOffset) * Math.Cos(radians)) + yOffset);
kernel[compass, resultY, resultX] = baseKernel[y, x]; } } }
return kernel; }

Butterfly: Prewitt 3 x 3 x 8

Butterfly Prewitt 3 x 3 x 8

Implementing Compass Edge Detection

The sample source code defines several which are implemented in . The following code snippet provides the of all defined:

public static double[, ,] Prewitt3x3x4 
{
    get 
    {
        double[,] baseKernel = new double[,]  
         { {  -1,  0,  1,  },  
           {  -1,  0,  1,  },  
           {  -1,  0,  1,  }, }; 

double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Prewitt3x3x8 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -1, 0, 1, }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 45);
return kernel; } }
public static double[, ,] Prewitt5x5x4 { get { double[,] baseKernel = new double[,] { { -2, -1, 0, 1, 2, }, { -2, -1, 0, 1, 2, }, { -2, -1, 0, 1, 2, }, { -2, -1, 0, 1, 2, }, { -2, -1, 0, 1, 2, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Kirsch3x3x4 { get { double[,] baseKernel = new double[,] { { -3, -3, 5, }, { -3, 0, 5, }, { -3, -3, 5, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Kirsch3x3x8 { get { double[,] baseKernel = new double[,] { { -3, -3, 5, }, { -3, 0, 5, }, { -3, -3, 5, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 45);
return kernel; } }
public static double[, ,] Sobel3x3x4 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -2, 0, 2, }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Sobel3x3x8 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -2, 0, 2, }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 45);
return kernel; } }
public static double[, ,] Sobel5x5x4 { get { double[,] baseKernel = new double[,] { { -5, -4, 0, 4, 5, }, { -8, -10, 0, 10, 8, }, { -10, -20, 0, 20, 10, }, { -8, -10, 0, 10, 8, }, { -5, -4, 0, 4, 5, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Scharr3x3x4 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -3, 0, 3, }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Scharr3x3x8 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -3, 0, 3, }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 45);
return kernel; } }
public static double[, ,] Scharr5x5x4 { get { double[,] baseKernel = new double[,] { { -1, -1, 0, 1, 1, }, { -2, -2, 0, 2, 2, }, { -3, -6, 0, 6, 3, }, { -2, -2, 0, 2, 2, }, { -1, -1, 0, 1, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Isotropic3x3x4 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -Math.Sqrt(2), 0, Math.Sqrt(2), }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 90);
return kernel; } }
public static double[, ,] Isotropic3x3x8 { get { double[,] baseKernel = new double[,] { { -1, 0, 1, }, { -Math.Sqrt(2), 0, Math.Sqrt(2), }, { -1, 0, 1, }, };
double[, ,] kernel = RotateMatrix(baseKernel, 45);
return kernel; } }

Notice how each property invokes the RotateMatrix method discussed in the previous section.

Butterfly: Scharr 3 x 3 x 8

Butterfly Scharr 3 x 3 x 8

The CompassEdgeDetectionFilter method is defined as an targeting the class. The purpose of this method is to act as a wrapper method encapsulating the technical implementation. The definition as follows:

public static Bitmap CompassEdgeDetectionFilter(this Bitmap sourceBitmap,  
                                    CompassEdgeDetectionType compassType) 
{ 
    Bitmap resultBitmap = null; 

switch (compassType) { case CompassEdgeDetectionType.Sobel3x3x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Sobel3x3x4, 1.0 / 4.0); } break; case CompassEdgeDetectionType.Sobel3x3x8: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Sobel3x3x8, 1.0/ 4.0); } break; case CompassEdgeDetectionType.Sobel5x5x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Sobel5x5x4, 1.0/ 84.0); } break; case CompassEdgeDetectionType.Prewitt3x3x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Prewitt3x3x4, 1.0 / 3.0); } break; case CompassEdgeDetectionType.Prewitt3x3x8: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Prewitt3x3x8, 1.0/ 3.0); } break; case CompassEdgeDetectionType.Prewitt5x5x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Prewitt5x5x4, 1.0 / 15.0); } break; case CompassEdgeDetectionType.Scharr3x3x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Scharr3x3x4, 1.0 / 4.0); } break; case CompassEdgeDetectionType.Scharr3x3x8: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Scharr3x3x8, 1.0 / 4.0); } break; case CompassEdgeDetectionType .Scharr5x5x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Scharr5x5x4, 1.0 / 21.0); } break; case CompassEdgeDetectionType.Kirsch3x3x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Kirsch3x3x4, 1.0 / 15.0); } break; case CompassEdgeDetectionType.Kirsch3x3x8: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Kirsch3x3x8, 1.0 / 15.0); } break; case CompassEdgeDetectionType.Isotropic3x3x4: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Isotropic3x3x4, 1.0 / 3.4); } break; case CompassEdgeDetectionType.Isotropic3x3x8: { resultBitmap = sourceBitmap.ConvolutionFilter(Matrix.Isotropic3x3x8, 1.0 / 3.4); } break; }
return resultBitmap; }

Rose: Scharr 3 x 3 x 8

Rose Scharr 3 x 3 x 8

Notice from the code snippet listed above, each case statement invokes the ConvolutionFilter method. This method has been defined as an targeting the class. The ConvolutionFilter performs the actual task of . This method implements each passed as a parameter, the highest result value will be determined as the output value. The definition as follows:

private static Bitmap ConvolutionFilter(this Bitmap sourceBitmap,  
                                     double[,,] filterMatrix,  
                                           double factor = 1,  
                                                int bias = 0)  
{
    BitmapData sourceData = sourceBitmap.LockBits(new Rectangle(0, 0, 
                             sourceBitmap.Width, sourceBitmap.Height), 
                                               ImageLockMode.ReadOnly,  
                                         PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte [sourceData.Stride * sourceData.Height]; byte[] resultBuffer = new byte [sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length); sourceBitmap.UnlockBits(sourceData);
double blue = 0.0; double green = 0.0; double red = 0.0;
double blueCompass = 0.0; double greenCompass = 0.0; double redCompass = 0.0;
int filterWidth = filterMatrix.GetLength(1); int filterHeight = filterMatrix.GetLength(0);
int filterOffset = (filterWidth-1) / 2; int calcOffset = 0;
int byteOffset = 0;
for (int offsetY = filterOffset; offsetY < sourceBitmap.Height - filterOffset; offsetY++) { for (int offsetX = filterOffset; offsetX < sourceBitmap.Width - filterOffset; offsetX++) { blue = 0; green = 0; red = 0;
byteOffset = offsetY * sourceData.Stride + offsetX * 4;
for (int compass = 0; compass < filterMatrix.GetLength(0); compass++) {
blueCompass = 0.0; greenCompass = 0.0; redCompass = 0.0;
for (int filterY = -filterOffset; filterY <= filterOffset; filterY++) { for (int filterX = -filterOffset; filterX <= filterOffset; filterX++) { calcOffset = byteOffset + (filterX * 4) + (filterY * sourceData.Stride);
blueCompass += (double)(pixelBuffer[calcOffset]) * filterMatrix[compass, filterY + filterOffset, filterX + filterOffset];
greenCompass += (double)(pixelBuffer[calcOffset + 1]) * filterMatrix[compass, filterY + filterOffset, filterX + filterOffset];
redCompass += (double)(pixelBuffer[calcOffset + 2]) * filterMatrix[compass, filterY + filterOffset, filterX + filterOffset]; } }
blue = (blueCompass > blue ? blueCompass : blue); green = (greenCompass > green ? greenCompass : green); red = (redCompass > red ? redCompass : red); }
blue = factor * blue + bias; green = factor * green + bias; red = factor * red + bias;
if(blue > 255) { blue = 255; } else if(blue < 0) { blue = 0; }
if(green > 255) { green = 255; } else if(green < 0) { green = 0; }
if(red > 255) { red = 255; } else if(red < 0) { red = 0; }
resultBuffer[byteOffset] = (byte)(blue); resultBuffer[byteOffset + 1] = (byte)(green); resultBuffer[byteOffset + 2] = (byte)(red); resultBuffer[byteOffset + 3] = 255; } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length); resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Rose: Isotropic 3 x 3 x 8

Rose Isotropic 3 x 3 x 8

Sample Images

This article features a number of sample images. All featured images have been licensed allowing for reproduction. The following image files feature a sample images:

The Original Image

Original Image

Butterfly: Isotropic 3 x 3 x 4

Butterfly Isotropic 3 x 3 x 4

Butterfly: Isotropic 3 x 3 x 8

Butterfly Isotropic 3 x 3 x 8

Butterfly: Kirsch 3 x 3 x 4

Butterfly Kirsch 3 x 3 x 4

Butterfly: Kirsch 3 x 3 x 8

Butterfly Kirsch 3 x 3 x 8

Butterfly: Prewitt 3 x 3 x 4

Butterfly Prewitt 3 x 3 x 4

Butterfly: Prewitt 3 x 3 x 8

Butterfly Prewitt 3 x 3 x 8

Butterfly: Prewitt 5 x 5 x 4

Butterfly Prewitt 5 x 5 x 4

Butterfly: Scharr 3 x 3 x 4

Butterfly Scharr 3 x 3 x 4

Butterfly: Scharr 3 x 3 x 8

Butterfly Scharr 3 x 3 x 8

Butterfly: Scharr 5 x 5 x 4

Butterfly Scharr 5 x 5 x 4

Butterfly: Sobel 3  x 3 x 4

Butterfly Sobel 3  x 3 x 4

Butterfly: Sobel 3 x 3 x 8

Butterfly Sobel 3 x 3 x 8

Butterfly: Sobel 5 x 5 x 4

Butterfly Sobel 5 x 5 x 4

Related Articles and Feedback

Feedback and questions are always encouraged. If you know of an alternative implementation or have ideas on a more efficient implementation please share in the comments section.

I’ve published a number of articles related to imaging and images of which you can find URL links here:

C# How to: Image Transform Shear

Article Purpose

This article is focussed on illustrating the steps required in performing an . All of the concepts explored have been implemented by means of raw pixel data processing, no conventional drawing methods, such as GDI, are required.

Rabbit: Shear X 0.4, Y 0.4

Rabbit Shear X 0.4, Y 0.4

Sample Source Code

This article is accompanied by a sample source code Visual Studio project which is available for download here.

Using the Sample Application

article features a based sample application which is included as part of the accompanying sample source code. The concepts explored in this article can be illustrated in a practical implementation using the sample application.

The sample application enables a user to load source/input from the local system when clicking the Load Image button. In addition users are also able to save output result to the local file system by clicking the Save Image button.

Image can be applied to either X or Y, or both X and Y pixel coordinates. When using the sample application the user has option of adjusting Shear factors, as indicated on the user interface by the numeric up/down controls labelled Shear X and Shear Y.

The following image is a screenshot of the Image Transform Shear Sample Application in action:

Image Transform Shear Sample Application

Rabbit: Shear X -0.5, Y -0.25

Rabbit Shear X -0.5, Y -0.25

Image Shear Transformation

A good definition of the term can be found on the Wikipedia :

In , a shear mapping is a that displaces each point in fixed direction, by an amount proportional to its signed distance from a line that is to that direction.[1] This type of mapping is also called shear transformation, transvection, or just shearing

A can be applied as a horizontal shear, a vertical shear or as both. The algorithms implemented when performing a can be expressed as follows:

Horizontal Shear Algorithm

Horizontal Shear Algorithm

Vertical Shear Algorithm

Vertical Shear Algorithm

The algorithm description:

  • Shear(x) : The result of a horizontal – The calculated X-Coordinate representing a .
  • Shear(y) : The result of a vertical – The calculated Y-Coordinate representing a .
  • σ : The lower case version of the Greek alphabet letter Sigma – Represents the Shear Factor.
  • x : The X-Coordinate originating from the source/input – The horizontal coordinate value intended to be sheared.
  • y : The Y-Coordinate originating from the source/input – The vertical coordinate value intended to be sheared.
  • H : Source height in pixels.
  • W : Source width in pixels.

Note: When performing a implementing both the horizontal and vertical planes each coordinate plane can be calculated using a different shearing factor.

The algorithms have been adapted in order to implement a middle pixel offset by means of subtracting the product of the related plane boundary and the specified Shearing Factor, which will then be divided by a factor of two.

Rabbit: Shear X 1.0, Y 0.1

Rabbit Shear X 1.0, Y 0.1

Implementing a Shear Transformation

The sample source code performs through the implementation of the ShearXY and ShearImage.

The ShearXY targets the structure. The algorithms discussed in the previous sections have been implemented in this function from a C# perspective. The definition as illustrated by the following code snippet:

public static Point ShearXY(this Point source, double shearX, 
                                               double shearY, 
                                               int offsetX,  
                                               int offsetY) 
{
    Point result = new Point(); 

result.X = (int)(Math.Round(source.X + shearX * source.Y)); result.X -= offsetX;
result.Y = (int)(Math.Round(source.Y + shearY * source.X)); result.Y -= offsetY;
return result; }

Rabbit: Shear X 0.0, Y 0.5

Rabbit Shear X 0.0, Y 0.5

The ShearImage targets the class. This method expects as parameter values a horizontal and a vertical shearing factor. Providing a shearing factor of zero results in no shearing being implemented in the corresponding direction. The definition as follows:

public static Bitmap ShearImage(this Bitmap sourceBitmap, 
                               double shearX, 
                               double shearY) 
{ 
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle(0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height];
byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length);
sourceBitmap.UnlockBits(sourceData);
int xOffset = (int )Math.Round(sourceBitmap.Width * shearX / 2.0);
int yOffset = (int )Math.Round(sourceBitmap.Height * shearY / 2.0);
int sourceXY = 0; int resultXY = 0;
Point sourcePoint = new Point(); Point resultPoint = new Point();
Rectangle imageBounds = new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height);
for (int row = 0; row < sourceBitmap.Height; row++) { for (int col = 0; col < sourceBitmap.Width; col++) { sourceXY = row * sourceData.Stride + col * 4;
sourcePoint.X = col; sourcePoint.Y = row;
if (sourceXY >= 0 && sourceXY + 3 < pixelBuffer.Length) { resultPoint = sourcePoint.ShearXY(shearX, shearY, xOffset, yOffset);
resultXY = resultPoint.Y * sourceData.Stride + resultPoint.X * 4;
if (imageBounds.Contains(resultPoint) && resultXY >= 0) { if (resultXY + 6 <= resultBuffer.Length) { resultBuffer[resultXY + 4] = pixelBuffer[sourceXY];
resultBuffer[resultXY + 5] = pixelBuffer[sourceXY + 1];
resultBuffer[resultXY + 6] = pixelBuffer[sourceXY + 2];
resultBuffer[resultXY + 7] = 255; }
if (resultXY - 3 >= 0) { resultBuffer[resultXY - 4] = pixelBuffer[sourceXY];
resultBuffer[resultXY - 3] = pixelBuffer[sourceXY + 1];
resultBuffer[resultXY - 2] = pixelBuffer[sourceXY + 2];
resultBuffer[resultXY - 1] = 255; }
if (resultXY + 3 < resultBuffer.Length) { resultBuffer[resultXY] = pixelBuffer[sourceXY];
resultBuffer[resultXY + 1] = pixelBuffer[sourceXY + 1];
resultBuffer[resultXY + 2] = pixelBuffer[sourceXY + 2];
resultBuffer[resultXY + 3] = 255; } } } } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length);
resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Rabbit: Shear X 0.5, Y 0.0

Rabbit Shear X 0.5, Y 0.0

Sample Images

This article features a number of sample images. All featured images have been licensed allowing for reproduction.

The sample images featuring the image of a Desert Cottontail Rabbit is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported license and can be downloaded from Wikipedia. The original author is attributed as Larry D. Moore.

The sample images featuring the image of a Rabbit in Snow is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported license and can be downloaded from Wikipedia. The original author is attributed as George Tuli.

The sample images featuring the image of an Eastern Cottontail Rabbit has been released into the public domain by its author. The original image can be downloaded from .

The sample images featuring the image of a Mountain Cottontail Rabbit is in the public domain in the United States because it is a work prepared by an officer or employee of the United States Government as part of that person’s official duties under the terms of Title 17, Chapter 1, Section 105 of the US Code. The original image can be downloaded from .

Rabbit: Shear X 1.0, Y 0.0

Rabbit Shear X 1.0, Y 0.0

Rabbit: Shear X 0.5, Y 0.1

Rabbit Shear X 0.5, Y 0.1

Rabbit: Shear X -0.5, Y -0.25

Rabbit Shear X -0.5, Y -0.25

Rabbit: Shear X -0.5, Y 0.0

Rabbit Shear X -0.5, Y 0.0

Rabbit: Shear X 0.25, Y 0.0

Rabbit Shear X 0.25, Y 0.0

Rabbit: Shear X 0.50, Y 0.0

Rabbit Shear X 0.50, Y 0.0

Rabbit: Shear X 0.0, Y 0.5

Rabbit Shear X 0.0, Y 0.5

Rabbit: Shear X 0.0, Y 0.25

Rabbit Shear X 0.0, Y 0.25

Rabbit: Shear X 0.0, Y 1.0

Rabbit Shear X 0.0, Y 1.0

Related Articles and Feedback

Feedback and questions are always encouraged. If you know of an alternative implementation or have ideas on a more efficient implementation please share in the comments section.

I’ve published a number of articles related to imaging and images of which you can find URL links here:

C# How to: Image Transform Rotate

Article Purpose

This article provides a discussion exploring the concept of rotation as a . In addition to conventional rotation this article illustrates the concept of individual colour channel rotation.

Daisy: Rotate Red 0o, Green 10o, Blue 20o

Daisy Rotate Red 0 Green 10 Blue 20 

Sample Source Code

This article is accompanied by a sample source code Visual Studio project which is available for download .

Using the Sample Application

A Sample Application has been included in the sample source code that accompanies this article. The sample application serves as an implementation of the concepts discussed throughout this article. Concepts can be easily tested and replicated using the sample application.

Daisy: Rotate Red 15o, Green 5o, Blue 10o

Daisy Rotate Red 15 Green 5 Blue 10

When using the sample application users are able to load source/input from the local system by clicking the Load Image button. Required user input via the user interface can be found in the form of three numeric up/down controls labelled Blue, Green and Red respectively. Each control represents the degree to which the related colour component should be rotated. Possible input values range from –360 to 360. Positive values result in clockwise rotation, whereas negative values result in counter clockwise rotation. The sample application enables users to save result to the local file system by clicking the Save Image button.

The following image is a screenshot of the Image Transform Rotate sample application in action:

Image Transform Rotate Sample Application

Image Rotation Transformation

A applied to an from a theoretical point of view is based in . From we learn the following :

In mathematics, transformation geometry (or transformational geometry) is the name of a mathematical and approach to the study of by focusing on groups of , and the properties of figures that are under them. It is opposed to the classical synthetic geometry approach of Euclidean geometry, that focus on geometric constructions.

Rose: Rotate Red –20o, Green 0o, Blue 20o

Rose Rotate Red -20 Green 0 Blue 20

In this article rotation is implemented through applying a set algorithm to the coordinates of each pixel forming part of a source/input . In the corresponding result the calculated rotated pixel coordinates in terms of colour channel values will be assigned to the colour channel values of the original pixel.

The algorithms implemented when calculating  a pixel’s rotated coordinates can be expressed as follows:

RotateX_Algorithm

RotateY_Algorithm

Symbols/variables contained in the algorithms:

  • R (x) : The result of rotating a pixel’s x-coordinate.
  • R (y) : The result of rotating a pixel’s y-coordinate.
  • x : The source pixel’s x-coordinate.
  • y : The source pixel’s y-coordinate.
  • W : The width in pixels of the source .
  • H : The height in pixels of the source .
  • ɑ : The lower case Greek alphabet letter alpha. The value represented by alpha reflects the degree of rotation.

Butterfly: Rotate Red 10o, Green 0o, Blue 0o

Butterfly Rotate Red 10 Green 0 Blue 0

In order to apply a each pixel forming part of the source/input should be iterated. The algorithms expressed above should be applied to each pixel.

The pixel coordinates located at exactly the middle of an can be calculated through dividing the width with a factor of two in regards to the X-coordinate. The Y-coordinate can be calculated through dividing the height also with a factor of two. The algorithms calculate the coordinates of the middle pixel and implements the coordinates as offsets. Implementing the pixel offsets  results in being rotated around the ’s middle, as opposed to the the top left pixel (0,0).

This article and the associated sample source code extends the concept of traditional rotation through implementing rotation on a per colour channel basis. Through user input the individual degree of rotation can be specified for each colour channel, namely Red, Green and Blue. Functionality has been implemented allowing each colour channel to be rotated to a different degree. In essence the algorithms described above have to be implemented three times per pixel iterated.

Daisy: Rotate Red 30o, Green 0o, Blue 180o

Daisy Rotate Red 30 Green 0 Blue 180 

Implementing a Rotation Transformation

The sample source code implements a through the of two : RotateXY and RotateImage.

The RotateXY targets the structure. This method serves as an encapsulation of the logic behind calculating rotating coordinates at a specified angle. The practical C# code implementation of the algorithms discussed in the previous section can be found within this method. The definition as follows:

public static Point RotateXY(this Point source, double degrees,
                                       int offsetX, int offsetY)
{ 
   Point result = new Point();
 
   result.X = (int)(Math.Round((source.X - offsetX) *
              Math.Cos(degrees) - (source.Y - offsetY) *
              Math.Sin(degrees))) + offsetX;

result.Y = (int)(Math.Round((source.X - offsetX) * Math.Sin(degrees) + (source.Y - offsetY) * Math.Cos(degrees))) + offsetY;
return result; }

Rose: Rotate Red –60o, Green 0o, Blue 60o

Rose Rotate Red -60 Green 0 Blue 60

The RotateImage targets the class. This method expects three rotation degree/angle values, each corresponding to a colour channel. Positive degrees result in clockwise rotation and negative values result in counter clockwise rotation.  The definition as follows:

public static Bitmap RotateImage(this Bitmap sourceBitmap,  
                                       double degreesBlue, 
                                      double degreesGreen, 
                                        double degreesRed) 
{ 
    BitmapData sourceData = 
               sourceBitmap.LockBits(new Rectangle(0, 0, 
               sourceBitmap.Width, sourceBitmap.Height), 
               ImageLockMode.ReadOnly, 
               PixelFormat.Format32bppArgb); 

byte[] pixelBuffer = new byte[sourceData.Stride * sourceData.Height];
byte[] resultBuffer = new byte[sourceData.Stride * sourceData.Height];
Marshal.Copy(sourceData.Scan0, pixelBuffer, 0, pixelBuffer.Length);
sourceBitmap.UnlockBits(sourceData);
//Convert to Radians degreesBlue = degreesBlue * Math.PI / 180.0; degreesGreen = degreesGreen * Math.PI / 180.0; degreesRed = degreesRed * Math.PI / 180.0;
//Calculate Offset in order to rotate on image middle int xOffset = (int )(sourceBitmap.Width / 2.0); int yOffset = (int )(sourceBitmap.Height / 2.0);
int sourceXY = 0; int resultXY = 0;
Point sourcePoint = new Point(); Point resultPoint = new Point();
Rectangle imageBounds = new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height);
for (int row = 0; row < sourceBitmap.Height; row++) { for (int col = 0; col < sourceBitmap.Width; col++) { sourceXY = row * sourceData.Stride + col * 4;
sourcePoint.X = col; sourcePoint.Y = row;
if (sourceXY >= 0 && sourceXY + 3 < pixelBuffer.Length) { //Calculate Blue Rotation
resultPoint = sourcePoint.RotateXY(degreesBlue, xOffset, yOffset);
resultXY = (int)(Math.Round( (resultPoint.Y * sourceData.Stride) + (resultPoint.X * 4.0)));
if (imageBounds.Contains(resultPoint) && resultXY >= 0) { if (resultXY + 6 < resultBuffer.Length) { resultBuffer[resultXY + 4] = pixelBuffer[sourceXY];
resultBuffer[resultXY + 7] = 255; }
if (resultXY + 3 < resultBuffer.Length) { resultBuffer[resultXY] = pixelBuffer[sourceXY];
resultBuffer[resultXY + 3] = 255; } }
//Calculate Green Rotation
resultPoint = sourcePoint.RotateXY(degreesGreen, xOffset, yOffset);
resultXY = (int)(Math.Round( (resultPoint.Y * sourceData.Stride) + (resultPoint.X * 4.0)));
if (imageBounds.Contains(resultPoint) && resultXY >= 0) { if (resultXY + 6 < resultBuffer.Length) { resultBuffer[resultXY + 5] = pixelBuffer[sourceXY + 1];
resultBuffer[resultXY + 7] = 255; }
if (resultXY + 3 < resultBuffer.Length) { resultBuffer[resultXY + 1] = pixelBuffer[sourceXY + 1];
resultBuffer[resultXY + 3] = 255; } }
//Calculate Red Rotation
resultPoint = sourcePoint.RotateXY(degreesRed, xOffset, yOffset);
resultXY = (int)(Math.Round( (resultPoint.Y * sourceData.Stride) + (resultPoint.X * 4.0)));
if (imageBounds.Contains(resultPoint) && resultXY >= 0) { if (resultXY + 6 < resultBuffer.Length) { resultBuffer[resultXY + 6] = pixelBuffer[sourceXY + 2]; resultBuffer[resultXY + 7] = 255; }
if (resultXY + 3 < resultBuffer.Length) { resultBuffer[resultXY + 2] = pixelBuffer[sourceXY + 2];
resultBuffer[resultXY + 3] = 255; } } } } }
Bitmap resultBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);
BitmapData resultData = resultBitmap.LockBits(new Rectangle (0, 0, resultBitmap.Width, resultBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(resultBuffer, 0, resultData.Scan0, resultBuffer.Length);
resultBitmap.UnlockBits(resultData);
return resultBitmap; }

Daisy: Rotate Red 15o, Green 5o, Blue 5o

Daisy Rotate Red 15 Green 5 Blue 5

Sample Images

This article features a number of sample images. All featured images have been licensed allowing for reproduction.

The sample images featuring an image of a yellow daisy is licensed under the Creative Commons Attribution-Share Alike 2.5 Generic license and can be downloaded from Wikimedia.org.

The sample images featuring an image of a white daisy is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported license and can be downloaded from Wikipedia.

The sample images featuring an image of a CPU is licensed under the Creative Commons Attribution-Share Alike 2.0 Generic license. The original author is credited as Andrew Dunn. The original image can be downloaded from .

The sample images featuring an image of a rose is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported, 2.5 Generic, 2.0 Generic and 1.0 Generic license. The original image can be downloaded from .

The sample images featuring an image of a butterfly is licensed under the Creative Commons Attribution 3.0 Unported license and can be downloaded from Wikimedia.org.

The Original Image

Intel_80486DX2_bottom

CPU: Rotate Red 90o, Green 0o, Blue –30o

CPU Rotate Red 90 Green 0 Blue -30

CPU: Rotate Red 0o, Green 10o, Blue 0o

CPU Rotate Red 0 Green 10 Blue 0

CPU: Rotate Red –4o, Green 4o, Blue 6o

CPU Rotate Red -4 Green 4 Blue 6

CPU: Rotate Red 10o, Green 0o, Blue 0o

CPU Rotate Red 10 Green 0 Blue 0

CPU: Rotate Red 10o, Green –5o, Blue 0o

CPU Rotate Red 10 Green -5 Blue 0

CPU: Rotate Red 10o, Green 0o, Blue 10o

CPU Rotate Red 10 Green 0 Blue 10

CPU: Rotate Red –10o, Green 10o, Blue 0o

CPU Rotate Red -10 Green 10 Blue 0

CPU: Rotate Red 30o, Green –30o, Blue 0o

CPU Rotate Red 30 Green -30 Blue 0

CPU: Rotate Red 40o, Green 20o, Blue 0o

CPU Rotate Red 40 Green 20 Blue 0

CPU: Rotate Red 40o, Green 20o, Blue 0o

CPU Rotate Red 60 Green 30 Blue 0

Related Articles and Feedback

Feedback and questions are always encouraged. If you know of an alternative implementation or have ideas on a more efficient implementation please share in the comments section.

I’ve published a number of articles related to imaging and images of which you can find URL links here:


Dewald Esterhuizen

Blog Stats

  • 843,493 hits

Enter your email address to follow and receive notifications of new posts by email.

Join 228 other subscribers

Archives


%d bloggers like this: