Thursday 11 December 2014

Programming - Elite Dangerous Tools - Greyscale (with Full Code)

Update: You can now support this project at Patreon!

One of the problems in our tools to capture market data and read the data optically is to take screenshots from the screen, or be provided screenshots by the client itself, load them and convert them to greyscale.

Now, my preferred method of converting to greyscale is going to use CImg to load the image into memory.

I always load as "unsigned characters" (raw bytes) of data, and I'm going to assume we've got the file on disk as a bitmap - just to save us the complexity of Jpeg or PNG loading for now - so on disk we have an image which is a colour Bitmap.

To load this into CImg we want to create a "cimg_library::CImg<unsigned char>" instance, so the first thing I'm going to do is typedef this as our "Bitmap" type:

typedef cimg_library::CImg<unsigned char> Bitmap;

With this type we then need a helper function, which gives us a loaded image:

Bitmap* LoadImage(const std::string& p_Filename)
{
return new Bitmap(p_Filename.c_str());
}

Of course there are already classes called "Bitmap" in the C++ space, so for safety I'll wrap everything in the final code in a namespace of "Xelous", watch out for that later!

Now, with our image in hand we need to understand how CImg lets us parse over the individual pixels, well the CImg class provides us with a function called "data" which takes the x, y and z of the pixel (z being the layer, but I always use 0) and then the channel as an integer.

Our channels are:

enum ColourChannels
{
Red,
Green,
Blue
};

So red is zero, green is one and blue is two.  Taking a look at code to just output the RGB of each pixel therefore looks something like this:

#include <iostream>
#include <string>
#include "CImg.h"

typedef cimg_library::CImg<unsigned char> Bitmap;

Bitmap* LoadImage(const std::string& p_Filename)
{
  return new Bitmap(p_Filename.c_str());
}

enum ColourChannels
{
  Red,
  Green,
  Blue
};

int main()
{
  Bitmap* image = LoadImage ("C:\\Images\\Image1.bmp");
  if (image != nullptr)
  {
    for (int y = 0; y < image->height(); ++y)
    {
      for (int x = 0; x < image->width(); ++x)
      {
        unsigned char l_r = *image->data(x, y, 0, ColourChannels::Red);
        unsigned char l_g = *image->data(x, y, 0, ColourChannels::Green);
        unsigned char l_b = *image->data(x, y, 0, ColourChannels::Blue);
      
        int l_redValue = static_cast<int>(l_r);
        int l_greenValue = static_cast<int>(l_g);
        int l_blueValue = static_cast<int>(l_b);
      
        std::cout << "Pos (" << x << ", " << y << ") =";
        std::cout << "[" << l_redValue << ", " << l_greenValue << ", " << l_blueValue << "]" << std::endl;
      }
    }
    delete image;
  }
}

The output from this program looks something like this (assuming you have "C:\Images\Image1.bmp" present)...


As you can see each pixel position is slowly listing out it's colours, this is a really slow boring program, but it opens up to us the inside of the bitmap, we could for example swap every pixel of one colour for another... and save the image again...

What does our original image look like?...


Lets just set every pixel to black, or zero...

int main()
{
  Bitmap* image = LoadImage("C:\\Images\\Image1.bmp");
  if (image != nullptr)
  {
    for (int y = 0; y < image->height(); ++y)
    {
      for (int x = 0; x < image->width(); ++x)
      {
        unsigned char l_r = *image->data(x, y, 0, ColourChannels::Red);
        unsigned char l_g = *image->data(x, y, 0, ColourChannels::Green);
        unsigned char l_b = *image->data(x, y, 0, ColourChannels::Blue);

        int l_redValue = static_cast<int>(l_r);
        int l_greenValue = static_cast<int>(l_g);
        int l_blueValue = static_cast<int>(l_b);

        int l_n = 0; // Pick a colour

        *image->data(x, y, 0, ColourChannels::Red) = static_cast<unsigned char>(l_n);
        *image->data(x, y, 0, ColourChannels::Green) = static_cast<unsigned char>(l_n);
        *image->data(x, y, 0, ColourChannels::Blue) = static_cast<unsigned char>(l_n);
      }
    }
    image->save("C:\\Images\\Image2.bmp");

    delete image;
  }
}

The new parts here are of course the RGB being set back into the pixel and the image then being saved.

The output is a very boring looking image...


So now what kind of things can we do to the value of "int l_n" here to give us grey scale?

Well, there are a few different algorithms and other bloggers do a better job than I will at explaining them...


