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

package com.elphel.imagej.orthomosaic;

import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.DoubleFHT;
import com.elphel.imagej.common.DoubleGaussianBlur;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.PolynomialApproximation;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.ims.Imx5;
import com.elphel.imagej.readers.ElphelTiffReader;
import com.elphel.imagej.readers.ImagejJp4Tiff;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TileNeibs;

import Jama.Matrix;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.gui.PointRoi;
import ij.gui.Roi;
import ij.io.FileSaver;
import ij.plugin.filter.AVI_Writer;
import ij.plugin.filter.GaussianBlur;
import ij.process.ColorProcessor;
import ij.process.FloatPolygon;
import ij.process.FloatProcessor;
import ij.process.ImageConverter;
import ij.process.ImageProcessor;
import loci.formats.FormatException;

public class OrthoMap implements Comparable <OrthoMap>, Serializable{
	private static final long serialVersionUID = 1L;
	public static final String DSI_SUFFIX =    "-INTER-INTRA-LMA.tiff";
	public static final String MERGED_SUFFIX = "-MERGED.tiff";
	public static boolean FIX_VERT_Y = false; // true; // temporarily fix vertical Y coordinate bug (use -GCORR in the filename?)
	public static int gpu_width =  4096;
	public static int gpu_height =  4096;
//	public static final String [] KEY_DIRS= {"rootDirectory", // from EyesisCorrectionParameters
//			"sourceDirectory","linkedModels","videoDirectory","x3dDirectory","resultsDirectory",
//			"kernelsDirectory", "patternsDirectory"};
	public static final String [] kernel_paths = {
			"--- no convolution (same altitude) ---",
			"/media/elphel/NVME/lwir16-proc/ortho_videos/kernel_25_50.tiff", 
			"/media/elphel/NVME/lwir16-proc/ortho_videos/kernel_50_75.tiff",
			"/media/elphel/NVME/lwir16-proc/ortho_videos/kernel_50_100.tiff"
	};

	public static String pattern_dir= "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/debug/mines/pattern_25m_zoom1/synthetic/";
	public static String [] pattern_files=
		{"patterns_50m_zoom1_200x200.tiff",
		"patterns_50m_evening_zoom1_200x200.tiff",
		"patterns_50m_evening_01_zoom1_200x200_00.tiff",
		"patterns_50m_evening_02_zoom1_200x200_00.tiff",
		"patterns_50m_evening_03_zoom1_200x200_00.tiff",
		"patterns_50m_evening_04_zoom1_200x200_00.tiff",
		"patterns_50m_evening_05_zoom1_200x200_00.tiff",
		"mine1_zoom0.tiff",
		"mine2_zoom0.tiff",
		"mine3_zoom0.tiff",
		"mine4_zoom0.tiff",
		"mine6_zoom0.tiff"};
	
	
	public static final String ALT_SUFFIX = "-ALT";
	public           String                       path;        // full path to the model directory (including /vXX?)
//	public  double []                  lla;      // lat/long/alt
//	public LocalDateTime                        dt;
	// affine convert (input) rectified coordinates (meters) relative to vert_meters to source image
	// coordinates relative to vert_meters
	public           double [][]                  affine = new double[][] {{1,0,0},{0,1,0}}; // relative to vert_meters[], positive Y is down (as in images)
	public           double [][]                  ers_affine = new double[][] {{1,0},{0,1}}; // orientation only for remaining ERS, positive Y is down (as in images)
	public           FloatImageData               orig_image;
	public           FloatImageData               alt_image;
//	public int                                  orig_zoom_level;
//	public boolean                              orig_zoom_valid;
//	public double                               need_extra_zoom;
	public           double                       averageRawPixel = Double.NaN; // measure of scene temperature
	HashMap          <String, PairwiseOrthoMatch> pairwise_matches;
	public           SensorTemperatureData[]      temp_data;
	public           double                       agl = Double.NaN;
	public           int                          num_scenes = -1;;   // number of scenes that made up this image
	public           double                       sfm_gain = Double.NaN;     // maximal SfM gain of this map
	public           double []                    equalize = {1,0}; // rectified value = equalize[0]*source_value+equalize[1]
	public           double []                    qorient = null; // quternion representing orientation+scale of the scene
// really transient	
	transient HashMap <Integer, FloatImageData> images;
	public transient String                     name;  // timestamp
	public transient double                       ts;

	

	private void writeObject(ObjectOutputStream oos) throws IOException {
		oos.defaultWriteObject();
//		oos.writeObject(path);
//		oos.writeObject(temp_data); // temporary, while transient
//		oos.writeObject(agl);
//		oos.writeObject(num_scenes);
//		oos.writeObject(sfm_gain);
//		oos.writeObject(equalize);
//		oos.writeObject(qorient);
	}
	
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
//		path = (String) ois.readObject();
		name = getNameFromPath(path);
		ts= Double.parseDouble(name.replace("_", "."));
//		temp_data = (SensorTemperatureData[]) ois.readObject();
//		agl = (double)  ois.readObject();
//		num_scenes = (int) ois.readObject();
//		sfm_gain = (double) ois.readObject();
//		equalize = (double []) ois.readObject();
//		if (OrthoMapsCollection.CURRENT_VERSION >= OrthoMapsCollection.VERSION_POST_ORIENT) {
//			qorient = (double[]) ois.readObject();
//		}
		images = new HashMap <Integer, FloatImageData>(); // field images was not saved
//		averageImagePixel = Double.NaN; // average image pixel value (to combine with raw)
		
	}

	public double [] getQOrinet() {
		return qorient;
	}
	public void setQOrient(double [] q) {
		qorient = q;
	}
	public double [][] getERSAffine(){
		return ers_affine;
	}
	public void setERSAffine(double [][] ers_affine) { // cloned
//		this.ers_affine = ers_affine;
		this.ers_affine = new double [][] {ers_affine[0].clone(),ers_affine[1].clone()}; 
	}
	
	double getEqualized(double d) {
		return d * equalize[0] + equalize[1];
	}
	
	double [] getEqualize() {
		return equalize;
	}
	
	void setEqualize(double [] equalize) {
		this.equalize = equalize;
	}
	
    @Override
    public int compareTo(OrthoMap otherPlayer) {
        return Double.compare(ts, otherPlayer.ts);
    }
	
	public static void setGPUWidthHeight(
			int width,
			int height) {
		gpu_width = width;
		gpu_height = height;		
	}

	public int getWidth() {
		return getImage().getWidth(); // orig_width;
	}
	public int getHeight() {
		return getImage().getHeight(); //orig_height;
	}
	public int getAltWidth() {
		return getAlt().getWidth(); // orig_width;
	}
	public int getAltHeight() {
		return getAlt().getHeight(); //orig_height;
	}
	
	public void setMatch(
			String name,
			PairwiseOrthoMatch match) {
		pairwise_matches.put(name, match);
	}

	public void unsetMatch(
			String name) {
		pairwise_matches.remove(name);
	}

	public PairwiseOrthoMatch getMatch(
			String name,
			boolean with_undefined) {
		PairwiseOrthoMatch match = pairwise_matches.get(name);
		if (match == null) return null;
		if (with_undefined || match.isDefined()) return match;
		return null;
	}
	public static boolean isAffineNonTrivial(double [][] affine) {
		if (affine == null) {
			return false;
		}
		return ( affine[0][0] != 1.0) || (affine[0][1] != 0.0)  || (affine[0][2] != 0.0) ||
				(affine[1][0] != 0.0) || (affine[1][1] != 1.0)  || (affine[1][2] != 0.0);
	}
	public boolean isAffineNonTrivial() {
		return isAffineNonTrivial(this.affine);
	}
	
	
	public PairwiseOrthoMatch getMatch(String name) {
		return getMatch(name, false); // old - do not count undefined
//		return pairwise_matches.get(name);
	}
	
	public void unsetMatches(boolean undefined_only) {
		if (undefined_only) {
			String [] matches = pairwise_matches.keySet().toArray(new String[0]);
			for (String match:matches) {
				PairwiseOrthoMatch pm = getMatch(match); 
				if ((pm == null) || !pm.isDefined()) {
					unsetMatch(match);
				}
			}
		} else {
			pairwise_matches = new HashMap<String, PairwiseOrthoMatch>();
		}
	}
	
	public int getOriginalZoomLevel() {
		return getImage().getZoomLevel(); // image, not altitude!
//		return orig_zoom_level;
	}
