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

import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.calibration.CalibrationFileManagement;
import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.common.DoubleGaussianBlur;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.PolynomialApproximation;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.TDCorrTile;
import com.elphel.imagej.tileprocessor.TileNeibs;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.gui.PointRoi;
import ij.io.FileSaver;
import ij.process.FloatPolygon;
import ij.text.TextWindow;


public class OrthoMapsCollection  implements Serializable{
	private static final long serialVersionUID = 1L;
	public static final int MODE_IMAGE = 0;
	public static final int MODE_ALT =   1;
	public static final int MODE_MASK =  2;
	
	public static final int VERSION_PRE_ORIENT =  100;
	public static final int VERSION_POST_ORIENT = 101;
	public static final int VERSION_ORANGE =      102;
	
	public static int       LATEST_VERSION = VERSION_ORANGE; // 100; // use when read from .list
///	public transient int    current_version = LATEST_VERSION;
	public static int       CURRENT_VERSION = LATEST_VERSION;
	
	/*
	public static final int PAIR_NONE =      0;
	public static final int PAIR_DEFINED =   1;
	public static final int PAIR_UNDEFINED = 2;
	public static final int PAIR_FAILED =   -1;
	
	public static final int HEUR_LAST_SEQ =  1; // use last of connected following this  
	public static final int HEUR_DIV_LONG =  2; // divide long connected series  
	public static final int HEUR_MIN_DIA =   4; // minimal diameter increase (or no increase)  
	public static final int HEUR_SAME_DIA =  8; // minimal diameter increase (or no increase)
	*/  
	
	public static final String [] KEY_DIRS= {"rootDirectory", // from EyesisCorrectionParameters
			"sourceDirectory","linkedModels","linkedCenters","videoDirectory","x3dDirectory","resultsDirectory","scenesDirectory",
			"kernelsDirectory", "patternsDirectory","cuasSeed"};
	public static final String [] NAME_FH = {"full","half"};
	public static final String [] NAME_MO = {"main","other"};
	
	public            OrthoMap []                       ortho_maps;
	private           HashMap<String,ModelRegex>        model_regexes;
	private           String                            current_model_regex="";
	private           ArrayList<AltitudeMismatchKernel> kernels;
	private           ArrayList<GroundObjectPattern>    patterns;
	
	// real transient:
	public  transient long                              version = -1;
	private transient HashMap<Double,Integer>           map_index;
	private transient HashMap<String,Integer>           map_index_string;
	
	
	private void writeObject(ObjectOutputStream oos) throws IOException { // ?
		oos.defaultWriteObject();
		oos.writeObject(AltitudeMismatchKernel.getKernelsDirectory());
//		oos.writeObject(kernels);
		oos.writeObject(GroundObjectPattern.getPatternsDirectory());		
//		oos.writeObject(patterns);
//		oos.writeObject(path);
		// ortho_maps is not transient
	}
//	@SuppressWarnings("unchecked")
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
		AltitudeMismatchKernel.setKernelsDirectory((String) ois.readObject());
//		kernels = (ArrayList<AltitudeMismatchKernel>) ois.readObject();
		GroundObjectPattern.setPatternsDirectory((String) ois.readObject());
//		patterns = (ArrayList<GroundObjectPattern>)   ois.readObject();
		
		reindex(); // will recreate map_index, map_index_string
		// lla is not transient
	}
	
	public OrthoMapsCollection(
			String path, // lit file
			OrthoMapsCollection orthoMapsCollection) { // if null will create new, if not null - will update other  
		if (path.endsWith(".list")) {
//			String[] scenes0 = new String[1];
			kernels = new ArrayList<AltitudeMismatchKernel>(); 
			patterns = new ArrayList<GroundObjectPattern>();
			model_regexes = new HashMap<String,ModelRegex>();
			String [] regex_use_p = new String[1];
			String[] paths = getPathsAndScenesFromSourceList(
					path,
					null, // scenes0,
					kernels,       // ArrayList<AltitudeMismatchKernel> kernels,
					patterns,      // ArrayList<GroundObjectPattern>    patterns);
					model_regexes, // ArrayList<ModelRegex>             model_regexes,
					regex_use_p,   // String []                         regex_use) {
					null,          // ArrayList<AffineImport>           affine_import,
					null);         // ArrayList<Affine2Import>          affine2_import) {

			current_model_regex = regex_use_p[0];
			paths = ModelRegex.removeDuplicatesSort(paths);        
			String [] orig_paths = paths.clone();
			boolean [] replaced = new boolean [paths.length];
			if (current_model_regex != null) {
				ModelRegex regex = model_regexes.get(current_model_regex);
				int num_replaced = 0;
				int num_changed = 0;
				if (regex != null) {
					for (int i = 0; i < paths.length;i++) {
						String replacement = regex.replaceByFilter(paths[i]);
						if (replacement != null) {
							paths[i] = replacement; 
							replaced[i] = true;
							num_replaced++;
							if (!replacement.equals(orig_paths[i])) {
								num_changed++;
							}
						}
					}
					System.out.println(String.format("Replacing with regex \"%s\" (%s)",
							regex.name, regex.regex));
					System.out.println(String.format("Replaced %d files (of %d), changed %d.", num_replaced, paths.length,num_changed));
				} else {
					System.out.println("Replacement regex \""+current_model_regex+"\" does not exist");
				}
			}
//			String scenes_path = scenes0[0]; // not used!
			ArrayList<String> new_paths = new ArrayList<String>();
			if (orthoMapsCollection != null) {
				 // 1. update existing maps
				for (int i = 0; i < paths.length; i++) {
					String name = OrthoMap.getNameFromPath(paths[i]);
					int indx = orthoMapsCollection.getIndex(name);
					if (indx >= 0) {
						OrthoMap omap = orthoMapsCollection.ortho_maps[indx];
						omap.orig_image = new FloatImageData(path); // always replace (nothing to preserve
///						omap.dt = omap.orig_image.getDT(); // just in case?
						// see if alt image is already initialized
						if (omap.alt_image != null) {
							FloatImageData alt_image = OrthoMap.getAlt(path);
							if (alt_image != null) {
								omap.alt_image = alt_image;
							} else {
								System.out.println("New altitude file does not exist, keeping old one: "+omap.alt_image.path);
							}
						}
					} else {
						new_paths.add(paths[i]);
					}
				}
			 // 2. add new maps (none will be removed)
				if (!new_paths.isEmpty()) {
					// need to create new maps, increase orthoMapsCollection.ortho_maps, reorder and reindex it
					int num_old_maps = orthoMapsCollection.ortho_maps.length;
					int num_new_maps = new_paths.size();
					ortho_maps = new OrthoMap[num_old_maps+num_new_maps];
					System.arraycopy(
							orthoMapsCollection.ortho_maps, 0,
							ortho_maps, 0,
							num_old_maps);
					int map_index = num_old_maps;
					for (String scene_path: new_paths) {
						ortho_maps[map_index] = new OrthoMap(scene_path); // , scene_path);
						ortho_maps[map_index++].setAffine();
					}
					Arrays.sort(ortho_maps); // mixed old+new, previous scene numbers will no longer be valui
					orthoMapsCollection.ortho_maps = ortho_maps; 
					orthoMapsCollection.reindex();
					
				} else {
					System.out.println("No new scenes added");
				}
				return; // Constructed this should not be used, only the old orthoMapsCollection
			}
			ortho_maps = new OrthoMap[paths.length];
			for (int n = 0; n < ortho_maps.length; n++) {
				ortho_maps[n] = new OrthoMap(paths[n]); // , scene_path);
				ortho_maps[n].setAffine();
			}
			Arrays.sort(ortho_maps);
			reindex();
		} else {
			System.out.println("OrthoMapsCollection(): path should end with \".list\"");
		}
	}
	
	public OrthoMap getMap(String name) {
		int indx = getIndex(name);
		if (indx < 0) return null;
		return ortho_maps[indx];
	}
	
	public static double [][] unityAffine() {
		return new double [][] {{1,0,0},{0,1,0}};
	}
	
	public void updateFiles (
			String path) {
		if (path.endsWith(".list")) {
			new OrthoMapsCollection(path, this); // will update this instead of creating a new instance
		} else {
			System.out.println("updateFiles(): path should end with \".list\"");
		}
	}
	
	// add update kernels, update patterns
	public void updateKernels (
			String path) {
		if (path.endsWith(".list")) {
			kernels = new ArrayList<AltitudeMismatchKernel>(); 
			getPathsAndScenesFromSourceList(
					path,
					null,
					kernels, // ArrayList<AltitudeMismatchKernel> kernels,
					null,    // ArrayList<GroundObjectPattern>    patterns);
					null,    // ArrayList<ModelRegex>             model_regexes,
					null,    // String []                         regex_use) {
					null,    // ArrayList<AffineImport>           affine_import,
					null);   // ArrayList<Affine2Import>          affine2_import) {
		} else {
			System.out.println("updateKernels(): path should end with \".list\"");
		}
	}
	
	public void updatePatterns (
			String path) {
		if (path.endsWith(".list")) {
			patterns = new ArrayList<GroundObjectPattern>();
			getPathsAndScenesFromSourceList(
					path,
					null,
					null,     // ArrayList<AltitudeMismatchKernel> kernels,
					patterns, // ArrayList<GroundObjectPattern>    patterns);
					null,     // ArrayList<ModelRegex>             model_regexes,
					null,     // String []                         regex_use) {
					null,     // ArrayList<AffineImport>           affine_import,
					null);    // ArrayList<Affine2Import>          affine2_import) {
					
		} else {
			System.out.println("updatePatterns(): path should end with \".list\"");
		}
	}
	
	
	static String getSceneDir(String scenes_path, String name) {
		File [] scene_dirs = (new File(scenes_path)).listFiles(); // may contain non-directories, will be filtered by filterScenes
		//scene_dirs[3].listFiles()
		Arrays.sort(scene_dirs);
		for (int i = scene_dirs.length-1; i>=0; i--) {
			if (scene_dirs[i].isDirectory()) {
				String dirname = scene_dirs[i].getName();
				if (name.compareTo(dirname) >= 0) {
					// verify it has needed scene
					File [] segment = scene_dirs[i].listFiles();
					for (int j = 0; j < segment.length; j++) {
						if (segment[j].getName().equals(name)) {
							return segment[j].toString(); 
						}
					}
				}
			}
		}
		return null;
	}
	
	
	
	public int reindex() {
		map_index = new HashMap<Double,Integer>();
		map_index_string = new HashMap<String,Integer>();
		for (int i = 0; i < ortho_maps.length; i++) {
			map_index.put(ortho_maps[i].getTimeStamp(), i);
			map_index_string.put(ortho_maps[i].name,i);
		}
//		updateNumberScenes();
		return ortho_maps.length;
	}
	
	OrthoMap [] getMaps() {
		return ortho_maps;
	}
	
	public String[] getNames() {
		String [] names = new String [ortho_maps.length];
		for (int n = 0; n < ortho_maps.length; n++) {
			names[n] = ortho_maps[n].getName();
		}
		return names;
	}

	public void getAllTemperatures() {
		for (int n = 0; n < ortho_maps.length; n++) {
			ortho_maps[n].getTemperature();
			ortho_maps[n].getTemperatureAndTelemetry();
		}
	}
	
	public void updateLLA() { // make automatic by saving/testing file modification stamp?
		for (int n = 0; n < ortho_maps.length; n++) {
			if (n == 530) {
				System.out.println("updateLLa(): n="+n);
			}
			ortho_maps[n].updateLLA();
			IJ.showStatus("Updating LLA "+n+" of "+ortho_maps.length);
			IJ.showProgress(1.0*n/ortho_maps.length);
		}
		updateAGL();
	}
	
	public void updateAGL() {
		for (int n = 0; n < ortho_maps.length; n++) {
			IJ.showStatus("Updating AGL "+n+" of "+ortho_maps.length);
			ortho_maps[n].agl = Double.NaN;
			ortho_maps[n].agl = ortho_maps[n].getAGL();
			IJ.showProgress(1.0*n/ortho_maps.length);
			
		}		
	}
	
	public void updateNumberScenes() {
		for (int n = 0; n < ortho_maps.length; n++) {
			ortho_maps[n].num_scenes = -1;
			ortho_maps[n].num_scenes = ortho_maps[n].getNumberScenes();
		}		
	}	
	
	public void updateSfmGain() {
		for (int n = 0; n < ortho_maps.length; n++) {
			ortho_maps[n].sfm_gain = Double.NaN;
			ortho_maps[n].sfm_gain = ortho_maps[n].getSfmGain();
		}		
	}	
	
	/*
	public static String[] getPathsFromSorceList(
			String files_list) {
		return getPathsAndScenesFromSourceList(files_list, null);
	}
    */
	public static String[] getPathsAndScenesFromSourceList(
			String files_list,
			String[] scenes0,
			ArrayList<AltitudeMismatchKernel> kernels,
			ArrayList<GroundObjectPattern>    patterns,
			HashMap<String,ModelRegex>        model_regexes,
			String []                         regex_use,
			ArrayList<AffineImport>           affine_import,
			ArrayList<Affine2Import>          affine2_import) {
		List<String> lines;
		List<String> rel_files = new ArrayList<String>(); 
		Path seq_path = Paths.get(files_list);
		try {
			lines = Files.readAllLines(seq_path, StandardCharsets.UTF_8);
			//                lines.stream().forEach(System.out::println);
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
		Path base_path = seq_path.getParent();
		HashMap<String,String> dir_map = new HashMap<String,String>();
		for (String line:lines){
			if (line.split("#").length > 0) {
				String[] tokens = line.split("#")[0].trim().split("[\\s,;=]+");
				if ((tokens.length > 2) && (tokens[0].toUpperCase().equals("SET"))) {
					parse_set:
					{
						for (String dir_name:KEY_DIRS) if (dir_name.equals(tokens[1])) {
							dir_map.put(dir_name,tokens[2]);
							System.out.println("Parsed SET: "+tokens[1]+" in line: "+line);
							break parse_set;
						}
						System.out.println("*********** Unknown SET: "+tokens[1]+" in line: "+line);
					}
				} else if ((tokens.length > 0) && (tokens[0].toUpperCase().equals("KERNEL"))) {
					if ((kernels != null) && (tokens.length > 4)) {
						kernels.add(new AltitudeMismatchKernel(
								tokens[1],
								Integer.parseInt(tokens[2]),
								Double.parseDouble(tokens[3]),
								Double.parseDouble(tokens[4])));
					}
				} else if ((tokens.length > 0) && (tokens[0].toUpperCase().equals("PATTERN"))) {
					if ((patterns != null) && (tokens.length > 4)) {
						patterns.add(new GroundObjectPattern(
								tokens[1],
								Integer.parseInt(tokens[2]),
								tokens[3],
								LocalDateTime.parse(tokens[4])));
					}
				} else if ((tokens.length > 0) && (tokens[0].toUpperCase().equals("REGEX"))) {
					if ((model_regexes != null) && (tokens.length > 4)) {
						model_regexes.put(tokens[1],new  ModelRegex(
								tokens[1],                        // String        name,
								tokens[2],                        //	String        regex,
								LocalDateTime.parse(tokens[3]),   // LocalDateTime start_date,
								LocalDateTime.parse(tokens[4]))); // 	LocalDateTime end_date);
					}
				} else if ((tokens.length > 0) && (tokens[0].toUpperCase().equals("REGEX_USE"))) {
					if ((regex_use != null) && (tokens.length > 1)) {
						regex_use[0] = tokens[1];
					}
				} else if ((tokens.length > 0) && AffineImport.matches(tokens)) {
					if (affine_import != null) {
						affine_import.add(new AffineImport(tokens));
					}
				} else if ((tokens.length > 0) && Affine2Import.matches(tokens)) {
					if (affine2_import != null) {
						affine2_import.add(new Affine2Import(tokens));
					}
					
				} else if ((tokens.length == 1) && (tokens[0].length() > 0)) {
					rel_files.add(tokens[0]);
				}
			}
		}
		if (dir_map.containsKey("rootDirectory")) {
			base_path=base_path.resolve(Paths.get(dir_map.get("rootDirectory")));
			File base_dir = new File(base_path.toString());
		}
		String sourceDirectory = base_path.toString();
		// set sourceDirectory: 
		if (dir_map.containsKey("sourceDirectory")) {
			sourceDirectory=(base_path.resolve(Paths.get(dir_map.get("sourceDirectory")))).toString();
		}
		Path source_path = Paths.get(sourceDirectory);
		//			File source_dir = new File(source_path.toString());
		String [] paths = new String[rel_files.size()];
		for (int i = 0; i < paths.length; i++) {
			paths[i] = (source_path.resolve(Paths.get(rel_files.get(i)))).toString();
		}
		if (dir_map.containsKey("scenesDirectory")) {
			if (scenes0 != null) {
				scenes0[0]=(base_path.resolve(Paths.get(dir_map.get("scenesDirectory")))).toString();
			}
		}
		
		String kernelsDirectory = base_path.toString();
		// set sourceDirectory: 
		if (dir_map.containsKey("kernelsDirectory")) {
			kernelsDirectory=(base_path.resolve(Paths.get(dir_map.get("kernelsDirectory")))).toString();
		}
		AltitudeMismatchKernel.setKernelsDirectory(kernelsDirectory);
		
		String patternsDirectory = base_path.toString();
		// set sourceDirectory: 
		if (dir_map.containsKey("patternsDirectory")) {
			patternsDirectory=(base_path.resolve(Paths.get(dir_map.get("patternsDirectory")))).toString();
		}
		GroundObjectPattern.setPatternsDirectory(patternsDirectory);
		
		return paths;
	}
	
	
	/**
	 * Get rectified bounds of all provided ortho images relative to the origin (vertical
	 * point) of the first one in pixels at the provided zoom level. Center of the first
	 * image correspond to the {0,0} point from which the bounds are counted
	 * @param zoom_level zoom level - 0 corresponds to 1pix=1cm scale, +1 - to 1pix = 0.5cm
	 * @return {{min_x,max_x},{min_y, max_y}} relative to the origin of the first image
	 */
	public int [][] getBoundsPixels(
			int zoom_level, // maps[0] as a reference
			int [] indices) {
		double [][] bounds_meters = getBoundsMeters(indices);
		double pix_size = OrthoMap.getPixelSizeMeters (zoom_level);
		int [][] bounds_pix = new int[2][2];
		for (int n = 0; n < bounds_pix.length; n++) {
			bounds_pix[n][0] = (int) Math.floor(bounds_meters[n][0]/pix_size);
			bounds_pix[n][1] = (int) Math.ceil (bounds_meters[n][1]/pix_size);
		}
		return bounds_pix;
	}
	public int [][] getBoundsPixels(
			int           zoom_level, // maps[0] as a reference
			int []        indices,
			double [][][] affines) {
		double [][] bounds_meters = (affines == null) ? getBoundsMeters(indices):getBoundsMeters(indices,affines);
		double pix_size = OrthoMap.getPixelSizeMeters (zoom_level);
		int [][] bounds_pix = new int[2][2];
		for (int n = 0; n < bounds_pix.length; n++) {
			bounds_pix[n][0] = (int) Math.floor(bounds_meters[n][0]/pix_size);
			bounds_pix[n][1] = (int) Math.ceil (bounds_meters[n][1]/pix_size);
		}
		return bounds_pix;
	}

	public int [][] getBoundsPixels(
			int zoom_level) { // maps[0] as a reference
		return getBoundsPixels(zoom_level, null);
	}
	//double [][][] affines
	/**
	 * Get rectified bounds of all provided ortho images relative to the origin (vertical
	 * point) of the first one in meters
	 * @return {{min_x,max_x},{min_y,max_y}} bounds that include all provided maps
	 *         relative to the origin (vertical point) of the first image
	 */
	/*
	public double [][] getBoundsMeters(){ // maps[0] as a reference
		double [][] bounds = ortho_maps[0].getBoundsMeters(true);
		for (int nmap = 1; nmap < ortho_maps.length; nmap++) {
			double [][] bounds_other = ortho_maps[nmap].getBoundsMeters(true);
			double [] enuOffset = ortho_maps[0].enuOffsetTo(ortho_maps[nmap]);
			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
			for (int n = 0; n < bounds.length; n++) {
				bounds[n][0] = Math.min(bounds[n][0],bounds_other[n][0]+ rd[n]);
				bounds[n][1] = Math.max(bounds[n][1],bounds_other[n][1]+ rd[n]);
			}
		}
		return bounds;
	}
	*/
	public double [][] getBoundsMeters(){ // maps[0] as a reference
		return getBoundsMeters(null);
	}
	/**
	 * Get rectified bounds of all (if indices==null) or selected by indices[] 
	 * provided ortho images relative to the origin (vertical
	 * point) of the first one in meters
	 * @param indices - image indices to process or null to process all images
	 * @return {{min_x,max_x},{min_y,max_y}} bounds that include all provided maps
	 *         relative to the origin (vertical point) of the first image
	 */

	public double [][] getBoundsMeters(int [] indices){ // maps[0] as a reference
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		double [][] bounds = ortho_maps[indices[0]].getBoundsMeters(true);
		for (int imap = 1; imap < indices.length; imap++) {
			int nmap = indices[imap];
			double [][] bounds_other = ortho_maps[nmap].getBoundsMeters(true);
			double [] enuOffset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
			for (int n = 0; n < bounds.length; n++) {
				bounds[n][0] = Math.min(bounds[n][0],bounds_other[n][0]+ rd[n]);
				bounds[n][1] = Math.max(bounds[n][1],bounds_other[n][1]+ rd[n]);
			}
		}
		return bounds;
	}

	public double [][] getBoundsMeters(
			int    [] indices,
			double [][][] affines){ // maps[0] as a reference
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		if (affines == null) {
			affines = new double [ortho_maps.length][][];
			for (int i = 0; i < affines.length; i++) {
				affines[i] = ortho_maps[indices[i]].affine;
			}
		}
		double [][] bounds = ortho_maps[indices[0]].getBoundsMeters(true,affines[0]);
		for (int imap = 1; imap < indices.length; imap++) {
			int nmap = indices[imap];
			double [][] bounds_other = ortho_maps[nmap].getBoundsMeters(true,affines[imap]);
			double [] enuOffset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
			for (int n = 0; n < bounds.length; n++) {
				bounds[n][0] = Math.min(bounds[n][0],bounds_other[n][0]+ rd[n]);
				bounds[n][1] = Math.max(bounds[n][1],bounds_other[n][1]+ rd[n]);
			}
		}
		return bounds;
	}
	
	
	/**
	 * Get rectified overlap bounds of two provided ortho images relative to the origin (vertical
	 * point) of the first one in meters. Use specified affine transforms, not saved with the orto map
	 * @param ref_index - index of the first (reference) map
	 * @param other_index - index of the second (other) map
	 * @param ref_affine - specified affine transform of the reference image (referenced to its vertical_point)
	 * @param other_affine - specified affine transform of the other image (referenced to its vertical_point)
	 * @return {{min_x,max_x},{min_y,max_y}} bounds that include an overlap of the two provided maps
	 *         relative to the origin (vertical point) of the first image.
	 * Returns null if there is no overlap
	 */
	private double [][] getOverlapMeters(
			int ref_index,
			int other_index,
			double [][] ref_affine,
			double [][] other_affine){
		double [][] bounds = ortho_maps[ref_index].getBoundsMeters(true,ref_affine);
		double [][] bounds_other = ortho_maps[other_index].getBoundsMeters(true,other_affine);
		double [] enuOffset = ortho_maps[ref_index].enuOffsetTo(ortho_maps[other_index]);
		double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
		for (int n = 0; n < bounds.length; n++) {
			bounds[n][0] = Math.max(bounds[n][0],bounds_other[n][0]+ rd[n]);
			bounds[n][1] = Math.min(bounds[n][1],bounds_other[n][1]+ rd[n]);
			if (bounds[n][0] > bounds[n][1]) {
				return null;
			}
		}
		return bounds;
	}
	
	/**
	 * Use object coordinates (now metric) on the composite image to
	 * determine corresponding pixel coordinates of the source images
	 * @param objects ArrayList of objects with the scene name and xy coordinates
	 *                in the composite image. Will fill in pixel coordinates of the objects 
	 * @param zoom_level zoom level of the composite image
	 */
	public void reverseRender(
			ArrayList<ObjectLocation> objects,
			int         zoom_level,
			int []      indices){ // null or selected image indices
		int [][] bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
				zoom_level,
				indices); 
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		/*
		int [] indices = new int [ortho_maps.length]; //maybe will get rid of
		for (int i = 0; i < indices.length; i++) {
			indices[i] = i;
		}
		*/
		final int reference_index = (indices == null)?0:indices[0]; 
		for (ObjectLocation ol: objects) {
			int nmap =getIndex(ol.getName());
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(ortho_maps[nmap].getImage().getZoomLevel()); // pix per meter
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [] enu_offset = ortho_maps[reference_index].enuOffsetTo(ortho_maps[nmap]);
			double [] scaled_out_center = { // xy center to apply affine to
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			double [] metric_center = { // back to meters to match objects
					scaled_out_center[0]/scale,
					scaled_out_center[1]/scale};
			double dX = ol.getMetric()[0]-metric_center[0]; // matching dX, dY in renderMulti()
			double dY = ol.getMetric()[1]-metric_center[1];
			
//			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to thje top-left
//			for (int n = 0; n< 2; n++) {
//				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
//				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
//			}
			// Output window size (keep original affine - OK if will not exactly fit)
//			final int ownd_width =  obounds[0][1] - obounds[0][0];
//			final int ownd_height = obounds[1][1] - obounds[1][0];
//			final int ownd_len = ownd_width * ownd_height;
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final double [][] affine = ortho_maps[nmap].affine; // only here use provided
//			final int src_width =      ortho_maps[nmap].getImageData().width;
//			final int src_height =     ortho_maps[nmap].getImageData().height;
//			final float [] src_img =   ortho_maps[nmap].getImageData().data; // FIXME: will not use
			double [] xy_src = { // pixels of the source image
					src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
					src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
			ol.setPixels(xy_src);
		}
	}

	public ImagePlus renderMulti (
			String        title,
			int           eq_mode, // 0 - ignore equalization, 1 - use stored equalization, 2 - calculate equalization 
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			int           temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,    // use for a single pair only
			boolean       show_centers,
			int           zoom_level,
			int []        origin){
		boolean show_2d_histogram = true; // false;
		int num_images =  (indices != null)? indices.length : ortho_maps.length;
		int []     wh = new int[2];
		double [][] centers = new double [(indices !=null)? indices.length: ortho_maps.length][];
		double [][] dmulti = renderMultiDouble (
				null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
				indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				bounds_to_indices, // boolean       bounds_to_indices,
				affines,           // affines,    // double [][][] affines, // null or [indices.length][2][3]
				null,              // double [][]   equalize,
				(eq_mode != 1),    // boolean       ignore_equalize,
				null,              // warp,       // FineXYCorr    warp,,
				zoom_level,        // int         zoom_level,
				wh,                // int []      wh,
				origin,            // int []      origin){ // maps[0] as a reference
				centers);          // double [][] centers)
		
		if ((eq_mode == 2) && (num_images == 2)) {
			boolean [] mask = null;
			if (show_2d_histogram) {
				mask = new boolean [dmulti[0].length];
				//			int hist_min = -140,hist_max=135; // 700,200
				//			int hist_min = -100,hist_max=100; // 700,200
				int hist_min = -350, hist_max=350; // 700,200
				int hist_width= hist_max-hist_min;
				double [] hist = new double [hist_width*hist_width];
				for (int i = 0; i < dmulti[0].length; i++) if (!Double.isNaN(dmulti[0][i]) && !Double.isNaN(dmulti[1][i])){
					int x = (int) Math.round(dmulti[0][i]) - hist_min;
					int y = (int) Math.round(dmulti[1][i]) - hist_min;
					if ((x>=0) && (y>=0) && (x < hist_width)  && (y < hist_width)) {
						int indx = y*hist_width+x;
						hist[indx] += 1.0;
						mask[i] = true;
					}
				}
				ShowDoubleFloatArrays.showArrays(
						hist,
						hist_width,
						hist_width,
						title+"hist_"+hist_min+"_"+hist_max);
			}
			double [] regression = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti[1], // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] inv_regression = PolynomialApproximation.invertRegression(regression);
			double [] regression10 = PolynomialApproximation.getOrthoRegression(
					dmulti[1], // final double []  data_x,
					dmulti[0], // final double []  data_y,
					mask);     // final boolean [] mask)
			/*
			double [] dmulti1 = dmulti[1].clone();
			double [] regression00 = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti[0], // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] regression11 = PolynomialApproximation.getOrthoRegression(
					dmulti[1], // final double []  data_x,
					dmulti[1], // final double []  data_y,
					mask);     // final boolean [] mask)
			*/
			PolynomialApproximation.applyRegression(
					dmulti[1], // dmulti1,       // final double [] data, // clone by caller
					inv_regression); // final double [] regression) {
			/*
			double [] regression01 = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti1, // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] dmulti12 = dmulti[1].clone();
			for (int i = 0; i < dmulti12.length; i++) {
				dmulti12[i] = 2.0*dmulti12[i]+10; 
			}
			double [] regression012 = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti12, // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] dmulti13 = dmulti[1].clone();
			for (int i = 0; i < dmulti13.length; i++) {
				dmulti13[i] = dmulti13[i]+10; 
			}
			double [] regression013 = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti13, // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] dmulti14 = dmulti[1].clone();
			for (int i = 0; i < dmulti14.length; i++) {
				dmulti14[i] = 2*dmulti14[i]; 
			}
			double [] regression014 = PolynomialApproximation.getOrthoRegression(
					dmulti[0], // final double []  data_x,
					dmulti14, // final double []  data_y,
					mask);     // final boolean [] mask)
			double [] regression_single = getYXRegression(
					dmulti[0], // final double []  data_x,
					dmulti[1]); // final double []  data_y,
			*/
			System.out.println();
		}
		
		
		
		String [] map_names = new String[num_images + (((num_images==2))? 1 : 0)];
//		float [][] extra_multi = new float [map_names.length][];
		double [][] extra_multi = new double [map_names.length][];
		for (int n = 0; n <num_images; n++) {
			int mapn= (indices != null)? indices[n] : n;
			map_names[n] = ortho_maps[mapn].getName()+"_"+ortho_maps[mapn].getLocalDateTime().toString().replace("T","_")+"_UTC";
			map_names[n] += String.format(" raw=%7.1f", ortho_maps[mapn].getTemperature());
			extra_multi[n] = dmulti[n];
		}
		if (map_names.length > num_images) { // inefficient way, only for display
			// correct(offset) pixel values relative to multi[0]
			int map0= (indices != null)? indices[0] : 0;
			for (int n = 1; n < num_images; n++) {
				//temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
				int mapn= (indices != null)? indices[n] : n;
				double offs = 0;
				switch (temp_mode) {
				case 1:
					offs = ortho_maps[map0].getAveragePixel()-ortho_maps[mapn].getAveragePixel();
					break;
				case 2:
					offs = ortho_maps[mapn].getTemperature()-ortho_maps[mapn].getAveragePixel()
					-(ortho_maps[map0].getTemperature()-ortho_maps[map0].getAveragePixel());
					break;
				}
//				float foffs= (float) offs;
				for (int i = 0; i < extra_multi[n].length; i++) if (!Double.isNaN(extra_multi[n][i])) {
					extra_multi[n][i]+= offs;
				}
			}
			map_names[num_images] = "Diff 0-1";
			extra_multi[num_images] = new double[extra_multi[0].length];
			for (int i = 0; i < extra_multi[num_images].length; i++) {
				extra_multi[num_images][i] = extra_multi[1][i]-extra_multi[0][i];
			}
		}
		ImageStack stack;
			stack = ShowDoubleFloatArrays.makeStack( 
        		extra_multi,
        		wh[0],
        		wh[1],
        		map_names,
        		false);
        ImagePlus imp = new ImagePlus(title, stack);
        if (show_centers) {
        	PointRoi roi = new PointRoi();
        	for (int i = 0; i < centers.length; i++) {
        		roi.addPoint(centers[i][0],centers[i][1], i+1);
        	}
        	roi.setOptions("label");
        	imp.setRoi(roi);
        }
        imp.show(); // debugging
        return imp;
	}
	
	public static double [] getYXRegression(
			final double []  data_x,
			final double []  data_y) {
		double s0=0,sx=0,sx2=0,sy = 0, sxy=0;
		for (int i = 0; i < data_x.length; i++) {
			double x = data_x[i];
			double y = 2*data_x[i];
			if (!Double.isNaN(x) && !Double.isNaN(y)) {
				s0+= 1.0;
				sx+= x;
				sx2+=x*x;
				sy+= y;
				sxy += x*y;
			}
		}
		double d = (s0*sx2-sx*sx);
		double a = (sxy*s0-sy*sx)/d;
		double b = (sy*sx2-sx*sxy)/d;
		return new double [] {a,b};
	}
	
	/**
	 * Rectify and render multiple images (as slices) matching vert_meters to
	 * their (vert_meters points) provided IMS coordinates
	 * @param maps array of the ortho images 
	 * @param zoom_level zoom level (0 - 1pix=1cm, 1 - 1pix=0.5cm, -1 - 1pix=2cm
	 * @param wh null or int [2], will return {width, height}
	 * @param origin - null or double [2], will return {x0, y0} in pixels
	 * @param centers - null or double[maps.length][] - will return image centers coordinates
	 * @return
	 */
	
	public float [][] renderMulti (
			int mode,    // 0 - regular image, 1 - altitudes, 2 - black/white mask			
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,
			int           zoom_level,
			int []        wh,
			int []        origin, // maps[0] as a reference
			double [][]   centers){
		final boolean use_alt = mode == MODE_ALT;
		final int dbg_x=2783, dbg_y=-5228;
		int [][] bounds;
		if (affines == null) {
			bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
					zoom_level,
					bounds_to_indices? indices: null);
		} else {
			bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
					zoom_level,
					bounds_to_indices? indices: null,
							affines);
		}
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		if (wh != null) {
			wh[0] = width;
			wh[1] = height;
		}
		if (origin != null) {
			origin[0] = -bounds[0][0];
			origin[1] = -bounds[1][0];
		}
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		
		final float [][] fpixels = new float [indices.length][width * height];
		for (int indx = 0; indx < indices.length; indx++) { //:indices) { // = 0; nmap< ortho_maps.length; nmap++) {
			final int findx= indx;
			final int nmap = indices[indx]; // nmap;
			final double [][] affine = (affines !=null) ? affines[indx]: ortho_maps[nmap].affine; // only here use provided
			Arrays.fill(fpixels[findx], Float.NaN);
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
//			final int orig_zoom_level = getOriginalZoomLevel()
			final int orig_zoom_level = use_alt? ortho_maps[nmap].getAlt().getZoomLevel(): ortho_maps[nmap].getImage().getZoomLevel();
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(orig_zoom_level); // pix per meter
//			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true,affine); // keep original bounds
			double [] enu_offset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			final double [] scaled_out_center = { // xy center to apply affine to in output pixels
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			if (centers != null) {
				centers[findx] = scaled_out_center;
			}
			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to the top-left
			for (int n = 0; n< 2; n++) { // output pixels
				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
			}
			// Output window size (keep original affine - OK if will not exactly fit)
			final int ownd_width =  obounds[0][1] - obounds[0][0];
			final int ownd_height = obounds[1][1] - obounds[1][0];
			final int ownd_len = ownd_width * ownd_height;
//			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (false,affine); // using provided affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final int src_width = use_alt? ortho_maps[nmap].getAltData().width: ortho_maps[nmap].getImageData().width;
			final int src_height = use_alt? ortho_maps[nmap].getAltData().height : ortho_maps[nmap].getImageData().height;
			if ((indx==0) && (warp != null)) { // set center from the first image
				warp.setRender(
						zoom_level, // int zoom_lev,
						scaled_out_center[0], // double px0, // in render pixels
						scaled_out_center[1]); // // double py0);
			}
			final float [] src_img = use_alt? ortho_maps[nmap].getAltData().readFData() : ortho_maps[nmap].getImageData().readFData();
			final Rectangle warp_woi =((indx==1) && (warp != null))? warp.getRenderWOI():null; 
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			/*
			boolean tilt_alt = (ground_planes != null) && (ground_planes[indx] != null);
			final float [] src_img = use_alt?
					(tilt_alt? ortho_maps[nmap].getAltData().data.clone(): ortho_maps[nmap].getAltData().data) :
						ortho_maps[nmap].getImageData().data;
			if (tilt_alt) {
				// apply tilt to the source altitudes								
				double [] vert_meters = ortho_maps[nmap].getVertMeters();
				// altitude at top-left pixel;
				final double top_left_alt = 
						ground_planes[indx][2] - vert_meters[0]*ground_planes[indx][0] - vert_meters[1]*ground_planes[indx][1];
				final double [] tilt_XY = {ground_planes[indx][0]/src_scale, ground_planes[indx][1]/src_scale};
				for (int ithread = 0; ithread < threads.length; ithread++) {
					threads[ithread] = new Thread() {
						public void run() {
							for (int nPix = ai.getAndIncrement(); nPix < src_img.length; nPix = ai.getAndIncrement()) {
								int py = nPix / src_width;
								int px = nPix % src_width;
								src_img[nPix] -= (top_left_alt + px * tilt_XY[0] + py * tilt_XY[1]);
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
				ai.set(0);
			}
			 */
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < ownd_len; nPix = ai.getAndIncrement()) {
							int opX = nPix % ownd_width + obounds[0][0]; // absolute output pX, pY
							int opY = nPix / ownd_width + obounds[1][0];
							double dX = (opX - scaled_out_center[0]) /scale; // in meters
							double dY = (opY - scaled_out_center[1]) /scale;
							double [] xy_src = { // pixels of the source image
									src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
									src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
							// limit to the source image
							if ((((int) opX)==dbg_x) && (((int) opY)==dbg_y)) {
								System.out.println("opX="+opX+", opy="+opY);
							}
							if ((warp_woi != null) && (warp_woi.contains(opX,opY))) {
								double [] dxy = warp.getWarp(opX,opY);
								xy_src[0] += dxy[0];
								xy_src[1] += dxy[1];
							}
							
							if  ((xy_src[0] >= 0) && (xy_src[0] < (src_width-1)) &&
									(xy_src[1] >= 0) && (xy_src[1] < (src_height-1))) { 
								int [] ixy_src = {(int) Math.floor(xy_src[0]), (int)Math.floor(xy_src[1]) };
								double [] kxy = {xy_src[0]-ixy_src[0], xy_src[1]-ixy_src[1]}; 
								int indx00 = ixy_src[0] + ixy_src[1] * src_width; 
								double d00 = src_img[indx00];
								if (!Double.isNaN(d00)) {
									double d01 = src_img[indx00 + 1];
									double d10 = src_img[indx00 + src_width];
									double d11 = src_img[indx00 + src_width + 1];
									double d = d00*(1.0 - kxy[0])*(1.0 - kxy[1])+
											d01*       kxy[0] *(1.0 - kxy[1])+
											d10*(1.0 - kxy[0])*       kxy[1]+
											d11*       kxy[0] *       kxy[1];
									fpixels[findx][opX + opY*width] = (float) d;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return fpixels;
	}
	
	
	public double [][] renderMultiDouble ( // should work with just a single image (indice.length ==1)?
			double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][] for old way alt
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			double [][][] affines, // null or [indices.length][2][3]
			double [][]   equalize_in,
			boolean       ignore_equalize,
			FineXYCorr    warp,
			int           zoom_level,
			int []        wh,
			int []        origin, // maps[0] as a reference
			double [][]   centers){
		boolean updateStatus = true;
		boolean use_alt = ground_planes != null;
		final int dbg_x=707, dbg_y=-615;
		int [][] bounds;
		if (affines == null) {
			bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
					zoom_level,
					bounds_to_indices? indices: null);
		} else {
			bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
					zoom_level,
					bounds_to_indices? indices: null,
							affines);
		}
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		if (wh != null) {
			wh[0] = width;
			wh[1] = height;
		}
		if (origin != null) {
			origin[0] = -bounds[0][0];
			origin[1] = -bounds[1][0];
		}
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		
		final double [][] dpixels = new double [indices.length][width * height];
		for (int indx = 0; indx < indices.length; indx++) { //:indices) { // = 0; nmap< ortho_maps.length; nmap++) {
			if (updateStatus) IJ.showStatus("Preparing slice "+indx+" of "+indices.length);
			final int findx= indx;
			final int nmap = indices[indx]; // nmap;
			final double [][] affine = (affines !=null) ? affines[indx]: ortho_maps[nmap].affine; // only here use provided
			final double [] equalize = ignore_equalize ? null:((equalize_in != null) ? equalize_in[indx] : ortho_maps[nmap].equalize);
			Arrays.fill(dpixels[findx], Float.NaN);
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
			final int orig_zoom_level = use_alt? ortho_maps[nmap].getAlt().getZoomLevel(): ortho_maps[nmap].getImage().getZoomLevel();
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(orig_zoom_level); // pix per meter
//			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(ortho_maps[nmap].orig_zoom_level); // pix per meter
		// metric bounds of the rectified image relative to its origin
///			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true,affine); // keep original bounds
			double [] enu_offset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			final double [] scaled_out_center = { // xy center to apply affine to in output pixels
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			if (centers != null) {
				centers[findx] = scaled_out_center;
			}
			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to the top-left
			for (int n = 0; n< 2; n++) { // output pixels
				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
			}
			// Output window size (keep original affine - OK if will not exactly fit)
			final int ownd_width =  obounds[0][1] - obounds[0][0];
			final int ownd_height = obounds[1][1] - obounds[1][0];
			final int ownd_len = ownd_width * ownd_height;
///			final double [][] affine = (affines !=null) ? affines[indx]: ortho_maps[nmap].affine; // only here use provided
///			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (false,affine); // using provided affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final int src_width = use_alt? ortho_maps[nmap].getAltData().width: ortho_maps[nmap].getImageData().width;
			final int src_height = use_alt? ortho_maps[nmap].getAltData().height : ortho_maps[nmap].getImageData().height;
//			final float [] src_img = use_alt? ortho_maps[nmap].getAltData().data : ortho_maps[nmap].getImageData().data;
			if ((indx==0) && (warp != null)) { // set center from the first image
				warp.setRender(
						zoom_level, // int zoom_lev,
						scaled_out_center[0], // double px0, // in render pixels
						scaled_out_center[1]); // // double py0);
			}
			boolean tilt_alt = (ground_planes != null) && (ground_planes[indx] != null);
			final float [] src_img = use_alt?
					(tilt_alt? ortho_maps[nmap].getAltData().readFData().clone(): ortho_maps[nmap].getAltData().readFData()) :
						ortho_maps[nmap].getImageData().readFData();
			
			final Rectangle warp_woi =((indx==1) && (warp != null))? warp.getRenderWOI():null; 
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			if (tilt_alt) {
				// apply tilt to the source altitudes								
				double [] vert_meters = ortho_maps[nmap].getVertMeters();
				// altitude at top-left pixel;
				final double top_left_alt = 
						ground_planes[indx][2] - vert_meters[0]*ground_planes[indx][0] - vert_meters[1]*ground_planes[indx][1];
				final double [] tilt_XY = {ground_planes[indx][0]/src_scale, ground_planes[indx][1]/src_scale};
				for (int ithread = 0; ithread < threads.length; ithread++) {
					threads[ithread] = new Thread() {
						public void run() {
							for (int nPix = ai.getAndIncrement(); nPix < src_img.length; nPix = ai.getAndIncrement()) {
								int py = nPix / src_width;
								int px = nPix % src_width;
								src_img[nPix] -= (top_left_alt + px * tilt_XY[0] + py * tilt_XY[1]);
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
				ai.set(0);
			}
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < ownd_len; nPix = ai.getAndIncrement()) {
							int opX = nPix % ownd_width + obounds[0][0]; // absolute output pX, pY
							int opY = nPix / ownd_width + obounds[1][0];
							double dX = (opX - scaled_out_center[0]) /scale; // in meters
							double dY = (opY - scaled_out_center[1]) /scale; // in meters, Y-down
							double [] xy_src = { // pixels of the source image
									src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
									src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
							// limit to the source image
							if ((((int) opX)==dbg_x) && (((int) opY)==dbg_y)) {
								System.out.println("opX="+opX+", opy="+opY);
							}
							if ((warp_woi != null) && (warp_woi.contains(opX,opY))) {
								double [] dxy = warp.getWarp(opX,opY);
								xy_src[0] += dxy[0];
								xy_src[1] += dxy[1];
							}
							if  ((xy_src[0] >= 0) && (xy_src[0] < (src_width-1)) &&
									(xy_src[1] >= 0) && (xy_src[1] < (src_height-1))) { 
								int [] ixy_src = {(int) Math.floor(xy_src[0]), (int)Math.floor(xy_src[1]) };
								double [] kxy = {xy_src[0]-ixy_src[0], xy_src[1]-ixy_src[1]}; 
								int indx00 = ixy_src[0] + ixy_src[1] * src_width; 
								double d00 = src_img[indx00];
								if (!Double.isNaN(d00)) {
									double d01 = src_img[indx00 + 1];
									double d10 = src_img[indx00 + src_width];
									double d11 = src_img[indx00 + src_width + 1];
									double d = d00*(1.0 - kxy[0])*(1.0 - kxy[1])+
											d01*       kxy[0] *(1.0 - kxy[1])+
											d10*(1.0 - kxy[0])*       kxy[1]+
											d11*       kxy[0] *       kxy[1];
									if (equalize != null) {
										d = equalize[0] * d + equalize[1];
									}
									dpixels[findx][opX + opY*width] = d;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return dpixels;
	}

	
	public int getIndex(String sindx) {
		Integer indx = map_index_string.get(sindx);
		if (indx == null) {
			return -1;
		}
		return indx;
	}
	
	/*
	public float [][] getPaddedPairGPU(
			String [] spair,
			int       zoom_lev){
		int [] pair = new int [spair.length];
		for (int i = 0; i < pair.length; i++) {
			pair[i] = getIndex(spair[i]);
		}
		return getPaddedPairGPU(
				pair, // int [] pair,
				zoom_lev); // int    zoom_lev,
	}

	private float [][] getPaddedPairGPU(
			int [] pair,
			int    zoom_lev){
		float [][] gpu_pair_img = new  float [2][];
		for (int n = 0; n < pair.length; n++) {
			gpu_pair_img[n] = ortho_maps[pair[n]].getPaddedGPU (
							zoom_lev); // int zoom_level,
		}
		return gpu_pair_img;
	}
	*/
	
    
	public PairwiseOrthoMatch SpiralMatch (
			CLTParameters    clt_parameters,
			double           frac_remove, //  =        0.25
			double           metric_error,
			boolean          pmtch_use_affine,
			double           max_std,     // maximal standard deviation to limit center area  
			double           min_std_rad, // minimal radius of the central area (if less - fail)
			double           rad_fraction,
			double           max_tile_rad, //  = 30;			
			double           fill_fraction,
			double           fill_fraction_final,
			double           ease_nosfm,
			int []           gpu_pair,
			double [][][]    affines_init,  // here in meters, relative to vertical points
			int              zoom_lev,
			double           pix_step,
			double           pix_range,
			double           good_rms,
			double           max_rms,
			int              num_tries, //  = 5
			int              min_overlap, //  = 5
			boolean          ignore_rms,
			double []        max_rms_iter, //  = {1.0, 0.6};//
			double           overlap,
			double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
			double           pull_tilt,        // > 0
			double           pull_scale,       // = 0
			int              debugLevel){
		double [][] affine1 = new double [][] {affines_init[1][0].clone(),affines_init[1][1].clone()};
		double [][][] affines = new double [][][] {affines_init[0],affine1};
		boolean  show_vf = false;
		boolean  batch_mode = true;
		double [][] ground_planes = null; // null or double[2] - will return ground planes:
		double pix_size = OrthoMap.getPixelSizeMeters (zoom_lev);
		int nx = 0, ny=0; // number of steps (pix_step size) in hor and vert directions (both pos and neg)
		int nabs = (int) Math.ceil(pix_range/pix_step);
		PairwiseOrthoMatch 	pairwiseOrthoMatch = new PairwiseOrthoMatch(
				affines[1], //  double [][] affine,
				null, // double [][] jtj,
				Double.NaN, // double rms,
				zoom_lev, // int zoom_lev);
				overlap); // double overlap);
//		double           max_std =     0.5;      // maximal standard deviation to limit center area  
//		double           min_std_rad = 2.0;  // minimal radius of the central area (if less - fail)
		int best_nx = -1, best_ny = -1;
		PairwiseOrthoMatch best_pom = null;
		while ((Math.abs(nx) <= nabs) && (Math.abs(ny) <= nabs)) {
			for (int i = 0; i < affines_init.length; i++) {
				System.arraycopy(affines_init[1][i], 0, affine1[i], 0, affine1[i].length);
			}
			affine1[0][2] += pix_step * pix_size * nx;
			affine1[1][2] += pix_step * pix_size * ny;
			pairwiseOrthoMatch.rms = Double.NaN;
			
			correlateOrthoPair(
					clt_parameters,  // CLTParameters    clt_parameters,
					pairwiseOrthoMatch, //PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
					min_overlap,     // 			int              min_overlap,
					max_std,         // double           max_std,      // maximal standard deviation to limit center area  
					min_std_rad,     // double           min_std_rad,  // minimal radius of the central area (if less - fail)
					frac_remove,     // double           frac_remove, //  =        0.25
					metric_error,    // 			double           metric_error,
					ignore_rms,      // boolean ignore_prev_rms,
					num_tries,       //  = 5int              num_tries, //  = 5
					false,           // boolean          calc_warp, (will return null if false)
					batch_mode,      // boolean          batch_mode,
					gpu_pair,        // String []        gpu_spair,
					affines,         // double [][][]    affines, // on top of GPS offsets
					null,            // woi,             // Rectangle        woi,
					zoom_lev,        // int              zoom_lev,
					show_vf,         // boolean show_vf,
					ground_planes,   // double [][]      ground_planes, // null or double[2] - will return ground planes
					rad_fraction,    // double           rad_fraction,
					max_tile_rad,    // double           max_tile_rad, //  = 30;
					fill_fraction,   // double           fill_fraction,
					fill_fraction_final, // double           fill_fraction_final,
					ease_nosfm,      // double           ease_nosfm,
					max_rms_iter, // double []        max_rms_iter, //  = {1.0, 0.6};//
					pull_skew,        // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
					pull_tilt,        // double           pull_tilt,        // > 0
					pull_scale,       // double           pull_scale,       // = 0
					debugLevel-4);     // final int        debugLevel)
			pairwiseOrthoMatch.setAffine(affines[1]); // modified by correlateOrthoPair
			if (debugLevel > -4) {
				System.out.println(String.format("SpiralMatch(): %3d-%3d nx = %3d, ny=%3d, RMSE=%8.6f",
						gpu_pair[0], gpu_pair[1], nx,ny,pairwiseOrthoMatch.rms)); // if NaN - provide reason
			}
			if (!Double.isNaN(pairwiseOrthoMatch.rms) && ((best_pom == null) || !(best_pom.rms <= pairwiseOrthoMatch.rms))) {
				pairwiseOrthoMatch.nxy = new int [] {nx,ny};
				best_pom = pairwiseOrthoMatch.clone();
				best_nx = nx;
				best_ny = ny;
//				System.out.println("best_pom.rms = "+best_pom.rms);
			}
			if (pairwiseOrthoMatch.rms < good_rms) {
				break;
			}
			// update nx, ny
			if ((nx >   ny) && (nx > -ny)){
				ny++;
			} else if ((nx <=  ny) && (nx > -ny)){
				nx--;
			} else if ((ny <= -nx) && (ny >  nx)){
				ny--;
			} else {
				nx++;
			}
		}
		
		if (pairwiseOrthoMatch.rms < good_rms) { // immediately OK
			best_pom.ok = true;
			if (debugLevel > -4) {
				System.out.println("SpiralMatch(): best RMSE="+best_pom.rms+" < "+good_rms+
						" for nx = "+best_nx+", ny="+best_ny+", using it.");
			}
			return best_pom; // pairwiseOrthoMatch; // pairwiseOrthoMatch.affine will have adjusted affine[1]
		}
		if ((best_pom != null) && (best_pom.rms <= max_rms)) {
			if (debugLevel > -4) {
				System.out.println("SpiralMatch(): best RMSE="+best_pom.rms+" < "+max_rms+
						" for nx = "+best_nx+", ny="+best_ny+", using it.");
			}
			best_pom.ok = true;
			return best_pom;
		}
		// delegating too large rms to the caller (it will have best_pom.ok==false
		if ((debugLevel > -4) && ((best_pom == null) || Double.isNaN(best_pom.rms))){
			System.out.println("SpiralMatch(): Failed to find a good match candidate. Best RMSE="+
			((best_pom != null)? best_pom.rms:"N/A")+", max_rms= "+max_rms+"");
		}
		return best_pom; // null; // pairwiseOrthoMatch.affine will have adjusted affine[1]
	}
	
	
	public FineXYCorr correlateOrthoPair(
			CLTParameters    clt_parameters,
			PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed
			int              min_overlap,
			double           max_std,      // maximal standard deviation to limit center area  
			double           min_std_rad,  // minimal radius of the central area (if less - fail)
			double           frac_remove, //  =        0.25
			double           metric_error,
			boolean          ignore_prev_rms,
			int              num_tries, //  = 5
			boolean          calc_warp,
			boolean          batch_mode,
			int []           gpu_pair,
			double [][][]    affines,  // here in meters, relative to vertical points
			Rectangle        woi,
			int              zoom_lev,
			boolean          show_vf,
			double [][]      ground_planes, // null or double[2] - will return ground planes:
			double           rad_fraction,
			double           max_tile_rad, //  = 30;
			double           fill_fraction,
			double           fill_fraction_final,
			double           ease_nosfm,
			double []        max_rms_iter, //  = {1.0, 0.6};//
			double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
			double           pull_tilt,        // > 0
			double           pull_scale,       // = 0
			int              debugLevel){
		double vf_drop_big = 0.9; // 0.5? If vector_field threshold drops more, allow RMS to increase
//		double rad_fraction =        0.5; // center circle radius fraction of 0.5* min(width, height) in tiles
//		double fill_fraction =       0.25; // should be populated not less than this
//		double fill_fraction_final = 0.4; // should be populated not less than this
//		double ease_nosfm =          2.0; // ease metric_error when no SfM gain == 0;
//		double max_tile_rad = 30;
		double center_radius_change = 1.5; // if increases by more than this, disregard RMSE worsening
		boolean show_lma_dbg = !batch_mode && (debugLevel > 1); 
		if (woi == null) {
			woi = new Rectangle();
		}
		int min_tiles_overlap = min_overlap/GPUTileProcessor.DTT_SIZE/GPUTileProcessor.DTT_SIZE;
	 	if (pairwiseOrthoMatch != null) {
	 		pairwiseOrthoMatch.rms = Double.NaN; // mark as failed
	 	}
		boolean show_gpu_img = true; // (debugLevel > 1);
		boolean show_tile_centers = false; // true; // (debugLevel > 1);
		if (!batch_mode) {
			debugLevel = 0;
		} else {
			show_gpu_img =      false; // (debugLevel > 1);
			show_tile_centers = false; // true; // (debugLevel > 1);
		}
    	boolean show_vector_field = show_vf || (!batch_mode && (debugLevel>-1)); // true;
		double [][] bounds_overlap_meters = getOverlapMeters(
				gpu_pair[0],  // int ref_index,
				gpu_pair[1],  // int other_index)
				affines[0],   // double [][] ref_affine,
				affines[1]);  // double [][] other_affine
				
		if ((bounds_overlap_meters == null) || (bounds_overlap_meters[0] == null) || (bounds_overlap_meters[1] == null)) {
			if (debugLevel > -4) {
				System.out.println("correlateOrthoPair(): no overlap");
			}
			return null;
		}
		double pix_size = OrthoMap.getPixelSizeMeters (zoom_lev);
	    int [] overlap_wh_pixel = new int [2];
	    for (int i = 0; i < 2; i++) {
	    	overlap_wh_pixel[i] = (
	    			(int) Math.ceil(bounds_overlap_meters[i][1]/pix_size))
	    			- ((int) Math.floor(bounds_overlap_meters[i][0]/pix_size));
	    }
		// convert to pixels,shift top-left to [0,0] (remember offsets, limit w,h,
		// change to pixels last, remember TL in meters?
		// keep center where it was
		// {bounds_overlap_meters[0][0],bounds_overlap_meters[1][0],
		double [] enuOffset = ortho_maps[gpu_pair[0]].enuOffsetTo(ortho_maps[gpu_pair[1]]);
		double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
		double [][] tlo_rect_metric = new double [2][2]; // top-left of overlap referenced to it's own vertical point (subtract!)
		tlo_rect_metric[0][0] = bounds_overlap_meters[0][0]; // relative to ref vert_meters
		tlo_rect_metric[0][1] = bounds_overlap_meters[1][0]; // vert_meters
		tlo_rect_metric[1][0] = bounds_overlap_meters[0][0] - rd[0];  // relative to other vert_meters
		tlo_rect_metric[1][1] = bounds_overlap_meters[1][0] - rd[1];

		double [][] tlo_src_metric = new double[tlo_rect_metric.length][2]; // relative to it's own vert_meters
		for (int n=0; n <tlo_src_metric.length; n++) {
			for (int i = 0; i < 2; i++) { // subtracting tl_rect_metric[n] (-1)
				tlo_src_metric[n][i] =
						tlo_rect_metric[n][0] * affines[n][i][0] +
						tlo_rect_metric[n][1] * affines[n][i][1] + affines[n][i][2];
			}
		}
		/// referenced to top-left pixel of the gpu image
		double [][] tlo_source_pixel = new double[tlo_src_metric.length][2];
		for (int n=0; n <tlo_source_pixel.length; n++) {
			for (int i = 0; i < 2; i++) {
				tlo_source_pixel[n][i] = (tlo_src_metric[n][i] + ortho_maps[gpu_pair[n]].getVertMeters()[i])/pix_size;
			}
		}
		
		double [][][] affines_gpu = new double [affines.length][2][3]; // relative to top left corners, in pixels
		float [][] gpu_pair_img = new  float [2][];
		for (int n = 0; n < gpu_pair.length; n++) {
			for (int i = 0; i < 2; i++) {
				for (int j = 0; j < 2; j++) {
					affines_gpu[n][i][j] = affines[n][i][j]; 
				}
				affines_gpu[n][i][2] = tlo_source_pixel[n][i];
			}
			gpu_pair_img[n] = ortho_maps[gpu_pair[n]].getPaddedGPU (zoom_lev); // int zoom_level,
		}
		boolean invert_second = (debugLevel > 1000);
		if (debugLevel > 10) {
			return null;
		}
		if (invert_second) {
			for (int i = 0; i < gpu_pair_img[1].length; i++) {
				gpu_pair_img[1][i]= -gpu_pair_img[1][i];
			}
		}
		if (show_gpu_img) {
			String [] dbg_titles = {ortho_maps[gpu_pair[0]].getName(),ortho_maps[gpu_pair[1]].getName()};
    		ShowDoubleFloatArrays.showArrays(
    				gpu_pair_img,
    				OrthoMap.gpu_width,
    				OrthoMap.gpu_height,
    				true,
    				"gpu_img",
    				dbg_titles);

		}
		woi.width =  overlap_wh_pixel[0];
		woi.height = overlap_wh_pixel[1]; // = new Rectangle(0, 0, overlap_wh_pixel[0], overlap_wh_pixel[1]);
		if (woi.width * woi.height < min_overlap) {
		 	if (debugLevel > -4) {
		 		System.out.println("correlateOrthoPair(): too small overlap: "+
		 				woi.width+" * "+ woi.height+" = "+(woi.width * woi.height)+" < "+min_overlap+"");
		 	}
		 	return null;
		}
		if (woi.width > OrthoMap.gpu_width) {
			if (debugLevel > -3) {
				System.out.println("correlateOrthoPair() correlation woi.width="+woi.width+" > gpu_width="+OrthoMap.gpu_width+". Truncating.");
			}
			woi.width = OrthoMap.gpu_width;
		}
		if (woi.height > OrthoMap.gpu_height) {
			if (debugLevel > -3) {
				System.out.println("correlateOrthoPair() correlation woi.height="+woi.height+" > gpu_height="+OrthoMap.gpu_height+". Truncating.");
			}
			woi.height = OrthoMap.gpu_height;
		}
		
		double tile_rad = rad_fraction * 0.5*Math.min(woi.width, woi.height)/GPUTileProcessor.DTT_SIZE;
		tile_rad = Math.min(tile_rad, max_tile_rad); 
		int min_tiles_rad = (int) (fill_fraction * 4 * tile_rad * tile_rad);
		
		final int  gpu_width = OrthoMap.gpu_width; // static
		final int  gpu_height = OrthoMap.gpu_height; // static
		int tilesX = gpu_width/GPUTileProcessor.DTT_SIZE; 
    	int tilesY =  gpu_height/GPUTileProcessor.DTT_SIZE;
    	Rectangle tile_woi = scaleRectangle (woi, GPUTileProcessor.DTT_SIZE);
		// uses fixed_size gpu image size
		TpTask [][] tp_tasks = new TpTask [2][];
		
//		int num_tries = 5;
		double [] prev_rms = {Double.NaN,Double.NaN};
		double rel_improve = 1E-3;
		double [][] elevations = new double [tp_tasks.length][];
		double [][] ground_planes_metric = new double [tp_tasks.length][];
		boolean [][] ground_planes_masks = new boolean[2][];
		double    initial_above = 3; // m
		double    initial_below = 5; //3; // m
		int       num_refine =    3;
		double    frac_above =    0.5; // 0.3;
		double    frac_below =    0.01; // 0.1;
		double [][] jtj = null;
		double [] rms =      {Double.NaN, Double.NaN};
		double [] rms_pure = {Double.NaN, Double.NaN};
		double last_center_radius = 0;
		double previous_center_radius = 1.0;
		OrthoPairLMA  orthoPairLMA = null;
		double [][][] vector_field = new double[gpu_pair.length][][];
		double [][]   tile_centers = null;
		double [] ground_planes_weight = null;	
		double previous_vf_threshold = Double.POSITIVE_INFINITY;
		double vf_threshold = 0;
		for (int ntry = 0; ntry < num_tries; ntry++) {
			if (!batch_mode) {
				if (debugLevel>-3) {
					System.out.println("correlateOrthoPair(): ntry="+ntry);
				}
			}
			String dbg_suffix = batch_mode? null: String.format("_%02d", ntry);
			vector_field = 
					ComboMatch.rectilinearVectorField(//rectilinearCorrelate_TD( // scene0/scene1
							clt_parameters,      // final CLTParameters          clt_parameters,
							gpu_pair_img,        // final float  [][]            fpixels, // to check for empty
							gpu_width,           // final int                    img_width,
							woi,                 // Rectangle                    woi, // if null, use full GPU window
							affines_gpu,         // final double [][][]          affine,  // [2][2][3] affine coefficients to translate common to 2 images
							tp_tasks,            // TpTask [][]                  tp_tasks_o,
							batch_mode,          // final boolean                batch_mode,
							dbg_suffix,          //  final String                 dbg_suffix, // for image_names
							debugLevel);         // final int                    debugLevel);
			if (tp_tasks[0].length < min_tiles_overlap) {
				if (pairwiseOrthoMatch != null) {
					pairwiseOrthoMatch.rms = Double.NaN; // mark as failed
				}
				if (debugLevel > -4) {
					System.out.println("correlateOrthoPair(): too small non-null overlap: "+
							tp_tasks[0].length +" < "+min_tiles_overlap+"");
				}
				return null;
			}
			
			// get elevations
//			double ease_nosfm = 3.0;
			int zoom_lev_tiles = zoom_lev-3;
			ground_planes_weight = null;
			for (int num_elevations = 0; num_elevations < 1; num_elevations++) {
				for (int n = 0; n < gpu_pair.length; n++) {
					double sfm_gain = ortho_maps[gpu_pair[n]].getSfmGain();
					double metric_error_adj = metric_error;
					if (sfm_gain == 0.0) {
						metric_error_adj *= ease_nosfm;
						if (debugLevel > -3) {
							System.out.println("SfM gain == 0 for scene #"+gpu_pair[n]+
									", icreasing metric_error to "+metric_error_adj);
						}
					}
					elevations[n] = ortho_maps[gpu_pair[n]].getTileElevations(
							zoom_lev, // final int zoom_level,
							tp_tasks[n], // final TpTask [] tp_tasks, 
							tilesX, // final int tilesX,
							tilesY, // final int tilesY,
							debugLevel); // final int debugLevel)
					String debug_title = (show_vector_field && (ntry==0))? (ortho_maps[gpu_pair[n]].getName()+"_plane"+dbg_suffix):null;
					ground_planes_metric[n] = ortho_maps[gpu_pair[n]].getPlaneMeters (
							zoom_lev_tiles, // int       zoom_lev, // of the data (3 levels down for tiles)
							elevations[n],  // double [] elev,
							tilesX,         // int       width,
							initial_above,  // double    initial_above, // from average
							initial_below,  // double    initial_below, // from average, // positive value
							num_refine,     // int       num_refine,
							frac_above,     // double    frac_above,
							frac_below,     // double    frac_below,
							null, // 			double [][] tile_plane_elev, // null or double[2][] will return per-tile elevations
							debug_title);   // String    debug_title)
					ground_planes_masks[n] = ortho_maps[gpu_pair[n]].removeHighElevationMismatch (
							zoom_lev_tiles,          // int        zoom_lev, // of the data (3 levels down for tiles)
							elevations[n],           // double []  elev,
							tilesX, // int        width,
							ground_planes_metric[n], // double []  plane_metric, // tiltx,tilty, offs - in meters
							metric_error_adj);           // double     metric_error);
				}
				ground_planes_weight = new double[ ground_planes_masks[0].length];
				int num_left = 0; // , num_was = tp_tasks[0].length;
				for (int i = 0; i < ground_planes_weight.length; i++) {
					if (ground_planes_masks[0][i] && ground_planes_masks[1][i]) {
						num_left++;
						ground_planes_weight[i] = 1.0;
					}
				}
				if (debugLevel > -3) {
					System.out.println("correlateOrthoPair(): left "+num_left+" tiles (was "+tp_tasks[0].length+" before filtering). metric_error="+metric_error);
					System.out.println();
				}
				if (num_left < tp_tasks[0].length/4) {
					num_elevations--;
					if (debugLevel > -4) {
						System.out.println("too few tiles remain, try again. metric_error="+metric_error);
					}
					pairwiseOrthoMatch.rms = Double.NaN; // failed
					return null;
				}
				if (num_left < min_tiles_overlap) {
					if (pairwiseOrthoMatch != null) {
						pairwiseOrthoMatch.rms = Double.NaN; // mark as failed
					}
					if (debugLevel > -4) {
						System.out.println("correlateOrthoPair(): too small non-null, filtered by elevations overlap: "+
								num_left +" < "+min_tiles_overlap+"");
					}
					return null;
				}
				
			}
			
	    	double max_err=              7.0;
	    	int num_bins =            1000;
	    	boolean ignore_strength = false;
	    	//double  frac_remove =        0.15;
	    	double [][] vf_error2 = new double [vector_field.length][]; // may be able to skip [0]
	    	double [][][] vector_field_bkp = show_vector_field? new double[vector_field.length][][]:null;
	    	if (vector_field_bkp != null) {
	    		for (int i = 0; i < vector_field_bkp.length; i++) {
	    			vector_field_bkp[i] = vector_field[i].clone(); 
	    		}
	    	}
	    	for (int nvf_mode = 0; nvf_mode < vector_field.length; nvf_mode++) {
	    		vf_error2[nvf_mode] = getVectorFieldError2(
	    				vector_field[nvf_mode], // double [][] vector_field,
	    			    tilesX,                 // int         width, // tilesX
	    			    tile_woi);              // Rectangle   twoi); // in tiles
	    		double threshold = filterVectorField(
	    				vector_field[nvf_mode], // final double [][] vector_field,
	    				vf_error2[nvf_mode],    // final double []   vf_error2,
	    				frac_remove,            // final double      frac_remove,
	    				ignore_strength,        // final boolean     ignore_strength,
	    				num_bins,               // final int         num_bins,
	    				max_err,                // final double      max_err,
	    				tilesX, // final int         width, // tilesX
	    				tile_woi);              //final Rectangle   twoi); // in tiles
	    		if (nvf_mode == 0) {
	    			vf_threshold = threshold; // if it drops significantly - ignore rms increase
	    		}
				if (debugLevel>-3) {
					System.out.println("vector_field layer= "+nvf_mode+" error threshold = "+threshold+" pix.");
				}
	    	}
	    	
	    	// TODO: use crop, keep (0,0) (woi.x+woi.width, woi.y+woi.height 
			if (vector_field_bkp != null) { // show_vector_field) {
				int dbg_width =  tile_woi.x+tile_woi.width;
				int dbg_height = tile_woi.y+tile_woi.height;
				double [][][][] vf_all = {vector_field_bkp,vector_field};
				double [][] dbg_vf = new double [8 * vector_field.length+5][dbg_width * dbg_height];
				String [] dbg_titles = new String[dbg_vf.length];
				String [] prefix= {"single","single_filtered","neibs","neibs_filtered"};
				for (int n = 0; n < 2*vector_field.length; n++) {
					dbg_titles [4*n+0] = prefix[n]+"-vx";	
					dbg_titles [4*n+1] = prefix[n]+"-vy";	
					dbg_titles [4*n+2] = prefix[n]+"-str";	
					dbg_titles [4*n+3] = prefix[n]+"-err";	
				}
				dbg_titles [8 * vector_field.length + 0] = "mask-elev";
				dbg_titles [8 * vector_field.length + 1] = "elev-0";
				dbg_titles [8 * vector_field.length + 2] = "elev-1";
				dbg_titles [8 * vector_field.length + 3] = "mask-0";
				dbg_titles [8 * vector_field.length + 4] = "mask-1";
						
				for (int i = 0; i < dbg_vf.length; i++) {
					Arrays.fill(dbg_vf[i], Double.NaN);
					
				}
				for (int l = 0; l<dbg_vf[0].length; l++) { // local tile
					int lx = l % dbg_width;
					int ly = l / dbg_width;
					int t = lx + ly * tilesX; // full width
					for (int n = 0; n < vector_field.length; n++) {
						for (int m = 0; m < vf_all.length; m++) {
							if (vf_all[m][n][t] != null) {
								for (int k = 0; k < 3; k++) {
									dbg_vf[m*4 + n*8 + k][l] = vf_all[m][n][t][k];
								}
								// will automatically apply filtered
								dbg_vf[m*4 + n*8 + 3][l] = Math.sqrt(vf_error2[n][t]);
							}
						}
					}
					dbg_vf [8 * vector_field.length + 0][l] = ground_planes_weight[t];
					dbg_vf [8 * vector_field.length + 1][l] = elevations[0][t];
					dbg_vf [8 * vector_field.length + 2][l] = elevations[1][t];
					dbg_vf [8 * vector_field.length + 3][l] = ground_planes_masks[0][t]?1.0:0.0;
					dbg_vf [8 * vector_field.length + 4][l] = ground_planes_masks[1][t]?1.0:0.0;
				}
	    		ShowDoubleFloatArrays.showArrays(
	    				dbg_vf,
	    				dbg_width,
	    				dbg_height,
	    				true,
	    				"vector_field"+dbg_suffix,
	    				dbg_titles);
			}

			
			
			
			// may use tl_rect_metric to remap to the original image
			tile_centers = new double [vector_field[0].length][];
			for (TpTask task: tp_tasks[1]) {
				int ti = task.getTileY() * tilesX + task.getTileX();
				tile_centers[ti] = task.getDoubleCenterXY();
			}
			if (show_tile_centers){
				double [][] dbg_img = new double [6][tile_centers.length];
				String [] dbg_titles = {"cX","cY","px0","py0", "px1","py1"};
				for (int i = 0; i< dbg_img.length;i++) Arrays.fill(dbg_img[i], Double.NaN);
				for (int t = 0; t < tp_tasks[0].length; t++) {
					TpTask task0 = tp_tasks[0][t];
					TpTask task1 = tp_tasks[1][t];
					int ti = task0.getTileY() * tilesX + task0.getTileX();
					dbg_img[0][ti] = task0.getDoubleCenterXY()[0]; // same for task0, task1 
					dbg_img[1][ti] = task1.getDoubleCenterXY()[1]; 
					dbg_img[2][ti] = task0.getXY()[0][0];
					dbg_img[3][ti] = task0.getXY()[0][1];
					dbg_img[4][ti] = task1.getXY()[0][0];
					dbg_img[5][ti] = task1.getXY()[0][1];
				} // getXY()
				ShowDoubleFloatArrays.showArrays(
						dbg_img,
						tilesX,
						tile_centers.length/tilesX,
						true,
						"tile_centers"+dbg_suffix,
						dbg_titles);
			}
			if (!batch_mode) {
				if (debugLevel>-3) {
					System.out.println("correlateOrthoPair(): before LMA, ntry="+ntry);
				}
			}
			boolean origin_center = true; // false - old mode

			orthoPairLMA = new OrthoPairLMA(origin_center);
			// vector_field[1] - neighbors
			double lambda =            0.1;
			double lambda_scale_good = 0.5;
			double lambda_scale_bad =  8.0;
			double lambda_max =      100;
			double rms_diff =          0.001;
			int    num_iter =         20;
			boolean last_run =       false;
			int min_good_tiles =      min_tiles_overlap/2;
			
			// show_lma_dbg
			String dbg_lma_prefix = show_lma_dbg? ("LMA_"+gpu_pair[0]+"-"+gpu_pair[1]+"_ntry"+ntry+"_"):null;
			
			int num_good_tiles = orthoPairLMA.prepareLMA(
					// will always calculate relative affine, starting with unity
					tilesX,          // int           width,
					vector_field[1], // double [][]   vector_XYS,    // optical flow X,Y, confidence obtained from the correlate2DIterate()
					tile_centers,    // double [][]   centers,       // tile centers (in pixels)
					ground_planes_weight, // null,            // double []     weights_extra, // optional, may be null 
					true,            // boolean       first_run,
					min_good_tiles,  // int           min_good_tiles,
					max_std,         // double        max_std,      // maximal standard deviation to limit center area  
					min_std_rad,     // double        min_std_rad,  // minimal radius of the central area (if less - fail)
					tile_rad ,       // double        tile_rad,
					min_tiles_rad,   // int           min_tiles_rad,
					affines_gpu[1],  // double [][]   src_affine,
					pull_skew,       // double        pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
					pull_tilt,       // double        pull_tilt,        // > 0
					pull_scale,      // double        pull_scale,       // = 0
					debugLevel);     // final int     debug_level)
			last_center_radius = orthoPairLMA.getCenterRadius();
			double center_radius_increase = 
					Double.isInfinite(previous_center_radius)?
							(Double.isInfinite(last_center_radius)?1:0):
								last_center_radius/previous_center_radius;
			if ((ntry == 0) && Double.isInfinite(last_center_radius)) {
				center_radius_increase = 1.0; // if starts with infinity - no increase  
			}
			if ((min_std_rad >0) && (last_center_radius < 1)) {
				if (debugLevel>-3) {
					System.out.println("correlateOrthoPair(): center_radius="+
							orthoPairLMA.getCenterRadius()+" < "+ 1);
				}
				return null;
			}
			
			if (num_good_tiles < min_good_tiles) {
				if (debugLevel>-4) {
					System.out.println("correlateOrthoPair(): num_good_tiles="+
				    num_good_tiles+ " < "+min_good_tiles);
				}
			 	if (pairwiseOrthoMatch != null) {
			 		pairwiseOrthoMatch.rms = Double.NaN; // mark as failed
			 	}
				return null;
			}
			int lma_rslt = orthoPairLMA.runLma( // <0 - failed, >=0 iteration number (1 - immediately)
					lambda,             // double lambda,           // 0.1
					lambda_scale_good,  // double lambda_scale_good,// 0.5
					lambda_scale_bad,   // double lambda_scale_bad, // 8.0
					lambda_max,         // double lambda_max,       // 100
					rms_diff,           // double rms_diff,         // 0.001
					num_iter,           // int    num_iter,         // 20
					last_run,           // boolean last_run,
					dbg_lma_prefix,     // String dbg_prefix,
					debugLevel);        // int    debug_level)
			if (debugLevel > -3) {
				System.out.println("LMA result = "+lma_rslt);
			}
			if (lma_rslt < 0) {
				System.out.println("LMA failed, result="+lma_rslt);
				return null;
			}
			rms =      orthoPairLMA.getRms();
			if (debugLevel > -3) {
				System.out.println("RMS= "+rms[0]+" ("+orthoPairLMA.getInitialRms()[0]+
						"), RMS pure="+rms[1]+" ("+orthoPairLMA.getInitialRms()[1]+")");
			}
			if (max_rms_iter != null) {
				int max_rms_index = Math.min(max_rms_iter.length-1, ntry);
				if (rms[0] > max_rms_iter[max_rms_index]) {
					if (debugLevel > -3) {
						System.out.println("RMS= "+rms[0]+" > max_rms_iter["+
								max_rms_index+"] = "+max_rms_iter[max_rms_index]);
					}
					return null;
				}
			}
			
			if (rms[0] > prev_rms[0]) {
				if (debugLevel > -3) {
					if ((rms[0]-prev_rms[0])/prev_rms[0] < rel_improve) {
						System.out.println("LMA RMSE worsened, but less than improvement threshold: new "+rms[0]+" ("+ orthoPairLMA.getInitialRms()[0]+"), prev="+prev_rms[0]);
					} else {
						System.out.println("LMA RMSE worsened: new "+rms[0]+" ("+ orthoPairLMA.getInitialRms()[0]+"), prev="+prev_rms[0]);
					}
				}
				if (ignore_prev_rms) {
					if (debugLevel > -3) {
						System.out.println("Will continue, as ignore_prev_rms is set");
					}					
				} else {
					if (center_radius_increase > center_radius_change) {
						if (debugLevel > -3) {
							System.out.println("Will continue, as center_radius increased from "+
									previous_center_radius+" to "+	last_center_radius +" (more than "+
									center_radius_change+"x).");
						}
					} else {
						if (vf_threshold <  (vf_drop_big * previous_vf_threshold)) {
							if (debugLevel > -3) {
								System.out.println("Will continue, as vf_threshold dropped from "+
										previous_vf_threshold+" to "+	vf_threshold +" (more than "+
										vf_drop_big+"x).");
							}
						} else {
							break;
						}
					}
				}
			}
			affines_gpu[1]=OrthoMap.combineAffine(affines_gpu[1], orthoPairLMA.getAffine());
			jtj = orthoPairLMA.getLastJtJ();
			if ((rms[0] <= prev_rms[0]) && ((prev_rms[0] - rms[0])/prev_rms[0] < rel_improve)) {
				if (debugLevel > -2) {
					System.out.println("LMA relative RMSE improvement = "+((prev_rms[0] - rms[0])/prev_rms[0])+" < "+rel_improve+", exiting.");
				}
				if (ignore_prev_rms) {
					if (debugLevel > -3) {
						System.out.println("Will continue, as ignore_prev_rms is set");
					}					
				} else {
					if (center_radius_increase > center_radius_change) {
						if (debugLevel > -3) {
							System.out.println("Will continue, as center_radius increased from "+
									previous_center_radius+" to "+	last_center_radius +" (more than "+
									center_radius_change+"x).");
						}
					} else {
						if (vf_threshold <  (vf_drop_big * previous_vf_threshold)) {
							if (debugLevel > -3) {
								System.out.println("Will continue, as vf_threshold dropped from "+
										previous_vf_threshold+" to "+	previous_vf_threshold +" (more than "+
										vf_drop_big+"x).");
							}
						} else {
							break;
						}
					}
				}
			}

			if (center_radius_increase > center_radius_change) {
				num_tries ++;
				if (debugLevel > -2) {
					System.out.println("Increasing num_tries to "+num_tries+", as center_radius increased from "+
							previous_center_radius+" to "+	last_center_radius +" (more than "+
							center_radius_change+"x).");
				}
			}
			
			
			prev_rms=rms.clone();
			previous_center_radius = last_center_radius;
			previous_vf_threshold = vf_threshold;
			
		} // for (int ntry = 0; ntry < num_tries; ntry++) {
		// recheck here
//		double tile_rad = rad_fraction * 0.5*Math.min(woi.width, woi.height)/GPUTileProcessor.DTT_SIZE;
		if (num_tries > 0) {
			min_tiles_rad = (int) (fill_fraction_final * 4 * tile_rad * tile_rad);
			last_center_radius = orthoPairLMA.getCenterRadius(
					max_std,         // final double      max_std,      // maximal standard deviation to limit center area
					tile_rad,        // final double      min_radius,
					min_tiles_rad,   // final int         min_tiles,
					vector_field[1], // final double [][] vector_XYS,
					ground_planes_weight, //, // null,            // final double []   weights_extra, // null or additional weights (such as elevation-based)
					tile_centers);   // final double [][] centers)

			if ((min_std_rad > 0) && (last_center_radius < min_std_rad)) {
				if (debugLevel>-3) {
					System.out.println("correlateOrthoPair(): center_radius="+
							last_center_radius+" < "+ min_std_rad);
				}
				return null;
			}
		}
		
		if (pairwiseOrthoMatch != null) {
			pairwiseOrthoMatch.jtj = jtj;
			pairwiseOrthoMatch.rms=  rms[1]; // pure rms
			pairwiseOrthoMatch.zoom_lev = zoom_lev;
		}
		
		double [] tlo_src_metric_other = new double[2]; // check affines[][][] here - 
		for (int i = 0; i < 2; i++) {
			tlo_src_metric_other[i] = affines_gpu[1][i][2] * pix_size - ortho_maps[gpu_pair[1]].getVertMeters()[i];
			for (int j = 0; j < 2; j++) {
				affines[1][i][j] = affines_gpu[1][i][j];
			}
		}
		for (int i = 0; i < 2; i++) {
			affines[1][i][2] = tlo_src_metric_other[i]  -
					tlo_rect_metric[1][0] * affines[1][i][0] -
					tlo_rect_metric[1][1] * affines[1][i][1];
		}
		
		if (debugLevel>-3) {
			System.out.println("correlateOrthoPair(): adjusted affines[1]");
			System.out.println("[["+affines[1][0][0]+","+affines[1][0][1]+","+affines[1][0][2]+"],");
			System.out.println(" ["+affines[1][1][0]+","+affines[1][1][1]+","+affines[1][1][2]+"]]");
		}
		
		// modify affines[1]
		
		if ((debugLevel > 1) && (num_tries>0)) {// show result here
			String [] map_names = {ortho_maps[gpu_pair[0]].getName(),ortho_maps[gpu_pair[1]].getName()};
			ShowDoubleFloatArrays.showArrays( 
					gpu_pair_img,
					OrthoMap.gpu_width,
					OrthoMap.gpu_height,
					true,
					"gpu_pair-zoom"+zoom_lev+"-"+ortho_maps[gpu_pair[0]].getName()+"-"+ortho_maps[gpu_pair[1]].getName(),
					map_names);
		}
		if (calc_warp) {
			double scale = 2.0;
			String dbg_suffix = batch_mode? null: "_warp";

		// return only neighbors vector field
			double [][] vf = ComboMatch.rectilinearVectorField(//rectilinearCorrelate_TD( // scene0/scene1
					clt_parameters,    // final CLTParameters          clt_parameters,
					gpu_pair_img,      // final float  [][]            fpixels, // to check for empty
					gpu_width,         // final int                    img_width,
					woi,               // Rectangle                    woi, // if null, use full GPU window
					affines_gpu,       // final double [][][]          affine,  // [2][2][3] affine coefficients to translate common to 2 images
					tp_tasks,          // TpTask [][]                  tp_tasks_o,
					batch_mode,        // final boolean                batch_mode,
					dbg_suffix,        // final String                 dbg_suffix,
					debugLevel)[1];    // final int                    debugLevel);
			double [][] tile_elevs = null;
			if (ground_planes != null) {
				for (int n = 0; n < ground_planes.length; n++) {
					ground_planes[n] = getGroundPlane(
							gpu_pair[n],     // int         scene_num,
							tp_tasks[n],     // TpTask [] tp_tasks,
							zoom_lev,        // int         zoom_lev,
							tilesX,          // int         tilesX,
							tilesY,          // int         tilesY,
							debugLevel);     // int         debugLevel)
				}
			}
			if (show_vf && (num_tries == 0) && (debugLevel > 1)) { // not needed now
				tile_elevs = new double [tp_tasks.length][];
				for (int n = 0; n < tile_elevs.length; n++) {
					String debug_title = (show_vector_field && (debugLevel>0))? (ortho_maps[gpu_pair[n]].getName()+"_plane"+dbg_suffix):null;
					tile_elevs[n] = getTileElevations(
							gpu_pair[n],     // int         scene_num,
							tp_tasks[n],     // TpTask [] tp_tasks,
							zoom_lev,        // int         zoom_lev,
							tilesX,          // int         tilesX,
							tilesY,          // int         tilesY,
							tile_woi,        // Rectangle tile_woi, // only width, height are used top-left corners are the same
							debug_title,     // String      debug_title,
							debugLevel);     // int         debugLevel)
				}
				String [] dbg_titles = new String[tile_elevs.length];
				String dbg_title = "tile_elevations";
				for (int i =0; i < tile_elevs.length; i++) {
					dbg_titles[i] = ortho_maps[gpu_pair[i]].getName();
					dbg_title += "-"+ortho_maps[gpu_pair[i]].getName();
				}
	    		ShowDoubleFloatArrays.showArrays(
	    				tile_elevs,
	    				tile_woi.width,
	    				tile_woi.height,
	    				true,
	    				dbg_title ,
	    				dbg_titles);
				System.out.println();
			}
			
			double [][] vf_interpolated = interpolateVectorField(
					vf,                // final double [][] vector_field, // sparse {vx,vy,strength}
					tp_tasks[1],       // final TpTask []   tp_tasks, // to remove no-overlap tiles
					tilesX,            // final int gpu_tilesX, // 512
					tile_woi,          // final Rectangle tile_woi, // only width, height are used top-left corners are the same
					scale,             // final double scale,
					1);                // final int debugLevel); // vector_filed_filled - use 2
			double [] tl_metric = {bounds_overlap_meters[0][0],bounds_overlap_meters[1][0]};
			
			FineXYCorr warp = new FineXYCorr(
					zoom_lev,          // int zoom_lev,
					tile_woi.width,    // int width,
					tl_metric,         // double [] tl_metric,// relative to the reference image vertical point, normally negative
					vf_interpolated);  // double [][] vf);
			return warp;
		}
		return null;
	}
	
	public double [] getTileElevations(
			int         scene_num,
			TpTask []   tp_tasks,
			int         zoom_lev,
			int         tilesX,
			int         tilesY,
			final Rectangle   tile_woi, // only width, height are used top-left corners are the same
			String      debug_title,
			int         debugLevel) {
		double    initial_above = 3; // m
		double    initial_below = 3; // m
		int       num_refine =    3;
		double    frac_above =    0.3;
		double    frac_below =    0.1;
		int zoom_lev_tiles = zoom_lev-3;
			double [] elevations = ortho_maps[scene_num].getTileElevations(
					zoom_lev, // final int zoom_level,
					tp_tasks, // final TpTask [] tp_tasks, 
					tilesX, // final int tilesX,
					tilesY, // final int tilesY,
					debugLevel); // final int debugLevel)
			double [] tile_plane_elev = new double [elevations.length];
			double [] ground_planes_metric = ortho_maps[scene_num].getPlaneMeters (
					zoom_lev_tiles,  // int       zoom_lev, // of the data (3 levels down for tiles)
					elevations,      // double [] elev,
					tilesX,          // int       width,
					initial_above,   // double    initial_above, // from average
					initial_below,   // double    initial_below, // from average, // positive value
					num_refine,      // int       num_refine,
					frac_above,      // double    frac_above,
					frac_below,      // double    frac_below,
					tile_plane_elev, // 			double [][] tile_plane_elev, // null or double[2][] will return per-tile elevations
					debug_title);    // String    debug_title)
		// tile_woi, //
			
		double [] elev_crop = new double[tile_woi.width*tile_woi.height];
		for (int row = 0; row < tile_woi.height; row++) {
			System.arraycopy(
					tile_plane_elev,
					row * tilesX,
					elev_crop,
					row*tile_woi.width,
					tile_woi.width);
		}
			
		return elev_crop;
	}
	
	/**
	 *  Get planar approximation of the ground
	 * @param scene_num scene index
	 * @param tp_tasks
	 * @param tp_tasks array of non-empty tiles to be processed in the GPU. Integer tx and ty reference
	 *        tile on a tile grid (8x8 pix), center X,Y for the only "sensor" 0 - float coordinates
	 *        corresponding to the current GPU zoom level
	 * @param zoom_lev zoom level of the GPU image, 0 is 1pix=1cm, -1 is 1pix=2cm
	 * @param tilesX
	 * @param tilesY
	 * @param debugLevel
	 * @return plane parameters - all metric, relative to the vertical point:
	 *         {tilt_x (m/m), tilt_y (m/m), offset (elevation at vertical point in meters)      
	 */
	public double [] getGroundPlane(
			int         scene_num,
			TpTask []   tp_tasks,
			int         zoom_lev,
			int         tilesX,
			int         tilesY,
			int         debugLevel) {
		double    initial_above = 3; // m
		double    initial_below = 3; // m
		int       num_refine =    3;
		double    frac_above =    0.3;
		double    frac_below =    0.1;
		int zoom_lev_tiles = zoom_lev-3;
			double [] elevations = ortho_maps[scene_num].getTileElevations(
					zoom_lev, // final int zoom_level,
					tp_tasks, // final TpTask [] tp_tasks, 
					tilesX, // final int tilesX,
					tilesY, // final int tilesY,
					debugLevel); // final int debugLevel)
			double [] ground_plane_metric = ortho_maps[scene_num].getPlaneMeters (
					zoom_lev_tiles,  // int       zoom_lev, // of the data (3 levels down for tiles)
					elevations,      // double [] elev,
					tilesX,          // int       width,
					initial_above,   // double    initial_above, // from average
					initial_below,   // double    initial_below, // from average, // positive value
					num_refine,      // int       num_refine,
					frac_above,      // double    frac_above,
					frac_below,      // double    frac_below,
					null, // 			double [][] tile_plane_elev, // null or double[2][] will return per-tile elevations
					null);    // String    debug_title)
		// tile_woi, //
		return ground_plane_metric;
	}

	
	
	
	
	/**
	 * Project object offset to along and perpendicular to the scenes offset
	 * @param object_offset object center pixel x and y offset between images (y - down) 
	 * @param scene_offset pixel offset between images 
	 * @return A pair of offsets: parallel (to scene offset) and perpendicular. Negative parallel
	 * offsets corresponds to objects above ground  
	 */
	public static double [] projectOffsetOnVector(
			double [] object_offset,
			double [] scene_offset) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		if (!(l1 > 0.00001)) {
			double [] offs_rot = new double [2];
			offs_rot[0] = ( object_offset[0]*scene_offset[0] + object_offset[1]*scene_offset[1]) / l1;
			offs_rot[1] = (-object_offset[0]*scene_offset[1] + object_offset[1]*scene_offset[0]) / l1;
			return offs_rot;
		}
		return object_offset;
	}
	
	/**
	 * Estimate object height if its offset between image is caused by its elevation
	 * @param object_offset object center pixel x and y offset between images (y - down) 
	 * @param scene_offset pixel offset between images 
	 * @param zoom_level images zoom level
	 * @param agl scene (second) altitude above ground in meters
	 * @return Estimated object elevation above ground in meters
	 */
	public static double offsetToElevation(
			double [] object_offset,
			double [] scene_offset,
			int       zoom_level,
			double    agl) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		double offs_par = projectOffsetOnVector(object_offset, scene_offset)[0]; // *OrthoMap.getPixelSizeMeters(zoom_level);
		return -agl * offs_par / l1 ;
	}
	
	/**
	 * Calculate (negative) pixel offset parallel to the scene offset corresponding to object height
	 * @param height
	 * @param scene_offset
	 * @param zoom_level
	 * @param agl
	 * @return
	 */
	public static double elevationToParallelOffset(
			double    height,
			double [] scene_offset,
			int       zoom_level,
			double    agl) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		return -height/agl * l1 ;
	}
	
	
	
	public static Rectangle scaleRectangle(
			Rectangle woi,
			int tile_size){
		int min_tx = woi.x/tile_size;
		int min_ty = woi.y/tile_size;
		int max_tx1 = (woi.x + woi.width +  (tile_size -1)) / tile_size; 
		int max_ty1 = (woi.y + woi.height + (tile_size -1)) / tile_size; 
		return new Rectangle(min_tx,min_ty,max_tx1 - min_tx,max_ty1 - min_ty);
	}
	
	public static double [] getVectorFieldError2(
			double [][] vector_field,
			int         width, // tilesX
			Rectangle   twoi) { // in tiles
		final Rectangle woi = (twoi != null)? twoi: (new Rectangle (0,0,width,vector_field.length/width));
		final int num_tiles = woi.width*woi.height;
		final int tlTile = width * woi.y + woi.x;
		final double [] vf_error2 = new double [vector_field.length];
//		Arrays.fill(vf_error2, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if (vector_field[tile] != null) {
							double [] xys = vector_field[tile];
							vf_error2[tile] += xys[0]*xys[0]+xys[1]*xys[1]; 
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return vf_error2;
	}
	
	public static double filterVectorField(
			final double [][] vector_field,
			final double []   vf_error2,
			final double      frac_remove,
			final boolean     ignore_strength,
			final int         num_bins,
			final double      max_err,
			final int         width, // tilesX
			final Rectangle   twoi) { // in tiles
		final Rectangle woi = (twoi != null)? twoi: (new Rectangle (0,0,width,vector_field.length/width));
		final int STRENGTH_INDEX=2;
		final double max_err2 = max_err*max_err;
		final double scale = num_bins/max_err2;
		final int num_tiles = woi.width*woi.height;
		final int tlTile = width * woi.y + woi.x;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [][] hist2 = new double [threads.length][num_bins];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if (!Double.isNaN(vf_error2[tile]) && (vector_field[tile] != null)){ // one test is enough
							int bin = (int) Math.round(vf_error2[tile]*scale);
							if (bin >= num_bins) {
								bin= num_bins - 1;
							}
							hist2[thread_num][bin] += ignore_strength? 1.0 : vector_field[tile][STRENGTH_INDEX]; 
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		final double [] hist =    new double [num_bins];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int bin = ai.getAndIncrement(); bin < num_bins; bin = ai.getAndIncrement()) {
						for (int i = 0; i < hist2.length; i++) {
							hist[bin]+= hist2[i][bin];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double sw = 0;
		for (int bin = 0; bin < num_bins; bin++) {
			sw += hist[bin];
		}
		double aremove = sw * frac_remove;
		double sh = 0, shp = 0;
		double t2 = max_err2;
		for (int bin = num_bins-1; bin>=0; bin--) {
			shp = sh;
			sh += hist[bin];
			if (sh > aremove) {
				double r = (sh-aremove)/(sh-shp);
				t2 = (bin + r)/scale;
				break;
			}
		}
		final double threshold2 = t2;
		// remove bad tiles
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if ((vector_field[tile] != null) && !(vf_error2[tile] <= threshold2)){
							vector_field[tile]= null;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return Math.sqrt(threshold2);
	}	
	
	
	
	
	/**
	 * Start with this when reading from saved data
	 * @param path
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public static OrthoMapsCollection readOrthoMapsCollection (
			String path) throws IOException, ClassNotFoundException {
		// try reading current_version, if fails - restart without it (for older formats)
		FileInputStream fileInputStream  = new FileInputStream(path);
		ObjectInputStream objectInputStream  = new ObjectInputStream(fileInputStream);
		int current_version = (int) objectInputStream.readObject(); // reads OrthoMapsCollection
		OrthoMapsCollection.CURRENT_VERSION = current_version; // trying here 08.29.2024 - before orthoMapsCollection exists
		System.out.println("readOrthoMapsCollection(): got current_version="+current_version);
		OrthoMapsCollection orthoMapsCollection = (OrthoMapsCollection) objectInputStream.readObject();
		objectInputStream.close();
		System.out.println("readOrthoMapsCollection(): got orthoMapsCollection, current_version="+current_version);
		return orthoMapsCollection;
	}
	
	public void writeOrthoMapsCollection (String path) throws IOException {
		FileOutputStream fileOutputStream = new FileOutputStream(path);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 		objectOutputStream.writeObject(LATEST_VERSION); // current_version);
		objectOutputStream.writeObject(this);
	    objectOutputStream.flush();
	    objectOutputStream.close();
	}
	
	public static double [][] interpolateVectorField(
			final double [][] vector_field, // sparse {vx,vy,strength}
			final TpTask []   tp_tasks, // to remove no-overlap tiles
			final int         gpu_tilesX, // 512
			final Rectangle   tile_woi, // only width, height are used top-left corners are the same
			final double      scale,
			final int         debugLevel) {
		final int tiles = tile_woi.width*tile_woi.height;
		final double [][] vf = new double[2][tile_woi.width*tile_woi.height];
		for (int i = 0; i < vf.length; i++) {
			Arrays.fill(vf[i], Double.NaN);
		}
		final boolean [] mask = (tp_tasks==null)? null: new boolean [tiles];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < tiles; ipix = ai.getAndIncrement()) {
						int y = ipix / tile_woi.width;
						int x = ipix % tile_woi.width;
						double [] v = vector_field[y*gpu_tilesX+x];
						if (v != null) {
							for (int i = 0; i < vf.length; i++) {
								vf[i][ipix] = scale*v[i];
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);

		if (mask != null) {
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int iTile = ai.getAndIncrement(); iTile < tp_tasks.length; iTile = ai.getAndIncrement()) {
							TpTask task = tp_tasks[iTile];
							int[] txy = task.getTileXY();
							if ((txy[0] < tile_woi.width) && (txy[1] < tile_woi.height)) {
								mask[txy[0] + txy[1] * tile_woi.width] = true;
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
        
        // now fill NaNs in each of vf[0], vf[1]
        double [][]  vf_filled = new double[vf.length][];
		int       num_passes = 100;
		double    max_diff = 1E-4;
    	for (int i = 0; i < vf.length; i++) {
    		vf_filled[i] = OpticalFlow.fillGapsDouble(
    				vf[i],                // double []  data,
    				mask,                 // boolean [] mask_in, // do not process if false (may be null)
    				tile_woi.width,       // int       width,
    				0,                    // int       max_grow,
    				num_passes,           // int       num_passes,
    				max_diff,             // double    max_diff,
    				ImageDtt.THREADS_MAX, // int       threadsMax,
    				debugLevel-1);        // int       debug_level)
    	}		
		if (debugLevel > 0) {
			String [] dbg_titles = {"x-raw","x_filled","y-raw","y_filled"};
			double [][] dbg_img = {vf[0], vf_filled[0],vf[1], vf_filled[1]};
    		ShowDoubleFloatArrays.showArrays(
    				dbg_img,
    				tile_woi.width,
    				dbg_img[0].length / tile_woi.width,
    				true,
    				"vector_field_filled",
    				dbg_titles);
		}
		double [][] vf_out = new double [tiles][];
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < tiles; ipix = ai.getAndIncrement()) {
						if (!Double.isNaN(vf_filled[0][ipix]) && !Double.isNaN(vf_filled[1][ipix])) {
							vf_out[ipix] = new double[] {vf_filled[0][ipix],vf_filled[1][ipix]};
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return vf_out;
	}
	
	public ImagePlus patternMatchDualWrap (
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,
			double [][]   ground_planes) { // null or) {    // use for a single pair only
		//indices //getAGL()
		OrthoMapsParameters omp = new OrthoMapsParameters(); // will set defaults

		double scene0_agl = ortho_maps[indices[0]].getAGL();
		// Overwrite some defaults based on AGL
		if (scene0_agl <= ((50+75)/2)) { // add versions for morning/morning or morning/evening
			omp.setDefaults50();
		} else if (scene0_agl < ((75+100)/2)) { // 75m defaults
			omp.setDefaults75();
		} else { // 100m defaults
			omp.setDefaults100();
		}
		omp.setupDialog(
				indices,
				ortho_maps);
		
		return patternMatchDual (
				indices,             // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				affines,             // double [][][] affines, // null or [indices.length][2][3] 
				warp,                // FineXYCorr    warp);
				ground_planes,       // double [][]   ground_planes,
				omp);//OrthoMapsParameters omp );
	}

	public ImagePlus patternMatchDual (
			int []              indices, // null or which indices to use (normally just 2 for pairwise comparison)
			double [][][]       affines, // null or [indices.length][2][3] 
			FineXYCorr          warp,    // use for a single pair only
			double [][]         ground_planes,
			OrthoMapsParameters omp) {
		boolean sort_by_best = true; // false - as was, by selected pattern
		double ease_first_best = 0.9; // before recenter
		String basename = OrthoMapsParameters.getBaseName(
				ortho_maps, // OrthoMap []           ortho_maps,			
				indices);   // int []                indices) 
		int         debugLevel            = omp.debugLevel;		
		
		// sample around + and - 128/spectrum_sample[0]
		// from (128/spectrum_sample[0])/spectrum_sample[1]
		// to (128/spectrum_sample[0])*spectrum_sample[1].
		// Calculate average of FFT power spectrum on both sides, in both directions and get ratio
		double [] spectrum_sample = {14, 1.5, 1.0};
		 
		double search_radius_recenter = 3; // how far to look for updated correlation peak after recenter
		if (warp != null) {
			warp.scale_warp = omp.scale_warp;
		}
		int last_scene = indices.length-1;
    	int min_zoom_lev = ortho_maps[indices[0]].getOriginalZoomLevel();
    	int max_zoom_lev = ortho_maps[indices[0]].getOriginalZoomLevel();

    	for (int i = 0; i < indices.length; i++) {
    		min_zoom_lev = Math.min(min_zoom_lev, ortho_maps[indices[i]].getOriginalZoomLevel());
    		max_zoom_lev = Math.max(max_zoom_lev, ortho_maps[indices[i]].getOriginalZoomLevel());
    	}
		int     zoom_level = min_zoom_lev;
		int []  wh = new int[2];
		int []  origin = new int[2];
		double [][] centers = new double [indices.length][];
		double [][] dmulti = renderMultiDouble (
				null,       // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
				indices,    // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				true,       // boolean       bounds_to_indices,
				affines,    // double [][][] affines, // null or [indices.length][2][3]
				null,       // double [][]   equalize,
				false,      // boolean       ignore_equalize,
				warp,       // FineXYCorr    warp,,
				zoom_level, // int         zoom_level,
				wh,         // int []      wh,
				origin,     // int []      origin){ // maps[0] as a reference
				centers);   // double [][] centers)
		double [][] dalt = null;
		if (ground_planes != null) {
			dalt = renderMultiDouble (
					ground_planes, // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
					indices,       // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
					true,          // boolean       bounds_to_indices,
					affines,       // double [][][] affines, // null or [indices.length][2][3]
					null,         // double [][]   equalize,
					true,         // boolean       ignore_equalize,
					warp,          // FineXYCorr    warp,,
					zoom_level,    // int         zoom_level,
					wh,            // int []      wh,
					origin,        // int []      origin){ // maps[0] as a reference
					null); // centers);      // double [][] centers) // already set with dmulti
		}
		
		
		int width =  wh[0];
		int height = wh[1];
		String [] dbg_titles = new String[dmulti.length];
		for (int i = 0; i < dbg_titles.length; i++) {
			dbg_titles[i] = ""+i;
		}
		ImagePlus img_first = ShowDoubleFloatArrays.makeArrays(
				dmulti,
				width,
				height,
				basename, // ortho_maps[indices[0]].getName(),
				dbg_titles);
		showSaveImagePlus(
				img_first, // ImagePlus imp,
				omp.show_images, // boolean   show,
				omp.save_images, // boolean   save,
				omp.save_dir, // String    save_dir,
				debugLevel); // int       debugLevel)
		if (dalt != null) {
			ImagePlus img_alt = ShowDoubleFloatArrays.makeArrays(
					dalt,
					width,
					height,
					basename + "-alt", // ortho_maps[indices[0]].getName()+"-alt",
					dbg_titles);
			showSaveImagePlus(
					img_alt, // ImagePlus imp,
					omp.show_images, // boolean   show,
					omp.save_images, // boolean   save,
					omp.save_dir, // String    save_dir,
					debugLevel); // int       debugLevel)
		}
		
		
		double [][] src_marks = null;
		
		double [][][] patterns_all =   new double [indices.length][][];
		double [][] kernels_all =      new double [indices.length][];
		
		GroundObjectPattern [] gops =  new GroundObjectPattern[indices.length];
		AltitudeMismatchKernel[] ak =  new AltitudeMismatchKernel[indices.length]; 
		int [] pattern_sizes =         new int[indices.length];
		String [][]  pattern_labels =  new String[indices.length][];
		double [][][] corrs_out =      new double[indices.length][][]; 
		double [][][] corr_patterns =  new double[indices.length][][];
		int    [][][] icorr_patterns=  new int[indices.length][][]; // will only be used for main !
		
		int []        zoomout =        new int[indices.length];
		double [][]   bestcorr =       new double[indices.length][];
		double [][]   fullcorr =       new double[indices.length][];
		int [][]      bestpatt =       new int   [indices.length][];
		double []     min_corrs_full = new double[indices.length];
		ArrayList <ItemMatch> matches_list = new ArrayList <ItemMatch>();
		double []    adv_radii =             new double[indices.length];
		int    []    corr_radius =           new int [indices.length];
		for (int scene_num = 0; scene_num < indices.length; scene_num++) { // will probably just use first one, second - derivative
			gops[scene_num] = GroundObjectPattern.getPattern(
					omp.object_type, // String        object_type,
					ortho_maps[indices[scene_num]].getLocalDateTime(), // LocalDateTime utcDateTime,
					patterns); // ArrayList<GroundObjectPattern> patterns)
			if (gops[scene_num] == null) {
				System.out.println("Failed to find a pattern for object \""+omp.object_type+"\", aborting operation.");
				return null;
			}
			zoomout[scene_num] = 1;
			for (int i = zoom_level; i < gops[scene_num].getZoomLevel();i++) {
				zoomout[scene_num] *= 2;
			}
			adv_radii[scene_num] = omp.adv_radius/zoomout[scene_num];
			corr_radius[scene_num] = (int) (Math.sqrt(0.5)* adv_radii[scene_num]) -1 ;
			
			
			ImagePlus imp_pattern = new ImagePlus(gops[scene_num].getPatternPath());
			pattern_sizes[scene_num] =  imp_pattern.getWidth();
			if (pattern_sizes[scene_num] == 0) {
				System.out.println("testPatternCorrelate(): pattern \""+gops[scene_num].getPatternPath()+"\" is not found.");
				return null;
			}
			ImageStack stack_pattern = imp_pattern.getStack();
			int nSlices =       stack_pattern.getSize();
			patterns_all[scene_num] =   new double[nSlices][];
			pattern_labels[scene_num] = new String[nSlices];
			for (int k = 0; k < patterns_all[scene_num].length; k++) {
				pattern_labels[scene_num][k]=stack_pattern.getShortSliceLabel(k+1);
				float [] fpixels_pattern = (float[]) stack_pattern.getPixels(k+1);
				patterns_all[scene_num][k]=new double[fpixels_pattern.length];
				for (int i = 0; i < fpixels_pattern.length; i++) {
					patterns_all[scene_num][k][i] = fpixels_pattern[i];
				}
			}
			ak[scene_num] = AltitudeMismatchKernel.getKernel(
					gops[scene_num].getZoomLevel(), // zoom_level,       // int                               zoom_level,
					gops[scene_num].getAGL(), // double                            agl_from,
					ortho_maps[indices[scene_num]].getAGL(), // double                            agl_to,
					kernels); // ArrayList<AltitudeMismatchKernel> kernels)
			if (ak[scene_num] != null) {
				String kernel_path = ak[scene_num].getKernelPath();
				ImagePlus imp_kernel = new ImagePlus(kernel_path);
				if (imp_kernel.getWidth() > 0) {
					float [] kernel_pixels = (float[]) imp_kernel.getProcessor().getPixels();
					kernels_all[scene_num] = new double[kernel_pixels.length];
					for (int i = 0; i < kernel_pixels.length; i++) {
						kernels_all[scene_num][i] = kernel_pixels[i];
					}
				}
			}
			corrs_out[scene_num] =     new double[patterns_all[scene_num].length][]; 
			corr_patterns[scene_num] = new double[patterns_all[scene_num].length][];
			
			for (int n = 0; n < patterns_all[scene_num].length; n++) {
				if (omp.convolve_after) {
					corr_patterns[scene_num][n] = OrthoMap.patternZoomCropPad(
							patterns_all[scene_num][n],      // double [] pattern,
							pattern_sizes[scene_num],        // int       pattern_size,
							omp.corr_size,                       // int       size,
							zoomout[scene_num],              // int       zoomout,
							false);                          // out_normalize); // boolean   normalize) 
					if (kernels_all[scene_num] != null) {
						corr_patterns[scene_num][n] = OrthoMap.convolveWithKernel(
								corr_patterns[scene_num][n], // final double [] data,
								kernels_all[scene_num],      // final double [] kernel,
								omp.corr_size);                  // final int width)
					}
				} else {
					double [] pattern = patterns_all[scene_num][n].clone();
					if (kernels_all[scene_num] != null) {
						pattern = OrthoMap.convolveWithKernel(
								pattern,                 // final double [] data,
								kernels_all[scene_num],  // final double [] kernel,
								pattern_sizes[scene_num]);              // final int width)
					}
					corr_patterns[scene_num][n] = OrthoMap.patternZoomCropPad(
							pattern,                     // double [] pattern,
							pattern_sizes[scene_num],    // int       pattern_size,
							omp.corr_size,                   // int       size,
							zoomout[scene_num],          // int       zoomout,
							false);                      // out_normalize); // boolean   normalize) 
				}
			}
			if (scene_num == 0) {
				boolean debug = false; // debugLevel > 0; // 1;
				icorr_patterns[scene_num] =  OrthoMap.getIntPatterns(
						corr_patterns[scene_num], // double [][] patterns,
						omp.abs_edge_frac,                // double edge_frac, // 0.15
						omp.abs_oversize,                 // double oversize,
						debug);                       // boolean debug);
			}
			// so far contrast is only for the main scene
			if ((debugLevel > -1) && (omp.show_images || omp.save_images) && (scene_num == 0)) {
				String [] pattern_titles = new String[corr_patterns[scene_num].length];
				for (int i = 0; i < pattern_titles.length; i++) {
					pattern_titles[i] = "patt-"+i;
				}
				ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
						corr_patterns[scene_num],
						omp.corr_size,
						omp.corr_size,
						"pattern-"+scene_num+"_"+omp.corr_size+"x"+omp.corr_size+(omp.convolve_after?"_convolved_after":"_convolved_before"),
						pattern_titles);
			    showSaveImagePlus(
			    		imp,         // ImagePlus imp,
			    		omp.show_images, // boolean   show,
			    		omp.save_images, // boolean   save,
			    		omp.save_dir,    // String    save_dir,
			            debugLevel); // int       debugLevel)
				double [][] dbg_img = new double [icorr_patterns[scene_num].length][icorr_patterns[scene_num][0].length];
				for (int n = 0; n < dbg_img.length; n++) {
					for (int i= 0; i < dbg_img[n].length; i++) if (icorr_patterns[scene_num][n][i] != 0){
						switch (icorr_patterns[scene_num][n][i]) {
						case 1: dbg_img[n][i] =  1.0; break;
						case 2: dbg_img[n][i] = -1.0; break;
						case 3: dbg_img[n][i] = -2.0; break;
						}
					}
				}
				ImagePlus imp1 = ShowDoubleFloatArrays.makeArrays(
						dbg_img,
						omp.corr_size,
						omp.corr_size,
						"integer_patterns",
						pattern_titles);
			    showSaveImagePlus(
			    		imp1,         // ImagePlus imp,
			    		omp.show_images, // boolean   show,
			    		omp.save_images, // boolean   save,
			    		omp.save_dir,    // String    save_dir,
			            debugLevel); // int       debugLevel)
			}
		}
		
		// Splitting - above done for both scenes, below - just for the main one
		int scene_num = 0;
		double hi_lo_freq = 0.0;
		double [][] hi_freq_arr = null;
		int []  hi_freq_wh = new int[2];
		{
			// Just testing for now
			int shrink_sel = 4;
			double spectrum_sigma = 2.0;
			int blank_xy = (int) Math.round(spectrum_sample[1]);
			double [][] hi_freq = OrthoMap.getHiFreqCirc(
					dmulti[scene_num],  // final double [] data,
					width,              // final int       width,
					omp.corr_size,          // final int       size,      // power of 2, such as 64
					spectrum_sample[0], // final double    center_period,// center frequency is size/center_period
					spectrum_sample[1], // final double    range_period, // ~1.5 - from center/range to center*range
					blank_xy,           // final int       blank_xy, //
					hi_freq_wh,         // final int []    wh,           // result size
					debugLevel);        // final int       debugLevel);
			hi_freq_arr = new double [4][hi_freq.length];
			for (int i = 0; i < hi_freq_arr.length; i++) {
				Arrays.fill(hi_freq_arr[i], Double.NaN);
			}
			for (int i = 0; i < hi_freq.length;i++) if (hi_freq[i] != null) {
				hi_freq_arr[2][i] = hi_freq[i][0];
				hi_freq_arr[3][i] = hi_freq[i][1];
				hi_freq_arr[1][i] = hi_freq[i][1]/hi_freq[i][0];
			}
			
			hi_freq_arr[0] = hi_freq_arr[1].clone();
			TileNeibs tn =  new TileNeibs(hi_freq_wh[0],hi_freq_wh[1]); 
			OrthoMap.fillNaNs(
					hi_freq_arr[0], // double [] data,
					tn, // TileNeibs tn,
					3); // int min_neibs)
			(new DoubleGaussianBlur()).blurDouble(
					hi_freq_arr[0], // double[] pixels,
					hi_freq_wh[0], // int width,
					hi_freq_wh[1], // int height,
					spectrum_sigma, // double sigmaX,
					spectrum_sigma, // double sigmaY,
					0.01); // double accuracy);
			boolean [] mask = new boolean[hi_freq.length];
			for (int i = 0; i < mask.length;i++) {
				mask[i] = hi_freq[i] != null;
			}
			tn.shrinkSelection(
					shrink_sel, // final int        shrink,           // grow tile selection by 1 over non-background tiles 1: 4 directions, 2 - 8 directions, 3 - 8 by 1, 4 by 1 more
					mask,       // final boolean [] tiles,  
					null);      // final boolean [] prohibit)
			double swd = 0.0, sw = 0.0;
			for (int i = 0; i < mask.length;i++) if (mask[i]){
				swd += hi_freq[i][1]/hi_freq[i][0];
				sw+= 1.0;
			}
			hi_lo_freq = swd/sw;
			String [] hi_freq_titles = {"blur","ratio","low","high"};
			ImagePlus img_hi_freq = ShowDoubleFloatArrays.makeArrays(
					hi_freq_arr,
					hi_freq_wh[0],
					hi_freq_wh[1],
//					indices[scene_num]+"-"+ortho_maps[indices[scene_num]].getName()+"-HIGH_FREQ",
					basename+"-HIGH_FREQ", // 
					hi_freq_titles);
			showSaveImagePlus(
					img_hi_freq,         // ImagePlus imp,
					omp.show_images, // boolean   show,
					omp.save_images, // boolean   save,
					omp.save_dir,    // String    save_dir,
					debugLevel); // int       debugLevel)
			
			System.out.println("Average hi_lo_freq="+hi_lo_freq);
			System.out.println();
		}
		
		
		corrs_out[scene_num] = correlateAllPatterns(
				dmulti[scene_num],           // double []    data,
				width,                       // int          width,
				omp.corr_size,                   // int          corr_size,    
				corr_patterns[scene_num],    // double [][]  patterns,  
				false,                       // boolean      convolve, 
				omp.phaseCoeff[scene_num],       // double       phaseCoeff,
				omp.lpf_sigma[scene_num],        //double       lpf_sigma,
				src_marks,                   // double [][]  src_marks,
				ortho_maps[indices[scene_num]].getName(), // String       prefix, // ortho_maps[indices[scene_num]].getName()
				omp.save_dir,                    // String       save_dir,
				omp.show_images,                 // boolean      show,
				omp.save_images,                 // boolean      save,
				debugLevel);                 // int          debugLevel)
		bestcorr[scene_num] = new double [dmulti[scene_num].length];
		fullcorr[scene_num] = new double [dmulti[scene_num].length];
		bestpatt[scene_num] = new int [bestcorr[scene_num].length];
		min_corrs_full[scene_num] = omp.min_corrs[scene_num] * omp.min_corr_full_rel;

		// TODO: scale adv_radius, corr_radius
		ArrayList<Point> plist =OrthoMap.combineDirCorrs (
				corrs_out[scene_num],      // final double [][] corrs,
				width,                     // final int         width,
				fullcorr[scene_num],       // final double []   fullcorr_in,
				bestcorr[scene_num],       // final double []   bestcorr_in
				bestpatt[scene_num],       // final int []      bestpatt_in,
				ease_first_best*omp.min_corrs[scene_num],      // final double      min_corr,
				min_corrs_full[scene_num], // final double      min_corr_full,
				omp.full_preference,           // final double      full_preference,
				omp.max_min_ratio,             // final double      max_min_ratio,
				omp.combine_full[0],           // final boolean     combine_full,    // multiply by normalized full pattern correlation maximum
				adv_radii[scene_num],      // final double      adv_radius,
				corr_radius[scene_num]);   // final int         corr_radius)

		for (Point p : plist) {
			double [] matches = new double [corrs_out[scene_num].length + 1];
			double [] match_xy = {p.x%width, p.x/width}; // here int values
			int    best_patt = p.y + 1;
			matches[0] = bestcorr[scene_num][p.x];
			for (int i = 1; i < matches.length; i++) {
				matches[i] = corrs_out[scene_num][i-1][p.x];
			}
			ItemMatch item_match = new ItemMatch (
					indices.length,
					match_xy); 
			item_match.addPatternMatches(
					gops[scene_num],        // GroundObjectPattern groundObjectPattern,
					matches,    // double [] matches,
					best_patt); // int      best_sub);
			matches_list.add(item_match);
		}
///		int sort_pattern_index = 0; // used in other places, not just for sorting
		int sort_pattern_mode = sort_by_best? -1 : 0; // -1 - best match, regardles off best pointer 0; //combo. 1 - full pattern
		ArrayList<Integer> match_sort =  ItemMatch.sortByMatch(
				matches_list,        // ArrayList<ItemMatch> match_list,
				gops[scene_num],
				false,               // boolean              keep_removed,// GroundObjectPattern groundObjectPattern,
				sort_pattern_mode); // int indx)
		// Print all results
		String log_lines = "\n====="+
		(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+
				"=====\n");
		log_lines += "Match candidates ("+match_sort.size()+
				") that exceed minimal correlation strength of "+omp.min_corrs[scene_num]+"\n";
		
		System.out.println("Match candidates ("+match_sort.size()+
				") that exceed minimal correlation strength of "+omp.min_corrs[scene_num]);
		// remove non-overlapped from the list
		int num_non_overlap = 0;
		for (int i = match_sort.size()-1; i >=0; i--) {
			int indx =match_sort.get(i);
			ItemMatch match = matches_list.get(indx);
			int [] icenter_xy = match.getIntXY();
			for (int sn = 1; sn < indices.length; sn++) {
				if (Double.isNaN(dmulti[sn][icenter_xy[0] + icenter_xy[1] * width])) {
					match.remove();       // mark as removed
					match_sort.remove(i); // remove from index list
					log_lines += "Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
							" as it is not in the scenes overlap area\n";
					if (debugLevel > -1) {
						System.out.println("Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
								" as it is not in the scenes overlap area");
					}
					num_non_overlap++;
					break;
				}
			}
		}

		// near the edge - check both images
		for (int i = match_sort.size()-1; i >=0; i--) {
			int indx =match_sort.get(i);
			ItemMatch match = matches_list.get(indx);
			int [] icenter_xy = match.getIntXY();
			boolean has_NaN = true;
			seach_NaN: {
				int half_size=omp.corr_size/2;
				if  ((icenter_xy[0] < half_size) || (icenter_xy[1] < half_size) ||
						(icenter_xy[0] > (width - half_size)) || (icenter_xy[1] > (height - half_size))) {
					break seach_NaN;								
				}
				for (int y = (icenter_xy[1] - half_size); y < (icenter_xy[1] + half_size); y++) {
					int indx0 = y * width;
					for (int dy =- half_size; dy < half_size; dy++) {
						for (int x = (icenter_xy[0] - half_size); x < (icenter_xy[0] + half_size); x++) {
							int indx1 = indx0 + x;
							for (int sn = 0; sn < indices.length; sn++) {
								if (Double.isNaN(dmulti[sn][indx1])) {
									break seach_NaN;								
								}
							}
						}
					}

				}
				has_NaN = false;
			}
			if (has_NaN) {
				match.remove();       // mark as removed
				match_sort.remove(i); // remove from index list
				log_lines += "Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
						" as it is near the edge of one of the images\n";
				
				if (debugLevel > -1) {
					System.out.println("Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
							" as it is near the edge of one of the images");
				}
				num_non_overlap++;
			}
		}

		if (num_non_overlap > 0) {
			log_lines += "Removed "+num_non_overlap+" objects outside of the scenes overlap, "+
					match_sort.size()+" remain\n";
			System.out.println("Removed "+num_non_overlap+" objects outside of the scenes overlap, "+
					match_sort.size()+" remain");
		}
		
		if (omp.append_log) { // starting log with a date/time separator
			String log_path = omp.save_dir+basename+OrthoMapsParameters.LOG_SUFFIX;
			CalibrationFileManagement.saveStringToFile (
					log_path,      //String path,
					log_lines, // data,
					true);         // boolean append)
		}
		double [] scene_xy_offset =   {centers[last_scene][0]-centers[0][0],centers[last_scene][1]-centers[0][1]};
		double    scene_agl = gops[last_scene].getAGL(); // second scene agl
		
		omp.generateReport( // only goes to log
				null,                        // String                log_line, // should end with \n or be null
				false,                       // boolean               show, // ands with .show_images
				false,                       // boolean               save, // ands with .save_images
				true,                        // boolean               gen_results,
				false,                       // boolean               gen_parameters,
				ortho_maps,                  // OrthoMap []           ortho_maps,			
				new int [] {indices[0]},     // int []                indices,
				match_sort,                  // ArrayList<Integer>    match_sort,
				matches_list,                // ArrayList <ItemMatch> matches_list,
				zoom_level,                  // int                   zoom_level,
				scene_xy_offset,             // double []             scene_xy_offset,
				scene_agl,                   // double                scene_agl,
				corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
				origin,                      // int []                origin,
				centers,                     // double [][]           centers,
				gops[scene_num],             // GroundObjectPattern   gop,
				hi_lo_freq,                  // double                hi_lo_freq,
				hi_freq_wh,                  // int []                hi_freq_wh,
				hi_freq_arr);                // double [][]           hi_freq_arr,
		if (debugLevel > 0) { 
			omp.printReport(
					true,                        // boolean               gen_results,
					false,                       // boolean               gen_parameters,
					ortho_maps,                  // OrthoMap []           ortho_maps,			
					new int [] {indices[0]},     // int []                indices,
					match_sort,                  // ArrayList<Integer>    match_sort,
					matches_list,                // ArrayList <ItemMatch> matches_list,
					zoom_level,                  // int                   zoom_level,
					scene_xy_offset,             // double []             scene_xy_offset,
					scene_agl,                   // double                scene_agl,
					corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
					origin,                      // int []                origin,
					centers,                     // double [][]           centers,
					gops[scene_num],             // GroundObjectPattern   gop,
					hi_lo_freq,                  // double                hi_lo_freq,
					hi_freq_wh,                  // int []                hi_freq_wh,
					hi_freq_arr);                // double [][]           hi_freq_arr,
		}
		PointRoi roi = new PointRoi();
		roi.setOptions("nolabel"); // label");
		for (int mn=0; mn < match_sort.size(); mn++) {
			roi.addPoint(omp.extr_size/2,omp.extr_size/2,mn+1); // ,1);
		}

		// Extract main scene areas around candidates
		ItemMatch.setExtractedObjects(
				scene_num,          // int                   scene_num,
				omp.extr_size,          // int                   extr_size,
				gops[scene_num],    //GroundObjectPattern   gop,
				matches_list,       // ArrayList <ItemMatch> matches_list,
				match_sort,         // ArrayList<Integer>    match_sort,
				dmulti[scene_num],  // double[]              data,
				width,              // int                   width,
				debugLevel);        // int                   debugLevel)
		// calculate absolute contrasts for all patterns, update best_sub modes: 0 - keep, 1 - keep type, 2 keep type and if half +/-1,
		// 3 - free, 4 - force round objects. This may change best subpattern and should be reflected by filters. Maybe even reorder candidates?
		if (omp.corr_centered) { // re-evaluate correlations centered, recreate and reorder list
			log_lines = "";
			
			for (int mn=0; mn < match_sort.size(); mn++) {
				int indx =match_sort.get(mn);
				ItemMatch match = matches_list.get(indx);
				double [] center_xy = match.getXY();
				// correlate each extracted object with all patterns, save to corrs_out_centered
				// get correlation for the pattern[0] and re-center if needed
				//						CorrelationPeakStats[] filter_data = new CorrelationPeakStats[2];
				double [] corrs_centered = OrthoMap.correlateWithPattern(
						match.extracted_nodc[scene_num],     // extr_data[mn],                       // final double [] data,
						omp.corr_size,                           // final int       width,
						omp.corr_size,                           // final int       psize,      // power of 2, such as 64
						corr_patterns[scene_num][0], // final double [] pattern,    // [psize*psize]
						false,                               // final boolean   convolve,   // convolve, not correlate
						omp.phaseCoeff[scene_num],               // final double    phaseCoeff,
						omp.lpf_sigma[scene_num],                // final double    lpf_sigma, // 0 - do not filter
						debugLevel);                         // final int       debugLevel) {
				match.setCorrFull(
						scene_num, // int scene_num,
						corr_patterns[scene_num].length, // int num_patt,
						corrs_centered); // double [] corr_data)
				match.filter_data[scene_num][0] = new CorrelationPeakStats( // null pointer
						match.getCorr(scene_num,0),
						// corrs_centered, // double [] data, // square data
						null,                      // double [] cent_xy, // if null, use center of the square
						search_radius_recenter,    // search_radius,            // double    radius, // search for maximum within this radius. Should radius be 0 here?
						omp.filt_frac_max,                  // double    frac_max)
						0,                         // filt_other_rad,           // double    other_radius,
						debugLevel);
				double [] matches = new double [corr_patterns[scene_num].length + 1];
				int best_indx = 0;
				if (Double.isNaN(match.filter_data[scene_num][0].best_d)) {
					log_lines += "Could not find recentered maximum for ["+center_xy[0]+"/"+center_xy[1]+
							"], removing this peak.\n";
					if (debugLevel > -4) {
						System.out.println("Could not find recentered maximum for ["+center_xy[0]+"/"+center_xy[1]+
								"], removing this peak.");
					}
					// will add fake with all 0
				} else {
					//							double [] cent_offs = filter_data[0].cent_offs;
					double [] cent_offs = match.filter_data[scene_num][0].cent_offs;
					int [] old_xy = new int [] {(int) Math.round(center_xy[0]), (int) Math.round(center_xy[1])};
					center_xy[0]+=cent_offs[0];
					center_xy[1]+=cent_offs[1];
					int [] new_xy = new int [] {(int) Math.round(center_xy[0]), (int) Math.round(center_xy[1])};
					match.setComboPXY(center_xy); // update center
					if ((new_xy[0] != old_xy[0]) || (new_xy[1] != old_xy[1])) {
						log_lines += "Centered object position shifted from ["+old_xy[0]+"/"+old_xy[1]+
								"], to ["+new_xy[0]+"/"+new_xy[1]+"], rebuilding extracts.\n";
						if (debugLevel > -4) {
							System.out.println("Centered object position shifted from ["+old_xy[0]+"/"+old_xy[1]+
									"], to ["+new_xy[0]+"/"+new_xy[1]+"], rebuilding extracts.");
						}
						match.extractObject(
								dmulti[scene_num], // double [] data,
								width,             // int       width,
								scene_num,         // int       scene_num,
								omp.extr_size);        // int       extr_size);

						corrs_centered = OrthoMap.correlateWithPattern(
								match.extracted_nodc[scene_num],     // final double [] data,
								omp.corr_size,                           // final int       width,
								omp.corr_size,                           // final int       psize,      // power of 2, such as 64
								corr_patterns[scene_num][0], // final double [] pattern,    // [psize*psize]
								false,                               // final boolean   convolve,   // convolve, not correlate
								omp.phaseCoeff[scene_num],               // final double    phaseCoeff,
								omp.lpf_sigma[scene_num],                // final double    lpf_sigma, // 0 - do not filter
								debugLevel);                         // final int       debugLevel) {
						match.setCorr (
								scene_num,       // int scene_num,
								0,               // int patt_index,
								corrs_centered); // double [] corr_data)


					}
					// calculate correlations for the rest of patterns
					for (int patt_indx = 1; patt_indx < corr_patterns[scene_num].length; patt_indx++) {
						corrs_centered = OrthoMap.correlateWithPattern(
								match.extracted_nodc[scene_num],     // final double [] data,
								omp.corr_size,                           // final int       width,
								omp.corr_size,                           // final int       psize,      // power of 2, such as 64
								corr_patterns[scene_num][patt_indx], // final double [] pattern,    // [psize*psize]
								false,                               // final boolean   convolve,   // convolve, not correlate
								omp.phaseCoeff[scene_num],               // final double    phaseCoeff,
								omp.lpf_sigma[scene_num],                // final double    lpf_sigma, // 0 - do not filter
								debugLevel);                         // final int       debugLevel) {
						match.setCorr (
								scene_num,       // int scene_num,
								patt_indx,       // int patt_index,
								corrs_centered); // double [] corr_data)
					}
					double [][] corrs_all = match.getCorrs(
							scene_num); // int scene_num);
					int cent_indx = (omp.corr_size+1)*omp.corr_size/2;
					double best_v = corrs_all[1][cent_indx]; 
					double worst_v = best_v;
					double full_v = corrs_all[0][cent_indx];
					best_indx=1;
					for (int patt_indx = 2; patt_indx < corrs_all.length; patt_indx++) {
						if (corrs_all[patt_indx][cent_indx] > best_v) {
							best_v=corrs_all[patt_indx][cent_indx];
							best_indx = patt_indx;
						}
						if (corrs_all[patt_indx][cent_indx] < worst_v) {
							worst_v = corrs_all[patt_indx][cent_indx];
						}
					}
					if (full_v * omp.full_preference > best_v) {
						best_indx = 0;
					} else {
						if ((worst_v > 0) && (best_v/worst_v < omp.max_min_ratio)) {
							best_indx = 0;
						}
					}
					matches[0] = corrs_all[best_indx][cent_indx];
					for (int i = 1; i < matches.length; i++) {
						matches[i] = corrs_all[i-1][cent_indx];
					}
				}
				// select best pattern similar to combineDirCorrs(), using center pixel
				// add to a new list (and then replace main list and reorder?)
				//matches_list_centered
				match.addPatternMatches(  // will update existing match too
						gops[scene_num],  // GroundObjectPattern groundObjectPattern,
						matches,          // double [] matches,
						best_indx + 1);   // int      best_sub);

			} // for (int mn=0; mn < match_sort.size(); mn++) {
			// some matches are already marked as removed
			match_sort =  ItemMatch.sortByMatch(
					matches_list,        // ArrayList<ItemMatch> match_list,
					gops[scene_num],     // GroundObjectPattern groundObjectPattern,
					false,               // boolean              keep_removed,// GroundObjectPattern groundObjectPattern,
					sort_pattern_mode);  // int indx)
			// Print all results
			log_lines += "Centered match candidates ("+match_sort.size()+")\n";

			System.out.println("Centered match candidates ("+match_sort.size()+
					")"); // +min_corrs[scene_num]);
			if (omp.append_log) { // starting log with a date/time separator
				String log_path = omp.save_dir+basename+OrthoMapsParameters.LOG_SUFFIX;
				CalibrationFileManagement.saveStringToFile (
						log_path,      //String path,
						log_lines, // data,
						true);         // boolean append)
			}
			
			
			// print updated list
			omp.generateReport( // only goes to log
					null,                        // String                log_line, // should end with \n or be null
					false,                       // boolean               show, // ands with .show_images
					false,                       // boolean               save, // ands with .save_images
					true,                        // boolean               gen_results,
					false,                       // boolean               gen_parameters,
					ortho_maps,                  // OrthoMap []           ortho_maps,			
					new int [] {indices[0]},     // int []                indices,
					match_sort,                  // ArrayList<Integer>    match_sort,
					matches_list,                // ArrayList <ItemMatch> matches_list,
					zoom_level,                  // int                   zoom_level,
					scene_xy_offset,             // double []             scene_xy_offset,
					scene_agl,                   // double                scene_agl,
					corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
					origin,                      // int []                origin,
					centers,                     // double [][]           centers,
					gops[scene_num],             // GroundObjectPattern   gop,
					hi_lo_freq,                  // double                hi_lo_freq,
					hi_freq_wh,                  // int []                hi_freq_wh,
					hi_freq_arr);                // double [][]           hi_freq_arr,
			if (debugLevel > 0) { 
				omp.printReport(
						true,                        // boolean               gen_results,
						false,                       // boolean               gen_parameters,
						ortho_maps,                  // OrthoMap []           ortho_maps,			
						new int [] {indices[0]},     // int []                indices,
						match_sort,                  // ArrayList<Integer>    match_sort,
						matches_list,                // ArrayList <ItemMatch> matches_list,
						zoom_level,                  // int                   zoom_level,
						scene_xy_offset,             // double []             scene_xy_offset,
						scene_agl,                   // double                scene_agl,
						corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
						origin,                      // int []                origin,
						centers,                     // double [][]           centers,
						gops[scene_num],             // GroundObjectPattern   gop,
						hi_lo_freq,                  // double                hi_lo_freq,
						hi_freq_wh,                  // int []                hi_freq_wh,
						hi_freq_arr);                // double [][]           hi_freq_arr,
			}
		} else { // reuse already calculated correlations
			for (int mn=0; mn < match_sort.size(); mn++) {
				int indx =match_sort.get(mn);
				ItemMatch match = matches_list.get(indx);
				match.extractCorrs( // copy all 9 layers
						corrs_out[scene_num], // double [][] corr_data,
						width, // int         width,
						scene_num, // int         scene_num,
						omp.extr_size); // int         extr_size)
			}
		}

		setAbsoluteContrasts( // move later than corr_centered
				omp,                       // OrthoMapsParameters   omp
				gops[scene_num],           // GroundObjectPattern   gop,
				matches_list,              // ArrayList <ItemMatch> matches_list,
				match_sort,                // ArrayList<Integer>    match_sort,
				scene_num,                 // int                   scene_num,
				icorr_patterns[scene_num], // int [][]              ipatterns,
				debugLevel); // int                   debugLevel

		for (int mn=0; mn < match_sort.size(); mn++) {
			int indx =match_sort.get(mn);
			ItemMatch match = matches_list.get(indx);
			// should be already updated by setAbsoluteContrasts();
			int best_patt = match.getPatternMatch(gops[scene_num]).getBestSub() - 1;
			//					double [] center_xy = match.getXY();
			match.filter_data[scene_num][0] = new CorrelationPeakStats( // null pointer
					match.getCorr(scene_num,0),// corrs_centered, // double [] data, // square data
					null,                      // double [] cent_xy, // if null, use center of the square
					omp.search_radius,             // search_radius,            // double    radius, // search for maximum within this radius. Should radius be 0 here?
					omp.filt_frac_max,                  // double    frac_max)
					omp.filt_other_rad,            // filt_other_rad,           // double    other_radius,
					debugLevel);

			match.setCorrHalf(
					scene_num, // int scene_num,
					best_patt, // int     best_patt,
					omp.combine_full[0]); // boolean combine_full) {

			match.filter_data[scene_num][1] = new CorrelationPeakStats(
					match.getCorrHalf(scene_num), // double [] data, // square data
					null,                         // double [] cent_xy, // if null, use center of the square
					omp.search_radius,                // double    radius, // search for maximum within this radius. Should radius be 0 here?
					omp.filt_frac_max,                     // double    frac_max)
					omp.filt_other_rad,               // double    other_radius,
					debugLevel);                  // final int       debugLevel) {
		}

		boolean               show_centers = true;
		if (indices.length == 1) { // for a single-scene show before filtering by the first(only) scene
			for (int sn = 0; sn < indices.length; sn++) {
				String prefix = ortho_maps[indices[sn]].getName()+"-prefilter1";
				String suffix0 = "_pc"+omp.phaseCoeff[sn]+"_lpf"+omp.lpf_sigma[sn];
				for (int mode = 0; mode < 4; mode++) { // all 4 image types
					String suffix = (mode <2)? "":suffix0;
					if ((sn == 0) || (mode == 0)) {
						ItemMatch.getImageExtracts(
								prefix,          // String                prefix, // include PC, filter here
								suffix,          // String                suffix, // w/o .tiff
								omp.show_images,     // boolean               show,
								omp.save_images,     // boolean               save,
								false,           // boolean               show_removed,
								omp.save_dir,        // String                save_dir,
								sn,              // int                   scene_num,
								omp.remove_dc,       // boolean               nodc,
								mode,            // int                   mode, // 0 - extract, 1 - masked, 2 - corr full, 3 - corr half
								show_centers,    // boolean               show_centers,
								omp.extr_size,       // int                   extr_size,
								gops[scene_num], // GroundObjectPattern   gop,
								matches_list,    // ArrayList <ItemMatch> matches_list,
								match_sort,      // ArrayList<Integer>    match_sort,
								debugLevel);     // int                   debugLevel)
					}
				}
			}
		}
		if (!omp.filt_atonce) { // first filter using scene 1 only
			boolean[] filt_main_other = new boolean [indices.length]; //{true,false};
			filt_main_other[0] = true;
			int num_removed = filterCandidates(
					omp,                // OrthoMapsParameters   omp,
					basename,           // String                basename,
					filt_main_other,    // new boolean []        filt_main_other,
					gops[scene_num],    // GroundObjectPattern   gop,
					zoom_level,         // int       zoom_level,
					scene_agl,          // double                scene_agl,
					scene_xy_offset,    // double []             scene_xy_offset, //second scene vertical projection offset from the first scene
					matches_list,       // ArrayList <ItemMatch> matches_list,
					match_sort,         // ArrayList<Integer>    match_sort,
					debugLevel);        // int                   debugLevel )
//			if (!filt_keep_pre && (removed.length > match_sort.size())) { // decimate data arrays
//				removed = new boolean [match_sort.size()];
//			}
		}
		if (indices.length > 1) { // for a multi-scene show after filtering by the first scene
			// here extract for the second scene
			for (int sn = 1; sn < indices.length; sn++) {
				ItemMatch.setExtractedObjects(
						sn,           // int                   scene_num,
						omp.extr_size,    // int                   extr_size,
						gops[sn],     // GroundObjectPattern   gop,
						matches_list, // ArrayList <ItemMatch> matches_list,
						match_sort,   // ArrayList<Integer>    match_sort,
						dmulti[sn],   // double[]              data,
						width,        // int                   width,
						debugLevel);  // int                   debugLevel)
			}					

			for (int sn = 0; sn < indices.length; sn++) {
				String prefix = ortho_maps[indices[sn]].getName()+"-postfilter1";
				String suffix0 = "_pc"+omp.phaseCoeff[sn]+"_lpf"+omp.lpf_sigma[sn];
				for (int mode = 0; mode < 4; mode++) { // all 4 image types
					String suffix = (mode <2)? "":suffix0;
					if ((sn == 0) || (mode == 0)) {
						ItemMatch.getImageExtracts(
								prefix,          // String                prefix, // include PC, filter here
								suffix,          // String                suffix, // w/o .tiff
								omp.show_images,     // boolean               show,
								omp.save_images,     // boolean               save,
								false,           // boolean               show_removed,
								omp.save_dir,        // String                save_dir,
								sn,              // int                   scene_num,
								omp.remove_dc,       // boolean               nodc,
								mode,            // int                   mode, // 0 - extract, 1 - masked, 2 - corr full, 3 - corr half
								show_centers,    // boolean               show_centers,
								omp.extr_size,       // int                   extr_size,
								gops[scene_num], // GroundObjectPattern   gop,
								matches_list,    // ArrayList <ItemMatch> matches_list,
								match_sort,      // ArrayList<Integer>    match_sort,
								debugLevel);     // int                   debugLevel)
					}
				}
			}
		}
		// display extracted correlations for the main scene:
		if (indices.length > 1) {
			// print updated table after some scenes may be removed by the filter
			omp.generateReport( // only goes to log
					"\nAfter first scene filtering:\n", // String                log_line, // should end with \n or be null
					false,                       // boolean               show, // ands with .show_images
					false,                       // boolean               save, // ands with .save_images
					true,                        // boolean               gen_results,
					false,                       // boolean               gen_parameters,
					ortho_maps,                  // OrthoMap []           ortho_maps,			
					new int [] {indices[0]},     // int []                indices,
					match_sort,                  // ArrayList<Integer>    match_sort,
					matches_list,                // ArrayList <ItemMatch> matches_list,
					zoom_level,                  // int                   zoom_level,
					scene_xy_offset,             // double []             scene_xy_offset,
					scene_agl,                   // double                scene_agl,
					corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
					origin,                      // int []                origin,
					centers,                     // double [][]           centers,
					gops[scene_num],             // GroundObjectPattern   gop,
					hi_lo_freq,                  // double                hi_lo_freq,
					hi_freq_wh,                  // int []                hi_freq_wh,
					hi_freq_arr);                // double [][]           hi_freq_arr,
			if (debugLevel > 0) { 
				System.out.println("\nAfter first scene filtering:");
				omp.printReport(
						true,                        // boolean               gen_results,
						false,                        // boolean               gen_parameters,
						ortho_maps,                  // OrthoMap []           ortho_maps,			
						new int [] {indices[0]},     // int []                indices,
						match_sort,                  // ArrayList<Integer>    match_sort,
						matches_list,                // ArrayList <ItemMatch> matches_list,
						zoom_level,                  // int                   zoom_level,
						scene_xy_offset,             // double []             scene_xy_offset,
						scene_agl,                   // double                scene_agl,
						corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
						origin,                      // int []                origin,
						centers,                     // double [][]           centers,
						gops[scene_num],             // GroundObjectPattern   gop,
						hi_lo_freq,                  // double                hi_lo_freq,
						hi_freq_wh,                  // int []                hi_freq_wh,
						hi_freq_arr);                // double [][]           hi_freq_arr,
			}
		}

		// Correlate second scene only for selected fragments
		for (int scene_other = 1; scene_other < indices.length; scene_other++) { // normally just 1
			for (int mn=0; mn < match_sort.size(); mn++) {
				int indx =match_sort.get(mn);
				ItemMatch match = matches_list.get(indx);
				int best_patt = match.getPatternMatch(gops[scene_num]).getBestSub() - 1;
				// correlate extracted squares of the other scene with full pattern 
				double [] corr_full = OrthoMap.correlateWithPattern(
						match.extracted_nodc[scene_other],   // extr_data[mn],                       // final double [] data,
						omp.corr_size,                           // final int       width,
						omp.corr_size,                           // final int       psize,      // power of 2, such as 64
						corr_patterns[scene_other][0],       // final double [] pattern,    // [psize*psize]
						false,                               // final boolean   convolve,   // convolve, not correlate
						omp.phaseCoeff[scene_other],             // final double    phaseCoeff,
						omp.lpf_sigma[scene_other],              // final double    lpf_sigma, // 0 - do not filter
						debugLevel);                         // final int       debugLevel) {
				// set [9] array of correlations and copy full pattern ([0]) data. Other (1-9) will not be used
				// for scene_other 
				match.setCorrFull(
						scene_other,                         // int scene_num,
						corr_patterns[scene_other].length,   // int num_patt,
						corr_full);                     // double [] corr_data)

				match.filter_data[scene_other][0] = new CorrelationPeakStats( // null pointer
						match.getCorr(scene_other,0),        // double [] data, // square data
						null,                                // double [] cent_xy, // if null, use center of the square
						omp.search_radius,                       // search_radius,            // double    radius, // search for maximum within this radius. Should radius be 0 here?
						omp.filt_frac_max,                            // double    frac_max)
						omp.filt_other_rad,                      // double    other_radius,
						debugLevel);
				if (best_patt > 0) { // not full match - for full match it will copy full correlation
					double [] corr_half = OrthoMap.correlateWithPattern(
							match.extracted_nodc[scene_other],     // extr_data[mn],                       // final double [] data,
							omp.corr_size,                             // final int       width,
							omp.corr_size,                             // final int       psize,      // power of 2, such as 64
							corr_patterns[scene_other][best_patt], // final double [] pattern,    // [psize*psize]
							false,                                 // final boolean   convolve,   // convolve, not correlate
							omp.phaseCoeff[scene_other],               // final double    phaseCoeff,
							omp.lpf_sigma[scene_other],                // final double    lpf_sigma, // 0 - do not filter
							debugLevel);                           // final int       debugLevel) {
					// setting only one correlation of 8 halves 
					match.setCorr (
							scene_other,     // int scene_num,
							best_patt,       // int patt_index,
							corr_half);      // double [] corr_data)
				}
				// Do it in any case, if best_patt==0 it will copy, optionally limit by 0 and square full correlation
				match.setCorrHalf( // D
						scene_other, // int scene_num,
						best_patt, // int     best_patt,
						omp.combine_full[1]); // boolean combine_full) {
				// set statistics for half-correlations of the second scene
				// before it was just a copy of full, regardless of combine_full[1]
				match.filter_data[scene_other][1] = new CorrelationPeakStats(
						match.getCorrHalf(scene_other), // double [] data, // square data
						null,                           // double [] cent_xy, // if null, use center of the square
						omp.search_radius,                  // double    radius, // search for maximum within this radius. Should radius be 0 here?
						omp.filt_frac_max,                       // double    frac_max)
						omp.filt_other_rad,                 // double    other_radius,
						debugLevel);                    // final int       debugLevel) {
			}
			// show correlations for the second scene
			String prefix = ortho_maps[indices[scene_num]].getName()+"-postfilter1";
			String suffix0 = "_pc"+omp.phaseCoeff[scene_other]+"_lpf"+omp.lpf_sigma[scene_other];
			for (int mode = 2; mode < 4; mode++) { // all 2 correlation types (full and half)
				String suffix = (mode < 2)? "":suffix0;
				ItemMatch.getImageExtracts(
						prefix,          // String                prefix, // include PC, filter here
						suffix,          // String                suffix, // w/o .tiff
						omp.show_images,     // boolean               show,
						omp.save_images,     // boolean               save,
						false,           // boolean               show_removed,
						omp.save_dir,        // String                save_dir,
						scene_other,     // int                   scene_num,
						omp.remove_dc,       // boolean               nodc,
						mode,            // int                   mode, // 0 - extract, 1 - masked, 2 - corr full, 3 - corr half
						show_centers,    // boolean               show_centers,
						omp.extr_size,       // int                   extr_size,
						gops[scene_num], // GroundObjectPattern   gop,
						matches_list,    // ArrayList <ItemMatch> matches_list,
						match_sort,      // ArrayList<Integer>    match_sort,
						debugLevel);     // int                   debugLevel)
			}
		}
		// final filter
		boolean[] filt_main_other = new boolean [indices.length]; //{true,false};
		Arrays.fill(filt_main_other, true);
///		int num_removed = 
				filterCandidates( // will filter again if already
				omp,                // OrthoMapsParameters   omp,
				basename,           // String                basename,
				filt_main_other,    // new boolean []            filt_main_other,
				gops[scene_num],    // GroundObjectPattern   gop,
				zoom_level,         // int                   zoom_level,
				scene_agl,          // double                scene_agl,
				scene_xy_offset,    // double []             scene_xy_offset, //second scene vertical projection offset from the first scene
				matches_list,       // ArrayList <ItemMatch> matches_list,
				match_sort,         // ArrayList<Integer>    match_sort,
				debugLevel);        // int                   debugLevel )

		if (!omp.filt_keep) { // show final filtered images again - just extracts (2) and all correlations (4)
			for (int sn = 0; sn < indices.length; sn++) {
				String prefix = ortho_maps[indices[sn]].getName()+"-final";
				String suffix0 = "_pc"+omp.phaseCoeff[sn]+"_lpf"+omp.lpf_sigma[sn];
				for (int mode = 0; mode < 4; mode++) { // all 4 image types
					String suffix = (mode <2)? "":suffix0;
					if ((sn == 0) || (mode != 1)) { // masked - for main scene only
						ItemMatch.getImageExtracts(
								prefix,          // String                prefix, // include PC, filter here
								suffix,          // String                suffix, // w/o .tiff
								omp.show_images,     // boolean               show,
								omp.save_images,     // boolean               save,
								false,           // boolean               show_removed,
								omp.save_dir,        // String                save_dir,
								sn,              // int                   scene_num,
								omp.remove_dc,       // boolean               nodc,
								mode,            // int                   mode, // 0 - extract, 1 - masked, 2 - corr full, 3 - corr half
								show_centers,    // boolean               show_centers,
								omp.extr_size,       // int                   extr_size,
								gops[scene_num], // GroundObjectPattern   gop,
								matches_list,    // ArrayList <ItemMatch> matches_list,
								match_sort,      // ArrayList<Integer>    match_sort,
								debugLevel);     // int                   debugLevel)
					}
				}
			}
		}

		omp.generateReport( // final
				null,                        // String                log_line, // should end with \n or be null
				true,                        // boolean              show, // ands with .show_images
				true,                        // boolean               save, // ands with .save_images
				true,                        // boolean               gen_results,
				true,                        // boolean               gen_parameters,
				ortho_maps,                  // OrthoMap []           ortho_maps,			
				indices,                     // int []                indices,
				match_sort,                  // ArrayList<Integer>    match_sort,
				matches_list,                // ArrayList <ItemMatch> matches_list,
				zoom_level,                  // int                   zoom_level,
				scene_xy_offset,             // double []             scene_xy_offset,
				scene_agl,                   // double                scene_agl,
				corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
				origin,                      // int []                origin,
				centers,                     // double [][]           centers,
				gops[scene_num],             // GroundObjectPattern   gop,
				hi_lo_freq,                  // double                hi_lo_freq,
				hi_freq_wh,                  // int []                hi_freq_wh,
				hi_freq_arr);                // double [][]           hi_freq_arr,
		if (debugLevel > -3) { 
			omp.printReport(
					true,                        // boolean               gen_results,
					true,                        // boolean               gen_parameters,
					ortho_maps,                  // OrthoMap []           ortho_maps,			
					indices,                     // int []                indices,
					match_sort,                  // ArrayList<Integer>    match_sort,
					matches_list,                // ArrayList <ItemMatch> matches_list,
					zoom_level,                  // int                   zoom_level,
					scene_xy_offset,             // double []             scene_xy_offset,
					scene_agl,                   // double                scene_agl,
					corrs_out[scene_num].length, // int                   num_patterns, // corrs_out[scene_num].length
					origin,                      // int []                origin,
					centers,                     // double [][]           centers,
					gops[scene_num],             // GroundObjectPattern   gop,
					hi_lo_freq,                  // double                hi_lo_freq,
					hi_freq_wh,                  // int []                hi_freq_wh,
					hi_freq_arr);                // double [][]           hi_freq_arr,
		}		
		
		ImagePlus img_final = null;
		{ // Create image with marked mines				
			PointRoi final_roi = new PointRoi();
			final_roi.setOptions("label");
			for (int i = 0; i < match_sort.size(); i++) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				//						double [] match_values = match.getMatchValues(gops[scene_num]);
				double [] center_xy = match.getXY();
				final_roi.addPoint(center_xy[0], center_xy[1]); // ,1);
			}
			String []  final_titles = new String[indices.length];
			double [][] final_map = new double [indices.length][];
			for (int i = 0; i < final_map.length; i++) {
				final_titles[i] = ortho_maps[indices[i]].getName();
				final_map[i] =  dmulti[i];
			}
			String final_title =indices[0]+"";
			for (int i = 1; i < final_map.length; i++) {
				final_title +="-"+indices[i];
			}
			final_title += "_"+final_titles[0];
			for (int i = 1; i < final_map.length; i++) {
				final_title +="-"+final_titles[i];
			}
			final_title += "-MARKED_"+omp.object_type+"_"+match_sort.size()+"_OBJECTS";
			img_final = ShowDoubleFloatArrays.makeArrays(
					final_map,
					width,
					height,
					final_title,				
					final_titles);
			img_final.setRoi(final_roi);
			showSaveImagePlus(
					img_final,            // ImagePlus imp,
					omp.show_final_image, // boolean   show,
					omp.save_images,      // boolean   save,
					omp.save_dir,         // String    save_dir,
					debugLevel);          // int       debugLevel)
		}

		System.out.println("patternMatchDual(): all correlations DONE");
		return img_final;
	}
	
	public static double[][]  correlateAllPatterns(
			double []    data,
			int          width,
			int          corr_size,    
			double [][]  patterns,  
			boolean      convolve, 
			double       phaseCoeff,
			double       lpf_sigma,
			double [][]  src_marks,
			String       prefix, // ortho_maps[indices[scene_num]].getName(). null - do not show
			String       save_dir,
			boolean      show,
			boolean      save,
			int          debugLevel) {
		double [][] corrs_out = new double [patterns.length][];
		for (int n = 0; n < patterns.length; n++) {
			corrs_out[n]= OrthoMap.correlateWithPattern(
					data,        // final double [] data,
					width,       // final int       width,
					corr_size,   // final int       psize,      // power of 2, such as 64
					patterns[n], // final double [] pattern,    // [psize*psize]
					convolve,    // final boolean   convolve,   // convolve, not correlate
					phaseCoeff,  // final double    phaseCoeff,
					lpf_sigma,   // final double    lpf_sigma, // 0 - do not filter
					debugLevel); // final int       debugLevel) {
		}
		if ((prefix != null) && (show || save)) {
			String [] patt_titles = new String[corrs_out.length];
			for (int i = 0; i < patt_titles.length; i++) {
				patt_titles[i] = "patt_"+i;
			}
			int height = data.length/width;
			ImagePlus imp_corr = ShowDoubleFloatArrays.makeArrays(
					corrs_out,
					width,
					height,
					prefix+"-PATTERN_CORRS_PC"+phaseCoeff+"_LPF"+lpf_sigma,				
					patt_titles); // test_titles,
			if (src_marks != null) {
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imp_corr.setRoi(roi);
			}
		    showSaveImagePlus(
		    		imp_corr,         // ImagePlus imp,
		            show, // boolean   show,
		            save, // boolean   save,
		            save_dir,    // String    save_dir,
		            debugLevel); // int       debugLevel)
//			imp_corr.show();
		}
		return corrs_out;
	}
	
	/**
	 * Returns boolean array of original length
	 * @param filt_keep
	 * @param filt_main_other
	 * @param min_corrs
	 * @param min_corr_full_rel
	 * @param filt_max_radius
	 * @param filt_elongation
	 * @param filt_dist
	 * @param filt_height
	 * @param filter_data
	 * @param scene_xy_offset
	 * @param matches_list
	 * @param match_sort
	 * @param debugLevel
	 * @return
	 */
	public static int filterCandidates(
			OrthoMapsParameters   omp,
			String                basename,
			boolean []            filt_main_other, // length== number of scenes
			GroundObjectPattern   gop,
			int                   zoom_level,
			double                scene_agl,
			double []             scene_xy_offset, //second scene vertical projection offset from the first scene
			ArrayList <ItemMatch> matches_list,
			ArrayList<Integer>    match_sort,
			int                   debugLevel ) { // >0 - print
		StringBuffer sb =    new StringBuffer();
		int num_scenes = filt_main_other.length;
		int num_removed = 0;
		double [][] min_corrs_other = {
				{omp.min_corrs[0]*omp.min_corr_full_rel, omp.min_corrs[0]},
				{omp.min_corrs[1]*omp.min_corr_full_rel, omp.min_corrs[1]}};
		double offset_from_height = elevationToParallelOffset( // will be negative
				omp.filt_height,     // double    height,
				scene_xy_offset, // double [] scene_offset,
				zoom_level,      // int       zoom_level,
				scene_agl) ;//  double    agl)
		for (int mn=0; mn < match_sort.size(); mn++){
			ItemMatch match = matches_list.get(match_sort.get(mn));
			if (match.isRemoved()) {
				continue; // already removed
			}
			CorrelationPeakStats[][] filter_data = match.filter_data;
			
			int [] ixy = match.getIntXY();
			String name = ixy[0]+"/"+ixy[1];
			double abs_contrast = match.getAbsoluteContrast();
			double roundness = match.getRoundness(gop);
			int best_patt= match.getPatternMatch(gop).getBestSub(); // 1-based
			double best_value = match.getMatchBestValue(gop);
			boolean is_partial = best_patt != 1; // only apply to combined full/half correlations, just full may have nearby maximums near the same size
			double [] strength_full_half_ratio = new double[num_scenes];
			for (int scene_num = 0; scene_num < num_scenes; scene_num++) if (filt_main_other[scene_num] && (filter_data[scene_num] != null)){
					strength_full_half_ratio[scene_num] = filter_data[scene_num][0].best_d / filter_data[scene_num][1].best_d;
			}
			// filt_abs_easepart
			double min_abs_contrast = omp.filt_abs_contrast;
			if (is_partial) {
				min_abs_contrast *= omp.filt_abs_easepart;
			}
			if (abs_contrast < min_abs_contrast) {
				sb.append(name+": filtered out by "+NAME_MO[0]+" scene because its absolute contrast = "+
						abs_contrast+ " < "+min_abs_contrast+(is_partial?(". Eased for partial from "+omp.filt_abs_contrast):""));
				sb.append("\n");
				if (!match.isRemoved()) num_removed++;
				match.remove();
			}
			if (!is_partial && !(roundness >= omp.filt_roundness[0])) { // NaN - also bad
				sb.append(name+": filtered out by "+NAME_MO[0]+" scene because it is full/round, and its roundness = "+
						roundness+ " < "+omp.filt_roundness[0]);
				sb.append("\n");
				if (!match.isRemoved()) num_removed++;
				match.remove();
			}
			// only apply to full patterns, because partial can be filtered without it
			if (!is_partial && !(best_value >= omp.filt_best[0])) { // NaN - also bad
				sb.append(name+": filtered out by "+NAME_MO[0]+
						" scene because it is full-pattern and its best correlation value = "+
						best_value+ " < "+omp.filt_best[0]);
				sb.append("\n");
				if (!match.isRemoved()) num_removed++;
				match.remove();
			}
			
			for (int hf = 0; hf < 2; hf++) {
				for (int scene_num = 0; scene_num < num_scenes; scene_num++) if (filt_main_other[scene_num]){
					if (Double.isNaN(filter_data[scene_num][hf].best_d)) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+"/"+NAME_FH[hf]+
								" as correlation maximum is not found");
						sb.append("\n");						
						if (!match.isRemoved()) num_removed++;
						match.remove();
						continue;
					}
					double eff_rad =       filter_data[scene_num][hf].eff_rad;
					double elong =         filter_data[scene_num][hf].elong;
					double corr_val =      filter_data[scene_num][hf].best_d;
					double dist =          filter_data[scene_num][hf].dist;
					double near_max_frac = filter_data[scene_num][hf].max_other;
					double [] second_xy_offsets = filter_data[scene_num][hf].cent_offs;					
					// calculate projection
					double [] scnd_xy_proj =  projectOffsetOnVector(
							second_xy_offsets, // double [] object_offset,
							scene_xy_offset); // double [] scene_offset)
					if (corr_val < min_corrs_other[scene_num][hf]) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+" correlation maximum value for "+
								NAME_FH[hf]+ " = "+corr_val+" < "+ min_corrs_other[scene_num][hf]);
						sb.append("\n");						if (scene_num == 0) {
							sb.append(
							"For the main scene it could happen if the sub_pattern index changed after"+
							" the absolute contrast evaluation.");
							sb.append("\n");
						}
						if (!match.isRemoved()) num_removed++;
						match.remove();
					}
					if (is_partial && (hf > 0) && (corr_val < omp.min_corr_half_rel * omp.min_corrs[scene_num])) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+
								" because it is partial and correlation maximum value for "+
								NAME_FH[hf]+ " = "+corr_val+" < "+ omp.min_corr_half_rel*min_corrs_other[scene_num][hf]+
								" (increased by "+omp.min_corr_half_rel+" from base "+omp.min_corrs[scene_num]+").");
						sb.append("\n");
						if (!match.isRemoved()) num_removed++;
						match.remove();
					}
					
					if (scene_num > 0) { // other scene
						//	{best_d, eff_rad, elong, dist, cent_offs[0], cent_offs[1]} ;
						if (dist > omp.filt_dist[0]) {
							sb.append(name+": filtered out by "+NAME_MO[scene_num]+" distance from the main scene peak "+
									NAME_FH[hf]+ " = "+dist+" > "+omp.filt_dist[0]);
							sb.append("\n");
							if (!match.isRemoved()) num_removed++;
							match.remove();
						}
						// distance for main only or both?
						if (Math.abs(scnd_xy_proj[1]) > omp.filt_dist[1]) {
							sb.append(name+": filtered out by "+NAME_MO[scene_num]+" distance from the main scene peak perpendicular to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[1]+", abs() > "+omp.filt_dist[1]);
							sb.append("\n");
							if (!match.isRemoved()) num_removed++;
							match.remove();
						}
						// offset_from_height
						if (scnd_xy_proj[0] > omp.filt_dist[1]) {
							sb.append(name+": filtered out by "+NAME_MO[scene_num]+
									" distance from the main scene peak parallel to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[0]+" > "+omp.filt_dist[1]+
									" it could happen if the object is deep below average ground surface.");
							sb.append("\n");
							if (!match.isRemoved()) num_removed++;
							match.remove();
						}
						if (scnd_xy_proj[0] < (-omp.filt_dist[1]+offset_from_height)) {
							sb.append(name+": filtered out by "+NAME_MO[scene_num]+
									" distance from the main scene peak parallel to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[0]+" < "+(-omp.filt_dist[1]+offset_from_height)+
									".\n This limit accounts for objects above ground up to "+omp.filt_height+" meters ("+
									offset_from_height+" pix.");
							sb.append("\n");
							if (!match.isRemoved()) num_removed++;
							match.remove();
						}
					}
					if (eff_rad > omp.filt_max_radius[scene_num][hf]) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+" scene radius for "+
								NAME_FH[hf]+ " pattern: "+eff_rad+" > "+ omp.filt_max_radius[scene_num][hf]);
						sb.append("\n");
						if (!match.isRemoved()) num_removed++;
						match.remove();
					}
					if (elong > omp.filt_elongation[scene_num][hf]) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+" scene peak cros-section elongation for "+
								NAME_FH[hf]+ " pattern: "+elong+" > "+ omp.filt_elongation[scene_num][hf]);
						sb.append("\n");
						if (!match.isRemoved()) num_removed++;
						match.remove();
					}
					if (is_partial && (omp.filt_other_frac != null) && (hf > 0) && (near_max_frac > omp.filt_other_frac[scene_num])) {
						sb.append(name+": filtered out by "+NAME_MO[scene_num]+
								" because it matches partial pattern ("+best_patt+") and combined full/partial correlation has a near peak "+
								near_max_frac+" > "+ omp.filt_other_frac[scene_num]+" of the used peak.");
						sb.append("\n");
						if (!match.isRemoved()) num_removed++;
						match.remove();
					}
					if (hf==0) {
						if ( strength_full_half_ratio[scene_num] < omp.filt_full_half_frac[scene_num]) {
							sb.append(name+": filtered out by "+NAME_MO[scene_num]+
									" because its correlation with the full patern strengh ratio to the partial pattern one = "+
									strength_full_half_ratio[scene_num]+" < "+ omp.filt_full_half_frac[scene_num]+" (minimum allowed).");
							sb.append("\n");
							if (!match.isRemoved()) num_removed++;
							match.remove();
						}
					}
				}				
			}
		}
		StringBuffer sb1 = new StringBuffer();
		if (! omp.filt_keep) {
			if (num_removed > 0) {
				for (int mn=match_sort.size()-1; mn >=0; mn--) {
					ItemMatch match = matches_list.get(match_sort.get(mn));
					if (match.isRemoved()) {
						match_sort.remove(mn);
					}
				}
				sb1.append("Filtered out "+num_removed+
						" scenes by the filter.\n");
			} else {
				sb1.append("No scenes were filtered out by the filter\n");
			}
			sb1.append(match_sort.size()+" candidate(s) remain\n");
		}
		if (debugLevel > 0) {
			System.out.println(sb.toString());
		}
		if (debugLevel > -3) {
			System.out.println(sb1.toString());
		}
		sb.append(sb1);
		if (omp.append_log && (omp.save_dir!= null)) {
			String log_path = omp.save_dir+basename+OrthoMapsParameters.LOG_SUFFIX;
			CalibrationFileManagement.saveStringToFile (
					log_path,      //String path,
					sb.toString(), // data,
					true);         // boolean append)
		}
		return num_removed;
	}

	/**
	 * Set absolute contrasts for each object, optionally changing sub-pattern index. Absolute contrast
	 * is a difference between average (w/o outliers) data value in zone1 (center area of the pattern) and
	 * the ring around it (zone2).
	 * On return the matches_list data is modified - absolute contrasts are set, and best_pattern may be
	 * updated. 
	 * Gets extracted_objects square arrays for each of the object, so the center of objects is in the
	 * center of the square from ItemMatch instance from the list. Calculated (for debug) masked image is 
	 * also saved there.
	 * 
	 * @param force_round overwrite mode with 4 - force pattern index to full (round) one
	 * @param mode Update of the best sub-pattern index:
	 *             0 - keep current
	 *             1 - keep type (full/half) and allow half direction to change by -1,0, or +1
	 *             2 - keep type (full/half)
	 *             3 - no restrictions - find the best sub-pattern index from scratch,
	 *             4 - force round (full pattern)
	 * @param neg_better    center lower (colder) than around
	 * @param outliers_frac fraction of outliers to remove while calculating averages
	 * @param obscure_warm allow obscurant (that makes half-pattern from a full one only to be warmer
	 *        than the object (defined below fraction between the object center region and peripheral
	 *        ring. For hot objects obscurant should be colder than such fraction.
	 * @param obscure_frac fraction between center average and peripheral ring to compare when obscure_warm
	 *        is set.
	 * @param gop  GroundObjectPattern instance used
	 * @param matches_list list of pattern matches
	 * @param match_sort   sorted list of indices in pattern matches list, so the first is the best match.
	 * @param scene_num - 0 for main, 1 - "other" 
	 * @param ipatterns integer square patterns with the same dimensions as the extracted_objects. Value
	 *        0 - do not use this pixel, 1 - Zone1 (inside the pattern), 2 - Zone2 - reference ring around
	 *        the pattern.
	 * @param debugLevel debug level. >0 - display images, >-4 - print.
	 * @return true
	 */
	public static boolean setAbsoluteContrasts(
			OrthoMapsParameters   omp,
			/*
			boolean               force_round,
			int                   mode, //  0 - keep, 1 keep type and if half +/-1, 2 - keep type, 3 - any
			boolean               neg_better, // more negative is difference - stronger result
			double                outliers_frac,
			boolean               obscure_warm, // =  true; // obscured can only be by warmer objects 
			double                obscure_frac, //  0.25; // obscured threshold between center and outer
			*/ 
			GroundObjectPattern   gop,
			ArrayList <ItemMatch> matches_list,
			ArrayList<Integer>    match_sort,
			int                   scene_num,
			int [][]              ipatterns,
			int                   debugLevel) {
		boolean   debug = debugLevel > 1; 
		int mode = omp.abs_mode;
		for (int mn=0; mn < match_sort.size(); mn++) {
			if (debugLevel > 1) {
				System.out.println("setAbsoluteContrasts(): Calculating absolute contrast for match #"+mn);
			}
			int indx =match_sort.get(mn);
			ItemMatch match = matches_list.get(indx);
			boolean [] try_sub = new boolean [ipatterns.length];
			int best_patt= match.getPatternMatch(gop).getBestSub() - 1; // 0-based
			int num_halves = ipatterns.length - 1; // == 8
			if (omp.abs_force_round) {
				mode = 4;
			}
			switch (mode) {
			case 0: try_sub[best_patt] = true; 	break; // only one
			case 1: // keep type and +-1 for direction
				if (best_patt == 0) {
					try_sub[0] = true;
				} else {
					for (int offs = -1; offs <= 1 ; offs++) {
						int i = best_patt+offs;
						if (i < 1) {
							i += num_halves;
						} else if (i > num_halves) {
							i -= num_halves;
						}
						try_sub[i] = true;
					}
				}
				break;
			case 2: // keep type
				if (best_patt == 0) {
					try_sub[0] = true;
				} else {
					for (int i = 1; i < try_sub.length; i++) {
						try_sub[i] = true;
					}
				}
				break;
			case 3: // any
				for (int i = 0; i < try_sub.length; i++) {
					try_sub[i] = true;
				}
				break;
			case 4: try_sub[0] = true; 	break; // only full pattern
			}
			double [][] dir_contrasts = new double [try_sub.length][];
			int best_index = -1;
			for (int i = 0; i < try_sub.length; i++) if ((i==0) || try_sub[i]) {
				dir_contrasts[i]= OrthoMap.getAbsoluteContrast(
						match.extracted_objects[scene_num], // extracted_objects[mn], // double [] data,
						ipatterns[i],                       // int    [] ipattern,
						omp.abs_outliers_frac,                      // double    outliers_frac,
						omp.abs_obscure_frac,                       // double    obscure_frac, //  0.25; // obscured threshold between center and outer 
						null,
						debug);                             // boolean   debug)				
				if (omp.abs_invert) {
					dir_contrasts[i][0] *= -1;
				}
				boolean bad_obscurant = omp.abs_obscure_warm && (dir_contrasts[i][1] < 0);
				if (try_sub[i] && (dir_contrasts[i][0] > 0)) {
					if (bad_obscurant) {
						if (debugLevel > -4) {
							System.out.println("setAbsoluteContrasts(): removing pattern_sub="+i
									+", match object="+mn+" because of the bad obscurant (cold for cold object)");
						}
					} else {
						best_index = i;
					}
				}
				
			}
			if (best_index < 0) {
				best_index = 0;
				
				if (debugLevel > -3) {
					System.out.println("setAbsoluteContrasts(): no candidates found (probably after removing bad obscurants) #"
							+mn+", giving a chance as full pattern. Was (1 for full):"+
							(best_patt+1)+", new is "+(best_index + 1));
				}
			}
			if (best_index != best_patt) {
				if (debugLevel > -3) {
					System.out.println("setAbsoluteContrasts(): updating best pattern index for match #"
							+mn+". Was (1 for full):"+(best_patt+1)+", new is "+(best_index + 1));
				}
				match.getPatternMatch(gop).setBestSub(best_index + 1);
			}
			match.setAbsoluteContrast(dir_contrasts[best_index][0]); // the higher the better
			if (debugLevel > -3) {
				System.out.println("setAbsoluteContrasts(): Set absolute contrast for match #"+mn+" = "+
						dir_contrasts[best_index][0]+
						", over threshold="+dir_contrasts[best_index][1]+".");
			}
			match.extracted_masked[scene_num] = new double [match.extracted_objects[scene_num].length];
			OrthoMap.getAbsoluteContrast( // calculate and set extracted_masked
					match.extracted_objects[scene_num], // double [] data,
					ipatterns[best_index],              // int    [] ipattern,
					omp.abs_outliers_frac,              // double    outliers_frac,
					omp.abs_obscure_frac,               // double    obscure_frac, //  0.25; // obscured threshold between center and outer 
					match.extracted_masked[scene_num],
					debug);                             // boolean   debug)				
		}		
		return true;
	}
	
	
	public String selectOneScene(
			int num_scene,
			int default_choice,
			int num_choice_lines) {
		String [] fs = {"first","second"};
		String [] lines = new String [ortho_maps.length];
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz
		for (int indx = 0; indx < ortho_maps.length; indx++) {
			OrthoMap map = ortho_maps[indx];
			LocalDateTime ldt = map.getLocalDateTime();
			ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC
			ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of("Europe/Kyiv"));
			String sdt = zonedDateTime.format(formatter);
			String name = map.getName();
			double agl = map.getAGL();
			lines[indx]=String.format(
					"%3d %17s %26s %6.2f",indx, name, sdt, agl);
		}
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select the "+fs[num_scene]+" image from the list ",1200,100);
		gd.addChoice(fs[num_scene]+" image:",
				lines,
				lines[default_choice],
				"Select "+fs[num_scene]+" scene", num_choice_lines);
		gd.showDialog();
		if (gd.wasCanceled()) return null;
		int scene_number= gd.getNextChoiceIndex();
		return ortho_maps[scene_number].getName();
	}

	public String[] selectTwoScenes(
			int default_choice,
			int num_choice_lines) {
		String [] fs = {"first","second"};
		String [] lines = new String [ortho_maps.length];
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz
		for (int indx = 0; indx < ortho_maps.length; indx++) {
			OrthoMap map = ortho_maps[indx];
			LocalDateTime ldt = map.getLocalDateTime();
			ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC
			ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of("Europe/Kyiv"));
			String sdt = zonedDateTime.format(formatter);
			String name = map.getName();
			double agl = map.getAGL();
			lines[indx]=String.format(
					"%3d %17s %26s %6.2f",indx, name, sdt, agl);
		}
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select two image scenes",1200,150);
		for (int num_scene = 0; num_scene < 2; num_scene++) {
			gd.addChoice(fs[num_scene]+" image:",
					lines,
					lines[default_choice],
					"Select "+fs[num_scene]+" scene", num_choice_lines);
		}
		gd.showDialog();
		if (gd.wasCanceled()) return null;
		String [] names = new String[2];
		for (int num_scene = 0; num_scene < 2; num_scene++) {
			int scene_number= gd.getNextChoiceIndex();
			names[num_scene] = ortho_maps[scene_number].getName();
		}		
		return names;
	}

	
	
	public String [] getScenesList(int [] indices) {
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int indx = 0; indx < ortho_maps.length; indx++) {
				indices[indx] = indx;	
			}
		}
		String [] lines = new String [indices.length];
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz
		int dbg_indx = -530;
		for (int i = 0; i < indices.length; i++) {
			int indx = indices[i];
			if (indx == dbg_indx) {
				System.out.println("indx="+indx);
			}
			OrthoMap map = ortho_maps[indx];
			LocalDateTime ldt = map.getLocalDateTime();
			ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC
			ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of("Europe/Kyiv"));
			String sdt = zonedDateTime.format(formatter);
			String name = map.getName();
			double agl = map.getAGL();
			lines[i]=String.format(
					"%3d %17s %26s %6.2f",indx, name, sdt, agl);
		}
		return lines;
	}

	public int [] getScenesSelection(
			CLTParameters    clt_parameters,			
			String purpose) { // " to generate stats", " to generate images"
		boolean  filter_scenes = true;
		boolean  select_scenes = true;
		double   flt_min_sfm = clt_parameters.imp.flt_min_sfm;
		double   flt_max_sfm = clt_parameters.imp.flt_max_sfm;
//		int      flt_alt = clt_parameters.imp.flt_alt;
		int      flt_orient = clt_parameters.imp.flt_orient;
		boolean  flt_update_config = false;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select scenes filter",800,400);
		gd.addCheckbox    ("Filter scenes",               filter_scenes, "Filter scenes.");
		gd.addCheckbox    ("Select scenes",               select_scenes, "Select scenes manually.");
		gd.addNumericField("Minimal SfM gain",            flt_min_sfm,  3,7,"","Minimal SfM gain of the scenes.");
		gd.addNumericField("Maximal SfM gain",            flt_max_sfm,  3,7,"","Maximal SfM gain of the scenes.");
//		gd. addChoice("Filter by pairwise ALT availability",IntersceneMatchParameters.FLT_ALT_MODES, IntersceneMatchParameters.FLT_ALT_MODES[flt_alt],"Filter by pairwise ALT availability.");
		gd. addChoice("Filter by orientation availability",IntersceneMatchParameters.FLT_ORIENT_MODES, IntersceneMatchParameters.FLT_ORIENT_MODES[flt_orient],"Filter by the scene orientation availability.");
		gd.addCheckbox    ("Update configuration",        flt_update_config, "Update matching configuration parameters to be saved as defaults.");

		gd.showDialog();
		if (gd.wasCanceled()) return null;
		filter_scenes =        gd.getNextBoolean();
		select_scenes =        gd.getNextBoolean();
		flt_min_sfm =          gd.getNextNumber();
		flt_max_sfm =          gd.getNextNumber();
//        flt_alt =              gd.getNextChoiceIndex();
        flt_orient =           gd.getNextChoiceIndex();
        flt_update_config =    gd.getNextBoolean();
        if (flt_update_config) {
    		clt_parameters.imp.flt_min_sfm = flt_min_sfm;
    		clt_parameters.imp.flt_max_sfm = flt_max_sfm; 
//    		clt_parameters.imp.flt_alt =     flt_alt;     
    		clt_parameters.imp.flt_orient =  flt_orient;  
        }
		int [] indices = new int [ortho_maps.length];
		for (int indx = 0; indx < ortho_maps.length; indx++) {
			indices[indx] = indx;	
		}
		if (filter_scenes) {
			boolean [] selection = new boolean [ortho_maps.length];
			int num_selected = 0;
			for (int indx = 0; indx < ortho_maps.length; indx++) {
				if ((ortho_maps[indx].getSfmGain()<flt_min_sfm) || (ortho_maps[indx].getSfmGain()>flt_max_sfm)) {
					selection[indx]=false;
					continue;
				}
				if ((ortho_maps[indx].getQOrinet()==null) && (flt_orient == 1)) {
					selection[indx]=false;
					continue;
				}
				if ((ortho_maps[indx].getQOrinet()!=null) && (flt_orient == 2)) {
					selection[indx]=false;
					continue;
				}
				selection[indx]=true;
				num_selected++;
			}
			int [] selected_indices = new int [num_selected];
			int indx = 0;
			for (int i = 0; i < ortho_maps.length; i++) if (selection[i]){
				selected_indices[indx++] = i;
			}
			indices = selected_indices;
		}
		if (select_scenes) {
			String [] lines = getScenesList(indices);
			boolean [] selection = new boolean [indices.length];
			gd = new GenericJTabbedDialog("Select scenes",1200,1000);
			gd.addCheckbox    ("Select all",   false, "Select all scenes listed");
			for (int i = 0; i < lines.length; i++) {
				gd.addCheckbox    (lines[i],   false, "Select scene number "+indices[i]);
			}
			gd.showDialog();
			if (gd.wasCanceled()) return null;
			boolean sel_all =  gd.getNextBoolean();
			int num_selected = 0;
			for (int i = 0; i < lines.length; i++) {
				selection[i] = gd.getNextBoolean();
				if (selection[i]) num_selected++;
			}
			if (sel_all) {
				Arrays.fill(selection,  true);
			} else {
				int [] sel_indices = new int [num_selected];
				int indx=0;
				for (int i = 0; i < lines.length; i++) if (selection[i]) {
					sel_indices[indx++] = indices[i];
				}
				indices = sel_indices;
			}
		}
		return indices;
	}
	
	public int [] getScenesSelection(
			boolean [] pre_selects,
			String purpose) { // " to generate stats", " to generate images"
		String [] lines = getScenesList(null);
		int [] sel_50 = {125,126,127,129,135,136,137,138,139,140,141,144,145,146,147,148,155,156,157,158};
		int [] sel_75 = {170,177,178,183,187,188,189,192,193,199,200,204};
		int [] sel_100 = {207,208,212,213,214,215,218,220};
		int [] range_50_evn=  {  0, 48};
		int [] range_25_mor=  { 49,124};
		int [] range_50_mor=  {125,164};
		int [] range_75_mor=  {166,204};
		int [] range_100_mor= {205,221};
		int [][] ranges = {null,null,null,null,range_50_evn,range_25_mor,range_50_mor,range_75_mor,range_100_mor};
		
		boolean [] selection = new boolean [lines.length];
		if (pre_selects != null) {
			if (pre_selects[0]) {
				Arrays.fill(selection, true);
			}
			if (pre_selects[1]) {
				for (int i:sel_50) {
					selection[i] = true;
				}
			}
			if (pre_selects[2]) {
				for (int i:sel_75) {
					selection[i] = true;
				}
			}
			if (pre_selects[3]) {
				for (int i:sel_100) {
					selection[i] = true;
				}
			}
			for (int n = 4; n < 9; n++) if (pre_selects[n]){
				for (int i = ranges[n][0]; i<=ranges[n][1]; i++) {
					selection[i] = true;
				}
			}
		}
		pre_selects = new boolean[9];
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select scenes",1200,1000);
		gd.addCheckbox    ("Select all maps",         pre_selects[0], "Select all scenes, reopen this dialog.");
		gd.addCheckbox    ("Select  50m used maps",   pre_selects[1], "Select 50m scenes, reopen this dialog.");
		gd.addCheckbox    ("Select  75m used maps",   pre_selects[2], "Select 50m scenes, reopen this dialog.");
		gd.addCheckbox    ("Select 100m used maps",   pre_selects[3], "Select 50m scenes, reopen this dialog.");
		gd.addCheckbox    ("Select all 50m evening",  pre_selects[4], "Select all 50m evening scenes, reopen this dialog.");
		gd.addCheckbox    ("Select all 25m morning",  pre_selects[5], "Select all 25m morning scenes, reopen this dialog.");
		gd.addCheckbox    ("Select all 50m morning",  pre_selects[6], "Select all 50m morning scenes, reopen this dialog.");
		gd.addCheckbox    ("Select all 75m morning",  pre_selects[7], "Select all 75m morning scenes, reopen this dialog.");
		gd.addCheckbox    ("Select all 100m morning", pre_selects[8], "Select all 100m morning scenes, reopen this dialog.");
		for (int i = 0; i < lines.length; i++) {
			gd.addCheckbox    (lines[i],   selection[i], "Select scene number "+i);
		}
		gd.showDialog();
		if (gd.wasCanceled()) return null;
		boolean presel_any = false;
		for (int i = 0; i < pre_selects.length; i++) { 
			pre_selects[i]= gd.getNextBoolean();
			presel_any |= pre_selects[i]; 
		}
		if (presel_any) {
			return getScenesSelection(pre_selects, purpose);
		}
		int num_sel = 0;
		for (int i = 0; i < selection.length; i++) {
			selection[i] = gd.getNextBoolean();
			if (selection[i]) {
				num_sel++;
			}
		}
		int [] indices = new int [num_sel];
		int indx=0;
		for (int i = 0; i < selection.length; i++) if (selection[i]) {
			indices[indx++] = i;
		}		
		return indices;
	}
	
	public boolean equalizeIntersectedPairs(
			CLTParameters    clt_parameters,
			String orthoMapsCollection_path) {
		boolean  use_inv = false;
		int [] indices = getScenesSelection(
				clt_parameters, // CLTParameters       clt_parameters,				
				" to build a equalize intensities"); // String purpose)
		PairwiseOrthoMatch [][] matches = new PairwiseOrthoMatch[indices.length][indices.length];
		ArrayList<Point> pairs_list = new ArrayList<Point>();
		int num_new = 0;
		for (int i = 0; i < indices.length-1; i++) {
			int scene0 = indices[i]; 
			for (int j = i+1; j < indices.length; j++){
				int scene1 = indices[j];
				PairwiseOrthoMatch match = ortho_maps[scene0].getMatch(ortho_maps[scene1].getName());
				PairwiseOrthoMatch inv_match = use_inv? ortho_maps[scene1].getMatch(ortho_maps[scene0].getName()):null;
				if ((match != null) || (inv_match != null)){
					if (match == null) {
						double [] enuOffset = ortho_maps[scene0].enuOffsetTo(ortho_maps[scene1]);
						double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
						match = inv_match.getInverse(rd);
					}
					if (inv_match == null) {
						double [] enuOffset = ortho_maps[scene1].enuOffsetTo(ortho_maps[scene0]);
						double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
						inv_match = match.getInverse(rd);
					}
				}
				if (match != null) {
					if (!match.isSetEqualize2to1()) {
						num_new++;
					}
					matches[i][j] = match;
					matches[j][i] = inv_match;
					pairs_list.add(new Point(i,j)); // only once?
				}
			}
		}
	
		boolean      skip_exist    = clt_parameters.imp.pwise_skip_exist;   //
		boolean      save_each     = clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean      log_append    = clt_parameters.imp.pwise_log_append;   //
		String       log_path      = clt_parameters.imp.pwise_log_path;     // 
		int          debugLevel    = clt_parameters.imp.pwise_debug;        //

		GenericJTabbedDialog gd = new GenericJTabbedDialog("Pairwise Match Parameters",1200,1000);
		gd.addMessage("Number of scenes - "+indices.length+
				", number of pairs - "+pairs_list.size()+
				", number of new pairs - "+num_new);
	    gd.addCheckbox    ("Skip existing",              skip_exist, "Do not regenerate if match with same or higher resolution exists.");
   	    gd.addCheckbox    ("Save state after each pair", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gd.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gd.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gd.addNumericField("Pairwise match debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
 		gd.showDialog();
		if (gd.wasCanceled()) return false;
		skip_exist       = gd.getNextBoolean();
		save_each        = gd.getNextBoolean();
		log_append       = gd.getNextBoolean();
		log_path         = gd.getNextString();
		debugLevel = (int) gd.getNextNumber();
		if (log_append && (log_path != null)) { // assuming directory exists
			StringBuffer sb = new StringBuffer();
			sb.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
			sb.append("num_scenes\t"+  indices.length+"\n");
			sb.append("num_pairs\t"+   pairs_list.size()+"\n");
			sb.append("num_new\t"+     num_new+"\n");
			sb.append(String.format("%4s\t%4s\t%17s\t%17s\t%6s\t%3s\t%6s\t%8s\t%4s\n",
					"scn1","scn2","timestamp1","timestamp2","ovrlp","zl","a","b","old"));
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
			if (debugLevel>-3) {
				System.out.print(sb.toString());
			}
		}
		
		for (Point p:pairs_list) {
			StringBuffer sb = new StringBuffer();
			PairwiseOrthoMatch match = matches[p.x][p.y];
			int scene0 = indices[p.x]; 
			int scene1 = indices[p.y];
			int [] indices_pair = {scene0,scene1}; 

			if (skip_exist && match.isSetEqualize2to1()) {
				sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%6.4f\t%8.4f\tOLD\n",
						scene0, scene1, ortho_maps[scene0].getName(), ortho_maps[scene1].getName(),
						match.overlap,  match.zoom_lev, match.equalize1to0[0],match.equalize1to0[1]));
				
			} else {
				double [][][] affines = {{{1,0,0},{0,1,0}},match.getAffine()};
				int []     wh = new int[2];
				int [] origin = new int[2];			
				double [][] centers = new double [indices.length][];
				double [][] dmulti = renderMultiDouble (
						null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
						indices_pair,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
						true, // boolean       bounds_to_indices,
						affines,           // affines,    // double [][][] affines, // null or [indices.length][2][3]
						null,              // double [][]   equalize,
						true,              // boolean       ignore_equalize,
						null,              // warp,       // FineXYCorr    warp,,
						match.zoom_lev,    // int         zoom_level,
						wh,                // int []      wh,
						origin,            // int []      origin){ // maps[0] as a reference
						centers);          // double [][] centers)

				double [] regression = PolynomialApproximation.getOrthoRegression(
						dmulti[0], // final double []  data_x,
						dmulti[1], // final double []  data_y,
						null);     // final boolean [] mask)
				double [] inv_regression = PolynomialApproximation.invertRegression(regression);
				PolynomialApproximation.applyRegression(
						dmulti[1], // dmulti1,       // final double [] data, // clone by caller
						inv_regression); // final double [] regression) {
				match.setEqualize2to1(inv_regression);
				if (save_each && (orthoMapsCollection_path != null)) {
					try {
						writeOrthoMapsCollection(orthoMapsCollection_path);
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					if (debugLevel > -4) {
						System.out.println("Saved data to "+ orthoMapsCollection_path);
					}
				}
				sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%6.4f\t%8.4f\tNEW\n",
						scene0, scene1, ortho_maps[scene0].getName(), ortho_maps[scene1].getName(),
						match.overlap,  match.zoom_lev, match.equalize1to0[0],match.equalize1to0[1]));
			}
			if (log_append && (log_path != null)) { // assuming directory exists
				CalibrationFileManagement.saveStringToFile (
						log_path,          //String path,
						sb.toString(), // data,
						true); // boolean append)
			}			
			if (debugLevel>-3) {
				System.out.print(sb.toString());
			}
			
		}
		
		if (orthoMapsCollection_path != null) {
			try {
				writeOrthoMapsCollection(orthoMapsCollection_path);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if (debugLevel > -4) {
				System.out.println("Saved data to "+ orthoMapsCollection_path);
			}
		}
		StringBuffer sb = new StringBuffer();
		sb.append("\nSUMMARY\n");
		sb.append("processed\t"+(skip_exist?num_new:pairs_list.size()) +"\n");
		sb.append("new\t"+ num_new+"\n");
		sb.append("skipped\t"+(skip_exist?(pairs_list.size()-num_new):0)+"\n");
    	if (debugLevel > -3) {
    		System.out.print(sb.toString());
    	}
		if (log_append && (log_path != null)) { // assuming directory exists
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
        	if (debugLevel > -4) {
        		System.out.println("Appended log file "+ log_path);
        	}
		}		
		return true;
	}
	
	public boolean getOverlapPairs(
			CLTParameters    clt_parameters,
			String orthoMapsCollection_path) {

		int [] indices = getScenesSelection(
				clt_parameters, // CLTParameters       clt_parameters,				
				" to find intersects"); // String purpose)
		if (indices == null) {
			return false;
		}
		int     zoom_lev =          clt_parameters.imp.pwise_zoom; // -5;
		double  min_overlap_frac =  clt_parameters.imp.pwise_overlap; // 0.25;
		boolean ignore_affines =    false;
		boolean from_scratch =      false;

		
		
		boolean bounds_to_indices = true;
		int debugLevel = clt_parameters.imp.pwise_debug;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Get scene intersections",1200,300);
		gd.addNumericField("Zoom level",                 zoom_lev,  0,4,"",
				"Zoom level: +1 - zoom in twice, -1 - zoom out twice");
		gd.addNumericField("Minimal overlap",            min_overlap_frac,    3,7,"",
				"Minimal overlap as a fraction of the smaller image.");
	    gd.addCheckbox    ("Ignore known affines",       ignore_affines, "Ignore previously calculated scenes' affines.");
	    gd.addCheckbox    ("Start from scratch",         from_scratch,   "Remove all pairwise affines (false - keep them).");

		gd.addNumericField("Pairwise match debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");

		gd.showDialog();
		if (gd.wasCanceled()) return false;
		zoom_lev =     (int) gd.getNextNumber();
		min_overlap_frac =   gd.getNextNumber();
		ignore_affines =     gd.getNextBoolean();
		from_scratch =       gd.getNextBoolean();
		debugLevel=    (int) gd.getNextNumber();
		int [] wh =     new int[2];
		int [] origin = new int[2];
		double [][] centers =new double [indices.length][];
		double [][][] affines = null;
		if (ignore_affines) {
			affines = new double [indices.length][2][3];
			for (int i = 0; i < affines.length; i++) {
				affines[i][0][0] = 1.0;
				affines[i][1][1] = 1.0;
			}
		}
		for (int i = 0; i < indices.length; i++) {
			// 	public void unsetMatches(boolean undefined_only) {
			ortho_maps[indices[i]].unsetMatches(!from_scratch);

		}
		double [][] dmulti = renderMultiDouble (
				null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
				indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				bounds_to_indices, // boolean       bounds_to_indices,
				affines,           // affines,    // double [][][] affines, // null or [indices.length][2][3]
				null,              // double [][]   equalize,
				true,              // boolean       ignore_equalize,
				null,              // warp,       // FineXYCorr    warp,,
				zoom_lev,          // int         zoom_level,
				wh,                // int []      wh,
				origin,            // int []      origin){ // maps[0] as a reference
				centers);          // double [][] centers)

		double [] overlaps = getFracOverlaps(
				dmulti); // final double [][] dmulti) {
		int num_overlaps_all = 0, num_overlaps_sel = 0;
		boolean [][] intersects = new boolean [dmulti.length][dmulti.length];
		for (int i = 0; i < dmulti.length-1; i++) {
			for (int j = i+1; j < dmulti.length; j++) {
				int indx = i*dmulti.length+j;
				double overlap = overlaps[indx];
				if (overlap > 0) {
					num_overlaps_all++;
					if (overlap > min_overlap_frac) {
						String name2 = ortho_maps[indices[j]].getName();
						PairwiseOrthoMatch match = ortho_maps[indices[i]].getMatch(name2);
						if (match == null) {
							match = new PairwiseOrthoMatch (
									null,              // double [][] affine,
									new double [6][6], // double [][] jtj,
									Double.NaN,        // double rms,
									zoom_lev,          // int zoom_lev,
									overlap);          // double overlap)
							ortho_maps[indices[i]].setMatch(name2, match);
						} else {
							match.setOverlap(overlap); // just update overlap
						}
						num_overlaps_sel++;
						intersects[i][j] = true;
					}
				}
			}
		}
		
		int [] groups = new int [dmulti.length];
		for (int i = 0; i < groups.length; i++) {
			groups[i] = i;
		}
		for (int i = 0; i< (groups.length-1); i++) {
			for (int j = i+1; j < groups.length; j++) if (intersects[i][j] && (groups[j] != groups[i])){
				int g0 =  groups[i];
				int g1 =  groups[j];
				for (int k = 0; k < groups.length; k++) {
					if (groups[k] == g1) {
						groups[k] = g0;
					}
				}
			}
		}
		HashSet<Integer> hs = new HashSet<Integer>();
		for (int i:groups) {
			hs.add(i);
		}
		System.out.println("getIntersectedPairs(): num_overlaps_all="+num_overlaps_all+
				", num_overlaps_sel="+num_overlaps_sel+", number of disconnected groups="+hs.size());
		if (debugLevel > -4) {
			ShowDoubleFloatArrays.showArrays( 
					overlaps,
					"Overlaps");
		}
		if (orthoMapsCollection_path != null) {
        	try {
        		writeOrthoMapsCollection(orthoMapsCollection_path);
        	} catch (IOException e) {
        		// TODO Auto-generated catch block
        		e.printStackTrace();
        	}
        	if (debugLevel > -4) {
        		System.out.println("Saved data to "+ orthoMapsCollection_path);
        	}
		}
		return true;
	}

	
	
	
	
	public boolean getIntersectedPairs(
			CLTParameters    clt_parameters,
			String orthoMapsCollection_path) {

		int [] indices = getScenesSelection(
				clt_parameters, // CLTParameters       clt_parameters,				
				" to find intersects"); // String purpose)
		if (indices == null) {
			return false;
		}
		int     zoom_lev =     clt_parameters.imp.pwise_zoom; // -5;
		double  min_overlap_frac =  clt_parameters.imp.pwise_overlap; // 0.25;
		boolean bounds_to_indices = true;
		int debugLevel = clt_parameters.imp.pwise_debug;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Get scene intersections",1200,300);
		gd.addNumericField("Zoom level",                 zoom_lev,  0,4,"",
				"Zoom level: +1 - zoom in twice, -1 - zoom out twice");
		gd.addNumericField("Minimal overlap",                min_overlap_frac,    3,7,"",
				"Minimal overlap as a fraction of the smaller image.");
		gd.addNumericField("Pairwise match debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");

		gd.showDialog();
		if (gd.wasCanceled()) return false;
		zoom_lev =     (int) gd.getNextNumber();
		min_overlap_frac =   gd.getNextNumber();
		debugLevel=    (int) gd.getNextNumber();
		int [] wh =     new int[2];
		int [] origin = new int[2];
		double [][] centers =new double [indices.length][];			
		double [][] dmulti = renderMultiDouble (
				null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
				indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				bounds_to_indices, // boolean       bounds_to_indices,
				null,              // affines,    // double [][][] affines, // null or [indices.length][2][3]
				null,              // double [][]   equalize,
				true,              // boolean       ignore_equalize,
				null,              // warp,       // FineXYCorr    warp,,
				zoom_lev,          // int         zoom_level,
				wh,                // int []      wh,
				origin,            // int []      origin){ // maps[0] as a reference
				centers);          // double [][] centers)

		double [] overlaps = getFracOverlaps(
				dmulti); // final double [][] dmulti) {
		int num_overlaps_all = 0, num_overlaps_sel = 0;
		boolean [][] intersects = new boolean [dmulti.length][dmulti.length];
		for (int i = 0; i < overlaps.length; i++) if (overlaps[i] > 0){
			num_overlaps_all++;
			if (overlaps[i] >= min_overlap_frac) {
				num_overlaps_sel++;
				int row = i / dmulti.length;
				int col = i % dmulti.length;
				intersects[row][col] = true;
			}
		}
		int [] groups = new int [dmulti.length];
		for (int i = 0; i < groups.length; i++) {
			groups[i] = i;
		}
		for (int i = 0; i< (groups.length-1); i++) {
			for (int j = i+1; j < groups.length; j++) if (intersects[i][j] && (groups[j] != groups[i])){
				int g0 =  groups[i];
				int g1 =  groups[j];
				for (int k = 0; k < groups.length; k++) {
					if (groups[k] == g1) {
						groups[k] = g0;
					}
				}
			}
		}
		HashSet<Integer> hs = new HashSet<Integer>();
		for (int i:groups) {
			hs.add(i);
		}
		System.out.println("getIntersectedPairs(): num_overlaps_all="+num_overlaps_all+
				", num_overlaps_sel="+num_overlaps_sel+", number of disconnected groups="+hs.size());
		if (debugLevel > -4) {
			ShowDoubleFloatArrays.showArrays( 
					overlaps,
					"Overlaps");
		}

		boolean matches_ok= generatePairwiseMatches(
				clt_parameters,            // CLTParameters clt_parameters,
				min_overlap_frac,          // double  min_overlap_frac
				indices,                   // int []        indices,
				overlaps,                  // double [] overlaps,
				//			intersects,                // boolean [][]  boverlaps)
				orthoMapsCollection_path); // String orthoMapsCollection_path
		return matches_ok;
	}
	
	
	
	
	public boolean generatePairwiseMatches(
			CLTParameters clt_parameters,
			double        min_overlap_frac,
			int []        indices,
			double []     overlaps,			
			String        orthoMapsCollection_path) {
// gen number of pairs:
		int num_scenes = indices.length;
		int num_pairs = 0;
		int num_new = 0;
		for (int i = 0; i < indices.length-1; i++) {
			for (int j = i+1; j < indices.length; j++) if (overlaps[i * indices.length +j] >= min_overlap_frac){
				num_pairs++;
				int [] ipair = {indices[i], indices[j]};
				if ((ortho_maps[ipair[0]].getMatch(ortho_maps[ipair[1]].getName()) == null) &&
						(ortho_maps[ipair[1]].getMatch(ortho_maps[ipair[0]].getName()) == null)){
					num_new++;
				}
			}
		}
		
		boolean      skip_exist    = clt_parameters.imp.pwise_skip_exist;   // 
		boolean      refine_exist  = clt_parameters.imp.pwise_refine_exist; // if false, start from scratch, true - start from previous
		boolean      delete_failed = clt_parameters.imp.pwise_delete_fail;  // delete existing match if now failed
		boolean      gen_inverse   = clt_parameters.imp.pwise_gen_inverse;  // generate inverse matches
		boolean      save_each     = clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean      log_append    = clt_parameters.imp.pwise_log_append;   //
		String       log_path      = clt_parameters.imp.pwise_log_path;     // 
		int          debugLevel    = clt_parameters.imp.pwise_debug;        //
		
		//Initial spiral search for image matching
		double  search_step =         clt_parameters.imp.ospir_step;     // 8.0; // pix
		double  search_range =        clt_parameters.imp.ospir_range;    // 50.0; // pix
		double  double_threshold =    clt_parameters.imp.ospir_double;   //
		double  good_rms =            clt_parameters.imp.ospir_good_rms; // 0.27; //
		double  max_rms =             clt_parameters.imp.ospir_max_rms;  // 0.35; //
		int     min_overlap =         clt_parameters.imp.ospir_overlap;  // 3000; // do not try to match if there is too small overlap (scaled pixels)
		int     num_iter_lma =        clt_parameters.imp.ospir_num_iter; // 5;
		double [] max_rms_iter =      clt_parameters.imp.ospir_rms_iter; // {1.0, 0.6};//
		boolean spiral_ignore_rms =   clt_parameters.imp.ospir_ignore_rms; // false
		int     spiral_debug =        clt_parameters.imp.ospir_debug;    // 0;
		
		//Final pairwise scenes matching
		double  frac_remove  =        clt_parameters.imp.pmtch_frac_remove;// 0.15;
		double  metric_err =          clt_parameters.imp.pmtch_metric_err;// 0.05; // 0.02;//  2 cm
		boolean pmtch_use_affine =    clt_parameters.imp.pmtch_use_affine;		
		double  max_std =             clt_parameters.imp.pmtch_max_std;// 1.5;      // maximal standard deviation to limit center area  
		double  min_std_rad =         clt_parameters.imp.pmtch_min_std_rad;// 2.0;  // minimal radius of the central area (if less - fail)
		boolean ignore_prev_rms =     clt_parameters.imp.pmtch_ignore_rms;// true; 
		int     num_tries =           clt_parameters.imp.pmtch_num_iter;// 10;
		
		double rad_fraction =         clt_parameters.imp.pmtch_cent_rad; // center circle radius fraction of 0.5* min(width, height) in tiles
		double max_tile_rad =         clt_parameters.imp.pmtch_max_cent_rad;// maximal center radius in tiles (limit pmtch_cent_rad)
		double fill_fraction =        clt_parameters.imp.pmtch_cent_fill; // should be populated not less than this
		double fill_fraction_final =  clt_parameters.imp.pmtch_cent_final; // should be populated not less than this during final pass
		double ease_nosfm =           clt_parameters.imp.pmtch_ease_nosfm; // ease metric_error when no SfM gain == 0;
		double pull_skew =            clt_parameters.imp.pmtch_pull_skew;        // ~rotation, = 0 fraction of the total weight == 1
		double pull_tilt =            clt_parameters.imp.pmtch_pull_tilt;     // > 0
		double pull_scale =           clt_parameters.imp.pmtch_pull_scale;       // = 0
		
		int min_scene =              0;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Pairwise Match Parameters",1200,1000);
		gd.addMessage("Number of scenes - "+num_scenes+
				", number of pairs (w/o inverse) - "+num_pairs+
				", number of new pairs - "+num_new);
	    gd.addCheckbox    ("Skip existing",               skip_exist, "Do not regenerate if match with same or higher resolution exists.");
	    gd.addCheckbox    ("Refine existing",             refine_exist, "Refine existing matches (false - start from scratch with spiral search).");
		gd.addCheckbox    ("Delete failed",               delete_failed, "Delete previous matches if it failed now.");
	    gd.addCheckbox    ("Generate inverse matches",    gen_inverse, "Generate (refine if exist and enabled) inverse matches.");
	    
	    gd.addCheckbox    ("Save state after each match", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gd.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gd.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gd.addNumericField("Pairwise match debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
		
		gd.addMessage  ("Initial spiral search for image matching");
		gd.addNumericField("Spiral search step",          search_step,  3,7,"scaled pix",	"Distance between spiral search probes, in scaled pixels.");
		gd.addNumericField("Spiral search radius",        search_range, 3,7,"scaled pix",	"Maximal radius of the spiral search, in scaled pixels.");
		gd.addNumericField("Mitigate small overlap",      double_threshold, 3,7,"","For small overlaps increase zoom by 1 and range - twice.");
		gd.addNumericField("RMSE to end search",          good_rms,     3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels.");
		gd.addNumericField("Satisfactory RMSE",           max_rms,      3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels.");
		gd.addNumericField("Minimal overlap",             min_overlap,  0,4,"scaled pix ^ 2","Minimal overlap area in square scaled pixels.");
		gd.addNumericField("LMA iterations",              num_iter_lma, 0,2,"",              "Number of LMA iterations during spiral search.");
		gd.addNumericField("RMSE at first iteration",     max_rms_iter[0],  3,7,"scaled pix","Maximal RMSE at first iteration.");
		gd.addNumericField("RMSE at second iteration",    max_rms_iter[1],  3,7,"scaled pix","Maximal RMSE at second iteration.");
		
		gd.addCheckbox    ("Ignore worsening RMSE",       spiral_ignore_rms, "Ignore worsening/not improving RMSE during spiral search.");
		gd.addNumericField("Spiral search debug level",   spiral_debug, 0,3,"","Debug level during Spiral search.");

		gd.addMessage  ("Final pairwise scenes matching");
		gd.addNumericField("Remove fraction of worst matches", frac_remove,  3,7,"",	"When fitting scenes remove this fraction of worst match tiles.");
		gd.addNumericField("Maximal metric error",             metric_err,  3,7,"m",	"Maximal tolerable fitting error caused by elevation variations.");
		gd.addCheckbox    ("Use scenes' affine",               pmtch_use_affine, "Use known scenes' affine matrices, false - start from scratch (unity) ones.");
		gd.addNumericField("Central area standard deviation",  max_std,  3,7,"",	"Central area limit by the standard deviation.");
		gd.addNumericField("Central area minimal radius",      min_std_rad,  3,7,"tile",	"Minimal radius of the central area after all LMA passes.");
		gd.addCheckbox    ("Ignore previous RMSE",             ignore_prev_rms, "Do not exit full fitting cycles if the RMSE worsened/not improved.");
		gd.addNumericField("Number of fitting iterations",     num_tries, 0,3,"","number of full fittng iterations.");
		gd.addNumericField("Central area radius as fraction",  rad_fraction,   3,7,"",	"Central area radius as fraction of half minimal WOI dimension.");
		gd.addNumericField("Maximal central area radius",      max_tile_rad,   3,7,"tiles",	"Absolute limit to the center area radius (eases bad peripheral matching).");
		gd.addNumericField("Central area minimal fill",        fill_fraction,  3,7,"",	"Central area minimal fill for all but the last iteration.");
		gd.addNumericField("Central area minimal fill final",  fill_fraction_final, 3,7,"",	"Central area minimal fill for the last iteration.");
		gd.addNumericField("Relax metric error for no-SfM",    ease_nosfm, 3,7,"",	"Relax metric error for no-SfM scenes (sfm_gain==0).");
		gd.addNumericField("Pull skew (rotation)",             pull_skew, 3,7,"",	"Prevent pairwise match from rotation.");
		gd.addNumericField("Pull tilt",                        pull_tilt, 3,7,"",	"Prevent pairwise match from tilt.");
		gd.addNumericField("Pull scale",                       pull_scale, 3,7,"",	"Prevent pairwise match from scaling.");
		gd.addNumericField("Start scene (skip all earlier)",   min_scene, 0,3,"","To be able to continue skipping some.");
		//
		
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		skip_exist       = gd.getNextBoolean();
		refine_exist     = gd.getNextBoolean();
		delete_failed    = gd.getNextBoolean();
		gen_inverse      = gd.getNextBoolean();
		save_each        = gd.getNextBoolean();
		log_append       = gd.getNextBoolean();
		log_path         = gd.getNextString();
		debugLevel = (int) gd.getNextNumber(); 	
		
		search_step      =       gd.getNextNumber();        
		search_range     =       gd.getNextNumber();
		double_threshold =       gd.getNextNumber();
		good_rms         =       gd.getNextNumber();
		max_rms          =       gd.getNextNumber();
		min_overlap      = (int) gd.getNextNumber();
		num_iter_lma     = (int) gd.getNextNumber();
		max_rms_iter[0]  =       gd.getNextNumber();
		max_rms_iter[1]  =       gd.getNextNumber();
		spiral_ignore_rms=       gd.getNextBoolean();
		spiral_debug     = (int) gd.getNextNumber();
		                         
		frac_remove      =       gd.getNextNumber();
		metric_err       =       gd.getNextNumber();
		pmtch_use_affine=        gd.getNextBoolean();
		max_std          =       gd.getNextNumber();
		min_std_rad      =       gd.getNextNumber();
		ignore_prev_rms  =       gd.getNextBoolean();
		num_tries        = (int) gd.getNextNumber();		

		rad_fraction =           gd.getNextNumber();
		max_tile_rad =           gd.getNextNumber();		
		fill_fraction =          gd.getNextNumber();  
		fill_fraction_final=     gd.getNextNumber();   
		ease_nosfm =             gd.getNextNumber();
		pull_skew =              gd.getNextNumber();
		pull_tilt =              gd.getNextNumber();
		pull_scale =             gd.getNextNumber();
		min_scene =        (int) gd.getNextNumber();
		
		return generatePairwiseMatches(
				clt_parameters,           // CLTParameters clt_parameters,
				min_overlap_frac,          // double  min_overlap_frac,
				indices,                  // int []       indices,
				overlaps,                 // double [] overlaps,
				skip_exist,               // boolean      skip_existing,
				refine_exist,             // boolean      refine_existing, // if false, start from scratch, true - start from previous
				delete_failed,            // boolean       delete_failed,   // if false, start from scratch, true - start from previous
				gen_inverse,              // boolean      gen_inverse,    // generate inverse matches
				save_each,                // boolean      save_each,       // save state file after each match
				log_append,               // boolean      log_append,      //
				log_path,                 // String       log_path,
				orthoMapsCollection_path, // String orthoMapsCollection_path
				search_step,              // double        search_step,
				search_range,             // double        search_range,
				good_rms,                 // double        good_rms,
				max_rms,                  // double        max_rms,
				min_overlap,              // int           min_overlap,
				num_iter_lma,             // int           num_iter_lma,
				spiral_ignore_rms,        // boolean       ignore_rms,
				spiral_debug,             // int           spiral_debug,
				frac_remove,              // double        frac_remove,
				metric_err,               // double        metric_error,
				pmtch_use_affine,         // boolean       pmtch_use_affine,				
				max_std,                  // double        max_std,  
				min_std_rad,              // double        min_std_rad,
				ignore_prev_rms,          // boolean       ignore_prev_rms, 
				num_tries,                // int           num_tries,
				rad_fraction,             // double        rad_fraction,
				max_tile_rad,             // double        max_tile_rad,
				fill_fraction,            // double        fill_fraction,
				fill_fraction_final,      // double        fill_fraction_final,
				ease_nosfm,               // double        ease_nosfm,
				double_threshold,         // double        double_threshold,
				max_rms_iter,             // double []     max_rms_iter,
				min_scene,                // int           min_scene,
				pull_skew,                // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
				pull_tilt,                // double           pull_tilt,        // > 0
				pull_scale,               // double           pull_scale,       // = 0
				debugLevel);              // int          debugLevel
	}
	
	public boolean generatePairwiseMatches(
			CLTParameters clt_parameters,
			double        min_overlap_frac,
			int []        indices,
			double []     overlaps,
			boolean       skip_existing,
			boolean       refine_existing, // if false, start from scratch, true - start from previous
			boolean       delete_failed,   // if false, start from scratch, true - start from previous
			boolean       gen_inverse,     // generate inverse matches
			boolean       save_each,       // save state file after each match
			boolean       log_append,      //
			String        log_path,
			String        orthoMapsCollection_path,
			
			double        search_step,
			double        search_range_in,
			double        good_rms,
			double        max_rms,
			int           min_overlap,
			int           num_iter_lma,
			boolean       ignore_rms,
			int           spiral_debug,
			
			double        frac_remove,
			double        metric_error,
			boolean       pmtch_use_affine,			
			double        max_std,  
			double        min_std_rad,
			boolean       ignore_prev_rms, 
			int           num_tries,
			double        rad_fraction,
			double        max_tile_rad,
			double        fill_fraction,
			double        fill_fraction_final,
			double        ease_nosfm,
			double        double_threshold,
			double []     max_rms_iter,
			int           min_scene,
			double        pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
			double        pull_tilt,        // > 0
			double        pull_scale,       // = 0
			int           debugLevel) {
		
		boolean batch_mode = true;
		int num_scenes = indices.length;
		int num_pairs = 0;
		int num_new = 0;
		
		ArrayList<Point> pairs = new ArrayList<Point>();
		for (int i = 0; i < indices.length-1; i++) {
			for (int j = i+1; j < indices.length; j++) if (overlaps[i * indices.length +j] >= min_overlap_frac){
				pairs.add(new Point(i,j));
				if (gen_inverse) {
					pairs.add(new Point(j,i));
				}
				num_pairs++;
				int [] ipair = {indices[i], indices[j]};
				if ((ortho_maps[ipair[0]].getMatch(ortho_maps[ipair[1]].getName()) == null) &&
						(ortho_maps[ipair[1]].getMatch(ortho_maps[ipair[0]].getName()) == null)){
					num_new++;
				}
			}
		}
		
		if (log_append && (log_path != null)) { // assuming directory exists
			StringBuffer sb = new StringBuffer();
			sb.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
			sb.append("num_scenes\t"+  num_scenes+"\n");
			sb.append("pairs.size()\t"+pairs.size()+"\n");
			sb.append("num_pairs\t"+   num_pairs+"\n");
			sb.append("num_new\t"+     num_new+"\n");
			sb.append(String.format("%4s\t%4s\t%17s\t%17s\t%6s\t%3s\t%4s,\t%4s\t%6s\t%6s\t%7s\n",
					"scn1","scn2","timestamp1","timestamp2","ovrlp","zl","nx","ny","RMS-sp","RMSfin","fzl","removed"));
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
			if (debugLevel>-3) {
				System.out.print(sb.toString());
			}
		}
		ArrayList<Point> failed_pairs = new ArrayList<Point>();
		ArrayList<Point> new_pairs =    new ArrayList<Point>(); // started from spiral, not from the same or inverse
		for (Point pair:pairs) {
			int [] ipair = {indices[pair.x], indices[pair.y]};
			if (ipair[0] < min_scene) {
				System.out.println ("Skipping "+ipair[0]+":"+ipair[1]+" until "+min_scene);
				continue;
			}
			boolean direct = indices[pair.x] < indices[pair.y];
	    	int min_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	int max_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	double max_agl =   ortho_maps[ipair[0]].getAGL();
	    	for (int i = 0; i < ipair.length; i++) {
	    		max_agl =      Math.max(max_agl, ortho_maps[ipair[i]].getAGL());	    		
	    		min_zoom_lev = Math.min(min_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    		max_zoom_lev = Math.max(max_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    	}
        	double agl_ratio = max_agl/50.0;
        	double metric_error_adj = metric_error * agl_ratio * agl_ratio; // metric_error settings is good for 50m. Increase for higher Maybe squared?
	    	int initial_zoom = max_zoom_lev - 4;  // another algorithm?
	    	// overlaps
	    	int overlap_indx = direct ? (pair.x * indices.length + pair.y) : (pair.y * indices.length + pair.x);
	    	double overlap_frac = overlaps[overlap_indx];
	    	double search_range = search_range_in;
	    	while (overlap_frac < double_threshold) {
	    		overlap_frac *= 2;
	    		initial_zoom += 1;
	    		search_range *= 2;
	    	}
	    	
			PairwiseOrthoMatch direct_match = ortho_maps[ipair[0]].getMatch(ortho_maps[ipair[1]].getName());
			PairwiseOrthoMatch inv_match =  ortho_maps[ipair[1]].getMatch(ortho_maps[ipair[0]].getName());
			boolean use_direct =  false;
			boolean use_inverse = false;
			boolean skip =        false;
			if (!direct && (inv_match != null)) { // prefer inverse
				use_inverse = true;
			} else if (direct_match != null) {
				if (skip_existing && (direct_match.overlap > 0)) { // do not skip old pairs - refine them
					skip = true;
				} else if (refine_existing) {
					use_direct = true;
				} // else will start from scratch
			} else if (inv_match != null) {
				if (direct && refine_existing) {
					use_inverse = true;
				}
			}
			if (skip) {
				if (log_append && (log_path != null)) { // assuming directory exists
					StringBuffer sb = new StringBuffer();
					sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tSKIP\t\t\t%6.4f\t%3d\n",
							ipair[0], ipair[1],
							ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
							overlaps[overlap_indx],
							initial_zoom,
							direct_match.rms,
							direct_match.zoom_lev));
					CalibrationFileManagement.saveStringToFile (
							log_path,          //String path,
							sb.toString(), // data,
							true); // boolean append)
				}
				continue;
			}
			PairwiseOrthoMatch pairwiseOrthoMatch = null;
			
			// unityAffine()
			
			double [][] affine0 = pmtch_use_affine?ortho_maps[ipair[0]].getAffine():unityAffine(); // {{1,0,0},{0,1,0}}; // will always stay the same
			double [][] affine1 = pmtch_use_affine?ortho_maps[ipair[0]].getAffine():unityAffine(); // {{1,0,0},{0,1,0}}; // here (manual mode) start from the center, may use prediction in auto
			double [][][] affines = new double[][][] {affine0,affine1};
			double spiral_rms = Double.NaN;
			if (use_direct) {
				pairwiseOrthoMatch=direct_match;
			} else if (use_inverse) {
				double [] enuOffset = ortho_maps[ipair[0]].enuOffsetTo(ortho_maps[ipair[1]]);
				double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
				// create inverted pairwiseOrthoMatch
				pairwiseOrthoMatch = inv_match.getInverse(rd);
			} else { // run spiral search
				// now always unity, but after partial adjustment may be non-unity
				// TODO: Not yet tested with intermediate adjustment.
				pairwiseOrthoMatch = SpiralMatch (
						clt_parameters,  // CLTParameters    clt_parameters,
						frac_remove,     // double           frac_remove, //  =        0.25
						metric_error_adj,// double           metric_error,
						pmtch_use_affine, // boolean          pmtch_use_affine,
						max_std,         // double           max_std,     // maximal standard deviation to limit center area  
						min_std_rad,     // double           min_std_rad, // minimal radius of the central area (if less - fail)
						rad_fraction,    // double           rad_fraction,
						max_tile_rad,    // double           max_tile_rad, //  = 30;						
						fill_fraction,   // double           fill_fraction,
						fill_fraction_final, // double           fill_fraction_final,
						ease_nosfm,      // double           ease_nosfm,
						ipair,           // int []           gpu_pair,
						affines,         // double [][][]    affines_init,  // here in meters, relative to vertical points
						initial_zoom,    // int              zoom_lev,
						search_step,     // double           pix_step,
						search_range,    // double           pix_range,
						good_rms,        // double           good_rms,
						max_rms,         // 			double           max_rms,
						num_iter_lma,    // int              num_tries, //  = 5
						min_overlap,     // int              min_overlap, // 3000
						ignore_rms,      // boolean          ignore_rms,
						max_rms_iter,    // double []        max_rms_iter, //  = {1.0, 0.6};//
						overlaps[overlap_indx], // double           overlap,
						pull_skew,        // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
						pull_tilt,        // double           pull_tilt,        // > 0
						pull_scale,       // double           pull_scale,       // = 0
						spiral_debug);   // int              debugLevel)
				// late failure
				if ((pairwiseOrthoMatch != null) && !Double.isNaN(pairwiseOrthoMatch.rms) && !pairwiseOrthoMatch.ok) {
//					String str_failed = delete_failed?
//							"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tFAILED\tREMOVED\n":
//							"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tFAILED\n";
					String str_failed = delete_failed?
							"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%3d\t%3d\t%6.4f\tREMOVED\n":
							"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%3d\t%3d\t%6.4f\n";
					if (log_append && (log_path != null)) { // assuming directory exists
						StringBuffer sb = new StringBuffer();
						sb.append(String.format(str_failed,
								ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
								overlaps[overlap_indx],  initial_zoom,
								pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],pairwiseOrthoMatch.rms));
						CalibrationFileManagement.saveStringToFile (
								log_path,          //String path,
								sb.toString(), // data,
								true); // boolean append)
					}
				}
				if ((pairwiseOrthoMatch == null) || !pairwiseOrthoMatch.ok) {
					if (delete_failed) {
						ortho_maps[ipair[0]].unsetMatch(ortho_maps[ipair[1]].getName());
					}
					if ((pairwiseOrthoMatch != null) && Double.isNaN(pairwiseOrthoMatch.rms)) {
						String str_failed = delete_failed?"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tFAILED\tREMOVED\n":"%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tFAILED\n";
						if (log_append && (log_path != null)) { // assuming directory exists
							StringBuffer sb = new StringBuffer();
							sb.append(String.format(str_failed,
									ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
									overlaps[overlap_indx],  initial_zoom));
							CalibrationFileManagement.saveStringToFile (
									log_path,          //String path,
									sb.toString(), // data,
									true); // boolean append)
						}
					}
					failed_pairs.add(pair);
					pairwiseOrthoMatch = null;
					continue;
				} else {
					spiral_rms = pairwiseOrthoMatch.rms;
					new_pairs.add(pair); // adjusted with spiral
				}
			}
			// refine match with high resolution
			affines[1] = pairwiseOrthoMatch.getAffine();
			affines[1][0] = affines[1][0].clone(); 
			affines[1][1] = affines[1][1].clone();
			Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair()			
			correlateOrthoPair(
					clt_parameters,  // CLTParameters    clt_parameters,
					pairwiseOrthoMatch, //PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
					0,                // 			int              min_overlap,
					max_std,         // double           max_std,      // maximal standard deviation to limit center area  
					min_std_rad,     // double           min_std_rad,  // minimal radius of the central area (if less - fail)
					frac_remove,     // double           frac_remove, //  =        0.25
					metric_error_adj,// 			double           metric_error,
					ignore_prev_rms, // boolean ignore_prev_rms,
					num_tries,       //  = 5int              num_tries, //  = 5
					false, // ,            // boolean          calc_warp, (will return null if false)
					batch_mode,      // boolean          batch_mode,
					ipair,           // String []        gpu_spair,
					affines,         // double [][][]    affines, // on top of GPS offsets
					woi,             // Rectangle        woi,
					min_zoom_lev,    // int              zoom_lev,
					false, // show_vf,         // boolean show_vf,
					null, // ground_planes,   // double [][]      ground_planes, // null or double[2] - will return ground planes
					rad_fraction,    // double           rad_fraction,
					max_tile_rad,    // double           max_tile_rad, //  = 30;
					fill_fraction,   // double           fill_fraction,
					fill_fraction_final, // double           fill_fraction_final,
					ease_nosfm,      // double           ease_nosfm,
					null,            //  double []        max_rms_iter, //  = {1.0, 0.6};//	
					pull_skew,        // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
					pull_tilt,        // double           pull_tilt,        // > 0
					pull_scale,       // double           pull_scale,       // = 0
					debugLevel-4);   // final int        debugLevel)
			boolean failed_refine = Double.isNaN(pairwiseOrthoMatch.rms);
			// Create log line and write it
			StringBuffer sb = new StringBuffer();
			sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d",
					ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
					overlaps[overlap_indx],  initial_zoom));

			if (use_direct || use_inverse) {
				sb.append("\t\t\t"); 
			} else {
				sb.append(String.format("\t%3d\t%3d\t%6.4f",
						pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],spiral_rms)); 
			}
			if (failed_refine) {
				if (delete_failed) {
					ortho_maps[ipair[0]].unsetMatch(ortho_maps[ipair[1]].getName());
				}
				String str_failed = delete_failed?"\tFAILED\t\tREMOVED\n":"\tFAILED\n";

				failed_pairs.add(pair);
				sb.append(String.format(str_failed));
				if (debugLevel > -4) System.out.print("Final adjustment (1)"+str_failed);				
			} else {
				pairwiseOrthoMatch.overlap = overlaps[overlap_indx]; // needed here if refining old/manual w/o overlap 
				sb.append(String.format("\t%6.4f\t%3d\n",pairwiseOrthoMatch.rms,pairwiseOrthoMatch.zoom_lev)); 
				if (debugLevel > -4) System.out.println("Final adjustment (2) RMSE="+pairwiseOrthoMatch.rms+
						", overlap = "+pairwiseOrthoMatch.overlap);				
				ortho_maps[ipair[0]].setMatch(ortho_maps[ipair[1]].getName(),pairwiseOrthoMatch);
				if (save_each && (orthoMapsCollection_path != null)) {
		        	try {
		        		writeOrthoMapsCollection(orthoMapsCollection_path);
		        	} catch (IOException e) {
		        		// TODO Auto-generated catch block
		        		e.printStackTrace();
		        	}
		        	if (debugLevel > -4) {
		        		System.out.println("Saved data to "+ orthoMapsCollection_path);
		        	}
				}
			}
			if (log_append && (log_path != null)) { // assuming directory exists
				CalibrationFileManagement.saveStringToFile (
						log_path,          //String path,
						sb.toString(), // data,
						true); // boolean append)
			}			
			// save if save_each
			// does it have direct or inverted pair
		}
		if (log_append && (log_path != null)) { // assuming directory exists
			StringBuffer sb = new StringBuffer();
			sb.append("\nSUMMARY\n");
			sb.append("good\t"+(pairs.size()-failed_pairs.size())+"\n");
			sb.append("new\t"+ new_pairs.size()+"\n");
			sb.append("failed\t"+failed_pairs.size()+"\n");
			if (!failed_pairs.isEmpty()) {
				for (int i = 0; i < failed_pairs.size(); i++) {
					int [] ipair = {indices[failed_pairs.get(i).x], indices[failed_pairs.get(i).y]};
					sb.append(String.format("%4d\t%4d\t%s\t%s\n",
							ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName()));
				}
			}
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
        	if (debugLevel > -4) {
        		System.out.println("Appended log file "+ log_path);
        	}
		}
		if (orthoMapsCollection_path != null) {
        	try {
        		writeOrthoMapsCollection(orthoMapsCollection_path);
        	} catch (IOException e) {
        		// TODO Auto-generated catch block
        		e.printStackTrace();
        	}
        	if (debugLevel > -4) {
        		System.out.println("Saved data to "+ orthoMapsCollection_path);
        	}
		}
		return true;
	}


	
	/**
	 * Add new scene pairs from the already adjusted scenes
	 * @param clt_parameters
	 * @param orthoMapsCollection_path
	 * @return
	 */
	
	public boolean augmentPairwiseAffines(
			CLTParameters clt_parameters,
			String        orthoMapsCollection_path) {
		// Create list of all pairs (after recreating all overlaps with updated affines)
		ArrayList<Point> pairs_list = new ArrayList<Point>(); 
		for (OrthoMap map : ortho_maps) {
			for (String other_name: map.pairwise_matches.keySet()) {
				pairs_list.add(new Point(
						getIndex(map.getName()),
						getIndex(other_name)));
			}
		}
		// sort pairs_list by x then y
		Collections.sort(pairs_list, new Comparator<Point>() {
			@Override
			public int compare(Point lhs, Point rhs) {
				return (rhs.x > lhs.x) ? -1 : (rhs.x < lhs.x) ? 1 :
					((rhs.y > lhs.y) ? -1 : (rhs.y < lhs.y) ? 1 : 0); // increasing
			}
		});

		// convert ArrayList<Point> to array int[][]
		int [][] available_pairs = new int [pairs_list.size()][2];
		for (int i = 0; i < available_pairs.length; i++) {
			available_pairs[i][0] = pairs_list.get(i).x;
			available_pairs[i][1] = pairs_list.get(i).y;
		}
		
		boolean flt_undef_only =     false; // clt_parameters.imp.flt_undef_only;    //  false;
		double  flt_min_overlap =    clt_parameters.imp.flt_min_overlap;   //  0.0;
		double  flt_max_overlap =    clt_parameters.imp.flt_max_overlap;   //  1.0;
		boolean flt_filt_zoom =      clt_parameters.imp.flt_filt_zoom;     //  true;
		int     flt_min_zoom =       clt_parameters.imp.flt_min_zoom;      // -2;
		int     flt_max_zoom =       clt_parameters.imp.flt_max_zoom;      // 10;
		double  flt_min_sfm =        clt_parameters.imp.flt_min_sfm;       //  0.0;
		double  flt_max_sfm =        clt_parameters.imp.flt_max_sfm;       //1000.0;
        int     flt_alt =            clt_parameters.imp.flt_alt;           // 0;

//		double  flt_min_rms =        0;     // clt_parameters.imp.flt_min_rms;       //  0.0;
//		double  flt_max_rms =        2.0;   // clt_parameters.imp.flt_max_rms;       //  2.0;
        double  flt_min_rms =        clt_parameters.imp.flt_min_rms;       //  0.0;
		double  flt_max_rms =        clt_parameters.imp.flt_max_rms;       //  2.0;
		boolean flt_nan_rms =        true;  // clt_parameters.imp.flt_nan_rms;       //  false;
        
		boolean flt_show_names =     true;  // clt_parameters.imp.flt_show_names;    //  true;
		boolean flt_show_overlaps =  true;  // clt_parameters.imp.flt_show_overlaps; //  true;
		boolean flt_show_rms =       true;  // clt_parameters.imp.flt_show_rms;      //  true;
		boolean flt_show_zoom =      true;  // clt_parameters.imp.flt_show_zoom;     //  true;
		boolean flt_show_alt =       true;  // clt_parameters.imp.flt_show_alt;     //  true;
		//Initial spiral search for image matching		
		boolean ospir_augment =       clt_parameters.imp.ospir_augment;    // true
		double  max_rms =             clt_parameters.imp.ospir_max_rms;    // 0.35; //
		double  max_rms_refine =      clt_parameters.imp.pwise_max_rms;    // 0.35; //
		int     min_overlap_tiles =   clt_parameters.imp.ospir_overlap;    // 3000; // do not try to match if there is too small overlap (scaled pixels)
		double  double_threshold =    clt_parameters.imp.ospir_double;     // increase resolution if too small overlap in tiles
		int     num_iter_lma =        clt_parameters.imp.ospir_num_iter;   // 5;
		double [] max_rms_iter =      clt_parameters.imp.ospir_rms_iter;   // {1.0, 0.6};//
		boolean lores_ignore_rms =    clt_parameters.imp.ospir_ignore_rms; // false
		
		//Final pairwise scenes matching
		double  frac_remove  =        clt_parameters.imp.pmtch_frac_remove;// 0.15;
		double  metric_err =          clt_parameters.imp.pmtch_metric_err;// 0.05; // 0.02;//  2 cm
//		boolean pmtch_use_affine =    clt_parameters.imp.pmtch_use_affine;		
		double  max_std =             clt_parameters.imp.pmtch_max_std;// 1.5;      // maximal standard deviation to limit center area  
		double  min_std_rad =         clt_parameters.imp.pmtch_min_std_rad;// 2.0;  // minimal radius of the central area (if less - fail)
		boolean ignore_prev_rms =     clt_parameters.imp.pmtch_ignore_rms;// true; 
		int     num_tries =           clt_parameters.imp.pmtch_num_iter;// 10;
		double rad_fraction =         clt_parameters.imp.pmtch_cent_rad; // center circle radius fraction of 0.5* min(width, height) in tiles
		double max_tile_rad =         clt_parameters.imp.pmtch_max_cent_rad;// maximal center radius in tiles (limit pmtch_cent_rad)
		double fill_fraction =        clt_parameters.imp.pmtch_cent_fill; // should be populated not less than this
		double fill_fraction_final =  clt_parameters.imp.pmtch_cent_final; // should be populated not less than this during final pass
		double ease_nosfm =           clt_parameters.imp.pmtch_ease_nosfm; // ease metric_error when no SfM gain == 0;
		int    min_scene =            0;
		double pull_skew =            clt_parameters.imp.pmtch_pull_skew;        // ~rotation, = 0 fraction of the total weight == 1
		double pull_tilt =            clt_parameters.imp.pmtch_pull_tilt;     // > 0
		double pull_scale =           clt_parameters.imp.pmtch_pull_scale;       // = 0
		// log/save parameters
		boolean save_each     =       clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean log_append    =       clt_parameters.imp.pwise_log_append;   //
		String  log_path      =       clt_parameters.imp.pwise_log_path;     // 
		int     debugLevel    =       clt_parameters.imp.pwise_debug;        //
		
		boolean flt_update_config =  false;
		
		GenericJTabbedDialog gdf = new GenericJTabbedDialog("Select pairs filter/display",800,1200);
		gdf.addMessage("Filter pairs parameters");
		gdf.addNumericField("Minimal scene overlap (0..1)",flt_min_overlap,  3,7,"", "Minimal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addNumericField("Maximal scene overlap (0..1)",flt_max_overlap,  3,7,"", "Maximal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addCheckbox    ("Filter by zoom level"  ,      flt_filt_zoom, "Filter by the zoom level used for matching.");
		gdf.addNumericField("Minimal zoom",                flt_min_zoom, 0,3,"","Minimal zoom level used for matching.");
		gdf.addNumericField("Maximal zoom",                flt_max_zoom, 0,3,"","Maximal zoom level used for matching.");
		gdf.addNumericField("Minimal SfM gain",            flt_min_sfm,  3,7,"","Minimal SfM gain of the minimum in the scene pair.");
		gdf.addNumericField("Maximal SfM gain",            flt_max_sfm,  3,7,"","Maximal SfM gain of the minimum in the scene pair.");
		gdf.addMessage("Normally, NaN RMS should be selected for new pairs. Other settings to re-adjust previously adjusted ones!!!");
 		gdf.addNumericField("Minimal RMSE",                flt_min_rms,  3,7,"",	 "Minimal LMA RMSE of the scene pair.");
		gdf.addNumericField("Maximal RMSE",                flt_max_rms,  3,7,"",	 "Maximal LMA RMSE of the scene pair.");
		gdf.addCheckbox    ("NaN RMS (failed match)",      flt_nan_rms, "Keep only failed matches with RMSE=NaN.");
		
		gdf. addChoice("Filter by pairwise ALT availability",IntersceneMatchParameters.FLT_ALT_MODES, IntersceneMatchParameters.FLT_ALT_MODES[flt_alt],"Filter by pairwise ALT availability.");

		gdf.addMessage("Low-resolution match parameters");
		gdf.addCheckbox    ("Use low-res in augmentation", ospir_augment, "Use low-res matching during augmenting (false - skip, go to high-res).");
		gdf.addNumericField("Good RMSE, low-res",          max_rms,      3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels, during spiral.");
		gdf.addNumericField("Good RMSE, final",            max_rms_refine,3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels, during refine.");
		gdf.addNumericField("Minimal overlap",             min_overlap_tiles,  0,4,"scaled pix ^ 2","Minimal overlap area in square scaled pixels.");
		gdf.addNumericField("Mitigate small overlap",      double_threshold, 3,7,"","For small overlaps increase zoom by 1 and range - twice.");
		gdf.addNumericField("LMA iterations",              num_iter_lma, 0,2,"",              "Number of LMA iterations during spiral search.");
		gdf.addNumericField("RMSE at first iteration",     max_rms_iter[0],  3,7,"scaled pix","Maximal RMSE at first iteration.");
		gdf.addNumericField("RMSE at second iteration",    max_rms_iter[1],  3,7,"scaled pix","Maximal RMSE at second iteration.");
		gdf.addCheckbox    ("Ignore worsening low-res RMSE",lores_ignore_rms, "Ignore worsening/not improving RMSE low-res matching.");
		gdf.addMessage  ("Final (high-res) pairwise scenes matching");
		gdf.addNumericField("Remove fraction of worst matches", frac_remove,  3,7,"",	"When fitting scenes remove this fraction of worst match tiles.");
		gdf.addNumericField("Maximal metric error",             metric_err,  3,7,"m",	"Maximal tolerable fitting error caused by elevation variations.");
//		gdf.addCheckbox    ("Use scenes' affine",               pmtch_use_affine, "Use known scenes' affine matrices, false - start from scratch (unity) ones.");
		gdf.addNumericField("Central area standard deviation",  max_std,  3,7,"",	"Central area limit by the standard deviation.");
		gdf.addNumericField("Central area minimal radius",      min_std_rad,  3,7,"tile",	"Minimal radius of the central area after all LMA passes.");
		gdf.addCheckbox    ("Ignore previous RMSE",             ignore_prev_rms, "Do not exit full fitting cycles if the RMSE worsened/not improved.");
		gdf.addNumericField("Number of fitting iterations",     num_tries, 0,3,"","number of full fittng iterations.");
		gdf.addNumericField("Central area radius as fraction",  rad_fraction,   3,7,"",	"Central area radius as fraction of half minimal WOI dimension.");
		gdf.addNumericField("Maximal central area radius",      max_tile_rad,   3,7,"tiles",	"Absolute limit to the center area radius (eases bad peripheral matching).");
		gdf.addNumericField("Central area minimal fill",        fill_fraction,  3,7,"",	"Central area minimal fill for all but the last iteration.");
		gdf.addNumericField("Central area minimal fill final",  fill_fraction_final, 3,7,"",	"Central area minimal fill for the last iteration.");
		gdf.addNumericField("Relax metric error for no-SfM",    ease_nosfm, 3,7,"",	"Relax metric error for no-SfM scenes (sfm_gain==0).");
		gdf.addNumericField("Pull skew (rotation)",             pull_skew, 3,7,"",	"Prevent pairwise match from rotation.");
		gdf.addNumericField("Pull tilt",                        pull_tilt, 3,7,"",	"Prevent pairwise match from tilt.");
		gdf.addNumericField("Pull scale",                       pull_scale, 3,7,"",	"Prevent pairwise match from scaling.");

		gdf.addMessage("Log and Save, and Debug parameters");
	    gdf.addCheckbox    ("Save state after each match", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gdf.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gdf.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gdf.addNumericField("Debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
		gdf.addNumericField("Start scene (skip all earlier)",   min_scene, 0,3,"","To be able to continue skipping some.");
		gdf.addCheckbox    ("Update configuration",        flt_update_config, "Update matching configuration parameters to be saved as defaults.");
		
		gdf.showDialog();
		if (gdf.wasCanceled()) return false;
		flt_min_overlap   =       gdf.getNextNumber();
		flt_max_overlap   =       gdf.getNextNumber();
        flt_filt_zoom     =       gdf.getNextBoolean();
        flt_min_zoom      = (int) gdf.getNextNumber(); 
        flt_max_zoom      = (int) gdf.getNextNumber(); 
        flt_min_sfm       =       gdf.getNextNumber();
        flt_max_sfm       =       gdf.getNextNumber();
		flt_min_rms       =       gdf.getNextNumber();
		flt_max_rms       =       gdf.getNextNumber();
		flt_nan_rms       =       gdf.getNextBoolean();
        flt_alt           =       gdf.getNextChoiceIndex();
		ospir_augment     =       gdf.getNextBoolean();
		max_rms           =       gdf.getNextNumber();	
		max_rms_refine    =       gdf.getNextNumber();
		min_overlap_tiles = (int) gdf.getNextNumber();
		double_threshold  =       gdf.getNextNumber();
		num_iter_lma      = (int) gdf.getNextNumber();
		max_rms_iter[0]   =       gdf.getNextNumber();
		max_rms_iter[1]   =       gdf.getNextNumber();
		lores_ignore_rms =        gdf.getNextBoolean();
		frac_remove      =        gdf.getNextNumber();
		metric_err       =        gdf.getNextNumber();
//		pmtch_use_affine=         gdf.getNextBoolean();
		max_std          =        gdf.getNextNumber();
		min_std_rad      =        gdf.getNextNumber();
		ignore_prev_rms  =        gdf.getNextBoolean();
		num_tries        =  (int) gdf.getNextNumber();		
		rad_fraction =            gdf.getNextNumber();
		max_tile_rad =            gdf.getNextNumber();		
		fill_fraction =           gdf.getNextNumber();  
		fill_fraction_final=      gdf.getNextNumber();   
		ease_nosfm =              gdf.getNextNumber();
		pull_skew =               gdf.getNextNumber();
		pull_tilt =               gdf.getNextNumber();
		pull_scale =              gdf.getNextNumber();

		save_each         =       gdf.getNextBoolean();
		log_append        =       gdf.getNextBoolean();
		log_path          =       gdf.getNextString();
		debugLevel =        (int) gdf.getNextNumber();
		min_scene =         (int) gdf.getNextNumber();
		flt_update_config =       gdf.getNextBoolean();
		
		if (flt_update_config) {
			clt_parameters.imp.flt_min_overlap =   flt_min_overlap;
			clt_parameters.imp.flt_max_overlap =   flt_max_overlap;
			clt_parameters.imp.flt_filt_zoom =     flt_filt_zoom;
			clt_parameters.imp.flt_min_zoom =      flt_min_zoom;
			clt_parameters.imp.flt_max_zoom =      flt_max_zoom;
    		clt_parameters.imp.flt_min_sfm =       flt_min_sfm;
    		clt_parameters.imp.flt_max_sfm =       flt_max_sfm;
			clt_parameters.imp.flt_alt =           flt_alt;			
			clt_parameters.imp.ospir_augment =     ospir_augment;       
			clt_parameters.imp.ospir_max_rms =     max_rms;
			clt_parameters.imp.pwise_max_rms =     max_rms_refine;
			clt_parameters.imp.ospir_overlap =     min_overlap_tiles;
			clt_parameters.imp.ospir_double =      double_threshold;
			clt_parameters.imp.ospir_num_iter =    num_iter_lma;
			clt_parameters.imp.ospir_rms_iter =    max_rms_iter;
			clt_parameters.imp.ospir_ignore_rms =  lores_ignore_rms;   
			clt_parameters.imp.pmtch_frac_remove =  frac_remove;
			clt_parameters.imp.pmtch_metric_err =   metric_err;
//			clt_parameters.imp.pmtch_use_affine =   pmtch_use_affine;
			clt_parameters.imp.pmtch_max_std =      max_std;
			clt_parameters.imp.pmtch_min_std_rad =  min_std_rad;
			clt_parameters.imp.pmtch_ignore_rms =   ignore_prev_rms;
			clt_parameters.imp.pmtch_num_iter =     num_tries;
			clt_parameters.imp.pmtch_cent_rad =     rad_fraction;
			clt_parameters.imp.pmtch_max_cent_rad = max_tile_rad;
			clt_parameters.imp.pmtch_cent_fill =    fill_fraction;
			clt_parameters.imp.pmtch_cent_final =   fill_fraction_final;
			clt_parameters.imp.pmtch_ease_nosfm =   ease_nosfm;
			clt_parameters.imp.pmtch_pull_skew =    pull_skew;
			clt_parameters.imp.pmtch_pull_tilt =    pull_tilt;
			clt_parameters.imp.pmtch_pull_scale =   pull_scale;

			clt_parameters.imp.pwise_save_each  = save_each;  
			clt_parameters.imp.pwise_log_append = log_append; 
			clt_parameters.imp.pwise_log_path   = log_path;   
			clt_parameters.imp.pwise_debug      = debugLevel; 
		}

		available_pairs = filterPairs(
				available_pairs, // int [][] plist_in,
				flt_undef_only,  // boolean  undef_only,
				flt_min_overlap, // double   min_overlap,
				flt_max_overlap, // double   max_overlap,
				flt_min_rms,     // double   min_rms,
				flt_max_rms,     // double   max_rms,
				flt_nan_rms,     // boolean  nan_rms)
				flt_filt_zoom,   // boolean          filt_zoom,
				flt_min_zoom,    // int              min_zoom,
				flt_max_zoom,    // int              max_zoom)
				flt_min_sfm,     // double           min_sfm,
				flt_max_sfm,     // double           max_sfm,
				flt_alt);       // int               flt_alt)
		String [] choices_all = textPairs (
				available_pairs,   // int [][] plist,
				flt_show_names,    // boolean          show_names,
				flt_show_overlaps, // boolean          show_overlap,
				flt_show_rms,      // boolean          show_rms,
				flt_show_zoom,     // boolean          show_zoom,
				flt_show_alt,      // boolean          show_alt,
				true,              // boolean          use_tab,
				null);             // String           extra_line)
		if (debugLevel > 0) {
			System.out.println("Selected "+available_pairs.length+" scene pairs for matching");
			for (int i = 0; i < available_pairs.length; i++) {
				System.out.println(String.format("%4d:%s",i,choices_all[i]));
			}
		}
		if (available_pairs.length == 0) {
			return false;
		}
		
		return augmentPairwiseAffines(
				clt_parameters,	     // CLTParameters clt_parameters,
				available_pairs,     // int [][]      available_pairs, 
				//Initial spiral search for image matching		
				ospir_augment,	     // boolean       ospir_augment,
				max_rms,	         // double        max_rms,
				max_rms_refine,	     // double        max_rms_refine,
				min_overlap_tiles,   // int           min_overlap_tiles,
				double_threshold,    // double        double_threshold, 
				num_iter_lma,	     // int           num_iter_lma,
				max_rms_iter,	     // double []     max_rms_iter,
				lores_ignore_rms,    // boolean       lores_ignore_rms
				//Final pairwise scenes matching
				frac_remove,	     // double        frac_remove,
				metric_err,	         // double        metric_error,
//				pmtch_use_affine,    // boolean       pmtch_use_affine,
				max_std,	         // double        max_std,
				min_std_rad,	     // double        min_std_rad,
				ignore_prev_rms,     // boolean       ignore_prev_rms,
				num_tries,	         // int           num_tries,
				rad_fraction,	     // double        rad_fraction,
				max_tile_rad,	     // double        max_tile_rad,
				fill_fraction,	     // double        fill_fraction,
				fill_fraction_final, //	double        fill_fraction_final,
				ease_nosfm,	         // double        ease_nosfm,
				min_scene,           // int           min_scene,
				pull_skew,	         // double        pull_skew,
				pull_tilt,	         // double        pull_tilt,
				pull_scale,	         // double        pull_scale,
				// log/save parameters
				save_each,	         // boolean        save_each,
				log_append,	         // boolean        log_append,
				log_path,	         // String         log_path,
				orthoMapsCollection_path, // String orthoMapsCollection_path
				debugLevel);         // int           debugLevel)
	}
	
	
	public boolean generatePairwiseAffines(
			CLTParameters clt_parameters,
			String        orthoMapsCollection_path) {
		int [] indices = getScenesSelection(
				clt_parameters, // CLTParameters       clt_parameters,				
				" to find intersects"); // String purpose)
		if ((indices == null) || (indices.length < 2)) {
			System.out.println ("generatePairwiseAffines(): indices[] is null or too short, exiting");
			return false;
		}
		
		int [] groups = new int [indices.length];
		for (int i = 0; i < indices.length; i++) {
			groups[i] = i;
		}
		int num_defined = 0;
		int num_undefined = 0;
		boolean single_pair = indices.length==2;
		for (int i = 0; i < indices.length-1; i++) {
			for (int j = i+1; j < indices.length; j++){
				String name2 = ortho_maps[indices[j]].getName();
				PairwiseOrthoMatch match = ortho_maps[indices[i]].getMatch(name2,true);
				if (match != null) {
					if (match.isDefined() && !single_pair) {
						num_defined++;
					} else {
						num_undefined++;
					}
				}
			}
		}
		HashSet<Integer> hs = new HashSet<Integer>();
		for (int i:groups) {
			hs.add(i);
		}
		
		boolean      dry_run       = false; 
		boolean      save_each     = clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean      log_append    = clt_parameters.imp.pwise_log_append;   //
		String       log_path      = clt_parameters.imp.pwise_log_path;     // 
		int          debugLevel    = clt_parameters.imp.pwise_debug;        //
		double    min_overlap_frac = clt_parameters.imp.pwise_overlap; // 0.25;
		
		//Initial spiral search for image matching
		
		double  search_step =         clt_parameters.imp.ospir_step;     // 8.0; // pix
		double  search_range =        clt_parameters.imp.ospir_range;    // 50.0; // pix
		double  double_threshold =    clt_parameters.imp.ospir_double;   //
		double  good_rms =            clt_parameters.imp.ospir_good_rms; // 0.27; //
		double  max_rms =             clt_parameters.imp.ospir_max_rms;  // 0.35; //
		double  max_rms_refine =      clt_parameters.imp.pwise_max_rms;  // 0.35; //
		int     min_overlap =         clt_parameters.imp.ospir_overlap;  // 3000; // do not try to match if there is too small overlap (scaled pixels)
		int     num_iter_lma =        clt_parameters.imp.ospir_num_iter; // 5;
		double [] max_rms_iter =      clt_parameters.imp.ospir_rms_iter; // {1.0, 0.6};//
		boolean spiral_ignore_rms =   clt_parameters.imp.ospir_ignore_rms; // false
		int     spiral_debug =        clt_parameters.imp.ospir_debug;    // 0;
		
		//Final pairwise scenes matching
		double  frac_remove  =        clt_parameters.imp.pmtch_frac_remove;// 0.15;
		double  metric_err =          clt_parameters.imp.pmtch_metric_err;// 0.05; // 0.02;//  2 cm
		boolean pmtch_use_affine =    clt_parameters.imp.pmtch_use_affine;		
		double  max_std =             clt_parameters.imp.pmtch_max_std;// 1.5;      // maximal standard deviation to limit center area  
		double  min_std_rad =         clt_parameters.imp.pmtch_min_std_rad;// 2.0;  // minimal radius of the central area (if less - fail)
		boolean ignore_prev_rms =     clt_parameters.imp.pmtch_ignore_rms;// true; 
		int     num_tries =           clt_parameters.imp.pmtch_num_iter;// 10;
		
		double rad_fraction =         clt_parameters.imp.pmtch_cent_rad; // center circle radius fraction of 0.5* min(width, height) in tiles
		double max_tile_rad =         clt_parameters.imp.pmtch_max_cent_rad;// maximal center radius in tiles (limit pmtch_cent_rad)
		double fill_fraction =        clt_parameters.imp.pmtch_cent_fill; // should be populated not less than this
		double fill_fraction_final =  clt_parameters.imp.pmtch_cent_final; // should be populated not less than this during final pass
		double ease_nosfm =           clt_parameters.imp.pmtch_ease_nosfm; // ease metric_error when no SfM gain == 0;

		double pull_skew =            clt_parameters.imp.pmtch_pull_skew;        // ~rotation, = 0 fraction of the total weight == 1
		double pull_tilt =            clt_parameters.imp.pmtch_pull_tilt;     // > 0
		double pull_scale =           clt_parameters.imp.pmtch_pull_scale;       // = 0
		
		boolean use_multi = true;
		int                          heur = 15;
		int min_scene =              0;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Pairwise Match Parameters",1200,1100);
		gd.addMessage("Number of scenes - "+indices.length+
				", number of defined pairs - "+num_defined+
				", number of undefined pairs - "+num_undefined);
//	            +", number of disconnected groups - "+hs.size());
		//
	    gd.addCheckbox    ("Dry run",                     dry_run,   "Create pairs, do not adjust.");
///	    gd.addCheckbox    ("Skip existing",               skip_exist, "Do not regenerate if match with same or higher resolution exists.");
///	    gd.addCheckbox    ("Refine existing",             refine_exist, "Refine existing matches (false - start from scratch with spiral search).");
///		gd.addCheckbox    ("Delete failed",               delete_failed, "Delete previous matches if it failed now.");
///	    gd.addCheckbox    ("Generate inverse matches",    gen_inverse, "Generate (refine if exist and enabled) inverse matches.");
	    
	    gd.addCheckbox    ("Save state after each match", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gd.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gd.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gd.addNumericField("Pairwise match debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
		gd.addNumericField("Minimal overlap fraction",    min_overlap_frac,  3,7,"",	    "Minimal overlap fraction.");
		
		gd.addMessage  ("Initial spiral search for image matching");
		gd.addNumericField("Spiral search step",          search_step,  3,7,"scaled pix",	"Distance between spiral search probes, in scaled pixels.");
		gd.addNumericField("Spiral search radius",        search_range, 3,7,"scaled pix",	"Maximal radius of the spiral search, in scaled pixels.");
		gd.addNumericField("Mitigate small overlap",      double_threshold, 3,7,"","For small overlaps increase zoom by 1 and range - twice.");
		gd.addNumericField("RMSE to end search",          good_rms,     3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels.");
		gd.addNumericField("Satisfactory RMSE, spiral",   max_rms,      3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels, during spiral.");
		gd.addNumericField("Satisfactory RMSE, final",    max_rms_refine,3,7,"scaled pix",	"Maximal RMSE to consider match, in scaled pixels, during refine.");
		gd.addNumericField("Minimal overlap",             min_overlap,  0,4,"scaled pix ^ 2","Minimal overlap area in square scaled pixels.");
		gd.addNumericField("LMA iterations",              num_iter_lma, 0,2,"",              "Number of LMA iterations during spiral search.");
		gd.addNumericField("RMSE at first iteration",     max_rms_iter[0],  3,7,"scaled pix","Maximal RMSE at first iteration.");
		gd.addNumericField("RMSE at second iteration",    max_rms_iter[1],  3,7,"scaled pix","Maximal RMSE at second iteration.");
		
		gd.addCheckbox    ("Ignore worsening RMSE",       spiral_ignore_rms, "Ignore worsening/not improving RMSE during spiral search.");
		gd.addNumericField("Spiral search debug level",   spiral_debug, 0,3,"","Debug level during Spiral search.");

		gd.addMessage  ("Final pairwise scenes matching");
		gd.addNumericField("Remove fraction of worst matches", frac_remove,  3,7,"",	"When fitting scenes remove this fraction of worst match tiles.");
		gd.addNumericField("Maximal metric error",             metric_err,  3,7,"m",	"Maximal tolerable fitting error caused by elevation variations.");
		gd.addCheckbox    ("Use scenes' affine",               pmtch_use_affine, "Use known scenes' affine matrices, false - start from scratch (unity) ones.");
		gd.addNumericField("Central area standard deviation",  max_std,  3,7,"",	"Central area limit by the standard deviation.");
		gd.addNumericField("Central area minimal radius",      min_std_rad,  3,7,"tile",	"Minimal radius of the central area after all LMA passes.");
		gd.addCheckbox    ("Ignore previous RMSE",             ignore_prev_rms, "Do not exit full fitting cycles if the RMSE worsened/not improved.");
		gd.addNumericField("Number of fitting iterations",     num_tries, 0,3,"","number of full fittng iterations.");
		gd.addNumericField("Central area radius as fraction",  rad_fraction,   3,7,"",	"Central area radius as fraction of half minimal WOI dimension.");
		gd.addNumericField("Maximal central area radius",      max_tile_rad,   3,7,"tiles",	"Absolute limit to the center area radius (eases bad peripheral matching).");
		gd.addNumericField("Central area minimal fill",        fill_fraction,  3,7,"",	"Central area minimal fill for all but the last iteration.");
		gd.addNumericField("Central area minimal fill final",  fill_fraction_final, 3,7,"",	"Central area minimal fill for the last iteration.");
		gd.addNumericField("Relax metric error for no-SfM",    ease_nosfm, 3,7,"",	"Relax metric error for no-SfM scenes (sfm_gain==0).");
		
		gd.addNumericField("Pull skew (rotation)",             pull_skew, 3,7,"",	"Prevent pairwise match from rotation.");
		gd.addNumericField("Pull tilt",                        pull_tilt, 3,7,"",	"Prevent pairwise match from tilt.");
		gd.addNumericField("Pull scale",                       pull_scale, 3,7,"",	"Prevent pairwise match from scaling.");
		
		gd.addNumericField("Start scene (skip all earlier)",   min_scene, 0,3,"","To be able to continue skipping some.");
		gd.addNumericField("Heuristics bitmap",                heur, 0,3,"","Bitmap of modes to suggest the next pair.");
		gd.addCheckbox    ("Use multiple threads",             use_multi, "Use multiple threads (may be disabled in debug mode).");
		//
		
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		dry_run          = gd.getNextBoolean();
///		skip_exist       = gd.getNextBoolean();
///		refine_exist     = gd.getNextBoolean();
///		delete_failed    = gd.getNextBoolean();
///		gen_inverse      = gd.getNextBoolean();
		save_each        = gd.getNextBoolean();
		log_append       = gd.getNextBoolean();
		log_path         = gd.getNextString();
		debugLevel = (int) gd.getNextNumber(); 	
		min_overlap_frac =       gd.getNextNumber(); 
		search_step      =       gd.getNextNumber();        
		search_range     =       gd.getNextNumber();
		double_threshold =       gd.getNextNumber();
		good_rms         =       gd.getNextNumber();
		max_rms          =       gd.getNextNumber();
		max_rms_refine   =       gd.getNextNumber();
		min_overlap      = (int) gd.getNextNumber();
		num_iter_lma     = (int) gd.getNextNumber();
		max_rms_iter[0]  =       gd.getNextNumber();
		max_rms_iter[1]  =       gd.getNextNumber();
		spiral_ignore_rms=       gd.getNextBoolean();
		spiral_debug     = (int) gd.getNextNumber();
		                         
		frac_remove      =       gd.getNextNumber();
		metric_err       =       gd.getNextNumber();
		pmtch_use_affine=        gd.getNextBoolean();
		max_std          =       gd.getNextNumber();
		min_std_rad      =       gd.getNextNumber();
		ignore_prev_rms  =       gd.getNextBoolean();
		num_tries        = (int) gd.getNextNumber();		

		rad_fraction =           gd.getNextNumber();
		max_tile_rad =           gd.getNextNumber();		
		fill_fraction =          gd.getNextNumber();  
		fill_fraction_final=     gd.getNextNumber();   
		ease_nosfm =             gd.getNextNumber();
		
		pull_skew =              gd.getNextNumber();
		pull_tilt =              gd.getNextNumber();
		pull_scale =             gd.getNextNumber();
		
		min_scene =        (int) gd.getNextNumber();
		heur =             (int) gd.getNextNumber();
		use_multi =              gd.getNextBoolean();
		double max_rmse_reuse= max_rms_refine;
		int [][] pairs_defined = single_pair?(new int[0][]):filterPairs(
				indices,          // int []  indices_in,
				min_overlap_frac, // double  min_overlap_frac,
				max_rmse_reuse,   // double  max_rmse,
				true,             // boolean max_resolution,
				null);            // int [][] remove_pairs
		int [][] pairs_undefined = filterPairs(
				indices,          // int []  indices_in,
				min_overlap_frac, // double  min_overlap_frac,
				0,                // max_rmse_reuse,   // double  max_rmse,
				false,            // boolean max_resolution,
				pairs_defined);   // int [][] remove_pairs
		/*
		PairsGraph  pairsGraph = new PairsGraph(
				this,             // OrthoMapsCollection orthoMapsCollection,
				indices,          // int []  indices,
				min_overlap_frac, // double  min_overlap_frac,
				max_rmse_reuse,   // double  max_rmse_reuse,
				use_multi,        // boolean multi,
				debugLevel);      // int     debugLevel);
		*/
		PairsGraph  pairsGraph = new PairsGraph(
				this,             // OrthoMapsCollection orthoMapsCollection,
				pairs_undefined,  // int [][] undefined_pairs,
				pairs_defined,    // int [][] defined_pairs,
				use_multi,        // boolean multi,
				debugLevel);      // int     debugLevel);
		
		if (dry_run) {
			return pairsGraph.dryRun(
					heur,        // int heur,
					debugLevel); // int debugLevel)
			
		} else {
			if (log_append && (log_path != null)) { // assuming directory exists
				StringBuffer sb = new StringBuffer();
				sb.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
				sb.append("num_scenes\t"+     indices.length+"\n");
				sb.append("num_defined\t"+    num_defined+"\n");
				sb.append("num_undefined\t"+  num_undefined+"\n");
				sb.append("num_groups\t"+     pairsGraph.getNumberOfGroups()+"\n");
				sb.append(String.format("%4s\t%4s\t%17s\t%17s\t%6s\t%3s\t%4s,\t%4s\t%6s\t%6s\t%7s\n",
						"scn1","scn2","timestamp1","timestamp2","ovrlp","zl","nx","ny","RMS-sp","RMSfin","fzl","removed"));
				CalibrationFileManagement.saveStringToFile (
						log_path,          //String path,
						sb.toString(), // data,
						true); // boolean append)
				if (debugLevel>-3) {
					System.out.print(sb.toString());
				}
			}
			
		return generatePairwiseAffines(
				clt_parameters,           // CLTParameters clt_parameters,
				pairsGraph,               // PairsGraph    pairsGraph,
				heur,                     // int           heur,
				min_overlap_frac,         // double  min_overlap_frac,
//				indices,                  // int []       indices,
//				overlaps,                 // double [] overlaps,
//				skip_exist,               // boolean      skip_existing,
//				refine_exist,             // boolean      refine_existing, // if false, start from scratch, true - start from previous
//				delete_failed,            // boolean       delete_failed,   // if false, start from scratch, true - start from previous
//				gen_inverse,              // boolean      gen_inverse,    // generate inverse matches
				save_each,                // boolean      save_each,       // save state file after each match
				log_append,               // boolean      log_append,      //
				log_path,                 // String       log_path,
				orthoMapsCollection_path, // String orthoMapsCollection_path
				search_step,              // double        search_step,
				search_range,             // double        search_range,
				good_rms,                 // double        good_rms,
				max_rms,                  // double        max_rms,
				max_rms_refine,           // double        max_rms_refine,
				min_overlap,              // int           min_overlap,
				num_iter_lma,             // int           num_iter_lma,
				spiral_ignore_rms,        // boolean       ignore_rms,
				spiral_debug,             // int           spiral_debug,
				frac_remove,              // double        frac_remove,
				metric_err,               // double        metric_error,
				pmtch_use_affine,         // boolean       pmtch_use_affine,				
				max_std,                  // double        max_std,  
				min_std_rad,              // double        min_std_rad,
				ignore_prev_rms,          // boolean       ignore_prev_rms, 
				num_tries,                // int           num_tries,
				rad_fraction,             // double        rad_fraction,
				max_tile_rad,             // double        max_tile_rad,
				fill_fraction,            // double        fill_fraction,
				fill_fraction_final,      // double        fill_fraction_final,
				ease_nosfm,               // double        ease_nosfm,
				double_threshold,         // double        double_threshold,
				max_rms_iter,             // double []     max_rms_iter,
				min_scene,                // int           min_scene,
				pull_skew,        // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
				pull_tilt,        // double           pull_tilt,        // > 0
				pull_scale,       // double           pull_scale,       // = 0
				debugLevel);              // int          debugLevel
		}
	}
	
	public boolean augmentPairwiseAffines(
			CLTParameters clt_parameters,
			int [][]      available_pairs, 
			//Initial spiral search for image matching		
			boolean       ospir_augment,
			double        max_rms,
			double        max_rms_refine,
			int           min_overlap_tiles,
			double        double_threshold,
			int           num_iter_lma,
			double []     max_rms_iter,
			boolean       lores_ignore_rms,
			//Final pairwise scenes matching
			double        frac_remove,
			double        metric_error,
			double        max_std,
			double        min_std_rad,
			boolean       ignore_prev_rms,
			int           num_tries,
			double        rad_fraction,
			double        max_tile_rad,
			double        fill_fraction,
			double        fill_fraction_final,
			double        ease_nosfm,
			int           min_scene,
			double        pull_skew,
			double        pull_tilt,
			double        pull_scale,
			// log/save parameters
			boolean       save_each,
			boolean       log_append,
			String        log_path,
			String        orthoMapsCollection_path,
			int           debugLevel) {
		boolean batch_mode = true;
		boolean  show_vf = false;
		boolean use_degrees = true;
		double [][] ground_planes = null; // null or double[2] - will return ground planes:
		if (log_append && (log_path != null)) { // assuming directory exists
			String svd_title=SingularValueDecomposition.titleString(use_degrees);
			StringBuffer sb = new StringBuffer();
			sb.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
			sb.append("number of scenes pairs\t"+     available_pairs.length+"\n");
			sb.append(String.format("%4s\t%4s\t%17s\t%17s\t%6s\t%3s\t%6s\t%6s\t%7s\t%7s\t%s\n",
					"scn1","scn2","timestamp1","timestamp2","ovrlp","zl","RMS-sp","RMSfin","fzl","removed",svd_title));
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
			if (debugLevel>-3) {
				System.out.print(sb.toString());
			}
		}
		
		int num_pairs = 0; // available_pairs.length
		ArrayList<Point> failed_pairs = new ArrayList<Point>();
		for (int npair = 0; npair < available_pairs.length; npair++) {
			int [] ipair = available_pairs[npair];
			PairwiseOrthoMatch pairwiseOrthoMatch = ortho_maps[ipair[0]].getMatch(ortho_maps[ipair[1]].getName(), true);
			double [][] daffine = null;
    		if (pairwiseOrthoMatch != null) {
    			double [][] aff0 = ortho_maps[ipair[0]].getAffine();            			
    			double [][] aff1 = ortho_maps[ipair[1]].getAffine();
    			double [] enuOffset = ortho_maps[ipair[1]].enuOffsetTo(ortho_maps[ipair[0]]);
    			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
    			PairwiseOrthoMatch aff_match = new PairwiseOrthoMatch (
    					aff0, // double [][] affine0,
    					aff1, // double [][] affine1,
    					rd);  // double [] rd);
    			daffine = aff_match.getAffine();
    			pairwiseOrthoMatch.setAffine(daffine);
    		} else {
    			System.out.println("BUG: Missing pair for ["+ipair[0]+", "+ipair[1]+"] ");
    			continue;
    		}
			if (ipair[0] < min_scene) {
				System.out.println ("Skipping "+ipair[0]+":"+ipair[1]+" until "+min_scene);
				continue;
			}
			
			// only do low-res if ospir_augment
			
//			boolean direct = ipair[0] < ipair[1]; // always?
	    	int min_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	int max_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	double max_agl =   ortho_maps[ipair[0]].getAGL();
	    	for (int i = 0; i < ipair.length; i++) {
	    		max_agl =      Math.max(max_agl, ortho_maps[ipair[i]].getAGL());	    		
	    		min_zoom_lev = Math.min(min_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    		max_zoom_lev = Math.max(max_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    	}
        	double agl_ratio = max_agl/50.0;
        	double metric_error_adj = metric_error * agl_ratio * agl_ratio; // metric_error settings is good for 50m. Increase for higher Maybe squared?
	    	int initial_zoom = max_zoom_lev - 4;  // another algorithm?
	    	// overlaps
	    	double overlap_frac = pairwiseOrthoMatch.getOverlap(); // .pairsGraph.getOverlap(next_pair);
	    	double overlap_frac_mod = overlap_frac;
	    	while (overlap_frac_mod < double_threshold) {
	    		overlap_frac_mod *= 2;
	    		initial_zoom += 1;
	    	}
			// unityAffine()
	    	// use unityAffine() for 0; getaffine for second?
			double [][] affine0 = unityAffine(); // ortho_maps[ipair[0]].getAffine();
			double [][] affine1 = daffine; // ortho_maps[ipair[1]].getAffine();
			double [][][] affines = new double[][][] {affine0,affine1};
			boolean success = true;
			Point pair = new Point(ipair[0],ipair[1]);
			if (ospir_augment) {
				correlateOrthoPair(
						clt_parameters,     // CLTParameters    clt_parameters,
						pairwiseOrthoMatch, // PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
						min_overlap_tiles,  // int              min_overlap,
						max_std,            // double           max_std,      // maximal standard deviation to limit center area  
						min_std_rad,        // double           min_std_rad,  // minimal radius of the central area (if less - fail)
						frac_remove,        // double           frac_remove, //  =        0.25
						metric_error_adj, // metric_error,       // 			double           metric_error,
						lores_ignore_rms,   // boolean ignore_prev_rms,
						num_tries,          //  = 5int              num_tries, //  = 5
						false,              // boolean          calc_warp, (will return null if false)
						batch_mode,         // boolean          batch_mode,
						ipair,              // int []           gpu_pair,
						affines,            // double [][][]    affines, // on top of GPS offsets
						null,               // woi,             // Rectangle        woi,
						initial_zoom,       // int              zoom_lev,
						show_vf,            // boolean show_vf,
						ground_planes,      // double [][]      ground_planes, // null or double[2] - will return ground planes
						rad_fraction,       // double           rad_fraction,
						max_tile_rad,       // double           max_tile_rad, //  = 30;
						fill_fraction,      // double           fill_fraction,
						fill_fraction_final,// double           fill_fraction_final,
						ease_nosfm,         // double           ease_nosfm,
						max_rms_iter,       // double []        max_rms_iter, //  = {1.0, 0.6};//
						pull_skew,          // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
						pull_tilt,          // double           pull_tilt,        // > 0
						pull_scale,         // double           pull_scale,       // = 0
						debugLevel-4);     // final int        debugLevel)
				pairwiseOrthoMatch.setAffine(affines[1]); // modified by correlateOrthoPair ALREADY SET
				if (debugLevel > -4) {
					System.out.println(String.format("Low-res Match(): %3d-%3d RMSE=%8.6f",
							ipair[0], ipair[1], pairwiseOrthoMatch.rms)); // if NaN - provide reason
				}
				if (pairwiseOrthoMatch.rms < max_rms) {
					pairwiseOrthoMatch.ok = true;
				}
				success &= pairwiseOrthoMatch.ok; // && !Double.isNaN(pairwiseOrthoMatch.rms)
				
				if (!success) {
					String str_failed = "%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%6.4f\tFAILED\n";
					if (log_append && (log_path != null)) { // assuming directory exists
						StringBuffer sb = new StringBuffer();
						sb.append(String.format(str_failed,
								ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
								overlap_frac,  initial_zoom,
//								pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],
								pairwiseOrthoMatch.rms));
						CalibrationFileManagement.saveStringToFile (
								log_path,          //String path,
								sb.toString(), // data,
								true); // boolean append)
					}
					failed_pairs.add(pair);
					continue;
				}
			} // if (ospir_augment) {
//			PairwiseOrthoMatch pairwiseOrthoMatch_lores =  pairwiseOrthoMatch.clone();
			double lores_rms = pairwiseOrthoMatch.getRMS();
			// high-res

			affines[1][0] = affines[1][0].clone(); 
			affines[1][1] = affines[1][1].clone();
			Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair()			
			correlateOrthoPair(
					clt_parameters,  // CLTParameters    clt_parameters,
					pairwiseOrthoMatch, //PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
					0,                // 			int              min_overlap,
					max_std,         // double           max_std,      // maximal standard deviation to limit center area  
					min_std_rad,     // double           min_std_rad,  // minimal radius of the central area (if less - fail)
					frac_remove,     // double           frac_remove, //  =        0.25
					metric_error_adj,// 			double           metric_error,
					ignore_prev_rms, // boolean ignore_prev_rms,
					num_tries,       //  = 5int              num_tries, //  = 5
					false, // ,            // boolean          calc_warp, (will return null if false)
					batch_mode,      // boolean          batch_mode,
					ipair,           // String []        gpu_spair,
					affines,         // double [][][]    affines, // on top of GPS offsets
					woi,             // Rectangle        woi,
					min_zoom_lev,    // int              zoom_lev,
					false,           // show_vf,         // boolean show_vf,
					null,            // ground_planes,   // double [][]      ground_planes, // null or double[2] - will return ground planes
					rad_fraction,    // double           rad_fraction,
					max_tile_rad,    // double           max_tile_rad, //  = 30;
					fill_fraction,   // double           fill_fraction,
					fill_fraction_final, // double           fill_fraction_final,
					ease_nosfm,      // double           ease_nosfm,
					null,            //  double []        max_rms_iter, //  = {1.0, 0.6};//
					pull_skew,       // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
					pull_tilt,       // double           pull_tilt,        // > 0
					pull_scale,      // double           pull_scale,       // = 0
					debugLevel-4);   // final int        debugLevel)
			boolean failed_refine = Double.isNaN(pairwiseOrthoMatch.rms) || (pairwiseOrthoMatch.rms > max_rms_refine);
			// Create log line and write it
			StringBuffer sb = new StringBuffer();
			sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d",
					ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
					overlap_frac,  initial_zoom));

			sb.append(String.format("\t%6.4f",
//					pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],
					lores_rms)); 
			if (failed_refine) {
				String str_failed = String.format("\t%6.4f\tFAILED\n",pairwiseOrthoMatch.rms);
				failed_pairs.add(pair);
				sb.append(String.format(str_failed));
				if (debugLevel > -4) System.out.print("Final adjustment (3)"+str_failed);				
			} else {
				pairwiseOrthoMatch.overlap = overlap_frac; // needed here if refining old/manual w/o overlap 
				SingularValueDecomposition svd = SingularValueDecomposition.singularValueDecompose(pairwiseOrthoMatch.getAffine());
				String ssvd = svd.toString(use_degrees, 1);
				sb.append(String.format("\t%6.4f\t%3d\t       \t%s\n",pairwiseOrthoMatch.rms,pairwiseOrthoMatch.zoom_lev, ssvd)); 
				if (debugLevel > -4) System.out.println("Final adjustment (4) RMSE="+pairwiseOrthoMatch.rms+
						", overlap = "+pairwiseOrthoMatch.overlap);				
				ortho_maps[ipair[0]].setMatch(ortho_maps[ipair[1]].getName(),pairwiseOrthoMatch);
				if (save_each && (orthoMapsCollection_path != null)) {
					try {
						writeOrthoMapsCollection(orthoMapsCollection_path);
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					if (debugLevel > -4) {
						System.out.println("Saved data to "+ orthoMapsCollection_path);
					}
				}
			}
			if (log_append && (log_path != null)) { // assuming directory exists
				CalibrationFileManagement.saveStringToFile (
						log_path,          //String path,
						sb.toString(), // data,
						true); // boolean append)
			}
			success &= !failed_refine;
			if (success) {
				num_pairs++;
			}
		} // for (int npair = 0; npair < available_pairs.length; npair++) {
		if (log_append && (log_path != null)) { // assuming directory exists
			StringBuffer sb = new StringBuffer();
			sb.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
			sb.append("\nSUMMARY\n");
			sb.append("new\t"+ num_pairs+"\n");
//			sb.append("new\t"+ new_pairs.size()+"\n");
			sb.append("failed\t"+failed_pairs.size()+"\n");
			if (!failed_pairs.isEmpty()) {
				for (int i = 0; i < failed_pairs.size(); i++) {
//					int [] ipair = {indices[failed_pairs.get(i).x], indices[failed_pairs.get(i).y]};
					int [] ipair = {failed_pairs.get(i).x, failed_pairs.get(i).y};
					sb.append(String.format("%4d\t%4d\t%s\t%s\n",
							ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName()));
				}
			}
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
        	if (debugLevel > -4) {
        		System.out.println("Appended log file "+ log_path);
        	}
		}
		if (orthoMapsCollection_path != null) {
        	try {
        		writeOrthoMapsCollection(orthoMapsCollection_path);
        	} catch (IOException e) {
        		// TODO Auto-generated catch block
        		e.printStackTrace();
        	}
        	if (debugLevel > -4) {
        		System.out.println("Saved data to "+ orthoMapsCollection_path);
        	}
		}
		return true;
	}
	
	
	
	
	public boolean generatePairwiseAffines(
			CLTParameters clt_parameters,
			PairsGraph    pairsGraph,
			int           heur,
			double        min_overlap_frac,
//			int []        indices,
//			double []     overlaps,
//			boolean       skip_existing,
//			boolean       refine_existing, // if false, start from scratch, true - start from previous
//			boolean       delete_failed,   // if false, start from scratch, true - start from previous
//			boolean       gen_inverse,     // generate inverse matches
			boolean       save_each,       // save state file after each match
			boolean       log_append,      //
			String        log_path,
			String        orthoMapsCollection_path,
			
			double        search_step,
			double        search_range_in,
			double        good_rms,
			double        max_rms,
			double        max_rms_refine,
			int           min_overlap,
			int           num_iter_lma,
			boolean       ignore_rms,
			int           spiral_debug,
			double        frac_remove,
			double        metric_error,
			boolean       pmtch_use_affine,			
			double        max_std,  
			double        min_std_rad,
			boolean       ignore_prev_rms, 
			int           num_tries,
			double        rad_fraction,
			double        max_tile_rad,
			double        fill_fraction,
			double        fill_fraction_final,
			double        ease_nosfm,
			double        double_threshold,
			double []     max_rms_iter,
			int           min_scene,
			double        pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
			double        pull_tilt,        // > 0
			double        pull_scale,       // = 0
			int           debugLevel) {
		int []       indices = pairsGraph.getIndices();
		boolean batch_mode = true;
//		int num_scenes = indices.length;
		int num_pairs = 0;
		ArrayList<Point> failed_pairs = new ArrayList<Point>();
		int ok_dist = 1;
		int [] start_ij = {0,0};
		int [] next_pair;

		while (!pairsGraph.isSingleConnected()) {
			next_pair = pairsGraph.suggestPairSingle( // single-connected, no cycles
					// add some parameters
					heur, // int         heur,
					ok_dist, // int         ok_dist)
					start_ij, // int []      start_ij) //  = new int [indices.length];
					pairsGraph.useMultiThreaded(), // multi,
					debugLevel);
			if (next_pair == null) {
				System.out.println("No pairs suggested,  isSingleConnected()="+pairsGraph.isSingleConnected());
				break;
			}
			
			if (next_pair[2] > PairsGraph.HEUR_LAST_SEQ) {
				heur &= ~PairsGraph.HEUR_LAST_SEQ;
			}
			if (next_pair[2] > PairsGraph.HEUR_DIV_LONG) {
				heur &= ~PairsGraph.HEUR_DIV_LONG;
			}
			if (next_pair.length > 3) {
				ok_dist = next_pair[3];
				start_ij[0] = next_pair[0];
				start_ij[1] = next_pair[1];
				if (debugLevel > 0) {
					System.out.println(String.format("%3d: %3d-%3d %2d (%3d)", num_pairs,next_pair[0], next_pair[1],next_pair[2],next_pair[3]));
				}

			} else {
				if (debugLevel > 0) {
					System.out.println(String.format("%3d: %3d-%3d %2d", num_pairs,next_pair[0], next_pair[1],next_pair[2]));
				}
			}
			
			int [] ipair = {indices[next_pair[0]], indices[next_pair[1]]};
			if (ipair[0] < min_scene) {
				System.out.println ("Skipping "+ipair[0]+":"+ipair[1]+" until "+min_scene);
				continue;
			}
			boolean direct = ipair[0] < ipair[1]; // always?
	    	int min_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	int max_zoom_lev = ortho_maps[ipair[0]].getOriginalZoomLevel();
	    	double max_agl =   ortho_maps[ipair[0]].getAGL();
	    	for (int i = 0; i < ipair.length; i++) {
	    		max_agl =      Math.max(max_agl, ortho_maps[ipair[i]].getAGL());	    		
	    		min_zoom_lev = Math.min(min_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    		max_zoom_lev = Math.max(max_zoom_lev, ortho_maps[ipair[i]].getOriginalZoomLevel());
	    	}
        	double agl_ratio = max_agl/50.0;
        	double metric_error_adj = metric_error * agl_ratio * agl_ratio; // metric_error settings is good for 50m. Increase for higher Maybe squared?
	    	int initial_zoom = max_zoom_lev - 4;  // another algorithm?
	    	// overlaps
	    	double overlap_frac = pairsGraph.getOverlap(next_pair);
	    	double overlap_frac_mod = overlap_frac;
	    	double search_range = search_range_in;
	    	while (overlap_frac_mod < double_threshold) {
	    		overlap_frac_mod *= 2;
	    		initial_zoom += 1;
	    		search_range *= 2;
	    	}
			// unityAffine()
			
			double [][] affine0 = pmtch_use_affine?ortho_maps[ipair[0]].getAffine():unityAffine(); // {{1,0,0},{0,1,0}}; // will always stay the same
			double [][] affine1 = pmtch_use_affine?ortho_maps[ipair[1]].getAffine():unityAffine(); // {{1,0,0},{0,1,0}}; // here (manual mode) start from the center, may use prediction in auto
			double [][][] affines = new double[][][] {affine0,affine1};
			double spiral_rms = Double.NaN;
				// now always unity, but after partial adjustment may be non-unity
				// TODO: Not yet tested with intermediate adjustment.
			PairwiseOrthoMatch pairwiseOrthoMatch = SpiralMatch (
					clt_parameters,  // CLTParameters    clt_parameters,
					frac_remove,     // double           frac_remove, //  =        0.25
					metric_error_adj,// double           metric_error,
					pmtch_use_affine, // boolean          pmtch_use_affine,
					max_std,         // double           max_std,     // maximal standard deviation to limit center area  
					min_std_rad,     // double           min_std_rad, // minimal radius of the central area (if less - fail)
					rad_fraction,    // double           rad_fraction,
					max_tile_rad,    // double           max_tile_rad, //  = 30;						
					fill_fraction,   // double           fill_fraction,
					fill_fraction_final, // double           fill_fraction_final,
					ease_nosfm,      // double           ease_nosfm,
					ipair,           // int []           gpu_pair,
					affines,         // double [][][]    affines_init,  // here in meters, relative to vertical points
					initial_zoom,    // int              zoom_lev,
					search_step,     // double           pix_step,
					search_range,    // double           pix_range,
					good_rms,        // double           good_rms,
					max_rms,         // 			double           max_rms,
					num_iter_lma,    // int              num_tries, //  = 5
					min_overlap,     // int              min_overlap, // 3000
					ignore_rms,      // boolean          ignore_rms,
					max_rms_iter,    // double []        max_rms_iter, //  = {1.0, 0.6};//
					overlap_frac,    // double           overlap, // OR IS IT overlap_frac_mod ?
					pull_skew,       // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
					pull_tilt,       // double           pull_tilt,        // > 0
					pull_scale,      // double           pull_scale,       // = 0
					spiral_debug);   // int              debugLevel)
			
			// late failure
			if ((pairwiseOrthoMatch != null) && !Double.isNaN(pairwiseOrthoMatch.rms) && !pairwiseOrthoMatch.ok) {
				String str_failed = "%4d\t%4d\t%s\t%s\t%6.4f\t%3d\t%3d\t%3d\t%6.4f\tFAILED\n";
				if (log_append && (log_path != null)) { // assuming directory exists
					StringBuffer sb = new StringBuffer();
					sb.append(String.format(str_failed,
							ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
							overlap_frac,  initial_zoom,
							pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],pairwiseOrthoMatch.rms));
					CalibrationFileManagement.saveStringToFile (
							log_path,          //String path,
							sb.toString(), // data,
							true); // boolean append)
				}
			}
			Point pair = new Point(next_pair[0],next_pair[1]);
			boolean success = (pairwiseOrthoMatch != null) && pairwiseOrthoMatch.ok; // && !Double.isNaN(pairwiseOrthoMatch.rms)
			if (!success) { // not yet add to pairsGraph until verified with high-res
				if ((pairwiseOrthoMatch == null) || Double.isNaN(pairwiseOrthoMatch.rms)) { // too high RMS already reported
					String str_failed = "%4d\t%4d\t%s\t%s\t%6.4f\t%3d\tFAILED\n";
					if (log_append && (log_path != null)) { // assuming directory exists
						StringBuffer sb = new StringBuffer();
						sb.append(String.format(str_failed,
								ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
								overlap_frac,
								initial_zoom));
						CalibrationFileManagement.saveStringToFile (
								log_path,          //String path,
								sb.toString(), // data,
								true); // boolean append)
					}
				}
				failed_pairs.add(pair);
			} else { // success!
				spiral_rms = pairwiseOrthoMatch.rms;
//				new_pairs.add(pair); // adjusted with spiral
			// refine match with high resolution
				affines[1] = pairwiseOrthoMatch.getAffine();
				affines[1][0] = affines[1][0].clone(); 
				affines[1][1] = affines[1][1].clone();
				Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair()			
				correlateOrthoPair(
						clt_parameters,  // CLTParameters    clt_parameters,
						pairwiseOrthoMatch, //PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
						0,                // 			int              min_overlap,
						max_std,         // double           max_std,      // maximal standard deviation to limit center area  
						min_std_rad,     // double           min_std_rad,  // minimal radius of the central area (if less - fail)
						frac_remove,     // double           frac_remove, //  =        0.25
						metric_error_adj,// 			double           metric_error,
						ignore_prev_rms, // boolean ignore_prev_rms,
						num_tries,       //  = 5int              num_tries, //  = 5
						false, // ,            // boolean          calc_warp, (will return null if false)
						batch_mode,      // boolean          batch_mode,
						ipair,           // String []        gpu_spair,
						affines,         // double [][][]    affines, // on top of GPS offsets
						woi,             // Rectangle        woi,
						min_zoom_lev,    // int              zoom_lev,
						false, // show_vf,         // boolean show_vf,
						null, // ground_planes,   // double [][]      ground_planes, // null or double[2] - will return ground planes
						rad_fraction,    // double           rad_fraction,
						max_tile_rad,    // double           max_tile_rad, //  = 30;
						fill_fraction,   // double           fill_fraction,
						fill_fraction_final, // double           fill_fraction_final,
						ease_nosfm,      // double           ease_nosfm,
						null,            //  double []        max_rms_iter, //  = {1.0, 0.6};//
						pull_skew,        // double           pull_skew,        // ~rotation, = 0 fraction of the total weight == 1
						pull_tilt,        // double           pull_tilt,        // > 0
						pull_scale,       // double           pull_scale,       // = 0
						debugLevel-4);   // final int        debugLevel)
				boolean failed_refine = Double.isNaN(pairwiseOrthoMatch.rms) || (pairwiseOrthoMatch.rms > max_rms_refine);
				// Create log line and write it
				StringBuffer sb = new StringBuffer();
				sb.append(String.format("%4d\t%4d\t%s\t%s\t%6.4f\t%3d",
						ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName(),
						overlap_frac,  initial_zoom));

				sb.append(String.format("\t%3d\t%3d\t%6.4f",
						pairwiseOrthoMatch.nxy[0],pairwiseOrthoMatch.nxy[1],spiral_rms)); 
				if (failed_refine) {
					String str_failed = String.format("\t%6.4f\tFAILED\n",pairwiseOrthoMatch.rms);
					failed_pairs.add(pair);
					sb.append(String.format(str_failed));
					if (debugLevel > -4) System.out.print("Final adjustment (3)"+str_failed);				
				} else {
					pairwiseOrthoMatch.overlap = overlap_frac; // needed here if refining old/manual w/o overlap 
					sb.append(String.format("\t%6.4f\t%3d\n",pairwiseOrthoMatch.rms,pairwiseOrthoMatch.zoom_lev)); 
					if (debugLevel > -4) System.out.println("Final adjustment (4) RMSE="+pairwiseOrthoMatch.rms+
							", overlap = "+pairwiseOrthoMatch.overlap);				
					ortho_maps[ipair[0]].setMatch(ortho_maps[ipair[1]].getName(),pairwiseOrthoMatch);
					if (save_each && (orthoMapsCollection_path != null)) {
						try {
							writeOrthoMapsCollection(orthoMapsCollection_path);
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						if (debugLevel > -4) {
							System.out.println("Saved data to "+ orthoMapsCollection_path);
						}
					}
				}
				if (log_append && (log_path != null)) { // assuming directory exists
					CalibrationFileManagement.saveStringToFile (
							log_path,          //String path,
							sb.toString(), // data,
							true); // boolean append)
				}
				success &= !failed_refine;
			}
			pairsGraph.recordPair(
					next_pair,    // int []   pair,
					success,      // boolean  success,
					pairsGraph.useMultiThreaded());
			if (success) {
				num_pairs++;
			}
		} // next pair
		
		if (log_append && (log_path != null)) { // assuming directory exists
			StringBuffer sb = new StringBuffer();
			sb.append("\nSUMMARY\n");
			sb.append("new\t"+ num_pairs+"\n");
//			sb.append("new\t"+ new_pairs.size()+"\n");
			sb.append("failed\t"+failed_pairs.size()+"\n");
			if (!failed_pairs.isEmpty()) {
				for (int i = 0; i < failed_pairs.size(); i++) {
					int [] ipair = {indices[failed_pairs.get(i).x], indices[failed_pairs.get(i).y]};
					sb.append(String.format("%4d\t%4d\t%s\t%s\n",
							ipair[0], ipair[1], ortho_maps[ipair[0]].getName(), ortho_maps[ipair[1]].getName()));
				}
			}
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
        	if (debugLevel > -4) {
        		System.out.println("Appended log file "+ log_path);
        	}
		}
		if (orthoMapsCollection_path != null) {
        	try {
        		writeOrthoMapsCollection(orthoMapsCollection_path);
        	} catch (IOException e) {
        		// TODO Auto-generated catch block
        		e.printStackTrace();
        	}
        	if (debugLevel > -4) {
        		System.out.println("Saved data to "+ orthoMapsCollection_path);
        	}
		}
		return true;
	}
	
	
	public boolean altutudeMatchPairs(
			CLTParameters clt_parameters,
			String        orthoMapsCollection_path) {
        //OrientationSceneLMA.testGetPairErrQuaternion ();	


        
        
        // Create list of all pairs (after recreating all overlaps with updated affines)
		ArrayList<Point> pairs_list = new ArrayList<Point>();
		boolean dbg00 = false;
		for (OrthoMap map : ortho_maps) {
//			if (getIndex(map.getName()) == 87) {
//				System.out.println("index="+getIndex(map.getName()));
//			}
			for (String other_name: map.pairwise_matches.keySet()) {
				pairs_list.add(new Point(
						getIndex(map.getName()),
						getIndex(other_name)));
				if (dbg00) {
					System.out.println(getIndex(map.getName())+"-"+getIndex(other_name)+" "+map.getName()+"-"+other_name);
					if(getIndex(map.getName()) == getIndex(other_name)) {
						System.out.println();
					}
				}
			}
		}
		// sort pairs_list by x then y
		Collections.sort(pairs_list, new Comparator<Point>() {
			@Override
			public int compare(Point lhs, Point rhs) {
				return (rhs.x > lhs.x) ? -1 : (rhs.x < lhs.x) ? 1 :
					((rhs.y > lhs.y) ? -1 : (rhs.y < lhs.y) ? 1 : 0); // increasing
			}
		});

		// convert ArrayList<Point> to array int[][]
		int [][] available_pairs = new int [pairs_list.size()][2];
		for (int i = 0; i < available_pairs.length; i++) {
			available_pairs[i][0] = pairs_list.get(i).x;
			available_pairs[i][1] = pairs_list.get(i).y;
		}
		
		boolean flt_undef_only =     false; // clt_parameters.imp.flt_undef_only;    //  false;
		double  flt_min_overlap =    clt_parameters.imp.flt_min_overlap;   //  0.0;
		double  flt_max_overlap =    clt_parameters.imp.flt_max_overlap;   //  1.0;
		double  flt_min_rms =        clt_parameters.imp.flt_min_rms;       //  0.0;
		double  flt_max_rms =        clt_parameters.imp.flt_max_rms;       //  2.0;
		boolean flt_nan_rms =        clt_parameters.imp.flt_nan_rms;       //  false;
		
		boolean flt_filt_zoom =      clt_parameters.imp.flt_filt_zoom;     //  true;
		int     flt_min_zoom =       clt_parameters.imp.flt_min_zoom;      // -2;
		int     flt_max_zoom =       clt_parameters.imp.flt_max_zoom;      // 10;
		double  flt_min_sfm =        clt_parameters.imp.flt_min_sfm;       //  0.0;
		double  flt_max_sfm =        clt_parameters.imp.flt_max_sfm;       //1000.0;
        int     flt_alt =            clt_parameters.imp.flt_alt;           // 0;
		boolean flt_show_names =     true;  // clt_parameters.imp.flt_show_names;    //  true;
		boolean flt_show_overlaps =  true;  // clt_parameters.imp.flt_show_overlaps; //  true;
		boolean flt_show_rms =       true;  // clt_parameters.imp.flt_show_rms;      //  true;
		boolean flt_show_zoom =      true;  // clt_parameters.imp.flt_show_zoom;     //  true;
		boolean flt_show_alt =       true;  // clt_parameters.imp.flt_show_alt;     //  true;

		boolean alt_overwrite =      clt_parameters.imp.alt_overwrite; // false; // overwrite existing altitude match pairs
		boolean alt_pairwise =       clt_parameters.imp.alt_pairwise; // false; // overwrite existing altitude match pairs
		
		double  alt_sigma =          clt_parameters.imp.alt_sigma;        // 5.0; Reduce weight of the border tiles, Gaussian sigma in tiles to apply to weights.
		double  alt_abs_outliers =   clt_parameters.imp.alt_abs_outliers; // 3.0;     // remove absolute outliers when fitting planes 
		double  alt_outliers =       clt_parameters.imp.alt_outliers;     // 0.05; Remove outliers when fitting planes, removed fraction.
		int     alt_refine =         clt_parameters.imp.alt_refine;       // 1; Refine altitude difference plane after removing outliers (0 - no outlier removal, 1 - remove outliers and refine once, ...)
		
		double  metric_err =         clt_parameters.imp.pmtch_metric_err;// 0.05; // 0.02;//  2 cm

		double  weight_rot =         clt_parameters.imp.alt_weight_rot;    // >0 weight of pairs errors in qn3
		double  weight_tilt =        clt_parameters.imp.alt_weight_tilt;   // >0 weight of pairs errors in qn1, qn2
		double  weight_scale =       clt_parameters.imp.alt_weight_scale;  // >0 weight in pairs scale-1.0 errors
		double  pull  =              clt_parameters.imp.alt_pull;          // 0 <= pull <1 - fraction of all RMS contributors
		double  pull_rots =          clt_parameters.imp.alt_pull_rots;     // >=0 weight of sum of rotations, may be 0, normalized by pull value 
		double  pull_tilts  =        clt_parameters.imp.alt_pull_tilts;    // >=0 weights of sum of qn1 and qn2 of scenes, normalized by pull value
		double  pull_scales  =       clt_parameters.imp.alt_pull_scales;   // >=0 weights of scales of scenes, normalized by pull value
		
		// log/save parameters
		boolean save_each     =      clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean log_append    =      clt_parameters.imp.pwise_log_append;   //
		String  log_path      =      clt_parameters.imp.pwise_log_path;     // 
		int     debugLevel    =      clt_parameters.imp.pwise_debug;        //
		
		boolean flt_update_config =  false;
		boolean select_pairs =       false;
		
		GenericJTabbedDialog gdf = new GenericJTabbedDialog("Select pairs filter/display",800,1200);
		gdf.addMessage("Filter pairs parameters");
		gdf.addNumericField("Minimal scene overlap (0..1)",flt_min_overlap,  3,7,"", "Minimal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addNumericField("Maximal scene overlap (0..1)",flt_max_overlap,  3,7,"", "Maximal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addCheckbox    ("Filter by zoom level"  ,      flt_filt_zoom, "Filter by the zoom level used for matching.");
		gdf.addNumericField("Minimal zoom",                flt_min_zoom, 0,3,"","Minimal zoom level used for matching.");
		gdf.addNumericField("Maximal zoom",                flt_max_zoom, 0,3,"","Maximal zoom level used for matching.");
		gdf.addNumericField("Minimal SfM gain",            flt_min_sfm,  3,7,"","Minimal SfM gain of the minimum in the scene pair.");
		gdf.addNumericField("Maximal SfM gain",            flt_max_sfm,  3,7,"","Maximal SfM gain of the minimum in the scene pair.");
 		gdf.addNumericField("Minimal RMSE",                flt_min_rms,  3,7,"",	 "Minimal LMA RMSE of the scene pair.");
		gdf.addNumericField("Maximal RMSE",                flt_max_rms,  3,7,"",	 "Maximal LMA RMSE of the scene pair.");
		gdf.addCheckbox    ("NaN RMS (failed match)",      flt_nan_rms, "Keep only failed matches with RMSE=NaN.");
		
		gdf.addChoice("Filter by pairwise ALT availability",IntersceneMatchParameters.FLT_ALT_MODES, IntersceneMatchParameters.FLT_ALT_MODES[flt_alt],"Filter by pairwise ALT availability.");
		gdf.addMessage("Alt match parameters");
		gdf.addCheckbox    ("Overwrite existing alt_data", alt_overwrite, "Overwrite already defined altitude match pairs.");
		gdf.addCheckbox    ("Use pairwise affines",        alt_pairwise,  "Use pairwise affines if available (false - always recalculate from individual).");
		gdf.addNumericField("Border sigma",                alt_sigma,  3,7,"tiles",	"Reduce weight of the border tiles, Gaussian sigma in tiles to apply to weights.");
		gdf.addNumericField("Absolute outliers offset",    alt_abs_outliers,  3,7,"m","Remove absolute outliers when fitting planes.");
		gdf.addNumericField("Fraction of ouliers",         alt_outliers,  3,7,"",	"Remove outliers when fitting planes, removed fraction.");
		gdf.addNumericField("Number of alt plane refines", alt_refine, 0,3,"",      "Refine altitude difference plane after removing outliers (0 - no outlier removal, 1 - remove outliers and refine once, ...)");
		
		
		
		gdf.addMessage("Log and Save, and Debug parameters");
	    gdf.addCheckbox    ("Save state after each match", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gdf.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gdf.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gdf.addNumericField("Debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
		gdf.addCheckbox    ("Update configuration",        flt_update_config, "Update matching configuration parameters to be saved as defaults.");
		gdf.addCheckbox    ("Select filtered pairs",       select_pairs, "Manually select from the filtered pairs.");
		
		gdf.showDialog();
		if (gdf.wasCanceled()) return false;
		flt_min_overlap   =       gdf.getNextNumber();
		flt_max_overlap   =       gdf.getNextNumber();
        flt_filt_zoom     =       gdf.getNextBoolean();
        flt_min_zoom      = (int) gdf.getNextNumber(); 
        flt_max_zoom      = (int) gdf.getNextNumber();
        flt_min_sfm       =       gdf.getNextNumber();
        flt_max_sfm       =       gdf.getNextNumber();
		flt_min_rms       =       gdf.getNextNumber();
		flt_max_rms       =       gdf.getNextNumber();
		flt_nan_rms       =       gdf.getNextBoolean();
        flt_alt           =       gdf.getNextChoiceIndex();
        alt_overwrite     =       gdf.getNextBoolean();
        alt_pairwise      =       gdf.getNextBoolean();
        alt_sigma         =       gdf.getNextNumber();
        alt_abs_outliers  =       gdf.getNextNumber();
        alt_outliers      =       gdf.getNextNumber();
        alt_refine        = (int) gdf.getNextNumber();
		save_each         =       gdf.getNextBoolean();
		log_append        =       gdf.getNextBoolean();
		log_path          =       gdf.getNextString();
		debugLevel =        (int) gdf.getNextNumber();
		flt_update_config =       gdf.getNextBoolean();
		select_pairs =            gdf.getNextBoolean();
		
		if (flt_update_config) {
			clt_parameters.imp.flt_min_overlap =   flt_min_overlap;
			clt_parameters.imp.flt_max_overlap =   flt_max_overlap;
			clt_parameters.imp.flt_filt_zoom =     flt_filt_zoom;
			clt_parameters.imp.flt_min_zoom =      flt_min_zoom;
			clt_parameters.imp.flt_max_zoom =      flt_max_zoom;
    		clt_parameters.imp.flt_min_sfm =       flt_min_sfm;
    		clt_parameters.imp.flt_max_sfm =       flt_max_sfm;
			clt_parameters.imp.flt_min_rms =       flt_min_rms;
			clt_parameters.imp.flt_max_rms =       flt_max_rms;
			clt_parameters.imp.flt_nan_rms =       flt_nan_rms;
			
			clt_parameters.imp.flt_alt =           flt_alt;
			clt_parameters.imp.alt_overwrite =     alt_overwrite; 
			clt_parameters.imp.alt_pairwise =      alt_pairwise;
   			clt_parameters.imp.alt_sigma    =      alt_sigma;
			clt_parameters.imp.alt_abs_outliers =  alt_abs_outliers;
			clt_parameters.imp.alt_outliers =      alt_outliers;
			clt_parameters.imp.alt_refine   =      alt_refine;
			
			clt_parameters.imp.pwise_save_each  = save_each;  
			clt_parameters.imp.pwise_log_append = log_append; 
			clt_parameters.imp.pwise_log_path   = log_path;   
			clt_parameters.imp.pwise_debug      = debugLevel; 
		}

		available_pairs = filterPairs(
				available_pairs, // int [][] plist_in,
				flt_undef_only,  // boolean  undef_only,
				flt_min_overlap, // double   min_overlap,
				flt_max_overlap, // double   max_overlap,
				flt_min_rms,     // double   min_rms,
				flt_max_rms,     // double   max_rms,
				flt_nan_rms,     // boolean  nan_rms)
				flt_filt_zoom,   // boolean          filt_zoom,
				flt_min_zoom,    // int              min_zoom,
				flt_max_zoom,    // int              max_zoom)
				flt_min_sfm,     // double           min_sfm,
				flt_max_sfm,     // double           max_sfm,
				flt_alt);       // int               flt_alt)\
		if (select_pairs) {
			String [] choices_all = textPairs (
					available_pairs,   // int [][] plist,
					flt_show_names,    // boolean          show_names,
					flt_show_overlaps, // boolean          show_overlap,
					flt_show_rms,      // boolean          show_rms,
					flt_show_zoom,     // boolean          show_zoom,
					flt_show_alt,      // boolean          show_alt,
					false,             // boolean          use_tab,
					null); // flt_extra_line);   // String           extra_line)
			GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pairs",1200,1000);
			boolean [] bselected_pairs = new boolean [choices_all.length];
			for (int i = 0; i < choices_all.length; i++) {
				gdc.addCheckbox    (i+": "+choices_all[i],    bselected_pairs[i],  "Select this scene pair for processing.");
			}
			gdc.showDialog();
			if (gdc.wasCanceled()) return false;
			int num_pairs = 0;
			for (int i = 0; i < choices_all.length; i++) {
				bselected_pairs[i] = gdc.getNextBoolean();
				if (bselected_pairs[i]) num_pairs++;
			}
			int [][] selected_pairs = new int[num_pairs][];
			int indx = 0;
			for (int i = 0; i < bselected_pairs.length; i++) if (bselected_pairs[i]){
				selected_pairs[indx++]= available_pairs[i];
			}
			available_pairs = selected_pairs;
		}
		
		
		
		String [] choices_all = textPairs (
				available_pairs,   // int [][] plist,
				flt_show_names,    // boolean          show_names,
				flt_show_overlaps, // boolean          show_overlap,
				flt_show_rms,      // boolean          show_rms,
				flt_show_zoom,     // boolean          show_zoom,
				flt_show_alt,      // boolean          show_alt,
				true,              // boolean          use_tab,
				null);             // String           extra_line)
		if (debugLevel > 0) {
			System.out.println("Selected "+available_pairs.length+" scene pairs for matching");
			for (int i = 0; i < available_pairs.length; i++) {
				System.out.println(String.format("%4d:%s",i,choices_all[i]));
			}
		}
		if (available_pairs.length == 0) {
			return false;
		}
	////
	return 	OrthoAltitudeMatch.altutudeMatchPairs(
			clt_parameters,           // CLTParameters clt_parameters,
			this,                     // OrthoMapsCollection orthoMapsCollection, 
			available_pairs,          // int [][]      available_pairs,
			select_pairs || alt_overwrite,            // boolean             alt_overwrite, // overwrite existing altitude match pairs
			alt_pairwise,             // boolean             alt_pairwise,  // use pairwise affines if available
			alt_sigma,                // double  alt_sigma,     // 5.0; Reduce weight of the border tiles, Gaussian sigma in tiles to apply to weights.
			alt_abs_outliers,         //double              alt_abs_outliers, //  =   3.0;     // remove absolute outliers when fitting planes 
			alt_outliers,             // double  alt_outliers,  // 0.05; Remove outliers when fitting planes, removed fraction.
			alt_refine,               // int     alt_refine,    // 1; Refine altitude difference plane after removing outliers (0 - no outlier removal, 1 - remove outliers and refine once, ...)
			metric_err,               // double  metric_error,
			weight_rot,               // double  weight_rot,    // >0 weight of pairs errors in qn3
			weight_tilt,              // double  weight_tilt,   // >0 weight of pairs errors in qn1, qn2
			weight_scale,             // double  weight_scale,  // >0 weight in pairs scale-1.0 errors
			pull,                     // double  pull,          // 0 <= pull <1 - fraction of all RMS contributors
			pull_rots,                // double  pull_rots,     // >=0 weight of sum of rotations, may be 0, normalized by pull value 
			pull_tilts,               // double  pull_tilts,    // >=0 weights of sum of qn1 and qn2 of scenes, normalized by pull value
			pull_scales,              // double  pull_scales,   // >=0 weights of scales of scenes, normalized by pull value
			// log/save parameters
			save_each,                // boolean save_each,
			log_append,               // boolean log_append,
			log_path,                 // String  log_path,
			orthoMapsCollection_path, // String  orthoMapsCollection_path,
			debugLevel);              // int     debugLevel)
    }		
		
	public boolean displayScenePairs(
			CLTParameters clt_parameters,
			String        orthoMapsCollection_path) {
		// Create list of all pairs (after recreating all overlaps with updated affines)
		ArrayList<Point> pairs_list = new ArrayList<Point>(); 
		boolean dbg00=false;
		for (OrthoMap map : ortho_maps) {
			if (getIndex(map.getName()) == 87) {
				System.out.println("index="+getIndex(map.getName()));
			}
			for (String other_name: map.pairwise_matches.keySet()) {
				pairs_list.add(new Point(
						getIndex(map.getName()),
						getIndex(other_name)));
				if (dbg00) {
					System.out.println(getIndex(map.getName())+"-"+getIndex(other_name)+" "+map.getName()+"-"+other_name);
					if(getIndex(map.getName()) == getIndex(other_name)) {
						System.out.println();
					}
				}
			}
		}
		// sort pairs_list by x then y
		Collections.sort(pairs_list, new Comparator<Point>() {
			@Override
			public int compare(Point lhs, Point rhs) {
				return (rhs.x > lhs.x) ? -1 : (rhs.x < lhs.x) ? 1 :
					((rhs.y > lhs.y) ? -1 : (rhs.y < lhs.y) ? 1 : 0); // increasing
			}
		});

		// convert ArrayList<Point> to array int[][]
		int [][] available_pairs = new int [pairs_list.size()][2];
		for (int i = 0; i < available_pairs.length; i++) {
			available_pairs[i][0] = pairs_list.get(i).x;
			available_pairs[i][1] = pairs_list.get(i).y;
		}
		
		boolean flt_undef_only =     false; // clt_parameters.imp.flt_undef_only;    //  false;
		double  flt_min_overlap =    clt_parameters.imp.flt_min_overlap;   //  0.0;
		double  flt_max_overlap =    clt_parameters.imp.flt_max_overlap;   //  1.0;
		double  flt_min_rms =        clt_parameters.imp.flt_min_rms;       //  0.0;
		double  flt_max_rms =        clt_parameters.imp.flt_max_rms;       //  2.0;
		boolean flt_nan_rms =        clt_parameters.imp.flt_nan_rms;       //  false;
		
		boolean flt_filt_zoom =      clt_parameters.imp.flt_filt_zoom;     //  true;
		int     flt_min_zoom =       clt_parameters.imp.flt_min_zoom;      // -2;
		int     flt_max_zoom =       clt_parameters.imp.flt_max_zoom;      // 10;
		double  flt_min_sfm =        clt_parameters.imp.flt_min_sfm;       //  0.0;
		double  flt_max_sfm =        clt_parameters.imp.flt_max_sfm;       //1000.0;
        int     flt_alt =            clt_parameters.imp.flt_alt;           // 0;
		boolean flt_show_names =     true;  // clt_parameters.imp.flt_show_names;    //  true;
		boolean flt_show_overlaps =  true;  // clt_parameters.imp.flt_show_overlaps; //  true;
		boolean flt_show_rms =       true;  // clt_parameters.imp.flt_show_rms;      //  true;
		boolean flt_show_zoom =      true;  // clt_parameters.imp.flt_show_zoom;     //  true;
		boolean flt_show_alt =       true;  // clt_parameters.imp.flt_show_alt;     //  true;
		boolean flt_show_svd =       true;
		boolean flt_show_degrees =   true;
		

		boolean alt_overwrite =      clt_parameters.imp.alt_overwrite; // false; // overwrite existing altitude match pairs
		boolean alt_pairwise =       clt_parameters.imp.alt_pairwise; // false; // overwrite existing altitude match pairs
		double  metric_err =         clt_parameters.imp.pmtch_metric_err;// 0.05; // 0.02;//  2 cm

		// log/save parameters
		boolean save_each     =      clt_parameters.imp.pwise_save_each;    // save state file after each match
		boolean log_append    =      clt_parameters.imp.pwise_log_append;   //
		String  log_path      =      clt_parameters.imp.pwise_log_path;     // 
		int     debugLevel    =      clt_parameters.imp.pwise_debug;        //
		
		boolean flt_update_config =  false;
		
		
		GenericJTabbedDialog gdf = new GenericJTabbedDialog("Select pairs filter/display",800,1200);
		gdf.addMessage("Filter pairs parameters");
		gdf.addNumericField("Minimal scene overlap (0..1)",flt_min_overlap,  3,7,"", "Minimal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addNumericField("Maximal scene overlap (0..1)",flt_max_overlap,  3,7,"", "Maximal overlap of the scenes to keep (0-no overlap, 1.0 - smaller scene is inside the parger one.");
		gdf.addCheckbox    ("Filter by zoom level"  ,      flt_filt_zoom, "Filter by the zoom level used for matching.");
		gdf.addNumericField("Minimal zoom",                flt_min_zoom, 0,3,"","Minimal zoom level used for matching.");
		gdf.addNumericField("Maximal zoom",                flt_max_zoom, 0,3,"","Maximal zoom level used for matching.");
		gdf.addNumericField("Minimal SfM gain",            flt_min_sfm,  3,7,"","Minimal SfM gain of the minimum in the scene pair.");
		gdf.addNumericField("Maximal SfM gain",            flt_max_sfm,  3,7,"","Maximal SfM gain of the minimum in the scene pair.");
 		gdf.addNumericField("Minimal RMSE",                flt_min_rms,  3,7,"",	 "Minimal LMA RMSE of the scene pair.");
		gdf.addNumericField("Maximal RMSE",                flt_max_rms,  3,7,"",	 "Maximal LMA RMSE of the scene pair.");
		gdf.addCheckbox    ("NaN RMS (failed match)",      flt_nan_rms, "Keep only failed matches with RMSE=NaN.");
		
		gdf.addChoice("Filter by pairwise ALT availability",IntersceneMatchParameters.FLT_ALT_MODES, IntersceneMatchParameters.FLT_ALT_MODES[flt_alt],"Filter by pairwise ALT availability.");
		gdf.addMessage("Alt match parameters");
		gdf.addCheckbox    ("Overwrite existing alt_data", alt_overwrite, "Overwrite already defined altitude match pairs.");
		gdf.addCheckbox    ("Use pairwise affines",        alt_pairwise,  "Use pairwise affines if available (false - always recalculate from individual).");
		gdf.addMessage("Log and Save, and Debug parameters");
	    gdf.addCheckbox    ("Save state after each match", save_each, "Update state file after each match generation to mitigate possible crashes.");
	    gdf.addCheckbox    ("Write log file",              log_append, "Enable writing log file with matching results.");
	    gdf.addStringField ("Log file full path",          log_path, 150, "Path of the log file to be appended.");	
		gdf.addNumericField("Debug level",  debugLevel, 0,3,"","Debug level during Spiral search.");
		gdf.addCheckbox    ("Update configuration",        flt_update_config, "Update matching configuration parameters to be saved as defaults.");
		
		gdf.showDialog();
		if (gdf.wasCanceled()) return false;
		flt_min_overlap   =       gdf.getNextNumber();
		flt_max_overlap   =       gdf.getNextNumber();
        flt_filt_zoom     =       gdf.getNextBoolean();
        flt_min_zoom      = (int) gdf.getNextNumber(); 
        flt_max_zoom      = (int) gdf.getNextNumber();
        flt_min_sfm       =       gdf.getNextNumber();
        flt_max_sfm       =       gdf.getNextNumber();
		flt_min_rms       =       gdf.getNextNumber();
		flt_max_rms       =       gdf.getNextNumber();
		flt_nan_rms       =       gdf.getNextBoolean();
        flt_alt           =       gdf.getNextChoiceIndex();
        alt_overwrite     =       gdf.getNextBoolean();
        alt_pairwise     =        gdf.getNextBoolean();
		save_each         =       gdf.getNextBoolean();
		log_append        =       gdf.getNextBoolean();
		log_path          =       gdf.getNextString();
		debugLevel =        (int) gdf.getNextNumber();
		flt_update_config =       gdf.getNextBoolean();
		
		if (flt_update_config) {
			clt_parameters.imp.flt_min_overlap =   flt_min_overlap;
			clt_parameters.imp.flt_max_overlap =   flt_max_overlap;
			clt_parameters.imp.flt_filt_zoom =     flt_filt_zoom;
			clt_parameters.imp.flt_min_zoom =      flt_min_zoom;
			clt_parameters.imp.flt_max_zoom =      flt_max_zoom;
    		clt_parameters.imp.flt_min_sfm =       flt_min_sfm;
    		clt_parameters.imp.flt_max_sfm =       flt_max_sfm;
			clt_parameters.imp.flt_min_rms =       flt_min_rms;
			clt_parameters.imp.flt_max_rms =       flt_max_rms;
			clt_parameters.imp.flt_nan_rms =       flt_nan_rms;
			
			clt_parameters.imp.flt_alt =           flt_alt;
			clt_parameters.imp.alt_overwrite =     alt_overwrite; 
			clt_parameters.imp.alt_pairwise =      alt_pairwise; 
			clt_parameters.imp.pwise_save_each  = save_each;  
			clt_parameters.imp.pwise_log_append = log_append; 
			clt_parameters.imp.pwise_log_path   = log_path;   
			clt_parameters.imp.pwise_debug      = debugLevel; 
		}

		available_pairs = filterPairs(
				available_pairs, // int [][] plist_in,
				flt_undef_only,  // boolean  undef_only,
				flt_min_overlap, // double   min_overlap,
				flt_max_overlap, // double   max_overlap,
				flt_min_rms,     // double   min_rms,
				flt_max_rms,     // double   max_rms,
				flt_nan_rms,     // boolean  nan_rms)
				flt_filt_zoom,   // boolean          filt_zoom,
				flt_min_zoom,    // int              min_zoom,
				flt_max_zoom,    // int              max_zoom)
				flt_min_sfm,     // double           min_sfm,
				flt_max_sfm,     // double           max_sfm,
				flt_alt);       // int               flt_alt)
		boolean use_tab=true;
		boolean flt_show_sfm=flt_show_alt;
		String [] choices_all = textPairs (
				available_pairs,   // int [][] plist,
				flt_show_names,    // boolean          show_names,
				flt_show_overlaps, // boolean          show_overlap,
				flt_show_rms,      // boolean          show_rms,
				flt_show_zoom,     // boolean          show_zoom,
				flt_show_alt,      // boolean          show_alt,
				use_tab,           // boolean          use_tab,
				null,              // String           extra_line)
				flt_show_svd,      // boolean          show_svd,
				flt_show_degrees); // boolean          use_degrees);
			
		{
			System.out.println("Selected\t"+available_pairs.length+"\tscene pairs for matching");
			if (use_tab) {
				System.out.print(String.format("#\t%3s\t%3s","i0","i1"));
				if (flt_show_names)    System.out.print(String.format("\t%17s\t%17s","timestamp 0","timestamp 1"));
				if (flt_show_overlaps) System.out.print(String.format("\t%5s", "olap%"));
				if (flt_show_rms)      System.out.print(String.format("\t%5s", "RMSE"));
				if (flt_show_zoom)     System.out.print(String.format("\t%4s", "Zoom"));
				if (flt_show_alt)      System.out.print(String.format("\t%7s\t%7s\t%7s", "tiltX","tiltY","offs"));
				if (flt_show_sfm)      System.out.print(String.format("\t%5s\t%5s", "SfM0","SfM1"));
				if (flt_show_svd) {
					String svd_title=SingularValueDecomposition.titleString(flt_show_degrees);
					System.out.print("\t"+ svd_title);
				}
				System.out.println();
			}
			for (int i = 0; i < available_pairs.length; i++) {
//				System.out.println(String.format("%4d:%s",i,choices_all[i]));
				System.out.println(String.format(use_tab?"%4d\t%s":"%4d:%s",i,choices_all[i]));
			}
		}
		if (available_pairs.length == 0) {
			return false;
		}
	////
		return true;
	}	
	
	
	public boolean processComboMap(
			CLTParameters    clt_parameters,
			int debugLevel) {
		int [] indices = getScenesSelection(
				clt_parameters, // CLTParameters       clt_parameters,				
				" to process/display"); // String purpose)
		if (indices == null) {
			return false;
		}
		boolean more = true;
		while (more) { // generate multiple images/stats for the same selection
			more = processComboSelection(
					clt_parameters, // CLTParameters    clt_parameters,
					indices,
					debugLevel);
		}
		return true;
	}
	
	public boolean processComboSelection(
			CLTParameters    clt_parameters,
			int [] indices,
			int debugLevel) {
		boolean show_map_stats =      false;      
		boolean show_combo_map =      false; // true;  // false; // true;
		boolean show_alt_map =        false;
		boolean show_combo_mask =     false; // generate image mas (where it is defined)
		boolean show_frames =         false;
		int     frame_shrink =        4; 
		int     frame_grow =          4;
		double  frame_blur =          1.0;
		double  frame_cut_frac =      0.01;
		int     zoom_lev =           -2; // 0; // +1 - zoom in twice, -1 - zoom out twice
		int     margins =             0; // 10;
		boolean show_centers =        true;
		boolean bounds_to_indices =   true;
		boolean merge_layers =        false;  // instead of individuals
		boolean ignore_affines =      false;
		boolean ignore_equalize =     false;
		boolean use_orientations =    true;
		boolean keep_window =         true;
		double  scale_tilt =          1.0;
		

//		String        save_top_dir =      "/media/elphel/NVME/lwir16-proc/ortho_videos/debug/sept12-13/pattern_match/";
//		String        sub_dir =           "combo_maps";
		String        save_top_dir =      clt_parameters.imp.patt_save_top;
		String        sub_dir =           clt_parameters.imp.patt_save_subdir;
		boolean       show_images =       true;
		boolean       save_images =       true;
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Combo map/stats generation",1200,1000);
		gd.addCheckbox    ("Show statistics for ortho images", show_map_stats,  "Generate and show statistics for ortho maps.");
		gd.addCheckbox    ("Show image map",             show_combo_map,  "Generate composite map of images.");
		gd.addCheckbox    ("Show altitudes",             show_alt_map,    "Generate composite process altitude maps.");
		gd.addCheckbox    ("Show image masks",           show_combo_mask, "Generate composite binary image.");
		gd.addCheckbox    ("Show image frames",          show_frames,     "Generate image borders.");
		gd.addNumericField("Shrink inner frames",        frame_shrink,  0,4,"pix","Shrink inner of the frames from the actual image borders.");
		gd.addNumericField("Grow outer frames",          frame_grow,    0,4,"pix","Grow outer of the frames from the actual image borders.");
		gd.addNumericField("Blur frames",                frame_blur,    3,7,"pix","Blur frames for anti-aliasing.");
		gd.addNumericField("Cut threshold",              frame_cut_frac, 3,7,"","After blurring, replace lower values by NaN.");
 		
		gd.addNumericField("Zoom level",                 zoom_lev,  0,4,"",
				"Zoom level: +1 - zoom in twice, -1 - zoom out twice");
		gd.addNumericField("Margins",                    margins,  0,4,"",
				"Add margins around images");

  		gd.addCheckbox    ("Show transformation centers",show_centers, "Mark verticals from the UAS on the ground.");
		gd.addCheckbox    ("Bounds to selected images",  bounds_to_indices, "Set combo image bounds to selected images only. False - all images.");
		gd.addCheckbox    ("Merge layers",               merge_layers, "Generate composite binary image.");

		gd.addCheckbox    ("Ignore affines",             ignore_affines,   "Ignore available affines, use unity.");
		gd.addCheckbox    ("Ignore equalization",        ignore_equalize,  "Ignore available intensity equalization, use unity.");
		gd.addCheckbox    ("Use scene orientations",     use_orientations, "Do not resize window to accomodate transformed image.");
		gd.addCheckbox    ("Keep sorce window",          keep_window,      "Use scenes scales/orientations.");
		gd.addNumericField("Scale tilt",                 scale_tilt,    3,7,"pix","Blur frames for anti-aliasing.");

		
		
		gd.addStringField ("Pattern match save directory",      save_top_dir, 120, "Top directory to save combo maps");
		gd.addStringField ("Save subdirectory",                 sub_dir, 80, "Subdirectory for versions of the same scene/pair of scenes");
		gd.addCheckbox    ("Show generated images",             show_images, "Display generated images.");
		gd.addCheckbox    ("Save generated images",             save_images, "Save generated image to the location defined by above..");
		
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		show_map_stats =     gd.getNextBoolean();
		show_combo_map =     gd.getNextBoolean();
		show_alt_map =       gd.getNextBoolean();
		show_combo_mask =    gd.getNextBoolean();
		
		show_frames =        gd.getNextBoolean(); 
		frame_shrink = (int) gd.getNextNumber();
		frame_grow =   (int) gd.getNextNumber();
		frame_blur =         gd.getNextNumber();
		frame_cut_frac =     gd.getNextNumber();
		zoom_lev =     (int) gd.getNextNumber();
		margins =      (int) gd.getNextNumber();
		show_centers =       gd.getNextBoolean();
		bounds_to_indices =  gd.getNextBoolean();
		merge_layers =       gd.getNextBoolean();
		ignore_affines =     gd.getNextBoolean();
		ignore_equalize =    gd.getNextBoolean();
		use_orientations =   gd.getNextBoolean();
		keep_window =        gd.getNextBoolean();
		scale_tilt =         gd.getNextNumber();
		
		save_top_dir=        gd.getNextString();
		sub_dir=             gd.getNextString();
		show_images=         gd.getNextBoolean();
		save_images=         gd.getNextBoolean();
		
		String save_dir = null;
		if (save_images) {
			if (!save_top_dir.endsWith(Prefs.getFileSeparator())) {
				save_top_dir+= Prefs.getFileSeparator();
			}
			if (indices.length == 1) {
				save_dir=String.format("%s%03d-%s",
						save_top_dir,indices[0],ortho_maps[indices[0]].getName());
			} else if (indices.length == 2) {
				save_dir=String.format("%s%03d-%03d_%s-%s",
						save_top_dir,indices[0],indices[1],ortho_maps[indices[0]].getName(),ortho_maps[indices[1]].getName());
			}
			save_dir=save_top_dir;

			if (!sub_dir.equals("")) {
				save_dir+= Prefs.getFileSeparator()+sub_dir;
			}
			// create if it does not exist
			File  fsave_dir = new File(save_dir);
			fsave_dir.mkdirs();
			save_dir += Prefs.getFileSeparator();
		}
		
		if (show_map_stats) {
			String time_zone_name = "Europe/Kyiv";
			String [] stats = getScenesStats(indices, time_zone_name); // int [] indices)
			TextWindow tw = new TextWindow("Ortho_images_stats", stats[0], stats[1], 1250,1000);
			if (save_images && (save_dir != null)) {
				String path = save_dir+"ortho_images_stats.csv";
				tw.getTextPanel().saveAs(path);
				if (debugLevel > -4) {
					System.out.println("Saved stats to "+path);
				}
			}
		}
		
		double pix_m = OrthoMap.getPixelSizeMeters (zoom_lev);
		if (use_orientations) {
			System.out.println("using orientations");
			for (int nscene=0; nscene<indices.length; nscene++) {
				int indx = indices[nscene];
				System.out.println("Scene="+indx+", agl="+ortho_maps[indx].getAGL());
				
				double [] qorient = ortho_maps[indx].getQOrinet(); // QuatUtils.invert(ortho_maps[indx].getQOrinet());
				if (qorient == null) {
					qorient = new double [] {1,0,0,0};
				}
				final int src_width = ortho_maps[indx].getImageData().width;   // assuming same image size for alt and texture
				final int src_height = ortho_maps[indx].getImageData().height; // assuming same image size for alt and texture
				final double [] image_data = ortho_maps[indx].getImageData().getDData();
				final double [] alt_data = ortho_maps[indx].getAltData().getDData();
				double    pix_size_meters = OrthoMap.getPixelSizeMeters(ortho_maps[indx].getOriginalZoomLevel());
				//ortho_maps[indx]
				int hwidth = src_width/2;
				int hheight = src_height/2;
				double [] xyz_center = {hwidth, hheight, alt_data[hheight*src_width + hwidth]/ pix_size_meters}; // in "pixels"
				Rectangle owoi = new Rectangle();
				int       stride = 4; // >2, 3 - OK
				double    rfz = 0.3; // w= 1/(x^2+y^2+rfz^2)
				
				double [][] rotation_map_notilt = 	rotateAltCenter(
						qorient,      // final double [] quat_in,
						xyz_center,   // final double [] xyz_center,
						alt_data,     // final double [] alt_in,
						src_width,    // final int       width,
						pix_size_meters, // final double    pix_size_meters,
						owoi,         // final Rectangle owoi, // should not be null - will provide offset and new size
						stride,       // final int       stride, // >=2, use 4?
						rfz,          // final double    rfz,   // 0.3 w= 1/(x^2+y^2+rfz^2)
						keep_window,  // final boolean keep_window,
						0, // scale_tilt,   // final double  scale_tilt,
						debugLevel);  // final int debugLevel)
				double [] image_out_notilt = applySceneRotation(
						image_data,   // final double []   texture_in,
						src_width,    // final int         width_in,
						rotation_map_notilt, // final double [][] rotation_map,
						debugLevel);  // final int debugLevel)
				double [] alt_out_notilt =  getRotatedAlt(
						rotation_map_notilt);// final double [][] rotation_map)
				
				
				double [][] rotation_map = 	rotateAltCenter(
						qorient,      // final double [] quat_in,
						xyz_center,   // final double [] xyz_center,
						alt_data,     // final double [] alt_in,
						src_width,    // final int       width,
						pix_size_meters, // final double    pix_size_meters,
						owoi,         // final Rectangle owoi, // should not be null - will provide offset and new size
						stride,       // final int       stride, // >=2, use 4?
						rfz,          // final double    rfz,   // 0.3 w= 1/(x^2+y^2+rfz^2)
						keep_window,  // final boolean keep_window,
						scale_tilt,   // final double  scale_tilt,
						debugLevel);  // final int debugLevel)
				final double [] xy_center_out = {xyz_center[0]-owoi.x, xyz_center[1]-owoi.y}; // do outside by caller - new rotation center
				double [] image_out = applySceneRotation(
						image_data,   // final double []   texture_in,
						src_width,    // final int         width_in,
						rotation_map, // final double [][] rotation_map,
						debugLevel);  // final int debugLevel)
				double [] alt_out =  getRotatedAlt(
						rotation_map);// final double [][] rotation_map)
				double [] image_data_padded = debugPadInToOut(
						image_data,  // final double []   texture_in,
						src_width,   // final int         width_in,
						owoi);       // final Rectangle   woi)
				double [] alt_data_padded = debugPadInToOut(
						alt_data,    // final double []   texture_in,
						src_width,   // final int         width_in,
						owoi);       // final Rectangle   woi)
				String [] titles= {"img_src","img_rot","img_out","alt_src","alt_rot","alt_out"};
				double [][] dbg_img = {image_data_padded,image_out_notilt,image_out,alt_data_padded, alt_out_notilt, alt_out};
				String title = String.format("%03d-%s", indx, ortho_maps[indx].getName()+"-ROTATION_DBG");
				ImageStack stack = ShowDoubleFloatArrays.makeStack( 
						dbg_img,
						owoi.width,
						owoi.height,
						titles,
						false);
				ImagePlus imp = new ImagePlus(title, stack);
				if (show_centers) {
					PointRoi roi = new PointRoi();
					roi.addPoint(xy_center_out[0], xy_center_out[1], 0+1);
					roi.setOptions("label");
					imp.setRoi(roi);
		        }
		        imp.show(); // debugging
			}
			
		}
		if (show_combo_map) {
			int [] wh =     new int[2];
			int [] origin = new int[2];
			double [][] centers = show_centers? (new double [indices.length][]): null;
			double [][][] affines = null;
			if (ignore_affines) {
				affines = new double [indices.length][2][3];
				for (int i = 0; i < indices.length; i++) {
					affines[i][0][0] = 1;
					affines[i][1][1] = 1;
				}
			}
			
			double [][] dmulti = renderMultiDouble (
					null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
					indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
					bounds_to_indices, // boolean       bounds_to_indices,
					affines,           // null,            // affines,    // double [][][] affines, // null or [indices.length][2][3]
					null,              // double [][]   equalize,
					ignore_equalize,   // true,              // boolean       ignore_equalize,
					null,              // warp,       // FineXYCorr    warp,,
					zoom_lev,          // int         zoom_level,
					wh,                // int []      wh,
					origin,            // int []      origin){ // maps[0] as a reference
					centers);          // double [][] centers)
			if (margins > 0) {
				addMargins(
						dmulti,   // double [][] dmulti,
						wh,       // int width,
						margins); // int margins)
			}
			showSaveMap(
					indices, // int []      indices,
					dmulti,  // double [][] dmulti,
					wh[0],   // int         width,
					
					centers, // int    [][] centers,
					merge_layers, // boolean     merge_layers,
					false,   // boolean     use_max,
					show_images, // boolean     show,
					save_images, // boolean     save,
					save_dir, // String      save_dir,
					"_"+indices.length+"_MAP-"+pix_m, // String      suffix
					debugLevel); // int         debugLevel)			
		}
		if (show_alt_map) {
			int [] wh =     new int[2];
			int [] origin = new int[2];
			double [][] centers = show_centers? (new double [indices.length][]): null;
			double [][] dmulti = renderMultiDouble (
					new double [indices.length][] , // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
					indices,                        // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
					bounds_to_indices,              // boolean       bounds_to_indices,
					null,                           // affines,    // double [][][] affines, // null or [indices.length][2][3]
					null,                           // double [][]   equalize,
					true,                           // boolean       ignore_equalize,
					null,                           // warp,       // FineXYCorr    warp,,
					zoom_lev,                       // int         zoom_level,
					wh,                             // int []      wh,
					origin,                         // int []      origin){ // maps[0] as a reference
					centers);                       // double [][] centers)
			if (margins > 0) {
				addMargins(
						dmulti,   // double [][] dmulti,
						wh,       // int width,
						margins); // int margins)
			}
			showSaveMap(
					indices, // int []      indices,
					dmulti,  // double [][] dmulti,
					wh[0],   // int         width,
					centers, // int    [][] centers,
					merge_layers, // boolean     merge_layers,
					false,   // boolean     use_max,
					show_images, // boolean     show,
					save_images, // boolean     save,
					save_dir, // String      save_dir,
					"_"+indices.length+"_ALT-"+pix_m, // String      suffix
					debugLevel); // int         debugLevel)			
		}
		if (show_combo_mask) {
			int [] wh =     new int[2];
			int [] origin = new int[2];
			double [][] centers = show_centers? (new double [indices.length][]): null;
			double [][] dmulti = renderMultiDouble (
					null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
					indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
					bounds_to_indices, // boolean       bounds_to_indices,
					null,              // affines,    // double [][][] affines, // null or [indices.length][2][3]
					null,              // double [][]   equalize,
					true,              // boolean       ignore_equalize,
					null,              // warp,       // FineXYCorr    warp,,
					zoom_lev,          // int         zoom_level,
					wh,                // int []      wh,
					origin,            // int []      origin){ // maps[0] as a reference
					centers);          // double [][] centers)
			if (margins > 0) {
				addMargins(
						dmulti,   // double [][] dmulti,
						wh,       // int width,
						margins); // int margins)
			}
			for (int n = 0; n < dmulti.length; n++) {
				for (int i = 0; i < dmulti[n].length; i++) {
					dmulti[n][i] = Double.isNaN(dmulti[n][i])? 0:1;
				}
			}
			showSaveMap(
					indices, // int []      indices,
					dmulti,  // double [][] dmulti,
					wh[0],   // int         width,
					centers, // int    [][] centers,
					merge_layers, // boolean     merge_layers,
					true,    // boolean     use_max,
					show_images, // boolean     show,
					save_images, // boolean     save,
					save_dir, // String      save_dir,
					"_"+indices.length+"_MASK-"+pix_m, // String      suffix
					debugLevel); // int         debugLevel)			
		}

		if (show_frames) {
			int [] wh =     new int[2];
			int [] origin = new int[2];
			double [][] centers = show_centers? (new double [indices.length][]): null;
			double [][] dmulti = renderMultiDouble (
					null,              // double [][]   ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt
					indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
					bounds_to_indices, // boolean       bounds_to_indices,
					null,              // affines,    // double [][][] affines, // null or [indices.length][2][3]
					null,              // double [][]   equalize,
					true,              // boolean       ignore_equalize,
					null,              // warp,       // FineXYCorr    warp,,
					zoom_lev,          // int         zoom_level,
					wh,                // int []      wh,
					origin,            // int []      origin){ // maps[0] as a reference
					centers);          // double [][] centers)
			if (margins > 0) {
				addMargins(
						dmulti,   // double [][] dmulti,
						wh,       // int width,
						margins); // int margins)
			}
			double [][] dframes = new double [dmulti.length][];
			for (int n = 0; n < dmulti.length; n++) {
				dframes[n] = createBorderByNaN(
						dmulti[n],       // double [] data,
						wh[0],           // int width,
						frame_shrink,    // int     frame_shrink, 
						frame_grow,      // int     frame_grow,
						frame_blur,      // double  frame_blur,
						frame_cut_frac); // double  cut_frac)
				
//				for (int i = 0; i < dmulti[n].length; i++) {
//					dmulti[n][i] = Double.isNaN(dmulti[n][i])? 0:1;
//				}
			}
			showSaveMap(
					indices, // int []      indices,
					dframes,  // double [][] dmulti,
					wh[0],   // int         width,
					centers, // int    [][] centers,
					merge_layers, // boolean     merge_layers,
					true,   // boolean     use_max,
					show_images, // boolean     show,
					save_images, // boolean     save,
					save_dir, // String      save_dir,
					"_"+indices.length+"_FRAME-"+pix_m, // String      suffix
					debugLevel); // int         debugLevel)			
		}
		
		/*
		show_frames =        gd.getNextBoolean(); 
		frame_shrink = (int) gd.getNextNumber();
		frame_grow =   (int) gd.getNextNumber();
		frame_blur =         gd.getNextNumber();
 
		 */
		//showScenesStats
		//show_combo_mask
		return true;
	}
	public static void addMargins(
			double [][] dmulti,
			int [] wh,
			int margins){
		int width = wh[0];
		int height = dmulti[0].length/width;
		int widthm = width+2*margins;
		int heightm = height + 2* margins;
		int indx0m = (widthm + 1) * margins;
		for (int n = 0; n < dmulti.length; n++) {
			double [] d = new double[widthm*heightm];
			Arrays.fill(d, Double.NaN);
			for (int row = 0; row < height; row++) {
				System.arraycopy(
						dmulti[n],
						row*width,
						d,
						indx0m + widthm * row,
						width);
			}
			dmulti[n] = d;
		}
		wh[0] = widthm;
		wh[1] = heightm;
	}
	
	public static double [] createBorderByNaN(
			double [] data,
			int width,
			int     frame_shrink, 
			int     frame_grow,
			double  frame_blur,
			double  cut_frac) {
		double [] dframe =new double [data.length];
		int height = data.length/width;
		final TileNeibs tnSurface = new TileNeibs(width, height);
        boolean [] mask_in = new boolean[data.length];
        for (int i = 0; i < data.length; i++) {
        	mask_in[i] = !Double.isNaN(data[i]);
        }
		boolean [] mask_out = mask_in.clone();
		tnSurface.shrinkSelection(
				frame_shrink, // shrink,
				mask_in,      // tiles,
				null);        // prohibit);
		tnSurface.growSelection(
				frame_grow,   // grow,
				mask_out,     // tiles,
				null);        // prohibit);
		if (frame_blur > 0) {
			for (int i = 0; i < dframe.length; i++) {
				dframe[i] = (mask_out[i] && !mask_in[i]) ? 1.0 : 0.0;
			}
//			DoubleGaussianBlur gb = new DoubleGaussianBlur();
			(new DoubleGaussianBlur()).blurDouble(
					dframe,
					width,
					height,
					frame_blur,
					frame_blur,
					0.01);
			double max = 0.0;
			for (int i = 0; i < dframe.length; i++) {
				max = Math.max(max, dframe[i]);
			}
			double threshold = max*cut_frac;
			for (int i = 0; i < dframe.length; i++) {
				if (dframe[i] < threshold) {
					dframe[i] = Double.NaN;
				}
			}
		} else {
			for (int i = 0; i < dframe.length; i++) {
				dframe[i] = (mask_out[i] && !mask_in[i]) ? 1.0 : Double.NaN;
			}
		}

		
		return dframe;
		
	}
	
	public void showSaveMap(
			int []      indices,
			double [][] dmulti,
			int         width,
			double [][] centers,
			boolean     merge_layers,
			boolean     use_max,
			boolean     show,
			boolean     save,
			String      save_dir,
			String      suffix, // "_"+indices.length+"_MAP"
			int         debugLevel) {
		boolean     show_centers = centers != null;
		if (merge_layers) {
			dmulti = new double [][] {mergeLayers(dmulti,use_max)};
		}
		String [] titles = new String[dmulti.length];
		for (int i = 0; i < titles.length; i++) {
			titles[i] = String.format("%03d-%s", indices[i], ortho_maps[indices[i]].getName());
		}
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				dmulti,
				width,
				dmulti[0].length/width,
				ortho_maps[indices[0]].getName()+(merge_layers? "_MERGED":"")+suffix+".tiff",
				titles);
        if (show_centers) {
        	PointRoi roi = new PointRoi();
        	for (int i = 0; i < centers.length; i++) {
        		roi.addPoint(centers[i][0],centers[i][1], 1+(merge_layers?0:i));
        	}
        	roi.setOptions("label");
        	imp.setRoi(roi);
        }
		showSaveImagePlus(
				imp,        // ImagePlus imp,
				show, // boolean   show,
				save, // boolean   save,
				save_dir, // String    save_dir,
				debugLevel); // int       debugLevel)
		
	}
	
	
	public static double [] mergeLayers(
			double [][] layers,
			boolean use_max) { // false - use last
		if ((layers == null) || (layers.length == 0)){
			return null;
		}
		double [] merged = layers[0].clone();
		for (int n = 1; n < layers.length; n++) {
			if (use_max) {
				for (int i = 0; i < merged.length; i++) if (!Double.isNaN(layers[n][i])) {
					if (Double.isNaN(merged[i])) {
						merged[i] = layers[n][i];
					} else {
						merged[i] = Math.max(layers[n][i],merged[i]);
					}
				}

			} else {
				for (int i = 0; i < merged.length; i++) if (!Double.isNaN(layers[n][i])) {
					merged[i] = layers[n][i];
				}
			}
		}
		return merged;
	}
	
	/*
	public boolean showScenesStats() {
		String time_zone_name = "Europe/Kyiv";
		int [] indices = getScenesSelection(
				
				null, // boolean select_all,
				" to generate stats"); // String purpose)
		if (indices == null) {
			return false;
		}
		String [] stats = getScenesStats(indices, time_zone_name); // int [] indices)
		new TextWindow("Ortho_images_stats", stats[0], stats[1], 1250,1000);
		return true;
	}
	*/
	
	public String[] getScenesStats(int [] indices, String time_zone_name) {
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		boolean y_down_ccw = true;
		String [] stats = new String[2]; // header, body
		StringBuffer sb = new StringBuffer();
//		String head_fmt = "%3s\t%17s\t%26s\t%13s\t%13s\t%7s\t%6s\t%7s\t%6s\t%6s\t%6s\t%6s\n";
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss.SS zzz");// may be VV instead of zzz
		//
		stats [0] = String.format(
				"%3s\t%17s\t%26s\t%11s\t%11s\t%7s\t"+
						"%6s\t%7s\t%6s\t%6s\t%6s\t%6s\t"+
						"%6s\t%6s\t"+
						"%7s\t%7s\t%7s\t%7s\t%7s\t%7s\t"+
						"%7s\t%7s\t%7s\t%7s",
						"#", "timestamp", "date/time", "latitude", "longitude", "GND-ASL",
						"AGL","pix(cm)", "width", "height","vert_x","vert_y",
						"scenes", "sfm",
						"scale","tilt","beta","rot","offs-X","offs-Y",
						"a00","a01","a10","a11");
		for (int indx:indices) {
			OrthoMap map = ortho_maps[indx];
			LocalDateTime ldt = map.getLocalDateTime();
			ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC
			ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of(time_zone_name));
			String sdt = zonedDateTime.format(formatter);
			String name = map.getName();
			double [] lla = map.getLLA();
			double agl = map.getAGL();
			double pix_size_cm =  100*map.getOrigPixMeters();
			int width = map.getWidth();
			int height = map.getHeight();
			int [] vert_pix = map.getVertPixels();
			double sfm_gain = map.getSfmGain();
			int scenes = map.getNumberScenes();
			double [][] affine = map.getAffine();
			SingularValueDecomposition svd_st =SingularValueDecomposition.singularValueDecomposeScaleTiltGamma(
					affine,
					y_down_ccw); // boolean y_down_ccw)
//			double [] svd_st = OrthoMap.singularValueDecomposeScaleTilt(
//					affine,
//					y_down_ccw); // boolean y_down_ccw)
			// svd_st[0] - now gamma - short axis relative to map 
			sb.append(String.format(
					"%3d\t%17s\t%26s\t%11.6f\t%11.6f\t%7.2f\t"+
							"%6.2f\t%7.4f\t%6d\t%6d\t%6d\t%6d\t"+
							"%6d\t%6.2f"+
							"\t%7.4f\t%7.4f\t%7.4f\t%7.4f\t%7.3f\t%7.3f"+
							"\t%7.4f\t%7.4f\t%7.4f\t%7.4f\n",
							indx, name, sdt, lla[0], lla[1], lla[2] - agl,//  ground level
							agl, pix_size_cm, width, height, vert_pix[0], vert_pix[1],
							scenes, sfm_gain,
							svd_st.scale,svd_st.ratio,svd_st.beta,svd_st.rot, affine[0][2], affine[1][2],
							affine[0][0], affine[0][1], affine[1][0], affine[1][1]));
		}
		stats[1] = sb.toString();
		return stats;
	}
	public static void showSaveImagePlus(
			ImagePlus imp,
			boolean   show,
			boolean   save,
			String    save_dir,
			int       debugLevel) {
		if (save && (save_dir != null)) {
			if (!save_dir.endsWith(Prefs.getFileSeparator())) {
				save_dir += Prefs.getFileSeparator();
			}
			String save_path = save_dir+imp.getTitle();
			if (!save_path.endsWith(".tiff")) {
				save_path+=".tiff";
			}
			FileSaver imp_fs = new FileSaver(imp);
			imp_fs.saveAsTiff(save_path);
			if (debugLevel > -4) {
				System.out.println("Saved "+save_path);	
			}
		}
		if (show) {
			imp.show();
		}
	}
	
	public static double [] getFracOverlaps(
			final double [][] dmulti) {
		final double [] overlaps = new double [dmulti.length*dmulti.length];

		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nscene = ai.getAndIncrement(); nscene < (dmulti.length-1); nscene = ai.getAndIncrement()) {
						double [] data0 = dmulti[nscene];
						int num0 = 0;
						for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data0[i])) {
							num0++;
						}

						for (int nscene1 = nscene+1; nscene1 < dmulti.length; nscene1++) {
							double [] data1 = dmulti[nscene1];
							int num1 = 0, num_intersect=0; 
							for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data1[i])) {
								num1++;
								if (!Double.isNaN(data0[i])) {
									num_intersect++;
								}
							}
							overlaps[nscene * dmulti.length  + nscene1] = (1.0 * num_intersect)/ Math.min(num0, num1);
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return overlaps;
	}
	
	public int [][] filterPairs(
			int []   indices_in,
			double   min_overlap_frac,
			double   max_rmse,
			boolean  max_resolution, // require defined and full resolution
			int [][] remove_pairs) { 
		if (indices_in == null) {
			indices_in = new int [ortho_maps.length];
			for (int i = 0; i < indices_in.length; i++) {
				indices_in[i] = i;
			}
		}
		boolean [] used_scenes = new boolean [ortho_maps.length];
		for (int i:indices_in) {
			used_scenes[i] = true;
		}
		HashSet <Point> donotuse_set = new HashSet<Point>();
		if (remove_pairs != null) {
			for (int [] p: remove_pairs) {
				donotuse_set.add(new Point(p[0],p[1]));
			}
		}
		ArrayList<Point> pairs_list = new ArrayList<Point>();
		for (int scene0:indices_in) {
			Set<String> match_names = ortho_maps[scene0].pairwise_matches.keySet();
			ArrayList<String> names_list = new ArrayList<String>();
			names_list.addAll(match_names);
			Collections.sort(names_list);
			for (String scene1_name:names_list) {
				PairwiseOrthoMatch match = ortho_maps[scene0].getMatch(scene1_name, !max_resolution); // may be not defined only
				if (match != null) {
					int scene1 = getIndex(scene1_name);
					if ( used_scenes[scene1] && (!max_resolution || match.isDefined()) &&
							(match.getOverlap() >= min_overlap_frac) &&
							((max_rmse <= 0) || (match.getRMS() <= max_rmse))
							){
						if (max_resolution) {
							int min_zoom = Math.min(ortho_maps[scene0].getOriginalZoomLevel(), ortho_maps[scene1].getOriginalZoomLevel());
							if (match.getZoomLevel() < min_zoom) {
								continue;
							}
						}
						Point p = new Point(scene0, scene1);
						if (remove_pairs != null) {
							if (donotuse_set.contains(p)) {
								continue;
							}
						}
						System.out.println(String.format("%5d: %3d-%3d, %7.4f, %7.4f",
								pairs_list.size(), scene0, scene1, match.getOverlap(), match.getRMS()));
						pairs_list.add(p);
					}
				}
			}
		}
		int [][] pairs = new int [pairs_list.size()][2];
		for (int i = 0; i < pairs.length; i++) {
			Point p = pairs_list.get(i);
			pairs[i][0] = p.x;
			pairs[i][1] = p.y;
		}
		return pairs;
	}
	
	public int [] getScenesFromPairs(
			int [][] pairs,
			int [] indices_in) { // preselected indices or null
		boolean [] used_scenes = new boolean [ortho_maps.length];
		if (indices_in != null) {
			for (int i:indices_in) {
				used_scenes[i] = true;
			}
		}
		for (int [] p:pairs) {
			used_scenes[p[0]] = true;
			used_scenes[p[1]] = true;
		}
		int indx = 0;
		for (int i = 0; i < used_scenes.length; i++) if (used_scenes[i]){
			indx++;	
		}
		int [] indices = new int [indx];
		indx = 0;
		for (int i = 0; i < used_scenes.length; i++) if (used_scenes[i]){
			indices[indx++] = i;	
		}
		return indices;
	}
	/**
	 * Re-index pairs to exclude scenes not present in indices[] 
	 * @param pairs   - array of start,end absolute scene numbers
	 * @param indices - ascending array of used scenes (or null - all scenes)
	 * @return  re-indexed pairs, indices exclude scenes that are not in indices[]
	 */
	public int [][] condensePairs(
			int [][] pairs_all,
			int [] indices) {
		if (indices == null) {
			return pairs_all;
		}
		int [] reindex = new int [ortho_maps.length];
		int [][] pairs = new int [pairs_all.length][2];
		Arrays.fill(reindex, -1);
		for (int i = 0; i < indices.length; i++) {
			reindex[indices[i]] = i;
		}
		for (int i = 0; i < pairs.length; i++) {
			pairs[i][0] = reindex[pairs_all[i][0]];
			pairs[i][1] = reindex[pairs_all[i][1]];
		}
		return pairs;
	}

	/**
	 * Filter list of image pairs by overlap and RMSE
	 * @param list_in     array of image pairs indices
	 * @param undef_only  only keep undefined pairs (no affine transform), ignore RMSE parameters
	 * @param min_overlap minimal pair overlap
	 * @param max_overlap maximal pair overlap
	 * @param min_rms     minimal RMSE 
	 * @param max_rms     maximal RMSE 
	 * @param nan_rms     use only NaN RMSE pairs
	 * @param filt_zoom   filter by zoom
	 * @param min_zoom    minimal zoom (including)
	 * @param max_zoom    maximal zoom (including)
	 * @param min_sfm     minimal SfM gain of the minimum in the scene pair 
	 * @param max_sfm     maximal SfM gain of the minimum in the scene pair 
	 * @param filt_alt    filter by alt data availability (0 - no filter, 1 - with alt only, 2 - without ALT only)
	 * @return            filtered array of pair indices
	 */
	
	public int [][] filterPairs(
			int [][]         list_in,
			boolean          undef_only,
			double           min_overlap,
			double           max_overlap,
			double           min_rms,
			double           max_rms,
			boolean          nan_rms,
			boolean          filt_zoom,
			int              min_zoom,
			int              max_zoom,
			double           min_sfm,
			double           max_sfm,
			int              filt_alt){
		ArrayList<Point> plist = new ArrayList<Point>();
		int num_all = 0;
		int num_def = 0;
		for (int [] ipair: list_in) {
			Point pair = new Point(ipair[0],ipair[1]);

			PairwiseOrthoMatch pairwiseOrthoMatch = ortho_maps[pair.x].getMatch(
					ortho_maps[pair.y].getName(), undef_only || nan_rms);
			double sfm_gain = Math.min(ortho_maps[pair.x].getSfmGain(), ortho_maps[pair.y].getSfmGain());
			if (pairwiseOrthoMatch != null) {
				num_all++;
				boolean defined = pairwiseOrthoMatch.isDefined();
				if (defined) {
					num_def++;
				}
				if (filt_alt != 0) { // has alt data
					if (pairwiseOrthoMatch.alt_data != null) {
						if (filt_alt == 2) {
							continue; // skip if no-ALT only
						}
					} else { // does not have alt data
						if (filt_alt == 1) {
							continue; // skip if ALT only
						}
					}
				}

				if ((sfm_gain < min_sfm) || (sfm_gain > max_sfm)) {
					continue;
				}
				
				if (undef_only) {
					if (!defined) {
						plist.add(pair);
					}
				} else {
					if ((pairwiseOrthoMatch.overlap >= min_overlap) && (pairwiseOrthoMatch.overlap <= max_overlap)) {
						if (nan_rms) {
							if (Double.isNaN(pairwiseOrthoMatch.rms)) {
								plist.add(pair);
							}
						} else if ((pairwiseOrthoMatch.rms >= min_rms) && (pairwiseOrthoMatch.rms <= max_rms)) {
							if (!filt_zoom || 
									((pairwiseOrthoMatch.zoom_lev >= min_zoom) && (pairwiseOrthoMatch.zoom_lev <= max_zoom))) {
								plist.add(pair);
							}
						}
					}
				}
			} else {
//				System.out.println("pair=["+pair.x+","+pair.y+"] "+
//						ortho_maps[pair.x].getName()+"->"+ortho_maps[pair.y].getName());
			}
		}
		System.out.println("num_all = "+num_all+", num_def="+num_def+", remained="+plist.size());
		int [][] pairs = new int [plist.size()][2];
		for (int i = 0; i < pairs.length; i++) {
			pairs[i][0] = plist.get(i).x;
			pairs[i][1] = plist.get(i).y;
		}
		return pairs;
	}
	
	/**
	 * Create text representation of the image pairs for drop-down selection lists
	 * @param plist ArrayList<Point> as pairs of scenes indices
	 * @param show_names   show full scene names (timestamps)
	 * @param show_overlap show scene overlap in percents
	 * @param show_rms     show pairs RMSE
	 * @param show_zoom    show pairs zoom level
	 * @param show_alt     show altitude data
	 * @param extra_line   null or an extra string to be added as a last element
	 * @return Array of    strings to be shown in a drop-down list
	 */
	public String [] textPairs (
			int [][] plist,
			boolean          show_names,
			boolean          show_overlap,
			boolean          show_rms,
			boolean          show_zoom,
			boolean          show_alt,
			boolean          use_tab,
			String           extra_line) {
		return 	textPairs (
				plist, // int [][] plist,
				show_names, // boolean          show_names,
				show_overlap, // boolean          show_overlap,
				show_rms, // boolean          show_rms,
				show_zoom, // boolean          show_zoom,
				show_alt, // boolean          show_alt,
				use_tab, // boolean          use_tab,
				extra_line, // String           extra_line,
				false, // boolean          show_svd,
				false); // boolean          use_degrees);

	}	
	public String [] textPairs (
			int [][] plist,
			boolean          show_names,
			boolean          show_overlap,
			boolean          show_rms,
			boolean          show_zoom,
			boolean          show_alt,
			boolean          use_tab,
			String           extra_line,
			boolean          show_svd,
			boolean          use_degrees) {
		boolean show_sfm = show_alt;

		// TODO: show SfM of each scene
		String [] text_pairs = new String [plist.length+((extra_line !=null)? 1 : 0)];
		for (int i = 0; i < plist.length; i++) {
			int [] pair = plist[i];
			double [] sfm_gain = {ortho_maps[pair[0]].getSfmGain(), ortho_maps[pair[1]].getSfmGain()};
			PairwiseOrthoMatch pairwiseOrthoMatch = ortho_maps[pair[0]].getMatch(
	                ortho_maps[pair[1]].getName(), true); // include undefined
//			if (pairwiseOrthoMatch== null) {
//				System.out.println("pair=["+pair[0]+","+pair[1]+"] "+
//						ortho_maps[pair[0]].getName()+"->"+ortho_maps[pair[1]].getName());
//			}
			text_pairs[i] =  String.format(use_tab?"%3d\t%3d":"%3d -> %3d", pair[0], pair[1]);
			if (show_names)	text_pairs[i] +=  String.format(use_tab?"\t%17s\t%17s":" (%17s -> %17s)", ortho_maps[pair[0]].getName(),ortho_maps[pair[1]].getName());
			if (pairwiseOrthoMatch != null) {
				if (show_overlap) text_pairs[i] +=  String.format(use_tab?"\t%5.1f%%":" %5.1f%%", 100*pairwiseOrthoMatch.overlap);
				if (show_rms) text_pairs[i] +=  String.format(use_tab?"\t%5.3f":" %5.3f", pairwiseOrthoMatch.rms);
				if (show_zoom) text_pairs[i] +=  String.format(use_tab?"\t%4d":" Z%3d", pairwiseOrthoMatch.zoom_lev);
				if (show_alt){
					if (pairwiseOrthoMatch.alt_data != null) {
						text_pairs[i] +=  String.format(use_tab?"\t%7.4f\t%7.4f\t%7.3f":" A %7.4f %7.4f %7.3f",
								pairwiseOrthoMatch.alt_data[0],pairwiseOrthoMatch.alt_data[1],pairwiseOrthoMatch.alt_data[2]); //" ALT";
					} else if (use_tab) {
						text_pairs[i] +=  String.format("\t%7s\t%7s\t%7s","---","---","---");
					}
				}
				if (show_sfm) text_pairs[i] +=  String.format(use_tab?"\t%5.1f\t%5.1f":" S %5.1f %5.1f", sfm_gain[0],sfm_gain[1]);
				if (show_svd && use_tab && (pairwiseOrthoMatch.getAffine()!=null)) { // now only for tab
					SingularValueDecomposition svd = SingularValueDecomposition.singularValueDecompose(pairwiseOrthoMatch.getAffine());
					String ssvd = svd.toString(use_degrees, 1);
					text_pairs[i] +=  "\t"+ssvd;
				}
			}
		}
		if (extra_line !=null) {
			text_pairs[plist.length] = extra_line;
		}
		return text_pairs;
	}
	
	public static double [][] rotateAltCenter(
			final double [] quat_in,
			final double [] xyz_center,
			final double [] alt_in,
			final int       width,
			final double    pix_size_meters,
			final Rectangle owoi, // should not be null - will provide offset and new size
			final int       stride, // >=2, use 4?
			final double    rfz,   // 0.3 w= 1/(x^2+y^2+rfz^2)
			final boolean keep_window,
			final double  scale_tilt,
			final int debugLevel) {
		final double discard_frac = 0.1; // discard interpolation if it is extrapolation more than this 
		final int    len_in = alt_in.length;
		final int    height = len_in/width;
		final double r2fz = rfz*rfz; 
		final double x0=xyz_center[0], y0=xyz_center[1],z0=xyz_center[2]; // xyz_center[2] now in "pixels"
		final double scale = QuatUtils.norm(quat_in);
		System.out.println("scale = "+scale+", NOT USED - set to k=1.0");
		final double k = 1.0;
		final double [] quat = QuatUtils.normalize(quat_in);
		quat[2]*= -1; // y is down
		quat[3]*= -1; // rotation does not match tilts
		
		quat[1]*=scale_tilt;
		quat[2]*=scale_tilt;
		QuatUtils.normalizeInPlace(quat);
		
		final double [][] xyz1 = new double [alt_in.length][];
		final Thread[] threads = ImageDtt.newThreadArray();
		
		final double [][][] xy_mn_mx_thread= new double [threads.length][2][2];
		final double [] pos_neg_inf = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
		final double [][] xy_mn_mx= {pos_neg_inf.clone(),pos_neg_inf.clone()}; 
		for (int nthread=0; nthread <  threads.length; nthread++) {
			xy_mn_mx_thread[nthread] = new double [][] {pos_neg_inf.clone(),pos_neg_inf.clone()};
		}
//		Arrays.fill(xy_mn_mx_thread,xy_mn_mx);
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                	int nthread = ati.getAndIncrement();
                	double [] xyz = new double[3];
                	double [] xyz_out = new double[3];
                    for (int ipix = ai.getAndIncrement(); ipix < alt_in.length; ipix = ai.getAndIncrement()) if (!Double.isNaN(alt_in[ipix])){
                    	xyz[0] = ipix % width - x0;
                    	xyz[1] = ipix/ width -  y0;
                    	xyz[2] = alt_in[ipix]/ pix_size_meters - z0 ; // meters -> pix
                    	QuatUtils.applyTo(
                    			k,     // final double    k, // scale
                    			quat,  // final double [] q,
                    			xyz,   // final double [] in,
                    			xyz_out); // final double [] out)
                    	xyz1[ipix]= new double[3];
                    	xyz1[ipix][0]=xyz_out[0]+x0;
                    	xyz1[ipix][1]=xyz_out[1]+y0;
                    	xyz1[ipix][2]=xyz_out[2]+z0;
                    	xy_mn_mx_thread[nthread][0][0] = Math.min(xy_mn_mx_thread[nthread][0][0], xyz1[ipix][0]);
                    	xy_mn_mx_thread[nthread][0][1] = Math.max(xy_mn_mx_thread[nthread][0][1], xyz1[ipix][0]);
                    	xy_mn_mx_thread[nthread][1][0] = Math.min(xy_mn_mx_thread[nthread][1][0], xyz1[ipix][1]);
                    	xy_mn_mx_thread[nthread][1][1] = Math.max(xy_mn_mx_thread[nthread][1][1], xyz1[ipix][1]);
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		for (int i = 0; i < xy_mn_mx_thread.length; i++) {
			xy_mn_mx[0][0]=Math.min(xy_mn_mx[0][0], xy_mn_mx_thread[i][0][0]);
			xy_mn_mx[0][1]=Math.max(xy_mn_mx[0][1], xy_mn_mx_thread[i][0][1]);
			xy_mn_mx[1][0]=Math.min(xy_mn_mx[1][0], xy_mn_mx_thread[i][1][0]);
			xy_mn_mx[1][1]=Math.max(xy_mn_mx[1][1], xy_mn_mx_thread[i][1][1]);
		}
		int x_min = keep_window ? 0:      ((int) Math.floor(xy_mn_mx[0][0])-1); 
		int y_min = keep_window ? 0:      ((int) Math.floor(xy_mn_mx[1][0])-1); 
		int x_max = keep_window ? width:  ((int) Math.ceil(xy_mn_mx[0][1]) +1); 
		int y_max = keep_window ? height: ((int) Math.ceil(xy_mn_mx[1][1]) +1);
		//final Rectangle 
			owoi.x = x_min;
			owoi.y = y_min;
			owoi.width= x_max-x_min;
			owoi.height=y_max-y_min;
		final int olen = owoi.width*owoi.height;
		final double [][] alt_out = new double [olen][]; // from rectified to alt_in, each element - {x,y,z}
		
		final double [] xyz_center_out = {xyz_center[0]-x_min, xyz_center[1]-y_min}; // do outside by caller - new rotation center
		// shift x,y in xyz1 to match new window
		ai.set(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int ipix = ai.getAndIncrement(); ipix < alt_in.length; ipix = ai.getAndIncrement()) if (xyz1[ipix] != null){
                    	xyz1[ipix][0] -= owoi.x;
                    	xyz1[ipix][1] -= owoi.y;
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);

		final double [] weights = new double [owoi.width*owoi.height]; // when mixing same result from multiple sources
		final int stride_w = (width -1) / stride + 1;
		final int stride_h = (height -1) / stride + 1;
		final int stride_l = stride_w * stride_h;
		
		final int [] neib_oindices = { 0,  1, owoi.width, owoi.width+1}; // output indices, Z-order 
		final int [] neib_in_indices = { 0,  1, width, width+1}; // input indices, Z-order 
		final int [][] neib2_indices = { // Z-shape
				{-width-1, -width,        -1,       0}, // top-left (each also in Z-order)
				{-width,   -width+1,       0,       1}, // top-right (each also in Z-order)
				{      -1,        0, width-1, width  }, // bottom-left (each also in Z-order)
				{       0,        1, width,   width+1}  // bottom-right (each also in Z-order)
		};
		for (int offs_y = 0; offs_y < stride; offs_y++) {
			for (int offs_x = 0; offs_x < stride; offs_x++) {
				final int foffs_y = offs_y, foffs_x=offs_x;
				ai.set(0);
		        for (int ithread = 0; ithread < threads.length; ithread++) {
		            threads[ithread] = new Thread() {
		                public void run() {
		                	double [] vh = new double[2], vv=new double[2], v=new double[2], o=new double[2];
		                	double [] xyz = new double[3];
            				double [][] oxyz4 =  new double[4][];
            				double [][] ixyz4 =  new double[4][3];
		                    for (int ipix = ai.getAndIncrement(); ipix < stride_l; ipix = ai.getAndIncrement()) {
		                    	int pix_y = (ipix/stride_w)*stride + foffs_y; 
		                    	int pix_x = (ipix%stride_w)*stride + foffs_x;
		                    	if ((pix_x < width) && (pix_y < height)) {
		                    		int pix = pix_y * width + pix_x;
		                    		double [] xyz1p = xyz1[pix];
//		                    		int x1 = (int) xyz1p[0];
//		                    		int y1 = (int) xyz1p[1];
		                    		
		                    		if (xyz1p != null) {
			                    		int x1 = (int) Math.floor(xyz1p[0]);
			                    		int y1 = (int) Math.floor(xyz1p[1]);
			                    		int x2 = (int) Math.ceil(xyz1p[0]);
			                    		int y2 = (int) Math.ceil(xyz1p[1]);
		                    			// accumulate 4 corners around the fractional
		                    			int [][] xy_dir= {{x1,y1},{x2,y1},{x1,y2},{x2,y2}};
		                    			for (int dir = 0; dir < 4; dir++) {
		                    				int [] indices_in =  neib2_indices[dir];  // input indices (presumably) around output
		                    				// make sure all defined and surround an output node
		                    				boolean defined4 = true;
//		                    				double [][] oxyz4 =  new double[4][];
//		                    				double [][] ixyz4 =  new double[4][];
		                    				for (int i = 0; i < 4; i++) {
		                    					int indx= pix+indices_in[i];
		                    					if ((indx < 0) || (indx >= len_in) || (xyz1[indx] == null)) {
		                    						defined4 = false;
		                    						break;
		                    					}
		                    					oxyz4[i] = xyz1[indx];
		                    					ixyz4[i][0]= indx%width;
		                    					ixyz4[i][1]= indx/width;
		                    					ixyz4[i][2]= xyz1[indx][2]; // Z same as in in oxyz4
		                    				}
		                    				if (!defined4) {
		                    					continue;
		                    				}
		                    				int oindx = neib_oindices[dir]+ (y1*owoi.width + x1);
		                    				if ((oindx < 0) || (oindx >= olen)) {
		                    					continue;
		                    				}
		                    				o[0] = oindx % owoi.width; // output integer coordinates that should be inside oxyz4
		                    				o[1] = oindx / owoi.width;
		                    				// average horizontal and vertical vectors
		                    				for (int i = 0; i < 2; i++) {
		                    					vh[i] = (oxyz4[1][i]-oxyz4[0][i]+oxyz4[3][i]-oxyz4[2][i])/2;
		                    					vv[i] = (oxyz4[2][i]-oxyz4[0][i]+oxyz4[3][i]-oxyz4[1][i])/2;
		                    					v[i]= o[i] - oxyz4[0][i]; 
		                    				}
		                    				// find ax, ay for bilinear interpolation
		                    				double lvh = Math.sqrt(vh[0]*vh[0]+vh[1]*vh[1]);
		                    				double lvv = Math.sqrt(vv[0]*vv[0]+vv[1]*vv[1]);
		                    				double lv =  Math.sqrt( v[0]* v[0]+ v[1]* v[1]);
		                    				double ax = (v[0]*vh[0]+v[1]*vh[1])/lv/lvh;
		                    				double ay = (v[0]*vv[0]+v[1]*vv[1])/lv/lvv;
		                    				// discard interpolation if it is extrapolation by more than discard_frac
		                    				if ((ax < -discard_frac) || (ay < -discard_frac) || (ax > 1 + discard_frac) || (ax > 1 + discard_frac)) {
		                    					continue;
		                    				}
		                    				//xyz = new double[3];
		                    				for (int i = 0; i <3; i++) {
		                    					xyz[i] = (1-ax)*(1-ay)*ixyz4[0][i]+
		                    							 (  ax)*(1-ay)*ixyz4[1][i]+
		                    							 (1-ax)*(  ay)*ixyz4[2][i]+
		                    							 (  ax)*(  ay)*ixyz4[3][i];
		                    				}
		                    				xyz[2] *=  pix_size_meters; // pix-> meters
		                    				
		                    				double dax = (ax > 0.5) ? (1-ax) : ax;
		                    				double day = (ay > 0.5) ? (1-ay) : ay;
		                    				double w =r2fz / (dax*dax+day*day+r2fz);

		                    				//weights[]
		                    				if (alt_out[oindx] == null) {
		                    					alt_out[oindx] = xyz.clone();
		                    					weights[oindx] = w;
		                    				} else {
		                    					double w_new = (weights[oindx]+ w);
		                    					double w0 = weights[oindx]/w_new;
		                    					double w1 = w /w_new;
		                    					for (int i = 0; i < xyz.length; i++) {
		                    						alt_out[oindx][i] = w0 * alt_out[oindx][i] + w1 * xyz[i];
		                    					}
		                    					weights[oindx] = w_new;
		                    				}
		                    			}
		                    		}
		                    	}
		                    }
		                }
		            };
		        }		      
		        ImageDtt.startAndJoin(threads);
			}
		}
		return  alt_out;
	}
	
	public static double [] applySceneRotation(
			final double []   texture_in,
			final int         width_in,
			final double [][] rotation_map,
//			final int         width_out,
			final int debugLevel) {
		final int    height_in = texture_in.length/width_in;
		final double [] texture_out = new double[rotation_map.length];
		Arrays.fill(texture_out, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int opix = ai.getAndIncrement(); opix < rotation_map.length; opix = ai.getAndIncrement()) if (rotation_map[opix] != null){
//                    	int x = opix % width_out;
//                   	int y = opix / width_out;
                    	double [] xyz = rotation_map[opix];
                    	int ipx0 = (int) Math.floor(xyz[0]);
                    	int ipy0 = (int) Math.floor(xyz[1]);
                    	if ((ipx0 >= width_in) || (ipy0 >= height_in)) {
                    		continue;
                    	}
                    	int ipx1 = ipx0 + 1;
                    	if (ipx1 >= width_in) ipx1 = width_in-1; 
                    	int ipy1 = ipy0 + 1;
                    	if (ipy1 >= height_in) ipy1 = height_in-1;
                    	if ((ipx1 < 0) || (ipy1 < 0)) {
                    		continue;
                    	}
                    	if (ipx0 < 0) ipx0 = ipx1;
                    	if (ipy0 < 0) ipy0 = ipy1;
                    	
                    	
                    	double fx = xyz[0] - ipx0;
                    	double fy = xyz[1] - ipy0;
                    	texture_out[opix] =
                    			(1-fx) * (1-fy) * texture_in[ipy0 * width_in + ipx0] +
                    			(  fx) * (1-fy) * texture_in[ipy0 * width_in + ipx1] +
                    			(1-fx) * (  fy) * texture_in[ipy1 * width_in + ipx0] +
                    			(  fx) * (  fy) * texture_in[ipy1 * width_in + ipx1];
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		return texture_out;
	}

	public static double [] getRotatedAlt(
			final double [][] rotation_map) {
		final double [] alt_out = new double[rotation_map.length];
		Arrays.fill(alt_out, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int opix = ai.getAndIncrement(); opix < rotation_map.length; opix = ai.getAndIncrement()) if (rotation_map[opix] != null){
                    	alt_out[opix] = rotation_map[opix][2];
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		return alt_out;
		
	}
	
	public static double [] debugPadInToOut(
			final double []   texture_in,
			final int         width_in,
			final Rectangle   woi) {
		final int height_in = texture_in.length/width_in;
		final double [] texture_out = new double[woi.width*woi.height];
		Arrays.fill(texture_out, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int opix = ai.getAndIncrement(); opix < texture_out.length; opix = ai.getAndIncrement()) {
                    	int x_out = opix % woi.width;
                    	int y_out = opix / woi.width;
                    	int x_in = x_out+woi.x;
                    	int y_in = y_out+woi.y;
                    	if ((x_in >=0) && (y_in >= 0) && (x_in < width_in) && (y_in < height_in)) {
                    		texture_out[opix] = texture_in[y_in * width_in + x_in];	
                    	}
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
        return texture_out;
		
	}
	

}
