7
\$\begingroup\$

Last night I was messing with image programming in C# and I tried to make a program that takes an image and transforms it into another image, built with emojis.

EmojifyEngine.cs

/// <summary>
/// Holds all methods involved in transforming a image into a emojified version of the image.
/// </summary>
static class EmojifyEngine
{
    /// <summary>
    /// Channel to debug all the process.
    /// </summary>
    /// FOR CODE REVIEW: DO NOT WORRY ABOUT THIS, IT JUSTS PRINTS TEXT IN A PRETTIER WAY
    private static TextChannel EmojiChannel = new TextChannel("emojify", ConsoleColor.Blue);

    // IO stuff

    /// <summary>
    /// Folder that holds all emoji's bitmaps.
    /// </summary>
    private const string EMOJI_FOLDER = @"emojis_small\topados";
    /// <summary>
    /// The count of how many emojis are there in EMOJI_FOLDER.
    /// </summary>
    private const int EMOJI_COUNT = 844;

    // processing stuff

    /// <summary>
    /// The size of each emoji.
    /// Emojis are often called "chunks".
    /// </summary>
    private const int CHUNK_SIZE = 64; // 64 by 64 pixels

    /// <summary>
    /// Wether this class is processing a image or not.
    /// </summary>
    public static bool IsProcessing = false;

    // Data stuff

    /// <summary>
    /// The average color of each emoji.
    /// </summary>
    private static Color[] Emojis = new Color[EMOJI_COUNT];

    /// <summary>
    /// Returns true if the EmojifyEngine is initialized.
    /// </summary>
    private static bool Initialized = false;

    /// <summary>
    /// Initializes the EmojifyEngine.
    /// </summary>
    private static void Initialize()
    {
        if (Initialized) return;

        EmojiChannel.Write("Inicializando...");
        ProcessEmojis();

        Initialized = true;
    }

    /// <summary>
    /// Converts a given bitmap to it's emojified version.
    /// </summary>
    /// <param name="bmp">The bitmap to be converted.</param>
    /// <returns>The emojified bitmap.</returns>
    public static Bitmap ToEmoji(Bitmap bmp)
    {
        if (bmp == null)
            throw new ArgumentNullException("The param 'bmp' cannot be null.");

        if (!Initialized) // initialize if not initialized yet.
            Initialize();

        if(IsProcessing)
        {
            throw new Exception("EmojifyEngine is busy.");
        }

        IsProcessing = true;

        bmp = FixImage(bmp);

        // calculate the dimensions of each chunk.
        int chunkDimensions = (int)Math.Sqrt(CHUNK_SIZE);

        // calculate how many chunks will be in the image
        int chunkCount = (bmp.Size.Height * bmp.Size.Width) / CHUNK_SIZE;

        // save the chunks
        Color[] chunks = new Color[chunkCount];

        // save the chunks' locations
        Rectangle[] chunkLocations = new Rectangle[chunkCount];

        EmojiChannel.Write("Imagem carregada de tamanho: ({0}, {1})", bmp.Width, bmp.Height);
        EmojiChannel.Write("Cortando imagem em {0} pedaços de {1}.", chunkCount, CHUNK_SIZE);
        EmojiChannel.Write("Cada pedaço terá tamanho de {0} por {1} pixels.", chunkDimensions, chunkDimensions);

        System.Drawing.Imaging.PixelFormat format = System.Drawing.Imaging.PixelFormat.Format8bppIndexed;

        int counter = 0;

        for (int x = 0; x < bmp.Size.Width; x += chunkDimensions)
        {
            for (int y = 0; y < bmp.Size.Height; y += chunkDimensions)
            {
                Rectangle chunckRect = new Rectangle(x, y, chunkDimensions, chunkDimensions);

                Bitmap temp = bmp.Clone(chunckRect,
                    format);

                string emoji = GetEmoji(temp.AverageColor());

                chunks[counter] = temp.AverageColor();
                chunkLocations[counter] = chunckRect;

                counter++;
            }
        }

        EmojiChannel.Write("Processei {0} chunks.", chunkCount);

        Bitmap[] emojis = GetEmojis(chunks);
        Bitmap resultImage = MountImage(emojis, chunkLocations, bmp.Size, bmp);

        IsProcessing = false;
        return resultImage;
    }

