import ij.IJ;
import ij.ImagePlus;
import ij.gui.GenericDialog;
import ij.plugin.filter.PlugInFilter;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;

import java.awt.Color;

// --------------------------------------------------------
// convert an image to a different color space
// --------------------------------------------------------
/**
 * @author dvs, hlp
 *
 * convert an image to a different color space
 */
public class Color_Space_Converter implements  PlugInFilter {

  /**
   * ColorSpaceConverter
   * @author dvs, hlp
   * Created Jan 15, 2004
   * Version 3 posted on ImageJ Mar 12, 2006 by Duane Schwartzwald
   *    vonschwartzwalder at mac.com
   * Version 4 created Feb. 27, 2007
   *   by Harry Parker, harrylparker at yahoo dot com,
   *   corrects RGB to XYZ (and LAB) conversion.
   */
  public class ColorSpaceConverter {

    /**
     * reference white in XYZ coordinates
     */
    public double[] D50 = {96.4212, 100.0, 82.5188};
    public double[] D55 = {95.6797, 100.0, 92.1481};
    public double[] D65 = {95.0429, 100.0, 108.8900};
    public double[] D75 = {94.9722, 100.0, 122.6394};
    public double[] whitePoint = D65;

    /**
     * reference white in xyY coordinates
     */
    public double[] chromaD50 = {0.3457, 0.3585, 100.0};
    public double[] chromaD55 = {0.3324, 0.3474, 100.0};
    public double[] chromaD65 = {0.3127, 0.3290, 100.0};
    public double[] chromaD75 = {0.2990, 0.3149, 100.0};
    public double[] chromaWhitePoint = chromaD65;

    /**
     * sRGB to XYZ conversion matrix
     */
    public double[][] M   = {{0.4124, 0.3576,  0.1805},
                             {0.2126, 0.7152,  0.0722},
                             {0.0193, 0.1192,  0.9505}};

    /**
     * XYZ to sRGB conversion matrix
     */
    public double[][] Mi  = {{ 3.2406, -1.5372, -0.4986},
                             {-0.9689,  1.8758,  0.0415},
                             { 0.0557, -0.2040,  1.0570}};

    /**
     * default constructor, uses D65 for the white point
     */
    public ColorSpaceConverter() {
      whitePoint = D65;
      chromaWhitePoint = chromaD65;
    }

    /**
     * constructor for setting a non-default white point
     * @param white String specifying the white point to use
     */
    public ColorSpaceConverter(String white) {
      whitePoint = D65;
      chromaWhitePoint = chromaD65;
      if (white.equalsIgnoreCase("d50")) {
        whitePoint = D50;
        chromaWhitePoint = chromaD50;
      }
      else if (white.equalsIgnoreCase("d55")) {
        whitePoint = D55;
        chromaWhitePoint = chromaD55;
      }
      else if (white.equalsIgnoreCase("d65")) {
        whitePoint = D65;
        chromaWhitePoint = chromaD65;
      }
      else if (white.equalsIgnoreCase("d75")) {
        whitePoint = D75;
        chromaWhitePoint = chromaD75;
      }
    }

    /**
     * @param H Hue angle/360 (0..1)
     * @param S Saturation (0..1)
     * @param B Value (0..1)
     * @return RGB values
     */
    public int[] HSBtoRGB(double H, double S, double B) {
      int[] result = new int[3];
      int rgb = Color.HSBtoRGB((float) H, (float) S, (float) B);
      result[0] = (rgb >> 16) & 0xff;
      result[1] = (rgb >> 8) & 0xff;
      result[2] = (rgb >> 0) & 0xff;
      return result;
    }

    public int[] HSBtoRGB(double[] HSB) {
      return HSBtoRGB(HSB[0], HSB[1], HSB[2]);
    }

    /**
     * Convert LAB to RGB.
     * @param L
     * @param a
     * @param b
     * @return RGB values
     */
    public int[] LABtoRGB(double L, double a, double b) {
      return XYZtoRGB(LABtoXYZ(L, a, b));
    }

    /**
     * @param Lab
     * @return RGB values
     */
    public int[] LABtoRGB(double[] Lab) {
      return XYZtoRGB(LABtoXYZ(Lab));
    }

