/*
 * Modification History
 *
 * 2001-February-26		Jason Rohrer
 * Created. 
 * Added functions for converting to and from gray byte arrays.
 *
 * 2001-September-19		Jason Rohrer
 * Added an RGB->HSB conversion function, which was copied
 * from minorGems/ai/robotics/ImageStatistics.
 * Added RGB<->YIQ functions.
 *
 * 2001-September-20		Jason Rohrer
 * Fixed a bug in the YIQ conversion.
 * Got rid of this bug fix, as it distorts the YIQ space,
 * and there is no way to prevent colors from going out of the
 * [0,1] range all of the time anyway.
 *
 * 2001-September-24		Jason Rohrer
 * Added RGB<->YCbCr functions.
 * Abstracted out a common coefficient multiplying function.
 */

#ifndef IMAGE_COLOR_CONVERTER_INCLUDED
#define IMAGE_COLOR_CONVERTER_INCLUDED

#include "Image.h"


/**
 * A container class for static functions that convert
 * images between various color spaces.
 *
 * @author Jason Rohrer
 */ 
class ImageColorConverter {
	
	public:
		
		/**
		 * Converts a 3-channel RGB image to a 1-channel grayscale
		 * image using an NTSC luminosity standard.
		 *
		 * @param inImage the RGB image to convert.  Must be destroyed
		 *   by caller.
		 *
		 * @return a new, grayscale version of inImage.  Must be
		 *   destroyed by the caller.  Returns NULL if inImage
		 *   is not a 3-channel image.
		 */
		static Image *RGBToGrayscale( Image *inImage );
		
		/**
		 * Converts a 1-channel grayscae image to a 3-channel RGB
		 * image.
		 *
		 * @param inImage the grayscale image to convert.  Must be destroyed
		 *   by caller.
		 *
		 * @return a new, RGB version of inImage.  Must be
		 *   destroyed by the caller.  Returns NULL if inImage
		 *   is not a 1-channel image.
		 */
		static Image *grayscaleToRGB( Image *inImage );
		

		/**
		 * Converts a 1-channel grayscae image to a 1-channel byte array.
		 *
		 * @param inImage the grayscale image to convert.  Must be destroyed
		 *   by caller.
		 * @param inChannelNumber the channel number to use as the 
		 *   gray channel.  Defaults to 0;
		 *
		 * @return a new byte array with one byte per image pixel,
		 *   and image [0,1] values mapped to [0,255].
		 */
		static unsigned char *grayscaleToByteArray( Image *inImage,
			int inChannelNumber = 0 );
		
		/**
		 * Converts a byte array to a 1-channel grayscale
		 * image.
		 *
		 * @param inBytes the byte array to convert.  Must be destroyed
		 *   by caller.
		 * @param inWidth the width of the image contained in the byte array.
		 * @param inHeight the height of the image contained in the byte array.
		 *
		 * @return a new, grayscale image version of the byte array.  Must be
		 *   destroyed by the caller.
		 */
		static Image *byteArrayToGrayscale( unsigned char *inBytes,
			int inWidth, int inHeight );


		
		/**
		 * Converts an RGB image to HSB.
		 *
		 * @param inRGBImage the rgb image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is the HSB conversion of the
		 *   RGB image.  Must be destroyed by caller.
		 */
		static Image *RGBToHSB( Image *inRGBImage );



		/**
		 * Converts an RGB image to YIQ.
		 *
		 * Note that color values in the resulting YIQ
		 * image may lie outside of the range [0,1].
		 *
		 * @param inRGBImage the rgb image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is the YIQ conversion of the
		 *   RGB image.  Must be destroyed by caller.
		 */
		static Image *RGBToYIQ( Image *inRGBImage );



		/**
		 * Converts a YIQ image to RGB.
		 *
		 *
		 * Note that color values in the resulting RGB
		 * image may lie outside of the range [0,1].
		 *
		 * @param inYIQImage the rgb image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is the RGB conversion of the
		 *   YIQ image.  Must be destroyed by caller.
		 */
		static Image *YIQToRGB( Image *inYIQImage );



		/**
		 * Converts an RGB image to YCbCr.
		 *
		 * Note that in the YCbCr standard, Y is in the range
		 * [0,1], while Cb and Cr are both in the range [-0.5, 0.5].
		 * This function returns Cb and Cr components shifted
		 * into the range [0,1].
		 *
		 * @param inRGBImage the rgb image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is the YCbCr conversion of the
		 *   RGB image.  Must be destroyed by caller.
		 */
		static Image *RGBToYCbCr( Image *inRGBImage );



