package com.elphel.imagej.vegetation;

import java.awt.Rectangle;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.DoubleGaussianBlur;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.jp4.JP46_Reader_camera;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TileNeibs;

import Jama.Matrix;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.io.FileSaver;

public class VegetationLMA {
	public static final String PAR_EXT =            ".par-tiff";

	public static final int TVAO_TERRAIN =          0;
	public static final int TVAO_VEGETATION =       1;
	public static final int TVAO_ALPHA =            2;
	public static final int TVAO_ELEVATION =        3;
	public static final int TVAO_SCENE_OFFSET =     4;
	public static final int TVAO_TYPES =            5;
	
	public static final int YSRC_SCENE =            0;
	public static final int YSRC_FINDEX =           1;
	public static final int YSRC_NEIB0 =            2;
	public static final int YSRC_NEIBS =            4;
	public static final int YSRC_SIZE =             YSRC_NEIB0 + YSRC_NEIBS;
	
	public static final int DATA_SOURCE_HEAD =      0; // header row with { scene, full_indx, terr_indx, scale_indx}, always present
	public static final int DATA_SOURCE_CORN_VEGET =1; // [4]/null Z-shape 4 corners vegetation, either >= as parameter index or -1 - (x + image_width*y) of the unmodified full index
	public static final int DATA_SOURCE_CORN_ALPHA =2; // [4]/null Z-shape 4 corners vegetation alpha, either >= as parameter index or -1 - (x + image_width*y) of the unmodified full index
	public static final int DATA_SOURCE_NEIB =      3; // [4]/null or 4 neighbors (CW), either y_vector index or -1 -(x + image_width*y) of the unmodified terrain
	public static final int DATA_SOURCE_HEAD_SCENE = 0; // header row scene's subindex
	public static final int DATA_SOURCE_HEAD_FINDEX =1; // header row full frame index's subindex
	public static final int DATA_SOURCE_HEAD_TINDEX =2; // header row texture parameter subindex
	public static final int DATA_SOURCE_HEAD_SINDEX =3; // header row frame offset subindex
	public static final int DATA_SOURCE_ITEMS = DATA_SOURCE_NEIB + 1; // == 4 number or rows in data source element 
	public static final int DATA_SOURCE_HEAD_SIZE = DATA_SOURCE_HEAD_SINDEX+1;
	public static final int [] EMPTY4=               {-1,-1,-1,-1};
	
//	public static final int [] SAVE_TYPES = {TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_SCENE_OFFSET};
	public static final int [] SAVE_TYPES = {TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_ELEVATION, TVAO_SCENE_OFFSET};

	private final Rectangle    full;
	private final int          image_length;
	private final int          num_scenes;
	private Rectangle          woi =             null;
	private Rectangle          woi_veg =         null; // extended woi to include offset vegetation
	// should be non-null everywhere where elevation is defined?
	private double []          vegetation_average;  // full image average vegetation (with nulls)
	private double []          vegetation_filtered; // same as vegetation_average, but has more NaN to be used to select initial terrain
	private double []          vegetation_pull;    // have nan far from vegetation, use to pull LMA to this  
	private double []          terrain_average;    // full image average terrain (no nulls)
	private double [][]        terrain_rendered;    // terrain rendered for scenes (has nulls)
	private double [][]        terrain_rendered_hf; // terrain rendered for scenes (has nulls)
	private double [][][]      vegetation_offsets; // [num_scenes][pixle]{dx,dy} differential offsets of vegetation to terrain grid
	private boolean            diff_offsets;
	// next 3 arrays are full [image_width*image_height], but will be modified only inside woi
	public  double [][]        tvao;            // [0][image_length] - terrain, [1][image_length] - vegetation,
	
	private double [][][]     scales_xy;         // [scene][pixel{scale_x,scale_y}
	private VegetationModel   vegetationModel = null;

//	public int               min_dependent_scenes = 1;
	
	private double            max_warp =             1.8;
	private int               max_offset =           22;               
	
//[2][image_length] - alpha, [3][num_scenes] - per-scene offset (mostly for vignetting. MAYBE add per-scene pair of tilts
	private int [][]          par_index;       // indices -  [0..4][full_pixel] - same as for tvao, value - parameter index
	private int [][]          par_rindex;      // parameter -> pair of type (0..3) and full window pixel index
	private int               num_pars_terrain; 
	private int               num_pars_vegetation; 
	private int               num_pars_alpha;            //== num_pars_vegetation
	private int               num_pars_elevation;        //== num_pars_vegetation
	private int               num_pars_scenes;
	
	private int               ind_pars_terrain =   0;    // index of the first terrain parameter
	private int               ind_pars_vegetation;       // index of the first vegetation parameter
	private int               ind_pars_alpha;            // index of the first alpha parameter
	private int               ind_pars_elevation;        // index of the first elevation parameter
	private int               ind_pars_scenes;           // index of the first scene parameter
	private int [][]          y_src;             // data pointers for y-vector
	private int [][]          y_src_hf; // subset of pointers that have all 4 neighbors   
	private int [][]          y_src_scene;       // [scene]{start_y_src, end_y_src+1} pointers to y_src per scene
	private int [][]          y_wsrc;            // [scene][windex] ysrc index for scene, windex [num_scenes][woi_length]
	// elevation-dependent parameters, calculated once if elevations are not adjusted or each time if they are
	private double []         elev_radius;       // for the future - make variable-size influence of the vegetation to mitigate far influenced pixels
	private int [][][]        elev_woi4;         // [scene][woi_veg][~4] indices (in woi) of 4 (or more/less) neighbors of projections (negatives)
	private double [][][]     elev_weights4;     // [scene][woi_veg][~4] weights of influence for 4 neighbors
	private double [][]       elev_sum_weights;  // [scene][woi] sum weights from up to 4 elevation pixels (even more with overlap)
	// derivatives, only defined when elevations are adjusted
	private double [][][]     elev_dweights4;    // [scene][woi_veg][4] derivatives of weights of influence for 4 neighbors with respect to elevations
	private double [][][]     elev_dsum_weights; // [scene][elev_par][woi] derivative of elev_sum_weights by each of the elevation parameter
//	private boolean [][]      elev_dsum_pivot;   // [elev_par][elev_par] which elevation parameters have common dependents
	// possible to combine the next 2 (overwrite) 
	private ArrayList<HashSet<Integer>> elev_contribs; // new ArrayList<HashSet<Integer>>(woi_length);
	private ArrayList<HashSet<Integer>> param_contribs; // new ArrayList<HashSet<Integer>>(woi_length);
	private boolean [][]      param_pivot;   // [all_pars][all_pars] which parameters parameters have common dependents

	
	// TODO: Compare with/without pivot to verify/troubleshoot.
	
	
	@Deprecated
	private int [][][]        data_source; // [index][windx][DATA_SOURCE_ITEMS]:
	                                       // 0:{ scene, full_indx, terr_indx, scale_indx} - always present as parameters
	                                       // 1: [4]/null Z-shape 4 corners vegetation, either >= as parameter index or -1 - (x + image_width*y) of the unmodified full index
	                                       // 2: [4]/null Z-shape 4 corners vegetation alpha, either >= as parameter index or -1 - (x + image_width*y) of the unmodified full index
	                                       // 3: [4]/null or 4 neighbors (CW), either y_vector index or -1 -(x + image_width*y) of the unmodified terrain
	@Deprecated
	private double [][]       corners_weights; // matches data_source, non-nulls specify Z-shape 4 corners of vegetation/alpha weights
	
	private boolean []        valid_terrain =    null; // [woi.width*woi.height] valid terrain and y_vector pixels
	private boolean []        overlaid =         null; // [woi.width*woi.height] this valid terrain pixel is overlaid by vegetation (in all valid scenes) 
	private boolean []        valid_vegetation = null;
	private double [][][]     warps =            null; // measure of horizontal and vertical distances between the vegetation projections - use to filter scenes
	
//	private int []            used_scenes_indices;
	private final double []   scene_weights;
	private boolean []        valid_scenes;
	
	private double []         parameters_vector = null;
	private double []         y_vector = null;
	private double            weight_pure =     0;
	private double []         weights; // normalized so sum is 1.0 for all - samples and extra regularization
	
//	private boolean            start_warm_veget =   true;   // start with vegetation warmer than terrain
//	private double             terrain_warmest =   90;     // pull vegetations to warm, terrain to cold
//	private double             initial_split =      0.1;    // pull vegetations to warm, terrain to cold
//	private double             min_split_frac =     0.15;   // minimal modality fraction to use split by temperature
	private double             terr_difference =  100;      // pull vegetation to be this warmer	
	private double             terr_pull_cold =     0;      // pull vegetations to warm, terrain to cold
	
	// next two just for saving?
	private double             hifreq_weight;  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
	private double             reg_weights;    // fraction of the total weight used for regularization
	
	private boolean []         fits;

	private double             alpha_initial_contrast = 1.0; // >=1.0
	private double             alpha_loss =         0; // not used with cosine alpha
	private double             alpha_offset =       0; // if >0,  start losses above 0.0 and below 1.0;
	private double             alpha_lpf =          0;
	private boolean            alpha_piece_linear = true;
	private double             alpha_scale_avg =    1.0; // 1.1; // scale average alpha (around 0.5) when pulling to it
	private double             alpha_push =        12.0;   // push from alpha==0.5
	private double             alpha_push_neutral = 0.8; // alpha point from which push (closer to opaque)	
	private double             alpha_push_center =  1.5; // weight of center alpha pixel relative to each of the 4 ortho ones
	private double             alpha_mm_hole    =   0.1; // NaN to disable. Local "almost minimum" (lower than this fraction between min and max neighbor) is not subject to alpha_lpf
	private boolean            alpha_en_holes   =   true; // Search for small semi-transparent holes, disable diffusion of local alpha minimums
	private double             alpha_diff_hole =    0.01; // minimal alpha difference between min and max neighbor to be considered a hole

	
	private boolean            from_file =       false;

	
	private double             terr_lpf =        0;
	private double             veget_lpf =       0;
	private double             elevation_lpf =   0;
	
	private boolean []         lpf_fixed = {true, true,true, true, true}; // using Laplacian to lpf parameters, pull to fixed ones too
	
	// when unsharp mask is applied , pulling to 0 (when alpha is 0 (for vegetation) or 1.0 (for terrain) makes sense
	private double             terr_pull0 =      0; // now - pull to filled terrain - terrain_average
	private double             veget_pull0 =     0; // now - pull to vegetation_pull (extended vegetation)
	private double             elevation_pull0 = 0; // now - pull to initial elevation
	private double             scenes_pull0 =    0; // pull average scene offset to 0;
	private boolean            use_scenes_pull0 = true; // derivative, set in setWeights
	private double             scale_scenes_pull = 0; // used in getFxDerivs to scale scene offsets as their weight will be reg_weights / extra_samples	
	
	
	public double             boost_parallax =  1;
	public double             um_sigma =        0;       // just use in debug image names
	public double             um_weight =       0;
	
	// data used to calculate lpf pull of the alpha pixel to average of four neighbors. Below similar (weaker pull) for terrain and vegetation
	// to smooth areas where there is no data from available images. 
	public int [][]           alpha_neibs;   // corresponds to parameters for alpha (num_pars_vegetation_alpha), each has 4 ortho neibs, -1 - border, >= 0
	// - parameter index, <=-2 -2-full_pixel_index
	public int [][]           terr_neibs;    // corresponds to parameters for terrain, for smoothing undefined terrain, similar to alpha_neibs
	public int [][]           veget_neibs;   // corresponds to parameters for vegetation, for smoothing undefined vegetation, similar to alpha_neibs
	public int [][]           elevation_neibs;   // corresponds to parameters for elevation, for smoothing undefined elevations, similar to alpha_neibs
	
	
	public double             delta=            1e-5; // 7;
	
	public int                debug_index;
	public double [][]        debug_image;
	public static double []   debug_alpha_scales = {0,50, 0, 10}; // {100,150};
	public String             debug_path = "/media/elphel/SSD3-4GB/lwir16-proc/berdich3/debug/vegetation/lma_um/";
	
	private double []         last_rms =        null; // {rms, rms_pure}, matching this.vector
	private double []         good_or_bad_rms = null; // just for diagnostics, to read last (failed) rms
	private double []         initial_rms =     null; // {rms, rms_pure}, first-calcualted rms
	private double []         last_ymfx =       null;
	private double [][]       last_jt =         null;
	
	public boolean            skip_ref_scene = false;
	
	public boolean            debug_deriv = true;
	public int                debug_width =     12;
	public int                debug_decimals =  9;

	public VegetationLMA (
			int                width,
			boolean            diff_mode,
			double []          terrain_average,    // full image average terrain (no nulls!)
			double []          vegetation_average, // full image average vegetation (with nulls)
			double [][]        terrain_rendered,   // terrain rendered for scenes (has nulls)
			double [][][]      vegetation_offsets  // [num_scenes][pixel]{dx,dy} differential offsets of vegetation to terrain grid
			  ) {
		full = new Rectangle(0,0,width, terrain_average.length/width);
		image_length =  terrain_average.length;
		diff_offsets = diff_mode;
		num_scenes =   vegetation_offsets.length;
		this.vegetation_average = vegetation_average;
		this.vegetation_filtered= vegetation_average; // obsolete
		this.terrain_average =    terrain_average;   
		this.terrain_rendered =   terrain_rendered;  
		this.vegetation_offsets = vegetation_offsets;
		tvao = new double[TVAO_TYPES][];
		tvao[TVAO_TERRAIN] =          this.terrain_average.clone();
		tvao[TVAO_VEGETATION] =       this.vegetation_average.clone();
		tvao[TVAO_ALPHA] =            new double [image_length]; // 0 - use terrain
		tvao[TVAO_SCENE_OFFSET] =     new double [num_scenes];

		setupLaplacians(0.0); // double weight_diag)
		
		scene_weights = new double [num_scenes];
	}
	
	public VegetationLMA (
			VegetationModel vegetationModel,
			double alpha_initial_contrast) {
		this.alpha_initial_contrast = alpha_initial_contrast;
		full = vegetationModel.full;
		image_length = full.width * full.height;
		num_scenes = vegetationModel.terrain_scenes_render.length;
		vegetation_average =vegetationModel.vegetation_full;      // vegetation_average_render;
		vegetation_filtered=vegetationModel.vegetation_filtered;  // same with more NaNs for initial selection
		vegetation_pull =   vegetationModel.vegetation_pull;      // pull LMA to this level
		terrain_average =   vegetationModel.terrain_filled;       // terrain filled under vegetation, use to pull terrain level to it
		terrain_rendered =  vegetationModel.terrain_scenes_render;  
		vegetation_offsets =vegetationModel.vegetation_warp;
		tvao = new double[TVAO_TYPES][];
		tvao[TVAO_TERRAIN] =          this.terrain_average.clone();
		tvao[TVAO_VEGETATION] =       this.vegetation_average.clone();
		tvao[TVAO_SCENE_OFFSET] =     new double [num_scenes];
		setupInitialTerrainVegetationAlpha(
				alpha_initial_contrast,           // double alpha_contrast,
				vegetationModel.terrain_filled,   // double [] terrain_filled,
				vegetationModel.vegetation_pull,  // double [] vegetation_extended,
				vegetationModel.vegetation_full); // double [] vegetation) { // best guess
		setupLaplacians(0.0); // double weight_diag)
		diff_offsets = vegetationModel.diff_mode;
		this.vegetationModel =        vegetationModel; // to access scene names, directories, reference index
		scene_weights = new double [num_scenes];
		scales_xy = getScalesXY(vegetationModel.scale_dirs);
		tvao[TVAO_ELEVATION] =  vegetationModel.elevations.clone();
		return;
	}
	private static double [][][] getScalesXY(
			final double [][][] scales_mag_dir) {
		final int num_scenes = scales_mag_dir.length;
		final int num_pixels = scales_mag_dir[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][][] scales_xy = new double [num_scenes][num_pixels][];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) {
						for (int npix = 0; npix < num_pixels; npix++) if (scales_mag_dir[nScene][npix] != null) {
							double scale = scales_mag_dir[nScene][npix][0];
							double angle = scales_mag_dir[nScene][npix][1];
							scales_xy[nScene][npix] = new double [] {
									scale*Math.cos(angle),
									scale*Math.sin(angle)};
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return scales_xy;
	}
	
	
	
	private void setupLaplacians(double weight_diag) {
		terrain_rendered_hf = new double [terrain_rendered.length][];
		for (int nscene = 0; nscene <terrain_rendered_hf.length; nscene++) {
			terrain_rendered_hf[nscene] =  laplacian(
					false,                    // final boolean   gaussian,
					terrain_rendered[nscene], // final double [] data_in,
					full.width,               // final int       width,
					weight_diag);             // final double    weight_diag)
		}
	}
	private void setupInitialTerrainVegetationAlpha(
			double alpha_contrast,
			double [] terrain_filled,
			double [] vegetation_extended,
			double [] vegetation) { // best guess
		double dbg_lim = 0.01;
		tvao[TVAO_TERRAIN] =          terrain_filled.clone(); // ==  vegetationModel.terrain_filled
		tvao[TVAO_VEGETATION] =        vegetation_extended.clone();
		tvao[TVAO_ALPHA] = new double [image_length];
		for (int npix = 0; npix < image_length; npix++) {
			double t =  terrain_filled[npix];
			double v =  vegetation[npix];
			double vp = vegetation_extended[npix];
			double a =  0; // Double.NaN; // .isNaN(vegetation_filtered[npix])? 0.0:1.0; // not needed
			if (!Double.isNaN(t) && !Double.isNaN(v) && !Double.isNaN(vp)) {
				if (v < t) {
					a = 0.0;
				} else if (v > vp) {
					a = 1.0;
				} else  {
					a = (v-t)/(vp-t);
					if (alpha_contrast != 1.0) {
						a = 0.5 + (a - 0.5)*alpha_contrast;
						if (a < 0) a = 0;
						else if (a > 1.0) a = 1;
					}
				}
//				if (a < dbg_lim) a = dbg_lim;
//				if ( a > 1.0-dbg_lim) a = 1.0-dbg_lim;
//				tvao[TVAO_ALPHA][npix] = a;
			}
			if (a < dbg_lim) a = dbg_lim;
			if ( a > 1.0-dbg_lim) a = 1.0-dbg_lim;
			tvao[TVAO_ALPHA][npix] = a;
		}
	}
	
	
	
	@SuppressWarnings("unused")
	private static double [] getInitialAlpha(
			double [] vegetation_filtered,
			double transparent,
			double opaque) {
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [] alpha = new double [vegetation_filtered.length];
	// vegetation_filtered
    // tvao[TVAO_ALPHA] 
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < vegetation_filtered.length; nPix = ai.getAndIncrement()) {
						alpha[nPix] = Double.isNaN( vegetation_filtered[nPix])? transparent: opaque;
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return alpha;
	}
	
	
	
	public int prepareLMA(
			final boolean            keep_parameters,
			final Rectangle          woi,
			final double             max_warp, // 1.8 - do not use scenes where distance between vegetation projection exceeds this
			final int                max_offset,               // maximal "elevation" to consider
//			final int                min_total_scenes,
//			final int                min_samples_scene, // 10
//			final int                min_valid_pixels,
//			final boolean            start_warm_veget, // start with vegetation warmer than terrain -> USED now by vegetation_filtered
//			final double             terrain_warmest,  // warmest terrain (above is initially vegetation) NOT USED
//			final double             initial_split,    // initial alpha: terrain 0.0+, vegetation 1.0-. USED
//			final double             min_split_frac,   // minimal modality fraction to use split by temperature NOT USED?
            final double             terr_difference,  // pull vegetation to be this warmer			
			final double             terr_pull_cold,   // pull vegetation to warm, terrain to cold
			final double             default_alpha,
			final double             hifreq_weight,  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
			
			final boolean            fit_terr,
			final boolean            fit_veget,
			final boolean            fit_alpha,
			final boolean            fit_scenes,
			final boolean            fit_elevations,
			
			final double             reg_weights,    // fraction of the total weight used for regularization
			final double             alpha_loss,     // quadratic loss when alpha reaches -1.0 or 2.0 
			final double             alpha_offset,   // quadratic loss when alpha reaches -1.0 or 2.0
			final double             alpha_lpf,      // pull vegetation alpha to average of 4 neighbors
			final boolean            alpha_piece_linear, // true - piece-linear, false - half-cosine
			final double             alpha_scale_avg, //  = 1.2; // scale average alpha (around 0.5) when pulling to it
			final double             alpha_push,      //  5.0;   // push from alpha==0.5
			final double             alpha_push_neutral, //  = 0.8; // alpha point from which push (closer to opaque)
			final double             alpha_push_center,// 1.5; // weight of center alpha pixel relative to each of the 4 ortho ones
			final boolean            alpha_en_holes, // Search for small semi-transparent holes, disable diffusion of local alpha minimums
			final double             alpha_mm_hole, //     = 0.1; // NaN to disable. Local "almost minimum" (lower than this fraction between min and max neighbor) is not subject to alpha_lpf			
			final double             terr_lpf,       // pull terrain to average of 4 neighbors (very small)
			final double             veget_lpf,      // pull vegetation to average of 4 neighbors (very small - maybe not needed)
			final double             elevation_lpf,
			final double             terr_pull0,     // pull terrain to zero (makes sense with UM
			final double             veget_pull0,    // pull vegetation to zero (makes sense with UM
			final double             scenes_pull0,
			final double             boost_parallax, // increase weight of scene with maximal parallax relative to the reference scene
			final double             max_parallax,  // do not consider maximal parallax above this (consider it a glitch) 
			final double             um_sigma,       // just use in debug image names
			final double             um_weight,
			String                   parameters_read_path, 
			final int                debugLevel) {
		this.woi =               woi;
		this.max_warp =          max_warp;
//		this.min_dependent_scenes = min_dependent_scenes;
		this.max_offset =           max_offset;
//		double alpha_contrast =  1.0; 
//		this.start_warm_veget =  start_warm_veget;       
//		this.terrain_warmest =   terrain_warmest;        
//		this.initial_split =     initial_split;
//		this.min_split_frac =    min_split_frac;
		this.terr_difference =   terr_difference;        
		this.terr_pull_cold =    terr_pull_cold;       		
		this.hifreq_weight =     hifreq_weight;  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
		
		fits = new boolean[TVAO_TYPES];
		fits[TVAO_TERRAIN] =     fit_terr;
		fits[TVAO_VEGETATION] =  fit_veget;
		fits[TVAO_ALPHA] =       fit_alpha;
		fits[TVAO_SCENE_OFFSET] =fit_scenes;
		fits[TVAO_ELEVATION] =   fit_elevations;
		
		this.reg_weights =       reg_weights;    // fraction of the total weight used for regularization
		this.alpha_loss =        alpha_loss;
		this.alpha_offset =      alpha_offset;
		this.alpha_lpf =         alpha_lpf;
		this.alpha_scale_avg =   alpha_scale_avg;
		this.alpha_piece_linear= alpha_piece_linear;
		this.alpha_push =        alpha_push; //    5.0;   // push from alpha==0.5
		this.alpha_push_neutral = alpha_push_neutral; // 0.8; // alpha point from which push (closer to opaque)
		this.alpha_push_center = alpha_push_center; // 1.5; // weight of center alpha pixel relative to each of the 4 ortho ones
		this.alpha_en_holes =    alpha_en_holes;
		this.alpha_mm_hole =     alpha_mm_hole;
		this.terr_lpf =          terr_lpf;
		this.veget_lpf =         veget_lpf;
		this.elevation_lpf =     elevation_lpf;
		this.terr_pull0 =        terr_pull0;
		this.veget_pull0 =       veget_pull0;
		this.scenes_pull0 =      scenes_pull0;
		this.boost_parallax =    boost_parallax;
		this.um_sigma =          um_sigma;      // just use in debug image names
		this.um_weight =         um_weight;

		this.elev_radius = new double[full.width*full.height];
		Arrays.fill(elev_radius, 1.0);
		// calculate warps (as distances between vegetation projections of this pixel and its neighbors)
		warps = getMaxWarp(
				tvao[TVAO_ELEVATION], // final double []     elevations,
				scales_xy,  // final double [][][] scales_xy,
				full.width);     // final int           width)
		
		if (debugLevel > -2) {
			// not enough memory to allocate 468000MB (640x327580x585) stack
			// vegetationModel.reference_scene
			showWarps(
					warps,                                           // double [][][] warps,					
					tvao[TVAO_ELEVATION],                            // double []     elevations,
					scales_xy,                                       // double [][][] scales_xy,
					full.width,                                      // int           width,
					vegetationModel.reference_scene+"_warps.tiff" ); // title)
		}
		
//		valid_scenes = getValidScenes(max_offset); // final int    max_offset)
		woi_veg = new Rectangle();
		int        max_sacrifice = 5;
		boolean [] valid_pixels_in = null;
		int []     valid_stats = new int[5];
		int []     max_warp_scene_pix = new int[2]; 

		boolean [][] valid_scene_pix = getValidScenesPixels(
				warps,              // final double[][][] warps,
				max_warp,           // final double       max_warp,
				valid_pixels_in,    // final boolean [] valid_pixels_in, // should be initialized to woi, normally set to all true 
				max_offset,         // final int        max_offset,
				max_sacrifice,      // final int        max_sacrifice,
				valid_stats,        // final int []       stats,
				max_warp_scene_pix, // final int []       max_warp_scene_pix, 
				this.woi,           // final Rectangle  woi, 
				this.woi_veg);      // final Rectangle  woi_veg) // should be initialized, will be modified
		valid_scenes =     valid_scene_pix[0];
		valid_terrain =    valid_scene_pix[1];
		valid_vegetation = valid_scene_pix[2];
		overlaid =         valid_scene_pix[3];
		
		if (debugLevel > -2) {
			System.out.println("Got "+valid_stats[0]+" valid scenes (of "+this.terrain_rendered.length+
					"), "+valid_stats[1]+" valid WOI pixels (of "+(this.woi.width*this.woi.height)+
					"), of them "+valid_stats[3]+" have vegetation influence, valid vegetation pixels "+
					valid_stats[2] + " (of "+(this.woi_veg.width*this.woi_veg.height)+"). Removed "+
					valid_stats[4]+" scenes because of the warp > "+max_warp+".");
			if ((warps != null) && (max_warp_scene_pix != null)) {
				int warp_scene = max_warp_scene_pix[0];
				int warp_pix =   max_warp_scene_pix[1];
				double [] max_warps = warps[warp_scene][warp_pix];
				System.out.println("Maximal warp is "+max_warps[0]+" (horizontal "+max_warps[1]+", vertical "+max_warps[2]+
						") at scene "+warp_scene+", pix "+warp_pix+ ", absolute: px="+(warp_pix%full.width)+", py="+(warp_pix/full.width)+").");
			}
		}
		if (valid_stats[0]==0) {
			System.out.println("Too few valid scenes/pixels.");
			return -1;
		}

		
		
		if (debugLevel > -2) { // 3) {
			int first_scene = num_scenes;
			int last_scene =  -1;
			for (int i = 0; i < num_scenes; i++) if (valid_scenes[i]){
				first_scene = Math.min(first_scene, i);
				last_scene =  Math.max(last_scene,  i);
			}
			
			String [] titles = {"vegetation", "woi", "terrain", "overlaid", "elevation","first_dx","first_dy","first_warp","last_dx","last_dy","last_warp"}; 
			double [][] dbg_img = new double [titles.length][woi_veg.width * woi_veg.height];
			for (int i = 0; i < dbg_img.length; i++) {
				Arrays.fill(dbg_img[i], Double.NaN);
			}
			for (int i = 0; i < dbg_img[0].length; i++) {
				int x = i % woi_veg.width + woi_veg.x;
				int y = i / woi_veg.width + woi_veg.y;
				dbg_img[0][i] = valid_vegetation[i]? 5:0;
				dbg_img[1][i] = woi.contains(x,y) ? 5.0 : 0;
				if (woi.contains(x,y)) {
					int wx = x - woi.x;
					int wy = y - woi.y;
					int wpix = wx + wy*woi.width;
					dbg_img[2][i] = valid_terrain[wpix]? 5:0;
					dbg_img[3][i] = overlaid[wpix]? 5:0;
				}
				int npix = x + y *full.width;
				double [] scales_first =  scales_xy[first_scene][npix];
				double [] scales_last =   scales_xy[last_scene][npix];
				double [] warps_first =  warps[first_scene][npix];
				double [] warps_last =   warps[last_scene][npix];
				double elevation =  tvao[TVAO_ELEVATION][npix];
				dbg_img[4][i] =     elevation;
				if (scales_first != null) {
					double dx_first = scales_first[0] * elevation;
					double dy_first = scales_first[1] * elevation;
					dbg_img[5][i] = dx_first;     
					dbg_img[6][i] = dy_first;
				}
				if (warps_first != null) {
					dbg_img[7][i] = warps_first[0];
				}
				
				if (scales_last != null) {
					double dx_last =  scales_last[0] *  elevation;
					double dy_last =  scales_last[1] *  elevation;
					dbg_img[8][i] = dx_last;     
					dbg_img[9][i] = dy_last;
				}
				if (warps_last != null) {
					dbg_img[10][i] = warps_last[0];
				}
			}
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					woi_veg.width,
					woi_veg.height,
					true,
					"woi_veg_uses_"+woi_veg.x+"-"+woi_veg.y+"-"+woi_veg.width+"-"+woi_veg.height,
					titles);
		}
//		used_veg = valid_scene_pix[2];
		
		if ("RESTORE".equals(parameters_read_path)) {
			from_file = false;
			String  restore_dir =    debug_path;
			String  debug_title = getParametersDebugTitle();
			if (!restore_dir.endsWith(Prefs.getFileSeparator())) {
				restore_dir += Prefs.getFileSeparator();
			}
			
			String restore_path=restore_dir + debug_title+"-live.tiff";
			parameters_read_path = restore_path;
		}
		
		
		final double []     scene_weights = setupSceneWeights(
				boost_parallax, // double boost_parallax)
				woi,            // Rectangle woi)
				max_parallax); // final double ceil_parallax)

		setupParametersIndices(
				fits,                 // boolean [] fits,
				debugLevel);          // final int debugLevel)
		
		alpha_neibs = getNeighbors(
				TVAO_ALPHA,      // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars_alpha,  // final int ind_samples, // ind_pars_vegetation_alpha
				num_pars_alpha); // final int num_samples);  // num_pars_vegetation_alpha
		terr_neibs = getNeighbors(
				TVAO_TERRAIN,               // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars_terrain,           // final int ind_samples, // ind_pars_vegetation_alpha
				num_pars_terrain);          // final int num_samples);  // num_pars_vegetation_alpha
		veget_neibs = getNeighbors(
				TVAO_VEGETATION,            // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars_vegetation,        // final int ind_samples, // ind_pars_vegetation_alpha
				num_pars_vegetation);       // final int num_samples);  // num_pars_vegetation_alpha
		elevation_neibs = getNeighbors(
				TVAO_ELEVATION,             // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars_elevation,        // final int ind_samples, // ind_pars_vegetation_alpha
				num_pars_elevation);       // final int num_samples);  // num_pars_vegetation_alpha

		setupYSrc ( // updated, generates both y and y_hf(different lengths)
				(hifreq_weight > 0)); // boolean use_hf);
		
		if (!keep_parameters) {
			setupParametersVector (); // updated for elevation
		}
		
		from_file = false;
		if (parameters_read_path != null) {
			 readParametersFromImage(
					 parameters_read_path, // String    path,
						parameters_vector, // double [] vector,
						1) ;// int       gap)
		}
		
		setupYVector(); //(hifreq_weight > 0)); // boolean use_hf);
		
		setupWeights( // after setupParametersIndices
				scene_weights,
				reg_weights,      // final double reg_weights,
				hifreq_weight );  // final double hifreq_weight );
		last_jt = new double [parameters_vector.length][];
		return weights.length;
	}
	
