/**
 * File: Histogram.java
 * Date: December 30, 2012
 * Author: Ferad Zyulkyarov (feradz@gmail.com)
 * 
 * This file implements a Histogram class which abstracts the histograms objects for
 * color and grayscale images. Instances of this class contain:
 * <ul>
 *   <li>
 *   	Histogram for a gray scale image which is obtained by converting a (color) 
 *      image into a gray scale image using the available filters in Java framework and
 *      then reading the values representing the gray pixels.
 *   </li>
 *   <li>
 *      Histogram for the combined red(R), green(G) and blue (B) pixel values which is
 *      obtained based on the following formula: 
 *      RGB_Combined = R*0.2126 + G*0.7152 + B*0.0722.
 *      This weighted formula represents the luminance intensity for a given pixel. 
 *      This is the CIE 1931 standard way for computing luminance for sRGB images.
 *   </li>
 *   <li>
 *      Histogram for the combined red(R), green(G) and blue (B) pixel values which is
 *      obtained based on the following formula: 
 *      RGB_Combined = R*0.299 + G*0.587 + B*0.114.
 *      This weighted formula represents the perceived luminance intensity for a given 
 *      pixel.
 *   </li>
 *   <li>
 *     Histogram for the red channel.
 *   </li>
 *   <li>
 *     Histogram for the green channel.
 *   </li>
 *   <li>
 *     Histogram for the blue channel.
 *   </li>
 * </ul>
 */
package com.feradz;

import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorConvertOp;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

/**
 * Abstracts an image histogram object. 
 *
 */
public class Histogram {
	
	/**
	 * The gray histogram obtained using the method {@link #parseGrayHistogram(BufferedImage)}.
	 * This histogram is calculated by converting the color image into a gray scale image
	 * using the filters coming with the Java framework. We do not know how exactly
	 * the conversion is done.
	 */
	private int[] _grayHistogram;
	
	/**
	 * The histogram for the red channel of a RGB color image.
	 */
	private int[] _redHistogram;
	
	/**
	 * The histogram for the green channel of a RGB color image.
	 */
	private int[] _greenHistogram;
	
	/**
	 * The histogram for the blue channel of a RGB color image. 
	 */
	private int[] _blueHistogram;
	
	/**
	 * The histogram combined for the red, green and blue colors. This histogram
	 * represents a histogram for a gray scale image which can be obtained from
	 * the color image based on CIE 1931 standard. The following formula is used 
	 * to obtain this histogram: RGB_Combined = R*0.2126 + G*0.7152 + B*0.0722.
	 */
	private int[] _computedGrayHistogram1;
	
	/**
	 * The histogram combined for the red, green and blue colors. This histogram
	 * represents a histogram for a gray scale image which can be obtained from
	 * the color image. The following formula is used to obtain this histogram:
	 * RGB_Combined = R*0.299 + G*0.587 + B*0.114.
	 */
	private int[] _computedGrayHistogram2;
	
	/**
	 * The luminance histogram.
	 */
	private int[] _lumincanceHistogram;
	
	/**
	 * The file name of the image.
	 */
	private String _fileName;
	
	/**
	 * A constructor for a Histogram object.
	 * @param fileName the name of the image file to compute the histogram.
	 */
	public Histogram(String fileName) {
		_fileName = fileName;
		_grayHistogram = new int[256];
		_computedGrayHistogram1 = new int[256];
		_computedGrayHistogram2 = new int[256];
		_redHistogram = new int[256];
		_greenHistogram = new int[256];
		_blueHistogram = new int[256];
		_lumincanceHistogram = new int[256];
	}
	
	/**
	 * A factory object which creates a histogram object for an image.
	 * @param fileName the file name of the image to compute a histogram.
	 * @return A histogram object for the specified image.
	 * @throws IOException if the specified image file does not exists or
	 *    there is some problem reading it.
	 */
	public static Histogram getHisrogram(String fileName) throws IOException {
		File f = new File(fileName);
		BufferedImage img = ImageIO.read(f);
		Histogram h = new Histogram(fileName);
		h.parseGrayHistogram(img);
		h.parseHistogramForColors(img);
		return h;
	}
	
