package yi.iu.processor.texture.reg;

import java.util.*;

import java.awt.*;
import java.awt.image.*;

import yi.ai.cluster.*;
import yi.iu.imgdb.learn.*;
import yi.iu.processor.channel.*;
import yi.iu.processor.color.*;
import yi.iu.processor.color.quan.*;
import yi.iu.processor.filter.*;
import yi.iu.processor.morph.*;
import yi.iu.processor.resample.*;
import yi.iu.processor.texture.gabor.*;
import yi.iu.region.*;
import yi.math.*;
import yi.stat.hist.*;
import yi.util.config.*;
import yi.util.io.*;
import yi.util.io.input.*;
import yi.util.io.output.*;
import yi.util.log.*;

/**
 * Class ColorGuidedTextureSeg is to do texture-based segmentation of images.
 * Given an image, it first invokes yi.iu.processor.texture.reg.ColorPatternSeg
 * to roughly segment the images to patterned color regions.  Then it does
 * texture feature (Gabor, for now) based segmentation within each patterned
 * color regions.  Finally, it do the (roughly) connected component analysis and
 * filtering.
 */
public class ColorGuidedTextureSeg implements Configurable, RstReusable, RepresentableFeatureGenerator{
  /**
   * cache the gabor results
   */
  double[][] m_features = null;

  /**
   * Dilation is used in the first step of finding the patterned color regions.
   * It may also be used (there are other options) in the (roughly) connected
   * component analysis setp.
   */
  public int    DILATION_DISK_SIZE = 5;
  /**
   * Gussian smooth theta.  theta = GAUSSIAN_THETA * Math.sqrt(width*height).
   * Eg. width = 600, height = 400, theta = around 5
   * Used in the pre-process of connected component analysis on the texture
   * segments.
   */
  public double GAUSSIAN_THETA = 0.01;
  /**
   * filter threshold.  detail comparison see cv010
   * Used in the pre-process of connected component analysis on the texture
   * segments.
   */
  public double FILTER_THRESHOLD = 0.2;
  /**
   * If a region is smaller than x*y*MINIMUM_REGION_SIZE, it will be discarded.
   */
  public double MINIMUM_REGION_SIZE = 0.01;
  /**
   * In the procedure, two color regions will be combined if they are overlapped
   * a lot.  This is the threshold.  Used in ColorPatternSeg
   */
  public double COMBINE_THRESHOLD = 0.5;

  /**
   * Within each patterned color region, a texture feature based segmentation
   * is performed.  K-Means clustering is used and this is the parameter of K
   */
  public int KMEANS_K = 8;

  public static int MASKED = 0;

  /**
   * Feed Gabor feature extraction processor with luminance data
   * @see #GABOR_DATA_FEED
   */
  public final static int GABOR_USE_LUMINANCE = 0;
  /**
   * Feed Gabor feature extraction processor with average of H,S,I.  This method
   * is used in Selim's code
   * @see #GABOR_DATA_FEED
   */
  public final static int GABOR_USE_HSI_AVERAGE = 1;
  /**
   * Feed Gabor feature extraction processor with a mistaken data format:
   * luminance data is first extracteda and then that data will be treated as
   * blue component of a rgb color and fed into the average of H,S,I procedure.
   * @see #GABOR_DATA_FEED
   */
  public final static int GABOR_USE_MISTAKE = 2;

  /**
   * How the rgb colors will be transformed before fed into Gabor process.
   */
  public int GABOR_DATA_FEED = GABOR_USE_HSI_AVERAGE;

  /**
   * The Gabor results will be normalized within the whole image before
   * clustering.  The unit will be picked up from all dimensions.  e.g. for 30D
   * Gabors, the maximum will be pickup from 30D space, and all the data will
   * be normalized by that maximum
   */
  public static int GLOBAL_NORMALIZE = 0;

  /**
   * The Gabor results will be normalized within the whole image before
   * clustering.  The unit will be picked up from each dimensions.  e.g. for 30D
   * 5 scales Gabors, the maximum will be pickup from each scale, and the data
   * will be normalized by the corresponding maximum
   */
  public static int SCALE_NORMALIZE = 1;

  /**
   * The Gabor results will be normalized within each connected color region
   * before clustering
   */
  public static int REGION_GLOBAL_NORMALIZE = 2;

  /**
   * The Gabor results will be normalized within each connected color region
   * before clustering
   */
  public static int REGION_SCALE_NORMALIZE = 3;

  /**
   * The Gabor results will be normalized by a fixed scale
   */
  public static int FIXED_NORMALIZE = 4;

  /**
   * The scale for fixed normalization
   */
  public double FIXED_NORMALIZE_SCALE = 5;

  /**
   * The normalization method to be used
   */
  public int NORMALIZE_METHOD = GLOBAL_NORMALIZE;

  /**
   * data are pre-smoothed before sent to gabor
   */
  public boolean PRE_GABOR_SMOOTH = false;

  /**
   * data are pre-smoothed before sent to gabor
   */
  public double PRE_GABOR_SMOOTH_THETA = 1.0;

  /**
   * How many pixels are effected by the boundary.  When do texture
   * segmentation, the boundary is not reliable.  Remove the boundary of width
   * of Gabor.maskwidth * BOUNDARY_SIZE
   */
  public double BOUNDARY_EFFECT_SIZE = 1;

  public static final int GENERATE_TEXTURE_REGION = 0;


  public static final int GENERATE_COLOR_HIST_TEXTURE_REGION = 1;

  public int GENERATE_REGION_TYPE = GENERATE_COLOR_HIST_TEXTURE_REGION;

  public int RESAMPLER = Resampler.NEAREST_RESAMPLER;

  public int    REUSE  = DIU.INPUT_NONE;
  public int    OUTPUT = DOU.OUTPUT_NONE;
  public boolean SKIP_EXISTING = false;

  public int    LOG_LEVEL = 3;

