Gigi Labs

Please follow Gigi Labs for the latest articles.
Showing posts with label sdl_image. Show all posts
Showing posts with label sdl_image. Show all posts

Saturday, February 22, 2014

SDL2: Converting an Image to Grayscale

Hello folks! :)

[Update 2015-11-14: This article is out of date. Check out the latest version at Gigi Labs.]

In the previous article, "SDL2: Pixel Drawing", we saw how to draw pixels onto a blank texture that we created in code. Today, on the other hand, we'll see how we can manipulate pixels on an existing image, such as a photo we loaded from disk. We'll also learn how to manipulate individual bits in an integer using what are called bitwise operators, and ultimately we'll convert an image to grayscale.

We first need to set up an SDL2 project. After following the steps in "SDL2: Setting up SDL2 in Visual Studio (2013 or any other)", you will also need to add SDL2_image.lib to your Linker Input (so that it reads "SDL2.lib; SDL2main.lib; SDL2_image.lib", and place the Visual C++ development libraries obtained from the SDL_image 2.0 homepage into the appropriate folders of your SDL2 directory. After you build your project the first time, make sure you also place all the necessary DLLs (including SDL2.dll, SDL2_image.dll and all the rest) into the same folder as your executable - see "SDL2: Loading Images with SDL_image" in case you need a refresher.

And in fact we're going to start with the code from "SDL2: Loading Images with SDL_image", which is the following (adapted a little bit):

#include <SDL.h>
#include <SDL_image.h>

int main(int argc, char ** argv)
{
    bool quit = false;
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);
    IMG_Init(IMG_INIT_JPG);

    SDL_Window * window = SDL_CreateWindow("SDL2 Grayscale",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);
    SDL_Surface * image = IMG_Load("PICT3159.JPG");
    SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer,
        image);

    while (!quit)
    {
        SDL_WaitEvent(&event);

        switch (event.type)
        {
        case SDL_QUIT:
            quit = true;
            break;
        }

        SDL_RenderCopy(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);
    }

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(image);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    IMG_Quit();
    SDL_Quit();

    return 0;
}

We can also use the same sample image as in that project, so make sure it's in the appropriate folders where Visual Studio can find it.

Now, let's get down to business.

You see, the problem here is that we can't quite touch the texture pixels directly. So instead, we need to work a little bit similarly to "SDL2: Pixel Drawing": we create our own texture, and then copy the surface pixels over to it. So we throw out the line calling SDL_CreateTextureFromSurface(), and replace it with the following:

    SDL_Texture * texture = SDL_CreateTexture(renderer,
        SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STATIC,
        image->w, image->h);

Then, at the beginning of the while loop, add this:

        SDL_UpdateTexture(texture, NULL, image->pixels,
            image->w * sizeof(Uint32));

If you try and run program now, it will pretty much explode. That's because our code is assuming that our image uses 4 bytes per pixel (ARGB - see "SDL2: Pixel Drawing"). That's something that depends on the image, and this particular JPG image is most likely 3 bytes per pixel. I don't know much about the JPG format, but I'm certain that it doesn't support transparency, so the alpha channel is out.

The good news is that it's possible to convert the surface into one that has a familiar pixel format. To do this, we use SDL_ConvertSurfaceFormat(). Add the following before the while loop:

    image = SDL_ConvertSurfaceFormat(image, SDL_PIXELFORMAT_ARGB8888, 0);

What this does is take our surface (in this case the one that image points to) and return an equivalent surface with the pixel format we specify. Now that the new image has the familiar ARGB format, we can easily access and manipulate the pixels. Add the following after the line you just added (before the while loop) to typecast the surface pixels from void * to Uint32 * which we can work with:

    Uint32 * pixels = (Uint32 *)image->pixels;

So far so good:


Now, let's add some code do our grayscale conversion. We're going to convert the image to grayscale when the user presses the 'G' key, so let us first add some code within the switch statement to handle that:

        case SDL_KEYDOWN:
            switch (event.key.keysym.sym)
            {
            case SDLK_g:
                for (int y = 0; y < image->h; y++)
                {
                    for (int x = 0; x < image->w; x++)
                    {
                        Uint32 pixel = pixels[y * image->w + x];
                        // TODO convert pixel to grayscale here
                    }
                }
                break;
            }
            break;

This is where bit manipulation comes in. You see, each pixel is a 32-bit integer which in concept looks something like this (actual values are invented, just for illustration):

Alpha Red Green Blue
11111111 10110101 10101000 01101111

So let's say we want to extract the Red component. Its value is 10110101 in binary, or 181 in decimal. But since it's in the third byte from right, its value is much greater than that. So we first shift the bits to the right by 16 spaces to move it to the first byte from right:



Alpha Red
00000000 00000000 11111111 10110101

...but we still can't interpret the integer as just red, since the alpha value is still there. We want to extract just that last byte. To do that, we perform a bitwise AND operation:

Pixel 11111111 10110101
Mask 00000000 11111111
Red AND Mask 00000000 10110101

We do an AND operation between our pixel value and a value where only the last byte worth of bits are set to 1. That allows us to extract our red value.

In code, this is how it works:

                        Uint8 r = pixel >> 16 & 0xFF;
                        Uint8 g = pixel >> 8 & 0xFF;
                        Uint8 b = pixel & 0xFF;

The >> operator shifts bits to the right, and the & is a bitwise AND operator. Each colour byte is shifted to the last byte and then ANDed with the value 0xFF, which is hexadecimal notation for what would be 255 in decimal, or 11111111 in binary. That way, we can extract all three colours individually.