	public double [] getParametersVector() {
		return parameters_vector;
	}
	
	private double [] setupSceneWeights(
			final double    boost_parallax,
			final Rectangle woi,
			final double    ceil_parallax) {
		// find max parallax
		final double ceil_parallax2 = ceil_parallax * ceil_parallax;
		final double [] scene_weights = new double[vegetation_offsets.length];
		if (boost_parallax == 0) {
			for (int nscene = 0; nscene < vegetation_offsets.length; nscene++) {
				scene_weights[nscene] = 1.0;
			}
		} else {
			final double [] max_parallax2 = new double [vegetation_offsets.length];
			double super_max_parallax2 = 0;
			for (int nscene = 0; nscene < vegetation_offsets.length; nscene++) {
				// only compare for selected window!
				for (int drow = 0; drow < woi.height; drow++) {
					int row = woi.y + drow;
					for (int dcol = 0; dcol < woi.width; dcol++) {
						int col = woi.x + dcol;	
//						int windx =dcol + drow * woi.width;
						int indx = row*full.width + col;
						double [] d =  vegetation_offsets[nscene][indx];
						if (d != null) {
							double d2 = Math.min(ceil_parallax2, d[0]*d[0] + d[1]*d[1]);
							/*
						if (d2 > max_parallax2[nscene]) {
							System.out.println("setupSceneWeights(): nscene="+nscene+", pix =  ("+(i%640)+","+(i/640)+") ="+i+" d2="+d2);
						}
							 */
							max_parallax2[nscene] = Math.max(max_parallax2[nscene], d2);
						}
					}
				}
				super_max_parallax2 = Math.max(super_max_parallax2, max_parallax2[nscene]);
			}
			for (int nscene = 0; nscene < vegetation_offsets.length; nscene++) {
				scene_weights[nscene] = 1.0+(boost_parallax - 1) * Math.sqrt(max_parallax2[nscene]/super_max_parallax2);
			}
		}
		return scene_weights;
	}
	
