/**
 **
 ** ComboMatch.java - Multi-image rectilinear feature matching for orthomosaic assembly
 **
 ** Copyright (C) 2024-2025 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  ComboMatch.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.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
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.Properties;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.calibration.CalibrationFileManagement;
import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.cameras.ColorProcParameters;
import com.elphel.imagej.cameras.EyesisCorrectionParameters;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.ims.Imx5;
import com.elphel.imagej.readers.ElphelTiffReader;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TDCorrTile;

import Jama.Matrix;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.PointRoi;
import ij.process.FloatPolygon;

public class ComboMatch {
	public static final String [] SUFFIXES_BL_BC=     {"","-BL","-BC"};
	public static GPUTileProcessor GPU_TILE_PROCESSOR = null;
	public static GpuQuad   GPU_QUAD_AFFINE = null;
	public static ImagePlus imp_src1 = null;
	public static ImagePlus imp_src2 = null;
	public static ImagePlus [] imp_src; //  = new ImagePlus[2];  
	public static ImagePlus [] imp_alt; //  = new ImagePlus[2];  
	public static Properties [] imp_prop; //  = new Properties[2];
	public static int gpu_max_width=  4096;
	public static int gpu_max_height= 4096;
	public static String [] FILES_LISTS_PATHS = {
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/nov3_50-75-orange",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/nov3_50-75",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_sep12-13_50-25-50-75-100m",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_19_sep13_25-50-75-100m",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_nov3_50-75",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_sep12-50m",
			"/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_sep12-13_50-25-50-75-100m-SUBSET"};
	
	
	public static boolean openTestPairGps(
			 CLTParameters    clt_parameters,
			 GPUTileProcessor gpu_tile_processor, // initialized by the caller
			 boolean          dflt_options,
			 boolean          extract_objects,
			 boolean          manual_pair,
			 int              debugLevel) {
		boolean create_kernels = debugLevel>1000;
		if (create_kernels) {
			OrthoMap.combineKernels();
			return true;
		}
		GPU_TILE_PROCESSOR = gpu_tile_processor;
		PairwiseOrthoMatch pairwiseOrthoMatch = null;
		String [] pair_names = new String[2];
		ImagePlus imp_sel = manual_pair? WindowManager.getCurrentImage() : null;
		String [] gpu_spair = null;
		boolean use_marked_image =    false; // will be set if found
		String [] all_scenes = null;
		ArrayList<ObjectLocation> object_list = null;
		int zool_lev_objects = 0;
		if (extract_objects) {
			if (imp_sel == null) {
				System.out.println("Need multi-sliced image with marked objects as points");
				return false;
			} 
			int [] zoom_lev_objectsp = new int [1];
			object_list = getObjectsFromSelectedImage(
					imp_sel,
					zoom_lev_objectsp); // ImagePlus imp)
			zool_lev_objects = zoom_lev_objectsp[0];
		} else {
			if (imp_sel != null) {
				pair_names = new String[2];
				String [][] all_scenes1=new String[1][];
				pairwiseOrthoMatch = getPairFromSelectedImage(
						imp_sel, // ImagePlus imp,
						pair_names, // String [] names)
						all_scenes1); // String [][] all_scenes
				if (pairwiseOrthoMatch != null) {
					gpu_spair = pair_names;
					use_marked_image = true;
					all_scenes = all_scenes1[0];
				}
			}
		}
// find -L /media/elphel/SSD3-4GB/lwir16-proc/berdich3/linked/linked_1697875868-1697879449-b/ -type f -name "*-GCORR-GEO.tiff" | sort  > GCORR-GEO.list
// find -L . -path "*-BC-FLAT_CLN-VERT-GCORR-GEO.tiff" -newermt "2024-06-01" -print > GCORR-GEO.list
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_03_short.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/ortho_maps_collection.data";

//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_04_short_20min.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_04_short_20min.data";
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_05_short_27.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_05_short_27.data";
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_07_short_27.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_07_short_27.data";
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_08_november.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_08_november.data";
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_10_short.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_10_short.data";
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_19_sep13_25-50-75-100m.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_19_sep13_25-50-75-100m.data";
//		String [] files_lists_paths = FILES_LISTS_PATHS;
		
//		int default_list_choice = 2; // files_lists_paths.length-1;
//		String files_list_path =          "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_nov3_50-75.list";
//		String orthoMapsCollection_path = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/maps_nov3_50-75.data";
//maps_nov3_50-75		
//maps_19_sep13.list		
		//maps_09_short.list
//maps_08_november.list		
//		int [] gpu_ipair = {1,2};
//		int [] gpu_ipair = {4,9}; //-> {4,24}
//		int [] gpu_ipair = {4,18};
		/*
		if (gpu_spair == null) {
//		String [] gpu_spair = {"1697877420_556997", "1697877527_942766"}; // 1697877564_888405"}; //58, 220 
//		String [] gpu_spair = {"1697877419_940125", "1697877527_942766"}; // 1697877564_888405"}; //58, 220 
//		String [] gpu_spair = {"1697877412_004148", "1697877522_274211"}; // 1697877564_888405"}; //58, 220 
//		String [] gpu_spair = {"1697877409_353265", "1697877518_773045"};
//		String [] gpu_spair = {"1697877465_672024", "1697877563_587972"};
//		String [] gpu_spair = {"1697877465_672024", "1697878995_797332"};
//		String [] gpu_spair = {"1697877465_672024", "1697877528_776377"};
//		String [] gpu_spair = {"1694564245_111645", "1694564701_230240"}; // Morning Sep, 13
//		String [] gpu_spair = {"1694564245_111645", "1694564822_637346"}; // Morning Sep, 13
//		String [] gpu_spair = {"1694564248_145989", "1694564819_336247"}; // Morning Sep, 13
//		String [] gpu_spair = {"1694564269_536448", "1694564822_637346"}; // Morning Sep, 13 50m, 75m
//		String [] gpu_spair = {"1694564269_536448", "1694565541_040640"};  // Morning Sep, 13 50m, 100m
//		String [] gpu_spair = {"1694564822_637346", "1694565541_040640"};  // Morning Sep, 13 75m, 100m
//		String [] gpu_spair = {"1694564263_701171", "1694564816_468625"};  // Morning Sep, 13 50m, 75m
//		String [] gpu_spair = {"1694564266_568793", "1694564819_336247"};  // Morning Sep, 13 50m, 75m
//		String [] gpu_spair = {"1694564270_086631", "1694564822_637346"};  // Morning Sep, 13 50m, 75m
//		String [] gpu_spair = {"1694564272_770858", "1694564778_589341"};  // Morning Sep, 13 50m, 75m #13-#63
//		String [] gpu_spair = {"1694564275_538447", "1694564744_411290"};  // Morning Sep, 13 50m, 75m #14-#58
//			gpu_spair = new String[]{"1694564270_086631", "1694563011_045597"};  // Morning Sep, 13 50m, 25m #88-#68
			gpu_spair = new String[]{"1694564291_293695","1694564778_589341"};  // Morning Sep, 13 50m, 75m #97, #139
		}
		*/		
//		double [][][] image_enuatr = {{{0,0,0},{0,0,0}},{{0,0,0},{0,0,0}}};
		int gpu_width=  clt_parameters.imp.rln_gpu_width; // 3008;
		int gpu_height= clt_parameters.imp.rln_gpu_height; // 3008;
		int zoom_lev = -3; // 0; // +1 - zoom in twice, -1 - zoom out twice
		boolean show_combo =               false; // true;
		boolean create_pairwise_matches =  false;
		boolean create_pairwise_affines =  false;
		boolean augment_pairwise_affines = false; // needs to repeat create_pairwise_matches to update overlaps
		boolean equalize_overlaps =        false;
		boolean altitude_match_pairs =     false;
		boolean display_pairs =            false;
		boolean create_map =               false;
		boolean create_equalize =          false;
		boolean use_saved_collection = true; // false;
		boolean save_collection =      false; // true;
		boolean process_correlation = true; // use false to save new version of data
		int     num_tries_fit =       10;
		boolean update_match =        true; // use false to save new version of data
		boolean render_match =        false; // true;
		boolean test_multi_lma =      false;
		boolean pattern_match =       !dflt_options; //true; // false;

		boolean bounds_to_indices =   true;
		int     temp_mode =           0;
		boolean restore_temp =        true;
		double  frac_remove  =        clt_parameters.imp.pmtch_frac_remove; //   0.15;
		double  metric_error =        clt_parameters.imp.pmtch_metric_err; // 0.05; // 0.02;//  2 cm
		
		boolean import_export =       false;
//		boolean update_files =        false;
//		boolean update_lla =          false; // re-read file metadata
//		boolean update_kernel_patterns = false;
//		boolean update_bl_bc =        false;
//		boolean export_affine =       false; // export per-scene affines
//		boolean export_affine2 =      false; // export per-pair affines


		boolean fix_duplicates =      false;
		boolean log_append =           clt_parameters.imp.pwise_log_append;
		String log_path =              clt_parameters.imp.pwise_log_path;
		String omtch_img_set =         clt_parameters.imp.omtch_img_set;
		
//		boolean READ_NO_ALT =          false; // to read older format data file where pairs do not have alt_data[]
		
//		int  suffix_bc_bl_indx =   2;
		if (!use_marked_image) {
			process_correlation=false; // use already adjusted by default
		}
		if (process_correlation) {
			render_match = true; // default
			pattern_match = false;
		}
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,900);
		gd.addChoice      ("Files list/data path (w/o extension):", FILES_LISTS_PATHS, omtch_img_set);
		gd.addCheckbox    ("Use saved maps collection ", use_saved_collection, " (if available). If false - use files list.");
		gd.addCheckbox    ("Save maps collection", save_collection, "Save maps collection to be able to restore.");
		gd.addCheckbox    ("Process correlations", process_correlation, "false to skip to just regenerate new save file.");
		gd.addNumericField("Num tries fit",        num_tries_fit,  0,4,"", "Try matching images this number of times");

		gd.addCheckbox    ("Update match if calculated", update_match, "Will update correlation match for a pair if found.");
		gd.addCheckbox    ("Render match", render_match, "Render a pair of matched images.");
		gd.addCheckbox    ("Test multi LMA", test_multi_lma, "Temporary debug.");
		//
		gd.addCheckbox    ("Pattern match", pattern_match, "Search for patterns for both images in a pair, first is primary.");
		
		gd.addCheckbox    ("Bounds to selected images", bounds_to_indices, "Set combo image bounds to selected images only. False - all images.");
		gd.addNumericField("Temp mode",          temp_mode,  0,4,"",
				"O - do not modify average pixels, 1 - equalize second image to first, 2 - try to account for raw average");
		gd.addNumericField("Zoom level",              zoom_lev,  0,4,"",
				"Zoom level: +1 - zoom in twice, -1 - zoom out twice");
		gd.addNumericField("GPU image width",              gpu_width,  0,4,"",
				"GPU image width");
		gd.addNumericField("GPU image height",             gpu_height,  0,4,"",
				"GPU image height");
		gd.addCheckbox    ("Show combo maps/stats",            show_combo, "Generate/save combo maps and stats.");
		gd.addCheckbox    ("Create overlap pairs",             create_pairwise_matches, "Create scene pairs overlaps.");
		gd.addCheckbox    ("Create pairwise affines",          create_pairwise_affines, "Create affines for scene pairs.");
		gd.addCheckbox    ("Augment pairwise affines",         augment_pairwise_affines, "Augment pairwise affines after building initial map and re-running create_pairwise_matches.");
		gd.addCheckbox    ("Equalize overlap pairs",           equalize_overlaps, "Equalize intensities in overlaps.");
		gd.addCheckbox    ("Altitude match pairs",             altitude_match_pairs, "Pairwise match scenes altitude.");
		gd.addCheckbox    ("Display pairs",                    display_pairs, "Display pairwise match data.");
		gd.addCheckbox    ("Create map",                       create_map, "Create combined map from pairwise matches.");
		gd.addCheckbox    ("Equalize intensities",             create_equalize, "Create map intensities equalization from pairwise matches.");
		gd.addNumericField("Remove fraction of worst matches",   frac_remove,  3,7,"",	"When fitting scenes remove this fraction of worst match.");
		gd.addNumericField("Maximal metric error",      metric_error,  3,7,"m",	"Maximal tolerable fitting error caused by elevation variations.");
		if (use_marked_image ) {
			gd.addCheckbox    ("Use marked image data",   true, "Use markes from the selected image");
		}
		gd.addCheckbox    ("Import/Export operations",  import_export, "Import/export and update operations");
//		gd.addCheckbox    ("Update scene files",        update_files, "Re-read files as specified in .list file, possibly changing versions");
//		gd.addCheckbox    ("Update files metadata",     update_lla, "Re-read files metadata (if it was modified)");
//		gd.addCheckbox    ("Update kernels/patterns",   update_kernel_patterns, "Re-read kernels and patterns from *.list file");
		gd.addCheckbox    ("Remove duplicate scenes",   fix_duplicates, "Remove scenes with the same timestamp");

//		gd.addCheckbox    ("Update BC/BL suffix",       update_bl_bc,
//				"Change source filenames to use -BC for bicubic, -BL - for bilinear, or empty - for old bilinear files");
//		gd.addChoice("BL/BC suffix:",        
//				SUFFIXES_BL_BC,
//				SUFFIXES_BL_BC[suffix_bc_bl_indx],
//				"Select interpolation mode of the source files: old bilinear (empty), bilinear (-BL), or bicubic (-BC)", 0);

//		gd.addCheckbox    ("Export scene affines",      export_affine,  "Export per-scene affines in text format for migration.");
//		gd.addCheckbox    ("Export pairs affines",      export_affine2, "Export per-pair  affines in text format for migration.");

		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.addCheckbox    ("Read no-alt data (old)",    READ_NO_ALT, "Read older format data file where pairs do not have alt_data[].");



		//		update_kernel_patterns
		gd.showDialog();
		if (gd.wasCanceled()) return false;


		omtch_img_set = ComboMatch.FILES_LISTS_PATHS[gd.getNextChoiceIndex()];
		String files_list_path =         omtch_img_set+".list";
		String orthoMapsCollection_path =omtch_img_set+".data";
		String affines_path =            omtch_img_set+".affines"; // for export per-scene affines
		String affines2_path =           omtch_img_set+".affines2"; // for export per-pair affines
		
		use_saved_collection =     gd.getNextBoolean();
		save_collection =          gd.getNextBoolean();
		String orthoMapsCollection_savepath = save_collection?orthoMapsCollection_path:null;
		process_correlation=       gd.getNextBoolean();
		num_tries_fit =      (int) gd.getNextNumber();
		update_match=              gd.getNextBoolean();
		render_match=              gd.getNextBoolean();
		test_multi_lma=            gd.getNextBoolean();
		pattern_match=             gd.getNextBoolean();
		
		bounds_to_indices=         gd.getNextBoolean();
		temp_mode =          (int) gd.getNextNumber();
		zoom_lev =            (int) gd.getNextNumber();
		gpu_width =           (int) gd.getNextNumber();
		gpu_height =          (int) gd.getNextNumber();

		OrthoMap.setGPUWidthHeight(gpu_width,gpu_height);
		
		show_combo =                gd.getNextBoolean();
		create_pairwise_matches =   gd.getNextBoolean();
		create_pairwise_affines =   gd.getNextBoolean();
		augment_pairwise_affines =  gd.getNextBoolean();
		equalize_overlaps =         gd.getNextBoolean();
		altitude_match_pairs =      gd.getNextBoolean();
		display_pairs =             gd.getNextBoolean();
		create_map =                gd.getNextBoolean();
		create_equalize =           gd.getNextBoolean();
		frac_remove =               gd.getNextNumber();
		metric_error=               gd.getNextNumber();
		if (use_marked_image ) { // will only be used if found and asked
			use_marked_image=       gd.getNextBoolean();
		}
		import_export =             gd.getNextBoolean();
//		update_files=               gd.getNextBoolean();
//		update_lla=                 gd.getNextBoolean();
//		update_kernel_patterns=     gd.getNextBoolean();
		fix_duplicates=             gd.getNextBoolean();
