package com.elphel.imagej.orthomosaic;

import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.TDCorrTile;

import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.PointRoi;
import ij.process.FloatPolygon;


public class OrthoMapsCollection  implements Serializable{
	private static final long serialVersionUID = 1L;
	public static final String [] KEY_DIRS= {"rootDirectory", // from EyesisCorrectionParameters
			"sourceDirectory","linkedModels","videoDirectory","x3dDirectory","resultsDirectory","scenesDirectory",
			"kernelsDirectory", "patternsDirectory"};
	public static final String [] NAME_FH = {"full","half"};
	public static final String [] NAME_MO = {"main","other"};

	OrthoMap []                                 ortho_maps;
	transient HashMap<Double,Integer>           map_index;
	transient HashMap<String,Integer>           map_index_string;
	transient ArrayList<AltitudeMismatchKernel> kernels;
	transient ArrayList<GroundObjectPattern>    patterns;
	
	
	private void writeObject(ObjectOutputStream oos) throws IOException { // ?
		oos.defaultWriteObject();
		oos.writeObject(AltitudeMismatchKernel.getKernelsDirectory());
		oos.writeObject(kernels);
		oos.writeObject(GroundObjectPattern.getPatternsDirectory());		
		oos.writeObject(patterns);
//		oos.writeObject(path);
		// ortho_maps is not transient
	}
	@SuppressWarnings("unchecked")
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
		AltitudeMismatchKernel.setKernelsDirectory((String) ois.readObject());
		kernels = (ArrayList<AltitudeMismatchKernel>) ois.readObject();
		GroundObjectPattern.setPatternsDirectory((String) ois.readObject());
		patterns = (ArrayList<GroundObjectPattern>)   ois.readObject();
		
