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

import java.awt.Rectangle;
import java.io.File;
import java.io.FileFilter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
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.CholeskyBlock;
import com.elphel.imagej.common.CholeskyBlockTest;
import com.elphel.imagej.common.CholeskyFloat;
import com.elphel.imagej.common.CholeskyLDLTMulti;
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.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TileNeibs;
import com.elphel.imagej.tileprocessor.TileProcessor;

import Jama.CholeskyDecomposition;
import Jama.Matrix;
import ij.IJ;
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 String [] TVAO_NAMES = {"TERRAIN","VEGETATION","ALPHA","ELEVATION","TERR_ELEVATION","TERR_ELEV_PIX","SCENE_OFFSET"};
	public static final String [] TVAO_names = {"terr_fit_terr", "terr_fit_veget", "terr_fit_alpha", "terr_fit_elevations",
			                                    "terr_fit_terr_elev", "terr_fit_terr_elev_pix", "terr_fit_scenes"};        
	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_TERR_ELEV =        4; // common for all the tile
	public static final int TVAO_TERR_ELEV_PIX =    5; // per-pixel terrain elevation
	public static final int TVAO_SCENE_OFFSET =     6;
	public static final int TVAO_TYPES =            TVAO_SCENE_OFFSET + 1;
	
	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 SAMPLES_Y =               0;
	public static final int SAMPLES_Y_HF =            1;
	public static final int SAMPLES_Y_AVG =           2;
	public static final int SAMPLES_SCENES =          3;
	public static final int SAMPLES_ALPHA_PULL =      4;
	public static final int SAMPLES_ALPHA_LPF =       5;
	public static final int SAMPLES_TERRAIN_PULL =    6;
	public static final int SAMPLES_TERRAIN_LPF =     7;
	public static final int SAMPLES_VEGETATION_PULL = 8;
	public static final int SAMPLES_VEGETATION_LPF =  9;
	public static final int SAMPLES_ELEVATION_PULL = 10;
	public static final int SAMPLES_ELEVATION_LPF =  11;
	public static final int SAMPLES_ELEVATION_ALPHA =12; // loss for low elevation * high alpha
	public static final int SAMPLES_TERR_ELEV_PULL = 13;
	public static final int SAMPLES_TERR_ELEV_LPF =  14;
	public static final int SAMPLES_TOTAL =          15; // {total samples, extra_samples} 
	public static final int SAMPLES_SIZE =  SAMPLES_TOTAL+1;
	public static final int SAMPLES_EXTRA = SAMPLES_SCENES; // start of "extra" samples
	
	
	public final static String[] SAMPLES_NAMES = {"SAMPLES_Y", "SAMPLES_Y_HF", "SAMPLES_Y_AVG",
			"SAMPLES_SCENES","SAMPLES_ALPHA_PULL","SAMPLES_ALPHA_LPF","SAMPLES_TERRAIN_PULL",
			"SAMPLES_TERRAIN_LPF","SAMPLES_VEGETATION_PULL","SAMPLES_VEGETATION_LPF",
			"SAMPLES_ELEVATION_PULL","SAMPLES_ELEVATION_LPF","SAMPLES_ELEVATION_ALPHA",
			"SAMPLES_TERR_ELEV_PULL", "SAMPLES_TERR_ELEV_LPF", "SAMPLES_TOTAL"};	
	// Add terrain elevation !!!
	public static final int [] SAVE_TYPES =
		{TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_ELEVATION,	TVAO_TERR_ELEV_PIX, TVAO_SCENE_OFFSET};

	public static final int CHOLESKY_BLOCK =         0; // multithreaded
	public static final int CHOLESKY_JAMA =          1; // standard, from JAMA
	public static final int CHOLESKY_FLOAT =         2; // floating point precision (for GPU eveluation
	public static final int CHOLESKY_LU =            3; // LU instead of Cholesky (old way)
	public final static String[] CHOLESKY_NAMES = {"multi-threaded", "jama", "float","LU"};
	
	
	
	private final Rectangle   full;
	private final int         image_length;
	// should be non-null everywhere where elevation is defined?
	private double []         vegetation_average;  // full image average vegetation (with nulls)
	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
	// 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;
	private int [][]          par_index;       // indices -  [0..6][full_pixel] - same as for tvao, value - parameter index
	private int [][]          par_rindex;      // parameter -> pair of type (0..4) and full window pixel index
	private int []            num_pars = new int [TVAO_TYPES];
	private int []            ind_pars = new int [TVAO_TYPES]; // should be in increasing order?
	
	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]
	private double            terrain_correction_weight = 1.0;
	private double            weight_to_scene_weight = 1;
	private boolean           recalc_average = false; //apply transparency to average mismatch
	

	private int [][]          samples_pointers;  // {start,length} for each samples section, for extra 3-rd is  ind_pars_*
	
	// 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 double            elev_radius_extra =  1.2;      // scale both radii when setupElevationLMA(), setupTerrainElevationLMA(), and setupTerrainElevationPixLMA() 
	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 NOT USED yes , not used (with derivatives verified by delta)
	private double [][][]     elev_dsum_weights; // [scene][elev_par][woi] derivative of elev_sum_weights by each of the elevation parameter
	

	// elevation-dependent parameters, calculated once if terrain elevation is not adjusted or each time if they are
	// consider a special case when terrain elevation is 0
	private double            terr_elev_radius = 1.5; // for the future - make variable-size influence of the vegetation to mitigate far influenced pixels
	private int [][][]        terr_elev_woi4;         // [scene][woi_terr][~4] indices (in woi) of 4 (or more/less) neighbors of projections (negatives)
	private double [][][]     terr_elev_weights4;     // [scene][woi_terr][~4] weights of influence for 4 neighbors
	private double [][]       terr_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 [][][]     terr_elev_dweights4;    // [scene][woi_terr][4] derivatives of weights of influence for 4 neighbors with respect to elevations
	private double [][]       terr_elev_dsum_weights; // [scene][woi] derivative of elev_sum_weights by terr_elev parameter
	
	private int [][][]        terr_elpix_woi4;         // [scene][woi_veg][~4] indices (in woi) of 4 (or more/less) neighbors of projections (negatives)
	private double [][][]     terr_elpix_weights4;     // [scene][woi_veg][~4] weights of influence for 4 neighbors
	private double [][]       terr_elpix_sum_weights;  // [scene][woi] sum weights from up to 4 elevation pixels (even more with overlap)
	// derivatives, only defined when elevations are adjusted NOT USED yes , not used (with derivatives verified by delta)
	private double [][][]     terr_elpix_dsum_weights; // [scene][elev_par][woi] derivative of terr_elpix_sum_weights by each of the elevation parameter
	
	
	
	
	private double            terr_elev_last = Double.NaN; // terrain elevation for which terr_elev_woi4, ... where last calculated
	private double            terr_elev_last_success =  0; // terrain elevation last successfully used
///	private double            terr_elev_initial =      0; // use with initial rms
	// 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 ArrayList<HashSet<Integer>> terr_elev_contribs; // new ArrayList<HashSet<Integer>>(woi_length);
	private ArrayList<HashSet<Integer>> terr_elpix_contribs; // new ArrayList<HashSet<Integer>>(woi_length);
	
	
	private boolean [][]      param_pivot;   // [all_pars][all_pars] which parameters parameters have common dependents
	private int [][]          param_lists;   // [all_pars][related]  - for each parameter - null or integer array of related (by JtJ) parameters
	
	private boolean []        valid_scenes;            // [num_scenes]
	private boolean []        valid_terr_proj =  null; // [woi.width*woi.height] valid terrain and y_vector pixels
	private boolean []        valid_vegetation = null; // [woi_veg.width*woi_veg.height] valid vegetation pixels
	private boolean []        has_vegetation =   null; // [woi.width*woi.height] this valid terrain pixel is overlaid by vegetation (in all valid scenes) 
	private boolean []        valid_terrain =    null; // [woi_terr.width*woi_terr.height] valid terrain pixels
	private boolean []        has_terrain =      null; // [woi.width*woi.height] has terrain source pixels
	private double [][][]     warps =            null; // measure of horizontal and vertical distances between the vegetation projections - use to filter scenes
	
	private final double []   scene_weights;
	
	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
	
	boolean []                 fits_disable =          new boolean[TVAO_TYPES];              // (+) 
	boolean []                 fits_disable_terronly = new boolean[TVAO_TYPES];              // (+) 
	private double             terrain_correction = 0.02; // (+) weight of y_wavg
	
	// Parameters saved/resored in a file
	private final int          num_scenes;                // (+) 
	private Rectangle          woi =             null;    // (+)
	private Rectangle          woi_terr =        null;    // ()  extended woi to variable-elevation terrain
	private Rectangle          woi_veg =         null;    // (*) extended woi to include offset vegetation
	private Rectangle          woi_max =         null;    //     contains woi, woi_veg, and woi_terr
	
	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 = new boolean[TVAO_TYPES];// (+) 
//	public boolean             terr_elevation_full = false; // false - common terrain elevation, true - per-pixel terrain elevation
	private double             alpha_loss =         0;    // (+) quadratic loss
	private double             alpha_loss_lin =     0.5;  // (+) linear loss
	private double             alpha_offset =       0;    // (+) if >0,  start losses below 1.0;
	private double             alpha_0offset =      0;    // (+) if >0,  start losses above 0.0
	private double             alpha_min_veg =      0.5;  // (+) if (alpha-alpha_offset)/(1-2*alpha_offset) < alpha_min_veg, pull down to lpha_offset
	private double             alpha_max_terrain =  0.75; // (+) increase pull vegetation if below
	private double             alpha_pull_pwr =     1.0;  // (+) raise extra pull to that power
	
	private double             alpha_lpf =          0;    // (+) 
	private double             alpha_lpf_border =   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 double             w_alpha_neib =       0.7;  // (*) weight of each of the neighbors relative to the center when calculating average alpha
	private double             holes_pwr =          2.0;  // (*) Raise alpha (0..1) to this power when replacing vegetation cold spots with holes
	private double             terr_lpf =           0;    // (+)
	private double             veget_lpf =          0;    // (+)
	private double             elevation_lpf =      0;    // (*)
	private double             terr_elev_lpf =      0;    // (*)
	
	private boolean []         lpf_fixed = {true, true,true, 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             terr_pull_up =       0;    // (+) Terrain pixels pull to initial (pre-adjustment) values when it is colder than initial
	private double             terr_pull_avg =      0;    // (+) pull terrain to the initial offset by the average offset of all terrain pixels  
	private double             veget_pull0 =        0;    // (+) now - pull to vegetation_pull (extended vegetation)
	private double             veget_pull_low_alpha = 10; // () scale pull0 for low alpha (mostly terrain) 
	public  double             elevation_pull0 =    0;    // (*) now - pull to initial elevation
	private double             terr_elev_pull0 =    0;    // (*)
	
	public boolean             elev_alpha_en =      false;// (+) false; // Enable loss for low vegetation with high opacity
	public double              elev_alpha =         1.0;  // (+) multiply alpha by under-low elevation for loss
	public double              elev_alpha_pwr =     2.0;  // raise alpha to this power (when alpha > 0)
	public double              low_veget =          2.0;  // (+) (pix) Elevation considered low (lower loss for high alpha)
	
	private double             scenes_pull0 =       0;    // (*)  pull average scene offset to 0;
	private double             scale_scenes_pull =  0;    // (?) used in getFxDerivs to scale scene offsets as their weight will be reg_weights / extra_samples	
	
    // scaling elevation losses for high elevations (decrease pull and/or lpf for high elevations)
	public double              elev_scale_thresh =  1.0;   // (+) reduce losses for higher (initial) elevations TODO: consider actual elevations
	public boolean             elev_scale_pull =   false;  // (+) scale elevation pull losses for high elevations 
	public boolean             elev_scale_lpf =    false;  // (+) scale elevation diffusion losses for high elevations 
	// tree-top removal
	public boolean             ttop_en =           false; // (+) remove tree tops from transparency weights
	public double              ttop_gb =           1.0;   // (+) Elevation Gaussian blur sigma to detect tree tops
	public double              ttop_min =          3.0;   // (+) Minimal tree top elevation
	public double              ttop_rel_lev =      0.9;   // (+) Relative (to the top height) sample level
	public double              ttop_rel_rad =      0.25;  // (+) Relative (to the top height) sample ring radius
	public double              ttop_frac =         0.5;   // (+) Minimal fraction of the ring pixels below sample level
	public double              ttop_rem_rad =      0.25;  // (+) Relative (to the top height) remove transparency radius
//	public boolean             terr_only_special = true;  // (+) special sequences for terrain-only tiles 
//	public boolean             terr_only_pix =     true;  // (+) force per-pixel terrain elevation in terrain-only mode, overwrite fits_disable[TVAO_TERR_ELEV_PIX]
	
	public  double             boost_parallax =     1;    // (+)
	public  double             max_parallax =      10;    // (+)
	public double              max_warp =           3.8;       // 1.8 - do not use scenes where distance between vegetation projection exceeds this
	public int                 max_elevation =     22;   // maximal "elevation" to consider
	public int                 max_elev_terr =      2;   // maximal terrain "elevation" to consider
	public double              max_elev_terr_chg =  0.5;// Maximal terrain elevation change from last successfully used .
	public double              elevation_radius =   1.5; // Radius of elevation/vegetation influence.
	private double             elevation_lpf_transp = 1000; // lpf for transparent areas
	private double             elevation_lpf_pow =    2;    // lpf for transparent areas power
	
	// 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[TVAO_VEGETATION]_alpha), each has 4 ortho neibs, -1 - border, >= 0
	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 int [][]           terr_elev_neibs;   // corresponds to parameters for terrain elevation, for smoothing undefined terrain elevations
	public int [][]           neibs_neibs;
	
	public double             delta=            1e-5; // 7;
	private boolean           from_file =       false;
	public boolean            show_extra = true;  // show extra samples
	public int                debug_index;
	public int                debug_iter;  // LMA iteration for images
	public int                debug_length=200;  // maximal number of debug slices to save
	public double [][]        debug_image;
	public int    []          debug_iters;
	public int    []          debug_series;
	public double [][]        debug_rms;
	public boolean            debug_improved =  true; // last LMA run, from rslt[0];
	public boolean []         debug_improvements;

	public static double []   debug_alpha_scales = {0, 50, 0, 10, 25, 50, 25, 1}; // {100,150};
	public static boolean     debug_show_iter_results = false; // true;
	public String             debug_path = "/media/elphel/SSD3-4GB/lwir16-proc/berdich3/debug/vegetation/lma_um/";
	public boolean            debug_save_improved = true;
	public boolean            debug_save_worsened = true;

	
	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  double            terrain_offset =  Double.NaN; // average offset from the initial
	public boolean            debug_source_vegetation = false;
	public int [][][]         debug_source_veget = null;
	
	public int                cholesky_mode =     0; // 0 - cholesky_block; 1 - cholesky_standard; 2 - cholesky_float; 3  - old 
	
	/**
	 * What WOI to apply to fX items
	 * @param sample_type 
	 * @return Rectangle woi, woi_wer, woi_terr or null if not applicable
	 */
	public Rectangle getSampleWoi(
			int sample_type) {
		switch (sample_type) {
		case SAMPLES_Y:              return null;
		case SAMPLES_Y_HF:           return null;
		case SAMPLES_Y_AVG:          return woi;
		case SAMPLES_SCENES:         return null;
		case SAMPLES_ALPHA_PULL:     return woi_veg;
		case SAMPLES_ALPHA_LPF:      return woi_veg;
		case SAMPLES_TERRAIN_PULL:   return woi_terr;
		case SAMPLES_TERRAIN_LPF:    return woi_terr;
		case SAMPLES_VEGETATION_PULL:return woi_veg;
		case SAMPLES_VEGETATION_LPF: return woi_veg;
		case SAMPLES_ELEVATION_PULL: return woi_veg;
		case SAMPLES_ELEVATION_LPF:  return woi_veg;
		case SAMPLES_TERR_ELEV_PULL: return woi_terr;
		case SAMPLES_TERR_ELEV_LPF:  return woi_terr;
		case SAMPLES_ELEVATION_ALPHA:return woi_veg;
		default: return null;            
		}
	}
	
	/**
	 * What parameter type to apply to fX items (and skip missing)
	 * @param sample_type 
	 * @return parameter typ to use or -1 if not applicable
	 */
	public static int getSamplePar(
			int sample_type) {
		switch (sample_type) {
		case SAMPLES_Y:              return -1;
		case SAMPLES_Y_HF:           return -1;
		case SAMPLES_Y_AVG:          return -1;
		case SAMPLES_SCENES:         return TVAO_SCENE_OFFSET;
		case SAMPLES_ALPHA_PULL:     return TVAO_ALPHA;
		case SAMPLES_ALPHA_LPF:      return TVAO_ALPHA;
		case SAMPLES_TERRAIN_PULL:   return TVAO_TERRAIN;
		case SAMPLES_TERRAIN_LPF:    return TVAO_TERRAIN;
		case SAMPLES_VEGETATION_PULL:return TVAO_VEGETATION;
		case SAMPLES_VEGETATION_LPF: return TVAO_VEGETATION;
		case SAMPLES_ELEVATION_PULL: return TVAO_ELEVATION;
		case SAMPLES_ELEVATION_LPF:  return TVAO_ELEVATION;
		case SAMPLES_TERR_ELEV_PULL: return TVAO_TERR_ELEV_PIX;
		case SAMPLES_TERR_ELEV_LPF:  return TVAO_TERR_ELEV_PIX;
		case SAMPLES_ELEVATION_ALPHA:return TVAO_ELEVATION;
		default: return -1;            
		}
	}
	
	public Rectangle getFull() {
		return full;
	}
	public Rectangle getWoi() {
		return woi;
	}
	public Rectangle getWoiVeg() {
		return woi_veg;
	}
	public VegetationModel getModel() {
		return vegetationModel;
	}
	
	
	public VegetationLMA (
			VegetationModel vegetationModel,
			double    alpha_offs,
			double    alpha_min,
			double    alpha_max,
			double    alpha_sigma,
			int       cholesky_mode,
			int debugLevel) {
		this.cholesky_mode = cholesky_mode;
//		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_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(); // set inside setupInitialTerrainVegetationAlpha()
		tvao[TVAO_SCENE_OFFSET] =     new double [num_scenes];
		setupInitialTerrainVegetationAlpha(
				true,                             // boolean   blur_vegetation,
				alpha_offs,                       // double    alpha_offs,
				alpha_min,                        // double    alpha_min,
				alpha_max,                        // double    alpha_max,
				alpha_sigma,                      // double    alpha_sigma,
				vegetationModel.terrain_filled,   // double [] terrain_filled,
				vegetationModel.vegetation_pull,  // double [] vegetation_extended,
				vegetationModel.vegetation_full,  // double [] vegetation) { // best guess
				vegetationModel.elevations,       //double [] elevation,
				debugLevel);                      // int debugLevel) {
		
		setupLaplacians(0.0); // double weight_diag)
		this.vegetationModel =        vegetationModel; // to access scene names, directories, reference index
		scene_weights = new double [num_scenes];
		scales_xy = getScalesXY(vegetationModel.scale_dirs);
		/*
		int grow_width = 24; // max of woi.width, woi.height plus 2 * max_terr_elev
		// TODO: make it once for tiled (save with VegetationModel
		fillScalesXY(
				scales_xy, // double [][][] scalesXY,
				full.width, // int           width) {
				grow_width, // int           grow_width,
				debugLevel); // int           debugLevel)
        */
		tvao[TVAO_ELEVATION] =  vegetationModel.elevations.clone();
		// 		vegetation_pull =   vegetationModel.vegetation_pull;      // pull LMA to this level
		vegetation_pull =tvao[TVAO_VEGETATION].clone();
		tvao[TVAO_TERR_ELEV] =    new double[1];
		tvao[TVAO_TERR_ELEV][0] = 0.0;

		tvao[TVAO_TERR_ELEV_PIX] = new double[full.width * full.height];
		
		if (debugLevel > -2) {
			String [] titles = {"TVAO_TERRAIN", "TVAO_VEGETATION", "vegetation_pull", "vegetation_full", "elevations", "TVAO_ALPHA"};
			double [][] dbg_img = {tvao[TVAO_TERRAIN],tvao[TVAO_VEGETATION],vegetationModel.vegetation_pull,vegetationModel.vegetation_full,vegetationModel.elevations,tvao[TVAO_ALPHA]};
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					full.width,
					full.height,
					true,
					"ALPHA",
					titles);
		}
		return;
	}
	
	public 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;
	}
	
	public boolean fillScalesWOI(
			final Rectangle woi_in,
			final boolean debug) {
		final Rectangle woi = woi_in.intersection(full); // if woi_in extends beyond full
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicBoolean ahas_null = new AtomicBoolean(false);
		final int woi_length = woi.width*woi.height;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int wIndx = ai.getAndIncrement(); (wIndx < woi_length) && !ahas_null.get(); wIndx = ai.getAndIncrement()){
						int findx = getWoiIndex(
								woi, // Rectangle woi_src,
								full, // Rectangle woi_dst,
								full, // Rectangle woi_full,
								wIndx); // int indx)
						for (int nscene = 0;nscene <scales_xy.length; nscene++) {
							if (scales_xy[nscene][findx] == null) {
								ahas_null.set(true);
								break;
							}
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		if (ahas_null.get()) {
			if (debug) {
				System.out.println("fillScalesWOI(): detected null in scales_xy inside woi="+woi.toString()+", filling missing pairs");
			}
			boolean ok =  fillScalesXY( // FIXME: make it work with woi outside of full!
					scales_xy,  // double [][][] scalesXY,
					full.width, // int           width,
					woi,        // Rectangle     woi,
					false); // debug);     // false); // boolean       debug)
			if (!ok) {
				System.out.println("fillScalesWOI(): ***** FAILED. *****");
				return false;
			}
			if (debug) {
				System.out.println("fillScalesWOI(): DONE.");
			}
		}
		return true; // 
	}
	
	/*
	private static void fillScalesXY(
			double [][][] scalesXY,
			int           width,
			int           grow_width,
			boolean       debug) {
		final int num_pixels = scalesXY[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][] full_scales = new double [2][num_pixels];
		for (int nscene = 0; nscene < scalesXY.length; nscene++) {
			final int nScene = nscene;
			if (debug) {
				System.out.println("fillScalesXY(): filling scalesXY for scene "+nScene);
			}
			Arrays.fill(full_scales[0],Double.NaN);
			Arrays.fill(full_scales[1],Double.NaN);
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < num_pixels; nPix = ai.getAndIncrement()) if (scalesXY[nScene][nPix] != null){
							full_scales[0][nPix] = scalesXY[nScene][nPix][0];
							full_scales[1][nPix] = scalesXY[nScene][nPix][1];
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			for (int i = 0; i < 2; i++) {
				full_scales[i] = TileProcessor.fillNaNs(
						full_scales[i], // final double [] data,
		    			null,            // final boolean [] prohibit,
		    			width,           // int       width,
		    			2 * grow_width,  // 100, // 2*width, // 16,           // final int grow,
		    			0.7,             // double    diagonal_weight, // relative to ortho
		    			100,             // int       num_passes,
		    			0.03);           // final double     max_rchange, //  = 0.01 - does not need to be accurate
			}
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < num_pixels; nPix = ai.getAndIncrement()) if (scalesXY[nScene][nPix] == null){
							if (!Double.isNaN(full_scales[0][nPix]) && !Double.isNaN(full_scales[1][nPix])) {
								scalesXY[nScene][nPix] = new double [] {
										full_scales[0][nPix],
										full_scales[1][nPix]};
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		return;
	}
	*/
	
	
	private static boolean fillScalesXY(
			double [][][] scalesXY,
			int           width,
			Rectangle     woi,
			boolean       debug) {
		final int woi_length = woi.width*woi.height;
		final int grow_width = Math.max(woi.width,woi.height);
		final Rectangle full_woi = new Rectangle (0,0,width,scalesXY[0].length/width);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][] full_scales = new double [2][woi_length];
		final int       num_passes = 20;
		final AtomicBoolean ano_scales = new AtomicBoolean(false);
		for (int nscene = 0; nscene < scalesXY.length; nscene++) {
			final int nScene = nscene;
			if (debug) {
				System.out.println("fillScalesXY(): filling scalesXY for scene "+nScene);
			}
			Arrays.fill(full_scales[0],Double.NaN);
			Arrays.fill(full_scales[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()) {
							int findx = getWoiIndex(
									woi, // Rectangle woi_src,
									full_woi, // Rectangle woi_dst,
									full_woi, // Rectangle woi_full,
									wIndx);   // int indx)
							if (scalesXY[nScene][findx] != null) {
								full_scales[0][wIndx] = scalesXY[nScene][findx][0];
								full_scales[1][wIndx] = scalesXY[nScene][findx][1];
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			for (int i = 0; i < 2; i++) {
				full_scales[i] = TileProcessor.fillNaNs(
						full_scales[i], // final double [] data,
		    			null,            // final boolean [] prohibit,
		    			woi.width,       // int       width,
		    			2 * grow_width,  // 100, // 2*width, // 16,           // final int grow,
		    			0.7,             // double    diagonal_weight, // relative to ortho
		    			num_passes,      // int       num_passes,
		    			0.03);           // final double     max_rchange, //  = 0.01 - does not need to be accurate
			}
			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) && !ano_scales.get(); wIndx = ai.getAndIncrement()) {
							int findx = getWoiIndex(
									woi, // Rectangle woi_src,
									full_woi, // Rectangle woi_dst,
									full_woi, // Rectangle woi_full,
									wIndx);   // int indx)
							if (Double.isNaN(full_scales[0][wIndx]) || Double.isNaN(full_scales[0][wIndx])) {
								ano_scales.set(true);
								if (debug) { 
									System.out.println("fillScalesXY(): BUG:  full_scales[0]["+wIndx+"] ="+full_scales[0][wIndx]+
											", full_scales[1]["+wIndx+"] ="+full_scales[1][wIndx]);
								}
								continue;
//								throw new IllegalArgumentException("fillScalesXY(): BUG:  full_scales[0]["+wIndx+"] ="+full_scales[0][wIndx]+
//										", full_scales[1]["+wIndx+"] ="+full_scales[1][wIndx]);
							}
							scalesXY[nScene][findx] = new double [] {
									full_scales[0][wIndx],
									full_scales[1][wIndx]};
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			if (ano_scales.get()) {
				break;
			}
		}
		if (ano_scales.get()) {
			System.out.println("fillScalesXY(): Missing scales for at least some scenes, can not fill NaNs");
			return false;
		}
		
		if (debug) {
			System.out.println("fillScalesXY(): filling scalesXY DONE.");
		}
		return true;
	}

	 
	
	private static void fillScalesXY1(
			double [][][] scalesXY,
			int           width,
			Rectangle     woi,
			boolean       debug) {
		final int woi_length = woi.width*woi.height;
		final int grow_width = Math.max(woi.width,woi.height);
		final double [][][] scalesXY_woi = new double [scalesXY.length][woi_length][2];
//		final int num_pixels = scalesXY[0].length;
		final Rectangle full_woi = new Rectangle (0,0,width,scalesXY[0].length/width);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][] full_scales = new double [2][woi_length];
		for (int nscene = 0; nscene < scalesXY.length; nscene++) {
			final int nScene = nscene;
			if (debug) {
				System.out.println("fillScalesXY(): filling scalesXY for scene "+nScene);
				Runtime runtime = Runtime.getRuntime();
				runtime.gc();
				System.out.println("----- fillScalesXY() --- Free memory="+runtime.freeMemory()+" (of "+runtime.totalMemory()+")");
			}
			Arrays.fill(full_scales[0],Double.NaN);
			Arrays.fill(full_scales[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()) {
							int findx = getWoiIndex(
									woi, // Rectangle woi_src,
									full_woi, // Rectangle woi_dst,
									full_woi, // Rectangle woi_full,
									wIndx);   // int indx)
							if (scalesXY[nScene][findx] != null) {
								full_scales[0][wIndx] = scalesXY[nScene][findx][0];
								full_scales[1][wIndx] = scalesXY[nScene][findx][1];
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			for (int i = 0; i < 2; i++) {
				full_scales[i] = TileProcessor.fillNaNs(
						full_scales[i], // final double [] data,
		    			null,            // final boolean [] prohibit,
		    			woi.width,       // int       width,
		    			2 * grow_width,  // 100, // 2*width, // 16,           // final int grow,
		    			0.7,             // double    diagonal_weight, // relative to ortho
		    			100,             // int       num_passes,
		    			0.03);           // final double     max_rchange, //  = 0.01 - does not need to be accurate
			}
			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()) {
							/*
							int findx = getWoiIndex(
									woi, // Rectangle woi_src,
									full_woi, // Rectangle woi_dst,
									full_woi, // Rectangle woi_full,
									wIndx);   // int indx)
							*/
							if (Double.isNaN(full_scales[0][wIndx]) || Double.isNaN(full_scales[0][wIndx])) {
								throw new IllegalArgumentException("fillScalesXY(): BUG:  full_scales[0]["+wIndx+"] ="+full_scales[0][wIndx]+
										", full_scales[1]["+wIndx+"] ="+full_scales[1][wIndx]);
							}
							/*
							scalesXY[nScene][findx] = new double [] {
									full_scales[0][wIndx],
									full_scales[1][wIndx]};
							*/
							scalesXY_woi[nScene][wIndx][0] = full_scales[0][wIndx];
							scalesXY_woi[nScene][wIndx][1] = full_scales[1][wIndx];
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		if (debug) {
			System.out.println("fillScalesXY(): filling scalesXY_woi DONE.");
		}
		
		for (int nscene = 0; nscene < scalesXY.length; nscene++) {
			final int nScene = nscene;
			if (debug) {
				System.out.println("fillScalesXY(): copying scalesXY_woi->scalesXY for scene "+nScene);
			}
			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()) {
							int findx = getWoiIndex(
									woi, // Rectangle woi_src,
									full_woi, // Rectangle woi_dst,
									full_woi, // Rectangle woi_full,
									wIndx);   // int indx)
							scalesXY[nScene][findx] = scalesXY_woi[nScene][wIndx]; 
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			
		}
		
		if (debug) {
			System.out.println("fillScalesXY(): filling scalesXY DONE.");
		}
		
		return;
	}
	
	
	
	
	
	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(
			boolean   blur_vegetation,
			double    alpha_offs,
			double    alpha_min,
			double    alpha_max,
			double    alpha_sigma,
			double [] terrain_filled,
			double [] vegetation_extended,
			double [] vegetation, // best guess
			double [] elevation,
			int debugLevel) {
		int dbg_pix = 90337;
//		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];
		double [] vegetation_blur = vegetation_extended.clone(); // .clone();
    	double scale_sigma = 5.0; // non-NaN - 5 pixels from original NaN for each sigma
    	int grow = 2* Math.min((int) Math.ceil(alpha_sigma * scale_sigma), full.width);
    	vegetation_blur = TileProcessor.fillNaNs(
    			vegetation_blur, // final double [] data,
    			null,            // final boolean [] prohibit,
    			full.width,      // int       width,
    			// CAREFUL ! Remaining NaN is grown by unsharp mask filter ************* !
    			grow,            // 100, // 2*width, // 16,           // final int grow,
    			0.7,             // double    diagonal_weight, // relative to ortho
    			100,             // int       num_passes,
    			0.03);           // final double     max_rchange, //  = 0.01 - does not need to be accurate
    	(new DoubleGaussianBlur()).blurDouble(
    			vegetation_blur, //
    			full.width,
    			full.height,
    			alpha_sigma,     // double sigmaX,
    			alpha_sigma,     // double sigmaY,
    			0.01);           // double accuracy)
		
		for (int npix = 0; npix < image_length; npix++) {
			if (npix==dbg_pix) {
				System.out.println("setupInitialTerrainVegetationAlpha(): npix="+npix);
			}
			double t =  terrain_filled[npix];
			double v =  vegetation[npix];
//			double vp = vegetation_extended[npix];
			double bp = vegetation_blur[npix];
			double ep = elevation[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(bp) && !Double.isNaN(ep)) {
				if ((v > t) && (bp > t)) {
					double f = (v-t)/(bp-t);
					if (f > alpha_min) {
						if ( f > alpha_max) {
							a = 1.0;
						} else {
							a = (f-alpha_min) / (alpha_max - alpha_min);
						}
					}
				}
			}
			a = alpha_offs + (1 - 2 * alpha_offs) * a;
			tvao[TVAO_ALPHA][npix] = a;
			if (blur_vegetation) {
				tvao[TVAO_VEGETATION][npix] = bp;
			}
		}
		return;
	}
	
	
	public int prepareLMA(
			final boolean            keep_parameters,
			final Rectangle          woi_in,
			final Rectangle          woi_veg_in, // used when loading from file (may be different)
			final Rectangle          woi_terr_in, // used when loading from file (may be different)
			final double             max_warp, // 1.8 - do not use scenes where distance between vegetation projection exceeds this
			final int                max_elevation,               // maximal "elevation" to consider
			final int                max_elev_terr,               // maximal terrain "elevation" to consider
			final double             max_elev_terr_chg, // 0.5 maximal terrain elevation change from last successfully used 
			final double             elevation_radius, //Radius of elevation/vegetation influence.
			final double             terr_elev_radius, //  = 1.5; 
			final double             elev_radius_extra, //  =  1.2;      // scale both radii when setupElevationLMA(), setupTerrainElevationLMA(), and setupTerrainElevationPixLMA() 
			final boolean [][]       valid_scene_pix_in, // may have nulls or be shorter (do not update "overlaid")     
			final double             hifreq_weight,  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
			final double             terrain_correction,
			final boolean            fit_terr,
			final boolean            fit_veget,
			final boolean            fit_alpha,
			final boolean            fit_scenes,
			final boolean            fit_elevations,
			final boolean            fit_terr_elev,
			final boolean            fit_terr_elev_pix,
			final boolean []         fits_disable,
			final boolean []         fits_disable_terronly,
			final double             reg_weights,    // fraction of the total weight used for regularization
			final double             alpha_loss,     // alpha quadratic growing loss for when out of [0,1] range
			final double             alpha_loss_lin, // alpha linear growing loss for when out of [0,1] range and below minimal vegetation alpha
			final double             alpha_offset,   // quadratic loss when alpha > 1 - alpha_offset
			final double             alpha_0offset,  // quadratic loss when alpha < alpha0_offset
			final double             alpha_min_veg, // 0.5; // if (alpha-alpha_offset)/(1-2*alpha_offset) < alpha_min_veg, pull down to lpha_offset
			final double             alpha_max_terrain, // 0.75; // () increase pull vegetation if below
			final double             alpha_pull_pwr,    // 1.0;  // () raise extra pull to that power
			final double             alpha_lpf,      // pull vegetation alpha to average of 4 neighbors
			final double             alpha_lpf_border, //  pull to average of 4 neighbors for border tiles (to keep stable)
			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             alpha_diff_hole, // 0.01; // Minimal alpha difference between min and max neighbor to be considered a hole
			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_elev_lpf,
			final double             terr_pull0,     // pull terrain to initial (pre-adjustment) values
			final double             terr_pull_up,   // Terrain pixels pull to initial (pre-adjustment) values when it is colder than initial.
			final double             terr_pull_avg,  // pull terrain to the initial offset by the average offset of all terrain pixels  
			final double             veget_pull0,    // pull vegetation to initial (pre-adjustment) values
			final double             veget_pull_low_alpha, //  10; // ()scale pull0 for low alpha (mostly terrain) 
			final double             elev_pull0,     // pull elevation to initial (pre-adjustment) values
			final double             terr_elev_pull0, // pull terrain elevation to segment average
			final boolean            elev_alpha_en,  // false; // Enable loss for low vegetation with high opacity
			final double             elev_alpha,     // 1.0;   // multiply alpha by under-low elevation for loss
			final double             elev_alpha_pwr, // 2.0;  // raise alpha to this power (when alpha > 0)
			final double             low_veget,      // 2.0;   // (pix) Elevation considered low (lower loss for high alpha)
			final double             scenes_pull0,
 			final double             elev_scale_thresh, // 1.0;// reduce losses for higher (initial) elevations TODO: consider actual elevations
			final boolean            elev_scale_pull,// false; // scale elevation pull losses for high elevations 
			final boolean            elev_scale_lpf, // false; // scale elevation diffusion losses for high elevations 
			final boolean            ttop_en,        // false; // remove tree tops from transparency weights
			final double             ttop_gb,        // 1.0;   // Elevation Gaussian blur sigma to detect tree tops
			final double             ttop_min,       // 3.0;   // Minimal tree top elevation
			final double             ttop_rel_lev,   // 0.9;   // Relative (to the top height) sample level
			final double             ttop_rel_rad,   // 0.25;  // Relative (to the top height) sample ring radius
			final double             ttop_frac,      // 0.5;   // Minimal fraction of the ring pixels below sample level
			final double             ttop_rem_rad,   // 0.25;  // Relative (to the top height) remove transparency radius
///			final boolean            terr_only_special,//true; // special sequences for terrain-only tiles
///			final boolean            terr_only_pix,    //true; // force per-pixel terrain elevation in terrain-only mode, overwrite fits_disable[TVAO_TERR_ELEV_PIX]
			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 int                debugLevel,
			final String             debug_path,
			final boolean            debug_save_improved, // Save debug image after successful LMA step."); 
			final boolean            debug_save_worsened) { // Save debug image after unsuccessful LMA step.");
		this.woi =               woi_in;
		this.hifreq_weight =     hifreq_weight;  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
		this.terrain_correction = terrain_correction;
//		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;
		fits[TVAO_TERR_ELEV] =     fit_terr_elev || fit_terr_elev_pix; // always both
		fits[TVAO_TERR_ELEV_PIX] = fit_terr_elev || fit_terr_elev_pix;
		this.fits_disable =         fits_disable.clone();
		this.fits_disable_terronly =  (fits_disable_terronly != null)? fits_disable_terronly.clone() : null;
		// TODO: modify fits_disable with setter only ? 
		this.fits_disable[TVAO_TERR_ELEV] |= !(this.fits_disable[TVAO_TERR_ELEV_PIX] && fits[TVAO_TERR_ELEV_PIX]); // can not be enabled simultaneously
		if (this.fits_disable_terronly!= null) {
			this.fits_disable_terronly[TVAO_TERR_ELEV] |= !(this.fits_disable_terronly[TVAO_TERR_ELEV_PIX] && fits[TVAO_TERR_ELEV_PIX]); // can not be enabled simultaneously
		}
		this.reg_weights =       reg_weights;    // fraction of the total weight used for regularization
		this.alpha_loss =        alpha_loss;
		this.alpha_loss_lin =    alpha_loss_lin;
		this.alpha_offset =      alpha_offset;
		this.alpha_0offset =     alpha_0offset;
		this.alpha_min_veg =     Math.max(alpha_min_veg,0);
		this.alpha_max_terrain = Math.max(alpha_max_terrain,0);
		this.alpha_pull_pwr =    alpha_pull_pwr;
		this.alpha_lpf =         alpha_lpf;
		this.alpha_lpf_border =  alpha_lpf_border;
		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_elev_lpf =     terr_elev_lpf;
		this.terr_pull0 =        terr_pull0;
		this.terr_pull_up=       (terr_pull_up > 0) ? terr_pull_up : terr_pull0;
		this.terr_pull_avg =     terr_pull_avg;
		this.veget_pull0 =       veget_pull0;
		this.veget_pull_low_alpha = veget_pull_low_alpha;
		this.elevation_pull0 =   elev_pull0;
		this.terr_elev_pull0 =   terr_elev_pull0;
	    this.elev_alpha_en =     elev_alpha_en;
	    this.elev_alpha =        elev_alpha; 
	    this.elev_alpha_pwr =    elev_alpha_pwr;
	    this.low_veget =         low_veget;    
		this.scenes_pull0 =      scenes_pull0;
		
		this.elev_scale_thresh = elev_scale_thresh;
		this.elev_scale_pull =   elev_scale_pull;
		this.elev_scale_lpf =    elev_scale_lpf;
		this.ttop_en =           ttop_en;
		this.ttop_gb =           ttop_gb;
		this.ttop_min =          ttop_min;
		this.ttop_rel_lev =      ttop_rel_lev;
		this.ttop_rel_rad =      ttop_rel_rad;
		this.ttop_frac =         ttop_frac;
		this.ttop_rem_rad =      ttop_rem_rad;		
///		this.terr_only_special = terr_only_special;		
///		this.terr_only_pix =     terr_only_pix;		
		this.boost_parallax =    boost_parallax;
		this.max_parallax =      max_parallax; // parallax limit when evaluating boost parallax
		this.max_warp =          max_warp;
		this.max_elevation =     max_elevation;
		this.max_elev_terr =     max_elev_terr;
		this.max_elev_terr_chg = max_elev_terr_chg;
		this.elevation_radius =  elevation_radius;
		if ((debug_path != null) && (debug_path.length() > 0)) {
			this.debug_path = debug_path;
		}
		this.terr_elev_radius =   terr_elev_radius;
		this.elev_radius_extra =  elev_radius_extra;
		this.debug_save_improved = debug_save_improved;
		this.debug_save_worsened = debug_save_worsened;
		this.elev_sum_weights = null;  // calculate elevations on next getFxDErivsrun

		
//		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, elevation_radius); // 5);
		
		Rectangle woi_terr_ext = expandWoi(woi,max_elev_terr);
		boolean ok = fillScalesWOI(
				woi_terr_ext,       // final Rectangle woi,
				(debugLevel > -2)); // final boolean debug)
		if (!ok) {
			return -1;
		}
		// 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 (!vegetationModel.tile_woi && (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)
///		this.woi_veg =  (woi_veg_in  != null)? woi_veg_in :  new Rectangle();
		/// when restoring, woi_veg_in may be null while woi_terr != null. It means that there is no vegetation saved, only terrain
		this.woi_veg =  (woi_terr_in  != null)? woi_veg_in :  new Rectangle(); 
		this.woi_terr = (woi_terr_in != null)? woi_terr_in : new Rectangle();
		int        max_sacrifice = 5;
		boolean [] valid_pixels_in = null;
		int []     valid_stats = new int[7];
		int []     max_warp_scene_pix = new int[2];
		// woi_veg_in
		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_elevation,      // final int          max_offset,
				max_elev_terr,      // final int          max_elev_terr,
				elev_radius,        // final double []    elev_radius, // this.elevation_radius
				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
				this.woi_terr);     // final Rectangle    woi_terr) // should be initialized, will be modified
		if (valid_scene_pix == null) {
			System.out.println("prepareLMA(): ***** getValidScenesPixels() FAILED *****");
			return -1;
		}
		if (valid_scene_pix_in != null) {
			String [] names = {"scenes","woi pixels","vegetation pixels"};
			boolean mismatch = false;
			// TODO: Compare and notify if different
			for (int i = 0; i < names.length; i++) {
				boolean mismatch_this = false;
				if ((i >= valid_scene_pix_in.length ) || (valid_scene_pix_in[i] == null)) {
					mismatch = true;
					continue;
				}
				int num_gen=0, num_restore = 0;
				for (int n = 0; n < Math.min(valid_scene_pix[i].length,valid_scene_pix_in[i].length); n++) {
					if (valid_scene_pix[i][n]!=valid_scene_pix_in[i][n]) {
						mismatch_this=true;
					}
					if (valid_scene_pix[i][n]) num_gen++;
					if (valid_scene_pix_in[i][n]) num_restore++;
				}
				if (mismatch_this) {
					System.out.println("Restored valid "+names[i]+" do not match generated ones, number of restored = "+num_restore+
							", number of generated = "+num_gen);
				}
				mismatch |= mismatch_this;
			}
			if (mismatch) {
				System.out.println("Using restored valid scenes and pixels ('overlaid' is always generated)");
				for (int i = 0; i < valid_scene_pix_in.length; i++) {
					if (valid_scene_pix_in[i] != null) {
						valid_scene_pix[i]=valid_scene_pix_in[i];
					}
				}
			}
		}
		valid_scenes =     valid_scene_pix[0]; // [num_scenes]
		valid_terr_proj =  valid_scene_pix[1]; // [woi.width*woi.height] valid terrain and y_vector pixels
		valid_vegetation = valid_scene_pix[2]; // [woi_veg.width*woi_veg.height] valid vegetation pixels
		has_vegetation =   valid_scene_pix[3]; // [woi.width*woi.height] this valid terrain pixel is overlaid by vegetation (in all valid scenes) 
		valid_terrain =    valid_scene_pix[4]; // [woi_terr.width*woi_terr.height] valid terrain pixels
		has_terrain =      valid_scene_pix[5]; // [woi.width*woi.height] has terrain source pixels
		if (valid_vegetation == null) {
			woi_veg = null;
		}
		woi_max = new Rectangle (woi);
		if (woi_veg != null) { // here - never, as it will be (0,0,0,0)
			woi_max.add(woi_veg);
		}
		woi_max.add(woi_terr);
		
		
		if (woi_veg == null) {
			if (debugLevel > -3) {
				System.out.println("prepareLMA() as there is no vegitation (woi_veg==null),"+
			" fitting for VEGETATION,ALPHA, and ELEVATION is disabled");
				fits[TVAO_VEGETATION] = false;
				fits[TVAO_ALPHA] =      false;
				fits[TVAO_ELEVATION] =  false;
			}
			if (fits_disable_terronly!= null) {
				for (int i = 0; i < this.fits_disable.length; i++) {
					this.fits_disable[i] = fits_disable_terronly[i];
				}
				if (debugLevel > -3) {
					System.out.println("Using fits="+IntersceneMatchParameters.booleansToString(this.fits,2)+
							" fits_disable = "+IntersceneMatchParameters.booleansToString(this.fits_disable,2));
				}
			}
			/*
			if (terr_only_special) {
				fits_disable[TVAO_TERR_ELEV] =     terr_only_pix;
				fits_disable[TVAO_TERR_ELEV_PIX] = !terr_only_pix;
			}
			*/
		}
		
		
		if (debugLevel > -2) {
			String s = (this.woi_veg == null) ? " no valid vegetation\n" : valid_stats[2] + " valid vegetation pixels (of "+(this.woi_veg.width*this.woi_veg.height)+")\n";
			System.out.println("Got\t"+valid_stats[0]+" valid scenes (of "+this.terrain_rendered.length+"),\n"+
					valid_stats[1] + " valid WOI pixels (of "+(this.woi.width*this.woi.height)+"), of them "+
					valid_stats[3] + " have vegetation influence, "+valid_stats[5]+" have terrain influence,\n"+
					s + // valid_stats[2] + " valid vegetation pixels (of "+(this.woi_veg.width*this.woi_veg.height)+")\n"+
					valid_stats[4] + " valid terrain pixels    (of "+(this.woi_terr.width*this.woi_terr.height)+").\n"+
					"Removed "+valid_stats[6]+" 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 (!vegetationModel.tile_woi && (debugLevel > -2)) { // 3) {
			double [] bool_min_max = {0.0, 5.0}; 
			int woi_max_length = woi_max.width * woi_max.height;
			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 = {"woi", "woi_veg", "woi_terr", "terr_proj", "vegetation", "has_veg", "terrain", "has_terr", "elevation","first_dx","first_dy","first_warp","last_dx","last_dy","last_warp"}; 
			double [][] dbg_img = new double [titles.length][woi_max_length];
			for (int i = 0; i < dbg_img.length; i++) {
				Arrays.fill(dbg_img[i], Double.NaN);
			}
			for (int wm_indx = 0; wm_indx < woi_max_length; wm_indx++) {
				int findx = getWoiIndex(
						woi_max,  // Rectangle woi_src,
						full,     // Rectangle woi_dst,
						full,     // Rectangle woi_full,
						wm_indx); // int indx)
				int windx = getWoiIndex(
						woi_max,  // Rectangle woi_src,
						woi,      // Rectangle woi_dst,
						full,     // Rectangle woi_full,
						wm_indx); // int indx)
				int wvindx = (woi_veg == null) ? -1 : getWoiIndex(
						woi_max,  // Rectangle woi_src,
						woi_veg,  // Rectangle woi_dst,
						full,     // Rectangle woi_full,
						wm_indx); // int indx)
				int wtindx = getWoiIndex(
						woi_max,  // Rectangle woi_src,
						woi_terr, // Rectangle woi_dst,
						full,     // Rectangle woi_full,
						wm_indx); // int indx)
///				int [] fxy =  woiXY(full,     findx);
				int [] wxy =  woiXY(woi,      windx);
				int [] wvxy = (woi_veg == null) ? null : woiXY(woi_veg,  wvindx);
				int [] wtxy = woiXY(woi_terr, wtindx);
				
				dbg_img[0][wm_indx] = (wxy !=  null) ? bool_min_max[1]:bool_min_max[0]; // "woi"
				dbg_img[1][wm_indx] = (wvxy != null) ? bool_min_max[1]:bool_min_max[0]; // "woi_veg"
				dbg_img[2][wm_indx] = (wtxy != null) ? bool_min_max[1]:bool_min_max[0]; // "woi_terr"
				
				if (windx >=  0)  dbg_img[3][wm_indx] =  valid_terr_proj [windx]?  bool_min_max[1] : bool_min_max[0]; // "terr_proj",
				if (wvindx >= 0)  dbg_img[4][wm_indx] =  (valid_vegetation==null) ? Double.NaN: (valid_vegetation[wvindx]? bool_min_max[1] : bool_min_max[0]); // "vegetation"
				if (windx >=  0)  dbg_img[5][wm_indx] =  has_vegetation  [windx]?  bool_min_max[1] : bool_min_max[0]; // "has_veg",
				if (wtindx >= 0)  dbg_img[6][wm_indx] =  valid_terrain   [wtindx]? bool_min_max[1] : bool_min_max[0]; // "terrain"
				if (windx >=  0)  dbg_img[7][wm_indx] =  has_terrain     [windx]?  bool_min_max[1] : bool_min_max[0]; // "has_terr",
				double [] scales_first =  scales_xy[first_scene][findx];
				double [] scales_last =   scales_xy[last_scene][findx];
				double [] warps_first =   warps[first_scene][findx];
				double [] warps_last =    warps[last_scene][findx];
				double elevation =        tvao[TVAO_ELEVATION][findx];
				dbg_img[8][wm_indx] =     elevation;
				if (scales_first != null) {
					double dx_first =      scales_first[0] * elevation;
					double dy_first =      scales_first[1] * elevation;
					dbg_img[ 9][wm_indx] = dx_first;     
					dbg_img[10][wm_indx] = dy_first;
				}
				if (warps_first != null) {
					dbg_img[11][wm_indx] = warps_first[0];
				}

				if (scales_last != null) {
					double dx_last =  scales_last[0] *  elevation;
					double dy_last =  scales_last[1] *  elevation;
					dbg_img[12][wm_indx] = dx_last;     
					dbg_img[13][wm_indx] = dy_last;
				}
				if (warps_last != null) {
					dbg_img[14][wm_indx] = warps_last[0];
				}
			}
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					woi_max.width,
					woi_max.height,
					true,
					"woi_max_uses_"+woi_max.x+"-"+woi_max.y+"-"+woi_max.width+"-"+woi_max.height,
					titles);
		}

		double [] scene_weights0 = //sets this.scene_weights too
				setupSceneWeights(
				boost_parallax, // double boost_parallax)
				woi,            // Rectangle woi)
				max_parallax); // final double ceil_parallax)
		System.arraycopy( // this.scene_weights is final, so copy
				scene_weights0,
				0,
				this.scene_weights,
				0,
				num_scenes);
		setupParametersIndices(
				fits,                 // boolean [] fits,
				debugLevel);          // final int debugLevel)
		
		alpha_neibs = (woi_veg == null) ? null :getNeighbors(
				TVAO_ALPHA,                   // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars[TVAO_ALPHA],         // final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
				num_pars[TVAO_ALPHA]);        // final int num_samples);  // num_pars[TVAO_VEGETATION]_alpha
		terr_neibs = getNeighbors(
				TVAO_TERRAIN,                 // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars[TVAO_TERRAIN],       // final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
				num_pars[TVAO_TERRAIN]);      // final int num_samples);  // num_pars[TVAO_VEGETATION]_alpha
		veget_neibs = (woi_veg == null) ? null :getNeighbors(
				TVAO_VEGETATION,              // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars[TVAO_VEGETATION],    // final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
				num_pars[TVAO_VEGETATION]);   // final int num_samples);  // num_pars[TVAO_VEGETATION]_alpha
		elevation_neibs = (woi_veg == null) ? null :getNeighbors(
				TVAO_ELEVATION,               // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars[TVAO_ELEVATION],     // final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
				num_pars[TVAO_ELEVATION]);    // final int num_samples);  // num_pars[TVAO_VEGETATION]_alpha
		terr_elev_neibs = (woi_terr == null) ? null :getNeighbors(
				TVAO_TERR_ELEV_PIX,           // final int tvao,        // TVAO_VEGETATION_ALPHA
				ind_pars[TVAO_TERR_ELEV_PIX], // final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
				num_pars[TVAO_TERR_ELEV_PIX]);// final int num_samples);  // num_pars[TVAO_VEGETATION]_alpha

		neibs_neibs = new int [par_rindex.length][];
		addSecondNeighbors(
				neibs_neibs,                      // final int [][] second_neibs,
				terr_neibs,                       // final int [][] par_neibs,
				ind_pars[TVAO_TERRAIN]);          // final int      start_index)
		if (woi_veg != null) {
			addSecondNeighbors(
					neibs_neibs,                  // final int [][] second_neibs,
					alpha_neibs,                  // final int [][] par_neibs,
					ind_pars[TVAO_ALPHA]);        // final int      start_index)
			addSecondNeighbors(
					neibs_neibs,                  // final int [][] second_neibs,
					veget_neibs,                  // final int [][] par_neibs,
					ind_pars[TVAO_VEGETATION]);   // final int      start_index)
			addSecondNeighbors(
					neibs_neibs,                  // final int [][] second_neibs,
					elevation_neibs,              // final int [][] par_neibs,
					ind_pars[TVAO_ELEVATION]);    // final int      start_index)
		}
		if (woi_terr != null) {
			addSecondNeighbors(
					neibs_neibs,                  // final int [][] second_neibs,
					terr_elev_neibs,              // final int [][] par_neibs,
					ind_pars[TVAO_TERR_ELEV_PIX]);// final int      start_index)
			
		}
		if (debugLevel > 4) {
			showPivot(
					neibs_neibs,         // int [][] neibs_neibs,
					"neibs_neibs.tiff"); // String title)
		}
		
		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;

		boolean use_terr_corr =        (terrain_correction>=0);  // maybe not needed   
		setupYVector(
				use_terr_corr, // boolean use_terr_corr); //(hifreq_weight > 0)); // boolean use_hf);
				scene_weights); // 
		setupWeights( // after setupParametersIndices
				scene_weights,
				reg_weights,     // final double reg_weights,
				hifreq_weight,   // final double hifreq_weight );
				debugLevel); // final int    debugLevel) {
				
		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[scales_xy.length];
		if (boost_parallax == 0) {
			for (int nscene = 0; nscene < scales_xy.length; nscene++) {
				scene_weights[nscene] = 1.0;
			}
		} else {
			final double [] max_parallax2 = new double [scales_xy.length];
			double super_max_parallax2 = 0;
			for (int nscene = 0; nscene < scales_xy.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 =  scales_xy[nscene][indx];
						if (d != null) {
							double d2 = Math.min(ceil_parallax2, d[0]*d[0] + d[1]*d[1]);
							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 < scales_xy.length; nscene++) {
				scene_weights[nscene] = 1.0+(boost_parallax - 1) * Math.sqrt(max_parallax2[nscene]/super_max_parallax2);
			}
		}
		return scene_weights;
	}
	
	private double [] setupSceneWeightsOld(
			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("getYminusFxWeighted(): wd=NaN, y_vector["+n+"]="+y_vector[n]+", fx["+n+"]="+fx[n]+", weights["+n+"]="+weights[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 double[][] yMinusFxSummary(
			final double []   fx,
			final boolean show) {
		final double [][] stats = new double [samples_pointers.length][2];
		final Thread[]      threads =     ImageDtt.newThreadArray();
		final AtomicInteger ai =          new AtomicInteger(0);
		final AtomicInteger ati =         new AtomicInteger(0);
		for (int n = 0; n < SAMPLES_TOTAL; n++) {
			final int nstat = n;
			final int nstat_start =  samples_pointers[nstat][0];
			final int nstat_length = samples_pointers[nstat][1];
			if (nstat_length > 0) {
				ai.set(0);
				ati.set(0);
				final double [][] stats_threaded = new double [threads.length][2];
				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 < nstat_length; n = ai.getAndIncrement()) {
								int indx = nstat_start + n;
								double y = (indx < y_vector.length) ? y_vector[indx] : 0;
								double d = y - fx[indx]; // - fx[n]; // +y_vector[i]
								double w = weights[indx];
								double wd = d * w;
								if (Double.isNaN(wd)) {
									System.out.println("yMinusFxSummary(): wd=NaN, y_vector["+n+"]="+y_vector[n]+", fx["+n+"]="+fx[n]+", weights["+n+"]="+weights[n]);
								}
								stats_threaded[nthread][0] += wd * d;
								stats_threaded[nthread][1] += w;
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
				for (int nthread = 0; nthread < stats_threaded.length; nthread++) {
					stats[nstat][0] += stats_threaded[nthread][0];
					stats[nstat][1] += stats_threaded[nthread][1];
				}
			}
		}
		for (int nstat = 0; nstat < SAMPLES_TOTAL; nstat++)  {
			stats[SAMPLES_TOTAL][0] += stats[nstat][0];
			stats[SAMPLES_TOTAL][1] += stats[nstat][1];
		}
		
		if (show) {
			System.out.println(String.format("%2s, %24s, %12s, %12s, %12s, %7s, %7s, %7s", "#", "name", "RMS_avg", "RMS_contrib", "weight", "start","end","length"));
			for (int nstat = 0; nstat < stats.length; nstat++) if (stats[nstat][1] > 0) {
				int istart =   samples_pointers[nstat][0];
				int ilength =  samples_pointers[nstat][1];
				int iend = istart+ilength-1;
				double rms_avg =     Math.sqrt(stats[nstat][0]/stats[nstat][1]);
				double rms_contrib = Math.sqrt(stats[nstat][0] * stats[nstat][1]);
				double weight = stats[nstat][1];
				System.out.println(String.format("%2d, %24s, %12.8f, %12.8f, %12.8f, %7d, %7d, %7d",
						nstat, SAMPLES_NAMES[nstat],
						rms_avg, rms_contrib, weight,
						istart, iend, ilength));
			}			
		}
		return stats;
	}
	
	private int getIndxTerrElev() {
		return (num_pars[TVAO_TERR_ELEV] > 0) ? ind_pars[TVAO_TERR_ELEV] : -1; // index of terrain elevation or -1
	}
	private int getIndxTerrElevPix() {
		return (num_pars[TVAO_TERR_ELEV_PIX] > 0) ? ind_pars[TVAO_TERR_ELEV_PIX] : -1; // index of terrain elevation pix or -1
	}
	private int getNumTerrElevPix() {
		return num_pars[TVAO_TERR_ELEV_PIX];
	}
	
	/**
	 * Extract average of TVAO_TERR_ELEV_PIX parameters to TVAO_TERR_ELEV if
	 * it is not disabled
	 */
	public void preRunLma() {
		int par_indx_terr_elev = getIndxTerrElev();        // index of terrain elevation or -1
		int par_indx_terr_elev_pix = getIndxTerrElevPix(); // index of terrain elevation pix or -1
		if ((par_indx_terr_elev >= 0) && (par_indx_terr_elev_pix >= 0) && !fits_disable[TVAO_TERR_ELEV]) {
			double s = 0;
			for (int i = 0; i < num_pars[TVAO_TERR_ELEV_PIX]; i++) {
				s += parameters_vector[par_indx_terr_elev_pix+i];
			}
			s /= num_pars[TVAO_TERR_ELEV_PIX];
			parameters_vector[par_indx_terr_elev] = s;
			for (int i = 0; i < num_pars[TVAO_TERR_ELEV_PIX]; i++) {
				parameters_vector[par_indx_terr_elev_pix+i] -= s;
			}
		}
		return;
	}
	
	/**
	 * Add TVAO_TERR_ELEV parameter to all of TVAO_TERR_ELEV_PIX parameters if TVAO_TERR_ELEV is not disabled
	 */
	public void postRunLma() {
		int par_indx_terr_elev = getIndxTerrElev();        // index of terrain elevation or -1
		int par_indx_terr_elev_pix = getIndxTerrElevPix(); // index of terrain elevation pix or -1
		if ((par_indx_terr_elev >= 0) && (par_indx_terr_elev_pix >= 0) && !fits_disable[TVAO_TERR_ELEV]) {
			double s = parameters_vector[par_indx_terr_elev];
			for (int i = 0; i < num_pars[TVAO_TERR_ELEV_PIX]; i++) {
				parameters_vector[par_indx_terr_elev_pix+i] += s;
			}
			parameters_vector[par_indx_terr_elev] = 0;
		}
		return;
	}
	
	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)
	{
		preRunLma();
		boolean [] rslt = {false,false};
		this.last_rms = null; // remove?
		int iter = 0;
		if (debug_level > -2) { // 1) {
			System.out.println((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime()))+
					" Starting LMA");
		}

		if (dbg_prefix != null) {
//			 debugStateImage(dbg_prefix+"-initial");
		}
		for (iter = 0; iter < num_iter; iter++) {
			if ((vegetationModel.SYNC_COMMAND != null) && (vegetationModel.SYNC_COMMAND.stopRequested.get()>0)) {
				int stop_requested = vegetationModel.SYNC_COMMAND.stopRequested.getAndSet(0);
//				IJ.getNumber("Any number", 0);
				if (stop_requested > 1) {
					String msg= "iter_step="+iter+", num_iter=num_iter. Exit iteretions?";
					boolean ok_break= IJ.showMessageWithCancel("Stop Requested", msg); 
					System.out.println(">>> Stop Requested : ok_break="+ok_break);
					break;
				} else if (stop_requested > 0) {
					String msg= "iter_step="+iter+", num_iter=num_iter. Continue";
					IJ.showMessageWithCancel("Pause Requested", msg); 
					System.out.println(">>> Pause Requested, (in debugger only - put a breakpoint here. Use ABORT button to exit.");
					System.out.println();
					stop_requested = vegetationModel.SYNC_COMMAND.stopRequested.getAndSet(0);
					if (stop_requested > 1) {
						break;
					}
				}
			}
			debug_iter = iter; // for images
			rslt =  lmaStep(
					lambda,
					rms_diff,
					debug_level);
			if (dbg_prefix != null) {
//				 debugStateImage(dbg_prefix+"-step_"+iter);
			}
			
			if (rslt == null) {
				postRunLma();
				return -1; // false; // need to check
			}
			if (debug_level > -2) { // 1) {
				String lma_series = "";
				if (vegetationModel != null) {
					lma_series = " Series " + vegetationModel.step_restore;
				}
						
				System.out.println((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime()))+
						lma_series + " LMA step"+String.format("%3d (of%3d)",iter,num_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;
				boolean show_YfX = debug_show_iter_results; // (debug_level < 10);
				if (show_YfX) {
					String YfX_title = vegetationModel.reference_scene+"-iter"+iter;
					showYfX(
							null, // double [] vector,
							YfX_title); // String title)
				}
				
			} 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 > -2)) {
			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();
		}
		if (debug_level > -2) {
			double [] fx = getFxDerivs(
					parameters_vector, // double []         vector,
					null,           // final double [][] jt, // should be null or initialized with [vector.length][]
					debug_level);      // final int         debug_level)
			yMinusFxSummary(     
					fx, // final double []   fx,
					true); // final boolean show)
		}
		postRunLma();
		return rslt[0]? iter : -1;
	}

	private boolean [] lmaStep(
			double lambda,
			double rms_diff,
			int debug_level) {
		boolean [] rslt = {false,false};
		int par_indx_terr_elev = getIndxTerrElev(); // index of terrain elevation or -1 
		// 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 ((par_indx_terr_elev >=0) && !Double.isNaN(terr_elev_last)) {
				terr_elev_last_success = terr_elev_last;
///				terr_elev_initial =     terr_elev_last;
			}
			if (debug_level > -1) {
				yMinusFxSummary(     
						fx, // final double []   fx,
						true); // final boolean show)
			}
		
			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(
						parameters_vector, // double [] vector
						last_ymfx, // double [] ymfx,
						last_rms,  // double [] rms,
						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 > 4) && (samples_pointers[SAMPLES_Y_AVG][1] > 0)){
					String dbg_title = "terrain_corr_fX-INITIAL";
					debugTerrCorr(
							fx,         // double [] fx,
							dbg_title); // String title)
				}
			}
		}
		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,
					show_extra ? last_ymfx : null,        // double [] ymfx,
					1,                 // int       gap)
					wh,
					null); // String    title)
			ShowDoubleFloatArrays.showArrays(
					debug_image,
					wh[0],
					wh[1],
					"parameters.vector-"+debug_index);
		}
		
		boolean decimate = disabledParams(); // at least one parameter is
		int [][] par_map = decimate ? getParsAllDecimatedAllIndices() : null;
		double [][] last_jt_decimated = decimateJt( // will not decimate if par_map == null
				par_map,       // final int    [][] par_map, // // only [1] used
				this.last_jt); // final double [][] jt_full)
		Matrix y_minus_fx_weighted = new Matrix(this.last_ymfx, this.last_ymfx.length);
		boolean debug_jtj = debug_level>4;
		Matrix wjtjlambda = new Matrix(getWJtJlambda(
				lambda,            //
				last_jt_decimated, // this.last_jt, // double [][] jt) // null
				param_pivot,       // final boolean [][] pivot)));
				par_map,           //final int [][]     par_map, // jt already remapped, apply par_map to pivot only
				debug_jtj));       // final boolean debug));
		if (debug_level > -2) { // 1) {
			System.out.println((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime()))+
					" getWJtJlambda() DONE, starting matrix inversion");
		}
		
		if (debug_level>4) {
			System.out.println("JtJ + lambda*diag(JtJ");
			wjtjlambda.print(18, 6);
		}
		Matrix jty = (new Matrix(last_jt_decimated)).times(y_minus_fx_weighted);
		Matrix mdelta = null; //  jtjl_inv.times(jty);