//		update_bl_bc=               gd.getNextBoolean();
//		suffix_bc_bl_indx =         gd.getNextChoiceIndex();

//		export_affine =             gd.getNextBoolean();
//		export_affine2 =            gd.getNextBoolean();


		log_append =                gd.getNextBoolean();
		log_path =                  gd.getNextString();
//              READ_NO_ALT =               gd.getNextBoolean();
//		PairwiseOrthoMatch.READ_NO_ALT = READ_NO_ALT; // modifies PairwiseOrthoMatch.readObject() behavior
		OrthoMapsCollection maps_collection=null;
		boolean use_files_list = false;
		if (use_saved_collection) {
			try {
				maps_collection = OrthoMapsCollection.readOrthoMapsCollection (orthoMapsCollection_path);
			} catch (ClassNotFoundException | IOException e) {
				use_files_list = true;
				System.out.println("Saved data not found (or is corrupt): "+orthoMapsCollection_path+", building from a list "+files_list_path);
			}
			//files_list_path
			if (!use_files_list) {
//				if (update_kernel_patterns) {
//					maps_collection.updateKernels(files_list_path);
//					maps_collection.updatePatterns(files_list_path);
//				}
//				if (update_files) {
//					maps_collection.updateFiles(files_list_path);
//				}
			}
		} else {
			use_files_list = true;
		}
		if (use_files_list) { // using files list
			maps_collection = new OrthoMapsCollection(files_list_path, null); // should have ".list" extension
			maps_collection.updateNumberScenes();
			maps_collection.updateSfmGain();
		}
		
		if (import_export) {
			boolean OK = updateImportExport(
					omtch_img_set, // String files_base
					maps_collection); //						OrthoMapsCollection maps_collection
			if (!OK) {
				return false;
			}
		}
		
		
		/*
		if (export_affine) {
			StringBuffer sb = new StringBuffer();
			for (OrthoMap omap :  maps_collection.ortho_maps) {
				String name = omap.getName();
				double [][] affine = omap.getAffine();
				if (affine != null) {
					sb.append(String.format("AFFINE %s %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f\n",
							name, affine[0][0], affine[0][1], affine[0][2], affine[1][0], affine[1][1], affine[1][2]));
				}
			}
			CalibrationFileManagement.saveStringToFile (
					affines_path,          //String path,
					sb.toString(), // data,
					false); // boolean append)
		}

		if (export_affine2) {
			StringBuffer sb = new StringBuffer();
			ArrayList<Point> pairs_list = new ArrayList<Point>(); 
			for (OrthoMap map : maps_collection.ortho_maps) {
				for (String other_name: map.pairwise_matches.keySet()) {
					pairs_list.add(new Point(
							maps_collection.getIndex(map.getName()),
							maps_collection.getIndex(other_name)));
				}
			}
			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
				}
			});
			for (Point pair: pairs_list) {
				PairwiseOrthoMatch pom = maps_collection.ortho_maps[pair.x].getMatch(
						maps_collection.ortho_maps[pair.y].getName(), false); // undef_only || nan_rms);
						if ((pom !=null) && pom.isDefined()) {
							double overlap = pom.overlap;
							double [][] affine = pom.getAffine();
							sb.append(String.format("AFFINE2 %s %s %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f\n",
									maps_collection.ortho_maps[pair.x].getName(),
									maps_collection.ortho_maps[pair.y].getName(),
									overlap,
									affine[0][0], affine[0][1], affine[0][2], affine[1][0], affine[1][1], affine[1][2]));
						}
			}
			CalibrationFileManagement.saveStringToFile (
					affines2_path,          //String path,
					sb.toString(), // data,
					false); // boolean append)
		}

		if (export_affine || export_affine2) {
			return true; // do not save
		}
*/

		if (fix_duplicates) {
			removeDuplicateScenes (maps_collection);
		}

/*		
		if (update_bl_bc) {
			boolean OK = updateBlBcFileNames(
					SUFFIXES_BL_BC[suffix_bc_bl_indx], // String suffix,
					"-FLAT",// String before,
					maps_collection); // OrthoMapsCollection maps_collection)
			if (!OK) {
				System.out.println("Failed to update filenames for different interpolation mode");
			}
		}
		*/
		
		
		String [] names = maps_collection.getNames(); // null
		if (object_list != null) {
			int       corr_size =                128;
			int       extr_size =     200; // 256;
			int       zoomout =         2; //  1; //2; // 1; 2 for simulated,1 - for extracted 
			int       patt_choice =     3;
			double    phaseCoeff =      0.98;
			double    min_corr  =       0.01; // 0.0025; // real max >0.005 with scale -500000;
			int       radius_search =  32;
			int       radius_centroid = 4;
			double    lim_rad =        80; // 60; // 40; // 50.0;
			double    trans_width =    60; // 40; // 20.0;
			double    reversal_rad =   0; // 26;// 22; // cut at first direction reversal after
			double    frac_outliers =   0.4;
			int default_kernel =        1; // 0; // 1; // default kernel choice 25-> 50// 1 for simulated, 0 - for extracted 
			boolean   only_correlate =  true; // false; // true;
			int       sub_pattern_index = 0;
			
			double    inv_fat_zero =   300;
			double    inv_lpf_sigma =    .05;
			int       inv_scale_radial = 8;   // scale up radial resolution 
			double    inv_lim_rad =     50.0; // outside all 0
			double    inv_trans_width = 20.0; // lim_rad-trans_width - start reducing
			double    inv_blur_center =  1.0; // cosine blur (in original pixels) in the center (<blur_radius) area
			double    inv_blur_radius =100.0; // constant blur radius
			double    inv_blur_rate =    0.2; // blur increase outside of blur_radius
			
			
//			String pattern_dir= "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/debug/mines/pattern_25m_zoom1/synthetic/";
			String pattern_dir= "/media/elphel/NVME/lwir16-proc/ortho_videos/mines_extract/evening_50m_sept12/debug_cross/new_synth/";
			String [] pattern_files={
					"patterns_r30.0_e8.0_ir10.0_ie8.0_is-0.1_or45.0_oe30.0_os-1.5_h8_w0.6_s-40.0_200x200.tif",
					"old_02.tiff",
					"patterns_r30.0_e24.0_ir10.0_ie8.0_is-0.1_or62.0_oe40.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e8.0_ir10.0_ie8.0_is-0.1_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e8.0_ir10.0_ie8.0_is0.0_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e6.0_ir10.0_ie8.0_is0.0_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e6.0_ir10.0_ie6.0_is-1.0_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e6.0_ir10.0_ie6.0_is-0.8_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e4.0_ir10.0_ie4.0_is-1.0_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e5.0_ir12.0_ie5.0_is-1.0_or45.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e5.0_ir12.0_ie5.0_is-1.0_or45.0_oe20.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e5.0_ir12.0_ie5.0_is-1.0_or50.0_oe40.0_os-1.0_h8_w0.6_s-60.0_200x200.tif", // best
					"patterns_r30.0_e5.0_ir12.0_ie5.0_is-1.0_or55.0_oe50.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e5.0_ir12.0_ie5.0_is0.0_or50.0_oe40.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"mine2_multi_zoom-1-INV_PATTERN_fo0.4_lr80.0_tw60.0_rr0.0_128x128.tif",
					"mine2_multi_zoom-1-INV_RADIAL_PATTERN_fo0.4_lr80.0_tw60.0_rr0.0_128x128.tif",
					"patterns_r17.0_e15.0_ir10.0_ie8.0_is0.0_or50.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif", // mine2
					"patterns_r17.0_e22.0_ir10.0_ie8.0_is0.0_or50.0_oe30.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e25.0_ir10.0_ie8.0_is0.0_or50.0_oe25.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e25.0_ir10.0_ie8.0_is0.0_or55.0_oe25.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e25.0_ir10.0_ie8.0_is0.0_or45.0_oe20.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e25.0_ir10.0_ie8.0_is0.0_or40.0_oe20.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e25.0_ir10.0_ie8.0_is0.0_or35.0_oe15.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r20.0_e20.0_ir10.0_ie8.0_is0.0_or35.0_oe15.0_os-1.0_h8_w0.6_s-60.0_200x200.tif",
					"patterns_r30.0_e15.0_ir15.0ie8.0_is-0.05_h8_w0.6_s-200.0_200x200.tiff", // morning used
					"--- 30, 28 edge 10 --",
					"patterns_r30.0_e10.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r30.0_e10.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r28.0_e10.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r28.0_e10.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"--- 26, 32 edge 10 --",
					"patterns_r26.0_e10.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r26.0_e10.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
//					"patterns_r32.0_e10.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-170.0_200x200.tif",
					"patterns_r32.0_e10.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r32.0_e10.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"---- 30, 26 edge 6 ---",
					"patterns_r30.0_e6.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r30.0_e6.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r26.0_e6.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r26.0_e6.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"---- 30, 26 edge 8 ---",					
					"patterns_r30.0_e8.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r30.0_e8.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r26.0_e8.0_ir15.0_ie8.0_is-0.05_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif",
					"patterns_r26.0_e8.0_ir15.0_ie8.0_is-0.3_or42.0_oe6.0_os0.0_h8_w0.6_s-200.0_200x200.tif"
					};
			
			
			GenericJTabbedDialog gdo = new GenericJTabbedDialog("Set image pair",1200,1000);
			gdo.addNumericField("Correlation size",          corr_size,       0,4,"",
					"Correlation size, power of 2");
			gdo.addNumericField("Extracted size",            extr_size,  0,4,"",
					"size of a square output size");
//			gdo.addStringField ("Pattern directory",          OrthoMap.pattern_dir, 180, 	"Absolute path including trailing \"/\".");
//			gdo.addChoice      ("Pattern filename:",          OrthoMap.pattern_files, OrthoMap.pattern_files[patt_choice]);
			gdo.addStringField ("Pattern directory",          pattern_dir, 180, 	"Absolute path including trailing \"/\".");
			gdo.addChoice      ("Pattern filename:",          pattern_files, OrthoMap.pattern_files[patt_choice]);
			gdo.addNumericField("Zoom-out factor",            zoomout,  0,4,"x", "Reduce pattern resolution to match image.");
			gdo.addNumericField("Phase correlation coefficient",  phaseCoeff,  3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
			gdo.addNumericField("Minimal correlation",        min_corr,  5,7,"","Minimal correlation value to keep.");
			gdo.addNumericField("Search radius",              radius_search,    0,4,"pix", "Radius to search for the object center.");
			gdo.addNumericField("Centroid radius",            radius_centroid,  0,4,"pix", "Limit centroid radius.");
			gdo.addMessage("Clean-up circular pattern:");
		
			gdo.addNumericField("Limit output patter radius", lim_rad,        0,4,"pix", "All zero outside this radius.");
			gdo.addNumericField("Transition width",           trans_width,    0,4,"pix", "cosine transition width.");
			gdo.addNumericField("Last reversal radius",       reversal_rad,   0,4,"pix", "Cut at first derivative reversal after this.");
			gdo.addNumericField("Outlier fraction",           frac_outliers,  3,7,"", "Remove outliers when averaging different directions.");
			gdo.addCheckbox     ("Only correlate",            only_correlate, "do not generate circular kernel.");
			gdo.addNumericField("subpattern index",           sub_pattern_index,0,4,"", "0 - full pattern, 1-8 - half-pattern.");

			gdo.addMessage("Radial kernel inversion");
			gdo.addNumericField("Inversion fat zero",         inv_fat_zero,  3,7,"", "Add to amplitude to reduce noise.");
			gdo.addNumericField("LPF sigma",                  inv_lpf_sigma,  3,7,"", "Filter inversion result.");
			gdo.addNumericField("Scale up radial resolution", inv_scale_radial,0,4,"", "Increase radial resolution for artifacts reduction.");
			gdo.addNumericField("Inverted kernel max radius", inv_lim_rad,      3,7,"pix", "Crop result kernel to this radius.");
			gdo.addNumericField("Fade out width",             inv_trans_width,  3,7,"pix", "Fade-out width for radial crop.");
			gdo.addNumericField("Center area blur",           inv_blur_center,  3,7,"pix", "Blur while integrating radial profile (center high-res area).");
			gdo.addNumericField("Start radial blur",          inv_blur_radius,  3,7,"pix", "Center area radius, blur more outside of it.");
			gdo.addNumericField("Radial blur increase rate",  inv_blur_rate,  3,7,"pix/pix", "Rate of the radial blur increase outside of the central area "+
			"- pixel of (biderectional) blur for each radial pixel outside of center area.");

			//only_correlate
			gdo.showDialog();
			if (gdo.wasCanceled()) return false;
			corr_size =                (int) gdo.getNextNumber();
			extr_size =           (int) gdo.getNextNumber();
			pattern_dir=                gdo.getNextString();		
			String pattern_file =       pattern_files[gdo.getNextChoiceIndex()];
			zoomout=              (int) gdo.getNextNumber();
			phaseCoeff=                 gdo.getNextNumber();
			min_corr=                   gdo.getNextNumber();
			radius_search=        (int) gdo.getNextNumber();
			radius_centroid=      (int) gdo.getNextNumber();
			lim_rad=                    gdo.getNextNumber();      
			trans_width=                gdo.getNextNumber();
			reversal_rad=               gdo.getNextNumber();
			frac_outliers=              gdo.getNextNumber();
			only_correlate =            gdo.getNextBoolean();
			sub_pattern_index=    (int) gdo.getNextNumber();
			
			inv_fat_zero =              gdo.getNextNumber();
			inv_lpf_sigma =             gdo.getNextNumber();
			inv_scale_radial =    (int) gdo.getNextNumber();
			inv_lim_rad =               gdo.getNextNumber();
			inv_trans_width =           gdo.getNextNumber();
			inv_blur_center =           gdo.getNextNumber();
			inv_blur_radius =           gdo.getNextNumber();
			inv_blur_rate =   			gdo.getNextNumber();
			
			System.out.println("Will extract objects here, image zoom_level="+zool_lev_objects);
			for (ObjectLocation ol: object_list) {
				System.out.println(ol.name+": "+ol.xy_meters[0]+"/"+ol.xy_meters[1]);
			}
			int [] indices = null;
			int [][] bounds = maps_collection.getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
					zool_lev_objects,
					indices); 
			int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
			int height = bounds[1][1] - bounds[1][0];
			if ((width != imp_sel.getWidth()) || (height != imp_sel.getHeight())) {
				System.out.println("Marked image ("+imp_sel.getWidth()+"x"+imp_sel.getHeight()+") does not match the full map size ("+
						width+"x"+height+"), will try just for a pair of the first two slices");
				String [] labels = imp_sel.getStack().getSliceLabels();
				indices = new int [2];
				for (int i = 0; i < indices.length; i++) {
					indices[i] = maps_collection.getIndex(labels[i].substring(0, 17));
				}
				bounds = maps_collection.getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
						zool_lev_objects,
						indices);
				width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
				height = bounds[1][1] - bounds[1][0];
				if ((width != imp_sel.getWidth()) || (height != imp_sel.getHeight())) {
					System.out.println("Marked image ("+imp_sel.getWidth()+"x"+imp_sel.getHeight()+") does not match the 2-image map size ("+
							width+"x"+height+") either.Giving up");
					return false;
				} else {
					System.out.println("Marked image ("+imp_sel.getWidth()+"x"+imp_sel.getHeight()+") does match the 2-image map size ("+
							width+"x"+height+"). Using it.");
				}
			}
			
			maps_collection.reverseRender( // assuming image, not alt
					object_list,        // ArrayList<ObjectLocation> objects,
					zool_lev_objects,   // int         zoom_level){
					indices);           // int []      indices){ // null or selected image indices
			System.out.println("Object coordinates in source images: ");
			for (ObjectLocation ol: object_list) {
				System.out.println(ol.name+": "+ol.getPixels()[0]+"/"+ol.getPixels()[1]);
			}
			double [][] object_stack = new double [object_list.size()][];
			String [] object_titles = new String [object_stack.length];
			for (int i = 0; i < object_stack.length; i++) {
				ObjectLocation ol = object_list.get(i);
				object_titles[i] = ol.getName();
				object_stack[i] = ol.extractObjectImage(
						maps_collection,
						extr_size);

			}

			ImagePlus imp_obj = ShowDoubleFloatArrays.makeArrays(
					object_stack,
					extr_size,
					extr_size,
					OrthoMap.removeKnownExtension(imp_sel.getTitle())+
					OrthoMap.removeKnownExtension(pattern_file)+":"+sub_pattern_index+
					"-OBJECT_"+extr_size+"x"+extr_size+".tiff",				
					object_titles); // test_titles,
			imp_obj.show();
			
			// get pattern(s)
