package com.elphel.imagej.orthomosaic;

import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

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.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TDCorrTile;

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

public class ComboMatch {
	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 boolean openTestPairGps(
			 CLTParameters    clt_parameters,
			 GPUTileProcessor gpu_tile_processor, // initialized by the caller
			 int        debugLevel) {
		GPU_TILE_PROCESSOR = gpu_tile_processor;
		PairwiseOrthoMatch pairwiseOrthoMatch = null;
		String [] pair_names = new String[2];
		ImagePlus imp_sel = WindowManager.getCurrentImage();
		String [] gpu_spair = null;
		boolean use_marked_image =    false; // will be set if found
		String [] all_scenes = null;
		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

//		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";
//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_map = false; // true;
		boolean use_alt = false;
		boolean show_centers = true;
		boolean use_saved_collection = true; // false;
		boolean save_collection = true;
		boolean process_correlation = true; // use false to save new version of data
		boolean update_match =        true; // use false to save new version of data
		boolean render_match =        true;
		boolean restore_temp        = true;
		double  frac_remove  =        0.15;
		double  metric_error =        0.05; // 0.02;//  2 cm
		if (!use_marked_image) {
			process_correlation=false; // use already adjusted by defualt
		}
		
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,900);
		gd.addStringField ("Image list full path",  files_list_path, 180, "Image list full path.");
		gd.addStringField ("Maps collection save path",  orthoMapsCollection_path, 180, "Save path for serialized map collection data.");
		gd.addCheckbox    ("Use saved maps collection", use_saved_collection, "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.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.");
		
		//
//		for (int n = 0; n < image_paths_pre.length; n++) {
//			gd.addStringField ("Image path "+n,  image_paths_pre[n], 180, "Image "+n+" full path w/o ext");		
//		}
//		gd.addStringField ("First image path",  image_paths_pre[0], 180, "First image full path w/o ext");		
//		gd.addStringField ("Second image path", image_paths_pre[1], 180, "Second image full path w/o ext");		
		for (int n = 0; n < image_enuatr.length; n++) {
			gd.addMessage("image["+n+"] pose");
			gd.addNumericField("East",   image_enuatr[n][0][0],  3,7,"m",	"Move image "+n+" East.");
			gd.addNumericField("North",  image_enuatr[n][0][1],  3,7,"m",	"Move image "+n+" North.");
			gd.addNumericField("Up",     image_enuatr[n][0][2],  3,7,"m",	"Move image "+n+" Up.");
			gd.addNumericField("Azimuth",image_enuatr[n][1][0],  3,7,"deg",	"Rotate image "+n+" around vertical clockwise.");
			gd.addNumericField("Tilt",   image_enuatr[n][1][1],  3,7,"deg",	"Rotate image "+n+" around East.");
			gd.addNumericField("Roll",   image_enuatr[n][1][2],  3,7,"deg",	"Rotate image "+n+" around North.");
		}
		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 transformation centers", show_centers, "Mark verticals from the UAS on the ground.");
		gd.addCheckbox    ("Show combo image map",      show_combo_map, "Load and process altitude maps.");
		gd.addCheckbox    ("Show altitude combo image", use_alt, "Load and process altitude maps.");
//		gd.addStringField ("First matching timestamp",  gpu_spair[0], 20, "First GPU-matching timestamp with '_' for decimal point.");
//		gd.addStringField ("Second matching timestamp", gpu_spair[1], 20, "First GPU-matching timestamp with '_' for decimal point.");
		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.showDialog();
		if (gd.wasCanceled()) return false;
		files_list_path = gd.getNextString();
		
		orthoMapsCollection_path = gd.getNextString();
		use_saved_collection =     gd.getNextBoolean();
		save_collection =          gd.getNextBoolean();
		process_correlation=       gd.getNextBoolean();
		update_match=              gd.getNextBoolean();
		render_match=              gd.getNextBoolean();
		for (int n = 0; n < image_enuatr.length; n++) {
			image_enuatr[n][0][0] = gd.getNextNumber();
			image_enuatr[n][0][1] = gd.getNextNumber();
			image_enuatr[n][0][2] = gd.getNextNumber();
			image_enuatr[n][1][0] = gd.getNextNumber();
			image_enuatr[n][1][1] = gd.getNextNumber();
			image_enuatr[n][1][2] = gd.getNextNumber();
		}
		zoom_lev =            (int) gd.getNextNumber();
		gpu_width =           (int) gd.getNextNumber();
		gpu_height =          (int) gd.getNextNumber();