  public boolean DEMO_NORMALIZATION = false;  // normalized features
  public boolean DEMO_TEXTURE_SEG_BEFORE_FILTER = false;
  public boolean DEMO_TEXTURE_SEG_AFTER_FILTER = false;
  public boolean DEMO_COLOR_PATTERN_SEG = false;
  public boolean DEMO_ONE_BY_ONE = false;
  /**
   * Demo the clustering result within each color based segmentation.
   */
  public boolean DEMO_CLUSTERING_WITHIN_COLOR_SEG = false;
  /**
   * Demo the resultant regions with its Gabors
   */
  public boolean DEMO_REGIONS = false;
  public boolean DEMO_TEXTURE_REGIONS = true;  // final result

  public String[] getParameterNames() {
    return new String[] {
      "DILATION_DISK_SIZE",
      "FILTER_THRESHOLD",
      "COMBINE_THRESHOLD",
      "GABOR_DATA_FEED",
      "NORMALIZE_METHOD",
      "FIXED_NORMALIZE_SCALE",
      "KMEANS_K",
      "MINIMUM_REGION_SIZE",
      "BOUNDARY_EFFECT_SIZE",
      "RESAMPLER",
      "LOG_LEVEL",
      "REUSE",
      "OUTPUT",
      "SKIP_EXISTING",
      "DEMO_NORMALIZATION",
      "DEMO_TEXTURE_SEG_BEFORE_FILTER",
      "DEMO_TEXTURE_SEG_AFTER_FILTER",
      "DEMO_COLOR_PATTERN_SEG",
      "DEMO_CLUSTERING_WITHIN_COLOR_SEG",
      "DEMO_REGIONS",
      "DEMO_TEXTURE_REGIONS",
      "DEMO_ONE_BY_ONE"
    };
  }

  public ColorGuidedTextureSeg() {
    Configuration.configParameters(this);
  }


  public Object reuse(int reuse_level) {
    if (reuse_level != DIU.INPUT_NONE) {
      try {
        DS_GaborTextureRegions rst = (DS_GaborTextureRegions)DIU.in(
            reuse_level,
            DOU.shortName(this.getClass().getName()),
            "DS_GaborTextureRegions",
            Class.forName("yi.iu.processor.texture.reg.DS_GaborTextureRegions"));
        return rst;
      }
      catch (Exception e) {
        LogHandler.log(this, LOG_LEVEL, 1, "Can't Reuse DS_GaborTextureRegions:\n" + e);
      }
    }
    return null;
  }

  void output(int output_level, DS_GaborTextureRegions ds){
    if (output_level != DOU.OUTPUT_NONE) {
      try {
        DOU.out(
            output_level,
            DOU.shortName(this.getClass().getName()),
            "DS_GaborTextureRegions",
            ds);
      } catch (DataOutputException e) {
        LogHandler.log(this, LOG_LEVEL, 1, e.toString());
      }
    }
  }

  public Object reusePolygonRegionTextureStat(int reuse_level) {
    if (reuse_level != DIU.INPUT_NONE) {
      try {
        DS_PolygonRegionTextureStat rst = (DS_PolygonRegionTextureStat)DIU.in(
            reuse_level,
            DOU.shortName(this.getClass().getName()),
            "DS_PolygonRegionTextureStat",
            Class.forName("yi.iu.processor.texture.reg.DS_PolygonRegionTextureStat"));
        return rst;
      }
      catch (Exception e) {
        LogHandler.log(this, LOG_LEVEL, 1, "Can't Reuse DS_PolygonRegionTextureStat:\n" + e);
      }
    }
    return null;
  }

  void outputPolygonRegionTextureStat(int output_level, DS_PolygonRegionTextureStat ds){
    if (output_level != DOU.OUTPUT_NONE) {
      try {
        DOU.out(
            output_level,
            DOU.shortName(this.getClass().getName()),
            "DS_PolygonRegionTextureStat",
            ds);
      } catch (DataOutputException e) {
        LogHandler.log(this, LOG_LEVEL, 1, e.toString());
      }
    }
  }

  public int[] getLabels(int[] pixels, int width, int height) {


    // use processor ColorPatternSeg to roughly segment the image to patterned
    // color regions
    ColorPatternSeg color_seg = new ColorPatternSeg();
    color_seg.DILATION_DISK_SIZE = DILATION_DISK_SIZE;
    color_seg.COMBINE_THRESHOLD = COMBINE_THRESHOLD;
    DS_ColorPatternRegions color_regions = color_seg.getReusableColorPatternRegions(pixels, width, height);
    int[] color_region_labels = color_regions.getLabels();

    if (DEMO_COLOR_PATTERN_SEG) {
      int[] region_img = yi.util.ImageUtil.makeNewImageFromLabel(color_region_labels);
      String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
      yi.util.io.OutputResult.outputGIFImage(img_name + "_patterned_color_regions.gif", region_img, width, height);
    }

    // use texture segmentation
    int color_region_num = yi.stat.StatUtil.maximum(color_region_labels) + 1;
    Clusters texture_segs = textureSegOnRegions(
      pixels, color_region_labels, color_region_num, width, height);
    int[] texture_region_labels = texture_segs.getClusterMap();
    double[][] texture_means = texture_segs.getClusterMeans();

    // demo the regions after texture segmentation
    if (DEMO_TEXTURE_SEG_BEFORE_FILTER) {
      int[] region_img = yi.util.ImageUtil.makeNewImageFromLabel(texture_region_labels);
      String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
      yi.util.io.OutputResult.outputGIFImage(img_name + "_before_filter.gif", region_img, width, height);
    }

    // find the connected components (do some pre-processing)
    ConnectedRegionGenerator crg = new ConnectedRegionGenerator();
    crg.DILATION_DISK_SIZE = DILATION_DISK_SIZE;
    crg.FILTER_THRESHOLD = FILTER_THRESHOLD;
    crg.GAUSSIAN_THETA = GAUSSIAN_THETA;
    crg.MINIMUM_REGION_SIZE = MINIMUM_REGION_SIZE;
    int[] filtered_label = crg.getConnectedRegions(
      texture_region_labels, width, height, ConnectedRegionGenerator.PRE_SMOOTH_NONE, true);
      // ignore label #0 pixels since they are unclustered ones.

    // demo the regions after filtering
    if (DEMO_TEXTURE_SEG_AFTER_FILTER) {
      int[] region_img = yi.util.ImageUtil.makeNewImageFromLabel(filtered_label);
      String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
      yi.util.io.OutputResult.outputGIFImage(img_name + "_filtered.gif", region_img, width, height);
    }

    return filtered_label;

  }