//			String pattern_path=OrthoMap.pattern_dir+pattern_file;
			String pattern_path=pattern_dir+pattern_file;
			ImagePlus imp_pattern = new ImagePlus(pattern_path);
			int pattern_size =  imp_pattern.getWidth();
			if (pattern_size == 0) {
				System.out.println("testPatternCorrelate(): pattern \""+pattern_path+"\" is not found.");
				return false;
			}
			System.out.println("Using pattern file: "+pattern_path+", subpattern="+sub_pattern_index);
			ImageStack stack_pattern = imp_pattern.getStack();
			int nSlices = stack_pattern.getSize();
			double [][] patterns = new double[nSlices][];
			String [] pattern_labels = new String[nSlices];
			for (int n = 0; n < patterns.length; n++) {
				pattern_labels[n]=stack_pattern.getShortSliceLabel(n+1);
				float [] fpixels_pattern = (float[]) stack_pattern.getPixels(n+1);
				patterns[n]=new double[fpixels_pattern.length];
				for (int i = 0; i < fpixels_pattern.length; i++) {
					patterns[n][i] = fpixels_pattern[i];
				}
			}
			double [] kernel =  OrthoMap.getConvolutionKernel(default_kernel);
			double [][] centers = new double [object_stack.length][];
			
			double [][] corr_ret = new double [object_stack.length][corr_size*corr_size];
			for (int i = 0; i < object_stack.length; i++) {
				String    dbg_prefix = "corr_patt_"+i+"-"+OrthoMap.removeKnownExtension(pattern_file)+":"+sub_pattern_index;
				ObjectLocation ol = object_list.get(i);
				centers[i] = ObjectLocation.getPatternCenter(
						object_stack[i], // double [] data,
						patterns[sub_pattern_index],     // double [] pattern,
						kernel,          // double [] kernel,
						zoomout,         // int       zoomout,
						phaseCoeff,      // double    phaseCoeff,
						min_corr,        // double    min_corr,
						radius_search,   // int       radius_search,
						radius_centroid, // int       radius_centroid,
						corr_ret[i],     //double [] corr_ret, // null or double[corr_size*corr_size]
						dbg_prefix,      // String    dbg_prefix,
						debugLevel);     // int       debugLevel)
				System.out.println(i+": center at "+centers[i][0]+"/"+centers[i][1]+", strength="+centers[i][2]);
			}
			PointRoi roi = new PointRoi();
			roi.setOptions("label");
			for (int i = 0; i < centers.length; i++) {
				roi.addPoint(centers[i][0]+extr_size/2, centers[i][1]+extr_size/2, i+1); // ,1);
			}
			imp_obj.setRoi(roi);
			// calculate statistics for cor_ret[i]
			CorrelationPeakStats [] stats = new CorrelationPeakStats [object_stack.length];
			double search_rad = 15.0;
			double frac_max =   0.5;
			double other_rad = 25;
			for (int i = 0; i < object_stack.length; i++) {
				double [] a_cent= {corr_size/2+centers[i][0],corr_size/2+centers[i][1]};
				stats[i]=new CorrelationPeakStats(
						corr_ret[i], // double [] data, // square data
						a_cent, // centers[i],  // double [] cent_xy, // if null, use center of the square
						search_rad,  // double    radius, // search for maximum within this radius
						frac_max,    // double    frac_max,
						other_rad,
						0);          // int       debugLevel) 
			}
			
			System.out.println("\nUsing pattern file: "+pattern_path+", subpattern="+sub_pattern_index);
			System.out.println(String.format("%2s (%9s): %7s %7s %7s %7s %7s %7s %17s",
					"#","  X/Y  ","strengh", "radius", " elong ", " dist ", "  dx  ","  dy  ",
					"image timestamp"));
			for (int i = 0; i < object_stack.length; i++) {
				ObjectLocation ol =  object_list.get(i);
				System.out.println(String.format("%2d (%4d/%4d): %7.5f %7.3f %7.3f %7.3f %7.3f %7.3f %17s",
						i, ol.getPixels()[0], ol.getPixels()[1],
						stats[i].best_d,stats[i].eff_rad,stats[i].elong,stats[i].dist,
						stats[i].cent_offs[0]+centers[i][0],stats[i].cent_offs[1]+centers[i][1],
						ol.getName()));
			}
			System.out.println();
			
			if (only_correlate) {
				System.out.println("testPatternCorrelate(): correlation DONE, only_correlate is set to true, exiting");
				return true;
			}
			
//			imp_obj.show();
			// First process each captured object separately, then compare and average?
			// TODO: use finer grid, deconvolve with altitude kernel, calculate effective width of the pattern
			double [][] output_patterns = new double [object_stack.length+1][];
			String [] output_patt_titles = new String[output_patterns.length];
			output_patt_titles[output_patt_titles.length-1]= "average";
			output_patterns[object_stack.length] = new double [corr_size*corr_size];
//object_titles
			boolean debug_good = debugLevel > -4;
			boolean [][] good_pix = debug_good ? (new boolean[object_stack.length][extr_size*extr_size]) : null; // debugging
			String settings_str = "_fo"+frac_outliers+"_lr"+lim_rad+"_tw"+trans_width+"_rr"+reversal_rad;
			for (int i = 0; i < object_stack.length; i++) {
				output_patterns[i] = ObjectLocation.getRadialPattern(
						object_stack[i], // double [] data,
						centers[i],      // double [] xy_offs,
						corr_size,       // int       corr_size, 
						lim_rad,         // double    lim_rad,     // outside all 0
						trans_width,     //double    trans_width, // lim_rad-trans_width - start reducing
						frac_outliers,   // double    frac_outliers,
						reversal_rad,   // double     reversal_rad,
						((good_pix != null)? good_pix[i] : null), // boolean [] good,       // null or same size as data
						debugLevel);     // int       debugLevel)
				output_patt_titles[i] = object_titles[i];
				for (int j = 0; j < output_patterns[i].length; j++) {
					output_patterns[object_stack.length][j] += output_patterns[i][j];
				}
			}
			for (int j = 0; j < output_patterns[0].length; j++) {
				output_patterns[object_stack.length][j] /= object_stack.length;
			}
		
			double [][] inverted_patterns = new double [output_patterns.length][];
			for (int i = 0; i < output_patterns.length; i++) {
				//pattern_file
				String dbg_prefix = OrthoMap.removeKnownExtension(pattern_file)+"-"+i;
				inverted_patterns[i] = ObjectLocation.invertRadialKernel(
						output_patterns[i], // double [] kernel,
						inv_fat_zero, // double    fat_zero,
						inv_lpf_sigma, // double    lpf_sigma,
						inv_scale_radial, // int       scale_radial, // scale up radial resolution 
						inv_lim_rad, // double    lim_rad,     // outside all 0
						inv_trans_width, // double    trans_width, // lim_rad-trans_width - start reducing
						inv_blur_center, // double    blur_center, // cosine blur (in original pixels) in the center (<blur_radius) area
						inv_blur_radius, // double    blur_radius, // constant blur radius
						inv_blur_rate,   // double    blur_rate,   // blur increase outside of blur_radius
						centers, // // double [][] centers,
						object_stack, // double [][] src_cuts,
						dbg_prefix, // String dbg_prefix,
						1); // int       debugLevel)
			}
			ImagePlus imp_inv_patt = ShowDoubleFloatArrays.makeArrays(
					inverted_patterns,
					corr_size,
					corr_size,
					OrthoMap.removeKnownExtension(imp_sel.getTitle())+"-INV_RADIAL_PATTERN"+settings_str+"_"+corr_size+"x"+corr_size+".tiff",				
					output_patt_titles); // test_titles,
			imp_inv_patt.show();
			
			ImagePlus imp_out_patt = ShowDoubleFloatArrays.makeArrays(
					output_patterns,
					corr_size,
					corr_size,
					OrthoMap.removeKnownExtension(imp_sel.getTitle())+"-PATTERN"+settings_str+"_"+corr_size+"x"+corr_size+".tiff",				
					output_patt_titles); // test_titles,
			imp_out_patt.show();
			if (good_pix != null) {
				double [][] object_stack_masked = new double [2 * object_stack.length][]; 
				String [] object_titles_masked = new String [2 * object_stack.length];
				PointRoi roi_masked = new PointRoi();
				roi_masked.setOptions("label");
				for (int i = 0; i < centers.length; i++) {
					roi_masked.addPoint(centers[i][0]+extr_size/2, centers[i][1]+extr_size/2, 2*i+1); // ,1);
					roi_masked.addPoint(centers[i][0]+extr_size/2, centers[i][1]+extr_size/2, 2*i+2); // ,1);
				}

				for (int i = 0; i < object_stack.length; i++) {
					object_stack_masked[2*i + 0] = object_stack[i];
					object_stack_masked[2*i + 1] = object_stack[i].clone();
					for (int j = 0; j < object_stack[i].length; j++) {
						if (!good_pix[i][j]) {
							object_stack_masked[2*i + 1][j] = Double.NaN;
						}
					}
				}
				ImagePlus imp_obj_masked = ShowDoubleFloatArrays.makeArrays(
						object_stack_masked,
						extr_size,
						extr_size,
						OrthoMap.removeKnownExtension(imp_sel.getTitle())+"-OBJECT_MASKED"+settings_str+"_"+extr_size+"x"+extr_size+".tiff",				
						object_titles_masked); // test_titles,
				imp_obj_masked.setRoi(roi_masked);
				imp_obj_masked.show();
			}
			return true;
		}
		
		
		if (use_marked_image) {
			// verify matching names
			//all_scenes
			boolean image_ok = false;
			wrong_image:{
				if (names.length != all_scenes.length) {
					break wrong_image;
				}
				for (int i = 0; i < names.length; i++) {
					if (!names[i].equals(all_scenes[i])) {
						break wrong_image;
					}
				}
				image_ok = true;
			}
			if (!image_ok) {
				System.out.println("Using wrong image - scenes names do not match!");
				return false;
			}
			maps_collection.ortho_maps[maps_collection.getIndex(gpu_spair[0])].setMatch(gpu_spair[1],pairwiseOrthoMatch);
		}
		
		//getTemperature()
		// get all temperatures
		maps_collection.getAllTemperatures();
//		if (update_lla) {
//			System.out.println("Updating map files metadata");
//			maps_collection.updateLLA();
//			maps_collection.updateNumberScenes();
//			maps_collection.updateSfmGain();
//		}
		
        // which pair to compare
