1. Introduction
Innumerable are the methods by which images can be manipulated so as to create new ones; some of these methods are quite simple, yet create new ones which appear cool and special. A number of software applications create these cool effects, and it is fascinating to understand the inner mathematical operations of these effects. These so called mathematical operations are often simple arithmetic manipulations, easy to understand and implement; their beauty is that we can easily see the effects on the images.
We find many of these mathematical formulae scattered over the Internet; whereas others remain hidden as 'trade secrets' within commercial applications. In this article, we present a program which applies some interesting effects to your favourite images; and we also present the formulae and simple implementations to create these effects. To keep the code reasonably simple and understandable, we've not attempted any optimizations to increase speed. It is our hope that you will enjoy these cool effects, and also get an understanding of the corresponding mathematics and code.
2. Objective
This objective of this article, and software application is to:
- Explore the mathematics, rather arithmetic, behind some of these image effects.
- Enable the user to select an effect from one of these:
-
- Sepia: Your photo gradually fades over the years.
- Pixelate: Someone just averages out blocks of the picture.
- Quantize: Fifty shades of grey just merge into one.
- Wave: A sine wave just passes by.
- Solarize: Someone just mixes up the colours.
- Parabolize: Yet another colour mix up.
- X-ray: Wilhelm Roentgen stops by, and says 'Hello, how do you do?'.
- Canvas: A weaver weaves your image on canvas.
- Dither: A drop of ink ponders - 'Should I deposit, or not?'.
- Edge: Everything between the edges just gets washed out.
- Emboss: A bas-relief artist sees your picture.
- Glass: Someone sends you a ground glass sheet.
- Oil Paint: Not Picasso, or van Gogh, though.
- Outline: Another edgy wash out.
- Vary parameters for these effects, and get a (near) immediate preview.
- Save the resulting image.
Some of these effects, as mentioned earlier are taken from other public-domain sources, and we acknowledge them gratefully; whereas the other effects are our own 'findings' - we will point them out appropriately.
An important aspect in our implementation is that we'd like all the code to be ours. No usage of third-party DLLs. Yes - we have a DLL in our application, but its code is entirely ours.
There's a reason for keeping the code self-contained, without external dependencies (other than Windows APIs). We see a trend among beginners / students today to use image processing packages, especially one-word commands. While this facilitates speed of learning, our impression is that it may leave gaps in understanding the implementation behind such commands. In this application, there are no 'commands'; we directly manipulate the individual R, G, B values via code.
3. Algorithms for these effects
We've created a PDF file containing all the formulae, along with short descriptions of the steps. This document is among the downloads above. This is intended to serve as a handy reference to the methods. We've tried to keep the descriptions simple, easy to follow, and quick to read. We've also tried to keep the description brief and to-the-point, so that in six pages, it covers all our fourteen effects. Should you find difficulty in understanding these descriptions, please write back to us via the forum.
4. Usage of the software
The main screen for this application is shown below:
As in an earlier article by one of the authors, we use the strategy of fitting the input image to size 600 x 600, and apply all effects on this scaled image during user interaction. Some real-life images are really large; applying the effect on such images sizes may cause user annoyance in terms of delayed response. We therefore scale to 600 x 600, and then use this for preview. This happens reasonably in real time.
We've designed the application to be reasonably intuitive, and self-explanatory. However, for completeness, we list the steps to use it:
- Open an image by clicking on the Open Image button. A default image is available for you to play with.
- Select any effect using the effect selection combo shown below.
- Vary any parameters to see the effect on the right pane. Some of the effects enable you to change colours by clicking on the buttons shown below.
- Save the resulting image by right-clicking on the right pane.
5. A Look at the Effects
In this section, we show the effects on a representative image. This image shown below, and is of a statue of Kempegowda, who founded our city, Bengaluru. more than 400 years ago. One of the authors recently visited the city centre, and shot this picture. This picture represents at once the older and newer aspects of our city, with a modern building on the left. Incidentally, the international airport here is named after him. With the intention of testing our cool effects of text also, we've included his name on the image, in the local language Kannada, and in English.
Now, to each of these effects:
5a. Sepia
Code: SepiaAlgorithm.cs. The code for computing the sepia tone is:
for (el = 0; el < CurrentHeight; ++el) {
for (k = 0; k < CurrentWidth; ++k) {
index = el * CurrentWidth + k;
r = Pixels8RedCurrent[index];
g = Pixels8GreenCurrent[index];
b = Pixels8BlueCurrent[index];
intensity = 0.3 * r + 0.6 * g + 0.1 * b;
if (intensity > threshold)
tone = 255.0;
else
tone = intensity + 255.0 - threshold;
dRed = tone;
pixDoubleRed.Add(dRed);
pixDoubleGreen.Add(dGreen);
pixDoubleBlue.Add(dBlue);
}
}
}
Here, Pixels8RedCurrent[]
are the red pixels from the input image, whereas pixDoubleRed[]
are the corresponding ones from the intermediate double image created. These are subsequently scaled to the range 0 - 255 to get back the sepia toned image.
For two different settings of the sepia slider, we get these images:
5b. Pixelate
Code: PixelateAlgorithm.cs.
The most important method here is to setup the block geometry. This is achieved using the following code. The mathematics (rather arithmetic for this is explained in the PDF file available for download).
void SetupBlockGeometry(List<algorithmparameter> value) {
var userBlockWidth = value.First(x => x.ParameterName == "BlockWidth");
noHorizBlocks = userBlockWidth.Value;
if (noHorizBlocks >= CurrentWidth)
noHorizBlocks = CurrentWidth / 2;
blockWidth = CurrentWidth / noHorizBlocks;
blockWidths = Enumerable.Repeat(blockWidth, noHorizBlocks).ToList();
var horizList = Enumerable.Range(0, noHorizBlocks - 1).ToList();
Random random = new Random(Environment.TickCount);
shuffledHorizList = horizList.OrderBy(k => random.Next()).ToList();
int diffH = CurrentWidth - noHorizBlocks * blockWidth;
if (diffH > 0) {
++blockWidths[noHorizBlocks - 1];
for (int i = 0; i < diffH - 1; ++i) {
++blockWidths[shuffledHorizList[i]];
}
}
}
This is followed by averaging the colour over a block.
For two different parameter settings, we get these images:
5c. Quantize
Code: QuantizeAlgorithm.cs. The relevant code extract is:
for (int i = 0; i < Pixels8RedCurrent.Count; i++) {
if (!bw) {
Pixels8RedResult[i] = (byte)((Pixels8RedCurrent[i] / sizeOfStep) * sizeOfStep);
} else {
bGray = (byte)(0.3 * Pixels8RedCurrent[i] + 0.6 * Pixels8GreenCurrent[i]
+ 0.1 * Pixels8BlueCurrent[i]);
Pixels8RedResult[i] = (byte)((bGray / sizeOfStep) * sizeOfStep);
}
}
This code just does a rounding-off of the pixel values to the nearest mid-point of the corresponding range. We also provide a binarize functionality.
5d. Wave
Code: WaveAlgorithm.cs
The relevant code extract is here (again, refer to the PDF for the math). This is a case of image warping.
for (el = 0; el < CurrentHeight; ++el) {
y = el;
w2 = CurrentWidth * y;
for (k = 0; k < CurrentWidth; ++k) {
x = k;
if (currentSelection == 1) {
y = Convert.ToInt32(el + 20.0 * Math.Sin(2.0 * Math.PI * k / 128.0));
}
w1 = CurrentWidth * y + x;
r = Pixels8RedCurrent[w1];
w1 = CurrentWidth * el + k;
Pixels8RedResult[w1] = r;
}
}
5e. Solarize
Code: SolarizeAlgorithm.cs
The relevant code is:
for (el = 0; el < CurrentHeight; ++el) {
w2 = CurrentWidth * el;
for (k = 0; k < CurrentWidth; ++k) {
w1 = w2 + k;
r = Pixels8RedCurrent[w1];
if (r > threshold)
r = (byte)(255 - r);
Pixels8RedResult[w1] = r;
}
}
This involves some kind of inversion on the pixel values, depending on the user-specified threshold.
5f. Parabolize
Code: ParabolaAlgorithm.cs
Code is similar to solarize, but with a different formula. Refer to the PDF notes.
5g. X-Ray
Code: XRayAlgorithm.cs
This is an easy algorithm, whose code is:
for (el = 0; el < CurrentHeight; ++el) {
w2 = el * CurrentWidth;
for (k = 0; k < CurrentWidth; ++k) {
index = w2 + k;
r = Pixels8RedCurrent[index];
g = Pixels8GreenCurrent[index];
b = Pixels8BlueCurrent[index];
dGray = 0.3 * r + 0.6 * g + 0.1 * b;
dGray = 255 + factor - dGray;
byteGray = (byte)dGray;
SetBackgroundColour(colour, index, byteGray);
}
}
5h. Canvas
Code: CanvasAlgorithm.cs
As written in the PDF file, this and all subsequent algorithms are non-scalable algorithms.
This algorithm makes use of two images, one of which is a texture image. Both the input image and the texture image are composited to create the new one.
if (bVal < 128) {
resultRed = (byte)((bVal * r) >> 7);
} else {
resultRed = (byte)(255 - (((255 - bVal) * (255 - r)) >> 7));
}
Here, bVal
comes from the texture image, whereas r, g and b are from the input image. This is one way of compositing; there are many other ways possible.
5i. Dither
Code: DitherAlgorithm.cs
This makes use of a dither matrix, ten of which are provided in the code.
for (int j = 0; j < height; ++j) {
start = width * j;
for (int i = 0; i < width; ++i) {
i1 = i + start;
threshold = ditherMatrix[methodValue.Value - 1, j % n, i % n];
if (PixGray[i1] > threshold) {
SetBackgroundColour(colour, i1, value);
} else {
SetBackgroundColour(colour, i1, 0);
}
}
}
Here, pixGray
is the grayscale value.
5j. Edge
Code: EdgeAlgorithm.cs
This is quite an elaborate algorithm, which is explained in detail in the PDF notes. The relevant code extract is:
ComputeGrayscaleImage();
ComputePixGrayAverage(CurrentHeight, CurrentWidth);
ComputeDxAndDyImages(CurrentHeight, CurrentWidth);
ComputeThetaAndMagnitudeImages(CurrentHeight, CurrentWidth);
ComputeMaxAndMinMagnitudeImage();
CreateFinalImage(CurrentHeight, CurrentWidth, threshold);
5k. Emboss
Code: EmbossAlgorithm.cs
This uses an emboss matrix, and is explained in the PDF Notes. Five such emboss matrices are provided. The relevant code extract is:
ComputeGrayscaleImage();
ComputeDoubleImage();
ComputeMaxAndMinDoubleImage();
CreateFinalImage(colour);
5l. Glass
Code: GlassAlgorithm.cs
This is explained in detail in the PDF Notes. A pixel at location (i, j) is replaced by another within a user-specified neighbourhood of this pixel. Looking at how the code works is left as an exercise to you.
5m. Oil Paint
Code: OilPaintAlgorithm.cs
The math for this also explained in the PDF Notes. Essentially, a pixel at location (i, j) is replaced by the most frequently occurring pixel value within a neighbourhood.
5n. Outline
Code: EdgeAlgorithm.cs
This is a special case of the Edge algorithm mentioned above.
6. Usage of the MVVM Paradigm
The initial version of this application was based on WinForms, where the algorithmic and presentation aspects were very tightly coupled; this was never published. We decided to convert this to WPF, and redesign to MVVM. Among other things, this enables reuse of the Algorithm part.
The three components have the following responsibilities:
- View: The UI appearance without application logic. For example:
CoolImageEffects.ImageProcessingView.
- ViewModel: Preparation of the data from the model for viewing, and linking workflow actions (logic) to the UI. For example
CoolImageEffects.ImageProcessingViewModel
.
- Model: Gateway to the domain entities and logic. For example ImageSource, Effects and Algorithm options.
With this MVVM paradigm, we decouple Presentation Logic from the Algorithm part. There are two projects in our application:
- Algorithm : Contains the domain entities and algorithm processing logic. This creates a DLL.
- CoolImageEffects: Contains view and view-model components. This is the executable part.
All these are encapsulated in a Visual Studio 2010 solution.
7. More on the Code
As mentioned earlier, the code is organized into two projects.
7a. Algorithm Project
This is the core of the application. The following is the organization of files within this project.
The most important set of files here are:
ImageData.cs
: which stores the image attributes (width, height), and the image buffers (R, G, B pixel values). Additionally, it has methods common to all images for loading, saving, setting background colour, transforming to grayscale, etc.
AlgorithmBase.cs
: which serves as the abstract base class for all algorithms. It declares the method ApplyEffect, which is implemented in its child classes.
- Algorithm implementations, which implement the actual cool effect algorithms.
7b. CoolImageEffects Project
This is the application part. The organization of files here is:
Important folders here are:
- Converter: where the various conversions are done.
- View: which houses the views appropriate to the different cool effects.
- ViewModel: where the view models are housed.
As the focus here is more on the algorithmic part, we don't describe this in a greater detail.
7c. Including your own effect
You may come up with a nice and cool effect, and may want to try it out. This application provides that facilty for you, provided you are good at C# programming. The steps to do so are:
- Add your class to Algorithm project. Derive it from
AlgorithmBase
. If you look at this class, you'll find two set of variables: Pixels8nnnCurrent
and Pixels8nnnResult
, where nnn is one of Red, Green or Blue. The ...Current
variables are pixel buffers from the input image, whereas the ...Result
variables are pixel buffers from the output image. These are linear buffers.
- Implement the methods
ApplyEffect
, GetOptions
and GetDisplayInfo
within that class. Study the code of any algorithm, implement accordingly.
- Add the name of your algorithm to the
Effects
enum.
- Instantiate your algorithm and add to dictionary
availableAlgorithms
in ImageProcessingAlgorithm
class.
Since we've provided the platform for you to experiment your cool effect, we'd appreciate if you can also make available your nice cool effect to the community at large.
7d. Your Downloads
- Source code as a zip file.
- Executable as a zip file.
- PDF documentation as a zip file.
8. Points of Interest
- If you've glanced at the PDF document in the downloads, you'll notice two types of effects - Scalable and Non-scalable. Scalable means that there is no dependence on image size - two images with the same 'picture', with different sizes yield identical results. This is not so with the non-scalable effects; the user may see an effect on the screen, and when saving on the original image, may visually notice an entirely different effect. To remedy this to some extent, we provide two saving options - saving the original image, and saving the scaled image. These are available via context menu on the right hand image pane on the screen.
- "This application has no bugs" - we believe we've matured enough not to make such a claim. Though we've tested using different image types and sizes, there's a non-zero possibility that your image and user interaction may take the program flow through untested territory. Do send us feedback on this and we'll rectify.
- "This is the most optimal implementation of MVVM" - this is yet another claim we'd not like to make. Yes - this is a solution which works, is reasonably optimal, and creates near real-time effects for the user. Further, some parts of the code are intentionally kept sub-optimal for easy understanding by beginners. For example, the Oil Paint algorithm has two separate methods for 3 x 3 and 5 x 5, apparently duplicating code; though these can be unified, a beginner may find it difficult to understand. For us, understandabilty of the code, especially the algorithm part is important. However, let this not deter you from providing inputs on the code.
9. Closure
We find these effects to be cool, and have therefore named the application accordingly. We believe that you'll also find them cool enough. We hope you'll enjoy this application, and benefit from its effects. Show this to children at home, make them play with it, and let us know their feedback.
Among all these effects we've encapsulated in this small application, which do you like the most? What other parameters could be added to that effect, to make it more appealing to you? What other effects would you like to get added? Please write on these aspects too.
As mentioned earlier, the algorithms for these effects, are not new. However, the packaging is new. And the encapsulation of these effects into a self-contained code base, hopefully not too difficult to follow for a beginner, is perhaps new. The code base will also allow you to add new cool effects without too much of a hassle. If you feel happy and satisfied after viewing and saving your images, do let us know. Even otherwise, we appreciate your feedback.
10. Thanks
This application was developed during spare time, while we are employed at Philips India; with the intention of learning while building a simple image processing application. We express our sincere thanks to the Director of our Department and to Philips Intellectual Property and Standards Department for allowing us to publish this article online.
History
- Version 1.0: 29 May 2015.