	/**
	 * Compute the histogram of an image by converting it to a gray scale image
	 * using the available filters in Java.
	 * @param img {@link BufferedImage}  which representation the image.
	 */
	public void parseGrayHistogram(BufferedImage img){
		int height = img.getHeight();
		int width = img.getWidth();
		BufferedImage grayImg = convertToGrayscale(img);
		byte[] pix = new byte[1];
		int pixInt = 0;
		Raster grayImgRaster = grayImg.getRaster();
		
		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				grayImgRaster.getDataElements(x, y, pix);
				pixInt = (int)(pix[0]) & 0xFF;
				_grayHistogram[pixInt] ++;
			}
		}
	}
	
	/**
	 * Compute the histograms for the red, green, blue and their combinations.
	 * @param img {@link BufferedImage} object which represents the image.
	 */
	public void parseHistogramForColors(BufferedImage img) {
		int height = img.getHeight();
		int width = img.getWidth();		
		Color pixelColor = null;
		int computedGray = 0;
		boolean isGray = true;
		int r = 0;
		int g = 0;
		int b = 0;
		int luminance = 0;
		
		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				pixelColor = new Color(img.getRGB(x, y));
				r = pixelColor.getRed();
				g = pixelColor.getGreen();
				b = pixelColor.getBlue();
				
				_redHistogram[r]++;
				_greenHistogram[g]++;
				_blueHistogram[b]++;
				
				if (!(r == g && g == b)) {
					isGray = false;
				}
				
				computedGray = computeGrayColor1(pixelColor);
				_computedGrayHistogram1[computedGray]++;
				computedGray = computeGrayColor2(pixelColor);
				_computedGrayHistogram2[computedGray]++;
				
				luminance = computeLuminance(pixelColor);
				_lumincanceHistogram[luminance]++;
			}
		}
		
		if (isGray) {
			//
			// If for all pixel in the image the red, green and blue values were equal,
			// this is a gray scale image. Then use directly one of the computed histograms.
			//
			_computedGrayHistogram1 = _redHistogram;
			_computedGrayHistogram2 = _redHistogram;
		}
	}
	
	/**
	 * Converts a {@link BufferedImage} object into a gray scale image using
	 * the available filters in Java.
	 * @param src The {@link BufferedImage} to be converted.
	 * @return a {@link BufferedImage} which is converted in gray scale.
	 */
	private static BufferedImage convertToGrayscale(BufferedImage src) {
		ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
		BufferedImageOp op = new ColorConvertOp(cs, null);
		BufferedImage grayImg = op.filter(src, null);
		return grayImg;
	}
	
	/**
	 * Computes the combined histogram for a color pixel based on the
	 * following formula: RGB_Combined = R*0.2126 + G*0.7152 + B*0.0722
	 * @param c the color of a pixel.
	 * @return the combined value which is computed from R, G and B colors.
	 */
	private static int computeGrayColor1(Color c) {
		int r = c.getRed();
		int g = c.getGreen();
		int b = c.getBlue();
		int gray = (int)((double)(r*0.2126) + (double)(g*0.7152) + (double)(b*0.0722));
		return gray;
	}
	
	/**
	 * Computes the combined histogram for a color pixel based on the
	 * following formula: RGB_Combined = R*0.299 + G*0.587 + B*0.114
	 * @param c the color of a pixel.
	 * @return the combined value which is computed from R, G and B colors.
	 */
	private static int computeGrayColor2(Color c) {
		int r = c.getRed();
		int g = c.getGreen();
		int b = c.getBlue();
		int gray = (int)((double)(r*0.299) + (double)(g*0.587) + (double)(b*0.114));
		return gray;
	}
	
	/**
	 * Computes the luminance of a color pixel.
	 * @param c the color values.
	 * @return the corresponding lumincancevalue.
	 */
	private static int computeLuminance(Color c) {
		int r = c.getRed();
		int g = c.getGreen();
		int b = c.getBlue();
		int max = Math.max(Math.max(r, g), b);
		int min = Math.min(Math.min(r, g), b);
		int luminance = (max + min) / 2;
		return luminance;
	}
	
	/**
	 * 
	 * @return The gray histogram calculated by converting the color image into a 
	 * gray scale image using the filters coming with the Java framework. We do 
	 * not know how exactly the conversion is done.
	 */
	public int[] getGrayHistogram() {
		return _grayHistogram;
	}
	
	/**
	 * 
	 * @return 	The histogram combined for the red, green and blue colors. This histogram
	 * represents a histogram for a gray scale image which can be obtained from
	 * the color image based on CIE 1931 standard. The following formula is used 
	 * to obtain this histogram: RGB_Combined = R*0.2126 + G*0.7152 + B*0.0722.
	 */
	public int[] getComputedGrayHistogram1() {
		return _computedGrayHistogram1;
	}
	
	/**
	 * @return The histogram combined for the red, green and blue colors. This histogram
	 * represents a histogram for a gray scale image which can be obtained from
	 * the color image. The following formula is used to obtain this histogram:
	 * RGB_Combined = R*0.299 + G*0.587 + B*0.114.
	 */
	public int[] getComputedGrayHistogram2() {
		return _computedGrayHistogram2;
	}
	
	/**
	 * 
	 * @return The histogram for the red channel.
	 */
	public int[] getRedHistogram() {
		return _redHistogram;
	}
	
	/**
	 * 
	 * @return The histogram for the green channel.
	 */
	public int[] getGreenHistogram() {
		return _greenHistogram;
	}
	
	/**
	 * 
	 * @return The histogram for the blue channel.
	 */
	public int[] getBlueHistogram() {
		return _blueHistogram;
	}
	
	/**
	 * 
	 * @return The luminance histogram.
	 */
	public int[] getLuminanceHistogram() {
		return _lumincanceHistogram;
	}
	
	/**
	 * 
	 * @return The file name of the image.
	 */
	public String getFileName() {
		return _fileName;
	}

	/**
	 * Normalizes the values of a histogram.
	 * @param height the maximum value to normalize the values.
	 * @param histogram the histogram to be normalized.
	 * @return a histogram normalized based on @height.
	 */
	public static int[] normalize(int height, int[] histogram) {
		int max = 0;
		double ratio = 0;
		int[] res = new int[histogram.length];
		for (int i = 0; i < histogram.length; i++) {
			if (histogram[i] > max) {
				max = histogram[i];
			}
		}
		
		ratio = ((double)max)/height;
		for (int i = 0; i < res.length; i++) {
			res[i] = (int) (histogram[i] / ratio);
		}
		
		return res;
	}
	
	/**
	 * Smooths a histogram.
	 * @param h a histogram to be smoothed.
	 * @return a smoothed histogram.
	 */
	public static int[] smooth(int[] original) {
		int[] smoothed = new int[original.length];
		double[] mask = new double[] {0.25, 0.5, 0.25};
		
		smoothed[0] = original[0];
		smoothed[original.length - 1] = original[original.length - 1];
		
		for (int histInd = 1; histInd < original.length - 1; histInd++) {
			double smoothedValue = 0;
			for (int maskInd = 0; maskInd < mask.length; maskInd++ ) {
				smoothedValue += original[histInd - 1 + maskInd] * mask[maskInd]; 
			}
			smoothed[histInd] = (int) smoothedValue;
		}
		
		return smoothed;
	}
}