  /**
   * Using color information to guide texture segmentation.  Steps:
   * 1. invokes yi.iu.processor.texture.reg.ColorPatternSeg to roughly segment
   *    the images to patterned color regions;
   * 2. within each region, do texture segmenation;
   * 3. (roughly) connected component analysis and filtering.
   * @param pixels image pixels
   * @param width image width
   * @param height image height
   * @return DS_GaborTextureRegions having all the GaborTextureRegions and the
   *         labels for each pixel.  The un-clustering pixels have been removed.
   */
  public DS_GaborTextureRegions getTextureRegions(int[] pixels, int width, int height) {


    int[] filtered_label = getLabels(pixels, width, height);
    int filtered_label_num = yi.stat.StatUtil.maximum(filtered_label) + 1;

    // construct the results
    GaborTextureRegion[] texture_regions = new GaborTextureRegion[filtered_label_num - 1];
    Region[] r = Region.getRegions(filtered_label, filtered_label_num, new Dimension(width, height));

//    double[][] means_array = Region.getMeans(filtered_label, m_features, Gabor.numOrients*Gabor.numScales);
    // use median instead of mean
    // since the points on the boundary are not stable and they will bias the
    // the mean.  on the other hand, median is less sensitive.
    double[][] means_array = Region.getMedians(filtered_label, m_features, Gabor.numOrients*Gabor.numScales);
    int[][] coarse_color_hists = null;
    int[][] color_hists = null;
    int[] mean_colors = null;
    if (GENERATE_REGION_TYPE == GENERATE_COLOR_HIST_TEXTURE_REGION) {
      // label statistics
      SimpleHistogram hist = new SimpleHistogram(filtered_label);
      coarse_color_hists = getCoarseColorHistogram(pixels, filtered_label, filtered_label_num);
      color_hists = getColorHistogram(pixels, filtered_label, filtered_label_num);
      mean_colors = yi.iu.processor.color.reg.ColorRegionUtil.getMeanColors(pixels, filtered_label, 1, 0); // 0 as background
    }
    // !! the background is useless, so regions #0 is useless
    for (int i=1; i<filtered_label_num; i++) {
      double[] gabor = means_array[i];
      if (GENERATE_REGION_TYPE == GENERATE_COLOR_HIST_TEXTURE_REGION) {
        texture_regions[i-1] = new ChromaticGaborTextureRegion(gabor, r[i], coarse_color_hists[i], color_hists[i], mean_colors[i]);
      } else {
        texture_regions[i-1] = new GaborTextureRegion(gabor, r[i]);
      }
    }
    // demo each region
    if (DEMO_REGIONS) {
      for (int i=1; i<filtered_label_num; i++) {
        int[] the_region = new int[filtered_label.length];
        for (int j=0;j<the_region.length;j++) the_region[j] = (filtered_label[j]==i)?1:0;

        yi.util.ImageUtil.makeImageFromBinary(the_region);

        BufferedImage bimg = new BufferedImage(width, height,
                      BufferedImage.TYPE_INT_ARGB);
        bimg.setRGB(0, 0, width, height, the_region, 0, width);
        Graphics2D g = (Graphics2D)(bimg.getGraphics());
        g.setColor(java.awt.Color.red);
        double[] gabor =  means_array[i];
        int count = 0;
        for (int j=0; j<Gabor.numScales; j++) {
          StringBuffer str = new StringBuffer();
          for (int k=0; k<Gabor.numOrients; k++, count++) {
            if (k!=0) str.append(',');
            str.append(gabor[count]);
          }
          g.drawString(str.toString(), 0, j*20 + 20);
        }
        int color = mean_colors[i];
        int[] rgb = ColorUtil.RGB2IntArray(color);
        String rgb_str = "(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
        g.drawString(rgb_str, 0, 140);
        bimg.getRGB(0, 0, width, height, the_region, 0, width);
        String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
        yi.util.io.OutputResult.outputGIFImage(img_name + ".rgn" + i + ".gif", the_region, width, height);

      }
    }
//    int rng1_num = 0;
//    for (int j=0;j<filtered_label.length;j++) if (filtered_label[j]==1) rng1_num ++;
//    double[] rng1 = new double[rng1_num];
//    rng1_num = 0;
//    for (int j=0;j<filtered_label.length;j++) {
//      if (filtered_label[j]==1) {
//        rng1[rng1_num] = m_features[j][0];
//        rng1_num ++;
//      }
//    }
//    int[] rng1_img = new int[filtered_label.length];
//    for (int j=0;j<filtered_label.length;j++) {
//      if (filtered_label[j]==1) {
//        rng1_img[j] = (int)(m_features[j][0] * 8);
//      }
//    }
//    OutputResult.print("region_1", rng1);
//    yi.util.ImageUtil.makeImageFromGray(rng1_img);
//    OutputResult.outputGIFImage("region_1.gif", rng1_img, width, height);

//    endTime = System.currentTimeMillis();
//    System.out.println("construct results running time: " + (endTime-startTime));

    return new DS_GaborTextureRegions(width, height, 1, texture_regions, filtered_label);
  }

  public DS_PolygonRegionTextureStat getPolygonRegionTextureStat(int[] pixels, int width, int height) {


    int[] filtered_label = getLabels(pixels, width, height);
    int filtered_label_num = yi.stat.StatUtil.maximum(filtered_label) + 1;

    return new DS_PolygonRegionTextureStat(m_features, filtered_label, width, height, true);

  }