		reindex(); // will recreate map_index, map_index_string
		// lla is not transient
	}
	
	public OrthoMapsCollection(
			String path) {
		if (path.endsWith(".list")) {
			String[] scenes0 = new String[1];
			kernels = new ArrayList<AltitudeMismatchKernel>(); 
			patterns = new ArrayList<GroundObjectPattern>();
			
			String[] paths = getPathsAndScenesFromSourceList(
					path,
					scenes0,
					kernels,   // ArrayList<AltitudeMismatchKernel> kernels,
					patterns); // ArrayList<GroundObjectPattern>    patterns);
			
			String scenes_path = scenes0[0];
			double [][] affine = {{1,0,0},{0,1,0}}; // maybe later calculate from mage_enuatr
			ortho_maps = new OrthoMap[paths.length];
			
			for (int n = 0; n < ortho_maps.length; n++) {
				String name = OrthoMap.getNameFromPath(paths[n]);
				ortho_maps[n] = new OrthoMap(paths[n]); // , scene_path);
				ortho_maps[n].setAffine(affine);
			}
		} else {
			System.out.println("OrthoMapsCollection(): path should end with \".list\"");
		}
		Arrays.sort(ortho_maps);
		reindex();
	}
	// add update kernels, update patterns
	public void updateKernels (
			String path) {
		if (path.endsWith(".list")) {
			kernels = new ArrayList<AltitudeMismatchKernel>(); 
			getPathsAndScenesFromSourceList(
					path,
					null,
					kernels, // ArrayList<AltitudeMismatchKernel> kernels,
					null);   // ArrayList<GroundObjectPattern>    patterns);
		} else {
			System.out.println("updateKernels(): path should end with \".list\"");
		}
	}
	
	public void updatePatterns (
			String path) {
		if (path.endsWith(".list")) {
			patterns = new ArrayList<GroundObjectPattern>();
			getPathsAndScenesFromSourceList(
					path,
					null,
					null, // ArrayList<AltitudeMismatchKernel> kernels,
					patterns);   // ArrayList<GroundObjectPattern>    patterns);
		} else {
			System.out.println("updatePatterns(): path should end with \".list\"");
		}
	}
	
	
	static String getSceneDir(String scenes_path, String name) {
		File [] scene_dirs = (new File(scenes_path)).listFiles(); // may contain non-directories, will be filtered by filterScenes
		//scene_dirs[3].listFiles()
		Arrays.sort(scene_dirs);
		for (int i = scene_dirs.length-1; i>=0; i--) {
			if (scene_dirs[i].isDirectory()) {
				String dirname = scene_dirs[i].getName();
				if (name.compareTo(dirname) >= 0) {
					// verify it has needed scene
					File [] segment = scene_dirs[i].listFiles();
					for (int j = 0; j < segment.length; j++) {
						if (segment[j].getName().equals(name)) {
							return segment[j].toString(); 
						}
					}
				}
			}
		}
		return null;
	}
	
	
	
	public int reindex() {
		map_index = new HashMap<Double,Integer>();
		map_index_string = new HashMap<String,Integer>();
		for (int i = 0; i < ortho_maps.length; i++) {
			map_index.put(ortho_maps[i].ts, i);
			map_index_string.put(ortho_maps[i].name,i);
		}
		return ortho_maps.length;
	}
	
	OrthoMap [] getMaps() {
		return ortho_maps;
	}
	
	public String[] getNames() {
		String [] names = new String [ortho_maps.length];
		for (int n = 0; n < ortho_maps.length; n++) {
			names[n] = ortho_maps[n].getName();
		}
		return names;
	}

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

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

	/**
	 * Get rectified overlap bounds of two provided ortho images relative to the origin (vertical
	 * point) of the first one in meters. Use specified affine transforms, not saved with the orto map
	 * @param ref_index - index of the first (reference) map
	 * @param other_index - index of the second (other) map
	 * @param ref_affine - specified affine transform of the reference image (referenced to its vertical_point)
	 * @param other_affine - specified affine transform of the other image (referenced to its vertical_point)
	 * @return {{min_x,max_x},{min_y,max_y}} bounds that include an overlap of the two provided maps
	 *         relative to the origin (vertical point) of the first image.
	 * Returns null if there is no overlap
	 */
	private double [][] getOverlapMeters(
			int ref_index,
			int other_index,
			double [][] ref_affine,
			double [][] other_affine){
		double [][] bounds = ortho_maps[ref_index].getBoundsMeters(true,ref_affine);
		double [][] bounds_other = ortho_maps[other_index].getBoundsMeters(true,other_affine);
		double [] enuOffset = ortho_maps[ref_index].enuOffsetTo(ortho_maps[other_index]);
		double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
		for (int n = 0; n < bounds.length; n++) {
			bounds[n][0] = Math.max(bounds[n][0],bounds_other[n][0]+ rd[n]);
			bounds[n][1] = Math.min(bounds[n][1],bounds_other[n][1]+ rd[n]);
			if (bounds[n][0] > bounds[n][1]) {
				return null;
			}
		}
		return bounds;
	}
	
	/**
	 * Use object coordinates (now metric) on the composite image to
	 * determine corresponding pixel coordinates of the source images
	 * @param objects ArrayList of objects with the scene name and xy coordinates
	 *                in the composite image. Will fill in pixel coordinates of the objects 
	 * @param zoom_level zoom level of the composite image
	 */
	public void reverseRender(
			ArrayList<ObjectLocation> objects,
			int         zoom_level,
			int []      indices){ // null or selected image indices
		int [][] bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
				zoom_level,
				indices); 
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		/*
		int [] indices = new int [ortho_maps.length]; //maybe will get rid of
		for (int i = 0; i < indices.length; i++) {
			indices[i] = i;
		}
		*/
		final int reference_index = (indices == null)?0:indices[0]; 
		for (ObjectLocation ol: objects) {
			int nmap =getIndex(ol.getName());
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(ortho_maps[nmap].orig_zoom_level); // pix per meter
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [] enu_offset = ortho_maps[reference_index].enuOffsetTo(ortho_maps[nmap]);
			double [] scaled_out_center = { // xy center to apply affine to
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			double [] metric_center = { // back to meters to match objects
					scaled_out_center[0]/scale,
					scaled_out_center[1]/scale};
			double dX = ol.getMetric()[0]-metric_center[0]; // matching dX, dY in renderMulti()
			double dY = ol.getMetric()[1]-metric_center[1];
			
//			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to thje top-left
//			for (int n = 0; n< 2; n++) {
//				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
//				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
//			}
			// Output window size (keep original affine - OK if will not exactly fit)
//			final int ownd_width =  obounds[0][1] - obounds[0][0];
//			final int ownd_height = obounds[1][1] - obounds[1][0];
//			final int ownd_len = ownd_width * ownd_height;
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final double [][] affine = ortho_maps[nmap].affine; // only here use provided
//			final int src_width =      ortho_maps[nmap].getImageData().width;
//			final int src_height =     ortho_maps[nmap].getImageData().height;
//			final float [] src_img =   ortho_maps[nmap].getImageData().data; // FIXME: will not use
			double [] xy_src = { // pixels of the source image
					src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
					src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
			ol.setPixels(xy_src);
		}
	}
	
	
	public ImagePlus renderMulti (
			String      title,
			boolean     use_alt,
			boolean     show_centers,
			int         zoom_level,
			int         temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
			int []      origin){
		return renderMulti (
				title,        // String        title,
				use_alt,      // boolean       use_alt,
				null,         // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				false,        // bounds_to_indices, // boolean       bounds_to_indices,
				temp_mode,    // int           temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
				null,         // double [][][] affines, // null or [indices.length][2][3]
				null,         // FineXYCorr    warp,
				show_centers, // boolean       show_centers,
				zoom_level,   // int           zoom_level,
				origin);      // int []        origin)
	}
	
	public ImagePlus renderMulti (
			String        title,
			boolean       use_alt,
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			int           temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,    // use for a single pair only
			boolean       show_centers,
			int           zoom_level,
			int []        origin){
		int []     wh = new int[2];
//		boolean       bounds_to_indices = true;
		double [][] centers = new double [(indices !=null)? indices.length: ortho_maps.length][];
		float [][] multi = renderMulti (
				use_alt,    // boolean     use_alt,
				indices,    // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				bounds_to_indices, // boolean       bounds_to_indices,
				affines,    // double [][][] affines, // null or [indices.length][2][3]
				warp,       // FineXYCorr    warp,,
				zoom_level, // int         zoom_level,
				wh,         // int []      wh,
				origin,     // int []      origin){ // maps[0] as a reference
				centers);   // double [][] centers)
		int num_images =  (indices != null)? indices.length : ortho_maps.length;
//		String [] map_names = new String[(indices != null) ? indices.length : ortho_maps.length];
		String [] map_names = new String[num_images + ((num_images==2)? 1 : 0)];
		float [][] extra_multi = new float [map_names.length][];
		for (int n = 0; n <num_images; n++) {
			int mapn= (indices != null)? indices[n] : n;
			map_names[n] = ortho_maps[mapn].getName()+"_"+ortho_maps[mapn].getLocalDateTime().toString().replace("T","_")+"_UTC";
			map_names[n] += String.format(" raw=%7.1f", ortho_maps[mapn].getTemperature());
			extra_multi[n] = multi[n];
		}
		if (map_names.length>num_images) { // inefficient way, only for display
			// correct(offset) pixel values relative to multi[0]
			int map0= (indices != null)? indices[0] : 0;
			if (!use_alt) {
				for (int n = 1; n < num_images; n++) {
                    //temp_mode, // 0 - do nothing, 1 - equalize average,2 - try to correct
					int mapn= (indices != null)? indices[n] : n;
					double offs = 0;
					switch (temp_mode) {
					case 1:
						offs = ortho_maps[map0].getAveragePixel()-ortho_maps[mapn].getAveragePixel();
						break;
					case 2:
						offs = ortho_maps[mapn].getTemperature()-ortho_maps[mapn].getAveragePixel()
								-(ortho_maps[map0].getTemperature()-ortho_maps[map0].getAveragePixel());
						break;
					}
					float foffs= (float) offs;
					for (int i = 0; i < extra_multi[n].length; i++) if (!Float.isNaN(extra_multi[n][i])) {
						extra_multi[n][i]+= foffs;
					}
				}
			}
			map_names[num_images] = "Diff 0-1";
			extra_multi[num_images] = new float[extra_multi[0].length];
			for (int i = 0; i < extra_multi[num_images].length; i++) {
				extra_multi[num_images][i] = extra_multi[1][i]-extra_multi[0][i];
			}
		}
        ImageStack stack = ShowDoubleFloatArrays.makeStack( 
        		extra_multi,
        		wh[0],
        		wh[1],
        		map_names,
        		false);
        ImagePlus imp = new ImagePlus(title, stack);
        if (show_centers) {
        	PointRoi roi = new PointRoi();
        	for (int i = 0; i < centers.length; i++) {
        		roi.addPoint(centers[i][0],centers[i][1], i+1);
        	}
        	roi.setOptions("label");
        	imp.setRoi(roi);
        }
        imp.show(); // debugging
        return imp;
        
	}
	
	
	/**
	 * Rectify and render multiple images (as slices) matching vert_meters to
	 * their (vert_meters points) provided IMS coordinates
	 * @param show altitudes instead of thermal data 
	 * @param maps array of the ortho images 
	 * @param zoom_level zoom level (0 - 1pix=1cm, 1 - 1pix=0.5cm, -1 - 1pix=2cm
	 * @param wh null or int [2], will return {width, height}
	 * @param origin - null or double [2], will return {x0, y0} in pixels
	 * @param centers - null or double[maps.length][] - will return image centers coordinates
	 * @return
	 */
	public float [][] renderMulti (
			boolean     use_alt,
//			OrthoMap [] maps,
			int         zoom_level,
			int []      wh,
			int []      origin, // maps[0] as a reference
			double [][] centers){
		return renderMulti (
				use_alt,    // boolean       use_alt,
				null,       // int []        indices,
				false,      // boolean       bounds_to_indices,
				null,       // double [][][] affines,
				null,       // FineXYCorr    warp,
				zoom_level, // int           zoom_level,
				wh,         // int []        wh,
				origin,     // int []        origin, // maps[0] as a reference
				centers);   // double [][]   centers)
	}
	
	public float [][] renderMulti (
			boolean       use_alt,
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,
			int           zoom_level,
			int []        wh,
			int []        origin, // maps[0] as a reference
			double [][]   centers){
		final int dbg_x=2783, dbg_y=-5228;
		int [][] bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
				zoom_level,
				bounds_to_indices? indices: null); 
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		if (wh != null) {
			wh[0] = width;
			wh[1] = height;
		}
		if (origin != null) {
			origin[0] = -bounds[0][0];
			origin[1] = -bounds[1][0];
		}
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		
		final float [][] fpixels = new float [indices.length][width * height];
//		for (int nmap:indices) { // = 0; nmap< ortho_maps.length; nmap++) {
		for (int indx = 0; indx < indices.length; indx++) { //:indices) { // = 0; nmap< ortho_maps.length; nmap++) {
			final int findx= indx;
			final int nmap = indices[indx]; // nmap;
			Arrays.fill(fpixels[findx], Float.NaN);
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(ortho_maps[nmap].orig_zoom_level); // pix per meter
		// metric bounds of the rectified image relative to its origin
//			double [][] mbounds = (affines !=null) ?
//					ortho_maps[nmap].getBoundsMeters(true, affines[indx]): // use provided affine
//						ortho_maps[nmap].getBoundsMeters(true);
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [] enu_offset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			final double [] scaled_out_center = { // xy center to apply affine to
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			if (centers != null) {
				centers[findx] = scaled_out_center;
			}
			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to thje top-left
			for (int n = 0; n< 2; n++) {
				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
			}
			// Output window size (keep original affine - OK if will not exactly fit)
			final int ownd_width =  obounds[0][1] - obounds[0][0];
			final int ownd_height = obounds[1][1] - obounds[1][0];
			final int ownd_len = ownd_width * ownd_height;
//			double [][] src_bounds=(affines !=null) ? 
//					ortho_maps[nmap].getBoundsMeters (true, affines[indx]): // use provided affine
//						ortho_maps[nmap].getBoundsMeters (true);
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final double [][] affine = (affines !=null) ? affines[indx]: ortho_maps[nmap].affine; // only here use provided
			final int src_width = use_alt? ortho_maps[nmap].getAltData().width: ortho_maps[nmap].getImageData().width;
			final int src_height = use_alt? ortho_maps[nmap].getAltData().height : ortho_maps[nmap].getImageData().height;
			final float [] src_img = use_alt? ortho_maps[nmap].getAltData().data : ortho_maps[nmap].getImageData().data;
			if ((indx==0) && (warp != null)) { // set center from the first image
				warp.setRender(
						zoom_level, // int zoom_lev,
						scaled_out_center[0], // double px0, // in render pixels
						scaled_out_center[1]); // // double py0);
			}
			final Rectangle warp_woi =((indx==1) && (warp != null))? warp.getRenderWOI():null; 
			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 nPix = ai.getAndIncrement(); nPix < ownd_len; nPix = ai.getAndIncrement()) {
							int opX = nPix % ownd_width + obounds[0][0]; // absolute output pX, pY
							int opY = nPix / ownd_width + obounds[1][0];
							double dX = (opX - scaled_out_center[0]) /scale; // in original image scale 
							double dY = (opY - scaled_out_center[1]) /scale;
							double [] xy_src = { // pixels of the source image
									src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
									src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
							// limit to the source image
							if ((warp_woi != null) && (warp_woi.contains(opX,opY))) {
								if ((opX==dbg_x) && (opY==dbg_y)) {
									System.out.println("opX="+opX+", opy="+opY);
								}
								double [] dxy = warp.getWarp(opX,opY);
								xy_src[0] += dxy[0];
								xy_src[1] += dxy[1];
							}
							if  ((xy_src[0] >= 0) && (xy_src[0] < (src_width-1)) &&
									(xy_src[1] >= 0) && (xy_src[1] < (src_height-1))) { 
								int [] ixy_src = {(int) Math.floor(xy_src[0]), (int)Math.floor(xy_src[1]) };
								double [] kxy = {xy_src[0]-ixy_src[0], xy_src[1]-ixy_src[1]}; 
								int indx00 = ixy_src[0] + ixy_src[1] * src_width; 
								double d00 = src_img[indx00];
								if (!Double.isNaN(d00)) {
									double d01 = src_img[indx00 + 1];
									double d10 = src_img[indx00 + src_width];
									double d11 = src_img[indx00 + src_width + 1];
									double d = d00*(1.0 - kxy[0])*(1.0 - kxy[1])+
											d01*       kxy[0] *(1.0 - kxy[1])+
											d10*(1.0 - kxy[0])*       kxy[1]+
											d11*       kxy[0] *       kxy[1];
									fpixels[findx][opX + opY*width] = (float) d;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return fpixels;
	}
	
	
	public double [][] renderMultiDouble (
			boolean       use_alt,
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			boolean       bounds_to_indices,
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,
			int           zoom_level,
			int []        wh,
			int []        origin, // maps[0] as a reference
			double [][]   centers){
		final int dbg_x=2783, dbg_y=-5228;
		int [][] bounds = getBoundsPixels( // should be for rectified, {-bounds[0][0], -bounds[0][1]} - exact center
				zoom_level,
				bounds_to_indices? indices: null); 
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0];
		if (wh != null) {
			wh[0] = width;
			wh[1] = height;
		}
		if (origin != null) {
			origin[0] = -bounds[0][0];
			origin[1] = -bounds[1][0];
		}
		if (indices == null) {
			indices = new int [ortho_maps.length];
			for (int i = 0; i < indices.length; i++) {
				indices[i] = i;
			}
		}
		
		final double [][] dpixels = new double [indices.length][width * height];
		for (int indx = 0; indx < indices.length; indx++) { //:indices) { // = 0; nmap< ortho_maps.length; nmap++) {
			final int findx= indx;
			final int nmap = indices[indx]; // nmap;
			Arrays.fill(dpixels[findx], Float.NaN);
			final double scale = 1.0/OrthoMap.getPixelSizeMeters(zoom_level);
			final double src_scale = 1.0/OrthoMap.getPixelSizeMeters(ortho_maps[nmap].orig_zoom_level); // pix per meter
		// metric bounds of the rectified image relative to its origin
			double [][] mbounds = ortho_maps[nmap].getBoundsMeters(true); // keep original bounds
			double [] enu_offset = ortho_maps[indices[0]].enuOffsetTo(ortho_maps[nmap]);
			final double [] scaled_out_center = { // xy center to apply affine to
					-bounds[0][0] + scale * enu_offset[0],
					-bounds[1][0] - scale * enu_offset[1]};
			if (centers != null) {
				centers[findx] = scaled_out_center;
			}
			final int [][] obounds = new int [2][2]; // output (rectified, combined) image bounds, relative to thje top-left
			for (int n = 0; n< 2; n++) {
				obounds[n][0] = (int) Math.floor(scaled_out_center[n] + scale*mbounds[n][0]);
				obounds[n][1] = (int) Math.ceil (scaled_out_center[n] + scale*mbounds[n][1]);
			}
			// Output window size (keep original affine - OK if will not exactly fit)
			final int ownd_width =  obounds[0][1] - obounds[0][0];
			final int ownd_height = obounds[1][1] - obounds[1][0];
			final int ownd_len = ownd_width * ownd_height;
			double [][] src_bounds=ortho_maps[nmap].getBoundsMeters (true); // using original affines
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final double [][] affine = (affines !=null) ? affines[indx]: ortho_maps[nmap].affine; // only here use provided
			final int src_width = use_alt? ortho_maps[nmap].getAltData().width: ortho_maps[nmap].getImageData().width;
			final int src_height = use_alt? ortho_maps[nmap].getAltData().height : ortho_maps[nmap].getImageData().height;
			final float [] src_img = use_alt? ortho_maps[nmap].getAltData().data : ortho_maps[nmap].getImageData().data;
			if ((indx==0) && (warp != null)) { // set center from the first image
				warp.setRender(
						zoom_level, // int zoom_lev,
						scaled_out_center[0], // double px0, // in render pixels
						scaled_out_center[1]); // // double py0);
			}
			final Rectangle warp_woi =((indx==1) && (warp != null))? warp.getRenderWOI():null; 
			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 nPix = ai.getAndIncrement(); nPix < ownd_len; nPix = ai.getAndIncrement()) {
							int opX = nPix % ownd_width + obounds[0][0]; // absolute output pX, pY
							int opY = nPix / ownd_width + obounds[1][0];
							double dX = (opX - scaled_out_center[0]) /scale; // in original image scale 
							double dY = (opY - scaled_out_center[1]) /scale;
							double [] xy_src = { // pixels of the source image
									src_scale * (affine[0][0]*dX + affine[0][1]*dY + affine[0][2] + src_center[0]),
									src_scale * (affine[1][0]*dX + affine[1][1]*dY + affine[1][2] + src_center[1])};
							// limit to the source image
							if ((warp_woi != null) && (warp_woi.contains(opX,opY))) {
								if ((opX==dbg_x) && (opY==dbg_y)) {
									System.out.println("opX="+opX+", opy="+opY);
								}
								double [] dxy = warp.getWarp(opX,opY);
								xy_src[0] += dxy[0];
								xy_src[1] += dxy[1];
							}
							if  ((xy_src[0] >= 0) && (xy_src[0] < (src_width-1)) &&
									(xy_src[1] >= 0) && (xy_src[1] < (src_height-1))) { 
								int [] ixy_src = {(int) Math.floor(xy_src[0]), (int)Math.floor(xy_src[1]) };
								double [] kxy = {xy_src[0]-ixy_src[0], xy_src[1]-ixy_src[1]}; 
								int indx00 = ixy_src[0] + ixy_src[1] * src_width; 
								double d00 = src_img[indx00];
								if (!Double.isNaN(d00)) {
									double d01 = src_img[indx00 + 1];
									double d10 = src_img[indx00 + src_width];
									double d11 = src_img[indx00 + src_width + 1];
									double d = d00*(1.0 - kxy[0])*(1.0 - kxy[1])+
											d01*       kxy[0] *(1.0 - kxy[1])+
											d10*(1.0 - kxy[0])*       kxy[1]+
											d11*       kxy[0] *       kxy[1];
									dpixels[findx][opX + opY*width] = (float) d;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return dpixels;
	}

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

	private float [][] getPaddedPairGPU(
			int [] pair,
			int    zoom_lev){
		float [][] gpu_pair_img = new  float [2][];
		for (int n = 0; n < pair.length; n++) {
			gpu_pair_img[n] = ortho_maps[pair[n]].getPaddedGPU (
							zoom_lev); // int zoom_level,
		}
		return gpu_pair_img;
	}
	
	/*
	public double [][] correlateOrthoPair(
			CLTParameters    clt_parameters,
			String           first_name,
			String           second_name,
			int              zoom_lev,
			double []        offset_xy_second, // on top of affine and GPS
			final int        debugLevel){
		int first_index = map_index_string.get(first_name);
		int second_index = map_index_string.get(second_name);
		return correlateOrthoPair(
				   clt_parameters,   // CLTParameters    clt_parameters,
				   first_index,      // int              first_index,
				   second_index,     // int              second_index,
				   zoom_lev,         // int              zoom_lev,
				   offset_xy_second, // double []        offset_xy_second, // on top of affine and GPS
				   debugLevel);      // final int        debugLevel)
	}
	
	private double [][] correlateOrthoPair(
			CLTParameters    clt_parameters,
			int              first_index,
			int              second_index,
			int              zoom_lev,
			double []        offset_xy_second, // on top of affine and GPS
			final int        debugLevel){
		int [] gpu_pair = {first_index,second_index};
		double [][][] bounds_out = new double [2][][];
		float [][] gpu_pair_img = new  float [2][];
		for (int n = 0; n < gpu_pair.length; n++) {
			gpu_pair_img[n] = ortho_maps[gpu_pair[n]].getPaddedGPU (
							zoom_lev); // int zoom_level,
		}
		if (debugLevel > 1) {
			String [] map_names = {ortho_maps[gpu_pair[0]].getName(),ortho_maps[gpu_pair[1]].getName()};
			ShowDoubleFloatArrays.showArrays( 
					gpu_pair_img,
					OrthoMap.gpu_width,
					OrthoMap.gpu_height,
					true,
					"gpu_pair-zoom"+zoom_lev+"-"+ortho_maps[gpu_pair[0]].getName()+"-"+ortho_maps[gpu_pair[1]].getName(),
					map_names);
		}
		return null;
	} */
    
	
	public FineXYCorr correlateOrthoPair(
			CLTParameters    clt_parameters,
			PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics			
			double           frac_remove, //  =        0.25
			double           metric_error,
			boolean          ignore_prev_rms,
			int              num_tries, //  = 5
			boolean          calc_warp,
			boolean          batch_mode,
			String []        gpu_spair,
			double [][][]    affines, // here in meters, relative to vertical points
			Rectangle        woi,
			int              zoom_lev,
			final int        debugLevel){
		int [] gpu_pair = new int[gpu_spair.length];
		for (int i = 0; i < gpu_pair.length; i++) {
			gpu_pair[i] = getIndex(gpu_spair[i]);
		}
		return correlateOrthoPair(
				clt_parameters,  // CLTParameters    clt_parameters,
				pairwiseOrthoMatch, //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
				calc_warp,       // boolean          calc_warp,
				batch_mode,      // boolean          batch_mode,
				gpu_pair,        // int []           gpu_pair,
				affines,         // double [][][]    affines, // here in meters, relative to vertical points
				woi,             // Rectangle        woi,
				zoom_lev,        // int              zoom_lev,
				debugLevel);     // final int        debugLevel)
	}
	
	public FineXYCorr correlateOrthoPair(
			CLTParameters    clt_parameters,
			PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed
			double           frac_remove, //  =        0.25
			double           metric_error,
			boolean          ignore_prev_rms,
			int              num_tries, //  = 5
			boolean          calc_warp,
			boolean          batch_mode,
			int []           gpu_pair,
			double [][][]    affines,  // here in meters, relative to vertical points
			Rectangle        woi,
			int              zoom_lev,
			int              debugLevel){
		if (woi == null) {
			woi = new Rectangle();
		}
		boolean show_gpu_img = true; // (debugLevel > 1);
		boolean show_tile_centers = false; // true; // (debugLevel > 1);
		if (!batch_mode) {
			debugLevel = 0;
		} else {
			show_gpu_img =      false; // (debugLevel > 1);
			show_tile_centers = false; // true; // (debugLevel > 1);
		}
    	boolean show_vector_field = !batch_mode && (debugLevel>-1); // true;
		double [][] bounds_overlap_meters = getOverlapMeters(
				gpu_pair[0],  // int ref_index,
				gpu_pair[1],  // int other_index)
				affines[0],   // double [][] ref_affine,
				affines[1]);  // double [][] other_affine
				
		if (bounds_overlap_meters == null) {
			if (debugLevel > -3) {
				System.out.println("correlateOrthoPair(): no overlap");
				return null;
			}
		}
		double pix_size = OrthoMap.getPixelSizeMeters (zoom_lev);
	    int [] overlap_wh_pixel = new int [2];
	    for (int i = 0; i < 2; i++) {
	    	overlap_wh_pixel[i] = ((int) Math.ceil(bounds_overlap_meters[i][1]/pix_size)) - ((int) Math.floor(bounds_overlap_meters[i][0]/pix_size));
	    }
		// convert to pixels,shift top-left to [0,0] (remember offsets, limit w,h,
		// change to pixels last, remember TL in meters?
		// keep center where it was
		// {bounds_overlap_meters[0][0],bounds_overlap_meters[1][0],
		double [] enuOffset = ortho_maps[gpu_pair[0]].enuOffsetTo(ortho_maps[gpu_pair[1]]);
		double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
		double [][] tlo_rect_metric = new double [2][2]; // top-left of overlap referenced to it's own vertical point (subtract!)
		tlo_rect_metric[0][0] = bounds_overlap_meters[0][0]; // relative to ref vert_meters
		tlo_rect_metric[0][1] = bounds_overlap_meters[1][0]; // vert_meters
		tlo_rect_metric[1][0] = bounds_overlap_meters[0][0]  // relative to other vert_meters
				- rd[0]; //  + ortho_maps[gpu_pair[1]].vert_meters[0]- ortho_maps[gpu_pair[0]].vert_meters[0];
		tlo_rect_metric[1][1] = bounds_overlap_meters[1][0]
				- rd[1]; //  + ortho_maps[gpu_pair[1]].vert_meters[1]- ortho_maps[gpu_pair[0]].vert_meters[1];

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

		}
		woi.width =  overlap_wh_pixel[0];
		woi.height = overlap_wh_pixel[1]; // = new Rectangle(0, 0, overlap_wh_pixel[0], overlap_wh_pixel[1]);
		if (woi.width > OrthoMap.gpu_width) {
			if (debugLevel > -3) {
				System.out.println("correlateOrthoPair() correlation woi.width="+woi.width+" > gpu_width="+OrthoMap.gpu_width+". Truncating.");
			}
			woi.width = OrthoMap.gpu_width;
		}
		if (woi.height > OrthoMap.gpu_height) {
			if (debugLevel > -3) {
				System.out.println("correlateOrthoPair() correlation woi.height="+woi.height+" > gpu_height="+OrthoMap.gpu_height+". Truncating.");
			}
			woi.height = OrthoMap.gpu_height;
		}
		final int  gpu_width = OrthoMap.gpu_width; // static
		final int  gpu_height = OrthoMap.gpu_height; // static
		int tilesX = gpu_width/GPUTileProcessor.DTT_SIZE; 
    	int tilesY =  gpu_height/GPUTileProcessor.DTT_SIZE;
    	Rectangle tile_woi = scaleRectangle (woi, GPUTileProcessor.DTT_SIZE);
		// uses fixed_size gpu image size
		TpTask [][] tp_tasks = new TpTask [2][];
		
//		int num_tries = 5;
		double prev_rms = Double.NaN;
		double rel_improve = 1E-3;
		double [][] elevations = new double [tp_tasks.length][];
		double [][] ground_planes_metric = new double [tp_tasks.length][];
		boolean [][] ground_planes_masks = new boolean[2][];
		double    initial_above = 3; // m
		double    initial_below = 3; // m
		int       num_refine =    3;
		double    frac_above =    0.3;
		double    frac_below =    0.1;
		double [][] jtj = null;
		double rms = Double.NaN;
		for (int ntry = 0; ntry < num_tries; ntry++) {
			if (!batch_mode) {
				if (debugLevel>-3) {
					System.out.println("correlateOrthoPair(): ntry="+ntry);
				}
			}
			String dbg_suffix = batch_mode? null: String.format("_%02d", ntry);
			double [][][] vector_field = 
					ComboMatch.rectilinearVectorField(//rectilinearCorrelate_TD( // scene0/scene1
							clt_parameters, // final CLTParameters          clt_parameters,
							gpu_pair_img,        // final float  [][]            fpixels, // to check for empty
							gpu_width,      // final int                    img_width,
							woi, // Rectangle                    woi, // if null, use full GPU window
							affines_gpu,         // final double [][][]          affine,  // [2][2][3] affine coefficients to translate common to 2 images
							tp_tasks, // TpTask [][]                  tp_tasks_o,
							batch_mode,          // final boolean                batch_mode,
							dbg_suffix,//  final String                 dbg_suffix, // for image_names
							debugLevel);    // final int                    debugLevel);
			// get elevations
			int zoom_lev_tiles = zoom_lev-3;
			double [] ground_planes_weight = null;
			for (int num_elevations = 0; num_elevations < 1; num_elevations++) {
				for (int n = 0; n < gpu_pair.length; n++) {
					elevations[n] = ortho_maps[gpu_pair[n]]. getTileElevations(
							zoom_lev, // final int zoom_level,
							tp_tasks[n], // final TpTask [] tp_tasks, 
							tilesX, // final int tilesX,
							tilesY, // final int tilesY,
							debugLevel); // final int debugLevel)
					String debug_title = (show_vector_field && (ntry==0))? (ortho_maps[gpu_pair[n]].getName()+"_plane"+dbg_suffix):null;
					ground_planes_metric[n] = ortho_maps[gpu_pair[n]].getPlaneMeters (
							zoom_lev_tiles, // int       zoom_lev, // of the data (3 levels down for tiles)
							elevations[n],  // double [] elev,
							tilesX,         // int       width,
							initial_above,  // double    initial_above, // from average
							initial_below,  // double    initial_below, // from average, // positive value
							num_refine,     // int       num_refine,
							frac_above,     // double    frac_above,
							frac_below,     // double    frac_below,
							debug_title);   // String    debug_title)
					ground_planes_masks[n] = ortho_maps[gpu_pair[n]].removeHighElevationMismatch (
							zoom_lev_tiles,          // int        zoom_lev, // of the data (3 levels down for tiles)
							elevations[n],           // double []  elev,
							tilesX, // int        width,
							ground_planes_metric[n], // double []  plane_metric, // tiltx,tilty, offs - in meters
							metric_error);           // double     metric_error);
				}
				ground_planes_weight = new double[ ground_planes_masks[0].length];
				int num_left = 0; // , num_was = tp_tasks[0].length;
				for (int i = 0; i < ground_planes_weight.length; i++) {
					if (ground_planes_masks[0][i] && ground_planes_masks[1][i]) {
						num_left++;
						ground_planes_weight[i] = 1.0;
					}
				}
				System.out.println("correlateOrthoPair(): left "+num_left+" tiles (was "+tp_tasks[0].length+" before filtering). metric_error="+metric_error);
				System.out.println();
				if (num_left < tp_tasks[0].length/4) {
					num_elevations--;
					System.out.println("too few tiles remain, try again. metric_error="+metric_error);
				}
			}

			
//	    	Rectangle tile_woi = scaleRectangle (woi, GPUTileProcessor.DTT_SIZE);
	    	double max_err=              7.0;
	    	int num_bins =            1000;
	    	boolean ignore_strength = false;
	    	//double  frac_remove =        0.15;
	    	double [][] vf_error2 = new double [vector_field.length][]; // may be able to skip [0]
	    	double [][][] vector_field_bkp = show_vector_field? new double[vector_field.length][][]:null;
	    	if (vector_field_bkp != null) {
	    		for (int i = 0; i < vector_field_bkp.length; i++) {
	    			vector_field_bkp[i] = vector_field[i].clone(); 
	    		}
	    	}
	    	for (int nvf_mode = 0; nvf_mode < vector_field.length; nvf_mode++) {
	    		vf_error2[nvf_mode] = getVectorFieldError2(
	    				vector_field[nvf_mode], // double [][] vector_field,
	    			    tilesX,                 // int         width, // tilesX
	    			    tile_woi);              // Rectangle   twoi); // in tiles
	    		double threshold = filterVectorField(
	    				vector_field[nvf_mode], // final double [][] vector_field,
	    				vf_error2[nvf_mode],    // final double []   vf_error2,
	    				frac_remove,            // final double      frac_remove,
	    				ignore_strength,        // final boolean     ignore_strength,
	    				num_bins,               // final int         num_bins,
	    				max_err,                // final double      max_err,
	    				tilesX, // final int         width, // tilesX
	    				tile_woi);              //final Rectangle   twoi); // in tiles
				if (debugLevel>-3) {
					System.out.println("vector_field layer= "+nvf_mode+" error threshold = "+threshold+" pix.");
				}
	    	}
	    	
	    	// TODO: use crop, keep (0,0) (woi.x+woi.width, woi.y+woi.height 
			if (vector_field_bkp != null) { // show_vector_field) {
//				double [][] dbg_vf = new double [8 * vector_field.length][tilesX * tilesY];
				int dbg_width =  tile_woi.x+tile_woi.width;
				int dbg_height = tile_woi.y+tile_woi.height;
				double [][][][] vf_all = {vector_field_bkp,vector_field};
				double [][] dbg_vf = new double [8 * vector_field.length+5][dbg_width * dbg_height];
				String [] dbg_titles = new String[dbg_vf.length];
				String [] prefix= {"single","single_filtered","neibs","neibs_filtered"};
				for (int n = 0; n < 2*vector_field.length; n++) {
					dbg_titles [4*n+0] = prefix[n]+"-vx";	
					dbg_titles [4*n+1] = prefix[n]+"-vy";	
					dbg_titles [4*n+2] = prefix[n]+"-str";	
					dbg_titles [4*n+3] = prefix[n]+"-err";	
				}
				dbg_titles [8 * vector_field.length + 0] = "mask-elev";
				dbg_titles [8 * vector_field.length + 1] = "elev-0";
				dbg_titles [8 * vector_field.length + 2] = "elev-1";
				dbg_titles [8 * vector_field.length + 3] = "mask-0";
				dbg_titles [8 * vector_field.length + 4] = "mask-1";
						
				for (int i = 0; i < dbg_vf.length; i++) {
					Arrays.fill(dbg_vf[i], Double.NaN);
					
				}
				for (int l = 0; l<dbg_vf[0].length; l++) { // local tile
					int lx = l % dbg_width;
					int ly = l / dbg_width;
					int t = lx + ly * tilesX; // full width
					for (int n = 0; n < vector_field.length; n++) {
						for (int m = 0; m < vf_all.length; m++) {
							if (vf_all[m][n][t] != null) {
								for (int k = 0; k < 3; k++) {
									dbg_vf[m*4 + n*8 + k][l] = vf_all[m][n][t][k];
								}
								// will automatically apply filtered
								dbg_vf[m*4 + n*8 + 3][l] = Math.sqrt(vf_error2[n][t]);
							}
						}
					}
					dbg_vf [8 * vector_field.length + 0][l] = ground_planes_weight[t];
					dbg_vf [8 * vector_field.length + 1][l] = elevations[0][t];
					dbg_vf [8 * vector_field.length + 2][l] = elevations[1][t];
					dbg_vf [8 * vector_field.length + 3][l] = ground_planes_masks[0][t]?1.0:0.0;
					dbg_vf [8 * vector_field.length + 4][l] = ground_planes_masks[1][t]?1.0:0.0;
				}
	    		ShowDoubleFloatArrays.showArrays(
	    				dbg_vf,
	    				dbg_width,
	    				dbg_height,
	    				true,
	    				"vector_field"+dbg_suffix,
	    				dbg_titles);
			}

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


			OrthoPairLMA orthoPairLMA = new OrthoPairLMA();
			// vector_field[1] - neighbors
			double lambda =            0.1;
			double lambda_scale_good = 0.5;
			double lambda_scale_bad =  8.0;
			double lambda_max =      100;
			double rms_diff =          0.001;
			int    num_iter =         20;
			boolean last_run =       false;
			orthoPairLMA.prepareLMA(
					// will always calculate relative affine, starting with unity
					tilesX, // int           width,
					vector_field[1], // double [][]   vector_XYS,    // optical flow X,Y, confidence obtained from the correlate2DIterate()
					tile_centers, // double [][]   centers,       // tile centers (in pixels)
					null, // double []     weights_extra, // optional, may be null 
					true, // boolean       first_run,
					debugLevel); // final int     debug_level)
			int lma_rslt = orthoPairLMA.runLma( // <0 - failed, >=0 iteration number (1 - immediately)
					lambda,             // double lambda,           // 0.1
					lambda_scale_good,  // double lambda_scale_good,// 0.5
					lambda_scale_bad,   // double lambda_scale_bad, // 8.0
					lambda_max,         // double lambda_max,       // 100
					rms_diff,           // double rms_diff,         // 0.001
					num_iter,           // int    num_iter,         // 20
					last_run,           // boolean last_run,
					debugLevel);       // int    debug_level)
			if (debugLevel > -3) {
				System.out.println("LMA result = "+lma_rslt);
			}
			if (lma_rslt < 0) {
				System.out.println("LMA failed, result="+lma_rslt);
				return null;
			}
			rms = orthoPairLMA.getRms();
			if (debugLevel > -3) {
				System.out.println("RMS= "+rms+" ("+orthoPairLMA.getInitialRms()+")");
			}
			if (rms > prev_rms) {
				if (debugLevel > -3) {
					if ((rms-prev_rms)/prev_rms < rel_improve) {
						System.out.println("LMA RMSE worsened, but less than improvement threshold: new"+rms+" ("+ orthoPairLMA.getInitialRms()+"), prev="+prev_rms);
					} else {
						System.out.println("LMA RMSE worsened: new "+rms+" ("+ orthoPairLMA.getInitialRms()+"), prev="+prev_rms);
					}
				}
				if (ignore_prev_rms) {
					if (debugLevel > -3) {
						System.out.println("Will continue, as ignore_prev_rms is set");
					}					
				} else {
					break;
				}
			}
			affines_gpu[1]=OrthoMap.combineAffine(affines_gpu[1], orthoPairLMA.getAffine());
			jtj = orthoPairLMA.getLastJtJ();
			if ((prev_rms - rms)/prev_rms < rel_improve) {
				if (debugLevel > -2) {
					System.out.println("LMA relative RMSE improvement = "+((prev_rms - rms)/prev_rms)+" < "+rel_improve+", exiting.");
				}
				if (ignore_prev_rms) {
					if (debugLevel > -3) {
						System.out.println("Will continue, as ignore_prev_rms is set");
					}					
				} else {
					break;
				}
			}
			prev_rms=rms;
		} // for (int ntry = 0; ntry < num_tries; ntry++) {
		if (pairwiseOrthoMatch != null) {
			pairwiseOrthoMatch.jtj = jtj;
			pairwiseOrthoMatch.rms=  rms;
		}
		
		double [] tlo_src_metric_other = new double[2]; // check affines[][][] here - 
		for (int i = 0; i < 2; i++) {
			tlo_src_metric_other[i] = affines_gpu[1][i][2] * pix_size - ortho_maps[gpu_pair[1]].vert_meters[i];
			for (int j = 0; j < 2; j++) {
				affines[1][i][j] = affines_gpu[1][i][j];
			}
		}
		for (int i = 0; i < 2; i++) {
			affines[1][i][2] = tlo_src_metric_other[i]  -
					tlo_rect_metric[1][0] * affines[1][i][0] -
					tlo_rect_metric[1][1] * affines[1][i][1];
		}
		if (debugLevel>-3) {
			System.out.println("correlateOrthoPair(): adjusted affines[1]");
			System.out.println("[["+affines[1][0][0]+","+affines[1][0][1]+","+affines[1][0][2]+"],");
			System.out.println(" ["+affines[1][1][0]+","+affines[1][1][1]+","+affines[1][1][2]+"]]");
		}
		
		// modify affines[1]
		
		if ((debugLevel > 1) && (num_tries>0)) {// show result here
			String [] map_names = {ortho_maps[gpu_pair[0]].getName(),ortho_maps[gpu_pair[1]].getName()};
			ShowDoubleFloatArrays.showArrays( 
					gpu_pair_img,
					OrthoMap.gpu_width,
					OrthoMap.gpu_height,
					true,
					"gpu_pair-zoom"+zoom_lev+"-"+ortho_maps[gpu_pair[0]].getName()+"-"+ortho_maps[gpu_pair[1]].getName(),
					map_names);
		}
		if (calc_warp) {
			double scale = 2.0;
			String dbg_suffix = batch_mode? null: "_warp";

		// return only neighbors vector field
			double [][] vf = ComboMatch.rectilinearVectorField(//rectilinearCorrelate_TD( // scene0/scene1
					clt_parameters,    // final CLTParameters          clt_parameters,
					gpu_pair_img,      // final float  [][]            fpixels, // to check for empty
					gpu_width,         // final int                    img_width,
					woi,               // Rectangle                    woi, // if null, use full GPU window
					affines_gpu,       // final double [][][]          affine,  // [2][2][3] affine coefficients to translate common to 2 images
					tp_tasks,          // TpTask [][]                  tp_tasks_o,
					batch_mode,        // final boolean                batch_mode,
					dbg_suffix,        // final String                 dbg_suffix,
					debugLevel)[1];    // final int                    debugLevel);
			double [][] vf_interpolated = interpolateVectorField(
					vf,                // final double [][] vector_field, // sparse {vx,vy,strength}
					tp_tasks[1],       // final TpTask []   tp_tasks, // to remove no-overlap tiles
					tilesX,            // final int gpu_tilesX, // 512
					tile_woi,          // final Rectangle tile_woi, // only width, height are used top-left corners are the same
					scale,             // final double scale,
					1);                // final int debugLevel); // vector_filed_filled - use 2
			double [] tl_metric = {bounds_overlap_meters[0][0],bounds_overlap_meters[1][0]};
			
			FineXYCorr warp = new FineXYCorr(
					zoom_lev,          // int zoom_lev,
					tile_woi.width,    // int width,
					tl_metric,         // double [] tl_metric,// relative to the reference image vertical point, normally negative
					vf_interpolated);  // double [][] vf);
			return warp;
		}
		return null;
	}
	
	/**
	 * Project object offset to along and perpendicular to the scenes offset
	 * @param object_offset object center pixel x and y offset between images (y - down) 
	 * @param scene_offset pixel offset between images 
	 * @return A pair of offsets: parallel (to scene offset) and perpendicular. Negative parallel
	 * offsets corresponds to objects above ground  
	 */
	public static double [] projectOffsetOnVector(
			double [] object_offset,
			double [] scene_offset) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		double [] offs_rot = new double [2];
		offs_rot[0] = ( object_offset[0]*scene_offset[0] + object_offset[1]*scene_offset[1]) / l1;
		offs_rot[1] = (-object_offset[0]*scene_offset[1] + object_offset[1]*scene_offset[0]) / l1;
		return offs_rot;
	}
	
	/**
	 * Estimate object height if its offset between image is caused by its elevation
	 * @param object_offset object center pixel x and y offset between images (y - down) 
	 * @param scene_offset pixel offset between images 
	 * @param zoom_level images zoom level
	 * @param agl scene (second) altitude above ground in meters
	 * @return Estimated object elevation above ground in meters
	 */
	public static double offsetToElevation(
			double [] object_offset,
			double [] scene_offset,
			int       zoom_level,
			double    agl) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		double offs_par = projectOffsetOnVector(object_offset, scene_offset)[0]; // *OrthoMap.getPixelSizeMeters(zoom_level);
		return -agl * offs_par / l1 ;
	}
	
	/**
	 * Calculate (negative) pixel offset parallel to the scene offset corresponding to object height
	 * @param height
	 * @param scene_offset
	 * @param zoom_level
	 * @param agl
	 * @return
	 */
	public static double elevationToParallelOffset(
			double    height,
			double [] scene_offset,
			int       zoom_level,
			double    agl) {
		double l1 = Math.sqrt(scene_offset[0]*scene_offset[0] + scene_offset[1]*scene_offset[1]);
		return -height/agl * l1 ;
	}
	
	
	
	public static Rectangle scaleRectangle(
			Rectangle woi,
			int tile_size){
		int min_tx = woi.x/tile_size;
		int min_ty = woi.y/tile_size;
		int max_tx1 = (woi.x + woi.width +  (tile_size -1)) / tile_size; 
		int max_ty1 = (woi.y + woi.height + (tile_size -1)) / tile_size; 
		return new Rectangle(min_tx,min_ty,max_tx1 - min_tx,max_ty1 - min_ty);
	}
	
	public static double [] getVectorFieldError2(
			double [][] vector_field,
			int         width, // tilesX
			Rectangle   twoi) { // in tiles
		final Rectangle woi = (twoi != null)? twoi: (new Rectangle (0,0,width,vector_field.length/width));
		final int num_tiles = woi.width*woi.height;
		final int tlTile = width * woi.y + woi.x;
		final double [] vf_error2 = new double [vector_field.length];
//		Arrays.fill(vf_error2, Double.NaN);
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if (vector_field[tile] != null) {
							double [] xys = vector_field[tile];
							vf_error2[tile] += xys[0]*xys[0]+xys[1]*xys[1]; 
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return vf_error2;
	}
	
	public static double filterVectorField(
			final double [][] vector_field,
			final double []   vf_error2,
			final double      frac_remove,
			final boolean     ignore_strength,
			final int         num_bins,
			final double      max_err,
			final int         width, // tilesX
			final Rectangle   twoi) { // in tiles
		final Rectangle woi = (twoi != null)? twoi: (new Rectangle (0,0,width,vector_field.length/width));
		final int STRENGTH_INDEX=2;
		final double max_err2 = max_err*max_err;
		final double scale = num_bins/max_err2;
		final int num_tiles = woi.width*woi.height;
		final int tlTile = width * woi.y + woi.x;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [][] hist2 = new double [threads.length][num_bins];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if (!Double.isNaN(vf_error2[tile]) && (vector_field[tile] != null)){ // one test is enough
							int bin = (int) Math.round(vf_error2[tile]*scale);
							if (bin >= num_bins) {
								bin= num_bins - 1;
							}
							hist2[thread_num][bin] += ignore_strength? 1.0 : vector_field[tile][STRENGTH_INDEX]; 
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		final double [] hist =    new double [num_bins];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int bin = ai.getAndIncrement(); bin < num_bins; bin = ai.getAndIncrement()) {
						for (int i = 0; i < hist2.length; i++) {
							hist[bin]+= hist2[i][bin];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double sw = 0;
		for (int bin = 0; bin < num_bins; bin++) {
			sw += hist[bin];
		}
		double aremove = sw * frac_remove;
		double sh = 0, shp = 0;
		double t2 = max_err2;
		for (int bin = num_bins-1; bin>=0; bin--) {
			shp = sh;
			sh += hist[bin];
			if (sh > aremove) {
				double r = (sh-aremove)/(sh-shp);
				t2 = (bin + r)/scale;
				break;
			}
		}
		final double threshold2 = t2;
		// remove bad tiles
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
						int itileX= nTile%woi.width;
						int itileY= nTile/woi.width;
						int tile = tlTile + itileY * width + itileX;
						if ((vector_field[tile] != null) && !(vf_error2[tile] <= threshold2)){
							vector_field[tile]= null;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return Math.sqrt(threshold2);
	}	
	
	
	
	
	
	public static OrthoMapsCollection readOrthoMapsCollection (
			String path) throws IOException, ClassNotFoundException {
		FileInputStream fileInputStream  = new FileInputStream(path);
		ObjectInputStream objectInputStream  = new ObjectInputStream(fileInputStream);
		OrthoMapsCollection orthoMapsCollection = (OrthoMapsCollection) objectInputStream.readObject();
		objectInputStream.close();
		return orthoMapsCollection;
	}
	
	public void writeOrthoMapsCollection (String path) throws IOException {
		FileOutputStream fileOutputStream = new FileOutputStream(path);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
		objectOutputStream.writeObject(this);
	    objectOutputStream.flush();
	    objectOutputStream.close();
	}
	
	public static double [][] interpolateVectorField(
			final double [][] vector_field, // sparse {vx,vy,strength}
			final TpTask []   tp_tasks, // to remove no-overlap tiles
			final int         gpu_tilesX, // 512
			final Rectangle   tile_woi, // only width, height are used top-left corners are the same
			final double      scale,
			final int         debugLevel) {
		final int tiles = tile_woi.width*tile_woi.height;
		final double [][] vf = new double[2][tile_woi.width*tile_woi.height];
		for (int i = 0; i < vf.length; i++) {
			Arrays.fill(vf[i], Double.NaN);
		}
		final boolean [] mask = (tp_tasks==null)? null: new boolean [tiles];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < tiles; ipix = ai.getAndIncrement()) {
						int y = ipix / tile_woi.width;
						int x = ipix % tile_woi.width;
						double [] v = vector_field[y*gpu_tilesX+x];
						if (v != null) {
							for (int i = 0; i < vf.length; i++) {
								vf[i][ipix] = scale*v[i];
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);

		if (mask != null) {
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int iTile = ai.getAndIncrement(); iTile < tp_tasks.length; iTile = ai.getAndIncrement()) {
							TpTask task = tp_tasks[iTile];
							int[] txy = task.getTileXY();
							if ((txy[0] < tile_woi.width) && (txy[1] < tile_woi.height)) {
								mask[txy[0] + txy[1] * tile_woi.width] = true;
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
        
        // now fill NaNs in each of vf[0], vf[1]
        double [][]  vf_filled = new double[vf.length][];
		int       num_passes = 100;
		double    max_diff = 1E-4;
    	for (int i = 0; i < vf.length; i++) {
    		vf_filled[i] = OpticalFlow.fillGapsDouble(
    				vf[i],                // double []  data,
    				mask,                 // boolean [] mask_in, // do not process if false (may be null)
    				tile_woi.width,       // int       width,
    				0,                    // int       max_grow,
    				num_passes,           // int       num_passes,
    				max_diff,             // double    max_diff,
    				ImageDtt.THREADS_MAX, // int       threadsMax,
    				debugLevel-1);        // int       debug_level)
    	}		
		if (debugLevel > 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);
		}
		double [][] vf_out = new double [tiles][];
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < tiles; ipix = ai.getAndIncrement()) {
						if (!Double.isNaN(vf_filled[0][ipix]) && !Double.isNaN(vf_filled[1][ipix])) {
							vf_out[ipix] = new double[] {vf_filled[0][ipix],vf_filled[1][ipix]};
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return vf_out;
	}
	
	public ImagePlus patternMatchDualWrap (
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp) {    // use for a single pair only
		
//		double []     min_corrs = {0.008, 0.0035}; // morning/evening
//		double []     min_corrs = {0.0095, 0.0035}; // morning/evening
		double []     min_corrs = {0.0085, 0.0036}; // 0.0035}; // morning/evening
		double []     phaseCoeff = {0.98,0.5};
//		double        min_corr_full_rel = 0.75; // try 0.85
		double        min_corr_full_rel = 0.85; // try 0.85
		double        full_preference  =  1.4;
		double        max_min_ratio =     3.0;
		boolean[]     combine_full =      {true, true}; // try for other olso?
		int           corr_size =       128; // pix
		double        adv_radius =       30.0; // cm 
		double        search_radius =    15;    // look for other scene max within this distance from main
		String        object_type =      "TM62";
		double        scale_warp =        1.0; // use 4?
		int           extr_size =       128;
		boolean       remove_dc =        true;
		
		// filters
		boolean       filt_keep =       false; // list filtered out, but actually keep them
		boolean       filt_atonce =     false; // apply main/second together, false - first filter main only
		double        filt_frac_max =   0.6;   // 0.5;  // measure are at fraction of max
		double [][]   filt_max_radius = {{1.0,1.0},{1.2,1.2}}; // {1.0,1.0}}; // [second/main][full/half] - inactive now
//		double [][]   filt_elongation = {{1.5,1.5},{1.7,1.7}}; // [second/main][full/half] ellipse axis ratio
		double [][]   filt_elongation = {{1.7,1.7},{1.7,1.7}}; // [second/main][full/half] ellipse axis ratio
		double        filt_other_rad  = 25; // should be no disconnected high peaks in this radius (only for halves multiplied)
		double []     filt_other_frac = {0.8,0.8}; // no pixels around max at filt_other_rad should be higher than this fraction of max
		
//		double []     filt_dist =       {8.0,5.0};  // full offset and perpendicular to images offsets
		// make conditional distance - decrease if small offset (increase for large to accommodate +30/-10cm
		double []     filt_dist =       {10.0,5.0};  // full offset and perpendicular to images offsets
		double        filt_height =     0.2; // allow extra negative offset parallel to image offset, corresponding to object above ground
		
		
		int           debugLevel =        1;

		GenericJTabbedDialog gd = new GenericJTabbedDialog("Correlate two images with patterns",1200,900);
		gd.addNumericField("Minimal correlation, first",        min_corrs[0],  5,7,"","Minimal correlation value to keep, first scene.");
		gd.addNumericField("Minimal correlation, second",       min_corrs[1],  5,7,"","Minimal correlation value to keep, second scene.");
//		gd.addNumericField("Maximal peak area, first",          max_area[0],  5,7,"","Maximal cross-section area of the peak at certain height.");
//		gd.addNumericField("Maximal peak area, second",         max_area[1],  5,7,"","Maximal cross-section area of the peak at certain height.");
		gd.addNumericField("Phase correlation main",            phaseCoeff[0],      3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
		gd.addNumericField("Phase correlation other",           phaseCoeff[1], 3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
		gd.addNumericField("Minimal relative full correlation", min_corr_full_rel,  5,7,"","Minimal relative correlation value with full circular pattern to keep.");
		gd.addNumericField("Prefer full pattern",               full_preference,  3,7,"","Prefer full pattern to partial; scale by this value.");
		gd.addNumericField("Max/min halves ratio",              max_min_ratio,  3,7,"","Ratio of halves best/worst to use half-pattern.");
		gd.addCheckbox    ("Combine with full pattern, main",   combine_full[0], "Multiply by normalized full pattern correlation maximum.");
		gd.addCheckbox    ("Combine with full pattern, other",  combine_full[1], "Multiply by normalized full pattern correlation maximum.");
		gd.addNumericField("Correlation size",                  corr_size,  0,4,"pix", "Should be power of 2.");
		gd.addNumericField("Adversarial distance",              adv_radius,  1,6,"pix", "Suppress weaker if they have closer strong ones.");
		gd.addNumericField("Search other scene max",            search_radius,  1,6,"pix", "Search for other scene max from the main one.");
		gd.addStringField ("Object type",                       object_type, 20,"Object type/name.");
		gd.addNumericField("Scale warp",                        scale_warp,  1,6,"x", "Scale second image warping.");
		gd.addNumericField("Extract object size",               extr_size,  0,4,"", "Size of square object image to extract.");
		gd.addCheckbox    ("Remove DC",                         remove_dc, "Remove DC in extracted object images.");
		gd.addMessage("--- Filtering results ---");
		gd.addCheckbox    ("Keep filtered out",                 filt_keep, "List filtered out, but actaually keep them.");
		gd.addCheckbox    ("Apply filters at once",             filt_atonce, "If false, first filter by main scene only, do not process secondary scene if main failed.");
		gd.addNumericField("Peak hight fraction",               filt_frac_max,     5,7,"","Fraction of the peak hight for eigenvlues (radius and elongation).");
		gd.addNumericField("Max radius, main, full",            filt_max_radius[0][0],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, main, halves",          filt_max_radius[0][1],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, other, full",           filt_max_radius[1][0],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, other, half",           filt_max_radius[1][1],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max elongation, main, full",        filt_elongation[0][0],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, main, halves",      filt_elongation[0][1],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, other, full",       filt_elongation[1][0],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, other, halves",     filt_elongation[1][1],  5,7,"","Maximal peak ellipse axis ratio at certain height.");
		
		gd.addNumericField("Other peak distance",               filt_other_rad,  5,7,"pix","Should be no disconnected peaks within this radius from the used one. Set to 0 to disable.");
		gd.addNumericField("Other peak fraction, main scene",   filt_other_frac[0],  5,7,"","Maximal near peak fraction of the used maximum. Only for halves.");
		gd.addNumericField("Other peak fraction, other scene",  filt_other_frac[1],  5,7,"","Maximal near peak fraction of the used maximum. Only for halves.");
		
		gd.addNumericField("Maximal offset",                    filt_dist[0],     5,7,"pix","Maximal other scene offset from the main.");
		gd.addNumericField("Maximal offset perpendicular",      filt_dist[1],     5,7,"pix","Maximal other scene offset perpendicular to offset between scenes.");
		gd.addNumericField("Maximal object height",             filt_height,      5,7,"m",  "Maximal object height to allow extra offset.");
		//
		
		gd.addNumericField("Debug level",                       debugLevel,  0,4,"", "Debug level.");
		gd.showDialog();
		if (gd.wasCanceled()) return null;
		min_corrs[0]=         gd.getNextNumber();
		min_corrs[1]=         gd.getNextNumber();
		
//		max_area[0]=          gd.getNextNumber();
//		max_area[1]=          gd.getNextNumber();
		
		phaseCoeff[0]=        gd.getNextNumber();
		phaseCoeff[1]=      gd.getNextNumber();
		min_corr_full_rel=    gd.getNextNumber();
		full_preference=      gd.getNextNumber();
		max_min_ratio=        gd.getNextNumber();		
		combine_full[0] =     gd.getNextBoolean();
		combine_full[1] =     gd.getNextBoolean();
		corr_size=      (int) gd.getNextNumber();
		adv_radius=           gd.getNextNumber();
		search_radius=        gd.getNextNumber();
		object_type=          gd.getNextString();		
		scale_warp =          gd.getNextNumber();
		extr_size=      (int) gd.getNextNumber();
		remove_dc=            gd.getNextBoolean();
		
		filt_keep=            gd.getNextBoolean();
		filt_atonce=          gd.getNextBoolean();
		filt_frac_max=        gd.getNextNumber();
		filt_max_radius[0][0]=gd.getNextNumber();
		filt_max_radius[0][1]=gd.getNextNumber();
		filt_max_radius[1][0]=gd.getNextNumber();
		filt_max_radius[1][1]=gd.getNextNumber();
		filt_elongation[0][0]=gd.getNextNumber();
		filt_elongation[0][1]=gd.getNextNumber();
		filt_elongation[1][0]=gd.getNextNumber();
		filt_elongation[1][1]=gd.getNextNumber();
		
		filt_other_rad=       gd.getNextNumber();
		filt_other_frac[0]=   gd.getNextNumber();
		filt_other_frac[1]=   gd.getNextNumber();
		
		filt_dist[0] =        gd.getNextNumber();
		filt_dist[1] =        gd.getNextNumber();
		filt_height =        gd.getNextNumber();
		debugLevel=     (int) gd.getNextNumber();

		return patternMatchDual (
				indices,           // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				affines,           // double [][][] affines, // null or [indices.length][2][3] 
				warp,              // FineXYCorr    warp);
				min_corrs,         // double []     min_corrs, // one per
				phaseCoeff,        // double []     phaseCoeff
				min_corr_full_rel, // double        min_corr_full_rel,
				full_preference,   // double        full_preference,
				max_min_ratio,     // double        max_min_ratio,
				combine_full,      // boolean[]     combine_full,
				corr_size,         // int           corr_size,
				adv_radius,        // double        adv_radius,    // 
				search_radius,     // double        search_radius, // 
				object_type,       // String        object_type,
				scale_warp,        // double        scale_warp,
				extr_size,         // int           extr_size,
				remove_dc,         // boolean remove_dc,
				filt_keep,         // boolean       filt_keep,
				filt_atonce,       // boolean       filt_atonce,
				filt_frac_max,     // double        frac_max,  // measure are at fraction of max			
				filt_max_radius,   // double [][]   filt_max_radius,
				filt_elongation,   // double   [][]   filt_elongation,
				filt_other_rad,    // double        filt_other_rad,  // should be no disconnected high peaks in this radius (only for halves multiplied)
				filt_other_frac,   // double []     filt_other_frac, // no pixels around max at filt_other_rad should be higher than this fraction of max
				filt_dist,         // double []     filt_dist,
				filt_height,       // double        filt_height,
				debugLevel);       // int           debugLevel)			
	}

	public ImagePlus patternMatchDual (
			int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
			double [][][] affines, // null or [indices.length][2][3] 
			FineXYCorr    warp,    // use for a single pair only
			double []     min_corrs, // one per
			double []     phaseCoeff, // one per
			double        min_corr_full_rel,
			double        full_preference,
			double        max_min_ratio,
			boolean[]     combine_full,
			int           corr_size,
			double        adv_radius, //
			double        search_radius, // look for other scene max within this distance from main
			String        object_type,
			double        scale_warp,
			int           extr_size,
			boolean       remove_dc,
			boolean       filt_keep,
			boolean       filt_atonce,
			double        frac_max,  // measure are at fraction of max			
			double [][]   filt_max_radius,
			double [][]   filt_elongation,
			double        filt_other_rad,  // should be no disconnected high peaks in this radius (only for halves multiplied)
			double []     filt_other_frac, // no pixels around max at filt_other_rad should be higher than this fraction of max
			double []     filt_dist,
			double        filt_height,
			
			int           debugLevel) {
		warp.scale_warp = scale_warp; 
		int       corr_radius = (int) (Math.sqrt(0.5)* adv_radius) -1 ;
		//						String  title_match = OrthoMap.removeKnownExtension(imp_src.getTitle())+"-PATTERN_CORRS"
    	int min_zoom_lev = Math.min(
    			ortho_maps[indices[0]].getOriginalZoomLevel(),
    			ortho_maps[indices[1]].getOriginalZoomLevel());
    	int max_zoom_lev = Math.max(
    			ortho_maps[indices[0]].getOriginalZoomLevel(),
    			ortho_maps[indices[1]].getOriginalZoomLevel());
		int     zoom_level = min_zoom_lev;
		int []  wh = new int[2];
		int []  origin = new int[2];
		double [][] centers = new double [indices.length][];
		double [][] dmulti = renderMultiDouble (
				false,    // boolean     use_alt,
				indices,    // int []        indices, // null or which indices to use (normally just 2 for pairwise comparison)
				true, // boolean       bounds_to_indices,
				affines,    // double [][][] affines, // null or [indices.length][2][3]
				warp,       // FineXYCorr    warp,,
				zoom_level, // int         zoom_level,
				wh,         // int []      wh,
				origin,     // int []      origin){ // maps[0] as a reference
				centers);   // double [][] centers)
		int width =  wh[0];
		int height = wh[1];
		double [][] src_marks = null;
		
		double [][][] patterns_all = new double [indices.length][][];
		double [][] kernels_all = new double [indices.length][];
		
		GroundObjectPattern [] gops = new GroundObjectPattern[indices.length];
		AltitudeMismatchKernel[] ak = new AltitudeMismatchKernel[indices.length]; 
		int [] pattern_sizes = new int[indices.length];
		String [][]  pattern_labels = new String[indices.length][];
		double [][][] corrs_out =     new double[indices.length][][]; 
		double [][][] convolve_out =  new double[indices.length][][]; 
		double [][][] corr_patterns = new double[indices.length][][];
		double [][][] filtered_corrs = new double[indices.length][][];		
		int [] zoomout = new int[indices.length];
		ImagePlus [] imp_corrs = new ImagePlus[indices.length];
		double [][]   bestcorr = new double[indices.length][];
		double [][]   fullcorr = new double[indices.length][];
		int [][]      bestpatt = new int   [indices.length][];
		double [] min_corrs_full = new double[indices.length];
		ArrayList<ArrayList<Point>> object_lists = new ArrayList<ArrayList<Point>>();
		ImagePlus [] imps_best = new ImagePlus[indices.length];
		ImagePlus [] imps_bestpatt = new ImagePlus[indices.length];
		double [][] convolved_merged = new double[indices.length][];
		ImagePlus [] imps_patt_corrs = new ImagePlus[indices.length];
		ImagePlus [] imps_patt_conv = new ImagePlus[indices.length];
		ImagePlus [] imps_extracted = new ImagePlus[indices.length];
		ImagePlus [] imps_extracted_corr = new ImagePlus[indices.length];
		ImagePlus [] imps_extracted_corr_half = new ImagePlus[indices.length];
		
		ArrayList <ItemMatch> matches_list = new ArrayList <ItemMatch>();
		double [][][] extracted_objects =    new double [indices.length][][]; 
		double [][][] extr_corr =            new double [indices.length][][]; 
//		double [][][] extr_corr_best =       new double [indices.length][][];   // only for main - already combined 
		double [][][] extr_corr_half =       new double [indices.length][][]; 
		double [][][][] filter_data =        new double [indices.length][][][]; // [scene][max][0-full, 1 - half][0- max, 1 - area, 2 - distance]
		int [] best_patt_indx;  
		
		for (int scene_num = 0; scene_num < indices.length; scene_num++) { // will probably just use first one, second - derivative
			gops[scene_num] = GroundObjectPattern.getPattern(
					object_type, // String        object_type,
					ortho_maps[indices[scene_num]].getLocalDateTime(), // LocalDateTime utcDateTime,
					patterns); // ArrayList<GroundObjectPattern> patterns)
			if (gops[scene_num] == null) {
				System.out.println("Failed to find a pattern for object \""+object_type+"\", aborting operation.");
				return null;
			}
			ImagePlus imp_pattern = new ImagePlus(gops[scene_num].getPatternPath());
			pattern_sizes[scene_num] =  imp_pattern.getWidth();
			if (pattern_sizes[scene_num] == 0) {
				System.out.println("testPatternCorrelate(): pattern \""+gops[scene_num].getPatternPath()+"\" is not found.");
				return null;
			}
			ImageStack stack_pattern = imp_pattern.getStack();
			int nSlices =       stack_pattern.getSize();
			patterns_all[scene_num] =   new double[nSlices][];
			pattern_labels[scene_num] = new String[nSlices];
			for (int k = 0; k < patterns_all[scene_num].length; k++) {
				pattern_labels[scene_num][k]=stack_pattern.getShortSliceLabel(k+1);
				float [] fpixels_pattern = (float[]) stack_pattern.getPixels(k+1);
				patterns_all[scene_num][k]=new double[fpixels_pattern.length];
				for (int i = 0; i < fpixels_pattern.length; i++) {
					patterns_all[scene_num][k][i] = fpixels_pattern[i];
				}
			}
			ak[scene_num] = AltitudeMismatchKernel.getKernel(
					gops[scene_num].getZoomLevel(), // zoom_level,       // int                               zoom_level,
					gops[scene_num].getAGL(), // double                            agl_from,
					ortho_maps[indices[scene_num]].getAGL(), // double                            agl_to,
					kernels); // ArrayList<AltitudeMismatchKernel> kernels)
			if (ak[scene_num] != null) {
				String kernel_path = ak[scene_num].getKernelPath();
				ImagePlus imp_kernel = new ImagePlus(kernel_path);
				if (imp_kernel.getWidth() > 0) {
					float [] kernel_pixels = (float[]) imp_kernel.getProcessor().getPixels();
					kernels_all[scene_num] = new double[kernel_pixels.length];
					for (int i = 0; i < kernel_pixels.length; i++) {
						kernels_all[scene_num][i] = kernel_pixels[i];
					}
				}
			}
			corrs_out[scene_num] =     new double[patterns_all[scene_num].length][]; 
			convolve_out[scene_num] =  new double[patterns_all[scene_num].length][]; 
			corr_patterns[scene_num] = new double[patterns_all[scene_num].length][];
			
			zoomout[scene_num] = 1;
			for (int i = zoom_level; i < gops[scene_num].getZoomLevel();i++) {
				zoomout[scene_num] *= 2;
			}
			
			for (int n = 0; n < patterns_all[scene_num].length; n++) {
				corr_patterns[scene_num][n] = OrthoMap.patternZoomCropPad(
						patterns_all[scene_num][n],      // double [] pattern,
						pattern_sizes[scene_num],        // int       pattern_size,
						corr_size,                       // int       size,
						zoomout[scene_num],              // int       zoomout,
						false);                          // out_normalize); // boolean   normalize) 
				if (kernels_all[scene_num] != null) {
					corr_patterns[scene_num][n] = OrthoMap.convolveWithKernel(
							corr_patterns[scene_num][n], // final double [] data,
							kernels_all[scene_num],      // final double [] kernel,
							corr_size);                  // final int width)
				}
				
			}				
		}
		// Splitting - above done for both scenes, below - just for the main one
		for (int scene_num = 0; scene_num < indices.length; scene_num++) { // will probably just use first one, second - derivative
			for (int n = 0; n < patterns_all[scene_num].length; n++) {
				corrs_out[scene_num][n]= OrthoMap.correlateWithPattern(
						dmulti[scene_num],           // final double [] data,
						width,                       // final int       width,
						corr_size,                   // final int       psize,      // power of 2, such as 64
						corr_patterns[scene_num][n], // final double [] pattern,    // [psize*psize]
						false,                       // final boolean   convolve,   // convolve, not correlate
						phaseCoeff[scene_num],       // final double    phaseCoeff,
						debugLevel);                 // final int       debugLevel) {
			}
			String [] patt_titles = new String[corrs_out[scene_num].length];
			for (int i = 0; i < patt_titles.length; i++) {
				patt_titles[i] = "patt_"+i;
			}
			imp_corrs[scene_num] = ShowDoubleFloatArrays.makeArrays(
					corrs_out[scene_num],
					width,
					height,
					ortho_maps[indices[scene_num]].getName()+"-PATTERN_CORRS",				
					patt_titles); // test_titles,
			if (src_marks != null) {
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imp_corrs[scene_num].setRoi(roi);
			}
			imp_corrs[scene_num].show();
			bestcorr[scene_num] = new double [dmulti[scene_num].length];
			fullcorr[scene_num] = new double [dmulti[scene_num].length];
			bestpatt[scene_num] = new int [bestcorr[scene_num].length];
			min_corrs_full[scene_num] = min_corrs[scene_num] * min_corr_full_rel;
			
			// TODO: scale adv_radius, corr_radius
			ArrayList<Point> plist =OrthoMap.combineDirCorrs (
				corrs_out[scene_num],      // final double [][] corrs,
				width,                     // final int         width,
				fullcorr[scene_num],       // final double []   fullcorr_in,
				bestcorr[scene_num],       // final double []   bestcorr_in
				bestpatt[scene_num],       // final int []      bestpatt_in,
				min_corrs[scene_num],      // final double      min_corr,
				min_corrs_full[scene_num], // final double      min_corr_full,
				full_preference,           // final double      full_preference,
				max_min_ratio,             // final double      max_min_ratio,
				combine_full[0],           // final boolean     combine_full,    // multiply by normalized full pattern correlation maximum
				adv_radius,                // final double      adv_radius,
				corr_radius);              // final int         corr_radius)
//			GroundObjectPattern gop = gops[scene_num];
			for (Point p : plist) {
				double [] matches = new double [corrs_out[scene_num].length + 1];
				double [] match_xy = {p.x%width, p.x/width}; // here int values
				int    best_patt = p.y + 1;
				matches[0] = bestcorr[scene_num][p.x];
				for (int i = 1; i < matches.length; i++) {
					matches[i] = corrs_out[scene_num][i-1][p.x];
				}
				ItemMatch item_match = new ItemMatch (match_xy); 
				item_match.addPatternMatches(
						gops[scene_num],        // GroundObjectPattern groundObjectPattern,
						matches,    // double [] matches,
						best_patt); // int      best_sub);
				matches_list.add(item_match);
			}
			int sort_pattern_index = 0; //combo. 1 - full pattern
			ArrayList<Integer> match_sort =  ItemMatch.sortByMatch(
					matches_list,        // ArrayList<ItemMatch> match_list,
					gops[scene_num],                 // GroundObjectPattern groundObjectPattern,
					sort_pattern_index); // int indx)
			// Print all results
			System.out.println("Match candidates ("+match_sort.size()+
					") that exceed minimal correlation strength of "+min_corrs[scene_num]);
			// remove non-overlapped from the list
			int num_non_overlap = 0;
			for (int i = match_sort.size()-1; i >=0; i--) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				int [] icenter_xy = match.getIntXY();
				for (int sn = 1; sn < indices.length; sn++) {
					if (Double.isNaN(dmulti[sn][icenter_xy[0] + icenter_xy[1] * width])) {
						match_sort.remove(i);
						if (debugLevel > -1) {
							System.out.println("Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
									" as it is not in the scenes overlap area");
						}
						num_non_overlap++;
						break;
					}
				}
				
			}
			
			// near the edge - check both images
			for (int i = match_sort.size()-1; i >=0; i--) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				int [] icenter_xy = match.getIntXY();
				boolean has_NaN = true;
				seach_NaN: {
					int half_size=corr_size/2;
					if  ((icenter_xy[0] < half_size) || (icenter_xy[1] < half_size) ||
							(icenter_xy[0] > (width - half_size)) || (icenter_xy[1] > (height - half_size))) {
						break seach_NaN;								
					}
					for (int y = (icenter_xy[1] - half_size); y < (icenter_xy[1] + half_size); y++) {
						int indx0 = y * width;
						for (int dy =- half_size; dy < half_size; dy++) {
							for (int x = (icenter_xy[0] - half_size); x < (icenter_xy[0] + half_size); x++) {
								int indx1 = indx0 + x;
								for (int sn = 0; sn < indices.length; sn++) {
									if (Double.isNaN(dmulti[sn][indx1])) {
										break seach_NaN;								
									}
								}
							}
						}

					}
					has_NaN = false;
				}
				if (has_NaN) {
					match_sort.remove(i);
					if (debugLevel > -1) {
						System.out.println("Removed object at x="+icenter_xy[0]+", y="+icenter_xy[1]+
								" as it is near the edge of one of the images");
					}
					num_non_overlap++;
				}
			}			
			if (num_non_overlap > 0) {
				System.out.println("Removed "+num_non_overlap+" objects outside of the scenes overlap, "+match_sort.size()+" remain");
			}
			System.out.print(String.format("%4s: %9s %8s %3s"," # ", "  x/y  ", " best ","sub"));
			for (int j = 0; j < (corrs_out[scene_num].length + 1); j++) if (j != sort_pattern_index) {
				System.out.print(String.format(" %8s ","match_"+j));
			}
			System.out.println();
			for (int i = 0; i < match_sort.size(); i++) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				double [] match_values = match.getMatchValues(gops[scene_num]);
				int [] icenter_xy = match.getIntXY();
				System.out.print(String.format("%4d: %4d/%4d %8.5f %3d",
						i,
						icenter_xy[0],icenter_xy[1],
						match.getMatchValue(gops[scene_num],sort_pattern_index),
						match.getPatternMatch(gops[scene_num]).getBestSub()));
				for (int j = 0; j < match_values.length; j++) if (j != sort_pattern_index) {
					System.out.print(String.format(" %8.5f",match_values[j]));
					if (j == (match_values.length-1)) {
						System.out.println();
					} else {
						System.out.print(",");
					}
				}
			}
//			boolean remove_dc = true;
			if (scene_num == 0) {
				double [][][] extract_display = new double [extracted_objects.length][][]; // to keep/display both dc/no-dc versions
				PointRoi roi = new PointRoi();
				roi.setOptions("nolabel"); // label");
				for (int mn=0; mn < match_sort.size(); mn++) {
					roi.addPoint(extr_size/2,extr_size/2,mn+1); // ,1);
				}
				// extract already calculated correlations for scene 0
				filter_data[scene_num] = new double[match_sort.size()][2][]; // [scene][max][0-full, 1 - half][0- max, 1 - area, 2 - distance]
				extr_corr[scene_num] =      new double [match_sort.size()][]; // extracted_objects[scene_num].length][];
				extr_corr_half[scene_num] = new double [match_sort.size()][]; // extracted_objects[scene_num].length][];
//				extr_corr_best[scene_num] = new double [match_sort.size()][]; // combined full with halves
				best_patt_indx = new int [match_sort.size()]; 
				for (int mn=0; mn < match_sort.size(); mn++) {
//					best_patt_indx[mn] = 
					int indx =match_sort.get(mn);
					ItemMatch match = matches_list.get(indx);
					int best_patt = match.getPatternMatch(gops[scene_num]).getBestSub() - 1;
					best_patt_indx[mn] = best_patt;
					double [] center_xy = match.getXY();
					extr_corr[scene_num][mn] = ObjectLocation.extractObjectImage(
							center_xy,  // double [] center_xy,
							extr_size,  // int       size,
							corrs_out[scene_num][0], // double [] src_img,
							width);     // int width)
					filter_data[scene_num][mn][0] = ObjectLocation.getMaxLocAreaNaN(
							extr_corr[scene_num][mn], // double [] data, // square data
							null, // double [] cent_xy, // if null, use center of the square
							search_radius, // double    radius, // search for maximum within this radius. Should radius be 0 here?
							frac_max, // double    frac_max)
							filt_other_rad, // double    other_radius,
							debugLevel);                   // final int       debugLevel) {
					extr_corr_half[scene_num][mn] = ObjectLocation.extractObjectImage(
							center_xy,  // double [] center_xy,
							extr_size,  // int       size,
							corrs_out[scene_num][best_patt], // double [] src_img,
//							bestcorr[scene_num], // double [] src_img,
							width);     // int width)
					// combine from scratch, best[] uses thresholding inside
					if (combine_full[0]) {
						double full_max_value = filter_data[scene_num][mn][0][0];
						if (!Double.isNaN(full_max_value)) {
							// multiply by full correlation, delete by full correlation maximum
							for (int i = 0; i < extr_corr_half[scene_num][mn].length; i++){
								extr_corr_half[scene_num][mn][i] *= Math.max(0, extr_corr[scene_num][mn][i]/full_max_value);
							}
						}
					}
					filter_data[scene_num][mn][1] = ObjectLocation.getMaxLocAreaNaN(
							extr_corr_half[scene_num][mn], // double [] data, // square data
							null, // double [] cent_xy, // if null, use center of the square
							search_radius, // double    radius, // search for maximum within this radius. Should radius be 0 here?
							frac_max, // double    frac_max)
							filt_other_rad, // double    other_radius,
							debugLevel);                   // final int       debugLevel) {
				}
				double [] scene_xy_offset =   {centers[1][0]-centers[0][0],centers[1][1]-centers[0][1]};
				boolean [] removed = null;
				double    scene_agl = gops[1].getAGL(); // second scene agl  
				if (!filt_atonce) {
					boolean[] filt_main_other = {true,false};
					//int num_removed = 
					removed = filterCandidates(
							filt_keep,         // boolean       filt_keep,
							filt_main_other,   // new boolean []            filt_main_other,
							min_corrs,         //double []             min_corrs, // one per
							min_corr_full_rel, // double                min_corr_full_rel,
							filt_max_radius,   // double [][]           filt_max_radius,
							filt_elongation,   // double [][]           filt_elongation,
							filt_dist,         // double []             filt_dist,
							filt_height,       // double                filt_height,
							best_patt_indx,    // int []                best_patt_indx,
							filt_other_frac,   //double []             filt_other_frac, // null - disable, for all full (round) patterns. 
							filter_data,       // double [][][][]       filter_data,
							zoom_level,        // int       zoom_level,
							scene_agl,         // double                scene_agl,
							scene_xy_offset,   // double []             scene_xy_offset, //second scene vertical projection offset from the first scene
							matches_list,      // ArrayList <ItemMatch> matches_list,
							match_sort,        // ArrayList<Integer>    match_sort,
							debugLevel);       // int                   debugLevel )
					if (!filt_keep && (removed.length > match_sort.size())) { // decimate data arrays
						for (int sn = 0; sn < filter_data.length; sn++) {
							filter_data[sn] =       removeFilteredOut (filter_data[sn], removed);
							extracted_objects[sn] = removeFilteredOut (extracted_objects[sn], removed);
							extr_corr[sn] =         removeFilteredOut (extr_corr[sn], removed);
							extr_corr_half[sn] =    removeFilteredOut (extr_corr_half[sn], removed);
						}
						best_patt_indx = removeFilteredOut(best_patt_indx, removed);
						removed = new boolean [match_sort.size()];
					}
					
//					System.out.println(num_removed+ "scenes filtered out, "+match_sort.size()+" candidates remain");
				}
				// show extracted scenes after some were optionally removed by the main scene filters
				for (int sn = 0; sn < indices.length; sn++) {
					showExtractedImages(
							ortho_maps[indices[scene_num]].getName()+"-prefilter", // String prefix
							sn,                 //int                   scene_num,
							sort_pattern_index, // int                   sort_pattern_index,
							extr_size,          // int                   extr_size,
							gops[scene_num],                // GroundObjectPattern   gop,
							matches_list,       // ArrayList <ItemMatch> matches_list,
							match_sort,         // ArrayList<Integer>    match_sort,
							extracted_objects,  // double[][][]          extracted_objects, // may be null - will not be generated
							extract_display,    // double[][][]          extract_display,   // may be null - will not be displayed 
							dmulti,             // double[][]            dmulti,
							width,              // int                   width,
							remove_dc,          // boolean               remove_dc,
							imps_extracted,     //ImagePlus[]           imps_extracted, // may be null - will not be displayed
							debugLevel);        // int                   debugLevel
				}
				// display extracted correlations for the main scene:
				if (debugLevel > 0) {
					showExtractedCorrelations(
							ortho_maps[indices[scene_num]].getName()+"-prefilter", // String prefix
							scene_num, // int scene_num,
							phaseCoeff, // double []             phaseCoeff,
							extr_corr, // double [][][]         extr_corr,
							extr_corr_half, // double [][][]         extr_corr_half,
							extr_size, // int                   extr_size,
							gops[scene_num], // GroundObjectPattern   gop,
							matches_list, // ArrayList <ItemMatch> matches_list,
							match_sort, // ArrayList<Integer>    match_sort,
							imps_extracted_corr, // ImagePlus[]           imps_extracted_corr, // may be null - will not be displayed
							imps_extracted_corr_half); // ImagePlus[]           imps_extracted_corr_half
				}
				// Correlate second scene only for selected fragments
				for (int scene_other = 1; scene_other < indices.length; scene_other++) { // normally just 1
					filter_data[scene_other] = new double[match_sort.size()][2][]; // [scene][max][0-full, 1 - half][0- max, 1 - area, 2 - distance]
					double [][] extr_data =      new double [match_sort.size()][]; //extracted_objects[scene_other].length][];
					extr_corr[scene_other] =      new double [match_sort.size()][]; //extracted_objects[scene_other].length][];
					extr_corr_half[scene_other] = new double [match_sort.size()][]; //extracted_objects[scene_other].length][];
					for (int mn=0; mn < match_sort.size(); mn++) {
						int indx =match_sort.get(mn);
						ItemMatch match = matches_list.get(indx);
						int best_patt = match.getPatternMatch(gops[scene_num]).getBestSub() - 1;
						extr_data[mn] = extract_display[scene_other][mn].clone();
						OrthoMap.removeDC(extr_data[mn]); // now always removing DC
						extr_corr[scene_other][mn] =OrthoMap.correlateWithPattern(
								extr_data[mn],                 // final double [] data,
								corr_size,                         // final int       width,
								corr_size,                     // final int       psize,      // power of 2, such as 64
								corr_patterns[scene_other][0], // final double [] pattern,    // [psize*psize]
								false,                         // final boolean   convolve,   // convolve, not correlate
								phaseCoeff[scene_other],               // final double    phaseCoeff,
								debugLevel);                   // final int       debugLevel) {
						filter_data[scene_other][mn][0] = ObjectLocation.getMaxLocAreaNaN(
								extr_corr[scene_other][mn], // double [] data, // square data
								null, // double [] cent_xy, // if null, use center of the square
								search_radius, // double    radius, // search for maximum within this radius. Should radius be 0 here?
								frac_max, // double    frac_max)
								filt_other_rad, // double    other_radius,
								debugLevel);                   // final int       debugLevel) {
						if (best_patt > 0) { // not full match
							extr_corr_half[scene_other][mn] =OrthoMap.correlateWithPattern(
									extr_data[mn],                 // final double [] data,
									corr_size,                         // final int       width,
									corr_size,                     // final int       psize,      // power of 2, such as 64
									corr_patterns[scene_other][best_patt], // final double [] pattern,    // [psize*psize]
									false,                         // final boolean   convolve,   // convolve, not correlate
									phaseCoeff[scene_other],               // final double    phaseCoeff,
									debugLevel);                   // final int       debugLevel) {
							if (combine_full[1]) {
								double full_max_value = filter_data[scene_other][mn][0][0];
								if (!Double.isNaN(full_max_value)) {
									// multiply by full correlation, delete by full correlation maximum
									for (int i = 0; i < extr_corr_half[scene_other][mn].length; i++){
										extr_corr_half[scene_other][mn][i] *= Math.max(0, extr_corr[scene_other][mn][i]/full_max_value);
									}
								}
							}
							
						} else {
							// Maybe correlate with all and make sure the best is +/- 1 from the main scene
							extr_corr_half[scene_other][mn] = extr_corr[scene_other][mn];
						}
						
						filter_data[scene_other][mn][1] = ObjectLocation.getMaxLocAreaNaN(
								extr_corr_half[scene_other][mn], // double [] data, // square data
								null, // double [] cent_xy, // if null, use center of the square
								search_radius, // double    radius, // search for maximum within this radius. Should radius be 0 here?
								frac_max, // double    frac_max)
								filt_other_rad, // double    other_radius,
								debugLevel);                   // final int       debugLevel) {
 					}
					
					if (debugLevel > 0) {
						showExtractedCorrelations(
								ortho_maps[indices[scene_num]].getName()+"-prefilter", // String prefix
								scene_other,               // int scene_num,
								phaseCoeff,                // double []             phaseCoeff,
								extr_corr,                 // double [][][]         extr_corr,
								extr_corr_half,            // double [][][]         extr_corr_half,
								extr_size,                 // int                   extr_size,
								// or should it be other gop?
								gops[scene_num],           // GroundObjectPattern   gop,
								matches_list,              // ArrayList <ItemMatch> matches_list,
								match_sort,                // ArrayList<Integer>    match_sort,
								imps_extracted_corr,       // ImagePlus[]           imps_extracted_corr, // may be null - will not be displayed
								imps_extracted_corr_half); // ImagePlus[]           imps_extracted_corr_half
						}
				}
				// final filter
				removed = filterCandidates( // will filter again if already
						filt_keep,         // boolean       filt_keep,
						new boolean[] {true,true},   // new boolean []            filt_main_other,
						min_corrs,         //double []             min_corrs, // one per
						min_corr_full_rel, // double                min_corr_full_rel,
						filt_max_radius,   // double [][]           filt_max_radius,
						filt_elongation,   // double [][]           filt_elongation,
						filt_dist,         // double []             filt_dist,
						filt_height,       // double                filt_height,
						best_patt_indx,    // int []                best_patt_indx,
						filt_other_frac,   //double []             filt_other_frac, // null - disable, for all full (round) patterns. 
						filter_data,       // double [][][][]       filter_data,
						zoom_level,        // int       zoom_level,
						scene_agl,         // double                scene_agl,
						scene_xy_offset,   // double []             scene_xy_offset, //second scene vertical projection offset from the first scene
						matches_list,      // ArrayList <ItemMatch> matches_list,
						match_sort,        // ArrayList<Integer>    match_sort,
						debugLevel);       // int                   debugLevel )

				if (!filt_keep && (removed.length > match_sort.size())) { // decimate data arrays
					for (int sn = 0; sn < filter_data.length; sn++) {
						filter_data[sn] =       removeFilteredOut (filter_data[sn], removed);
						extracted_objects[sn] = removeFilteredOut (extracted_objects[sn], removed);
						extr_corr[sn] =         removeFilteredOut (extr_corr[sn], removed);
						extr_corr_half[sn] =    removeFilteredOut (extr_corr_half[sn], removed);
					}
					best_patt_indx = removeFilteredOut(best_patt_indx, removed);
					removed = new boolean [match_sort.size()];
				}
				
				ImagePlus [] imps_extracted_final = new ImagePlus[indices.length];
				ImagePlus [] imps_extracted_corr_final = new ImagePlus[indices.length];
				ImagePlus [] imps_extracted_corr_half_final = new ImagePlus[indices.length];

				if (!filt_keep) { // show final filtered images again - just extracts (2) and all correlations (4)
					for (int sn = 0; sn < indices.length; sn++) {
						showExtractedImages(
								ortho_maps[indices[scene_num]].getName()+"-final", // String prefix
								sn,                 //int                   scene_num,
								sort_pattern_index, // int                   sort_pattern_index,
								extr_size,          // int                   extr_size,
								gops[scene_num],                // GroundObjectPattern   gop,
								matches_list,       // ArrayList <ItemMatch> matches_list,
								match_sort,         // ArrayList<Integer>    match_sort,
								extracted_objects,  // double[][][]          extracted_objects, // may be null - will not be generated
								extract_display,    // double[][][]          extract_display,   // may be null - will not be displayed 
								dmulti,             // double[][]            dmulti,
								width,              // int                   width,
								remove_dc,          // boolean               remove_dc,
								imps_extracted_final,     //ImagePlus[]           imps_extracted, // may be null - will not be displayed
								debugLevel);        // int                   debugLevel
						showExtractedCorrelations(
								ortho_maps[indices[scene_num]].getName()+"-final", // String prefix
								sn,                // int scene_num,
								phaseCoeff,        // double []             phaseCoeff,
								extr_corr,         // double [][][]         extr_corr,
								extr_corr_half,    // double [][][]         extr_corr_half,
								extr_size,         // int                   extr_size,
								gops[scene_num],   // GroundObjectPattern   gop,
								matches_list,      // ArrayList <ItemMatch> matches_list,
								match_sort,                // ArrayList<Integer>    match_sort,
								imps_extracted_corr_final, // ImagePlus[]           imps_extracted_corr, // may be null - will not be displayed
								imps_extracted_corr_half_final); // ImagePlus[]           imps_extracted_corr_half
					}
				}
				
//				double    scene_agl = gops[1].getAGL(); // second scene agl  
				System.out.println(String.format("Main scene:  (%03d) %17s",indices[0],ortho_maps[indices[0]].getName()));
				System.out.println(String.format("Other scene: (%03d) %17s",indices[1],ortho_maps[indices[1]].getName()));
				System.out.println("Searching for object type="+object_type+", found "+match_sort.size()+" of them."+
				" PhaseCoeff=["+phaseCoeff[0]+","+phaseCoeff[1]+"], min_corrs=["+min_corrs[0]+","+min_corrs[1]+
				"], min_corr_full_rel="+min_corr_full_rel+", frac_max="+frac_max);
				System.out.println("filt_max_radius main =["+filt_max_radius[0][0]+","+filt_max_radius[0][1]+"], "+
						"filt_max_radius other=["+filt_max_radius[1][0]+","+filt_max_radius[1][1]+"], "+
						"filt_elongation main=["+filt_elongation[0][0]+","+filt_elongation[0][1]+"], "+
						"filt_elongation other=["+filt_elongation[1][0]+","+filt_elongation[1][1]+"]");
				System.out.println("filt_dist=["+filt_dist[0]+", "+filt_dist[1]+"] "+
						"(First value is the total offset in pixels, second accounts for parallax.) "+
						" filt_height="+filt_height+ " meters.");
				System.out.println("filt_other_rad="+filt_other_rad+"pix,  filt_other_frac=["+filt_other_frac[0]+", "+filt_other_frac[1]+"].");
				System.out.println("Second image is offset by dx="+scene_xy_offset[0]+", dy="+scene_xy_offset[1]+", dist="+
						Math.sqrt(scene_xy_offset[0]*scene_xy_offset[0]+scene_xy_offset[1]*scene_xy_offset[1])+" pix");
				System.out.println("Updated object list with main scene peak areas and secondary scene maximums, areas and maximum distances from the main");
				System.out.print(String.format("%4s: %9s %8s %3s"," # ", "  x/y  ", " best ","sub"));
				for (int j = 0; j < (corrs_out[scene_num].length + 1); j++) if (j != sort_pattern_index) {
					System.out.print(String.format(" %8s ","match_"+j));
				}
				System.out.print(String.format("%6s %6s %6s %6s %6s | %8s %6s %6s %6s %7s %7s %7s %7s %7s | %8s %6s %6s %6s %6s %7s %7s %7s",
						"mf-rad","mf-eln","mh-rad","mh-eln", "mh-ofr", // main: full radius, full elongation, half radius, half elongation
						"of-corr","of-rad","of-eln","of-dst","of-dx","of-dy","of-par","of-prp","elevat",
						"oh-corr","oh-rad","oh-eln", "oh-ofr", "oh-dst","oh-dx","oh-dy","removed"));
				System.out.println();
				
				for (int i = 0; i < match_sort.size(); i++) {
					int indx =match_sort.get(i);
					ItemMatch match = matches_list.get(indx);
					double [] match_values = match.getMatchValues(gops[scene_num]);
					int [] icenter_xy = match.getIntXY();
					System.out.print(String.format("%4d: %4d/%4d %8.5f %3d",
							i,
							icenter_xy[0],icenter_xy[1],
							match.getMatchValue(gops[scene_num],sort_pattern_index),
							match.getPatternMatch(gops[scene_num]).getBestSub()));
					for (int j = 0; j < match_values.length; j++) if (j != sort_pattern_index) {
						System.out.print(String.format(" %8.5f",match_values[j]));
						if (j != (match_values.length-1)) {
							System.out.print(",");
						} else {
							System.out.print(" ");
						}
					}
//                               0      1        2     3        4             5 					
//		return new double [] {best_d, eff_rad, elong, dist, cent_offs[0], cent_offs[1]} ;
					double [] second_xy_offsets = {filter_data[1][i][0][4],filter_data[1][i][0][5]};
					// try elevation for second scene and full pattern only
					double [] scnd_xy_proj = projectOffsetOnVector(
							second_xy_offsets, // double [] object_offset,
							scene_xy_offset); // double [] scene_offset)
					double  obj_elev =        offsetToElevation(
							second_xy_offsets, //double [] object_offset,
							scene_xy_offset,   // double [] scene_offset,
							zoom_level,        // int       zoom_level,
							scene_agl);        // double    agl)
//			System.out.println(String.format("best_d=%7.5f, rad=%6.3f, elong=%6.3f, dist=%6.3f, dx=%6.3f, dy=%6.3f",
					String sremoved = removed[i]?" removed":"";
					System.out.print(String.format(
							"%6.3f %6.3f %6.3f %6.3f %6.3f | %8.5f %6.3f %6.3f %6.3f %7.3f %7.3f %7.3f %7.3f %7.3f | %8.5f %6.3f %6.3f %6.3f %6.3f %7.3f %7.3f %7s",
							// main full and best half areas only, radius and elongation
							filter_data[0][i][0][1], filter_data[0][i][0][2], // main full:      radius and elongation
							filter_data[0][i][1][1], filter_data[0][i][1][2], // main best half: radius and elongation
							filter_data[0][i][1][6], // other max fraction of the main one
							// for second full - everything and parallel/perpendicular+elevation
							filter_data[1][i][0][0], filter_data[1][i][0][1], filter_data[1][i][0][2], // full: corr, radius, elongation
							filter_data[1][i][0][3], filter_data[1][i][0][4], filter_data[1][i][0][5], // full: dist, dx, dy
							scnd_xy_proj[0], scnd_xy_proj[1],obj_elev,                           // full: parallel, perpnd, elevation(m)
							
							filter_data[1][i][1][0], filter_data[1][i][1][1], filter_data[1][i][1][2], // half: corr, radius, elongation
							filter_data[1][i][1][6], 
							filter_data[1][i][1][3], filter_data[1][i][1][4], filter_data[1][i][1][5],  // half: dist, dx, dy
							sremoved));
					System.out.println();
				}
				System.out.println();
				
				//ortho_maps[indices[0]]
				//double [][] dmulti
				//ortho_maps[indices[scene_num]].getName()
				/*
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imp_corrs[scene_num].setRoi(roi);

				 */
				{ // Create image with marked mines				
					PointRoi final_roi = new PointRoi();
					final_roi.setOptions("label");
					for (int i = 0; i < match_sort.size(); i++) {
						int indx =match_sort.get(i);
						ItemMatch match = matches_list.get(indx);
						double [] match_values = match.getMatchValues(gops[scene_num]);
						double [] center_xy = match.getXY();
						final_roi.addPoint(center_xy[0], center_xy[1]); // ,1);
					}
					String []  final_titles = new String[indices.length];
					double [][] final_map = new double [indices.length][];
					for (int i = 0; i < final_map.length; i++) {
						final_titles[i] = ortho_maps[indices[i]].getName();
						final_map[i] =  dmulti[i];
					}
					String final_title = final_titles[0]+"-"+final_titles[0]+"-MARKED_"+object_type+"_"+match_sort.size()+"_OBJECTS";

					ImagePlus img_final = ShowDoubleFloatArrays.makeArrays(
							final_map,
							width,
							height,
							final_title,				
							final_titles);
					img_final.setRoi(final_roi);
					img_final.show();
				}
			} // if (scene_num == 0) {
			if (debugLevel > -1000) {
				return null;
			}
			object_lists.add(plist); // old way
			System.out.println("patternMatchDual(): Found "+object_lists.get(scene_num).size()+" candidates for scene #"+scene_num);
			imps_best[scene_num] = ShowDoubleFloatArrays.makeArrays(
					bestcorr[scene_num],
					width,
					height,
					ortho_maps[indices[scene_num]].getName()+"-BEST_CORR");				
			double [] dbg_bestpatt = new double [bestpatt[scene_num].length];
			Arrays.fill(dbg_bestpatt, Double.NaN);
			for (int i = 0; i <bestpatt[scene_num].length; i++) {
				if (bestpatt[scene_num][i] >=0) {
					dbg_bestpatt[i] = bestpatt[scene_num][i];
				}
			}
			imps_bestpatt[scene_num] = ShowDoubleFloatArrays.makeArrays(
					dbg_bestpatt,
					width,
					height,
					ortho_maps[indices[scene_num]].getName()+"-BEST_PATT");				
			if (src_marks != null) {
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imps_best[scene_num].setRoi(roi);
				imps_bestpatt[scene_num].setRoi(roi);
			}
			imps_best[scene_num].show();
			imps_bestpatt[scene_num].show();
			filtered_corrs[scene_num]= OrthoMap.splitPattenCorrelations(
					corr_patterns[scene_num].length, // final int         num_patterns,
					bestcorr[scene_num],        // final double []   bestcorr,
					bestpatt[scene_num]);       // final int []      bestpatt)		

			for (int n = 0; n < corr_patterns[scene_num].length; n++) {
				convolve_out[scene_num][n]= OrthoMap.correlateWithPattern(
						filtered_corrs[scene_num][n],// corrs_out[n], // final double [] data,
						width,                // final int       width,
						corr_size,            // final int       psize,      // power of 2, such as 64
						corr_patterns[scene_num][n], // final double [] pattern,    // [psize*psize]
						true,                 // final boolean   convolve,   // convolve, not correlate
						phaseCoeff[scene_num],// final double    phaseCoeff,
						debugLevel);          // final int       debugLevel) {
			}
			convolved_merged[scene_num] = OrthoMap.mergePattenConvolutions(
					convolve_out[scene_num]); // final double [][] convs)
			String [] rslt_titles =new String [corr_patterns[scene_num].length+2]; 
			double [][] rslt = new double [corr_patterns[scene_num].length + 2][]; // ]{data, corr_out};
			double [][] conv_rslt = new double [corr_patterns[scene_num].length + 2][]; // ]{data, corr_out};
			rslt[0] = dmulti[scene_num];
			conv_rslt[0] = dmulti[scene_num];
			rslt_titles[0] = "original";
			rslt[1] = bestcorr[scene_num];
			conv_rslt[1] = convolved_merged[scene_num];
			rslt_titles[1] = "combined";
			for (int i = 0; i < corr_patterns[scene_num].length; i++) {
				rslt[i+2] = filtered_corrs[scene_num][i];
				conv_rslt[i+2] = convolve_out[scene_num][i];
				rslt_titles[i+2] = "corr_"+pattern_labels[scene_num][i];
			}
			imps_patt_corrs[scene_num] = ShowDoubleFloatArrays.makeArrays(
					rslt,
					width,
					height,
					ortho_maps[indices[scene_num]].getName()+"-PATTERN_CORR",
					rslt_titles);
			if (src_marks != null) {
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imps_patt_corrs[scene_num].setRoi(roi);
			}
			imps_patt_corrs[scene_num].show();
			
			imps_patt_conv[scene_num] = ShowDoubleFloatArrays.makeArrays(
					conv_rslt,
					width,
					height,
					ortho_maps[indices[scene_num]].getName()+"-PATTERN_CORR_CONVOLVE_min_corr"+min_corrs[scene_num],
					rslt_titles);
			if (src_marks != null) {
				PointRoi roi = new PointRoi();
				roi.setOptions("label");
				for (int i = 0; i < src_marks.length; i++) {
					roi.addPoint(src_marks[i][0],src_marks[i][1]); // ,1);
				}
				imps_patt_conv[scene_num].setRoi(roi);
			}
			imps_patt_conv[scene_num].show();
			System.out.println("patternMatchDual(): correlation done for scene "+scene_num);
			
		}
		System.out.println("patternMatchDual(): all correlations DONE");
	
		return null;
	}
	
	public static double [][][] removeFilteredOut(
			double [][][] data,
			boolean [] remove){
		if (data == null) {
			return null;
		}
		int num_left = data.length;
		for (int i = 0; i < remove.length; i++) {
			if (remove[i]) {
				num_left--;
			}
		}
		double [][][] new_data = new double[num_left][][];
		int indx = 0;
		for (int i = 0; i < remove.length; i++) if (!remove[i]) {
			new_data[indx++] = data[i];
		}
		return new_data;
	}

	public static double [][] removeFilteredOut(
			double [][] data,
			boolean [] remove){
		if (data == null) {
			return null;
		}
		int num_left = data.length;
		for (int i = 0; i < remove.length; i++) {
			if (remove[i]) {
				num_left--;
			}
		}
		double [][] new_data = new double[num_left][];
		int indx = 0;
		for (int i = 0; i < remove.length; i++) if (!remove[i]) {
			new_data[indx++] = data[i];
		}
		return new_data;
	}

	public static int [] removeFilteredOut(
			int [] data,
			boolean [] remove){
		if (data == null) {
			return null;
		}
		int num_left = data.length;
		for (int i = 0; i < remove.length; i++) {
			if (remove[i]) {
				num_left--;
			}
		}
		int [] new_data = new int[num_left];
		int indx = 0;
		for (int i = 0; i < remove.length; i++) if (!remove[i]) {
			new_data[indx++] = data[i];
		}
		return new_data;
	}
	
	
	
	/**
	 * Returns boolean array of original length
	 * @param filt_keep
	 * @param filt_main_other
	 * @param min_corrs
	 * @param min_corr_full_rel
	 * @param filt_max_radius
	 * @param filt_elongation
	 * @param filt_dist
	 * @param filt_height
	 * @param filter_data
	 * @param scene_xy_offset
	 * @param matches_list
	 * @param match_sort
	 * @param debugLevel
	 * @return
	 */
	public static boolean[] filterCandidates(
			boolean               filt_keep,
			boolean []            filt_main_other,
			double []             min_corrs, // one per
			double                min_corr_full_rel,
			double [][]           filt_max_radius,
			double [][]           filt_elongation,
			double []             filt_dist,
			double                filt_height,
			int []                best_patt_indx,
			double []             filt_other_frac, // null - disable, for all full (round) patterns. 
			double [][][][]       filter_data,
			int                   zoom_level,
			double                scene_agl,
			double []             scene_xy_offset, //second scene vertical projection offset from the first scene
			ArrayList <ItemMatch> matches_list,
			ArrayList<Integer>    match_sort,
			
			int                   debugLevel ) {
		boolean [] remove = new boolean [match_sort.size()];
		int num_removed = 0;
		double [] min_corrs_other = {min_corrs[1]*min_corr_full_rel, min_corrs[1]};
		double offset_from_height = elevationToParallelOffset( // will be negative
				filt_height,     // double    height,
				scene_xy_offset, // double [] scene_offset,
				zoom_level,      // int       zoom_level,
				scene_agl) ;//  double    agl)
		for (int mn=0; mn < match_sort.size(); mn++) {
			for (int hf = 0; hf < 2; hf++) {
				int indx =match_sort.get(mn);
				ItemMatch match = matches_list.get(indx);
				int [] ixy = match.getIntXY();
				String name = ixy[0]+"/"+ixy[1];
				boolean is_partial = best_patt_indx[mn] != 0; // only apply to combined full/halv correlations, just full may have nearby maximums near the same size
				for (int scene_num = 0; scene_num < 2; scene_num++) if (filt_main_other[scene_num]){
					if (Double.isNaN(filter_data[scene_num][mn][hf][0])) {
						System.out.println(name+": filtered out by "+NAME_MO[scene_num]+"/"+NAME_FH[hf]+
								" as correlation maximum is not found");
						if (!remove[mn]) num_removed++;
						remove[mn] = true;
						continue;
					}
					double eff_rad =       filter_data[scene_num][mn][hf][1];
					double elong =         filter_data[scene_num][mn][hf][2];
					double corr_val =      filter_data[scene_num][mn][hf][0];
					double dist =          filter_data[scene_num][mn][hf][3];
					double near_max_frac = filter_data[scene_num][mn][hf][6];
					double [] second_xy_offsets = {filter_data[scene_num][mn][hf][4],filter_data[scene_num][mn][hf][5]};					
					// calculate projection
					double [] scnd_xy_proj =  projectOffsetOnVector(
							second_xy_offsets, // double [] object_offset,
							scene_xy_offset); // double [] scene_offset)
					if (scene_num > 0) { // other scene
						//	{best_d, eff_rad, elong, dist, cent_offs[0], cent_offs[1]} ;
						if (corr_val < min_corrs_other[hf]) {
							System.out.println(name+": filtered out by "+NAME_MO[scene_num]+" correlation maximum value for "+
									NAME_FH[hf]+ " = "+corr_val+" < "+ min_corrs_other[hf]);
							if (!remove[mn]) num_removed++;
							remove[mn] = true;
						}
						if (dist > filt_dist[0]) {
							System.out.println(name+": filtered out by "+NAME_MO[scene_num]+" distance from the main scene peak "+
									NAME_FH[hf]+ " = "+dist+" > "+filt_dist[0]);
							if (!remove[mn]) num_removed++;
							remove[mn] = true;
						}
						// distance for main only or both?
						if (Math.abs(scnd_xy_proj[1]) > filt_dist[1]) {
							System.out.println(name+": filtered out by "+NAME_MO[scene_num]+" distance from the main scene peak perpendicular to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[1]+", abs() > "+filt_dist[1]);
							if (!remove[mn]) num_removed++;
							remove[mn] = true;
						}
						// offset_from_height
						if (scnd_xy_proj[0] > filt_dist[1]) {
							System.out.println(name+": filtered out by "+NAME_MO[scene_num]+
									" distance from the main scene peak parallel to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[0]+" > "+filt_dist[1]+
									" it could happen if the object is deep below average ground surface.");
							if (!remove[mn]) num_removed++;
							remove[mn] = true;
						}
						if (scnd_xy_proj[0] < (-filt_dist[1]+offset_from_height)) {
							System.out.println(name+": filtered out by "+NAME_MO[scene_num]+
									" distance from the main scene peak parallel to the scene offset vector "+
									NAME_FH[hf]+ " = "+scnd_xy_proj[0]+" < "+(-filt_dist[1]+offset_from_height)+
									".\n This limit accounts for objects above ground up to "+filt_height+" meters ("+
									offset_from_height+" pix.");
							if (!remove[mn]) num_removed++;
							remove[mn] = true;
						}
					}
					if (eff_rad > filt_max_radius[scene_num][hf]) {
						System.out.println(name+": filtered out by "+NAME_MO[scene_num]+" scene radius for "+
								NAME_FH[hf]+ " pattern: "+eff_rad+" > "+ filt_max_radius[scene_num][hf]);
						if (!remove[mn]) num_removed++;
						remove[mn] = true;
					}
					if (elong > filt_elongation[scene_num][hf]) {
						System.out.println(name+": filtered out by "+NAME_MO[scene_num]+" scene peak cros-section elongation for "+
								NAME_FH[hf]+ " pattern: "+elong+" > "+ filt_elongation[scene_num][hf]);
						if (!remove[mn]) num_removed++;
						remove[mn] = true;
					}
					if ((filt_other_frac != null) && (hf > 0) && (near_max_frac > filt_other_frac[scene_num])) {
						System.out.println(name+": filtered out by "+NAME_MO[scene_num]+
								" because it matches partial pattern and combined full/partial correlation has a near peak "+
								near_max_frac+" > "+ filt_other_frac[scene_num]+" of the used peak.");
						if (!remove[mn]) num_removed++;
						remove[mn] = true;
					}
				}				
			}
		}
		if (! filt_keep) {
			if (num_removed > 0) {
				for (int mn=match_sort.size()-1; mn >=0; mn--) {
					if (remove[mn]) {
						match_sort.remove(mn);
					}
				}
				System.out.println("Filtered out "+num_removed+
						" scenes by the filter.");
//				remove = new boolean [match_sort.size()];
			} else {
				System.out.println("No scenes were filtered out by the filter");
			}
			System.out.println(match_sort.size()+" candidate(s) remain");
		}
		return remove;
	}
	
	
	
	
	//ortho_maps[indices[scene_num]].getName()
	public static void showExtractedImages(
			String                prefix,
			int                   scene_num,
			int                   sort_pattern_index,
			int                   extr_size,
			GroundObjectPattern   gop,
			ArrayList <ItemMatch> matches_list,
			ArrayList<Integer>    match_sort,
			double[][][]          extracted_objects, // may be null - will not be generated
			double[][][]          extract_display,   // may be null - will not be displayed 
			double[][]            dmulti,
			int                   width,
			boolean               remove_dc,
			ImagePlus[]           imps_extracted, // may be null - will not be displayed
			int                   debugLevel
			) {
		if (match_sort.isEmpty()) {
			System.out.println("List of detected objects is empty, nothing to display");
			return;
		}
		PointRoi roi = new PointRoi();
		roi.setOptions("nolabel"); // label");
		for (int mn=0; mn < match_sort.size(); mn++) {
			roi.addPoint(extr_size/2,extr_size/2,mn+1); // ,1);
		}
		if (extracted_objects != null) {
			extracted_objects[scene_num] = new double [match_sort.size()][];
		}
		if (extract_display != null ) {
			extract_display[scene_num] = new double [match_sort.size()][];
		}
		for (int mn=0; mn < match_sort.size(); mn++) {
			int indx =match_sort.get(mn);
			ItemMatch match = matches_list.get(indx);
			double [] center_xy = match.getXY();
			//some may be missing on a second image
			if (extracted_objects != null) {
				extracted_objects[scene_num][mn] = ObjectLocation.extractObjectImage(
						center_xy,  // double [] center_xy,
						extr_size,  // int       size,
						dmulti[scene_num], // double [] src_img,
						width);     // int width)
			}
			if (extract_display != null) {
				if (extracted_objects != null) {
					extract_display[scene_num][mn] =extracted_objects[scene_num][mn].clone();
				} else {
					extract_display[scene_num][mn] = ObjectLocation.extractObjectImage(
							center_xy,  // double [] center_xy,
							extr_size,  // int       size,
							dmulti[scene_num], // double [] src_img,
							width);     // int width)
				}
				if (remove_dc) {
					OrthoMap.removeDC(extract_display[scene_num][mn]); 
				}
			}
		}
		if ((debugLevel > 0) && (imps_extracted != null) && (extract_display != null)) {
			String [] extr_titles = new String [match_sort.size()];
			for (int mn=0; mn < match_sort.size(); mn++) {
				int indx =match_sort.get(mn);
				ItemMatch match = matches_list.get(indx);
				int [] ixy = match.getIntXY();
				extr_titles[mn] = ixy[0]+"/"+ ixy[1]+":"+String.format("%7.5f",
				match.getMatchValue(gop,sort_pattern_index));
			}
			imps_extracted[scene_num] = ShowDoubleFloatArrays.makeArrays(
					extract_display[scene_num],
					extr_size,
					extr_size,
					prefix+"-objects_"+scene_num+(remove_dc?"_no-DC":"_with-DC"),				
					extr_titles); // test_titles,
			imps_extracted[scene_num].setRoi(roi); // null
			imps_extracted[scene_num].show();
		}
	}
	
	public static void showExtractedCorrelations(
			String prefix,
			int scene_num,
			double []             phaseCoeff,
			double [][][]         extr_corr,      // may be null if imps_extracted_corr == null too
			double [][][]         extr_corr_half, // may be null if imps_extracted_corr_half == null too
			int                   extr_size,
			GroundObjectPattern   gop,
			ArrayList <ItemMatch> matches_list,
			ArrayList<Integer>    match_sort,
			ImagePlus[]           imps_extracted_corr, // may be null - will not be displayed
			ImagePlus[]           imps_extracted_corr_half  // may be null - will not be displayed
			) {
		if (match_sort.isEmpty()) {
			System.out.println("List of detected objects is empty, nothing to display");
			return;
		}
		PointRoi roi = new PointRoi();
		roi.setOptions("nolabel"); // label");
		for (int mn=0; mn < match_sort.size(); mn++) {
			roi.addPoint(extr_size/2,extr_size/2,mn+1); // ,1);
		}

		String [] extr_titles_full = new String [match_sort.size()];
		String [] extr_titles_half = new String [match_sort.size()];
		for (int mn=0; mn < match_sort.size(); mn++) {
			int indx =match_sort.get(mn);
			ItemMatch match = matches_list.get(indx);
			int [] ixy = match.getIntXY();
			int best_patt = match.getPatternMatch(gop).getBestSub() - 1;
			extr_titles_full[mn] = ixy[0]+"/"+ ixy[1];
			extr_titles_half[mn] = extr_titles_full[mn]+":"+best_patt;
		}
		if ((extr_corr != null) && (extr_corr[scene_num] != null) &&(imps_extracted_corr != null)) {
			imps_extracted_corr[scene_num] = ShowDoubleFloatArrays.makeArrays(
					extr_corr[scene_num],
					extr_size,
					extr_size,
					prefix+"-objects_corr_full-"+scene_num+"_pc"+phaseCoeff[scene_num],				
					extr_titles_full); // test_titles,
			imps_extracted_corr     [scene_num].setRoi(roi);
			imps_extracted_corr     [scene_num].show();
		}
		if ((extr_corr_half != null) && (extr_corr_half[scene_num] != null) && (imps_extracted_corr_half != null)) {
			imps_extracted_corr_half[scene_num] = ShowDoubleFloatArrays.makeArrays(
					extr_corr_half[scene_num],
					extr_size,
					extr_size,
					prefix+"-objects_corr_halves-"+scene_num+"_pc"+phaseCoeff[scene_num],				
					extr_titles_half); // test_titles,
			imps_extracted_corr_half[scene_num].setRoi(roi);
			imps_extracted_corr_half[scene_num].show();
		}
	}
	
	
}