//		boolean use_cholesky = true; // false;
		double matrix_start_time = ((double) System.nanoTime()) * 1E-9; 
		switch (cholesky_mode) { // Cholesky does not copy array
		case CHOLESKY_BLOCK:
			try {
				mdelta =  (new CholeskyBlock(wjtjlambda)).solve(jty); // does not modify arrays (both matrices)
			} catch (RuntimeException e) {
				rslt[1] = true;
				if (debug_level > -2) {
					System.out.println("CHOLESKY_BLOCK: Singular Matrix!");
				}
				return rslt;
			}
			break;
		case CHOLESKY_JAMA:
			try {
				mdelta =  (new CholeskyDecomposition(new Matrix(wjtjlambda.getArrayCopy()))).solve(jty);
			} catch (RuntimeException e) {
				rslt[1] = true;
				if (debug_level > -2) {
					System.out.println("CHOLESKY_JAMA: Singular Matrix!");
				}
				return rslt;
			}
			break;
		case CHOLESKY_FLOAT:
			try {
				mdelta =  (new CholeskyFloat(wjtjlambda)).solve(jty); // does not modify arrays (both matrices)
			} catch (RuntimeException e) {
				rslt[1] = true;
				if (debug_level > -2) {
					System.out.println("CHOLESKY_BLOCK: Singular Matrix!");
				}
				return rslt;
			}
			break;
		case CHOLESKY_LU: // old way - inverse() using LU
			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(last_jt_decimated)).times(y_minus_fx_weighted);
			if (debug_level>2) {
				System.out.println("Jt * (y-fx)");
				jty.print(18, 6);
			}
			mdelta = jtjl_inv.times(jty);
			break;
		
		}
		
		/*
		if (use_cholesky) {
			try {
				mdelta =  (new CholeskyDecomposition(wjtjlambda)).solve(jty);
			} catch (RuntimeException e) {
				rslt[1] = true;
				if (debug_level > -2) {
					System.out.println("Singular Matrix!");
				}
				return rslt;
			}
		} else { // old way - inverse() using LU
			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(last_jt_decimated)).times(y_minus_fx_weighted);
			if (debug_level>2) {
				System.out.println("Jt * (y-fx)");
				jty.print(18, 6);
			}
			mdelta = jtjl_inv.times(jty);
		}
		*/
		
		
		if (debug_level>-2) {
			System.out.println("lmaStep(): Matrix inverted in "+((((double) System.nanoTime()) * 1E-9)-matrix_start_time)+
					", used matrix mode "+CHOLESKY_NAMES[cholesky_mode]+" ("+cholesky_mode+")");
		}