  public DS_PolygonRegionTextureStat getReusablePolygonRegionTextureStat(int[] pixels, int width, int height, double scale) {

      DS_PolygonRegionTextureStat rst = (DS_PolygonRegionTextureStat)reusePolygonRegionTextureStat(REUSE);

      if (rst == null) {
        rst = getPolygonRegionTextureStat(pixels, width, height);
        rst.setScale(scale);
      }

      outputPolygonRegionTextureStat(OUTPUT, rst);

      return rst;

  }

  public Object reuseTextureLabeledRgnFts(int reuse_level) {
    if (reuse_level != DIU.INPUT_NONE) {
      try {
        TextureLabeledRgnFts rst = (TextureLabeledRgnFts)DIU.in(
            reuse_level,
            DOU.shortName(this.getClass().getName()),
            "TextureLabeledRgnFts",
            Class.forName("yi.iu.processor.texture.reg.TextureLabeledRgnFts"));
        return rst;
      }
      catch (Exception e) {
        LogHandler.log(this, LOG_LEVEL, 1, "Can't Reuse TextureLabeledRgnFts:\n" + e);
      }
    }
    return null;
  }

  public Object reuseTextureLabRgnFtsPxlVal(int reuse_level) {
    if (reuse_level != DIU.INPUT_NONE) {
      try {
        TextureLabRgnFtsPxlVal rst = (TextureLabRgnFtsPxlVal)DIU.in(
            reuse_level,
            DOU.shortName(this.getClass().getName()),
            "TextureLabRgnFtsPxlVal",
            Class.forName("yi.iu.processor.texture.reg.TextureLabRgnFtsPxlVal"));
        return rst;
      }
      catch (Exception e) {
        LogHandler.log(this, LOG_LEVEL, 1, "Can't Reuse TextureLabRgnFtsPxlVal:\n" + e);
      }
    }
    return null;
  }

  void outputTextureLabeledRgnFts(int output_level, TextureLabeledRgnFts ds){
    if (output_level != DOU.OUTPUT_NONE) {
      try {
        DOU.out(
            output_level,
            DOU.shortName(this.getClass().getName()),
            "TextureLabeledRgnFts",
            ds);
      } catch (DataOutputException e) {
        LogHandler.log(this, LOG_LEVEL, 1, e.toString());
      }
    }
  }

  void outputTextureLabRgnFtsPxlVal(int output_level, TextureLabRgnFtsPxlVal ds){
    if (output_level != DOU.OUTPUT_NONE) {
      try {
        DOU.out(
            output_level,
            DOU.shortName(this.getClass().getName()),
            "TextureLabRgnFtsPxlVal",
            ds);
      } catch (DataOutputException e) {
        LogHandler.log(this, LOG_LEVEL, 1, e.toString());
      }
    }
  }

  public TextureLabeledRgnFts getTextureLabeledRgnFts(int[] pixels, int width, int height,
      int old_w, int old_h, int rst_w, int rst_h) {

    int[] filtered_label = getLabels(pixels, width, height);
    int filtered_label_num = yi.stat.StatUtil.maximum(filtered_label) + 1;

    TextureLabeledRgnFts ds = new TextureLabeledRgnFts(m_features, filtered_label, width, height, old_w, old_h, true, Gabor.numScales * Gabor.numOrients);
    ds.scale(rst_w, rst_h);
    return ds;
  }

  public TextureLabRgnFtsPxlVal getTextureLabRgnFtsPxlVal(int[] pixels, int width, int height,
      int old_w, int old_h, int rst_w, int rst_h) {

    int[] filtered_label = getLabels(pixels, width, height);
    int filtered_label_num = yi.stat.StatUtil.maximum(filtered_label) + 1;


    TextureLabRgnFtsPxlVal ds = new TextureLabRgnFtsPxlVal(m_features, filtered_label, width, height, old_w, old_h, true, Gabor.numScales * Gabor.numOrients);
    ds.scale(rst_w, rst_h);
    return ds;

  }

  public TextureLabeledRgnFts getReusableTextureLabeledRgnFts(int[] pixels, int width, int height,
      int old_w, int old_h, int rst_w, int rst_h) {

      TextureLabeledRgnFts rst = (TextureLabeledRgnFts)reuseTextureLabeledRgnFts(REUSE);

      if (rst == null) {
        rst = getTextureLabeledRgnFts(pixels, width, height, old_w, old_h, rst_w, rst_h);
      }

      outputTextureLabeledRgnFts(OUTPUT, rst);

      return rst;

  }

  public TextureLabRgnFtsPxlVal getReusableTextureLabRgnFtsPxlVal(int[] pixels, int width, int height,
      int old_w, int old_h, int rst_w, int rst_h) {

      TextureLabRgnFtsPxlVal rst = (TextureLabRgnFtsPxlVal)reuseTextureLabRgnFtsPxlVal(REUSE);

      if (rst == null) {
        rst = getTextureLabRgnFtsPxlVal(pixels, width, height, old_w, old_h, rst_w, rst_h);
      }

      outputTextureLabRgnFtsPxlVal(OUTPUT, rst);

      return rst;

  }

  public DS_GaborTextureRegions getReusableTextureRegions(int[] pixels, int width, int height, double scale) {

      DS_GaborTextureRegions rst = (DS_GaborTextureRegions)reuse(REUSE);

      if (rst == null) {
        rst = getTextureRegions(pixels, width, height);
        rst.setScale(scale);
      }

      output(OUTPUT, rst);

      return rst;

  }