// Generate ALT image path from the GEO
	public static String getAltPath(String path) {
		int p1 = path.lastIndexOf(".");
		return path.substring(0,p1)+ALT_SUFFIX+".tiff";
	}
	public static String getNameFromPath(String path) {
		int p1 = path.lastIndexOf(Prefs.getFileSeparator());
		if (p1 < 0) return null;
		int p2 = path.indexOf("-", p1+1);
		if (p2 < 0) return null;
		return path.substring(p1+1, p2);
	}

	public static String getModelPathFromPath(String path) {
		int p1 = path.lastIndexOf(Prefs.getFileSeparator());
		if (p1 < 0) return null;
		return path.substring(0,p1);
	}
	public String getScenesPath() {
		return getModelPathFromPath(this.path)+"/jp4";
	}

	/**
	 * Get number of scenes used to generate this ortho image. Looks in subdirectories of the directory
	 * where the image file is stored (currently they are v01, vo2, ...) and in subdirectories finds the
	 * latest *-MERGED.tiff file. Each scene has a slice in this file 
	 * @return number of scenes that made this ortho image, also sets the num_scenes (saved in database)
	 * so next time reading files will not be needed
	 */
	public int getNumberScenes() {
		if (num_scenes < 0) {
			String merged_path = getLatestFilePath(MERGED_SUFFIX); 
			if (merged_path != null) {
				ImagePlus imp_merged = new ImagePlus(merged_path);
				if (imp_merged.getWidth() > 0) {
					num_scenes = imp_merged.getImageStack().getSize();
				}
			}
		}
		return num_scenes;
	}
	
	/**
	 * Get maximal "SfM gain" - ratio between used interscene offset and the camera baseline.
	 * Looks in subdirectories of the directory  where the image file is stored (currently they are
	 * v01, vo2, ...) and in subdirectories finds the latest *-INTER-INTRA-LMA.tiff file. 
	 * Those files have a slice (OpticalFlow.COMBO_DSN_INDX_SFM_GAIN + 1 as slices a numbered from 1)
	 * that contains per-tile SfM gain.
	 * @return Maximal SfM gain 
	 */
	
	public double getSfmGain() {
		if (Double.isNaN(sfm_gain)) {
			String dsi_path = getLatestFilePath(DSI_SUFFIX);
			if (dsi_path != null) {
				ImagePlus imp_dsi = new ImagePlus(dsi_path);
				if (imp_dsi.getWidth() > 0) {
					float [] pixels = (float[]) imp_dsi.getImageStack().getPixels(OpticalFlow.COMBO_DSN_INDX_SFM_GAIN + 1);
					sfm_gain = 0;
					for (int i = 0; i < pixels.length; i++) {
						if (pixels[i] > sfm_gain) {
							sfm_gain = pixels[i];
						}
					}
				}
			}
		}
		return sfm_gain;
	}
	
	public String getLatestFilePath(String suffix) {
		String model_dir = getModelPathFromPath(this.path);
		File [] subdirs = (new File(model_dir)).listFiles();
		long latest_ms = 0;
		File latest_file = null;
		for (File subdir:subdirs) if (subdir.isDirectory()) {
			File [] file_vers = subdir.listFiles();
			for (File file:file_vers) if (file.isFile() && file.toString().endsWith(suffix)){
				long modified = file.lastModified();
				if (modified > latest_ms) {
					latest_ms = modified;
					latest_file = file;
				}
			}
		}
		if (latest_file != null) {
			return latest_file.getPath();
		}
		return null;
	}
	
	/**
	 * For debuggig, updating after lla generation fixed
	 */
	public void updateLLA() {
		if (orig_image != null) {
			orig_image.updateLLA();
		}
		if (alt_image != null) {
			alt_image.updateLLA();
		}
	}
	
	/*
	public void updateLLA() {
		Properties imp_prop = null;
		try {
			imp_prop = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		lla = ElphelTiffReader.getLLA(imp_prop);
	}
	*/
	
	/**
	 * Gets average pixel value of all sensors of the reference scene 
	 * @return
	 */
	public double getTemperature() {
		if (Double.isNaN(averageRawPixel)) {
			final String TIFF_EXT=".tiff";
			final File [] raw_files = (new File(getScenesPath())).listFiles();
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			final AtomicInteger ati = new AtomicInteger(0);
			final double [] avg_arr = new double [threads.length];
			final double [] npix_arr = new double [threads.length];
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						int thread_num = ati.getAndIncrement();
						ImagejJp4Tiff imagejJp4Tiff = new ImagejJp4Tiff();						
						for (int iFile = ai.getAndIncrement(); iFile < raw_files.length; iFile = ai.getAndIncrement()) {
							if (raw_files[iFile].getName().endsWith(TIFF_EXT)) {
								ImagePlus imp = null;
								String spath = raw_files[iFile].toString();
	                			try {
	                				imp= imagejJp4Tiff.readTiffJp4(spath);
	                			} catch (IOException e) {
	                				System.out.println("getImagesMultithreaded IOException " + spath);
	                			} catch (FormatException e) {
	                				System.out.println("getImagesMultithreaded FormatException " + spath);
	                			}
	                			if (imp != null) {
	                				npix_arr[thread_num] = imp.getWidth()*imp.getHeight();
	                				float [] fpixels = (float[]) imp.getProcessor().getPixels();
	                				for (int i = 0; i < fpixels.length; i++) {
	                					avg_arr[thread_num] += fpixels[i];
	                				}
	                				avg_arr[thread_num] /= npix_arr[thread_num];
	                			}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			double avg=0, num=0;
			for (int i = 0; i < avg_arr.length; i++) {
				avg+=avg_arr[i]*npix_arr[i];
				num+=npix_arr[i];
			}
			averageRawPixel = avg/num;
		}
		return averageRawPixel;	
	}
	
	public double getTemperatureAndTelemetry() {
		if (this.temp_data == null) {
			final String TIFF_EXT=".tiff";
			final File [] raw_files = (new File(getScenesPath())).listFiles();
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			final AtomicInteger ati = new AtomicInteger(0);
			final double [] avg_arr = new double [threads.length];
			final double [] npix_arr = new double [threads.length];
			final SensorTemperatureData []  temp_data = new SensorTemperatureData[raw_files.length];
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						int thread_num = ati.getAndIncrement();
						ImagejJp4Tiff imagejJp4Tiff = new ImagejJp4Tiff();						
						for (int iFile = ai.getAndIncrement(); iFile < raw_files.length; iFile = ai.getAndIncrement()) {
							if (raw_files[iFile].getName().endsWith(TIFF_EXT)) {
								ImagePlus imp = null;
								String spath = raw_files[iFile].toString();
	                			try {
	                				imp= imagejJp4Tiff.readTiffJp4(spath);
	                			} catch (IOException e) {
	                				System.out.println("getImagesMultithreaded IOException " + spath);
	                			} catch (FormatException e) {
	                				System.out.println("getImagesMultithreaded FormatException " + spath);
	                			}
	                			if (imp != null) {
	                				int channel_sep = spath.lastIndexOf("_");
	                				int channel_dot = spath.lastIndexOf(".");
	                				int chn = Integer.parseInt(spath.substring(channel_sep+1, channel_dot));
	                				temp_data[chn] = new SensorTemperatureData();
	                				Properties properties = imp.getProperties();
	                				temp_data[chn].TLM_FPA_KELV =  Double.parseDouble(properties.getProperty("TLM_FPA_KELV"));
	                				temp_data[chn].TLM_FFC_KELV =  Double.parseDouble(properties.getProperty("TLM_FFC_KELV"));	                				temp_data[chn].TLM_FPA_KELV = Double.parseDouble(properties.getProperty("TLM_FPA_KELV"));
	                				temp_data[chn].TLM_CORE_TEMP = Double.parseDouble(properties.getProperty("TLM_CORE_TEMP"));
	                				temp_data[chn].TLM_FRAME =     Integer.parseInt(properties.getProperty("TLM_FRAME"));
	                				temp_data[chn].TLM_FRAME_FFC = Integer.parseInt(properties.getProperty("TLM_FRAME_FFC"));
	                				npix_arr[thread_num] = imp.getWidth() * imp.getHeight();
	                				float [] fpixels = (float[]) imp.getProcessor().getPixels();
	                				for (int i = 0; i < fpixels.length; i++) {
	                					avg_arr[thread_num] += fpixels[i];
	                				}
	                				avg_arr[thread_num] /= npix_arr[thread_num];
	                				temp_data[chn].averagePixelValue = avg_arr[thread_num]; 
	                			}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			double avg=0, num=0;
			for (int i = 0; i < avg_arr.length; i++) {
				avg+=avg_arr[i]*npix_arr[i];
				num+=npix_arr[i];
			}
			averageRawPixel = avg/num;
			this.temp_data = temp_data; 
		}
		return averageRawPixel;	
	}

	
	public OrthoMap (
			String path) {
			this.path = path;
		name = getNameFromPath(path);
		ts= Double.parseDouble(name.replace("_", "."));
		
		// initialize and read Exif for the images
		orig_image = new FloatImageData(path);
//		dt = orig_image.getDT();
		/*
		
		Properties imp_prop = null;
		try {
			imp_prop = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			lla = ElphelTiffReader.getLLA(imp_prop);
		} catch (NullPointerException e) {
			System.out.println("No GPS data in "+path);
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		dt = ElphelTiffReader.getLocalDateTime(imp_prop);
		vert_meters =      ElphelTiffReader.getXYOffsetMeters(imp_prop);
		orig_pix_meters = ElphelTiffReader.getPixelSize(imp_prop)[0];
		if (FIX_VERT_Y) {
			int height_pix =   ElphelTiffReader.getHeight(imp_prop);
			double height_meters = height_pix * orig_pix_meters;
			vert_meters[1] = height_meters-vert_meters[1];
		}
		orig_width =  ElphelTiffReader.getWidth(imp_prop);
		orig_height = ElphelTiffReader.getHeight(imp_prop);
		
		orig_zoom_level = FloatImageData.getZoomLevel(orig_pix_meters);
		orig_zoom_valid = FloatImageData.isZoomValid(orig_pix_meters);
		need_extra_zoom = FloatImageData.needZoomIn(orig_pix_meters);
		*/
		images = new HashMap <Integer, FloatImageData>();
		pairwise_matches = new HashMap<String, PairwiseOrthoMatch>();
		averageRawPixel = Double.NaN; // measure of scene temperature
//		averageImagePixel = Double.NaN; // average image pixel value (to combine with raw)
	}

	/**
	 * Update filename (different version), update image and alt data
	 * @param filename
	 * @return true if OK, false - error
	 */
	public boolean setFileName(String filename) {
		int p1 = path.lastIndexOf(Prefs.getFileSeparator());
		if (p1 < 0) {
			return false;
		}
		path= path.substring(0, p1+1) + filename;
		if (orig_image != null) {
			File image_file = new File(path);
			if (!image_file.exists()) {
				System.out.println("Image file does not exist: "+path);
				return false;
			}
			orig_image = new FloatImageData(path);
//			dt = orig_image.getDT();
		}
		if (alt_image != null) {
			String alt_path = getAltPath(path);
			File alt_file = new File(alt_path);
			if (!alt_file.exists()) {
				System.out.println("Altitudes file does not exist: "+alt_path);
				return false;
			}
			alt_image = new FloatImageData(alt_path);
		}
		return true;
	}
	
	/**
	 * Initialize and return image data instance, do not read image data, only the metadata
	 * @return image data instance with read metadata
	 */
	public FloatImageData getImage() { 
		if (orig_image == null) {
			File image_file = new File(path);
			if (!image_file.exists()) {
				System.out.println("Image file does not exist: "+path);
				return null;
			}
			orig_image = new FloatImageData(path);
//			dt = orig_image.getDT();
		}
		return orig_image;
	}

	/**
	 * Initialize and return altitude data instance, do not read image data, only the metadata
	 * @return altitude data instance with read metadata
	 */
	public FloatImageData getAlt() {
		if (alt_image == null) {
			alt_image = getAlt(path); // img path, not alt
		}
		return alt_image;
	}
	
	public static FloatImageData getAlt(String img_path) {
		String alt_path = getAltPath(img_path);
		File alt_file = new File(alt_path);
		if (!alt_file.exists()) {
			System.out.println("Altitudes file does not exist: "+alt_path);
			return null;
		}
		return new FloatImageData(alt_path);
	}

	
	/*
	public OrthoMap (
			String path) {
			this.path = path;
		name = getNameFromPath(path);
		ts= Double.parseDouble(name.replace("_", "."));
		Properties imp_prop = null;
		try {
			imp_prop = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			lla = ElphelTiffReader.getLLA(imp_prop);
		} catch (NullPointerException e) {
			System.out.println("No GPS data in "+path);
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		dt = ElphelTiffReader.getLocalDateTime(imp_prop);
		vert_meters =      ElphelTiffReader.getXYOffsetMeters(imp_prop);
		orig_pix_meters = ElphelTiffReader.getPixelSize(imp_prop)[0];
		if (FIX_VERT_Y) {
			int height_pix =   ElphelTiffReader.getHeight(imp_prop);
			double height_meters = height_pix * orig_pix_meters;
			vert_meters[1] = height_meters-vert_meters[1];
		}
		orig_width =  ElphelTiffReader.getWidth(imp_prop);
		orig_height = ElphelTiffReader.getHeight(imp_prop);
		
		orig_zoom_level = FloatImageData.getZoomLevel(orig_pix_meters);
		orig_zoom_valid = FloatImageData.isZoomValid(orig_pix_meters);
		need_extra_zoom = FloatImageData.needZoomIn(orig_pix_meters);
		images = new HashMap <Integer, FloatImageData>();
		pairwise_matches = new HashMap<String, PairwiseOrthoMatch>();
		averageRawPixel = Double.NaN; // measure of scene temperature
		averageImagePixel = Double.NaN; // average image pixel value (to combine with raw)
	}

	 */
	
	
	
	
	public double getTimeStamp() {
		return ts;
	}
	public LocalDateTime getLocalDateTime() {
		return getImage().getDT();
//		return dt;
	}
	
	public String getName() {
		return name;
	}

	public String getPath() {
		return path;
	}
/*	
	public void setPath(String path) {
		this.path = path;
	}
*/	
	
	public String getFileName() {
		int p1 = path.lastIndexOf(Prefs.getFileSeparator()); 
		if (p1 < 0) {
			return null;
		}
		return path.substring(p1+1);
	}
	
	public void setAffine(double [][] affine) {
		this.affine = new double[][] {affine[0].clone(),affine[1].clone()};
	}

	public void setAffine() {
		this.affine = new double[][] {{1,0,0},{0,1,0}};
	}
	
	public double [][] getAffine(){
		return affine;
	}

	public void removeImageData() { // to save memory
		orig_image=null;
	}
	
	public void removeAltData() { // to save memory
		alt_image = null;
	}
	
	public void removeZoomLevel(int zoom_level) { // either to save memory or remove to later replace
		images.remove(zoom_level);
	}
	
	public int [] getZoomLevels() {
		Integer [] ks = (Integer[]) images.keySet().toArray();
		int [] zoom_levels = new int[ks.length];
		for (int i = 0; i < ks.length; i++) {
			zoom_levels[i] = ks[i];
		}
		return zoom_levels;
	}
	
	public void removeAllButZoomLevel(int zoom_level) { // either to save memory or remove to later replace
		int [] zoom_levels = getZoomLevels();
		for (int zl:zoom_levels) if (zl !=zoom_level) {
			images.remove(zl);
		}
	}

	
	public FloatImageData getImageData() {
		getImage();
		orig_image.readFData();
//		getAveragePixel();
		return orig_image;
	}
	public FloatImageData getAltData() {
		getAlt();
		alt_image.readFData();
		return alt_image;
	}
	
	public double getAveragePixel() {
		getImage();
		return orig_image.getAveragePixel();
	}
	
	/**
	 * Get average altitude above ground (AGL)
	 * @return AGL or NaN
	 */
	public double getAGL() {
		if (Double.isNaN(agl)) {
			final FloatImageData src_elev = getAltData();
			if (src_elev == null) {
				System.out.println("getTileElevations(): No elevation data available for "+name);
				return Double.NaN;
			}
			double average_asl = getMaskedAverage (
					src_elev.readFData(), // final double [] data,
					null); // final boolean [] mask) 
			agl= getLLA()[2] - average_asl;
		}
		return agl;
	}
	
	/**
	 * Get coordinates and altitude
	 * @return {latitude(deg), longitude(deg), altitude(m)}
	 */
	public double [] getLLA() {
		return (orig_image == null) ? null : orig_image.getLLA();
	}
	
	/**
	 * Get original image resolution as pixel size in meters
	 * @return image pixel size in meters
	 */
	public double getOrigPixMeters() {
		return getImage().getPixMeters(); // orig_pix_meters;
	}
	
	/**
	 * Get vertical point offset from the top-left corner of the original orthoimage in meters (right and down)
	 * @return vertical point X,Y offset in meters from the top-left image corner
	 */

	public double [] getVertMeters() {
		return getImage().getVertMeters(); // ;
	}

	public double [] getVertMetersAlt() {
		return getAlt().getVertMeters(); // ;
	}
	
	/**
	 * Get vertical point offset from the top-left corner of the original orthoimage in pixels
	 * of the original resolution
	 * @return vertical point X,Y offset in pixels from the top-left image corner in original resolution
	 */
	public int [] getVertPixels() {
		return 		getImage().getVertPixels();
	}
	
	/*
	public ImagePlus getOriginalImage(boolean show_markers) {
		FloatImageData orig_image = getImageData();
		double [] vm = getVertMeters();
		if (orig_image != null) {
			String full_name = path.substring(path.lastIndexOf(Prefs.getFileSeparator()) + 1);
			ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
					orig_image.readFData(), // float[] pixels,
					getWidth(),
					getHeight(),
					full_name);
			if (show_markers) {
				PointRoi roi = new PointRoi();
				roi.addPoint(vm[0],vm[1]);
				roi.setOptions("label");
				imp.setRoi(roi);
			}
			return imp;
		}
		return null;
	}
	*/
	public double [] enuOffsetTo(OrthoMap other) {
		return Imx5.enuFromLla(other.getLLA(), getLLA());
	}
	
	public static double getPixelSizeMeters (int zoom_level ) {
		double ps = 0.01; // 1 cm;
		if (zoom_level >0) {
			for (int i = 0; i < zoom_level; i++) {
				ps /= 2;
			}
		} else {
			for (int i = 0; i > zoom_level; i--) {
				ps *= 2;
			}
		}
		return ps;
	}
	//	private double [][]               affine = new double[2][3];
	// vert_meters =     ElphelTiffReader.getXYOffsetMeters(imp_prop);
	
	
	/**
	 * Get top-left, top-right, bottom-right, bottom-left corners coordinates relative
	 * to a "vertical" point. {{-,-},{+,-},{+,+},{-,+}} 
	 * @return double [4][2] array [corner number]{x,y} 
	 */
	public double [][] get4SourceCornersMeters(){
		getImage();
		double width_meters =  getWidth() *  orig_image.getPixMeters();
		double height_meters = getHeight() * orig_image.getPixMeters();
		double [] vert_meters = orig_image.getVertMeters(); 
		return new double[][] { // CW from TL
			{             - vert_meters[0],               - vert_meters[1]},
			{width_meters - vert_meters[0],               - vert_meters[1]},
			{width_meters - vert_meters[0], height_meters - vert_meters[1]},
			{             - vert_meters[0], height_meters - vert_meters[1]}};
	}
	
	/**
	 * Get metric bounds of this image (zero point at vert_meters) with the affine transform
	 * stored with this image.
	 * @param rectified if true, use rectified (inverse-transformed) image, false - original  
	 * @return rectified {{x_min, x_max},{y_min,y_max}} 
	 */
	public double [][] getBoundsMeters (boolean rectified){
		return getBoundsMeters (rectified, affine);
	}
	
	/**
	 * Get metric bounds of this image (zero point at vert_meters) with specified affine transform
	 * @param rectified if true, use rectified (inverse-transformed) image, false - original
	 * @param affine - 6-element affine transform  
	 * @return rectified {{x_min, x_max},{y_min,y_max}} 
	 */
	public double [][] getBoundsMeters (
			boolean     rectified,
			double [][] affine){
		double [][] corners = get4SourceCornersMeters();
		if (rectified) {
			double [][] inv_aff = invertAffine(affine);
			double [][] rec_corners = new double [4][2];
			for (int n = 0; n < rec_corners.length; n++) {
				rec_corners[n][0] = inv_aff[0][0]*corners[n][0] + inv_aff[0][1]*corners[n][1] + inv_aff[0][2]; 
				rec_corners[n][1] = inv_aff[1][0]*corners[n][0] + inv_aff[1][1]*corners[n][1] + inv_aff[1][2]; 
			}
			corners = rec_corners;
		}
		double [][] bounds = new double [2][2]; // rows: x,y. Columns: min,max.
		for (int n = 0; n < 2; n++) {
			bounds[n][0] = corners[0][n];
			bounds[n][1] = bounds[n][0];
			for (int i = 1; i < corners.length; i++) {
				bounds[n][0]=Math.min(bounds[n][0], corners[i][n]);
				bounds[n][1]=Math.max(bounds[n][1], corners[i][n]);
			}
		}
		return bounds;
	}
	
	
	
	
	/**
	 * Get pixel bounds of this image (zero point at vert_meters) as doubles (to be able to
	 * offset before converting to int.
	 * @param rectified if true, use rectified (inverse-transformed) image, false - original  
	 * @return rectified {{x_min, x_max},{y_min,y_max}} 
	 */
	public double [][] getBoundPixels(
			int zoom_level,
			boolean rectified){
		double [][] bounds_meters = getBoundsMeters (rectified);
		double pix_size = getPixelSizeMeters (zoom_level);
		double [][] bounds_pix = new double[2][2];
		for (int n = 0; n < bounds_pix.length; n++) {
			for (int i = 0; i < 2; i++) {
				bounds_pix[n][i] = bounds_meters[n][i]/pix_size;
			}
		}
		return bounds_pix;
	}
	
	/**
	 * In preparation for the GPU correlation (zoomed out images of the fixed
	 * size are calculated separately and have NaN for unused pixels.
	 * Calculate overlap area bounds relative to the reference image origin
	 * (vertical point) and top-left (of the rectified overlap area) referenced
	 * affine matrices for both images ([2][2][3]).
	 * The overlap area should fit into gpu_width, gpu_hight, the affine-generated
	 * image coordinates will be limited during TpTask[][] generation 
	 * @param ref_map    OrthoMap instance for the reference map
	 * @param var_map    OrthoMap instance for the variable map instance
	 * @param zoom_lev   zoom level (0 - 1 pix=1cm, 1 - 1 pix = 0.5cm, -1 - 1 pix = 2 cm.
	 * @param var_offset additional (to affine and lla, vert_meters) x,y offset of
	 *                   the var_map (in output pixels). Used for spiral search for initial
	 *                   match.
	 * @param gpu_affine combined [2][2][3] array for convertion from the rectified overlap
	 *                   area to the GPU input images. Referenced to the top-left pixel
	 *                   of the rectified overlap area and top-left image pixels
	 * @return {{min_x,max_x}{min_y, max_y}) of the rectified area where (0,0) corresponds
	 *         (is centered) to the reference image origin (vertical point)  
	 */
	public static int [][] getPairOvelapBoundsAffine(
			OrthoMap ref_map,
			OrthoMap var_map,
			int      zoom_lev,
			int []   var_offset,
			double [][][] gpu_affine){
		return null;
	}

	/**
	 * Convert from the affine matrix that calculates image coordinates from the rectified one
	 * (relative to the origin point) to the matrix that provides rectified coordinates from the
	 * image ones. Both are in meters relative to the "vert_meters" point
	 * @param affine [2][3] array were the last column is made of the offsets (in meters)
	 * @return [2][3] array that converts from the image metric coordinates relative to the
	 * "vert_meters" point to the rectified metric coordinates relative to the same point.
	 * 
	 */
	public static double [][] invertAffine(double [][] affine){
		Matrix A = new Matrix (
				new double [][] {{affine[0][0],affine[0][1]},{affine[1][0],affine[1][1]}});
		Matrix Ai = A.inverse();
		Matrix B = new Matrix(new double [][] {{affine[0][2]},{affine[1][2]}});
		Matrix AiB = Ai.times(B);
		double [][] iaffine = {
				{Ai.get(0,0),Ai.get(0,1), -AiB.get(0,0)},
				{Ai.get(1,0),Ai.get(1,1), -AiB.get(1,0)}};
		return iaffine;
	}
	
	public boolean downScaleForGPU (int zoom_level){
		FloatImageData orig_image = getImageData();
		if (orig_image == null) {
			System.out.println("Original image is null");
			return false;
		}
		if (!orig_image.isZoomValid()) {
			System.out.println("Original zoom level is invalid, need_extra_zoom = "+orig_image.needZoomIn());
			return false;
		}
		if (zoom_level > orig_image.getZoomLevel()) {
			System.out.println("Requested zoom level is too high ("+zoom_level+" > "+orig_image.getZoomLevel()+")");
			return false;
		}
		if (images.get(zoom_level) != null) {
			return true; // already exists
		}
		int rscale = 1;
		for (int i = orig_image.getZoomLevel(); i > zoom_level; i--) {
			rscale *= 2;
		}
		final int frscale = rscale;
		int swidth =  getWidth();
		int sheight = getHeight();
		final float [] spix = orig_image.readFData();
		if (swidth*sheight != spix.length) {
			System.out.println ("downScaleForGPU(): swidth="+ swidth+", sheight="+sheight+", swidth*sheight="+(swidth*sheight)+", spix.length="+spix.length);
			System.out.println ();
		}
		final int width = (swidth+frscale-1)/frscale;
		final int height = (sheight+frscale-1)/frscale;
		final int tiles = width * height;
		final float [] opix = new float [tiles];
		Arrays.fill(opix, Float.NaN);
		final double [][] wnd = new double [frscale] [frscale];
		for (int i = 0; i <frscale; i++) {
			double w = Math.sin((i+0.5)/frscale*Math.PI);
			for (int j = 0; j <frscale; j++) {
				wnd[i][j] = w*Math.sin((j+0.5)/frscale*Math.PI);
			}
		}
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int tile = ai.getAndIncrement(); tile < tiles; tile = ai.getAndIncrement()) {
                    	int tileX = tile%width;
                    	int tileY = tile/width;
                    	int tileW= (frscale*(tileX+1) <= swidth) ? frscale:(swidth - frscale*tileX);  
                    	int tileH= (frscale*(tileY+1) <= sheight) ? frscale:(sheight - frscale*tileY);
                    	double sw = 0.0, swd = 0.0;
                    	for (int py = 0; py < tileH; py++) {
                    		int ls = (tileY*frscale + py)*swidth;
                    		for (int px = 0; px < tileW; px++) {
                    			int indx = ls + tileX*frscale+px;
                    			if ((indx >=0) && (indx < spix.length)) {
                    				double d = spix[ls + tileX*frscale+px]; //java.lang.ArrayIndexOutOfBoundsException: Index 3892800 out of bounds for length 3892680
                    				if (!Double.isNaN(d)) {
                    					double w = wnd[py][px];
                    					sw += w;
                    					swd += w*d;
                    				}
                    			} else {
                    				System.out.println("downScaleForGPU(): indx = "+indx+" ("+spix.length+"), tileX= "+
                    						tileX+", tileY="+tileY+", px="+px+", py="+py+", ls="+ls);	
                    			}
                    		}
                    	}
            			if (sw > 0) {
            				opix[tileY * width + tileX] = (float) (swd/sw);
            			}
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
        
        images.put(zoom_level, new FloatImageData (
        		orig_image,   // 		FloatImageData master,
        		zoom_level,   // 		int       zoom_level,
        		opix));       // 		float[]   data) {

		return true;
	}
	
	/**
	 * Get elevation map with the same resolution as vector_field corresponding to the GPU-processed
	 * data. Array is organized at a tile grid (width corresponding to GPU width in tiles),source 
	 * tp_tasks provides tile center coordinates for the current GPU zoom level, they are scaled for
	 * the source elevation data
	 * @param zoom_level zoom level of the GPU image, 0 is 1pix=1cm, -1 is 1pix=2cm
	 * @param tp_tasks array of non-empty tiles to be processed in the GPU. Integer tx and ty reference
	 *        tile on a tile grid (8x8 pix), center X,Y for the only "sensor" 0 - float coordinates
	 *        corresponding to the current GPU zoom level
	 * @param tilesX
	 * @param tilesY
	 * @param debugLevel
	 * @return
	 */
	public double [] getTileElevations(
			final int zoom_level,
			final TpTask [] tp_tasks, 
			final int tilesX,
			final int tilesY,
			final int debugLevel) {
		final FloatImageData src_elev = getAltData();
		if (src_elev == null) {
			if (debugLevel > -4) {
				System.out.println("getTileElevations(): No elevation data available for "+name);
			}
			return null;
		}
		final int src_width = src_elev.width;
		final int src_height = src_elev.height;
		final float [] felev = src_elev.readFData(); 
		final double [] elev = new double [tilesX*tilesY];
		Arrays.fill(elev, Double.NaN);
		final int diff_zoom_lev = zoom_level-getAlt().getZoomLevel(); // orig_zoom_level;
		final double scale = 1.0/getScale (diff_zoom_lev); //	int zoom_lev);
		
		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 iTile = ai.getAndIncrement(); iTile < tp_tasks.length; iTile = ai.getAndIncrement()) {
						TpTask task = tp_tasks[iTile];
						float [] xy = task.getXY()[0];
						double px = xy[0] * scale; 
						double py = xy[1] * scale;
						// linear interpolate
						int ipx = (int) Math.floor(px);
						int ipy = (int) Math.floor(py);
						double fpx = px -ipx;
						double fpy = py -ipy;
						if ((ipx >= 0) && (ipy >= 0) && (ipx < (src_width-1)) && (ipy < (src_height-1))) {
							int indx00 = ipy*src_width+ipx;
							int indx = task.getTileY()*tilesX + task.getTileX();
							elev[indx] =
									(1.0-fpx)*(1.0-fpy)*felev[indx00] +
									(    fpx)*(1.0-fpy)*felev[indx00 + 1] +
									(1.0-fpx)*(    fpy)*felev[indx00 + src_width] + // throwing
									(    fpx)*(    fpy)*felev[indx00 + src_width + 1];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return elev;
	}
	
	/**
	 * Get scale from zoom level: 0-> 1.0, -1 -> 0.5
	 * @param zoom_lev
	 * @return
	 */
	public static double getScale (
			int zoom_lev) {
		double s = 1.0;
		int zl = zoom_lev;
		for (; zl< 0; zl++) s *= 0.5;
		for (; zl> 0; zl--) s *= 2.0;
		return s;
	}
	
	/**
	 * Copy prepared scaled image to the top-left corner of the fixed-size
	 * float[] array for the GPU input
	 * @param zoom_level
	 * @return
	 */
	public float [] getPaddedGPU (
			int zoom_level){
		boolean got_image = downScaleForGPU (zoom_level); // will create if does not exist
		if (!got_image) {
			System.out.println("getPaddedGPU(): failed to prepare a scaled image");
			return null;
		}
		FloatImageData fid = images.get(zoom_level);
		
		final float [] padded_gpu = new float[gpu_width*gpu_height];
		final int swidth =  fid.width;
		final int sheight = fid.height;
		final float [] spix = fid.readFData();
		Arrays.fill(padded_gpu, Float.NaN);
		final int cheight = Math.min(sheight,gpu_height);
		final int cwidth =  Math.min(swidth, gpu_width);

		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 row = ai.getAndIncrement(); row < cheight; row = ai.getAndIncrement()) {
        				System.arraycopy(
        						spix,
        						row*swidth,
        						padded_gpu,
        						row*gpu_width,
        						cwidth);
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		return padded_gpu;
	}
	
	public static void generateDATI(
			ImagePlus imp,
			final int fft_size,
			int [] slices,
			int debugLevel) { // >0
		double [] kernel = getConvolutionKernel();
		debugLevel = 1;
		boolean  flat_high_res =        false;
		boolean  temp_high_res =        false; // false - use convolved with kernel
		
		double   neib_radius =          2.0; // 1.5;
		double   neib_radius_other=     1.5; // 1.1;
		double   max_heib_diff =       20; // 30; // 50; // not much filtering
		double   max_neib_diff_other=  30;// 50; // 60;
		boolean  four_sides =          true;//false;
		int      num_temp =           256; // 1024; // height
		int      num_dati =           256; // 1024; // width
		double   frac_hi_temp =         0.005;
		double   frac_lo_temp =         0.0001;
		double   frac_hi_dati =         0.005;
		double   frac_lo_dati =         0.005;
		boolean  invert_y =             true; // high temperature - up
		int      mine_number =          2; // 100; // all which of the first min_markers are mines (can be > length)
		double   falsepos_rel_radius=   0.05; // 0.04; // relative (to selected ranges) radius of false positives

		
		final boolean  generate_dati_histogram =  true;
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,600);
		gd.addCheckbox    ("Use high-res for flat points",      flat_high_res, "If false - use low-res convolved version.");
		gd.addCheckbox    ("Use high-res for histogram",        temp_high_res, "If false -  - use low-res convolved version.");
		gd.addMessage("--- Finding locally flat points to use for the 2D histogram ---");
		gd.addCheckbox    ("4-sides flat",                      four_sides, "Require local min-max in both horizontal and vertical direction (false - one pair is enough).");
		gd.addNumericField("Flatness check in first image",     neib_radius,  3,7,"pix",	"Low/high res as selected above.");
		gd.addNumericField("Flatness check in second image",    neib_radius_other,  3,7,"pix",	"In second image.");
		gd.addNumericField("Maximal difference in first image", max_heib_diff,  3,7,"counts",	"Maximal difference from the center pixel within the specified radius.");
		gd.addNumericField("Maximal difference in first image", max_neib_diff_other,  3,7,"counts",	"Maximal difference from the center pixel within the specified radius.");
		gd.addMessage("--- Generating 2D histogram ---");
		gd.addNumericField("Histogram height (temperature)",    num_temp,  0,4,"pix", "Number of temperature bins,");
		gd.addNumericField("Histogram width (DATI)",            num_dati,  0,4,"pix", "Number of DATI bins,");
		gd.addNumericField("Discard high temperature samples",  frac_hi_temp,  4,8,"",	"Fraction of all samples.");
		gd.addNumericField("Discard low  temperature samples",  frac_lo_temp,  4,8,"",	"Fraction of all samples.");
		gd.addNumericField("Discard high DATI samples",         frac_hi_dati,  4,8,"",	"Fraction of all samples.");
		gd.addNumericField("Discard low  DATI samples",         frac_lo_dati,  4,8,"",	"Fraction of all samples.");
		
		gd.addCheckbox    ("High temperature - up",             invert_y, "Plot high-temperature up (y-axis). High DATI is always to the right (x-axis).");
		gd.addNumericField("Number of marked mines",            mine_number,  0,4,"", "First marks are considered mines, other - just samples to see in the histogram.");
		gd.addNumericField("Cluster radius (relative)",         falsepos_rel_radius,  3,7,"","Relative to temperature and DATI plotted ranges.");
		gd.addMessage("Histogram may be manually blurred with sigma=2.0" );
		gd.showDialog();
		if (gd.wasCanceled()) return;
        flat_high_res=              gd.getNextBoolean();
        temp_high_res=              gd.getNextBoolean();
        four_sides=                 gd.getNextBoolean();
        neib_radius=                gd.getNextNumber();
        neib_radius_other=          gd.getNextNumber();
        max_heib_diff=              gd.getNextNumber();
        max_neib_diff_other=        gd.getNextNumber();
        num_temp=             (int) gd.getNextNumber();
        num_dati=             (int) gd.getNextNumber();
        frac_hi_temp=               gd.getNextNumber();
        frac_lo_temp=               gd.getNextNumber();
        frac_hi_dati=               gd.getNextNumber();
        frac_lo_dati=               gd.getNextNumber();
        invert_y=                   gd.getNextBoolean();
        mine_number=         (int)  gd.getNextNumber();
        falsepos_rel_radius=        gd.getNextNumber();
		
		final int slice =      flat_high_res? 0: 1; // 1;             // convolved
		final int temp_index = temp_high_res? 0: 1;
		ImageStack stack = imp.getStack();
		final int width = stack.getWidth();
		final int height = stack.getHeight();
		//		Roi roi= imp.getRoi();
		//		boolean good_ROI = false;
		

		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final double [][] full_img = new double [4][width*height];
		final int [] out_slices = {0,2,1,3}; // {high-res, low-res, convoled_high_res,dati} 
		for (int n = 0; n < 2; n++) {
			final int fn = out_slices[n];
			final float [] fpixels = (float[]) stack.getPixels(slices[n]);
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int ipix = ai.getAndIncrement(); ipix < full_img[fn].length; ipix = ai.getAndIncrement()) {
							full_img[fn][ipix] = fpixels[ipix]; 
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		if (kernel != null) {
			full_img[out_slices[2]] = convolveWithKernel(
					full_img[out_slices[0]], //  final double [] data,
					kernel, // final double [] kernel,
					width); // final int width)
		} else {
			full_img[out_slices[2]] = full_img[out_slices[0]].clone();
		}
		// calculate DATI
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < full_img[0].length; ipix = ai.getAndIncrement()) {
						full_img[out_slices[3]][ipix] = full_img[out_slices[1]][ipix]-full_img[out_slices[2]][ipix]; 
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (!generate_dati_histogram) { // show just that, otherwise add more layers
			if (debugLevel>0) {
				String [] test_titles= {"high_res","convolved", "low_res", "DATI"};
				ShowDoubleFloatArrays.showArrays(
						full_img,
						width,
						height,
						true,
						removeKnownExtension(imp.getTitle())+"-DATI",
						test_titles);
			}
		} else { 
			int [] nonNaNs = {2};         // low_res
			int    slice_other = 2;       // -1 to skip testing for flatness
			boolean [] flat = extractFlatPixels(
					full_img,             // final double [][] data,
					 width,               // final int         width,
					 slice,               // final int         slice,
					 nonNaNs,             // final int []      nonNaNs,
					 slice_other,         // final int         slice_other, // <0 if not controlled
					 four_sides,          // final boolean     four_sides, // false - only opposite
					 neib_radius,         // final double      neib_radius,
					 max_heib_diff,       // final double      max_heib_diff,
					 neib_radius_other,   // final double      neib_radius_other,
					 max_neib_diff_other, // final double      max_neib_diff_other,
					 debugLevel);         // final int         debugLevel)
			double [] data_temp = full_img[temp_index]; // [1]; // [0];  // may be one of the others : [1], [2]
			if (debugLevel >-4) {
				String sres = (new String[] {"high res","low res (convolved with kernel)"})[temp_index];
				System.out.println("Using reference layer "+temp_index+" ("+sres+").");
			}
			double [] data_dati = full_img[3];  // may be one of the others : [1], [2]
			// get mine markers
			 double [][]   mine_markers = null;
			 PointRoi pRoi = (PointRoi) imp.getRoi();
			 if (pRoi != null) {
				 FloatPolygon fp = pRoi.getContainedFloatPoints();
				 mine_markers = new double [fp.npoints][2];
				 for (int np = 0; np< mine_markers.length; np++) {
					 mine_markers[np][0] =  fp.xpoints[np];
					 mine_markers[np][1] =  fp.ypoints[np];
				 }
			 }
			 final ArrayList<Integer> falsepos_list=new ArrayList<Integer>();
			
			 ImagePlus imp_dati =	mapFlatPixels(
					 data_temp,           // final double []     temp, // may try different
					 data_dati,           // final double []     dati,
					 flat,                // final boolean[]     flat,
					 width,               // final int           width,
					 num_temp,            // final int           num_temp, // height
					 num_dati,            // final int           num_dati, // width
					 invert_y,            // final boolean       invert_y,
					 frac_hi_temp,        // final double        frac_hi_temp,
					 frac_lo_temp,        // final double        frac_lo_temp,
					 frac_hi_dati,        // final double        frac_hi_dati,
					 frac_lo_dati,        // final double        frac_lo_dati,
					 mine_markers,        // final double [][]   mine_markers, // may be null of the original image
					 mine_number,         // final int           mine_number, // which of the first min_markers are mines (can be > length)
					 falsepos_rel_radius, // final double        falsepos_rel_radius, // relative (to selected ranges) radius of false positives
					 falsepos_list,       // final ArrayList<Integer> falsepos_list,  // initialized list of false positive indices in temp[],dati[], flat
					 debugLevel);         // final int           debugLevel)
			 String title = removeKnownExtension(imp.getTitle())+"-"+imp_dati.getTitle();
			 imp_dati.setTitle(title);
			 imp_dati.show();
			 if ((falsepos_list != null) && (falsepos_list.size() > 0)) {
				 System.out.println("generateDATI(): Found "+falsepos_list.size()+" potential false+real positives, marked in the image");
			 }
			 if (debugLevel>-4) {
				 String [] test_titles= {"high_res","convolved", "low_res", "DATI"};
				 String impfp_title = removeKnownExtension(imp.getTitle())+"-FALSE";
			        ImageStack stack_impfp = ShowDoubleFloatArrays.makeStack( 
			        		full_img,
							 width,
							 height,
							 test_titles,
			        		false);
			        ImagePlus impfp = new ImagePlus(impfp_title, stack_impfp);
		         	PointRoi roifp = new PointRoi();
		         	for (int ipix:falsepos_list) {
		         		roifp.addPoint(ipix % width, ipix / width, 4); 
		         	}
		         	roifp.setOptions("label, circle");
		         	impfp.setRoi(roifp);
		         	impfp.show();
			 }
		}
		/*
		*/
		
		
		System.out.println("generateDATI() Done");
	}

	/**
	 * Read one of the convolution kernels (or null if no convolution is needed)
	 * @return square kernel as a 1-d double array or null
	 */
	public static double [] getConvolutionKernel() {
		return getConvolutionKernel(kernel_paths.length-1);
	}
	public static double [] getConvolutionKernel(int choice) {
		String kernel_path = null;
		GenericJTabbedDialog gds = new GenericJTabbedDialog("Select kernel path",1200,400);
		gds.addChoice("Kernel path:", kernel_paths, kernel_paths[choice]);
		gds.showDialog();
		if (gds.wasCanceled()) return null;
		int kernel_index = gds.getNextChoiceIndex();
		double [] kernel = null;
		if (kernel_index > 0) { 
			kernel_path= kernel_paths[kernel_index];
			ImagePlus imp_kernel = new ImagePlus(kernel_path);
			if (imp_kernel.getWidth() == 0) {
				System.out.println("generateDATI(): precomputed kernel "+kernel_path+
						" is not found");
				return null;
			}
			float [] kernel_pixels = (float[]) imp_kernel.getProcessor().getPixels();
			kernel = new double[kernel_pixels.length];
			for (int i = 0; i < kernel.length; i++) {
				kernel[i] = kernel_pixels[i];
			}
		}
		return kernel;
	}
	
	
	public static ImagePlus mapFlatPixels(
			final double []     temp, // may try different
			final double []     dati,
			final boolean[]     flat,
			final int           width,
			final int           num_temp, // height
			final int           num_dati, // width
			final boolean       invert_y,
			final double        frac_hi_temp,
			final double        frac_lo_temp,
			final double        frac_hi_dati,
			final double        frac_lo_dati,
			final double [][]   mine_markers, // may be null of the original image
			final int           mine_number_in, // which of the first min_markers are mines (can be > length)
			final double        falsepos_rel_radius, // relative (to selected ranges) radius of false positives
			final ArrayList<Integer> falsepos_list,  // initialized list of false positive indices in temp[],dati[], flat
			final int           debugLevel) {
		final int mine_number = (mine_markers != null)? Math.min(mine_number_in, mine_markers.length):0;
		final int num_bins =        1000;
		final double [][] min_max = new double [2][];
		min_max[0] = getMinMax(
				temp,          // final double []     data,
				flat,          // final boolean[]     mask, // may be null
				null,          // final double []     weights, // may be null
				frac_hi_temp,  // final double        frac_hi,
				frac_lo_temp,  // final double        frac_lo,
				num_bins,      // int                 num_bins,
				debugLevel);  // final int           debug_level)
		min_max[1] = getMinMax(
				dati,          // final double []     data,
				flat,          // final boolean[]     mask, // may be null
				null,          // final double []     weights, // may be null
				frac_hi_temp,  // final double        frac_hi,
				frac_lo_temp,  // final double        frac_lo,
				num_bins,      // int                 num_bins,
				debugLevel);  // final int           debug_level)
		if (debugLevel > -4) {
			System.out.println("mapFlatPixels(): Minimal temperature: "+min_max[0][0]+", maximal: "+min_max[0][1]);
			System.out.println("                 Minimal difference:  "+min_max[1][0]+", maximal: "+min_max[1][1]);
		}
		
		final double [] scales = {
				num_temp/( min_max[0][1]- min_max[0][0]),
				num_dati/( min_max[1][1]- min_max[1][0])
		};
		  		
		final double [] map =  new double [num_dati*num_temp];
		
		for (int i = 0; i < flat.length; i++) if (flat[i]) {
			// TODO: average dati and temp over circles?
			// TODO: use high-res (raw 50m) for temp?
			// TODO: add markers
			// TODO: read temperatures
			int y = (int) ((temp[i] - min_max[0][0]) * scales[0]); 
			int x = (int) ((dati[i] - min_max[1][0]) * scales[1]);
			if (invert_y) y = num_temp - y -1;
			if ((y >= 0) && (y < num_temp) && (x >=0) && (x <= num_dati)) {
				map[y*num_dati + x] += 1.0;
			}
		}
		String [] map_names = {"map"}; // maybe add more later
		String map_title = String.format("DATI_map_t%f06.1:%f06.1_d%f06.1:%f06.1",
				min_max[0][0],min_max[0][1],min_max[1][0],min_max[1][1]);
        ImageStack stack = ShowDoubleFloatArrays.makeStack( 
        		new double [][] {map},
        		num_dati,
        		num_temp,
        		map_names,
        		false);
        ImagePlus imp = new ImagePlus(map_title, stack);
        final double [][] pix_mine_centers = new double [mine_number][2]; 
        if (mine_markers != null) {
        	int slice_map = 1;
        	PointRoi roi = new PointRoi();
        	for (int i = 0; i < mine_markers.length; i++) {
        		int indx = ((int) Math.round(mine_markers[i][1])) * width + ((int) Math.round(mine_markers[i][0]));
    			int x = (int) ((dati[indx] - min_max[1][0]) * scales[1]);
    			int y = (int) ((temp[indx] - min_max[0][0]) * scales[0]);
        		if (i < mine_number) {
        			pix_mine_centers[i][0] = x; // before limit 
        			pix_mine_centers[i][1] = y; // before invert and limit 
        		}

    			if (invert_y) y = num_temp - y -1;
    			if (y < 0) y = 0;
    			else if (y >= num_temp) y= num_temp - 1;
    			if (x < 0) x = 0;
    			else if (x >= num_dati) x= num_dati - 1;
        		roi.addPoint(x,y, slice_map);
        		
        	}
        	roi.setOptions("label");
        	imp.setRoi(roi);
        }
        if (falsepos_list != null) {
        	final double pix_radius_temp = falsepos_rel_radius * (min_max[0][1]-min_max[0][0]) * scales[0];
        	final double pix_radius_dati = falsepos_rel_radius * (min_max[1][1]-min_max[1][0]) * scales[1];

    		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
    		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 < flat.length; iPix = ai.getAndIncrement()) if (flat[iPix]){
    		    			int x = (int) ((dati[iPix] - min_max[1][0]) * scales[1]);
    		    			int y = (int) ((temp[iPix] - min_max[0][0]) * scales[0]);
    		    			for (int imine = 0; imine < mine_number; imine++) {
    		    				double dx = (x - pix_mine_centers[imine][0])/pix_radius_dati;
    		    				double dy = (y - pix_mine_centers[imine][1])/pix_radius_temp;
    		    				if ((dx*dx+dy*dy) < 1.0) {
    		    					falsepos_list.add(iPix);
    		    					break;
    		    				}
    		    			}
    					}
    				}
    			};
    		}		      
    		ImageDtt.startAndJoin(threads);
        }
		return imp;
	}
	
	public static double [] getMinMax(
			final double []     data,
			final boolean[]     mask, // may be null
			final double []     weights, // may be null
			final double        frac_hi,
			final double        frac_lo,
			final int           num_bins,
			final int           debug_level) {
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [][] min_max_thread = new double [threads.length][2];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					min_max_thread[thread_num][0] = Double.POSITIVE_INFINITY;
					min_max_thread[thread_num][1] = Double.NEGATIVE_INFINITY;
					for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) {
						if (!Double.isNaN(data[iPix]) && ((mask==null) || (mask[iPix]))){
							if (data[iPix] < min_max_thread[thread_num][0]) {
								min_max_thread[thread_num][0] = data[iPix]; 
							}
							if (data[iPix] > min_max_thread[thread_num][1]) {
								min_max_thread[thread_num][1] = data[iPix]; 
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final double [] min_max = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
		for (int i = 0; i < min_max_thread.length; i++) {
			if (min_max_thread[i][0] < min_max[0]) {
				min_max[0] = min_max_thread[i][0];
			}
			if (min_max_thread[i][1] > min_max[1]) {
				min_max[1] = min_max_thread[i][1];
			}
		}
		if ((frac_hi==0) && (frac_lo == 0)) {
			return min_max; // no need for the histograms
		}
		
		final double [][] histograms = new double [threads.length][num_bins];
		final double scale = num_bins / (min_max[1]-min_max[0]);
		ai.set(0);
		ati.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) {
						if (!Double.isNaN(data[iPix]) && ((mask==null) || (mask[iPix]))){
							int bin = (int) Math.round((data[iPix] - min_max[0])*scale);
							if (bin < 0) bin=0;
							else if (bin >= num_bins) bin=num_bins-1;
							double w = (weights == null)? 1.0: weights[iPix];
							histograms[thread_num][bin] += w;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		final double [] histogram =    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 < histograms.length; i++) {
							histogram[bin]+= histograms[i][bin];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double sw = 0;
		for (int bin = 0; bin < num_bins; bin++) {
			sw += histogram[bin];
		}
		double trlow = sw * frac_lo;
		double trhigh = sw * frac_hi;
		double sh = 0, shp = 0;
		double threshold_high = min_max[1];
		if (frac_hi > 0) {
			for (int bin = num_bins-1; bin>=0; bin--) {
				shp = sh;
				sh += histogram[bin];
				if (sh > trhigh) {
					double r = (sh-trhigh)/(sh-shp);
					threshold_high = min_max[0] + (bin + r)/scale;
					break;
				}
			}
		}
		sh = 0; 
		shp = 0;
		double threshold_low = min_max[0];
		if (frac_lo > 0) {
			for (int bin = 0; bin < num_bins; bin++) {
				shp = sh;
				sh += histogram[bin];
				if (sh > trlow) {
					double r = (trlow-shp)/(sh-shp);
					threshold_low = min_max[0] + (bin + r)/scale;
					break;
				}
			}
		}
		return new double[] {threshold_low,threshold_high};
	}

	
	
	
	
	
	
	
	

	/**
	 * Extracts ROI, rounds to fft_size and deconvolves 2-nd slice (lower resolution) with
	 * the first one (higher resolution)
	 * @param imp
	 */
	public static void createMismatchedResolutionKernel(
			ImagePlus imp,
			final int fft_size,
			int [] slices,
			int kernel_radius,
			boolean hor_sym,
			boolean vert_sym,
			boolean all_sym,
			int debugLevel) { // >0
		ImageStack stack = imp.getStack();
		final int width = stack.getWidth();
		Roi roi= imp.getRoi();
		boolean good_ROI = false;
		Rectangle rroi = null;
		if (roi != null) {
			good_ROI = roi.getType()== Roi.RECTANGLE;
		}
		if (good_ROI) {
			rroi=roi.getBounds();
			if ((rroi.width != fft_size) || (rroi.height != fft_size)) {
				good_ROI=false;
			}
		}
		if (slices == null) {
			slices = new int [] {1,2}; 
		}
		if (!good_ROI) {
				System.out.println("it needs rectangular selection of "+fft_size+"x"+fft_size);
				return;
		}
		roi= imp.getRoi(); // retry roi
		rroi=roi.getBounds();
		if (rroi.width != fft_size) {
			rroi.width = fft_size;
		}
		if (rroi.height != fft_size) {
			rroi.height = fft_size;
		}
		final double [][] dpixels = new double [2][fft_size*fft_size];
		final int tl = rroi.y*width+rroi.x; // top left corner index 
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int n = 0; n < 2; n++) {
			final int fn = n;
			final float [] fpixels = (float[]) stack.getPixels(slices[fn]);
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int ipix = ai.getAndIncrement(); ipix < dpixels[fn].length; ipix = ai.getAndIncrement()) {
							int ix = ipix % fft_size;
							int iy = ipix / fft_size;
							dpixels[fn][ipix] = fpixels[tl+iy*width+ix]; 
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		double fat_zero = 1000; // 300 - too small
		double [] deconvolved= deconvolvePair(
				dpixels,
				fat_zero,
				debugLevel);
		if (debugLevel>0) {
			double [][] test_img = new double [][] {dpixels[0], dpixels[1], deconvolved}; // dpixels modified - DC, window
			String [] test_titles= {"high_res","low_res","deconvolved"};
			ShowDoubleFloatArrays.showArrays(
					test_img,
					fft_size,
					fft_size,
					true,
					removeKnownExtension(imp.getTitle())+"deconvolved",
					test_titles);
		}

		double [] kernel = extractKernel(
				deconvolved,  // double [] data,
				kernel_radius, // int       kernel_radius,
				hor_sym,       // boolean   hor_sym,
				vert_sym,      // boolean   vert_sym,
				all_sym,       // boolean   all_sym,
				debugLevel);   // int       debugLevel)
		System.out.println("Manually duplicate and save the last slice of teh displayed kernel stack");
		System.out.println("createMismatchedResolutionKernel() Done");
		// TODO - add automatic saving of the kernel
	}

	public static boolean [] extractFlatPixels(
			final double [][] data,
			final int         width,
			final int         slice,
			final int []      nonNaNs,
			final int         slice_other, // <0 if not controlled
			final boolean     four_sides, // false - only opposite
			final double      neib_radius,
			final double      max_heib_diff,
			final double      neib_radius_other,
			final double      max_neib_diff_other,
			final int         debugLevel) {
		final boolean dati_subtract_regression=true;
		final int regression_temp = slice;
		final int height = data[slice].length/width;
		final boolean [] flat = new boolean [data[slice].length];
		final double [] data_slice =       data[slice];
		final double [] data_slice_other = (slice_other >=0) ? data[slice_other]: null;
		final double [] dati = data[3];
		final double [] temp = data[regression_temp];
		
		final int [][] neibs=       getCirclePoints(neib_radius);
		final int [][] neibs_other= (slice_other >=0) ? getCirclePoints(neib_radius_other):null;
		final TileNeibs tn =  new TileNeibs(width, height);
		final int [][] opposite_pairs= {{1,3},{0,2}}; // may be modified to 8 dirs
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final int dbg_pix = 1123*2288+1156; // 2037*3060+1631;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int [] neibs_ind = new int [4];
					for (int ipix = ai.getAndIncrement(); ipix < data_slice.length; ipix = ai.getAndIncrement()) if (!Double.isNaN(data[slice][ipix])){
						if (ipix == dbg_pix) {
							System.out.println("extractFlatPixels(): ipix="+ipix);
						}
						pix_process: {
							double d = data_slice[ipix];
							for (int s:nonNaNs) {
								if (Double.isNaN(data[s][ipix])) {
									break pix_process;
								}
							}
							for (int i = 0; i < TileNeibs.DIRS/2; i++) {
								neibs_ind[i]= tn.getNeibIndex(ipix, i*2);
							}
							int num_pairs = 0;
							for (int i = 0; i < opposite_pairs.length; i++) {
								int p0= neibs_ind[opposite_pairs[i][0]];
								int p1= neibs_ind[opposite_pairs[i][1]];
								if ((p0>=0) && (p1>=0) && ((data_slice[p0] - d) * (data_slice[p1] - d) > 0)) { // correct NaN
									num_pairs++;
								}
							}
							if ((num_pairs > 0) && (!four_sides || (num_pairs > 1))) { // first condition met, try neib_radius
								for (int[] ip : neibs) {
									int p = tn.getNeibIndex(ipix, ip[0], ip[1]);
									if (p >= 0) {
										double d1 = data_slice[p];
										if (!Double.isNaN(d1)) { // Skip NaNs, they are OK 
											if (Math.abs(d1-d) > max_heib_diff) {
												break pix_process;
											}
										}
									}
								}
								if (data_slice_other != null) {
									double d_other = data_slice_other[ipix];
									// Here it should be already tested for NaN as slice_other should be in nonNaNs array to test
									for (int[] ip : neibs_other) {
										int p = tn.getNeibIndex(ipix, ip[0], ip[1]);
										if (p >= 0) {
											double d1 = data_slice_other[p];
											if (!Double.isNaN(d1)) { // Skip NaNs, they are OK 
												if (Math.abs(d1-d_other) > max_neib_diff_other) {
													break pix_process;
												}
											}
										}
									}
								}
								flat[ipix] = true;
							}
						} // pix_process
					} // for (int ipix
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double [] ab = PolynomialApproximation.getYXRegression(
				temp, // final double []  data_x,
				dati, // final double []  data_y,
				flat); // final boolean [] mask);
		double a = ab[0];
		double b = ab[1];
		if (debugLevel > -4) {
			System.out.println("extractFlatPixels(): regression a="+a+", b="+b);
		}
		double [] orig_dati = dati.clone(); 
		if (dati_subtract_regression) {
//			final double [] corr_dati = dati.clone();
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int ipix = ai.getAndIncrement(); ipix < data_slice.length; ipix = ai.getAndIncrement())
							if (!Double.isNaN(dati[ipix])){
								dati[ipix] -= a * temp[ipix] + b; 	
							}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		if (debugLevel>0) {
			String [] test_titles = {"high_res","convolved", "low_res", "DATI", "orig_dati","flat"};
			double [][] dbg_img = new double [test_titles.length][];
			for (int i = 0; i < data.length; i++) {
				dbg_img[i] = data[i];
			}
			dbg_img[4] = orig_dati;
			dbg_img[5] = new double [flat.length];
			int num_flat=0;
			for (int i = 0; i < flat.length; i++) {
				dbg_img[5][i] = flat[i]? 100:Double.NaN;
				if (flat[i]) num_flat++;
			}
			
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					width,
					height,
					true,
					"DATI-flat_fs"+four_sides+"_nr"+neib_radius+"_or"+neib_radius_other+"_md"+max_heib_diff+"_od"+max_neib_diff_other,
					test_titles);
			
			System.out.println("extractFlatPixels(): found      "+num_flat+" points.");
			System.out.println("                     four_sides=         "+four_sides);
			System.out.println("                     neib_radius=        "+neib_radius);
			System.out.println("                     neib_radius_other=  "+neib_radius_other);
			System.out.println("                     max_heib_diff=      "+max_heib_diff);
			System.out.println("                     max_neib_diff_other="+max_neib_diff_other);
		}
		return flat;
	}
	
	
	private static int [][] getCirclePoints(
			double radius) {
		ArrayList<Point> point_list = new ArrayList<Point>();
		double r2 =radius*radius;
		int ir = (int) radius;
		for (int y = -ir; y <= ir; y++) {
			for (int x = -ir; x <= ir; x++) {
				if ((y*y+x*x) < r2) point_list.add(new Point(x,y));
			}
		}
		int [][] point_arr = new int[point_list.size()][2];
		for (int i = 0; i < point_arr.length; i++) {
			point_arr[i][0] = point_list.get(i).x;
			point_arr[i][1] = point_list.get(i).y;
		}
		return point_arr;
	}
	///media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/kernels/
	/*
 /media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/kernels/kernel_25_50.tiff
 /media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/kernels/kernel_50_100.tiff
 /media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/kernels/kernel_50_75.tiff
	 */
	public static void combineKernels() { // specific hard-wired kernels
		String kernels_dir =   "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/kernels/";
		String kernel_25_50_after = "kernel_25_50_after.tiff";
		/*
		String kernel_25_50 =  "kernel_25_50.tiff";
		String kernel_50_75 =  "kernel_50_75.tiff";
		String kernel_50_100 = "kernel_50_100.tiff";
		String kernel_25_75 =  "kernel_25_75.tiff";
		String kernel_25_100 = "kernel_25_100.tiff";
*/
		String [] kernels_paths = {
				kernels_dir+kernel_25_50_after}; // 
//				kernels_dir+kernel_25_50,
//				kernels_dir+kernel_50_75,
//				kernels_dir+kernel_50_100};
		double [][] kernels_data = new double [kernels_paths.length][];
		for (int n = 0; n < kernels_data.length; n++) {
			ImagePlus imp = new ImagePlus(kernels_paths[n]);
			float [] pixels = (float []) imp.getProcessor().getPixels();
			kernels_data[n] = new double [pixels.length];
			for (int i = 0; i < pixels.length; i++) {
				kernels_data[n][i] = pixels[i];
			}
		}
		ArrayList<Double> scale_list = new ArrayList<Double>();
		for (double scale = 1.5; scale < 3.0; scale += 0.05) {
			scale_list.add(scale);
		}
		for (double scale = 3.0; scale <= 4.5; scale += 0.1) {
			scale_list.add(scale);
		}
		
		
		
		for (double scale:scale_list) {
			double [] dkernel_scaled = scaleKernel(scale, kernels_data[0]);
			int size = (int) Math.sqrt(dkernel_scaled.length);
			String title = String.format("kernel_25x%4.2f.tiff",scale);
			ImagePlus imp_kernel = ShowDoubleFloatArrays.makeArrays(
					dkernel_scaled,
					size,
					size,
					title);
//			imp_kernel.show();
			String kpath = kernels_dir+title;
			FileSaver imp_kernel_fs = new FileSaver(imp_kernel);
			imp_kernel_fs.saveAsTiff(kpath);
//			imp_kernel//
			
		}
		/*
		double [] dkernel_25_75 =  combineKernels(
				kernels_data[0],
				scaleKernel(2, kernels_data[1]));
		double [] dkernel_25_100 = combineKernels(
				kernels_data[0],
				scaleKernel(2, kernels_data[2]));
		double [] dkernel_25_50_2x = scaleKernel(2, kernels_data[0]);
		double [] dkernel_25_50_3x = scaleKernel(3, kernels_data[0]);
		double [] dkernel_25_50_4x = scaleKernel(4, kernels_data[0]);
		int size_25_75 =  (int) Math.sqrt(dkernel_25_75.length);
		int size_25_100 = (int) Math.sqrt(dkernel_25_100.length);
		ImagePlus [] imp_kernel = new ImagePlus[5];
		imp_kernel[0] = ShowDoubleFloatArrays.makeArrays(
				dkernel_25_75,
				size_25_75,
				size_25_75,
				kernel_25_75);
		imp_kernel[1] = ShowDoubleFloatArrays.makeArrays(
				dkernel_25_100,
				size_25_100,
				size_25_100,
				kernel_25_100);
		imp_kernel[2] = ShowDoubleFloatArrays.makeArrays(
				dkernel_25_50_2x,
				(int) Math.sqrt(dkernel_25_50_2x.length),
				(int) Math.sqrt(dkernel_25_50_2x.length),
				"kernel_25_50_2x");
		imp_kernel[3] = ShowDoubleFloatArrays.makeArrays(
				dkernel_25_50_3x,
				(int) Math.sqrt(dkernel_25_50_3x.length),
				(int) Math.sqrt(dkernel_25_50_3x.length),
				"kernel_25_50_3x");
		imp_kernel[4] = ShowDoubleFloatArrays.makeArrays(
				dkernel_25_50_4x,
				(int) Math.sqrt(dkernel_25_50_4x.length),
				(int) Math.sqrt(dkernel_25_50_4x.length),
				"kernel_25_50_4x");
		imp_kernel[0].show();
		imp_kernel[1].show();
		imp_kernel[2].show();
		imp_kernel[3].show();
		imp_kernel[4].show();
		*/
		System.out.println("combineKernels(): Created kernels");
	}
	
	/*
	public static double [] scaleKernel(
			int       scale, // normally is exactly twice
			double [] kernel1) {
		int kernel_size1 = (int) Math.sqrt(kernel1.length);
		int radius1 = (kernel_size1 - 1)/2;
		int radius = scale * radius1;
		int kernel_size = 2 * radius + 1;
		double [] kernel = new double [kernel_size*kernel_size];
		double rscale = 1.0/scale;
		for (int dy = -radius; dy <= radius; dy++) {
			int y = radius + dy;
			double y_in = radius1 + dy*rscale;
			int iy0 = (int) Math.floor(y_in);
			double fy = y_in-iy0;
			int iy1 = Math.min(iy0+1, kernel_size1-1);
			for (int dx = -radius; dx <= radius; dx++) {
				int x = radius + dx;
				double x_in = radius1 + dx*rscale;
				int ix0 = (int) Math.floor(x_in);
				double fx = x_in-ix0;
				int ix1 = Math.min(ix0+1, kernel_size1-1);
				kernel[y*kernel_size + x] =
						(1-fy)*(1-fx)*kernel1[iy0*kernel_size1+ix0]+
						(1-fy)*(  fx)*kernel1[iy0*kernel_size1+ix1]+
						(  fy)*(1-fx)*kernel1[iy1*kernel_size1+ix0]+
						(  fy)*(  fx)*kernel1[iy1*kernel_size1+ix1];
			}
		}
		return kernel;
	}
	*/

	public static double [] scaleKernel(
			double       scale, // normally is exactly twice
			double [] kernel1) {
		int kernel_size1 = (int) Math.sqrt(kernel1.length);
		int radius1 = (kernel_size1 - 1)/2;
		int radius = (int) Math.floor(scale * radius1);
		int kernel_size = 2 * radius + 1;
		double [] kernel = new double [kernel_size*kernel_size];
		double rscale = 1.0/scale;
		for (int dy = -radius; dy <= radius; dy++) {
			int y = radius + dy;
			double y_in = radius1 + dy*rscale;
			int iy0 = (int) Math.floor(y_in);
			double fy = y_in-iy0;
			int iy1 = Math.min(iy0+1, kernel_size1-1);
			for (int dx = -radius; dx <= radius; dx++) {
				int x = radius + dx;
				double x_in = radius1 + dx*rscale;
				int ix0 = (int) Math.floor(x_in);
				double fx = x_in-ix0;
				int ix1 = Math.min(ix0+1, kernel_size1-1);
				kernel[y*kernel_size + x] =
						(1-fy)*(1-fx)*kernel1[iy0*kernel_size1+ix0]+
						(1-fy)*(  fx)*kernel1[iy0*kernel_size1+ix1]+
						(  fy)*(1-fx)*kernel1[iy1*kernel_size1+ix0]+
						(  fy)*(  fx)*kernel1[iy1*kernel_size1+ix1];
			}
		}
		// normalize result 
		double s = 0;
		for (int i = 0; i < kernel.length; i++) s+= kernel[i];
		System.out.println("scaleKernel(): s="+s);
		double k = 1/s;
		for (int i = 0; i < kernel.length; i++) kernel[i] *= k;
		return kernel;
	}

	
	
	
	public static double [] combineKernels(
			double [] kernel1,
			double [] kernel2) {
		int kernel_size1 = (int) Math.sqrt(kernel1.length);
		int kernel_size2 = (int) Math.sqrt(kernel2.length);
		int kernel_radius1 = (kernel_size1 - 1)/2;
		int kernel_radius2 = (kernel_size2 - 1)/2;
		int kernel_radius = kernel_radius1 + kernel_radius2;
		int kernel_size = 2*kernel_radius + 1;
		double [] kernel = new double [kernel_size * kernel_size];
		int indx_tl = kernel_radius2 * (kernel_size + 1);
		for (int y = 0; y < kernel_size1; y++) {
			System.arraycopy(
					kernel1,
					y* kernel_size1,
					kernel,
					indx_tl + y * kernel_size,
					kernel_size1);
		}
		
		kernel = convolveWithKernel(
						kernel,       // final double [] data,
						kernel2,      // final double [] kernel,
						kernel_size); // final int width);
		// normalize result 
		double s = 0;
		for (int i = 0; i < kernel.length; i++) s+= kernel[i];
		System.out.println("combineKernels(): s="+s);
		double k = 1/s;
		for (int i = 0; i < kernel.length; i++) kernel[i] *= k;
		return kernel;
		
	}
	
	
	public static double [] convolveWithKernel(
			final double [] data,
			final double [] kernel,
			final int width) {
		final int height = data.length / width;
		final double [] convolved = new double [data.length];
		Arrays.fill(convolved, Double.NaN);
		final int kernel_size = (int) Math.sqrt(kernel.length); // supposed to be odd
		final int kernel_radius = (kernel_size-1)/2; 

		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final int kernel_center= kernel_radius * (kernel_size + 1); 
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < data.length; ipix = ai.getAndIncrement()) if (!Double.isNaN(data[ipix])){
						int ix = ipix % width;
						int iy = ipix / width;
						int dx_min=-kernel_radius,dx_max=kernel_radius, dy_min=-kernel_radius,dy_max=kernel_radius;
						if (ix < kernel_radius)             dx_max = ix;
						if (iy < kernel_radius)             dy_max = iy;
						if (ix >= (width -  kernel_radius)) dx_min = ix - width + 1;
						if (iy >= (height - kernel_radius)) dy_min = iy - height + 1;
						double swd = 0, sw = 0;
						for (int dy = dy_min; dy <= dy_max; dy++) {
							for (int dx = dx_min; dx <= dx_max; dx++) {
								int src_dindex =     ipix          - dy * width       - dx;
								double d = data[src_dindex];
								if (!Double.isNaN(d)) {
									int src_kindex = kernel_center + dy * kernel_size + dx;
									double k = kernel [src_kindex];
									sw += k;
									swd += d * k;
								}
								convolved[ipix] = swd/sw;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return convolved;
	}
	
	/**
	 * Calculate absolute contrast of the pattern
	 * @param data extracted square image with the object exactly
	 *             in the center
	 * @param ipattern square pattern array corresponding to data. 1 means 
	 *                 average (w/o outliers) and add, 2 - average and subtract
	 * @param outliers_frac - fraction of outliers to remove while averaging
	 * @param obscure_frac fraction between center average and peripheral ring to compare when obscure_warm
	 *        is set.
	 * @param masked_data null or double[data.length] - will return a copy of data
	 *        with NaN for all unused for averaging 
	 * @param debug show debug images
	 * @return a pair of : difference between average in-pattern (1) and outside (2) average
	 *         values, and difference between third zone and a threshold between averages of zone1
	 *         and zone2. For full patterns (no zone3) this value is NaN. Positive values are good,
	 *         regardless the cold or warm center. For cold patterns (morning) positive means
	 *         that zone 3 minimum is warmer than fraction point between zone1 and zone2 (cold 
	 *         object obscured by something warmer, for hot - that obscurant is colder.
	 *           
	 */
	public static double [] getAbsoluteContrast(
			double [] data,
			int    [] ipattern,
			double    outliers_frac,
//			boolean   obscure_warm, // =  true; // obscured can only be by warmer objects 
			double    obscure_frac, //  0.25; // obscured threshold between center and outer 
			double [] masked_data, // null or double [data.length] to return masked data
			boolean   debug) {
		int num_segments = 3;
		if (debug) {
			int size = (int) Math.sqrt(data.length);
			ShowDoubleFloatArrays.showArrays(
					data,
					size,
					size,
					"extracted_data");
			
		}
		ArrayList<ArrayList<Integer>> lists = new ArrayList<ArrayList<Integer>>();
		for (int i = 0; i < num_segments; i++) {
			lists.add(new ArrayList<Integer>());
		}
		double [] swd = new double [num_segments],
				sw = new double [num_segments],
				avg= new double[num_segments],
				fracw=new double[num_segments];
		for (int n = 0; n < num_segments; n++) { // swd.length; n++) {
			ArrayList<Integer> list = lists.get(n);
			int n1 = n+1;
			for (int i = 0; i < ipattern.length; i++) if (ipattern[i] == n1){
				sw[n] += 1;
				swd[n]+= data[i];
				list.add(i);
			}
			avg[n] = swd[n]/sw[n];
			fracw[n] = (1.0-outliers_frac) * sw[n];
		}
		double [] zone3_min_max = {Double.NaN, Double.NaN};
		int zone3=3;
		for (int i:lists.get(zone3-1)) {
			if (Double.isNaN(zone3_min_max[0]) || (data[i] < zone3_min_max[0])) {
				zone3_min_max[0] = data[i];
			}
			if (Double.isNaN(zone3_min_max[1]) || (data[i] > zone3_min_max[1])) {
				zone3_min_max[1] = data[i];
			}
		}

		
		
		if (debug) {
			System.out.println("getAbsoluteContrast() before outliers: avg[0] ="+avg[0]+", avg[1]="+avg[1]+", avg[0]-avg[1]="+(avg[0]-avg[1]));
		}
		
		if (outliers_frac > 0) {
			for (int n = 0; n < swd.length; n++) {
				ArrayList<Integer> list = lists.get(n);
				Collections.sort(list, new Comparator<Integer>() {
					@Override
					public int compare(Integer lhs, Integer rhs) {
						double rhsd = data[rhs];
						double lhsd = data[lhs];
						return (rhsd > lhsd) ? -1 : (rhsd < lhsd) ? 1 : 0;
					}
				});
				while (!list.isEmpty() && (sw[n] >= fracw[n])) {
					int indx_first = list.get(0);
					int indx_last = list.get(list.size()-1);
					boolean remove_first = Math.abs(data[indx_first] - avg[n]) > Math.abs(data[indx_last] - avg[n]);
					int indx_worst = remove_first? indx_first : indx_last;
					double d = data[indx_worst];
					double sw_new = sw[n] - 1.0;
					avg[n] = (avg[n] * sw[n] - d)/sw_new;
					sw[n] = sw_new;
					list.remove(remove_first ? 0 : list.size()-1);
				}
			}
		}
		
		double threshold = (1.0 - obscure_frac) * avg[0] + obscure_frac * avg[1];
		double over_thresh = (avg[0] < avg[1]) ? (zone3_min_max[0] - threshold) : (threshold - zone3_min_max[1]);
		
		
		if (masked_data != null) {
			Arrays.fill(masked_data, Double.NaN);
			for (int n = 0; n < lists.size(); n++) {
				ArrayList<Integer> list = lists.get(n);
				for (Integer i: list) {
					masked_data[i] = data[i];
				}
			}
		}
		if (debug) {
			int    [] ipattern1 = new int [ipattern.length];
			for (int n = 0; n < lists.size(); n++) {
				ArrayList<Integer> list = lists.get(n);
				for (Integer i: list) {
					ipattern1[i] = n+1;
				}
			}
			int [][] ipatterns = {ipattern, ipattern1};
			double [][] dbg_img = new double [2][ipattern.length];
			for (int n = 0; n < ipatterns.length; n++) {
				for (int i= 0; i < ipatterns[n].length; i++) if (ipatterns[n][i] != 0){
					switch (ipatterns[n][i]) {
					case 1: dbg_img[n][i] = 1.0; break;
					case 2: dbg_img[n][i] = -1.0; break;
					case 3: dbg_img[n][i] = -2.0; break;
					}
				}
			}
			int size = (int) Math.sqrt(ipattern.length);
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					size,
					size,
					true,
					"integer_patterns_before_after");
		}
		if (debug) {
			System.out.println("getAbsoluteContrast() after outliers: avg[0] ="+avg[0]+", avg[1]="+avg[1]+", avg[0]-avg[1]="+(avg[0]-avg[1]));
		}
		return new double [] {avg[0]-avg[1], over_thresh};
	}
	
	/**
	 * From existing pattern (only for simple dark main scene)
	 * create two zones to calculate average (w/o outliers) inside
	 * the pattern and around it. Mark inner with 1, outer - 2,
	 * 3 - potential obscurant for partial patterns
	 * keep other 0. Then later find 2 averages (removing outliers) and their
	 * difference - absolute contrast.
	 * @param patterns set of square patterns, first [0] for full pattern,
	 *                 others - for half-patterns cut in different directions. 
	 * @param edge_frac consider in-pattern if normalized value (normalized by
	 *                  the value with maximal absolute value) is above
	 *                  1-edge_frac, completely out if it is below edge_frac.
	 * @param oversize scale patterns by this value to create outer area (marked 2)
	 * @param debug  show debug images
	 * @return integer array corresponding to the original patterns. 1 means
	 *         "in-patter" (average w/o outliers and add), 2 means "around pattern"-
	 *         average w/o outliers and subtract.
	 */
	public static int[][] getIntPatterns(
			double [][] patterns,
			double edge_frac, // 0.15
			double oversize,
			boolean debug) {
		double low_thresh = edge_frac;
		double high_thresh = 1.0 - edge_frac;
		int size = (int) Math.sqrt(patterns[0].length);
		double xc = size/2;
		double yc = size/2;
		int [][] ipatterns = new int [patterns.length][patterns[0].length];
		double amax = 0;
		double absmax = 0;
		for (int i = 0; i < patterns[0].length; i++) {
			double d = patterns[0][i];
			double ad = Math.abs(d);
			if (ad > absmax) {
				absmax = ad;
				amax = d;
			}
		}
		double scale = 1.0/amax;
		double [][] npatterns = new double [patterns.length][patterns[0].length];
		for (int n = 0; n < patterns.length; n++) {
			for (int i = 0; i < patterns[n].length; i++) {
				npatterns[n][i] = patterns[n][i] * scale;
			}
		}
		// create full pattern first
		int num_1 = 0, num_2=0;
		for (int y=0; y < size; y++) {
			double ys= yc+ (y-yc)/oversize;
			for (int x=0; x < size; x++) {
				int indx = y*size+x;
				if (npatterns[0][indx] > high_thresh) {
					ipatterns[0][indx] = 1;
					num_1++;
				} else if (npatterns[0][indx] <= low_thresh) {// interpolate scaled version, between - keep zero
					double xs = xc+ (x-xc)/oversize;
					// interpolate
					int iys = (int) Math.floor(ys);
					double fys = ys-iys;
					int ixs = (int) Math.floor(xs);
					double fxs = xs-ixs;
					int sindx = iys*size+ixs;
					double sd0 = 
							(1-fys)*(1-fxs)*npatterns[0][sindx] + 
							(1-fys)*(  fxs)*npatterns[0][sindx+1] + 
							(  fys)*(1-fxs)*npatterns[0][sindx+size] + 
							(  fys)*(  fxs)*npatterns[0][sindx+size+1]; 
					if (sd0 > high_thresh) {
						ipatterns[0][indx] = 2;
						num_2++;
					}
				}
			}
		}
		if ((num_1 == 0) || (num_2==0)) {
			System.out.println("getIntPatterns(): wrong pattern or parameters: num_1="+num_1+", num2="+num_2);
			return null;
		}
		// Other patterns
		for (int n = 1; n < npatterns.length; n++) {
			for (int y=0; y < size; y++) {
				double ys= yc+ (y-yc)/oversize;
				for (int x=0; x < size; x++) {
					int indx = y*size+x;
					if (ipatterns[0][indx] == 1) {
						if (npatterns[n][indx]/npatterns[0][indx] > 0.5) {
							ipatterns[n][indx] = ipatterns[0][indx]; //= 1
						}
					} else if (ipatterns[0][indx] == 2) {
						double xs = xc+ (x-xc)/oversize;
						// interpolate
						int iys = (int) Math.floor(ys);
						double fys = ys-iys;
						int ixs = (int) Math.floor(xs);
						double fxs = xs-ixs;
						int sindx = iys*size+ixs;
						double sd0 = 
								(1-fys)*(1-fxs)*npatterns[0][sindx] + 
								(1-fys)*(  fxs)*npatterns[0][sindx+1] + 
								(  fys)*(1-fxs)*npatterns[0][sindx+size] + 
								(  fys)*(  fxs)*npatterns[0][sindx+size+1]; 
						
						double sdn = 
								(1-fys)*(1-fxs)*npatterns[n][sindx] + 
								(1-fys)*(  fxs)*npatterns[n][sindx+1] + 
								(  fys)*(1-fxs)*npatterns[n][sindx+size] + 
								(  fys)*(  fxs)*npatterns[n][sindx+size+1]; 
						if (sdn/sd0 > 0.5) {
							ipatterns[n][indx] = ipatterns[0][indx]; // =2
						} else {
							ipatterns[n][indx] = 3; // =2
						}
					}
				}
			}
		}
		if (debug) {
			ShowDoubleFloatArrays.showArrays(
					npatterns,
					size,
					size,
					true,
					"normalized_patterns");
			double [][] dbg_img = new double [ipatterns.length][ipatterns[0].length];
			for (int n = 0; n < ipatterns.length; n++) {
				for (int i= 0; i < ipatterns[n].length; i++) if (ipatterns[n][i] != 0){
					switch (ipatterns[n][i]) {
					case 1: dbg_img[n][i] =  1.0; break;
					case 2: dbg_img[n][i] = -1.0; break;
					case 3: dbg_img[n][i] = -2.0; break;
					}
				}
			}
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					size,
					size,
					true,
					"integer_patterns");
		}
		return ipatterns;
	}
	
	
	
	public static double [] extractKernel(
			double [] data,
			int       kernel_radius,
			boolean   hor_sym,
			boolean   vert_sym,
			boolean   all_sym,
			int       debugLevel) {
		hor_sym |= all_sym;
		vert_sym |= all_sym;
		double [][] dbg_img = (debugLevel > -4)? new double [4][]: null;
		int kernel_size = 2*kernel_radius + 1;
		int fft_size = (int) Math.sqrt(data.length);
		double [] kernel = new double [kernel_size*kernel_size];
		int data_tl = (fft_size/2 - kernel_radius) * ( fft_size + 1);
		for (int row = 0; row < kernel_size; row++) {
			System.arraycopy(
					data,
					data_tl + row * fft_size,
					kernel,
					row* kernel_size,
					kernel_size);
		}
		double sum = 0;
		for (int i = 0; i < kernel.length; i++) {
			sum += kernel[i];
		}
		double s = 1.0/sum;
		for (int i = 0; i < kernel.length; i++) {
			kernel[i] *= s;
		}
		if (dbg_img != null) dbg_img[0] = kernel.clone();
		if (hor_sym) {
			for (int row = 0; row < kernel_size; row++) {
				for (int col = 1; col <= kernel_radius; col++) {
					int indx = row*kernel_size + kernel_radius; // center of the line
					double d = 0.5*(kernel[indx-col] + kernel[indx+col]);
					kernel[indx-col] = d;
					kernel[indx+col] = d;
				}
			}
		}
		if (dbg_img != null) dbg_img[1] = kernel.clone();
		if (vert_sym) {
			for (int row = 1; row <= kernel_radius; row++) {
				int indx0= (kernel_radius-row)*kernel_size;
				int indx1= (kernel_radius+row)*kernel_size;
				for (int col = 0; col < kernel_size; col++) {
					double d = 0.5*(kernel[indx0+col] + kernel[indx1+col]);
					kernel[indx0+col] = d;
					kernel[indx1+col] = d;
				}
			}
		}
		if (dbg_img != null) dbg_img[2] = kernel.clone();
		if (all_sym) {
			for (int row = 0; row < (kernel_size-1); row++) {
				for (int col = row+1; col < kernel_size; col++) {
					int indx0 = row * kernel_size + col;
					int indx1 = col * kernel_size + row;
					double d = 0.5*(kernel[indx0] + kernel[indx1]);
					kernel[indx0] = d;
					kernel[indx1] = d;
				}
			}
		}
		if (dbg_img != null) dbg_img[3]= kernel.clone();
		if (dbg_img != null) {
			String [] dbg_titles= {"orig","hor","vert","all"};
			ShowDoubleFloatArrays.showArrays(
					dbg_img,
					kernel_size,
					kernel_size,
					true,
					"kernel",
					dbg_titles);
		}
		return kernel;
	}
	
	
	public static double [] deconvolvePair(
			double [][] data,
			double fat_zero, // 1000
			int debugLevel) {
		boolean zero_phase=true;
		int fft_size = (int) Math.sqrt(data[0].length);
		DoubleFHT doubleFHT = new DoubleFHT();
		doubleFHT.updateMaxN(data[1]);
		double [] w1d = doubleFHT.getHamming1d(fft_size);
		for (int n= 0; n < data.length; n++) {
			removeDC(data[n]);
			multiplySquareBy1D(data[n], w1d);
			removeDC(data[n]); // once more?
		}
		double [][] data_bkp = null;
		if (debugLevel > 0) {
			data_bkp = new double[][] {data[0].clone(),data[1].clone()};
		}
		doubleFHT.swapQuadrants(data[0]);
		doubleFHT.swapQuadrants(data[1]);
		if (!doubleFHT.transform(data[1],false))  return null; // direct FHT
		if (!doubleFHT.transform(data[0],false)) return null; // direct FHT {
		if (debugLevel > 0) {
			ShowDoubleFloatArrays.showArrays( // here every element
					data[1],
					fft_size,
					fft_size,
					"data[1]");
		}

		if (debugLevel > 0) {
			double [] amp = doubleFHT.calculateAmplitude(data[0]);
			ShowDoubleFloatArrays.showArrays(
					amp,
					fft_size,
					fft_size,
					"amp");
		}
		double [] deconv = doubleFHT.divide(data[1],data[0], fat_zero);
		if (debugLevel > 0) {
			ShowDoubleFloatArrays.showArrays(
					deconv,
					fft_size,
					fft_size,
					"deconv");
		}
//		if (zero_phase) {
			double [] div_amp = doubleFHT.calculateAmplitudeNoSwap(deconv);
			double [] div_phase = DoubleFHT.calculatePhaseNoSwap(deconv);
			double [][] amp_phase = {div_amp, div_phase};
			ShowDoubleFloatArrays.showArrays(
					amp_phase,
					fft_size,
					fft_size,
					true,
					"deconvolved-amp-phase",
					new String[] {"amp","phase"});
			/*
			if (debugLevel > 0) {
				ShowDoubleFloatArrays.showArrays(
						div_amp,
						fft_size,
						fft_size,
						"div_amp");
			}
			deconv = doubleFHT.setReal(div_amp);
			if (debugLevel > 0) {
				ShowDoubleFloatArrays.showArrays(
						deconv,
						fft_size,
						fft_size,
						"deconv_real");
			}
			*/
			
//		}
		
		// try with zero phase
		
		
        //if (filter!=null) multiplyByReal(first, filter); // add filter if needed
		doubleFHT.transform(deconv,true) ; // inverse transform
		doubleFHT.swapQuadrants(deconv);
		if (debugLevel > 0) {
			data[0]= data_bkp[0]; 
			data[1]= data_bkp[1]; 
		}
		return deconv;
	}
	

	
	
	public static void removeDC(
			final double [] data) {
		 final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		 final AtomicInteger ai = new AtomicInteger(0);
		 final AtomicInteger ati = new AtomicInteger(0);
		 final double [] avg_arr = new double [threads.length];
		 final double [] npix_arr = new double [threads.length];
		 for (int ithread = 0; ithread < threads.length; ithread++) {
			 threads[ithread] = new Thread() {
				 public void run() {
					 int thread_num = ati.getAndIncrement();
					 for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) if (!Double.isNaN(data[iPix])){
						 avg_arr[thread_num] += data[iPix];
						 npix_arr[thread_num] += 1.0;
					 }
				 }
			 };
		 }		      
		 ImageDtt.startAndJoin(threads);
		 double avg=0, num=0;
		 for (int i = 0; i < avg_arr.length; i++) {
			 avg+=avg_arr[i];
			 num+=npix_arr[i];
		 }
		 final double favg = avg/num;
		 ai.set(0);
		 for (int ithread = 0; ithread < threads.length; ithread++) {
			 threads[ithread] = new Thread() {
				 public void run() {
					 for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()){
						 data[iPix] -= favg;
					 }
				 }
			 };
		 }		      
		 ImageDtt.startAndJoin(threads);
	}
	
	public static void multiplySquareBy1D(
			final double [] data,
			final double[] wnd1d) {
		final int width = wnd1d.length;
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		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 < data.length; iPix = ai.getAndIncrement()) if (!Double.isNaN(data[iPix])){
						int x = iPix % width;
						int y = iPix / width;
						data[iPix] *= wnd1d[x] * wnd1d[y];
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
	}
	
	
	public static void testVideo(ImagePlus imp) {
		String path_prefix = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/videos/video_";
		boolean   keep_original =        true;
		boolean   um_apply =             true;
		double    um_sigma =             10.0;
		double    um_weight =            0.9;
		boolean   auto_range =           false;
		double    mono_range =           300.0; 
		boolean   annotate   =           true;
		Color     annotate_color =       new Color( 255, 180,  50);
		boolean   annotate_transparent = false;
		int       annotate_font_size =   36;
		boolean   annotate_font_bold =   true;
		double    video_fps =            10;
		boolean   avi_compress_jpeg =    false;
		int       aviJpegQuality =       95;
		boolean   run_ffmpeg =           true;
		String    video_ext =            ".webm";
		String    video_codec =          "vp8";
		int       video_crf =            10; // lower - better, larger file size
		double  video_bitrate_m =        50; //  clt_parameters.imp.video_bitrate_m;
		boolean   remove_avi =           true;
		int       debugLevel =           0;
		int num_slices = imp.getImageStack().getSize();
		int first_slice =         1;
		int last_slice =          num_slices;
		boolean   create_image =  false; // instead of video
		int       border_width =  3; 
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Parameters for video from orthographic imges",1200,800);
		gd.addStringField ("Output video path prefix",  path_prefix, 180,
				"Absolute path prefix for the video (image title will be added).");
		gd.addCheckbox    ("Keep original image", keep_original, "If true, will generate new RGB image, if false - replace existent.");
		gd.addCheckbox    ("Apply unsharp mask filter", um_apply, "Apply unsharp mask filter.");
		gd.addNumericField("Unsharp mask sigma", um_sigma,  3,7,"",	"Unsharp mask sigma.");
		gd.addNumericField("Unsharp mask weight", um_weight,  3,7,"",	"Unsharp mask weight.");
		gd.addCheckbox    ("Auto range while congerting to RGB", auto_range, "Autorange 32-bit floating point image when converting to RGB.");
		gd.addNumericField("Fixed range during conversion to RGB", mono_range,  3,7,"",	"Fixed range.");
		gd.addCheckbox    ("Annotate each frame with timestamp+date/time", annotate,
				"Embed annotation in the bottom-right corner of each frame of the converted image file and video.");
		{
			String scolor = (annotate_color==null)?"none":String.format("%08x", IntersceneMatchParameters.getLongColor(annotate_color));
			gd.addStringField ("Annotation color (hex number)",scolor, 8, "Any invalid hex number disables annotation");
		}
		gd.addCheckbox    ("Annotation transparent background", annotate_transparent, "Put annotation text directly over images.");
		gd.addNumericField("Annotation font size", annotate_font_size, 0, 4, "", "Annotation font size.");
		gd.addCheckbox    ("Bold annotation", annotate_font_bold, "Use bold font for annotations.");
		gd.addNumericField("Video output frame rate", video_fps,  3,7,"fps", "Frame rate of the video.");
		gd.addCheckbox    ("Compress AVI with JPEG", avi_compress_jpeg, "Use JPEG for AVI compression (false - use raw).");
		gd.addNumericField("AVI JPEG quality", aviJpegQuality, 0, 4, "", "AVI JPEG quality if JPEG compression is used.");
		gd.addCheckbox    ("Convert AVI to WEBM", run_ffmpeg, "Use ffmpeg to convert intermediate AVI video to WEBM.");
		gd.addStringField ("WEBM output extension",  video_ext, 5,"WEBM output file extension including dot, normally \".webm\".");
		gd.addStringField ("WEBM codec",  video_codec, 5,"WEBM codec \"vp8\" or \"vp9\"(vp9 had problems).");
		gd.addNumericField("WEBM CRF", video_crf, 0, 4, "", "WEBM compression quality (lower - better, 10 - good).");
		gd.addCheckbox    ("Remove AVI", remove_avi, "Remove AVI (large file) after WEBM generation.");
		gd.addNumericField("Debug level", debugLevel, 0, 4, "", "Debug level (not yet used).");
		
		gd.addMessage("--- parameters for single image generation ---");
		gd.addCheckbox    ("Create image instead of video", create_image, "Create image instead of video.");
		gd.addNumericField("First slice", first_slice, 0, 4, "", "First slice number (starts with 1)");
		gd.addNumericField("Last slice", last_slice, 0, 4, "", "Last slice to combine.");
		gd.addNumericField("Border width", border_width, 0, 4, "pix", "Uses \"Annotation color\".");
		gd.showDialog();
		if (gd.wasCanceled()) return;
		path_prefix =          gd.getNextString();
		keep_original =        gd.getNextBoolean();
		um_apply =             gd.getNextBoolean();
		um_sigma=              gd.getNextNumber();
		um_weight=             gd.getNextNumber();
		auto_range =           gd.getNextBoolean();
		mono_range =           gd.getNextNumber();
		annotate =             gd.getNextBoolean();
        {
			String scolor =             gd.getNextString();
			long lcolor = -1;
			try {
				lcolor = Long.parseLong(scolor,16);
				annotate_color = IntersceneMatchParameters.setLongColor(lcolor);
			} catch(NumberFormatException e){
				annotate_color = null;
			}
        }
		annotate_transparent =     gd.getNextBoolean();
		annotate_font_size = (int) gd.getNextNumber();		
		annotate_font_bold =       gd.getNextBoolean();
		video_fps =                gd.getNextNumber();
		avi_compress_jpeg =        gd.getNextBoolean();
		aviJpegQuality =     (int) gd.getNextNumber();
		run_ffmpeg =               gd.getNextBoolean();
		video_ext =                gd.getNextString();
		video_codec =              gd.getNextString();
		video_crf =          (int) gd.getNextNumber();
		remove_avi =               gd.getNextBoolean();
		debugLevel =         (int) gd.getNextNumber();
		create_image=gd.getNextBoolean();
		first_slice =        (int) gd.getNextNumber();
		last_slice =         (int) gd.getNextNumber();
		border_width =       (int) gd.getNextNumber();
		
		if (create_image) {
			keep_original = false; // not used
		}
		ImagePlus imp_um;
		if (um_apply) {
			imp_um = applyUnsharpMask(
					imp,           // final ImagePlus imp,
					keep_original, // final boolean   keep_original,
					um_sigma,      // 10,            // final double    um_sigma,
					um_weight);    // 0.9);          // final double    um_weight
		} else {
			imp_um = keep_original? imp.duplicate():imp;
		}
        System.out.println("applyUnsharpMask() DONE!");	
        if (create_image) {
        	ImagePlus imp_rgb_single =  convertToColorAndCombine(
        			imp_um,         // ImagePlus imp_src,
        			first_slice,    // int       first_slice,
        			last_slice,     // int       last_slice,
        			border_width,   // int       border_width,
        			mono_range,     // double    mono_range,  // >0 - apply 
        			annotate_color, // Color     border_color,
        			debugLevel);    // int       debugLevel);
        	imp_rgb_single.show();
        	System.out.println("convertToColorAndCombine() DONE!");
        	return;
        }
        
    	ImagePlus imp_rgb = convertToColorAnnotate(
    			imp_um,               // ImagePlus imp_src,
    			false,                // boolean   keep_original,
    			auto_range? 0:mono_range, // 300,  // double    mono_range,  // >0 - apply 
    			annotate,             // boolean   annotate,
    			annotate_color,       // Color.RED,  //Color    annotate_color,
    			annotate_transparent, // boolean   annotate_transparent,
    			annotate_font_size,   // int       annotate_font_size, // 12
    			annotate_font_bold,   // boolean   annotate_font_bold,
    			debugLevel);          // int       debugLevel)
    	imp_rgb.show();
        System.out.println("convertToColorAnnotate() DONE!");
        String path = path_prefix+imp_rgb.getTitle();
        int mode_avi = avi_compress_jpeg? 1 : 0;
        saveRGBSlicesAsVideo(
        		imp_rgb,        // ImagePlus  imp,
//    			0,              // int        col_mode, // 0 - mono, 1 - color
    			path,           // String     path,
    			video_fps,      // double     video_fps,
    			mode_avi,       // int        mode_avi, // 0 - raw, 1 - JPEG, 2 - PNG
    			aviJpegQuality, // int        aviJpegQuality,
    			run_ffmpeg,     // boolean    run_ffmpeg,
    			video_ext,      // ".webm",   // String     video_ext,
    			video_codec,    // "vp8",     // String     video_codec,
    			video_crf,      // 10,        // int        video_crf, // lower - better, larger file size
    			video_bitrate_m, // double        video_bitrate_m,
    			remove_avi,     // boolean    remove_avi,
    			debugLevel);    // final int  debugLevel
    			        
        System.out.println("saveRGBSlicesAsVideo() DONE!");
	}
	
	
	public static ImagePlus applyUnsharpMask(
			final ImagePlus imp_src,
			final boolean   keep_original, 
			final double    um_sigma,
			final double    um_weight
			) {
		final float fum_weight=(float) um_weight;
		final ImagePlus imp = keep_original? imp_src.duplicate() : imp_src;
		final ImageStack fstack_scenes = imp.getStack();
		final int nSlices = fstack_scenes.getSize();
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < nSlices; nSlice = ai.getAndIncrement()) {
						FloatProcessor fp = (FloatProcessor) fstack_scenes.getProcessor(nSlice+1);
						float [] fpixels = (float[]) fstack_scenes.getPixels(nSlice+1);
						float [] fpixels_orig = fpixels.clone();
						(new GaussianBlur()).blurFloat(
								fp,       // FloatProcessor ip,
								um_sigma, // double sigmaX,
								um_sigma, // double sigmaY,
								0.01);    // double accuracy)
						for (int i = 0; i < fpixels.length; i++) {
							fpixels[i] = fpixels_orig[i] - fum_weight * fpixels[i];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		imp.setTitle(removeKnownExtension(imp.getTitle())+String.format("-UM%.1f_%.3f",um_sigma,um_weight));
		return imp;
	}
	
	public static ImagePlus convertToColorAnnotate(
			ImagePlus imp_src,
			boolean   keep_original,
			double    mono_range,  // >0 - apply 
			boolean   annotate,
			Color     annotate_color,
			boolean   annotate_transparent,
			int       annotate_font_size, // 12
			boolean   annotate_font_bold,
			int       debugLevel) {
		final ImagePlus imp = keep_original? imp_src.duplicate() : imp_src;
		if (imp.getType() != ImagePlus.COLOR_RGB) {
			if (mono_range > 0) {
				imp.getProcessor().setMinAndMax(-mono_range/2, mono_range/2);
			}
			ImageConverter imageConverter = new ImageConverter(imp);
			imageConverter.convertToRGB();
		}
		final Color fcolor = annotate_color;
		final ImageStack fstack_scenes = imp.getImageStack();
		int title_len = 0;
		final int nSlices = fstack_scenes.getSize();
		for (int i = 0; i < nSlices; i++) {
			if (fstack_scenes.getSliceLabel(i+1).length() > title_len) {
				title_len=fstack_scenes.getSliceLabel(i+1).length();
			}
		}
		final int annotate_size = title_len; //
		final int width =  imp_src.getWidth();
		final int height = imp_src.getHeight();
		final double char_wtoh = 0.6;
		final int posX= width - ((int) (char_wtoh* annotate_size * annotate_font_size)); //  119; // 521;
		final int posY= height + 1;  // 513;
		final Font font = new Font("Monospaced", Font.PLAIN | (annotate_font_bold ? Font.BOLD: 0), annotate_font_size);
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < nSlices; nSlice = ai.getAndIncrement()) {
						String scene_title = fstack_scenes.getSliceLabel(nSlice+1);
						ImageProcessor ip = fstack_scenes.getProcessor(nSlice+1);
						ip.setColor(fcolor); // Color.BLUE);
						ip.setFont(font);
						if (!annotate_transparent) { // toRGB || !annotate_transparent_mono) {
							ip.drawString(scene_title, posX, posY,Color.BLACK);
						} else {
							ip.drawString(scene_title, posX, posY); // transparent
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		imp.setTitle(removeKnownExtension(imp.getTitle())+((mono_range > 0) ? String.format("_%.0f",mono_range):"_A"));
		return imp;
	}
	
	public static ImagePlus convertToColorAndCombine(
			ImagePlus imp_src,
			int       first_slice,
			int       last_slice,
			int       border_width,
			double    mono_range,  // >0 - apply 
			Color     border_color,
			int       debugLevel) {
		int width =  imp_src.getWidth();
		int height = imp_src.getHeight();
		int [] pixels = new int [width* height];
		ImageStack stack = imp_src.getStack();
		boolean [] mask = new boolean[pixels.length];
		final TileNeibs tnSurface = new TileNeibs(width, height);

		for (int nslice = first_slice; nslice <= last_slice; nslice++) {
			float [] fpixels = (float[]) stack.getPixels(nslice);
			Arrays.fill(mask, false);
			for (int i = 0; i <fpixels.length; i++) if (!Float.isNaN(fpixels[i])) {
				mask[i]= true;
				int v255 = (int) Math.round(255*(fpixels[i] + mono_range/2)/mono_range);
				if (v255 < 0) v255= 0;
				else if (v255 > 255) {
					v255 = 255;
				}
				pixels[i] = v255 * 0x10101;
			}
			if (border_width > 0) {
				tnSurface.growSelection(
						border_width, // grow,
						mask, // tiles,
						null); // prohibit);
				int border_RGB = border_color.getRGB();
				for (int i = 0; i <fpixels.length; i++) if (Float.isNaN(fpixels[i]) && mask[i]) {
					pixels[i]=border_RGB;
				}
			}
			if (debugLevel >=0) {
				System.out.println("Slice "+nslice+" is done. Last is "+last_slice);
			}
		}
		
		ColorProcessor cp=new ColorProcessor(width,height);
		cp.setPixels(pixels);
		ImagePlus imp_combo=new ImagePlus(
				removeKnownExtension(imp_src.getTitle())+"-collage_"+first_slice+"-"+last_slice,cp);
		return imp_combo;
	}
	
	
	public static String removeKnownExtension(String path) {
		String [] remove_ext = {".tiff", ".tif", ".avi"};
		for (String ext:remove_ext) {
			if (path.endsWith(ext)) {
				path = path.substring(0,path.length()-ext.length());
			}
		}
		return path;

	}
	public static boolean saveRGBSlicesAsVideo(
			ImagePlus  imp,
//			int        col_mode, // 0 - mono, 1 - color
			String     path,
			double     video_fps,
			int        mode_avi, // compression
			int        aviJpegQuality,
			boolean    run_ffmpeg,
			String     video_ext,
			String     video_codec,
			int        video_crf,
			double     video_bitrate_m,
			boolean    remove_avi,
			final int  debugLevel
			) {
		boolean generate_mapped = true; // false - here just simulate
		// no_combine, stereo_2_images, stereo_anaglyth
		String avi_path = removeKnownExtension(path)+String.format("-FPS%04.1f",video_fps)+".avi";
		imp.getCalibration().fps = video_fps;
		video:
		{
			try {
				(new AVI_Writer()).writeImage (
						imp, // ImagePlus imp,
						avi_path, // String path,
						mode_avi, // int compression,
						aviJpegQuality); //int jpegQuality)
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				break video;
			}
			System.out.println("saveAVIInModelDirectory(): saved "+avi_path);
			if (!run_ffmpeg) {
				break video; // webm not requested
			}

			String webm_path = avi_path.substring(0, avi_path.length()-4)+video_ext;
			// added -y not to as "overwrite y/n?"
			//ffmpeg -i input_file.mkv -c copy -metadata:s:v:0 stereo_mode=1 output_file.mkv 
			//https://ffmpeg.org/ffmpeg-formats.html
			//ffmpeg -i sample_left_right_clip.mpg -an -c:v libvpx -metadata stereo_mode=left_right -y stereo_clip.webm
			//anaglyph_cyan_red 
			String stereo_meta = ""; // used in OpticalFlow
			String shellCommand = String.format("ffmpeg -y -i %s -c %s -b:v %fM -crf %d %s %s",
					avi_path, video_codec, video_bitrate_m, video_crf, stereo_meta, webm_path);
			Process p = null;
			if (generate_mapped) {
				int exit_code = -1;
				try {
					p = Runtime.getRuntime().exec(shellCommand);
				} catch (IOException e) {
					System.out.println("Failed shell command: \""+shellCommand+"\"");
				}
				if (p != null) {
					try {
						p.waitFor();
					} catch (InterruptedException e) {
						System.out.println("saveRGBSlicesAsVideo(): failed waiting for process to finish.");
						// TODO Auto-generated catch block
						e.printStackTrace();
						return false;
					}
					exit_code = p.exitValue();
				}
				System.out.println("Ran shell command: \""+shellCommand+"\" -> "+exit_code);
				// Check if webm file exists
				if ((exit_code != 0) || !(new File(webm_path)).exists()) {
					System.out.println("Failed to create : \""+webm_path+"\"");
					break video;
				}
			} else {
				System.out.println("Simulated shell command: \""+shellCommand);
			}
			if (remove_avi && generate_mapped) {
				(new File(avi_path)).delete();
				System.out.println("Deleted AVI video file: \""+avi_path+"\"");
			}
		}
		return true;
	}
	
	public static double [][] combineAffine(
			double [][] ref_affine,
			double [][] other_affine){
		Matrix m_ref =   new Matrix(ref_affine);
		Matrix m_other = new Matrix(other_affine);
		Matrix A1 =  m_ref.getMatrix(0,1,0,1);
		Matrix A2 =m_other.getMatrix(0,1,0,1);
		Matrix B1 =  m_ref.getMatrix(0,1,2,2);
		Matrix B2 =m_other.getMatrix(0,1,2,2);
		Matrix A = A2.times(A1);
		Matrix B = A2.times(B2).plus(B1);
		double [][] affine = {
				{A.get(0,0),A.get(0,1),B.get(0,0)}, 
				{A.get(1,0),A.get(1,1),B.get(1,0)}};
		return affine;
	}
	
	/**
	 * Get planar approximation of the ground
	 * 
	 * @param zoom_lev      zoom level of the provided elevation data (0- 1pix=1 cm, -1: 1 pix=2cm)
	 * @param elev          elevation data, meters ASL
	 * @param width         pixel width corresponding to the elev[] array
	 * @param initial_above meters above average to ignore for the histogram.
	 * @param initial_below meters below average (positive value) to ignore for the histogram
	 * @param num_refine    number of the plane generation refine passes
	 * @param frac_above    remove high fraction of all tiles
	 * @param frac_below    remove low fraction of all tiles
	 * @param tile_plane_elev null or double[2][] will return per-tile elevations
	 * @param debug_title   Name of the debug image or null to skip
	 * 
	 * @return plane parameters - all metric, relative to the vertical point:
	 *         {tilt_x (m/m), tilt_y (m/m), offset (elevation at vertical point in meters)      
	 */
	public double [] getPlaneMeters (
			int       zoom_lev, // of the data (3 levels down for tiles)
			double [] elev,
			int       width,
			double    initial_above, // from average
			double    initial_below, // from average, // positive value
			int       num_refine,
			double    frac_above,
			double    frac_below,
			double [] tile_plane_elev, // null or double[2][] will return per-tile elevations
			String    debug_title) {
		final int     num_bins = 1024;
		FloatImageData alt = getAlt();
		int orig_zoom_level = alt.getZoomLevel();
		double orig_pix_meters = alt.getPixMeters();
		double [] vert_meters = alt.getVertMeters();
		int diff_zoom_lev = zoom_lev - orig_zoom_level;
		double pix_size = orig_pix_meters / getScale(diff_zoom_lev); // zoom_lev negative, pixel larger
		double [] pix_vertical = {vert_meters[0]/pix_size,vert_meters[1]/pix_size}; // image center in pixels

		double avg_all = getMaskedAverage (
				elev, // final double [] data,
				null, // final boolean [] mask)
				null); // final double []  stdp);
		double abs_high= avg_all + initial_above;
		double abs_low=  avg_all - initial_below;
		boolean [] mask = 	removeAbsoluteLowHigh (
				elev,     // final double [] data,
				null,     // final boolean [] mask,
				abs_high, // final double threshold_high,
				abs_low); // final double threshold_low),
		double [] plane_pix = {0,0,avg_all,pix_vertical[0], pix_vertical[1]}; // 5 elements here
		for (int ntry = 0; ntry < num_refine; ntry++) {
			if (ntry > 0) {
				// remove relatives, start from new mask
				mask = removeRelativeLowHigh (
						elev,       // final double [] data,
						null,       // final boolean [] mask_in,
						initial_above,   //  final double abs_high,
						-initial_below,    // final double abs_low,
						frac_above, // final double rhigh,
						frac_below, // final double rlow,
						plane_pix,  // final double []  ground_plane, // tiltx,tilty, offs, x0(pix), y0(pix) or null
						width,      // final int     width, // only used with ground_plane != null;
						num_bins);  // final int    num_bins) 				
				
			}
			plane_pix=  getPlane(
					elev, // final double []   data,
					mask , // final boolean [] mask,
					null, // final double []  weight,
					width, // final int        width,
					pix_vertical); // final double []  xy0)
		}
		if (tile_plane_elev != null) {
			for (int pix = 0; pix < elev.length; pix++) {
				int px = pix % width;
				int py = pix / width;
				double x = px - plane_pix[3];
				double y = py - plane_pix[4];
				double plane_elev = plane_pix[2]+ x *plane_pix[0] +y * plane_pix[1];
				tile_plane_elev[pix] = elev [pix] - plane_elev; 
			}
		}
		if (debug_title != null) {
			String [] dbg_titles = {"elev", "plane", "diff"};
			double [][] dbg_img = new double [dbg_titles.length][elev.length];
			dbg_img[0] = elev;
			for (int pix = 0; pix < elev.length; pix++) {
				int px = pix % width;
				int py = pix / width;
				double x = px - plane_pix[3];
				double y = py - plane_pix[4];
				dbg_img[1][pix] = plane_pix[2]+ x *plane_pix[0] +y * plane_pix[1];
				dbg_img[2][pix] = dbg_img[0] [pix] - dbg_img[1][pix]; 
			}
    		ShowDoubleFloatArrays.showArrays(
    				dbg_img,
    				width,
    				elev.length / width,
    				true,
    				debug_title,
    				dbg_titles);
		}
		return new double [] {plane_pix[0]/pix_size, plane_pix[1]/pix_size, plane_pix[2]}; // no xy0
	}
	
	
	/**
	 * Convert float[] elevation map to double[], optionally scale resolution
	 * down by integer value without interpolation
	 * @param data       original float elevations ASL
	 * @param downscale  integer downscale value
	 * @param orig_width original image width in pixels
	 * @return converted to double [] and optionally scaled down data (width
	 *         and height scaled by integer division)
	 */
	public static double [] scaleElevationResolution(
			final float [] data,
			final int      downscale,
			final int      orig_width) {
		final int orig_height = data.length/orig_width;
		final int width = orig_width / downscale; // floor
		final int height = orig_height / downscale; // floor
		final double [] data_out = new double [width*height];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		if (downscale == 1) { // fast, just copy
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) {
							data_out[iPix] = data[iPix];
						}
					}
				};
			}		      
		} else {
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) {
							int px = iPix % width;
							int py = iPix / width;
							data_out[iPix] = data[px + py*orig_width];
						}
					}
				};
			}		      
		}
		ImageDtt.startAndJoin(threads);
		return data_out;
	}
	/**
	 * Calculate average of a double array, ignore masked out (!mask[i] and NaNs in the data)
	 * @param data double data array
	 * @param mask optional (may be null) boolean mask - ignore data items that have corresponding mask element false
	 * @param stdp null or double[1] standard deviation
	 * @return average value of the non-NaN and not disabled by mask elements of data[] 
	 */
	public static double getMaskedAverage (
			final double []  data,
			final boolean [] mask,
			final double []  stdp) {
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [] avg_arr = new double [threads.length];
		final double [] std_arr = new double [threads.length];
		final double [] npix_arr = new double [threads.length];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) if (!Double.isNaN(data[iPix]) && ((mask == null) || (mask[iPix]))){
						avg_arr[thread_num] += data[iPix];
						std_arr[thread_num] += data[iPix]*data[iPix];
						npix_arr[thread_num] += 1.0;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double avg=0, num=0,sum2=0;
		for (int i = 0; i < avg_arr.length; i++) {
			avg+=avg_arr[i];
			num+=npix_arr[i];
			sum2+=std_arr[i];
		}
		avg /= num;
		sum2 /= num;
		if (stdp != null) {
			stdp[0] = Math.sqrt(sum2- avg*avg);
		}
		return avg;
	}

	/**
	 * Calculate average of a float array, ignore masked out (!mask[i] and NaNs in the data)
	 * @param data double data array
	 * @param mask optional (may be null) boolean mask - ignore data items that have corresponding mask element false
	 * @return average value of the non-NaN and not disabled by mask elements of data[] 
	 */

	private static double getMaskedAverage (
			final float []   data,
			final boolean [] mask) {
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final double [] avg_arr = new double [threads.length];
		final double [] npix_arr = new double [threads.length];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int iPix = ai.getAndIncrement(); iPix < data.length; iPix = ai.getAndIncrement()) if (!Double.isNaN(data[iPix]) && ((mask == null) || (mask[iPix]))){
						avg_arr[thread_num] += data[iPix];
						npix_arr[thread_num] += 1.0;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		double avg=0, num=0;
		for (int i = 0; i < avg_arr.length; i++) {
			avg+=avg_arr[i];
			num+=npix_arr[i];
		}
		return avg/num;
	}
	
	
	/**
	 * Remove double [] data elements that are lower than threshold_low or greater than threshold_high, ignore masked out and NaN elements
	 * @param data double [] input data
	 * @param mask optional (may be null) mask for the data array. Will be modified if provided
	 * @param threshold_high remove higher elements () and NaNs in the data
	 * @param threshold_low remove lower elements () and NaNs in the data
	 * @return updated mask (same instance if not null) that disables (makes false) filtered out data [] elements
	 */
	public static boolean [] removeAbsoluteLowHigh (
			final double [] data,
			final boolean [] mask,
			final double threshold_high,
			final double threshold_low) {
		final boolean [] mask_out = (mask == null) ? new boolean [data.length] : mask;
		if (mask == null) {
			Arrays.fill(mask_out, true);
		}
		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 < data.length; iPix = ai.getAndIncrement()){
						double d = data[iPix];
						if (!((d >= threshold_low) && (d <= threshold_high))) { // removes NaNs too
							mask_out[iPix] = false;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return mask_out;
	}

	/**
	 * Create mask to disable tiles that have elevations significantly deviating from a
	 * flat surface and from the vertical point, so the vertical deviations may cause horizontal
	 * matching errors.
	 * @param zoom_lev zoom level of the elevation data (0- 1pix=1cm, -1 - 1pix = 2cm, etc.)
	 * @param elev         array (decimated) of metric elevations ASL
	 * @param width        width in pixels represented by elev array.
	 * @param plane_metric metric plane parameters - {tilt_x (m/m), tilt_y (m/m), offset at vertical point(m)}
	 * @param metric_error maximal allowed metric error (mask tiles that may potentially have larger error).
	 * @return boolean [] mask of the same size as elev, where false disables tiles
	 */
	public boolean [] removeHighElevationMismatch (
			int        zoom_lev, // of the data (3 levels down for tiles)
			double []  elev,
			int        width,
			double []  plane_metric, // tiltx,tilty, offs - in meters
			double     metric_error) {
		double     camera_height = getLLA()[2]- plane_metric[2];
		FloatImageData alt = getAlt();
		int orig_zoom_level = alt.getZoomLevel();
		double orig_pix_meters = alt.getPixMeters();
		double [] vert_meters = alt.getVertMeters();
		int diff_zoom_lev = zoom_lev - orig_zoom_level;
		double pix_size = orig_pix_meters / getScale(diff_zoom_lev); // zoom_lev negative, pixel larger
		double [] pix_vertical = {vert_meters[0]/pix_size,vert_meters[1]/pix_size}; // image center in pixels
		double     max_r_diff = metric_error * camera_height / pix_size;
		double [] plane_pix =
			{plane_metric[0]*pix_size,plane_metric[1]*pix_size,plane_metric[2],pix_vertical[0],pix_vertical[1]};
		boolean [] mask = removeHighProjection (
				elev, // final double []  data,
				null, // final boolean [] mask_in,    // will be modified if provided
				max_r_diff, // final double     max_r_diff,
				plane_pix, // final double []  ground_plane, // tiltx,tilty, offs, x0(pix), y0(pix) or null
				width); // final int        width)
		return mask;
	}
	
	
	
	/**
	 * Remove specified fractions (0 <= (rlow+rhigh) <= 1.0) of too low  and too high values 
	 * @param data double [] input data
	 * @param mask_in optional (may be null) mask for the data array. Will be modified if provided.
	 * @param abs_high high limit of the histogram (reasonably higher than useful range of the data[])
	 * @param abs_low low limit of the histogram (reasonably lower than useful range of the data[])
	 * @param rhigh fraction of the highest data[] elements to remove
	 * @param rlow fraction of the lowest data[] elements to remove
	 * @param ground_plane null or {tilt_x, tilt_y, offs, x0,y0}, where tilt_x and tilt_y are per pixel.
	 *                     x0, y0 are also in pixels (not meters)
	 * @param width - width of the data[] array. Only used if ground_plane != null                     
	 * @param num_bins number of the histogram bins
	 * @return boolean array of the remaining data elements. Input mask_in array (if not null) is modified too.
	 */
	public static boolean [] removeRelativeLowHigh (
			final double [] data,
			final boolean [] mask_in,
			final double abs_high,
			final double abs_low,
			final double rhigh,
			final double rlow,
			final double []  ground_plane, // tiltx,tilty, offs, x0(pix), y0(pix) or null
			final int     width, // only used with ground_plane != null;
			final int    num_bins) {
		final boolean [] mask = (mask_in == null) ? new boolean [data.length] : mask_in;
		if (mask_in == null) {
			Arrays.fill(mask, true);
		}
		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];
		final double scale = num_bins/(abs_high - abs_low);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()) if (mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							if (ground_plane != null) {
								double x = nPix % width - ground_plane[3];
								double y = nPix/width - ground_plane[4];
								double tilt_x = ground_plane[0];
								double tilt_y = ground_plane[1];
								double offs = ground_plane[2];
								d -= x * tilt_x + y * tilt_y + offs;
							}
							int bin = Math.min(Math.max(((int) Math.round((d-abs_low)*scale)), 0), num_bins-1);
							hist2[thread_num][bin] += 1.0; 
						}
					}
				}
			};
		}		      
		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 trlow = sw * rlow;
		double trhigh = sw * rhigh;
		double sh = 0, shp = 0;
		double threshold_high = abs_high;
		for (int bin = num_bins-1; bin>=0; bin--) {
			shp = sh;
			sh += hist[bin];
			if (sh > trhigh) {
				double r = (sh-trhigh)/(sh-shp);
				threshold_high = abs_low + (bin + r)/scale;
				break;
			}
		}
		sh = 0; 
		shp = 0;
		double threshold_low = abs_low;
		for (int bin = 0; bin < num_bins; bin++) {
			shp = sh;
			sh += hist[bin];
			if (sh > trlow) {
				double r = (trlow-shp)/(sh-shp);
				threshold_low = abs_low + (bin + r)/scale;
				break;
			}
		}
		final double fthreshold_low = threshold_low;
		final double fthreshold_high = threshold_high;
		if (ground_plane == null) {
			return removeAbsoluteLowHigh (
					data,           // final double [] data,
					mask,           // final boolean [] mask,
					fthreshold_high,  // final double threshold_high,
					fthreshold_low); // final double threshold_low)
		}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