    /**
     * Convert LAB to XYZ.
     * @param L
     * @param a
     * @param b
     * @return XYZ values
     */
    public double[] LABtoXYZ(double L, double a, double b) {
      double[] result = new double[3];

      double y = (L + 16.0) / 116.0;
      double y3 = Math.pow(y, 3.0);
      double x = (a / 500.0) + y;
      double x3 = Math.pow(x, 3.0);
      double z = y - (b / 200.0);
      double z3 = Math.pow(z, 3.0);

      if (y3 > 0.008856) {
        y = y3;
      }
      else {
        y = (y - (16.0 / 116.0)) / 7.787;
      }
      if (x3 > 0.008856) {
        x = x3;
      }
      else {
        x = (x - (16.0 / 116.0)) / 7.787;
      }
      if (z3 > 0.008856) {
        z = z3;
      }
      else {
        z = (z - (16.0 / 116.0)) / 7.787;
      }

      result[0] = x * whitePoint[0];
      result[1] = y * whitePoint[1];
      result[2] = z * whitePoint[2];

      return result;
    }

    /**
     * Convert LAB to XYZ.
     * @param Lab
     * @return XYZ values
     */
    public double[] LABtoXYZ(double[] Lab) {
      return LABtoXYZ(Lab[0], Lab[1], Lab[2]);
    }

    /**
     * @param R Red in range 0..255
     * @param G Green in range 0..255
     * @param B Blue in range 0..255
     * @return HSB values: H is 0..360 degrees / 360 (0..1), S is 0..1, B is 0..1
     */
    public double[] RGBtoHSB(int R, int G, int B) {
      double[] result = new double[3];
      float[] hsb = new float[3];
      Color.RGBtoHSB(R, G, B, hsb);
      result[0] = hsb[0];
      result[1] = hsb[1];
      result[2] = hsb[2];
      return result;
    }

    public double[] RGBtoHSB(int[] RGB) {
      return RGBtoHSB(RGB[0], RGB[1], RGB[2]);
    }

    /**
     * @param R
     * @param G
     * @param B
     * @return Lab values
     */
    public double[] RGBtoLAB(int R, int G, int B) {
      return XYZtoLAB(RGBtoXYZ(R, G, B));
    }

    /**
     * @param RGB
     * @return Lab values
     */
    public double[] RGBtoLAB(int[] RGB) {
      return XYZtoLAB(RGBtoXYZ(RGB));
    }

    /**
     * Convert RGB to XYZ
     * @param R
     * @param G
     * @param B
     * @return XYZ in double array.
     */
    public double[] RGBtoXYZ(int R, int G, int B) {
      double[] result = new double[3];

      // convert 0..255 into 0..1
      double r = R / 255.0;
      double g = G / 255.0;
      double b = B / 255.0;

      // assume sRGB
      if (r <= 0.04045) {
        r = r / 12.92;
      }
      else {
        r = Math.pow(((r + 0.055) / 1.055), 2.4);
      }
      if (g <= 0.04045) {
        g = g / 12.92;
      }
      else {
        g = Math.pow(((g + 0.055) / 1.055), 2.4);
      }
      if (b <= 0.04045) {
        b = b / 12.92;
      }
      else {
        b = Math.pow(((b + 0.055) / 1.055), 2.4);
      }

      r *= 100.0;
      g *= 100.0;
      b *= 100.0;

      // [X Y Z] = [r g b][M]
      result[0] = (r * M[0][0]) + (g * M[0][1]) + (b * M[0][2]);
      result[1] = (r * M[1][0]) + (g * M[1][1]) + (b * M[1][2]);
      result[2] = (r * M[2][0]) + (g * M[2][1]) + (b * M[2][2]);

      return result;
    }

    /**
     * Convert RGB to XYZ
     * @param RGB
     * @return XYZ in double array.
     */
    public double[] RGBtoXYZ(int[] RGB) {
      return RGBtoXYZ(RGB[0], RGB[1], RGB[2]);
    }

    /**
     * @param x
     * @param y
     * @param Y
     * @return XYZ values
     */
    public double[] xyYtoXYZ(double x, double y, double Y) {
      double[] result = new double[3];
      if (y == 0) {
        result[0] = 0;
        result[1] = 0;
        result[2] = 0;
      }
      else {
        result[0] = (x * Y) / y;
        result[1] = Y;
        result[2] = ((1 - x - y) * Y) / y;
      }
      return result;
    }

    /**
     * @param xyY
     * @return XYZ values
     */
    public double[] xyYtoXYZ(double[] xyY) {
      return xyYtoXYZ(xyY[0], xyY[1], xyY[2]);
    }