    /// <summary>
    /// Loads and process all emojis.
    /// This method will calculate the average color of each emoji.
    /// </summary>
    private static void ProcessEmojis()
    {
        int counter = 0;

        foreach (string file in Directory.GetFiles(EMOJI_FOLDER))
        {
            Bitmap bmp = new Bitmap(file); // load file to bitmap
            Emojis[counter] = bmp.AverageColor();

            counter++;
        }

        EmojiChannel.Write("Processed {0} emojis.", counter);
    }

    /// <summary>
    /// Gets an emoji's file path based on a color.
    /// </summary>
    /// <param name="color">The color.</param>
    /// <returns>The nearest match file path.</returns>
    private static string GetEmoji(Color color)
    {
        ColorDifference.Method method = ColorDifference.Method.CIE76;

        int argb = color.ToArgb();

        double delta = ColorDifference.Calculate(method, argb, Emojis[0].ToArgb());
        int match = 41; // arbitrary value

        for (int i = 1; i < Emojis.Length; ++i)
        {
            double n_delta = Math.Abs(ColorDifference.Calculate(method, argb, Emojis[i].ToArgb()));

            if (n_delta < delta)
            {
                match = i;
                delta = n_delta;
            }
        }

        return Path.Combine(EMOJI_FOLDER, String.Format("{0}.png", match));
    }

    /// <summary>
    /// Gets all emojis as bitmaps chunks based on a color array.
    /// </summary>
    /// <param name="colors">The colors to get the bitmap from.</param>
    /// <returns>The chunks.</returns>
    private static Bitmap[] GetEmojis(Color[] colors)
    {
        Bitmap[] result = new Bitmap[colors.Length];

        for (int i = 0; i < colors.Length; ++i)
        {
            string emoji = GetEmoji(colors[i]);
            result[i] = (Bitmap)Image.FromFile(emoji);
        }

        return result;
    }

    /// <summary>
    /// Mounts the image.
    /// </summary>
    /// <param name="chunks">The emojis.</param>
    /// <param name="locations">The emojis location.</param>
    /// <param name="size">The result file size.</param>
    /// <param name="original">The original image.</param>
    /// <returns>The final image.</returns>
    private static Bitmap MountImage(Bitmap[] chunks, Rectangle[] locations, Size size, Bitmap original)
    {
        if (chunks.Length != locations.Length)
        {
            EmojiChannel.Write("O tamanho dos chunks não igual ao tamanho das localizações!");
            return null;
        }

        EmojiChannel.Write("Criando imagem com tamanho ({0}, {1}).", size.Width, size.Height);

        Bitmap result = new Bitmap(size.Width, size.Height);

        using (Graphics g = Graphics.FromImage(result))
        {
            for (int i = 0; i < chunks.Length; ++i)
            {
                g.DrawImage(chunks[i], locations[i]);
            }
        }

        // do final processing
        return result.Blend(original);
    }

    /// <summary>
    /// This scales the image to the nearest size possible.
    /// </summary>
    /// <param name="image">The image to be scaled.</param>
    /// <returns>The scaled image.</returns>
    private static Bitmap FixImage(Bitmap image)
    {
        if (image == null)
            throw new NullReferenceException("Imagem é nula!");

        //int dimensions = (int)Math.Sqrt(GetClosestSize(image.Size.Height * image.Size.Width));
        int width = GetClosestSize(image.Width);
        int heigth = GetClosestSize(image.Height);

        EmojiChannel.Write("Dimensions: ({0}, {1})", width, heigth);

        Bitmap fixedImage = image.resizeImage(heigth, width);

        return fixedImage;
    }