//					int thread_num = ati.getAndIncrement();
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()) if (mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							double x = nPix % width - ground_plane[3];
							double y = nPix/width - ground_plane[4];
							double tilt_x = ground_plane[0];
							double tilt_y = ground_plane[1];
							double offs = ground_plane[2];
							d -= x * tilt_x + y * tilt_y + offs;
							if (!((d >= fthreshold_low) && (d <= fthreshold_high))) {
								mask[nPix] = false;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return mask;
	}

	/**
	 * Remove specified fractions (0 <= (rlow+rhigh) <= 1.0) of too low  and too high values 
	 * @param data double [] input data
	 * @param mask_in optional (may be null) mask for the data array. Will be modified if provided.
	 * @param abs_dif high limit of the histogram (reasonably higher than useful range of the data[])
	 * @param rel_frac fraction of the highest data[] elements to remove
	 * @param ground_plane null or {tilt_x, tilt_y, offs, x0,y0}, where tilt_x and tilt_y are per pixel.
	 *                     x0, y0 are also in pixels (not meters)
	 * @param width - width of the data[] array. Only used if ground_plane != null                     
	 * @param num_bins number of the histogram bins
	 * @return boolean array of the remaining data elements. Input mask_in array (if not null) is modified too.
	 */
	public static boolean [] removeRelativeLowHigh (
			final double [] data,
			final boolean [] mask_in,
			final double abs_diff,
			final double rel_frac,
			final double []  ground_plane, // tiltx,tilty, offs, x0(pix), y0(pix) or null
			final int     width, // only used with ground_plane != null;
			final int    num_bins) {
		final boolean [] mask = (mask_in == null) ? new boolean [data.length] : mask_in;
		if (mask_in == null) {
			Arrays.fill(mask, true);
		}
		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];
		final double scale = num_bins/abs_diff;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()) if (mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							if (ground_plane != null) {
								double x = nPix % width - ground_plane[3];
								double y = nPix/width - ground_plane[4];
								double tilt_x = ground_plane[0];
								double tilt_y = ground_plane[1];
								double offs = ground_plane[2];
								d -= x * tilt_x + y * tilt_y + offs;
							}
							int bin = Math.min(Math.max(((int) Math.round(Math.abs(d)*scale)), 0), num_bins-1);
							hist2[thread_num][bin] += 1.0; 
						}
					}
				}
			};
		}		      
		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 trlow = sw * rlow;
		double trhigh = sw * rel_frac; // rhigh;
		double sh = 0, shp = 0;
		double threshold_high = abs_diff; // abs_high;
		for (int bin = num_bins-1; bin>=0; bin--) {
			shp = sh;
			sh += hist[bin];
			if (sh > trhigh) {
				double r = (sh-trhigh)/(sh-shp);
				threshold_high = (bin + r)/scale; // abs_low + (bin + r)/scale;
				break;
			}
		}
		sh = 0; 
		shp = 0;
		/*
		double threshold_low = abs_low;
		for (int bin = 0; bin < num_bins; bin++) {
			shp = sh;
			sh += hist[bin];
			if (sh > trlow) {
				double r = (trlow-shp)/(sh-shp);
				threshold_low = abs_low + (bin + r)/scale;
				break;
			}
		}
		final double fthreshold_low = threshold_low;
		*/
		final double fthreshold_high = threshold_high;
		if (ground_plane == null) {
			return removeAbsoluteLowHigh (
					data,           // final double [] data,
					mask,           // final boolean [] mask,
					fthreshold_high,  // final double threshold_high,
					-fthreshold_high); // fthreshold_low); // final double threshold_low)
		}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