		/**
		 * Converts a YCbCr image to RGB.
		 *
		 *
		 * Note that in the YCbCr standard, Y is in the range
		 * [0,1], while Cb and Cr are both in the range [-0.5, 0.5].
		 * This function expects input Cb and Cr components to be shifted
		 * into the range [0,1].
		 *
		 * @param inYCbCrImage the rgb image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is the RGB conversion of the
		 *   YCbCr image.  Must be destroyed by caller.
		 */
		static Image *YCbCrToRGB( Image *inYCbCrImage );


		
	protected:

		/**
		 * Converts an 3-channel image to another 3-channel
		 * image using a matrix of conversion coefficients.
		 *
		 * The following formulae are used;
		 * outChan0 = inC00 * inChan0 + inC01 * inChan1 + inC02 * inChan2
		 * outChan1 = inC10 * inChan0 + inC11 * inChan1 + inC12 * inChan2
		 * outChan2 = inC20 * inChan0 + inC21 * inChan1 + inC22 * inChan2
		 *
		 * @param inImage the image to convert.
		 *   Must be destroyed by caller.
		 *
		 * @return a new image that is inImage converted, or NULL if
		 *   conversion failed (usually because inImage does not
		 *   contain 3 channels).
		 *   Must be destroyed by caller.
		 */
		static Image *coefficientConvert( Image *inImage,
										  double inC00,
										  double inC01,
										  double inC02,
										  double inC10,
										  double inC11,
										  double inC12,
										  double inC20,
										  double inC21,
										  double inC22 );
											 
	};		



inline Image *ImageColorConverter::RGBToGrayscale( Image *inImage ) {
	int w = inImage->getWidth();
	int h = inImage->getHeight();
	int numPixels = w * h;
	
	Image *grayImage = new Image( w, h, 1 );
	
	double *red = inImage->getChannel( 0 );
	double *green = inImage->getChannel( 1 );
	double *blue = inImage->getChannel( 2 );
	
	double *gray = grayImage->getChannel( 0 );
	
	for( int i=0; i<numPixels; i++ ) {
		// NTSC luminosity formula
		gray[i] = .299 * red[i] + .587 * green[i] + .114 * blue[i];
		}
		
	return grayImage;
	}
		
		

inline Image *ImageColorConverter::grayscaleToRGB( Image *inImage ) {
	int w = inImage->getWidth();
	int h = inImage->getHeight();
	int numPixels = w * h;
	
	Image *rgbImage = new Image( w, h, 3 );
	
	double *red = rgbImage->getChannel( 0 );
	double *green = rgbImage->getChannel( 1 );
	double *blue = rgbImage->getChannel( 2 );
	
	double *gray = inImage->getChannel( 0 );
	
	for( int i=0; i<numPixels; i++ ) {
		red[i] = gray[i];
		green[i] = gray[i];
		blue[i] = gray[i];
		}
		
	return rgbImage;
	}


inline unsigned char *ImageColorConverter::
	grayscaleToByteArray( Image *inImage, int inChannelNumber ) {
	
	int w = inImage->getWidth();
	int h = inImage->getHeight();
	int numPixels = w * h;
	
	unsigned char *bytes = new unsigned char[ numPixels ];
	
	double *gray = inImage->getChannel( inChannelNumber );
	
	for( int i=0; i<numPixels; i++ ) {
		bytes[i] = (unsigned char)( 255 * gray[i] );
		}
		
	return bytes;
	}



inline Image *ImageColorConverter::
	byteArrayToGrayscale( unsigned char *inBytes,
	int inWidth, int inHeight ) {
	
	int w = inWidth;
	int h = inHeight;
	int numPixels = w * h;
	
	Image *grayImage = new Image( w, h, 1 );
	
	
	double *gray = grayImage->getChannel( 0 );
	
	for( int i=0; i<numPixels; i++ ) {
		gray[i] = (double)( inBytes[i] ) / 255.0;
		}
		
	return grayImage;
	}