    /// <summary>
    /// Gets the closest multiple of 64 given an integer n.
    /// </summary>
    /// <param name="n">The integer.</param>
    /// <returns>The closest multiple.</returns>
    private static int GetClosestSize(int n)
    {
        int factor = 64;
        int nearestMultiple =
                (int)Math.Round(
                     (n / (double)factor),
                     MidpointRounding.AwayFromZero
                 ) * factor;

        return nearestMultiple;
    }
}

ImageUtils.cs

/// <summary>
/// Holds all common methods to process images.
/// </summary>
public static class ImageUtils
{
    /// <summary>
    /// Calculates the average color of an image.
    /// </summary>
    /// <param name="bmp">The image to be calculated.</param>
    /// <returns>The average color.</returns>
    public static Color AverageColor(this Bitmap bmp)
    {
        int arraySize = bmp.Height * bmp.Width;

        int[] redValues = new int[arraySize];
        int[] greenValues = new int[arraySize];
        int[] blueValues = new int[arraySize];

        int count = 0;

        for (int x = 0; x < bmp.Size.Width; ++x)
        {
            for (int y = 0; y < bmp.Size.Height; ++y)
            {
                redValues[count] = bmp.GetPixel(x, y).R;
                greenValues[count] = bmp.GetPixel(x, y).G;
                blueValues[count] = bmp.GetPixel(x, y).B;

                count++;
            }
        }

        int r = Average(redValues);
        int g = Average(greenValues);
        int b = Average(blueValues);

        return Color.FromArgb(r, g, b);
    }

    /// <summary>
    /// Calculates the average of an integer array.
    /// </summary>
    /// <param name="array">The array.</param>
    /// <returns>The average.</returns>
    public static int Average(int[] array)
    {
        int result = 0;

        foreach (int elem in array)
            result += elem;

        return result / array.Length;
    }

    public static Bitmap ScaleImage(this Bitmap image, int maxWidth, int maxHeight)
    {
        var ratioX = (double)maxWidth / image.Width;
        var ratioY = (double)maxHeight / image.Height;
        var ratio = Math.Min(ratioX, ratioY);

        var newWidth = (int)(image.Width * ratio);
        var newHeight = (int)(image.Height * ratio);

        var newImage = new Bitmap(newWidth, newHeight);

        using (var graphics = Graphics.FromImage(newImage))
            graphics.DrawImage(image, 0, 0, newWidth, newHeight);

        return newImage;
    }

    /// <summary>
    /// Resizes an image.
    /// Taken from StackOverflow.
    /// </summary>
    /// <param name="image">The image to be resized.</param>
    /// <param name="new_height">The new height.</param>
    /// <param name="new_width">The new width.</param>
    /// <returns></returns>
    public static Bitmap resizeImage(this Bitmap image, int new_height, int new_width)
    {
        Bitmap new_image = new Bitmap(new_width, new_height);
        Graphics g = Graphics.FromImage((Image)new_image);
        g.InterpolationMode = InterpolationMode.High;
        g.DrawImage(image, 0, 0, new_width, new_height);
        return new_image;
    }

    /// <summary>
    /// Blends two images together.
    /// Taken from StackOverflow.
    /// </summary>
    /// <param name="A">The first image.</param>
    /// <param name="B">The second image.</param>
    /// <returns>The blended image.</returns>
    public static Bitmap Blend(this Bitmap A, Bitmap B)
    {
        if (A == null || B == null)
            return null;

        if (A.Size != B.Size)
        {
            B = B.resizeImage(A.Height, A.Width);
        }

        Bitmap[] bmSrcs = new Bitmap[2];

        bmSrcs[0] = A;
        bmSrcs[1] = B;

        Bitmap bmDst = new Bitmap(bmSrcs[0].Width, bmSrcs[0].Height);
        for (int y = 0; y < bmSrcs[0].Height; y++)
        {
            for (int x = 0; x < bmSrcs[0].Width; x++)
            {
                int a = 0, r = 0, g = 0, b = 0, iCount = 0;
                foreach (Bitmap bmSrc in bmSrcs)
                {
                    Color colSrc = bmSrc.GetPixel(x, y);
                    // check alpha (transparency): ignore transparent pixels
                    if (colSrc.A > 0)
                    {
                        a += colSrc.A;
                        r = Math.Max(r, colSrc.R);
                        g = Math.Max(g, colSrc.G);
                        b = Math.Max(b, colSrc.B);
                        iCount++;
                    }
                }
                Color colDst = Color.FromArgb(iCount > 1 ? (int)Math.Round((double)a / iCount) : a, r, g, b);
                bmDst.SetPixel(x, y, colDst);
            }
        }

        return bmDst;
    }

