/*
 **
 ** Distortions.java - Calculate lens distortion parameters from the pattern image
 **
 ** Copyright (C) 2011-2013 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **  
 **  MatchSimulatedPattern.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/>.
 ** -----------------------------------------------------------------------------**
 **
 */

import ij.*;
//import ij.process.*;
import ij.gui.GenericDialog;
import ij.io.FileSaver;
import ij.io.Opener;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.text.TextWindow;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
//import java.io.StringWriter;
import java.util.List;
import java.util.ArrayList;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.SwingUtilities;

import Jama.LUDecomposition;
import Jama.Matrix;
//import src.java.org.apache.commons.configuration.*;
import org.apache.commons.configuration.*;
// to work both in Eclipse and ImageJ:
// 1 - put commons-configuration-1.7.jar under ImageJ plugins directory (I used ImageJ-Elphel)
// 2 - in Eclipse project properties -> Build Path -> Libraries -> Add External jar
public class Distortions {
	private showDoubleFloatArrays SDFA_INSTANCE=new showDoubleFloatArrays(); // just for debugging?
    int numInputs=27; // with A8...// 24;   // parameters in subcamera+...
    int numOutputs=16; // with A8...//13;  // parameters in a single camera
    // {description, units, "S"/"C" (Subcamera/Eyesis)}
// 	public double [][][] gridGeometry=null;
	// create initial uniform grid
	public PatternParameters patternParameters;
	public LensDistortionParameters lensDistortionParameters;
	public RefineParameters refineParameters= new RefineParameters(); //create with default values
	public FittingStrategy fittingStrategy=null;
//	public double patternWidth;  // pattern full width in mm
//	public double patternHeight; // pattern full height in mm
//	public double patternHalfPeriod; // distance between opposite sign nodes
//	public double patternTilt;        // pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
    public double [][][][] gridOnSensor =null; // [v][u][px,py][0-value, 1..14 - derivative]
    public double [][] interParameterDerivatives=new double[this.numInputs][]; //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21)
    public double []   currentVector; // current variable parameter vector
    public double []   Y=null; // array of "y" - for each grid image, each defined grid node - 2 elements
    public int    []   imageStartIndex=null; // elements containing index of the start point of the selected image, first element 0, last - total number of points.
    public double []   weightFunction=null; //  array of weights for pixels (to fade values near borders), corresponding to Y array
//    public double []   imageSetWeight=null;
    public double      sumWeights;
    public double [][] targetXYZ=null; // array of target {x,y,z} matching each image each grid point 
    public double [][] jacobian=null; // partial derivatives of fX (above) by parameters to be adjusted (rows)
    public double []   nextVector; // next variable parameter vector
    public double []   currentfX=null; // array of "f(x)" - simulated data for all images, combining pixel-X and pixel-Y (odd/even)
    public double []   nextfX=null; // array of "f(x)" - simulated data for all images, combining pixel-X and pixel-Y (odd/even)
//    public double [][] savedJacobian=null; // Jacobian saved from previous calculation
    public double      currentRMS=-1.0; // calculated RMS for the currentVector->currentfX
    public double      nextRMS=-1.0; // calculated RMS for the nextVector->nextfX
    public double      firstRMS=-1.0; // RMS before current series of LMA started

    public double      currentRMSPure=-1.0; // calculated RMS for the currentVector->currentfX
    public double      nextRMSPure=-1.0; // calculated RMS for the nextVector->nextfX
    public double      firstRMSPure=-1.0; // RMS before current series of LMA started
    
    public double lambdaStepUp=   8.0; // multiply lambda by this if result is worse
    public double lambdaStepDown= 0.5; // multiply lambda by this if result is better
    public double thresholdFinish=0.001; // (copied from series) stop iterations if 2 last steps had less improvement (but not worsening ) 
    public int    numIterations=  100; // maximal number of iterations
    public double maxLambda=      100.0;  // max lambda to fail
    
    public double lambda=0.001;        // copied from series
    public double [] lastImprovements= {-1.0,-1.0}; // {last improvement, previous improvement}. If both >0 and < thresholdFinish - done
    public int    iterationStepNumber=0;
    public boolean stopEachStep=  true;  // open dialog after each fitting step
    public boolean stopEachSeries=true;  // open dialog when each fitting series finished
    public boolean stopOnFailure= true;  // open dialog when fitting series failed
    public boolean showParams=   false;   // show modified parameters
    public boolean showThisImages=false; // show debug images for the current ("this" state,before correction) state of parameters
    public boolean showNextImages=false; // show debug images for the current (after correction) state of parameters
    public boolean askFilter=     false; // show debug images for the current (after correction) state of parameters
    
 //   public boolean showGridCorr=  true;  // show grid correction
 //   public boolean showIndividual=true;  // show individual image residuals
 //   public double  corrScale=     1.0;   // scale grid correction before applying
    
    public int     seriesNumber=0; // just for the dialog
    public boolean saveSeries=false;   // just for the dialog
    public double [][][] pixelCorrection=null; // for each sensor: corr-X, corr-Y, mask, flat-field-Red, flat-field-Green, flat-field-Blue
    public String []  pathNames=null;
    public int        pixelCorrectionDecimation=1;
    public int        pixelCorrectionWidth=2592;
    public int        pixelCorrectionHeight=1936;
    public double     RMSscale=Math.sqrt(2.0); // errors for x and y are calculated separately, so actual error is larger
    
    public boolean  showIndex=true;
    public boolean  showRMS=true;
    public boolean  showPoints=true;
    public boolean  showLensLocation=true;
    public boolean  showEyesisParameters=true;
    public boolean  showIntrinsicParameters=true;
    public boolean  showExtrinsicParameters=true;
    public int      extraDecimals=0;

    public boolean   threadedLMA=true; // use threaded/partial method to solve LMA 
    public LMAArrays lMAArrays=null;
    public LMAArrays  savedLMAArrays=null;
    public long startTime=0;
    public int debugLevel=2;
    public boolean updateStatus=true;
    public int threadsMax=100;
    public AtomicInteger stopRequested=null; // 1 - stop now, 2 - when convenient
    
    public String [] status ={"",""};  
/*
    public void showStatus (String msg, int slot){
    	String separator = " | ";
    	if ((slot<0) || (slot>=status.length)) return;
    	status[slot]=msg;
    	String s=status[0];
    	for (int i=1;i<status.length;i++) if (status[i].length()>0) s+=separator+status[i];
    	IJ.showStatus(s);
    }
    public void showProgress(double frac){
    	if ((frac>=1.0) || (frac <=0)) showStatus("",1);
    	showStatus(IJ.d2s(100*frac,2)+"%",1);
    }
    public void showProgress(int currentIndex, int finalIndex){
    	showProgress((currentIndex+1.0)/(double)finalIndex);
    }
 */  
    public class LMAArrays {
        public double [][] jTByJ=  null; // jacobian multiplied by Jacobian transposed
        public double []   jTByDiff=null; // jacobian multiplied difference vector
        public LMAArrays clone() {
        	LMAArrays lma=new LMAArrays();
        	lma.jTByJ = this.jTByJ.clone();
        	for (int i=0;i<this.jTByJ.length;i++) lma.jTByJ[i]=this.jTByJ[i].clone();
        	lma.jTByDiff=this.jTByDiff.clone();
        	return lma;
        }
    }
    public Distortions (){}
	public Distortions (
			LensDistortionParameters lensDistortionParameters,
			PatternParameters patternParameters,
			RefineParameters refineParameters,
			AtomicInteger stopRequested
	){
//		this.patternParameters=patternParameters.clone();  // why clone here?
//		this.lensDistortionParameters=lensDistortionParameters.clone();
		this.patternParameters=patternParameters;  // why clone here?
		this.lensDistortionParameters=lensDistortionParameters;
		this.refineParameters=refineParameters;
		this.stopRequested=stopRequested;
	}
	public int getNumInputs(){return numInputs;}
	public int getNumOutputs(){return numOutputs;}
/**
 * Prerequisites:
 * this.patternParameters, this.fittingStrategy are already initialized
 * 
 */
	/*
	private void initImageSetAndGrids(){  // never used??
// Calculate patter x,y,z==0 and alpha (1.0 - inside, 0.0 - outside) for the grid
// TODO: and save/restore to file to account for non-perfect grid
		patternParameters.calculateGridGeometry();
//  Read all grid data files (4-slice TIFF images) and create  pixelsXY and  pixelsUV arrays		
		fittingStrategy.distortionCalibrationData.readAllGrids(patternParameters);
		if (this.debugLevel>3) {
			for (int n=0;n<fittingStrategy.distortionCalibrationData.pixelsXY.length;n++) {
				for (int i=0;i<fittingStrategy.distortionCalibrationData.pixelsXY[n].length;i++){
					System.out.println(n+":"+i+"  "+
							fittingStrategy.distortionCalibrationData.pixelsUV[n][i][0]+"/"+
							fittingStrategy.distortionCalibrationData.pixelsUV[n][i][1]+"  "+
							IJ.d2s(fittingStrategy.distortionCalibrationData.pixelsXY[n][i][0], 2)+"/"+
							IJ.d2s(fittingStrategy.distortionCalibrationData.pixelsXY[n][i][1], 2)
					);
				}
			}
		}
	}
	*/
	
	public void resetGridImageMasks(){
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		System.out.println("resetGridImageMasks()");
		for (int imgNum=0;imgNum<numImg;imgNum++){
			fittingStrategy.distortionCalibrationData.gIP[imgNum].resetMask();
		}
	}
	// TODO - make station-dependent? Pass sensor mask and combine it?
	public void calculateGridImageMasks(
			final double minContrast,
			final double shrinkBlurSigma,
			final double shrinkBlurLevel,
			final int threadsMax,
			final boolean updateStatus
			){
		final int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		final  DistortionCalibrationData.GridImageParameters [] distortionCalibrationData=this.fittingStrategy.distortionCalibrationData.gIP;
		if (updateStatus) IJ.showStatus("Calculating grid image masks...");
		System.out.print("Calculating grid image masks...");
		System.out.print(" minContrast="+minContrast+" shrinkBlurSigma="+shrinkBlurSigma+" shrinkBlurLevel="+shrinkBlurLevel);

   		final AtomicInteger imageNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger imageFinishedAtomic = new AtomicInteger(0);
   		final Thread[] threads = newThreadArray(threadsMax);
   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				public void run() {
   					for (int imgNum=imageNumberAtomic.getAndIncrement(); imgNum<numImg;imgNum=imageNumberAtomic.getAndIncrement()){
   						distortionCalibrationData[imgNum].calculateMask(
   			        			minContrast,
   			        			shrinkBlurSigma,
   			        			shrinkBlurLevel);
							final int numFinished=imageFinishedAtomic.getAndIncrement();
   							SwingUtilities.invokeLater(new Runnable() {
   								public void run() {
   									if (updateStatus) IJ.showProgress(numFinished,numImg);
   								}
   							});

   					} // for (int numImage=imageNumberAtomic.getAndIncrement(); ...
   				} // public void run() {
   			};
   		}
   		startAndJoin(threads);
		if (updateStatus) IJ.showProgress(0);
		if (updateStatus) IJ.showStatus("Calculating grid image masks... DONE");
		System.out.println("  Done");

	}

	
/**
 * once per fitting strategy series:
 *   1) repeat for each image/point patternParameters.getXYZM(int u, int v) and create
 *      this.targetXYZ;
 *   
 *   2)fittingStrategy.buildParameterMap (int numSeries) 
 *   Creates map from the parameter vector index to the {grid image number, parameter number}
 *   When the parameter is shared by several images, the map points to the one which value will be used
 *   (they might be different). Timestamp of the masterImages[] is used to determine which image to use.  
 *   Simultaneously creates this.reverseParameterMap that maps each of the image/parameter to the parameter vector
 *   Needs to be run for each new strategy series
 *   
 * 	 3)this.currentVector=fittingStrategy.getSeriesVector(); // and save it in the class instance
 *   Calculate vector of the parameters used in LMA algorithm, extracted from the
 *   individual data, using parameter map (calculated once after changing series)
 *
 *    public double []   currentVector; // current variable parameter vector
 *    
 */
	final public int filterMulti=            1;
	final public int filterContrast=         2;
	final public int filterSensor=           4;
	final public int filterTargetMask=       8;
	final public int filterTargetAlpha=     16;
	final public int filterTargetErrors=    32;
	final public int filterMaskBadNodes=    64;
	final public int filterDiameter=       128; // use measured grid "diameter" to change image weight 
	final public int filterChannelWeights= 256; // different weights for channels (higher weight for bottom sensors) 
	final public int filterYtoX=           512; // different weights for channels (higher weight for bottom sensors) 

	final public int filterForAll=             filterMulti+filterContrast+filterSensor+filterTargetMask+filterTargetAlpha+filterTargetErrors+filterMaskBadNodes+
	filterDiameter+filterChannelWeights+filterYtoX;
	final public int filterForSensor=          filterMulti+filterContrast             +filterTargetMask+filterTargetAlpha+filterTargetErrors+filterMaskBadNodes+
	filterDiameter+filterChannelWeights+filterYtoX;
	final public int filterForTargetGeometry=  filterMulti+filterContrast+filterSensor+filterMaskBadNodes+filterDiameter+filterChannelWeights+filterYtoX;
	final public int filterForTargetFlatField= filterMulti+filterContrast+filterSensor+filterMaskBadNodes+filterDiameter+filterChannelWeights+filterYtoX;

	public int selectFilter(int dfltFilter){
		GenericDialog gd = new GenericDialog("Select series to process");
		int filter=    dfltFilter;
		gd.addCheckbox("filterMulti",         (filterForAll & filterMulti)!=0);
		gd.addCheckbox("filterContrast",      (filterForAll & filterContrast)!=0);
		gd.addCheckbox("filterSensor",        (filterForAll & filterSensor)!=0);
		gd.addCheckbox("filterTargetMask",    (filterForAll & filterTargetMask)!=0);
		gd.addCheckbox("filterTargetAlpha",   (filterForAll & filterTargetAlpha)!=0);
		gd.addCheckbox("filterTargetErrors",  (filterForAll & filterTargetErrors)!=0);
		gd.addCheckbox("filterMaskBadNodes",  (filterForAll & filterMaskBadNodes)!=0);
		gd.addCheckbox("filterDiameter",      (filterForAll & filterDiameter)!=0); 
		gd.addCheckbox("filterChannelWeights",(filterForAll & filterChannelWeights)!=0); 
		gd.addCheckbox("filterYtoX",          (filterForAll & filterYtoX)!=0); 
		
		
		
		gd.showDialog();
		if (gd.wasCanceled()) return filter;
		filter=0;
		if (gd.getNextBoolean()) filter |= filterMulti;
		if (gd.getNextBoolean()) filter |= filterContrast;
		if (gd.getNextBoolean()) filter |= filterSensor;
		if (gd.getNextBoolean()) filter |= filterTargetMask;
		if (gd.getNextBoolean()) filter |= filterTargetAlpha;
		if (gd.getNextBoolean()) filter |= filterTargetErrors;
		if (gd.getNextBoolean()) filter |= filterMaskBadNodes;
		if (gd.getNextBoolean()) filter |= filterDiameter; 
		if (gd.getNextBoolean()) filter |= filterChannelWeights; 
		if (gd.getNextBoolean()) filter |= filterYtoX; 
		if (this.debugLevel>1) System.out.println("Using filter bitmap: "+filter);
		return filter;
    }
	
	public void initFittingSeries(
			boolean justSelection, // use series to get selection only
			int filter,
			int numSeries) {
		if (initFittingSeries(
			justSelection, // use series to get selection only
			filter,
			numSeries,
			1)){
			initFittingSeries(
					justSelection, // use series to get selection only
					filter,
					numSeries,
					2);
		}
		
	}
	//returns true if some images were disabled and re-calculation is needed
	public boolean initFittingSeries(
			boolean justSelection, // use series to get selection only
			int filter,
			int numSeries,
			int pass) {
		boolean skipMinVal=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalValidNodes<0;
		if ((pass>1) && skipMinVal){ System.out.println("initFittingSeries("+justSelection+","+filter+","+numSeries+"), skipMinVal="+skipMinVal); return false;} // debug - skipping new functionality
		System.out.println("initFittingSeries("+justSelection+","+filter+","+numSeries+"), pass="+pass);
		//TODO: ********* Implement comments above ************
		  // calculate total number of x/y pairs in the selected images
		if (numSeries<0)justSelection=true;
		if ((pass==1) && (numSeries>=0)) fittingStrategy.invalidateSelectedImages(numSeries); // next selectedImages() will select all, including empty
		if (!justSelection) {
			fittingStrategy.buildParameterMap (numSeries); // also sets currentSeriesNumber
		} else{
			fittingStrategy.currentSeriesNumber=numSeries;
		}
		int numXYPairs=0; 
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		if (this.debugLevel>3)	System.out.println("initFittingSeries("+numSeries+"), numImg="+numImg);
		if ((pass==1) && (numSeries>=0) && !skipMinVal) fittingStrategy.initSelectedValidImages(numSeries); // copy from selected images

		boolean [] selectedImages=fittingStrategy.selectedImages(numSeries); // -1 OK, will select all
		if (this.debugLevel>3)	System.out.println("initFittingSeries("+numSeries+"), selectedImages.length="+selectedImages.length);
		for (int imgNum=0;imgNum<numImg;imgNum++) if (selectedImages[imgNum]) {
			numXYPairs+=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;
		}
		this.targetXYZ=new double[numXYPairs][3];
		this.Y= new double[numXYPairs*2];
		this.weightFunction=new double[numXYPairs*2];
		this.sumWeights=0.0;
		this.imageStartIndex=new int [numImg+1];
		// added here, was using pixelCorrectionDecimation==1
		this.pixelCorrectionDecimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		this.pixelCorrectionWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		this.pixelCorrectionHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;
	
		
		int sensorCorrWidth= (this.pixelCorrectionWidth-1)/this.pixelCorrectionDecimation+1;
		double [] multiWeight=new double [numImg];
		for (int imgNum=0;imgNum<numImg;imgNum++) multiWeight[imgNum]=0.0;
        double minimalGridContrast=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalGridContrast;
        double shrinkBlurSigma=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.shrinkBlurSigma;
        double shrinkBlurLevel=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.shrinkBlurLevel;
        calculateGridImageMasks(
        		minimalGridContrast, // final double minContrast,
        		shrinkBlurSigma, //final double shrinkBlurSigma,
        		shrinkBlurLevel, //final double shrinkBlurLevel,
    			100, //final int threadsMax,
    			true //final boolean updateStatus
    			);
//        this.imageSetWeight=new double[this.fittingStrategy.distortionCalibrationData.gIS.length];
        
        if ((filter & this.filterChannelWeights)!=0) calculateChannelsWeights(
        		this.fittingStrategy.currentSeriesNumber,
        		fittingStrategy.distortionCalibrationData.eyesisCameraParameters.balanceChannelWeightsMode);
        
        for (int imgSet=0;imgSet<this.fittingStrategy.distortionCalibrationData.gIS.length;imgSet++){
        	this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight=0.0;
        	int numUsed=0;
        	int stationNumber=0;
        	int numInSet=((this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet!=null)?
        			this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet.length:0);
        	for (int i=0;i<numInSet;i++){
        		if (this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet[i]!=null) {
        			stationNumber=this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet[i].getStationNumber(); // should be the same for all images
        			int imgNum=this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet[i].imgNumber;
        			if ((imgNum>=0) && selectedImages[imgNum]) numUsed++; // counting only selected in this fitting series, not all enabled !
        		}
        	}
        	if (numUsed>0) {
        		double d;
        		switch (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightMultiImageMode){
        		case 0: d=1.0; break;
        		case 1: d=Math.pow(numUsed,fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightMultiExponent);
        		break; 
        		case 2: d=(numUsed>1)?(Math.pow(numUsed,fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightMultiExponent)):0.001; break; // virtually eliminate single-image sets, but prevent errors
        		case 3: d=numUsed*numUsed; break;
        		default: d=1.0;
        		}
        		d*=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.stationWeight[stationNumber];
//        		set weight will be calculated as sum of all points weights 
//        		this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight=d;
        		for (int i=0;i<numInSet;i++){
        			if (this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet[i]!=null) {
        				int imgNum=this.fittingStrategy.distortionCalibrationData.gIS[imgSet].imageSet[i].imgNumber;
        				if ((imgNum>=0) && selectedImages[imgNum]) multiWeight[imgNum]= d;
        			}
        		}
        	}
        }
        int patternMaskIndex=3;
        int patternAlphaIndex=7;
        int patternErrorMaskIndex=8;
		int index=0;
		double weightScaleX=1.0,weightScaleY=1.0;
		if (((filter & this.filterYtoX)!=0) && (this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightYtoX!=1.0)) {
			weightScaleX/=Math.sqrt(this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightYtoX);
			weightScaleY*=Math.sqrt(this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightYtoX);
		}
		double weightSumXY=weightScaleX+weightScaleY;
		for (int imgNum=0;imgNum<numImg;imgNum++){
			this.imageStartIndex[imgNum]=index;
			if (selectedImages[imgNum]) {
				int chnNum=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
				int station=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
				int setNumber=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getSetNumber();
				double [] gridWeight=fittingStrategy.distortionCalibrationData.gIP[imgNum].getGridWeight();
				double gridImageWeight=1.0;
				if (((filter & this.filterDiameter)!=0) && (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightDiameterExponent>0.0)) {
					gridImageWeight*=Math.pow(setImageDiameter(imgNum),fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightDiameterExponent);
				}
				if ((filter & this.filterChannelWeights)!=0) {
					gridImageWeight*=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[station][chnNum].getChannelWeightCurrent();
				}
				for (int pointNumber=0;pointNumber<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;pointNumber++){

					double [] XYZMP=patternParameters.getXYZMPE(
							fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointNumber][0], 
							fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointNumber][1],
							station,
							chnNum,
							false);
//		 * @return null if out of grid, otherwise X,Y,Z,mask (binary),R (~0.5..1.2),G,B,alpha (0.0..1.0)
/*					double [] XYZM=patternParameters.getXYZM( // will throw if outside or masked out
							fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointNumber][0], 
							fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointNumber][1]);*/
					this.targetXYZ[index][0]=XYZMP[0];
					this.targetXYZ[index][1]=XYZMP[1];
					this.targetXYZ[index][2]=XYZMP[2];
					double weight=1.0;
					if ((filter & this.filterSensor)!=0) {
						weight*=fittingStrategy.distortionCalibrationData.getMask( // returns 1.0 if sensor mask is not available
								chnNum,
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][0],
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][1]);
					}
//					Individual image mask is needed as some parts can be obscured by moving parts - not present on  all images.
//					grid "contrast" may be far from 1.0 but probably should work OK
///					double gridContrast= fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][2]-minimalGridContrast;//minimalGridContrast\
					if ((filter & this.filterContrast)!=0) {
						double gridContrast= gridWeight[pointNumber];
						weight*=gridContrast;
						if (Double.isNaN(gridContrast) && (this.debugLevel>1)) System.out.println("gridContrast=NaN, imgNum="+imgNum);

					}
					if ((filter & this.filterTargetMask)!=0) {
						weight*=XYZMP[patternMaskIndex];//DONE: Use grid mask also (fade out outer grid nodes?)
						if (Double.isNaN(XYZMP[patternMaskIndex]) && (this.debugLevel>1)) System.out.println("XYZMP["+patternMaskIndex+"]=NaN, imgNum="+imgNum);
					}
					if ((filter & this.filterTargetAlpha)!=0) {
						weight*=XYZMP[patternAlphaIndex];//DONE: Use grid mask also (fade out outer grid nodes?)
						if (Double.isNaN(XYZMP[patternAlphaIndex]) && (this.debugLevel>1)) System.out.println("XYZMP["+patternAlphaIndex+"]=NaN, imgNum="+imgNum);
					}
					if ((filter & this.filterTargetErrors)!=0) {
						weight*=XYZMP[patternErrorMaskIndex];//DONE: Use grid mask also (fade out outer grid nodes?)
						if (Double.isNaN(XYZMP[patternErrorMaskIndex]) && (this.debugLevel>1)) System.out.println("XYZMP["+patternErrorMaskIndex+"]=NaN, imgNum="+imgNum);
					}
					if ((filter & this.filterMulti)!=0) {
						weight*=multiWeight[imgNum];
						if (Double.isNaN(multiWeight[imgNum]) && (this.debugLevel>1)) System.out.println("multiWeight["+imgNum+"]=NaN, imgNum="+imgNum);
					}				
					if ((filter & this.filterMaskBadNodes)!=0) {
						if (fittingStrategy.distortionCalibrationData.gIP[imgNum].isNodeBad(pointNumber)) weight=0.0;
					}
					
					//fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightMultiExponent)
					if (((filter & this.filterDiameter)!=0) && (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.weightDiameterExponent>0.0)) {
						weight*=gridImageWeight;
						if (Double.isNaN(gridImageWeight) && (this.debugLevel>1)) System.out.println("gridImageWeight=NaN, imgNum="+imgNum);

					}
					
					
					
					
					if (Double.isNaN(weight)) {
						weight=0.0; // find who makes it NaN
						if (Double.isNaN(multiWeight[imgNum])) System.out.println("weight is null, imgNum="+imgNum);
					}

					
					this.weightFunction[2*index]=  weight*weightScaleX;
					this.weightFunction[2*index+1]=weight*weightScaleY;
					this.sumWeights+=              weight*weightSumXY;
	        		this.fittingStrategy.distortionCalibrationData.gIS[setNumber].setWeight+=2.0*weight;  // used for variances - proportional to the set weight
					if (this.pixelCorrection==null){
						this.Y[2*index]=  fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][0];
						this.Y[2*index+1]=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][1];
					} else {
// TODO: remove and use new code (if tested OK)						
						double [] pXY={
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][0],
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][1]
						};
// TODO: Should it be interpolated? Correction is normally small/smooth, so it may be not important						
						int indexXY=((int) Math.floor(pXY[0]/this.pixelCorrectionDecimation)) +
						((int) Math.floor(pXY[1]/this.pixelCorrectionDecimation))*sensorCorrWidth;
						if (this.pixelCorrection[chnNum][0].length<=indexXY){
							System.out.println("initFittingSeries("+numSeries+") bug:");
							System.out.println("this.pixelCorrection["+chnNum+"][0].length="+this.pixelCorrection[chnNum][0].length);
							System.out.println("indexXY="+indexXY+" pXY[0]="+pXY[0]+", pXY[1]="+pXY[1]+" sensorCorrWidth="+sensorCorrWidth);
							
						} else {
							this.Y[2*index]=  pXY[0]-this.pixelCorrection[chnNum][0][indexXY]; //java.lang.ArrayIndexOutOfBoundsException: 3204663
							this.Y[2*index+1]=pXY[1]-this.pixelCorrection[chnNum][1][indexXY];
						}
// TODO: remove above and un-comment below	(after testing)					
/*						
						double [] vector=interpolateCorrectionVector(
								chnNum,
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][0],
								fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[pointNumber][1]);
						this.Y[2*index]=  pXY[0]-vector[0];
						this.Y[2*index+1]=pXY[1]-vector[1];
*/
					}
					index++;
				}
//				numXYPairs+=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length; ??
			}
		}
		this.imageStartIndex[numImg]=index; // one after last 
		if ((pass==1) && (numSeries>=0) && !skipMinVal){
    		// count non-zero weight nodes for each image, disable image if this number uis less than
    		int needReCalc=0;
    		for (int imgNum=0;imgNum<numImg;imgNum++) if (selectedImages[imgNum]) {
    			index=this.imageStartIndex[imgNum];
    			int numValidNodes=0;
    			for (int pointNumber=0;pointNumber<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;pointNumber++){
    				if (2*(index+pointNumber)>=this.weightFunction.length){
    					System.out.println("BUG@535: this.weightFunction.length="+this.weightFunction.length+" index="+index+
    							" pointNumber="+pointNumber+" imgNum="+imgNum+" pixelsUV.length="+
    							fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length+
    							" numXYPairs="+numXYPairs);
    					
    					continue;
    				}
    				if (this.weightFunction[2*(index+pointNumber)]>0.0) numValidNodes++; //OOB 5064
    			}
    			if (numValidNodes<this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalValidNodes){
    				this.fittingStrategy.invalidateSelectedImage(numSeries,imgNum);
    				needReCalc++;
    				if (this.debugLevel>1){
    					System.out.println("Number of valid nodes in image #"+imgNum+" is "+numValidNodes+" < "+
    							this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalValidNodes+
    							", this image will be temporarily disabled");
    				}
    			}
    		}
    		if (needReCalc>0) {
    			if (this.debugLevel>1) System.out.println("Number of temporarily disabled images="+needReCalc );
    			return true; // will need a second pass
    		} else {
    			if (this.debugLevel>1) System.out.println("No images disabled, no need for pass #2");
    		}
    	}
		// Normalize set weights
		int numSetsUsed=0;
		double totalSetWeight=0.0;
        for (int imgSet=0;imgSet<this.fittingStrategy.distortionCalibrationData.gIS.length;imgSet++){
        	if (this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight>0){
        		numSetsUsed++;
        		totalSetWeight+=this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight;
        	}
        }
        double setWeightScale=numSetsUsed/totalSetWeight;
        if (numSetsUsed>0){
            for (int imgSet=0;imgSet<this.fittingStrategy.distortionCalibrationData.gIS.length;imgSet++){
            	if (this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight>0){
            		numSetsUsed++;
            		this.fittingStrategy.distortionCalibrationData.gIS[imgSet].setWeight*=setWeightScale;
            	}
            }
        }
// last? not here!        
//		this.imageStartIndex[numImg]=index; 
		if (justSelection) {
			this.currentVector = null;
			this.lambda=0.0;

		} else {
			this.currentVector =fittingStrategy.getSeriesVector(); // here?
			// for now - use common parameters, later maybe restore /add individual
			//    	this.lambda=fittingStrategy.getLambda();
			//    	was commented out???

			this.lambda=fittingStrategy.getLambda();
		   	if ((this.fittingStrategy.varianceModes!=null)
		   			&& (this.fittingStrategy.varianceModes[numSeries]!=this.fittingStrategy.varianceModeDisabled)) fittingStrategy.buildVariancesMaps (numSeries); // return value lost
		}
//    	this.thresholdFinish=fittingStrategy.getStepDone();
    	this.iterationStepNumber=0;
    	// should be calculated after series weights are set
//    public int    []   imageStartIndex=null; // elements containing index of the start point of the selected image, first element 0, last - total number of points.
// TODO: add copying  lambdaStepUp,lambdaStepDown? 
    	return false;
	}
	
	
	public void calculateChannelsWeights(
			int numSeries,
			double balanceChannelWeightsMode){
		if (balanceChannelWeightsMode==0) return; // keep current weights
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		int numStations=fittingStrategy.distortionCalibrationData.getNumStations();
		int numChannels=fittingStrategy.distortionCalibrationData.getNumChannels();

		if (balanceChannelWeightsMode<0) { //copy specified defaults to current values
			for (int station=0;station<numStations;station++){
				for (int chn=0;chn<numChannels;chn++){
					this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[station][chn].setChannelWeightCurrent(
							this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][chn].getChannelWeightDefault()); // from station 0
				}
			}
		} else {
			double exp=balanceChannelWeightsMode;
			double [][] sumChnWeights=new double [numStations][numChannels];
			double [] avgWeights=new double [numStations];
			int [] numNonzeroChannels=new int [numStations];
			for (int station=0;station<numStations;station++){
				avgWeights[station] =0.0;
				numNonzeroChannels[station] =0;
				for (int chn=0;chn<numChannels;chn++) sumChnWeights[station][chn] =0.0;
			}
			boolean [] selectedImages=fittingStrategy.selectedImages(numSeries); // -1 OK, will select all
			for (int imgNum=0;imgNum<numImg;imgNum++)if (selectedImages[imgNum]) {
					int chn=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
					int station=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
					sumChnWeights[station][chn]+=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getNumContrastNodes(
							this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalGridContrast);
			}
			for (int station=0;station<numStations;station++){
				for (int chn=0;chn<numChannels;chn++) if (sumChnWeights[station][chn]>0) {
					avgWeights[station]+=sumChnWeights[station][chn];
					numNonzeroChannels[station]++;
				}
				if (numNonzeroChannels[station]>0) avgWeights[station]/=numNonzeroChannels[station];
			}
			for (int station=0;station<numStations;station++){
				for (int chn=0;chn<numChannels;chn++) if (sumChnWeights[station][chn]>0) {
					double weight=(sumChnWeights[station][chn]>0.0)?Math.pow(avgWeights[station]/sumChnWeights[station][chn],exp):0.0;
					this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[station][chn].setChannelWeightCurrent(
							weight);
				}
			}
			
		}
		
	}
	
	public double setImageDiameter(int imgNum){
		int debugThreshold=2;
		int chnNum=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
        double minimalGridContrast=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.minimalGridContrast;
		int station=this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
		EyesisSubCameraParameters esp=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[station][chnNum];
        double r0pix=1000.0*esp.distortionRadius/esp.pixelSize;
        this.fittingStrategy.distortionCalibrationData.gIP[imgNum].setImageDiameter( // need to get image center px,py. Maybe r0 - use to normalize result diameter
    			esp.px0, // double xc,
    			esp.py0, // double yc,
    			r0pix,   // double r0,
    			minimalGridContrast,//  double minContrast
    			(this.debugLevel>debugThreshold)?imgNum:-1);
        return this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getGridDiameter();
	}
	
	public void listImageSets(){ // TODO: use series -1 - should work now
//		boolean [] oldSelection=this.fittingStrategy.selectAllImages(0); // enable all images in series 0
		if (this.fittingStrategy.distortionCalibrationData.gIS!=null){
			if (this.debugLevel>2){
				System.out.println("listImageSets() 1: ");
				for (int is=0;is<this.fittingStrategy.distortionCalibrationData.gIS.length;is++){
					System.out.println("listImageSets() 1: "+is+": tilt="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].goniometerTilt+" axial="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].goniometerAxial+" estimated="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].orientationEstimated);
				}
			}
		}

		int filter=this.filterForAll;
		if (this.askFilter) filter=selectFilter(filter);
		initFittingSeries(false,filter,-1); // first step in series
		if (this.fittingStrategy.distortionCalibrationData.gIS!=null){
			if (this.debugLevel>2){
				System.out.println("listImageSets() 2: ");
				for (int is=0;is<this.fittingStrategy.distortionCalibrationData.gIS.length;is++){
					System.out.println("listImageSets() 2: "+is+": tilt="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].goniometerTilt+" axial="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].goniometerAxial+" estimated="+
							this.fittingStrategy.distortionCalibrationData.gIS[is].orientationEstimated);
				}
			}
		}
//	    initFittingSeries(true,this.filterForAll,0); // will set this.currentVector
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		double [] errors=calcErrors(calcYminusFx(this.currentfX));
		int [] numPairs=calcNumPairs();
		
	    int [][] imageSets=this.fittingStrategy.distortionCalibrationData.listImages(false); // true - only enabled images
	    int [] numSetPoints=new int [imageSets.length];
	    double [] rmsPerSet=new double[imageSets.length];
	    boolean [] hasNaNInSet=new boolean[imageSets.length];
	    for (int setNum=0;setNum<imageSets.length;setNum++){
	    	double error2=0.0;
	    	int numInSet=0;
    		hasNaNInSet[setNum]=false;
	    	for (int imgInSet=0;imgInSet<imageSets[setNum].length;imgInSet++) {
	    		int imgNum=imageSets[setNum][imgInSet];
	    		int num=numPairs[imgNum];
	    		if (Double.isNaN(errors[imgNum])){
	    			hasNaNInSet[setNum]=true;
	    		} else {
	    			error2+=errors[imgNum]*errors[imgNum]*num;
	    			numInSet+=num;
	    		}
	    	}
	    	numSetPoints[setNum]=numInSet;
	    	rmsPerSet[setNum]=Math.sqrt(error2/numInSet);
	    }
	    this.fittingStrategy.distortionCalibrationData.listImageSet(numSetPoints, rmsPerSet,hasNaNInSet );
//		this.fittingStrategy.setImageSelection(0, oldSelection); // restore original selection in series 0
	}
	
	
	public void updateSensorMasks(){
		int alphaIndex=2;
		if (this.pixelCorrection==null){
			System.out.println("Sensor data is null, can not update sensor masks");
			return;
		}
		if (this.debugLevel>0) System.out.println("Updating sensor masks in sensor data");
		for (int i=0;(i<this.fittingStrategy.distortionCalibrationData.sensorMasks.length) && (i<this.pixelCorrection.length);i++){
			this.pixelCorrection[i][alphaIndex]=this.fittingStrategy.distortionCalibrationData.sensorMasks[i].clone();
		}
		
	}

	public boolean correctPatternFlatField(boolean enableShow){
		if (this.debugLevel>0) System.out.println("=== Performing pattern flat field correction");
		this.patternParameters.updateNumStations(this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getNumStations());
		//		if (this.refineParameters.flatFieldUseSelectedChannels && (ABERRATIONS_PARAMETERS!=null))selectedChannels=ABERRATIONS_PARAMETERS.selectedChannels;
		double [][] masks= nonVignettedMasks(
				this.refineParameters.flatFieldShrink,
				this.refineParameters.flatFieldNonVignettedRadius,
				this.refineParameters.flatFieldMinimalAlpha);
		/*	    if (selectedChannels!=null){
	    	for (int nChn=0;nChn<masks.length;nChn++) if ((nChn<selectedChannels.length)&&!selectedChannels[nChn]) masks[nChn]=null;
	    }
		 */	    
		if (enableShow && this.refineParameters.flatFieldShowSensorMasks) (new showDoubleFloatArrays()).showArrays( //java.lang.ArrayIndexOutOfBoundsException: 313632
				masks,
				this.pixelCorrectionWidth/ this.pixelCorrectionDecimation,
				this.pixelCorrectionHeight/this.pixelCorrectionDecimation,
				true,
		"nonVinetting masks");
		double [][][][] sensorGrids=calculateGridFlatField(
				this.refineParameters.flatFieldSerNumber,
				masks,
				this.refineParameters.flatFieldMinimalContrast,
				this.refineParameters.flatFieldMinimalAccumulate,
				this.refineParameters.flatFieldUseInterpolate,
				this.refineParameters.flatFieldMaskThresholdOcclusion, // suspect occlusion only if grid is missing in the area where sensor mask is above this threshold
				this.refineParameters.flatFieldShrinkOcclusion,
				this.refineParameters.flatFieldFadeOcclusion,
				this.refineParameters.flatFieldIgnoreSensorFlatField);
		double [][][] geometry= patternParameters.getGeometry();
		if (enableShow && this.refineParameters.flatFieldShowIndividual){
			for (int station=0;station<sensorGrids.length;station++) if (sensorGrids[station]!=null){
				for (int i=0;i<sensorGrids[station].length;i++) if (sensorGrids[station][i]!=null){
					(new showDoubleFloatArrays()).showArrays(
							sensorGrids[station][i],
							geometry[0].length,
							geometry.length,
							true,
							"chn"+i+":"+station+"-pattern");
				}
			}
		}
		double [][][][] patternArray= combineGridFlatField(
				this.refineParameters.flatFieldReferenceStation,
				sensorGrids,
				this.refineParameters.flatFieldShrinkForMatching,
				this.refineParameters.flatFieldResetMask,
				this.refineParameters.flatFieldMaxRelDiff,
				this.refineParameters.flatFieldShrinkMask,
				this.refineParameters.flatFieldFadeBorder);
		if (enableShow && this.refineParameters.flatFieldShowResult) {
			String [] titles={"Alpha","Red","Green","Blue","Number of images used"};
			for (int station=0;station<patternArray.length;station++) if (patternArray[station]!=null){
				for (int nView=0;nView<patternArray[station].length;nView++) if (patternArray[station][nView]!=null){
					(new showDoubleFloatArrays()).showArrays(
							patternArray[station][nView],
							geometry[0].length,
							geometry.length,
							true,
							"St"+station+"_V"+nView+"_Pattern_Colors "+this.refineParameters.flatFieldMaxRelDiff,
							titles);
				}
			}
		}
		if (this.refineParameters.flatFieldApplyResult) applyGridFlatField(patternArray); // {alpha, red,green,blue, number of images used}[pixel_index]
		return true;
	}
	
	public boolean modifyPixelCorrection(
			boolean   enableShow,
			int       threadsMax,
			boolean   updateStatus,
			int debugLevel
	){
		int filter=this.filterForSensor;
		if (this.askFilter) filter=selectFilter(filter);
    	initFittingSeries(true,filter,this.seriesNumber); // first step in series now uses pattern alpha
//    	initFittingSeries(true,this.filterForSensor,this.seriesNumber); // first step in series now uses pattern alpha
    	this.currentfX=calculateFxAndJacobian(this.currentVector, false);
    	//        	this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.debugLevel>2) {
    		System.out.println("this.currentVector");
    		for (int i=0;i<this.currentVector.length;i++){
    			System.out.println(i+": "+ this.currentVector[i]);
    		}
    	}
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [][][] sensorXYRGBCorr=  allImagesCorrectionMapped(
				selectedImages,
				enableShow && this.refineParameters.showPerImage,
				this.refineParameters.showIndividualNumber,
				threadsMax,
				updateStatus,
				debugLevel);
    	String [] titles={"X-corr(pix)","Y-corr(pix)","weight","Red","Green","Blue"};
		int decimate=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int sWidth= (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
		int sHeight=(fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
		if (enableShow && this.refineParameters.showUnfilteredCorrection) {
			for (int numChn=0;numChn<sensorXYRGBCorr.length;numChn++) if (sensorXYRGBCorr[numChn]!=null){
				//this.SDFA_INSTANCE.showArrays(sensorXYRGBCorr[numChn], sWidth, sHeight,  true, "chn_"+numChn+"_extra_correction", titles);
				showWithRadialTangential(
						titles,
						"chn_"+numChn+"_extra_correction",
						sensorXYRGBCorr[numChn], // [0] - dx, [1] - dy
						sWidth,
						decimate,
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].px0, // using station 0
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].py0);
			}
		}
    	//eyesisSubCameras
    	//extrapolate
    	// TODO: different extrapolation for FF - not circular where shades are in effect (top/bottom)
    	if (!this.refineParameters.sensorExtrapolateDiff) { // add current correction BEFORE extrapolating/blurring
    		addOldXYCorrectionToCurrent(
    				this.refineParameters.correctionScale,
    				sensorXYRGBCorr 
    		);
    	}
    	if (this.refineParameters.extrapolate) {
    		allSensorsExtrapolationMapped(
    				0, //final int stationNumber, // has to be selected
    				sensorXYRGBCorr, //final double [][][] gridPCorr,
    				this.refineParameters.sensorShrinkBlurComboSigma,
    				this.refineParameters.sensorShrinkBlurComboLevel,
    				this.refineParameters.sensorAlphaThreshold,
    				this.refineParameters.sensorStep,
    				this.refineParameters.sensorInterpolationSigma,
    				this.refineParameters.sensorTangentialRadius, 
    				this.refineParameters.sensorScanDistance,
    				this.refineParameters.sensorResultDistance,
    				this.refineParameters.sensorInterpolationDegree,
    				threadsMax,
    				updateStatus,
    				enableShow && this.refineParameters.showExtrapolationCorrection, //final boolean showDebugImages,
    				debugLevel
    		);
    	}
    	if (this.refineParameters.smoothCorrection) {
    		boolean [] whichBlur={true,true,false,true,true,true}; // all but weight
    		IJ.showStatus("Bluring sensor corrections...");
    		for (int numChn=0;numChn<sensorXYRGBCorr.length;numChn++) if (sensorXYRGBCorr[numChn]!=null){
    			DoubleGaussianBlur gb=new DoubleGaussianBlur();
    			for (int m=0;m<whichBlur.length;m++) if (whichBlur[m]){
    				gb.blurDouble(
    						sensorXYRGBCorr[numChn][m],
    						sWidth,
    						sHeight,
    						this.refineParameters.smoothSigma/decimate,
    						this.refineParameters.smoothSigma/decimate,
    						0.01);
    			}
    			IJ.showProgress(numChn+1, sensorXYRGBCorr.length);
    		}
    		IJ.showProgress(1.0);
    	}
    	
    	if (enableShow && this.refineParameters.showThisCorrection ) {
    		for (int numChn=0;numChn<sensorXYRGBCorr.length;numChn++) if (sensorXYRGBCorr[numChn]!=null){
//    		   this.SDFA_INSTANCE.showArrays(sensorXYRGBCorr[numChn], sWidth, sHeight,  true, "chn_"+numChn+"_filtered", titles);
				showWithRadialTangential(
						titles,
						"chn_"+numChn+"_filtered",
						sensorXYRGBCorr[numChn], // [0] - dx, [1] - dy
						sWidth,
						decimate,
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].px0, // using station 0
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].py0);
    		}
    	}
    	if (this.refineParameters.sensorExtrapolateDiff) { // add current correction AFTER extrapolationg/bluring
    		addOldXYCorrectionToCurrent(
    				this.refineParameters.correctionScale,
    				sensorXYRGBCorr 
    		);
    	}

//   	if (!selectCorrectionScale()) return false;
		IJ.showStatus("Applying corrections:"+((!this.refineParameters.applyCorrection && !this.refineParameters.applyFlatField)?
				"none ":((this.refineParameters.applyCorrection?"geometry ":"")+(this.refineParameters.applyFlatField?"flat field":""))));
    	boolean result=applySensorCorrection(
    			this.refineParameters.applyCorrection,
    			this.refineParameters.applyFlatField,
    			this.refineParameters.correctionScale,
    			sensorXYRGBCorr, //sensorXYCorr, // modified to accept both 7(old) and 6(new) entries 
    			fittingStrategy.distortionCalibrationData);
    	if (enableShow && this.refineParameters.showCumulativeCorrection) {
    		for (int numChn=0;numChn<sensorXYRGBCorr.length;numChn++) if (sensorXYRGBCorr[numChn]!=null){
//    		   this.SDFA_INSTANCE.showArrays(sensorXYRGBCorr[numChn], sWidth, sHeight,  true, "Cumulative_chn_"+numChn+"_corrections", titles);
				showWithRadialTangential(
						titles,
						"Cumulative_chn_"+numChn+"_corrections",
						sensorXYRGBCorr[numChn], // [0] - dx, [1] - dy
						sWidth,
						decimate,
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].px0, // using station 0
						fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numChn].py0);
    		}
    	}
    	if (result) {
			updateCameraParametersFromCalculated(false); // update camera parameters from enabled only images (may overwrite some of the above)

    	}
		IJ.showStatus("");
    	return result;
    }
	
	public void showWithRadialTangential(
			String [] preTitles,
			String title,
			double [][] preData, // [0] - dx, [1] - dy
			int width,
			int deciamte,
			double x0,
			double y0){
		int indexDx=0;
		int indexDy=1;
		int indexDr=0;
		int indexDt=1;
		int indexDa=2;
		String [] extraTitles={"R-corr(pix)","T-corr{pix)","A-corr(pix)"};
		int newImages=extraTitles.length;
		int length=preData[0].length;
		int height=length/width;
		double [][] data= new double [preData.length+newImages] [length];
		String [] titles= new String [preTitles.length+newImages];
		for (int i=0;i<preData.length;i++){
			data[i+newImages]=preData[i];
			titles[i+newImages]=preTitles[i];
		}
		for (int i=0;i<newImages;i++){
			titles[i]=extraTitles[i];
			data[i]=new double[length];
		}
		Point2D Z=new Point2D.Double(0.0,0.0);
		for (int i=0;i<length;i++){
			Point2D R=new Point2D.Double((deciamte*(i%width))-x0,(deciamte*(i/width))-y0);
			double r=R.distance(Z);
			Point2D uR=new Point2D.Double(1.0,0.0);
			if (r>0) uR.setLocation(R.getX()/r,R.getY()/r);
			Point2D dXY=new Point2D.Double(preData[indexDx][i],preData[indexDy][i]);
			data[indexDr][i]= dXY.getX()*uR.getX()+dXY.getY()*uR.getY();
			data[indexDt][i]=-dXY.getX()*uR.getY()+dXY.getY()*uR.getX();
			data[indexDa][i]=dXY.distance(Z);
		}
	   this.SDFA_INSTANCE.showArrays(data, width, height,  true, title, titles);
	}
	

	public void addOldXYCorrectionToCurrent(
    		double scale,
    		double [][][] sensorXYCorr
			){
        if (this.pixelCorrection==null) return; // no modifications are needed
		for (int i=0;i<sensorXYCorr.length;i++) if ((sensorXYCorr[i]!=null) && (this.pixelCorrection[i]!=null)) {
			for (int j=0;j<sensorXYCorr[i][0].length;j++){
				sensorXYCorr[i][0][j]=this.pixelCorrection[i][0][j]+scale*sensorXYCorr[i][0][j];
				sensorXYCorr[i][1][j]=this.pixelCorrection[i][1][j]+scale*sensorXYCorr[i][1][j];
			}    	
		}
	}
	
	
  
	public void patternErrors(
			final int       threadsMax,
			final boolean   updateStatus,
			final int debugLevel
			){
		GenericDialog gd=new GenericDialog("Setup pattern errors map");
		gd.addNumericField("Series number", this.seriesNumber, 0,2,"");
		gd.addCheckbox    ("Show map", true);
		
		gd.addNumericField("Minimal RMS", .07, 3,6,"pix");
		gd.addNumericField("Maximal RMS", 0.12, 3,6,"pix");
		gd.addNumericField("Expand EMS mask", 1, 0,2,"nodes");
		gd.addCheckbox    ("Update pattern weights", false);
		gd.addCheckbox    ("Reset error-based target map", false);
		gd.showDialog();
		if (gd.wasCanceled()) return;
		this.seriesNumber =      (int) gd.getNextNumber();
		boolean showMap=               gd.getNextBoolean();
 		double minRMS =                gd.getNextNumber();
 		double maxRMS =                gd.getNextNumber();
		int expandMask =         (int) gd.getNextNumber();

		boolean updateMap=              gd.getNextBoolean();
		boolean resetMap=              gd.getNextBoolean();
		
		if (resetMap){
			this.patternParameters.resetPatternErrorMask();
			return;
		} else {
			double [] worstImageNumber=calculatePatterErrorRMS(
					this.seriesNumber,
					threadsMax,
					updateStatus,
					debugLevel);
			this.patternParameters.savePatternErrorMask();
			double [] savedMask=this.patternParameters.getSavedPatternErrorMask();
			this.patternParameters.calculatePatternErrorMask(maxRMS,minRMS);
			for (int i=0;i<expandMask;i++)this.patternParameters.expandPatternErrorMask();
			if (showMap){
				String [] titles={"mask","rms","worst image number","savedMask"};
				double [][] debugData={
						this.patternParameters.getPatternErrorMask(),
						this.patternParameters.getPatternErrors(),
						worstImageNumber,
						savedMask};
				 Rectangle gridDimensions=patternParameters.getUVDimensions();
				(new showDoubleFloatArrays()).showArrays(
						debugData,
						gridDimensions.width,
						gridDimensions.height,
						true,
						"TM_"+maxRMS+":"+minRMS,
						titles);
			}
			if (!updateMap) {
				System.out.println("Restoring mask to the previous state");
				this.patternParameters.restorePatternErrorMask();
			}
		}
	}

	
	
	public double []  calculatePatterErrorRMS( // returns worst image number array
			final int       series,
			final int       threadsMax,
			final boolean   updateStatus,
			final int debugLevel

	){
    	if (fittingStrategy==null) {
    		String msg="Fitting strategy does not exist, exiting";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (fittingStrategy.distortionCalibrationData.eyesisCameraParameters==null){
    		String msg="Eyesis camera parameters (and sensor dimensions) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	//	fittingStrategy.distortionCalibrationData.readAllGrids(); 
//    	if (! selectGridEnhanceParameters()) return false;
//    	if (series<0) return null; // false; // make "all " later?
    	this.seriesNumber=series;
    	
    	initFittingSeries(true,this.filterForTargetGeometry,this.seriesNumber); // first step in series now uses pattern alpha
    	this.currentfX=calculateFxAndJacobian(this.currentVector, false);
    	//        	this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.debugLevel>2) {
    		System.out.println("this.currentVector");
    		for (int i=0;i<this.currentVector.length;i++){
    			System.out.println(i+": "+ this.currentVector[i]);
    		}
    	}
		final boolean [] selectedImages=fittingStrategy.selectedImages();
		final Rectangle gridDimensions=patternParameters.getUVDimensions();
		final int width=  gridDimensions.width;
		final int height= gridDimensions.height;
//		final int U0=     gridDimensions.x;
//		final int V0=     gridDimensions.y;
		final double [][] gridErrors=new double [4][width*height]; // added debug features - worst image number
		for (int n=0;n<gridErrors.length;n++) for (int i=0;i<gridErrors[n].length;i++) gridErrors[n][i]=0.0;
		int numSelected=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) numSelected++;
		final int finalSelected=numSelected;
		if (updateStatus) IJ.showStatus("Calculating pattern grid errors...");
   		final AtomicInteger imageNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger imageFinishedAtomic = new AtomicInteger(0);
   		final Thread[] threads = newThreadArray(threadsMax);
   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				public void run() {
   					double [][] partialGridErrors=new double [4][width*height];
   					for (int n=0;n<partialGridErrors.length;n++) for (int i=0;i<partialGridErrors[n].length;i++) partialGridErrors[n][i]=0.0;
   					for (int imgNum=imageNumberAtomic.getAndIncrement(); imgNum<selectedImages.length;imgNum=imageNumberAtomic.getAndIncrement()){
   						if (selectedImages[imgNum]){
   							accumulatePatternErrors(
   									partialGridErrors,
   									imgNum,
   									gridDimensions);
   							final int numFinished=imageFinishedAtomic.getAndIncrement();
   							SwingUtilities.invokeLater(new Runnable() {
   								public void run() {
   									if (updateStatus) IJ.showProgress(numFinished,finalSelected);
   								}
   							});
   						} //if (selectedImages[numImage]){
   					} // for (int numImage=imageNumberAtomic.getAndIncrement(); ...
   					combinePatternErrors(partialGridErrors,gridErrors);
   				} // public void run() {
   			};
   		}
   		startAndJoin(threads);
   		for (int i=0;i<gridErrors[0].length;i++){
   			gridErrors[0][i]=(gridErrors[0][i]>0.0)?Math.sqrt(gridErrors[0][i]/gridErrors[1][i]):Double.NaN;
   			
   		}
   		patternParameters.setPatternErrors(gridErrors[0]);
   		return gridErrors[2]; // worst image number for target grid nodes
	}
	
	public void accumulatePatternErrors(
			double [][] errorMap,
			int imgNum,
			Rectangle gridDimensions){
		int width=  gridDimensions.width;
//		int height= gridDimensions.height;
		int U0=     gridDimensions.x; // location of the grid center (U==0,V==0)
		int V0=     gridDimensions.y;
		double [] diff=calcYminusFx(this.currentfX, 2*this.imageStartIndex[imgNum],2*this.imageStartIndex[imgNum+1]);
		int [][] imgUV=	  this.fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV;
		for (int i=0;i<imgUV.length;i++){
			int index=width*(imgUV[i][1]+V0) + (imgUV[i][0]+U0);
			double w=this.weightFunction[2*(this.imageStartIndex[imgNum]+i)];
			double dX=diff[2*i];
			double dY=diff[2*i+1];
			double e2w=w*(dX*dX+dY*dY);
			errorMap[0][index]+=e2w;
			errorMap[1][index]+=w;
			if (e2w>errorMap[3][index]){
				errorMap[3][index]=e2w;    // worst error for this node
				errorMap[2][index]=imgNum; // worst (for that particular grig node) image number
			}
		}
	}
	
	public synchronized void combinePatternErrors(
			double [][] partialErrorMap,
			double [][] fullErrorMap ){
//		for (int n=0;n<fullErrorMap.length;n++) for (int i=0;i<fullErrorMap[n].length;i++) fullErrorMap[n][i]+=partialErrorMap[n][i];
		for (int i=0;i<fullErrorMap[0].length;i++){
			fullErrorMap[0][i]+=partialErrorMap[0][i];
			fullErrorMap[1][i]+=partialErrorMap[1][i];
			if (fullErrorMap[3][i]<partialErrorMap[3][i]){
				fullErrorMap[2][i]=partialErrorMap[2][i];
				fullErrorMap[3][i]=partialErrorMap[3][i];
			}
			
		}

	}

	
	
	
	/**
	 * Calculate each sensor correction increment for geometry and photometry contributed by all images selected in a series
	 * @param selectedImages process only selected images 
	 * @param showIndividual show per-image intermediate results
	 * @param threadsMax maximal number of concurrent threads
	 * @param updateStatus update IJ status/progress
	 * @param debugLevel debug level
	 * @return [sensor]{dpX,dpY,alpha,R,G,B}[pixelIndex] . dpX, dpY - correction to previous, RGB - total FF, not increment! 
	 */

	public double [][][]  allImagesCorrectionMapped(
			final boolean [] selectedImages,
			final boolean showIndividual,
			final int showIndividualNumber,
			final int       threadsMax,
			final boolean   updateStatus,
			final int debugLevel
			){
		int numChannels=  fittingStrategy.distortionCalibrationData.getNumChannels(); // number of used channels
		final double [][][] gridPCorr=new double [numChannels][][];
		for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) gridPCorr[chnNum]=null;
		int numSelected=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) numSelected++;
		final int finalSelected=numSelected;
		if (updateStatus) IJ.showStatus("Calculating sensor corrections...");
   		final AtomicInteger imageNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger imageFinishedAtomic = new AtomicInteger(0);
   		final Thread[] threads = newThreadArray(threadsMax);
   		final AtomicInteger stopRequested=this.stopRequested;
		final AtomicBoolean interruptedAtomic=new AtomicBoolean();
		final int alphaIndex=2;

   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				public void run() {
   					for (int imgNum=imageNumberAtomic.getAndIncrement(); (imgNum<selectedImages.length) && !interruptedAtomic.get();imgNum=imageNumberAtomic.getAndIncrement()){
   						if (selectedImages[imgNum]){
   							int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
   							double [][] singleCorr=
   								singleImageCorrectionMapped(
   									imgNum, // image number
   									showIndividual && ((showIndividualNumber<0) || (showIndividualNumber==chnNum)),
   									debugLevel);
   							combineImageCorrection(
   									chnNum,
   									gridPCorr,
   									singleCorr
   							);
   							final int numFinished=imageFinishedAtomic.getAndIncrement();
   							SwingUtilities.invokeLater(new Runnable() {
   								public void run() {
   									if (updateStatus) IJ.showProgress(numFinished,finalSelected);
   								}
   							});
   	   						if (stopRequested.get()==1){ // ASAP
   	   							interruptedAtomic.set(true);
   	   						}
   						} //if (selectedImages[numImage]){
   					} // for (int numImage=imageNumberAtomic.getAndIncrement(); ...
   				} // public void run() {
   			};
   		}
   		startAndJoin(threads);
   		// divide by weight;
   		for (int nChn=0;nChn<gridPCorr.length;nChn++) if (gridPCorr[nChn]!=null){
   			for (int i=0;i<gridPCorr[nChn].length;i++) {
   				if (i!=alphaIndex){
   					for (int j=0; j<gridPCorr[nChn][i].length;j++){
   						if (gridPCorr[nChn][alphaIndex][j]>0) gridPCorr[nChn][i][j]/=gridPCorr[nChn][alphaIndex][j];
   					}
   				}
   			}   								
   		}
   		
		if (updateStatus) IJ.showProgress(0);
		
   		if (interruptedAtomic.get()) {
   			System.out.println("allImagesCorrection() aborted by user request");
   			return null;
   		}
   		return gridPCorr;
	}

	public void allSensorsExtrapolationMapped(
			final int stationNumber, // has to be selected
			final double [][][] gridPCorr,
			final double shrinkBlurComboSigma,
			final double shrinkBlurComboLevel,
			final double alphaThreshold,
			final double step,
			final double interpolationSigma,
			final double tangentialRadius, 
			final int    scanDistance,
			final int resultDistance,
			final int interpolationDegree,
			final int       threadsMax,
			final boolean   updateStatus,
			final boolean showDebugImages,
			final int debugLevel
			){
		if (updateStatus) IJ.showStatus("Extrapolating sensor corrections...");
   		final AtomicInteger sensorNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger sensorFinishedAtomic = new AtomicInteger(0);
   		final Thread[] threads = newThreadArray(threadsMax);
   		final AtomicInteger stopRequested=this.stopRequested;
		final AtomicBoolean interruptedAtomic=new AtomicBoolean();
		final EyesisSubCameraParameters [] eyesisSubCameras = this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[stationNumber];
		final double [][] sensorMasks=this.fittingStrategy.distortionCalibrationData.sensorMasks;

		final int alphaIndex=2;

		final int sensorWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		final int sensorHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;
		final int decimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		final int width= (sensorWidth-1)/decimation+1; // decimated width (648)
		final int height= (sensorHeight-1)/decimation+1; // decimated width (648)
		final boolean extraShowDebug=showDebugImages&& (debugLevel>2);

   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				public void run() {
   					DoubleGaussianBlur gb=null;
   					double [][] debugMasks1=null;
   					double [][] debugMasks2=null;
   					String [] debugMaskTitles={"original","blured"};
   					if (extraShowDebug){
   						debugMasks1=new double[2][];
   						debugMasks2=new double[2][];
   					}
   					if (shrinkBlurComboSigma>0.0) gb=new DoubleGaussianBlur();
   					for (int sensorNum=sensorNumberAtomic.getAndIncrement(); (sensorNum<gridPCorr.length) && !interruptedAtomic.get();sensorNum=sensorNumberAtomic.getAndIncrement()){
   						if (gridPCorr[sensorNum]!=null){
   							final double [] centerPXY={
   									eyesisSubCameras[sensorNum].px0,
   									eyesisSubCameras[sensorNum].py0
   							};
   							if (shrinkBlurComboSigma>0.0){
   								double sigma=shrinkBlurComboSigma/decimation;
   								int margin=(int) (2*sigma);
   								int width1=width+2*margin;
   								int height1=height+2*margin;
   			   					if (extraShowDebug) debugMasks2[0]=gridPCorr[sensorNum][alphaIndex].clone();
   								double [] mask= addMarginsThreshold(
   										gridPCorr[sensorNum][alphaIndex], // double [] data,
   										0.0, // double threshold,
   										width,
   										height,
   										margin);
   			   					if (extraShowDebug) debugMasks1[0]=mask.clone();
   								gb.blurDouble(
   										mask,
   										width1,
   										height1,
   										sigma,
   										sigma,
   										0.01);

   								double k=1.0/(1.0-shrinkBlurComboLevel);
   								for (int i=0;i<mask.length;i++) {
   									mask[i]=k*(mask[i]-shrinkBlurComboLevel);
   									mask[i]=(mask[i]>0.0)?(mask[i]*mask[i]):0.0;
   								}
   								if (extraShowDebug) debugMasks1[1]=mask.clone();
   								gridPCorr[sensorNum][alphaIndex]=removeMargins(
   										mask, //double [] data,
   										width, // w/o margins
   										height,
   										margin); //mask; // replace with 0.0 .. 1.0 mask
   								if (extraShowDebug) debugMasks2[1]=gridPCorr[sensorNum][alphaIndex].clone();
   								if (extraShowDebug) {
   									(new showDoubleFloatArrays()).showArrays(
   											debugMasks1,
   											width1,
   											height1,
   											true,
   											"M1-"+sensorNum,
   											debugMaskTitles);
   									(new showDoubleFloatArrays()).showArrays(
   											debugMasks2,
   											width,
   											height,
   											true,
   											"M2-"+sensorNum,
   											debugMaskTitles);
   								}

   							}
   							singleSensorExtrapolationMapped(
   									sensorNum,
   									gridPCorr[sensorNum],
   									sensorMasks[sensorNum],
   									width,
   									decimation,
   									alphaThreshold,
 									step,
 									centerPXY,
   									interpolationSigma,
   									tangentialRadius, 
   									scanDistance,
   									resultDistance,
   									interpolationDegree,
   									(shrinkBlurComboSigma>0.0),
   									showDebugImages,
   									debugLevel);
   							final int numFinished=sensorFinishedAtomic.getAndIncrement();
   							SwingUtilities.invokeLater(new Runnable() {
   								public void run() {
   									if (updateStatus) IJ.showProgress(numFinished,gridPCorr.length);
   								}
   							});
   	   						if (stopRequested.get()==1){ // ASAP
   	   							interruptedAtomic.set(true);
   	   						}
   						} //if (selectedImages[numImage]){
   					} // for (int numImage=imageNumberAtomic.getAndIncrement(); ...
   				} // public void run() {
   			};
   		}
   		startAndJoin(threads);
		if (updateStatus) IJ.showProgress(0);
   		return;
	}
	public double [] addMargins(
			double [] data,
			double marginData,
			int width,
			int height,
			int margin){
		int width1= width+ 2*margin;
		int height1=height+2*margin;
		int length1=width1*height1;
		double [] result = new double [length1];
		for (int i=0;i<length1;i++) result[i] = marginData;
		int indexDest=margin*(width1+1);
		int indexSrc=0;
		for (int y=0;y<height;y++){
			for (int x=0;x<width;x++){
				result[indexDest++]=data[indexSrc++];
			}
			indexDest+=2*margin;
		}
		return result;
	}

	public double [] addMarginsThreshold(
			double [] data,
			double threshold,
			int width,
			int height,
			int margin){
		int width1= width+ 2*margin;
		int height1=height+2*margin;
		int length1=width1*height1;
		double [] result = new double [length1];
		for (int i=0;i<length1;i++) result[i] = -1.0;
		int indexDest=margin*(width1+1);
		int indexSrc=0;
		for (int y=0;y<height;y++){
			for (int x=0;x<width;x++){
				result[indexDest++]=(data[indexSrc++]>threshold)?1.0:-1.0;
			}
			indexDest+=2*margin;
		}
		return result;
	}

	public double [] removeMargins(
			double [] data,
			int width, // w/o margins
			int height,
			int margin){
		int width1= width+ 2*margin;
//		int height1=height+2*margin;
		int length=width*height;
		double [] result = new double [length];
		int indexSrc=margin*(width1+1);
		int indexDest=0;
		for (int y=0;y<height;y++){
			for (int x=0;x<width;x++){
				result[indexDest++]=data[indexSrc++];
			}
			indexSrc+=2*margin;
		}
		return result;
	}
	
	public void singleSensorExtrapolationMapped(
			int sensoNum,
			double [][] gridPCorr,
			double [] sensorMask,
			int width,
			int decimation,
			double alphaThreshold,
			double step,
			double [] centerPXY,
			double interpolationSigma, // sensor pixels
			double tangentialRadius, 
			int    scanDistance,       // sensor pixels
			int resultDistance,
			int interpolationDegree,
			boolean useAlpha, // false - sensor mask
			boolean showDebugImages,
			int debugLevel
			){
		int dxIndex=0;
		int alphaIndex=2;
		int rIndex=3;
		int height=gridPCorr[0].length/width;
		double gaussianK=-0.5/(interpolationSigma*interpolationSigma);
		double tangR0=tangentialRadius*Math.sqrt(width*height)*decimation/2; // sigma in tangential direction is interpolationSigma*(1+r/tangR0), in radial - interpolationSigma
		PolynomialApproximation polynomialApproximation =new PolynomialApproximation(0);// no debug
		int length=gridPCorr[0].length;
		DirInc dirInc= new DirInc(width,height);
		int [] iMap = new int[length];
		for (int i=0;i<length;i++) iMap[i]= (gridPCorr[alphaIndex][i]>=alphaThreshold)?1:0;
		List <Integer>waveList=new ArrayList<Integer>(1000);
		for (int index0=0;index0<length;index0++) if (iMap[index0]==0){
			for (int iDir=0;iDir<8;iDir+=2){
				int index=dirInc.newIndex(index0,iDir);
				if ((index>=0) && (iMap[index]==1)){
					iMap[index0]=2;
					waveList.add(new Integer(index0));
					break;
				}
			}
		}
// decimate the wave list
		List <Integer> seedList=new ArrayList<Integer>(1000);
		int oldIndex=0; // find better start?
		int s2= (int) Math.floor(step*step);
		while (waveList.size()>0){
			int oldX=oldIndex%width;
			int oldY=oldIndex/width;
			int bestD2=height*height+width*width;
			int nBest=-1;
			for (int n=0;n<waveList.size();n++){
				int index=waveList.get(n);
				int dx=index%width-oldX;
				int dy=index/width-oldY;
				int d2=dx*dx+dy*dy;
				if (d2<bestD2){
					bestD2=d2;
					nBest=n;
				}
			}
			oldIndex=waveList.remove(nBest);
			seedList.add(new Integer(oldIndex));
			oldX=oldIndex%width;
			oldY=oldIndex/width;
			// remove all closer than step
			for (int n=0;n<waveList.size();n++){ // size will change
				int index=waveList.get(n);
				int dx=index%width-oldX;
				int dy=index/width-oldY;
				int d2=dx*dx+dy*dy;
				if (d2<s2){
					waveList.remove(n);
				}
			}
			
		} //while (waveList.size()>0)
		// debug show waves?
		Rectangle full=new Rectangle (0,0,width,height);
		double [][] extrapolated=new double [gridPCorr.length][length];
		for (int n=0;n<extrapolated.length;n++) for (int i=0;i<extrapolated[n].length;i++) extrapolated[n][i]=0.0;
		int halfScanSize=scanDistance/decimation+1;
		int halfInterpolteSize=resultDistance/decimation+1;
		for (int n=0; n<seedList.size();n++) {
			int index0=seedList.get(n);
			int x0=index0%width;
			int y0=index0/width;
			double [] dCxy0={
					x0*decimation-centerPXY[0],
					y0*decimation-centerPXY[1]
			};
			double r0=Math.sqrt(dCxy0[0]*dCxy0[0]+dCxy0[1]*dCxy0[1]);
			final Rectangle scan =full.intersection(new Rectangle (x0-halfScanSize,y0-halfScanSize,2*halfScanSize+1,2*halfScanSize+1));
			waveList.clear();
			for (int y=scan.y;y<(scan.y+scan.height);y++) for (int x=scan.x;x<(scan.x+scan.width);x++) {
				int index=y*width+x;
				if (iMap[index]==1)	waveList.add(new Integer(index));
			}
			double [][][] data = new double [5][waveList.size()][3]; // x,y,w
			double sumWeights=0.0;
			double rScaleTangSigma=1.0/(1.0+r0/tangR0); // 
			for (int i=0;i<data[0].length;i++){
				int index=waveList.get(i);
				int x=index%width;
				int y=index/width;
				double [] dCxy={
						x*decimation-centerPXY[0],
						y*decimation-centerPXY[1]
				};
				double [] ddCxy={
						dCxy[0]-dCxy0[0],
						dCxy[1]-dCxy0[1]
				};
				double rc=Math.sqrt(dCxy[0]*dCxy[0]+dCxy[1]*dCxy[1]); // distance from lens center (in sensor pixels)
				double rDiff=rc-r0;
				double [] uRadVect={(rc>0.0)?(dCxy[0]/rc):0.0, (rc>0.0)?(dCxy[1]/rc):0.0};

				double distRad= ddCxy[0]*uRadVect[0]+ddCxy[1]*uRadVect[1]; // radial distance form the center (seed point)
				double distTan=-ddCxy[0]*uRadVect[1]+ddCxy[1]*uRadVect[0]; // tangential distance form the center (seed point)
				distTan*=rScaleTangSigma; // // for the center (seed point). was  distTan/=(1.0+rc/tangR0);  
				double w=Math.exp(gaussianK*(distRad*distRad+distTan*distTan))*gridPCorr[alphaIndex][index]; 
				sumWeights+=w;

				double dRad= gridPCorr[dxIndex+0][index]*uRadVect[0]+gridPCorr[dxIndex+1][index]*uRadVect[1]; // radial component
				double dTan=-gridPCorr[dxIndex+0][index]*uRadVect[1]+gridPCorr[dxIndex+1][index]*uRadVect[0]; // tangential component
				data[0][i][1]=dRad;
				data[1][i][1]=dTan;

				data[2][i][1]=gridPCorr[rIndex+0][index]; // R
				data[3][i][1]=gridPCorr[rIndex+1][index]; // G
				data[4][i][1]=gridPCorr[rIndex+2][index]; // B
				for (int j=0;j<data.length;j++){
					data[j][i][0]=rDiff;
					data[j][i][2]=w;
				}
			}
			sumWeights*=rScaleTangSigma; // normalize for expanded in one dimension gaussian
			double [][] poly=new double [data.length][];
			for (int j=0;j<poly.length;j++) {
				poly[j]=polynomialApproximation.polynomialApproximation1d(data[j],interpolationDegree);
			}
			if (poly[0]==null) { // all will be either null, or not - [0] testing is enough
				System.out.println("singleSensorExtrapolationMapped() BUG - poly[0]==null");
//				stageReprojPXY[index0]=null;
				continue;
			}
			final Rectangle rInterpolate =full.intersection(new Rectangle (x0-halfInterpolteSize,y0-halfInterpolteSize,2*halfInterpolteSize+1,2*halfInterpolteSize+1));
			for (int y=rInterpolate.y;y<(rInterpolate.y+rInterpolate.height);y++) for (int x=rInterpolate.x;x<(rInterpolate.x+rInterpolate.width);x++) {
				int index=y*width+x;
				double [] dCxy={
						x*decimation-centerPXY[0],
						y*decimation-centerPXY[1]
				};
				double [] ddCxy={
						dCxy[0]-dCxy0[0],
						dCxy[1]-dCxy0[1]
				};
				double rc=Math.sqrt(dCxy[0]*dCxy[0]+dCxy[1]*dCxy[1]); // distance from lens center (in sensor pixels)
				double rDiff=rc-r0;
				double [] uRadVect={(rc>0.0)?(dCxy[0]/rc):0.0, (rc>0.0)?(dCxy[1]/rc):0.0};

				double distRad= ddCxy[0]*uRadVect[0]+ddCxy[1]*uRadVect[1]; // radial distance form the center (seed point)
				double distTan=-ddCxy[0]*uRadVect[1]+ddCxy[1]*uRadVect[0]; // tangential distance form the center (seed point)
				distTan*=rScaleTangSigma; 
				double w=Math.exp(gaussianK*(distRad*distRad+distTan*distTan)); //*gridPCorr[alphaIndex][index];
				w*=sumWeights; // more points were used in coefficients calculation, more trust to that extrapolation
				// extrapolate each value using polynomial coefficients
				double [] results= new double [poly.length];
				for (int nPar=0;nPar<results.length;nPar++){
					double rN=1.0;
					results[nPar]=0.0;
					for (int dgr=0;dgr<poly[nPar].length;dgr++){
						results[nPar]+=poly[nPar][dgr]*rN;
						rN*=rDiff;
					}
				}
				// restore dX, dY from radial/tangential
				double [] diffPXY={
						results[0]*uRadVect[0]-results[1]*uRadVect[1],
						results[0]*uRadVect[1]+results[1]*uRadVect[0]};
                //accumulate
				extrapolated[dxIndex+0][index]+=diffPXY[0]*w;
				extrapolated[dxIndex+1][index]+=diffPXY[1]*w;
				extrapolated[rIndex+0][index]+=results[2]*w;
				extrapolated[rIndex+1][index]+=results[3]*w;
				extrapolated[rIndex+2][index]+=results[4]*w;
				extrapolated[alphaIndex][index]+=w;
			}
		} // for (int n=0; n<seedList.size();n++) {
		// divide by weight
		for (int index=0;index<length;index++) if (extrapolated[alphaIndex][index]>0.0){
			for (int i=0;i<extrapolated.length;i++) if (i!=alphaIndex){
				extrapolated[i][index]/=extrapolated[alphaIndex][index];
			}
		}
		// debug show here extrapolated
		if (showDebugImages){
			String [] debugTiles={"dX","dY","alpha","R","G","B","mask"};
			double [] debugMask=new double[length];
			for (int i=0;i<length;i++) debugMask[i]=iMap[i];
			for (int n=0; n<seedList.size();n++) {
				int index=seedList.get(n);
				debugMask[index]+=3.0;
			}
			//iMap[index0]
			double [][] debugData={
					extrapolated[0],
					extrapolated[1],
					extrapolated[2],
					extrapolated[3],
					extrapolated[4],
					extrapolated[5],
					debugMask};
			(new showDoubleFloatArrays()).showArrays(
					debugData,
					width,
					height,
					true,
					"EX-"+sensoNum,
					debugTiles);

		}
		// mix interpolated with original data
// double [] sensorMask,
//gridPCorr		
		for (int index=0;index<length;index++) if (extrapolated[alphaIndex][index]>0.0){
			for (int i=0;i<extrapolated.length;i++) if (i!=alphaIndex){
				double w=useAlpha?(gridPCorr[alphaIndex][index]):((gridPCorr[alphaIndex][index]>0.0)?sensorMask[index]:0.0);
				gridPCorr[i][index]=gridPCorr[i][index]*w+extrapolated[i][index]*(1.0-w);
			}
		}
	}
	
	
	
	
	
	
	public synchronized void combineImageCorrection(
			int chnNum,
			double [][][] gridPCorr,
			double [][] singleCorr
	){
		int alphaIndex=2;

		if (gridPCorr[chnNum]==null){
			gridPCorr[chnNum]=new double [singleCorr.length][singleCorr[0].length];
			for (int i=0;i<singleCorr.length;i++) for (int j=0; j<singleCorr[i].length;j++){
				gridPCorr[chnNum][i][j]=0.0;	
			}
		}
		for (int i=0;i<singleCorr.length;i++) {
			if (i==alphaIndex){
				for (int j=0; j<singleCorr[i].length;j++) gridPCorr[chnNum][i][j]+=singleCorr[i][j];
			} else {
				for (int j=0; j<singleCorr[i].length;j++) gridPCorr[chnNum][i][j]+=singleCorr[i][j]*singleCorr[alphaIndex][j];
			}
		}   								
	}

	/**
	 * Calculate sensor correction increment for geometry and photometry contributed by a single image 
	 * @param imgNum  number of image
	 * @param maxSensorMask maximal value of the sensor mask for this sensor to start extrapolating 
	 * @param minContrast minimal measured grid contrast to seed extrapolating  - to prevent expansion in the areas where this particular sensor has bad data
	 * @param minTargetAlpha - minimal alpha of the target node
	 * @param useTargetAlpha   false - only use contrast of the detected grid, true - multiply contrast by grid alpha
	 * @param showIntermediate - show intermediate data as images
	 * @param debugLevel debug level
	 * @return scan-line pixels additional correction arrays {dpX,dpY,alpha,R,G,B}[pixelIndex]
	 */
	public double [][]  singleImageCorrectionMapped(
			int imgNum, // image number
			boolean showIntermediate,
			int debugLevel
			){
		CorrectionInNodes correctionInNodes=extractNodeCorrections(
				imgNum, // image number
				showIntermediate,
				debugLevel);
		if (showIntermediate) correctionInNodes.show("finNode-"+imgNum);
		int sensorWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		int sensorHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;
		int decimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		double [][] additionalCorrection=correctionInNodes.mapToPixels(
				decimation,
				sensorWidth,
				sensorHeight,
				debugLevel);
		if (showIntermediate){
			String [] dbgTitles={"dPX","dPY","alpha","R","G","B"};
			(new showDoubleFloatArrays()).showArrays(
					additionalCorrection,
					sensorWidth/decimation,
					sensorHeight/decimation,
					true,
					"AC-"+imgNum,
					dbgTitles);
		}
		
		return additionalCorrection;
	}

	
	/**
	 * @param imgNum  number of image
	 * @param showIntermediate - show intermediate images
	 * @param debugLevel debug level
	 * @return CorrectionInNodes data correction, image and grid data for some target grid nodes
	 */
	
	public CorrectionInNodes extractNodeCorrections(
			int imgNum, // image number
			boolean showIntermediate,
			int debugLevel
			){
//		int debugThreshold=2;
    	int imgRGBIndex=   3;
		int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
		int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
		LensDistortionParameters lensDistortionParameters= setupLensDistortionParameters(
				imgNum,
				debugLevel);     // Axial - may be Double.NaN
		
		int [][] imgUV=	  fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV;
		double [][] imgXY=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY; // for each image, each grid node - a set of of {px,py,contrast,vignR,vignG,vignB} vign* is in the 0..1.0 range
		if ((imgUV==null) || (imgUV.length==0)) {
			System.out.println("expandMeasuredGrid("+imgNum+",..) empty image");
			return null;
		}
		int minU=imgUV[0][0];
		int minV=imgUV[0][1];
		int maxU=minU;
		int maxV=minV;
		for (int i=1;i<imgUV.length;i++){
			if (minU>imgUV[i][0]) minU=imgUV[i][0];
			if (minV>imgUV[i][1]) minV=imgUV[i][1];
			if (maxU<imgUV[i][0]) maxU=imgUV[i][0];
			if (maxV<imgUV[i][1]) maxV=imgUV[i][1];
		}
		int extraMargins=1;
		int [] uv0= {minU-extraMargins,minV-extraMargins}; // target U,V at the stageXYA[0]
		int width= maxU-minU+1+2*extraMargins;
		int height=maxV-minV+1+2*extraMargins;
		double [][] stagePXY= new double [width*height][]; //reprojected {px,py}
		double [][] stageDiffPXY=   new double [width*height][]; // difference between corrected measured and reprojected (to add to correction)
		double [][] stageDiffRGB=   new double [width*height][]; // difference (measured RGB)/(grid RGB) and current correction RGB (pixel sensitivity RGB)
		double [] stageMask=        new double [width*height];   // weight 
		for (int i=0;i<stagePXY.length;i++) {
			stagePXY[i]=null;
			stageDiffPXY[i]  =null;
			stageDiffRGB[i] = null;
		}
//		int vignRIndex=3; //  in measured data
		int corrRIndex=3; // in correction vector
//		int gridRIndex=3; // in reprojected vector
		double [] diff=calcYminusFx(this.currentfX, 2*this.imageStartIndex[imgNum],2*this.imageStartIndex[imgNum+1]);
		double [][] photometrics=patternParameters.getPhotometricBySensor(station,chnNum); // head/bottom grid intensity/alpha
		int targetGridWidth=getGridWidth();
		double [][] debugRGB=null;
		if (showIntermediate){
			debugRGB = new double [12][width*height];
			for (int n=0;n<debugRGB.length;n++) for (int i=0;i<debugRGB[n].length;i++) debugRGB[n][i]=0.0;
		}
		for (int i=0;i<imgUV.length;i++){
			int index=width*(imgUV[i][1]-uv0[1]) + (imgUV[i][0]-uv0[0]);
			int targetGridIndex=targetGridWidth*(imgUV[i][1]+patternParameters.V0) +(imgUV[i][0]+patternParameters.U0); // index in photometrics[][]
			int doublePairIndex=2*(this.imageStartIndex[imgNum]+i); // number of a pair in a full vector
			stageMask[index]=this.weightFunction[doublePairIndex];
			stagePXY[index]=null;
			double [] debugCorrVector=null;
			if (showIntermediate) {
				debugCorrVector=interpolateCorrectionVector ( //  vector of {corrX, corrY, alpha, flatfield_red, flatfield_green, flatfield_blue}
						chnNum,
						imgXY[i][0], //double px, measured
						imgXY[i][1]); //double py, measured);
			}
			double [] reprojectedNode= reprojectGridNode( //{pX,pY,grid mask (binary), grid R, grid G, grid B, alpha}
					lensDistortionParameters,
					imgNum,
					imgUV[i][0], //int u, // grid signed u,v
					imgUV[i][1]); //int v
			if (reprojectedNode==null) {
				continue; // out of grid - should not happen here (now - also: target point behind the camera sensor)?
			}
//			double [] reprojPXY={reprojectedNode[0],reprojectedNode[1]};
			double [] nodePXY={this.Y[doublePairIndex],this.Y[doublePairIndex+1]};	
			stagePXY[index]=nodePXY;// measured pixels Px,Py with correction applied  // reprojPXY;
//			double [] diffPXY= {imgXY[i][0]-corrVector[0]-reprojectedNode[0],imgXY[i][1]-debugCorrVector[1]-reprojectedNode[1]};
			double [] diffPXY= {diff[2*i],diff[2*i+1]};
			stageDiffPXY[index]=diffPXY;
			//{px,py,contrast,vignR,vignG,vignB}
			double [] diffRGB={0.0,0.0,0.0};
			for (int c=0;c<diffRGB.length;c++){
				double gridPhotometrics=photometrics[c][targetGridIndex];
//				if (gridPhotometrics>0.0) diffRGB[c]=imgXY[i][imgRGBIndex+c]/gridPhotometrics-debugCorrVector[corrRIndex+c];
				if (gridPhotometrics>0.0) diffRGB[c]=imgXY[i][imgRGBIndex+c]/gridPhotometrics; // don't use old correction at all!
			}
			stageDiffRGB[index]=diffRGB;
			stageMask[index]=this.weightFunction[2*(this.imageStartIndex[imgNum]+i)];
			
			if (showIntermediate) for (int c=0;c<3;c++){
				debugRGB[4*c+0][index]=photometrics[c][targetGridIndex];
				debugRGB[4*c+1][index]=imgXY[i][imgRGBIndex+c];
				debugRGB[4*c+2][index]=debugCorrVector[corrRIndex+c];
				debugRGB[4*c+3][index]=imgXY[i][imgRGBIndex+c]/photometrics[c][targetGridIndex];
			}
			
		}
		if (showIntermediate){
			double [][] debugData = new double [8][width*height];
			String [] dbgTitles={"rep-X","rep-Y","dX","dY","R","G","B","Weight"};//
			for (int i=0;i<debugData[0].length;i++){
				if (stagePXY[i]==null){
					for (int j=0;j<debugData.length;j++) {
						debugData[j][i]=Double.NaN; // 0.0?
					}
				} else {
					debugData[0][i]=stagePXY[i][0];
					debugData[1][i]=stagePXY[i][1];
					debugData[2][i]=  stageDiffPXY[i][0];
					debugData[3][i]=  stageDiffPXY[i][1];
					debugData[4][i]=  stageDiffRGB[i][0];
					debugData[5][i]=  stageDiffRGB[i][1];
					debugData[6][i]=  stageDiffRGB[i][2];
					debugData[7][i]=     stageMask[i];
				}

			}
			(new showDoubleFloatArrays()).showArrays(
					debugData,
					width,
					height,
					true,
					"PRE_EXP-"+imgNum+"-"+chnNum,
					dbgTitles);
			String [] dbgTitles1={"R-tar","R-grid","R-corr","R-FF","G-tar","G-grid","G-corr","G-FF","B-tar","B-grid","B-corr","B-FF",};//
			(new showDoubleFloatArrays()).showArrays(
					debugRGB,
					width,
					height,
					true,
					"CORR-RGB-"+imgNum+"-"+chnNum,
					dbgTitles1);

		}
		CorrectionInNodes correctionInNodes=new CorrectionInNodes(
				imgNum,
				uv0[0],
				uv0[1],
				width,
				height,
				stagePXY,
				stageDiffPXY,
				stageDiffRGB,
				stageMask
				);
		return correctionInNodes;
	}
	
	class CorrectionInNodes{
		public int numImg;
		public Rectangle uv0;
		public double [][] reprojPXY; //= new double [width*height][]; //reprojected {px,py}
		public double [][] diffPXY; //=   new double [width*height][]; // difference between corrected measured and reprojected (to add to correction)
		public double [][] diffRGB; //=   new double [width*height][]; // difference (measured RGB)/(grid RGB) and current correction RGB (pixel sensitivity RGB)
		public double []   mask;
//		public int stageMasksSensor=0, stageMasksTarget=1, stageMasksContrast=2;
		public CorrectionInNodes (
				int numImg,
				int u0,
				int v0,
				int width,
				int height,
				double [][] reprojPXY, //= new double [width*height][]; //reprojected {px,py}
				double [][] diffPXY, //=   new double [width*height][]; // difference between corrected measured and reprojected (to add to correction)
				double [][] diffRGB, //=   new double [width*height][]; // difference (measured RGB)/(grid RGB) and current correction RGB (pixel sensitivity RGB)
				double [] mask //=     new double [width*height][]; // {sensor mask, target mask, measured contrast}
		){
			this.numImg=numImg;
			this.uv0=new Rectangle(u0,v0,width,height);
			this.reprojPXY=reprojPXY; //= new double [width*height][]; //reprojected {px,py}
			this.diffPXY=diffPXY; //=   new double [width*height][]; // difference between corrected measured and reprojected (to add to correction)
			this.diffRGB=diffRGB; //=   new double [width*height][]; // difference (measured RGB)/(grid RGB) and current correction RGB (pixel sensitivity RGB)
			this.mask=mask; //=     new double [width*height][]; // {sensor mask, target mask, measured contrast}
		}

		
		public void show(
				String title
				){
				double [][] debugData = new double [8][this.uv0.width*this.uv0.height];
				String [] dbgTitles={"rep-X","rep-Y","dX","dY","R","G","B","Weight"};
				for (int i=0;i<debugData[0].length;i++){
					if (this.reprojPXY[i]==null){
						for (int j=0;j<debugData.length;j++) {
							debugData[j][i]=Double.NaN; // 0.0?
						}
					} else {
						debugData[0][i]=this.reprojPXY[i][0];
						debugData[1][i]=this.reprojPXY[i][1];
						debugData[2][i]=  this.diffPXY[i][0];
						debugData[3][i]=  this.diffPXY[i][1];
						debugData[4][i]=  this.diffRGB[i][0];
						debugData[5][i]=  this.diffRGB[i][1];
						debugData[6][i]=  this.diffRGB[i][2];
						debugData[7][i]=     this.mask[i];
					}
				}
				(new showDoubleFloatArrays()).showArrays(
						debugData,
						this.uv0.width,
						this.uv0.height,
						true,
						title,
						dbgTitles);
		}
		/**
		 * Convert correction for grid nodes (detected and extrapolated) into decimated pixel array
		 * result should be added to the current (prior) correction. Use alpha as weight when accumulating for multiple images
		 * @param decimation decimate correction pixels from sensor pixels 
		 * @param sensorWidth sensor width in pixels (2592)
		 * @param sensorHeight sensor height in pixels (1936)
		 * @param debugLevel debug level (verbose if >3)
		 * @return scan-line pixels correction arrays {dpX,dpY,alpha,R,G,B}[pixelIndex]
		 */
		public double [][] mapToPixels(
				int decimation,
				int sensorWidth,
				int sensorHeight,
				int debugLevel){
			int debugThreshold=2;
			int sWidth= (sensorWidth-1)/decimation+1; // decimated width (648)
			int sHeight=(sensorHeight-1)/decimation+1; // decimated height (484)

			int [] uvInc={0,1,this.uv0.width,this.uv0.width+1}; // four corners as vu index
			int [][] cycles={ // counter-clockwise corners bounding the area  (only orthogonal sides?)
					{1,0,2},
					{2,3,1},
					{0,2,3},
					{3,1,0}};

			double [][] thisPCorr=  new double [6][sWidth*sHeight]; // calculate for a single (this) image, accumulate in the end
			int    []   thisCounted=new    int    [sWidth*sHeight]; // some pixels accumulated twice - divide in the end
			for (int n=0;n<thisPCorr.length;n++) for (int i=0;i<thisPCorr[0].length;i++) thisPCorr[n][i]=0.0;
			for (int i=0;i<thisCounted.length;i++) thisCounted[i]=0;

			// now use imgData array to fill thisPCorr by linear interpolation
			for (int v=0;v<(this.uv0.height-1); v++) for (int u=0; u<(this.uv0.width-1);u++){
				int vu=u+this.uv0.width*v;
                double [][] cornerXY =new double[4][];
                for (int i=0;i<uvInc.length;i++){
                	int vu1=vu+uvInc[i];
                	cornerXY[i]=null;
                	if (this.reprojPXY[vu1]!=null){
                		double w=this.mask[vu1];
                		if (w>0.0) {
                			cornerXY[i]=new double[2];
                			cornerXY[i][0]=this.reprojPXY[vu1][0];
                			cornerXY[i][1]=this.reprojPXY[vu1][1];
                		}
                	}
                }
                boolean [] cycleFits=new boolean[cycles.length];  
                boolean anyFits=false;
                for (int i=0;i<cycles.length;i++){
                	cycleFits[i]=true;
                	for (int j=0;j<cycles[i].length;j++) if (cornerXY[cycles[i][j]]==null) {
                		cycleFits[i]=false;
                		break;
                	}
                	anyFits |=cycleFits[i];
                }
                if (!anyFits) continue; // not a single cycle
				if (debugLevel>debugThreshold) {
					String debugString="cycleFits ";
					for (int i =0;i<cycleFits.length; i++) debugString+=" "+cycleFits[i];
					System.out.println(debugString);
				}
                if (cycleFits[0]&&cycleFits[1]){ // remove overlaps
                	cycleFits[2]=false;
                	cycleFits[3]=false;
                }
                boolean minMaxUndefined=true;
				double minX=0,maxX=0,minY=0,maxY=0;
				// find bounding rectangle;
				for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
					int [] cycle=cycles[nCycle];
					for (int corner=0; corner<cycle.length;corner++){
						if (minMaxUndefined || (minX>cornerXY[cycle[corner]][0])) minX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (maxX<cornerXY[cycle[corner]][0])) maxX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (minY>cornerXY[cycle[corner]][1])) minY=cornerXY[cycle[corner]][1];
						if (minMaxUndefined || (maxY<cornerXY[cycle[corner]][1])) maxY=cornerXY[cycle[corner]][1];
						minMaxUndefined=false;
					}
				}
				int iMinX=(int) Math.floor(minX/decimation);
				int iMinY=(int) Math.floor(minY/decimation);
				int iMaxX=(int) Math.ceil(maxX/decimation);
				int iMaxY=(int) Math.ceil(maxY/decimation);
				// not sure if these checks are needed, got out of bounds wheriDy was =484=sHeight
				if (iMinX<0) iMinX=0;
				if (iMaxX>=sWidth) iMaxX=sWidth-1;
				if (iMinY<0) iMinY=0;
				if (iMaxY>=sHeight) iMaxY=sHeight-1;
				double [] originXY=new double [2];
				double [] endXY=new double [2];
				boolean debugHadPixels=false;
//TODO: scan X,Y in this rectangle, for points in defined squares/triangles find if the point is inside (accurate not to loose any).
				for (int idY=iMinY; idY<=iMaxY;idY++){
					
					double pY=idY*decimation; // in sensor pixels
					for (int idX=iMinX; idX<=iMaxX;idX++){
						double pX=idX*decimation; // in sensor pixels
						// scan allowed triangles, usually 2
						for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
							int [] cycle=cycles[nCycle];
							// is this point inside?
							boolean inside=true;
							for (int nEdge=0;nEdge<cycle.length;nEdge++){
								int nextNEdge=(nEdge==(cycle.length-1))?0:(nEdge+1);

								originXY[0]=this.reprojPXY[vu+uvInc[cycle[nEdge]]][0];     // imgData[2][vu+uvInc[cycle[nEdge]]];
								originXY[1]=this.reprojPXY[vu+uvInc[cycle[nEdge]]][1];     // imgData[3][vu+uvInc[cycle[nEdge]]];
								endXY[0]=   this.reprojPXY[vu+uvInc[cycle[nextNEdge]]][0]; // imgData[2][vu+uvInc[cycle[nextNEdge]]];
								endXY[1]=   this.reprojPXY[vu+uvInc[cycle[nextNEdge]]][1]; // imgData[3][vu+uvInc[cycle[nextNEdge]]];
								if (((pX-originXY[0])*(endXY[1]-originXY[1]) - (pY-originXY[1])*(endXY[0]-originXY[0]))<0.0){
									inside=false;
									break;
								}
							}
							if (!inside) continue; // point is outside of the interpolation area, try next triangle (if any)
							if (debugLevel>debugThreshold) {
								System.out.println("idX="+idX+" idY="+idY+" nCycle="+nCycle);
								String debugString1="cycle:";
								for (int i =0;i<cycle.length; i++) debugString1+=" "+cycle[i];
								System.out.println(debugString1);
							}

							/* interpolate: 
							1. taking cycles[0] as origin and two (non co-linear) edge vectors - V1:from 0 to 1 and V2 from 1 to 2
							    find a1 and a2  so that vector V  (from 0  to pXY) = a1*V1+ a2*V2
							2. if F0 is the value of the interpolated function at cycles[0], F1 and F2 - at cycles[1] and cycles2
							   then F=F0+(F1-F0)*a1 +(F2-F1)*a2    
							 */
							double [] XY0={this.reprojPXY[vu+uvInc[cycle[0]]][0],this.reprojPXY[vu+uvInc[cycle[0]]][1]};
							double [] XY1={this.reprojPXY[vu+uvInc[cycle[1]]][0],this.reprojPXY[vu+uvInc[cycle[1]]][1]};
							double [] XY2={this.reprojPXY[vu+uvInc[cycle[2]]][0],this.reprojPXY[vu+uvInc[cycle[2]]][1]};
							double [] V= {pX-XY0[0],pY-XY0[1]};
							double [][] M={
									{XY1[0]-XY0[0],XY2[0]-XY1[0]},
									{XY1[1]-XY0[1],XY2[1]-XY1[1]}};
							double det=M[0][0]*M[1][1]-M[1][0]*M[0][1];
							double [][] MInverse={
									{ M[1][1]/det,-M[0][1]/det},
									{-M[1][0]/det, M[0][0]/det}};
							double [] a12={
									MInverse[0][0]*V[0]+MInverse[0][1]*V[1],
									MInverse[1][0]*V[0]+MInverse[1][1]*V[1]};
							int pCorrIndex=idY*sWidth+idX;
// some points may be accumulated multiple times - thisPCorr[3] will take care of this
							if (debugLevel>debugThreshold) {
								System.out.println("XY0="+IJ.d2s(XY0[0],3)+":"+IJ.d2s(XY0[1],3));
								System.out.println("XY1="+IJ.d2s(XY1[0],3)+":"+IJ.d2s(XY1[1],3));
								System.out.println("XY2="+IJ.d2s(XY2[0],3)+":"+IJ.d2s(XY2[1],3));
								System.out.println("M00="+IJ.d2s(M[0][0],3)+" M01="+IJ.d2s(M[0][1],3));
								System.out.println("M10="+IJ.d2s(M[1][0],3)+" M11="+IJ.d2s(M[1][1],3));
								System.out.println("MInverse00="+IJ.d2s(MInverse[0][0],5)+" MInverse01="+IJ.d2s(MInverse[0][1],5));
								System.out.println("MInverse10="+IJ.d2s(MInverse[1][0],5)+" MInverse11="+IJ.d2s(MInverse[1][1],5));
								System.out.println("a12="+IJ.d2s(a12[0],3)+":"+IJ.d2s(a12[1],3));
								System.out.println("this.diffPXY[vu+uvInc[cycle[0]]][0]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[0]]][0],3)+
										"this.diffPXY[vu+uvInc[cycle[0]]][1]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[0]]][1],3));
								System.out.println("this.diffPXY[vu+uvInc[cycle[1]]][0]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[1]]][0],3)+
										"this.diffPXY[vu+uvInc[cycle[1]]][1]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[1]]][1],3));
								System.out.println("this.diffPXY[vu+uvInc[cycle[2]]][0]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[2]]][0],3)+
										"this.diffPXY[vu+uvInc[cycle[2]]][1]="+IJ.d2s(this.diffPXY[vu+uvInc[cycle[2]]][1],3));
							}

							double [] corr={
									 this.diffPXY[vu+uvInc[cycle[0]]][0]+ // dPx
									(this.diffPXY[vu+uvInc[cycle[1]]][0]-this.diffPXY[vu+uvInc[cycle[0]]][0])*a12[0]+
									(this.diffPXY[vu+uvInc[cycle[2]]][0]-this.diffPXY[vu+uvInc[cycle[1]]][0])*a12[1],

									 this.diffPXY[vu+uvInc[cycle[0]]][1]+ // dPy
									(this.diffPXY[vu+uvInc[cycle[1]]][1]-this.diffPXY[vu+uvInc[cycle[0]]][1])*a12[0]+
									(this.diffPXY[vu+uvInc[cycle[2]]][1]-this.diffPXY[vu+uvInc[cycle[1]]][1])*a12[1],

									 this.mask[vu+uvInc[cycle[0]]]+ // alpha
									(this.mask[vu+uvInc[cycle[1]]]-this.mask[vu+uvInc[cycle[0]]])*a12[0]+
									(this.mask[vu+uvInc[cycle[2]]]-this.mask[vu+uvInc[cycle[1]]])*a12[1],

									 this.diffRGB[vu+uvInc[cycle[0]]][0]+ // Red measured/pattern
									(this.diffRGB[vu+uvInc[cycle[1]]][0]-this.diffRGB[vu+uvInc[cycle[0]]][0])*a12[0]+
									(this.diffRGB[vu+uvInc[cycle[2]]][0]-this.diffRGB[vu+uvInc[cycle[1]]][0])*a12[1],

									 this.diffRGB[vu+uvInc[cycle[0]]][1]+ // Red measured/pattern
									(this.diffRGB[vu+uvInc[cycle[1]]][1]-this.diffRGB[vu+uvInc[cycle[0]]][1])*a12[0]+
									(this.diffRGB[vu+uvInc[cycle[2]]][1]-this.diffRGB[vu+uvInc[cycle[1]]][1])*a12[1],

									 this.diffRGB[vu+uvInc[cycle[0]]][2]+ // Red measured/pattern
									(this.diffRGB[vu+uvInc[cycle[1]]][2]-this.diffRGB[vu+uvInc[cycle[0]]][2])*a12[0]+
									(this.diffRGB[vu+uvInc[cycle[2]]][2]-this.diffRGB[vu+uvInc[cycle[1]]][2])*a12[1]};
							if (debugLevel>debugThreshold) {
								System.out.println("corr="+IJ.d2s(corr[0],3)+" "+IJ.d2s(corr[1],3)+" "+IJ.d2s(corr[2],3));
							}
 if (pCorrIndex>thisPCorr[0].length) {
//	 System.out.println("imgNum=" + imgNum+": "+	fittingStrategy.distortionCalibrationData.gIP[imgNum].path);
	 System.out.println("thisPCorr[0].length="+thisPCorr[0].length+" pCorrIndex="+pCorrIndex+" sWidth="+sWidth+" idY="+idY+" idX="+idX);
 }
                            for (int i=0;i<corr.length;i++) {
                            	thisPCorr[i][pCorrIndex]+= corr[i]; // OOB: -8, -1433
                            }
							thisCounted[pCorrIndex]++;

							if (debugLevel>debugThreshold) {
								debugHadPixels=true;
							}
						}
					} // idX
					// use same order in calculations, make sure no gaps
				} // idY
				if ((debugLevel>debugThreshold) && (debugHadPixels)){
//					if (!debugExit) {
						System.out.println(
								" minX="+IJ.d2s(minX,1)+
								" maxX="+IJ.d2s(maxX,1));
						System.out.println(
								" minY="+IJ.d2s(minY,1)+
								" maxY="+IJ.d2s(maxY,1));
						System.out.println(
								" iMinX="+iMinX+
								" iMaxX="+iMaxX);
						System.out.println(
								" iMinY="+iMinY+
								" iMaxY="+iMaxY);
//					}
//					if (!debugExit) debugCntr--;
//					if (debugCntr==0) debugExit=true; // exit after first non-empty tile
					
				}
			} //for (int v=0;v<(this.uv0.height-1); v++) for (int u=0; u<(this.uv0.width-1);u++){
            for (int i=0;i<thisCounted.length;i++) if (thisCounted[i]>1) {
            	for (int j=0;j<thisPCorr[i].length;j++)	thisPCorr[j][i]/= thisCounted[i];
            }
            return thisPCorr;
		}
	}
	 
	class DirInc{
		private int top=   1 | 2 | 4 | 8 | 16;
		private int bottom=1 |             16 | 32 | 64 | 128;
		private int left=  1 | 2 |                   64 | 128;
		private int right=         4 | 8 | 16 | 32 | 64;
		private int [] inc=null;
		private int [] validDirs=null;
		private double [][] unityVector=null;
		public int dirs=8;
		public DirInc(int width, int height){
//			int [] dirs8={1,1+width,width,-1+width,-1,-1-width,-width,1-width};
			int [][] incXY8={{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
			this.inc=new int [incXY8.length];
			this.unityVector=new double [incXY8.length][2];
			for (int i=0;i<incXY8.length;i++){
				this.inc[i]=incXY8[i][0]+width*incXY8[i][1];
				double len=Math.sqrt(incXY8[i][0]*incXY8[i][0]+incXY8[i][1]*incXY8[i][1]);
				this.unityVector[i][0]=incXY8[i][0]/len;
				this.unityVector[i][1]=incXY8[i][1]/len;
			}
//			this.inc=dirs8;
			this.validDirs=new int [width*height];
			for (int i=0;i<this.validDirs.length;i++) this.validDirs[i]=0xff;
			for (int i=0;i<width;i++){
				this.validDirs[                 i]&=top;
				this.validDirs[(height-1)*width+i]&=bottom;
			}
			for (int i=0;i<height;i++){
				this.validDirs[i*width]&=left;
				this.validDirs[i*width + width-1]&=right;
			}
			
		}
		public int newIndex(int oldIndex, int dir){
			if ((validDirs[oldIndex] & (1<<dir))==0) return -1; // invalid dir for this location (border)
			return oldIndex+this.inc[dir];
		}
		public double [] unity(int dir) {
			return this.unityVector[(dir+this.unityVector.length)%this.unityVector.length];
		}
	}

	public class PixXYUV{
		double [][]xy=null;
		int [][]uv=null;
		double [] alpha=null;
		double [][]dxy=null;
		public PixXYUV(){}
		public PixXYUV(int len){
			this.uv=new int [len][2];	
			this.xy=new double [len][2];	
			this.alpha=new double [len];	
			this.dxy=new double [len][2];	
		}
	}
	
	
	
	
	/**
	 * Interpolate (bi-linear) X/Y corrections and flat-field data for the sensor
	 * @param chnNum - sensor (channel) number
	 * @param px     - pixel X coordinate (non-decimated)
	 * @param py     - pixel Y coordinate (non-decimated)
	 * @return       - vector of {corrX, corrY, alpha, flatfield_red, flatfield_green, flatfield_blue}
	 */
	public double [] interpolateCorrectionVector (
			int chnNum,
			double px,
			double py){
		if (this.pixelCorrection==null){
			double [] vector={0.0,0.0,1.0,1.0,1.0,1.0};
			return vector;
		}
		this.pixelCorrectionDecimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		this.pixelCorrectionWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		this.pixelCorrectionHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;

		int sensorCorrWidth= (this.pixelCorrectionWidth-1)/this.pixelCorrectionDecimation+1;
		int sensorCorrHeight=this.pixelCorrection[chnNum][0].length/sensorCorrWidth;
		int [] ix={(int) Math.floor(px/this.pixelCorrectionDecimation), (int) Math.floor(px/this.pixelCorrectionDecimation)+1};
		int [] iy={(int) Math.floor(py/this.pixelCorrectionDecimation),(int) Math.floor(py/this.pixelCorrectionDecimation)+1};
		for (int i=0;i<2;i++){
			if (ix[i]<0) ix[i]=0;
			else if (ix[i]>=sensorCorrWidth) ix[i]=sensorCorrWidth-1;
			if (iy[i]<0) iy[i]=0;
			else if (iy[i]>=sensorCorrHeight) iy[i]=sensorCorrHeight-1;
		}
		int index00=ix[0] + iy[0]*sensorCorrWidth;
		int indexX0=ix[1] + iy[0]*sensorCorrWidth;
		int index0Y=ix[0] + iy[1]*sensorCorrWidth;
		int indexXY=ix[1] + iy[1]*sensorCorrWidth;

		double corrDX=0,corrDY=0;
		if ((px>ix[0])&& (px<ix[1])) corrDX=px-ix[0];
		if ((py>iy[0])&& (py<iy[1])) corrDY=py-iy[0];
		double [] vector=new double [this.pixelCorrection[chnNum].length];
		for (int n=0;n<vector.length;n++){
			// bilinear interpolation			
			vector[n]=
				(1-corrDX)* (1-corrDY)* this.pixelCorrection[chnNum][n][index00]+
				corrDX * (1-corrDY)* this.pixelCorrection[chnNum][n][indexX0]+
				(1-corrDX)*    corrDY * this.pixelCorrection[chnNum][n][index0Y]+
				corrDX *    corrDY * this.pixelCorrection[chnNum][n][indexXY];
		}
		return vector;
	}
	/**
	 * Bilinear interpolate sensor mask array
	 * @param mask decimated mask data
	 * @param px     - pixel X coordinate (non-decimated)
	 * @param py     - pixel Y coordinate (non-decimated)
	 * @return interpolated mask data at specified fractional pixel
	 */
	public double interpolateMask (
			double [] mask,
			double px,
			double py){
		this.pixelCorrectionDecimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		this.pixelCorrectionWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		this.pixelCorrectionHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;

		int sensorCorrWidth= (this.pixelCorrectionWidth-1)/this.pixelCorrectionDecimation+1;
		int sensorCorrHeight=mask.length/sensorCorrWidth;
		int [] ix={(int) Math.floor(px/this.pixelCorrectionDecimation), (int) Math.floor(px/this.pixelCorrectionDecimation)+1};
		int [] iy={(int) Math.floor(py/this.pixelCorrectionDecimation),(int) Math.floor(py/this.pixelCorrectionDecimation)+1};
		for (int i=0;i<2;i++){
			if (ix[i]<0) ix[i]=0;
			else if (ix[i]>=sensorCorrWidth) ix[i]=sensorCorrWidth-1;
			if (iy[i]<0) iy[i]=0;
			else if (iy[i]>=sensorCorrHeight) iy[i]=sensorCorrHeight-1;
		}
		int index00=ix[0] + iy[0]*sensorCorrWidth;
		int indexX0=ix[1] + iy[0]*sensorCorrWidth;
		int index0Y=ix[0] + iy[1]*sensorCorrWidth;
		int indexXY=ix[1] + iy[1]*sensorCorrWidth;

		double corrDX=0,corrDY=0;
		if ((px>ix[0])&& (px<ix[1])) corrDX=px-ix[0];
		if ((py>iy[0])&& (py<iy[1])) corrDY=py-iy[0];
		double result=
				(1-corrDX)* (1-corrDY)* mask[index00]+
				corrDX * (1-corrDY)* mask[indexX0]+
				(1-corrDX)*    corrDY * mask[index0Y]+
				corrDX *    corrDY * mask[indexXY];
		return result;
	}
	
	
/**
 *   after fitting finished and accepted - 	fittingStrategy.saveSeriesVector(double [] vector)	
 */
	public void saveFittingSeries() {
		fittingStrategy.saveSeriesVector(this.currentVector);
	}
	/*
	 * For each image in the series:

    	public double [] fittingStrategy.getImageParametersVector(int numImg, double [] parameterVector);
    	 * Calculates current values of all parameters for the particular sensor - some ("fixed")
    	 * are taken from the data stored for this individual image, others - from the parameter
    	 * vector (used in fitting)
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return vector used for the current image (parameters influencing the acquired grid
    	 * on the sensor (common parameters and those of the sensor's subchannel)

   public void calcInterParamers(
    		double [] parVect,
    		boolean [] mask, // calculate only selected derivatives (all parVect values are still
    		boolean calculateDerivatives // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
    		){
     * Calculate/set  this.lensDistortionParameters and this.interParameterDerivatives
     * @param parVect 21-element vector for eyesis sub-camera, including common and individual parameters
     * @param mask -mask - which partial derivatives are needed to be calculated (others will be null)
     * @param calculateDerivatives calculate array of partial derivatives, if false - just the values


For each point in the image
      public double [][] lensDistortionParameters.reorderPartialDerivatives (double [][] srcDerivatives){
      double [][] lensDistortionParameters.calcPartialDerivatives(
        		double xp, // target point horizontal, positive - right,  mm
        		double yp, // target point vertical,   positive - down,  mm
        		double zp, // target point horizontal, positive - away from camera,  mm
        		boolean calculateAll){ // calculate derivatives, false - values only

    public double [][] interParameterDerivatives=null; //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21)
    public double []   currentVector; // current variable parameter vector
    public double []   Y=null; // array of "y" - for each grid image, each defined grid node - 2 elements
    public double [][] targetXYZ=null; // array of target {x,y,z} matching each image each grid point 
    public double []   fX=null; // array of "f(x)" - simulated data for all images, combining pixel-X and pixel-Y (odd/even)
    public double [][] jacobian=null; // partial derivatives of fX (above) by parameters to be adjusted (rows)
	 */
	
	public ImagePlus simulatePatternOnSensor(
			int stationNumber,
			int subCam,
			double goniometerTilt,
			double goniometerAxial,
			SimulationPattern.SimulParameters simulParametersDefault,
			int threadsMax,
			boolean updateStatus,
			int mspDebugLevel,
			int global_debug_level, // DEBUG_LEVEL
			int debug_level // debug level used inside loops
	){
		MatchSimulatedPattern matchSimulatedPattern = new MatchSimulatedPattern(64); // new instance, all reset, FFTSize=64 will not be used
		matchSimulatedPattern.debugLevel = mspDebugLevel;
		//		MatchSimulatedPattern.DistortionParameters distortionParameters = modifyDistortionParameters();
		//		SimulationPattern.SimulParameters simulParameters = modifySimulParameters();
		int sensorWidth=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorWidth(subCam);
		int sensorHeight=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorHeight(subCam);

		double [][][] hintGrid=estimateGridOnSensor(
				stationNumber,
				subCam,
				goniometerTilt, // Tilt, goniometerHorizontal
				goniometerAxial,  // Axial,goniometerAxial
				-1 // use camera parameters, not imageSet
		);
		if (hintGrid==null){
			String msg="Grid is not visible for subcamera="+subCam+",  tilt="+goniometerTilt+", axial="+goniometerAxial;
			IJ.showMessage("Error",msg);
			System.out.println("Error: "+msg);
			return null;
		}
		if (global_debug_level>1){
			double [][] pixels=new double[4][hintGrid.length*hintGrid[0].length];
			int index=0;
			String [] titles={"pixel-X","pixel-Y","grid-U","grid-V"};
			for (int v=0; v<hintGrid.length;v++) for (int u=0;u<hintGrid[v].length;u++){
				if (hintGrid[v][u]!=null){
					for (int i=0; i<4;i++)	pixels[i][index]=hintGrid[v][u][i];
				} else {
					for (int i=0; i<4;i++)	pixels[i][index]=-1;
				}
				index++;
			}
			(new showDoubleFloatArrays()).showArrays(pixels, hintGrid[0].length, hintGrid.length,  true, "hintGrid", titles);
			
		}

		if (global_debug_level>0){
			System.out.println("simulatePatternOnSensor(): subcamera="+subCam+",  tilt="+goniometerTilt+", axial="+goniometerAxial);
		}
		int numCells=matchSimulatedPattern.restoreSimulatedPatternGridFromHint(hintGrid, sensorWidth, sensorHeight);
		matchSimulatedPattern.recalculateWaveVectors (
				   updateStatus,
				   debug_level);// debug level used inside loops

		if (global_debug_level>0){
			System.out.println("simulatePatternOnSensor(): "+numCells+" grid cells");
		}
		SimulationPattern.SimulParameters simulParameters = simulParametersDefault.clone();
		SimulationPattern simulationPattern=new SimulationPattern(simulParameters);
		double [][] xy0={{simulParameters.offsetX,simulParameters.offsetY},{simulParameters.offsetX-0.5,simulParameters.offsetY-0.5}} ;
// TODO: add marks for the laser pointers when visible?
		float[] simPixels=simulationPattern.simulateGrid (
				matchSimulatedPattern.getDArray(),
				2, // gridFrac, // number of grid steps per pattern full period
				simulParameters,
				matchSimulatedPattern.getWOI(),
				1, // simulParameters.subdiv/2,
				xy0[0],    // add to patternGrid xy
				threadsMax,
				updateStatus,
				(debug_level>1)?1:0); //debug_level); // debug level
		if (global_debug_level>0){
			System.out.println("simulatePatternOnSensor(): simPixels.length="+simPixels.length+" sensorWidth="+sensorWidth+" sensorHeight="+sensorHeight);
		}
		for (int i=0;i<simPixels.length;i++) simPixels[i]*=255.0;
		ImageProcessor ip_simGrid = new FloatProcessor(sensorWidth, sensorHeight);
		ip_simGrid.setPixels(simPixels);
		ip_simGrid.resetMinAndMax();
		ImagePlus imp_simGrid= new ImagePlus("Simulated_Grid_CHN"+subCam+"_TILT"+goniometerTilt+"_AXIAL"+goniometerAxial, ip_simGrid);
		return imp_simGrid;
	}

//TODO: add additional parameter - process all, but with matched pointers less than 2	
	public int applyHintedGrids(
			MatchSimulatedPattern.LaserPointer laserPointer, // LaserPointer object that specifies actual laser pointers on the target
			boolean removeOutOfGridPointers,
			double  hintGridTolerance, // allowed mismatch (fraction of period) or 0 - orientation only
			boolean processAll, // if true - process all images, false - only disabled
			boolean ignoreLaserPointers, // ignore laser pointers, rely on hints only
			boolean processBlind, // try to match without known orientation and no laser pointers
			int     imageNumber, // <0 - all, >=0 only this image
			boolean useSetData,
			int threadsMax,
			boolean updateStatus,
			int mspDebugLevel,
			int global_debug_level, // DEBUG_LEVEL
			int debug_level // debug level used inside loops
	){
		int debugThreshold0=0;
		int debugThreshold=2;
		MatchSimulatedPattern matchSimulatedPattern = new MatchSimulatedPattern(64); // new instance, all reset, FFTSize=64 will not be used
		// next 2 lines are not needed for the new instance, but can be
		// used alternatively if keeping it
		//		matchSimulatedPattern.invalidateFlatFieldForGrid(); // Reset Flat Filed calibration - different image.
		//		matchSimulatedPattern.invalidateFocusMask();
		matchSimulatedPattern.debugLevel = mspDebugLevel;
		//		ImagePlus imp_eq = matchSimulatedPattern.applyFlatField(images[nImg]); // current image with grid flat-field  correction

		//		if (debug_level > 0){
		//			System.out.println("\n   ======= Looking for grid, matching pointers in image " +images[nImg].getTitle()+
		//					", initial number of pointers was "+numPointers);
		//		}
		//matchSimulatedPatterns[numSensor].getChannel(images[numSensor])+" ");
		//		MatchSimulatedPattern.DistortionParameters distortionParameters = modifyDistortionParameters();
		//		SimulationPattern.SimulParameters simulParameters = modifySimulParameters();

		boolean noMessageBoxes=true;
		double [] xy0={0.0,0.0} ; //(old) debug only
		int numSuccess=0;
		DistortionCalibrationData dcd=fittingStrategy.distortionCalibrationData;
		for (int numGridImage=0;numGridImage<dcd.gIP.length;numGridImage++)
			if (((imageNumber<0) || (imageNumber==numGridImage)) &&(processAll ||
					(!dcd.gIP[numGridImage].enabled &&
							((hintGridTolerance>0.0)||((dcd.gIP[numGridImage].matchedPointers>0)) && !ignoreLaserPointers)))){ // skip no-pointers if only orientation is hinted
				if (((dcd.gIP[numGridImage].matchedPointers==0) || ignoreLaserPointers)&&
						(dcd.gIS[dcd.get_gIS_index(numGridImage)].orientationEstimated)) {
					if ( !processBlind) {
						if (this.debugLevel>0) {
							System.out.println("\n**** Orientation is not known exactly for image # "+numGridImage+" - "+dcd.gIP[numGridImage].path+
							", and there are no laser pointer references (processBlind==false) - skipping");
						}
						continue;
					} else {
						if (this.debugLevel>0) {
							System.out.println("\n**** Orientation is not known exactly for image # "+numGridImage+" - "+dcd.gIP[numGridImage].path+
							", and there are no laser pointer references, but processBlind is enabled, proceeding");
						}
					}
				}
				if (this.debugLevel>debugThreshold0) {
					System.out.println("\n---- applyHintedGrids() image #"+numGridImage+" (imageNumber="+imageNumber+") "+
							" dcd.gIP["+numGridImage+"].pixelsXY.length="+dcd.gIP[numGridImage].pixelsXY.length+
							" dcd.gIP["+numGridImage+"].pixelsXY_extra.length="+dcd.gIP[numGridImage].pixelsXY_extra.length+
							" grid period="+dcd.gIP[numGridImage].gridPeriod
							);
					if (this.debugLevel>(debugThreshold)){
						for (int i=0;i<dcd.gIP[numGridImage].pixelsXY.length;i++){
							System.out.println(i+": dcd.gIP["+numGridImage+"].pixelsXY={"+dcd.gIP[numGridImage].pixelsXY[i][0]+
									","+dcd.gIP[numGridImage].pixelsXY[i][1]+"}"+
									" uv={"+dcd.gIP[numGridImage].pixelsUV[i][0]+
									","+dcd.gIP[numGridImage].pixelsUV[i][1]+"}");
						}
						for (int i=0;i<dcd.gIP[numGridImage].pixelsXY_extra.length;i++){
							System.out.println(i+": dcd.gIP["+numGridImage+"].pixelsXY_extra={"+dcd.gIP[numGridImage].pixelsXY_extra[i][0]+
									","+dcd.gIP[numGridImage].pixelsXY_extra[i][1]+"}"+
									" uv={"+dcd.gIP[numGridImage].pixelsUV_extra[i][0]+
									","+dcd.gIP[numGridImage].pixelsUV_extra[i][1]+"}");
						}
					}
				}
				
				double [][][] pixelsXYSet={
							dcd.gIP[numGridImage].pixelsXY,
							dcd.gIP[numGridImage].pixelsXY_extra};
				int   [][][] pixelsUVSet={
						dcd.gIP[numGridImage].pixelsUV,
						dcd.gIP[numGridImage].pixelsUV_extra};

				matchSimulatedPattern.restorePatternGridFromGridList( 
						pixelsXYSet, //double [][] pixelsXY,
						pixelsUVSet, // int [][] pixelsUV,
						dcd.gIP[numGridImage].intensityRange
				); // width and height will be calculated from maximal of pixelsXY
				boolean OK=matchSimulatedPattern.createUV_INDEX( /// **** fails here
						null, //imp, // or null - just to determine WOI (when getWOI matches image size) 
						xy0, // add to patterGrid xy, null OK
						threadsMax,
						updateStatus,
						global_debug_level, // DEBUG_LEVEL
						debug_level); // debug level used inside loops
				if (!OK) {
					System.out.println("++++++ BUG: in applyHintedGrids() failed in createUV_INDEX()");
					continue;
				}

				//				   int numPointers=(laserPointer!=null)?laserPointer.laserUVMap.length:0;
				//				   double [][] pointersXY=(numPointers>0)?getPointersXY(imp, numPointers):null;
				double [] goniometerTiltAxial=dcd.getImagesetTiltAxial(numGridImage);
				if ((goniometerTiltAxial==null) || Double.isNaN(goniometerTiltAxial[0])  || Double.isNaN(goniometerTiltAxial[1])){
					if (this.debugLevel>0) {
						System.out.println("No goniometer orientation is available for image # "+numGridImage+" - "+dcd.gIP[numGridImage].path);
					}
				} else {
					int station=dcd.getImageStation(numGridImage);
					int setNumber=dcd.gIP[numGridImage].setNumber;
					double [][][] hintGrid=estimateGridOnSensor(
							station, // station number
							dcd.gIP[numGridImage].channel,
							goniometerTiltAxial[0], // Tilt, goniometerHorizontal
							goniometerTiltAxial[1],  // Axial,goniometerAxial
							setNumber // -1 or specific image set
					);
					if (global_debug_level>0){
						System.out.println("\n**** applyHintedGrids(): processing grid image # "+numGridImage+", path="+dcd.gIP[numGridImage].path);
					}
					if (hintGrid==null){
						if (global_debug_level>0){
							System.out.println("estimateGridOnSensor() failed - skipping");
						}
						dcd.gIP[numGridImage].hintedMatch =0;
						continue;
					}
					int rslt= matchSimulatedPattern.combineGridCalibration(
							laserPointer, // LaserPointer object or null
							ignoreLaserPointers?null:dcd.gIP[numGridImage].laserPixelCoordinates, //pointersXY,
							removeOutOfGridPointers, //
							hintGrid, // predicted grid array (or null)
							hintGridTolerance, // allowed mismatch (fraction of period) or 0 - orientation only
							global_debug_level, // DEBUG_LEVEL
							noMessageBoxes );
					if (global_debug_level>0){
						System.out.println("applyHintedGrids(): rslt="+rslt);
					}
					if (rslt<0) { // failed hinting
						dcd.gIP[numGridImage].hintedMatch =0;
					} else {
// re-create pixelsXY, pixelsXY_extra, pixelsUV, pixelsUV_extra	
		            	int size=0;
		            	int size_extra=0;
/*	            		System.out.println("numGridImage="+numGridImage+" matchSimulatedPattern.getHeight()="+matchSimulatedPattern.getHeight()+
	            				" matchSimulatedPattern.getWidth()="+matchSimulatedPattern.getWidth()+
	            				" matchSimulatedPattern.targetUV is "+((matchSimulatedPattern.targetUV==null)?"null":"not null")+
	            				" matchSimulatedPattern.pixelsUV is "+((matchSimulatedPattern.pixelsUV==null)?"null":"not null")
	            				);
	            		System.out.println(
	            				" matchSimulatedPattern.targetUV[0] is "+((matchSimulatedPattern.targetUV[0]==null)?"null":"not null")+
	            				" matchSimulatedPattern.pixelsUV[0] is "+((matchSimulatedPattern.pixelsUV[0]==null)?"null":"not null")
	            				);*/
		            	for (int v=0;v<matchSimulatedPattern.getHeight();v++) for (int u=0;u<matchSimulatedPattern.getWidth();u++) {
/*		            		System.out.println("v="+v+", u="+u);
		            		System.out.println(" matchSimulatedPattern.targetUV[v][u] is "+((matchSimulatedPattern.targetUV[v][u]==null)?"null":"not null"));
		            		System.out.println(" matchSimulatedPattern.pixelsUV[v][u] is "+((matchSimulatedPattern.pixelsUV[v][u]==null)?"null":"not null"));*/
		            		if ((matchSimulatedPattern.targetUV[v][u]!=null) && (matchSimulatedPattern.pixelsUV [v][u]!=null)){

		            			if ((matchSimulatedPattern.targetUV[v][u]!=null) && (matchSimulatedPattern.pixelsUV [v][u]!=null) &&
		            					(matchSimulatedPattern.pixelsUV[v][u][0]>=0.0) || (matchSimulatedPattern.pixelsUV[v][u][1]>=0.0)) { // disregard negative sensor pixels
//				            		System.out.println(" matchSimulatedPattern.targetUV[v][u] is "+((matchSimulatedPattern.targetUV[v][u]==null)?"null":"not null"));
//				            		System.out.println(" matchSimulatedPattern.targetUV[v][u][0]= "+matchSimulatedPattern.targetUV[v][u][0]);
//				            		System.out.println(" matchSimulatedPattern.targetUV[v][u][1]= "+matchSimulatedPattern.targetUV[v][u][1]); //********
//				            		System.out.println(" patternParameters is "+((patternParameters==null)?"null":"not null"));
//				            		int tu=matchSimulatedPattern.targetUV[v][u][0];
//				            		int tv=matchSimulatedPattern.targetUV[v][u][1];
//
				            		if (patternParameters.getXYZM(matchSimulatedPattern.targetUV[v][u][0],matchSimulatedPattern.targetUV[v][u][1],false,station)!=null) {
		            					size++;
		            				} else {
		            					size_extra++;
		            				}
	            			}
		            		}
		            	}
		            	dcd.gIP[numGridImage].resetMask();
		            	dcd.gIP[numGridImage].pixelsXY=new double [size][6];
		            	dcd.gIP[numGridImage].pixelsUV=new int    [size][2];
		            	dcd.gIP[numGridImage].pixelsXY_extra=new double [size_extra][6];
		            	dcd.gIP[numGridImage].pixelsUV_extra=new int    [size_extra][2];
		            	int index=0;
		            	int index_extra=0;
		            	for (int v=0;v<matchSimulatedPattern.getHeight();v++) for (int u=0;u<matchSimulatedPattern.getWidth();u++) {
/*		            		System.out.println("+ v="+v+", u="+u);
		            		System.out.println(" + matchSimulatedPattern.targetUV[v][u] is "+((matchSimulatedPattern.targetUV[v][u]==null)?"null":"not null"));
		            		System.out.println(" + matchSimulatedPattern.pixelsUV[v][u] is "+((matchSimulatedPattern.pixelsUV[v][u]==null)?"null":"not null"));*/
		            		if ((matchSimulatedPattern.targetUV[v][u]!=null) &&(matchSimulatedPattern.pixelsUV[v][u]!=null) ) {
//			            		System.out.println("++ v="+v+", u="+u+" index="+index+" ("+size+"), index_extra="+index_extra+" ("+size_extra+")");
		            		
		            			if ((matchSimulatedPattern.targetUV[v][u]!=null) &&(matchSimulatedPattern.pixelsUV[v][u]!=null) &&
		            					(matchSimulatedPattern.pixelsUV[v][u][0]>=0.0) || (matchSimulatedPattern.pixelsUV[v][u][1]>=0.0)) { // disregard negative sensor pixels
		            				if (
		            						(v>=matchSimulatedPattern.gridContrastBrightness[0].length) ||
		            						(u>=matchSimulatedPattern.gridContrastBrightness[0][0].length)){
		            					System.out.println(
		            							" matchSimulatedPattern.gridContrastBrightness[0].length="+matchSimulatedPattern.gridContrastBrightness[0].length+
		            							" matchSimulatedPattern.gridContrastBrightness[0][0].length="+matchSimulatedPattern.gridContrastBrightness[0][0].length+
		            							" v="+v+" u="+u);
		            				}
		            			}

		            			if (patternParameters.getXYZM(matchSimulatedPattern.targetUV[v][u][0],matchSimulatedPattern.targetUV[v][u][1],false,station)!=null) {
		            				dcd.gIP[numGridImage].pixelsXY[index][0]=matchSimulatedPattern.pixelsUV[v][u][0];
		            				dcd.gIP[numGridImage].pixelsXY[index][1]=matchSimulatedPattern.pixelsUV[v][u][1];
		            				dcd.gIP[numGridImage].pixelsUV[index][0]=matchSimulatedPattern.targetUV[v][u][0];
		            				dcd.gIP[numGridImage].pixelsUV[index][1]=matchSimulatedPattern.targetUV[v][u][1];
		            				dcd.gIP[numGridImage].pixelsXY[index][2]=matchSimulatedPattern.gridContrastBrightness[0][v][u]; // grid contrast
		            				dcd.gIP[numGridImage].pixelsXY[index][3]=matchSimulatedPattern.gridContrastBrightness[1][v][u]/dcd.gIP[numGridImage].intensityRange[0]; // red
		            				dcd.gIP[numGridImage].pixelsXY[index][4]=matchSimulatedPattern.gridContrastBrightness[2][v][u]/dcd.gIP[numGridImage].intensityRange[1]; // green
		            				dcd.gIP[numGridImage].pixelsXY[index][5]=matchSimulatedPattern.gridContrastBrightness[3][v][u]/dcd.gIP[numGridImage].intensityRange[2]; // blue
		            				index++;
		            			} else {
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][0]=matchSimulatedPattern.pixelsUV[v][u][0];
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][1]=matchSimulatedPattern.pixelsUV[v][u][1];
		            				dcd.gIP[numGridImage].pixelsUV_extra[index_extra][0]=matchSimulatedPattern.targetUV[v][u][0];
		            				dcd.gIP[numGridImage].pixelsUV_extra[index_extra][1]=matchSimulatedPattern.targetUV[v][u][1];
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][2]=matchSimulatedPattern.gridContrastBrightness[0][v][u]; // grid contrast
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][3]=matchSimulatedPattern.gridContrastBrightness[1][v][u]/dcd.gIP[numGridImage].intensityRange[0]; // red
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][4]=matchSimulatedPattern.gridContrastBrightness[2][v][u]/dcd.gIP[numGridImage].intensityRange[1]; // green
		            				dcd.gIP[numGridImage].pixelsXY_extra[index_extra][5]=matchSimulatedPattern.gridContrastBrightness[3][v][u]/dcd.gIP[numGridImage].intensityRange[2]; // blue
		            				index_extra++;
		            			}
		            		}
		            	}
		            	dcd.gIP[numGridImage].hintedMatch =(hintGridTolerance>0.0)?2:1; // orientation or both orientation and translation
						dcd.gIP[numGridImage].matchedPointers=rslt; // update number of matched pointers
						if ((dcd.gIP[numGridImage].hintedMatch>1) || (dcd.gIP[numGridImage].matchedPointers>0)) numSuccess++;
						System.out.println("applyHintedGrids(): dcd.gIP["+numGridImage+"].hintedMatch="+dcd.gIP[numGridImage].hintedMatch+
								" dcd.gIP["+numGridImage+"].matchedPointers="+dcd.gIP[numGridImage].matchedPointers+ " points:"+index+" extra points:"+index_extra);
					}
				}
			}
		return numSuccess;
	}
	
	public void showGridImage(int numGridImage){
		DistortionCalibrationData.GridImageParameters grid=fittingStrategy.distortionCalibrationData.gIP[numGridImage];
/*
    		public double [][] pixelsXY=  null; // for each image, each grid node - a pair of {px,py}
    		public int    [][] pixelsUV=  null; // for each image, each grid node - a pair of {gridU, gridV}
    		public double [][] pixelsXY_extra=  null; // extra data, for nodes that are out of the physical grid (may be needed after re-calibration)
    		public int    [][] pixelsUV_extra=  null; 
		
*/
		boolean valid=false;
		int minU=0,maxU=0,minV=0,maxV=0;
		for (int i=0;i<grid.pixelsUV.length;i++){
			if (!valid){
				minU=grid.pixelsUV[i][0];
				minV=grid.pixelsUV[i][1];
				maxU=minU;
				maxV=minV;
				valid=true;
			} else {
				if (minU>grid.pixelsUV[i][0]) minU=grid.pixelsUV[i][0];
				if (minV>grid.pixelsUV[i][1]) minV=grid.pixelsUV[i][1];
				if (maxU<grid.pixelsUV[i][0]) maxU=grid.pixelsUV[i][0];
				if (maxV<grid.pixelsUV[i][1]) maxV=grid.pixelsUV[i][1];
			}
		}
		for (int i=0;i<grid.pixelsUV_extra.length;i++){
			if (!valid){
				minU=grid.pixelsUV_extra[i][0];
				minV=grid.pixelsUV_extra[i][1];
				maxU=minU;
				maxV=minV;
				valid=true;
			} else {
				if (minU>grid.pixelsUV_extra[i][0]) minU=grid.pixelsUV_extra[i][0];
				if (minV>grid.pixelsUV_extra[i][1]) minV=grid.pixelsUV_extra[i][1];
				if (maxU<grid.pixelsUV_extra[i][0]) maxU=grid.pixelsUV_extra[i][0];
				if (maxV<grid.pixelsUV_extra[i][1]) maxV=grid.pixelsUV_extra[i][1];
			}
		}
		String [] titles={"X","Y","U","V","valid","extra"};
		int height=maxV-minV+1;
		int width= maxU-minU+1;
//		System.out.println("showGridImage(): minU="+minU+" maxU="+maxU+" minV="+minV+" maxV="+maxV+" width="+width+" height="+height);
//		System.out.println("showGridImage(): grid.pixelsXY.length="+grid.pixelsXY.length+" grid.pixelsXY.length="+grid.pixelsXY.length);
		double [][] pixels=new double [titles.length][width*height];
		for (int i=0;i<pixels[0].length;i++) {
			pixels[0][i]=-1.0; // x
			pixels[1][i]=-1.0; // y
			pixels[2][i]= 0.0; // u
			pixels[3][i]= 0.0; // v
			pixels[4][i]=-1000.0; // valid
			pixels[5][i]=-1000.0; // extra
		}
		for (int i=0;i<grid.pixelsUV.length;i++){
			int u=grid.pixelsUV[i][0]-minU;
			int v=grid.pixelsUV[i][1]-minV;
			int index=u+width*v;
			pixels[0][index]=grid.pixelsXY[i][0];
			pixels[1][index]=grid.pixelsXY[i][1];
			pixels[2][index]=grid.pixelsUV[i][0];
			pixels[3][index]=grid.pixelsUV[i][1];
			pixels[4][index]=1000.0;
		}
		for (int i=0;i<grid.pixelsUV_extra.length;i++){
			int u=grid.pixelsUV_extra[i][0]-minU;
			int v=grid.pixelsUV_extra[i][1]-minV;
			int index=u+width*v;
			pixels[0][index]=grid.pixelsXY_extra[i][0];
			pixels[1][index]=grid.pixelsXY_extra[i][1];
			pixels[2][index]=grid.pixelsUV_extra[i][0];
			pixels[3][index]=grid.pixelsUV_extra[i][1];
			pixels[4][index]=1000.0;
		}
		(new showDoubleFloatArrays()).showArrays(pixels, width, height,  true, "grid-"+numGridImage, titles);

	}
	public void showGridAndHint(){
		GenericDialog gd=new GenericDialog("Show selected grid and/or hint grid");
		gd.addNumericField("Grid Image index", 0,0);
		gd.addCheckbox("Show grid image", true);
		gd.addCheckbox("Show hint grid", true);
		gd.addCheckbox("Use imageSet data if available (unchecked - camera data)", true);

		gd.showDialog();
		if (gd.wasCanceled()) return;
		int numGridImage= (int) gd.getNextNumber();
		boolean showGrid=gd.getNextBoolean();
		boolean showHint=gd.getNextBoolean();
		boolean useSetData=gd.getNextBoolean();
		IJ.showStatus("grid: "+((fittingStrategy.distortionCalibrationData.gIP[numGridImage].path==null)?"":fittingStrategy.distortionCalibrationData.gIP[numGridImage].path));
//		showStatus("grid: "+((fittingStrategy.distortionCalibrationData.gIP[numGridImage].path==null)?"":fittingStrategy.distortionCalibrationData.gIP[numGridImage].path),0);
	
        if (showGrid)	showGridImage(numGridImage);	
        if (showHint)	calcAndShowHintGrid(numGridImage,useSetData);	
	}

	
	
	public void calcAndShowHintGrid(int numGridImage, boolean useSetData){
		double [] goniometerTiltAxial=fittingStrategy.distortionCalibrationData.getImagesetTiltAxial(numGridImage);
		if ((goniometerTiltAxial==null) || Double.isNaN(goniometerTiltAxial[0])  || Double.isNaN(goniometerTiltAxial[1])){
			if (this.debugLevel>0)System.out.println("No goniometer orientation is available for image # "+numGridImage+" - "+fittingStrategy.distortionCalibrationData.gIP[numGridImage].path);
			GenericDialog gd=new GenericDialog("Specify camera orientation (channel"+fittingStrategy.distortionCalibrationData.gIP[numGridImage].channel+")");
			gd.addMessage("No goniometer orientation is available for image # "+numGridImage+" - "+fittingStrategy.distortionCalibrationData.gIP[numGridImage].path+
			", please specify orientation manually");
			gd.addNumericField("Camera tilt (0 - vertical, >0 looking above horizon on the target", 0.0, 1,6,"degrees");
			gd.addNumericField("Camera axial (0 - subcamera 0 looking to the target, >0 - rotated clockwise", 0.0, 1,6,"degrees");
			gd.showDialog();
			if (gd.wasCanceled()) return;
			goniometerTiltAxial=new double[2];
			goniometerTiltAxial[0]=       gd.getNextNumber();
			goniometerTiltAxial[1]=      gd.getNextNumber();
		}
		double [][][] hintGrid=estimateGridOnSensor(
				fittingStrategy.distortionCalibrationData.getImageStation(numGridImage), // station number
				fittingStrategy.distortionCalibrationData.gIP[numGridImage].channel,
				goniometerTiltAxial[0], // Tilt, goniometerHorizontal
				goniometerTiltAxial[1],  // Axial,goniometerAxial
				useSetData?fittingStrategy.distortionCalibrationData.gIP[numGridImage].setNumber:-1
		);
		showHintGrid(hintGrid,"hint-"+numGridImage);

	}
	public void showHintGrid(double [][][] hintGrid){
		showHintGrid(hintGrid,"hintGrid");
	}
	
	public void showHintGrid(double [][][] hintGrid, String title){
		double [][] pixels=new double[4][hintGrid.length*hintGrid[0].length];
		int index=0;
		String [] titles={"pixel-X","pixel-Y","grid-U","grid-V"};
		for (int v=0; v<hintGrid.length;v++) for (int u=0;u<hintGrid[v].length;u++){
			if (hintGrid[v][u]!=null){
				for (int i=0; i<4;i++)	pixels[i][index]=hintGrid[v][u][i];
			} else {
				for (int i=0; i<4;i++)	pixels[i][index]=0;
			}
			index++;
		}
		(new showDoubleFloatArrays()).showArrays(pixels, hintGrid[0].length, hintGrid.length,  true, title, titles);
	}
	
	/**
	 * Calculate grid on sensor using current camera parameters (including goniometer angles), sub-camera number
	 * @param subCamera
	 * @return grid array [v][u][0- x,  1 - y, 2 - u, 3 - v] 
	 */
	/*
	 // wrong, orientation depends on timestamp
	public double [][][] estimateGridOnSensor(
			int subCamera){
		double [] parVector=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getParametersVector(subCamera);
		return estimateGridOnSensor(
				subCamera,
				parVector[fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerHorizontalIndex()],
				parVector[fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerAxialIndex()]);
	}
	*/

	
	public LensDistortionParameters setupLensDistortionParameters(
			int numImg,
//			int stationNumber,
//			int subCamera,
			int debugLevel){     // Axial - may be Double.NaN
		
//		double [] parVector=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getParametersVector(stationNumber,subCamera);
//		System.out.println("setupLensDistortionParameters(): subCamera="+subCamera+", goniometerHorizontal="+goniometerHorizontal+", goniometerAxial="+goniometerAxial);
    	boolean isTripod=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.isTripod;
		LensDistortionParameters lensDistortionParameters = new LensDistortionParameters (
	    		isTripod,
	            null, //double [][] interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
//	            this.fittingStrategy.distortionCalibrationData.pars[numImg], //parVector,
	            this.fittingStrategy.distortionCalibrationData.getParameters(numImg), //parVector,
	    		null, //boolean [] mask, // calculate only selected derivatives (all parVect values are still
	    		debugLevel
				);
		return lensDistortionParameters;
	}
	
	public LensDistortionParameters setupLensDistortionParameters(
			int stationNumber,
			int subCamera,
			double goniometerHorizontal, // Tilt - may be Double.NaN 
			double goniometerAxial,
			int debugLevel){     // Axial - may be Double.NaN
		double [] parVector=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getParametersVector(stationNumber,subCamera);
		int goniometerHorizontalIndex=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerHorizontalIndex();
		int goniometerAxialIndex=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerAxialIndex();
//		int sensorWidth=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorWidth(subCamera);
//		int sensorHeight=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorHeight(subCamera);
		if (!Double.isNaN(goniometerHorizontal))parVector[goniometerHorizontalIndex]=goniometerHorizontal;
		if (!Double.isNaN(goniometerAxial))parVector[goniometerAxialIndex]=goniometerAxial;
//		System.out.println("setupLensDistortionParameters(): subCamera="+subCamera+", goniometerHorizontal="+goniometerHorizontal+", goniometerAxial="+goniometerAxial);
    	boolean isTripod=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.isTripod;
		LensDistortionParameters lensDistortionParameters = new LensDistortionParameters (
	    		isTripod,
	            null, //double [][] interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
	    		parVector,
	    		null, //boolean [] mask, // calculate only selected derivatives (all parVect values are still
	    		debugLevel
				);
		return lensDistortionParameters;
	}

	/**
	 * Calculate grid projection to pixel X, Y (not counting sensor correction (add?) and grid photometrics
	 * @param lensDistortionParameters LensDistortionParameters instance created for particular image with setupLensDistortionParameters()
	 * @param numImg image number
	 * @param u grid U (signed, 0 in the center)
	 * @param v grid V (signed, 0 in the center)
	 * @return [7] {pX,pY,grid mask (binary), grid R, grid G, grid B, alpha}
	 */
	
	public double [] reprojectGridNode(
			LensDistortionParameters lensDistortionParameters,
			int numImg,
			int u, // grid signed u,v
			int v
	){
		int debugThreshold=1;
		int nChn=   this.fittingStrategy.distortionCalibrationData.gIP[numImg].channel;
		int station=this.fittingStrategy.distortionCalibrationData.gIP[numImg].getStationNumber();
//		if (!lensDistortionParameters.isTargetVisible(false)) return null; // camera is looking away from the target (does not mean target is in FOV)
//		double [][][] patternGeometry=this.patternParameters.getGeometry(); // [v][u]{x,y,z,alpha} - no photometric
		double [] result= new double[7];
			double [] XYZMP=this.patternParameters.getXYZMP( // null pointer
					u,
					v,
					station,
					nChn,
					false);
			if (XYZMP==null) return null;
			// project the target point to this sensor	
			double [][]pXY=  lensDistortionParameters.calcPartialDerivatives(
					XYZMP[0], // target point horizontal, positive - right,  mm
					XYZMP[1], // target point vertical,   positive - down,  mm
					XYZMP[2], // target point horizontal, positive - away from camera,  mm
					false); // calculate derivatives, false - values only (NaN for behind points - only when false here)
			if (Double.isNaN(pXY[0][0])) {
				if (this.debugLevel>debugThreshold){
					System.out.println("reprojectGridNode(...,"+numImg+","+u+","+"v"+") - point behind the sensor");
				}
				return null; // point behind camera
			}
			result[0]=pXY[0][0];
			result[1]=pXY[0][1];
			result[2]=XYZMP[3]; // binary mask
			result[3]=XYZMP[4]; // R
			result[4]=XYZMP[5]; // G
			result[5]=XYZMP[6]; // B
			result[6]=XYZMP[7]; // alpha
// get photometrics here
			
		return result;

		
		
	}
	
	/**
	 * Calculate grid on sensor using current Camera parameters, sub-camera number and the two goniometer angles
	 * @param stationNumber
	 * @param subCamera
	 * @param goniometerHorizontal
	 * @param goniometerAxial
	 * @param imageSet - if >=0 - use this set number data  instead of the camera data)
	 * @return grid array [v][u][0- x,  1 - y, 2 - u, 3 - v] 
	 */
	
	// TODO:calcInterParamers() -> lensDistortionParameters.lensCalcInterParamers
	public double [][][] estimateGridOnSensor( // not yet thread safe
			int stationNumber,
			int subCamera,
			double goniometerHorizontal, // Tilt 
			double goniometerAxial,     // Axial
			int  imageSet){
		int debugThreshold=2;
		// Get parameter vector (22) for the selected sensor, current Eyesisparameters and specified orientation angles 		
		double [] parVector=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getParametersVector(stationNumber,subCamera);
		if ((imageSet>=0) &&
				(this.fittingStrategy.distortionCalibrationData.gIS!=null) &&
				(this.fittingStrategy.distortionCalibrationData.gIS[imageSet]!=null)){
			this.fittingStrategy.distortionCalibrationData.gIS[imageSet].updateParameterVectorFromSet(parVector);
		}
		int goniometerHorizontalIndex=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerHorizontalIndex();
		int goniometerAxialIndex=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getGoniometerAxialIndex();
		int sensorWidth=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorWidth(subCamera);
		int sensorHeight=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getSensorHeight(subCamera);
		parVector[goniometerHorizontalIndex]=goniometerHorizontal;
		parVector[goniometerAxialIndex]=     goniometerAxial;
		System.out.println("estimateGridOnSensor(): subCamera="+subCamera+", goniometerHorizontal="+goniometerHorizontal+", goniometerAxial="+goniometerAxial);
		calcInterParamers(
				this.lensDistortionParameters, // 22-long parameter vector for the image
				null, // this.interParameterDerivatives, // [22][]
				parVector,
				null); // if no derivatives, null is OK
//				false); // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
		if (!lensDistortionParameters.isTargetVisible(this.debugLevel>0)) {
			if (this.debugLevel>debugThreshold) System.out.println("Camera is looking away from the target");
//			return null; // camera is looking away from the target (does not mean target is in FOV)
		}
		double [][][] patternGeometry=this.patternParameters.getGeometry(); // [v][u]{x,y,z,alpha} - no photometric
		double [][][] result= new double[patternGeometry.length][patternGeometry[0].length][4];
		int visibleCells=0;
		double [][] debugPixels=null;
		String [] debugTitles={"pX","pY","X","Y","Z","mask"};
		if (this.debugLevel>debugThreshold){
			debugPixels=new double [6][patternGeometry.length*patternGeometry[0].length];
			for (int c=0;c<debugPixels.length;c++) for (int i=0;i<debugPixels[c].length;i++) debugPixels[c][i]=Double.NaN;
		}
		// was bug cased by +/- infinity (and sometimes numbers falling into the sensor range) when the image plane intersected target
		// simple fix - remove pixels with too few neighbors (maybe just all border pixels?
		
		
		for (int v=0;v<result.length;v++) for (int u=0;u<result[v].length;u++){
			int [] iUV=this.patternParameters.uvIndicesToUV (u, v);
			if (iUV==null) {
				result[v][u]=null;
			} else {
				double [] XYZM=this.patternParameters.getXYZM(iUV[0],iUV[1],stationNumber);
// project the target point to this sensor	
				double [][]pXY=  this.lensDistortionParameters.calcPartialDerivatives(
						XYZM[0], // target point horizontal, positive - right,  mm
						XYZM[1], // target point vertical,   positive - down,  mm
						XYZM[2], // target point horizontal, positive - away from camera,  mm
						false); // calculate derivatives, false - values only (NaN for behind points - only when false here)
// verify the grid is inside the sensor area (may use sensor mask later too? probably not needed)
				// Now NaN if point is behind the sensor
				if (Double.isNaN(pXY[0][0]) || (pXY[0][0]<0) || (pXY[0][0]>=sensorWidth) || (pXY[0][1]<0) || (pXY[0][1]>=sensorHeight)){
					if (this.debugLevel>debugThreshold){
						System.out.println("--- estimateGridOnSensor():v="+v+" u="+u+" X="+XYZM[0]+" Y="+XYZM[1]+" Z="+XYZM[2]+" M="+XYZM[3]+
								" pXY[0][0]="+pXY[0][0]+", pXY[0][1]="+pXY[0][1]+", iUV[0]="+iUV[0]+", iUV[1]="+iUV[1]); 
					}
					result[v][u]=null;
				} else {
					double [] resultCell={pXY[0][0],pXY[0][1],iUV[0],iUV[1]};
					result[v][u]=resultCell;
					if (this.debugLevel>debugThreshold){
						System.out.println("+++ estimateGridOnSensor():v="+v+" u="+u+" X="+XYZM[0]+" Y="+XYZM[1]+" Z="+XYZM[2]+" M="+XYZM[3]+
								" pXY[0][0]="+pXY[0][0]+", pXY[0][1]="+pXY[0][1]+", iUV[0]="+iUV[0]+", iUV[1]="+iUV[1]); 
					}
					visibleCells++;
				}
				if (this.debugLevel>debugThreshold){
					int uv=u+v*result[v].length;
					debugPixels[0][uv]=pXY[0][0];
					debugPixels[1][uv]=pXY[0][1];
					debugPixels[2][uv]=XYZM[0];
					debugPixels[3][uv]=XYZM[1];
					debugPixels[4][uv]=XYZM[2];
				}
			}
		}
		// now filter border nodes
		boolean [] mask= new boolean [patternGeometry.length*patternGeometry[0].length];
		int index=0;
		for (int v=0;v<result.length;v++) for (int u=0;u<result[v].length;u++){
			mask [index++]=(result[v][u]!=null) &&
			               ((v==0) || (result[v-1][u]!=null)) &&
			               ((v==(result.length-1)) || (result[v+1][u]!=null)) &&
			               ((u==0) || (result[v][u-1]!=null))&&
			               ((u==(result[v].length-1)) || (result[v][u+1]!=null));
		}
		index=0;
		for (int v=0;v<result.length;v++) for (int u=0;u<result[v].length;u++){
			if (!mask[index++]) result[v][u]=null;
		}
		
		if (this.debugLevel>debugThreshold){
			for (int v=0;v<result.length;v++) for (int u=0;u<result[v].length;u++){
				int uv=u+v*result[v].length;
				debugPixels[5][uv]=(result[v][u]!=null)?3000:-3000; // masked
			}
			(new showDoubleFloatArrays()).showArrays(
					debugPixels,
					result[0].length,
					result.length,
					true,
					"Hinted-All",
					debugTitles);
		}		
		if (this.debugLevel>1) {
			System.out.println("Grid in the FOV of the subcamera "+subCamera+
					" tilt="+goniometerHorizontal+" axial="+goniometerAxial+" has "+visibleCells+" cells");
		}
		if (visibleCells==0) return null; // no grid cells in FOV
		return result;
	}
	
    public void debugCompareInterparameterDerivatives(
    		double [] vector,
    		int imgNum,
    		double delta){
		if (this.debugLevel>1) {
			System.out.println("debugCompareInterparameterDerivatives(vector, imgNum="+imgNum+", delta="+delta+")");
			for (int ii=0;ii<vector.length;ii++) System.out.println(ii+": "+vector[ii]);
		}
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		if (imgNum<0){ // find first selected image
			boolean [] selectedImages=fittingStrategy.selectedImages();
			imgNum=0;
			while ((imgNum<numImg) && (!selectedImages[imgNum])) imgNum++;
		}
		if (imgNum>=numImg){
			IJ.showMessage("No images found for this fitting strategy");
			return; // no images found 
		}
		double [] imgVector=fittingStrategy.getImageParametersVector(imgNum, vector); //this.currentVector);
//		boolean [] imgMask= fittingStrategy.getImageParametersVectorMask(imgNum);
		boolean [] imgMask= new boolean[imgVector.length];
		for (int i=0;i<imgMask.length;i++) imgMask[i]=true;
		calcInterParamers(
				this.lensDistortionParameters,
				this.interParameterDerivatives, // [22][]
				imgVector,
				imgMask); // calculate only selected derivatives (all parVect values are still
//				true); // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
// reorder derivatives to match lensDistortionParameters.getExtrinsicVector(); (dist,x0,y0,yaw,pitch,roll)
//					double [] parameterVector0=lensDistortionParameters.getAllVector();
		double [] values=lensDistortionParameters.getExtrinsicVector();
		double [][] derivatives_true = new double [getNumInputs()][6];
		for (int i=0;i<getNumInputs();i++){
			derivatives_true[i][0]=this.interParameterDerivatives[i][2]; // d distance /d vector[i]
			derivatives_true[i][1]=this.interParameterDerivatives[i][0]; // d x0 /d vector[i]
			derivatives_true[i][2]=this.interParameterDerivatives[i][1]; // d y0 /d vector[i]
			derivatives_true[i][3]=this.interParameterDerivatives[i][3]; // d jaw /d vector[i]
			derivatives_true[i][4]=this.interParameterDerivatives[i][4]; // d pitch /d vector[i]
			derivatives_true[i][5]=this.interParameterDerivatives[i][5]; // d roll /d vector[i]
		}
		double [][] derivatives_delta = new double [getNumInputs()][values.length];
		for (int i=0;i<getNumInputs();i++){
			double [] vector_delta=imgVector.clone();
			vector_delta[i]+=delta;
			calcInterParamers(
					this.lensDistortionParameters,
					null, // this.interParameterDerivatives, // just values, no derivatives
					vector_delta, 
					imgMask);
//					false); // just values, no derivatives
			double [] values_delta=lensDistortionParameters.getExtrinsicVector();
			for (int j=0;j<derivatives_delta[i].length;j++) derivatives_delta[i][j]=(values_delta[j]-values[j])/delta; 
		}		
		String [] lensParNames = lensDistortionParameters.getExtrinsicNames();
	    String header="#\tphysical/lens\t ";
	    for (int i=0;i<lensParNames.length;i++)header+="\t"+lensParNames[i];
	    StringBuffer sb = new StringBuffer();
	    for (int parNum=0;parNum<getNumInputs();parNum++){
	    	sb.append(parNum+"\t"+fittingStrategy.distortionCalibrationData.parameterDescriptions[parNum][0]+"\tderivative");
	    	for (int i=0;i<lensParNames.length;i++) sb.append("\t"+derivatives_true[parNum][i]);
	    	sb.append("\n");
	    	sb.append("\t \tdelta");
	    	for (int i=0;i<lensParNames.length;i++) sb.append("\t"+derivatives_delta[parNum][i]);
	    	sb.append("\n");
	    	sb.append("\t \tdifference");
	    	for (int i=0;i<lensParNames.length;i++) sb.append("\t"+(derivatives_true[parNum][i]-derivatives_delta[parNum][i]));
	    	sb.append("\n");
	    	sb.append("---\t---\t---");
	    	for (int i=0;i<lensParNames.length;i++) sb.append("\t---");
	    	sb.append("\n");
	    }
	    new TextWindow("Comparisison of the interparameter dcerivatives (true and compared as deltas)", header, sb.toString(), 500,900);
    }
// after stepping back - no need to rerun calculateFxAndJacobian(false), just keep
	/**
	 *  Calculates f(X) and optionally Jacobian for the current parameters 
	 *  @parameter vector - parameter vector to be used
	 *  @parameter calcJacobian  if true, calculates Jacobian as this.jacobian
	 *  @return  f(X) - pixel coordinates for each (visible) grid pattern node for current parameters this.currentVector
	 *   as a 1-d array that alternates pixel-X and pixel-Y for all images
	 *   NOTE: this one is not thread safe
	 */
	public double [] calculateFxAndJacobian(double [] vector, boolean calcJacobian){ // when false, modifies only this.lensDistortionParameters.*
		if (vector==null) {
			calcJacobian=false;
//			vector = new double[0];
		}
		// TODO: verify classes/arrays exist?
        int doubleNumAllPoints=this.Y.length; // all points in all images multiplied by 2 (x and y error are separate)		
		int fittedParNumber=(vector==null)?0:vector.length; //this.currentVector.length;
		double [] vectorFX=new double[doubleNumAllPoints];
//		this.fX=new double[doubleNumAllPoints];
		if (this.debugLevel>2) {
			System.out.println("calculateFxAndJacobian(), calcJacobian="+calcJacobian);
			if (vector!=null) {
			  for (int ii=0;ii<vector.length;ii++) System.out.println(ii+": "+vector[ii]);
			} else {
				System.out.println("calculateFxAndJacobian() : vector==null");
			}
		}
		if (calcJacobian) {
			this.jacobian=new double[fittedParNumber][doubleNumAllPoints];
			for (int i=0;i<fittedParNumber;i++) for (int j=0;j<doubleNumAllPoints;j++) this.jacobian[i][j]=0.0;
		}
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		boolean [] selectedImages=fittingStrategy.selectedImages();
		int index=0;
		IJ.showProgress(0);
		for (int imgNum=0;imgNum<numImg;imgNum++) if (selectedImages[imgNum]) {
//			initialize arrays for parameters and derivatives conversion
			double [] imgVector=fittingStrategy.getImageParametersVector(imgNum, vector); // null is OK now
			boolean [] imgMask=null;
			int []     imgMap= null;
			if (calcJacobian) {
				imgMask= fittingStrategy.getImageParametersVectorMask(imgNum);
				int []     imgRMap=  fittingStrategy.getImageParametersVectorReverseMap(imgNum);
				imgMap=new int[vector.length];
				for (int i=0;i<imgMap.length;i++) imgMap[i]=-1;
				for (int i=0;i<imgRMap.length;i++) if (imgRMap[i]>=0)imgMap[imgRMap[i]]=i;
			}

// Calculate/set  this.lensDistortionParameters class, so it will calculate values/derivatives correctly)
// and this.interParameterDerivatives
//			if (this.debugLevel>1) {
			if (this.debugLevel>2) {
				System.out.println("calculateFxAndJacobian(), imgNum="+imgNum+" calcInterParamers():");
			}
			calcInterParamers(
					this.lensDistortionParameters,
					calcJacobian?this.interParameterDerivatives:null, // [22][]
					imgVector,
					imgMask); // imgMask may be null if no derivativescalculate only selected derivatives (all parVect values are still
			int numPoints=fittingStrategy.distortionCalibrationData.getImageNumPoints(imgNum);
			if (this.debugLevel>2) {
				System.out.println("calculateFxAndJacobian(), numPoints="+numPoints+" (imgNum="+imgNum+")");
			}
// iterate through points, for each calculate pixelx, pixely and derivatives			
			for (int pointNum=0;pointNum<numPoints;pointNum++){
				int fullIndex=index+pointNum;
				if (fullIndex>=this.targetXYZ.length){
					System.out.println("BUG: calculateFxAndJacobian() imgNum="+imgNum+" pointNum="+pointNum+" fullIndex="+fullIndex+" this.targetXYZ.length="+this.targetXYZ.length);
				}
				double [][]derivatives15=  lensDistortionParameters.calcPartialDerivatives(
						this.targetXYZ[fullIndex][0], // target point horizontal, positive - right,  mm
						this.targetXYZ[fullIndex][1], // target point vertical,   positive - down,  mm
						this.targetXYZ[fullIndex][2], // target point horizontal, positive - away from camera,  mm
						calcJacobian); // calculate derivatives, false - values only
	       		if (this.debugLevel>3) {
	    			System.out.println(fullIndex+": calculateFxAndJacobian->calcPartialDerivatives("+IJ.d2s(targetXYZ[fullIndex][0],2)+","+
	    					IJ.d2s(targetXYZ[fullIndex][1],2)+","+
	    					IJ.d2s(targetXYZ[fullIndex][2],2)+" ("+calcJacobian+") -> "+
	    					IJ.d2s(derivatives15[0][0],2)+"/"+IJ.d2s(derivatives15[0][1],2));
	    			String all="derivatives15:";
	    			for (int ii=0;ii<derivatives15.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives15[ii][0],3)+"/"+IJ.d2s(derivatives15[ii][1],3);
	    			System.out.println(all);
	    		}
				vectorFX[2*fullIndex]=  derivatives15[0][0];			
				vectorFX[2*fullIndex+1]=derivatives15[0][1];
				if (calcJacobian) {
					double [][]derivatives = lensDistortionParameters.reorderPartialDerivatives(derivatives15);
		       		if (this.debugLevel>3) {
		    			String all="derivatives:";
		    			for (int ii=0;ii<derivatives.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives[ii][0],3)+"/"+IJ.d2s(derivatives[ii][1],3);
		    			System.out.println(all);
		    		}
					for (int i=0;i<this.jacobian.length;i++) if (imgMap[i]>=0){
						double sX=0,sY=0;
						for (int k=0;k<derivatives.length;k++){
							sX+=this.interParameterDerivatives[imgMap[i]][k]*derivatives[k][0];
							sY+=this.interParameterDerivatives[imgMap[i]][k]*derivatives[k][1];
						}
						this.jacobian[i][2*fullIndex]=  sX;
						this.jacobian[i][2*fullIndex+1]=sY;
					}
				}
			}
			index+=numPoints;
			IJ.showProgress(imgNum, numImg-1);
		}
//		IJ.showProgress(0); not needed, will turn off automatically

		return vectorFX;
	}

	/**
	 * Calculate FX and (optionally) Jacobian for one image. FX is a single vector for all images, jacobian - only for one (to save on memory usage)
	 * @param numImage    number of image being processed
	 * @param vector      parameters vector
	 * @param patternXYZ  X,Y,Z of the physical target for each node of each image (TODO: memory may be reduced)
	 * @param vectorFX    Vector to be filled here , twice length as patternXYZ (x and y alternating)
	 * @param imageStartIndex  start index in patternXYZ array (length - difference to the next, includes extra last element)
	 * @param lensDistortionParameters LensDistortionParameters class instance (may be reused between calls) 
	 * @param calcJacobian calculate Jacobian matrix (if false - only FX)
	 * @return partial Jacobian matrix, number of rows= vector.length, number of columns - 2*indexCount
	 *   NOTE: this one is thread safe
	 */
	
	public double [][] calculatePartialFxAndJacobian(
			final int numImage,      // number of grid image
			final double [] vector,  // parameters vector
			final double [][] patternXYZ, // this.targetXYZ
			final double [] vectorFX,     // non-overlapping segments will be filled
			final int []  imageStartIndex, // start index in patternXYZ array (length - difference to the next, includes extra last element)
			final LensDistortionParameters lensDistortionParameters, // initialize one per each thread? Or for each call?
			boolean calcJacobian){ // when false, modifies only this.lensDistortionParameters.*
		final int    indexStart=imageStartIndex[numImage];      // start index in patternXYZ array
		final int    indexCount=imageStartIndex[numImage+1]-imageStartIndex[numImage]; // number of nodes in the current grid image
		int fittedParNumber=vector.length; //this.currentVector.length;
		if (this.debugLevel>3) {
			System.out.println("calculatePartialFxAndJacobian(), calcJacobian="+calcJacobian+" indexStart="+indexStart+" indexCount="+indexCount);
			for (int ii=0;ii<vector.length;ii++) System.out.println("vector["+ii+"]: "+vector[ii]);
		}
		boolean [] imgMask= fittingStrategy.getImageParametersVectorMask(numImage);        // thread safe
		int []     imgRMap=  fittingStrategy.getImageParametersVectorReverseMap(numImage); // thread safe
		int []     imgMap=new int[vector.length];
		for (int i=0;i<imgMap.length;i++) imgMap[i]=-1;
		for (int i=0;i<imgRMap.length;i++) if (imgRMap[i]>=0)imgMap[imgRMap[i]]=i;
		double [][] jacobian=null;
		if (calcJacobian) {
//			jacobian=new double[fittedParNumber][indexCount*2];
//			for (int i=0;i<fittedParNumber;i++) for (int j=0;j<jacobian[0].length;j++) jacobian[i][j]=0.0;
			jacobian=new double[fittedParNumber][];
			// TODO: verify that only small number of rows is calculated
			for (int i=0;i<fittedParNumber;i++) {
				if (imgMap[i]>=0) {
					jacobian[i]=new double [indexCount*2];
					for (int j=0;j<jacobian[i].length;j++) jacobian[i][j]=0.0;
				} else {
					jacobian[i]=null;
				}
			}
		}
		double [][] interParameterDerivatives=new double [getNumInputs()][];
		//			initialize arrays for parameters and derivatives conversion
		double [] imgVector=fittingStrategy.getImageParametersVector(numImage, vector);     // thread safe
		if (this.debugLevel>3) {
			String all="imgVector: ";
			for (int jj=0;jj<imgVector.length;jj++) all+=" "+imgVector[jj];
			System.out.println(all);
		}
		// Calculate/set  this.lensDistortionParameters class, so it will calculate values/derivatives correctly)
		// and this.interParameterDerivatives
		if (this.debugLevel>3) System.out.println("calculatePartialFxAndJacobian(), numImage="+numImage+" calcInterParamers():");
		calcInterParamers(
				lensDistortionParameters,
				calcJacobian?interParameterDerivatives:null, // [22][]
						imgVector,
						imgMask); // calculate only selected derivatives (all parVect values are still

		// iterate through points, for each calculate pixelx, pixely and derivatives			
		for (int pointNum=0;pointNum<indexCount;pointNum++){
			int fullIndex=indexStart+pointNum;
			double [][]derivatives15=  lensDistortionParameters.calcPartialDerivatives(
					patternXYZ[fullIndex][0], // target point horizontal, positive - right,  mm
					patternXYZ[fullIndex][1], // target point vertical,   positive - down,  mm
					patternXYZ[fullIndex][2], // target point horizontal, positive - away from camera,  mm
					calcJacobian); // calculate derivatives, false - values only
			if (this.debugLevel>3) {
				System.out.println(fullIndex+": calculateFxAndJacobian->calcPartialDerivatives("+IJ.d2s(patternXYZ[fullIndex][0],2)+","+
						IJ.d2s(patternXYZ[fullIndex][1],2)+","+
						IJ.d2s(patternXYZ[fullIndex][2],2)+" ("+calcJacobian+") -> "+
						IJ.d2s(derivatives15[0][0],2)+"/"+IJ.d2s(derivatives15[0][1],2));
				String all="derivatives15:";
				for (int ii=0;ii<derivatives15.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives15[ii][0],3)+"/"+IJ.d2s(derivatives15[ii][1],3);
				System.out.println(all);
			}
			vectorFX[2*fullIndex]=  derivatives15[0][0];			
			vectorFX[2*fullIndex+1]=derivatives15[0][1];
			if (calcJacobian) {
				double [][]derivatives = lensDistortionParameters.reorderPartialDerivatives(derivatives15);
				if (this.debugLevel>3) {
					String all="derivatives:";
					for (int ii=0;ii<derivatives.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives[ii][0],3)+"/"+IJ.d2s(derivatives[ii][1],3);
					System.out.println(all);
				}
				for (int i=0;i<jacobian.length;i++) if (imgMap[i]>=0){
					double sX=0,sY=0;
					for (int k=0;k<derivatives.length;k++){
						sX+=interParameterDerivatives[imgMap[i]][k]*derivatives[k][0];
						sY+=interParameterDerivatives[imgMap[i]][k]*derivatives[k][1];
					}
					jacobian[i][2*pointNum]=  sX;
					jacobian[i][2*pointNum+1]=sY;
				}
			}
		}
		return jacobian;
	}
	
	
	/**
	 * 
	 * @param vector - parameter vector to be used
	 * @param imgNumber - number of image to process or -1 - use the first of selected in this strategy
	 * @return return Jacobian matrix for the selected image and individual parameters
	 *   NOTE: this one is not thread safe (used this.lensDistortionParameters)
	 */
	// used only to debug derivatives (delta==0 - real derivatives, delta>0 - difference)
	public double [][] calculateJacobian16(double [] vector, int imgNumber, double delta){ // these parameters can work for one image only
        int doubleNumAllPoints=this.Y.length; // all points in all images multiplied by 2 (x and y error are separate)		
		double [][] jacobian16=new double[16][doubleNumAllPoints];
		double []   values=    new double[doubleNumAllPoints];
		
		for (int i=0;i<jacobian16.length;i++) for (int j=0;j<doubleNumAllPoints;j++) jacobian16[i][j]=0.0;
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		boolean [] selectedImages=fittingStrategy.selectedImages();
		int index=0;
		for (int imgNum=0;imgNum<numImg;imgNum++) if (selectedImages[imgNum]) {
			int numPoints=fittingStrategy.distortionCalibrationData.getImageNumPoints(imgNum);
			if (imgNumber<0) imgNumber=imgNum; // -1 - use the first image in the list 
			if (imgNum==imgNumber) {
				double [] imgVector=fittingStrategy.getImageParametersVector(imgNum, vector); //this.currentVector);
				boolean [] imgMask= fittingStrategy.getImageParametersVectorMask(imgNum);
				int []     imgRMap=  fittingStrategy.getImageParametersVectorReverseMap(imgNum);
				int []     imgMap=new int[vector.length];
				for (int i=0;i<imgMap.length;i++) imgMap[i]=-1;
				for (int i=0;i<imgRMap.length;i++) if (imgRMap[i]>=0)imgMap[imgRMap[i]]=i;
				// Calculate/set  this.lensDistortionParameters class, so it will calculate values/derivatives correctly)
				// and this.interParameterDerivatives
				if (this.debugLevel>2) {
					System.out.println("calculateJacobian15(), imgNum="+imgNum+" calcInterParamers():");
				}
				calcInterParamers(
						this.lensDistortionParameters,
						null, //this.interParameterDerivatives, // [22][]
						imgVector,
						imgMask); // calculate only selected derivatives (all parVect values are still
//						false); // probably can use false
				if (this.debugLevel>2) {
					System.out.println("calculateJacobian16(), numPoints="+numPoints+" (imgNum="+imgNum+")");
				}
				if (delta<=0) {
					// iterate through points, for each calculate pixelx, pixely and derivatives			
					for (int pointNum=0;pointNum<numPoints;pointNum++){
						int fullIndex=index+pointNum;
						double [][]derivatives15=  lensDistortionParameters.calcPartialDerivatives(
								targetXYZ[fullIndex][0], // target point horizontal, positive - right,  mm
								targetXYZ[fullIndex][1], // target point vertical,   positive - down,  mm
								targetXYZ[fullIndex][2], // target point horizontal, positive - away from camera,  mm
								true); // calculate derivatives, false - values only
						if (this.debugLevel>3) {
							System.out.println(fullIndex+": calculateFxAndJacobian->calcPartialDerivatives("+IJ.d2s(targetXYZ[fullIndex][0],2)+","+
									IJ.d2s(targetXYZ[fullIndex][1],2)+","+
									IJ.d2s(targetXYZ[fullIndex][2],2)+" -> "+
									IJ.d2s(derivatives15[0][0],2)+"/"+IJ.d2s(derivatives15[0][1],2));
							String all="derivatives15:";
							for (int ii=0;ii<derivatives15.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives15[ii][0],3)+"/"+IJ.d2s(derivatives15[ii][1],3);
							System.out.println(all);
						}
						double [][]derivatives = lensDistortionParameters.reorderPartialDerivativesAsNames(derivatives15);
						if (this.debugLevel>3) {
							String all="derivatives:";
							for (int ii=0;ii<derivatives.length;ii++) all+=" "+ii+":"+IJ.d2s(derivatives[ii][0],3)+"/"+IJ.d2s(derivatives[ii][1],3);
							System.out.println(all);
						}
						for (int i=0;i<derivatives.length;i++){
							jacobian16[i][2*fullIndex]= derivatives[i][0];
							jacobian16[i][2*fullIndex+1]=derivatives[i][1];
						}

					}
				} else {
					double [] parameterVector0=lensDistortionParameters.getAllVector();
					for (int pointNum=0;pointNum<numPoints;pointNum++){
						int fullIndex=index+pointNum;
						double [][]values2=  lensDistortionParameters.calcPartialDerivatives(
								targetXYZ[fullIndex][0], // target point horizontal, positive - right,  mm
								targetXYZ[fullIndex][1], // target point vertical,   positive - down,  mm
								targetXYZ[fullIndex][2], // target point horizontal, positive - away from camera,  mm
								false); // calculate derivatives, false - values only
						values[2*fullIndex]= values2[0][0];
						values[2*fullIndex+1]=values2[0][1];
					}
					for (int nPar=0;nPar<jacobian16.length;nPar++){
						double [] parameterVector=parameterVector0.clone();
						parameterVector[nPar]+=delta;
						lensDistortionParameters.setAllVector(parameterVector);
						for (int pointNum=0;pointNum<numPoints;pointNum++){
							int fullIndex=index+pointNum;
							double [][] values2=lensDistortionParameters.calcPartialDerivatives(
									targetXYZ[fullIndex][0], // target point horizontal, positive - right,  mm
									targetXYZ[fullIndex][1], // target point vertical,   positive - down,  mm
									targetXYZ[fullIndex][2], // target point horizontal, positive - away from camera,  mm
									false); // calculate derivatives, false - values only
							jacobian16[nPar][2*fullIndex]=  (values2[0][0]- values[2*fullIndex])/delta;
							jacobian16[nPar][2*fullIndex+1]=(values2[0][1]- values[2*fullIndex+1])/delta;
						}
					}
				}
				return jacobian16;
			}
			index+=numPoints;
		}
		return null; // should normally return from inside the for loop
	}
	
	
/*
List calibration	
 */
    public boolean listImageParameters(boolean silent){
    	if (this.fittingStrategy==null) {
        		String msg="Fitting strategy does not exist, exiting";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
    	}
    	int numSeries=fittingStrategy.getNumSeries();
    	if (silent) {
    		this.seriesNumber=0; 
    	} else {
    		GenericDialog gd = new GenericDialog("Settings for the parameter list");
    		gd.addNumericField("Iteration number to start (0.."+(numSeries-1)+")", this.seriesNumber, 0);
    		gd.addCheckbox("Show image number (from 0)",                           this.showIndex);
    		gd.addCheckbox("Show per-image RMS",                                   this.showRMS);
    		gd.addCheckbox("Show number of grid points",                           this.showPoints);
    		gd.addCheckbox("Show lens coordinates (relative to target)",           this.showLensLocation);

    		gd.addCheckbox("Show physical camera parameters",                      this.showEyesisParameters);
    		gd.addCheckbox("Show intrinsic lens/sensor parameters ",               this.showIntrinsicParameters);
    		gd.addCheckbox("Show intrinsic lens/sensor parameters",                this.showExtrinsicParameters);
    		gd.addNumericField("Extra decimal places (precision) in the list",     this.extraDecimals, 0);
    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		this.seriesNumber=          (int) gd.getNextNumber();
    		this.showIndex=                   gd.getNextBoolean();
    		this.showRMS=                     gd.getNextBoolean();
    		this.showPoints=                  gd.getNextBoolean();
    		this.showLensLocation=            gd.getNextBoolean();
    		this.showEyesisParameters=        gd.getNextBoolean();
    		this.showIntrinsicParameters=     gd.getNextBoolean();
    		this.showExtrinsicParameters=     gd.getNextBoolean();
    		this.extraDecimals=         (int) gd.getNextNumber();
    	}
// need to select strategy
	    initFittingSeries(true,this.filterForAll,this.seriesNumber); // will set this.currentVector
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		double [] errors=calcErrors(calcYminusFx(this.currentfX));
		double    rms=   calcError (calcYminusFx(this.currentfX));
		int [] numPairs=calcNumPairs();


		boolean [] selectedImages=fittingStrategy.selectedImages();
//TODO: add display of per-image RMS		
	    listImageParameters (
	    		selectedImages,
	    		rms,
	    		errors,
	    		numPairs,
	    		this.showIndex,
	    		this.showRMS,
	    		this.showPoints,
	    		this.showLensLocation,
	    		this.showEyesisParameters,
	    		this.showIntrinsicParameters,
	    		this.showExtrinsicParameters,
	    		this.extraDecimals);
    	return true;
    }
    
    public void markBadNodces(int seriesNumber,
    		int debugLevel){
    	int oldSeries=this.seriesNumber;
    	this.seriesNumber=seriesNumber;
    	int totalBadNodes=markBadNodes(
    			fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMS,
    			fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMSNonweighted,
    			false,
    			debugLevel
    	);
    	if (debugLevel>0) {
    		System.out.println("Marked "+totalBadNodes+" nodes as bad (excessive errors, used fitting series #"+this.seriesNumber+")");
    	}
    	this.seriesNumber=oldSeries;
    }

    public boolean dialogMarkBadNodes(int debugLevel){
    	int numSeries=fittingStrategy.getNumSeries();
    	boolean verbose=false;
    	GenericDialog gd = new GenericDialog("Select parameters for marking bad nodes");
    	gd.addNumericField("Series number to use for selection (0.."+(numSeries-1)+")", this.seriesNumber, 0);
    	gd.addNumericField("Remove nodes with error greater than scaled RMS in that image, weighted",  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMS, 2,6,"xRMS");
    	gd.addNumericField("Same, not weghted (not more permissive near the borders with low weight)", fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMSNonweighted, 2,6,"xRMS");
    	gd.addCheckbox    ("Verbose (report number of bad nodes per image)",verbose);
    	gd.showDialog();
    	if (gd.wasCanceled()) return false;
    	this.seriesNumber=          (int) gd.getNextNumber();
    	fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMS=            gd.getNextNumber();
    	fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMSNonweighted= gd.getNextNumber();
    	verbose=                                                                                   gd.getNextBoolean();
    	int totalBadNodes=markBadNodes(
    			fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMS,
    			fittingStrategy.distortionCalibrationData.eyesisCameraParameters.removeOverRMSNonweighted,
    			verbose,
    			debugLevel
    	);
    	if (debugLevel>0) {
    		System.out.println("Marked "+totalBadNodes+" nodes as bad (excessive errors");
    	}
    	return true;
    }
    
    public boolean removeOutLayers(int series, int numOutLayers){
    	int numSeries=fittingStrategy.getNumSeries();
    	boolean removeEmpty=false;
    	boolean recalculate=false;
		int filter=filterForAll;
    	if ((series<0) || (numOutLayers<0)) {
    		GenericDialog gd = new GenericDialog("Select series to process");
    		gd.addNumericField("Iteration number to start (0.."+(numSeries-1)+")", this.seriesNumber, 0);
    		gd.addCheckbox("Recalculate parameters vector from selected strategy",recalculate);
    		gd.addNumericField("Number of outlayers to show", 10, 0);
    		gd.addCheckbox("Remove empty (rms==NaN) images", removeEmpty);
    		gd.addCheckbox("Ask filter (current filter="+filter+")",    this.askFilter);
    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		this.seriesNumber=          (int) gd.getNextNumber();
    		recalculate=                      gd.getNextBoolean();
    		numOutLayers=               (int) gd.getNextNumber();
    		removeEmpty=                      gd.getNextBoolean();
    		this.askFilter=                   gd.getNextBoolean();
    		if (this.askFilter) filter=  selectFilter(filter);
    		filter=0;
    	} else {
    		this.seriesNumber=series;
    	}
//	    initFittingSeries(!recalculate,this.filterForAll,this.seriesNumber); // will set this.currentVector
	    initFittingSeries(!recalculate,this.filterForAll,this.seriesNumber); // will set this.currentVector
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		double [] errors=calcErrors(calcYminusFx(this.currentfX)); // seem to have errors? - now may return NaN!
		double    rms=   calcError (calcYminusFx(this.currentfX));
		boolean [] selectedImages=fittingStrategy.selectedImages();
		int numSelectedNotNaNImages=0;
		int numNaN=0;
		for (int i=0;i<selectedImages.length;i++) if (selectedImages[i]) {
			if (!Double.isNaN(errors[i])) numSelectedNotNaNImages++;
			else numNaN++;
		}
		int [] imgIndices=new int[numSelectedNotNaNImages];
		int index=0;
		for (int i=0;i<selectedImages.length;i++) if ( selectedImages[i] && !Double.isNaN(errors[i])) imgIndices[index++]=i; // OOB 2389

		if (numOutLayers>numSelectedNotNaNImages) numOutLayers=numSelectedNotNaNImages;
		int [] indices=new int [numOutLayers];
		boolean [] availableImages=selectedImages.clone();
		for (int i=0;i<selectedImages.length;i++) if (selectedImages[i] && Double.isNaN(errors[i])) availableImages[i]=false;
		
		
		if ((this.debugLevel>0) && (numNaN>0)){
			System.out.println("removeOutLayers(): Number of empty (rms=NaN) images="+numNaN+":");
			int n=0;
			for (int i=0;i<selectedImages.length;i++) if (selectedImages[i] && Double.isNaN(errors[i])){
				n++;
				System.out.println(n+": "+i+": "+this.fittingStrategy.distortionCalibrationData.gIP[i].path);
			}
		}
		if (removeEmpty){
			int n=0;
			for (int i=0;i<selectedImages.length;i++) if (selectedImages[i] && Double.isNaN(errors[i])){
				n++;
				if (this.debugLevel>0) System.out.println(n+"removing empty image #"+i+": "+this.fittingStrategy.distortionCalibrationData.gIP[i].path);
				this.fittingStrategy.distortionCalibrationData.gIP[i].enabled=false;
				this.fittingStrategy.distortionCalibrationData.gIP[i].hintedMatch=-1; // so can be re-calibrated again w/o others
			}

		}
		
		System.out.println("removeOutLayers(): availableImages.length="+availableImages.length+" numSelectedNotNaNImages="+numSelectedNotNaNImages);
		for (int n=0;n<numOutLayers;n++){
			double maxRMS=-1.0;
			indices[n]=-1;
			for (int i=0;i<availableImages.length;i++)if (availableImages[i] && (Double.isNaN(errors[i]) || (errors[i]>maxRMS))){ // Double.NaN will be greater
					maxRMS=errors[i];
					indices[n]=i;
			}
			if (indices[n]<0){
				System.out.println("removeOutLayers(): indices["+n+"]="+indices[n]);
				continue;
			}
			availableImages[indices[n]]=false; // java.lang.ArrayIndexOutOfBoundsException: -1
		}
		
		GenericDialog gd = new GenericDialog("Select images to remove (RMS="+IJ.d2s(rms,3)+")");
		if (this.debugLevel>0) System.out.println("Listing "+numOutLayers+" worst images:");
		for (int n=0;n<indices.length;n++){
			String msg=n+" ("+indices[n]+" / "+ this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].setNumber+"): "+
			IJ.d2s(errors[indices[n]],3)+" "+
			this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].path+
			" ("+this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].pixelsXY.length+
			" points) "+selectedImages[indices[n]];
			
			if (this.debugLevel>0) System.out.println(
					msg);
			gd.addCheckbox(msg, true);
		}
		WindowTools.addScrollBars(gd);
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		if (this.debugLevel>0) System.out.println("Removing outlayers:");
		for (int n=0;n<indices.length;n++){
			if (gd.getNextBoolean()) {
				if (this.debugLevel>0) System.out.println(n+" :"+IJ.d2s(errors[indices[n]],3)+" "+this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].path);
				this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].enabled=false;
				this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].hintedMatch=-1; // so can be re-calibrated again w/o others
			}
		}
		fittingStrategy.distortionCalibrationData.updateSetOrientation(null); // remove orientation information from the image set if none is enabled
		return true;
    }

    public boolean removeOutLayerSets(int numOutLayers){
    	boolean removeEmptySets=false;
    	if (numOutLayers<0) {
    		GenericDialog gd = new GenericDialog("Select sets to process");
    		gd.addNumericField("Series number (<0 - all images)", -1, 0);
    		gd.addNumericField("Number of outlayers to show", 5, 0);
    		gd.addCheckbox("Remove empty sets", removeEmptySets);
    		gd.addCheckbox("Ask for weight function filter",     this.askFilter);
    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		this.seriesNumber= (int) gd.getNextNumber();
    		numOutLayers=      (int) gd.getNextNumber();
    		removeEmptySets=         gd.getNextBoolean();
    		this.askFilter=         gd.getNextBoolean();
    	}
//		boolean [] oldSelection=this.fittingStrategy.selectAllImages(0); // enable all images in series 0
		int filter=this.filterForAll;
		if (this.askFilter) filter=selectFilter(filter);
    	initFittingSeries(true,filter,this.seriesNumber); // will set this.currentVector
//    	initFittingSeries(true,this.filterForAll,this.seriesNumber); // will set this.currentVector
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		double [] errors=calcErrors(calcYminusFx(this.currentfX));
		double    rms=   calcError (calcYminusFx(this.currentfX));
//		boolean [] selectedImages=fittingStrategy.selectedImages();
		int [] numPairs=calcNumPairs();
	    int [][] imageSets=this.fittingStrategy.distortionCalibrationData.listImages(false); // true - only enabled images
	    int [] numSetPoints=new int [imageSets.length];
	    double [] rmsPerSet=new double[imageSets.length];
	    boolean [] hasNaNInSet=new boolean[imageSets.length];
	    boolean [] allNaNInSet=new boolean[imageSets.length];
	    for (int setNum=0;setNum<imageSets.length;setNum++){
	    	double error2=0.0;
	    	int numInSet=0;
    		hasNaNInSet[setNum]=false;
    		for (int imgInSet=0;imgInSet<imageSets[setNum].length;imgInSet++){
	    		int imgNum=imageSets[setNum][imgInSet];
	    		int num=numPairs[imgNum];
	    		if (Double.isNaN(errors[imgNum])){
	    			hasNaNInSet[setNum]=true;
	    		} else {
	    			error2+=errors[imgNum]*errors[imgNum]*num;
	    			numInSet+=num;
	    			
	    		}
	    	}
    		allNaNInSet[setNum]=(numInSet==0);
	    	numSetPoints[setNum]=numInSet;
	    	rmsPerSet[setNum]=(numInSet>0)?Math.sqrt(error2/numInSet):Double.NaN;
	    }
//		int numSelectedNotNaNSets=0;
		int numSelectedSets=0;
		int numNaN=0;
		for (int i=0;i<imageSets.length;i++)  {
//			if (!Double.isNaN(rmsPerSet[i])) numSelectedSets++;
			if (!allNaNInSet[i]) numSelectedSets++;
			else numNaN++;
		}
//		int [] imgIndices=new int[numSelectedNotNaNSets];
//		int index=0;
//		for (int i=0;i<imageSets.length;i++) if ( selectedImages[i]) imgIndices[index++]=i;

		if (numOutLayers>numSelectedSets) numOutLayers=numSelectedSets;
		int [] indices=new int [numOutLayers];
		boolean [] availableSets= new boolean  [imageSets.length];
		for (int i=0;i<imageSets.length;i++) availableSets[i]= !allNaNInSet[i]; //!Double.isNaN(rmsPerSet[i]);
		if (removeEmptySets  && (numNaN>0)){ //(this.debugLevel>0)
			if (this.debugLevel>0) System.out.println("removeOutLayerSets(): Number of empty (rms=NaN) sets="+numNaN+":");
//			int n=0;
			for (int setNum=0;setNum<imageSets.length;setNum++) if (!availableSets[setNum]){
//				n++;
				if (this.debugLevel>0) System.out.println("Set "+setNum);
	    		for (int imgInSet=0;imgInSet<imageSets[setNum].length;imgInSet++){
					int numImg=imageSets[setNum][imgInSet];
					if (this.debugLevel>0) System.out.println(setNum+":"+imgInSet+" #"+ numImg+" "+IJ.d2s(errors[numImg],3)+" "+
							this.fittingStrategy.distortionCalibrationData.gIP[numImg].path);
					this.fittingStrategy.distortionCalibrationData.gIP[numImg].enabled=false;
					this.fittingStrategy.distortionCalibrationData.gIP[numImg].hintedMatch=-1; // so can be re-calibrated again w/o others
	    		}
			}
		}
		
		System.out.println("removeOutLayerSets(): availableSets.length="+availableSets.length+" numSelectedSets="+numSelectedSets);
		for (int n=0;n<numOutLayers;n++){
			double maxRMS=-1.0;
			indices[n]=-1;
			for (int i=0;i<availableSets.length;i++)if (availableSets[i] && (rmsPerSet[i]>maxRMS)){ // NaN are already skipped
					maxRMS=rmsPerSet[i];
					indices[n]=i;
			}
			if (indices[n]<0){
				System.out.println("removeOutLayerSets(): indices["+n+"]="+indices[n]);
				continue;
			}
			availableSets[indices[n]]=false; // java.lang.ArrayIndexOutOfBoundsException: -1
		}
		
		GenericDialog gd = new GenericDialog("Select image Sets to remove (RMS="+IJ.d2s(rms,3)+")");
		if (this.debugLevel>0) System.out.println("Listing "+numOutLayers+" worst image sets");
		for (int n=0;n<indices.length;n++){
			int numSet=indices[n];
			double setWeight=this.fittingStrategy.distortionCalibrationData.gIS[numSet].setWeight;
			if (this.debugLevel>0) System.out.println(n+" ("+numSet+"): "+(hasNaNInSet[numSet]?"* ":"")+IJ.d2s(rmsPerSet[numSet],3)+
					" points: "+numSetPoints[numSet]+" weight:"+setWeight);
			gd.addCheckbox(n+": "+numSet+": "+(hasNaNInSet[numSet]?"* ":"")+IJ.d2s(rmsPerSet[numSet],3)+" weight:"+setWeight, true);
			for (int i=0;i<imageSets[numSet].length;i++){
				int numImg=imageSets[numSet][i];
				double diameter=this.fittingStrategy.distortionCalibrationData.gIP[numImg].getGridDiameter();
				gd.addMessage(i+":"+numImg+": "+IJ.d2s(errors[numImg],3)+" "+
						" ("+this.fittingStrategy.distortionCalibrationData.gIP[numImg].pixelsXY.length+" points, diameter="+diameter+") "+		
						this.fittingStrategy.distortionCalibrationData.gIP[numImg].path);
				if (this.debugLevel>0) System.out.println("  --- "+numImg+": "+IJ.d2s(errors[numImg],3)+" "+
						" ("+this.fittingStrategy.distortionCalibrationData.gIP[numImg].pixelsXY.length+" points, diameter="+diameter+") "+		
						this.fittingStrategy.distortionCalibrationData.gIP[numImg].path);
			}
		}
		WindowTools.addScrollBars(gd);
		gd.showDialog();
		if (gd.wasCanceled()){
//			this.fittingStrategy.setImageSelection(0, oldSelection); // restore original selection in series 0
			return false;
		}
		if (this.debugLevel>0) System.out.println("Removing outlayers:");
		for (int n=0;n<indices.length;n++){
			if (gd.getNextBoolean()) {
				int numSet=indices[n];
				if (this.debugLevel>0) System.out.println(" Removing imgages in image set "+numSet);
				for (int i=0;i<imageSets[numSet].length;i++){
					int numImg=imageSets[numSet][i];
					if (this.debugLevel>0) System.out.println(n+":"+i+" "+IJ.d2s(errors[numImg],3)+" "+
							this.fittingStrategy.distortionCalibrationData.gIP[numImg].path);
					this.fittingStrategy.distortionCalibrationData.gIP[numImg].enabled=false;
					this.fittingStrategy.distortionCalibrationData.gIP[numImg].hintedMatch=-1; // so can be re-calibrated again w/o others
				}
			}
		}
		// next is not needed
		fittingStrategy.distortionCalibrationData.updateSetOrientation(null); // remove orientation information from the image set if none is enabled
//		this.fittingStrategy.setImageSelection(0, oldSelection); // restore original selection in series 0
		return true;
    }
    
    public boolean removeOutLayersJunk(int series, int numOutLayers){
    	int numSeries=fittingStrategy.getNumSeries();
    	if ((series<0) || (numOutLayers<0)) {
    		GenericDialog gd = new GenericDialog("Select series to process");
    		gd.addNumericField("Iteration number to start (0.."+(numSeries-1)+")", this.seriesNumber, 0);
    		gd.addNumericField("Number of outlayers to show", 10, 0);
    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		this.seriesNumber=          (int) gd.getNextNumber();
    		numOutLayers=               (int) gd.getNextNumber();
    	} else {
    		this.seriesNumber=series;
    	}
	    initFittingSeries(true,this.filterForAll,this.seriesNumber); // will set this.currentVector
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		double [] errors=calcErrors(calcYminusFx(this.currentfX));
		double    rms=   calcError (calcYminusFx(this.currentfX));
		boolean [] selectedImages=fittingStrategy.selectedImages();
		int numSelectedImages=0;
		for (int i=0;i<selectedImages.length;i++) if (selectedImages[i]) numSelectedImages++;
		int [] imgIndices=new int[numSelectedImages];
		int index=0;
		for (int i=0;i<selectedImages.length;i++) if ( selectedImages[i]) imgIndices[index++]=i;

		if (numOutLayers>numSelectedImages) numOutLayers=numSelectedImages;
		int [] indices=new int [numOutLayers];
		int [] indicesSelected=new int [numOutLayers];
		boolean [] availableImages=new boolean[numSelectedImages];
		for (int i=0;i<availableImages.length;i++)availableImages[i]=true;
		for (int n=0;n<numOutLayers;n++){
			double maxRMS=0;
			indices[n]=-1;
			indicesSelected[n]=-1;
			int imgIndex=0;
			for (int i=0;i<selectedImages.length;i++)if (selectedImages[i]){
				if (availableImages[imgIndex] && (errors[imgIndex]>maxRMS)){
					maxRMS=errors[imgIndex];
					indicesSelected[n]=imgIndex;
					indices[n]=i;
				}
				imgIndex++;
			}
			availableImages[indicesSelected[n]]=false;
		}
		GenericDialog gd = new GenericDialog("Select images to remove (RMS="+IJ.d2s(rms,3)+")");
		for (int n=0;n<indices.length;n++){
			gd.addCheckbox(indices[n]+": "+IJ.d2s(errors[indicesSelected[n]],3)+" "+this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].path, true);
		}
		WindowTools.addScrollBars(gd);
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		for (int n=0;n<indices.length;n++){
			if (gd.getNextBoolean()) this.fittingStrategy.distortionCalibrationData.gIP[indices[n]].enabled=false;
		}
		return true;
    }
    
	/**
	 * Opens a text window with the parameter table
	 * @param imageSelection which images include in the output
	 * @param showEyesisParameters show physical location/attitude based on Eyesis
	 * @param showIntrinsicParameters show lens distortion/alignment parameters)
	 * @param showExtrinsicParameters show position/attitude of the individual cameras
	 * @param extraDecimals add this many decimals to data 
	 */
	public void listImageParameters (boolean [] imageSelection,
    		double rms,
    		double [] errors,
    		int    [] numPairs,
    		boolean showIndex,
    		boolean showErrors,
    	    boolean showPoints,
    		boolean showLensCoordinates,
			boolean showEyesisParameters,
			boolean showIntrinsicParameters,
			boolean showExtrinsicParameters,
			int extraDecimals){
		int numImages=0;

		for (int i=0;i<fittingStrategy.distortionCalibrationData.getNumImages();i++) {
			if((imageSelection==null) || ((i<imageSelection.length) && imageSelection[i])) numImages++;
		}
		double  [][] intrinsic=new double [numImages][];
		double  [][] extrinsic=new double [numImages][];
		double  [][] lensCoordinates=new double [numImages][];
		int [] imgIndices=new int[numImages];
		int index=0;
		for (int i=0;i<fittingStrategy.distortionCalibrationData.getNumImages();i++) {
			if((imageSelection==null) || ((i<imageSelection.length) && imageSelection[i])) imgIndices[index++]=i;
		}
		for (int imgIndex=0;imgIndex<numImages;imgIndex++) {
			int imgNum=imgIndices[imgIndex]; // image number
			if (this.debugLevel>2) {
				System.out.println("listImageParameters(), imgNum="+imgNum+" calcInterParamers():");
			}
			calcInterParamers(
					this.lensDistortionParameters,
					null, //this.interParameterDerivatives, // [22][]
//					fittingStrategy.distortionCalibrationData.pars[imgNum], // 22-long parameter vector for the image
					fittingStrategy.distortionCalibrationData.getParameters(imgNum), // 22-long parameter vector for the image
					null); // if no derivatives, null is OK
//					false); // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
			intrinsic[imgIndex]=      lensDistortionParameters.getIntrinsicVector().clone();
			extrinsic[imgIndex]=      lensDistortionParameters.getExtrinsicVector().clone();
			lensCoordinates[imgIndex]=lensDistortionParameters.getLensCenterCoordinates();
		}
	
	    String header="Name\tUnits";
		for (int imgIndex=0;imgIndex<numImages;imgIndex++)
			header+="\t"+IJ.d2s(fittingStrategy.distortionCalibrationData.getImageTimestamp(imgIndices[imgIndex]),6);
	    StringBuffer sb = new StringBuffer();
	    if (showIndex) {
			sb.append("index \t");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++){
				sb.append("\t"+imgIndices[imgIndex]);
			}
			sb.append("\n");
	    	
	    }
		if (showErrors) {
				sb.append("--- RMS "+IJ.d2s(rms,3+extraDecimals)+"\tpix");
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
					int imgNum=imgIndices[imgIndex]; // image number
					sb.append("\t"+IJ.d2s(errors[imgNum],3+extraDecimals));
				}
				sb.append("\n");
		}
		if (showPoints) {
			int totalPoints=0;
			for (int imgIndex=0;imgIndex<numImages;imgIndex++){
				totalPoints+=numPairs[imgIndices[imgIndex]];
			}
			sb.append(" points "+totalPoints+"\t");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++){
				sb.append("\t"+numPairs[imgIndices[imgIndex]]);
			}
			sb.append("\n");
			sb.append(" Diameter\trel");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++){
				int imgNum=imgIndices[imgIndex]; // image number
				sb.append("\t"+IJ.d2s(this.fittingStrategy.distortionCalibrationData.gIP[imgNum].getGridDiameter(),2));
			}
			sb.append("\n");
		}
		if (showEyesisParameters) {
//			getImageSubcamera
			sb.append("Sub-camera\t");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++){
				int imgNum=imgIndices[imgIndex]; // image number
				sb.append("\t"+fittingStrategy.distortionCalibrationData.getImageSubcamera(imgNum));
			}
			sb.append("\n");
			
			for (int parNumber=0;parNumber<fittingStrategy.distortionCalibrationData.parameterDescriptions.length;parNumber++){
				sb.append(
						fittingStrategy.distortionCalibrationData.parameterDescriptions[parNumber][0]+"\t"+
						fittingStrategy.distortionCalibrationData.parameterDescriptions[parNumber][2]);
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
					int imgNum=imgIndices[imgIndex]; // image number
//					sb.append("\t"+IJ.d2s(fittingStrategy.distortionCalibrationData.pars[imgNum][parNumber],3+extraDecimals)); // TODO: make an array of decimals per parameter
					sb.append("\t"+IJ.d2s(fittingStrategy.distortionCalibrationData.getParameterValue(imgNum,parNumber),3+extraDecimals)); // TODO: make an array of decimals per parameter
				}
				sb.append("\n");
			}
		}
		if (showIntrinsicParameters) {
			sb.append("--- Intrinsic\t");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++) sb.append("\t---");
			sb.append("\n");
			for (int parNumber=0;parNumber<lensDistortionParameters.getIntrinsicNames().length;parNumber++){
				sb.append(
						lensDistortionParameters.getIntrinsicNames()[parNumber]+"\t"+
						lensDistortionParameters.getIntrinsicUnits()[parNumber]);
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
					sb.append("\t"+IJ.d2s(intrinsic[imgIndex][parNumber],3+extraDecimals)); // TODO: make an array of decimals per parameter
				}
				sb.append("\n");
			}
		}
		if (showExtrinsicParameters ||  showLensCoordinates) {
			sb.append("--- Extrinsic\t");
			for (int imgIndex=0;imgIndex<numImages;imgIndex++) sb.append("\t---");
			sb.append("\n");
	        if (showLensCoordinates){
				sb.append("Lens X(right)\tmm");
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
//					int imgNum=imgIndices[imgIndex]; // image number
					sb.append("\t"+IJ.d2s(lensCoordinates[imgIndex][0],3+extraDecimals));
				}
				sb.append("\n");
				sb.append("Lens Y(down)\tmm");
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
//					int imgNum=imgIndices[imgIndex]; // image number
					sb.append("\t"+IJ.d2s(lensCoordinates[imgIndex][1],3+extraDecimals));
				}
				sb.append("\n");
				sb.append("Lens Z(into)\tmm");
				for (int imgIndex=0;imgIndex<numImages;imgIndex++){
//					int imgNum=imgIndices[imgIndex]; // image number
					sb.append("\t"+IJ.d2s(lensCoordinates[imgIndex][2],3+extraDecimals));
				}
				sb.append("\n");
	        }
			if (showExtrinsicParameters){
				for (int parNumber=0;parNumber<lensDistortionParameters.getExtrinsicNames().length;parNumber++){
					sb.append(
							lensDistortionParameters.getExtrinsicNames()[parNumber]+"\t"+
							lensDistortionParameters.getExtrinsicUnits()[parNumber]);
					for (int imgIndex=0;imgIndex<numImages;imgIndex++){
						sb.append("\t"+IJ.d2s(extrinsic[imgIndex][parNumber],3+extraDecimals)); // TODO: make an array of decimals per parameter
					}
					sb.append("\n");
				}
			}
		}
	    new TextWindow("Camera/lens parameters", header, sb.toString(), 500,900);
	}
	
	/**
	 * Calculate differences vector
	 * @param fX vector of calculated pixelX,pixelY on the sensors
	 * @return same dimension vector of differences from this.Y (measured grid pixelxX, pixelY)
	 */
	public double [] calcYminusFx(double [] fX){
		double [] result=this.Y.clone();
		for (int i=0;i<result.length;i++) result[i]-=fX[i];
	    return result;	
	}
	/**
	 * Calcualte partial differences vector 
	 * @param fX vector of reprojected pixelX,pixelY on the sensors (number of elements - double number of points
	 * @param startIndex start index to extract (even number, twice point index)
	 * @param endIndex end index (1 greater than the last to extract)
	 * @return partial differences (measured/corrected -reprojected), twice number of points long 
	 */
	public double [] calcYminusFx(double [] fX, int startIndex, int endIndex){
		double [] result=new double [endIndex-startIndex];
		for (int i=0;i<result.length;i++) {
			int index=startIndex+i;
			result[i]=this.Y[index]-fX[index];
		}
		return result;	
	}

	
	/**
	 * Calculate the RMS from the differences vector
	 * @param diff - differences vector
	 * @return RMS for the mean error (in sensor pixels) 
	 */
	public double calcError(double [] diff){
		double result=0;
		if (this.weightFunction!=null) {
			for (int i=0;i<diff.length;i++) result+=diff[i]*diff[i]*this.weightFunction[i];
			result/=this.sumWeights;
		} else {
			for (int i=0;i<diff.length;i++) result+=diff[i]*diff[i];
			result/=diff.length;
		}
		return Math.sqrt(result)*this.RMSscale;
	}

	
	public double calcErrorDiffY(double [] fX){
		double result=0;
		if (this.weightFunction!=null) {
			for (int i=0;i<fX.length;i++){
				double diff=this.Y[i]-fX[i];
				result+=diff*diff*this.weightFunction[i];
			}
			result/=this.sumWeights;
		} else {
			for (int i=0;i<fX.length;i++){
				double diff=this.Y[i]-fX[i];
				result+=diff*diff;
			}
			result/=fX.length;
		}
		return Math.sqrt(result)*this.RMSscale;
	}
	public double calcErrorDiffY(
			double [] fX,
			double [] extraWeightedErrors,
			double [] extraWeights){
		double result=0;
		double effectiveWeight;
		if (this.weightFunction!=null) {
			effectiveWeight=this.sumWeights;
			for (int i=0;i<fX.length;i++){
				double diff=this.Y[i]-fX[i];
				result+=diff*diff*this.weightFunction[i];
			}
		} else {
			effectiveWeight=fX.length;
			for (int i=0;i<fX.length;i++){
				double diff=this.Y[i]-fX[i];
				result+=diff*diff;
			}
		}
		if ((extraWeightedErrors!=null) && (extraWeights!=null)) {
			for (int i=0;i<extraWeightedErrors.length;i++){
				result+=extraWeightedErrors[i];
				effectiveWeight+=extraWeights[i];
			}
		}
		result/=effectiveWeight;
		return Math.sqrt(result)*this.RMSscale;
	}

	public void resetBadNodes(){
		for (int imgNum=0;imgNum<fittingStrategy.distortionCalibrationData.gIP.length;imgNum++) if (fittingStrategy.distortionCalibrationData.gIP[imgNum]!=null){
			fittingStrategy.distortionCalibrationData.gIP[imgNum].resetBadNodes();
		}
	}

    public int markBadNodes(
//    		int numSeries,
    		double removeOverRMS,
    		double removeOverRMSNonweighted,
    		boolean verbose,
    		int debugLevel){
    	int debugThreshold=2;
//		this.seriesNumber=series;
    	resetBadNodes(); // before calculating weight function
	    initFittingSeries(false,this.filterForAll,this.seriesNumber); // recalculate 
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
    	int totalBadNodes=0;
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [] diff=calcYminusFx(this.currentfX);
		int index=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
			double e2 =0.0;
//			errors[imgNum]=0.0;
			if (selectedImages[imgNum]) {
				int numThisRemoved=0;
				int len=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length*2;
				double w=0;
				int nw=0;
				if (this.weightFunction!=null) {
					for (int i=index;i<index+len;i++) {
						e2+=diff[i]*diff[i]*this.weightFunction[i];
						w+=this.weightFunction[i];
						if (this.weightFunction[i]>0.0) nw++;
					}
				} else {
					for (int i=index;i<index+len;i++) {
						e2+=diff[i]*diff[i];
						w+=1.0;
						nw++;
					}
				}
				if (w>0.0) {
//					e2/=w;
					double threshold2Weighted=   2.0*removeOverRMS*removeOverRMS*e2/nw; // 2.0 because x^2+y^2
					double threshold2NonWeighted=2.0*removeOverRMSNonweighted*removeOverRMSNonweighted*e2/w; // 2.0 because x^2+y^2
					if (debugLevel>debugThreshold){
						boolean someRemoved=false;
						for (int i=index;i<index+len;i+=2) {
							e2=(diff[i]*diff[i]+ diff[i+1]*diff[i+1]);
							if ((e2>threshold2NonWeighted) || ((this.weightFunction!=null) && ((e2*this.weightFunction[i]) > threshold2Weighted )) ) {
								double ww=(this.weightFunction==null)?1.0:(this.weightFunction[i]);
								if (ww>0.0) someRemoved=true;

							}
						}
						if (someRemoved || (debugLevel>2)) System.out.println("imgNum="+imgNum+" len="+len+" e2/w="+(e2/w)+" w="+w+" e2/nw="+(e2/nw)+
								" threshold2Weighted="+threshold2Weighted+" threshold2NonWeighted="+threshold2NonWeighted);
					}

					for (int i=index;i<index+len;i+=2) {
						e2=(diff[i]*diff[i]+ diff[i+1]*diff[i+1]);
						if ((e2>threshold2NonWeighted) || ((this.weightFunction!=null) && ((e2*this.weightFunction[i]) > threshold2Weighted )) ) {
							double ww=(this.weightFunction==null)?1.0:(this.weightFunction[i]);
							int pointIndex=(i-index)/2;
							if (ww>0.0) {
								fittingStrategy.distortionCalibrationData.gIP[imgNum].setBadNode(pointIndex);
								numThisRemoved++;
								if (debugLevel>debugThreshold){
									int iu=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointIndex][0];
									int iv=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[pointIndex][1];
									System.out.println(numThisRemoved+": "+pointIndex +
											" uv="+iu+":"+iv+
											" e2="+e2+" ww="+ww+" e2w="+
											(e2*ww)+" ["+diff[i]+":"+diff[i+1]+"]");
								}
							}
						}
					}
					if (verbose && (numThisRemoved>0)) {
						System.out.println("Image "+imgNum+": removed "+numThisRemoved+" nodes over threshold");
					}
					totalBadNodes+=numThisRemoved;
				}
				index+=len;
			}
		}
    	return totalBadNodes;
    }
	public boolean showImageReprojectionErrorsDialog( int debugLevel){
	    GenericDialog gd = new GenericDialog("Show Reprojection errors for image/image set/image selection");
		gd.addNumericField("Series number for image selection (-1 - all enabled images)", -1, 0);
		gd.addNumericField("Single image number to show (<0 - do not select)", -1,0);
		gd.addNumericField("Image set number to show (<0 - do not select)", -1,0);
		gd.addCheckbox("Ask for weight function filter",     this.askFilter);
//		gd.addNumericField("Weight function filter (-1 - use default for all )",-1,0);
	    gd.showDialog();
	    if (gd.wasCanceled()) return false;
	    this.seriesNumber=        (int) gd.getNextNumber();
	    int singleImageNumber=    (int) gd.getNextNumber();
	    int imageSetNumber=       (int) gd.getNextNumber();
	    this.askFilter=                 gd.getNextBoolean();
//	    int weightFunctionFilter= (int) gd.getNextNumber();
		int filter=this.filterForAll;
		if (this.askFilter) filter=selectFilter(filter);
	    int [] imageNumbers = null;
	    if (singleImageNumber>=0){
	    	imageNumbers=new int [1];
	    	imageNumbers[0]=singleImageNumber;
	    } else if (imageSetNumber>=0){
	    	int numInSet=0;
	    	for (int nChn=0;nChn<this.fittingStrategy.distortionCalibrationData.gIS[imageSetNumber].imageSet.length;nChn++){
	    		if (this.fittingStrategy.distortionCalibrationData.gIS[imageSetNumber].imageSet[nChn]!=null) numInSet++;
	    	}
	    	imageNumbers=new int [numInSet];
	    	numInSet=0;
	    	for (int nChn=0;nChn<this.fittingStrategy.distortionCalibrationData.gIS[imageSetNumber].imageSet.length;nChn++){
	    		if (this.fittingStrategy.distortionCalibrationData.gIS[imageSetNumber].imageSet[nChn]!=null) {
	    			imageNumbers[numInSet++]=this.fittingStrategy.distortionCalibrationData.gIS[imageSetNumber].imageSet[nChn].imgNumber;
	    		}
	    	}
	    }
	    showImageReprojectionErrors(
	    		imageNumbers, // if null - use all images in a series
	    		filter, //weightFunctionFilter,
	    		debugLevel);
	    return true;
		
	}
    
    public void showImageReprojectionErrors(
    		int [] imageNumbers, // if null - use all images in a series
    		int filter,
    		int debugLevel){
    	if (filter<0) filter=this.filterForAll;
    	if (debugLevel>1) {
    		System.out.print("showImageReprojectionErrors: ");
    		if (imageNumbers!=null){
    			for (int i=0;i<imageNumbers.length;i++) System.out.print(" "+imageNumbers[i]);
    		} else {
        		System.out.println("imageNumbers is NULL");
    		}
    	}
	    initFittingSeries(false, filter,this.seriesNumber); // recalculate 
		this.currentfX=calculateFxAndJacobian(this.currentVector, false); // is it always true here (this.jacobian==null)
		boolean [] tmpSelectedImages=fittingStrategy.selectedImages();
		boolean [] selectedImages;
		double [] diff=calcYminusFx(this.currentfX);
		if ((imageNumbers!=null) && (imageNumbers.length>0)){
			selectedImages=new boolean[tmpSelectedImages.length];
			for (int i=0;i<selectedImages.length;i++) selectedImages[i]=false;
			for (int i=0;i<imageNumbers.length;i++) if ((imageNumbers[i]>=0) && (imageNumbers[i]<=selectedImages.length)){
				selectedImages[imageNumbers[i]]=tmpSelectedImages[imageNumbers[i]];
			}
		} else {
			selectedImages=tmpSelectedImages;
		}
		int width= getGridWidth();
		int height=getGridHeight();
		double [][] imgData=new double[5][height * width]; // dPX, dPY, err
		String [] titles={"dX","dY", "Err","W_Err","Weight"};
		for (int i=0;i<(width*height);i++){
			for (int c=0;c<imgData.length;c++) imgData[c][i]=Double.NaN;
		}
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]){
			int len=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;
			int index=this.imageStartIndex[imgNum]; // pair index
			for (int i=0;i<len;i++){
				int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+this.patternParameters.U0;
				int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+this.patternParameters.V0;
				int vu=u+width*v;
				double w=this.weightFunction[2*(index+i)];
				double dx=diff[2*(index+i)];
				double dy=diff[2*(index+i)+1];
				double e2=dx*dx+dy*dy;
				if (w>0.0){
					if (Double.isNaN(imgData[0][vu])) for (int c=0;c<imgData.length;c++) imgData[c][vu]=0.0;
					imgData[0][vu]+=dx*w;
					imgData[1][vu]+=dy*w;
					imgData[2][vu]+=e2*w;
					imgData[4][vu]+=w;
				}
			}
		}
		int nonEmpty=0;
	    double sumWeights=0.0;
	    for (int vu=0;vu<(width*height);vu++) if (!Double.isNaN(imgData[0][vu])){
	    	nonEmpty++;
	    	sumWeights+=imgData[4][vu];
	    }
		if ((nonEmpty==0) || (sumWeights==0.0)){
			System.out.println("showImageReprojectionErrors():  No non-empty points");
			return;
		}
		double averageWeight=sumWeights/nonEmpty;
	    for (int vu=0;vu<(width*height);vu++) if (!Double.isNaN(imgData[0][vu])){
	    	imgData[0][vu]/=imgData[4][vu];
	    	imgData[1][vu]/=imgData[4][vu];
	    	imgData[2][vu] =Math.sqrt(imgData[2][vu]/imgData[4][vu]);
	    	imgData[3][vu] =Math.sqrt(imgData[2][vu]/averageWeight);
	    }
	    String title="RPRJ";
	    int maxNumInTitle=10;
	    for (int i=0;(i<imageNumbers.length) && (i<maxNumInTitle); i++) title+="-"+imageNumbers[i];
		(new showDoubleFloatArrays()).showArrays(
				imgData,
				width,
				height,
				true,
				title,
				titles);
    }
    
    
    
    
	public double [] calcErrors(double [] diff){
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [] errors=new double [selectedImages.length];
		int index=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
			errors[imgNum]=Double.NaN; //0.0;
			if (selectedImages[imgNum]) {
				errors[imgNum]=0.0;
				int len=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length*2;
				double w=0;
				if (this.weightFunction!=null) {
					for (int i=index;i<index+len;i++) {
						errors[imgNum]+=diff[i]*diff[i]*this.weightFunction[i];
						w+=this.weightFunction[i];
					}
				} else {
					for (int i=index;i<index+len;i++) {
						errors[imgNum]+=diff[i]*diff[i];
						w+=1.0;
					}
				}
				if (w>0.0) {
					errors[imgNum]/=w;
					errors[imgNum]=Math.sqrt(errors[imgNum])*this.RMSscale;
				} else {
					errors[imgNum]=Double.NaN;
				}
				index+=len;
			}
		}
		return errors;
	}
	/**
	 * Calculate number of used grid points (x/y pairs) for each image in the current fitting series
	 * @return
	 */
	public int [] calcNumPairs(){
		boolean [] selectedImages=fittingStrategy.selectedImages();
		int [] numPairs=new int [selectedImages.length];
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
			numPairs[imgNum]=0;
			if (selectedImages[imgNum]) {
				numPairs[imgNum]=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;
			}
		}
		return numPairs;
	}
	/**
	 * Calculate corrections to the current parameter values 
	 * @param fX calculated grid pixelX, PixelY for current parameter values
	 * @param lambda damping parameter
	 * @return array of deltas to be applied to the coefficients
	 */
	public double [] solveLevenbergMarquardtOldNotUsed(double [] fX, double lambda){
		// calculate JtJ
		double [] diff=calcYminusFx(fX);
		int numPars=this.jacobian.length; // number of parameters to be adjusted
		int length=diff.length; // should be the same as this.jacobian[0].length 
	    double [][] JtByJmod=new double [numPars][numPars]; //Transposed Jacobian multiplied by Jacobian
	    double [] JtByDiff=new double [numPars];
	    for (int i=0;i<numPars;i++) for (int j=i;j<numPars;j++){
	    	JtByJmod[i][j]=0.0;
	    	if (this.weightFunction!=null)
	    		for (int k=0;k<length;k++) JtByJmod[i][j]+=this.jacobian[i][k]*this.jacobian[j][k]*this.weightFunction[k];
	    	else
	    		for (int k=0;k<length;k++) JtByJmod[i][j]+=this.jacobian[i][k]*this.jacobian[j][k];
	    }
	    for (int i=0;i<numPars;i++) { // subtract lambda*diagonal , fill the symmetrical half below the diagonal
	    	JtByJmod[i][i]+=lambda*JtByJmod[i][i]; //Marquardt mod
	    	for (int j=0;j<i;j++) JtByJmod[i][j]= JtByJmod[j][i]; // it is symmetrical matrix, just copy
	    }
	    for (int i=0;i<numPars;i++) {
	    	JtByDiff[i]=0.0;
	    	if (this.weightFunction!=null)
		    	for (int k=0;k<length;k++) JtByDiff[i]+=this.jacobian[i][k]*diff[k]*this.weightFunction[k];
	    	else
		    	for (int k=0;k<length;k++) JtByDiff[i]+=this.jacobian[i][k]*diff[k];

	    }
//	    M*Ma=Mb
	    Matrix M=new Matrix(JtByJmod);
//  public Matrix (double vals[], int m) {
/*
		if (this.debugLevel>2) {
			for (int n=0;n<fittingStrategy.distortionCalibrationData.pixelsXY.length;n++) {
				for (int i=0;i<fittingStrategy.distortionCalibrationData.pixelsXY[n].length;i++){
					System.out.println(n+":"+i+"  "+
							fittingStrategy.distortionCalibrationData.pixelsUV[n][i][0]+"/"+
							fittingStrategy.distortionCalibrationData.pixelsUV[n][i][1]+"  "+
							IJ.d2s(fittingStrategy.distortionCalibrationData.pixelsXY[n][i][0], 2)+"/"+
							IJ.d2s(fittingStrategy.distortionCalibrationData.pixelsXY[n][i][1], 2)
					);
				}
			}
		}
	    
 */
	    
		if (this.debugLevel>3) {
//		if (this.debugLevel>1) {
			System.out.println("Jt*J -lambda* diag(Jt*J), lambda="+lambda+":");
			M.print(10, 5);
		}

	    Matrix Mb=new Matrix(JtByDiff,numPars); // single column
	    if (!(new LUDecomposition(M)).isNonsingular()){
	    	double [][] arr=M.getArray();
			System.out.println("Singular Matrix "+arr.length+"x"+arr[0].length);
			// any rowsx off all 0.0?
			for (int n=0;n<arr.length;n++){
				boolean zeroRow=true;
				for (int i=0;i<arr[n].length;i++) if (arr[n][i]!=0.0){
					zeroRow=false;
					break;
				}
				if (zeroRow){
					System.out.println("Row of all zeros: "+n);
				}
			}
//			M.print(10, 5);
	    	return null;
	    }
	    
//	    Matrix Ma=M.solve(Mb); // singular
	    if (this.debugLevel>0) System.out.print("Running Cholesky decomposition...");
	    long decompositionTime=System.nanoTime();
	    Matrix Ma=M.chol().solve(Mb); // singular
	    decompositionTime=System.nanoTime()-decompositionTime;
	    if (this.debugLevel>0) System.out.println("done in "+(decompositionTime/1E9)+" sec");
	    
	    return Ma.getColumnPackedCopy();
	}

	
	
	public LMAArrays calculateJacobianArrays(double [] fX){
		// calculate JtJ
		double [] diff=calcYminusFx(fX);
		int numPars=this.jacobian.length; // number of parameters to be adjusted
		int length=diff.length; // should be the same as this.jacobian[0].length 
	    double [][] JtByJmod=new double [numPars][numPars]; //Transposed Jacobian multiplied by Jacobian
	    double [] JtByDiff=new double [numPars];
	    for (int i=0;i<numPars;i++) for (int j=i;j<numPars;j++){
	    	JtByJmod[i][j]=0.0;
	    	if (this.weightFunction!=null)
	    		for (int k=0;k<length;k++) JtByJmod[i][j]+=this.jacobian[i][k]*this.jacobian[j][k]*this.weightFunction[k];
	    	else
	    		for (int k=0;k<length;k++) JtByJmod[i][j]+=this.jacobian[i][k]*this.jacobian[j][k];
	    }
	    for (int i=0;i<numPars;i++) { // subtract lambda*diagonal , fill the symmetrical half below the diagonal
//	    	JtByJmod[i][i]+=lambda*JtByJmod[i][i]; //Marquardt mod
	    	for (int j=0;j<i;j++) JtByJmod[i][j]= JtByJmod[j][i]; // it is symmetrical matrix, just copy
	    }
	    for (int i=0;i<numPars;i++) {
	    	JtByDiff[i]=0.0;
	    	if (this.weightFunction!=null)
		    	for (int k=0;k<length;k++) JtByDiff[i]+=this.jacobian[i][k]*diff[k]*this.weightFunction[k];
	    	else
		    	for (int k=0;k<length;k++) JtByDiff[i]+=this.jacobian[i][k]*diff[k];

	    }
	    
   		LMAArrays lMAArrays = new LMAArrays();
   		lMAArrays.jTByJ=JtByJmod;
   		lMAArrays.jTByDiff=JtByDiff;
   		return lMAArrays;
/*	    
	    
//	    M*Ma=Mb
	    Matrix M=new Matrix(JtByJmod);
//  public Matrix (double vals[], int m) {
	    
		if (this.debugLevel>2) {
//		if (this.debugLevel>1) {
			System.out.println("Jt*J -lambda* diag(Jt*J), lambda="+lambda+":");
			M.print(10, 5);
		}

	    Matrix Mb=new Matrix(JtByDiff,numPars); // single column
	    if (!(new LUDecomposition(M)).isNonsingular()){
			System.out.println("Singular Matrix");
			M.print(10, 5);
	    	return null;
	    }
	    Matrix Ma=M.solve(Mb); // singular
	    return Ma.getColumnPackedCopy();
*/	    
	}
	
	
	
	public LMAArrays calculateJacobianArrays (
			final boolean [] selectedImages, // selected images to process
			final double [] Y,  // should be initialized 
			final double [] fX, // should be initialized to correct length, data is not needed
			final double [] vector,  // parameters vector
			final int    [] imageStartIndex, // index of the first point of each image (including extra element in the end so n+1 is always valid)
			final double [][] patternXYZ, // this.targetXYZ
			final double [] weightFunction, // may be null - make it twice smaller? - same for X and Y?
			final LensDistortionParameters lensDistortionParametersProto,
//			final double lambda,
			int threadsMax,
			boolean updateStatus){
		
		// calculate JtJ
//		double [] diff=calcYminusFx(fX);
//		int numPars=this.jacobian.length; // number of parameters to be adjusted
		final int numPars=vector.length; // number of parameters to be adjusted
//		int length=diff.length; // should be the same as this.jacobian[0].length 
//		final int length=fX.length; // should be the same as this.jacobian[0].length 
		final double [][] JtByJmod=new double [numPars][numPars]; //Transposed Jacobian multiplied by Jacobian
		final double [] JtByDiff=new double [numPars];
	    for (int i=0;i<numPars;i++){
	    	JtByDiff[i]=0.0;
	    	for (int j=0;j<numPars;j++) JtByJmod[i][j]=0.0;
	    }
	    final int debugLevel=this.debugLevel;
   		final Thread[] threads = newThreadArray(threadsMax);
   		final AtomicInteger imageNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger imageFinishedAtomic = new AtomicInteger(0);
   		final double [] progressValues=new double [selectedImages.length];
   		int numSelectedImages=0;
   		for (int i=0;i<selectedImages.length;i++) if (selectedImages[i]) numSelectedImages++;
   		int selectedIndex=0;
   		for (int i=0;i<selectedImages.length;i++) {
   			progressValues[i]=(selectedIndex+1.0)/numSelectedImages;
   			if (selectedImages[i]) selectedIndex++;
   			if (selectedIndex>=numSelectedImages) selectedIndex--;
   		}
   		final AtomicInteger stopRequested=this.stopRequested;
		final AtomicBoolean interruptedAtomic=new AtomicBoolean();
   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				public void run() {
   					LensDistortionParameters lensDistortionParameters=lensDistortionParametersProto.clone(); // see - if that is needed - maybe new is OK
   					//   					LensDistortionParameters lensDistortionParameters= new LensDistortionParameters();
   					for (int numImage=imageNumberAtomic.getAndIncrement(); (numImage<selectedImages.length) && !interruptedAtomic.get();numImage=imageNumberAtomic.getAndIncrement()){
   						double [][] partialJacobian= calculatePartialFxAndJacobian(
   								numImage,      // number of grid image
   								vector,  // parameters vector
   								patternXYZ, // this.targetXYZ
   								fX,     // non-overlapping segments will be filled
   								imageStartIndex, // start index in patternXYZ array (length - difference to the next, includes extra last element)
   								lensDistortionParameters, // initialize one per each tread? Or for each call?
   								true); // when false, modifies only this.lensDistortionParameters.*

   						int length=2*(imageStartIndex[numImage+1]-imageStartIndex[numImage]);
   						int start= 2*imageStartIndex[numImage];

   						double [][] partialJtByJmod=new double [numPars][numPars]; // out of heap space
   						double []   partialJtByDiff=new double [numPars];
 
   						for (int i=0;i<numPars;i++) if (partialJacobian[i]!=null) {
   							for (int j=i;j<numPars;j++) if (partialJacobian[j]!=null) {
   								partialJtByJmod[i][j]=0.0;
   								if (weightFunction!=null) {
   									for (int k=0;k<length;k++) partialJtByJmod[i][j]+=partialJacobian[i][k]*partialJacobian[j][k]*weightFunction[start+k];
   								} else {
   									for (int k=0;k<length;k++) partialJtByJmod[i][j]+=partialJacobian[i][k]*partialJacobian[j][k];
   								}
   							}	
   						}

   						double [] partialDiff=new double[length];
   						for (int k=0;k<length;k++) 	partialDiff[k]=Y[start+k]-fX[start+k];

   						for (int i=0;i<numPars;i++) if (partialJacobian[i]!=null) {
   							partialJtByDiff[i]=0.0;
   							if (weightFunction!=null)
   								for (int k=0;k<length;k++) partialJtByDiff[i]+=partialJacobian[i][k]*partialDiff[k]*weightFunction[start+k];
   							else
   								for (int k=0;k<length;k++) partialJtByDiff[i]+=partialJacobian[i][k]*partialDiff[k];

   						}
   						// wrong! fix it
/*   						
   						synchronized(this){
   							for (int i=0;i<numPars;i++) if (partialJacobian[i]!=null){
   								JtByDiff[i]+=partialJtByDiff[i];
   								for (int j=i;j<numPars;j++) JtByJmod[i][j]+=partialJtByJmod[i][j];
   							}
   						}
 */  						
   						synchronizedCombinePartialJacobians(
   								JtByJmod, //Transposed Jacobian multiplied by Jacobian
   								JtByDiff,
   								partialJacobian,
   								partialJtByDiff,
   								partialJtByJmod,
   								numPars	);
   						
   						final int numFinished=imageFinishedAtomic.getAndIncrement();
//   						IJ.showProgress(progressValues[numFinished]);
   						SwingUtilities.invokeLater(new Runnable() {
   							public void run() {
   								// Here, we can safely update the GUI
   								// because we'll be called from the
   								// event dispatch thread
   								IJ.showProgress(progressValues[numFinished]);
   							}
   						});
   						if (stopRequested.get()==1){ // ASAP
   							interruptedAtomic.set(true);
   						}
//   						if (debugLevel>1) System.out.println("IJ.showProgress("+progressValues[numImage]+")");
//   						if (debugLevel>1) IJ.showStatus("Progress "+IJ.d2s(100*progressValues[numImage],2)+"%");
   					}
   				}
   			};
   		}
   		startAndJoin(threads);
   		if (interruptedAtomic.get()) {
   			System.out.println("calculateJacobianArrays() aborted by user request");
   			return null;
   		}
   		if (debugLevel>3){
   			String msg="calculateJacobianArrays() ALL_trace=";
   			for (int ii=0;ii<numPars;ii++) msg+=IJ.d2s(JtByJmod[ii][ii],5);
   			System.out.println(msg);

   		}

   		for (int i=0;i<numPars;i++) { // subtract lambda*diagonal , fill the symmetrical half below the diagonal
   			for (int j=0;j<i;j++) JtByJmod[i][j]= JtByJmod[j][i]; // it is symmetrical matrix, just copy
   		}
   		LMAArrays lMAArrays = new LMAArrays();
   		lMAArrays.jTByJ=JtByJmod;
   		lMAArrays.jTByDiff=JtByDiff;
   		if (debugLevel>3){
   			String msg="calculateJacobianArrays() lMAArrays.jTByJ trace=";
   			for (int ii=0;ii<numPars;ii++) msg+=IJ.d2s(lMAArrays.jTByJ[ii][ii],5);
   			System.out.println(msg);

   		}
   		return lMAArrays;
	}

	public synchronized void synchronizedCombinePartialJacobians(
			double [][] JtByJmod, //Transposed Jacobian multiplied by Jacobian
			double []   JtByDiff,
			double [][] partialJacobian,
			double []   partialJtByDiff,
			double [][] partialJtByJmod,
			int numPars
	){
		for (int i=0;i<numPars;i++) if (partialJacobian[i]!=null){
			JtByDiff[i]+=partialJtByDiff[i];
			for (int j=i;j<numPars;j++) JtByJmod[i][j]+=partialJtByJmod[i][j];
		}
	}

	
	
	
	public double [] solveLMA(
			LMAArrays lMAArrays,
			double lambda){
		double [][] JtByJmod= lMAArrays.jTByJ.clone();
		int numPars=JtByJmod.length;
		for (int i=0;i<numPars;i++){
			JtByJmod[i]=lMAArrays.jTByJ[i].clone();
   			JtByJmod[i][i]+=lambda*JtByJmod[i][i]; //Marquardt mod
		}
//	    M*Ma=Mb
	    Matrix M=new Matrix(JtByJmod);
//  public Matrix (double vals[], int m) {
	    
		if (this.debugLevel>2) {
//		if (this.debugLevel>1) {
			System.out.println("Jt*J -lambda* diag(Jt*J), lambda="+lambda+":");
			M.print(10, 5);
		}

	    Matrix Mb=new Matrix(lMAArrays.jTByDiff,numPars); // single column
	    if (!(new LUDecomposition(M)).isNonsingular()){
	    	double [][] arr=M.getArray();
			System.out.println("Singular Matrix "+arr.length+"x"+arr[0].length);
			// any rowsx off all 0.0?
			for (int n=0;n<arr.length;n++){
				boolean zeroRow=true;
				for (int i=0;i<arr[n].length;i++) if (arr[n][i]!=0.0){
					zeroRow=false;
					break;
				}
				if (zeroRow){
					System.out.println("Row of all zeros: "+n);
				}
			}
//			M.print(10, 5);
	    	return null;
	    }
	    Matrix Ma=M.solve(Mb); // singular
	    return Ma.getColumnPackedCopy();
		
	}
	
	
	/**
	 * Calculates  next parameters vector, holds some arrays
	 * @param numSeries
	 * @return array of two booleans: { improved, finished}
	 */
	public boolean [] stepLevenbergMarquardtFirst(int numSeries){
		double [] deltas=null;
		if (this.currentVector==null) {
			int filter=this.filterForAll;
			if (this.askFilter) filter=selectFilter(filter);
			initFittingSeries(false,filter,numSeries); // first step in series
			this.currentRMS=-1;
			this.currentRMSPure=-1;
			this.currentfX=null; // invalidate
			this.jacobian=null;  // invalidate
			this.lMAArrays=null;
			lastImprovements[0]=-1.0;
			lastImprovements[1]=-1.0;
		}
		// calculate  this.currentfX, this.jacobian if needed
		if (this.debugLevel>2) {
			System.out.println("this.currentVector");
			for (int i=0;i<this.currentVector.length;i++){
				System.out.println(i+": "+ this.currentVector[i]);
			}
		}
		//    	if ((this.currentfX==null)|| ((this.jacobian==null) && !this.threadedLMA )) {
		if ((this.currentfX==null)|| (this.lMAArrays==null)) {
			if (this.updateStatus){
//				IJ.showStatus(this.seriesNumber+": "+"Step #"+this.iterationStepNumber+" RMS="+IJ.d2s(this.currentRMS,8)+ " ("+IJ.d2s(this.firstRMS,8)+")");
				IJ.showStatus(this.seriesNumber+": initial Jacobian matrix calculation. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
			}
			if (this.debugLevel>1) {
				System.out.println(this.seriesNumber+": initial Jacobian matrix calculation. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
			}
    		if (this.threadedLMA) {
    			this.currentfX=new double[this.Y.length];
    			//    			deltas=solveLevenbergMarquardtThreaded(
    			this.lMAArrays=calculateJacobianArrays(
    					this.fittingStrategy.selectedImages(), // selected images to process
    					this.Y,  // should be initialized 
    					this.currentfX, // should be initialized to correct length, data is not needed
    					this.currentVector,  // parameters vector
    					this.imageStartIndex, // index of the first point of each image (including extra element in the end so n+1 is always valid)
    					this.targetXYZ, // this.targetXYZ
    					this.weightFunction, // may be null - make it twice smaller? - same for X and Y?
    					this.lensDistortionParameters,
    					//    					this.lambda,
    					this.threadsMax,
    					this.updateStatus);
    			if (this.lMAArrays == null) {
    				return null ; // aborted
    			}
    		} else {
    			this.currentfX=calculateFxAndJacobian(this.currentVector, true); // is it always true here (this.jacobian==null)
    			this.lMAArrays=calculateJacobianArrays(this.currentfX);
//    			deltas=solveLevenbergMarquardt(this.currentfX,this.lambda);
    		}
    		// add termes that push selected extrinsic parameters towards average (global, per station, per tilt-station)
    		this.currentRMSPure= calcErrorDiffY(this.currentfX);
    	   	if ((this.fittingStrategy.varianceModes!=null) && (this.fittingStrategy.varianceModes[numSeries]!=this.fittingStrategy.varianceModeDisabled)) {
    	   		this.fittingStrategy.addVarianceToLMA(
    	    			numSeries,
    	    			this.currentVector,
    	    			this.lMAArrays.jTByJ, // jacobian multiplied by Jacobian transposed (or null)
    	    			this.lMAArrays.jTByDiff);
    	   		this.currentRMS= calcErrorDiffY(
    	   				this.currentfX,
    	   				this.fittingStrategy.getVarianceError2(), //double [] extraWeightedErrors,
    	   				this.fittingStrategy.getWeights()); //double [] extraWeights);
    			if (this.updateStatus){
    				IJ.showStatus(this.seriesNumber+": initial RMS="+IJ.d2s(this.currentRMS,8)+
    						" ("+IJ.d2s(this.currentRMSPure,8)+")"+
    						". Calculating next Jacobian. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
    			}
    			if (this.debugLevel>1) {
    				System.out.println(this.seriesNumber+": initial RMS="+IJ.d2s(this.currentRMS,8)+
    						" ("+IJ.d2s(this.currentRMSPure,8)+")"+
    						". Calculating next Jacobian. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
    			}

    	   	} else {
        		this.currentRMS= this.currentRMSPure;
    			if (this.updateStatus){
    				IJ.showStatus(this.seriesNumber+": initial RMS="+IJ.d2s(this.currentRMS,8)+
    						". Calculating next Jacobian. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
    			}
    			if (this.debugLevel>1) {
    				System.out.println(this.seriesNumber+": initial RMS="+IJ.d2s(this.currentRMS,8)+
    						". Calculating next Jacobian. Points:"+this.Y.length+" Parameters:"+this.currentVector.length);
    			}
    	   	}
    	} else {
    		this.currentRMSPure= calcErrorDiffY(this.currentfX);
    	   	if ((this.fittingStrategy.varianceModes!=null) && (this.fittingStrategy.varianceModes[numSeries]!=this.fittingStrategy.varianceModeDisabled)) {
    	   		this.fittingStrategy.addVarianceToLMA(// recalculating as this may keep from nextVector (or just being restored)
    	    			numSeries,
    	    			this.currentVector,
    	    			null, //this.lMAArrays.jTByJ, // jacobian multiplied by Jacobian transposed (or null)
    	    			null); //this.lMAArrays.jTByDiff);

    	   		this.currentRMS= calcErrorDiffY(
    	   				this.currentfX,
    	   				this.fittingStrategy.getVarianceError2(), //double [] extraWeightedErrors,
    	   				this.fittingStrategy.getWeights()); //double [] extraWeights);
    	   	} else {
        		this.currentRMS= this.currentRMSPure;
    	   	}
    		
    	}
//		this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.firstRMS<0) {
    		this.firstRMS=this.currentRMS;
    		this.firstRMSPure=this.currentRMSPure;
    	}
// calculate deltas
//    	double [] deltas=solveLevenbergMarquardt(this.currentfX,fittingStrategy.getLambda());
    	
		deltas=solveLMA(this.lMAArrays,	this.lambda	);
		
    	boolean matrixNonSingular=true;
    	if (deltas==null) {
    		deltas=new double[this.currentVector.length];
    		for (int i=0;i<deltas.length;i++) deltas[i]=0.0;
    		matrixNonSingular=false;
    	}
		if (this.debugLevel>2) {
			System.out.println("deltas");
			for (int i=0;i<deltas.length;i++){
				System.out.println(i+": "+ deltas[i]);
			}
		}
// apply deltas    	
    	this.nextVector=this.currentVector.clone();
    	for (int i=0;i<this.nextVector.length;i++) this.nextVector[i]+=deltas[i];
// another option - do not calculate J now, just fX. and late - calculate both if it was improvement    	
//    	save current Jacobian
		if (this.debugLevel>2) {
			System.out.println("this.nextVector");
			for (int i=0;i<this.nextVector.length;i++){
				System.out.println(i+": "+ this.nextVector[i]);
			}
		}

//        this.savedJacobian=this.jacobian;
        this.savedLMAArrays=lMAArrays.clone();
        this.jacobian=null; // not needed, just to catch bugs
// calculate next vector and Jacobian  (this.jacobian)  	
//    	this.nextfX=calculateFxAndJacobian(this.nextVector,true); //=========== OLD
    	
		if (this.threadedLMA) {
			this.nextfX=new double[this.Y.length];
			//    			deltas=solveLevenbergMarquardtThreaded(
			this.lMAArrays=calculateJacobianArrays(
					this.fittingStrategy.selectedImages(), // selected images to process
					this.Y,  // should be initialized 
					this.nextfX, // should be initialized to correct length, data is not needed
					this.nextVector,  // parameters vector
					this.imageStartIndex, // index of the first point of each image (including extra element in the end so n+1 is always valid)
					this.targetXYZ, // this.targetXYZ
					this.weightFunction, // may be null - make it twice smaller? - same for X and Y?
					this.lensDistortionParameters,
					//    					this.lambda,
					this.threadsMax,
					this.updateStatus);
			if (this.lMAArrays == null) {
				return null ; // aborted
			}
		} else {
	    	this.nextfX=calculateFxAndJacobian(this.nextVector,true);
			this.lMAArrays=calculateJacobianArrays(this.nextfX);
		}
//		this.nextRMS=calcErrorDiffY(this.nextfX);
		
		this.nextRMSPure= calcErrorDiffY(this.nextfX);
	   	if ((this.fittingStrategy.varianceModes!=null) && (this.fittingStrategy.varianceModes[numSeries]!=this.fittingStrategy.varianceModeDisabled)) {
	   		this.fittingStrategy.addVarianceToLMA(
	    			numSeries,
	    			this.nextVector,
	    			this.lMAArrays.jTByJ, // jacobian multiplied by Jacobian transposed (or null)
	    			this.lMAArrays.jTByDiff);
	   		this.nextRMS= calcErrorDiffY(
	   				this.nextfX,
	   				this.fittingStrategy.getVarianceError2(), //double [] extraWeightedErrors,
	   				this.fittingStrategy.getWeights()); //double [] extraWeights);
	   	} else {
	   		this.nextRMS= this.nextRMSPure;
	   	}

		this.lastImprovements[1]=this.lastImprovements[0];
		this.lastImprovements[0]=this.currentRMS-this.nextRMS;
		if (this.debugLevel>2) {
			System.out.println("stepLMA this.currentRMS="+this.currentRMS+
					", this.currentRMSPure="+this.currentRMSPure+
					", this.nextRMS="+this.nextRMS+
					", this.nextRMSPure="+this.nextRMSPure+
					", delta="+(this.currentRMS-this.nextRMS)+
					", deltaPure="+(this.currentRMSPure-this.nextRMSPure));
		}
		boolean [] status={matrixNonSingular && (this.nextRMS<=this.currentRMS),!matrixNonSingular};
		// additional test if "worse" but the difference is too small, it was be caused by computation error, like here:
		//stepLevenbergMarquardtAction() step=27, this.currentRMS=0.17068403807026408,   this.nextRMS=0.1706840380702647
		
		if (!status[0] && matrixNonSingular) {
			if (this.nextRMS<(this.currentRMS+this.currentRMS*this.thresholdFinish*0.01)) {
				this.nextRMS=this.currentRMS;
				this.nextRMSPure=this.currentRMSPure;
				status[0]=true;
				status[1]=true;
				this.lastImprovements[0]=0.0;
				if (this.debugLevel>1) {
					System.out.println("New RMS error is larger than the old one, but the difference is too small to be trusted ");
					System.out.println(
							"stepLMA this.currentRMS="+this.currentRMS+
							", this.currentRMSPure="+this.currentRMSPure+
							", this.nextRMS="+this.nextRMS+
							", this.nextRMSPure="+this.nextRMSPure+
							", delta="+(this.currentRMS-this.nextRMS)+
							", deltaPure="+(this.currentRMSPure-this.nextRMSPure));
				}
				
			}
		}
    	if (status[0] && matrixNonSingular) { //improved
    		status[1]=(this.iterationStepNumber>this.numIterations) || ( // done
    				(this.lastImprovements[0]>=0.0) &&
    				(this.lastImprovements[0]<this.thresholdFinish*this.currentRMS) &&
    				(this.lastImprovements[1]>=0.0) &&
    				(this.lastImprovements[1]<this.thresholdFinish*this.currentRMS));
    	} else if (matrixNonSingular){
//    		this.jacobian=this.savedJacobian;// restore saved Jacobian
    		this.lMAArrays=this.savedLMAArrays; // restore Jt*J and Jt*diff
    		
    		status[1]=(this.iterationStepNumber>this.numIterations) || // failed
    		((this.lambda*this.lambdaStepUp)>this.maxLambda);
    	}
///this.currentRMS    	
//TODO: add other failures leading to result failure?    	
		if (this.debugLevel>2) {
			System.out.println("stepLevenbergMarquardtFirst("+numSeries+")=>"+status[0]+","+status[1]);
		}
		return status;
    }
    /**
     * Apply fitting step 
     */
    public void stepLevenbergMarquardtAction(){// 
    	this.iterationStepNumber++;
// apply/revert,modify lambda    	
		if (this.debugLevel>1) {
			System.out.println(
					"stepLevenbergMarquardtAction() step="+this.iterationStepNumber+
					", this.currentRMS="+this.currentRMS+
					", this.currentRMSPure="+this.currentRMSPure+
					", this.nextRMS="+this.nextRMS+
					", this.nextRMSPure="+this.nextRMSPure+
					" lambda="+this.lambda+" at "+IJ.d2s(0.000000001*(System.nanoTime()-this.startTime),3)+" sec");
		}
    	if (this.nextRMS<this.currentRMS) { //improved
    		this.lambda*=this.lambdaStepDown;
    		this.currentRMS=this.nextRMS;
    		this.currentRMSPure=this.nextRMSPure;
    		this.currentfX=this.nextfX;
    		this.currentVector=this.nextVector;
    	} else {
    		this.lambda*=this.lambdaStepUp;
//    		this.jacobian=this.savedJacobian;// restore saved Jacobian
    		this.lMAArrays=this.savedLMAArrays; // restore Jt*J and Jt*diff
    	}
    }
    
    /**
     * Dialog to select Levenberg-Marquardt algorithm and related parameters
     * @return true if OK, false if canceled
     */
    public boolean selectLMAParameters(){
    	int numSeries=fittingStrategy.getNumSeries();
	    GenericDialog gd = new GenericDialog("Levenberg-Marquardt algorithm parameters for cameras distortions/locations");
		gd.addNumericField("Iteration number to start (0.."+(numSeries-1)+")", this.seriesNumber, 0);
		gd.addNumericField("Initial LMA Lambda ",            this.lambda, 5);
		gd.addNumericField("Multiply lambda on success",     this.lambdaStepDown, 5);
		gd.addNumericField("Threshold RMS to exit LMA",      this.thresholdFinish, 7,9,"pix");
		gd.addNumericField("Multiply lambda on failure",     this.lambdaStepUp, 5);
		gd.addNumericField("Threshold lambda to fail",       this.maxLambda, 5);
		gd.addNumericField("Maximal number of iterations",   this.numIterations, 0);

		gd.addCheckbox("Dialog after each iteration step",   this.stopEachStep);
		gd.addCheckbox("Dialog after each iteration series", this.stopEachSeries);
		gd.addCheckbox("Dialog after each failure",          this.stopOnFailure);
		gd.addCheckbox("Ask for weight function filter",     this.askFilter);
		
		gd.addCheckbox("Show modified parameters",           this.showParams);
		gd.addCheckbox("Show debug images before correction",this.showThisImages);
		gd.addCheckbox("Show debug images after correction", this.showNextImages);
		gd.addNumericField("Maximal number of threads",   this.threadsMax, 0);
		gd.addCheckbox("Use memory-saving/multithreaded version", this.threadedLMA);
	    gd.showDialog();
	    if (gd.wasCanceled()) return false;
	    this.seriesNumber=     (int) gd.getNextNumber();
		this.lambda=                 gd.getNextNumber();
		this.lambdaStepDown=         gd.getNextNumber();
		this.thresholdFinish=        gd.getNextNumber();
		this.lambdaStepUp=           gd.getNextNumber();
		this.maxLambda=              gd.getNextNumber();
		this.numIterations=    (int) gd.getNextNumber();
		this.stopEachStep=           gd.getNextBoolean();
		this.stopEachSeries=         gd.getNextBoolean();
		this.stopOnFailure=          gd.getNextBoolean();
		this.askFilter=              gd.getNextBoolean();
		this.showParams=             gd.getNextBoolean();
		this.showThisImages=         gd.getNextBoolean();
		this.showNextImages=         gd.getNextBoolean();
		this.threadsMax=       (int) gd.getNextNumber();
		this.threadedLMA=            gd.getNextBoolean();
	    return true;
    }
    
    
    public boolean dialogLMAStep(boolean [] state){
    	String [] states={
    			"Worse, increase lambda",
    			"Better, decrease lambda",
    			"Failed to fit",
    			"Fitting Successful"};
    	int iState=(state[0]?1:0)+(state[1]?2:0);
    
	    GenericDialog gd = new GenericDialog("Levenberg-Marquardt algorithm step");
    	String [][] parameterDescriptions=fittingStrategy.distortionCalibrationData.parameterDescriptions;
    	gd.addMessage("Current state="+states[iState]);
    	gd.addMessage("Iteration step="+this.iterationStepNumber);
    	
    	gd.addMessage("Initial RMS="+IJ.d2s(this.firstRMS,6)+", Current RMS="+IJ.d2s(this.currentRMS,6)+", new RMS="+IJ.d2s(this.nextRMS,6));
    	gd.addMessage("Pure initial RMS="+IJ.d2s(this.firstRMSPure,6)+", Current RMS="+IJ.d2s(this.currentRMSPure,6)+", new RMS="+IJ.d2s(this.nextRMSPure,6));
    	if (this.showParams) {
    		for (int i=0;i<this.currentVector.length;i++){
    			int parNum=fittingStrategy.parameterMap[i][1];
    			int imgNum=fittingStrategy.parameterMap[i][0];
    			double delta= this.nextVector[i] - this.currentVector[i];
    			gd.addMessage(i+": "+parameterDescriptions[parNum][0]+
    					"["+imgNum+"]("+parameterDescriptions[parNum][2]+") "+IJ.d2s(this.currentVector[i],3)+
    					" + "+IJ.d2s(delta,3)+" = "+IJ.d2s(this.nextVector[i],3));
    		}
    	}
		gd.addNumericField("Lambda ",                        this.lambda, 5);
		gd.addNumericField("Multiply lambda on success",     this.lambdaStepDown, 5);
		gd.addNumericField("Threshold RMS to exit LMA",      this.thresholdFinish, 7,9,"pix");
		gd.addNumericField("Multiply lambda on failure",     this.lambdaStepUp, 5);
		gd.addNumericField("Threshold lambda to fail",       this.maxLambda, 5);
		gd.addNumericField("Maximal number of iterations",   this.numIterations, 0);

		gd.addCheckbox("Dialog after each iteration step",   this.stopEachStep);
		gd.addCheckbox("Dialog after each iteration series", this.stopEachSeries);
		gd.addCheckbox("Dialog after each failure",          this.stopOnFailure);
		gd.addCheckbox("Show modified parameters",           this.showParams);
		gd.addCheckbox("Show debug images before correction",this.showThisImages);
		gd.addCheckbox("Show debug images after correction", this.showNextImages);
		gd.addMessage("Done will save the current (not new!) state and exit, Continue will proceed according to LMA");
		gd.enableYesNoCancel("Continue", "Done");
		WindowTools.addScrollBars(gd);

	    gd.showDialog();
	    if (gd.wasCanceled()) {
	    	this.saveSeries=false;
	    	return false;
	    }
		this.lambda=                 gd.getNextNumber();
		this.lambdaStepDown=         gd.getNextNumber();
		this.thresholdFinish=        gd.getNextNumber();
		this.lambdaStepUp=           gd.getNextNumber();
		this.maxLambda=              gd.getNextNumber();
		this.numIterations=    (int) gd.getNextNumber();
		this.stopEachStep=           gd.getNextBoolean();
		this.stopEachSeries=         gd.getNextBoolean();
		this.stopOnFailure=          gd.getNextBoolean();
		this.showParams=             gd.getNextBoolean();
		this.showThisImages=         gd.getNextBoolean();
		this.showNextImages=         gd.getNextBoolean();
	    this.saveSeries=true;
	    return gd.wasOKed();
    }
  
    public boolean modifyGrid(
    		DistortionCalibrationData distortionCalibrationData,
			int threadsMax,
			boolean updateStatus){
    	if (fittingStrategy==null) {
    		String msg="Fitting strategy does not exist, exiting";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (distortionCalibrationData.sensorMasks==null){
    		String msg="Sensor mask(s) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (distortionCalibrationData.eyesisCameraParameters==null){
    		String msg="Eyesis camera parameters (and sensor dimensions) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
//    	if (! selectGridEnhanceParameters()) return false;
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0xdc0, fittingStrategy.getNumSeries());
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0x31cc0, (this.seriesNumber>=0)?this.seriesNumber:0); // 0x1dco with show result, but we can not show it easily
// todo - add and implement 0x10000 to show just one individual image    	
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0x21cc0, (this.seriesNumber>=0)?this.seriesNumber:0); // 0x1dco with show result, but we can not show it easily
    	int series=refineParameters.showDialog(
    			"Select Grid Tuning Parameters",
    			0x61000,
    			((this.seriesNumber>=0)?this.seriesNumber:0),
    			null); // averageRGB - only for target flat-field correction
    	
    	
    	if (series<0) return false;
    	this.seriesNumber=series;
		
		int filter=this.filterForTargetGeometry;
		if (this.askFilter) filter=selectFilter(filter);
    	initFittingSeries(true,filter,this.seriesNumber); // first step in series
//    	initFittingSeries(true,this.filterForTargetGeometry, this.seriesNumber); // first step in series
    	this.currentfX=calculateFxAndJacobian(this.currentVector, false);
    	//        	this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.debugLevel>2) {
    		System.out.println("this.currentVector");
    		for (int i=0;i<this.currentVector.length;i++){
    			System.out.println(i+": "+ this.currentVector[i]);
    		}
    	}
    	if (this.showThisImages) showDiff (this.currentfX, "residual-series-"+this.seriesNumber);
    	if (this.refineParameters.resetVariations) {
    		this.patternParameters.resetStationZCorr();
    	}
    	double [][][] correctionCombo= calculateGridXYZCorr3D(
    			this.refineParameters.variationPenalty,
    			this.refineParameters.fixXY,
                this.refineParameters.useVariations?(this.fittingStrategy.zGroups[this.seriesNumber]):null, //stationGroups,
				this.refineParameters.grid3DCorrection,
				this.refineParameters.rotateCorrection,
				this.refineParameters.grid3DMaximalZCorr, //20.0,
				this.refineParameters.noFallBack,
				this.refineParameters.targetShowPerImage,
				threadsMax,
				updateStatus);
    	double [][] gridXYZCorr=correctionCombo[0];
		double [][] gridZCorr3d =correctionCombo[1];
		double [][] gridZCorr3dWeight =correctionCombo[2];
		String [] titles={"X-correction(mm)","Y-correction(mm)","Z-correction","Weight"};
    	String [] titlesStations=new String [2*gridZCorr3d.length];
    	for (int i=0;i<gridZCorr3d.length;i++){
    		titlesStations[i]="Z_"+i;
    		titlesStations[i+gridZCorr3d.length]="W_"+i;
    	}
    	if (this.refineParameters.targetShowThisCorrection) {
    		if (this.debugLevel>1){
    			double [][] debugData=new double [2*gridZCorr3d.length][];
    	    	for (int i=0;i<gridZCorr3d.length;i++){
    	    		debugData[i]=gridZCorr3d[i];
    	    		debugData[i+gridZCorr3d.length]=gridZCorr3dWeight[i];
    	    	}
        		this.SDFA_INSTANCE.showArrays(debugData, getGridWidth(), getGridHeight(),  true, "Z corrections", titlesStations);
    		}
    	}


// TODO: make configurable and optional		
		shrinkExtrapolateGridCorrection(
				gridXYZCorr, // dx,dy,dz, mask >0
				gridZCorr3d,
				getGridWidth(),
				1, //preShrink,
				5, // expand,
				3.0, //  sigma,
				2.0); //double ksigma
		
    	if (this.refineParameters.targetShowThisCorrection) {
    		this.SDFA_INSTANCE.showArrays(gridXYZCorr, getGridWidth(), getGridHeight(),  true, "Grid corrections", titles);
    		if (this.debugLevel>1){
    			
    		}
    	}
    	if (!this.refineParameters.targetApplyCorrection) return false;
    	patternParameters.applyGridCorrection(gridXYZCorr, this.refineParameters.targetCorrectionScale);
    	patternParameters.applyZGridCorrection(gridZCorr3d, this.refineParameters.targetCorrectionScale);
    	return true;
    }

    public boolean modifyGrid0(DistortionCalibrationData distortionCalibrationData){
    	if (fittingStrategy==null) {
    		String msg="Fitting strategy does not exist, exiting";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (distortionCalibrationData.sensorMasks==null){
    		String msg="Sensor mask(s) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (distortionCalibrationData.eyesisCameraParameters==null){
    		String msg="Eyesis camera parameters (and sensor dimensions) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
//    	if (! selectGridEnhanceParameters()) return false;
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0xdc0, fittingStrategy.getNumSeries());
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0x31cc0, (this.seriesNumber>=0)?this.seriesNumber:0); // 0x1dco with show result, but we can not show it easily
// todo - add and implement 0x10000 to show just one individual image    	
//    	int series=refineParameters.showDialog("Select Grid Tuning Parameters", 0x21cc0, (this.seriesNumber>=0)?this.seriesNumber:0); // 0x1dco with show result, but we can not show it easily
    	int series=refineParameters.showDialog(
    			"Select Grid Tuning Parameters",
    			0x61000,
    			((this.seriesNumber>=0)?this.seriesNumber:0),
    			null); // averageRGB - only for target flat-field correction
    	
    	
    	if (series<0) return false;
    	this.seriesNumber=series;
    	
    	
    	
		int filter=this.filterForTargetGeometry;
		if (this.askFilter) filter=selectFilter(filter);
    	initFittingSeries(true,filter,this.seriesNumber); // first step in series
//    	initFittingSeries(true,this.filterForTargetGeometry, this.seriesNumber); // first step in series
    	this.currentfX=calculateFxAndJacobian(this.currentVector, false);
    	//        	this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.debugLevel>2) {
    		System.out.println("this.currentVector");
    		for (int i=0;i<this.currentVector.length;i++){
    			System.out.println(i+": "+ this.currentVector[i]);
    		}
    	}
    	if (this.showThisImages) showDiff (this.currentfX, "residual-series-"+this.seriesNumber);
    	
    	double [][] gridXYZCorr=null;
		gridXYZCorr=	calculateGridXYZCorr3D(
//				distortionCalibrationData,
				this.refineParameters.grid3DCorrection,
				this.refineParameters.rotateCorrection,
				this.refineParameters.grid3DMaximalZCorr, //20.0,
				this.refineParameters.targetShowPerImage);

// TODO: make configurable and optional		
		shrinkExtrapolateGridCorrection(
				gridXYZCorr, // dx,dy,dz, mask >0
				null,
				getGridWidth(),
				1, //preShrink,
				5, // expand,
				3.0, //  sigma,
				2.0); //double ksigma
		
    	String [] titles={"X-correction(mm)","Y-correction(mm)","Z-correction","Weight"};
    	if (this.refineParameters.targetShowThisCorrection) {
    		this.SDFA_INSTANCE.showArrays(gridXYZCorr, getGridWidth(), getGridHeight(),  true, "Grid corrections", titles);
    	}
    	if (!this.refineParameters.targetApplyCorrection) return false;
    	patternParameters.applyGridCorrection(gridXYZCorr, this.refineParameters.targetCorrectionScale);
    	return true;
    }

    public boolean modifyPixelCorrection(DistortionCalibrationData distortionCalibrationData){ // old
    	if (fittingStrategy==null) {
    		String msg="Fitting strategy does not exist, exiting";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if (distortionCalibrationData.eyesisCameraParameters==null){
    		String msg="Eyesis camera parameters (and sensor dimensions) are not defined";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	//	fittingStrategy.distortionCalibrationData.readAllGrids(); 
//    	if (! selectGridEnhanceParameters()) return false;
    	
    	int series=refineParameters.showDialog(
    			"Select Lens Distrortion Residual Compensation Parameters",
    			0x1efff,
    			((this.seriesNumber>=0)?this.seriesNumber:0),
    			null); // averageRGB - only for target flat-field correction
    	if (series<0) return false;
    	this.seriesNumber=series;
    	
		int filter=this.filterForSensor;
		if (this.askFilter) filter=selectFilter(filter);
    	initFittingSeries(true,filter,this.seriesNumber); // first step in series now uses pattern alpha
//    	initFittingSeries(true,this.filterForSensor,this.seriesNumber); // first step in series now uses pattern alpha
    	this.currentfX=calculateFxAndJacobian(this.currentVector, false);
    	//        	this.currentRMS= calcError(calcYminusFx(this.currentfX));
    	if (this.debugLevel>2) {
    		System.out.println("this.currentVector");
    		for (int i=0;i<this.currentVector.length;i++){
    			System.out.println(i+": "+ this.currentVector[i]);
    		}
    	}
 //   	if (this.showThisImages) showDiff (this.currentfX, "residual-series-"+this.seriesNumber);
    	double [][][] sensorXYCorr=	calculateSensorXYCorr(
    			distortionCalibrationData,
    			this.refineParameters.showPerImage,
    			this.refineParameters.showIndividualNumber,
    			this.refineParameters.usePatternAlpha);
    	String [] titles={"X-corr(pix)","Y-corr(pix)","alpha","weight","Red","Green","Blue"};
		int decimate=distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int sWidth= (distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
		int sHeight=(distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
    	if (this.refineParameters.showUnfilteredCorrection) {
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    		   this.SDFA_INSTANCE.showArrays(sensorXYCorr[numChn], sWidth, sHeight,  true, "chn_"+numChn+"_extra_correction", titles);
    		}
    	}
    	if (this.refineParameters.extrapolate) {
    		boolean [] whichExtrapolate={true, true,false,false,true,true,true};
    		boolean [] whichPositive=   {false,false,false,false,true,true,true};
    		IJ.showStatus("Extrapolating sensor corrections...");
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    			for (int i=0;i<whichPositive.length;i++) if (whichPositive[i]){
    				logScale(sensorXYCorr[numChn][i],this.refineParameters.fatZero);
    			}
    		    boolean extrapolateOK=extrapolateSensorCorrection( //ava.lang.NullPointerException  at Distortions.modifyPixelCorrection(Distortions.java:2595)
    		    		whichExtrapolate,
    		    		sensorXYCorr[numChn],
    		    		sensorXYCorr[numChn][2],// alpha - it is more pessimistic than fittingStrategy.distortionCalibrationData.sensorMasks[numChn]
//    		    		fittingStrategy.distortionCalibrationData.sensorMasks[numChn],
    		    		this.refineParameters.alphaThreshold,
    		    		this.refineParameters.extrapolationSigma,
    		    		this.refineParameters.extrapolationKSigma);
    			IJ.showProgress(numChn+1, sensorXYCorr.length);
    			for (int i=0;i<whichPositive.length;i++) if (whichPositive[i]){
    				unLogScale(sensorXYCorr[numChn][i],this.refineParameters.fatZero);
    			}
    			if (!extrapolateOK) sensorXYCorr[numChn]=null; // no correction for too small areas
     		}
			IJ.showProgress(1.0);
    	}
    	
    	if (this.refineParameters.showExtrapolationCorrection && this.refineParameters.extrapolate && this.refineParameters.smoothCorrection) {
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    		   this.SDFA_INSTANCE.showArrays(sensorXYCorr[numChn], sWidth, sHeight,  true, "chn_"+numChn+"_extrapolated", titles);
    		}
    	}

    	if (this.refineParameters.smoothCorrection) {
    		boolean [] whichBlur={true,true,false,false,true,true,true};
    		IJ.showStatus("Bluring sensor corrections...");
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    			DoubleGaussianBlur gb=new DoubleGaussianBlur();
    			for (int m=0;m<whichBlur.length;m++) if (whichBlur[m]){
    			gb.blurDouble(sensorXYCorr[numChn][m],
    					sWidth,
    					sHeight,
    					this.refineParameters.smoothSigma/decimate,
    					this.refineParameters.smoothSigma/decimate,
    					 0.01);
    			}
    			IJ.showProgress(numChn+1, sensorXYCorr.length);
     		}
			IJ.showProgress(1.0);
    	}    	
    	if (this.refineParameters.showThisCorrection ) {
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    		   this.SDFA_INSTANCE.showArrays(sensorXYCorr[numChn], sWidth, sHeight,  true, "chn_"+numChn+"_filtered", titles);
    		}
    	}
//   	if (!selectCorrectionScale()) return false;
		IJ.showStatus("Applying corrections:"+((!this.refineParameters.applyCorrection && !this.refineParameters.applyFlatField)?
				"none ":((this.refineParameters.applyCorrection?"geometry ":"")+(this.refineParameters.applyFlatField?"flat field":""))));
		
		
		addOldXYCorrectionToCurrent(
    			this.refineParameters.correctionScale,
    			sensorXYCorr);
		
    	boolean result=applySensorCorrection(
    			this.refineParameters.applyCorrection,
    			this.refineParameters.applyFlatField,
    			this.refineParameters.correctionScale,
    			sensorXYCorr,
    			distortionCalibrationData);
    	if (this.refineParameters.showCumulativeCorrection) {
    		for (int numChn=0;numChn<sensorXYCorr.length;numChn++) if (sensorXYCorr[numChn]!=null){
    		   this.SDFA_INSTANCE.showArrays(sensorXYCorr[numChn], sWidth, sHeight,  true, "Cumulative_chn_"+numChn+"_corrections", titles);
    		}
    	}
    	if (result) {
//    		updateCameraParametersFromCalculated();
    		
    		// NEED to update from all?
//			updateCameraParametersFromCalculated(true); // update camera parameters from all (even disabled) images
			updateCameraParametersFromCalculated(false); // update camera parameters from enabled only images (may overwrite some of the above)

    	}
		IJ.showStatus("");

    	return result;
    }
    
    public void resetSensorCorrection(){
    	this.pixelCorrection=null;
    	this.pathNames=null;
    }   
    public void resetSensorCorrection(int sensorNum){
    	if ((this.pixelCorrection!=null) && (sensorNum<this.pixelCorrection.length) && (sensorNum>=0)) {
    		this.pixelCorrection[sensorNum]=null;
    		this.pathNames[sensorNum]=null;
    	}
    }
    public void initSensorCorrection(){
    	int numLayers=7;
    	int numChannels=this.fittingStrategy.distortionCalibrationData.getNumChannels(); // number of used channels
    	this.pixelCorrection=new double [numChannels][][];
    	this.pathNames=new String[numChannels];
    	double [][] masks=this.fittingStrategy.distortionCalibrationData.calculateSensorMasks();
    	for (int i=0;i<this.pixelCorrection.length;i++){
    		this.pixelCorrection[i]=new double [numLayers][];
    		this.pathNames[i]=null;
    		for (int n=0;n<numLayers;n++) this.pixelCorrection[i][n]=new double [masks[i].length];
    		for (int j=0;j<masks[i].length;j++) {
        		this.pixelCorrection[i][0][j]=0.0;
        		this.pixelCorrection[i][1][j]=0.0;
        		this.pixelCorrection[i][2][j]=masks[i][j];
    			this.pixelCorrection[i][3][j]=1.0;
    			this.pixelCorrection[i][4][j]=1.0;
    			this.pixelCorrection[i][5][j]=1.0;
    		}
    	}
    }
    
    /*
     * Adds new correction to the current one with the result to the new one. If update, the old arrays are also modified/created
     */
    public boolean applySensorCorrection(
    		boolean update,
    		boolean updateFlatField,
    		double scale,
    		double [][][] sensorXYCorr,
    		DistortionCalibrationData distortionCalibrationData){
		int numLayers=6;
		int decimate=distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int width= distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		int height=distortionCalibrationData.eyesisCameraParameters.sensorHeight;
    	if ((this.pixelCorrection!=null) && (this.pixelCorrectionDecimation!=decimate)){
    		IJ.showMessage("Error","Can not apply correction as the current correction and the new one have different decimations");
    		return false;
    	}
    	if ((this.pixelCorrection==null) && !update && !updateFlatField) return true;
    	if (update){
    		this.pixelCorrectionDecimation=decimate;
    		this.pixelCorrectionWidth=width;
    		this.pixelCorrectionHeight=height;
    	}
        if (this.pixelCorrection==null) {
        	if (this.debugLevel>1) System.out.println("Initializing pixelCorrection array");
        	this.pixelCorrection=new double [sensorXYCorr.length][][];
        	this.pathNames=new String[sensorXYCorr.length];
        	for (int i=0;i<this.pixelCorrection.length;i++){
        		this.pixelCorrection[i]=null;
        		this.pathNames[i]=null;
        	}
        }
        
        if (this.pixelCorrection.length<sensorXYCorr.length){ // OK to update even if !update
        	if (this.debugLevel>1) System.out.println("Increasing number of sensors in pixelCorrection array");
        	double [][][] tmp=new double[sensorXYCorr.length][][];
        	String [] tmpPaths=new String[sensorXYCorr.length];
        	for (int i=0;i<tmp.length;i++){
        		if (i<this.pixelCorrection.length){
        			tmp[i]=this.pixelCorrection[i];
        			tmpPaths[i]=this.pathNames[i];
        		}
        		else {
        			tmp[i]=null;
        			tmpPaths[i]=null;
        		}
        	}
        	this.pixelCorrection=tmp;
        	this.pathNames=tmpPaths;
        }
        for (int i=0;i<sensorXYCorr.length;i++) if (sensorXYCorr[i]!=null){
        	boolean in6=sensorXYCorr[i].length==6; // was - 7
        	int indxR=in6?3:4;
        	int indxG=in6?4:5;
        	int indxB=in6?5:6;
 //       	System.out.println("applySensorCorrection(): in6="+in6+" indxR="+indxR+" indxG="+indxG+" indxB="+indxB);
        	double [] sensorMask=in6?((fittingStrategy.distortionCalibrationData.sensorMasks==null)?null:fittingStrategy.distortionCalibrationData.sensorMasks[i]):sensorXYCorr[i][2];      	
        	if (this.pixelCorrection[i]==null) {
        		if (update || updateFlatField) {
        			this.pixelCorrection[i]=new double [numLayers][];
        			this.pixelCorrection[i][0]=sensorXYCorr[i][0];
        			this.pixelCorrection[i][1]=sensorXYCorr[i][1];
        			if (sensorMask!=null){
        				this.pixelCorrection[i][2]=sensorMask;
        			} else {
        				this.pixelCorrection[i][2]= new double[this.pixelCorrection[i][0].length];
    					for (int j=0;j<this.pixelCorrection[i][2].length;j++) this.pixelCorrection[i][2][j]=1.0;
        			}
        			if (sensorXYCorr[i].length>=7){
        				this.pixelCorrection[i][3]=sensorXYCorr[i][indxR];
        				this.pixelCorrection[i][4]=sensorXYCorr[i][indxG];
        				this.pixelCorrection[i][5]=sensorXYCorr[i][indxB];
        			} else {
//        				for (int n=2;n<numLayers;n++){
        				for (int n=3;n<numLayers;n++){
        					this.pixelCorrection[i][n]=new double[this.pixelCorrection[0].length];
        					for (int j=0;j<this.pixelCorrection[i][0].length;j++) this.pixelCorrection[i][n][j]=1.0;
        				}

        			}
        		}
        	} else  {
        		for (int j=0;j<sensorXYCorr[i][0].length;j++){
        			// removed - now it is already done
///        			sensorXYCorr[i][0][j]=this.pixelCorrection[i][0][j]+scale*sensorXYCorr[i][0][j];
///        			sensorXYCorr[i][1][j]=this.pixelCorrection[i][1][j]+scale*sensorXYCorr[i][1][j];
        			if (scale==1.0) { // recovering from Double.NaN in old values - still do not know where it came from in the first place
        			} else {
        				if (!in6){
        					sensorXYCorr[i][2][j]=this.pixelCorrection[i][2][j]+scale*(sensorXYCorr[i][2][j]-this.pixelCorrection[i][2][j]);
        				}
            			sensorXYCorr[i][indxR][j]=this.pixelCorrection[i][3][j]+scale*(sensorXYCorr[i][indxR][j]-this.pixelCorrection[i][3][j]);
            			sensorXYCorr[i][indxG][j]=this.pixelCorrection[i][4][j]+scale*(sensorXYCorr[i][indxG][j]-this.pixelCorrection[i][4][j]);
            			sensorXYCorr[i][indxB][j]=this.pixelCorrection[i][5][j]+scale*(sensorXYCorr[i][indxB][j]-this.pixelCorrection[i][5][j]);
        				
        			}
        		}
        		if (update){
        			this.pixelCorrection[i][0]=sensorXYCorr[i][0];
        			this.pixelCorrection[i][1]=sensorXYCorr[i][1];
        		}
        		if (updateFlatField){
        			if (!in6){
        				this.pixelCorrection[i][2]=sensorXYCorr[i][2];
        			}
        			this.pixelCorrection[i][3]=sensorXYCorr[i][indxR];
        			this.pixelCorrection[i][4]=sensorXYCorr[i][indxG];
        			this.pixelCorrection[i][5]=sensorXYCorr[i][indxB];
        		}
        	}
        }
        return true;
    	
    }
    public String getSensorPath(int numSensor){ //<0 - first available;
    	if ((this.pathNames == null) || (numSensor>=this.pathNames.length)) return null;
    	if (numSensor>=0) return this.pathNames[numSensor];
    	for (int i=0;i<this.pathNames.length;i++) if ((this.pathNames[i]!=null) && (this.pathNames[i].length()>0)) return this.pathNames[i];
    	return null;
    }
    public void saveDistortionAsImageStack(
    		CalibrationHardwareInterface.CamerasInterface camerasInterface, // to save channel map
    		String title,
    		String path,
    		boolean emptyOK){
    	int indexPeriod=path.indexOf('.',path.lastIndexOf(Prefs.getFileSeparator()));
    	int indexSuffix=indexPeriod;
    	String digits="0123456789";
    	for (int i=1;i<=2;i++) if (digits.indexOf(path.charAt(indexSuffix-1))>=0) indexSuffix--; // remove 1 or 2 digits before period
    	boolean hadSuffix= (path.charAt(indexSuffix-1)=='-');
    	int numSubCameras=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0].length;
    	for (int chNum=0;chNum<numSubCameras;chNum++) if (emptyOK  || ((this.pixelCorrection!=null) && (chNum<this.pixelCorrection.length) && (this.pixelCorrection[chNum]!=null)))  {
    		String channelPath= (hadSuffix?path.substring(0,indexSuffix):(path.substring(0,indexPeriod)+"-"))+
    		String.format("%02d",chNum)+path.substring(indexPeriod);
    		saveDistortionAsImageStack(
    				camerasInterface, // to save channel map
    				title,
    				channelPath,
    				chNum,
    				emptyOK);
    	}
    }
    
    public ImagePlus saveDistortionAsImageStack(
    		CalibrationHardwareInterface.CamerasInterface camerasInterface, // to save channel map
    		String title,
    		String path,
    		int numSensor,
    		boolean emptyOK){
    	ImagePlus imp=getDistortionAsImageStack(
    			camerasInterface, // to save channel map
    			title,
    			numSensor,
    			emptyOK);
    	if (imp==null) return null;
    	boolean realData= (this.pixelCorrection!=null) && (this.pixelCorrection[numSensor]!=null);
    	FileSaver fs=new FileSaver(imp);
    	String msg="Saving "+(realData?"":"EMPTY")+" sensor distortions to "+path;
    	if (updateStatus) IJ.showStatus(msg);
    	if (this.debugLevel>0) System.out.println(msg);
    	fs.saveAsTiffStack(path);
    	if (this.pathNames==null){
    		this.pathNames=new String[this.fittingStrategy.distortionCalibrationData.getNumChannels()];
    		for (int i=0;i<this.pathNames.length;i++) this.pathNames[i]=null; 
    	}
    	this.pathNames[numSensor]=path;
    	return imp;
    }

//  /    	int numChannels=this.fittingStrategy.distortionCalibrationData.getNumChannels(); // number of used channels
// TODO: Currently saves data from Station 0  
    public ImagePlus getDistortionAsImageStack(
    		CalibrationHardwareInterface.CamerasInterface camerasInterface, // to save channel map
    		String title,
    		int numSensor,
    		boolean emptyOK){
    	int stationNumber=0;
    	String [] titles={"X-corr","Y-corr","mask","R-vign","G-vign","B-vign"};
    	double [][] pixelCorr=null;
    	if (!emptyOK &&((this.pixelCorrection==null) ||
    			(numSensor<0) ||
    		(numSensor>=this.pixelCorrection.length) ||
    		(this.pixelCorrection[numSensor]==null)))
    			{
    		String msg="Sensor correction data is not available";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
    	if ((this.pixelCorrection!=null) && (numSensor>=0) && (numSensor<this.pixelCorrection.length))
    		pixelCorr=this.pixelCorrection[numSensor];
    	int width=this.pixelCorrectionWidth/this.pixelCorrectionDecimation;
    	int height=this.pixelCorrectionHeight/this.pixelCorrectionDecimation;
//    	int length=this.pixelCorrection[numSensor][0].length; // should be == width*height
    	int length=width*height;
    	
    	float [][]pixels=new float [titles.length][length]; // dx, dy, sensor mask,v-r,v-g,v-b
    	// assuming all sensors have the same dimension
    	double [] mask=null;
    	if (fittingStrategy.distortionCalibrationData.sensorMasks.length<=numSensor) return null; // no data
    	if ((fittingStrategy.distortionCalibrationData.sensorMasks!=null) &&
    			(fittingStrategy.distortionCalibrationData.sensorMasks[numSensor]!=null)){
    		mask=fittingStrategy.distortionCalibrationData.sensorMasks[numSensor];
    	}

    	for (int index=0;index<length;index++){
    		if (pixelCorr==null){
        		pixels[0][index]=  0.0f;
        		pixels[1][index]=  0.0f;
        		for (int n=3;n<pixels.length;n++) pixels[n][index]= 1.0f; // normalize?
    		} else {
    			pixels[0][index]=  (float) pixelCorr[0][index];
    			pixels[1][index]=  (float) pixelCorr[1][index];
        		for (int n=3;n<pixels.length;n++) pixels[n][index]= (float) pixelCorr[n][index];
    		}
    		// get sensor mask here
    		pixels[2][index]=  (mask==null)? 1.0f:((float) mask[index]);
    		
    	}
    	ImagePlus imp=null;
  		ImageStack stack=new ImageStack(width,height);
   		for (int n=0;n<pixels.length;n++)  stack.addSlice(titles[n],    pixels[n]);
   		imp = new ImagePlus(title, stack);
        // set properties sufficient to un-apply distortions to the image
   		// First - corrections
    	EyesisSubCameraParameters subCam=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[stationNumber][numSensor];
    	double entrancePupilForward=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.entrancePupilForward[stationNumber];
    	imp.setProperty("VERSION",  "1.0");
    	imp.setProperty("comment_arrays",  "Array corrections from acquired image to radially distorted, in pixels");
    	imp.setProperty("arraysSet",  ""+(pixelCorr!=null)); // per-pixel arrays are not set, using 0.0
    	imp.setProperty("maskSet",     ""+(mask!=null)); // per-pixel masks is not set, using 1.0
    	imp.setProperty("pixelCorrectionWidth",  ""+this.pixelCorrectionWidth);
    	imp.setProperty("pixelCorrectionHeight", ""+this.pixelCorrectionHeight);
    	imp.setProperty("pixelCorrectionDecimation", ""+this.pixelCorrectionDecimation);
    	imp.setProperty("comment_decimation", "when decimation use integer divide to find the index, corection values are in non-decimated pixels");
    	imp.setProperty("distortion_formula",  "(normalized by distortionRadius in mm) Rdist/R=A8*R^7+A7*R^6+A6*R^5+A5*R^4+A*R^3+B*R^2+C*R+(1-A6-A7-A6-A5-A-B-C)");
    	imp.setProperty("distortionRadius", ""+subCam.distortionRadius);
    	imp.setProperty("distortionRadius_unuts", "mm");
    	imp.setProperty("focalLength", ""+subCam.focalLength);
    	imp.setProperty("focalLength_units", "mm");
    	imp.setProperty("pixelSize", ""+subCam.pixelSize);
    	imp.setProperty("pixelSize_units", "um");
    	imp.setProperty("distortionA8", ""+subCam.distortionA8);
    	imp.setProperty("distortionA7", ""+subCam.distortionA7);
    	imp.setProperty("distortionA6", ""+subCam.distortionA6);
    	imp.setProperty("distortionA5", ""+subCam.distortionA5);
    	imp.setProperty("distortionA", ""+subCam.distortionA);
    	imp.setProperty("distortionB", ""+subCam.distortionB);
    	imp.setProperty("distortionC", ""+subCam.distortionC);
    	imp.setProperty("comment_px0_py0", "lens center on the sensor, in pixels");
    	imp.setProperty("px0", ""+subCam.px0);
    	imp.setProperty("py0", ""+subCam.py0);
    	imp.setProperty("comment_azimuth", "lens center azimuth, CW from top, degrees");
    	imp.setProperty("azimuth", ""+subCam.azimuth);
    	imp.setProperty("comment_radius", "lens center distance from the camera vertical axis, mm");
    	imp.setProperty("radius",  ""+subCam.radius);
    	imp.setProperty("comment_height", "lens center vertical position from the head center, mm");
    	imp.setProperty("height",  ""+subCam.height);
    	imp.setProperty("comment_heading", "lens heading - added to azimuth");
    	imp.setProperty("heading",  ""+subCam.phi);
    	imp.setProperty("comment_elevation", "lens elevation from horizontal, positive - above horizon, degrees");
    	imp.setProperty("elevation",  ""+subCam.theta);
    	imp.setProperty("comment_roll", "lens rotation around the lens axis. Positive - CW looking to the target, degrees");
    	imp.setProperty("roll",  ""+subCam.psi);

    	imp.setProperty("comment_channel", "number of the sensor (channel) in the camera");
    	imp.setProperty("channel",  ""+numSensor);
    	imp.setProperty("comment_subcamera", "number of the subcamera with individual IP, starting with 0");
    	imp.setProperty("subcamera",  ""+camerasInterface.getSubCamera(numSensor));
    	imp.setProperty("comment_subchannel", "number of the sensor port on a subcamera (0..2)");
    	imp.setProperty("subchannel",  ""+camerasInterface.getSubChannel(numSensor));
    	imp.setProperty("comment_entrancePupilForward",  "entrance pupil distance from the azimuth/radius/height, outwards in mm");
    	imp.setProperty("entrancePupilForward",  ""+entrancePupilForward); // currently global, decoders will use per-sensor
    	
    	
    	
    	//camerasInterface, numSensor
    	(new JP46_Reader_camera(false)).encodeProperiesToInfo(imp);
    	imp.getProcessor().resetMinAndMax();
    	return imp;
    }
    
    
    public void setDistortionFromImageStack(String path, boolean overwriteExtrinsic){
    	int indexPeriod=path.indexOf('.',path.lastIndexOf(Prefs.getFileSeparator()));
    	int numSubCameras=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0].length;
    	for (int chNum=0;chNum<numSubCameras;chNum++){
    		String channelPath=path.substring(0,indexPeriod-2)+String.format("%02d",chNum)+path.substring(indexPeriod);
    		try { // disable here for now
    			setDistortionFromImageStack(channelPath, chNum, false, overwriteExtrinsic);
    		} catch (Exception e) {
    			System.out.println("setDistortionFromImageStack(): " + e.toString());
    			e.printStackTrace();
    		}
    	}
    }
    
    public void setDistortionFromImageStack(String path, int numSensor, boolean reportProblems, boolean overwriteExtrinsic){
		Opener opener=new Opener();
		ImagePlus imp=opener.openImage("", path);
    	if (imp==null) {
    		if (!reportProblems) return;
    		String msg="Failed to read sensor calibration data file "+path;
    		IJ.showMessage("Error",msg);
    		System.out.println(msg);
    		throw new IllegalArgumentException (msg);
    	}
		if (this.debugLevel>0) System.out.println("Read "+path+" as a sensor calibration data");
    	(new JP46_Reader_camera(false)).decodeProperiesFromInfo(imp);
    	setDistortionFromImageStack(imp, numSensor, overwriteExtrinsic);
    	this.pathNames[numSensor]=path;
    }
    
    //TODO: look more after testing. Currently all station parameters are set from the sensor images, may be minor differences
    public void setDistortionFromImageStack(ImagePlus imp, int numSensor, boolean overwriteExtrinsic){
//    	int corrX=0,corrY=1,
    	int corrMask=2;
    	if (numSensor<0) {
    		System.out.println("setDistortionFromImageStack(): Tried to read negative channel");
    		return;
    	}
//		System.out.println("setDistortionFromImageStack(): processing channel channel "+numSensor);
    	if (imp == null){
    		String msg="Distortions image is null";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
        String [] requiredProperties={
        		"pixelCorrectionWidth",
        		"pixelCorrectionHeight",
        		"pixelCorrectionDecimation",
        		"distortionRadius",
        		"focalLength",
        		"pixelSize",
//        		"distortionA8",
//        		"distortionA7",
//        		"distortionA6",
        		"distortionA5",
        		"distortionA",
        		"distortionB",
        		"distortionC",
        		"px0",
        		"py0"};
        for (int i=0; i<requiredProperties.length;i++) if (imp.getProperty(requiredProperties[i])==null){
    		String msg="Required property "+requiredProperties[i]+" is not defined in "+imp.getTitle();
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
        }
        
    	if (imp.getStackSize()<3){
    		String msg="Expecting >=3 slices, got "+imp.getStackSize();
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
    	}
		
		ImageStack stack = imp.getStack();
		float [][] pixels =new float[stack.getSize()][];
    	for (int i=0;i<pixels.length;i++) pixels[i]= (float[]) stack.getPixels(i+1);


        int numSubCameras=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0].length;
        if (numSensor>=numSubCameras){
    		String msg="Loaded calibration channel number "+numSensor+"is higher than maximal in the system "+(numSubCameras-1);
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
        }
//		System.out.println("setDistortionFromImageStack(): processing channel channel "+numSensor);
      
    	EyesisSubCameraParameters subCam;
    	EyesisCameraParameters cam=     fittingStrategy.distortionCalibrationData.eyesisCameraParameters;
        this.pixelCorrectionWidth=      Integer.parseInt  ((String) imp.getProperty("pixelCorrectionWidth"));
        this.pixelCorrectionHeight=     Integer.parseInt  ((String) imp.getProperty("pixelCorrectionHeight"));
        this.pixelCorrectionDecimation= Integer.parseInt  ((String) imp.getProperty("pixelCorrectionDecimation"));
        
        if ((this.fittingStrategy!=null) && (this.fittingStrategy.distortionCalibrationData!=null) && (this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters!=null)){
    		this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks=this.pixelCorrectionDecimation;
    		this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth=  this.pixelCorrectionWidth;
    		this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight=this.pixelCorrectionHeight;
        }
        for (int stationNumber=0;stationNumber<fittingStrategy.distortionCalibrationData.eyesisCameraParameters.numStations; stationNumber++){
        	subCam=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[stationNumber][numSensor];

        	subCam.distortionRadius=        Double.parseDouble((String) imp.getProperty("distortionRadius"));
        	subCam.focalLength=             Double.parseDouble((String) imp.getProperty("focalLength"));
        	subCam.pixelSize=               Double.parseDouble((String) imp.getProperty("pixelSize"));
        	if (imp.getProperty("distortionA8")!=null) {
        		subCam.distortionA8=            Double.parseDouble((String) imp.getProperty("distortionA8"));
        	} else subCam.distortionA8=0.0;
        	if (imp.getProperty("distortionA7")!=null) {
        		subCam.distortionA7=            Double.parseDouble((String) imp.getProperty("distortionA7"));
        	} else subCam.distortionA7=0.0;
        	if (imp.getProperty("distortionA6")!=null) {
        		subCam.distortionA6=            Double.parseDouble((String) imp.getProperty("distortionA6"));
        	} else subCam.distortionA6=0.0;
        	subCam.distortionA5=            Double.parseDouble((String) imp.getProperty("distortionA5"));
        	subCam.distortionA=             Double.parseDouble((String) imp.getProperty("distortionA"));
        	subCam.distortionB=             Double.parseDouble((String) imp.getProperty("distortionB"));
        	subCam.distortionC=             Double.parseDouble((String) imp.getProperty("distortionC"));
        	subCam.px0=                     Double.parseDouble((String) imp.getProperty("px0"));
        	subCam.py0=                     Double.parseDouble((String) imp.getProperty("py0"));
        	if (imp.getProperty("azimuth")  !=null) subCam.azimuth= Double.parseDouble((String) imp.getProperty("azimuth"));
        	if (imp.getProperty("radius")   !=null) subCam.radius=  Double.parseDouble((String) imp.getProperty("radius"));
        	if (imp.getProperty("height")   !=null) subCam.height=  Double.parseDouble((String) imp.getProperty("height"));
        	if (imp.getProperty("entrancePupilForward")!=null) cam.entrancePupilForward[stationNumber]= Double.parseDouble((String) imp.getProperty("entrancePupilForward"));
        	if (imp.getProperty("heading")  !=null) subCam.phi=     Double.parseDouble((String) imp.getProperty("heading"));
        	if (imp.getProperty("elevation")!=null) subCam.theta=   Double.parseDouble((String) imp.getProperty("elevation"));
        	if (imp.getProperty("roll")!=null) subCam.psi=          Double.parseDouble((String) imp.getProperty("roll"));
        	// Update intrinsic image parameters        
        	this.lensDistortionParameters.pixelSize=subCam.pixelSize;
        	this.lensDistortionParameters.distortionRadius=subCam.distortionRadius;
        }
        for (int imgNum=0;imgNum<fittingStrategy.distortionCalibrationData.getNumImages();imgNum++){
        	int imageSubCam=  fittingStrategy.distortionCalibrationData.getImageSubcamera(imgNum);
        	int stationNumber=fittingStrategy.distortionCalibrationData.getImageStation(imgNum);
        	if (imageSubCam==numSensor){
        		// vector from the data we just set
        		double [] parVector=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.getParametersVector(stationNumber,imageSubCam);
        		if (overwriteExtrinsic) fittingStrategy.distortionCalibrationData.setSubcameraParameters(parVector,imgNum);
        		else                    fittingStrategy.distortionCalibrationData.setIntrinsicParameters(parVector,imgNum);
        	}
        }
        
        // now read the calibration data and mask
    	if (this.pixelCorrection==null) {
    		this.pixelCorrection=new double [numSubCameras][][];
    		this.pathNames=new String [numSubCameras];
    		for (int i=0;i<this.pixelCorrection.length;i++){
    			this.pixelCorrection[i]=null;
    			this.pathNames[i]=null;
    		}
    	}
        if (numSensor>=this.pixelCorrection.length){ // increase number of elements
        	double [][][] tmp=this.pixelCorrection.clone();
        	String [] tmpPaths=this.pathNames.clone();
        	this.pixelCorrection=new double[numSensor+1][][];
        	this.pathNames=new String[numSensor+1];
        	for (int i=0;i<this.pixelCorrection.length;i++)
        		if (i<tmp.length){
        			this.pixelCorrection[i]=tmp[i];
        			this.pathNames[i]=tmpPaths[i];
        		}else {
        			this.pixelCorrection[i]=null;
        			this.pathNames[i]=null;
        		}
        }
        int numLayers=6; //corr-x, corr-y,mask, ff-R, ff-G, ff-b
        if (numLayers<pixels.length) numLayers=pixels.length; // for the future?
//        this.pixelCorrection[numSensor]=new double [pixels.length] [pixels[0].length];
        this.pixelCorrection[numSensor]=new double [numLayers][pixels[0].length];
        for (int i= 0;i<this.pixelCorrection[numSensor][0].length;i++){
        	for (int n=0;n<pixels.length;n++)	this.pixelCorrection[numSensor][n][i]=pixels[n][i]; // mask will go to two places
        }
        if (pixels.length<numLayers){
            for (int i= 0;i<this.pixelCorrection[numSensor][0].length;i++){
            	for (int n=pixels.length;n<numLayers;n++)	this.pixelCorrection[numSensor][n][i]=1.0; // default ff if no data is available
            }
        }
        // now mask    
        boolean defined=false;
		for (int i=0;i<pixels[2].length;i++) if ((pixels[2][i]!=0.0) && (pixels[2][i]!=1.0)){
			defined=true;
			break;
		}
//    	System.out.println("setDistortionFromImageStack(): defined="+defined );
		if (defined) {
	        if (this.fittingStrategy.distortionCalibrationData.sensorMasks==null) {
	        	this.fittingStrategy.distortionCalibrationData.sensorMasks=new double [numSubCameras][];
	        	for (int i=0;i<this.fittingStrategy.distortionCalibrationData.sensorMasks.length;i++)
	        		this.fittingStrategy.distortionCalibrationData.sensorMasks[i]=null;
//	        	System.out.println("setDistortionFromImageStack(): created this.fittingStrategy.distortionCalibrationData.sensorMasks["+numSubCameras+"] of null-s" );
	        }
	        if (numSensor>=this.fittingStrategy.distortionCalibrationData.sensorMasks.length){ // increase number of elements
	        	double [][] tmp=this.fittingStrategy.distortionCalibrationData.sensorMasks;
	        	this.fittingStrategy.distortionCalibrationData.sensorMasks=new double[numSensor+1][];
	        	for (int i=0;i<this.fittingStrategy.distortionCalibrationData.sensorMasks.length;i++)
	        		if (i<tmp.length)this.fittingStrategy.distortionCalibrationData.sensorMasks[i]=tmp[i];
	        		else this.fittingStrategy.distortionCalibrationData.sensorMasks[i]=null;
	        }
	        if (this.fittingStrategy.distortionCalibrationData.sensorMasks[numSensor]==null){
	        	this.fittingStrategy.distortionCalibrationData.sensorMasks[numSensor]=new double[pixels[corrMask].length];
//	        	System.out.println("setDistortionFromImageStack(): created this.fittingStrategy.distortionCalibrationData.sensorMasks["+numSensor+"] of ["+pixels[corrMask].length+"]" );
	        }
	        for (int i= 0;i<this.fittingStrategy.distortionCalibrationData.sensorMasks[numSensor].length;i++) // null pointer
	        	this.fittingStrategy.distortionCalibrationData.sensorMasks[numSensor][i]=pixels[corrMask][i];
		}
    }
    /**
     * Accumulate per-sensor grid R,G,B intensities using current sensor flat-field values
     * @param serNumber - fitting series number to select images (-1 - all enabled) 
     * @param sensorMasks "pessimistic" masks to use only center (low-vignetting) part of each sensor (at least on the first runs?)
     * @param minContrast - minimal contrast to consider a node
     * @param threshold - not yet used - disregard grid nodes with low data - in the end
     * @param interpolate - interpolate sensor data
     * @param maskThresholdOcclusion suspect occlusion only if grid is missing in the area where sensor mask is above this threshold
     * @param expandOcclusion - shrink defined grid on image by this steps - to handle occlusion by rollers
     * @param fadeOcclusion - fade shrank occlusion border 
     * @param ignoreSensorFlatField - ignorfe previously calculated sensors flat-field calibration
     * @return
     */
    
    public double [][][][] calculateGridFlatField(
    		int serNumber,
    		double [][] sensorMasks,
    		double minContrast,
    		double threshold,
    		boolean interpolate,
    		double maskThresholdOcclusion,
    		int expandOcclusion,
    		double fadeOcclusion,
    		boolean ignoreSensorFlatField){
   // TODO: add standard weight function used elsethere.
    	int indexContrast=2;
 //   	int indexRGB=3;
    	boolean [] selectedImages=fittingStrategy.selectedImages(serNumber); // negative series number OK - will select all enabled
    	int gridHeight=this.patternParameters.gridGeometry.length;
    	int gridWidth=this.patternParameters.gridGeometry[0].length;
    	// was not here
		this.pixelCorrectionDecimation=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		this.pixelCorrectionWidth=   fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth;
		this.pixelCorrectionHeight=  fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight;

//		int sensorCorrWidth= (this.pixelCorrectionWidth-1)/this.pixelCorrectionDecimation+1;
    	int maxChannel=0;
//    	int numVierws=  this.patternParameters.getNumViews();
    	int numStations=this.patternParameters.getNumStations();
       	for (int numImg=0;numImg<fittingStrategy.distortionCalibrationData.gIP.length;numImg++) if (selectedImages[numImg]){
    		if (fittingStrategy.distortionCalibrationData.gIP[numImg].channel>maxChannel) maxChannel=fittingStrategy.distortionCalibrationData.gIP[numImg].channel;
    	}

    	double [][][][] sensorGrids=new double [numStations][maxChannel+1][][]; //{alpha, red,green, blue}
    	for (int ns=0;ns<sensorGrids.length;ns++) for (int n=0;n<sensorGrids[ns].length;n++) sensorGrids[ns][n]=null;
    	// For each sensor separately accumulate grid intensity using current sensor flat field calibration
    	for (int numImg=0;numImg<fittingStrategy.distortionCalibrationData.gIP.length;numImg++) if (selectedImages[numImg]) {
    		int channel=fittingStrategy.distortionCalibrationData.gIP[numImg].channel;
    		int station=fittingStrategy.distortionCalibrationData.gIP[numImg].getStationNumber();
    		if (sensorMasks[channel]==null) continue;
    		if (sensorGrids[station][channel]==null){ // null pointer
    			sensorGrids[station][channel]=new double [4][gridHeight*gridWidth]; //{alpha, red,green, blue}
    			for (int c=0;c<sensorGrids[station][channel].length;c++){
    				for (int i=0;i<sensorGrids[station][channel][0].length;i++) sensorGrids[station][channel][c][i]=0.0;
    			}
    		}
    		double [][] pixelsXY=fittingStrategy.distortionCalibrationData.gIP[numImg].pixelsXY;
    		if ((pixelsXY.length<1) || (pixelsXY[0].length<6)){
    			if (this.debugLevel>0) System.out.println("No flat-field data in image #"+numImg+
    					" - "+fittingStrategy.distortionCalibrationData.gIP[numImg].path+
    					" pixelsXY.length="+pixelsXY.length+
    					" pixelsXY[0].length="+((pixelsXY.length==0)?"nan": pixelsXY[0].length));
    			continue;
    		}
    		int [][]    pixelsUV=fittingStrategy.distortionCalibrationData.gIP[numImg].pixelsUV;
    		double [] defaultVector={0.0, 0.0, 0.0, 1.0, 1.0, 1.0};
 //   		double [] sensorMask=sensorMasks[channel];
    		// detect if there is any occlusion (i.e. by goniometer rollers)
//    		double [] green=new double [pixelsXY.length];
    		boolean [] bMask=new boolean [gridHeight*gridWidth];
    		double [] mask=new double[bMask.length];
    		for (int i=0;i<bMask.length;i++){
    			bMask[i]=false;
    			mask[i]=0.0;
    		}
    		for (int i=0;i<pixelsXY.length;i++){
    			double [] xyzmrgb=patternParameters.getXYZM(
    					pixelsUV[i][0], 
    					pixelsUV[i][1],
    					false,
    					station);
    			if (xyzmrgb!=null){
    	   			int index=patternParameters.getGridIndex(pixelsUV[i][0], pixelsUV[i][1]);
    	   			bMask[index]=(pixelsXY[i][indexContrast]>=minContrast);
    	   			mask[index]=interpolateMask (
        					sensorMasks[channel],
        					pixelsXY[i][0], 
        					pixelsXY[i][1]);
    			}
    		}
    		boolean [] occlusionMask=new boolean[bMask.length];
    		for (int i=0;i<occlusionMask.length;i++){
    			occlusionMask[i]=false;
    		}
    		boolean occlusion=false;
    		for (int i=1;i<(gridHeight-1);i++){
    			for (int j=1;j<(gridWidth-1);j++){
    				int index=i*gridWidth+j;
    				if (bMask[index]){
    					if ((   !bMask[(i-1)*gridWidth+j] ||
    							!bMask[(i+1)*gridWidth+j] ||
    							!bMask[i*    gridWidth+j-1] ||
    							!bMask[i*    gridWidth+j+1]) &&
    							(mask[index]>=maskThresholdOcclusion)
    					){
    						occlusionMask[index]=true;
    						occlusion=true;
    					}
    				}
    			}
    		}
    		if (occlusion){
    			for (int n=0;n<expandOcclusion;n++){ // expand
    				boolean [] bMaskPrevious=occlusionMask.clone();
    				for (int i=1;i<(gridHeight-1);i++){
    					for (int j=1;j<(gridWidth-1);j++){	
    						if (!occlusionMask[i*gridHeight+j]){
    							if (
    									bMaskPrevious[(i-1)*gridWidth+j] ||
    									bMaskPrevious[(i+1)*gridWidth+j] ||
    									bMaskPrevious[i*    gridWidth+j-1] ||
    									bMaskPrevious[i*    gridWidth+j+1]){
    								occlusionMask[i*gridWidth+j]=true;
    							}
    						}
    					}
    				}
    			}
        		double [] maskNonOccluded=new double [occlusionMask.length];
        		for (int i=0;i<maskNonOccluded.length;i++) maskNonOccluded[i]=occlusionMask[i]?0.0:1.0;
        		if (fadeOcclusion>0.0){
        			(new DoubleGaussianBlur() ).blurDouble(
        					maskNonOccluded,
        					gridWidth,
        					gridHeight,
        					fadeOcclusion,
        					fadeOcclusion,
        					0.01);
        		}
        		if (fadeOcclusion>=0.0 )for (int i=0;i<mask.length;i++){
    				double d=2.0*(maskNonOccluded[i]-0.5);
    				mask[i]*=(!occlusionMask[i] && (d>0))?(d*d):0.0;
    			}
    		}
    		for (int i=0;i<pixelsXY.length;i++){
    			double [] xyzmrgb=patternParameters.getXYZM(
    					pixelsUV[i][0], 
    					pixelsUV[i][1],
    					false,
    					station);
    			if (xyzmrgb==null) continue; // out of grid
    			double [] vector=ignoreSensorFlatField?defaultVector:
    				((interpolate)?
    					interpolateCorrectionVector (
    							channel,
    							pixelsXY[i][0], 
    							pixelsXY[i][1]):
 						getCorrectionVector (
 								channel,
 								pixelsXY[i][0], 
 								pixelsXY[i][1]))	;
    			int index=patternParameters.getGridIndex(pixelsUV[i][0], pixelsUV[i][1]);
    			double weight=mask[index];
    			
    			sensorGrids[station][channel][0][index]+=weight;
    			for (int c=0;c<3;c++)if (vector[c+3]>0.0){
    				sensorGrids[station][channel][c+1][index]+=weight*pixelsXY[i][c+3]/vector[c+3];
    			}
    		}
    	}
    	for (int station=0;station<sensorGrids.length;station++){
    		for (int channel=0;channel<sensorGrids[station].length; channel++) if (sensorGrids[station][channel]!=null){
    			if (this.pixelCorrection[channel]==null) {
    				sensorGrids[station][channel]=null;
    			} else {
    				for (int i=0;i<sensorGrids[station][channel][0].length;i++){
    					if (sensorGrids[station][channel][0][i]<threshold) {
    						for (int j=0;j<sensorGrids[station][channel].length;j++) sensorGrids[station][channel][j][i]=0.0;
    					} else {
    						for (int j=1;j<sensorGrids[station][channel].length;j++) sensorGrids[station][channel][j][i]/=sensorGrids[station][channel][0][i];
    					}
    				}
    			}
    		}
    	}
    	return sensorGrids;
    }
    
    public double [] getCorrectionVector(
			int chnNum,
			double px,
			double py){
		int sensorCorrWidth= (this.pixelCorrectionWidth-1)/this.pixelCorrectionDecimation+1;
		int indexXY=((int) Math.floor(px/this.pixelCorrectionDecimation)) +
		((int) Math.floor(py/this.pixelCorrectionDecimation))*sensorCorrWidth;
		double []vector=new double[this.pixelCorrection[chnNum].length];
		for (int i=0;i<vector.length;i++) vector[i]=this.pixelCorrection[chnNum][i][indexXY];
        return vector;
    }
    
    /**
     * Calculate color flat-field data for the pattern grid, calculate pattern grid mask (alpha)
     * @param referenceStation - station number for unity target brightness
     * @param flatFields partial, per-station, per-sensor pattern flat-field data
     * @param shrinkForMatching shrink pattern mask for calculating pattern average (removing unreliable borders)
     * @param resetMask reset pattern mask to default before (re)-calculating mask
     * @param maxDiffNeighb maximal relative difference between neghbor nodes (ignoring off-grid)
     * @param shrinkMask shrink result mask
     * @param fadeMask smooth fade the alpha on the pattern edge, keep zeros zeros
     * @return {alpha, r,g,b,number of images used} for each view group separately
     */
    
    public double [][][][] combineGridFlatField(
    		int referenceStation,
    		double [][][][] flatFields,
    		double shrinkForMatching,
    		boolean resetMask,
    		double maxDiffNeighb,  // maximal allowed relative difference between neighbour nodes (relative), 0 - do not  filter any
    		int shrinkMask, // shrink result mask
    		double fadeMask
    		){  
    	int maskIndex=3;
//    	if (resetMask) patternParameters.calculateGridGeometry(false);
    	if (resetMask) patternParameters.calculateGridGeometryAndPhotometric(false);
    	double [][][] gridGeometry= patternParameters.getGeometry();
    	int [] viewMap=	patternParameters.getViewMap();

    	int gridHeight=gridGeometry.length;
    	int gridWidth=gridGeometry[0].length;
    	int numStations=patternParameters.getNumStations();
    	int numViews=patternParameters.getNumViews();
    	double [][][][] viewPatterns=new double [numStations][numViews][][];
		double [][][] gridMask= new double[numStations][numViews][gridWidth*gridHeight];
		double [][] scaleIndividual=new double[flatFields[referenceStation].length][3]; // scale individual sensor patters before averaging
    	for (int station=0;station<numStations;station++){
    		for (int numView=0;numView<numViews;numView++){
    			viewPatterns[station][numView]=null;
//    			double [] gridMask= new double[gridWidth*gridHeight];
    			for (int v=0;v<gridHeight;v++) for (int u=0;u<gridWidth;u++) gridMask[station][numView][u+v*gridWidth]=(gridGeometry[v][u]!=null)?gridGeometry[v][u][maskIndex]:0.0;
    			if (shrinkForMatching>0){
    				(new DoubleGaussianBlur() ).blurDouble(gridMask[station][numView], gridWidth, gridHeight, shrinkForMatching, shrinkForMatching, 0.01);
    				for (int i=0;i<gridMask[station][numView].length;i++){
    					double d=2.0*(gridMask[station][numView][i]-0.5);
    					gridMask[station][numView][i]=(d>0)?(d*d):(0.0);
    				}
    			}
    			for (int v=0;v<gridHeight;v++) for (int u=0;u<gridWidth;u++){
    				if ((gridGeometry[v][u]==null) || (gridGeometry[v][u][maskIndex]<=0.0)) gridMask[station][numView][u+v*gridWidth]=0.0;
    			}
    			if (this.debugLevel>2){
    				this.SDFA_INSTANCE.showArrays(gridMask[station][numView], gridWidth, gridHeight,   "MATCH_MASK"+numView);
    			}
//    			double [][] scaleIndividual=new double[flatFields[station].length][3]; // scale individual sensor patters before averaging
    			//    		for (int numSensor=0;numSensor<flatFields.length; numSensor++ ) if (flatFields[numSensor]!=null){
    			// process only sensors from the same view of the target (i.e. 0 - eyesis head, 1 - eyesis bottom)
    			if (station==referenceStation) {
    				int numUsedSensors=0;
    				for (int numSensor=0;numSensor<flatFields[station].length; numSensor++ ) if ((flatFields[station][numSensor]!=null) && (viewMap[numSensor]==numView)){
    					numUsedSensors++;
    					double [] weightedSums={0.0,0.0,0.0};
    					double sumWeights=0;
    					for (int i=0;i<flatFields[station][numSensor][0].length;i++){
    						if ((gridMask[station][numView][i]>0.0) && (flatFields[station][numSensor][0][i]>1.0)){ // more than one overlapping image
    							double weight=flatFields[station][numSensor][0][i]*gridMask[station][numView][i];
    							sumWeights+=weight;
    							for (int c=0;c<weightedSums.length;c++) weightedSums[c]+=weight*flatFields[station][numSensor][c+1][i];

    						}
    					}
    					for (int c=0;c<weightedSums.length;c++){
    						scaleIndividual[numSensor][c]=patternParameters.averageRGB[c]*sumWeights/weightedSums[c];
    						if (this.debugLevel>2){
    							System.out.println("combineGridFlatField(): scaleIndividual["+numSensor+"]["+c+"]="+scaleIndividual[numSensor][c]);
    						}
    					}

    				}
    				if (numUsedSensors==0){
    					System.out.println("No data for target view #"+numView+" reference station ="+referenceStation);
    					continue;
    				}
    			}

    		}
    	}
    	for (int station=0;station<numStations;station++){
    		for (int numView=0;numView<numViews;numView++){
    			//    		double [][] combinedPattern=new double [5][gridWidth*gridHeight];
    			viewPatterns[station][numView]=new double [5][gridWidth*gridHeight];
    			double [][] combinedPattern=viewPatterns[station][numView];
    			for (int i=0;i<combinedPattern[0].length;i++){
    				double sumWeights=0;
    				double [] weightedSums={0.0,0.0,0.0};
    				for (int numSensor=0;numSensor<flatFields[station].length; numSensor++ ) if ((flatFields[station][numSensor]!=null) && (viewMap[numSensor]==numView)){
    					double weight=flatFields[station][numSensor][0][i];
    					sumWeights+=weight;
    					for (int c=0;c<weightedSums.length;c++) weightedSums[c]+=weight*flatFields[station][numSensor][c+1][i]*scaleIndividual[numSensor][c];
    				}
    				combinedPattern[4][i]=sumWeights; // just for debugging - no, actually used? - number of images used for this grid node 
    				for (int c=0;c<weightedSums.length;c++){
    					combinedPattern[c+1][i]=(sumWeights>0.0)?(weightedSums[c]/sumWeights):0.0;
    				}

    			}
    			/*
    		}
    	}
    	for (int station=0;station<viewPatterns.length;station++){
    		for (int numView=0;numView<viewPatterns[station].length;numView++){

    			double [][] combinedPattern=viewPatterns[station][numView];
    	*/		
    			//    	double [] gridMask[station][numView]= new double[gridWidth*gridHeight];
    			// calculate final mask
    			for (int v=0;v<gridHeight;v++) for (int u=0;u<gridWidth;u++) gridMask[station][numView][u+v*gridWidth]=(gridGeometry[v][u]!=null)?gridGeometry[v][u][maskIndex]:0.0;
    			if (maxDiffNeighb>0.0) { // throw away bad (having sharp gradients) nodes
    				int expWidth=gridWidth+2;
    				int expHeight=gridHeight+2;
    				double [] expandedGrid=new double [expWidth*expHeight];
    				boolean [] enabled=new boolean[expandedGrid.length];
    				for (int v=0;v<expHeight;v++) for (int u=0;u<expWidth;u++){
    					int index=u+expWidth*v;
    					if ((u==0) || (v==0) || (u==(expWidth-1)) || (v==(expHeight-1))){
    						expandedGrid[index]=0.0;
    						enabled[index]=false;
    					} else {
    						int indexSrc=(u-1)+gridWidth*(v-1);
    						expandedGrid[index]=(combinedPattern[1][indexSrc]+combinedPattern[2][indexSrc]+combinedPattern[3][indexSrc])/3.0; // average value;
    						enabled[index]=gridMask[station][numView][indexSrc]>0.0;
    					}
    				}
    				boolean [] badNodes=enabled.clone();
    				int [] dirs={
    						-expWidth-1,-expWidth,-expWidth+1,  1,
    						expWidth+1, expWidth, expWidth-1, -1};
    				int numBadOnTheBorder=1; // just to make while(true) happy
    				int minNeighb=3; // remove nodes with less than 3 neighbors
    				while (numBadOnTheBorder>0){
    					// build/update badNodes array
    					numBadOnTheBorder=0;
    					int numBad=0;
    					double [] diffs = new double [8];
    					int [] indices=new int [8]; 
    					for (int i=0;i<8;i++) {
    						diffs[i]=  -1.0; // diff==0 on isolated pair?
    						indices[i]=-1;
    					}
    					for (int index=0;index<badNodes.length;index++) if (badNodes[index]){
    						int numNeighb=0;
    						double maxDiff=0.0;
    						for (int dir=0;dir<dirs.length;dir++) {
    							int index1=index+dirs[dir];
    							if (enabled[index1]) {
    								numNeighb++;
    								double d=2.0*Math.abs((expandedGrid[index1]-expandedGrid[index])/(expandedGrid[index]+expandedGrid[index1]));
    								if (maxDiff<d) maxDiff=d;
    							}
    						}
    						if ((maxDiff<((maxDiffNeighb*numNeighb)/minNeighb)) && (numNeighb>=minNeighb)){ //more neighbors - more likely to keep
    							badNodes[index]=false; // rehabilitate node
    						} else {
    							numBad++;
    							if (numNeighb<8) { // do nothing if bad node is inside - it may be removed in the next passes
    								numBadOnTheBorder++;
    								if (maxDiff>diffs[numNeighb]){
    									diffs[numNeighb]=maxDiff;
    									indices[numNeighb]=index;
    								}
    							}
    						}
    					}
    					if (this.debugLevel>1) System.out.println("combineGridFlatField(): "+numBad+" bad nodes, "+numBadOnTheBorder+" of them on the border");
    					if (numBadOnTheBorder==0) break; // nothing to remove - break from the while(true) loop
    					// find bad node with least enabled neighbors - there will be one at least
    					for (int n=0;n<8;n++){
    						if (indices[n]>=0){
    							enabled[indices[n]]=false;   // disable this node
    							badNodes[indices[n]]=false;  // and remove from bad nodes (it is dead now)
    							// Any orphans around (were not bad, but now have to few neighbors)
    							for (int dir=0;dir<dirs.length;dir++) {
    								int index1=indices[n]+dirs[dir];
    								if (enabled[index1]) {
    									badNodes[index1]=true;
    								}
    							}
    							break;
    						}
    					}
    				}
    				// shrink enabled cells by shrinkMask
    				for (int n=0;n<shrinkMask;n++){
    					for (int i=0;i<badNodes.length;i++) badNodes[i]=false;
    					for (int v=1;v<(expHeight-1);v++) for (int u=1;u<(expWidth-1);u++){
    						int index=u+expWidth*v;
    						badNodes[index]=!enabled[index+1] || !enabled[index-1] || !enabled[index+expWidth] || !enabled[index-expWidth];
    					}
    					for (int i=0;i<badNodes.length;i++) if (badNodes[i]) enabled[i]=false;
    				}
    				// copy back to the gridMask[station][numView]
    				for (int v=1;v<(expHeight-1);v++) for (int u=1;u<(expWidth-1);u++){
    					int index=u+expWidth*v;
    					int indexSrc=(u-1)+gridWidth*(v-1);
    					if (!enabled[index]) gridMask[station][numView][indexSrc]=0.0;
    				}
    				for (int i=0;i<gridMask[station][numView].length;i++) if (gridMask[station][numView][i]==0.0){
    					combinedPattern[1][i]=0.0;
    					combinedPattern[2][i]=0.0;
    					combinedPattern[3][i]=0.0;
    				}

    			}
    			// fade mask on the borders, keep zeros - zeros
    			if (fadeMask>0.0){
    				double [] gridMask1=gridMask[station][numView].clone();
    				(new DoubleGaussianBlur() ).blurDouble(gridMask[station][numView], gridWidth, gridHeight, fadeMask, fadeMask, 0.01);
    				for (int i=0;i<gridMask[station][numView].length;i++){
    					double d=2.0*(gridMask[station][numView][i]-0.5);
    					gridMask[station][numView][i]=(gridMask1[i]>0)?((d>0)?(d*d):(0.0)):0.0;
    					if (combinedPattern[4][i]==0.0) gridMask[station][numView][i]=0.0; // how can it be zero combinedPattern[4][i] with non-zero gridMask[i]?
    				}
    			}
        		combinedPattern[0]=gridMask[station][numView];
    		}
    	}
    	return viewPatterns;
    }
    /**
     * Applies calculated [][] pattern alpha, r,g,b to the current grid geometry
     * @param patternFlatField
     */
    void applyGridFlatField(
    		double [][][][] patternFlatField // {alpha, red,green,blue, number of images used}[pixel_index] for each view of the pattern
    ){
    	for (int station=0;station<patternParameters.getNumStations();station++){
    		for (int nView=0;nView<patternParameters.getNumViews();nView++) 
    			if ((patternFlatField[station]!=null) && (patternFlatField[station][nView]!=null)) {
    				double [][] photometrics=patternParameters.getPhotometricByView(station,nView);
    				photometrics[0]=patternFlatField[station][nView][1].clone(); // red
    				photometrics[1]=patternFlatField[station][nView][2].clone(); // green
    				photometrics[2]=patternFlatField[station][nView][3].clone(); // blue
    				photometrics[3]=patternFlatField[station][nView][0].clone(); // alpha
    			}
    	}
    	/*    	
    	double [][][] gridGeometry= patternParameters.getGeometry();
    	int gridHeight=gridGeometry.length;
    	int gridWidth=gridGeometry[0].length;
    	for (int v=0;v<gridHeight;v++) for (int u=0;u<gridWidth;u++) {
    		int index=u+v*gridWidth;
    		gridGeometry[v][u][3]=patternFlatField[0][index];
    		gridGeometry[v][u][4]=patternFlatField[1][index];
    		gridGeometry[v][u][5]=patternFlatField[2][index];
    		gridGeometry[v][u][6]=patternFlatField[3][index];
    	}
    	 */
    }
    /**
     * Remove areas on the target flat-field data with specular reflections of the lamps by matching different views
     * @param highPassSigma - subtract weighted average of the difference with this
     * @param thershold mismatch causing 50% drop of the weight function
     * @param numIterations number of iterations of comparing to the weighted/masked average
     * @param apply apply changes
     * @param debugShowMode 0 - do not show debug images, 1 show only during last iteration, 2 - show always
     */
    public void removeSpecular (
    		boolean positiveDiffOnly,
    		double highPassSigma,
    		double lowPassSigma,
    		double thershold,
    		int numIterations,
    		boolean apply,
    		int debugShowMode){ // 0 - none, 1 - last iteration, 2 - all iterations
    	int debugThreshold=1;
    	int length=0;
    	
    	double [][][] weights=new double [patternParameters.getNumStations()][patternParameters.getNumViews()][];
		double [][][][] photometrics=new double [patternParameters.getNumStations()][patternParameters.getNumViews()][][];
		double [][][][] highPassDiff=new double [patternParameters.getNumStations()][patternParameters.getNumViews()][][];
		double [][][][] lowPassDiff=new double [patternParameters.getNumStations()][patternParameters.getNumViews()][][];

		int width=  patternParameters.gridGeometry[0].length;
		int height= patternParameters.gridGeometry.length;
    	for (int station=0;station<patternParameters.getNumStations();station++){
    		for (int nView=0;nView<patternParameters.getNumViews();nView++) { 
    			photometrics[station][nView]=patternParameters.getPhotometricByView(station,nView);
    			if (photometrics[station][nView]!=null){
    				length=photometrics[0][0][3].length; // should all be the same length (or null)
    				weights[station][nView]=new double [length];
    				for (int nPix=0;nPix<length;nPix++) weights[station][nView][nPix]=(photometrics[station][nView][3][nPix]>0.0)?1.0:0.0;
    			} else {
    				weights[station][nView]=null;
    			}
    		}
    	}
    	double threshold23=9.0*thershold*thershold;
    	for (int nIter=0;nIter<numIterations;nIter++){
    		boolean showDebug=(debugShowMode>1) || ((debugShowMode>0) && (nIter== (numIterations-1)));
        	// Calculate weighted average among different stations/views.
    		double [][] average=new double [4][length];
    		for (int nPix=0;nPix<length;nPix++){
    			double w0=0.0;
    	    	for (int station=0;station<patternParameters.getNumStations();station++){
    	    		for (int nView=0;nView<patternParameters.getNumViews();nView++) {
    	    			if (photometrics[station][nView]!=null){
    	    				double w=weights[station][nView][nPix]*photometrics[station][nView][3][nPix];
    	    				average[0][nPix]+=w*photometrics[station][nView][0][nPix];
    	    				average[1][nPix]+=w*photometrics[station][nView][1][nPix];
    	    				average[2][nPix]+=w*photometrics[station][nView][2][nPix];
    	    				w0+=w;
    	    			}
    	    		}
    	    	}
    	    	double k= (w0>0.0)?(1.0/w0):0.0;
    	    	average[0][nPix]*=k;
    	    	average[1][nPix]*=k;
    	    	average[2][nPix]*=k;
    	    	average[3][nPix]=w0/(patternParameters.getNumStations()*patternParameters.getNumViews());
    		}
    		double [][][][] diffFromAverage=new double [photometrics.length][photometrics[0].length][4][length];
        	// Scale each station/view for best fit
	    	for (int station=0;station<patternParameters.getNumStations();station++){
	    		for (int nView=0;nView<patternParameters.getNumViews();nView++) {
	    			double scale=0.0;
	    			if (photometrics[station][nView]!=null){
	    				double [] weightsHighLowPass=new double[length];
	    				double sf2=0.0,sfg=0.0;
	    				for (int nPix=0;nPix<length;nPix++){
    	    				double w=weights[station][nView][nPix]*photometrics[station][nView][3][nPix];
    	    				weightsHighLowPass[nPix]=w;
    	    				sf2+=w*(photometrics[station][nView][0][nPix]*photometrics[station][nView][0][nPix]+
    	    						photometrics[station][nView][1][nPix]*photometrics[station][nView][1][nPix]+
    	    						photometrics[station][nView][2][nPix]*photometrics[station][nView][2][nPix]);
    	    				sfg+=w*(photometrics[station][nView][0][nPix]*average[0][nPix]+
    	    						photometrics[station][nView][1][nPix]*average[1][nPix]+
    	    						photometrics[station][nView][2][nPix]*average[2][nPix]);
	    					
	    				}
	    				scale=sfg/sf2;
	    				if ((this.debugLevel>=debugThreshold) && showDebug){
	    					System.out.println("removeSpecular(), pass"+nIter+" scale["+station+"]["+nView+"]="+scale);
	    				}
//	    				Calculate difference from average
	    				for (int nPix=0;nPix<length;nPix++){
	    					if (photometrics[station][nView][3][nPix]>0.0){
	    						for (int c=0;c<3;c++){
	    							double d=scale*photometrics[station][nView][c][nPix]-average[c][nPix];
	    							diffFromAverage[station][nView][c][nPix]=d;
	    						}
	    					}
	    				}
	    				if (highPassSigma>0.0){
	    					double [] weightsHighPass=weightsHighLowPass.clone();
	    					(new DoubleGaussianBlur()).blurDouble(
	    							weightsHighPass,
	    							width,
	    							height,
	    							highPassSigma,
	    							highPassSigma,
	    							0.01);
	    					highPassDiff[station][nView]=new double [3][];
	    					for (int c=0;c<3;c++){
	    	    				highPassDiff[station][nView][c]=diffFromAverage[station][nView][c].clone(); // deep
	    						
	    						for (int nPix=0;nPix<length;nPix++){
	    							highPassDiff[station][nView][c][nPix]*=weightsHighLowPass[nPix];
	    						}	    				
		    					(new DoubleGaussianBlur()).blurDouble(
		    							highPassDiff[station][nView][c],
		    							width,
		    							height,
		    							highPassSigma,
		    							highPassSigma,
		    							0.01);
	    						for (int nPix=0;nPix<length;nPix++){
	    							highPassDiff[station][nView][c][nPix]=
	    								diffFromAverage[station][nView][c][nPix]-
	    								((weightsHighPass[nPix]>0)?(highPassDiff[station][nView][c][nPix]/weightsHighPass[nPix]):0.0);
	    						}	    				
	    					}
	    				} else {
    	    				highPassDiff[station][nView]=diffFromAverage[station][nView].clone(); // shallow
	    				}
	    				
	    				if (lowPassSigma>0.0){
	    					double [] weightsLowPass=weightsHighLowPass.clone();
	    					(new DoubleGaussianBlur()).blurDouble(
	    							weightsLowPass,
	    							width,
	    							height,
	    							lowPassSigma,
	    							lowPassSigma,
	    							0.01);
	    					lowPassDiff[station][nView]=new double [3][];
	    					for (int c=0;c<3;c++){
	    						lowPassDiff[station][nView][c]=highPassDiff[station][nView][c].clone();
	    						for (int nPix=0;nPix<length;nPix++){
	    							lowPassDiff[station][nView][c][nPix]*=weightsHighLowPass[nPix];
	    						}	    				
		    					(new DoubleGaussianBlur()).blurDouble(
		    							lowPassDiff[station][nView][c],
		    							width,
		    							height,
		    							lowPassSigma,
		    							lowPassSigma,
		    							0.01);
	    						for (int nPix=0;nPix<length;nPix++){
	    							lowPassDiff[station][nView][c][nPix]=
	    								(weightsLowPass[nPix]>0)?(lowPassDiff[station][nView][c][nPix]/weightsLowPass[nPix]):0.0;
	    						}	    				
	    					}
	    				} else {
    	    				lowPassDiff[station][nView]=highPassDiff[station][nView].clone(); // shallow
    					}

	    				
	    				
// TODO: display, calculate new weight from filtered difference.	    				
// Calculate new weight
	    				for (int nPix=0;nPix<length;nPix++){
	    					if (photometrics[station][nView][3][nPix]>0.0){
	    						double e2=0.0;
	    						for (int c=0;c<3;c++){
	    							double d=lowPassDiff[station][nView][c][nPix];
//	    							double d=scale*photometrics[station][nView][c][nPix]-average[c][nPix];
//	    							diffFromAverage[station][nView][c][nPix]=d;
	    							if (!positiveDiffOnly || (d>0)) e2+=d*d;
	    						}
	    						weights[station][nView][nPix]=1.0/(e2/threshold23+1.0);
	    					} else {
	    						weights[station][nView][nPix]=0.0;
	    					}
	    				}
	    				
	    			}
	    		}
	    	}
	    	if ((this.debugLevel>=debugThreshold) && showDebug) {
	    		String [] titles = new String [weights.length*weights[0].length];
	    		double [][] debugData= new double [weights.length*weights[0].length][];
	        	for (int station=0;station<patternParameters.getNumStations();station++){
	        		for (int nView=0;nView<patternParameters.getNumViews();nView++) {
	        			int n=station*weights[0].length+nView;
	        			titles[n]="S"+station+" V"+nView;
	        			if (photometrics[station][nView]!=null){
	        				debugData[n]=weights[station][nView];
	        			}
	        		}
	        	}
	        	(new showDoubleFloatArrays()).showArrays(
	        			debugData,
	        			width,
	        			height,
	        			true,
	        			"GridWeights"+nIter,
	        			titles);
	        	double [][] debugDiffGreen= new double [weights.length*weights[0].length][];
	        	double [][] debugHighpassDiffGreen= new double [weights.length*weights[0].length][];
	        	double [][] debugLowpassDiffGreen= new double [weights.length*weights[0].length][];
	        	for (int station=0;station<patternParameters.getNumStations();station++){
	        		for (int nView=0;nView<patternParameters.getNumViews();nView++) {
	        			int n=station*weights[0].length+nView;
	        			debugDiffGreen[n]=diffFromAverage[station][nView][1];
	        			debugHighpassDiffGreen[n]=highPassDiff[station][nView][1];
	        			debugLowpassDiffGreen[n]=lowPassDiff[station][nView][1];
	        		}
	        	}
	        	if (this.debugLevel>=(debugThreshold+1)) (new showDoubleFloatArrays()).showArrays(
	        			debugDiffGreen,
	        			width,
	        			height,
	        			true,
	        			"DiffGreen"+nIter,
	        			titles);
	        	if (this.debugLevel>=(debugThreshold+1)) (new showDoubleFloatArrays()).showArrays(
	        			debugHighpassDiffGreen,
	        			width,
	        			height,
	        			true,
	        			"HighpassGreen"+nIter,
	        			titles);
	        	
	        	(new showDoubleFloatArrays()).showArrays(
	        			debugLowpassDiffGreen,
	        			width,
	        			height,
	        			true,
	        			"LowpassGreen"+nIter,
	        			titles);
	        	
	        	String [] averageTitles={"Red","Green","Blue","Weight"};
	        	(new showDoubleFloatArrays()).showArrays(
	        			average,
	        			width,
	        			height,
	        			true,
	        			"Average-"+nIter,
	        			averageTitles);
	        	
	    	}
    	} // for (int nIter=0;nIter<numIterations;nIter++){
    	// Apply new weights
    	if (apply) {
    		for (int station=0;station<patternParameters.getNumStations();station++){
    			for (int nView=0;nView<patternParameters.getNumViews();nView++) {
    				if (photometrics[station][nView]!=null){
    					for (int nPix=0;nPix<length;nPix++) photometrics[station][nView][3][nPix]*=weights[station][nView][nPix];
    				}
    			}
    		}
    	}
    	
    }

    /**
     * 
     * @param shrink sensor mask by this amount (sensor, non-decimated pixels)
     * @param radius radial mask - zero if farther than radius, 0.5*(cos(pi*r/radius)+1.0) if less 
     * @param minimalAlpha - zero mask below this threshold 
     * @return returns arrray with the same size as sensorMask that corresponds to low-vignetting areas of the sensor/lens 
     */
    // Using station 0 - should be not much difference
    public double [][] nonVignettedMasks(
    		double shrink,
    		double radius,
    		double minimalAlpha){
    	if (this.pixelCorrection==null){
    		initSensorCorrection();
    	}
    	double [][]masks=new double [this.pixelCorrection.length][];
    	int maskIndex=2;
    	for (int numSensor=0;numSensor<masks.length;numSensor++){
    		if (this.pixelCorrection[numSensor]==null) masks[numSensor] = null;
    		else {
    			masks[numSensor] = fittingStrategy.distortionCalibrationData.nonVignettedMask(
    					this.pixelCorrection[numSensor][maskIndex],
    	        		this.pixelCorrectionWidth,
    	        		this.pixelCorrectionHeight,
    	        		fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numSensor].px0,     // lens center X (sensor, non-decimated pix)
    	        		fittingStrategy.distortionCalibrationData.eyesisCameraParameters.eyesisSubCameras[0][numSensor].py0,     // lens center Y (sensor, non-decimated pix)
    	        		shrink,
    	        		radius,
    	        		minimalAlpha);
//    			System.out.println("nonVignettedMasks(), masks["+numSensor+"].length="+masks[numSensor].length);
    		}
    	}
    	return masks;
    }   
    
    
    public boolean LevenbergMarquardt(boolean openDialog){
    	if (this.fittingStrategy==null) {
        		String msg="Fitting strategy does not exist, exiting";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
    	}
//    	fittingStrategy.distortionCalibrationData.readAllGrids(); 
    	if (openDialog && !selectLMAParameters()) return false;
    	this.startTime=System.nanoTime();
//    	while (this.seriesNumber<fittingStrategy.getNumSeries()){ // TODO: Add "stop" tag to series
    	this.firstRMS=-1; //undefined
    	this.fittingStrategy.invalidateSelectedImages(this.seriesNumber); // undo any filters, only user selection of the  images will be in effect
    	while (this.fittingStrategy.isSeriesValid(this.seriesNumber)){ // TODO: Add "stop" tag to series
    		this.currentVector=null; // invalidate for the new series
    		boolean wasLastSeries=false;
    		while (true) { // loop for the same series
    			
    			boolean [] state=stepLevenbergMarquardtFirst(this.seriesNumber);
    			if (!this.fittingStrategy.isSeriesValid(this.seriesNumber)){
    				System.out.println("Series "+this.seriesNumber+" is invalid when weight function filters are applied (probably removed some images)");
    				return false;
    			}
    			if (state==null) {
    				String msg="Calculation aborted by user request";
    				IJ.showMessage(msg);
    				System.out.println(msg);
    				return false; 
    			}
    			
    			if (this.debugLevel>1) System.out.println(this.seriesNumber+":"+this.iterationStepNumber+": stepLevenbergMarquardtFirst("+this.seriesNumber+")==>"+state[1]+":"+state[0]);
				boolean cont=true;
				// Make it success if this.currentRMS<this.firstRMS even if LMA failed to converge
				if (state[1] && !state[0] && (this.firstRMS>this.currentRMS)){
					if (this.debugLevel>1) System.out.println("LMA failed to converge, but RMS improved from the initial value ("+this.currentRMS+" < "+this.firstRMS+")");
					state[0]=true;
				}
    			if (
    					(this.stopRequested.get()>0) || // graceful stop requested
    					(this.stopEachStep) ||
    					(this.stopEachSeries && state[1]) ||
    					(this.stopOnFailure && state[1] && !state[0])){
    				
    				if (this.debugLevel>0){
    					if (this.stopRequested.get()>0) System.out.println("User requested stop");
    					System.out.println("LevenbergMarquardt(): series:step ="+this.seriesNumber+":"+this.iterationStepNumber+
    						", RMS="+IJ.d2s(this.currentRMS,8)+
    						" ("+IJ.d2s(this.firstRMS,8)+") "+
    						", RMSPure="+IJ.d2s(this.currentRMSPure,8)+
    						" ("+IJ.d2s(this.firstRMSPure,8)+
    						") at "+ IJ.d2s(0.000000001*(System.nanoTime()-this.startTime),3));
    				}
    				long startDialogTime=System.nanoTime();
    				cont=dialogLMAStep(state);
    				this.stopRequested.set(0); // Will not stop each run
    				this.startTime+=(System.nanoTime()-startDialogTime); // do not count time used by the User.
    				if (this.showThisImages) showDiff (this.currentfX, "fit-"+this.iterationStepNumber);
    				if (this.showNextImages) showDiff (this.nextfX,    "fit-"+(this.iterationStepNumber+1));
    			}
				stepLevenbergMarquardtAction(); // apply step - in any case?
				if (this.updateStatus){
   	   				IJ.showStatus(this.seriesNumber+": "+"Step #"+this.iterationStepNumber+
   	   						" RMS="+IJ.d2s(this.currentRMS,8)+
   	   						" ("+IJ.d2s(this.firstRMS,8)+")"+
   	   						" RMSPure="+IJ.d2s(this.currentRMSPure,8)+
   	   						" ("+IJ.d2s(this.firstRMSPure,8)+")"+
   	   						" ");
//   	   				showStatus(this.seriesNumber+": "+"Step #"+this.iterationStepNumber+" RMS="+IJ.d2s(this.currentRMS,8)+ " ("+IJ.d2s(this.firstRMS,8)+")",0);
				}
				if (!cont){
					if (this.saveSeries) {
						saveFittingSeries(); // will save series even if it ended in failure, vector will be only updated
						updateCameraParametersFromCalculated(true); // update camera parameters from all (even disabled) images
						updateCameraParametersFromCalculated(false); // update camera parameters from enabled only images (may overwrite some of the above)
					}
					// if RMS was decreased. this.saveSeries==false after dialogLMAStep(state) only if "cancel" was pressed 
					return this.saveSeries; // TODO: Maybe change result?
				}
//stepLevenbergMarquardtAction();    			
    			if (state[1]) {
    				if (!state[0]) return false; // sequence failed
    				saveFittingSeries();
					updateCameraParametersFromCalculated(true); // update camera parameters from all (even disabled) images
					updateCameraParametersFromCalculated(false); // update camera parameters from enabled only images (may overwrite some of the above)
					wasLastSeries=this.fittingStrategy.isLastSeries(this.seriesNumber);
    				this.seriesNumber++;
    				break; // while (true), proceed to the next series
    			}
    		}
//    		if (this.fittingStrategy.isLastSeries(this.seriesNumber)) break;
    		if (wasLastSeries) break;
//    		this.seriesNumber++;		
    	} // while (this.fittingStrategy.isSeriesValid(this.seriesNumber)){ // TODO: Add "stop" tag to series
		if (this.debugLevel>0) System.out.println("LevenbergMarquardt(): series="+this.seriesNumber+
				", RMS="+this.currentRMS+
				" ("+this.firstRMS+") "+
				", RMSPure="+this.currentRMSPure+
				" ("+this.firstRMSPure+
				") at "+ IJ.d2s(0.000000001*(System.nanoTime()-this.startTime),3));
    	return true; // all series done
    	
    }
    
    
    /**
     * Show debug image (see showDiff (int imgNumber, double [] fX, String title ) above)
     * for each image used in the current fitting series
     * @param fX - calculated data for all images (use with this.Y)
     * @param title - Image title
     */
    	public void showDiff (double [] fX, String title ){
    		boolean [] selectedImages=fittingStrategy.selectedImages();
    		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
    			showDiff (imgNum, fX, title+"-"+imgNum);
    		}
    	}

	/**
	 * Shows a 7-slice image for provided f(X) array (this.Y is also used):
	 * 1 - distance   - sqrt (dx^2+dy^2)
	 * 2 - difference for pixel-X
	 * 3 - difference for pixel-Y
	 * 4 - calculated pixel-X
	 * 5 - calculated pixel-Y
	 * 6 - measured   pixel-X
	 * 7 - measured   pixel-Y 
	 * @param imgNumber - number of image
	 * @param fX - calculated data for all images (use with this.Y)
	 * @param title - Image title
	 */

	public void showDiff (int imgNumber, double [] fX, String title ){
		String [] titles={"distance","diff-X","diff-Y","f(x)-X","f(x)-Y","y-X","y-Y"};
		double [] diff=calcYminusFx(fX);
// find data range for the selected image
		int index=0;
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		boolean [] selectedImages=fittingStrategy.selectedImages();
		for (int imgNum=0;(imgNum<imgNumber) && (imgNum<numImg) ;imgNum++) if (selectedImages[imgNum])
			index+=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;
		int width=getGridWidth();
		if (this.debugLevel>1) {
			System.out.println("showDiff("+imgNumber+",...): fX.length="+fX.length+" this image index="+index);
		}
		double [][] imgData=new double[7][getGridHeight() * width];
		for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
		
		for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV.length;i++){
			int u=fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV[i][0]+patternParameters.U0;
			int v=fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV[i][1]+patternParameters.V0;
			int vu=u+width*v;
			imgData[0][vu]=   Math.sqrt(diff[2*(index+i)]*diff[2*(index+i)] + diff[2*(index+i)+1]*diff[2*(index+i)+1]);
			imgData[1][vu]=   diff[2*(index+i)]; // out of bound 1410
			imgData[2][vu]=   diff[2*(index+i)+1];
			imgData[3][vu]=     fX[2*(index+i)];
			imgData[4][vu]=     fX[2*(index+i)+1];
			imgData[5][vu]= this.Y[2*(index+i)];
			imgData[6][vu]= this.Y[2*(index+i)+1];
		}
		this.SDFA_INSTANCE.showArrays(imgData, width, getGridHeight(),  true, title, titles);
	}
	/**
	 * Calculates corrections to X and Y coordinates of the grid nodes
	 * @param variationPenalty - cost of different Z for different stations
	 * @param fixXY - if true, do not adjust X,Y - only Z
	 * @param stationGroupsIn - consider some stations have the same pattern - assign them the same number. Negative - do not process the station
	 * @param grid3DCorrection - if true - calculate 3d correction, false - slow 3d (2d perpendicular to view) 
	 * @param maxZCorr - maximal allowed correction in Z-direction (if wants more, will fall back to 2-d correction (perpendicular to the view)
	 * @param showIndividual - show individual images
	 * @return  combination of 3 arrays: 1 (original) - first index - 0 - correction x (mm), 1 - correction y(mm), 2 - correction z(mm)  3 - weight (number of images used)
	 * 2 - gridZCorr3d - per station differential Z correction
	 * 3 - gridZCorr3dWeight - per station weight of Z-corrections
	 */

	public double [][][] calculateGridXYZCorr3D(
			double variationPenalty,
            boolean fixXY,
            int [] stationGroupsIn,
			boolean grid3DCorrection,
			boolean rotateCorrection,
			double maxZCorr,
			boolean noFallBack,
			boolean showIndividual,
			int threadsMax,
			boolean updateStatus
			){
		int debugThreshold=2;
		// Normalize stationGroups
		int numStations=fittingStrategy.distortionCalibrationData.getNumStations();
		int [] stationGroups=new int [numStations];
		int [] stationGroupsTmp=(stationGroupsIn==null)?(new int [0]):stationGroupsIn.clone();
		for (int i=0;i<numStations;i++) stationGroups[i]=-1;
		int numberOfZGroups=0;
		for (int i=0;i<stationGroupsTmp.length;i++) if (stationGroupsTmp[i]>=0){
			for (int j=i;j<stationGroupsTmp.length;j++) if (stationGroupsTmp[j]==stationGroupsTmp[i]){
				stationGroups[j]=numberOfZGroups;
				if (j>i) stationGroupsTmp[j]=-1;
			}
			numberOfZGroups++;
		}
		if (numberOfZGroups==0) {
			System.out.println ("calculateGridXYZCorr3D(), no groups defined - using a single group for all stations");
			numberOfZGroups=1;
			for (int i=0;i<numStations;i++) stationGroups[i]=0;
		}
		if (this.debugLevel>1) {
			System.out.println ("calculateGridXYZCorr3D(), groups: "+numberOfZGroups);
			for (int i=0;i<stationGroups.length;i++) if (stationGroups[i]>=0){
				System.out.println ("  station "+i+": group "+stationGroups[i]);
			}
		}

		int width=getGridWidth();
		int height=getGridHeight();
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [][] cameraXYZ=new double [selectedImages.length][];

		double [][][] gridCorr2d=calculateGridXYZCorr2D(
				width,
				height,
				stationGroups,
				selectedImages,
				cameraXYZ,
				this.lensDistortionParameters,
				showIndividual,
				threadsMax,
				updateStatus);
		
		
		IJ.showStatus("Calculating pattern 3d correction...");
// now using gridCorr2d[imgNum], cameraXYZ[imgNum] and patternParameters.gridGeometry[v][u] find the 3d correction     public double [][][] gridGeometry=null; // [v][u]{x,y,z,"alpha"} alpha=0 - no ghrid, 1 - grid
		double [][] gridCorr3d=  new double [4][width*height]; 
		double [][] gridZCorr3d =new double [numStations][width*height];
		double [][] gridZCorr3dWeight =new double [numStations][width*height];
		for (int n=0;n<gridCorr3d.length;n++) for (int i=0;i<gridCorr3d[0].length;i++) gridCorr3d[n][i]=0.0;
		for (int n=0;n<gridZCorr3d.length;n++) for (int i=0;i<gridZCorr3d[0].length;i++){
			gridZCorr3d[n][i]=0.0;
			gridZCorr3dWeight[n][i]=0.0;
		}
		double Cx,Cy,Cz,Cxy,Cxz,Cyz;
		double [] V= new double[3];
		double [] V2= new double[3];
		int debugIndex=(height/2)*width+ (width/2);
		int debugIndex1=(height/2)*width+ (width/4);
		double [] alphaStation=new double [numStations];
		int zIndex=fixXY?0:2;
		int numVariables=numberOfZGroups+zIndex;
		double [][] aM=new double [numVariables][numVariables];
		double [][] aB=new double [numVariables][1];
		double []   zPerStation= new double [numStations];

		for (int v=0;v<height;v++) for (int u=0;u<width; u++){
			int index=u+v*width;
			boolean thisDebug=(this.debugLevel>debugThreshold) && ((index==debugIndex) || (index==debugIndex1));
			if (thisDebug) System.out.println("calculateGridXYZCorr3D() debug("+this.debugLevel+"): index="+index+" v="+v+" u="+u);
			for (int i=0;i<numVariables;i++){
				aB[i][0]=0.0;
				for (int j=0;j<numVariables;j++) aM[i][j]=0.0; 
			}
			for (int i=0;i<numStations;i++) alphaStation[i]=0.0;
			double alpha=0.0;
			boolean fallBack2D=true;
			if (grid3DCorrection) {
				for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
					int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
					int zGroup=stationGroups[station];
					if ((gridCorr2d[imgNum]!=null)  && (gridCorr2d[imgNum][3][index]>0.0) && (zGroup>=0)) {
						zPerStation[station]=gridCorr2d[imgNum][2][index]; // should all be the same for the same station
						// calculate unity vector from the camera lens to the grid point
						double absV=0.0;
						for (int i=0;i<V.length;i++){
							V[i]=patternParameters.gridGeometry[v][u][i]+gridCorr2d[imgNum][i][index]-cameraXYZ[imgNum][i]; // corrected value, including zCorr
							absV+=V[i]*V[i];
						}
						absV=Math.sqrt(absV);
						if (absV>0) for (int i=0;i<V.length;i++) V[i]/=absV;
						for (int i=0;i<V.length;i++) V2[i]=V[i]*V[i];
						if (thisDebug) System.out.println(" imgNum="+imgNum+" V[0]="+IJ.d2s(V[0],4)+" V[1]="+IJ.d2s(V[1],4)+" V[2]="+IJ.d2s(V[2],4)+
								" V2[0]="+IJ.d2s(V2[0],4)+" V2[1]="+IJ.d2s(V2[1],4)+" V2[2]="+IJ.d2s(V2[2],4));
						// When performin 3-d correction (X,Y,Z) the result point has to have minimal weighted sum of squared distances to all rays 
// when summing for different stations, multiply W by sign(image belongs to station) 	
/*
Px, Py - calculated correction for individual image
V={Vx,Vy,Vz} unity vector from the camera lens center to the {Px,Py,0}
A - vector from the {Px,Py,0} to {X,Y,Z} = {X-Px,Y-Py,Z}
Projection of A on V will have length of A(.)V, Vector B=V*(A(.)V)
Vector D=A-B = A - V*(A(.)V)
D2=D(.)D= A(.)A - 2* (A(.)V ) * (A(.)V ) + (A(.)V ) * (A(.)V ) = A(.)A -  (A(.)V ) * (A(.)V )
D2=A(.)A -  (A(.)V )^2

A(.)A=(X-Px)^2 + (Y-Py)^2 + Z^2 =X^2 -2*X*Px +Px^2 +Y^2 -2*Y*Py +Py^2 +Z^2
A(.)A=X^2 -2*X*Px +Px^2 +Y^2 -2*Y*Py +Py^2 +Z^2
A(.)V=      (X-Px)*Vx + (Y-Py)*Vy + Z*Vz
(A(.)V)^2= ((X-Px)*Vx + (Y-Py)*Vy + Z*Vz)^2 = ((X-Px)*Vx)^2 + ((Y-Py)*Vy)^2 + (Z*Vz)^2 + 2*((X-Px)*Vx)*((Y-Py)*Vy)+ 2*((X-Px)*Vx)*(Z*Vz)+2*((Y-Py)*Vy)*(Z*Vz)
(A(.)V)^2= X^2*Vx^2 +Px^2*Vx^2 - 2*X*Px*Vx^2 +Y^2*Vy^2+Py^2*Vy^2-2*Y*Py*Vy^2 +Z^2*Vz^2 +2*X*Y*Vx*Vy +2*Px*Py*Vx*Vy - 2*X*Py*Vx*Vy - 2*Y*Px*Vx*Vy +2*X*Z*Vx*Vz - 2*Z*Px*Vx*Vz +2*Y*Z*Vy*Vz -2*z*Py*Vy*Vz

D2=
  +X^2 - X^2*Vx^2
  +Y^2 - Y^2*Vy^2 
  +Z^2 - Z^2*Vz^2
-2*X*Y* Vx*Vy
-2*X*Z* Vx*Vz
-2*Y*Z* Vy*Vz
-2*X*Px +2*X*Px*Vx^2+ 2*X*Py*Vx*Vy
-2*Y*Py +2*Y*Py*Vy^2+ 2*Y*Px*Vx*Vy
+2*Z*Px*Vx*Vz   +2*Z*Py*Vy*Vz
+Px^2  +Py^2 -Px^2*Vx^2   -Py^2*Vy^2    -2*Px*Py*Vx*Vy

0= dD2/dX/2= X*(1-Vx^2) - Y* Vx*Vy - Z* Vx*Vz -Px + Px*Vx^2  + Py*Vx*Vy
0= dD2/dY/2= Y*(1-Vy^2) - X* Vx*Vy - Z* Vy*Vz -Py + Py*Vy^2  + Px*Vx*Vy
0= dD2/dZ/2= Z*(1-Vz^2) - X* Vx*Vz - Y* Vy*Vz     + Px*Vx*Vz + Py*Vy*Vz


 X*(Vx^2-1) + Y* (Vx*Vy)  + Z* (Vx*Vz)   =  Px * (Vx^2-1)  + Py* (Vx*Vy)
 X*(Vx*Vy)  + Y* (Vy^2-1) + Z* (Vy*Vz)   =  Px * (Vx*Vy)   + Py * (Vy^2-1)
 X*(Vx*Vz)  + Y* (Vy*Vz)  + Z* (Vz^2-1)  =  Px * (Vx*Vz)   + Py* (Vy*Vz)

						
 */
				
//   | sum(Wi*Cxi),  sum(Wi*Cxyi), sum(Wi*Cxzi) |
//M= | sum(Wi*Cxyi), sum(Wi*Cyi ), sum(Wi*Cyzi) |
//   | sum(Wi*Cxzi), sum(Wi*Cyzi), sum(Wi*Czi ) |
				
//   | sum(Wi*(P0xi*Cxi + P0yi*Cxyi + P0zi*Cxzi)) |
//B= | sum(Wi*(P0yi*Cyi + P0xi*Cyxi + P0zi*Cyzi)) |
//   | sum(Wi*(P0zi*Czi + P0yi*Czyi + P0xi*Czxi)) |
/*				
	X*(Vxi^2-1) + Y*(Vxi*Vyi) + Z*(Vxi*Vzi) = P0xi*(Vxi^2-1) +P0yi*(Vxi*Vyi) + P0zi*(Vxi*Vzi)
	X*(Vxi*Vyi) + Y*(Vyi^2-1) + Z*(Vyi*Vzi) = P0xi*(Vxi*Vyi) +P0yi*(Vyi^2-1) + P0zi*(Vyi*Vzi)
	X*(Vxi*Vzi) + Y*(Vxi*Vyi) + Z*(Vzi^2-1) = P0xi*(Vxi*Vzi) +P0yi*(Vxi*Vyi) + P0zi*(Vzi^2-1)

	X*Cx  + Y*Cxy + Z*Cxz = P0xi*Cx  +P0yi*Cxy + P0zi*Cxz
	X*Cxy + Y*Cy  + Z*Cyz = P0xi*Cxy +P0yi*Cy  + P0zi*Cyz
	X*Cxz + Y*Cyz + Z*Cz  = P0xi*Cxz +P0yi*Cyz + P0zi*Cz
	P0zi==0.0, so - now we'll use P0zi - difference from this station to average

	X*Cx  + Y*Cxy + Z*Cxz = P0xi*Cx  +P0yi*Cxy
	X*Cxy + Y*Cy  + Z*Cyz = P0xi*Cxy +P0yi*Cy
	X*Cxz + Y*Cyz + Z*Cz  = P0xi*Cxz +P0yi*Cyz

*/
						Cx=V2[0]-1.0;
						Cy=V2[1]-1.0;
						Cz=V2[2]-1.0;
						Cxy= V[0]*V[1];
						Cxz= V[0]*V[2];
						Cyz= V[1]*V[2];
						if (thisDebug) System.out.println(" Cx="+IJ.d2s(Cx,6)+" Cy="+IJ.d2s(Cy,6)+" Cz="+IJ.d2s(Cz,6)+
								" Cxy="+IJ.d2s(Cxy,6)+" Cxz="+IJ.d2s(Cxz,6)+" Cyz="+IJ.d2s(Cyz,6));


						double W=gridCorr2d[imgNum][3][index];
						double Px=gridCorr2d[imgNum][0][index];
						double Py=gridCorr2d[imgNum][1][index];
						double Pz=gridCorr2d[imgNum][2][index];
						alpha+=W;
						alphaStation[station]+=W;
						if (thisDebug) System.out.println(imgNum+": Px="+IJ.d2s(Px,6)+" Py="+IJ.d2s(Py,6)+" W="+IJ.d2s(W,6));
						if (zIndex>0){ // X,Y correction is enabled, not only Z
							aM[0][0]+=W*Cx;
							aM[0][1]+=W*Cxy;
							aM[1][1]+=W*Cy;
							aM[0][2+zGroup]+=W*Cxz;
							aM[1][2+zGroup]+=W*Cyz;
							aB[0][0]+=W*(Px*Cx  + Py*Cxy + Pz*Cxz);
							aB[1][0]+=W*(Px*Cxy + Py*Cy  + Pz*Cyz);
						}
						aM[zIndex+zGroup][zIndex+zGroup]+=W*(Cz-variationPenalty); // -1>>Cz<0
						aB[zIndex+zGroup][0]+=W*(Px*Cxz + Py*Cyz + Pz*Cz);
					}
				}
				if (zIndex>0){// X,Y correction is enabled, not only Z
					aM[1][0]+=aM[0][1]; // why "+=" - just "="
					for (int zGroup=0;zGroup<numberOfZGroups;zGroup++){
						aM[zIndex+zGroup][0]+=aM[0][zIndex+zGroup];
						aM[zIndex+zGroup][1]+=aM[1][zIndex+zGroup];
					}
				}
				Matrix M=new Matrix(aM);
				Matrix B=new Matrix(aB);
				if (thisDebug) {
					System.out.println(" M:");
					M.print(8, 6);
					System.out.println(" B:");
					B.print(8, 6);
				}

				//			boolean fallBack2D=true;
				if ((new LUDecomposition(M)).isNonsingular()){
					double [] dXYZ=M.solve(B).getRowPackedCopy();
//// Now save per station group (with weights)
					if (zIndex>0){// X,Y correction is enabled, not only Z
						for (int i=0;i<2;i++) gridCorr3d[i][index]=dXYZ[i];
					}
					double zAverage=0.0;
					double sumW=0;
					for (int station=0;station<numStations;station++){
						double w=alphaStation[station];
						sumW+=w;
						gridZCorr3dWeight[station][index]=w;
						int zGroup=stationGroups[station];
						zAverage+=w*dXYZ[zIndex+zGroup];
					}
					if (sumW>0.0) {
						zAverage/=sumW;
						gridCorr3d[2][index]=zAverage; // weighted average of grid Z correction (from current pattern Z)
						gridCorr3d[3][index]=alpha; // same as sumW?
// second pass - calculate per-station Z corrections - referenced to existent current values						
//zPerStation[station]
						for (int station=0;station<numStations;station++){
							int zGroup=stationGroups[station];
//							gridZCorr3d[station][index]=dXYZ[zIndex+zGroup]-zPerStation[station]; // differential from the current pattern geometry
							gridZCorr3d[station][index]=dXYZ[zIndex+zGroup]-zAverage; // differential from the current pattern geometry
						}
					}
					fallBack2D=false; //TODO:  make sure delta Z (Math.abs(gridCorr3d[2][index])) is not too big!!
					
					if (Math.abs(gridCorr3d[2][index])>maxZCorr) {
						fallBack2D=true; // temporary limit
					}
					if (thisDebug) System.out.println(" dX="+IJ.d2s(gridCorr3d[0][index],6)+" dY="+IJ.d2s(gridCorr3d[1][index],6)+" dZ="+IJ.d2s(gridCorr3d[2][index],6));
				}
			}
			if(fallBack2D && !(grid3DCorrection && noFallBack)) { // make a 2d averaging of weighted dx, dy correction - separately for each station group
				double [] gridZcorrPerGroup=      new double [numberOfZGroups];
				double [] gridZcorrAddPerGroup=      new double [numberOfZGroups];
				double [] gridZcorrWeightPerGroup=new double [numberOfZGroups];
				for (int i=0;i<numberOfZGroups;i++){
					gridZcorrPerGroup[i]=0.0;
					gridZcorrWeightPerGroup[i]=0.0;
					gridZcorrAddPerGroup[i]=0.0;
				}
				for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
					int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
					int zGroup=stationGroups[station];
					if ((gridCorr2d[imgNum]!=null)  && (gridCorr2d[imgNum][3][index]>0.0) && (zGroup>=0)) {
						double w=gridCorr2d[imgNum][3][index];
						double z=gridCorr2d[imgNum][2][index]; // difference from average Z
						gridZcorrPerGroup[zGroup]+=w*z;
						gridZcorrWeightPerGroup[zGroup]+=w;
					}
				}
				for (int i=0;i<numberOfZGroups;i++) if (gridZcorrWeightPerGroup[i]>0.0) gridZcorrPerGroup[i]/=gridZcorrWeightPerGroup[i];
				for (int i=0;i<gridCorr3d.length;i++) gridCorr3d[i][index]=0.0;
				
				double s=0;
				for (int imgNum=0;imgNum<selectedImages.length;imgNum++) {
					int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
					int zGroup=stationGroups[station];
					if ((gridCorr2d[imgNum]!=null)  && (gridCorr2d[imgNum][3][index]>0.0) && (zGroup>=0)) {
						double z=patternParameters.gridGeometry[v][u][2]+gridZcorrPerGroup[zGroup];
						double [] cv={
								patternParameters.gridGeometry[v][u][0]-cameraXYZ[imgNum][0],
								patternParameters.gridGeometry[v][u][1]-cameraXYZ[imgNum][1],
								z-cameraXYZ[imgNum][2]};
						double cv2=cv[0]*cv[0]+cv[1]*cv[1]+cv[2]*cv[2];
						double acv=Math.sqrt(cv2);
						for (int i=0;i<3;i++)cv[i]/=acv; // make unity vector;
						// intersection of the corrected view ray with the average taget plane
						double [] dXYplane0={-gridZcorrPerGroup[zGroup]/cv[2]*cv[0],-gridZcorrPerGroup[zGroup]/cv[2]*cv[1]};
						double [] modCorrXY={gridCorr2d[imgNum][0][index]+dXYplane0[0], gridCorr2d[imgNum][1][index]+dXYplane0[1]};
						double kv=(modCorrXY[0]*cv[0]+modCorrXY[1]*cv[1])/cv2;
						double w=gridCorr2d[imgNum][3][index];
						gridCorr3d[0][index]+=w*(gridCorr2d[imgNum][0][index]-cv[0]*kv);
						gridCorr3d[1][index]+=w*(gridCorr2d[imgNum][1][index]-cv[1]*kv);
						gridZcorrAddPerGroup[zGroup]+=w*(                            -cv[2]*kv);
// not finished per station/per group 2d correction, will just use corerction average						
						gridCorr3d[2][index]+=w*(                            -cv[2]*kv);
						s+=w;
					}
				}
				for (int i=0;i<numberOfZGroups;i++) if (gridZcorrWeightPerGroup[i]>0.0) gridZcorrAddPerGroup[i]/=gridZcorrWeightPerGroup[i];
				for (int station=0;station<numStations;station++){
					int zGroup=stationGroups[station];
					gridZCorr3d[station][index]=gridZcorrAddPerGroup[zGroup]; // differential from the current pattern geometry
				}
				if (s>0){
					gridCorr3d[0][index]/=s;
					gridCorr3d[1][index]/=s;
					gridCorr3d[2][index]/=s;
				} else {
					gridCorr3d[0][index]=0.0;
					gridCorr3d[1][index]=0.0;
					gridCorr3d[2][index]=0.0;
				}
				gridCorr3d[3][index]=s;
				if (thisDebug) System.out.println(" Using 2d averaging: dX="+IJ.d2s(gridCorr3d[0][index],6)+
						" dY="+IJ.d2s(gridCorr3d[1][index],6)+" dZ="+IJ.d2s(gridCorr3d[2][index],6));
			}
		}
		// Make average correction zero is it needed?
		// create "reliable" mask for averaging/tilting - disregard the outmost grid pixels
		boolean [] reliable=new boolean [width*height];
		double wThreshold=0.0;
		for (int v=0;v<height;v++) for (int u=0;u<width;u++){
			int index=u+v*width;
			reliable[index]=false;
			if ((v>0) && (u>0) && (v<(height-1)) && (u<(width-1)) &&
					(gridCorr3d[3][index]>wThreshold) &&
					(gridCorr3d[3][index-1]>wThreshold) &&
					(gridCorr3d[3][index+1]>wThreshold) &&
					(gridCorr3d[3][index-width]>wThreshold) &&
					(gridCorr3d[3][index+width]>wThreshold) ){
				reliable[index]=true;
			}

		}
		double corrAverage;
		for (int c=0;c<3;c++){ 
			corrAverage=0.0;
			double s=0.0;
			for (int i=0;i<gridCorr3d[0].length;i++) if (reliable[i]) {
				corrAverage+=gridCorr3d[c][i]*gridCorr3d[3][i];
				s+=gridCorr3d[3][i];
			}
			corrAverage/=s;
			//			System.out.println("zCorrAverage["+c+"="+corrAverage);
			for (int i=0;i<gridCorr3d[c].length;i++) gridCorr3d[c][i]-=corrAverage;
		}
		// for Z correction compensate for x/y tilts
		String [] titles={"X-correction(mm)","Y-correction(mm)","Z-correction","Weight"};
		if (rotateCorrection) {
			double SX=0.0,SX2=0.0,SZ=0.0,SXY=0.0,SXZ=0.0,S0=0.0,SY=0.0,SY2=0.0,SYZ=0.0;
			double [][] gridGeom=new double [3][gridCorr3d[0].length];
			for (int c=0;c<gridGeom.length;c++) for (int i=0;i<gridGeom[c].length;i++)gridGeom[c][i]=0.0;

			for (int v=0;v<height;v++) for (int u=0;u<width; u++){
				int index=u+v*width;
				double W=gridCorr3d[3][index];
				gridGeom[0][index]=patternParameters.gridGeometry[v][u][0];
				gridGeom[1][index]=patternParameters.gridGeometry[v][u][1];
				gridGeom[2][index]=W;
				if ((reliable[index]) && (W>0.0)){
					S0+=W;
					SX+=  W*patternParameters.gridGeometry[v][u][0];
					SX2+= W*patternParameters.gridGeometry[v][u][0]*patternParameters.gridGeometry[v][u][0];
					SY+=  W*patternParameters.gridGeometry[v][u][1];
					SY2+= W*patternParameters.gridGeometry[v][u][1]*patternParameters.gridGeometry[v][u][1];
					SXY+= W*patternParameters.gridGeometry[v][u][0]*patternParameters.gridGeometry[v][u][1];
					SZ+=  W*gridCorr3d[2][index];
					SXZ+= W*gridCorr3d[2][index]*patternParameters.gridGeometry[v][u][0];
					SYZ+= W*gridCorr3d[2][index]*patternParameters.gridGeometry[v][u][1];
				}
			}
			double [][] aM1= {
					{SX2, SXY, SX},
					{SXY, SY2, SY},
					{SX,  SY,  S0}};
			double [][] aB1= {{SXZ},{SYZ},{SZ}};
			Matrix M=new Matrix(aM1);
			Matrix B=new Matrix(aB1);
			if (this.debugLevel>2) {
				System.out.println(" M:");
				M.print(8, 6);
				System.out.println(" B:");
				B.print(8, 6);
				System.out.println(" Ax,Ay,B:");
				M.solve(B).print(8, 6);
			}
			double [] tilts=M.solve(B).getRowPackedCopy(); // singular ???
			if (this.debugLevel>2) {
				if (this.refineParameters.showThisCorrection) {
					this.SDFA_INSTANCE.showArrays(gridCorr3d, getGridWidth(), getGridHeight(),  true, "before tilt:", titles);
				}
			}
			for (int v=0;v<height;v++) for (int u=0;u<width; u++){
				int index=u+v*width;
				gridCorr3d[2][index]-=tilts[0]*patternParameters.gridGeometry[v][u][0]+tilts[1]*patternParameters.gridGeometry[v][u][1]+tilts[2];
			}
		}
    	if (this.debugLevel>2) {
    		if (this.refineParameters.showThisCorrection) {
    			double [][] gridCorr3dClone=new double [4][width*height];
    			for (int c=0;c<gridCorr3dClone.length;c++) for (int i=0;i<gridCorr3dClone[c].length;i++)
    				gridCorr3dClone[c][i]=reliable[i]? gridCorr3d[c][i]:0.0;
    			this.SDFA_INSTANCE.showArrays(gridCorr3dClone, getGridWidth(), getGridHeight(),  true, "after tilt:", titles);
    		}
    	}
    	IJ.showStatus("");
    	
// combine in a single array?    	
    	
    	double [][][] result={gridCorr3d,gridZCorr3d,gridZCorr3dWeight};
		return  result;
	}
	public double [][][] calculateGridXYZCorr2D(
			final int width,
			final int height,
			final int [] stationGroups,
			final boolean [] selectedImages,
			final double [][] cameraXYZ,
			final LensDistortionParameters lensDistortionParametersProto,
			final boolean showIndividual,
			final int threadsMax,
			final boolean updateStatus
			){
		final int [][] dirs=            {{0,0},{-1,0},{1,0},{0,-1},{0,1}}; // possible to make 8 directions
		final double [][][] derivatives={ // for of /du, /dv 3 variants, depending on which neighbors are available
				{
					{ 0.0,-0.5, 0.5, 0.0, 0.0},
					{ 1.0,-1.0, 0.0, 0.0, 0.0},
					{-1.0, 0.0, 1.0, 0.0, 0.0}
				},
				{
					{ 0.0, 0.0, 0.0,-0.5, 0.5},
					{ 1.0, 0.0, 0.0,-1.0, 0.0},
					{-1.0, 0.0, 0.0, 0.0, 1.0}}};
		final double [][][] gridCorr2d=new double [selectedImages.length][][]; // per-image grid {dx,dy,weight} corrections
		for (int i=0;i<gridCorr2d.length;i++) {
			gridCorr2d[i]=null;
			cameraXYZ[i]=null;
		}
		// Should it be just once - common for all images? (removed from the "for" loop)
		final double [] diff=calcYminusFx(this.currentfX);
		final int debugLevel=this.debugLevel;
		final int [] imageStartIndex=this.imageStartIndex;
		final double [] Y=this.Y;
		final double [] weightFunction= this.weightFunction;
   		final Thread[] threads = newThreadArray(threadsMax);
   		final AtomicInteger imageNumberAtomic = new AtomicInteger(0);
   		final AtomicInteger imageFinishedAtomic = new AtomicInteger(0);
   		final double [] progressValues=new double [selectedImages.length];
   		int numSelectedImages=0;
   		for (int i=0;i<selectedImages.length;i++) if (selectedImages[i]) numSelectedImages++;
   		int selectedIndex=0;
   		for (int i=0;i<selectedImages.length;i++) {
   			progressValues[i]=(selectedIndex+1.0)/numSelectedImages;
   			if (selectedImages[i]) selectedIndex++;
   			if (selectedIndex>=numSelectedImages) selectedIndex--;
   		}
		IJ.showStatus("Calculating pattern geometry correction...");
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					LensDistortionParameters lensDistortionParameters=lensDistortionParametersProto.clone(); // see - if that is needed - maybe new is OK
					//   					LensDistortionParameters lensDistortionParameters= new LensDistortionParameters();
					for (int imgNum=imageNumberAtomic.getAndIncrement(); imgNum<selectedImages.length; imgNum=imageNumberAtomic.getAndIncrement()) if (selectedImages[imgNum]){
						//		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
						int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
						if (stationGroups[station]<0) continue; // do not process images that do not belong to selected stations
						gridCorr2d[imgNum]=new double [4][width*height]; // dx, dy only - added zCorr per station
						for (int n=0;n<gridCorr2d[imgNum].length;n++) for (int i=0;i<gridCorr2d[imgNum][0].length;i++) gridCorr2d[imgNum][n][i]=0.0;
						//		int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
						cameraXYZ[imgNum]=new double[3];
						// The following method sets this.lensDistortionParameters and invokes this.lensDistortionParameters.recalcCommons();
						calcInterParamers(
								lensDistortionParameters,
								null, //this.interParameterDerivatives, // [22][]
//								fittingStrategy.distortionCalibrationData.pars[imgNum], // 22-long parameter vector for the image
								fittingStrategy.distortionCalibrationData.getParameters(imgNum), // 22-long parameter vector for the image
								null); // if no derivatives, null is OK
						//				false); // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
						cameraXYZ[imgNum]=lensDistortionParameters.getLensCenterCoordinates();
						if (debugLevel>2) {
							System.out.println("calculateGridXYZCorr(): imgNum="+imgNum+" lens coordinates (mm)={"+
									IJ.d2s(cameraXYZ[imgNum][0],3)+", "+IJ.d2s(cameraXYZ[imgNum][1],3)+", "+IJ.d2s(cameraXYZ[imgNum][2],3)+"}");
						}
						//		double [] diff=calcYminusFx(this.currentfX); // removed from the loop
						// find data range for the selected image
						int index=imageStartIndex[imgNum]; // set when fitting series is init
						double [][] imgData=new double[showIndividual?7:5][getGridHeight() * width]; // dPX, dPY, Px, Py, alpha
						for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
						// first pass - prepare [v][u]arrays
						for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
							int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0;
							int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0;
							int vu=u+width*v;
							imgData[0][vu]= diff[2*(index+i)]; // out of bound 1410
							imgData[1][vu]= diff[2*(index+i)+1];
							imgData[2][vu]= Y[2*(index+i)];  // measured pixel x
							imgData[3][vu]= Y[2*(index+i)+1];// measured pixel y

							//				imgData[4][vu]= fittingStrategy.distortionCalibrationData.getMask(chnNum, imgData[2][vu], imgData[3][vu]);

							if (weightFunction!=null) {
								imgData[4][vu]= weightFunction[2*(index+i)];
							} else {
								imgData[4][vu]= 1.0;
							}
						}
						// second pass - calculate derivatives, and residuals in mm
						for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
							int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0;
							int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0;
							int vu=u+width*v;
							gridCorr2d[imgNum][0][vu]=0.0;
							gridCorr2d[imgNum][1][vu]=0.0;
							gridCorr2d[imgNum][2][vu]=patternParameters.getZCorr(vu,station); // per-station Z correction from average
							gridCorr2d[imgNum][3][vu]=0.0; // weight
							double [][] gXY =new double[dirs.length][3];
							//			double [][] gpXY=new double[dirs.length][2];
							double [][] gpXY=new double[dirs.length][3];
							boolean [] dirMask=new boolean [dirs.length];
							for (int dir=0;dir<dirs.length;dir++){
								int u1=u+dirs[dir][0];
								int v1=v+dirs[dir][1];
								int vu1=u1+width*v1;
								dirMask[dir] = (u1>=0) && (v1>=0) && (u1<width) && (v1<height) && (imgData[4][vu1]>0);
								if (dirMask[dir]){
									gXY[dir][0]= patternParameters.gridGeometry[v1][u1][0];
									gXY[dir][1]= patternParameters.gridGeometry[v1][u1][1];
									gXY[dir][2]= patternParameters.gridGeometry[v1][u1][2]; // Here - average Z
									gpXY[dir][0]=imgData[2][vu1];
									gpXY[dir][1]=imgData[3][vu1];
								} else {
									gXY[dir][0]= 0.0;
									gXY[dir][1]= 0.0;
									gXY[dir][2]= 0.0;
									gpXY[dir][0]=0.0;
									gpXY[dir][1]=0.0;

								}
							}
							int [] variants={-1,-1}; // {horizontal, vertical}
							boolean variantsExist=true;
							for (int duv=0;duv<2;duv++){ // 0 - horizontal, 1 - vertical
								for (int variant=0;variant<derivatives[duv].length;variant++) { // variants: 0 half of right/left, 1 left deriv, 2 - right deriv
									boolean fit=true;
									for (int dir=0;dir<dirs.length;dir++) if ((derivatives[duv][variant][dir]!=0) && !dirMask[dir]){
										fit=false;
										break;
									}
									if (fit) {
										variants[duv]=variant;
										break;
									}
								}
								if (variants[duv]<0) { // could not find any variant to calculate derivatives for this direction
									variantsExist=false;
									break; 
								}
							}
							if (!variantsExist){
								imgData[4][vu]=0.0;
								continue;
							}
							double [][] dXY_dUV= new double [2][2];
							double [][] dpXY_dUV=new double [2][2];
							for (int nom=0;nom<2;nom++) { // 0-x, 1 - y
								for (int denom=0;denom<2;denom++) { //0 - du, 1 - dv
									dXY_dUV [nom][denom]=0.0;
									dpXY_dUV[nom][denom]=0.0;
									for (int dir=0;dir<dirs.length;dir++){
										dXY_dUV [nom][denom]+=gXY [dir][nom]*derivatives[denom][variants[denom]][dir];
										dpXY_dUV[nom][denom]+=gpXY[dir][nom]*derivatives[denom][variants[denom]][dir];
									}
								}
							}
							double [] dpXY={imgData[0][vu],imgData[1][vu]};
							Matrix MdpXY=    new Matrix(dpXY,2); // 2 rows
							Matrix MdXY_dUV= new Matrix(dXY_dUV);
							Matrix MdpXY_dUV=new Matrix(dpXY_dUV);
							if ((new LUDecomposition(MdpXY_dUV)).isNonsingular()){
								/*
								 * MdpXY= MdpXY_dUV* MdUV
								 * MdXY=  MdXY_dUV * MdUV
								 * MdUV=  MdpXY_dUV.solve(MdpXY); 			
								 * MdXY=  MdXY_dUV * MdpXY_dUV.solve(MdpXY);
								 */
								Matrix MdXY=MdXY_dUV.times(MdpXY_dUV.solve(MdpXY));
								double [] dXY=MdXY.getRowPackedCopy();
								gridCorr2d[imgNum][0][vu]=dXY[0];
								gridCorr2d[imgNum][1][vu]=dXY[1];
								gridCorr2d[imgNum][3][vu]=imgData[4][vu]; // weight
							}
						} // end scanning pixels
						if (showIndividual) {
							String [] titles={"diff-X","diff-Y","pX","pY","alpha","X-correction(mm)","Y-correction(mm)","Z-correction(mm)"};
							(new showDoubleFloatArrays()).showArrays(imgData, width, height,  true, "Grid"+imgNum, titles);
						}
   						final int numFinished=imageFinishedAtomic.getAndIncrement();
//						IJ.showProgress(progressValues[numFinished]);
						SwingUtilities.invokeLater(new Runnable() {
							public void run() {
								// Here, we can safely update the GUI
								// because we'll be called from the
								// event dispatch thread
								IJ.showProgress(progressValues[numFinished]);
							}
						});
					}
				}
			};
		}
		startAndJoin(threads);

		IJ.showProgress(1.0);
		return gridCorr2d;
	}


	
	
	
	/**
	 * Calculates corrections to X and Y coordinates of the grid nodes
	 * //@param distortionCalibrationData - used to receive sensor mask(s)
	 * @param grid3DCorrection - if true - calculate 3d correction, false - slow 3d (2d perpendicular to view) 
	 * @param maxZCorr - maximal allowed correction in Z-direction (if wants more, will fall back to 2-d correction (perpendicular to the view)
	 * @param showIndividual - show individual images
	 * @return first index - 0 - correction x (mm), 1 - correction y(mm), 2 - correction z(mm)  3 - weight (number of images used)
	 */

	public double [][] calculateGridXYZCorr3D( // old version
//			DistortionCalibrationData distortionCalibrationData,
			boolean grid3DCorrection,
			boolean rotateCorrection,
			double maxZCorr,
			boolean showIndividual){
		int width=getGridWidth();
		int height=getGridHeight();
		int [][] dirs=            {{0,0},{-1,0},{1,0},{0,-1},{0,1}}; // possible to make 8 directions
		double [][][] derivatives={ // for of /du, /dv 3 variants, depending on which neighbors are available
				{
					{ 0.0,-0.5, 0.5, 0.0, 0.0},
					{ 1.0,-1.0, 0.0, 0.0, 0.0},
					{-1.0, 0.0, 1.0, 0.0, 0.0}
				},
				{
					{ 0.0, 0.0, 0.0,-0.5, 0.5},
					{ 1.0, 0.0, 0.0,-1.0, 0.0},
					{-1.0, 0.0, 0.0, 0.0, 1.0}}};
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [][][] gridCorr2d=new double [selectedImages.length][][]; // per-image grid {dx,dy,weight} corrections
		
		double [][] cameraXYZ=new double [selectedImages.length][];
		for (int i=0;i<gridCorr2d.length;i++) {
			gridCorr2d[i]=null;
			cameraXYZ[i]=null;
		}
		int numSelected=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) numSelected++;
		int numProcessed=0;
		IJ.showStatus("Calculating pattern geometry correction...");
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
			gridCorr2d[imgNum]=new double [3][width*height]; // dx, dy only
			for (int n=0;n<gridCorr2d[imgNum].length;n++) for (int i=0;i<gridCorr2d[imgNum][0].length;i++) gridCorr2d[imgNum][n][i]=0.0;
//			int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
			cameraXYZ[imgNum]=new double[3];
			// The following method sets this.lensDistortionParameters and invokes this.lensDistortionParameters.recalcCommons();
			calcInterParamers(
					this.lensDistortionParameters,
					null, //this.interParameterDerivatives, // [22][]
//					fittingStrategy.distortionCalibrationData.pars[imgNum], // 22-long parameter vector for the image
					fittingStrategy.distortionCalibrationData.getParameters(imgNum), // 22-long parameter vector for the image
					null); // if no derivatives, null is OK
//					false); // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
			cameraXYZ[imgNum]=lensDistortionParameters.getLensCenterCoordinates();
			if (this.debugLevel>2) {
				System.out.println("calculateGridXYZCorr(): imgNum="+imgNum+" lens coordinates (mm)={"+
						IJ.d2s(cameraXYZ[imgNum][0],3)+", "+IJ.d2s(cameraXYZ[imgNum][1],3)+", "+IJ.d2s(cameraXYZ[imgNum][2],3)+"}");
			}
			double [] diff=calcYminusFx(this.currentfX);
			// find data range for the selected image
			int index=this.imageStartIndex[imgNum]; // set when fitting series is init
/*
			int index=0;
			int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
			
			for (int iNum=0;(iNum<imgNum) && (iNum<numImg) ;iNum++) if (selectedImages[iNum]) //
				index+=fittingStrategy.distortionCalibrationData.gIP[iNum].pixelsUV.length;
			//System.out.println ("+++++++++++++imgNum="+imgNum+" index="+index);
*/
			if (this.debugLevel>2) {
				System.out.println("calculateGridXYZCorr(): fX.length="+this.currentfX.length+" this image index="+index);
			}
			double [][] imgData=new double[showIndividual?7:5][getGridHeight() * width]; // dPX, dPY, Px, Py, alpha
			for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
			// first pass - prepare [v][u]arrays
			for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
				int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0;
				int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0;
				int vu=u+width*v;
				imgData[0][vu]=   diff[2*(index+i)]; // out of bound 1410
				imgData[1][vu]=   diff[2*(index+i)+1];
				imgData[2][vu]= this.Y[2*(index+i)];  // measured pixel x
				imgData[3][vu]= this.Y[2*(index+i)+1];// measured pixel y

				//				imgData[4][vu]= fittingStrategy.distortionCalibrationData.getMask(chnNum, imgData[2][vu], imgData[3][vu]);

				if (this.weightFunction!=null) {
					imgData[4][vu]= this.weightFunction[2*(index+i)];
				} else {
					imgData[4][vu]= 1.0;
				}
				//				if (imgNum==1) System.out.println ("---index="+index+" i="+i+" vu="+vu+ " v="+v+" u="+u+" x="+IJ.d2s(this.Y[2*(index+i)],1)+" y="+IJ.d2s(this.Y[2*(index+i)+1],1));
			}
			// second pass - calculate derivatives, and residuals in mm
			for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
				int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0;
				int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0;
				int vu=u+width*v;
				gridCorr2d[imgNum][0][vu]=0.0;
				gridCorr2d[imgNum][1][vu]=0.0;
				gridCorr2d[imgNum][2][vu]=0.0; // weight

				double [][] gXY =new double[dirs.length][3];
				double [][] gpXY=new double[dirs.length][2];
				boolean [] dirMask=new boolean [dirs.length];
				for (int dir=0;dir<dirs.length;dir++){
					int u1=u+dirs[dir][0];
					int v1=v+dirs[dir][1];
					int vu1=u1+width*v1;
					dirMask[dir] = (u1>=0) && (v1>=0) && (u1<width) && (v1<height) && (imgData[4][vu1]>0);
					gXY[dir][0]= dirMask[dir]?patternParameters.gridGeometry[v1][u1][0]:0.0;
					gXY[dir][1]= dirMask[dir]?patternParameters.gridGeometry[v1][u1][1]:0.0;
					gXY[dir][2]= dirMask[dir]?patternParameters.gridGeometry[v1][u1][2]:0.0; // Add per-station (optionally)
					gpXY[dir][0]=dirMask[dir]?imgData[2][vu1]:0.0;
					gpXY[dir][1]=dirMask[dir]?imgData[3][vu1]:0.0;
				}
				int [] variants={-1,-1}; // {horizontal, vertical}
				boolean variantsExist=true;
				for (int duv=0;duv<2;duv++){ // 0 - horizontal, 1 - vertical
					for (int variant=0;variant<derivatives[duv].length;variant++) { // variants: 0 half of right/left, 1 left deriv, 2 - right deriv
						boolean fit=true;
						for (int dir=0;dir<dirs.length;dir++) if ((derivatives[duv][variant][dir]!=0) && !dirMask[dir]){
							fit=false;
							break;
						}
						if (fit) {
							variants[duv]=variant;
							break;
						}
					}
					if (variants[duv]<0) { // could not find any variant to calculate derivatives for this direction
						variantsExist=false;
						break; 
					}
				}
				if (!variantsExist){
					imgData[4][vu]=0.0;
					continue;
				}
				double [][] dXY_dUV= new double [2][2];
				double [][] dpXY_dUV=new double [2][2];
				for (int nom=0;nom<2;nom++) { // 0-x, 1 - y
					for (int denom=0;denom<2;denom++) { //0 - du, 1 - dv
						dXY_dUV [nom][denom]=0.0;
						dpXY_dUV[nom][denom]=0.0;
						for (int dir=0;dir<dirs.length;dir++){
							dXY_dUV [nom][denom]+=gXY [dir][nom]*derivatives[denom][variants[denom]][dir];
							dpXY_dUV[nom][denom]+=gpXY[dir][nom]*derivatives[denom][variants[denom]][dir];
						}
					}
					
				}
				double [] dpXY={imgData[0][vu],imgData[1][vu]};
				Matrix MdpXY=    new Matrix(dpXY,2); // 2 rows
				Matrix MdXY_dUV= new Matrix(dXY_dUV);
				Matrix MdpXY_dUV=new Matrix(dpXY_dUV);
				if ((new LUDecomposition(MdpXY_dUV)).isNonsingular()){
					/*
					 * MdpXY= MdpXY_dUV* MdUV
					 * MdXY=  MdXY_dUV * MdUV
					 * MdUV=  MdpXY_dUV.solve(MdpXY); 			
					 * MdXY=  MdXY_dUV * MdpXY_dUV.solve(MdpXY);
					 */
					Matrix MdXY=MdXY_dUV.times(MdpXY_dUV.solve(MdpXY));
					double [] dXY=MdXY.getRowPackedCopy();
					gridCorr2d[imgNum][0][vu]=dXY[0];
					gridCorr2d[imgNum][1][vu]=dXY[1];
					gridCorr2d[imgNum][2][vu]=imgData[4][vu]; // weight
				}
			} // end scanning pixels
			if (showIndividual) {
		        String [] titles={"diff-X","diff-Y","pX","pY","alpha","X-correction(mm)","Y-correction(mm)","Z-correction(mm)"};
				this.SDFA_INSTANCE.showArrays(imgData, width, height,  true, "Grid"+imgNum, titles);
			}
			IJ.showProgress(++numProcessed,numSelected);
		}
		IJ.showProgress(1.0);
		IJ.showStatus("Calculating pattern 3d correction...");
// now using gridCorr2d[imgNum], cameraXYZ[imgNum] and patternParameters.gridGeometry[v][u] find the 3d correction     public double [][][] gridGeometry=null; // [v][u]{x,y,z,"alpha"} alpha=0 - no ghrid, 1 - grid
		double [][] gridCorr3d=new double [4][width*height];
		for (int n=0;n<gridCorr3d.length;n++) for (int i=0;i<gridCorr3d[0].length;i++) gridCorr3d[n][i]=0.0;
		double Cx,Cy,Cz,Cxy,Cxz,Cyz;
		double [] V= new double[3];
		double [] V2= new double[3];
		int debugIndex=(height/2)*width+ (width/2);
		int debugIndex1=(height/2)*width+ (width/4);
		for (int v=0;v<height;v++) for (int u=0;u<width; u++){
			int index=u+v*width;
			boolean thisDebug=(this.debugLevel>1) && ((index==debugIndex) || (index==debugIndex1));
			if (thisDebug) System.out.println("calculateGridXYZCorr3D() debug("+this.debugLevel+"): index="+index+" v="+v+" u="+u);
			double [][] aM={{0.0,0.0,0.0},{0.0,0.0,0.0},{0.0,0.0,0.0}};
			double [][] aB ={{0.0},{0.0},{0.0}};
			double alpha=0.0;
			boolean fallBack2D=true;
			if (grid3DCorrection) {
				for (int imgNum=0;imgNum<selectedImages.length;imgNum++)
					if ((gridCorr2d[imgNum]!=null)  && (gridCorr2d[imgNum][2][index]>0.0)) {
						// calculate unity vector from the camera lens to the grid point
						double absV=0.0;
						for (int i=0;i<V.length;i++){
							V[i]=patternParameters.gridGeometry[v][u][i]-cameraXYZ[imgNum][i];
							absV+=V[i]*V[i];
						}
						absV=Math.sqrt(absV);
						if (absV>0) for (int i=0;i<V.length;i++) V[i]/=absV;
						for (int i=0;i<V.length;i++) V2[i]=V[i]*V[i];
						if (thisDebug) System.out.println(" imgNum="+imgNum+" V[0]="+IJ.d2s(V[0],4)+" V[1]="+IJ.d2s(V[1],4)+" V[2]="+IJ.d2s(V[2],4)+
								" V2[0]="+IJ.d2s(V2[0],4)+" V2[1]="+IJ.d2s(V2[1],4)+" V2[2]="+IJ.d2s(V2[2],4));
						// When performin 3-d correction (X,Y,Z) the result point has to have minimal weighted sum of squared distances to all rays 
// when summing for different stations, multiply W by sign(image belongs to station) 	
/*
Px, Py - calculated correction for individual image
V={Vx,Vy,Vz} unity vector from the camera lens center to the {Px,Py,0}
A - vector from the {Px,Py,0} to {X,Y,Z} = {X-Px,Y-Py,Z}
Projection of A on V will have length of A(.)V, Vector B=V*(A(.)V)
Vector D=A-B = A - V*(A(.)V)
D2=D(.)D= A(.)A - 2* (A(.)V ) * (A(.)V ) + (A(.)V ) * (A(.)V ) = A(.)A -  (A(.)V ) * (A(.)V )
D2=A(.)A -  (A(.)V )^2

A(.)A=(X-Px)^2 + (Y-Py)^2 + Z^2 =X^2 -2*X*Px +Px^2 +Y^2 -2*Y*Py +Py^2 +Z^2
A(.)A=X^2 -2*X*Px +Px^2 +Y^2 -2*Y*Py +Py^2 +Z^2
A(.)V=      (X-Px)*Vx + (Y-Py)*Vy + Z*Vz
(A(.)V)^2= ((X-Px)*Vx + (Y-Py)*Vy + Z*Vz)^2 = ((X-Px)*Vx)^2 + ((Y-Py)*Vy)^2 + (Z*Vz)^2 + 2*((X-Px)*Vx)*((Y-Py)*Vy)+ 2*((X-Px)*Vx)*(Z*Vz)+2*((Y-Py)*Vy)*(Z*Vz)
(A(.)V)^2= X^2*Vx^2 +Px^2*Vx^2 - 2*X*Px*Vx^2 +Y^2*Vy^2+Py^2*Vy^2-2*Y*Py*Vy^2 +Z^2*Vz^2 +2*X*Y*Vx*Vy +2*Px*Py*Vx*Vy - 2*X*Py*Vx*Vy - 2*Y*Px*Vx*Vy +2*X*Z*Vx*Vz - 2*Z*Px*Vx*Vz +2*Y*Z*Vy*Vz -2*z*Py*Vy*Vz

D2=
  +X^2 - X^2*Vx^2
  +Y^2 - Y^2*Vy^2 
  +Z^2 - Z^2*Vz^2
-2*X*Y* Vx*Vy
-2*X*Z* Vx*Vz
-2*Y*Z* Vy*Vz
-2*X*Px +2*X*Px*Vx^2+ 2*X*Py*Vx*Vy
-2*Y*Py +2*Y*Py*Vy^2+ 2*Y*Px*Vx*Vy
+2*Z*Px*Vx*Vz   +2*Z*Py*Vy*Vz
+Px^2  +Py^2 -Px^2*Vx^2   -Py^2*Vy^2    -2*Px*Py*Vx*Vy

0= dD2/dX/2= X*(1-Vx^2) - Y* Vx*Vy - Z* Vx*Vz -Px + Px*Vx^2  + Py*Vx*Vy
0= dD2/dY/2= Y*(1-Vy^2) - X* Vx*Vy - Z* Vy*Vz -Py + Py*Vy^2  + Px*Vx*Vy
0= dD2/dZ/2= Z*(1-Vz^2) - X* Vx*Vz - Y* Vy*Vz     + Px*Vx*Vz + Py*Vy*Vz


 X*(Vx^2-1) + Y* (Vx*Vy)  + Z* (Vx*Vz)   =  Px * (Vx^2-1)  + Py* (Vx*Vy)
 X*(Vx*Vy)  + Y* (Vy^2-1) + Z* (Vy*Vz)   =  Px * (Vx*Vy)   + Py * (Vy^2-1)
 X*(Vx*Vz)  + Y* (Vy*Vz)  + Z* (Vz^2-1)  =  Px * (Vx*Vz)   + Py* (Vy*Vz)

						
 */
				
//   | sum(Wi*Cxi),  sum(Wi*Cxyi), sum(Wi*Cxzi) |
//M= | sum(Wi*Cxyi), sum(Wi*Cyi ), sum(Wi*Cyzi) |
//   | sum(Wi*Cxzi), sum(Wi*Cyzi), sum(Wi*Czi ) |
				
//   | sum(Wi*(P0xi*Cxi + P0yi*Cxyi + P0zi*Cxzi)) |
//B= | sum(Wi*(P0yi*Cyi + P0xi*Cyxi + P0zi*Cyzi)) |
//   | sum(Wi*(P0zi*Czi + P0yi*Czyi + P0xi*Czxi)) |
/*				
	X*(Vxi^2-1) + Y*(Vxi*Vyi) + Z*(Vxi*Vzi) = P0xi*(Vxi^2-1) +P0yi*(Vxi*Vyi) + P0zi*(Vxi*Vzi)
	X*(Vxi*Vyi) + Y*(Vyi^2-1) + Z*(Vyi*Vzi) = P0xi*(Vxi*Vyi) +P0yi*(Vyi^2-1) + P0zi*(Vyi*Vzi)
	X*(Vxi*Vzi) + Y*(Vxi*Vyi) + Z*(Vzi^2-1) = P0xi*(Vxi*Vzi) +P0yi*(Vxi*Vyi) + P0zi*(Vzi^2-1)

	X*Cx  + Y*Cxy + Z*Cxz = P0xi*Cx  +P0yi*Cxy + P0zi*Cxz
	X*Cxy + Y*Cy  + Z*Cyz = P0xi*Cxy +P0yi*Cy  + P0zi*Cyz
	X*Cxz + Y*Cyz + Z*Cz  = P0xi*Cxz +P0yi*Cyz + P0zi*Cz
	P0zi==0.0, so - now we'll use P0zi - difference from this station to average

	X*Cx  + Y*Cxy + Z*Cxz = P0xi*Cx  +P0yi*Cxy
	X*Cxy + Y*Cy  + Z*Cyz = P0xi*Cxy +P0yi*Cy
	X*Cxz + Y*Cyz + Z*Cz  = P0xi*Cxz +P0yi*Cyz

*/
						Cx=V2[0]-1.0;
						Cy=V2[1]-1.0;
						Cz=V2[2]-1.0;
						Cxy= V[0]*V[1];
						Cxz= V[0]*V[2];
						Cyz= V[1]*V[2];
						if (thisDebug) System.out.println(" Cx="+IJ.d2s(Cx,6)+" Cy="+IJ.d2s(Cy,6)+" Cz="+IJ.d2s(Cz,6)+
								" Cxy="+IJ.d2s(Cxy,6)+" Cxz="+IJ.d2s(Cxz,6)+" Cyz="+IJ.d2s(Cyz,6));


						double W=gridCorr2d[imgNum][2][index];
						double Px=gridCorr2d[imgNum][0][index];
						double Py=gridCorr2d[imgNum][1][index];
						alpha+=W;
						if (thisDebug) System.out.println(imgNum+": Px="+IJ.d2s(Px,6)+" Py="+IJ.d2s(Py,6)+" W="+IJ.d2s(W,6));
						aM[0][0]+=W*Cx;
						aM[0][1]+=W*Cxy;
						aM[0][2]+=W*Cxz;
						aM[1][1]+=W*Cy;
						aM[1][2]+=W*Cyz;
						aM[2][2]+=W*Cz;
						aB[0][0]+=W*(Px*Cx  + Py*Cxy);// Pz==0.0
						aB[1][0]+=W*(Px*Cxy + Py*Cy);// Pz==0.0
						aB[2][0]+=W*(Px*Cxz + Py*Cyz);// Pz==0.0
					}			
				aM[1][0]+=aM[0][1];
				aM[2][0]+=aM[0][2];
				aM[2][1]+=aM[1][2];
				Matrix M=new Matrix(aM);
				Matrix B=new Matrix(aB);
				if (thisDebug) {
					System.out.println(" M:");
					M.print(8, 6);
					System.out.println(" B:");
					B.print(8, 6);
				}

				//			boolean fallBack2D=true;
				if ((new LUDecomposition(M)).isNonsingular()){
					double [] dXYZ=M.solve(B).getRowPackedCopy();
					for (int i=0;i<3;i++) gridCorr3d[i][index]=dXYZ[i];
					gridCorr3d[3][index]=alpha;
					fallBack2D=false; //TODO:  make sure delta Z (Math.abs(gridCorr3d[2][index])) is not too big!!
					if (Math.abs(gridCorr3d[2][index])>maxZCorr) {

						fallBack2D=true; // temporary limit
					}
					if (thisDebug) System.out.println(" dX="+IJ.d2s(gridCorr3d[0][index],6)+" dY="+IJ.d2s(gridCorr3d[1][index],6)+" dZ="+IJ.d2s(gridCorr3d[2][index],6));
				}
			}
			if(fallBack2D) { // make a 2d averaging of weighted dx, dy correction
				for (int i=0;i<gridCorr3d.length;i++) gridCorr3d[i][index]=0.0;
				double s=0;
				for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if ((gridCorr2d[imgNum]!=null) &&(gridCorr2d[imgNum][2][index]>0.0)) {
					double W=gridCorr2d[imgNum][2][index];
					//			V[i]=patternParameters.gridGeometry[v][u][i]-cameraXYZ[imgNum][i];

					double [] cv={
							patternParameters.gridGeometry[v][u][0]-cameraXYZ[imgNum][0],
							patternParameters.gridGeometry[v][u][1]-cameraXYZ[imgNum][1],
							patternParameters.gridGeometry[v][u][2]-cameraXYZ[imgNum][2]};
					double cv2=cv[0]*cv[0]+cv[1]*cv[1]+cv[2]*cv[2];
					double kv=(gridCorr2d[imgNum][0][index]*cv[0]+gridCorr2d[imgNum][1][index]*cv[1])/cv2;
					gridCorr3d[0][index]+=W*(gridCorr2d[imgNum][0][index]-cv[0]*kv);
					gridCorr3d[1][index]+=W*(gridCorr2d[imgNum][1][index]-cv[1]*kv);
					gridCorr3d[2][index]+=W*(                            -cv[2]*kv);
					s+=W;
				}
				if (s>0){
					gridCorr3d[0][index]/=s;
					gridCorr3d[1][index]/=s;
					gridCorr3d[2][index]/=s;
				} else {
					gridCorr3d[0][index]=0.0;
					gridCorr3d[1][index]=0.0;
					gridCorr3d[2][index]=0.0;
				}
				gridCorr3d[3][index]=s;
				if (thisDebug) System.out.println(" Using 2d averaging: dX="+IJ.d2s(gridCorr3d[0][index],6)+
						" dY="+IJ.d2s(gridCorr3d[1][index],6)+" dZ="+IJ.d2s(gridCorr3d[2][index],6));
			}
		}
		// Make average correction zero is it needed?
		// create "reliable" mask for averaging/tilting - disregard the outmost grid pixels
		boolean [] reliable=new boolean [width*height];
		double wThreshold=0.0;
		for (int v=0;v<height;v++) for (int u=0;u<width;u++){
			int index=u+v*width;
			reliable[index]=false;
			if ((v>0) && (u>0) && (v<(height-1)) && (u<(width-1)) &&
					(gridCorr3d[3][index]>wThreshold) &&
					(gridCorr3d[3][index-1]>wThreshold) &&
					(gridCorr3d[3][index+1]>wThreshold) &&
					(gridCorr3d[3][index-width]>wThreshold) &&
					(gridCorr3d[3][index+width]>wThreshold) ){
				reliable[index]=true;
			}
			
		}
		double corrAverage;
		for (int c=0;c<3;c++){ 
			corrAverage=0.0;
			double s=0.0;
			for (int i=0;i<gridCorr3d[0].length;i++) if (reliable[i]) {
				corrAverage+=gridCorr3d[c][i]*gridCorr3d[3][i];
				s+=gridCorr3d[3][i];
			}
			corrAverage/=s;
//			System.out.println("zCorrAverage["+c+"="+corrAverage);
			for (int i=0;i<gridCorr3d[c].length;i++) gridCorr3d[c][i]-=corrAverage;
		}
// for Z correction compensate for x/y tilts
		String [] titles={"X-correction(mm)","Y-correction(mm)","Z-correction","Weight"};
		if (rotateCorrection) {
			double SX=0.0,SX2=0.0,SZ=0.0,SXY=0.0,SXZ=0.0,S0=0.0,SY=0.0,SY2=0.0,SYZ=0.0;
			double [][] gridGeom=new double [3][gridCorr3d[0].length];
			for (int c=0;c<gridGeom.length;c++) for (int i=0;i<gridGeom[c].length;i++)gridGeom[c][i]=0.0;

			for (int v=0;v<height;v++) for (int u=0;u<width; u++){
				int index=u+v*width;
				double W=gridCorr3d[3][index];
				gridGeom[0][index]=patternParameters.gridGeometry[v][u][0];
				gridGeom[1][index]=patternParameters.gridGeometry[v][u][1];
				gridGeom[2][index]=W;
				if ((reliable[index]) && (W>0.0)){
					S0+=W;
					SX+=  W*patternParameters.gridGeometry[v][u][0];
					SX2+= W*patternParameters.gridGeometry[v][u][0]*patternParameters.gridGeometry[v][u][0];
					SY+=  W*patternParameters.gridGeometry[v][u][1];
					SY2+= W*patternParameters.gridGeometry[v][u][1]*patternParameters.gridGeometry[v][u][1];
					SXY+= W*patternParameters.gridGeometry[v][u][0]*patternParameters.gridGeometry[v][u][1];
					SZ+=  W*gridCorr3d[2][index];
					SXZ+= W*gridCorr3d[2][index]*patternParameters.gridGeometry[v][u][0];
					SYZ+= W*gridCorr3d[2][index]*patternParameters.gridGeometry[v][u][1];
				}
			}
			double [][] aM= {
					{SX2, SXY, SX},
					{SXY, SY2, SY},
					{SX,  SY,  S0}};
			double [][] aB= {{SXZ},{SYZ},{SZ}};
			Matrix M=new Matrix(aM);
			Matrix B=new Matrix(aB);
			if (this.debugLevel>2) {
				System.out.println(" M:");
				M.print(8, 6);
				System.out.println(" B:");
				B.print(8, 6);
				System.out.println(" Ax,Ay,B:");
				M.solve(B).print(8, 6);
			}
			double [] tilts=M.solve(B).getRowPackedCopy();
			if (this.debugLevel>2) {
				if (this.refineParameters.showThisCorrection) {
					this.SDFA_INSTANCE.showArrays(gridCorr3d, getGridWidth(), getGridHeight(),  true, "before tilt:", titles);
				}
			}
			for (int v=0;v<height;v++) for (int u=0;u<width; u++){
				int index=u+v*width;
				gridCorr3d[2][index]-=tilts[0]*patternParameters.gridGeometry[v][u][0]+tilts[1]*patternParameters.gridGeometry[v][u][1]+tilts[2];
			}
		}
    	if (this.debugLevel>2) {
    		if (this.refineParameters.showThisCorrection) {
    			double [][] gridCorr3dClone=new double [4][width*height];
    			for (int c=0;c<gridCorr3dClone.length;c++) for (int i=0;i<gridCorr3dClone[c].length;i++)
    				gridCorr3dClone[c][i]=reliable[i]? gridCorr3d[c][i]:0.0;
    			this.SDFA_INSTANCE.showArrays(gridCorr3dClone, getGridWidth(), getGridHeight(),  true, "after tilt:", titles);
    		}
    	}
    	IJ.showStatus("");
		return  gridCorr3d;
	}
/**
 * 
 * @param gridCorr3D Array of grid corrections (1-st index: dx, dy, dz, mask (>0 - valid point)
 * @param gridZCorr Optional per-station z-correction (or null)
 * @param width // width of the grid array
 * @param preShrink // shrink input array by this number of pixels (hor/vert) befere extrapolating (remove bad border nodes)
 * @param expand    // expand/extrapolate this number of steps after shrinking (or until no pixels left
 * @param sigma     // effective radius for fitting the extrapolation plane, in nodes 
 * @param ksigma    // size if square to consider (measured in ksigma-s). 2.0 means square is 4*sigma by 4*sigma
 * @return true if OK, false if error
 */
	public boolean shrinkExtrapolateGridCorrection(
			double [][] gridCorr3D, // dx,dy,dz, mask >0
			double [][] gridZCorr, // per-station additional Z-correction (or null)
			int width,
			int preShrink,
			int expand,
			double sigma,
			double ksigma){
		int length=gridCorr3D[0].length;
        int height=	length/width;
//		int decimate=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
//		int sWidth= (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
//		int sHeight=(fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
//		double sigma=nsigma/decimate;
		boolean [] fMask=new boolean[length];
		for (int i=0;i<fMask.length;i++)
			fMask[i]=gridCorr3D[3][i]>0;
		int len= (int) Math.ceil(sigma*ksigma);
		double [] gaussian=new double[len+1];
		double k=0.5/sigma/sigma;
		for (int i=0;i<=len;i++) gaussian[i]=Math.exp(-i*i*k);
		int [][] dirs={{-1,0},{1,0},{0,-1},{0,1}};
		List <Integer> extList=new ArrayList<Integer>(1000);
		Integer Index,Index2;
		extList.clear();
		// create initial wave
		int debugThreshold=2;
		if (this.debugLevel>debugThreshold) System.out.println("shrinkExtrapolateGridCorrection width="+width+" height="+height);
		
		for (int iy=0;iy<height;iy++) for (int ix=0;ix<width;ix++) {
			Index=iy*width+ix;
			if (fMask[Index]) {
				int numNew=0;
				for (int dir=0;dir<dirs.length;dir++){
					int ix1=ix+dirs[dir][0];
					int iy1=iy+dirs[dir][1];
					// Will not shrink from the array border!
					if ((ix1>=0) && (iy1>=0) && (ix1<width) && (iy1<height)) {
						if (!fMask[iy1*width+ix1]) numNew++;
					}
					if (numNew>0) extList.add(Index); // neighbor will have non-singular matrix
				}
			}
		}
		if (this.debugLevel>debugThreshold) System.out.println("Initial wave length="+extList.size());
		// now shrink 
		// unmask current wave
		for (int i=extList.size()-1; i>=0;i--) fMask[extList.get(i)]=false;
		if (extList.size()==0) return false; // no points
		for (int nShrink=0;nShrink<preShrink;nShrink++){
			int size=extList.size();
			if (this.debugLevel>debugThreshold) System.out.println("shrinking, size="+size);
			if (size==0) return false; // no points
			// wave step, unmasking
			for (int i=0; i<size;i++) {
				Index=extList.get(0);
				extList.remove(0);
				int iy=Index/width;
				int ix=Index%width;
				for (int dir=0;dir<dirs.length;dir++){
					int ix1=ix+dirs[dir][0];
					int iy1=iy+dirs[dir][1];
					if ((ix1>=0) && (iy1>=0) && (ix1<width) && (iy1<height)){ 
						Index=iy1*width+ix1;
						if (fMask[Index]){
							extList.add(Index);
							fMask[Index]=false; // restore later?
						}
					}
				}
			}
		}
		// restore mask on the front
		for (int i=extList.size()-1; i>=0;i--) fMask[extList.get(i)]=true;
		
		   // repeat with the wave until there is place to move, but not more than "expand" steps
		   int [] dirs2=new int [2];
		   for (int n=0; (n<expand) && (extList.size()>0); n++ ){
			   if (this.updateStatus) IJ.showStatus("Expanding, step="+(n+1)+" (of "+expand+"), extList.size()="+extList.size());
//			   if (this.updateStatus) showStatus("Expanding, step="+(n+1)+" (of "+expand+"), extList.size()="+extList.size(),0);
			   if (this.debugLevel>debugThreshold) System.out.println("Expanding, step="+n+", extList.size()="+extList.size());
			   // move wave front 1 pixel hor/vert        	
			   for (int i=extList.size();i>0;i--){ // repeat current size times
				   Index=extList.get(0);
				   extList.remove(0);
				   int iy=Index/width;
				   int ix=Index%width;
				   for (int dir=0;dir<dirs.length;dir++){
					   int ix1=ix+dirs[dir][0];
					   int iy1=iy+dirs[dir][1];
					   if ((ix1>=0) && (iy1>=0) && (ix1<width) && (iy1<height)){ 
						   Index=iy1*width+ix1;
						   if (!fMask[Index]){
							   // verify it has neighbors in the perpendicular direction to dir
							   dirs2[0]=(dir+2) & 3;
							   dirs2[1]=dirs2[0] ^ 1;
							   for (int dir2=0;dir2<dirs2.length;dir2++){
								   int ix2=ix+dirs[dirs2[dir2]][0]; // from the old, not the new point!
								   int iy2=iy+dirs[dirs2[dir2]][1];
								   if ((ix2>=0) && (iy2>=0) && (ix2<width) && (iy2<height)){ 
									   Index2=iy2*width+ix2;
									   if (fMask[Index2]){ // has orthogonal neighbor, OK to add
										   extList.add(Index);
										   fMask[Index]=true; // remove later
										   break;
									   }
								   }
							   }
						   }
					   }
				   }
			   }
			   // now un-mask the pixels in new list new
			   for (int i =0;i<extList.size();i++){
				   Index=extList.get(i);
				   fMask[Index]=false; // now mask is only set for known pixels
			   }
	// Calculate values (extrapolate) for the pixels in the list
			/*
Err = sum (W(x,y)*(f(x,y)-F0-Ax*(x-X0)-Ay*(y-Y0))^2)=
sum (Wxy*(Fxy^2+F0^2+Ax^2*(x-X0)^2+Ay^2*(y-Y0)^2
-2*Fxy*F0 -2*Fxy*Ax*(x-X0) - 2*Fxy*Ay*(y-Y0)
+2*F0*Ax*(x-X0) + 2*F0*Ay*(y-Y0) 
+2*Ax*(x-X0)*Ay*(y-Y0))
(1)0=dErr/dF0= 2*sum (Wxy*(F0-Fxy+Ax*(x-X0)+Ay(y-Y0)))
(2)0=dErr/dAx= 2*sum (Wxy*(Ax*(x-X0)^2-Fxy*(x-X0) +F0*(x-X0)+Ay*(x-x0)*(y-Y0)))
(3)0=dErr/dAy= 2*sum (Wxy*(Ay*(y-y0)^2-Fxy*(y-Y0) +F0*(y-Y0)+Ax*(x-x0)*(y-Y0)))

S0 = sum(Wxy)
SF=  sum(Wxy*Fxy)
SX=  sum(Wxy*(x-X0)
SY=  sum(Wxy*(y-Y0)
SFX= sum(Wxy*Fxy*(x-X0)
SFY= sum(Wxy*Fxy*(y-Y0)
SX2= sum(Wxy*(x-X0)^2
SY2= sum(Wxy*(y-Y0)^2
SXY= sum(Wxy*(x-X0)*(y-Y0)

(1) F0*S0 - SF + Ax*SX +Ay*Sy = 0
(2) Ax*SX2-SFX+F0*SX+Ay*SXY = 0
(3) Ay*Sy2 -SFY + F0*SY +Ax*SXY = 0

(1) F0*S0  + Ax*SX +Ay*SY = SF
(2) Ax*SX2+F0*SX+Ay*SXY = SFX
(3) Ay*Sy2  + F0*SY +Ax*SXY = SFY


   | F0 |
V= | Ax |
   | Ay |

     | SF  |
B =  | SFX |
     | SFY |

     | S0  SX   SY  |
M =  | SX  SX2  SXY | 
     | SY  SXY  SY2 |

M * V = B
			 */
			   int numOriginalComponents=3;
			   boolean useExtra=gridZCorr!=null;
			for (int i =0;i<extList.size();i++){
        		Index=extList.get(i);
        		int iy=Index/width;
        		int ix=Index%width;
        		double [] S0=new double [3+(useExtra?gridZCorr.length:0)];
        		for (int ii=0;ii<S0.length;ii++) S0[ii]=0.0;
//        		double [] S0= {0.0,0.0,0.0};
        		double [] SF= S0.clone();
        		double [] SX= S0.clone();
        		double [] SY= S0.clone();
        		double [] SFX=S0.clone();
        		double [] SFY=S0.clone();
        		double [] SX2=S0.clone();
        		double [] SY2=S0.clone();
        		double [] SXY=S0.clone();
        		int iYmin=iy-len; if (iYmin<0) iYmin=0;
        		int iYmax=iy+len; if (iYmax>=height) iYmax=height-1;
        		int iXmin=ix-len; if (iXmin<0) iXmin=0;
        		int iXmax=ix+len; if (iXmax>=width) iXmax=width-1;
        		for (int iy1=iYmin;iy1<=iYmax;iy1++) for (int ix1=iXmin;ix1<=iXmax;ix1++) {
        			int ind=ix1+iy1*width;
        			if (fMask[ind]){
        				double w=gaussian[(iy1>=iy)?(iy1-iy):(iy-iy1)]*gaussian[(ix1>=ix)?(ix1-ix):(ix-ix1)];
        				for (int m=0;m<S0.length;m++){
        					double d=(m<numOriginalComponents)?gridCorr3D[m][ind]:gridZCorr[m-numOriginalComponents][ind];
        					S0[m]+= w;
        					SF[m]+= w*d;
        					SX[m]+= w*(ix1-ix);
        					SY[m]+= w*(iy1-iy);
        					SFX[m]+=w*d*(ix1-ix);
        					SFY[m]+=w*d*(iy1-iy);
        					SX2[m]+=w*(ix1-ix)*(ix1-ix);
        					SY2[m]+=w*(iy1-iy)*(iy1-iy);
        					SXY[m]+=w*(ix1-ix)*(iy1-iy);
        				}
        			}
        			
        		}
        		for (int m=0;m<S0.length;m++){
        			double [][] aB={{SF[m]},{SFX[m]},{SFY[m]}};
        			double [][] aM={
        					{S0[m],SX[m], SY[m]},
        					{SX[m],SX2[m],SXY[m]},
        					{SY[m],SXY[m],SY2[m]}
        			};
        			Matrix B=new Matrix(aB);
        			Matrix M=new Matrix(aM);
        			Matrix V=M.solve(B);
        			if (m<numOriginalComponents) gridCorr3D[m][Index]=V.get(0,0);
        			else gridZCorr[m-numOriginalComponents][Index]=V.get(0,0);
        		}
    			if (this.debugLevel>debugThreshold) System.out.println("updated v="+(Index/width)+" u="+(Index%width)+" {"+
    					IJ.d2s(gridCorr3D[0][Index],2)+","+IJ.d2s(gridCorr3D[1][Index],2)+","+IJ.d2s(gridCorr3D[2][Index],2)+"}");
			}
			
// set mask again for the new calculated layer of pixels			
			for (int i =0;i<extList.size();i++){
        		Index=extList.get(i);
				fMask[Index]=true;
			}
        }
	   return true;	
	}
	
	public void logScale(
			double [] data,
			double fatZero){
		for (int i=0;i<data.length;i++){
			double d=((data[i]>=0)?data[i]:0.0);
			data[i]=(fatZero>0)?(Math.log(fatZero+d)):d;
		}
	}
	public void unLogScale(
			double [] data,
			double fatZero){
		for (int i=0;i<data.length;i++){
			if (fatZero>0.0) data[i]=Math.exp(data[i])-fatZero;
			if (data[i]<0.0) data[i]=0.0;
		}
	}
	
	/**
	 * Extrapolates sensor correction beyond known data (in-place) 
	 * @param fieldXY [2][nPixels] vector field to extrapolate
	 * @param sMask [nPixels] alpha (0.0 .. 1.0) "reliability" mask to apply to vector field 
	 * @param alphaThreshold start with pixels with alpha above this value (disregard border unreliable pixels) 
	 * @param nsigma when fitting plane through new point use Gaussian weight function for the neighbors
	 *  (normalized to non-decimated points)
	 * @param ksigma Process pixels in a square with the side 2*sigma*ksigma
	 * @return false if nothing to extrapolate (too small mask)?
	 */
	public boolean extrapolateSensorCorrection(
			boolean [] whichExtrapolate,
			double [][] fieldXY,
			double []sMask,
			double alphaThreshold,
			double nsigma,
			double ksigma){
		
		int decimate=fittingStrategy.distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int sWidth= (fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
		int sHeight=(fittingStrategy.distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
		double sigma=nsigma/decimate;
		boolean [] fMask=new boolean[fieldXY[0].length];
		for (int i=0;i<fMask.length;i++)
			fMask[i]=sMask[i]>=alphaThreshold;
		int len= (int) Math.ceil(sigma*ksigma);
		double [] gaussian=new double[len+1];
		double k=0.5/sigma/sigma;
		for (int i=0;i<=len;i++) gaussian[i]=Math.exp(-i*i*k);
		int [][] dirs={{-1,0},{1,0},{0,-1},{0,1}};
		List <Integer> extList=new ArrayList<Integer>(1000);
		Integer Index;
		extList.clear();
		// create initial wave
		if (this.debugLevel>2) System.out.println("extrapolateSensorCorrection() decimate="+decimate+", sWidth="+sWidth+" sHeight="+sHeight);
		for (int iy=0;iy<sHeight;iy++) for (int ix=0;ix<sWidth;ix++) {
			Index=iy*sWidth+ix;
			if (fMask[Index]) {
				int numOld=0;
				int numNew=0;
				for (int dir=0;dir<dirs.length;dir++){
					int ix1=ix+dirs[dir][0];
					int iy1=iy+dirs[dir][1];
					if ((ix1>=0) && (iy1>=0) && (ix1<sWidth) && (iy1<sHeight)) {
						if (fMask[iy1*sWidth+ix1]) numOld++;
						else numNew++;
					}
					if ((numNew>0) && (numOld>1)) extList.add(Index); // neighbor will have non-singular matrix
				}
			}
		}
		if (extList.size()==0) return false;
        while (extList.size()>0){
    		if (this.debugLevel>2) System.out.println("extList.size()="+extList.size());
        	
        	// move wave front 1 pixel hor/vert        	
        	for (int i=extList.size();i>0;i--){ // repeat current size times
        		Index=extList.get(0);
        		extList.remove(0);
        		int iy=Index/sWidth;
        		int ix=Index%sWidth;
				for (int dir=0;dir<dirs.length;dir++){
					int ix1=ix+dirs[dir][0];
					int iy1=iy+dirs[dir][1];
					if ((ix1>=0) && (iy1>=0) && (ix1<sWidth) && (iy1<sHeight)){ 
						Index=iy1*sWidth+ix1;
						if (!fMask[Index]){
							extList.add(Index);
							fMask[Index]=true; // remove later
						}
					}
				}
        	}
			// now un-mask the pixels in new list new
			for (int i =0;i<extList.size();i++){
        		Index=extList.get(i);
				fMask[Index]=false; // now mask is only set for known pixels
			}
// Calculate values (extrapolate) for the pixels in the list
			/*
Err = sum (W(x,y)*(f(x,y)-F0-Ax*(x-X0)-Ay*(y-Y0))^2)=
sum (Wxy*(Fxy^2+F0^2+Ax^2*(x-X0)^2+Ay^2*(y-Y0)^2
-2*Fxy*F0 -2*Fxy*Ax*(x-X0) - 2*Fxy*Ay*(y-Y0)
+2*F0*Ax*(x-X0) + 2*F0*Ay*(y-Y0) 
+2*Ax*(x-X0)*Ay*(y-Y0))
(1)0=dErr/dF0= 2*sum (Wxy*(F0-Fxy+Ax*(x-X0)+Ay(y-Y0)))
(2)0=dErr/dAx= 2*sum (Wxy*(Ax*(x-X0)^2-Fxy*(x-X0) +F0*(x-X0)+Ay*(x-x0)*(y-Y0)))
(3)0=dErr/dAy= 2*sum (Wxy*(Ay*(y-y0)^2-Fxy*(y-Y0) +F0*(y-Y0)+Ax*(x-x0)*(y-Y0)))

S0 = sum(Wxy)
SF=  sum(Wxy*Fxy)
SX=  sum(Wxy*(x-X0)
SY=  sum(Wxy*(y-Y0)
SFX= sum(Wxy*Fxy*(x-X0)
SFY= sum(Wxy*Fxy*(y-Y0)
SX2= sum(Wxy*(x-X0)^2
SY2= sum(Wxy*(y-Y0)^2
SXY= sum(Wxy*(x-X0)*(y-Y0)

(1) F0*S0 - SF + Ax*SX +Ay*Sy = 0
(2) Ax*SX2-SFX+F0*SX+Ay*SXY = 0
(3) Ay*Sy2 -SFY + F0*SY +Ax*SXY = 0

(1) F0*S0  + Ax*SX +Ay*SY = SF
(2) Ax*SX2+F0*SX+Ay*SXY = SFX
(3) Ay*Sy2  + F0*SY +Ax*SXY = SFY


   | F0 |
V= | Ax |
   | Ay |

     | SF  |
B =  | SFX |
     | SFY |

     | S0  SX   SY  |
M =  | SX  SX2  SXY | 
     | SY  SXY  SY2 |

M * V = B
			 */
			double [] zeros= new double[whichExtrapolate.length];
			for (int i=0;i<zeros.length;i++)zeros[i]=0.0;
			for (int i =0;i<extList.size();i++){
        		Index=extList.get(i);
        		int iy=Index/sWidth;
        		int ix=Index%sWidth;
        		double [] S0= zeros.clone();
        		double [] SF= zeros.clone();
        		double [] SX= zeros.clone();
        		double [] SY= zeros.clone();
        		double [] SFX=zeros.clone();
        		double [] SFY=zeros.clone();
        		double [] SX2=zeros.clone();
        		double [] SY2=zeros.clone();
        		double [] SXY=zeros.clone();
        		int iYmin=iy-len; if (iYmin<0) iYmin=0;
        		int iYmax=iy+len; if (iYmax>=sHeight) iYmax=sHeight-1;
        		int iXmin=ix-len; if (iXmin<0) iXmin=0;
        		int iXmax=ix+len; if (iXmax>=sWidth) iXmax=sWidth-1;
        		for (int iy1=iYmin;iy1<=iYmax;iy1++) for (int ix1=iXmin;ix1<=iXmax;ix1++) {
        			int ind=ix1+iy1*sWidth;
        			if (fMask[ind]){
        				double w=gaussian[(iy1>=iy)?(iy1-iy):(iy-iy1)]*gaussian[(ix1>=ix)?(ix1-ix):(ix-ix1)];
        				for (int m=0;m<whichExtrapolate.length;m++) if(whichExtrapolate[m]){
        					S0[m]+= w;
        					SF[m]+= w*fieldXY[m][ind];
        					SX[m]+= w*(ix1-ix);
        					SY[m]+= w*(iy1-iy);
        					SFX[m]+=w*fieldXY[m][ind]*(ix1-ix);
        					SFY[m]+=w*fieldXY[m][ind]*(iy1-iy);
        					SX2[m]+=w*(ix1-ix)*(ix1-ix);
        					SY2[m]+=w*(iy1-iy)*(iy1-iy);
        					SXY[m]+=w*(ix1-ix)*(iy1-iy);
        				}
        			}
        			
        		}
        		for (int m=0;m<whichExtrapolate.length;m++) if(whichExtrapolate[m]){
        			double [][] aB={{SF[m]},{SFX[m]},{SFY[m]}};
        			double [][] aM={
        					{S0[m],SX[m], SY[m]},
        					{SX[m],SX2[m],SXY[m]},
        					{SY[m],SXY[m],SY2[m]}
        			};
        			Matrix B=new Matrix(aB);
        			Matrix M=new Matrix(aM);
        			Matrix V=M.solve(B);
        			fieldXY[m][Index]=V.get(0,0);
        		}

			}
			
// set mask again for the new calculated layer of pixels			
			for (int i =0;i<extList.size();i++){
        		Index=extList.get(i);
				fMask[Index]=true;
			}
        }
		return true;
		
	}
	
	
	/**
	 * Calculates residual correction from  the measured sensor pX, pY to the calculated {pixel X, pixel Y}
	 * @param distortionCalibrationData
	 * @param showIndividual - show individual images
	 * @param showIndividualNumber - which image to show (-1 - all)
	 * @return 
	 */
	public double [][][] calculateSensorXYCorr(
			DistortionCalibrationData distortionCalibrationData,
			boolean showIndividual,
			int showIndividualNumber, // which image to show (-1 - all)
			boolean useGridAlpha // use grid alpha, false - use old calculations
			){
		int decimate=distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int sWidth= (distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
		int sHeight=(distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
		int numChannels=distortionCalibrationData.getNumChannels(); // number of used channels
		int width=getGridWidth();
		int height=getGridHeight();
    	int imgRGBIndex=   3;
    	
		int [] uvInc={0,1,width,width+1}; // four corners as vu index
		int [][] cycles={ // counter-clockwise corners bounding the area  (only orthogonal sides?)
				{1,0,2},
				{2,3,1},
				{0,2,3},
				{3,1,0}};
		double [][][] gridPCorr=new double [numChannels][][];
		for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) gridPCorr[chnNum]=null; 
		boolean [] selectedImages=fittingStrategy.selectedImages();
		boolean debugExit=false;
		int debugCntr=2;
		int numSelected=0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) numSelected++;
		int numProcessed=0;
		IJ.showStatus("Calculating sensor corrections...");
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
			if (debugExit) break;
			int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
			int station=fittingStrategy.distortionCalibrationData.gIP[imgNum].getStationNumber(); // number of sub-camera
			double [][] photometrics=patternParameters.getPhotometricBySensor(station,chnNum); // head/bottom grid intensity/alpha
			
			if (showIndividual && ((showIndividualNumber<0) || (showIndividualNumber==chnNum))) {
				String [] titles={"R","G","B","A"};
				this.SDFA_INSTANCE.showArrays(photometrics, width, height,  true, "Photometrics"+chnNum+"-"+imgNum, titles);
			}

			
			// initialize this array if it is needed, leave unused null
			if (gridPCorr[chnNum]==null){
				 gridPCorr[chnNum]=new double [7][sWidth*sHeight];
				for (int n=0;n<gridPCorr[chnNum].length;n++) for (int i=0;i<gridPCorr[chnNum][0].length;i++) gridPCorr[chnNum][n][i]=0.0;
			}
			double [][] thisPCorr=null;
			
			thisPCorr=new double [7][sWidth*sHeight]; // calculate for a single (this) image, accumulate in the end
			for (int n=0;n<thisPCorr.length;n++) for (int i=0;i<thisPCorr[0].length;i++) thisPCorr[n][i]=0.0;
			double [] diff=calcYminusFx(this.currentfX);
			// find data range for the selected image
			int index=0;
			int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
			for (int iNum=0;(iNum<imgNum) && (iNum<numImg) ;iNum++) if (selectedImages[iNum])
				index+=fittingStrategy.distortionCalibrationData.gIP[iNum].pixelsUV.length;
			if (this.debugLevel>2) {
				System.out.println("calculateGridXYCorr(): fX.length="+this.currentfX.length+" this image index="+index);
			}
			double [][] imgData=new double[8][height * width]; // dPX, dPY, Px, Py, alpha,R,G,B
			for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
			
			for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
				int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0; // starting from 0
				int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0; // starting from 0
				int vu=u+width*v;
				imgData[0][vu]=   diff[2*(index+i)];
				imgData[1][vu]=   diff[2*(index+i)+1];
				imgData[2][vu]= this.Y[2*(index+i)];  // measured pixel x
				imgData[3][vu]= this.Y[2*(index+i)+1];// measured pixel y
				imgData[4][vu]= this.weightFunction[2*(index+i)];
				
				for (int c=0;c<3;c++){
//					double g=gridGeometry[v][u][gridRGBIndex+c];
					double g=photometrics[c][vu];
					imgData[5+c][vu]=(g>0)?(fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsXY[i][imgRGBIndex+c]/g):g;
				}
			}
			if (showIndividual && ((showIndividualNumber<0) || (showIndividualNumber==chnNum))) {
				String [] titles={"dPx","dPy","Px","Py","A","R","G","B"};// dPX, dPY, Px, Py, alpha,R,G,B - rgb - full, not incremental
				this.SDFA_INSTANCE.showArrays(imgData, width, height,  true, "imgData"+imgNum, titles);
				
			}

			// now use imgData array to fill thisPCorr by linear interpolation
			for (int v=0;v<(height-1); v++) for (int u=0; u<(width-1);u++){
				if (debugExit) break;
				int vu=u+width*v;
                double [][] cornerXY =new double[4][];
                for (int i=0;i<uvInc.length;i++){
                	int vu1=vu+uvInc[i];	
                	if (imgData[4][vu1]>0.0){
                		cornerXY[i]=new double[2];
                		cornerXY[i][0]=imgData[2][vu1];
                		cornerXY[i][1]=imgData[3][vu1];
                	} else cornerXY[i]=null;
                }
                boolean [] cycleFits=new boolean[cycles.length];
                boolean anyFits=false;
                for (int i=0;i<cycles.length;i++){
                	cycleFits[i]=true;
                	for (int j=0;j<cycles[i].length;j++) if (cornerXY[cycles[i][j]]==null) {
                		cycleFits[i]=false;
                		break;
                	}
                	anyFits |=cycleFits[i];
                }
                if (!anyFits) continue; // not a single cycle
				if ((this.debugLevel>3) && !debugExit) {
					String debugString="cycleFits ";
					for (int i =0;i<cycleFits.length; i++) debugString+=" "+cycleFits[i];
					System.out.println(debugString);
				}
                if (cycleFits[0]&&cycleFits[1]){ // remove overlaps
                	cycleFits[2]=false;
                	cycleFits[3]=false;
                }
                boolean minMaxUndefined=true;
				double minX=0,maxX=0,minY=0,maxY=0;
				// find bounding rectangle;
				for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
					int [] cycle=cycles[nCycle];
					for (int corner=0; corner<cycle.length;corner++){
						if (minMaxUndefined || (minX>cornerXY[cycle[corner]][0])) minX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (maxX<cornerXY[cycle[corner]][0])) maxX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (minY>cornerXY[cycle[corner]][1])) minY=cornerXY[cycle[corner]][1];
						if (minMaxUndefined || (maxY<cornerXY[cycle[corner]][1])) maxY=cornerXY[cycle[corner]][1];
						minMaxUndefined=false;
					}
				}
				int iMinX=(int) Math.floor(minX/decimate);
				int iMinY=(int) Math.floor(minY/decimate);
				int iMaxX=(int) Math.ceil(maxX/decimate);
				int iMaxY=(int) Math.ceil(maxY/decimate);
				// not sure if these checks are needed, got out of bounds wheriDy was =484=sHeight
				if (iMinX<0) iMinX=0;
				if (iMaxX>=sWidth) iMaxX=sWidth-1;
				if (iMinY<0) iMinY=0;
				if (iMaxY>=sHeight) iMaxY=sHeight-1;
				double [] originXY=new double [2];
				double [] endXY=new double [2];
				boolean debugHadPixels=false;
//TODO: scan X,Y in this rectangle, for points in defined squares/triangles find if the point is inside (accurate not to loose any).
				for (int idY=iMinY; idY<=iMaxY;idY++){
					
					double pY=idY*decimate; // in sensor pixels
					for (int idX=iMinX; idX<=iMaxX;idX++){
						double pX=idX*decimate; // in sensor pixels
						// scan allowed triangles, usually 2
						for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
							int [] cycle=cycles[nCycle];
							// is this point inside?
							if (debugExit) {
								for (int nEdge=0;nEdge<cycle.length;nEdge++){
									int nextNEdge=(nEdge==(cycle.length-1))?0:(nEdge+1);
									System.out.println("nEdge="+nEdge+" nextNEdge"+nextNEdge);

									originXY[0]=imgData[2][vu+uvInc[cycle[nEdge]]];
									originXY[1]=imgData[3][vu+uvInc[cycle[nEdge]]];
									endXY[0]=   imgData[2][vu+uvInc[cycle[nextNEdge]]];
									endXY[1]=   imgData[3][vu+uvInc[cycle[nextNEdge]]];
									System.out.println("--- pX="+IJ.d2s(pX,1)+" originXY[0]="+IJ.d2s(originXY[0],1)+
											" endXY[1]="+IJ.d2s(endXY[1],1)+" originXY[1]="+IJ.d2s(originXY[1],1));
									System.out.println("--- pY="+IJ.d2s(pY,1)+" originXY[1]="+IJ.d2s(originXY[1],1)+
											" endXY[0]="+IJ.d2s(endXY[0],1)+" originXY[0]="+IJ.d2s(originXY[0],1));
									System.out.println("Cross-product="+IJ.d2s(((pX-originXY[0])*(endXY[1]-originXY[1]) - (pY-originXY[1])*(endXY[0]-originXY[0])),1));
									
								}
							}

							boolean inside=true;
							for (int nEdge=0;nEdge<cycle.length;nEdge++){
								int nextNEdge=(nEdge==(cycle.length-1))?0:(nEdge+1);

								originXY[0]=imgData[2][vu+uvInc[cycle[nEdge]]];
								originXY[1]=imgData[3][vu+uvInc[cycle[nEdge]]];
								endXY[0]=   imgData[2][vu+uvInc[cycle[nextNEdge]]];
								endXY[1]=   imgData[3][vu+uvInc[cycle[nextNEdge]]];
								if (((pX-originXY[0])*(endXY[1]-originXY[1]) - (pY-originXY[1])*(endXY[0]-originXY[0]))<0.0){
									inside=false;
									break;
								}
							}
							if (!inside) continue; // point is outside of the interpolation area, try next triangle (if any)
//							if ((this.debugLevel>3) && !debugExit) {
							if (this.debugLevel>3) {
								System.out.println("idX="+idX+" idY="+idY+" nCycle="+nCycle);
								String debugString1="cycle:";
								for (int i =0;i<cycle.length; i++) debugString1+=" "+cycle[i];
								System.out.println(debugString1);
							}

							/* interpolate: 
							1. taking cycles[0] as origin and two (non co-linear) edge vectors - V1:from 0 to 1 and V2 from 1 to 2
							    find a1 and a2  so that vector V  (from 0  to pXY) = a1*V1+ a2*V2
							2. if F0 is the value of the interpolated function at cycles[0], F1 and F2 - at cycles[1] and cycles2
							   then F=F0+(F1-F0)*a1 +(F2-F1)*a2    
							 */
							double [] XY0={imgData[2][vu+uvInc[cycle[0]]],imgData[3][vu+uvInc[cycle[0]]]};
							double [] XY1={imgData[2][vu+uvInc[cycle[1]]],imgData[3][vu+uvInc[cycle[1]]]};
							double [] XY2={imgData[2][vu+uvInc[cycle[2]]],imgData[3][vu+uvInc[cycle[2]]]};
							double [] V= {pX-XY0[0],pY-XY0[1]};
							double [][] M={
									{XY1[0]-XY0[0],XY2[0]-XY1[0]},
									{XY1[1]-XY0[1],XY2[1]-XY1[1]}};
							double det=M[0][0]*M[1][1]-M[1][0]*M[0][1];
							double [][] MInverse={
									{ M[1][1]/det,-M[0][1]/det},
									{-M[1][0]/det, M[0][0]/det}};
							double [] a12={
									MInverse[0][0]*V[0]+MInverse[0][1]*V[1],
									MInverse[1][0]*V[0]+MInverse[1][1]*V[1]};
							int pCorrIndex=idY*sWidth+idX;
// some points may be accumulated multiple times - thisPCorr[3] will take care of this
							if (this.debugLevel>3) {
								System.out.println("XY0="+IJ.d2s(XY0[0],3)+":"+IJ.d2s(XY0[1],3));
								System.out.println("XY1="+IJ.d2s(XY1[0],3)+":"+IJ.d2s(XY1[1],3));
								System.out.println("XY2="+IJ.d2s(XY2[0],3)+":"+IJ.d2s(XY2[1],3));
								System.out.println("M00="+IJ.d2s(M[0][0],3)+" M01="+IJ.d2s(M[0][1],3));
								System.out.println("M10="+IJ.d2s(M[1][0],3)+" M11="+IJ.d2s(M[1][1],3));
								System.out.println("MInverse00="+IJ.d2s(MInverse[0][0],5)+" MInverse01="+IJ.d2s(MInverse[0][1],5));
								System.out.println("MInverse10="+IJ.d2s(MInverse[1][0],5)+" MInverse11="+IJ.d2s(MInverse[1][1],5));
								System.out.println("a12="+IJ.d2s(a12[0],3)+":"+IJ.d2s(a12[1],3));
								System.out.println("imgData[0][vu+uvInc[cycle[0]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[0]]],3)+
										"imgData[1][vu+uvInc[cycle[0]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[0]]],3));
								System.out.println("imgData[0][vu+uvInc[cycle[1]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[1]]],3)+
										"imgData[1][vu+uvInc[cycle[1]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[1]]],3));
								System.out.println("imgData[0][vu+uvInc[cycle[2]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[2]]],3)+
										"imgData[1][vu+uvInc[cycle[2]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[2]]],3));
							}

							double [] corr={
									 imgData[0][vu+uvInc[cycle[0]]]+ // dPx
									(imgData[0][vu+uvInc[cycle[1]]]-imgData[0][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[0][vu+uvInc[cycle[2]]]-imgData[0][vu+uvInc[cycle[1]]])*a12[1],
									
									 imgData[1][vu+uvInc[cycle[0]]]+ // dPy
									(imgData[1][vu+uvInc[cycle[1]]]-imgData[1][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[1][vu+uvInc[cycle[2]]]-imgData[1][vu+uvInc[cycle[1]]])*a12[1],
									
									 imgData[4][vu+uvInc[cycle[0]]]+ // alpha
									(imgData[4][vu+uvInc[cycle[1]]]-imgData[4][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[4][vu+uvInc[cycle[2]]]-imgData[4][vu+uvInc[cycle[1]]])*a12[1],
									 imgData[5][vu+uvInc[cycle[0]]]+ // Red measured/pattern
									(imgData[5][vu+uvInc[cycle[1]]]-imgData[5][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[5][vu+uvInc[cycle[2]]]-imgData[5][vu+uvInc[cycle[1]]])*a12[1],
									 imgData[6][vu+uvInc[cycle[0]]]+ // Green measured/pattern
									(imgData[6][vu+uvInc[cycle[1]]]-imgData[6][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[6][vu+uvInc[cycle[2]]]-imgData[6][vu+uvInc[cycle[1]]])*a12[1],
									 imgData[7][vu+uvInc[cycle[0]]]+ // Blue  measured/pattern
									(imgData[7][vu+uvInc[cycle[1]]]-imgData[7][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[7][vu+uvInc[cycle[2]]]-imgData[7][vu+uvInc[cycle[1]]])*a12[1]
									};
							if (this.debugLevel>3) {
								System.out.println("corr="+IJ.d2s(corr[0],3)+" "+IJ.d2s(corr[1],3)+" "+IJ.d2s(corr[2],3));
							}
 if (pCorrIndex>thisPCorr[0].length) {
	 System.out.println("imgNum=" + imgNum+": "+	fittingStrategy.distortionCalibrationData.gIP[imgNum].path);
	 System.out.println("thisPCorr[0].length="+thisPCorr[0].length+" pCorrIndex="+pCorrIndex+" sWidth="+sWidth+" idY="+idY+" idX="+idX);
 }
							thisPCorr[0][pCorrIndex]+= corr[0];// dPx							
							thisPCorr[1][pCorrIndex]+= corr[1];// dPy
							thisPCorr[2][pCorrIndex]+= corr[2];// alpha
							thisPCorr[3][pCorrIndex]+= 1.0;    // number of times accumulated
							thisPCorr[4][pCorrIndex]+= corr[3];// red
							thisPCorr[5][pCorrIndex]+= corr[4];// green
							thisPCorr[6][pCorrIndex]+= corr[5];// blue

							if (this.debugLevel>3) {
								debugHadPixels=true;
//								if (!debugExit) debugCntr--;
//								if (debugCntr==0) debugExit=true; // exit after first non-empty tile
							}

//gridPCorr[chnNum]
						}
					} // idX
					// use same order in calculations, make sure no gaps
				} // idY
				if ((this.debugLevel>3) && (debugHadPixels)){
					if (!debugExit) {
						System.out.println(
								" minX="+IJ.d2s(minX,1)+
								" maxX="+IJ.d2s(maxX,1));
						System.out.println(
								" minY="+IJ.d2s(minY,1)+
								" maxY="+IJ.d2s(maxY,1));
						System.out.println(
								" iMinX="+iMinX+
								" iMaxX="+iMaxX);
						System.out.println(
								" iMinY="+iMinY+
								" iMaxY="+iMaxY);
					}
					if (!debugExit) debugCntr--;
					if (debugCntr==0) debugExit=true; // exit after first non-empty tile
					
				}
			} // finished image
			
			
/*			if (showIndividual) {
				String [] titles={"dPx","dPy","alpha","Multiple","Red","Green","Blue"};
				this.SDFA_INSTANCE.showArrays(thisPCorr, sWidth, sHeight,  true, "thisPCorr_pre"+imgNum, titles);
			}
*/			
			// some points may be calculated multiple times 
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (thisPCorr[3][i]>=1.0){
				thisPCorr[0][i]/=thisPCorr[3][i]; // dPx
				thisPCorr[1][i]/=thisPCorr[3][i]; // dPy
				thisPCorr[2][i]/=thisPCorr[3][i]; // alpha
				thisPCorr[4][i]/=thisPCorr[3][i]; // r
				thisPCorr[5][i]/=thisPCorr[3][i]; // g
				thisPCorr[6][i]/=thisPCorr[3][i]; // b
			}

			if (showIndividual && ((showIndividualNumber<0) || (showIndividualNumber==chnNum))) {
				String [] titles={"dPx","dPy","alpha","Multiple","Red","Green","Blue"};
				this.SDFA_INSTANCE.showArrays(thisPCorr, sWidth, sHeight,  true, "thisPCorr"+imgNum, titles);
			}
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (thisPCorr[2][i]>0){
				gridPCorr[chnNum][0][i]+=thisPCorr[0][i]*thisPCorr[2][i];
				gridPCorr[chnNum][1][i]+=thisPCorr[1][i]*thisPCorr[2][i];
				/**TODO: not used anyway - just for debugging? see if just the sensor mask should go here? Or when saving?*/
				if (gridPCorr[chnNum][2][i]<thisPCorr[2][i]) gridPCorr[chnNum][2][i]=thisPCorr[2][i]; // best alpha
				gridPCorr[chnNum][3][i]+=                thisPCorr[2][i]; // sum of weights from all images
				gridPCorr[chnNum][4][i]+=thisPCorr[4][i]*thisPCorr[2][i];
				gridPCorr[chnNum][5][i]+=thisPCorr[5][i]*thisPCorr[2][i];
				gridPCorr[chnNum][6][i]+=thisPCorr[6][i]*thisPCorr[2][i];
			}
			IJ.showProgress(++numProcessed, numSelected);
		}
/*		
		if (showIndividual) {
			String [] titles={"dPx","dPy","alpha","Multiple","Red","Green","Blue"};
			for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) if (gridPCorr[chnNum]!=null) this.SDFA_INSTANCE.showArrays(gridPCorr[chnNum], sWidth, sHeight,  true, "gridPCorr1"+chnNum, titles);
		}
*/		
		for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) if (gridPCorr[chnNum]!=null){
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (gridPCorr[chnNum][2][i]>0){ //null pointer
				gridPCorr[chnNum][0][i]/=gridPCorr[chnNum][3][i];
				gridPCorr[chnNum][1][i]/=gridPCorr[chnNum][3][i];
				gridPCorr[chnNum][4][i]/=gridPCorr[chnNum][3][i];
				gridPCorr[chnNum][5][i]/=gridPCorr[chnNum][3][i];
				gridPCorr[chnNum][6][i]/=gridPCorr[chnNum][3][i];
			}
		}
/*		
		if (showIndividual) {
			String [] titles={"dPx","dPy","alpha","Multiple","Red","Green","Blue"};
			for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) if (gridPCorr[chnNum]!=null) this.SDFA_INSTANCE.showArrays(gridPCorr[chnNum], sWidth, sHeight,  true, "gridPCorr2"+chnNum, titles);
		}
*/		
		return gridPCorr;
	}
		
	public double [][][] calculateSensorXYCorrOld(
			DistortionCalibrationData distortionCalibrationData,
			boolean showIndividual
			){
		int decimate=distortionCalibrationData.eyesisCameraParameters.decimateMasks;
		int sWidth= (distortionCalibrationData.eyesisCameraParameters.sensorWidth-1)/decimate+1;
		int sHeight=(distortionCalibrationData.eyesisCameraParameters.sensorHeight-1)/decimate+1;
		int numChannels=distortionCalibrationData.getNumChannels(); // number of used channels
		int width=getGridWidth();
		int height=getGridHeight();
		int [] uvInc={0,1,width,width+1}; // four corners as vu index
		int [][] cycles={ // counter-clockwise corners bounding the area  (only orthogonal sides?)
//				{0,2,3,1}, // 
				{1,0,2},
				{2,3,1},
				{0,2,3},
				{3,1,0}};
		
		// prepare grid correction arrays
//		if ((decimate!=)
//	    public double [][][] pixelCorrection=null;
//	    public int        pixelCorrectionDecimation=1;

		double [][][] gridPCorr=new double [numChannels][][];
		for (int chnNum=0;chnNum<gridPCorr.length;chnNum++) gridPCorr[chnNum]=null; 
		boolean [] selectedImages=fittingStrategy.selectedImages();
		boolean debugExit=false;
		int debugCntr=2;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
			if (debugExit) break;
			int chnNum=fittingStrategy.distortionCalibrationData.gIP[imgNum].channel; // number of sub-camera
			// initialize this array if it is needed, leave unused null
			if (gridPCorr[chnNum]==null){
				 gridPCorr[chnNum]=new double [4][sWidth*sHeight];
				for (int n=0;n<gridPCorr[chnNum].length;n++) for (int i=0;i<gridPCorr[chnNum][0].length;i++) gridPCorr[chnNum][n][i]=0.0;
			}
			double [][] thisPCorr=null;
			
			thisPCorr=new double [4][sWidth*sHeight]; // calculate for a single (this) image, accumulate in the end
			for (int n=0;n<thisPCorr.length;n++) for (int i=0;i<thisPCorr[0].length;i++) thisPCorr[n][i]=0.0;
			double [] diff=calcYminusFx(this.currentfX);
			// find data range for the selected image
			int index=0;
			int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
			for (int iNum=0;(iNum<imgNum) && (iNum<numImg) ;iNum++) if (selectedImages[iNum])
				index+=fittingStrategy.distortionCalibrationData.gIP[iNum].pixelsUV.length;
			if (this.debugLevel>2) {
				System.out.println("calculateGridXYCorr(): fX.length="+this.currentfX.length+" this image index="+index);
			}
			double [][] imgData=new double[5][getGridHeight() * width]; // dPX, dPY, Px, Py, alpha
			for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
			// first pass - prepare [v][u]arrays
			double []mask= fittingStrategy.distortionCalibrationData.calculateImageGridMask(imgNum);
			for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;i++){
				int u=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][0]+patternParameters.U0;
				int v=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV[i][1]+patternParameters.V0;
				int vu=u+width*v;
				imgData[0][vu]=   diff[2*(index+i)];
				imgData[1][vu]=   diff[2*(index+i)+1];
				imgData[2][vu]= this.Y[2*(index+i)];  // measured pixel x
				imgData[3][vu]= this.Y[2*(index+i)+1];// measured pixel y
//				imgData[2][vu]= this.currentfX[2*(index+i)];  // calculated pixel x
//				imgData[3][vu]= this.currentfX[2*(index+i)+1];// calculated pixel y
// next line not needed? - no, let it stay, 0.0 / >0 will be needed
// TODO: Use grid alpha				
				imgData[4][vu]= fittingStrategy.distortionCalibrationData.getMask(mask, imgData[2][vu], imgData[3][vu]); // will decimate
			}
			// now use imgData array to fill thisPCorr by linear interpolation
			for (int v=0;v<(height-1); v++) for (int u=0; u<(width-1);u++){
				if (debugExit) break;
				int vu=u+width*v;
                double [][] cornerXY =new double[4][];
                for (int i=0;i<uvInc.length;i++){
                	int vu1=vu+uvInc[i];	
                	if (imgData[4][vu1]>0.0){
                		cornerXY[i]=new double[2];
                		cornerXY[i][0]=imgData[2][vu1];
                		cornerXY[i][1]=imgData[3][vu1];
                	} else cornerXY[i]=null;
                }
                boolean [] cycleFits=new boolean[cycles.length];   
                boolean anyFits=false;
                for (int i=0;i<cycles.length;i++){
                	cycleFits[i]=true;
                	for (int j=0;j<cycles[i].length;j++) if (cornerXY[cycles[i][j]]==null) {
                		cycleFits[i]=false;
                		break;
                	}
                }
                if (!anyFits) continue; // not a single cycle
				if ((this.debugLevel>3) && !debugExit) {
					String debugString="cycleFits ";
					for (int i =0;i<cycleFits.length; i++) debugString+=" "+cycleFits[i];
					System.out.println(debugString);
				}
                if (cycleFits[0]&&cycleFits[1]){ // remove overlaps
                	cycleFits[2]=false;
                	cycleFits[3]=false;
                }
                boolean minMaxUndefined=true;
				double minX=0,maxX=0,minY=0,maxY=0;
				// find bounding rectangle;
				for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
					int [] cycle=cycles[nCycle];
					for (int corner=0; corner<cycle.length;corner++){
						if (minMaxUndefined || (minX>cornerXY[cycle[corner]][0])) minX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (maxX<cornerXY[cycle[corner]][0])) maxX=cornerXY[cycle[corner]][0];
						if (minMaxUndefined || (minY>cornerXY[cycle[corner]][1])) minY=cornerXY[cycle[corner]][1];
						if (minMaxUndefined || (maxY<cornerXY[cycle[corner]][1])) maxY=cornerXY[cycle[corner]][1];
						minMaxUndefined=false;
					}
				}
				int iMinX=(int) Math.floor(minX/decimate);
				int iMinY=(int) Math.floor(minY/decimate);
				int iMaxX=(int) Math.ceil(maxX/decimate);
				int iMaxY=(int) Math.ceil(maxY/decimate);
				double [] originXY=new double [2];
				double [] endXY=new double [2];
				boolean debugHadPixels=false;
//TODO: scan X,Y in this rectangle, for points in defined squares/triangles find if the point is inside (accurate not to loose any).
				for (int idY=iMinY; idY<=iMaxY;idY++){
					
					double pY=idY*decimate; // in sensor pixels
					for (int idX=iMinX; idX<=iMaxX;idX++){
						double pX=idX*decimate; // in sensor pixels
						// scan allowed triangles, usually 2
						for (int nCycle=0;nCycle<cycles.length;nCycle++) if (cycleFits[nCycle]){
							int [] cycle=cycles[nCycle];
							// is this point inside?
							if (debugExit) {
								for (int nEdge=0;nEdge<cycle.length;nEdge++){
									int nextNEdge=(nEdge==(cycle.length-1))?0:(nEdge+1);
									System.out.println("nEdge="+nEdge+" nextNEdge"+nextNEdge);

									originXY[0]=imgData[2][vu+uvInc[cycle[nEdge]]];
									originXY[1]=imgData[3][vu+uvInc[cycle[nEdge]]];
									endXY[0]=   imgData[2][vu+uvInc[cycle[nextNEdge]]];
									endXY[1]=   imgData[3][vu+uvInc[cycle[nextNEdge]]];
									System.out.println("--- pX="+IJ.d2s(pX,1)+" originXY[0]="+IJ.d2s(originXY[0],1)+
											" endXY[1]="+IJ.d2s(endXY[1],1)+" originXY[1]="+IJ.d2s(originXY[1],1));
									System.out.println("--- pY="+IJ.d2s(pY,1)+" originXY[1]="+IJ.d2s(originXY[1],1)+
											" endXY[0]="+IJ.d2s(endXY[0],1)+" originXY[0]="+IJ.d2s(originXY[0],1));
									System.out.println("Cross-product="+IJ.d2s(((pX-originXY[0])*(endXY[1]-originXY[1]) - (pY-originXY[1])*(endXY[0]-originXY[0])),1));
									
								}
							}

							boolean inside=true;
							for (int nEdge=0;nEdge<cycle.length;nEdge++){
								int nextNEdge=(nEdge==(cycle.length-1))?0:(nEdge+1);

								originXY[0]=imgData[2][vu+uvInc[cycle[nEdge]]];
								originXY[1]=imgData[3][vu+uvInc[cycle[nEdge]]];
								endXY[0]=   imgData[2][vu+uvInc[cycle[nextNEdge]]];
								endXY[1]=   imgData[3][vu+uvInc[cycle[nextNEdge]]];
								if (((pX-originXY[0])*(endXY[1]-originXY[1]) - (pY-originXY[1])*(endXY[0]-originXY[0]))<0.0){
									inside=false;
									break;
								}
							}
							if (!inside) continue; // point is outside of the interpolation area, try next triangle (if any)
//							if ((this.debugLevel>3) && !debugExit) {
							if (this.debugLevel>3) {
								System.out.println("idX="+idX+" idY="+idY+" nCycle="+nCycle);
								String debugString1="cycle:";
								for (int i =0;i<cycle.length; i++) debugString1+=" "+cycle[i];
								System.out.println(debugString1);
							}

							/* interpolate: 
							1. taking cycles[0] as origin and two (non co-linear) edge vectors - V1:from 0 to 1 and V2 from 1 to 2
							    find a1 and a2  so that vector V  (from 0  to pXY) = a1*V1+ a2*V2
							2. if F0 is the value of the interpolated function at cycles[0], F1 and F2 - at cycles[1] and cycles2
							   then F=F0+(F1-F0)*a1 +(F2-F1)*a2    
							 */
							double [] XY0={imgData[2][vu+uvInc[cycle[0]]],imgData[3][vu+uvInc[cycle[0]]]};
							double [] XY1={imgData[2][vu+uvInc[cycle[1]]],imgData[3][vu+uvInc[cycle[1]]]};
							double [] XY2={imgData[2][vu+uvInc[cycle[2]]],imgData[3][vu+uvInc[cycle[2]]]};
							double [] V= {pX-XY0[0],pY-XY0[1]};
							double [][] M={
									{XY1[0]-XY0[0],XY2[0]-XY1[0]},
									{XY1[1]-XY0[1],XY2[1]-XY1[1]}};
							double det=M[0][0]*M[1][1]-M[1][0]*M[0][1];
							double [][] MInverse={
									{ M[1][1]/det,-M[0][1]/det},
									{-M[1][0]/det, M[0][0]/det}};
							double [] a12={
									MInverse[0][0]*V[0]+MInverse[0][1]*V[1],
									MInverse[1][0]*V[0]+MInverse[1][1]*V[1]};
							int pCorrIndex=idY*sWidth+idX;
// some points may be accumulated multiple times - thisPCorr[3] will take care of this
							if (this.debugLevel>3) {
								System.out.println("XY0="+IJ.d2s(XY0[0],3)+":"+IJ.d2s(XY0[1],3));
								System.out.println("XY1="+IJ.d2s(XY1[0],3)+":"+IJ.d2s(XY1[1],3));
								System.out.println("XY2="+IJ.d2s(XY2[0],3)+":"+IJ.d2s(XY2[1],3));
								System.out.println("M00="+IJ.d2s(M[0][0],3)+" M01="+IJ.d2s(M[0][1],3));
								System.out.println("M10="+IJ.d2s(M[1][0],3)+" M11="+IJ.d2s(M[1][1],3));
								System.out.println("MInverse00="+IJ.d2s(MInverse[0][0],5)+" MInverse01="+IJ.d2s(MInverse[0][1],5));
								System.out.println("MInverse10="+IJ.d2s(MInverse[1][0],5)+" MInverse11="+IJ.d2s(MInverse[1][1],5));
								System.out.println("a12="+IJ.d2s(a12[0],3)+":"+IJ.d2s(a12[1],3));
								System.out.println("imgData[0][vu+uvInc[cycle[0]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[0]]],3)+
										"imgData[1][vu+uvInc[cycle[0]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[0]]],3));
								System.out.println("imgData[0][vu+uvInc[cycle[1]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[1]]],3)+
										"imgData[1][vu+uvInc[cycle[1]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[1]]],3));
								System.out.println("imgData[0][vu+uvInc[cycle[2]]]="+IJ.d2s(imgData[0][vu+uvInc[cycle[2]]],3)+
										"imgData[1][vu+uvInc[cycle[2]]]="+IJ.d2s(imgData[1][vu+uvInc[cycle[2]]],3));
							}

							double [] corr={
									imgData[0][vu+uvInc[cycle[0]]]+ // dPx
									(imgData[0][vu+uvInc[cycle[1]]]-imgData[0][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[0][vu+uvInc[cycle[2]]]-imgData[0][vu+uvInc[cycle[1]]])*a12[1],
									imgData[1][vu+uvInc[cycle[0]]]+ // dPy
									(imgData[1][vu+uvInc[cycle[1]]]-imgData[1][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[1][vu+uvInc[cycle[2]]]-imgData[1][vu+uvInc[cycle[1]]])*a12[1],
									imgData[4][vu+uvInc[cycle[0]]]+ // alpha
									(imgData[4][vu+uvInc[cycle[1]]]-imgData[4][vu+uvInc[cycle[0]]])*a12[0]+
									(imgData[4][vu+uvInc[cycle[2]]]-imgData[4][vu+uvInc[cycle[1]]])*a12[1]};
							if (this.debugLevel>3) {
								System.out.println("corr="+IJ.d2s(corr[0],3)+" "+IJ.d2s(corr[1],3)+" "+IJ.d2s(corr[2],3));
							}

							thisPCorr[0][pCorrIndex]+= corr[0];// dPx
							thisPCorr[1][pCorrIndex]+= corr[1];// dPy
							thisPCorr[2][pCorrIndex]+= corr[2];// alpha
							thisPCorr[3][pCorrIndex]+= 1.0;    // number of times accumulated

							if (this.debugLevel>3) {
								debugHadPixels=true;
//								if (!debugExit) debugCntr--;
//								if (debugCntr==0) debugExit=true; // exit after first non-empty tile
							}

//gridPCorr[chnNum]
						}
					} // idX
					// use same order in calculations, make sure no gaps
				} // idY
				if ((this.debugLevel>3) && (debugHadPixels)){
					if (!debugExit) {
						System.out.println(
								" minX="+IJ.d2s(minX,1)+
								" maxX="+IJ.d2s(maxX,1));
						System.out.println(
								" minY="+IJ.d2s(minY,1)+
								" maxY="+IJ.d2s(maxY,1));
						System.out.println(
								" iMinX="+iMinX+
								" iMaxX="+iMaxX);
						System.out.println(
								" iMinY="+iMinY+
								" iMaxY="+iMaxY);
					}
					if (!debugExit) debugCntr--;
					if (debugCntr==0) debugExit=true; // exit after first non-empty tile
					
				}
			} // finished image
			// some points may be calculated multiple times 
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (thisPCorr[3][i]>=1.0){
				thisPCorr[0][i]/=thisPCorr[3][i];
				thisPCorr[1][i]/=thisPCorr[3][i];
				thisPCorr[2][i]/=thisPCorr[3][i];
			}

			if (showIndividual) {
				String [] titles={"dPx","dPy","alpha","Multiple"};
				this.SDFA_INSTANCE.showArrays(thisPCorr, sWidth, sHeight,  true, "thisPCorr"+imgNum, titles);
			}
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (thisPCorr[2][i]>0){
				gridPCorr[chnNum][0][i]+=thisPCorr[0][i]*thisPCorr[2][i];
				gridPCorr[chnNum][1][i]+=thisPCorr[1][i]*thisPCorr[2][i];
				/**TODO: not used anyway - just for debugging? see if just the sensor mask should go here? Or when saving?*/
				if (gridPCorr[chnNum][2][i]<thisPCorr[2][i]) gridPCorr[chnNum][2][i]=thisPCorr[2][i]; // best alpha
				gridPCorr[chnNum][3][i]+=                thisPCorr[2][i]; // sum of weights from all images
			}
			
		}
		for (int chnNum=0;chnNum<gridPCorr.length;chnNum++){
			for (int i=0;i<gridPCorr[chnNum][0].length;i++) if (gridPCorr[chnNum][2][i]>0){ //null pointer
				gridPCorr[chnNum][0][i]/=gridPCorr[chnNum][3][i];
				gridPCorr[chnNum][1][i]/=gridPCorr[chnNum][3][i];
			}
		}
		return gridPCorr;
	}
	
/**
 * Calculate partial derivative analytically (as the Jacobian calculation) and by difference divided by delta and compare
 * Done to debug derivatives calculation
 */
	public void compareDerivatives(){
		if (fittingStrategy==null) {
			String msg="Fitting strategy does not exist, exiting";
			IJ.showMessage("Error",msg);
			throw new IllegalArgumentException (msg);
		}
    	int numSeries=fittingStrategy.getNumSeries();
	    GenericDialog gd = new GenericDialog("Debug: verifying partial derivatives calculation, select series number");
		gd.addNumericField("Series number to show (0.."+(numSeries-1), this.seriesNumber, 0);
		gd.addCheckbox("Show actual parameters (false: X0,Y0,distance, angles)", true);
		gd.addCheckbox("Apply sensor mask (fade near edges)", true);
		gd.addCheckbox("Debug derivatives (show analytic/difference match)",true);
	    gd.showDialog();
	    if (gd.wasCanceled()) return;
	    this.seriesNumber=     (int) gd.getNextNumber();
	    boolean useActualParameters=gd.getNextBoolean();
	    boolean applySensorMask=gd.getNextBoolean();
	    boolean debugDerivatives=gd.getNextBoolean();
	    // currently not possible to debug "internal" parameters, so
//	    debugDerivatives&=useActualParameters; //*******************
		initFittingSeries(false,filterForAll,this.seriesNumber);
		int numPars=this.currentVector.length;
    	String [] parameterNames;
    	String [] parameterUnits;
    	
    	if (useActualParameters) {
    		parameterNames=new String[fittingStrategy.distortionCalibrationData.parameterDescriptions.length];
    		parameterUnits=new String[fittingStrategy.distortionCalibrationData.parameterDescriptions.length];
    		for (int i=0;i<parameterNames.length;i++){
    			// TODO: move to DdistortionCalibrationData methods()
    			parameterNames[i]=fittingStrategy.distortionCalibrationData.parameterDescriptions[i][0];
    			parameterUnits[i]=fittingStrategy.distortionCalibrationData.parameterDescriptions[i][2];
    		}
    	} else {
    		parameterNames=lensDistortionParameters.getAllNames();
    		parameterUnits=lensDistortionParameters.getAllUnits();
    	}
	    gd = new GenericDialog((debugDerivatives?"Debug: verifying partial derivatives calculation,":"Showing partial derivatives,") +" select parameter number");
	    if (useActualParameters) {
	    	for (int i=0;i<this.currentVector.length;i++){
	    		int parNum=fittingStrategy.parameterMap[i][1];
	    		int imgNum=fittingStrategy.parameterMap[i][0];
	    		gd.addMessage(i+": "+parameterNames[parNum]+
	    				"["+imgNum+"]("+parameterUnits[parNum]+") "+IJ.d2s(this.currentVector[i],3));
	    	}
			gd.addNumericField("Select parameter number (0.."+(numPars-1)+") from above", 0, 0);
	    } else {
	    	for (int i=0;i<parameterNames.length;i++){
	    		gd.addMessage(i+": "+parameterNames[i]+"("+parameterUnits[i]+") ");
	    	}
			gd.addNumericField("Select parameter number (0.."+(parameterNames.length-1)+") from above", 0, 0);

	    }
		if (debugDerivatives) gd.addNumericField("Select delta to increment selected parameter", .01, 5);
		if (debugDerivatives) gd.addCheckbox("Show inter-parameter derivatives matrix", true);
	    gd.showDialog();
	    if (gd.wasCanceled()) return;
	    int selectedParameter=     (int) gd.getNextNumber();
	    double delta=0;
	    if (debugDerivatives) delta=     gd.getNextNumber();
	    boolean showInterparameterDerivatives=false;
	    if (debugDerivatives) showInterparameterDerivatives=gd.getNextBoolean();
		double [] this_currentfX=null;
	    double [] d_derivative;
	    double [] d_delta=null;
	    String title;
	    if (useActualParameters) {
	    	this_currentfX=calculateFxAndJacobian(this.currentVector, true); // is it always true here (this.jacobian==null)
	    	d_derivative=this.jacobian[selectedParameter].clone();
	    	if (debugDerivatives) {
	    		double[] modVector=this.currentVector.clone();
	    		modVector[selectedParameter]+=delta;
	    		d_delta=calculateFxAndJacobian(modVector, true);
	    		if (this.debugLevel>3) {
	    			for (int i=0;i<d_delta.length;i++) {
	    				System.out.println(i+": "+IJ.d2s(d_delta[i],3)+" - "+IJ.d2s(this_currentfX[i],3)+" = "
	    						+ IJ.d2s(d_delta[i]-this_currentfX[i],3));
	    			}

	    		}
	    		for (int i=0;i<d_delta.length;i++) d_delta[i]= (d_delta[i]-this_currentfX[i])/delta;
	    	}
		    int parNum=fittingStrategy.parameterMap[selectedParameter][1];
			int imgNum=fittingStrategy.parameterMap[selectedParameter][0];
			title=parameterNames[parNum]+"_derivatives:"+imgNum;
	    } else {
	    	d_derivative=calculateJacobian16(this.currentVector, -1,0.0)[selectedParameter].clone();
	    	if (debugDerivatives) d_delta=     calculateJacobian16(this.currentVector, -1,delta)[selectedParameter].clone();
			title=parameterNames[selectedParameter]+"_derivatives";
	    }
	    if (this.debugLevel>3) {
		    for (int i=0;i<d_delta.length;i++) {
		    	System.out.println(i+":: "+IJ.d2s(d_delta[i],3)+" - "+IJ.d2s(d_derivative[i],3));
		    }
	    }
	    double [] sumWeight=showCompareDerivatives (d_derivative, d_delta, applySensorMask, !useActualParameters,  title ); // d_delta==null - no debug
	    if (showInterparameterDerivatives && (delta>0)) {
	    debugCompareInterparameterDerivatives(
	    		this.currentVector.clone(),
	    		-1, //int imgNum,
	    		delta);
	    }
	    for (int i=0;i<sumWeight.length; i++) if (sumWeight[i]>0.0){
	    	System.out.println("Image "+i+", "+title+"derivative RMS="+sumWeight[i]);
	    }
	}
	
	
	/**
	 * Show comparison of the calculated partial derivatives in Jacobian and approximated by difference
	 * for incremented parameters 
	 * @param imgNumber - number of image in series to show
	 * @param d_derivative vector array of "true" derivatives (from Jacobian)
	 * @param d_delta approximated derivatives from varying parameter
	 * @param title image title
	 * @return rms 
	 */
	public double showCompareDerivatives(int imgNumber, double [] d_derivative, double [] d_delta, boolean applySensorMask, String title ){
		String [] titlesDebug={"dX-derivative","dY-derivative","abs-derivative","diff-X (should be 0)","diff-Y (should be 0)","dX-delta/delta","dY-delta/delta"};
		String [] titlesNoDebug={"dX-derivative","dY-derivative","abs-derivative"};
		String [] titles= (d_delta==null)? titlesNoDebug:titlesDebug;
		double [] d_diff=new double [d_derivative.length];
		double [] aDeriv=new double [d_derivative.length/2];
		
		if (d_delta!=null) for (int i=0;i<d_diff.length;i++) d_diff[i]=d_derivative[i]-d_delta[i];
// find data range for the selected image
		int index=0;
		int numImg=fittingStrategy.distortionCalibrationData.getNumImages();
		boolean [] selectedImages=fittingStrategy.selectedImages();
		for (int imgNum=0;(imgNum<imgNumber) && (imgNum<numImg) ;imgNum++) if (selectedImages[imgNum])
			index+=fittingStrategy.distortionCalibrationData.gIP[imgNum].pixelsUV.length;
		double sumWeights=0.0;
		double sumDerivatives2=0.0;
		double w,sqrtW;
		for (int i=2*index;i<2*(2*index+fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV.length);i++){
			w=applySensorMask?this.weightFunction[i]:1.0;
			if (w<0.0) w=0.0;
			sumWeights+=w;
			sumDerivatives2+=d_derivative[i]*d_derivative[i]*w;
			sqrtW=Math.sqrt(w);
			d_derivative[i]*=sqrtW; // for display
			if (d_delta!=null) d_delta[i]*=sqrtW;
			if ((i&1)==0) aDeriv[i>>1]=Math.sqrt(d_derivative[i]*d_derivative[i]+d_derivative[i+1]*d_derivative[i+1]);
		}		
		sumDerivatives2=Math.sqrt(sumDerivatives2/sumWeights*2.0); // 2.0 because x,y pair should not be averaged, just added
		titles[2]+=":rms="+sumDerivatives2;
		int width=getGridWidth();
		double [][] imgData=new double[titles.length][getGridHeight() * width];
		for (int i=0;i<imgData.length;i++) for (int j=0;j<imgData[i].length;j++)imgData[i][j]=0.0;
		
		for (int i=0;i<fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV.length;i++){
			int u=fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV[index+i][0]+patternParameters.U0;
			int v=fittingStrategy.distortionCalibrationData.gIP[imgNumber].pixelsUV[index+i][1]+patternParameters.V0;
			int vu=u+width*v;
			imgData[0][vu]=   d_derivative[2*(index+i)];
			imgData[1][vu]=   d_derivative[2*(index+i)+1];
			imgData[2][vu]=   aDeriv[index+i];
			if (d_delta!=null) {
				imgData[3][vu]=   d_diff[2*(index+i)];
				imgData[4][vu]=   d_diff[2*(index+i)+1];
				imgData[5][vu]=   d_delta[2*(index+i)];
				imgData[6][vu]=   d_delta[2*(index+i)+1];
			}
		}
		this.SDFA_INSTANCE.showArrays(imgData, width, getGridHeight(),  true, title, titles);
		return sumDerivatives2;
	}
	/**
	 * Show comparison of the calculated partial derivatives in Jacobian and approximated by difference
	 * for incremented parameters (for all selected images in the series) 
	 * @param d_derivative vector array of "true" derivatives (from Jacobian)
	 * @param d_delta approximated derivatives form varying parameter
	 * @param applySensorMask Multiply by sensor mask (fade near edges)
	 * @param single calculate just the first selected image
	 * @param title image title
	 * @return array of rms
	 */

	public double[] showCompareDerivatives (double [] d_derivative, double [] d_delta, boolean applySensorMask, boolean single, String title ){
		boolean [] selectedImages=fittingStrategy.selectedImages();
		double [] diffs= new double [selectedImages.length];
		for (int imgNum=0;imgNum<diffs.length;imgNum++) diffs[imgNum]=0.0;
		for (int imgNum=0;imgNum<selectedImages.length;imgNum++) if (selectedImages[imgNum]) {
			diffs[imgNum] =showCompareDerivatives(imgNum, d_derivative, d_delta, applySensorMask, title+"-"+imgNum);
			if (single) break;
		}
		return diffs;
	}

	/**
	 * 
	 * @param delta if 0 - actual derivatives, >0 - approximate derivatives by deltas
	 * @return for each v,u - values and derivatives
	 */
	public double [][][][] calcGridOnSensor( double delta) {
    	int gridHeight=patternParameters.gridGeometry.length;
    	int gridWidth=patternParameters.gridGeometry[0].length;
    	this.gridOnSensor=new double[gridHeight][gridWidth][2][15];
    	double [][] node;
 //   	double [][][] nodes=new double [15][][];
    	boolean dMode=delta>0;
        if (this.debugLevel>2){
        	System.out.println("calcGridOnSensor()");
        	System.out.println("this.lensDistortionParameters.distance="+IJ.d2s(this.lensDistortionParameters.distance, 3));
        	System.out.println("this.lensDistortionParameters.x0="+      IJ.d2s(this.lensDistortionParameters.x0, 3));
        	System.out.println("this.lensDistortionParameters.y0="+      IJ.d2s(this.lensDistortionParameters.y0, 3));
        	System.out.println("this.lensDistortionParameters.z0="+      IJ.d2s(this.lensDistortionParameters.z0, 3));
        	System.out.println("this.lensDistortionParameters.pitch="+   IJ.d2s(this.lensDistortionParameters.pitch, 3));
        	System.out.println("this.lensDistortionParameters.yaw="+IJ.d2s(this.lensDistortionParameters.yaw, 3));
        	System.out.println("this.lensDistortionParameters.roll="+IJ.d2s(this.lensDistortionParameters.roll, 3));
        	System.out.println("this.lensDistortionParameters.focalLength="+IJ.d2s(this.lensDistortionParameters.focalLength, 3));
        	System.out.println("this.lensDistortionParameters.px0="+IJ.d2s(this.lensDistortionParameters.px0, 3));
        	System.out.println("this.lensDistortionParameters.py0="+IJ.d2s(this.lensDistortionParameters.py0, 3));
        	System.out.println("this.lensDistortionParameters.distortionA8="+IJ.d2s(this.lensDistortionParameters.distortionA8, 5));
        	System.out.println("this.lensDistortionParameters.distortionA7="+IJ.d2s(this.lensDistortionParameters.distortionA7, 5));
        	System.out.println("this.lensDistortionParameters.distortionA6="+IJ.d2s(this.lensDistortionParameters.distortionA6, 5));
        	System.out.println("this.lensDistortionParameters.distortionA5="+IJ.d2s(this.lensDistortionParameters.distortionA5, 5));
        	System.out.println("this.lensDistortionParameters.distortionA="+IJ.d2s(this.lensDistortionParameters.distortionA, 5));
        	System.out.println("this.lensDistortionParameters.distortionB="+IJ.d2s(this.lensDistortionParameters.distortionB, 5));
        	System.out.println("this.lensDistortionParameters.distortionC="+IJ.d2s(this.lensDistortionParameters.distortionC, 5));
        }
        LensDistortionParameters ldp=this.lensDistortionParameters.clone();
//		public void setLensDistortionParameters(LensDistortionParameters ldp
       
        for (int v=0; v<gridHeight; v++) for (int u=0; u<gridWidth; u++) if (patternParameters.gridGeometry[v][u][3]>0) {
        	this.lensDistortionParameters.setLensDistortionParameters(ldp); // restore
        	node=this.lensDistortionParameters.calcPartialDerivatives(
        			patternParameters.gridGeometry[v][u][0],//double xp, // target point horizontal, positive - right,  mm
        			patternParameters.gridGeometry[v][u][1],//double yp, // target point vertical,   positive - down,  mm
        			patternParameters.gridGeometry[v][u][2],//double zp, // target point horizontal, positive - away from camera,  mm
        			!dMode);//boolean calculateAll){ // calculate derivatives, false - values only
        	if (this.debugLevel>3) {
        		System.out.println("calcPartialDerivatives("+
        				IJ.d2s(patternParameters.gridGeometry[v][u][0],2)+","+
        				IJ.d2s(patternParameters.gridGeometry[v][u][1],2)+","+
        				IJ.d2s(patternParameters.gridGeometry[v][u][2],2)+" ("+true+") -> "+
        				IJ.d2s(node[0][0],2)+"/"+IJ.d2s(node[0][1],2));
        	}
        	if (dMode) {
//        		double []pXY=node[0]; // px,py values 
        		this.gridOnSensor[v][u][0][0]=node[0][0]; 
        		this.gridOnSensor[v][u][1][0]=node[0][1];
        		for (int j=1;j<15;j++) {  // was 14
        			this.lensDistortionParameters.setLensDistortionParameters(ldp, j, delta); // set one of the parameters (j) with added delta to ldp
                	node=this.lensDistortionParameters.calcPartialDerivatives(
                			patternParameters.gridGeometry[v][u][0],//double xp, // target point horizontal, positive - right,  mm
                			patternParameters.gridGeometry[v][u][1],//double yp, // target point vertical,   positive - down,  mm
                			patternParameters.gridGeometry[v][u][2],//double zp, // target point horizontal, positive - away from camera,  mm
                			false);
            		this.gridOnSensor[v][u][0][j]=(node[0][0]-this.gridOnSensor[v][u][0][0])/delta; 
            		this.gridOnSensor[v][u][1][j]=(node[0][1]-this.gridOnSensor[v][u][1][0])/delta;
        		}

        	} else for (int i=0;i<2;i++) for (int j=0;j<15;j++){ // was 14
        		this.gridOnSensor[v][u][i][j]=node[j][i]; 
        	}

        } else {
        	this.gridOnSensor[v][u]=null;
        }
        return this.gridOnSensor;
    }
    public int getGridWidth() {
    	return patternParameters.gridGeometry[0].length;
    }
    public int getGridHeight() {
    	return patternParameters.gridGeometry.length;
    }
    /**
     * Calculate/set  this.lensDistortionParameters and this.interParameterDerivatives
     * UPDATE - Modifies lensDistortionParameters, not "this" formulti-threaded 
     * @param parVect 21-element vector for eyesis sub-camera, including common and individual parameters
     * @param mask -mask - which partial derivatives are needed to be calculated (others will be null)
     * @param calculateDerivatives calculate array of partial derivatives, if false - just the values
     */
    public void calcInterParamers(
    		LensDistortionParameters lensDistortionParameters,
            double [][] interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
    		double [] parVect,
    		boolean [] mask // calculate only selected derivatives (all parVect values are still
//    		boolean calculateDerivatives // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
    		){
    	boolean calculateDerivatives=(interParameterDerivatives!=null);  // calculate this.interParameterDerivatives -derivatives array (false - just this.values) 
    	 // change meaning of goniometerHorizontal (tripod vertical) and goniometerAxial (tripod horizontal) 
    	boolean isTripod=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.isTripod;
    	double azimuth=parVect[0];
    	double radius= parVect[1];
    	double height= parVect[2];
    	double phi=    parVect[3];
    	double theta=  parVect[4];
    	double psi=    parVect[5];
    	double goniometerHorizontal=parVect[6];
    	double goniometerAxial=parVect[7];
    	double interAxisDistance=parVect[8];
    	double interAxisAngle=parVect[9];
    	double horAxisErrPhi=parVect[10];
    	double horAxisErrPsi=parVect[11];
    	double entrancePupilForward=parVect[12];
    	double centerAboveHorizontal=parVect[13];
    	double GXYZ0=parVect[14];
    	double GXYZ1=parVect[15];
    	double GXYZ2=parVect[16];
    	
    	double cPS=   Math.cos(psi*Math.PI/180); // subCam.psi
    	double sPS=   Math.sin(psi*Math.PI/180); // subCam.psi
    	double cTH=   Math.cos(theta*Math.PI/180); // subCam.theta
    	double sTH=   Math.sin(theta*Math.PI/180); // subCam.theta
    	double cAZP=  Math.cos((azimuth+phi)*Math.PI/180); //subCam.azimuth+subCam.phi
    	double sAZP=  Math.sin((azimuth+phi)*Math.PI/180); //subCam.azimuth+subCam.phi
    	double cAZ=   Math.cos(azimuth*Math.PI/180); //subCam.azimuth
    	double sAZ=   Math.sin(azimuth*Math.PI/180); //subCam.azimuth
    	double cGA=   Math.cos(goniometerAxial*Math.PI/180); //eyesisCameraParameters.goniometerAxial
    	double sGA=   Math.sin(goniometerAxial*Math.PI/180); //eyesisCameraParameters.goniometerAxial
    	double cGH=   Math.cos(goniometerHorizontal*Math.PI/180); //eyesisCameraParameters.goniometerHorizontal
    	double sGH=   Math.sin(goniometerHorizontal*Math.PI/180); //eyesisCameraParameters.goniometerHorizontal
    	double cGIAA= Math.cos(interAxisAngle*Math.PI/180); // eyesisCameraParameters.interAxisAngle
    	double sGIAA= Math.sin(interAxisAngle*Math.PI/180); //eyesisCameraParameters.interAxisAngle
    	double cHAEPH=Math.cos(horAxisErrPhi*Math.PI/180); //eyesisCameraParameters.horAxisErrPhi
    	double sHAEPH=Math.sin(horAxisErrPhi*Math.PI/180); //eyesisCameraParameters.horAxisErrPhi
    	double cHAEPS=Math.cos(horAxisErrPsi*Math.PI/180); //eyesisCameraParameters.horAxisErrPsi
    	double sHAEPS=Math.sin(horAxisErrPsi*Math.PI/180); //eyesisCameraParameters.horAxisErrPsi\
    	
/*
 0) Translate by distance to entrance pupil (lens center)
    	| Xc0 |   | 0                     |   |Xc|
    	| Yc0 | = | 0                     | + |Yc|
    	| Zc0 |   | entrancePupilForward  |   |Zc|
    	*/
    	double [][] aT0={{0.0},{0.0},{entrancePupilForward}};
    	Matrix T0=new Matrix(aT0);
    	
/*
Converting from the sub-camera coordinates to the target coordinates
1) rotate by -psi around CZ: Vc1= R1*Vc
| Xc1 |   | cos(psi)  sin(psi)    0         |   |Xc0|
| Yc1 | = |-sin(psi)  cos(psi)    0         | * |Yc0|
| Zc1 |   |    0         0        1         |   |Zc0|
*/
    	double [][] aR1={{cPS,sPS,0.0},{-sPS,cPS,0.0},{0.0,0.0,1.0}};
    	Matrix R1=new Matrix(aR1);
/*    	
2) rotate by - theta around C1X:Vc2= R2*Vc1
| Xc2 |   |    1         0         0        |   |Xc1|
| Yc2 | = |    0    cos(theta)   sin(theta) | * |Yc1|
| Zc2 |   |    0   -sin(theta)   cos(theta) |   |Zc1|
*/
    	double [][] aR2={{1.0,0.0,0.0},{0.0,cTH,sTH},{0.0,-sTH,cTH}};
    	Matrix R2=new Matrix(aR2);
/*    	
3) rotate by -(azimuth+phi) around C2Y:Vc3= R3*Vc2
| Xc3 |   | cos(azimuth+phi)    0   sin(azimuth+phi)   |   |Xc2|
| Yc3 | = |     0               1         0            | * |Yc2|
| Zc3 |   | -sin(azimuth+phi)   0   cos(azimuth+phi)   |   |Zc2|
*/
    	double [][] aR3={{cAZP,0.0,sAZP},{0.0,1.0,0.0},{-sAZP,0.0,cAZP}};
    	Matrix R3=new Matrix(aR3);
/*    	
4) Now axes are aligned, just translate to get to eyesis coordinates: Vey= T1+Vc3
| Xey |   |      r * sin (azimuth)       |   |Xc3|
| Yey | = | height+centerAboveHorizontal | + |Yc3|
| Zey |   |      r * cos (azimuth)       |   |Zc3|
*/
    	double [][] aT1={{radius*sAZ},{(height+centerAboveHorizontal)},{radius*cAZ}}; // {{subCam.radius*sAZ},{subCam.height},{subCam.radius*cAZ}};
    	Matrix T1=new Matrix(aT1);
    	
/**
5) rotate around moving goniometer axis, by (-goniometerAxial) - same as around EY: Vgm1=R4*Vey
| Xgm1 |   |    1           0                     0              |   |Xey|
| Ygm1 | = |    0   cos(goniometerAxial)  sin(goniometerAxial)   | * |Yey|
| Zgm1 |   |    0   -sin(goniometerAxial) cos(goniometerAxial)   |   |Zey|
*/
    	double [][] aR4_tripod=    {{1.0,0.0,0.0},{0.0,cGA,sGA},{0.0,-sGA,cGA}};
/*    	
5) rotate around moving goniometer axis, by (-goniometerAxial) - same as around EY: Vgm1=R4*Vey
| Xgm1 |   | cos(goniometerAxial)    0   sin(goniometerAxial)   |   |Xey|
| Ygm1 | = |        0                1             0            | * |Yey|
| Zgm1 |   |-sin(goniometerAxial)    0   cos(goniometerAxial)   |   |Zey|
*/
		double [][] aR4_goniometer={{cGA,0.0,sGA},{0.0,1.0,0.0},{-sGA,0.0,cGA}};
		Matrix R4=new Matrix(isTripod?aR4_tripod:aR4_goniometer);
/*    	
6) move to the goniometer horizontal axis:Vgm2=T2+Vgm1
| Xgm2 |   |                 0  |   |Xgm1|
| Ygm2 | = |                 0  | + |Ygm1|
| Zgm2 |   | interAxisDistance  |   |Zgm1|
*/
    	double [][] aT2={{0.0},{0.0},{interAxisDistance}}; //eyesisCameraParameters.interAxisDistance
    	Matrix T2=new Matrix(aT2);
/*    	
7) rotate around Zgm2 by -interAxisAngle, so Xgm3 axis is the same as horizontal goniometer axis: Vgm3=R5*Vgm2
| Xgm3 |   | cos(interAxisAngle)  sin(interAxisAngle)    0         |   |Xgm2|
| Ygm3 | = |-sin(interAxisAngle)  cos(interAxisAngle)    0         | * |Ygm2|
| Zgm3 |   |    0                         0              1         |   |Zgm2|
*/
    	double [][] aR5={{cGIAA,sGIAA,0.0},{-sGIAA,cGIAA,0.0},{0.0,0.0,1.0}};
    	Matrix R5=new Matrix(aR5);
/**
8)  rotate around goniometer horizontal axis (Xgm3) by -goniometerHorizontal: Vgm4=R6*Vgm3
| Xgm4 |   |    cos(goniometerHorizontal)  0   sin(goniometerHorizontal) |   |Xgm3|
| Ygm4 | = |               0               1               0             | * |Ygm3|
| Zgm4 |   |   -sin(goniometerHorizontal)  0   cos(goniometerHorizontal) |   |Zgm3|
*/
    	double [][] aR6_tripod=    {{cGH,0.0,sGH},{0.0,1.0,0.0},{-sGH,0.0,cGH}};
/*    	
8)  rotate around goniometer horizontal axis (Xgm3) by -goniometerHorizontal: Vgm4=R6*Vgm3
| Xgm4 |   |    1                 0                           0            |   |Xgm3|
| Ygm4 | = |    0    cos(goniometerHorizontal)   sin(goniometerHorizontal) | * |Ygm3|
| Zgm4 |   |    0   -sin(goniometerHorizontal)   cos(goniometerHorizontal) |   |Zgm3|
*/
    	double [][] aR6_goniometer={{1.0,0.0,0.0},{0.0,cGH,sGH},{0.0,-sGH,cGH}};
		Matrix R6=new Matrix(isTripod?aR6_tripod:aR6_goniometer);
/*    	
9) Correct roll error of the goniometer horizontal axis - rotate by -horAxisErrPsi around Zgm4: Vgm5=R7*Vgm4
| Xgm5 |   | cos(horAxisErrPsi)  sin(horAxisErrPsi)    0         |   |Xgm4|
| Ygm5 | = |-sin(horAxisErrPsi)  cos(horAxisErrPsi)    0         | * |Ygm4|
| Zgm5 |   |         0                   0             1         |   |Zgm4|
*/
    	double [][] aR7={{cHAEPS,sHAEPS,0.0},{-sHAEPS,cHAEPS,0.0},{0.0,0.0,1.0}};
    	Matrix R7=new Matrix(aR7);
/*    	
10) Correct azimuth error of the goniometer hoirizontal axis - rotate by -horAxisErrPhi around Ygm5: Vgm6=R8*Vgm5
 
| Xgm6 |   | cos(horAxisErrPhi)      0   sin(horAxisErrPhi)   |   |Xgm5|
| Ygm6 | = |        0                1             0          | * |Ygm5|
| Zgm6 |   |-sin(horAxisErrPhi)      0   cos(horAxisErrPhi)   |   |Zgm5|
For Tripod - rotate around X-axis (like theta)
| Xgm6 |   |   1           0                       0      |   |Xgm5|
| Ygm6 | = |   0  cos(horAxisErrPhi)  sin(horAxisErrPhi)  | * |Ygm5|
| Zgm6 |   |   0 -sin(horAxisErrPhi)  cos(horAxisErrPhi)  |   |Zgm5|

*/
    	double [][] aR8_tripod=    {{1.0,   0.0,   0.0},{0.0,cHAEPH,sHAEPH},{0.0,  -sHAEPH,cHAEPH}};
    	double [][] aR8_goniometer={{cHAEPH,0.0,sHAEPH},{0.0,   1.0,   0.0},{-sHAEPH,  0.0,cHAEPH}};
    	Matrix R8=new Matrix(isTripod?aR8_tripod:aR8_goniometer);
/*    	
11) translate to the target zero point: Vt=  T3+Vgm6
| Xt |   | GXYZ[0]  |   |Xgm6|
| Yt | = |-GXYZ[1]  | + |Ygm6| // Y - up positive
| Zt |   |-GXYZ[2]  |   |Zgm6| // Z - away positive
    	 */
//    	double [][] aT3={{parVect[12]},{-parVect[13]},{-parVect[14]}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
   	
    	double [][] aT3={{GXYZ0},{-GXYZ1},{-GXYZ2}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
//    	double [][] aT3={{parVect[12]},{ parVect[13]},{-parVect[14]}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
    	Matrix T3=new Matrix(aT3);

// MA=R8*R7*R6*R5*R4*R3*R2*R1;
// MB=T3+(R8*R7*R6*R5*(T2+R4*T1)); - old
// MB=T3+(R8*R7*R6*R5*(T2+R4*(T1+R3*R2*R1*T0)));
        Matrix MA=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
//        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1)))))));
        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1.plus(R3.times(R2.times(R1.times(T0)))) )))))));
		if (this.debugLevel>2) {
			System.out.println("MA:");
			MA.print(10, 5);
			System.out.println("MB:");
			MB.print(10, 5);
			System.out.println("T3:");
			T3.print(10, 5);
//			System.out.println("interParameterDerivatives[0]="+sprintfArray(interParameterDerivatives[0]));
		}
        double [] extrinsicParams=parametersFromMAMB(MA,MB); // all after 6 are 0;
        lensDistortionParameters.distance=extrinsicParams[2];
        lensDistortionParameters.x0=extrinsicParams[0];
        lensDistortionParameters.y0=extrinsicParams[1];
        lensDistortionParameters.z0=0.0; // used here
        lensDistortionParameters.pitch=extrinsicParams[4];
        lensDistortionParameters.yaw=  extrinsicParams[3];
        lensDistortionParameters.roll= extrinsicParams[5];
//		lensDistortionParameters.focalLength=parVect[15]; //subCam.focalLength;
//		lensDistortionParameters.px0=parVect[16]; //subCam.px0;
//		lensDistortionParameters.py0=parVect[17]; //subCam.py0;
//		lensDistortionParameters.distortionA5=parVect[18]; //subCam.distortion5;
//		lensDistortionParameters.distortionA=parVect[19]; //subCam.distortionA;
//		lensDistortionParameters.distortionB=parVect[20]; //subCam.distortionB;
//		lensDistortionParameters.distortionC=parVect[21]; //subCam.distortionC;

		lensDistortionParameters.focalLength=parVect[17]; //subCam.focalLength;
		lensDistortionParameters.px0=parVect[18]; //subCam.px0;
		lensDistortionParameters.py0=parVect[19]; //subCam.py0;
		lensDistortionParameters.distortionA8=parVect[20]; //subCam.distortion5;
		lensDistortionParameters.distortionA7=parVect[21]; //subCam.distortion5;
		lensDistortionParameters.distortionA6=parVect[22]; //subCam.distortion5;
		lensDistortionParameters.distortionA5=parVect[23]; //subCam.distortion5;
		lensDistortionParameters.distortionA=parVect[24]; //subCam.distortionA;
		lensDistortionParameters.distortionB=parVect[25]; //subCam.distortionB;
		lensDistortionParameters.distortionC=parVect[26]; //subCam.distortionC;

		
		lensDistortionParameters.recalcCommons();
        
        
        if (this.debugLevel>2){
        	System.out.println("lensDistortionParameters.recalcCommons()");
        	System.out.println("lensDistortionParameters.distance="+IJ.d2s(lensDistortionParameters.distance, 3));
        	System.out.println("lensDistortionParameters.x0="+      IJ.d2s(lensDistortionParameters.x0, 3));
        	System.out.println("lensDistortionParameters.y0="+      IJ.d2s(lensDistortionParameters.y0, 3));
        	System.out.println("lensDistortionParameters.z0="+      IJ.d2s(lensDistortionParameters.z0, 3));
        	System.out.println("lensDistortionParameters.pitch="+   IJ.d2s(lensDistortionParameters.pitch, 3));
        	System.out.println("lensDistortionParameters.yaw="+IJ.d2s(lensDistortionParameters.yaw, 3));
        	System.out.println("lensDistortionParameters.roll="+IJ.d2s(lensDistortionParameters.roll, 3));
        	System.out.println("lensDistortionParameters.focalLength="+IJ.d2s(lensDistortionParameters.focalLength, 3));
        	System.out.println("lensDistortionParameters.px0="+IJ.d2s(lensDistortionParameters.px0, 3));
        	System.out.println("lensDistortionParameters.py0="+IJ.d2s(lensDistortionParameters.py0, 3));
        	System.out.println("lensDistortionParameters.distortionA8="+IJ.d2s(lensDistortionParameters.distortionA8, 4));
        	System.out.println("lensDistortionParameters.distortionA7="+IJ.d2s(lensDistortionParameters.distortionA7, 4));
        	System.out.println("lensDistortionParameters.distortionA6="+IJ.d2s(lensDistortionParameters.distortionA6, 4));
        	System.out.println("lensDistortionParameters.distortionA5="+IJ.d2s(lensDistortionParameters.distortionA5, 4));
        	System.out.println("lensDistortionParameters.distortionA="+IJ.d2s(lensDistortionParameters.distortionA, 4));
        	System.out.println("lensDistortionParameters.distortionB="+IJ.d2s(lensDistortionParameters.distortionB, 4));
        	System.out.println("lensDistortionParameters.distortionC="+IJ.d2s(lensDistortionParameters.distortionC, 4));
        }
        if (!calculateDerivatives) return; 
/* Calculate all derivativs as a matrix.
 *  Input parameters (columns):
0    	public double azimuth; // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
1    	public double radius;  // mm, distance from the rotation axis
2   	public double height;       // mm, up (was downwards?) - from the origin point
3   	public double phi;     // degrees, optical axis from azimuth/r vector, clockwise
4   	public double theta;   // degrees, optical axis from the eyesis horizon, positive - up
5   	public double psi;     // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
6   	public double goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
7   	public double goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
8   	public double interAxisDistance; // distance in mm between two goniometer axes
9    	public double interAxisAngle;    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
10   	public double horAxisErrPhi;   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
11   	public double horAxisErrPsi;   // angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
Two new parameters
12    	public double entrancePupilForward; // common to all lenses - distance from the sensor to the lens entrance pupil
13    	public double centerAboveHorizontal; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to 
14(12)  x	public double [] GXYZ=null; // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
15(13)  y
16(14)  z
17(15)	public double focalLength=4.5;
18(16)	public double px0=1296.0;          // center of the lens on the sensor, pixels
19(17)		public double py0=968.0;           // center of the lens on the sensor, pixels
20(18)		public double distortionA8=0.0; // r^5 (normalized to focal length or to sensor half width?)
21(19)		public double distortionA7=0.0; // r^5 (normalized to focal length or to sensor half width?)
22(20)		public double distortionA6=0.0; // r^5 (normalized to focal length or to sensor half width?)
23(21)		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
24(22)		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
25(23)		public double distortionB=0.0; // r^3
26(24)		public double distortionC=0.0; // r^2

 * Output parameters (rows):
0		public double x0=0;      // lens axis from pattern center, mm (to the right)
1		public double y0=0;      // lens axis from pattern center, mm (down)
2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
6		public double focalLength=4.5;
7		public double px0=1296.0;           // center of the lens on the sensor, pixels
8		public double py0=968.0;           // center of the lens on the sensor, pixels
 		public double distortionRadius=  2.8512; // mm - half width of the sensor
9		public double distortionA8=0.0; // r^8 (normalized to focal length or to sensor half width?)
10		public double distortionA7=0.0; // r^7 (normalized to focal length or to sensor half width?)
11		public double distortionA6=0.0; // r^6 (normalized to focal length or to sensor half width?)
12		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
13		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
14		public double distortionB=0.0; // r^3
15		public double distortionC=0.0; // r^2
 */
//        interParameterDerivatives=new double[getNumInputs()][]; //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21)
        
//calculate dMA_azimuth        
//calculate dMB_azimuth        
/*
// MA=R8*R7*R6*R5*R4*R3*R2*R1;
// MB=T3+(R8*R7*R6*R5*(T2+R4*T1)); - old
// MB=T3+(R8*R7*R6*R5*(T2+R4*(T1+R3*R2*R1*T0)));
        Matrix MA=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1)))))));  old
        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1.plus(R3.times(R2.times(R1.times(T0)))) )))))));
3) rotate by -(azimuth+phi) around C2Y:Vc3= R3*Vc2
| Xc3 |   | cos(azimuth+phi)    0   sin(azimuth+phi)   |   |Xc2|
| Yc3 | = |     0               1         0            | * |Yc2|
| Zc3 |   | -sin(azimuth+phi)   0   cos(azimuth+phi)   |   |Zc2|
    	double [][] aR3={{cAZP,0.0,sAZP},{0.0,1.0,0.0},{-sAZP,0.0,cAZP}};
    	Matrix R3=new Matrix(aR3);
4) Now axes are aligned, just translate to get to eyesis coordinates: Vey= T1+Vc3
| Xey |   |  r * sin (azimuth)  |   |Xc3|
| Yey | = |             height  | + |Yc3|
| Zey |   |  r * cos (azimuth)  |   |Zc3|
    	double [][] aT1={{subCam.radius*sAZ},{subCam.height},{subCam.radius*cAZ}};
    	Matrix T1=new Matrix(aT1);

        Matrix 
// Make a function MA, MB - >parameters (column) - reuse it above and for each    interParameterDerivatives row
Matrix dMA_azimuth=R8*R7*R6*R5*R4*dR3_azimuth*R2*R1;
Matrix dMB_azimuth=T3+(R8*R7*R6*R5*(R4*dT1_azimuth));

Use extrinsicParams=parametersFromMAMB(dMA_azimuth,dMB_azimuth);
	       		if (this.debugLevel>3) {
	    			System.out.println("calculateFxAndJacobian->calcPartialDerivatives("+IJ.d2s(targetXYZ[fullIndex][0],2)+","+
	    					IJ.d2s(targetXYZ[fullIndex][1],2)+","+
	    					IJ.d2s(targetXYZ[fullIndex][2],2)+" ("+calcJacobian+") -> "+
	    					IJ.d2s(derivatives14[0][0],2)+"/"+IJ.d2s(derivatives14[0][1],2));
	    		}

Which parameters affect which matrices
                                               R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 || T0 | T1 | T2 | T3 |
0    	public double azimuth;              //    |    | +  |    |    |    |    |    ||    | +  |    |    |
1    	public double radius;               //    |    |    |    |    |    |    |    ||    | +  |    |    |
2   	public double height;               //    |    |    |    |    |    |    |    ||    | +  |    |    |
3   	public double phi;                  //    |    | +  |    |    |    |    |    ||    |    |    |    |
4   	public double theta;                //    | +  |    |    |    |    |    |    ||    |    |    |    |
5   	public double psi;                  // +  |    |    |    |    |    |    |    ||    |    |    |    |
6   	public double goniometerHorizontal; //    |    |    |    |    | +  |    |    ||    |    |    |    |
7   	public double goniometerAxial;      //    |    |    | +  |    |    |    |    ||    |    |    |    |
8   	public double interAxisDistance;    //    |    |    |    |    |    |    |    ||    |    | +  |    |
9    	public double interAxisAngle;       //    |    |    |    | +  |    |    |    ||    |    |    |    |
10   	public double horAxisErrPhi;        //    |    |    |    |    |    |    | +  ||    |    |    |    |
11   	public double horAxisErrPsi;        //    |    |    |    |    |    | +  |    ||    |    |    |    |
12      public double entrancePupilForward  //    |    |    |    |    |    |    |    || +  |    |    |    |
13      public double centerAboveHorizontal //    |    |    |    |    |    |    |    ||    | +  |    |    |
14  x	public double [] GXYZ=null;         //    |    |    |    |    |    |    |    ||    |    |    | +  |
15  y                                       //    |    |    |    |    |    |    |    ||    |    |    | +  |
16  z                                       //    |    |    |    |    |    |    |    ||    |    |    | +  |

 */
// Below can be optimized with common intermediate results
   		if (this.debugLevel>2) {
   			for (int i=0;i<parVect.length;i++){
   				System.out.println("calcInterParamers(): parVect["+i+"]="+parVect[i]);
   			}
   		}
//0    	public double azimuth; // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
        if (mask[0]) {
        	double [][] adR3_azimuth={{-sAZP,0.0,cAZP},{0.0,0.0,0.0},{-cAZP,0.0,-sAZP}};
        	Matrix dR3_azimuth=new Matrix(adR3_azimuth);
//        	double [][] adT1_azimuth={{radius*cAZ},{height},{-radius*sAZ}}; //{{subCam.radius*cAZ},{subCam.height},{-subCam.radius*sAZ}}
        	double [][] adT1_azimuth={{radius*cAZ},{0.0},{-radius*sAZ}}; //{{subCam.radius*cAZ},{subCam.height},{-subCam.radius*sAZ}}
        	Matrix dT1_azimuth=new Matrix(adT1_azimuth);
        	
        	Matrix dMA_azimuth=R8.times(R7.times(R6.times(R5.times(R4.times(dR3_azimuth.times(R2.times(R1)))))));
        	Matrix dMB0_azimuth=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_azimuth)))));
        	Matrix dMB_azimuth=dMB0_azimuth.plus(dMA_azimuth.times(T0)); // new term
        	interParameterDerivatives[0]=d_parametersFromMAMB(dMA_azimuth,dMB_azimuth,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_azimuth:");
    			dMA_azimuth.print(10, 5);
    			System.out.println("dMB_azimuth:");
    			dMB_azimuth.print(10, 5);
    			System.out.println("interParameterDerivatives[0]="+sprintfArray(interParameterDerivatives[0]));
    		}
        	
        } else interParameterDerivatives[0]=null;
//1    	public double radius;  // mm, distance from the rotation axis
        if (mask[1]) {
        	double [][] adT1_radius={{sAZ},{0.0},{cAZ}}; //{{subCam.radius*sAZ},{0.0},{subCam.radius*cAZ}}
        	Matrix dT1_radius=new Matrix(adT1_radius);
        	Matrix dMA_radius=new Matrix(3,3,0.0);
        	Matrix dMB_radius=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_radius)))));
        	interParameterDerivatives[1]=d_parametersFromMAMB(dMA_radius,dMB_radius,MA,MB,false); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_radius:");
    			dMA_radius.print(10, 5);
    			System.out.println("dMB_radius:");
    			dMB_radius.print(10, 5);
    			System.out.println("interParameterDerivatives[1]="+sprintfArray(interParameterDerivatives[1]));
    		}
        } else interParameterDerivatives[1]=null; 
//2   	public double height;       // mm, downwards - from the origin point
        if (mask[2]) {
        	double [][] adT1_height={{0.0},{1.0},{0.0}};
        	Matrix dT1_height=new Matrix(adT1_height);
        	Matrix dMA_height=new Matrix(3,3,0.0);
        	Matrix dMB_height=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_height)))));
        	interParameterDerivatives[2]=d_parametersFromMAMB(dMA_height,dMB_height,MA,MB,false); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_height:");
    			dMA_height.print(10, 5);
    			System.out.println("dMB_height:");
    			dMB_height.print(10, 5);
    			System.out.println("interParameterDerivatives[2]="+sprintfArray(interParameterDerivatives[2]));
    		}
        } else interParameterDerivatives[2]=null; 
//3   	public double phi;     // degrees, optical axis from azimuth/r vector, clockwise
        if (mask[3]) {
        	double [][] adR3_phi={{-sAZP,0.0,cAZP},{0.0,0.0,0.0},{-cAZP,0.0,-sAZP}}; // same as adR3_azimuth
        	Matrix dR3_phi=new Matrix(adR3_phi); // same as dR3_azimuth
        	Matrix dMA_phi=R8.times(R7.times(R6.times(R5.times(R4.times(dR3_phi.times(R2.times(R1))))))); //same as dMA_azimuth
//        	Matrix dMB_phi=new Matrix(3,1,0.0);
        	Matrix dMB_phi=dMA_phi.times(T0); // new term
        	interParameterDerivatives[3]=d_parametersFromMAMB(dMA_phi,dMB_phi,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_phi:");
    			dMA_phi.print(10, 5);
    			System.out.println("dMB_phi:");
    			dMB_phi.print(10, 5);
    			System.out.println("interParameterDerivatives[3]="+sprintfArray(interParameterDerivatives[3]));
    		}
        } else interParameterDerivatives[3]=null; 
//4   	public double theta;   // degrees, optical axis from the eyesis horizon, positive - up
        if (mask[4]) {
        	double [][] adR2_theta={{0.0,0.0,0.0},{0.0,-sTH,cTH},{0.0,-cTH,-sTH}};
        	Matrix dR2_theta=new Matrix(adR2_theta);
        	Matrix dMA_theta=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(dR2_theta.times(R1)))))));
//        	Matrix dMB_theta=new Matrix(3,1,0.0);
        	Matrix dMB_theta=dMA_theta.times(T0); // new term
        	interParameterDerivatives[4]=d_parametersFromMAMB(dMA_theta,dMB_theta,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_theta:");
    			dMA_theta.print(10, 5);
    			System.out.println("dMB_theta:");
    			dMB_theta.print(10, 5);
    			System.out.println("interParameterDerivatives[4]="+sprintfArray(interParameterDerivatives[4]));
    		}
        } else interParameterDerivatives[4]=null; 
//5   	public double psi;     // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
        if (mask[5]) {
        	double [][] adR1_psi={{-sPS,cPS,0.0},{-cPS,-sPS,0.0},{0.0,0.0,0.0}};
        	Matrix dR1_psi=new Matrix(adR1_psi);
        	Matrix dMA_psi=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(dR1_psi)))))));
//        	Matrix dMB_psi=new Matrix(3,1,0.0);
        	Matrix dMB_psi=dMA_psi.times(T0); // new term
        	interParameterDerivatives[5]=d_parametersFromMAMB(dMA_psi,dMB_psi,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
/*    			
    			System.out.print("R1:");
    			R1.print(10, 5);
    			System.out.print("dR1_psi:");
    			dR1_psi.print(10, 5);
    			Matrix R82_psi=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2))))));
    			System.out.print("R82_psi:");
    			R82_psi.print(10, 5);
    			*/
    			System.out.print("dMA_psi:");
    			dMA_psi.print(10, 5);
    			System.out.print("dMB_psi:");
    			dMB_psi.print(10, 5);
    			System.out.println("interParameterDerivatives[5]="+sprintfArray(interParameterDerivatives[5]));
    		}
        } else interParameterDerivatives[5]=null; 
//6   	public double goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
        if (mask[6]) {
/* define for isTripod */
        	double [][] adR6_goniometerHorizontal_tripod=    {{-sGH,0.0,cGH},{0.0, 0.0,0.0},{-cGH, 0.0,-sGH}};
        	double [][] adR6_goniometerHorizontal_goniometer={{ 0.0,0.0,0.0},{0.0,-sGH,cGH},{ 0.0,-cGH,-sGH}};
        	Matrix dR6_goniometerHorizontal=new Matrix(isTripod?adR6_goniometerHorizontal_tripod:adR6_goniometerHorizontal_goniometer);
        	Matrix dMA_goniometerHorizontal=R8.times(R7.times(dR6_goniometerHorizontal.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
        	Matrix dMB_goniometerHorizontal=R8.times(R7.times(dR6_goniometerHorizontal.times(R5.times(T2.plus(R4.times(T1))))));
        	interParameterDerivatives[6]=d_parametersFromMAMB(dMA_goniometerHorizontal,dMB_goniometerHorizontal,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_goniometerHorizontal:");
    			dMA_goniometerHorizontal.print(10, 5);
    			System.out.println("dMB_goniometerHorizontal:");
    			dMB_goniometerHorizontal.print(10, 5);
    			System.out.println("interParameterDerivatives[6]="+sprintfArray(interParameterDerivatives[6]));
    		}
        } else interParameterDerivatives[6]=null; 
//7   	public double goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
        if (mask[7]) {
// define for isTripod
        	double [][] adR4_goniometerAxial_tripod=    {{ 0.0,0.0,0.0},{0.0,-sGA,cGA},{ 0.0,-cGA,-sGA}};
        	double [][] adR4_goniometerAxial_goniometer={{-sGA,0.0,cGA},{0.0, 0.0,0.0},{-cGA, 0.0,-sGA}};
        	Matrix dR4_goniometerAxial=new Matrix(isTripod?adR4_goniometerAxial_tripod:adR4_goniometerAxial_goniometer);
        	Matrix dMA_goniometerAxial=R8.times(R7.times(R6.times(R5.times(dR4_goniometerAxial.times(R3.times(R2.times(R1)))))));
        	Matrix dMB_goniometerAxial=R8.times(R7.times(R6.times(R5.times(dR4_goniometerAxial.times(T1)))));
        	interParameterDerivatives[7]=d_parametersFromMAMB(dMA_goniometerAxial,dMB_goniometerAxial,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_goniometerAxial:");
    			dMA_goniometerAxial.print(10, 5);
    			System.out.println("dMB_goniometerAxial:");
    			dMB_goniometerAxial.print(10, 5);
    			System.out.println("interParameterDerivatives[7]="+sprintfArray(interParameterDerivatives[7]));
    		}
        } else interParameterDerivatives[7]=null; 
//8   	public double interAxisDistance; // distance in mm between two goniometer axes
        if (mask[8]) {
        	double [][] adT2_interAxisDistance={{0.0},{0.0},{1.0}};
        	Matrix dT2_interAxisDistance=new Matrix(adT2_interAxisDistance);
        	Matrix dMA_interAxisDistance=new Matrix(3,3,0.0);
        	Matrix dMB_interAxisDistance=R8.times(R7.times(R6.times(R5.times(dT2_interAxisDistance))));
        	interParameterDerivatives[8]=d_parametersFromMAMB(dMA_interAxisDistance,dMB_interAxisDistance,MA,MB,false); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_interAxisDistance:");
    			dMA_interAxisDistance.print(10, 5);
    			System.out.println("dMB_interAxisDistance:");
    			dMB_interAxisDistance.print(10, 5);
    			System.out.println("interParameterDerivatives[8]="+sprintfArray(interParameterDerivatives[8]));
    		}
        } else interParameterDerivatives[8]=null; 
//9    	public double interAxisAngle;    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
        if (mask[9]) {
        	double [][] adR5_interAxisAngle={{-sGIAA,cGIAA,0.0},{-cGIAA,-sGIAA,0.0},{0.0,0.0,0.0}};
        	Matrix dR5_interAxisAngle=new Matrix(adR5_interAxisAngle);
        	Matrix dMA_interAxisAngle=R8.times(R7.times(R6.times(dR5_interAxisAngle.times(R4.times(R3.times(R2.times(R1)))))));
        	Matrix dMB_interAxisAngle=R8.times(R7.times(R6.times(dR5_interAxisAngle.times(T2.plus(R4.times(T1))))));    	
        	interParameterDerivatives[9]=d_parametersFromMAMB(dMA_interAxisAngle,dMB_interAxisAngle,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_interAxisAngle:");
    			dMA_interAxisAngle.print(10, 5);
    			System.out.println("dMB_interAxisAngle:");
    			dMB_interAxisAngle.print(10, 5);
    			System.out.println("interParameterDerivatives[9]="+sprintfArray(interParameterDerivatives[9]));
    		}
        } else interParameterDerivatives[9]=null; 
//10   	public double horAxisErrPhi;   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
        
// change sHAEPH to rotate like theta /1 0 0 /0 c s/ 0 -s c/ (derivative -/0 0 0 /0 -s c/ 0 -c -s/         
        if (mask[10]) {
        	double [][] adR8_horAxisErrPhi_tripod=    {{0.0,   0.0,    0.0},{0.0,-sHAEPH,cHAEPH},{    0.0,-cHAEPH,-sHAEPH}};
        	double [][] adR8_horAxisErrPhi_goniometer={{-sHAEPH,0.0,cHAEPH},{0.0,    0.0,   0.0},{-cHAEPH,    0.0,-sHAEPH}};
        	Matrix dR8_horAxisErrPhi=new Matrix(isTripod?adR8_horAxisErrPhi_tripod:adR8_horAxisErrPhi_goniometer);
        	Matrix dMA_horAxisErrPhi=dR8_horAxisErrPhi.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
        	Matrix dMB_horAxisErrPhi=dR8_horAxisErrPhi.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1))))));
        	interParameterDerivatives[10]=d_parametersFromMAMB(dMA_horAxisErrPhi,dMB_horAxisErrPhi,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_horAxisErrPhi:");
    			dMA_horAxisErrPhi.print(10, 5);
    			System.out.println("dMB_horAxisErrPhi:");
    			dMB_horAxisErrPhi.print(10, 5);
    			System.out.println("interParameterDerivatives[10]="+sprintfArray(interParameterDerivatives[10]));
    		}
        } else interParameterDerivatives[10]=null; 
//11   	public double horAxisErrPsi;   // angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
        if (mask[11]) {
        	double [][] adR7_horAxisErrPsi={{-sHAEPS,cHAEPS,0.0},{-cHAEPS,-sHAEPS,0.0},{0.0,0.0,0.0}};
        	Matrix dR7_horAxisErrPsi=new Matrix(adR7_horAxisErrPsi);
        	Matrix dMA_horAxisErrPsi=R8.times(dR7_horAxisErrPsi.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
        	Matrix dMB_horAxisErrPsi=R8.times(dR7_horAxisErrPsi.times(R6.times(R5.times(T2.plus(R4.times(T1))))));
        	interParameterDerivatives[11]=d_parametersFromMAMB(dMA_horAxisErrPsi,dMB_horAxisErrPsi,MA,MB,true); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_horAxisErrPsi:");
    			dMA_horAxisErrPsi.print(10, 5);
    			System.out.println("dMB_horAxisErrPsi:");
    			dMB_horAxisErrPsi.print(10, 5);
    			System.out.println("interParameterDerivatives[11]="+sprintfArray(interParameterDerivatives[11]));
    		}
        } else interParameterDerivatives[11]=null; 
//                                          // R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 || T0 | T1 | T2 | T3 |
//12   	public double entrancePupilForward  //    |    |    |    |    |    |    |    || +  |    |    |    |
//13   	public double centerAboveHorizontal //    |    |    |    |    |    |    |    ||    | +  |    |    |

        if (mask[12]) {
        	double [][] adT0_entrancePupilForward={{0.0},{0.0},{1.0}};
        	Matrix dT0_entrancePupilForward=new Matrix(adT0_entrancePupilForward);
        	Matrix dMA_entrancePupilForward=new Matrix(3,3,0.0);
        	Matrix dMB_entrancePupilForward=MA.times(dT0_entrancePupilForward);
        	interParameterDerivatives[12]=d_parametersFromMAMB(dMA_entrancePupilForward,dMB_entrancePupilForward,MA,MB,false); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_centerAboveHorizontal:");
    			dMA_entrancePupilForward.print(10, 5);
    			System.out.println("dMB_centerAboveHorizontal:");
    			dMB_entrancePupilForward.print(10, 5);
    			System.out.println("interParameterDerivatives[12]="+sprintfArray(interParameterDerivatives[12]));
    		}
        } else interParameterDerivatives[12]=null; 
        
        if (mask[13]) {
        	double [][] adT1_centerAboveHorizontal={{0.0},{1.0},{0.0}};
        	Matrix dT1_centerAboveHorizontal=new Matrix(adT1_centerAboveHorizontal);
        	Matrix dMA_centerAboveHorizontal=new Matrix(3,3,0.0);
        	Matrix dMB_centerAboveHorizontal=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_centerAboveHorizontal)))));
        	interParameterDerivatives[13]=d_parametersFromMAMB(dMA_centerAboveHorizontal,dMB_centerAboveHorizontal,MA,MB,false); // all after 6 are 0;
    		if (this.debugLevel>2) {
    			System.out.println("dMA_centerAboveHorizontal:");
    			dMA_centerAboveHorizontal.print(10, 5);
    			System.out.println("dMB_centerAboveHorizontal:");
    			dMB_centerAboveHorizontal.print(10, 5);
    			System.out.println("interParameterDerivatives[13]="+sprintfArray(interParameterDerivatives[13]));
    		}
        } else interParameterDerivatives[13]=null; 
        
        
//14  x	public double [] GXYZ=null; // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
        if (mask[14]) {
        	double [][] adT3_GXYZ0={{1.0},{0.0},{0.0}};
        	Matrix dMA_GXYZ0=new Matrix(3,3,0.0);
        	Matrix dMB_GXYZ0=new Matrix(adT3_GXYZ0);
        	interParameterDerivatives[14]=d_parametersFromMAMB(dMA_GXYZ0,dMB_GXYZ0,MA,MB,false); // all after 6 are 0;
        	if (this.debugLevel>2) {
        		System.out.println("dMA_GXYZ0:");
        		dMA_GXYZ0.print(10, 5);
        		System.out.println("dMA_GXYZ0:");
        		dMB_GXYZ0.print(10, 5);
        		System.out.println("interParameterDerivatives[14]="+sprintfArray(interParameterDerivatives[14]));
        	}
        } else interParameterDerivatives[14]=null; 
//13  y
        if (mask[15]) {
//    	double [][] adT3_GXYZ1={{0.0},{1.0},{0.0}};
        	double [][] adT3_GXYZ1={{0.0},{-1.0},{0.0}}; // up - positive
//        	double [][] adT3_GXYZ1={{0.0},{ 1.0},{0.0}}; // up - positive
        	Matrix dMA_GXYZ1=new Matrix(3,3,0.0);
        	Matrix dMB_GXYZ1=new Matrix(adT3_GXYZ1);
        	interParameterDerivatives[15]=d_parametersFromMAMB(dMA_GXYZ1,dMB_GXYZ1,MA,MB,false); // all after 6 are 0;
        	if (this.debugLevel>2) {
        		System.out.println("dMA_GXYZ1:");
        		dMA_GXYZ1.print(10, 5);
        		System.out.println("dMB_GXYZ1:");
        		dMB_GXYZ1.print(10, 5);
        		System.out.println("interParameterDerivatives[15]="+sprintfArray(interParameterDerivatives[15]));
        	}
        } else interParameterDerivatives[15]=null; 
//14  z
        if (mask[16]) {
//    	double [][] adT3_GXYZ2={{0.0},{0.0},{1.0}};
        	double [][] adT3_GXYZ2={{0.0},{0.0},{-1.0}}; // away - positive
        	Matrix dMA_GXYZ2=new Matrix(3,3,0.0);
        	Matrix dMB_GXYZ2=new Matrix(adT3_GXYZ2);
        	interParameterDerivatives[16]=d_parametersFromMAMB(dMA_GXYZ2,dMB_GXYZ2,MA,MB,false); // all after 6 are 0;
        	if (this.debugLevel>2) {
        		System.out.println("dMA_GXYZ2:");
        		dMA_GXYZ2.print(10, 5);
        		System.out.println("dMB_GXYZ2:");
        		dMB_GXYZ2.print(10, 5);
        		System.out.println("interParameterDerivatives[16]="+sprintfArray(interParameterDerivatives[16]));
        	}
        } else interParameterDerivatives[16]=null; 
// now fill the rest, unchanged parameters
/*
    int numInputs=22;   //was 21  parameters in subcamera+...
    int numOutputs=13;  //was 12  parameters in a single camera

 */
//17 (15)		public double focalLength=4.5;
//18 (16)		public double px0=1296.0;          // center of the lens on the sensor, pixels
//19 (17)		public double py0=968.0;           // center of the lens on the sensor, pixels
//20 (18)		public double distortionA8=0.0; // r^8 (normalized to focal length or to sensor half width?)
//21 (19)		public double distortionA7=0.0; // r^7 (normalized to focal length or to sensor half width?)
//22 (20)		public double distortionA6=0.0; // r^6 (normalized to focal length or to sensor half width?)
//23 (21)		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
//24 (22)		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
//25 (23)		public double distortionB=0.0; // r^3
//26 (24)		public double distortionC=0.0; // r^2

//        for (int inpPar=15; inpPar<getNumInputs(); inpPar++){
        for (int inpPar=17; inpPar<getNumInputs(); inpPar++){ // OK with A8
        	if (mask[inpPar]){
        		interParameterDerivatives[inpPar]=new double[getNumOutputs()];
        		for (int outPar=0; outPar<getNumOutputs(); outPar++){
        			interParameterDerivatives[inpPar][outPar]=((getNumOutputs()-outPar)==(getNumInputs()-inpPar))?1.0:0.0;
            		if (this.debugLevel>2) {
            			System.out.println("interParameterDerivatives["+inpPar+"]["+outPar+"]="+interParameterDerivatives[inpPar][outPar]);
            		}
        		}
        	} else interParameterDerivatives[inpPar]=null;
        }
    }
 
    
    
    public String sprintfArray(double []arr){
    	String result="";
    	for (int i=0;i<arr.length;i++) result+= ((i>0)?", ":"")+arr[i];
    	return "["+result+"]";
    }
    
     public double [] parametersFromMAMB(Matrix MA, Matrix MB){
    	double [] result= new double [getNumOutputs()]; // only first 6 are used
    	for (int i=6; i<getNumOutputs();i++) result[i]=0.0;
/*
0		public double x0=0;      // lens axis from pattern center, mm (to the right)
1		public double y0=0;      // lens axis from pattern center, mm (down)
2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise

        Vt=MB+MA*Vc
        calculate X0,Y0 (Z0==0), dist, phi, theta, psi from MB and MA
        | MA00 MA01 MA02 |
        | MA10 MA11 MA12 |
        | MA20 MA21 MA22 |
        ===================
        | MB0 |
        | MB1 |
        | MB2 |
        Take point Pc on a sub-camera axis and on the target Zt=0 plane: [0,0,dist]
        MA*Pc+MB= dist*[MA02,MA12,MA22]+MB =[Tx0,Ty0,0]
        MA02*dist+MB0=Tx0
        MA12*dist+MB1=Ty0
        MA22*dist+MB2=0
        dist=-MB2/MA22;
        Tx0=MA02*(-MB2/MA22)+MB0
        Ty0=MA12*(-MB2/MA22)+MB1
        Tx0=MB0-MB2*MA02/MA22
        Ty0=MB1-MB2*MA12/MA22
        */
    	result[2]=-MB.get(2,0)/MA.get(2,2); // distance
    	result[0]= MB.get(0,0)-MB.get(2,0)*MA.get(0,2)/MA.get(2,2); // x0
    	result[1]= MB.get(1,0)-MB.get(2,0)*MA.get(1,2)/MA.get(2,2); // y0
        /*
        // now find phi, theta, psi
        MA - rotation from camera to target, transp(MA) - rotation from target to camera - same as rot(phi, theta,psi)
        MT=transp(MA),
        */
        /*
        MA[1,2]= sin(theta)
        MA[0,2]= cos(theta)*sin(phi)
        MA[2,2]= cos(theta)*cos(phi)
        MA[1,0]=-cos(theta)*sin(psi)
        MA[1,1]= cos(theta)*cos(psi)
        theta=arcsin(MA[1,2])
        phi=  atan2(MA[0,2],MA[2,2])
        psi= -atan2(MA[1,0],MA[1,1])
         */
        result[4]=180.0/Math.PI* Math.asin(MA.get(1, 2));  //pitch
        result[3]=  180.0/Math.PI* Math.atan2(MA.get(0,2),MA.get(2, 2));//yaw
        result[5]=-180.0/Math.PI* Math.atan2(MA.get(1, 0),MA.get(1, 1));//roll
    	return result;
    }
    /**
     * 
     * @param d_MA differential of the rotational matrix MA by some parameter
     * @param d_MB differential of the translational matrix MB by some parameter
     * @param MA - rotation matrix
     * @param MB - translation matrix
     * @param isAngle - when true, the partial derivative is for angles, d_MA, d_MB should be divided by 180/pi
     * @return differentials of the {x0,y0,dist,phi,theta,psi} by that parameter
     */
    public double [] d_parametersFromMAMB(Matrix d_MA, Matrix d_MB, Matrix MA, Matrix MB, boolean isAngle){
    	double [] result= new double [getNumOutputs()]; // only first 6 are used
    	for (int i=6; i<getNumOutputs();i++) result[i]=0.0;
/*
0		public double x0=0;      // lens axis from pattern center, mm (to the right)
1		public double y0=0;      // lens axis from pattern center, mm (down)
2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise

        Vt=MB+MA*Vc
        calculate X0,Y0 (Z0==0), dist, phi, theta, psi from MB and MA
        | MA00 MA01 MA02 |
        | MA10 MA11 MA12 |
        | MA20 MA21 MA22 |
        ===================
        | MB0 |
        | MB1 |
        | MB2 |
        Take point Pc on a sub-camera axis and on the target Zt=0 plane: [0,0,dist]
        MA*Pc+MB= dist*[MA02,MA12,MA22]+MB =[Tx0,Ty0,0]
        MA02*dist+MB0=Tx0
        MA12*dist+MB1=Ty0
        MA22*dist+MB2=0
        dist=-MB2/MA22;
        Tx0=MB0-MB2*MA02/MA22
        Ty0=MB1-MB2*MA12/MA22
        */
//    	result[2]=-MB.get(2,0)/MA.get(2,2); // distance
/*
        d_dist=-(d_MB2/MA22 - d_MA22*MB2/(MA22^2) ;
 */
    	double K=isAngle?(Math.PI/180):1.0;
    	result[2]=K*(-d_MB.get(2,0)/MA.get(2,2)+
    	d_MA.get(2,2)*MB.get(2,0)/(MA.get(2,2)*MA.get(2,2))); // d_distance
    	/*
        Tx0=MB0-MB2*MA02/MA22
        d_Tx0=d_MB0 -  d_MB2*MA02/MA22 -d_MA02*MB2/MA22 +d_MA22*MB2*MA02/(MA22^2)
    	 */
    	result[0]= K*(d_MB.get(0,0) -
    	d_MB.get(2,0)*MA.get(0,2)/MA.get(2,2) -
    	d_MA.get(0,2)*MB.get(2,0)/MA.get(2,2) +
    	d_MA.get(2,2)*MB.get(2,0)*MA.get(0,2)/(MA.get(2,2)*MA.get(2,2))); // x0

    	/*
        Ty0=MB0-MB2*MA02/MA22
        d_Ty0=d_MB1 -  d_MB2*MA12/MA22 -d_MA12*MB2/MA22 +d_MA22*MB2*MA12/(MA22^2)
    	 */
    	result[1]= K*(d_MB.get(1,0) -
    	d_MB.get(2,0)*MA.get(1,2)/MA.get(2,2) -
    	d_MA.get(1,2)*MB.get(2,0)/MA.get(2,2) +
    	d_MA.get(2,2)*MB.get(2,0)*MA.get(1,2)/(MA.get(2,2)*MA.get(2,2))); // y0
        /*
        // now find phi, theta, psi
        MA - rotation from camera to target, transp(MA) - rotation from target to camera - same as rot(phi, theta,psi)
        MT=transp(MA),
        */
        /*
        MA[1,2]= sin(theta)
        MA[0,2]= cos(theta)*sin(phi)
        MA[2,2]= cos(theta)*cos(phi)
        MA[1,0]=-cos(theta)*sin(psi)
        MA[1,1]= cos(theta)*cos(psi)
        theta=arcsin(MA[1,2])
        phi=  atan2(MA[0,2],MA[2,2])
        psi= -atan2(MA[1,0],MA[1,1])
        arcsin(x)'= 1/sqrt(1-x^2)
        arccos(x)'=-1/sqrt(1-x^2)
        arctan(x)'= 1/sqrt(1+x^2)

         */
/*
        theta=arcsin(MA12)
        d_theta=d_MA12/sqrt(1-MA12^2)
 */
        result[4]=K*d_MA.get(1, 2)*180.0/Math.PI/Math.sqrt(1.0-MA.get(1, 2)*MA.get(1, 2));  //pitch
/*
        phi=  atan2(MA02,MA22)
        d_phi=(d_MA02*MA22-d_MA22*MA02) / (MA22^2+MA02^2)
 */

        result[3]=  K*180.0/Math.PI*(d_MA.get(0,2)*MA.get(2,2) - d_MA.get(2,2)*MA.get(0,2)) /
          ((MA.get(2,2)*MA.get(2,2)) + (MA.get(0,2)*MA.get(0,2)));//yaw
/*
        psi=  -atan2(MA10,MA11)
        d_psi=-(d_MA10*MA11-d_MA11*MA10) / (MA10^2+MA11^2)
*/
        result[5]=  -K* 180.0/Math.PI*(d_MA.get(1,0)*MA.get(1,1) - d_MA.get(1,1)*MA.get(1,0)) /
          ((MA.get(1,1)*MA.get(1,1)) + (MA.get(1,0)*MA.get(1,0)));//roll
    	return result;
    }
    
    public double [][] prepareDisplayGrid(){
    	int gridHeight=this.patternParameters.gridGeometry.length;
    	int gridWidth=this.patternParameters.gridGeometry[0].length;
    	double [][] dgrid=new double[3][gridHeight*gridWidth];
    	double average;
    	int num,index;
    	for (int i=0;i<dgrid.length;i++){
    		average=0.0;
    		num=0;
    		for (int v=0; v<gridHeight; v++) for (int u=0; u<gridWidth; u++) if (this.patternParameters.gridGeometry[v][u][3]>0) {
    			average+=this.patternParameters.gridGeometry[v][u][i];
    			num++;
    		}
    		average/=num;
    		index=0;
    		for (int v=0; v<gridHeight; v++) for (int u=0; u<gridWidth; u++) if (this.patternParameters.gridGeometry[v][u][3]>0) {
    			dgrid[i][index++]=this.patternParameters.gridGeometry[v][u][i];
    		} else {
    			dgrid[i][index++]=average;
    		}
    	}
    	return dgrid;
    }
    public String [] displayGridTitles() {
    	String [] titles={"Grid-X","Grid-Y","Grid-Z"};
    	return titles;
    }
    public String [] displayGridOnSensorTitles() {
    	String [] titles={
    			"PX","PY",
    			"dPX/dphi","dPY/dphi",
    			"dPX/dtheta","dPY/dtheta",
    			"dPX/dpsi","dPY/dpsi",
    			"dPX/dX0","dPY/dX0",
    			"dPX/dY0","dPY/dY0",
    			"dPX/dZ0","dPY/dZ0",
    			"dPX/df","dPY/df",
    			"dPX/ddist","dPY/dist",
    			"dPX/dDa","dPY/dDa",
    			"dPX/dDb","dPY/dDb",
    			"dPX/dDc","dPY/dDc",
    			"dPX/dPX0","dPY/dPX0",
    			"dPX/dPY0","dPY/dPY0"
    	};
    	return titles;
    }
    public double [][] prepareDisplayGridOnSensor(boolean showAll){
    	int gridHeight=this.patternParameters.gridGeometry.length;
    	int gridWidth=this.patternParameters.gridGeometry[0].length;
//    	double [][] dgrid=new double[showAll?28:2][gridHeight*gridWidth];
    	double [][] dgrid=new double[showAll?(2*15):2][gridHeight*gridWidth];
    	double average;
    	int num,index;
    	for (int i=0;i<dgrid.length/2;i++) for (int j=0;j<2;j++){
    		int ii=i*2+j;
    		average=0.0;
    		num=0;
    		for (int v=0; v<gridHeight; v++) for (int u=0; u<gridWidth; u++) if (this.patternParameters.gridGeometry[v][u][3]>0) {
    			average+=this.gridOnSensor[v][u][j][i];
    			num++;
    		}
    		average/=num;
    		index=0;
    		for (int v=0; v<gridHeight; v++) for (int u=0; u<gridWidth; u++) if (this.patternParameters.gridGeometry[v][u][3]>0) {
    			dgrid[ii][index++]=this.gridOnSensor[v][u][j][i];
    		} else {
    			dgrid[ii][index++]=average;
    		}
    	}
    	return dgrid;
    }
    /**
     * initialize image data with camera defaults
     * @param distortionCalibrationData grid distortionCalibrationData
     * @param eyesisCameraParameters deafault camera parameters
     */
    
    // Used in Aberration_Calibration
    public void initImageSet(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters) {
//    	Distortions.DistortionCalibrationData distortionCalibrationData= new Distortions.DistortionCalibrationData(filenames);
    	for (int i=0;i<distortionCalibrationData.getNumImages();i++){
    		int stationNumber=distortionCalibrationData.getImageStation(i);
    		int subCam=distortionCalibrationData.getImageSubcamera(i);
    		distortionCalibrationData.setParameters(eyesisCameraParameters.getParametersVector(stationNumber,subCam), i);
    		this.lensDistortionParameters.pixelSize=eyesisCameraParameters.getPixelSize(subCam);
    		this.lensDistortionParameters.distortionRadius=eyesisCameraParameters.getDistortionRadius(subCam);
    	}
    }
    public void copySensorConstants(EyesisCameraParameters eyesisCameraParameters) { // copy from the first channel
    		this.lensDistortionParameters.pixelSize=eyesisCameraParameters.getPixelSize(0);
    		this.lensDistortionParameters.distortionRadius=eyesisCameraParameters.getDistortionRadius(0);
    }
    
    /**
     * Update per-image parameters from those of the camera and those that have the same timestamp. Usually needed after adding or
     * enabling new images.
     * @param distortionCalibrationData grid distortionCalibrationData
     * @param eyesisCameraParameters - camera parameters (common and per sub-camera)
     * @return true if dialog was not canceled and programs ran
     */
    
    public boolean interactiveUpdateImageSet(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters
    ){
    	boolean [] parameterMask= new boolean[distortionCalibrationData.getNumParameters()];
    	boolean [] channelMask=   new boolean[distortionCalibrationData.getNumSubCameras()];
    	boolean [] stationMask=   new boolean[distortionCalibrationData.getNumStations()];
    	for (int i=0;i<parameterMask.length;i++) parameterMask[i]=false;
    	for (int i=0;i<channelMask.length;i++)   channelMask[i]=  true;
    	for (int i=0;i<stationMask.length;i++)   stationMask[i]=  true;
    	GenericDialog gd=new GenericDialog("Update (new) image settings from known data");
    	gd.addMessage("Select which individual image parameters to be updated from the camera parameters");
    	for (int i=0;i<parameterMask.length;i++) gd.addCheckbox(i+": "+distortionCalibrationData.getParameterName(i), parameterMask[i]);
    	gd.addMessage("----------");
    	gd.addMessage("Select which channels (sub-cameras) to update");
    	for (int i=0;i<channelMask.length;i++) gd.addCheckbox("Subcamera "+i, channelMask[i]);
    	if (stationMask.length>1) {
        	gd.addMessage("----------");
        	gd.addMessage("Select which stations (camera/goniometer locations) to update");
        	for (int i=0;i<stationMask.length;i++) gd.addCheckbox("Station "+i, stationMask[i]);
    	}
    	gd.addMessage("----------");
    	gd.addCheckbox("Applying known extrinsic parameters to the same timestamp images", true);
    	gd.addCheckbox("Use closest (by motor steps) image if none for the same timestamp is enabled", true);
    	gd.addCheckbox("Verticaly center the camera head by calculateing center above horizontal", false);
//    	gd.addCheckbox("Update currently disabled images", true);
	    WindowTools.addScrollBars(gd);
    	gd.showDialog();
    	if (gd.wasCanceled()) return false;
    	for (int i=0;i<parameterMask.length;i++) parameterMask[i]= gd.getNextBoolean();
    	for (int i=0;i<channelMask.length;i++)   channelMask[i]=   gd.getNextBoolean();
    	if (stationMask.length>1) {
    		for (int i=0;i<stationMask.length;i++) stationMask[i]= gd.getNextBoolean();
    	}
    	boolean updateFromTimestamps= gd.getNextBoolean();
    	boolean allowClosest=         gd.getNextBoolean();
    	boolean reCenterVertically=   gd.getNextBoolean();
    	if (reCenterVertically){
    		eyesisCameraParameters.recenterVertically(channelMask, stationMask);
    		for (int i=0;i<channelMask.length;i++) channelMask[i]= true;
    	}

		
//		boolean updateDisabled=       gd.getNextBoolean();
    	updateImageSetFromCamera(
    			distortionCalibrationData,
    			eyesisCameraParameters,
    			parameterMask, //boolean [] parameterMask,
    			channelMask, // copy X,Y,Z (usually true)
    			stationMask // copy 2 goniometer angles (usually false)
    	);
    	if (updateFromTimestamps) {
    		updateImageSetFromSameTimestamps(
    				distortionCalibrationData,
    				eyesisCameraParameters,
    				null, // boolean [] selectedImages,
    				null, //boolean [] parameterMask,
    				allowClosest
    				//,updateDisabled	
    		);
    		distortionCalibrationData.updateSetOrientation(null); // update orientation of image sets
    	}
    	return true;
    }
    
    public boolean setSetFromClosestAndEstimateOrientation(
    		int numSet,
    		boolean [] selectedImages,
    		boolean [] parameterMask,
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters){
    	if (selectedImages==null) {
    		selectedImages= new boolean[distortionCalibrationData.getNumImages()];
    		for (int i=0;i<selectedImages.length;i++) selectedImages[i]=distortionCalibrationData.gIP[i].enabled;
    	}
    	if (parameterMask==null) {
    		parameterMask= new boolean[distortionCalibrationData.getNumParameters()];
    		for (int i=0;i<parameterMask.length;i++) parameterMask[i]=true;
    	}
    	for (int i=0;i<parameterMask.length;i++) {
    		if (distortionCalibrationData.isSubcameraParameter(i))    	parameterMask[i]=false;
    	}

    	int enabledImage=getClosestImage( // {numEnabledSet,enabledChannel,enabledImage};
	    		distortionCalibrationData,
	    		selectedImages,
	    		numSet);
    	if (enabledImage<0) return false; // failed to find closest
		updateSetFromClosest(
				numSet,
				enabledImage,
				parameterMask,
				distortionCalibrationData);
		// invalidate current angles
		distortionCalibrationData.gIS[numSet].goniometerAxial=Double.NaN;
		distortionCalibrationData.gIS[numSet].goniometerTilt= Double.NaN;
		// re-estimate orientation
		double [] ta=distortionCalibrationData.getImagesetTiltAxial(distortionCalibrationData.gIS[numSet].timeStamp); // updates tilt/axial
	    if ((ta==null) || Double.isNaN(ta[0]) || Double.isNaN(ta[1])) return false;
	    return true;
    }
    

    public boolean interactiveUpdateImageSetOld(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters
    ){
    	GenericDialog gd=new GenericDialog("Update (new) image settings from known data");
    	gd.addCheckbox("Update per-image parameters from those of the camera", true);
    	gd.addCheckbox("Copy location of the camera (X,Y,Z)", true);
    	gd.addCheckbox("Copy orientation of the camera (tilt and axial)", false);
    	gd.addMessage("");
    	gd.addCheckbox("Update per-image parameters from those with the same timestamp", true);
    	gd.addCheckbox("Use closest (by motor steps) image if none for the same timestamp is enabled", true);
//    	gd.addCheckbox("Update currently disabled images", true);

    	gd.showDialog();
    	if (gd.wasCanceled()) return false;
    	boolean updateFromCamera=     gd.getNextBoolean();
    	boolean copyLocation=         gd.getNextBoolean();
    	boolean copyOrientation=      gd.getNextBoolean();
    	boolean updateFromTimestamps= gd.getNextBoolean();
    	boolean allowClosest=         gd.getNextBoolean();
//		boolean updateDisabled=       gd.getNextBoolean();
   	
    	boolean [] parameterMask= new boolean[distortionCalibrationData.getNumParameters()];
    	for (int i=0;i<parameterMask.length;i++) {
    		parameterMask[i]=true;
    		if (distortionCalibrationData.isLocationParameter(i)    && !copyLocation)    	parameterMask[i]=false;
    		if (distortionCalibrationData.isOrientationParameter(i) && !copyOrientation)	parameterMask[i]=false;
    	}
    	
    	if (updateFromCamera) updateImageSetFromCamera(
    			distortionCalibrationData,
    			eyesisCameraParameters,
    			parameterMask, //boolean [] parameterMask,
    			null,
    			null
    	);
    	if (updateFromTimestamps) {
    		updateImageSetFromSameTimestamps(
    				distortionCalibrationData,
    				eyesisCameraParameters,
    				null, // boolean [] selectedImages,
    				null, //boolean [] parameterMask,
    				allowClosest
//    				,updateDisabled
    		);
    		distortionCalibrationData.updateSetOrientation(null); // update orientation of image sets
    	}
    	return true;
    }
    
    
    /**
     * Copies selected parameters from the camera parameters to per-image parameters (i.e. for new/previously disabled images)
     * @param distortionCalibrationData grid distortionCalibrationData
     * @param eyesisCameraParameters - camera parameters (common and per sub-camera)
     * @param parameterMask when element is true - copy parameters, false - keep current value. Null - selects all (filtered by the next parameters)
     * @param copyLocation copy location (x,Y,Z) of the camera , normally should be true
     * @param copyOrientation copy 2 goniometer angles, normally should be false
     */
    public void updateImageSetFromCamera(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters,
    		boolean [] parameterMask,
    		boolean [] channelMask,
    		boolean [] stationMask
    		) { 
//    	Distortions.DistortionCalibrationData distortionCalibrationData= new Distortions.DistortionCalibrationData(filenames);
    	for (int i=0;i<distortionCalibrationData.getNumImages();i++){
    		int stationNumber=distortionCalibrationData.getImageStation(i);
    		int subCam=distortionCalibrationData.getImageSubcamera(i);
    		if ((channelMask!=null) && !channelMask[subCam])        continue;
    		if ((stationMask!=null) && !stationMask[stationNumber]) continue;
    		double [] oldVector=distortionCalibrationData.getParameters(i);
    		double [] newVector=eyesisCameraParameters.getParametersVector(stationNumber,subCam);
    		for (int j=0;j<oldVector.length;j++) if (parameterMask[j]) oldVector[j]=newVector[j];
    		distortionCalibrationData.setParameters(oldVector, i);
    		this.lensDistortionParameters.pixelSize=eyesisCameraParameters.getPixelSize(subCam);
    		this.lensDistortionParameters.distortionRadius=eyesisCameraParameters.getDistortionRadius(subCam);
    	}
    }
    /**
     * Copies selected (normally all) parameters from the selected images with the same timestamp (i.e. for new/previously disabled images)
     * @param distortionCalibrationData grid distortionCalibrationData
     * @param eyesisCameraParameters - camera parameters (common and per sub-camera)
     * @param selectedImages Use only selected images (null - all enabled)
     * @param parameterMask when element is true - copy parameters, false - keep current value. Null - selects all (and should be normally null)
     * @param allowClosest If there is no enabled image for the current timestamp, find the closest selected using motor coordinates
     * @param updateDisabled update disable images also
     */
    public void updateImageSetFromSameTimestamps(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		EyesisCameraParameters eyesisCameraParameters,
    		boolean [] selectedImages,
    		boolean [] parameterMask,
    		boolean allowClosest
    		){
		System.out.println("updateImageSetFromSameTimestamps(), allowClosest="+allowClosest); //+" updateDisabled="+updateDisabled);
    	if (selectedImages==null) {
    		selectedImages= new boolean[distortionCalibrationData.getNumImages()];
    		for (int i=0;i<selectedImages.length;i++) selectedImages[i]=distortionCalibrationData.gIP[i].enabled;
//    		for (int i=0;i<selectedImages.length;i++) selectedImages[i]=distortionCalibrationData.gIP[i].enabled || updateDisabled;
    	}
    	if (parameterMask==null) {
    		parameterMask= new boolean[distortionCalibrationData.getNumParameters()];
    		for (int i=0;i<parameterMask.length;i++) parameterMask[i]=true;
    	}
    	for (int i=0;i<parameterMask.length;i++) {
    		if (distortionCalibrationData.isSubcameraParameter(i))    	parameterMask[i]=false;
    	}
    	for (int numSet=0; numSet<distortionCalibrationData.gIS.length;numSet++){
// find enabled image for this set
    		int enabledImage=-1;
    		// look for enabled image in the same imageSet
    		
    		for (int nChn=0;nChn<distortionCalibrationData.gIS[numSet].imageSet.length;nChn++) if (distortionCalibrationData.gIS[numSet].imageSet[nChn]!=null){
    			int img=distortionCalibrationData.gIS[numSet].imageSet[nChn].imgNumber;
    			if (selectedImages[img]){
        			enabledImage=img;
    				break;
    			}
    		}
    		// look for closest in the other imageSet
    		if ((enabledImage<0) && (allowClosest)){
    			enabledImage=getClosestImage( // {numEnabledSet,enabledChannel,enabledImage};
    			    		distortionCalibrationData,
    			    		selectedImages,
    			    		numSet);
    		}
    		if (enabledImage>=0){
    			updateSetFromClosest(
    					numSet,
    					enabledImage,
    					parameterMask,
    					distortionCalibrationData);
    		}
    	}

    }
    
    public int getClosestImage(
    		Distortions.DistortionCalibrationData distortionCalibrationData,
    		boolean [] selectedImages,
    		int numSet
    ){
    	int enabledChannel=-1;
    	int enabledImage=-1;
    	if (distortionCalibrationData.gIS[numSet].motors==null ){
    		if (this.debugLevel>0) System.out.println("getClosestSetChannelImage(): No motor data for timestamp "+distortionCalibrationData.gIS[numSet].timeStamp);
    		return -1;
    	}
    	double d2Min=-1;
    	for (int numOtherSet=0;numOtherSet<distortionCalibrationData.gIS.length;numOtherSet++)
    		if ((numOtherSet!=numSet) &&
    				(distortionCalibrationData.gIS[numOtherSet].stationNumber==distortionCalibrationData.gIS[numSet].stationNumber) &&
    				(distortionCalibrationData.gIS[numOtherSet].motors!=null) &&
    				(distortionCalibrationData.gIS[numOtherSet].imageSet!=null) 
    		) {
    			enabledChannel=-1;
    			int otherImage=-1;
    			for (int nChn=0;nChn<distortionCalibrationData.gIS[numOtherSet].imageSet.length;nChn++)
    				if (distortionCalibrationData.gIS[numOtherSet].imageSet[nChn]!=null){
    					otherImage=distortionCalibrationData.gIS[numOtherSet].imageSet[nChn].imgNumber;
    					if (selectedImages[otherImage]){
    						enabledChannel=nChn;
    						break;
    					}
    				}
    			if (enabledChannel>=0){
    				double d2=0;
    				for (int k=0;k<distortionCalibrationData.gIS[numOtherSet].motors.length;k++){
    					d2+=1.0*(distortionCalibrationData.gIS[numOtherSet].motors[k]-distortionCalibrationData.gIS[numSet].motors[k])*
    					(distortionCalibrationData.gIS[numOtherSet].motors[k]-distortionCalibrationData.gIS[numSet].motors[k]);
    				}
    				if ((d2Min<0) || (d2Min>d2)) {
    					d2Min=d2;
    					enabledImage=otherImage;
    				}
    			}
    		}
    	return enabledImage;
    }

    public void updateSetFromClosest(
    		int numSet,
    		int enabledImage,
    		boolean [] parameterMask,
    		Distortions.DistortionCalibrationData distortionCalibrationData
    		){
		int numEnabledSet=distortionCalibrationData.gIP[enabledImage].setNumber;
		distortionCalibrationData.gIS[numSet].setSetVector(distortionCalibrationData.gIS[numEnabledSet].getSetVector());
		System.out.println("getClosestSetChannelImage(): imageSet "+numSet+" set orientationEstimated=true, updated from imageSet "+numEnabledSet);
		distortionCalibrationData.gIS[numSet].orientationEstimated=(numSet!=numEnabledSet); 
		double [] newVector=distortionCalibrationData.getParameters(enabledImage);
		for (int nChn=0;nChn<distortionCalibrationData.gIS[numSet].imageSet.length;nChn++)
			if (distortionCalibrationData.gIS[numSet].imageSet[nChn]!=null){ // will copy back to itself, OK
				int targetImage=distortionCalibrationData.gIS[numSet].imageSet[nChn].imgNumber;
				double [] oldVector=distortionCalibrationData.getParameters(targetImage);
				for (int j=0;j<oldVector.length;j++) if (parameterMask[j]) oldVector[j]=newVector[j];
				distortionCalibrationData.setParameters(oldVector, targetImage);
			}
    }
    
    // TODO: Add updating to all Stations depending on type of adjustment. Initially only teh same station as image will be updated
    // Not needed - for "super" unselected images are also updated
    /**
     * Update camera/subcamera parameters from the currently selected set of images
     * several images may have different values for the same parameter, in that case
     * these parameters will have the value of the last image
     */
    public void updateCameraParametersFromCalculated(
    		boolean allImages ){
    	int numSeries=allImages?(-1):this.fittingStrategy.currentSeriesNumber;
		boolean [] selectedImages=fittingStrategy.selectedImages(numSeries); // all enabled
		boolean [] selectedImagesDebug=null;
		boolean debugThis=false;
		int maxDebugImages=10;
		if (this.debugLevel>2){
			int numSel=0;
			for (int i=0;i<selectedImages.length;i++) if (selectedImages[i]) numSel++;
			if (numSel<=maxDebugImages) debugThis=true;
			else {
				System.out.println ("Too many images ("+numSel+">"+ maxDebugImages +") to debug, skipping console println.");
				selectedImagesDebug=fittingStrategy.selectedImages(this.fittingStrategy.currentSeriesNumber); // all enabled

			}
		}
		for (int numImg=0;numImg<selectedImages.length; numImg++) if (selectedImages[numImg]){ // here only adjusted images should participate
			int subCam=fittingStrategy.distortionCalibrationData.getImageSubcamera(numImg);
//			double [] par=fittingStrategy.distortionCalibrationData.pars[numImg];
			double [] par=fittingStrategy.distortionCalibrationData.getParameters(numImg);
    		boolean [] update=new boolean[par.length];
    		for (int i=0;i<update.length;i++) update[i]=true;
    		int stationNumber=fittingStrategy.distortionCalibrationData.getImageStation(numImg);
    		// TODO: maybe determine - which parameters to be updated, not all - i.e. "super-common", or having the same value, etc.
    		// but all those intrinsic are required to match calibration files saved 
			fittingStrategy.distortionCalibrationData.eyesisCameraParameters.setParametersVector(par, update, stationNumber, subCam);
			if (debugThis || ((selectedImagesDebug!=null) && selectedImagesDebug[numImg])){
				System.out.println ("Updating from image #"+numImg+" (subCam="+subCam+" stationNumber="+stationNumber+"):");
//getParameterName				
				for (int i=0;i<par.length;i++){
					System.out.println(i+": "+fittingStrategy.distortionCalibrationData.getParameterName(i)+" = "+par[i]);
				}
			}
		}
		if (this.debugLevel>1) System.out.println("updateCameraParametersFromCalculated("+allImages+") for series="+numSeries);
		// Next line is not needed anymore (will harm as will set orientationEstimated for all unselected sets)
//		if (!allImages) fittingStrategy.distortionCalibrationData.updateSetOrientation(selectedImages); // only for selected images (not all enabled), OK
    }
	/* Create a Thread[] array as large as the number of processors available.
	 * From Stephan Preibisch's Multithreading.java class. See:
	 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
	 */
	private Thread[] newThreadArray(int maxCPUs) {
		int n_cpus = Runtime.getRuntime().availableProcessors();
		if (n_cpus>maxCPUs)n_cpus=maxCPUs;
		return new Thread[n_cpus];
	}
/* Start all given threads and wait on each of them until all are done.
	 * From Stephan Preibisch's Multithreading.java class. See:
	 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
	 */
	private static void startAndJoin(Thread[] threads)
	{
		for (int ithread = 0; ithread < threads.length; ++ithread)
		{
			threads[ithread].setPriority(Thread.NORM_PRIORITY);
			threads[ithread].start();
		}

		try
		{   
			for (int ithread = 0; ithread < threads.length; ++ithread)
				threads[ithread].join();
		} catch (InterruptedException ie)
		{
			throw new RuntimeException(ie);
		}
	}

    public static class RefineParameters{
    	  public boolean extrapolate=true;   // extrapolate sensor distortion correction
     	  public double alphaThreshold =0.8; // ignore sensor correction pixels with mask value below this
     	  public double fatZero=0.01;        // when extrapolatging color transfer coefficients (flat field) use this for logariphm
     	  public double extrapolationSigma=30.0; // sigmna for Gaussian weight function when fittinga plane to known pixels
     	                                         // calculated for non-decimated pixels
     	  public double extrapolationKSigma=2.0; // consider pixels in 2*extrapolationSigma*extrapolationKSigma square when fitting 
     	  public boolean smoothCorrection=true;  // apply Gaussian blur to calculated pixel correction field
     	  public double smoothSigma=50.0; // sigma for Gaussian weight function when fittinga plane to known pixels
     	  public double correctionScale=1.0; // scale correction when accumulating;
     	  public boolean showCumulativeCorrection=false; // show correction afther this one is applied
     	  public boolean showUnfilteredCorrection=true; // show this (additional) correction before extrapolation and/or smoothing
     	  public boolean showExtrapolationCorrection=false; // show Extrapolation
     	  public boolean showThisCorrection=false; // show this (additional) correction separately
     	  public boolean showPerImage=false;     // show residuals for each individual image
     	  public int     showIndividualNumber=0; // which image to show (-1 - all)
     	  public boolean applyCorrection=true;   // apply calculated corerction
     	  public boolean applyFlatField=true;   // apply calculated flat-field
     	  public boolean grid3DCorrection=true; // Correct patetrn grid node locations in 3d (false - in 2d only)
     	  public boolean rotateCorrection=true; // old value - did not yet understand why is it needed
     	  public double  grid3DMaximalZCorr=20.0; // Maximal Z-axis correc tion (if more will fall back to 2d correction algorithm)
     	  
     	  public boolean  useVariations=  false; // allow different Z for different stations (for not a wall/stable pattern)
     	  public double  variationPenalty=0.001; // "stiffness" of individual (per-station) Z-values of the target pattern
     	  public boolean  fixXY=          false; // adjust only Z of the target pattern, keep X and Y
     	  public boolean  resetVariations=false;
     	  public boolean  noFallBack=     true; // may have bugs - not tested yet
     	  
     	  
     	  public boolean usePatternAlpha= true;  // use pattern grid alpha data, false - old calculation 
     	  
// New individual parameters for modify pattern grid
     	 public boolean  targetShowPerImage=false;
     	 public boolean  targetShowThisCorrection=false;
     	 public boolean  targetApplyCorrection=true;
    	 public double   targetCorrectionScale=1.0; // scale correction when accumulating;
// New parameters for new sesnor correction
    	 public boolean sensorExtrapolateDiff =      false; // true - extrapolate correction, false - composite
    	 public double sensorShrinkBlurComboSigma =  50.0;
    	 public double sensorShrinkBlurComboLevel =  0.25;
    	 public double sensorAlphaThreshold =        0.1;
    	 public double sensorStep =                  5;
    	 public double sensorInterpolationSigma=     100;
    	 public double sensorTangentialRadius=       0.5;
    	 public int    sensorScanDistance=           200;
    	 public int    sensorResultDistance=         500;
    	 public int    sensorInterpolationDegree=    2;
//New parameters for Flat field correction
    	 public int flatFieldSerNumber=             -1;
    	 public int flatFieldReferenceStation=       0;
    	 public double flatFieldShrink=              100.0;
    	 public double flatFieldNonVignettedRadius = 1000.0;
    	 public double flatFieldMinimalAlpha =       0.01; // use %
    	 public double flatFieldMinimalContrast=     0.1; 
    	 public double flatFieldMinimalAccumulate =  0.01; // use %
    	 public double flatFieldShrinkForMatching =  2.0;
    	 public double flatFieldMaxRelDiff =         0.1;  // use %
    	 public int    flatFieldShrinkMask=          2;
    	 public double flatFieldFadeBorder =         2.0;
    	 //			gd.addMessage("Update pattern white balance (if the illumination is yellowish, increase red and green here)");
    	 //    		LENS_DISTORTIONS.patternParameters.averageRGB[0]=gd.getNextNumber();
    	 //    		LENS_DISTORTIONS.patternParameters.averageRGB[1]=gd.getNextNumber();
    	 //   		LENS_DISTORTIONS.patternParameters.averageRGB[2]=gd.getNextNumber();
    	 public boolean flatFieldResetMask=      true;
    	 public boolean flatFieldShowSensorMasks=false;
    	 public boolean flatFieldShowIndividual= false;
    	 public boolean flatFieldShowResult=     true;
    	 public boolean flatFieldApplyResult=    true;
    	 public boolean flatFieldUseInterpolate= true;
    	 public double  flatFieldMaskThresholdOcclusion=0.15; // use %
    	 public int     flatFieldShrinkOcclusion= 2;
    	 public double  flatFieldFadeOcclusion=   2.0;

    	 public boolean flatFieldIgnoreSensorFlatField= false;
//    	 public boolean flatFieldUseSelectedChannels= false;
// Other 
    	 public int    repeatFlatFieldSensor=10; // TODO: add stop !
    	 
    	 public double  specularHighPassSigma=            10.0;
    	 public double  specularLowPassSigma=              2.0;
    	 public double  specularDiffFromAverageThreshold= 0.01;
    	 public int     specularNumIter=                  5;
    	 public boolean specularApplyNewWeights=          true;
    	 public boolean specularPositiveDiffOnly=         true;
    	 public int     specularShowDebug=                1; // 0 - do not show, 1 - show on last iteration only, 2 - show always

     	  
     	  public RefineParameters(){}
     	  public RefineParameters(
     			  boolean extrapolate,
     			  double alphaThreshold,
     	     	  double fatZero,
     			  double extrapolationSigma,
     			  double extrapolationKSigma, 
     			  boolean smoothCorrection,
     			  double smoothSigma,
     	     	  double correctionScale,
     	     	  boolean showCumulativeCorrection,
     	     	  boolean showUnfilteredCorrection,
     	     	  boolean showExtrapolationCorrection,
     	     	  boolean showThisCorrection,
     	     	  boolean showPerImage,
     	     	  int     showIndividualNumber, // which image to show (-1 - all)
     	     	  boolean applyCorrection,
     	     	  boolean applyFlatField,   // apply calculated flat-field
     	    	  boolean grid3DCorrection, // Correct patetrn grid node locations in 3d (false - in 2d only)
     	    	  boolean rotateCorrection, // not clear
     	     	  double  grid3DMaximalZCorr, // Maximal Z-axis correc tion (if more will fall back to 2d correction algorithm)
     	     	  boolean useVariations,
     	     	  double  variationPenalty, // "stiffness" of individual (per-station) Z-values of the target pattern
     	     	  boolean  fixXY,
     	     	  boolean  resetVariations,
     	     	  boolean  noFallBack, // may have bugs - not tested yet
     	     	  boolean usePatternAlpha,
     	     	  boolean  targetShowPerImage,
     	     	  boolean  targetShowThisCorrection,
     	     	  boolean  targetApplyCorrection,
     	     	  double   targetCorrectionScale,
     	     	  boolean sensorExtrapolateDiff,
     	     	  double sensorShrinkBlurComboSigma,
     	     	  double sensorShrinkBlurComboLevel,
     	     	  double sensorAlphaThreshold,
     	     	  double sensorStep,
     	     	  double sensorInterpolationSigma,
     	     	  double sensorTangentialRadius,
     	     	  int    sensorScanDistance,
     	     	  int    sensorResultDistance,
     	     	  int    sensorInterpolationDegree,
     	     	  int flatFieldSerNumber,
     	     	  int flatFieldReferenceStation,
     	     	  double flatFieldShrink,
     	     	  double flatFieldNonVignettedRadius,
     	     	  double flatFieldMinimalAlpha,
     	     	  double flatFieldMinimalContrast,
     	     	  double flatFieldMinimalAccumulate,
     	     	  double flatFieldShrinkForMatching,
     	     	  double flatFieldMaxRelDiff,
     	     	  int    flatFieldShrinkMask,
     	     	  double flatFieldFadeBorder,
     	     	  boolean flatFieldResetMask,
     	     	  boolean flatFieldShowSensorMasks,
     	     	  boolean flatFieldShowIndividual,
     	     	  boolean flatFieldShowResult,
     	     	  boolean flatFieldApplyResult,
     	     	  boolean flatFieldUseInterpolate,
     	     	  double  flatFieldMaskThresholdOcclusion,
     	     	  int     flatFieldShrinkOcclusion,
     	     	  double  flatFieldFadeOcclusion,
     	     	  boolean flatFieldIgnoreSensorFlatField,
     	     	  int    repeatFlatFieldSensor,
     	    	  double  specularHighPassSigma,
     	    	  double  specularLowPassSigma,
     	    	  double  specularDiffFromAverageThreshold,
     	    	  int     specularNumIter,
     	    	  boolean specularApplyNewWeights,
     	    	  boolean specularPositiveDiffOnly,
     	    	  int     specularShowDebug){
     		  this.extrapolate=extrapolate;
     		  this.alphaThreshold=alphaThreshold;
         	  this.fatZero=fatZero;        // when extrapolatging color transfer coefficients (flat field) use this for logariphm
     		  this.extrapolationSigma=extrapolationSigma;
     		  this.extrapolationKSigma=extrapolationKSigma; 
     		  this.smoothCorrection=smoothCorrection;
     		  this.smoothSigma=smoothSigma;
     		  this.correctionScale=correctionScale;
     		  this.showCumulativeCorrection=showCumulativeCorrection;
     		  this.showUnfilteredCorrection=showUnfilteredCorrection;
     		  this.showExtrapolationCorrection=showExtrapolationCorrection;
     		  this.showThisCorrection=showThisCorrection;
     		  this.showPerImage=showPerImage;
     		  this.showIndividualNumber=showIndividualNumber; // which image to show (-1 - all)
     		  this.applyCorrection=applyCorrection;
     		  this.applyFlatField=applyFlatField;
     		  this.grid3DCorrection=grid3DCorrection;
 	    	  this.rotateCorrection=rotateCorrection; // not clear
         	  this.grid3DMaximalZCorr=grid3DMaximalZCorr; // Maximal Z-axis correc tion (if more will fall back to 2d correction algorithm)
         	  this.useVariations=useVariations;
         	  this.variationPenalty=variationPenalty; // "stiffness" of individual (per-station) Z-values of the target pattern
         	  this.fixXY=fixXY;
         	  this.resetVariations=resetVariations;
         	  this.noFallBack=     noFallBack; // may have bugs - not tested yet
         	  this.usePatternAlpha=usePatternAlpha;
         	  this.targetShowPerImage=       targetShowPerImage;
         	  this.targetShowThisCorrection=  targetShowThisCorrection;
         	  this.targetApplyCorrection=     targetApplyCorrection;
         	  this.targetCorrectionScale=     targetCorrectionScale;
         	  this.sensorExtrapolateDiff=     sensorExtrapolateDiff;
         	  this.sensorShrinkBlurComboSigma=sensorShrinkBlurComboSigma;
         	  this.sensorShrinkBlurComboLevel=sensorShrinkBlurComboLevel;
         	  this.sensorAlphaThreshold=sensorAlphaThreshold;
         	  this.sensorStep=sensorStep;
         	  this.sensorInterpolationSigma=sensorInterpolationSigma;
         	  this.sensorTangentialRadius=sensorTangentialRadius;
         	  this.sensorScanDistance=sensorScanDistance;
         	  this.sensorResultDistance=sensorResultDistance;
         	  this.sensorInterpolationDegree=sensorInterpolationDegree;
         	  this.flatFieldSerNumber=flatFieldSerNumber;
         	  this.flatFieldReferenceStation=flatFieldReferenceStation;
         	  this.flatFieldShrink=flatFieldShrink;
         	  this.flatFieldNonVignettedRadius=flatFieldNonVignettedRadius;
         	  this.flatFieldMinimalAlpha=flatFieldMinimalAlpha;
         	  this.flatFieldMinimalContrast=flatFieldMinimalContrast;
         	  this.flatFieldMinimalAccumulate=flatFieldMinimalAccumulate;
         	  this.flatFieldShrinkForMatching=flatFieldShrinkForMatching;
         	  this.flatFieldMaxRelDiff=flatFieldMaxRelDiff;
         	  this.flatFieldShrinkMask=flatFieldShrinkMask;
         	  this.flatFieldFadeBorder=flatFieldFadeBorder;
         	  this.flatFieldResetMask=flatFieldResetMask;
         	  this.flatFieldShowSensorMasks=flatFieldShowSensorMasks;
         	  this.flatFieldShowIndividual=flatFieldShowIndividual;
         	  this.flatFieldShowResult=flatFieldShowResult;
         	  this.flatFieldApplyResult=flatFieldApplyResult;
         	  this.flatFieldUseInterpolate=flatFieldUseInterpolate;
         	  this.flatFieldMaskThresholdOcclusion=flatFieldMaskThresholdOcclusion;
         	  this.flatFieldShrinkOcclusion=flatFieldShrinkOcclusion;
         	  this.flatFieldFadeOcclusion=flatFieldFadeOcclusion;
         	  this.flatFieldIgnoreSensorFlatField=flatFieldIgnoreSensorFlatField;
//         	  this.flatFieldUseSelectedChannels=flatFieldUseSelectedChannels;
         	  this.repeatFlatFieldSensor=repeatFlatFieldSensor;

         	  this.specularHighPassSigma=specularHighPassSigma;
         	  this.specularLowPassSigma=specularLowPassSigma;
         	  this.specularDiffFromAverageThreshold=specularDiffFromAverageThreshold;
         	  this.specularNumIter=specularNumIter;
         	  this.specularApplyNewWeights=specularApplyNewWeights;
         	  this.specularPositiveDiffOnly=specularPositiveDiffOnly;
         	  this.specularShowDebug=specularShowDebug;
     	  }
     	  public RefineParameters clone(){
     		  return new RefineParameters(
     				  this.extrapolate,
     				  this.alphaThreshold,
     	         	  this.fatZero,
     				  this.extrapolationSigma,
     				  this.extrapolationKSigma, 
     				  this.smoothCorrection,
     				  this.smoothSigma,
     				  this.correctionScale,
     				  this.showCumulativeCorrection,
     				  this.showUnfilteredCorrection,
     				  this.showExtrapolationCorrection,
     				  this.showThisCorrection,
     				  this.showPerImage,
     	     		  this.showIndividualNumber,
     				  this.applyCorrection,
     	     		  this.applyFlatField,
     	     		  this.grid3DCorrection,
         	    	  this.rotateCorrection, // not clear
     	        	  this.grid3DMaximalZCorr, // Maximal Z-axis correc tion (if more will fall back to 2d correction algorithm)
     	        	  this.useVariations,
     	         	  this.variationPenalty, // "stiffness" of individual (per-station) Z-values of the target pattern 
     	         	  this.fixXY,
     	         	  this.resetVariations,
     	         	  this.noFallBack, // may have bugs - not tested yet
     	     		  this.usePatternAlpha,
     	         	  this.targetShowPerImage,
     	         	  this.targetShowThisCorrection,
     	         	  this.targetApplyCorrection,
     	         	  this.targetCorrectionScale,
     	         	  this.sensorExtrapolateDiff,
     	         	  this.sensorShrinkBlurComboSigma,
     	         	  this.sensorShrinkBlurComboLevel,
     	         	  this.sensorAlphaThreshold,
     	         	  this.sensorStep,
     	         	  this.sensorInterpolationSigma,
     	         	  this.sensorTangentialRadius,
     	         	  this.sensorScanDistance,
     	         	  this.sensorResultDistance,
     	         	  this.sensorInterpolationDegree,
     	         	  this.flatFieldSerNumber,
     	         	  this.flatFieldReferenceStation,
     	         	  this.flatFieldShrink,
     	         	  this.flatFieldNonVignettedRadius,
     	         	  this.flatFieldMinimalAlpha,
     	         	  this.flatFieldMinimalContrast,
     	         	  this.flatFieldMinimalAccumulate,
     	         	  this.flatFieldShrinkForMatching,
     	         	  this.flatFieldMaxRelDiff,
     	         	  this.flatFieldShrinkMask,
     	         	  this.flatFieldFadeBorder,
     	         	  this.flatFieldResetMask,
     	         	  this.flatFieldShowSensorMasks,
     	         	  this.flatFieldShowIndividual,
     	         	  this.flatFieldShowResult,
     	         	  this.flatFieldApplyResult,
     	         	  this.flatFieldUseInterpolate,
     	         	  this.flatFieldMaskThresholdOcclusion,
     	         	  this.flatFieldShrinkOcclusion,
     	         	  this.flatFieldFadeOcclusion,
     	         	  this.flatFieldIgnoreSensorFlatField,
     	         	  this.repeatFlatFieldSensor,
         	    	  this.specularHighPassSigma,
         	    	  this.specularLowPassSigma,
         	    	  this.specularDiffFromAverageThreshold,
         	    	  this.specularNumIter,
         	    	  this.specularApplyNewWeights,
         	    	  this.specularPositiveDiffOnly,
         	    	  this.specularShowDebug);
     	  }
     	   	public void setProperties(String prefix,Properties properties){
        		
        		properties.setProperty(prefix+"extrapolate",this.extrapolate+"");
        		properties.setProperty(prefix+"alphaThreshold",this.alphaThreshold+"");
        		properties.setProperty(prefix+"fatZero",this.fatZero+"");
        		properties.setProperty(prefix+"extrapolationSigma",this.extrapolationSigma+"");
        		properties.setProperty(prefix+"extrapolationKSigma",this.extrapolationKSigma+"");
        		properties.setProperty(prefix+"smoothCorrection",this.smoothCorrection+"");
        		properties.setProperty(prefix+"smoothSigma",this.smoothSigma+"");
        		properties.setProperty(prefix+"correctionScale",this.correctionScale+"");
        		properties.setProperty(prefix+"showCumulativeCorrection",this.showCumulativeCorrection+"");
        		properties.setProperty(prefix+"showUnfilteredCorrection",this.showUnfilteredCorrection+"");
        		properties.setProperty(prefix+"showExtrapolationCorrection",this.showExtrapolationCorrection+"");
        		properties.setProperty(prefix+"showThisCorrection",this.showThisCorrection+"");
        		properties.setProperty(prefix+"showPerImage",this.showPerImage+"");
        		properties.setProperty(prefix+"showIndividualNumber",this.showIndividualNumber+"");
        		properties.setProperty(prefix+"applyCorrection",this.applyCorrection+"");
        		properties.setProperty(prefix+"applyFlatField",this.applyFlatField+"");
        		properties.setProperty(prefix+"grid3DCorrection",this.grid3DCorrection+"");
        		properties.setProperty(prefix+"rotateCorrection",this.rotateCorrection+"");
        		properties.setProperty(prefix+"grid3DMaximalZCorr",this.grid3DMaximalZCorr+"");
        		
        		properties.setProperty(prefix+"useVariations",this.useVariations+"");
        		properties.setProperty(prefix+"variationPenalty",this.variationPenalty+"");
        		properties.setProperty(prefix+"fixXY",this.fixXY+"");
        		
        		properties.setProperty(prefix+"resetVariations",this.resetVariations+"");
        		properties.setProperty(prefix+"noFallBack",this.noFallBack+"");
        		
        		properties.setProperty(prefix+"usePatternAlpha",this.usePatternAlpha+"");
        		
        		properties.setProperty(prefix+"targetShowPerImage",this.targetShowPerImage+"");
        		properties.setProperty(prefix+"targetShowThisCorrection",this.targetShowThisCorrection+"");
        		properties.setProperty(prefix+"targetApplyCorrection",this.targetApplyCorrection+"");
        		properties.setProperty(prefix+"targetCorrectionScale",this.targetCorrectionScale+"");
        		
        		properties.setProperty(prefix+"sensorExtrapolateDiff",this.sensorExtrapolateDiff+"");
        		properties.setProperty(prefix+"sensorShrinkBlurComboSigma",this.sensorShrinkBlurComboSigma+"");

        		properties.setProperty(prefix+"sensorShrinkBlurComboLevel",this.sensorShrinkBlurComboLevel+"");
        		properties.setProperty(prefix+"sensorAlphaThreshold",this.sensorAlphaThreshold+"");
        		properties.setProperty(prefix+"sensorStep",this.sensorStep+"");
        		properties.setProperty(prefix+"sensorInterpolationSigma",this.sensorInterpolationSigma+"");
        		properties.setProperty(prefix+"sensorTangentialRadius",this.sensorTangentialRadius+"");
        		
        		properties.setProperty(prefix+"sensorScanDistance",this.sensorScanDistance+"");
        		properties.setProperty(prefix+"sensorResultDistance",this.sensorResultDistance+"");
        		properties.setProperty(prefix+"sensorInterpolationDegree",this.sensorInterpolationDegree+"");
        		properties.setProperty(prefix+"flatFieldSerNumber",this.flatFieldSerNumber+"");
        		properties.setProperty(prefix+"flatFieldReferenceStation",this.flatFieldReferenceStation+"");
        		
        		properties.setProperty(prefix+"flatFieldShrink",this.flatFieldShrink+"");
        		properties.setProperty(prefix+"flatFieldNonVignettedRadius",this.flatFieldNonVignettedRadius+"");
        		properties.setProperty(prefix+"flatFieldMinimalAlpha",this.flatFieldMinimalAlpha+"");
        		
        		properties.setProperty(prefix+"flatFieldMinimalContrast",this.flatFieldMinimalContrast+"");
        		properties.setProperty(prefix+"flatFieldMinimalAccumulate",this.flatFieldMinimalAccumulate+"");
        		properties.setProperty(prefix+"flatFieldShrinkForMatching",this.flatFieldShrinkForMatching+"");
        		
        		properties.setProperty(prefix+"flatFieldMaxRelDiff",this.flatFieldMaxRelDiff+"");
        		properties.setProperty(prefix+"flatFieldShrinkMask",this.flatFieldShrinkMask+"");
        		properties.setProperty(prefix+"flatFieldFadeBorder",this.flatFieldFadeBorder+"");
        		properties.setProperty(prefix+"flatFieldResetMask",this.flatFieldResetMask+"");
        		properties.setProperty(prefix+"flatFieldShowSensorMasks",this.flatFieldShowSensorMasks+"");

        		properties.setProperty(prefix+"flatFieldShowIndividual",this.flatFieldShowIndividual+"");
        		properties.setProperty(prefix+"flatFieldShowResult",this.flatFieldShowResult+"");
        		properties.setProperty(prefix+"flatFieldApplyResult",this.flatFieldApplyResult+"");
        		properties.setProperty(prefix+"flatFieldUseInterpolate",this.flatFieldUseInterpolate+"");
        		properties.setProperty(prefix+"flatFieldMaskThresholdOcclusion",this.flatFieldMaskThresholdOcclusion+"");

        		properties.setProperty(prefix+"flatFieldShrinkOcclusion",this.flatFieldShrinkOcclusion+"");
        		properties.setProperty(prefix+"flatFieldFadeOcclusion",this.flatFieldFadeOcclusion+"");
        		properties.setProperty(prefix+"flatFieldIgnoreSensorFlatField",this.flatFieldIgnoreSensorFlatField+"");
        		properties.setProperty(prefix+"repeatFlatFieldSensor",this.repeatFlatFieldSensor+"");

        		properties.setProperty(prefix+"specularHighPassSigma",this.specularHighPassSigma+"");
        		properties.setProperty(prefix+"specularLowPassSigma", this.specularLowPassSigma+"");
        		properties.setProperty(prefix+"specularDiffFromAverageThreshold",this.specularDiffFromAverageThreshold+"");
        		properties.setProperty(prefix+"specularNumIter",this.specularNumIter+"");
        		properties.setProperty(prefix+"specularApplyNewWeights",this.specularApplyNewWeights+"");
        		properties.setProperty(prefix+"specularPositiveDiffOnly",this.specularPositiveDiffOnly+"");
        		properties.setProperty(prefix+"specularShowDebug",this.specularShowDebug+"");
        	}

     	   	public void getProperties(String prefix,Properties properties){
        		if (properties.getProperty(prefix+"extrapolate")!=null)
        			this.extrapolate=Boolean.parseBoolean(properties.getProperty(prefix+"extrapolate"));
        		if (properties.getProperty(prefix+"alphaThreshold")!=null)
        			this.alphaThreshold=Double.parseDouble(properties.getProperty(prefix+"alphaThreshold"));
        		if (properties.getProperty(prefix+"fatZero")!=null)
        			this.fatZero=Double.parseDouble(properties.getProperty(prefix+"fatZero"));
        		if (properties.getProperty(prefix+"extrapolationSigma")!=null)
        			this.extrapolationSigma=Double.parseDouble(properties.getProperty(prefix+"extrapolationSigma"));
        		if (properties.getProperty(prefix+"extrapolationKSigma")!=null)
        			this.extrapolationKSigma=Double.parseDouble(properties.getProperty(prefix+"extrapolationKSigma"));
        		if (properties.getProperty(prefix+"smoothCorrection")!=null)
        			this.smoothCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"smoothCorrection"));
        		if (properties.getProperty(prefix+"smoothSigma")!=null)
        			this.smoothSigma=Double.parseDouble(properties.getProperty(prefix+"smoothSigma"));
        		if (properties.getProperty(prefix+"correctionScale")!=null)
        			this.correctionScale=Double.parseDouble(properties.getProperty(prefix+"correctionScale"));
        		if (properties.getProperty(prefix+"showCumulativeCorrection")!=null)
        			this.showCumulativeCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"showCumulativeCorrection"));
        		if (properties.getProperty(prefix+"showUnfilteredCorrection")!=null)
        			this.showUnfilteredCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"showUnfilteredCorrection"));
        		if (properties.getProperty(prefix+"showExtrapolationCorrection")!=null)
        			this.showExtrapolationCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"showExtrapolationCorrection"));
        		if (properties.getProperty(prefix+"showThisCorrection")!=null)
        			this.showThisCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"showThisCorrection"));
        		if (properties.getProperty(prefix+"showPerImage")!=null)
        			this.showPerImage=Boolean.parseBoolean(properties.getProperty(prefix+"showPerImage"));
        		if (properties.getProperty(prefix+"showIndividualNumber")!=null)
        			this.showIndividualNumber=Integer.parseInt(properties.getProperty(prefix+"showIndividualNumber"));
        		if (properties.getProperty(prefix+"applyCorrection")!=null)
        			this.applyCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"applyCorrection"));
        		if (properties.getProperty(prefix+"applyFlatField")!=null)
        			this.applyFlatField=Boolean.parseBoolean(properties.getProperty(prefix+"applyFlatField"));
        		if (properties.getProperty(prefix+"grid3DCorrection")!=null)
        			this.grid3DCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"grid3DCorrection"));
        		if (properties.getProperty(prefix+"rotateCorrection")!=null)
        			this.rotateCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"rotateCorrection"));
        		if (properties.getProperty(prefix+"grid3DMaximalZCorr")!=null)
        			this.grid3DMaximalZCorr=Double.parseDouble(properties.getProperty(prefix+"grid3DMaximalZCorr"));
        		
        		if (properties.getProperty(prefix+"useVariations")!=null)
        			this.useVariations=Boolean.parseBoolean(properties.getProperty(prefix+"useVariations"));
        		if (properties.getProperty(prefix+"variationPenalty")!=null)
        			this.variationPenalty=Double.parseDouble(properties.getProperty(prefix+"variationPenalty"));
        		if (properties.getProperty(prefix+"fixXY")!=null)
        			this.fixXY=Boolean.parseBoolean(properties.getProperty(prefix+"fixXY"));
        		if (properties.getProperty(prefix+"resetVariations")!=null)
        			this.resetVariations=Boolean.parseBoolean(properties.getProperty(prefix+"resetVariations"));
        		if (properties.getProperty(prefix+"noFallBack")!=null)
        			this.noFallBack=Boolean.parseBoolean(properties.getProperty(prefix+"noFallBack"));
        		if (properties.getProperty(prefix+"usePatternAlpha")!=null)
        			this.usePatternAlpha=Boolean.parseBoolean(properties.getProperty(prefix+"usePatternAlpha"));
        		if (properties.getProperty(prefix+"targetShowPerImage")!=null)
        			this.targetShowPerImage=Boolean.parseBoolean(properties.getProperty(prefix+"targetShowPerImage"));
        		if (properties.getProperty(prefix+"targetShowThisCorrection")!=null)
        			this.targetShowThisCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"targetShowThisCorrection"));
        		if (properties.getProperty(prefix+"targetApplyCorrection")!=null)
        			this.targetApplyCorrection=Boolean.parseBoolean(properties.getProperty(prefix+"targetApplyCorrection"));
        		if (properties.getProperty(prefix+"targetCorrectionScale")!=null)
        			this.targetCorrectionScale=Double.parseDouble(properties.getProperty(prefix+"targetCorrectionScale"));
        		if (properties.getProperty(prefix+"sensorExtrapolateDiff")!=null)
        			this.sensorExtrapolateDiff=Boolean.parseBoolean(properties.getProperty(prefix+"sensorExtrapolateDiff"));
        		if (properties.getProperty(prefix+"sensorShrinkBlurComboSigma")!=null)
        			this.sensorShrinkBlurComboSigma=Double.parseDouble(properties.getProperty(prefix+"sensorShrinkBlurComboSigma"));
        		if (properties.getProperty(prefix+"sensorShrinkBlurComboLevel")!=null)
        			this.sensorShrinkBlurComboLevel=Double.parseDouble(properties.getProperty(prefix+"sensorShrinkBlurComboLevel"));
        		if (properties.getProperty(prefix+"sensorAlphaThreshold")!=null)
        			this.sensorAlphaThreshold=Double.parseDouble(properties.getProperty(prefix+"sensorAlphaThreshold"));
        		if (properties.getProperty(prefix+"sensorStep")!=null)
        			this.sensorStep=Double.parseDouble(properties.getProperty(prefix+"sensorStep"));
        		if (properties.getProperty(prefix+"sensorInterpolationSigma")!=null)
        			this.sensorInterpolationSigma=Double.parseDouble(properties.getProperty(prefix+"sensorInterpolationSigma"));
        		if (properties.getProperty(prefix+"sensorTangentialRadius")!=null)
        			this.sensorTangentialRadius=Double.parseDouble(properties.getProperty(prefix+"sensorTangentialRadius"));
        		if (properties.getProperty(prefix+"sensorScanDistance")!=null)
        			this.sensorScanDistance=Integer.parseInt(properties.getProperty(prefix+"sensorScanDistance"));
        		if (properties.getProperty(prefix+"sensorResultDistance")!=null)
        			this.sensorResultDistance=Integer.parseInt(properties.getProperty(prefix+"sensorResultDistance"));
        		if (properties.getProperty(prefix+"sensorInterpolationDegree")!=null)
        			this.sensorInterpolationDegree=Integer.parseInt(properties.getProperty(prefix+"sensorInterpolationDegree"));
        		if (properties.getProperty(prefix+"flatFieldSerNumber")!=null)
        			this.flatFieldSerNumber=Integer.parseInt(properties.getProperty(prefix+"flatFieldSerNumber"));
        		if (properties.getProperty(prefix+"flatFieldReferenceStation")!=null)
        			this.flatFieldReferenceStation=Integer.parseInt(properties.getProperty(prefix+"flatFieldReferenceStation"));
        		if (properties.getProperty(prefix+"flatFieldShrink")!=null)
        			this.flatFieldShrink=Double.parseDouble(properties.getProperty(prefix+"flatFieldShrink"));
        		if (properties.getProperty(prefix+"flatFieldNonVignettedRadius")!=null)
        			this.flatFieldNonVignettedRadius=Double.parseDouble(properties.getProperty(prefix+"flatFieldNonVignettedRadius"));
        		if (properties.getProperty(prefix+"flatFieldMinimalAlpha")!=null)
        			this.flatFieldMinimalAlpha=Double.parseDouble(properties.getProperty(prefix+"flatFieldMinimalAlpha"));
        		if (properties.getProperty(prefix+"flatFieldMinimalContrast")!=null)
        			this.flatFieldMinimalContrast=Double.parseDouble(properties.getProperty(prefix+"flatFieldMinimalContrast"));
        		if (properties.getProperty(prefix+"flatFieldMinimalAccumulate")!=null)
        			this.flatFieldMinimalAccumulate=Double.parseDouble(properties.getProperty(prefix+"flatFieldMinimalAccumulate"));
        		if (properties.getProperty(prefix+"flatFieldShrinkForMatching")!=null)
        			this.flatFieldShrinkForMatching=Double.parseDouble(properties.getProperty(prefix+"flatFieldShrinkForMatching"));
        		if (properties.getProperty(prefix+"flatFieldMaxRelDiff")!=null)
        			this.flatFieldMaxRelDiff=Double.parseDouble(properties.getProperty(prefix+"flatFieldMaxRelDiff"));
        		if (properties.getProperty(prefix+"flatFieldShrinkMask")!=null)
        			this.flatFieldShrinkMask=Integer.parseInt(properties.getProperty(prefix+"flatFieldShrinkMask"));
        		if (properties.getProperty(prefix+"flatFieldFadeBorder")!=null)
        			this.flatFieldFadeBorder=Double.parseDouble(properties.getProperty(prefix+"flatFieldFadeBorder"));
        		if (properties.getProperty(prefix+"flatFieldResetMask")!=null)
        			this.flatFieldResetMask=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldResetMask"));
        		if (properties.getProperty(prefix+"flatFieldShowSensorMasks")!=null)
        			this.flatFieldShowSensorMasks=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldShowSensorMasks"));
        		if (properties.getProperty(prefix+"flatFieldShowIndividual")!=null)
        			this.flatFieldShowIndividual=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldShowIndividual"));
        		if (properties.getProperty(prefix+"flatFieldShowResult")!=null)
        			this.flatFieldShowResult=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldShowResult"));
        		if (properties.getProperty(prefix+"flatFieldApplyResult")!=null)
        			this.flatFieldApplyResult=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldApplyResult"));
        		if (properties.getProperty(prefix+"flatFieldUseInterpolate")!=null)
        			this.flatFieldUseInterpolate=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldUseInterpolate"));
        		if (properties.getProperty(prefix+"flatFieldMaskThresholdOcclusion")!=null)
        			this.flatFieldMaskThresholdOcclusion=Double.parseDouble(properties.getProperty(prefix+"flatFieldMaskThresholdOcclusion"));
        		if (properties.getProperty(prefix+"flatFieldShrinkOcclusion")!=null)
        			this.flatFieldShrinkOcclusion=Integer.parseInt(properties.getProperty(prefix+"flatFieldShrinkOcclusion"));
        		if (properties.getProperty(prefix+"flatFieldFadeOcclusion")!=null)
        			this.flatFieldFadeOcclusion=Double.parseDouble(properties.getProperty(prefix+"flatFieldFadeOcclusion"));
        		if (properties.getProperty(prefix+"flatFieldIgnoreSensorFlatField")!=null)
        			this.flatFieldIgnoreSensorFlatField=Boolean.parseBoolean(properties.getProperty(prefix+"flatFieldIgnoreSensorFlatField"));
        		if (properties.getProperty(prefix+"repeatFlatFieldSensor")!=null)
        			this.repeatFlatFieldSensor=Integer.parseInt(properties.getProperty(prefix+"repeatFlatFieldSensor"));


        		if (properties.getProperty(prefix+"specularHighPassSigma")!=null)
        			this.specularHighPassSigma=Double.parseDouble(properties.getProperty(prefix+"specularHighPassSigma"));
        		if (properties.getProperty(prefix+"specularLowPassSigma")!=null)
        			this.specularLowPassSigma=Double.parseDouble(properties.getProperty(prefix+"specularLowPassSigma"));
        		if (properties.getProperty(prefix+"specularDiffFromAverageThreshold")!=null)
        			this.specularDiffFromAverageThreshold=Double.parseDouble(properties.getProperty(prefix+"specularDiffFromAverageThreshold"));
        		if (properties.getProperty(prefix+"specularNumIter")!=null)
        			this.specularNumIter=Integer.parseInt(properties.getProperty(prefix+"specularNumIter"));
        		if (properties.getProperty(prefix+"specularApplyNewWeights")!=null)
        			this.specularApplyNewWeights=Boolean.parseBoolean(properties.getProperty(prefix+"specularApplyNewWeights"));
        		if (properties.getProperty(prefix+"specularPositiveDiffOnly")!=null)
        			this.specularPositiveDiffOnly=Boolean.parseBoolean(properties.getProperty(prefix+"specularPositiveDiffOnly"));
        		if (properties.getProperty(prefix+"specularShowDebug")!=null)
        			this.specularShowDebug=Integer.parseInt(properties.getProperty(prefix+"specularShowDebug"));
        	}

        	public int showDialog(String title, int parMask, int numSeries, double [] averageRGB) {
        		// sensor 0xfff, grid - 0xcc0 // cannot show result (cumulative) grid correction
        		GenericDialog gd = new GenericDialog(title);
        		if (numSeries>=0) gd.addNumericField("Fitting strategy series number (selects images to process) ", numSeries,0);
    			if ((parMask&0x200000)!=0) gd.addNumericField("Repeat target/sensor flat-field calculation", this.repeatFlatFieldSensor,0,3,"times");
    			
    			
    			//sensorExtrapolateDiff
    			if ((parMask&0x80000)!=0) gd.addCheckbox("Extrapolate incremetal (not checked - cumulative) correction",  this.sensorExtrapolateDiff);
        		if ((parMask&0x80000) !=0) gd.addNumericField("Shrink-blur combined sigma", this.sensorShrinkBlurComboSigma, 2,6,"sensor pixels"); // 20
        		if ((parMask&0x80000) !=0) gd.addNumericField("Shrink-blur combined level (-1..+1)", this.sensorShrinkBlurComboLevel, 2,6,""); // 0
        		if ((parMask&0x80000) !=0) gd.addNumericField("Combined alpha extrapolation threshold", this.sensorAlphaThreshold, 2,6,""); // normalize later?
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation seed step",this.sensorStep, 1,4,"decimated pixels");
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation gaussian sigma", this.sensorInterpolationSigma, 2,6,"sensor pixels"); // 50
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation effective radius (doubling sigma in tangential direction)", this.sensorTangentialRadius, 2,6,"fraction of full image radius");
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation half-square side for polynomial approximation", this.sensorScanDistance, 0,3,"sensor pixels");
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation half-square side for extrapolation", this.sensorResultDistance, 0,3,"sensor pixels");
        		if ((parMask&0x80000) !=0) gd.addNumericField("Extrapolation polynomial degree", this.sensorInterpolationDegree, 0,1,"");
        		
    			if ((parMask&0x100000)!=0) gd.addNumericField("Fitting series number (to select images), negative - use all enabled images", this.flatFieldSerNumber,0);
    			if ((parMask&0x100000)!=0) gd.addNumericField("Reference station number (unity target brightness)", this.flatFieldReferenceStation,0);
    			if ((parMask&0x100000)!=0) gd.addNumericField("Shrink sensor mask",    this.flatFieldShrink, 1,6,"sensor pix");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Non-vignetted radius", this.flatFieldNonVignettedRadius, 1,6,"sensor pix");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Minimal alpha",        100.0*this.flatFieldMinimalAlpha, 3,7,"%");
    			
    			if ((parMask&0x100000)!=0) gd.addNumericField("Minimal contrast (occlusion detection)", this.flatFieldMinimalContrast, 3,7,"(0 .. ~0.8");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Minimal alpha for accumulation", 100.0*this.flatFieldMinimalAccumulate, 3,7,"%");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Shrink pattern for matching", this.flatFieldShrinkForMatching, 3,7,"grid nodes");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Maximal relative difference between nodes", 100.0*this.flatFieldMaxRelDiff, 3,7,"%");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Shrink pattern border", this.flatFieldShrinkMask, 0,3,"grid nodes");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Fade pattern border", this.flatFieldFadeBorder, 3,7,"grid nodes");
    			if ((parMask&0x100000)!=0) gd.addMessage("Update pattern white balance (if the illumination is yellowish, increase red and green here)");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Average grid RED   (1.0 for white)",  averageRGB[0], 3,5,"x"); //
    			if ((parMask&0x100000)!=0) gd.addNumericField("Average grid GREEN (1.0 for white)",  averageRGB[1], 3,5,"x"); //
    			if ((parMask&0x100000)!=0) gd.addNumericField("Average grid BLUE  (1.0 for white)",  averageRGB[2], 3,5,"x"); //
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Reset pattern mask",               this.flatFieldResetMask);
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Show non-vignetting sensor masks", this.flatFieldShowSensorMasks);
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Show per-sensor patterns",         this.flatFieldShowIndividual);
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Show result mask",                 this.flatFieldShowResult);
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Apply pattern flat field and mask",this.flatFieldApplyResult);
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Use interpolation for sensor correction",this.flatFieldUseInterpolate);
    			if ((parMask&0x100000)!=0) gd.addNumericField("Suspect occlusion only if grid is missing in the area where sensor mask is above this threshold",100.0* this.flatFieldMaskThresholdOcclusion, 3,7,"%");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Expand suspected occlusion  area", this.flatFieldShrinkOcclusion, 0,3,"grid nodes");
    			if ((parMask&0x100000)!=0) gd.addNumericField("Fade grid on image (occlusion handling)", this.flatFieldFadeOcclusion, 3,7,"grid nodes");
    			if ((parMask&0x100000)!=0) gd.addCheckbox("Ignore existent sensor flat-field calibration",this.flatFieldIgnoreSensorFlatField);
    			
    			if ((parMask&0x400000)!=0) gd.addMessage("Specular reflections removal parameters:");
    			if ((parMask&0x400000)!=0) gd.addCheckbox("Apply new (after removal of specular reflections) weights",           this.specularApplyNewWeights);
    			if ((parMask&0x400000)!=0) gd.addCheckbox("Process only positive difference from average",                       this.specularPositiveDiffOnly);
    			if ((parMask&0x400000)!=0) gd.addNumericField("High-pass sigma for difference from average (to detect specular)",this.specularHighPassSigma, 3,7,"pix");
    			if ((parMask&0x400000)!=0) gd.addNumericField("Low-pass sigma for difference from average (to detect specular)",this.specularLowPassSigma, 3,7,"pix");
    			
    			if ((parMask&0x400000)!=0) gd.addNumericField("Difference from average threshold",                          100.0*this.specularDiffFromAverageThreshold, 3,7,"%");
    			if ((parMask&0x400000)!=0) gd.addNumericField("Number of iterations for calculating average",                    this.specularNumIter, 0);

    			if ((parMask&0x400000)!=0) gd.addNumericField("Debug show mode (0 - off, 1 - last iteration only, 2 - all iterations)",this.specularShowDebug, 0);
    			

        		if ((parMask &     1) !=0) gd.addCheckbox    ("Extrapolate correction results", this.extrapolate);
        		if ((parMask &     2) !=0) gd.addNumericField("Threshold alpha (discard pixels with mask below that value)", this.alphaThreshold,3);
        		if ((parMask &0x8000) !=0) gd.addNumericField("Fat zero for color trasfer functions", this.fatZero,3);
        		if ((parMask &     4) !=0) gd.addNumericField("Fitting radius for extrapolation, Gaussian weight function sigma (in non-decimated pixels) ",    this.extrapolationSigma,3);
        		if ((parMask &     8) !=0) gd.addNumericField("Fitting scan half-size of the square, in multiples of Fitting Radius", this.extrapolationKSigma,3);
        		if ((parMask &  0x10) !=0) gd.addCheckbox    ("Apply smoothing to the correction results", this.smoothCorrection);
        		if ((parMask &  0x20) !=0) gd.addNumericField("Smoothing sigma, in non-decimated pixels",  this.smoothSigma,3);
        		if ((parMask &  0x40) !=0) gd.addCheckbox    ("Apply correction",                          this.applyCorrection);
        		if ((parMask&0x40000) !=0) gd.addCheckbox    ("Apply correction",                          this.targetApplyCorrection);
        		if ((parMask &0x4000) !=0) gd.addCheckbox    ("Apply flat-field correction",               this.applyFlatField);
        		if ((parMask &  0x80) !=0) gd.addNumericField("Scale correction before applying",          this.correctionScale,3);
        		if ((parMask&0x40000) !=0) gd.addNumericField("Scale correction before applying",          this.targetCorrectionScale,3);
        		if ((parMask & 0x100) !=0) gd.addCheckbox    ("Show result (cumulative) correction",       this.showCumulativeCorrection);
        		if ((parMask & 0x200) !=0) gd.addCheckbox    ("Show additional correction before blurring",this.showUnfilteredCorrection);
        		if ((parMask & 0x200) !=0) gd.addCheckbox    ("Show correction extrapolatiuon",            this.showExtrapolationCorrection);
        		if ((parMask & 0x400) !=0) gd.addCheckbox    ("Show this (additional) correction",         this.showThisCorrection);
        		if ((parMask&0x40000) !=0) gd.addCheckbox    ("Show this (additional) correction",         this.targetShowThisCorrection);
        		if ((parMask & 0x800) !=0) gd.addCheckbox    ("Show individual, per-image residuals",      this.showPerImage);
        		if ((parMask&0x40000) !=0) gd.addCheckbox    ("Show individual, per-image residuals",      this.targetShowPerImage);
        		if ((parMask&0x10000) !=0) gd.addNumericField("Show individual residuals for image number (<0 - all images)", this.showIndividualNumber,0);
        		if ((parMask &0x1000) !=0) gd.addCheckbox    ("Correct patetrn grid node locations in 3d (false - in 2d only)",  this.grid3DCorrection);
        		if ((parMask &0x1000) !=0) gd.addCheckbox    ("Rotate final 3d pattern correction (?)",   this.rotateCorrection);
        		if ((parMask&0x20000) !=0) gd.addNumericField("Maximal Z-axis correction (if more will fall back to 2d correction algorithm)", this.grid3DMaximalZCorr,1,3,"mm");
        		if ((parMask&0x20000) !=0) gd.addCheckbox    ("Use Z-variations of the pattern for different stations",   this.useVariations);
        		if ((parMask&0x20000) !=0) gd.addNumericField("Penalty for different Z for the same target nodes for different stations", 100.0*this.variationPenalty,3,7,"%");
        		if ((parMask&0x20000) !=0) gd.addCheckbox    ("Keep X and Y pattern correction, adjust only Z",this.fixXY);
        		if ((parMask&0x20000) !=0) gd.addCheckbox    ("Reset previous Z variations before calculating the new one",   this.resetVariations);
        		if ((parMask&0x20000) !=0) gd.addCheckbox    ("Do not fall back to 2-d calculation if 3d fails",   this.noFallBack);
        		if ((parMask &0x2000) !=0) gd.addCheckbox    ("Use pattern grid alpha data",  this.usePatternAlpha);
    			WindowTools.addScrollBars(gd);
        		gd.showDialog();
        		if (gd.wasCanceled()) return -1;
        		int selectedSeries=0;
        		if (numSeries>=0)          selectedSeries=          (int) gd.getNextNumber();
    			if ((parMask&0x200000)!=0) this.repeatFlatFieldSensor=  (int) gd.getNextNumber();
    			
        		if ((parMask&0x80000) !=0) this.sensorExtrapolateDiff=          gd.getNextBoolean();
        		if ((parMask&0x80000) !=0) this.sensorShrinkBlurComboSigma=     gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorShrinkBlurComboLevel=     gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorAlphaThreshold=           gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorStep=                     gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorInterpolationSigma=       gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorTangentialRadius=         gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorScanDistance=       (int) gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorResultDistance=     (int) gd.getNextNumber();
        		if ((parMask&0x80000) !=0) this.sensorInterpolationDegree=(int) gd.getNextNumber();

    			if ((parMask&0x100000)!=0) this.flatFieldSerNumber=         (int) gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldReferenceStation=  (int) gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldShrink=                  gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldNonVignettedRadius=      gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldMinimalAlpha=       0.01*gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldMinimalContrast=         gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldMinimalAccumulate=  0.01*gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldShrinkForMatching=       gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldMaxRelDiff=         0.01*gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldShrinkMask=        (int) gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldFadeBorder=              gd.getNextNumber();
    			if ((parMask&0x100000)!=0) averageRGB[0]=                         gd.getNextNumber();
    			if ((parMask&0x100000)!=0) averageRGB[1]=                         gd.getNextNumber();
    			if ((parMask&0x100000)!=0) averageRGB[2]=                         gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldResetMask=               gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldShowSensorMasks=         gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldShowIndividual=          gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldShowResult=              gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldApplyResult=             gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldUseInterpolate=          gd.getNextBoolean();
    			if ((parMask&0x100000)!=0) this.flatFieldMaskThresholdOcclusion=0.01*gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldShrinkOcclusion=   (int) gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldFadeOcclusion=           gd.getNextNumber();
    			if ((parMask&0x100000)!=0) this.flatFieldIgnoreSensorFlatField=   gd.getNextBoolean();
//    			if ((parMask&0x100000)!=0) this.flatFieldUseSelectedChannels=     gd.getNextBoolean();

    			if ((parMask&0x400000)!=0) this.specularApplyNewWeights=              gd.getNextBoolean();
    			if ((parMask&0x400000)!=0) this.specularPositiveDiffOnly=             gd.getNextBoolean();
    			if ((parMask&0x400000)!=0) this.specularHighPassSigma=                gd.getNextNumber();
    			if ((parMask&0x400000)!=0) this.specularLowPassSigma=                gd.getNextNumber();
    			if ((parMask&0x400000)!=0) this.specularDiffFromAverageThreshold=0.01*gd.getNextNumber();;
    			if ((parMask&0x400000)!=0) this.specularNumIter=                (int) gd.getNextNumber();
    			if ((parMask&0x400000)!=0) this.specularShowDebug=              (int) gd.getNextNumber();


        		if ((parMask &     1) !=0) this.extrapolate=              gd.getNextBoolean();
        		if ((parMask &     2) !=0) this.alphaThreshold=           gd.getNextNumber();
        		if ((parMask &0x8000) !=0) this.fatZero=                  gd.getNextNumber();
        		if ((parMask &     4) !=0) this.extrapolationSigma=       gd.getNextNumber();
        		if ((parMask &     8) !=0) this.extrapolationKSigma=      gd.getNextNumber();
        		if ((parMask &  0x10) !=0) this.smoothCorrection=         gd.getNextBoolean();
        		if ((parMask &  0x20) !=0) this.smoothSigma=              gd.getNextNumber();
        		if ((parMask &  0x40) !=0) this.applyCorrection=          gd.getNextBoolean();
        		if ((parMask&0x40000) !=0) this.targetApplyCorrection=    gd.getNextBoolean();
        		if ((parMask &0x4000) !=0) this.applyFlatField=           gd.getNextBoolean();
        		if ((parMask &  0x80) !=0) this.correctionScale=          gd.getNextNumber();
        		if ((parMask&0x40000) !=0) this.targetCorrectionScale=    gd.getNextNumber();
        		if ((parMask & 0x100) !=0) this.showCumulativeCorrection= gd.getNextBoolean();
        		if ((parMask & 0x200) !=0) this.showUnfilteredCorrection= gd.getNextBoolean();
        		if ((parMask & 0x200) !=0) this.showExtrapolationCorrection= gd.getNextBoolean();
        		if ((parMask & 0x400) !=0) this.showThisCorrection=       gd.getNextBoolean();
        		if ((parMask&0x40000) !=0) this.targetShowThisCorrection= gd.getNextBoolean();
        		if ((parMask & 0x800) !=0) this.showPerImage=             gd.getNextBoolean();
        		if ((parMask&0x40000) !=0) this.targetShowPerImage=       gd.getNextBoolean();
        		if ((parMask&0x10000) !=0) this.showIndividualNumber=(int)gd.getNextNumber();
        		if ((parMask &0x1000) !=0) this.grid3DCorrection=         gd.getNextBoolean();
        		if ((parMask &0x1000) !=0) this.rotateCorrection=         gd.getNextBoolean();
        		if ((parMask&0x20000) !=0) this.grid3DMaximalZCorr=       gd.getNextNumber();

        		if ((parMask&0x20000) !=0) this.useVariations=            gd.getNextBoolean();
        		if ((parMask&0x20000) !=0) this.variationPenalty =   0.01*gd.getNextNumber();
        		if ((parMask&0x20000) !=0) this.fixXY=                    gd.getNextBoolean();
        		if ((parMask&0x20000) !=0) this.resetVariations=          gd.getNextBoolean();
        		if ((parMask&0x20000) !=0) this.noFallBack=               gd.getNextBoolean();
        		if ((parMask &0x2000) !=0) this.usePatternAlpha=          gd.getNextBoolean();
        		return selectedSeries;
        	}
    }
    
    
    public static class EyesisCameraParameters{
    	public double [] goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    	public double [] goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    	public EyesisSubCameraParameters [][] eyesisSubCameras=null;
    	public double [] interAxisDistance; // distance in mm between two goniometer axes
    	public double [] interAxisAngle;    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    	                            // clockwise when eyesis is in 'normal' position, looking to the target
    	public double [] horAxisErrPhi;   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    	public double [] horAxisErrPsi;   // angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
    	public double [] entrancePupilForward; // common to all lenses - distance from the sensor to the lens entrance pupil
    	public double [] centerAboveHorizontal; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each SFE)
    	public double [][] GXYZ=null; // [numStations]{x,y,z}  coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system

    	// non-adjustable parameters, not parts of vector
    	public int     numStations;
    	public double [] stationWeight; // reprojection error weights (close station - relax errors)
    	public boolean isTripod=false; // when true - make goniometerHorizontal rotation around "vertical" axis and "goniometerAxial" - around 
        // rotated horizontal.
    	public int sensorWidth=      2592;
    	public int sensorHeight=     1936;
    	public int    shrinkGridForMask=4; //2; //shrink detected grids by one point for/vert this number of times before calculating masks
    	public double maskBlurSigma=    -3; //2.0;   // blur sensor masks (>0 - pixels, <0 - in grid units)
    	public int    decimateMasks=     1;
    	public double badNodeThreshold=0.1; // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    	public int    maxBadNeighb=      1; // maximal number of bad nodes around the corrected one to fix
    	public int    minimalValidNodes=50; // do not use images with less than this number of non-zero nodes (after all applicable weight masks) 
    	public int    weightMultiImageMode=1; // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set to weightMultiExponent power)
    	public double weightMultiExponent= 1.0;
    	public double weightDiameterExponent=1.0; // if( >0) use grid diameter to scale weights of this image
    	public double weightYtoX=1.0; // relative Y-to-X errors weight (to somewhat compensate for rectabular shape of the sensor)
    	public double minimalGridContrast=0.4; // (normally max ~0.8)
    	public double shrinkBlurSigma = 4.0;
    	public double shrinkBlurLevel = 0.5;
    	public double balanceChannelWeightsMode=-1.0; // <0 - use defaults, 0 - keep, >0 balance by number of points to this power 
    	public double removeOverRMS=2.0;            // error is multiplied by weight function before comparison (more permissive on the borders
    	public double removeOverRMSNonweighted=4.0; // error is not multiplied (no more permissions on tyhe borders
        public int [] extrinsicIndices={6,7,8,9,10,11,12,13,14,15,16}; //support variations
        public double [][] variationsDefaults={
        		null,               // 0
        		null,               // 1
        		null,               // 2
        		null,               // 3
        		null,               // 4
        		null,               // 5
        		{10.0,0.1,0.0,1.0}, // 6 goniometerHorizontal
        		{10.0,0.1,0.0,1.0}, // 7 goniometerAxial
        		{10.0,2.0,0.0,1.0}, // 8 interAxisDistance
        		{10.0,0.2,0.0,1.0}, // 9 interAxisAngle
        		{10.0,0.2,0.0,1.0}, // 10 horAxisErrPhi
        		{10.0,0.2,0.0,1.0}, // 11 horAxisErrPsi
        		{10.0,2.0,0.0,1.0}, // 12 entrancePupilForward
        		{10.0,2.0,0.0,1.0}, // 13 centerAboveHorizontal
        		{10.0,2.0,0.0,1.0}, // 14 GXYZ0
        		{10.0,2.0,0.0,1.0}, // 15 GXYZ1
        		{10.0,2.0,0.0,1.0}  // 16 GXYZ2
        };
        public int    tiltIndex=6;
        private ParameterVariationCosts []  parameterVariationCosts=null;
        
        public boolean isExtrinsic (int index){
        	for (int i=0;i<this.extrinsicIndices.length; i++) if (this.extrinsicIndices[i]==index) return true;
        	return false;
        }
        public boolean isTilt (int index){
        	return (index==this.tiltIndex);
        }
    	public boolean isVarianceCostSet(int index){
    		return (parameterVariationCosts!=null) && (index< parameterVariationCosts.length) && (parameterVariationCosts[index]!=null);
    	}
    	public double varianceCostScale(int index){
    		if (!isVarianceCostSet(index)) return -1.0;
    		return parameterVariationCosts[index].scale;
    	}
    	public double varianceCostVariationAbs(int index){
    		if (!isVarianceCostSet(index)) return -1.0;
    		return parameterVariationCosts[index].variationAbs;
    	}
    	public double varianceCostVariationDiff(int index){
    		if (!isVarianceCostSet(index)) return -1.0;
    		return parameterVariationCosts[index].variationDiff;
    	}
    	public double varianceCostVariationExponent(int index){
    		if (!isVarianceCostSet(index)) return -1.0;
    		return parameterVariationCosts[index].exponent;
    	}
    	public String varianceCostVariationName(int index){
    		if (!isVarianceCostSet(index)) return null;
    		return parameterVariationCosts[index].parName;
    	}
    	public int getVarParsLength(){
    		return parameterVariationCosts.length;
    	}
    	public boolean editCostProperties(
    			int index,
       			String parameterName,
       			String parameterDescription,
       			String parameterUnits) {
    		if (!isExtrinsic(index)) return false; // does not have varaince parameters
    		getVariationCosts();
    		if (parameterVariationCosts[index]==null) parameterVariationCosts[index]=new ParameterVariationCosts(
					this.variationsDefaults[index][0],	
					this.variationsDefaults[index][1],	
					this.variationsDefaults[index][2],	
					this.variationsDefaults[index][3]);
    		return parameterVariationCosts[index].showVarianceDialog(
           			parameterName,
           			parameterDescription,
           			parameterUnits);
    	}
        private ParameterVariationCosts [] getVariationCosts(){
        	if (this.parameterVariationCosts==null){
            	int max=0;
            	for (int i=0;i<this.extrinsicIndices.length; i++) if (this.extrinsicIndices[i]>max) max=this.extrinsicIndices[i];
        		this.parameterVariationCosts=new ParameterVariationCosts [max+1];
        		for (int i=0;i<this.parameterVariationCosts.length;i++) this.parameterVariationCosts[i]=null;
        	}
        	return this.parameterVariationCosts;
        }
        public void getCostsPropertiesXML(String prefix,XMLConfiguration hConfig){
    		getVariationCosts();
    		for (int i=0;i<this.extrinsicIndices.length;i++){
    			int index=this.extrinsicIndices[i];
//    			System.out.println("getCostsPropertiesXML("+prefix+",hconfig) index="+index);
            		if (hConfig.configurationsAt(prefix+"varianceCosts_"+index).size()!=0){
//    				System.out.println("hConfig.configurationAt(prefix+\"varianceCosts_\"+index).isEmpty()=false");
    				this.parameterVariationCosts[index]=new ParameterVariationCosts(
    						this.variationsDefaults[index][0],	
    						this.variationsDefaults[index][1],	
    						this.variationsDefaults[index][2],	
    						this.variationsDefaults[index][3]);
    				boolean isSet= this.parameterVariationCosts[index].getPropertiesXML(prefix+"varianceCosts_"+index+".", hConfig);
    				if (!isSet) this.parameterVariationCosts[index]=null;
    			}
    		}
    	}      
        public void setCostsPropertiesXML(String prefix,XMLConfiguration hConfig){
    		if (this.parameterVariationCosts==null) return;
    		for (int i=0;i<this.extrinsicIndices.length;i++){
    			int index=this.extrinsicIndices[i];
    			if (this.parameterVariationCosts[index]!=null) {
    				hConfig.addProperty(prefix+"varianceCosts_"+index,"");
    				this.parameterVariationCosts[index].setPropertiesXML(prefix+"varianceCosts_"+index+".", hConfig);
    			}
    		}
    	}      
        private class ParameterVariationCosts{
    		public double scale = 1.0; // 1 pixel
    		public double variationAbs  =0.0; // variation of the parameter to cost 1 pixel
    		public double variationDiff =0.0; // variation of the parameter to cost 1 pixel
    		public double exponent=         1.0; // 1.0 - square diff
    		public String parName=null;
 //   		public ParameterVariationCosts(){}
    		public ParameterVariationCosts(
    				double scale,
    				double variationAbs,
    				double variationDiff,
    				double exponent,
    				String parName){
				this.scale=scale;
				this.variationAbs=variationAbs;
				this.variationDiff=variationDiff;
				this.exponent=exponent;
				this.parName=parName;
    		}
    		public ParameterVariationCosts(
    				double scale,
    				double variationAbs,
    				double variationDiff,
    				double exponent){
				this.scale=scale;
				this.variationAbs=variationAbs;
				this.variationDiff=variationDiff;
				this.exponent=exponent;
    		}
    		public ParameterVariationCosts clone(){
    			return new ParameterVariationCosts(
    					this.scale,
    					this.variationAbs,
    					this.variationDiff,
    					this.exponent,
    					this.parName);
    		}

        	public void setPropertiesXML(String prefix,XMLConfiguration hConfig){
        		hConfig.addProperty(prefix+"scale",this.scale+"");
        		hConfig.addProperty(prefix+"variationAbs",this.variationAbs+"");
        		hConfig.addProperty(prefix+"variationDiff",this.variationDiff+"");
        		hConfig.addProperty(prefix+"exponent",this.exponent+"");
        		if (this.parName!=null) hConfig.addProperty(prefix+"parName",this.parName+"");
        	}
        	public boolean getPropertiesXML(String prefix,XMLConfiguration hConfig){
//    			System.out.println("getPropertiesXML("+prefix+",hconfig)");
        		boolean isSet=false;
        		
        		if (hConfig.getString(prefix+"scale")!=null){
//        			System.out.println("getPropertiesXML("+prefix+",hconfig), hConfig.getString(prefix+\"scale\")!=null");
        			this.scale=Double.parseDouble(hConfig.getString(prefix+"scale"));
        			isSet=true;
        		}
        		if (hConfig.getString(prefix+"variationAbs")!=null){
//        			System.out.println("getPropertiesXML("+prefix+",hconfig), hConfig.getString(prefix+\"variationAbs\")!=null");
        			this.variationAbs=Double.parseDouble(hConfig.getString(prefix+"variationAbs"));
        			isSet=true;
        		}
        		if (hConfig.getString(prefix+"variationDiff")!=null){
//       			System.out.println("getPropertiesXML("+prefix+",hconfig), hConfig.getString(prefix+\"variationDiff\")!=null");
        			this.variationDiff=Double.parseDouble(hConfig.getString(prefix+"variationDiff"));
        			isSet=true;
        		}
        		if (hConfig.getString(prefix+"exponent")!=null){
//        			System.out.println("getPropertiesXML("+prefix+",hconfig), hConfig.getString(prefix+\"exponent\")!=null");
        			this.exponent=Double.parseDouble(hConfig.getString(prefix+"exponent"));
        			isSet=true;
        		}
        		if (hConfig.getString(prefix+"parName")!=null){
//        			System.out.println("getPropertiesXML("+prefix+",hconfig), hConfig.getString(prefix+\"parName\")!=null");
        			this.parName=hConfig.getString(prefix+"parName");
        			isSet=true;
        		}
        		return isSet;
        	}

           	public boolean showVarianceDialog(
           			String parameterName,
           			String parameterDescription,
           			String parameterUnits) {
           		String title="Setup costs for image set variance of "+parameterName;
        		GenericDialog gd = new GenericDialog(title);
        		gd.addMessage("Parameter: "+parameterName+" - "+parameterDescription);
				gd.addNumericField("Effective cost of the image set when the "+parameterName+" variance reaches value below",this.scale, 2,4,"pix");
				gd.addNumericField("Variance amount from the average of "+parameterName+" to result in the specified cost", this.variationAbs, 3,8,parameterUnits);
				gd.addNumericField("Variance amount from the p to result in the specified cost", this.variationDiff, 3,8,parameterUnits);
				gd.addNumericField("Exponent of the cost vs. variance (1.0 - squared err "+parameterName+" to result in the specified cost", this.exponent, 3,8,"");
        	    WindowTools.addScrollBars(gd);
        		gd.showDialog();
        		if (gd.wasCanceled()) return false;
				this.scale=         gd.getNextNumber();
				this.variationAbs=  gd.getNextNumber();
				this.variationDiff= gd.getNextNumber();
				this.exponent= gd.getNextNumber();
				this.parName=parameterName;
				return true;
           	}
    	}

    	public int getNumStations() {return this.numStations;}
    	public EyesisCameraParameters () {} // just create new instance, all parameters data will be provided additionally
    	public EyesisCameraParameters (
    			int numStations,
    			boolean isTripod,
    	    	double goniometerHorizontal, // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    	    	double goniometerAxial, // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    			int numSubCameras,
    	    	double interAxisDistance, // distance in mm between two goniometer axes, positive if the vertical axis (when Eyesis is head up) is closer to the target
    	    	double interAxisAngle,    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    	    	                            // clockwise when eyesis is in 'normal' position, looking to the target
    	    	double horAxisErrPhi,   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    	    	double horAxisErrPsi,   // angle in degrees "horizontal" goniometer axis is rotated around moving Z axis (CW looking at target)
    	    	double entrancePupilForward, // common to all lenses - distance from the sensor to the lens entrance pupil
    	    	double centerAboveHorizontal, // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    	    	double GXYZ_0, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    	    	double GXYZ_1, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    	    	double GXYZ_2, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    	    	int sensorWidth,
    	    	int sensorHeight,
    	    	int    shrinkGridForMask, //shrink detected grids by one point for/vert this number of times before calculating masks
    	    	double maskBlurSigma,      // blur sensor masks (in grid units)
    	    	int    decimateMasks,       // reduce masks resolution
    	    	double badNodeThreshold, // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    	    	int    maxBadNeighb, // maximal number of bad nodes around the corrected one to fix
    	    	int minimalValidNodes,
    	    	int    weightMultiImageMode, // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
    	    	double  weightMultiExponent, 
    	    	double  weightDiameterExponent, // if( >0) use grid diameter to scale weights of this image
    	    	double weightYtoX,
    	    	double minimalGridContrast,
    	    	double shrinkBlurSigma,
    	    	double shrinkBlurLevel,
    	    	double balanceChannelWeightsMode,
    	    	double removeOverRMS,
    	    	double removeOverRMSNonweighted
    			){
    		double [] GXYZ={GXYZ_0,GXYZ_1,GXYZ_2};
    		setSameEyesisCameraParameters (
    				numStations,
    				isTripod,
    				goniometerHorizontal, // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    				goniometerAxial, // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    				numSubCameras,
    				interAxisDistance, // distance in mm between two goniometer axes
    				interAxisAngle,    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    				// clockwise when eyesis is in 'normal' position, looking to the target
    				horAxisErrPhi,   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    				horAxisErrPsi,   // angle in degrees "horizontal" goniometer axis is rotated moving Z axis (CW looking at target)
    				entrancePupilForward, // common to all lenses - distance from the sensor to the lens entrance pupil
    				centerAboveHorizontal, // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    				GXYZ, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    				sensorWidth,
    				sensorHeight,
    				shrinkGridForMask, //shrink detected grids by one point for/vert this number of times before calculating masks
    				maskBlurSigma,      // blur sensor masks (in grid units)
    				decimateMasks,       // reduce masks resolution
    				badNodeThreshold, // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    				maxBadNeighb, // maximal number of bad nodes around the corrected one to fix
    				minimalValidNodes,
    				weightMultiImageMode, // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
        	    	weightMultiExponent, 
        	    	weightDiameterExponent, // if( >0) use grid diameter to scale weights of this image
        	    	weightYtoX,
        	    	minimalGridContrast,
        	    	shrinkBlurSigma,
    		    	shrinkBlurLevel,
    		    	balanceChannelWeightsMode,
        	    	removeOverRMS,
        	    	removeOverRMSNonweighted
    		);
    	}
    	public EyesisCameraParameters (
    			int numStations,
    			boolean isTripod,
    	    	double goniometerHorizontal, // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    	    	double goniometerAxial, // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    			int numSubCameras,
    	    	double interAxisDistance, // distance in mm between two goniometer axes
    	    	double interAxisAngle,    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    	    	                            // clockwise when eyesis is in 'normal' position, looking to the target
    	    	double horAxisErrPhi,   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    	    	double horAxisErrPsi,   // angle in degrees "horizontal" goniometer axis is rotated moving Z axis (CW looking at target)
    	    	double entrancePupilForward, // common to all lenses - distance from the sensor to the lens entrance pupil
    	    	double centerAboveHorizontal, // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    	    	double [] GXYZ, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    	    	int sensorWidth,
    	    	int sensorHeight,
    	    	int    shrinkGridForMask, //shrink detected grids by one point for/vert this number of times before calculating masks
    	    	double maskBlurSigma,      // blur sensor masks (in grid units)
    	    	int    decimateMasks,       // reduce masks resolution
    	    	double badNodeThreshold, // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    	    	int    maxBadNeighb, // maximal number of bad nodes around the corrected one to fix
    	    	int    minimalValidNodes,
    	    	int    weightMultiImageMode, // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
    	    	double  weightMultiExponent, 
    	    	double  weightDiameterExponent, // if( >0) use grid diameter to scale weights of this image
    	    	double weightYtoX,
    	    	double minimalGridContrast,
    	    	double shrinkBlurSigma,
    	    	double shrinkBlurLevel,
    	    	double balanceChannelWeightsMode,
    	    	double removeOverRMS,
    	    	double removeOverRMSNonweighted
    			){
    		setSameEyesisCameraParameters (
    				numStations,
    				isTripod,
    				goniometerHorizontal, // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    				goniometerAxial, // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    				numSubCameras,
    				interAxisDistance, // distance in mm between two goniometer axes
    				interAxisAngle,    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    				// clockwise when eyesis is in 'normal' position, looking to the target
    				horAxisErrPhi,   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    				horAxisErrPsi,   // angle in degrees "horizontal" goniometer axis is rotated moving Z axis (CW looking at target)
    				entrancePupilForward, // common to all lenses - distance from the sensor to the lens entrance pupil
    				centerAboveHorizontal, // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    				GXYZ, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    				sensorWidth,
    				sensorHeight,
    				shrinkGridForMask, //shrink detected grids by one point for/vert this number of times before calculating masks
    				maskBlurSigma,      // blur sensor masks (in grid units)
    				decimateMasks,       // reduce masks resolution
    				badNodeThreshold, // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    				maxBadNeighb, // maximal number of bad nodes around the corrected one to fix
    				minimalValidNodes,
    				weightMultiImageMode, // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
        	    	weightMultiExponent, 
        	    	weightDiameterExponent, // if( >0) use grid diameter to scale weights of this image
        	    	weightYtoX,
        	    	minimalGridContrast,
        	    	shrinkBlurSigma,
    		    	shrinkBlurLevel,
    		    	balanceChannelWeightsMode,
        	    	removeOverRMS,
        	    	removeOverRMSNonweighted
    		);
    	}
    	void setSameEyesisCameraParameters (
    			int numStations,
    			boolean isTripod,
    	    	double goniometerHorizontal, // goniometer rotation around "horizontal" axis (tilting from the target - positive)
    	    	double goniometerAxial, // goniometer rotation around Eyesis axis (clockwise in plan - positive 
    			int numSubCameras,
    	    	double interAxisDistance, // distance in mm between two goniometer axes
    	    	double interAxisAngle,    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    	    	                            // clockwise when eyesis is in 'normal' position, looking to the target
    	    	double horAxisErrPhi,   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    	    	double horAxisErrPsi,   // angle in degrees "horizontal" goniometer axis is rotated moving Z axis (CW looking at target)
    	    	double entrancePupilForward, // common to all lenses - distance from the sensor to the lens entrance pupil
    	    	double centerAboveHorizontal, // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    	    	double [] GXYZ, // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
    	    	int sensorWidth,
    	    	int sensorHeight,
    	    	int    shrinkGridForMask, //shrink detected grids by one point for/vert this number of times before calculating masks
    	    	double maskBlurSigma,      // blur sensor masks (in grid units)
    	    	int    decimateMasks,       // reduce masks resolution
    	    	double badNodeThreshold, // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    	    	int    maxBadNeighb, // maximal number of bad nodes around the corrected one to fix
    	    	int    minimalValidNodes,
    	    	int    weightMultiImageMode, // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
    	    	double  weightMultiExponent, 
    	    	double  weightDiameterExponent, // if( >0) use grid diameter to scale weights of this image
    	    	double weightYtoX,
    	    	double minimalGridContrast,
    	    	double shrinkBlurSigma,
    	    	double shrinkBlurLevel,
    	    	double balanceChannelWeightsMode,
    	    	double removeOverRMS,
    	    	double removeOverRMSNonweighted
    			){
    		this.numStations=numStations;
    		this.isTripod=isTripod;
	    	this.sensorWidth=sensorWidth;
	    	this.sensorHeight=sensorHeight;
	    	this.shrinkGridForMask=shrinkGridForMask; //shrink detected grids by one point for/vert this number of times before calculating masks
	    	this.maskBlurSigma=maskBlurSigma;      // blur sensor masks (in grid units)
	    	this.decimateMasks=decimateMasks;
	    	this.badNodeThreshold=badNodeThreshold; // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
	    	this.maxBadNeighb=maxBadNeighb; // maximal number of bad nodes around the corrected one to fix
	    	this.minimalValidNodes=minimalValidNodes;
	    	this.weightMultiImageMode=weightMultiImageMode; // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
	    	this.weightMultiExponent=weightMultiExponent; 
	    	this.weightDiameterExponent=weightDiameterExponent; // if( >0) use grid diameter to scale weights of this image
	    	this.weightYtoX=weightYtoX;
	    	this.minimalGridContrast=minimalGridContrast;
	    	this.goniometerHorizontal=new double[numStations];
    		this.goniometerAxial=new double[numStations]; 
	    	this.interAxisDistance=new double[numStations];
	    	this.interAxisAngle=new double[numStations];
	    	this.horAxisErrPhi=new double[numStations];
	    	this.horAxisErrPsi=new double[numStations];
	    	this.entrancePupilForward=new double[numStations]; // common to all lenses - distance from the sensor to the lens entrance pupil
	    	this.centerAboveHorizontal=new double[numStations]; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
	    	this.GXYZ=new double[numStations][];
	    	if (numSubCameras>0) this.eyesisSubCameras=new EyesisSubCameraParameters[numStations][];
	    	this.stationWeight=new double[numStations];
	    	for (int numStation=0;numStation<numStations;numStation++) {
	    		this.stationWeight[numStation]=1.0;
	    		this.goniometerHorizontal[numStation]=goniometerHorizontal;
	    		this.goniometerAxial[numStation]=goniometerAxial; 
		    	this.interAxisDistance[numStation]=interAxisDistance;
		    	this.interAxisAngle[numStation]=interAxisAngle;
		    	this.horAxisErrPhi[numStation]=horAxisErrPhi;
		    	this.horAxisErrPsi[numStation]=horAxisErrPsi;
		    	this.entrancePupilForward[numStation]=entrancePupilForward; // common to all lenses - distance from the sensor to the lens entrance pupil
		    	this.centerAboveHorizontal[numStation]=centerAboveHorizontal; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each
		    	this.GXYZ[numStation]=new double [3];
		    	for (int i=0;i<3;i++) this.GXYZ[numStation][i]=GXYZ[i];
		    	if (numSubCameras>0) initSubCameras(numStation,numSubCameras);
	    	}
    	}
    	
    	public EyesisCameraParameters clone() {
    		EyesisCameraParameters result= new EyesisCameraParameters ();
    		copyData(
        			this.numStations,
        			this,
        			result);
    		return result;
    	}

    	
    	/**
    	 * Capoy parameters from source EysesisCamerParameters to destination, trimming/expanding nu,ber of stations
    	 * @param newNumStations new number of stations
    	 * @param source source EysesisCamerParameters
    	 * @param destination destination EysesisCamerParameters
    	 */
    	public void copyData(
    			int newNumStations,
    			EyesisCameraParameters source,
    			EyesisCameraParameters destination) {
    		destination.numStations=newNumStations;
    		destination.isTripod=source.isTripod;
    		destination.sensorWidth=source.sensorWidth;
    		destination.sensorHeight=source.sensorHeight;
    		destination.shrinkGridForMask=source.shrinkGridForMask; //shrink detected grids by one point for/vert this number of times before calculating masks
    		destination.maskBlurSigma=source.maskBlurSigma;      // blur sensor masks (in grid units)
    		destination.decimateMasks=source.decimateMasks;
    		destination.badNodeThreshold=source.badNodeThreshold; // filter out grid nodes with difference from quadratically predicted from 8 neighbors in pixels
    		destination.maxBadNeighb=source.maxBadNeighb; // maximal number of bad nodes around the corrected one to fix
    		destination.minimalValidNodes=source.minimalValidNodes;
    		destination.weightMultiImageMode=source.weightMultiImageMode; // increase weight for multi-image sets (0 - do not increase, 1 - multiply by number of images in a set)
    		destination.weightMultiExponent= source.weightMultiExponent; // if( >0) use grid diameter to scale weights of this image
    		destination.weightDiameterExponent=source.weightDiameterExponent;
    		destination.weightYtoX=source.weightYtoX;
	    	destination.minimalGridContrast=source.minimalGridContrast;
	    	destination.shrinkBlurSigma=source.shrinkBlurSigma;
	    	destination.shrinkBlurLevel=source.shrinkBlurLevel;
	    	destination.balanceChannelWeightsMode=source.balanceChannelWeightsMode;
	    	destination.removeOverRMSNonweighted=source.removeOverRMSNonweighted;
    		destination.goniometerHorizontal=new double[destination.numStations];
    		destination.goniometerAxial=new double[destination.numStations]; 
    		destination.interAxisDistance=new double[destination.numStations];
    		destination.interAxisAngle=new double[destination.numStations];
    		destination.horAxisErrPhi=new double[destination.numStations];
    		destination.horAxisErrPsi=new double[destination.numStations];
    		destination.entrancePupilForward=new double[destination.numStations]; // common to all lenses - distance from the sensor to the lens entrance pupil
    		destination.centerAboveHorizontal=new double[destination.numStations]; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each 
    		destination.GXYZ=new double[destination.numStations][];
    		if (source.eyesisSubCameras!=null) destination.eyesisSubCameras=new EyesisSubCameraParameters[destination.numStations][];
    		else destination.eyesisSubCameras=null;
    		destination.stationWeight=new double[destination.numStations];
    		for (int numStation=0;numStation<destination.numStations;numStation++) {
    			int srcNumStation=(numStation<source.numStations)?numStation:(source.numStations-1);
    			destination.stationWeight[numStation]=source.stationWeight[srcNumStation];
    			destination.goniometerHorizontal[numStation]=source.goniometerHorizontal[srcNumStation];
    			destination.goniometerAxial[numStation]=source.goniometerAxial[srcNumStation]; 
    			destination.interAxisDistance[numStation]=source.interAxisDistance[srcNumStation];
    			destination.interAxisAngle[numStation]=source.interAxisAngle[srcNumStation];
    			destination.horAxisErrPhi[numStation]=source.horAxisErrPhi[srcNumStation];
    			destination.horAxisErrPsi[numStation]=source.horAxisErrPsi[srcNumStation];
    			destination.entrancePupilForward[numStation]=source.entrancePupilForward[srcNumStation]; // common to all lenses - distance from the sensor to the lens entrance pupil
    			destination.centerAboveHorizontal[numStation]=source.centerAboveHorizontal[srcNumStation]; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each
    			destination.GXYZ[numStation]=new double [3];
    			for (int i=0;i<3;i++) destination.GXYZ[numStation][i]=source.GXYZ[srcNumStation][i];
    			if (destination.eyesisSubCameras!=null){
    				destination.eyesisSubCameras[numStation]=new EyesisSubCameraParameters[source.eyesisSubCameras[srcNumStation].length];
    				for (int i=0; i<destination.eyesisSubCameras[numStation].length;i++) if (source.eyesisSubCameras[srcNumStation][i]!=null){
    					destination.eyesisSubCameras[numStation][i]=source.eyesisSubCameras[srcNumStation][i].clone();
    				}
    			}
    		}
    	}
    	
    	public void setProperties(String prefix,Properties properties){
    		properties.setProperty(prefix+"isTripod",this.isTripod+"");
    		properties.setProperty(prefix+"sensorWidth",this.sensorWidth+"");
    		properties.setProperty(prefix+"sensorHeight",this.sensorHeight+"");
    		properties.setProperty(prefix+"shrinkGridForMask",this.shrinkGridForMask+"");
    		properties.setProperty(prefix+"maskBlurSigma",this.maskBlurSigma+"");
    		properties.setProperty(prefix+"decimateMasks",this.decimateMasks+"");
    		properties.setProperty(prefix+"badNodeThreshold",this.badNodeThreshold+"");
    		properties.setProperty(prefix+"maxBadNeighb",this.maxBadNeighb+"");
    		properties.setProperty(prefix+"minimalValidNodes",this.minimalValidNodes+"");
    		properties.setProperty(prefix+"weightMultiImageMode",this.weightMultiImageMode+"");
    		properties.setProperty(prefix+"weightMultiExponent",this.weightMultiExponent+"");
    		properties.setProperty(prefix+"weightDiameterExponent",this.weightDiameterExponent+"");
    		properties.setProperty(prefix+"weightYtoX",this.weightYtoX+"");
    		properties.setProperty(prefix+"minimalGridContrast",this.minimalGridContrast+"");
    		properties.setProperty(prefix+"shrinkBlurSigma",this.shrinkBlurSigma+"");
    		properties.setProperty(prefix+"shrinkBlurLevel",this.shrinkBlurLevel+"");
    		properties.setProperty(prefix+"balanceChannelWeightsMode",this.balanceChannelWeightsMode+"");
    		properties.setProperty(prefix+"removeOverRMS",this.removeOverRMS+"");
    		properties.setProperty(prefix+"removeOverRMSNonweighted",this.removeOverRMSNonweighted+"");
			properties.setProperty(prefix+"numSubCameras",this.eyesisSubCameras[0].length+"");
    		properties.setProperty(prefix+"numStations",this.numStations+"");
    		for (int numStation=0;numStation<this.numStations;numStation++){
    			properties.setProperty(prefix+"stationWeight_"+numStation,this.stationWeight[numStation]+"");
    			properties.setProperty(prefix+"goniometerHorizontal_"+numStation,this.goniometerHorizontal[numStation]+"");
    			properties.setProperty(prefix+"goniometerAxial_"+numStation,this.goniometerAxial[numStation]+"");
    			properties.setProperty(prefix+"interAxisDistance_"+numStation,this.interAxisDistance[numStation]+"");
    			properties.setProperty(prefix+"interAxisAngle_"+numStation,this.interAxisAngle[numStation]+"");
    			properties.setProperty(prefix+"horAxisErrPhi_"+numStation,this.horAxisErrPhi[numStation]+"");
    			properties.setProperty(prefix+"horAxisErrPsi_"+numStation,this.horAxisErrPsi[numStation]+"");
    			properties.setProperty(prefix+"entrancePupilForward_"+numStation,this.entrancePupilForward[numStation]+"");
    			properties.setProperty(prefix+"centerAboveHorizontal_"+numStation,this.centerAboveHorizontal[numStation]+"");
    			properties.setProperty(prefix+"GXYZ_0_"+numStation,this.GXYZ[numStation][0]+"");
    			properties.setProperty(prefix+"GXYZ_1_"+numStation,this.GXYZ[numStation][1]+"");
    			properties.setProperty(prefix+"GXYZ_2_"+numStation,this.GXYZ[numStation][2]+"");
    			for (int i=0;i<this.eyesisSubCameras[numStation].length;i++) {
    				this.eyesisSubCameras[numStation][i].setProperties(prefix+numStation+"_subCamera_"+i+'.',properties);
    			}
    		}
 //   		setCostsProperties(prefix,properties);
    	}
    	public void getProperties(String prefix,Properties properties){
    		if (properties.getProperty(prefix+"isTripod")!=null)
    			this.isTripod=Boolean.parseBoolean(properties.getProperty(prefix+"isTripod"));
    		if (properties.getProperty(prefix+"sensorWidth")!=null)
    			this.sensorWidth=Integer.parseInt(properties.getProperty(prefix+"sensorWidth"));
    		if (properties.getProperty(prefix+"sensorHeight")!=null)
    			this.sensorHeight=Integer.parseInt(properties.getProperty(prefix+"sensorHeight"));
    		if (properties.getProperty(prefix+"shrinkGridForMask")!=null)
    			this.shrinkGridForMask=Integer.parseInt(properties.getProperty(prefix+"shrinkGridForMask"));
    		if (properties.getProperty(prefix+"maskBlurSigma")!=null)
    			this.maskBlurSigma=Double.parseDouble(properties.getProperty(prefix+"maskBlurSigma"));
    		if (properties.getProperty(prefix+"decimateMasks")!=null)
    			this.decimateMasks=Integer.parseInt(properties.getProperty(prefix+"decimateMasks"));

    		if (properties.getProperty(prefix+"badNodeThreshold")!=null)
    			this.badNodeThreshold=Double.parseDouble(properties.getProperty(prefix+"badNodeThreshold"));
    		if (properties.getProperty(prefix+"maxBadNeighb")!=null)
    			this.maxBadNeighb=Integer.parseInt(properties.getProperty(prefix+"maxBadNeighb"));
    		if (properties.getProperty(prefix+"minimalValidNodes")!=null)
    			this.minimalValidNodes=Integer.parseInt(properties.getProperty(prefix+"minimalValidNodes"));
    		if (properties.getProperty(prefix+"weightMultiImageMode")!=null)
    			this.weightMultiImageMode=Integer.parseInt(properties.getProperty(prefix+"weightMultiImageMode"));
    		if (properties.getProperty(prefix+"weightMultiExponent")!=null)
    			this.weightMultiExponent=Double.parseDouble(properties.getProperty(prefix+"weightMultiExponent"));
    		if (properties.getProperty(prefix+"weightDiameterExponent")!=null)
    			this.weightDiameterExponent=Double.parseDouble(properties.getProperty(prefix+"weightDiameterExponent"));
    		if (properties.getProperty(prefix+"weightYtoX")!=null)
    			this.weightYtoX=Double.parseDouble(properties.getProperty(prefix+"weightYtoX"));
    		if (properties.getProperty(prefix+"minimalGridContrast")!=null)
    			this.minimalGridContrast=Double.parseDouble(properties.getProperty(prefix+"minimalGridContrast"));
    		if (properties.getProperty(prefix+"shrinkBlurSigma")!=null)
    			this.shrinkBlurSigma=Double.parseDouble(properties.getProperty(prefix+"shrinkBlurSigma"));
    		if (properties.getProperty(prefix+"shrinkBlurLevel")!=null)
    			this.shrinkBlurLevel=Double.parseDouble(properties.getProperty(prefix+"shrinkBlurLevel"));
    		if (properties.getProperty(prefix+"balanceChannelWeightsMode")!=null)
    			this.balanceChannelWeightsMode=Double.parseDouble(properties.getProperty(prefix+"balanceChannelWeightsMode"));
    		if (properties.getProperty(prefix+"removeOverRMS")!=null)
    			this.removeOverRMS=Double.parseDouble(properties.getProperty(prefix+"removeOverRMS"));
    		if (properties.getProperty(prefix+"removeOverRMSNonweighted")!=null)
    			this.removeOverRMSNonweighted=Double.parseDouble(properties.getProperty(prefix+"removeOverRMSNonweighted"));
    		boolean multiStation=true; // new default
    		int newNumStations=1;
    		int numSubCameras=0;
    		if (properties.getProperty(prefix+"numSubCameras")!=null) numSubCameras=Integer.parseInt(properties.getProperty(prefix+"numSubCameras"));
    		if (properties.getProperty(prefix+"numStations")!=null){
    			newNumStations=Integer.parseInt(properties.getProperty(prefix+"numStations"));
    		}else {
    			multiStation=false; // old config format
    		}
// TODO: trim/expand stations    		
    		updateNumstations (newNumStations);
//    		this.numStations
    		
// read old/new format data
//    		this.numStations=newNumStations;
    		if (multiStation){
    			for (int numStation=0;numStation<this.numStations;numStation++){
        			if (properties.getProperty(prefix+"stationWeight_"+numStation)!=null)
        				this.stationWeight[numStation]=Double.parseDouble(properties.getProperty(prefix+"stationWeight_"+numStation));
        			if (properties.getProperty(prefix+"goniometerHorizontal_"+numStation)!=null)
        				this.goniometerHorizontal[numStation]=Double.parseDouble(properties.getProperty(prefix+"goniometerHorizontal_"+numStation));
        			if (properties.getProperty(prefix+"goniometerAxial_"+numStation)!=null)
        				this.goniometerAxial[numStation]=Double.parseDouble(properties.getProperty(prefix+"goniometerAxial_"+numStation));
        			if (properties.getProperty(prefix+"interAxisDistance_"+numStation)!=null)
        				this.interAxisDistance[numStation]=Double.parseDouble(properties.getProperty(prefix+"interAxisDistance_"+numStation));
        			if (properties.getProperty(prefix+"interAxisAngle_"+numStation)!=null)
        				this.interAxisAngle[numStation]=Double.parseDouble(properties.getProperty(prefix+"interAxisAngle_"+numStation));
        			if (properties.getProperty(prefix+"horAxisErrPhi_"+numStation)!=null)
        				this.horAxisErrPhi[numStation]=Double.parseDouble(properties.getProperty(prefix+"horAxisErrPhi_"+numStation));
        			if (properties.getProperty(prefix+"horAxisErrPsi_"+numStation)!=null)
        				this.horAxisErrPsi[numStation]=Double.parseDouble(properties.getProperty(prefix+"horAxisErrPsi_"+numStation));
        			if (properties.getProperty(prefix+"entrancePupilForward_"+numStation)!=null)
        				this.entrancePupilForward[numStation]=Double.parseDouble(properties.getProperty(prefix+"entrancePupilForward_"+numStation));
        			if (properties.getProperty(prefix+"centerAboveHorizontal_"+numStation)!=null)
        				this.centerAboveHorizontal[numStation]=Double.parseDouble(properties.getProperty(prefix+"centerAboveHorizontal_"+numStation));
        			if (properties.getProperty(prefix+"GXYZ_0_"+numStation)!=null)
        				this.GXYZ[numStation][0]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_0_"+numStation));
        			if (properties.getProperty(prefix+"GXYZ_1_"+numStation)!=null)
        				this.GXYZ[numStation][1]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_1_"+numStation));
        			if (properties.getProperty(prefix+"GXYZ_2_"+numStation)!=null)
        				this.GXYZ[numStation][2]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_2_"+numStation));
        			if (numSubCameras>0) {
        				initSubCameras(numStation, numSubCameras); // set array with default parameters
        				for (int i=0;i<numSubCameras;i++){
        					this.eyesisSubCameras[numStation][i].getProperties(prefix+numStation+"_subCamera_"+i+'.',properties);
        				}
        			}
    			}
    		} else { // read old format
    			if (properties.getProperty(prefix+"goniometerHorizontal")!=null)
    				this.goniometerHorizontal[0]=Double.parseDouble(properties.getProperty(prefix+"goniometerHorizontal"));
    			if (properties.getProperty(prefix+"goniometerAxial")!=null)
    				this.goniometerAxial[0]=Double.parseDouble(properties.getProperty(prefix+"goniometerAxial"));
    			if (properties.getProperty(prefix+"interAxisDistance")!=null)
    				this.interAxisDistance[0]=Double.parseDouble(properties.getProperty(prefix+"interAxisDistance"));
    			if (properties.getProperty(prefix+"interAxisAngle")!=null)
    				this.interAxisAngle[0]=Double.parseDouble(properties.getProperty(prefix+"interAxisAngle"));
    			if (properties.getProperty(prefix+"horAxisErrPhi")!=null)
    				this.horAxisErrPhi[0]=Double.parseDouble(properties.getProperty(prefix+"horAxisErrPhi"));
    			if (properties.getProperty(prefix+"horAxisErrPsi")!=null)
    				this.horAxisErrPsi[0]=Double.parseDouble(properties.getProperty(prefix+"horAxisErrPsi"));
    			if (properties.getProperty(prefix+"entrancePupilForward")!=null)
    				this.entrancePupilForward[0]=Double.parseDouble(properties.getProperty(prefix+"entrancePupilForward"));
    			if (properties.getProperty(prefix+"centerAboveHorizontal")!=null)
    				this.centerAboveHorizontal[0]=Double.parseDouble(properties.getProperty(prefix+"centerAboveHorizontal"));
    			if (properties.getProperty(prefix+"GXYZ_0")!=null)
    				this.GXYZ[0][0]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_0"));
    			if (properties.getProperty(prefix+"GXYZ_1")!=null)
    				this.GXYZ[0][1]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_1"));
    			if (properties.getProperty(prefix+"GXYZ_2")!=null)
    				this.GXYZ[0][2]=Double.parseDouble(properties.getProperty(prefix+"GXYZ_2"));
    			System.out.println(" === numSubCameras="+numSubCameras);
    			if (numSubCameras>0) {
    				initSubCameras(0, numSubCameras); // set array with default parameters
    				for (int i=0;i<numSubCameras;i++){
    					System.out.println("this.eyesisSubCameras[0]["+i+"].getProperties("+prefix+"subCamera_"+i+".,properties);");
    					this.eyesisSubCameras[0][i].getProperties(prefix+"subCamera_"+i+'.',properties);
    				}
    			}
    		}
//    		getCostsProperties(prefix,properties);
    	}
    	void  updateNumstations (int newNumStations){
//    		System.out.println("updateNumstations("+newNumStations+"), was "+this.numStations);
    		if (newNumStations==this.numStations) return;
    		System.out.println("updateNumstations("+newNumStations+"), was "+this.numStations);
    		EyesisCameraParameters newData=this.clone();
    		copyData(
    				newNumStations,
    				newData,
        			this);
    		System.out.println("updateNumstations() after copyData() this.numStations="+this.numStations);
    	}
    	
    	// returns -1 - canceled, 0 - done, >0 - number of sub-camera to edit

    	private int subShowDialog(String title, int nextSubCamera) {
    		GenericDialog gd = new GenericDialog(title);
//        	public double goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
//        	public double goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
//this.isTripod    		
    		gd.addCheckbox("Tripod mode (first vertical axis, then horizontal), changes meaning of the next 2 fields",this.isTripod);
    		gd.addMessage("=== Camera parameters to be fitted ===");
    		for (int numStation=0;numStation<this.numStations;numStation++) {
    			if (this.numStations>1){
    				gd.addMessage("--- Station number "+numStation+" ---");
    				gd.addNumericField("Station reprojection errors weight",      100*this.stationWeight[numStation], 1,5,"%");
    			}
    			if (this.isTripod) {
    				gd.addNumericField("Tripod rotation around vertical axis (clockwise from top - positive)",this.goniometerHorizontal[numStation], 3,7,"degrees");
    				gd.addNumericField("Tripod rotation around horizontal axis (camera up - positive)",       this.goniometerAxial[numStation], 3,7,"degrees");
    			} else {
    				gd.addNumericField("Goniometer rotation around 'horizontal' axis (tilting from the target - positive)",this.goniometerHorizontal[numStation], 3,7,"degrees");
    				gd.addNumericField("Rotation around Eyesis main axis (clockwise in plan - positive)",      this.goniometerAxial[numStation], 3,7,"degrees");
    			}

    			gd.addNumericField("Distance between goniometer axes",                                     this.interAxisDistance[numStation], 3,7,"mm");
    			gd.addNumericField("Angle error between goniometer axes (<0 if vertical axis rotated CW )",this.interAxisAngle[numStation],  3,7,"degrees");
    			if (this.isTripod) {
    				gd.addNumericField("Vertical tripod axis tilt  from true vertical",                    this.horAxisErrPhi[numStation],  3,7,"degrees");
    				gd.addNumericField("Vertical tripod axis roll error from true vertical",               this.horAxisErrPsi[numStation],  3,7,"degrees");
    			} else {
    				gd.addNumericField("Horizontal axis azimuth error (CW in plan)",                       this.horAxisErrPhi[numStation],  3,7,"degrees");
    				gd.addNumericField("Horizontal axis roll error (CW looking to target)",                this.horAxisErrPsi[numStation],  3,7,"degrees");
    			}

    			gd.addNumericField("Distance between the lens entrace pupil and the sensor",               this.entrancePupilForward[numStation],  3,7,"mm");
    			gd.addNumericField("Camera center above goniometer horizontal axis",                       this.centerAboveHorizontal[numStation],  3,7,"mm");

    			gd.addNumericField("Goniometer reference point position X (target coordinates, left)",     this.GXYZ[numStation][0], 3,7,"mm");
    			gd.addNumericField("Goniometer reference point position Y (target coordinates, up)",       this.GXYZ[numStation][1], 3,7,"mm");
    			gd.addNumericField("Goniometer reference point position Z (target coordinates, away)",     this.GXYZ[numStation][2], 3,7,"mm");
    		}
    		gd.addMessage("=== Other parameters ===");

    		gd.addNumericField("Image sensor width (maximal if different)",                            this.sensorWidth, 0,4,"pix");
    		gd.addNumericField("Image sensor height (maximal if different)",                           this.sensorHeight, 0,4,"pix");
    		gd.addNumericField("Shrink detected grid by this number of nodes (half/periods) for masks",this.shrinkGridForMask, 0,4,"grid nodes");
    		gd.addNumericField("Gaussian blur masks for the sensors (positive - pixels, negative - grid half-periods)", this.maskBlurSigma, 2,6,"pix");
    		gd.addNumericField("Reduce sensor resolution when calculating masks",                      this.decimateMasks, 0);

    		gd.addNumericField("Filter out grid nodes with difference from quadratically predicted from 8 neighbors", this.badNodeThreshold, 2,6,"pix");
    		gd.addNumericField("Maximal number of bad nodes around the corrected one to fix",          this.maxBadNeighb, 0);
    		gd.addNumericField("Minimal number of valid (with all filters applied) nodes in each image",this.minimalValidNodes, 0);
    		gd.addNumericField("Increase weight of the multi-image sets (0 - do not increase, 1 - multiply by number of images in a set (to power ), 2 - same but remove single-image ",this.weightMultiImageMode, 0);
    		gd.addNumericField("Increase weight of the multi-image sets power (used with mode above)", this.weightMultiExponent, 2,6,"");
    		gd.addNumericField("Increase weight of the images by the power of their diameters", this.weightDiameterExponent, 2,6,"");
    		gd.addNumericField("Increase weight Y-error with respect to X-error",                100.0*this.weightYtoX, 2,6,"%");
    		gd.addNumericField("Filter out grid nodes with the contrast less than this value (maximal is ~0.8) ", this.minimalGridContrast, 2,6,"");
    		gd.addNumericField("Shrink-blur detected grids alpha",                                     this.shrinkBlurSigma, 2,6,"grid nodes");
    		gd.addNumericField("Shrink-blur detected grids  level (-1..+1)",                           this.shrinkBlurLevel, 2,6,"");
    		gd.addNumericField("Channel balace mode: <0 - use specified defaults, 0 - keep curent, >0 - exponent for correction (1.0 - precise equalization)", this.balanceChannelWeightsMode, 3,6,"");
    		gd.addNumericField("Remove nodes with error greater than scaled RMS in that image, weighted",  this.removeOverRMS, 2,6,"xRMS");
    		gd.addNumericField("Same, not weghted (not more permissive near the borders with low weight)", this.removeOverRMSNonweighted, 2,6,"xRMS");
    		gd.addNumericField("Number of sub-camera modules",                                         this.eyesisSubCameras[0].length,   0,2,"");
    		gd.addNumericField("Number of sub-camera module to edit (<=0  - none)",                    nextSubCamera,   0,2,"");
   	        gd.enableYesNoCancel("OK", "Done");
    	    WindowTools.addScrollBars(gd);
    		gd.showDialog();
    		if (gd.wasCanceled()) return -1;
    		this.isTripod=                 gd.getNextBoolean();
    		for (int numStation=0;numStation<this.numStations;numStation++) {
    			if (this.numStations>1){
    				this.stationWeight[numStation]=0.01*gd.getNextNumber();
    			}
    			this.goniometerHorizontal[numStation]=     gd.getNextNumber();
    			this.goniometerAxial[numStation]=          gd.getNextNumber();
    			this.interAxisDistance[numStation]=        gd.getNextNumber();
    			this.interAxisAngle[numStation]=           gd.getNextNumber();
    			this.horAxisErrPhi[numStation]=            gd.getNextNumber();
    			this.horAxisErrPsi[numStation]=            gd.getNextNumber();
    			this.entrancePupilForward[numStation]=     gd.getNextNumber();
    			this.centerAboveHorizontal[numStation]=    gd.getNextNumber();

    			this.GXYZ[numStation][0]=                  gd.getNextNumber();
    			this.GXYZ[numStation][1]=                  gd.getNextNumber();
    			this.GXYZ[numStation][2]=                  gd.getNextNumber();
    		}
    		this.sensorWidth=         (int) gd.getNextNumber();
    		this.sensorHeight=        (int) gd.getNextNumber();
    		this.shrinkGridForMask=   (int) gd.getNextNumber();
    		this.maskBlurSigma=             gd.getNextNumber();
    		this.decimateMasks=       (int) gd.getNextNumber();
	    	this.badNodeThreshold=          gd.getNextNumber();
	    	this.maxBadNeighb=        (int) gd.getNextNumber();
	    	this.minimalValidNodes=   (int) gd.getNextNumber();
	    	this.weightMultiImageMode=(int) gd.getNextNumber();
    		this.weightMultiExponent=       gd.getNextNumber();
    		this.weightDiameterExponent=    gd.getNextNumber();
    		this.weightYtoX=           0.01*gd.getNextNumber();
	    	this.minimalGridContrast=       gd.getNextNumber();
	    	this.shrinkBlurSigma =          gd.getNextNumber();
	    	this.shrinkBlurLevel =          gd.getNextNumber();
	    	this.balanceChannelWeightsMode= gd.getNextNumber();
	    	this.removeOverRMS =            gd.getNextNumber();
	    	this.removeOverRMSNonweighted=  gd.getNextNumber();
    		int numSubCams=           (int) gd.getNextNumber();
    		int numSubCam=            (int) gd.getNextNumber();
    		if (numSubCams!=this.eyesisSubCameras[0].length){
    			this.eyesisSubCameras=new EyesisSubCameraParameters[this.numStations][];
    	    	for (int numStation=0;numStation<numStations;numStation++) {
    	    		initSubCameras(numStation,numSubCams); // re-initialize from defaults, discard current!
    	    	}
    		}
    		if (numSubCam<0)numSubCam=0;
    		return gd.wasOKed()?numSubCam:-1;
    	}

    	public boolean showSubcameraDialog(
    			int numSubCam,
    			String title) {
    		GenericDialog gd = new GenericDialog(title);
    		for (int numStation=0;numStation<this.numStations;numStation++) {
    			EyesisSubCameraParameters subCam=this.eyesisSubCameras[numStation][numSubCam];
    			if (subCam!=null) {
		    		gd.addMessage("Channel weight "+subCam.channelWeightCurrent);
    				if (numStation==0){
        				gd.addNumericField("Channel "+numSubCam+" default weight",      subCam.channelWeightDefault, 3,6,"");
    				}
    				if (this.numStations>1) gd.addMessage("--- Station number "+numStation+" ---");
    				gd.addNumericField("Subcamera azimuth",                             subCam.azimuth, 3,7,"degrees");
    				gd.addNumericField("Subcamera distance from the axis",              subCam.radius,  3,7,"mm");
    				gd.addNumericField("Subcamera height from the 'equator'",           subCam.height,  3,7,"mm");
    				gd.addNumericField("Optical axis heading (relative to azimuth)",    subCam.phi,     3,7,"degrees");
    				gd.addNumericField("Optical axis elevation (up from equator)",      subCam.theta,   3,7,"degrees");
    				gd.addNumericField("Camera roll, positive CW looking to the target",subCam.psi,     3,7,"degrees");
    				gd.addNumericField("Lens focal length",               subCam.focalLength, 3,6,"mm");
    				gd.addNumericField("Sensor pixel period",             subCam.pixelSize, 3,6,"um");
    				gd.addNumericField("Distortion radius (half width)",  subCam.distortionRadius, 5,8,"mm");
    				gd.addNumericField("Distortion A8 (r^8)",             subCam.distortionA8, 6,8,"");
    				gd.addNumericField("Distortion A7 (r^7)",             subCam.distortionA7, 6,8,"");
    				gd.addNumericField("Distortion A6 (r^6)",             subCam.distortionA6, 6,8,"");
    				gd.addNumericField("Distortion A5 (r^5)",             subCam.distortionA5, 6,8,"");
    				gd.addNumericField("Distortion A (r^4)",              subCam.distortionA, 6,8,"");
    				gd.addNumericField("Distortion B (r^3)",              subCam.distortionB, 6,8,"");
    				gd.addNumericField("Distortion C (r^2)",              subCam.distortionC, 6,8,"");
    				gd.addNumericField("Lens axis on the sensor (horizontal, from left edge)", subCam.px0, 2,7,"pixels");
    				gd.addNumericField("Lens axis on the sensor (vertical, from top  edge)",   subCam.py0, 2,7,"pixels");
    			}
    		}
   	        gd.enableYesNoCancel("OK", "Done");
    	    WindowTools.addScrollBars(gd);
    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		double channelWeightDefault=1.0;
    		for (int numStation=0;numStation<this.numStations;numStation++) {
    			EyesisSubCameraParameters subCam=this.eyesisSubCameras[numStation][numSubCam];
    			if (subCam!=null) {
    				if (numStation==0){
    		    		gd.addMessage("Channel weight "+subCam.channelWeightCurrent);
    		    		channelWeightDefault=gd.getNextNumber();
    				}
    				subCam.channelWeightDefault= channelWeightDefault; // assign to all stations
    				subCam.azimuth=         gd.getNextNumber();
    				subCam.radius=          gd.getNextNumber();
    				subCam.height=          gd.getNextNumber();
    				subCam.phi=             gd.getNextNumber();
    				subCam.theta=           gd.getNextNumber();
    				subCam.psi=             gd.getNextNumber();
    				subCam.focalLength=     gd.getNextNumber();
    				subCam.pixelSize=       gd.getNextNumber();
    				subCam.distortionRadius=gd.getNextNumber();
    				subCam.distortionA8=    gd.getNextNumber();
    				subCam.distortionA7=    gd.getNextNumber();
    				subCam.distortionA6=    gd.getNextNumber();
    				subCam.distortionA5=    gd.getNextNumber();
    				subCam.distortionA=     gd.getNextNumber();
    				subCam.distortionB=     gd.getNextNumber();
    				subCam.distortionC=     gd.getNextNumber();
    				subCam.px0=             gd.getNextNumber();
    				subCam.py0=             gd.getNextNumber();
    			}
    		}
    		return gd.wasOKed();
    	}

    	// TODO: make subcameras show numbers from 0
    	public boolean showDialog(String title) {
    		System.out.println("this.eyesisSubCameras.length="+this.eyesisSubCameras.length);
    		System.out.println("this.eyesisSubCameras[0].length="+this.eyesisSubCameras[0].length);
    		int subCam=1;
    		while (subCam<=this.eyesisSubCameras[0].length) {
        		System.out.println("subCam="+subCam);
    			subCam=subShowDialog(title, subCam);
    			if (subCam<0) return false;
    			if (subCam==0) return true;
    			while (subCam<=this.eyesisSubCameras[0].length) {
//    				if (!this.eyesisSubCameras[subCam-1].showDialog(title+": subcamera "+subCam)) break;
    				if (!showSubcameraDialog(subCam-1,title+": subcamera "+subCam+" ("+(subCam-1)+")")) break;
    				subCam++;
    			}
    		}
    		return true;
    	}
        /**
         * 
         * @param eyesisCameraParameters current parameters of the Eyesis camera, subcameras and goniometer
         * @param subCamNumber number of sub-camera (from 0)
         * @return array of the parameters (both individual sub-camera and common to all sub-cameras)
         *
         */
        public double [] getParametersVector(
        		int stationNumber,
        		int subCamNumber // 
                ){
        	if (
        			(this.eyesisSubCameras==null) ||
        			(this.numStations<=stationNumber) ||
        			(this.eyesisSubCameras.length<=stationNumber) ||
        			(this.eyesisSubCameras[stationNumber].length<=subCamNumber)) throw new IllegalArgumentException
            ("Nonexistent subcamera "+subCamNumber+ " and/or station number="+stationNumber+" this.numStations="+this.numStations+" this.eyesisSubCameras.length="+this.eyesisSubCameras.length);
        	
        	EyesisSubCameraParameters subCam=this.eyesisSubCameras[stationNumber][subCamNumber];
//        	System.out.println("getParametersVector("+stationNumber+","+subCamNumber+"), subCam is "+((subCam==null)?"null":"NOT null"));
        	double [] parVect={
        			subCam.azimuth,            // 0 azimuth of the lens entrance pupil center, degrees, clockwise looking from top
        			subCam.radius,             // 1 mm, distance from the rotation axis
        			subCam.height,             // 2 mm, up (was downwards?) - from the origin point
        			subCam.phi,                // 3 degrees, optical axis from azimuth/r vector, clockwise
        			subCam.theta,              // 4 degrees, optical axis from the eyesis horizon, positive - up
        			subCam.psi,                // 5 degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
        			this.goniometerHorizontal[stationNumber], // 6 goniometer rotation around "horizontal" axis (tilting from the target - positive)
        			this.goniometerAxial[stationNumber],      // 7 goniometer rotation around Eyesis axis (clockwise in plan - positive 
        			this.interAxisDistance[stationNumber],    // 8 distance in mm between two goniometer axes
        			this.interAxisAngle[stationNumber],       // 9 angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
        			this.horAxisErrPhi[stationNumber],        //10 angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
        			this.horAxisErrPsi[stationNumber],        //11 angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
            		this.entrancePupilForward[stationNumber], //12 common to all lenses - distance from the sensor to the lens entrance pupil
            		this.centerAboveHorizontal[stationNumber],//13 camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each
        			
        			this.GXYZ[stationNumber][0],              //14 (12) coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
        			this.GXYZ[stationNumber][1],              //15 (13)  y
        			this.GXYZ[stationNumber][2],              //16 (14)  z
        			subCam.focalLength,        //17 (15)  lens focal length
        			subCam.px0,                //18 (16) center of the lens on the sensor, pixels
        			subCam.py0,                //19 (17) center of the lens on the sensor, pixels
        			subCam.distortionA8,       //20 (18)  r^8 (normalized to focal length or to sensor half width?)
        			subCam.distortionA7,       //21 (19)  r^7 (normalized to focal length or to sensor half width?)
        			subCam.distortionA6,       //22 (20)  r^6 (normalized to focal length or to sensor half width?)
        			subCam.distortionA5,       //23 (21)  r^5 (normalized to focal length or to sensor half width?)
        			subCam.distortionA,        //24 (22)  r^4 (normalized to focal length or to sensor half width?)
        			subCam.distortionB,        //25 (23)  r^3
        			subCam.distortionC,        //26 (24)  r^2
        	};
        	// Global parameters, not adjusted - just copied once when camera is selected
    // or should they stay fixed and not copied at all?    	
//    		this.lensDistortionParameters.pixelSize=subCam.pixelSize; // has to be set separately
//    		this.lensDistortionParameters.distortionRadius=subCam.distortionRadius;
        	return parVect;
        }
        
 //       public int getNumSubCameras (){return (this.eyesisSubCameras==null)?0:this.eyesisSubCameras.length;}
        public int getGoniometerHorizontalIndex(){return 6;}
        public int getGoniometerAxialIndex(){return 7;}
        public int getSensorWidth() { return this.sensorWidth;}
        public int getSensorHeight() { return this.sensorHeight;}
        public int getSensorWidth(int subCam) { return this.sensorWidth;} // for the future? different sensors
        public int getSensorHeight(int subCam) { return this.sensorHeight;}// for the future? different sensors
        public double getPixelSize(int subCamNumber){return  this.eyesisSubCameras[0][subCamNumber].pixelSize;} // use station 0's pixel size
        public double getDistortionRadius(int subCamNumber){return  this.eyesisSubCameras[0][subCamNumber].distortionRadius;}

        public void setParametersVectorAllStations(
        		double [] parVect,
        		boolean [] update,
        		int subCamNumber // 
                ){
        	for (int stationNumber=0;stationNumber<this.numStations;stationNumber++){
        		setParametersVector(
                		parVect,
                		update,
                		stationNumber,
                		subCamNumber );
        	}
        	
        }

        /**
         * Set camera/subcamera parameters from the parameters vector
         * @param parVect array of the parameters (both individual sub-camera and common to all sub-cameras)
         * @param update - which parameter of the vector to update
         * @param eyesisCameraParameters current parameters of the Eyesis camera, subcameras and goniometer
         * @param subCamNumber number of sub-camera (from 0)
         */
        public void setParametersVector(
        		double [] parVect,
        		boolean [] update,
        		int stationNumber,
        		int subCamNumber // 
                ){
        	if (parVect.length!=27) throw new IllegalArgumentException ("Wrong length of the parameters vector: "+parVect.length+"(should be 27)");
        	if (
        			(this.eyesisSubCameras==null) ||
        			(this.numStations<=stationNumber) ||
        			(this.eyesisSubCameras.length<=stationNumber) ||
        			(this.eyesisSubCameras[stationNumber].length<=subCamNumber)) throw new IllegalArgumentException
            ("Nonexistent subcamera "+subCamNumber+ " and/or station number="+stationNumber);
        	
        	
        	EyesisSubCameraParameters subCam=this.eyesisSubCameras[stationNumber][subCamNumber];
        	if (update[0]) subCam.azimuth=parVect[0];            // 0 azimuth of the lens entrance pupil center, degrees, clockwise looking from top
        	if (update[1]) subCam.radius=parVect[1];             // 1 mm, distance from the rotation axis
        	if (update[2]) subCam.height=parVect[2];             // 2 mm, up (was downwards?) - from the origin point
        	if (update[2]) subCam.phi=parVect[3];                // 3 degrees, optical axis from azimuth/r vector, clockwise
        	if (update[4]) subCam.theta=parVect[4];              // 4 degrees, optical axis from the eyesis horizon, positive - up
        	if (update[5]) subCam.psi=parVect[5];                // 5 degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
        	if (update[6]) this.goniometerHorizontal[stationNumber]=parVect[6]; // 6 goniometer rotation around "horizontal" axis (tilting from the target - positive)
        	if (update[7]) this.goniometerAxial[stationNumber]=parVect[7];      // 7 goniometer rotation around Eyesis axis (clockwise in plan - positive 
        	if (update[8]) this.interAxisDistance[stationNumber]=parVect[8];    // 8 distance in mm between two goniometer axes
        	if (update[9]) this.interAxisAngle[stationNumber]=parVect[9];       // 9 angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
        	if (update[10]) this.horAxisErrPhi[stationNumber]=parVect[10];       //10 angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
        	if (update[11]) this.horAxisErrPsi[stationNumber]=parVect[11];       //11 angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
        	if (update[12]) this.entrancePupilForward[stationNumber]=parVect[12];             //12 coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
        	if (update[13]) this.centerAboveHorizontal[stationNumber]=parVect[13];             //13  y
        	if (update[14]) this.GXYZ[stationNumber][0]=parVect[14];             //14 coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
        	if (update[15]) this.GXYZ[stationNumber][1]=parVect[15];             //15  y
        	if (update[16]) this.GXYZ[stationNumber][2]=parVect[16];             //16  z
        	if (update[17]) subCam.focalLength=parVect[17];       //17  lens focal length
        	if (update[18]) subCam.px0=parVect[18];               //18 center of the lens on the sensor, pixels
        	if (update[19]) subCam.py0=parVect[19];               //19 center of the lens on the sensor, pixels
        	if (update[20]) subCam.distortionA8=parVect[20];      //20  r^8 (normalized to focal length or to sensor half width?)
        	if (update[21]) subCam.distortionA7=parVect[21];      //21  r^7 (normalized to focal length or to sensor half width?)
        	if (update[22]) subCam.distortionA6=parVect[22];      //22  r^6 (normalized to focal length or to sensor half width?)
        	if (update[23]) subCam.distortionA5=parVect[23];      //23  r^5 (normalized to focal length or to sensor half width?)
        	if (update[24]) subCam.distortionA= parVect[24];      //24  r^4 (normalized to focal length or to sensor half width?)
        	if (update[25]) subCam.distortionB= parVect[25];      //25  r^3
        	if (update[26]) subCam.distortionC= parVect[26];      //26  r^2
        }
    	public void initSubCameras(
    			int numStation,
    			int numSubCameras){
    		System.out.println("initSubCameras("+numStation+","+numSubCameras+")");
    		this.eyesisSubCameras[numStation]=new EyesisSubCameraParameters[numSubCameras];
    		for (int i=0;i<numSubCameras;i++)  this.eyesisSubCameras[numStation][i]=null;
    		if (numSubCameras==3) {
    			this.eyesisSubCameras[numStation][0]=new EyesisSubCameraParameters( //TODO:  modify for lens adjustment defaults?
    					0.0, // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					52.53, // double radius,  // mm, distance from the rotation axis
    					34.64, // double height,  // mm, up (was downwards) - from the origin point
    					0.0, // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0, //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					180.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    			this.eyesisSubCameras[numStation][1]=new EyesisSubCameraParameters( //TODO:  modify for lens adjustment defaults?
    					30.0, // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					60.0, // double radius,  // mm, distance from the rotation axis
    					-17.32, // double height,  // mm, up (was downwards) - from the origin point
    					-30.0, // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0, //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					180.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    			this.eyesisSubCameras[numStation][2]=new EyesisSubCameraParameters( //TODO:  modify for lens adjustment defaults?
    					-30.0, // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					60.0, // double radius,  // mm, distance from the rotation axis
    					-17.32, // double height,  // mm, up (was downwards) - from the origin point
    					30.0, // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0, //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					180.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    		} else if (numSubCameras==1) {
    			this.eyesisSubCameras[numStation][0]=new EyesisSubCameraParameters( //TODO:  modify for lens adjustment defaults?
    					0.0, // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					0.0, // double radius,  // mm, distance from the rotation axis
    					0.0, // double height,  // mm, up (was downwards) - from the origin point
    					0.0, // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0, //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					0.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    		} else {
    			// default setup for the 26 sub-cameras	    		
    			for (int i=0;i<8;i++) if (i<numSubCameras) 	this.eyesisSubCameras[numStation][i]=new EyesisSubCameraParameters( // top 8 cameras
    					45.0*i,      // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					41.540,      // double radius,  // mm, distance from the rotation axis
    					42.883,      // double height,  // mm, up (was downwards?) - from the origin point
    					0.0,         // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					60.0,        //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					90.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    			for (int i=8;i<16;i++) if (i<numSubCameras) 	this.eyesisSubCameras[numStation][i]=new EyesisSubCameraParameters( // middle 8 cameras
    					45.0*(i-8),  // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					54.525,      // double radius,  // mm, distance from the rotation axis
    					0.0, // double height,  // mm, up (was downwards) - from the origin point
    					0.0,         // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0,         //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					90.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    			for (int i=16;i<24;i++) if (i<numSubCameras) 	this.eyesisSubCameras[numStation][i]=new EyesisSubCameraParameters( // bottom eight cameras
    					45.0*(i-16), // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					41.540,      // double radius,  // mm, distance from the rotation axis
    					-42.883,     // double height,  // mm, up (was downwards?) - from the origin point
    					0.0,         // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					-60.0,       //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					-90.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					1.0); //channelWeightDefault
    			if (24<numSubCameras) 	this.eyesisSubCameras[numStation][24]=new EyesisSubCameraParameters(
    					90,      // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					12.025,  // double radius,  // mm, distance from the rotation axis
    					-807.0,  // double height,  // mm, up - from the origin point
    					0.0,     // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0,     //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					90.0,  //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5, // double focalLength
    		    		2.2, // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					4.0); //channelWeightDefault
    			if (25<numSubCameras) 	this.eyesisSubCameras[numStation][25]=new EyesisSubCameraParameters(
    					270,     // double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    					12.025,  // double radius,  // mm, distance from the rotation axis
    					-841.0,  // double height,  // mm, up - from the origin point
    					0.0,     // double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    					0.0,     //double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    					-90.0,    //double psi;      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    		    		4.5,    // double focalLength
    		    		2.2,    // double pixelSize (um)
    		    		2.8512, //double distortionRadius mm - half width of the sensor
    		    		0.0, // double distortionA8 // r^8 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA7 // r^7 (normalized to focal length or to sensor half width?)
    		    		0.0, // double distortionA6 // r^6 (normalized to focal length or to sensor half width?)
    		    		0.0,    // double distortionA5 // r^5 (normalized to focal length or to sensor half width?)
    		    		0.0,    // double distortionA // r^4 (normalized to focal length or to sensor half width?)
    		    		0.0,    // double distortionB // r^3
    		    		0.0,    // double distortionC // r^2
    		    		1296.0, // double px0=1296.0;          // center of the lens on the sensor, pixels
    					968.0, // double py0=968.0;           // center of the lens on the sensor, pixels
    					4.0); //channelWeightDefault
    		}
    	}
    	public void recenterVertically(boolean [] subcams, boolean [] stations){
    		boolean sameCenterAboveHorizontal=true;
    		double cah=Double.NaN;
    		for (int i=0;i<stations.length;i++){
    			if (stations[i]) {
    				if (Double.isNaN(cah)) cah=this.centerAboveHorizontal[i];
    				else if (cah!=this.centerAboveHorizontal[i]) sameCenterAboveHorizontal=false;
    			}
    		}
    		if (Double.isNaN(cah)){
    			System.out.println("No stations enabled, nothing to do for vertical centering");
    			return;
    		}
    		System.out.println("centerAboveHorizontal "+
    		(sameCenterAboveHorizontal?"is common for all stations":"differs between stations"));
    		
    		if (sameCenterAboveHorizontal){
    	   		System.out.println("Re-centering vertically for centerAboveHorizontal common for all stations ");
	
    			double sumWeightedHeights=0.0;
    			double sumWeights=0.0;
    					
    			for (int i=0;i<stations.length;i++) if (stations[i]) {
    				for (int subIndex=0; subIndex<subcams.length;subIndex++) if (subcams[subIndex]){
    					System.out.println("Averaging station "+i+", subcamera "+subIndex);
    					sumWeights+=stationWeight[i];
    					sumWeightedHeights+=stationWeight[i]*eyesisSubCameras[i][subIndex].height;
    				}
    			}
        		if (sumWeights==0.0){
        			System.out.println("No subcams are enabled, nothing to do for vertical centering");
        			return;
        		}
    			sumWeightedHeights/=sumWeights;
    			for (int i=0;i<stations.length;i++) if (stations[i]) {
    				for (int subIndex=0; subIndex<subcams.length;subIndex++) { // need to update all channels, not only averaged
    					eyesisSubCameras[i][subIndex].height-=sumWeightedHeights;
    				}
					this.centerAboveHorizontal[i]+=sumWeightedHeights;
    			}
    		} else {
    	   		System.out.println("Re-centering vertically for centerAboveHorizontal individual for each station");
    			for (int i=0;i<stations.length;i++) if (stations[i]) {
        			double sumWeightedHeights=0.0;
        			double sumWeights=0.0;
    				for (int subIndex=0; subIndex<subcams.length;subIndex++) if (subcams[subIndex]){
    					System.out.println("Averaging station "+i+", subcamera "+subIndex);
    					sumWeights+=stationWeight[i];
    					sumWeightedHeights+=stationWeight[i]*eyesisSubCameras[i][subIndex].height;
    				}
            		if (sumWeights==0.0){
            			System.out.println("No subcams are enabled, nothing to do for vertical centering");
            			return;
            		}
        			sumWeightedHeights/=sumWeights;
    				for (int subIndex=0; subIndex<subcams.length;subIndex++) {  // need to update all channels, not only averaged
    					eyesisSubCameras[i][subIndex].height-=sumWeightedHeights;
    				}
					this.centerAboveHorizontal[i]+=sumWeightedHeights;
    			}

    		}
    		
    		
    	}
    }
    public static class EyesisSubCameraParameters{
    	// origin is on the rotation axis of the tube body closest to the goniometer horizontal axis
    	public double azimuth; // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    	public double radius;  // mm, distance from the rotation axis
    	public double height;       // mm, up - from the origin point
    	public double phi;     // degrees, optical axis from azimuth/r vector, clockwise
    	public double theta;   // degrees, optical axis from the eyesis horizon, positive - up
    	public double psi;     // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
		public double focalLength=4.5;
		public double pixelSize=  2.2; //um
		public double distortionRadius=  2.8512; // mm - half width of the sensor
		public double distortionA8=0.0; //r^8 (normalized to focal length or to sensor half width?)
		public double distortionA7=0.0; //r^7 (normalized to focal length or to sensor half width?)
		public double distortionA6=0.0; //r^6 (normalized to focal length or to sensor half width?)
		public double distortionA5=0.0; //r^5 (normalized to focal length or to sensor half width?)
		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
		public double distortionB=0.0; // r^3
		public double distortionC=0.0; // r^2
		public double px0=1296.0;          // center of the lens on the sensor, pixels
		public double py0=968.0;           // center of the lens on the sensor, pixels
		public double channelWeightDefault=1.0;
		public double channelWeightCurrent=1.0;
    	public EyesisSubCameraParameters(
    			double azimuth, // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
    			double radius,  // mm, distance from the rotation axis
    			double height,  // mm, up from the origin point
    			double phi,     // degrees, optical axis from azimuth/r vector, clockwise
    			double theta,   // degrees, optical axis from the eyesis horizon, positive - up
    			double psi,      // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
    			double focalLength,
    			double pixelSize,//um
    			double distortionRadius, //mm - half width of the sensor
    			double distortionA8, // r^8 
    			double distortionA7, // r^7 
    			double distortionA6, // r^5 
    			double distortionA5, // r^5 
    			double distortionA, // r^4 (normalized to focal length or to sensor half width?)
    			double distortionB, // r^3
    			double distortionC, // r^2
    			double px0,           // center of the lens on the sensor, pixels
    			double py0,           // center of the lens on the sensor, pixels
    			double channelWeightDefault
    	){
    		this.azimuth=azimuth;
    		this.radius=radius;
    		this.height=height;
    		this.phi=phi;
    		this.theta=theta;
    		this.psi=psi;
    		this.focalLength=focalLength;
    		this.pixelSize=  pixelSize; //um
    		this.distortionRadius=  distortionRadius; // mm - half width of the sensor
    		this.distortionA8=distortionA8;
    		this.distortionA7=distortionA7;
    		this.distortionA6=distortionA6;
    		this.distortionA5=distortionA5;
    		this.distortionA=distortionA; // r^4 (normalized to focal length or to sensor half width?)
    		this.distortionB=distortionB; // r^3
    		this.distortionC=distortionC; // r^2
    		this.px0=px0;
    		this.py0=py0;
    		this.channelWeightDefault=channelWeightDefault;
    		this.channelWeightCurrent=this.channelWeightDefault;
    	}
    	public EyesisSubCameraParameters clone() {
    		return new EyesisSubCameraParameters(
    				this.azimuth,
    				this.radius,
    				this.height,
    				this.phi,
    				this.theta,
    				this.psi,
    	    		this.focalLength,
    	    		this.pixelSize,
    	    		this.distortionRadius,
    	    		this.distortionA8,
    	    		this.distortionA7,
    	    		this.distortionA6,
    	    		this.distortionA5,
    	    		this.distortionA,
    	    		this.distortionB,
    	    		this.distortionC,
    	    		this.px0,
    	    		this.py0,
    	    		this.channelWeightDefault
    				);
    	}

    	public void setProperties(String prefix,Properties properties){
    		properties.setProperty(prefix+"azimuth",this.azimuth+"");
    		properties.setProperty(prefix+"radius",this.radius+"");
    		properties.setProperty(prefix+"height",this.height+"");
    		properties.setProperty(prefix+"phi",this.phi+"");
    		properties.setProperty(prefix+"theta",this.theta+"");
    		properties.setProperty(prefix+"psi",this.psi+"");
			properties.setProperty(prefix+"focalLength",this.focalLength+"");
			properties.setProperty(prefix+"pixelSize",this.pixelSize+"");
			properties.setProperty(prefix+"distortionRadius",this.distortionRadius+"");
			properties.setProperty(prefix+"distortionA8",this.distortionA8+"");
			properties.setProperty(prefix+"distortionA7",this.distortionA7+"");
			properties.setProperty(prefix+"distortionA6",this.distortionA6+"");
			properties.setProperty(prefix+"distortionA5",this.distortionA5+"");
			properties.setProperty(prefix+"distortionA",this.distortionA+"");
			properties.setProperty(prefix+"distortionB",this.distortionB+"");
			properties.setProperty(prefix+"distortionC",this.distortionC+"");
			properties.setProperty(prefix+"px0",this.px0+"");
			properties.setProperty(prefix+"py0",this.py0+"");
			properties.setProperty(prefix+"channelWeightDefault",this.channelWeightDefault+"");
    	}
    	public void getProperties(String prefix,Properties properties){
    		if (properties.getProperty(prefix+"azimuth")!=null)
    			this.azimuth=Double.parseDouble(properties.getProperty(prefix+"azimuth"));
    		if (properties.getProperty(prefix+"radius")!=null)
    			this.radius=Double.parseDouble(properties.getProperty(prefix+"radius"));
    		if (properties.getProperty(prefix+"height")!=null)
    			this.height=Double.parseDouble(properties.getProperty(prefix+"height"));
    		if (properties.getProperty(prefix+"phi")!=null)
    			this.phi=Double.parseDouble(properties.getProperty(prefix+"phi"));
    		if (properties.getProperty(prefix+"theta")!=null)
    			this.theta=Double.parseDouble(properties.getProperty(prefix+"theta"));
    		if (properties.getProperty(prefix+"psi")!=null)
    			this.psi=Double.parseDouble(properties.getProperty(prefix+"psi"));
			if (properties.getProperty(prefix+"focalLength")!=null)
				this.focalLength=Double.parseDouble(properties.getProperty(prefix+"focalLength"));
			if (properties.getProperty(prefix+"pixelSize")!=null)
				this.pixelSize=Double.parseDouble(properties.getProperty(prefix+"pixelSize"));
			if (properties.getProperty(prefix+"distortionRadius")!=null)
				this.distortionRadius=Double.parseDouble(properties.getProperty(prefix+"distortionRadius"));
			if (properties.getProperty(prefix+"distortionA8")!=null)
				this.distortionA8=Double.parseDouble(properties.getProperty(prefix+"distortionA8"));
			if (properties.getProperty(prefix+"distortionA7")!=null)
				this.distortionA7=Double.parseDouble(properties.getProperty(prefix+"distortionA7"));
			if (properties.getProperty(prefix+"distortionA6")!=null)
				this.distortionA6=Double.parseDouble(properties.getProperty(prefix+"distortionA6"));
			if (properties.getProperty(prefix+"distortionA5")!=null)
				this.distortionA5=Double.parseDouble(properties.getProperty(prefix+"distortionA5"));
			if (properties.getProperty(prefix+"distortionA")!=null)
				this.distortionA=Double.parseDouble(properties.getProperty(prefix+"distortionA"));
			if (properties.getProperty(prefix+"distortionB")!=null)
				this.distortionB=Double.parseDouble(properties.getProperty(prefix+"distortionB"));
			if (properties.getProperty(prefix+"distortionC")!=null)
				this.distortionC=Double.parseDouble(properties.getProperty(prefix+"distortionC"));
			if (properties.getProperty(prefix+"px0")!=null)
				this.px0=Double.parseDouble(properties.getProperty(prefix+"px0"));
			if (properties.getProperty(prefix+"py0")!=null)
				this.py0=Double.parseDouble(properties.getProperty(prefix+"py0"));
			if (properties.getProperty(prefix+"channelWeightDefault")!=null) {
				this.channelWeightDefault=Double.parseDouble(properties.getProperty(prefix+"channelWeightDefault"));
	    		this.channelWeightCurrent=this.channelWeightDefault;
			}
    	}
    	public void setChannelWeightCurrent(
    			double weight){
    		this.channelWeightCurrent=weight;
    	}
    	public double getChannelWeightCurrent(){
    		return this.channelWeightCurrent;
    	}
    	public double getChannelWeightDefault(){
    		return this.channelWeightDefault;
    	}

    }
    
	/* gridGeometry:
	 * [v][u][0] - x(mm) of the node(u,v), right (looking to the wall) - positive
	 * [v][u][1] - y(mm) of the node(u,v), down - positive	
	 * [v][u][2] - z(mm) of the node(u,v), away (into the wall) - positive	
	 * [v][u][3] - mask 0.0 - outside of the pattern rectangle, 1.0 - on the pattern
	 * RGB moved to Photometric
	 * [v][u][4] - R-intensity (normalized to 1.0 full scale)
	 * [v][u][5] - G-intensity
	 * [v][u][6] - B-intensity
	 * ... repeat 3,4,5,6 for different views of the pattern (from different points)
	 */
	public static class PatternParameters {
		public double patternWidth;  // pattern full width in mm
		public double patternHeight; // pattern full height in mm
		public double patternHalfPeriod; // distance between opposite sign nodes
		public double patternTilt;       // pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
		public int [] viewMap=null; // should have
		public int    numStations=1;
		public int    numberOfViews=1;
        public double [][][] gridGeometry=null; // [v][u]{x,y,z,{"alpha", R-intensity, G--intensity, B--intensity} alpha=0 - no ghrid, 1 - grid}
        public double [][][] stationZCorr=null; // [v][u]{station0... stationN} - per-station addition to the gridGeometry[][][2] 
        public double [] averageRGB={1.0,1.0,1.0}; 
    	public int numGeometricChannels=4;   // x,y,z,mask
    	public int numPhotometricChannels=4; // r,g,b,a
    	public int defaultNumberOfChannels=26;

//        public int numLayers=7; // number of layers in gridGeometry
        public int U0=0;  //39
        public int V0=0;  //36
        public int debugLevel=2;
        public boolean updateStatus=true;
        public String pathName=null; // path from which the grid was read/ where it was last saved to
        public double [][][][] photometricByView=null; // [numStation][numView]{a,r,g,b}[pixel]
//        public double [][][] photometricBySensor=null;
        public double [] patternErrors=null; // weighted RMS error for each pattern node, calculated
        public double [] patternErrorMask=null; // weighted RMS error for each pattern node, calculated
        public double [] patternErrorMaskSaved=null; // weighted RMS error for each pattern node, calculated
        
        public void initStationZCorr(){
        	this.stationZCorr=new double [this.gridGeometry.length][this.gridGeometry[0].length][this.numStations];
        	for (int v=0;v<this.stationZCorr.length;v++) for (int u=0;u<this.stationZCorr[0].length;u++) for (int s=0;s<this.stationZCorr[0][0].length;s++){
        		this.stationZCorr[v][u][s]=0.0;
        	}
        }
        public void resetStationZCorr(){
        	this.stationZCorr=null;
        }
        public Rectangle getUVDimensions(){
        	return new Rectangle(U0,V0,gridGeometry[0].length,gridGeometry.length);
        }
        public void setPatternErrors(double [] patternErrors){
        	this.patternErrors=patternErrors;
        }
        public double [] getPatternErrors(){
        	return this.patternErrors;
        }
        public double [] getPatternErrorMask(){
        	return this.patternErrorMask;
        }
        public double [] calculatePatternErrorMask(double maxRMS, double minRMS){
        	double [] mask=new double [this.patternErrors.length]; // null pointer
        	double a=1.0/(maxRMS-minRMS);
        	boolean binary=maxRMS==minRMS;
        	
        	for (int i=0;i<this.patternErrors.length;i++){
        		if (Double.isNaN(this.patternErrors[i])) mask[i]=0.0;
        		else {
        			if (binary) mask[i]=(this.patternErrors[i]>=maxRMS)?0.0:1.0;
        			else {
        				double x= a*(this.patternErrors[i]-minRMS);
        				if (x<=0) mask[i]=1.0;
        				else if (x>=1.0) mask[i]=0.0;
        				else mask[i]=1.0+2*x*x*x-3*x*x;
        			}
        		}
        	}
        	this.patternErrorMask=mask;
        	return mask;
        }
        public double [] expandPatternErrorMask(){
        	Rectangle dimensions=getUVDimensions();
        	int [] dirs={1,dimensions.width+1,dimensions.width,dimensions.width-1,-1,-dimensions.width-1,-dimensions.width,dimensions.width+1};
        	double [] mask=this.patternErrorMask.clone();
        	for (int v=1;v<(dimensions.height-1);v++) for (int u=1;u<(dimensions.width-1);u++){
        		int index=v*dimensions.width+u;
        		double min=this.patternErrorMask[index];
        		for (int iDir=0;iDir<dirs.length;iDir++){
        			if (this.patternErrorMask[index+dirs[iDir]]<min) min=this.patternErrorMask[index+dirs[iDir]];
        		}
        		mask[index]=min;
        	}
        	this.patternErrorMask=mask;
        	return this.patternErrorMask;
        }
        //patternErrorMaskSaved
        public void resetPatternErrorMask(){
        	this.patternErrorMask=null;
        }
        public void savePatternErrorMask(){
        	if (this.patternErrorMask!=null) this.patternErrorMaskSaved=this.patternErrorMask.clone();
        	else this.patternErrorMaskSaved=null;
        }
        public void restorePatternErrorMask(){
        	if (this.patternErrorMaskSaved!=null) this.patternErrorMask=this.patternErrorMaskSaved.clone();
        	else this.patternErrorMask=null;
        }
        public double [] getSavedPatternErrorMask(){
        	return this.patternErrorMaskSaved;
        }

		public PatternParameters(
				int [] viewMap,
				int numStations,
				double patternWidth,  // pattern full width in mm
				double patternHeight, // pattern full height in mm
				double patternHalfPeriod, // distance between opposite sign nodes
				double patternTilt        // pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
		){
			this.numStations=numStations;
			this.patternWidth=patternWidth;
			this.patternHeight=patternHeight;
			this.patternHalfPeriod=patternHalfPeriod;
			this.patternTilt=patternTilt;
			setPhotometric(viewMap);
			calculateGridGeometryAndPhotometric(true);
		}
		public void updateNumStations(int numStations){
			this.numStations=numStations;
			if (numStations==this.photometricByView.length) {
				return;
			}
			System.out.println ("Updating pattern number of stations from "+this.photometricByView.length+" to "+numStations);
			double [][][][] photometricByViewCopy=this.photometricByView;
			this.photometricByView=new double [this.numStations][][][];
			for (int i=0;i<this.photometricByView.length;i++){
				if (i<photometricByViewCopy.length){
					this.photometricByView[i]= photometricByViewCopy[i];
				} else { // deep clone last station
					int iSource=photometricByViewCopy.length-1;
					this.photometricByView[i]=new double [photometricByViewCopy[iSource].length][][];
					for (int j=0;j<photometricByViewCopy[iSource].length;j++){
						if (photometricByViewCopy[iSource][j]==null) {
							this.photometricByView[i][j]=null;
						} else {
							this.photometricByView[i][j]=new double [photometricByViewCopy[iSource][j].length][];
							for (int k=0;k<photometricByViewCopy[iSource][j].length;k++){
								if (photometricByViewCopy[iSource][j][k]==null) {
									this.photometricByView[i][j][k]=null;
								} else {
									this.photometricByView[i][j][k]=photometricByViewCopy[iSource][j][k].clone();
								}								
							}
						}
					}
				}
			}
		}
		public void setPhotometric(int [] viewMap){
			if (viewMap==null){
				this.viewMap=null;
			} else{
				this.viewMap=viewMap.clone();
			}
			setPhotometric();
		}
		public void setPhotometric(){
			if (this.viewMap==null){
				this.viewMap=new int[1];
				this.viewMap[0]=0;
			}
			int maxView=0;
			for (int i=0;i<this.viewMap.length;i++) if (this.viewMap[i]>maxView) maxView=this.viewMap[i];
			this.numberOfViews=maxView+1;
			this.photometricByView=new double [this.numStations][this.numberOfViews][4][];
		}
		public void initDefaultChannels(int num){
			this.viewMap=new int[num];
			for (int i=0;i<num;i++) this.viewMap[i]=(i<24)?0:1;
			this.numberOfViews=2;
		}
		
		public double [][] getPhotometricByView(int stationNumber, int nView){
			if (stationNumber>=this.photometricByView.length) stationNumber=this.photometricByView.length-1;
			if (nView>=this.photometricByView[stationNumber].length) nView=this.photometricByView[stationNumber].length-1;
			return this.photometricByView[stationNumber][nView];
		}
		public double [][] getPhotometricBySensor(int stationNumber,int nSensor){
        	if (getNumStations()<=stationNumber) updateNumStations(stationNumber+1);
//			if (stationNumber>=this.photometricByView.length) stationNumber=this.photometricByView.length-1;
			int isens=nSensor;
			if (nSensor>=this.viewMap.length){
				System.out.println("nSensor="+nSensor+" this.viewMap.length="+this.viewMap.length+" this.photometricByView.length="+this.photometricByView.length);
				nSensor=this.viewMap.length-1;
			}
			int nView=this.viewMap[nSensor];
			if (nView>=this.photometricByView[stationNumber].length){
				System.out.println("nSensor was "+isens+", nView="+nView+" this.photometricByView["+stationNumber+"].length="+this.photometricByView[stationNumber].length);
				nView=this.photometricByView.length-1;
			}
			return this.photometricByView[stationNumber][nView];
		}
//		public int getNumViews(){return this.photometricByView[0].length;}
		public int getNumViews(){return this.numberOfViews;}
		public int getNumStations(){return this.numStations;}
		public void setNumStations(int numStations){this.numStations=numStations;}
		public int [] getViewMap(){return this.viewMap;}
		
		public int getNumGeometricChannels(){return this.numGeometricChannels;}
		public int getNumPhotometricChannels(){return this.numPhotometricChannels;}

		public PatternParameters(
				int [] viewMap,
				double patternWidth,  // pattern full width in mm
				double patternHeight, // pattern full height in mm
				double patternHalfPeriod, // distance between opposite sign nodes
				double patternTilt,        // pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
				double [] averageRGB
		){
			this.patternWidth=patternWidth;
			this.patternHeight=patternHeight;
			this.patternHalfPeriod=patternHalfPeriod;
			this.patternTilt=patternTilt;
			this.averageRGB=averageRGB.clone();
			setPhotometric(viewMap);
			calculateGridGeometryAndPhotometric(true);
		}
		public PatternParameters clone() {
			PatternParameters patternParameters= new PatternParameters(
					this.viewMap,
					this.patternWidth,  // pattern full width in mm
					this.patternHeight, // pattern full height in mm
					this.patternHalfPeriod, // distance between opposite sign nodes
					this.patternTilt,        // pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
					this.averageRGB);
			patternParameters.debugLevel=this.debugLevel;
			patternParameters.updateStatus=this.updateStatus;
			patternParameters.pathName=this.pathName;
			return patternParameters;
		}
        public ImagePlus saveGridAsImageStack(String title, String path){
        	ImagePlus imp=getGridAsImageStack(title);
        	if (imp==null) return null;
	   				FileSaver fs=new FileSaver(imp);
	   				if (updateStatus) IJ.showStatus("Saving grid "+path);
	   				if (imp.getStackSize()>1)
	   					fs.saveAsTiffStack(path);
	   				else
	   					fs.saveAsTiff(path);
	   				
	   				this.pathName=path;
	   				if (this.debugLevel>0) System.out.println("Pattern saved as "+this.pathName);
        	return imp;
        }
        
        public String selectAndSave(boolean smart, String defaultPath){
			String [] extensions={".grid-tiff","-grid.tiff"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Pattern grid *.grid-tiff files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					true,
					"Save Pattern Grid Geometry",
					"Save",
					parFilter,
					(defaultPath==null)?this.pathName:defaultPath); //String defaultPath
			if ((pathname!=null)&& (pathname.length()!=0)) saveGridAsImageStack("Pattern Grid", pathname);
			return pathname;
        }

        
        public String selectAndRestore(boolean smart, String defaultPath, int numStations){
			String [] extensions={".grid-tiff","-grid.tiff"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Pattern grid *.grid-tiff files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					false,
					"Restore Pattern Grid Geometry",
					"Restore",
					parFilter,
					(defaultPath==null)?this.pathName:defaultPath); //String defaultPath
			if ((pathname==null) || (pathname=="")) return null;
			setNumStations(numStations);
			setGridFromImageStack(pathname);
			return pathname;
        }
        public void setGridFromImageStack(String path){
    		Opener opener=new Opener();
			ImagePlus imp=opener.openImage("", path);
        	if (imp==null) {
        		String msg="Failed to read grid geometry file "+path;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	(new JP46_Reader_camera(false)).decodeProperiesFromInfo(imp);
        	setGridFromImageStack(imp);
        	this.pathName=path;
			if (this.debugLevel>0) System.out.println("Opened "+path+" as a stack of the pattern grid geometry");
        }
        /**
         * Loads grid geomnetry (data in mm, X - left, Y - down, Z - into the wall) frome mult-slice
         * ImagePlus. Properties should be set or decoded from the info in the tiff file
         * @param imp - ImagePlus stack, containg x,y,z,alpha slices
         */
        public void setGridFromImageStack(ImagePlus imp){
        	int indexAlpha=3;
        	if (imp == null){
        		String msg="Grid image is null";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (imp.getStackSize()<4){
        		String msg="Expected >=4 slice image with grid geometry";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (	(imp.getProperty("U0") == null) ||
        			(imp.getProperty("V0") == null)){
        		String msg="Properties \"U0\" and/or \"V0\" do not exist";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	this.U0=Integer.parseInt((String) imp.getProperty("U0")); 
        	this.V0=Integer.parseInt((String) imp.getProperty("V0"));
        	// Other properties that are needed only during pattern generation:
        	if (imp.getProperty("patternWidth")!=null) this.patternWidth=Double.parseDouble((String) imp.getProperty("patternWidth"));
        	if (imp.getProperty("patternHeight")!=null) this.patternHeight=Double.parseDouble((String) imp.getProperty("patternHeight"));
        	if (imp.getProperty("patternHalfPeriod")!=null) this.patternHalfPeriod=Double.parseDouble((String) imp.getProperty("patternHalfPeriod"));
        	if (imp.getProperty("patternTilt")!=null) this.patternTilt=Double.parseDouble((String) imp.getProperty("patternTilt"));
        	
        	if (imp.getProperty("AverageRed")!=null)   this.averageRGB[0]=Double.parseDouble((String) imp.getProperty("AverageRed"));
        	if (imp.getProperty("AverageGreen")!=null) this.averageRGB[1]=Double.parseDouble((String) imp.getProperty("AverageGreen"));
        	if (imp.getProperty("AverageBlue")!=null)  this.averageRGB[2]=Double.parseDouble((String) imp.getProperty("AverageBlue"));
        	int numZCorr=0;
        	if (imp.getProperty("numZCorr")!=null)  numZCorr=Integer.parseInt((String) imp.getProperty("numZCorr"));

 //       	if (imp.getProperty("numStations")!=null)  this.numStations=Integer.parseInt((String) imp.getProperty("numStations"));
        	
        	int width=imp.getWidth();
        	int height=imp.getHeight();
    		ImageStack stack = imp.getStack();
        	if (stack==null) {
        		String msg="Expected a image stack with grid geometry";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
    		float [][] pixels =new float[stack.getSize()][];
        	for (int i=0;i<pixels.length;i++) pixels[i]= (float[]) stack.getPixels(i+1);
        	if (this.debugLevel>3){
        		System.out.println("setGridFromImageStack() width="+width+", height="+height+", pixels[0].length="+pixels[0].length);
        	}
        	this.gridGeometry=new double[height][width][getNumGeometricChannels()]; // x,y,z, alpha
        	
        	
        	boolean geometryMaskOnly=(pixels.length==getNumGeometricChannels());
        	boolean singleNoAlpha=(pixels.length==7);
        	int totalNumViews=(geometryMaskOnly || singleNoAlpha)?1:((pixels.length-getNumGeometricChannels()-numZCorr)/getNumPhotometricChannels());
        	int fileNumStations=totalNumViews/getNumViews(); // keep current number of stations
        	int length=height*width;
        	for (int v=0;v<height;v++) for (int u=0;u<width;u++) for (int n=0;n<getNumGeometricChannels();n++){ // x,y,z, alpha
        		this.gridGeometry[v][u][n]=pixels[n][v*width+u];
        	}
        	
        	if (fileNumStations!=getNumStations()){
            	if (this.debugLevel>0){
            		System.out.println("File has "+totalNumViews+" photometric slices, expected "+(getNumStations()*getNumViews())+
            				" ("+getNumStations()+" stations, "+getNumViews()+" views ), skipping loading photometric data");
            	}
            	/// TODO: Fix me!
            	return; //
        	}
        	if (numZCorr>0) {
        		if (numZCorr==getNumStations()) {
        			if (this.debugLevel>0){
        				System.out.println("Loading zCorr data: "+getNumStations()+" slices");
        			}
        			this.stationZCorr=new double [height][width][numZCorr];
        			for (int v=0;v<height;v++) for (int u=0;u<width;u++) for (int n=0;n<numZCorr;n++){
        				this.stationZCorr[v][u][n]=pixels[n+getNumGeometricChannels()][v*width+u];
        			}
        		} else {
            		System.out.println("File has "+numZCorr+" ZCorr slices, current number of stations is "+getNumStations()+
            				", skipping loading zCorr data (per-station pattern Z-correction from the average Z)");
        			
        		}

        	}
        	
        	if (this.debugLevel>0){
        		System.out.println("Loading photometric data: "+(getNumStations()*getNumViews())+" slices "+
        				" ("+getNumStations()+" stations, "+getNumViews()+" vies )");
        	}
        	
        	this.photometricByView=new double [this.numStations][this.numberOfViews][getNumPhotometricChannels()][length]; // r,g,b,a
        	for (int numStation=0;numStation<this.numStations;numStation++) {
        		int useNumStation=numStation;
        		if ((useNumStation>0) && (useNumStation>=fileNumStations)) useNumStation=fileNumStations-1;
        		for (int nView=0;nView<getNumViews();nView++) {
        			for (int chn=0;chn<getNumPhotometricChannels();chn++){
        				int pixIndex=numGeometricChannels+numZCorr+(nView + getNumViews()*useNumStation)*getNumPhotometricChannels()+chn;
        				if ((pixIndex>=pixels.length) && (chn>2)) pixIndex=indexAlpha; // use mask for non-existent alpha
        				if (pixIndex>=pixels.length) {
        					for (int i=0;i<length;i++) {
        						this.photometricByView[useNumStation][nView][chn][i]=(pixels[indexAlpha][i]>0.5)?this.averageRGB[chn]:0.0; // 0<=chn<=2 here OOB =3
        					}
        				} else {
        					for (int i=0;i<length;i++) {
        						this.photometricByView[useNumStation][nView][chn][i]=pixels[pixIndex][i]; //OOB12
        					}
        				}
        			}
        		}
        	}	
        }
 //getNumPhotometricChannels()       
        public ImagePlus getGridAsImageStack(String title){
        	if (this.gridGeometry==null){
        		String msg="Grid geometry does not exist, nothing to convert";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	int width=this.gridGeometry[0].length;
        	int height=this.gridGeometry.length;
        	int length=height*width;
        	int numZCorr=0;
        	if (this.stationZCorr!=null) {
        		for (int v=0;(v<height) && (numZCorr==0);v++ ) for (int u=0;u<width;u++) if (this.stationZCorr[v][u]!=null){
        			numZCorr=this.stationZCorr[v][u].length;
        			break;
        		}
        	}
//        	float [][]pixels=new float [this.gridGeometry[0][0].length][width*height];
        	float [][] pixels=new float [getNumGeometricChannels()+numZCorr+getNumViews()*getNumStations()*getNumPhotometricChannels()] [length];
        	String [] titles=new String[pixels.length];
        	String [] geometricTitles=  {"X","Y","Z","Mask"};
        	String [] zCorrTitles=new String [numZCorr];
        	for (int i=0;i<numZCorr;i++) zCorrTitles[i]="dZ"+i;
        	String [] photometricTitles={"red","green","blue","alpha"};
        	
        	int index=0;
        	for (int v=0;v<height;v++) for (int u=0;u<width;u++){
        		for (int n=0;n<getNumGeometricChannels();n++) { // should be 4==numGeometricChannels
        			pixels[n][index]=  (float) this.gridGeometry[v][u][n];
        		}
        		for (int n=0;n<numZCorr;n++) { // should be 4==numGeometricChannels
        			pixels[n+getNumGeometricChannels()][index]=  (this.stationZCorr[v][u]!=null)?((float) this.stationZCorr[v][u][n]):0.0f;
        		}
        		index++;
        	}
        	for (int n=0;n<getNumGeometricChannels();n++) titles[n]=geometricTitles[n];
        	for (int numStation=0;numStation<this.numStations;numStation++) {
        		for (int nView=0;nView<getNumViews();nView++) {
        			for (int chn=0;chn<getNumPhotometricChannels();chn++){
        				//        		int pixIndex=getNumGeometricChannels()+chn*this.getNumPhotometricChannels()();
        				int pixIndex=getNumGeometricChannels()+numZCorr+(nView + numStation*getNumViews()) *getNumPhotometricChannels()+chn;
        				for (int i=0;i<length;i++) pixels[pixIndex][i]= (float) this.photometricByView[numStation][nView][chn][i]; //OOB 8 //oob 12
        				titles[pixIndex]=photometricTitles[chn]+nView;
        			}
        		}
        	}
        	for (int i=0;i<numZCorr;i++){
        		titles[getNumGeometricChannels()+i]=zCorrTitles[i];
        	}
        	ImagePlus imp=null;
      		ImageStack stack=new ImageStack(width,height);
       		for (int n=0;n<pixels.length;n++)  stack.addSlice(titles[n],    pixels[n]);
       		imp = new ImagePlus(title, stack);
        	imp.setProperty("patternWidth", ""+this.patternWidth);
        	imp.setProperty("patternHeight", ""+this.patternHeight);
        	imp.setProperty("patternHalfPeriod", ""+this.patternHalfPeriod);
        	imp.setProperty("patternTilt", ""+this.patternTilt);
        	imp.setProperty("U0", ""+this.U0);
        	imp.setProperty("V0", ""+this.V0);
        	imp.setProperty("AverageRed",   ""+this.averageRGB[0]);
        	imp.setProperty("AverageGreen", ""+this.averageRGB[1]);
        	imp.setProperty("AverageBlue",  ""+this.averageRGB[2]);
        	if (numZCorr>0) imp.setProperty("numZCorr",  ""+numZCorr);
        	(new JP46_Reader_camera(false)).encodeProperiesToInfo(imp);
        	imp.getProcessor().resetMinAndMax();
        	return imp;
        }
        
        public void applyGridCorrection(double [][] gridCorr){
        	applyGridCorrection(gridCorr, 1.0);
        }
        /**
         * Apply X,Y,Z correction to the current grid geometry
         * @param gridCorr array [4][width*height] of the corrections to grid geometry (x,y,z,weight).
         * Weight is not used, just for information (1.0 - one image file used with full weight)
         * @param scale scale correction
         */
        public void applyGridCorrection(double [][] gridCorr, double scale){
        	if (this.gridGeometry==null){
        		String msg="Grid geometry does not exist, nothing to apply correction to";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	int height=this.gridGeometry.length;
        	int width=this.gridGeometry[0].length;
        	if ((gridCorr==null) || (gridCorr.length!=4) || (gridCorr[0].length!=(width*height))){
        		String msg="Correction is null or does not match pattern grid geometry";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	for (int v=0;v<height;v++) for (int u=0;u<width;u++) if (this.gridGeometry[v][u][3]>0.0){ // only apply to defined grid
        		int vu=v*width+u;
        		for (int n=0;n<3;n++) this.gridGeometry[v][u][n]+= scale*gridCorr[n][vu];
        	}

        }
        
        public void applyZGridCorrection(
    			double [][] gridZCorr3d,
    			double scale){
        	int height=this.gridGeometry.length;
        	int width=this.gridGeometry[0].length;
        	if ((gridZCorr3d==null) || (gridZCorr3d.length!=this.numStations) || (gridZCorr3d[0].length!=(width*height))){
        		String msg="Correction is null or does not match pattern grid geometry";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	this.stationZCorr=new double [height][width][this.numStations];
        	for (int v=0;v<height;v++) for (int u=0;u<width;u++) {
        		int vu=v*width+u;
        		for (int s=0;s<this.numStations;s++){
        	        		this.stationZCorr[v][u][s]=scale*gridZCorr3d[s][vu];
        		}
        	}
    	}

        
		public void setProperties(String prefix,Properties properties){
			properties.setProperty(prefix+"patternWidth",this.patternWidth+"");
			properties.setProperty(prefix+"patternHeight",this.patternHeight+"");
			properties.setProperty(prefix+"patternHalfPeriod",this.patternHalfPeriod+"");
			properties.setProperty(prefix+"patternTilt",this.patternTilt+"");
			properties.setProperty(prefix+"averageRGB_0",this.averageRGB[0]+"");
			properties.setProperty(prefix+"averageRGB_1",this.averageRGB[1]+"");
			properties.setProperty(prefix+"averageRGB_2",this.averageRGB[2]+"");
			if (this.viewMap!=null){
				properties.setProperty(prefix+"viewMap_length",this.viewMap.length+"");
				for (int i=0;i<this.viewMap.length;i++) {
					properties.setProperty(prefix+"viewMap_"+i,this.viewMap[i]+"");
				}
			}
//			this.viewMap;
		}
		public void getProperties(String prefix,Properties properties){
			if (properties.getProperty(prefix+"patternWidth")!=null)
				this.patternWidth=Double.parseDouble(properties.getProperty(prefix+"patternWidth"));
			if (properties.getProperty(prefix+"patternHeight")!=null)
				this.patternHeight=Double.parseDouble(properties.getProperty(prefix+"patternHeight"));
			if (properties.getProperty(prefix+"patternHalfPeriod")!=null)
				this.patternHalfPeriod=Double.parseDouble(properties.getProperty(prefix+"patternHalfPeriod"));
			if (properties.getProperty(prefix+"patternTilt")!=null)
				this.patternTilt=Double.parseDouble(properties.getProperty(prefix+"patternTilt"));
			if (properties.getProperty(prefix+"averageRGB_0")!=null)
				this.averageRGB[0]=Double.parseDouble(properties.getProperty(prefix+"averageRGB_0"));
			if (properties.getProperty(prefix+"averageRGB_1")!=null)
				this.averageRGB[1]=Double.parseDouble(properties.getProperty(prefix+"averageRGB_1"));
			if (properties.getProperty(prefix+"averageRGB_2")!=null)
				this.averageRGB[2]=Double.parseDouble(properties.getProperty(prefix+"averageRGB_2"));
			if (properties.getProperty(prefix+"viewMap_length")!=null) {
				this.viewMap=new int [Integer.parseInt(properties.getProperty(prefix+"viewMap_length"))];
				for (int i=0;i<this.viewMap.length;i++) {
					this.viewMap[i]=0;
					if (properties.getProperty(prefix+"viewMap_"+i)!=null)
						this.viewMap[i]=Integer.parseInt(properties.getProperty(prefix+"viewMap_"+i));
				}
			}
			setPhotometric();
			calculateGridGeometryAndPhotometric(true); // may need to setup this.photometricBySensor
		}
		public boolean showDialog() {
			GenericDialog gd = new GenericDialog("Initial Wall pattern parameters");
			gd.addNumericField("Pattern full width",                  this.patternWidth, 1,6,"mm"); // pattern full width in mm
			gd.addNumericField("Pattern full height",                 this.patternHeight, 1,6,"mm"); // pattern full width in mm
			gd.addNumericField("Distance between opposite sign nodes",this.patternHalfPeriod, 4,8,"mm"); // istance between opposite sign nodes in mm
			gd.addNumericField("Pattern tilt (clockwise)",            this.patternTilt, 1,5,"degrees"); //  pattern tilt (degrees) - U clockwise from X-right (V clockwise from Y-down)
			gd.addNumericField("Average grid RED   (1.0 for white)",  this.averageRGB[0], 3,5,"x"); //
			gd.addNumericField("Average grid GREEN (1.0 for white)",  this.averageRGB[1], 3,5,"x"); //
			gd.addNumericField("Average grid BLUE  (1.0 for white)",  this.averageRGB[2], 3,5,"x"); //
			
			gd.addNumericField("Number of sensors (>24 - two groups, 0 - do not change)",this.defaultNumberOfChannels,0); //
			
			gd.addMessage("Pressing OK will recalculate grid and clear current grid calibration");
			gd.showDialog();
			if (gd.wasCanceled()) return false;
			this.patternWidth=         gd.getNextNumber();
			this.patternHeight=        gd.getNextNumber();
			this.patternHalfPeriod=    gd.getNextNumber();
			this.patternTilt=          gd.getNextNumber();
			this.averageRGB[0]=        gd.getNextNumber();
			this.averageRGB[1]=        gd.getNextNumber();
			this.averageRGB[2]=        gd.getNextNumber();
			int numberOfChannels= (int)gd.getNextNumber();
			
			if (numberOfChannels>0){
				initDefaultChannels(numberOfChannels);
//				setPhotometric();
			}
			setPhotometric();
			calculateGridGeometryAndPhotometric(true);
			return true;
		}
/**
* Calculate pattern x,y,z==0 and alpha (1.0 - inside, 0.0 - outside) for the grid
* @param resetAll - if true - reset all grid info, if false - only recalculate mask and reset flat field info, keep distortions
*/
		public void calculateGridGeometryAndPhotometric(boolean resetAll){
			// this.photometricByView should be initialized 
			int indexAlpha=3;
			int indexMask=3;
			double cosA=Math.cos(this.patternTilt/180*Math.PI);
			double sinA=Math.sin(this.patternTilt/180*Math.PI);
			double halfWidth=0.5*this.patternWidth;
			double halfHeight=0.5*this.patternHeight;
			double [][] uv={
					{( halfWidth*cosA+halfHeight*sinA)/this.patternHalfPeriod,
						(-halfWidth*sinA+halfHeight*cosA)/this.patternHalfPeriod},
						{( halfWidth*cosA-halfHeight*sinA)/this.patternHalfPeriod,
							(-halfWidth*sinA-halfHeight*cosA)/this.patternHalfPeriod}};
			double [] maxUV={Math.max(Math.abs(uv[0][0]), Math.abs(uv[1][0])),Math.max(Math.abs(uv[0][1]), Math.abs(uv[1][1]))};
			this.U0=(int)Math.ceil(maxUV[0]);
			this.V0=(int)Math.ceil(maxUV[1]);
			resetAll |= (this.gridGeometry==null);
			double x,y;
			// in any case
			int len=(2*this.U0+1)*(2*this.V0+1);
			for (int station=0;station<getNumStations();station++){
				for (int i=0;i<getNumViews();i++){
					for (int chn=0;chn<this.getNumPhotometricChannels();chn++){
						this.photometricByView[station][i][chn]=new double [len]; // r,g,b,alpha
					}
				}
			}

			if (resetAll) {
				for (int station=0;station<getNumStations();station++){
					for (int i=0;i<getNumViews();i++){
						for (int chn=0;chn<this.getNumPhotometricChannels();chn++){
							this.photometricByView[station][i][chn]=new double [len]; // r,g,b,alpha
							for (int j=0;j<len;j++){
								this.photometricByView[station][i][chn][j]=0.0;
							}
						}
					}
				}
				this.gridGeometry=new double[2*this.V0+1][2*this.U0+1][getNumGeometricChannels()]; // without resetAll all properties should be the same
				for (int v=-this.V0; v<=this.V0;v++) for (int u=-this.U0; u<=this.U0;u++){
					x=(u*cosA-v*sinA)*this.patternHalfPeriod;
					y=(u*sinA+v*cosA)*this.patternHalfPeriod;
					boolean inGrid=((x>=-halfWidth) && (x<=halfWidth) && (y>=-halfHeight) && (y<=halfHeight));
					this.gridGeometry[v+this.V0][u+this.U0][0]=x;
					this.gridGeometry[v+this.V0][u+this.U0][1]=y;
					this.gridGeometry[v+this.V0][u+this.U0][2]=0.0;
					this.gridGeometry[v+this.V0][u+this.U0][3]=inGrid?1.0:0.0; // common for all views - to be combined with per-view?
				}

			}
			// always - copy mask to alpha for each view
			int index=0;
			for (int v=-this.V0; v<=this.V0;v++) for (int u=-this.U0; u<=this.U0;u++){
				for (int station=0;station<getNumStations();station++){
					for (int nView=0;nView<getNumViews();nView++) {
						this.photometricByView[station][nView][indexAlpha][index]=this.gridGeometry[v+this.V0][u+this.U0][indexMask];
						for (int c=0;c<3;c++) {
							this.photometricByView[station][nView][c][index]=averageRGB[c];
						}
					}
				}
				index++;
			}			
		}

		
		public int [] uvIndicesToUV (int u1, int v1){
        	if ((v1<0) || (u1<0) ||
        			(v1 >= this.gridGeometry.length) ||
        			(u1 >= this.gridGeometry[0].length) ||
        			(this.gridGeometry[v1][u1][3]==0)) return null;
			int [] iUV={u1-this.U0, v1-this.V0};
			return iUV;
		}
		public double[] getXYZM(int u, int v, boolean verbose, int station){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length) ||
        			(this.gridGeometry[v1][u1][3]==0)) {
        		if ((this.debugLevel>1) && verbose && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))){
            		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
            		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
            		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		  //IJ.showMessage("Error",msg);
        		  System.out.println(msg);
        		}
        		return null;
        	}
        	if (this.stationZCorr==null) return this.gridGeometry[v1][u1];
        	double [] result=this.gridGeometry[v1][u1].clone();
        	result[2]+=this.stationZCorr[v1][u1][station];
			return result;
//			return this.gridGeometry[v1][u1];
		}
		public int getGridIndex(int u, int v){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length) ||
        			(this.gridGeometry[v1][u1][3]==0)) {
        		return -1;
        	}
			return u1+this.gridGeometry[0].length*v1;
		}
		
		public double[] getXYZM(int u, int v, int station){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((this.debugLevel >1) && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))) {
        		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
        		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
        		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (this.stationZCorr==null) return this.gridGeometry[v1][u1];
        	double [] result=this.gridGeometry[v1][u1].clone();
        	result[2]+=this.stationZCorr[v1][u1][station];
			return result;
		}
		public double[] getXYZMAverage(int u, int v){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((this.debugLevel >1) && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))) {
        		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
        		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
        		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gridGeometry[v1][u1];
		}
		public double getZCorr(int u, int v, int station){ // u=0,v=0 - center!
        	if (this.stationZCorr==null) return 0.0;
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((this.debugLevel >1) && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))) {
        		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
        		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
        		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	 return this.stationZCorr[v1][u1][station];
		}
		public double[] getXYZMAverage(int vu){ // u=0,v=0 - center!
			int width=this.gridGeometry[0].length;
			int u1=vu%width;
			int v1=vu/width;
        	if ((this.debugLevel >1) && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))) {
        		String msg="Requested (acquired) grid (point vu="+vu+") is outside of the physical grid ["+
        		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
        		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gridGeometry[v1][u1];
		}
		public double getZCorr(int vu, int station){ // u=0,v=0 - center!
        	if (this.stationZCorr==null) return 0.0;
			int width=this.gridGeometry[0].length;
			int u1=vu%width;
			int v1=vu/width;
        	if ((this.debugLevel >1) && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))) {
        		String msg="Requested (acquired) grid (point vu="+vu+") is outside of the physical grid ["+
        		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
        		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	 return this.stationZCorr[v1][u1][station];
		}

		
		
		/**
		 * Return grid geometry and photometics: X,Y,Z,mask,R,G,B,Alpha
		 * @param u       signed grid U (0 in the center)
		 * @param v       signed grid V (0 in the center)
		 * @param station station number
		 * @param channel channel (sensor) number
		 * @param verbose report out of grid
		 * @return null if out of grid, otherwise X,Y,Z,mask (binary),R (~0.5..1.2),G,B,alpha (0.0..1.0)
		 */
		public double[] getXYZMP(
				int u,
				int v,
				int station,
				int channel,
				boolean verbose){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length) ||
        			(this.gridGeometry[v1][u1][3]==0)) {
        		if ((this.debugLevel>1) && verbose && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))){
            		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
            		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
            		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		  //IJ.showMessage("Error",msg);
        		  System.out.println(msg);
        		}
        		return null;
        	}
        	int index=u1+v1*this.gridGeometry[0].length;
        	if (getNumStations()<=station) updateNumStations(station+1);
        	int nView=this.viewMap[channel];
			if (nView>=this.photometricByView[station].length){  // OOB 1// NUll pointer - need to run F-field first?
				nView=this.photometricByView.length-1;
			}

        	double [] result= { // null
        			this.gridGeometry[v1][u1][0],
        			this.gridGeometry[v1][u1][1],
        			this.gridGeometry[v1][u1][2]+((this.stationZCorr!=null)?this.stationZCorr[v1][u1][station]:0.0), // per-station correction
        			this.gridGeometry[v1][u1][3],
        			this.photometricByView[station][nView][0][index],
        			this.photometricByView[station][nView][1][index],
        			this.photometricByView[station][nView][2][index],
        			this.photometricByView[station][nView][3][index]
        	};
			return result;
		}
		public double[] getXYZMPE(
				int u,
				int v,
				int station,
				int channel,
				boolean verbose){ // u=0,v=0 - center!
			int u1=u+this.U0;
			int v1=v+this.V0;
        	if ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length) ||
        			(this.gridGeometry[v1][u1][3]==0)) {
        		if ((this.debugLevel>1) && verbose && ((v1<0) || (u1<0) || (v1 >= this.gridGeometry.length) || (u1 >= this.gridGeometry[0].length))){
            		String msg="Requested (acquired) grid (point u="+u+",v="+v+") is outside of the physical grid ["+
            		(-this.U0)+"..."+(this.gridGeometry[0].length-this.U0-1)+"]x["+
            		(-this.V0)+"..."+(this.gridGeometry.length-this.V0-1)+"]";
        		  //IJ.showMessage("Error",msg);
        		  System.out.println(msg);
        		}
        		return null;
        	}
        	int index=u1+v1*this.gridGeometry[0].length;
        	if (getNumStations()<=station) updateNumStations(station+1);
        	int nView=this.viewMap[channel];
			if (nView>=this.photometricByView[station].length){  // OOB 1// NUll pointer - need to run F-field first?
				nView=this.photometricByView.length-1;
			}
        	double [] result= { // null
        			this.gridGeometry[v1][u1][0],
        			this.gridGeometry[v1][u1][1],
        			this.gridGeometry[v1][u1][2]+((this.stationZCorr!=null)?this.stationZCorr[v1][u1][station]:0.0), // per-station correction
        			this.gridGeometry[v1][u1][3],
        			this.photometricByView[station][nView][0][index],
        			this.photometricByView[station][nView][1][index],
        			this.photometricByView[station][nView][2][index],
        			this.photometricByView[station][nView][3][index],
        			(this.patternErrorMask==null)?1.0:this.patternErrorMask[index]
        	};
			return result;
		}
		public double [][][] getGeometry(){return this.gridGeometry;} 
	}
	
	
	

	public class LensDistortionParameters {
		/*
		 * Hugin Rsrc=a*Rdest^4+b*Rdest^3+c*Rdest^2+d*Rdest; d=1-(a+b+c)
		 */
		// lens parameters (add more later)
		public double focalLength=4.5;
		public double pixelSize=  2.2; //um
		public double distortionRadius=  2.8512; // mm - half width of the sensor
		public double distortionA8=0.0; // r^8 (normalized to focal length or to sensor half width?)
		public double distortionA7=0.0; // r^7 (normalized to focal length or to sensor half width?)
		public double distortionA6=0.0; // r^6 (normalized to focal length or to sensor half width?)
		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
		public double distortionB=0.0; // r^3
		public double distortionC=0.0; // r^2
		// orientation/position parameters
		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
		public double x0=0;      // lens axis from pattern center, mm (to the right)
		public double y0=0;      // lens axis from pattern center, mm (down)
		public double z0=0;      // lens axis from pattern center, mm (away from the camera)
		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
		public double px0=1296.0;           // center of the lens on the sensor, pixels
		public double py0=968.0;           // center of the lens on the sensor, pixels
		public boolean flipVertical; // acquired image is mirrored vertically (mirror used)
		public int debugLevel=1; // was 2

// intermediate values
		public double phi, theta,psi,cPH,sPH,cTH,sTH,cPS,sPS;
		public double [][] rotMatrix=new double[3][3]; // includes mirroring for Y (target coordinates y- down, camera - y  up)
		
	    public LensDistortionParameters(
//	    		LensDistortionParameters lensDistortionParameters,
	    		boolean isTripod,
	            double [][] interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
	    		double [] parVect,
	    		boolean [] mask, // calculate only selected derivatives (all parVect values are still
	    		int debugLevel
//	    		boolean calculateDerivatives // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
	    		){
	    	this.debugLevel=debugLevel;
	    	lensCalcInterParamers( // changed name to move calcInterParamers method from enclosing class
		    		isTripod,
		            interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
		    		parVect,
		    		mask // calculate only selected derivatives (all parVect values are still
//		    		boolean calculateDerivatives // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
		    		);
	    }

		public LensDistortionParameters(
				double focalLength,
				double pixelSize,  //um
				double distortionRadius, // mm
				double distortionA8, // r^8
				double distortionA7, // r^7
				double distortionA6, // r^6
				double distortionA5, // r^5
				double distortionA, // r^4
				double distortionB, // r^3
				double distortionC, // r^2
				// orientation/position parameters
				double yaw,   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
				double pitch, // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
				double roll,  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
				double x0,      // lens axis from pattern center, mm (to the right)
				double y0,      // lens axis from pattern center, mm (down)
				double z0,      // lens axis from pattern center, mm (away from the camera)
				double distance, // distance from the lens input pupil to the pattern plane along the camera axis, mm
				double px0,           // center of the lens on the senosr, pixels
				double py0,           // center of the lens on the senosr, pixels
				boolean flipVertical // acquired image is mirrored vertically (mirror used)
		){
			setLensDistortionParameters(
			focalLength,
			pixelSize,
			distortionRadius,
			distortionA8,
			distortionA7,
			distortionA6,
			distortionA5,
			distortionA,
			distortionB,
			distortionC,
			yaw,
			pitch,
			roll,
			x0,
			y0,
			z0,
			distance,
			px0,
			py0,
			flipVertical);
		}
		public LensDistortionParameters(){
			this.focalLength=4.5;
			this.pixelSize=2.2;
			this.distortionRadius=2.8512;
			this.distortionA8=0.0;
			this.distortionA7=0.0;
			this.distortionA6=0.0;
			this.distortionA5=0.0;
			this.distortionA=0.0;
			this.distortionB=0.0;
			this.distortionC=0.0;
			this.yaw=0.0;
			this.pitch=0.0;
			this.roll=0.0;
			this.x0=0.0;
			this.y0=0.0;
			this.z0=0.0;
			this.distance=2360.0;
			this.px0=1296.0;
			this.py0=968.0;
			this.flipVertical=true;
		}
		public LensDistortionParameters clone() {
			return new LensDistortionParameters(
					this.focalLength,
					this.pixelSize,
					this.distortionRadius,
					this.distortionA8,
					this.distortionA7,
					this.distortionA6,
					this.distortionA5,
					this.distortionA,
					this.distortionB,
					this.distortionC,
					this.yaw,
					this.pitch,
					this.roll,
					this.x0,
					this.y0,
					this.z0,
					this.distance,
					this.px0,
					this.py0,
					this.flipVertical
			);
		}
		public void setLensDistortionParameters(
				double focalLength,
				double pixelSize,  //um
				double distortionRadius, // mm
				double distortionA8, // r^7
				double distortionA7, // r^6
				double distortionA6, // r^5
				double distortionA5, // r^4
				double distortionA, // r^4
				double distortionB, // r^3
				double distortionC, // r^2
				// orientation/position parameters
				double yaw,   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
				double pitch, // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
				double roll,  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
				double x0,      // lens axis from pattern center, mm (to the right)
				double y0,      // lens axis from pattern center, mm (down)
				double z0,      // lens axis from pattern center, mm (away)
				double distance, // distance from the lens input pupil to the pattern plane along the camera axis, mm
				double px0,           // center of the lens on the sensor, pixels
				double py0,           // center of the lens on the sensor, pixels
				boolean flipVertical // acquired image is mirrored vertically (mirror used)
		){
			this.focalLength=focalLength;
			this.pixelSize=pixelSize;
			this.distortionRadius=distortionRadius;
			this.distortionA8=distortionA8;
			this.distortionA7=distortionA7;
			this.distortionA6=distortionA6;
			this.distortionA5=distortionA5;
			this.distortionA=distortionA;
			this.distortionB=distortionB;
			this.distortionC=distortionC;
			this.yaw=yaw;
			this.pitch=pitch;
			this.roll=roll;
			this.x0=x0;
			this.y0=y0;
			this.z0=z0;
			this.distance=distance;
			this.px0=px0;
			this.py0=py0;
			this.flipVertical=flipVertical;
			recalcCommons();
		}
		public void setIntrincicFromSubcamera(EyesisSubCameraParameters pars){
			setLensDistortionParameters(
					pars.focalLength,
					pars.pixelSize,  //um
					pars.distortionRadius, // mm
					pars.distortionA8, // r^7
					pars.distortionA7, // r^6
					pars.distortionA6, // r^5
					pars.distortionA5, // r^4
					pars.distortionA,  // r^4
					pars.distortionB,  // r^3
					pars.distortionC,  // r^2
					// orientation/position parameters
					this.yaw,          // (keep) angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
					this.pitch,        // (keep) angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
					this.roll,         // (keep) angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
					this.x0,           // (keep) lens axis from pattern center, mm (to the right)
					this.y0,           // (keep) lens axis from pattern center, mm (down)
					this.z0,           // (keep) lens axis from pattern center, mm (away)
					this.distance,     // (keep) distance from the lens input pupil to the pattern plane along the camera axis, mm
					pars.px0,          //        center of the lens on the sensor, pixels
					pars.py0,          // center of the lens on the sensor, pixels
					this.flipVertical  // (keep)  acquired image is mirrored vertically (mirror used)
			);
		}
		
		
		public void setLensDistortionParameters(LensDistortionParameters ldp
		){
			this.focalLength=ldp.focalLength;
			this.pixelSize=ldp.pixelSize;
			this.distortionRadius=ldp.distortionRadius;
			this.distortionA8=ldp.distortionA8;
			this.distortionA7=ldp.distortionA7;
			this.distortionA6=ldp.distortionA6;
			this.distortionA5=ldp.distortionA5;
			this.distortionA=ldp.distortionA;
			this.distortionB=ldp.distortionB;
			this.distortionC=ldp.distortionC;
			this.yaw=ldp.yaw;
			this.pitch=ldp.pitch;
			this.roll=ldp.roll;
			this.x0=ldp.x0;
			this.y0=ldp.y0;
			this.z0=ldp.z0;
			this.distance=ldp.distance;
			this.px0=ldp.px0;
			this.py0=ldp.py0;
			this.flipVertical=ldp.flipVertical;
			recalcCommons();
		}
		// just for debugging				
		public void setLensDistortionParameters(LensDistortionParameters ldp,
				int index, // parameter to add delta, 1..13->14->17
				double delta
		){
			this.focalLength=ldp.focalLength+((index==7)?delta:0);
			this.pixelSize=ldp.pixelSize;
			this.distortionRadius=ldp.distortionRadius;
			this.distortionA8=ldp.distortionA8+((index==9)?delta:0);
			this.distortionA7=ldp.distortionA7+((index==10)?delta:0);
			this.distortionA6=ldp.distortionA6+((index==11)?delta:0);
			this.distortionA5=ldp.distortionA5+((index==12)?delta:0);
			this.distortionA=ldp.distortionA+((index==13)?delta:0);
			this.distortionB=ldp.distortionB+((index==14)?delta:0);
			this.distortionC=ldp.distortionC+((index==15)?delta:0);
			this.yaw=ldp.yaw+((index==1)?delta:0);
			this.pitch=ldp.pitch+((index==2)?delta:0);
			this.roll=ldp.roll+((index==3)?delta:0);
			this.x0=ldp.x0+((index==4)?delta:0);
			this.y0=ldp.y0+((index==5)?delta:0);
			this.z0=ldp.z0+((index==6)?delta:0);
			this.distance=ldp.distance+((index==8)?delta:0);
			this.px0=ldp.px0+((index==16)?delta:0);
			this.py0=ldp.py0+((index==17)?delta:0);
			this.flipVertical=ldp.flipVertical;
			recalcCommons();
		}
		
// recalculate common (point-invariant) intermediate values (cos, sin, rotation matrix) 		
        public void recalcCommons(){
//		public double phi, theta,psi,cPH,sPH,cTH,sTH,cPS,sPS;
//    		public double [][] rotMatrix=new double[3][3];
        	this.phi=  this.yaw*Math.PI/180;
        	this.theta=this.pitch*Math.PI/180;
        	this.psi=  this.roll*Math.PI/180;
        	this.sPH=Math.sin(this.phi);
        	this.cPH=Math.cos(this.phi);
        	this.sTH=Math.sin(this.theta);
        	this.cTH=Math.cos(this.theta);
        	this.sPS=Math.sin(this.psi);
        	this.cPS=Math.cos(this.psi);
/*
| Xe |   |   0  |   | cPS*cPH+sPS*sTH*sPH   -sPS*cTH  -cPS*sPH+sPS*sTH*cPH |   | Xp |
| Ye | = |   0  | + | sPS*cPH-cPS*sTH*sPH    cPS*cTH  -sPS*sPH-cPS*sTH*cPH | * |-Yp |
| Ze |   | dist |   | cTH*sPH                    sTH       cTH*cPH         |   | Zp |

| PX | =(1000*f)/(Ze*Psz) * |  Xe | + | PX0 |
| PY | =                    | -Ye |   | PY0 |


 Xe =   (cPS*cPH+sPS*sTH*sPH)*Xp   +sPS*cTH*Yp  +(-cPS*sPH+sPS*sTH*cPH)*Zp
 Ye =   (sPS*cPH-cPS*sTH*sPH)*Xp   -cPS*cTH*Yp  +(-sPS*sPH-cPS*sTH*cPH)*Zp
 Ze =   (cTH*sPH)*Xp               -sTH*Yp      +( cTH*cPH)* Zp + dist

        	
theta==0, psi==0:        	
 Xe =   (cPH)*Xp 
 Ye =   Yp
 Ze =   cPH* Zp + dist

(4) PXmmc =f/(cPH* Zp + dist)*  (cPH)*Xp  // mm, left from the lens axis intersection with the sensor
        	
dPXmmc/dphi=
        	
 */
        	this.rotMatrix[0][0]= cPS*cPH+sPS*sTH*sPH;
        	this.rotMatrix[0][1]= sPS*cTH;
           	this.rotMatrix[0][2]=-cPS*sPH+sPS*sTH*cPH;
           	this.rotMatrix[1][0]= sPS*cPH-cPS*sTH*sPH;
           	this.rotMatrix[1][1]=-cPS*cTH;
           	this.rotMatrix[1][2]=-sPS*sPH-cPS*sTH*cPH;
           	this.rotMatrix[2][0]= cTH*sPH;
           	this.rotMatrix[2][1]=-sTH;
           	this.rotMatrix[2][2]= cTH*cPH;
           	if (this.debugLevel>2){
           		System.out.println("recalcCommons():this.rotMatrix:");
        		(new Matrix(this.rotMatrix)).print(10, 5);
           	}
        	
        }
        
        private String [][] descriptions=
        {
        		{"distance",    "Distance from the intersection of the lens axis with z=0 target plane to the camera lens entrance pupil", "mm", "e"},
        		{"x0",          "Lens axis from pattern center, (to the right)", "mm","e"},
        		{"y0",          "Lens axis from pattern center, (down)", "mm","e"},
        		{"yaw",         "Angle from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top", "degrees","e"},
        		{"pitch",       "Angle from perpendicular to the pattern, 0 - towards wall, positive - up", "degrees","e"},
        		{"roll",        "Angle around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise", "degrees","e"},
        		{"focalLength", "Lens focal length", "mm","i"},
        		{"px0",         "horizontal (left to right) pixel number of the lens axis intersection with the sensor", "pix","i"},
        		{"py0",         "vertical (up to down) pixel number of the lens axis intersection with the sensor", "pix","i"},
        		{"distortionA8","Lens distortion coefficient for r^8 (r0=half sensor width)", "r0","i"},
        		{"distortionA7","Lens distortion coefficient for r^7 (r0=half sensor width)", "r0","i"},
        		{"distortionA6","Lens distortion coefficient for r^6 (r0=half sensor width)", "r0","i"},
        		{"distortionA5","Lens distortion coefficient for r^5 (r0=half sensor width)", "r0","i"},
        		{"distortionA", "Lens distortion coefficient for r^4 (r0=half sensor width)", "r0","i"},
        		{"distortionB", "Lens distortion coefficient for r^3 (r0=half sensor width)", "r0","i"},
        		{"distortionC", "Lens distortion coefficient for r^2 (r0=half sensor width)", "r0","i"}
        };
        private int numberIntExtrisic(String type){
        	int num=0;
        	for (int i=0;i<this.descriptions.length;i++) if (type.indexOf(descriptions[i][3].charAt(0))>=0) num++;
        	return num;
        }
        /**
         * Verifies that the camera is looking towards the target
         * @return true if looking tio the target, false - if away
         */
        public boolean isTargetVisible(boolean verbose){
        	if (verbose) System.out.println("isTargetVisible(): this.distance="+this.distance+", this.yaw="+this.yaw+", this.pitch="+this.pitch);
        	if (this.distance <=0.0) return false;
        	if (Math.cos(this.yaw*Math.PI/180)<0.0) return false;
        	if (Math.cos(this.pitch*Math.PI/180)<0.0) return false;
        	return true;
        }

        public double [] getExtrinsicVector(){
        	double [] extVector = {
        			this.distance,
        			this.x0,
        			this.y0,
        			this.yaw,
        			this.pitch,
        			this.roll
        			};
        	return extVector;
        }
        public double [] getIntrinsicVector(){
        	double [] extVector = {
        			this.focalLength,
        			this.px0,
        			this.py0,
        			this.distortionA8,
        			this.distortionA7,
        			this.distortionA6,
        			this.distortionA5,
        			this.distortionA,
        			this.distortionB,
        			this.distortionC
        	};
        	return extVector;
        }
        public double [] getAllVector(){
        	double [] allVector = new double[getExtrinsicVector().length+getIntrinsicVector().length];
        	int index=0;
        	double [] extVector=getExtrinsicVector();
        	double [] intVector=getIntrinsicVector();
        	for (int i=0;i<extVector.length;i++) allVector[index++]= extVector[i];
        	for (int i=0;i<intVector.length;i++) allVector[index++]= intVector[i];
        	return allVector;
        }
        public void setAllVector(double [] vector){
        	if (vector.length!=(getExtrinsicVector().length+getIntrinsicVector().length)){
        		String msg="Parameter vector should have exactly"+(getExtrinsicVector().length+getIntrinsicVector().length)+" elements";
        		IJ.showMessage("Error",msg); 
        		throw new IllegalArgumentException (msg);
        	}
        	this.distance=    vector[ 0];
        	this.x0=          vector[ 1];
        	this.y0=          vector[ 2];
        	this.yaw=         vector[ 3];
        	this.pitch=       vector[ 4];
        	this.roll=        vector[ 5];
        	this.focalLength= vector[ 6];
        	this.px0=         vector[ 7];
        	this.py0=         vector[ 8];
        	this.distortionA8=vector[ 9];
        	this.distortionA7=vector[10];
        	this.distortionA6=vector[11];
        	this.distortionA5=vector[12];
        	this.distortionA= vector[13];
        	this.distortionB= vector[14];
        	this.distortionC= vector[15];
        	recalcCommons();
        }
        
        public String [] getExtrinsicNames()       {return getDescriptionStrings("e", 0);}
        public String [] getExtrinsicDescriptions(){return getDescriptionStrings("e", 1);}
        public String [] getExtrinsicUnits()       {return getDescriptionStrings("e", 2);}
        public String [] getIntrinsicNames()       {return getDescriptionStrings("i", 0);}
        public String [] getIntrinsicDescriptions(){return getDescriptionStrings("i", 1);}
        public String [] getIntrinsicUnits()       {return getDescriptionStrings("i", 2);}
        public String [] getAllNames()             {return getDescriptionStrings("ei", 0);}
        public String [] getAllDescriptions()      {return getDescriptionStrings("ei", 1);}
        public String [] getAllUnits()             {return getDescriptionStrings("ei", 2);}
        public String [] getAllFlags()             {return getDescriptionStrings("ei", 3);}

        private String [] getDescriptionStrings(String type, int var){
        	String [] s=new String [numberIntExtrisic(type)];
        	int num=0;
        	for (int i=0;i<this.descriptions.length;i++) if (type.indexOf(descriptions[i][3].charAt(0))>=0) s[num++]=this.descriptions[i][var];
        	return s;
        }
        
        
/*
 * Calculate pixel value of projection from the pattern point [xp,yp,zp] using current distortion/position parameters
(1) Xe =   (cPS*cPH+sPS*sTH*sPH)*(Xp-X0)   +sPS*cTH*(Yp-Y0)  +(-cPS*sPH+sPS*sTH*cPH)*(Zp-Z0)
(2) Ye =   (sPS*cPH-cPS*sTH*sPH)*(Xp-X0)   -cPS*cTH*(Yp-Y0)  +(-sPS*sPH-cPS*sTH*cPH)*(Zp-Z0)
(3) Ze =   (cTH*sPH)*(Xp-X0)               -sTH*(Yp-Y0)      +( cTH*cPH)* (Zp-Z0) + dist

(4) PXmmc =f/Ze*  Xe  // mm, left from the lens axis intersection with the sensor
(5) PYmmc =f/Ze*  Ye  // mm, up from the lens axis intersection with the sensor

(6) r=sqrt(PXmmc^2+PYmmc^2) // distance from the image point to the lens axis intersection with the sensor (pinhole model)
(7) kD=(Da*(r/r0)^3+Db*(r/r0)^2+Dc*(r/r0)^1+(1-Da-Db-Dc)) correction to the actual distance from the image point to the lens axis due to distortion
(8) xDist = kD *  PXmmc // horisontal distance (mm) from the lens axis on the sensor to the image point, mm (positive - right)
(9) yDist = kD *  PYmmc // vertical distance (mm) from the lens axis on the sensor to the image point, mm (positive - up)


(10) PX = 1000/Psz*( xDist) + PX0 // horizontal pixel of the image (positive - right)
(11) PY = 1000/Psz*(-yDist) + PY0 // vertical pixel of the image (positive - down)
        
 */
        public double [] patternToPixels(
        		double xp, // target point horizontal, positive - right,  mm
        		double yp, // target point vertical,   positive - down,  mm
        		double zp // target point horizontal, positive - away from camera,  mm
        		){
        	return calcPartialDerivatives(xp,yp,zp,false)[0];
        }
/*
0		public double x0=0;      // lens axis from pattern center, mm (to the right)
1		public double y0=0;      // lens axis from pattern center, mm (down)
2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
6		public double focalLength=4.5;
7		public double px0=1296.0;           // center of the lens on the sensor, pixels
8		public double py0=968.0;           // center of the lens on the sensor, pixels
 		public double distortionRadius=  2.8512; // mm - half width of the sensor
9		public double distortionA8=0.0; // r^8 (normalized to focal length or to sensor half width?)
10		public double distortionA7=0.0; // r^7 (normalized to focal length or to sensor half width?)
11		public double distortionA6=0.0; // r^6 (normalized to focal length or to sensor half width?)
12		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
13		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
14		public double distortionB=0.0; // r^3
15		public double distortionC=0.0; // r^2

 */
        /**
         *  extract needed ones, and reorder partial derivatives to match calcInterParameters
         *  @param srcDerivatives - values and 15 derivatives for px, py
         */
        public double [][] reorderPartialDerivatives (double [][] srcDerivatives){
        	int [] order={
        			 4, // 0		public double x0=0;      // lens axis from pattern center, mm (to the right)
        			 5, // 1		public double y0=0;      // lens axis from pattern center, mm (down)
        			 8, // 2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
        			 1, // 3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
        			 2, // 4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
        			 3, // 5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
        			 7, // 6		public double focalLength=4.5;
        			16, // 7		public double px0=1296.0;           // center of the lens on the sensor, pixels
        			17, // 8		public double py0=968.0;           // center of the lens on the sensor, pixels
        			    // 	public double distortionRadius=  2.8512; // mm - half width of the sensor
       			     9, // 9		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
    			    10, // 10		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
    			    11, // 11		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			12, // 12		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			13, // 13		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
        			14, // 14		public double distortionB=0.0; // r^3
        			15  // 15		public double distortionC=0.0; // r^2
        	};
        	double [][] result = new double [order.length][2];
        	for (int i=0; i<order.length;i++){
        		result[i][0]=srcDerivatives[order[i]][0];
        		result[i][1]=srcDerivatives[order[i]][1];
        	}
        	return result;
        	
        }
/**
 * Reorder to match the sequence of names - seems to be different :-(        
 * @param srcDerivatives  values and 15 derivatives for px, py
 * @return
 */
        public double [][] reorderPartialDerivativesAsNames (double [][] srcDerivatives){
        	int [] order={
        			 8, // 2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
        			 4, // 0		public double x0=0;      // lens axis from pattern center, mm (to the right)
        			 5, // 1		public double y0=0;      // lens axis from pattern center, mm (down)
        			 1, // 3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
        			 2, // 4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
        			 3, // 5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
        			 7, // 6		public double focalLength=4.5;
        			16, // 7		public double px0=1296.0;           // center of the lens on the sensor, pixels
        			17, // 8		public double py0=968.0;           // center of the lens on the sensor, pixels
        			 9, // 9		public double distortionA8=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			10, // 10		public double distortionA7=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			11, // 11		public double distortionA6=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			12, // 12		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
        			13, // 13		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
        			14, // 14		public double distortionB=0.0; // r^3
        			15  // 15		public double distortionC=0.0; // r^2
        	};
        	double [][] result = new double [order.length][2];
        	for (int i=0; i<order.length;i++){
        		result[i][0]=srcDerivatives[order[i]][0];
        		result[i][1]=srcDerivatives[order[i]][1];
        	}
        	return result;
        	
        }
/**
 * Calculate lens center location in target coordinates
 * @return lens center coordinates
 */
        public double [] getLensCenterCoordinates(){
        	double [] p={
        			 this.x0-this.distance*this.rotMatrix[2][0],
        			-this.y0-this.distance*this.rotMatrix[2][1], // this.y0 - up?
        			 this.z0-this.distance*this.rotMatrix[2][2]};
/*        	
        	Matrix MR=new Matrix(this.rotMatrix);
        	Matrix MRT=MR.transpose();
        	Matrix E=MR.times(MRT);
        	System.out.println("===MR==");  	MR.print(8, 5);System.out.println("");
        	System.out.println("===MRT==");  	MRT.print(8, 5);System.out.println("");
        	System.out.println("===E===");  	E.print(8, 5);System.out.println("");
        	
        	System.out.println("x0="+IJ.d2s(this.x0,1)+" y0="+IJ.d2s(this.y0,1)+" z0="+IJ.d2s(this.z0,1)+" this.distance="+IJ.d2s(this.distance,1));
        	System.out.println("phi="+IJ.d2s(this.phi,4)+" theta="+IJ.d2s(this.theta,4)+" psi="+IJ.d2s(this.psi,4));
        	
        	System.out.println("|"+IJ.d2s(this.rotMatrix[0][0],4)+" "+IJ.d2s(this.rotMatrix[0][1],4)+" "+IJ.d2s(this.rotMatrix[0][2],4)+"|");
        	System.out.println("|"+IJ.d2s(this.rotMatrix[1][0],4)+" "+IJ.d2s(this.rotMatrix[1][1],4)+" "+IJ.d2s(this.rotMatrix[1][2],4)+"|");
        	System.out.println("|"+IJ.d2s(this.rotMatrix[2][0],4)+" "+IJ.d2s(this.rotMatrix[2][1],4)+" "+IJ.d2s(this.rotMatrix[2][2],4)+"|");
*/        	
        	return p;
        }

/*
 * Calculate pixel value and partial derivatives for different parameters
 * of projection from the pattern point [xp,yp,zp] using current distortion/position parameters
 * [0][0] - pixel x - NaN if looking away
 * [1][ 0] - pixel y- NaN if looking away
 * [*][ 1] - pixel x[or y] partial derivative for phi    (yaw)
 * [*][ 2] - pixel x[or y] partial derivative for theta  (pitch)
 * [*][ 3] - pixel x[or y] partial derivative for psi    (roll)
 * [*][ 4] - pixel x[or y] partial derivative for X0 (intersection of the lens axis with zp==Z0 plane of the target)
 * [*][ 5] - pixel x[or y] partial derivative for Y0 (intersection of the lens axis with zp==Z0 plane of the target)
 * [*][ 6] - pixel x[or y] partial derivative for Z0 (intersection of the lens axis with zp==Z0 plane of the target) - not used
 * [*][ 7] - pixel x[or y] partial derivative for f  (focal length)
 * [*][ 8] - pixel x[or y] partial derivative for dist (distance from [X0,Y0,Z0] to the lens entrance pupil
 * [*][ 9] - pixel x[or y] partial derivative for Da8 (distortion coefficient for r^8)
 * [*][10] - pixel x[or y] partial derivative for Da7 (distortion coefficient for r^7)
 * [*][11] - pixel x[or y] partial derivative for Da6 (distortion coefficient for r^6)
 * [*][12] - pixel x[or y] partial derivative for Da5 (distortion coefficient for r^5)
 * [*][13] - pixel x[or y] partial derivative for Da (distortion coefficient for r^4)
 * [*][14] - pixel x[or y] partial derivative for Db (distortion coefficient for r^3)
 * [*][15] - pixel x[or y] partial derivative for Dc (distortion coefficient for r^2)
 * [*][16] - pixel x[or y] partial derivative for PX0 (lens axis on the sensor, horizontal, right,in pixels)
 * [*][17] - pixel x[or y] partial derivative for PY0 (lens axis on the sensor, vertical, down, in pixels)
 */
        /*
         * TODO: minimaize calculations for individual {xp,yp,zp}
         */
        public double [][] calcPartialDerivatives(
        		double xp, // target point horizontal, positive - right,  mm
        		double yp, // target point vertical,   positive - down,  mm
        		double zp, // target point horizontal, positive - away from camera,  mm
        		boolean calculateAll){ // calculate derivatives, false - values only
        	double partDeriv[][] = new double [calculateAll?18:1][2];
//        	double [] XYZ= {xp-this.x0, yp-this.y0, zp-this.z0};
        	double [] XYZ= {xp-this.x0, yp+this.y0, zp-this.z0};
        	double [] XeYeZe={
        			this.rotMatrix[0][0]*XYZ[0] + this.rotMatrix[0][1]*XYZ[1] + this.rotMatrix[0][2]*XYZ[2],
        			this.rotMatrix[1][0]*XYZ[0] + this.rotMatrix[1][1]*XYZ[1] + this.rotMatrix[1][2]*XYZ[2],
        			this.rotMatrix[2][0]*XYZ[0] + this.rotMatrix[2][1]*XYZ[1] + this.rotMatrix[2][2]*XYZ[2]+this.distance
        	};
        	double [] PXYmmc={this.focalLength/XeYeZe[2]*XeYeZe[0],this.focalLength/XeYeZe[2]*XeYeZe[1]};
        	double r= Math.sqrt(PXYmmc[0]*PXYmmc[0]+PXYmmc[1]*PXYmmc[1]);
        	double rr=r/this.distortionRadius;
//        	double kD=((this.distortionA*rr+this.distortionB)*rr+this.distortionC)*rr + 1.0-this.distortionA-this.distortionB-this.distortionC;
        	double kD=((((((this.distortionA8*rr+this.distortionA7)*rr+ this.distortionA6)*rr+ this.distortionA5)*rr+this.distortionA)*rr+this.distortionB)*rr+this.distortionC)*rr +
        	1.0-this.distortionA8-this.distortionA7-this.distortionA6-this.distortionA5-this.distortionA-this.distortionB-this.distortionC;
        	double [] xyDist={kD*PXYmmc[0],kD*PXYmmc[1]};
        	partDeriv[0][0]=  1000.0/this.pixelSize*xyDist[0] + this.px0;
        	partDeriv[0][1]= -1000.0/this.pixelSize*xyDist[1] + this.py0;
        	if (!calculateAll) {
        		// TODO: Looking away from the target, trying only with no dervatives. Do the same for derivatives too?
            	if (XeYeZe[2]<0.0) {
            		partDeriv[0][0]=Double.NaN;
            		partDeriv[0][1]=Double.NaN;
            	}
        		return partDeriv;
        	}
        	
            double [][] dXeYeZe=new double[9][3]; //[14];
            dXeYeZe[0][0]=XeYeZe[0];
            dXeYeZe[0][1]=XeYeZe[1];
            dXeYeZe[0][2]=XeYeZe[2];
// /dphi            
            dXeYeZe[1][0]=(-cPS*sPH + sPS*sTH*cPH)*XYZ[0]                    +(-cPS*cPH-sPS*sTH*sPH)*XYZ[2];
            dXeYeZe[1][1]=(-sPS*sPH-cPS*sTH*cPH)  *XYZ[0]                    +(-sPS*cPH+cPS*sTH*sPH)*XYZ[2];
            dXeYeZe[1][2]=(cTH*cPH)*XYZ[0]                                     -cTH*sPH* XYZ[2];
// /dtheta
            dXeYeZe[2][0]=(sPS*cTH*sPH)*XYZ[0]              -sPS*sTH*XYZ[1]  +(sPS*cTH*cPH)*XYZ[2];
            dXeYeZe[2][1]=(-cPS*cTH*sPH)*XYZ[0]             +cPS*sTH*XYZ[1]  +(-cPS*cTH*cPH)*XYZ[2];
            dXeYeZe[2][2]=(-sTH*sPH)*XYZ[0]                 -cTH*XYZ[1]      -sTH*cPH* XYZ[2];
// /dpsi            
            dXeYeZe[3][0]=(-sPS*cPH+cPS*sTH*sPH)*XYZ[0]     +cPS*cTH*XYZ[1]  +(sPS*sPH+cPS*sTH*cPH)*XYZ[2];
            dXeYeZe[3][1]=(cPS*cPH+ sPS*sTH*sPH)*XYZ[0]     +sPS*cTH*XYZ[1]  +(-cPS*sPH+sPS*sTH*cPH)*XYZ[2];
            dXeYeZe[3][2]=0.0;
// /dX0
//          dXeYeZe[4][0]=-cPS*cPH+sPS*sTH*sPH; // bad?
            dXeYeZe[4][0]=-cPS*cPH-sPS*sTH*sPH; // bad?
            dXeYeZe[4][1]=-sPS*cPH+cPS*sTH*sPH;
            dXeYeZe[4][2]=-cTH*sPH;
// /dY0
//            dXeYeZe[5][0]=-sPS*cTH;
//            dXeYeZe[5][1]= cPS*cTH;
//            dXeYeZe[5][2]= sTH;
            dXeYeZe[5][0]=+sPS*cTH;
            dXeYeZe[5][1]=-cPS*cTH;
            dXeYeZe[5][2]=-sTH;
// /dZ0
//          dXeYeZe[6][0]= cPS*sPH+sPS*sTH*cPH; //bad?
            dXeYeZe[6][0]= cPS*sPH-sPS*sTH*cPH; //bad?
            dXeYeZe[6][1]= sPS*sPH+cPS*sTH*cPH;
            dXeYeZe[6][2]=-cTH*cPH;
// /df
            dXeYeZe[7][0]=0.0;
            dXeYeZe[7][1]=0.0;
            dXeYeZe[7][2]=0.0;
// /ddist
            dXeYeZe[8][0]=0.0;
            dXeYeZe[8][1]=0.0;
            dXeYeZe[8][2]=1.0;

            double [][] dPXYmmc=new double[9][2]; //[14];
            dPXYmmc[0][0]=PXYmmc[0];
            dPXYmmc[0][1]=PXYmmc[1];
//(4) PXmmc =f/Ze*  Xe  // mm, left from the lens axis intersection with the sensor
//dPXmmc/dphi   = f/Ze * dXe/dphi - f*Xe/Ze^2 * dZe/dphi
            dPXYmmc[1][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[1][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[1][2]);
//dPXmmc/dtheta = f/Ze * dXe/dtheta - f*Xe/Ze^2 * dZe/dtheta
            dPXYmmc[2][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[2][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[2][2]);
//dPXmmc/dpsi   = f/Ze * dXe/dpsi - f*Xe/Ze^2 * dZe/dpsi
            dPXYmmc[3][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[3][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[3][2]);
//dPXmmc/dX0    = f/Ze * dXe/dX0 - f*Xe/Ze^2 * dZe/dX0
            dPXYmmc[4][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[4][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[4][2]);
//dPXmmc/dY0    = f/Ze * dXe/dY0 - f*Xe/Ze^2 * dZe/dY0
            dPXYmmc[5][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[5][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[5][2]);
//dPXmmc/dZ0    = f/Ze * dXe/dZ0 - f*Xe/Ze^2 * dZe/dZ0
            dPXYmmc[6][0]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[6][0]-dXeYeZe[0][0]/dXeYeZe[0][2]*dXeYeZe[6][2]); //bad?
//dPXmmc/df     = Xe/Ze
            dPXYmmc[7][0]=dXeYeZe[0][0]/dXeYeZe[0][2];
//dPXmmc/ddist =  - f*Xe/Ze^2
            dPXYmmc[8][0]=-this.focalLength*dXeYeZe[0][0]/(dXeYeZe[0][2]*dXeYeZe[0][2]);

//(5) PYmmc =f/Ze*  Ye  // mm, up from the lens axis intersection with the sensor
//dPYmmc/dphi   = f/Ze * dYe/dphi - f*Ye/Ze^2 * dZe/dphi
            dPXYmmc[1][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[1][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[1][2]);
//dPYmmc/dtheta = f/Ze * dYe/dtheta - f*Ye/Ze^2 * dZe/dtheta
            dPXYmmc[2][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[2][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[2][2]);
//dPYmmc/dpsi   = f/Ze * dYe/dpsi - f*Ye/Ze^2 * dZe/dpsi
            dPXYmmc[3][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[3][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[3][2]);
//dPYmmc/dX0    = f/Ze * dYe/dX0 - f*Ye/Ze^2 * dZe/dX0
            dPXYmmc[4][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[4][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[4][2]);
//dPYmmc/dY0    = f/Ze * dYe/dY0 - f*Ye/Ze^2 * dZe/dY0
            dPXYmmc[5][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[5][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[5][2]);
//dPYmmc/dZ0    = f/Ze * dYe/dZ0 - f*Ye/Ze^2 * dZe/dZ0
            dPXYmmc[6][1]=this.focalLength/dXeYeZe[0][2]*(dXeYeZe[6][1]-dXeYeZe[0][1]/dXeYeZe[0][2]*dXeYeZe[6][2]); // good?
//dPYmmc/df     = Ye/Ze
            dPXYmmc[7][1]=dXeYeZe[0][1]/dXeYeZe[0][2];
//dPYmmc/ddist =  - f*Ye/Ze^2
            dPXYmmc[8][1]=-this.focalLength*dXeYeZe[0][1]/(dXeYeZe[0][2]*dXeYeZe[0][2]);
            double[] dr=new double [9];
//(6) r=sqrt(PXmmc^2+PYmmc^2) // distance from the image point to the lens axis intersection with the sensor (pinhole model)
            dr[0]=r;
            // Added 3 zeros
            if (dr[0]<0.00000001*this.distortionRadius) dr[0]=0.00000001*this.distortionRadius; // avoid 1/0.0
// dr/dphi   = (PXmmc*dPXmmc/dphi+PYmmc*dPYmmc/dphi)/r
            dr[1]=(PXYmmc[0]*dPXYmmc[1][0]+PXYmmc[1]*dPXYmmc[1][1])/dr[0];
// dr/dtheta = (PXmmc*dPXmmc/dtheta+PYmmc*dPYmmc/dtheta)/r
            dr[2]=(PXYmmc[0]*dPXYmmc[2][0]+PXYmmc[1]*dPXYmmc[2][1])/dr[0];
// dr/dpsi   = (PXmmc*dPXmmc/dpsi+PYmmc*dPYmmc/dpsi)/r
            dr[3]=(PXYmmc[0]*dPXYmmc[3][0]+PXYmmc[1]*dPXYmmc[3][1])/dr[0];
// dr/dX0    = (PXmmc*dPXmmc/dX0+PYmmc*dPYmmc/dX0)/r
            dr[4]=(PXYmmc[0]*dPXYmmc[4][0]+PXYmmc[1]*dPXYmmc[4][1])/dr[0];
// dr/dY0    = (PXmmc*dPXmmc/dY0+PYmmc*dPYmmc/dY0)/r
            dr[5]=(PXYmmc[0]*dPXYmmc[5][0]+PXYmmc[1]*dPXYmmc[5][1])/dr[0];
// dr/dZ0    = (PXmmc*dPXmmc/dZ0+PYmmc*dPYmmc/dZ0)/r
            dr[6]=(PXYmmc[0]*dPXYmmc[6][0]+PXYmmc[1]*dPXYmmc[6][1])/dr[0];
// dr/df     = (PXmmc*dPXmmc/df+PYmmc*dPYmmc/df)/r
            dr[7]=(PXYmmc[0]*dPXYmmc[7][0]+PXYmmc[1]*dPXYmmc[7][1])/dr[0];
// dr/ddist =  (PXmmc*dPXmmc/ddist+PYmmc*dPYmmc/ddist)/r
            dr[8]=(PXYmmc[0]*dPXYmmc[8][0]+PXYmmc[1]*dPXYmmc[8][1])/dr[0];
//            double[] dkD=new double [12];
            double[] dkD=new double [16];
// (7) kD=(Da*(r/r0)^3+Db*(r/r0)^2+Dc*(r/r0)^1+(1-Da-Db-Dc)) correction to the actual distance from the image point to the lens axis due to distortion
// (7) kD=(Da5*(r/r0)^4+(Da*(r/r0)^3+Db*(r/r0)^2+Dc*(r/r0)^1+(1-Da-Db-Dc-Da5)) correction to the actual distance from the image point to the lens axis due to distortion
            dkD[0]=kD;
// dkD/dr=  Da/r0^3 * 3*r^2 + Db/r0^2*2*r + Dc/r0
// dkD/dr=  Da5/r0^4 * 4*r^3 + Da/r0^3 * 3*r^2 + Db/r0^2*2*r + Dc/r0
            
// dkD/dr=  1/r0*(3*Da*(r/r0)^2 + 2*Db*(r/r0) + Dc)
// dkD/dr=  1/r0*(4*Da5*(r/r0)^3 + 3*Da*(r/r0)^2 + 2*Db*(r/r0) + Dc)            
            
            double dkDdr=1.0/this.distortionRadius*((((((8*this.distortionA8*rr+6*this.distortionA7)*rr+5*this.distortionA6)*rr+4*this.distortionA5)*rr+3*this.distortionA)*rr+2*this.distortionB)*rr + this.distortionC);
// dkD/dphi   = dkD/dr * dr/dphi
            dkD[1]=dkDdr*dr[1];
// dkD/dtheta = dkD/dr * dr/dtheta
            dkD[2]=dkDdr*dr[2];
// dkD/dpsi   = dkD/dr * dr/dpsi
            dkD[3]=dkDdr*dr[3];
// dkD/dX0    = dkD/dr * dr/dX0
            dkD[4]=dkDdr*dr[4];
// dkD/dY0    = dkD/dr * dr/dY0
            dkD[5]=dkDdr*dr[5];
// dkD/dZ0    = dkD/dr * dr/dZ0
            dkD[6]=dkDdr*dr[6];
// dkD/df     = dkD/dr * dr/df
            dkD[7]=dkDdr*dr[7];
// dkD/ddist  = dkD/dr * dr/ddist
            dkD[8]=dkDdr*dr[8];

// dkD/dDa8   = (r/r0)^7 - 1
            dkD[ 9]=rr*rr*rr*rr*rr*rr*rr-1.0;
// dkD/dDa7   = (r/r0)^6 - 1
            dkD[10]=rr*rr*rr*rr*rr*rr-1.0;
// dkD/dDa6   = (r/r0)^5 - 1
            dkD[11]=rr*rr*rr*rr*rr-1.0;
            
            
// dkD/dDa5   = (r/r0)^4 - 1
            dkD[12]=rr*rr*rr*rr-1.0;
// dkD/dDa    = (r/r0)^3 - 1
            dkD[13]=rr*rr*rr-1.0;
// dkD/dDb    = (r/r0)^2 - 1
            dkD[14]=rr*rr-1.0;
// dkD/dDc    = (r/r0)   - 1
            dkD[15]=rr-1.0;
            
//            double[][] dxyDist=new double [12][2];
            double[][] dxyDist=new double [16][2];
// (8) xDist = kD *  PXmmc // horisontal distance (mm) from the lens axis on the sensor to the image point, mm (positive - right)
// (9) yDist = kD *  PYmmc // vertical distance (mm) from the lens axis on the sensor to the image point, mm (positive - up)
            dxyDist[0][0]=xyDist[0];
            dxyDist[0][1]=xyDist[1];
// dxDist/dphi   = dkD/dphi*PXmmc   + kD*dPXmmc/dphi
// dyDist/dphi   = dkD/dphi*PYmmc   + kD*dPYmmc/dphi
            dxyDist[ 1][0]=dkD[ 1]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 1][0];
            dxyDist[ 1][1]=dkD[ 1]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 1][1];
// dxDist/dtheta = dkD/dtheta*PXmmc + kD*dPXmmc/dtheta
            dxyDist[ 2][0]=dkD[ 2]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 2][0];
            dxyDist[ 2][1]=dkD[ 2]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 2][1];
// dxDist/dpsi   = dkD/dpsi*PXmmc   + kD*dPXmmc/dpsi
            dxyDist[ 3][0]=dkD[ 3]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 3][0];
            dxyDist[ 3][1]=dkD[ 3]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 3][1];
// dxDist/dX0    = dkD/dX0*PXmmc    + kD*dPXmmc/dX0
            dxyDist[ 4][0]=dkD[ 4]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 4][0]; // large error
            dxyDist[ 4][1]=dkD[ 4]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 4][1];
// dxDist/dY0    = dkD/dY0*PXmmc    + kD*dPXmmc/dY0
            dxyDist[ 5][0]=dkD[ 5]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 5][0];
            dxyDist[ 5][1]=dkD[ 5]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 5][1];
// dxDist/dZ0    = dkD/dZ0*PXmmc    + kD*dPXmmc/dZ0
            dxyDist[ 6][0]=dkD[ 6]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 6][0]; // large error
            dxyDist[ 6][1]=dkD[ 6]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 6][1];
// dxDist/df     = dkD/df*PXmmc     + kD*dPXmmc/df
            dxyDist[ 7][0]=dkD[ 7]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 7][0];
            dxyDist[ 7][1]=dkD[ 7]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 7][1];
// dxDist/ddist  = dkD/ddist*PXmmc  + kD*dPXmmc/ddist
            dxyDist[ 8][0]=dkD[ 8]*dPXYmmc[0][0]+dkD[0]*dPXYmmc[ 8][0];
            dxyDist[ 8][1]=dkD[ 8]*dPXYmmc[0][1]+dkD[0]*dPXYmmc[ 8][1];

// dxDist/dDa5    = dkD/dDa8*PXmmc
            dxyDist[ 9][0]=dkD[ 9]*dPXYmmc[0][0];
            dxyDist[ 9][1]=dkD[ 9]*dPXYmmc[0][1];
// dxDist/dDa5    = dkD/dDa7*PXmmc
            dxyDist[10][0]=dkD[10]*dPXYmmc[0][0];
            dxyDist[10][1]=dkD[10]*dPXYmmc[0][1];
// dxDist/dDa5    = dkD/dD6a*PXmmc
            dxyDist[11][0]=dkD[11]*dPXYmmc[0][0];
            dxyDist[11][1]=dkD[11]*dPXYmmc[0][1];
            
            
// dxDist/dDa5    = dkD/dDa5*PXmmc`
            dxyDist[12][0]=dkD[12]*dPXYmmc[0][0];
            dxyDist[12][1]=dkD[12]*dPXYmmc[0][1];
// dxDist/dDa    = dkD/dDa*PXmmc
            dxyDist[13][0]=dkD[13]*dPXYmmc[0][0];
            dxyDist[13][1]=dkD[13]*dPXYmmc[0][1];
// dxDist/dDb    = dkD/dDb*PXmmc
            dxyDist[14][0]=dkD[14]*dPXYmmc[0][0];
            dxyDist[14][1]=dkD[14]*dPXYmmc[0][1];
// dxDist/dDc    = dkD/dDc*PXmmc
            dxyDist[15][0]=dkD[15]*dPXYmmc[0][0];
            dxyDist[15][1]=dkD[15]*dPXYmmc[0][1];

            double K=Math.PI/180; // multiply all derivatives my angles 
// (10) PX = 1000/Psz*( xDist) + PX0 // horizontal pixel of the image (positive - right)
// dPX/dphi   =  1000/Psz* dxDist/dphi
        	partDeriv[ 1][0]=  K*1000.0/this.pixelSize*dxyDist[ 1][0];
        	partDeriv[ 1][1]= -K*1000.0/this.pixelSize*dxyDist[ 1][1];
// dPX/dtheta =  1000/Psz* dxDist/dtheta
        	partDeriv[ 2][0]=  K*1000.0/this.pixelSize*dxyDist[ 2][0];
        	partDeriv[ 2][1]= -K*1000.0/this.pixelSize*dxyDist[ 2][1];
// dPX/dpsi   =  1000/Psz* dxDist/dpsi
        	partDeriv[ 3][0]=  K*1000.0/this.pixelSize*dxyDist[ 3][0];
        	partDeriv[ 3][1]= -K*1000.0/this.pixelSize*dxyDist[ 3][1];
// dPX/dX0    =  1000/Psz* dxDist/dX0
        	partDeriv[ 4][0]=  1000.0/this.pixelSize*dxyDist[ 4][0]; // large error
        	partDeriv[ 4][1]= -1000.0/this.pixelSize*dxyDist[ 4][1];
// dPX/dY0    =  1000/Psz* dxDist/dY0
        	partDeriv[ 5][0]=  1000.0/this.pixelSize*dxyDist[ 5][0];
        	partDeriv[ 5][1]= -1000.0/this.pixelSize*dxyDist[ 5][1];
// dPX/dZ0    =  1000/Psz* dxDist/dZ0
        	partDeriv[ 6][0]=  1000.0/this.pixelSize*dxyDist[ 6][0]; // large error, including DC (sig -0.8..+0.5, err -0.2..-0.1
        	partDeriv[ 6][1]= -1000.0/this.pixelSize*dxyDist[ 6][1];
// dPX/df     =  1000/Psz* dxDist/df
        	partDeriv[ 7][0]=  1000.0/this.pixelSize*dxyDist[ 7][0];
        	partDeriv[ 7][1]= -1000.0/this.pixelSize*dxyDist[ 7][1];
// dPX/ddist  =  1000/Psz* dxDist/ddist
        	partDeriv[ 8][0]=  1000.0/this.pixelSize*dxyDist[ 8][0];
        	partDeriv[ 8][1]= -1000.0/this.pixelSize*dxyDist[ 8][1];

// dPX/dDa8    =  1000/Psz* dxDist/dDa5
        	partDeriv[ 9][0]=  1000.0/this.pixelSize*dxyDist[ 9][0];
        	partDeriv[ 9][1]= -1000.0/this.pixelSize*dxyDist[ 9][1];
// dPX/dDa7    =  1000/Psz* dxDist/dDa5
        	partDeriv[10][0]=  1000.0/this.pixelSize*dxyDist[10][0];
        	partDeriv[10][1]= -1000.0/this.pixelSize*dxyDist[10][1];
// dPX/dDa6    =  1000/Psz* dxDist/dDa5
        	partDeriv[11][0]=  1000.0/this.pixelSize*dxyDist[11][0];
        	partDeriv[11][1]= -1000.0/this.pixelSize*dxyDist[11][1];
        	
// dPX/dDa5    =  1000/Psz* dxDist/dDa5
        	partDeriv[12][0]=  1000.0/this.pixelSize*dxyDist[12][0];
        	partDeriv[12][1]= -1000.0/this.pixelSize*dxyDist[12][1];
// dPX/dDa    =  1000/Psz* dxDist/dDa
        	partDeriv[13][0]=  1000.0/this.pixelSize*dxyDist[13][0];
        	partDeriv[13][1]= -1000.0/this.pixelSize*dxyDist[13][1];
// dPX/dDb    =  1000/Psz* dxDist/dDb
        	partDeriv[14][0]=  1000.0/this.pixelSize*dxyDist[14][0];
        	partDeriv[14][1]= -1000.0/this.pixelSize*dxyDist[14][1];
// dPX/dDc    =  1000/Psz* dxDist/dDc
        	partDeriv[15][0]=  1000.0/this.pixelSize*dxyDist[15][0];
        	partDeriv[15][1]= -1000.0/this.pixelSize*dxyDist[15][1];
// dPX/dPX0   =  1
// dPY/dPX0   =  0
        	partDeriv[16][0]=  1.0;
        	partDeriv[16][1]=  0.0;
// dPX/dPY0   =  0
// dPY/dPY0   =  1
        	partDeriv[17][0]=  0.0;
        	partDeriv[17][1]=  1.0;
            return partDeriv;
        }
        
        
        public void setProperties(String prefix,Properties properties){
			properties.setProperty(prefix+"focalLength",this.focalLength+"");
			properties.setProperty(prefix+"pixelSize",this.pixelSize+"");
			properties.setProperty(prefix+"distortionRadius",this.distortionRadius+"");
			properties.setProperty(prefix+"distortionA8",this.distortionA8+"");
			properties.setProperty(prefix+"distortionA7",this.distortionA7+"");
			properties.setProperty(prefix+"distortionA6",this.distortionA6+"");
			properties.setProperty(prefix+"distortionA5",this.distortionA5+"");
			properties.setProperty(prefix+"distortionA",this.distortionA+"");
			properties.setProperty(prefix+"distortionB",this.distortionB+"");
			properties.setProperty(prefix+"distortionC",this.distortionC+"");
			properties.setProperty(prefix+"yaw",this.yaw+"");
			properties.setProperty(prefix+"pitch",this.pitch+"");
			properties.setProperty(prefix+"roll",this.roll+"");
			properties.setProperty(prefix+"x0",this.x0+"");
			properties.setProperty(prefix+"y0",this.y0+"");
			properties.setProperty(prefix+"z0",this.z0+"");
			properties.setProperty(prefix+"distance",this.distance+"");
			properties.setProperty(prefix+"px0",this.px0+"");
			properties.setProperty(prefix+"py0",this.py0+"");
			properties.setProperty(prefix+"flipVertical",this.flipVertical+"");
		}
		public void getProperties(String prefix,Properties properties){
			if (properties.getProperty(prefix+"focalLength")!=null)
				this.focalLength=Double.parseDouble(properties.getProperty(prefix+"focalLength"));
			if (properties.getProperty(prefix+"pixelSize")!=null)
				this.pixelSize=Double.parseDouble(properties.getProperty(prefix+"pixelSize"));
			if (properties.getProperty(prefix+"distortionRadius")!=null)
				this.distortionRadius=Double.parseDouble(properties.getProperty(prefix+"distortionRadius"));
			if (properties.getProperty(prefix+"distortionA8")!=null)
				this.distortionA8=Double.parseDouble(properties.getProperty(prefix+"distortionA8"));
			if (properties.getProperty(prefix+"distortionA7")!=null)
				this.distortionA7=Double.parseDouble(properties.getProperty(prefix+"distortionA7"));
			if (properties.getProperty(prefix+"distortionA6")!=null)
				this.distortionA6=Double.parseDouble(properties.getProperty(prefix+"distortionA6"));
			if (properties.getProperty(prefix+"distortionA5")!=null)
				this.distortionA5=Double.parseDouble(properties.getProperty(prefix+"distortionA5"));
			if (properties.getProperty(prefix+"distortionA")!=null)
				this.distortionA=Double.parseDouble(properties.getProperty(prefix+"distortionA"));
			if (properties.getProperty(prefix+"distortionB")!=null)
				this.distortionB=Double.parseDouble(properties.getProperty(prefix+"distortionB"));
			if (properties.getProperty(prefix+"distortionC")!=null)
				this.distortionC=Double.parseDouble(properties.getProperty(prefix+"distortionC"));
			if (properties.getProperty(prefix+"yaw")!=null)
				this.yaw=Double.parseDouble(properties.getProperty(prefix+"yaw"));
			if (properties.getProperty(prefix+"pitch")!=null)
				this.pitch=Double.parseDouble(properties.getProperty(prefix+"pitch"));
			if (properties.getProperty(prefix+"roll")!=null)
				this.roll=Double.parseDouble(properties.getProperty(prefix+"roll"));
			if (properties.getProperty(prefix+"x0")!=null)
				this.x0=Double.parseDouble(properties.getProperty(prefix+"x0"));
			if (properties.getProperty(prefix+"y0")!=null)
				this.y0=Double.parseDouble(properties.getProperty(prefix+"y0"));
			if (properties.getProperty(prefix+"z0")!=null)
				this.z0=Double.parseDouble(properties.getProperty(prefix+"z0"));
			if (properties.getProperty(prefix+"distance")!=null)
				this.distance=Double.parseDouble(properties.getProperty(prefix+"distance"));
			if (properties.getProperty(prefix+"px0")!=null)
				this.px0=Double.parseDouble(properties.getProperty(prefix+"px0"));
			if (properties.getProperty(prefix+"py0")!=null)
				this.py0=Double.parseDouble(properties.getProperty(prefix+"py0"));
			if (properties.getProperty(prefix+"flipVertical")!=null)
				this.flipVertical=Boolean.parseBoolean(properties.getProperty(prefix+"flipVertical"));
			recalcCommons();
		}
		public boolean showDialog() {
			GenericDialog gd = new GenericDialog("Lens distortion, location and orientation");
			gd.addNumericField("Lens focal length",               this.focalLength, 3,6,"mm");
			gd.addNumericField("Sensor pixel period",             this.pixelSize, 3,6,"um");
			gd.addNumericField("Distortion radius (halw width)",  this.distortionRadius, 5,8,"mm");
			gd.addNumericField("Distortion A8(r^5)",              this.distortionA8, 6,8,"");
			gd.addNumericField("Distortion A7(r^5)",              this.distortionA7, 6,8,"");
			gd.addNumericField("Distortion A6(r^5)",              this.distortionA6, 6,8,"");
			gd.addNumericField("Distortion A5(r^5)",              this.distortionA5, 6,8,"");
			gd.addNumericField("Distortion A (r^4)",              this.distortionA, 6,8,"");
			gd.addNumericField("Distortion B (r^3)",              this.distortionB, 6,8,"");
			gd.addNumericField("Distortion C (r^2)",              this.distortionC, 6,8,"");
			gd.addNumericField("Lens axis from perpendicular to the pattern, positive - clockwise (from top)", this.yaw, 2,6,"degrees");
			gd.addNumericField("Lens axis from perpendicular to the pattern, positive - up",                   this.pitch, 2,6,"degrees");
			gd.addNumericField("Rotation around lens axis, positive - clockwise (looking to pattern)",         this.roll, 2,6,"degrees");
			gd.addNumericField("Lens axis from the pattern center, (to the right)",                            this.x0, 1,6,"mm");
			gd.addNumericField("Lens axis from the pattern center, (down)",                                    this.y0, 1,6,"mm");
			gd.addNumericField("Lens axis from the pattern center, (away from camera, normally 0.0)",          this.z0, 1,6,"mm");
			gd.addNumericField("Distance from the lens input pupil to the pattern plane along the camera axis",this.distance, 1,6,"mm");
			gd.addNumericField("Lens axis on the sensor (horizontal, from left edge)",                         this.px0, 1,6,"pixels");
			gd.addNumericField("Lens axis on the sensor (vertical, from top edge)",                            this.py0, 1,6,"pixels");
			gd.addCheckbox    ("Camera looks through the mirror",                                              this.flipVertical);
		
			gd.showDialog();
			if (gd.wasCanceled()) return false;
			this.focalLength=     gd.getNextNumber();
			this.pixelSize=       gd.getNextNumber();
			this.distortionRadius=gd.getNextNumber();
			this.distortionA8=    gd.getNextNumber();
			this.distortionA7=    gd.getNextNumber();
			this.distortionA6=    gd.getNextNumber();
			this.distortionA5=    gd.getNextNumber();
			this.distortionA=     gd.getNextNumber();
			this.distortionB=     gd.getNextNumber();
			this.distortionC=     gd.getNextNumber();
			this.yaw=             gd.getNextNumber();
			this.pitch=           gd.getNextNumber();
			this.roll=            gd.getNextNumber();
			this.x0=              gd.getNextNumber();
			this.y0=              gd.getNextNumber();
			this.z0=              gd.getNextNumber();
			this.distance=        gd.getNextNumber(); 
			this.px0=             gd.getNextNumber();
			this.py0=             gd.getNextNumber();
			this.flipVertical=   gd.getNextBoolean();
			return true;
		}
	    /**
	     * Calculate/set  this.lensDistortionParameters and this.interParameterDerivatives
	     * @param parVect 21-element vector for eyesis sub-camera, including common and individual parameters
	     * @param mask -mask - which partial derivatives are needed to be calculated (others will be null)
	     * @param calculateDerivatives calculate array of partial derivatives, if false - just the values
	     */
	    public void lensCalcInterParamers(
//	    		LensDistortionParameters lensDistortionParameters,
	    		boolean isTripod,
	            double [][] interParameterDerivatives, //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21) if null - just values, no derivatives
	    		double [] parVect,
	    		boolean [] mask // calculate only selected derivatives (all parVect values are still
//	    		boolean calculateDerivatives // calculate this.interParameterDerivatives -derivatives array (false - just this.values)
	    		){
	    	LensDistortionParameters lensDistortionParameters=this;
	    	boolean calculateDerivatives=(interParameterDerivatives!=null);  // calculate this.interParameterDerivatives -derivatives array (false - just this.values) 
	    	 // change meaning of goniometerHorizontal (tripod vertical) and goniometerAxial (tripod horizontal) 
//	    	boolean isTripod=this.fittingStrategy.distortionCalibrationData.eyesisCameraParameters.isTripod;
	    	double azimuth=parVect[0];
	    	double radius= parVect[1];
	    	double height= parVect[2];
	    	double phi=    parVect[3];
	    	double theta=  parVect[4];
	    	double psi=    parVect[5];
	    	double goniometerHorizontal=parVect[6];
	    	double goniometerAxial=parVect[7];
	    	double interAxisDistance=parVect[8];
	    	double interAxisAngle=parVect[9];
	    	double horAxisErrPhi=parVect[10];
	    	double horAxisErrPsi=parVect[11];
	    	double entrancePupilForward=parVect[12];
	    	double centerAboveHorizontal=parVect[13];
	    	double GXYZ0=parVect[14];
	    	double GXYZ1=parVect[15];
	    	double GXYZ2=parVect[16];
	    	
	    	double cPS=   Math.cos(psi*Math.PI/180); // subCam.psi
	    	double sPS=   Math.sin(psi*Math.PI/180); // subCam.psi
	    	double cTH=   Math.cos(theta*Math.PI/180); // subCam.theta
	    	double sTH=   Math.sin(theta*Math.PI/180); // subCam.theta
	    	double cAZP=  Math.cos((azimuth+phi)*Math.PI/180); //subCam.azimuth+subCam.phi
	    	double sAZP=  Math.sin((azimuth+phi)*Math.PI/180); //subCam.azimuth+subCam.phi
	    	double cAZ=   Math.cos(azimuth*Math.PI/180); //subCam.azimuth
	    	double sAZ=   Math.sin(azimuth*Math.PI/180); //subCam.azimuth
	    	double cGA=   Math.cos(goniometerAxial*Math.PI/180); //eyesisCameraParameters.goniometerAxial
	    	double sGA=   Math.sin(goniometerAxial*Math.PI/180); //eyesisCameraParameters.goniometerAxial
	    	double cGH=   Math.cos(goniometerHorizontal*Math.PI/180); //eyesisCameraParameters.goniometerHorizontal
	    	double sGH=   Math.sin(goniometerHorizontal*Math.PI/180); //eyesisCameraParameters.goniometerHorizontal
	    	double cGIAA= Math.cos(interAxisAngle*Math.PI/180); // eyesisCameraParameters.interAxisAngle
	    	double sGIAA= Math.sin(interAxisAngle*Math.PI/180); //eyesisCameraParameters.interAxisAngle
	    	double cHAEPH=Math.cos(horAxisErrPhi*Math.PI/180); //eyesisCameraParameters.horAxisErrPhi
	    	double sHAEPH=Math.sin(horAxisErrPhi*Math.PI/180); //eyesisCameraParameters.horAxisErrPhi
	    	double cHAEPS=Math.cos(horAxisErrPsi*Math.PI/180); //eyesisCameraParameters.horAxisErrPsi
	    	double sHAEPS=Math.sin(horAxisErrPsi*Math.PI/180); //eyesisCameraParameters.horAxisErrPsi\
	    	
	/*
	 0) Translate by distance to entrance pupil (lens center)
	    	| Xc0 |   | 0                     |   |Xc|
	    	| Yc0 | = | 0                     | + |Yc|
	    	| Zc0 |   | entrancePupilForward  |   |Zc|
	    	*/
	    	double [][] aT0={{0.0},{0.0},{entrancePupilForward}};
	    	Matrix T0=new Matrix(aT0);
	    	
	/*
	Converting from the sub-camera coordinates to the target coordinates
	1) rotate by -psi around CZ: Vc1= R1*Vc
	| Xc1 |   | cos(psi)  sin(psi)    0         |   |Xc0|
	| Yc1 | = |-sin(psi)  cos(psi)    0         | * |Yc0|
	| Zc1 |   |    0         0        1         |   |Zc0|
	*/
	    	double [][] aR1={{cPS,sPS,0.0},{-sPS,cPS,0.0},{0.0,0.0,1.0}};
	    	Matrix R1=new Matrix(aR1);
	/*    	
	2) rotate by - theta around C1X:Vc2= R2*Vc1
	| Xc2 |   |    1         0         0        |   |Xc1|
	| Yc2 | = |    0    cos(theta)   sin(theta) | * |Yc1|
	| Zc2 |   |    0   -sin(theta)   cos(theta) |   |Zc1|
	*/
	    	double [][] aR2={{1.0,0.0,0.0},{0.0,cTH,sTH},{0.0,-sTH,cTH}};
	    	Matrix R2=new Matrix(aR2);
	/*    	
	3) rotate by -(azimuth+phi) around C2Y:Vc3= R3*Vc2
	| Xc3 |   | cos(azimuth+phi)    0   sin(azimuth+phi)   |   |Xc2|
	| Yc3 | = |     0               1         0            | * |Yc2|
	| Zc3 |   | -sin(azimuth+phi)   0   cos(azimuth+phi)   |   |Zc2|
	*/
	    	double [][] aR3={{cAZP,0.0,sAZP},{0.0,1.0,0.0},{-sAZP,0.0,cAZP}};
	    	Matrix R3=new Matrix(aR3);
	/*    	
	4) Now axes are aligned, just translate to get to eyesis coordinates: Vey= T1+Vc3
	| Xey |   |      r * sin (azimuth)       |   |Xc3|
	| Yey | = | height+centerAboveHorizontal | + |Yc3|
	| Zey |   |      r * cos (azimuth)       |   |Zc3|
	*/
	    	double [][] aT1={{radius*sAZ},{(height+centerAboveHorizontal)},{radius*cAZ}}; // {{subCam.radius*sAZ},{subCam.height},{subCam.radius*cAZ}};
	    	Matrix T1=new Matrix(aT1);
	    	
	/**
	5) rotate around moving goniometer axis, by (-goniometerAxial) - same as around EY: Vgm1=R4*Vey
	| Xgm1 |   |    1           0                     0              |   |Xey|
	| Ygm1 | = |    0   cos(goniometerAxial)  sin(goniometerAxial)   | * |Yey|
	| Zgm1 |   |    0   -sin(goniometerAxial) cos(goniometerAxial)   |   |Zey|
	*/
	    	double [][] aR4_tripod=    {{1.0,0.0,0.0},{0.0,cGA,sGA},{0.0,-sGA,cGA}};
	/*    	
	5) rotate around moving goniometer axis, by (-goniometerAxial) - same as around EY: Vgm1=R4*Vey
	| Xgm1 |   | cos(goniometerAxial)    0   sin(goniometerAxial)   |   |Xey|
	| Ygm1 | = |        0                1             0            | * |Yey|
	| Zgm1 |   |-sin(goniometerAxial)    0   cos(goniometerAxial)   |   |Zey|
	*/
			double [][] aR4_goniometer={{cGA,0.0,sGA},{0.0,1.0,0.0},{-sGA,0.0,cGA}};
			Matrix R4=new Matrix(isTripod?aR4_tripod:aR4_goniometer);
	/*    	
	6) move to the goniometer horizontal axis:Vgm2=T2+Vgm1
	| Xgm2 |   |                 0  |   |Xgm1|
	| Ygm2 | = |                 0  | + |Ygm1|
	| Zgm2 |   | interAxisDistance  |   |Zgm1|
	*/
	    	double [][] aT2={{0.0},{0.0},{interAxisDistance}}; //eyesisCameraParameters.interAxisDistance
	    	Matrix T2=new Matrix(aT2);
	/*    	
	7) rotate around Zgm2 by -interAxisAngle, so Xgm3 axis is the same as horizontal goniometer axis: Vgm3=R5*Vgm2
	| Xgm3 |   | cos(interAxisAngle)  sin(interAxisAngle)    0         |   |Xgm2|
	| Ygm3 | = |-sin(interAxisAngle)  cos(interAxisAngle)    0         | * |Ygm2|
	| Zgm3 |   |    0                         0              1         |   |Zgm2|
	*/
	    	double [][] aR5={{cGIAA,sGIAA,0.0},{-sGIAA,cGIAA,0.0},{0.0,0.0,1.0}};
	    	Matrix R5=new Matrix(aR5);
	/**
	8)  rotate around goniometer horizontal axis (Xgm3) by -goniometerHorizontal: Vgm4=R6*Vgm3
	| Xgm4 |   |    cos(goniometerHorizontal)  0   sin(goniometerHorizontal) |   |Xgm3|
	| Ygm4 | = |               0               1               0             | * |Ygm3|
	| Zgm4 |   |   -sin(goniometerHorizontal)  0   cos(goniometerHorizontal) |   |Zgm3|
	*/
	    	double [][] aR6_tripod=    {{cGH,0.0,sGH},{0.0,1.0,0.0},{-sGH,0.0,cGH}};
	/*    	
	8)  rotate around goniometer horizontal axis (Xgm3) by -goniometerHorizontal: Vgm4=R6*Vgm3
	| Xgm4 |   |    1                 0                           0            |   |Xgm3|
	| Ygm4 | = |    0    cos(goniometerHorizontal)   sin(goniometerHorizontal) | * |Ygm3|
	| Zgm4 |   |    0   -sin(goniometerHorizontal)   cos(goniometerHorizontal) |   |Zgm3|
	*/
	    	double [][] aR6_goniometer={{1.0,0.0,0.0},{0.0,cGH,sGH},{0.0,-sGH,cGH}};
			Matrix R6=new Matrix(isTripod?aR6_tripod:aR6_goniometer);
	/*    	
	9) Correct roll error of the goniometer horizontal axis - rotate by -horAxisErrPsi around Zgm4: Vgm5=R7*Vgm4
	| Xgm5 |   | cos(horAxisErrPsi)  sin(horAxisErrPsi)    0         |   |Xgm4|
	| Ygm5 | = |-sin(horAxisErrPsi)  cos(horAxisErrPsi)    0         | * |Ygm4|
	| Zgm5 |   |         0                   0             1         |   |Zgm4|
	*/
	    	double [][] aR7={{cHAEPS,sHAEPS,0.0},{-sHAEPS,cHAEPS,0.0},{0.0,0.0,1.0}};
	    	Matrix R7=new Matrix(aR7);
	/*    	
	10) Correct azimuth error of the goniometer hoirizontal axis - rotate by -horAxisErrPhi around Ygm5: Vgm6=R8*Vgm5
	 
	| Xgm6 |   | cos(horAxisErrPhi)      0   sin(horAxisErrPhi)   |   |Xgm5|
	| Ygm6 | = |        0                1             0          | * |Ygm5|
	| Zgm6 |   |-sin(horAxisErrPhi)      0   cos(horAxisErrPhi)   |   |Zgm5|
	For Tripod - rotate around X-axis (like theta)
	| Xgm6 |   |   1           0                       0      |   |Xgm5|
	| Ygm6 | = |   0  cos(horAxisErrPhi)  sin(horAxisErrPhi)  | * |Ygm5|
	| Zgm6 |   |   0 -sin(horAxisErrPhi)  cos(horAxisErrPhi)  |   |Zgm5|

	*/
	    	double [][] aR8_tripod=    {{1.0,   0.0,   0.0},{0.0,cHAEPH,sHAEPH},{0.0,  -sHAEPH,cHAEPH}};
	    	double [][] aR8_goniometer={{cHAEPH,0.0,sHAEPH},{0.0,   1.0,   0.0},{-sHAEPH,  0.0,cHAEPH}};
	    	Matrix R8=new Matrix(isTripod?aR8_tripod:aR8_goniometer);
	/*    	
	11) translate to the target zero point: Vt=  T3+Vgm6
	| Xt |   | GXYZ[0]  |   |Xgm6|
	| Yt | = |-GXYZ[1]  | + |Ygm6| // Y - up positive
	| Zt |   |-GXYZ[2]  |   |Zgm6| // Z - away positive
	    	 */
//	    	double [][] aT3={{parVect[12]},{-parVect[13]},{-parVect[14]}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
	   	
	    	double [][] aT3={{GXYZ0},{-GXYZ1},{-GXYZ2}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
//	    	double [][] aT3={{parVect[12]},{ parVect[13]},{-parVect[14]}};//{{eyesisCameraParameters.GXYZ[0]},{eyesisCameraParameters.GXYZ[1]},{eyesisCameraParameters.GXYZ[2]}};
	    	Matrix T3=new Matrix(aT3);

	// MA=R8*R7*R6*R5*R4*R3*R2*R1;
	// MB=T3+(R8*R7*R6*R5*(T2+R4*T1)); - old
	// MB=T3+(R8*R7*R6*R5*(T2+R4*(T1+R3*R2*R1*T0)));
	        Matrix MA=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
//	        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1)))))));
	        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1.plus(R3.times(R2.times(R1.times(T0)))) )))))));
			if (this.debugLevel>2) {
				System.out.println("MA:");
				MA.print(10, 5);
				System.out.println("MB:");
				MB.print(10, 5);
				System.out.println("T3:");
				T3.print(10, 5);
//				System.out.println("interParameterDerivatives[0]="+sprintfArray(interParameterDerivatives[0]));
			}
	        double [] extrinsicParams=parametersFromMAMB(MA,MB); // all after 6 are 0;
	        lensDistortionParameters.distance=extrinsicParams[2];
	        lensDistortionParameters.x0=extrinsicParams[0];
	        lensDistortionParameters.y0=extrinsicParams[1];
	        lensDistortionParameters.z0=0.0; // used here
	        lensDistortionParameters.pitch=extrinsicParams[4];
	        lensDistortionParameters.yaw=  extrinsicParams[3];
	        lensDistortionParameters.roll= extrinsicParams[5];
//			lensDistortionParameters.focalLength=parVect[15]; //subCam.focalLength;
//			lensDistortionParameters.px0=parVect[16]; //subCam.px0;
//			lensDistortionParameters.py0=parVect[17]; //subCam.py0;
//			lensDistortionParameters.distortionA5=parVect[18]; //subCam.distortion5;
//			lensDistortionParameters.distortionA=parVect[19]; //subCam.distortionA;
//			lensDistortionParameters.distortionB=parVect[20]; //subCam.distortionB;
//			lensDistortionParameters.distortionC=parVect[21]; //subCam.distortionC;

			lensDistortionParameters.focalLength=parVect[17]; //subCam.focalLength;
			lensDistortionParameters.px0=parVect[18]; //subCam.px0;
			lensDistortionParameters.py0=parVect[19]; //subCam.py0;
			lensDistortionParameters.distortionA8=parVect[20]; //subCam.distortion5;
			lensDistortionParameters.distortionA7=parVect[21]; //subCam.distortion5;
			lensDistortionParameters.distortionA6=parVect[22]; //subCam.distortion5;
			lensDistortionParameters.distortionA5=parVect[23]; //subCam.distortion5;
			lensDistortionParameters.distortionA=parVect[24]; //subCam.distortionA;
			lensDistortionParameters.distortionB=parVect[25]; //subCam.distortionB;
			lensDistortionParameters.distortionC=parVect[26]; //subCam.distortionC;

			
			lensDistortionParameters.recalcCommons();
	        
	        
	        if (this.debugLevel>2){
	        	System.out.println("lensDistortionParameters.recalcCommons()");
	        	System.out.println("lensDistortionParameters.distance="+IJ.d2s(lensDistortionParameters.distance, 3));
	        	System.out.println("lensDistortionParameters.x0="+      IJ.d2s(lensDistortionParameters.x0, 3));
	        	System.out.println("lensDistortionParameters.y0="+      IJ.d2s(lensDistortionParameters.y0, 3));
	        	System.out.println("lensDistortionParameters.z0="+      IJ.d2s(lensDistortionParameters.z0, 3));
	        	System.out.println("lensDistortionParameters.pitch="+   IJ.d2s(lensDistortionParameters.pitch, 3));
	        	System.out.println("lensDistortionParameters.yaw="+IJ.d2s(lensDistortionParameters.yaw, 3));
	        	System.out.println("lensDistortionParameters.roll="+IJ.d2s(lensDistortionParameters.roll, 3));
	        	System.out.println("lensDistortionParameters.focalLength="+IJ.d2s(lensDistortionParameters.focalLength, 3));
	        	System.out.println("lensDistortionParameters.px0="+IJ.d2s(lensDistortionParameters.px0, 3));
	        	System.out.println("lensDistortionParameters.py0="+IJ.d2s(lensDistortionParameters.py0, 3));
	        	System.out.println("lensDistortionParameters.distortionA8="+IJ.d2s(lensDistortionParameters.distortionA8, 5));
	        	System.out.println("lensDistortionParameters.distortionA7="+IJ.d2s(lensDistortionParameters.distortionA7, 5));
	        	System.out.println("lensDistortionParameters.distortionA6="+IJ.d2s(lensDistortionParameters.distortionA6, 5));
	        	System.out.println("lensDistortionParameters.distortionA5="+IJ.d2s(lensDistortionParameters.distortionA5, 5));
	        	System.out.println("lensDistortionParameters.distortionA="+IJ.d2s(lensDistortionParameters.distortionA, 5));
	        	System.out.println("lensDistortionParameters.distortionB="+IJ.d2s(lensDistortionParameters.distortionB, 5));
	        	System.out.println("lensDistortionParameters.distortionC="+IJ.d2s(lensDistortionParameters.distortionC, 5));
	        }
	        if (!calculateDerivatives) return; 
	/* Calculate all derivativs as a matrix.
	 *  Input parameters (columns):
	0    	public double azimuth; // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
	1    	public double radius;  // mm, distance from the rotation axis
	2   	public double height;       // mm, up (was downwards?) - from the origin point
	3   	public double phi;     // degrees, optical axis from azimuth/r vector, clockwise
	4   	public double theta;   // degrees, optical axis from the eyesis horizon, positive - up
	5   	public double psi;     // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
	6   	public double goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
	7   	public double goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
	8   	public double interAxisDistance; // distance in mm between two goniometer axes
	9    	public double interAxisAngle;    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
	10   	public double horAxisErrPhi;   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
	11   	public double horAxisErrPsi;   // angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
	Two new parameters
	12    	public double entrancePupilForward; // common to all lenses - distance from the sensor to the lens entrance pupil
	13    	public double centerAboveHorizontal; // camera center distance along camera axis above the closest point to horizontal rotation axis (adds to 
	14(12)  x	public double [] GXYZ=null; // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
	15(13)  y
	16(14)  z
	17(15)	public double focalLength=4.5;
	18(16)	public double px0=1296.0;          // center of the lens on the sensor, pixels
	19(17)		public double py0=968.0;           // center of the lens on the sensor, pixels
	20(18)		public double distortionA8=0.0; // r^8 (normalized to focal length or to sensor half width?)
	21(19)		public double distortionA7=0.0; // r^7 (normalized to focal length or to sensor half width?)
	22(20)		public double distortionA6=0.0; // r^6 (normalized to focal length or to sensor half width?)
	23(21)		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	24(22)		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
	25(23)		public double distortionB=0.0; // r^3
	26(22)		public double distortionC=0.0; // r^2

	 * Output parameters (rows):
	0		public double x0=0;      // lens axis from pattern center, mm (to the right)
	1		public double y0=0;      // lens axis from pattern center, mm (down)
	2		public double distance=2360; // distance from the lens input pupil to the pattern plane along the camera axis, mm
	3		public double yaw=0.0;   // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - clockwise from top
	4		public double pitch=0.0; // angle in degrees from perpendicular to the pattern, 0 - towards wall, positive - up
	5		public double roll=0.0;  // angle in degrees rotation around camera optical axis (perpendicular to pattern if yaw==0, pitch==0), positive - clockwise
	6		public double focalLength=4.5;
	7		public double px0=1296.0;           // center of the lens on the sensor, pixels
	8		public double py0=968.0;           // center of the lens on the sensor, pixels
	 		public double distortionRadius=  2.8512; // mm - half width of the sensor
	9		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	10		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	11		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	12		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	13		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
	14		public double distortionB=0.0; // r^3
	15		public double distortionC=0.0; // r^2
	 */
//	        interParameterDerivatives=new double[getNumInputs()][]; //partial derivative matrix from subcamera-camera-goniometer to single camera (12x21)
	        
	//calculate dMA_azimuth        
	//calculate dMB_azimuth        
	/*
	// MA=R8*R7*R6*R5*R4*R3*R2*R1;
	// MB=T3+(R8*R7*R6*R5*(T2+R4*T1)); - old
	// MB=T3+(R8*R7*R6*R5*(T2+R4*(T1+R3*R2*R1*T0)));
	        Matrix MA=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
	        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1)))))));  old
	        Matrix MB=T3.plus(R8.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1.plus(R3.times(R2.times(R1.times(T0)))) )))))));
	3) rotate by -(azimuth+phi) around C2Y:Vc3= R3*Vc2
	| Xc3 |   | cos(azimuth+phi)    0   sin(azimuth+phi)   |   |Xc2|
	| Yc3 | = |     0               1         0            | * |Yc2|
	| Zc3 |   | -sin(azimuth+phi)   0   cos(azimuth+phi)   |   |Zc2|
	    	double [][] aR3={{cAZP,0.0,sAZP},{0.0,1.0,0.0},{-sAZP,0.0,cAZP}};
	    	Matrix R3=new Matrix(aR3);
	4) Now axes are aligned, just translate to get to eyesis coordinates: Vey= T1+Vc3
	| Xey |   |  r * sin (azimuth)  |   |Xc3|
	| Yey | = |             height  | + |Yc3|
	| Zey |   |  r * cos (azimuth)  |   |Zc3|
	    	double [][] aT1={{subCam.radius*sAZ},{subCam.height},{subCam.radius*cAZ}};
	    	Matrix T1=new Matrix(aT1);

	        Matrix 
	// Make a function MA, MB - >parameters (column) - reuse it above and for each    interParameterDerivatives row
	Matrix dMA_azimuth=R8*R7*R6*R5*R4*dR3_azimuth*R2*R1;
	Matrix dMB_azimuth=T3+(R8*R7*R6*R5*(R4*dT1_azimuth));

	Use extrinsicParams=parametersFromMAMB(dMA_azimuth,dMB_azimuth);
		       		if (this.debugLevel>3) {
		    			System.out.println("calculateFxAndJacobian->calcPartialDerivatives("+IJ.d2s(targetXYZ[fullIndex][0],2)+","+
		    					IJ.d2s(targetXYZ[fullIndex][1],2)+","+
		    					IJ.d2s(targetXYZ[fullIndex][2],2)+" ("+calcJacobian+") -> "+
		    					IJ.d2s(derivatives14[0][0],2)+"/"+IJ.d2s(derivatives14[0][1],2));
		    		}

	Which parameters affect which matrices
	                                               R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 || T0 | T1 | T2 | T3 |
	0    	public double azimuth;              //    |    | +  |    |    |    |    |    ||    | +  |    |    |
	1    	public double radius;               //    |    |    |    |    |    |    |    ||    | +  |    |    |
	2   	public double height;               //    |    |    |    |    |    |    |    ||    | +  |    |    |
	3   	public double phi;                  //    |    | +  |    |    |    |    |    ||    |    |    |    |
	4   	public double theta;                //    | +  |    |    |    |    |    |    ||    |    |    |    |
	5   	public double psi;                  // +  |    |    |    |    |    |    |    ||    |    |    |    |
	6   	public double goniometerHorizontal; //    |    |    |    |    | +  |    |    ||    |    |    |    |
	7   	public double goniometerAxial;      //    |    |    | +  |    |    |    |    ||    |    |    |    |
	8   	public double interAxisDistance;    //    |    |    |    |    |    |    |    ||    |    | +  |    |
	9    	public double interAxisAngle;       //    |    |    |    | +  |    |    |    ||    |    |    |    |
	10   	public double horAxisErrPhi;        //    |    |    |    |    |    |    | +  ||    |    |    |    |
	11   	public double horAxisErrPsi;        //    |    |    |    |    |    | +  |    ||    |    |    |    |
	12      public double entrancePupilForward  //    |    |    |    |    |    |    |    || +  |    |    |    |
	13      public double centerAboveHorizontal //    |    |    |    |    |    |    |    ||    | +  |    |    |
	14  x	public double [] GXYZ=null;         //    |    |    |    |    |    |    |    ||    |    |    | +  |
	15  y                                       //    |    |    |    |    |    |    |    ||    |    |    | +  |
	16  z                                       //    |    |    |    |    |    |    |    ||    |    |    | +  |

	 */
	// Below can be optimized with common intermediate results
	   		if (this.debugLevel>2) {
	   			for (int i=0;i<parVect.length;i++){
	   				System.out.println("calcInterParamers(): parVect["+i+"]="+parVect[i]);
	   			}
	   		}
	//0    	public double azimuth; // azimuth of the lens entrance pupil center, degrees, clockwise looking from top
	        if (mask[0]) {
	        	double [][] adR3_azimuth={{-sAZP,0.0,cAZP},{0.0,0.0,0.0},{-cAZP,0.0,-sAZP}};
	        	Matrix dR3_azimuth=new Matrix(adR3_azimuth);
//	        	double [][] adT1_azimuth={{radius*cAZ},{height},{-radius*sAZ}}; //{{subCam.radius*cAZ},{subCam.height},{-subCam.radius*sAZ}}
	        	double [][] adT1_azimuth={{radius*cAZ},{0.0},{-radius*sAZ}}; //{{subCam.radius*cAZ},{subCam.height},{-subCam.radius*sAZ}}
	        	Matrix dT1_azimuth=new Matrix(adT1_azimuth);
	        	
	        	Matrix dMA_azimuth=R8.times(R7.times(R6.times(R5.times(R4.times(dR3_azimuth.times(R2.times(R1)))))));
	        	Matrix dMB0_azimuth=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_azimuth)))));
	        	Matrix dMB_azimuth=dMB0_azimuth.plus(dMA_azimuth.times(T0)); // new term
	        	interParameterDerivatives[0]=d_parametersFromMAMB(dMA_azimuth,dMB_azimuth,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_azimuth:");
	    			dMA_azimuth.print(10, 5);
	    			System.out.println("dMB_azimuth:");
	    			dMB_azimuth.print(10, 5);
	    			System.out.println("interParameterDerivatives[0]="+sprintfArray(interParameterDerivatives[0]));
	    		}
	        	
	        } else interParameterDerivatives[0]=null;
	//1    	public double radius;  // mm, distance from the rotation axis
	        if (mask[1]) {
	        	double [][] adT1_radius={{sAZ},{0.0},{cAZ}}; //{{subCam.radius*sAZ},{0.0},{subCam.radius*cAZ}}
	        	Matrix dT1_radius=new Matrix(adT1_radius);
	        	Matrix dMA_radius=new Matrix(3,3,0.0);
	        	Matrix dMB_radius=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_radius)))));
	        	interParameterDerivatives[1]=d_parametersFromMAMB(dMA_radius,dMB_radius,MA,MB,false); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_radius:");
	    			dMA_radius.print(10, 5);
	    			System.out.println("dMB_radius:");
	    			dMB_radius.print(10, 5);
	    			System.out.println("interParameterDerivatives[1]="+sprintfArray(interParameterDerivatives[1]));
	    		}
	        } else interParameterDerivatives[1]=null; 
	//2   	public double height;       // mm, downwards - from the origin point
	        if (mask[2]) {
	        	double [][] adT1_height={{0.0},{1.0},{0.0}};
	        	Matrix dT1_height=new Matrix(adT1_height);
	        	Matrix dMA_height=new Matrix(3,3,0.0);
	        	Matrix dMB_height=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_height)))));
	        	interParameterDerivatives[2]=d_parametersFromMAMB(dMA_height,dMB_height,MA,MB,false); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_height:");
	    			dMA_height.print(10, 5);
	    			System.out.println("dMB_height:");
	    			dMB_height.print(10, 5);
	    			System.out.println("interParameterDerivatives[2]="+sprintfArray(interParameterDerivatives[2]));
	    		}
	        } else interParameterDerivatives[2]=null; 
	//3   	public double phi;     // degrees, optical axis from azimuth/r vector, clockwise
	        if (mask[3]) {
	        	double [][] adR3_phi={{-sAZP,0.0,cAZP},{0.0,0.0,0.0},{-cAZP,0.0,-sAZP}}; // same as adR3_azimuth
	        	Matrix dR3_phi=new Matrix(adR3_phi); // same as dR3_azimuth
	        	Matrix dMA_phi=R8.times(R7.times(R6.times(R5.times(R4.times(dR3_phi.times(R2.times(R1))))))); //same as dMA_azimuth
//	        	Matrix dMB_phi=new Matrix(3,1,0.0);
	        	Matrix dMB_phi=dMA_phi.times(T0); // new term
	        	interParameterDerivatives[3]=d_parametersFromMAMB(dMA_phi,dMB_phi,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_phi:");
	    			dMA_phi.print(10, 5);
	    			System.out.println("dMB_phi:");
	    			dMB_phi.print(10, 5);
	    			System.out.println("interParameterDerivatives[3]="+sprintfArray(interParameterDerivatives[3]));
	    		}
	        } else interParameterDerivatives[3]=null; 
	//4   	public double theta;   // degrees, optical axis from the eyesis horizon, positive - up
	        if (mask[4]) {
	        	double [][] adR2_theta={{0.0,0.0,0.0},{0.0,-sTH,cTH},{0.0,-cTH,-sTH}};
	        	Matrix dR2_theta=new Matrix(adR2_theta);
	        	Matrix dMA_theta=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(dR2_theta.times(R1)))))));
//	        	Matrix dMB_theta=new Matrix(3,1,0.0);
	        	Matrix dMB_theta=dMA_theta.times(T0); // new term
	        	interParameterDerivatives[4]=d_parametersFromMAMB(dMA_theta,dMB_theta,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_theta:");
	    			dMA_theta.print(10, 5);
	    			System.out.println("dMB_theta:");
	    			dMB_theta.print(10, 5);
	    			System.out.println("interParameterDerivatives[4]="+sprintfArray(interParameterDerivatives[4]));
	    		}
	        } else interParameterDerivatives[4]=null; 
	//5   	public double psi;     // degrees, rotation (of the sensor) around the optical axis. Positive if camera is rotated clockwise looking to the target
	        if (mask[5]) {
	        	double [][] adR1_psi={{-sPS,cPS,0.0},{-cPS,-sPS,0.0},{0.0,0.0,0.0}};
	        	Matrix dR1_psi=new Matrix(adR1_psi);
	        	Matrix dMA_psi=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(dR1_psi)))))));
//	        	Matrix dMB_psi=new Matrix(3,1,0.0);
	        	Matrix dMB_psi=dMA_psi.times(T0); // new term
	        	interParameterDerivatives[5]=d_parametersFromMAMB(dMA_psi,dMB_psi,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	/*    			
	    			System.out.print("R1:");
	    			R1.print(10, 5);
	    			System.out.print("dR1_psi:");
	    			dR1_psi.print(10, 5);
	    			Matrix R82_psi=R8.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2))))));
	    			System.out.print("R82_psi:");
	    			R82_psi.print(10, 5);
	    			*/
	    			System.out.print("dMA_psi:");
	    			dMA_psi.print(10, 5);
	    			System.out.print("dMB_psi:");
	    			dMB_psi.print(10, 5);
	    			System.out.println("interParameterDerivatives[5]="+sprintfArray(interParameterDerivatives[5]));
	    		}
	        } else interParameterDerivatives[5]=null; 
	//6   	public double goniometerHorizontal; // goniometer rotation around "horizontal" axis (tilting from the target - positive)
	        if (mask[6]) {
	/* define for isTripod */
	        	double [][] adR6_goniometerHorizontal_tripod=    {{-sGH,0.0,cGH},{0.0, 0.0,0.0},{-cGH, 0.0,-sGH}};
	        	double [][] adR6_goniometerHorizontal_goniometer={{ 0.0,0.0,0.0},{0.0,-sGH,cGH},{ 0.0,-cGH,-sGH}};
	        	Matrix dR6_goniometerHorizontal=new Matrix(isTripod?adR6_goniometerHorizontal_tripod:adR6_goniometerHorizontal_goniometer);
	        	Matrix dMA_goniometerHorizontal=R8.times(R7.times(dR6_goniometerHorizontal.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
	        	Matrix dMB_goniometerHorizontal=R8.times(R7.times(dR6_goniometerHorizontal.times(R5.times(T2.plus(R4.times(T1))))));
	        	interParameterDerivatives[6]=d_parametersFromMAMB(dMA_goniometerHorizontal,dMB_goniometerHorizontal,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_goniometerHorizontal:");
	    			dMA_goniometerHorizontal.print(10, 5);
	    			System.out.println("dMB_goniometerHorizontal:");
	    			dMB_goniometerHorizontal.print(10, 5);
	    			System.out.println("interParameterDerivatives[6]="+sprintfArray(interParameterDerivatives[6]));
	    		}
	        } else interParameterDerivatives[6]=null; 
	//7   	public double goniometerAxial; // goniometer rotation around Eyesis axis (clockwise in plan - positive 
	        if (mask[7]) {
	// define for isTripod
	        	double [][] adR4_goniometerAxial_tripod=    {{ 0.0,0.0,0.0},{0.0,-sGA,cGA},{ 0.0,-cGA,-sGA}};
	        	double [][] adR4_goniometerAxial_goniometer={{-sGA,0.0,cGA},{0.0, 0.0,0.0},{-cGA, 0.0,-sGA}};
	        	Matrix dR4_goniometerAxial=new Matrix(isTripod?adR4_goniometerAxial_tripod:adR4_goniometerAxial_goniometer);
	        	Matrix dMA_goniometerAxial=R8.times(R7.times(R6.times(R5.times(dR4_goniometerAxial.times(R3.times(R2.times(R1)))))));
	        	Matrix dMB_goniometerAxial=R8.times(R7.times(R6.times(R5.times(dR4_goniometerAxial.times(T1)))));
	        	interParameterDerivatives[7]=d_parametersFromMAMB(dMA_goniometerAxial,dMB_goniometerAxial,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_goniometerAxial:");
	    			dMA_goniometerAxial.print(10, 5);
	    			System.out.println("dMB_goniometerAxial:");
	    			dMB_goniometerAxial.print(10, 5);
	    			System.out.println("interParameterDerivatives[7]="+sprintfArray(interParameterDerivatives[7]));
	    		}
	        } else interParameterDerivatives[7]=null; 
	//8   	public double interAxisDistance; // distance in mm between two goniometer axes
	        if (mask[8]) {
	        	double [][] adT2_interAxisDistance={{0.0},{0.0},{1.0}};
	        	Matrix dT2_interAxisDistance=new Matrix(adT2_interAxisDistance);
	        	Matrix dMA_interAxisDistance=new Matrix(3,3,0.0);
	        	Matrix dMB_interAxisDistance=R8.times(R7.times(R6.times(R5.times(dT2_interAxisDistance))));
	        	interParameterDerivatives[8]=d_parametersFromMAMB(dMA_interAxisDistance,dMB_interAxisDistance,MA,MB,false); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_interAxisDistance:");
	    			dMA_interAxisDistance.print(10, 5);
	    			System.out.println("dMB_interAxisDistance:");
	    			dMB_interAxisDistance.print(10, 5);
	    			System.out.println("interParameterDerivatives[8]="+sprintfArray(interParameterDerivatives[8]));
	    		}
	        } else interParameterDerivatives[8]=null; 
	//9    	public double interAxisAngle;    // angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
	        if (mask[9]) {
	        	double [][] adR5_interAxisAngle={{-sGIAA,cGIAA,0.0},{-cGIAA,-sGIAA,0.0},{0.0,0.0,0.0}};
	        	Matrix dR5_interAxisAngle=new Matrix(adR5_interAxisAngle);
	        	Matrix dMA_interAxisAngle=R8.times(R7.times(R6.times(dR5_interAxisAngle.times(R4.times(R3.times(R2.times(R1)))))));
	        	Matrix dMB_interAxisAngle=R8.times(R7.times(R6.times(dR5_interAxisAngle.times(T2.plus(R4.times(T1))))));    	
	        	interParameterDerivatives[9]=d_parametersFromMAMB(dMA_interAxisAngle,dMB_interAxisAngle,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_interAxisAngle:");
	    			dMA_interAxisAngle.print(10, 5);
	    			System.out.println("dMB_interAxisAngle:");
	    			dMB_interAxisAngle.print(10, 5);
	    			System.out.println("interParameterDerivatives[9]="+sprintfArray(interParameterDerivatives[9]));
	    		}
	        } else interParameterDerivatives[9]=null; 
	//10   	public double horAxisErrPhi;   // angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
	        
	// change sHAEPH to rotate like theta /1 0 0 /0 c s/ 0 -s c/ (derivative -/0 0 0 /0 -s c/ 0 -c -s/         
	        if (mask[10]) {
	        	double [][] adR8_horAxisErrPhi_tripod=    {{0.0,   0.0,    0.0},{0.0,-sHAEPH,cHAEPH},{    0.0,-cHAEPH,-sHAEPH}};
	        	double [][] adR8_horAxisErrPhi_goniometer={{-sHAEPH,0.0,cHAEPH},{0.0,    0.0,   0.0},{-cHAEPH,    0.0,-sHAEPH}};
	        	Matrix dR8_horAxisErrPhi=new Matrix(isTripod?adR8_horAxisErrPhi_tripod:adR8_horAxisErrPhi_goniometer);
	        	Matrix dMA_horAxisErrPhi=dR8_horAxisErrPhi.times(R7.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
	        	Matrix dMB_horAxisErrPhi=dR8_horAxisErrPhi.times(R7.times(R6.times(R5.times(T2.plus(R4.times(T1))))));
	        	interParameterDerivatives[10]=d_parametersFromMAMB(dMA_horAxisErrPhi,dMB_horAxisErrPhi,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_horAxisErrPhi:");
	    			dMA_horAxisErrPhi.print(10, 5);
	    			System.out.println("dMB_horAxisErrPhi:");
	    			dMB_horAxisErrPhi.print(10, 5);
	    			System.out.println("interParameterDerivatives[10]="+sprintfArray(interParameterDerivatives[10]));
	    		}
	        } else interParameterDerivatives[10]=null; 
	//11   	public double horAxisErrPsi;   // angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
	        if (mask[11]) {
	        	double [][] adR7_horAxisErrPsi={{-sHAEPS,cHAEPS,0.0},{-cHAEPS,-sHAEPS,0.0},{0.0,0.0,0.0}};
	        	Matrix dR7_horAxisErrPsi=new Matrix(adR7_horAxisErrPsi);
	        	Matrix dMA_horAxisErrPsi=R8.times(dR7_horAxisErrPsi.times(R6.times(R5.times(R4.times(R3.times(R2.times(R1)))))));
	        	Matrix dMB_horAxisErrPsi=R8.times(dR7_horAxisErrPsi.times(R6.times(R5.times(T2.plus(R4.times(T1))))));
	        	interParameterDerivatives[11]=d_parametersFromMAMB(dMA_horAxisErrPsi,dMB_horAxisErrPsi,MA,MB,true); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_horAxisErrPsi:");
	    			dMA_horAxisErrPsi.print(10, 5);
	    			System.out.println("dMB_horAxisErrPsi:");
	    			dMB_horAxisErrPsi.print(10, 5);
	    			System.out.println("interParameterDerivatives[11]="+sprintfArray(interParameterDerivatives[11]));
	    		}
	        } else interParameterDerivatives[11]=null; 
//	                                          // R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 || T0 | T1 | T2 | T3 |
	//12   	public double entrancePupilForward  //    |    |    |    |    |    |    |    || +  |    |    |    |
	//13   	public double centerAboveHorizontal //    |    |    |    |    |    |    |    ||    | +  |    |    |

	        if (mask[12]) {
	        	double [][] adT0_entrancePupilForward={{0.0},{0.0},{1.0}};
	        	Matrix dT0_entrancePupilForward=new Matrix(adT0_entrancePupilForward);
	        	Matrix dMA_entrancePupilForward=new Matrix(3,3,0.0);
	        	Matrix dMB_entrancePupilForward=MA.times(dT0_entrancePupilForward);
	        	interParameterDerivatives[12]=d_parametersFromMAMB(dMA_entrancePupilForward,dMB_entrancePupilForward,MA,MB,false); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_centerAboveHorizontal:");
	    			dMA_entrancePupilForward.print(10, 5);
	    			System.out.println("dMB_centerAboveHorizontal:");
	    			dMB_entrancePupilForward.print(10, 5);
	    			System.out.println("interParameterDerivatives[12]="+sprintfArray(interParameterDerivatives[12]));
	    		}
	        } else interParameterDerivatives[12]=null; 
	        
	        if (mask[13]) {
	        	double [][] adT1_centerAboveHorizontal={{0.0},{1.0},{0.0}};
	        	Matrix dT1_centerAboveHorizontal=new Matrix(adT1_centerAboveHorizontal);
	        	Matrix dMA_centerAboveHorizontal=new Matrix(3,3,0.0);
	        	Matrix dMB_centerAboveHorizontal=R8.times(R7.times(R6.times(R5.times(R4.times(dT1_centerAboveHorizontal)))));
	        	interParameterDerivatives[13]=d_parametersFromMAMB(dMA_centerAboveHorizontal,dMB_centerAboveHorizontal,MA,MB,false); // all after 6 are 0;
	    		if (this.debugLevel>2) {
	    			System.out.println("dMA_centerAboveHorizontal:");
	    			dMA_centerAboveHorizontal.print(10, 5);
	    			System.out.println("dMB_centerAboveHorizontal:");
	    			dMB_centerAboveHorizontal.print(10, 5);
	    			System.out.println("interParameterDerivatives[13]="+sprintfArray(interParameterDerivatives[13]));
	    		}
	        } else interParameterDerivatives[13]=null; 
	        
	        
	//14  x	public double [] GXYZ=null; // coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system
	        if (mask[14]) {
	        	double [][] adT3_GXYZ0={{1.0},{0.0},{0.0}};
	        	Matrix dMA_GXYZ0=new Matrix(3,3,0.0);
	        	Matrix dMB_GXYZ0=new Matrix(adT3_GXYZ0);
	        	interParameterDerivatives[14]=d_parametersFromMAMB(dMA_GXYZ0,dMB_GXYZ0,MA,MB,false); // all after 6 are 0;
	        	if (this.debugLevel>2) {
	        		System.out.println("dMA_GXYZ0:");
	        		dMA_GXYZ0.print(10, 5);
	        		System.out.println("dMA_GXYZ0:");
	        		dMB_GXYZ0.print(10, 5);
	        		System.out.println("interParameterDerivatives[14]="+sprintfArray(interParameterDerivatives[14]));
	        	}
	        } else interParameterDerivatives[14]=null; 
	//13  y
	        if (mask[15]) {
//	    	double [][] adT3_GXYZ1={{0.0},{1.0},{0.0}};
	        	double [][] adT3_GXYZ1={{0.0},{-1.0},{0.0}}; // up - positive
//	        	double [][] adT3_GXYZ1={{0.0},{ 1.0},{0.0}}; // up - positive
	        	Matrix dMA_GXYZ1=new Matrix(3,3,0.0);
	        	Matrix dMB_GXYZ1=new Matrix(adT3_GXYZ1);
	        	interParameterDerivatives[15]=d_parametersFromMAMB(dMA_GXYZ1,dMB_GXYZ1,MA,MB,false); // all after 6 are 0;
	        	if (this.debugLevel>2) {
	        		System.out.println("dMA_GXYZ1:");
	        		dMA_GXYZ1.print(10, 5);
	        		System.out.println("dMB_GXYZ1:");
	        		dMB_GXYZ1.print(10, 5);
	        		System.out.println("interParameterDerivatives[15]="+sprintfArray(interParameterDerivatives[15]));
	        	}
	        } else interParameterDerivatives[15]=null; 
	//14  z
	        if (mask[16]) {
//	    	double [][] adT3_GXYZ2={{0.0},{0.0},{1.0}};
	        	double [][] adT3_GXYZ2={{0.0},{0.0},{-1.0}}; // away - positive
	        	Matrix dMA_GXYZ2=new Matrix(3,3,0.0);
	        	Matrix dMB_GXYZ2=new Matrix(adT3_GXYZ2);
	        	interParameterDerivatives[16]=d_parametersFromMAMB(dMA_GXYZ2,dMB_GXYZ2,MA,MB,false); // all after 6 are 0;
	        	if (this.debugLevel>2) {
	        		System.out.println("dMA_GXYZ2:");
	        		dMA_GXYZ2.print(10, 5);
	        		System.out.println("dMB_GXYZ2:");
	        		dMB_GXYZ2.print(10, 5);
	        		System.out.println("interParameterDerivatives[16]="+sprintfArray(interParameterDerivatives[16]));
	        	}
	        } else interParameterDerivatives[16]=null; 
	// now fill the rest, unchanged parameters
	/*
	    int numInputs=22;   //was 21  parameters in subcamera+...
	    int numOutputs=13;  //was 12  parameters in a single camera

	 */
	//17 (15)		public double focalLength=4.5;
	//18 (16)		public double px0=1296.0;          // center of the lens on the sensor, pixels
	//19 (17)		public double py0=968.0;           // center of the lens on the sensor, pixels
	//20 (18)		public double distortionA8=0.0; // r^5 (normalized to focal length or to sensor half width?)
	//21 (19)		public double distortionA7=0.0; // r^5 (normalized to focal length or to sensor half width?)
	//22 (20)		public double distortionA6=0.0; // r^5 (normalized to focal length or to sensor half width?)
	//23 (21)		public double distortionA5=0.0; // r^5 (normalized to focal length or to sensor half width?)
	//24 (22)		public double distortionA=0.0; // r^4 (normalized to focal length or to sensor half width?)
	//25 (23)		public double distortionB=0.0; // r^3
	//26 (24)		public double distortionC=0.0; // r^2

//	        for (int inpPar=15; inpPar<getNumInputs(); inpPar++){
	        for (int inpPar=17; inpPar<getNumInputs(); inpPar++){ // OK with A8
	        	if (mask[inpPar]){
	        		interParameterDerivatives[inpPar]=new double[getNumOutputs()];
	        		for (int outPar=0; outPar<getNumOutputs(); outPar++){
	        			interParameterDerivatives[inpPar][outPar]=((getNumOutputs()-outPar)==(getNumInputs()-inpPar))?1.0:0.0;
	            		if (this.debugLevel>2) {
	            			System.out.println("interParameterDerivatives["+inpPar+"]["+outPar+"]="+interParameterDerivatives[inpPar][outPar]);
	            		}
	        		}
	        	} else interParameterDerivatives[inpPar]=null;
	        }
	    }
	 
	  
		
		
		
		
	}
// stores per pattern image camera/subcamera parameters, filenames, ...
// saves / restores them from a disk file
    public static class DistortionCalibrationData{
    	public String pathName=null;
    	public EyesisCameraParameters eyesisCameraParameters;
//        public int       numStations=1;    // number of differnt camera tripod/goniometer stations (locations)
        public int       numSubCameras=1;
        public int       numPointers=4;    // maximal number of pointers to look for
        public int       numMotors  =3;    // maximal number of motors to look for
        public GridImageParameters [] gIP= null; // per-grid image parameters
        public GridImageSet []        gIS= null; // sets of images with the same timestamp 
//    	public String [] paths=null;
//    	private ImagePlus []     gridImages=null; // array of grid images (to be used instead of files)
//    	public double [] timestamps=null;
//    	public int    [] channels=null;
        // keep for now?
    	public double [][] pars=null; // for each defined image: set of (22) parameters
//    	public double [][][] pixelsXY=null; // for each image, each grid node - a pair of {px,py}
//    	public int    [][][] pixelsUV=null; // for each image, each grid node - a pair of {gridU, gridV}
    	public double [][] sensorMasks= null; // per-channel (not image) mask
    	
    	//pixelsXY, pixelsUV should match, second dimension is variable
    	public boolean updateStatus=true;
    	public int     debugLevel=2;
    	private showDoubleFloatArrays SDFA_INSTANCE=null; // just for debugging
    	public int getNumStations(){
    		return (eyesisCameraParameters==null)?0:eyesisCameraParameters.getNumStations();
    	}

    	public class GridImageParameters{
    		public int         imgNumber=-1; // index of this image (for pars[][])
    		private int        setNumber=-1; // long overdue  - will be some inconsistency
    		GridImageSet       gridImageSet=null;
    		public int         stationNumber=0; // changes when camera/goniometer is moved to new position
    		public String      path=null;
    		public double [][] laserPixelCoordinates=null; // first index - absolute number of pointer. Each element may be either null or {x,y} pair
    		public int         matchedPointers=0;
    		public int         hintedMatch=-1; // -1 - not tried, 0 - no real grid (i.e. double reflection), applied orientation, applied orientation and shift
    		public boolean     enabled=true; //false;  // to mask out some images from all strategy steps (i.e w/o reliable absolute calibration)
    		public boolean     flatFieldAvailable=false; // grid files have flat field data
    		public boolean     newEnabled=false;
    		public int []      motors=null;
    		public ImagePlus   gridImage=null;
    		public double      timestamp=-1;
    		public int         channel=  -1;
    		public double []   intensityRange={255.0,255.0,255.0}; // r,g,b - used to normalize vign*
    		public double []   saturation={255.0,255.0,255.0}; // r,g,b - saturation range read from images
//    		public double  []  pars=null; // set of (22) parameters
    		public double [][] pixelsXY=   null; // for each image, each grid node - a set of of {px,py,contrast,vignR,vignG,vignB} vign* is in the 0..1.0 range
    		public double []   pixelsMask= null; // for each image, each grid node - weight function derived from contrast and 3 parameters
    		public int    [][] pixelsUV=  null; // for each image, each grid node - a pair of {gridU, gridV}
    		public boolean  [] badNodes=  null; // if not null, marks node with excessive errors 
    		public double [][] pixelsXY_extra=  null; // extra data, for nodes that are out of the physical grid (may be needed after re-calibration)
    		public int    [][] pixelsUV_extra=  null; 
    		public double      gridPeriod=0.0;  // average grid period, in pixels (to filter out (double-) reflected images
    		public boolean     noUsefulPSFKernels=false; // used to mark images w/o good PSF data
    		public double      diameter=0.0;
    		final int contrastIndex=2;
    		int getSetNumber(){return this.setNumber;}
        	public GridImageParameters(int index){
        		this.imgNumber=index;
        	}
        	public int getStationNumber(){ // TODO: make only a single station number - in GridImageSet?
        		return this.stationNumber;
        	}
        	public void setStationNumber(int stationNumber){ // TODO: make only a single station number - in GridImageSet?
        		this.stationNumber=stationNumber;
        	}
        	public double []getGridWeight(){
        		return this.pixelsMask;
        	}
        	public void resetMask(){
        			this.pixelsMask=null;
    	    }
        	public void resetBadNodes(){
        		this.badNodes=null;
        	}
        	public void setBadNode(int index){
        		if (this.badNodes==null){
        			this.badNodes=new boolean[this.pixelsXY.length]; // let it throw if null
        			for (int i=0;i<this.badNodes.length;i++)this.badNodes[i]=false;
        		}
        		this.badNodes[index]=true;
        	}
        	public boolean isNodeBad(int index){
        		if (this.badNodes==null) return false;
        		if (index>=this.badNodes.length) {
        			System.out.println("### isNodeBad("+index+") - OOB, as this.badNodes="+this.badNodes.length);
        			return true;
        		}
        		return this.badNodes[index]; //OOB
        	}

        	public int getNumContrastNodes(double minContrast){
        	    int num=0;	
        		for (int i=0;i<this.pixelsXY.length;i++) if (this.pixelsXY[i][contrastIndex]>=minContrast) num++;
        		return num;
        	}
        	/**
        	 * Calculate "diameter" of the image to be used for image weight
        	 * @param xc image center pixel X
        	 * @param yc image center pixel Y
        	 * @param r0 reference diameter
        	 * @param minContrast minimal contrast to count the node
        	 */
        	
        	public void setImageDiameter( // need to get image center px,py. Maybe r0 - use to normalize result diameter
        			double xc,
        			double yc,
        			double r0,
        			double minContrast,
        			int dbgImgNum //>=0 - debug print with image number
        			){
        		boolean debug=(dbgImgNum>=0);
        		// find the farthest point from the center
        		double maxR2=-1;
        		int firstIndex=0;
        		for (int i=0;i<this.pixelsXY.length;i++) if (this.pixelsXY[i][contrastIndex]>=minContrast){
        			double dx=this.pixelsXY[i][0]-xc;
        			double dy=this.pixelsXY[i][1]-yc;
        			double r2=dx*dx+dy*dy;
        			if (r2>maxR2) {
        				maxR2=r2;
        				firstIndex=i;
        			}
        		}
        		if (maxR2<=0) {
        			this.diameter=0.0;
        			return;
        		}
        		double maxDx=this.pixelsXY[firstIndex][0]-xc;
        		double maxDy=this.pixelsXY[firstIndex][1]-yc;

        		if (debug) System.out.print("setImageDiameter("+IJ.d2s(xc,2)+","+IJ.d2s(yc,2)+","+IJ.d2s(r0,2)+","+IJ.d2s(minContrast,4)+","+dbgImgNum+") ---- > ");
        		if (debug) System.out.print(" maxR2="+IJ.d2s(maxR2,2)+" maxDx="+IJ.d2s(maxDx,2)+" maxDy="+IJ.d2s(maxDy,2));
        		double maxAamb=0;
        		double dbgDx=0.0,dbgDy=0.0;
        		for (int i=0;i<this.pixelsXY.length;i++) if (this.pixelsXY[i][contrastIndex]>=minContrast){
//        			double dx=maxDx-this.pixelsXY[i][0];
//        			double dy=maxDy-this.pixelsXY[i][1];
        			double dx=this.pixelsXY[firstIndex][0]-this.pixelsXY[i][0];
        			double dy=this.pixelsXY[firstIndex][1]-this.pixelsXY[i][1];
        			double aAmb=dx*maxDx+dy*maxDy;
        			if (aAmb>maxAamb) {
        				maxAamb=aAmb;
        				dbgDx=this.pixelsXY[i][0]; // debug only !
        				dbgDy=this.pixelsXY[i][1]; // debug only !
        			}
        		}
        		this.diameter=maxAamb/Math.sqrt(maxR2)/r0;
        		if (debug) System.out.println(" maxAamb="+IJ.d2s(maxAamb,2)+" dbgDx="+IJ.d2s(dbgDx,2)+" dbgDy="+IJ.d2s(dbgDy,2)+" --> "+IJ.d2s(this.diameter,2));
        	}
        	/**
        	 * Uzses data calculated by  setImageDiameter();
        	 * @return detected grid diameter (along the radius) to be uses as image weight (in r0 units)
        	 */
        	public double getGridDiameter(){
        		return this.diameter;
        	}

        	public void calculateMask(
        			double minContrast,
        			double shrinkBlurSigma,
        			double shrinkBlurLevel){
        		if (this.pixelsMask!=null) return; // need to reset ro re-calculate 
        		if (this.pixelsUV==null) {this.pixelsMask=null; return; }
        		if (this.pixelsUV.length==0){ this.pixelsMask=new double[0]; return; }
        		
        		this.pixelsMask=new double [this.pixelsUV.length];
        		if (shrinkBlurSigma<=0){
            		for (int i=0;i<this.pixelsUV.length;i++){
            			this.pixelsMask[i]=(this.pixelsXY[i][contrastIndex]>=minContrast)?1.0:0.0;
            		}
            		return;
        		}
        		int minU=this.pixelsUV[0][0],minV=this.pixelsUV[0][1];
        		int maxU=minU,maxV=minV;
        		int margin=(int) (2*shrinkBlurSigma);
        		for (int i=0;i<this.pixelsUV.length;i++){
        			if (this.pixelsUV[i][0]>maxU) maxU=this.pixelsUV[i][0];
        			if (this.pixelsUV[i][0]<minU) minU=this.pixelsUV[i][0];
        			if (this.pixelsUV[i][1]>maxV) maxV=this.pixelsUV[i][1];
        			if (this.pixelsUV[i][1]<minV) minV=this.pixelsUV[i][1];
        		}
        		
        		int U0=minU-margin;
        		int V0=minV-margin;
        		int width= (maxU-minU+1+2*margin);
        		int height=(maxV-minV+1+2*margin);
        		double [] mask = new double [width*height];
        		for (int i=0;i<mask.length;i++) mask[i]=-1.0;
        		for (int i=0;i<this.pixelsUV.length;i++){
        			int index=(this.pixelsUV[i][0]-U0)+width*(this.pixelsUV[i][1]-V0);
        			mask[index]=(this.pixelsXY[i][contrastIndex]>=minContrast)?1.0:-1.0; // java.lang.ArrayIndexOutOfBoundsException: 2230
        		}        		
        		(new DoubleGaussianBlur()).blurDouble(
							mask,
							width,
							height,
							shrinkBlurSigma,
							shrinkBlurSigma,
							0.01);
        		double k=1.0/(1.0-shrinkBlurLevel);
        		double dbgMax=0.0;
        		for (int i=0;i<this.pixelsUV.length;i++){
        			int index=(this.pixelsUV[i][0]-U0)+width*(this.pixelsUV[i][1]-V0);
        			double d=k*(mask[index]-shrinkBlurLevel);
        			this.pixelsMask[i]=(d>0.0)?(d*d):0.0;
        			if (this.pixelsMask[i]>dbgMax) dbgMax=this.pixelsMask[i];
        		}
 //      		System.out.print(" "+IJ.d2s(dbgMax,2)+" ");
        	}
    	}
    	public class GridImageSet{
    		private int numPars=27;
    		private int thisParsStartIndex=6;

    		public int         stationNumber=0; // changes when camera/goniometer is moved to new position
    		public GridImageParameters [] imageSet=null;
//    		public GridImageParameters firstImage=null; // first non-null image in the sert (update to have current parameters?)
    		public double timeStamp;
    		public int [] motors=null;
    		public double goniometerAxial=Double.NaN;
    		public double goniometerTilt=Double.NaN;
    		public double interAxisDistance;    // 8 distance in mm between two goniometer axes
    		public double interAxisAngle;       // 9 angle in degrees between two goniometer axes minus 90. negative if "vertical" axis is rotated
    		public double horAxisErrPhi;        //10 angle in degrees "horizontal" goniometer axis is rotated around target Y axis from target X axis (CW)
    		public double horAxisErrPsi;        //11 angle in degrees "horizontal" goniometer axis is rotated around moving X axis (up)
    		public double entrancePupilForward; //12 common to all lenses - distance from the sensor to the lens entrance pupil
    		public double centerAboveHorizontal;//13 camera center distance along camera axis above the closest point to horizontal rotation axis (adds to height of each
    		public double [] GXYZ=new double [3];  //14 (12) coordinates (in mm) of the goniometer horizontal axis closest to the moving one in target system 
//			this.GXYZ[stationNumber][1],              //15 (13)  y
//			this.GXYZ[stationNumber][2],              //16 (14)  z
    		public boolean orientationEstimated=true; // orientation is estimated from other stes, notr adjusted by LMA
    		public double setWeight=0.0; // weight of this set when calculating errors
    		public void setEstimatedFromNonNaN(){
    			this.orientationEstimated= Double.isNaN(this.goniometerTilt) ||  Double.isNaN(this.goniometerAxial);
    		}
    		public int getMinIndex(){
    			return this.thisParsStartIndex;
    		}
    		public int getMaxIndexPlusOne(){
    			return this.thisParsStartIndex+getSetVector().length;
    		}
    		
    		public double [] getSetVector(){
    			double [] sv={
    		    		this.goniometerTilt,
    		    		this.goniometerAxial,
    		    		this.interAxisDistance,
    		    		this.interAxisAngle,
    		    		this.horAxisErrPhi,
    		    		this.horAxisErrPsi,
    		    		this.entrancePupilForward,
    		    		this.centerAboveHorizontal,
    		    		this.GXYZ[0], 
    		    		this.GXYZ[1], 
    		    		this.GXYZ[2] 
    			};
    			return sv;
    		}
    		public void setSetVector(double [] vector){
    			if (vector.length!=getSetVector().length){
    				String msg="Wrong parameter vector length - got:"+vector.length+", expected: "+getSetVector().length;
    				IJ.showMessage("Error",msg);
    				throw new IllegalArgumentException (msg);
    			}
	    		this.goniometerTilt=       vector[ 0];
	    		this.goniometerAxial=      vector[ 1];
	    		this.interAxisDistance=    vector[ 2];
	    		this.interAxisAngle=       vector[ 3];
	    		this.horAxisErrPhi=        vector[ 4];
	    		this.horAxisErrPsi=        vector[ 5];
	    		this.entrancePupilForward= vector[ 6];
	    		this.centerAboveHorizontal=vector[ 7];
	    		this.GXYZ[0]=              vector[ 8]; 
	    		this.GXYZ[1]=              vector[ 9]; 
	    		this.GXYZ[2]=              vector[10]; 
    		}

    		public double getParameterValue(int index){
    			int thisIndex=index-this.thisParsStartIndex;
    			double [] sv=getSetVector();
    			if ((thisIndex<0) || (index >sv.length)) return Double.NaN;
    			return sv[thisIndex];
    			
    		}
    		public void setParameterValue(int index,
    				double value,
    				boolean updateEstimated){
    			int thisIndex=index-this.thisParsStartIndex;
    			switch (thisIndex){
    			case  0:
    				this.goniometerTilt=       value;
    				setEstimatedFromNonNaN();
    				break;
    			case  1:
    				this.goniometerAxial=      value;
    				setEstimatedFromNonNaN();
    				break;
    			case  2: this.interAxisDistance=    value; break;
    			case  3: this.interAxisAngle=       value; break;
    			case  4: this.horAxisErrPhi=        value; break;
    			case  5: this.horAxisErrPsi=        value; break;
    			case  6: this.entrancePupilForward= value; break;
    			case  7: this.centerAboveHorizontal=value; break;
    			case  8: this.GXYZ[0]=              value; break; 
    			case  9: this.GXYZ[1]=              value; break; 
    			case 10: this.GXYZ[2]=              value; break; 
    			}
    		}

    		public double [] updateParameterVectorFromSet(double [] vector){
    			if (vector==null){
    				vector=new double [this.numPars];
    				for (int i=0;i<vector.length;i++) vector[i]=Double.NaN;
    			}
    			if (vector.length!=this.numPars){
    				String msg="Wrong parameter vector length - got:"+vector.length+", expected: "+this.numPars;
    				IJ.showMessage("Error",msg);
    				throw new IllegalArgumentException (msg);
    			}
    			double [] sv=getSetVector();
    			for (int i=0;i<sv.length;i++) if (!Double.isNaN(sv[i])) vector[i+this.thisParsStartIndex]=sv[i];
    			return vector;
    		}
    		public double [] updateParameterVectorFromSet(double [] vector, boolean [] mask){
    			if (vector==null){
    				vector=new double [this.numPars];
    				for (int i=0;i<vector.length;i++) vector[i]=Double.NaN;
    			}
    			if (vector.length!=this.numPars){
    				String msg="Wrong parameter vector length - got:"+vector.length+", expected: "+this.numPars;
    				IJ.showMessage("Error",msg);
    				throw new IllegalArgumentException (msg);
    			}
    			double [] sv=getSetVector();
    			for (int i=0;i<sv.length;i++) if (!Double.isNaN(sv[i]) && mask[this.thisParsStartIndex+ i]) vector[i+this.thisParsStartIndex]=sv[i];
    			return vector;
    		}
    		
    		public void updateSetFromParameterVector(double [] vector){
    			if (vector.length!=this.numPars){
    				String msg="Wrong parameter vector length - got:"+vector.length+", expected: "+this.numPars;
    				IJ.showMessage("Error",msg);
    				throw new IllegalArgumentException (msg);
    			}
	    		this.goniometerTilt=       vector[this.thisParsStartIndex+ 0];
	    		this.goniometerAxial=      vector[this.thisParsStartIndex+ 1];
	    		this.interAxisDistance=    vector[this.thisParsStartIndex+ 2];
	    		this.interAxisAngle=       vector[this.thisParsStartIndex+ 3];
	    		this.horAxisErrPhi=        vector[this.thisParsStartIndex+ 4];
	    		this.horAxisErrPsi=        vector[this.thisParsStartIndex+ 5];
	    		this.entrancePupilForward= vector[this.thisParsStartIndex+ 6];
	    		this.centerAboveHorizontal=vector[this.thisParsStartIndex+ 7];
	    		this.GXYZ[0]=              vector[this.thisParsStartIndex+ 8]; 
	    		this.GXYZ[1]=              vector[this.thisParsStartIndex+ 9]; 
	    		this.GXYZ[2]=              vector[this.thisParsStartIndex+10]; 
    		}

    		public void updateSetFromParameterVector(double [] vector, boolean [] mask){
    			if (vector.length!=this.numPars){
    				String msg="Wrong parameter vector length - got:"+vector.length+", expected: "+this.numPars;
    				IJ.showMessage("Error",msg);
    				throw new IllegalArgumentException (msg);
    			}
	    		if (mask[this.thisParsStartIndex+ 0]) this.goniometerTilt=       vector[this.thisParsStartIndex+ 0];
	    		if (mask[this.thisParsStartIndex+ 1]) this.goniometerAxial=      vector[this.thisParsStartIndex+ 1];
	    		if (mask[this.thisParsStartIndex+ 2]) this.interAxisDistance=    vector[this.thisParsStartIndex+ 2];
	    		if (mask[this.thisParsStartIndex+ 3]) this.interAxisAngle=       vector[this.thisParsStartIndex+ 3];
	    		if (mask[this.thisParsStartIndex+ 4]) this.horAxisErrPhi=        vector[this.thisParsStartIndex+ 4];
	    		if (mask[this.thisParsStartIndex+ 5]) this.horAxisErrPsi=        vector[this.thisParsStartIndex+ 5];
	    		if (mask[this.thisParsStartIndex+ 6]) this.entrancePupilForward= vector[this.thisParsStartIndex+ 6];
	    		if (mask[this.thisParsStartIndex+ 7]) this.centerAboveHorizontal=vector[this.thisParsStartIndex+ 7];
	    		if (mask[this.thisParsStartIndex+ 8]) this.GXYZ[0]=              vector[this.thisParsStartIndex+ 8]; 
	    		if (mask[this.thisParsStartIndex+ 9]) this.GXYZ[1]=              vector[this.thisParsStartIndex+ 9]; 
	    		if (mask[this.thisParsStartIndex+10]) this.GXYZ[2]=              vector[this.thisParsStartIndex+10]; 
    		}

    		public double getSetWeight(){return this.setWeight;}
        	public int getStationNumber(){ // TODO: make only a single station number - in GridImageSet?
        		return this.stationNumber;
        	}
        	public void setStationNumber(int stationNumber){ // TODO: make only a single station number - in GridImageSet?
        		this.stationNumber=stationNumber;
        	}
    	}
    	
        public String [][] parameterDescriptions ={
        		{"subcamAzimuth",        "Subcamera azimuth, clockwise looking from top","degrees","S","E"},                                    // 0
        		{"subcamDistance",       "Subcamera distance from the axis","mm","S","E"},                                                      // 1
        		{"subcamHeight",         "Subcamera height from the 'equator'","mm","S","E"},                                                   // 2
        		{"subcamHeading",        "Optical axis heading (relative to azimuth)","degrees","S","E"},                                       // 3
        		{"subcamElevation",      "Optical axis elevation (up from equator)","degrees","S","E"},                                         // 4
        		{"subcamRoll",           "Subcamera roll, positive CW looking to the target","degrees","S","E"},                                // 5
    			{"goniometerHorizontal", "Goniometer rotation around 'horizontal' axis (tilting from the target - positive)","degrees","R","E"},// 6
    			{"goniometerAxial",      "Rotation around Eyesis main axis (clockwise in plan - positive)","degrees","R","E"},                  // 7 
    			{"interAxisDistance",    "Distance between goniometer axes","mm","C","E"},                                                      // 8
    			{"interAxisAngle",       "Angle error between goniometer axes (<0 if vertical axis rotated CW )","degrees","C","E"},            // 9
    			{"horAxisErrPhi",        "Horizontal axis azimuth error (CW in plan)","degrees","C","E"},                                       //10
    			{"horAxisErrPsi",        "Horizontal axis roll error (CW looking to target)","degrees","C","E"},                                //11
    			{"entrancePupilForward", "Distance from the sensor to the lens entrance pupil","mm","C","E"},                              //12
    			{"centerAboveHorizontal","CenterAboveHorizontal","mm","C","E"},                                                            //13
    			{"GXYZ0",                "Goniometer reference point position X (target coordinates, left)","mm","T","E"},                      //14 (12) 
    			{"GXYZ1",                "Goniometer reference point position Y (target coordinates, up)","mm","T","E"},                        //15 (13)
    			{"GXYZ2",                "Goniometer reference point position Z (target coordinates, away)","mm","T","E"} ,                     //16 (14)
    			{"subcamFocalLength",    "Lens focal length","mm","S","I"},                                                                     //17 (15)
    			{"subcamPX0",            "Lens axis on the sensor (horizontal, from left edge)","pixels","S","I"},                              //18 (16)
    			{"subcamPY0",            "Lens axis on the sensor (vertical, from top edge)","pixels","S","I"},                                 //19 (17)
    			{"subcamDistortionA8",   "Distortion A8(r^5)","relative","S","I"},                                                              //20 (18)
    			{"subcamDistortionA7",   "Distortion A7(r^5)","relative","S","I"},                                                              //21 (19)
    			{"subcamDistortionA6",   "Distortion A6(r^5)","relative","S","I"},                                                              //22 (20)
    			{"subcamDistortionA5",   "Distortion A5(r^5)","relative","S","I"},                                                              //23 (21)
    			{"subcamDistortionA",    "Distortion A (r^4)","relative","S","I"},                                                              //24 (22)
    			{"subcamDistortionB",    "Distortion B (r^3)","relative","S","I"},                                                              //25 (23)
    			{"subcamDistortionC",    "Distortion C (r^2)","relative","S","I"}                                                               //26 (24)
        };
        public String [] channelSuffixes={ // natural order (same as array indices, may be modified to camera/subcamera
        		"00","01","02","03","04","05","06","07","08","09",
        		"10","11","12","13","14","15","16","17","18","19",
        		"20","21","22","23","24","25","26","27","28","29"};
        public int getParameterIndexByName(String name){
        	for (int i=0;i<this.parameterDescriptions.length;i++) if (this.parameterDescriptions[i][0].equals(name)){
        		return i;
        	}
        	return -1;
        }

/**
 * Initialize data from scratch using filenames "grid-<timestamp-seconds>_<timestamp-microseconds>-<channel-number>.tiff        
 * @param filenames List of grid filenames (2-slice TIFFs)
 */

        public DistortionCalibrationData (
        		String [] filenames,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters
        		) {
    	    String [][] stationFilenames={filenames};
        	setupDistortionCalibrationData(
        			stationFilenames,
            		patternParameters,
            		eyesisCameraParameters // debugLevel
            		);
        }
        public DistortionCalibrationData (
        		String [] filenames,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters,
        		int debugLevel
        		) {
        	    this.debugLevel=debugLevel;
        	    String [][] stationFilenames={filenames};
        	setupDistortionCalibrationData(
        			stationFilenames,
            		patternParameters,
            		eyesisCameraParameters // debugLevel
            		);
        }

        public DistortionCalibrationData (
        		String [][] stationFilenames,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters,
        		int debugLevel
        		) {
        	    this.debugLevel=debugLevel;
        	setupDistortionCalibrationData(
        			stationFilenames,
            		patternParameters,
            		eyesisCameraParameters // debugLevel
            		);
        }

        public void setupDistortionCalibrationData (
        		String [][] stationFilenames,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters
        		) {
        	this.eyesisCameraParameters=eyesisCameraParameters;
        	int numSubCameras=(eyesisCameraParameters==null)?1:eyesisCameraParameters.eyesisSubCameras[0].length;
        	this.numSubCameras=numSubCameras;
        	this.eyesisCameraParameters.numStations=stationFilenames.length;
        	int numFiles=0;
        	for (int i=0;i<stationFilenames.length;i++) numFiles+=stationFilenames[i].length;
        	this.gIP=new GridImageParameters[numFiles];

//        	this.paths=new String [filenames.length];
//        	this.timestamps=new double [filenames.length];
//        	this.channels=     new int [filenames.length];
        	
        	int numFile=0;
        	for (int numStation=0;numStation<stationFilenames.length;numStation++){
        		for (int index=0;index<stationFilenames[numStation].length;index++){
        			
        			System.out.println(numFile+" ("+numStation+":"+index+"): "+stationFilenames[numStation][index]);
        			this.gIP[numFile]=new GridImageParameters(numFile);
        			this.gIP[numFile].path=stationFilenames[numStation][index]; //Exception in thread "Run$_AWT-EventQueue-0" java.lang.NullPointerException at Distortions$DistortionCalibrationData.<init>(Distortions.java:5987)
        			this.gIP[numFile].setStationNumber(numStation);
        			int i1=stationFilenames[numStation][index].indexOf('-',stationFilenames[numStation][index].lastIndexOf(Prefs.getFileSeparator()));
        			int i2=stationFilenames[numStation][index].indexOf('-',i1+1);
        			int i3=stationFilenames[numStation][index].indexOf('.',i2+1);
        			// Extract timestamp from the filename        		
        			if ((i1<0) || (i2<0)) {
        				String msg="invalid file format - '"+stationFilenames[numStation][index]+"', should be '<timestamp-seconds>_<timestamp-microseconds>-<channel-number>.tiff'";
        				IJ.showMessage("Error",msg);
        				throw new IllegalArgumentException (msg);
        			}
        			// Extract channel number from the filename  

        			this.gIP[numFile].timestamp=Double.parseDouble(stationFilenames[numStation][index].substring(i1+1,i2).replace('_','.'));
        			String channelSuffix=stationFilenames[numStation][index].substring(i2+1,i3);
        			this.gIP[numFile].channel=-1;
        			for (int j=0;j<this.channelSuffixes.length;j++) if (channelSuffix.equals(this.channelSuffixes[j])) {
        				this.gIP[numFile].channel=j;
        				break;
        			}
        			if (this.gIP[numFile].channel<0) {
        				String msg="invalid file format (channel suffix not recognized) - '"+stationFilenames[numStation][index]+"', should be '<timestamp-seconds>_<timestamp-microseconds>-<channel-number>.tiff'";
        				msg+="\nThis channel suffix is "+channelSuffix+", available channel suffixes are:\n";
        				for (int j=0;j<this.channelSuffixes.length;j++) msg+=this.channelSuffixes[j]+", ";
        				IJ.showMessage("Error",msg);
        				throw new IllegalArgumentException (msg);
        			}
        			numFile++;
        		}
        	}
// Create parameters array
//        	this.pars=new double[this.gIP.length][parameterDescriptions.length];
        	initPars (this.gIP.length,parameterDescriptions.length);
        	if (this.debugLevel>1) System.out.println("setupDistortionCalibrationData(): Resetting this.gIS");
        	this.gIS=null; // so it will be initialized in readAllGrids() 
        	readAllGrids(patternParameters); // prepare grid parameters for LMA
        	// no orientation
        	
        }

  //	   		return (Integer) this.images[sensorNum].getProperty("POINTERS");
      
        public DistortionCalibrationData (
        		EyesisCameraParameters eyesisCameraParameters
        		) {
        	int numSubCameras=(eyesisCameraParameters==null)?1:eyesisCameraParameters.eyesisSubCameras[0].length;
        	
        	this.numSubCameras=numSubCameras;
        	this.eyesisCameraParameters=eyesisCameraParameters;
        }

         
        public DistortionCalibrationData (
        		ImagePlus [] images, // images in the memory
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters
        		) {
        	int numSubCameras=(eyesisCameraParameters==null)?1:eyesisCameraParameters.eyesisSubCameras[0].length;
        	this.numSubCameras=numSubCameras;
        	this.eyesisCameraParameters=eyesisCameraParameters;
        	setImages(images,patternParameters);
        }
        
        public int get_gIS_index(int numImg){
        	if (this.gIS==null) return -1;
        	for (int i=0;i<this.gIS.length;i++)
        		if (this.gIS[i].imageSet!=null)
        			for (int j=0;j<this.gIS[i].imageSet.length;j++)
        				if ((this.gIS[i].imageSet[j]!=null) &&(this.gIS[i].imageSet[j].imgNumber==numImg)) return i;
        	return -1;
        	
        }
        
        public void listCameraParameters(){
            int numSubCameras=getNumSubCameras();
            if (this.gIP!=null) {
            	int maxChn=0;
            	for (int i=0;i<this.gIP.length;i++) if ((this.gIP[i]!=null) && (this.gIP[i].channel>maxChn)){
            		maxChn=this.gIP[i].channel;
            	}
            	numSubCameras=maxChn+1;
            }
        	String header="Name\tUnits";
        	StringBuffer sb = new StringBuffer();
        	for (int i=0;i<numSubCameras;i++) header+="\t"+i;
            for (int stationNumber=0;stationNumber<this.eyesisCameraParameters.numStations;stationNumber++){
            	if (this.eyesisCameraParameters.numStations>1){
                	sb.append("Station "+stationNumber+" W="+(100*this.eyesisCameraParameters.stationWeight[stationNumber])+"%");  for (int i=-1;i<numSubCameras;i++) sb.append("\t===");  sb.append("\n");
            	}
            	double [][] cameraPars=new double [numSubCameras][];
            	for (int i=0;i<numSubCameras;i++) cameraPars[i]=eyesisCameraParameters.getParametersVector(stationNumber,i);
            	// parameters same order as in this
            	for (int n=0;n<cameraPars[0].length;n++) if (isSubcameraParameter(n) && isIntrinsicParameter(n)){
            		sb.append(getParameterName(n)+"\t"+getParameterUnits(n));
            		for (int i=0;i<numSubCameras;i++) sb.append("\t"+IJ.d2s(cameraPars[i][n],3));
            		sb.append("\n");
            	}
            	sb.append("---");  for (int i=-1;i<numSubCameras;i++) sb.append("\t");  sb.append("\n");

            	for (int n=0;n<cameraPars[0].length;n++) if (isSubcameraParameter(n) && !isIntrinsicParameter(n)){
            		sb.append(getParameterName(n)+"\t"+getParameterUnits(n));
            		for (int i=0;i<numSubCameras;i++) sb.append("\t"+IJ.d2s(cameraPars[i][n],3));
            		sb.append("\n");
            	}
            	sb.append("---");  for (int i=-1;i<numSubCameras;i++) sb.append("\t");  sb.append("\n");
            	for (int n=0;n<cameraPars[0].length;n++) if (
            			!isSubcameraParameter(n)&&
            			!isLocationParameter(n)&&
            			!isOrientationParameter(n)){
            		sb.append(getParameterName(n)+"\t"+getParameterUnits(n));
            		sb.append("\t"+IJ.d2s(cameraPars[0][n],3));
            		for (int i=1;i<numSubCameras;i++) sb.append("\t---");
            		sb.append("\n");
            	}
            	sb.append("---");  for (int i=-1;i<numSubCameras;i++) sb.append("\t");  sb.append("\n");
            	for (int n=0;n<cameraPars[0].length;n++) if (isLocationParameter(n)){
            		sb.append(getParameterName(n)+"\t"+getParameterUnits(n));
            		sb.append("\t"+IJ.d2s(cameraPars[0][n],3));
            		for (int i=1;i<numSubCameras;i++) sb.append("\t---");
            		sb.append("\n");
            	}
            	sb.append("---");  for (int i=-1;i<numSubCameras;i++) sb.append("\t");  sb.append("\n");
            	for (int n=0;n<cameraPars[0].length;n++) if (isOrientationParameter(n)){
            		sb.append(getParameterName(n)+"\t"+getParameterUnits(n));
            		sb.append("\t"+IJ.d2s(cameraPars[0][n],3));
            		for (int i=1;i<numSubCameras;i++) sb.append("\t---");
            		sb.append("\n");
            	}
            }
     	    new TextWindow("Camera parameters", header, sb.toString(), 85*(numSubCameras+3),600);
         }


        public void setImages(
        		ImagePlus [] images,  // images in the memory
        		PatternParameters patternParameters){
        	this.gIP=new GridImageParameters[images.length];
        	for (int i=0;i<images.length;i++){
        		this.gIP[i]=new GridImageParameters(i);
        		this.gIP[i].path=      images[i].getTitle(); // not real path?
        		this.gIP[i].timestamp= getImageTimestamp(images[i]);
        		System.out.println(i+": "+this.gIP[i].path+" - timestamp="+this.gIP[i].timestamp);
        		this.gIP[i].channel=   getImageChannel(images[i]);
            	this.gIP[i].gridImage=images[i]; // free later?

        	}
// Create parameters array
//        	this.pars=new double[images.length][parameterDescriptions.length];
        	initPars (this.gIP.length,parameterDescriptions.length);
        	this.gIS=null; // so it will be created in readAllGrids()
        	readAllGrids(patternParameters); // prepare grid parameters for LMA
        	// no orientation
        }
        
        public void listImageSet(){
        	listImageSet(null,null, null);
        } 
        
        public void listImageSet(
        		int [] numPoints,
        		double [] setRMS,
        		boolean [] hasNaNInSet){
        	if ((this.gIS==null) || (this.gIS.length==0)){
        		return;
        	}
        	String header="#\ttimestamp";
        	if (this.eyesisCameraParameters.numStations>1) header+="\tStation";
        	header+="\tAxial\tTilt\thorPhi\thorPsi\tX\tY\tZ\tMotor2\tMotor3";
        	if (numPoints!=null) header+="\tNumPoints";
        	header+="\tEnabled\tMatched";
        	if (setRMS!=null) header+="\tRMS\tWeight";
        	for (int n=0;n<this.gIS[0].imageSet.length;n++) header+="\t"+n;
    		StringBuffer sb = new StringBuffer();
    		
    		for (int i=0;i<this.gIS.length;i++){
    			double firstHorAxisErrPhi=Double.NaN;
    			double firstHorAxisErrPsi=Double.NaN;
    			double firstGXYZ0=        Double.NaN;
    			double firstGXYZ1=        Double.NaN;
    			double firstGXYZ2=        Double.NaN;
//    			int index=-1;
//    			for (int ni=0;ni<this.gIS[i].imageSet.length;ni++) if (this.gIS[i].imageSet[ni]!=null){
//    				index=this.gIS[i].imageSet[ni].imgNumber;
//    				break;
//   			}
//				int horAxisErrPhiIndex=getParameterIndexByName("horAxisErrPhi");
//				int horAxisErrPsiIndex=getParameterIndexByName("horAxisErrPsi");
//				if ((index>=0) && (horAxisErrPhiIndex>=0)) { 
//					firstHorAxisErrPhi=this.pars[index][horAxisErrPhiIndex];
					firstHorAxisErrPhi=this.gIS[i].horAxisErrPhi;
//				}
//				if ((index>=0) && (horAxisErrPsiIndex>=0)) { 
//					firstHorAxisErrPsi=this.pars[index][horAxisErrPsiIndex];
					firstHorAxisErrPsi=this.gIS[i].horAxisErrPsi;
//				}
//				int GXYZ0Index=getParameterIndexByName("GXYZ0");
//				if ((index>=0) && (GXYZ0Index>=0)) { 
//					firstGXYZ0=this.pars[index][GXYZ0Index];
					firstGXYZ0=this.gIS[i].GXYZ[0];
//				}
//				int GXYZ1Index=getParameterIndexByName("GXYZ1");
//				if ((index>=0) && (GXYZ1Index>=0)) { 
//					firstGXYZ1=this.pars[index][GXYZ1Index];
					firstGXYZ1=this.gIS[i].GXYZ[1];
//				}
//				int GXYZ2Index=getParameterIndexByName("GXYZ2");
//				if ((index>=0) && (GXYZ2Index>=0)) { 
//					firstGXYZ2=this.pars[index][GXYZ2Index];
					firstGXYZ2=this.gIS[i].GXYZ[2];
//				}
    			sb.append(i+"\t"+IJ.d2s(this.gIS[i].timeStamp,6));
    			if (this.eyesisCameraParameters.numStations>1)	sb.append(i+"\t"+ this.gIS[i].getStationNumber());
    			sb.append("\t"+(Double.isNaN(this.gIS[i].goniometerAxial)?"---":((this.gIS[i].orientationEstimated?"(":"")+IJ.d2s(this.gIS[i].goniometerAxial,3)+(this.gIS[i].orientationEstimated?")":""))));
    			sb.append("\t"+(Double.isNaN(this.gIS[i].goniometerTilt)?"---":((this.gIS[i].orientationEstimated?"(":"")+IJ.d2s(this.gIS[i].goniometerTilt,3)+(this.gIS[i].orientationEstimated?")":""))));
    			sb.append("\t"+(Double.isNaN(firstHorAxisErrPhi)?"---":IJ.d2s(firstHorAxisErrPhi,3)));
    			sb.append("\t"+(Double.isNaN(firstHorAxisErrPsi)?"---":IJ.d2s(firstHorAxisErrPsi,3)));
    			
    			sb.append("\t"+(Double.isNaN(firstGXYZ0)?"---":IJ.d2s(firstGXYZ0,3)));
    			sb.append("\t"+(Double.isNaN(firstGXYZ1)?"---":IJ.d2s(firstGXYZ1,3)));
    			sb.append("\t"+(Double.isNaN(firstGXYZ2)?"---":IJ.d2s(firstGXYZ2,3)));

    			if (this.gIS[i].motors==null) {
    				sb.append("\t"+"bug"+"\t"+"bug");
    			} else {
    				sb.append("\t"+this.gIS[i].motors[1]+"\t"+this.gIS[i].motors[2]); // null pointer here????
    			}
            	if (numPoints!=null) sb.append("\t"+numPoints[i]);
            	int numEnImages=0;
            	for (int n=0;n<this.gIS[i].imageSet.length;n++)if (this.gIS[i].imageSet[n]!=null){
            		if (this.gIS[i].imageSet[n].enabled) numEnImages++;
            	}            	
            	sb.append("\t"+numEnImages);
            	int matchedPointersInSet=0;
            	for (int n=0;n<this.gIS[i].imageSet.length;n++){
            		if (this.gIS[i].imageSet[n]!=null){
            			matchedPointersInSet+=this.gIS[i].imageSet[n].matchedPointers;            			
            		}
            	}
            	sb.append("\t"+matchedPointersInSet);
            	if (setRMS!=null) {
            		sb.append("\t"+(((hasNaNInSet!=null) && hasNaNInSet[i])?"*":"")+IJ.d2s(setRMS[i],3));
            		sb.append("\t"+IJ.d2s(this.gIS[i].setWeight,3));
            	}
            	for (int n=0;n<this.gIS[i].imageSet.length;n++){
            		sb.append("\t");
            		if (this.gIS[i].imageSet[n]!=null){
            			int numPointers=0; // count number of laser pointers
            			if (this.gIS[i].imageSet[n].laserPixelCoordinates!=null){
            				for (int j=0;j<this.gIS[i].imageSet[n].laserPixelCoordinates.length;j++) if (this.gIS[i].imageSet[n].laserPixelCoordinates[j]!=null) numPointers++;
            			}
            			if (!this.gIS[i].imageSet[n].enabled) sb.append("(");
            			sb.append(numPointers+"("+this.gIS[i].imageSet[n].matchedPointers+"):"+this.gIS[i].imageSet[n].hintedMatch +
            					" "+IJ.d2s(this.gIS[i].imageSet[n].gridPeriod,1));
            			if (!this.gIS[i].imageSet[n].enabled) sb.append(")");
            			
            		}
            	}
            	sb.append("\n");
    		}
			new TextWindow("Image calibration state (pointers/hinted state)", header, sb.toString(), 900,1400);
        }
        /**
         * crete list of image indices per image set
         * @return array of image indices for each image set 
         */
        public int [][] listImages(boolean enabledOnly){
        	int [][] imageSets = new int [this.gIS.length][];
    		for (int i=0;i<this.gIS.length;i++){
    			int setSize=0;
    			for (int n=0;n<this.gIS[i].imageSet.length;n++) if ((this.gIS[i].imageSet[n]!=null) && (this.gIS[i].imageSet[n].imgNumber>=0) && (!enabledOnly || this.gIS[i].imageSet[n].enabled)) setSize++;
    			imageSets[i]=new int [setSize];
    		}
    		for (int i=0;i<this.gIS.length;i++){
    			int index=0;
    			for (int n=0;n<this.gIS[i].imageSet.length;n++) if ((this.gIS[i].imageSet[n]!=null) && (this.gIS[i].imageSet[n].imgNumber>=0) && (!enabledOnly || this.gIS[i].imageSet[n].enabled)) imageSets[i][index++]=this.gIS[i].imageSet[n].imgNumber;
    		}
        	return imageSets;
        }
        
        /**
         * Filter images (grids) by calibration status with laser pointers and "hinted" from the camera orientation
         * buildImageSets may be needed to be re-ran (if it was ran with all=false)
         * @param resetHinted - if true - reset status of "hinted" calibration to undefined
         * @param minPointers minimal number of laser pointers considered to be enough (usually 2, as mirror/non-mirror is apriori known
         * @parame minGridPeriod - minimal detected grid period as a fraction of the maximal (filtering reflected grids)
         * @return number of enabled images
         */
        public int [] filterImages(
        		boolean resetHinted,
        		int minPointers,
        		double minGridPeriodFraction,
        		boolean disableNoVignetting,
        		int minGridNodes){
        	int notEnoughNodes=0;
        	int numEnabled=0;
        	int newEnabled=0;
        	int maxPeriod=100;
        	int periodSubdivide=10;
        	int numBins=maxPeriod*periodSubdivide;
        	double [] periodHistogram=new double[numBins];
        	double [] medianGridPeriod=new double [this.eyesisCameraParameters.numStations];
        	double [] maxGridPeriod=new double [this.eyesisCameraParameters.numStations];
        	double [] minGridPeriod=new double [this.eyesisCameraParameters.numStations];
        	for (int stationNumber=0;stationNumber<this.eyesisCameraParameters.numStations;stationNumber++){
        		for (int i=0;i<numBins;i++) periodHistogram[i]=0.0;
        		int numSamples=0;
        		for (int i=0;i<this.gIP.length;i++) if (this.gIP[i].getStationNumber()==stationNumber){
        			if (!Double.isNaN(this.gIP[i].gridPeriod)) {
        				int iPeriod=(int) Math.round(this.gIP[i].gridPeriod*periodSubdivide);
        				if (iPeriod>=numBins) iPeriod=numBins-1;
        				else if (iPeriod<0) iPeriod=0; // does not count NaN
        				if (iPeriod>0) {
        					periodHistogram[iPeriod]++;
        					numSamples++;
        				}
        			}
        		}
        		int sumLess=0;
        		medianGridPeriod[stationNumber]=0.0;
        		for (int i=0;i<numBins;i++){
        			sumLess+=periodHistogram[i];
        			if (sumLess>(numSamples/2)) {
        				medianGridPeriod[stationNumber]=(1.0*i)/periodSubdivide;
        				break;
        			}
        		}

        		maxGridPeriod[stationNumber]=0.0;
        		for (int i=0;i<this.gIP.length;i++) if (this.gIP[i].getStationNumber()==stationNumber){
        			if (this.gIP[i].gridPeriod>maxGridPeriod[stationNumber]) maxGridPeriod[stationNumber]=this.gIP[i].gridPeriod;
        		}
        		minGridPeriod[stationNumber]=medianGridPeriod[stationNumber]*minGridPeriodFraction;
            	System.out.print("Station "+stationNumber+ ": maximal grid period="+maxGridPeriod[stationNumber]+" minimal grid period="+minGridPeriod[stationNumber]+" median grid period="+medianGridPeriod[stationNumber]+" numSamples="+numSamples);
            	if (minGridPeriodFraction>0.0) maxGridPeriod[stationNumber]=medianGridPeriod[stationNumber]/minGridPeriodFraction;
            	System.out.println(" new maximal grid period="+maxGridPeriod[stationNumber]);
        	}
        	// set which image set each image belongs
        	int [] gIS_index=new int [this.gIP.length];
        	for (int i=0;i<gIS_index.length;i++)gIS_index[i]=-1;
        	if (this.gIS!=null){
            	for (int i=0;i<this.gIS.length;i++) if (this.gIS[i].imageSet!=null)for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null){
            		gIS_index[this.gIS[i].imageSet[j].imgNumber]=i;
            	}
        	}
        	int numNoVignetting=0;
        	for (int i=0;i<this.gIP.length;i++){
        		int stationNumber=this.gIP[i].getStationNumber();
        		boolean wasEnabled=this.gIP[i].enabled;
        		if (resetHinted) this.gIP[i].hintedMatch=-1; // undefined
        		if (Double.isNaN(this.gIP[i].gridPeriod) ||
        				((minGridPeriodFraction>0) && ((this.gIP[i].gridPeriod<minGridPeriod[stationNumber]) || (this.gIP[i].gridPeriod>maxGridPeriod[stationNumber])))){
        			this.gIP[i].hintedMatch=0; // is it needed? 
        			this.gIP[i].enabled=false; // failed against minimal grid period (too far) - probably double reflection in the windows
        		}
        		if (this.gIP[i].hintedMatch==0) this.gIP[i].enabled=false; // failed against predicted grid
        		else {
        			if (
        					(this.gIP[i].matchedPointers>=minPointers) ||
        					((this.gIP[i].matchedPointers>0) && (this.gIP[i].hintedMatch>0)) || // orientation and one pointer
        					(this.gIP[i].hintedMatch>1)) {
        				// before enabling - copy orientation from gIS  
        				if (!this.gIP[i].enabled && (gIS_index[i]>=0)){ 
        					if (!Double.isNaN(this.gIS[gIS_index[i]].goniometerTilt))	setGH(i,this.gIS[gIS_index[i]].goniometerTilt );
        					if (!Double.isNaN(this.gIS[gIS_index[i]].goniometerAxial))	setGA(i,this.gIS[gIS_index[i]].goniometerAxial );
        				}
        				this.gIP[i].enabled=true;
        			}
        			else this.gIP[i].enabled=false;
        		}
        			
        		if (disableNoVignetting) {
        			if (this.gIP[i].enabled &!this.gIP[i].flatFieldAvailable) numNoVignetting++;
        			this.gIP[i].enabled &= this.gIP[i].flatFieldAvailable;
        		}
        		if (this.gIP[i].motors==null) this.gIP[i].enabled=false; // got some no-motor images made without scanning
        		
        		/* Disable no-pointer, new, number of points less than required */
        		if (this.gIP[i].enabled && !wasEnabled && (this.gIP[i].matchedPointers==0) && (this.gIP[i].pixelsXY.length<minGridNodes)){
        			this.gIP[i].enabled=false;
        			notEnoughNodes++;
        		}
        		
            	if (this.gIP[i].enabled) numEnabled++;
            	this.gIP[i].newEnabled=this.gIP[i].enabled&&!wasEnabled;
            	if (this.gIP[i].newEnabled) newEnabled++;
        	}
        	// may need buildImageSets
        	int [] result={numEnabled,newEnabled,numNoVignetting,notEnoughNodes};
        	return result;
        }
// TODO:
        // 1 -  Filter by lasers/hint state
        // 2 - recalculate hinted
        // connect "enabled" to strategies (not done yet)
      //  applyHintedGrids90 - moved to the parent class
        
        
        /**
         * Create array of image sets ("panoramas"), sorted by timestamps
         * @return number of sets
         */
        public int buildImageSets(boolean preserveSet){
        	if (this.debugLevel>0) {
        		System.out.println("buildImageSets("+preserveSet+")");
        	}
        	if (!preserveSet){
        		List <Double> timeStampList=new ArrayList<Double>(this.gIP.length);
        		int numChannels=0;
        		for (int i=0;i<this.gIP.length;i++) {
        			if (this.gIP[i].channel>numChannels) numChannels=this.gIP[i].channel;
        			int j=0;
        			Double ts=this.gIP[i].timestamp;
        			if (!timeStampList.contains(ts)){
        				for (;(j<timeStampList.size()) && (ts>timeStampList.get(j));j++);
        				timeStampList.add(j,ts);
        			}
        		}
        		numChannels++;
        		this.gIS=new GridImageSet[timeStampList.size()];
        		for (int i=0;i<this.gIS.length;i++){
        			this.gIS[i]=new GridImageSet();
        			this.gIS[i].timeStamp=timeStampList.get(i);
        			this.gIS[i].imageSet=new GridImageParameters [numChannels];
        			for (int j=0;j<numChannels;j++) this.gIS[i].imageSet[j]=null;

        		}
        		for (int i=0;i<this.gIP.length;i++) {
        			Double ts=this.gIP[i].timestamp;
        			int iIS=timeStampList.indexOf(ts);
        			this.gIS[iIS].setStationNumber(this.gIP[i].getStationNumber());
        			this.gIS[iIS].imageSet[this.gIP[i].channel]=this.gIP[i];
//        			if (this.gIP[i].motors!=null) this.gIS[iIS].motors=this.gIP[i].motors;
        			this.gIP[i].setNumber=iIS;
        			this.gIP[i].gridImageSet=this.gIS[iIS];
        		}
        		// verify that station number is the same for the same timestamp
        		for (int i=0;i<this.gIP.length;i++) {
        			Double ts=this.gIP[i].timestamp;
        			int iIS=timeStampList.indexOf(ts);
        			if (this.gIS[iIS].getStationNumber()!=this.gIP[i].getStationNumber()){
        				String msg="Inconsistent station number for timestamp "+ts+": this.gIS[iIS].getStationNumber()="+this.gIS[iIS].getStationNumber()+
        				" this.gIP[i].getStationNumber()="+this.gIP[i].getStationNumber()+", using "+this.gIS[iIS].getStationNumber();
        				System.out.println(msg);
        				IJ.showMessage("Error:",msg);
        				this.gIP[i].setStationNumber(this.gIS[iIS].getStationNumber());
        			}
        		}
        	}
    		for (int i=0;i<this.gIP.length;i++) {
    			int iIS=this.gIP[i].setNumber;
    			if (this.gIP[i].motors!=null) this.gIS[iIS].motors=this.gIP[i].motors.clone();
    		}
        	return this.gIS.length;
        }
        
        /**
         * Create array of image sets ("panoramas"), sorted by timestamps
         * @param all // use all images (false - only enabled)
         * @return number of sets
         */
        
        public int buildImageSetsOld(boolean all){
        	List <Double> timeStampList=new ArrayList<Double>(this.gIP.length);
        	int numChannels=0;
        	for (int i=0;i<this.gIP.length;i++) if (all || this.gIP[i].enabled){
        		if (this.gIP[i].channel>numChannels) numChannels=this.gIP[i].channel;
        		int j=0;
        		Double ts=this.gIP[i].timestamp;
        		if (!timeStampList.contains(ts)){
        			for (;(j<timeStampList.size()) && (ts>timeStampList.get(j));j++);
        			timeStampList.add(j,ts);
        		}
        	}
        	numChannels++;
        	this.gIS=new GridImageSet[timeStampList.size()];
        	for (int i=0;i<this.gIS.length;i++){
        		this.gIS[i]=new GridImageSet();
        		this.gIS[i].timeStamp=timeStampList.get(i);
        		this.gIS[i].imageSet=new GridImageParameters [numChannels];
        		for (int j=0;j<numChannels;j++) this.gIS[i].imageSet[j]=null;

        	}
        	for (int i=0;i<this.gIP.length;i++) if (all || this.gIP[i].enabled){
        		Double ts=this.gIP[i].timestamp;
        		int iIS=timeStampList.indexOf(ts);
        		this.gIS[iIS].imageSet[this.gIP[i].channel]=this.gIP[i];
        		if (this.gIP[i].motors!=null) this.gIS[iIS].motors=this.gIP[i].motors;
        	}
        	return this.gIS.length;
        }

        /**
         * Set goniometer initial orientation from the image with maximal number of laser pointers (make averaging later?)
         * Needed before LMA to have some reasonable initial orientation
         * @param overwriteAll if true, overwrite orientation data even if it is alredy not NaN, false -skipp those that have orientation set 
         */
        public void setInitialOrientation(boolean overwriteAll){
			if (this.debugLevel>0) {
				System.out.println("setInitialOrientation("+overwriteAll+"), debugLevel= "+this.debugLevel);
			}

        	for (int i=0; i<this.gIS.length;i++){
        		int stationNumber=this.gIS[i].getStationNumber();
        		int bestRating=-1;
        		int bestChannel=-1;
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if ((this.gIS[i].imageSet[j]!=null) && this.gIS[i].imageSet[j].enabled){
        			int thisRating=this.gIS[i].imageSet[j].matchedPointers+((this.gIS[i].imageSet[j].hintedMatch>0)?1:0); // rate hintedMatch 2 higher?
        			if (thisRating>bestRating) {
        				bestRating=thisRating;
        				bestChannel=j;
        			}
        		}
        		if (bestRating>0){
        			if (overwriteAll || Double.isNaN(this.gIS[i].goniometerAxial)){
 //       				System.out.println("setInitialOrientation("+overwriteAll+"),  Double.isNaN(this.gIS["+i+"].goniometerAxial)="+Double.isNaN(this.gIS[i].goniometerAxial));
        				this.gIS[i].goniometerAxial=-this.eyesisCameraParameters.eyesisSubCameras[stationNumber][bestChannel].azimuth;
        				for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) setGA(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerAxial);
            			this.gIS[i].orientationEstimated=true;
            			if (this.debugLevel>1) {
            				System.out.println("Setting goniometerAxial for the image set #"+i+" ("+this.gIS[i].timeStamp+") to "+this.gIS[i].goniometerAxial+" +++++ orientationEstimated==true +++++");
            			}
        			}
        			if (overwriteAll || Double.isNaN(this.gIS[i].goniometerTilt )){
//        				System.out.println("setInitialOrientation("+overwriteAll+"),  Double.isNaN(this.gIS["+i+"].goniometerTilt)="+Double.isNaN(this.gIS[i].goniometerTilt));
        				this.gIS[i].goniometerTilt= -this.eyesisCameraParameters.eyesisSubCameras[stationNumber][bestChannel].theta;
        				for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) setGH(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerTilt);
            			this.gIS[i].orientationEstimated=true;
            			if (this.debugLevel>1) {
            				System.out.println("Setting goniometerTilt for the image set #"+i+" ("+this.gIS[i].timeStamp+") to "+this.gIS[i].goniometerTilt+" ===== orientationEstimated==true =====");
            			}
        			}
        		}
        	}
        }
        /**
         * update image set (panorama, set of simultaneous images) goniometer orientation from the image parameters, do after running LMA 
         * @param selectedImages boolean array of selected images (in current strategy) or null (all selected)
         */
// TODO: potential problem here if only some images were enabled in the strategy -- FIXED
// TODO: Add other extrinsic parameters here to sets?
        /**
         * Updated version - only flag as orientationEstimated if no enabled images exist in the set or any of the angles is NaN
         * Temporarily duplicate  image parameters from those of the set (should not be needed)
         * selectedImages will not be used 
         */
        public void updateSetOrientation(boolean [] selectedImages){ // if selectedImages[] is not null will set orientationEstimated for unselected images
        	if (this.gIS==null){
        		String msg="Image set is not initilaized";
        		System.out.println(msg);
        		IJ.showMessage(msg);
        	}
        	
        	for (int i=0; i<this.gIS.length;i++){
        		this.gIS[i].orientationEstimated=true;
        		if (!Double.isNaN(this.gIS[i].goniometerAxial) && !Double.isNaN(this.gIS[i].goniometerTilt)) {
        			for (int j=0;j<this.gIS[i].imageSet.length;j++) if ((this.gIS[i].imageSet[j]!=null) && this.gIS[i].imageSet[j].enabled){
        				if ((selectedImages==null) || selectedImages[this.gIS[i].imageSet[j].imgNumber]) {
        					this.gIS[i].goniometerAxial-=360.0*Math.floor((this.gIS[i].goniometerAxial+180.0)/360.0);
        					this.gIS[i].orientationEstimated=false;
        					break; // set from the first non-null, enabled image
        				}
        			}
        		}
        		if (!this.gIS[i].orientationEstimated){
        			// now fill that data to all disabled images of the same set (just for listing RMS errors and debugging)
        			for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) { // fill even those that are enabled 
        				setGA(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerAxial );
        				setGH(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerTilt );
        			}
        		} else {
        			this.gIS[i].goniometerAxial=Double.NaN;
        			this.gIS[i].goniometerTilt= Double.NaN;
        			System.out.println("updateSetOrientation(): imageSet "+i+" orientationEstimated == true");
        		}
        	}
        }
        
        public void updateSetOrientationOld(boolean [] selectedImages){
        	if (this.gIS==null){
        		String msg="Image set is not initilaized";
        		System.out.println(msg);
        		IJ.showMessage(msg);
        	}
        	for (int i=0; i<this.gIS.length;i++){
        		if (selectedImages==null){ // if all selected - remove orientation if there are no enabled images (i.e. after removeOutlayers)
    				this.gIS[i].goniometerAxial=Double.NaN;
    				this.gIS[i].goniometerTilt= Double.NaN;
    				this.gIS[i].orientationEstimated=true;
        			
        		}
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if ((this.gIS[i].imageSet[j]!=null) && this.gIS[i].imageSet[j].enabled){
        			if ((selectedImages==null) || selectedImages[this.gIS[i].imageSet[j].imgNumber]) {
        				this.gIS[i].goniometerAxial=getGA(this.gIS[i].imageSet[j].imgNumber);  //update - most likely will do nothing (if set has non-NaN)
        				this.gIS[i].goniometerTilt= getGH(this.gIS[i].imageSet[j].imgNumber);
        				this.gIS[i].goniometerAxial-=360.0*Math.floor((this.gIS[i].goniometerAxial+180.0)/360.0);
        				this.gIS[i].orientationEstimated=false;
        				break; // set from the first non-null, enabled image
        			}
        		}
        		// now fill that data to all disabled images of the same set (just for listing RMS errors and debugging)
        		if (!Double.isNaN(this.gIS[i].goniometerAxial) && !Double.isNaN(this.gIS[i].goniometerTilt)){
        			for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) { // fill even those that are enabled 
        				setGA(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerAxial );
        				setGH(this.gIS[i].imageSet[j].imgNumber,this.gIS[i].goniometerTilt );
        			}
        		}
        	}
        }
        
        public boolean isEstimated(int imgNum){
        	if (this.gIS==null) {
            	String msg="Image sets are not initialized";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (this.gIP[imgNum].gridImageSet!=null) return this.gIP[imgNum].gridImageSet.orientationEstimated;
        	// should not get here 
        	System.out.println("FIXME: isEstimated("+imgNum+"): this.gIP["+imgNum+"].gridImageSet==null");
        	for (int i=0;i<this.gIS.length;i++)	if (this.gIS[i].imageSet!=null){
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if ((this.gIS[i].imageSet[j]!=null) && (this.gIS[i].imageSet[j].imgNumber==imgNum)){
        			return this.gIS[i].orientationEstimated;
        		}
        	}
        	String msg="Image with index "+imgNum+" is not in the image set";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
        }
        public boolean isEstimatedOld(int imgNum){
        	if (this.gIS==null) {
            	String msg="Image sets are not initialized";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	for (int i=0;i<this.gIS.length;i++)	if (this.gIS[i].imageSet!=null){
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if ((this.gIS[i].imageSet[j]!=null) && (this.gIS[i].imageSet[j].imgNumber==imgNum)){
        			return this.gIS[i].orientationEstimated;
        		}
        	}
        	String msg="Image with index "+imgNum+" is not in the image set";
    		IJ.showMessage("Error",msg);
    		throw new IllegalArgumentException (msg);
        }
        int getNumberOfEstimated(boolean enabledOnly) {
        	int numEstimated=0;
        	if (this.gIS==null) return 0;
        	for (int i=0;i<this.gIS.length;i++)	if (this.gIS[i].imageSet!=null){
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) {
        			if ((!enabledOnly || this.gIS[i].imageSet[j].enabled) && this.gIS[i].orientationEstimated) numEstimated++;
        			
        		}
        	}
        	return numEstimated;
        }

        int [] getNumberOfEstimatedPerStation(boolean enabledOnly) {
        	int [] numEstimated=new int [this.eyesisCameraParameters.numStations];
        	for (int i=0;i<numEstimated.length;i++) numEstimated[i]=0;
        	if (this.gIS!=null){
        		for (int i=0;i<this.gIS.length;i++)	if (this.gIS[i].imageSet!=null){
        			for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) {
        				if ((!enabledOnly || this.gIS[i].imageSet[j].enabled) && this.gIS[i].orientationEstimated) numEstimated[this.gIS[i].getStationNumber()]++;
        			}
        		}
        	}
        	return numEstimated;
        }

        
        int getNumEnabled(){
        	int num=0;
        	for (int i=0;i<this.gIP.length;i++) if ((this.gIP[i]!=null) && this.gIP[i].enabled) num++;
        	return num;
        }

        int getNumNewEnabled(){
        	int num=0;
        	for (int i=0;i<this.gIP.length;i++) if ((this.gIP[i]!=null) && this.gIP[i].enabled && this.gIP[i].newEnabled) num++;
        	return num;
        }

        int [] getNumNewEnabledPerStation(){
        	int [] numEnabled=new int [this.eyesisCameraParameters.numStations];
        	for (int i=0;i<numEnabled.length;i++) numEnabled[i]=0;
        	for (int i=0;i<this.gIP.length;i++) if ((this.gIP[i]!=null) && this.gIP[i].enabled && this.gIP[i].newEnabled) numEnabled[this.gIP[i].getStationNumber()]++;//  OOB 837
        	return numEnabled;
        }

        
        
        boolean [] selectNewEnabled () {
        	boolean [] newEnabled=new boolean [this.gIP.length] ;
        	for (int i=0;i<this.gIP.length;i++) newEnabled[i]= (this.gIP[i]!=null) && this.gIP[i].enabled && this.gIP[i].newEnabled;
        	return newEnabled;
        }

        boolean [] selectEstimated (boolean enabledOnly) {
        	if (this.gIS==null) {
            	String msg="Image sets are not initialized";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	
        	boolean [] estimated=new boolean [getNumImages()];
        	for (int i=0;i<estimated.length;i++) estimated[i]=false;
        	for (int i=0;i<this.gIS.length;i++)	if (this.gIS[i].imageSet!=null){
        		for (int j=0;j<this.gIS[i].imageSet.length;j++) if (this.gIS[i].imageSet[j]!=null) {
        			if ((!enabledOnly || this.gIS[i].imageSet[j].enabled) ) estimated[this.gIS[i].imageSet[j].imgNumber]= this.gIS[i].orientationEstimated;
        			
        		}
        	}
        	return estimated;
        }
        public void enableSelected(boolean [] selected){
        	for (int i=0;i<this.gIP.length  ;i++) if (this.gIP[i]!=null){
        		int i1=i;
        		if (i1>=selected.length) i1=selected.length-1;
        		this.gIP[i].enabled = selected[i1];
        	}
        }
        /**
         * Calculate goniometer orientation for one of the "known" images/grids
         * @param imgNum grid image number
         * @return pair of {goniometerHorizontal, goniometerAxial} (in angular degrees)
         */
        public double [] getImagesetTiltAxial(int imgNum){
        	return getImagesetTiltAxial(this.gIP[imgNum].timestamp);
        }
        /**
         * Return pair of {goniometerHorizontal, goniometerAxial} for the specified timestamp
         * updateSetOrientation() should be called after LMA or other updates to camera parameters
         * @param timeStamp - double timestamp identifying imageset (image does not need to be a part of selected grid files)
         * @return null if no images set has the specified timestamp, may contain Double.NaN if the orientation was not set.
         */
        public double [] getImagesetTiltAxial(double timeStamp){
        	int mAxial=1;     // m2 
        	int mHorizontal=2;// m3
        	// this is probably already set
        	for (int i=0;i<this.gIS.length;i++){
        		if ((this.gIS[i].imageSet!=null) && (this.gIS[i].imageSet.length>0) && (this.gIS[i].imageSet[0]!=null)) this.gIS[i].setStationNumber(this.gIS[i].imageSet[0].getStationNumber());
            }
        	for (int i=0;i<this.gIS.length;i++)
        		if (this.gIS[i].timeStamp==timeStamp) {
    				int iBest=i;
        			if (Double.isNaN(this.gIS[i].goniometerTilt) || Double.isNaN(this.gIS[i].goniometerAxial)) {
// find the closest one (by motors)
        				if (this.gIS[i].motors==null) {
                			if (this.debugLevel>0) System.out.println("getImagesetTiltAxial("+timeStamp+"): No motor data");
        					return null;
        				}
// Maybe later use both motors, for now - just the axial. It seems to have <0.5 degree error (but accumulates gradually as there are friction rollers involved).
        				int thisMotorHorizontal=this.gIS[i].motors[mHorizontal];
        				int thisMotorAxial=     this.gIS[i].motors[mAxial];
        				int stationNumber=      this.gIS[i].getStationNumber();
            			ArrayList<Integer> setList=new ArrayList<Integer>(100);
            			for (int j=0;j<this.gIS.length;j++) {
            				if (this.gIS[j]==null){
            					System.out.println("BUG?: getImagesetTiltAxial("+timeStamp+"): this.gIS["+j+"]==null");
            					continue;
            				}
            				if (this.gIS[j].motors==null){
            					System.out.println("BUG?: getImagesetTiltAxial("+timeStamp+"): this.gIS["+j+"].motors==null");
            					continue;
            				}
            				if ( //   (j!=i)  && // not needed - this set does not have orientation
            						(this.gIS[j].getStationNumber()==stationNumber) &&
            						(this.gIS[j].motors[mHorizontal]==thisMotorHorizontal) &&
            						!Double.isNaN(this.gIS[j].goniometerTilt) &&
            						!Double.isNaN(this.gIS[j].goniometerAxial)){
            					setList.add(new Integer(j));
            				}
            			}
            			if (setList.size()>=2){
            				if (this.debugLevel>2) System.out.println("getImagesetTiltAxial("+timeStamp+"): estimating orientation for set # "+i+": this.debugLevel="+this.debugLevel);
            				// find the closest one
            				int indexClosest=setList.get(0);
            				double dClosest=Math.abs(this.gIS[indexClosest].motors[mAxial]-thisMotorAxial);
            				for (int j=1;j<setList.size();j++) if (Math.abs(this.gIS[setList.get(j)].motors[mAxial]-thisMotorAxial)<dClosest){
            					indexClosest=setList.get(j);
            					dClosest=Math.abs(this.gIS[indexClosest].motors[mAxial]-thisMotorAxial);
            				}
            				// try to get the second on the other side than the closest first
            				int indexSecond=-1;
            				for (int j=0;j<setList.size();j++) {
            					if (((this.gIS[indexClosest].motors[mAxial]-thisMotorAxial)*
            							(this.gIS[setList.get(j)].motors[mAxial]-thisMotorAxial)<0) && // different side 
            							((indexSecond<0) || (Math.abs(this.gIS[setList.get(j)].motors[mAxial]-thisMotorAxial)<dClosest))){
            						indexSecond=setList.get(j);
            						dClosest=Math.abs(this.gIS[indexSecond].motors[mAxial]-thisMotorAxial);
            					}
            				}            				
            				if (this.debugLevel>2) System.out.println("indexSecond="+indexSecond);
            				if (indexSecond<0){ // no sets on the opposite side from the indexClosest, use second closest on the same side as indexClosest
                				for (int j=0;j<setList.size();j++) {
                					if ((setList.get(j)!=indexClosest) &&
                							((indexSecond<0) || (Math.abs(this.gIS[setList.get(j)].motors[mAxial]-thisMotorAxial)<dClosest))){
                						indexSecond=setList.get(j);
                						dClosest=Math.abs(this.gIS[indexSecond].motors[mAxial]-thisMotorAxial);
                					}
                				}            				
            					
            				}
            				if (indexSecond<0){ // no second sets at all
            					System.out.println("getImagesetTiltAxial("+timeStamp+") - this is a BUG ");
            				} else {
            					// now linear interpolate axail between theses two sets: indexClosest and indexSecond. (resolve/ guess crossing 360
            					double axialClosest=this.gIS[indexClosest].goniometerAxial;
            					double axialSecond= this.gIS[indexSecond].goniometerAxial;
            					axialClosest-=360.0*Math.floor((axialClosest+180.0)/360.0);
            					axialSecond-= 360.0*Math.floor((axialSecond+ 180.0)/360.0);
                				if (this.debugLevel>2) System.out.println("getImagesetTiltAxial("+timeStamp+"):"+
                						" same tilt - "+setList.size()+
                						" axialClosest="+axialClosest+
                						" axialSecond="+axialSecond+
                						" motor closest="+this.gIS[indexClosest].motors[mAxial]+
                						" motor second="+this.gIS[indexSecond].motors[mAxial]);
            					// axial motor has the same sign/direction as the axial angle
            					if (this.gIS[indexSecond].motors[mAxial]>this.gIS[indexClosest].motors[mAxial]){
            						if (axialSecond<axialClosest) axialSecond+=360.0;
            					} else {
            						if (axialSecond>axialClosest) axialClosest+=360.0;
            					}
            					this.gIS[i].goniometerAxial=
            						axialClosest+
            						(axialSecond-axialClosest)*
            						(thisMotorAxial-this.gIS[indexClosest].motors[mAxial])/
            						(this.gIS[indexSecond].motors[mAxial]-this.gIS[indexClosest].motors[mAxial]);
            					this.gIS[i].goniometerTilt=
            						this.gIS[indexClosest].goniometerTilt+
            						(this.gIS[indexSecond].goniometerTilt-this.gIS[indexClosest].goniometerTilt)*
            						(thisMotorAxial-this.gIS[indexClosest].motors[mAxial])/
            						(this.gIS[indexSecond].motors[mAxial]-this.gIS[indexClosest].motors[mAxial]);
            				}
            			} else { // old way
            				double d2Min=-1;
            				for (int j=0;j<this.gIS.length;j++) if ((j!=i) && (this.gIS[j].motors!=null) &&
            						!Double.isNaN(this.gIS[j].goniometerTilt) && !Double.isNaN(this.gIS[j].goniometerAxial)) {
            					double d2=0;
            					for (int k=0;k<this.gIS[j].motors.length;k++){
            						d2+=1.0*(this.gIS[j].motors[k]-this.gIS[i].motors[k])*
            						(this.gIS[j].motors[k]-this.gIS[i].motors[k]);
            					}
            					if ((d2Min<0) || (d2Min>d2)) {
            						d2Min=d2;
            						iBest=j;
            					}
            				}
            			}
        			}
        			double [] result = {
        					this.gIS[iBest].goniometerTilt,
        					this.gIS[iBest].goniometerAxial
        			};
        			if (iBest!=i){
            			if (this.debugLevel>0) System.out.println("Orientation for set # "+i+" timestamp "+IJ.d2s(this.gIS[i].timeStamp,6)+
            					") is not defined, using # "+iBest+" (timestamp "+IJ.d2s(this.gIS[iBest].timeStamp,6)+")" );
            			this.gIS[i].orientationEstimated=true;
    					this.gIS[i].goniometerTilt= this.gIS[iBest].goniometerTilt;
    					this.gIS[i].goniometerAxial=this.gIS[iBest].goniometerAxial;
        			}
       				return result; // may have Double.NaN
        	}
        	return null;
        }
        
        public double getImageTimestamp(ImagePlus image){
        	if ((image.getProperty("timestamp")==null) || (((String) image.getProperty("timestamp")).length()==0)) {
        		(new JP46_Reader_camera(false)).decodeProperiesFromInfo(image);
        	}
        	return Double.parseDouble((String) image.getProperty("timestamp"));
        }

        public int getImageChannel(ImagePlus image){
        	if ((image.getProperty("channel")==null) || (((String) image.getProperty("channel")).length()==0)) {
        		(new JP46_Reader_camera(false)).decodeProperiesFromInfo(image);
        	}

        	String channelSuffix=(String) image.getProperty("channel");
        	int channel=-1;
        	for (int j=0;j<this.channelSuffixes.length;j++){
//        		System.out.println("== j="+j);
//        		System.out.println("channelSuffix="+channelSuffix);
//        		System.out.println("this.channelSuffixes[j]="+this.channelSuffixes[j]);
        		if (channelSuffix.equals(this.channelSuffixes[j])) {
        			channel=j;
        			break;
        		}
        	}
        	if (channel<0) {
        		String msg="Channel not recognized) - this channel suffix is "+channelSuffix+", available channel suffixes are:\n";
        		for (int j=0;j<this.channelSuffixes.length;j++) msg+=this.channelSuffixes[j]+", ";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return channel; 
        }
 
        /**
         * initialize image data with camera defaults
         * @param distortionCalibrationData grid distortionCalibrationData
         * @param eyesisCameraParameters deafault camera parameters
         * @return
         */
        // Used in Goniometer
        public void initImageSet( 
        		EyesisCameraParameters eyesisCameraParameters) {
        	for (int i=0;i<this.getNumImages();i++){
        		int subCam=this.getImageSubcamera(i);
        		int stationNumber=this.getImageStation(i);
        		this.setParameters(eyesisCameraParameters.getParametersVector(stationNumber,subCam), i);
        	}
        }
        
        
        
// constructor from XML file
        
        public DistortionCalibrationData (
        		boolean smart,
        		String defaultPath,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters,
				ImagePlus[] gridImages  ){ // null - use specified files
			String [] extensions={".dcal-xml","-distcal.xml"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Distortion calibration *.dcal-xml files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					false,
					"Restore Calibration Parameters",
					"Restore",
					parFilter,
					defaultPath); //String defaultPath
			if ((pathname==null) || (pathname=="")) return;
//			setGridImages(gridImages);
//TODO: these images will be overwritten by setFromXML !!!!!!!!!			
			this.gIS=null; // So readAllGrids will create it 
        	setFromXML(
        			pathname,
            		eyesisCameraParameters);
			if (gridImages!=null) {
//				this.pathName="";  // modified, keep the path anyway
// overwrite saved paths with the provided images, number of images{ should match
				if (this.gIP.length!=gridImages.length){
					String msg="Number of provided images ("+gridImages.length+") does not match parameters restored from the "+pathname+" ("+this.gIP.length+")";
		    		IJ.showMessage("Error",msg);
		    		throw new IllegalArgumentException (msg);
				}
				for (int i=0;i<this.gIP.length;i++){
					this.gIP[i].gridImage=gridImages[i];
					this.gIP[i].path=null; // not needed, just in case
					this.gIP[i].enabled=true;// enable all (actually just one) acquired images
				}
//				setGridImages(gridImages);
			}
        	readAllGrids(patternParameters); // prepare grid parameters for LMA
			updateSetOrientation(null); // update orientation of image sets (built in readAllGrids() UPDATE - not anymore)

        }
/*
        public DistortionCalibrationData(
        		String pathname,
        		PatternParameters patternParameters,
        		EyesisCameraParameters eyesisCameraParameters) {
        	setFromXML(
        			pathname,
            		eyesisCameraParameters);
        	System.out.println("DistortionCalibrationData("+pathname+",eyesisCameraParameters) 1 -> this.gIS.length="+((this.gIS==null)?"null":this.gIS.length));
        	readAllGrids(patternParameters); // prepare grid parameters for LMA (now will preserve this.gIS if it is non-null)
        	System.out.println("DistortionCalibrationData("+pathname+",eyesisCameraParameters) 2 -> this.gIS.length="+((this.gIS==null)?"null":this.gIS.length));
			updateSetOrientation(null); // update orientation of image sets (built in readAllGrids())
        	}
*/        
        public void setFromXML(String pathname,
        		EyesisCameraParameters eyesisCameraParameters) {
        	this.eyesisCameraParameters=eyesisCameraParameters;

        	XMLConfiguration hConfig=null;
        	try {
				hConfig=new XMLConfiguration(pathname);
			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
    		this.numSubCameras=Integer.parseInt(hConfig.getString("subcameras","1"));	
        	System.out.println("Number of subcameras is "+this.numSubCameras);
			int num=hConfig.getMaxIndex("file");
			num++;
        	this.gIP=new GridImageParameters[num];
        	this.pars=new double[num][parameterDescriptions.length];
        	System.out.println("Number of pattern grid images in "+pathname+" is "+num);
        	
        	int numSets=hConfig.getMaxIndex("set")+1; // see if it returns -1 for none
        	System.out.println("Number of image sets in "+pathname+" is "+numSets);
        	if (numSets>0){
            	this.gIS=new GridImageSet[numSets];
            	for (int i=0;i<numSets;i++) {
            		HierarchicalConfiguration sub = hConfig.configurationAt("set("+i+")");
            		int index=Integer.parseInt(sub.getString("index"));
            		this.gIS[index]=new GridImageSet();
            		this.gIS[index].timeStamp=     Double.parseDouble(sub.getString("timestamp"));
            		this.gIS[index].stationNumber= Integer.parseInt(sub.getString("stationNumber"));
                	int minIndex=       this.gIS[index].getMinIndex();
                	int maxIndexPlusOne=this.gIS[index].getMaxIndexPlusOne();
                	for (int j=minIndex;j<maxIndexPlusOne;j++) if (sub.getString(parameterDescriptions[j][0])!=null) {
                		this.gIS[index].setParameterValue(j,Double.parseDouble((sub.getString(parameterDescriptions[j][0]))), false);
                	}
            		if (sub.getString("orientationEstimated")!=null) {
            			this.gIS[i].orientationEstimated=Boolean.parseBoolean(sub.getString("orientationEstimated"));
/*                    	System.out.println(i+": restored orientationEstimated="+this.gIS[i].orientationEstimated+
                    			" tilt="+this.gIS[i].goniometerTilt+
                    			" axial="+this.gIS[i].goniometerAxial);*/
            		} else {
                		this.gIS[i].setEstimatedFromNonNaN();
/*                    	System.out.println(i+": guessed. orientationEstimated="+this.gIS[i].orientationEstimated+
                    			" tilt="+this.gIS[i].goniometerTilt+
                    			" axial="+this.gIS[i].goniometerAxial);*/
            		}
            		
            	}

        	} else {
        		this.gIS=null; // has to be build later
        	}
        	
        	for (int i=0;i<num;i++) {
        		this.gIP[i]=new GridImageParameters(i);
        		HierarchicalConfiguration sub = hConfig.configurationAt("file("+i+")");
        		this.gIP[i].imgNumber=i;
        		this.gIP[i].path=sub.getString("name");	
        		this.gIP[i].timestamp=Double.parseDouble(sub.getString("timestamp"));	
        		this.gIP[i].channel=Integer.parseInt(sub.getString("channel"));
        		if (sub.getString("stationNumber")!=null) this.gIP[i].setStationNumber(Integer.parseInt(sub.getString("stationNumber")));
        		else this.gIP[i].setStationNumber(0);
        		if (sub.getString("enabled")!=null) this.gIP[i].enabled=Boolean.parseBoolean(sub.getString("enabled"));
        		if (sub.getString("noUsefulPSFKernels")!=null) this.gIP[i].noUsefulPSFKernels=Boolean.parseBoolean(sub.getString("noUsefulPSFKernels"));
        		if (sub.getString("setNumber")!=null) {
        			this.gIP[i].setNumber=Integer.parseInt(sub.getString("setNumber"));
        		} else {
        			this.gIP[i].setNumber=-1;
        		}
        		for (int j=0;j<this.parameterDescriptions.length;j++){
        			if (sub.getString(parameterDescriptions[j][0])!=null)
        				this.pars[i][j] = Double.parseDouble(sub.getString(parameterDescriptions[j][0]));
        			else 
        				this.pars[i][j] = Double.NaN;
        		}
        	}
        	if (this.gIS!=null){
            	System.out.println("Using stored image set data");
        		for (int is=0;is<this.gIS.length;is++){
            		this.gIS[is].imageSet=new GridImageParameters [this.numSubCameras];
            		for (int j=0;j<this.numSubCameras;j++) this.gIS[is].imageSet[j]=null;
        		}
        		for (int ip=0;ip<this.gIP.length;ip++) if (this.gIP[ip].setNumber>=0) {
        			this.gIS[this.gIP[ip].setNumber].imageSet[this.gIP[ip].channel]=this.gIP[ip];
        			this.gIP[ip].gridImageSet=this.gIS[this.gIP[ip].setNumber];
        			//this.gIP[i].channel
        		}
        		
        	} else {
            	System.out.println("Re-creating image set data from individual images (old format)");
            	System.out.println("WARNING: Some parameters may get from unused images and so have wrong values");
            	buildImageSets(false); // from scratch
            	// copying only parameters that have the same values for all images in a set
            	for (int is=0;is<this.gIS.length;is++){
            		int minIndex=       this.gIS[is].getMinIndex();
            		int maxIndexPlusOne=this.gIS[is].getMaxIndexPlusOne();
            		for (int pi=minIndex;pi<maxIndexPlusOne;pi++){
                		double parVal=Double.NaN;
                		boolean differs=false;
            			for (int j=0;j<this.gIS[is].imageSet.length;j++) if (this.gIS[is].imageSet[j]!=null) {
            				int imgNum=this.gIS[is].imageSet[j].imgNumber;
            				if (!Double.isNaN(this.pars[imgNum][pi])){
            					if (!Double.isNaN(parVal) && (parVal!=this.pars[imgNum][pi])){
            						differs=true;
            						break;
            					} else {
            						parVal=this.pars[imgNum][pi];
            					}
            				}
            				if (!differs && !Double.isNaN(parVal)){
            					this.gIS[is].setParameterValue(pi,parVal,false);
            				}
            				if (differs){
            					System.out.println("ImageSet #"+is+": "+parameterDescriptions[j][0] +" has different values for individual images, skipping");
            				}
            			}
            		}
            		this.gIS[is].setEstimatedFromNonNaN();
            		//orientationEstimated
            		System.out.println(is+": tilt="+this.gIS[is].goniometerTilt+" axial="+this.gIS[is].goniometerAxial+" estimated="+this.gIS[is].orientationEstimated);

            	}
            	System.out.println("setFromXML("+pathname+",eyesisCameraParameters) 1 -> this.gIS.length="+this.gIS.length);
        	}
//        	System.out.println("setFromXML("+pathname+",eyesisCameraParameters) 2 -> this.gIS.length="+((this.gIS==null)?"null":this.gIS.length));
        	this.pathName=pathname; // where this instance was created from
        }
  //http://commons.apache.org/configuration/userguide/howto_xml.html
        public String getPath(){
        	return this.pathName;
        }
        public String selectAndSaveToXML(boolean smart, String defaultPath){
        	return selectAndSaveToXML(smart, defaultPath, null);
        }
        public String selectAndSaveToXML(boolean smart, String defaultPath, String comment){
			String [] extensions={".dcal-xml","-distcal.xml"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Distortion calibration *.dcal-xml files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					true,
					"Save Calibration Parameters",
					"Save",
					parFilter,
					(defaultPath==null)?this.pathName:defaultPath); //String defaultPath
			if (pathname!=null) saveToXML(pathname,comment);
			return pathname;
        }
        public boolean saveTimestampedToXML(String pathname, String comment) {
        	return saveToXML(pathname+"_"+IJ.d2s(0.000001*(System.nanoTime()/1000),6).replace('.', '_')+".dcal-xml",      // full path or null
        			null);
        }        
        public boolean saveToXML(String pathname) {
        	return saveToXML(pathname,null);
        }        
        public boolean saveToXML(String pathname, String comment) {
        	XMLConfiguration hConfig=new XMLConfiguration();
        	if (comment!=null) hConfig.addProperty("comment",comment);
        	hConfig.setRootElementName("distortionCalibrationParameters");
        	hConfig.addProperty("subcameras",this.numSubCameras);
        	for (int i=0;i<this.gIP.length;i++){
            	hConfig.addProperty("file","");
            	hConfig.addProperty("file.setNumber",this.gIP[i].setNumber);
            	hConfig.addProperty("file.name",this.gIP[i].path);
            	hConfig.addProperty("file.enabled",this.gIP[i].enabled);
            	hConfig.addProperty("file.timestamp",IJ.d2s(this.gIP[i].timestamp,6));
            	hConfig.addProperty("file.channel",this.gIP[i].channel);
            	hConfig.addProperty("file.stationNumber",this.gIP[i].getStationNumber());
            	hConfig.addProperty("file.noUsefulPSFKernels",this.gIP[i].noUsefulPSFKernels);
            	for (int j=0;j<this.parameterDescriptions.length;j++){
                	hConfig.addProperty("file."+parameterDescriptions[j][0],this.pars[i][j]);
            	}
        	}
// save image sets
        	for (int i=0;i<this.gIS.length;i++){
            	hConfig.addProperty("set","");
            	hConfig.addProperty("set.index",i);
            	hConfig.addProperty("set.stationNumber",this.gIS[i].stationNumber);
            	hConfig.addProperty("set.timestamp",    IJ.d2s(this.gIS[i].timeStamp,6));
            	hConfig.addProperty("set.orientationEstimated",this.gIS[i].orientationEstimated);
            	double [] vector = this.gIS[i].updateParameterVectorFromSet(null); // unused parameters will be NaN
            	for (int j=0;j<vector.length;j++) if (!Double.isNaN(vector[j])){
            		hConfig.addProperty("set."+parameterDescriptions[j][0],vector[j]);
            	}
        	}
       	
//        	hConfig.addProperty("grids","");
        	File file=new File (pathname);
        	BufferedWriter writer;
			try {
				writer = new BufferedWriter(new FileWriter(file));
	        	hConfig.save(writer);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			this.pathName=pathname;
        	return true;
        }
      
//    		public double      gridPeriod=0.0;  // average grid period, in pixels (to filter out (double-) reflected images
        public double calcGridPeriod(int fileNumber){
        	if ((this.gIP[fileNumber].pixelsXY==null) || (this.gIP[fileNumber].pixelsXY.length<3)) {
        		this.gIP[fileNumber].gridPeriod=Double.NaN;
        	} else {
        		double [][][] data =new double [this.gIP[fileNumber].pixelsXY.length][2][2];
        		// U(x,y), v(x,y)
        		for (int i=0;i<data.length;i++){
        			data[i][0][0]=this.gIP[fileNumber].pixelsXY[i][0];
        			data[i][0][1]=this.gIP[fileNumber].pixelsXY[i][1];
        			data[i][1][0]=this.gIP[fileNumber].pixelsUV[i][0];
        			data[i][1][1]=this.gIP[fileNumber].pixelsUV[i][1];
        		}
        		if (this.debugLevel>3) {
        			System.out.println("calcGridPeriod("+fileNumber+"), debugLevel="+this.debugLevel+":");
            		for (int i=0;i<data.length;i++)System.out.println(i+": {{"+data[i][0][0]+","+data[i][0][1]+"},{"+data[i][1][0]+","+data[i][1][1]+"}}");
        		}
         	   double [][] coeff=new PolynomialApproximation(this.debugLevel).quadraticApproximation(data, true); // force linear
         	   if (coeff!=null) {
         	     this.gIP[fileNumber].gridPeriod=2.0/Math.sqrt(coeff[0][0]*coeff[0][0]+coeff[0][1]*coeff[0][1]+coeff[1][0]*coeff[1][0]+coeff[1][1]*coeff[1][1]);
         	     if (this.debugLevel>3) {
         	    	System.out.println("coeff[][]={{"+coeff[0][0]+","+coeff[0][1]+"},{"+coeff[1][0]+","+coeff[1][1]+"}}");
         	     }
         	   } else {
        		  this.gIP[fileNumber].gridPeriod=Double.NaN;
         	   }
        	}
    		if (this.debugLevel>3) {
    			System.out.println("calcGridPeriod("+fileNumber+") => "+this.gIP[fileNumber].gridPeriod);
    		}
        	return this.gIP[fileNumber].gridPeriod;

        }
        
        public boolean readAllGrids(PatternParameters patternParameters){
        	boolean disableNoFlatfield=false;  // true only for processing transitional images - mixture of ff/ no-ff 
			System.out.println("readAllGrids(), this.debugLevel="+this.debugLevel+" this.gIS is "+((this.gIS==null)?"null":"not null")); 
        	int numImages=getNumImages();
//        	this.pixelsXY=new double[numImages][][];
//        	this.pixelsUV=new int[numImages][][];
    		Opener opener=new Opener();
    		JP46_Reader_camera jp4_reader= new JP46_Reader_camera(false);
    		ImagePlus imp_grid=null;
    		ImageStack stack;
    		int numOfGridNodes=0;
    		int numOfGridNodes_extra=0;
        	for (int fileNumber=0;fileNumber<numImages;fileNumber++){
        		if (this.gIP[fileNumber].gridImage!=null){ // use in-memory grid images instead of the files
        			int numGridImg=fileNumber;
        			if (numGridImg>=this.gIP.length) numGridImg=this.gIP.length-1;
        			if (this.updateStatus) IJ.showStatus("Using in-memory grid image "+(fileNumber+1)+" (of "+(numImages)+"): "+
        					this.gIP[numGridImg].gridImage.getTitle());
        			if (this.debugLevel>1) System.out.print((fileNumber+1)+": "+this.gIP[numGridImg].gridImage.getTitle());
        			imp_grid=this.gIP[numGridImg].gridImage;
        		} else {
        			if (this.updateStatus) IJ.showStatus("Reading grid file "+(fileNumber+1)+" (of "+(numImages)+"): "+this.gIP[fileNumber].path);
        			if (this.debugLevel>1) System.out.print(fileNumber+" ("+this.gIP[fileNumber].getStationNumber()+"): "+this.gIP[fileNumber].path);
        			imp_grid=opener.openImage("", this.gIP[fileNumber].path);  // or (path+filenames[nFile])
        			if (imp_grid==null) {
        				String msg="Failed to read grid file "+this.gIP[fileNumber].path;
        				IJ.showMessage("Error",msg);
        				throw new IllegalArgumentException (msg);
        			}
// TODO: here - need to decode properties
        			jp4_reader.decodeProperiesFromInfo(imp_grid);
        		}
        		this.gIP[fileNumber].laserPixelCoordinates=getPointersXY(imp_grid, this.numPointers);
        		this.gIP[fileNumber].motors=getMotorPositions(imp_grid, this.numMotors);
        		this.gIP[fileNumber].matchedPointers=getUsedPonters(imp_grid);
//        		this.gIP[fileNumber].enabled=true; // will filter separately
        		this.gIP[fileNumber].hintedMatch=-1; // unknown yet
        		
        		double [] saturations=new double [4];
        		for (int i=0;i<saturations.length;i++) {
        			saturations[i]=Double.NaN;
        			if (imp_grid.getProperty("saturation_" + i) !=null) saturations[i]=Double.parseDouble((String) imp_grid.getProperty("saturation_" + i)); 
        		}
        		if (!Double.isNaN(saturations[1])) this.gIP[fileNumber].saturation[0]=saturations[1];
        		if (!Double.isNaN(saturations[2])) this.gIP[fileNumber].saturation[2]=saturations[2];
        		if (!Double.isNaN(saturations[0]) && !Double.isNaN(saturations[3])) this.gIP[fileNumber].saturation[1]=0.5*(saturations[0]+saturations[3]);
        		else {
            		if (!Double.isNaN(saturations[0])) this.gIP[fileNumber].saturation[1]=saturations[0];
            		if (!Double.isNaN(saturations[3])) this.gIP[fileNumber].saturation[1]=saturations[3];
        		}
        		
                stack=imp_grid.getStack();
            	if ((stack==null) || (stack.getSize()<4)) {
            		String msg="Expected a 8-slice stack in "+this.gIP[fileNumber].path;
            		IJ.showMessage("Error",msg);
            		throw new IllegalArgumentException (msg);
            	}
        		float [][] pixels=new float[stack.getSize()][]; // now - 8 (x,y,u,v,contrast, vignR,vignG,vignB
            	for (int i=0;i<pixels.length;i++) pixels[i]= (float[]) stack.getPixels(i+1); // pixel X : negative - no grid here
            	
            	if (this.eyesisCameraParameters.badNodeThreshold>0.0){
            		boolean thisDebug =false;
//            		thisDebug|=        (fileNumber== 720); // chn 25
//            		thisDebug|=        (fileNumber== 793); // chn 10
//            		thisDebug|=        (fileNumber== 895); // chn 15
//            		thisDebug|=        (fileNumber==1359); // chn  0
//            		thisDebug|=        (fileNumber==1029); // chn  2
//            		thisDebug|=        (fileNumber==1081); // chn 14

//            		int maxBadNeighb=1; // 7 of 8 shold be good
            		
            		
                 int numBadNodes=fixBadGridNodes(
                		pixels,
                		stack.getWidth(),
                		this.eyesisCameraParameters.badNodeThreshold,
                		this.eyesisCameraParameters.maxBadNeighb,
                		this.debugLevel+(thisDebug?3:0),
                		thisDebug?("fixBad-"+fileNumber):null
                		);
                 if (this.debugLevel>1) {
                  if (numBadNodes>0)
                	  System.out.print("  -- replaced "+numBadNodes+" bad grid nodes");
                  System.out.println();
                 }
            	}

            	
        		int sensorWidth=this.eyesisCameraParameters.getSensorWidth(this.gIP[fileNumber].channel);
        		int sensorHeight=this.eyesisCameraParameters.getSensorHeight(this.gIP[fileNumber].channel);
            	int station=this.gIP[fileNumber].getStationNumber();
            	int size=0;
            	int size_extra=0;
            	for (int i=0;i<pixels[0].length;i++) if ((pixels[0][i]>=0) && (pixels[1][i]>=0) && (pixels[0][i]<sensorWidth) && (pixels[1][i]<sensorHeight)){
            		int u=(int) Math.round(pixels[2][i]);
            		int v=(int) Math.round(pixels[3][i]);
//            		if (patternParameters.getXYZM(u,v,this.debugLevel>1)!=null) size++;
            		if (patternParameters.getXYZM(u,v,false,station)!=null) size++;
            		else size_extra++;
            	}
            	this.gIP[fileNumber].resetMask();
            	this.gIP[fileNumber].pixelsXY=new double [size][6];
            	this.gIP[fileNumber].pixelsUV=new int    [size][2];
            	this.gIP[fileNumber].pixelsXY_extra=new double [size_extra][6];
            	this.gIP[fileNumber].pixelsUV_extra=new int    [size_extra][2];
            	
            	
            	numOfGridNodes+=size;
            	numOfGridNodes_extra+=size_extra;
            	int index=0;
            	int index_extra=0;
//            	boolean vignettingAvailable=pixels.length>=8;
    			this.gIP[fileNumber].flatFieldAvailable=pixels.length>=8;

            	if (disableNoFlatfield && !this.gIP[fileNumber].flatFieldAvailable) this.gIP[fileNumber].enabled=false; // just to use old mixed data
            	for (int i=0;i<pixels[0].length;i++) if ((pixels[0][i]>=0) && (pixels[1][i]>=0) && (pixels[0][i]<sensorWidth) && (pixels[1][i]<sensorHeight)) {
            		int u=(int) Math.round(pixels[2][i]);
            		int v=(int) Math.round(pixels[3][i]);
//            		if (patternParameters.getXYZM(u,v,this.debugLevel>1)!=null) {
            		if (patternParameters.getXYZM(u,v,false,station)!=null) {
            			this.gIP[fileNumber].pixelsXY[index][0]=pixels[0][i];
            			this.gIP[fileNumber].pixelsXY[index][1]=pixels[1][i];
            			this.gIP[fileNumber].pixelsUV[index][0]= u;
            			this.gIP[fileNumber].pixelsUV[index][1]= v;
            			if (this.gIP[fileNumber].flatFieldAvailable){
            				this.gIP[fileNumber].pixelsXY[index][2]=pixels[4][i];
            				for (int n=0;n<3;n++) this.gIP[fileNumber].pixelsXY[index][n+3]=pixels[n+5][i]/this.gIP[fileNumber].intensityRange[n];
            			} else {
            				for (int n=0;n<4;n++)this.gIP[fileNumber].pixelsXY[index][n+2]=1.0;
            			}
            			index++;
            		} else {
            			this.gIP[fileNumber].pixelsXY_extra[index_extra][0]=pixels[0][i];
            			this.gIP[fileNumber].pixelsXY_extra[index_extra][1]=pixels[1][i];
            			this.gIP[fileNumber].pixelsUV_extra[index_extra][0]= u;
            			this.gIP[fileNumber].pixelsUV_extra[index_extra][1]= v;
            			if (this.gIP[fileNumber].flatFieldAvailable){
            				this.gIP[fileNumber].pixelsXY_extra[index_extra][2]=pixels[4][i];
            				for (int n=0;n<3;n++){
            					this.gIP[fileNumber].pixelsXY_extra[index_extra][n+3]=pixels[n+5][i]/this.gIP[fileNumber].intensityRange[n];
            				}
            			} else {
            				for (int n=0;n<4;n++)this.gIP[fileNumber].pixelsXY_extra[index_extra][n+2]=1.0;
            			}
            			index_extra++;
            		}
            	}
            	
            	
            	calcGridPeriod(fileNumber); // will be used to filter out reflections 
//System.out.println ("pixelsXY["+fileNumber+"]length="+pixelsXY[fileNumber].length);
        	}
    		if (this.debugLevel>3) {
    			System.out.println("readAllGrids(), numImages="+numImages);
    			for (int n=0;n<this.gIP.length;n++) {
					System.out.println(n+": length="+this.gIP[n].pixelsXY.length);
	    			System.out.println("pixelsUV[][][0]/pixelsUV[][][1] pixelsXY[][][0]/pixelsXY[][][1]");
					for (int i=0;i<this.gIP[n].pixelsXY.length;i++){
    					System.out.println(n+":"+i+"  "+
    							this.gIP[n].pixelsUV[i][0]+"/"+
    							this.gIP[n].pixelsUV[1][1]+"  "+
    							IJ.d2s(this.gIP[n].pixelsXY[i][0], 2)+"/"+
    							IJ.d2s(this.gIP[n].pixelsXY[i][1], 2)
    					);
    				}
    			}
    		}
    		if (this.debugLevel>0) {
    			System.out.println("readAllGrids(), numImages="+numImages+", total number of grid nodes="+numOfGridNodes+", unused nodes "+numOfGridNodes_extra);
    		}
    		 // probably - do not need to verify that this.gIS is null - should do that anyway. UPDATE: no, now reading config file creates gIS
/*    		
    		if (this.gIS!=null){
				System.out.println("readAllGrids() 1: ");
    			for (int is=0;is<this.gIS.length;is++){
    				System.out.println("readAllGrids() 1: "+is+": tilt="+this.gIS[is].goniometerTilt+" axial="+this.gIS[is].goniometerAxial+" estimated="+this.gIS[is].orientationEstimated);
    			}
    		}
    		*/
    		buildImageSets(this.gIS!=null);
    		/*
    		if (this.gIS!=null){
				System.out.println("readAllGrids() 2: ");
    			for (int is=0;is<this.gIS.length;is++){
    				System.out.println("readAllGrids() 2: "+is+": tilt="+this.gIS[is].goniometerTilt+" axial="+this.gIS[is].goniometerAxial+" estimated="+this.gIS[is].orientationEstimated);
    			}
    		}
    		*/
        	return true;
        }
        /**
         * Sometimes "Process grid files" generates outlayers (by 0.1..5 pixels) TODO: find the bug
         * This program replaces the "bad" ones with predicted by 8 neighbors using 2-nd order interpolation
         * @param fPixels stack of pX,pY,target-U,target-V,contrast (some bad pixels have low contrast), red,green,blue
         * @param width grid width
         * @param tolerance maximal tolerated difference between the predicted by 8 neigbors and center pixels
         * @parame maxBadNeighb - maximal number of bad cells among 8 neighbors
         * @parame gebugLevel debug level
         * @return number of fixed nodes
         * Neighbors of bad pixels can be reported bad, so they have to be re-tried with the worst removed
         */
        public int fixBadGridNodes(
        		float [][] fpixels,
        		int width,
        		double tolerance,
        		int maxBadNeighb,
        		int debugLevel,
        		String dbgTitle){
        	int debugThreshold=3;
        	double tolerance2=tolerance*tolerance;
        	double tolerance2Final=10.0*tolerance2; // final pass - fix even if the surronding are not that good
        	int [][] dirs8=   {{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
        	int [] dirs8Index={1,width+1,width,width-1,-1,-width-1,-width,-width+1};
        	double [] diffs2=new double [fpixels[0].length];
        	int height=diffs2.length/width;
        	for (int i=0;i<diffs2.length;i++) diffs2[i]=-1.0; // no nodes
        	double [][][] data=new double [8][3][];
        	for (int i=0;i<data.length;i++){
        		data[i][0]=new double[2];
        		data[i][1]=new double[2];
        		data[i][2]=new double[1];
        	}
        	PolynomialApproximation polynomialApproximation=new PolynomialApproximation(0); // do not report linear
        	double maxDiff2=0.0;
        	for (int y=1; y<(height-1);y++) for (int x=1;x<(width-1);x++) {
        		int index=y*width+x;
        		if (fpixels[0][index]>=0.0){
        			int numNonZero=0;
        			for (int iDir=0;iDir<dirs8.length;iDir++){
        				int index1=index+dirs8[iDir][1]*width+dirs8[iDir][0];
        				data[iDir][0][0]=dirs8[iDir][0];
        				data[iDir][0][1]=dirs8[iDir][1];
        				data[iDir][1][0]=fpixels[0][index1];
        				data[iDir][1][1]=fpixels[1][index1];
        				if ((fpixels[0][index1]<0) || (fpixels[1][index1]<0)){
        					data[iDir][2][0]=0.0;
        				} else {
        					data[iDir][2][0]=1.0;
        					numNonZero++;
        				}
        			}
        			if (numNonZero<8) continue; // should all be defined
        			double [][] coeff=polynomialApproximation.quadraticApproximation(
        					data,
        					false); // boolean forceLinear  // use linear approximation
        			if (coeff!=null) {
        				if ((coeff[0].length<6) || (coeff[1].length<6)){
        					if (debugLevel>0){
            					System.out.println("fixBadGridNodes() linear interpolate for x="+x+", y="+y);
            					for (int j=0;j<data.length;j++){
            						System.out.println(j+" "+data[j][0][0]+"/"+data[j][0][1]+" - "+data[j][1][0]+"/"+data[j][1][1]+" : "+data[j][2][0]);
            					}
            				}
        				}
        				double dx=coeff[0][coeff[0].length-1] - fpixels[0][index];
        				double dy=coeff[1][coeff[1].length-1] - fpixels[1][index];
        				diffs2[index]=dx*dx+dy*dy;
        				if (diffs2[index]>maxDiff2) maxDiff2=diffs2[index];
        			} else {
        				if (debugLevel>0){
        					System.out.println("fixBadGridNodes() failed for x="+x+", y="+y);
        				}
        			}
        		}
        	}
        	if (maxDiff2<=tolerance2) return 0; // nothing to fix
        	// here - first debug show?
        	boolean [] localWorst=new boolean[diffs2.length];
        	int numBad=0;
        	for (int i=0;i<localWorst.length;i++){
        		if (diffs2[i]<tolerance2){
        			localWorst[i]=false;
        		} else {
        			localWorst[i]=true;
        			for (int iDir=0;iDir<dirs8Index.length;iDir++) if (diffs2[i+dirs8Index[iDir]] > diffs2[i]){
        				localWorst[i]=false;
        				break;
        			}
        			if (localWorst[i]) numBad++;
        		}
        	}
        	if (numBad==0) {
				System.out.println("fixBadGridNodes() BUG - should not get here.");
        		return 0; // should not get here - 
        	}
        	double [][] dbgData=null;
			if (debugLevel>debugThreshold){
				dbgData=new double[9][];
				dbgData[0]=diffs2.clone();
				dbgData[2]=dbgData[0].clone();
				for (int i=0;i< dbgData[2].length;i++) if (!localWorst[i]) dbgData[2][i]=-1.0; 
//				(new showDoubleFloatArrays()).showArrays(diffs2, width, height,  "diffs2");
			}        	
        	// Trying to eliminate all non local worst (may that is just extra as there anot too many bad nodes)
        	int numStillBad=0;
        	for (int i=0;i<localWorst.length;i++) if (localWorst[i]){
        		for (int iDir0=0;iDir0<dirs8Index.length;iDir0++) if (diffs2[i+dirs8Index[iDir0]] > tolerance2){ // don't bother with not-so-bad
        			int index=i+dirs8Index[iDir0]; // will never be on the border as diffs2 is <=0.0 there
        			int numNonZero=0;
        			for (int iDir=0;iDir<dirs8.length;iDir++){
        				int index1=index+dirs8[iDir][1]*width+dirs8[iDir][0];
        				data[iDir][0][0]=dirs8[iDir][0];
        				data[iDir][0][1]=dirs8[iDir][1];
        				data[iDir][1][0]=fpixels[0][index1];
        				data[iDir][1][1]=fpixels[1][index1];
        				if ((data[iDir][1][0]<0) || (data[iDir][1][1]<0) || localWorst[index1]){
            				data[iDir][2][0]=0.0;
        				} else {
        					data[iDir][2][0]=1.0;
        					numNonZero++;
        				}

        			}
    				if (debugLevel>3){
    					System.out.print("+++ fixBadGridNodes() trying to fix for x="+(index%width)+", y="+(index/width)+", iDir0="+iDir0+" numNonZero="+numNonZero+" maxBadNeighb="+maxBadNeighb);
    				}

        			if (numNonZero<(data.length-maxBadNeighb-1)) continue;
        			double [][] coeff=polynomialApproximation.quadraticApproximation(
        					data,
        					false); // boolean forceLinear  // use linear approximation
        			if (coeff!=null) {
        				double dx=coeff[0][coeff[0].length-1] - fpixels[0][index];
        				double dy=coeff[1][coeff[1].length-1] - fpixels[1][index];
        				if (debugLevel>3){
        					System.out.print("fixBadGridNodes() old diffs2["+index+"]="+diffs2[index]);
        				}
        				diffs2[index]=dx*dx+dy*dy; // updated value
        				if (debugLevel>3){
        					System.out.print(" new diffs2["+index+"]="+diffs2[index]);
        				}
        				if (diffs2[index]>tolerance2) {
        					numStillBad++;
            				if (debugLevel>3){
            					System.out.print(" --- BAD");
            				}
        				} else if (debugLevel>3){
        					System.out.print(" --- GOOD");
        				}
        				if ((coeff[0].length<6) || (coeff[1].length<6)){
        					if (debugLevel>3){
            					System.out.print("fixBadGridNodes() 2 linear interpolate for x="+(index%width)+", y="+(index/width));
            					for (int j=0;j<data.length;j++){
            						System.out.println(j+" "+data[j][0][0]+"/"+data[j][0][1]+" - "+data[j][1][0]+"/"+data[j][1][1]+" : "+data[j][2][0]);
            					}
            				}
        				}
        			} else {
        				if (debugLevel>3){
        					System.out.println("fixBadGridNodes() failed for x="+(index%width)+", y="+(index/width)+", iDir0="+iDir0);
        				}
        			}
        			if (debugLevel>3) System.out.println();
        		}
        	}
        	if (numStillBad>0){
        		if (debugLevel>3){
        			System.out.println("fixBadGridNodes(): numStillBad="+numStillBad+" > 0 - probably near the border, just make sure  OK.");
        		}
        	}
			if (debugLevel>debugThreshold){
				dbgData[1]=diffs2.clone();
				for (int i=0;i< dbgData[1].length;i++) if (localWorst[i]) dbgData[1][i]=0.0;
				dbgData[3]=new double[dbgData[0].length];
				for (int i=0;i< dbgData[3].length;i++)  dbgData[3][i]=0.0;
				dbgData[4]=dbgData[3].clone();
				dbgData[5]=dbgData[3].clone();
				dbgData[6]=dbgData[3].clone();
				dbgData[7]=dbgData[3].clone();
				dbgData[8]=dbgData[3].clone();
				for (int i=0;i< dbgData[3].length;i++)  {
					dbgData[3][i]=fpixels[0][i];
					dbgData[4][i]=fpixels[1][i];
			    }
			}        	

// TODO - try to fix some around pixels first?			
			
// Actually patching locally worst nodes
        	for (int index=0;index<localWorst.length;index++) if (localWorst[index]){
        		int numNonZero=0;
    			for (int iDir=0;iDir<dirs8.length;iDir++){
    				int index1=index+dirs8[iDir][1]*width+dirs8[iDir][0];
    				data[iDir][0][0]=dirs8[iDir][0];
    				data[iDir][0][1]=dirs8[iDir][1];
    				data[iDir][1][0]=fpixels[0][index1];
    				data[iDir][1][1]=fpixels[1][index1];
    				if (diffs2[index1]>tolerance2Final){ // increased tolerance for the final correction
    					data[iDir][2][0]=0.0; // do not count neighbors who are bad themselves
    				} else {
    					data[iDir][2][0]=1.0;
    					numNonZero++;
    				}
    			}
    			if (numNonZero<(data.length-maxBadNeighb)){
    				if (debugLevel>3){
    					System.out.println("fixBadGridNodes() failed x="+(index%width)+", y="+(index/width)+", number of good neighbors="+numNonZero);
    				}
    				continue; // do not fix anything
    			}
    			double [][] coeff=polynomialApproximation.quadraticApproximation(
    					data,
    					false); // boolean forceLinear  // use linear approximation
    			if (coeff!=null) {
    				if ((coeff[0].length<6) || (coeff[1].length<6)){
    					if (debugLevel>3){
        					System.out.println("fixBadGridNodes() linear interpolate for x="+(index%width)+", y="+(index/width));
        					for (int j=0;j<data.length;j++){
        						System.out.println(j+" "+data[j][0][0]+"/"+data[j][0][1]+" - "+data[j][1][0]+"/"+data[j][1][1]+" : "+data[j][2][0]);
        					}
        					for (int n=0;n<coeff.length;n++){
        						for (int j=0;j<coeff[n].length;j++){
        							System.out.print(coeff[n][j]+" ");
        						}
        						System.out.println();
        					}
        				}
    				} else if (debugLevel>3){
    					System.out.println("fixBadGridNodes() qudratic interpolate for x="+(index%width)+", y="+(index/width));
    					for (int j=0;j<data.length;j++){
    						System.out.println(j+" "+data[j][0][0]+"/"+data[j][0][1]+" - "+data[j][1][0]+"/"+data[j][1][1]+" : "+data[j][2][0]);
    					}
    					for (int n=0;n<coeff.length;n++){
    						for (int j=0;j<coeff[n].length;j++){
    							System.out.print(coeff[n][j]+" ");
    						}
    						System.out.println();
    					}
    					if (((index%width)==19) && ((index/width)==57)){
    						coeff=(new PolynomialApproximation(4)).quadraticApproximation(
    		    					data,
    		    					false);
    					}
    				}
    				fpixels[0][index]=(float) coeff[0][coeff[0].length-1];
    				fpixels[1][index]=(float) coeff[1][coeff[1].length-1];
    			} else {
    				if (debugLevel>3){
    					System.out.println("fixBadGridNodes() failed for x="+(index%width)+", y="+(index/width)+", last pass");
    				}
    			}
        	}
			if (debugLevel>debugThreshold){
				for (int i=0;i< dbgData[3].length;i++)  {
					dbgData[5][i]=fpixels[0][i];
					dbgData[6][i]=fpixels[1][i];
					dbgData[7][i]=dbgData[3][i]-fpixels[0][i];
					dbgData[8][i]=dbgData[4][i]-fpixels[1][i];
			    }
				 
				String [] dbgTitles={"diff20","diff2Mod","localWorst", "old-X", "old-Y", "new-X", "new-Y","old-new-X","old-new-Y"};
				if (dbgTitle!=null) (new showDoubleFloatArrays()).showArrays(dbgData, width, height, true,  dbgTitle, dbgTitles);
			}        	
        	return numBad;
        }
        
// TODO: Move all custom image properties (including encode/decode from JP4_reader_camera) to a separate class.
// below is a duplicatie from MatchSimulatedPattern        
        
        public double[][] getPointersXY(ImagePlus imp, int numPointers){
			   // read image info to properties (if it was not done yet - should it?
			   if ((imp.getProperty("timestamp")==null) || (((String) imp.getProperty("timestamp")).length()==0)) {
				   JP46_Reader_camera jp4_instance= new JP46_Reader_camera(false);
				   jp4_instance.decodeProperiesFromInfo(imp);
			   }
			   double [][] pointersXY=new double[numPointers][];
			   int numPointerDetected=0;
			   for (int i=0;i<pointersXY.length;i++) {
				   pointersXY[i]=null;
				   if ((imp.getProperty("POINTER_X_"+i)!=null) && (imp.getProperty("POINTER_Y_"+i)!=null)) {
					   pointersXY[i]=new double[2];
					   pointersXY[i][0]=Double.parseDouble((String) imp.getProperty("POINTER_X_"+i));
					   pointersXY[i][1]=Double.parseDouble((String) imp.getProperty("POINTER_Y_"+i));
					   numPointerDetected++;
				   }
			   }
			   if (numPointerDetected>0) return pointersXY;
			   else return null;
		   }
        
        public int [] getMotorPositions(ImagePlus imp, int numMotors){
        	// read image info to properties (if it was not done yet - should it?
        	if ((imp.getProperty("timestamp")==null) || (((String) imp.getProperty("timestamp")).length()==0)) {
        		JP46_Reader_camera jp4_instance= new JP46_Reader_camera(false);
        		jp4_instance.decodeProperiesFromInfo(imp);
        	}
        	int [] motorPos=new int [numMotors];
        	int numMotorsDetected=0;
        	for (int i=0;i<motorPos.length;i++) {
        		motorPos[i]=0;
        		if (imp.getProperty("MOTOR"+(i+1))!=null) {
        			motorPos[i]=Integer.parseInt((String) imp.getProperty("MOTOR"+(i+1)));
        			numMotorsDetected++;
        		}
        	}
        	if (numMotorsDetected>0) return motorPos;
        	else return null;
        }
        
        public int  getUsedPonters(ImagePlus imp){
        	// read image info to properties (if it was not done yet - should it?
        	if ((imp.getProperty("timestamp")==null) || (((String) imp.getProperty("timestamp")).length()==0)) {
        		JP46_Reader_camera jp4_instance= new JP46_Reader_camera(false);
        		jp4_instance.decodeProperiesFromInfo(imp);
        	}
        	if (imp.getProperty("USED_POINTERS")!=null) {
        		return Integer.parseInt((String) imp.getProperty("USED_POINTERS"));
        	}
        	return 0;
        }
        
        
        public int getImageNumPoints(int numImg){
        	return this.gIP[numImg].pixelsUV.length;
        }
       
        public void initPars(int numImages, int numPars) {
        	this.pars=new double [numImages][numPars];
        	for (int i=0;i<numImages;i++) for (int j=0;j<numPars;j++) this.pars[i][j]=Double.NaN;
        }
        public double getParameterValue(int numImg, int numPar){
        	if ((numImg<0) || (numImg>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+numImg;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if ((numPar<0) || (numPar>=this.pars[numImg].length)) {
        		String msg="There are only "+this.pars[numImg].length+" parameters defined, requested #"+numPar;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	double par=(this.gIP[numImg].gridImageSet!=null)?this.gIP[numImg].gridImageSet.getParameterValue(numPar):Double.NaN;
        	if (Double.isNaN(par)) par=this.pars[numImg][numPar];
        	return par;
        }
        public void setParameterValue(int numImg, int numPar, double value, boolean updateEstimated){
        	if ((numImg<0) || (numImg>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+numImg;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if ((numPar<0) || (numPar>=this.pars[numImg].length)) {
        		String msg="There are only "+this.pars[numImg].length+" parameters defined, requested #"+numPar;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	this.pars[numImg][numPar]=value;
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.setParameterValue(numPar,value,updateEstimated);
        }

        public void setParameters(double [] parameters, int numImg){
        	if ((numImg<0) || (numImg>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+numImg;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	this.pars[numImg]=parameters.clone();
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.updateSetFromParameterVector(parameters);
        }

        public int getParametersLength(int numImg){
        	return this.pars[numImg].length;
        }
        public double [] getParameters(int numImg){
        	if ((numImg<0) || (numImg>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+numImg;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	double [] parameters=this.pars[numImg].clone();
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.updateParameterVectorFromSet(parameters);
        	return parameters;
        }
        public double [] getAzEl(int imgNum){ // get sensor azimuth and elevation DANGEROUS - absolute indices of parameters
        	if ((imgNum<0) || (imgNum>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+imgNum;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	double [] azel={this.pars[imgNum][0],this.pars[imgNum][4]};
        	return azel;
        }
        // set goniometer horizontal axis angle and goniometer axial angles in all images 
        public void setGHGA(double gh, double ga){
        	for (int imgNum=0;imgNum<this.pars.length;imgNum++) setGHGA( imgNum, gh,ga);
        }
        public void setGHGA(int imgNum, double gh, double ga){
        	setGH(imgNum, gh);
        	setGA(imgNum, ga);
        }
        public void setGH(int numImg, double gh){
        	this.pars[numImg][6]=gh;
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.goniometerTilt= gh;
        }
        public void setGA(int numImg,  double ga){
        	this.pars[numImg][7]=ga;
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.goniometerAxial= ga;
        }
        public double getGH(int numImg){
        	if (this.gIP[numImg].gridImageSet!=null) return this.gIP[numImg].gridImageSet.goniometerTilt;
        	return this.pars[numImg][6];
        }
        
        public double getGA(int numImg){
        	if (this.gIP[numImg].gridImageSet!=null) return this.gIP[numImg].gridImageSet.goniometerAxial;
        	return this.pars[numImg][7];
        }
        
        public void setParameters(double [] parameters, int numImg, boolean[] mask){
        	if ((numImg<0) || (numImg>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+numImg;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if ((this.pars[numImg].length!=parameters.length) || (this.pars[numImg].length!=mask.length)) {
        		String msg="Vector lengths for image #"+numImg+
        		" mismatch: this.pars["+numImg+"].length="+this.pars[numImg].length+
        		" parameters.length="+parameters.length+
        		" mask.length="+mask.length;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	for (int i=0;i<mask.length;i++) if (mask[i])this.pars[numImg][i]=parameters[i];
        	if (this.gIP[numImg].gridImageSet!=null) this.gIP[numImg].gridImageSet.updateSetFromParameterVector(parameters,mask);

        }
        
        public void setIntrinsicParameters(double [] parameters, int num){
        	if ((num<0) || (num>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (this.pars[num].length!=parameters.length) {
        		String msg="Vector lengths for image #"+num+
        		" mismatch: this.pars["+num+"].length="+this.pars[num].length+
        		" parameters.length="+parameters.length;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	for (int i=0;i<parameters.length;i++) if (isIntrinsicParameter(i))this.pars[num][i]=parameters[i];
        	// no need to update image sets
        }
        public void setSubcameraParameters(double [] parameters, int num){
        	if ((num<0) || (num>=this.pars.length)) {
        		String msg="There are only "+this.pars.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (this.pars[num].length!=parameters.length) {
        		String msg="Vector lengths for image #"+num+
        		" mismatch: this.pars["+num+"].length="+this.pars[num].length+
        		" parameters.length="+parameters.length;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	for (int i=0;i<parameters.length;i++) if (isSubcameraParameter(i))this.pars[num][i]=parameters[i];
        	// no need to update image sets
        }
        
        
        public String getParameterName(int num){
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return parameterDescriptions[num][0];
        	
        }
        public String getParameterDescription(int num){
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.parameterDescriptions[num][1];
        	
        }
        public String getParameterUnits(int num){
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.parameterDescriptions[num][2];
        	
        }
        public boolean isSubcameraParameter(int num){
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return (this.parameterDescriptions[num][3].equals("S"));
        	
        }
        public boolean isLocationParameter(int num){ //X,Y or Z location of the camera
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return (this.parameterDescriptions[num][3].equals("T"));
        }

        public boolean isOrientationParameter(int num){ //one of the 2 goniometer orientation angles
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return (this.parameterDescriptions[num][3].equals("R"));
        }

        public boolean isIntrinsicParameter(int num){ // updated from image calibration file
        	if ((num<0) || (num>=this.parameterDescriptions.length)) {
        		String msg="There are only "+this.parameterDescriptions.length+" parameters defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return (this.parameterDescriptions[num][4].equals("I"));
        	
        }
        public String getImagePath(int num) {
        	if ((num<0) || (num>=this.gIP.length)) {
        		String msg="There are only "+this.gIP.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gIP[num].path;
        }
        public int getImageSubcamera(int num) {
        	if ((num<0) || (num>=this.gIP.length)) {
        		String msg="There are only "+this.gIP.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gIP[num].channel;
        }
        public int getImageStation(int num) {
        	if ((num<0) || (num>=this.gIP.length)) {
        		String msg="There are only "+this.gIP.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gIP[num].getStationNumber();
        }
        public double getImageTimestamp(int num) {
        	if ((num<0) || (num>=this.gIP.length)) {
        		String msg="There are only "+this.gIP.length+" images defined, requested #"+num;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	return this.gIP[num].timestamp;
        }
        public int getNumImages() {
        	return this.gIP.length;
        }
        public int getNumParameters() {
        	return this.parameterDescriptions.length;
        }
        public int getNumSubCameras() {
        	return this.numSubCameras;
        }
        /**
         * 
         * @param imgNumber number of grid image to edit parameters (location, distortion) for
         * @return <2 - canceled, -1 - done, els - number of the next image to edit
         */
        public int editImageParameters(int imgNumber){
        	if ((this.gIP==null) || (imgNumber<0) ||(imgNumber>=this.gIP.length)) return -3;
			int sub=getImageSubcamera(imgNumber);
       	    String sTS=IJ.d2s(getImageTimestamp(imgNumber),6);
    		GenericDialog gd = new GenericDialog("Manually editing per-image parameters, timestamp="+sTS+
    				", subchannel-"+sub+" "+getImagePath(imgNumber));
    	    for (int i=0;i<getNumParameters();i++){
    	    	gd.addNumericField(
    	    			i+": "+getParameterDescription(i)+"["+ getParameterName(i)+"] "+
    	    			(isSubcameraParameter(i)?"S ":"  "),
//    	    			this.pars[imgNumber][i],5,10, getParameterUnits(i));
    	    			this.getParameterValue(imgNumber,i),5,10, getParameterUnits(i));
    	    }
    	    gd.addNumericField("Next image to edit (0.."+this.pars.length+", -1 - none) ", imgNumber+1,0);
   	        gd.enableYesNoCancel("OK", "Done");
    	    WindowTools.addScrollBars(gd);
    	    gd.showDialog();
    	    if (gd.wasCanceled()) return -2;
    	    for (int i=0;i<getNumParameters();i++){
//    	    	this.pars[imgNumber][i]= gd.getNextNumber();
    	    	this.setParameterValue(imgNumber,i, gd.getNextNumber(),true);
    	    }
    	    imgNumber= (int) gd.getNextNumber();
    	    if ((imgNumber<0) || (imgNumber>=getNumImages())) return -1;
    		if (!gd.wasOKed()) return -1; // pressed Done (no need to ask for the next number)
            return imgNumber;
        }
        public void setMaskFromImageStack(String path){
    		Opener opener=new Opener();
			if (this.debugLevel>1) System.out.println("Opening "+path+" as a stack of sensor masks");
			ImagePlus imp=opener.openImage("", path);
        	if (imp==null) {
        		String msg="Failed to read sensors mask file "+path;
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	(new JP46_Reader_camera(false)).decodeProperiesFromInfo(imp);
        	if (imp.getProperty("shrinkGridForMask")!=null)
        		eyesisCameraParameters.shrinkGridForMask=Integer.parseInt((String) imp.getProperty("shrinkGridForMask"));
            	if (imp.getProperty("maskBlurSigma")!=null)
            		eyesisCameraParameters.maskBlurSigma=Double.parseDouble((String) imp.getProperty("maskBlurSigma"));
            	if (imp.getProperty("decimateMasks")!=null)
            		eyesisCameraParameters.decimateMasks=Integer.parseInt((String) imp.getProperty("decimateMasks"));
            	if (imp.getProperty("sensorWidth")!=null)
            		eyesisCameraParameters.sensorWidth=Integer.parseInt((String) imp.getProperty("sensorWidth"));
            	if (imp.getProperty("sensorHeight")!=null)
            		eyesisCameraParameters.sensorHeight=Integer.parseInt((String) imp.getProperty("sensorHeight"));
        	setMaskFromImageStack(imp);
        }
        /**
         * Find number of channels in this camera
         * @return maximal number of channel used plus one
         */
        public int getNumChannels(){
        	int nChn=-1;
        	for (int i=0;i<this.gIP.length;i++) if (this.gIP[i].channel>nChn) nChn=this.gIP[i].channel;
        	return nChn+1;
        }
        
        public double getMask(int chnNum, double px, double py){
        	int width= eyesisCameraParameters.sensorWidth/eyesisCameraParameters.decimateMasks;
        	int height=eyesisCameraParameters.sensorHeight/eyesisCameraParameters.decimateMasks;
        	int iPX= ((int) Math.round(px))/eyesisCameraParameters.decimateMasks;
        	int iPY= ((int) Math.round(py))/eyesisCameraParameters.decimateMasks;
        	if ((iPX<0) || (iPY<0) || (iPX>=width) || (iPY>=height)) return 0.0;
        	if ((this.sensorMasks==null) || (this.sensorMasks[chnNum]==null)) return 1.0;
        	return this.sensorMasks[chnNum][iPY*width+iPX];
        }
        public double getMask(double[] mask, double px, double py){
        	if (mask==null) return 0;
        	int width= eyesisCameraParameters.sensorWidth/eyesisCameraParameters.decimateMasks;
        	int height=eyesisCameraParameters.sensorHeight/eyesisCameraParameters.decimateMasks;
        	int iPX= ((int) Math.round(px))/eyesisCameraParameters.decimateMasks;
        	int iPY= ((int) Math.round(py))/eyesisCameraParameters.decimateMasks;
        	if ((iPX<0) || (iPY<0) || (iPX>=width) || (iPY>=height)) return 0.0;
        	return mask[iPY*width+iPX];  // null ponter
        }
        
        
        public void setMaskFromImageStack(ImagePlus imp){
        	if (imp == null){
        		String msg="sensors mask image is null";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	if (imp.getProperty("decimateMasks")!=null)
        		eyesisCameraParameters.decimateMasks=Integer.parseInt((String) imp.getProperty("decimateMasks"));
        	eyesisCameraParameters.sensorWidth= imp.getWidth()*eyesisCameraParameters.decimateMasks;
        	eyesisCameraParameters.sensorHeight=imp.getHeight()*eyesisCameraParameters.decimateMasks;
        	if (imp.getProperty("sensorWidth")!=null)
        		eyesisCameraParameters.sensorWidth=Integer.parseInt((String) imp.getProperty("sensorWidth"));
        	if (imp.getProperty("sensorHeight")!=null)
        		eyesisCameraParameters.sensorHeight=Integer.parseInt((String) imp.getProperty("sensorHeight"));
        	
    		if (this.sensorMasks==null) {
    			this.sensorMasks=new double[getNumChannels()][];
    			for (int i=0;i<this.sensorMasks.length;i++) this.sensorMasks[i]=null;
    		}
    		int numChannels=imp.getStackSize();
    		float [][] pixels =new float[numChannels][];
    		if (numChannels==1){
    			pixels[0]=(float[]) imp.getProcessor().getPixels();
    		} else {
        		ImageStack stack = imp.getStack();
            	if (stack==null) {
            		String msg="Expected a image stack with masks";
            		IJ.showMessage("Error",msg);
            		throw new IllegalArgumentException (msg);
            	}
            	for (int i=0;i<numChannels;i++) pixels[i]= (float[]) stack.getPixels(i+1);
    		}
    		for (int numChn=0;(numChn<numChannels) && (numChn<this.sensorMasks.length);numChn++){
    			//Make shure masks contain non-zero (>0.0) pixels, otherwise skip those
        		boolean defined=false;
        		for (int i=0;i<pixels[numChn].length;i++) if (pixels[numChn][i]>0.0){
        			defined=true;
        			break;
        		}
    			if (defined) {
    				this.sensorMasks[numChn]=new double [pixels[numChn].length];
    				for (int i=0;i<this.sensorMasks[numChn].length;i++) this.sensorMasks[numChn][i]=pixels[numChn][i];
    			}
    		}
        }
        
        public ImagePlus saveMaskAsImageStack(String title, String path){
        	ImagePlus imp=getMaskAsImageStack(title);
        	if (imp==null) return null;
	   				FileSaver fs=new FileSaver(imp);
	   				if (updateStatus) IJ.showStatus("Saving masks "+path);
	   				if (this.debugLevel>0) System.out.println("Saving masks "+path);
	   				if (imp.getStackSize()>1)
	   					fs.saveAsTiffStack(path);
	   				else
	   					fs.saveAsTiff(path);
        	return imp;
        }

        public ImagePlus getMaskAsImageStack(String title){
        	if (this.sensorMasks==null){
        		String msg="Sensor mask array does not exist, nothing to convert";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
        	int width= eyesisCameraParameters.sensorWidth/eyesisCameraParameters.decimateMasks;
        	int height=eyesisCameraParameters.sensorHeight/eyesisCameraParameters.decimateMasks;
        	float [][]pixels=new float [getNumChannels()][width*height];
        	ImagePlus imp=null;
        	for (int numChn=0;numChn<getNumChannels();numChn++){
        		if (this.sensorMasks[numChn]==null) for (int i=0;i<pixels[numChn].length;i++)pixels[numChn][i]=0.0F;
        		else for (int i=0;i<pixels[numChn].length;i++)pixels[numChn][i]=(float) this.sensorMasks[numChn][i];
        	}
        	if (this.sensorMasks.length>0){
        		ImageStack stack=new ImageStack(width,height);
        		for (int numChn=0;numChn<pixels.length;numChn++)  stack.addSlice("chn-"+numChn,    pixels[numChn]);
        		imp = new ImagePlus(title, stack);
        	} else {
        		ImageProcessor  ip =new FloatProcessor(width,height);
        		ip.setPixels(pixels[0]);
        		imp=new ImagePlus(title, ip);
        	}
// TODO: add more properties here (MAC+channel)? preserve other properties?        	
        	imp.setProperty("sensorWidth", ""+eyesisCameraParameters.sensorWidth);
        	imp.setProperty("sensorHeight", ""+eyesisCameraParameters.sensorHeight);
        	imp.setProperty("shrinkGridForMask", ""+eyesisCameraParameters.shrinkGridForMask);
        	imp.setProperty("maskBlurSigma", ""+eyesisCameraParameters.maskBlurSigma);
        	imp.setProperty("decimateMasks", ""+eyesisCameraParameters.decimateMasks);
        	
        	(new JP46_Reader_camera(false)).encodeProperiesToInfo(imp);
        	imp.getProcessor().resetMinAndMax();
        	return imp;
        }
        /**
         * Generate low-vignetting sensor mask for flat-field calculation
         * @param sensorMask sensor mask, decimated array
         * @param width  sensor width, pixels
         * @param height sensor height, pixels
         * @param shrink shrink sensor mask by this amount (sensor, non-decimated pixels)
         * @param radius radial mask - zero if farther than radius, 0.5*(cos(pi*r/radius)+1.0) if less 
         * @param minimalAlpha - zero mask below this threshold 
         * @return returns arrray with the same size as sensorMask that corresponds to low-vignetting areas of the sensor/lens 
         */

        public double [] nonVignettedMask(
        		double [] sensorMask,
        		int width,
        		int height,
        		double x0,     // lens center X (sensor, non-decimated pix)
        		double y0,     // lens center Y (sensor, non-decimated pix)
        		double shrink,
        		double radius,
        		double minimalAlpha){

        	int decimate= (int) Math.round(Math.sqrt(width*height/sensorMask.length));
        	int dcmWidth= width/decimate;
        	int dcmHeight=height/decimate;
        	double [] mask= sensorMask.clone();
        	if (shrink>0){
        		(new DoubleGaussianBlur() ).blurDouble(mask, dcmWidth, dcmHeight, shrink/decimate, shrink/decimate, 0.01);
        		for (int i=0;i<mask.length;i++){
        			double d=2*(mask[i]-0.5);
        			mask[i]=(d>0)?(d*d):(0.0);
        		}
        	}
        	if (radius>0.0){
        		int index=0;
        		for (int iy=0; iy<dcmHeight;iy++) for (int ix=0; ix<dcmWidth;ix++){
        			double r=Math.sqrt((iy*decimate-y0)*(iy*decimate-y0)+(ix*decimate-x0)*(ix*decimate-x0))/radius;
        			double k=(r>1.0)?0.0:(0.5*(Math.cos(Math.PI*r)+1.0));
        			mask[index++]*=k;
        		}
        	}
        	if (minimalAlpha>0.0) for (int i=0;i<mask.length;i++) if (mask[i]<minimalAlpha) mask[i]=0.0;
        	return mask;
        }
        
        public double [][] calculateSensorMasks() {
        	return calculateSensorMasks(
        			eyesisCameraParameters.decimateMasks,
        			eyesisCameraParameters.sensorWidth,
        			eyesisCameraParameters.sensorHeight,
        			eyesisCameraParameters.shrinkGridForMask,
        			eyesisCameraParameters.maskBlurSigma);
        }
        /**
         * 
         * @param width image width, in pixels (pixel X coordinates are between 0 and width-1, inclusive)
         * @param height image height, in pixels (pixel Y coordinates are between 0 and height-1, inclusive)
         * @param shrinkGridForMask shrink detected grids by this number of nodes in each direction before bluring
         * @param sigmaUV Gaussian sigma fro bluring of the sensor mask (if negative - in grid inter-node distances)
         * @return array of pixel arrays (or nulls) for each camera subchannel (also keeps it in the class instance)
         */
        public double [][] calculateSensorMasks( int decimate, int width, int height, int shrinkGridForMask, double sigmaUV) {
        	int dWidth=  (width -1)/decimate+1;
        	int dHeight= (height-1)/decimate+1;
        	int numChannels=getNumChannels();
        	this.sensorMasks=new double [numChannels][];
        	DoubleGaussianBlur gb=new DoubleGaussianBlur();
        	if ((this.debugLevel>1) && (SDFA_INSTANCE==null)) SDFA_INSTANCE=new showDoubleFloatArrays();
			if (this.debugLevel>2)System.out.println("calculateSensorMasks("+width+","+height+","+shrinkGridForMask+","+sigmaUV+")");
        	for (int chNum=0;chNum<numChannels; chNum++){
        		this.sensorMasks[chNum]=new double[dWidth*dHeight];
        		for (int i=0;i<this.sensorMasks[chNum].length;i++) this.sensorMasks[chNum][i]=0.0;
        		double rAverage=0.0;
        		double rAverageNum=0.0;
        		for (int imgNum=0;imgNum<this.gIP.length;imgNum++) if (this.gIP[imgNum].channel==chNum){ // image is for this this channel
        	        double [][] preMask=preCalculateSingleImageMask(imgNum, decimate, width, height, shrinkGridForMask);
        	        if (preMask==null) continue; //nothing in this channel
            		rAverage+=preMask[0][0];
            		rAverageNum+=preMask[0][1];
       			    for (int i=0;i<this.sensorMasks[chNum].length;i++) if (preMask[1][i]>0.0) this.sensorMasks[chNum][i]=1.0;
        		}
        		if (rAverageNum==0.0) continue; // nothing to blur/process for this channel
        		rAverage/=rAverageNum; // average distance to the fartherst node from the current
        		double      sigma=sigmaUV;
        		if(sigma<0) sigma*=-rAverage;
        		gb.blurDouble(this.sensorMasks[chNum], dWidth, dHeight, sigma/decimate, sigma/decimate, 0.01);

        		//    this.sensorMasks[chNum] now contains 0.0/1.0 mask. Blur it
        		//	    		gb.blurDouble(pointedBayer[bayerR], halfWidth, halfHeight, this.lowpassSigma, this.lowpassSigma, 0.01);
        		//	    		if (debugLevel>2) sdfra_instance.showArrays(pointedBayer[bayerR].clone(), halfWidth, halfHeight, title+"-smooth");

        	}
        	return this.sensorMasks;
        }
        public double [] calculateImageGridMask(int imgNum) {
        	return calculateImageGridMask(
        			imgNum,
        			eyesisCameraParameters.decimateMasks,
        			eyesisCameraParameters.sensorWidth,
        			eyesisCameraParameters.sensorHeight,
        			eyesisCameraParameters.shrinkGridForMask,
        			eyesisCameraParameters.maskBlurSigma);
        }

        public double [] calculateImageGridMask(int imgNum, int decimate, int width, int height, int shrinkGridForMask, double sigmaUV) {
        	int dWidth=  (width -1)/decimate+1;
        	int dHeight= (height-1)/decimate+1;
        	DoubleGaussianBlur gb=new DoubleGaussianBlur();
        	if ((this.debugLevel>1) && (SDFA_INSTANCE==null)) SDFA_INSTANCE=new showDoubleFloatArrays();
        	if (this.debugLevel>2)System.out.println("calculateSensorMasks("+width+","+height+","+shrinkGridForMask+","+sigmaUV+")");
        	double [][] preMask=preCalculateSingleImageMask(imgNum, decimate, width, height, shrinkGridForMask);
        	if (preMask==null) return null; //nothing in this channel
        	double rAverage=preMask[0][0];
        	double rAverageNum=preMask[0][1];
        	if (rAverageNum==0.0) return null; // nothing to blur/process for this channel
        	rAverage/=rAverageNum; // average distance to the fartherst node from the current
        	double      sigma=sigmaUV;
        	if(sigma<0) sigma*=-rAverage;
// old version, trying new - will influence all sensor masks!!        	
//        	gb.blurDouble(preMask[1], dWidth, dHeight, sigma/decimate, sigma/decimate, 0.01);
        	double [] mask0=preMask[1].clone();
        	gb.blurDouble(preMask[1], dWidth, dHeight, sigma/decimate, sigma/decimate, 0.01);
        	for (int i=0;i<preMask[1].length;i++){
				double d=2.0*(preMask[1][i]-0.5);
				preMask[1][i]=((mask0[i]>0) && (d>0))?(d*d):0.0;
        	}
        	return preMask[1];
        }
        
        
        
        /**
         * 
         * @param imgNum number of image to process
         * @param decimate - reduce image resolution for the mask
         * @param width - image width (actual will be divided by decimate
         * @param height- image height (actual will be divided by decimate
         * @param shrinkGridForMask shrink defined grid before bluring
         * @return array of 2 rows - [0] has just rAverage and rAverageNum for the average radius of the grid [1] - mask (1.0/0.0)
         *         or null if there are no grid nodes at all;
         */
        public double [][] preCalculateSingleImageMask(
        		int imgNum,
        		int decimate,
        		int width,
        		int height,
        		int shrinkGridForMask){
        	if (!this.gIP[imgNum].enabled) return null; // this image is disabled, ignore it
        	int [][] dirs={{-1,0},{1,0},{0,-1},{0,1}};
        	double rAverage=0.0;
        	double rAverageNum=0.0;
        	int i;
        	int dWidth=  (width -1)/decimate+1;
        	int dHeight= (height-1)/decimate+1;
        	double [] mask = new double [dWidth*dHeight];
        	boolean hasGrid=false;
        	for (i=0;i<this.gIP[imgNum].pixelsXY.length;i++) if ((this.gIP[imgNum].pixelsXY[i]!=null) &&(this.gIP[imgNum].pixelsXY[i][0]>=0)) {
        		hasGrid=true;
        		break;
        	}
        	if (!hasGrid) return null; // image has no grid nodes
        	int minU=this.gIP[imgNum].pixelsUV[i][0];
        	int minV=this.gIP[imgNum].pixelsUV[i][1];
        	int maxU=minU;
        	int maxV=minV;
        	for (;i<this.gIP[imgNum].pixelsXY.length;i++) if ((this.gIP[imgNum].pixelsXY[i]!=null) &&(this.gIP[imgNum].pixelsXY[i][0]>=0)) {
        		if (this.gIP[imgNum].pixelsUV[i][0]<minU) minU=this.gIP[imgNum].pixelsUV[i][0];
        		if (this.gIP[imgNum].pixelsUV[i][1]<minV) minV=this.gIP[imgNum].pixelsUV[i][1];
        		if (this.gIP[imgNum].pixelsUV[i][0]>maxU) maxU=this.gIP[imgNum].pixelsUV[i][0];
        		if (this.gIP[imgNum].pixelsUV[i][1]>maxV) maxV=this.gIP[imgNum].pixelsUV[i][1];
        	}
        	if (this.debugLevel>2)System.out.println("calculateSensorMasks, imgNum="+imgNum+", minU="+minU+", maxU="+maxU+", minV="+minV+", maxV="+maxV);
        	// restore the grid rectangle for u,v ->pixel-x,pixel-y
        	double [][][] pXY=new double[maxV-minV+1][maxU-minU+1][2];
        	int [][]iMask=new int [pXY.length][pXY[0].length];
        	for (int v=0;v<pXY.length;v++) for (int u=0;u<pXY[0].length;u++) {
        		pXY[v][u][0]=-1;
        		iMask[v][u]=0;
        	}
        	if (this.debugLevel>2)System.out.println("calculateSensorMasks, pXY.length="+pXY.length+", pXY[0].length="+pXY[0].length);
        	for (i=0;i<this.gIP[imgNum].pixelsXY.length;i++) if ((this.gIP[imgNum].pixelsXY!=null) &&(this.gIP[imgNum].pixelsXY[i][0]>=0)) {
        		int v=this.gIP[imgNum].pixelsUV[i][1]-minV;
        		int u=this.gIP[imgNum].pixelsUV[i][0]-minU;
        		pXY[v][u][0]=this.gIP[imgNum].pixelsXY[i][0]; // out of bounds 22
        		pXY[v][u][1]=this.gIP[imgNum].pixelsXY[i][1];
        		//    			if (this.debugLevel>2)System.out.println("calculateSensorMasks, i="+i+", pXY["+v+"]["+u+"]={"+pXY[v][u][0]+","+pXY[v][u][1]+"}");
        		iMask[v][u]=1;
        	}
        	if (this.debugLevel>3){
        		double [][] testArray=new double[3][pXY.length*pXY[0].length];
        		int index=0;
        		for (int v=0;v<iMask.length;v++) for (int u=0;u<iMask[0].length;u++){
        			testArray[0][index]=pXY[v][u][0];	
        			testArray[1][index]=pXY[v][u][1];	
        			testArray[2][index++]=iMask[v][u];

        		}
        		String [] dbgTitles={"X","Y","iMask"};
        		this.SDFA_INSTANCE.showArrays(testArray, pXY[0].length, pXY.length,  true, "original", dbgTitles);

        	}
        	// shrink the grid
        	int vMax=iMask.length-1;
        	int uMax=iMask[0].length-1;
        	if (this.debugLevel>2)System.out.println("calculateSensorMasks, uMax="+uMax+", vMax="+vMax);
        	for (int n=0;n<shrinkGridForMask;n++) {
        		for (int v=0;v<iMask.length;v++) for (int u=0;u<iMask[0].length;u++) if (iMask[v][u]>0){

        			if ((v==0) || (v==vMax) || (u==0) || (u==uMax) ||
        					(iMask[v-1][u]==-n) || (iMask[v+1][u]==-n) ||(iMask[v][u-1]==-n) || (iMask[v][u+1]==-n)) {
        				iMask[v][u]=-n-1;
        			}
        		}
        	}
        	if (this.debugLevel>3){
        		double [][] testArray1=new double[3][pXY.length*pXY[0].length];
        		int index=0;
        		for (int v=0;v<iMask.length;v++) for (int u=0;u<iMask[0].length;u++){
        			testArray1[0][index]=pXY[v][u][0];	
        			testArray1[1][index]=pXY[v][u][1];	
        			testArray1[2][index++]=iMask[v][u];

        		}
        		String [] dbgTitles={"X","Y","iMask"};
        		this.SDFA_INSTANCE.showArrays(testArray1, pXY[0].length, pXY.length,  true, "shrank", dbgTitles);
        	}

        	// now in remaining grid nodes iMask[v][u]>0 (0 and negative - no grid)
        	// accumulate pixels around the grid points
        	for (int v=0;v<iMask.length;v++) for (int u=0;u<iMask[0].length;u++)if (iMask[v][u]>0){
        		// find the radius - distance to the fartherst of the 4 (existent) neighbors (if none exist - disregard the node)
        		double r2Max=0;
        		for (int d=0;d<dirs.length;d++){
        			int u1=u+dirs[d][0];
        			int v1=v+dirs[d][1];
        			double r2;
        			if ((v1>=0) && (v1<=vMax) && (u1>=0) && (u1<=uMax)){
        				r2=(pXY[v1][u][0]-pXY[v][u][0])*(pXY[v1][u][0]-pXY[v][u][0])+
        				(pXY[v][u1][0]-pXY[v][u][0])*(pXY[v][u1][0]-pXY[v][u][0]);
        				if (r2Max<r2) r2Max=r2;
        			}
        		}
        		if (r2Max==0.0) continue; // nothing around - skip this node
        		// calculate average radius (for bluring)
        		double r=Math.sqrt(r2Max);
        		rAverage+=r;
        		rAverageNum++; 

        		int iR= (int) Math.round(r);
        		int iX0= (int) Math.round (pXY[v][u][0]);
        		int iY0= (int) Math.round (pXY[v][u][1]);
        		int xLowLim=iX0-iR;
        		int xHighLim=iX0+iR;
        		int yLowLim=iY0-iR;
        		int yHighLim=iY0+iR;
        		if (xLowLim<0)       xLowLim=0;
// decimation apply below        		
        		if (xHighLim>=width) xHighLim=width-1;
        		if (yLowLim<0)       yLowLim=0;
        		if (yHighLim>=height)yHighLim=height-1;
        		for (int iY=yLowLim;iY<=yHighLim;iY+=decimate)for (int iX=xLowLim;iX<=xHighLim;iX+=decimate){
        			double r2=(iX-pXY[v][u][0])*(iX-pXY[v][u][0])+(iY-pXY[v][u][1])*(iY-pXY[v][u][1]);
        			if (r2<=r2Max) {
        				if (decimate==1) mask[iY*width+iX]=1.0;
        				else mask[(iY/decimate)*dWidth+(iX/decimate)]=1.0;
        			}
        		}
        	}
        	double [][] result= {{rAverage,rAverageNum},mask};
        	return  result;

        }
        
    }
     
    /**
     * 
     * Specifies images to process and parameters to adjust
     * Each parameter cab be:
     * 0 - "fixed" - use individual, per-image parameters, do not modify them
     * 1 - "common" - parameters are common for all selected images.
     *                When saving - save to all selected images.
     *                When loading (if different) use "master image" (or closest to it in time)
     * 2 - "super common" - same as common, but save to all images, not just selected
     * 3 - "individual"
     * 4 - "per group"
     * 5 - "per-station"
     * 6 - "per-station" save to all (super)
     * 7 - "weak common" - like common, but enable small individual variations (for a price) - not yet implemented, will have separate weight fixed/floating
     * 8 - "weak station" - like per-station, but enable individual (for a price)
     * 
+====================+===========+===========+
|                    |  Same TS  |  Diff TS  |
|                    +-----+-----+-----+-----+
|                    |  C  |  I  |  C  |  I  |
+============+=======+=====+=====+=====+=====+
| Same       |Eyesis |  X  |  X  |  C  |  I  |
| Subcamera  +-------+-----+-----+-----+-----+
|            |Subcam |  X  |  X  |  C  |  I* |
+============+=======+=====+=====+=====+=====+
| Different  |Eyesis |  C  |  C  |  C  |  I  |
| Subcameras +-------+-----+-----+-----+-----+
|            |Subcam |  I  |  I  |  I  |  I  |
+============+=======+=====+=====+=====+=====+
I* - special case when the subcamera is being adjusted/replaced. How to deal with it?

     *
     */
    public static class FittingStrategy{
    	public String pathName=null; // path to XML file this instance was created from
    	public DistortionCalibrationData distortionCalibrationData=null;// per-image parameters
    	private boolean [][] selectedImages=null; // images selected for each step (will be masked with enabled images)
    	private boolean [][] selectedValidImages=null; // images selected for each step same as selected, but only if number of weight>0.0 nodes > threshold
    	public boolean [] stopAfterThis=null;
    	final public int modeFixed=0,modeCommon=1,modeSupercommon=2,modeIndividual=3,modeGroup=4;
    	final public int modeStation=5,modeSuperStation=6,modeWeakCommon=7,modeWeakStation=8,modeTiltEqualize=9;
    	public String[] definedModes={
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all",     // modeSuperStation=6
    			"weak common, save to  all",     // modeWeakCommon=7
    			"weak per-station, save to  all" // modeWeakStation=8
    			};
    	public String[] definedModesNoWeak={     // show for parameters that can not use weak
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all"      // modeSuperStation=6
//    			"weak common",              // modeWeakCommon=7
//    			"weak per-station",         // modeWeakStation=8
    			};
    	public String[] definedModesTiltEq={      // show for parameters that can not use weak
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all",     // modeSuperStation=6
    			"weak common, save to  all",     // modeWeakCommon=7
    			"weak per-station, save to  all",// modeWeakStation=8
    			"tilt equalize"                 // modeTiltEqualize
    			};
    	public String[] definedModesAll= definedModesTiltEq;
    	public int [][] parameterMode=null; // per series, per-parameter
    	public int [][][] parameterGroups=null; // per series, per-parameter - null or array of group numbers (1 element per image)
    	public int [][] zGroups=null;
    	public boolean saveUnusedGroups=false; // purge groups for parameters when saving to XML, preserve if true 
    	public double [] lambdas=null;   // LMA initial lambda for each step
    	public double defaultLambda=0.001;
    	public int [][] parameterList=null; // list of all parameters in the system, each has subcamera number and parameters index
    	public boolean [] parameterEnable=null; // select which parameters (of some 320 to display in selector)
    	public int [] masterImages=null;        // number of image no take "common" parameters from if they are different
    	public double defaultStepDone=1.0E-6;
    	public double [] stepDone=null;// delta_error to error ratio to consider series finished
//    	public double [] parameterVector;
    	public int [][] parameterMap=null;// ** valid for one strategy sequence *** for each parameter vector element hold image number/parameter number
    	public int [][] reverseParameterMap=null; // reversed map - for each [imageNumber][parameterNumber] -> vector index (-1 fixed)
    	public int currentSeriesNumber=-1;     // currently selected strategy step (for which parameterMap and reverseParameterMap)
    	public int debugLevel=2;
    	public int [] varianceModes=null;      // per-series: 0 - disabled, 1 - constant imageSets weight, 2 - variable imageSets weight
    	final public int varianceModeDisabled=0,varianceModeSameWeight=1,varianceModeVariableWeight=2;
    	public String [] definedVarianceModes={
    			"disabled",
    			"same weight",
    			"variable weight",
    	};
    	
    	// next arrays will be initialized at buildVariancesMaps only if at least some parameters use variances, otherwise they will be null
    	public double [] variationsAverages=null; // holds per extrinsic parameter or per parameter/per station average values
    	public int    [] averageCellIndex=  null; // for each element in the parameters vector holds index in  variationsAverages array
    	public double [] weightOnAverage=   null; // how variation of this parameter influences average (for all or for currenrt station 
    	public double [] weightVariance=    null; // weight for LMA over variance of parameters from average 
    	public double [] varianceErrorsSquared=  null; // weighted squared errors 
    	
    	
    	
    	public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData// per-image parameters
    			) {
    		this.distortionCalibrationData=distortionCalibrationData;
    		setDflt(0);    		
    	}
		public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData,// per-image parameters
    	    	int numSeries  // number of iteration series
    			){
    		this.distortionCalibrationData=distortionCalibrationData;
    		this.selectedImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];
    		this.selectedValidImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];
    		
    		setDflt(numSeries);    		
    	}
		
	   	public FittingStrategy(
        		boolean smart,
        		String defaultPath,
    	    	DistortionCalibrationData distortionCalibrationData // per-image parameters
    			) {
			String [] extensions={".stg-xml","-strategy.xml"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"*.stg-xml files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					false,
					"Restore Fitting Strategy",
					"Restore",
					parFilter,
					defaultPath); //String defaultPath
			if ((pathname==null) || (pathname=="")) return;
	   		setFromXML(
	    	    	distortionCalibrationData, // per-image parameters
	    	    	pathname);
			System.out.println("Restored fitting strategy from "+pathname);
	   	}
		
		
    	/**
    	 * Reads FittingStrategy from an XML file
    	 * @param distortionCalibrationData should be defined before this class!
    	 * @param pathname pathe to the saved data
    	 */
	   	public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData, // per-image parameters
    	    	String pathname
    			) {
	   		setFromXML(
	    	    	distortionCalibrationData, // per-image parameters
	    	    	pathname);
	   	}
    	public void setFromXML(
    	    	DistortionCalibrationData distortionCalibrationData, // per-image parameters
    	    	String pathname
    			) {
    		this.distortionCalibrationData=distortionCalibrationData;
        	XMLConfiguration hConfig=null;
        	try {
				hConfig=new XMLConfiguration(pathname);
			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			hConfig.setThrowExceptionOnMissing(false); // default value, will return null on missing
// read parameterList
			int len=Integer.parseInt(hConfig.getString("parameterMap.length","0"));
			if (len<=0) {
				String msg="No parameterMap specified in "+ pathname;
				IJ.showMessage("Error",msg);
				throw new IllegalArgumentException (msg);
			}
	    	this.parameterList=new int [len][2];
	    	this.parameterEnable=new boolean[len];
    		for (int i=0; i<this.parameterList.length; i++){
    			this.parameterList[i][0]=Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".subcamera"));
    			this.parameterList[i][1]=Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".index"));
    			this.parameterEnable[i]= (Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".visible"))>0);
    		}
			int nSer=Integer.parseInt(hConfig.getString("series.number","0"));
			if (nSer==0) return; // arrays will just be null
			this.selectedImages=new boolean[nSer][];
			this.selectedValidImages=new boolean[nSer][];
			this.masterImages=new int[nSer];
			this.stopAfterThis=new boolean[nSer];
			for (int i=0;i<nSer;i++) this.stopAfterThis[i]=true; // older configuration files did not have it
			this.parameterMode=new int[nSer][len];
			this.varianceModes=new int[nSer];
			this.zGroups=new int [nSer][];
			this.parameterGroups=new int[nSer][len][];
			this.lambdas=new double  [nSer];
			this.stepDone=new double [nSer];
        	for (int i=0;i<nSer;i++){ // iterate through series
//this.stopAfterThis        		
// read selected images (no check here that it matches to the    distortionCalibrationData!
        		String sSeries="series.series_"+i;
        		String fs=hConfig.getString(sSeries+".selectedImages");
        		this.selectedImages[i]=new boolean[fs.length()];
        		this.selectedValidImages[i]=null;
        		for (int j=0;j<this.selectedImages[i].length;j++) this.selectedImages[i][j]=(fs.charAt(j)=='+');
    			this.masterImages[i]=Integer.parseInt(hConfig.getString(sSeries+".masterImage"));
    			if (hConfig.getString(sSeries+".varianceModes")!=null) {
    				this.varianceModes[i]=Integer.parseInt(hConfig.getString(sSeries+".varianceModes"));
    			} else {
    				this.varianceModes[i]=varianceModeDisabled;
    			}
    			
        		for (int j=0;j<this.parameterList.length;j++){
        			int nPar=this.parameterList[j][1];
        			int nSub=this.parameterList[j][0];
        			String hconfigName=sSeries+".parameterMode."+
    				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
    				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"");
//        			System.out.println("Setting this.parameterMode["+i+"]["+j+"] from " +hconfigName);
        			if (hConfig.getString(hconfigName)!=null)  this.parameterMode[i][j]=Integer.parseInt(hConfig.getString(hconfigName));
        			else System.out.println("Failed to set this.parameterMode["+i+"]["+j+"] from " +hconfigName+" - maybe it is a new parameter not present in the file "+pathname);
        			this.parameterGroups[i][j]=null;
//Try to read series
        			for (int ni=0;ni<this.selectedImages.length;ni++){
        				String sGroup=hConfig.getString(sSeries+".parameterMode."+
                				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
                				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"")+
                				"_group"+ni);
        				if (sGroup!=null){
        					if (this.parameterGroups[i][j]==null){
        						this.parameterGroups[i][j]=new int[this.selectedImages.length];
        						for (int ni1=0;ni1<this.selectedImages.length;ni1++)this.parameterGroups[i][j][ni1]=0;
        					}
        					this.parameterGroups[i][j][ni]=Integer.parseInt(sGroup);
        				}
        			}
        		}
        		this.lambdas[i]=Double.parseDouble(hConfig.getString(sSeries+".lambdas"));
        		this.stepDone[i]=Double.parseDouble(hConfig.getString(sSeries+".stepDone"));
        		if (hConfig.getString(sSeries+".stopAfterThis")!=null){
        			this.stopAfterThis[i]=Boolean.parseBoolean(hConfig.getString(sSeries+".stopAfterThis"));
        		}
    			String zGroupsName=sSeries+".zGroups";
    			if (hConfig.getString(zGroupsName)!=null) {
    				int numZgroups=Integer.parseInt(hConfig.getString(zGroupsName));
    				this.zGroups[i]=new int [numZgroups];
    				for (int nZGroup=0;nZGroup<numZgroups;nZGroup++){
    					this.zGroups[i][nZGroup]=Integer.parseInt(hConfig.getString(sSeries+".zGroup_"+nZGroup));
    				}
    			} else {
    				this.zGroups[i]=null;
    			}
        	}
//        	if (hConfig.getString("variances")!=null){
//            if (!hConfig.configurationAt("variances").isEmpty()){
       		if (hConfig.configurationsAt("variances").size()>0){
//        		System.out.println("hConfig.configurationAt(\"variances\").isEmpty()=false");
        		this.distortionCalibrationData.eyesisCameraParameters.getCostsPropertiesXML("variances.",hConfig);
        	} 
        	this.pathName=pathname;
//        	distortionCalibrationData.readAllGrids();
    	}
    	
/**
 * Adjust  selectedImages and parameterGroups after number of images may change, old number of images should be >0
 * If the new number is larger, the first images attributes will be copied, the extra ones - use those of the last of the old ones	
 * @param newNumberOfImages new number of images used by this series
 */
    	public void adjustNumberOfImages(int newNumberOfImages){
    		if ((this.selectedImages == null) ||(this.selectedImages[0].length==0)) {
    			String msg="selectedImages array is "+((this.selectedImages == null)?"null":"empty");
    			IJ.showMessage("Error",msg); 
    			throw new IllegalArgumentException (msg);
    		}
    		if (this.selectedImages[0].length==newNumberOfImages) return;
    		for (int nSer=0;nSer<this.selectedImages.length;nSer++){
    			boolean [] tmp=selectedImages[nSer].clone();
    			this.selectedImages[nSer]=new boolean[newNumberOfImages];
    			this.selectedValidImages[nSer]=null; // just invalidate
    			for (int i=0;i<newNumberOfImages;i++) this.selectedImages[nSer][i]=(i<tmp.length)?tmp[i]:tmp[tmp.length-1];
    			for (int nPar=0;nPar<this.parameterList.length;nPar++){
    				if (this.parameterGroups[nSer][nPar]!=null){
    					int [] tmpGroup=this.parameterGroups[nSer][nPar].clone();
    					this.parameterGroups[nSer][nPar]=new int[newNumberOfImages];
    	    			for (int i=0;i<newNumberOfImages;i++) this.parameterGroups[nSer][nPar][i]=(i<tmpGroup.length)?tmpGroup[i]:tmpGroup[tmpGroup.length-1];
    				}
    			}
    			
    		}
    	}
    	
    	public String selectAndSaveToXML(
    			boolean smart,
    			String defaultPath){
			String [] extensions={".stg-xml","-strategy.xml"};
			CalibrationFileManagement.MultipleExtensionsFileFilter parFilter = new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"*.stg-xml files");
			if ((defaultPath==null) || (defaultPath.length()==0)){
				defaultPath=this.pathName;
			}
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					true,
					"Save Fitting Strategy",
					"Save",
					parFilter,
					defaultPath); //String defaultPath
			if (pathname!=null) saveToXML(pathname);
			return pathname;

    	}
    	  //http://commons.apache.org/configuration/userguide/howto_xml.html      
        public boolean saveToXML(String pathname) {
        	if (pathname==null) return false;
        	XMLConfiguration hConfig=new XMLConfiguration();
        	hConfig.setRootElementName("FittingStrategy");
//        	write which of the overall parameter correspond to each global/subcamera one, and which parameters are hidden/shown
    		hConfig.addProperty("parameterMap","");
    		hConfig.addProperty("parameterMap.length",this.parameterList.length);
    		for (int i=0; i<this.parameterList.length; i++){
        		hConfig.addProperty("parameterMap.par_"+i,"");
        		hConfig.addProperty("parameterMap.par_"+i+".subcamera",this.parameterList[i][0]);
        		hConfig.addProperty("parameterMap.par_"+i+".index",this.parameterList[i][1]);
        		hConfig.addProperty("parameterMap.par_"+i+".visible",this.parameterEnable[i]?"1":"0");
    		}
        	
    		hConfig.addProperty("series","");
    		hConfig.addProperty("series.number",this.selectedImages.length);
    		
        	for (int i=0;i<this.selectedImages.length;i++){ // iterate through series
        		String sSeries="series.series_"+i;
        		hConfig.addProperty(sSeries,"");
        		String fs="";
        		for (int j=0;j<this.selectedImages[i].length;j++)fs+=this.selectedImages[i][j]?"+":"-";
        		hConfig.addProperty(sSeries+".selectedImages",fs);
        		hConfig.addProperty(sSeries+".masterImage",this.masterImages[i]);
        		if (this.varianceModes!=null) hConfig.addProperty(sSeries+".varianceModes",this.varianceModes[i]);
        		hConfig.addProperty(sSeries+".parameterMode","");
        		for (int j=0;j<this.parameterList.length;j++){
        			int nPar=this.parameterList[j][1];
        			int nSub=this.parameterList[j][0];
            		hConfig.addProperty(sSeries+".parameterMode."+
            				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
            				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):""),
            				this.parameterMode[i][j]);
// cleaning up output - removing unused groups (may disable
            		if (((this.parameterMode[i][j]==this.modeGroup) || this.saveUnusedGroups ) &&  (this.parameterGroups[i][j]!=null)){
            			for (int ni=0;ni<this.parameterGroups[i][j].length;ni++){
                    		hConfig.addProperty(sSeries+".parameterMode."+
                    				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
                    				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"") +"_group"+ni,
                    				this.parameterGroups[i][j][ni]);
            			}
            		}
        		}
        		if (this.zGroups[i]!=null) {
        			hConfig.addProperty(sSeries+".zGroups", this.zGroups[i].length);
    				for (int nZGroup=0;nZGroup<this.zGroups[i].length;nZGroup++){
    					hConfig.addProperty(sSeries+".zGroup_"+nZGroup,this.zGroups[i][nZGroup]);
    				}
        		}
        		hConfig.addProperty(sSeries+".lambdas",lambdas[i]);
        		hConfig.addProperty(sSeries+".stepDone",stepDone[i]);
        		hConfig.addProperty(sSeries+".stopAfterThis",this.stopAfterThis[i]);
        	}
        	hConfig.addProperty("variances","");
       		this.distortionCalibrationData.eyesisCameraParameters.setCostsPropertiesXML("variances.",hConfig);
        	File file=new File (pathname);
        	BufferedWriter writer;
			try {
				writer = new BufferedWriter(new FileWriter(file));
	        	hConfig.save(writer);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			this.pathName=pathname;
        	return true;
        }
        /**
         * Verifies that series exists and is not empty (valid for LMA)
         * @param num - series to verify
         * @return true if series is valid
         */
    	public boolean isSeriesValid(int num){
    		if ((num<0) || (num>=this.selectedImages.length)) return false;
    		boolean someImagesSelected=false;
    		for (int i=0;i<this.selectedImages[num].length;i++) if (this.selectedImages[num][i]){
    			someImagesSelected=true;
    			break;
    		}
    		if (!someImagesSelected) {
    			if (this.debugLevel>0) System.out.println("isSeriesValid("+num+"): no images selected");
    			return false;
    		}
    		boolean someParametersSelected=false;
    		for (int i=0;i<this.parameterMode[num].length;i++) if (this.parameterMode[num][i]>0){
    			someParametersSelected=true;
    			break;
    		}
    		if (!someParametersSelected) {
    			if (this.debugLevel>0) System.out.println("isSeriesValid("+num+"): no parameters selected");
    			return false;
    		}
    		return true;
    	}
    	/**
    	 * Determins if LMA should stop after this series (either flag is set or next is invalid)
    	 * @param num number of series to test
    	 * @return true if LMA should stop
    	 */
    	public boolean isLastSeries(int num){
    		if (this.stopAfterThis[num]) return true;
    		if (!isSeriesValid(num+1)) return true;
    		return false;
    	}

        /**
         * 
         * Specifies images to process and parameters to adjust
         * Each parameter cab be:
         * 0 - "fixed" - use individual, per-image parameters, do not modify them
         * 1 - "common" - parameters are common for all selected images.
         *                When saving - save to all selected images.
         *                When loading (if different) use "master image" (or closest to it in time)
         * 2 - "super common" - same as common, but save to all images, not just selected
         * 3 - "individual"
    +====================+===========+===========+
    |                    |  Same TS  |  Diff TS  |
    |                    +-----+-----+-----+-----+
    |                    |  C  |  I  |  C  |  I  |
    +============+=======+=====+=====+=====+=====+
    | Same       |Eyesis |  X  |  X  |  C  |  I  |
    | Subcamera  +-------+-----+-----+-----+-----+
    |            |Subcam |  X  |  X  |  C  |  I* |
    +============+=======+=====+=====+=====+=====+
    | Different  |Eyesis |  C  |  C  |  C  |  I  |
    | Subcameras +-------+-----+-----+-----+-----+
    |            |Subcam |  X  |  X  |  X  |  X  | subcameras for different subcameras are individual parameters
    +============+=======+=====+=====+=====+=====+
    I* - special case when the subcamera is being adjusted/replaced. How to deal with it?

         *
         */
        
 //    	public int [][] reverseParameterMap=null; // reversed map - for each [imageNumber][parameterNumber] -> vector index (-1 fixed)\
    	// selects all enabled images in the specified series (modifies strategy series!)
    	/**
    	 * selects all enabled images in the specified series (modifies strategy series!)
    	 * @param ser number of fitting strategy series to set
    	 * @return old value for series images selection
    	 */
    	public boolean [] selectAllImages(int ser){
 //   		this.selectedValidImages[ser]=null; // just invalidate - do nothing, so restore would work
    		boolean [] oldSelection=this.selectedImages[ser].clone();
			for (int i=0;i<this.selectedImages[ser].length;i++) this.selectedImages[ser][i]=true; // select all\
			return oldSelection;
    	}
    	/**
    	 * Sets image selection. Can be used to restore saved selection after selectAllImages(int ser)
    	 * @param ser number of fitting strategy series to set
    	 * @param selection array specifying selected images
    	 */
    	public void setImageSelection(int ser, boolean [] selection){
    		this.selectedImages[ser]=selection.clone();
    	}
    	public boolean [] selectedAllImages() {
//    		if (this.currentSeriesNumber<0) return null;
			return selectedAllImages(this.currentSeriesNumber);
		}
    	public boolean [] selectedAllImages(int ser) {
    		if ((ser<0) || (ser>=this.selectedImages.length)){
    			boolean [] allImages=new boolean[this.selectedImages[0].length];
    			for (int i=0;i<allImages.length;i++) allImages[i]=true; // select all
    			return allImages;
    		} else {
        		return this.selectedImages[ser];
    		}
		}
    	public boolean [] selectedImages() {
//    		if (this.currentSeriesNumber<0) return null;
			return selectedImages(this.currentSeriesNumber);
		}
    	public boolean [] selectedImagesNoBadKernels(int ser) {
    		boolean [] selected=selectedImages(ser);
    		for (int i=0;i<selected.length;i++) selected[i]&= !this.distortionCalibrationData.gIP[i].noUsefulPSFKernels;
    		return selected;
    	}
    	public void setNoUsefulPSFKernels(int i, boolean noUsefulPSFKernels){
    		this.distortionCalibrationData.gIP[i].noUsefulPSFKernels=noUsefulPSFKernels;
    	}
    	public void invalidateSelectedImages(int ser){
    		this.selectedValidImages[ser]=null;
    	}
    	public void initSelectedValidImages(int ser){
    		this.selectedValidImages[ser]=this.selectedImages[ser].clone();
    	}
    	public void invalidateSelectedImage(int ser,int nImg){
    		this.selectedValidImages[ser][nImg]=false;
    	}
    	
    	//this.selectedValidImages
    	public boolean [] selectedImages(int ser) {
    		return selectedImages(ser,false);
    	}
    	public boolean [] selectedImages(int ser, boolean userSelection) {
    		boolean [] selectedMasked;
    		if ((ser<0) || (ser>=this.selectedImages.length)){
    			selectedMasked=new boolean[this.selectedImages[0].length];
    			for (int i=0;i<selectedMasked.length;i++) selectedMasked[i]=true; // select all
    		} else {
    			selectedMasked=this.selectedImages[ser].clone();
    		}
    		for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= this.distortionCalibrationData.gIP[i].enabled;
    		// unselect empty
    		// TODO: add minimal number of nodes?
			if ((ser>=0) && (this.selectedValidImages[ser]!=null) && !userSelection){
	    		for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= this.selectedValidImages[ser][i];
			} else {
				for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= (this.distortionCalibrationData.gIP[i].pixelsXY.length>0);
			}
    		return selectedMasked; //this.selectedImages[ser];
    	}
    	public int getNumSeries(){
    		return this.selectedImages.length;
    	}
    	
       	/**
    	 * Creates map from the parameter vector index to the {grid image number, parameter number}
    	 * When the parameter is shared by several images, the map points to the one which value will be used
    	 * (they might be different). Timestamp of the masterImages[] is used to determine which image to use.  
    	 * Simultaneously creates this.reverseParameterMap that maps each of the image/parameter to the parameter vector
    	 * Needs to be run for each new strategy series
    	 * @param numSeries number of fitting strategy series
    	 * @return this.parameterMap
    	 */

    	public int  buildParameterMap (int numSeries){
    		int numPars=this.distortionCalibrationData.getNumParameters();
    		int numImg=this.distortionCalibrationData.getNumImages();
    		int numTPars=this.parameterMode[numSeries].length;
    		this.reverseParameterMap=new int [numImg][numPars];
//			boolean [] selectedEnabledImagesAll=selectedImages(numSeries,true); // strictly as in series, including no valid points ones
			boolean [] selectedEnabledImages=selectedImages(numSeries);
// set defaults - -1 - "fixed", use individual parameter from this image    		
    		for (int i=0;i<numImg;i++) for (int j=0;j<numPars;j++) this.reverseParameterMap[i][j]=-1;
    		int vectorIndex=0;
//    		int [][] tmpMap=new int[numTPars][3]; // temporary array parameterMap[][] (will be truncated)
    		int [][] tmpMap=new int[numPars*numImg][3]; // temporary array parameterMap[][] (will be truncated)
			double masterTS=this.distortionCalibrationData.getImageTimestamp(this.masterImages[numSeries]); // timestamp of the master image
// iterate through all global/subcamera parameters    		
    		for (int numTPar=0;numTPar<numTPars;numTPar++) if (this.parameterMode[numSeries][numTPar]!=this.modeFixed){ // skip "fixed"
    			boolean isCommon=
    				(this.parameterMode[numSeries][numTPar]==this.modeCommon) ||
    				(this.parameterMode[numSeries][numTPar]==this.modeSupercommon);
    			boolean isStation=
    				(this.parameterMode[numSeries][numTPar]==this.modeStation) ||
    				(this.parameterMode[numSeries][numTPar]==this.modeSuperStation);
    			boolean isGroup=(this.parameterMode[numSeries][numTPar]==this.modeGroup);
    			int numSub=this.parameterList[numTPar][0]; // number of sub-camera for this total parameter index
    			int numPar=this.parameterList[numTPar][1]; // number of per-image parameter for this total parameter index
    			boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(numPar);
    			if (this.debugLevel>2) System.out.println("numTPar="+numTPar+" numSub="+numSub+" numPar="+numPar);
// iterate through available images
    			for (int numThisImg=0;numThisImg<numImg;numThisImg++) {
    				if ((selectedEnabledImages[numThisImg]) &&
    						(!isSubCamera || (numSub==this.distortionCalibrationData.getImageSubcamera(numThisImg))) &&
    						(this.reverseParameterMap[numThisImg][numPar]<0)){ // image used, this cell is not (yet) defined
    					if (this.debugLevel>2){
    						System.out.println("buildParameterMap("+numSeries+"): numThisImg="+numThisImg+", numPar="+numPar+", vectorIndex="+vectorIndex);
    					}
// assign it a new parameter    					
    					this.reverseParameterMap[numThisImg][numPar]=vectorIndex;
    					double thisTS=this.distortionCalibrationData.getImageTimestamp(numThisImg);
    					int thisStation=this.distortionCalibrationData.gIP[numThisImg].getStationNumber();
// set pointer to this first image    					
    				    tmpMap[vectorIndex][0]=numThisImg; // vectorindex==22 > tmpMap.length?
    				    tmpMap[vectorIndex][1]=numPar;
    				    tmpMap[vectorIndex][2]=this.parameterMode[numSeries][numTPar];
    				    double minDist=Math.abs(this.distortionCalibrationData.getImageTimestamp(numThisImg)-masterTS);
    				    if (this.debugLevel>2) System.out.println("vectorIndex="+vectorIndex+" numThisImg="+numThisImg);
// see if same parameter in some other image(s) is shared    
    					for (int numOtherImg=numThisImg+1;numOtherImg<numImg;numOtherImg++)
    						if ((selectedEnabledImages[numOtherImg]) && // OOB 1
        						(!isSubCamera || (numSub==this.distortionCalibrationData.getImageSubcamera(numOtherImg))) &&
        						(this.reverseParameterMap[numOtherImg][numPar]<0)){ // image used, this cell is not (yet) defined
    							if ((this.distortionCalibrationData.getImageTimestamp(numOtherImg)==thisTS) ||  // same parameter same timestamp - same group even if is set differently
    									(isStation && (this.distortionCalibrationData.gIP[numOtherImg].getStationNumber()==thisStation)) || // new
    									isCommon ||
    									(isGroup && (this.parameterGroups[numSeries][numTPar][numThisImg]==
    										this.parameterGroups[numSeries][numTPar][numOtherImg]))){
    								// assign it a the same parameter    					
    		    					this.reverseParameterMap[numOtherImg][numPar]=vectorIndex;
    		    					double thisDist=Math.abs(this.distortionCalibrationData.getImageTimestamp(numThisImg)-masterTS);
    		    					if (thisDist<minDist) {
    		    						minDist=thisDist;
    		        				    tmpMap[vectorIndex][0]=numOtherImg;
    		    					}
    							}
    					}
    				    vectorIndex++;  
    				}
    			}
    			
    		}
    		// reverseParameterMap built,   vectorIndex equals to the total number of parameters needed for fitting
    		//truncate tmpMap into this.parameterMap[][]    		
    		this.parameterMap=new int[vectorIndex][];
    		for (int i=0;i<vectorIndex;i++) this.parameterMap[i] =tmpMap[i]; 
    		this.currentSeriesNumber=numSeries;
		   	if (this.debugLevel>2) System.out.println("this.parameterMap.length="+this.parameterMap.length);
    		return this.parameterMap.length;
    	}
    	
    	/**
    	 * Prepare data for calculating additional LMA terms for parameter variances
    	 * @param numSeries fitting series to use
    	 * @return number of parameters for which variance is considered
    	 */
    	public int  buildVariancesMaps (
    			int numSeries){
    		if (this.debugLevel>0){
    			System.out.println("buildVariancesMaps("+numSeries+")");
    		}
    		if ((this.varianceModes==null) || (this.varianceModes[numSeries]==varianceModeDisabled)){
    				this.averageCellIndex=null;
    				this.variationsAverages=null;
    				this.weightVariance=null;
    				this.varianceErrorsSquared=null;
    				return 0;

    		}
    		int debugThreshold=2;
    		boolean useSetWeights=(this.varianceModes[numSeries]==varianceModeVariableWeight);
    		int numPars=this.distortionCalibrationData.getNumParameters();
    		int numStations=this.distortionCalibrationData.eyesisCameraParameters.getNumStations();
//    		int numTPars=this.parameterMode[numSeries].length;
    		int numSeriesPars=this.parameterMap.length;
			// count tilt positions per station
			List<Integer>  tiltList= new ArrayList<Integer>(1000);
			Integer iStationTilt;
			int tiltMotorIndex=2;
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				if (this.parameterMap[numSPar]==null){
					System.out.println("buildVariancesMaps() BUG - ,this.parameterMap["+numSPar+"]==null, numSeriesPars="+numSeriesPars);
					continue;
				}
				int imgNumber=this.parameterMap[numSPar][0]; // null pointer for triclops after adding variances for tilt
				int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
				int station=  this.distortionCalibrationData.gIP[imgNumber].getStationNumber();
				if (this.distortionCalibrationData.gIS[setNumber]==null){
					System.out.println("buildVariancesMaps() BUG - ,this.distortionCalibrationData.gIS["+setNumber+"]==null, numSeriesPars="+numSeriesPars+
							" numSPar="+numSPar+" imgNumber="+imgNumber+" station="+station);
					continue;
				}
				if (this.distortionCalibrationData.gIS[setNumber].motors==null){
					System.out.println("buildVariancesMaps() BUG - ,this.distortionCalibrationData.gIS["+setNumber+"].motors==null, numSeriesPars="+numSeriesPars+
							" numSPar="+numSPar+" imgNumber="+imgNumber+" station="+station);
					continue;
				}

				
				int tiltMotor=this.distortionCalibrationData.gIS[setNumber].motors[tiltMotorIndex];// null pointer for triclops after adding variances for tilt
				
				
				iStationTilt=station+numStations*tiltMotor;
				if (!tiltList.contains(iStationTilt)) tiltList.add(iStationTilt);
			}
			int numStationTilts=tiltList.size();

			int [] numGroups=new int [numPars];
			for (int i=0;i<numGroups.length;i++)numGroups[i]=0;
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int parIndex=this.parameterMap[numSPar][1];
//				if ((numGroups[parIndex]==0) && (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))){
				if (numGroups[parIndex]==0){
					boolean isSet=this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex);
					switch (this.parameterMap[numSPar][2]){
					case modeWeakCommon:
						if (isSet) numGroups[parIndex]=1;
						if (this.debugLevel>debugThreshold) System.out.println(">>>1 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					case modeWeakStation:
						if (isSet) numGroups[parIndex]=numStations;
						if (this.debugLevel>debugThreshold) System.out.println(">>>2 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					case modeTiltEqualize:
						if (isSet) numGroups[parIndex]=numStationTilts;
						if (this.debugLevel>debugThreshold) System.out.println(">>>3 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() numGroups:");
    			for (int i=0;i<numGroups.length;i++) if (numGroups[i]>0) System.out.println("--- "+i+":"+numGroups[i]);
    		}

			int startIndex=0;
			for (int i=0;i<numGroups.length;i++){
				if (numGroups[i]>0){
					int num=numGroups[i];
					numGroups[i]=startIndex;
					startIndex+=num;
				} else {
					numGroups[i]=-1;
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() start indices:");
    			for (int i=0;i<numGroups.length;i++) if (numGroups[i]>=0) System.out.println("--- "+i+":"+numGroups[i]);
    		}

    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() startIndex="+startIndex);
    		}

			if (startIndex==0){ // no variance parameters defined
				this.averageCellIndex=null;
				this.variationsAverages=null;
				this.weightVariance=null;
				this.varianceErrorsSquared=null;
				return 0;
			}

			// Assign average cell indices for each vector element, for each paramter it can be just 1 for global, numStations or number of tilt positions
			this.averageCellIndex=new int [numSeriesPars];
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				this.averageCellIndex[numSPar]=-1; // not applicable
				int parIndex=this.parameterMap[numSPar][1];
//			for (int numTPar=0;numTPar<numTPars;numTPar++) {
//				int parIndex=this.parameterList[numTPar][1];
				if ((numGroups[parIndex]>=0) && (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))){
					int imgNumber=this.parameterMap[numSPar][0];
					int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
					int station=  this.distortionCalibrationData.gIP[imgNumber].getStationNumber();

					switch (this.parameterMap[numSPar][2]){
					case modeWeakCommon:
						this.averageCellIndex[numSPar]=numGroups[parIndex];
						break;
					case modeWeakStation:
						this.averageCellIndex[numSPar]=numGroups[parIndex]+station;
						break;
					case modeTiltEqualize:
						int tiltMotor=this.distortionCalibrationData.gIS[setNumber].motors[tiltMotorIndex];
						iStationTilt=station+numStations*tiltMotor;
						this.averageCellIndex[numSPar]=numGroups[parIndex]+tiltList.indexOf(iStationTilt);
						break;
					}
				}
			}
    		if (this.debugLevel>1){
    			System.out.println("buildVariancesMaps() numStationTilts="+numStationTilts+" numSeriesPars="+numSeriesPars+ " useSetWeights="+useSetWeights+
    					" number of averages="+startIndex);
    		}
			
			this.variationsAverages=new double [startIndex]; // total number of different averages
			int [] numContributors=new int [startIndex];
			for (int i=0;i<this.variationsAverages.length;i++) {
				this.variationsAverages[i]=0.0;
				numContributors[i]=0;
			}
			// Calculate total weights for averages and number of contributors (to remove single-contributor items)
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					double w;
					if (useSetWeights){
						int imgNumber=this.parameterMap[numSPar][0];
						int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
						w=this.distortionCalibrationData.gIS[setNumber].getSetWeight();
					} else {
						w=1.0;
					}
					if (w>0.0) { // do not count zero-contributors (are they possible?)
						this.variationsAverages[avIndex]+=w;
						numContributors[avIndex]++;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() numContributors:");
    			for (int i=0;i<numContributors.length;i++) if (numContributors[i]>0) System.out.println("--- "+i+":"+numContributors[i]);
    		}

			// calculate each parameter share of the average TODO: disable if share =100% (single contributor)
			int numVariancePars=0;
			this.weightOnAverage=new double [numSeriesPars];
			this.weightVariance= new double [numSeriesPars];
			this.varianceErrorsSquared= new double [numSeriesPars];

			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				this.weightOnAverage[numSPar]=0.0;
				this.weightVariance[numSPar]=0.0;
				this.varianceErrorsSquared[numSPar]=Double.NaN;
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					if (numContributors[avIndex]<2){
						this.averageCellIndex[numSPar]=-1; // single element - impossible to use
						if (this.debugLevel>0) System.out.println("buildVariancesMaps(): removed parameter #"+numSPar+" - single contributor to average");
					} else {
						numVariancePars++;
						double w;
						if (useSetWeights){
							int imgNumber=this.parameterMap[numSPar][0];
							int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
							w=this.distortionCalibrationData.gIS[setNumber].getSetWeight();
						} else w=1.0;
						this.weightOnAverage[numSPar]=w/this.variationsAverages[avIndex];
						this.weightVariance[numSPar]= w;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("average indices/shares:");
    			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
    				int avIndex=this.averageCellIndex[numSPar];
    				if (avIndex>=0){
    					int parIndex=this.parameterMap[numSPar][1];
    					System.out.println("==="+numSPar+" parIndex="+parIndex+" avIndex="+avIndex+
    							" weightOnAverage="+this.weightOnAverage[numSPar]+
    							" weightVariance="+this.weightVariance[numSPar]);
    				}
    			}
    		}
			if (numVariancePars==0){
				this.averageCellIndex=null;
				this.variationsAverages=null;
				this.weightVariance=null;
				this.varianceErrorsSquared=null;
				return 0;
			}
			// TODO this.variationsAverages needs to be reset to all zeros each calculations
			return numVariancePars;
    	}
    	/**
    	 * Ammend LMA arrays to pull individual values of the extrinsic parameters to their averages
    	 * Averages may be global, per-station or per same tilt motor position (same station)
    	 * The function to be minimized is
    	 * f(x) Scale*(1/p*Xeff+(1-1/p)*pow(Xeff,p)
    	 * Xeff=x/X0*(1-shareInAverage)
    	 * and x is difference between the poarameter and it's average over the specified set (all/station/tiltStation)
    	 * @param numSeries fitting series number
    	 * @param vector - parameters vector
    	 * @param jTByJ Jacobian transposed multiplied by Jacobian - some diagonal elements may be modified (weighted)  or null (will add to old values)
    	 * @param jTByDiff Jacobian transposed multiplied by difference vector (weighted)  or null (will add to old values)
    	 * @return true if parameter variances are applicable to the fitting series
    	 */
    	public boolean addVarianceToLMA(
    			int numSeries,
    			final double [] vector,
    	        double [][] jTByJ, // jacobian multiplied by Jacobian transposed
    	        double []   jTByDiff){ // jacobian multiplied difference vector
    		if (this.averageCellIndex==null) return false; // parameter varainces are not used
//    		int numTPars=this.parameterMode[numSeries].length;
    		int numSeriesPars=this.parameterMap.length;
    		int debugThreshold=2;

//    		double [] fX=   new double [numTPars];
//    		double [] diagJ=new double [numTPars]; // diagonal of Jacobian (here parameters only influence themselves)
    		double [] weights=new double [this.variationsAverages.length];
    		for (int i=0;i<this.variationsAverages.length;i++) {
    			this.variationsAverages[i]=0.0;
    			weights[i]=0.0;
    		}
    		// calculate appropriate averages
    		for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
    			this.varianceErrorsSquared[numSPar]=0.0;
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					this.variationsAverages[avIndex]+=this.weightOnAverage[numSPar]*vector[numSPar];
    			}
    		}
    		// now fX and diagJ
    		int numModded=0;
    		for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int avIndex=this.averageCellIndex[numSPar];
				int parIndex=this.parameterMap[numSPar][1];
//    			if ((avIndex>=0) &&  (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))) {
    			if (avIndex>=0) {
    				double scale=this.distortionCalibrationData.eyesisCameraParameters.varianceCostScale(parIndex);
    				double x0=   this.distortionCalibrationData.eyesisCameraParameters.varianceCostVariationAbs(parIndex);
    				double exp=  this.distortionCalibrationData.eyesisCameraParameters.varianceCostVariationExponent(parIndex);
    				double xEff=(vector[numSPar]-this.variationsAverages[avIndex])*(1.0-this.weightOnAverage[numSPar])/x0; //OOB-1
    				double axEff=Math.abs(xEff);
    				double sxEff=Math.signum(xEff);
    				double k=1.0/exp;
    				double fX=sxEff*scale*(k*axEff+(1.0-k)*Math.pow(axEff,exp));
    				double diagJ=scale*((1.0-this.weightOnAverage[numSPar])/x0)*(k+(exp-1)*Math.pow(axEff,exp-1.0));
    				this.varianceErrorsSquared[numSPar]=this.weightVariance[numSPar]*fX*fX;
    				if (jTByJ!=null) jTByJ[numSPar][numSPar]+=this.weightVariance[numSPar]*diagJ*diagJ;
    				if (jTByDiff!=null) jTByDiff[numSPar]-= this.weightVariance[numSPar]*fX*diagJ;
    				if (this.debugLevel>debugThreshold){
    					System.out.print  (numSPar+" "+parIndex+" "+avIndex+": "+" scale="+scale);
    					System.out.print  (" vector["+numSPar+"]="+vector[numSPar]+" ("+this.variationsAverages[avIndex]+") xEff="+xEff+" fX="+fX+" k="+k+" exp="+exp );
    					System.out.print  (" diagJ="+diagJ+" "+xEff+" jTByJ+"+ (this.weightVariance[numSPar]*diagJ*diagJ) );
    					System.out.print  (" jTByDiff+"+(-this.weightVariance[numSPar]*fX*diagJ)+" ves="+(this.weightVariance[numSPar]*fX*fX));
    					System.out.println(" weight="+this.weightVariance[numSPar]);
    				}
    				if (this.weightVariance[numSPar]!=0.0) numModded++;
    			}
    		}
			if (this.debugLevel>1)System.out.println ("addVarianceToLMA() - modified "+numModded+" elements");
    		return true;
    	}
    	public double [] getVarianceError2(){ return (this.averageCellIndex==null)?null:this.varianceErrorsSquared;}
    	public double [] getWeights(){ return (this.averageCellIndex==null)?null:this.weightVariance;}
    	
    	/**
    	 * 
    	 * @return number of the current fitting strategy series
    	 */
    	public int getCurrentSeries(){
    		return this.currentSeriesNumber;
    	}
    	
    	/**
    	 * Calculate vector of the parameters used in LMA algorithm, extracted from the
    	 * individual data, using parameter map (calculated once after changing series)
    	 * @return vector of parameters used for the LMA
    	 */
    	public double [] getSeriesVector(){
        	if (this.parameterMap==null) {
        		String msg="Series parameters map is not calculated";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
    		double [] vector=new double[this.parameterMap.length];
    		for (int i=0;i<vector.length;i++) {
//    			vector[i]=this.distortionCalibrationData.pars[this.parameterMap[i][0]][this.parameterMap[i][1]];
    			vector[i]=this.distortionCalibrationData.getParameterValue(this.parameterMap[i][0],this.parameterMap[i][1]); // (numImg, numPar)
    		}
    		return vector;
    	}
    	
    	/**
    	 * Saves data from the parameter vector to that of the images 
    	 * @param vector vector of parameters (after LMA fitting)
    	 */
    	// TODO: Update the temporarily disabled images also, when possible (modify buildParameterMap also?
    	public void saveSeriesVector(double [] vector){
    		if ((this.parameterMap==null) ||(vector==null) || (vector.length!=this.parameterMap.length) ) {
    			String msg="Vector length does not match parameters length";
    			IJ.showMessage("Error",msg);
    			throw new IllegalArgumentException (msg);
    		}
    		// save for "individual" and "common" - and "station" (week - same as individual here)
    		boolean [] selectedEnabledImages=selectedImages(this.currentSeriesNumber);
    		for (int numImg=0;numImg<this.reverseParameterMap.length;numImg++)	if (selectedEnabledImages[numImg]){
    			for (int nPar=0;nPar<this.reverseParameterMap[numImg].length;nPar++) if (this.reverseParameterMap[numImg][nPar]>=0){
//    				this.distortionCalibrationData.pars[numImg][nPar]=vector[this.reverseParameterMap[numImg][nPar]];
    				this.distortionCalibrationData.setParameterValue(numImg,nPar,vector[this.reverseParameterMap[numImg][nPar]],true);
    				
    				if (this.debugLevel>2){
    					System.out.println(" Updated image "+numImg+" "+distortionCalibrationData.getParameterName(nPar)+" = "+
    				//			this.distortionCalibrationData.pars[numImg][nPar]) ; //vector[this.reverseParameterMap[numImg][nPar]]);
    							this.distortionCalibrationData.getParameterValue(numImg,nPar));
    				}
    			}
    		}
    		// propagate to other (unselected) images for "super common" parameters
    		for (int vPar=0; vPar<this.parameterMap.length;vPar++) {
    			if (
    					(this.parameterMap[vPar][2]==modeSupercommon) ||
    					(this.parameterMap[vPar][2]==modeSuperStation)	){ // "super common" No "weak" here!

    				//        		int nSub=this.parameterMap[vPar][0]; // that's an image number, not a subcamera number
    				int nSub=this.distortionCalibrationData.gIP[this.parameterMap[vPar][0]].channel;
    				int nStation=this.distortionCalibrationData.gIP[this.parameterMap[vPar][0]].getStationNumber();
    				boolean superCommon=(this.parameterMap[vPar][2]==modeSupercommon);
    				int nPar=this.parameterMap[vPar][1];
    				//this.distortionCalibrationData.channels[i]
    				boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(nPar);
    				for (int numImg=0;numImg<this.reverseParameterMap.length;numImg++) {
    					if (superCommon || (this.distortionCalibrationData.gIP[numImg].getStationNumber()==nStation)) {

    						if (this.debugLevel>2){
    							System.out.println("saveSeriesVector(): numImg="+numImg+" nPar="+nPar+" isSubCamera="+isSubCamera+
    									" nSub="+nSub+" distortionCalibrationData.getImageSubcamera("+numImg+")="+distortionCalibrationData.getImageSubcamera(numImg)+
    									" vector["+vPar+"]="+vector[vPar]);
    						}
    						if ((!isSubCamera) || (nSub==this.distortionCalibrationData.getImageSubcamera(numImg))) {
//    							this.distortionCalibrationData.pars[numImg][nPar]=vector[vPar];
    							this.distortionCalibrationData.setParameterValue(numImg,nPar,vector[vPar],true);
    						}
    					}
    				}
    			}
    		}
    	}
    	
    	/**
    	 * Calculates current values of all parameters for the particular sensor - some ("fixed")
    	 * are taken from the data stored for this individual image, others - from the parameter
    	 * vector (used in fitting) UPDATE: works with null parameterVector
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return vector used for the current image (parameters influencing the acquired grid
    	 * on the sensor (common parameters and those of the sensor's subchannel)
    	 */
    	public double [] getImageParametersVector(int numImg, double [] parameterVector){
//    		double [] vector = this.distortionCalibrationData.pars[numImg].clone();
    		double [] vector = this.distortionCalibrationData.getParameters(numImg); // returns a copy, no clone() is needed
    		if ((parameterVector!=null) && (this.reverseParameterMap!=null) && (this.reverseParameterMap[numImg]!=null)){
    			for (int i=0;i<vector.length;i++){
    				if (this.reverseParameterMap[numImg][i]>=0) vector[i]=parameterVector[this.reverseParameterMap[numImg][i]];
    			}
    		}
    		return vector;
    	}
    	/**
    	 * Calculates which of all parameters for the particular sensor are to be adjusted (to reduce calcualtions)
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return mask vector to be used with the results of getImageParametersVector() 
    	 */
    	public boolean [] getImageParametersVectorMask (int numImg){
    		if ((this.reverseParameterMap==null) || (this.reverseParameterMap[numImg]==null)) return null;
//    		boolean [] mask =new boolean [this.distortionCalibrationData.pars[numImg].length];
    		boolean [] mask =new boolean [this.distortionCalibrationData.getParametersLength(numImg)];
    		for (int i=0;i<mask.length;i++){
    			mask[i]=(this.reverseParameterMap[numImg][i]>=0);
    		}
    		return mask;
    	}
    	/**
    	 * Calculates index in the parameter vector corresponding to each image parameter vector element
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return mask vector to be used with the results of getImageParametersVector() 
    	 */
    	public int [] getImageParametersVectorReverseMap (int numImg){
    		if ((this.reverseParameterMap==null) || (this.reverseParameterMap[numImg]==null)) return null;
//    		int  [] map =new int [this.distortionCalibrationData.pars[numImg].length];
    		int  [] map =new int [this.distortionCalibrationData.getParametersLength(numImg)];
    		for (int i=0;i<map.length;i++){
    			map[i]=this.reverseParameterMap[numImg][i];
    		}
    		return map;
    	}
    	/**
    	 * Opens a text window wityh a table that shows map from each element of the parameters vector
    	 * to the image number used as a source of this parameter
    	 * @param title Window title
    	 */
    	public void showCurrentParameterMap(String title){
    	    String header="#\tMode\tName\tSubcamera\tImage Number\tMode";
    	    StringBuffer sb = new StringBuffer();
    	    for (int i=0; i<this.parameterMap.length;i++){
    	        sb.append(
    	        		i+"\t"+
    	        		this.definedModesAll[this.parameterMap[i][2]]+"\t"+
    	        		this.distortionCalibrationData.getParameterName(this.parameterMap[i][1])+"\t"+
    	        		this.distortionCalibrationData.getImageSubcamera(this.parameterMap[i][0])+"\t"+
    	        		this.parameterMap[i][0]+"\t"+
    	        		this.definedModesAll[this.parameterMap[i][2]]+"\n");
    	    }
    	    new TextWindow(title, header, sb.toString(), 800,600);
    	}
    	/**
    	 * Opens window with a table mapping each image (column) parameter (row) to parameter vector element
    	 * "-" (-1 in teh table) - fixed parameter, use one from the individual image
    	 * @param title Window title
    	 */
    	public void showCurrentReverseParameterMap(String title){
    	    String header="Image";
			boolean [] selectedEnabledImages=selectedImages(this.currentSeriesNumber);
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	header+="\t"+i;
    	    }
    	    StringBuffer sb = new StringBuffer();
    	    sb.append("timestmps");
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	sb.append("\t"+IJ.d2s(this.distortionCalibrationData.getImageTimestamp(i),6));
    	    }
    	    sb.append("\n");
    	    sb.append("subcamera");
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	sb.append("\t"+this.distortionCalibrationData.getImageSubcamera(i));
    	    }
    	    sb.append("\n");
    	    
    	    for (int k=0;k<this.reverseParameterMap[0].length;k++){
//    			boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(k);
//        	    sb.append(this.distortionCalibrationData.getParameterName(this.parameterMap[k][1])); // name of parameter
        	    sb.append(this.distortionCalibrationData.getParameterName(k)); // name of parameter
        	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
        	    	int mode =this.reverseParameterMap[i][k];
        	    		sb.append("\t"+((mode>=0)?mode:"-"));
        	    	
        	    }
        	    sb.append("\n");
    	    	
    	    }
    	    new TextWindow(title, header, sb.toString(), 800,600);
    	}
    	
    	/**
    	 * Add/reduce number of series in this fitting strategy
    	 * @param numSeries new number of series
    	 */
    	public void setLength(int numSeries){
    		if (numSeries<=0) {
    			setDflt(0);
    			return;
    		}
    		if ((this.selectedImages!=null) && (numSeries==this.selectedImages.length)) return;
    		if ((this.selectedImages==null) && (numSeries==0)) return;
        	boolean [][] oldSelectedImages=null; // images selected for each step
        	int [][] oldParameterMode=null;
        	int [] oldVarianceModes=null;
        	int [][] oldZGroups=null;
        	int [][][] oldParameterGroups=null;
        	double [] oldLambdas=null;   // LMA initial lambda for each step
    		int [] oldMasterImages=null;
        	double [] oldStepDone=null;
        	boolean [] oldStopAfterThis=null;
            int oldNumSeries=(this.selectedImages==null)?0:this.selectedImages.length;
            int oldLength=0;
    		if (this.selectedImages!=null){
    			// deep clone this.* to old*;
    			oldSelectedImages=new boolean[oldNumSeries][];
    			oldParameterMode=new int[oldNumSeries][];
    			oldParameterGroups=new int[oldNumSeries][][];
    			oldZGroups=new int[oldNumSeries][];
    			for (int i=0;i<oldNumSeries;i++){
    				oldSelectedImages[i]=this.selectedImages[i].clone();
        			oldParameterMode[i]= this.parameterMode[i].clone(); // out of bound - 2
        			oldParameterGroups[i]=new int[this.parameterGroups[i].length][];
        			for (int j=0;j<oldParameterGroups[i].length;j++)
        				oldParameterGroups[i][j]=(parameterGroups[i][j]==null)?null:parameterGroups[i][j].clone();
        			if (this.zGroups[i]!=null) oldZGroups[i]=this.zGroups[i].clone();
        			else this.zGroups[i]=oldZGroups[i];
    			}
    			oldVarianceModes=(this.varianceModes==null)?null:this.varianceModes.clone();
    			oldLambdas=this.lambdas.clone();
    			oldLength=this.selectedImages.length;
        		oldMasterImages=this.masterImages.clone();
        		oldStepDone=this.stepDone.clone();
        		oldStopAfterThis=this.stopAfterThis.clone();
    		}
    		setDflt(numSeries);
    		if (this.selectedImages!=null) for (int i=0;(i<this.selectedImages.length) && (i<oldLength);i++){
    			this.selectedImages[i]=oldSelectedImages[i];
    			this.parameterMode[i]= oldParameterMode[i];
    			this.parameterGroups[i]=oldParameterGroups[i];
    			this.lambdas[i]=oldLambdas[i];
    			this.masterImages[i]=oldMasterImages[i];
        		this.stepDone[i]=oldStepDone[i];
        		this.stopAfterThis[i]=oldStopAfterThis[i];
    			this.varianceModes[i]=oldVarianceModes[i];
    			this.zGroups[i]=(oldZGroups[i]!=null)?oldZGroups[i].clone():null;
    		}
    	}
    	
    	public void updateNumberOfSubcameras(){
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		initParameterList();
    		int numSubPars=0;
    		int [] subParIndex=new int[numPars];
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) subParIndex[numSubPars++]=i;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		int oldTotalPars=this.parameterEnable.length;
    		if (oldTotalPars==totalNumPars) return;
    		if (oldTotalPars>numPars) { // more than one subcamera, so last numSubPars are subcamera parameters
        		for (int i=0;i<numSubPars;i++)subParIndex[i]=(oldTotalPars-numSubPars)+i;
    		}
//       public boolean isSubcameraParameter(int num){
    		
    		int numSeries=this.parameterMode.length;
    		if (oldTotalPars<totalNumPars){ // grow, repeat last subcamera
    			if (this.debugLevel>1) System.out.println("Increasing total number of parameters in fitting strategy: "+oldTotalPars+" -> "+totalNumPars);
    			for (int nSer=0;nSer<numSeries;nSer++){
    				int [] parameterMode_tmp=this.parameterMode[nSer]; //.clone();
    				int [][] parameterGroups_tmp=this.parameterGroups[nSer]; //.clone();
            		this.parameterMode[nSer]=new int[totalNumPars];
            		this.parameterGroups[nSer]=new int[totalNumPars][];
            		for (int nTPar=0;nTPar<oldTotalPars;nTPar++){
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[nTPar];
            			this.parameterGroups[nSer][nTPar]=parameterGroups_tmp[nTPar];
            		}
            		for (int nTPar=oldTotalPars;nTPar<totalNumPars;nTPar++){
            			int index= subParIndex[(nTPar-oldTotalPars) % numSubPars];
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[index];
            			this.parameterGroups[nSer][nTPar]=(parameterGroups_tmp[index]==null)?null:parameterGroups_tmp[index].clone();
            		}
    			}
				boolean []  parameterEnable_tmp=this.parameterEnable; //.clone();
        		this.parameterEnable= new boolean [totalNumPars];
        		for (int nTPar=0;nTPar<oldTotalPars;nTPar++){
        			this.parameterEnable[nTPar]=parameterEnable_tmp[nTPar];
        		}
        		for (int nTPar=oldTotalPars;nTPar<totalNumPars;nTPar++){
        			int index= subParIndex[(nTPar-oldTotalPars) % numSubPars];
        			this.parameterEnable[nTPar]=parameterEnable_tmp[index];
        		}
    		} else { // shrink
    			if (this.debugLevel>1) System.out.println("Reducing total number of parameters in fitting strategy: "+oldTotalPars+" -> "+totalNumPars);
    			for (int nSer=0;nSer<numSeries;nSer++){
    				int [] parameterMode_tmp=this.parameterMode[nSer]; //.clone();
    				int [][] parameterGroups_tmp=this.parameterGroups[nSer]; //.clone();
            		this.parameterMode[nSer]=new int[totalNumPars];
            		this.parameterGroups[nSer]=new int[totalNumPars][];
            		for (int nTPar=0;nTPar<totalNumPars;nTPar++){
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[nTPar];
            			this.parameterGroups[nSer][nTPar]=parameterGroups_tmp[nTPar];
            		}
    			}
				boolean []  parameterEnable_tmp=this.parameterEnable; //.clone();
        		this.parameterEnable= new boolean [totalNumPars];
        		for (int nTPar=0;nTPar<totalNumPars;nTPar++){
        			this.parameterEnable[nTPar]=parameterEnable_tmp[nTPar];
        		}
    		}
    	}
    	
    	private void setDflt(int numSeries){
    		if (numSeries==0) {
        		this.selectedImages=null;
        		this.lambdas=null;
        		this.stepDone=null;
        		this.parameterMode=null;
        		this.parameterGroups=null;
        		this.varianceModes=null;
        		this.zGroups=null;
                return;
    		}
    		this.stopAfterThis=new boolean[numSeries];
    		for (int i=0;i<numSeries;i++) this.stopAfterThis[i]=true;

// calculate total (potential) number of parameters in the system
//TODO: modify for groups
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		int numSubPars=0;
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) numSubPars++;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		// number of parameters to adjust may be greater, if they were changing between the images
    		this.selectedImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];//
    		this.selectedValidImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];//
    		this.masterImages=new int[numSeries];
    		this.lambdas=new double[numSeries];
    		this.stepDone=new double[numSeries];
    		this.parameterMode=new int[numSeries][totalNumPars];
    		this.varianceModes=new int[numSeries];
    		this.parameterGroups=new int[numSeries][totalNumPars][];
    		this.parameterEnable= new boolean [totalNumPars];
    		this.zGroups=new int[numSeries][];
    		for (int i=0;i<this.selectedImages.length;i++){
    			invalidateSelectedImages(i);
    			for (int j=0;j<this.selectedImages[i].length;j++){
    				this.selectedImages[i][j]=false;
    			}
    			for (int j=0;j<this.parameterMode[i].length;j++){
    				this.parameterMode[i][j]=this.modeFixed; // fixed
    				this.parameterGroups[i][j]=null; // no groups yet defined
    			}
    			this.lambdas[i]=defaultLambda;
    			this.stepDone[i]=defaultStepDone;
    			this.masterImages[i]=0; // first image, supposingly the earliest timestamp
    			this.varianceModes[i]=varianceModeDisabled;
        		this.zGroups[i]=null;
    		}
    		
    		initParameterList();    		

    		for (int i=0;i<numPars;i++){
    			this.parameterEnable[i]=true; // initially enable all parameters for the first camera
    		}
    		for (int i=1;i<numSubCams;i++){
    			int i1=numPars+numSubPars*(i-1);
    			int j1=0;
    			for (int j=0;j<numPars;j++) if (this.distortionCalibrationData.isSubcameraParameter(j)){
    				this.parameterEnable[i1+j1]=false; // initially disable all other subcamera parameters
        			j1++;
    			}
    		}
    	}
    	
    	private void initParameterList(){
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		int numSubPars=0;
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) numSubPars++;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		this.parameterList=new int[totalNumPars][2];

    		for (int i=0;i<numPars;i++){
    			this.parameterList[i][0]=0;
    			this.parameterList[i][1]=i;
//    			this.parameterEnable[i]=true; // initially enable all parameters for the first camera
    		}
    		
    		for (int i=1;i<numSubCams;i++){
    			int i1=numPars+numSubPars*(i-1);
    			int j1=0;
    			for (int j=0;j<numPars;j++) if (this.distortionCalibrationData.isSubcameraParameter(j)){
    				this.parameterList[i1+j1][0]=i;
    				this.parameterList[i1+j1][1]=j;
 //   				this.parameterEnable[i1+j1]=false; // initially disable all other subcamera parameters
        			j1++;
    			}
    		}
    		
    	}
    	/**
    	 * Find parameter number from subcamera and index
    	 * @param nSub number of subcamera (channel)
    	 * @param index index of parameter
    	 * @return number of parameter that has specified sub-camera and index. -1 if such does not exist
    	 */
    	private int getParameterNumber(int nSub, int index){
    		for (int i=0;i<this.parameterList.length;i++) if ((this.parameterList[i][0]==nSub) && (this.parameterList[i][1]==index)) return i;
    		return -1;
    	}
    	
    	/**
    	 * 
    	 * @param numSeries Number of series to edit
    	 * @param useImages Select images for this series
    	 * @param fromToImages - limit number of checkboxes, otherwise window does not show the bottom ones
    	 * @param useParameters Select parameters for this series
    	 * @param askNextSeries Ask for next series number
    	 * @param zeroAndOther use 2 channels 0 and "other", propagete settings for channel 1 to all the rest
    	 * @return -2 - cancel, -1, done, otherwise - number of step to edit
    	 */
    	public int selectStrategyStep(
    			int numSeries,
    			boolean useImages,
				int [] fromToImages,
    			boolean allImages,
    			boolean useParameters,
    			boolean askLambdas,
    			boolean askNextSeries,
    			boolean zeroAndOther
    			){
    		boolean showDirectMap=false;
    		boolean showReverseMap=false;
    		// if current series is not valid (probably just started a new one) - look for the last valid (if any)
    		// and copy it;
    		int numEstimated=this.distortionCalibrationData.getNumberOfEstimated(true); //(boolean enabledOnly
    		int [] numEstimatedPerStation=this.distortionCalibrationData.getNumberOfEstimatedPerStation(true); //(boolean enabledOnly
    		String sNumEstimatedPerStation="";
    		for (int i=0;i<numEstimatedPerStation.length;i++){
    			if (i>0) sNumEstimatedPerStation+=", ";
    			sNumEstimatedPerStation+=numEstimatedPerStation[i];
    		}
    		int numNewEnabled=this.distortionCalibrationData.getNumNewEnabled();
    		int [] numNewEnabledPerStation=this.distortionCalibrationData.getNumNewEnabledPerStation();
    		String sNumNewEnabledPerStation="";
    		for (int i=0;i<numNewEnabledPerStation.length;i++){
    			if (i>0) sNumNewEnabledPerStation+=", ";
    			sNumNewEnabledPerStation+=numNewEnabledPerStation[i];
    		}
     		if (!isSeriesValid(numSeries)){
    			int sourceSeries=-1;
    			for (int i=numSeries-1;i>=0;i--) if (isSeriesValid(i)){
    				sourceSeries=i;
    				break;
    			}
    			if (sourceSeries>=0){
    				invalidateSelectedImages(numSeries); // just in case -= will be recalculated
    				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
    					this.selectedImages[numSeries][i]=this.selectedImages[sourceSeries][i]; // copy for all, not only enabled
    				}
    				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i]){
    					this.parameterMode[numSeries][i]=this.parameterMode[sourceSeries][i];
    					this.parameterGroups[numSeries][i]=(this.parameterGroups[sourceSeries][i]==null)?null:this.parameterGroups[sourceSeries][i].clone();
    				}
    				this.masterImages[numSeries]=this.masterImages[sourceSeries];
    				this.lambdas[numSeries]=this.lambdas[sourceSeries];
    				this.stepDone[numSeries]=this.stepDone[sourceSeries];
    				this.stopAfterThis[numSeries]=this.stopAfterThis[sourceSeries];
    				this.varianceModes[numSeries]=this.varianceModes[sourceSeries];
    				this.zGroups[numSeries]=(this.zGroups[sourceSeries]!=null)?this.zGroups[sourceSeries].clone():null;
    			}
    		}
    		
			GenericDialog gd = new GenericDialog("Fitting Strategy Step Configuration, step "+numSeries+" number of enabled images="+this.distortionCalibrationData.getNumEnabled());
			gd.addCheckbox("Copy all from previous series (ignore all other fields)", false);
			gd.addCheckbox("Remove all (but first) images, reopen dialog", false); // remove all will be invalid, copied from the previous
			gd.addCheckbox("Select all images, reopen dialog", false);
			if (numEstimated>0){
				gd.addMessage("There are "+numEstimated+" ("+sNumEstimatedPerStation+") enabled images that have estimated orientation");
				gd.addCheckbox("Select them and only them (and re-open dialog)", false);
			} else {
				gd.addMessage("There are no enabled images with estimated (from neighbors) orientation");
			}
			if (numNewEnabled>0){
				gd.addMessage("There are "+numNewEnabled+" ("+sNumNewEnabledPerStation+" new enabled images");
				gd.addCheckbox("Select them and only them (and re-open dialog)", false);
			} else {
				gd.addMessage("There are no new enabled images");
			}
			int numStations=this.distortionCalibrationData.eyesisCameraParameters.getNumStations();
			boolean [] constrainByStation=new boolean[numStations];
			for (int i=0;i<constrainByStation.length;i++) constrainByStation[i]=true;
			if (this.distortionCalibrationData.eyesisCameraParameters.numStations>1){
				gd.addMessage("Constrain by stations");
//				gd.addCheckbox("Remove images of unselected stations below", true);
				for (int i=0;i<this.distortionCalibrationData.eyesisCameraParameters.numStations;i++) 	gd.addCheckbox("Station "+i, constrainByStation[i]);				
			}
			
			if (useImages) {
	    		gd.addNumericField("Image sepection range, from", fromToImages[0], 0);
	    		gd.addNumericField("Image sepection range, up to (inclufing)", fromToImages[1], 0);
				gd.addMessage("Select files to include");
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++)
					if ((allImages || this.distortionCalibrationData.gIP[i].enabled) && (i>=fromToImages[0]) && (i<=fromToImages[1])){
					int hm=this.distortionCalibrationData.gIP[i].hintedMatch;
					gd.addCheckbox    (i+" - "+(this.distortionCalibrationData.gIP[i].enabled?"":"(disabled) ")+
							IJ.d2s(this.distortionCalibrationData.gIP[i].timestamp,6)+
							": "+this.distortionCalibrationData.gIP[i].channel+
							" matched "+this.distortionCalibrationData.gIP[i].matchedPointers+" pointers"+
							", hinted state: "+((hm<0)?"undefined":((hm==0)?"failed":((hm==1)?"orientation":"orientation and translation"))),
							this.selectedImages[numSeries][i]);
				}
				if (allImages) gd.addCheckbox("Enable selected, disable deselected images", false);
 
				gd.addNumericField("The 'master' (used for common parameters)", this.masterImages[numSeries], 0);

			}
			if (useParameters) {
				gd.addMessage("Select parameters to fit");
				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i] &&
						(!zeroAndOther || (this.parameterList[i][0] <=1) || (this.parameterList[i][0] ==24))){ // in "zeroAndOther" mode do not show other subcameras
					int parIndex=this.parameterList[i][1];
					int subCam=this.parameterList[i][0];
					boolean isSub=this.distortionCalibrationData.isSubcameraParameter(parIndex);
					boolean defined=false;
					double min=0.0,max=0.0;
					for (int imgNumber=0;imgNumber<this.distortionCalibrationData.getNumImages(); imgNumber++)
						if (this.selectedImages[numSeries][imgNumber] && this.distortionCalibrationData.gIP[imgNumber].enabled){
							int sub=this.distortionCalibrationData.gIP[imgNumber].channel;
							if (!isSub || (sub==subCam) ||  // global or same subcamera
									(zeroAndOther && (subCam>=1) && (subCam<24) && (sub>=1) && (sub<24)) || // both head "other"
									(zeroAndOther && (subCam>=24) && (sub>=24) )
									) { // both subcameras are "other" subcameras
								double parValue=this.distortionCalibrationData.getParameterValue(imgNumber,parIndex);
								if (!defined) {
//									min=this.distortionCalibrationData.pars[imgNumber][parIndex];
									min=parValue;
									max=min;
									defined=true;
								}
//								if (this.distortionCalibrationData.pars[imgNumber][parIndex]<min) min=this.distortionCalibrationData.pars[imgNumber][parIndex];
//								if (this.distortionCalibrationData.pars[imgNumber][parIndex]>max) max=this.distortionCalibrationData.pars[imgNumber][parIndex];
								if (parValue<min) min=parValue;
								if (parValue>max) max=parValue;
							}
						}
//					System.out.println(i+": "+parIndex+":"+subCam+"defined="+defined+" min="+min+" max="+max);
					// undefined, min, max
					String sValue=(defined)?((min==max)?(min+""):(min+"..."+max)):"undefined";
					String sChn=(zeroAndOther && (subCam>=1)&& (subCam<24))?"-head-other":
						((zeroAndOther && (subCam>=24))?"-bottom":("-"+subCam));
					boolean noWeak=!this.distortionCalibrationData.eyesisCameraParameters.isExtrinsic(parIndex);
					boolean isTilt=this.distortionCalibrationData.eyesisCameraParameters.isTilt(parIndex);

					gd.addChoice( // ArrayIndexOutOfBoundsException: 9
							this.distortionCalibrationData.getParameterName(parIndex)+
							" ("+sValue+" "+
							this.distortionCalibrationData.getParameterUnits(parIndex)+")"+
							(this.distortionCalibrationData.isSubcameraParameter(parIndex)?(" sub"+sChn):"com "),
							(isTilt?this.definedModesTiltEq:(noWeak?this.definedModesNoWeak:this.definedModes)),
							this.definedModesAll[this.parameterMode[numSeries][i]]); // definedModesAll - includes all others
				}
			}
			if (askLambdas) {
				gd.addNumericField("Initial lambda for the L-M algorithm", this.lambdas[numSeries], 6,8,"");
				gd.addStringField("Relative decrese in error to error ratio to consider series finished", ""+this.stepDone[numSeries], 8);
				gd.addCheckbox("Stop after this series", this.stopAfterThis[numSeries]);
			}
			if (this.varianceModes!=null) gd.addChoice(
					"Processing of selected parameters variances",
					this.definedVarianceModes,
					this.definedVarianceModes[this.varianceModes[numSeries]]);
			gd.addCheckbox("Edit variances costs", false);
			if (numStations>1){
				int oldZGroupsLength=(this.zGroups[numSeries]!=null)?this.zGroups[numSeries].length:0;
				int [] oldZGroups={};
				if (oldZGroupsLength>0) oldZGroups=this.zGroups[numSeries].clone();
				this.zGroups[numSeries]=new int [numStations];
				int nextZGroup=-1;
				for (int i=0;i<numStations;i++){
					if (i<oldZGroupsLength) {
						this.zGroups[numSeries][i]=oldZGroups[i];
						if (this.zGroups[numSeries][i]>nextZGroup) nextZGroup=this.zGroups[numSeries][i];
					} else {
						this.zGroups[numSeries][i]=++nextZGroup;
					}
				}
				String [] zGroupsChoices=new String [numStations+1];
				zGroupsChoices[0]="Not used";
				for (int i=0;i<numStations;i++) zGroupsChoices[i+1]="Group "+i;
				gd.addMessage ("Select groups of same pattern for different stations (pattern did not move between measurements). Used for pattern correction command");
				for (int i=0;i<numStations;i++){
					int zG=(this.zGroups[numSeries][i]<0)?0:(this.zGroups[numSeries][i]+1);
					gd.addChoice( // ArrayIndexOutOfBoundsException: 9
							"Same target group for station " +i,
							zGroupsChoices,
							zGroupsChoices[zG]); // definedModesAll - includes all others
					
				}
			} else {
				this.zGroups[numSeries]=new int [1];
				this.zGroups[numSeries][0]=0;
			}
			
			if (askNextSeries) {
				gd.addCheckbox("Rebuild/Show parameter Map", showDirectMap);
				gd.addCheckbox("Rebuild/Show reverse parameter Map", showReverseMap);
				gd.addNumericField("Next series to edit (<0 - done)", numSeries+1, 0);
			}

			if (askNextSeries) gd.enableYesNoCancel("OK", "Done");
			WindowTools.addScrollBars(gd);
			gd.showDialog();
			if (gd.wasCanceled()) return -2;
			boolean copyFromPrevious=gd.getNextBoolean();
			boolean removeAllImages=gd.getNextBoolean();
			boolean selectAllImages=gd.getNextBoolean();
			boolean selectEstimated=false;
			if (numEstimated>0) selectEstimated=gd.getNextBoolean();
			boolean selectNewEnabled=false;
			if (numNewEnabled>0) selectNewEnabled=gd.getNextBoolean();
			if (selectNewEnabled) {
				this.selectedImages[numSeries]=this.distortionCalibrationData.selectNewEnabled();
				return numSeries; // caller will repeat with the same series
			}
			
			if (this.distortionCalibrationData.eyesisCameraParameters.numStations>1){
//				boolean removeUnselectedStations=gd.getNextBoolean();
				for (int i=0;i<constrainByStation.length; i++) constrainByStation[i]=gd.getNextBoolean();
			}

			if (selectEstimated) {
				this.selectedImages[numSeries]=this.distortionCalibrationData.selectEstimated(true); //(boolean enabledOnly
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
					this.selectedImages[numSeries][i]&=constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
				}
				return numSeries; // caller will repeat with the same series
			}
			
			if (copyFromPrevious || removeAllImages || selectAllImages) {
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
//					this.selectedImages[numSeries][i]=false; // invalidate - all, regardless of .enabled
					this.selectedImages[numSeries][i]=selectAllImages || ((i==0) && removeAllImages); // invalidate - all, regardless of .enabled
					this.selectedImages[numSeries][i]&=constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
				}
				return numSeries; // caller will repeat with the same series
			}
			boolean enableDisableSelected=false;
			if (useImages) {
	    		fromToImages[0]=    (int) gd.getNextNumber();
	    		fromToImages[1]=    (int) gd.getNextNumber();
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++)
					if ((allImages || this.distortionCalibrationData.gIP[i].enabled) && (i>=fromToImages[0]) && (i<=fromToImages[1])){
					this.selectedImages[numSeries][i]=gd.getNextBoolean();
				}
				if (allImages) enableDisableSelected=gd.getNextBoolean();
				this.masterImages[numSeries]=(int) gd.getNextNumber();
				if (this.masterImages[numSeries]<0)this.masterImages[numSeries]=0;
				if (this.masterImages[numSeries]>=this.selectedImages[numSeries].length)this.masterImages[numSeries]=this.selectedImages[numSeries].length;
			}
			if (useParameters) {
				int [] lastGroups=null;
				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i] &&
						(!zeroAndOther || (this.parameterList[i][0] <=1) || (this.parameterList[i][0]==24))){ // in "zeroAndOther" mode do not show other subcameras
					//				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i]){
					this.parameterMode[numSeries][i]=gd.getNextChoiceIndex();
					if (this.parameterMode[numSeries][i]==this.modeGroup) {
						if (this.parameterGroups[numSeries][i]!=null) lastGroups=this.parameterGroups[numSeries][i]; // default groups
						else if (lastGroups!=null) this.parameterGroups[numSeries][i]=lastGroups.clone(); // may be null
						selectGroups(numSeries,i);
					}
				}
				if (zeroAndOther){
					for (int i =0; i<this.parameterEnable.length;i++) {
						if ((this.parameterList[i][0]>1) && (this.parameterList[i][0]!=24)){ // "other" subchannels - copy from subchannel1
							int refChannel=(this.parameterList[i][0]<24)?1:24;
							int iSub1=getParameterNumber(refChannel, this.parameterList[i][1]);
							if (this.parameterEnable[iSub1]){
								//					System.out.println(	"parameter number="+i+" this.parameterList[i][0]="+this.parameterList[i][0]+" this.parameterList[i][1]="+this.parameterList[i][1]+" iSub1="+iSub1);
								this.parameterMode[numSeries][i]=this.parameterMode[numSeries][iSub1];
								if (this.parameterMode[numSeries][i]==this.modeGroup) { // copy groups from channel 1
									if (this.parameterGroups[numSeries][i]!=null) this.parameterGroups[numSeries][i]=this.parameterGroups[numSeries][iSub1].clone(); // may be null
									else this.parameterGroups[numSeries][i]=null;
								}
							}
						}
					}
				}
			}
			if (askLambdas) {
				this.lambdas[numSeries]=gd.getNextNumber();
				this.stepDone[numSeries]=Double.parseDouble(gd.getNextString());
				this.stopAfterThis[numSeries]=gd.getNextBoolean();
			}
    		if (enableDisableSelected) {
    			this.distortionCalibrationData.enableSelected(this.selectedImages[numSeries]);
    		}
    	
    		if (this.varianceModes!=null) this.varianceModes[numSeries]=gd.getNextChoiceIndex();
    		
    		boolean editVariancesCosts=gd.getNextBoolean();
    		if (editVariancesCosts){
				for (int i =0; i<this.parameterList.length;i++) {
					int parIndex=this.parameterList[i][1];
					if ((this.parameterMode[numSeries][i]==modeWeakCommon) ||
							(this.parameterMode[numSeries][i]==modeWeakStation) ||
							(this.parameterMode[numSeries][i]==modeTiltEqualize)
					){
						if (!this.distortionCalibrationData.eyesisCameraParameters.isExtrinsic(parIndex)){
							System.out.println("BUG: this.parameterMode["+numSeries+"]["+i+"]="+this.parameterMode[numSeries][i]+
									", but this parameter ("+this.distortionCalibrationData.getParameterName(parIndex)+" is not valid for variances");
							continue;
						}
						this.distortionCalibrationData.eyesisCameraParameters.editCostProperties(
								parIndex,
								this.distortionCalibrationData.getParameterName(parIndex),
								this.distortionCalibrationData.getParameterDescription(parIndex),
								this.distortionCalibrationData.getParameterUnits(parIndex));
					}
				}
    		}
    		
			if (numStations>1){
				for (int i=0;i<numStations;i++){
					this.zGroups[numSeries][i]=gd.getNextChoiceIndex()-1;
				}
			}
    		
    		if (!gd.wasOKed()) return -1; // pressed Done (no need to ask for the next number)

			if (askNextSeries) {
				showDirectMap=gd.getNextBoolean();
				showReverseMap=gd.getNextBoolean();
				if (showDirectMap || showReverseMap){
					buildParameterMap(numSeries);
					if (showDirectMap)  showCurrentParameterMap       ("Parameter map");
					if (showReverseMap) showCurrentReverseParameterMap("Reverse parameter map");
				}

				int nextSeries= (int) gd.getNextNumber();
				if (nextSeries<-1) nextSeries=-1;
				return nextSeries;
			}
			return -1;
    	}

    	private boolean selectGroups(
				int numSeries,
				int numPar){
 //   		if (this.debugLevel>1){
 //   			System.out.println("selectGroups("+numSeries+", "+numPar+")");
 //   		}

    		
			int parIndex=this.parameterList[numPar][1];
			int subCam=this.parameterList[numPar][0];
			String name=this.distortionCalibrationData.getParameterName(parIndex)+
			(this.distortionCalibrationData.isSubcameraParameter(parIndex)?(" s"+subCam):"com ");
    		GenericDialog gd = new GenericDialog("Select image groups for "+name);
    		gd.addMessage("Select which images share the same value of "+name);
    		
    		if (this.parameterGroups[numSeries][numPar]==null) {
    			this.parameterGroups[numSeries][numPar]=new int [this.distortionCalibrationData.getNumImages()];
    			for (int i=0;i<this.parameterGroups[numSeries][numPar].length;i++)this.parameterGroups[numSeries][numPar][i]=0;
    		}
    		String [] choices=organizeGroups(this.parameterGroups[numSeries][numPar],false); // preserve group numbers
    		for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) if (this.selectedImages[numSeries][i]){
    			gd.addChoice (i+" - "+IJ.d2s(this.distortionCalibrationData.gIP[i].timestamp,6)+
    					": "+this.distortionCalibrationData.gIP[i].channel,
    					choices,
    					choices[this.parameterGroups[numSeries][numPar][i]]
    					);
    		}
			WindowTools.addScrollBars(gd);
          
			gd.showDialog();
			if (gd.wasCanceled()) return false;
    		for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) if (this.selectedImages[numSeries][i]){
    			this.parameterGroups[numSeries][numPar][i]=gd.getNextChoiceIndex();
    		}
			return true;
			
		}
		/**
		 * Organizes list of groups and creates a list of selection choices. If all members fit in the range
		 * of 0 (length-1) and (force==false), group numbers are preserved, otherwise they are renumbered 
		 * @param groups array of integers - group numbers
		 * @param force force renumbering groups
		 * @return list of selection choices
		 */
		private String [] organizeGroups(int []groups, boolean force) {
			if ((groups==null) || (groups.length==0)){
				String msg="Cannot organize mull or empty group";
				IJ.showMessage("Error",msg);
				throw new IllegalArgumentException (msg);
			}
			// See if 0.. length-1 groups is not enough to represent all numbers used
			for (int i=0;!force && (i<groups.length);i++) if ((groups[i]<0) || (groups[i]>=(groups.length-1))) force = true;
			if (force){
				int [] tmp=groups.clone();
				for (int i=0;i<groups.length;i++) groups[i]=-1;
				int groupNumber=0;
				int max=tmp[0]; for (int i=0;i<tmp.length;i++) if (max<tmp[i]) max=tmp[i];
				for (boolean organized=false; !organized;){
					int min=max+1;
					for (int i=0;i<tmp.length;i++) if ((groups[i]<0) && (min>tmp[i])) min=tmp[i];
					if (min>max) organized=true;
					else {
						for (int i=0;i<tmp.length;i++) if ((groups[i]<0) && (min == tmp[i])) {
							groups[i]=groupNumber;
						}
						groupNumber++;
					}
				}
			}
			String [] rslt= new String [groups.length];
			for (int i=0;i<rslt.length;i++) rslt[i]="Group "+(i+1);
			return rslt;

		}
    	public boolean selectStrategy(int startSerNumber){
    		int defaultLength=20;
    		boolean selectImages=    false;
    		boolean allImages=       false;
    		boolean selectParameters=true;
    		boolean askNextSeries=   true;
    		boolean askLambdas=      true;
    		boolean askParameterMask=false;
			boolean zeroAndOther=    true;
			int [] fromToImages={0,500};

    		int numSeries=startSerNumber;
    		int oldLength=(this.selectedImages==null)?0:this.selectedImages.length;
    		
    		GenericDialog gd = new GenericDialog("Fitting Strategy Step Configuration");
    		if (oldLength<=0) gd.addNumericField("Number of series in this strategy", defaultLength, 0);
    		gd.addNumericField("Number of series to edit (<0 - none)", numSeries, 0);
//    		gd.addNumericField("Number of series in this strategy", (oldLength>0)?oldLength:1, 0); //
    		gd.addCheckbox    ("Select images",selectImages);
    		gd.addNumericField("Show image checkboxes from", fromToImages[0], 0);
    		gd.addNumericField("Show image checkboxes up to (inclufing)", fromToImages[1], 0);
    		gd.addCheckbox    ("Select from all images (false - only enabled)",allImages);
    		gd.addCheckbox    ("Select parameters",selectParameters);
    		gd.addCheckbox    ("Ask for initial lambda",askLambdas);
    		gd.addCheckbox    ("Ask for parameter mask",askParameterMask);
    		gd.addCheckbox    ("Use only channel 0 and \"all other channels\"",zeroAndOther);
    		gd.addCheckbox    ("Ask for next series",askNextSeries);
    		if (oldLength>0)  gd.addNumericField("Increase number of series in this strategy", oldLength, 0);
   	        gd.enableYesNoCancel("OK", "Done");

    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		int numberOfSeries=0;
    		if (oldLength<=0) numberOfSeries= (int) gd.getNextNumber();
    		numSeries=          (int) gd.getNextNumber();
    		selectImages=             gd.getNextBoolean();
    		fromToImages[0]=    (int) gd.getNextNumber();
    		fromToImages[1]=    (int) gd.getNextNumber();
    		allImages=                gd.getNextBoolean();
    		selectParameters=         gd.getNextBoolean();
    		askLambdas=               gd.getNextBoolean();
    		askParameterMask=         gd.getNextBoolean();
    		zeroAndOther=             gd.getNextBoolean();
    		askNextSeries=            gd.getNextBoolean();
    		if (oldLength>0) numberOfSeries= (int) gd.getNextNumber();
    		if (numberOfSeries!=oldLength)  setLength(numberOfSeries);
    		if (!gd.wasOKed()) return true;
    		if (askParameterMask) setParameterSelectionMask(zeroAndOther);
    		while ((numSeries>=0) && (numSeries<this.selectedImages.length)){
    			numSeries=selectStrategyStep(
    					numSeries,
    					selectImages,
    					fromToImages,
    					allImages,
    					selectParameters,
    					askLambdas,
    					askNextSeries,
    					zeroAndOther);
    		}
    		return true;
    	}

    	public boolean setParameterSelectionMask(boolean zeroAndOther){
    		GenericDialog gd = new GenericDialog("Set Parameter Selection Mask");
    		gd.addMessage("Common parameters and sub-camera 0 parameters");
    		int subCam=0;
    		boolean showParameter=true;
    		for (int i=0;i<this.parameterList.length;i++){
    			if (this.parameterList[i][0]!=subCam){
    				subCam=this.parameterList[i][0];
    				if (zeroAndOther) {
    					showParameter=true;
    					if (subCam==1) gd.addMessage("Other sub-cameras parameters");
    					else if (subCam==24) gd.addMessage("Bottom sub-cameras parameters");
//    					else break;
    					else {
    						showParameter=false;
//    						continue;
    					}
    				} else {
    					gd.addMessage("Sub-camera "+subCam+" parameters");
    				}
    			}
        		if (showParameter) gd.addCheckbox (this.distortionCalibrationData.getParameterName(parameterList[i][1]),this.parameterEnable[i]);
    		}
      	     WindowTools.addScrollBars(gd);
//   	     gd.setBackground(Color.white);
			gd.showDialog();
			if (gd.wasCanceled()) return false;
			subCam=0;
			showParameter=true;
			//May add disablong all other subcameras, but try keeping it for later running with zeroAndOther==false
    		for (int i=0;i<this.parameterList.length;i++){
    			if (this.parameterList[i][0]!=subCam){
    				subCam=this.parameterList[i][0];
					showParameter=true;
//					if (subCam>1) break;
					if ((subCam>1) && (subCam!=24))	showParameter=false;
    			}
    			if (showParameter) this.parameterEnable[i]=gd.getNextBoolean();
    		}
    		return true;
    	}
//this.currentSeriesNumber
    	public double getLambda(){
    		return getLambda(this.currentSeriesNumber);
    	}
    	public double getLambda(int numSeries){
    		return this.lambdas[numSeries];

    	}
    	public double getStepDone(){
    		return getStepDone(this.currentSeriesNumber);
    	}
    	public double getStepDone(int numSeries){
    		return this.stepDone[numSeries];
    	}
    }
    public static class DistortionProcessConfiguration{
    	public String  sourceDirectory="";
    	public String  gridDirectory=  "";
    	public boolean useLaserPonters=true;
    	public boolean useNoPonters=true; // use images that do not have any lasre pointers
    	public boolean showAcquiredImages=true;
    	public boolean saveAcquiredImages=true;
    	public boolean selectSourceFiles = true;
    	public boolean removeOutOfGridPointers=true;
    	public boolean showGridImages=false;
    	public boolean saveGridImages=true;
    	public int     debugLevel=1;

    	public String selectSourceDirectory(boolean smart, String defaultPath, boolean newAllowed) {
    		String dir= CalibrationFileManagement.selectDirectory(
    				smart,
    				newAllowed, // save  
    				"Source (acquired from the camera) image directory", // title
    				"Select source directory", // button
    				null, // filter
    				defaultPath); // this.sourceDirectory);
    		if (dir!=null) this.sourceDirectory=dir;
    		return dir;
    	}
    	public String selectGridFileDirectory(boolean smart, String defaultPath, boolean newAllowed) {
    		String dir= CalibrationFileManagement.selectDirectory(
    				smart,
    				newAllowed, // save  
    				"Grid files directory (grid patterns extracted from the images)", // title
    				"Select grid files directory", // button
    				null, // filter
    				defaultPath); //this.sourceDirectory);
    		if (dir!=null) this.gridDirectory=dir;
    		return dir;
    	}
    	
    	public void setProperties(String prefix,Properties properties){
    		properties.setProperty(prefix+"sourceDirectory",        this.sourceDirectory);
    		properties.setProperty(prefix+"gridDirectory",          this.gridDirectory);
    		properties.setProperty(prefix+"useLaserPonters",        this.useLaserPonters+"");
    		properties.setProperty(prefix+"useNoPonters",           this.useNoPonters+"");
    		properties.setProperty(prefix+"showAcquiredImages",     this.showAcquiredImages+"");
    		properties.setProperty(prefix+"saveAcquiredImages",     this.saveAcquiredImages+"");
    		properties.setProperty(prefix+"selectSourceFiles",      this.selectSourceFiles+"");
    		properties.setProperty(prefix+"removeOutOfGridPointers",this.removeOutOfGridPointers+"");
    		properties.setProperty(prefix+"showGridImages",         this.showGridImages+"");
    		properties.setProperty(prefix+"saveGridImages",         this.saveGridImages+"");
    		properties.setProperty(prefix+"debugLevel",             this.debugLevel+"");
    	}
       	public void getProperties(String prefix,Properties properties){
    		if (properties.getProperty(prefix+"sourceDirectory")!=null)
    			this.sourceDirectory=properties.getProperty(prefix+"sourceDirectory");
    		if (properties.getProperty(prefix+"gridDirectory")!=null)
    			this.gridDirectory=properties.getProperty(prefix+"gridDirectory");
    		if (properties.getProperty(prefix+"useLaserPonters")!=null)
    			this.useLaserPonters=Boolean.parseBoolean(properties.getProperty(prefix+"useLaserPonters"));
    		if (properties.getProperty(prefix+"useNoPonters")!=null)
    			this.useNoPonters=Boolean.parseBoolean(properties.getProperty(prefix+"useNoPonters"));
    		if (properties.getProperty(prefix+"showAcquiredImages")!=null)
    			this.showAcquiredImages=Boolean.parseBoolean(properties.getProperty(prefix+"showAcquiredImages"));
    		if (properties.getProperty(prefix+"saveAcquiredImages")!=null)
    			this.saveAcquiredImages=Boolean.parseBoolean(properties.getProperty(prefix+"saveAcquiredImages"));
    		if (properties.getProperty(prefix+"selectSourceFiles")!=null)
    			this.selectSourceFiles=Boolean.parseBoolean(properties.getProperty(prefix+"selectSourceFiles"));
    		if (properties.getProperty(prefix+"removeOutOfGridPointers")!=null)
    			this.removeOutOfGridPointers=Boolean.parseBoolean(properties.getProperty(prefix+"removeOutOfGridPointers"));
    		if (properties.getProperty(prefix+"showGridImages")!=null)
    			this.showGridImages=Boolean.parseBoolean(properties.getProperty(prefix+"showGridImages"));
    		if (properties.getProperty(prefix+"saveGridImages")!=null)
    			this.saveGridImages=Boolean.parseBoolean(properties.getProperty(prefix+"saveGridImages"));
    		if (properties.getProperty(prefix+"debugLevel")!=null)
    			this.debugLevel=Integer.parseInt(properties.getProperty(prefix+"debugLevel"));
       	}
    	public boolean showDialog(String title) {
    		GenericDialog gd = new GenericDialog(title);
    		gd.addStringField("Source (acquired from the camera) image directory, blank will open selection window",this.sourceDirectory,40);
    		gd.addStringField("Grid files directory (grid patterns extracted from the images, blank will open selection window)",this.gridDirectory,40);
    		gd.addCheckbox   ("Locate laser pointers for each image", this.useLaserPonters);
    		gd.addCheckbox   ("Use images that do not contain laser pointers", this.useLaserPonters);
    		gd.addCheckbox   ("Show images after acquisition",        this.showAcquiredImages);
    		gd.addCheckbox   ("Save acquired images",                 this.saveAcquiredImages);
    		gd.addCheckbox   ("Individually select source image (false use all directory)", this.selectSourceFiles);
    		gd.addCheckbox   ("Remove detected laser pointers if they are outside of the grid", this.removeOutOfGridPointers);
    		gd.addCheckbox   ("Show grid files as images",            this.showGridImages);
    		gd.addCheckbox   ("Save grid files",                      this.saveGridImages);
    		gd.addNumericField("Debug level",                         this.debugLevel,0);
    	    WindowTools.addScrollBars(gd);
    	    gd.showDialog();
    	    if (gd.wasCanceled()) return false;
    	    String newSourceDirectory=   gd.getNextString();
    	    String newGridDirectory=     gd.getNextString();
    	    this.useLaserPonters=        gd.getNextBoolean();
    		this.useLaserPonters=        gd.getNextBoolean();
    	    this.showAcquiredImages=     gd.getNextBoolean();
    	    this.saveAcquiredImages=     gd.getNextBoolean();
    	    this.selectSourceFiles=      gd.getNextBoolean();
    	    this.removeOutOfGridPointers=gd.getNextBoolean();
    	    this.showGridImages=         gd.getNextBoolean();
    	    this.saveGridImages=         gd.getNextBoolean();
    	    this.debugLevel=       (int) gd.getNextNumber();
    	    System.out.println("1.newSourceDirectory = "+newSourceDirectory);
    	    System.out.println("1.newGridDirectory = "+  newGridDirectory);
    	    if ((newSourceDirectory.length()==0) || (newSourceDirectory.indexOf('?')>=0)) 
    	    	newSourceDirectory= selectSourceDirectory(false, this.sourceDirectory, true);
    	    else
    	    	newSourceDirectory= selectSourceDirectory(true, newSourceDirectory, true); // if matches, no dialog
    	    if (newSourceDirectory!=null) this.sourceDirectory=newSourceDirectory;
    	    if ((newGridDirectory.length()==0) || (newGridDirectory.indexOf('?')>=0))
    	    	newGridDirectory= selectGridFileDirectory(false, this.gridDirectory, true);
    	    else
    	    	newGridDirectory= selectGridFileDirectory(true, newGridDirectory, true);
    	    if (newGridDirectory!=null) this.gridDirectory=newGridDirectory;
//    	    System.out.println("2.newSourceDirectory = "+newSourceDirectory);
//    	    System.out.println("2.newGridDirectory = "+  newGridDirectory);
//    	    System.out.println("this.sourceDirectory = "+this.sourceDirectory);
//    	    System.out.println("this.gridDirectory = "+  this.gridDirectory);
    	    return true;
    	}

    	public String [] selectSourceFiles(){
    		return selectSourceFiles(!this.selectSourceFiles);
    	}
    	public String [] selectSourceFiles(boolean allFiles){
			String [] extensions={".tif",".tiff"};
			if (this.sourceDirectory.length()==0){
    	    	String newSourceDirectory= selectSourceDirectory(true, this.sourceDirectory, true);
    	    	if (newSourceDirectory!=null) this.sourceDirectory=newSourceDirectory;
			}
			String [] defaultPaths={this.sourceDirectory+Prefs.getFileSeparator()};
			if (this.sourceDirectory.length()==0){
				defaultPaths[0]="";
			}
			
			CalibrationFileManagement.MultipleExtensionsFileFilter sourceFilter =
				new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Source files");
			String [] sourceFiles=null;
    		if (allFiles){
				File dir= new File (this.sourceDirectory);
				if (this.debugLevel>1) System.out.println("selectSourceFiles, dir="+this.sourceDirectory);
				if (!dir.exists()) {
					String error="Source directory "+this.sourceDirectory+" does not exist.";
            		IJ.showMessage("No files selected");
					if (this.debugLevel>1) System.out.println("selectSourceFiles() ERROR:"+error);
					return null;
				}
				File [] fileList=dir.listFiles(sourceFilter);
				if (this.debugLevel>1) System.out.println("Source directory "+this.sourceDirectory+" has "+fileList.length+" files.");
				sourceFiles = new String[fileList.length];
				for (int i=0;i<sourceFiles.length;i++) sourceFiles[i]=fileList[i].getPath();
    		} else {
    				new CalibrationFileManagement.MultipleExtensionsFileFilter("",extensions,"Source files");
    			sourceFiles=CalibrationFileManagement.selectFiles(false,
    					"Select Source files, saved as TIFF",
    					"Select",
    					sourceFilter,
    					defaultPaths); // String [] defaultPaths); //this.sourceDirectory // null
    	       	if ((sourceFiles==null) || (sourceFiles.length==0)) {
					if (this.debugLevel>1) System.out.println("selectSourceFiles() ERROR: No files selected");
            		IJ.showMessage("No files selected");
            		return null;
            	}
    		}
	       	return sourceFiles;
    	}
    	
    }

}   