inline Image *ImageColorConverter::RGBToHSB( Image *inRGBImage ) {
	// idea modeled after Java Color class.
	
	if( inRGBImage->getNumChannels() != 3 ) {
		printf(
			"RGBtoHSB requires a 3-channel image as input.\n" );
		return NULL;
		}

	int w = inRGBImage->getWidth();
	int h = inRGBImage->getHeight();

	int numPixels = w * h;
				
	Image *hsbImage = new Image( w, h, 3 );

	double *redChannel = inRGBImage->getChannel( 0 );
	double *greenChannel = inRGBImage->getChannel( 1 );
	double *blueChannel = inRGBImage->getChannel( 2 );

	double *hueChannel = hsbImage->getChannel( 0 );
	double *satChannel = hsbImage->getChannel( 1 );
	double *brightChannel = hsbImage->getChannel( 2 );

				
	for( int i=0; i<numPixels; i++ ) {
		int r = (int)( 255 * redChannel[i] );
		int g = (int)( 255 * greenChannel[i] );
		int b = (int)( 255 * blueChannel[i] );

		double hue, sat, bright;
				
		int cmax = (r > g) ? r : g;
		if (b > cmax) {
			cmax = b;
			}

		int cmin = (r < g) ? r : g;
		if (b < cmin) {
			cmin = b;
			}
								
		bright = ( (double)cmax ) / 255.0;
		if( cmax != 0 ) {
			sat = ( (double)( cmax - cmin ) ) / ( (double) cmax );
			}
		else {
			sat = 0;
			}
		if( sat == 0 ) {
			hue = 0;
			}
		else {
			double redc =
				( (double)( cmax - r ) ) / ( (double)( cmax - cmin ) );
			double greenc =
				( (double) ( cmax - g ) ) / ( (double)( cmax - cmin ) );
			double bluec =
				( (double)( cmax - b ) ) / ( (double)( cmax - cmin ) );

			if( r == cmax ) {
				hue = bluec - greenc;
				}
			else if( g == cmax ) {
				hue = 2.0 + redc - bluec;
				}
			else {
				hue = 4.0 + greenc - redc;
				}
			hue = hue / 6.0;
												
			if( hue < 0 ) {
				hue = hue + 1.0;
				}
			}

		hueChannel[i] = hue;
		satChannel[i] = sat;
		brightChannel[i] = bright;
		}

	return hsbImage;
	}



inline Image *ImageColorConverter::RGBToYIQ( Image *inRGBImage ) {
	if( inRGBImage->getNumChannels() != 3 ) {
		printf(
			"RGBtoYIQ requires a 3-channel image as input.\n" );
		return NULL;
		}

	Image *yiqImage = coefficientConvert( inRGBImage,
										  0.299,  0.587,  0.114,
										  0.596, -0.274, -0.322,
										  0.212, -0.523,  0.311 );

	return yiqImage;
	}



inline Image *ImageColorConverter::YIQToRGB( Image *inYIQImage ) {
	if( inYIQImage->getNumChannels() != 3 ) {
		printf(
			"YIQtoRGB requires a 3-channel image as input.\n" );
		return NULL;
		}

	Image *rgbImage = coefficientConvert( inYIQImage,
										  1.0,  0.956,  0.621,
										  1.0, -0.272, -0.647,
										  1.0, -1.105,  1.702 );
	return rgbImage;
	}



inline Image *ImageColorConverter::RGBToYCbCr( Image *inRGBImage ) {
	if( inRGBImage->getNumChannels() != 3 ) {
		printf(
			"RGBtoYCbCr requires a 3-channel image as input.\n" );
		return NULL;
		}

	// coefficients taken from the color space faq
	/*
	  RGB -> YCbCr (with Rec 601-1 specs)
	  Y  =  0.2989 * Red + 0.5866 * Green + 0.1145 * Blue
	  Cb = -0.1687 * Red - 0.3312 * Green + 0.5000 * Blue
	  Cr =  0.5000 * Red - 0.4183 * Green - 0.0816 * Blue

	  YCbCr (with Rec 601-1 specs) -> RGB
	  Red   = Y + 0.0000 * Cb + 1.4022 * Cr
	  Green = Y - 0.3456 * Cb - 0.7145 * Cr
	  Blue  = Y + 1.7710 * Cb + 0.0000 * Cr
	*/
	
	Image *ycbcrImage = coefficientConvert( inRGBImage,
											 0.2989,  0.5866,  0.1145,
											-0.1687, -0.3312,  0.5000,
											 0.5000, -0.4183, -0.0816 );

	// adjust the Cb and Cr channels so they are in the range [0,1]
	int numPixels = ycbcrImage->getWidth() * ycbcrImage->getHeight();
	double *cbChannel = ycbcrImage->getChannel( 1 );
	double *crChannel = ycbcrImage->getChannel( 2 );
	for( int i=0; i<numPixels; i++ ) {
		cbChannel[i] += 0.5;
		crChannel[i] += 0.5;
		}

	// no need to clip pixels to the range [0,1], since
	// all possible rgb pixel values are represented by in-range
	// yCbCr pixel values

	
	return ycbcrImage;
	}