    public static Bitmap ReplaceColor(this Bitmap image, Color oldColor, Color newColor)
    {
        Bitmap result = new Bitmap(image.Width, image.Height);

        for (int x = 0; x < result.Width; ++x)
            for (int y = 0; y < result.Height; ++y)
                result.SetPixel(x, y, result.GetPixel(x, y) == oldColor ? newColor : result.GetPixel(x, y));

        return result;
    }
}

ColorDifference.cs

/// <summary>
/// Precisely calculates the difference between two colors.
/// Taken from StackOverflow.
/// </summary>
static class ColorDifference
{
    public enum Method
    {
        Binary, // true or false, 0 is false
        Square,
        Dimensional,
        CIE76
    }

    public static double Calculate(Method method, int argb1, int argb2)
    {
        int[] c1 = ColorConversion.ArgbToArray(argb1);
        int[] c2 = ColorConversion.ArgbToArray(argb2);
        return Calculate(method, c1[1], c2[1], c1[2], c2[2], c1[3], c2[3], c1[0], c2[0]);
    }

    public static double Calculate(Method method, int r1, int r2, int g1, int g2, int b1, int b2, int a1 = -1, int a2 = -1)
    {
        switch (method)
        {
            case Method.Binary:
                return (r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2) ? 0 : 100;
            case Method.CIE76:
                return CalculateCIE76(r1, r2, g1, g2, b1, b2);
            case Method.Dimensional:
                if (a1 == -1 || a2 == -1) return Calculate3D(r1, r2, g1, g2, b1, b2);
                else return Calculate4D(r1, r2, g1, g2, b1, b2, a1, a2);
            case Method.Square:
                return CalculateSquare(r1, r2, g1, g2, b1, b2, a1, a2);
            default:
                throw new InvalidOperationException();
        }
    }

    public static double Calculate(Method method, Color c1, Color c2, bool alpha)
    {
        switch (method)
        {
            case Method.Binary:
                return (c1.R == c2.R && c1.G == c2.G && c1.B == c2.B && (!alpha || c1.A == c2.A)) ? 0 : 100;
            case Method.CIE76:
                if (alpha) throw new InvalidOperationException();
                return CalculateCIE76(c1, c2);
            case Method.Dimensional:
                if (alpha) return Calculate4D(c1, c2);
                else return Calculate3D(c1, c2);
            case Method.Square:
                if (alpha) return CalculateSquareAlpha(c1, c2);
                else return CalculateSquare(c1, c2);
            default:
                throw new InvalidOperationException();
        }
    }

    // A simple idea, based on on a Square
    public static double CalculateSquare(int argb1, int argb2)
    {
        int[] c1 = ColorConversion.ArgbToArray(argb1);
        int[] c2 = ColorConversion.ArgbToArray(argb2);
        return CalculateSquare(c1[1], c2[1], c1[2], c2[2], c1[3], c2[3]);
    }

    public static double CalculateSquare(Color c1, Color c2)
    {
        return CalculateSquare(c1.R, c2.R, c1.G, c2.G, c1.B, c2.B);
    }

    public static double CalculateSquareAlpha(int argb1, int argb2)
    {
        int[] c1 = ColorConversion.ArgbToArray(argb1);
        int[] c2 = ColorConversion.ArgbToArray(argb2);
        return CalculateSquare(c1[1], c2[1], c1[2], c2[2], c1[3], c2[3], c1[0], c2[0]);
    }