  static final double FIRST_SIXTH = 1.0/6;
  static final int HUE_LEVEL = 12;
  static final int GRAY_LEVEL = 4;
  /**
   * Hue takes [0, HUE_LEVEL), gray takes [HUE_LEVEL, HUE_LEVEL + GRAY_LEVEL)
   * @param rgb
   * @return
   */
  int quantizeRgb(int rgb) {
    int[] rgb_array = yi.iu.processor.color.ColorUtil.RGB2IntArray(rgb);
    float[] hsb = Color.RGBtoHSB(rgb_array[0], rgb_array[1], rgb_array[2], null);
    float h=hsb[0],s=hsb[1],v=hsb[2];
    if (s < FIRST_SIXTH) {
      int index = (int)(v * GRAY_LEVEL);
      if (index >= GRAY_LEVEL) index = GRAY_LEVEL - 1;
      return index + HUE_LEVEL;
    }
    int index_h = (int)(h*HUE_LEVEL);
    if (index_h >= HUE_LEVEL) index_h = HUE_LEVEL - 1;
    return index_h;

  }

  int[][] getCoarseColorHistogram(int[] rgbs, int[] labels, int label_num) {
    int[][] hists = new int[label_num][16];
    for (int i=0; i<rgbs.length; i++) {
      int bin = quantizeRgb(rgbs[i]);
      hists[labels[i]][bin] ++;
    }
    return hists;
  }

  int[][] getColorHistogram(int[] rgbs, int[] labels, int label_num) {
    int[][] hists = new int[label_num][SmithColorQuantization.MAX_LABEL + 1];
    for (int i=0; i<rgbs.length; i++) {
      int bin = SmithColorQuantization.quantize(rgbs[i]);
      hists[labels[i]][bin] ++;
    }
    return hists;
  }

  int[] getLuminance(int[] pixels, int width, int height) {
    return CYMaker.getLuminance(pixels, new Dimension(width, height));
  }

  int[] getHSIAverage(int[] pixels, int width, int height) {
    int[][] hsi = CYMaker.getHSI(pixels, new Dimension(width, height));
    int[] average = new int[pixels.length];
    for (int i=0; i<average.length; i++) {
      average[i] = (int)Math.round((hsi[i][0] + hsi[i][1] + hsi[i][2]) / 3.0);
    }
    return average;
  }

  int[] getMistaken(int[] pixels, int width, int height) {
    pixels = getLuminance(pixels, width, height);
    return getHSIAverage(pixels, width, height);
  }

  DoubleMatrix calculateGabor(int[] pixels, int width, int height){
    Dimension d = new Dimension(width, height);

    // transform the 3D rgb colors to acceptable 1D data to gabor process
    int[] pixels_1D;
    if (GABOR_DATA_FEED == GABOR_USE_LUMINANCE) {
      pixels_1D = getLuminance(pixels, width, height);
    }
    else if (GABOR_DATA_FEED == GABOR_USE_HSI_AVERAGE) {
      pixels_1D = getHSIAverage(pixels, width, height);
    }
    else {  // GABOR_DATA_FEED == GABOR_USE_MISTAKE
      pixels_1D =getMistaken(pixels, width, height);
    }

    if (PRE_GABOR_SMOOTH) {
      GaussianSmooth smoother = new GaussianSmooth(PRE_GABOR_SMOOTH_THETA);
      pixels_1D = smoother.getProcessed(pixels_1D, new Dimension(width, height));
      int[] smoothed_img = yi.util.ImageUtil.makeNewImageFromGray(pixels_1D);
      String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
      yi.util.io.OutputResult.outputGIFImage(img_name + ".smoothed.gif", smoothed_img, width, height);
    }

    // prepare the data to feed into Gabor: change it to matrix and change the
    // data type from integer to double
    double [][] data_2D = new double[height][width];
    int count = 0;
    for (int y=0; y<height; y++) {
      for (int x=0; x<width; x++, count++) {
        data_2D[y][x] = pixels_1D[count];
      }
    }
    DoubleMatrix image = new DoubleMatrix(height, width, data_2D);

    // compute texture features

//    // Use yi's code
//    Gabor.numOrients = 4; Gabor.numScales = 4;
//    DoubleMatrix features = Gabor.getFiltered(image, Gabor.numScales, Gabor.numOrients);

    // Use Selim's code
    DoubleMatrix features = (new Gabor()).getReusableGaborRst(image, Gabor.numScales, Gabor.numOrients);

    m_features = features.data;

    return features;
  }