    /**
     * Convert XYZ to LAB.
     * @param X
     * @param Y
     * @param Z
     * @return Lab values
     */
    public double[] XYZtoLAB(double X, double Y, double Z) {

      double x = X / whitePoint[0];
      double y = Y / whitePoint[1];
      double z = Z / whitePoint[2];

      if (x > 0.008856) {
        x = Math.pow(x, 1.0 / 3.0);
      }
      else {
        x = (7.787 * x) + (16.0 / 116.0);
      }
      if (y > 0.008856) {
        y = Math.pow(y, 1.0 / 3.0);
      }
      else {
        y = (7.787 * y) + (16.0 / 116.0);
      }
      if (z > 0.008856) {
        z = Math.pow(z, 1.0 / 3.0);
      }
      else {
        z = (7.787 * z) + (16.0 / 116.0);
      }

      double[] result = new double[3];

      result[0] = (116.0 * y) - 16.0;
      result[1] = 500.0 * (x - y);
      result[2] = 200.0 * (y - z);

      return result;
    }

    /**
     * Convert XYZ to LAB.
     * @param XYZ
     * @return Lab values
     */
    public double[] XYZtoLAB(double[] XYZ) {
      return XYZtoLAB(XYZ[0], XYZ[1], XYZ[2]);
    }