    public static double CalculateSquareAlpha(Color c1, Color c2)
    {
        return CalculateSquare(c1.R, c2.R, c1.G, c2.G, c1.B, c2.B, c1.A, c2.A);
    }

    public static double CalculateSquare(int r1, int r2, int g1, int g2, int b1, int b2, int a1 = -1, int a2 = -1)
    {
        if (a1 == -1 || a2 == -1) return (Math.Abs(r1 - r2) + Math.Abs(g1 - g2) + Math.Abs(b1 - b2)) / 7.65;
        else return (Math.Abs(r1 - r2) + Math.Abs(g1 - g2) + Math.Abs(b1 - b2) + Math.Abs(a1 - a2)) / 10.2;
    }

    // from:http://stackoverflow.com/questions/9018016/how-to-compare-two-colors
    public static double Calculate3D(int argb1, int argb2)
    {
        int[] c1 = ColorConversion.ArgbToArray(argb1);
        int[] c2 = ColorConversion.ArgbToArray(argb2);
        return Calculate3D(c1[1], c2[1], c1[2], c2[2], c1[3], c2[3]);
    }

    public static double Calculate3D(Color c1, Color c2)
    {
        return Calculate3D(c1.R, c2.R, c1.G, c2.G, c1.B, c2.B);
    }

    public static double Calculate3D(int r1, int r2, int g1, int g2, int b1, int b2)
    {
        return Math.Sqrt(Math.Pow(Math.Abs(r1 - r2), 2) + Math.Pow(Math.Abs(g1 - g2), 2) + Math.Pow(Math.Abs(b1 - b2), 2)) / 4.41672955930063709849498817084;
    }

    // Same as above, but made 4D to include alpha channel
    public static double Calculate4D(int argb1, int argb2)
    {
        int[] c1 = ColorConversion.ArgbToArray(argb1);
        int[] c2 = ColorConversion.ArgbToArray(argb2);
        return Calculate4D(c1[1], c2[1], c1[2], c2[2], c1[3], c2[3], c1[0], c2[0]);
    }

    public static double Calculate4D(Color c1, Color c2)
    {
        return Calculate4D(c1.R, c2.R, c1.G, c2.G, c1.B, c2.B, c1.A, c2.A);
    }

    public static double Calculate4D(int r1, int r2, int g1, int g2, int b1, int b2, int a1, int a2)
    {
        return Math.Sqrt(Math.Pow(Math.Abs(r1 - r2), 2) + Math.Pow(Math.Abs(g1 - g2), 2) + Math.Pow(Math.Abs(b1 - b2), 2) + Math.Pow(Math.Abs(a1 - a2), 2)) / 5.1;
    }

    /**
    * Computes the difference between two RGB colors by converting them to the L*a*b scale and
    * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
    */
    public static double CalculateCIE76(int argb1, int argb2)
    {
        return CalculateCIE76(Color.FromArgb(argb1), Color.FromArgb(argb2));
    }

    public static double CalculateCIE76(Color c1, Color c2)
    {
        return CalculateCIE76(c1.R, c2.R, c1.G, c2.G, c1.B, c2.B);
    }

    public static double CalculateCIE76(int r1, int r2, int g1, int g2, int b1, int b2)
    {
        int[] lab1 = ColorConversion.ColorToLab(r1, g1, b1);
        int[] lab2 = ColorConversion.ColorToLab(r2, g2, b2);
        return Math.Sqrt(Math.Pow(lab2[0] - lab1[0], 2) + Math.Pow(lab2[1] - lab1[1], 2) + Math.Pow(lab2[2] - lab1[2], 2)) / 2.55;
    }
}


internal static class ColorConversion
{

