/// *********************************************************************************************************
///  © 2014 www.jakemdrew.com All rights reserved. 
///  This source code is licensed under The GNU General Public License (GPLv3):  
///  http://opensource.org/licenses/gpl-3.0.html
/// *********************************************************************************************************

/// *********************************************************************************************************
/// RgbProjector - Luminosity Histograms for Image Similarity.
///
/// The code in the this file was entirely adapted from the EyeOpen.SimilarImagesFinder project:
///    https://similarimagesfinder.codeplex.com/
///
/// Adapted By - Jake Drew 
/// Version -    1.0, 06/23/2014
/// *********************************************************************************************************

using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

/// 
/// ***The code in the this file was entirely adapted from the EyeOpen.SimilarImagesFinder project:
///    https://similarimagesfinder.codeplex.com/
/// 
/// The following classes: 
///     1. Extact / create RGB projections from Bitmap images.
///     2. Compare RGB projections for similarity.
/// 
namespace ImageClustering
{
    /// 
    /// Holds an RGB projection extracted from a Bitmap image.
    /// 
    public class RgbProjection
    {
        public double[] horizontalProjection;
        public double[] verticalProjection;

        public RgbProjection(double[] horizontal, double[] vertical)
        {
            horizontalProjection = horizontal;
            verticalProjection = vertical;
        }
    }

    /// 
    /// Creates RGB projections from an image and compares projections for similarity.
    /// 
    public class RgbProjector
    {
        /// 
        /// Extract the RBG projection from a Bitmap Image.
        /// 
        /// The image to process.
        /// Return horizontal RGB projection in value [0] and vertical RGB projection in value [1].
        public static RgbProjection GetRgbProjections(Bitmap bitmap)
        {
            var width = bitmap.Width - 1;
            var height = bitmap.Width - 1;

            var horizontalProjection = new double[width];
            var verticalProjection = new double[height];

            var bitmapData1 = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            unsafe
            {
                var imagePointer1 = (byte*)bitmapData1.Scan0;

                for (var y = 0; y < height; y++)
                {
                    for (var x = 0; x < width; x++)
                    {
                        var blu = imagePointer1[0];
                        var green = imagePointer1[1];
                        var red = imagePointer1[2];

                        int luminosity = (byte)(((0.2126 * red) + (0.7152 * green)) + (0.0722 * blu));

                        horizontalProjection[x] += luminosity;
                        verticalProjection[y] += luminosity;

                        imagePointer1 += 4;
                    }

                    imagePointer1 += bitmapData1.Stride - (bitmapData1.Width * 4);
                }
            }

            MaximizeScale(ref horizontalProjection, height);
            MaximizeScale(ref verticalProjection, width);

            bitmap.UnlockBits(bitmapData1);
            return new RgbProjection(horizontalProjection, verticalProjection);
        }

        /// 
        /// Optimize the range of values.
        /// 
        /// The array to process.
        /// The max value for the elements.
        private static void MaximizeScale(ref double[] projection, double max)
        {
            var minValue = double.MaxValue;
            var maxValue = double.MinValue;

            for (var i = 0; i < projection.Length; i++)
            {
                if (projection[i] > 0)
                {
                    projection[i] = projection[i] / max;
                }

                if (projection[i] < minValue)
                {
                    minValue = projection[i];
                }

                if (projection[i] > maxValue)
                {
                    maxValue = projection[i];
                }
            }

            if (maxValue == 0)
            {
                return;
            }

            for (var i = 0; i < projection.Length; i++)
            {
                if (maxValue == 255)
                {
                    projection[i] = 1;
                }
                else
                {
                    projection[i] = (projection[i] - minValue) / (maxValue - minValue);
                }
            }
        }

        /// 
        /// Calculate the similarity between two RGB projections, horizontal and vertical.
        /// 
        /// The RGB projection to compare with.
        /// Return the max similarity value betweem horizontal and vertical RGB projections.
        public static double CalculateSimilarity(RgbProjection source, RgbProjection compare)
        {
            var horizontalSimilarity = CalculateProjectionSimilarity(source.horizontalProjection, compare.horizontalProjection);
            var verticalSimilarity = CalculateProjectionSimilarity(source.verticalProjection, compare.verticalProjection);
            return (horizontalSimilarity + verticalSimilarity) / 2;
        }

        /// 
        /// Calculate the similarity to another RGB projection.
        /// 
        /// The source RGB projection.
        /// The RGB projection to compare with.
        /// Return a value from 0 to 1 that is the similarity.
        private static double CalculateProjectionSimilarity(double[] source, double[] compare)
        {
            if (source.Length != compare.Length)
            {
                throw new ArgumentException();
            }

            var frequencies = new Dictionary();

            ////Calculate frequencies
            for (var i = 0; i < source.Length; i++)
            {
                var difference = source[i] - compare[i];
                difference = Math.Round(difference, 2);
                difference = Math.Abs(difference);
                if (frequencies.ContainsKey(difference))
                {
                    frequencies[difference] = frequencies[difference] + 1;
                }
                else
                {
                    frequencies.Add(difference, 1);
                }
            }

            var deviation = frequencies.Sum(value => (value.Key * value.Value));

            ////Calculate "weighted mean"
            ////http://en.wikipedia.org/wiki/Weighted_mean
            deviation /= source.Length;

            ////Maximize scale
            deviation = (0.5 - deviation) * 2;

            return deviation;
        }

        /// 
        /// Resize an image in high resolution
        /// 
        /// The image to resize.
        /// The expected width.
        /// the expected height.
        /// 
        public static Bitmap ResizeBitmap(Bitmap bitmap, int width, int height)
        {
            var result = new Bitmap(width, height);
            using (var graphic = Graphics.FromImage((System.Drawing.Image)result))
            {
                graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
                graphic.DrawImage(bitmap, 0, 0, width - 1, height - 1);
            }

            return result;
        }
    }
}