  /**
   * Do texture based segmentation on each pre-defined region.  Label 0 will be
   * considered as background and so will be ignored.
   * @param pixels
   * @param region_label
   * @param label_num region number.  The the values of labels are [0..label_num)
   * @param width
   * @param height
   * @return
   */
  Clusters textureSegOnRegions(int[] pixels, int[] labels, int label_num, int width, int height) {

    // calculate Gabor
    DoubleMatrix features = calculateGabor(pixels, width, height);

    // save the Gabor result


    int boundary_effect_size = (int)Math.round(Gabor.maskSize*BOUNDARY_EFFECT_SIZE);

//    // remove the boundary effect
//    for (int i=0; i<features.width; i++) {
//      for (int y = 0; y < height; y++) {
//        for (int x = 0; x < boundary_effect_size; x++)
//          features.data[y*width+x][i] = 0;
//        for (int x = width - boundary_effect_size; x < width; x++)
//          features.data[y*width+x][i] = 0;
//      }
//      for (int x = 0; x < width; x++) {
//        for (int y = 0; y < boundary_effect_size; y++)
//          features.data[y*width+x][i] = 0;
//        for (int y = height - boundary_effect_size; y < height; y++)
//          features.data[y*width+x][i] = 0;
//      }
//    }

    // remove the boundary

    // smooth the gabor responses a little bit
//    GaussianSmooth smooth = new GaussianSmooth(3);
//    for (int i=0; i<features.width; i++) {
//      double[] oned_data = new double[features.height];
//      for (int j=0; j<features.height; j++) oned_data[j] = features.data[j][i];
//      double[] smoothed = smooth.getProcessed(oned_data, width, height);
//      for (int j=0; j<features.height; j++) features.data[j][i] = smoothed[j];
//    }

//

//    setBoundary(labels, width, height, boundary_effect_size, 0);

//    // smooth each pixel in (SMOOTH_WIDTH) x (SMOOTH_HEIGHT) block
//    double[] dat = new double[features.height];
//    for (int i = 0; i < features.width; i++) { // for each dim
//      for (int j = 0; j < features.height; j++) {
//        dat[j] = features.data[j][i];
//      }
//      double[] smoothed = Average.average(dat, labels, width, height, 11,
//                                          11);
//      for (int j = 0; j < features.height; j++) {
//        features.data[j][i] = smoothed[j];
//      }
//    }
//


    int[][] int_features_data = prepareDataForClustering(features);

//    yi.iu.ui.InfoFrame frame2 = new yi.iu.ui.InfoFrame("gabors rounded", pixels, width, height, int_features_data);
//    frame2.show();

    setBoundary(labels, width, height, boundary_effect_size, 0);

    yi.util.label.Label.sequential(labels, null, true);
    label_num = yi.stat.StatUtil.maximum(labels) + 1;


//    int[][] rounded_features_data = new int[ features.height ][ features.width ];
//    for( int c = 0; c < features.width; c++ ) {
//      for (int r = 0; r < features.height; r++) {
//        rounded_features_data[r][c] = (int) Math.round(features.data[r][c]);
//      }
//    }


//    yi.iu.ui.InfoFrame frame = new yi.iu.ui.InfoFrame("gabors", pixels, width, height, int_features_data);
//    frame.show();

    // clustering on each region
    int seg_num = 1;  // region #0 is igored since it is background
    int pnum = labels.length;
    int[] seg_rst = new int[pnum]; // to record the result

    for (int i = 1; i < label_num; i++) {  // deal with the labels one by one
//      System.out.println("region_number = " + seg_num);
      // construct the binary image corresponding one label
      int[] mask = new int[pnum];
      for (int j=0; j<pnum; j++) mask[j] = (labels[j]==i) ? 1 : MASKED;

      // process on the binary image
      Clusters clusters = getTextureSegWithMask(int_features_data, features.height, features.width, mask);
//      Clusters clusters = getTextureSegWithMask_eliminateBoundary(int_features_data, features.height, features.width, mask, width, height);

//      System.out.println("region #" + i + " is clustered to " + clusters.getClusterNum() + " clusters");
      int[] segs = clusters.getClusterMap();

      // gether the result on this label into the returning result
      for (int j=0; j<pnum; j++) {
        // in returned binary, 0 is counted as one region
        if (labels[j]==i) seg_rst[j] = seg_num + segs[j];
      }

      // remember the means
      // the means are not that useful since they may have been normalized
      int num_clusters = clusters.getClusterNum();

      // increase the number of current segments
      seg_num += num_clusters;
    }

    // recalculate the means
//    // to save the running time, disable this calculation, actually the results are useless anyway
//    // since the segmented region will be filtered (merge + connected components)
//    double[][] means_array = Region.getMeans(seg_rst, features.data, Gabor.numOrients*Gabor.numScales);
    double[][] means_array = null;
    return new Clusters(seg_num, features.width, means_array, pixels.length, seg_rst);
  }

  /**
   * Set values on the boundary
   */
  void setBoundary(int[] labels, int width, int height, int boundary_effect_size, int value) {
    for (int y=0; y<height; y++) {
      for (int x=0; x<boundary_effect_size; x++) labels[y*width+x] =value;
      for (int x=width-boundary_effect_size; x<width; x++) labels[y*width+x] =value;
    }
    for (int x=0; x<width; x++) {
      for (int y=0; y<boundary_effect_size; y++) labels[y*width+x] =value;
      for (int y=height-boundary_effect_size; y<height; y++) labels[y*width+x] =value;
    }
  }

  /**
   * Set values on the boundary
   */
  void setBoundary(double[] labels, int width, int height, int boundary_effect_size, double value) {
    for (int y=0; y<height; y++) {
      for (int x=0; x<boundary_effect_size; x++) labels[y*width+x] =value;
      for (int x=width-boundary_effect_size; x<width; x++) labels[y*width+x] =value;
    }
    for (int x=0; x<width; x++) {
      for (int y=0; y<boundary_effect_size; y++) labels[y*width+x] =value;
      for (int y=height-boundary_effect_size; y<height; y++) labels[y*width+x] =value;
    }
  }

  /**
   * The idea is within each patterned color region, remove the pixels on the
   * boundary before do texture based segmentation since those pixels on
   * boundary are not that reliable.  This one is not finished yet.  One reason
   * to discard this idea is that some region will be totally discarded because
   * they are not big enough.
   * @param features
   * @param data_len
   * @param feature_num
   * @param mask
   * @param width
   * @param height
   * @return
   * @deprecated
   */
  Clusters getTextureSegWithMask_eliminateBoundary(
             int[][] features, int data_len, int feature_num, int[] mask, int width, int height) {
    int erosion_size = (int)(Gabor.maskSize * BOUNDARY_EFFECT_SIZE);
    Erosion erosion = new Erosion(Erosion.DISK, erosion_size);
    // normalized mask to 0/255
    int[] normalized_mask = new int[mask.length];
    for (int i=0; i<mask.length; i++) {
      normalized_mask[i] = (mask[i] == MASKED)?0:255;

    }
    // check the shrinked size
    int shrinked_mask_size = 0;
    for (int i=0; i<normalized_mask.length; i++) {
      if (normalized_mask[i] != MASKED) shrinked_mask_size++;
    }
    System.out.println("shrinked mask size = " + shrinked_mask_size);
    if (shrinked_mask_size ==0) return new Clusters(0, feature_num, (double[][])null, data_len, new int[features.length]);

    int[] shrinked_mask = erosion.getProcessed(mask, new Dimension(width,height));

    // demo the erosion
    int[] demo_img = yi.util.ImageUtil.makeNewImageFromBinary(mask);
    OutputResult.outputGIFImage("before_erosion", demo_img, width, height, true);
    demo_img = yi.util.ImageUtil.makeNewImageFromBinary(shrinked_mask);
    OutputResult.outputGIFImage("after_erosion", demo_img, width, height, true);

    return getTextureSegWithMask(features, data_len, feature_num, shrinked_mask);
  }