inline Image *ImageColorConverter::YCbCrToRGB( Image *inYCbCrImage ) {
	if( inYCbCrImage->getNumChannels() != 3 ) {
		printf(
			"YCbCrtoRGB requires a 3-channel image as input.\n" );
		return NULL;
		}

	// adjust the normalized Cb and Cr channels
	// so they are in the range [-0.5,0.5]
	
	int numPixels = inYCbCrImage->getWidth() * inYCbCrImage->getHeight();
	double *cbChannel = inYCbCrImage->getChannel( 1 );
	double *crChannel = inYCbCrImage->getChannel( 2 );
	for( int i=0; i<numPixels; i++ ) {
		cbChannel[i] -= 0.5;
		crChannel[i] -= 0.5;
		}

	// coefficients taken from the color space faq
	/*
	  RGB -> YCbCr (with Rec 601-1 specs)
	  Y  =  0.2989 * Red + 0.5866 * Green + 0.1145 * Blue
	  Cb = -0.1687 * Red - 0.3312 * Green + 0.5000 * Blue
	  Cr =  0.5000 * Red - 0.4183 * Green - 0.0816 * Blue

	  YCbCr (with Rec 601-1 specs) -> RGB
	  Red   = Y + 0.0000 * Cb + 1.4022 * Cr
	  Green = Y - 0.3456 * Cb - 0.7145 * Cr
	  Blue  = Y + 1.7710 * Cb + 0.0000 * Cr
	*/
	
	Image *rgbImage = coefficientConvert( inYCbCrImage,
										  1.0,  0.0000,  1.4022,
										  1.0, -0.3456, -0.7145,
										  1.0,  1.7710,  0.0000 );

	// clip r, g, and b channels to the range [0,1], since
	// some YCbCr pixel values might map out of this range
	// (in other words, some YCbCr values map outside of rgb space)
	for( int c=0; c<3; c++ ) {
		double *channel = rgbImage->getChannel( c );
		
		for( int p=0; p<numPixels; p++ ) {
			if( channel[p] < 0 ) {
				channel[p] = 0;
				}
			else if( channel[p] > 1 ) {
				channel[p] = 1.0;
				}
			}
		}
	
	return rgbImage;
	}



inline Image *ImageColorConverter::coefficientConvert( Image *inImage,
													   double inC00,
													   double inC01,
													   double inC02,
													   double inC10,
													   double inC11,
													   double inC12,
													   double inC20,
													   double inC21,
													   double inC22 ) {
	if( inImage->getNumChannels() != 3 ) {
		return NULL;
		}

	
	int w = inImage->getWidth();
	int h = inImage->getHeight();

	int numPixels = w * h;
				
	Image *outImage = new Image( w, h, 3 );

	double *outChannel0 = outImage->getChannel( 0 );
	double *outChannel1 = outImage->getChannel( 1 );
	double *outChannel2 = outImage->getChannel( 2 );

	double *inChannel0 = inImage->getChannel( 0 );
	double *inChannel1 = inImage->getChannel( 1 );
	double *inChannel2 = inImage->getChannel( 2 );
	
	for( int i=0; i<numPixels; i++ ) {
		outChannel0[i] =
			inC00 * inChannel0[i] +
			inC01 * inChannel1[i] +
			inC02 * inChannel2[i];
		outChannel1[i] =
			inC10 * inChannel0[i] +
			inC11 * inChannel1[i] +
			inC12 * inChannel2[i];
		outChannel2[i] =
			inC20 * inChannel0[i] +
			inC21 * inChannel1[i] +
			inC22 * inChannel2[i];
		}

	return outImage;
	}



#endif