//					int thread_num = ati.getAndIncrement();
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()) if (mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							double x = nPix % width - ground_plane[3];
							double y = nPix/width - ground_plane[4];
							double tilt_x = ground_plane[0];
							double tilt_y = ground_plane[1];
							double offs = ground_plane[2];
							d -= x * tilt_x + y * tilt_y + offs;
//							if (!((d >= fthreshold_low) && (d <= fthreshold_high))) {
							if (!((d >= -fthreshold_high) && (d <= fthreshold_high))) {
								mask[nPix] = false;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return mask;
	}
	
	
	
	/**
	 * Remove (mask out) tiles that have high product of distance (in pixels) from the vertical point
	 * by the metric difference between the elevation and an approximation plane 
	 * @param data elevation data, decimated to match GPU tile grid. May have NaNs.
	 * @param mask_in null or boolean[] mask same dimension as the data[]/ False disables processing,
	 *                same as NaN in data[]. Will be modified if provided;
	 * @param max_r_diff maximal product of pixel distance from the vertical point and elevation difference
	 *                   from the plane.
	 * @param ground_plane {tilt_x, tilt_y, offs, x0(pix), y0(pix)} or null tilt_x and tilt_y are measured
	 *                     in meters/pixel, offs - in meters and x0,y0 - in pixels
	 * @param width        pixel width of the data[]
	 * @return boolean [] mask with disabled tiles
	 */
	private static boolean [] removeHighProjection (
			final double []  data,
			final boolean [] mask_in,    // will be modified if provided
			final double     max_r_diff,
			final double []  ground_plane, // tiltx,tilty, offs, x0(pix), y0(pix) or null
			final int        width) {
		final double max_r2_diff = max_r_diff*max_r_diff;
		final boolean [] mask = (mask_in == null) ? new boolean [data.length] : mask_in; 
		if (mask_in == null) {
			Arrays.fill(mask, true);
		}
		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 < data.length; nPix = ai.getAndIncrement()) if (mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							double x = nPix % width - ground_plane[3];
							double y = nPix/width - ground_plane[4];
							double tilt_x = ground_plane[0];
							double tilt_y = ground_plane[1];
							double offs = ground_plane[2];
							d -= x * tilt_x + y * tilt_y + offs;
							double d2 = d*d;
							double r2 = x*x + y*y;
							double d2r2 = d2*r2;
							if (d2r2 > max_r2_diff) {
								mask[nPix] = false;
							}
						} else {
							mask[nPix] = false;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return mask;
	}
	
	public static double [] getBorderWeights(
			final double []  data,
			final double     sigma,
			int              width) {
		double [] weights = new double [data.length];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()){
						weights[nPix] = Double.isNaN(data[nPix])? -1:1;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		// TODO: Make threaded !
		(new DoubleGaussianBlur()).blurDouble(
				weights,              // double[] pixels,
				width,                // int width,
				weights.length/width, // int height,
				sigma,                // double sigmaX,
				sigma,                // double sigmaY,
				0.01);                // double accuracy);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < data.length; nPix = ai.getAndIncrement()){
						if (weights[nPix] < 0) {
							weights[nPix] = 0;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return weights;
	}

	
	/**
	 * Fit a plane to the input data, relative to the specified point in Rectangle wnd 
	 * @param data double input data array
	 * @param mask mask that disables somew input data (or null)
	 * @param weight optional weights array (same size as data and mask) 
	 * @param width  width of the rectangle
	 * @param xy0    a pair of x0, y0 - origin where the plane is referenced too
	 * @return double array of {tiltx, tilty, offset, xy0[0] and xy0[1]).
	 */
	public static double [] getPlane(
			final double []   data,
			final boolean [] mask,
			final double []  weight,
			final int        width,
			final double []  xy0) {
		return getPlane(
				   data, // final double []   data,
				mask,  // final boolean [] mask,
				weight, //final double []  weight,
				width, // final int        width,
				xy0, // final double []  xy0,
				null); // String dbg_title)
	}
	public static double [] getPlane(
			final double []   data,
			final boolean [] mask,
			final double []  weight,
			final int        width,
			final double []  xy0,
			String dbg_title) {
		final double [][][] mdatai = new double [data.length][][];
		AtomicInteger anum_good = new AtomicInteger(0);
		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 < data.length; nPix = ai.getAndIncrement()) if ((mask == null) || mask[nPix]){
						double d = data[nPix];
						if (!Double.isNaN(d)) {
							int x = nPix % width;  
							int y = nPix / width;
							double dx = x-xy0[0];
							double dy = y-xy0[1];
							int mindx = anum_good.getAndIncrement();
							mdatai[mindx] = new double[3][];
							mdatai[mindx][0] = new double [2];
							mdatai[mindx][0][0] = dx;
							mdatai[mindx][0][1] = dy;
							mdatai[mindx][1] = new double [1];
							mdatai[mindx][1][0] =  d; 
							mdatai[mindx][2] = new double [1];
							mdatai[mindx][2][0] =  (weight == null)? 1.0 : weight[nPix];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		final double [][][] mdata = new double [anum_good.get()][][];
		System.arraycopy(mdatai, 0, mdata, 0, mdata.length);
		double[][] approx2d = (new PolynomialApproximation()).quadraticApproximation(
				mdata,
				true,       // boolean forceLinear,  // use linear approximation
				null,       // damping,    // double [] damping, null OK
				-1);        // debug level
		if (approx2d != null) {
			if (dbg_title != null) {
				double [] plane=new double[data.length];
				double [] diff=new double[data.length];
				double [] dweight = (weight != null) ? weight: new double[data.length];
				double [] dmask =   new double[data.length];
				double [] masked = new double[data.length];
				Arrays.fill(masked, Double.NaN);
				for (int nPix = 0; nPix < plane.length; nPix++) {
					int x = nPix % width;  
					int y = nPix / width;
					double dx = x-xy0[0];
					double dy = y-xy0[1];
					plane[nPix] = approx2d[0][2]+approx2d[0][0]*dx+approx2d[0][1]*dy;
					diff[nPix] = data[nPix]-plane[nPix];
					dmask[nPix] = ((mask == null) || Double.isNaN(data[nPix]))? Double.NaN : (mask[nPix]?2:1);
					if ((mask == null) || mask[nPix]) {
						masked[nPix] = diff[nPix];
					}
				}
				ShowDoubleFloatArrays.showArrays(
						new double[][] {data,plane,diff,masked,dweight,dmask},
						width,
						data.length/width,
						true,
						dbg_title,
						new String[] {"data","approx","diff","masked","weight","mask"});
			}
			
			return new double[] {approx2d[0][0], approx2d[0][1], approx2d[0][2], xy0[0], xy0[1]}; // tiltX, tiltY, offset
		}
		return null;
	}
	
	
	/**
	 * Trying to estimate image OTF to modify correlation results. Some images are better, some - worse
	 * (blurred because of elevation errors)? For all image or parts of it? So on some images all
	 * real objects (and false ones) get higher correlation, on some - all get lower. So some compensation
	 * on image quality may help to discriminate   
	 * @param data   image to process (may have NaNs)
	 * @param width  image width
	 * @param size   FFT size (now 128)
	 * @param center_period period (in pixels) corresponding to the frequency to measure OTF derivative 
	 * @param range_period relative frequency range to average: low band from center/range_period to center,
	 *                     high band - from center to center*range_period
	 * @param wh           if not null, should be int[2] - will return {tilesX,tilesY} for the result
	 * @param debugLevel
	 * @return per tile: null or a pair of high_frequency_response/low_frequency_response (around center)
	 *         for horizontal and vertical directions
	 */
	public static double [][] getHiFreq(
			final double [] data,
			final int       width,
			final int       size,      // power of 2, such as 64
			final double    center_period,// center frequency is size/center_period
			final double    range_period, // ~1.5 - from center/range to center*range
			final int []    wh,           // result size
			final int       debugLevel){
	    final int dbg_x = -2668;
	    final int dbg_y =  256;

		final int height = data.length/width;
		final int tilesX = (int) Math.ceil(width/(size/2)) + 1;
		final int tilesY = (int) Math.ceil(height/(size/2)) + 1;
		if (wh != null) {
			wh[0] = tilesX;
			wh[1] = tilesY;
		}
		final double [][] hi_feq = new double [tilesX*tilesY][];
		int center_freq = (int) Math.round(size/center_period);
		int low_freq = (int) Math.round(size/center_period/range_period);
		int high_freq = (int) Math.round(size/center_period*range_period);
		final int [][][] ranges = {		// {first, second},{low freq, high freq}, {start, end}
			{   {size/2 - center_freq + 1, size/2 - low_freq},
				{size/2 - high_freq,      size/2 - center_freq - 1}
			},
			{{size/2 + low_freq,       size/2 + center_freq - 1},
			{size/2 + center_freq + 1, size/2 + high_freq}}};
		final double [] range_npix = {
				size*(center_freq-low_freq),
				size*(high_freq-center_freq)};
		final double [] window = new double [size*size];
		final double [] wnd1d = new double[size/2];
		for (int i = 0; i < size/2; i++) {
			wnd1d[i] = Math.sin((i+0.5)*Math.PI/size);
		}
		for (int i = 0; i < size/2; i++) {
			for (int j = 0; j < size/2; j++) {
				double w = wnd1d[i]*wnd1d[j];
				int k = i*size + j;
				window[k] = w;
				window[window.length-1-k] = w;
				k = (i+ 1)*size - 1 -j;
				window[k] = w;
				window[window.length-1-k] = w;
			}
		}
		
		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() {
					double [] dtile = new double [size*size];
					TileNeibs tn =  new TileNeibs(size,size);
					DoubleFHT doubleFHT = new DoubleFHT();
					for (int nTile = ai.getAndIncrement(); nTile <hi_feq.length; nTile = ai.getAndIncrement()){
						int tileX = nTile % tilesX;
						int tileY = nTile / tilesX;
						int px0 = (size/2) * tileX; // absolute in the original/result image 
						int py0 = (size/2) * tileY;
						int x0 = Math.max(0, -px0);
						int y0 = Math.max(0, -py0);
						int x1 = Math.min(size, width- px0);
						int y1 = Math.min(size, height-py0);
						boolean dbg_tile = (Math.abs((px0 + size/2) - dbg_x) < size/4) && (Math.abs((py0 + size/2) - dbg_y) < size/4);
						if (dbg_tile) {
							System.out.println("getHiFreq(): tileX="+tileX+", tileY="+tileY);
							System.out.println("getHiFreq(): px0="+px0+", py0="+py0);
						}
						int lwidth=x1-x0;
						boolean has_NaN = false; 
						if ((x0>0) || (y0>0) || (x1 < size) || (y1 < size)) {
							Arrays.fill(dtile,Double.NaN);
						}
						for (int y = y0; y < y1; y++) {
							System.arraycopy(
									data,
									(py0 + y)*width+(px0+x0),
									dtile,
									y*size+x0,
									lwidth);
						}
						for (int i = 0; i < dtile.length; i++) {
							if (Double.isNaN(dtile[i])) {
								has_NaN = true;
								break;
							}
						}
						if (has_NaN) {
							fillNaNs(dtile, tn, 3);
						}
						for (int i = 0; i < dtile.length; i++) {
							dtile[i] *= window[i];
						}
						if (dbg_tile) {
							String [] rslt_titles= {"windowed"};
							ShowDoubleFloatArrays.showArrays(
									new double[][] {dtile},
									size,
									size,
									true,
									"windowed_data_tx"+tileX+"_ty"+tileY,
									rslt_titles);
						}
						double [] amp = doubleFHT.getFreqAmplitude(dtile);
						if (dbg_tile) {
							String [] rslt_titles= {"amplitude"};
							ShowDoubleFloatArrays.showArrays(
									new double[][] {amp},
									size,
									size,
									true,
									"amplitude_tx"+tileX+"_ty"+tileY,
									rslt_titles);
						}
						double [][] lo_hi_avg = new double[2][2]; // {x,y}{low,high}
						for (int hl = 0; hl < 2; hl++) { // 0 - low, 1 - high
							for (int sf = 0; sf < 2; sf++) { // 0 - fist, 1 second range
								for (int i = ranges[sf][hl][0]; i <=ranges[sf][hl][1]; i++) { 
									for (int j = 0; j < size; j++) {
										lo_hi_avg[0][hl] += amp[j*size + i];
										lo_hi_avg[1][hl] += amp[i*size + j];
									}
								}
							}
						}
						hi_feq[nTile] = new double[2];
						for (int yx = 0; yx < 2; yx++) { // 0 - y, 1 - x
							for (int hl = 0; hl < 2; hl++) { // 0 - low, 1 - high
								lo_hi_avg[yx][hl] /= range_npix[hl];
							}
							hi_feq[nTile][yx] = lo_hi_avg[yx][1]/lo_hi_avg[yx][0];  
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return hi_feq;
	}
	

	/**
	 * Similar to above, but calculates for 2 rings 
	 * @param data
	 * @param width
	 * @param size
	 * @param center_period
	 * @param range_period
	 * @param blank_xy      discard data long x and y axes (probably remaining row/column noise?)
	 * @param wh
	 * @param debugLevel
	 * @return
	 */
	public static double [][] getHiFreqCirc(
			final double [] data,
			final int       width,
			final int       size,      // power of 2, such as 64
			final double    center_period,// center frequency is size/center_period
			final double    range_period, // ~1.5 - from center/range to center*range
			final int       blank_xy, // 
			final int []    wh,           // result size
			final int       debugLevel){
	    final int dbg_x = -1144; // -2668;
	    final int dbg_y = 199;  //  256;

		final int height = data.length/width;
		final int tilesX = (int) Math.ceil(width/(size/2)) + 1;
		final int tilesY = (int) Math.ceil(height/(size/2)) + 1;
		if (wh != null) {
			wh[0] = tilesX;
			wh[1] = tilesY;
		}
		final double [][] hi_feq = new double [tilesX*tilesY][];
		int center_freq = (int) Math.round(size/center_period);
		int low_freq = (int) Math.round(size/center_period/range_period);
		int high_freq = (int) Math.round(size/center_period*range_period);
		final double blank2 = (blank_xy-1) * (blank_xy-1) + 0.5;
		final double [][] masks= new double [2][size*size];
		for (int y = 0; y<size; y++) {
			double y2 = (y-size/2);
			y2*=y2;
			if ((blank_xy == 0) || (y2 > blank2)) {
				for (int x = 0; x < size; x++) {
					double x2 = (x-size/2);
					x2*=x2;
					if ((blank_xy == 0) || (x2 > blank2)) {
						double r = Math.sqrt(x2+y2);
						if ((r >= low_freq) && (r <= high_freq)) {
							int indx = y*size + x;
							if (r < center_freq) {
								masks[0][indx] =  Math.sin(Math.PI *(center_freq - r)/(center_freq-low_freq));
							} else {
								masks[1][indx] =  Math.sin(Math.PI *(r - center_freq)/(high_freq -center_freq));
							}
						}
					}
				}
			}
		}
		if ((dbg_x >=0) && (dbg_y >=0)) {
			String [] rslt_titles= {"low_mask","high_mask"};
			ShowDoubleFloatArrays.showArrays(
					masks,
					size,
					size,
					true,
					"masks",
					rslt_titles);
		}
		for (int n = 0; n < 2; n++) {
			double s=0.0;
			for (int i = 0; i < masks[n].length; i++) {
				s+=masks[n][i];
			}
			s = 1/s;
			for (int i = 0; i < masks[n].length; i++) {
				masks[n][i]*=s;
			}
		}
		final double [] window = new double [size*size];
		final double [] wnd1d = new double[size/2];
		for (int i = 0; i < size/2; i++) {
			wnd1d[i] = Math.sin((i+0.5)*Math.PI/size);
		}
		for (int i = 0; i < size/2; i++) {
			for (int j = 0; j < size/2; j++) {
				double w = wnd1d[i]*wnd1d[j];
				int k = i*size + j;
				window[k] = w;
				window[window.length-1-k] = w;
				k = (i+ 1)*size - 1 -j;
				window[k] = w;
				window[window.length-1-k] = w;
			}
		}
		
		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() {
					double [] dtile = new double [size*size];
					TileNeibs tn =  new TileNeibs(size,size);
					DoubleFHT doubleFHT = new DoubleFHT();
					for (int nTile = ai.getAndIncrement(); nTile <hi_feq.length; nTile = ai.getAndIncrement()){
						int tileX = nTile % tilesX;
						int tileY = nTile / tilesX;
						int px0 = (size/2) * tileX; // absolute in the original/result image 
						int py0 = (size/2) * tileY;
						int x0 = Math.max(0, -px0);
						int y0 = Math.max(0, -py0);
						int x1 = Math.min(size, width- px0);
						int y1 = Math.min(size, height-py0);
						boolean dbg_tile = (Math.abs((px0 + size/2) - dbg_x) < size/4) && (Math.abs((py0 + size/2) - dbg_y) < size/4);
						if (dbg_tile) {
							System.out.println("getHiFreq(): tileX="+tileX+", tileY="+tileY);
							System.out.println("getHiFreq(): px0="+px0+", py0="+py0);
						}
						int lwidth=x1-x0;
						boolean has_NaN = false; 
						if ((x0>0) || (y0>0) || (x1 < size) || (y1 < size)) {
							Arrays.fill(dtile,Double.NaN);
						}
						for (int y = y0; y < y1; y++) {
							System.arraycopy(
									data,
									(py0 + y)*width+(px0+x0),
									dtile,
									y*size+x0,
									lwidth);
						}
						for (int i = 0; i < dtile.length; i++) {
							if (Double.isNaN(dtile[i])) {
								has_NaN = true;
								break;
							}
						}
						if (has_NaN) {
							continue;
//							fillNaNs(dtile, tn, 3);
						}
						for (int i = 0; i < dtile.length; i++) {
							dtile[i] *= window[i];
						}
						if (dbg_tile) {
							String [] rslt_titles= {"windowed"};
							ShowDoubleFloatArrays.showArrays(
									new double[][] {dtile},
									size,
									size,
									true,
									"windowed_data_tx"+tileX+"_ty"+tileY,
									rslt_titles);
						}
						double [] amp2 = doubleFHT.getFreqAmplitude2(dtile);
						if (dbg_tile) {
							String [] rslt_titles= {"amplitude2","low_mask","high_mask"};
							ShowDoubleFloatArrays.showArrays(
									new double[][] {amp2,masks[0],masks[1]},
									size,
									size,
									true,
									"amplitude2_tx"+tileX+"_ty"+tileY,
									rslt_titles);
						}
						hi_feq[nTile] = new double[masks.length];
						for (int n = 0; n < masks.length; n++) { 
							for (int i = 0; i < masks[n].length; i++) {
								hi_feq[nTile][n] += masks[n][i] * amp2[i];
							}
							hi_feq[nTile][n] = Math.sqrt(hi_feq[nTile][n]);
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return hi_feq;
	}
	
	
	
	
	public static double [] correlateWithPattern(
			final double [] data,
			final int       width,
			final int       psize,      // power of 2, such as 64
			final double [] pattern,    // [psize*psize]
			final boolean   convolve,   // convolve, not correlate
			final double    phaseCoeff,
			final double    lpf_sigma, // 0 - do not filter
			final int       debugLevel) {
		final int height = data.length/width;
		final double [] dout = new double [data.length];
		final double [] window = new double [psize*psize];
		final double [] wnd1d = new double[psize/2];
		for (int i = 0; i < psize/2; i++) {
			wnd1d[i] = Math.sin((i+0.5)*Math.PI/psize);
		}
		for (int i = 0; i < psize/2; i++) {
			for (int j = 0; j < psize/2; j++) {
				double w = wnd1d[i]*wnd1d[j];
				int k = i*psize + j;
				window[k] = w;
				window[window.length-1-k] = w;
				k = (i+ 1)*psize - 1 -j;
				window[k] = w;
				window[window.length-1-k] = w;
			}
		}
		final DoubleFHT doubleFHT0 = new DoubleFHT();
		final double [] patternFD = pattern.clone();
		doubleFHT0.transformPattern(patternFD);
		
		if ((width== psize) && (height == psize)) {
			return correlateWithPatternSquare(
					data,        // double [] data,
					pattern,     // double [] pattern,
					convolve,    //final boolean   convolve,   // convolve, not correlate
					window,      // double [] wnd,
					phaseCoeff, // double    phaseCoeff)
					lpf_sigma); // double    lpf_sigma)
		}
		
		final int tilesX = (int) Math.ceil(width/(psize/2)) + 1;
		final int tilesY = (int) Math.ceil(height/(psize/2)) + 1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
//		final int dbg_tileX = 20;
//		final int dbg_tileY =  1;
	    final int dbg_x = -2668;
	    final int dbg_y =  256;
		for (int passY = 0; passY < 2; passY++) { // to isolate threads results - no overlap
			final int tileY0=passY;
			final int ntilesY=(tilesY + 1 - passY)/ 2;
			for (int passX = 0; passX < 2; passX++) {
				final int tileX0=passX;
				final int ntilesX=(tilesX + 1 - passX)/ 2;
				final int ntiles = ntilesY*ntilesX;
				ai.set(0);
				for (int ithread = 0; ithread < threads.length; ithread++) {
					threads[ithread] = new Thread() {
						public void run() {
							double [] dtile = new double [pattern.length];
							TileNeibs tn =  new TileNeibs(psize,psize);
							DoubleFHT doubleFHT = new DoubleFHT();
							double [] filter = doubleFHT.getFrequencyFilter (
									dtile, // double [] data, // just to find out size
									0,    // double highPassSigma,
									lpf_sigma); // double lowPassSigma
							for (int nTile = ai.getAndIncrement(); nTile < ntiles; nTile = ai.getAndIncrement()){
								int tileX = tileX0 - 1 + 2 * (nTile % ntilesX); // nTile % ntilesX;
								int tileY = tileY0 - 1 + 2 * (nTile / ntilesX); // nTile / ntilesX;
								int px0 = (psize/2) * tileX; // absolute in the original/result image 
								int py0 = (psize/2) * tileY;
								int x0 = Math.max(0, -px0);
								int y0 = Math.max(0, -py0);
								int x1 = Math.min(psize, width- px0);
								int y1 = Math.min(psize, height-py0);
								boolean dbg_tile = (Math.abs((px0 + psize/2) - dbg_x) < psize/4) && (Math.abs((py0 + psize/2) - dbg_y) < psize/4);
								if (dbg_tile) {
									System.out.println("correlateWithPattern(): tileX="+tileX+", tileY="+tileY);
									System.out.println("correlateWithPattern(): px0="+px0+", py0="+py0);
								}

								int lwidth=x1-x0;
								boolean has_NaN = false; 
								if ((x0>0) || (y0>0) || (x1 < psize) || (y1 < psize)) {
									Arrays.fill(dtile,Double.NaN);
								}
								for (int y = y0; y < y1; y++) {
									System.arraycopy(
											data,
											(py0 + y)*width+(px0+x0),
											dtile,
											y*psize+x0,
											lwidth);
								}
								for (int i = 0; i < dtile.length; i++) {
									if (Double.isNaN(dtile[i])) {
										has_NaN = true;
										break;
									}
								}
								if (has_NaN) {
									fillNaNs(dtile, tn, 3);
								}
								for (int i = 0; i < dtile.length; i++) {
									dtile[i] *= window[i];
								}
								if (dbg_tile) {
									String [] rslt_titles= {"windowed","pattern"};
									ShowDoubleFloatArrays.showArrays(
											new double[][] {dtile, pattern},
											psize,
											psize,
											true,
											"input_corr_tx"+tileX+"_ty"+tileY,
											rslt_titles);
								}
								if (convolve) {   // convolve, not correlate
									dtile = doubleFHT.convolvePattern (
											dtile,      // double [] first,
											patternFD,  // double [] secondFD,
											filter,     // double [] filter,       //  high/low pass filtering
											null);      // double [] first_save )  //null-OK
								} else {
									// now cross-correlate with fat zero dtile and
									// important to assign: dtile is modified, but not to the result!
									dtile = doubleFHT.phaseCorrelatePattern ( // new
											dtile,      // double [] first,
											patternFD,  // double [] secondFD,
											phaseCoeff, // double    phaseCoeff,
											filter,     // double [] filter,       //  high/low pass filtering
											null);      // double [] first_save )  //null-OK
								}								
								if (dbg_tile) {
									String [] rslt_titles= {"corr","pattern"};
									ShowDoubleFloatArrays.showArrays(
											new double[][] {dtile, pattern},
											psize,
											psize,
											true,
											"corr_tx"+tileX+"_ty"+tileY,
											rslt_titles);
								}
								
								// multiply result by a window second time and add to the output array 
								for (int y = y0; y < y1; y++) {
									for (int x = x0; x < x1; x++) {
										int i = y * psize + x;
										dout[(py0 + y) * width+(px0 + x)] += dtile[i]*window[i];
									}
								}
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
			}
		}
		return dout;
	}
	
	public static double [] correlateWithPatternSquare(
			double [] data_in,
			double [] pattern_in,
			boolean   convolve,   // convolve, not correlate
			double [] wnd_in,
			double    phaseCoeff,
			double    lpf_sigma) {
//		phaseCoeff = 0.5;
		boolean dbg=false;
		double [] data = data_in.clone();
		double [] pattern = pattern_in.clone();
		double [] wnd = wnd_in.clone();
		
		int       size = (int) Math.sqrt(data.length);
		double [] data_orig = data.clone();
		for (int i = 0; i < data.length; i++) {
			data[i] *= wnd[i];
		}
		if (dbg) {
			String [] rslt_titles= {"original","window","windowed","pattern"};
			ShowDoubleFloatArrays.showArrays(
					new double[][] {data_orig, wnd, data, pattern},
					size,
					size,
					true,
					convolve?"input_conv":"input_corr",
					rslt_titles);
		}
		DoubleFHT doubleFHT = new DoubleFHT();
		double [] filter = doubleFHT. getFrequencyFilter (
				data, // double [] data, // just to find out size
				0,    // double highPassSigma,
				lpf_sigma); // double lowPassSigma
		double [] data_orig2 = data.clone();
		double [] pattern_orig = pattern.clone();
		boolean use_new = true; // false;
		double [] corr_out = null;
		if (use_new) {
			final double [] patternFD = pattern.clone();
			doubleFHT.transformPattern(patternFD);
			if (convolve) {
				corr_out = doubleFHT.convolvePattern (
						data,       // double [] first,
						patternFD,  // double [] secondFD,
						filter,     // double [] filter,       //  high/low pass filtering
						null);      // double [] first_save )  //null-OK
			} else {
				corr_out = doubleFHT.phaseCorrelatePattern ( // new
					data,       // double [] first,
					patternFD,  // double [] secondFD,
					phaseCoeff, // double    phaseCoeff,
					filter,     // double [] filter,       //  high/low pass filtering
					null);      // double [] first_save )  //null-OK
			}
		} else {
			if (convolve) {
				corr_out = doubleFHT. convolve ( // new
						data,    // double [] first,
						pattern, // double [] second,
						filter); // double [] filter,     //  high/low pass filtering
			} else {
				corr_out = doubleFHT. phaseCorrelate ( // new
					data,        // double [] first,
					pattern,     // double [] second,
					phaseCoeff,  // double    phaseCoeff,
					filter,      // double [] filter,     //  high/low pass filtering
					null,        // double [] first_save,
					null);       // double [] second_save )  //null-OK
			}
		}
		
		if (dbg) {
			String [] rslt_titles= {"corr", "original","window","windowed","pattern"};
			ShowDoubleFloatArrays.showArrays(
					new double[][] {corr_out, data_orig, wnd, data_orig2, pattern_orig},
					size,
					size,
					true,
					convolve?"output_conv":("output_corr"+phaseCoeff),
					rslt_titles);
		}
		if (dbg) System.out.println("testPhaseCorr() done");
		return corr_out;
	}
	
	public static void fillNaNs(
			double [] data,
			TileNeibs tn,
			int min_neibs) { // 3
		boolean dbg=false;
		double [] data_orig = dbg? data.clone():null;
		boolean [] known = new boolean [data.length];
		boolean [] wave = new boolean [data.length];
		double wdiag=0.7;
		double [] weights = {1.0,wdiag,1.0,wdiag,1.0,wdiag,1.0,wdiag};
		
		ArrayList<Integer> front_list = new ArrayList<Integer>(); 
		for (int i = 0; i < data.length; i++) {
			if (Double.isNaN(data[i]) && !wave[i]) {
				int num_neibs = 0;
				for (int dir=0; dir < TileNeibs.DIRS; dir++) {
					int i1 = tn.getNeibIndex(i, dir);
					if ((i1 >= 0) && !Double.isNaN(data[i1])) {
						num_neibs++;
					}
				}
				if (num_neibs >= min_neibs) {
					front_list.add(i);
					wave[i]=true;
				}
			} else {
				known[i] = true;
			}
		}
		while (!front_list.isEmpty()) {
			for (int i:front_list) {
				double swd=0,sw=0;
				for (int dir=0; dir < TileNeibs.DIRS; dir++)  {
					int i1 = tn.getNeibIndex(i, dir);
					if ((i1 >= 0) && (known[i1])) {
						double d = data[i1];
						if (!Double.isNaN(d)) {
							sw +=  weights[dir]; 
							swd += weights[dir] * d;
						}
					}
				}		
				data[i] = swd/sw;
			}
			int was_size = front_list.size();
			for (int j = 0; j< was_size; j++) {
				int i = front_list.remove(0);
				known[i] = true;
				for (int dir=0; dir < TileNeibs.DIRS; dir++) {
					int i1 = tn.getNeibIndex(i, dir);
					if ((i1 >= 0) && Double.isNaN(data[i1]) && !wave[i1]) {
						// check number of known neighbors
						int num_neibs = 0;
						for (int dir1 = 0; dir1 < TileNeibs.DIRS; dir1++) {
							int i2 = tn.getNeibIndex(i1, dir1);
							if ((i2 >= 0) && known[i2]) {
								num_neibs++;
							}
						}
						if (num_neibs >= min_neibs) {
							front_list.add(i1);
							wave[i1]=true;
						}
					}
				}
			}
		}
		if (data_orig != null) {
			String [] rslt_titles= {"original","filled"};
			ShowDoubleFloatArrays.showArrays(
					new double[][] {data_orig, data},
					tn.getSizeX(),
					tn.getSizeY(),
					true,
					"fill_NaN",
					rslt_titles);
			
		}
	}
	
	public static void testPatternCorrelate(
			ImagePlus imp_src) {
		/*
		String pattern_dir= "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/debug/mines/pattern_25m_zoom1/synthetic/";
		String [] pattern_files=
			{"patterns_50m_zoom1_200x200.tiff",
			"patterns_50m_evening_zoom1_200x200.tiff",
			"patterns_50m_evening_01_zoom1_200x200_00.tiff",
			"patterns_50m_evening_02_zoom1_200x200_00.tiff",
			"patterns_50m_evening_03_zoom1_200x200_00.tiff",
			"patterns_50m_evening_04_zoom1_200x200_00.tiff",
			"patterns_50m_evening_05_zoom1_200x200_00.tiff"};
      */			
		int       zoomout =     2; // 1;
		int       corr_size = 128; // 256;
		double    phaseCoeff = 0.5;
		int       debugLevel = 1;
		double    min_corr  =          0.01; // 0.0025; // real max >0.005 with scale -500000;
		double    min_corr_full_rel  = 0.75; // minimal correlation with full pattern
		double    full_preference =    1.4;  // prefer full pattern if its strength is slightly lower than for partial
		double    max_min_ratio =      3.0;  // ratio of halves best/worst to use half-pattern     
		boolean   combine_full = true;
		double    adv_radius = 60.0; // pixels
		int       corr_radius = (int) (Math.sqrt(0.5)* adv_radius) -1 ; // 30; // may use same as adv_radius; but not more
		double [][] src_marks = null;
		PointRoi pRoi = (PointRoi) imp_src.getRoi();
		if (pRoi != null) {
			FloatPolygon fp = pRoi.getContainedFloatPoints();
			src_marks = new double[fp.npoints][2];
			for (int i = 0; i < src_marks.length; i++) {
				src_marks[i][0] = fp.xpoints[i];
				src_marks[i][1] = fp.ypoints[i];
			}
		}
		
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Correlate image with pattern",1200,500);
		gd.addStringField ("Pattern directory",  pattern_dir, 180, 	"Absolute path including trailing \"/\".");
//		gd.addStringField ("Pattern filename",   pattern_file,180, 	"Pattern filename.");
		gd.addChoice("Pattern filename:", pattern_files, pattern_files[pattern_files.length-1]);
		
		gd.addNumericField("Zoom-out factor",            zoomout,  0,4,"x", "Reduce pattern resolution to match image.");
		gd.addNumericField("Phase correlation coefficient",  phaseCoeff,  3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
		gd.addNumericField("Minimal correlation",        min_corr,  5,7,"","Minimal correlation value to keep.");
		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",  combine_full, "Multiply by normalized full pattern correlation maximum.");
		gd.addNumericField("Adversarial distance",       adv_radius,  1,6,"pix", "Suppress weaker if they have closer strong ones.");
		gd.addNumericField("Correlation peak max radius",corr_radius,  0,4,"pix", "Limit correlation peak radius. Should be <= adversarial");
		gd.addNumericField("Correlation size",           corr_size,  0,4,"pix", "Should be power of 2.");
		gd.addNumericField("Debug level",                debugLevel,  0,4,"", "Debug level.");
		gd.showDialog();
		if (gd.wasCanceled()) return;
		pattern_dir=        gd.getNextString();		
//		pattern_file=       gd.getNextString();
		String pattern_file = pattern_files[gd.getNextChoiceIndex()];
		zoomout=        (int) gd.getNextNumber();
		phaseCoeff=           gd.getNextNumber();
		min_corr=             gd.getNextNumber();
		min_corr_full_rel=    gd.getNextNumber();
		full_preference=      gd.getNextNumber();
		max_min_ratio=        gd.getNextNumber();		
		combine_full =        gd.getNextBoolean();
		adv_radius=           gd.getNextNumber();
		corr_radius=    (int) gd.getNextNumber();
		corr_size=      (int) gd.getNextNumber();
		debugLevel=     (int) gd.getNextNumber();
		
		if (corr_radius > ( (int) (Math.sqrt(0.5)* adv_radius) -1)){
			corr_radius  =  (int) (Math.sqrt(0.5)* adv_radius) -1;
			System.out.println("testPatternCorrelate(): Reducing corr_radius to "+corr_radius+" to avoid thread races");
		}
		
		double min_corr_full = min_corr*min_corr_full_rel;
		float [] fpixels = (float[]) imp_src.getProcessor().getPixels();
		int      width = imp_src.getWidth();
		int      height = imp_src.getHeight();
		double [] data = new double [fpixels.length];
		
		for (int i = 0; i < data.length; i++) {
			data[i] = fpixels[i];
		}
		// get pattern(s)
		String pattern_path=pattern_dir+pattern_file;
		ImagePlus imp_pattern = new ImagePlus(pattern_path);
		int pattern_size =  imp_pattern.getWidth();
		if (pattern_size == 0) {
			System.out.println("testPatternCorrelate(): pattern \""+pattern_path+"\" is not found.");
			return;
		}
		double [] kernel =  getConvolutionKernel();
		
		ImageStack stack_pattern = imp_pattern.getStack();
		int nSlices = stack_pattern.getSize();
		double [][] patterns = new double[nSlices][];
		String [] pattern_labels = new String[nSlices];
		for (int n = 0; n < patterns.length; n++) {
			pattern_labels[n]=stack_pattern.getShortSliceLabel(n+1);
			float [] fpixels_pattern = (float[]) stack_pattern.getPixels(n+1);
			patterns[n]=new double[fpixels_pattern.length];
			for (int i = 0; i < fpixels_pattern.length; i++) {
				patterns[n][i] = fpixels_pattern[i];
			}
		}

		double [][] corrs_out =     new double[patterns.length][]; 
		double [][] convolve_out =  new double[patterns.length][]; 
		double [][] corr_patterns = new double[patterns.length][];
		for (int n = 0; n < patterns.length; n++) {
			corr_patterns[n] = patternZoomCropPad(
					patterns[n],      // double [] pattern,
					pattern_size, // int       pattern_size,
					corr_size,    // int       size,
					zoomout,      // int       zoomout,
					false); // true);        // out_normalize); // boolean   normalize) 
			if (kernel != null) {
				corr_patterns[n] = convolveWithKernel(
						corr_patterns[n], // final double [] data,
						kernel,           // final double [] kernel,
						corr_size);       // final int width)
			}
			corrs_out[n]= correlateWithPattern(
					data,             // final double [] data,
					width,            // final int       width,
					corr_size,        // final int       psize,      // power of 2, such as 64
					corr_patterns[n], //final double [] pattern,    // [psize*psize]
					false,            // 			final boolean   convolve,   // convolve, not correlate
					phaseCoeff,       // final double    phaseCoeff,
					0,                // final double    lpf_sigma, // 0 - do not filter
					debugLevel);      // final int       debugLevel) {
		}
		String [] patt_titles = new String[corrs_out.length];
		for (int i = 0; i < patt_titles.length; i++) {
			patt_titles[i] = "patt_"+i;
		}
		ImagePlus imp_corrs = ShowDoubleFloatArrays.makeArrays(
				corrs_out,
				width,
				height,
				removeKnownExtension(imp_src.getTitle())+"-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.setRoi(roi);
		}
		imp_corrs.show();
		
//data
		double []   bestcorr = new double [data.length];
		double []   fullcorr = new double [data.length];
		int []      bestpatt = new int [bestcorr.length];
		ArrayList<Point> object_list =  combineDirCorrs (
				corrs_out,       // final double [][] corrs,
				width,           // final int         width,
				fullcorr,        // final double []   fullcorr_in,
				bestcorr,        // final double []   bestcorr_in
				bestpatt,        // final int []      bestpatt_in,
				min_corr,        // final double      min_corr,
				min_corr_full,   // final double      min_corr_full,
				full_preference, // final double      full_preference,
				max_min_ratio,   // final double      max_min_ratio,
				combine_full,    // final boolean     combine_full,    // multiply by normalized full pattern correlation maximum
				adv_radius,      // final double      adv_radius,
				corr_radius);    // final int         corr_radius)
		
		
		
		
		System.out.println("testPatternCorrelate(): Found "+object_list.size()+" candidates");
		ImagePlus imp_best = ShowDoubleFloatArrays.makeArrays(
				bestcorr,
				width,
				height,
				removeKnownExtension(imp_src.getTitle())+"-BEST_CORR");				
		double [] dbg_bestpatt = new double [bestpatt.length];
		Arrays.fill(dbg_bestpatt, Double.NaN);
		for (int i = 0; i <bestpatt.length; i++) {
			if (bestpatt[i] >=0) {
				dbg_bestpatt[i] = bestpatt[i];
			}
		}
		ImagePlus imp_bestpatt = ShowDoubleFloatArrays.makeArrays(
				dbg_bestpatt,
				width,
				height,
				removeKnownExtension(imp_src.getTitle())+"-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);
			}
			imp_best.setRoi(roi);
			imp_bestpatt.setRoi(roi);
		}
		imp_best.show();
		imp_bestpatt.show();
		
		
		double [][] filtered_corrs = splitPattenCorrelations(
				patterns.length, // final int         num_patterns,
				bestcorr,        // final double []   bestcorr,
				bestpatt);       // final int []      bestpatt)		
		
		for (int n = 0; n < patterns.length; n++) {
			convolve_out[n]= correlateWithPattern(
					filtered_corrs[n],// corrs_out[n], // final double [] data,
					width,            // final int       width,
					corr_size,        // final int       psize,      // power of 2, such as 64
					corr_patterns[n], // final double [] pattern,    // [psize*psize]
					true,             // final boolean   convolve,   // convolve, not correlate
					phaseCoeff,       // final double    phaseCoeff,
					0,                // final double    lpf_sigma, // 0 - do not filter
					debugLevel);      // final int       debugLevel) {
		}
		double [] convolved_merged = mergePattenConvolutions(
				convolve_out); // final double [][] convs)
		
		String [] rslt_titles =new String [patterns.length+2]; 
		double [][] rslt = new double [patterns.length + 2][]; // ]{data, corr_out};
		double [][] conv_rslt = new double [patterns.length + 2][]; // ]{data, corr_out};
		rslt[0] = data;
		conv_rslt[0] = data;
		rslt_titles[0] = "original";

		rslt[1] = bestcorr;
		conv_rslt[1] = convolved_merged;
		rslt_titles[1] = "combined";
		
		for (int i = 0; i < patterns.length; i++) {
			rslt[i+2] = filtered_corrs[i];
			conv_rslt[i+2] = convolve_out[i];
			rslt_titles[i+2] = "corr_"+pattern_labels[i];
		}
		
		ImagePlus imp_patt_corrs = ShowDoubleFloatArrays.makeArrays(
				rslt,
				width,
				height,
				removeKnownExtension(imp_src.getTitle())+"-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);
			}
			imp_patt_corrs.setRoi(roi);
		}
		imp_patt_corrs.show();
		
		
		ImagePlus imp_patt_conv = ShowDoubleFloatArrays.makeArrays(
				conv_rslt,
				width,
				height,
				removeKnownExtension(imp_src.getTitle())+"-PATTERN_CORR_CONVOLVE_min_corr"+min_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);
			}
			imp_patt_conv.setRoi(roi);
		}
		imp_patt_conv.show();
		System.out.println("testPatternCorrelate(): correlation done");
	}
	
	/**
	 * Combine results of the phase correlations with different patters
	 * (now with semi-obscured half-circles), using the best (highest value match),
	 * filter by correlation value and suppress weeaker maximums by stronger neighbors
	 * @param corrs  array of 2D correlation results [num_pattern][pixel]
	 * @param width width of the correlation array
	 * @param bestcorr_in null or double array  [corrs[0].length] will contain
	 *        best correlation value from multiple patterns of corrs[][].
	 * @param bestpatt_in null or integer array  [corrs[0].length] will contain
	 *        number of the best pattern 
	 * @param min_corr - minimal correlation value to use (lower - replace with 0)
	 * @param adv_radius - suppress weaker local maximums near the stronger one 
	 * @param corr_radius - maximal correlation peak radius (zero out outside).
	 *                      Should be <= adv_radius to avoid thread races 
	 * @return ArrayList of integer pairs - pixel index (x+y*width) and the pattern index
	 */
	public static ArrayList<Point> combineDirCorrs (
			final double [][] corrs,
			final int         width,
			final double []   bestcorr_in,
			final int []      bestpatt_in,
			final double      min_corr,
			final double      adv_radius,
			final int         corr_radius) { 
		final int height = corrs[0].length/width;
		final double [] bestcorr = (bestcorr_in != null) ? bestcorr_in: (new double [corrs[0].length]);
		final int []    bestpatt = (bestpatt_in != null) ? bestpatt_in: (new int    [corrs[0].length]);
		// before adversarial filtering
		final double [] bestcorr0  = new double [corrs[0].length];
		final int []    bestpatt0 =  new int    [corrs[0].length];
		Arrays.fill(bestpatt, -1);
		Arrays.fill(bestpatt0, -1); // maybe not needed
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final TileNeibs tn =  new TileNeibs(width, height);
		// Combine patterns correlations
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < bestcorr0.length; ipix = ai.getAndIncrement()) {
						for (int n = 0; n < corrs.length; n++) {
							if (corrs[n][ipix] > bestcorr0[ipix]) {
								bestcorr0[ipix] = corrs[n][ipix];
								bestpatt0[ipix] = n;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// mark all local max
		ai.set(0);
		final AtomicInteger ati = new AtomicInteger(0);
		ArrayList<ArrayList<Point>> lists = new ArrayList<ArrayList<Point>>();
		for (int i = 0; i < threads.length; i++) {
			lists.add(new ArrayList<Point>());
		}
		final int iradius = (int) Math.ceil(adv_radius);
		final double adv_radius2 = adv_radius * adv_radius;
		final double corr_radius2 = corr_radius*corr_radius;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					final TileNeibs tn =  new TileNeibs(width, height);
					int corr_diameter = 2*corr_radius+1;
					TileNeibs tnl =  new TileNeibs(corr_diameter,corr_diameter);
					int local_center = (2 * corr_radius + 2) * corr_radius;
					boolean [] mask = new boolean [corr_diameter*corr_diameter];

					for (int ipix = ai.getAndIncrement(); ipix < bestcorr0.length; ipix = ai.getAndIncrement()) if (bestcorr0[ipix] >= min_corr) {
						if (ipix == -792148) {
							System.out.println("combineDirCorrs(): ipix="+ipix);
						}
						is_max: {
							for (int dir = 0; dir < TileNeibs.DIRS; dir++) {
								int ipix1 = tn.getNeibIndex(ipix, dir);
								if ((ipix1 >= 0) && (bestcorr0[ipix1] > bestcorr0[ipix])){
									break is_max;
								}
							}
							// compare with others in adversarial radius, if equal - use higher index ipix
							for (int dy = -iradius; dy <= iradius; dy++) {
								for (int dx = -iradius; dx <= iradius; dx++) {
									double r2 = dx*dx + dy*dy;
									if ((r2 < adv_radius2) && (r2 > 2.1)) {
										int ipix1 = tn.getNeibIndex(ipix, dx, dy);
										if (ipix1 >= 0) {
											if (bestcorr0[ipix1] > bestcorr0[ipix]) {
												break is_max;
											} else if ((bestcorr0[ipix1] == bestcorr0[ipix]) && (ipix1 < ipix)) {
												break is_max;
											}
										}
									}
								}								
							}
							int patt = bestpatt0[ipix];
							lists.get(thread_num).add(new Point(ipix,patt));
							// Below should be no conflicts as no intersect
							Arrays.fill(mask, false);
							for (int dy = -corr_radius; dy <= corr_radius; dy++) {
								for (int dx = -corr_radius; dx <= corr_radius; dx++) {
									double r2 = dx*dx + dy*dy;
									if (r2 < corr_radius2) {
										int ipix1 = tn.getNeibIndex(ipix, dx, dy);
										// TODO: mark mask with continuous pixels
										if (ipix1 >= 0) {
											int li =  tnl.getNeibIndex(local_center, dx, dy); // should be always
											if (li < 0) {
												System.out.println("combineDirCorrs(): BUG1 li<0");
											} else {
												mask[li] = corrs[patt][ipix1] >= min_corr; // only consider the same pattern
											}
										}										
									}
								}
							}
							int [] clusters = tnl.enumerateClusters(
									mask, // boolean [] tiles,
									null,   // int []     num_clusters,
									false); // boolean ordered)
							int center_cluster = clusters[local_center];
							// Copy continuous pixels from the selected pattern
							for (int dy = -corr_radius; dy <= corr_radius; dy++) {
								for (int dx = -corr_radius; dx <= corr_radius; dx++) {
									int li =  tnl.getNeibIndex(local_center, dx, dy); // should be always
									if (li < 0) {
										System.out.println("combineDirCorrs(): BUG2 li<0");
									} else {
										if (clusters[li] == center_cluster) {
											int ipix1 = tn.getNeibIndex(ipix, dx, dy);
											if (ipix1 >= 0) {
												bestcorr[ipix1] = corrs[patt][ipix1];
												bestpatt[ipix1] = patt;
											}
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// combine all lists
		ArrayList<Point> list = new ArrayList<Point>();
		for (int i = 0; i < lists.size(); i++) {
			list.addAll(lists.get(i));
		}
		return list;
	}
	
	/**
	 * Combine results of the phase correlations with different patters
	 * (now with semi-obscured half-circles), using the best (highest value match),
	 * filter by correlation value and suppress weaker maximums by stronger neighbors
	 * @param corrs            array of 2D correlation results [num_pattern][pixel]
	 * @param width            width of the correlation array
	 * @param fullcorr_in      null or double array  [corrs[0].length] will contain
	 *                         full pattern (corrs[0][]) correlation value.
	 * @param bestcorr_in      null or double array  [corrs[0].length] will contain
	 *                         best correlation value from multiple patterns of corrs[][].
	 * @param bestpatt_in      null or integer array  [corrs[0].length] will contain
	 *                         number of the best pattern. 
	 * @param min_corr minimal correlation value to use (lower - replace with 0)
	 * @param min_corr_full    correlation with full pattern still needs to exceed this
	 *                         even if correlation with partial (obscured) pattern will
	 *                         be used
	 * @param full_preference  when looking for the best pattern to match, increase correlation
	 *                         with full pattern
	 * @param max_min_ratio    ratio of halves best/worst to use half-pattern                         
	 * @param combine_full     multiply best pattern correlation by relative (to the center pixel)
	 *                         correlation by the full pattern correlation 
	 * @param adv_radius       suppress weaker local maximums near the stronger one 
	 * @param corr_radius      maximal correlation peak radius (zero out outside).
	 *                         Should be <= adv_radius to avoid thread races 
	 * @return ArrayList of integer pairs - pixel index (x+y*width) and the pattern index
	 */
	public static ArrayList<Point> combineDirCorrs (
			final double [][] corrs,
			final int         width,
			final double []   fullcorr_in,
			final double []   bestcorr_in,
			final int []      bestpatt_in,
			final double      min_corr,
			final double      min_corr_full,
			final double      full_preference,
			final double      max_min_ratio, // ratio of halves best/worst to use half-pattern
			final boolean     combine_full,    // multiply by normalized full pattern correlation maximum
			final double      adv_radius,
			final int         corr_radius) { 
		final int height = corrs[0].length/width;
		final int full_patt_indx = 0; 
		final double [] fullcorr = (fullcorr_in != null) ? fullcorr_in: (new double [corrs[0].length]); 
		// before adversarial filtering
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final TileNeibs tn =  new TileNeibs(width, height);
		final AtomicInteger ati = new AtomicInteger(0);
		ArrayList<ArrayList<Point>> lists = new ArrayList<ArrayList<Point>>();
		for (int i = 0; i < threads.length; i++) {
			lists.add(new ArrayList<Point>());
		}
		// create fullcorr and list using only full correlation. Later will deal with obscured patterns
		final int iradius = (int) Math.ceil(adv_radius);
		final double adv_radius2 = adv_radius * adv_radius;
		final double corr_radius2 = corr_radius*corr_radius;
		
		final double [] bestcorr = (bestcorr_in != null) ? bestcorr_in: (new double [corrs[0].length]);
		final int []    bestpatt = (bestpatt_in != null) ? bestpatt_in: (new int    [corrs[0].length]);
		Arrays.fill(bestpatt, -1);
		final int dbg_pix =  -2827823; // 3649679;// #3 
		final int dbg_pix1 = -3649679;// #3 
		final int dbg_pix0=  1439210; // -2827822; // 3649679;// #3 
		final int dbg_pix2 = 1442449; // -2831060; 
		final int dbg_pix3 = 1439211; // 2827822; 
		final int dbg_x = 1445;
		final int dbg_y = 2338;
		final int dbg_tolerance = -10;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					final TileNeibs tn =  new TileNeibs(width, height);
					int corr_diameter = 2*corr_radius+1;
					TileNeibs tnl =  new TileNeibs(corr_diameter,corr_diameter);
					int local_center = (2 * corr_radius + 2) * corr_radius;
					boolean [] mask = new boolean [corr_diameter*corr_diameter];

					for (int ipix = ai.getAndIncrement(); ipix < fullcorr.length; ipix = ai.getAndIncrement()) if (corrs[full_patt_indx][ipix] >= min_corr_full) {
						boolean dbg= (ipix==dbg_pix) || (ipix==dbg_pix1) || (ipix==dbg_pix2) || (ipix==dbg_pix3);
						if (dbg) {
							System.out.println("combineDirCorrs(): ipix="+ipix);
						}
						is_max: {
							for (int dir = 0; dir < TileNeibs.DIRS; dir++) {
								int ipix1 = tn.getNeibIndex(ipix, dir);
								if ((ipix1 >= 0) && (corrs[full_patt_indx][ipix1] > corrs[full_patt_indx][ipix])){
									break is_max;
								}
							}
							if (dbg_tolerance >= 0) {
								int dbg_px = ipix % width;
								int dbg_py = ipix / width;
								if ((Math.abs(dbg_px-dbg_x) <= dbg_tolerance) || (Math.abs(dbg_py-dbg_y) <= dbg_tolerance)) {
									System.out.println("combineDirCorrs(): ipix="+ipix+
											", dbg_px="+dbg_px+", dbg_py="+dbg_py);
									System.out.println();
								}
							}
							// compare with others in adversarial radius, if equal - use higher index ipix
							for (int dy = -iradius; dy <= iradius; dy++) {
								for (int dx = -iradius; dx <= iradius; dx++) {
									double r2 = dx*dx + dy*dy;
									if ((r2 < adv_radius2) && (r2 > 2.1)) {
										int ipix1 = tn.getNeibIndex(ipix, dx, dy);
										if (ipix1 >= 0) {
											if (corrs[full_patt_indx][ipix1] > corrs[full_patt_indx][ipix]) {
												break is_max;
											} else if ((corrs[full_patt_indx][ipix1] == corrs[full_patt_indx][ipix]) && (ipix1 < ipix)) {
												break is_max;
											}
										}
									}
								}								
							}
							//corrs[full_patt_indx]
							// find best patter here, using full_preference;
							//max_min_ratio, // ratio of halves best/worst to use half-pattern
							int patt = full_patt_indx;
							double best_val = full_preference*corrs[full_patt_indx][ipix];
							double max_val = corrs[1][ipix];
							double min_val = max_val;
							int max_indx = 1;
							for (int i = 1; i < corrs.length; i++) {
								if (corrs[i][ipix] > best_val) {
									best_val = corrs[i][ipix];
									patt = i;
								}
								if (corrs[i][ipix] > max_val) {
									max_val = corrs[i][ipix];
									max_indx = i;
								}
								if (corrs[i][ipix] < min_val) {
									min_val = corrs[i][ipix]; 
								}
							}
							if ((min_val == 0) ||(max_val/min_val > max_min_ratio)) {
								patt = max_indx;
							}
							if (corrs[patt][ipix] < min_corr) {
								break is_max; // too weak maximum
							}
//							int patt = bestpatt0[ipix];
							lists.get(thread_num).add(new Point(ipix,patt));
							// Below should be no conflicts as no intersect
							Arrays.fill(mask, false);
							for (int dy = -corr_radius; dy <= corr_radius; dy++) {
								for (int dx = -corr_radius; dx <= corr_radius; dx++) {
									double r2 = dx*dx + dy*dy;
									if (r2 < corr_radius2) {
										int ipix1 = tn.getNeibIndex(ipix, dx, dy);
										// TODO: mark mask with continuous pixels
										if (ipix1 >= 0) {
											int li =  tnl.getNeibIndex(local_center, dx, dy); // should be always
											if (li < 0) {
												System.out.println("combineDirCorrs(): BUG1 li<0");
											} else {
												mask[li] = (corrs[patt][ipix1] >= min_corr) && // only consider the same pattern
														 (corrs[full_patt_indx][ipix1] >= min_corr_full);
											}
										}										
									}
								}
							}
							int [] clusters = tnl.enumerateClusters(
									mask, // boolean [] tiles,
									null,   // int []     num_clusters,
									false); // boolean ordered)
							if (dbg) {
								String[] dbg_titles={"mask","cluster"};
								double [][] dbg_img = new double [2][mask.length];
								for (int i = 0; i < mask.length; i++) {
									dbg_img[0][i] = mask[i]? corrs.length:0;
									dbg_img[1][i] = clusters[i];
								}
								ShowDoubleFloatArrays.showArrays(
										dbg_img,
										corr_diameter,
										corr_diameter,
										true,
										"clusters_"+ipix,
										dbg_titles);
							}

							int center_cluster = clusters[local_center];
							// Copy continuous pixels from the selected pattern
							for (int dy = -corr_radius; dy <= corr_radius; dy++) {
								for (int dx = -corr_radius; dx <= corr_radius; dx++) {
									int li =  tnl.getNeibIndex(local_center, dx, dy); // should be always
									if (li < 0) {
										System.out.println("combineDirCorrs(): BUG2 li<0");
									} else {
										if (clusters[li] == center_cluster) {
											int ipix1 = tn.getNeibIndex(ipix, dx, dy);
											if (ipix1 == dbg_pix0) {
												System.out.println("ipix1="+ipix1);
											}
											if (ipix1 >= 0) {
												bestcorr[ipix1] = corrs[patt][ipix1];
												if (combine_full) {
													bestcorr[ipix1] *= Math.max(0, corrs[full_patt_indx][ipix1]/corrs[full_patt_indx][ipix]);
												}
												bestpatt[ipix1] = patt;
											}
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// combine all lists
		ArrayList<Point> list = new ArrayList<Point>();
		for (int i = 0; i < lists.size(); i++) {
			list.addAll(lists.get(i));
		}
		return list;
	}
	
	/**
	 * Split correlation filter results into per-pattern ones before convolution.
	 * Temporary solution as convolution is needed only for a few found maximums
	 * @param num_patterns number of patters (should match bestpatt maximal value +1)
	 * @param bestcorr best correlation values
	 * @param bestpatt best pattern index matching bestcorr
	 * @return [num_patterns][] arrays of per-pattern correlation results to be convolved
	 *         with individual patterns
	 */
	public static double [][] splitPattenCorrelations(
			final int         num_patterns,
			final double []   bestcorr,
			final int []      bestpatt){
		final double [][] corrs = new double [num_patterns][bestcorr.length];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < bestcorr.length; ipix = ai.getAndIncrement()) if (bestcorr[ipix] > 0){
						corrs[bestpatt[ipix]][ipix] = bestcorr[ipix];
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return corrs;
	}

	public static double [] mergePattenConvolutions(
			final double [][] convs){
		final double [] combo_conv = new double [convs[0].length];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ipix = ai.getAndIncrement(); ipix < combo_conv.length; ipix = ai.getAndIncrement()){
						for (int n = 0; n < convs.length; n++) {
							combo_conv[ipix] += convs[n][ipix];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return combo_conv;
	}
	
	
	public static void testPatternGenerate() {
		int       half_size = 100;
		boolean   half_pix = false; // center between pixels
		boolean   evening_mode = true; // false; // glare around object, use radius 
		double    radius =   30; // 32; // 32;
		double    edge=      15; //  4;
		double    radius_in= 15; // if 0 - skip
		double    edge_in =   8;
		double    scale_in = -0.05;
		double    radius_out= 42;
		double    edge_out =  6;
		double    scale_out = 1.5;
		double    scale =  -200; // black 200
		
		boolean   normalize = false;
		int       halves_number = 8; // hidden mines - number of diameter cuts
		double    cut_frac =  0.6; // half-cut width fraction of diameter 
		
		int       corr_size = 128;
		int       zoomout =   2;
		boolean   out_normalize = false; // true;
		
		GenericJTabbedDialog gd0 = new GenericJTabbedDialog("Create circular pattern",1200,600);
		gd0.addCheckbox    ("Evening mode",         evening_mode, "True - evening (cooling down), false - morning (warming up).");
		gd0.showDialog();
		if (gd0.wasCanceled()) return;
		evening_mode=        gd0.getNextBoolean();
		if (evening_mode) {
			radius =   30; // 32; // 32;
			edge=       8; // 8; //  4;
			radius_in= 10; // if 0 - skip
			edge_in =   8;
			scale_in = 0; // -0.1; // 7;
			radius_out= 45; // 42;
			edge_out =  30; // 8;
			scale_out =-1.0; // relative to scale
			scale =   -60; // black 200
		} else {
			radius_out= 42;
			edge_out =  6;
			scale_out =.08;
			radius =   30; // 32; // 32;
			edge=      10; //  4;
			radius_in= 15; // if 0 - skip
			edge_in =   8;
			scale_in = -0.05;
			scale =  -170; // black 200
		}
		
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Create circular pattern",1200,600);
		gd.addNumericField("Half pattern size",    half_size,  0,4,"pix", "Pattern will be square, twice this side.");
		gd.addCheckbox    ("Pattern center between pixels",      half_pix, "If false - center will be at integer pixel X/Y.");
//		gd.addCheckbox    ("Evening mode",         evening_mode, "True - evening (cooling down), false - morning (warming up).");
		gd.addNumericField("Radius at half height",  radius,  3,7,"pix",	".");
		gd.addNumericField("Edge width",  edge,  3,7,"pix",	"Transition from full on to zero.");
		gd.addNumericField("Optional inner feature radius",  radius_in,  3,7,"pix",	"Put 0 if not needed.");
		gd.addNumericField("Inner feature edge",   edge_in,  3,7,"pix",	"Transition for the inner feature.");
		gd.addNumericField("Inner feature scale",  scale_in,  3,7,"",	"Relative to the main feature.");
		gd.addNumericField("Optional outer feature radius",  radius_out, 3,7,"pix",	"Put 0 if not needed.");
		gd.addNumericField("Outer feature edge",   edge_out,  3,7,"pix","Transition for the outer feature.");
		gd.addNumericField("Outer feature scale",  scale_out,  3,7,"",	"Relative to the main feature.");
		gd.addNumericField("Overall pattern scale",  scale,  3,7,"",	"Multiply pattern by this number.");
		gd.addCheckbox    ("Normalize",      normalize, "Make sum of all pixel equal to 1.0");
		gd.addMessage("--- obscured round objects, trying halves ---");
		gd.addNumericField("Halves directions",    halves_number,  0,4,"", "Number of directions for diameter cuts for obscured objects.");
		gd.addNumericField("Cut relative widths",  cut_frac,  3,7,"",	"Width of diameter cuts as a fraction of diameter.");
		
		gd.addMessage("--- testing scaling/cropping/padding ---");

		gd.addNumericField("Output size (power of 2)", corr_size,  0,4,"pix", "Output size for correlation.");
		gd.addNumericField("Zoom out factor",          zoomout,  0,4,"", "Zoom out factor for correlation.");
		gd.addCheckbox    ("Normalize zoomed out",     out_normalize, "Make sum of all pixel equal to 1.0");
		
		
		gd.showDialog();
		if (gd.wasCanceled()) return;
		half_size=     (int) gd.getNextNumber();
		half_pix=            gd.getNextBoolean();
//		evening_mode=        gd.getNextBoolean();
		radius=              gd.getNextNumber();
		edge=                gd.getNextNumber();
		radius_in=           gd.getNextNumber();
		edge_in=             gd.getNextNumber();
		scale_in=            gd.getNextNumber();
		radius_out=          gd.getNextNumber();
		edge_out=            gd.getNextNumber();
		scale_out=           gd.getNextNumber();
		scale=               gd.getNextNumber();
		normalize=           gd.getNextBoolean();
		
		halves_number= (int) gd.getNextNumber();
		cut_frac=            gd.getNextNumber();

		corr_size=     (int) gd.getNextNumber();
		zoomout=       (int) gd.getNextNumber();
		out_normalize=       gd.getNextBoolean();
		
		double [] kernel =  getConvolutionKernel();
		int size = 2*half_size;
		double [] pattern = generateTrippleCircularPattern( // generateDoubleCircularPattern (
				half_size,  // int       half_size,
				half_pix,   // boolean   half_pix, // center between pixels
				radius,     // double    radius,
				edge,       // double    edge,
				radius_in,  // double    radius_in, // if 0 - skip
				edge_in,    // double    edge_in,
				scale_in,   // double    scale_in)
				radius_out, // double    radius_out, // if 0 - skip
				edge_out,   // double    edge_out,
				scale_out); // double    scale_out)
		
		if (normalize) {
			double sum_pix = 0;
			for (int i = 0; i < pattern.length; i++) {
				sum_pix+=pattern[i];
			}
			scale = 1.0/sum_pix;
		}
		for (int i = 0; i < pattern.length; i++) {
			pattern[i]*=scale;
		}
		double [][] patterns = new double [halves_number+1][];
		double xc= half_size + (half_pix? 0.5:0);
		double yc= half_size + (half_pix? 0.5:0);
		for (int n = 0; n < patterns.length; n++) {
			patterns[n] = pattern.clone();
			if (n > 0) {
				double phi = 2*Math.PI*(n - 1)/halves_number;
				double ck = Math.cos(phi)/(cut_frac * radius);
				double sk = Math.sin(phi)/(cut_frac * radius);
				for (int iy = 0; iy< size; iy++) {
					double dy = iy - yc;
					for (int ix = 0; ix < size; ix++) {
						double dx = ix - xc; 
						double l = dx*ck+dy*sk;
						if (l <= -1) {
							patterns[n][iy* size + ix] = 0;  
						} else if (l < 1) {
							patterns[n][iy* size + ix] *=	0.5*(1 + Math.sin(l* Math.PI/2));
						}
					}
				}
			}
		}
		
		String pattern_name = "patterns_r"+radius+"_e"+edge+
				"_ir"+radius_in+ "_ie"+edge_in+ "_is"+scale_in+
				"_or"+radius_out+"_oe"+edge_out+"_os"+scale_out+
				"_h"+halves_number+"_w"+cut_frac+
				"_s"+scale+	"_"+size+"x"+size;
		String [] pattern_titles = new String [patterns.length];
		pattern_titles[0] = "full";
		for (int n = 1; n < pattern_titles.length; n++) {
			pattern_titles[n] = String.format("%5.1f deg", 360.0*(n-1)/halves_number);
		}
//		 public static ImagePlus makeArrays(double[][] pixels, int width, int height,  String title, String [] titles) {
		
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				patterns, // float[] pixels,
				size,
				size,
				pattern_name,
				pattern_titles);
		imp.show();
		double [][] corr_patterns = new double [patterns.length][];
		for (int n = 0; n < corr_patterns.length; n++) {
			corr_patterns[n] = patternZoomCropPad(
					patterns[n], // double [] pattern,
					size, // int       pattern_size,
					corr_size, // int       size,
					zoomout, // int       zoomout,
					out_normalize); // boolean   normalize) 
			if (kernel != null) {
				corr_patterns[n] = convolveWithKernel(
						corr_patterns[n],  // final double [] data,
						kernel,       // final double [] kernel,
						corr_size); // final int width)
			}
		}
		
		ShowDoubleFloatArrays.showArrays(
				corr_patterns,
				corr_size,
				corr_size,
				true,
				"corr_patterns_h"+halves_number+"_w"+cut_frac+
				"_"+corr_size+"x"+corr_size+"-zoomout"+zoomout,
				pattern_titles);
		System.out.println("testPatternGenerate(): Patterns created");
	}
	
	public static double [] patternZoomCropPad(
			double [] pattern,
			int       pattern_size,
			int       size,
			int       zoomout,
			boolean   normalize) {
		
		double [] pout = new double[size*size];
		int xyc = size/2;
		int pxyc = pattern_size/2;
		double sum_pix = 0.0;
		for (int y = 0; y < size; y++) {
			int py = pxyc + (y-xyc)*zoomout;
			if ((py >=0) && (py < pattern_size)) {
				for (int x = 0; x < size; x++) {
					int px = pxyc + (x-xyc)*zoomout;
					if ((px >=0) && (px < pattern_size)) {
						double d = pattern[py*pattern_size + px];
						pout[y*size+x] = d;
						sum_pix += d;
					}
				}
			}
		}
		if (normalize) {
//			double scale = 1.0/sum_pix;
			double scale = -500000.0/sum_pix; //TODO: calculate better
			for (int i = 0; i < pout.length; i++) {
				pout[i] *= scale;
			}
		}
		return pout;
	}
	
	
	
	public static double [] generateDoubleCircularPattern (
			int       half_size,
			boolean   half_pix, // center between pixels
			double    radius,
			double    edge,
			double    radius_in, // if 0 - skip
			double    edge_in,
			double    scale_in) {
		double [] pattern = generateCircularPattern (
				half_size, // int       half_size,
				half_pix,  // boolean   half_pix, // center between pixels
				radius,    // double    radius,
				edge);     // double    edge) 
		if (radius_in > 0) {
			double [] pattern1 = generateCircularPattern (
					half_size, // int       half_size,
					half_pix,  // boolean   half_pix, // center between pixels
					radius_in, // double    radius,
					edge_in);  // double    edge)
			for (int i = 0; i < pattern.length; i++) {
				pattern[i] += scale_in*pattern1[i];
			}
		}
		return pattern;
	}
	
	public static double [] generateTrippleCircularPattern (
			int       half_size,
			boolean   half_pix, // center between pixels
			double    radius,
			double    edge,
			double    radius_in, // if 0 - skip
			double    edge_in,
			double    scale_in,
			double    radius_out, // if 0 - skip
			double    edge_out,
			double    scale_out) {
		double [] pattern = generateCircularPattern (
				half_size, // int       half_size,
				half_pix,  // boolean   half_pix, // center between pixels
				radius,    // double    radius,
				edge);     // double    edge) 
		if (radius_in > 0) {
			double [] pattern1 = generateCircularPattern (
					half_size, // int       half_size,
					half_pix,  // boolean   half_pix, // center between pixels
					radius_in, // double    radius,
					edge_in);  // double    edge)
			for (int i = 0; i < pattern.length; i++) {
				pattern[i] += scale_in*pattern1[i];
			}
		}
		if (radius_out > 0) {
			double [] pattern2 = generateCircularPattern (
					half_size, // int       half_size,
					half_pix,  // boolean   half_pix, // center between pixels
					radius_out, // double    radius,
					edge_out);  // double    edge)
			for (int i = 0; i < pattern.length; i++) {
				pattern[i] += scale_out*pattern2[i];
			}
		}
		return pattern;
	}
	
	public static double [] generateCircularPattern (
			int       half_size,
			boolean   half_pix, // center between pixels
			double    radius,
			double    edge) {
		double min_r = radius - edge/2;
		double max_r = radius + edge/2;
		double [] profile = new double [(int)Math.ceil(max_r)+1];
		for (int i = 0; i < profile.length; i++) {
			if (i < min_r) {
				profile[i] = 1;
			} else if (i < max_r) {
				profile[i] = 0.5 * (1.0 +Math.cos(Math.PI *(i-min_r)/(max_r-min_r)));
			}
		}
		return generateRotationalPattern (
				half_size, // int       half_size,
				half_pix,  // boolean   half_pix, // center between pixels 
				profile);  // double [] profile)
	}
	
	
	
	public static double [] generateRotationalPattern (
			int       half_size,
			boolean   half_pix, // center between pixels 
			double [] profile) {
		int size = 2 * half_size;
		double [] pattern = new double [size*size];
		double xc= half_size + (half_pix? 0.5:0);
		double yc= half_size + (half_pix? 0.5:0);
		for (int iy = 0; iy< size; iy++) {
			for (int ix = 0; ix < size; ix++) {
				double r = Math.sqrt((ix-xc)*(ix-xc) + (iy-yc)*(iy-yc));
				int ir = (int) Math.floor(r);
				if (ir < profile.length) {
					double d0 = profile[ir];
					double d1 = (ir < (profile.length-1))?profile[ir+1] : 0;
					double fr = r - ir;
					pattern[iy* size + ix] = fr * d1 + (1-fr) * d0;
				}
			}
		}
		return pattern;
	}
	
	/**
	 * Decomposing linear transform into rotation
	 * R1={{cos(beta),sin(beta)},{-sin(beta), cos(beta)}} ,
	 * non-uniform scale transform W={{w1,0}{0,w2}}, and rotation
	 * R2={{cos(gamma),sin(gamma)},{-sin(gamma), cos(gamma)}}
	 * A=R1*W*R2 using singular value decomposition -- tested
	 * https://math.stackexchange.com/questions/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation   
	 * @param A - input 2x2 matrix
	 * @return {s,beta,w,gamma,beta+gamma}
	 * A = B + C
	 * b00= b11; c00=-c01
	 * b10=-b01; c10= c01
	 */
	public static double [] singularValueDecompose(
			double [][] A) {
		double a00=A[0][0],a01=A[0][1],a10=A[1][0],a11=A[1][1];
		double b00=(a00+a11)/2; // , b11 = b00;
		double c00=(a00-a11)/2; //, c11 =-c00;
		double b01=(a01-a10)/2; //, b10 =-b01;
		double c01=(a01+a10)/2; //, c10 = c01;
		double w1_p_w2_2= Math.sqrt(b00*b00+b01*b01);
		double w1_m_w2_2= Math.sqrt(c00*c00+c01*c01);
		double w1 = w1_p_w2_2 + w1_m_w2_2;
		double w2 = w1_p_w2_2 - w1_m_w2_2;
		double g_p_b = Math.atan2(b01, b00);
		double g_m_b = Math.atan2(c01, c00);
		double gamma = (g_p_b + g_m_b)/2; 
		double beta =  (g_p_b - g_m_b)/2; 
		return new double [] {beta,w1,w2,gamma,g_p_b};
	}
	/**
	 * Use singular value decomposition and then split scaling {{w1,0},{0,w1}}
	 * into overall scaling caused by zoom != 1.0 because of altitude error
	 * and unidirectional scaling caused by tilted projection plane. As the
	 * input linear transformation matrix converts ground coordinates to source
	 * image coordinates, the scale in the tilt direction is > than scale in the
	 * perpendicular direction (tilt axis).
	 * Matrix R1 is additionally rotated by PI/2 if needed so W={{w1,0},{0,w2}}
	 * has w2>=w1 and W={{s,0},{0,s/t}}, where t <= 1.0 and equals to cos(tilt) 
	 * 
	 * @param A - linear transformation matrix from rectified ground coordinates
	 *        to source image coordinates. OK to use 2x3 affine matrix,extra
	 *        components will be ignored. 
	 * @param y_down_ccw - positive Y is down, positive angles are CCW
	 * @return {gamma, s, t, beta+gamma}, beta+gamma - total rotation
	 */
	public static double [] singularValueDecomposeScaleTilt(
			double [][] A,
			boolean y_down_ccw) {
		double [] svd=singularValueDecompose(A);
		double w1 = svd[1], w2 = svd[2], g_p_b=svd[4]; // pure rotation // , beta=svd[0]
		double gamma = svd[3];
		// For Y-down, angles are CW positive, for Y-up angles are CCW positive
		// Affines are Y-up?
		
		// considering tilt in y direction (unless long_axis), it should have higher scale
		// (and source image coordinates), while X should correspond to
		// the axis of rotation, and scale is just scale caused by error in
		// altitude.
		double s = Math.min(w1, w2);
		double t = w1/w2; // <=1.0, ==cos(tilt)
		if (w1 > w2) { // rotate tilt by PI/2
			t = w2/w1;
			gamma += Math.PI/2; // start with rotation (last in matrices)
			if (gamma > Math.PI) {
				gamma -= 2* Math.PI;
			}
		}
		
		if (gamma > Math.PI/2) {
			gamma -= Math.PI;
		} else if (gamma < -Math.PI) {
			gamma += Math.PI;
		}
		
		if (y_down_ccw) {
			gamma =-gamma; 
			g_p_b = -g_p_b; // pure rotation
		}
		
		return new double [] {gamma, s,t, g_p_b};
	}
	
	public static double [] singularValueDecomposeScaleTiltOld(
			double [][] A,
			boolean y_down_ccw) {
		double [] svd=singularValueDecompose(A);
		double w1 = svd[1], w2 = svd[2], beta=svd[0], g_p_b=svd[4];
		// For Y-down, angles are CW positive, for Y-up angles are CCW positive
		// Affines are Y-up?
		if (y_down_ccw) {
			beta =-beta; // find where is the error that beta is negated, check rotation
			g_p_b = -g_p_b;
		}
		
		// considering tilt in y direction, it should have higher scale
		// (and source image coordinates), while X should correspond to
		// the axis of rotation, and scale is just scale caused by error in
		// altitude.
		double s = Math.min(w1, w2);
		double t = w1/w2; // <=1.0, ==cos(tilt)
		if (w1 > w2) { // rotate tilt by PI/2
			t = w2/w1;
			beta += Math.PI/2;
			if (beta > Math.PI) {
				beta -= 2* Math.PI;
			}
		}
		// beta should be in the range of +/-pi/2
		if (beta > Math.PI/2) {
			beta -= Math.PI;
		} else if (beta < -Math.PI) {
			beta += Math.PI;
		}
		return new double [] {beta, s,t, g_p_b};
	}
	
	public static Rectangle getDefinedBounds(
			final double [][] data_slices,
			final int width) { // null or same length as data_slices[i]
		final int height = data_slices[0].length/width;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger ati = new AtomicInteger(0);
		final int [][][] bounds =  new int [threads.length][2][2]; // [thread][x/y][min/max]
		for (int i = 0; i < bounds.length; i++) {
			bounds[i][0][0] = width; // min x
			bounds[i][1][0] = height;// min y
			bounds[i][0][1] = -1;    // max x
			bounds[i][1][1] = -1;    // max y
		}
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int thread_num = ati.getAndIncrement();
					for (int ipix = ai.getAndIncrement(); ipix < data_slices[0].length; ipix = ai.getAndIncrement()) {
						pix_loop: {
							for (int i = 0; i < data_slices.length; i++) {
								if (Double.isNaN(data_slices[i][ipix])) {
									break pix_loop;
								}
							}
							int px = ipix % width;
							int py = ipix / width;
							if (px < bounds[thread_num][0][0]) bounds[thread_num][0][0] = px; 
							if (px > bounds[thread_num][0][1]) bounds[thread_num][0][1] = px; 
							if (py < bounds[thread_num][1][0]) bounds[thread_num][1][0] = py; 
							if (py > bounds[thread_num][1][1]) bounds[thread_num][1][1] = py; 
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		for (int i = 1; i < bounds.length; i++) {
			for (int j = 0; j < 2; j++) {
				bounds [0][j][0] = Math.min(bounds [0][j][0], bounds [i][j][0]);
				bounds [0][j][1] = Math.max(bounds [0][j][1], bounds [i][j][1]);
			}
		}
		Rectangle rbounds = new Rectangle(
				bounds[0][0][0],
				bounds [0][1][0],
				bounds[0][0][1]-bounds[0][0][0]+1,
				bounds[0][1][1]-bounds[0][1][0]+1);
		return rbounds;
	}
	
 	public static double [] subtractWoi(
			final double [] data0,
			final double [] data1,
			final int       width,
			Rectangle woi_in) {
		final int height = data0.length/width;
		if (woi_in == null) {
			woi_in =new Rectangle(0,0,width,height);
		}
		final Rectangle woi = new Rectangle(woi_in);
		final int woi_len = woi.width*woi.height;
		final double [] data_out =new double [woi_len];
		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 < woi_len; iPix = ai.getAndIncrement()) {
						int x = woi.x + (iPix % woi.width);
						int y = woi.y + (iPix / woi.width);
						int nPix = y * width + x;
						data_out[iPix] = data1[nPix] - data0[nPix];
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return data_out;
	}

 	public static double [] extractWoi(
			final double [] data0,
			final int       width,
			Rectangle woi_in) {
		final int height = data0.length/width;
		if (woi_in == null) {
			woi_in =new Rectangle(0,0,width,height);
		}
		final Rectangle woi = new Rectangle(woi_in);
		final int woi_len = woi.width*woi.height;
		final double [] data_out =new double [woi_len];
		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 < woi_len; iPix = ai.getAndIncrement()) {
						int x = woi.x + (iPix % woi.width);
						int y = woi.y + (iPix / woi.width);
						int nPix = y * width + x;
						data_out[iPix] = data0[nPix];
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return data_out;
	}
 	
 	
}