The simplest is to add the red, green and blue values together then divide them by three to give us the average colour channel value, assigning this back to all three colour channels we create a shade of grey...


This code however looks like this:

int main()
{
  Bitmap* image = LoadImage("C:\\Images\\Image1.bmp");
  if (image != nullptr)
  {
    for (int y = 0; y < image->height(); ++y)
    {
      for (int x = 0; x < image->width(); ++x)
      {
        unsigned char l_r = *image->data(x, y, 0, ColourChannels::Red);
        unsigned char l_g = *image->data(x, y, 0, ColourChannels::Green);
        unsigned char l_b = *image->data(x, y, 0, ColourChannels::Blue);

        int l_redValue = static_cast<int>(l_r);
        int l_greenValue = static_cast<int>(l_g);
        int l_blueValue = static_cast<int>(l_b);

        // Average
        int l_Sum = l_redValue + l_greenValue + l_blueValue;
        
        double l_Average = l_Sum / 3.0;

        int l_n = static_cast<int>(std::round(l_Average));

        *image->data(x, y, 0, ColourChannels::Red) = static_cast<unsigned char>(l_n);
        *image->data(x, y, 0, ColourChannels::Green) = static_cast<unsigned char>(l_n);
        *image->data(x, y, 0, ColourChannels::Blue) = static_cast<unsigned char>(l_n);
      }
    }
    image->save("C:\\Images\\Image2.bmp");

    delete image;
  }
}

This is the average algorithm... And we could have placed this code into a function to use, taking in the three unsigned chars and giving us whatever value... Implementing functions for the other greyscale algorithms as we go along... My implementation goes like this, adding the algorithms as a selection:

///<Summary>
/// The types of Greyscaling algorithms
///</Summary>
enum GreyscaleAlgorithms
{
  Average,
  Lightness,
  Luminosity
};
Our function can then look like this:

void ConvertToGreyscale(Bitmap* p_InputImage,
  const GreyscaleAlgorithms& p_Algorithm);
  
And our changes inside relate to our calculating the new pixel value before we assign it back to all three channels:

double l_a = 0;
switch (p_Algorithm)
{
  case GreyscaleAlgorithms::Average:
    l_a = DoubleToInteger(Average(l_r, l_g, l_b));
    break;

  case GreyscaleAlgorithms::Lightness:
    l_a = Lightness(l_r, l_g, l_b);
    break;

  case GreyscaleAlgorithms::Luminosity:
    l_a = Luminosity(l_r, l_g, l_b);
    break;
}

int l_n = DoubleToInteger(l_a);

///<Summary>
/// Average Grey
///</Summary>
double GreyscaleHelper::Average(const unsigned char& p_red,
  const unsigned char& p_green,
  const unsigned char& p_blue)
{
  int l_t = static_cast<int>(p_red)+
            static_cast<int>(p_green)+
            static_cast<int>(p_blue);
  return l_t / 3.0f;
}

///<Summary>
/// Lightness grey
///</Summary>
double GreyscaleHelper::Lightness(const unsigned char& p_red,
  const unsigned char& p_green,
  const unsigned char& p_blue)
{
  int l_max = std::max(
      static_cast<int>(p_red),
        std::max(
          static_cast<int>(p_green),
          static_cast<int>(p_blue)));
  int l_min = std::min(
      static_cast<int>(p_red),
        std::min(
          static_cast<int>(p_green),
          static_cast<int>(p_blue)));
  return (l_max + l_min) / 2.0f;
}

///<Summary>
/// Luminosity grey
///</Summary>
double GreyscaleHelper::Luminosity(const unsigned char& p_red,
  const unsigned char& p_green,
  const unsigned char& p_blue)
{
  double l_red = 0.21 * static_cast<int>(p_red);
  double l_green = 0.72 * static_cast<int>(p_green);
  double l_blue = 0.07 * static_cast<int>(p_blue);
  return (l_red + l_green + l_blue);
}

///<Summary>
/// Double rounded to integer
///</Summary>
int GreyscaleHelper::DoubleToInteger(const double& p_Value)
{
  return static_cast<int>(std::round(p_Value));
}

This probably looks like a lot of gibberish, but no it really is Greyscale code... On this page... Is the complete source code for this portion of my work so far, but here are some examples from my original image...

Note: I can't remember where I got the original image - the internets obviously - so if this lovely bowl of fruit is yours, let me know... I'll give credit.

Average

Lightness

Luminosity

No comments:

Post a Comment