Distortions.java Source code

Java tutorial

Introduction

Here is the source code for Distortions.java

Source

/*
 **
 ** 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
                true // filter border
        );
        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
                            true // filter border
                    );
                    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), true // filter border
        );
        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;
    }

    /**
     * Apply sensor correction to the projected grid (generated by estimateGridOnSensor()) 
     * @param gridOnSensor array [v][u][0- x,  1 - y, 2 - targetAbsolute-u, 3 - targetAbsolute-v] 
     * @param subCamera channel number
     * @return true if the correction was applied (in-place) false if no correction is available
     */
    public boolean correctGridOnSensor(double[][][] gridOnSensor, int subCamera) {
        if (this.pixelCorrection == null)
            return false;
        for (double[][] row : gridOnSensor)
            for (double[] cell : row)
                if ((cell != null) && (cell.length > 1)) {
                    double[] corrXYARGB = interpolateCorrectionVector( // vector of {corrX, corrY, alpha, flatfield_red, flatfield_green, flatfield_blue}
                            subCamera, //int chnNum,
                            cell[0], //double px,
                            cell[1]); //double py)
                    cell[0] += corrXYARGB[0]; // measured-> corrected : subtract, projected->simulated:add;
                    cell[1] += corrXYARGB[1] + 0.0; // Debugging by adding +1.0!!
                }
        //      System.out.println("================== Added +0.0 to pixel y for debugging purposes! =====================");
        return true;
    }

    /**
     * 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, boolean filterBorder) {
        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);
        }
        if (!Double.isNaN(goniometerHorizontal)) {
            int goniometerHorizontalIndex = fittingStrategy.distortionCalibrationData.eyesisCameraParameters
                    .getGoniometerHorizontalIndex();
            parVector[goniometerHorizontalIndex] = goniometerHorizontal;
        }
        if (!Double.isNaN(goniometerAxial)) {
            int goniometerAxialIndex = fittingStrategy.distortionCalibrationData.eyesisCameraParameters
                    .getGoniometerAxialIndex();
            parVector[goniometerAxialIndex] = goniometerAxial;
        }
        int sensorWidth = fittingStrategy.distortionCalibrationData.eyesisCameraParameters
                .getSensorWidth(subCamera);
        int sensorHeight = fittingStrategy.distortionCalibrationData.eyesisCameraParameters
                .getSensorHeight(subCamera);
        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];
                    }
                }
            }
        if (filterBorder) {
            // 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, boolean[] selectedChannels) {
        int numSeries = fittingStrategy.getNumSeries();
        boolean removeEmpty = false;
        boolean recalculate = false;
        boolean applyChannelFilter = 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);
            if (selectedChannels != null) {
                String s = "";
                for (boolean b : selectedChannels)
                    s += b ? "+" : "-";
                gd.addCheckbox("Filter by channel selection (" + s + ")", applyChannelFilter);
            }
            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();
            if (selectedChannels != null)
                applyChannelFilter = gd.getNextBoolean();
            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;
        }
        if (!applyChannelFilter)
            selectedChannels = null;
        //       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();
        if (selectedChannels != null) {
            selectedImages = selectedImages.clone(); // disconnect from original for modification
            for (int i = 0; i < selectedImages.length; i++)
                if (selectedImages[i]) {
                    int chn = this.fittingStrategy.distortionCalibrationData.gIP[i].channel;
                    if ((chn < 0) || (chn >= selectedChannels.length) || !selectedChannels[chn]) {
                        selectedImages[i] = false;
                    }
                }
        }
        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) {
        boolean eachImageInSet = false;
        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("Open each image in the set", eachImageInSet);
        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();
        eachImageInSet = gd.getNextBoolean();
        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;
                }
            }
            if (eachImageInSet) {
                for (int nChn = 0; nChn < imageNumbers.length; nChn++) {
                    int[] imageNumber = { imageNumbers[nChn] };
                    showImageReprojectionErrors(imageNumber, // if null - use all images in a series
                            filter, //weightFunctionFilter,
                            debugLevel);
                }
                // Do not exit, continue and show combine reprojection errors for all set
            }
        }
        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
        imp.setProperty("comment_defects", "Sensor hot/cold pixels list as x:y:difference");
        if (subCam.defectsXY != null) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < subCam.defectsXY.length; i++) {
                if (sb.length() > 0)
                    sb.append(" ");
                sb.append(subCam.defectsXY[i][0] + ":" + subCam.defectsXY[i][1] + ":" + subCam.defectsDiff[i]);
            }
            imp.setProperty("defects", sb.toString());
            //          } else {
            //          imp.setProperty("defects", null);
        }

        //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;
            if (imp.getProperty("defects") != null) {
                String sDefects = (String) imp.getProperty("defects");
                String[] asDefects = sDefects.trim().split(" ");
                subCam.defectsXY = new int[asDefects.length][2];
                subCam.defectsDiff = new double[asDefects.length];
                for (int i = 0; i < asDefects.length; i++) {
                    String[] stDefect = asDefects[i].split(":");
                    subCam.defectsXY[i][0] = Integer.parseInt(stDefect[0]);
                    subCam.defectsXY[i][1] = Integer.parseInt(stDefect[1]);
                    subCam.defectsDiff[i] = Double.parseDouble(stDefect[2]);
                }
            } else {
                subCam.defectsXY = null;
                subCam.defectsDiff = null;
            }
        }
        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 sensor 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 int[][] defectsXY = null; // pixel defects coordinates list (starting with worst)
        public double[] defectsDiff = null; // pixel defects value (diff from average of neighbors), matching defectsXY

        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;
            this.defectsXY = null; // pixel defects coordinates list (starting with worst)
            this.defectsDiff = null; // pixel defects value (diff from average of neighbors), matching defectsXY

        }

        // defects are not cloned!
        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();
            // use lower station if grid file does not have current           
            int useStation = (this.stationZCorr[v1][u1].length > station) ? station
                    : (this.stationZCorr[v1][u1].length - 1);
            result[2] += this.stationZCorr[v1][u1][useStation];
            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];
            int useStation = (this.stationZCorr != null)
                    ? ((this.stationZCorr[v1][u1].length > station) ? station
                            : (this.stationZCorr[v1][u1].length - 1))
                    : 0;
            if (nView >= this.photometricByView[station].length) { // OOB 1// NUll pointer - need to run F-field first? (oob1 when grid had less than now
                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][2]
                            + ((this.stationZCorr != null) ? this.stationZCorr[v1][u1][useStation] : 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 boolean overwriteResultFiles = false;
        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 + "overwriteResultFiles", this.overwriteResultFiles + "");
            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 + "overwriteResultFiles") != null)
                this.overwriteResultFiles = Boolean
                        .parseBoolean(properties.getProperty(prefix + "overwriteResultFiles"));
            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.addCheckbox("Overwrite existing result files", this.overwriteResultFiles);

            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.overwriteResultFiles = 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;
        }

    }

}