We can finally perform the actual grayscaling operation. A simple way to do this might be to average the three colours and set each component to that average:

                        Uint8 v = (r + g + b) / 3;

Then, we pack the individual colour bytes back into a 32-bit integer. We follow the opposite method that we used to extract them in the first place: they are each already at the last byte, so all we need to do is left-shift them into position. Once that is done, we replace the actual pixel in the surface with the grayscaled one:

                        pixel = (0xFF << 24) | (v << 16) | (v << 8) | v;
                        pixels[y * image->w + x] = pixel;

If we now run the program and press the 'G' key, this is what we get:


It looks right, doesn't it? Well, it's not. There's an actual formula for calculating the correct grayscale value (v in our code), which according to Real-Time Rendering is:

Y = 0.212671R + 0.715160G + 0.072169B

The origin of this formula is beyond the scope of this article, but it's due to the fact that humans are sensitive to different colours in different ways - in fact there is a particular affinity to green, hence why it is allocated the greatest portion of the pixel colour. So now all we have to do is replace the declaration of v with the following:

                        Uint8 v = 0.212671f * r
                            + 0.715160f * g
                            + 0.072169f * b;

And with this, the image appears somewhat different:


This approach gives us a more even distribution of grey shades - in particular certain areas such as the trees are much lighter and we can make out the details more easily.

That's all, folks! :) In this article, we learned how to convert an image to grayscale by working on each individual pixel. To do this, we had to resort to converting an image surface to a pixel format we could work with, and then copy the pixels over to a texture for display in the window. To actually perform the grayscale conversion, we learned about bitwise operators which assisted us in dealing with the individual colours. Finally, although averaging the colour channels gives us something in terms of shades of grey, there is a formula that is used for proper grayscale conversion.

Thanks for reading. Come back for the next article! :)

Monday, November 25, 2013

SDL2: Loading Images with SDL_image

Welcome!

[Update 2015-11-14: This article is out of date. Check out the latestversion at Gigi Labs.]

In the previous article, "SDL2: Displaying an Image in the Window", we saw how we could load bitmaps using the SDL_LoadBMP() function. Unfortunately, working with bitmaps is very limiting, and the core SDL2 library does not provide the means to work with other image formats. However, such functionality is provided by an extension library called SDL_image. In this article, we will learn how to set up SDL_image and also how to use it to load other image formats.

Setting up SDL_image is not very different from setting up SDL 2.0 itself. You need to go to the SDL_image homepage and download the development libraries and runtime binaries:


Let's concentrate on those development libraries for the moment. Once you extract the contents of the zip file, you'll be able to find an include folder and a lib folder among other stuff. The include folder contains a single header file, SDL_image.h. This needs to go into your C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include\SDL2 folder (this is for Visual Studio 2010; you may need to change the v7.0A part depending on the version you're using), or wherever your other SDL2 header files are located, and drop the SDL_image.h file there.

As for the lib folder, whatever is in the x86 subfolder goes into the C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib folder, and whatever is in the x64 subfolder goes into C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib\x64.

With that done, let's set up a new project using Visual Studio 2010. Create an Empty Project from the Visual C++ category. Right click on the project and select Properties. In Configuration Properties -> Linker -> System, change the SubSystem to Windows (/SUBSYSTEM:WINDOWS). Then go to Input (also under Linker) and replace the Additional Dependencies with the following:

SDL2.lib;SDL2main.lib;SDL2_image.lib

You'll notice this is a little different from "SDL2: Setting up SDL2 in Visual Studio 2010" in that we've added the SDL2_image.lib. Since this is a new library, we need to link it into our program to use its functionality.

Now let's try some code. Add a file called main.cpp, and let's start off with the following code (it's the same as that used in "SDL2: Displaying an Image in the Window", without the error checking to keep it concise, and also filling the window):
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>

int main(int argc, char ** argv)
{
    bool quit = false;
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_Window * window = SDL_CreateWindow("SDL2 Displaying Image",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);
    SDL_Surface * image = SDL_LoadBMP("image.bmp");
    SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer,
        image);

    while (!quit)
    {
       SDL_WaitEvent(&event);

       switch(event.type)
       {
       case SDL_QUIT:
           quit = true;
           break;
       }

       SDL_RenderCopy(renderer, texture, NULL, NULL);
       SDL_RenderPresent(renderer);
    }

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(image);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

Now, we're going to load this nice photo (.jpg format) taken in Gardaland in 2006:


Doing this is quite simple. First, we initialise SDL_image by calling IMG_Init() right after the call to SDL_Init():
    IMG_Init(IMG_INIT_JPG);
...and before the call to SDL_Quit(), we shut down SDL_Image using IMG_Quit():

    IMG_Quit();
All we have left to do now is replace the line calling SDL_LoadBMP() with one that uses IMG_Load() instead:
    SDL_Surface * image = IMG_Load("PICT3159.JPG");
You should now be able to build this program (Ctrl+Shift+B). Before you run it, though, make sure that SDL2.dll is in the same folder as your executable. You'll also need to toss in all the DLLs from the runtime binaries package (most likely the x86 one is what you'll be using by default) - that includes SDL2_image.dll along with format-specific files such as libjpeg-9.dll, libpng16-16.dll, etc. If you're going to run from Visual Studio, make sure the image is in the same folder as your main.cpp file; otherwise if you're running straight from the executable, the image should be in the same folder with it.

And voilĂ :


Isn't that sweet? The SDL_image library allows you to load a large variety of image formats, by just replacing SDL_LoadBMP() with IMG_Load(). You'll need to initialise and cleanup the library (although it seems to work even without this) and remember to link the library and include the appropriate header file. But as you can see, it's pretty straightforward.