	private double [] getYminusFxWeighted(
			final double []   fx,
			final double []   rms_fp // null or [2]
			) {
		final Thread[]      threads =     ImageDtt.newThreadArray();
		final AtomicInteger ai =          new AtomicInteger(0);
		final AtomicInteger ati =         new AtomicInteger(0);
		final double []     wymfw =       new double [fx.length];
		double [] swd2 = new double[threads.length];
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					int nthread =  ati.getAndIncrement();
					for (int n = ai.getAndIncrement(); n < y_vector.length; n = ai.getAndIncrement()) {
						double d = y_vector[n] - fx[n]; // - fx[n]; // +y_vector[i]
						double wd = d * weights[n];
						if (Double.isNaN(wd)) {
							System.out.println("wd=NaN, n="+n);
						}
						wymfw[n] = wd;
						swd2[nthread] += d * wd;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double s_rms_pure = 0;
		for (int n = 0; n < swd2.length; n++) {
			s_rms_pure += swd2[n];
		}
//		System.out.println("ai.get()="+ai.get());
		// important to set - after first cycle ai is left 16(number of threads) larger than number of cycles!
		// It is so, because it first increments, then tests if (n < num_pairs)
		
		// Regularization part where y is assumed==0, only fX is used
		
		ai.set(y_vector.length); // not yet used
		ati.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int nthread =  ati.getAndIncrement();
					for (int n = ai.getAndIncrement(); n < fx.length; n = ai.getAndIncrement()) {
						double d = -fx[n]; // - fx[n]; // +y_vector[i]
						double wd = d * weights[n];
						wymfw[n] = wd;
						swd2[nthread] += d * wd;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		double s_rms = 0; // start from scratch
		for (int n = 0; n < swd2.length; n++) {
			s_rms += swd2[n];
		}
		if (Double.isNaN(s_rms)) {
			System.out.println("s_rms = NaN");
		}
		
		if (rms_fp != null) {
			rms_fp[0] = Math.sqrt(s_rms);
			rms_fp[1] = Math.sqrt(s_rms_pure/weight_pure);
		}
		return wymfw;
	}
	
	public int runLma( // <0 - failed, >=0 iteration number (1 - immediately)
			double lambda,           // 0.1
			double lambda_scale_good,// 0.5
			double lambda_scale_bad, // 8.0
			double lambda_max,       // 100
			double rms_diff,         // 0.001
			int    num_iter,         // 20
			boolean last_run,
			String dbg_prefix,
			int    debug_level)
	{
		boolean [] rslt = {false,false};
		this.last_rms = null; // remove?
		int iter = 0;
		if (dbg_prefix != null) {
//			 debugStateImage(dbg_prefix+"-initial");
		}
		for (iter = 0; iter < num_iter; iter++) {
			rslt =  lmaStep(
					lambda,
					rms_diff,
					debug_level);
			if (dbg_prefix != null) {
//				 debugStateImage(dbg_prefix+"-step_"+iter);
			}
			
			if (rslt == null) {
				return -1; // false; // need to check
			}
			if (debug_level > -2) { // 1) {
				System.out.println("LMA step"+String.format("%3d",iter)+": {"+rslt[0]+","+rslt[1]+"} full RMS= "+good_or_bad_rms[0]+
						" ("+initial_rms[0]+"), pure RMS="+good_or_bad_rms[1]+" ("+initial_rms[1]+") + lambda="+lambda);
			}
			if (rslt[1]) {
				break;
			}
			if (rslt[0]) { // good
				lambda *= lambda_scale_good;
			} else {
				lambda *= lambda_scale_bad;
				if (lambda > lambda_max) {
					break; // not used in lwir
				}
			}
		}
		if (rslt[0]) { // better
			if (iter >= num_iter) { // better, but num tries exceeded
				if (debug_level > 1) System.out.println("Step "+iter+": Improved, but number of steps exceeded maximal");
			} else {
				if (debug_level > 1) System.out.println("Step "+iter+": LMA: Success");
			}

		} else { // improved over initial ?
			if (last_rms[0] < initial_rms[0]) { // NaN
				rslt[0] = true;
				if (debug_level > 1) System.out.println("Step "+iter+": Failed to converge, but result improved over initial");
			} else {
				if (debug_level > 1) System.out.println("Step "+iter+": Failed to converge");
			}
		}
		boolean show_intermediate = true;
		if (show_intermediate && (debug_level > 0)) {
			System.out.println("LMA: full RMS="+last_rms[0]+" ("+initial_rms[0]+"), pure RMS="+last_rms[1]+" ("+initial_rms[1]+") + lambda="+lambda);
		}
		if (debug_level > 2){ 
			System.out.println("iteration="+iter);
		}
		if (debug_level > 0) {
			if ((debug_level > 1) ||  last_run) { // (iter == 1) || last_run) {
				if (!show_intermediate) {
					System.out.println("LMA: iter="+iter+",   full RMS="+last_rms[0]+" ("+initial_rms[0]+"), pure RMS="+last_rms[1]+" ("+initial_rms[1]+") + lambda="+lambda);
				}
			}
		}
		if ((debug_level > -2) && !rslt[0]) { // failed
			if ((debug_level > 1) || (iter == 1) || last_run) {
				System.out.println("LMA failed on iteration = "+iter);
			}
			System.out.println();
		}

		return rslt[0]? iter : -1;
	}

	private boolean [] lmaStep(
			double lambda,
			double rms_diff,
			int debug_level) {
		boolean [] rslt = {false,false};
		// maybe the following if() branch is not needed - already done in prepareLMA !
		if (this.last_rms == null) { //first time, need to calculate all (vector is valid)
			last_rms = new double[2];
			if (debug_level > 1) {
				System.out.println("lmaStep(): first step");
			}
			double [] fx = getFxDerivs(
					parameters_vector, // double []         vector,
					last_jt,           // final double [][] jt, // should be null or initialized with [vector.length][]
					debug_level);      // final int         debug_level)
			last_ymfx = getYminusFxWeighted(
					fx, // final double []   fx,
					last_rms); // final double []   rms_fp // null or [2]
			this.initial_rms = this.last_rms.clone();
			this.good_or_bad_rms = this.last_rms.clone();

			if (debug_level > -1) { // temporary
				/*
				dbgYminusFxWeight(
						this.last_ymfx,
						this.weights,
						"Initial_y-fX_after_moving_objects");
                */
			}
			if (last_ymfx == null) {
				return null; // need to re-init/restart LMA
			}
			// TODO: Restore/implement
			if (debug_level > 4) {
				double    delta = this.delta;
			 	double delta_err= debugDerivs (
			 			parameters_vector, // double [] vector,
						delta,             // double    delta,
						debug_level, //int       debugLevel,
						false); // boolean   show_img)
				System.out.println("\nMaximal error = "+delta_err);
			}
			if (debug_level > -2) { //
				String  save_dir =    debug_path;
				String  debug_title = getParametersDebugTitle();
				boolean save_all =    (debug_image != null);
				boolean show_this =   debug_level > 3;
				boolean show_all =    debug_level > 4;
				debugImageSaveShow(
						save_dir,     // String  save_dir, // save if not null
						debug_title, // String  title,    // not null
						save_all,    // boolean save_all,
						show_this,   // boolean show_this,
						show_all);   // boolean show_all)
			}
			
		}
		if (debug_level > 3) { // 0) {
			double    delta =  this.delta; // 1E-3;
			if (debug_level > 4) {
				double delta_err= debugDerivs (
						parameters_vector, // double [] vector,
						delta,             // double    delta,
						debug_level, //int       debugLevel,
						false); // boolean   show_img)
				System.out.println("\nMaximal error = "+delta_err);
			}
			int [] wh = new int[2];
			double [] debug_image =  parametersImage (
					parameters_vector, // double [] vector,
					1,                // int       gap)
					wh,
					null); // String    title)
			ShowDoubleFloatArrays.showArrays(
					debug_image,
					wh[0],
					wh[1],
					"parameters.vector-"+debug_index);
		}
		
		
		Matrix y_minus_fx_weighted = new Matrix(this.last_ymfx, this.last_ymfx.length);

		Matrix wjtjlambda = new Matrix(getWJtJlambda(
				lambda,       // *10, // temporary
				this.last_jt, // double [][] jt) // null
				null));       // final boolean [][] pivot)));
		
		if (debug_level>4) {
			System.out.println("JtJ + lambda*diag(JtJ");
			wjtjlambda.print(18, 6);
		}
		Matrix jtjl_inv = null;
		try {
			jtjl_inv = wjtjlambda.inverse(); // check for errors
		} catch (RuntimeException e) {
			rslt[1] = true;
			if (debug_level > -2) {
				System.out.println("Singular Matrix!");
			}

			return rslt;
		}
		if (debug_level>4) {
			System.out.println("(JtJ + lambda*diag(JtJ).inv()");
			jtjl_inv.print(18, 6);
		}
//last_jt has NaNs
		Matrix jty = (new Matrix(this.last_jt)).times(y_minus_fx_weighted);
		if (debug_level>2) {
			System.out.println("Jt * (y-fx)");
			jty.print(18, 6);
		}
		
		
		Matrix mdelta = jtjl_inv.times(jty);
		if (debug_level>2) {
			System.out.println("mdelta");
			mdelta.print(18, 6);
		}

		double scale = 1.0;
		double []  delta =      mdelta.getColumnPackedCopy();
		double []  new_vector = parameters_vector.clone();
		for (int i = 0; i < parameters_vector.length; i++) {
			new_vector[i] += scale * delta[i];
		}
		
		double [] fx = getFxDerivs(
				new_vector, // double []         vector,
				last_jt,           // final double [][] jt, // should be null or initialized with [vector.length][]
				debug_level);      // final int         debug_level)
		double [] rms = new double[2];
		last_ymfx = getYminusFxWeighted(
//				vector_XYS, // final double [][] vector_XYS,
				fx, // final double []   fx,
				rms); // final double []   rms_fp // null or [2]
		if (debug_level > 2) {
			/*
			dbgYminusFx(this.last_ymfx, "next y-fX");
			dbgXY(new_vector, "XY-correction");
			*/
		}

		if (last_ymfx == null) {
			return null; // need to re-init/restart LMA
		}

		this.good_or_bad_rms = rms.clone();
		if (rms[0] < this.last_rms[0]) { // improved
			rslt[0] = true;
			rslt[1] = rms[0] >=(this.last_rms[0] * (1.0 - rms_diff));
			this.last_rms = rms.clone();
			this.parameters_vector = new_vector.clone();
			if (debug_level > -2) { //
				String  save_dir =    debug_path;
				String  debug_title = getParametersDebugTitle();
				boolean save_all =    (debug_image != null);
				boolean show_this =   debug_level > 3;
				boolean show_all =    debug_level > 4;
				debugImageSaveShow(
						save_dir,     // String  save_dir, // save if not null
						debug_title, // String  title,    // not null
						save_all,    // boolean save_all,
						show_this,   // boolean show_this,
						show_all);   // boolean show_all)
			}
		} else { // worsened
			rslt[0] = false;
			rslt[1] = false; // do not know, caller will decide
			// restore state
			fx = getFxDerivs(
					parameters_vector, // double []         vector,
					last_jt,           // final double [][] jt, // should be null or initialized with [vector.length][]
					debug_level);      // final int         debug_level)
			last_ymfx = getYminusFxWeighted(
					fx, // final double []   fx,
					this.last_rms); // final double []   rms_fp // null or [2]
			if (last_ymfx == null) {
				return null; // need to re-init/restart LMA
			}
			if (debug_level > 2) {
				/*
				 dbgJacobians(
							corr_vector, // GeometryCorrection.CorrVector corr_vector,
							1E-5, // double delta,
							true); //boolean graphic)
							*/
			}
		}
		return rslt;
	}
	
	public String getParametersDebugTitle() {
		// alpha_en_holes -> alpha_mm_hole = NaN;
		// hifreq_weight
		String  debug_title = "parameters_vector-x"+woi.x+"-y"+woi.y+"-w"+woi.width+"-h"+woi.height;
		debug_title += "-hf"+hifreq_weight;
		debug_title += "-al"+alpha_loss+"-alo"+alpha_offset+"-alp"+alpha_lpf+"-als"+alpha_scale_avg+(alpha_piece_linear?"-alin":"-acos");
		debug_title += "-ap"+alpha_push+"-apn"+alpha_push_neutral+"-apc"+alpha_push_center+(alpha_en_holes?("-amm"+alpha_mm_hole):"");
		debug_title += "-tl"+terr_lpf+"-vl"+veget_lpf+"-tp"+terr_pull0+"-vp"+veget_pull0;
		debug_title += "-bp"+boost_parallax;
		debug_title += from_file?"-file":"-new";
		if (um_weight > 0) {
			debug_title +="-um"+um_sigma+"_"+um_weight;
		}
		return debug_title;
	}
	
	
	public void debugImageSaveShow(
			String  save_dir, // save if not null
			String  title,    // not null
			boolean save_all,
			boolean show_this,
			boolean show_all) {
		int [] wh = new int [2];
		double [] debug_img =  parametersImage (
				parameters_vector, // double [] vector,
				1,                // int       gap)
				wh,
				null); // title);
		String title_all = title+"-live";
		if ((debug_image != null) && (debug_index < debug_image.length)) {
			title += "-"+debug_index;
			debug_image[debug_index++] = debug_img;
		}
		
		if (show_this) {
			ShowDoubleFloatArrays.showArrays(
					debug_img,
					wh[0],
					wh[1],
					title);
		}
		if (save_all || show_all) {
			double [][] dbg_img = new double [debug_index][];
			String [] titles = new String[debug_index];
			for (int i = 0; i < debug_index; i++) {
				titles[i] = ""+i;
				dbg_img[i] = debug_image[i];
			}
			if (show_all) {
				ShowDoubleFloatArrays.showArrays(
						debug_image,
						wh[0],
						wh[1],
						true,
						title_all,
						titles);
			}
			if (save_all) {
				String file_path = save_dir+title_all+".tiff";
				(new File(save_dir)).mkdirs();
				ImageStack imageStack = ShowDoubleFloatArrays.makeStack(dbg_img, wh[0], wh[1], titles);
				ImagePlus imp = new ImagePlus( title, imageStack);
				FileSaver fs=new FileSaver(imp);
				fs.saveAsTiff(file_path);
				System.out.println("saveDoubleArrayInModelDirectory(): saved "+file_path);
			}
		}
	}
	
	
	private double [][] getWJtJlambda(
			final double      lambda,
			final double [][] jt,
			final boolean [][] pivot)
	{
		final int num_pars = jt.length;
		final int num_pars2 = num_pars * num_pars;
		final int nup_points = jt[0].length;
		final double [][] wjtjl = new double [num_pars][num_pars];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int indx = ai.getAndIncrement(); indx < num_pars2; indx = ai.getAndIncrement()) {
						int i = indx / num_pars;
						int j = indx % num_pars;
						// diagonals are set true in pivot[i][i] = true;
						if ((j >= i) && ((pivot == null) || pivot[i][j])) {
							double d = 0.0;
							for (int k = 0; k < nup_points; k++) {
								if (jt[i][k] != 0) {
									d+=0;
								}
								d += weights[k]*jt[i][k]*jt[j][k];
							}
							wjtjl[i][j] = d;
							if (i == j) {
								wjtjl[i][j] += d * lambda;
							} else {
								wjtjl[j][i] = d;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return wjtjl;
	}
	
	
	public double [] getRms() {
		return last_rms;
		
	}

	public double [] getInitialRms() {
		return initial_rms;
	}	
	
	public void updateFromParameters(
			double [] vector) {
		
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int pindx = ai.getAndIncrement(); pindx < vector.length; pindx = ai.getAndIncrement()) {
						int [] indices = par_rindex[pindx];
						tvao[indices[0]][indices[1]] = vector[pindx];
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
	}
	
	public void showResults(
			String      title,
			double []   vector,
			double      threshold_terrain,
			double      min_max_terrain, //0.1
			double      min_terrain, //0.001
			double      min_vegetation) { // 0.5
		double [][] tvas = getTVASWoi(
				vector);    // final double [] vector)
		double [][] terrain_weights_max = getTerrainWeights(
				threshold_terrain, // final double      alpha_threshold, // discard images with too low transparency
				vector);           // final double []   vector)
		double [] terrain_masked =     tvas[0].clone();
		double [] vegetation_masked =  tvas[1].clone();
		for (int i = 0; i < terrain_masked.length; i++) {
			if (!(terrain_weights_max[0][i] >= min_terrain))      terrain_masked[i] = Double.NaN;
			if (!(terrain_weights_max[1][i] >= min_max_terrain)) terrain_masked[i] = Double.NaN;
			if (!(tvas[2][i] >=      min_vegetation)) vegetation_masked[i] = Double.NaN;
		}
		double [][] tvww = {terrain_masked, vegetation_masked, tvas[0], tvas[1], terrain_weights_max[0], terrain_weights_max[1],tvas[2]}; // no scene offsets
		String [] titles = {"terr>"+min_terrain,"veget>"+min_vegetation,"terr","veget","terr_str","terr_max","alpha"};
		
		ShowDoubleFloatArrays.showArrays(
				tvww,
				woi.width,
				woi.height,
				true,
				title,
				titles);
		return;
	}
	
	public double [][] getTVASWoi(
			final double [] vector){
		final int woi_length = woi.width*woi.height;
		double [][] tvas = new double [4][];
		for (int i = 0; i < 3; i++) tvas[i] = new double [woi_length];
		tvas[3] = new double [num_scenes];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int windx = ai.getAndIncrement(); windx < woi_length; windx = ai.getAndIncrement()) {
						int x = windx % woi.width + woi.x;
						int y = windx / woi.width + woi.y;
						int indx = x + y * full.width;
						for (int i = 0; i < 3; i++) {
							tvas[i][windx] = tvao[i][indx]; // non-adjusted parameters
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		
		
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int pindx = ai.getAndIncrement(); pindx < vector.length; pindx = ai.getAndIncrement()) {
						int [] indices = par_rindex[pindx];
						if (indices[0] <3) {
							int wx = indices[1] % full.width - woi.x;
							int wy = indices[1] / full.width - woi.y;
							int windx = wx + wy * woi.width;
							tvas[indices[0]][windx] = vector[pindx]; // all adjusted parameters, including offsets
						} else {
							tvas[indices[0]][ indices[1]] = vector[pindx]; // all adjusted parameters, including offsets
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return tvas;
	}
	
	public static double [] laplacian(
			final boolean   gaussian,
			final double [] data_in,
			final int       width,
			final double    weight_diag) {
		final boolean zero_missing = true;  // set no-neighbor to 0, not NaN
		final int height = data_in.length/width;
		final double [] data_out = new double [data_in.length];
		Arrays.fill(data_out, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [] weights8 = new double [8];
		weights8[0] = 0.25/(weight_diag+1);
		weights8[1] = weights8[0]*weight_diag;
		for (int i = 2; i < 8;i++) {
			weights8[i] = weights8[i % 2];
		}
		final int dir_step = (weight_diag > 0) ? 1 : 2;
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width,height);
					for (int nPix = ai.getAndIncrement(); nPix < data_in.length; nPix = ai.getAndIncrement()) if(!Double.isNaN(data_in[nPix])){
						double sw = 0, swd = 0;
						for (int dir = 0; dir < 8; dir += dir_step) {
							int nPix1= tn.getNeibIndex(nPix, dir);
							if ((nPix1 >= 0) && !Double.isNaN(data_in[nPix1])) {
								sw  += weights8[dir];
								swd += data_in[nPix1]*weights8[dir];
							}
						}
						if (sw != 0) {
							swd /= sw;
							data_out[nPix] = gaussian? swd : (data_in[nPix] - swd); 
						} else if (zero_missing) { // so each source non-NaN will have non-NaN Laplacian
							data_out[nPix] = 0;
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return data_out;
		
	}
	
	
	
	private double [][] getTerrainWeights(
			final double      alpha_threshold, // discard images with too low transparency
			final double []   vector)
	{
		final int woi_length = woi.width*woi.height;
		final double [][] terrain_scene_weights = new double [num_scenes][woi_length];
		final double [][] terrain_scene_max = new double [num_scenes][woi_length];
		final double [][] full_scene_weights = new double [num_scenes][woi_length];
        final double [][] terrain_weights = new double [2][woi_length];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] alpha =      new double [4];
					for (int n = ai.getAndIncrement(); n < y_vector.length; n = ai.getAndIncrement()) {
						int nscene =        data_source[n][0][0];
						int indx =          data_source[n][0][1];
						int x =             indx % full.width - woi.x;
						int y =             indx / full.width - woi.y;
						int windx =         x + y * woi.width;
						double [] cw =      corners_weights[n]; 
						int [] indx_alpha = data_source[n][2];
						double sum_a =0;
						if (cw != null) {
							for (int i = 0; i < 4; i++ ) {
								int ia=indx_alpha[i];
								alpha[i] =      cw[i] * ((ia >= 0) ? vector[ia]: tvao[TVAO_ALPHA][-1-ia]);
								sum_a += alpha[i];
							}
							double tw = Math.max(0, 1.0 - sum_a);
							terrain_scene_max[nscene][windx] = tw;
							if (tw >= alpha_threshold) {
								terrain_scene_weights[nscene][windx] = weights[n] * tw;
							}
							full_scene_weights[nscene][windx] =  weights[n];
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		// sum for all scenes, divide (undefined - also 0)
		Arrays.fill(terrain_weights[0], Double.NaN);
		Arrays.fill(terrain_weights[1], Double.NaN);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int windx = ai.getAndIncrement(); windx < woi_length; windx = ai.getAndIncrement()) {
						double sum_w = 0, sum_wt = 0, max_t = 0;
						for (int nscene = 0; nscene < num_scenes;nscene++) {
							sum_wt += terrain_scene_weights[nscene][windx];
							sum_w +=  full_scene_weights[nscene][windx];
							max_t = Math.max(max_t, terrain_scene_max[nscene][windx]);
						}
						if (sum_w > 0) {
							terrain_weights[0][windx] = sum_wt/sum_w;
							terrain_weights[1][windx] = max_t;
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		
		// regularization weights and derivatives
		return terrain_weights;
    }
	
	private void setupElevationLMA( // return pivot?
			final double []   vector,
			final boolean     calc_derivatives,
			final int debugLevel) {
		final boolean elevation_parameters = (num_pars_elevation > 0);
		final boolean need_derivs = elevation_parameters && calc_derivatives;
		if (!need_derivs && (elev_sum_weights != null)) {
			return; // nothing to do
		}
		final int woi_length =     woi.width * woi.height;
		final int woi_veg_length = woi_veg.width * woi_veg.height;
//		final int num_pixels =     full.width * full.height;
		elev_woi4 =     new int [num_scenes][][];
		elev_weights4 = new double [num_scenes][][];
		elev_sum_weights = new double [num_scenes][];
		if (need_derivs) {
			elev_dweights4 =    new double [num_scenes][][];
			elev_dsum_weights = new double [num_scenes][][];
//			elev_dsum_pivot = new boolean [num_pars_elevation][num_pars_elevation];
		}
	//elev_radius[num_pixels];	
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
//		final boolean [][][] pivot_thread = new boolean[threads.length][woi_veg_length][num_pars_elevation];
		final ArrayList<ArrayList<HashSet<Integer>>> contrib_threads = new ArrayList<ArrayList<HashSet<Integer>>>(threads.length);
		if (need_derivs) {
			for (int nthread = 0; nthread < threads.length; nthread++) {
				ArrayList<HashSet<Integer>> contrib_thread = new ArrayList<HashSet<Integer>>(woi_length);
				contrib_threads.add(contrib_thread);
			}
		}
		
		final int woi_x1 = woi.x+woi.width;
		final int woi_y1 = woi.y+woi.height;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int nthread = ati.getAndIncrement();
					double [] wnd_x = new double [3];
					double [] dwnd_x= new double [3];
					ArrayList<HashSet<Integer>> contrib_thread = null;
					if (need_derivs) {
						contrib_thread = contrib_threads.get(nthread);
						for (int i = 0; i < woi_length; i++) {
							contrib_thread.add(new HashSet<Integer>());
						}
					}

					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement())  if (valid_scenes[nScene]){
						elev_woi4[nScene] = new int [woi_veg_length][];
						elev_weights4[nScene] = new double [woi_veg_length][];
						elev_sum_weights[nScene] = new double [woi_length];
						if (need_derivs) {
							elev_dweights4[nScene] = new double [woi_veg_length][];
							elev_dsum_weights[nScene] = new double [num_pars_elevation][woi_length];
						}
						for (int wvindex = 0; wvindex < woi_veg_length; wvindex++) if (valid_vegetation[wvindex]) {
							int wvx = wvindex % woi_veg.width;   // relative to woi_veg
							int wvy = wvindex / woi_veg.width;   // relative to woi_veg
							int x = wvx + woi_veg.x;             // relative to full
							int y = wvy + woi_veg.y;             // relative to full
							int wx = x - woi.x;                  // relative to woi
							int wy = x - woi.y;                  // relative to woi
							int npix = x + y * full.width;
//							double elevation =  tvao[TVAO_ELEVATION][npix];
							double elevation;
							if (elevation_parameters) {
								int npar = par_index[TVAO_ELEVATION][npix]; 
								if (npar >= 0) {
									elevation = vector[npar];
								} else {
									System.out.println("setupElevationLMA(): BUG:  par_index["+TVAO_ELEVATION+"]["+npix+"] ="+npar);
									continue;
								}
							} else {
								elevation =  tvao[TVAO_ELEVATION][npix];
							}

							double [] scales =  scales_xy[nScene][npix];
							double radius = elev_radius[npix];
							/*
							int diameter = (int) Math.floor(2*radius);
							int diameter2 = diameter * diameter;
							if (wnd_x.length < (diameter+1)) {
								wnd_x = new double[diameter+1];
								if (need_derivs) {
									dwnd_x = new double[diameter+1];
								}
							}
							*/
							Arrays.fill(wnd_x, Double.NaN);
							double px = x - scales[0] * elevation;
							double py = y - scales[1] * elevation;
							double px0 = px - radius, py0 = py - radius;
							int ipx0 = (int) Math.floor(px0) + 1;
							int ipy0 = (int) Math.floor(py0) + 1;
							double px1 = px+radius, py1 = py+radius;
							int ipx1 = (int) Math.ceil(px1) - 1;
							int ipy1 = (int) Math.ceil(py1) -1;
							int dia_y = ipy1-ipy0+1; 
							int dia_x = ipx1-ipx0+1;
							int dia2 = dia_y*dia_x;
							if (wnd_x.length < dia_x) {
								wnd_x = new double[dia_x];
								if (need_derivs) {
									dwnd_x = new double[dia_x];
								}
							}
							
							for (int ipy = ipy0; ipy < py1; ipy++) if (ipy >= woi.y){
								if (ipy >= woi_y1) break;
								int wpy = ipy-woi.y;
								double wnd_y = 0.5*(1.0+Math.cos((ipy-py)*Math.PI/radius));
								double dwnd_y = need_derivs?
										(-0.5*Math.PI/radius*scales[1]*Math.sin((ipy-py)*Math.PI/radius))   :
											0;
								int dy = ipy - ipy0;
								for (int ipx = ipx0; ipx < px1; ipx++)  if (ipx >= woi.x){
									if (ipx >= woi_x1) break;
									int wpx = ipx-woi.x;
									int wpindex = wpx + wpy*woi.width;
									if (overlaid[wpindex]) {
										if (elev_woi4[nScene][wvindex] == null) {
											elev_woi4[nScene][wvindex] = new int[dia2]; // meter2];
											Arrays.fill(elev_woi4[nScene][wvindex], -1);
											elev_weights4[nScene][wvindex] = new double[dia2]; // meter2];meter2];
											if (need_derivs) {
												elev_dweights4[nScene][wvindex] = new double[dia2]; // meter2];meter2];
											}
										}
										int dx = ipx - ipx0;
										int dindx = dx + dy*dia_x; // meter;
										elev_woi4[nScene][wvindex][dindx] = wpindex;
										if (Double.isNaN(wnd_x[dx])) {
											wnd_x[dx]=0.5*(1.0+Math.cos((ipx-px)*Math.PI/radius));
											if (need_derivs) {
												dwnd_x[dx]=-0.5*Math.PI/radius*scales[0]*Math.sin((ipx-px)*Math.PI/radius);
											}
										}
										double ww= wnd_y*wnd_x[dx];
										elev_weights4[nScene][wvindex][dindx] = ww;
										elev_sum_weights[nScene][wpindex] += ww;
										if (need_derivs) {
											double dww =  wnd_y * dwnd_x[dx] +  dwnd_y*wnd_x[dx];
											elev_dweights4[nScene][wvindex][dindx] =  dww;
											int par_indx_elev = par_index[TVAO_ELEVATION][npix]; // ipx+ipy*full.width];
											elev_dsum_weights[nScene][par_indx_elev-ind_pars_elevation][wpindex] += dww;
											contrib_thread.get(wpindex).add(par_indx_elev); // full parameter index 
//											pivot_thread[nthread][wvindex][par_indx_elev] = true;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		if (need_derivs) {
			// combine pivots
//			final boolean [][] pivot_combo = new boolean[woi_veg_length][num_pars_elevation];
			final ArrayList<HashSet<Integer>> contrib_combo = new ArrayList<HashSet<Integer>>(woi_length);
			for (int i = 0; i < woi_length; i++) {
				contrib_combo.add(new HashSet<Integer>());
			}
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement())  if (valid_terrain[wPix]){
							HashSet <Integer> contribs = contrib_combo.get(wPix);
							for (int nthread = 0; nthread < threads.length; nthread++) {
								contribs.addAll(contrib_threads.get(nthread).get(wPix));
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);

			elev_contribs = contrib_combo; // for each of the woi pixels - set of full elevation parameter indices that influence it 
			/*
			elev_dsum_pivot = getPivotFromSets(
					ind_pars_elevation, // final int par_index,
					num_pars_elevation, // final int par_number,
					contrib_combo); // final ArrayList<HashSet<Integer>> sets)
			*/
			
		}
		if (debugLevel > 1) {
			String title = "setupElevationLMA.tiff";
			String [] titles = new String [num_scenes];
			String [] top_titles = {"sum_weights","contribs"};
			double [][][] dbg_img = new double [top_titles.length][num_scenes][];
			for (int nscene = 0; nscene < num_scenes; nscene++) {
				titles[nscene]= "scene_"+nscene;
				if (valid_scenes[nscene]) {
					for (int t = 0; t < dbg_img.length; t++) {
						dbg_img[t][nscene] = new double [woi_length];
						Arrays.fill(dbg_img[t][nscene], Double.NaN);
					}
					for (int i = 0; i < woi_length; i++) {
						dbg_img[0][nscene][i] = elev_sum_weights[nscene][i];	
					}
					if (need_derivs) {
						for (int i = 0; i < woi_length; i++) {
							dbg_img[1][nscene][i] = elev_contribs.get(i).size();
						}
					}
				}
			}
			ShowDoubleFloatArrays.showArraysHyperstack(
					dbg_img,    // double[][][] pixels, 
					woi.width,      // int          width, 
					title,      // "terrain_vegetation_render.tiff", // 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)
		}
		return;		
	}
	
	
	/**
	 * Create a pivot (square symmetrical) array of the dependent parameters to improve
	 * performance of JtJ calculation (only multiply rows that are marked in the pivot table
	 * @param par_index start parameter index of the table
	 * @param par_number number of parameters (table width==height)
	 * @param sets ArrayList of sets of parameter numbers, parameters in the same set will have
	 *        true on their intersection
	 * @return square boolean array showing non-zero elements in JtJ matrix
	 */
	private static boolean [][] getPivotFromSets(
			final int par_index,
			final int par_number,
			final ArrayList<HashSet<Integer>> sets) {
		final boolean [][] pivot = new boolean [par_number][par_number];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPar = ai.getAndIncrement(); nPar < par_number; nPar = ai.getAndIncrement()){
						int full_par = nPar+par_index;
						for (HashSet<Integer> par_set:sets) {
							if (par_set.contains(full_par)) {
								for (int par:par_set) {
									pivot[nPar][par - par_index] = true; // will add symmetrical and self
								}
							}
						}
						pivot[nPar][nPar] = true;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return pivot;
	}

	/**
	 * Add neighbors of parameters to the pivot (square symmetrical) array of the dependent
	 * parameters to improve performance of JtJ calculation.
	 * @param pivot 2D pivot table to be augmented
	 * @param neibs array of the 4 neighbors for each parameter (only non-negative count)
	 * @param start_index start parameter index in the neibs[][] array
	 */
	private static void pivotAddNeighbors(
			final boolean [][] pivot,
			final int     [][] neibs,
			final int          start_index) {
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPar = ai.getAndIncrement(); nPar < neibs.length; nPar = ai.getAndIncrement()){
						int full_par = nPar+start_index;
						int [] this_neibs = neibs[nPar];
						for (int dir = 0; dir < this_neibs.length; dir ++) {
							int neib = this_neibs[dir];
							if (neib >=0) {
								pivot[full_par][neib] = true;
							}
						}
						pivot[full_par][full_par] = true;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// transposed
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPar = ai.getAndIncrement(); nPar < neibs.length; nPar = ai.getAndIncrement()){
						int full_par = nPar+start_index;
						int [] this_neibs = neibs[nPar];
						for (int dir = 0; dir < this_neibs.length; dir ++) {
							int neib = this_neibs[dir];
							if (neib >=0) {
								pivot[neib][full_par] = true;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
	}
	
	
	private double [] getFxDerivs(
			final double []   vector,
			final double [][] jt, // should be null or initialized with [vector.length][]
			final int         debug_level)	{
		final boolean dbg1 = (debug_level > 4);
//		final int     dbg_y_indx = 900; // and multiples ?
		final int     dbg_npar =   900; // first vegetation
		
		setupElevationLMA( // return pivot?
				vector, // final double []   vector,
				(jt != null), // final boolean     calc_derivatives,
				debug_level); // final int debugLevel) {
		final int woi_length =     woi.width * woi.height;
		final int woi_veg_length = woi_veg.width * woi_veg.height;
		final int num_pixels =     full.width * full.height;
		
		final boolean need_derivs =           (jt != null);
		final boolean elevation_parameters =  (num_pars_elevation  > 0);
		final boolean vegetation_parameters = (num_pars_vegetation > 0);
		final boolean alpha_parameters =      (num_pars_alpha      > 0);
		final boolean terrain_parameters =    (num_pars_terrain    > 0);
		final boolean scenes_parameters =     (num_pars_scenes     > 0);
		
		final boolean use_hf =    (y_src_hf != null); // twice longer
		// using 0.5*(1-cos(alpha/2pi) instead of alpha. alpha < 0 -> 0, alpha > 1 -> 1. Depends on other terms for stability 
		double [] fX = new double [weights.length]; // num_pairs + vector.length];
		if (need_derivs) {
			for (int i = 0; i < jt.length; i++) {
				jt[i] = new double [weights.length]; // weights.length];
			}
		}
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final ArrayList<ArrayList<HashSet<Integer>>> contrib_threads = new ArrayList<ArrayList<HashSet<Integer>>>(threads.length);
		if (need_derivs) {
			// For pivot matrix generation (which of the JtJ elements are non-zero)
			for (int nthread = 0; nthread < threads.length; nthread++) {
				ArrayList<HashSet<Integer>> contrib_thread = new ArrayList<HashSet<Integer>>(woi_length);
				contrib_threads.add(contrib_thread);
			}
		}
		
		int dbg_scene = -20;
		final int dbg_n = -7402; // -69992;
		final int fdbg_scene = dbg_scene;
		final int dbg_cols=2,dbg_rows=2;
		
		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					// vegetation projections to terrain/y_vector/fX pixels
					double [] alpha_woi =   new double [woi_length];
					double [] veget_woi =   new double [woi_length];
					double [][] dalpha_woi = null; 
					double [][] dveget_woi = null; 
					int nthread = ati.getAndIncrement();
					ArrayList<HashSet<Integer>> contrib_thread = null;
					if (need_derivs) {
						contrib_thread = contrib_threads.get(nthread);
						for (int i = 0; i < woi_length; i++) {
							contrib_thread.add(new HashSet<Integer>());
						}
						dalpha_woi =   new double [num_pars_alpha][woi_length]; // first index is 0 if not adjusted 
						dveget_woi =   new double [num_pars_vegetation][woi_length]; // first index is 0 if not adjusted 
					}
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) if (valid_scenes[nScene]){
						Arrays.fill(alpha_woi,0);
						Arrays.fill(veget_woi,0);
						if (need_derivs) {
							for (int i = 0; i < dalpha_woi.length; i++) {
								Arrays.fill(dalpha_woi[i],0);
							}
							for (int i = 0; i < dveget_woi.length; i++) {
								Arrays.fill(dveget_woi[i],0);
							}
						}
						double scene_offset;
						int nspar = -1; // scene offset parameter
						if (scenes_parameters) {
							nspar = par_index[TVAO_SCENE_OFFSET][nScene]; // full parameter index
							if (nspar >= 0) {
								scene_offset = vector[nspar];
							} else {
								System.out.println("getFxDerivs(): BUG:  par_index["+TVAO_SCENE_OFFSET+"]["+nScene+"] ="+nspar);
								scene_offset = 0;
							}	
						} else {
							scene_offset = tvao[TVAO_SCENE_OFFSET][nScene];
						}
						for (int wvindex = 0; wvindex < woi_veg_length; wvindex++) if (valid_vegetation[wvindex]) {
							int wvx = wvindex % woi_veg.width;   // relative to woi_veg
							int wvy = wvindex / woi_veg.width;   // relative to woi_veg
							int x = wvx + woi_veg.x;             // relative to full
							int y = wvy + woi_veg.y;             // relative to full
							int npix = x + y * full.width;
							double vegetation;
							double alpha;
							int nvpar = -1;
							int napar = -1;
							if (vegetation_parameters) {
								nvpar = par_index[TVAO_VEGETATION][npix]; 
								if (nvpar >= 0) {
									vegetation = vector[nvpar];
								} else {
									System.out.println("getFxDerivs(): BUG:  par_index["+TVAO_VEGETATION+"]["+npix+"] ="+nvpar);
									continue;
								}	
							} else {
								vegetation =  tvao[TVAO_ELEVATION][npix];
							}
							if (alpha_parameters) {
								napar = par_index[TVAO_ALPHA][npix]; 
								if (napar >= 0) {
									alpha = vector[napar];
								} else {
									System.out.println("getFxDerivs(): BUG:  par_index["+TVAO_ALPHA+"]["+npix+"] ="+napar);
									continue;
								}	
							} else {
								alpha =  tvao[TVAO_ALPHA][npix];
							}
							int [] terrain_woi_ind =      elev_woi4[nScene][wvindex];
							if (terrain_woi_ind != null) {
								double [] terrain_woi_weights = elev_weights4[nScene][wvindex];
								for (int i = 0; i < terrain_woi_ind.length; i++) {
									int windx = terrain_woi_ind[i];
									if (windx >= 0) {
										double w =  terrain_woi_weights[i];
										veget_woi[windx] += w * vegetation;
										alpha_woi[windx] += w * alpha;
										if (need_derivs) {
											if (nvpar >= 0) {
												contrib_thread.get(windx).add(nvpar); // full parameter index
												dveget_woi[nvpar-ind_pars_vegetation][windx] += w;
											}
											if (napar >= 0) {
												contrib_thread.get(windx).add(napar); // full parameter index
												dalpha_woi[napar-ind_pars_alpha][windx] += w;
											}
										}
									}
								}
							}
								
						} // for (int wvindex = 0; wvindex < woi_veg_length; wvindex++) if (valid_vegetation[wvindex]) {
						
						for (int windx = 0; windx < woi_length; windx++) if (valid_terrain[windx]) {
							int wx = windx % woi.width;   // relative to woi
							int wy = windx / woi.width;   // relative to woi
							int x = wx + woi.x;             // relative to full
							int y = wy + woi.y;             // relative to full
							int npix = x + y * full.width;
							double terrain;
							int ntpar = -1; // full parameter
							if (terrain_parameters) {
								ntpar = par_index[TVAO_TERRAIN][npix]; // full parameter index
								if (ntpar >= 0) {
									terrain = vector[ntpar];
								} else {
									System.out.println("getFxDerivs(): BUG:  par_index["+TVAO_TERRAIN+"]["+npix+"] ="+ntpar);
									continue;
								}	
							} else {
								terrain =  tvao[TVAO_TERRAIN][npix];
							}
							
//							terrain_woi[windx] = terrain; not needed,
							// Fill in fX for this scene
							int y_indx = y_wsrc[nScene][windx]; // y_indx is different for threads, no conflict
							double d = terrain;
							if (overlaid[windx]) {
								double sw = elev_sum_weights[nScene][windx];
								if (sw == 0) {
									System.out.println("getFxDerivs(): BUG:  elev_sum_weights["+nScene+"]["+windx+"]=0");
								} else { // OK, non-zero
									double avg_veget = veget_woi[windx] / sw;
									double avg_alpha = alpha_woi[windx] / sw;
							        double k;
							        if (alpha_piece_linear) {
							            k = (avg_alpha < 0)? 0: ((avg_alpha > 1) ? 1.0: avg_alpha);	
							        } else {
							            k = (avg_alpha < 0)? 0: ((avg_alpha > 1) ? 1.0: 0.5 * (1.0 - Math.cos(avg_alpha*Math.PI)));	
							        }
							        d = terrain * (1.0 - k) + avg_veget * k;
							        if (need_derivs) {
							        	if (ntpar >= 0) {
							                jt[ntpar][y_indx] = 1 - k;// sum_a; // d/dterrain
							        	}
							        	if (vegetation_parameters || alpha_parameters) {
							        		HashSet<Integer> pars_set =  contrib_thread.get(windx); // may have unrelated (from other scenes) but still a small number
							        		for (int npar:  pars_set) {
							        			int nvpar = npar - ind_pars_vegetation; // 0-based index of the vegetation
							        			if((nvpar >= 0) && (nvpar < num_pars_vegetation)) {
							        				jt[npar][y_indx] = k * dveget_woi[nvpar][windx]/sw ;
								        			contrib_thread.get(windx).add(npar); // full parameter index, vegetation
								        			if (dbg1 && (npar== dbg_npar)) {
								        				System.out.println("nscene="+nScene+" ntherad="+nthread+"y_indx="+y_indx+" windx="+windx+" sw="+sw+" d="+d+" k="+k+" jt="+jt[npar][y_indx]+" dveget_woi="+dveget_woi[nvpar][windx]);
								        			}
							        			}  else {
								        			int napar = npar - ind_pars_alpha;
								        			if ((napar >= 0) && (napar < num_pars_alpha)) {
								        				if ((avg_alpha >= 0) && (avg_alpha <= 1.0)) {
								                            if (alpha_piece_linear) {
								                            	jt[npar][y_indx] = dalpha_woi[napar][windx]/sw * (avg_veget - terrain);
								                            } else {
								                            	jt[npar][y_indx] = dalpha_woi[napar][windx]/sw * (avg_veget - terrain) *
								                            			0.5 * Math.PI * Math.sin(avg_alpha * Math.PI);
								                            }
								        				}
									        			contrib_thread.get(windx).add(npar); // full parameter index, alpha
								        			}							        				
							        			}
							        		}
							        	}
							        	if (elevation_parameters) { // here - just derivatives as direct influence of elevations is already processed
							        		// we have dsw_delevations (for all elevation parameters) - ,
							        		HashSet<Integer> epars_set =  elev_contribs.get(windx); // may have unrelated (from other scenes) but still a small number
							        		for (int npar:  epars_set) { // elevation parameters - full
							        			int nepar = npar-ind_pars_elevation;
							        			if ((nepar >=0 ) && (nepar < num_pars_elevation)) {
							        				int findx = par_rindex[npar][1]; // full image pixel;
							        				double veget_src = tvao[TVAO_VEGETATION][findx];
							        				if (vegetation_parameters) {
							        					int veg_indx = par_index[TVAO_VEGETATION][findx];
							        					if (veg_indx >=0) {
							        						veget_src=vector[veg_indx];
							        					} else {
															System.out.println("getFxDerivs(): BUG: par_index["+TVAO_VEGETATION+"]["+findx+"]="+veg_indx);
							        					}
							        				}
							        				double alpha_src = tvao[TVAO_ALPHA][findx];
							        				if (alpha_parameters) {
							        					int alpha_indx = par_index[TVAO_ALPHA][findx];
							        					if (alpha_indx >=0) {
							        						alpha_src=vector[alpha_indx];
							        					} else {
															System.out.println("getFxDerivs(): BUG: par_index["+TVAO_ALPHA+"]["+findx+"]="+alpha_indx);
							        					}
							        				}
							        				// do not forget that no alpha derivatives if alpha outside of (0,1)
							        				
							        				double dsw_dnepar = elev_dsum_weights[nScene][nepar][windx];
							        				// d = terrain * (1.0 - k) + avg_veget * k;
							        				// dd_depar = k * davg_veget_depar +dk_depar * (avg_veget-terrain)
							        				// dk_depar = dk_dalpha_avg * dalpha_avg_depar
							        				// dalpha_avg_depar = 1.0 | 0.5 * PI * sin(avg_alpha * PI) (depending on mode
							        				double davg_veget_depar= dsw_dnepar/sw * (veget_src - avg_veget);
							        				double dd_depar = k * davg_veget_depar;
							        				if ((avg_alpha >= 0) && (avg_alpha <= 1.0)) {
							        					double dk_dalpha_avg = alpha_piece_linear ? 1.0 :  (0.5 * Math.PI * Math.sin(avg_alpha*Math.PI));
							        					double dalpha_avg_depar =  dsw_dnepar/sw * (alpha_src - avg_alpha);
							        					double dk_depar = dk_dalpha_avg * dalpha_avg_depar;
							        					dd_depar += dk_depar * (avg_veget-terrain);
							        				}
							        				jt[npar][y_indx] = dd_depar;
							        				// add to the set of parameters
								        			contrib_thread.get(windx).add(npar); // full parameter index, elevation
							        			} else { // bug
													System.out.println("getFxDerivs(): BUG: npar="+npar+" -> nepar="+nepar+" >= num_pars_elevation="+num_pars_elevation);
							        			}
							        		}
							        	}
							        }
								}
							} else { // terrain-only pixel
								d = terrain;
								if (need_derivs && (ntpar >= 0)) {
									jt[ntpar][y_indx] = 1.0;
								}
							}
							fX[y_indx] = d + scene_offset;
							if (need_derivs && (ntpar >= 0)) {
								contrib_thread.get(windx).add(ntpar); // full parameter index
							}
							if (need_derivs && (nspar >= 0)) {
								jt[nspar][y_indx] = 1.0;
								contrib_thread.get(windx).add(nspar); // full parameter index
							}
						} // for (int windx = 0; windx < woi_length; windx++) if (valid_terrain[windx]) {						
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		// process hi-frequnecy (laplacians)  - needs contrib_threads combining
		// do later after more parameter inter-relations are added
		// combine contrib_threads
		final ArrayList<HashSet<Integer>> contrib_combo = need_derivs? new ArrayList<HashSet<Integer>>(woi_length): null;
		if (need_derivs) {
			for (int i = 0; i < woi_length; i++) {
				contrib_combo.add(new HashSet<Integer>());
			}
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement())  if (valid_terrain[wPix]){
							HashSet <Integer> contribs = contrib_combo.get(wPix);
							for (int nthread = 0; nthread < threads.length; nthread++) {
								contribs.addAll(contrib_threads.get(nthread).get(wPix));
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			// will overwrite or keep them 
			param_contribs = contrib_combo; // for each of the woi pixels - set of full elevation parameter indices that influence it 
		}
		// TODO: no contributors consolidation here if no HF
// 
		if (use_hf) {
			final int ind_next = y_src.length;
			ai.set(0);
			ati.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						int nthread = ati.getAndIncrement();
						ArrayList<HashSet<Integer>> contrib_thread = null;
						if (need_derivs) {
							contrib_thread = contrib_threads.get(nthread);
							if (contrib_thread.size() != woi_length) {
								contrib_thread.clear();
								for (int i = 0; i < woi_length; i++) {
									contrib_thread.add(new HashSet<Integer>());
								}
							} else {
								for (int i = 0; i < woi_length; i++) {
									contrib_thread.get(i).clear();
								}								
							}
						}						

						for (int n = ai.getAndIncrement(); n < y_src_hf.length; n = ai.getAndIncrement()) {
							int n_hf = n+ind_next; // full y/fX index
							int nscene = y_src_hf[n][YSRC_SCENE];
							int findx =  y_src_hf[n][YSRC_FINDEX];
							int wx = (findx % full.width) - woi.x; 
							int wy = (findx / full.width) - woi.y;
							int windx = wx + wy * woi.width;
							int n_lf = y_wsrc[nscene][windx];
							double d_lf = fX[n_lf]; // center value
							double swd = 0, sw=0;
							for (int dir = 0; dir < YSRC_NEIBS; dir++) {
								int nindx = y_src_hf[n][dir + YSRC_NEIB0]; 
								if (nindx >=0) {
									//see, maybe discard all border tiles 
									double d_hf = fX[nindx]; // neighbor value
									swd += d_hf;
									sw += 1;
								}
							}
							if (sw < YSRC_NEIBS) {
								System.out.println("getFxDerivs(): BUG: HF - not all neighbors: n="+n+", scene="+nscene+
										"findx="+findx+", neibs = ["+y_src_hf[n][YSRC_NEIB0+0]+", "+y_src_hf[n][YSRC_NEIB0+1]+
										", "+y_src_hf[n][YSRC_NEIB0+2]+", "+y_src_hf[n][YSRC_NEIB0+3]+"]");
								continue;
							}
							swd /= sw;
							fX[n_hf] = d_lf - swd;
							if (jt != null) {
								// copy all derivatives from the center pixel lf (no need to add contibutors
								HashSet<Integer> pars_set_center =  param_contribs.get(windx); // contributors before laplacians
				        		for (int npar:  pars_set_center) { // all parameters here
				        			jt[npar][n_hf] = jt[npar][n_lf];
				        		}
				        		// add scaled by -0.25 (-1/sw) parameters from all neighbors;
								for (int dir = 0; dir < YSRC_NEIBS; dir++) {
									int n_neib = y_src_hf[n][dir + YSRC_NEIB0]; // full
									int findx_neib = y_src[n_neib][YSRC_FINDEX]; // full
									int wx_neib = (findx_neib % full.width) - woi.x; 
									int wy_neib = (findx_neib / full.width) - woi.y;
									int windx_neib = wx_neib + wy_neib * woi.width;
									HashSet<Integer> pars_set_neib =  param_contribs.get(windx_neib); // contributors before laplacians
					        		for (int npar_neib:  pars_set_neib) { // all parameters here
					        			jt[npar_neib][n_hf] -= jt[npar_neib][n_neib]/sw;
					        			contrib_thread.get(windx).add(npar_neib); // full parameter index					        			
					        		}
								}				        		
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			// re-consolidate contributors
			if (need_derivs) {
				for (int i = 0; i < woi_length; i++) {
					contrib_combo.get(i).clear(); // erase sets; add(new HashSet<Integer>());
				}
				ai.set(0);
				for (int ithread = 0; ithread < threads.length; ithread++) {
					threads[ithread] = new Thread() {
						public void run() {
							for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement())  if (valid_terrain[wPix]){
								HashSet <Integer> contribs = contrib_combo.get(wPix);
								for (int nthread = 0; nthread < threads.length; nthread++) {
									contribs.addAll(contrib_threads.get(nthread).get(wPix));
								}
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
				// will overwrite or keep them 
				param_contribs = contrib_combo; // for each of the woi pixels - set of full elevation parameter indices that influence it 
			}
		} // if (use_hf) {
		
		// regularization weights and derivatives
		int ind_next = y_vector.length;
		if (use_scenes_pull0) { // single sample after differences
			fX[ind_next] =0.0;
			for (int n = 0; n < num_pars_scenes; n++) { // only count adjustable scene offsets
				int np = ind_pars_scenes + n; // index of the alpha parameter
				int nscene = par_rindex[np][1];
				double sw=scene_weights[nscene] * scale_scenes_pull;
				fX[ind_next] += sw * vector[np];
				if (need_derivs) {
					jt[np][ind_next] = sw;
				}
			}
			ind_next++;
		} // if (use_scenes_pull0) { // single sample after differences
		
		

		// splitting alpha_lpf from alpha_loss+alpha_push
		// No need here to add contributors as this should not depend on any other parameters 
		if (fits[TVAO_ALPHA] &&  ((alpha_loss > 0) || (alpha_push > 0))) {
			int dbg_nx = -76340;
			final int ind_y_alpha_loss = ind_next;
			ind_next += num_pars_alpha;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_alpha; n = ai.getAndIncrement()) {
							int np = ind_pars_alpha + n; // index of the alpha parameter
							int nx = n + ind_y_alpha_loss; //  y_vector.length; // x - index
							if (nx == dbg_nx) {
								System.out.println("getFxDerivs(): n="+n+", nx="+nx);
							}
							double d = 0;
							fX[nx] = 0.0;
							if (alpha_loss > 0) {
								double alpha = vector[np];
								if (alpha < alpha_offset) {
									d = alpha- alpha_offset;
								} else if (alpha > (1 - alpha_offset)) {
									d = alpha - (1.0 - alpha_offset);
								}
								if (d != 0) {
									fX[nx] = d * d * alpha_loss;
									if (jt != null) {
										jt[np][nx] = 2 * alpha_loss * d; // d/dalpha[i]		
									}							
								}
							}
							double avg = 0;
							int nn = 0;
							// removing all dependence on neighbors!
							/*
							double neib_min = Double.POSITIVE_INFINITY, neib_max = Double.NEGATIVE_INFINITY;
							for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
								int di =  alpha_neibs[n][i];
								d=0;
								if (di >= 0) {
									d = vector[di];  // d - full parameter index
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								} else if (di < -1) {
									d = tvao[TVAO_ALPHA][-di - 2]; 
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								}
							}
							avg /= nn; // average
							*/
							if (alpha_push > 0) {
								/*
								 * p = alpha_push
								 * n = alpha_push_neutral
								 * a0 =        p/(2 * n)
								 * a1 =        p/(2 * (1 - n))
								 * f1(x) =     p/(2 * n)* x * (1 - x/(2*n))
								 * f2(x) =     p * (x - 2*n + 1) * (1 - x) / (4 * (1-n)*(1-n) 
								 * df1(x)/dx = p/(2 * n^2 )*( n - x)
								 * df2(x)/dx = p* (n-x)/(2*(1-n)^2)
								 */
								// average including center:
///								double sum_w = (nn + alpha_push_center);
///								double avg_c = (avg * nn + vector[np] * alpha_push_center) / sum_w;
								
								double sum_w = 1; // (nn + alpha_push_center);
								double avg_c = vector[np];
								
								double one_minus_n = 1.0 - alpha_push_neutral;
///								double one_minus_p = 1.0 - alpha_push;
								double x_minus_p = avg_c - alpha_push;
								if ((avg_c > 0) || (avg_c < 1.0)) {
									if (avg_c <= alpha_push_neutral) {
										fX[nx] += alpha_push / (2 * alpha_push_neutral) * avg_c * (1 - avg_c / (2 * alpha_push_neutral));
									} else {
										fX[nx] += alpha_push * (avg_c - 2 * alpha_push_neutral + 1) * (1 - avg_c) / (4 *one_minus_n*one_minus_n); 
									}
									if (jt != null) {
										double dd;
										if (avg_c <= alpha_push_neutral) {
											dd = alpha_push / (sum_w * 2 * alpha_push_neutral * alpha_push_neutral ) * (alpha_push_neutral  - avg_c);
										} else {
											dd = alpha_push / (sum_w * 2 * one_minus_n * one_minus_n)  * (alpha_push_neutral  - avg_c); 
										}
										jt[np][nx] += dd * alpha_push_center; // 2 * alpha_loss * d; // d/dalpha[i]	
										/*
										for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
											int di =  alpha_neibs[n][i];
											if (di >= 0) {
												jt[di][nx] += dd; // 2 * alpha_loss * d; // d/dalpha[i]
											}
										}
										*/
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		} // if ((alpha_loss > 0) || (alpha_push > 0)){
		
		if (fits[TVAO_ALPHA] && (alpha_lpf >= 0)) {
			int dbg_nx = -76340;
			final int ind_y_alpha_lpf = ind_next;
			ind_next += num_pars_alpha;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_alpha; n = ai.getAndIncrement()) {
							int np = ind_pars_alpha + n; // index of the alpha parameter
							int nx = n + ind_y_alpha_lpf; //  y_vector.length; // x - index
							if (nx == dbg_nx) {
								System.out.println("getFxDerivs(): n="+n+", nx="+nx);
							}
							double d = 0;
							fX[nx] = 0.0;
							double avg = 0;
							int nn = 0;
							double neib_min = Double.POSITIVE_INFINITY, neib_max = Double.NEGATIVE_INFINITY;
							for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
								int di =  alpha_neibs[n][i];
								d=0;
								if (di >= 0) {
									d = vector[di];  // d - full parameter index
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								} else if ((di < -1) && lpf_fixed[TVAO_ALPHA]) {
									d = tvao[TVAO_ALPHA][-di - 2]; 
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								}
							}
							avg /= nn; // average
							// add cost for difference between this alpha and average of 4 neighbors (when they exist
							// applies to alpha before cosine, so it will pull borders even when alpha<0 or alpha > 1 (zero derivatives)
							//alpha_scale_avg
							double mm = neib_min + (neib_max-neib_min) * alpha_mm_hole;
							double effective_alpha_lpf = alpha_lpf; 
							if (alpha_en_holes && !Double.isNaN(alpha_mm_hole) && (vector[np] <= mm) && ((neib_max-neib_min) >= alpha_diff_hole)) {
								effective_alpha_lpf = 0.0; // disable alpha_lpf
							}
							// disable pull to average of neighbors for small holes in vegetation (local "almost minimum")
							///								if (Double.isNaN(alpha_mm_hole) || (vector[np] > mm) || ((neib_max-neib_min) < alpha_diff_hole)) {
							//		this.alpha_mm_hole =     alpha_mm_hole;
							if (alpha_scale_avg == 1) {
								fX[nx] = effective_alpha_lpf * (vector[np] - avg);
								if (jt != null) {
									jt[np][nx] += effective_alpha_lpf;
									for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
										int di =  alpha_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= effective_alpha_lpf/nn;
										}
									}
								}
							} else {
								if ((avg > 0) && (avg < 1)) { 
									double savg = 0.5 + (avg - 0.5) * alpha_scale_avg;
									fX[nx] = effective_alpha_lpf * (vector[np] - savg);
									if (jt != null) {
										//											jt[np][nx] += effective_alpha_lpf;
										for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
											int di =  alpha_neibs[n][i];
											if (di >= 0) {
												jt[di][nx] -= effective_alpha_lpf/nn * alpha_scale_avg;
											}
										}
									}
								} else {
									if (avg <= 0) {
										fX[nx] = effective_alpha_lpf * (vector[np] + 0.5*(alpha_scale_avg - 1.0));
									} else { // avg > 1
										fX[nx] = effective_alpha_lpf * (vector[np] - 0.5*(alpha_scale_avg + 1.0));
									}
								}
								if (jt != null) {
									jt[np][nx] += effective_alpha_lpf;
								}
							}	
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		} // if (alpha_lpf >= 0) {
		
		if (fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
			final int ind_y_terr = ind_next;
			ind_next += num_pars_terrain;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_terrain; n = ai.getAndIncrement()) {
							int np = ind_pars_terrain + n; // index of the terrain parameter
							int nx = n + ind_y_terr; //  y_vector.length; // x - index
							double d = 0;
							if (terr_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < terr_neibs[n].length; i++) { // now 4, may be increased
									int di =  terr_neibs[n][i];
									d=0;
									if (di >= 0) {
										d = vector[di];  // d - full parameter index
										avg+=d;
										nn++;
									} else if ((di < -1) && lpf_fixed[TVAO_TERRAIN]) {
										d = tvao[TVAO_TERRAIN][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average
								fX[nx] += terr_lpf * (vector[np] - avg); //  + terr_pull0 * (vector[np]);
								if (terr_pull0 > 0) {
//									int findx = data_source[np][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
									int findx =  y_src[np][YSRC_FINDEX];
									double terr_pull = terrain_average[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if (Double.isNaN(terr_pull)) {
										terr_pull= 0; // should not happen
									}
									fX[nx] += terr_pull0 * (vector[np] - terr_pull);
								}
								if (jt != null) {
									jt[np][nx] += terr_lpf + terr_pull0;
									for (int i = 0; i < terr_neibs[n].length; i++) { // now 4, may be increased
										int di =  terr_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= terr_lpf/nn;
										}
									}								
								}							
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		if (fits[TVAO_VEGETATION] && (veget_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and terrain)
			final int ind_y_veget = ind_next;
			ind_next += num_pars_vegetation;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_vegetation; n = ai.getAndIncrement()) {
							int np = ind_pars_vegetation + n; // index of the alpha parameter
							int nx = n + ind_y_veget; //  y_vector.length; // x - index
							double d = 0;
							if (veget_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < veget_neibs[n].length; i++) { // now 4, may be increased
									int di =  veget_neibs[n][i];
									d=0;
									if (di >= 0) {
										d = vector[di];  // d - full parameter index
										avg+=d;
										nn++;
									} else if ((di < -1) && lpf_fixed[TVAO_VEGETATION]) {
										d = tvao[TVAO_VEGETATION][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average
								fX[nx] += veget_lpf * (vector[np] - avg); //   + veget_pull0 * vector[np];
								if (veget_pull0 > 0) {
//									int findx = data_source[np][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
									int findx =  y_src[np][YSRC_FINDEX];
									
									double veget_pull = vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if (Double.isNaN(veget_pull)) {
										veget_pull= 0; // should not happen unless too far from the vegetation
									}
									fX[nx] += veget_pull0 * (vector[np] - veget_pull);
								}
								
								if (jt != null) {
									jt[np][nx] += veget_lpf + veget_pull0;
									for (int i = 0; i < veget_neibs[n].length; i++) { // now 4, may be increased
										int di =  veget_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= veget_lpf/nn;
										}
									}								
								}
								if (terr_pull_cold > 0) {
									// find corresponding terrain
									int findx = par_rindex[np][1];
									int tindx = par_index[TVAO_TERRAIN][findx];
									if (tindx >= 0) { // only if both terrain and vegetation exist for this pixel
										int ntp = ind_pars_terrain + tindx; // index in vector[] 
										fX[nx] += terr_pull_cold * (vector[np] - vector[ntp] - terr_difference);
										if (jt != null) {
											jt[np][nx] +=  terr_pull_cold;
											jt[ntp][nx] -= terr_pull_cold;
										}										
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		
		if (fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and terrain)
			final int ind_y_elevation = ind_next;
			ind_next += num_pars_elevation;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_elevation; n = ai.getAndIncrement()) {
							int np = ind_pars_elevation + n; // index of the alpha parameter
							int nx = n + ind_y_elevation; //  y_vector.length; // x - index
							double d = 0;
							if (elevation_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < elevation_neibs[n].length; i++) { // now 4, may be increased
									int di =  elevation_neibs[n][i];
									d=0;
									if (di >= 0) {
										d = vector[di];  // d - full parameter index
										avg+=d;
										nn++;
									} else if ((di < -1) && lpf_fixed[TVAO_ELEVATION]) {
										d = tvao[TVAO_ELEVATION][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average
								fX[nx] += elevation_lpf * (vector[np] - avg); //   + veget_pull0 * vector[np];
								if (elevation_pull0 > 0) {
//									int findx = data_source[np][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
									int findx =  y_src[np][YSRC_FINDEX];
									
									double elevation_pull = tvao[TVAO_ELEVATION][findx]; // vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if (Double.isNaN(elevation_pull)) {
										elevation_pull= 0; // should not happen unless too far from the vegetation
									}
									fX[nx] += veget_pull0 * (vector[np] - elevation_pull);
								}
								
								if (jt != null) {
									jt[np][nx] += elevation_lpf + elevation_pull0;
									for (int i = 0; i < elevation_neibs[n].length; i++) { // now 4, may be increased
										int di =  elevation_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= elevation_lpf/nn;
										}
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		// create pivot table 
		if (need_derivs) {
			param_pivot =getPivotFromSets(
					0, // ind_pars_elevation,     // final int par_index,
					vector.length,                // num_pars_elevation, // final int par_number,
					contrib_combo);               // final ArrayList<HashSet<Integer>> sets)
			if (debug_level>4) 	showPivot(param_pivot, "pivot-sets");
			if (fits[TVAO_ALPHA] && (alpha_lpf >= 0)) {
				 pivotAddNeighbors(
							param_pivot,          // final boolean [][] pivot,
							alpha_neibs,          // final int     [][] neibs,
							ind_pars_alpha);      // final int          start_index)
					if (debug_level>4) 	showPivot(param_pivot, "pivot-alpha");
	        }   
			if (fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
				 pivotAddNeighbors(
							param_pivot,          // final boolean [][] pivot,
							terr_neibs,           // final int     [][] neibs,
							ind_pars_terrain);    // final int          start_index)
					if (debug_level>4) 	showPivot(param_pivot, "pivot-terrain");
	        }
			if (fits[TVAO_VEGETATION] && (veget_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and terrain)
				 pivotAddNeighbors(
							param_pivot,          // final boolean [][] pivot,
							veget_neibs,          // final int     [][] neibs,
							ind_pars_vegetation); // final int          start_index)
					if (debug_level>4) 	showPivot(param_pivot, "pivot-vegetation");

	        }
	   		if (fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) {
				 pivotAddNeighbors(
							param_pivot,          // final boolean [][] pivot,
							elevation_neibs,      // final int     [][] neibs,
							ind_pars_elevation);  // final int          start_index)
					if (debug_level>4) 	showPivot(param_pivot, "pivot-elevation");
	        }
		}
		
		return fX;
		
	}
	
	public static void showPivot(
			boolean [][] pivot,
			String title) {
		int size = pivot.length;
		if (pivot[0].length != size) {
			throw new IllegalArgumentException("showPivot(): Expecting square array, got "+
					pivot[0].length+"x"+size+".");

		}
		
		double [] dbg_img = new double [size*size];
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size; j++) {
				dbg_img[i*size+j] = pivot[i][j]? 1.0 : 0.0;
			}
		}
		ShowDoubleFloatArrays.showArrays(
				dbg_img,
				title);
	}
	
	
	
	private double [] getFxDerivs_old(
			final double []   vector,
			final double [][] jt, // should be null or initialized with [vector.length][]
			final int         debug_level)	{
		setupElevationLMA( // return pivot?
				vector, // final double []   vector,
				(jt != null), // final boolean     calc_derivatives,
				debug_level); // final int debugLevel) {

		
		
		final boolean use_hf = y_vector.length > data_source.length; // twice longer
		// using 0.5*(1-cos(alpha/2pi) instead of alpha. alpha < 0 -> 0, alpha > 1 -> 1. Depends on other terms for stability 
		double [] fX = new double [weights.length]; // num_pairs + vector.length];
		if (jt != null) {
			for (int i = 0; i < jt.length; i++) {
				jt[i] = new double [weights.length]; // weights.length];
			}
		}
		int dbg_scene = -20;
		final int dbg_n = -7402; // -69992;
		final int fdbg_scene = dbg_scene;
		final int dbg_cols=2,dbg_rows=2;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] vegetation = new double [4]; 
					double [] alpha =      new double [4];
					for (int n = ai.getAndIncrement(); n < data_source.length; n = ai.getAndIncrement()) { // low-frequency
						if (n == dbg_n) {
							System.out.println("getFxDerivs(): n="+n);
						}
						int indx =    data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
						int tindx =   data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_TINDEX];
						int scene =   data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_SCENE];
						if (scene == fdbg_scene) {
							int drow = indx / full.width -woi.y;
							int dcol = indx % full.width -woi.x;
							if ((dcol < dbg_cols) && (drow < dbg_rows)) {
								System.out.println("getFxDerivs(): scene="+scene+", drow="+drow+" dcol="+dcol);
							}
							
						}
						
						double terrain = (tindx >= 0) ? vector[tindx] : tvao[TVAO_TERRAIN][indx];
						double [] cw = corners_weights[n]; 
						double d; 
						int [] indx_vegetation = data_source[n][DATA_SOURCE_CORN_VEGET]; 
						int [] indx_alpha =      data_source[n][DATA_SOURCE_CORN_ALPHA];
						double sum_v =0, sum_a =0, sum_vw =0, sum_aw =0;
						if (cw != null) {
							for (int i = 0; i < 4; i++ ) {
								int iv = indx_vegetation[i], ia=indx_alpha[i];
								/*
								double v = (iv >= 0) ? vector[iv]: tvao[TVAO_VEGETATION][-1-iv];
								double a = (ia >= 0) ? vector[ia]: tvao[TVAO_ALPHA][-1-ia];
								if (!Double.isNaN(v)) {
									sum_vw += cw[i];
									sum_v +=cw[i] * v;
								}
								if (!Double.isNaN(a)) {
									sum_aw += cw[i];
									sum_a +=cw[i] * a;
								}
								*/
								vegetation[i] = cw[i] * ((iv >= 0) ? vector[iv]: tvao[TVAO_VEGETATION][-1-iv]);
								alpha[i] =      cw[i] * ((ia >= 0) ? vector[ia]: tvao[TVAO_ALPHA][-1-ia]);
								sum_v += vegetation[i];
								sum_a += alpha[i];
							}
						}
						if ((cw != null) && !Double.isNaN(sum_v) && !Double.isNaN(sum_a)){
							double k;
							if (alpha_piece_linear) {
								k = (sum_a < 0)? 0: ((sum_a > 1) ? 1.0: sum_a);	
							} else {
								k = (sum_a < 0)? 0: ((sum_a > 1) ? 1.0: 0.5 * (1.0 - Math.cos(sum_a*Math.PI)));	
							}
							
							d = terrain * (1.0 - k) + sum_v * k;
							
							if (jt != null) {
								if (tindx >= 0) {
									jt[tindx][n] = 1 - k;// sum_a; // d/dterrain
								}
								for (int i = 0; i < 4; i++ ) {
									if ((indx_vegetation != null) && (indx_vegetation[i] >= 0)) {
										jt[data_source[n][DATA_SOURCE_CORN_VEGET][i]][n] = cw[i] * k; // sum_a; // d/dvegetation[i]
									}
									if ((indx_alpha != null) && (indx_alpha[i] >= 0) && (sum_a > 0) && (sum_a < 1.0)) {
									    if (alpha_piece_linear) {
									    	jt[data_source[n][DATA_SOURCE_CORN_ALPHA][i]][n] = cw[i] * (sum_v - terrain); // d/dalpha[i]
									    } else {
									    	jt[data_source[n][DATA_SOURCE_CORN_ALPHA][i]][n] = cw[i] * (sum_v - terrain) *0.5*Math.PI *Math.sin(sum_a*Math.PI); // d/dalpha[i]
									    }
									}
								}								
							}

						} else {
							d = terrain;
							if ((jt != null) &&(tindx >= 0)) {
								jt[tindx][n] = 1;// sum_a; // d/dterrain
							}
						}
						if (data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_SINDEX] >= 0) { // can only be reference scene? use offset = 0 here.
							double scene_offs = vector[data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_SINDEX]];
							d += scene_offs;
							if (jt != null) {
								jt[data_source[n][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_SINDEX]][n] = 1;
							}
						} 
						fX[n] = d;
						if (Double.isNaN(fX[n])) {
							System.out.println("fX["+n+"]= NaN");
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		
		if (use_hf) {
			final int ind_next = data_source.length;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n_lf = ai.getAndIncrement(); n_lf < data_source.length; n_lf = ai.getAndIncrement()) {
							if (n_lf == dbg_n) {
								System.out.println("getFxDerivs(): n_lf="+n_lf);
							}
							int n_hf = n_lf + ind_next;
							int [] neibs = data_source[n_lf][DATA_SOURCE_NEIB];
							if (neibs != null) { // nothing to do if null
								double d_lf = fX[n_lf]; // center value
								double swd = 0, sw=0;
								for (int dir = 0; dir < 4; dir++) {
									int nindx = neibs[dir]; 
									if (nindx >=0) {
										//see, maybe discard all border tiles 
										double d_hf = fX[neibs[dir]]; // neighbor value
										swd += d_hf;
										sw += 1;
									}
								}
//								if (sw > 0) { // nothing to do if 0
								if (sw >=4) { // try discarding all with partial neighbors 
									swd /= sw;
									fX[n_hf] = d_lf - swd;
									if (jt != null) {
										// center derivative same as _lf with respect to all it depends on except scene offset
										int pindx =data_source[n_lf][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_TINDEX]; 
										if (pindx >= 0) jt [pindx][n_hf] = jt[pindx][n_lf]; // d/dterrain always? not anymore
										for (int i = 0; i < 4; i++ ){
											if ( data_source[n_lf][DATA_SOURCE_CORN_VEGET] != null) {
												pindx = data_source[n_lf][DATA_SOURCE_CORN_VEGET][i];
												if (pindx >= 0) jt[pindx][n_hf] = jt[pindx][n_lf]; // d/dvegetation[i]
											}
											if ( data_source[n_lf][DATA_SOURCE_CORN_ALPHA] != null) {
												pindx = data_source[n_lf][DATA_SOURCE_CORN_ALPHA][i];
												if (pindx >= 0) jt[pindx][n_hf] = jt[pindx][n_lf]; // d/dalpha[i]
											}
										}
										// scale/negate derivatives of all neighbors; -1.0/sw
										for (int neib:neibs) if (neib >=0) { // neib - index in data_source
											pindx =data_source[neib][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_TINDEX]; 
											if (pindx >= 0) jt [pindx][n_hf] -= jt[pindx][neib]/sw; // d/dterrain always?

											for (int i = 0; i < 4; i++ ){
												if ( data_source[neib][DATA_SOURCE_CORN_VEGET] != null) {
													pindx = data_source[neib][DATA_SOURCE_CORN_VEGET][i];
													if (pindx >= 0) jt[pindx][n_hf] -= jt[pindx][neib]/sw; // d/dvegetation[i]
												}
												if ( data_source[neib][DATA_SOURCE_CORN_ALPHA] != null) {
													pindx = data_source[neib][DATA_SOURCE_CORN_ALPHA][i];
													if (pindx >= 0) jt[pindx][n_hf] -= jt[pindx][neib]/sw; // d/dalpha[i]
												}
											}
										}
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		
		// regularization weights and derivatives
		int ind_next = y_vector.length;
		if (use_scenes_pull0) { // single sample after differences
			fX[ind_next] =0.0;
			for (int n = 0; n < num_pars_scenes; n++) { // only count adjustable scene offsets
				int np = ind_pars_scenes + n; // index of the alpha parameter
				int nscene = par_rindex[np][1];
				double sw=scene_weights[nscene] * scale_scenes_pull;
				fX[ind_next] += sw * vector[np];
				if (jt != null) {
					jt[np][ind_next] = sw;
				}
			}
			ind_next++;
		}
		
		// splitting alpha_lpf from alpha_loss+alpha_push
		if (fits[TVAO_ALPHA] &&  ((alpha_loss > 0) || (alpha_push > 0))) {
			int dbg_nx = -76340;
			final int ind_y_alpha_loss = ind_next;
			ind_next += num_pars_alpha;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_alpha; n = ai.getAndIncrement()) {
							int np = ind_pars_alpha + n; // index of the alpha parameter
							int nx = n + ind_y_alpha_loss; //  y_vector.length; // x - index
							if (nx == dbg_nx) {
								System.out.println("getFxDerivs(): n="+n+", nx="+nx);
							}
							double d = 0;
							fX[nx] = 0.0;
							if (alpha_loss > 0) {
								double alpha = vector[np];
								if (alpha < alpha_offset) {
									d = alpha- alpha_offset;
								} else if (alpha > (1 - alpha_offset)) {
									d = alpha - (1.0 - alpha_offset);
								}
								if (d != 0) {
									fX[nx] = d * d * alpha_loss;
									if (jt != null) {
										jt[np][nx] = 2 * alpha_loss * d; // d/dalpha[i]		
									}							
								}
							}
							double avg = 0;
							int nn = 0;
							double neib_min = Double.POSITIVE_INFINITY, neib_max = Double.NEGATIVE_INFINITY;
							for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
								int di =  alpha_neibs[n][i];
								d=0;
								if (di >= 0) {
									d = vector[di];  // d - full parameter index
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								} else if (di < -1) {
									d = tvao[TVAO_ALPHA][-di - 2]; 
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								}
							}
							avg /= nn; // average
							if (alpha_push > 0) {
								/*
								 * p = alpha_push
								 * n = alpha_push_neutral
								 * a0 =        p/(2 * n)
								 * a1 =        p/(2 * (1 - n))
								 * f1(x) =     p/(2 * n)* x * (1 - x/(2*n))
								 * f2(x) =     p * (x - 2*n + 1) * (1 - x) / (4 * (1-n)*(1-n) 
								 * df1(x)/dx = p/(2 * n^2 )*( n - x)
								 * df2(x)/dx = p* (n-x)/(2*(1-n)^2)
								 */
								// average including center:
								double sum_w = (nn + alpha_push_center);
								double avg_c = (avg * nn + vector[np] * alpha_push_center) / sum_w;
								double one_minus_n = 1.0 - alpha_push_neutral;
								double one_minus_p = 1.0 - alpha_push;
								double x_minus_p = avg_c - alpha_push;
								if ((avg_c > 0) || (avg_c < 1.0)) {
//									fX[nx] += alpha_push * avg_c * (1.0 - avg_c);
									if (avg_c <= alpha_push_neutral) {
										fX[nx] += alpha_push / (2 * alpha_push_neutral) * avg_c * (1 - avg_c / (2 * alpha_push_neutral));
									} else {
										fX[nx] += alpha_push * (avg_c - 2 * alpha_push_neutral + 1) * (1 - avg_c) / (4 *one_minus_n*one_minus_n); 
									}
									if (jt != null) {
										//									double dd = alpha_push / sum_w * (1 - 2 * avg_c);
										double dd;
										if (avg_c <= alpha_push_neutral) {
											dd = alpha_push / (sum_w * 2 * alpha_push_neutral * alpha_push_neutral ) * (alpha_push_neutral  - avg_c);
										} else {
											dd = alpha_push / (sum_w * 2 * one_minus_n * one_minus_n)  * (alpha_push_neutral  - avg_c); 
										}
										jt[np][nx] += dd * alpha_push_center; // 2 * alpha_loss * d; // d/dalpha[i]		
										for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
											int di =  alpha_neibs[n][i];
											if (di >= 0) {
												jt[di][nx] += dd; // 2 * alpha_loss * d; // d/dalpha[i]
											}
										}
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		} // if ((alpha_loss > 0) || (alpha_push > 0)){
		
		
		
		
		if (fits[TVAO_ALPHA] && (alpha_lpf >= 0)) {
			int dbg_nx = -76340;
			final int ind_y_alpha_lpf = ind_next;
			ind_next += num_pars_alpha;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_alpha; n = ai.getAndIncrement()) {
							int np = ind_pars_alpha + n; // index of the alpha parameter
							int nx = n + ind_y_alpha_lpf; //  y_vector.length; // x - index
							if (nx == dbg_nx) {
								System.out.println("getFxDerivs(): n="+n+", nx="+nx);
							}
							double d = 0;
							fX[nx] = 0.0;
							double avg = 0;
							int nn = 0;
							double neib_min = Double.POSITIVE_INFINITY, neib_max = Double.NEGATIVE_INFINITY;
							for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
								int di =  alpha_neibs[n][i];
								d=0;
								if (di >= 0) {
									d = vector[di];  // d - full parameter index
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								} else if (di < -1) {
									d = tvao[TVAO_ALPHA][-di - 2]; 
									avg+=d;
									if (d < neib_min) neib_min = d; 
									if (d > neib_max) neib_max = d; 
									nn++;
								}
							}
							avg /= nn; // average
							// add cost for difference between this alpha and average of 4 neighbors (when they exist
							// applies to alpha before cosine, so it will pull borders even when alpha<0 or alpha > 1 (zero derivatives)
							//alpha_scale_avg
							double mm = neib_min + (neib_max-neib_min) * alpha_mm_hole;
							double effective_alpha_lpf = alpha_lpf; 
							if (alpha_en_holes && !Double.isNaN(alpha_mm_hole) && (vector[np] <= mm) && ((neib_max-neib_min) >= alpha_diff_hole)) {
								effective_alpha_lpf = 0.0; // disable alpha_lpf
							}
							// disable pull to average of neighbors for small holes in vegetation (local "almost minimum")
							///								if (Double.isNaN(alpha_mm_hole) || (vector[np] > mm) || ((neib_max-neib_min) < alpha_diff_hole)) {
							//		this.alpha_mm_hole =     alpha_mm_hole;
							if (alpha_scale_avg == 1) {
								fX[nx] = effective_alpha_lpf * (vector[np] - avg);
								if (jt != null) {
									jt[np][nx] += effective_alpha_lpf;
									for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
										int di =  alpha_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= effective_alpha_lpf/nn;
										}
									}
								}
							} else {
								if ((avg > 0) && (avg < 1)) { 
									double savg = 0.5 + (avg - 0.5) * alpha_scale_avg;
									fX[nx] = effective_alpha_lpf * (vector[np] - savg);
									if (jt != null) {
										//											jt[np][nx] += effective_alpha_lpf;
										for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
											int di =  alpha_neibs[n][i];
											if (di >= 0) {
												jt[di][nx] -= effective_alpha_lpf/nn * alpha_scale_avg;
											}
										}
									}
								} else {
									if (avg <= 0) {
										fX[nx] = effective_alpha_lpf * (vector[np] + 0.5*(alpha_scale_avg - 1.0));
									} else { // avg > 1
										fX[nx] = effective_alpha_lpf * (vector[np] - 0.5*(alpha_scale_avg + 1.0));
									}
								}
								if (jt != null) {
									jt[np][nx] += effective_alpha_lpf;
								}
							}	
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		} // if (alpha_lpf >= 0) {
		
		if (fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
			final int ind_y_terr = ind_next;
			ind_next += num_pars_terrain;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_terrain; n = ai.getAndIncrement()) {
							int np = ind_pars_terrain + n; // index of the terrain parameter
							int nx = n + ind_y_terr; //  y_vector.length; // x - index
							double d = 0;
							if (terr_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < terr_neibs[n].length; i++) { // now 4, may be increased
									int di =  terr_neibs[n][i];
									d=0;
									if (di >= 0) {
										d = vector[di];  // d - full parameter index
										avg+=d;
										nn++;
									} else if (di < -1) {
										d = tvao[TVAO_TERRAIN][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average
								fX[nx] += terr_lpf * (vector[np] - avg); //  + terr_pull0 * (vector[np]);
								if (terr_pull0 > 0) {
									int findx = data_source[np][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
									double terr_pull = terrain_average[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if (Double.isNaN(terr_pull)) {
										terr_pull= 0; // should not happen
									}
									fX[nx] += terr_pull0 * (vector[np] - terr_pull);
								}
								
								if (jt != null) {
									jt[np][nx] += terr_lpf + terr_pull0;
									for (int i = 0; i < terr_neibs[n].length; i++) { // now 4, may be increased
										int di =  terr_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= terr_lpf/nn;
										}
									}								
								}							
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		if (fits[TVAO_VEGETATION] && (veget_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and terrain)
			final int ind_y_veget = ind_next;
			ind_next += num_pars_vegetation;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < num_pars_vegetation; n = ai.getAndIncrement()) {
							int np = ind_pars_vegetation + n; // index of the alpha parameter
							int nx = n + ind_y_veget; //  y_vector.length; // x - index
							double d = 0;
							if (veget_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < veget_neibs[n].length; i++) { // now 4, may be increased
									int di =  veget_neibs[n][i];
									d=0;
									if (di >= 0) {
										d = vector[di];  // d - full parameter index
										avg+=d;
										nn++;
									} else if (di < -1) {
										d = tvao[TVAO_VEGETATION][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average
								fX[nx] += veget_lpf * (vector[np] - avg); //   + veget_pull0 * vector[np];
								if (veget_pull0 > 0) {
									int findx = data_source[np][DATA_SOURCE_HEAD][DATA_SOURCE_HEAD_FINDEX];
									double veget_pull = vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if (Double.isNaN(veget_pull)) {
										veget_pull= 0; // should not happen unless too far from the vegetation
									}
									fX[nx] += veget_pull0 * (vector[np] - veget_pull);
								}
								
								if (jt != null) {
									jt[np][nx] += veget_lpf + veget_pull0;
									for (int i = 0; i < veget_neibs[n].length; i++) { // now 4, may be increased
										int di =  veget_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= veget_lpf/nn;
										}
									}								
								}
								if (terr_pull_cold > 0) {
									// find corresponding terrain
									int findx = par_rindex[np][1];
									int tindx = par_index[TVAO_TERRAIN][findx];
									if (tindx >= 0) { // only if both terrain and vegetation exist for this pixel
										int ntp = ind_pars_terrain + tindx; // index in vector[] 
										fX[nx] += terr_pull_cold * (vector[np] - vector[ntp] - terr_difference);
										if (jt != null) {
											jt[np][nx] +=  terr_pull_cold;
											jt[ntp][nx] -= terr_pull_cold;
										}										
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		return fX;		
	}
	
	
	private double [][] getFxDerivsDelta(
			double []         vector,
			final double      delta,
			final int         debug_level) {
		int dbg_n = -2256; // 1698; // -2486;
		double [][] jt =  new double [vector.length][weights.length];
		for (int nv = 0; nv < vector.length; nv++) {
			if (nv == dbg_n) {
				System.out.println("getFxDerivsDelta(): nv="+nv);
			}
			boolean is_elevation = par_rindex[nv][0] == TVAO_ELEVATION; // elev_sum_weights
			double [] vpm = vector.clone();
			vpm[nv]+= 0.5*delta;
			if (is_elevation) {
				elev_sum_weights = null; // force re-calculation
			}
			double [] fx_p =  getFxDerivs(
					vpm,
					null, // final double [][] jt, // should be null or initialized with [vector.length][]
					debug_level);
			vpm[nv]-= delta;
			if (is_elevation) {
				elev_sum_weights = null; // force re-calculation
			}
			double [] fx_m =  getFxDerivs(
					vpm,
					null, // final double [][] jt, // should be null or initialized with [vector.length][]
					debug_level);
			for (int i = 0; i < weights.length; i++) if (weights[i] > 0) {
				jt[nv][i] = (fx_p[i]-fx_m[i])/delta;
			}
		}
		return jt;
	}

	private int [][] getParamDebugIndices(
			int gap) {
//		int woi_width3=woi.width*3 + 2 * gap;
		int woi_width4=woi_veg.width * 4 + 3 * gap;
		int num_scene_rows = (int) Math.ceil(1.0 * num_scenes / woi_width4); // some scenes have NaN
		int [][] indices = new int [woi_width4 * (woi_veg.height + gap+ num_scene_rows)][2];
		for (int i = 0; i < indices.length; i++) {
			indices[i][0] = -1; // unused
		}
//		Arrays.fill(indices, new int [] {});
//		int [] save_types = {TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_ELEVATION, TVAO_SCENE_OFFSET};
		for (int h = 0; h < woi_veg.height; h++) {
			for (int n = 0; n < 4; n++) {
				for (int w = 0; w < woi_veg.width; w++) {
					int indx = (h + woi_veg.y) * full.width + (w + woi_veg.x);
					int pindx = par_index[SAVE_TYPES[n]][indx];
					if (pindx >= 0) {
						indices [(h * woi_width4) + (woi_veg.width + gap) * n + w][0] = 0;
						indices [(h * woi_width4) + (woi_veg.width + gap) * n + w][1] = pindx;
					} else {
						indices [(h * woi_width4) + (woi_veg.width + gap) * n + w][0] = SAVE_TYPES[n] + 1; // type + 1
						indices [(h * woi_width4) + (woi_veg.width + gap) * n + w][1] = indx;
					}
				}
			}
		}
		for (int i = 0; i < par_index[TVAO_SCENE_OFFSET].length; i++) {
			int pindx = par_index[TVAO_SCENE_OFFSET][i]; // some will be -1
			if (pindx >= 0) {
				indices [((woi_veg.height + gap) * woi_width4) + i][0] = 0;
				indices [((woi_veg.height + gap) * woi_width4) + i][1] = pindx;
			} else {
				indices [((woi_veg.height + gap) * woi_width4) + i][0] = TVAO_SCENE_OFFSET + 1; // type + 1
				indices [((woi_veg.height + gap) * woi_width4) + i][1] = i;
			}
		}
		return indices;
	}
	
	public void showYfX(
			double [] vector,
			String title) {
		if (vector == null) {
			vector = parameters_vector;
		}
		final boolean use_hf = y_vector.length > y_src.length; // twice longer
		double [][][] img_data = new double [use_hf? 6 : 3][num_scenes+1][woi.width*woi.height];
		
		String [] top_titles = use_hf? (new String[]{"Y_lf", "fX_lf", "Ylf-fXlf","Y_hf", "fX_hf", "Yhf-fXhf"}): new String[]{"Y", "fX", "Y-fX"};

		for (int n = 0; n < img_data.length; n++) {
			for (int nscene = 0; nscene < num_scenes; nscene++) {
				Arrays.fill(img_data[n][nscene], Double.NaN);
			}
		}
		double [] fX = getFxDerivs( // longer than y_vector
				vector, // final double []   vector,
				null,   // final double [][] jt, // should be null or initialized with [vector.length][]
				-1);    // final int         debug_level)

		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int n_lf = ai.getAndIncrement(); n_lf < y_src.length; n_lf = ai.getAndIncrement()) {
						int nscene =   y_src[n_lf][YSRC_SCENE];
						int indx =     y_src[n_lf][YSRC_FINDEX];
						int wx = (indx % full.width) - woi.x; 
						int wy = (indx / full.width) - woi.y;
						int windx = wx + wy * woi.width;
						img_data[0][nscene][windx] = y_vector[n_lf];
						img_data[1][nscene][windx] = fX[n_lf];
						img_data[2][nscene][windx] = y_vector[n_lf]-fX[n_lf];
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (img_data.length > 3) {
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n_hf = ai.getAndIncrement(); n_hf < y_src_hf.length; n_hf = ai.getAndIncrement()) {
							int nscene =   y_src_hf[n_hf][YSRC_SCENE];
							int indx =     y_src_hf[n_hf][YSRC_FINDEX];
							int wx = (indx % full.width) - woi.x; 
							int wy = (indx / full.width) - woi.y;
							int windx = wx + wy * woi.width;
							img_data[3][nscene][windx] = hifreq_weight * y_vector[n_hf + y_src.length];
							img_data[4][nscene][windx] = hifreq_weight * fX[n_hf + y_src.length];
							img_data[5][nscene][windx] = hifreq_weight * (y_vector[n_hf + y_src.length]-fX[n_hf + y_src.length]);

						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		
		double [][] sw = new double [img_data.length][img_data[0][0].length];
		for (int slice=0; slice < img_data.length; slice++) {
			for (int nscene = 0; nscene < num_scenes; nscene++) {
				for (int npix = 0; npix < img_data[slice][nscene].length; npix++) if (!Double.isNaN(img_data[slice][nscene][npix])){
					double w = scene_weights[nscene];
					sw[slice][npix] += w;
					img_data[slice][num_scenes][npix] += w * img_data[slice][nscene][npix]; 
				}
			}
			for (int npix = 0; npix < sw[slice].length; npix++) {
				double w = sw[slice][npix];
				if (w > 0) {
					img_data[slice][num_scenes][npix] /= w;
				} else {
					img_data[slice][num_scenes][npix] = Double.NaN;
				}
			}			
		}
		
		String [] scene_titles = new String[num_scenes+1];
		for (int i = 0; i < num_scenes; i++) {
			scene_titles[i] = ""+i;
		}
		scene_titles[num_scenes] = "average";

		ShowDoubleFloatArrays.showArraysHyperstack(
				img_data,           // double[][][] pixels, 
				woi.width,                // int          width, 
				title, // "terrain_vegetation_render.tiff", // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				scene_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)
		return;
	}
	
	public double [] parametersImage (
			double [] vector,
			int       gap,
			int []    wh,
			String    title) {
		int [][] indices = getParamDebugIndices (gap);
//		int woi_width3=woi.width*3 + 2 * gap;
		int woi_width4=woi_veg.width*4 + 3 * gap;
		double [] dbg_img = new double [indices.length];
		for (int i = 0; i < indices.length; i++) {
			if (indices[i][0] < 0) { // unused, no mapping to parameter of fixed value
				dbg_img[i] = Double.NaN;
			} else if (indices[i][0] > 0) {
				dbg_img[i] = Double.NaN;
				int type = indices[i][0] -1;
				dbg_img[i] = tvao[type][indices[i][1]];
			} else {
				dbg_img[i]= vector[indices[i][1]];
			}
			if (!Double.isNaN(dbg_img[i])) {
				// scale alpha for the same image
				int w = i % woi_width4;
				int h = i / woi_width4;
				int nsub = w % (woi_veg.width + gap);
				if (nsub == 2) { // alpha
					dbg_img[i] = debug_alpha_scales[0] + (debug_alpha_scales[1] - debug_alpha_scales[0]) * dbg_img[i];  
				} else if (nsub== 3) { // elevation
					dbg_img[i] = debug_alpha_scales[2] + (debug_alpha_scales[3] - debug_alpha_scales[2]) * dbg_img[i];  
				}
			}
		}
		if (wh != null) {
			wh[0] = woi_width4;
			wh[1] = indices.length/woi_width4;
		}
		if (title != null) {
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					woi_width4,
					indices.length/woi_width4,
					title);
		}
		return dbg_img;
	}
	
	
	
	public static String getSavePath(
			String dir,
			String title) { // no .par-tiff
		return getSavePath(dir,title,PAR_EXT);
	}

	public static String getSavePath(
			String dir,
			String title,
			String ext) {
		if (!dir.endsWith(Prefs.getFileSeparator())) {
			dir+=Prefs.getFileSeparator();
		}
		return dir+title+ext;
	}

	
	
	public void saveParametersFile(
			String dir,
			String title, // no .par-tiff
			double [] vector) {
		int woi_length = woi.width*woi.height;
		int ext_length = woi_length; 
		if (vector == null) {
			vector = parameters_vector;
		}
		 
		int height = woi.height;
		
		if (num_scenes > woi_length) { // very unlikely but still may happen
			height = (num_scenes + woi.width - 1) / woi.width;
			ext_length = woi.width * height; 
		}
		// assuming
//		int [] save_types = {TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_SCENE_OFFSET};
		String [] titles = {"terrain","vegetation","alpha","elevation", "offset","terrain-index","vegetation-index","alpha-index","elevation-index", "offset-index"};
		double [][] data = new double [titles.length][ext_length];
		for (int i = 0; i < TVAO_TYPES; i++) Arrays.fill(data[i],Double.NaN);
		int [][] indices = new int [TVAO_TYPES][ext_length];
		for (int n = 0; n < TVAO_TYPES; n++) {
			for (int i = 0; (i < indices[n].length) && (i < par_index[n].length); i++) {
				int indx = i;
				if (n < TVAO_SCENE_OFFSET) {
					int w = i % woi.width;
					int h = i / woi.width;
					indx = (h + woi.y) * full.width + (w + woi.x);
				}
				int pindx = par_index[n][indx];
				indices[n][i] =  pindx;
				if (pindx < 0) {
//					if (n == TVAO_SCENE_OFFSET) {
//						data[n][i] = Double.NaN;
//					} else {
					if (tvao[n] == null) {
						data[n][i] = Double.NaN;
					} else {
						data[n][i] = tvao[n][indx];
					}
					/*
					if (data[n] == null) {
						System.out.println("data["+n+"] == null");
					}
					if (tvao[n] == null) {
						System.out.println("tvao["+n+"] == null");
					}
						data[n][i] = tvao[n][indx];
//					}
  */
				} else {
					data[n][i] = vector[pindx];
				}
				data[n+TVAO_TYPES][i] = indices[n][i];
				
			}
		}
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				data,
				woi.width,
				height,
				title, // +"-objects_"+scene_num,				
				titles); // test_titles,
		imp.setProperty("WOI_X",                     ""+woi.x);
		imp.setProperty("WOI_Y",                     ""+woi.y);
		imp.setProperty("WOI_WIDTH",                 ""+woi.width);
		imp.setProperty("WOI_HEIGHT",                ""+woi.height);
		imp.setProperty("FULL_WIDTH",                ""+full.width);
		imp.setProperty("FULL_HEIGHT",               ""+full.height);
		
		imp.setProperty("NUM_SCENES",                ""+num_scenes);
		imp.setProperty("DIFF_OFFSETS",              ""+diff_offsets);
		imp.setProperty("NUM_PARS_TERRAIN",          ""+num_pars_terrain);
		imp.setProperty("NUM_PARS_VEGETATION",       ""+num_pars_vegetation);
		imp.setProperty("NUM_PARS_VEGETATION_ALPHA", ""+num_pars_alpha);
		imp.setProperty("NUM_PARS_SCENES",           ""+num_pars_scenes);
		imp.setProperty("IND_PARS_TERRAIN",          ""+ind_pars_terrain);
		imp.setProperty("IND_PARS_VEGETATION",       ""+ind_pars_vegetation);
		imp.setProperty("IND_PARS_VEGETATION_ALPHA", ""+ind_pars_alpha);
		imp.setProperty("IND_PARS_SCENES",           ""+ind_pars_scenes);
		
		
		imp.setProperty("HIGHFREQ_WEIGHT",           ""+hifreq_weight);
		imp.setProperty("REG_WEIGHTS",               ""+reg_weights);
		imp.setProperty("ALPHA_LOSS",                ""+alpha_loss);
		imp.setProperty("ALPHA_OFFSET",              ""+alpha_offset);
		imp.setProperty("ALPHA_LPF",                 ""+alpha_lpf);
		imp.setProperty("ALPHA_PIECE_LINEAR",        ""+alpha_piece_linear);
		imp.setProperty("ALPHA_SCALE_AVG",           ""+alpha_scale_avg);
		imp.setProperty("ALPHA_PUSH",                ""+alpha_push);
		imp.setProperty("ALPHA_PUSH_NEUTRAL",        ""+alpha_push_neutral);
		imp.setProperty("ALPHA_PUSH_CENTER",         ""+alpha_push_center );
		imp.setProperty("ALPHA_EN_HOLES",            ""+alpha_en_holes );
		imp.setProperty("ALPHA_MM_HOLE",             ""+alpha_mm_hole);
		imp.setProperty("ALPHA_DIFF_HOLE",           ""+alpha_diff_hole);
		imp.setProperty("TERR_LPF",                  ""+terr_lpf);
		imp.setProperty("VEGET_LPF",                 ""+veget_lpf);
		imp.setProperty("TERR_PULL0",                ""+terr_pull0);
		imp.setProperty("VEGET_PULL0",               ""+veget_pull0);
		imp.setProperty("BOOST_PARALLAX",            ""+boost_parallax);
		imp.setProperty("UM_SIGMA",                  ""+um_sigma);
		imp.setProperty("UM_WEIGHT",                 ""+um_weight);
		String path = getSavePath(
				dir, // String dir,
				title); // String title)
		JP46_Reader_camera.encodeProperiesToInfo(imp);			
		FileSaver fs=new FileSaver(imp);
		fs.saveAsTiff(path);
		System.out.println("saveParametersFile(): saved "+path); // dir+title+PAR_EXT);
		return;
	}
	
	public int getSaveIndex(int tvao_type) {
		for (int i = 0; i < SAVE_TYPES.length; i++) {
			if (SAVE_TYPES[i] == tvao_type) return i;
			
		}
		return -1;
	}
	
	
	public double [][] restoreParametersFile( //FIXME: Not finished for real import ! 
			String    path,
			Rectangle file_woi) {//  if not null, will return woi data and not input parameters to this instance
		ImagePlus imp_pars = new ImagePlus (path);
		if (imp_pars.getWidth()==0) {
			throw new IllegalArgumentException("Could not read "+path);
		}
		int num_slices = imp_pars.getStack().getSize();
		boolean no_elevation = num_slices == 8;
		boolean has_elevation = num_slices == 10;
		
		if (!no_elevation && !has_elevation) {
			throw new IllegalArgumentException("Expecting an 10- or 8-slice file, got "+num_slices);
		}
		int num_types = has_elevation? 5 : 4;
		int width = imp_pars.getWidth();
		int height = imp_pars.getHeight();
		double [][] data = new double[TVAO_TYPES][width*height];
		int [][] indices = new int[TVAO_TYPES][width*height];
		for (int n = 0; n < num_types; n++) {
			int nt = has_elevation ? n : SAVE_TYPES[n];
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1);
			for (int i = 0; i < data[n].length; i++) {
				data[nt][i] = pixels[i];
			}
		}
		for (int n = 0; n < num_types; n++) {
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1+ num_types);
			int nt = has_elevation ? n : SAVE_TYPES[n];
			for (int i = 0; i < data[n].length; i++) {
				indices[nt][i] = (int) pixels[i];
			}
		}
		if (!has_elevation) {
			Arrays.fill(indices[TVAO_ELEVATION],-1);
			Arrays.fill(data[TVAO_ELEVATION],Double.NaN);
		}
		
		JP46_Reader_camera.decodeProperiesFromInfo(imp_pars);
		Rectangle full = new Rectangle (
				0,
				0,
				Integer.parseInt((String) imp_pars.getProperty("FULL_WIDTH")),
				Integer.parseInt((String) imp_pars.getProperty("FULL_HEIGHT")));
		if (!full.equals(this.full)) {
			throw new IllegalArgumentException("Full image window differes, this is ("+this.full.width+" x "+this.full.height+"), file contains ("+
					+full.width+" x "+full.height+")");
		}
		int num_scenes = Integer.parseInt((String) imp_pars.getProperty("NUM_SCENES"));
		
		if (num_scenes != this.num_scenes) {
			throw new IllegalArgumentException("Number of scenes can not be changed, this has "+this.num_scenes+", file has "+
					+num_scenes+".");
		}
		
		Rectangle woi = new Rectangle (
				Integer.parseInt((String) imp_pars.getProperty("WOI_X")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_Y")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_WIDTH")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_HEIGHT")));
		
		if (file_woi != null) {
			file_woi.x = woi.x;
			file_woi.y = woi.y;
			file_woi.width =  woi.width;
			file_woi.height = woi.height;
// Remove undefined values that were not adjusted for this woi			
			for (int n = 0; n < TVAO_TYPES; n++) {
				for (int i = 0; i < data[n].length; i++) {
					if (indices[n][i] < 0) {
						data[n][i] = Double.NaN;
					}
				}
			}
			// Trim data to actiual
//			int num_pars_scenes =           Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_SCENES"));
//			double [] scene_offsets = new double [num_pars_scenes];
			double [] scene_offsets = new double [num_scenes];
			System.arraycopy(data[getSaveIndex(TVAO_SCENE_OFFSET)], 0, scene_offsets, 0, scene_offsets.length);
			// temporary patch, later they will be NaNs already - Already done !
			for (int i = 0; i < scene_offsets.length; i++) {
				if (indices[TVAO_SCENE_OFFSET][i] < 0) {
					data[TVAO_SCENE_OFFSET][i] = Double.NaN;
				}
			}
			
			for (int n = 0; n < SAVE_TYPES.length; n++) if (SAVE_TYPES[n] != TVAO_SCENE_OFFSET){
				if (woi.width * woi.height < data[n].length ) {
					double [] d = new double [woi.width * woi.height];
					System.arraycopy(data[n], 0, d, 0, d.length);
					data[n] = d;
				}
			}
			data[3] = scene_offsets;
		} else {
			this.woi = woi;
			diff_offsets =              Boolean.parseBoolean((String) imp_pars.getProperty("DIFF_OFFSETS"));
			alpha_piece_linear =        Boolean.parseBoolean((String) imp_pars.getProperty("ALPHA_PIECE_LINEAR"));
			num_pars_terrain =          Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERRAIN"));
			num_pars_vegetation =       Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_VEGETATION"));
			num_pars_alpha = Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_VEGETATION_ALPHA"));
			num_pars_scenes =           Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_SCENES"));
			ind_pars_terrain =          Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERRAIN"));
			ind_pars_vegetation =       Integer.parseInt((String) imp_pars.getProperty("IND_PARS_VEGETATION"));
			ind_pars_alpha = Integer.parseInt((String) imp_pars.getProperty("IND_PARS_VEGETATION_ALPHA"));
			ind_pars_scenes =           Integer.parseInt((String) imp_pars.getProperty("IND_PARS_SCENES"));

			if (imp_pars.getProperty("HIGHFREQ_WEIGHT") != null) hifreq_weight = Double.parseDouble((String) imp_pars.getProperty("HIGHFREQ_WEIGHT"));
			if (imp_pars.getProperty("REG_WEIGHTS") != null)     reg_weights = Double.parseDouble((String) imp_pars.getProperty("REG_WEIGHTS"));
			
			alpha_loss =                Double.parseDouble((String) imp_pars.getProperty("ALPHA_LOSS"));
			alpha_offset =              Double.parseDouble((String) imp_pars.getProperty("ALPHA_OFFSET"));
			alpha_lpf =                 Double.parseDouble((String) imp_pars.getProperty("ALPHA_LPF"));
			alpha_scale_avg =           Double.parseDouble((String) imp_pars.getProperty("ALPHA_SCALE_AVG"));
			alpha_push =                Double.parseDouble((String) imp_pars.getProperty("ALPHA_PUSH"));
			alpha_push_neutral =        Double.parseDouble((String) imp_pars.getProperty("ALPHA_PUSH_NEUTRAL"));
			alpha_push_center =         Double.parseDouble((String) imp_pars.getProperty("ALPHA_PUSH_CENTER"));
			
			if (imp_pars.getProperty("ALPHA_EN_HOLES") != null) alpha_en_holes = Boolean.parseBoolean((String) imp_pars.getProperty("ALPHA_EN_HOLES"));
			
			
			alpha_mm_hole =             Double.parseDouble((String) imp_pars.getProperty("ALPHA_MM_HOLE"));
			alpha_diff_hole =           Double.parseDouble((String) imp_pars.getProperty("ALPHA_DIFF_HOLE"));
			terr_lpf =                  Double.parseDouble((String) imp_pars.getProperty("TERR_LPF"));
			veget_lpf =                 Double.parseDouble((String) imp_pars.getProperty("VEGET_LPF"));
			terr_pull0 =                Double.parseDouble((String) imp_pars.getProperty("TERR_PULL0"));
			veget_pull0 =               Double.parseDouble((String) imp_pars.getProperty("VEGET_PULL0"));
			boost_parallax =            Double.parseDouble((String) imp_pars.getProperty("BOOST_PARALLAX"));
			um_sigma =                  Double.parseDouble((String) imp_pars.getProperty("UM_SIGMA"));
			um_weight =                 Double.parseDouble((String) imp_pars.getProperty("UM_WEIGHT"));
// TODO: build other data structures and make instance consistent, without it this does not allow reading from file			
			
		}
		return data;
	}

	public void combineSegments(
			VegetationSegment [] segments,
			int                  width,
			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
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		double ksigma = 1.0; 
		double min_sw = 1e-4;
		int full_length = full.width*full.height;
		int accum_indx = segments.length;
		double [][][] preview_data = new double [3][accum_indx+1][full_length];
		String [] top_titles = {"terrain","vegetation","alpha"};
		String [] titles = new String[accum_indx+1];
		for (int ns = 0; ns < segments.length; ns++) {
			Rectangle woi = segments[ns].woi;
			titles[ns] = woi.toString();
			int woi_length = woi.width * woi.height;  
			double [][] woi_tva = segments[ns].tva;
			for (int t = 0; t < 3; t++) {
				Arrays.fill(preview_data[t][ns], Double.NaN);
				for (int windx = 0; windx < woi_length; windx++) {
					int x = windx % woi.width + woi.x;
					int y = windx / woi.width + woi.y;
					int indx = x + y * full.width;
					preview_data[t][ns][indx] = woi_tva[t][windx];
				}
			}
		}
		titles[accum_indx] = "accum";
		double [][] sum_w =  new double [preview_data.length][full_length];
		double [][] sum_wd = new double [preview_data.length][full_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;
				int y = windx / woi.width + woi.y;
				int indx = x + y * full.width;
				for (int t = 0; t < preview_data.length; t++) {
					if (!Double.isNaN(woi_tva[t][windx])) {
						double w = wnd[windx];
						sum_w [t][indx] += w;
						sum_wd[t][indx] += w * woi_tva[t][windx] ;
					}
				}				
			}
		}
		
		for (int t = 0; t < preview_data.length; t++) {
			for (int i = 0; i < full_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];
				}
			}
		}
		// Combine offsets per scene

		double [][] combined_offsets = new double [num_scenes][full_length];
		String [] titles_scenes = new String [num_scenes];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] sw =  new double [full_length];
					for (int nscene = ai.getAndIncrement(); nscene < num_scenes; nscene = ai.getAndIncrement()) {
						titles_scenes[nscene] = "scene "+nscene;
						double [] swd = combined_offsets[nscene];
						Arrays.fill(sw, 0);
						for (int ns = 0; ns < segments.length; ns++) {
							Rectangle woi = segments[ns].woi;
							int woi_length = woi.width*woi.height;
							double scene_offset = segments[ns].scene_offsets[nscene];
							if (!Double.isNaN(scene_offset)) {
								double [] wnd = overlapCosineWindow (woi, width);
								for (int windx = 0; windx < woi_length; windx++) {
									int x = windx % woi.width + woi.x;
									int y = windx / woi.width + woi.y;
									int indx = x + y * full.width;
									for (int t = 0; t < preview_data.length; t++) {
										double w = wnd[windx];
										sw [indx] += w;
										swd[indx] += w * scene_offset ;
									}				
								}
							}
						}
						(new DoubleGaussianBlur()).blurDouble(
								sw,          //
								full.width,
								full.height,
								ksigma*width,              // double sigmaX,
								ksigma*width,              // double sigmaY,
								0.01);                 // double accuracy)
						(new DoubleGaussianBlur()).blurDouble(
								swd,          //
								full.width,
								full.height,
								ksigma*width,              // double sigmaX,
								ksigma*width,              // double sigmaY,
								0.01);                 // double accuracy)

						for (int i = 0; i < full_length; i++) {
							if (sw[i] < min_sw) {
								swd[i] = Double.NaN;
							} else {
								swd[i] /= sw[i];
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		double [][] tva = {preview_data[0][accum_indx],preview_data[1][accum_indx],preview_data[2][accum_indx]};
		vegetationModel.setTVA(tva);

	//				new String[] {"full average", "filtered average"});
	
		String ref_scene = vegetationModel.reference_scene;
		String render_title = "render-ro_"+render_open+"-rna_"+render_no_alpha+"-amn"+alpha_min+"-amx"+alpha_max+"-wo"+weight_opaque+"-bp"+boost_parallax+".tiff"; 
		
		
		double [] render = renderWithAlpha(
				preview_data[2][accum_indx], // final double []   alpha,
				combined_offsets,            // final double [][] scene_offsets, // subtract if not null
				render_open,                 // final boolean     render_open,      // render open areas (no vegetation offset)
				render_no_alpha,             // final boolean     render_no_alpha,  // render where no opacity is available
				alpha_min,                   // final double      alpha_min,        // below - completely transparent vegetation
				alpha_max,                   // final double      alpha_max,        // above - completely opaque
				weight_opaque,               // final double      weight_opaque,    // render through completely opaque vegetation
				boost_parallax,              // final double      boost_parallax) {    // increase weights of scenes with high parallax relative to the reference one
				max_parallax);               // final double      max_parallax)
		
		// num_exaggerate
		double [][] result_renders = new double [1+num_exaggerate][];
		String []   result_titles = new String [result_renders.length];
		result_titles[0] = "full average";
		result_titles[1] = "filtered average";
		result_renders[0] = terrain_average;
		result_renders[1] = render;
		for (int n = 2; n <=  num_exaggerate; n++) {
			result_titles[n]="exaggerate x "+n;
			result_renders[n] = new double [full_length];
			for (int i = 0; i < full_length; i++) {
				result_renders[n][i] = terrain_average[i] + n * (render[i] - terrain_average[i]);
			}
		}
		
		ShowDoubleFloatArrays.showArrays(
				combined_offsets,
				full.width,
				full.height,
				true,
				ref_scene+"-scene_offsets",
				titles_scenes);
		
		ShowDoubleFloatArrays.showArraysHyperstack(
				preview_data,              // double[][][] pixels, 
				full.width,                // int          width, 
				ref_scene+"-preview_segments",        // 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)
		
		ShowDoubleFloatArrays.showArrays(
				result_renders,
				full.width,
				full.height,
				true,
				ref_scene+"-"+render_title,
				result_titles);
		String [] synt_titles = {"render","synthetic", "synt_terrain", "synt vegetation","masked render", "render"};
		final double [][][] synt_data = new double [synt_titles.length][num_scenes][];
		final double      terrain_max = alpha_min;
		final double      vegetation_min = 0.5; // alpha_max;
		String synt_name = "synthetic-terr"+terrain_max+"-veg"+vegetation_min;
		synt_data[0] = terrain_rendered;
		synt_data[5] = terrain_rendered;
		double [][][] synt_tv= renderSynthetic(
				preview_data[2][accum_indx], // final double []   alpha,
				combined_offsets,            // final double [][] scene_offsets, // subtract if not null
				terrain_max,                 // final double      terrain_max,      // below - terrain is visible
				vegetation_min,              // final double      vegetation_min,   // above - vegetation is visible
				boost_parallax,              // final double      boost_parallax,   // increase weights of scenes with high parallax relative to the reference one
				max_parallax);               //final double      max_parallax) {
		synt_data[2] = synt_tv[0];
		synt_data[3] = synt_tv[1];
/*		
		for (int nscene = 0; nscene < num_scenes; nscene++) { // use threads
			synt_data[1][nscene] = synt_tv[0][nscene].clone(); 
			synt_data[4][nscene] = terrain_rendered[nscene].clone();
			double [] veg_scene = synt_tv[1][nscene];
			for (int i = 0; i < veg_scene.length; i++) {
				if (!Double.isNaN(veg_scene[i])) {
					synt_data[1][nscene][i] = veg_scene[i];
					synt_data[4][nscene][i] = Double.NaN;
				}
			}
		}
*/
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nscene = ai.getAndIncrement(); nscene < num_scenes; nscene = ai.getAndIncrement()) {
						synt_data[1][nscene] = synt_tv[0][nscene].clone(); 
						synt_data[4][nscene] = terrain_rendered[nscene].clone();
						double [] veg_scene = synt_tv[1][nscene];
						for (int i = 0; i < veg_scene.length; i++) {
							if (!Double.isNaN(veg_scene[i])) {
								synt_data[1][nscene][i] = veg_scene[i];
								synt_data[4][nscene][i] = Double.NaN;
							}
						}
						
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		
		
		
		ShowDoubleFloatArrays.showArraysHyperstack(
				synt_data,              // double[][][] pixels, 
				full.width,                // int          width, 
				ref_scene+"-"+synt_name,        // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				titles_scenes,                    // String []    titles, // all slices*frames titles or just slice titles or null
				synt_titles,                // String []    frame_titles, // frame titles or null
				true);                     // boolean      show)
		
		
		
		return;
		
	}
	
	public double [][][] renderSynthetic(
			final double []   alpha,
			final double [][] scene_offsets, // subtract if not null
			final double      terrain_max,      // below - terrain is visible
			final double      vegetation_min,   // above - vegetation is visible
			final double      boost_parallax,   // increase weights of scenes with high parallax relative to the reference one
			final double      max_parallax) {			
		final int full_length = full.width*full.height;
		final double [][] tva = vegetationModel.getTVA();
		final double [] terrain_dflt = vegetationModel.terrain_average_render;
		final double [] vegetation_dflt = vegetationModel.vegetation_full; // vegetation_average_render;
		final int INDX_TERR = 0, INDX_VEG=1;
		final double [][][] synthetic = new double [2][num_scenes][full_length];
		for (int t = 0; t < synthetic.length; t++) {
			for (int nscene = 0; nscene < synthetic[t].length; nscene++) {
				Arrays.fill(synthetic[t][nscene], Double.NaN);
			}
		}
		final double [][] synthetic_terrain =    synthetic[INDX_TERR];
		final double [][] synthetic_vegetation = synthetic[INDX_VEG];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) {
						for (int npix = 0; npix < full_length; npix++) {
							int x = npix % full.width;
							int y = npix / full.width;
							double d0= 0;
							if (scene_offsets != null) {
								double offs = scene_offsets[nScene][npix];
								if (!Double.isNaN(offs)) d0 = offs;
							}
							double [] veg_xy = vegetation_offsets[nScene][npix];
							double terrain_pix = (Double.isNaN(tva[TVAO_TERRAIN][npix])) ? terrain_dflt[npix] : (tva[TVAO_TERRAIN][npix] + d0);  // added offset to adjusted only
							if (veg_xy == null) {
								synthetic_terrain[nScene][npix] = terrain_pix;
							}  else { 
								veg_xy = veg_xy.clone();
								if (diff_offsets) {
									veg_xy[0] += x;
									veg_xy[1] += y;
								}
								int x0 = (int) Math.floor(veg_xy[0]);
								int y0 = (int) Math.floor(veg_xy[1]);
								if (    full.contains(x0,  y0)   &&
										full.contains(x0+1,y0)   &&
										full.contains(x0,  y0+1) &&
										full.contains(x0+1,y0+1)) {
									double fx = veg_xy[0] - x0;
									double fy = veg_xy[1] - y0;
									double [] corn_w = {(1-fx)*(1-fy), fx*(1-fy), (1-fx)*fy, fx*fy};
									int veg_indx = x0+full.width*y0;
									int [] veg_indx4 = {veg_indx, veg_indx+1, veg_indx+full.width, veg_indx+full.width+1};
									double saw = 0, sawd = 0, sawv = 0;
									for (int i = 0; i < 4; i++) {
										double a = alpha[veg_indx4[i]];
										double v = tva[TVAO_VEGETATION][veg_indx4[i]];//vegetation_dflt
										if (Double.isNaN(v)) v = vegetation_dflt[veg_indx4[i]];
										if (!Double.isNaN(a)) {
											saw +=  corn_w[i]; 
											sawd += corn_w[i] * a; 
											sawv += corn_w[i] * v; 
										}
									}
									if (saw > 0) {
										sawd /= saw; // here it is interpolated alpha
										sawv /= saw;
										if (sawd <= terrain_max) {
											synthetic_terrain[nScene][npix] = terrain_pix;									
										}
										if (sawd >=  vegetation_min) {
											synthetic_vegetation[nScene][npix] = sawv + d0; // added offset										
										}
									} else { // no alpha
										synthetic_terrain[nScene][npix] =terrain_pix;
									}
								} else { // offset points outside of the image - unlikely
									synthetic_terrain[nScene][npix] = terrain_pix;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return synthetic;
	}
	
	
	public double [] renderWithAlpha(
			final double []   alpha,
			final double [][] scene_offsets, // subtract if not null
			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 full_length = full.width*full.height;
		final double [] render = new double [full_length];
		Arrays.fill(render, Double.NaN);
		final double []     scene_weights = setupSceneWeights(
				boost_parallax, // double boost_parallax)
				full,           // Rectangle woi)
				max_parallax);  //final double    ceil_parallax)				
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < full_length; nPix = ai.getAndIncrement()) {
						int x = nPix % full.width;
						int y = nPix / full.width;
						double sw = 0, swd = 0;
						for (int nscene = 0; nscene < num_scenes; nscene++) if (!Double.isNaN(terrain_rendered[nscene][nPix])) { 
							double scene_weight = scene_weights[nscene];
							double [] veg_xy = vegetation_offsets[nscene][nPix];
							double terrain = terrain_rendered[nscene][nPix];
							if (scene_offsets != null) {
								double offs = scene_offsets[nscene][nPix];
								if (!Double.isNaN(offs)) {
									terrain -= offs;
								}
								
							}
							if (veg_xy == null) {
								if (render_open) {
									sw += scene_weight;
									swd += scene_weight * terrain;
								}
							} else { // veg_xy !== null
								veg_xy = veg_xy.clone();
								if (diff_offsets) {
									veg_xy[0] += x;
									veg_xy[1] += y;
								}
								int x0 = (int) Math.floor(veg_xy[0]);
								int y0 = (int) Math.floor(veg_xy[1]);
								if (    full.contains(x0,  y0)   &&
										full.contains(x0+1,y0)   &&
										full.contains(x0,  y0+1) &&
										full.contains(x0+1,y0+1)) {
									double fx = veg_xy[0] - x0;
									double fy = veg_xy[1] - y0;
									double [] corn_w = {(1-fx)*(1-fy), fx*(1-fy), (1-fx)*fy, fx*fy};
									int veg_indx = x0+full.width*y0;
									int [] veg_indx4 = {veg_indx, veg_indx+1, veg_indx+full.width, veg_indx+full.width+1};
									double saw = 0, sawd = 0;
									for (int i = 0; i < 4; i++) {
										double a = alpha[veg_indx4[i]];
										if (!Double.isNaN(a)) {
											saw +=  corn_w[i]; 
											sawd += corn_w[i] * a; 
										}
									}
									if (saw > 0) {
										sawd /= saw; // here it is interpolated alpha
										double k =  weight_opaque + (1- weight_opaque)*Math.min(1.0, Math.max(0, (alpha_max - sawd)/(alpha_max-alpha_min)));
										sw += scene_weight * k;
										swd += scene_weight * k * terrain;
									} else {
										if (render_no_alpha) { // include no-alpha data is available (TODO: reduce weight?)
											sw += scene_weight;
											swd += scene_weight * terrain;
										}
									}
								} else if (render_open) { // offset points outside of the image - unlikely
									sw += scene_weight;
									swd += scene_weight * terrain;
								}
							}
						}
						if (sw == 0) {
							render[nPix] = Double.NaN;
						} else {
							render[nPix] = swd/sw;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return render;
		
	}
	
	
	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 VegetationSegment [] readAllSegments(
			String dir_path) {
		
		FileFilter parFileFilter = new FileFilter() 
		{
			//Override accept method
			public boolean accept(File file) {

				//if the file extension is .log return true, else false
				if (file.isFile() && file.getName().endsWith(PAR_EXT)) {
					return true;
				}
				return false;
			}
		};
		File dir = new File(dir_path);
		File [] par_files = dir.listFiles(parFileFilter);
		VegetationSegment [] segments = new VegetationSegment [par_files.length];
		for (int n = 0; n < par_files.length; n++) {
			Rectangle woi = new Rectangle();
			double [][] tvao = restoreParametersFile( // already trimmed to actual lengths FIXME: Not finished for real import ! 
					par_files[n].getPath(), //  String    path,
					woi); // Rectangle file_woi)
			segments[n] = new VegetationSegment(
					woi, // Rectangle   woi,
					tvao[TVAO_SCENE_OFFSET], // datadouble []   scene_offsets,
					new double [][] {tvao[0],tvao[1],tvao[2]}); // double [][]  tva) {} {}
		}
		// sort in line-scan order
		Arrays.sort(segments, new Comparator<VegetationSegment>() {
			@Override
			public int compare(VegetationSegment lhs, VegetationSegment rhs) {
				return (rhs.woi.y > lhs.woi.y) ? -1 : (rhs.woi.y < lhs.woi.y) ? 1 :
					((rhs.woi.x > lhs.woi.x) ? -1 : (rhs.woi.x < lhs.woi.x) ? 1 : 0); // increasing
			}
		});
		
		
		
		
		return segments;
	}
	
	
	public void readParametersFromImage(
			String    path,
			double [] vector,
			int       gap) {
		int [][] indices = getParamDebugIndices (gap);
		ImagePlus imp_pars = new ImagePlus (path);
		if (imp_pars.getWidth()==0) {
			throw new IllegalArgumentException("Could not read "+path);
		}
//		int woi_width3=woi.width*3 + 2 * gap;
		int woi_width4=woi_veg.width*4 + 3 * gap;
		int height = indices.length/woi_width4;
		if ((woi_width4 != imp_pars.getWidth()) || (height != imp_pars.getHeight())) {
			throw new IllegalArgumentException("Read image size does not match required: ("+imp_pars.getWidth()+"x"+imp_pars.getHeight()+
					") != ("+woi_width4+"x"+height+")");
		}
        // float [] pixels = (float[]) imp_pars.getProcessor().getPixels();

		int num_slices = imp_pars.getStack().getSize();
		float [] pixels = (float[]) imp_pars.getStack().getPixels(num_slices); // last slice
		
		for (int i = 0; i < indices.length; i++) {
			if (indices[i][0] == 0) { // parameter, > 0 nonmodified type+1, then [1] is full index. 
				double d = pixels[i];
				// scale alpha for the same image
				int w = i % woi_width4;
				int h = i / woi_width4;
				
				int nsub = w % (woi_veg.width + gap);
				if (nsub == 2) { // alpha
					d = (d- debug_alpha_scales[0])/(debug_alpha_scales[1] - debug_alpha_scales[0]);
				} else if (nsub== 3) { // elevation
					d = (d- debug_alpha_scales[2])/(debug_alpha_scales[3] - debug_alpha_scales[2]);
				}
				vector[indices[i][1]] = d;
			}
		}
		from_file = true;
		return;
	}
	
	
	
	
	
	public double debugDerivs (
			double [] vector,
			double    delta,
			int       debugLevel,
			boolean   show_img) {
		double  [][] jt_delta =  getFxDerivsDelta (
				vector, // double []         vector,
				delta,  // final double      delta,
				debugLevel); // final int         debug_level)
		double [][] jt = new double [jt_delta.length][];
		getFxDerivs(
				vector,      // final double []   vector,
				jt,          // final double [][] jt, // should be null or initialized with [vector.length][]
				debugLevel); // final int         debug_level)
		double [][] jt_diff = new double [jt_delta.length][jt_delta[0].length];
		double max_err=0;
		for (int n = 0; n < jt_diff.length; n++) {
			for (int w = 0; w < jt_diff[0].length; w++) {
				jt_diff[n][w] = jt[n][w] - jt_delta[n][w];
				int [] ti = par_rindex[n];
				if (Math.abs(jt_diff[n][w]) > max_err) {
					System.out.println("debugDerivs(): n="+n+" ("+ti[0]+":"+ti[1]+"), w = "+w +", diff="+jt_diff[n][w]+" jt="+jt[n][w]+" jt_delta="+jt_delta[n][w]);
				} else if (Math.abs(jt_diff[n][w]) > .1) {
					System.out.println("debugDerivs(): n="+n+" ("+ti[0]+":"+ti[1]+"), w = "+w +", diff="+jt_diff[n][w]+" jt="+jt[n][w]+" jt_delta="+jt_delta[n][w]);
				}
				max_err = Math.max(max_err, Math.abs(jt_diff[n][w]));
			}
		}
		System.out.println("delta = "+delta+", max_err = "+max_err);
		// think of visualization
		/**/
		if (show_img) {
			String [] frame_titles = {"jt","jt_delta", "jt_diff"}; 
			double [][][] debug_img = new double [frame_titles.length][][];
		}
		/**/
		return max_err;
	}
	
	
	private void setupWeights( // after setupParametersIndices
			final double [] scene_weights_in,
			final double reg_weights,
			final double hifreq_weight) {
		boolean use_hf = (y_src_hf != null);
		use_scenes_pull0 = (scenes_pull0 >=0);
		int extra_samples = 0;
		// using >=0 no use 0 as NOP but reserve space, <0 - do not reserve space
		
		//(alpha_loss > 0)
		if (use_scenes_pull0) extra_samples += 1;
		if ((alpha_loss > 0) || (alpha_push > 0)) extra_samples+= num_pars_alpha; // need to split loss (always positive) from alpha_lpf
		if (alpha_lpf >= 0)                       extra_samples+= num_pars_alpha;
		if (terr_lpf >= 0)                        extra_samples+= num_pars_terrain;
		if (veget_lpf >= 0)                       extra_samples+= num_pars_vegetation;
		if (elevation_lpf >= 0)                   extra_samples+= num_pars_elevation;

		double reg_sample_weight = reg_weights/extra_samples; // weight of each regularization sample
		if (scene_weights_in != null) {
			System.arraycopy(
					scene_weights_in,
					0,
					scene_weights,
					0,
					num_scenes);
		} else {
			Arrays.fill (scene_weights, 1.0);
		}
		weights = new double [y_vector.length + extra_samples];
		double s = 0;
		for (int ny = 0; ny < y_src.length; ny++) {
			int nscene =   y_src[ny][YSRC_SCENE];
			s += scene_weights[nscene];
		}
		double s_scenes = s; // sum of all scene weight. Maybe skip scenes that do not exist?
		if (use_hf) { // second pass, repeating for possible future modifications
			for (int ny = 0; ny < y_src_hf.length; ny++) {
				int nscene =   y_src_hf[ny][YSRC_SCENE];
				s += scene_weights[nscene] * hifreq_weight ;
			}
		}
		double s_pull_0 = s_scenes * scenes_pull0 * woi.width*woi.height;
		final double s0 = s;
		s *= (1+ reg_weights);
		final double k = 1.0/s;
		weight_pure = s0/s;
		scale_scenes_pull = scenes_pull0 * extra_samples / s_scenes; // or use extra_samples instead of woi.width*woi.height ?
		
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nw = ai.getAndIncrement(); nw < weights.length; nw = ai.getAndIncrement()) {
						if (nw  < y_src.length) { // DC differences
							weights[nw] = scene_weights[y_src[nw][YSRC_SCENE]] * k;
							if (Double.isNaN(weights[nw])) {
								System.out.println("weights["+nw+"]=NaN");
							}

						} else if (nw  < y_vector.length) {	// HF differences if exist
							weights[nw] = scene_weights[y_src_hf[nw-y_src.length][YSRC_SCENE]] * k * hifreq_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("weights["+nw+"]=NaN");
							}
						} else {
							weights[nw] = reg_sample_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("weights["+nw+"]=NaN");
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	private void setupYVector() {
		final int ds_length = y_src.length + ((y_src_hf != null)? y_src_hf.length : 0);
		y_vector = new double [ds_length];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ny = ai.getAndIncrement(); ny < y_src.length; ny = ai.getAndIncrement()) {
						int nscene =   y_src[ny][YSRC_SCENE];
						int indx =     y_src[ny][YSRC_FINDEX];
						y_vector[ny] = terrain_rendered[nscene][indx];
						if (Double.isNaN(y_vector[ny])) {
							System.out.println("y_vector["+ny+"]=NaN");
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (y_src_hf != null) {
			ai.set(0);
			final int hf_offset = y_src.length;
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int ny = ai.getAndIncrement(); ny < y_src_hf.length; ny = ai.getAndIncrement()) {
							int nscene =   y_src_hf[ny][YSRC_SCENE];
							int indx =     y_src_hf[ny][YSRC_FINDEX];
							y_vector[hf_offset + ny] = terrain_rendered_hf[nscene][indx];
							if (Double.isNaN(y_vector[hf_offset + ny])) {
								System.out.println("y_vector["+(hf_offset + ny)+"]=NaN");
							}
						}
					}
				};
			}		     
			ImageDtt.startAndJoin(threads);
		}
		return;
	}
	

	private int [][] getNeighbors(
			final int tvao,        // TVAO_VEGETATION_ALPHA
			final int ind_samples, // ind_pars_vegetation_alpha
			final int num_samples  // num_pars_vegetation_alpha
			){
		final int [][] alpha_neibs = new int[num_samples][4];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				TileNeibs tn = new TileNeibs(full.width, full.height);
				public void run() {
					for (int i = ai.getAndIncrement(); i < num_samples; i = ai.getAndIncrement()) {
						int pindx = ind_samples + i; // full parameter index
						int indx = par_rindex[pindx][1]; // full pixel index
						for (int dir4 = 0; dir4 < 4; dir4++) { // ortho only
							int dir = 2 * dir4;
							int nindx = tn.getNeibIndex(indx, dir);
							if (nindx < 0) {
								alpha_neibs[i][dir4] = -1;
							} else if (par_index[tvao][nindx] >= 0) {
								alpha_neibs[i][dir4] = par_index[tvao][nindx];
							} else { // pull to fixed alpha value
								alpha_neibs[i][dir4] = -2 -indx; // OFFSET by 2 !
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		//ind_pars_vegetation_alpha
		return alpha_neibs;
	}

	private void setupParametersVector() {
		// setup terrain parameters (use inner woi)
		for (int drow = 0; drow < woi.height; drow++) {
			int row = woi.y + drow;
			for (int dcol = 0; dcol < woi.width; dcol++) {
				int col = woi.x + dcol;	
				int indx = row*full.width + col;
				if (par_index[TVAO_TERRAIN][indx] >= 0) {
					parameters_vector[par_index[TVAO_TERRAIN][indx]] =    tvao[TVAO_TERRAIN][indx];
					if (Double.isNaN(parameters_vector[par_index[TVAO_TERRAIN][indx]])) {
						System.out.println("parameters_vector[par_index["+TVAO_TERRAIN+"]["+indx+"]]=NaN");
					}
				}
			}
		}
		for (int drow = 0; drow < woi_veg.height; drow++) {
			int row = woi_veg.y + drow;
			for (int dcol = 0; dcol < woi_veg.width; dcol++) {
				int col = woi_veg.x + dcol;	
				int indx = row*full.width + col;
		
				if (par_index[TVAO_VEGETATION][indx] >= 0) {
					parameters_vector[par_index[TVAO_VEGETATION][indx]] = tvao[TVAO_VEGETATION][indx];
					if (Double.isNaN(parameters_vector[par_index[TVAO_VEGETATION][indx]])) {
						System.out.println("parameters_vector[par_index["+TVAO_VEGETATION+"]["+indx+"]]=NaN");
					}
				}
				
				if (par_index[TVAO_ALPHA][indx] >= 0) {
					parameters_vector[par_index[TVAO_ALPHA][indx]] =  tvao[TVAO_ALPHA][indx];
					if (Double.isNaN(parameters_vector[par_index[TVAO_ALPHA][indx]])) {
						System.out.println("parameters_vector[par_index["+TVAO_ALPHA+"]["+indx+"]]=NaN");
					}
				}
				
				if (par_index[TVAO_ELEVATION][indx] >= 0) {
					parameters_vector[par_index[TVAO_ELEVATION][indx]] =  tvao[TVAO_ELEVATION][indx];
					if (Double.isNaN(parameters_vector[par_index[TVAO_ELEVATION][indx]])) {
						System.out.println("parameters_vector[par_index["+TVAO_ELEVATION+"]["+indx+"]]=NaN");
					}
				}
			}
		}
		for (int i = 0; i <  par_index[TVAO_SCENE_OFFSET].length; i++) if (par_index[TVAO_SCENE_OFFSET][i] >= 0){
			parameters_vector[par_index[TVAO_SCENE_OFFSET][i]]= tvao[TVAO_SCENE_OFFSET][i];
			if (Double.isNaN(parameters_vector[par_index[TVAO_SCENE_OFFSET][i]])) {
				System.out.println("parameters_vector[par_index["+TVAO_SCENE_OFFSET+"]["+i+"]]");
			}
		}
		return;
	}

	private void setupParametersIndices(
			boolean [] fits,
			final int debugLevel) {
		par_index = new int [TVAO_TYPES][];
		par_index[TVAO_TERRAIN] =          new int [image_length];
		par_index[TVAO_VEGETATION] =       new int [image_length];
		par_index[TVAO_ALPHA] =            new int [image_length];
		par_index[TVAO_SCENE_OFFSET] =     new int [num_scenes];
		par_index[TVAO_ELEVATION] =        new int [image_length];
		int max_pars = 0;
		for (int i = 0; i < par_index.length; i++) {
			max_pars += par_index[i].length;
		}
		int [][] par_rindex = new int [max_pars][2];
		
		Arrays.fill(par_index[TVAO_TERRAIN],     -1);
		Arrays.fill(par_index[TVAO_VEGETATION],  -1);
		Arrays.fill(par_index[TVAO_ALPHA],       -1);
		Arrays.fill(par_index[TVAO_ELEVATION],   -1);
		Arrays.fill(par_index[TVAO_SCENE_OFFSET],-1);
		int par_num = 0;
		ind_pars_terrain = 0;
		if (fits[TVAO_TERRAIN]) {
			for (int drow = 0; drow < woi.height; drow++) { // use inner woi for terrain
				int row = woi.y + drow;
				for (int dcol = 0; dcol < woi.width; dcol++) {
					int col = woi.x + dcol;	
					int windx =dcol + drow * woi.width;
					int indx = row*full.width + col;
					if (valid_terrain[windx]) {	 // woi				
						par_rindex[par_num][0] = TVAO_TERRAIN;
						par_rindex[par_num][1] = indx;
						par_index[TVAO_TERRAIN][indx] = par_num++;
					}
				}
			}
		}
		num_pars_terrain =    par_num;
		ind_pars_vegetation = par_num;
		
		if (fits[TVAO_VEGETATION]) {
			for (int drow = 0; drow < woi_veg.height; drow++) {
				int row = woi_veg.y + drow;
				for (int dcol = 0; dcol < woi_veg.width; dcol++) {
					int col = woi_veg.x + dcol;	
					int windx =dcol + drow * woi_veg.width;
					int indx = row*full.width + col;
					if (valid_vegetation[windx]) {	 // woi_veg				
						par_rindex[par_num][0] = TVAO_VEGETATION;
						par_rindex[par_num][1] = indx;
						par_index[TVAO_VEGETATION][indx] = par_num++;
					} else {
						par_index[TVAO_VEGETATION][indx] = -2 -indx;
					}
				}
			}
		}
		ind_pars_alpha = par_num;
		num_pars_vegetation = par_num - ind_pars_vegetation;
		if (fits[TVAO_ALPHA]) {
			for (int drow = 0; drow < woi_veg.height; drow++) {
				int row = woi_veg.y + drow;
				for (int dcol = 0; dcol < woi_veg.width; dcol++) {
					int col = woi_veg.x + dcol;	
					int windx =dcol + drow * woi_veg.width;
					int indx = row*full.width + col;
					if (valid_vegetation[windx]) {	 // woi_veg				
						par_rindex[par_num][0] = TVAO_ALPHA;
						par_rindex[par_num][1] = indx;
						par_index[TVAO_ALPHA][indx] = par_num++;
					} else {
						par_index[TVAO_ALPHA][indx] = -2 -indx;
					}
				}
			}
		}
		num_pars_alpha = par_num - ind_pars_alpha; // normally should be == num_pars_vegetation
		ind_pars_elevation =    par_num;
		if (fits[TVAO_ELEVATION]) {
			for (int drow = 0; drow < woi_veg.height; drow++) {
				int row = woi_veg.y + drow;
				for (int dcol = 0; dcol < woi_veg.width; dcol++) {
					int col = woi_veg.x + dcol;	
					int windx =dcol + drow * woi_veg.width;
					int indx = row*full.width + col;
					if (valid_vegetation[windx]) {	 // woi_veg				
						par_rindex[par_num][0] = TVAO_ELEVATION;
						par_rindex[par_num][1] = indx;
						par_index[TVAO_ELEVATION][indx] = par_num++;
					} else {
						par_index[TVAO_ELEVATION][indx] = -2 -indx;
					}
				}
			}
		}
		num_pars_elevation = par_num - ind_pars_elevation; // normally should be == num_pars_vegetation
		ind_pars_scenes =    par_num;
		if (fits[TVAO_SCENE_OFFSET]) {
			for (int nscene = 0; nscene < num_scenes; nscene++) {
				if (valid_scenes [nscene]) {
					par_rindex[par_num][0] = TVAO_SCENE_OFFSET;
					par_rindex[par_num][1] = nscene;
					par_index[TVAO_SCENE_OFFSET][nscene] = par_num++;
				}
			}
		}
		num_pars_scenes = par_num - ind_pars_scenes;
		this.par_rindex = new int [par_num][2];
		System.arraycopy(par_rindex, 0, this.par_rindex, 0, par_num);
		parameters_vector = new double [par_num];
		return;
	}
	
	private void setupYSrc (boolean use_hf) {
		final int woi_length = woi.width * woi.height;
		y_src_scene = new int [num_scenes][];
		y_wsrc =      new int [num_scenes][];
		int [][] ysrc = new int [num_scenes*woi.width*woi.height][YSRC_SIZE];
		int y_indx = 0;
		for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]) {
			y_wsrc[nscene] = new int [woi_length];
			Arrays.fill(y_wsrc[nscene], -1);
			int y_start = y_indx;
			for (int wrow = 0; wrow < woi.height; wrow++) {
				int row = woi.y + wrow;
				for (int wcol = 0; wcol < woi.width; wcol++) {
					int col = woi.x + wcol;	
					int indx = row*full.width + col;
					int windx = wrow*woi.width+wcol;
					double terrain = terrain_rendered[nscene][indx];
					if (valid_terrain[windx] && !Double.isNaN(terrain)) { // should be always?
						y_wsrc[nscene][windx] =     y_indx;
						ysrc[y_indx][YSRC_SCENE] =  nscene;
						ysrc[y_indx][YSRC_FINDEX] = indx;
						y_indx++;
					}
				}
			}
			y_src_scene[nscene] = new int [] {y_start, y_indx};
		}
		this.y_src = new int [y_indx][];
		System.arraycopy(
				ysrc,
				0,
				this.y_src,
				0,
				y_indx);
		// setup neighbors
		final int [][] y_neibs = new int [num_scenes][image_length];
		for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]) {
			Arrays.fill(y_neibs[nscene], -1);
		}
   		final Thread[]      threads =     ImageDtt.newThreadArray();
		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 y_indx = ai.getAndIncrement(); y_indx < y_src.length; y_indx = ai.getAndIncrement()) {
						int nscene = y_src[y_indx][YSRC_SCENE];
						int indx =   y_src[y_indx][YSRC_FINDEX];
						y_neibs[nscene][indx] = y_indx;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final boolean [] all4 = new boolean [y_src.length];
		ai.set(0);
		AtomicInteger anum = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(full.width, full.height);
					for (int y_indx = ai.getAndIncrement(); y_indx < y_src.length; y_indx = ai.getAndIncrement()) {
						int nscene = y_src[y_indx][YSRC_SCENE];
						int indx =   y_src[y_indx][YSRC_FINDEX];
						Arrays.fill(y_src[y_indx], YSRC_NEIB0, YSRC_NEIB0+YSRC_NEIBS, -1);
						int num_neibs = 0;
						for (int dir4 = 0; dir4 < 4; dir4++) {
							int indx1 = tn.getNeibIndex(indx,  2* dir4);
							if (indx1 >= 0){
								int y_indx1 = y_neibs[nscene][indx1];
								if (y_indx1 < 0) {
									y_indx1 = -2 - indx1; // fixed value index // will probably not be used - only those that have all 4 neighbors 
								} else {
									num_neibs++;
								}
								y_src[y_indx][YSRC_NEIB0 + dir4] = y_indx1;
							}						
						}
						if (num_neibs>= 4) {
							all4[y_indx] = true;
							anum.getAndIncrement();
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (use_hf) {
			y_src_hf = new int [anum.get()][];
			y_indx = 0;
			for (int i = 0; i < y_src.length; i++) if (all4[i]){
				y_src_hf[y_indx++] = y_src[i];
			}
		} else {
			y_src_hf = null;
		}
		return;
	}
	
	private Rectangle expandWoi(
			int max_offset) {
		final int x0 =   Math.max(0,  woi.x - max_offset);
		final int x1p1 = Math.min(full.x + full.width,   woi.x + woi.width + max_offset); // full.x==0
		final int y0 =   Math.max(0,  woi.y - max_offset);
		final int y1p1 = Math.min(full.y + full.height,  woi.y + woi.height + max_offset); // full.y==0
		return new Rectangle (x0, y0, x1p1-x0, y1p1-y0);
	}
	
	/**
	 * Get valid scenes, valid
	 * get WOI the that includes all vegetation pixels that influence at least
	 * one pixel inside this.woi and return integer array corresponding to the rectangle with
	 * number of uses. This array will be used to distinguish between the fixed data and fitted
	 * parameters.
	 * 
	 * @param warps - distance from the vegetation projection to that of its neighbors to discard scenes with too high distance.
	 *        distance >=2.0 will make undefined vegetation values for some terrain pixels (maybe increase radius of influence
	 *        for them?). If null, do not filter by warp.
	 * @param max_warp - limit warp value to discard scene. if <= 1.0 - do not limit by warp         
	 * @param valid_pixels_in boolean array of valid terrain/y_vector pixels inside woi or null if all are valid.
	 * @param max_offset maximal "elevation" to consider
	 * @param max_sacrifice maximal number of first/last scenes that can be discarded to maximize number of terrain
	 *        pixels (inside woi) that have vegetation that influences them. Pixels that have vegetation cover in
	 *        some "middle" scenes, but not in others will have vegetation removed, so the potential vegetation
	 *        should be wide enough extending over areas that do not have actual vegetation.
	 * @param stats null or int [5] - will return number of valid scenes, number of terrain pixels, number of vegetation pixels,
	 *        number of terrain pixels that are influenced by the vegetation, and number of scenes removed for bad warp; 
	 * @param max_warp_scene_pix - null or int[2] - will return scene and full pixel that have highest warp                 
	 * @param woi - the terrain/y_vector area to run LMA. 
	 * @param woi_veg - should be initialized, will be updated to include vegetation pixels that overlap woi in
	 *        some scenes. 
	 * @return combined boolean 2d array: {"valid scenes", "valid pixels", "valid vegetation pixels",
	 *        "have vegetation influence"}.
	 *        Of them "valid scenes" is num_scenes long, "valid pixels" and "have vegetation influence" - woi.width*woi.heigh,
	 *        and "valid vegetation pixels" - woi_veg.width*woi_veg.height. 
	 */
	
	private boolean [][] getValidScenesPixels(
			final double[][][] warps,
			final double       max_warp,
			final boolean []   valid_pixels_in, // should be initialized to woi, normally set to all true 
			final int          max_offset,
			final int          max_sacrifice,
			final int []       stats,
			final int []       max_warp_scene_pix, 
			final Rectangle    woi, 
			final Rectangle    woi_veg // should be initialized, will be modified
			) {
		final int woi_length = woi.width*woi.height;
		final boolean [] valid_pixels = (valid_pixels_in != null) ? valid_pixels_in : (new boolean [woi_length]);
		if (valid_pixels_in == null) {
			Arrays.fill(valid_pixels, true);
		}
		final int num_scenes = this.scales_xy.length;
		final Rectangle woi_ext = expandWoi(max_offset);
		final int woi_ext_len = woi_ext.width*woi_ext.height;
		
		final Thread[]      threads =     ImageDtt.newThreadArray();
		final AtomicInteger ai =          new AtomicInteger(0);
		final Rectangle woi_plus1 = new Rectangle (woi.x-1, woi.y-1, woi.width+2,woi.height+2);
//		final int [] num_used = new int [woi_ext_len]; // [num_scenes]
		final boolean [] veg_scene_used_prefilter = new boolean [woi_ext_len]; //
		final boolean [] veg_scene_used = new boolean [woi_ext_len];
		final boolean [][][] has_veg_thread = new boolean [threads.length][num_scenes][woi_length];
		final boolean [] valid_scenes = new boolean[num_scenes];
		final int y0 = woi_ext.y, y1 = woi_ext.y + woi_ext.height, x0 = woi_ext.x, x1 = woi_ext.x + woi_ext.width;
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) {
						check_scene: {
							double [] terrain = terrain_rendered[nScene];
							for (int y = y0; y < y1; y++) {
								for (int x = x0; x < x1; x++) {
									if (Double.isNaN(terrain[x+y*full.width])) {
										break check_scene;
									}
								}
							}
							valid_scenes[nScene] = true;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		Rectangle dbg_rectangle = new Rectangle(80,61,3,3);
		int dbg_scene = -81;
		final AtomicInteger ati =          new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					int nthread = ati.getAndIncrement();
					for (int indx = ai.getAndIncrement(); indx < woi_ext_len; indx = ai.getAndIncrement()) {
						int y = indx / woi_ext.width  + woi_ext.y;
						int x = indx % woi_ext.width  + woi_ext.x;
						int npix = x + y * full.width;
						double elevation =  tvao[TVAO_ELEVATION][npix];
						double vegetation = tvao[TVAO_VEGETATION][npix];
						if (!Double.isNaN(elevation) && !Double.isNaN(vegetation)) {
							for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]){
								if ((nscene==dbg_scene) && dbg_rectangle.contains(x,y)) {
									System.out.println("dbg_scene="+dbg_scene+" x="+x+", y="+y);
								}
						//  for (int nscene = 0; nscene < 62; nscene++) if (valid_scenes[nscene]){
								double [] scales =  scales_xy[nscene][npix];
								if (scales != null) {
									double dx = scales[0] * elevation;
									double dy = scales[1] * elevation;
									int px05 = (int) Math.floor(x - dx); // TODO: check sign!
									int py05 = (int) Math.floor(y - dy); // TODO: check sign!
									if (woi_plus1.contains(px05,py05)) { // up to 1 pixel to the nearest
										// valid_pixels
										boolean has_valid_dependent = false;
										for (int yc = 0; yc <2; yc++) {
											int wy = py05+yc-woi.y;
											if ((wy >= 0) && (wy < woi.height)) {
												for (int xc = 0; xc < 2; xc++) {
													int wx = px05+xc-woi.x;
													if ((wx >=0) && (wx < woi.width)) {
														int wpix =wx + wy*woi.width; 
														if (valid_pixels[wpix]) {
															has_veg_thread[nthread][nscene][wpix] = true;
															has_valid_dependent = true;
														}
													}
												}
											}
										}
										if (has_valid_dependent) {
											veg_scene_used_prefilter[indx] = true;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		
		// combine from threads
		final boolean [][] has_veg = new boolean [num_scenes][woi_length]; 
		final AtomicInteger afirst_scene = new AtomicInteger(num_scenes);
		final AtomicInteger alast_scene =  new AtomicInteger(0);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement())  if (valid_scenes[nScene]){
						afirst_scene.getAndAccumulate(nScene, Math::min);
						alast_scene.getAndAccumulate(nScene, Math::max);
						for (int wpix = 0; wpix < woi_length; wpix++) {
							for (int nthread =0; nthread < has_veg_thread.length; nthread++) {
								has_veg[nScene][wpix] |=has_veg_thread[nthread][nScene][wpix];
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final int first_scene = afirst_scene.get();
		final int last_scene =  alast_scene.get();
		// now similar to orange brunch make all scenes have the same pixels that have vegetation - remove "unstable" (border) pixels 
		// dependency on vegetation and then first/last scenes
		final AtomicInteger anum_offsets = new AtomicInteger(0);
		final boolean [] valid_offset =    new boolean [woi_length];
		final AtomicInteger anum_offsets_middle = new AtomicInteger(0);
		final AtomicBoolean [] bad_offset_scenes = new AtomicBoolean[num_scenes];
		for (int nscene = first_scene; nscene <= last_scene; nscene++) {
			bad_offset_scenes[nscene] = new AtomicBoolean(false);
		}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement()) { // if (valid_pixels[wPix]){
						boolean has_offset_middle = true;
						for (int nscene = first_scene + max_sacrifice; nscene <= last_scene-max_sacrifice; nscene++) if (valid_scenes[nscene]){
							if (!has_veg[nscene][wPix]) {
								has_offset_middle = false;
								break;
							}
						}
						if (has_offset_middle) {
							boolean has_offset = true;
							for (int nscene = first_scene; nscene < first_scene + max_sacrifice; nscene++) if (valid_scenes[nscene]){
								if (!has_veg[nscene][wPix]) {
									has_offset = false;
									bad_offset_scenes[nscene].set(true);
								}
							}
							for (int nscene = last_scene-max_sacrifice + 1; nscene <= last_scene; nscene++) if (valid_scenes[nscene]){
								if (!has_veg[nscene][wPix]) {
									has_offset = false;
									bad_offset_scenes[nscene].set(true);
								}
							}
							valid_offset[wPix] = true;
							anum_offsets_middle.getAndIncrement();
							if (has_offset) {
								anum_offsets.getAndIncrement();
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
//		int num_removed = 0;
		if (anum_offsets.get() < anum_offsets_middle.get()) {
			System.out.println("getValidScenesPixels(): Had to remove some scenes that do not have offset data for pixels that have it in most scenes.");
			System.out.println("Number of pixels with offset data in most scenes: "+anum_offsets_middle.get()+", in all scenes: "+anum_offsets.get()+".");
			for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]) {
				if (bad_offset_scenes[nscene].get()) {
					valid_scenes[nscene] = false;
//					num_removed++;
					System.out.println("Removing scene "+nscene+".");
				}
			}
		}
		// re-scan woi_ext keeping only those that have dependents marked as having vegetation
		
		final AtomicInteger amax_x = new AtomicInteger(0);
		final AtomicInteger amax_y = new AtomicInteger(0);
		final AtomicInteger amin_x = new AtomicInteger(full.width);
		final AtomicInteger amin_y = new AtomicInteger(full.height);
//        ati.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
//					int nthread = ati.getAndIncrement();
					for (int indx = ai.getAndIncrement(); indx < woi_ext_len; indx = ai.getAndIncrement()) if (veg_scene_used_prefilter[indx]){
						int y = indx / woi_ext.width  + woi_ext.y;
						int x = indx % woi_ext.width  + woi_ext.x;
						int npix = x + y * full.width;
						double elevation =  tvao[TVAO_ELEVATION][npix];
						double vegetation = tvao[TVAO_VEGETATION][npix];
						if (!Double.isNaN(elevation) && !Double.isNaN(vegetation)) {
							for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]){
								double [] scales =  scales_xy[nscene][npix];
								if (scales != null) {
									double dx = scales[0] * elevation;
									double dy = scales[1] * elevation;
									int px05 = (int) Math.floor(x - dx);
									int py05 = (int) Math.floor(y - dy);
									if (woi_plus1.contains(px05,py05)) { // up to 1 pixel to the nearest
										// valid_pixels
										boolean has_valid_dependent = false;
										for (int yc = 0; yc <2; yc++) {
											int wy = py05+yc-woi.y;
											if ((wy >= 0) && (wy < woi.height)) {
												for (int xc = 0; xc < 2; xc++) {
													int wx = px05+xc-woi.x;
													if ((wx >=0) && (wx < woi.width)) {
														int wpix =wx + wy*woi.width; 
														if (valid_pixels[wpix] && valid_offset[wpix]) {
															has_valid_dependent = true;
														}
													}
												}
											}
										}
										if (has_valid_dependent) {
											veg_scene_used[indx] = true;
										}
									}
								}
							}
							if (veg_scene_used[indx]) { // at least once used
								amax_x.getAndAccumulate(x, Math::max);
								amax_y.getAndAccumulate(y, Math::max);
								amin_x.getAndAccumulate(x, Math::min);
								amin_y.getAndAccumulate(y, Math::min);
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
   
		// copy num_used to a smaller array
		woi_veg.x=amin_x.get();
		woi_veg.y=amin_y.get();
		woi_veg.width=  amax_x.get()-amin_x.get()+1;
		woi_veg.height= amax_y.get()-amin_y.get()+1;
		final boolean [] valid_vegetation = new boolean [woi_veg.width*woi_veg.height];
		for (int row = 0; row < woi_veg.height; row++) {
			int row_src = row + woi_veg.y - woi_ext.y;
			int col_src = woi_veg.x - woi_ext.x;
			System.arraycopy(
					veg_scene_used,
					col_src + row_src * woi_ext.width,
					valid_vegetation,
					row*woi_veg.width,
					woi_veg.width);
		}
		// delete scenes that have too high warp
		AtomicInteger abad_warp = new AtomicInteger(0);
		if ((warps != null) && (max_warp > 1.0)) {
			boolean [] valid_prewarp = valid_scenes.clone();
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement())  if (valid_scenes[nScene]){
							for (int wpix = 0; wpix < valid_vegetation.length; wpix++) if (valid_vegetation[wpix]) {
								int npix = (wpix % woi_veg.width + woi_veg.x) + (wpix / woi_veg.width + woi_veg.y)*full.width; 
								if ((warps[nScene][npix] != null) && (warps[nScene][npix][0] > max_warp)) {
									abad_warp.getAndIncrement();
									valid_scenes[nScene] = false;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			if (abad_warp.get() > 0) {
				System.out.println("getValidScenesPixels(): removing "+abad_warp.get()+" scenes as the have warp for used vegetation pixels > "+
						max_warp);
				for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_prewarp[nscene] && !valid_scenes[nscene]){
					System.out.println("Removing scene "+nscene);
				}
			}
		}
		if (stats != null) {
			AtomicInteger [] astats = new AtomicInteger[stats.length];
			for (int i = 0; i <astats.length; i++) {
				astats[i] = new AtomicInteger(0);
			}
			// get number of valid scenes
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement())  if (valid_scenes[nScene]){
							astats[0].getAndIncrement();
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
				threads[ithread] = new Thread() {
					public void run() {
						for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement()) {
							if (valid_pixels[wPix]) astats[1].getAndIncrement();
							if (valid_offset[wPix]) astats[3].getAndIncrement();
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			
			ai.set(0);
			int woi_veg_length = woi_veg.width*woi_veg.height;
			for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
				threads[ithread] = new Thread() {
					public void run() {
						for (int wPix = ai.getAndIncrement(); wPix < woi_veg_length; wPix = ai.getAndIncrement()) {
							if (valid_vegetation[wPix]) astats[2].getAndIncrement();
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			stats[0] = astats[0].get();
			stats[1] = astats[1].get();
			stats[2] = astats[2].get();
			stats[3] = astats[3].get();
			stats[4] = abad_warp.get();
		}
		if ((warps != null) && (max_warp_scene_pix != null)) {
			final int [][]  max_warp_scene_pix_thread = new int [threads.length][];
			final double [] max_warp_thread = new double [threads.length];
			ai.set(0);
			ati.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						int nthread = ati.getAndIncrement();
						for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement())  if (valid_scenes[nScene]){
							double mw = 0;
							for (int wpix = 0; wpix < valid_vegetation.length; wpix++) if (valid_vegetation[wpix]) {
								int npix = (wpix % woi_veg.width + woi_veg.x) + (wpix / woi_veg.width + woi_veg.y)*full.width; 
								if ((warps[nScene][npix] != null) && (warps[nScene][npix][0] > mw)) {
									mw = warps[nScene][npix][0];
									if (max_warp_scene_pix_thread[nthread] == null) {
										max_warp_scene_pix_thread[nthread] = new int[2];
									}
									max_warp_scene_pix_thread[nthread][0] = nScene;
									max_warp_scene_pix_thread[nthread][1] = npix;
									max_warp_thread[nthread] = mw;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			double mw = -1;
			for (int nthread = 0; nthread < threads.length; nthread++) {
				if (max_warp_thread[nthread] > mw) {
					mw = max_warp_thread[nthread];
					max_warp_scene_pix[0] = max_warp_scene_pix_thread[nthread][0];
					max_warp_scene_pix[1] = max_warp_scene_pix_thread[nthread][1];
				}
			}
		}
		return new boolean[][] {valid_scenes, valid_pixels, valid_vegetation, valid_offset};
	}
	

	public static void showWarps(
			double [][][] warps,
			double []     elevations,
			double [][][] scales_xy,
			int           width,
			String        title) {
		final int num_scenes = scales_xy.length;
		final int num_pixels = elevations.length;
		String [] titles = new String [num_scenes];
		for (int nscene = 0; nscene < titles.length; nscene++) {
			titles[nscene]= "scene_"+nscene;
		}
		String [] top_titles= {"max_warp","max_hor","max_vert","x_offs","y_offs"};
		double [][][] dbg_img =new double [top_titles.length][num_scenes][num_pixels];
		for (int t = 0; t < dbg_img.length; t++) {
			for (int nscene = 0; nscene < num_scenes; nscene++) {
				Arrays.fill(dbg_img[t][nscene], Double.NaN);
			}
		}
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			for (int npix = 0; npix < num_pixels;  npix++) {
				double [] scales =  scales_xy[nscene][npix];
				if (scales != null) {
					double elevation =  elevations[npix];
					double dx = scales[0] * elevation;
					double dy = scales[1] * elevation;
					dbg_img[3][nscene][npix] = dx;
					dbg_img[4][nscene][npix] = dy;
				}
				double [] warp = warps[nscene][npix];
				if (warp != null) {
					dbg_img[0][nscene][npix] = warp[0];
					dbg_img[1][nscene][npix] = warp[1];
					dbg_img[2][nscene][npix] = warp[2];
				}
			}
		}
		ShowDoubleFloatArrays.showArraysHyperstack(
				dbg_img,    // double[][][] pixels, 
				width,      // int          width, 
				title,      // "terrain_vegetation_render.tiff", // 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)
		return;
	}
	
	
	/**
	 * Calculate horizontal and vertical distances from this pixel's vegetation projections to those of their
	 * neighbors. If this distance is >= 2.0 - there will be no overlap and so no defined vegetation image value.
	 * It may be possible to increase the influence radius to mitigate it, will start with just selecting scenes
	 * where warps are limited to a certain value (warps may grow with LMA elevation adjustments)   
	 * @param elevations per-pixel "elevation" value measured as the horizontal pixel offset from the reference
	 *        scene at the most offset scene.
	 * @param scales_xy - slowly changing (ideally the same for the whole scene) scale to calculate horizontal
	 *        offset (scene-dependent) from elevation at this pixel (common for all scenes)
	 * @param width image width (640 pix)
	 * @return array of per-pixel sets of {maximal_warp, horizontal_warp, vertical_warp} or null where warps is not  
	 */
	public static double [][][] getMaxWarp(
			final double []     elevations,
			final double [][][] scales_xy,
			final int           width) {
		final int num_scenes = scales_xy.length;
		final int num_pixels = elevations.length;
		final int height = num_pixels/width;
		final double [][][] max_warps = new double [num_scenes][num_pixels][];
		final int [][] xy_offs= {{0,-1},{1,0},{0,1},{-1,0}};
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		int dbg_scene = 45;
		int dbg_pix = 32060;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) {
						for (int npix = 0; npix < num_pixels; npix++) if (scales_xy[nScene][npix] != null) {
							if ((nScene == dbg_scene) && (dbg_pix ==  npix)) {
								System.out.println();
							}
							double [] scales =  scales_xy[nScene][npix];
							double elevation =  elevations[npix];
							int y = npix / width;
							int x = npix % width;
							double dx = scales[0] * elevation;
							double dy = scales[1] * elevation;
							double xc = x-dx, yc =  y-dy; // shifted coordinates
							double [][] xy_neibs =  new double[4][];
//							double [][] scales4 = new double[4][];
							
							for (int dir4 = 0; dir4 < 4; dir4++) {
								int npix1 = tn.getNeibIndex(npix, 2 * dir4);
								if (npix1 >= 0){
									double [] scales_neib = scales_xy[nScene][npix1];
									if (scales_neib != null) {
										double dx_neib = scales_neib[0] * elevations[npix1];
										double dy_neib = scales_neib[1] * elevations[npix1];
										xy_neibs[dir4] = new double [] {
												x-dx_neib+xy_offs[dir4][0],	
												y-dy_neib+xy_offs[dir4][1]};
									}
								}
							}
							double [] warps = {Double.POSITIVE_INFINITY,Double.POSITIVE_INFINITY};
							for (int dir4 = 1; dir4 < 4; dir4+=2) if (xy_neibs[dir4] != null) {
								double d = xy_neibs[dir4][0] - xc;
								warps[0] = Math.min(warps[0], Math.abs(d));
							}
							for (int dir4 = 0; dir4 < 4; dir4+=2) if (xy_neibs[dir4] != null) {
								double d = xy_neibs[dir4][1] - yc;
								warps[1] = Math.min(warps[1], Math.abs(d));
							}
							if (Double.isFinite(warps[0]) && Double.isFinite(warps[1])) {
								double warp_max = 0;
								for (int i = 0; i < warps.length; i++) {
									if (Double.isFinite(warps[i])) {
										warp_max = Math.max(warp_max, warps[i]);
									} else {
										 warps[i] = Double.NaN;
									}
								}
								max_warps[nScene][npix] = new double [] {warp_max, warps[0], warps[1]};
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return max_warps;	
	}
	
	
	
	
	
}
