/**
 **
 ** VegetationSegment.java - Segmentation of vegetation regions from multi-spectral images
 **
 ** Copyright (C) 2024-2025 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  VegetationSegment.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.vegetation;

import java.awt.Rectangle;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.QuadCLT;

import ij.ImagePlus;
import ij.gui.GenericDialog;

public class VegetationSegment  implements Serializable {
	private static final long  serialVersionUID = 1L;
	public Rectangle   woi_max;
	public Rectangle   woi;
	public double []   scene_offsets;
	public double [][] tva; // woi_veg
	public double      terrain_offset;
	public double []   confidence; // woi
	public boolean []  overlaid;
	public String      path;
	
	
	public VegetationSegment(
			String      path, 
			Rectangle   woi_max,
			Rectangle   woi,
			double terrain_offset,
			double []   scene_offsets, // has NaNs
			double [][]  tva,
			double []   confidence,
			boolean []  overlaid) {
		this.path = path;
		this.woi = woi;
		this.woi_max = woi_max;
		this.scene_offsets = scene_offsets;
		this.tva =  tva;
		this.terrain_offset = terrain_offset;
		this.confidence = confidence;
		this.overlaid = overlaid;
		correctTerrainOffset();
	}
	
	
	public void correctTerrainOffset() {
		if (!Double.isNaN(terrain_offset) && (overlaid != null)) {
			int woi_length = woi.width * woi.height;
			for (int windx = 0; windx < woi_length; windx++) {
				int wx = windx % woi.width;
				int wy = windx / woi.height;
				int wvindx = (wx + woi.x - woi_max.x) + (wy + woi.y - woi_max.y) * woi_max.width;
				if (overlaid[windx]) {
					tva[0][wvindx] += terrain_offset;
				}
			}
		}
	}
	
	public double [] getSceneOffsets() {
		return scene_offsets;
	}
	
	public Rectangle getWoi() {
		return woi;
	}

	public Rectangle getWoiMax() {
		return woi_max;
	}
	
	
	public static Rectangle getBounds( // [0] - woi, [1] - woi_weg
			VegetationSegment [] segments,
			boolean use_veg) {
		Rectangle bounds = null;
		for (VegetationSegment segment:segments) {
			Rectangle selected_woi = use_veg ?segment.woi_max : segment.woi;
			if (bounds == null) {
				bounds = new Rectangle(selected_woi);
			} else {
				bounds.add(selected_woi);
			}
		}
		return bounds;
	}
	
	public static void combineSegments( // KeepPartial
			VegetationLMA        vegetationLMA,
			VegetationSegment [] segments,
			boolean              crop_combo,
			boolean              keep_partial,
			int                  width,
			final double         um_sigma,
			final double         um_weight,
			final boolean        render_open,      // render open areas (no vegetation offset)
			final boolean        render_no_alpha,  // render where no opacity is available
			final double         alpha_min,        // below - completely transparent vegetation
			final double         alpha_max,        // above - completely opaque
			final double         weight_opaque,    // render through completely opaque vegetation
			final double         boost_parallax,   // increase weights of scenes with high parallax relative to the reference one
			final double         max_parallax,			
			final int            num_exaggerate) { // show amplified difference from filtering
		//FIXME: Handle woi separately!
		final Rectangle combo_woi =     crop_combo? VegetationSegment.getBounds(segments, false):vegetationLMA.getFull();
		final Rectangle combo_woi_veg = crop_combo? VegetationSegment.getBounds(segments, true): vegetationLMA.getFull();
		final Rectangle out_woi = new Rectangle(combo_woi);
		out_woi.add(combo_woi_veg);
		
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		double ksigma = 1.0; 
		double min_sw = 1e-4;
		int out_length = out_woi.width*out_woi.height;
		int accum_indx = segments.length;
		String [] top_titles = {"terrain","vegetation","alpha","elevation","terrain_elevation","confidence","terrain_offset"};
		double [][][] preview_data = new double [top_titles.length][accum_indx+1][out_length]; // Java heap space
		String [] titles = new String[accum_indx+1];
		for (int ns = 0; ns < segments.length; ns++) {
			Rectangle woi =     segments[ns].woi; // for confidence and limits for terrain
			Rectangle woi_veg = segments[ns].woi_max;
			titles[ns] = woi.toString();
			int woi_length =     woi.width * woi.height;  
			int woi_veg_length = woi_veg.width * woi_veg.height;  
			double [][] woi_tva = segments[ns].tva;
			double []   confidence = segments[ns].confidence;
			double      terrain_offset = segments[ns].terrain_offset;
			for (int t = 0; t < top_titles.length; t++) {
				Arrays.fill(preview_data[t][ns], Double.NaN); // java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
				switch (t) {
				case 0: // terrain cut out from woi_tva
					for (int windx = 0; windx < woi_length; windx++) {
						int wx = windx % woi.width;
						int wy = windx / woi.width;
						int wxsrc = wx +  woi.x-woi_veg.x; // >= 0
						int wysrc = wy +  woi.y-woi_veg.y; // >= 0
						int wsrc = wxsrc + wysrc *  woi_veg.width; // index in woi_tva[t][windx];
						int x = wx +  woi.x - out_woi.x;
						int y = wy +  woi.y - out_woi.y;
						int indx = x + y * out_woi.width;
						preview_data[t][ns][indx] = woi_tva[t][wsrc]; // java.lang.ArrayIndexOutOfBoundsException: Index -43 out of bounds for length 550
					}
					break;
				case 1: // vegetation
				case 2: // alpha
				case 3: // elevation
					for (int windx = 0; windx < woi_veg_length; windx++) {
						int x = windx % woi_veg.width + woi_veg.x - out_woi.x;
						int y = windx / woi_veg.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
						preview_data[t][ns][indx] = woi_tva[t][windx];
					}
					break;
				case 4: // terrain elevation
					for (int windx = 0; windx < woi_veg_length; windx++) {
						int x = windx % woi_veg.width + woi_veg.x - out_woi.x;
						int y = windx / woi_veg.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
						preview_data[t][ns][indx] = woi_tva[t][windx]; // skipping terrain elevation average
					}
					break;
				case 5: // confidence
					for (int windx = 0; windx < woi_length; windx++) {
						int x = windx % woi.width + woi_veg.x - out_woi.x;
						int y = windx / woi.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
						preview_data[t][ns][indx] = confidence[windx];
					}
					break;
				case 6: // terrain_offset
					for (int windx = 0; windx < woi_length; windx++) {
						int x = windx % woi.width + woi_veg.x - out_woi.x;
						int y = windx / woi.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
						preview_data[t][ns][indx] = terrain_offset;
					}
					break;
				}
			}
		}
		titles[accum_indx] = "accum";
		double [][] sum_w =  new double [preview_data.length][out_length];
		double [][] sum_wd = new double [preview_data.length][out_length];
		for (int ns = 0; ns < segments.length; ns++) {
			Rectangle woi = segments[ns].woi;
			int woi_length = woi.width*woi.height;
//			double [][] woi_tva = segments[ns].tva;			
			double [] wnd = overlapCosineWindow (woi, width);
			for (int windx = 0; windx < woi_length; windx++) {
				int x = windx % woi.width + woi.x - out_woi.x;
				int y = windx / woi.width + woi.y - out_woi.y;
				int indx = x + y * out_woi.width;
				for (int t = 0; t < preview_data.length; t++) {
					double d = preview_data[t][ns][indx];
					if (!Double.isNaN(d)) {
						double w = wnd[windx];
						sum_w [t][indx] += w;
						sum_wd[t][indx] += w * d ;
					}
				}				
			}
		}
		
		for (int t = 0; t < preview_data.length; t++) {
			for (int i = 0; i < out_length; i++) {
				if (sum_w[t][i] == 0) {
					preview_data[t][accum_indx][i] = Double.NaN;
				} else {
					preview_data[t][accum_indx][i] = sum_wd[t][i]/sum_w[t][i];
				}
			}
		}
		VegetationModel vegetationModel = vegetationLMA.getModel();
		String ref_scene = vegetationModel.reference_scene;
		String pre_title = ref_scene+"-preview_segments-"+out_woi.x+"-"+out_woi.y+"-"+out_woi.width+"-"+out_woi.height;
		if (keep_partial) {
		ShowDoubleFloatArrays.showArraysHyperstack(
				preview_data,     // double[][][] pixels, 
				out_woi.width,    // int          width, 
				pre_title,        // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				titles,           // String []    titles, // all slices*frames titles or just slice titles or null
				top_titles,       // String []    frame_titles, // frame titles or null
				true);            // boolean      show)
		} else {
			double [][] accum_only = new double [preview_data.length][];
			for (int t = 0; t < preview_data.length; t++) {
				accum_only[t] = preview_data[t][accum_indx];
			}
			ShowDoubleFloatArrays.showArrays(
					accum_only,
					out_woi.width,
					out_woi.height,
					true,
					pre_title+"-ACCUM",
					top_titles);
		}
		
		double [] terrain_reference =   crop(
				vegetationModel.terrain_scenes_render[vegetationModel.reference_index],
				vegetationLMA.getFull().width,
				out_woi);
		double [] terrain_accumulated = crop(
				vegetationModel.terrain_average_render,
				vegetationLMA.getFull().width,
				out_woi);
		double [] terrain_um = 	terrain_accumulated.clone();
		for (int i = 0; i < terrain_um.length; i++) {
			if (Double.isNaN(terrain_um[i])) {
				terrain_um[i] = 0;
			}
		}
		VegetationModel.unsharpMask(
				terrain_um,    // final double [] data,
				out_woi.width, // final int       width,
				um_sigma,      // final double    um_sigma,
				um_weight);    // final double    um_weight)
		String [] result_titles = {"ref_render", "terrain_accum",String.format("terrain_accum_UM%5.2f-%4.2f",um_sigma,um_weight),"terrain"};
		double [][] result_img = {terrain_reference,terrain_accumulated,terrain_um,preview_data[0][accum_indx]};
		boolean normalize = true;
		int norm_index = 1;
		if (normalize) {
		result_img =  normalize (
				result_img, // final double [][] data,
				norm_index); // final int         norm_index){
		}
		String res_title = ref_scene+"-result_terrain-"+out_woi.x+"-"+out_woi.y+"-"+out_woi.width+"-"+out_woi.height;
		if (normalize) {
			res_title+= "-normalized"+norm_index;
		}
		ShowDoubleFloatArrays.showArrays(
				result_img,
				out_woi.width,
				out_woi.height,
				true,
				res_title,
				result_titles);
		return;
	}
	
	public static void combineSegments(
			VegetationLMA        vegetationLMA,
			VegetationSegment [] segments,
			boolean              crop_combo,
//			boolean              keep_partial,
			int                  width,
			final double         um_sigma,
			final double         um_weight,
			final boolean        render_open,      // render open areas (no vegetation offset)
			final boolean        render_no_alpha,  // render where no opacity is available
			final double         alpha_min,        // below - completely transparent vegetation
			final double         alpha_max,        // above - completely opaque
			final double         weight_opaque,    // render through completely opaque vegetation
			final double         boost_parallax,   // increase weights of scenes with high parallax relative to the reference one
			final double         max_parallax,			
			final int            num_exaggerate) { // show amplified difference from filtering
		//FIXME: Handle woi separately!
		final Rectangle combo_woi =     crop_combo? VegetationSegment.getBounds(segments, false):vegetationLMA.getFull();
		final Rectangle combo_woi_veg = crop_combo? VegetationSegment.getBounds(segments, true): vegetationLMA.getFull();
		final Rectangle out_woi = new Rectangle(combo_woi);
		out_woi.add(combo_woi_veg);
		
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		double ksigma = 1.0; 
		double min_sw = 1e-4;
		int out_length = out_woi.width*out_woi.height;
		int accum_indx = segments.length;
		String [] top_titles = {"terrain","vegetation","alpha","elevation","terrain_elevation","confidence","terrain_offset"};
//		double [][][] preview_data = new double [top_titles.length][accum_indx+1][out_length]; // Java heap space
		String [] titles = new String[accum_indx+1];
		
		double [][] sum_w =  new double [top_titles.length][out_length];
		double [][] sum_wd = new double [top_titles.length][out_length];
		
		for (int ns = 0; ns < segments.length; ns++) {
			Rectangle woi =     segments[ns].woi; // for confidence and limits for terrain
			Rectangle woi_veg = segments[ns].woi_max;
			titles[ns] = woi.toString();
			int woi_length =     woi.width * woi.height; 
			// creating larger window, still use only the woi for overlapping, discard outside
			int woi_veg_length = woi_veg.width * woi_veg.height;
			double [] wnd = overlapCosineWindow (woi, width);
			double [] wnd_veg = new double [woi_veg_length];
			for (int windx = 0; windx < woi_length; windx++) {
				int wx = windx % woi.width;
				int wy = windx / woi.width;
				int wvx = wx + woi.x - woi_veg.x;
				int wvy = wy + woi.y - woi_veg.y;
				wnd_veg[wvx + wvy * woi_veg.width] = wnd[windx];
			}
			double [][] woi_tva = segments[ns].tva;
			double []   confidence = segments[ns].confidence;
			double      terrain_offset = segments[ns].terrain_offset;
			for (int t = 0; t < top_titles.length; t++) {
//				Arrays.fill(preview_data[t][ns], Double.NaN); // java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
				switch (t) {
				case 0: // terrain cut out from woi_tva
					for (int windx = 0; windx < woi_length; windx++) {
						int wx = windx % woi.width;
						int wy = windx / woi.width;
						int wxsrc = wx +  woi.x-woi_veg.x; // >= 0
						int wysrc = wy +  woi.y-woi_veg.y; // >= 0
						int wsrc = wxsrc + wysrc *  woi_veg.width; // index in woi_tva[t][windx];
						int x = wx +  woi.x - out_woi.x;
						int y = wy +  woi.y - out_woi.y;
						int indx = x + y * out_woi.width;
//						preview_data[t][ns][indx] = woi_tva[t][wsrc]; // java.lang.ArrayIndexOutOfBoundsException: Index -43 out of bounds for length 550
						double d =  woi_tva[t][wsrc];
						if (!Double.isNaN(d)) {
							double w = wnd[windx];
							sum_w [t][indx] += w;
							sum_wd[t][indx] += w * d ;
						}
					}
					break;
				case 1: // vegetation
				case 2: // alpha
				case 3: // elevation
					for (int windx = 0; windx < woi_veg_length; windx++) {
						int x = windx % woi_veg.width + woi_veg.x - out_woi.x;
						int y = windx / woi_veg.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
//						preview_data[t][ns][indx] = woi_tva[t][windx];
						double d =  woi_tva[t][windx]; 
						if (!Double.isNaN(d)) {
							double w = wnd_veg[windx]; // Index 400 out of bounds for length 400
							sum_w [t][indx] += w;
							sum_wd[t][indx] += w * d ;
						}
					}
					break;
				case 4: // terrain elevation
					for (int windx = 0; windx < woi_veg_length; windx++) {
						int x = windx % woi_veg.width + woi_veg.x - out_woi.x;
						int y = windx / woi_veg.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
//						preview_data[t][ns][indx] = woi_tva[t][windx]; // skipping terrain elevation average
						double d =   woi_tva[t][windx]; 
						if (!Double.isNaN(d)) {
							double w = wnd_veg[windx];
							sum_w [t][indx] += w;
							sum_wd[t][indx] += w * d ;
						}
					}
					break;
				case 5: // confidence
					for (int windx = 0; windx < woi_length; windx++) {
						int x = windx % woi.width + woi_veg.x - out_woi.x;
						int y = windx / woi.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
//						preview_data[t][ns][indx] = confidence[windx];
						double d =   confidence[windx];
						if (!Double.isNaN(d)) {
							double w = wnd[windx];
							sum_w [t][indx] += w;
							sum_wd[t][indx] += w * d ;
						}
					}
					break;
				case 6: // terrain_offset
					for (int windx = 0; windx < woi_length; windx++) {
						int x = windx % woi.width + woi_veg.x - out_woi.x;
						int y = windx / woi.width + woi_veg.y - out_woi.y;
						int indx = x + y * out_woi.width;
//						preview_data[t][ns][indx] = terrain_offset;
						double d =   terrain_offset;
						if (!Double.isNaN(d)) {
							double w = wnd[windx];
							sum_w [t][indx] += w;
							sum_wd[t][indx] += w * d ;
						}
					}
					break;
				}
				
			}
		}

		for (int t = 0; t < sum_wd.length; t++) {
			for (int i = 0; i < out_length; i++) {
				if (sum_w[t][i] == 0) {
					sum_wd[t][i] = Double.NaN;
				} else {
					sum_wd[t][i] /= sum_w[t][i];
				}
			}
		}
		
		
		
		VegetationModel vegetationModel = vegetationLMA.getModel();
		String ref_scene = vegetationModel.reference_scene;
		String pre_title = ref_scene+"-preview_segments-"+out_woi.x+"-"+out_woi.y+"-"+out_woi.width+"-"+out_woi.height;
		ShowDoubleFloatArrays.showArrays(
				sum_wd,
				out_woi.width,
				out_woi.height,
				true,
				pre_title+"-ACCUM",
				top_titles);
		
		double [] terrain_reference =   crop(
				vegetationModel.terrain_scenes_render[vegetationModel.reference_index],
				vegetationLMA.getFull().width,
				out_woi);
		double [] terrain_accumulated = crop(
				vegetationModel.terrain_average_render,
				vegetationLMA.getFull().width,
				out_woi);
		double [] terrain_um = 	terrain_accumulated.clone();
		for (int i = 0; i < terrain_um.length; i++) {
			if (Double.isNaN(terrain_um[i])) {
				terrain_um[i] = 0;
			}
		}
		VegetationModel.unsharpMask(
				terrain_um,    // final double [] data,
				out_woi.width, // final int       width,
				um_sigma,      // final double    um_sigma,
				um_weight);    // final double    um_weight)
		String [] result_titles = {"ref_render", "terrain_accum",String.format("terrain_accum_UM%5.2f-%4.2f",um_sigma,um_weight),"terrain"};
		double [][] result_img =  {terrain_reference, terrain_accumulated, terrain_um, sum_wd[0]};
		boolean normalize = true;
		int norm_index = 1;
		if (normalize) {
		result_img =  normalize (
				result_img, // final double [][] data,
				norm_index); // final int         norm_index){
		}
		String res_title = ref_scene+"-result_terrain-"+out_woi.x+"-"+out_woi.y+"-"+out_woi.width+"-"+out_woi.height;
		if (normalize) {
			res_title+= "-normalized"+norm_index;
		}
		ShowDoubleFloatArrays.showArrays(
				result_img,
				out_woi.width,
				out_woi.height,
				true,
				res_title,
				result_titles);
		return;
	}
	
	
	public static ImagePlus combineSegmentsSceneOffsets(
			VegetationLMA        vegetationLMA,
			VegetationSegment [] segments,
			boolean              crop_combo,
			final int            width,
			final int            num_scenes
			) { // show amplified difference from filtering
		//FIXME: Handle woi separately!
		final Rectangle combo_woi =     crop_combo? VegetationSegment.getBounds(segments, false):vegetationLMA.getFull();
		final Rectangle combo_woi_veg = crop_combo? VegetationSegment.getBounds(segments, true): vegetationLMA.getFull();
		final Rectangle out_woi = new Rectangle(combo_woi);
		out_woi.add(combo_woi_veg);
		int out_length = out_woi.width*out_woi.height;
		double [][] sum_scene_w =  new double [num_scenes][out_length];
		double [][] sum_scene_wd = new double [num_scenes][out_length];
		
		for (int ns = 0; ns < segments.length; ns++) {
			Rectangle woi =     segments[ns].woi; // for confidence and limits for terrain
			int woi_length =     woi.width * woi.height; 
			// creating larger window, still use only the woi for overlapping, discard outside
			double [] wnd = overlapCosineWindow (woi, width);
			double [] scene_offsets = segments[ns].scene_offsets;
			int min_scenes = Math.min(num_scenes, scene_offsets.length);
			for (int windx = 0; windx < woi_length; windx++) {
				int wx = windx % woi.width;
				int wy = windx / woi.width;
				int x = wx +  woi.x - out_woi.x;
				int y = wy +  woi.y - out_woi.y;
				int indx = x + y * out_woi.width;
				for (int nscene = 0; nscene < min_scenes; nscene++) {
					double d =  scene_offsets[nscene];
					if (!Double.isNaN(d)) {
						double w = wnd[windx];
						sum_scene_w [nscene][indx] += w;
						sum_scene_wd[nscene][indx] += w * d ;
					}
				}
			}
		}
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			for (int i = 0; i < sum_scene_w[nscene].length; i++) {
				if (sum_scene_w [nscene][i] == 0) {
					sum_scene_wd [nscene][i] = Double.NaN;
				} else {
					sum_scene_wd [nscene][i] /= sum_scene_w [nscene][i];
				}
			}
		}
		
		VegetationModel vegetationModel = vegetationLMA.getModel();
		String [] scene_names=            vegetationLMA.getModel().getSceneNames();
		
		String ref_scene = vegetationModel.reference_scene;
		
		String pre_title = ref_scene+"-"+out_woi.x+"-"+out_woi.y+"-"+out_woi.width+"-"+out_woi.height;
		
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				sum_scene_wd,
				out_woi.width,
				out_woi.height,
				pre_title+"-SCENE_OFFSETS", // +"-objects_"+scene_num,				
				scene_names); // test_titles,
			return imp;
	}
	
	
	
	
	public static double [] overlapCosineWindow(
			Rectangle woi,
			int       width){
		if (width > woi.width/2) {
			width = woi.width/2;
		}
		if (width > woi.height/2) {
			width = woi.height/2;
		}
		double [] window = new double [woi.width*woi.height];
		double [] wnd_x = new double  [woi.width];
		double [] wnd_y = new double  [woi.height];
		Arrays.fill(wnd_x, 1.0);
		Arrays.fill(wnd_y, 1.0);
		for (int i = 0; i < width; i++) {
			double w = 0.5* (1 - Math.cos(Math.PI * (i + 0.5) / width));
			wnd_x[i] = w;
			wnd_x[woi.width - i - 1] = w;
			wnd_y[i] = wnd_x[i];
			wnd_y[woi.height - i - 1] = w;
		}
		int indx = 0;
		for (int y = 0; y < woi.height; y++) {
			for (int x = 0; x < woi.width; x++) {
				window[indx++] = wnd_x[x] * wnd_y[y];
			}
		}
		return window;
	}
	
	public static double [] crop(
			double [] data,
			int       width,
			Rectangle woi) {
		int height = data.length/width;
		if ((woi.x==0) && (woi.y==0) && (woi.width==width) && (woi.height==height)) {
			return data.clone();
		}
		double [] cropped = new double [woi.width*woi.height];
		Arrays.fill (cropped, Double.NaN);
		for (int row = 0; row < woi.height; row ++) {
			int src_row = row + woi.y;
			if ((src_row >0) && (src_row < height)) {
				int col0 = 0;
				int col1 = woi.width;
				int src_col0 = col0+woi.x;
				int src_col1 = src_col0+ woi.width;
				if (src_col0 < 0) {
					col0 -= src_col0;
					src_col0 = 0;
				}
				if (src_col1 > width) {
					col1 -= src_col1 - width;
					src_col1 = width;
				}
				int row_len = col1 - col0; 
				if (row_len > 0) {
					System.arraycopy(
							data,
							src_row * width + src_col0,
							cropped,
							row*woi.width + col0,
							row_len);
				}
			}
		}
		return cropped;
	}
	/**
	 * Normalize slices to match the selected one by average and standard deviation
	 * @param data       array of image slices
	 * @param norm_index index (0-based) of the reference slice
	 * @return normalized image
	 */
	public static double [][] normalize (
			final double [][] data,
			final int         norm_index){
		final int num_slices = data.length;
		final int num_pix = data[0].length; 
		double [][] normalized = new double [num_slices][];
		for (int i = 0; i < num_slices; i++) {
			normalized[i] = data[i].clone();
		}
		final double [] mean = new double[num_slices];
		final double [] std = new double[num_slices];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < num_slices; nSlice = ai.getAndIncrement()) {
						double [] slice = normalized[nSlice];
						double s0=0, s1=0, s2=0;
						for (int i = 0; i < slice.length; i++) if (!Double.isNaN(slice[i])) {
							s0 += 1;
							s1 += slice[i];
							s2 += slice[i]*slice[i];
						}
						s1 /= s0;
						s2 /= s0;
						mean[nSlice] = s1;
						std[nSlice] = Math.sqrt(s2 - s1 * s1);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final double [] a = new double [num_slices], b = new double[num_slices];
		Arrays.fill(a, 1);
		for (int n = 0; n < num_slices; n++) if (n != norm_index){
			a[n] = std[norm_index]/std[n];
			b[n] = mean[norm_index] - a[n]*mean[n]; 
		}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < num_pix; nPix = ai.getAndIncrement()) {
						for (int n = 0; n < num_slices; n++) if (n != norm_index){
							double d = normalized[n][nPix];
							if (!Double.isNaN(d)) {
								normalized[n][nPix] = a[n]*d + b[n]; 
							}
						}						
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return normalized;
	}

	public static ImagePlus generateSineTarget() {
		int    width =       1024;
		int    height =      1024;
		double period_hor =     0.3333;  // fraction of width -  1.0 single horizontal period
		double period_vert =    0.3333; // fraction of height - 1.0 single vertical period
		double phase_hor =      0;   // phase at left edge as fraction of 2*PI (0.25 - 90 degree shift)
		double phase_vert=      0;// phase at top edge as fraction of 2*PI (0.25 - 90 degree shift)
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Set CLT parameters",600,250);
		gd.addNumericField("Width",            width,      0,4,"pix","Image width." );
		gd.addNumericField("Height",           width,      0,4,"pix","Image height." );
		gd.addNumericField("Horizontal period",period_hor, 5,8,"",   "Horisontal period (1.0 - one full period, 0.2 - two full periods)." );
		gd.addNumericField("Vertical period",  period_vert,5,8,"",   "Vertical period (1.0 - one full period, 0.2 - two full periods)." );
		gd.addNumericField("Horizontal phase", phase_hor,  5,8,"",   "Horisontal phase at left edge (1.0 - 2*PI, 0.25 - 90 degrees)." );
		gd.addNumericField("Vertical phase",   phase_vert, 5,8,"",   "Vertical phase at top edge (1.0 - 2*PI, 0.25 - 90 degrees)." );
        gd.showDialog();
		if (gd.wasCanceled()) return null;
		width =       (int) gd.getNextNumber();
		height =      (int) gd.getNextNumber();
		period_hor =        gd.getNextNumber(); // fraction of width -  1.0 single horizontal period
		period_vert =       gd.getNextNumber(); // fraction of height - 1.0 single vertical period
		phase_hor =         gd.getNextNumber(); // phase at left edge as fraction of 2*PI (0.25 - 90 degree shift)
		phase_vert=         gd.getNextNumber(); // phase at top edge as fraction of 2*PI (0.25 - 90 degree shift)
		double [] data = generateSineTarget(
				width,       // int width,
				height,      // int height,
				period_hor,  // double period_hor,  // fraction of width -  1.0 single horizontal period
				period_vert, //double period_vert, // fraction of height - 1.0 single vertical period
				phase_hor,   // double phase_hor,   // phase at left edge as fraction of 2*PI (0.25 - 90 degree shift)
				phase_vert); // double phase_vert) {// phase at top edge as fraction of 2*PI (0.25 - 90 degree shift)
		String title = "pattern_perx"+period_hor+"_pery"+period_vert+"_phx"+phase_hor+"_phy"+phase_vert; 
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				data, // double[] pixels,
				width, // int width,
				height, // int height,
				title); // String title);
  	    imp.getProcessor().resetMinAndMax();
		imp.show();
		return imp;
	}

	
	public static double [] generateSineTarget(
			int width,
			int height,
			double period_hor,  // fraction of width -  1.0 single horizontal period
			double period_vert, // fraction of height - 1.0 single vertical period
			double phase_hor,   // phase at left edge as fraction of 2*PI (0.25 - 90 degree shift)
			double phase_vert) {// phase at top edge as fraction of 2*PI (0.25 - 90 degree shift)
		double [] data = new double[width*height];
		double [] vx= new double [width];
		for (int x = 0; x <width; x++) {
			vx[x] = Math.sin(2*Math.PI*((x / (period_hor*width)) + phase_hor));
		}
		for (int y = 0; y < height; y++) {
			double vy = Math.sin(2*Math.PI*((y / (period_vert * height)) + phase_vert));
			for (int x = 0; x <width; x++) {
				data [y*width+x] = vx[x] * vy;
			}
		}
		return data;
	}

	@SuppressWarnings("static-method")
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
	}

	@SuppressWarnings("static-method")
	private void writeObject(ObjectOutputStream oos) throws IOException {
		oos.defaultWriteObject();
	}
	
	public static void writeVegetationSegments(
			String path,
			VegetationSegment [] segments) throws IOException {
		FileOutputStream fileOutputStream = new FileOutputStream(path);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
		objectOutputStream.writeObject(segments.length); // current_version);
		for (VegetationSegment segment:segments) {
			objectOutputStream.writeObject(segment);
		}
		objectOutputStream.flush();
		objectOutputStream.close();
	}
	
	public static VegetationSegment [] readVegetationSegments (
			String path) throws IOException, ClassNotFoundException {
		FileInputStream fileInputStream  = new FileInputStream(path);
		ObjectInputStream objectInputStream  = new ObjectInputStream(fileInputStream);
		int num_segments = (int) objectInputStream.readObject(); // reads OrthoMapsCollection
		VegetationSegment [] segments = new VegetationSegment [num_segments];
		for (int i = 0; i < num_segments; i++) {
			segments[i] = (VegetationSegment) objectInputStream.readObject();
		}
		objectInputStream.close();
		return segments;
	}
	
}