//		
		/*
		CholeskyDecomposition choleskyDecomposition = new CholeskyDecomposition(wjtjlambda);
		CholeskyLDLTMulti choleskyLDLTMulti = new CholeskyLDLTMulti(wjtjlambda);
		Matrix mdelta_cholesky = choleskyDecomposition.solve(jty);
		Matrix mdelta_cholesky_multi = choleskyLDLTMulti.solve(jty);
		 */
		boolean save_cholesky = false; // true;
		boolean debug_cholesky = false; // true;
		if (save_cholesky) {
			int dbg_n = wjtjlambda.getRowDimension();
			double [] dbg_a0 = wjtjlambda.getRowPackedCopy();
			double [] dbg_a = new double[dbg_n*(dbg_n+1)];
			for (int i = 0; i < dbg_n; i++) {
				System.arraycopy(
						dbg_a0,
						i*dbg_n,
						dbg_a,
						i*(dbg_n+1),
						dbg_n);
				dbg_a[(i+1)*(dbg_n+1)-1] = jty.get(i,0);
			}
			ShowDoubleFloatArrays.showArrays(
					dbg_a, // double[] pixels,
					dbg_n+1, // int width,
					dbg_n, // int height,
					"spd_a_"+debug_iter); // String title)
		}
		if (debug_cholesky) {
//			CholeskyLDLTMulti.testCholesky(
//					wjtjlambda, // Matrix wjtjlambda,
//					jty);
			CholeskyBlockTest.testCholesky(
					wjtjlambda, // Matrix wjtjlambda,
					jty,
					"spd_a_"+debug_iter);

			
		}
		if (debug_level>2) {
			System.out.println("mdelta");
			mdelta.print(18, 6);
		}

		double scale = 1.0;
		double []  delta_decim=      mdelta.getColumnPackedCopy();
		double [] delta = expandDecimated(
				par_map,      //final int    [][] par_map, // only [0] used
				delta_decim); // final double [] data_decimated) 
		
		// Limit terrain elevation/ terrain elevation change
		int num_terr_elev_mods = limitTerrainElevation(
				delta,             // double [] delta,
				max_elev_terr,     // double    max_abs,
				max_elev_terr_chg, // double    max_diff,
				debug_level + 2);  // int debugLevel)
		num_terr_elev_mods += limitTerrainElevationPix(
				delta,             // double [] delta,
				max_elev_terr,     // double    max_abs,
				max_elev_terr_chg, // double    max_diff,
				debug_level + 2);  // int debugLevel)
		if ((debug_level > -3) && (num_terr_elev_mods > 0)) {
			System.out.println("lmaStep(): modified "+num_terr_elev_mods+" terrain elevation values to fit limits.");
		}
		/*
		if (par_indx_terr_elev >= 0) {
			double te_change = delta[par_indx_terr_elev];
			if (Math.abs(te_change) > max_elev_terr_chg) {
				delta[par_indx_terr_elev] = (te_change > 0) ? max_elev_terr_chg : -max_elev_terr_chg;
				if (debug_level > - 2) {
					System.out.println("lmaStep(): tried to adjust terrain elevation by "+te_change+", exceeding maximal change of "+max_elev_terr_chg+" pix.");
					System.out.println("lmaStep(): limiting delta["+par_indx_terr_elev+"] to "+delta[par_indx_terr_elev]+" pix.");
				}
			}
			double old_te = parameters_vector[par_indx_terr_elev];
			double new_te = old_te+delta[par_indx_terr_elev];
			if (Math.abs(new_te) > max_elev_terr) {
				double new_te_mod = (new_te > 0)?  max_elev_terr : -max_elev_terr;
				te_change = new_te_mod - new_te;
				delta[par_indx_terr_elev] = te_change;
				if (debug_level > - 2) {
					System.out.println("lmaStep(): tried to set terrain elevation to "+new_te+"(current is "+old_te+"), exceeding maximal allowed "+max_elev_terr+" pix.");
					System.out.println("lmaStep(): limiting delta["+par_indx_terr_elev+"] to "+delta[par_indx_terr_elev]);
				}
			}
		}
		*/
		
		
		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 > -1) {
			yMinusFxSummary(     
					fx, // final double []   fx,
					true); // final boolean show)
		}

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

		this.good_or_bad_rms = rms.clone();
		double [] dbg_ymfx = last_ymfx;
		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();
			terr_elev_last_success = terr_elev_last;
		} 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 (debug_level > 0) {
				yMinusFxSummary(     
						fx, // final double []   fx,
						true); // final boolean show)
			}
		}
		debug_improved = rslt[0];
		if (debug_level > -2) {
			if (rslt[0] ? debug_save_improved :  debug_save_worsened) {
				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(  // Uses last_ymfx which is already restored to previous
						new_vector,  // double [] vector
						dbg_ymfx,    // double [] ymfx,
						rms,         // double [] rms,
						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 > 4) && (samples_pointers[SAMPLES_Y_AVG][1] > 0)){
					String dbg_title = "terrain_corr_fX"+debug_iter;
					debugTerrCorr(
							fx,         // double [] fx,
							dbg_title); // String title)
				}
				
			}
		}
		if (last_ymfx == null) {
			return null; // need to re-init/restart LMA
		}
		return rslt;
	}
	
	/**
	 * Limit modification of the average terrain elevation by both result value and step modification
	 * @param delta      array of calculated differences to be applied to this.parameter_vector,
	 *                   will be modified if the modification violates the limits.
	 * @param max_abs    maximal absolute value of the modified vector value
	 * @param max_diff   maximal absolute value of change of the vector value
	 * @param debugLevel debug level (>0 - report modifications)
	 * @return           number of applied modifications to delta (here 0 or 1)
	 */
	private int limitTerrainElevation(
			double [] delta,
			double    max_abs,
			double    max_diff,
			int debugLevel) {
		int par_indx_terr_elev = getIndxTerrElev(); // index of terrain elevation or -1 
		int num_mod = 0;
		if (par_indx_terr_elev >= 0) {
			boolean mod = false;
			double te_change = delta[par_indx_terr_elev];
			if (Math.abs(te_change) > max_diff) {
				delta[par_indx_terr_elev] = (te_change > 0) ? max_diff : -max_diff;
				mod = true;
				if (debugLevel > 0) {
					System.out.println("limitTerrainElevation(): tried to adjust terrain elevation by "+te_change+", exceeding maximal change of "+max_elev_terr_chg+" pix.");
					System.out.println("limitTerrainElevation(): limiting delta["+par_indx_terr_elev+"] to "+delta[par_indx_terr_elev]+" pix.");
				}
			}
			double old_te = parameters_vector[par_indx_terr_elev];
			double new_te = old_te+delta[par_indx_terr_elev];
			if (Math.abs(new_te) > max_abs) {
				double new_te_mod = (new_te > 0)?  max_abs : -max_abs;
				te_change = new_te_mod - new_te;
				delta[par_indx_terr_elev] = te_change;
				mod = true;
				if (debugLevel > 0) {
					System.out.println("limitTerrainElevation(): tried to set terrain elevation to "+new_te+"(current is "+old_te+"), exceeding maximal allowed "+max_elev_terr+" pix.");
					System.out.println("limitTerrainElevation(): limiting delta["+par_indx_terr_elev+"] to "+delta[par_indx_terr_elev]);
				}
			}
			if (mod) {
				num_mod++;
			}
		}
		return num_mod;
	}
	
	/**
	 * Limit modification of the per-pixel terrain elevations by both result value and step modification
	 * @param delta      array of calculated differences to be applied to this.parameter_vector,
	 *                   will be modified if the modification violates the limits.
	 * @param max_abs    maximal absolute value of the modified vector value (single-pixel_
	 * @param max_diff   maximal absolute value of change of the vector value (single-pixel_
	 * @param debugLevel debug level (>0 - report modifications)
	 * @return           number of applied modifications to delta 
	 */

	private int limitTerrainElevationPix(
			double [] delta,
			double    max_abs,
			double    max_diff,
			int debugLevel) {
		int par_indx_terr_elev_pix = getIndxTerrElevPix(); // index of terrain elevation or -1 
		int par_num_terr_elev_pix = getNumTerrElevPix();
		int num_mod = 0;
		if (par_indx_terr_elev_pix >= 0) {
			for (int ipar = 0; ipar < par_num_terr_elev_pix; ipar++) {
				boolean mod = false;
				int npar = par_indx_terr_elev_pix + ipar;
				double te_change = delta[npar];
				if (Math.abs(te_change) > max_diff) {
					delta[npar] = (te_change > 0) ? max_diff : -max_diff;
					mod = true;
					if (debugLevel > 0) {
						System.out.println("limitTerrainElevationPix(): tried to adjust terrain pixel elevation "+ipar+" by "+te_change+", exceeding maximal change of "+max_elev_terr_chg+" pix.");
						System.out.println("limitTerrainElevationPix(): limiting delta["+npar+"] to "+delta[npar]+" pix.");
					}
				}
				double old_te = parameters_vector[npar];
				double new_te = old_te+delta[npar];
				if (Math.abs(new_te) > max_abs) {
					double new_te_mod = (new_te > 0)?  max_abs : -max_abs;
					te_change = new_te_mod - new_te;
					delta[npar] = te_change;
					mod = true;
					if (debugLevel > 0) {
						System.out.println("limitTerrainElevationPix(): tried to set terrain elevation "+ipar+" to "+new_te+"(current is "+old_te+"), exceeding maximal allowed "+max_elev_terr+" pix.");
						System.out.println("limitTerrainElevationPix(): limiting delta["+npar+"] to "+delta[npar]);
					}
				}
				if (mod) {
					num_mod++;
				}
			}
		}
		return num_mod;
	}
	
	
	public void debugTerrCorr(
			double [] fx,
			String title) {
//		String dbg_title = "terrain_corr_fX"+debug_iter;
		String [] dbg_titles = {"y","fX","diff"};
		double [][] dbg_img = new double [3][woi.width*woi.height];
		System.arraycopy(
				y_vector,
				samples_pointers[SAMPLES_Y_AVG][0],
				dbg_img[0],
				0,
				dbg_img[0].length);
		System.arraycopy(
				fx,
				samples_pointers[SAMPLES_Y_AVG][0],
				dbg_img[1],
				0,
				dbg_img[1].length);
		for (int i = 0; i < dbg_img[2].length; i++) {
			dbg_img[2][i] = dbg_img[0][i]-dbg_img[1][i]; 
		}
		ShowDoubleFloatArrays.showArrays(
				dbg_img,
				woi.width,
				woi.height,
				true,
				title,
				dbg_titles);

	}
	public String getParametersDebugTitle() {
		return getParametersDebugTitle(false);
	}
	public String getParametersDebugTitle(boolean short_name) {
		// alpha_en_holes -> alpha_mm_hole = NaN;
		// hifreq_weight
		String  debug_title = "par_vec-x"+woi.x+"-y"+woi.y+"-w"+woi.width+"-h"+woi.height;
		if (!short_name) {
			debug_title += "fit"+IntersceneMatchParameters.booleansToString(fits, -2); //+/-, no separators
			debug_title += "skp"+IntersceneMatchParameters.booleansToString(fits_disable, -2); //+/-, no separators
			debug_title += "hf"+hifreq_weight+"tc"+terrain_correction;
			debug_title += "al"+alpha_loss+"all"+alpha_loss_lin+"ao"+alpha_offset+"ao"+alpha_0offset+"av"+alpha_min_veg+"at"+alpha_max_terrain+"ap"+alpha_pull_pwr;
			debug_title += "ap"+alpha_lpf+"as"+alpha_scale_avg+(alpha_piece_linear?"ln":"cs");
			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+"el"+elevation_lpf+"tp"+terr_pull0+"tu"+terr_pull_up;
			debug_title += "ta"+terr_pull_avg+"vp"+veget_pull0+"vpl"+veget_pull_low_alpha+"ep"+elevation_pull0;
			if (elev_alpha_en) {
				debug_title += "ea"+elev_alpha+"ep"+elev_alpha_pwr+"lv"+low_veget;
			}
			debug_title += "sp"+scenes_pull0+"sp0"+ scenes_pull0+"bp"+boost_parallax;
			debug_title += "er"+elevation_radius+"tr"+ terr_elev_radius+"rx"+elev_radius_extra;
			debug_title += from_file?"-file":"-new";
		}
		return debug_title;
	}

	public static String getParametersDebugTitleEnd(boolean from_file) {
		return from_file?"-file":"-new";
	}

	
	
	public void debugImageSaveShow(
			double [] vector,
			double [] ymfx,
			double [] rms,
			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];
		if (vector ==null) {
			vector = parameters_vector;
		}
		if (ymfx==null) {
			ymfx = last_ymfx;
		}
		double [] debug_img =  parametersImage (
				vector,                        // double [] vector,
				show_extra ? ymfx : null, // double [] ymfx,		
				1,                             // int       gap)
				wh,
				null); // title);
		String title_all = title+"-live";
		if ((debug_image != null) && (debug_index < debug_image.length)) {
			if (debug_iters == null) {
				debug_iters = new int [debug_image.length];
			}
			if (debug_series == null) {
				debug_series = new int [debug_image.length];
			}
			if (debug_rms == null) {
				debug_rms = new double [debug_image.length][];
			}
			if (debug_improvements == null) {
				debug_improvements = new boolean [debug_image.length];
			}
			
			debug_iters[debug_index] = debug_iter;
			debug_series[debug_index] = (vegetationModel != null) ? vegetationModel.step_restore : -1;
			debug_rms[debug_index] = rms.clone();
			debug_improvements[debug_index] = debug_improved;
			title += "-"+debug_iter+ "_"+debug_index+String.format("_rms%8.5f_rms-pure%8.5f", rms[0],rms[1]);
			if (!debug_improved) {
				title+="-WORSE";
			}
			debug_image[debug_index++] = debug_img;
		}
		
		
		if (show_this) {
			String this_title = title;
			if (vegetationModel != null) {
				this_title += "-series"+vegetationModel.step_restore;
			}
			this_title += "-iter"+debug_iter;
			ShowDoubleFloatArrays.showArrays(
					debug_img,
					wh[0],
					wh[1],
					this_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] ="";
				if (debug_series[i] >= 0) {
					titles[i] += "series"+debug_series[i]+"-";
				}
				titles[i] += "iter"+debug_iters[i];
				if ((debug_rms != null) && (debug_rms[i] != null)) {
					titles[i]+=String.format("_rms%8.5f_rms-pure%8.5f", debug_rms[i][0], debug_rms[i][1]);
				}
				if (!debug_improvements[i]) {
					titles[i]+="-WORSE";
				}
				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);
			}
		}
		return;
	}
	
	
	private double [][] getWJtJlambdaDebug(
			final double      lambda,
			final double [][] jt,
			final boolean [][] pivot,
			final int [][]     par_map) {
		long startTime = System.nanoTime();
		double [][] jtj_full = getWJtJlambda(
				lambda, // final double      lambda,
				jt,     // final double [][] jt,
				null,   // final boolean [][] pivot,
				par_map, // final int [][]     par_map,
				false);
		long fullTime = System.nanoTime();
		double full_run_time = (fullTime - startTime) * 1E-9;
		System.out.println("getWJtJlambdaDebug(): full run time: "+full_run_time+"s.");
		
		double [][] jtj = getWJtJlambda(
				lambda, // final double      lambda,
				jt,     // final double [][] jt,
				pivot,  // final boolean [][] pivot,
				par_map, // final int [][]     par_map,
				false);
		long endTime = System.nanoTime();
		double pivot_run_time =  (endTime - fullTime) * 1E-9;
		System.out.println("getWJtJlambdaDebug():with pivot array: "+pivot_run_time+"s.");
		double max_err = 0;
		for (int par0 = 0; par0 < jtj_full.length; par0++) {
			for (int par1 = par0; par1 < jtj_full[par0].length; par1++) {
				double diff= jtj[par0][par1] - jtj_full[par0][par1];
				double adiff = Math.abs(diff);
				if (adiff > max_err) {
					System.out.println("getWJtJlambdaDebug(): par0="+par0+", par1="+par1+" diff="+diff+
							", jtj="+jtj[par0][par1]+", jtj_full="+jtj_full[par0][par1]);
					max_err = adiff;
				}
			}
		}
		System.out.println("getWJtJlambdaDebug(): max_err="+max_err);
		showPivot(
				jtj, // double [][] jtj,
				"jtj_pivoted.tiff"); // String title)
		showPivot(
				jtj_full, // double [][] jtj,
				"jtj_full.tiff"); // String title)
		return jtj_full; // old version
	}
	
	
	private double [][] getWJtJlambda(
			final double       lambda,
			final double [][]  jt,
			final boolean [][] pivot,
			final int [][]     par_map, // jt already remapped, apply par_map to pivot only
			final boolean debug)
	{
		if (debug && (pivot != null)) {
			return getWJtJlambdaDebug(
					lambda,   // final double      lambda,
					jt,       // final double [][] jt,
					pivot,    // final boolean [][] pivot,
					par_map); // final int [][]     par_map,

		}
		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;
						int pi = (par_map==null)? i : par_map[1][i];
						int pj = (par_map==null)? j : par_map[1][j];
						// diagonals are set true in pivot[i][i] = true;
						if ((j >= i) && ((pivot == null) || pivot[pi][pj])) { // 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 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 void setupElevationLMA( // return pivot?
			final double []   vector,
			final boolean     calc_derivatives,
			final int debugLevel) {
		final boolean elevation_parameters = (num_pars[TVAO_ELEVATION] > 0);
		final boolean need_derivs = elevation_parameters && calc_derivatives;
		if (!need_derivs && (elev_sum_weights != null) && !elevation_parameters) { // && !elevation_parameters for debugging derivatives
			return; // nothing to do
		}
		final int woi_length =     woi.width * woi.height;
		final int woi_veg_length = (woi_veg == null) ? 0 : woi_veg.width * woi_veg.height;
		elev_woi4 =     new int [num_scenes][][];
		elev_weights4 = new double [num_scenes][][];
		elev_sum_weights = new double [num_scenes][];
		if (need_derivs) {
			elev_dsum_weights = new double [num_scenes][][];
		}
	//elev_radius[num_pixels];	
		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 (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_dsum_weights[nScene] = new double [num_pars[TVAO_ELEVATION]][woi_length]; // used
						}
						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 fnpix = x + y * full.width;
							double elevation;
							if (elevation_parameters) {
								int npar = par_index[TVAO_ELEVATION][fnpix]; 
								if (npar >= 0) {
									elevation = vector[npar];
								} else {
									System.out.println("setupElevationLMA(): BUG:  par_index["+TVAO_ELEVATION+"]["+fnpix+"] ="+npar);
									continue;
								}
							} else {
								elevation =  tvao[TVAO_ELEVATION][fnpix];
							}

							double [] scales =  scales_xy[nScene][fnpix];
							double radius = elev_radius[fnpix] * elev_radius_extra; // *1.2;
							double px = x + scales[0] * elevation;      // ELEV-SIGN
							double py = y + scales[1] * elevation;      // ELEV-SIGN
							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];
								}
							}
							Arrays.fill(wnd_x, Double.NaN);
							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))   : // ELEV-SIGN
											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 (has_vegetation[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];
										}
										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); // ELEV-SIGN
											}
										}
										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];
											int par_indx_elev = par_index[TVAO_ELEVATION][fnpix]; // ipx+ipy*full.width];
											// As par_indx_elev <-> fnpix <->wvindx, there is only one term in a sum below
											elev_dsum_weights[nScene][par_indx_elev-ind_pars[TVAO_ELEVATION]][wpindex] += dww;
											contrib_thread.get(wpindex).add(par_indx_elev); // full parameter index 
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		if (need_derivs) {
			// combine pivots
			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_terr_proj[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 
		}
		if (debugLevel > 6) {
			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;		
	}

	private void setupTerrainElevationPixLMA(
			final double [] vector,
			final double    radius,
			final boolean   calc_derivatives,
			final int       debugLevel) {
		///		if (!fits_disable[TVAO_TERR_ELEV_PIX]) { // adjusting per-pixel terrain altitude
		///			return;
		///		}
		final boolean terr_elpix_parameters = (num_pars[TVAO_TERR_ELEV_PIX] > 0);
		final boolean need_derivs = terr_elpix_parameters && calc_derivatives;
		if (!need_derivs && (terr_elpix_sum_weights != null) && !terr_elpix_parameters) { // && !elevation_parameters for debugging derivatives
			return; // nothing to do
		}
		final int woi_length =     woi.width * woi.height;
		final int woi_terr_length = (woi_terr == null) ? 0 : woi_terr.width * woi_terr.height;
		terr_elpix_woi4 =     new int [num_scenes][][];
		terr_elpix_weights4 = new double [num_scenes][][];
		terr_elpix_sum_weights = new double [num_scenes][];
		if (need_derivs) {
			terr_elpix_dsum_weights = new double [num_scenes][][];
		}
		//elev_radius[num_pixels];	
		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 (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]){
						terr_elpix_woi4[nScene] = new int [woi_terr_length][];
						terr_elpix_weights4[nScene] = new double [woi_terr_length][];
						terr_elpix_sum_weights[nScene] = new double [woi_length];
						if (need_derivs) {
							terr_elpix_dsum_weights[nScene] = new double [num_pars[TVAO_TERR_ELEV_PIX]][woi_length]; // used
						}
						for (int wtindex = 0; wtindex < woi_terr_length; wtindex++) if (valid_terrain[wtindex]) {
							int wvx = wtindex % woi_terr.width;   // relative to woi_veg
							int wvy = wtindex / woi_terr.width;   // relative to woi_veg
							int x = wvx + woi_terr.x;             // relative to full
							int y = wvy + woi_terr.y;             // relative to full
							int fnpix = x + y * full.width;
							double terr_elev;
							if (terr_elpix_parameters) {
								int npar = par_index[TVAO_TERR_ELEV_PIX][fnpix]; 
								if (npar >= 0) {
									terr_elev = vector[npar];
								} else {
									System.out.println("setupElevationLMA(): BUG:  par_index["+TVAO_TERR_ELEV_PIX+"]["+fnpix+"] ="+npar);
									continue;
								}
							} else {
								terr_elev =  tvao[TVAO_TERR_ELEV_PIX][fnpix];
							}

							double [] scales =  scales_xy[nScene][fnpix];
							//							double radius = terr_elev_radius;
							double px = x + scales[0] * terr_elev;      // ELEV-SIGN
							double py = y + scales[1] * terr_elev;      // ELEV-SIGN
							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];
								}
							}
							Arrays.fill(wnd_x, Double.NaN);
							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))   : // ELEV-SIGN
											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 (has_terrain[wpindex]) {
										if (terr_elpix_woi4[nScene][wtindex] == null) {
											terr_elpix_woi4[nScene][wtindex] = new int[dia2]; // meter2];
											Arrays.fill(terr_elpix_woi4[nScene][wtindex], -1);
											terr_elpix_weights4[nScene][wtindex] = new double[dia2]; // meter2];meter2];
										}
										int dx = ipx - ipx0;
										int dindx = dx + dy*dia_x; // meter;
										terr_elpix_woi4[nScene][wtindex][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); // ELEV-SIGN
											}
										}
										double ww= wnd_y*wnd_x[dx];
										terr_elpix_weights4[nScene][wtindex][dindx] = ww;
										terr_elpix_sum_weights[nScene][wpindex] += ww;
										if (need_derivs) {
											double dww =  wnd_y * dwnd_x[dx] +  dwnd_y*wnd_x[dx];
											int par_indx_telev = par_index[TVAO_TERR_ELEV_PIX][fnpix]; // ipx+ipy*full.width];
											// As par_indx_telev <-> fnpix <->wtindx, there is only one term in a sum below
											terr_elpix_dsum_weights[nScene][par_indx_telev-ind_pars[TVAO_TERR_ELEV_PIX]][wpindex] += dww;
											contrib_thread.get(wpindex).add(par_indx_telev); // full parameter index 
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);

		if (need_derivs) {
			// combine pivots
			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_terr_proj[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);
			terr_elpix_contribs = contrib_combo; // for each of the woi pixels - set of full elevation parameter indices that influence it 
		}
		if (debugLevel > 6) {
			String title = "setupTerrainElevationPixLMA.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] = terr_elpix_sum_weights[nscene][i];	
					}
					if (need_derivs) {
						for (int i = 0; i < woi_length; i++) {
							dbg_img[1][nscene][i] = terr_elpix_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;		
	}
	
	private void setupTerrainElevationLMA( // return pivot?
			final double [] vector,
			final double    radius,
			final boolean   calc_derivatives,
			final int       debugLevel) {
///		if (!fits_disable[TVAO_TERR_ELEV]) {
///			return;
///		}
		final boolean terr_elev_parameters = (num_pars[TVAO_TERR_ELEV] > 0);
		final boolean need_derivs = terr_elev_parameters && calc_derivatives;
		if (!need_derivs && (terr_elev_sum_weights != null) && !terr_elev_parameters) { // !terr_elev_parameters for debugging derivatives
			return; // nothing to do
		}
		double terr_elevation0;
		if (terr_elev_parameters) {
			int npar = par_index[TVAO_TERR_ELEV][0]; // fnpix];
			if (npar >= 0) {
				terr_elevation0 = vector[npar];
			} else {
				throw new IllegalArgumentException("setupElevationLMA(): BUG:  par_index["+TVAO_TERR_ELEV+"]["+0+"] ="+npar);
			}
		} else {
			terr_elevation0 =  0;
		}
		final double terr_elevation = terr_elevation0;
		if ((terr_elevation == terr_elev_last) && (terr_elev_sum_weights != null) && !need_derivs && (debugLevel > -2)) {// added && !need_derivs
			System.out.println("Terrain elevation did not change, keeping "+terr_elevation);
			return;
		}
		
		
		// terr_elev_last
		final int woi_length =      woi.width * woi.height;
		final int woi_terr_length = woi_terr.width * woi_terr.height;
		terr_elev_woi4 =     new int [num_scenes][][];
		terr_elev_weights4 = new double [num_scenes][][];
		terr_elev_sum_weights = new double [num_scenes][];
		if (need_derivs) {
			terr_elev_dweights4 =    new double [num_scenes][][];
			terr_elev_dsum_weights = new double [num_scenes][]; // [];
		}
	//elev_radius[num_pixels];	
		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 (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]){
						terr_elev_woi4[nScene] = new int [woi_terr_length][];
						terr_elev_weights4[nScene] = new double [woi_terr_length][];
						terr_elev_sum_weights[nScene] = new double [woi_length];
						if (need_derivs) {
							terr_elev_dweights4[nScene] = new double [woi_terr_length][];
//							terr_elev_dsum_weights[nScene] = new double [num_pars[TVAO_TERR_ELEV]]; // [woi_length];
							terr_elev_dsum_weights[nScene] = new double [woi_length]; // [woi_length];
						}
						for (int wtindex = 0; wtindex < woi_terr_length; wtindex++) if (valid_terrain[wtindex]) {
							int wtx = wtindex % woi_terr.width;   // relative to woi_veg
							int wty = wtindex / woi_terr.width;   // relative to woi_veg
							int x = wtx + woi_terr.x;             // relative to full
							int y = wty + woi_terr.y;             // relative to full
							int fnpix = x + y * full.width;

							if ((terr_elevation != 0.0) || terr_elev_parameters) { // when terr_elev_parameters, even with terr_elevation == 0.0 derivative by terr_elev depends on neighbors
								double [] scales =  scales_xy[nScene][fnpix];
								//double radius = elev_radius[fnpix];
//								Arrays.fill(wnd_x, Double.NaN);
								double px = x + scales[0] * terr_elevation;      // ELEV-SIGN
								double py = y + scales[1] * terr_elevation;      // ELEV-SIGN
								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];
									}
								}
								Arrays.fill(wnd_x, Double.NaN);
								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))   : // ELEV-SIGN
												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 (has_terrain[wpindex]) {
											if (terr_elev_woi4[nScene][wtindex] == null) {
												terr_elev_woi4[nScene][wtindex] = new int[dia2]; // meter2];
												Arrays.fill(terr_elev_woi4[nScene][wtindex], -1);
												terr_elev_weights4[nScene][wtindex] = new double[dia2]; // meter2];meter2];
												if (need_derivs) {
													terr_elev_dweights4[nScene][wtindex] = new double[dia2]; // meter2];meter2];
												}
											}
											int dx = ipx - ipx0;
											int dindx = dx + dy*dia_x; // meter;
											terr_elev_woi4[nScene][wtindex][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); // ELEV-SIGN
												}
											}
											double ww= wnd_y*wnd_x[dx];
											terr_elev_weights4[nScene][wtindex][dindx] = ww;
											terr_elev_sum_weights[nScene][wpindex] += ww;
											if (need_derivs && terr_elev_parameters) { // otherwise par_index[TVAO_TERR_ELEV][0] will fail
												double dww =  wnd_y * dwnd_x[dx] +  dwnd_y*wnd_x[dx];
												terr_elev_dweights4[nScene][wtindex][dindx] =  dww;
												int par_indx_elev = par_index[TVAO_TERR_ELEV][0]; // fnpix]; // ipx+ipy*full.width];
//												terr_elev_dsum_weights[nScene][par_indx_elev-ind_pars[TVAO_TERR_ELEV]] += dww; // [wpindex] += dww;
												terr_elev_dsum_weights[nScene][wpindex] += dww; // [wpindex] += dww;
												contrib_thread.get(wpindex).add(par_indx_elev); // full parameter index 
											}
										}
									}
								}
							} else if (woi.contains(x,y)) { //if (terr_elevation != 0.0) { one-to-one correspondence between terrain parameters and output terrain
								int wx = x - woi.x;
								int wy = y - woi.y;
								int windx = wx + wy * woi.width;
								if (has_terrain[windx]) { // java.lang.ArrayIndexOutOfBoundsException: Index 3800 out of bounds for length 400
									terr_elev_woi4[nScene][wtindex] = new int[1];
									terr_elev_woi4[nScene][wtindex][0] = windx;
									terr_elev_weights4[nScene][wtindex] = new double[1]; // meter2];meter2];
									terr_elev_weights4[nScene][wtindex][0] = 1.0;
									terr_elev_sum_weights[nScene][windx] = 1.0;
									// no dependence on terr_elev here
									/*
									if (need_derivs) {
										terr_elev_dweights4[nScene][wtindex] = new double[1]; // meter2];meter2];
										terr_elev_dweights4[nScene][wtindex][0] = 1.0;
										terr_elev_dsum_weights[nScene][windx] = 1.0;
									}
									*/
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (need_derivs) {
			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_terr_proj[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);
			terr_elev_contribs = contrib_combo; // for each of the woi pixels - set of full elevation parameter indices that influence it 
		}
		
		if (debugLevel > 6) {
			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] = terr_elev_sum_weights[nscene][i];	
					}
					if (need_derivs) {
						for (int i = 0; i < woi_length; i++) {
							dbg_img[1][nscene][i] = terr_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)
		}
		terr_elev_last = terr_elevation; // Terrain elevation value for which data was processed
		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;
	}

	
	private static void addRelatedFromSets(
			final int [][] par_relations,
			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() {
					HashSet<Integer> this_par_set = new HashSet<Integer>(); //related parameters to nPar
					for (int nPar = ai.getAndIncrement(); nPar < par_number; nPar = ai.getAndIncrement()){
						this_par_set.clear();
						int full_par = nPar+par_index;
						for (HashSet<Integer> par_set:sets) {
							if (par_set.contains(full_par)) {
								this_par_set.addAll(par_set);
							}
						}
						if (!this_par_set.isEmpty()) {
							if (par_relations[full_par] != null) {
								for (int par: par_relations[full_par]) {
									this_par_set.add(par);
								}
							}
							int [] pars_arr = this_par_set.stream().mapToInt(Integer::intValue).toArray();
							Arrays.sort(pars_arr); // optional
							par_relations[full_par] = pars_arr;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	private static int [][] addNeighborsLists(
			final int [][] pars,
			final int [][] par_neibs) {
		final int [][] pars_with_neibs = new int [pars.length][];
		if (pars.length != par_neibs.length) {
			throw new IllegalArgumentException("addNeighborsLists(): pars.length="+pars.length+
					" != par_neibs.length ="+par_neibs.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() {
					HashSet<Integer> this_par_set = new HashSet<Integer>(); //related parameters to nPar
					for (int nPar = ai.getAndIncrement(); nPar < par_neibs.length; nPar = ai.getAndIncrement()){
//						if ((nPar==84) || (nPar==1373)) {
//							System.out.println("addNeighborsLists() nPar="+nPar);
//						}
						if ((pars[nPar] != null) && (pars[nPar].length > 0)) {
							this_par_set.clear();
							for (int other_par: pars[nPar]) {
								this_par_set.add(other_par);
								if ((par_neibs[other_par] != null) && (par_neibs[other_par].length > 0)) {
									int [] neib_pars = par_neibs[other_par]; // should all be >= 0
									for (int par: neib_pars) {
										this_par_set.add(par);
									}
								}
							}
							if (!this_par_set.isEmpty()) { // anything to add - probably always
								//https://stackoverflow.com/questions/2451184/how-can-i-convert-a-java-hashsetinteger-to-a-primitive-int-array
								int [] pars_arr = this_par_set.stream().mapToInt(Integer::intValue).toArray();
								Arrays.sort(pars_arr); // optional
								pars_with_neibs[nPar] = pars_arr;
							}							
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return pars_with_neibs;
	}
	
														
	public static boolean [][] listsToPivot(
			final int [][] par_lists){
		final boolean [][] pivot = new boolean [par_lists.length][par_lists.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() {
					for (int nPar = ai.getAndIncrement(); nPar < par_lists.length; nPar = ai.getAndIncrement()){
						pivot[nPar][nPar] = true;
						int [] pars = par_lists[nPar];
						if (pars != null ) {
							for (int par : pars) if (par > nPar){
								pivot[nPar][par] = true;
								pivot[par][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 pivotAddNeighbors0(
			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 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;
								int neib_rel = neib - start_index;
								if ((neib_rel >= 0) && (neib_rel < neibs.length)) {
									int [] this_neib_neibs = neibs[neib_rel];
									for (int dir_neib = 0; dir_neib < this_neib_neibs.length; dir_neib ++) {
										int neib_neib = this_neib_neibs[dir_neib];
										if (neib_neib >= 0) {
											pivot[full_par][neib_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;
								int neib_rel = neib - start_index;
								if ((neib_rel >= 0) && (neib_rel < neibs.length)) {
									int [] this_neib_neibs = neibs[neib_rel];
									for (int dir_neib = 0; dir_neib < this_neib_neibs.length; dir_neib ++) {
										int neib_neib = this_neib_neibs[dir_neib];
										if (neib_neib >= 0) {
											pivot[neib_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)	{
		return getFxDerivs(
				vector,           //final double []   vector,
				jt,               // final double [][] jt, // should be null or initialized with [vector.length][]
				null,             // final double [][] debug_data, // {elev* alpha shifted, terrain *(1-alpha) shifted, elev_sum_weights}  
				debug_level);     // final int         debug_level)
	}	
	
	
	
	private double [] getFxDerivs(
			final double []   vector,
			final double [][] jt, // should be null or initialized with [vector.length][]
			final double [][] extra_data, // {elev* alpha shifted, terrain *(1-alpha) shifted, elev_sum_weights}  
			final int         debug_level)	{
		final boolean dbg1 = (debug_level > 4);
//		final int     dbg_y_indx = 900; // and multiples ?
		final int     dbg_npar =   -2524; // -900; // first vegetation
		setupElevationLMA(
				vector, // final double []   vector,
				(jt != null), // final boolean     calc_derivatives,
				debug_level); // final int debugLevel) {
		final boolean use_terr_elev_pix = (getIndxTerrElevPix() >=0)  &&
				(!fits_disable[TVAO_TERR_ELEV_PIX] || fits_disable[TVAO_TERR_ELEV]); // when both are disabled
		final boolean use_terr_elev = (getIndxTerrElev() >=0) && !use_terr_elev_pix; 
		// only one of terr_elev_parameters, terr_elpix_parameters may be true simultaneously
		final boolean terr_elev_parameters = use_terr_elev; //  (num_pars[TVAO_TERR_ELEV]    > 0) && !fits_disable[TVAO_TERR_ELEV];
		final boolean terr_elpix_parameters = use_terr_elev_pix; // (num_pars[TVAO_TERR_ELEV_PIX]> 0) && !fits_disable[TVAO_TERR_ELEV_PIX];
		
		if (use_terr_elev_pix) { // (getIndxTerrElevPix() >=0) && !fits_disable[TVAO_TERR_ELEV_PIX]) {
			setupTerrainElevationPixLMA(
					vector,           // final double [] vector,
					terr_elev_radius * elev_radius_extra, // final double    radius,
					(jt != null),     // final boolean     calc_derivatives,
					debug_level);     // final int debugLevel)
			terr_elev_woi4 = null; // do not try common terrain elevation
		} else {
			terr_elpix_woi4 = null;
			if (use_terr_elev) { //(getIndxTerrElev() >=0) && !fits_disable[TVAO_TERR_ELEV]) {
				setupTerrainElevationLMA(
						vector,           // final double [] vector,
						terr_elev_radius, // final double    radius, no * elev_radius_extra (it was not increased)
						(jt != null),     // final boolean     calc_derivatives,
						debug_level);     // final int debugLevel) {
			} else {
				terr_elev_woi4 = null;
			}
		}
		
		final int woi_length =      woi.width      * woi.height;
		final int woi_veg_length =  (woi_veg == null) ? 0 : (woi_veg.width  * woi_veg.height);
		final int woi_terr_length = woi_terr.width * woi_terr.height;
		
		final boolean need_derivs =           (jt != null);
		final boolean elevation_parameters =  (num_pars[TVAO_ELEVATION]    > 0); // 0 when woi_veg==null
		final boolean vegetation_parameters = (num_pars[TVAO_VEGETATION]   > 0); // 0 when woi_veg==null
		final boolean alpha_parameters =      (num_pars[TVAO_ALPHA]        > 0); // 0 when woi_veg==null
		final boolean terrain_parameters =    (num_pars[TVAO_TERRAIN]      > 0);
		final boolean scenes_parameters =     (num_pars[TVAO_SCENE_OFFSET] > 0);
		final AtomicInteger anum_elev_err = new AtomicInteger(0);
		// 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];
			}
		}
		if (extra_data != null) {
			for (int i = 0; i < extra_data.length; i++) {
				extra_data[i] = new double [weights.length];
			}
			if (extra_data.length > 5) {
				System.arraycopy(
						weights,
						0,
						extra_data[5],
						0,
						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);
			}
		}
		final int dbg_y_indx = -340;
		final int dbg_scene = -9; // -105; // -20;
		final int dbg_windex = -234; // -1;
		final int dbg_wtindex = -340; // -1;
		final int dbg_n = -7402; // -69992;
		final int dbg_findx = -121775;
		final int fdbg_scene = dbg_scene;
		final int dbg_cols=2,dbg_rows=2;
		final int dbg_ntpar = -44;
		final double alpha_threshold = alpha_0offset + alpha_min_veg *(1 - alpha_offset - alpha_0offset);
		final double alpha_threshold_pull = alpha_0offset + alpha_max_terrain *(1 - alpha_offset - alpha_0offset);
		
		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 [] terrain_woi = new double [woi_length]; // needs to be divided by terrain_woi_weights
					double [][] dalpha_woi = null; 
					double [][] dveget_woi = null; 
					double [][] dterrain_woi_dterrain =  null; // derivatives of terrain in image [woi] per terrain [woi_terr] 
					double []   dterrain_woi_dtelev =  null;   // derivatives of terrain in image [woi] per terrain elevation [1]
////					double [][] dterrain_woi_dtelpix =  null;  // derivatives of terrain in image [woi] per terrain elevation [woi_terr]
					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[TVAO_ALPHA]][woi_length]; // first index is 0 if not adjusted 
						dveget_woi =   new double [num_pars[TVAO_VEGETATION]][woi_length]; // first index is 0 if not adjusted
						if (terrain_parameters) {
							dterrain_woi_dterrain =   new double [num_pars[TVAO_TERRAIN]][woi_length];
						}
						if (terr_elev_parameters) {
							dterrain_woi_dtelev = new double [woi_length];
						}
////						if (terr_elpix_parameters) {
////							dterrain_woi_dtelpix = new double [num_pars[TVAO_TERR_ELEV_PIX]][woi_length];
////						}
					}
					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);
						Arrays.fill(terrain_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);
							}
							if (dterrain_woi_dtelev != null) {
								Arrays.fill(dterrain_woi_dtelev, 0);
							}
							if (dterrain_woi_dterrain != null) {
								for (int i = 0; i < dterrain_woi_dterrain.length; i++) {
									Arrays.fill(dterrain_woi_dterrain[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];
						}
						if (woi_veg != null) {
							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;
								if (npix == dbg_findx) {
									System.out.println("getFxDerivs() 1: findx="+npix);
								}
								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_VEGETATION][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 [] result_woi_ind =      elev_woi4[nScene][wvindex]; // list of output pixels influenced by each vegetation pixel
								if (result_woi_ind != null) {
									double [] result_woi_weights = elev_weights4[nScene][wvindex];// same vegetation pixel, per result neighbor
									for (int i = 0; i < result_woi_ind.length; i++) {
										int windx = result_woi_ind[i];
										if (windx >= 0) {
											double w =  result_woi_weights[i];
											veget_woi[windx] += w * vegetation; // accumulate vegetation for the same result pixel
											alpha_woi[windx] += w * alpha;      // accumulate alpha for the same result pixel
											if (need_derivs) {
												if (nvpar >= 0) {
													contrib_thread.get(windx).add(nvpar); // full parameter index - this result pixel depends on this vegetation parameter
													dveget_woi[nvpar-ind_pars[TVAO_VEGETATION]][windx] += w; // this fX pixel's vegetation with respect to this vegetation parameter
												}
												if (napar >= 0) {
													contrib_thread.get(windx).add(napar); // full parameter index - this result pixel depends on this alpha parameter
													dalpha_woi[napar-ind_pars[TVAO_ALPHA]][windx] += w; // this fX pixel's alpha with respect to this alpha parameter
												}
											}
										}
									}
								}
							} // for (int wvindex = 0; wvindex < woi_veg_length; wvindex++) if (valid_vegetation[wvindex]) {
						} // if (woi_veg != null) {
						
						if (terr_elev_woi4 != null) { // common terrain elevation
							for (int wtindex = 0; wtindex < woi_terr_length; wtindex++) if ((terr_elev_woi4[nScene] != null) &&(terr_elev_woi4[nScene][wtindex] != null)) {
								int wtx = wtindex % woi_terr.width;   // relative to woi_terr
								int wty = wtindex / woi_terr.width;   // relative to woi_terr
								int x = wtx + woi_terr.x;             // relative to full
								int y = wty + woi_terr.y;             // relative to full
								int npix = x + y * full.width;
								if (npix == dbg_findx) {
									System.out.println("getFxDerivs() 2: findx="+npix);
								}
								if ((wtindex== dbg_wtindex) && (nScene ==dbg_scene)) {
									System.out.println("getFxDerivs() 2: nScene="+nScene+", windx="+wtindex);
								}

								double terrain; // [woi_terr], adjusted/elevated terrain
								int ntpar = -1;
								if (terrain_parameters) {
									ntpar = par_index[TVAO_TERRAIN][npix]; 
									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];
								}
								int [] result_woi_ind =      terr_elev_woi4[nScene][wtindex]; // single-element in (terr_elev == 0) and no fitting by terr_elev 
								if (result_woi_ind != null) { // now always
									double [] result_woi_weights = terr_elev_weights4[nScene][wtindex];
									for (int i = 0; i < result_woi_ind.length; i++) {
										int windx = result_woi_ind[i];
										if (windx >= 0) {
											double stw = terr_elev_sum_weights[nScene][windx];
											if (stw > 0) {
												double w =  result_woi_weights[i]; ///  /stw; // moved division to the woi domain (single division)
												terrain_woi[windx] += w * terrain; // accumulate terrain contributors for the same terrain output pixel (wiil need division by  terrain_woi_weights)
												if (need_derivs) {
													if (ntpar >= 0) {
														contrib_thread.get(windx).add(ntpar); // full parameter index - this result pixel depends on this terrain parameter
														// derivative  of fX with respect to a specific terrain pixel
														dterrain_woi_dterrain[ntpar-ind_pars[TVAO_TERRAIN]][windx] += w; // /stw; // this fX pixel's terrain with respect to this terrain parameter
													}
													// calculate dterrain_woi_dtelev - derivative of terrain_woi[windx] with respect to terr_elev
													if (terr_elev_parameters) {
														dterrain_woi_dtelev[windx] +=
																terrain * (terr_elev_dweights4[nScene][wtindex][i] * stw -
																		w * terr_elev_dsum_weights[nScene][windx])/(stw * stw);
													}
												}
											} else {
												System.out.println("getFxDerivs(): BUG: stw=terr_elev_sum_weights["+nScene+"]["+windx+"] ="+stw);
											}
										}
									}
								}
							}
						} // if (terr_elev_woi4 != null) {
						
						if (terr_elpix_woi4 != null) { // per-pixel terrain elevation. Make mutually-exclusive with terr_elev_woi4
							for (int wtindex = 0; wtindex < woi_terr_length; wtindex++) if ((terr_elpix_woi4[nScene] != null) && (terr_elpix_woi4[nScene][wtindex] != null)) {
								int wtx = wtindex % woi_terr.width;   // relative to woi_terr
								int wty = wtindex / woi_terr.width;   // relative to woi_terr
								int x = wtx + woi_terr.x;             // relative to full
								int y = wty + woi_terr.y;             // relative to full
								int npix = x + y * full.width;
								if (npix == dbg_findx) {
									System.out.println("getFxDerivs() 2a: findx="+npix);
								}
								if ((wtindex== dbg_wtindex) && (nScene ==dbg_scene)) {
									System.out.println("getFxDerivs() 2a: nScene="+nScene+", windx="+wtindex);
								}

								double terrain; // [woi_terr], adjusted/elevated terrain
								int ntpar = -1;
								if (terrain_parameters) {
									ntpar = par_index[TVAO_TERRAIN][npix]; 
									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];
								}
								int [] result_woi_ind =      terr_elpix_woi4[nScene][wtindex]; // list of output pixels influenced by each terrain pixel 
								if (result_woi_ind != null) { // now always
									double [] result_woi_weights = terr_elpix_weights4[nScene][wtindex];// same terrain pixel, per result neighbor
									for (int i = 0; i < result_woi_ind.length; i++) {
										int windx = result_woi_ind[i];
										if (windx >= 0) {
											double stw = terr_elpix_sum_weights[nScene][windx];
											if (stw > 0) {
												double w =  result_woi_weights[i]; ///  /stw; // moved division to the woi domain (single division)
												terrain_woi[windx] += w * terrain; // accumulate terrain contributors for the same terrain output pixel (wiil need division by  terrain_woi_weights)
												if (need_derivs) {
													if (ntpar >= 0) {
														contrib_thread.get(windx).add(ntpar); // full parameter index - this result pixel depends on this terrain parameter
														// derivative  of fX with respect to a specific terrain pixel
														dterrain_woi_dterrain[ntpar-ind_pars[TVAO_TERRAIN]][windx] += w; // /stw; // this fX pixel's terrain with respect to this terrain parameter
													}
													// derivatives per terrain elevation pixel  - separate, similar to elevation
													
													/*
													// calculate dterrain_woi_dtelev - derivative of terrain_woi[windx] with respect to terr_elev
													if (terr_elev_parameters) {
														dterrain_woi_dtelev[windx] +=
																terrain * (terr_elev_dweights4[nScene][wtindex][i] * stw -
																		w * terr_elev_dsum_weights[nScene][windx])/(stw * stw);
													}
													*/
												}
											} else {
												System.out.println("getFxDerivs(): BUG: stw=terr_elpix_sum_weights["+nScene+"]["+windx+"] ="+stw);
											}
										}
									}
								}
							}
						} // if (terr_elpix_woi4 != null) {
						
						
						for (int windx = 0; windx < woi_length; windx++) if (valid_terr_proj[windx]) {
							if ((windx== dbg_windex) && (nScene ==dbg_scene)) {
								System.out.println("getFxDerivs() nScene="+nScene+", windx="+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;
							if (npix == dbg_findx) {
								System.out.println("getFxDerivs() 3: findx="+npix);
							}
							double terrain = 0;
							double stw = 0;
							if (terr_elpix_parameters) { // almost always 
								stw = terr_elpix_sum_weights[nScene][windx];
							} else if (terr_elev_parameters) {
								stw = terr_elev_sum_weights[nScene][windx];
							}
							if (stw > 0) {
								terrain = terrain_woi[windx]/stw;// /terr_elev_sum_weights[nScene][windx]; // it already combines parameters and constants, divided by terr_elev_sum_weights[nScene][windx]
							}
							
							// Fill in fX for this scene
							int y_indx = y_wsrc[nScene][windx]; // y_indx is different for threads, no conflict
							if (y_indx == dbg_y_indx) {
								System.out.println("getFxDerivs() 1: y_indx="+y_indx);
							}
							double d = terrain; // combined from neighbors if needed
							
							if ((woi_veg != null) && has_vegetation[windx]) { // maybe is sufficient as it is always false   
								double sw = elev_sum_weights[nScene][windx];
								if (sw == 0) {
									anum_elev_err.getAndIncrement();
									if ((debug_level > -2) && (extra_data == null)){
										System.out.println("getFxDerivs(): BUG:  elev_sum_weights["+nScene+"]["+windx+"]=0. Restored with different overlaid?");
									}
								} else { // OK, non-zero
									double avg_veget = veget_woi[windx] / sw;
									double avg_alpha = alpha_woi[windx] / sw;
									if (Double.isNaN(avg_veget) || Double.isNaN(avg_alpha)) {
										System.out.println("getFxDerivs(): BUG: avg_veget="+avg_veget+", avg_alpha="+avg_alpha+",nScene="+nScene+", windx="+windx+". Restored with different overlaid?");
									} else {
										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 (extra_data != null) {
											if (extra_data.length > 0) {
												extra_data[0][y_indx] = k;
												if (extra_data.length > 1) {
													extra_data[1][y_indx] = terrain;
													if (extra_data.length > 2) {
														extra_data[2][y_indx] = terrain * (1.0 - k) + scene_offset;
														if (extra_data.length > 3) {
															extra_data[3][y_indx] = avg_veget * k + scene_offset;
															if (extra_data.length > 4) {
																extra_data[4][y_indx] = sw;
															}}}}}
										}

										if (need_derivs) {
											for (int npar: contrib_thread.get(windx)) {
												int typ_par = par_rindex[npar][0];
												if (typ_par == TVAO_TERRAIN) {
													//int rtpar = npar - ind_pars[TVAO_TERRAIN]
													jt[npar][y_indx] = dterrain_woi_dterrain[npar - ind_pars[TVAO_TERRAIN]][windx] * (1 - k) / stw;// sum_a; // d/dterrain
													if (npar == dbg_ntpar) {
														System.out.println("getFxDerivs() jt[t] 1: nScene="+nScene+", ntpar=" +(npar - ind_pars[TVAO_TERRAIN])+", jt["+npar+
																"]["+ y_indx+"]="+jt[npar][y_indx]+" k="+k);
													}
												}
											}
											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[TVAO_VEGETATION]; // 0-based index of the vegetation
													if((nvpar >= 0) && (nvpar < num_pars[TVAO_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[TVAO_ALPHA];
														if ((napar >= 0) && (napar < num_pars[TVAO_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);
												/// Which elevation parameters (full index) may have influenced this image pixel
												/// may have unrelated (from other scenes) but still a small number
												for (int npar:  epars_set) { // elevation parameters - full
													int nepar = npar-ind_pars[TVAO_ELEVATION];
													if ((nepar >=0 ) && (nepar < num_pars[TVAO_ELEVATION])) {
														int findx = par_rindex[npar][1]; // full image pixel;
														/// find vegetation for the pixel which elevation influences this one
														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);
															}
														}
														/// find alpha for the pixel which elevation influences this one
														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];
												        /*
												         * avg_veget, avg_alpha - weighted-averaged (divided by sw) from elevated contributors
												         * result pixel value d combining vegetation and terrain:
												         * ( 1) d = terrain * (1.0 - k) + avg_veget * k;
												         * derivative of the result pixel d with respect to iterated elevation parameter epar: 
												         * ( 2) dd_depar = k * davg_veget_depar +dk_depar * (avg_veget-terrain)
												         * derivative of k (vegetation contribution derived from alpha_avg) with respect
												         *  to the iterated elevation parameter epar: 
												         * ( 3) dk_depar = dk_dalpha_avg * dalpha_avg_depar
												         * ( 4) dk_dalpha_avg = 1.0 | 0.5 * PI * sin(avg_alpha * PI) (depending on mode)
												         * ( 5) avg_veget = sum(veget[epar]*w[epar])/sum(w[epar]), were [epar] means "corresponding to"
												         * ( 6) davg_veget_depar = d_(sum(veget[epar]*w[epar])/sum(w(epar)))_depar
												         * ( 7) davg_veget_depar = (d_(sum(veget[epar]*w[epar])_depar * sum(w[epar]) -  sum(veget[epar]*w[epar]) * d_(sum(w[epar]))_depar)/
												         * (sum(w[epar]) * sum(w[epar]))
												         * ( 8) sw = sum(w(epar)
												         * ( 9) dsw_dnepar =  d_(sum(w[epar]))_depar has only one term, so it is
												         * (10) dsw_dnepar =  d_w[epar]_depar 
												         * In d_(sum(veget[epar]*w[epar])_depar there is only one term (others do not depend on epar)
												         * so 
												         * (11) d_(sum(veget[epar]*w[epar])_depar = d_(veget[epar]*w[epar])_depar
												         * veget[epar] is just veget_src and does not depend on epar: 
												         * (12) d_(sum(veget[epar]*w[epar])_depar = veget_src * d_w[epar]_depar
												         * using (11) that dsw_dnepar has just one term:
												         * (13) d_(sum(veget[epar]*w[epar])_depar = veget_src * dsw_dnepar
												         * and (7) becomes
												         * (14) davg_veget_depar = (veget_src*dsw_dnepar*sw -sum(veget[epar]*w[epar])*dsw_dnepar)/(sw * sw) 
												         * using (5) 
												         * (15) davg_veget_depar = (veget_src*dsw_dnepar*sw - (avg_alpha*sw)*dsw_dnepar)/(sw * sw) 
												         * (16) davg_veget_depar = (veget_src*dsw_dnepar - veget_avg*dsw_dnepar)/sw
												         * (17) davg_veget_depar = dsw_dnepar(veget_src - veget_avg)/sw
												         * similarly if alpha >=0 and alpha <= 1.0:
												         * (18) davg_alpha_depar = dsw_dnepar(alpha_src - alpha_avg)/sw 
												         */ 
														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[TVAO_ELEVATION]="+num_pars[TVAO_ELEVATION]);
													}
												}
											} // if (elevation_parameters) {

											if (terr_elev_parameters) {
												contrib_thread.get(windx).add(ind_pars[TVAO_TERR_ELEV]); // full parameter index - this result pixel depends on this terrain parameter
												jt[ind_pars[TVAO_TERR_ELEV]][y_indx] += dterrain_woi_dtelev[windx] * (1 - k);// sum_a; // d/dterrain
											} // if (terr_elev_parameters) {
											
											if (terr_elpix_parameters && !fits_disable[TVAO_TERR_ELEV_PIX]) { // skip if derivatives not needed
												/// here - just derivatives as direct influence of elevations is already processed
												/// we have dsw_delevations (for all elevation parameters) - ,
												HashSet<Integer> tepars_set =  terr_elpix_contribs.get(windx);
												/// Which terrain elevation parameters (full index) may have influenced this image pixel
												/// may have unrelated (from other scenes) but still a small number
												for (int npar:  tepars_set) { // elevation parameters - full
													int ntepar = npar-ind_pars[TVAO_TERR_ELEV_PIX];
													if ((ntepar >=0 ) && (ntepar < num_pars[TVAO_TERR_ELEV_PIX])) {
														int findx = par_rindex[npar][1]; // full image pixel;
														/// find vegetation for the pixel which elevation influences this one
														double terr_src = tvao[TVAO_TERRAIN][findx];
														if (terrain_parameters) {
															int terr_indx = par_index[TVAO_TERRAIN][findx];
															if (terr_indx >=0) {
																terr_src=vector[terr_indx];
															} else {
																System.out.println("getFxDerivs(): BUG: par_index["+TVAO_TERRAIN+"]["+findx+"]="+terr_indx);
															}
														}
														double dsw_dntepar = terr_elpix_dsum_weights[nScene][ntepar][windx];
														/*
														 * terr_avg - weighted-averaged (divided by stw) from elevated terrain contributors
														 * result pixel value d combining vegetation and terrain:
														 * ( 1) d = terr_avg * (1.0 - k) + avg_veget * k;
														 * derivative of the result pixel d with respect to iterated elevation parameter tepar: 
														 * ( 2) dd_depar = (1.0 - k) * davg_terr_dtepar
														 * ( 5) terr_avg = sum(terrain[tepar]*w[tepar])/sum(w[tepar]), were [tepar] means "corresponding to"
														 * ( 6) davg_terr_dtepar = d_(sum(terr[tepar]*w[tepar])/sum(w(tepar)))_dtepar
														 * ( 7) davg_terr_dtepar = (d_(sum(terr[epar]*w[tepar])_dtepar * sum(w[tepar]) -  sum(terr[tepar]*w[tepar]) * d_(sum(w[tepar]))_dtepar)/
														 * (sum(w[tepar]) * sum(w[tepar]))
														 * ( 8) stw = sum(w(tepar)
														 * ( 9) dsw_dntepar = d_(sum(w[tepar]))_dtepar has only one term, so it is
														 * (10) dsw_dntepar = d_w[tepar]_dtepar 
														 * In d_(sum(terr[tepar]*w[tepar])_dtepar there is only one term (others do not depend on tepar)
														 * so 
														 * (11) d_(sum(terr[tepar]*w[tepar])_dtepar = d_(terr[tepar]*w[tepar])_dtepar
														 * terr[tepar] is just terr_src and does not depend on tepar: 
														 * (12) d_(sum(terr[tepar]*w[tepar])_dtepar = terr_src * d_w[tepar]_dtepar
														 * using (11) that dsw_dtnepar has just one term:
														 * (13) d_(sum(terr[tepar]*w[tepar])_dtepar = terr_src * dsw_dtnepar
														 * and (7) becomes
														 * (14) davg_terr_dtepar = (terr_src*dsw_dtnepar*stw -sum(terr[tepar]*w[tepar])*dsw_dtnepar)/(stw * stw) 
														 * using (5) 
														 *
														 * (16) davg_terr_dtepar = (terr_src*dsw_dnepar - terr_avg*dsw_dnepar)/sw
														 * (17) davg_terr_dtepar = dsw_dntepar(terr_src - terr_avg)/sw
														 */ 
														double davg_terrain_dtepar= dsw_dntepar/stw * (terr_src - terrain);
														double dd_depar = (1 - k) * davg_terrain_dtepar;
									                    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+" -> ntepar="+ntepar+" >= num_pars[TVAO_TERR_ELEV_PIX]="+num_pars[TVAO_TERR_ELEV_PIX]);
													}
												}

											} // if (terr_elpix_parameters) {
										}
									}
								}
							} else { // terrain-only pixel
								d = terrain;
								if (need_derivs) {
									for (int npar: contrib_thread.get(windx)) {
										if (par_rindex[npar][0] == TVAO_TERRAIN) {
											jt[npar][y_indx] = dterrain_woi_dterrain[npar - ind_pars[TVAO_TERRAIN]][windx]/stw;
											if (npar == dbg_ntpar) {
												System.out.println("getFxDerivs() jt[t] 2: nScene="+nScene+", jt["+npar+
														"]["+ y_indx+"]="+jt[npar][y_indx]);
											}
											
										}
									}
									if (terr_elev_parameters) {
										contrib_thread.get(windx).add(ind_pars[TVAO_TERR_ELEV]); // full parameter index - this result pixel depends on this terrain parameter
										jt[ind_pars[TVAO_TERR_ELEV]][y_indx] += dterrain_woi_dtelev[windx];// sum_a; // d/dterrain
									}
									if (terr_elpix_parameters && !fits_disable[TVAO_TERR_ELEV_PIX]) { // skip if derivatives are not needed
										HashSet<Integer> tepars_set =  terr_elpix_contribs.get(windx);
										for (int npar:  tepars_set) { // elevation parameters - full
											int ntepar = npar-ind_pars[TVAO_TERR_ELEV_PIX];
											if ((ntepar >=0 ) && (ntepar < num_pars[TVAO_TERR_ELEV_PIX])) {
												int findx = par_rindex[npar][1]; // full image pixel;
												double terr_src = tvao[TVAO_TERRAIN][findx];
												if (terrain_parameters) {
													int terr_indx = par_index[TVAO_TERRAIN][findx];
													if (terr_indx >=0) {
														terr_src=vector[terr_indx];
													} else {
														System.out.println("getFxDerivs(): BUG2: par_index["+TVAO_TERRAIN+"]["+findx+"]="+terr_indx);
													}
												}
												double dsw_dntepar = terr_elpix_dsum_weights[nScene][ntepar][windx];
												double davg_terrain_dtepar= dsw_dntepar/stw * (terr_src - terrain);
							                    jt[npar][y_indx] = davg_terrain_dtepar;
							                    contrib_thread.get(windx).add(npar); // full parameter index, elevation
											} else { // bug
												System.out.println("getFxDerivs(): BUG2: npar="+npar+" -> ntepar="+ntepar+" >= num_pars[TVAO_TERR_ELEV_PIX]="+num_pars[TVAO_TERR_ELEV_PIX]);
											}
										}
									} // if (terr_elpix_parameters) {
								}
							    if (extra_data != null) {
							    	double k = 0.0;
							        if (extra_data.length > 0) {
							            extra_data[0][y_indx] = k;
							            if (extra_data.length > 1) {
							                extra_data[1][y_indx] = terrain;
							                if (extra_data.length > 2) {
							                    extra_data[2][y_indx] = terrain * (1.0 - k) + scene_offset;
							                    if (extra_data.length > 3) {
							                        extra_data[3][y_indx] = scene_offset; // avg_veget * k + scene_offset;
							                        if (extra_data.length > 4) {
							                            extra_data[4][y_indx] = 0;//sw;
							                        }}}}}
							    }
							}
							fX[y_indx] = d + scene_offset;
							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);
	
//		anum_elev_err.getAndIncrement();
		if ((debug_level > -2) && (anum_elev_err.get() > 0)) {
			System.out.println("getFxDerivs(): anum_elev_err="+anum_elev_err.get());
		}
		
		// process hi-frequency (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_terr_proj[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 (samples_pointers[SAMPLES_Y_HF][1]>0) { // 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 < samples_pointers[SAMPLES_Y_HF][1]; n = ai.getAndIncrement()) {
							int n_hf = n+samples_pointers[SAMPLES_Y_HF][0]; // 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_terr_proj[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 (samples_pointers[SAMPLES_SCENES][1]>0) { // use_scenes_pull0) { // single sample after differences
			int ind_scenes = samples_pointers[SAMPLES_SCENES][0];
			fX[ind_scenes] = 0.0; // ind_next] =0.0;
			for (int n = 0; n < num_pars[TVAO_SCENE_OFFSET]; n++) { // only count adjustable scene offsets
				int np = ind_pars[TVAO_SCENE_OFFSET] + n; // index of the alpha parameter
				int nscene = par_rindex[np][1];
				double sw=scene_weights[nscene] * scale_scenes_pull;
				fX[ind_scenes] += sw * vector[np];
				if (need_derivs) {
					jt[np][ind_scenes] = sw;
				}
			}
//			ind_next++;
		} // if (use_scenes_pull0) { // single sample after differences
		
		if (samples_pointers[SAMPLES_Y_AVG][1]>0) { //
			final double [][]   fX_avg_thread = new double [threads.length][woi_length];
			final double [][][] jt_avg_thread = (jt != null) ? new double [threads.length][jt.length][woi_length] : null;
			final boolean debug_print = (debug_level >100);
			int dbg_windx = -246;
			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 yIndx = ai.getAndIncrement(); yIndx < y_src.length; yIndx = ai.getAndIncrement()){
							int nscene = y_src[yIndx][YSRC_SCENE];
							int findx =   y_src[yIndx][YSRC_FINDEX];
							int windx =  getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
							if (windx == dbg_windx) {
								System.out.println("getFxDerivs() SAMPLES_Y_AVG-1 windx="+windx);
							}
							if (debug_print &&  (windx == dbg_windx)) {
								System.out.println("yIndx="+yIndx+", nscene="+nscene+", indx="+findx+", windx="+windx+", fX[yIndx]="+fX[yIndx]);
							}
							double w = recalc_average ?  weights[yIndx]/weight_to_scene_weight: scene_weights[nscene]; // norm_scene_weights[nscene];
							w *= terrain_correction; 
							fX_avg_thread[nthread][windx] += w * fX[yIndx]; // already calculated
							if (jt_avg_thread != null) {
								HashSet<Integer> pars_contribs =  param_contribs.get(windx); // contributors before laplacians
								for (int np: pars_contribs) {
									jt_avg_thread[nthread][np][windx] += w * jt[np][yIndx];
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			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()){
							int nx = samples_pointers[SAMPLES_Y_AVG][0]+wIndx;
							if (wIndx == dbg_windx) {
								System.out.println("getFxDerivs() SAMPLES_Y_AVG-2 windx="+wIndx);
							}
							for (int nthread = 0; nthread < threads.length; nthread++) {
								fX[nx] += fX_avg_thread[nthread][wIndx];
								if (jt_avg_thread != null) {
									HashSet<Integer> pars_contribs =  param_contribs.get(wIndx); // contributors before laplacians
									for (int np: pars_contribs) {
										jt[np][nx] += jt_avg_thread[nthread][np][wIndx];
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		
		// splitting alpha_lpf from alpha_loss+alpha_push
		// No need here to add contributors as this should not depend on any other parameters 
		if (samples_pointers[SAMPLES_ALPHA_PULL][1] > 0) { // fits[TVAO_ALPHA] &&  ((alpha_loss > 0) || (alpha_push > 0))) {
			int dbg_nx = -120155; // 30578; // -120114; // -76340;
			final boolean dbg_old = false;
//			final double alpha_threshold = alpha_offset + alpha_min_veg *(1 - 2*alpha_offset);
//			final double alpha_threshold = alpha_0offset + alpha_min_veg *(1 - alpha_offset - alpha_0offset);
			final double alpha_plateau = alpha_threshold; //  * alpha_loss;
			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[TVAO_ALPHA]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ALPHA_PULL][2]; // index of the alpha parameter
							int nx = n + samples_pointers[SAMPLES_ALPHA_PULL][0]; //  y_vector.length; // x - index
							if (nx == dbg_nx) {
								System.out.println("getFxDerivs(): n="+n+", nx="+nx);
							}
							double dlin = 0;
							double dquad = 0;
							double ddlin = 0;
							double ddquad = 0;
//							double dd = 0;
							fX[nx] = 0.0;
							if (alpha_loss > 0) {
								double alpha = vector[np];
								if (alpha < alpha_0offset) {
									double d = alpha - alpha_0offset; // negative
									dquad =   - d * d;               // negative 
									ddquad =  - 2 * d;               // positive 
									dlin =      d;                   // negative
									ddlin =     1;                   // positive
								} else if (alpha < alpha_threshold) {
									dlin = (alpha - alpha_0offset);   // positive
									ddlin = 1;                       // positive
								} else if (alpha <= (1 - alpha_offset)) {
									dlin = alpha_plateau;
								} else { // if (alpha > (1 - alpha_offset))
									double d = alpha - (1.0 - alpha_offset); // positive
									dlin = d + alpha_plateau;                // positive
									ddlin = 1;                               // positive   
									dquad = d * d;                           // positive  
									ddquad = 2 * d;                          // positive
								}
								
								fX[nx] = dquad * alpha_loss + dlin * alpha_loss_lin;
								if (jt != null) {
									jt[np][nx] = ddquad * alpha_loss + ddlin * alpha_loss_lin; // d/dalpha[i]		
								}
								if (dbg_old) { 
									// old version 
									double d = 0;
									if (alpha < alpha_0offset) {
										d = alpha- alpha_0offset;
									} 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]		
										}							
									}
								}
								
							}
							if (alpha_push > 0) {
								// average including center:
								
								double sum_w = 1; // (nn + alpha_push_center);
								double avg_c = vector[np];
								
								double one_minus_n = 1.0 - alpha_push_neutral;
//								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]	
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		} // if ((alpha_loss > 0) || (alpha_push > 0)){
		
		if (samples_pointers[SAMPLES_ALPHA_LPF][1] > 0) { // fits[TVAO_ALPHA] && (alpha_lpf >= 0)) {
			int dbg_nx = -76340;
			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[TVAO_ALPHA]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ALPHA_LPF][2]; // index of the alpha parameter
							int nx = n + samples_pointers[SAMPLES_ALPHA_LPF][0]; //  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;
							int nn_vector = 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++;
									nn_vector++;
								} 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 (nn_vector < 4) {
								effective_alpha_lpf = alpha_lpf_border;
							}
							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 (samples_pointers[SAMPLES_TERRAIN_PULL][1]>0) { // fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
			final double [] offsets_sum_threaded = new double [threads.length];
			final AtomicInteger anum=new AtomicInteger(0);
			double offsets_sum = 0;
			if (terr_pull_avg > 0) { // TODO: need to join all terrain samples
				// calculate average offset between current and initial terrain pixels
				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 n = ai.getAndIncrement(); n < num_pars[TVAO_TERRAIN]; n = ai.getAndIncrement()) {
								int np = n + samples_pointers[SAMPLES_TERRAIN_PULL][2]; 
								int nx = n + samples_pointers[SAMPLES_TERRAIN_PULL][0];
								int findx =  par_rindex[np][1];
								double terr_pull = terrain_average[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
								if (Double.isNaN(terr_pull)) {
									terr_pull= 0; // should not happen
								}
								offsets_sum_threaded[nthread] += vector[np] - terr_pull;
								anum.getAndIncrement();
							}
						}
					};
				}
				ImageDtt.startAndJoin(threads);
				for (double d :offsets_sum_threaded) {
					offsets_sum += d;
				}
			}
			final int num = anum.get(); 
//			final double offset_avg; 
			terrain_offset = (num > 0)? (offsets_sum/num) : 0;
			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[TVAO_TERRAIN]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_TERRAIN_PULL][2]; 
							int nx = n + samples_pointers[SAMPLES_TERRAIN_PULL][0];
							boolean pull_up = false; 
							int findx =  par_rindex[np][1];
							double terr_pull = terrain_average[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
							if (Double.isNaN(terr_pull)) {
								terr_pull= 0; // should not happen
							}
							if ((terr_pull0 > 0) || (terr_pull_up > 0)) {
								double diff = vector[np] - terr_pull;
								pull_up = vector[np] < terr_pull;
								fX[nx] += (pull_up ? terr_pull_up : terr_pull0) * diff;
							}
							if (jt != null) {
								jt[np][nx] += pull_up ? terr_pull_up : terr_pull0; // terr_pull0;
							}
							if (terr_pull_avg >0) { // TODO: maybe apply sign of difference here?
								double diff = vector[np] - (terr_pull + terrain_offset);
								fX[nx] +=  terr_pull_avg * diff;
								if (jt != null) {
									jt[np][nx] += terr_pull_avg; // terr_pull0;
									// now apply to all (others and this) derivative form offset_avg
									double dd = terr_pull_avg / num;
									for (int n_other = 0; n_other < num_pars[TVAO_TERRAIN];  n_other++) {
										int np_other = n_other + samples_pointers[SAMPLES_TERRAIN_PULL][2]; 
										jt[np_other][nx] -= dd; // terr_pull0;
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		if (samples_pointers[SAMPLES_TERRAIN_LPF][1]>0) { // fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
			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[TVAO_TERRAIN]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_TERRAIN_LPF][2];
							int nx = n + samples_pointers[SAMPLES_TERRAIN_LPF][0];
							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 (jt != null) {
									jt[np][nx] += terr_lpf;
									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 (samples_pointers[SAMPLES_VEGETATION_PULL][1] > 0) { // fits[TVAO_VEGETATION] && (veget_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and 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[TVAO_VEGETATION]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_VEGETATION_PULL][2];
							int nx = n + samples_pointers[SAMPLES_VEGETATION_PULL][0];
							double d = 0;
							if (veget_pull0 > 0) {
								int findx =  par_rindex[np][1];
								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
								}
								// find alpha
								boolean holes_flat = false; // true;
								double delta_veget = vector[np] - veget_pull;
								double pull = veget_pull0;
								if (veget_pull_low_alpha > 0) {
									double scaled_pull =  veget_pull_low_alpha * veget_pull0;
									int npa = par_index[TVAO_ALPHA][findx]; // parameter for same-pixel alpha
//									int na = npa - ind_pars[TVAO_ALPHA];
									if (npa < 0) {
										System.out.println("getFxDerivs(): BUG: no alpha for vegetation, n="+n+", np="+np+", nx ="+nx+
												" findx="+findx+", npa="+npa);
									} else {
										double alpha = vector[npa];
										if (alpha < alpha_threshold_pull) {
											double k = (alpha_threshold_pull-alpha)/ alpha_threshold_pull;
											double extra_pull;
											if (holes_flat) {
												extra_pull = scaled_pull;
											} else {
												extra_pull = scaled_pull * Math.pow(k, alpha_pull_pwr);
												if (jt != null) { // d/d_alpha
													jt[npa][nx] += -scaled_pull * alpha_pull_pwr * delta_veget * Math.pow(k, alpha_pull_pwr - 1) / alpha_threshold_pull;
												}
											}
											pull = veget_pull0 + extra_pull;
										}
									}
								}
								fX[nx] += pull * delta_veget; // (vector[np] - veget_pull);
								if (jt != null) {
									jt[np][nx] += pull;
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}

		if (samples_pointers[SAMPLES_VEGETATION_LPF][1] > 0) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and terrain)
			if  (num_pars[TVAO_ALPHA] <= 0) {
				System.out.println("getFxDerivs(): BUG: Asjusting SAMPLES_VEGETATION_LPF requires alpha parameters aslo to be fitted");
			} else {
				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[TVAO_VEGETATION]; n = ai.getAndIncrement()) {
								int np = n + samples_pointers[SAMPLES_VEGETATION_LPF][2];
								int nx = n + samples_pointers[SAMPLES_VEGETATION_LPF][0];
								int findx = par_rindex[np][1];//
								int npa = par_index[TVAO_ALPHA][findx]; // parameter for same-pixel alpha
								int na = npa - ind_pars[TVAO_ALPHA];
								if (npa < 0) {
									System.out.println("getFxDerivs(): BUG: no alpha for vegetation, n="+n+", np="+np+", nx ="+nx+
											" findx="+findx+", npa="+npa);
									continue;
								}
								double d = 0;
								if (veget_lpf > 0) {
									double veget_avg4 = 0;
									int veget_nn4 = 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
											veget_avg4+=d;
											veget_nn4++;
										} else if ((di < -1) && lpf_fixed[TVAO_VEGETATION]) {
											d = tvao[TVAO_VEGETATION][-di - 2]; 
											veget_avg4+=d;
											veget_nn4++;
										}
									}
									if (veget_nn4 <= 0) {
										System.out.println("getFxDerivs(): BUG: veget_nn4="+veget_nn4+", n="+n+", np="+np+", nx ="+nx+
												" findx="+findx+", npa="+npa);
										continue;
									}
									veget_avg4 /= veget_nn4; // average
									double veget_lap = vector[np] - veget_avg4;
									if (veget_lap >=0) { // only for cold spots
										continue;
									}
									d = vector[npa]; // alpha
									double alpha_avg5; 
									if (alpha_piece_linear) {
										alpha_avg5 = (d < 0)? 0: ((d > 1) ? 1.0: d);	
									} else {
										alpha_avg5 = (d < 0)? 0: ((d > 1) ? 1.0: 0.5 * (1.0 - Math.cos(d*Math.PI)));	
									}
									double alpha_w5 = 1.0;
									for (int i = 0; i < alpha_neibs[n].length; i++) { // now 4, may be increased
										int di =  alpha_neibs[na][i];
										d=0;
										if (di >= 0) {
											d = vector[di];  // d - full parameter index
										} else if ((di < -1) && lpf_fixed[TVAO_ALPHA]) {
											d = tvao[TVAO_ALPHA][-di - 2]; 
										} else {
											continue; // do not add weight
										}
										double alpha;

										if (alpha_piece_linear) {
											alpha = (d < 0)? 0: ((d > 1) ? 1.0: d);	
										} else {
											alpha = (d < 0)? 0: ((d > 1) ? 1.0: 0.5 * (1.0 - Math.cos(d*Math.PI)));	
										}
										alpha_avg5 += alpha * w_alpha_neib;
										alpha_w5 += w_alpha_neib;
										// TODO: calculate derivatives
									}
									// limit to 0..1 and apply cos
									alpha_avg5 /= alpha_w5; // average
									if (alpha_avg5 > alpha_threshold) {
										alpha_avg5 = (alpha_avg5 - alpha_threshold) / (1.0 - alpha_threshold);
										double alpha_pwr =  Math.pow(alpha_avg5, holes_pwr);
										double val = veget_lap * alpha_pwr; 
										fX[nx] += veget_lpf * val;

										if (jt != null) {
											jt[np][nx] += veget_lpf * alpha_pwr;
											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* alpha_pwr/veget_nn4;
												}
											}
											/* dval_dai = dval_dalpha_pwr * dalpha_pwr_dalpha * dalpha_dfai * dfai_dai; */
											double dval_dalpha_pwr = veget_lpf * veget_lap;
											double dalpha_pwr_dalpha = holes_pwr* Math.pow(alpha_avg5, holes_pwr - 1);
											double dval_dalpha = dval_dalpha_pwr * dalpha_pwr_dalpha;
///											double dalpha_dai = 1.0 / alpha_w5; // dval_dalpha / alpha_w5;
											double dalpha_dai = 1.0 / alpha_w5 / (1.0 - alpha_threshold); // dval_dalpha / alpha_w5;
											if ((vector[npa] > 0) && (vector[npa] < 1.0)) {
												if (!alpha_piece_linear) {
													dalpha_dai *= 0.5 * Math.PI * Math.sin(vector[npa]*Math.PI);
												}
												jt[npa][nx] += dval_dalpha * dalpha_dai;
											}
											for (int i = 0; i < alpha_neibs[na].length; i++) { // now 4, may be increased
												int di =  alpha_neibs[na][i];
												if (di >= 0) {
													if ((vector[di] > 0) && (vector[di] < 1.0)) {
														//													dalpha_dai = dval_dalpha * w_alpha_neib / alpha_w5;
///														dalpha_dai = w_alpha_neib / alpha_w5;
														dalpha_dai = w_alpha_neib / alpha_w5 / (1.0 - alpha_threshold);
														if (!alpha_piece_linear) {
															dalpha_dai *= 0.5 * Math.PI * Math.sin(vector[di]*Math.PI);
														}
														jt[di][nx] += dval_dalpha * dalpha_dai;
													}
												}
											}
										}
									}
								}
							}
						}
					};
				}
				ImageDtt.startAndJoin(threads);
			}
		}
		
		if (samples_pointers[SAMPLES_ELEVATION_PULL][1]>0) { // fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and 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[TVAO_ELEVATION]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ELEVATION_PULL][2];
							int nx = n + samples_pointers[SAMPLES_ELEVATION_PULL][0];
//							double d = 0;
							if (elevation_pull0 > 0) {
								double eff_elevation_pull0 = elevation_pull0;
								int findx =  par_rindex[np][1];
								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
								}
								if (elev_scale_pull && (elevation_pull > elev_scale_thresh)) {
									eff_elevation_pull0 *= elev_scale_thresh /elevation_pull;
								}
								fX[nx] += eff_elevation_pull0 * (vector[np] - elevation_pull);
								if (jt != null) {
									jt[np][nx] += eff_elevation_pull0;
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		
		if (samples_pointers[SAMPLES_ELEVATION_LPF][1]>0) { // fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and 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[TVAO_ELEVATION]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ELEVATION_LPF][2];
							int nx = n + samples_pointers[SAMPLES_ELEVATION_LPF][0];
							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 vegetation
								double eff_elevation_lpf = elevation_lpf;
								if (elev_scale_lpf) {
									int findx = par_rindex[np][1];
									double elevation_pull = tvao[TVAO_ELEVATION][findx]; // vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
									if  (elevation_pull > elev_scale_thresh) {
										eff_elevation_lpf *= elev_scale_thresh /elevation_pull;
									}
								}
								fX[nx] += eff_elevation_lpf * (vector[np] - avg); //   + veget_pull0 * vector[np];
								if (jt != null) {
									jt[np][nx] += eff_elevation_lpf;
									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] -= eff_elevation_lpf/nn;
										}
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		// loss for low elevation (interfering with the terrain) and high alpha
		if ((samples_pointers[SAMPLES_ELEVATION_ALPHA][1]>0) && elev_alpha_en) { // fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) { // should be positive for pull0 and terr_pull_cold (difference between vegetation and 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[TVAO_ELEVATION]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ELEVATION_ALPHA][2];
							int nx = n + samples_pointers[SAMPLES_ELEVATION_ALPHA][0];
							if (elev_alpha > 0) {
								int findx =  par_rindex[np][1];
								int ap = par_index[TVAO_ALPHA][findx];
								if (ap >= 0) { // normally always
									double elev = vector[np];
									if (elev < low_veget) {
										double alpha = vector[ap];
										if (alpha > 0) {
											double alpha_pwr =  Math.pow(alpha, elev_alpha_pwr);
											fX[nx] += elev_alpha * (low_veget - elev) * alpha_pwr;
											if (jt != null) {
												jt[np][nx] += -elev_alpha * alpha_pwr;
												jt[ap][nx] +=  elev_alpha * (low_veget - elev) * elev_alpha_pwr * alpha_pwr/alpha;
											}
										}
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}

		if ((samples_pointers[SAMPLES_TERR_ELEV_PULL][1]>0) && (terr_elev_pull0 > 0) && terr_elpix_parameters) {
			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[TVAO_TERR_ELEV_PIX]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_TERR_ELEV_PULL][2];
							int nx = n + samples_pointers[SAMPLES_TERR_ELEV_PULL][0];
							if (terr_elev_pull0 > 0) {
								double eff_terr_elev_pull0 = terr_elev_pull0;
								int findx =  par_rindex[np][1];
								double terr_elev_pull = tvao[TVAO_TERR_ELEV_PIX][findx]; // vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
								if (Double.isNaN(terr_elev_pull)) {
									terr_elev_pull= 0; // should not happen unless too far from the vegetation
								}
///								if (elev_scale_pull && (terr_elev_pull > elev_scale_thresh)) {
///									eff_terr_elev_pull0 *= elev_scale_thresh / terr_elev_pull;
///								}
								fX[nx] += eff_terr_elev_pull0 * (vector[np] - terr_elev_pull);
								if (jt != null) {
									jt[np][nx] += eff_terr_elev_pull0;
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		if ((samples_pointers[SAMPLES_TERR_ELEV_LPF][1]>0)  && (terr_elev_lpf > 0) && terr_elpix_parameters) {
			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[TVAO_TERR_ELEV_PIX]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_TERR_ELEV_LPF][2];
							int nx = n + samples_pointers[SAMPLES_TERR_ELEV_LPF][0];
							double d = 0;
							if (terr_elev_lpf > 0) {
								double avg = 0;
								int nn = 0;
								for (int i = 0; i < terr_elev_neibs[n].length; i++) { // now 4, may be increased
									int di =  terr_elev_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_TERR_ELEV_PIX]) {
										d = tvao[TVAO_TERR_ELEV_PIX][-di - 2]; 
										avg+=d;
										nn++;
									}
								}
								avg /= nn; // average vegetation
								double eff_terr_elev_lpf = terr_elev_lpf;
///								if (elev_scale_lpf) {
///									int findx = par_rindex[np][1];
///									double terr_elev_pull = tvao[TVAO_TERR_ELEV_PIX][findx]; // vegetation_pull[findx]; // maybe use tvao[TVAO_TERRAIN][findx] ?
///									if  (terr_elev_pull > elev_scale_thresh) {
///										eff_terr_elev_lpf *= elev_scale_thresh /terr_elev_pull;
///									}
///								}
								fX[nx] += eff_terr_elev_lpf * (vector[np] - avg); //   + veget_pull0 * vector[np];
								if (jt != null) {
									jt[np][nx] += eff_terr_elev_lpf;
									for (int i = 0; i < terr_elev_neibs[n].length; i++) { // now 4, may be increased
										int di =  terr_elev_neibs[n][i];
										if (di >= 0) {
											jt[di][nx] -= eff_terr_elev_lpf/nn;
										}
									}								
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		
		// create pivot table 
		if (need_derivs) {
			param_lists = new int [par_rindex.length][];
			addRelatedFromSets(
					param_lists, // final int [][] par_relations,
					0, // final int par_index,
					param_lists.length, // final int par_number,
					contrib_combo); // final ArrayList<HashSet<Integer>> sets) {
			if (debug_level>4) 	showPivot(param_lists, "pivot-params-from-sets");

			param_lists = addNeighborsLists(
					param_lists, // final int [][] pars,
					neibs_neibs); // final int [][] par_neibs) 
			if (debug_level>4) 	showPivot(param_lists, "pivot-params-with-neibs");
			
			param_pivot = listsToPivot(
					param_lists); // final int [][] par_lists)
			
			if ((samples_pointers[SAMPLES_TERRAIN_PULL][1]>0) &&  (terr_pull_avg > 0)) {
			// make all terrain pixels to depend on each other
				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[TVAO_TERRAIN]; n = ai.getAndIncrement()) {
								int np = n + samples_pointers[SAMPLES_TERRAIN_PULL][2];
								for (int n_other = n+1; n_other < num_pars[TVAO_TERRAIN]; n_other++) {
									int np_other = n_other + samples_pointers[SAMPLES_TERRAIN_PULL][2];
									param_pivot[np][np_other] = true;
									param_pivot[np_other][np] = true;
								}
							}
						}
					};
				}
				ImageDtt.startAndJoin(threads);
			}
			if (debug_level>4) 	showPivot(param_pivot, "pivot-all");
		}
		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 boolean 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;
			}
		}
		int num_diff = 0;
		int num_non0 = 0;
		for (int i = 0; i < size; i++) {
			for (int j = i+1; j < size; j++) {
				if (dbg_img[i*size+j] != 0) {
					num_non0++;
				}
				if (dbg_img[i*size+j] != dbg_img[j*size+i]) {
					num_diff++;
				}
			}
		}
		if (num_diff != 0) {
			System.out.println("showPivot(boolean[][]): not-symmetrical, number of mismatches is "+num_diff);
		}
		System.out.println("showPivot(boolean[][]): num non-zero "+num_non0+" ("+(100.0*num_non0/size/size)+"%)");
		
		ShowDoubleFloatArrays.showArrays(
				dbg_img,
				title);
	}
	
	public static void showPivot(
			double [][] jtj,
			String title) {
		int size = jtj.length;
		if (jtj[0].length != size) {
			throw new IllegalArgumentException("showPivot(): Expecting square double array, got "+
					jtj[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] = (jtj[i][j]!=0) ? 1.0 : 0.0;
			}
		}
		
		int num_diff = 0;
		int num_non0 = 0;
		for (int i = 0; i < size; i++) {
			for (int j = i+1; j < size; j++) {
				if (dbg_img[i*size+j] != 0) {
					num_non0++;
				}
				if (dbg_img[i*size+j] != dbg_img[j*size+i]) {
					num_diff++;
				}
			}
		}
		if (num_diff != 0) {
			System.out.println("showPivot(double[][]): not-symmetrical, number of mismatches is "+num_diff);
		}
		System.out.println("showPivot(double[][]): num non-zero "+num_non0+" ("+(100.0*num_non0/size/size)+"%)");
		
		ShowDoubleFloatArrays.showArrays(
				dbg_img,
				title);
	}

	
	
	public static void showPivot( 
			int [][] neibs_neibs,
			String title) {
		int size = neibs_neibs.length;
		
		double [] dbg_img = new double [size*size];
		for (int i = 0; i < size; i++) {
			if (neibs_neibs[i] != null) {
				for (int j : neibs_neibs[i]) {
					dbg_img[i*size+j] = 1.0;
				}
			}
		}
		int num_diff = 0;
		int num_non0 = 0;
		for (int i = 0; i < size; i++) {
			for (int j = i+1; j < size; j++) {
				if (dbg_img[i*size+j] != 0) {
					num_non0++;
				}
				if (dbg_img[i*size+j] != dbg_img[j*size+i]) {
					num_diff++;
				}
			}
		}
		if (num_diff != 0) {
			System.out.println("showPivot(int[][]): not-symmetrical, number of mismatches is "+num_diff);
		}
		System.out.println("showPivot(int[][]): num non-zero "+num_non0+" ("+(100.0*num_non0/size/size)+"%)");
		
		ShowDoubleFloatArrays.showArrays(
				dbg_img,
				title);
	}

	public static int checkSquare( 
			int [][] neibs_neibs,
			String title) {
		int size = neibs_neibs.length;
		
		double [] dbg_img = new double [size*size];
		for (int i = 0; i < size; i++) {
			if (neibs_neibs[i] != null) {
				for (int j : neibs_neibs[i]) {
					dbg_img[i*size+j] = 1.0;
				}
			}
		}
		int num_diff = 0;
		for (int i = 0; i < size; i++) {
			for (int j = i+1; j < size; j++) {
				if (dbg_img[i*size+j] != dbg_img[j*size+i]) {
					num_diff++;
				}
			}
		}
		return num_diff;
	}
	
	
	
	
	
	private double [][] getFxDerivsDelta(
			double []         vector,
			final double      delta,
			final int         debug_level) {
		int dbg_n = 2524; // -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;
	}

	/**
	 * 
	 * @param extra
	 * @param gap
	 * @return {0,parameter_index}, {1, fX index} {2 +SAVE_TYPES[n],full index} 
	 */
	private int [][] getParamDebugIndices(
			boolean extra,
			int gap) {
		int num_img_cols = 6; // 5; // 4; // SAVE_TYPES.length 
		int num_first_row_cols = num_img_cols - 1; //==5 4; // SAVE_TYPES.length-1;
		int num_scenes_ext = num_scenes + 1;       // include terr_elev single-pix with scenes
		int woi_width= woi_max.width * num_img_cols + (num_img_cols-1) * gap;
		int num_scene_rows = (int) Math.ceil(1.0 * num_scenes_ext / woi_width); // some scenes have NaN
		int terr_elev_indx = ((num_scenes_ext / woi_width) + 1) * woi_width - 1;  // location of average, not per-pixel 
		int num_extra_rows = extra ? (2 *woi_max.height + 3 * gap) : 0;
		int main_hight = woi_max.height  + gap+ num_scene_rows;
		int extra_start = main_hight+gap;
		int [][] indices = new int [woi_width * (main_hight + num_extra_rows)][2];
		for (int i = 0; i < indices.length; i++) {
			indices[i][0] = -1; // unused
		}
		for (int h = 0; h < woi_max.height; h++) {
			for (int t = 0; t < num_first_row_cols; t++) { // column number
				for (int w = 0; w < woi_max.width; w++) {
					int indx = (h + woi_max.y) * full.width + (w + woi_max.x);
					// Add terrain elevation !!!
					int pindx = par_index[SAVE_TYPES[t]][indx];
					int indices_pix = (h * woi_width) + (woi_max.width + gap) * t + w;
					if (pindx >= 0) {
						indices [indices_pix][0] = 0;
						indices [indices_pix][1] = pindx;
					} else {
						// Add terrain elevation !!!
						indices [indices_pix][0] = SAVE_TYPES[t] + 2; // 1; // type + 1
						indices [indices_pix][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
			int indices_pix = ((woi_max.height + gap) * woi_width) + i;
			if (pindx >= 0) {
				indices [indices_pix][0] = 0;
				indices [indices_pix][1] = pindx;
			} else {
				indices [indices_pix][0] = TVAO_SCENE_OFFSET + 2; // type + 1
				indices [indices_pix][1] = i;
			}
		}
		// add  TERR_ELEV as the last in the TVAO_SCENE_OFFSET last row // terr_elev_indx
		{
			int pindx = par_index[TVAO_TERR_ELEV][0]; // some will be -1
			int indices_pix = ((woi_max.height + gap) * woi_width) + terr_elev_indx;
			if (pindx >= 0) {
				indices [indices_pix][0] = 0;
				indices [indices_pix][1] = pindx;
			} else { // FIXME: Understand and fix 2 next lines !!!
				indices [indices_pix][0] = -1; // TVAO_SCENE_OFFSET + 2; // type + 1
				indices [indices_pix][1] = terr_elev_indx;
			}			
		}
		// extra images:
		//	public static final int [] SAVE_TYPES = {TVAO_TERRAIN, TVAO_VEGETATION, TVAO_ALPHA, TVAO_ELEVATION, TVAO_SCENE_OFFSET};
		// first row:
		// terrain, vegetation, alpha, elevation pull  
		// second row:
		// terrain, vegetation, alpha, elevation lpf
		int [][] sample_types = {
				{SAMPLES_TERRAIN_PULL, SAMPLES_VEGETATION_PULL, SAMPLES_ALPHA_PULL,SAMPLES_ELEVATION_PULL,SAMPLES_TERR_ELEV_PULL,SAMPLES_Y_AVG},
				{SAMPLES_TERRAIN_LPF,  SAMPLES_VEGETATION_LPF,  SAMPLES_ALPHA_LPF, SAMPLES_ELEVATION_LPF, SAMPLES_TERR_ELEV_LPF, SAMPLES_ELEVATION_ALPHA}}; //  -1}};
		if (extra) {
			for (int pull_lpf = 0; pull_lpf < 2; pull_lpf++) {
				int row0 = extra_start + pull_lpf * (woi_max.height + gap); 
				for (int type = 0; type < num_img_cols; type++) {
					int col0 = (woi_max.width + gap) * type;
					int sample_type = sample_types[pull_lpf][type]; // lpf is always next after
					if (sample_type >= 0) {
						if (samples_pointers[sample_type][1] > 0) { // th
							Rectangle sample_woi = getSampleWoi(
									sample_type); // int sample_type);
							if (sample_woi == null) {
								continue;
							}
							int sample_par =  getSamplePar(
									sample_type); // int sample_type) ;
							for (int n = 0; n < samples_pointers[sample_type][1]; n++) {
								int nx = n + samples_pointers[sample_type][0]; // fX index
								int findx;
								int np = n + samples_pointers[sample_type][2]; // full parameter index
								if (sample_par < 0) {
									int x = sample_woi.x + (n % sample_woi.width); 
									int y = sample_woi.y + (n / sample_woi.width);
									findx = x + y * full.width;
								} else {
									findx =  par_rindex[np][1];
								}
								int wvx = findx % full.width - woi_max.x;
								int wvy = findx / full.width - woi_max.y;
								if ((wvx >=0) && (wvx < woi_max.width) && (wvy >=0) && (wvx < woi_max.height)) {
									int row = row0 + wvy;
									int col = col0 + wvx;
									int indx = row * woi_width + col;
									indices [indx][0] = 1;
									indices [indx][1] = nx;
								} else {
									System.out.println("getParamDebugIndices() BUG:  n="+n+" nx="+nx+" np="+np+" wvx="+wvx+", wvy="+wvy);
								}
							}
						}
					}
				}
			}
		}
		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
		final boolean use_hf = samples_pointers[SAMPLES_Y_HF][1] > 0 ;// y_vector.length > y_src.length; // twice longer
		//samples_pointers[SAMPLES_Y_HF][1]
		final int index_combo =    num_scenes;
		final int index_weighted = num_scenes+1;
		final int ext_scenes =     index_weighted+1; // number of scenes and averages 
		final int woi_length = woi.width*woi.height;
		String [] top_titles = use_hf? (
				new     String[]{"Y_lf", "fX_lf", "Ylf-fXlf","Y_hf", "fX_hf", "Yhf-fXhf", "alpha", "terrain", "terr-scaled","veget","sum_w","weights"}):
					new String[]{"Y", "fX", "Y-fX","alpha", "terrain", "terr-scaled","veget","sum_w","weights"};
		double [][][] img_data = new double [top_titles.length][ext_scenes][woi_length];

		double [][] extra_data = new double [6][];
		int    index_alpha = use_hf ? 6:3;
		int    index_extra_weight = 5; 
		int    index_weight =       index_alpha + index_extra_weight; 
		double scale_weights = 1e6; // to make easy-to-set image windows
		for (int n = 0; n < img_data.length; n++) {
			int num_nan_scenes = ( n== index_alpha) ? ext_scenes : num_scenes;
//			for (int nscene = 0; nscene < img_data[n].length; nscene++) {
			for (int nscene = 0; nscene < num_nan_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][]
				extra_data, // final double [][] debug_data, // {elev* alpha shifted, terrain *(1-alpha) shifted, elev_sum_weights} 
				-1);        // final int         debug_level)
		final double [][] sample_weights = new double [num_scenes][woi_length]; //sample weights per scene, per output pixel
		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];
						sample_weights[nscene][windx] = weights[n_lf];
						for (int i = 0; i < extra_data.length; i++) {
							double k = (i == index_extra_weight) ? scale_weights : 1.0;
							img_data[index_alpha + i][nscene][windx] = k* extra_data[i][n_lf];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		if (use_hf) {
			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++) {
			if (slice == index_alpha) { // min alpha instead of the average
				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 d = img_data[slice][nscene][npix];
						if (!(img_data[slice][index_combo][npix] <= d)) {
							img_data[slice][index_combo][npix] = d;
						}
					}
				}
			} else {
				for (int nscene = 0; nscene < num_scenes; nscene++) {
					double w = (slice == index_weight)? 1.0: scene_weights[nscene];
					for (int npix = 0; npix < img_data[slice][nscene].length; npix++) if (!Double.isNaN(img_data[slice][nscene][npix])){
						sw[slice][npix] += w;
						img_data[slice][index_combo][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][index_combo][npix] /= w;
					} else {
						img_data[slice][index_combo][npix] = Double.NaN;
					}
				}
			}
		}
		
		sw = new double [img_data.length][img_data[0][0].length]; // reset to all 0-s
		for (int slice=0; slice < img_data.length; slice++) {
			for (int nscene = 0; nscene < num_scenes; nscene++) {
//				double w = (slice == index_weight)? 1.0: scene_weights[nscene];
				for (int npix = 0; npix < img_data[slice][nscene].length; npix++) if (!Double.isNaN(img_data[slice][nscene][npix])){
					double w = (slice == index_weight)? 1.0: sample_weights[nscene][npix]; // extra_data[index_extra_weight][npix];
					if (!Double.isNaN(w)) {
						sw[slice][npix] += w;
						img_data[slice][index_weighted][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][index_weighted][npix] /= w;
				} else {
					img_data[slice][index_weighted][npix] = Double.NaN;
				}
			}
		}
		
		String [] scene_titles = new String[ext_scenes];
		for (int i = 0; i < num_scenes; i++) {
			scene_titles[i] = ""+i;
		}
		scene_titles[index_combo] = "combo";
		scene_titles[index_weighted] = "weighted";
		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,
			double [] ymfx, // 
			int       gap,
			int []    wh,
			String    title) {
		boolean   extra = ymfx != null; 
		int [][] indices = getParamDebugIndices (
				extra, // boolean extra,
				gap);
		int num_img_cols = 6; // 5; // 4;
		int woi_width= woi_max.width * num_img_cols + (num_img_cols-1) * gap;		
		int height = indices.length / woi_width;
		int num_extra_rows = extra ? (2 *woi_max.height + 3 * gap) : 0;
		int extra_start_row = height - num_extra_rows;
		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 - now used for terr_elev
				dbg_img[i] = Double.NaN;
			} else if (indices[i][0] == 0) {
				dbg_img[i]= vector[indices[i][1]];
			} else if (indices[i][0] == 1) {
				dbg_img[i]= -ymfx[indices[i][1]]; //  * weights[indices[i][1]];
			} else { //  if (indices[i][0] > 1) {
				dbg_img[i] = Double.NaN;
				int type = indices[i][0] -2;
				if (type >= tvao.length) {
					System.out.println("parametersImage() BUG: indices["+i+"][0]="+indices[i][0]);
				} else {
					if (indices[i][1] >= tvao[type].length) {
						System.out.println("parametersImage() BUG: indices["+i+"][1]="+indices[i][1]);
					} else {
						dbg_img[i] = tvao[type][indices[i][1]];
					}
				}
			}
			if (!Double.isNaN(dbg_img[i])) {
				// scale alpha for the same image
				int w = i % woi_width;
				int h = i / woi_width;
				int nsub = w / (woi_max.width + gap);
				if (h < woi_max.height) {
					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];  
					} else if (nsub== 4) { // terrain elevation
						dbg_img[i] = debug_alpha_scales[6] + (debug_alpha_scales[7] - debug_alpha_scales[6]) * dbg_img[i];  
					}
				} else if (h >= extra_start_row) {
					double d = dbg_img[i] * samples_pointers[SAMPLES_TOTAL][1] / (1.0 - weight_pure); // total extra weight
					d *= .1; // 0; // 00; // ???
					/*
					if (nsub == 0) {
						d *= woi.width*woi.height;
					} else {
						d *= woi_max.width*woi_max.height;
					}
					*/
					dbg_img[i] = debug_alpha_scales[4] + (debug_alpha_scales[5] - debug_alpha_scales[4]) * d;  
				}
			}
		}
		if (wh != null) {
			wh[0] = woi_width;
			wh[1] = indices.length/woi_width;
		}
		if (title != null) {
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					woi_width,
					indices.length/woi_width,
					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 num_save_types = SAVE_TYPES.length;
		int woi_length = woi_max.width*woi_max.height;
		int ext_length = woi_length; 
		if (vector == null) {
			vector = parameters_vector;
		}
		int height = woi_max.height;
		if (num_scenes > woi_length) { // very unlikely but still may happen
			height = (num_scenes + woi_max.width - 1) / woi_max.width;
			ext_length = woi_max.width * height; 
		}
		String [] titles = {"terrain","vegetation","alpha","elevation", "terr_elev", "offset",
				"terrain-index","vegetation-index","alpha-index","elevation-index","terr_elev-index", "offset-index"};
		double [][] data = new double [titles.length][ext_length];
		for (int i = 0; i < num_save_types; i++) Arrays.fill(data[i],Double.NaN);
		int [][] indices = new int [num_save_types][ext_length];
		for (int t = 0; t < num_save_types; t++) {
			int n = SAVE_TYPES[t]; // skipping TVAO_TERR_ELEV (common offset)
			for (int i = 0; (i < indices[t].length) && (i < par_index[n].length); i++) {
				int indx = i;
				if (n < TVAO_SCENE_OFFSET) { // TVAO_TERR_ELEV) {
					int w = i % woi_max.width;
					int h = i / woi_max.width;
					indx = (h + woi_max.y) * full.width + (w + woi_max.x); // full index
				}

				int pindx = par_index[n][indx];
				indices[t][i] =  pindx;
				if (pindx < 0) {
					if (tvao[n] == null) {
						data[t][i] = Double.NaN;
					} else {
						data[t][i] = tvao[n][indx]; // will be NaN for vegetation, alpha, elevation for terrain-only
					}
				} else {
					data[t][i] = vector[pindx];
					if (n == TVAO_TERR_ELEV_PIX) {
						data[t][i] += vector[par_index[TVAO_TERR_ELEV][0]]; // add average
					}
				}
				data[t+num_save_types][i] = indices[t][i];
//			java.lang.ArrayIndexOutOfBoundsException: Index 12 out of bounds for length 12 	at com.elphel.imagej.vegetation.VegetationLMA.saveParametersFile(VegetationLMA.java:5498)				
			}
		}
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				data,
				woi_max.width,
				height,
				title, // +"-objects_"+scene_num,				
				titles); // test_titles,
		imp.setProperty("NUM_SCENES",                ""+num_scenes);
		if (woi_veg != null) {
			imp.setProperty("WOI_VEG_X",                 ""+woi_veg.x);
			imp.setProperty("WOI_VEG_Y",                 ""+woi_veg.y);
			imp.setProperty("WOI_VEG_WIDTH",             ""+woi_veg.width);
			imp.setProperty("WOI_VEG_HEIGHT",            ""+woi_veg.height);
		} else {
			imp.setProperty("WOI_VEG_X",                 "-1");
			imp.setProperty("WOI_VEG_Y",                 "-1");
			imp.setProperty("WOI_VEG_WIDTH",             "-1");
			imp.setProperty("WOI_VEG_HEIGHT",            "-1");
		}
		imp.setProperty("WOI_TERR_X",                ""+woi_terr.x);
		imp.setProperty("WOI_TERR_Y",                ""+woi_terr.y);
		imp.setProperty("WOI_TERR_WIDTH",            ""+woi_terr.width);
		imp.setProperty("WOI_TERR_HEIGHT",           ""+woi_terr.height);
		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("HIGHFREQ_WEIGHT",           ""+hifreq_weight);
		imp.setProperty("TERRAIN_CORRECTION",        ""+terrain_correction);
		imp.setProperty("REG_WEIGHTS",               ""+reg_weights);
		imp.setProperty("FIT_TERRAIN",               ""+fits[TVAO_TERRAIN]);
		imp.setProperty("FIT_VEGETATION",            ""+fits[TVAO_VEGETATION]);
		imp.setProperty("FIT_ALPHA",                 ""+fits[TVAO_ALPHA]);
		imp.setProperty("FIT_ELEVATION",             ""+fits[TVAO_ELEVATION]);
		imp.setProperty("FIT_TERR_ELEVATION",        ""+fits[TVAO_TERR_ELEV]); // old?
		imp.setProperty("FIT_TERR_ELEV",             ""+fits[TVAO_TERR_ELEV]);
		imp.setProperty("FIT_TERR_ELEV_PIX",         ""+fits[TVAO_TERR_ELEV_PIX]);
		imp.setProperty("FIT_SCENE_OFFSET",          ""+fits[TVAO_SCENE_OFFSET]);

		imp.setProperty("FIT_DISABLE_TERRAIN",       ""+fits_disable[TVAO_TERRAIN]);
		imp.setProperty("FIT_DISABLE_VEGETATION",    ""+fits_disable[TVAO_VEGETATION]);
		imp.setProperty("FIT_DISABLE_ALPHA",         ""+fits_disable[TVAO_ALPHA]);
		imp.setProperty("FIT_DISABLE_ELEVATION",     ""+fits_disable[TVAO_ELEVATION]);
		imp.setProperty("FIT_DISABLE_TERR_ELEV",     ""+fits_disable[TVAO_TERR_ELEV]);
		imp.setProperty("FIT_DISABLE_TERR_ELEV_PIX", ""+fits_disable[TVAO_TERR_ELEV_PIX]);
		imp.setProperty("FIT_DISABLE_SCENE_OFFSET",  ""+fits_disable[TVAO_SCENE_OFFSET]);
//		imp.setProperty("TERR_ELEVATION_FULL",       ""+terr_elevation_full);
		imp.setProperty("ALPHA_LOSS",                ""+alpha_loss);
		imp.setProperty("ALPHA_LOSS_LIN",            ""+alpha_loss_lin);
		imp.setProperty("ALPHA_OFFSET",              ""+alpha_offset);
		imp.setProperty("ALPHA_0OFFSET",             ""+alpha_0offset);
		imp.setProperty("ALPHA_MIN_VEG",             ""+alpha_min_veg);
		imp.setProperty("ALPHA_MAX_TERRAIN",         ""+alpha_max_terrain);
		imp.setProperty("ALPHA_PULL_PWR",            ""+alpha_pull_pwr);
		imp.setProperty("ALPHA_LPF",                 ""+alpha_lpf);
		imp.setProperty("ALPHA_LPF_BORDER",          ""+alpha_lpf_border);
		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_MM_HOLE",             ""+alpha_mm_hole);
		imp.setProperty("ALPHA_EN_HOLES",            ""+alpha_en_holes );
		imp.setProperty("ALPHA_DIFF_HOLE",           ""+alpha_diff_hole);
		imp.setProperty("W_ALPHA_NEIB",              ""+w_alpha_neib);
		imp.setProperty("HOLES_PWR",                 ""+holes_pwr);
		imp.setProperty("TERR_LPF",                  ""+terr_lpf);
		imp.setProperty("VEGET_LPF",                 ""+veget_lpf);
		imp.setProperty("ELEVATION_LPF",             ""+elevation_lpf);
		imp.setProperty("TERR_ELEV_LPF",             ""+terr_elev_lpf);

		imp.setProperty("LPF_FIXED_TERRAIN",         ""+lpf_fixed[TVAO_TERRAIN]);
		imp.setProperty("LPF_FIXED_VEGETATION",      ""+lpf_fixed[TVAO_VEGETATION]);
		imp.setProperty("LPF_FIXED_ALPHA",           ""+lpf_fixed[TVAO_ALPHA]);
		imp.setProperty("LPF_FIXED_ELEVATION",       ""+lpf_fixed[TVAO_ELEVATION]);
		imp.setProperty("LPF_FIXED_TERR_ELEV",       ""+lpf_fixed[TVAO_TERR_ELEV]);
		imp.setProperty("LPF_FIXED_TERR_ELEV",       ""+lpf_fixed[TVAO_TERR_ELEV_PIX]);
		imp.setProperty("LPF_FIXED_SCENE_OFFSET",    ""+lpf_fixed[TVAO_SCENE_OFFSET]);
		imp.setProperty("TERR_PULL0",                ""+terr_pull0);
		imp.setProperty("TERR_PULL_UP",              ""+terr_pull_up);
		imp.setProperty("TERR_PULL_AVG",             ""+terr_pull_avg);
		imp.setProperty("VEGET_PULL0",               ""+veget_pull0);
		
		imp.setProperty("VEGET_PULL_LOW_ALPHA",      ""+veget_pull_low_alpha);
		
		imp.setProperty("ELEVATION_PULL0",           ""+elevation_pull0);
		imp.setProperty("TERR_ELEV_PULL0",           ""+terr_elev_pull0);
		imp.setProperty("ELEV_ALPHA_EN",             ""+elev_alpha_en);
		imp.setProperty("ELEV_ALPHA",                ""+elev_alpha);
		imp.setProperty("ELEV_ALPHA_PWR",            ""+elev_alpha_pwr);
		
		imp.setProperty("ELEV_LOW_VEGET",            ""+low_veget);
		imp.setProperty("SCENES_PULL0",              ""+scenes_pull0);
		imp.setProperty("SCALE_SCENES_PULL",         ""+scale_scenes_pull);
		
	    // scaling elevation losses for high elevations (decrease pull and/or lpf for high elevations)
		imp.setProperty("ELEV_SCALE_THRESH",         ""+elev_scale_thresh);
		imp.setProperty("ELEV_SCALE_PULL",           ""+elev_scale_pull);
		imp.setProperty("ELEV_SCALE_LPF",            ""+elev_scale_lpf);
		// tree-top removal
		imp.setProperty("TTOP_EN",                   ""+ttop_en);
		imp.setProperty("TTOP_GB",                   ""+ttop_gb);
		imp.setProperty("TTOP_MIN",                  ""+ttop_min);
		imp.setProperty("TTOP_REL_LEV",              ""+ttop_rel_lev);
		imp.setProperty("TTOP_REL_RAD",              ""+ttop_rel_rad);
		imp.setProperty("TTOP_FRAC",                 ""+ttop_frac);
		imp.setProperty("TTOP_REM_RAD",              ""+ttop_rem_rad);
///		imp.setProperty("TERR_ONLY_SPECIAL",         ""+terr_only_special);
///		imp.setProperty("TERR_ONLY_PIX",             ""+terr_only_pix);
		imp.setProperty("BOOST_PARALLAX",            ""+boost_parallax);
		imp.setProperty("MAX_PARALLAX",              ""+max_parallax);
		imp.setProperty("MAX_WARP",                  ""+max_warp);
		imp.setProperty("MAX_ELEVATION",             ""+max_elevation);
		imp.setProperty("MAX_ELEV_TERR",             ""+max_elev_terr);
		imp.setProperty("MAX_ELEV_TERR_CHN",         ""+max_elev_terr_chg);
		imp.setProperty("ELEVATION_RADIUS",          ""+elevation_radius);
		imp.setProperty("TERR_ELEV_RADIUS",          ""+terr_elev_radius);
		imp.setProperty("ELEV_RADIUS_EXTRA",         ""+elev_radius_extra);
		
		imp.setProperty("NUM_PARS_TERRAIN",          ""+num_pars[TVAO_TERRAIN]);
		imp.setProperty("NUM_PARS_VEGETATION",       ""+num_pars[TVAO_VEGETATION]);
		imp.setProperty("NUM_PARS_ALPHA",            ""+num_pars[TVAO_ALPHA]);
		imp.setProperty("NUM_PARS_ELEVATION",        ""+num_pars[TVAO_ELEVATION]);
		imp.setProperty("NUM_PARS_TERR_ELEV",        ""+num_pars[TVAO_TERR_ELEV]);
		imp.setProperty("NUM_PARS_TERR_ELEV_PIX",    ""+num_pars[TVAO_TERR_ELEV_PIX]);
		imp.setProperty("NUM_PARS_SCENES",           ""+num_pars[TVAO_SCENE_OFFSET]);
		imp.setProperty("IND_PARS_TERRAIN",          ""+ind_pars[TVAO_TERRAIN]);
		imp.setProperty("IND_PARS_VEGETATION",       ""+ind_pars[TVAO_VEGETATION]);
		imp.setProperty("IND_PARS_ALPHA",            ""+ind_pars[TVAO_ALPHA]);
		imp.setProperty("IND_PARS_ELEVATION",        ""+ind_pars[TVAO_ELEVATION]);
		imp.setProperty("IND_PARS_TERR_ELEV",        ""+ind_pars[TVAO_TERR_ELEV]);
		imp.setProperty("IND_PARS_TERR_ELEV_PIX",    ""+ind_pars[TVAO_TERR_ELEV]);
		imp.setProperty("IND_PARS_SCENES",           ""+ind_pars[TVAO_SCENE_OFFSET]);
		imp.setProperty("TERRAIN_OFFSET",            ""+terrain_offset);
		imp.setProperty("TERR_ELEV_LAST_SUCCESS",    ""+terr_elev_last_success);
		
// 	samples_pointers
		for (int nsp = 0; nsp < SAMPLES_SIZE; nsp++) {
			for (int i = 0; i < samples_pointers[nsp].length; i++) {
				imp.setProperty("SAMPLES_POINTERS_"+nsp+"_"+i,   ""+samples_pointers[nsp][i]);
			}
		}
		
		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 double [][] restoreParametersFile( //FIXME: Not finished for real import ! 
			String        path,
			boolean       keep_settings,
			double [] other_pars){
		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();
		int num_save_types = SAVE_TYPES.length;

//		if (num_slices != 10) {
		if (num_slices != 12) { // with terrain common elevation
			throw new IllegalArgumentException("Expecting an 12-slice file, got "+num_slices);
		}
		int width = imp_pars.getWidth();   // woi_max.width
		int height = imp_pars.getHeight(); // woi_max.height
		double [][] data = new double[num_save_types][width*height]; // woi_max
		int [][] indices = new int[num_save_types][width*height];
		for (int n = 0; n < num_save_types; n++) {
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1);
			for (int i = 0; i < data[n].length; i++) {
				data[n][i] = pixels[i];
			}
		}
		for (int n = 0; n < num_save_types; n++) {
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1+ num_save_types);
			for (int i = 0; i < data[n].length; i++) {
				indices[n][i] = (int) pixels[i];
			}
		}

		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")));
		Rectangle woi_veg = new Rectangle (
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_X")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_Y")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_WIDTH")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_HEIGHT")));
			if ((woi_veg.width <= 0) || (woi_veg.height <=0)) {
				woi_veg = null;
			}
		Rectangle woi_terr = new Rectangle (
				Integer.parseInt((String) imp_pars.getProperty("WOI_TERR_X")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_TERR_Y")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_TERR_WIDTH")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_TERR_HEIGHT")));
		this.woi_veg =  woi_veg;
		this.woi_terr = woi_terr;
		this.woi = woi;
		woi_max = new Rectangle (woi);
		if (woi_veg != null) {
			woi_max.add(woi_veg);
		}
		woi_max.add(woi_terr);
		
		if (!keep_settings) {
			hifreq_weight = Double.parseDouble((String) imp_pars.getProperty("HIGHFREQ_WEIGHT"));
			terrain_correction = getProperty(imp_pars,"TERRAIN_CORRECTION", terrain_correction);
			reg_weights = Double.parseDouble((String) imp_pars.getProperty("REG_WEIGHTS"));
			fits = new boolean[TVAO_TYPES];
			fits[TVAO_TERRAIN] =        Boolean.parseBoolean((String) imp_pars.getProperty("FIT_TERRAIN"));
			fits[TVAO_VEGETATION] =     Boolean.parseBoolean((String) imp_pars.getProperty("FIT_VEGETATION"));
			fits[TVAO_ALPHA] =          Boolean.parseBoolean((String) imp_pars.getProperty("FIT_ALPHA"));
			fits[TVAO_ELEVATION] =      Boolean.parseBoolean((String) imp_pars.getProperty("FIT_ELEVATION"));
			fits[TVAO_TERR_ELEV] =      getProperty(imp_pars,"FIT_TERR_ELEV",         fits[TVAO_TERR_ELEV]);
			fits[TVAO_TERR_ELEV_PIX] =  getProperty(imp_pars,"FIT_TERR_ELEV_PIX",     fits[TVAO_TERR_ELEV_PIX]);
			fits[TVAO_SCENE_OFFSET] =   Boolean.parseBoolean((String) imp_pars.getProperty("FIT_SCENE_OFFSET"));

			fits_disable[TVAO_TERRAIN] =      getProperty(imp_pars,"FIT_DISABLE_TERRAIN",      fits_disable[TVAO_TERRAIN]);
			fits_disable[TVAO_VEGETATION] =   getProperty(imp_pars,"FIT_DISABLE_VEGETATION",   fits_disable[TVAO_VEGETATION]);
			fits_disable[TVAO_ALPHA] =        getProperty(imp_pars,"FIT_DISABLE_ALPHA",        fits_disable[TVAO_ALPHA]);
			fits_disable[TVAO_ELEVATION] =    getProperty(imp_pars,"FIT_DISABLE_ELEVATION",    fits_disable[TVAO_ELEVATION]);
			fits_disable[TVAO_TERR_ELEV] =    getProperty(imp_pars,"FIT_DISABLE_TERR_ELEV",    fits_disable[TVAO_TERR_ELEV]);
			fits_disable[TVAO_TERR_ELEV_PIX] =getProperty(imp_pars,"FIT_DISABLE_TERR_ELEV_PIX",fits_disable[TVAO_TERR_ELEV_PIX]);
			fits_disable[TVAO_SCENE_OFFSET] = getProperty(imp_pars,"FIT_DISABLE_SCENE_OFFSET", fits_disable[TVAO_SCENE_OFFSET]);

//			terr_elevation_full = getProperty(imp_pars,"TERR_ELEVATION_FULL", terr_elevation_full);
			
			alpha_loss =                Double.parseDouble((String)   imp_pars.getProperty("ALPHA_LOSS"));
			alpha_loss_lin =            getProperty(imp_pars,"ALPHA_LOSS_LIN",      alpha_loss_lin);
			alpha_offset =              getProperty(imp_pars,"ALPHA_OFFSET",        alpha_offset);
			alpha_0offset =             getProperty(imp_pars,"ALPHA_0OFFSET",       alpha_0offset);
			alpha_min_veg =             getProperty(imp_pars,"ALPHA_MIN_VEG",       alpha_min_veg);
			alpha_max_terrain =         getProperty(imp_pars,"ALPHA_MAX_TERRAIN",   alpha_max_terrain);
			alpha_pull_pwr =            getProperty(imp_pars,"ALPHA_PULL_PWR",      alpha_pull_pwr);
			alpha_lpf =                 getProperty(imp_pars,"ALPHA_LPF",           alpha_lpf);
			alpha_lpf_border =          getProperty(imp_pars,"ALPHA_LPF_BORDER",    alpha_lpf_border);
			alpha_piece_linear =        Boolean.parseBoolean((String) imp_pars.getProperty("ALPHA_PIECE_LINEAR"));
			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"));
			alpha_mm_hole =             Double.parseDouble((String)   imp_pars.getProperty("ALPHA_MM_HOLE"));
			alpha_en_holes =            Boolean.parseBoolean((String) imp_pars.getProperty("ALPHA_EN_HOLES"));
			alpha_diff_hole =           Double.parseDouble((String)   imp_pars.getProperty("ALPHA_DIFF_HOLE"));
			w_alpha_neib =              Double.parseDouble((String)   imp_pars.getProperty("W_ALPHA_NEIB"));
			holes_pwr =                 Double.parseDouble((String)   imp_pars.getProperty("HOLES_PWR"));
			terr_lpf =                  Double.parseDouble((String) imp_pars.getProperty("TERR_LPF"));
			veget_lpf =                 Double.parseDouble((String) imp_pars.getProperty("VEGET_LPF"));
			elevation_lpf =             Double.parseDouble((String) imp_pars.getProperty("ELEVATION_LPF"));
			terr_elev_lpf =             Double.parseDouble((String) imp_pars.getProperty("TERR_ELEV_LPF"));
			lpf_fixed = new boolean[TVAO_TYPES];
			lpf_fixed[TVAO_TERRAIN] =     getProperty(imp_pars,"LPF_FIXED_TERRAIN",      lpf_fixed[TVAO_TERRAIN]);
			lpf_fixed[TVAO_VEGETATION] =  getProperty(imp_pars,"LPF_FIXED_VEGETATION",   lpf_fixed[TVAO_VEGETATION]);
			lpf_fixed[TVAO_ALPHA] =       getProperty(imp_pars,"LPF_FIXED_ALPHA",        lpf_fixed[TVAO_ALPHA]);
			lpf_fixed[TVAO_ELEVATION] =   getProperty(imp_pars,"LPF_FIXED_ELEVATION",    lpf_fixed[TVAO_ELEVATION]);
			lpf_fixed[TVAO_TERR_ELEV] =   getProperty(imp_pars,"LPF_FIXED_TERR_ELEV",    lpf_fixed[TVAO_TERR_ELEV]);
			lpf_fixed[TVAO_TERR_ELEV_PIX]=getProperty(imp_pars,"LPF_FIXED_TERR_ELEV_PIX",lpf_fixed[TVAO_TERR_ELEV_PIX]);
			lpf_fixed[TVAO_SCENE_OFFSET]= getProperty(imp_pars,"LPF_FIXED_SCENE_OFFSET", lpf_fixed[TVAO_SCENE_OFFSET]);
			
			terr_pull0=                 getProperty(imp_pars,"TERR_PULL0",            terr_pull0);
			terr_pull_up=               getProperty(imp_pars,"TERR_PULL_UP",          terr_pull_up);
			terr_pull_avg=              getProperty(imp_pars,"TERR_PULL_AVG",         terr_pull_avg);
			veget_pull0=                getProperty(imp_pars,"VEGET_PULL0",           veget_pull0);
			veget_pull_low_alpha=       getProperty(imp_pars,"VEGET_PULL_LOW_ALPHA",  veget_pull_low_alpha);
			elevation_pull0=            getProperty(imp_pars,"ELEVATION_PULL0",       elevation_pull0);
			terr_elev_pull0=            getProperty(imp_pars,"TERR_ELEV_PULL0",       terr_elev_pull0);
			// renew above using  getProperty(,,);
			elev_alpha_en=              getProperty(imp_pars,"ELEV_ALPHA_EN",         elev_alpha_en);
			elev_alpha =                getProperty(imp_pars,"ELEV_ALPHA",            elev_alpha);
			elev_alpha_pwr =            getProperty(imp_pars,"ELEV_ALPHA_PWR",        elev_alpha_pwr);
			low_veget =                 getProperty(imp_pars,"ELEV_LOW_VEGET",        low_veget);

			scenes_pull0 =              getProperty(imp_pars,"SCENES_PULL0",          scenes_pull0);
			scale_scenes_pull =         getProperty(imp_pars,"SCALE_SCENES_PULL",     scale_scenes_pull);
			elev_scale_thresh =         getProperty(imp_pars,"ELEV_SCALE_THRESH",     elev_scale_thresh);
			elev_scale_pull =           getProperty(imp_pars,"ELEV_SCALE_PULL",       elev_scale_pull);
			elev_scale_lpf =            getProperty(imp_pars,"ELEV_SCALE_LPF",        elev_scale_lpf);

			ttop_en =                   getProperty(imp_pars,"TTOP_EN",               ttop_en);
			ttop_gb =                   getProperty(imp_pars,"TTOP_GB",               ttop_gb);
			ttop_min =                  getProperty(imp_pars,"TTOP_MIN",              ttop_min);
			ttop_rel_lev =              getProperty(imp_pars,"TTOP_REL_LEV",          ttop_rel_lev);
			ttop_rel_rad =              getProperty(imp_pars,"TTOP_REL_RAD",          ttop_rel_rad);
			ttop_frac =                 getProperty(imp_pars,"TTOP_FRAC",             ttop_frac);
			ttop_rem_rad =              getProperty(imp_pars,"TTOP_REM_RAD",          ttop_rem_rad);
///			terr_only_special =         getProperty(imp_pars,"TERR_ONLY_SPECIAL",     terr_only_special);
///			terr_only_pix =             getProperty(imp_pars,"TERR_ONLY_PIX",         terr_only_pix);
			boost_parallax =            getProperty(imp_pars,"BOOST_PARALLAX",        boost_parallax);
			max_parallax =              getProperty(imp_pars,"MAX_PARALLAX",          max_parallax);
			max_warp =                  getProperty(imp_pars,"MAX_WARP",              max_warp);
			max_elevation =             getProperty(imp_pars,"MAX_ELEVATION",         max_elevation);
			max_elev_terr =             getProperty(imp_pars,"MAX_ELEV_TERR",         max_elev_terr);
			max_elev_terr_chg =         getProperty(imp_pars,"MAX_ELEV_TERR_CHG",     max_elev_terr_chg);
			elevation_radius =          getProperty(imp_pars,"ELEVATION_RADIUS",      elevation_radius);
			terr_elev_radius =          getProperty(imp_pars,"TERR_ELEV_RADIUS",      terr_elev_radius);
			elev_radius_extra =         getProperty(imp_pars,"ELEV_RADIUS_EXTRA",     elev_radius_extra);
			
			
			terrain_offset =            getProperty(imp_pars,"TERRAIN_OFFSET",        terrain_offset);
			terr_elev_last_success =    getProperty(imp_pars,"TERR_ELEV_LAST_SUCCESS",terr_elev_last_success);
			//				if (imp_pars.getProperty("TERRAIN_OFFSET") != null) terrain_offset = Double.parseDouble((String)   imp_pars.getProperty("TERRAIN_OFFSET"));
		}
		num_pars[TVAO_TERRAIN] =          Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERRAIN"));
		num_pars[TVAO_VEGETATION] =       Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_VEGETATION"));
		num_pars[TVAO_ALPHA] =            Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_ALPHA"));
		num_pars[TVAO_ELEVATION] =        Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_ELEVATION"));
		num_pars[TVAO_TERR_ELEV] =        Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERR_ELEV"));
		num_pars[TVAO_SCENE_OFFSET] =     Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_SCENES"));
		ind_pars[TVAO_TERRAIN] =          Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERRAIN"));
		ind_pars[TVAO_VEGETATION] =       Integer.parseInt((String) imp_pars.getProperty("IND_PARS_VEGETATION"));
		ind_pars[TVAO_ALPHA] =            Integer.parseInt((String) imp_pars.getProperty("IND_PARS_ALPHA"));
		ind_pars[TVAO_ELEVATION] =        Integer.parseInt((String) imp_pars.getProperty("IND_PARS_ELEVATION"));
		// bug fix (remove later)
		if (imp_pars.getProperty("IND_PARS_TERR_ELEV") == null) {
			ind_pars[TVAO_TERR_ELEV] =     ind_pars[TVAO_ELEVATION]+num_pars[TVAO_ELEVATION];
			ind_pars[TVAO_TERR_ELEV_PIX] = ind_pars[TVAO_TERR_ELEV]+num_pars[TVAO_TERR_ELEV];
		} else {
			ind_pars[TVAO_TERR_ELEV] =     Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERR_ELEV"));
			if (imp_pars.getProperty("TVAO_TERR_ELEV_PIX") == null) {
				ind_pars[TVAO_TERR_ELEV_PIX] = ind_pars[TVAO_TERR_ELEV]+num_pars[TVAO_TERR_ELEV];
			} else {
				ind_pars[TVAO_TERR_ELEV_PIX] = Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERR_ELEV_PIX"));
			}
		}
		ind_pars[TVAO_SCENE_OFFSET] =     Integer.parseInt((String) imp_pars.getProperty("IND_PARS_SCENES"));

		par_index = new int[TVAO_TYPES][];
		for (int n = 0; n < par_index.length; n++) {
			//((n == TVAO_TERR_ELEV) && terr_elevation_full)
			if (n == TVAO_TERR_ELEV) {
				par_index[n] = new int[1]; // terr_elevation_full? image_length: 1]; // num_pars[TVAO_TERR_ELEV]?
			} else 	if (n < TVAO_SCENE_OFFSET) {
				par_index[n] = new int[image_length]; // including		
			} else {
				par_index[n] = new int[num_scenes];		// same as when creating original
			}
			Arrays.fill(par_index[n], -1);
		}
		//			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..4) and full window pixel index
		int [] par_nums = {
				num_pars[TVAO_TERRAIN],
				num_pars[TVAO_VEGETATION],
				num_pars[TVAO_ALPHA],
				num_pars[TVAO_ELEVATION],
				num_pars[TVAO_TERR_ELEV],
				num_pars[TVAO_TERR_ELEV_PIX],
				num_pars[TVAO_SCENE_OFFSET]};

		int [][] samples_pointers_tmp = new int [SAMPLES_SIZE][3];
		update_samples_pointers: {
			for (int nsp = 0; nsp < SAMPLES_SIZE; nsp++) {
				for (int i = 0; i < samples_pointers_tmp[nsp].length; i++) {
					if (imp_pars.getProperty("SAMPLES_POINTERS_"+nsp+"_"+i) == null) {
						System.out.println("restoreParametersFile(): Incompatible samples_pointers[][], will not update");
						break update_samples_pointers;
					}
					samples_pointers_tmp[nsp][i] = Integer.parseInt((String) imp_pars.getProperty("SAMPLES_POINTERS_"+nsp+"_"+i));
				}
			}
			samples_pointers = samples_pointers_tmp;
		}
		// samples_pointers[SAMPLES_TOTAL][0] = total number of parameters
		int num_pars = 0;
		for (int n = 0; n < TVAO_TYPES; n++) {
			num_pars += par_nums[n];
		}
		parameters_vector = new double [num_pars];
//		if (terr_elevation_full) { // only initialize if terrain elevation was saved in the image, otherwise keep new initialized
//			par_rindex = new int [num_pars][2];
//		}
		for (int t = 0; t < SAVE_TYPES.length; t++) {
			int n = SAVE_TYPES[t]; // skipping TVAO_TERR_ELEV (common offset)
//			if ((n == TVAO_TERR_ELEV_PIX) && !terr_elevation_full) {
//				n = TVAO_TERR_ELEV;
//			}
			for (int indx = 0; ((indx < indices[t].length) && (indx < par_index[n].length)); indx++) { // woi_max
				int findx = indx;
				int pindx = indices[t][indx];
				// in old mode (!terr_elevation_full) will read TVAO_TERR_ELEV to TVAO_TERR_ELEV_PIX (single-element)
				if ((n < TVAO_SCENE_OFFSET) && (n != TVAO_TERR_ELEV)) { // (n == TVAO_TERR_ELEV) can only be if explicitly set "n = TVAO_TERR_ELEV"
					int x = (indx % woi_max.width) + woi_max.x;
					int y = (indx / woi_max.width) + woi_max.y;
					findx = x + y * full.width;
				}
				par_index[n][findx] = pindx;
				if (pindx >= 0) { // always? - not!
					parameters_vector[pindx] = data[t][indx];
					par_rindex[pindx][0] = n;     // type
					par_rindex[pindx][1] = findx; // full index
				}
			}
		}
		// fix old mode
		/*
		if (!terr_elevation_full) { // provided TVAO_TERR_ELEV, set it to all defined elements of TVAO_TERR_ELEV_PIX
			double terr_elev_avg = parameters_vector[ind_pars[TVAO_TERR_ELEV]];
			for (int i = 0; i < this.num_pars[TVAO_TERR_ELEV_PIX]; i++) {
				int pindx = ind_pars[TVAO_TERR_ELEV_PIX]+ i;
				parameters_vector[pindx] = terr_elev_avg;
				// Do not update keep as initialized as this data was not saved in the image 
///				par_index[TVAO_TERR_ELEV_PIX][i] = pindx;
///				par_rindex[pindx][0] = TVAO_TERR_ELEV_PIX;
			}
			
		}
		*/
		{
			int pindx = ind_pars[TVAO_TERR_ELEV];
			parameters_vector[pindx] =     0; // zero offset to average
			par_index[TVAO_TERR_ELEV][0] = pindx;
			par_rindex[pindx][0] = TVAO_TERR_ELEV;
			par_rindex[pindx][1] = 0; // full index
		}
		// TODO: build other data structures and make instance consistent, without it this does not allow reading from file			
		return data;
	}

	public double[][] restoreParametersFile( 
			String        path,
			int           debugLevel,
			String        debug_path,
			boolean       debug_save_improved, // Save debug image after successful LMA step."); 
			boolean       debug_save_worsened) { // Save debug image after unsuccessful LMA step.");
			
		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();
		int num_save_types = SAVE_TYPES.length;
		if (num_slices != 12) {
			throw new IllegalArgumentException("Expecting an 12-slice file, got "+num_slices);
		}
		int width = imp_pars.getWidth();   // woi_veg.width
		int height = imp_pars.getHeight(); // woi_veg.height
		double [][] data = new double[num_save_types][width*height];
		double [][] tvao_data = new double [TVAO_TYPES][];		
		int [][] indices = new int[num_save_types][width*height];
		for (int n = 0; n < num_save_types; n++) {
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1);
			for (int i = 0; i < data[n].length; i++) {
				data[n][i] = pixels[i];
			}
		}
		for (int n = 0; n < num_save_types; n++) {
			float [] pixels = (float[]) imp_pars.getStack().getPixels(n+1+ num_save_types);
			for (int i = 0; i < data[n].length; i++) {
				indices[n][i] = (int) pixels[i];
			}
		}

		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")));
		Rectangle woi_veg = new Rectangle (
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_X")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_Y")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_WIDTH")),
				Integer.parseInt((String) imp_pars.getProperty("WOI_VEG_HEIGHT")));
		if ((woi_veg.width <= 0) || (woi_veg.height <=0)) {
			woi_veg = null;
		}
		Rectangle woi_terr = new Rectangle (
				getProperty(imp_pars,"WOI_TERR_X",        woi.x),          // use woi for old files
				getProperty(imp_pars,"WOI_TERR_Y",        woi.y),          // use woi for old files
				getProperty(imp_pars,"WOI_TERR_WIDTH",    woi.width),      // use woi for old files
				getProperty(imp_pars,"WOI_TERR_HEIGHT",   woi.height));    // use woi for old files
		
		Rectangle woi_max = new Rectangle(woi);
		if (woi_veg != null) {
			woi_max.add(woi_veg);
		}
		woi_max.add(woi_terr);
		
//					getProperty(imp_pars,"MAX_ELEVATION",        max_elevation),          // final int                max_offset,               // maximal "elevation" to consider 					
		
		{
			// run prepareLMA from the file metadata

			String [] disable_names = new String [TVAO_NAMES.length];
			for (int i = 0; i < TVAO_NAMES.length; i++) {
				disable_names[i]= "FIT_DISABLE_"+TVAO_NAMES[i];
			}
			
			boolean [][] valid_scene_pix = new boolean [(woi_veg==null) ? 2 : 3][]; // no overlaid
			valid_scene_pix[0] = new boolean [num_scenes];
			// reverse index of SAVE_TYPES
			int [] rsave_types = new int [TVAO_TYPES];
			Arrays.fill(rsave_types, -1);
			for (int i = 0; i < SAVE_TYPES.length; i++) {
				rsave_types[SAVE_TYPES[i]] = i;
			}
			for (int n = 0; n < num_scenes; n++) {
//				valid_scene_pix[0][n] = indices[TVAO_SCENE_OFFSET][n] >=0;
				valid_scene_pix[0][n] = indices[rsave_types[TVAO_SCENE_OFFSET]][n] >=0;
			}
			valid_scene_pix[1] = new boolean [woi.width*woi.height];
			for (int n = 0; n < valid_scene_pix[1].length; n++) { // index inside woi
				int wmindex = getWoiIndex( // index inside woi_max and also the restored image
						woi,     // Rectangle woi_src,
						woi_max, // Rectangle woi_dst,
						full,    // Rectangle woi_full,
						n);      // int indx)
				valid_scene_pix[1][n] = indices[rsave_types[TVAO_TERRAIN]][wmindex] >=0;
			}
			if (woi_veg != null) {
				valid_scene_pix[2] = new boolean [woi_veg.width * woi_veg.height]; // here woi_veg, not woi_max
				for (int n = 0; n < valid_scene_pix[2].length; n++) { // index in woi_veg
					int wmindx =  getWoiIndex( // index inside woi_max and also the restored image
							woi_veg, // Rectangle woi_src,
							woi_max, // Rectangle woi_dst,
							full, // Rectangle woi_full,
							n); // int indx)
					valid_scene_pix[2][n] = indices[rsave_types[TVAO_VEGETATION]][wmindx] >=0;
				}
			}
//			int num_samples = 
//			terr_elevation_full = getProperty(imp_pars,"TERR_ELEVATION_FULL", terr_elevation_full);
			prepareLMA(
					false,               // final boolean            keep_parameters,
					woi,                 // final Rectangle   woi,
					/// Here woi_terr is always non-null and woi_veg may be null (if terrain-only)
					woi_veg,  // final Rectangle          woi_veg_in, // used when loading from file (may be different)
					woi_terr, // final Rectangle          woi_terr_in, // used when loading from file (may be different)
					getProperty(imp_pars,"MAX_WARP",             max_warp),               // final double             max_warp, // 1.8 - do not use scenes where distance between vegetation projection exceeds this
					getProperty(imp_pars,"MAX_ELEVATION",        max_elevation),          // final int                max_offset,               // maximal "elevation" to consider
					getProperty(imp_pars,"MAX_ELEV_TERR",        max_elev_terr),          // final int                max_elev_terr,               // maximal terrain "elevation" to consider
					getProperty(imp_pars,"MAX_ELEV_TERR_CHG",    max_elev_terr_chg),      // final double             max_elev_terr_chg, // 0.5 maximal terrain elevation change from last successfully used
					getProperty(imp_pars,"ELEVATION_RADIUS",     elevation_radius),       // final double             elevation_radius,  // Radius of elevation/vegetation influence.
					getProperty(imp_pars,"TERR_ELEV_RADIUS",     terr_elev_radius),       // final double             terr_elev_radius, //  = 1.5; 
					getProperty(imp_pars,"ELEV_RADIUS_EXTRA",    elev_radius_extra),      // final double             elev_radius_extra, //  =  1.2;      // scale both radii when setupElevationLMA(), setupTerrainElevationLMA(), and setupTerrainElevationPixLMA() 
					valid_scene_pix, // final boolean []         valid_scene_pix, valid_scene_pix_in, // may have nulls or be shorter (do not update "overlaid")         
					getProperty(imp_pars, "HIGHFREQ_WEIGHT",     hifreq_weight),          // final double             hifreq_weight,  // 22.5 0 - do not use high-freq. Relative weight of laplacian components
					getProperty(imp_pars, "TERRAIN_CORRECTION",  terrain_correction),     // final double             terrain_correction,
					getProperty(imp_pars, "FIT_TERRAIN",         fits[TVAO_TERRAIN]),     // final boolean            adjust_terr,
					getProperty(imp_pars, "FIT_VEGETATION",      fits[TVAO_VEGETATION]),  // final boolean            adjust_veget,
					getProperty(imp_pars, "FIT_ALPHA",           fits[TVAO_ALPHA]),       // final boolean            adjust_alpha,
					getProperty(imp_pars, "FIT_SCENE_OFFSET",    fits[TVAO_SCENE_OFFSET]),// final boolean            adjust_scenes,
					getProperty(imp_pars, "FIT_ELEVATION",       fits[TVAO_ELEVATION]),   // final boolean            adjust_elevations,
					getProperty(imp_pars, "FIT_TERR_ELEV",       fits[TVAO_TERR_ELEV]),   // final boolean            adjust_terr_elev,
					getProperty(imp_pars, "FIT_TERR_ELEV_PIX",   fits[TVAO_TERR_ELEV_PIX]),// final boolean           adjust_terr_elev_pix,
//					getProperty(imp_pars,"TERR_ELEVATION_FULL",  terr_elevation_full),
					getProperty(imp_pars, disable_names,         fits_disable),           // final boolean []         fit_disable,
					null, // fits_disable_terronly,//			final boolean []         fits_disable_terronly,
					getProperty(imp_pars, "REG_WEIGHTS",         reg_weights),            // final double             reg_weights,        // fraction of the total weight used for regularization
					getProperty(imp_pars, "ALPHA_LOSS",          alpha_loss),             // final double             alpha_loss,         // alpha quadratic growing loss for when out of [0,1] range
					getProperty(imp_pars, "ALPHA_LOSS_LIN",      alpha_loss_lin),         // final double             alpha_loss_lin, // alpha linear growing loss for when out of [0,1] range and below minimal vegetation alpha
					getProperty(imp_pars, "ALPHA_OFFSET",        alpha_offset),           // final double             alpha_offset,       // quadratic loss when alpha > 1 - alpha_offset
					getProperty(imp_pars, "ALPHA_0OFFSET",       alpha_0offset),          // final double             alpha_0offset,      // quadratic loss when alpha < alpha0_offset
					getProperty(imp_pars, "ALPHA_MIN_VEG",       alpha_min_veg),          // final double             alpha_min_veg, // 0.5; // if (alpha-alpha_offset)/(1-2*alpha_offset) < alpha_min_veg, pull down to lpha_offset
					getProperty(imp_pars, "ALPHA_MAX_TERRAIN",   alpha_max_terrain),      // final double             alpha_max_terrain, // 0.75; // () increase pull vegetation if below
					getProperty(imp_pars, "ALPHA_PULL_PWR",      alpha_pull_pwr),         // final double             alpha_pull_pwr,    // 1.0;  // () raise extra pull to that power
					getProperty(imp_pars, "ALPHA_LPF",           alpha_lpf),              // final double             alpha_lpf_border, //  pull to average of 4 neighbors for border tiles (to keep stable)
					getProperty(imp_pars, "ALPHA_LPF_BORDER",    alpha_lpf_border),       // final double             alpha_lpf,           // pull to average of 4 neighbors
					getProperty(imp_pars, "ALPHA_PIECE_LINEAR",  alpha_piece_linear),     // final boolean       alpha_piece_linear, // true - piece-linear, false - half-cosine
					getProperty(imp_pars, "ALPHA_SCALE_AVG",     alpha_scale_avg),        // final double             alpha_scale_avg, //  = 1.2; // scale average alpha (around 0.5) when pulling to it
					getProperty(imp_pars, "ALPHA_PUSH",          alpha_push),             // final double             alpha_push,      //  5.0;   // push from alpha==0.5
					getProperty(imp_pars, "ALPHA_PUSH_NEUTRAL",  alpha_push_neutral),     // double    alpha_push_neutral = 0.8; // alpha point from which push (closer to opaque)
					getProperty(imp_pars, "ALPHA_PUSH_CENTER",   alpha_push_center),      // final double             alpha_push_center,// 1.5; // weight of center alpha pixel relative to each of the 4 ortho ones
					getProperty(imp_pars, "ALPHA_EN_HOLES",      alpha_en_holes),         // final boolean            alpha_en_holes, // Search for small semi-transparent holes, disable diffusion of local alpha minimums
					getProperty(imp_pars, "ALPHA_MM_HOLE",       alpha_mm_hole),          // 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
					getProperty(imp_pars, "ALPHA_DIFF_HOLE",     alpha_diff_hole),        // final double             alpha_diff_hole, // 0.01; // Minimal alpha difference between min and max neighbor to be considered a hole
					getProperty(imp_pars, "TERR_LPF",            terr_lpf),               // final double             terr_lpf,           // pull terrain to average of 4 neighbors (very small)
					getProperty(imp_pars, "VEGET_LPF",           veget_lpf),              // final double             veget_lpf,          // pull vegetation to average of 4 neighbors (very small - maybe not needed)
					getProperty(imp_pars, "ELEVATION_LPF",       elevation_lpf),          // final double             elevation_lpf,
					getProperty(imp_pars, "TERR_ELEV_LPF",       terr_elev_lpf),          // terr_elev_lpf,  // final double             terr_elev_lpf,
					getProperty(imp_pars, "TERR_PULL0",          terr_pull0),             // final double             terr_pull0,     // pull terrain to initial (pre-adjustment) values
					getProperty(imp_pars, "TERR_PULL_UP",        terr_pull_up),           // final double             terr_pull_up,   // Terrain pixels pull to initial (pre-adjustment) values when it is colder than initial.
					getProperty(imp_pars, "TERR_PULL_AVG",       terr_pull_avg),          // final double             terr_pull_avg,  // pull terrain to the initial offset by the average offset of all terrain pixels  
					getProperty(imp_pars, "VEGET_PULL0",         veget_pull0),            // final double             veget_pull0,    // pull vegetation to initial (pre-adjustment) values
					getProperty(imp_pars, "VEGET_PULL_LOW_ALPHA",veget_pull_low_alpha),   // final double             veget_pull_low_alpha, //  10; // ()scale pull0 for low alpha (mostly terrain) 
					getProperty(imp_pars, "ELEVATION_PULL0",     elevation_pull0),        // final double             elev_pull0,     // pull elevation to initial (pre-adjustment) values
					getProperty(imp_pars, "TERR_ELEV_PULL0",     terr_elev_pull0),        // final double             terr_elev_pull0, // pull terrain elevation to segment average
					getProperty(imp_pars, "ELEV_ALPHA_EN",       elev_alpha_en),          // final boolean            elev_alpha_en,  // false; // Enable loss for low vegetation with high opacity
					getProperty(imp_pars, "ELEV_ALPHA",          elev_alpha),             // final double             elev_alpha,     // 1.0;   // multiply alpha by under-low elevation for loss
					getProperty(imp_pars, "ELEV_ALPHA_PWR",      elev_alpha_pwr),         // final double             elev_alpha_pwr, // 2.0;  // raise alpha to this power (when alpha > 0)
					getProperty(imp_pars, "ELEV_LOW_VEGET",      low_veget),              // final double             low_veget,      // 2.0;   // (pix) Elevation considered low (lower loss for high alpha)
					getProperty(imp_pars, "SCENES_PULL0",        scenes_pull0),           // final double             scenes_pull0,

			        getProperty(imp_pars, "ELEV_SCALE_THRESH",   elev_scale_thresh),      // final double             elev_scale_thresh,
			        getProperty(imp_pars, "ELEV_SCALE_PULL",     elev_scale_pull),        // final boolean            elev_scale_pull,
			        getProperty(imp_pars, "ELEV_SCALE_LPF",      elev_scale_lpf),         // final boolean            elev_scale_lpf,
			        getProperty(imp_pars, "TTOP_EN",             ttop_en),                // final boolean            ttop_en,
			        getProperty(imp_pars, "TTOP_GB",             ttop_gb),                // final double             ttop_gb,
			        getProperty(imp_pars, "TTOP_MIN",            ttop_min),               // final double             ttop_min,
			        getProperty(imp_pars, "TTOP_REL_LEV",        ttop_rel_lev),           // final double             ttop_rel_lev,
			        getProperty(imp_pars, "TTOP_REL_RAD",        ttop_rel_rad),           // final double             ttop_rel_rad,
			        getProperty(imp_pars, "TTOP_FRAC",           ttop_frac),              // final double             ttop_frac,
			        getProperty(imp_pars, "TTOP_REM_RAD",        ttop_rem_rad),           // final double             ttop_rem_rad,
///			        getProperty(imp_pars, "TERR_ONLY_SPECIAL",   terr_only_special),      // final boolean            terr_only_special,//true; // special sequences for terrain-only tiles
///			        getProperty(imp_pars, "TERR_ONLY_PIX",       terr_only_pix),          // final boolean            terr_only_pix,    //true; // force per-pixel terrain elevation in terrain-only mode, overwrite fits_disable[TVAO_TERR_ELEV_PIX]
			        getProperty(imp_pars, "BOOST_PARALLAX",      boost_parallax),         // final double             boost_parallax,     // increase weight of scene with maximal parallax relative to the reference scene
					getProperty(imp_pars, "MAX_PARALLAX",        max_parallax),           // final double             max_parallax,  // do not consider maximal parallax above this (consider it a glitch)
					
					debugLevel,                                                           // final int                debugLevel);
					debug_path,                                                           // final String             debug_path,
					debug_save_improved,                                                  // final boolean            debug_save_improved, // Save debug image after successful LMA step."); 
					debug_save_worsened);                                                 // final boolean            debug_save_worsened) // Save debug image after unsuccessful LMA step.");
				
			terr_elev_last_success =    getProperty(imp_pars,"TERR_ELEV_LAST_SUCCESS",terr_elev_last_success);
			
			num_pars[TVAO_TERRAIN] =          Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERRAIN"));
			num_pars[TVAO_VEGETATION] =       Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_VEGETATION"));
			num_pars[TVAO_ALPHA] =            Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_ALPHA"));
			num_pars[TVAO_ELEVATION] =        Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_ELEVATION"));
			num_pars[TVAO_TERR_ELEV] =        Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERR_ELEV"));
//			num_pars[TVAO_TERR_ELEV_PIX] =    Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_TERR_ELEV_PIX"));
			num_pars[TVAO_TERR_ELEV_PIX] =    getProperty(imp_pars, "NUM_PARS_TERR_ELEV_PIX",       num_pars[TVAO_TERR_ELEV_PIX]);    			
			num_pars[TVAO_SCENE_OFFSET] =     Integer.parseInt((String) imp_pars.getProperty("NUM_PARS_SCENES"));
			ind_pars[TVAO_TERRAIN] =          Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERRAIN"));
			ind_pars[TVAO_VEGETATION] =       Integer.parseInt((String) imp_pars.getProperty("IND_PARS_VEGETATION"));
			ind_pars[TVAO_ALPHA] =            Integer.parseInt((String) imp_pars.getProperty("IND_PARS_ALPHA"));
			ind_pars[TVAO_ELEVATION] =        Integer.parseInt((String) imp_pars.getProperty("IND_PARS_ELEVATION"));
			// bug fix (remove later)
			if (imp_pars.getProperty("TVAO_TERR_ELEV") != null) { // bug/typo fix
				imp_pars.setProperty("IND_PARS_TERR_ELEV",  imp_pars.getProperty("TVAO_TERR_ELEV"));
			}
			if (imp_pars.getProperty("IND_PARS_TERR_ELEV") == null) {
				ind_pars[TVAO_TERR_ELEV] =     ind_pars[TVAO_ELEVATION]+num_pars[TVAO_ELEVATION];
				ind_pars[TVAO_TERR_ELEV_PIX] = ind_pars[TVAO_TERR_ELEV]+num_pars[TVAO_TERR_ELEV];
			} else {
				ind_pars[TVAO_TERR_ELEV] =     Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERR_ELEV"));
				if (imp_pars.getProperty("TVAO_TERR_ELEV_PIX") == null) {
					ind_pars[TVAO_TERR_ELEV_PIX] = ind_pars[TVAO_TERR_ELEV]+num_pars[TVAO_TERR_ELEV];
				} else {
					ind_pars[TVAO_TERR_ELEV_PIX] = Integer.parseInt((String) imp_pars.getProperty("IND_PARS_TERR_ELEV_PIX"));
				}
			}
			ind_pars[TVAO_SCENE_OFFSET] =      Integer.parseInt((String) imp_pars.getProperty("IND_PARS_SCENES"));
			
			par_index = new int[TVAO_TYPES][];
			for (int n = 0; n < par_index.length; n++) {
				if (n == TVAO_TERR_ELEV) {
					par_index[n] = new int[1]; // terr_elevation_full? image_length: 1]; // num_pars[TVAO_TERR_ELEV]?
				} else 	if (n < TVAO_SCENE_OFFSET) {
					par_index[n] = new int[image_length];		
				} else {
					par_index[n] = new int[num_scenes];		// same as when creating original
				}
				Arrays.fill(par_index[n], -1);
			}
			//			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..4) and full window pixel index
			int [] par_nums = {
					num_pars[TVAO_TERRAIN],
					num_pars[TVAO_VEGETATION],
					num_pars[TVAO_ALPHA],
					num_pars[TVAO_ELEVATION],
					num_pars[TVAO_TERR_ELEV],
					num_pars[TVAO_TERR_ELEV_PIX],
					num_pars[TVAO_SCENE_OFFSET]};

			int [][] samples_pointers_tmp = new int [SAMPLES_SIZE][3];
			update_samples_pointers: {
				for (int nsp = 0; nsp < SAMPLES_SIZE; nsp++) {
					for (int i = 0; i < samples_pointers_tmp[nsp].length; i++) {
						if (imp_pars.getProperty("SAMPLES_POINTERS_"+nsp+"_"+i) == null) {
							System.out.println("restoreParametersFile(): Incompatible samples_pointers[][], will not update");
							break update_samples_pointers;
						}
						samples_pointers_tmp[nsp][i] = Integer.parseInt((String) imp_pars.getProperty("SAMPLES_POINTERS_"+nsp+"_"+i));
					}
				}
				samples_pointers = samples_pointers_tmp;
			}
			// samples_pointers[SAMPLES_TOTAL][0] = total number of parameters
			int num_pars = 0;
			for (int n = 0; n < TVAO_TYPES; n++) {
				num_pars += par_nums[n];
			}
//			parameters_vector = new double [num_pars];
			parameters_vector = new double [num_pars];
//			if (terr_elevation_full) { // only initialize if terrain elevation was saved in the image, otherwise keep new initialized
//				par_rindex = new int [num_pars][2];
//			}
//			for (int n = 0; n < TVAO_TYPES; n++) {
			for (int t = 0; t < SAVE_TYPES.length; t++) {
				int n = SAVE_TYPES[t]; // skipping TVAO_TERR_ELEV (common offset)
//				if ((n == TVAO_TERR_ELEV_PIX) && !terr_elevation_full) {
//					n = TVAO_TERR_ELEV;
//				}
				for (int indx = 0; ((indx < indices[t].length) && (indx < par_index[n].length)); indx++) { // woi_max
					int findx = indx;
					int pindx = indices[t][indx];
					// in old mode (!terr_elevation_full) will read TVAO_TERR_ELEV to TVAO_TERR_ELEV_PIX (single-element)
					if ((n < TVAO_SCENE_OFFSET) && (n != TVAO_TERR_ELEV)) { // (n == TVAO_TERR_ELEV) can only be if explicitly set "n = TVAO_TERR_ELEV"
						int x = (indx % woi_max.width) + woi_max.x;
						int y = (indx / woi_max.width) + woi_max.y;
						findx = x + y * full.width;
					}
					par_index[n][findx] = pindx;
					if (pindx >= 0) { // always? - not!
						parameters_vector[pindx] = data[t][indx];
						par_rindex[pindx][0] = n;     // type
						par_rindex[pindx][1] = findx; // full index
					}
				}
			}
			// fix old mode
			/*
			if (!terr_elevation_full) { // provided TVAO_TERR_ELEV, set it to all defined elements of TVAO_TERR_ELEV_PIX
				double terr_elev_avg = parameters_vector[ind_pars[TVAO_TERR_ELEV]];
				for (int i = 0; i < this.num_pars[TVAO_TERR_ELEV_PIX]; i++) {
					int pindx = ind_pars[TVAO_TERR_ELEV_PIX]+ i;
					parameters_vector[pindx] = terr_elev_avg;
					// Do not update keep as initialized as this data was not saved in the image 
	///				par_index[TVAO_TERR_ELEV_PIX][i] = pindx;
	///				par_rindex[pindx][0] = TVAO_TERR_ELEV_PIX;
				}
				
			}
			*/
			{
				int pindx = ind_pars[TVAO_TERR_ELEV];
				parameters_vector[pindx] =     0; // zero offset to average
				par_index[TVAO_TERR_ELEV][0] = pindx;
				par_rindex[pindx][0] = TVAO_TERR_ELEV;
				par_rindex[pindx][1] = 0; // full index
			}
			
			for (int t = 0; t < SAVE_TYPES.length; t++) {
				int n = SAVE_TYPES[t];
				if (n ==TVAO_SCENE_OFFSET) {
					tvao_data[n] = new double [num_scenes];
					if ( data[t] != null) {
						System.arraycopy(data[t], 0, tvao_data[n],0, num_scenes);
					}
				} else {
					tvao_data[n] = data[t];
				}
			}
			for (int n = 0; n < tvao_data.length; n++) {
				if (tvao_data[n] == null) {
					tvao_data[n] = new double [width*height];
				}
			}
			// TODO: build other data structures and make instance consistent, without it this does not allow reading from file
			boolean show_final_result = false;
			if ((show_final_result) && (debugLevel > -2)) { // 0)) {
				String reconstructed_title = "reference_scene"+"-recnstr-pre-step"; // +step_restore; // 
				showYfX(
						null, // double [] vector,
						reconstructed_title); // String title)
			}

			
			
		}
		return tvao_data;
	}
	
	
	
	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 VegetationSegment [] readAllSegments(
			String        dir_path,
			String        suffix,
			double        transparency_opaque,
			double        transparency_pedestal,
			double        transparency_frac,
			double        transparency_dist,			
			double        transparency_pow,			
			double        transparency_gb,
			// TODO: See if pass ttop_* parameters, common to all instead of the ones from the parameters files.
			int           debugLevel,
			String        debug_path,
			boolean       debug_save_improved, // Save debug image after successful LMA step."); 
			boolean       debug_save_worsened) { // Save debug image after unsuccessful LMA step.");
		
		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(suffix+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];
		int dbg_n = 13;
		boolean bug_check = false; // true;
		
		for (int n = 0; n < par_files.length; n++) {
			if (debugLevel > -3) {
				System.out.println("====== readAllSegments(): n="+n+ "(of "+(par_files.length)+"), "+par_files[n].getPath());
			}
			if (n==dbg_n) {
				System.out.println("readAllSegments(): n="+n);
			}// updates this to match file
			double [][] tvao =	restoreParametersFile( // should be 6, not 5 FIXME: Either here or inside
					par_files[n].getPath(), //  String    path,
					(bug_check? -3 : debugLevel),             // int           debugLevel,
					debug_path, // 		String        debug_path,
					debug_save_improved,    // boolean       debug_save_improved, // Save debug image after successful LMA step."); 
					debug_save_worsened);   //boolean       debug_save_worsened) // Save debug image after unsuccessful LMA step.");
			if ((woi_veg != null) && !woi_veg.contains(woi)) {
				System.out.println("****** readAllSegments(): bad data, woi_veg does not contain woi!");
				System.out.println("----- readAllSegments() segment "+(n+1)+" (of "+par_files.length+")");
				System.out.println("----- woi="+woi.toString()+", woi_veg="+woi_veg.toString());
				System.out.println("----- "+par_files[n].getPath());
			}
			if (!bug_check) {
				double [] confidence = getTransparencyConfidence( // woi
						null,                  // final double []   vector,
						transparency_opaque,   // final double      transparency_opaque,
						transparency_pedestal, // final double      transparency_pedestal,
						transparency_frac,     // final double      transparency_frac,
						transparency_dist,     // final double      transparency_dist,			
						transparency_pow,      // final double      transparency_pow,			
						transparency_gb,       // final double      transparency_gb,
						ttop_en,               // final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
						ttop_gb,               // final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
						ttop_min,              // final double      ttop_min,     //     3.0;   // Minimal tree top elevation
						ttop_rel_lev,          // final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
						ttop_rel_rad,          // final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
						ttop_frac,             // final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
						ttop_rem_rad,          // final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
						null);                 // transparency_data);    // final double [][][] transparency_data) { // double [3][][]
				segments[n] = new VegetationSegment(
						par_files[n].getPath(),  //  String    path, 
						woi_max, // woi_veg,                 // Rectangle   woi_veg,
						woi,                     // Rectangle   woi,
						terrain_offset,          // terrain_offset,
						tvao[TVAO_SCENE_OFFSET], // double []   scene_offsets,
						// terrain, vegetation, alpha, elevation, terrain_elevation
						new double [][] {tvao[0],tvao[1],tvao[2],tvao[3],tvao[5]}, // double [][]  tva) {} {} // skipping common terrain elevation
						confidence,              // double []   confidence
						has_vegetation);               //boolean []  overlaid)
				if (debugLevel>-2) {
					Runtime runtime = Runtime.getRuntime();
					runtime.gc();
					System.out.println("----- readAllSegments() segment "+(n+1)+" (of "+par_files.length+") --- Free memory="+runtime.freeMemory()+" (of "+runtime.totalMemory()+")");
					if (woi_veg == null) {
						System.out.println("----- woi="+woi.toString()+", woi_veg= null (terrain-only)");
					} else {
						System.out.println("----- woi="+woi.toString()+", woi_veg="+woi_veg.toString());
					}
					System.out.println("----- "+par_files[n].getPath());
				}
			}
		}
		// 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 double debugDerivs (
			double [] vector,
			double    delta,
			int       debugLevel,
			boolean   show_img) {
		double [][] jt = new double [vector.length][];
		boolean dbg_reset_before_elevation = false;
		getFxDerivs(
				vector,      // final double []   vector,
				jt,          // final double [][] jt, // should be null or initialized with [vector.length][]
				debugLevel); // final int         debug_level)
		double  [][] jt_delta =  getFxDerivsDelta (
				vector, // double []         vector,
				delta,  // final double      delta,
				debugLevel - 10); // 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++) {
			if (dbg_reset_before_elevation && (n == ind_pars[TVAO_ELEVATION])) {
				max_err = 0;
			}
			for (int w = 0; w < jt_diff[0].length; w++) {
				jt_diff[n][w] = jt[n][w] - jt_delta[n][w];
				if ((Math.abs(jt_diff[n][w]) > max_err) || (Math.abs(jt_diff[n][w]) > .001)) {
					int [] typ_offs = samplesTypeOffset(w);
					int [] ti = par_rindex[n];
					String s1 = String.format("n=%5d (%2d:%6d), w= %6d (%2d:%6d)",n, ti[0], ti[1], w, typ_offs[0],typ_offs[1]);
					String s2 = String.format(" diff=%12g, jt=%12.8f, jt_delta=%12.8f",jt_diff[n][w],jt[n][w],jt_delta[n][w]);
					String s3 = String.format("%53s",""); 
					if (typ_offs[0] == SAMPLES_Y) {
						int ny = typ_offs[1];
						int nscene =  y_src[ny][YSRC_SCENE];
						int findx =   y_src[ny][YSRC_FINDEX];
						int windx =   getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
						int x = findx % full.width;
						int y = findx / full.width;
						int wx = windx % woi.width;
						int wy = windx / woi.width;
						s3 = String.format("  scene=%3d, findx=%6d(%3d:%3d), windx=%4d(%2d:%2d)",nscene,findx,x,y,windx,wx,wy);
					} else if (typ_offs[0] == SAMPLES_Y_HF) {
						int ny = typ_offs[1];
						int nscene =  y_src_hf[ny][YSRC_SCENE];
						int findx =   y_src_hf[ny][YSRC_FINDEX];
						int windx =   getWoiIndex(null, woi, full, findx); //; // getWindexFromFindex(findx);
						int x = findx % full.width;
						int y = findx / full.width;
						int wx = windx % woi.width;
						int wy = windx / woi.width;
						s3 = String.format(" *scene=%3d, findx=%6d(%3d:%3d), windx=%4d(%2d:%2d)",nscene,findx,x,y,windx,wx,wy);
					}
					if        (Math.abs(jt_diff[n][w]) > max_err) {
						System.out.println("debugDerivs(): "+s1+s3+s2);
					} else if (Math.abs(jt_diff[n][w]) > .001) {
						System.out.println("debugDerivs(): "+s1+s3+s2);
					} 
					max_err = Math.max(max_err, Math.abs(jt_diff[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;
	}
	
	public int [] samplesTypeOffset (int sample_index) {
		for (int typ = 0; typ < SAMPLES_TOTAL; typ++) {
			if (samples_pointers[typ+1][0]>sample_index) {
				return new int [] {typ , sample_index - samples_pointers[typ][0]};
			}
		}
		return null;
	}

	
	private void setupWeights( // after setupParametersIndices
			final double [] scene_weights_in,
			final double reg_weights, // relative to y_vector samples -> fraction of 1.0
			final double hifreq_weight,
			final int    debugLevel) {
//		double terrain_correction_weight = 1.0;
		boolean use_hf =   (y_src_hf != null);
		int y_avg_len = y_vector.length - (y_src.length + ((y_src_hf != null) ? y_src_hf.length : 0));
		
		boolean use_scenes_pull0 = (scenes_pull0 >=0);
		// using >=0 no use 0 as NOP but reserve space, <0 - do not reserve space
		samples_pointers = new int [SAMPLES_SIZE][3]; // {start, length}
		samples_pointers[SAMPLES_Y][1] =                   y_src.length;
		if (y_src_hf != null) {
			samples_pointers[SAMPLES_Y_HF][1] =            y_src_hf.length;
		}
		if (y_avg_len > 0) {
			samples_pointers[SAMPLES_Y_AVG][1] =          y_avg_len;
		}
		if (use_scenes_pull0) {
			samples_pointers[SAMPLES_SCENES][1] =          1;
		}
		if  (fits[TVAO_ALPHA] && ((alpha_loss > 0) || (alpha_push > 0))) {
			samples_pointers[SAMPLES_ALPHA_PULL][1] =      num_pars[TVAO_ALPHA];
			samples_pointers[SAMPLES_ALPHA_PULL][2] =      ind_pars[TVAO_ALPHA];
		}
		if  (fits[TVAO_ALPHA] && ((alpha_lpf >= 0) || (alpha_lpf_border >= 0))) {
			samples_pointers[SAMPLES_ALPHA_LPF][1] =       num_pars[TVAO_ALPHA];
			samples_pointers[SAMPLES_ALPHA_LPF][2] =      ind_pars[TVAO_ALPHA];
		}
		if  (fits[TVAO_TERRAIN] && (terr_pull0 >= 0)) {
			samples_pointers[SAMPLES_TERRAIN_PULL][1] =    num_pars[TVAO_TERRAIN];
			samples_pointers[SAMPLES_TERRAIN_PULL][2] =    ind_pars[TVAO_TERRAIN];
		}
		if  (fits[TVAO_TERRAIN] && (terr_lpf >= 0)) {
			samples_pointers[SAMPLES_TERRAIN_LPF][1] =     num_pars[TVAO_TERRAIN];
			samples_pointers[SAMPLES_TERRAIN_LPF][2] =     ind_pars[TVAO_TERRAIN];
		}
		if  (fits[TVAO_VEGETATION] && (veget_pull0 >= 0)) {
			samples_pointers[SAMPLES_VEGETATION_PULL][1] = num_pars[TVAO_VEGETATION];
			samples_pointers[SAMPLES_VEGETATION_PULL][2] = ind_pars[TVAO_VEGETATION];
		}
		if  (fits[TVAO_VEGETATION] && (veget_lpf >= 0)) {
			samples_pointers[SAMPLES_VEGETATION_LPF][1] =  num_pars[TVAO_VEGETATION];
			samples_pointers[SAMPLES_VEGETATION_LPF][2] =  ind_pars[TVAO_VEGETATION];
		}
		if  (fits[TVAO_ELEVATION] && (elevation_pull0 >= 0)) {
			samples_pointers[SAMPLES_ELEVATION_PULL][1] =  num_pars[TVAO_ELEVATION];
			samples_pointers[SAMPLES_ELEVATION_PULL][2] =  ind_pars[TVAO_ELEVATION];
		}
		if  (fits[TVAO_ELEVATION] && (elevation_lpf >= 0)) {
			samples_pointers[SAMPLES_ELEVATION_LPF][1] =   num_pars[TVAO_ELEVATION];
			samples_pointers[SAMPLES_ELEVATION_LPF][2] =   ind_pars[TVAO_ELEVATION];
		}
		if  (fits[TVAO_ELEVATION] && fits[TVAO_ALPHA] && (elev_alpha_en)) {
			samples_pointers[SAMPLES_ELEVATION_ALPHA][1] = num_pars[TVAO_ELEVATION];
			samples_pointers[SAMPLES_ELEVATION_ALPHA][2] = ind_pars[TVAO_ELEVATION];
		}
		
		if  (fits[TVAO_TERR_ELEV_PIX] && (terr_elev_pull0 >= 0)) {
			samples_pointers[SAMPLES_TERR_ELEV_PULL][1] =  num_pars[TVAO_TERR_ELEV_PIX];
			samples_pointers[SAMPLES_TERR_ELEV_PULL][2] =  ind_pars[TVAO_TERR_ELEV_PIX];
		}
		
		if  (fits[TVAO_TERR_ELEV_PIX] && (terr_elev_lpf >= 0)) {
			samples_pointers[SAMPLES_TERR_ELEV_LPF][1] =  num_pars[TVAO_TERR_ELEV_PIX];
			samples_pointers[SAMPLES_TERR_ELEV_LPF][2] =  ind_pars[TVAO_TERR_ELEV_PIX];
		}
		
		for (int i = 1; i < samples_pointers.length; i++) {
			samples_pointers[i][0] = samples_pointers[i-1][0] + samples_pointers[i-1][1];  
		}
		samples_pointers[SAMPLES_TOTAL][1] = samples_pointers[SAMPLES_TOTAL][0] - samples_pointers[SAMPLES_EXTRA][0];

		int extra_samples = samples_pointers[SAMPLES_TOTAL][1];

		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 ;
			}
		}
		
		if (y_avg_len > 0) {
			for (int ny = 0; ny < y_avg_len; ny++) {
				s += terrain_correction_weight; // scene_weights[nscene] is included in y_wavg and fX, jt
			}
		}
		
		
///		double s_pull_0 = s_scenes * scenes_pull0 * woi.width*woi.height;
		final double s0 = s;
		s /= (1 - reg_weights); // *= (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  < samples_pointers[SAMPLES_Y_HF][0]) { // DC differences
							weights[nw] = scene_weights[y_src[nw][YSRC_SCENE]] * k;
							if (Double.isNaN(weights[nw])) {
								System.out.println("setupWeights() weights["+nw+"]=NaN - 1");
							}

						} else if (nw  < samples_pointers[SAMPLES_Y_AVG][0]) {	// 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("setupWeights() weights["+nw+"]=NaN - 2");
							}
						} else if (nw  < samples_pointers[SAMPLES_EXTRA][0]) {	// y_wavg if exists
							weights[nw] = k * terrain_correction_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("setupWeights() weights["+nw+"]=NaN - 3");
							}
							
						} else {
							weights[nw] = reg_sample_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("setupWeights() weights["+nw+"]=NaN - 4");
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double sw = 0;
		for (int n = 0; n < (samples_pointers.length -1); n++) {
			double swn = 0;
			for (int i = samples_pointers[n][0]; i < (samples_pointers[n][0]+samples_pointers[n][1]); i++){
				swn+= weights[i];
			}
			sw += swn;
			if (debugLevel > -2) {
				System.out.println("sum weights["+n+"] = "+swn+", total = "+sw);
			}
		}
		return;
	}
	
	public double [] getElevation (  // 0.0 in undefined pixels
			double [] vector_in,
			boolean   only_parameters) {
		if (woi_veg == null) {
			return null;
		}
		final double [] vector = (vector_in != null) ? vector_in : parameters_vector;
		final int woi_veg_length = woi_veg.width*woi_veg.height;
		final double [] elevations = new double [woi_veg_length]; 
		final boolean elevation_parameters = (num_pars[TVAO_ELEVATION] > 0);
		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 wvindex = ai.getAndIncrement(); wvindex < woi_veg_length; wvindex = ai.getAndIncrement()) {
						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 fpix = x + y * full.width;
						if (elevation_parameters) {
							int npar = par_index[TVAO_ELEVATION][fpix]; 
							if (npar >= 0) {
								elevations[wvindex] = vector[npar];
							} else if (!only_parameters) {
								elevations[wvindex] = tvao[TVAO_ELEVATION][fpix]; // use default
							}
						} else {
							elevations[wvindex] =  tvao[TVAO_ELEVATION][fpix];
						}
						if (Double.isNaN(elevations[wvindex])) {
							elevations[wvindex] = 0;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return elevations;
	}

	public double [] getAlpha ( // NaN in undefined pixels
			double [] vector_in,
			boolean   only_parameters) {
		final double [] vector = (vector_in != null) ? vector_in : parameters_vector;
		if (woi_veg==null) {
			return null;
		}
		final int woi_veg_length = woi_veg.width*woi_veg.height;
		final double [] alphas = new double [woi_veg_length];
		Arrays.fill(alphas, Double.NaN);
		final boolean alpha_parameters = (num_pars[TVAO_ALPHA] > 0);
		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 wvindex = ai.getAndIncrement(); wvindex < woi_veg_length; wvindex = ai.getAndIncrement()) {
						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 fpix = x + y * full.width;
						if (alpha_parameters) {
							int npar = par_index[TVAO_ALPHA][fpix]; 
							if (npar >= 0) {
								alphas[wvindex] =  Math.min(Math.max(vector[npar],0),1.0);
							} else if (!only_parameters) {
								alphas[wvindex] = tvao[TVAO_ALPHA][fpix]; // use default
							}
						} else {
							alphas[wvindex] =  tvao[TVAO_ALPHA][fpix];
						}
						if (!only_parameters && Double.isNaN(alphas[wvindex])) {
							alphas[wvindex] = 0;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return alphas;
	}
	
	public double [] getVegetation ( // NaN in undefined pixels
			double [] vector_in,
			boolean   only_parameters) {
		if (woi_veg == null) {
			return null;
		}
		final double [] vector = (vector_in != null) ? vector_in : parameters_vector;
		final int woi_veg_length = woi_veg.width*woi_veg.height;
		final double [] vegetations = new double [woi_veg_length]; 
		Arrays.fill(vegetations, Double.NaN);
		final boolean vegetation_parameters = (num_pars[TVAO_VEGETATION] > 0);
		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 wvindex = ai.getAndIncrement(); wvindex < woi_veg_length; wvindex = ai.getAndIncrement()) {
						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 fpix = x + y * full.width;
						if (vegetation_parameters) {
							int npar = par_index[TVAO_VEGETATION][fpix]; 
							if (npar >= 0) {
								vegetations[wvindex] = vector[npar];
							} else if (!only_parameters) {
								vegetations[wvindex] = tvao[TVAO_VEGETATION][fpix]; // use default
							}
						} else {
							vegetations[wvindex] =  tvao[TVAO_VEGETATION][fpix];
						}
						if (!only_parameters && Double.isNaN(vegetations[wvindex])) {
							vegetations[wvindex] = 0;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return vegetations;
	}
	
	
	public double [][] getTransparency(// [scene][windx]
			double [] vector_in,
			boolean   only_parameters) {
		final double [] vector = (vector_in != null) ? vector_in : parameters_vector;
		final int woi_length = woi.width*woi.height;
		final double [][] transparency = new double [num_scenes][woi_length];
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			Arrays.fill(transparency[nscene], Double.NaN);
		}
		double [][] extra_data = new double [1][]; // [0] will be alpha mapped to elevation offsets
		getFxDerivs(
				vector,     // final double []   vector,
				null,       // final double [][] jt, // should be null or initialized with [vector.length][]
				extra_data, // final double [][] debug_data, // {elev* alpha shifted, terrain *(1-alpha) shifted, elev_sum_weights} 
				-1);        // final int         debug_level)
		double [] alpha = extra_data[0]; 
		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 findx =  y_src[ny][YSRC_FINDEX];
						int windx =  getWoiIndex(null, woi, full, findx); //getWindexFromFindex(findx);
						double t =   1.0 -  alpha[ny];
						transparency[nscene][windx] = t;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return transparency;
	}
	
	public double [][] getTransparenctWeights(
			final double [][] transparency,
			final double      transparency_opaque,
			final double      transparency_pedestal,
			final double      transparency_frac,
			final double      transparency_dist, //0 - do not apply
			final double      transparency_pow){	
		final int dbg_windx = -113;
		final int woi_length = woi.width*woi.height; 
		final double [][] transparency_weights =  new double [num_scenes][woi_length];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
//		for (int nscene = 0; nscene < num_scenes; nscene++) {
//			Arrays.fill(transparency_weights[nscene], Double.NaN);
//		}
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] transp = new double [num_scenes];
					for (int wPix = ai.getAndIncrement(); wPix < woi_length; wPix = ai.getAndIncrement()) {
						if (wPix == dbg_windx) {
							System.out.println("getTransparenctWeights(): wPix="+wPix);
						}
						Arrays.fill(transp, Double.NaN);
						double t_max = Double.NaN;
						int n_max = -1;
						for (int nscene = 0; nscene < transp.length; nscene++) {
							double t = transparency[nscene][wPix];
							transp[nscene] = t;
							if (!Double.isNaN(t) && !(t_max >= t)) {
								t_max = t;
								n_max = nscene;
							}
						}
						if (!Double.isNaN(t_max)) {
							double t_max_offs = t_max - transparency_opaque;
							if (t_max_offs > 0) {
								int fpix = getWoiIndex(woi, null, full, wPix); // getFindexFromWindex(wPix);
								double [] scale_xy_max = scales_xy[n_max][fpix];
								for (int nscene = 0; nscene < transp.length; nscene++) if (!Double.isNaN(transp[nscene])){
									double tf = (transp[nscene] - transparency_opaque) / t_max_offs;
									if (tf >= transparency_frac) {
										if (transparency_dist > 0) {
											double scale_dist = 0;
											double [] scale_xy = scales_xy[nscene][fpix];
											if ((scale_xy_max != null) && (scale_xy != null)) {
												double dx = scale_xy[0] - scale_xy_max[0];
												double dy = scale_xy[1] - scale_xy_max[1];
												scale_dist = Math.sqrt(dx*dx+dy*dy);
											}
											tf *= (scale_dist + transparency_dist)/transparency_dist;
										}
									} else {
										tf = 0;
									}
									if (transparency_pow != 1.0) {
										tf = Math.pow(tf, transparency_pow);
									}
									transparency_weights[nscene][wPix] = tf + transparency_pedestal;  // all those that were not NaN will be at least transparency_pedestal
								}							
							} else {
								for (int nscene = 0; nscene < transp.length; nscene++) if (!Double.isNaN(transp[nscene])){
									transparency_weights[nscene][wPix] = transparency_pedestal;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return transparency_weights;
	}
	
	public double [] getTerrainConfidence( // woi.length
			double [][] transparency_weights) {
		final int woi_length = woi.width*woi.height; 
		final double [] transparency_confidence =  new double [woi_length];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		Arrays.fill(transparency_confidence, Double.NaN);
		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()) {
						double sd2 = 0;
						for (int nscene = 0; nscene < transparency_weights.length; nscene++) if (transparency_weights[nscene][wPix] > 0){
							double d = transparency_weights[nscene][wPix];
							sd2 += d * d;
						}
						transparency_confidence[wPix] = Math.sqrt(sd2);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return transparency_confidence;
		
	}
	
	public double [] getTransparencyConfidence(
			final double []   vector,
			final double      transparency_opaque,
			final double      transparency_pedestal,
			final double      transparency_frac,
			final double      transparency_dist,			
			final double      transparency_pow,			
			final double      transparency_gb,
			final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
			final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
			final double      ttop_min,     //     3.0;   // Minimal tree top elevation
			final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
			final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
			final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
			final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
			final double [][][] transparency_data) { // double [3][][]
		final double [] border_weights = {0.01, 0.03, 0.1, 0.3};
		final boolean apply_transparency_fade = true;
		final int woi_length =     woi.width * woi.height;
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][] transparency = getTransparency((vector==null)? parameters_vector:vector, false);
		final double [][] transparency_raw = transparency.clone(); // shallow
		double [][] transparency_fade = null; 
		if ((apply_transparency_fade) && (woi_veg != null)) { 
			transparency_fade = getSceneTransparencyFade(
					vector,          // final double []  vector,       // parameters vector or null
					border_weights); // final double []  border_weights);
			for (int i = 0; i < transparency.length; i++) {
				transparency[i] = new double [woi_length];
			}
			final double [][] transparency_fade_final =transparency_fade;
			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 windx = 0; windx < woi_length; windx++) {
								transparency[nScene][windx] = transparency_raw[nScene][windx] * transparency_fade_final[nScene][windx]; 
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		
		double [][] transparency_weights_preblur = getTransparenctWeights( // should be no NaNs!
				transparency,          // final double [][] transparency,
				transparency_opaque,   // final double      transparency_opaque,
				transparency_pedestal, // final double      transparency_pedestal,
				transparency_frac,     // final double      transparency_frac,
				transparency_dist,     // final double      transparency_dist); //0 - do not apply
				transparency_pow);     // final double      transparency_pow){
		final double [][] transparency_weights = new double [num_scenes][];
		if (transparency_gb <= 0) {
			for (int n = 0; n < transparency_weights.length; n++) {
				transparency_weights[n] = transparency_weights_preblur[n];
			}
		} else {
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						DoubleGaussianBlur gb = new DoubleGaussianBlur();
						for (int nScene = ai.getAndIncrement(); nScene < num_scenes; nScene = ai.getAndIncrement()) {
							transparency_weights[nScene] = transparency_weights_preblur[nScene].clone();
							gb.blurDouble(
									transparency_weights[nScene], //
			    			woi.width, // terrain woi
			    			woi.height,
			    			transparency_gb,     // double sigmaX,
			    			transparency_gb,     // double sigmaY,
			    			0.01);           // double accuracy)
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		if (transparency_data != null) {
			transparency_data[0] = transparency;
			transparency_data[1] = transparency_weights_preblur;
			transparency_data[2] = transparency_weights;
			if (transparency_data.length > 4) {
				transparency_data[3] = transparency_fade;
				transparency_data[4] = transparency_raw;
			}
		}
		double [] confidence = getTerrainConfidence(transparency_weights); // blurred
		return confidence;
	}
	
	
	
	/**
	 * Calculate weights of vegetation fade near the edges to use for weight reduction 
	 * @param vector          parameters vector or null to use current 
	 * @param border_weights array such as {0.01,0.03,0.1,0.3} to specify border fading. All outside
	 *                       the defined vegetation pixels will have weight 0.0, their ortho neighbors
	 *                       border_weights[0], their ortho neighbors - border_weights[1], etc. The
	 *                       inside area weight is 1.0
	 * @return weight in linescan order inside woi_veg. 
	 */
	public double [] getVegetationFade(
			final double []  vector,       // parameters vector or null
			final double []  border_weights) {
		if (woi_veg == null) {
			return null;
		}
		final int woi_veg_length = woi_veg.width * woi_veg.height;
		double [] alphas =       getAlpha (vector, true);
		double [] wnd = new double [woi_veg_length];
		boolean [] mask = new boolean [woi_veg_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 n = ai.getAndIncrement(); n < woi_veg_length; n = ai.getAndIncrement()) {
						mask[n] = !Double.isNaN(alphas[n]);
						if (mask[n]) {
							wnd[n] = Double.NaN;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		TileNeibs tn = new TileNeibs(woi_veg.width,woi_veg.height);
		for (int step = 0; step < border_weights.length; step++) {
			final double w = border_weights[step];
			final boolean last = step ==  border_weights.length -1;
			tn.shrinkSelection(
					1,     // final int        shrink,           // grow tile selection by 1 over non-background tiles 1: 4 directions, 2 - 8 directions, 3 - 8 by 1, 4 by 1 more
					mask,  // final boolean [] tiles,  
					null); // final boolean [] prohibit)
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int n = ai.getAndIncrement(); n < woi_veg_length; n = ai.getAndIncrement()) {
							if (Double.isNaN(wnd[n])) {
								if (!mask[n]) {
									wnd[n] = w;
								} else if (last) {
									wnd[n] = 1.0;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return wnd;
	}
	
	double [][] getSceneTransparencyFade(
			final double []  vector,       // parameters vector or null
			final double []  border_weights) {
		if (woi_veg == null) {
			return null;
		}
		final int woi_veg_length = woi_veg.width * woi_veg.height;
		final double [] vector_fade = (vector == null) ? parameters_vector.clone() : vector.clone();
		double [] vegetation_fade = getVegetationFade(
				vector,       // final double []  vector,       // parameters vector or null
				border_weights); // final double []  border_weights)
		double [] alpha_fade = new double [woi_veg_length];
		for (int i = 0; i < woi_veg_length; i++) {
			alpha_fade[i] = 1.0 - vegetation_fade[i];
		}
		
		// replace alpha with vegetation_fade in both vectro and tvao, saving tvao before and restoring it after
		
		double [] tvao_alpha = copyWoi ( // save tvao[TVAO_VEGETATION] inside woi_veg 
				full,                  // final Rectangle woi_src_in,
				woi_veg,               // final Rectangle woi_dst_in,
				full,                  // final Rectangle woi_full,
				tvao[TVAO_VEGETATION], // final double [] data_src,
			    null);                 // final double [] data_dst_in)
		
		copyWoi ( // save tvao[TVAO_VEGETATION] inside woi_veg 
				woi_veg,               // final Rectangle woi_src_in,
				full,                  // final Rectangle woi_dst_in,
				full,                  // final Rectangle woi_full,
				alpha_fade,            // final double [] data_src,
				tvao[TVAO_VEGETATION]);                 // final double [] data_dst_in)
		// replace alpha values in vector_fade
		
		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 = ai.getAndIncrement(); n < num_pars[TVAO_ALPHA]; n = ai.getAndIncrement()) {
						int np = n + samples_pointers[SAMPLES_ALPHA_PULL][2]; // index of the alpha parameter
						int findx = par_rindex[np][1]; // full pixel index
						if (findx >= 0) {
							vector_fade[np] = tvao[TVAO_VEGETATION][findx];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final double [][] transparency_fade = getTransparency(vector_fade, false);
		// restore tvao[TVAO_VEGETATION]
		copyWoi ( // save tvao[TVAO_VEGETATION] inside woi_veg 
				woi_veg,               // final Rectangle woi_src_in,
				full,                  // final Rectangle woi_dst_in,
				full,                  // final Rectangle woi_full,
				tvao_alpha,            // final double [] data_src,
				tvao[TVAO_VEGETATION]);// final double [] data_dst_in)
		return transparency_fade;
	}
	
	/**
	 * Get tree tops mask for woi_veg rectangle or null if there are no tree tops detected in this window
	 * @param vector        parameters vector or null (will use current parameters_vector
	 * @param ttop_en       if false, return null immediately
	 * @param ttop_gb       Elevation Gaussian blur sigma to detect tree tops
	 * @param ttop_min      Minimal tree top elevation
	 * @param ttop_rel_lev  Relative (to the top height) sample level
	 * @param ttop_rel_rad  Relative (to the top height) sample ring radius
	 * @param ttop_frac     Minimal fraction of the ring pixels below sample level
	 * @param ttop_rem_rad  Relative (to the top height) remove transparency radius
	 * @param dbg_prefix  - Generate image if not null
	 * @param debugLevel  - text debug verbosity
	 * @return array [woi_veg.width * woi_veg.height] for tree tops pixels (to be removed from transparency) 
	 */
	final boolean [] getTreeTops (
			final double []   vector,       // parameters vector or null  
			final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
			final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
			final double      ttop_min,     //     3.0;   // Minimal tree top elevation
			final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
			final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
			final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
			final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
			final boolean     ttop_no_border,//    true;  // No maximums on the border allowed 
			final String      dbg_prefix,
			final int         debugLevel) {
		if (!ttop_en || (woi_veg == null)) {
			return null;
		}
		final int woi_veg_length = woi_veg.width*woi_veg.height;
		final boolean [] tree_tops = new boolean [woi_veg_length];
		final double [] elevations = getElevation (vector,false);
		TileNeibs tn = new TileNeibs(woi_veg.width, woi_veg.height);
    	(new DoubleGaussianBlur()).blurDouble(
    			elevations, //
    			woi_veg.width,
    			woi_veg.height,
    			ttop_gb,     // double sigmaX,
    			ttop_gb,     // double sigmaY,
    			0.01);       // double accuracy)
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger anmax = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int wvindex = ai.getAndIncrement(); wvindex < woi_veg_length; wvindex = ai.getAndIncrement()) {
						find_max: {
							double elev_center = elevations[wvindex];
							if (elev_center > ttop_min) {
								for (int dir = 0; dir < TileNeibs.DIRS; dir++) {
									int wvindex1 = tn.getNeibIndex(wvindex, dir);
									if (wvindex1 >= 0) {
										if (elevations[wvindex1] > elev_center) {
											break find_max;
										}
									}
								}
								int wx = wvindex % woi_veg.width;   // relative to woi_veg
								int wy = wvindex / woi_veg.width;   // relative to woi_veg
								if (ttop_no_border && ((wx == 0) || (wy == 0) || (wx == (woi_veg.width - 1)) || (wy == (woi_veg.height - 1)))) {
									break find_max;
								}
								double level =         elev_center * ttop_rel_lev;
								double radius =        elev_center * ttop_rel_rad;
								double remove_radius = elev_center * ttop_rem_rad;
								int irad = (int) Math.ceil(radius);
								int y0 = Math.max(wy - irad, 0);
								int y1 = Math.min(wy + irad, woi_veg.height - 1);
								int x0 = Math.max(wx - irad, 0);
								int x1 = Math.min(wx + irad, woi_veg.width - 1);
								int n = 0, n_lower = 0;
								for (int y = y0; y <= y1; y++) {
									double dy = y - wy;
									double dy2 = dy*dy;
									for (int x = x0; x <= x1; x++) {
										double dx = x - wx;
										double dr = Math.sqrt(dy2 + dx*dx) - radius;
										if (Math.round(dr) == 0) {
											n ++;
											if (elevations[y * woi_veg.width + x] < level) {
												n_lower++;
											}
										}
									}
								}
								double frac = 1.0 * n_lower / n; 
								if (frac > ttop_frac) { // mark
									int iremove_radius = (int) Math.ceil(remove_radius);
									int yr0 = Math.max(wy - iremove_radius, 0);
									int yr1 = Math.min(wy + iremove_radius, woi_veg.height - 1);
									int xr0 = Math.max(wx - iremove_radius, 0);
									int xr1 = Math.min(wx + iremove_radius, woi_veg.width - 1);
									for (int y = yr0; y <= yr1; y++) {
										double dy = y - wy;
										double dy2 = dy*dy;
										for (int x = xr0; x <= xr1; x++) {
											double dx = x - wx;
											double r = Math.sqrt(dy2 + dx*dx);
											if (r <= remove_radius) {
												tree_tops[y * woi_veg.width + x] = true;
											}
										}
									}
									anmax.getAndIncrement();
									if (debugLevel > -2) {
										int findx = getWoiIndex(woi_veg, null, full, wvindex);
										int fx = findx % full.width; 
										int fy = findx / full.width;
										System.out.println ("getTreeTops(): found a tree top at xy=("+fx+","+fy+"), elevation = "+
										elev_center+ ">= "+ttop_min+ ", ring fraction="+frac+" >= "+ttop_frac+"." );
									}
									
									// debug print. Show results?
								} else {
									
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (dbg_prefix != null)  {
			double [] elevations_nonblurred = getElevation (vector, false);
			double [] alphas =       getAlpha (vector, true); //
			double [] vegetations =  getVegetation (vector, false);
			double [] transparency = new double [woi_veg_length];
			double [] ttops =        new double [woi_veg_length];
			double max_elev = 0;
			for (int i = 0; i < elevations.length; i++) {
				if (elevations[i] > max_elev) {
					max_elev = elevations[i];
				}
			}
			for (int i = 0; i < elevations.length; i++) {
				elevations_nonblurred[i] /= max_elev;
				elevations[i] /=            max_elev;
				transparency[i] = 1.0 - alphas[i];
				if (tree_tops[i]) {
					ttops[i] = 1.0;
					transparency[i] = 0;
				}
			}
			// elevations - blurred
			String [] dbg_titles = {"elevation","blurred","tree_tops","transparency","alpha","vegetation"};
			double [][] dbg_img = {elevations_nonblurred, elevations, ttops, transparency, alphas, vegetations};
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					woi_veg.width,
					woi_veg.height,
					true,
					dbg_prefix+"_TREE-TOPS-"+anmax.get(),
					dbg_titles);
		}
		
		if (anmax.get() == 0) {
			return null;
		}
		return tree_tops;
	}
	
	/**
	 * Clone vector and modify it's alpha making tree tops completely opaque (alpha = 1.0)
	 * @param vector    parameters vector or null (will use current parameters vector)
	 * @param tree_tops boolean array of tree tops pixels, matching Rectangle woi_veg  
	 * @return modified vector with tree tops opaque or the unmodified and not cloned vector if tree_tops in null 
	 */
	public double [] applyTreeTops(
			double [] vector,     
			boolean [] tree_tops) {
		if (tree_tops == null) {
			return (vector == null) ? parameters_vector : vector;
		} else {
			final double [] vector_applied = (vector == null) ? parameters_vector.clone() : vector.clone();
			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 = ai.getAndIncrement(); n < num_pars[TVAO_ALPHA]; n = ai.getAndIncrement()) {
							int np = n + samples_pointers[SAMPLES_ALPHA_PULL][2]; // index of the alpha parameter
							int findx = par_rindex[np][1]; // full pixel index
							if (findx >= 0) {
								int wvindx =  getWoiIndex(null, woi_veg, full, findx); // convert to woi_veg index 
								if (tree_tops[wvindx]) {
									vector_applied[np] = 1.0; // make opaque
								}
							}
							
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			return vector_applied;
		}
	}
	
	
	public double [] applyTransparency(
			final double []   vector,
			final double      transparency_opaque,
			final double      transparency_pedestal,
			final double      transparency_frac,
			final double      transparency_dist,			
			final double      transparency_pow,			
			final double      transparency_gb,
			final double      transparency_boost_in, 
			final boolean     recalc_average,
			final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
			final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
			final double      ttop_min,     //     3.0;   // Minimal tree top elevation
			final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
			final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
			final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
			final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
			final boolean     ttop_no_border,//    true;  // No maximums on the border allowed
			final String      dbg_prefix,
			final int         debugLevel) {
		this.recalc_average = recalc_average;
		final int woi_length = woi.width*woi.height;
		final double transparency_boost = Math.max(transparency_boost_in, 1.0);
		boolean use_hf =   (y_src_hf != null);		
		int y_avg_len = y_vector.length - (y_src.length + ((y_src_hf != null) ? y_src_hf.length : 0));
		double [][][] transparency_data = new double [5][][];
		boolean [] tree_tops = 	getTreeTops (
				vector,       // final double []   vector,       // parameters vector or null  
				ttop_en,      // final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
				ttop_gb,      // final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
				ttop_min,     // final double      ttop_min,     //     3.0;   // Minimal tree top elevation
				ttop_rel_lev, // final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
				ttop_rel_rad, // final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
				ttop_frac,    // final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
				ttop_rem_rad, // final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
				ttop_no_border,//final boolean     ttop_no_border,//    true;  // No maximums on the border allowed
				dbg_prefix,   // final String      dbg_prefix,
				debugLevel);  // debugLevel);  // final int         debugLevel) {
		double [] vector_mod = applyTreeTops(
				vector,       // double [] vector,     
				tree_tops);   // boolean [] tree_tops)
 
		double [] confidence = getTransparencyConfidence(
				vector_mod,                // final double []   vector,
				transparency_opaque,   // final double      transparency_opaque,
				transparency_pedestal, // final double      transparency_pedestal,
				transparency_frac,     // final double      transparency_frac,
				transparency_dist,     // final double      transparency_dist,			
				transparency_pow,      // final double      transparency_pow,			
				transparency_gb,       // final double      transparency_gb,
				ttop_en,               // final boolean     ttop_en,      //     false; // Remove tree tops from transparency weights
				ttop_gb,               // final double      ttop_gb,      //     1.0;   // Elevation Gaussian blur sigma to detect tree tops
				ttop_min,              // final double      ttop_min,     //     3.0;   // Minimal tree top elevation
				ttop_rel_lev,          // final double      ttop_rel_lev, //     0.9;   // Relative (to the top height) sample level
				ttop_rel_rad,          // final double      ttop_rel_rad, //     0.25;  // Relative (to the top height) sample ring radius
				ttop_frac,             // final double      ttop_frac,    //     0.5;   // Minimal fraction of the ring pixels below sample level
				ttop_rem_rad,          // final double      ttop_rem_rad, //     0.25;  // Relative (to the top height) remove transparency radius
				transparency_data);    // final double [][][] transparency_data) { // double [3][][]
		double [][] transparency =                 transparency_data[0];
		double [][] transparency_weights_preblur = transparency_data[1];
		double [][] transparency_weights =         transparency_data[2];
		double [][] transparency_fade =            transparency_data[3];
		double [][] transparency_raw =             transparency_data[4];

		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		double max_confidence = 0;
		for (int i = 0; i < confidence.length; i++) if (confidence[i] > max_confidence) {
			max_confidence = confidence[i];
		}
		double          max_scaled = max_confidence / transparency_boost;
		final double [] scales = new double [confidence.length];
		for (int i = 0; i < confidence.length; i++) {
			if (confidence[i] >=  max_scaled) {
				scales[i] = max_confidence / confidence[i];
			} else {
				scales[i] = transparency_boost;
			}
		}
		final double [][] scaled_weights = new double [num_scenes][woi_length];
		// normalize weights for confidence
		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()) {
						for (int windx = 0; windx < woi_length; windx++) {
							scaled_weights[nScene][windx] = transparency_weights[nScene][windx] * scales[windx];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		if (dbg_prefix != null) {
			String title =dbg_prefix+String.format("-opq%3f-pdst%5f-frac%3f-dst%2f-pwr%4f-gb%4f-boost%4f-avg%1d.tiff",
					transparency_opaque,transparency_pedestal,transparency_frac,
							transparency_dist,transparency_pow,transparency_gb, transparency_boost, recalc_average?1:0);
			int nscenes = transparency.length;
			String [] titles_top = {"transparency_fade", "transparity_raw", "transparency","pre-blur","weights","normalized"};
			String [] titles = new String[nscenes + 1];
			double [][][] dbg_img = new double[titles_top.length][titles.length][]; // woi_length];
			dbg_img[0][nscenes] = new double[woi_length];
			dbg_img[1][nscenes] = new double[woi_length];
			dbg_img[2][nscenes] = new double[woi_length];
			dbg_img[3][nscenes] = getTerrainConfidence(transparency_weights_preblur);
			dbg_img[4][nscenes] = getTerrainConfidence(transparency_weights);
			dbg_img[5][nscenes] = scales;
			for (int nscene = 0; nscene < transparency.length; nscene++) {
				titles[nscene] = "scene-"+nscene;
				dbg_img[0][nscene] = (transparency_fade != null) ? transparency_fade[nscene] : new double[woi_length];
				dbg_img[1][nscene] = transparency_raw[nscene];
				dbg_img[2][nscene] = transparency[nscene];
				dbg_img[3][nscene] = transparency_weights_preblur[nscene];
				dbg_img[4][nscene] = transparency_weights[nscene];
				dbg_img[5][nscene] = scaled_weights[nscene];
				for (int wpix = 0;wpix < woi_length; wpix++) {
					if (dbg_img[2][nscenes][wpix] < transparency[nscene][wpix]) { // takes care of NaN
						dbg_img[2][nscenes][wpix] = transparency[nscene][wpix];   
					}
					if (dbg_img[1][nscenes][wpix] < transparency_raw[nscene][wpix]) { // takes care of NaN
						dbg_img[1][nscenes][wpix] = transparency_raw[nscene][wpix];   
					}
					if ((transparency_fade != null)  && (dbg_img[0][nscenes][wpix] < transparency_fade[nscene][wpix])) { // takes care of NaN
						dbg_img[0][nscenes][wpix] = transparency_fade[nscene][wpix] ;   
					}
				}
			}
			titles[nscenes] = "max/conf";
			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
					titles_top,                     // String []    frame_titles, // frame titles or null
					true);                            // boolean      show)
		}
		
		double s = 0, ss=0;
		for (int ny = 0; ny < y_src.length; ny++) {
			int nscene =  y_src[ny][YSRC_SCENE];
			int findx =   y_src[ny][YSRC_FINDEX];
			int windx =   getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
			double w = scaled_weights[nscene][windx];
			if (Double.isNaN(w)) {
				System.out.println("applyTransparency() 1: ny="+ny+", nscene="+nscene+", findx="+findx+", windx="+windx+", w="+w);
			} else {
				s += w;
			}
			ss += scene_weights[nscene];
		}
//		double s_scenes = s; // sum of all scene weight. Maybe skip scenes that do not exist?
		double s_to_ss = s/ss;
		if (use_hf) {
			for (int ny = 0; ny < y_src_hf.length; ny++) {
				int nscene =   y_src_hf[ny][YSRC_SCENE];
				int findx =    y_src_hf[ny][YSRC_FINDEX];
				int windx =   getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
				double w = scaled_weights[nscene][windx] * hifreq_weight ;
				if (Double.isNaN(w)) {
					System.out.println("applyTransparency() 2: ny="+ny+", nscene="+nscene+", findx="+findx+", windx="+windx+", w="+w);
				} else {
					s += w;
				}
			}
		}
//		double s_y_yhf =    s; // 
		
		// TODO: If using scaled_weights (in setupYVectorTransparency and getFxDerivs),
		// then calculate following weights differently to keep ~ the same sum
		// use recalc_average / this.recalc_average to change weights
		
		
		if (y_avg_len > 0) {
			for (int ny = 0; ny < y_avg_len; ny++) {
				s += terrain_correction_weight; // scene_weights[nscene] is included in y_wavg and fX, jt
			}
		}
///		final double s0 = s;
		s /= (1 - reg_weights); // *= (1+ reg_weights);s *= (1+ reg_weights);
		final double k = 1.0/s; //corr_transp	10.85811847216958
		weight_to_scene_weight = k * s_to_ss;
///		weight_pure = s0/s;
//		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
//		final AtomicInteger ai = new AtomicInteger(0);
		ai.set(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  < samples_pointers[SAMPLES_Y_HF][0]) { // DC differences
							int ny = nw;
							int nscene =  y_src[ny][YSRC_SCENE];
							int findx =   y_src[ny][YSRC_FINDEX];
							int windx =   getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
							double w = scaled_weights[nscene][windx] * k;
							if (!Double.isNaN(w)) {
								weights[nw] = w;
							} else {
								System.out.println("weights["+nw+"]=NaN - 1");
							}

						} else if (nw  < samples_pointers[SAMPLES_Y_AVG][0]) {	// HF differences if exist
							int ny = nw-y_src.length;
							int nscene =  y_src_hf[ny][YSRC_SCENE];
							int findx =   y_src_hf[ny][YSRC_FINDEX];
							int windx =   getWoiIndex(null, woi, full, findx); //; // getWindexFromFindex(findx);
							double w = scaled_weights[nscene][windx] * k * hifreq_weight; // scene_weights[nscene] * k * hifreq_weight;
							if (!Double.isNaN(w)) {
								weights[nw] = w;
							} else {
								System.out.println("weights["+nw+"]=NaN - 2");
							}

						} else if (nw  < samples_pointers[SAMPLES_EXTRA][0]) {	// y_wavg if exists
							weights[nw] = k * terrain_correction_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("weights["+nw+"]=NaN - 3");
							}
						/*	
						} else {
							weights[nw] = reg_sample_weight;
							if (Double.isNaN(weights[nw])) {
								System.out.println("weights["+nw+"]=NaN - 4");
							}
*/							
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		if (y_src_hf != null) { // set average offset y - either old way with scene_weights[nscene] or new way with weights[ny]
			setupYVectorTransparency();
		}
		if (dbg_prefix != null) {
			double sw = 0;
			for (int n = 0; n < (samples_pointers.length -1); n++) {
				double swn = 0;
				for (int i = samples_pointers[n][0]; i < (samples_pointers[n][0]+samples_pointers[n][1]); i++){
					swn+= weights[i];
				}
				sw += swn;
				System.out.println("sum weights["+n+"] = "+swn+", total = "+sw);
			}
		}
		return confidence;
	}
	
	private void setupYVector(
			boolean use_terr_corr,
			double [] scene_weights) {
		final int woi_length = woi.width*woi.height;
		final int terr_start = y_src.length + ((y_src_hf != null)? y_src_hf.length : 0);
		final int ds_length =  terr_start + (use_terr_corr? woi_length: 0);
		y_vector = new double [ds_length];
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = 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);
		}
		if (use_terr_corr) {
			int dbg_windx = -142;
			ai.set(0);
			ati.set(0);
			final double [][] y_wavg_threads =    new double [threads.length][woi_length];
//			y_wavg = new double[woi_length];
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					int nthread = ati.getAndIncrement();
					public void run() {
						for (int ny = ai.getAndIncrement(); ny < y_src.length; ny = ai.getAndIncrement()) {
							int nscene =   y_src[ny][YSRC_SCENE];
							int findx =     y_src[ny][YSRC_FINDEX];
							int windx = getWoiIndex(null, woi, full, findx); //getWindexFromFindex(findx);
							double d =y_vector[ny] * scene_weights[nscene] * terrain_correction;
							if (Double.isNaN(d)) {
								System.out.println("y_vector["+ny+"]*scene_weights["+nscene+"]=NaN");
							}
							y_wavg_threads[nthread][windx] += d; 
							if (windx== dbg_windx) {
								System.out.println("setupYVector(): nscene="+nscene+", nthread="+nthread+
										" y_vector["+ny+"]="+y_vector[ny]+" scene_weights["+nscene+"]="+scene_weights[nscene]+" d="+d);
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			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 swy = 0;
							if (wIndx== dbg_windx) {
								System.out.println("setupYVector(): wIndx="+wIndx);
							}
							for (int nthread = 0; nthread < threads.length; nthread++) {
								swy +=y_wavg_threads[nthread][wIndx];
							}
							if (Double.isNaN(swy)) {
								System.out.println("y_vector["+(wIndx+terr_start)+"]=NaN");
							}
							y_vector[wIndx+terr_start] = swy; // /sw; (fX uses the same weights)
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return;
	}
	
	private void setupYVectorTransparency() { // this.recalc_average should be already set
		final int dbg_windx = -142;
		final int woi_length = woi.width*woi.height;
		final int terr_start = y_src.length + ((y_src_hf != null)? y_src_hf.length : 0);
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [][] y_wavg_threads =    new double [threads.length][woi_length];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				int nthread = ati.getAndIncrement();
				public void run() {
					for (int ny = ai.getAndIncrement(); ny < y_src.length; ny = ai.getAndIncrement()) {
						int nscene =   y_src[ny][YSRC_SCENE];
						int findx =     y_src[ny][YSRC_FINDEX];
						int windx = getWoiIndex(null, woi, full, findx); // getWindexFromFindex(findx);
//						double d =y_vector[ny] * scene_weights[nscene];
						double w = recalc_average ? (weights[ny] / weight_to_scene_weight): scene_weights[nscene];
						w *= terrain_correction;
						double d =y_vector[ny] * w;
						if (Double.isNaN(d)) {
							System.out.println("y_vector["+ny+"]*scene_weights["+nscene+"]=NaN");
						}
						y_wavg_threads[nthread][windx] += d; 
						if (windx== dbg_windx) {
							System.out.println("setupYVector(): nscene="+nscene+", nthread="+nthread+
									" y_vector["+ny+"]="+y_vector[ny]+" scene_weights["+nscene+"]="+scene_weights[nscene]+" d="+d);
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		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 swy = 0;
						if (wIndx== dbg_windx) {
							System.out.println("setupYVector(): wIndx="+wIndx);
						}
						for (int nthread = 0; nthread < threads.length; nthread++) {
							swy +=y_wavg_threads[nthread][wIndx];
						}
						if (Double.isNaN(swy)) {
							System.out.println("y_vector["+(wIndx+terr_start)+"]=NaN");
						}
						y_vector[wIndx+terr_start] = swy; // /sw; (fX uses the same weights)
						
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}

	
	/**
	 * Generate parameter indices (or negative full frame indices if there is no parameter)
	 * four ortho neighbors around each parameter 	
	 * @param tvao parameter type (one of TVAO_*)
	 * @param ind_samples parameter index of the first parameter of this (tvao) type 
	 * @param num_samples number of parameter of this type
	 * @return array of 4 parameter indices for each parameter of the tvao type: top(0), right(1), down(2) and left(3)
	 * If the neighbor is outside of full image (Rectangle full) - it will be -1,
	 * If the neighbor is not a parameter - -2-<full pixel index> 
	 */
	private int [][] getNeighbors(
			final int tvao,        // TVAO_VEGETATION_ALPHA
			final int ind_samples, // ind_pars[TVAO_VEGETATION]_alpha
			final int num_samples  // num_pars[TVAO_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 findx = par_rindex[pindx][1]; // full pixel index
						for (int dir4 = 0; dir4 < 4; dir4++) { // ortho only
							int dir = 2 * dir4;
							int nindx = tn.getNeibIndex(findx, 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 -findx; // OFFSET by 2 !
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		//ind_pars[TVAO_VEGETATION]_alpha
		return alpha_neibs;
	}

	/**
	 * Combine neighbors regardless of their nature (does it make sense for terrain and vegetation?)
	 * @param second_neibs combined neighbors - will be modified
	 * @param par_neibs    neighbors for one parameter type (contains negative values that are ignored here)
	 * @param start_index  start parameter index where par_neibs begin
	 */
	public static void addSecondNeighbors(
			final int [][] second_neibs,
			final int [][] par_neibs,
			final int      start_index) {
		if ((par_neibs == null) || (par_neibs.length==0)) {
			return; // nothing to do
		}
		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() {
					HashSet<Integer> neibs_set = new HashSet<Integer>();
					for (int nPar = ai.getAndIncrement(); nPar < par_neibs.length; nPar = ai.getAndIncrement()){
						neibs_set.clear();
						int full_par = nPar+start_index;
						neibs_set.add(full_par);
						int [] this_neibs = par_neibs[nPar];
						for (int dir = 0; dir < this_neibs.length; dir ++) {
							int neib = this_neibs[dir];
							if (neib >=0) {
								int neib_rel = neib - start_index;
								if ((neib_rel >= par_neibs.length) || (neib_rel < 0)){
									System.out.println("addSecondNeighbors() BUG: nPar="+nPar+" neib="+neib+" neib_rel="+
											neib_rel+" >= par_neibs.length="+par_neibs.length+" or < 0");
									continue;
								}
								neibs_set.add(neib);
								int [] this_neib_neibs = par_neibs[neib_rel];
								for (int dir_neib = 0; dir_neib < this_neib_neibs.length; dir_neib ++) {
									int neib_neib = this_neib_neibs[dir_neib];
									if (neib_neib >= 0) {
										int neib_neib_rel = neib_neib - start_index;
										if ((neib_neib_rel >= par_neibs.length) || (neib_neib_rel < 0)){
											System.out.println("addSecondNeighbors() BUG: nPar="+nPar+" neib="+
													neib+" neib_rel="+neib_rel+" neib_neib="+neib_neib+" neib_neib_rel="+
													neib_neib_rel+" >= par_neibs.length="+par_neibs.length+" or < 0");
											continue;
										}
										neibs_set.add(neib_neib);
									}
									
								}								
								
							}							
						} // for (int dir = 0; dir < this_neibs.length; dir ++) {
						if (!neibs_set.isEmpty()) {
							if (second_neibs[full_par] != null) {
								for (int neib: second_neibs[full_par]) {
									neibs_set.add(neib);
								}
							}
							//https://stackoverflow.com/questions/2451184/how-can-i-convert-a-java-hashsetinteger-to-a-primitive-int-array
							int [] neibs_arr = neibs_set.stream().mapToInt(Integer::intValue).toArray();
							Arrays.sort(neibs_arr); // optional
							second_neibs[full_par] = neibs_arr;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
	}
		
		
		
	
	
	
	private void setupParametersVector() {
		// setup terrain parameters (use inner woi)
		for (int drow = 0; drow < woi_terr.height; drow++) {
			int row = woi_terr.y + drow;
			for (int dcol = 0; dcol < woi_terr.width; dcol++) {
				int col = woi_terr.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");
					}
				}
			}
		}
		if (woi_veg != null) { // otherwise - terrain-only
			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");
						}
					}
				}
			}
		}
		/*
		 * Initialization of parameters_vector[par_index[TVAO_TERR_ELEV][i]]= 0.0 and 
		 * parameters_vector[par_index[TVAO_TERR_ELEV_PIX][i]]= 0.0 are not needed - they are 0.0  initially and
		 * may be modified only when loading partially fitted models.
		 * 
		 * Keeping as a reminder.
		 */
		for (int i = 0; i <  par_index[TVAO_TERR_ELEV].length; i++) if (par_index[TVAO_TERR_ELEV][i] >= 0){
			parameters_vector[par_index[TVAO_TERR_ELEV][i]]= 0.0; // now just single or no elements
		}
		for (int i = 0; i <  par_index[TVAO_TERR_ELEV_PIX].length; i++) if (par_index[TVAO_TERR_ELEV_PIX][i] >= 0){
			parameters_vector[par_index[TVAO_TERR_ELEV_PIX][i]]= 0.0; // now just single or no elements
		}
		
		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, // should have disabled vegetation, alphja, elevation when woi_veg = null 
			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_ELEVATION] =        new int [image_length];
		par_index[TVAO_TERR_ELEV] =        new int [1];
		par_index[TVAO_TERR_ELEV_PIX] =    new int [image_length];
		par_index[TVAO_SCENE_OFFSET] =     new int [num_scenes];
		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_TERR_ELEV],    -1);
		Arrays.fill(par_index[TVAO_TERR_ELEV_PIX],-1);
		Arrays.fill(par_index[TVAO_SCENE_OFFSET], -1);
		int par_num = 0;
		ind_pars[TVAO_TERRAIN] = 0;
		if (fits[TVAO_TERRAIN]) {
			for (int drow = 0; drow < woi_terr.height; drow++) { // terrain woi for terrain
				int row = woi_terr.y + drow;
				for (int dcol = 0; dcol < woi_terr.width; dcol++) {
					int col = woi_terr.x + dcol;	
					int wtindx =dcol + drow * woi_terr.width;
					int findx = row * full.width + col;
					if (valid_terrain[wtindx]) {	 // woi				
						par_rindex[par_num][0] = TVAO_TERRAIN;
						par_rindex[par_num][1] = findx;
						par_index[TVAO_TERRAIN][findx] = par_num++;
					}
				}
			}
		}
		num_pars[TVAO_TERRAIN] =    par_num;
		ind_pars[TVAO_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[TVAO_ALPHA] = par_num;
		num_pars[TVAO_VEGETATION] = par_num - ind_pars[TVAO_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[TVAO_ALPHA] = par_num - ind_pars[TVAO_ALPHA]; // normally should be == num_pars[TVAO_VEGETATION]
		ind_pars[TVAO_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[TVAO_ELEVATION] = par_num - ind_pars[TVAO_ELEVATION]; // normally should be == num_pars[TVAO_VEGETATION]
		ind_pars[TVAO_TERR_ELEV] = par_num;

		if (fits[TVAO_TERR_ELEV]) { // should be also true if fits[TVAO_TERR_ELEV_PIX]
			par_rindex[par_num][0] = TVAO_TERR_ELEV;
			par_rindex[par_num][1] = 0; // something more useful?
			par_index[TVAO_TERR_ELEV][0] = par_num++;
		}
		num_pars[TVAO_TERR_ELEV] = par_num - ind_pars[TVAO_TERR_ELEV]; // 1
		ind_pars[TVAO_TERR_ELEV_PIX] =    par_num;
		if (fits[TVAO_TERR_ELEV_PIX]) {
			for (int drow = 0; drow < woi_terr.height; drow++) {
				int row = woi_terr.y + drow;
				for (int dcol = 0; dcol < woi_terr.width; dcol++) {
					int col = woi_terr.x + dcol;	
					int wtindx =dcol + drow * woi_terr.width;
					int indx = row*full.width + col;
					if (valid_terrain[wtindx]) {	 // woi_veg				
						par_rindex[par_num][0] = TVAO_TERR_ELEV_PIX;
						par_rindex[par_num][1] = indx;
						par_index[TVAO_TERR_ELEV_PIX][indx] = par_num++;
					} else {
						par_index[TVAO_TERR_ELEV_PIX][indx] = -2 -indx;
					}
				}
			}
		}
		num_pars[TVAO_TERR_ELEV_PIX] = par_num - ind_pars[TVAO_TERR_ELEV_PIX]; // 1
		ind_pars[TVAO_SCENE_OFFSET] =    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[TVAO_SCENE_OFFSET] = par_num - ind_pars[TVAO_SCENE_OFFSET];
		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_terr_proj[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(
			Rectangle woi,
			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_elevation maximal "elevation" to consider
	 * @param max_elev_terr maximal terrain "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 [7] - will return number of valid scenes, number of Y pixels (woi), number of vegetation pixels,
	 *        number of Y pixels that are influenced by the vegetation,
	 *        number of valid_terrain pixels, number of Y-pixels (woi) that have terrain influence, 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. 
	 * @param woi_terr - should be initialized, will be updated to include terrain pixels that overlap woi in
	 *        some scenes. 
	 * @return combined boolean 2d array: {"valid_scenes", "valid_pixels", "valid_vegetation",
	 *        "has_vegetation", "valid_terrain", "has_terrain"}.
	 *        Of them "valid_scenes" is num_scenes long, "vvalid_pixels" and "has_vegetation", "has_terrain" - woi.width*woi.heigh,
	 *        "valid_vegetation" - woi_veg.width*woi_veg.height, and
	 *        "valid_terrain" -  woi_terr.width*woi_terr.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 (or null)
			final int          max_elevation,
			final int          max_elev_terr,
			final double []    elev_radius, // this.elevation_radius
			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 Rectangle    woi_terr // should be initialized, will be modified
			) {
		final int dbg_pix = 801; // 918;
		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(woi,max_elevation);
		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 boolean debug_source_veg = false; // this.debug_source_vegetation
		final AtomicInteger ati =          new AtomicInteger(0);
		final ArrayList<ArrayList<ArrayList<HashSet<Integer>>>> aaahi = this.debug_source_vegetation ? (new ArrayList<ArrayList<ArrayList<HashSet<Integer>>>>()) : null;
		if (this.debug_source_vegetation) {
			for (int ithread = 0; ithread < threads.length; ithread++) {
				aaahi.add(new ArrayList<ArrayList<HashSet<Integer>>>(num_scenes));
			}
		}
		if (debug_source_vegetation) {
			ai.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();
						ArrayList<ArrayList<HashSet<Integer>>> aahi = null;
						if (debug_source_vegetation) {
							aahi = aaahi.get(nthread);
							for (int nscene = 0; nscene < num_scenes; nscene++) {
								ArrayList<HashSet<Integer>> ahi = new ArrayList<HashSet<Integer>>(woi_ext_len);
								for (int i = 0; i < woi_length; i++) {
									ahi.add(new HashSet<Integer>());
								}
								aahi.add(ahi);
							}
						}
						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 radius = elev_radius[npix];
							double radius2 = radius*radius;
							int iradius = (int) Math.ceil (radius);
							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 ax0 = (int) Math.floor(x + dx - radius);
										int ax1 = (int) Math.ceil (x + dx + radius);
										int ay0 = (int) Math.floor(y + dy - radius);
										int ay1 = (int) Math.ceil (y + dy + radius);
										Rectangle ar = new Rectangle(ax0, ay0, ax1-ax0 +1, ay1-ay0 +1);
										if (woi.intersects(ar)) {
											boolean has_valid_dependent = false;
											for (int ay = ay0; ay <= ay1; ay++) {
												double ddy = ay - (y + dy);
												double ddy2 = ddy*ddy;
												for (int ax = ax0; ax <= ax1; ax++) {
													if (woi.contains(ax,ay)) {
														double ddx = ax - (x + dx);
														double ddr2 = ddy2 + ddx*ddx;
														if ((ddy2 <= radius2) && (ddx*ddx < radius2)) { // if (ddr2 <= radius2) {
															int wx = ax - woi.x;
															int wy = ay - woi.y;
															int wpix =wx + wy*woi.width; 
															if (valid_pixels[wpix]) {
																has_veg_thread[nthread][nscene][wpix] = true;
																has_valid_dependent = true;
																if (debug_source_vegetation) {
																	aahi.get(nscene).get(wpix).add(npix);
																}
															}
														}
													}
												}
											}
											if (has_valid_dependent) {
												veg_scene_used_prefilter[indx] = true;
											}
										}
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		} else {
			//		final HashSet<Integer> [][][] dbg_src_veg =  debug_source_veg ? (new HashSet<Integer>[threads.length][num_scenes][woi_length]): null;
			ai.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();
						//					ArrayList<ArrayList<HashSet<Integer>>> aahi = debug_source_veg ? 
						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 radius = elev_radius[npix];
							double radius2 = radius*radius;
							int iradius = (int) Math.ceil (radius);
							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 ax0 = (int) Math.floor(x + dx - radius);
										int ax1 = (int) Math.ceil (x + dx + radius);
										int ay0 = (int) Math.floor(y + dy - radius);
										int ay1 = (int) Math.ceil (y + dy + radius);
										Rectangle ar = new Rectangle(ax0, ay0, ax1-ax0 +1, ay1-ay0 +1);
										if (woi.intersects(ar)) {
											boolean has_valid_dependent = false;
											for (int ay = ay0; ay <= ay1; ay++) {
												double ddy = ay - (y + dy);
												double ddy2 = ddy*ddy;
												for (int ax = ax0; ax <= ax1; ax++) {
													if (woi.contains(ax,ay)) {
														double ddx = ax - (x + dx);
//														double ddr2 = ddy2 + ddx*ddx;
														if ((ddy2 <= radius2) && (ddx*ddx < radius2)) { // if (ddr2 <= radius2) {
															int wx = ax - woi.x;
															int wy = ay - woi.y;
															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);
		if (debug_source_vegetation) {
			debug_source_veget = new int [num_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]){
							debug_source_veget[nScene] = new int [woi_length][];
							for (int wpix = 0; wpix < woi_length; wpix++) {
								HashSet<Integer> hs = new HashSet<Integer>(); 
								for (int nthread =0; nthread < has_veg_thread.length; nthread++) {
//									ArrayList<HashSet<Integer>> ahi = aaahi.get(nthread).get(nScene); // ArrayList<ArrayList<HashSet<Integer>>> aahi = null;
									ArrayList<HashSet<Integer>> ahi = aaahi.get(nthread).get(nScene); // ArrayList<ArrayList<HashSet<Integer>>> aahi = null;
									if (!ahi.isEmpty()) {
										hs.addAll(ahi.get(wpix));
										if (!hs.isEmpty()) {
											//										debug_source_veget[nScene][wpix] = new int[woi_length];
											debug_source_veget[nScene][wpix] = hs.stream().mapToInt(Integer::intValue).toArray();
										}
									}
								}
							}
						}
					}
				};
			}		      
			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 [] has_vegetation =  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]){
						if (wPix == dbg_pix) {
							System.out.println ("getValidScenesPixels() 1 : wpix = "+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);
								}
							}
							has_vegetation[wPix] = true;
							anum_offsets_middle.getAndIncrement();
							if (has_offset) {
								anum_offsets.getAndIncrement();
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		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;
					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);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					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 radius = elev_radius[npix];
	                    double radius2 = radius*radius;
						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 ax0 = (int) Math.floor(x + dx - radius);
	                                int ax1 = (int) Math.ceil (x + dx + radius);
	                                int ay0 = (int) Math.floor(y + dy - radius);
	                                int ay1 = (int) Math.ceil (y + dy + radius);
	                                Rectangle ar = new Rectangle(ax0, ay0, ax1-ax0 +1, ay1-ay0 +1);
		                            if (woi.intersects(ar)) {
										// valid_pixels
										boolean has_valid_dependent = false;
	                                    for (int ay = ay0; ay <= ay1; ay++) {
	                                        double ddy = ay - (y + dy);
	                                        double ddy2 = ddy*ddy;
	                                        for (int ax = ax0; ax <= ax1; ax++) {
	                                            if (woi.contains(ax,ay)) {
	                                                double ddx = ax - (x + dx);
	                                                if ((ddy2 <= radius2) && (ddx*ddx < radius2)) { // if (ddr2 <= radius2) {
	                                                    int wx = ax - woi.x;
	                                                    int wy = ay - woi.y;
	                                                    int wpix =wx + wy*woi.width; 
														if (valid_pixels[wpix] && has_vegetation[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);
   
		Rectangle woi_veg_new = new Rectangle(amin_x.get(), amin_y.get(), amax_x.get()-amin_x.get()+1, amax_y.get()-amin_y.get()+1);	
		boolean continue_no_veg = true;
		boolean terrain_only = (woi_veg_new.width <= 0) || (woi_veg_new.width <= 0);
		if (terrain_only) {
			if (woi_veg != null) { // not to repeat
				System.out.println("woi_veg_new="+woi_veg_new.toString()+", no vegetation in woi! *****");
			}
			if (!continue_no_veg) {
				return null;
			}
			woi_veg_new = null;
		} else {
			if (!woi_veg_new.contains(woi)) {
				System.out.println("\n***** BUG FIX:woi_veg_new="+woi_veg_new.toString()+" does not contain woi="+woi.toString()+", increasing woi_veg_new");
				Rectangle r = new Rectangle(woi_veg_new);
				r.add(woi);
				woi_veg_new.x=r.x;
				woi_veg_new.y=r.y;
				woi_veg_new.width=  r.width;
				woi_veg_new.height= r.height;
				System.out.println("Updated woi_veg_new="+woi_veg_new.toString()+"\n");
			}
		}
		if (woi_veg == null) {
			System.out.println("No vegetation in provided file, using woi_veg, terrain_only was "+terrain_only);
			terrain_only = true; // is it already?
		} else {
			if (woi_veg.width == 0) {
				if (woi_veg_new != null) {
					woi_veg.x =      woi_veg_new.x;
					woi_veg.y =      woi_veg_new.y;
					woi_veg.width =  woi_veg_new.width;
					woi_veg.height = woi_veg_new.height;
				}

				// copy num_used to a smaller array
			} else {
				System.out.println("Using provided woi_veg="+woi_veg.toString()+"\n");
				if (terrain_only || !woi_veg.equals(woi_veg_new)) {
					if (terrain_only) {
						System.out.println("***** Provided woi_veg="+woi_veg.toString()+" does not match calculated one=null");
					} else {
						System.out.println("***** Provided woi_veg="+woi_veg.toString()+" does not match calculated one="+woi_veg_new.toString());
					}
					System.out.println("***** Using the provided one");
				}
			}
		}
		
		
		final boolean [] valid_vegetation = terrain_only? null :  new boolean [woi_veg.width*woi_veg.height];
		if (valid_vegetation != null) {
			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);
			}
		}

		
		// *****************************************************		
		
		
		// FIXME: create woi_terr by expanding woi ***********************************
		final Rectangle woi_terr_ext = expandWoi(woi,max_elev_terr);
		final int woi_terr_ext_len = woi_terr_ext.width*woi_terr_ext.height;
		
		final boolean []     terr_scene_used_prefilter = new boolean [woi_terr_ext_len]; // which of the source terrain pixel may be used 
		final boolean []     terr_scene_used =           new boolean [woi_terr_ext_len]; // which of the source terrain pixel may be used 
		final boolean [][][] has_terr_thread =           new boolean [threads.length][num_scenes][woi_length];
		ai.set(0);
		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 wtindx = ai.getAndIncrement(); wtindx < woi_terr_ext_len; wtindx = ai.getAndIncrement()) {
						int y = wtindx / woi_terr_ext.width  + woi_terr_ext.y;
						int x = wtindx % woi_terr_ext.width  + woi_terr_ext.x;
						int findx = x + y * full.width;
						double radius = elev_radius[findx]; // reuse same as for vegetation =1.5
						double terrain =    tvao[TVAO_TERRAIN][findx];
						if (!Double.isNaN(terrain)) {
							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);
								}
								double [] scales =  scales_xy[nscene][findx];
								if (scales != null) {
									boolean has_valid_dependent = false;
									for (int nelev = -max_elev_terr; nelev <= max_elev_terr; nelev++) {
										double elevation = nelev;
										double dx = scales[0] * elevation;
										double dy = scales[1] * elevation;
										int ax0 = (int) Math.floor(x + dx - radius);
										int ax1 = (int) Math.ceil (x + dx + radius);
										int ay0 = (int) Math.floor(y + dy - radius);
										int ay1 = (int) Math.ceil (y + dy + radius);
										Rectangle ar = new Rectangle(ax0, ay0, ax1-ax0 +1, ay1-ay0 +1);
										if (woi.intersects(ar)) {
//											boolean has_valid_dependent = false;
											for (int ay = ay0; ay <= ay1; ay++) {
												double ddy = ay - (y + dy);
												double ddy2 = ddy*ddy;
												for (int ax = ax0; ax <= ax1; ax++) {
													if (woi.contains(ax,ay)) {
														double ddx = ax - (x + dx);
//														double ddr2 = ddy2 + ddx*ddx;
//														if ((ddy2 <= radius2) && (ddx*ddx < radius2)) { // if (ddr2 <= radius2) {
															int wx = ax - woi.x;
															int wy = ay - woi.y;
															int wpix =wx + wy*woi.width; 
//															if (valid_pixels[wpix]) {
																has_terr_thread[nthread][nscene][wpix] = true;
																has_valid_dependent = true;
//															}
//														}
													}
												}
											}
										}
									} // for (int nelev = -max_elev_terr; nelev <= max_elev_terr; nelev++) {
									if (has_valid_dependent) {
										terr_scene_used_prefilter[wtindx] = true;
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		boolean [] has_terrain =  new boolean [woi_length];   // which image pixels in woi have source terrain
		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()){
						for (int nscene = 0; nscene < num_scenes; nscene++)  if (valid_scenes[nscene]){
							for (int nthread =0; nthread < has_veg_thread.length; nthread++) {
								has_terrain[wIndx] |=has_terr_thread[nthread][nscene][wIndx];
							}

						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);		
// *****************************************************		
		final AtomicInteger atmax_x = new AtomicInteger(0);
		final AtomicInteger atmax_y = new AtomicInteger(0);
		final AtomicInteger atmin_x = new AtomicInteger(full.width);
		final AtomicInteger atmin_y = new AtomicInteger(full.height);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
			threads[ithread] = new Thread() {
				public void run() {
					for (int indx = ai.getAndIncrement(); indx < woi_terr_ext_len; indx = ai.getAndIncrement()) if (terr_scene_used_prefilter[indx]){
						int y = indx / woi_terr_ext.width  + woi_terr_ext.y;
						int x = indx % woi_terr_ext.width  + woi_terr_ext.x;
						int findx = x + y * full.width;
//						double elevation =  tvao[TVAO_ELEVATION][findx];
//						double vegetation = tvao[TVAO_VEGETATION][findx];
						double terrain =    tvao[TVAO_TERRAIN][findx];
						if (!Double.isNaN(terrain)) {
							for (int nelev = -max_elev_terr; nelev <= max_elev_terr; nelev++) {
								double elevation = nelev;
								for (int nscene = 0; nscene < num_scenes; nscene++) if (valid_scenes[nscene]){
									double [] scales =  scales_xy[nscene][findx];
									if (scales != null) {
										double dx = scales[0] * elevation;
										double dy = scales[1] * elevation;
										int px05 = (int) Math.floor(x + dx); // ELEV-SIGN
										int py05 = (int) Math.floor(y + dy); // ELEV-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_terrain[wpix]) {
																has_valid_dependent = true;
															}
														}
													}
												}
											}
											if (has_valid_dependent) {
												terr_scene_used[indx] = true;
											}
										}
									}
								}
							} // for (int nelev = -max_elev_terr; nelev <= max_elev_terr; nelev++) {
							if (terr_scene_used[indx]) { // at least once used
								atmax_x.getAndAccumulate(x, Math::max);
								atmax_y.getAndAccumulate(y, Math::max);
								atmin_x.getAndAccumulate(x, Math::min);
								atmin_y.getAndAccumulate(y, Math::min);
							}
						} // if (!Double.isNaN(terrain)) {
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		Rectangle woi_terr_new = new Rectangle(atmin_x.get(), atmin_y.get(), atmax_x.get()-atmin_x.get()+1, atmax_y.get()-atmin_y.get()+1);	
		if (!woi_terr_new.contains(woi)) {
			System.out.println("\n***** BUG FIX:woi_terr_new="+woi_terr_new.toString()+" does not contain woi="+woi.toString()+", increasing woi_veg_new");
			Rectangle r = new Rectangle(woi_terr_new);
			r.add(woi);
			woi_terr_new.x=r.x;
			woi_terr_new.y=r.y;
			woi_terr_new.width=  r.width;
			woi_terr_new.height= r.height;
			System.out.println("Updated woi_terr_new="+woi_terr_new.toString()+"\n");
		}
		if (woi_terr.width == 0) {
			woi_terr.x =      woi_terr_new.x;
			woi_terr.y =      woi_terr_new.y;
			woi_terr.width =  woi_terr_new.width;
			woi_terr.height = woi_terr_new.height;
			// copy num_used to a smaller array
		} else {
			System.out.println("Using provided woi_terr="+woi_terr.toString()+"\n");
			if (!woi_terr.equals(woi_terr_new)) {
				System.out.println("***** Provided woi_veg="+woi_terr.toString()+" does not match calculated one="+woi_terr_new.toString());
				System.out.println("***** Using the provided one");
			}
		}
		
		final boolean [] valid_terrain = new boolean [woi_terr.width*woi_terr.height];
		for (int row = 0; row < woi_terr.height; row++) {
			int row_src = row + woi_terr.y - woi_terr_ext.y;
			int col_src = woi_terr.x - woi_terr_ext.x;
			System.arraycopy( // arraycopy: last source index 595 out of bounds for boolean[576]
					terr_scene_used,
					col_src + row_src * woi_terr_ext.width,
					valid_terrain,
					row*woi_terr.width,
					woi_terr.width);
		}
		
		
		// *****************************************************		
		
		// delete scenes that have too high warp
		AtomicInteger abad_warp = new AtomicInteger(0);
		if ((warps != null) && (max_warp > 1.0) && !terrain_only) {
			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 they 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 (has_vegetation[wPix]) astats[3].getAndIncrement();
							if (has_terrain[wPix])    astats[5].getAndIncrement();
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			if (!terrain_only) {
				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);
			}
			ai.set(0);
			int woi_terr_length = woi_terr.width*woi_terr.height;
			for (int ithread = 0; ithread < threads.length; ithread++) { // first sum for pairs
				threads[ithread] = new Thread() {
					public void run() {
						for (int wtPix = ai.getAndIncrement(); wtPix < woi_terr_length; wtPix = ai.getAndIncrement()) {
							if (valid_terrain[wtPix]) astats[4].getAndIncrement();
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			
			
			stats[0] = astats[0].get(); // number valid scenes
			stats[1] = astats[1].get(); // number of Y pixels (woi)
			stats[2] = astats[2].get(); // number of vegetation pixels (woi_veg)
			stats[3] = astats[3].get(); // number of Y pixels (woi) that are influenced by the vegetation
			stats[4] = astats[4].get(); // number of valid_terrain pixels (woi_terr)
			stats[5] = astats[5].get(); // number of Y pixels (woi) that are influenced by the terrain
			stats[6] = abad_warp.get(); // number of scenes removed for bad warp
		}
		if ((warps != null) && (max_warp_scene_pix != null) && !terrain_only) {
			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, has_vegetation, valid_terrain, has_terrain};
	}
	

	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;	
	}
	
	private boolean disabledParams() {
		for (boolean dis:fits_disable) if (dis){
			return true;
		}
		return false;
	}

	/**
	 * Convert from full parameter index to decimated parameter index (and vice versa)
	 * @return [0] - decimated parameter index for full parameter index (-1 for missing ones)
	 *         [1] - full parameter index for decimated index
	 */
	private int [][] getParsAllDecimatedAllIndices() {
		int []            num_pars_decim = new int [TVAO_TYPES];
		int []            ind_pars_decim = new int [TVAO_TYPES];
		int ind_next = 0;
		for (int typ = 0; typ < TVAO_TYPES; typ++) {
			ind_pars_decim[typ] = ind_next;
			int len = fits_disable[typ]? 0 : num_pars[typ];
			num_pars_decim[typ] = len;
			ind_next += len;
		}
		int [][] pars_map = new int [2][];
		pars_map[0] = new int[par_rindex.length];
		Arrays.fill(pars_map[0], -1);
		pars_map[1] = new int[ind_next]; // number of parameters after decimation
		for (int typ = 0; typ < TVAO_TYPES; typ++) if (num_pars_decim[typ] > 0) {
			for (int i = 0; i < num_pars_decim[typ]; i++) {
				int indx_all =   ind_pars[typ];
				int indx_decim = ind_pars_decim[typ];
				pars_map[0][indx_all +   i] = indx_decim+i;
				pars_map[1][indx_decim + i] = indx_all+i;
			}
		}
		return pars_map; // [0] - full parameter index to decimated parameter index (-1 - none), [1] decimated index to full index
	}
	
	public static double [][] decimateJt(
			final int    [][] par_map, // // only [1] used
			final double [][] jt_full) {
		if (par_map == null) {
			return jt_full;
		}
		final double [][] jt = new double [par_map[1].length][jt_full[0].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() {
					for (int indx_decim = ai.getAndIncrement(); indx_decim < jt.length; indx_decim = ai.getAndIncrement()) {
						jt[indx_decim] = jt_full[par_map[1][indx_decim]];
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return jt;
	}
	
	public static double [] expandDecimated(
			final int    [][] par_map, // only [0] used
			final double [] data_decimated) {
		if (par_map == null) {
			return data_decimated;
		}
		final double [] data_full = new double [par_map[0].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() {
					for (int indx_decim = ai.getAndIncrement(); indx_decim < data_decimated.length; indx_decim = ai.getAndIncrement()) {
						int indx = par_map[1][indx_decim];
						data_full[indx] = (indx >= 0) ? data_decimated[indx_decim] : 0.0;
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return data_full;
	}
	
	public static double getProperty(
			ImagePlus imp,
			String    name,
			double    dflt) {
		if (imp.getProperty(name) == null) return dflt;
		else return Double.parseDouble((String)  imp.getProperty(name));
	}

	public static int getProperty(
			ImagePlus imp,
			String    name,
			int       dflt) {
		if (imp.getProperty(name) == null) return dflt;
		else return Integer.parseInt((String)  imp.getProperty(name));
	}

	public static boolean getProperty(
			ImagePlus imp,
			String    name,
			boolean       dflt) {
		if (imp.getProperty(name) == null) return dflt;
		else return Boolean.parseBoolean((String)  imp.getProperty(name));
	}
	public static double [] getProperty(
			ImagePlus imp,
			String [] names,
			double [] current) {
		for (int i = 0; i < names.length; i++) {
			current[i] = getProperty(imp, names[i], current[i]);
		}
		return current;
	}	
	public static int [] getProperty(
			ImagePlus imp,
			String [] names,
			int    [] current) {
		for (int i = 0; i < names.length; i++) {
			current[i] = getProperty(imp, names[i], current[i]);
		}
		return current;
	}	
	public static boolean [] getProperty(
			ImagePlus  imp,
			String  [] names,
			boolean [] current) {
		for (int i = 0; i < names.length; i++) {
			current[i] = getProperty(imp, names[i], current[i]);
		}
		return current;
	}	
	public File [] getSegmentFiles(
			String dir_path) {
		String starts = getParametersDebugTitle(true);
		String suffix1 = getParametersDebugTitleEnd(false);
		String suffix2 = getParametersDebugTitleEnd(true);
		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(suffix1+PAR_EXT) || file.getName().endsWith(suffix2+PAR_EXT))&& file.getName().startsWith(starts)) {
					return true;
				}
				return false;
			}
		};
		File dir = new File(dir_path);
		File [] par_files = dir.listFiles(parFileFilter);
		return par_files;
	}
	
	/**
	 * Convert between indices inside rectangular WOIs
	 * @param woi_src source WOI (or null if it is the full image WOI)
	 * @param woi_dst destination WOI (or null if it is the full image WOI)
	 * @param indx source index inside woi_src 
	 * @param woi_full full WOI (not null)
	 * @return destination index inside woi_dst or -1 if indx is not inside woi_src/woi_full or
	 *         result is not inside  woi_dst
	 */
	public static int getWoiIndex(
			Rectangle woi_src,
			Rectangle woi_dst,
			Rectangle woi_full,
			int indx) {
		int x,y,row;
		if (indx < 0) {
			return -1;
		}
		if (woi_src != null) {
			x = indx % woi_src.width + woi_src.x;
			row = indx / woi_src.width;
			if (row >= woi_src.height) {
				return -1;
			}
			y = row + woi_src.y;
		} else {
			x = indx % woi_full.width;
			y = indx / woi_full.width;
			if (y >= woi_full.height) {
				return -1;
			}
		}
		if (woi_dst != null) {
			if (woi_dst.contains(x,y)) {
				return (x - woi_dst.x) + (y - woi_dst.y) * woi_dst.width;
			}
		} else {
			if (woi_full.contains(x,y)) {
				return  x + y * woi_full.width;
			}
		}
		return -1;
	}
	
	/**
	 * Calculate X,Y inside WOI from line-scan index inside WOI (woi.x and woi.y are ignored)
	 * @param woi  WOI 
	 * @param indx index inside woi (0 - top left of woi) 
	 * @return pair of {x,y} or null if index is outside of WOI
	 */
	public static int [] woiXY(
			Rectangle woi,
			int indx) {
		if (indx < 0) {
			return null;
		}
		int x = indx % woi.width;
		int y = indx / woi.width;
		if (y < woi.height) {
			return new int [] {x,y};
		}
		return null;
	}
	/**
	 * Copy data between WOIs
	 * @param woi_src_in source WOI (or null if it is the full image WOI)
	 * @param woi_dst_in destination WOI (or null if it is the full image WOI)
	 * @param woi_full full WOI (not null)
	 * @param data_src - contents of woi_src in linescan order 
	 * @param data_dst_in - current contents of woi_dst or null (will use NaN) to copy data to 
	 * @return dst_data
	 */
	public static double [] copyWoi (
			final Rectangle woi_src_in,
			final Rectangle woi_dst_in,
			final Rectangle woi_full,
			final double [] data_src,
		    final double [] data_dst_in) {
		
		final Rectangle woi_src = (woi_src_in != null) ? woi_src_in : woi_full;
		final Rectangle woi_dst = (woi_dst_in != null) ? woi_dst_in : woi_full;
		
		final int woi_dst_length = woi_dst.width * woi_dst.height; 
		final double [] data_dst = (data_dst_in != null) ? data_dst_in : new double[woi_dst_length];
		if (data_dst_in == null) {
			Arrays.fill (data_dst, Double.NaN);
		}
		final Rectangle woi_intersect = woi_src.intersection(woi_dst);
		if (!woi_intersect.isEmpty()) {
			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 irow = ai.getAndIncrement(); irow < woi_intersect.height; irow = ai.getAndIncrement()) {
							int inter_indx = irow * woi_intersect.width;
							int src_indx = getWoiIndex(
									woi_intersect, // Rectangle woi_src,
									woi_src,       // Rectangle woi_dst,
									woi_full,      // Rectangle woi_full,
									inter_indx);   // int indx)
							int dst_indx = getWoiIndex(
									woi_intersect, // Rectangle woi_src,
									woi_dst,       // Rectangle woi_dst,
									woi_full,      // Rectangle woi_full,
									inter_indx);   // int indx)
							System.arraycopy(
									data_src,
									src_indx,
									data_dst,
									dst_indx,
									woi_intersect.width);
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		return data_dst;
	}
	
}