		OrthoMap.setGPUWidthHeight(gpu_width,gpu_height);
		
		show_centers =              gd.getNextBoolean();
		show_combo_map =            gd.getNextBoolean();
		use_alt =                   gd.getNextBoolean();

//		gpu_spair[0] =              gd.getNextString();
//		gpu_spair[1] =              gd.getNextString();
		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();
		}
		OrthoMapsCollection maps_collection=null;
		if (use_saved_collection) {
			try {
				maps_collection = OrthoMapsCollection.readOrthoMapsCollection (orthoMapsCollection_path);
			} catch (ClassNotFoundException | IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} else {
			maps_collection = new OrthoMapsCollection(files_list_path); // should have ".list" extension
		}
		String [] names = maps_collection.getNames();
		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();
		
		
        // which pair to compare
//		String [] gpu_spair = {names[gpu_ipair[0]],names[gpu_ipair[1]]}; 
		int [] origin = new int[2];
		ImagePlus imp_img = null;
		if (show_combo_map) {
			imp_img = 	maps_collection.renderMulti (
					"multi_zoom"+zoom_lev, // String      title,
					false,             // boolean     use_alt,
					show_centers,      // boolean     show_centers,
					zoom_lev,          // int         zoom_level,
					origin);           // int []      origin){
			imp_img.show();
		}
		ImagePlus imp_alt = null;
		if (use_alt) {
			imp_alt =maps_collection.renderMulti (
					"multi_alt_zoom"+zoom_lev, // String      title,
					true,             // boolean     use_alt,
					show_centers,      // boolean     show_centers,
					zoom_lev,          // int         zoom_level,
					origin);           // int []      origin){
			imp_alt.show();
		}
        if (process_correlation || render_match ) {
//        	int [] gpu_pair;
        	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)));
        			}
        		}
        		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;
        		}
				String [] choices = getPairChoices(
						available_pairs, // int [][] pairs,
						names); // String [] names)

				GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,400);
				gdc.addChoice("Operation:", choices, choices[choices.length-1]);
				gdc.showDialog();
				if (gdc.wasCanceled()) return false;
				int pair= gdc.getNextChoiceIndex();
				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 = Math.min(
        			maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel(),
        			maps_collection.ortho_maps[gpu_pair[1]].getOriginalZoomLevel());
        	int max_zoom_lev = Math.max(
        			maps_collection.ortho_maps[gpu_pair[0]].getOriginalZoomLevel(),
        			maps_collection.ortho_maps[gpu_pair[1]].getOriginalZoomLevel());
        	int initial_zoom = max_zoom_lev - 3;  // another algorithm?

        	
        	System.out.println("Setting up GPU");
        	if (GPU_QUAD_AFFINE == null) {
        		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);
        	}
        	
        	
        	
        	double [][] affine0 = {{1,0,0},{0,1,0}}; // will always stay the same
			pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch(
					maps_collection.ortho_maps[gpu_pair[1]].getName());
			double [][] affine1 = null;
			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.println("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();
				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;
				if (!process_correlation) {
					zooms = new int[] {min_zoom_lev, 1000};
					num_tries = 0;
					update_match = false;
				}
				debugLevel = 0;
				boolean  batch_mode = true; // false; //  true;
				boolean ignore_prev_rms = true;
				Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair()
				for (int zi = 0; zi < zooms.length; zi++) {
					zoom_lev = zooms[zi];
					if (zoom_lev >=1000) {
						break;
					}
					// will modify affines[1], later add jtj, weight, smth. else?
					FineXYCorr warp = maps_collection.correlateOrthoPair(
							clt_parameters, // CLTParameters    clt_parameters,
							(process_correlation? pairwiseOrthoMatch: null), //PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics
							frac_remove,    // double           frac_remove, //  =        0.25
							metric_error, // 			double           metric_error,
							ignore_prev_rms, // boolean ignore_prev_rms,
							num_tries,      //  = 5int              num_tries, //  = 5
							true, // boolean          calc_warp,
							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,
							
							debugLevel);    // final int        debugLevel)
					//maps_collection.ortho_maps[gpu_pair[1]]
					if (process_correlation) {
						if (update_match) {
							pairwiseOrthoMatch.zoom_lev = zoom_lev;
							pairwiseOrthoMatch.affine = 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]+"]]");
							System.out.println();
						}
					}
					if (render_match) {
						ImagePlus imp_img_pair = 	maps_collection.renderMulti (
								"multi_"+gpu_spair[0]+"-"+gpu_spair[1]+"-zoom_"+min_zoom_lev+"_"+zoom_lev, // String      title,
								false,             // boolean     use_alt,
								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,
								show_centers,      // boolean     show_centers,
								min_zoom_lev,      // int         zoom_level,
								origin);           // int []      origin){
						imp_img_pair.show();

						/*
						
						int gpu_tilesX = gpu_width/GPUTileProcessor.DTT_SIZE; 
						Rectangle tile_woi = OrthoMapsCollection.scaleRectangle (woi, GPUTileProcessor.DTT_SIZE);
						double [][] vector_field_interpolated = interpolateVectorField(
								vector_field, // final double [][] vector_field, // sparse {vx,vy,strength}
								gpu_tilesX,   // final int gpu_tilesX, // 512
								tile_woi,     // final Rectangle tile_woi, // only width, height are used top-left corners are the same
								scale,        // final double scale,
								1);           // final int debugLevel);
						
						
						ImagePlus imp_img_pair = 	maps_collection.renderMulti (
								"multi_"+gpu_spair[0]+"-"+gpu_spair[1]+"-zoom_"+min_zoom_lev+"_"+zoom_lev, // String      title,
								false,             // boolean     use_alt,
								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] 
								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();
        	}
        }
        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 > 0) {
			String [] dbg_titles = {"x-raw","x_filled","y-raw","y_filled"};
			double [][] dbg_img = {vf[0], vf_filled[0],vf[1], vf_filled[1]};
    		ShowDoubleFloatArrays.showArrays(
    				dbg_img,
    				tile_woi.width,
    				dbg_img[0].length / tile_woi.width,
    				true,
    				"vector_field_filled",
    				dbg_titles);
		}
		
		
		
		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);
				}
				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;
				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",1200,400);
					gds.addChoice("Operation:", choices, choices[choices.length-1]);
					gds.showDialog();
					if (gds.wasCanceled()) return null;
					pair= gds.getNextChoiceIndex();
				}
				
				
				double [][] points_xy= new double [2][2];
				int [] pair_slices = new int[2];
				for (int n = 0; n < points_xy.length; n++) {
					int p = 2 * pair + n; // fp.npoints-2+n;
					points_xy[n][0] = fp.xpoints[p];
					points_xy[n][1] = fp.ypoints[p];
					pair_slices[n] = pRoi.getPointPosition(p); // works
				}
				PairwiseOrthoMatch match= new PairwiseOrthoMatch();
				match.affine = new double[][] {
					{1,0,(points_xy[1][0]-points_xy[0][0])*pix_size},
					{0,1,(points_xy[1][1]-points_xy[0][1])*pix_size}};
				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 = [["+match.affine[0][0]+", "+match.affine[0][1]+", "+match.affine[0][2]+"],");
				System.out.println("                ["+match.affine[1][0]+", "+match.affine[1][1]+", "+match.affine[1][2]+"]]");
				return match;
			}
		}
		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;
	}
	
	
	
	/*
	public static TDCorrTile [] rectilinearCorrelate_TD( // 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
			final boolean                batch_mode,
			final int                    debugLevel) {
	 */
	
	
	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
				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);
		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 [][][] 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)
		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)
		}
    	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("Set image pair",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;
			}
		}
	}
	
	
}