//		String [] gpu_spair = {names[gpu_ipair[0]],names[gpu_ipair[1]]}; 
		int [] origin = new int[2];
		if (show_combo) { // now all modes, and stats?
			maps_collection.processComboMap(
					clt_parameters, // CLTParameters    clt_parameters,
					debugLevel);
//			return true; // need to save AGL?
		}
		
		// for all modes - needed for create_overlaps || process_correlation || render_match || pattern_match
    	if (GPU_QUAD_AFFINE == null) {
    		// Fail loud rather than NPE deep in JCuda: the orthomosaic GPU_QUAD_AFFINE (rectilinear) path is
    		// not on the validated JNA surface yet (wider than CUAS: affine matching). Run orthomosaic on
    		// JCuda. The CUAS rectilinear path IS ported (GpuQuad.createRectilinear). By Claude on 06/26/2026.
    		if (GpuQuad.useJnaBackend()) {
    			throw new UnsupportedOperationException(
    					"ComboMatch GPU_QUAD_AFFINE (orthomosaic, rectilinear) is not yet ported to the JNA backend "+
    					"(-Dtp.backend=jna); run orthomosaic with the JCUDA backend.");
    		}
        	System.out.println("Setting up GPU");
    		try {
    			GPU_QUAD_AFFINE = new GpuQuad(//
    					GPU_TILE_PROCESSOR,   // GPUTileProcessor gpuTileProcessor,
    					gpu_max_width,        // final int        max_width,
    					gpu_max_height,       // final int        max_height,
    					1,                    // final int        num_colors, // normally 1?
    					clt_parameters.gpu_debug_level);
    		} catch (Exception e) {
    			System.out.println("Failed to initialize GpuQuad class");
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    			return false;
    		} // final int debugLevel);
    	}

		if (create_map) {
			int ok = OrthoMultiLMA.buildOrthoMap(
					clt_parameters,            // CLTParameters       clt_parameters,
					maps_collection, // OrthoMapsCollection maps_collection
					orthoMapsCollection_savepath); // String orthoMapsCollection_path
			return (ok >= 0);
		}

		if (create_equalize) {
			int ok = OrthoEqualizeLMA.buildEqualize(
					clt_parameters,            // CLTParameters       clt_parameters,
					maps_collection, // OrthoMapsCollection maps_collection
					orthoMapsCollection_savepath); // String orthoMapsCollection_path
			return (ok >= 0);
		}
		if (create_pairwise_matches) {
			boolean ok =maps_collection.getOverlapPairs(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
		
		if (augment_pairwise_affines) {
			boolean ok =maps_collection.augmentPairwiseAffines(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
		
		if (create_pairwise_affines) {
			boolean ok =maps_collection.generatePairwiseAffines(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
		
		
		if (equalize_overlaps) {
			boolean ok =maps_collection.equalizeIntersectedPairs(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
		
		if (altitude_match_pairs) {
			boolean ok =maps_collection.altutudeMatchPairs(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
		
		if (display_pairs) {
			boolean ok =maps_collection.displayScenePairs(
					clt_parameters, // CLTParameters    clt_parameters,
					orthoMapsCollection_savepath); // String orthoMapsCollection_path);
			return ok; // Just exit, do not try other commands.  if (!ok) return false;
		}
        if (process_correlation || render_match || pattern_match || test_multi_lma) {
//        	int [] gpu_pair;
			boolean removeTilt =   false;
			boolean removeRot =    false;
			boolean removeScale =  false;
			boolean removeOffset = false;
			boolean max_is_scale = false;

        	if (gpu_spair == null) {
        		ArrayList<Point> pairs_list = new ArrayList<Point>(); 
        		for (OrthoMap map : maps_collection.ortho_maps) {
        			for (String other_name: map.pairwise_matches.keySet()) {
        				pairs_list.add(new Point(
        						maps_collection.getIndex(map.getName()),
        						maps_collection.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
        			}
        		});
        		
        		
        		
        		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_list =           clt_parameters.imp.flt_list;          //  true;
        		boolean flt_undef_only =     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 =     clt_parameters.imp.flt_show_names;    //  true;
        		boolean flt_show_overlaps =  clt_parameters.imp.flt_show_overlaps; //  true;
        		boolean flt_show_rms =       clt_parameters.imp.flt_show_rms;      //  true;
        		boolean flt_show_zoom =      clt_parameters.imp.flt_show_zoom;     //  true;
        		boolean flt_show_alt =       clt_parameters.imp.flt_show_alt;      //  true;
        		boolean flt_update_config = false;
        		String  flt_extra_line =    "--- select a single image ---";
				GenericJTabbedDialog gdf = new GenericJTabbedDialog("Select pairs filter/display",800,600);
				gdf.addCheckbox    ("Filter pairs",                flt_list, "Filter available pairs.");
				gdf.addCheckbox    ("Keep undefined pairs only",   flt_undef_only, "Keep only undefined pairs (affines== null).");
				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.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.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. addChoice("Filter by pairwise ALT availability",IntersceneMatchParameters.FLT_ALT_MODES, IntersceneMatchParameters.FLT_ALT_MODES[flt_alt],
						"Filter by pairwise ALT availability.");
				
				gdf.addCheckbox    ("Show scene names",            flt_show_names, "Show scene full names (timestamps) in selection drop-down list.");
				gdf.addCheckbox    ("Show scene overlaps",         flt_show_overlaps, "Show scene overlaps (in percents) in selection drop-down list.");
				gdf.addCheckbox    ("Show pairs RMSE",             flt_show_rms, "Show scene match RMSE in selection drop-down list.");
				gdf.addCheckbox    ("Show zoom level",             flt_show_zoom, "Show zoom level.");
				gdf.addCheckbox    ("Show ALT",                    flt_show_alt,  "Show altitude data availability.");
				
				gdf.addCheckbox    ("Update configuration",        flt_update_config, "Update matching configuration parameters to be saved as defaults.");
				gdf.showDialog();
				if (gdf.wasCanceled()) return false;
				flt_list          = gdf.getNextBoolean();
				flt_undef_only    = gdf.getNextBoolean();
				flt_min_overlap   = gdf.getNextNumber();
				flt_max_overlap   = gdf.getNextNumber();
				flt_min_rms       = gdf.getNextNumber();
				flt_max_rms       = gdf.getNextNumber();
				flt_nan_rms       = gdf.getNextBoolean();
		        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_alt           = gdf.getNextChoiceIndex();

				flt_show_names    = gdf.getNextBoolean();
				flt_show_overlaps = gdf.getNextBoolean();
				flt_show_rms      = gdf.getNextBoolean();
				flt_show_zoom  =    gdf.getNextBoolean();				
				flt_show_alt  =     gdf.getNextBoolean();				
				flt_update_config = gdf.getNextBoolean();
				if (flt_update_config) {
					clt_parameters.imp.flt_list =          flt_list;
					clt_parameters.imp.flt_undef_only =    flt_undef_only;
					clt_parameters.imp.flt_min_overlap =   flt_min_overlap;
					clt_parameters.imp.flt_max_overlap =   flt_max_overlap;
					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_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.flt_show_names =    flt_show_names;
					clt_parameters.imp.flt_show_overlaps = flt_show_overlaps;
					clt_parameters.imp.flt_show_rms =      flt_show_rms;
					clt_parameters.imp.flt_show_zoom =     flt_show_zoom;
					clt_parameters.imp.flt_show_alt =      flt_show_alt;
				}

				if (flt_list) {
					available_pairs = maps_collection.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 = maps_collection.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,
						flt_extra_line);   // String           extra_line)
				
				GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,300);
				int num_choice_lines = 50;
				gdc.addChoice("Image pair:",
						choices_all,
						choices_all[choices_all.length - 1], // -1],
						"Select processed image pair or request a single image selection", num_choice_lines);
				gdc.addCheckbox    ("Remove tilt",             removeTilt,   "Remove tilts from the pairwise affine transform.");
				gdc.addCheckbox    ("Remove Rotation",         removeRot,    "Remove rotation from the pairwise affine transform.");
				gdc.addCheckbox    ("Remove Scale",            removeScale,  "Remove scale from the pairwise affine transform.");
				gdc.addCheckbox    ("Remove Translation",      removeOffset, "Remove translation from the pairwise affine transform.");
				gdc.addCheckbox    ("Max eigenvalue is scale", max_is_scale, "Maximal eigenvalue is scale (false - minimal is).");
				//        			boolean max_is_scale = false;

				gdc.showDialog();
				if (gdc.wasCanceled()) return false;
				int pair=      gdc.getNextChoiceIndex();
				removeTilt =   gdc.getNextBoolean();
				removeRot =    gdc.getNextBoolean();
				removeScale =  gdc.getNextBoolean();
				removeOffset = gdc.getNextBoolean();
				max_is_scale = gdc.getNextBoolean();
				if (pair >= (choices_all.length -1)) {
					int default_choice = 0;
					int num_scene_lines = 50;
					if (process_correlation || render_match) { // select a second image to match
						// select a second image and set gpu_spair
						int default_choice1 = 0;
						gpu_spair = maps_collection.selectTwoScenes(
								default_choice1, // int default_choice,
								num_scene_lines); // int num_choice_lines)
						if (gpu_spair == null) {
							return false;
						}
					} else {
						String scene_name = 	maps_collection.selectOneScene(
								0, // int num_scene,
								default_choice, // int default_choice,
								num_scene_lines); // int num_choice_lines)
						if (scene_name == null) {
							return false;
						}
						gpu_spair = new String[] {scene_name};
					}
				} else {
					gpu_spair = new String[] {
							maps_collection.ortho_maps[available_pairs[pair][0]].getName(),
							maps_collection.ortho_maps[available_pairs[pair][1]].getName()};
				}
        	}
        	int [] gpu_pair = new int[gpu_spair.length];
        	for (int i = 0; i < gpu_pair.length; i++) {
        		gpu_pair[i] = maps_collection.getIndex(gpu_spair[i]);
        	}
        	int min_zoom_lev = maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel();
        	int max_zoom_lev = maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel();
        	double max_agl = maps_collection.ortho_maps[gpu_pair[0]].getAGL();
        	for (int i = 0; i < gpu_pair.length; i++) {
        		max_agl = Math.max(max_agl, maps_collection.ortho_maps[gpu_pair[i]].getAGL());
        		min_zoom_lev = Math.min(min_zoom_lev, maps_collection.ortho_maps[gpu_pair[i]].getOriginalZoomLevel());
        		max_zoom_lev = Math.max(max_zoom_lev, maps_collection.ortho_maps[gpu_pair[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?
        	
        	// Here - always start with unity affine0 and affine1 from possibly matched pair 
    		double [][] affine0 = {{1,0,0},{0,1,0}}; // will always stay the same
    		double [][] affine1 = null;

        	if (gpu_spair.length < 2) {
        		System.out.println("Selected a single image, not a pair");
    			double [][][] affines = {affine0}; // or use affine1 = null as second?
				if (pattern_match) {
					ImagePlus imp_pat_match = maps_collection.patternMatchDualWrap (
							gpu_pair, // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
							affines,  // double [][][] affines, // null or [indices.length][2][3] 
							null, // warp);    // FineXYCorr    warp)
							null); // double [][]   ground_planes) TODO: add calculation of the ground plane for single images
					//						imp_pat_match.show();
				}
        		
        	} else { // gpu_spair.length >= 2
        		if (test_multi_lma) {
        			OrthoMultiLMA.testMultiLMA(
        					clt_parameters,  // CLTParameters       clt_parameters,
        					maps_collection, // OrthoMapsCollection maps_collection,
        					gpu_pair);       // int []              indices)
        			return true;
        		}
        		if (process_correlation && !use_marked_image) { // match may or may not exist
        			// if match exists - ask if use it. If not - open dialog and start spiral
        			pairwiseOrthoMatch = initialPairAdjust(
        					clt_parameters,  // CLTParameters       clt_parameters,
        					maps_collection, // OrthoMapsCollection maps_collection,
        					frac_remove,     // double              frac_remove, //  =        0.25
        					metric_error_adj,// double              metric_error,
        					gpu_spair,       // String[]            gpu_spair,
        					debugLevel);     // int                 debugLevel)
        			if (pairwiseOrthoMatch == null) { // if OK - either match existed or created by SpiralMatch()
        				return false;
        			}
        		} else {
        			pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch(
        					maps_collection.ortho_maps[gpu_pair[1]].getName());
        		}
        		if (pairwiseOrthoMatch == null) { // try undefined
        			pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch(
        					maps_collection.ortho_maps[gpu_pair[1]].getName(), true);
            		if (pairwiseOrthoMatch != null) {
            			double [][] aff0 = maps_collection.ortho_maps[gpu_pair[0]].getAffine();            			
            			double [][] aff1 = maps_collection.ortho_maps[gpu_pair[1]].getAffine();
            			double [] enuOffset = maps_collection.ortho_maps[gpu_pair[1]].enuOffsetTo(maps_collection.ortho_maps[gpu_pair[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);
            			double [][] daffine = aff_match.getAffine();
            			pairwiseOrthoMatch.setAffine(daffine);
            		}
        		}        		
        		
        		if (pairwiseOrthoMatch == null) {
        			System.out.println("No correlation data is available for pairs "+gpu_spair[0]+
        					" - "+gpu_spair[1]+" need to implement/search reverse,  a spiral search or restart command");
        			System.out.print("after loading a corresponding full map image and marking same feature on ");
        			System.out.println("each of the two selected slices, reference one first");
        			
        			
        			
        		} else {
        			affine1 = pairwiseOrthoMatch.getAffine().clone();
        			if (debugLevel > -4) {
            			System.out.println("removeTilt="+  removeTilt);
            			System.out.println("removeRot="+   removeRot);
            			System.out.println("removeScale="+ removeScale);
            			System.out.println("removeOffset="+removeOffset);
            			System.out.println("max_is_scale="+max_is_scale);
            			if (removeTilt || removeRot || removeScale || removeOffset) {
                			SingularValueDecomposition svd_affine_pair =  SingularValueDecomposition.singularValueDecomposeScaleTiltGamma(affine1, true); // y_down_ccw); // boolean y_down_ccw)
                			System.out.println("Full (original) affine transform:");
            	        	System.out.println(QuatUtils.affinesToString(affine1, "full_affine"));
            	        	System.out.println("full_affine_svd" + svd_affine_pair.toString(true)); // use_degrees));
            			}
        			}
        			affine1 = SingularValueDecomposition.removeTiltRotScale(
        					affine1,       // double [][] A,
        					removeTilt,    // boolean removeTilt,
        					removeRot,     // boolean removeRot,
        					removeScale,   // boolean removeScale,
        					removeOffset,  // boolean removeOffset)
        					max_is_scale); //boolean max_is_scale)
        			if (debugLevel > -4) {
            			if (removeTilt || removeRot || removeScale || removeOffset) {
                			SingularValueDecomposition svd_affine_pair =  SingularValueDecomposition.singularValueDecomposeScaleTiltGamma(affine1, true); // y_down_ccw); // boolean y_down_ccw)
                			System.out.println("Final differential affine transform:");
            	        	System.out.println(QuatUtils.affinesToString(affine1, "affine1    "));
            	        	System.out.println("affine1_svd    " + svd_affine_pair.toString(true)); // use_degrees));
            			}
        			}
        			
        			double [][][] affines = {affine0,affine1};
        			int [] zooms = {initial_zoom, min_zoom_lev, 1000,1000}; // make automatic
//        			double scale = 2.0; // scale vectors when warping;
        			//				int num_tries  = 5; // make configurable
        			if (!process_correlation || !use_marked_image) { // skip low-res
        				zooms = new int[] {min_zoom_lev, 1000};
        			}
        			if (!process_correlation) { // 0 LMA adjustments
        				num_tries_fit = 0;
        				update_match = false;
        			}
 //       			debugLevel = 0;
        			boolean  batch_mode = true; // false; //  true;
        			
        			boolean ignore_prev_rms = clt_parameters.imp.pmtch_ignore_rms; // true;
        			Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair()
        			double [][] ground_planes = null;
					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)
					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

        			for (int zi = 0; zi < zooms.length; zi++) {
        				zoom_lev = zooms[zi];
        				if (zoom_lev >=1000) {
        					break;
        				}
        				boolean show_vf = false; // render_match || pattern_match;
        				if (render_match || pattern_match) {
        					ground_planes = new double [gpu_pair.length][];
        				}
        				// will modify affines[1], later add jtj, weight, smth. else?
        				PairwiseOrthoMatch pmatch = process_correlation? pairwiseOrthoMatch: null;
        				
        				FineXYCorr warp = maps_collection.correlateOrthoPair(
        						clt_parameters,  // CLTParameters    clt_parameters,
        						(process_correlation? pairwiseOrthoMatch: null), //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_fit,   //  = 5int              num_tries, //  = 5
        						true,            // 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
        						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,
        						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);     // final int        debugLevel)
        				if ((warp == null) || ((pmatch != null) &&  Double.isNaN(pmatch.rms))) {
        					System.out.println("Failed correlateOrthoPair()");
        					return false;
        				}
        				//maps_collection.ortho_maps[gpu_pair[1]]
        				if (process_correlation) {
        					if (update_match) {
        						pairwiseOrthoMatch.zoom_lev = zoom_lev;
        						pairwiseOrthoMatch.setAffine(affines[1]);
        						maps_collection.ortho_maps[gpu_pair[0]].setMatch(
        								maps_collection.ortho_maps[gpu_pair[1]].getName(),
        								pairwiseOrthoMatch);
        						if (debugLevel > -4) {
        							System.out.println("Updated correleation data in the database");
        						}
        					} else {
        						if (debugLevel > -4) {
        							System.out.println("Updating correlation data is DISABLED");
        						}
        					}
        					System.out.println("Done");
        					if (debugLevel>-4) {
        						System.out.println("adjusted affines[1] for a pair: "+gpu_spair[0]+"/"+gpu_spair[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]+"]]");
        						if (pmatch != null) {
            						System.out.println("RMSE="+pmatch.rms);
        						}
        						System.out.println();
        					}
        					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(String.format("%d\t%d", gpu_pair[0], gpu_pair[1]));
        						sb.append(String.format("\t%6.4f\t%d\n", pairwiseOrthoMatch.rms, pairwiseOrthoMatch.zoom_lev));
        						CalibrationFileManagement.saveStringToFile (
        								log_path,          //String path,
        								sb.toString(), // data,
        								true); // boolean append)
        					}
        				}

        				if (pattern_match) {
        					ImagePlus imp_pat_match = maps_collection.patternMatchDualWrap (
        							gpu_pair, // 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); // null or
        					//						imp_pat_match.show();
        				}
        				if (render_match) {
        					String title=String.format("multi_%03d-%03d_%s-%s_zoom%d_%d",gpu_pair[0],gpu_pair[1],gpu_spair[0],gpu_spair[1],min_zoom_lev,zoom_lev);
        					if (removeTilt || removeRot || removeScale || removeOffset) {
        						title += "_no-";
        						if (removeTilt)   title += "T";
        						if (removeRot)    title += "R";
        						if (removeScale)  title += "S";
        						if (removeOffset) title += "O";
        						if (max_is_scale) title += "X"; 
        					}
        					// Avoid renderMulti() - it duplicates code renderMultiDouble()
        					int eq_mode = 2; // calculate
        					ImagePlus imp_img_pair = 	maps_collection.renderMulti (
        							//_zoom<integer> is needed for opening with "Extract Objects" command
        							title,             // String      title,
//        							OrthoMapsCollection.MODE_IMAGE,  // int           mode,    // 0 - regular image, 1 - altitudes, 2 - black/white mask       // boolean     use_alt,
        							eq_mode,           //int           eq_mode, // 0 - ignore equalization, 1 - use stored equalization, 2 - calculate equalization 
        							gpu_pair,          // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
        							bounds_to_indices, // boolean       bounds_to_indices,
        							temp_mode,         // int           temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
        							affines,           // double [][][] affines, // null or [indices.length][2][3]
        							warp,              // FineXYCorr    warp,
        							true,              // show_centers,      // boolean     show_centers,
        							min_zoom_lev,      // int         zoom_level,
        							origin);           // int []      origin){
        					imp_img_pair.show();
        				}
        			}
        		}
			}
        }
        if (save_collection) {
        	try {
        		maps_collection.writeOrthoMapsCollection(orthoMapsCollection_path);
        	} catch (IOException e) {
        		// TODO Auto-generated catch block
        		e.printStackTrace();
        	}
        	System.out.println("Saved data to "+ orthoMapsCollection_path);
        }
        return true;
	}
	
	
	
	public static PairwiseOrthoMatch initialPairAdjust(
			CLTParameters       clt_parameters,
			OrthoMapsCollection maps_collection,
			double              frac_remove, //  =        0.25
			double              metric_error,
			String[]            gpu_spair,
			int                 debugLevel) {
    	int [] gpu_pair = new int[gpu_spair.length];
    	gpu_pair[0] = maps_collection.getIndex(gpu_spair[0]);
    	int min_zoom_lev = maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel();
    	int max_zoom_lev = maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel();

    	for (int i = 0; i < gpu_pair.length; i++) {
    		gpu_pair[i] = maps_collection.getIndex(gpu_spair[i]);
    		min_zoom_lev = Math.min(min_zoom_lev, maps_collection.ortho_maps[gpu_pair[i]].getOriginalZoomLevel());
    		max_zoom_lev = Math.max(max_zoom_lev, maps_collection.ortho_maps[gpu_pair[i]].getOriginalZoomLevel());
    	}
    	int initial_zoom = max_zoom_lev - 4;  // another algorithm?

		PairwiseOrthoMatch pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch(
				maps_collection.ortho_maps[gpu_pair[1]].getName());
		PairwiseOrthoMatch inv_match =  maps_collection.ortho_maps[gpu_pair[1]].getMatch(
				maps_collection.ortho_maps[gpu_pair[0]].getName());
		// dialog - ask parameters and if has_match -ask if to use it (then just return true)
		// if has inv - ask and, if yes, = create inverted as initial
		boolean use_existing_pair =    clt_parameters.imp.ospir_existing; // false
		boolean invert_exixting_pair = clt_parameters.imp.ospir_invert;   // false
		double search_step =           clt_parameters.imp.ospir_step;     // 8.0; // pix
		double search_range =          clt_parameters.imp.ospir_range;    // 50.0; // pix
		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;
		boolean ignore_rms =           clt_parameters.imp.ospir_ignore_rms; // false
		int    spiral_debug =          clt_parameters.imp.ospir_debug;    // 0;
		boolean log_append =           clt_parameters.imp.pwise_log_append;
		String log_path =              clt_parameters.imp.pwise_log_path;
		
		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)
		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
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Setup Spiral Match",1200,350);
		if (pairwiseOrthoMatch != null) {
			gd.addCheckbox    ("Use existing image pair",  use_existing_pair, "Use existing affine settings for this pair, do not use spiral search.");
		} else {
			use_existing_pair = false;
		}
		if (inv_match != null) {
			gd.addCheckbox    ("Invert existing image pair", invert_exixting_pair, "Invert existing image pair affine transform, do not use spiral search.");
		} else {
			invert_exixting_pair = false;
		}
		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("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.");
		gd.addCheckbox    ("Ignore worsening RMSE",  ignore_rms, "Ignore worsening/not improving RMSE during spiral search.");
	    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("Spiral search debug level",spiral_debug, 0,3,"","Debug level during Spiral search.");
		
		gd.addMessage("Parameters, common to all matching, not only spiral");
		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.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.showDialog();
		if (gd.wasCanceled()) return null;
		if (pairwiseOrthoMatch != null) {
			use_existing_pair =    gd.getNextBoolean();
		}
		if (inv_match != null) {
			invert_exixting_pair = gd.getNextBoolean();
		}
		search_step=               gd.getNextNumber();
		search_range=              gd.getNextNumber();
		good_rms =                 gd.getNextNumber();
		max_rms =                  gd.getNextNumber();
		min_overlap =        (int) gd.getNextNumber();
		num_iter_lma =       (int) gd.getNextNumber();
		ignore_rms =               gd.getNextBoolean();
		log_append =               gd.getNextBoolean();
		log_path =                 gd.getNextString();
		spiral_debug =       (int) gd.getNextNumber();
		search_step=               gd.getNextNumber();
		pmtch_use_affine=          gd.getNextBoolean();
		max_std=                   gd.getNextNumber();
		min_std_rad=               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();
		
		if (use_existing_pair) {
			if (invert_exixting_pair) {
				System.out.println("Both direct and inverted matches are selected, using direct match");
			}
			return pairwiseOrthoMatch;
		} else if (invert_exixting_pair) {
			double [] enuOffset = maps_collection.ortho_maps[gpu_pair[0]].enuOffsetTo(maps_collection.ortho_maps[gpu_pair[1]]);
			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
			// create inverted pairwiseOrthoMatch - move to PairwiseOrthoMatch
			return inv_match.getInverse(rd);
		}
		double [][] affine0 = {{1,0,0},{0,1,0}}; // will always stay the same
		double [][] affine1 = {{1,0,0},{0,1,0}}; // here (manual mode) start from the center, may use prediction in auto
		double [][][] affines = new double[][][] {affine0,affine1};
		pairwiseOrthoMatch = maps_collection.SpiralMatch (
				clt_parameters,  // CLTParameters    clt_parameters,
				frac_remove,     // double           frac_remove, //  =        0.25
				metric_error,    // 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,
				gpu_pair,        // 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,
				null,//double []        max_rms_iter, //  = {1.0, 0.6};//
				1.0, // 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){
		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(String.format("%d\t%d", gpu_pair[0], gpu_pair[1]));
			if ((pairwiseOrthoMatch != null) && !Double.isNaN(pairwiseOrthoMatch.rms)) {
				sb.append(String.format("\t%d\t%d\t%6.4f\t%d\n",
						pairwiseOrthoMatch.nxy[0], pairwiseOrthoMatch.nxy[1], pairwiseOrthoMatch.rms, pairwiseOrthoMatch.zoom_lev));
			} else {
				sb.append("\tFAILED");
			}
			CalibrationFileManagement.saveStringToFile (
					log_path,          //String path,
					sb.toString(), // data,
					true); // boolean append)
		}
		return pairwiseOrthoMatch;
	}
	
	public static boolean removeDuplicateScenes(
			OrthoMapsCollection maps_collection) {
		String duplicate_name=null;
		do {
			HashSet<String> scene_names = new HashSet<String>();
			duplicate_name=null;
			for (int i = 0; i < maps_collection.ortho_maps.length; i++) {
				String name = maps_collection.ortho_maps[i].getName();
				if (scene_names.contains(name)) {
					duplicate_name=name;
					break;
				}
				scene_names.add(name);
			}
			if (duplicate_name != null) {
				long latest_modified = -1;
				int latest_index = -1;
				ArrayList<Integer> duplicates = new ArrayList<Integer>();
				for (int i = 0; i < maps_collection.ortho_maps.length; i++) {
					if (maps_collection.ortho_maps[i].getName().equals(duplicate_name)) {
						duplicates.add(i);
						long mod_ts = (new File(maps_collection.ortho_maps[i].getPath())).lastModified();
						if (mod_ts > latest_modified) {
							latest_index = i;
						}
					}
				}
				if (duplicates.size()> 1) {
					System.out.println("removeDuplicateScenes(): found " +duplicates.size()+ " duplicates for scene "+duplicate_name+":");
					for (int i :duplicates) {
						System.out.println(((i==latest_index)?"  KEEP ":"REMOVE ")+maps_collection.ortho_maps[i].getPath());
					}
					
					// combine pairwise matches, remove self-paired
					HashMap <String, PairwiseOrthoMatch>        combo_pairwise_matches = new HashMap <String, PairwiseOrthoMatch>();
					OrthoMap [] new_maps = new OrthoMap[maps_collection.ortho_maps.length-duplicates.size()+1];
					int indx = 0;
					int new_indx = -1;
					for (int i = 0; i < maps_collection.ortho_maps.length; i++) {
						if (duplicates.contains(i)) {
							for (String key:maps_collection.ortho_maps[i].pairwise_matches.keySet()) {
								if (!key.equals(duplicate_name) && !combo_pairwise_matches.containsKey(key)) {
									combo_pairwise_matches.put(key, maps_collection.ortho_maps[i].pairwise_matches.get(key));
								}
							}
						}
						if (!duplicates.contains(i) || (i == latest_index)) {
							
							if (i == latest_index) {
								new_indx = indx;
							}
							new_maps[indx++] = maps_collection.ortho_maps[i];
						}
					}
					maps_collection.ortho_maps = new_maps;
					maps_collection.ortho_maps[new_indx].pairwise_matches = combo_pairwise_matches;
					// remove pairs with (now) itself 
				} else {
					System.out.println("removeDuplicateScenes(): BUG - number of duplicates = "+duplicates.size()+", aborting.");
					return false;
				}
				// find latest file
			}
			
		} while (duplicate_name != null);
		maps_collection.reindex();
		return true;
	}
	

	
	public static boolean updateBlBcFileNames(
			String suffix,
			String before,
			OrthoMapsCollection maps_collection) {
		String [] filenames = new String [maps_collection.ortho_maps.length];
		for (int i = 0; i < filenames.length; i++) {
			filenames[i] = maps_collection.ortho_maps[i].getFileName(); // getName();
		}
		String [] old_names = filenames.clone();
		for (int i = 0; i < filenames.length; i++) {
			int insert_indx = filenames[i].indexOf(before);
			if (insert_indx < 0) {
				System.out.println("updateBlBcFileNames(): no substring '"+
						before+"' in filename "+filenames[i]);
				return false;
			}
			String stail =  filenames[i].substring(insert_indx);
			String prefix = filenames[i].substring(0, insert_indx);
			if (prefix.endsWith("-BL") || prefix.endsWith("-BC")) {
				prefix = prefix.substring(0, insert_indx - "-BC".length());
			}
			filenames[i] = prefix + suffix + stail;
		}
		for (int i = 0; i < filenames.length; i++) {
			maps_collection.ortho_maps[i].setFileName(filenames[i]);
			String path = maps_collection.ortho_maps[i].getPath();
			if (!(new File(path)).exists()) {
				System.out.println("updateBlBcFileNames() does not exist - fix manually: "+path);
				System.out.println("This can happen if files have different resolution for new run.");
				System.out.println("Verify path, then modify filenames[i], break at line above: 'maps_collection.ortho_maps[i].setFileName(filenames[i])'.");
				System.out.println("Reduce i by 1 and continue");
			} else {
				maps_collection.ortho_maps[i] = new OrthoMap(path);
			}
		}		
		return true;
	}
	
	
		/*
    	double [][] affine1 = {
    			{1,0,0},
    			{0,1,0}};

//    	affine1[0][2] = 0.64; affine1[1][2] = 1.253; // {"1697877412_004148", "1697877522_274211"}; //
		// (346.250-348.000)/12.5,(277.000-269.250)/12.5 = (-0.14, 0.62)
//    	affine1[0][2] =-0.14;  affine1[1][2] =  0.62;   // {"1697877409_353265", "1697877518_773045"}; //
//    	affine1[0][2] =-1.44;  affine1[1][2] =  1.653;  //"1697877465_672024", "1697877563_587972"
//    	affine1[0][2] =-0.25;  affine1[1][2] = -1.627;  //{"1697877465_672024", "1697878995_797332"};
//    	affine1[0][2] =-1.08;  affine1[1][2] =  3.267;  //{"1697877465_672024", "1697877528_776377"};        	
//    	affine1[0][2] =-1.2;   affine1[1][2] = -0.9066; //{"1694564245_111645", "1694564701_230240"};
//    	affine1[0][2] = 0.933; affine1[1][2] = -2.2667; //{"1694564245_111645", "1694564822_637346"};
//    	affine1[0][2] = 2.08;  affine1[1][2] = -0.093;  //{"1694564248_145989", "1694564819_336247"};
//    	affine1[0][2] =-1.88;  affine1[1][2] = -2.71;   //{"1694564269_536448", "1694564822_637346"};
//    	affine1[0][2] =-2.7;   affine1[1][2] = -0.59;   //{"1694564269_536448", "1694565541_040640"}; 50m-100m
//    	affine1[0][2] =-0.78;  affine1[1][2] =  2.1;    //{"1694564822_637346", "1694565541_040640"}; 75m-100m #75-#95
//    	affine1[0][2] =-0.92;  affine1[1][2] =  2.28;   //{"1694564263_701171", "1694564816_468625"}; 50m-75m
//    	affine1[0][2] =-5.37;  affine1[1][2] = -1.53;   //{"1694564266_568793", "1694564819_336247"}; 50m-75m
//    	affine1[0][2] = 1.92;  affine1[1][2] = -1.89;   //{"1694564270_086631", "1694564822_637346"}; 50m-75m
//    	affine1[0][2] = 0.52;  affine1[1][2] = -2.387;  //{"1694564272_770858", "1694564778_589341"}; 50m-75m #13-#63
//    	affine1[0][2] =-4.75;  affine1[1][2] = 0.44;    //{"1694564275_538447", "1694564744_411290"}; 50m-75m #14-#58
    	affine1[0][2] = 2.59;  affine1[1][2] = 0.133;   //{"1694564270_086631", "1694563011_045597"}; 50m, 25m #88-#68
//	String [] gpu_spair = {"1694564270_086631", "1694563011_045597"};  // Morning Sep, 13 50m, 25m #88-#68

//"1694564275_538447","1694564744_411290"  
			 */     	
		
        /*
adjusted affines[1] for a pair: 1697877465_672024/1697877563_587972 RMS= 0.14511264718699468 (0.14577676900752629)
[[1.0065661054211072,0.009234141675959222,-1.4706666954418992],
 [-0.004304610680434175,1.0012437786888277,1.6394608778426498]]
 
adjusted affines[1] for a pair: 1694564245_111645/1694564701_230240
[[1.021246208683918,0.006667337255903127,-1.0137566435443375],
 [-0.010035351748801541,1.0294363088409202,-0.8395952637299295]]

adjusted affines[1] for a pair: 1694564245_111645/1694564822_637346
[[1.0148318094351692,0.01611699521114023,1.1534804399270817],
 [-0.025584298199020607,1.0182605131307008,-2.157143969635918]]
 
 correlateOrthoPair(): adjusted affines[1] {"1694564269_536448", "1694564822_637346"}
[[1.0118405178770247,0.0243306610630595,-1.6570450812227402],
 [-0.030053577777794035,1.007846539173136,-2.564940874119216]]
 
adjusted affines[1] for a pair: 1694564269_536448/1694565541_040640
[[1.032352933444291,0.012425600945003055,-2.6012366590312457],
 [-0.032390806278202705,1.045595625691147,0.4724083642132655]]

 Done
adjusted affines[1] for a pair: 1694564263_701171/1694564816_468625 (9- 73)
[[1.015002848762524,0.016906877812481423,-0.827920765016369],
 [-0.016806089590596436,1.0126951556271355,2.453634727273915]]

adjusted affines[1] for a pair: 1694564266_568793/1694564819_336247
[[1.0208243680493612,-0.007571648582186553,-5.627387091202717],
 [0.00716961393885021,1.0116421129852677,-1.4359711787904814]]
 
 adjusted affines[1] for a pair: 1694564270_086631/1694564822_637346
[[1.01553064725059,-0.004358499034078345,1.9313585652985712],
 [0.006088341815143438,1.0077680445468122,-1.7817699129643216]]

adjusted affines[1] for a pair: 1694564272_770858/1694564778_589341
[[1.0120132362035976,0.03369140870847741,0.7958782949644478],
 [-0.03200874269606805,1.0088673434818738,-2.211451677912688]]
 
Bad, even with narrow WOI:
Done
adjusted affines[1] for a pair: 1694564275_538447/1694564744_411290
[[1.016644878296508,0.004929435606711805,-4.693221236458512],
 [-0.0049692732545438024,1.0068633692157516,0.3968458715663825]]

adjusted affines[1] for a pair: 1694564291_293695/1694564778_589341
[[1.0192476964620238,0.028926271825940476,4.945477523680564],
 [-0.03767671444357569,1.0214533434667925,-1.4730239331163375]]

         */
	
	public static double [][] interpolateVectorField(
			final double [][] vector_field, // sparse {vx,vy,strength}
			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 Rectangle tile_woi = OrthoMapsCollection.scaleRectangle (woi, GPUTileProcessor.DTT_SIZE);
		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 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);
        // 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,
    				null,                 // 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);          // int       debug_level)
    	}		
		if (debugLevel > 1) {
			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);
		}
		
		
		
		return vf_filled;
	}
	
	public static PairwiseOrthoMatch getPairFromSelectedImage(
			ImagePlus imp,
			String [] names,
			String [][] all_scenes) { // to verify the same scenes as expected
		// get zoom from the name and list of the slice names prefixes (before second "_"
		String title = imp.getTitle();
		String[] tokens = title.replace(".","_").split("_");
		for (int i = 0; i < tokens.length; i++) {
			if (tokens[i].startsWith("zoom")) {
				int zoom_level = Integer.parseInt(tokens[i].substring(4));
				System.out.println("zoom level = "+zoom_level);
				double pix_size = OrthoMap.getPixelSizeMeters (zoom_level); // meters
				ImageStack stack_scenes = imp.getStack();
				int nSlices = stack_scenes.getSize();
				String [] scene_names = new String [nSlices];
				for (int n = 0; n < scene_names.length; n++) {
					scene_names[n] = stack_scenes.getSliceLabel(n+1).substring(0,17); // wrong image opened-> StringIndexOutOfBoundsException: begin 0, end 17, length 8
				}
				if (all_scenes != null) {
					all_scenes[0] = scene_names;
				}
				PointRoi pRoi = (PointRoi) imp.getRoi();
				FloatPolygon fp = pRoi.getContainedFloatPoints();
				// Use only 2 last markers
				if (fp == null) {
					System.out.println("getPairFromSelectedImage(): no markers found");
					return null;
				}
				if (fp.npoints < 2) {
					System.out.println("getPairFromSelectedImage(): need at least2 markers");
					return null;
				}
				int pair = 0;
				boolean flip_pair = false;
				if (fp.npoints > 2)  { // select pair
					int [][] pairs = new int [fp.npoints/2][2];
					for (int j = 0; j < pairs.length; j++) {
						for (int k = 0; k < 2; k++) {
							pairs[j][k] = pRoi.getPointPosition(2*j+k);
						}
					}
					String [] choices = getPairChoices(
							pairs, // int [][] pairs,
							scene_names); // String [] names)

					GenericJTabbedDialog gds = new GenericJTabbedDialog("Select image pair from the image",1200,400);
					gds.addChoice("Image pair in the marked image:", choices, choices[choices.length-1]);
					gds.addCheckbox    ("Flip order", false, "Match first to second.");
					gds.showDialog();
					if (gds.wasCanceled()) return null;
					pair= gds.getNextChoiceIndex();
					flip_pair = gds.getNextBoolean();
				}
				
				
				double [][] points_xy= new double [2][2];
				int [] pair_slices = new int[2];
				for (int nn = 0; nn < points_xy.length; nn++) {
					int n= flip_pair? (points_xy.length-nn-1) : nn;
					int p = 2 * pair + n; // fp.npoints-2+n;
					points_xy[nn][0] = fp.xpoints[p];
					points_xy[nn][1] = fp.ypoints[p];
					pair_slices[nn] = pRoi.getPointPosition(p); // works
				}
//				PairwiseOrthoMatch match= new PairwiseOrthoMatch();
				double [][] affine = {
					{1,0,(points_xy[1][0]-points_xy[0][0])*pix_size},
					{0,1,(points_xy[1][1]-points_xy[0][1])*pix_size}};
				PairwiseOrthoMatch match= new PairwiseOrthoMatch(
						affine, // double [][] affine,
						new double [6][6], // double [][] jtj,
						Double.NaN, // double rms,
						zoom_level, // int zoom_lev,
						1.0); // double overlap);
				for (int n = 0; n < points_xy.length; n++) {	
					names[n] = stack_scenes.getSliceLabel(pair_slices[n]).substring(0,17);
				}
				System.out.println("names = [\""+names[0]+"\", \""+names[1]+"\"]");
				System.out.println("match.affine = [["+affine[0][0]+", "+affine[0][1]+", "+affine[0][2]+"],");
				System.out.println("                ["+affine[1][0]+", "+affine[1][1]+", "+affine[1][2]+"]]");
				return match;
			}
		}
		return null;
	}

	public static ArrayList<ObjectLocation> getObjectsFromSelectedImage(
			ImagePlus imp,
			int [] zoom_levp) {
		// get zoom from the name and list of the slice names prefixes (before second "_"
		String title = imp.getTitle();
		String[] tokens = title.replace(".","_").split("_");
		for (int i = 0; i < tokens.length; i++) {
			if (tokens[i].startsWith("zoom")) {
				int zoom_level = Integer.parseInt(tokens[i].substring(4));
				System.out.println("zoom level = "+zoom_level);
				double pix_size = OrthoMap.getPixelSizeMeters (zoom_level); // meters
				ImageStack stack_scenes = imp.getStack();
				int nSlices = stack_scenes.getSize();
				String [] scene_names = new String [nSlices];
				for (int n = 0; n < scene_names.length; n++) {
					scene_names[n] = stack_scenes.getSliceLabel(n+1);
					if (scene_names[n].length() >= 17) {
						scene_names[n] = scene_names[n].substring(0,17);
					}
				}
				PointRoi pRoi = (PointRoi) imp.getRoi();
				FloatPolygon fp = pRoi.getContainedFloatPoints();
				// Use only 2 last markers
				if (fp == null) {
					System.out.println("getPairFromSelectedImage(): no markers found");
					return null;
				}
				ArrayList<ObjectLocation> list = new ArrayList<ObjectLocation>();
				for (int np = 0; np < fp.npoints; np++) {
					String name = scene_names[pRoi.getPointPosition(np) -1];
					double [] xy_meters = {
							fp.xpoints[np] * pix_size, 
							fp.ypoints[np] * pix_size};
					list.add(new ObjectLocation (
							name,        // String name,
							xy_meters)); // double [] xy_meters)
				}
				if (zoom_levp != null) {
					zoom_levp[0] = zoom_level;
				}
				return list;
			}
		}
		return null;
	}
	
	
	public static String [] getPairChoices(
			int [][] pairs,
			String [] names) {
		String [] choices = new String[pairs.length];
		for (int i =0; i < choices.length; i++) {
			choices[i] = String.format("%3d -> %3d (%s -> %s)", pairs[i][0],pairs[i][1],names[pairs[i][0]], names[pairs[i][1]]);
		}
		return choices;
	}
	
	
	/**
	 * Calculate single-tile and neighbors vector fields
	 * @param clt_parameters meta parameters
	 * @param fpixels     {reference, other}, each img_width * img_height(calculated)
	 * @param img_width   image width 
	 * @param woi         image window to process,  if null, use full GPU window
	 * @param affine      [2][2][3] affine coefficients to translate common to 2 images
	 * @param tp_tasks_o  null or TpTask [2][] - will get copy of tasks for both images (different, centers, same tasks)
	 * @param batch_mode  avoid graphics debug in batch mode
	 * @param dbg_suffix  for image_names, such as batch_mode? null: String.format("_%02d", ntry);
	 * @param debugLevel  debug level
	 * @return [2:neibs/single][tilesX*tilesY][3:x,y,str]
	 */
	public static double [][][] rectilinearVectorField( // scene0/scene1
			final CLTParameters          clt_parameters,
			final float  [][]            fpixels, // to check for empty
			final int                    img_width,
			Rectangle                    woi, // if null, use full GPU window
			final double [][][]          affine,  // [2][2][3] affine coefficients to translate common to 2 images
			TpTask [][]                  tp_tasks_o,
			final boolean                batch_mode,
			final String                 dbg_suffix, // for image_names
			final int                    debugLevel) {
		int [] wh = {img_width, fpixels[0].length/img_width};
		TpTask [][] tp_tasks = GpuQuad.setRectilinearInterTasks(
				fpixels, // final float  [][]   fpixels, // to check for empty (no processing where it images have NaN
				img_width, // final int           img_width,
				woi,                     // Rectangle           woi,
				affine); // final double [][][] affine  // [2][2][3] affine coefficients to translate common to 2 images
		if (tp_tasks_o != null) {
			for (int i = 0; i < tp_tasks_o.length; i++) tp_tasks_o[i] = tp_tasks[i];
		}
		boolean is_aux =  true;
		boolean is_mono = true;
		boolean is_lwir = true;
		int     erase_cltr=-1;
		int     erase_clt= -1;
		ImageDtt image_dtt = new ImageDtt(
				1, // int numSensors,
				clt_parameters.transform_size,
				clt_parameters.img_dtt,
				is_aux, // ref_scene.isAux(),
				is_mono,  // ref_scene.isMonochrome(),
				is_lwir,
				clt_parameters.getScaleStrength(is_aux), // maybe something else
				GPU_QUAD_AFFINE);
		image_dtt.setRectilinearReferenceTD(
				erase_cltr,                  // final int                 erase_clt,
				fpixels[0], // final float []            fpixels_ref,
				wh,                         // final int []              wh,               // null (use sensor dimensions) or pair {width, height} in pixels
				clt_parameters.img_dtt,     // final ImageDttParameters  imgdtt_params,    // Now just extra correlation parameters, later will include, most others
				true,                       // final boolean             use_reference_buffer,
				tp_tasks[0],                // final TpTask[]            tp_tasks,
				clt_parameters.gpu_sigma_r, // final double              gpu_sigma_r,     // 0.9, 1.1
				clt_parameters.gpu_sigma_b, // final double              gpu_sigma_b,     // 0.9, 1.1
				clt_parameters.gpu_sigma_g, // final double              gpu_sigma_g,     // 0.6, 0.7
				clt_parameters.gpu_sigma_m, // final double              gpu_sigma_m,     //  =       0.4; // 0.7;
				batch_mode? -3: debugLevel);                // final int                 globalDebugLevel)
    	int tilesX =  img_width/GPUTileProcessor.DTT_SIZE;
    	int tilesY =  wh[1]/GPUTileProcessor.DTT_SIZE;
    	if (!batch_mode && (dbg_suffix != null)) {
    		renderFromTD (
    				true, // boolean             use_reference,
    				"ref"+dbg_suffix); //String              suffix
    	}
		float  [][][][]     fcorr_td = new float [tilesY][tilesX][][];  
				//null; // no accumulation, use data in GPU
		final double gpu_sigma_corr =     clt_parameters.getGpuCorrSigma(image_dtt.isMonochrome());
		final double gpu_sigma_rb_corr =  image_dtt.isMonochrome()? 1.0 : clt_parameters.gpu_sigma_rb_corr;
		final double gpu_sigma_log_corr = clt_parameters.getGpuCorrLoGSigma(image_dtt.isMonochrome());
		image_dtt.interRectilinearCorrTD(
				clt_parameters.img_dtt,     //final ImageDttParameters  imgdtt_params,    // Now just extra correlation parameters, later will include, most others
				batch_mode,                 // final boolean batch_mode,
				erase_clt,                  // final int                 erase_clt,
				fpixels[1],                 // final float []            fpixels,
				wh,                         // final int []              wh,               // null (use sensor dimensions) or pair {width, height} in pixels
				tp_tasks[1],                // final TpTask[]            tp_tasks,
				fcorr_td,                   // final float  [][][][]     fcorr_td,        // [tilesY][tilesX][pair][4*64] transform domain representation of 6 corr pairs
				clt_parameters.gpu_sigma_r, // final double              gpu_sigma_r,     // 0.9, 1.1
				clt_parameters.gpu_sigma_b, // final double              gpu_sigma_b,     // 0.9, 1.1
				clt_parameters.gpu_sigma_g, // final double              gpu_sigma_g,     // 0.6, 0.7
				clt_parameters.gpu_sigma_m, // final double              gpu_sigma_m,     //  =       0.4; // 0.7;
				gpu_sigma_rb_corr,          // final double              gpu_sigma_rb_corr,    //  = 0.5; // apply LPF after accumulating R and B correlation before G, monochrome ? 1.0 :
				gpu_sigma_corr,             // final double              gpu_sigma_corr,       //  =    0.9;gpu_sigma_corr_m
				gpu_sigma_log_corr,         // final double              gpu_sigma_log_corr,   // hpf to reduce dynamic range for correlations
				clt_parameters.corr_red,    // final double              corr_red, // +used
				clt_parameters.corr_blue,   // final double              corr_blue,// +used
				-1,                         // final int                 sensor_mask_inter, // The bitmask - which sensors to correlate, -1 - all.
				debugLevel);                // final int                 globalDebugLevel)
		if (!batch_mode && (dbg_suffix != null)) {
			ComboMatch.renderFromTD (
					false, // boolean             use_reference,
					"img"+dbg_suffix); //String              suffix
		}
//~ 		coord_motion = image_dtt.clt_process_tl_interscene(       // convert to pixel domain and process correlations already prepared in fcorr_td and/or fcorr_combo_td
// simplified version?
		// get TD interscene correlation of 2 scenes, use only combo (all channels) data
		TDCorrTile [] corr_tiles = TDCorrTile.getFromGpu(
				GPU_QUAD_AFFINE);
		
// depends on zoom level		
		double  neib_radius =      clt_parameters.imp.rln_neib_radius;
		boolean rln_neibs_fill =   clt_parameters.imp.rln_neibs_fill;		
		double  rln_fat_zero =     clt_parameters.imp.rln_fat_zero;
		boolean rln_use_neibs =    clt_parameters.imp.rln_use_neibs;
		double  rln_cent_radius =  clt_parameters.imp.rln_cent_radius;
		int     rln_n_recenter =   clt_parameters.imp.rln_n_recenter;
		double  rln_sngl_rstr =    clt_parameters.imp.rln_sngl_rstr;
		double  rln_neib_rstr =    clt_parameters.imp.rln_neib_rstr;
		
		double max_neib_radius = Math.min(woi.width, woi.height)/GPUTileProcessor.DTT_SIZE * clt_parameters.imp.rln_radius_frac;
		if (neib_radius > max_neib_radius) {
			neib_radius = max_neib_radius;
		}

		
		
		
		double [][][] corr_tiles_pd = new double [(neib_radius>0)? 2: 1][][];
		// use TDCorrTile.calcNeibs() here to get 8-neighbors
		corr_tiles_pd[0] = TDCorrTile.convertTDtoPD(
				GPU_QUAD_AFFINE,     // final GpuQuad       gpuQuad,
				corr_tiles,          // final TDCorrTile [] tiles,
				0xFE,                // final int           corr_type, // 0xFE
				rln_fat_zero,       // final double        gpu_fat_zero,
				debugLevel);         // final int           debug_level
		if (rln_use_neibs && (neib_radius > 0)) {
			TDCorrTile [] neibs_TD = TDCorrTile.calcNeibs(
					corr_tiles,      // TDCorrTile [] tiles,
					tilesX,          // final int     tilesX,
					neib_radius,     // final double  radius
					rln_neibs_fill);    // final boolean process_all			
					
			corr_tiles_pd[1] = TDCorrTile.convertTDtoPD(
					GPU_QUAD_AFFINE, // final GpuQuad       gpuQuad,
					neibs_TD,        // final TDCorrTile [] tiles,
					0xFE,            // final int           corr_type, // 0xFE
					rln_fat_zero,    // final double        gpu_fat_zero,
					debugLevel);     // final int           debug_level
		}
		double [][][] vector_field = new double [corr_tiles_pd.length][][];
		vector_field[0] = TDCorrTile.getMismatchVector( // full tiles in gpu (512*512)
				corr_tiles_pd[0], // final double[][] tiles,
				rln_sngl_rstr,    // double              rmax, 
				rln_cent_radius,  // final double        centroid_radius, // 0 - all same weight, > 0 cosine(PI/2*sqrt(dx^2+dy^2)/rad)
				rln_n_recenter,   // final int           n_recenter); //  re-center window around new maximum. 0 -no refines (single-pass)
				false); // final boolean     calc_fraction	){ // calculate fraction inside center circle
		if (corr_tiles_pd.length > 1) {
			vector_field[1] = TDCorrTile.getMismatchVector(
					corr_tiles_pd[1], // final double[][] tiles,
					rln_neib_rstr,    // double              rmax, 
					rln_cent_radius,  // final double        centroid_radius, // 0 - all same weight, > 0 cosine(PI/2*sqrt(dx^2+dy^2)/rad)
					rln_n_recenter,   // final int           n_recenter); //  re-center window around new maximum. 0 -no refines (single-pass)
					false); // final boolean     calc_fraction	){ // calculate fraction inside center circle
		}
    	boolean show_vector_field = (debugLevel>100); // true;
    	boolean show_2d_correlations = (debugLevel>0); // true;
    	final int corr_size = 2 * GPUTileProcessor.DTT_SIZE -1;  
		if (show_2d_correlations) {
    		double [][] dbg_2d_corrs = ImageDtt.corr_partial_dbg( // not used in lwir
    				corr_tiles_pd,                       // final double [][][]     corr_data,       // [layer][tile][(2*transform_size-1)*(2*transform_size-1)] // if null - will not calculate
    				tilesX,                              // final int               tilesX,
    				corr_size,                           //final int               corr_size,       // 15
    				clt_parameters.corr_border_contrast, // final double            border_contrast,
    				debugLevel);                         // final int               globalDebugLevel)
    		String [] dbg_titles= {"raw","neibs"};
    		ShowDoubleFloatArrays.showArrays(
    				dbg_2d_corrs,
    				tilesX * (corr_size + 1),
    				tilesY * (corr_size + 1),
    				true,
    				"corr2d",
    				dbg_titles);
    	}
		
		if (show_vector_field) {
			double [][] dbg_vf = new double [3 * vector_field.length][tilesX * tilesY];
			String [] dbg_titles = new String[dbg_vf.length];
			String [] prefix= {"single","neibs"};
			for (int n = 0; n < vector_field.length; n++) {
				dbg_titles [3*n+0] = prefix[n]+"-vx";	
				dbg_titles [3*n+1] = prefix[n]+"-vy";	
				dbg_titles [3*n+2] = prefix[n]+"-str";	
			}
			for (int i = 0; i < dbg_vf.length; i++) {
				Arrays.fill(dbg_vf[i], Double.NaN);
				
			}
			for (int t=0; t<dbg_vf[0].length; t++) {
				for (int n = 0; n < vector_field.length; n++) {
					if (vector_field[n][t] != null) {
						for (int k = 0; k < 3; k++) {
							dbg_vf[n*3 + k][t] = vector_field[n][t][k];
						}
					}
				}
			}
    		ShowDoubleFloatArrays.showArrays(
    				dbg_vf,
    				tilesX,
    				tilesY,
    				true,
    				"vector_field",
    				dbg_titles);
		}
		
		
		return vector_field; // corr_tiles;
	}
	
	
	
	
	
	
	public static boolean openTestPair() {
		String [] image_paths = {
				"/media/elphel/SSD3-4GB/lwir16-proc/berdich3/linked/linked_compare/02/1697877410_420287-RECT-PIX0.01-FLAT_CLN-GEO0.tiff",
				"/media/elphel/SSD3-4GB/lwir16-proc/berdich3/linked/linked_compare/02/1697877412_004148-RECT-PIX0.01-FLAT_CLN-GEO0.tiff"
		};
		
		double [][][] image_points = {
				{   { 487.0,   637.0}, 
					{2237.5,   932.5},
					{2004.5,   1929.5},
					{ 320.5,   1951.500}
				},{
					{ 629.0,   179.0},
					{2376.5,   479.5},
					{2139.833, 1474.5},
					{ 461.5,   1493.5}}};
		boolean set_coords = true; // false;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select processing moder",1090,900);
		gd.addStringField ("First image path",  image_paths[0], 120, "First image full path");		
		gd.addStringField ("Second image path", image_paths[1], 120, "Second image full path");		
		gd.addCheckbox    ("Set image markers to following coordinates", set_coords, "Delete markers and set them according to data.");
		for (int n = 0; n < image_points.length; n++) {
			for (int i = 0; i < image_points[n].length; i++) {
				gd.addMessage("image["+n+"] point "+i+":");
				gd.addNumericField("x =",              image_points[n][i][0],  3,7,"pix",
						"Marker "+i+ " for image "+n+" X-coordinate");
				gd.addNumericField("y =",              image_points[n][i][1],  3,7,"pix",
						"Marker "+i+ " for image "+n+" Y-coordinate");
			}
		}
		
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		
		image_paths[0] = gd.getNextString();
		image_paths[1] = gd.getNextString();
		set_coords =     gd.getNextBoolean();
		for (int n = 0; n < image_points.length; n++) {
			for (int i = 0; i < image_points[n].length; i++) {
				image_points[n][i][0] = gd.getNextNumber();
				image_points[n][i][1] = gd.getNextNumber();
			}
		}
		imp_src1 = new ImagePlus(image_paths[0]);
		imp_src2 = new ImagePlus(image_paths[1]);
		
		if (set_coords) {
			PointRoi [] rois = new PointRoi[image_points.length];
			for (int n = 0; n < image_points.length; n++) {
				rois[n] = new PointRoi();
				rois[n].setOptions("label");
				for (int i = 0; i < image_points[n].length; i++) {
					rois[n].addPoint(image_points[n][i][0],image_points[n][i][1],i+1);
				}
			}
			imp_src1.setRoi(rois[0]);
			imp_src2.setRoi(rois[1]);
		}
		
		imp_src1.show();
		imp_src2.show();
		if (!(imp_src1.getRoi() instanceof PointRoi) || !(imp_src2.getRoi() instanceof PointRoi)) {
			System.out.println("At least one iomage does not have PointRoi");
			return false;
		}
		FloatPolygon [] fp = {((PointRoi) imp_src1.getRoi()).getContainedFloatPoints(),
				((PointRoi) imp_src2.getRoi()).getContainedFloatPoints()};
		int expected_points = image_points[0].length;
		for (int n = 0; n < fp.length; n++) {
			if (fp[n].xpoints.length != expected_points) {
				System.out.println("openTestPair(): expecting "+expected_points+" points, image "+n+"has "+fp[n].xpoints.length);
				return false;
			}
		}
		double [][][] points_xy= new double [fp.length][fp[0].xpoints.length][2];
		for (int n = 0; n < points_xy.length; n++) {
			for (int i = 0; i < points_xy[n].length; i++) {
				points_xy[n][i][0] = fp[n].xpoints[i];
				points_xy[n][i][1] = fp[n].ypoints[i];
			}
		}
		System.out.println(String.format("%4s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s",
				"N","X0","Y0","X1","Y1", "length1", "length2", "ratio"));
		for (int i = 0; i < points_xy[0].length; i++) {
			int ip = (i - 1 + points_xy[0].length) % points_xy[0].length;
			double [] ll = new double[2];
			for (int n = 0; n < points_xy.length; n++) {
				double dx = points_xy[n][i][0]-points_xy[n][ip][0];
				double dy = points_xy[n][i][1]-points_xy[n][ip][1];
				ll[n] = Math.sqrt(dx*dx+dy*dy);
			}
			System.out.println(String.format("%4d\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.6f",
					i, points_xy[0][i][0], points_xy[0][i][1],points_xy[1][i][0], points_xy[1][i][1],
					ll[0], ll[1], ll[1]/ll[0]));
		}
		return true;
	}
	
	public static boolean testPair() {
		if (!(imp_src1.getRoi() instanceof PointRoi) || !(imp_src2.getRoi() instanceof PointRoi)) {
			System.out.println("At least one iomage does not have PointRoi");
			return false;
		}
		FloatPolygon [] fp = {((PointRoi) imp_src1.getRoi()).getContainedFloatPoints(),
				((PointRoi) imp_src2.getRoi()).getContainedFloatPoints()};
		int expected_points = 4; // image_points[0].length;
		for (int n = 0; n < fp.length; n++) {
			if (fp[n].xpoints.length != expected_points) {
				System.out.println("openTestPair(): expecting "+expected_points+" points, image "+n+"has "+fp[n].xpoints.length);
				return false;
			}
		}
		double [][][] points_xy= new double [fp.length][fp[0].xpoints.length][2];
		for (int n = 0; n < points_xy.length; n++) {
			for (int i = 0; i < points_xy[n].length; i++) {
				points_xy[n][i][0] = fp[n].xpoints[i];
				points_xy[n][i][1] = fp[n].ypoints[i];
			}
		}
		System.out.println(String.format("%4s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s",
				"N","X0","Y0","X1","Y1", "length1", "length2", "ratio"));
		for (int i = 0; i < points_xy[0].length; i++) {
			int ip = (i - 1 + points_xy[0].length) % points_xy[0].length;
			double [] ll = new double[2];
			for (int n = 0; n < points_xy.length; n++) {
				double dx = points_xy[n][i][0]-points_xy[n][ip][0];
				double dy = points_xy[n][i][1]-points_xy[n][ip][1];
				ll[n] = Math.sqrt(dx*dx+dy*dy);
			}
			System.out.println(String.format("%4d\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.6f",
					i, points_xy[0][i][0], points_xy[0][i][1],points_xy[1][i][0], points_xy[1][i][1],
					ll[0], ll[1], ll[1]/ll[0]));
		}
		
		double [][] ab = getAffineMatrix(
				points_xy[1], //  double [][] x,
				points_xy[0]); // double [][] y){
        double [][] a = {{ab[0][0],ab[0][1]},{ab[1][0],ab[1][1]}};
        double []   b = {ab[2][0],ab[2][1]};
        System.out.println("Matrix A:");
        System.out.println(String.format("%10.5f\t%10.5f", a[0][0], a[0][1]));
        System.out.println(String.format("%10.5f\t%10.5f", a[1][0], a[1][1]));
        System.out.println("Matrix B:");
        System.out.println(String.format("%10.5f", b[0]));
        System.out.println(String.format("%10.5f", b[1]));
        final int [] widths = {imp_src1.getWidth(),imp_src2.getWidth()}; 
        final int [] heights = {imp_src1.getHeight(),imp_src2.getHeight()}; 
        int [][][] corners = new int[2][4][2];
        for (int n =0; n < corners.length; n++) {
        	corners[n][0][0] = 0;
        	corners[n][0][1] = 0;
        	corners[n][1][0] = widths[n]-1;
        	corners[n][1][1] = 0;
        	corners[n][2][0] = widths[n]-1;
        	corners[n][2][1] = heights[n]-1;
        	corners[n][3][0] = 0;
        	corners[n][3][1] = heights[n]-1;
        }
        double [][] fcorners = new double[corners[1].length][2];
        int [][] xy_minmax = {{0, widths[0]-1},{0, heights[0]-1}};
        int [][] xy_minmax1 = new int[2][2];
        for (int i = 0; i < corners[1].length; i++) {
        	fcorners[i][0] = corners[1][i][0] * a[0][0]+ corners[1][i][1] * a[0][1] + b[0];
        	fcorners[i][1] = corners[1][i][0] * a[1][0]+ corners[1][i][1] * a[1][1] + b[1];
        	xy_minmax[0][0] = Math.min(xy_minmax[0][0], (int) Math.floor(fcorners[i][0]));
        	xy_minmax[0][1] = Math.max(xy_minmax[0][1], (int) Math.ceil(fcorners[i][0]));
        	xy_minmax[1][0] = Math.min(xy_minmax[1][0], (int) Math.floor(fcorners[i][1]));
        	xy_minmax[1][1] = Math.max(xy_minmax[1][1], (int) Math.ceil(fcorners[i][1]));
        	if (i == 0) {
        		xy_minmax1[0][0] = (int) Math.floor(fcorners[i][0]);
        		xy_minmax1[0][1] = (int) Math.ceil(fcorners[i][0]);
        		xy_minmax1[1][0] = (int) Math.floor(fcorners[i][1]);
        		xy_minmax1[1][1] = (int) Math.ceil(fcorners[i][1]);
        	} else {
        		xy_minmax1[0][0] = Math.min(xy_minmax1[0][0], (int) Math.floor(fcorners[i][0]));
        		xy_minmax1[0][1] = Math.max(xy_minmax1[0][1], (int) Math.ceil(fcorners[i][0]));
        		xy_minmax1[1][0] = Math.min(xy_minmax1[1][0], (int) Math.floor(fcorners[i][1]));
        		xy_minmax1[1][1] = Math.max(xy_minmax1[1][1], (int) Math.ceil(fcorners[i][1]));
        	}
        }
        final int x0 = Math.max(0, -xy_minmax[0][0]);
        final int y0 = Math.max(0, -xy_minmax[1][0]);
        final int width =  x0 + xy_minmax[0][1]+1;
        final int height = y0 + xy_minmax[1][1]+1;
        double [][] opix = new double [2][width*height];
        for (int n = 0; n < opix.length; n++) {
        	Arrays.fill(opix[n], Double.NaN);
        }
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int width0= widths[0];
		final int npix0 = width0*heights[0];
		final float [] pixels0 = (float []) imp_src1.getProcessor().getPixels();
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int ipix = ai.getAndIncrement(); ipix < npix0; ipix = ai.getAndIncrement()) {
                    	int y = ipix / width0;
                    	int x = ipix % width0;
                    	int p = x0 + x + (y0 + y)* width;
                    	opix[0][p] = pixels0[ipix];
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);

        final int x10 = x0+ xy_minmax1[0][0]; // output image coordiantes
        final int y10 = y0+ xy_minmax1[1][0]; // output image coordiantes
        final int width1 =  x0 + xy_minmax1[0][1] + 1;
        final int height1 = y0 + xy_minmax1[0][1] + 1;
        final int npix1 = width1 * height1; 
		double [][] ab1 = getAffineMatrix(
				points_xy[0], //  double [][] x,
				points_xy[1]); // double [][] y){
        final double [][] a1 = {{ab1[0][0],ab1[0][1]},{ab1[1][0],ab1[1][1]}};
        final double []   b1 = {ab1[2][0]-x0,ab1[2][1]-y0}; // from output coordinates to image2 coordinates
        System.out.println("Matrix A1:");
        System.out.println(String.format("%10.5f\t%10.5f", a1[0][0], a1[0][1]));
        System.out.println(String.format("%10.5f\t%10.5f", a1[1][0], a1[1][1]));
        System.out.println("Matrix B1:");
        System.out.println(String.format("%10.5f", b1[0]));
        System.out.println(String.format("%10.5f", b1[1]));
        ai.set(0);
        final int img2_width = widths[1];
        final int img2_height = heights[1];
		final float [] pixels1 = (float []) imp_src2.getProcessor().getPixels();
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int ipix = ai.getAndIncrement(); ipix < npix1; ipix = ai.getAndIncrement()) {
                    	int y = ipix / width1 + x10; // output image coordinates
                    	int x = ipix % width1 + y10; // output image coordinates
                    	int p = x + y * width;
                    	double fx = a1[0][0] * x + a1[0][1] * y + b1[0];
                    	double fy = a1[1][0] * x + a1[1][1] * y + b1[1];
                    	int ix = (int) Math.floor(fx);
                    	int iy = (int) Math.floor(fy);
                    	double dx = fx - ix;
                    	double dy = fy - iy;
                    	if ((ix >= 0) && (iy >= 0) && (ix < (img2_width -1)) && (iy < (img2_height -1))) {
                    		int indx0 = ix + iy * img2_width;
                    		double d00 = pixels1[indx0];
                    		double d01 = pixels1[indx0+1];
                    		double d10 = pixels1[indx0+img2_width];
                    		double d11 = pixels1[indx0+img2_width+1];
                    		opix[1][p] = d00*(1-dx)*(1-dy)+d01*dx*(1-dy)+d10*(1-dx)*dy+d11*dx*dy;
                    	}
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
        
        ShowDoubleFloatArrays.showArrays( 
        		opix,
        		width,
        		height,
        		true,
        		"overlap");

        
		return true;
	}
	
	public static double [][] getAffineMatrix(
			double [][] x,
			double [][] y){
		if (x.length != y.length) {
			System.out.println("getAffineMatrix(): x.length != y.length ("+x.length+" != "+y.length+")");
			return null;
		}
		/*
 Ui =  A00 * Xi + A01* Yi + B0
 Vi =  A10 * Xi + A11* Yi + B1
  
 A00^2*Xi^2 + A01^2*Yi^2 +B0^2 +Ui^2 + 2*A00*Xi*A01*Yi + 2*A00*Xi*B0 - 2*A00*Xi*Ui + 2*A01*Yi*B0 -2*A01*Yi*Ui - 2*B0*Ui
 d/2dA00 = A00*Xi^2 + A01*Xi*Yi + B0 *Xi - XiUi = 0
 d/2dA01 = A01*Yi^2 + A00*Xi*Yi + B0 *Yi - YiUi = 0
 d/2dB0 =  B0       + A00*Xi    + A01*Yi - Ui   = 0

 d/2dA10 = A10*Xi^2 + A11*Xi*Yi + B1 *Xi - XiVi = 0
 d/2dA11 = A11*Yi^2 + A10*Xi*Yi + B1 *Yi - YiVi = 0
 d/2dB1 =  B1       + A10*Xi    + A11*Yi - Vi   = 0
 
           SX2*A00 + SXY*A01 +   0*A10 +   0*A11 + SX*B0 +  0*B1  =  SXU
           SXY*A00 + SY2*A01 +   0*A10 +   0*A11 + SY*B0 +  0*B1  =  SYU
           SX *A00 + SY *A01 +   0*A10 +   0*A11 +  1*B0 +  0*B1  =  SU
           0  *A00 +  0 *A01 + SX2*A10 + SXY*A11 +  0*B0 + SX*B1  =  SXV
           0  *A00 +  0 *A01 + SXY*A10 + SY2*A11 +  0*B0 + SY*B1  =  SYV
           0  *A00 +  0 *A01 + SX *A10 + SY *A11 +  0*B0 +  1*B1  =  SV
        		 */
		double sx=0, sy=0, sx2=0, sy2=0, sxy=0,sxu=0,syu=0,sxv=0,syv=0,su=0,sv=0;
		for (int n = 0; n < x.length; n++) {
			sx += x[n][0];
			sy += x[n][1];
			sx2+= x[n][0]*x[n][0];
			sy2+= x[n][1]*x[n][1];
			sxy+= x[n][0]*x[n][1];
			
			sxu+= x[n][0]*y[n][0];
			syu+= x[n][1]*y[n][0];
			su+=  y[n][0];
			
			sxv+= x[n][0]*y[n][1];
			syv+= x[n][1]*y[n][1];
			sv+=  y[n][1];
		}
		double s0 = x.length;
		double [][] a = {
				{sx2, sxy,   0,   0, sx,  0},
				{sxy, sy2,   0,   0, sy,  0},
				{ sx,  sy,   0,   0, s0,  0},
				{  0,   0, sx2, sxy,  0, sx},
				{  0,   0, sxy, sy2,  0, sy},
				{  0,   0,  sx,  sy,  0, s0}};
		Matrix A = new Matrix(a);
//		A.print(15,4);
		double [][] b = {{sxu,syu,su,sxv,syv,sv}};
		Matrix B = new Matrix(b).transpose();
//		B.print(15,4);
		double [] v = A.solve(B).getRowPackedCopy();
		double [][] ab = {
				{v[0], v[1]},   // |a00, a01|,
				{v[2], v[3]},   // |a10, a11|,
				{v[4], v[5]}};  // | b0,  b1|,
		return ab;	
	}
	
	public static boolean testReadTiff() {
		String image_paths="";
		
		
		return true;
	}

	/**
	 * Pad images to fixed dimensions for GPU processing. If no zoom out
	 * is required - just copy, otherwise scale down towards top-left corner.
	 * All unused data is set to NaN
	 * 
	 * @param spix    source float image
	 * @param swidth  width of the source image
	 * @param zoomout zoom out level (binary logarithm), so 1 is zoom out twice
	 * @param width   width of the output image (multiple of 16?)
	 * @param height  height of the output image (multiple of 16?)
	 * @return padded (optionally scaled) image of width*height size
	 */
	
	public static float [] padScaleImage(
			float [] spix,
			int      swidth,
			int      zoomout,
			int      width,
			int      height) {
		float [] opix = new float [width*height];
		final int sheight = spix.length/swidth;
		int cwidth = Math.min(swidth,  width);
		Arrays.fill(opix, Float.NaN);
		if (zoomout == 0) {
			int cheight = Math.min(height, sheight);
			for (int line = 0; line < cheight; line++) {
				System.arraycopy(
						spix,
						line*swidth,
						opix,
						line*width,
						cwidth);
			}
		} else {
			int rscale = 1;
			for (int i = 0; i < zoomout;i++) {
				rscale *= 2;
			}
			final int frscale = rscale;
			final int tilesX = (swidth+frscale-1)/frscale;
			final int tilesY = (sheight+frscale-1)/frscale;
			final int tiles = tilesX*tilesY;
			final double [][] wnd = new double [frscale] [frscale];
			for (int i = 0; i <frscale; i++) {
				for (int j = 0; j <frscale; j++) {
					wnd[i][j] = Math.sin((i+0.5)/frscale*Math.PI);
				}
			}
			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 tile = ai.getAndIncrement(); tile < tiles; tile = ai.getAndIncrement()) {
	                    	int tileX = tile%tilesX;
	                    	int tileY = tile/tilesX;
	                    	if ((tileX < width) && (tileY < height)) {
	                    		int tileW= (frscale*(tileX+1) <= swidth) ? frscale:(swidth - frscale*tileX);  
	                    		int tileH= (frscale*(tileY+1) <= sheight) ? frscale:(sheight - frscale*tileY);
	                    		double sw = 0.0, swd = 0.0;
	                    		for (int py = 0; py < tileH; py++) {
	                    			int ls = (tileY*frscale + py)*swidth;
	                    			for (int px = 0; px < tileW; px++) {
	                    				double d = spix[ls + tileX*frscale+px];
	                    				if (!Double.isNaN(d)) {
	                    					double w = wnd[py][px];
	                    					sw += w;
	                    					swd += w*d;
	                    				}
	                    				if (sw > 0) {
	                    					opix[tileY * width + tileX] = (float) (swd/sw);
	                    				}
	                    			}
	                    		}
	                    	}
	                    }
	                }
	            };
	        }		      
	        ImageDtt.startAndJoin(threads);
		}
		return opix;
	}
	
	public static void renderFromTD (
			boolean             use_reference,
			String              suffix
			) {
		
		GPU_QUAD_AFFINE.execImcltRbgAll(
        		(GPU_QUAD_AFFINE.num_colors <=1), // isMonochrome(),
        		use_reference,
				null); // wh); //int [] wh
		// get data back from GPU
		final float [][][] iclt_fimg = new float [GPU_QUAD_AFFINE.num_cams][][];
		for (int ncam = 0; ncam < iclt_fimg.length; ncam++) {
			iclt_fimg[ncam] = GPU_QUAD_AFFINE.getRBG(ncam); // updated window
		}
		
		int out_width =  GPU_QUAD_AFFINE.getImageWidth();//   + gpuQuad.getDttSize(); // 2022/05/12 removed margins from gpuQuad.getRBG(ncam);
		int out_height = GPU_QUAD_AFFINE.getImageHeight(); // + gpuQuad.getDttSize(); // 2022/05/12 removed margins from gpuQuad.getRBG(ncam);
		
        ShowDoubleFloatArrays.showArrays( 
        		iclt_fimg[0],
        		out_width,
        		out_height,
        		true,
        		suffix);
	}
	
	public static void fillRandomFloatNans(
			float [][] pixels,
			double scale) {
		float fscale = (float) scale;
		Random rnd=new Random();
		for (int n = 0; n < pixels.length;n++) {
			for (int i = 0; i <pixels[n].length; i++) if (Float.isNaN(pixels[n][i])){
				pixels[n][i] = rnd.nextFloat() * fscale;
			}
		}
	}
	
	public static boolean updateImportExport(
			String files_base,
			OrthoMapsCollection maps_collection) {
		if  (maps_collection == null) {
			throw new IllegalArgumentException ("maps_collection is not initialized!");
		}
		int  suffix_bc_bl_indx =   2;
		boolean update_files =           false;
		boolean update_lla =             false; // re-read file metadata
		boolean update_kernel_patterns = false;
		boolean update_bl_bc =           false;
		boolean export_affine =          false; // export per-scene affines
		boolean export_affine2 =         false; // export per-pair affines
		boolean overwrite_affine =       false;
		boolean import_affine =          false; // export per-scene affines
		boolean import_affine2 =         false; // export per-pair affines
		String files_list_path =         files_base+".list";
//		String orthoMapsCollection_path =omtch_img_set+".data";
		String affines_path =            files_base+".affines"; // for export per-scene affines
		String affines2_path =           files_base+".affines2"; // for export per-pair affines


		GenericJTabbedDialog gd = new GenericJTabbedDialog("Select Update, Import or Export operations",1200,900);
		gd.addStringField ("List file full path",        files_list_path, 150, "Path to read/update directories and GEO files.");     
		gd.addStringField ("Scene affines full path",    affines_path, 150, "Path to import/export scenes affines.");     
		gd.addStringField ("Pairwise affines full path", affines2_path, 150, "Path to import/export scenes pairs affines.");     
		gd.addCheckbox    ("Update scene files",         update_files,   "Re-read files as specified in .list file, possibly changing versions");
		gd.addCheckbox    ("Update files metadata",      update_lla,     "Re-read files metadata (if it was modified), needed to calculate AGL.");
		gd.addCheckbox    ("Update kernels/patterns",    update_kernel_patterns, "Re-read kernels and patterns from *.list file");
		gd.addCheckbox    ("Export scene affines",       export_affine,  "Export per-scene affines in text format for migration.");
		gd.addCheckbox    ("Export pairs affines",       export_affine2, "Export per-pair  affines in text format for migration.");
		
		gd.addCheckbox    ("Overwrite affines on import",overwrite_affine, "Overwrite existing non-trivial affines on import.");
		gd.addCheckbox    ("Import scene affines",       import_affine,  "Export per-scene affines in text format for migration.");
		gd.addCheckbox    ("Import pairs affines",       import_affine2, "Export per-pair  affines in text format for migration.");
		gd.addCheckbox    ("Update BC/BL suffix",        update_bl_bc,
				"Change source filenames to use -BC for bicubic, -BL - for bilinear, or empty - for old bilinear files");
		gd.addChoice("BL/BC suffix:",        
				SUFFIXES_BL_BC,
				SUFFIXES_BL_BC[suffix_bc_bl_indx],
				"Select interpolation mode of the source files: old bilinear (empty), bilinear (-BL), or bicubic (-BC)", 0);
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		files_list_path =           gd.getNextString();
		affines_path =              gd.getNextString();
		affines2_path =             gd.getNextString();
   		update_files=               gd.getNextBoolean();
		update_lla=                 gd.getNextBoolean();
		update_kernel_patterns=     gd.getNextBoolean();
		export_affine =             gd.getNextBoolean();
		export_affine2 =            gd.getNextBoolean();
		overwrite_affine =          gd.getNextBoolean();
		import_affine =             gd.getNextBoolean();
		import_affine2 =            gd.getNextBoolean();
		update_bl_bc=               gd.getNextBoolean();
		suffix_bc_bl_indx =         gd.getNextChoiceIndex();
		
		if (update_files) {
			maps_collection.updateFiles(files_list_path);
			maps_collection.updateNumberScenes(); // number of scenes in each sequence
		}
		if (update_lla) {
			System.out.println("Updating map files metadata");
			maps_collection.updateLLA();
//			maps_collection.updateNumberScenes(); It is very long
			maps_collection.updateSfmGain();
		}
		if (update_kernel_patterns) {
			maps_collection.updateKernels(files_list_path);
			maps_collection.updatePatterns(files_list_path);
		}
		
		if (export_affine) {
			StringBuffer sb = new StringBuffer();
			for (OrthoMap omap :  maps_collection.ortho_maps) {
				String name = omap.getName();
				double [][] affine = omap.getAffine();
				if (affine != null) {
					sb.append(String.format("AFFINE %s %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f\n",
							name, affine[0][0], affine[0][1], affine[0][2], affine[1][0], affine[1][1], affine[1][2]));
				}
			}
			CalibrationFileManagement.saveStringToFile (
					affines_path,          //String path,
					sb.toString(), // data,
					false); // boolean append)
		}

		if (export_affine2) {
			StringBuffer sb = new StringBuffer();
			ArrayList<Point> pairs_list = new ArrayList<Point>(); 
			for (OrthoMap map : maps_collection.ortho_maps) {
				for (String other_name: map.pairwise_matches.keySet()) {
					pairs_list.add(new Point(
							maps_collection.getIndex(map.getName()),
							maps_collection.getIndex(other_name)));
				}
			}
			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
				}
			});
			for (Point pair: pairs_list) {
				PairwiseOrthoMatch pom = maps_collection.ortho_maps[pair.x].getMatch(
						maps_collection.ortho_maps[pair.y].getName(), false); // undef_only || nan_rms);
						if ((pom !=null) && pom.isDefined()) {
							double overlap = pom.overlap;
							double [][] affine = pom.getAffine();
							sb.append(String.format("AFFINE2 %s %s %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f  %11.8f\n",
									maps_collection.ortho_maps[pair.x].getName(),
									maps_collection.ortho_maps[pair.y].getName(),
									overlap,
									affine[0][0], affine[0][1], affine[0][2], affine[1][0], affine[1][1], affine[1][2]));
						}
			}
			CalibrationFileManagement.saveStringToFile (
					affines2_path,          //String path,
					sb.toString(), // data,
					false); // boolean append)
		}

		if (import_affine) {
			ArrayList<AffineImport> affine_import = new ArrayList<AffineImport>();
			OrthoMapsCollection.getPathsAndScenesFromSourceList(
					affines_path,
					null, // scenes0,
					null,       // ArrayList<AltitudeMismatchKernel> kernels,
					null,      // ArrayList<GroundObjectPattern>    patterns);
					null, // ArrayList<ModelRegex>             model_regexes,
					null,   // String []                         regex_use) {
					affine_import,          // ArrayList<AffineImport>           affine_import,
					null);         // ArrayList<Affine2Import>          affine2_import) {
			int num_updated_affines = 0;
			for (AffineImport affineImport: affine_import) {
				OrthoMap map =  maps_collection.getMap(affineImport.name);
				if (map != null) {
					if (overwrite_affine || !map.isAffineNonTrivial()) {
						map.setAffine(affineImport.affine);
						num_updated_affines++;
					}
				}
			}
//			maps_collection.updateNumberScenes();
			System.out.println("Updated "+num_updated_affines+" affines of "+affine_import.size()+" specified. There are "+
			maps_collection.ortho_maps.length+" maps in the system.");
		}
		if (import_affine2) {
			ArrayList<Affine2Import> affine2_import = new ArrayList<Affine2Import>();
			OrthoMapsCollection.getPathsAndScenesFromSourceList(
					affines2_path,
					null,            // scenes0,
					null,            // ArrayList<AltitudeMismatchKernel> kernels,
					null,            // ArrayList<GroundObjectPattern>    patterns);
					null,            // ArrayList<ModelRegex>             model_regexes,
					null,            // String []                         regex_use) {
					null,            // ArrayList<AffineImport>           affine_import,
					affine2_import); // ArrayList<Affine2Import>          affine2_import) {
			int num_updated_affines = 0;
			int num_new_affines = 0;
			for (Affine2Import affine2Import: affine2_import) {
				OrthoMap map =  maps_collection.getMap(affine2Import.name1);
				if (map != null) {
					PairwiseOrthoMatch pom = map.getMatch(affine2Import.name2,true);
					if (pom != null) {
						if (overwrite_affine || !pom.isAffineNonTrivial()) {
							pom.setOverlap(affine2Import.overlap);
							pom.setAffine(affine2Import.affine); // does not clone - OK
							num_updated_affines++;
						}						
					} else {
						OrthoMap map2 =  maps_collection.getMap(affine2Import.name2);
						int zoom_lev = Math.min(map.getOriginalZoomLevel(), map2.getOriginalZoomLevel());
						pom= new PairwiseOrthoMatch(
								affine2Import.affine,   // double [][] affine,
								null,                   // double [][] jtj,
								Double.NaN,             // double rms,
								zoom_lev,               // int zoom_lev,
								affine2Import.overlap); // double overlap)
						map.setMatch(map2.getName(), pom);
						num_new_affines++;
					}
				}
			}
			System.out.println("Updated "+num_updated_affines+" pairwise affines, setup new "+num_new_affines+" matches. Specified "+affine2_import.size()+" matches.");
		}

		if (update_bl_bc) {
			boolean OK = updateBlBcFileNames(
					SUFFIXES_BL_BC[suffix_bc_bl_indx], // String suffix,
					"-FLAT",// String before,
					maps_collection); // OrthoMapsCollection maps_collection)
			if (!OK) {
				System.out.println("Failed to update filenames for different interpolation mode");
			}
		}
		
		return true;
	}
	
	
	
	
}