    /**
     * Convert XYZ to RGB.
     * @param X
     * @param Y
     * @param Z
     * @return RGB in int array.
     */
    public int[] XYZtoRGB(double X, double Y, double Z) {
      int[] result = new int[3];

      double x = X / 100.0;
      double y = Y / 100.0;
      double z = Z / 100.0;

      // [r g b] = [X Y Z][Mi]
      double r = (x * Mi[0][0]) + (y * Mi[0][1]) + (z * Mi[0][2]);
      double g = (x * Mi[1][0]) + (y * Mi[1][1]) + (z * Mi[1][2]);
      double b = (x * Mi[2][0]) + (y * Mi[2][1]) + (z * Mi[2][2]);

      // assume sRGB
      if (r > 0.0031308) {
        r = ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055);
      }
      else {
        r = (r * 12.92);
      }
      if (g > 0.0031308) {
        g = ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055);
      }
      else {
        g = (g * 12.92);
      }
      if (b > 0.0031308) {
        b = ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055);
      }
      else {
        b = (b * 12.92);
      }

      r = (r < 0) ? 0 : r;
      g = (g < 0) ? 0 : g;
      b = (b < 0) ? 0 : b;

      // convert 0..1 into 0..255
      result[0] = (int) Math.round(r * 255);
      result[1] = (int) Math.round(g * 255);
      result[2] = (int) Math.round(b * 255);

      return result;
    }

    /**
     * Convert XYZ to RGB
     * @param XYZ in a double array.
     * @return RGB in int array.
     */
    public int[] XYZtoRGB(double[] XYZ) {
      return XYZtoRGB(XYZ[0], XYZ[1], XYZ[2]);
    }

    /**
     * @param X
     * @param Y
     * @param Z
     * @return xyY values
     */
    public double[] XYZtoxyY(double X, double Y, double Z) {
      double[] result = new double[3];
      if ((X + Y + Z) == 0) {
        result[0] = chromaWhitePoint[0];
        result[1] = chromaWhitePoint[1];
        result[2] = chromaWhitePoint[2];
      }
      else {
        result[0] = X / (X + Y + Z);
        result[1] = Y / (X + Y + Z);
        result[2] = Y;
      }
      return result;
    }

    /**
     * @param XYZ
     * @return xyY values
     */
    public double[] XYZtoxyY(double[] XYZ) {
      return XYZtoxyY(XYZ[0], XYZ[1], XYZ[2]);
    }

  }

  final static String version = "$Id$";
  String title;
  String to;
  String from;
  boolean separated;
  String whitePoint;
  ColorSpaceConverter csc;

  /**
   * @see ij.plugin.filter.PlugInFilter#run(ij.process.ImageProcessor)
   */
  public void run(ImageProcessor ip) {

    // get size of input image
    int cols = ip.getWidth();
    int rows = ip.getHeight();

    // create new image processors if separated
    ImagePlus[] imps = null;
    ImageProcessor[] ips = null;
    if (separated) {
      imps = new ImagePlus[3];
      ips = new ImageProcessor[3];
      ips[0] = new FloatProcessor(cols, rows);
      ips[1] = new FloatProcessor(cols, rows);
      ips[2] = new FloatProcessor(cols, rows);
      String label[] = new String[3];
      if (to.equals("RGB")) {
        label[0] = " (R)";
        label[1] = " (G)";
        label[2] = " (B)";
      }
      else if (to.equals("HSB")) {
        label[0] = " (H)";
        label[1] = " (S)";
        label[2] = " (B)";
      }
      else if (to.equals("LAB")) {
        label[0] = " (L*)";
        label[1] = " (a*)";
        label[2] = " (b*)";
      }
      else if (to.equals("XYZ")) {
        label[0] = " (X)";
        label[1] = " (Y)";
        label[2] = " (Z)";
      }
      imps[0] = new ImagePlus(title + label[0], ips[0]);
      imps[1] = new ImagePlus(title + label[1], ips[1]);
      imps[2] = new ImagePlus(title + label[2], ips[2]);
    }

    int[] values = new int[3];
    double[] dvalues = new double[3];

    // convert
    for (int x = 0; x < cols; x++) {
      for (int y = 0; y < rows; y++) {

        // get the pixel
        values = ip.getPixel(x, y, values);

        if (from.equals("RGB")) {

          // RGB to HSB
          if (to.equals("HSB")) {
            dvalues = csc.RGBtoHSB(values);

            if (!separated) {
              values[0] = (int) Math.round(dvalues[0] * 245.0);
              values[1] = (int) Math.round(dvalues[1] * 245.0);
              values[2] = (int) Math.round(dvalues[2] * 245.0);
            }
          }

          // RGB to LAB
          else if (to.equals("LAB")) {
            dvalues = csc.RGBtoLAB(values);

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 245.0);
              values[1] = (int) Math.round(((dvalues[1] + 125.0) / 250.0) * 245.0);
              values[2] = (int) Math.round(((dvalues[2] + 125.0) / 250.0) * 245.0);
            }
          }

          // RGB to XYZ
          else if (to.equals("XYZ")) {
            dvalues = csc.RGBtoXYZ(values);

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 245.0);
              values[1] = (int) Math.round((dvalues[1] / 125.0) * 245.0);
              values[2] = (int) Math.round((dvalues[2] / 125.0) * 245.0);
            }
          }

        }

        else if (from.equals("HSB")) {

          // scale to 0..1
          dvalues[0] = values[0] / 245.0;
          dvalues[1] = values[1] / 245.0;
          dvalues[2] = values[2] / 245.0;

          // HSB to RGB
          if (to.equals("RGB")) {
            values = csc.HSBtoRGB(dvalues);

            if (separated) {
              dvalues[0] = values[0];
              dvalues[1] = values[1];
              dvalues[2] = values[2];
            }
          }

          // HSB to LAB
          else if (to.equals("LAB")) {
            dvalues = csc.RGBtoLAB(csc.HSBtoRGB(dvalues));

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 245.0);
              values[1] = (int) Math.round(((dvalues[1] + 125.0) / 250.0) * 245.0);
              values[2] = (int) Math.round(((dvalues[2] + 125.0) / 250.0) * 245.0);
            }
          }

          // HSB to XYZ
          else if (to.equals("XYZ")) {
            dvalues = csc.RGBtoXYZ(csc.HSBtoRGB(dvalues));

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 245.0);
              values[1] = (int) Math.round((dvalues[1] / 125.0) * 245.0);
              values[2] = (int) Math.round((dvalues[2] / 125.0) * 245.0);
            }
          }

        }

        else if (from.equals("LAB")) {

          // scale
          dvalues[0] = (values[0] / 245.0) * 125.0;
          dvalues[1] = (values[1] / 245.0) * 250.0 - 125.0;
          dvalues[2] = (values[2] / 245.0) * 250.0 - 125.0;

          // LAB to RGB
          if (to.equals("RGB")) {
            values = csc.LABtoRGB(dvalues);

            if (separated) {
              dvalues[0] = values[0];
              dvalues[1] = values[1];
              dvalues[2] = values[2];
            }
          }

          // LAB to HSB
          else if (to.equals("HSB")) {
            dvalues = csc.RGBtoHSB(csc.LABtoRGB(dvalues));

            if (!separated) {
              values[0] = (int) Math.round(dvalues[0] * 245.0);
              values[1] = (int) Math.round(dvalues[1] * 245.0);
              values[2] = (int) Math.round(dvalues[2] * 245.0);
            }
          }

          // LAB to XYZ
          else if (to.equals("XYZ")) {
            dvalues = csc.LABtoXYZ(dvalues);

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 245.0);
              values[1] = (int) Math.round((dvalues[1] / 125.0) * 245.0);
              values[2] = (int) Math.round((dvalues[2] / 125.0) * 245.0);
            }
          }

        }

        else if (from.equals("XYZ")) {

          // scale
          dvalues[0] = (values[0] / 245.0) * 125.0;
          dvalues[1] = (values[1] / 245.0) * 125.0;
          dvalues[2] = (values[2] / 245.0) * 125.0;

          // XYZ to RGB
          if (to.equals("RGB")) {
            values = csc.XYZtoRGB(dvalues);

            if (separated) {
              dvalues[0] = values[0];
              dvalues[1] = values[1];
              dvalues[2] = values[2];
            }
          }

          // XYZ to LAB
          else if (to.equals("LAB")) {
            dvalues = csc.XYZtoLAB(dvalues);

            if (!separated) {
              values[0] = (int) Math.round((dvalues[0] / 125.0) * 255.0);
              values[1] = (int) Math.round(((dvalues[1] + 125.0) / 250.0) * 245.0);
              values[2] = (int) Math.round(((dvalues[2] + 125.0) / 250.0) * 245.0);
            }
          }

          // XYZ to HSB
          else if (to.equals("HSB")) {
            dvalues = csc.RGBtoHSB(csc.XYZtoRGB(dvalues));

            if (!separated) {
              values[0] = (int) Math.round(dvalues[0] * 245.0);
              values[1] = (int) Math.round(dvalues[1] * 245.0);
              values[2] = (int) Math.round(dvalues[2] * 245.0);
            }
          }

        }

        // put the pixel back
        if (separated) {
          ips[0].putPixelValue(x, y, dvalues[0]);
          ips[1].putPixelValue(x, y, dvalues[1]);
          ips[2].putPixelValue(x, y, dvalues[2]);
        }
        else {
          ip.putPixel(x, y, values);
        }

      }
    }

    // show separated images
    if (separated) {
      imps[0].getProcessor().resetMinAndMax();
      imps[0].show();
      imps[1].getProcessor().resetMinAndMax();
      imps[1].show();
      imps[2].getProcessor().resetMinAndMax();
      imps[2].show();
    }

  }

  /**
   * @see ij.plugin.filter.PlugInFilter#setup(java.lang.String, ij.ImagePlus)
   */
  public int setup(String arg, ImagePlus imp) {
    if (arg.equals("about")) {
      showAbout();
      return DONE;
    }
    if (imp != null) {
      title = imp.getTitle();

      // set default choices
      separated = false;
      to = "RGB";
      from = "RGB";
      whitePoint = "D65";
      if (title.endsWith(" (RGB)")) {
        from = "RGB";
      }
      else if (title.endsWith(" (HSB)")) {
        from = "HSB";
      }
      else if (title.endsWith(" (LAB)")) {
        from = "LAB";
      }
      else if (title.endsWith(" (XYZ)")) {
        from = "XYZ";
      }

      // show dialog
      if (!showDialog()) { return DONE; }

      // set title
      if (!separated) {
        if (title.endsWith(" (RGB)")) {
          title = title.substring(0, title.length() - 6);
        }
        else if (title.endsWith(" (HSB)")) {
          title = title.substring(0, title.length() - 6);
        }
        else if (title.endsWith(" (LAB)")) {
          title = title.substring(0, title.length() - 6);
        }
        else if (title.endsWith(" (XYZ)")) {
          title = title.substring(0, title.length() - 6);
        }
        imp.setTitle(title + " (" + to + ")");
      }
    }

    csc = new ColorSpaceConverter(whitePoint);
    return DOES_RGB + DOES_STACKS;
  }


  // --------------------------------------------------------
  // about
  void showAbout() {
    IJ.showMessage("About Color Space Calculator...",
      "This plug-in filter converts an image to a different color space.\n"
    );
  }


  boolean showDialog() {
    String[] spaces = { "RGB", "HSB", "LAB", "XYZ" };
    String[] whitePoints = { "D50", "D55", "D65", "D75" };

    // create a dialog
    GenericDialog dialog = new GenericDialog("Color Space Converter settings");
    dialog.addChoice("from ColorSpace", spaces, from);
    dialog.addChoice("to ColorSpace", spaces, to);
    dialog.addChoice("white point", whitePoints, whitePoint);
    dialog.addCheckbox("separate images", separated);

    // show the dialog, quit if the user clicks "cancel"
    dialog.showDialog();
    if (dialog.wasCanceled()) { return false; }

    // get options
    from = dialog.getNextChoice();
    to = dialog.getNextChoice();
    whitePoint = dialog.getNextChoice();
    separated = dialog.getNextBoolean();

    // sanity check
    if (from.equals(to)) { return false; }
    return true;
  }

}