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

Some Cool Image Effects

0.00/5 (No votes)
29 May 2015 1  
An application to create interesting effects for your favorite pictures.

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:
    1. Sepia: Your photo gradually fades over the years.
    2. Pixelate: Someone just averages out blocks of the picture.
    3. Quantize: Fifty shades of grey just merge into one.
    4. Wave: A sine wave just passes by.
    5. Solarize: Someone just mixes up the colours.
    6. Parabolize: Yet another colour mix up.
    7. X-ray: Wilhelm Roentgen stops by, and says 'Hello, how do you do?'.
    8. Canvas: A weaver weaves your image on canvas.
    9. Dither: A drop of ink ponders - 'Should I deposit, or not?'.
    10. Edge: Everything between the edges just gets washed out.
    11. Emboss: A bas-relief artist sees your picture.
    12. Glass: Someone sends you a ground glass sheet.
    13. Oil Paint: Not Picasso, or van Gogh, though.
    14. 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:

Cool Image Effects

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:

  1. Open an image by clicking on the Open Image button. A default image is available for you to play with.
  2. Select any effect using the effect selection combo shown below.

    Cool Image Effects

  3. 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.

    Cool Image Effects

  4. Save the resulting image by right-clicking on the right pane.

    Cool Image Effects

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.

Kempegowda

Now, to each of these effects:

5a. Sepia

Code: SepiaAlgorithm.cs. The code for computing the sepia tone is:

    // Target image
    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];
            // Grayscale
            intensity = 0.3 * r + 0.6 * g + 0.1 * b;

            // Red
            if (intensity > threshold)
                tone = 255.0;
            else
                tone = intensity + 255.0 - threshold;
            dRed = tone;

            // Similar code for green and blue components

            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:

KempegowdaKempegowda

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).

// Setting up the block widths and heights
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();

    // For the horizontal blocks
    var horizList = Enumerable.Range(0, noHorizBlocks - 1).ToList();
    // Shuffle the list.
    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]];
        }
    }

    // Similar code for the vertical block size computation
}

This is followed by averaging the colour over a block. 

For two different parameter settings, we get these images:

KempegowdaKempegowda

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);
        
        // Similar code for the green and blue components
    }
}

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.

 

KempegowdaKempegowda

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));
        } 

        // Source pixel
        w1 = CurrentWidth * y + x;
        r = Pixels8RedCurrent[w1];

        // Target pixel
        w1 = CurrentWidth * el + k;
        Pixels8RedResult[w1] = r;
 
        // Similar code for the green and blue components, and for the other cases
    }
}

KempegowdaKempegowda

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;
        // Similar code for green and blue components
    }
}

This involves some kind of inversion on the pixel values, depending on the user-specified threshold.

KempegowdaKempegowda

5f. Parabolize

Code: ParabolaAlgorithm.cs

Code is similar to solarize, but with a different formula. Refer to the PDF notes.

KempegowdaKempegowda

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) {
        // Source Pixel
        index = w2 + k;
        r = Pixels8RedCurrent[index];
        g = Pixels8GreenCurrent[index];
        b = Pixels8BlueCurrent[index];
        dGray = 0.3 * r + 0.6 * g + 0.1 * b;
        
        // Code for clamping the pixel values ...
        // Compute the negative grayscale value
        dGray = 255 + factor - dGray;
        // factor is a magic number

        byteGray = (byte)dGray;
        SetBackgroundColour(colour, index, byteGray);
    }
}

KempegowdaKempegowda

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));
}
// Similar code for green and blue components

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.

KempegowdaKempegowda

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.

KempegowdaKempegowda

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);

KempegowdaKempegowda

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);

KempegowdaKempegowda

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.

KempegowdaKempegowda

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.

KempegowdaKempegowda

5n. Outline

Code: EdgeAlgorithm.cs

This is a special case of the Edge algorithm mentioned above.

KempegowdaKempegowda

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:

  1. View: The UI appearance without application logic. For example: CoolImageEffects.ImageProcessingView.
  2. ViewModel: Preparation of the data from the model for viewing, and linking workflow actions (logic) to the UI. For example CoolImageEffects.ImageProcessingViewModel.
  3. 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:

  1. Algorithm : Contains the domain entities and algorithm processing logic. This creates a DLL.
  2. 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.

Cool Image Effects

The most important set of files here are:

  1. 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.
  2. AlgorithmBase.cs: which serves as the abstract base class for all algorithms. It declares the method ApplyEffect, which is implemented in its child classes.
  3. Algorithm implementations, which implement the actual cool effect algorithms.

7b. CoolImageEffects Project

This is the application part. The organization of files here is:

Cool Image Effects

Important folders here are:

  1. Converter: where the various conversions are done.
  2. View: which houses the views appropriate to the different cool effects.
  3. 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:

  1. 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.
  2. Implement the methods ApplyEffect, GetOptions and GetDisplayInfo within that class. Study the code of any algorithm, implement accordingly.
  3. Add the name of your algorithm to the Effects enum.
  4. 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

  1. Source code as a zip file.
  2. Executable as a zip file.
  3. PDF documentation as a zip file.

8. Points of Interest

  1. 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.
  2. "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.
  3. "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.

License

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

A list of licenses authors might use can be found here