    public static int[] ArgbToArray(int argb)
    {
        return new int[] { (argb >> 24), (argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF };
    }

    public static int[] ColorToLab(int R, int G, int B)
    {
        // http://www.brucelindbloom.com

        double r, g, b, X, Y, Z, fx, fy, fz, xr, yr, zr;
        double Ls, fas, fbs;
        double eps = 216.0f / 24389.0f;
        double k = 24389.0f / 27.0f;

        double Xr = 0.964221f;  // reference white D50
        double Yr = 1.0f;
        double Zr = 0.825211f;

        // RGB to XYZ
        r = R / 255.0f; //R 0..1
        g = G / 255.0f; //G 0..1
        b = B / 255.0f; //B 0..1

        // assuming sRGB (D65)
        if (r <= 0.04045) r = r / 12;
        else r = (float)Math.Pow((r + 0.055) / 1.055, 2.4);

        if (g <= 0.04045) g = g / 12;
        else g = (float)Math.Pow((g + 0.055) / 1.055, 2.4);

        if (b <= 0.04045) b = b / 12;
        else b = (float)Math.Pow((b + 0.055) / 1.055, 2.4);

        X = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b;
        Y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b;
        Z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b;

        // XYZ to Lab
        xr = X / Xr;
        yr = Y / Yr;
        zr = Z / Zr;

        if (xr > eps) fx = (float)Math.Pow(xr, 1 / 3.0);
        else fx = (float)((k * xr + 16.0) / 116.0);

        if (yr > eps) fy = (float)Math.Pow(yr, 1 / 3.0);
        else fy = (float)((k * yr + 16.0) / 116.0);

        if (zr > eps) fz = (float)Math.Pow(zr, 1 / 3.0);
        else fz = (float)((k * zr + 16.0) / 116);

        Ls = (116 * fy) - 16;
        fas = 500 * (fx - fy);
        fbs = 200 * (fy - fz);

        int[] lab = new int[3];
        lab[0] = (int)(2.55 * Ls + 0.5);
        lab[1] = (int)(fas + 0.5);
        lab[2] = (int)(fbs + 0.5);
        return lab;
    }

    private static double Difference(Color A, Color B)
    {
        long rmean = ((long)A.R + (long)B.R) / 2;

        long r = (long)A.R - (long)B.R;
        long g = (long)A.G - (long)B.G;
        long b = (long)A.B - (long)B.B;

        return Math.Sqrt((((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8));
    }
}

Test Cases

Original:

enter image description here

Blended:

enter image description here

Unblended:

enter image description here

How can I improve this program?

\$\endgroup\$
1
\$\begingroup\$

Initialize Vs static constructor

EmojifyEngine is a static class, rather than providing an Initialize method, you would be better off providing a static constructor that performs the class initialisation. This would make construction more straightforward and remove the need for the Initialized variable.

There can be only one

On the face of it, there doesn't seem to be any particular reason for the restriction on only being able to perform one ToEmoji conversion at a time. Unless I've missed something, once the class has been constructed, you only need read access to any of the variables stored at class level (All write actions are performed against local variables). I'd consider removing the IsProcessing variable and associated checks.

Magic Numbers

There are a lot of magic numbers in the ColorDifference class, particularly in the ColorToLab method. I'd consider if some of these might be more expressive as constants.

Color Matching

Overall, I think the conversion is quite good and has a powerful effect. However, looking at the unblended image, colour wise it isn't what I would be expecting. The original smile is mostly yellow. Although the match appears to have picked an yellow emoji like a lemon in the outer circle, this isn't the emoji that has been chosen for the face, instead a grayish bunny? has been chosen. Similarly, the eyes are brown, but colored emojis have been picked, although the brown one that is used for the smiles outline might have been more appropriate. From what I can tell, you're picking the emoji based around the average color of all the pixels in the block, versus the average color of the emoji. CIE76 has been superseded, so it may be that updating the color difference formula to use a more up to date method might help. Alternately, using a different method to determine the average (for example using the median, rather than the mean pixel colour) may result in a better colour match.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer. I had no idea about the static constructor thing, I will certainly use it! Using the median resulted in a terrible result, even worse than the current one. I sure will try to update the color matching method. \$\endgroup\$ – Guilherme Almeida Sep 14 '16 at 23:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.