  /**
   * Texture segmentation on a region within an image.  The region is marked
   * in the parameter mask.  In the returned pixels labels, the masked pixels
   * are remained as 0; the unmasked pixels will be labeled from 0.  It is OK
   * that the masked pixels and the region #0 pixels are all labeled as 0 since
   * the caller know the pixels masked.
   * @param features the feature vectors
   * @param data_len the length of the features
   * @param feature_num the dimension of feature data
   * @param mask the mask
   * @return clusters within the unmasked data
   */
  Clusters getTextureSegWithMask(int[][] features, int data_len, int feature_num, int[] mask) {

    // get the un_masked_num
    int un_masked_num = 0;
    for (int i=0; i<mask.length; i++) {
      if (mask[i] != MASKED) un_masked_num++;
    }
//    System.out.println("unmasked number = " + un_masked_num);

    // construct the data for clustering (eliminate those masked data)
    // also set up a map to the original data
    int[] map = new int[un_masked_num];
    int[][] unmasked_features = new int[un_masked_num][];
    int count = 0;
    for (int i=0; i<data_len; i++) {
      if (mask[i] == MASKED) continue;
      map[count] = i;
      unmasked_features[count] = features[i];
      count ++;
    }

//    if (LOCAL_NORMALIZE) {
//      // normalized the data to [0, 255]
//      int[][] normalized_data = new int[ un_masked_num ][ feature_num ];
//      for( int c = 0; c < feature_num; c++ ) {
//        double max_value = -1;
//        for(int r = 0; r < un_masked_num; r++ ) {
//          if( unmasked_features[ r ][ c ] > max_value )
//            max_value = unmasked_features[ r ][ c ];
//        }
//        //scale the double type feature values to the
//        //0-255 range because K-Means uses integer values
//        for(int r = 0; r < un_masked_num; r++ )
//          normalized_data[ r ][ c ] = (int) Math.round( 255.0 * unmasked_features[ r ][ c ] / max_value );
//      }
//      unmasked_features = normalized_data;
//    }
    // normalization within all the dimensions and within the region
    if (NORMALIZE_METHOD == REGION_GLOBAL_NORMALIZE) {
      unmasked_features = yi.stat.StatUtil.normalize(unmasked_features, 255);
    } else if  (NORMALIZE_METHOD == REGION_SCALE_NORMALIZE) {
      normalizeOnEachScale(unmasked_features);
    }

    // demo normalized feature to be clustered
    if (DEMO_NORMALIZATION) {
      Dimension d = yi.util.ImageUtil.getDimension();
      for (int j=0; j<unmasked_features[0].length; j++) {
        int[] normalized = new int[data_len];
        Arrays.fill(normalized, 0xff0000ff);
        for (int i=0; i<un_masked_num; i++) {
          normalized[map[i]] = yi.iu.processor.color.ColorUtil.packGray(unmasked_features[i][j]);
        }
        OutputResult.outputColorImage("normalized_feature", normalized, d.width, d.height, true);
      }
    }


    // k-means clustering
//    KMCluster cluster = new KMCluster(KMEANS_K, KMCluster.LINEAR, unmasked_features, null);
    int[][] init_means = new int[5][feature_num];
    Arrays.fill(init_means[0], 0);
    Arrays.fill(init_means[4], 32);
    Arrays.fill(init_means[1], 64);
    Arrays.fill(init_means[2], 128);
    Arrays.fill(init_means[3], 256);
    KMCluster cluster = new KMCluster(init_means, unmasked_features);
    Clusters clusters = cluster.clustering();
    int[] seg_labels = clusters.getClusterMap();

    // map inversely
    int[] segs = new int[data_len];
    for (int i=0; i<un_masked_num; i++) {
      segs[map[i]] = seg_labels[i];
    }

    // demo cluster results
    if (DEMO_CLUSTERING_WITHIN_COLOR_SEG) {
      Dimension d = yi.util.ImageUtil.getDimension();
      int[] segs_label = new int[mask.length];
      for (int i=0; i<mask.length; i++) {
        segs_label[i] = (mask[i] == MASKED)?0:segs[i]+1;
      }
      int[] segs_demo = yi.util.ImageUtil.makeNewImageFromLabel(segs_label);
      String img_name = Configuration.getStringParameter("SOURCE_NAME", "unknown");
      OutputResult.outputGIFImage(img_name + "color_seg_clst", segs_demo, d.width, d.height, true);
    }

    return new Clusters(clusters.getClusterNum(), feature_num, clusters.getClusterMeans(), data_len, segs);
  }

  void normalizeOnEachScale(int[][] data) {

    int count = 0;
    int count2 = 0;

    for (int s=0; s<Gabor.numScales; s++) {
      System.out.println("scale= " + s);

      // get the max for each scale
      int max = 0;
      for (int o=0; o<Gabor.numOrients; o++, count++) {
        System.out.println("find max, use " + count);

        for (int i=0; i<data.length; i++) {
          max = Math.max(max, data[i][count]);
        }
      }

      // normalize using the biggest one within the scale
      for (int o=0; o<Gabor.numOrients; o++, count2++) {
        System.out.println("normalize on" + count2 + " max = " + max);
        for (int i=0; i<data.length; i++) {
          data[i][count2] = yi.stat.StatUtil.normalize(data[i][count2], 0, max, 255);
        }
      }
    }
  }

  int[][] prepareDataForClustering(DoubleMatrix features) {
    // convert double Gabor feature data to integer type since K-Means uses only
    // integer values
    int[][] int_features_data = new int[ features.height ][ features.width ];
    for( int c = 0; c < features.width; c++ ) {
//      // find the max on column c
//      double max_value = -1;
//      for(int r = 0; r < features.height; r++ ) {
//        if( features.data[ r ][ c ] > max_value ) max_value = features.data[ r ][ c ];
//      }
//      System.out.println("max on column " + c + " = " + max_value);
//
//      // scale the double type feature values to the 0-255 range because K-Means
//      // uses integer values
      for(int r = 0; r < features.height; r++ ) {
//        if (GLOBAL_NORMALIZE) {
//          int_features_data[r][c] = (int) Math.round(255.0 * features.data[r][c] / max_value);
//        }
//        else {
          if (NORMALIZE_METHOD == FIXED_NORMALIZE) {
            int_features_data[r][c] = Math.min((int) Math.round(features.data[r][c] * FIXED_NORMALIZE_SCALE), 255) ;
          } else {
            int_features_data[r][c] = (int) Math.round(features.data[r][c] * 255) ;
          }

        }
//      }
    }

//    for (int c=0; c<features.width; c++) {
//      int[] data = new int[features.height];
//      for (int r= 0; r < features.height; r++ ) {
//        data[r] = int_features_data[r][c];
//      }
//      OutputResult.print("mask"+c,data, width, height);
//      yi.util.ImageUtil.makeImageFromGray(data);
//      OutputResult.outputGIFImage("mask"+c+".gif",data, width, height);
//    }
    if (NORMALIZE_METHOD == GLOBAL_NORMALIZE) {
//      int minimum  = yi.stat.StatUtil.minimum(int_features_data), maximum = yi.stat.StatUtil.maximum(int_features_data);
//      System.out.println("min=" + minimum +",max=" + maximum);
      int_features_data = yi.stat.StatUtil.normalize(int_features_data, 255);
    } else if (NORMALIZE_METHOD == SCALE_NORMALIZE) {
      normalizeOnEachScale(int_features_data);
    }

    if (DEMO_NORMALIZATION && (NORMALIZE_METHOD==GLOBAL_NORMALIZE || NORMALIZE_METHOD==SCALE_NORMALIZE || NORMALIZE_METHOD ==FIXED_NORMALIZE)) {

      Dimension d = yi.util.ImageUtil.getDimension();
      for (int j=0; j<int_features_data[0].length; j++) {
        int[] normalized = new int[int_features_data.length];
        for (int i=0; i<int_features_data.length; i++) {
          normalized[i] = yi.iu.processor.color.ColorUtil.packGray(int_features_data[i][j]);
        }
        OutputResult.outputGIFImage("global_normalized_feature." + yi.util.data.Conversion.toString(j, 2) + ".gif", normalized, d.width, d.height, false);
      }
    }


    return int_features_data;
  }


  public static void main(String[] args) {
    // init configuration
    Configuration.init(args);

    // get the image name
    String img_name = ReadArgu.getOption('i', args);
    if (img_name == null) {
      System.err.println("Usage: -i image_name");
      System.exit(1);
    }

    // read in image
    // get the scale
    String scale_str = ReadArgu.getOption('s', args);
    int scale = 1;
    if (scale_str != null) {
      scale = Integer.parseInt(scale_str);
    }

    // read in image
    Image image = yi.util.io.ImageReader.JAIReadImage(img_name);
    Dimension d = new Dimension();
    int[] pixels = yi.util.io.ImageReader.getPixels(image, d);

    // rescale the image if necessary
    double the_scale = 1.0;
    if (scale != 1) {
      ColorGuidedTextureSeg dummy = new ColorGuidedTextureSeg();
      ResampledDescriptor resampled = Resampler.scaleDown(pixels, d,
          dummy.RESAMPLER, scale);
      if (resampled != null) {
        pixels = resampled.m_resampled;
        d = resampled.m_newDimension;
        the_scale = resampled.m_scale;
        img_name = img_name + Resampler.RESIZED_FILE_SUFFIX + scale;
      }
    }

    // process
    try {

      Configuration.setParameter("SOURCE_NAME", img_name);
      yi.util.ImageUtil.setPixels(pixels);
      yi.util.ImageUtil.setDimension(d);

      /** @todo use ColorGuidedTextureSegWPCA for now */
      ColorGuidedTextureSeg processor = new ColorGuidedTextureSeg();
      long startTime = System.currentTimeMillis();
      //DS_GaborTextureRegions ds = processor.getReusableTextureRegions(pixels, d.width, d.height, 1.0);
      DS_PolygonRegionTextureStat ds = processor.getReusablePolygonRegionTextureStat(pixels, d.width, d.height, the_scale);
      long endTime = System.currentTimeMillis();
      System.out.println("running time: " + (endTime-startTime));

      int[] demo = ds.getPolygonDemo(null, true);
      yi.util.io.OutputResult.outputGIFImage(img_name + ".text.reg.gif", demo, d.width,
                                             d.height, false);

      if (processor.DEMO_ONE_BY_ONE) {
        ds.demoRegions(img_name, pixels, -1, 0xffff0000);
      }

//      // demo the results
//      if (cgts.DEMO_TEXTURE_REGIONS) {
//        int[] labels = rst.getLabels();
//        int[] region_img = yi.util.ImageUtil.makeNewImageFromLabel(labels);
//        yi.util.io.OutputResult.outputGIFImage(img_name + "_gabor_regions.gif", region_img, d.width, d.height);
//      }

    } catch (Exception e) {
      System.err.println("broken when processing " + img_name);
      e.printStackTrace();
    }

    System.exit(0);
  }

  public FeatureRepresentable generateFeatureRepresentable(String feature_type, int[] pixels, int width, int height) {
    FeatureRepresentable rst = null;

    int[] filtered_label = getLabels(pixels, width, height);
    int filtered_label_num = yi.stat.StatUtil.maximum(filtered_label) + 1;

    if (feature_type.indexOf("TextureLabRgnFtsPxlVal") >= 0) {
      rst = new TextureLabRgnFtsPxlVal(m_features, filtered_label, width, height, width, height, true, Gabor.numScales * Gabor.numOrients);
    }
    else if (feature_type.indexOf("TextureLabeledRgnFts") >= 0) {
      rst = new TextureLabeledRgnFts(m_features, filtered_label, width, height, width, height, true, Gabor.numScales * Gabor.numOrients);
    }

    return rst;
  }

}