/**
 **
 ** CuasMotion.java - UAS motion estimation and multi-target tracking via CLT correlation
 **
 ** Copyright (C) 2025-2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CuasMotion.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.cuas;

import java.awt.Color;
import java.awt.Font;
import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.imageio.ImageIO;
import java.nio.file.Paths;
import java.net.URISyntaxException;
import java.net.URL;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.common.DoubleGaussianBlur;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.ims.UasLogReader;
import com.elphel.imagej.tileprocessor.Correlation2d;
import com.elphel.imagej.tileprocessor.ErsCorrection;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TDCorrTile;
import com.elphel.imagej.tileprocessor.TileNeibs;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.process.ColorProcessor;
import ij.process.FloatProcessor;
import ij.process.ImageConverter;
import ij.process.ImageProcessor;

public class CuasMotion {
	final public static String CLEAN_SUFFIX = "-CLEAN";
	final static public  int MAX_ALT_TARGETS = 100; // convenient to be power of 10
	
	final static public String LEGEND_TITLE = "Legend";
	final static public String[] ICON_DESCRIPTIONS = {
			"Identified UAS",
			"Unidentified targets",
			"UAS flight log position (image pane)",
			"UAS flight log position (radar pane)",
			"Target detected position (radar pane)"};
	
	final static public int TARGET_X =                0;
	final static public int TARGET_Y =                1;
	final static public int TARGET_VX =               2;
	final static public int TARGET_VY =               3;
	final static public int TARGET_STRENGTH =         4;
	public static final int TARGET_MISMATCH_BEFORE =  5;
	public static final int TARGET_MISMATCH_AFTER =   6;
	final static public int TARGET_SCORE =            7;
	final static public int TARGET_SEQLEN =           8;
	final static public int TARGET_TRAVEL =           9;
	final static public int TARGET_DISPARITY =       10;
	final static public int TARGET_DISP_DIFF =       11;
	final static public int TARGET_DISP_STR =        12;
	final static public int TARGET_RANGE =           13;
	final static public int TARGET_NTILE =           14;
	final static public int TARGET_ID =              15;
	final static public int TARGET_LENGTH = TARGET_ID+1;
	
	final static public int IMPORTANCE_A =        0;
	final static public int IMPORTANCE_RMS =      1;
	final static public int IMPORTANCE_RMS_A =    2;
	final static public int IMPORTANCE_CENTER =   3;
	final static public int IMPORTANCE_MISMATCH = 4;
	final static public int IMPORTANCE_MATCH_LEN= 5;
	final static public int IMPORTANCE_TRAVEL=    6;
	final static public int IMPORTANCE_LENGTH = IMPORTANCE_TRAVEL + 1;
	
	final static public int TOTALS_ALL =        0;
	final static public int TOTALS_UNDEFINED =  1;
	final static public int TOTALS_GOOD =       2;
	final static public int TOTALS_BAD =        3;
	final static public int TOTALS_LENGTH =     TOTALS_BAD+1;
	
	public static String RESOURCE_GRAPICS_DIR ="graphics";
	public static String ICON_TARGET_1X =      "TDbox_dashed21x21_bold.png";
	public static String ICON_TARGET_2X =      "TDbox_dashed43x43_2px.png";
	public static String [][] TARGET_ICONS = {
			{"TDbox_dashed21x21_bold.png","TDbox_dashed43x43_2px.png"}, // unknown
			{"TDbox21x21.png",            "TDbox43x43_2px.png"},        // selected 
			{"Circle21x21.png",           "Circle43x43_2px.png"},       // friend
			{"diamond21x17.png",          "diamond43x35_2px.png"}       // foe
	};
	public static String ICON_BLUE = "Circle63x63blue.png";
	public static final String OMEGA = "\u03A9";
	public static final String [][] ANNOT_LABELS = { // first filed should be 4 character wide
			{"ID  ",   "Target ID", "Unique number of detected targets."}, // , some targets may be erroneously detected as separate ones."},
			{"DISP", "Disparity", "Raw measured disparity (in pixels), offset by the same \"disparity at infinity\"."},
			{"RNG ",  "Range",     "Distance to the target, calculated from the measured disparity."},
			{"TRNG", "True range","Distance to the test UAS obtained from its flight log."},
			{"AGL ",  "AGL",       "Altitude above ground level."},
			{"AZ  ",  "Azimuth",   "Target azimuth (degrees)."},
			{"EL  ",  "Elevation", "Target elevation (degrees)."},
			{null,    "Conditional Elevation","Display elevation only if altitude (AGL) is undefined due to too far targets."}, // not a label
			{OMEGA+"AZ ","Azimuth rotation","Horizontal apparent target movement in the camera field of view (degrees per second)."},
			{OMEGA+"EL ","Elevation rotation","Vertical apparent target movement in the camera field of view (degrees per second)."},
			{"VS  ", "Vertical Speed","Target vertical speed (m/s)."},
			{"GS  ", "Ground Speed","Horizontal target speed relative to the ground (m/s)."},
			{"HDG ","Heading","Target heading (degrees)."},
			{"ERRb","Segment Mismatch Before","Mismatch from the previous keyframe (now 10 frames @ 60fps) (pix)."},
			{"ERRa","Segment Mismatch After", "Mismatch from the next keyframes (now 10 frames @ 60fps) (pix)."},
			{"S   ",   "Score","Target 2D detection score."},
			{"SEQ ", "Sequence Length","Number of consecutive keyframes where the same target is detected."},
			{"TRV ", "Travel 2D","Bounding box diagonal of the target 2D travel in consecutive frames (pix)."}};

	public static final int ANNOT_ID =    0; // target ID (global index)
	public static final int ANNOT_DISP =  1; // disparity
	public static final int ANNOT_RANGE = 2; // range
	public static final int ANNOT_TRANG = 3; // true range
	public static final int ANNOT_AGL =   4; // altitude AGL
	public static final int ANNOT_AZ =    5; // azimuth
	public static final int ANNOT_EL =    6; // elevation
	public static final int ANNOT_ELNOAGL=7; // elevation if no AGL is available. should be after ANNOT_AGL
	public static final int ANNOT_OMEGA_AZ = 8; // omega_az
	public static final int ANNOT_OMEGA_EL = 9; // omega_el
	public static final int ANNOT_VS =   10; // vertical speed
	public static final int ANNOT_GS =   11; // ground speed
	public static final int ANNOT_HDG =  12; // heading
//	public static final int ANNOT_MISM = 13; // Mismatch before/after
	public static final int ANNOT_ERRB = 13; // Mismatch before
	public static final int ANNOT_ERRA = 14; // Mismatch after
	public static final int ANNOT_SCORE =15; // Score
	public static final int ANNOT_SEQ =  16; // Sequence length 
	public static final int ANNOT_TRV =  17; // Sequence travel (pixel diagonal of the bounding box of travel)
	/*
	public static final int ANNOT_CLEAN =
			(1 << ANNOT_ID) |
			(1 << ANNOT_RANGE) |
			(1 << ANNOT_TRANG) |
			(1 << ANNOT_AGL) |
			(1 << ANNOT_AZ) |
			(1 << ANNOT_ELNOAGL) |
			(1 << ANNOT_VS) |
			(1 << ANNOT_GS) |
			(1 << ANNOT_HDG);
	*/
	public static final int [] ANNOT_DEFAULTS = {
				(1 << ANNOT_ID) |
				(1 << ANNOT_DISP) |
				(1 << ANNOT_RANGE) |
				(1 << ANNOT_TRANG) |
				(1 << ANNOT_AGL) |
				(1 << ANNOT_AZ) |
				(1 << ANNOT_EL) |
				(1 << ANNOT_ELNOAGL) |
				(1 << ANNOT_OMEGA_AZ) |
				(1 << ANNOT_OMEGA_EL) |
				(1 << ANNOT_VS) |
				(1 << ANNOT_GS) |
				(1 << ANNOT_HDG) |				
				(1 << ANNOT_ERRB) |				
				(1 << ANNOT_ERRA) |				
				(1 << ANNOT_SCORE) |				
				(1 << ANNOT_SEQ) |				
				(1 << ANNOT_TRV),
				
				(1 << ANNOT_ID) |
				(1 << ANNOT_DISP) |
				(1 << ANNOT_RANGE) |
				(1 << ANNOT_TRANG) |
				(1 << ANNOT_AGL) |
				(1 << ANNOT_AZ) |
				(1 << ANNOT_ELNOAGL) |
				(1 << ANNOT_VS) |
				(1 << ANNOT_GS) |
				(1 << ANNOT_HDG)|		
				(1 << ANNOT_SCORE), 				
				
				(1 << ANNOT_ID) |
				(1 << ANNOT_RANGE) |
				(1 << ANNOT_TRANG) |
				(1 << ANNOT_AGL) |
				(1 << ANNOT_AZ) |
				(1 << ANNOT_ELNOAGL) |
				(1 << ANNOT_VS) |
				(1 << ANNOT_GS) |
				(1 << ANNOT_HDG)				
	};
	
	/*
	public static final char ANNOT_IMAGE_PANE = 'I';		
	public static final char ANNOT_RADAR_PANE = 'R';		
	public static final char ANNOT_CLEAN_VIEW = '+';
	*/
	public static final int  ANNOT_IMAGE_PANE_INDX = 0;		
	public static final int  ANNOT_RADAR_PANE_INDX = 1;		
	public static final int  ANNOT_CLEAN_VIEW_INDX = 2;		
	public static final int  ANNOT_VIEWS = ANNOT_CLEAN_VIEW_INDX+1;
	public static final String ANNOTS = "IR+"; // Image pane, Radar pane, clean mask 
	public static final String ANNOT_TIP = "\"I\" - show in image pane, \"R\" - show in radar pane, \"+\" - show in clean view (mask for both panes)";	
	
	// target data files analysis (GUI and future MCP)
	public static final String [] ANLZ_MODES = {"section","non-empty","non_NaN","==",">=","<="};
	public static final int       ANLZ_SECT =    0; // generate section 
	public static final int       ANLZ_NEMPTY =  1; // list tiles with targets
	public static final int       ANLZ_DEFINED = 2; // list tiles with targets with non-NaN parameter
	public static final int       ANLZ_EQ =      3; // list tiles with targets with parameter equal (such as FAILURE==0)
	public static final int       ANLZ_GE =      4; // list tiles with targets with parameter >= value (such as AMPLITUDE >= 1.0)
	public static final int       ANLZ_LE =      5; // list tiles with targets with parameter <= value (such as RMSE <= 0.5)

	
	public static void setCuasAnnots(
			String annot,
			int    indx,
			int [] annots) {
		for (int i = 0; i < ANNOT_VIEWS; i++) {
			annots[i] &= ~(1 << indx);
			if (annot.contains(ANNOTS.substring(i,i+1))) {
				annots[i] |= (1 << indx);
			}
		}
	}
	
	public static String getCuasAnnot (
			int    indx,
			int [] annots) {
		String annot = "";
		for (int i = 0; i < ANNOT_VIEWS; i++) {
			if ((annots[i] & (1 << indx)) != 0) {
				annot+= ANNOTS.substring(i,i+1);
			}
		}
		return annot;
	}

	private final  GPUTileProcessor gpuTileProcessor;
	private CLTParameters           clt_parameters=null;
	private QuadCLT                 parentCLT =  null;
	private double []               noise_map =  null;
	private double []               noise_map8 = null;
	private int                     debugLevel = 0;
	private GpuQuad                 gpuQuad = null;
	private ImageDtt                image_dtt = null;
	private int                     gpu_max_width;         
	private int                     gpu_max_height;
	private int                     tilesX;
	private int                     tilesY;
	private double []               sky_mask = null;
	private int                     corr_offset;
	private int                     corr_pairs;
	private UasLogReader            uasLogReader;
	private String []               scene_titles = null;
	private String []               slice_titles = null;
	private double [][][]           targets = null;
	private int                     start_frame = 0;
	
	
	public double [] getNoiseMap () {
		return noise_map;
	}

	public double [] getNoiseMap8 () {
		return noise_map8;
	}
	
	public UasLogReader getUasLogReader() {
		return uasLogReader;
	}
	
	private boolean slow_targets = false;

	public int getSeqLength() {
		return clt_parameters.imp.cuas_corr_offset + clt_parameters.imp.cuas_corr_pairs;
	}
	
	public static int getCorrInc(
			CLTParameters  clt_parameters) {
		return clt_parameters.imp.cuas_half_step ? (clt_parameters.imp.cuas_corr_offset/2) : clt_parameters.imp.cuas_corr_offset;
	}

	/**
	 * Geometric separation between adjacent keyframes in scene units.
	 *
	 * When {@code cuas_half_step} is enabled and {@code cuas_corr_offset} is odd, the
	 * actual scan index advance returned by {@link #getCorrInc(CLTParameters)} is truncated
	 * to an integer scene step. For chaining targets between neighboring keyframes we need
	 * the intended half-offset geometry rather than the truncated array index increment.
	 */
	public static double getCorrIncExact(
			CLTParameters clt_parameters) {
		return clt_parameters.imp.cuas_half_step ? (0.5 * clt_parameters.imp.cuas_corr_offset) : clt_parameters.imp.cuas_corr_offset;
	}
	
	public int getCorrInc() { // == frame_step;
		return getCorrInc(clt_parameters);
//		return clt_parameters.imp.cuas_half_step ? (clt_parameters.imp.cuas_corr_offset/2) : clt_parameters.imp.cuas_corr_offset;
	}
	public int getNumCorrSamples() {
		return slice_titles.length;
	}
	
	public int getFrameCenter(int nseq) {
		int frame0 = start_frame + getSeqLength()/2;
		int frame_center = frame0 + nseq * getCorrInc();
		return frame_center;
	}
	
	public double getInterseqScale() {   // multiply target velocity to get offset in the middle between the key frames
		return 0.5 * getCorrIncExact(clt_parameters) / corr_offset;
	}
	
	/**
	 * Multiply raw {@code RSLT_VX}/{@code RSLT_VY} pair displacement by this factor
	 * to get pixel offset per scene (at ~60Hz).
	 * @param clt_parameters
	 * @return coefficient for VX, VY
	 */
	public static double getFrameVelocityScale(CLTParameters  clt_parameters) {
		return 1.0/clt_parameters.imp.cuas_corr_offset;
	}
	
	
	public String [] getSceneTitles() {
		return scene_titles;
	}
	public String [] getSliceTitles() {
		return slice_titles;
	}
	public double [][][] getTargets(){
		return targets;
	}
	public void setTargets(double [][][] targets) {
		this.targets = targets;
	}
	
	/**
	 * Calculate window for accumulation consecutive scenes around the key frame
	 * @param smooth use cosine window, false - rectangular one
	 * @return window[2*half_range + 1], sum values = 1.0;
	 */
	public double [] getSegmentWindow(
			boolean smooth,
			boolean normalize) {
		final int half_range = getSeqLength()/2;
		final double [] window_full = new double [2*half_range + 1];
		double s0 = 1.0;
		window_full[half_range] = 1.0;
		double k = Math.PI/2/(half_range +0.5);
		for (int i = 1; i <= half_range; i ++) {
			window_full[half_range + i] = smooth ? (Math.cos(i*k)):1.0;
			window_full[half_range - i] = smooth ? (Math.cos(i*k)):1.0;
			s0+= 2 * window_full[half_range+i]; 
		}
		if (normalize) {
			for (int i = 0; i < window_full.length; i ++) {
				window_full[i] /= s0;
			}
		}
		return window_full;
		
	}

	public int getTilesX() {
		return tilesX;
	}
	public int getTilesY() {
		return tilesY;
	}

	
	public CuasMotion (
			CLTParameters     clt_parameters,
			String []         scene_titles,
			QuadCLT           parentCLT,
			float []          fpixels_avg,
			UasLogReader      uasLogReader,
			int               debugLevel) {
//		private double []               noise_map = null;

		this.debugLevel =     debugLevel;
		this.clt_parameters = clt_parameters;
		this.parentCLT =      parentCLT;
		this.scene_titles =   scene_titles;
		final double max_01_diff = 1E-3;
    	System.out.println("Setting up GPU for CuasMotion class");
    	gpuTileProcessor =    parentCLT.getGPUQuad().getGpuTileProcessor();
    	tilesX =              parentCLT.getTilesX();
    	tilesY =              parentCLT.getTilesY();
    	gpu_max_width =       tilesX * GPUTileProcessor.DTT_SIZE;
    	gpu_max_height =      tilesY * GPUTileProcessor.DTT_SIZE;
		corr_offset =         clt_parameters.imp.cuas_corr_offset;
		corr_pairs =          clt_parameters.imp.cuas_corr_pairs;
		double scale_window = clt_parameters.imp.cuas_scale_wnd; //  =     2.0;   //; When building noise map, scale search window range from original -8..+7 (2.0 -> -16..+15)

//		int     start_frame = 0;
		int num_corr_samples = (scene_titles.length - getSeqLength() - start_frame) / getCorrInc();
		slice_titles = new String [num_corr_samples];
		for (int nscan = 0; nscan < num_corr_samples; nscan++) {
			int frame_cent = start_frame + getCorrInc() * nscan + getSeqLength()/2; // debug only
			slice_titles[nscan] =  scene_titles[frame_cent];
		}
		try {
			gpuQuad = new GpuQuad(//single camera
					gpuTileProcessor,   // GPUTileProcessor gpuTileProcessor,
					gpu_max_width,        // final int        max_width,
					gpu_max_height,       // final int        max_height,
					1,                    // final int        num_colors, // normally 1?
					this.clt_parameters.gpu_debug_level);
		} catch (Exception e) {
			System.out.println("Failed to initialize GpuQuad class");
			// TODO Auto-generated catch block
			e.printStackTrace();
			return;
		} // final int debugLevel);
		this.uasLogReader = uasLogReader;
		boolean is_aux =  true;
		boolean is_mono = true;
		boolean is_lwir = true;
		image_dtt = new ImageDtt(
				1,                                       // int numSensors,
				GPUTileProcessor.DTT_SIZE,               // clt_parameters.transform_size,
				clt_parameters.img_dtt,
				is_aux,                                  // ref_scene.isAux(),
				is_mono,                                 // ref_scene.isMonochrome(),
				is_lwir,
				clt_parameters.getScaleStrength(is_aux), // maybe something else
				gpuQuad);
		if ((clt_parameters.imp.cuas_sky_path != null) && (clt_parameters.imp.cuas_sky_path.length() >0)) {
			ImagePlus imp_mask = new ImagePlus(clt_parameters.imp.cuas_sky_path);
			if (imp_mask.getWidth() == 0) {
				System.out.println("CuasMotion(): Failed to read sky mask from "+clt_parameters.imp.cuas_sky_path+", not using mask.");
				clt_parameters.imp.cuas_sky_path = null;
			} else {
				int [] wh = new int[2];
				double [][] masks = ShowDoubleFloatArrays.readDoubleArray(imp_mask, 0, wh);
				if ((wh[0] != gpu_max_width) || (wh[1] != gpu_max_height)) {
					throw new IllegalArgumentException(String.format("Mask image dimensions (%d x %d) do not match those of the image (%d x %d)", wh[0], wh[1], gpu_max_width,gpu_max_height));
				}
				//sky_mask
				double offs_x = clt_parameters.imp.cuas_sky_offset[0];
				double offs_y = clt_parameters.imp.cuas_sky_offset[1];
				double []mask = masks[0];
				if ((offs_x==0) && (offs_y ==0)) {
					sky_mask = masks[0];
				} else { // linear interpolate
					sky_mask = new double [mask.length];
					int [] mask01 = new int [sky_mask.length];
					for (int i = 0; i < mask01.length; i++) {
						if (Math.abs(sky_mask[i]) < max_01_diff) {
							mask01[i] = 0;
						} else if (Math.abs(sky_mask[i] - 1.0) < max_01_diff) {
							mask01[i] = 2;
						} else {
							mask01[i] = 1;
						}
					}
					for (int py = 0; py< gpu_max_height; py++) {
						double my = py +offs_y;
						int imy0 = Math.min(gpu_max_height-1, Math.max(0, ((int) Math.floor(my))));
						int imy1 = Math.min(gpu_max_height-1, Math.max(0, ((int) Math.ceil(my))));
						for (int px = 0; px< gpu_max_width; px++) {
							int indx = px + gpu_max_width * py;
							double mx = px +offs_x;
							int imx0 = Math.min(gpu_max_width-1, Math.max(0, ((int) Math.floor(mx))));
							int imx1 = Math.min(gpu_max_width-1, Math.max(0, ((int) Math.ceil(mx))));
							int ind00 = imx0 + gpu_max_width * imy0; 
							int ind01 = imx1 + gpu_max_width * imy0; 
							int ind10 = imx0 + gpu_max_width * imy1; 
							int ind11 = imx1 + gpu_max_width * imy1; 
							if ((mask01[ind00] == 0) && (mask01[ind01] == 0) && (mask01[ind10] == 0) && (mask01[ind11] == 0)) {
								sky_mask[indx] = 0;
							} else 	if ((mask01[ind00] == 2) && (mask01[ind01] == 2) && (mask01[ind10] == 2) && (mask01[ind11] == 2)) {
								sky_mask[indx] = 1;
							} else {
								double kx = (imx1 > imx0) ? ((mx - imx0)/(imx1-imx0)) :0;
								double ky = (imy1 > imy0) ? ((my - imy0)/(imy1-imy0)) :0;
								sky_mask[indx] =
										(1-ky)* (1-kx) *mask[ind00] +
										(1-ky)* (  kx) *mask[ind01] +
										(  ky)* (1-kx) *mask[ind10] +
										(  ky)* (  kx) *mask[ind11];
							}
						}
					}
				}
			}
		}
		noise_map = getNoiseMap(
				scale_window, // final double scale_window, // 1.0->8, 2.0 - 16.
				fpixels_avg); // final float []          fpixels_avg)
		noise_map8 = getNoiseMap(
				1.0, // final double scale_window, // 1.0->8, 2.0 - 16.
				fpixels_avg); // final float []          fpixels_avg)
		return;
	}
	
	public static void printStats(
			String              s,
			boolean             all,
			int []              num_all,
			int []              num_undef,
			int []              num_good,
			int []              num_bad) {
		int [] totals = getTotals(
				num_all,    // int []   num_all,
				num_undef,  // int []   num_undef,
				num_good,   // int []   num_good,
				num_bad);   // int []   num_bad,
		if (totals == null) {
			return;
		}
		if (s != null) {
			System.out.println(s+" stats:");
		}
		if (num_all != null)   printStatsLine("  all",all,num_all); 
		if (num_undef != null) printStatsLine("undef",all,num_undef); 
		if (num_good != null)  printStatsLine(" good",all,num_good); 
		if (num_bad != null)   printStatsLine("  bad",all,num_bad); 
		return;
	}
	
	private static void printStatsLine(
			String              s,
			boolean             all,
			int []              stats) {
		if (s != null) {
			System.out.print(s+": ");
		}
		int total = 0;
		for (int d:stats) total +=d;
		if (all) {
			System.out.print(String.format("total:%5d ",total));
			for (int nseq = 0; nseq < stats.length; nseq++) {
				System.out.print(nseq+":"+stats[nseq]);
				if (nseq < (stats.length-1)) {
					System.out.print(", ");
				}
			}
		} else {
			System.out.print(String.format("%5d ",total));
		}
		System.out.println();
	}
	public double [] getNoiseMap(
			final double    scale_window, // 1.0->8, 2.0 - 16.
			final float []  fpixels_avg) {
		final double [] noise_map = new double [tilesX*tilesY];
		final int num_tiles = tilesX * tilesY; // 80 * 64 = 5120
		
		// Find next power of 2 for bit reversal mapping
		int max_power_of_two = 1;
		int shift = 32;
		while (max_power_of_two < num_tiles) {
			max_power_of_two <<= 1;
			shift--;
		}
		final int final_shift = shift;
		final int limit_rtile = max_power_of_two;
		
		final int tileSize = GPUTileProcessor.DTT_SIZE; // 8
		final int width = tilesX * tileSize;
		final int height = tilesY * tileSize;
		final int scan_wnd = (int) (tileSize * scale_window);

		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 nRtile = ai.getAndIncrement(); nRtile < limit_rtile; nRtile = ai.getAndIncrement()) {
						// Bit reversal for cache-friendly access spread
						int ntile = Integer.reverse(nRtile) >>> final_shift;
						
						// Because max_power_of_two >= num_tiles, skip indices that map out of bounds
						if (ntile >= num_tiles) {
							continue;
						}

						int tileX = ntile % tilesX;
						int tileY = ntile / tilesX;
						
						// Define the 16x16 pixel window around tile centers at (4 + 8*tileX, 4 + 8*tileY)
						// Scan from (center - 8) to (center + 7) inclusive
						int px0 = Math.max(0,      tileSize * tileX + tileSize / 2 - scan_wnd);
						int px1 = Math.min(width,  tileSize * tileX + tileSize / 2 + scan_wnd);
						int py0 = Math.max(0,      tileSize * tileY + tileSize / 2 - scan_wnd);
						int py1 = Math.min(height, tileSize * tileY + tileSize / 2 + scan_wnd);
						
						double min_val = Double.POSITIVE_INFINITY;
						double max_val = Double.NEGATIVE_INFINITY;
						
						// Scan the pixels in this window
						for (int py = py0; py < py1; py++) {
							int row_offset = py * width;
							for (int px = px0; px < px1; px++) {
								float val = fpixels_avg[row_offset + px];
								if (val < min_val) min_val = val;
								if (val > max_val) max_val = val;
							}
						}
						
						noise_map[ntile] = max_val - min_val;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return noise_map;
	}
	
	
	
	public static int [] getRemain(
			final double [][][] target_sequence,
			int []              num_all_in,
			int []              num_undef_in,
			int []              num_good_in,
			int []              num_bad_in) {
		final int num_seq =   target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final int [] num_all =   (num_all_in !=   null) ? num_all_in :   new int [num_seq];
		final int [] num_good =  (num_good_in !=  null) ? num_good_in :  new int [num_seq];
		final int [] num_bad =   (num_bad_in !=   null) ? num_bad_in :   new int [num_seq];
		final int [] num_undef = (num_undef_in != null) ? num_undef_in : new int [num_seq];
		Arrays.fill(num_all,   0);
		Arrays.fill(num_good,  0);
		Arrays.fill(num_bad,   0);
		Arrays.fill(num_undef, 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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] ts = target_sequence[nSeq][ntile];
							if (ts != null) {
								num_all[nSeq]++;
								double ts_fail = ts[CuasMotionLMA.RSLT_FAIL];
								if (Double.isNaN(ts_fail)) {
									num_undef[nSeq]++;
								} else if (ts_fail == 0) {
									num_good[nSeq]++;
								} else {
									num_bad[nSeq]++;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return getTotals(
				num_all,   // int [] num_all,
				num_undef, // int [] num_undef,
				num_good,  // int [] num_good,
				num_bad);  // int [] num_bad)
	}

	public static int [] getTotals(
			int []              num_all,
			int []              num_undef,
			int []              num_good,
			int []              num_bad) {
		int num_seq = 0;
		if (num_all != null) {
			num_seq = num_all.length;
		} else if (num_undef != null) {
			num_seq = num_undef.length;
		} else if (num_good != null) {
			num_seq = num_good.length;
		} else if (num_bad != null) {
			num_seq = num_bad.length;
		}
		if (num_seq == 0) {
			return null;
		} else {
			int [] totals = new int[TOTALS_LENGTH];
			for (int nseq = 0; nseq < num_seq; nseq++) {
				if (num_all != null) totals[TOTALS_ALL] +=       num_all[nseq];
				if (num_undef != null) totals[TOTALS_UNDEFINED] += num_undef[nseq];
				if (num_good != null) totals[TOTALS_GOOD] +=      num_good[nseq];
				if (num_bad!= null) totals[TOTALS_BAD] +=       num_bad[nseq];
			}
			return totals;
		}
	}

	public static int getTotal(int [] num) {
		int total = 0;
		if (num != null) {
			for (int n:num) {
				total+=n;
			}
		}
		return total;
	}
	
	
	public static int addNewResults(
			final double [][][] target_sequence, // will only process non-nulls here
			final double [][][] new_sequence,
			final int when_iter,
			final boolean overwrite) {
		final int num_seq =   target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger anew = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) if (target_sequence[nSeq][ntile] != null) {
							double [] tg =  target_sequence[nSeq][ntile];
							if (overwrite || Double.isNaN(tg[CuasMotionLMA.RSLT_FAIL])) { // only those not defined yet
								double [] ns = new_sequence[nSeq][ntile];
								if ((ns != null) && !Double.isNaN(ns[CuasMotionLMA.RSLT_FAIL])){ // only defined as good/bad
									for (int i = 0; i < tg.length; i++) {
										if (!Double.isNaN(ns[i])) {
											tg[i] = ns[i];
										}
									}
									tg[CuasMotionLMA.RSLT_WHEN] = when_iter;
									anew.getAndIncrement();
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return anew.get();
	}
	public static double [][][] applyFilter(
			double [][][] target_sequence,
			int    [][]   filter5){
		double [][][] filtered_scan = new double [target_sequence.length][target_sequence[0].length][];
		for (int nscan = 0; nscan < target_sequence.length; nscan++) {
			for (int t=0; t<target_sequence[nscan].length; t++) {
//				if ((target_sequence[nscan][t] != null) && (filter5[nscan][t] >=0)) {
				if (filter5[nscan][t] >=0) {
					filtered_scan[nscan][t] = target_sequence[nscan][t].clone();
				}
			}
		}
		return filtered_scan;
	}
	
	/**
	 * Trying to fix centered mode probably broken when switching to multi-varian targets.
	 * It uses data from the selected target variant, including RSLT_X, RSLT_Y data 
	 * @param target_sequence_multi
	 * @param filter5
	 * @return
	 */
	
	public static double [][][] applyFilter(
			double [][][][] target_sequence_multi,
			int    [][]   filter5){
		double [][][] filtered_scan = new double [target_sequence_multi.length][target_sequence_multi[0].length][];
		for (int nscan = 0; nscan < target_sequence_multi.length; nscan++) {
			for (int t=0; t<target_sequence_multi[nscan].length; t++) {
				if ((target_sequence_multi[nscan][t] != null) && (filter5[nscan][t] >=0)) {
					filtered_scan[nscan][t] = target_sequence_multi[nscan][t][filter5[nscan][t]].clone();
				}
			}
		}
		return filtered_scan;
	}


	public static double [][][] applyFilter(
			final double [][][] target_sequence,
			final int           index,
			final double        threshold){
		final int num_seq =   target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		
		final double [][][] filtered_targets = new double [target_sequence.length][target_sequence[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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							if ((target_sequence[nSeq][ntile] != null) && (target_sequence[nSeq][ntile][index] > threshold) ) {
								filtered_targets[nSeq][ntile] = target_sequence[nSeq][ntile].clone();
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return filtered_targets;
	}
	
	
	
	
	
	public static ImagePlus showTargetSequence(
			double [][][] target_sequence, // rename
			String []     titles, // all slices*frames titles or just slice titles or null
			String        title,
			boolean       show,
			int           tilesX) {
		int num_fields = CuasMotionLMA.LMA_TITLES.length;
		int num_scenes = target_sequence.length;
		int num_tiles = target_sequence[0].length;
		String [] frame_titles = new String [CuasMotionLMA.LMA_TITLES.length + 1];
		for (int i = 0; i < CuasMotionLMA.LMA_TITLES.length; i++) {
			frame_titles[i] = CuasMotionLMA.LMA_TITLES[i];
		}
		frame_titles[CuasMotionLMA.LMA_TITLES.length] = CuasMotionLMA.EXTRA_SLICE_DISCARD_ON_LOAD; // "Targets"; // will be discarded
		double [][][] img_data = new double [num_fields+1][num_scenes][num_tiles];
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			for (int nfield = 0; nfield < img_data.length; nfield++) {
				Arrays.fill(img_data[nfield][nscene], Double.NaN);
			}
			for (int ntile=0; ntile<target_sequence[nscene].length; ntile++) {
				double [] target = target_sequence[nscene][ntile];
				if (target != null) {
					for (int nfield = 0; nfield < num_fields; nfield++) {
						img_data[nfield][nscene][ntile] = target[nfield];
					}
					// dangerous - saves and then loads (on reuse)
					img_data[num_fields][nscene][ntile] = ((target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE) &&
							Double.isNaN(target[CuasMotionLMA.RSLT_STRONGER]))? 1:0;
				}
			}
		}
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				img_data,       // double[][][] pixels, 
				tilesX,         // int          width, 
				title,          // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				titles,         // String []    titles, // all slices*frames titles or just slice titles or null
				frame_titles, // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				show);          // boolean      show)
		return imp;
	}
	@SuppressWarnings("unchecked")
	public static org.json.simple.JSONArray targetsAnalyzeMCP(String target_path, org.json.simple.JSONArray queries) {
		String [][] pvf_top_titles = new String[1][];
		String [][] pvf_titles = new String[1][];
		int [] err_num = new int [1];
		int [] wh =      new int[2];
		double [][][][] multi_targets = readSingleMultiTargets(
				target_path,
				wh,
				pvf_top_titles,
				pvf_titles,
				err_num);

		org.json.simple.JSONArray results = new org.json.simple.JSONArray();
		if (multi_targets == null) {
			org.json.simple.JSONObject err = new org.json.simple.JSONObject();
			err.put("error", "Failed to read target file. Error code: " + err_num[0]);
			results.add(err);
			return results;
		}

		int tilesX = wh[0];
		int tilesY = wh[1];
		final int tileSize = GPUTileProcessor.DTT_SIZE; // 8
		final int PAR_UPXY=-4,PAR_VPXY=-3,PAR_PXYAR=-2,PAR_PXY=-1; // By Claude on 05/08/2026

		for (Object qObj : queries) {
			org.json.simple.JSONObject q = (org.json.simple.JSONObject) qObj;
			org.json.simple.JSONObject res = new org.json.simple.JSONObject();
			
			String modeStr = (String) q.get("mode");
			if (modeStr == null) modeStr = "section";
			int mode = 0;
			for (int i = 0; i < ANLZ_MODES.length; i++) {
				if (ANLZ_MODES[i].equals(modeStr)) {
					mode = i; break;
				}
			}

			long tx_l = q.get("tx") != null ? (Long) q.get("tx") : 0;
			long ty_l = q.get("ty") != null ? (Long) q.get("ty") : 0;
			long tw_l = q.get("tw") != null ? (Long) q.get("tw") : 1;
			long th_l = q.get("th") != null ? (Long) q.get("th") : 1;
			long nseq0_l = q.get("nseq0") != null ? (Long) q.get("nseq0") : 0;
			long nseq1_l = q.get("nseq1") != null ? (Long) q.get("nseq1") : multi_targets.length - 1;
			
			int tx = Math.max(Math.min((int)tx_l, tilesX - 1), 0);
			int ty = Math.max(Math.min((int)ty_l, tilesY - 1), 0);
			int tw = Math.max(1, (int)tw_l);
			int th = Math.max(1, (int)th_l);
			int nseq0 = Math.max(Math.min((int)nseq0_l, multi_targets.length - 1), 0);
			int nseq1 = Math.max(Math.min((int)nseq1_l, multi_targets.length - 1), nseq0);

			res.put("mode", modeStr);

			if (mode == ANLZ_SECT) {
				boolean skip_empty = q.get("skip_empty") != null ? (Boolean) q.get("skip_empty") : true;
				// By Claude on 05/08/2026: params accepts strings ("RSLT_X","pxy","pxyar","vpxy","upxy")
				// or integers (numeric index into LMA_TITLES); default is ["pxy"]
				org.json.simple.JSONArray paramsJson = (org.json.simple.JSONArray) q.get("params");
				int[] params = new int[paramsJson != null ? paramsJson.size() : 1];
				if (paramsJson != null) {
					final String[] COMBO_NAMES = {"upxy", "vpxy", "pxyar", "pxy"};
					final int[]    COMBO_VALS  = {PAR_UPXY, PAR_VPXY, PAR_PXYAR, PAR_PXY};
					for (int i = 0; i < paramsJson.size(); i++) {
						Object p = paramsJson.get(i);
						if (p instanceof Long) {
							params[i] = ((Long) p).intValue();
						} else if (p instanceof String) {
							int idx = Integer.MIN_VALUE;
							for (int j = 0; j < COMBO_NAMES.length; j++) {
								if (COMBO_NAMES[j].equalsIgnoreCase((String) p)) { idx = COMBO_VALS[j]; break; }
							}
							if (idx == Integer.MIN_VALUE) {
								for (int j = 0; j < CuasMotionLMA.LMA_TITLES.length; j++) {
									if (CuasMotionLMA.LMA_TITLES[j].equalsIgnoreCase((String) p)) { idx = j; break; }
								}
							}
							params[i] = idx; // Integer.MIN_VALUE if not found → skipped in default case
						}
					}
				} else {
					params[0] = PAR_PXY;
				}

				org.json.simple.JSONArray seqResults = new org.json.simple.JSONArray();
				for (int nseq = nseq0; nseq <= nseq1; nseq++) {
					if (multi_targets[nseq] == null) continue;
					
					org.json.simple.JSONObject seqObj = new org.json.simple.JSONObject();
					seqObj.put("nseq", nseq);
					seqObj.put("title", pvf_titles[0] != null && nseq < pvf_titles[0].length ? pvf_titles[0][nseq] : "");
					
					org.json.simple.JSONArray tilesArr = new org.json.simple.JSONArray();
					for (int x = tx; x < Math.min(tx + tw, tilesX); x++) {
						for (int y = ty; y < Math.min(ty + th, tilesY); y++) {
							int ntile = x + y * tilesX;
							boolean has_targets = (multi_targets[nseq][ntile] != null) && (multi_targets[nseq][ntile].length > 0);
							if (!has_targets && skip_empty) continue;
							
							org.json.simple.JSONObject tileObj = new org.json.simple.JSONObject();
							tileObj.put("tx", x);
							tileObj.put("ty", y);
							
							org.json.simple.JSONArray targsArr = new org.json.simple.JSONArray();
							if (has_targets) {
								for (int ntarg = 0; ntarg < multi_targets[nseq][ntile].length; ntarg++) {
									if (multi_targets[nseq][ntile][ntarg] == null) continue;
									org.json.simple.JSONObject targObj = new org.json.simple.JSONObject();
									for (int npar : params) {
										switch (npar) {
											case PAR_UPXY:
												targObj.put("upx", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_PX]);
												targObj.put("upy", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_PY]);
												targObj.put("range", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_RANGE]);
												break;
											case PAR_VPXY:
												targObj.put("vx", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VX]);
												targObj.put("vy", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VY]);
												targObj.put("vconf", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VSTR]);
												break;
											case PAR_PXYAR: // By Claude on 05/08/2026
												targObj.put("px",   tileSize * x + tileSize/2.0 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_X]);
												targObj.put("py",   tileSize * y + tileSize/2.0 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_Y]);
												targObj.put("a",    multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_A]);
												targObj.put("rmse", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_RMS]);
												targObj.put("r",    multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_R1]);
												break;
											case PAR_PXY:
												targObj.put("px", tileSize * x + tileSize/2.0 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_X]);
												targObj.put("py", tileSize * y + tileSize/2.0 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_Y]);
												targObj.put("a",  multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_A]); // By Claude on 05/08/2026
												break;
											default:
												if (npar >= 0 && npar < multi_targets[nseq][ntile][ntarg].length) {
													targObj.put(CuasMotionLMA.LMA_TITLES[npar], multi_targets[nseq][ntile][ntarg][npar]); // By Claude on 05/08/2026
												}
										}
									}
									targsArr.add(targObj);
								}
							}
							tileObj.put("targets", targsArr);
							tilesArr.add(tileObj);
						}
					}
					if (!tilesArr.isEmpty() || !skip_empty) {
						seqObj.put("tiles", tilesArr);
						seqResults.add(seqObj);
					}
				}
				res.put("sequences", seqResults);
				
			} else {
				// discover modes
				int max_targets = q.get("max_targets") != null ? ((Long)q.get("max_targets")).intValue() : 50;
				int par_index = q.get("par_index") != null ? ((Long)q.get("par_index")).intValue() : 0;
				double par_val = q.get("par_val") != null ? ((Number)q.get("par_val")).doubleValue() : 0.0;
				
				org.json.simple.JSONArray foundTargets = new org.json.simple.JSONArray();
				int count = 0;
				
				for (int nseq = nseq0; nseq <= nseq1 && count < max_targets; nseq++) {
					if (multi_targets[nseq] == null) continue;
					for (int x = tx; x < Math.min(tx + tw, tilesX) && count < max_targets; x++) {
						for (int y = ty; y < Math.min(ty + th, tilesY) && count < max_targets; y++) {
							int ntile = x + y * tilesX;
							if (multi_targets[nseq][ntile] == null || multi_targets[nseq][ntile].length == 0) continue;
							
							for (int ntarg = 0; ntarg < multi_targets[nseq][ntile].length && count < max_targets; ntarg++) {
								if (multi_targets[nseq][ntile][ntarg] == null) continue;
								boolean match = false;
								double val = 0;
								if (par_index >= 0 && par_index < multi_targets[nseq][ntile][ntarg].length) {
									val = multi_targets[nseq][ntile][ntarg][par_index];
								}
								
								switch (mode) {
									case ANLZ_NEMPTY:
										match = true;
										break;
									case ANLZ_DEFINED:
										match = !Double.isNaN(val);
										break;
									case ANLZ_EQ:
										match = (val == par_val);
										break;
									case ANLZ_GE:
										match = (val >= par_val);
										break;
									case ANLZ_LE:
										match = (val <= par_val);
										break;
								}
								
								if (match) {
									org.json.simple.JSONObject matchObj = new org.json.simple.JSONObject();
									matchObj.put("nseq", nseq);
									matchObj.put("tx", x);
									matchObj.put("ty", y);
									matchObj.put("ntarg", ntarg);
									if (mode != ANLZ_NEMPTY) {
										matchObj.put("val", val);
									}
									foundTargets.add(matchObj);
									count++;
								}
							}
						}
					}
				}
				res.put("matches", foundTargets);
			}
			results.add(res);
		}
		return results;
	}

	public static void targetsAnalyze() {
		int       mode = 0;
		double    par_val = 0.0;
		int       max_targets = 50;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Set CLT parameters",1800,120);
		String target_path = "/home/elphel/lwir16-proc/LV/models/models_1773135103-1773135664/1773135476_186641-CENTER/debug/v011-07/1773135476_186641-CENTER-OFFS5-PAIRS20-RSTR0.01-RMS5.0-SRMS0.4-RRMS0.35-AMP0.4-FZ50.0-PCRA4-PS2-TUM100-TT5.0-MF0.55-HR0.45-NL-0.3-FH1.5-FL0.8-FS5.0-CENT-FAST-ADDED_NEW_CENTERED_MULTI-n19.tiff";
		gd.addStringField("Target path", target_path, 350, "Full target file path to analyze");
		gd.showDialog();
		if (gd.wasCanceled()) return;
		target_path = gd.getNextString();
		String [][] pvf_top_titles = new String[1][];
		String [][] pvf_titles = new String[1][];
		int [] err_num = new int [1];
		int [] wh =      new int[2];
		double [][][][] multi_targets = readSingleMultiTargets( // slices should contain two ":"			
				target_path,    // String      path,
				wh,             // int []      wh,            // should be null or int[2] 
				pvf_top_titles, // String [][] ptop_titles,   // should be null or String [1][]
				pvf_titles,     // String [][] pslice_titles, // should be null or String [1][]
				err_num);       // int      [] err_num)
		if (multi_targets == null) {
			System.out.println("targetsAnalyze() -> err="+err_num[0]);
			return;
		}
		int num_tiles = wh[0]*wh[1];
		boolean skip_empty = true;
		int nseq0 = 0;
		int nseq1 = multi_targets.length -1;
		int tx0 = wh[0] / 2;
		int ty0 = wh[1] / 2;
		int twidth =  1;
		int theight = 1;
		int num_pars = CuasMotionLMA.RSLT_LEN; //  pvf_top_titles[0].length; // .RSLT_LEN;
		int npar = CuasMotionLMA.RSLT_A;
		int fw = 9;
		final int PAR_UPXY=-4,PAR_VPXY=-3,PAR_PXYAR=-2,PAR_PXY=-1;
		String [] extra_choices = {"upX/upY/range", "vX/vY/conf", "pX/pY/A/RMSE/R", "pX/pY/A"};
		String [] choices = new String [num_pars+extra_choices.length];
		for (int i = -extra_choices.length; i < 0; i++) {
			choices[i + extra_choices.length] = String.format("%2d: %s", i, extra_choices[i+extra_choices.length]); // pvf_top_titles[0])
		}
		for (int i = 0; i < num_pars; i++) {
			choices[i + extra_choices.length] = String.format("%2d: %s", i, CuasMotionLMA.LMA_TITLES[i]); // pvf_top_titles[0])
		}
		while (true) {
			GenericJTabbedDialog gd1 = new GenericJTabbedDialog("Select Range",600,500);
//			gd1.addMessage(target_path);
			gd1.addNumericField("tile X (0.."+(wh[0] -1)+")", tx0, 0, 3, "",
					"Select tile X");
			gd1.addNumericField("tile Y (0.."+(wh[1] -1)+")", ty0, 0, 3, "",
					"Select tile Y");
			gd1.addNumericField("tiles width (1.."+(wh[0])+")", twidth, 0, 3, "",
					"Select tiles rectangle width");
			gd1.addNumericField("tile height (1.."+(wh[1])+")", theight, 0, 3, "",
					"Select tile rectangle height");
			gd1.addNumericField("Start sequence (0.."+(multi_targets.length -1)+")", nseq0, 0, 3, "",
					"Select first scene to output");
			gd1.addNumericField("End sequence (0.."+(multi_targets.length -1)+")", nseq1, 0, 3, "",
					"Select last scene to output");
			gd1.addChoice("Parameter", choices, choices[npar + extra_choices.length], "Parameter to display");
			
			gd1.addNumericField("Format data width", fw, 0, 3, "",
					"Parameter index to display");
			gd1.addCheckbox("Skip empty", skip_empty, "Do not show tiles with no targets");

			gd1.addChoice("Report mode", ANLZ_MODES, ANLZ_MODES[mode], "Select one of the analysis modes.");
			gd1.addNumericField("Parameter value", par_val, 4, 10, "",
					"Parameter value to compare");
			gd1.addNumericField("Number of tiles to show", max_targets, 0, 3, "",
					"Maximal number of tiles to show");
			
			
			gd1.showDialog();
			if (gd1.wasCanceled()) return;
			final int tileSize = GPUTileProcessor.DTT_SIZE; // 8
			tx0 =          (int) gd1.getNextNumber();
			ty0 =          (int) gd1.getNextNumber();
			twidth =       (int) gd1.getNextNumber();
			theight =      (int) gd1.getNextNumber();
			nseq0 =       (int) gd1.getNextNumber();
			nseq1 =       (int) gd1.getNextNumber();
			npar =              gd1.getNextChoiceIndex() - extra_choices.length;
			fw =          (int) gd1.getNextNumber();
			skip_empty =        gd1.getNextBoolean();
			
			mode =              gd1.getNextChoiceIndex();
			par_val =           gd1.getNextNumber();
			max_targets = (int) gd1.getNextNumber();
			
			
			tx0 =     Math.max(Math.min(tx0, wh[0] -1), 0);
			ty0 =     Math.max(Math.min(ty0, wh[1] -1), 0);
			twidth = Math.max(Math.min(twidth, wh[0] - tx0), 1);
			theight =Math.max(Math.min(theight, wh[1] - ty0), 1);
			nseq0 =  Math.max(Math.min(nseq0, multi_targets.length -1), 0);
			nseq1 =  Math.max(Math.min(nseq1, multi_targets.length -1), nseq0);
			npar =   Math.max(Math.min(npar, num_pars -1), -extra_choices.length);

			System.out.println("\nProcessing "+target_path);
			switch (mode) {
			case ANLZ_SECT:
				System.out.println("Displaying scenes from "+nseq0+" to "+nseq1+", parameter: "+choices[npar+extra_choices.length]); // CuasMotionLMA.LMA_TITLES
				System.out.println("tx="+tx0+", ty="+ty0+", twidth="+twidth+", theight="+theight);
//				int ntile = tx0 + ty0 * wh[0];
				
				for (int nseq = nseq0; nseq <= nseq1; nseq++) {
					boolean [][] has_targ = new boolean [theight][twidth];
					boolean has_targets = false;
					for (int ty = ty0; ty < (ty0+theight); ty++) {
						for (int tx = tx0; tx < (tx0+twidth); tx++) {
							int ntile = tx + ty * wh[0];
							has_targ[ty-ty0][tx-tx0] = (multi_targets[nseq] != null) &&
									(multi_targets[nseq][ntile] != null) &&
									(multi_targets[nseq][ntile].length > 0);
							has_targets |= has_targ[ty-ty0][tx-tx0];   
						}
					}
					if (has_targets || !skip_empty) {
						System.out.print(String.format("%17s, %4d, ",pvf_titles[0][nseq],nseq));
						for (int ty = ty0; ty < (ty0+theight); ty++) {
							for (int tx = tx0; tx < (tx0+twidth); tx++) {
								int ntile = tx + ty * wh[0];
//								boolean has_targets = (multi_targets[nseq] != null) && (multi_targets[nseq][ntile] != null) && (multi_targets[nseq][ntile].length > 0);
								if (has_targ[ty-ty0][tx-tx0] || !skip_empty) {
									if ((theight * twidth) > 1) {
										System.out.print(String.format("%5s,%2d:%2d, ",ntile,tx,ty));			
									}
//									System.out.print(String.format("%17s, %4d, ",pvf_titles[0][nseq],nseq));
									if (has_targ[ty-ty0][tx-tx0]) {
										int num_targ = multi_targets[nseq][ntile].length;
										for (int ntarg = 0; ntarg < num_targ; ntarg++) {
											switch (npar) {
											case PAR_UPXY:
												double upx = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_PX];
												double upy = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_PY];
												double urng = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_FL_RANGE];
												System.out.print(String.format("(%8.3f, %8.3f, %8.3f)",upx,upy,urng));
												break;
											case PAR_VPXY:
												double vx =    multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VX];
												double vy =    multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VY];
												double vconf = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VSTR];
												System.out.print(String.format("(%8.4f, %8.4f, %8.5f)",vx,vy,vconf));
												break;
											case PAR_PXYAR: //RSLT_A
												double px1 =  tileSize * tx + tileSize/2 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_X];
												double py1 =  tileSize * ty + tileSize/2 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_Y];
												double a1 =   multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_A];
												double rmse = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_RMS];
												double rp =   multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_R1];
												System.out.print(String.format("(%8.3f, %8.3f, %8.4f, %8.4f, %8.4f)",px1,py1,a1, rmse, rp));
												break;
											case PAR_PXY: //RSLT_A
												double px = tileSize * tx + tileSize/2 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_X];
												double py = tileSize * ty + tileSize/2 + multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_Y];
												double a =  multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_A];
												System.out.print(String.format("(%8.3f, %8.3f, %8.4f)",px,py,a));
												break;
												//
											default:
												System.out.print(String.format("%"+fw+"f",multi_targets[nseq][ntile][ntarg][npar]));
											}
//											if (ntarg < (num_targ-1)) {
												System.out.print(", ");
//											}
										}
									}
//									System.out.println();
								}
							}
						}
						System.out.println();
					}
				}
				System.out.println();
				break;
			case ANLZ_NEMPTY:  // list tiles with targets
			case ANLZ_DEFINED: // list tiles with targets with non-NaN parameter
			case ANLZ_EQ:      // list tiles with targets with parameter equal (such as FAILURE==0)
			case ANLZ_GE:      // list tiles with targets with parameter >= value (such as AMPLITUDE >= 1.0)
			case ANLZ_LE:      // list tiles with targets with parameter <= value (such as RMSE <= 0.5)
				System.out.print("\nLooking for tiles in each scene from "+nseq0+" to "+nseq1+
						", "+tx0+"<=tx<="+(tx0 + twidth-1)+", "+ty0+"<=ty<="+(ty0 + theight-1));
				switch (mode) {
				case ANLZ_NEMPTY:  System.out.print(" that contain at least one target"); break;
				case ANLZ_DEFINED: System.out.print(" that contain at least one target with non-NaN "+choices[npar+extra_choices.length]+" value"); break;
				case ANLZ_EQ:      // list tiles with targets with parameter equal (such as FAILURE==0)
				case ANLZ_GE:      // list tiles with targets with parameter >= value (such as AMPLITUDE >= 1.0)
				case ANLZ_LE:      // list tiles with targets with parameter <= value (such as RMSE <= 0.5)
					System.out.print(" that contain at least one target with "+choices[npar+extra_choices.length]+
							" "+ANLZ_MODES[mode]+" "+par_val+"."); break;
				}
				if (npar <0) {
					npar = 0; // use x instead of pseudo -1 (pX/pY)
				}
				
				if (max_targets > 0) {
					System.out.println(" Number of reported tiles is limited to "+max_targets);
				} else {
					System.out.println(" Unlimited number of reported tiles");
				}
				for (int nseq = nseq0; nseq <= nseq1; nseq++) {
					ArrayList<Integer> tiles_list = new ArrayList<Integer>();
					for (int ty = ty0; ty < (ty0+theight); ty++) {
						for (int tx = tx0; tx < (tx0+twidth); tx++) {
							int ntile = tx + ty * wh[0];
							boolean has_targets = (multi_targets[nseq] != null) &&
									(multi_targets[nseq][ntile] != null) &&
									(multi_targets[nseq][ntile].length > 0);
							if (has_targets) {
								search_targets: {
									if (mode == ANLZ_NEMPTY) {
										tiles_list.add(ntile);
										break search_targets;
									} else {
										int num_targ = multi_targets[nseq][ntile].length;
										for (int ntarg = 0; ntarg < num_targ; ntarg++) if (multi_targets[nseq][ntile][ntarg] != null){
											switch (mode) {
											case ANLZ_DEFINED:
												if (!Double.isNaN(multi_targets[nseq][ntile][ntarg][npar])){
													tiles_list.add(ntile);
													break search_targets;
												}
												break;
											case ANLZ_EQ:      // list tiles with targets with parameter equal (such as FAILURE==0)
												if (multi_targets[nseq][ntile][ntarg][npar] == par_val){
													tiles_list.add(ntile);
													break search_targets;
												}
												break;
											case ANLZ_GE:      // list tiles with targets with parameter >= value (such as AMPLITUDE >= 1.0)
												if (multi_targets[nseq][ntile][ntarg][npar] >= par_val){
													tiles_list.add(ntile);
													break search_targets;
												}
												break;
											case ANLZ_LE:      // list tiles with targets with parameter <= value (such as RMSE <= 0.5)
												if (multi_targets[nseq][ntile][ntarg][npar] <= par_val){
													tiles_list.add(ntile);
													break search_targets;
												}
												break;
											}									
										}								
									}
								}
							}
						} // for (int tx = tx0; tx < (tx0+twidth); tx++) {
					} // for (int ty = ty0; ty < (ty0+theight); ty++) {
					
					if (!tiles_list.isEmpty() || !skip_empty) {
						System.out.print(String.format("%17s, %4d, targets:%5d",pvf_titles[0][nseq],nseq, tiles_list.size()));
						if (!tiles_list.isEmpty()) {
							int ntiles = tiles_list.size();
							if (max_targets > 0) {
								ntiles = Math.min(ntiles, max_targets);
								System.out.print(" [");
								for (int tile: tiles_list) {
									System.out.print(String.format("%5d (%3d:%3d), ",tile,tile%wh[0],tile/wh[0]));
								}
								System.out.print(" ]");
							}
						}
						System.out.println();
					}					
				} // for (int nseq = nseq0; nseq <= nseq1; nseq++)				
				break;
			} // switch (mode)
		}
	}
	
	public static double [][][][] readSingleMultiTargets(
			String      path,
			int []      wh,            // should be null or int[2] 
			String [][] ptop_titles,   // should be null or String [1][]
			String [][] pslice_titles, // should be null or String [1][]
			int      [] err_num){
		if (err_num == null) {
			err_num = new int [1];
		}
		double [][][][] multi_targets = readDoubleHyperHyperstack( // slices should contain two ":"			
				path,           // String      path,
				wh,             // int []      wh,            // should be null or int[2] 
				ptop_titles,    // String [][] ptop_titles,   // should be null or String [1][]
				pslice_titles,  // String [][] pslice_titles, // should be null or String [1][]
				err_num);       // int      [] err_num)
		if (multi_targets == null) {
			System.out.println("readDoubleHyperHyperstack() -> err="+err_num[0]);
			if ((err_num[0] == 2) || (err_num[0] == 3)) {
				System.out.println("readDoubleHyperHyperstack(): trying to read a single-target array");
				double [][][] targets = CuasMotion.getTargetsFromHyperAugment(
						ptop_titles,          // String [][] pvf_top_titles,
						pslice_titles, // String [][] pvf_titles,
						path);
				if (targets != null) { // convert to multi-target
					multi_targets = convertToMultiTarget(targets); // final double [][][] target_sequence) {
					err_num[0] = -1; // got a single-target, converted to multi
				}
			}
		}
		return multi_targets;
	}
	public static double [][][][] readDoubleHyperHyperstack( // slices should contain two ":"
			String      path){
		return readDoubleHyperHyperstack(
				path,  // String      path,
				null,  // int []      wh,            // should be null or int[2] 
				null,  // String [][] ptop_titles,   // should be null or String [1][]
				null,  // String [][] pslice_titles, // should be null or String [1][]
				null); // int      [] err_num_ext);	}
	}
	
	public static double [][][][] readDoubleHyperHyperstack( // slices should contain two ":"
			String      path,
			int []      wh,            // should be null or int[2] 
			String [][] ptop_titles,   // should be null or String [1][]
			String [][] pslice_titles, // should be null or String [1][]
			int      [] err_num_ext){
		final int [] err_num = (err_num_ext != null) ? err_num_ext : (new int[1]);
		// will ignore last parameter "Targets" all above CuasMotionLMA.RSLT_LEN
		// X-OFFS:1773135470_584775-74:6 : <target-parameter-name>:<file_name>-<file-index>:target_index
		System.out.println("readDoubleHyperHyperstack(): reading "+path);
		ImagePlus imp = new ImagePlus(path);
		if (imp.getWidth() == 0) {
			System.out.println("readDoubleHyperHyperstack(): Failed reading hyperstack data from: " + path);
			err_num[0] = 1;
			return null;
		}
		if (wh == null) {
			wh = new int[2];
		}
//		if (wh!= null) {
			wh[0] = imp.getWidth();
			wh[1] = imp.getHeight();
//		}
		int num_slices = imp.getStackSize();
		String [] slice_labels = imp.getStack().getSliceLabels();
			
		ArrayList<String> top_titles = new ArrayList<String>(); // parameter names
		final HashMap<Integer,String> ts_map = new HashMap<Integer,String>(); // to notice missing timestamps (no targets at all
		final HashMap<Integer,Integer> offsets_map = new HashMap<Integer,Integer>(); // to notice missing timestamps (no targets at all
		int last_index = 0;
		for (int i = 0; i < num_slices; i++) {
			String [] label = slice_labels[i].split(":");
			if (label.length < 3) {
				System.out.println("readDoubleHyperHyperstack(): Slices should have two ':' separators in : " + path);
				err_num[0] = 2;
				return null;
			}
			if (!top_titles.contains(label[0])) { // target parameter name
				top_titles.add(label[0]);
			}
			String [] ts_indx = label[1].split("-");
			if (label.length<2) {
				System.out.println("readDoubleHyperHyperstack(): Slices should have '-' between the two ':' separators in : " + path);
				err_num[0] = 3; // it may be [][][], not [][][][] array
				return null;
			}
			int indx = Integer.parseInt(ts_indx[1]);
			if (ts_map.get(indx) == null) {
				ts_map.put(indx, ts_indx[0]);
				offsets_map.put(indx, i);
				last_index = Math.max(last_index, indx);
			}
		}
		String [] top_l =   top_titles.toArray(new String[0]);
		int num_indices = num_slices/top_l.length; // number of slices per parameters
		final int num_pars = Math.min(top_l.length, CuasMotionLMA.RSLT_LEN); // but preserve num_indices!
		final int [] indices = new int [last_index+1];
		Arrays.fill(indices, -1);
		for (int indx = 0; indx < indices.length; indx++) {
			Integer offs = offsets_map.get(indx);
			if (offs != null) {
				indices[indx] = offs;
			}
		}

		double [][] file_data = ShowDoubleFloatArrays.readDoubleArray(
				imp,       // ImagePlus   imp,
				0,         // 	int         num_slices, // (0 - all)
				wh);       // int []      wh); // int []      wh)
		int num_tiles = wh[0]*wh[1];
		double [][][][] targets = new double [indices.length][num_tiles][][];
		String [] scenes = new String [indices.length];
		for (int nseq = 0; nseq < indices.length; nseq++) {
			scenes[nseq] = ts_map.get(nseq);
		}
		int num_empty=0;
		for (int nseq = 0; nseq < indices.length; nseq++) if (scenes[nseq] == null){
			num_empty++;			
			if (nseq == 0) {
				int nseq1 = nseq+1;
				for (; (nseq1 < indices.length) && (scenes[nseq1] == null) ; nseq1++);
				if (nseq1 >= indices.length) {
					err_num[0] = 4;
					return null;
				}
				int nseq2 = indices.length-1;
				for (; (nseq2 > nseq1) && (scenes[nseq2] == null) ; nseq2--);
				if (nseq1 >= indices.length) {
					err_num[0] = 5;
					return null;
				}
				scenes[nseq]= missingTimestamp(
						nseq,            // int    indx, // may be outside of the range
						nseq1,          // int    indx0,
						scenes[nseq1],  // String ts0,
						nseq2,          // int    indx1,
						scenes[nseq2]); // String ts1)
			} else { // nseq>0, all previous are defined
				int nseq1 = nseq-1; // always defined already
				int nseq2 = nseq+1;
				for (; (nseq2 < indices.length) && (scenes[nseq2] == null) ; nseq2++);
				if (nseq2 >= indices.length) { // nothing later defined - should not happen as the last index comes from existing.
					System.out.println("readDoubleHyperHyperstack(): No timestamp after index " + nseq+ "(last is "+
							(indices.length-1)+"). Should not happen!");
					err_num[0] = 6; // should never happen
					return null;
				}
				scenes[nseq]= missingTimestamp(
						nseq,            // int    indx, // may be outside of the range
						nseq1,          // int    indx0,
						scenes[nseq1],  // String ts0,
						nseq2,          // int    indx1,
						scenes[nseq2]); // String ts1)
			}
		}
		if (num_empty > 0) {
			System.out.println("readDoubleHyperHyperstack(): replaced "+ num_empty+" empty scene numes");
		}
		IJ.showStatus("Extracting multi-target data...");
  		IJ.showProgress(0.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 nSeq = ai.getAndIncrement(); nSeq < indices.length; nSeq = ai.getAndIncrement())if (ts_map.get(nSeq) != null) { // if null - keep all tiles null
						IJ.showProgress(1.0*(nSeq+1) / indices.length);	
						int indx0 = indices[nSeq];
						if (indx0 < 0) {
							System.out.println("readDoubleHyperHyperstack(): indices[" + nSeq+ "] < 0, should not happen as ts_map.get(nseq) != null");
							err_num[0] = 7; // should never happen
							continue;
						}
						int nseq1 = nSeq+1;
						for (; (nseq1 < indices.length) && (indices[nseq1] < 0 ) ; nseq1++);
						int indx1 = (nseq1 < indices.length) ? indices[nseq1] : num_indices; // file_data.length;
						// indx0, indx1 are common for all tiles
						if (nSeq == (indices.length-1)) {
							System.out.println("readDoubleHyperHyperstack(): nseq=" + nSeq);
						}
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							int last_target = -1; // for this tile
							boolean [] target_exists = new boolean [indx1-indx0]; // allowing missing targets (should not happen)
							for (int ntarg = 0; ntarg < target_exists.length; ntarg++) {
								int indx = indx0+ntarg;
								for (int npar = 0; npar < num_pars; npar++) {
									int nslice = npar * num_indices + indx;
									if (!Double.isNaN(file_data[nslice][ntile])) {
										target_exists[ntarg]=true;
										last_target = ntarg;
										break;
									}
								}
							}
							// truncate by the last non-null, if any exist (if none - keep null) 
							// find maximal number of target in this tile that has at least one non-null parameter
							if (last_target >= 0) {
								targets[nSeq][ntile] = new double [last_target+1][];
								for (int ntarg = 0; ntarg <= last_target; ntarg++) {
									if (target_exists[ntarg]) { 
										int indx = indx0+ntarg;
										targets[nSeq][ntile][ntarg] = new double [num_pars];
										for (int npar = 0; npar < num_pars; npar++) {
											int nslice = npar * num_indices + indx;
											targets[nSeq][ntile][ntarg][npar] = file_data[nslice][ntile];
										}
									}
								}
							}
						}
						
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		IJ.showProgress(1.0);
		if (ptop_titles != null) {
			ptop_titles[0] = top_l;
		}
		if (pslice_titles != null) {
			pslice_titles[0] = scenes;
		}
		return targets;
	}
	
		
	
	
	public static String missingTimestamp(
			int    indx, // may be outside of the range
			int    indx0,
			String ts0,
			int    indx1,
			String ts1) {
		double dts0=Double.parseDouble(ts0.replace("_", "."));
		double dts1=Double.parseDouble(ts1.replace("_", "."));
		double dts = dts0+ (indx-indx0) * ((dts1-dts0)/(indx1-indx0));
		int sec = (int) dts0;
		int usec = (int) Math.round(1000000*(dts-sec));
		String ts = String.format("%d_%06d", sec, usec); 
		return ts;
	}


	
	public static double [][][] getTargetsFromHyperAugment(
			String path) {
		return getTargetsFromHyperAugment(
				null,  // String [][] pvf_top_titles,
				null,  // String [][] pvf_titles,
				path); //String path)
	}


	public static double [][][] getTargetsFromHyperAugment(
			String [][] pvf_top_titles,
			String [][] pvf_titles,
			String path){ // add empty fields to the end of each target if shorter than CuasMotionLMA.RSLT_LEN
		int [] wh = new int [2];
		if (pvf_top_titles == null) pvf_top_titles = new String[1][];
		if (pvf_titles == null)     pvf_titles = new String[1][];
		double [][][] targets_file = ShowDoubleFloatArrays.readDoubleHyperstack(
						path, // String      path,
						wh,            // int []      wh,            // should be null or int[2] 
						pvf_top_titles,   // String [][] ptop_titles,   // should be null or String [1][]
						pvf_titles);      // String [][] pslice_titles){// should be null or String [1][]
		if (targets_file == null) {
			return null;
		}
		int num_fields = targets_file.length;
		// fix - discarding last slice if it is called CuasMotionLMA.EXTRA_SLICE_DISCARD_ON_LOAD ("Targets")
		if (pvf_top_titles[0][pvf_top_titles[0].length-1].equals(CuasMotionLMA.EXTRA_SLICE_DISCARD_ON_LOAD)) {
			System.out.println("getTargetsFromHyperAugment(): removing last slice called "+ pvf_top_titles[0][pvf_top_titles[0].length-1]);
			num_fields--;
			String [] ss = new String[num_fields];
			System.arraycopy(pvf_top_titles[0], 0, ss, 0, num_fields);
			pvf_top_titles[0] = ss;
		}
		final int num_fields_augmented = Math.max(num_fields, CuasMotionLMA.RSLT_LEN);
		final int num_seq = targets_file[0].length;
		final int num_tiles = targets_file[0][0].length;
		final double [][][] target_sequence = new double [num_seq][num_tiles][];
		final Thread[] threads = ImageDtt.newThreadArray();                                               
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] fields = new double[num_fields_augmented];
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							boolean has_data = false;
							for (int npar = 0; npar < num_fields_augmented; npar++) {
								fields[npar] = targets_file[npar][nSeq][ntile];
								has_data |= !Double.isNaN(fields[npar]);
							}
							if (has_data) { // at least on is not NaN - create target
								target_sequence[nSeq][ntile] = fields.clone();
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return target_sequence;
	}
	
	// By Claude on 05/07/2026: pre-populate target_sequence_multi with FAIL_PENDING sentinels from motion_sequence.
	// Each sentinel is a clone of the motion entry with RSLT_FAIL=FAIL_PENDING, RSLT_CENTERED=CENTERED_NO,
	// RSLT_QSCORE initialized from RSLT_MSCORE. Preserves VX/VY/X/Y and all other motion fields.
	public static void initFromMotionSequence(
			final double [][][]   motion_sequence,
			final double [][][][] target_sequence_multi) {
		final int num_seq   = motion_sequence.length;
		final int num_tiles = motion_sequence[0].length;
		for (int nSeq = 0; nSeq < num_seq; nSeq++) {
			for (int ntile = 0; ntile < num_tiles; ntile++) {
				if (motion_sequence[nSeq][ntile] != null && target_sequence_multi[nSeq][ntile] == null) {
					double [] sentinel = motion_sequence[nSeq][ntile].clone();
					sentinel[CuasMotionLMA.RSLT_FAIL]     = CuasMotionLMA.FAIL_PENDING;
					sentinel[CuasMotionLMA.RSLT_CENTERED] = CuasMotionLMA.CENTERED_NO;
					sentinel[CuasMotionLMA.RSLT_QSCORE]   = motion_sequence[nSeq][ntile][CuasMotionLMA.RSLT_MSCORE];
					target_sequence_multi[nSeq][ntile] = new double [][] {sentinel};
				}
			}
		}
	}

	// By Claude on 05/07/2026: after each getEffectiveStrengthMV call, keep RSLT_QSCORE in FAIL_PENDING sentinels
	// current with the freshly computed RSLT_MSCORE so filter5 ranks by up-to-date motion confidence.
	public static void syncMotionScoreToSentinels(
			final double [][][]   motion_sequence,
			final double [][][][] target_sequence_multi) {
		final int num_seq   = motion_sequence.length;
		final int num_tiles = motion_sequence[0].length;
		for (int nSeq = 0; nSeq < num_seq; nSeq++) {
			for (int ntile = 0; ntile < num_tiles; ntile++) {
				double [] mv = motion_sequence[nSeq][ntile];
				if (mv != null && target_sequence_multi[nSeq][ntile] != null) {
					for (double [] entry : target_sequence_multi[nSeq][ntile]) {
						if (entry != null &&
								entry[CuasMotionLMA.RSLT_FAIL]     == CuasMotionLMA.FAIL_PENDING &&
								entry[CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO) {
							entry[CuasMotionLMA.RSLT_QSCORE] = mv[CuasMotionLMA.RSLT_MSCORE];
						}
					}
				}
			}
		}
	}

	// fills out additional fields in target_coords

	public static double [][] getEffectiveStrengthMV( // calculate tiles effective strength by the motion vectors. Combine with the target LMA?
			final double [][][] targets_sequence,
			int                 niter, // save iteration number on failure if >=
			final int           tilesX,
			final double        min_score_mv,
			double              speed_min,		
			double              speed_pref,		
			double              speed_boost	){
		final int num_seq = targets_sequence.length;
		final int num_tiles = targets_sequence[0].length;
		final double [][] effective_strength = new double [num_seq][num_tiles];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						double [][] targets = targets_sequence[nSeq];
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							if (targets[ntile] != null) {
								double es =	getEffectiveStrengthMV(
										targets[ntile],  // double [] target,
										speed_min,  // double speed_min,		
										speed_pref, // double speed_pref,		
										speed_boost); //double speed_boost)
								targets[ntile][CuasMotionLMA.RSLT_MSCORE] = es;
								effective_strength[nSeq][ntile] = es;
								if ((niter >= 0) && !(es >= min_score_mv)) {
									targets[ntile][CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_MOTION;
									targets[ntile][CuasMotionLMA.RSLT_WHEN] = niter;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return effective_strength;
	}
	
/*
	private static void setBadHorizon(
			final double [][][] target_sequence,
			final double        horizon_y,
			final int           tilesX){
		final int num_seq =   target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final int tilesY =    num_tiles/tilesX; 
		
		final int ihorizon = (horizon_y > 0) ?( (int) Math.ceil(horizon_y/GPUTileProcessor.DTT_SIZE)) : -1; // tiles with tileY >= ihorizon are removed
		if (ihorizon  > 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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
							for (int tileY = ihorizon; tileY < tilesY; tileY++) {
								int ntile = tileY*tilesX;
								for (int tileX = 0; tileX < tilesX; tileX++) {
									target_sequence[nSeq][ntile++] = null;
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
	}
*/	
	private static void setBadHorizon(
			final double [][][] target_sequence,
			final double        horizon_y, // <0 - do not filter by horizon
			final double []     noise8,    // null - do not filter by noise
			final double        noise_max,
			final int           tilesX){
		final int num_seq =   target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final int tilesY =    num_tiles/tilesX; 
		final int ihorizon = (horizon_y > 0) ?( (int) Math.ceil(horizon_y/GPUTileProcessor.DTT_SIZE)) : -1; // tiles with tileY >= ihorizon are removed
		final int ntile_horizon =  (ihorizon > 0) ? (ihorizon * tilesX) : num_tiles;
		if ((ihorizon  > 0) ||  (noise8 != null)){
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
							if (noise8 != null) {
								for (int ntile = 0; ntile < ntile_horizon; ntile++) if (noise8[ntile] > noise_max) {
									target_sequence[nSeq][ntile] = null;
								}
							}
							if (ihorizon > 0) {
								for (int tileY = ihorizon; tileY < tilesY; tileY++) {
									int ntile = tileY*tilesX;
									for (int tileX = 0; tileX < tilesX; tileX++) {
										target_sequence[nSeq][ntile++] = null;
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
	}
	
	
	private static double getEffectiveStrengthMV(
			double [] target,
			double speed_min,		
			double speed_pref,		
			double speed_boost) {
		if (target == null ) {
			return 0.0;
		}
		double val = target[CuasMotionLMA.RSLT_VSTR];
		if (!Double.isNaN(target[CuasMotionLMA.RSLT_VFRAC])) {
			val *= target[CuasMotionLMA.RSLT_VFRAC];
		}
		if ((speed_min > 0) ||(speed_pref >0)) {
			double vx = target[CuasMotionLMA.RSLT_VX];
			double vy = target[CuasMotionLMA.RSLT_VY];
			double v =  Math.sqrt(vx*vx + vy*vy);
			if (v < speed_min) {
				return 0;
			}
			if (v > speed_pref) {
				val *= Math.min(v / speed_pref, speed_boost);
			}
		}
		return val;
	}
	
	
	// now will skip tiles that have stronger neighbors
	public static double [][][] extendMotionScan(
			final double [][][] motion_scan,
			final boolean [][]  filtered, // centers, should be non-overlapped . May be [motion_scan.length][] - will be calculated and returned
			final int           tilesX,
			final int           range, // 1 or 2
			final int     []    remain){ // number of center tiles (3x3 groups in the output)
		final int num_seq = motion_scan.length;
		final int num_tiles = motion_scan[0].length;
		final int tilesY = num_tiles/ tilesX;
		final double [][][] extended = new double [num_seq][num_tiles][];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int irange = range; irange > 0; irange--) { // to try 5x5, but make sure 3x3 are always as needed
			ai.set(0);
			final int frange = irange;
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						TileNeibs tn = new TileNeibs(tilesX, tilesY);
						for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
							int num = 0;
							double [][] ms = motion_scan[nSeq];
							boolean has_filter = (filtered != null) && (filtered[nSeq] != null);
							boolean [] centers = has_filter ? filtered[nSeq] : new boolean [ms.length];
							if (!has_filter) {
								for (int i = 0; i < centers.length; i++) {
									centers[i] = (ms[i] != null) &&
											Double.isNaN(ms[i][CuasMotionLMA.RSLT_STRONGER]) && // should not have stronger neighbor
											(Double.isNaN(ms[i][CuasMotionLMA.RSLT_FAIL]) ||
													(ms[i][CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE) ||
													(ms[i][CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_PENDING)); // By Claude on 05/07/2026
								}
							}
							if ((filtered != null) && (filtered[nSeq] == null)) {
								filtered[nSeq] = centers;
							}
							for (int tileY = 0; tileY < tilesY; tileY++) {
								for (int tileX = 0; tileX < tilesX; tileX++) {
									int ntile = tileX + tilesX * tileY;
									if (centers[ntile]) {
										num++;
										double [] mv = motion_scan[nSeq][ntile];
										for (int dy = -frange; dy <= frange; dy++) {
											for (int dx = -frange; dx <= frange; dx++) {
												int indx = tn.getNeibIndex(ntile, dx, dy);
												if (indx >=0) {
													extended[nSeq][indx] = mv.clone(); // does not need to ne cloned
												}
											}
										}
									}
								}
							}
							if (remain != null) {
								remain[nSeq] = num;
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return extended;
	}
	
	
	public static double [][][] getTargetsFromCorr2d(
			CLTParameters     clt_parameters,
			boolean           batch_mode,
			CuasMotion        cuasMotion,
			float [][]        fpixels, // may be running average
			int               start_frame,
			int               corr_pairs,
			double            boost_pairs,
			int               corr_offset,
			int               corr_inc,
		    int               corr_ra_step, //    5;      // correlation step when using rolling average
		    int               slow_scale_samples,  // multiply number of samples
		    int               slow_scale_offset,   // multiply offset
			boolean           smooth,
			double            fat_zero,
			double            cent_radius,
			int               n_recenter,
			double            rstr,
			String            title,
			double [][][]     corr2d, // null or [(fpixels.length - seq_length - start_frame) / corr_step)[][]
			int               debugLevel){
		boolean boost_offset = false; // true;
		int     corr_offset_boosted = boost_offset ? 2* ((int) Math.round(0.5 * corr_offset * boost_pairs)) : corr_offset;
		double scale_vxy = 1.0 / (boost_offset ? boost_pairs : 1.0); 
		int seq_length = corr_offset + corr_pairs;
		int num_scenes = fpixels.length;
		int num_corr_samples = (num_scenes - seq_length - start_frame) / corr_inc;
		int num_tiles = cuasMotion.tilesX*cuasMotion.tilesY;
		double [][][] targets = new double [num_corr_samples][num_tiles][];
		IJ.showStatus("Performing Motion Scan");
  		IJ.showProgress(0.0);
		for (int nscan = 0; nscan < targets.length; nscan++) {
			int frame0 = start_frame + corr_inc * nscan;
			int corr_pairs_scaled = (int) Math.round(corr_pairs*boost_pairs);
			if (boost_pairs > 1) {
				frame0 -= (int) Math.round((corr_pairs * (boost_pairs - 1))/ 2);
			}
			int frame_cent = frame0 + corr_offset/2;
			if (boost_offset) {
				frame0 = frame_cent -  corr_offset_boosted/2;
			}
			int frame1 = frame0 + corr_offset_boosted; // corr_offset;
			
			String suffix_param = "-"+frame_cent+"-"+corr_offset_boosted+"-"+corr_pairs;
			String dbg_suffix = (title != null) ? (title+suffix_param) : null;
			if ((slow_scale_samples > 1) || (slow_scale_offset > 1)) {
				int frame_cent2 = 2 * frame0 + corr_offset_boosted;
				int scan_center2 = frame_cent2 + corr_pairs; // twice index of scan center
				corr_pairs_scaled = corr_pairs * slow_scale_samples;
				int frame_cent2_scaled = scan_center2 - corr_pairs_scaled; //  * slow_scale_samples;
				frame0 = (frame_cent2_scaled - corr_offset_boosted * slow_scale_offset)/2;
				frame1 = frame0 + corr_offset_boosted * slow_scale_offset;
				frame_cent = (frame0 + frame1)/2; // debug only
				
			}
			if (debugLevel > -4) {
				System.out.print(frame0+":"+frame1+"-"+corr_pairs_scaled+" ");
			}
			TDCorrTile [] tdCorrTiles = cuasMotion.correlatePairs(
					clt_parameters, // CLTParameters clt_parameters,
					fpixels,        // float [][]    fpixels,
					frame0,         // int           frame0,
					frame1,         // int           frame1,
					corr_pairs_scaled, // corr_pairs,     // int           frame_len,
					corr_ra_step,   // int           corr_ra_step,
					smooth,         // boolean       smooth, // use cosine mask
					true,           // batch_mode,     // final boolean batch_mode,
					dbg_suffix,     // final String  dbg_suffix, // for image_names
					debugLevel);    // int           debugLevel)
			double scale_fat_zero = 1.0 / boost_pairs; // TODO: scale according to actual number of pairs compared to nominal number of pairs
			
			// convert to pixel domain and display 
			double [][] corr_tiles_pd = cuasMotion.convertTDtoPD(
					tdCorrTiles,    // final TDCorrTile [] tiles,
					0xFE,           // final int           corr_type, // 0xFE
					fat_zero * scale_fat_zero, // *scale_fat_zero,  // final double        gpu_fat_zero,
					debugLevel);    // final int           debug_level

			// Indices into TDCorrTile.getMismatchVector() result {vx, vy, strength, fraction} // By Claude on 05/08/2026
			final int INDX_VX = 0, INDX_VY = 1, INDX_STRENGTH = 2, INDX_FRAC = 3;
			double [][] vector_field = TDCorrTile.getMismatchVector( // full tiles in gpu (512*512)
					corr_tiles_pd,  // final double[][] tiles,
					rstr,           // double              rmax,
					cent_radius,    // final double        centroid_radius, // 0 - all same weight, > 0 cosine(PI/2*sqrt(dx^2+dy^2)/rad)
					n_recenter,     // final int           n_recenter); //  re-center window around new maximum. 0 -no refines (single-pass)
					true);            // final boolean     calc_fraction	){ // calculate fraction inside center circle

			for (int ntile = 0; ntile < num_tiles; ntile++) if (vector_field[ntile] != null){
				double [] target =  CuasMotionLMA.getEmpty();
				// Store the raw displacement over one correlation-pair separation
				// (cuas_corr_offset scenes). Convert later with getFrameVelocityScale()
				target[CuasMotionLMA.RSLT_VX] =    vector_field[ntile][INDX_VX] * scale_vxy;
				target[CuasMotionLMA.RSLT_VY] =    vector_field[ntile][INDX_VY] * scale_vxy;
				target[CuasMotionLMA.RSLT_VSTR] =  vector_field[ntile][INDX_STRENGTH];
				target[CuasMotionLMA.RSLT_VFRAC] = vector_field[ntile][INDX_FRAC];
				targets[nscan][ntile] = target;
			}
			// for debugging: to see each 2d correlation
			if (corr2d != null) {
				corr2d[nscan] = corr_tiles_pd;
			}
			IJ.showProgress(1.0*(nscan+1) / targets.length);	
		}
		if (debugLevel > -4) {
			System.out.println();
		}
		IJ.showProgress(1.0);
		return targets;
	}

	// By Claude on 05/05/2026
	/**
	 * Refine motion vectors using a raised-cosine spatial mask around each known target.
	 * Reduces noise integration area from the full 16x16 tile to a small circle of radius r1.
	 * Re-runs pairwise phase correlation on the masked source frames (boosted pair count) and
	 * adds the resulting differential displacement to RSLT_VX/VY in targets_nonoverlap in-place.
	 * Called between shiftAndRenderAccumulate() and getAccumulatedCoordinatesMulti() so that
	 * getAccumulatedCoordinatesMulti() uses the refined vectors for target localisation.
	 * Only acts on tiles where RSLT_X/Y is already set (i.e., a prior centred pass ran).
	 */
	public static void refineMotionVectors(
			CLTParameters       clt_parameters,
			boolean             batch_mode,
			CuasMotion          cuasMotion,
			double              recalc_mv_boost,    // scale factor for number of correlation pairs (~4.0)
			double              recalc_mv_r0,       // inner radius in pixels: weight = 1.0
			double              recalc_mv_r1,       // outer radius in pixels: weight = 0.0
			float [][]          fpixels,            // source frames [nframes][width*height]
			double [][][]       targets_nonoverlap, // [nseq][ntile][] in-place RSLT_VX/VY update
			int                 frame0,             // frame index for nseq=0
			int                 corr_inc,           // frame step between scan positions
			int                 half_accum_range,   // corr_pairs/2
			boolean             smooth,
			int                 corr_offset,        // inter-frame distance for correlation pairs
			int                 dbg_iter_index,     // to generate different image names for different iteration to simplify "save as" images
			int                 debugLevel) {
		final int max_index_dbg = 10; // show images for the first iterations only
		final int tileSize = GPUTileProcessor.DTT_SIZE;   // 8 pixels
		final int tilesX   = cuasMotion.tilesX;
		final int width    = cuasMotion.gpu_max_width;
		final int height   = cuasMotion.gpu_max_height;
		final int nframes  = fpixels.length;

		final double fat_zero    = clt_parameters.imp.cuas_fat_zero;
		final double cent_radius = clt_parameters.imp.cuas_cent_radius;
		final int    n_recenter  = clt_parameters.imp.cuas_n_recenter;
		final double rstr        = clt_parameters.imp.cuas_rstr;

		// Boosted pair count, centered on frame_center
		final int corr_pairs_ref = (int) Math.round(2 * half_accum_range * recalc_mv_boost);

		// Hard-coded debug selectors: set >= 0 to enable per-scan/per-tile visualisation
		final int dbg_nseq = 40; // 41; // 157; // -(116); // 57; // 20; // -1;
		final int dbg_tile = 52+38*80;  // 50+38*80; // 55+38*80; // -(17 + 33*80); // 48+32*80; // 50+38*80; // -1;

		// Pre-compute integer-pixel raised-cosine mask kernel once (shared across all nseq)
		final int r1i = (int) Math.ceil(recalc_mv_r1);
		final int mside = 2 * r1i + 1;
		final float[] mask_kernel = new float[mside * mside];
		for (int dy = -r1i; dy <= r1i; dy++) {
			for (int dx = -r1i; dx <= r1i; dx++) {
				double r = Math.sqrt(dx * dx + dy * dy);
				float w;
				if      (r <= recalc_mv_r0) w = 1.0f;
				else if (r >= recalc_mv_r1) w = 0.0f;
				else w = (float)(0.5 * (Math.cos(Math.PI * (r - recalc_mv_r0) / (recalc_mv_r1 - recalc_mv_r0)) + 1.0));
				mask_kernel[(dy + r1i) * mside + (dx + r1i)] = w;
			}
		}
		if ((dbg_nseq >= 0 || dbg_tile >= 0) && (debugLevel >= 0) && (dbg_iter_index<= max_index_dbg)) {
			ShowDoubleFloatArrays.showArrays(mask_kernel.clone(), mside, mside,
					"refineMotionVectors-mask-r0_" + recalc_mv_r0 + "-r1_" + recalc_mv_r1+"-niter"+dbg_iter_index);
		}

		// Allocate staging array once; each nseq clears only the frames it needs
		final float[][] fpixels_masked = new float[nframes][width * height];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final boolean show_dbg = (dbg_nseq >= 0 || dbg_tile >= 0) && debugLevel >= 0;
		// Collect per-nseq correlation for end-of-method CORR2D stack
		final double[][][] corr2d_ref = show_dbg ? new double[targets_nonoverlap.length][][] : null;
		final String[] scene_titles_all = cuasMotion.getSceneTitles();

		for (int nseq = 0; nseq < targets_nonoverlap.length; nseq++) {
			// Skip scan positions where no target has a known centroid yet
			boolean has_centered = false;
			for (int ntile = 0; ntile < targets_nonoverlap[nseq].length; ntile++) {
				double[] t = targets_nonoverlap[nseq][ntile];
				if (t != null && !Double.isNaN(t[CuasMotionLMA.RSLT_X])) {
					has_centered = true;
					break;
				}
			}
			if (!has_centered) continue;

			int frame_center = frame0 + nseq * corr_inc;
			// Center the boosted correlation window on frame_center
			int frame0_ref  = frame_center - corr_pairs_ref / 2;
			int frame1_ref  = frame0_ref + corr_offset;

			// Determine the frame range actually accessed by correlatePairs
			// (pairs where frame0+dframe>=0 AND frame1+dframe<nframes)
			int fmin_alloc = Math.max(0, frame0_ref);
			int fmax_alloc = Math.min(nframes - 1, frame1_ref + corr_pairs_ref - 1);
			if (fmin_alloc > fmax_alloc) continue;  // no valid pairs in range

			// Clear staging frames for this scan position
			for (int f = fmin_alloc; f <= fmax_alloc; f++) Arrays.fill(fpixels_masked[f], 0.0f);

			// Pre-extract tile center/velocity for thread-safe parallel access
			final int ntiles = targets_nonoverlap[nseq].length;
			final boolean[] tile_valid = new boolean[ntiles];
			final double[]  tcx0 = new double[ntiles];
			final double[]  tcy0 = new double[ntiles];
			final double[]  tvx  = new double[ntiles];
			final double[]  tvy  = new double[ntiles];
			for (int ntile = 0; ntile < ntiles; ntile++) {
				double[] target = targets_nonoverlap[nseq][ntile];
				if (target == null || Double.isNaN(target[CuasMotionLMA.RSLT_X])) continue;
				tile_valid[ntile] = true;
				tcx0[ntile] = (ntile % tilesX) * tileSize + tileSize / 2.0 + target[CuasMotionLMA.RSLT_X];
				tcy0[ntile] = (ntile / tilesX) * tileSize + tileSize / 2.0 + target[CuasMotionLMA.RSLT_Y];
				tvx[ntile]  = target[CuasMotionLMA.RSLT_VX];
				tvy[ntile]  = target[CuasMotionLMA.RSLT_VY];
			}

			// Parallel over frames: each thread applies all tile masks to one frame
			final int fmin_f  = fmin_alloc;
			final int frange  = fmax_alloc - fmin_f + 1;
			final int fcenter = frame_center;
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nFr = ai.getAndIncrement(); nFr < frange; nFr = ai.getAndIncrement()) {
							int f = fmin_f + nFr;
							float[] src = fpixels[f];
							float[] dst = fpixels_masked[f];
							double offset_scale = (double)(f - fcenter) / corr_offset;
							for (int ntile = 0; ntile < ntiles; ntile++) {
								if (!tile_valid[ntile]) continue; // By Claude on 05/06/2026
								double cx = tcx0[ntile] + tvx[ntile] * offset_scale;
								double cy = tcy0[ntile] + tvy[ntile] * offset_scale;
								int icx = (int) Math.round(cx);
								int icy = (int) Math.round(cy);
								for (int dy = -r1i; dy <= r1i; dy++) {
									int py = icy + dy;
									if (py < 0 || py >= height) continue;
									for (int dx = -r1i; dx <= r1i; dx++) {
										int px = icx + dx;
										if (px < 0 || px >= width) continue;
										float w = mask_kernel[(dy + r1i) * mside + (dx + r1i)];
										if (w == 0.0f) continue;
										dst[py * width + px] += w * src[py * width + px];
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);

			// Debug: show full fmin..fmax masked frame stack for dbg_nseq
			if ((nseq == dbg_nseq) && (debugLevel >= 0) && (dbg_iter_index<= max_index_dbg)) {
				int fcount = fmax_alloc - fmin_alloc + 1;
				float[][] stack_slices = new float[fcount][];
				String[] stack_titles = new String[fcount];
				for (int fi = 0; fi < fcount; fi++) {
					stack_slices[fi] = fpixels_masked[fmin_alloc + fi].clone();
					int f = fmin_alloc + fi;
					stack_titles[fi] = (scene_titles_all != null && f < scene_titles_all.length)
							? scene_titles_all[f] : "f" + f;
				}
				ShowDoubleFloatArrays.showArrays(stack_slices, width, height, true,
						"refineMotionVectors-masked-nseq" + nseq+"-niter"+dbg_iter_index, stack_titles);
			}
			if ((nseq == dbg_nseq) && (debugLevel >= 0) && (dbg_iter_index<= max_index_dbg)) {
				int fcount = fmax_alloc - fmin_alloc + 1;
				float[][] stack_slices = new float[fcount][];
				String[] stack_titles = new String[fcount];
				for (int fi = 0; fi < fcount; fi++) {
					stack_slices[fi] = fpixels[fmin_alloc + fi].clone();
					int f = fmin_alloc + fi;
					stack_titles[fi] = (scene_titles_all != null && f < scene_titles_all.length)
							? scene_titles_all[f] : "f" + f;
				}
				ShowDoubleFloatArrays.showArrays(stack_slices, width, height, true,
						"refineMotionVectors-nseq" + nseq+"-niter"+dbg_iter_index, stack_titles);
			}

			TDCorrTile[] tdCorrTiles = cuasMotion.correlatePairs( // By Claude on 05/06/2026
					clt_parameters,                   // CLTParameters clt_parameters
					fpixels_masked,                   // float [][]    fpixels
					frame0_ref,                       // int           frame0
					frame1_ref,                       // int           frame1
					corr_pairs_ref,                   // int           frame_len
					1,                                // int           corr_ra_step (direct accumulation, no rolling avg)
					targets_nonoverlap[nseq],         // double [][]   targets_mv (per-tile motion centers)
					frame_center,                     // int           frame_center_mv
					corr_offset,                      // int           corr_offset_mv
					smooth,                           // boolean       smooth
					batch_mode,                       // boolean       batch_mode
					null,                             // String        dbg_suffix
					debugLevel);

			double scale_fat_zero = 1.0 / recalc_mv_boost;
			double[][] corr_tiles_pd = cuasMotion.convertTDtoPD(
					tdCorrTiles,
					0xFE,
					fat_zero * scale_fat_zero,
					debugLevel);
			if (corr2d_ref != null) {
				corr2d_ref[nseq] = corr_tiles_pd;
			}

			// Indices into TDCorrTile.getMismatchVector() result {vx, vy, strength, fraction} // By Claude on 05/08/2026
			final int INDX_VX = 0, INDX_VY = 1;
			double[][] vector_field = TDCorrTile.getMismatchVector(
					corr_tiles_pd,
					-rstr,        // rmax < 0: no relative-strength filter (same convention as getTargetsFromCorr2d)
					cent_radius,
					n_recenter,
					true);


			// Add differential MV to targets_nonoverlap in-place
			for (int ntile = 0; ntile < targets_nonoverlap[nseq].length; ntile++) {
				double[] target = targets_nonoverlap[nseq][ntile];
				if (target == null || Double.isNaN(target[CuasMotionLMA.RSLT_X])) continue;
				if (vector_field[ntile] == null) continue;
				if (ntile == dbg_tile && debugLevel >= 0) {
					System.out.printf("refineMotionVectors(): nseq=%3d ntile=%4d  before: VX=%9.4f VY=%9.4f  delta: dVX=%8.4f dVY=%8.4f  after: VX1=%9.4f VY1=%9.4f\n",
							nseq, ntile,
							target[CuasMotionLMA.RSLT_VX], target[CuasMotionLMA.RSLT_VY],
							vector_field[ntile][INDX_VX],   vector_field[ntile][INDX_VY],
							target[CuasMotionLMA.RSLT_VX] + vector_field[ntile][INDX_VX],
							target[CuasMotionLMA.RSLT_VY] + vector_field[ntile][INDX_VY]);
				}
				target[CuasMotionLMA.RSLT_VX] += vector_field[ntile][INDX_VX];
				target[CuasMotionLMA.RSLT_VY] += vector_field[ntile][INDX_VY];
			}
		}
		// Debug: CORR2D overview — one slice per keyframe, same layout as *CORR2D.tiff
		if (show_dbg && (corr2d_ref != null) && (dbg_iter_index<= max_index_dbg)) {
			final int corr_size = 2 * GPUTileProcessor.DTT_SIZE - 1; // 15
			String[] slice_titles_ref = new String[targets_nonoverlap.length];
			for (int i = 0; i < targets_nonoverlap.length; i++) {
				int frame_center = frame0 + i * corr_inc;
				slice_titles_ref[i] = (scene_titles_all != null && frame_center < scene_titles_all.length)
						? scene_titles_all[frame_center] : "nseq" + i;
			}
			// By Claude on 05/07/2026: corr_partial_dbg NPEs on null layers (skipped sequences); fill with empty arrays
			final int num_tiles = cuasMotion.tilesX * cuasMotion.tilesY;
			for (int i = 0; i < corr2d_ref.length; i++) {
				if (corr2d_ref[i] == null) corr2d_ref[i] = new double[num_tiles][];
			}
			double[][] dbg_2d_corrs = ImageDtt.corr_partial_dbg(
					corr2d_ref,
					cuasMotion.tilesX,
					corr_size,
					clt_parameters.corr_border_contrast,
					debugLevel);
			ShowDoubleFloatArrays.makeArrays(
					dbg_2d_corrs,
					cuasMotion.tilesX * (corr_size + 1),
					cuasMotion.tilesY * (corr_size + 1),
					"refineMotionVectors-CORR2D"+"-n"+dbg_iter_index,
					slice_titles_ref).show();
		}
	}

	/**
	 * Convert Transform-domain 2D correlation to phase correlation,
	 * inverse-transform it to pixel domain and return result as 
	 * sparse array of double[] tiles mapped in linescan order. Empty
	 * tiles are null. 
	 * @param tiles
	 * @param corr_type
	 * @param gpu_fat_zero
	 * @param debug_level
	 * @return sparse array in line-scan order. Each element either null or double[225]
	 */
	public double [][]  convertTDtoPD(
			final TDCorrTile [] tiles,
			final int           corr_type, // 0xFE
			final double        gpu_fat_zero,
			final int           debug_level
			) {
		return TDCorrTile.convertTDtoPD(
				gpuQuad,       // final GpuQuad       gpuQuad,
				tiles,         // final TDCorrTile [] tiles,
				corr_type,     // 0xFEfinal int           corr_type, // 0xFE
				gpu_fat_zero,  // final double        gpu_fat_zero,
				debug_level);  // final int           debug_level)
	}
	
	
	
	public TDCorrTile [] correlatePairs(
			CLTParameters clt_parameters,
			float [][]    fpixels,
			int           frame0, // OK if <0
			int           frame1, // OK if >= fpixels.length
			int           frame_len,
			int           corr_ra_step,
			boolean       smooth, // use cosine mask
			final boolean batch_mode,
			final String  dbg_suffix, // for image_names
			int           debugLevel) {
		return correlatePairs(clt_parameters, fpixels, frame0, frame1, frame_len, corr_ra_step,
				null, 0, 1, smooth, batch_mode, dbg_suffix, debugLevel);
	}

	// By Claude on 05/06/2026
	public TDCorrTile [] correlatePairs(
			CLTParameters clt_parameters,
			float [][]    fpixels,
			int           frame0, // OK if <0
			int           frame1, // OK if >= fpixels.length
			int           frame_len,
			int           corr_ra_step,
			double [][]   targets_mv,       // null: uniform grid; non-null: per-tile motion-tracking centers
			int           frame_center_mv,  // only used when targets_mv != null
			int           corr_offset_mv,   // only used when targets_mv != null
			boolean       smooth, // use cosine mask
			final boolean batch_mode,
			final String  dbg_suffix, // for image_names
			int           debugLevel) {
		TDCorrTile [] tdCorrTiles = new TDCorrTile[tilesX * tilesY];
		double sw = 0;
		for (int dframe = corr_ra_step/2; dframe < frame_len; dframe += corr_ra_step) {
			double weight = smooth ? Math.sin((dframe+0.5)/frame_len*Math.PI) : 1.0;
			if (((frame0+dframe) >= 0) && ((frame1+dframe) < fpixels.length)) {
				String dbg_n_suffix = (dbg_suffix != null) ? (dbg_suffix+"-"+dframe) : null;
				double offset_scale_ref = (targets_mv != null) ?
						(double)(frame0 + dframe - frame_center_mv) / corr_offset_mv : 0.0;
				double offset_scale_img = (targets_mv != null) ?
						(double)(frame1 + dframe - frame_center_mv) / corr_offset_mv : 0.0;
				TDCorrTile [] pairTiles = correlatePair(
						clt_parameters,          // CLTParameters clt_parameters,
						fpixels[frame0+dframe],  // float []      fpixels_ref,
						fpixels[frame1+dframe],  // float []      fpixels_img,
						targets_mv,              // double [][]   targets_mv,
						offset_scale_ref,        // double        offset_scale_ref,
						offset_scale_img,        // double        offset_scale_img,
						batch_mode,              // final boolean batch_mode,
						dbg_n_suffix,            // final String  dbg_suffix,
						debugLevel);             // int           debugLevel)
				TDCorrTile.accumulate(
						tdCorrTiles,             // final TDCorrTile [] dst,
						pairTiles,               // final TDCorrTile [] src,
						weight);                 // final double        src_weight)
				sw += weight;
			}
		}
		double scale = 1.0/sw;
		for (TDCorrTile tile : tdCorrTiles) if (tile != null) {
			tile.scale(scale);
		}
		return tdCorrTiles;
	}
	
	public void  accumulateMotionScene(
			CLTParameters     clt_parameters,
			final boolean     center,
			float []          fpixels,
			final double [][] target_sequence,
			final int         erase, // -1 - no, 0 - to 0, 1 - to NaN
			final boolean     batch_mode,
			final double      offset_scale,
			final double      magnitude_scale,
			final int         tilesX) {
		int [] wh = {gpu_max_width, gpu_max_height};
			TpTask [] tp_tasks = GpuQuad.setRectilinearMovingTasks(
					target_sequence,       // final double [][] vector_field,
					offset_scale,          // final double      offset_scale,
					magnitude_scale,       // final double      magnitude_scale,
					CuasMotionLMA.RSLT_VX, //  int         index_vx, 
					center ? CuasMotionLMA.RSLT_X: -1,                    // final int         index_xc,
					tilesX);               // final int         tilesX)
			image_dtt.setRectilinearReferenceTD(
					erase,                      // final int                 erase_clt,
					fpixels,                    // final float []            fpixels_ref,
					wh,                         // final int []              wh,               // null (use sensor dimensions) or pair {width, height} in pixels
					clt_parameters.img_dtt,     // final ImageDttParameters  imgdtt_params,    // Now just extra correlation parameters, later will include, most others
					false,                       // final boolean             use_reference_buffer,
					tp_tasks,                   // final TpTask[]            tp_tasks,
					clt_parameters.gpu_sigma_r, // final double              gpu_sigma_r,     // 0.9, 1.1
					clt_parameters.gpu_sigma_b, // final double              gpu_sigma_b,     // 0.9, 1.1
					clt_parameters.gpu_sigma_g, // final double              gpu_sigma_g,     // 0.6, 0.7
					clt_parameters.gpu_sigma_m, // final double              gpu_sigma_m,     //  =       0.4; // 0.7;
					batch_mode? -3: debugLevel);                // final int                 globalDebugLevel)
			boolean frame_debug = false;
			if (frame_debug) {
				renderFromTD (
						false, // boolean             use_reference,
						"render-from-TD"); // String              suffix)
			}
			return;
	}
	/**
	 * Calculate sparse ref_pXpYD for tiles with targets, for consecutive scenes corresponding to the same keyframe nsec 
	 * @param targets - targets for a single keyframe
	 * @return
	 */
	// Verify if the scenes are consecutive, not with some step ! They could now use a step not to correlate each pair after LPF of the input data
	// It could be wrong in the current code (04/12/2026)
	public double [][][] targetPxPyD( // original
			final double [][] targets) {
		final int half_accum_range = getSeqLength()/2;
		final int num_tiles = tilesX * tilesY;
		final int tileSize = GPUTileProcessor.DTT_SIZE;
		final double [][][] pXpYDs = new double [2* half_accum_range +1][num_tiles][];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] pXpYD = new double [3];
					for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) if (targets[nTile] != null) {
						double [] target = targets[nTile];
						int tileY = nTile / tilesX;
						int tileX = nTile % tilesX;
						double xc = tileSize * tileX + tileSize/2;  
						double yc = tileSize * tileY + tileSize/2;
						double xtk = xc + target[CuasMotionLMA.RSLT_X];  
						double ytk = yc + target[CuasMotionLMA.RSLT_Y];
						double vx = target[CuasMotionLMA.RSLT_VX]/corr_offset; // here vx is pixel offset between consecutive frames
						double vy = target[CuasMotionLMA.RSLT_VY]/corr_offset; // and target[CuasMotionLMA.RSLT_V{X,Y}] is the raw offset in a pair 
						double disp = target[CuasMotionLMA.RSLT_DISPARITY];
						if (Double.isNaN(disp)) {
							disp = 0.0;
						}
						pXpYD[2] = disp;
						for (int dseq = -half_accum_range; dseq <= half_accum_range; dseq++) {
							pXpYD[0] = xtk + vx * dseq;
							pXpYD[1] = ytk + vy * dseq;
							pXpYDs[dseq + half_accum_range][nTile] = pXpYD.clone();
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return pXpYDs;
	}
	
	public double [][][] targetPxPyDSingleTile(
			final double [][] targets,
			final int         ntile,
			final double      disparity,
			final int         first_offs,
			final int         last_offs) {
		final int num_tiles = tilesX * tilesY;
		final int tileSize = GPUTileProcessor.DTT_SIZE;
		final double [][][] pXpYDs = new double [last_offs-first_offs +1][num_tiles][];
		final double [] pXpYD = new double [3];
		double [] target = targets[ntile];
		if (target != null) {
			int tileY = ntile / tilesX;
			int tileX = ntile % tilesX;
			double xc = tileSize * tileX + tileSize/2;  
			double yc = tileSize * tileY + tileSize/2;
			double xtk = xc + target[CuasMotionLMA.RSLT_X];  
			double ytk = yc + target[CuasMotionLMA.RSLT_Y];
			double vx = target[CuasMotionLMA.RSLT_VX]/corr_offset; // here vx is pixel offset between consecutive frames?
			double vy = target[CuasMotionLMA.RSLT_VY]/corr_offset;
			double disp = disparity; // target[CuasMotionLMA.RSLT_DISPARITY];
			if (Double.isNaN(disp)) {
				disp = 0.0;
			}
			pXpYD[2] = disp;
			for (int dseq = first_offs; dseq <= last_offs; dseq++) {
				pXpYD[0] = xtk + vx * dseq;
				pXpYD[1] = ytk + vy * dseq;
				pXpYDs[dseq - first_offs][ntile] = pXpYD.clone();
			}
		} else {
			System.out.println ("targetPxPyDSingleTile(): BUG targets["+ntile+"] == null");
		}
		return pXpYDs;
	}
	
	
	/**
	 * Shift keyframe according to the vector_field, render and return result image. Does not need to reload keyframe image
	 * if it is already loaded in the GPU
	 * @param clt_parameters all the parameters
	 * @param fkeyframe keyframe data or null (if  keyframe did not change)
	 * @param targets [scene number][tile number] {vx, vy, ...}. nulls for empty tiles
	 * @param erase     -1 - no erase, 0 - to 0, 1 - to NaN  
	 * @param batch_mode disables graphics debug in batch mode
	 * @param offset_scale frame offset to multiply vX, vY when calculating total tile offsets
	 * @return
	 */
	public float [] renderMovingKeyframe(
			CLTParameters     clt_parameters,
			final boolean     center,
			float []          fkeyframe,   // if null, will assume it is already in the GPU
			final double [][] targets,
			final int         erase, // -1 - no, 0 - to 0, 1 - to NaN
			final boolean     batch_mode,
			final double      offset_scale) {
		int [] wh = {gpu_max_width, gpu_max_height};
			TpTask [] tp_tasks = GpuQuad.setRectilinearMovingTasks(
					targets,               // final double [][] vector_field,
					offset_scale,          // final double      offset_scale,
					0.0,                   // final double      magnitude_scale,
					CuasMotionLMA.RSLT_VX, //  int         index_vx, 
					-1,                    // final int         index_xc,
					tilesX);               // final int         tilesX)
			image_dtt.setRectilinearReferenceTD(
					erase,                      // final int                 erase_clt,
					fkeyframe,                  // final float []            fpixels_ref,
					wh,                         // final int []              wh,               // null (use sensor dimensions) or pair {width, height} in pixels
					clt_parameters.img_dtt,     // final ImageDttParameters  imgdtt_params,    // Now just extra correlation parameters, later will include, most others
					false,                      // final boolean             use_reference_buffer,
					tp_tasks,                   // final TpTask[]            tp_tasks,
					clt_parameters.gpu_sigma_r, // final double              gpu_sigma_r,     // 0.9, 1.1
					clt_parameters.gpu_sigma_b, // final double              gpu_sigma_b,     // 0.9, 1.1
					clt_parameters.gpu_sigma_g, // final double              gpu_sigma_g,     // 0.6, 0.7
					clt_parameters.gpu_sigma_m, // final double              gpu_sigma_m,     //  =       0.4; // 0.7;
					batch_mode? -3: debugLevel);                // final int                 globalDebugLevel)
			boolean frame_debug = false;
			if (frame_debug) {
				renderFromTD (
						false, // boolean             use_reference,
						"render-from-TD"); // String              suffix)
			}
			float [] rendered_frame = floatFromTD(false); 
			return rendered_frame;
	}
	
	
	
	
	public TDCorrTile [] correlatePair(
			CLTParameters clt_parameters,
			float []      fpixels_ref,
			float []      fpixels_img,
			final boolean batch_mode,
			final String  dbg_suffix, // for image_names
			int           debugLevel) {
		return correlatePair(clt_parameters, fpixels_ref, fpixels_img,
				null, 0.0, 0.0, batch_mode, dbg_suffix, debugLevel);
	}

	// By Claude on 05/06/2026
	public TDCorrTile [] correlatePair(
			CLTParameters  clt_parameters,
			float []       fpixels_ref,
			float []       fpixels_img,
			double [][]    targets_mv,      // null: uniform grid; non-null: per-tile motion-tracking centers
			double         offset_scale_ref,// (frame_ref - frame_center) / corr_offset; only used when targets_mv != null
			double         offset_scale_img,// (frame_img - frame_center) / corr_offset; only used when targets_mv != null
			final boolean  batch_mode,
			final String   dbg_suffix, // for image_names
			int            debugLevel) {
		int [] wh = {gpu_max_width, gpu_max_height};
		TpTask [] tp_ref, tp_img;
		if (targets_mv != null) {
			tp_ref = GpuQuad.setRectilinearMovingTasks(
					targets_mv,             // final double [][] targets_array,
					offset_scale_ref,       // final double      offset_scale,
					0.0,                    // final double      magnitude_scale,
					CuasMotionLMA.RSLT_VX,  // final int         index_vx,
					CuasMotionLMA.RSLT_X,   // final int         index_xc,
					tilesX);                // final int         tilesX
			tp_img = GpuQuad.setRectilinearMovingTasks(
					targets_mv,
					offset_scale_img,
					0.0,
					CuasMotionLMA.RSLT_VX,
					CuasMotionLMA.RSLT_X,
					tilesX);
		} else {
			Rectangle woi = new Rectangle(0, 0, wh[0], wh[1]);
			float [][] fpixels = {fpixels_ref, fpixels_img};
			TpTask [][] tp_tasks = GpuQuad.setRectilinearInterTasks(
					fpixels, wh[0], woi, null);
			tp_ref = tp_tasks[0];
			tp_img = tp_tasks[1];
		}
		int erase_cltr = -1;
		int erase_clt  = -1;
		image_dtt.setRectilinearReferenceTD(
				erase_cltr,                  // final int                 erase_clt,
				fpixels_ref,                 // final float []            fpixels_ref,
				wh,                          // final int []              wh,
				clt_parameters.img_dtt,      // final ImageDttParameters  imgdtt_params,
				true,                        // final boolean             use_reference_buffer,
				tp_ref,                      // final TpTask[]            tp_tasks,
				clt_parameters.gpu_sigma_r,  // final double              gpu_sigma_r,
				clt_parameters.gpu_sigma_b,  // final double              gpu_sigma_b,
				clt_parameters.gpu_sigma_g,  // final double              gpu_sigma_g,
				clt_parameters.gpu_sigma_m,  // final double              gpu_sigma_m,
				batch_mode ? -3 : debugLevel);
		if (!batch_mode && (dbg_suffix != null)) {
			renderFromTD(true, "ref"+dbg_suffix);
		}
		float [][][][] fcorr_td = new float [tilesY][tilesX][][];
		final double gpu_sigma_corr =     clt_parameters.getGpuCorrSigma(image_dtt.isMonochrome());
		final double gpu_sigma_rb_corr =  image_dtt.isMonochrome() ? 1.0 : clt_parameters.gpu_sigma_rb_corr;
		final double gpu_sigma_log_corr = clt_parameters.getGpuCorrLoGSigma(image_dtt.isMonochrome());
		image_dtt.interRectilinearCorrTD(
				clt_parameters.img_dtt,      // final ImageDttParameters  imgdtt_params,
				batch_mode,                  // final boolean batch_mode,
				erase_clt,                   // final int                 erase_clt,
				fpixels_img,                 // final float []            fpixels,
				wh,                          // final int []              wh,
				tp_img,                      // final TpTask[]            tp_tasks,
				fcorr_td,                    // final float  [][][][]     fcorr_td,
				clt_parameters.gpu_sigma_r,  // final double              gpu_sigma_r,
				clt_parameters.gpu_sigma_b,  // final double              gpu_sigma_b,
				clt_parameters.gpu_sigma_g,  // final double              gpu_sigma_g,
				clt_parameters.gpu_sigma_m,  // final double              gpu_sigma_m,
				gpu_sigma_rb_corr,           // final double              gpu_sigma_rb_corr,
				gpu_sigma_corr,              // final double              gpu_sigma_corr,
				gpu_sigma_log_corr,          // final double              gpu_sigma_log_corr,
				clt_parameters.corr_red,     // final double              corr_red,
				clt_parameters.corr_blue,    // final double              corr_blue,
				-1,                          // final int                 sensor_mask_inter,
				debugLevel);
		if (!batch_mode && (dbg_suffix != null)) {
			renderFromTD(false, "img"+dbg_suffix);
		}
		TDCorrTile [] tdCorrTiles = TDCorrTile.getFromGpu(gpuQuad);
		return tdCorrTiles;
	}
	
	public float [][] shiftAndRenderAccumulate(
			CLTParameters       clt_parameters,
			final boolean       center,
			final boolean       zero_fill,
			final float [][]    fpixels,
			final double [][][] target_sequence,
			final int           frame0,      // for vector_field[0]
			final int           frame_step,
			final int           half_range,  // scale for slow
			final double        boost_pairs,
			final boolean       smooth,
			final int           corr_offset, // interframe distance for correlation
			final boolean       batch_mode) {
		float [][] frames_accum = new float [target_sequence.length][];
		final int half_range_boosted = (int) (half_range * boost_pairs);
		
		final double [] window_full = new double [2 * half_range_boosted+1];
		double s0 = 1.0;
		window_full[half_range_boosted] = 1.0;
		double k = Math.PI/2/(half_range_boosted + 0.5);
		for (int i = 1; i <= half_range_boosted; i ++) {
			window_full[half_range_boosted + i] = smooth ? (Math.cos(i*k)):1.0;
			window_full[half_range_boosted - i] = smooth ? (Math.cos(i*k)):1.0; // By Claude on 05/11/2026
			s0+=2 * window_full[half_range_boosted + i];
		}
		for (int i = 0; i < window_full.length; i ++) {
			window_full[i] /= s0;
		}
		
//		final int erase = 1; //NaN
		for (int nseq = 0; nseq < target_sequence.length; nseq++) {
			int frame_center = frame0 + nseq * frame_step;
			boolean fits = (frame_center >= half_range_boosted) && (frame_center < (fpixels.length - half_range_boosted));
			double [] window = fits? window_full: window_full.clone();
			if (!fits) {
				s0=0;
				for (int i = 0; i < window.length; i++) {
					int i1 = frame_center - half_range_boosted + i;
					if ((i1 >= 0) && (i1 < fpixels.length)) {
						s0+= window[i];
					}
				}
				for (int i = 0; i < window.length; i++) {
					window[i] /= s0;
				}				
			}
			boolean first = true;
			for (int dframe = -half_range_boosted; dframe < half_range_boosted; dframe++) {
				double magnitude_scale = window [dframe + half_range_boosted];
				if (magnitude_scale > 0) { // to make sure <=0 did not get there accidentally
					int frame = frame_center + dframe;
					if ((frame >= 0) && (frame < fpixels.length) ) {
						if ((frame >=0) && (frame < fpixels.length)) {
							double offset_scale = (1.0 * dframe) / corr_offset;
							int erase = first ? 1 : -1; // NaN on first pass
							float [] fpix = first ? fpixels[frame].clone() : fpixels[frame];
							if (first) {
								for (int i = 0; i < fpix.length; i++) {
									fpix[i] *= -1.0;
								}
								magnitude_scale *= -1.0; // with >0 will set, not accumulate
							}
							accumulateMotionScene( // offsets Vx, Vy 
									clt_parameters,        // CLTParameters     clt_parameters,
									center,                // final boolean     center,
									fpix,                  // float []          fpixels,
									target_sequence[nseq], // final double [][] vector_field,
									erase,                 // final int         erase,
									batch_mode,            // final boolean     batch_mode,
									offset_scale,          // final double      offset_scale,
									magnitude_scale,       // final double      magnitude_scale, // first time - negative (will set)
									tilesX) ;              // final int         tilesX) {
							first = false;
//							result_frames[frame] = floatFromTD(false); // boolean  use_reference)
							boolean frame_debug = false;
							if (frame_debug) {
								renderFromTD (
										false, // boolean             use_reference,
										"render-from-TD_"+frame); // String              suffix)
							}
						}
					}
				}
			}
			// get form TD and invert
			frames_accum[nseq] =  floatFromTD(false); // boolean  use_reference)
			boolean frame_debug = false;
			if (frame_debug) {
				renderFromTD (
						false, // boolean             use_reference,
						"render-from-TD_accum"); // String              suffix)
			}
			
		}
		// invert multithreaded
		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() {
					// may be faster if process only where vector_field[nseq][ntile] is not null
					for (int nSeq = ai.getAndIncrement(); nSeq < frames_accum.length; nSeq = ai.getAndIncrement()) {
						for (int i = 0; i < frames_accum[nSeq].length; i++) {
							frames_accum[nSeq][i] *=-0.5; // By Claude on 05/11/2026: restore pre-fix amplitude (window fix doubled it)
							if (zero_fill && Double.isNaN(frames_accum[nSeq][i])){
								frames_accum[nSeq][i] = 0f;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		return frames_accum;
	}
	
	
	/**
	 * Generate target images in image pane
	 * @param clt_parameters
	 * @param centered         target is rendered in the center of the selected tile
	 * @param mask_width       mask width around the target    
	 * @param mask_blur        mask transition width    
	 * @param round            false - square (no blur), true - round
	 * @param target_keyframes accumulated target images (centered to the tiles if center is true)
	 * @param target_sequence  [scene][tile][] first two elements - targets vX, vY in pixels per frame. Empty are nulls, same velocities for 5x5  
	 * @param target_positions [scene][tile][] target positions of the targets relative to the tile center
	 * @param background       static scenes to be overlaid by the targets
	 * @param frame0           frame number of the background corresponding to the first keyframe 
	 * @param frame_step       numer of frames between key frames
	 * @param velocity_scale   velocity scale to apply to vX, vY. It is equal to 1.0/corr_ofset, where corr_offset is disparity in frames
	 *                         used for vector field generation
	 * @param batch_mode       batch mode - disable graphic debugging
	 * @param debugLevel       debug level
	 * @return float [][] rendered  
	 */
	public float [][] shiftAndRenderTargets(
			CLTParameters       clt_parameters,
			final boolean       centered,
			final double        mask_width,
			final double        mask_blur,
			final boolean       round,
			final float  [][]   target_keyframes,
			final double [][][] target_sequence,
			final float  [][]   background, // background image
			final int           frame0,
			final int           frame_step,
			final double        velocity_scale, // 1.0/(disparity in frames)
//			final double [][][] targets60hz, 
			final boolean       batch_mode,
			final int debugLevel) {
		final float [][] fpix_out = new float [background.length][];
		for (int i = 0; i < fpix_out.length; i++) {
			fpix_out[i]= background[i].clone();
		}
		final int tileSize = GPUTileProcessor.DTT_SIZE;		
		final int half_step0 = -(frame_step+1)/2;
		final int half_step1 = frame_step + half_step0;
		final int num_scenes = background.length;
		final int erase = 0; // probably not needed
		int [] poffs = new int[1];
		final double [] mask = createTargetMask (
				mask_width, // final double        mask_width,
				mask_blur,  // final double        mask_blur,
				round,      // final boolean       round,
				poffs);      // final int []        offs)
		final int offs = poffs[0];
		final int isize = 2* offs +1; 
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int nseq = 0; nseq < target_sequence.length; nseq++) {
			int fnseq = nseq;
			int frame_center = frame0 + nseq * frame_step;
			float [] keyframe = target_keyframes[nseq];
			ArrayList<Integer> target_list = new ArrayList<Integer>();
			for (int ntile = 0; ntile < target_sequence[nseq].length; ntile++) {
				double[] target = target_sequence[nseq][ntile]; 
				if ((target !=null) && (target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)){
					target_list.add(ntile);
				}
			}
			final int[] targets = target_list.stream().mapToInt(Integer::intValue).toArray();
			
			for (int dscene = half_step0; dscene < half_step1; dscene ++) {
				final int fdscene = dscene;
				int nscene = frame_center + dscene;
				if ((nscene >=0) && (nscene < num_scenes)) {
					double [][] targets_data = new double[targets.length][TARGET_LENGTH];
					float [] frame = renderMovingKeyframe( // uses targets that contain vx, vy
							clt_parameters, // CLTParameters     clt_parameters,
							// keep/velocity-shift regardless of if they are centered.
							false, // center,      // final boolean     center,
							keyframe,   // float []          fkeyframe,   // if null, will assume it is already in the GPU
							target_sequence[nseq], // final double [][] targets,
							erase,      // final int         erase, // -1 - no, 0 - to 0, 1 - to NaN
							batch_mode, // final boolean     batch_mode,
							-dscene*velocity_scale); // final double      offset_scale);
					ai.set(0);
					// previous renderMovingKeyframe() uses GPU, can not use threads
					/// TODO: Fix center parameter when copying below !
					for (int ithread = 0; ithread < threads.length; ithread++) {
						threads[ithread] = new Thread() {
							public void run() {
								for (int nTarget = ai.getAndIncrement(); nTarget < targets.length; nTarget = ai.getAndIncrement()) {
									int ntile = targets[nTarget];
//									double [] vvector = vector_field[fnseq][ntile];
									double [] target = target_sequence[fnseq][ntile];
									int tileX = ntile % tilesX;
									int tileY = ntile / tilesX;
									double xc = tileSize * tileX + tileSize/2;  
									double yc = tileSize * tileY + tileSize/2;
									double xtk = xc + target[CuasMotionLMA.RSLT_X];  
									double ytk = yc + target[CuasMotionLMA.RSLT_Y];
									double dx = target[CuasMotionLMA.RSLT_VX] * fdscene * velocity_scale;
									double dy = target[CuasMotionLMA.RSLT_VY] * fdscene * velocity_scale;
									targets_data[nTarget][TARGET_X] =               xtk + dx;
									targets_data[nTarget][TARGET_Y] =               ytk + dy;
									targets_data[nTarget][TARGET_VX] =              target[CuasMotionLMA.RSLT_VX];
									targets_data[nTarget][TARGET_VY] =              target[CuasMotionLMA.RSLT_VY];
									targets_data[nTarget][TARGET_STRENGTH] =        target[CuasMotionLMA.RSLT_A];
									targets_data[nTarget][TARGET_NTILE] =           ntile;
									targets_data[nTarget][TARGET_MISMATCH_BEFORE] = target[CuasMotionLMA.RSLT_MISMATCH_BEFORE];
									targets_data[nTarget][TARGET_MISMATCH_AFTER] =  target[CuasMotionLMA.RSLT_MISMATCH_AFTER];
									targets_data[nTarget][TARGET_SCORE] =           target[CuasMotionLMA.RSLT_QSCORE];
									targets_data[nTarget][TARGET_SEQLEN] =          target[CuasMotionLMA.RSLT_MATCH_LENGTH];
									targets_data[nTarget][TARGET_TRAVEL] =          target[CuasMotionLMA.RSLT_SEQ_TRAVEL];
									
									targets_data[nTarget][TARGET_DISPARITY] =       target[CuasMotionLMA.RSLT_DISPARITY];
									targets_data[nTarget][TARGET_DISP_STR] =        target[CuasMotionLMA.RSLT_DISP_STR];
									targets_data[nTarget][TARGET_DISP_DIFF] =       target[CuasMotionLMA.RSLT_DISP_DIFF];
									
									targets_data[nTarget][TARGET_RANGE] = // use RSLT_RANGE_LIN if available
											Double.isNaN(target[CuasMotionLMA.RSLT_RANGE_LIN])? target[CuasMotionLMA.RSLT_RANGE]:
												target[CuasMotionLMA.RSLT_RANGE_LIN];
									targets_data[nTarget][TARGET_ID] =              target[CuasMotionLMA.RSLT_TARGET_ID];
									
									
									
									int pxl = (int) Math.round(targets_data[nTarget][TARGET_X]) - offs;
									int pyt = (int) Math.round(targets_data[nTarget][TARGET_Y]) - offs;
									
									int pxl_targ = (int) Math.round(targets_data[nTarget][TARGET_X]) - offs;
									int pyt_targ = (int) Math.round(targets_data[nTarget][TARGET_Y]) - offs;
									if (centered) {
										pxl_targ = (int) Math.round(xc+dx) - offs;
										pyt_targ = (int) Math.round(yc+dy) - offs;
									}
									for (int ym = 0; ym < isize; ym++) {
										int py =    pyt + ym;
										int py_targ = pyt_targ + ym;
										if ((py >= 0) && (py < gpu_max_height)) {
											for (int xm = 0; xm < isize; xm++) {
												int px = pxl + xm;
												int px_targ = pxl_targ + xm;
												if ((px >= 0) && (px < gpu_max_width)) {
													int pix = py * gpu_max_width + px;
													int pix_targ = py_targ * gpu_max_width + px_targ;
													int pix_mask = ym * isize + xm;
													if ((mask[pix_mask] > 0) && !Double.isNaN(frame[pix])) {
														if (Float.isNaN(fpix_out[nscene][pix])) {
															fpix_out[nscene][pix] = frame[pix_targ];
														} else {
															double a = mask[pix_mask];
															fpix_out[nscene][pix] = (float) ((1 - a) * fpix_out[nscene][pix] + a * frame[pix_targ]); 
														}
													}
												}
											}
										}
									}									
								}
							}
						};
					}		      
					ImageDtt.startAndJoin(threads);
//					if (targets60hz != null) {
//						targets60hz[nscene] = targets_data;
//					}
					keyframe = null; // to prevent additional transfers to the GPU
				}
			}
		}	
		return fpix_out;
	}
	
	public static float [][] runningAverage(
			final float [][] fpixels,
			final int        ra_length,
			final int        width){
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		final int height =  num_pixels/width;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final float [][] out_pix = new float [num_scenes][num_pixels];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						Arrays.fill(out_pix[nSeq],Float.NaN);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					float [] line_ra = new float[width];
					for (int nLine = ai.getAndIncrement(); nLine < height; nLine = ai.getAndIncrement()) {
						Arrays.fill(line_ra, 0.0f);
						int pix0 = nLine* width;
						int head = 0;
						int tail = 0;
						for (; (head < num_scenes) || (tail < num_scenes);) {
							if (head < num_scenes) {
								float [] fpixels_head = fpixels[head];
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									line_ra[x] += fpixels_head[pix++]; 
								}
								head++;
							}
							if ((tail < (head - ra_length)) || (tail >= (num_scenes - ra_length))) {
								float [] fpixels_tail = fpixels[tail];
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									line_ra[x] -= fpixels_tail[pix++]; 
								}
								tail++;
							}
							int avg_len = head - tail;
							int avg_pos = (head + tail) / 2;
							if ((avg_pos < num_scenes) && (((avg_len + ra_length) % 2 == 0 ) || (head == 1))) { // update output array
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									out_pix[avg_pos][pix++] = line_ra[x]/avg_len; 
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);

		return out_pix;
	}
	
	public static float [][] runningGaussian(
			final float [][] fpixels,
			final double     ra_length,
			final int        width){
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		final int height =  num_pixels/width;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double sigma = 0.5* ra_length;
		final float [][] out_pix = new float [num_scenes][num_pixels];
		/*
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						Arrays.fill(out_pix[nSeq],Float.NaN);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		*/
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] temp_line = new double[width*num_scenes];
					DoubleGaussianBlur gb = new DoubleGaussianBlur();
					for (int nLine = ai.getAndIncrement(); nLine < height; nLine = ai.getAndIncrement()) {
						for (int nseq = 0; nseq < num_scenes; nseq++) {
							int src_index = nLine*width;
							int dst_index = nseq*width;
							for (int i = 0; i < width; i++) {
								temp_line[dst_index++] = fpixels[nseq][src_index++];
							}
						}
						gb.blur1Direction(
								temp_line,  // double [] pixels,
								width,      // int        width,
								num_scenes, // int       height,
								sigma,      // double     sigma,
								0.001,      // double   accuracy,
								false);     // boolean xDirection vertical along time line
						
						for (int nseq = 0; nseq < num_scenes; nseq++) {
							int dst_index = nLine*width;
							int src_index = nseq*width;
							for (int i = 0; i < width; i++) {
								out_pix[nseq][dst_index++] = (float)(temp_line[src_index++]);
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return out_pix;
	}

	public static float [][] runningGaussianNaN(
			final float [][] fpixels,
			final int        ra_length,
			final int        width){
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		final int height =  num_pixels/width;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double sigma = 0.5* ra_length;
		final float [][] out_pix = new float [num_scenes][num_pixels];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						System.arraycopy(
								fpixels[nSeq],
								0,
								out_pix[nSeq],
								0,
								num_pixels);
						/*
						for (int npix = 0; npix < num_pixels; npix++) {
							out_pix[nSeq][npix] = fpixels[nSeq][npix];
						}
						*/
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] temp_line = new double[width*num_scenes];
					boolean [] has_nans = new boolean[width];
					DoubleGaussianBlur gb = new DoubleGaussianBlur();
					for (int nLine = ai.getAndIncrement(); nLine < height; nLine = ai.getAndIncrement()) {
						Arrays.fill(has_nans, false);
						boolean has_some_nans = false; 
						for (int nseq = 0; nseq < num_scenes; nseq++) {
							int src_index = nLine*width;
							int dst_index = nseq*width;
							for (int x = 0; x < width; x++) {
								double dpix = out_pix[nseq][src_index++];
								temp_line[dst_index++] = dpix;
								boolean nan = Double.isNaN(dpix);
								has_nans[x] |= nan;
								has_some_nans |= nan;
							}
						}
						if (has_some_nans) {
							for (int x = 0; x <  width; x++) if (has_nans[x]){
								// duplicate first/last, interpolate middle
								if (Double.isNaN(temp_line[x])) {
									int i;
									for (i = 1; (i < num_scenes) && Double.isNaN(temp_line[i * width+x]) ; i++); //Index 318080 out of bounds for length 318080
									temp_line[x] = temp_line[i * width+x];
								}
								if (Double.isNaN(temp_line[(num_scenes-1)*width + x])) {
									int i;
									for (i = num_scenes-1; Double.isNaN(temp_line[i*width+x]) && (i >=0); i--);
									temp_line[(num_scenes - 1) * width + x] = temp_line[i*width+x];
								}
								// interpolate remaining gaps
								int i;
								for (i=0; i < num_scenes; i++) {
									if (Double.isNaN(temp_line[i * width + x])) {
										int gap_start = i-1;
										for (i = i+1; Double.isNaN(temp_line[i * width + x]); i++); // last is not NaN, set above
										int gap_end = i;
										double v0 = temp_line[gap_start * width + x];
										double v1 = temp_line[gap_end * width +   x];
										for (int k = 1; k < (gap_end - gap_start); k++) {
											temp_line[(gap_start + k) * width + x] = v0 + (v1-v0) * k; 
										}
									}
								}
							}
						}
						gb.blur1Direction(
								temp_line,  // double [] pixels,
								width,      // int        width,
								num_scenes, // int       height,
								sigma,      // double     sigma,
								0.001,      // double   accuracy,
								false);     // boolean xDirection vertical along time line
						
						for (int nseq = 0; nseq < num_scenes; nseq++) {
							int dst_index = nLine*width;
							int src_index = nseq*width;
							for (int i = 0; i < width; i++) {
								out_pix[nseq][dst_index++] = (float)(temp_line[src_index++]);
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return out_pix;
	}
	
	
	
	
	/**
	 * Version of runningAverage) that tolerates NaN value. It modifies fpixels[][] replacing first/last scene values if they were NaNs 
	 * @param fpixels
	 * @param ra_length
	 * @param width
	 * @return
	 */
	public static float [][] runningAverageNaN(
			final float [][] fpixels,
			final int        ra_length,
			final int        width){
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		final int height =  num_pixels/width;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final float [][] out_pix = new float [num_scenes][num_pixels];
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						Arrays.fill(out_pix[nSeq],Float.NaN);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					float [] line_ra = new float[width];
					int [] line_ra_length = new int [width];
					for (int nLine = ai.getAndIncrement(); nLine < height; nLine = ai.getAndIncrement()) {
						Arrays.fill(line_ra, 0.0f);
						Arrays.fill(line_ra_length, 0);
						int pix0 = nLine* width;
						// fix first and last pixels if they are nulls;
						for (int x = 0; x < width; x++) {
							if (Float.isNaN(fpixels[0][pix0+x])) {
								int i;
								for (i = 1; Float.isNaN(fpixels[i][pix0+x]) && (i < num_scenes); i++);
								fpixels[0][pix0+x] = fpixels[i][pix0+x];
							}
							if (Float.isNaN(fpixels[num_scenes-1][pix0+x])) {
								int i;
								for (i = num_scenes-1; Float.isNaN(fpixels[i][pix0+x]) && (i >=0); i--);
								fpixels[num_scenes-1][pix0+x] = fpixels[i][pix0+x];
							}							
						}
						
						int head = 0;
						int tail = 0;
						for (; (head < num_scenes) || (tail < num_scenes);) {
							if (head < num_scenes) {
								float [] fpixels_head = fpixels[head];
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									float d = fpixels_head[pix++];
									if (!Float.isNaN(d)) {
										line_ra[x] += d;
										line_ra_length[x] ++;
									}
								}
								head++;
							}
							if ((tail < (head - ra_length)) || (tail >= (num_scenes - ra_length))) {
								float [] fpixels_tail = fpixels[tail];
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									float d = fpixels_tail[pix++];
									if (!Float.isNaN(d)) {
										line_ra[x] -= d;
										line_ra_length[x]--;
									}
								}
								tail++;
							}
							int avg_len = head - tail;
							int avg_pos = (head + tail) / 2;
							if ((avg_pos < num_scenes) && (((avg_len + ra_length) % 2 == 0 ) || (head == 1))) { // update output array
								int pix = pix0; 
								for (int x = 0; x < width; x++) {
									out_pix[avg_pos][pix++] = line_ra[x]/line_ra_length[x]; // avg_len;// should never be 0 as the first and the last are not NaN; 
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return out_pix;
	}
	
	public static float [][] removeOutliers(
			final float [][] fpixels,
			final float [][] fpixels_avg,
			double      threshold){
		final float fthreshold = (float) threshold;
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		float [][] fpixels_filtered = new float [num_scenes][];
		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 nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						float [] fpix = fpixels[nSeq];
						float [] apix = fpixels_avg[nSeq];
						fpixels_filtered[nSeq]= fpix.clone();
						for (int pix = 0; pix < num_pixels; pix++) {
							if (Math.abs(fpix[pix] - apix[pix]) > fthreshold) {
								fpixels_filtered[nSeq][pix] = Float.NaN;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		return fpixels; // should be fpixels_filtered ???
	}
	
	public static float [][] subtract(
			float [][] fpixels,
			float [][] fpixels_avg){
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		float [][] fpixels_diff = new float [num_scenes][num_pixels];
		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 nSeq = ai.getAndIncrement(); nSeq < num_scenes; nSeq = ai.getAndIncrement()) {
						float [] fpix = fpixels[nSeq];
						float [] apix = fpixels_avg[nSeq];
						float [] dpix = fpixels_diff[nSeq];
						for (int pix = 0; pix < num_pixels; pix++) {
							dpix[pix] = fpix[pix] - apix[pix];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ai.set(0);
		return fpixels_diff;
	}
	
	
	

	private static double [] createTargetMask (
			final double        mask_width,
			final double        mask_blur,
			final boolean       round,
			final int []        offs) {
		int iwidth = 2*(((int) Math.ceil(mask_width))/2) + 1; // minimal odd
		double [] mask = new double [iwidth*iwidth];
		double r1 = mask_width/2;
		double r0 = r1 - mask_blur;
		double r02 = r0*r0;
		double r12 = r1*r1;
		int c = iwidth/2;
		if (offs != null) {
			offs[0] = c;
		}
		if (!round) {
			Arrays.fill(mask,  1.0);
		} else {
			for (int i = 0; i < mask.length; i++) {
				double x = (i % iwidth) - c;
				double y = (i / iwidth) - c;
				double r2 = x*x+y*y;
				if (r2 < r02) {
					mask[i] = 1.0;
				} else if (r2 >= r12) {
					mask[i] = 0.0;
				} else if (mask_blur > 0){
					double r = Math.sqrt(r2);
					double a = Math.PI * (r - r0) / mask_blur;
					mask[i] = 0.5 * (1.0 + Math.cos(a));
				}
			}
		}
		return mask;
	}
	
	
	public void renderFromTD (
			boolean             use_reference,
			String              suffix) {
		gpuQuad.execImcltRbgAll(
        		(gpuQuad.getNumColors() <=1), // isMonochrome(),
        		use_reference,
				null); // wh); //int [] wh
		// get data back from GPU
		final float [][][] iclt_fimg = new float [gpuQuad.getNumSensors()][][];
		for (int ncam = 0; ncam < iclt_fimg.length; ncam++) {
			iclt_fimg[ncam] = gpuQuad.getRBG(ncam); // updated window
		}
		int out_width =  gpuQuad.getImageWidth();
		int out_height = gpuQuad.getImageHeight();
        ShowDoubleFloatArrays.showArrays( 
        		iclt_fimg[0],
        		out_width,
        		out_height,
        		true,
        		suffix);
	}

	public double [] doubleFromTD (
			boolean  use_reference) {
		gpuQuad.execImcltRbgAll(
        		(gpuQuad.getNumColors() <=1), // isMonochrome(),
        		use_reference,
				null); // wh); //int [] wh
		// get data back from GPU
		final float [][][] iclt_fimg = new float [gpuQuad.getNumSensors()][][];
		for (int ncam = 0; ncam < iclt_fimg.length; ncam++) {
			iclt_fimg[ncam] = gpuQuad.getRBG(ncam); // updated window
		}
		double [] dpixels = new double [iclt_fimg[0].length];
		for (int i = 0; i < dpixels.length; i++) {
			dpixels[i] = iclt_fimg[0][0][i];
		}
		return dpixels;
	}
	
	public float [] floatFromTD (
			boolean  use_reference) {
		gpuQuad.execImcltRbgAll(
        		(gpuQuad.getNumColors() <=1), // isMonochrome(),
        		use_reference,
				null); // wh); //int [] wh
		// get data back from GPU
		final float [][][] iclt_fimg = new float [gpuQuad.getNumSensors()][][];
		for (int ncam = 0; ncam < iclt_fimg.length; ncam++) {
			iclt_fimg[ncam] = gpuQuad.getRBG(ncam); // updated window
		}
		return iclt_fimg[0][0].clone();
	}
	

	public ImagePlus convertToRgbAnnotateTargets(
			CLTParameters       clt_parameters,
			final boolean       clean_pass,
			final double        input_range, // 5
			final boolean       scale2x,
			final int           target_type, //  = 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
			final int           known_type, // 2;  // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
			final double        known_err,  // 20; //  Maximal distance between the detected target and UAS log position (in raw image pixels);
			final float  [][]   fpixels,
///			final double [][][] targets60hz,
			final boolean       change_color,
			final boolean       show_uas,
			final int           frame0,
			final int           frame_step,
			final int           width_src,
			final String        title,
			final String []     titles,
			final UasLogReader  uasLogReader,			
			final int           debugLevel) {
		final boolean annot_missing =     clt_parameters.imp.cuas_annot_missing; // false; // make a parameter.Reserve a line for requested but missing parameters
		int annot_mode0 = clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_IMAGE_PANE_INDX]; 
		if (clean_pass) {
			annot_mode0 &= clt_parameters.imp.cuas_annot_sel[ANNOT_CLEAN_VIEW_INDX]; //ANNOT_CLEAN;
		}
		final int annot_mode = annot_mode0; 
		if (debugLevel > -4) {
			System.out.println(String.format("Image annotation mode 0x%04x (0x%04x & 0x%04x)",
					annot_mode, clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_RADAR_PANE_INDX],clt_parameters.imp.cuas_annot_sel[ANNOT_CLEAN_VIEW_INDX]));
		}
		// parentCLT
//		final GeometryCorrection gc = parentCLT.getGeometryCorrection();
//		String uas_log_path = null;
		boolean  annotate_uas =  show_uas && (uasLogReader != null);
		/*
		if (annotate_uas) {
			String resource_name = ICON_BLUE;
			URL resourceUrl = CuasMotion.class.getClassLoader().getResource("graphics/"+resource_name);
			Path resourcePath = null;
			try {
				resourcePath = Paths.get(resourceUrl.toURI());
			} catch (URISyntaxException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			uas_log_path = resourcePath.toString();
			System.out.println("uas_log_path="+uas_log_path);
		}
		*/
		final ErsCorrection ersCorrection = parentCLT.getErsCorrection(); 
		final double [] camera_atr = (uasLogReader != null) ? uasLogReader.getCameraATR() : ersCorrection.getCameraATR();
		// prepare UAS log icon (blue circle)
		ColorProcessor uaslog_cp =	annotate_uas? getUasLogIconColorProcessor():null;

		// prepare target icons
		ColorProcessor cp_target_icon = getIconColorProcessor(
				target_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		ColorProcessor cp_uas_icon = getIconColorProcessor(
				known_type, // uas_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		final int uaslog_width = (uaslog_cp != null) ? uaslog_cp.getWidth() : 0; 
		final int target_icon_width = cp_target_icon.getWidth(); 
		final int uas_icon_width =    cp_uas_icon.getWidth();
		final int [] uaslog_pixels =      (uaslog_cp != null) ? (int[]) uaslog_cp.getPixels() : null;
		final int [] target_icon_pixels = (int[]) cp_target_icon.getPixels();
		final int [] uas_icon_pixels =    (int[]) cp_uas_icon.getPixels();
		final int [][] uaslog_rgba =      (uaslog_pixels != null) ? splitColorIcon(uaslog_pixels) : null;
		final int [][] target_icon_rgba = splitColorIcon(target_icon_pixels);
		final int [][] uas_icon_rgba =    splitColorIcon(uas_icon_pixels);
		
		final int scale = scale2x? 2 : 1;
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		final boolean annotate_mono =             clt_parameters.imp.annotate_mono; 
		final boolean annotate_transparent_mono = clt_parameters.imp.annotate_transparent_mono;
		final Color annotate_color_mono =         clt_parameters.imp.annotate_color_mono;
		
		final boolean annotate =   clt_parameters.imp.cuas_annotate;
		final Color   text_color = clt_parameters.imp.cuas_text_color;
		final Color   text_bg_color = clt_parameters.imp.cuas_text_bg;
		final Color   selected_color = clt_parameters.imp.cuas_selected_color;
		
		final boolean transparent_other = clt_parameters.imp.cuas_transparent;
		final boolean transparent_uas =   clt_parameters.imp.cuas_transparent_uas;
		
		
		final String  font_name =  clt_parameters.imp.cuas_font_name;
		final int     font_size =  clt_parameters.imp.cuas_font_size;
		final int     font_type =  clt_parameters.imp.cuas_font_type;
		final int     space_before_text = 2 * scale;
		final double  font_ratio =   clt_parameters.imp.cuas_font_spacing; // 1.2; // if 0 - will use default spacing ( ~=1.5)
		final int tileSize = GPUTileProcessor.DTT_SIZE;		
		final int num_scenes = fpixels.length;
		final int num_pixels = fpixels[0].length;
		final int num_seq = targets.length;
		ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
				fpixels, // float[][] pixels,
				width_src,   // int width,
				num_pixels/width_src,  // int height,
				title,   // String title,
				titles); // String [] titles)
		imp.getProcessor().setMinAndMax(-input_range/2, input_range/2);
		String imp_title = imp.getTitle();
		int width_new =  scale * imp.getWidth(); 
		int height_new = scale * imp.getHeight(); 
		if (scale2x) {
			ImageStack stack = imp.getStack();
			String [] labels = new String [stack.getSize()];
			for (int i = 0; i < labels.length; i++) {
				labels[i] = stack.getSliceLabel(i+1);
			}
			final FloatProcessor [] stackfp =  new FloatProcessor[stack.getSize()];
			for (int i = 0; i < stackfp.length; i++) {
				stackfp[i] = (FloatProcessor) stack.getProcessor(i+1);
			}
			System.out.println("stackfp.length="+stackfp.length);
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nSlice = ai.getAndIncrement(); nSlice < stackfp.length; nSlice = ai.getAndIncrement()) {
							stackfp[nSlice] = (FloatProcessor) stackfp[nSlice].resize(stackfp[nSlice].getWidth() * 2, stackfp[nSlice].getHeight()*2);
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			stack = new ImageStack(width_new,height_new);
			for (int i = 0; i < stackfp.length; i++) {
				stack.addSlice(labels[i],stackfp[i]);
			}			
			imp = new ImagePlus(imp_title, stack); // imp.getTitle(),stack);
		}
		final FloatProcessor [] float_processors = new FloatProcessor[imp.getStack().getSize()];
		final ColorProcessor [] color_processors = new ColorProcessor[float_processors.length];
		final String [] cp_labels = new String [imp.getStack().getSize()];
		for (int i = 0; i < float_processors.length; i++) {
			float_processors[i] = (FloatProcessor) imp.getStack().getProcessor(i+1);
			cp_labels[i] = imp.getStack().getSliceLabel(i+1);
		}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < float_processors.length; nSlice = ai.getAndIncrement()) {
						ImagePlus imp1 = new ImagePlus(cp_labels[nSlice],float_processors[nSlice]);
						ImageConverter imageConverter = new ImageConverter(imp1);
						imageConverter.convertToRGB();
						color_processors[nSlice] = (ColorProcessor) imp1.getProcessor();
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ImageStack color_stack = new ImageStack(width_new,height_new);
		for (int i = 0; i < color_processors.length; i++) {
			color_stack.addSlice(cp_labels[i],color_processors[i]);
		}			
		imp = new ImagePlus(imp_title, color_stack); // imp.getTitle(),stack);
		final int width = imp.getProcessor().getWidth();
		final int height = imp.getProcessor().getHeight();
		final Color fcolor = annotate_color_mono;
		final ImageStack fstack_scenes = imp.getImageStack();
		final int posX=  width - scale * 119; // 521;
		final int posY= height + scale *   1;  // 513;
		final Font font = new Font("Monospaced", Font.PLAIN, scale * 12);
		final Font font_target = new Font(font_name, font_type, scale * font_size);
		final int nSlices = fstack_scenes.getSize();
		if (annotate_mono) {
			ai.set(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_mono) {
								ip.drawString(scene_title, posX, posY); // transparent
							} else {
								ip.drawString(scene_title, posX, posY, Color.BLACK); // transparent
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		ImageStack stack = imp.getStack();
		int [][] ipixels = new int [num_scenes][];
		for (int i = 0; i < num_scenes; i++) {
			ipixels[i] = (int[]) stack.getPixels(i+1);
		}
		ai.set(0);
		final AtomicInteger amax =  new AtomicInteger(-1);
		final int [] uas_tiles = new int[num_seq];
		Arrays.fill(uas_tiles, -1);
		final int [][] local_tiles = new int [num_seq][];

		final int num_tiles = targets[0].length; // java.lang.NullPointerException
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < targets.length; nSeq = ai.getAndIncrement()) {
						double [][] targets_seq = targets[nSeq];
						/// key - local target_id > 0
						/// Key 0 - special case: tiles where UAS log data is stored (may be the same or different than detected tiles
						/// value - tile index 
						HashMap<Integer,Integer> local_tiles_map = new HashMap<Integer,Integer>(); //  
						int max_ltarg = -1;
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] target = targets_seq[ntile];
							if (target != null) {
								if (!Double.isNaN(target[CuasMotionLMA.RSLT_FL_PX])) {
									local_tiles_map.put(0, ntile); // flight log data
									max_ltarg = Math.max(max_ltarg, 0);
								}
								if (!Double.isNaN(target[CuasMotionLMA.RSLT_GLOBAL])) {
									int ltarg = (int) target[CuasMotionLMA.RSLT_GLOBAL];
									max_ltarg = Math.max(max_ltarg, ltarg);
									local_tiles_map.put(ltarg, ntile); // target data
								}								
							}
						}
						amax.getAndAccumulate(max_ltarg, Math::max);
						local_tiles[nSeq] = new int[max_ltarg+1];
						Arrays.fill(local_tiles[nSeq],-1);
						for (int i = 0; i <= max_ltarg; i++) if (local_tiles_map.containsKey(i)) {
							local_tiles[nSeq][i] = local_tiles_map.get(i);
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);

		final int max_ltarg_all = amax.get();
		if (max_ltarg_all < 0) {
			System.out.println(
					"convertToRgbAnnotateTargets(): no detected targets or UAS-log targets available for annotation in \"" +
					title + "\"; returning RGB stack without target overlays.");
			return imp;
		}

		final int [][] ltargets_first_last = new int [Math.max(0, amax.get())][]; // correct
		
		for (int nseq = 0; nseq < num_seq; nseq++) if ((local_tiles[nseq] != null) && (local_tiles[nseq].length > 0)){ // no if is not needed - bug fixed
			uas_tiles[nseq] = local_tiles[nseq][0];
		}
		{
			int uas_count = 0;
			for (int nseq = 0; nseq < num_seq; nseq++) if (uas_tiles[nseq] >= 0) uas_count++;
			System.out.println("convertToRgbAnnotateTargets(): "+uas_count+"/"+num_seq+
					" sequences have UAS flight log data (need >=2 consecutive for circle)");
			if (uas_count > 0) {
				for (int nseq = 0; nseq < num_seq; nseq++) if (uas_tiles[nseq] >= 0) {
					double [] t = targets[nseq][uas_tiles[nseq]];
					System.out.println("  seq="+nseq+" tile="+uas_tiles[nseq]+
							" px="+IJ.d2s(t[CuasMotionLMA.RSLT_FL_PX],1)+
							" py="+IJ.d2s(t[CuasMotionLMA.RSLT_FL_PY],1)+
							" range="+IJ.d2s(t[CuasMotionLMA.RSLT_FL_RANGE],1));
				}
			}
		}
		if (amax.get()>=0) {
			for (int ltarg = 1; ltarg <= ltargets_first_last.length; ltarg++) { // 0 - for UAS log
				int indx_first, indx_last;
				for (indx_first = 0; (indx_first < num_seq) && ((local_tiles[indx_first].length <= ltarg ) || (local_tiles[indx_first][ltarg] < 0)); indx_first++);
				if (indx_first < num_seq) {
					for (indx_last = num_seq - 1; (indx_last >= 0) && ((local_tiles[indx_last].length <= ltarg ) || (local_tiles[indx_last][ltarg] < 0)); indx_last--);  // Index 5 out of bounds for length 5
					ltargets_first_last[ltarg-1] = new int[] {indx_first,indx_last};
				}
			}
		}
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			color_processors[nscene].setFont(font_target); // to calculate text boxes, later will need to set it for all processors
		}
		
		ai.set(0);
		int [] scene_indices = getSceneIndices(
				slice_titles,  // String []           key_titles, // corresponding to top targets dimension
				scene_titles); // String []           scene_titles) { // all titles, one per frame
		final Rectangle full_wnd = new Rectangle(0,0,width,height);
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int [] conflict_pix = new int [width * height];
					ColorProcessor conflict_cp = new ColorProcessor(width, height, conflict_pix);
					for (int nSeq = ai.getAndIncrement(); nSeq < (targets.length - 1); nSeq = ai.getAndIncrement()) {
						Arrays.fill(conflict_pix, 0);
						// for now only for targets longer that 1 series
						int nscene0 = scene_indices[nSeq];
						int nscene1 = scene_indices[nSeq+1];
						double kscene = 1.0/(nscene1-nscene0);
						// plot UAS
						double [] uas_target0= null, uas_target1=null; 
						if ((uas_tiles[nSeq] >= 0) && (uas_tiles[nSeq+1] >= 0)) {
							uas_target0= targets[nSeq][uas_tiles[nSeq]];
							uas_target1= targets[nSeq+1][uas_tiles[nSeq+1]];
							for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
								double uk = (nscene - nscene0) * kscene; // 0 when nscene=nscene0 
								double upx =        interpolate(uas_target0[CuasMotionLMA.RSLT_FL_PX],  uas_target1[CuasMotionLMA.RSLT_FL_PX], uk);
								double upy =        interpolate(uas_target0[CuasMotionLMA.RSLT_FL_PY],  uas_target1[CuasMotionLMA.RSLT_FL_PY], uk);
								int xc = (int) Math.round(scale * upx);
								int yc = (int) Math.round(scale * upy);
								imprintPixelIcon(
										uaslog_rgba,     // int [][] icon,   // {R,G,B,A}
										uaslog_pixels,   // int []   icon_pix,
										uaslog_width,    // int      icon_width,
										ipixels[nscene], // int []   image,
										width,           // int      width,
										xc,              // int      xc, 
										yc);             // int      yc) {
							}
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		
		
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int [] conflict_pix = new int [width * height];
///					ColorProcessor conflict_cp = new ColorProcessor(width, height, conflict_pix);
					for (int nSeq = ai.getAndIncrement(); nSeq < (num_seq - 1); nSeq = ai.getAndIncrement()) {
///						int frame_center = frame0 + nSeq * frame_step;
						Arrays.fill(conflict_pix, 0);
						// for now only for targets longer that 1 series
						int nscene0 = scene_indices[nSeq];
						int nscene1 = scene_indices[nSeq+1];
						double kscene = 1.0/(nscene1-nscene0);

						// int get number of visible targets for this scene sequence
						int num_visible = 0;
						int [] target_ids = new int [local_tiles[nSeq].length]; // ltargets_first_last.length];
						for (int i = 1; i < local_tiles[nSeq].length; i++) {
							if ((local_tiles[nSeq][i] > 0) && (i < local_tiles[nSeq + 1].length) && (local_tiles[nSeq + 1][i] > 0)) { // visible in both this and next
								num_visible++;
								int ntile = local_tiles[nSeq][i]; // 1- based 0 - uas log
								double [] target =  targets[nSeq][ntile];
								target_ids[i] = (int) target[CuasMotionLMA.RSLT_TARGET_ID]; // i was out of range
							}
						}
						Integer [] targets_order_integer = new Integer [num_visible];
						int indx = 0;
						for (int i = 1; i < local_tiles[nSeq].length; i++) {
							if ((local_tiles[nSeq][i] > 0) && (i < local_tiles[nSeq + 1].length) && (local_tiles[nSeq + 1][i] > 0)) { // visible in both this and next
								targets_order_integer[indx++] = i;
							}
						}
						Arrays.sort(targets_order_integer, new Comparator<Integer>() { // 
							@Override
							public int compare(Integer lhs, Integer rhs) {
								return Integer.compare(target_ids[lhs], target_ids[rhs]);
							}
						});
						// 0-based indices, 0-based values.
						final int [] targets_order = Arrays.stream(targets_order_integer).mapToInt(Integer::intValue).toArray();
						
						// plot targets
						// now target_coord is ordered from highest priority to the lowest  java.lang.NegativeArraySizeException: -1
						if (local_tiles[nSeq].length == 0) {
							continue;
						}
						double [][][] target_coord = new double [local_tiles[nSeq].length-1][][]; // save centers of targets and text coordinates
						Rectangle [][] annot_boxes = new Rectangle[local_tiles[nSeq].length-1][]; // order will match target_coord order
						String [][]    annots =      new String [local_tiles[nSeq].length-1][];
						double [] uas_target0= null, uas_target1=null; 
						if ((uas_tiles[nSeq] >= 0) && (uas_tiles[nSeq+1] >= 0)) {
							uas_target0= targets[nSeq][uas_tiles[nSeq]];
							uas_target1= targets[nSeq+1][uas_tiles[nSeq+1]];
						}
					
						// prepare and save annotations to later make then non-conficting 
						for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
							int ltarget = targets_order[iltarget]; //  + 1;
							if (    (local_tiles[nSeq].length > ltarget) &&
									(local_tiles[nSeq+1].length > ltarget) &&
									(local_tiles[nSeq][ltarget] >=0) &&
									(local_tiles[nSeq+1][ltarget]>=0)) {
								int ntile0 = local_tiles[nSeq  ][ltarget];
								int ntile1 = local_tiles[nSeq+1][ltarget];
								double [] target0= targets[nSeq  ][ntile0];
								double [] target1= targets[nSeq+1][ntile1];
								double    px0 = (ntile0 % tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_X]; // ignoring velocities
								double    py0 = (ntile0 / tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_Y];
								double    px1 = (ntile1 % tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_X];
								double    py1 = (ntile1 / tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_Y];
								target_coord[iltarget] = new double [nscene1-nscene0+1][4];
								annot_boxes[iltarget] =  new Rectangle[nscene1-nscene0+1];
								annots[iltarget] =       new String[nscene1-nscene0+1];
								int target_id = target_ids[targets_order[iltarget]];
								boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
								int  icon_width = is_uas ? uas_icon_width: target_icon_width;
								for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <= (when drawing, here just not needed)
									double k = (nscene - nscene0) * kscene; // 0 when nscene=nscene0
									double px = scale * interpolate(px0, px1, k);
									double py = scale * interpolate(py0, py1, k);
									String annot_txt = getAnnotationText(
											clt_parameters, // CLTParameters clt_parameters,
											annot_mode,     // int           annot_mode,
											annot_missing,  // boolean       reserve_missing_fields, // make a parameter.Reserve a line for requested but missing parameters
											ntile0,         // int           ntile0,
											ntile1,         // int           ntile1,
											target0,        // double []     target0,
											target1,        // double []     target1,
											uas_target0,    // double []     uas_target0,
											uas_target1,    // double []     uas_target1,
											k,              // double        k,
											camera_atr,     // double []     camera_atr,
											ersCorrection,  // ErsCorrection ersCorrection)
											Double.NaN);    // double        fps); // if NaN, will use default 60Hz. Used only for omegas
									target_coord[iltarget][nscene-nscene0][0] = px; // null pointer
									target_coord[iltarget][nscene-nscene0][1] = py;
									Rectangle text_box = getStringBounds(color_processors[nscene], annot_txt, font_ratio);
									//								Rectangle abs_box = annot_boxes[iltarget][nscene-nscene0];
									double text_left =px + icon_width/2 + space_before_text;
									double text_top = py;
									target_coord[iltarget][nscene-nscene0][2] = text_left;
									target_coord[iltarget][nscene-nscene0][3] = text_top;
									annots[iltarget][nscene-nscene0] = annot_txt;
									annot_boxes[iltarget][nscene-nscene0] = text_box;
								}								
							}
						}
						
						// print annotations

						for (int iltarget = targets_order.length - 1; iltarget >=0; iltarget --) {
							int target_id = target_ids[targets_order[iltarget]];
							boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
							boolean target_text_transparent = is_uas ? transparent_uas : transparent_other;
///							int  icon_width = is_uas ? uas_icon_width: target_icon_width;
							for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
								color_processors[nscene].setColor(is_uas ? selected_color: text_color);
								String annot_txt=annots[iltarget][nscene-nscene0];
								int text_left = (int)Math.round(target_coord[iltarget][nscene-nscene0][2]);
								int text_top =  (int)Math.round(target_coord[iltarget][nscene-nscene0][3]);
								Rectangle abs_box = annot_boxes[iltarget][nscene-nscene0];
								abs_box.x += text_left;
								abs_box.y += text_top;
								if (full_wnd.contains(abs_box) ) {
									Color bg_color = target_text_transparent ? null : text_bg_color; // Color.BLACK;
									drawString(
											color_processors[nscene], // ImageProcessor ip,
											annot_txt,         // String         txt,
											text_left,         // int            left,
											text_top,          // int            top,
											bg_color,          // Color          bgColor, // null - transparent
											font_ratio);       // double         ratio) { // NaN or 0 - standard
								}
							}
						}
						
						// plot icons
						for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
							int target_id = target_ids[targets_order[iltarget]];
							boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
							int [] icon_pixels = is_uas ? uas_icon_pixels : target_icon_pixels;
							int [][] icon_rgba = is_uas ? uas_icon_rgba :   target_icon_rgba;
							int      icon_width = is_uas ? uas_icon_width: target_icon_width;
							for (int nscene = nscene0; nscene < nscene1; nscene++) { 
								int ixc= (int) Math.round(target_coord[iltarget][nscene-nscene0][0]);
								int iyc= (int) Math.round(target_coord[iltarget][nscene-nscene0][1]);
								imprintPixelIcon(
										icon_rgba,       // int [][] icon,
										icon_pixels,     // int []   icon_pix,
										icon_width,      // int      icon_width,
										ipixels[nscene], // int []   image,
										width,           // int      width,
										ixc,             // int      xc, 
										iyc);            // int      yc)
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return imp;
	}
		
	
	
	
	
	
	public static ColorProcessor getIconColorProcessor(
			int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
			boolean scale2x) { 
		String resource_name = TARGET_ICONS[target_type][scale2x? 1 : 0];
		URL resourceUrl = CuasMotion.class.getClassLoader().getResource("graphics/"+resource_name);
		Path undetected_resourcePath = null;
		try {
			undetected_resourcePath = Paths.get(resourceUrl.toURI());
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		String path = undetected_resourcePath.toString();
		ColorProcessor cp = null;
		try {
			cp = new ColorProcessor(ImageIO.read(new File(path)));
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		return cp;
	}
	
	public static ColorProcessor getUasLogIconColorProcessor() {
		String resource_name = ICON_BLUE;
		URL resourceUrl = CuasMotion.class.getClassLoader().getResource("graphics/"+resource_name);
		Path resourcePath = null;
		try {
			resourcePath = Paths.get(resourceUrl.toURI());
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		String uas_log_path = resourcePath.toString();
///		System.out.println("uas_log_path="+uas_log_path);
		ColorProcessor cp = null;
		try {
			cp = new ColorProcessor(ImageIO.read(new File(uas_log_path)));
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		return cp;
	}
	
	
	
	
	public static int [][] splitColorIcon(int [] icon){
		final int [][] icon_rgba = new int [icon.length][4]; 
		for (int i = 0; i < icon_rgba.length; i++) {
			for (int s = 0; s < 4; s++) {
				icon_rgba[i][s] = (icon[i] >> (8 * s)) & 0xff;
			}
		}
		return icon_rgba;
	}
	
	public static void imprintPixelIcon(
			int [][] icon,   // {R,G,B,A}
			int []   icon_pix,
			int      icon_width,
			int []   image,
			int      width,
			int      xc, 
			int      yc) {
		int icon_height = icon.length/icon_width;
		int height = image.length/width;
		int xl = xc - icon_width/2;
		int yt = yc - icon_height/2;
		int xr = xc + icon_width/2;
		int yb = yc + icon_height/2;
		
		if ((xl < 0) || (yt < 0) || (xr >= width) || (yb >= height)) {
			System.out.println("imprintPixelIcon(): outside of the window: xc="+xc+", yc="+yc+", width="+width+", height="+height);
			return;
		}
		
		for (int y = 0; y < icon_height; y++) {
			int py = yt + y;
			if ((py >= 0) && (py < height)) {
				for (int x = 0; x< icon_width; x++) {
					int px = xl +x;
					int dpix = x + y * icon_width;
					int ipix = px + py*width;
					if ((px >=0) && (px < width)) {
						int alpha = icon[dpix][3];
						if (alpha > 0) { // alpha
							int dp = icon_pix[x + y * icon_width];
							if (alpha == 255) {
								image[ipix] = dp;
							} else {
								double k = alpha/255.0;
								int img_pix = image[ipix];
								int new_pix = 0xff000000;
								for (int c = 0; c < 3; c++) {
									int rgb = (img_pix >> (8 * c)) & 0xff;
									rgb = (int) Math.round((1-k)*rgb + k* icon[dpix][c]);
									if (rgb > 255) rgb = 255;
									new_pix |= (rgb << (8*c));
								}
								image[ipix] = new_pix;
							}
						}
					}
				}
			}
		}
		return;
	}
	
	

	public static ImagePlus generateRadarImage(
			CLTParameters       clt_parameters,
			int                 annot_mode, // specify bits
			QuadCLT             scene,
			double [][][]       targets,
			UasLogReader        uasLogReader, // contains camera orientation (getCameraATR())
			String              title,
			String []           key_titles, // corresponding to top targets dimension
			ImagePlus           imp_lwir, // all titles, one per frame
			int                 debugLevel) {
		ImageStack stack_lwir = imp_lwir.getStack();
		String [] scene_titles = new String[stack_lwir.getSize()];
		for (int i = 0; i < stack_lwir.getSize(); i++) {
			scene_titles[i] = stack_lwir.getSliceLabel(i+1);
		}
		return generateRadarImage(
				clt_parameters,
				annot_mode,    // int           annot_mode, // specify bits
				scene,         // QuadCLT            scene,
				targets,
				uasLogReader,  // contains camera orientation (getCameraATR())
				title,
				key_titles,    // corresponding to top targets dimension
				scene_titles,  // all titles, one per frame
				debugLevel);
	}	
	
	public static ImagePlus generateRadarImage(
			CLTParameters       clt_parameters,
			int                 annot_mode, // specify bits
			QuadCLT             scene,
			double [][][]       targets,
			UasLogReader        uasLogReader, // contains camera orientation (getCameraATR())
			String              title,
			String []           key_titles, // corresponding to top targets dimension
			String []           scene_titles, // all titles, one per frame
			int                 debugLevel) {
		final int num_seq = key_titles.length;
		final int num_tiles = targets[0].length;
		final int sensor_width = scene.getGeometryCorrection().getSensorWH()[0];
		final int tileSize = GPUTileProcessor.DTT_SIZE;
		final int tilesX = sensor_width/tileSize;
		final double ifov = scene.getGeometryCorrection().getIFOV();
		final double radar_range =        clt_parameters.imp.cuas_radar_range;
		final String font_name =          clt_parameters.imp.cuas_font_name;
		final int    font_size_radar =    clt_parameters.imp.cuas_font_size; // 7; // 
		final double font_ratio_radar =   clt_parameters.imp.cuas_font_spacing; // 1.2; // if 0 - will use default spacing ( ~=1.5)
		final int    font_type =          clt_parameters.imp.cuas_font_type;
		final Color   text_color =        clt_parameters.imp.cuas_text_color;
		final Color   selected_color =    text_color; // clt_parameters.imp.cuas_selected_color;
		final boolean transparent_other = clt_parameters.imp.cuas_transparent;
		final boolean transparent_uas =   clt_parameters.imp.cuas_transparent_uas;
		final int     target_type =       clt_parameters.imp.cuas_target_type;     //  0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		final int     uas_type =          clt_parameters.imp.cuas_known_type;      //  2; // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		
		final boolean annot_missing =     clt_parameters.imp.cuas_annot_missing; // false; // make a parameter.Reserve a line for requested but missing parameters
		final boolean scale2x = true;
		final int     image_scale = 2; // here always 2
		
		final int    space_before_text = 2 * image_scale;
		final int    width  =       540;       // calculate
		final int    height =      1024;      // calculate
		final int 	 radar_height =       clt_parameters.imp.cuas_radar_height; // 950; // 970; // calculate
		// move to configs:
		final int    bottom_gap =         clt_parameters.imp.cuas_radar_bottom; // 10;
		final int    infinity_gap =       clt_parameters.imp.cuas_radar_top;    // 24; // 10; // add to radar_height
		final double ring_step =          clt_parameters.imp.cuas_grid_ring;    // 100.0;
		final double dir_step =           clt_parameters.imp.cuas_grid_line;    // 5.0; // degrees
		final int    grid_font_size =     clt_parameters.imp.cuas_grid_font;    // 7;
		final int    grid_azimuth_top =   clt_parameters.imp.cuas_grid_az_top;  // 5; // where to put azimuth
		final int    grid_margin =        clt_parameters.imp.cuas_grid_sides;   // 5; // grid annotation from left/right
		
		final Color  rings_color =        clt_parameters.imp.cuas_grid_color;   // new Color(100,100,100);
		final Color  uas_color =          clt_parameters.imp.cuas_radar_uas_color; // new Color(  0,100,140);
		final Color  target_color =       clt_parameters.imp.cuas_radar_color;  // new Color(  0,255,100);
		final double uas_radius =         clt_parameters.imp.cuas_radar_uas_rad;// 4.0;
		final double target_radius =      clt_parameters.imp.cuas_radar_radius; // 2.5;
		final boolean annotate_grid =     clt_parameters.imp.cuas_grid_annot;   // true;
		final Font font_grid = annotate_grid ? (new Font(font_name, font_type, image_scale * grid_font_size)): null;
		final Font font_target = new Font(font_name, font_type, image_scale * font_size_radar);
		
		final int [] camera_xy0 = new int[] {width/2,height-bottom_gap};
		
		int num_scenes = scene_titles.length;
		final ColorProcessor [] color_processors = new ColorProcessor[num_scenes];
		final int [][] pixels = new int[num_scenes][width*height];
		final ImageStack stack = new ImageStack (width, height);
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			color_processors[nscene] = new ColorProcessor(width, height, pixels[nscene]);
			stack.addSlice(scene_titles[nscene], color_processors[nscene]);
		}
//		final double camera_az = uasLogReader.getCameraATR()[0];
		final ErsCorrection ersCorrection = scene.getErsCorrection(); 
		final double [] camera_atr = (uasLogReader != null) ? uasLogReader.getCameraATR() : ersCorrection.getCameraATR();

		// prepare target icons
		ColorProcessor cp_target_icon = getIconColorProcessor(
				target_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		ColorProcessor cp_uas_icon = getIconColorProcessor(
				uas_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		final int target_icon_width = cp_target_icon.getWidth(); 
		final int uas_icon_width =    cp_uas_icon.getWidth();
		final int [] target_icon_pixels = (int[]) cp_target_icon.getPixels();
		final int [] uas_icon_pixels =    (int[]) cp_uas_icon.getPixels();
		final int [][] target_icon_rgba = splitColorIcon(target_icon_pixels);
		final int [][] uas_icon_rgba =    splitColorIcon(uas_icon_pixels);
		
		createGrid(
				color_processors[0], // ColorProcessor     colorProcessor,
				scene,               // QuadCLT            scene,
				camera_xy0,          // int []             camera_xy0,
				radar_height,        // int                height,
				camera_atr[0],       // double             camera_az,
				rings_color,         // Color              color_grid,
				font_grid,           // Font               font, // if null = do not annotate
				radar_range,         // double             max_z,     // corresponds to height
				grid_azimuth_top,    // int                grid_azimuth_top, // =  5; // where to put azimuth
				grid_margin,         // int                grid_margin, // =      5; // grid annotation from left/right
				ring_step,           // double             ring_step, //  = 100.0;
				dir_step);           // double             dir_step)  //  =  5.0; // degrees
		for (int nscene = 1; nscene < num_scenes; nscene++) {
			System.arraycopy(pixels[0], 0, pixels[nscene], 0, pixels[nscene].length);
		}

		ImagePlus imp_new = new ImagePlus(title,stack);
		
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		final AtomicInteger amax =  new AtomicInteger(-1);
		final int [] uas_tiles = new int[num_seq];
		Arrays.fill(uas_tiles, -1);
		final int [][] local_tiles = new int [num_seq][];
		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < targets.length; nSeq = ai.getAndIncrement()) {
						double [][] targets_seq = targets[nSeq];
						/// key - local target_id > 0
						/// Key 0 - special case: tiles where UAS log data is stored (may be the same or different than detected tiles
						/// value - tile index 
						HashMap<Integer,Integer> local_tiles_map = new HashMap<Integer,Integer>(); //  
						int max_ltarg = -1;
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] target = targets_seq[ntile];
							if (target != null) {
								if (!Double.isNaN(target[CuasMotionLMA.RSLT_FL_PX])) {
									local_tiles_map.put(0, ntile); // flight log data
									max_ltarg = Math.max(max_ltarg, 0);
								}
								if (!Double.isNaN(target[CuasMotionLMA.RSLT_GLOBAL])) {
									int ltarg = (int) target[CuasMotionLMA.RSLT_GLOBAL];
									max_ltarg = Math.max(max_ltarg, ltarg);
									local_tiles_map.put(ltarg, ntile); // target data
								}								
							}
						}
						amax.getAndAccumulate(max_ltarg, Math::max);
						local_tiles[nSeq] = new int[max_ltarg+1];
						Arrays.fill(local_tiles[nSeq],-1);
						for (int i = 0; i <= max_ltarg; i++) if (local_tiles_map.containsKey(i)) {
							local_tiles[nSeq][i] = local_tiles_map.get(i);
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		
		final int [][] ltargets_first_last = new int [Math.max(0, amax.get())][]; // correct
		
		for (int nseq = 0; nseq < num_seq; nseq++) if (local_tiles[nseq].length > 0){ // no if is not needed - bug fixed
			uas_tiles[nseq] = local_tiles[nseq][0];
		}
		if (amax.get()>=0) {
			for (int ltarg = 1; ltarg <= ltargets_first_last.length; ltarg++) { // 0 - for UAS log
				int indx_first, indx_last;
				for (indx_first = 0; (indx_first < num_seq) && ((local_tiles[indx_first].length <= ltarg ) || (local_tiles[indx_first][ltarg] < 0)); indx_first++);
				if (indx_first < num_seq) {
					for (indx_last = num_seq - 1; (indx_last >= 0) && ((local_tiles[indx_last].length <= ltarg ) || (local_tiles[indx_last][ltarg] < 0)); indx_last--);  // Index 5 out of bounds for length 5
					ltargets_first_last[ltarg-1] = new int[] {indx_first,indx_last};
				}
			}
		}
		for (int nscene = 0; nscene < num_scenes; nscene++) {
			color_processors[nscene].setFont(font_target); // to calculate text boxes, later will need to set it for all processors
		}
		
		// mark UAS log and targets on the radar
		// color_processors[0].setFont(font_target); // to calculate text boxes, later will need to set it for all processors
		
		ai.set(0);
		int [] scene_indices = getSceneIndices(
				key_titles,    // String []           key_titles, // corresponding to top targets dimension
				scene_titles); // String []           scene_titles) { // all titles, one per frame
		final double kpix = radar_height/radar_range;
		final Rectangle full_wnd = new Rectangle(0,0,width,height);
		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int [] conflict_pix = new int [width * height];
					ColorProcessor conflict_cp = new ColorProcessor(width, height, conflict_pix);
					for (int nSeq = ai.getAndIncrement(); nSeq < (targets.length - 1); nSeq = ai.getAndIncrement()) {
						//						color_processors[nSeq].setFont(font_target); // to calculate text boxes, later will need to set it for all processors
						Arrays.fill(conflict_pix, 0);
						// for now only for targets longer that 1 series
						int nscene0 = scene_indices[nSeq];
						int nscene1 = scene_indices[nSeq+1];
						double kscene = 1.0/(nscene1-nscene0);
						// plot UAS
						double [] uas_target0= null, uas_target1=null; 
						if ((uas_tiles[nSeq] >= 0) && (uas_tiles[nSeq+1] >= 0)) {
							uas_target0= targets[nSeq][uas_tiles[nSeq]];
							uas_target1= targets[nSeq+1][uas_tiles[nSeq+1]];
							for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
								double k = (nscene - nscene0) * kscene; // 0 when nscene=nscene0 
								double px =        interpolate(uas_target0[CuasMotionLMA.RSLT_FL_PX],  uas_target1[CuasMotionLMA.RSLT_FL_PX],k);
								double py =        interpolate(uas_target0[CuasMotionLMA.RSLT_FL_PY],  uas_target1[CuasMotionLMA.RSLT_FL_PY],k);
								double disparity = interpolate(uas_target0[CuasMotionLMA.RSLT_FL_DISP],uas_target1[CuasMotionLMA.RSLT_FL_DISP],k);
								// needed relative to the camera
								// {right, up, to_camera
								double [] xyz = ersCorrection.getWorldCoordinatesERS( // ersCorrection - reference
										px,                 // double px,                // pixel coordinate X in the reference view
										py,                 // double py,                // pixel coordinate Y in the reference view
										disparity,          // double disparity,         // reference disparity 
										true,               // boolean distortedView,    // This camera view is distorted (diff.rect), false - rectilinear
										OpticalFlow.ZERO3,  // double [] reference_xyz,  // this view position in world coordinates (typically ZERO3)
										OpticalFlow.ZERO3); // double [] reference_atr,  // this view orientation relative to world frame  (typically ZERO3)
								double x = camera_xy0[0] + xyz[0] * kpix;
								double y = camera_xy0[1] + xyz[2] * kpix; // positive Z - down in the image (verify)
								drawCircle(
										color_processors[nscene], // ColorProcessor colorProcessor,
										uas_color,                // Color color, // or null
										x,                        // double xc,
										y,                        // double yc,
										uas_radius); // double radius)
								drawCircle(
										conflict_cp, // ColorProcessor colorProcessor,
										uas_color,                // Color color, // or null
										x,                        // double xc,
										y,                        // double yc,
										uas_radius); // double radius)
							}
						}

						// int get number of visible targets for this scene sequence
						int num_visible = 0;
						int [] target_ids = new int [local_tiles[nSeq].length]; // ltargets_first_last.length];
						for (int i = 1; i < local_tiles[nSeq].length; i++) {
							if ((local_tiles[nSeq][i] > 0) && (i < local_tiles[nSeq + 1].length) && (local_tiles[nSeq + 1][i] > 0)) { // visible in both this and next
								num_visible++;
								int ntile = local_tiles[nSeq][i]; // 1- based 0 - uas log
								double [] target =  targets[nSeq][ntile];
								target_ids[i] = (int) target[CuasMotionLMA.RSLT_TARGET_ID]; // i was out of range
							}
						}
						Integer [] targets_order_integer = new Integer [num_visible];
						int indx = 0;
						for (int i = 1; i < local_tiles[nSeq].length; i++) {
							if ((local_tiles[nSeq][i] > 0) && (i < local_tiles[nSeq + 1].length) && (local_tiles[nSeq + 1][i] > 0)) { // visible in both this and next
								targets_order_integer[indx++] = i;
							}
						}
						Arrays.sort(targets_order_integer, new Comparator<Integer>() { // 
							@Override
							public int compare(Integer lhs, Integer rhs) {
								return Integer.compare(target_ids[lhs], target_ids[rhs]);
							}
						});
						// 0-based indices, 0-based values.
						final int [] targets_order = Arrays.stream(targets_order_integer).mapToInt(Integer::intValue).toArray();
						if (local_tiles[nSeq].length > 0) {
							// plot targets
							//						int icon_width = (int) Math.round(2 * target_radius); // use icon width when used
							// now target_coord is ordered from highest priority to the lowest
							double [][][] target_coord = new double [local_tiles[nSeq].length-1][][]; // save centers of targets and text coordinates
							Rectangle [][] annot_boxes = new Rectangle[local_tiles[nSeq].length-1][]; // order will match target_coord order
							String [][]    annots =      new String [local_tiles[nSeq].length-1][];
							// add integer color and bg_color (-1 - transparent)
							// iltarget: 0 - highest priority 
							for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
								int ltarget = targets_order[iltarget]; //  + 1;
								if (    (local_tiles[nSeq].length > ltarget) &&
										(local_tiles[nSeq+1].length > ltarget) &&
										(local_tiles[nSeq][ltarget] >=0) &&
										(local_tiles[nSeq+1][ltarget]>=0)) {
									int ntile0 = local_tiles[nSeq  ][ltarget];
									int ntile1 = local_tiles[nSeq+1][ltarget];
									double [] target0= targets[nSeq  ][ntile0];
									double [] target1= targets[nSeq+1][ntile1];
									double    px0 = (ntile0 % tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_X]; // ignoring velocities
									double    py0 = (ntile0 / tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_Y];
									double    range0= target0[CuasMotionLMA.RSLT_RANGE_LIN];
									double    px1 = (ntile1 % tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_X];
									double    py1 = (ntile1 / tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_Y];
									double    range1= target0[CuasMotionLMA.RSLT_RANGE_LIN];
									double    disparity0 = Double.NaN;
									double    disparity1 = Double.NaN;
									double    radar_x0, radar_y0, radar_x1, radar_y1;
									boolean   infinity_mode = false;
									if ((range0 <= radar_range) && (range1 <= radar_range)) {
										disparity0 = ersCorrection.getDisparityFromZ(range0);
										disparity1 = ersCorrection.getDisparityFromZ(range1);
										double [] xyz0 = ersCorrection.getWorldCoordinatesERS( // ersCorrection - reference
												px0,                 // double px,                // pixel coordinate X in the reference view
												py0,                 // double py,                // pixel coordinate Y in the reference view
												disparity0,          // double disparity,         // reference disparity 
												true,                // boolean distortedView,    // This camera view is distorted (diff.rect), false - rectilinear
												OpticalFlow.ZERO3,   // double [] reference_xyz,  // this view position in world coordinates (typically ZERO3)
												OpticalFlow.ZERO3);  // double [] reference_atr,  // this view orientation relative to world frame  (typically ZERO3)
										double [] xyz1 = ersCorrection.getWorldCoordinatesERS( // ersCorrection - reference
												px1,                 // double px,                // pixel coordinate X in the reference view
												py1,                 // double py,                // pixel coordinate Y in the reference view
												disparity1,          // double disparity,         // reference disparity 
												true,                // boolean distortedView,    // This camera view is distorted (diff.rect), false - rectilinear
												OpticalFlow.ZERO3,   // double [] reference_xyz,  // this view position in world coordinates (typically ZERO3)
												OpticalFlow.ZERO3);  // double [] reference_atr,  // this view orientation relative to world frame  (typically ZERO3)
										radar_x0 = camera_xy0[0] + xyz0[0] * kpix;
										radar_y0 = camera_xy0[1] + xyz0[2] * kpix; // positive Z - down in the image (verify)
										radar_x1 = camera_xy0[0] + xyz1[0] * kpix;
										radar_y1 = camera_xy0[1] + xyz1[2] * kpix; // positive Z - down in the image (verify)
									} else {
										radar_x0 = camera_xy0[0] + (px0 - sensor_width/2) * ifov * (radar_height + infinity_gap);
										radar_y0 = camera_xy0[1] - (radar_height + infinity_gap);
										radar_x1 = camera_xy0[0] + (px1 - sensor_width/2) * ifov * (radar_height + infinity_gap);
										radar_y1 = camera_xy0[1] - (radar_height + infinity_gap);
										infinity_mode = true;
									}
									//								target_coord[ltarget-1] = new double [nscene1-nscene0+1][2];
									target_coord[iltarget] = new double [nscene1-nscene0+1][4];
									annot_boxes[iltarget] =  new Rectangle[nscene1-nscene0+1];
									annots[iltarget] =       new String[nscene1-nscene0+1];
									int target_id = target_ids[targets_order[iltarget]];
									boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
									int  icon_width = is_uas ? uas_icon_width: target_icon_width;
									for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <= (when drawing, here just not needed)
										double k = (nscene - nscene0) * kscene; // 0 when nscene=nscene0
										double radar_x = interpolate(radar_x0, radar_x1, k);
										double radar_y = interpolate(radar_y0, radar_y1, k);
										String annot_txt = getAnnotationText(
												clt_parameters, // CLTParameters clt_parameters,
												annot_mode,     // int           annot_mode,
												annot_missing, // boolean       reserve_missing_fields, // make a parameter.Reserve a line for requested but missing parameters
												ntile0,         // int           ntile0,
												ntile1,         // int           ntile1,
												target0,        // double []     target0,
												target1,        // double []     target1,
												uas_target0,    // double []     uas_target0,
												uas_target1,    // double []     uas_target1,
												k,              // double        k,
												camera_atr,     // double []     camera_atr,
												ersCorrection,  // ErsCorrection ersCorrection)
												Double.NaN);    // double        fps); // if NaN, will use default 60Hz. Used only for omegas
										target_coord[iltarget][nscene-nscene0][0] = radar_x; // null pointer
										target_coord[iltarget][nscene-nscene0][1] = radar_y;
										Rectangle text_box = getStringBounds(color_processors[nscene], annot_txt, font_ratio_radar);

										//									Rectangle abs_box = annot_boxes[iltarget][nscene-nscene0];
										double text_left =radar_x + icon_width/2 + space_before_text;
										double text_top = radar_y;
										if (infinity_mode) {
											text_left =radar_x - icon_width/2;
											text_top = radar_y + icon_width/2 - text_box.y + space_before_text;
										}
										target_coord[iltarget][nscene-nscene0][2] = text_left;
										target_coord[iltarget][nscene-nscene0][3] = text_top;

										annots[iltarget][nscene-nscene0] = annot_txt;
										//									Rectangle text_box = getStringBounds(color_processors[nscene], annot_txt, font_ratio_radar);
										annot_boxes[iltarget][nscene-nscene0] = text_box;
									}								
								}
							}
							// plot all targets for the series on the same conflict_cp (for later use
							for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
								for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
									drawCircle(
											conflict_cp, // ColorProcessor colorProcessor,
											target_color,                              // Color color, // or null
											target_coord[iltarget][nscene-nscene0][0], // double xc,
											target_coord[iltarget][nscene-nscene0][1], // double yc,
											target_radius);                            // double radius)
								}
							}
							// now plot all text in reverse order

							// now plot all target circles in reverse order
							// TODO: adjust boxes to avoid overlaps - use conflict_cp to avoid targets
							for (int iltarget = targets_order.length - 1; iltarget >=0; iltarget --) {
								int target_id = target_ids[targets_order[iltarget]];
								boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
								boolean target_text_transparent = is_uas ? transparent_uas : transparent_other;
								int  icon_width = is_uas ? uas_icon_width: target_icon_width;
								for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
									color_processors[nscene].setColor(is_uas ? selected_color: text_color);
									String annot_txt=annots[iltarget][nscene-nscene0];
									/*
								int text_left = (int)Math.round(target_coord[iltarget][nscene-nscene0][0] + icon_width/2 + space_before_text);
								int text_top =  (int)Math.round(target_coord[iltarget][nscene-nscene0][1]);
									 */
									int text_left = (int)Math.round(target_coord[iltarget][nscene-nscene0][2]);
									int text_top =  (int)Math.round(target_coord[iltarget][nscene-nscene0][3]);
									Rectangle abs_box = annot_boxes[iltarget][nscene-nscene0];
									abs_box.x += text_left;
									abs_box.y += text_top;
									if (full_wnd.contains(abs_box) ) {
										Color bg_color = target_text_transparent ? null : Color.BLACK;
										drawString(
												color_processors[nscene], // ImageProcessor ip,
												annot_txt,         // String         txt,
												text_left,         // int            left,
												text_top,          // int            top,
												bg_color,          // Color          bgColor, // null - transparent
												font_ratio_radar); // double         ratio) { // NaN or 0 - standard
									}
								}
							}
							// plot icons
							for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
								int target_id = target_ids[targets_order[iltarget]];
								boolean is_uas = target_id == CuasMultiSeries.TARGET_INDEX_UAS;
								int [] icon_pixels = is_uas ? uas_icon_pixels : target_icon_pixels;
								int [][] icon_rgba = is_uas ? uas_icon_rgba :   target_icon_rgba;
								int      icon_width = is_uas ? uas_icon_width: target_icon_width;
								for (int nscene = nscene0; nscene < nscene1; nscene++) { 
									int ixc= (int) Math.round(target_coord[iltarget][nscene-nscene0][0]);
									int iyc= (int) Math.round(target_coord[iltarget][nscene-nscene0][1]);
									//								int [] image_pixels =  (int []) color_processors[nscene].getPixels();
									imprintPixelIcon(
											icon_rgba,      // int [][] icon,
											icon_pixels,    // int []   icon_pix,
											icon_width,     // int      icon_width,
											pixels[nscene], // int []   image,
											width,          // int      width,
											ixc,            // int      xc, 
											iyc);           // int      yc)
								}
							}
							// now plot targets (any order, just after everything else). Maybe use a special color for the UAS
							for (int iltarget = 0; iltarget < targets_order.length; iltarget ++) {
								for (int nscene = nscene0; nscene < nscene1; nscene++) { // threads collide if <=
									drawCircle(
											color_processors[nscene], // ColorProcessor colorProcessor,
											target_color,                              // Color color, // or null
											target_coord[iltarget][nscene-nscene0][0], // double xc,
											target_coord[iltarget][nscene-nscene0][1], // double yc,
											target_radius);                            // double radius)
								}
							}
						} else {
							System.out.println("local_tiles["+nSeq+"].length == 0");
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return imp_new;
	}
	
	/**
	 * ImageProcessor getStringBounds() does not work with the multiline text
	 * @param ip
	 * @param annot_txt
	 * @return
	 */
	public static Rectangle getStringBounds(
			ImageProcessor ip,
			String         txt) {
		return getStringBounds(
				ip,  // ImageProcessor ip,
				txt, // String txt,
				0);  // double ratio)
	}
	public static Rectangle getStringBounds(
			ImageProcessor ip,
			String         txt,
			double         ratio) {
		int font_size =   ip.getFont().getSize();
		int line_space =  (ratio > 0) ? ((int) Math.round(font_size * ratio)) : ip.getFontMetrics().getHeight();
		String lines[] =  txt.split("\\r?\\n");
		Rectangle bb = ip.getStringBounds(lines[0]);
		for (int i = 1; i < lines.length;i++) {
			Rectangle bb_line = ip.getStringBounds(lines[i]);
			bb_line.y += i * line_space;
			bb.add(bb_line);
		}
		return bb;
	}
	
	public static void drawString(
			ImageProcessor ip,
			String         txt,
			int            left,
			int            top,
			Color          bgColor, // null - transparent
			double         ratio) { // NaN or 0 - standard
		int font_size = ip.getFont().getSize();
		int line_space = (ratio > 0) ? ((int) Math.round(font_size * ratio)) : ip.getFontMetrics().getHeight();
		String lines[] = txt.split("\\r?\\n");
		for (int i = 0; i < lines.length;i++) {
			int line_top = top + i * line_space;
			if (bgColor == null) {
				ip.drawString(lines[i], left, line_top); // transparent
			} else {
				ip.drawString(lines[i], left, line_top, bgColor); // transparent
			}
		}		
		
	}
	
	
	public static String getAnnotationText(
			CLTParameters clt_parameters,
			int           annot_mode, // specify bits
			boolean       reserve_missing_fields, // make a parameter.Reserve a line for requested but missing parameters
			int           ntile0,
			int           ntile1,
			double []     target0,
			double []     target1,
			double []     uas_target0,
			double []     uas_target1,
			double        k,
			double []     camera_atr,
			ErsCorrection ersCorrection,
			double        fps) { // if NaN, will use default 60Hz. Used only for omegas
		if (Double.isNaN(fps)) {
			fps = 60.0;
		}
		if (target1 == null ) { // for compatibility with the camera view, where data is already interpolated
			target1 = target0;
			ntile1 = ntile0;
			uas_target1 = uas_target0;
			k = 0;
		}
		String number_unsigned_format = "%4.0f";
		String number_frac_format = "%4.1f";
		String number_signed_format = "%3.0f";
		String omega_format =  "%3.1f";
		
		String sinfinity = "\u221E";
		boolean  show_inf =      clt_parameters.imp.cuas_show_inf;        // true;  // Show distance greater than max (or negativce) as infinity
		boolean  show_inf_gt =   clt_parameters.imp.cuas_show_inf_gt;     // false; // Use ">max" instead of infinity symbol
		double   max_annot_range = clt_parameters.imp.cuas_rng_limit; // 5000, maybe make a separate parameter
		double   max_axial_range = clt_parameters.imp.cuas_radar_range; // may be a separate - maximal range for axial velocity/heading
		double velocity_scale = getFrameVelocityScale(clt_parameters);
		double ifov = ersCorrection.getIFOV();
		int sensor_width =  ersCorrection.getSensorWH()[0];
		int sensor_height = ersCorrection.getSensorWH()[1];
		int tileSize = GPUTileProcessor.DTT_SIZE;
		int tilesX = sensor_width/tileSize;
		int dbg_id = -21;
		double did =  target0[CuasMotionLMA.RSLT_TARGET_ID];
		int    id = (int) did;
		if (id == dbg_id) {
			System.out.println("getAnnotationText(): id = 21");
		}
		double px0 = (ntile0 % tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_X]; // ignoring velocities
		double py0 = (ntile0 / tilesX +0.5) * tileSize + target0[CuasMotionLMA.RSLT_Y];
		double range0= target0[CuasMotionLMA.RSLT_RANGE_LIN];
		double px1 = (ntile1 % tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_X];
		double py1 = (ntile1 / tilesX +0.5) * tileSize + target1[CuasMotionLMA.RSLT_Y];
		double range1= target0[CuasMotionLMA.RSLT_RANGE_LIN];
		double px = interpolate (px0, px1, k);
		double py = interpolate (py0, py1, k);
		
		double range = interpolate (range0, range1, k);
		// range may be Double.NaN if missing
		if (range > max_annot_range) {
			range = Double.POSITIVE_INFINITY;
		}
		boolean is_uas = (uas_target0 != null) && (uas_target1 != null) && (id == CuasMultiSeries.TARGET_INDEX_UAS);
		
		double true_range = is_uas ? interpolate (uas_target0[CuasMotionLMA.RSLT_FL_RANGE],uas_target1[CuasMotionLMA.RSLT_FL_RANGE],k): Double.NaN;
		double disparity = 0;
		if (!Double.isInfinite(range)) {
			disparity = ersCorrection.getDisparityFromZ(range);
		}
		double [] wxyz = null;
		double agl = Double.NaN;
		double az = Double.NaN;
		double el = Double.NaN;
		
		 double [][] icamera_atr = ErsCorrection.invertXYZATR(
				 OpticalFlow.ZERO3, // double [] source_xyz,
				 camera_atr); // double [] source_atr)
		if (disparity > 0) {
			wxyz = ersCorrection.getWorldCoordinatesERS( // ersCorrection - reference
					px,                  // double px,                // pixel coordinate X in the reference view
					py,                  // double py,                // pixel coordinate Y in the reference view
					disparity,           // double disparity,         // reference disparity 
					true,                // boolean distortedView,    // This camera view is distorted (diff.rect), false - rectilinear
					OpticalFlow.ZERO3,   // double [] reference_xyz,  // this view position in world coordinates (typically ZERO3)
					icamera_atr[1]);     // double [] reference_atr,  // this view orientation relative to world frame  (typically ZERO3)
			agl = wxyz[1];
			az = Math.atan2(wxyz[0], -wxyz[2]); // TODO: check sign
			el = Math.atan(wxyz[1]/wxyz[2]); // TODO: check sign
		} else {
			az =   (px - sensor_width/2) * ifov +  camera_atr[0];
			el = - (py - sensor_height/2) * ifov + camera_atr[1];
		}
		if (Double.isFinite(az)) { while (az <  0 )         az += 2*Math.PI; while (az >  2*Math.PI ) az -= 2*Math.PI; }
		if (Double.isFinite(el)) { while (el >  Math.PI)   el -= 2*Math.PI; while (el < -Math.PI)   el += 2*Math.PI; }
		double az_deg = az * 180/Math.PI;
		double el_deg = el * 180/Math.PI;
		double vx = interpolate (target0[CuasMotionLMA.RSLT_VX],target1[CuasMotionLMA.RSLT_VX],k) * velocity_scale;
		double vy = interpolate (target0[CuasMotionLMA.RSLT_VY],target1[CuasMotionLMA.RSLT_VY],k) * velocity_scale;
		double omega_az =  vx * ifov * fps;
		double omega_el = -vy * ifov * fps;
		double omega_az_degs = omega_az * 180/Math.PI;
		double omega_el_degs = omega_el * 180/Math.PI;
		
		
		
		double vel_away =  interpolate (target0[CuasMotionLMA.RSLT_VEL_AWAY], target1[CuasMotionLMA.RSLT_VEL_AWAY], k);
		if (range > max_axial_range) {
			vel_away = 0;
		}
		double vel_right = interpolate (target0[CuasMotionLMA.RSLT_VEL_RIGHT],target1[CuasMotionLMA.RSLT_VEL_RIGHT],k);
		double vel_up =    interpolate (target0[CuasMotionLMA.RSLT_VEL_UP],   target1[CuasMotionLMA.RSLT_VEL_UP],   k);
		double vel_hor = Math.sqrt(vel_away*vel_away + vel_right*vel_right);
		if (range > max_axial_range) {
			vel_hor *= Math.sqrt(2); // increase horizontal speed when one component is unknown by sqrt(2)
		}
		double hdg = Math.atan2(vel_right, vel_away)  + camera_atr[0];
		if (Double.isFinite(hdg)) { while (hdg < 0) hdg += 2*Math.PI; }
		double hdg_deg = hdg * 180/ Math.PI;
		
		StringBuffer sb = new StringBuffer();
		if ((annot_mode & (1 << ANNOT_ID)) != 0){
//			sb.append("ID  "+String.format(number_unsigned_format,did)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_ID][0]+String.format(number_unsigned_format,did)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_DISP)) != 0){
			if (!Double.isNaN(disparity)) {
//				sb.append("DISP"+String.format(number_unsigned_format,disparity*1000)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_DISP][0]+String.format(number_unsigned_format,disparity*1000)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}

		}
		if ((annot_mode & (1 << ANNOT_RANGE)) != 0){
			String srng = ANNOT_LABELS[ANNOT_RANGE][0];
			if (range <= max_annot_range) { // handles POSITIVE_INFINITY
				sb.append(srng+String.format(number_unsigned_format,range)+"\n");
			} else if (show_inf && !Double.isNaN(range)){
				if (show_inf_gt) {
					srng = srng.substring(0,3)+">";
//					sb.append("RNG>"+String.format(number_unsigned_format,max_annot_range)+"\n");
					sb.append(srng+String.format(number_unsigned_format,max_annot_range)+"\n");
				} else {
//					sb.append("RNG "+sinfinity+"\n");
					sb.append(srng+sinfinity+"\n");
				}
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}
		if ((annot_mode & (1 << ANNOT_TRANG)) != 0){
			if (!Double.isNaN(true_range)) {
//				sb.append("TRNG"+String.format(number_unsigned_format,true_range)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_TRANG][0]+String.format(number_unsigned_format,true_range)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}
		boolean has_agl = false;
		if ((annot_mode & (1 << ANNOT_AGL)) != 0){
			if (!Double.isNaN(agl) && (range <= max_annot_range)) {
//				sb.append("AGL "+String.format(number_unsigned_format,agl)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_AGL][0]+String.format(number_unsigned_format,agl)+"\n");
				has_agl = true;
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}	
		if ((annot_mode & (1 << ANNOT_AZ)) != 0){
			if (!Double.isNaN(az_deg)) {
//				sb.append("AZM "+String.format(number_unsigned_format,az_deg)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_AZ][0]+String.format(number_unsigned_format,az_deg)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}
		if (((annot_mode & (1 << ANNOT_EL)) != 0) || (!has_agl && ((annot_mode & (1 << ANNOT_ELNOAGL)) != 0))   ){
			if (!Double.isNaN(el_deg)) {
//				sb.append("EL  "+String.format(number_unsigned_format,el_deg)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_EL][0]+String.format(number_unsigned_format,el_deg)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}
		if ((annot_mode & (1 << ANNOT_OMEGA_AZ)) != 0){
//			sb.append(OMEGA+"AZ "+getSignedDouble(omega_az_degs,omega_format)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_OMEGA_AZ][0]+getSignedDouble(omega_az_degs,omega_format)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_OMEGA_EL)) != 0){
//			sb.append(OMEGA+"EL "+getSignedDouble(omega_el_degs,omega_format)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_OMEGA_EL][0]+getSignedDouble(omega_el_degs,omega_format)+"\n");
		}		
		
		if ((annot_mode & (1 << ANNOT_VS)) != 0){
			if (!Double.isNaN(vel_up) && (range <= max_annot_range)) {
//				sb.append("VS  "+getSignedDouble(vel_up,omega_format)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_VS][0]+getSignedDouble(vel_up,omega_format)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}		
		if ((annot_mode & (1 << ANNOT_GS)) != 0){
			if (!Double.isNaN(vel_hor) && (range <= max_annot_range)) {
//				sb.append("GS  "+String.format(number_frac_format,vel_hor)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_GS][0]+String.format(number_frac_format,vel_hor)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}		
		if ((annot_mode & (1 << ANNOT_HDG)) != 0){
			if (!Double.isNaN(hdg_deg) && (range <= max_annot_range)) {
//				sb.append("HDG "+String.format(number_unsigned_format,hdg_deg)+"\n");
				sb.append(ANNOT_LABELS[ANNOT_HDG][0]+String.format(number_unsigned_format,hdg_deg)+"\n");
			} else if (reserve_missing_fields){
				sb.append("\n");
			}
		}
		
		if ((annot_mode & (1 << ANNOT_ERRB)) != 0){
			double mismatch_before = target0[CuasMotionLMA.RSLT_MISMATCH_BEFORE];
//			sb.append("ERRb"+String.format(number_frac_format,mismatch_before)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_ERRB][0]+String.format(number_frac_format,mismatch_before)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_ERRA)) != 0){
			double mismatch_after =  target0[CuasMotionLMA.RSLT_MISMATCH_AFTER];
//			sb.append("ERRa"+String.format(number_frac_format,mismatch_after)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_ERRA][0]+String.format(number_frac_format,mismatch_after)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_SEQ)) != 0){
			double sequence_length = target0[CuasMotionLMA.RSLT_MATCH_LENGTH];
//			sb.append("SEQ "+String.format(number_unsigned_format,sequence_length)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_SEQ][0]+String.format(number_unsigned_format,sequence_length)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_TRV)) != 0){
			double sequence_travel = target0[CuasMotionLMA.RSLT_SEQ_TRAVEL];
//			sb.append("TRV "+String.format(number_frac_format,sequence_travel)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_TRV][0]+String.format(number_frac_format,sequence_travel)+"\n");
		}
		if ((annot_mode & (1 << ANNOT_SCORE)) != 0){
			double score = target0[CuasMotionLMA.RSLT_QSCORE];
//			sb.append("S   "+String.format(number_frac_format,score)+"\n");
			sb.append(ANNOT_LABELS[ANNOT_SCORE][0]+String.format(number_frac_format,score)+"\n");
		}
		//		if (!Double.isInfinite(range)) {
		return sb.toString();
	}
	
	/**
	 * Approximately calculate azimuth, elevation and angular velocities for far objects
	 * @param ersCorrection
	 * @param uasLogReader
	 * @param fps
	 * @param px
	 * @param py
	 * @param vx target velocity in pixels per frame
	 * @param vy target velocity in pixels per frame
	 * @return
	 */
	public static double [][] getPixToAzElev(
			ErsCorrection ersCorrection,
			UasLogReader  uasLogReader,			
			double        fps, // if NaN, will use default 60Hz. Used only for omegas
			double        px,
			double        py,
			double        vx,
			double        vy) {
		
		if (Double.isNaN(fps)) {
			fps = 60.0;
		}
		double [] camera_atr = (uasLogReader != null) ? uasLogReader.getCameraATR() : ersCorrection.getCameraATR();
		double ifov = ersCorrection.getIFOV();
		int sensor_width =  ersCorrection.getSensorWH()[0];
		int sensor_height = ersCorrection.getSensorWH()[1];
		double az = Double.NaN;
		double el = Double.NaN;
		az =   (px - sensor_width/2) * ifov +  camera_atr[0];
		el = - (py - sensor_height/2) * ifov + camera_atr[1];
		if (Double.isFinite(az)) { while (az <  0 )       az += 2*Math.PI; while (az >  2*Math.PI ) az -= 2*Math.PI; }
		if (Double.isFinite(el)) { while (el >  Math.PI) el -= 2*Math.PI; while (el < -Math.PI)    el += 2*Math.PI; }
		double az_deg = az * 180/Math.PI;
		double el_deg = el * 180/Math.PI;
		double omega_az =  vx * ifov * fps;
		double omega_el = -vy * ifov * fps;
		double omega_az_degs = omega_az * 180/Math.PI;
		double omega_el_degs = omega_el * 180/Math.PI;
		return new double [][] {{az_deg,el_deg},{omega_az_degs,omega_el_degs}};
	}	
	
	
	
	
	public static double interpolate (
			double v0,
			double v1,
			double k) {
		if (k==0) {
			return v0;
		} else {
			return k*v1 + (1-k) * v0;
		}
	}
	public static void drawCircle(
			ColorProcessor colorProcessor,
			Color color, // or null
			double xc,
			double yc,
			double radius) {
		int width =  colorProcessor.getWidth();
		int height = colorProcessor.getHeight();
		double radius2 = radius*radius;
		int icolor = color.getRGB() | 0xff000000; // needed?
		if (color != null) colorProcessor.setColor(color);
		int x0 = (int) Math.floor(xc - radius);
		int y0 = (int) Math.floor(yc - radius);
		int x1 = (int) Math.ceil (xc + radius);
		int y1 = (int) Math.ceil (yc + radius);
		if ((x0 < 0) || (y0 < 0) || (x1 >= width) || (y1 >= height)) {
			System.out.println("drawCircle(): outside of the window: xc="+xc+", yc="+yc+", width="+width+", height="+height+
					", color="+ color.toString());
			return;
		}
		int [] pixels = (int[]) colorProcessor.getPixels();
		for (int y = y0; y <= y1; y++) {
			double dy = y-yc;
			double dy2 = dy*dy;
			for (int x = x0; x < x1; x++) {
				double dx = x-xc;
				double r2 = dx*dx + dy2;
				if (r2 <= radius2) {
					pixels[x+y*width] = icolor; // java.lang.ArrayIndexOutOfBoundsException: Index -144985 out of bounds for length 552960 	at com.elphel.imagej.cuas.CuasMotion.drawCircle(CuasMotion.java:5197) at com.elphel.imagej.cuas.CuasMotion$33.run(CuasMotion.java:4662)

				}
			}
		}
	}
	
	
	private static int [] getSceneIndices(
			String []           key_titles, // corresponding to top targets dimension
			String []           scene_titles) { // all titles, one per frame
		int [] indices = new int [key_titles.length];
		int indx = 0;
		for (int i =0; i < key_titles.length; i++) {
			for (; !key_titles[i].equals(scene_titles[indx]); indx++);
			indices[i] = indx++;
		}
		return indices;
	}
	
	
	public static void createGrid(
			ColorProcessor     colorProcessor,
			QuadCLT            scene,
			int []             camera_xy0,
			int                height,
			double             camera_az,
			Color              color_grid,
			Font               font, // if null = do not annotate
			double             max_z,     // corresponds to height
			int                grid_azimuth_top, // =  5; // where to put azimuth
			int                grid_margin, // =      5; // grid annotation from left/right
			double             ring_step, //  = 100.0;
			double             dir_step)  //  =  5.0; // degrees
	{
		int width = colorProcessor.getWidth();
		double ifov = scene.getGeometryCorrection().getIFOV();
		int sensor_width = scene.getGeometryCorrection().getSensorWH()[0];
		double hfov = 2*Math.atan(ifov * sensor_width/2);
		double az0 = (camera_az - hfov/2);
		double az1 = (camera_az + hfov/2);
		double az0_deg = az0 * 180/Math.PI;
		double az1_deg = az1 * 180/Math.PI;
		int az_stp0_deg = (int) Math.ceil(az0_deg/dir_step);
		int az_stp1_deg = (int) Math.floor(az1_deg/dir_step);
		colorProcessor.setColor(color_grid);
		
		if (font != null) {
			colorProcessor.setFont(font);
		}
		
		//TODO: add labels to the grid
		int x0 = camera_xy0[0];
		int y0 = camera_xy0[1];
		for (int az_stp_deg = az_stp0_deg; az_stp_deg <= az_stp1_deg; az_stp_deg++) {
			double az_deg = az_stp_deg *  dir_step; // degrees
			double rel_az = az_deg * Math.PI/180-camera_az; // angle in radians from vertical
			int y1 = y0 - height;
			int x1 = (int) Math.round (x0 + height * Math.tan(rel_az));
			colorProcessor.drawLine(x0, y0, x1, y1);
			if (font!= null) {
				String txt = String.format("%.0f",az_deg);
				Rectangle txt_box = colorProcessor.getStringBounds(txt);
				int xt = x1 - txt_box.width/2;
				if (xt < grid_margin) {
					xt = grid_margin;
				}
				if ((xt + txt_box.width) > (width -grid_margin)) {
					xt = width -grid_margin - txt_box.width;
				}
				int yt = grid_azimuth_top - txt_box.y; // txt_box.y is negative 
				colorProcessor.drawString(txt, xt, yt, Color.BLACK); // black background
			}
		}
		
		double max_dist = max_z/Math.cos(hfov/2);
		for (double rad = ring_step; rad < max_dist; rad += ring_step) {
			double rad_pix = rad * height / max_z;
			// draw ring segment with 1-pix long segments
			double ang_step = 1.0/rad;
			boolean pen_down = false;
			int last_x= 0, last_y = 0;
			for (double az = az0-camera_az; az <= az1-camera_az; az+= ang_step) {
				int x = x0 + (int) Math.round(rad_pix*Math.sin(az));
				int dy = (int) Math.round(rad_pix*Math.cos(az));
				if (dy > height) {
					pen_down = false;
				} else {
					if (pen_down) {
						colorProcessor.lineTo(x, y0-dy);
					} else {
						colorProcessor.moveTo(x, y0-dy);
					}
					pen_down = true;
				}
				last_x = x;
				last_y = y0-dy;
			}
			if (font != null) {
				String txt = String.format("%.0f",rad);
				Rectangle txt_box = colorProcessor.getStringBounds(txt);
				int xt = last_x + grid_margin;
				if ((xt + txt_box.width) > (width - grid_margin)) {
					xt = width - grid_margin - txt_box.width;
				}
				// vertically align text center to the end of line
				int yt = last_y - txt_box.y - txt_box.height/2; // txt_box.y < 0
				colorProcessor.drawString(txt, xt, yt, Color.BLACK); // black background
			}
			
		}
		return;
	}
	
	
	
	
	public static ImagePlus increaseCanvas(
			ImagePlus imp, // color
			int new_width,
			int new_height,
			int x0,
			int y0) {
		final ImageStack stack = imp.getStack();
		final ImageStack new_stack = new ImageStack (new_width, new_height, stack.getSize());
		final int old_width =  imp.getWidth();
		final int old_height = imp.getHeight();
		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 nSlice = ai.getAndIncrement(); nSlice < stack.getSize(); nSlice = ai.getAndIncrement()) {
						ColorProcessor cp = (ColorProcessor) stack.getProcessor(nSlice+1);
						int [] old_pixels = (int []) cp.getPixels(); // [nSlice].getPixels();
						int [] new_pixels = new int[new_width * new_height];
						for (int row = 0; row < old_height; row++) {
							System.arraycopy(
									old_pixels,
									row * old_width,
									new_pixels,
									(y0 + row) * new_width + x0,
									old_width);
						}
						new_stack.setProcessor(new ColorProcessor(new_width, new_height, new_pixels), nSlice+1);
						new_stack.setSliceLabel(stack.getSliceLabel(nSlice+1), nSlice+1); // copy labels
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		String title2 = imp.getTitle()+"-2";
		ImagePlus imp_new = new ImagePlus(title2,new_stack);
		return imp_new;
	}
	public static void insertImage(
			ImagePlus imp_base, // color
			ImagePlus imp_insert, // color
			int x0, // where to insert
			int y0) {
		final int base_width =  imp_base.getWidth();
//		final int base_height = imp_base.getHeight();
		final int insert_width =  imp_insert.getWidth();
		final int insert_height = imp_insert.getHeight();

		final ImageStack stack_base =   imp_base.getStack();
		final ImageStack stack_insert = imp_insert.getStack();
		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 nSlice = ai.getAndIncrement(); nSlice < stack_base.getSize(); nSlice = ai.getAndIncrement()) {
						ColorProcessor cp_base = (ColorProcessor) stack_base.getProcessor(nSlice+1);
						int [] base_pixels = (int []) cp_base.getPixels(); // [nSlice].getPixels();
						ColorProcessor cp_insert = (ColorProcessor) stack_insert.getProcessor(nSlice+1); // slice out of range
						int [] insert_pixels = (int []) cp_insert.getPixels(); // [nSlice].getPixels();
						for (int row = 0; row < insert_height; row++) {
							System.arraycopy(
									insert_pixels,
									row * insert_width,
									base_pixels,
									(y0 + row) * base_width + x0,
									insert_width);
						}
//						new_stack.setProcessor(new ColorProcessor(new_width, new_height, new_pixels), nSlice+1);
//						new_stack.setSliceLabel(stack.getSliceLabel(nSlice+1), nSlice+1); // copy labels
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
//		String title2 = imp.getTitle()+"-2";
//		ImagePlus imp_new = new ImagePlus(title2,new_stack);
		return; //  imp_new;
	}
	public static String getSignedDouble(
			double d,
			String format) {
		String s = (d > 0)? "+" : ( (d <0)? "-":" ");
		return s+ String.format(format, Math.abs(d));
	}

	
	
	public static String saveAsVideo(
			CLTParameters       clt_parameters,
			QuadCLT             scene,     // if not null - use it's model directory
			String              file_path, // if not null, use it regardless of scene
			ImagePlus           imp_color,
			int debugLevel) {
		double  video_fps =          clt_parameters.imp.video_fps;
		int     mode_avi =           clt_parameters.imp.mode_avi;
		int     avi_JPEG_quality =   clt_parameters.imp.avi_JPEG_quality; // 90;
	    boolean run_ffmpeg =         clt_parameters.imp.run_ffmpeg;
		String  video_ext =          clt_parameters.imp.video_ext;
		String  video_codec =        clt_parameters.imp.video_codec.toLowerCase();
		int     video_crf =          clt_parameters.imp.video_crf;
		double  video_bitrate_m =    clt_parameters.imp.video_bitrate_m;		
		boolean remove_avi =         clt_parameters.imp.remove_avi;
		boolean dry_run = false;
		String avi_path = null;
		if (file_path != null) {
			try {
				avi_path=QuadCLT.saveAVI(
						file_path, // String      file_path, // forces avi, removes .tif, tiff, and .avi
						dry_run, // !generate_mapped, // boolean     dry_run, 
						mode_avi,         // int         avi_mode,
						avi_JPEG_quality, // int         avi_JPEG_quality,
						video_fps,        // double      fps,
						imp_color); // imp_scenes_pair[nstereo]);      // ImagePlus   imp)
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return null;
			}
		} else {
			try {
				avi_path=scene.saveAVIInModelDirectory(
						dry_run,          // !generate_mapped, // boolean     dry_run, 
						null,             // String      suffix, // null - use title from the imp
						mode_avi,         // int         avi_mode,
						avi_JPEG_quality, // int         avi_JPEG_quality,
						video_fps,        // double      fps,
						imp_color);       // imp_scenes_pair[nstereo]);      // ImagePlus   imp)
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return null;
			}
		}
		if (avi_path == null) {
			return null;
		}
		if (!run_ffmpeg) {
			return avi_path; // webm not requested
		}
		String webm_path = avi_path.substring(0, avi_path.length()-4)+video_ext;
		String stereo_meta = "";
		
		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 (!dry_run) {
			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) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				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+"\"");
				return avi_path;
			}
		} else {
			System.out.println("Simulated shell command: \""+shellCommand);

		}
		if (remove_avi && !dry_run) {
			(new File(avi_path)).delete();
			System.out.println("Deleted AVI video file: \""+avi_path+"\"");
		}
		System.out.println("Saved video to "+webm_path);
		return webm_path;
	}
	
	public void generateExtractFilterMovingTargets( // move parameters to clt_parameters
			final boolean       radar_mode,
			final boolean       video_pass, // if clt_parameters.cuas_clean_video=true, video_pass=0 - output TIFFS, but no video. If video_pass==1 - only video with no debug
			final boolean       batch_mode,
			final float [][]    fpixels,
			double [][][]       target_sequence,  // non-overlap only if consider marked stronger tiles
			final int           debugLevel) {
///		String model_prefix = parentCLT.getImageName()+getParametersSuffixRslt(clt_parameters,null);
		boolean clean_video =    clt_parameters.imp.cuas_clean_video;      //true;// save video without any debug information for targets, output in TIFF files. False - same output for video and TIFFs

		final int     corr_offset =       clt_parameters.imp.cuas_corr_offset;
		final int     corr_pairs =        clt_parameters.imp.cuas_corr_pairs;
		
		final boolean half_step =         clt_parameters.imp.cuas_half_step; // true;
		final boolean smooth =            clt_parameters.imp.cuas_smooth; // true;
		final int corr_inc =             half_step ? (corr_offset/2) : corr_offset;
		final int     half_accum_range =  corr_pairs/2;
		
		final double  mask_width =        clt_parameters.imp.cuas_mask_width;  // 9;
		final double  mask_blur =         clt_parameters.imp.cuas_mask_blur;   // 3;
		final boolean mask_round =        clt_parameters.imp.cuas_mask_round;  // false;
	    
		int     target_type =    clt_parameters.imp.cuas_target_type;     //  0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		int     known_type =     clt_parameters.imp.cuas_known_type;      //  2; // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		double  known_err =      clt_parameters.imp.cuas_known_err;       // 20; //  Maximal distance between the detected target and UAS log position (in raw image pixels);
		
		double  input_range =    clt_parameters.imp.cuas_input_range;     //  5;
		boolean scale2x =        clt_parameters.imp.cuas_scale2x;         // true;
		boolean ra_background =  clt_parameters.imp.cuas_ra_background;   // true;
		
		boolean intermed_low =   clt_parameters.imp.cuas_intermed_low;    // true;
		boolean intermed_high =  clt_parameters.imp.cuas_intermed_high;   // true;
		
		boolean save_mono =      clt_parameters.imp.cuas_save_mono;       // true;
		boolean save_color0 =     clt_parameters.imp.cuas_save_color;      // true;
		boolean save_video0 =     clt_parameters.imp.cuas_save_video; //  && video_pass;      // true;
		boolean annotate_uas =   clt_parameters.imp.cuas_annotate_uas;    // false; // show circle around UAS position from the flight log
		boolean show_target_color = clt_parameters.imp.cuas_target_color; // false; // show target score and sequence length in the final video
		boolean cuas_gaussian_ra = clt_parameters.imp.cuas_gaussian_ra;  // use temporal Gaussian instead of running average
	    boolean center_targ =    false; // clt_parameters.imp.cuas_center_targ;
	    boolean save_video = save_video0 &&  video_pass;
	    boolean save_color = save_color0 && !save_video; 
	    
		int start_frame = 0;
		int seq_length = corr_offset + corr_pairs;
		final int     frame0 = start_frame + seq_length/2;
		
		
		// Expand the sparse target field, then use the motion vectors as a virtual moving camera
		// to build long-exposure target frames before rendering target/background composites.
		// The accumulation itself is done by shiftAndRenderAccumulate().
		double [][][] extended_targets = extendMotionScan(
				target_sequence,     // final double [][][] motion_scan,
				null, // filter5,     // final boolean [][]  filtered, // centers, should be non-overlapped
				tilesX,    // final int           tilesX)
				2,                    // final int           range, // 1 or 2
				null); // remain);              // final int     []    remain)
		if (intermed_low) {
			ImagePlus imp_extended= showTargetSequence(
					extended_targets,    // double [][][] target_scene_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
//					model_prefix+"-EXTENDED-TORENDER",   // String        title,
					parentCLT.getImageName()+getParametersSuffixRslt(clt_parameters,"-EXTENDED-TORENDER"),					
					!batch_mode,    // boolean       show,
					tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_extended);
		}
		// TODO: determine integration of frames  on per-tile basis (USING 	public static final int RSLT_SLOW =            41; // 1 - slow, 0 - fast) 
	
		float [][] fpixels_accumulated5x5 = shiftAndRenderAccumulate(
				clt_parameters,       // CLTParameters       clt_parameters,
				center_targ, // false,                // final boolean       center,
				true,                 // final boolean       fill_zeros,
				fpixels,              // final float [][]    fpixels,
				extended_targets,     // final double [][][] vector_field,
				frame0,               // final int           frame0,      // for vector_field[0]
				corr_inc,             // final int           frame_step,
				half_accum_range,     // final int           half_range,
				1.0,                  // final double        boost_pairs,
				smooth,               // final boolean       smooth,
				corr_offset,          // final int           corr_offset, // interframe distance for correlation
				true);                // final boolean       batch_mode) {
		// zero-fill undefined tiles
		
		
		ImagePlus imp_accumulated5x5 = ShowDoubleFloatArrays.makeArrays(
				fpixels_accumulated5x5, // float[][] pixels,
				gpu_max_width, // int width,
				gpu_max_height, // int height,
				parentCLT.getImageName()+getParametersSuffixRslt(clt_parameters,"-ACCUMULATED"+(center_targ?"-CENTERED":"")),					
				slice_titles); //String [] titles)
		imp_accumulated5x5.getProcessor().setMinAndMax(-input_range/2, input_range/2);
		if (intermed_high) {
			if (!batch_mode) imp_accumulated5x5.show();
			parentCLT.saveImagePlusInModelDirectory(imp_accumulated5x5);            // ImagePlus   imp)
		}
		double  velocity_scale = 1.0/corr_offset;
//		double [][][] targets60hz = new double [fpixels.length][][];
		float  [][]  background = fpixels;
		String ra_bg_suffix=(ra_background? ("-RABG"+corr_pairs):"");
		if (ra_background) {
			if (cuas_gaussian_ra) {
				background = runningGaussian(
						fpixels,                   // final float [][] fpixels,
						corr_pairs,                // final int        ra_length,
						gpu_max_width); // final int        width)
			} else {
				background = runningAverage(
						fpixels,                   // final float [][] fpixels,
						corr_pairs,                // final int        ra_length,
						gpu_max_width); // final int        width)
			}
		}
		float [][] replaced_targets = shiftAndRenderTargets(
				clt_parameters,          // CLTParameters       clt_parameters,
				center_targ,             //	final boolean     center,
				mask_width,              // final double        mask_width,
				mask_blur,               // final double        mask_blur,
				mask_round,              // final boolean       round,
				fpixels_accumulated5x5,  // final float  [][]   target_keyframes,
				target_sequence,         // final double [][][] target_positions,
				background,              // final float  [][]   background, // background image
				frame0,                  // final int           frame0,
				corr_inc,                // final int           frame_step,
				velocity_scale,          // final double        velocity_scale, // 1.0/(disparity in frames)
///				targets60hz,             // final double [][][] targets60hz, 
				batch_mode,              // final boolean       batch_mode,
				debugLevel);             // final int debugLevel)
		if (save_mono) {
			ImagePlus imp_replaced_targets = ShowDoubleFloatArrays.makeArrays(
					replaced_targets, // float[][] pixels,
					gpu_max_width, // int width,
					gpu_max_height, // int height,
//					model_prefix+"-REPLACED-TARGETS"+(ra_background? "-RABG":""), //String title,
					parentCLT.getImageName()+getParametersSuffixRslt(clt_parameters,"-REPLACED-TARGETS"+(ra_background? "-RABG":"")),					
					scene_titles); //String [] titles)
			imp_replaced_targets.getProcessor().setMinAndMax(-input_range/2, input_range/2);			
			if (!batch_mode) {
				imp_replaced_targets.show();
			}
			parentCLT.saveImagePlusInModelDirectory(imp_replaced_targets);            // ImagePlus   imp)
		}
		boolean clean_pass = clean_video && video_pass;
		String clean_suffix = clean_pass? CLEAN_SUFFIX : "";
		ImagePlus imp_color = convertToRgbAnnotateTargets(
				clt_parameters,             // CLTParameters       clt_parameters,
				clean_pass,                 // final boolean       clean_pass,
				input_range,                // final double        input_range, // 5
				scale2x,                    // boolean       scale2x,
				target_type,                // final int target_type, //  = 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				known_type,                 // final int           known_type, // 2;  // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				known_err,                  // final double        known_err,  // 20; //  Maximal distance between the detected target and UAS log position (in raw image pixels);
				replaced_targets,           // final float  [][]   fpixels,
//				targets60hz,                // final double [][][] targets60hz,
				show_target_color,          // final boolean       change_color,
				annotate_uas,               // final boolean       show_uas,
				frame0,                     // final int           frame0,
				corr_inc,                   // final int           frame_step,
				gpu_max_width,   // final int           width,
//				model_prefix+"-RGB"+ra_bg_suffix+clean_suffix, // String              title,
				parentCLT.getImageName()+getParametersSuffixRslt(clt_parameters,"-RGB"+ra_bg_suffix+clean_suffix),
				scene_titles,               // String []           titles,
				uasLogReader,               // UasLogReader           uasLogReader,				
				debugLevel);                // final int           debugLevel) {
		if (radar_mode) {
			// extend image to the left 
			int height2 = imp_color.getHeight();
			int width2 = (int) (16.0/9.0*height2);
			int offset_x = width2 -  imp_color.getWidth();
			imp_color= increaseCanvas(
					imp_color, // ImagePlus imp, // color
					width2,    // int new_width,
					height2,   // int new_height,
					offset_x,  // int x0,
					0);        // int y0) 
			int    annot_mode = clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_RADAR_PANE_INDX];
			if (video_pass) {
				annot_mode &= clt_parameters.imp.cuas_annot_sel[ANNOT_CLEAN_VIEW_INDX]; //ANNOT_CLEAN;
			}
			if (debugLevel > -4) {
				System.out.println(String.format("Radar annotation mode 0x%04x (0x%04x & 0x%04x)",
						annot_mode, clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_RADAR_PANE_INDX],clt_parameters.imp.cuas_annot_sel[ANNOT_CLEAN_VIEW_INDX]));
			}
			ImagePlus img_radar = generateRadarImage(
					clt_parameters,
					annot_mode, // -1, // int           annot_mode, // specify bits
					parentCLT,    // QuadCLT            scene,
					target_sequence,
					uasLogReader,    // contains camera orientation (getCameraATR())
					parentCLT.getImageName(), // title,
					slice_titles,   // corresponding to top targets dimension
					scene_titles, // all titles, one per frame
					debugLevel);
			insertImage(
					imp_color, // ImagePlus imp_base, // color
					img_radar, // ImagePlus imp_insert, // color
					0, // int x0, // where to insert
					0); // int y0)
		}
		
		// save tiff in model directory
		if (imp_color != null) {
			if (save_color) {
				if (!batch_mode) {
					imp_color.show();
				}
				parentCLT.saveImagePlusInModelDirectory(imp_color);      // ImagePlus   imp)
			}
			// temporary, will use model directory
			if (save_video) {
				saveAsVideo( // will take name from the image title
						clt_parameters, // CLTParameters       clt_parameters,
						parentCLT,      // QuadCLT             scene,     // if not null - use it's model directory
						null, // video_path_tmp, // String              file_path,
						imp_color,      // ImagePlus           imp_color,
						debugLevel);    // int debugLevel) {
			}
		}
		return;
	}

	/**
	 * Fill traget fields for the best match error for the previous and next keyframe. Relies on the fields provided by
	 * getHalfBeforeAfterPixXY() 
	 * @param targets target data [keyframe index][ntile][]
	 * @param good_only exclude failed tiles
	 * @param tilesX number of tiles in a row (80 for Boson640)
	 */
	
	public static void calculateMismatchBeforeAfter(
			final double [][][][]  targets_multi,
			final boolean          good_only,
			final int              tilesX) {
		final int num_seq = targets_multi.length;
		final int num_tiles = targets_multi[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int dbg_seq = 26 - 1;
		final int dbg_tile = (38+45*tilesX);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(tilesX, num_tiles/tilesX); 
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							if ((nSeq==dbg_seq) && (ntile == dbg_tile)) {
								System.out.println("calculateMismatchBeforeAfter(): nSeq="+nSeq+", ntile="+ntile);
							}
							double [][] targets = targets_multi[nSeq][ntile];
							if (targets != null) { 
								for (int ntarg = 0; ntarg < targets.length; ntarg++) if (targets[ntarg] != null) {
									double [] target = targets[ntarg];
									if ((target != null) && (!good_only ||
											((target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
													(target[CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED)))) {
										double [] mm = {Double.NaN, Double.NaN};
										int [] mm_dirs = {-1,-1};
										int [] mm_alts = {0,0};
										//										if (target != null) {
										// should be no "continue" to always erase any previous values in these field by NaN
										boolean has_neib = true;
										look_for_neibs: {
											for (int dseq = -1; dseq <= 1; dseq+=2) {
												int nSeq1 = nSeq+dseq;
												if ((nSeq1 >= 0) && (nSeq1 < num_seq)) {
													for (int dir = 8; dir >= 0; dir--) {
														int tile1 = tn.getNeibIndex(ntile, dir);
														if ((tile1 >= 0) && (targets_multi[nSeq1][tile1] != null)) {
															for (int ntarg1 = 0; ntarg1 < targets_multi[nSeq1][tile1].length; ntarg1++) {
																double [] target1 = targets_multi[nSeq1][tile1][ntarg1];
																if  (!good_only || (
																		(target1[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
																		(target1[CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED))) {
																	break look_for_neibs;
																}
															}
														}
													}
												}
											}
											has_neib = false;
										}
										if (has_neib) {
											for (int dseq = -1; dseq <= 1; dseq+=2) {
												int indx = (dseq > 0) ? 1 : 0;
												int ba_indx_this =  CuasMotionLMA.RSLT_BX  + 2 * indx;
												int ba_indx_other = CuasMotionLMA.RSLT_BX  + 2 * (1 - indx);
												double [] this_ba = {target[ba_indx_this], target[ba_indx_this + 1]};	//{x,y}									
												int nSeq1 = nSeq+dseq;
												if ((nSeq1 >= 0) && (nSeq1 < num_seq)) {
													for (int dir = 8; dir >= 0; dir--) { // dir==8 is center tile - most likely
														int tile1 = tn.getNeibIndex(ntile, dir);
														if (tile1 >= 0) {
															double [][] targets_other = targets_multi[nSeq1][tile1];
															if (targets_other != null) {
																for (int ntarg1 = 0; ntarg1 < targets_multi[nSeq1][tile1].length; ntarg1++) {
																	double [] target_other = targets_other[ntarg1];
																	if ((target_other != null)  &&
																			(!good_only || (target_other[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) ) {
																		double [] other_ba = {target_other[ba_indx_other], target_other[ba_indx_other + 1]};
																		double dx = this_ba[0] - other_ba[0];
																		double dy = this_ba[1] - other_ba[1];
																		double mismatch = Math.sqrt(dx*dx + dy*dy);
																		if (!(mismatch > mm[indx])) { // tolerates NaN when not yet set
																			mm[indx] = mismatch;
																			mm_dirs[indx] = dir;
																			mm_alts[indx] = ntarg1;
																		}
																	}
																}
															}
														}
													}
												}
											}
										}
										target[CuasMotionLMA.RSLT_MISMATCH_BEFORE] = mm[0];
										target[CuasMotionLMA.RSLT_MISMATCH_AFTER] =  mm[1];
										target[CuasMotionLMA.RSLT_MISMATCH_DIRS] =   encodeDirs(mm_dirs,mm_alts);
									}
								}
							}
						} // for (int ntile = 0; ntile < num_tiles; ntile++)
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return; //  filtered_tiles;
	}
	
	private static double encodeDirs(int [] mm_dirs, int [] mm_alts) {
		int rslt = (mm_dirs[0]+1) + 10 * (mm_dirs[1]+1);
		rslt += 100 * (mm_alts[0] + MAX_ALT_TARGETS * mm_alts[1]) ;
		return rslt;
	}
	
	
	private static int [] decodeDirs(double ddirs) {
		int d = (int) Math.round(ddirs);
		int dirs =  d % 100;
		int layers = d / 100;
		return new int [] {(dirs % 10) -1, (dirs / 10) -1, layers % MAX_ALT_TARGETS, layers / MAX_ALT_TARGETS};
	}
	
	
	
	public static String getParametersSuffix(
			 CLTParameters       clt_parameters,	
			 String              prefix) {
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset;
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs;
		
		int     temporal_um =    clt_parameters.imp.cuas_temporal_um;
		double  tum_threshold =  clt_parameters.imp.cuas_tum_threshold;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
		
		int     precorr_ra =     clt_parameters.imp.cuas_precorr_ra;
		int     corr_ra_step =   clt_parameters.imp.cuas_corr_step;
		double  rstr =           clt_parameters.imp.cuas_rstr;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
	    double  lma_rms =        clt_parameters.imp.cuas_lma_rms; // =       1.5;  // Maximal RMS, regardless of amplitude
	    double  lma_arms =       clt_parameters.imp.cuas_lma_arms; // =      0.06; // Maximal absolute RMS, sufficient for any amplitude
	    double  lma_rrms =       clt_parameters.imp.cuas_lma_rrms; // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
	    double  lma_mina =       clt_parameters.imp.cuas_lma_mina; // =      1.0;  // Minimal A (amplitude)
	    double  fat_zero =       clt_parameters.imp.cuas_fat_zero;
	    double  lmax_fraction =  clt_parameters.imp.cuas_lmax_fraction; // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
	    double  lmax_hack_ridge =  clt_parameters.imp.cuas_lmax_hack_ridge; // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
	    
	    double  lmax_flt_neglim= clt_parameters.imp.cuas_lmax_flt_neglim;   // -0.3;   // limit negative data to reduce ridge influence (make -10 to disable)        
	    double  lmax_flt_hsigma =  clt_parameters.imp.cuas_lmax_flt_hsigma; // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile 
	    double  lmax_flt_lsigma =  clt_parameters.imp.cuas_lmax_flt_lsigma; // 1.0 // LPF - GB result of the previous subtraction
	    double  lmax_flt_scale =  clt_parameters.imp.cuas_lmax_flt_scale;   // 5.0 // scale filtering result
	    boolean center_targ =    clt_parameters.imp.cuas_center_targ;
	    
	    
		String ps = ((prefix != null)?prefix:"") + "-OFFS"+corr_offset+"-PAIRS"+corr_pairs+"-RSTR"+rstr+"-RMS"+lma_rms+
				"-SRMS"+lma_arms+"-RRMS"+lma_rrms+"-AMP"+lma_mina+"-FZ"+fat_zero+"-PCRA"+precorr_ra+"-PS"+corr_ra_step+
				"-TUM"+temporal_um+"-TT"+tum_threshold+"-MF"+lmax_fraction+
				"-HR"+lmax_hack_ridge+
				"-NL"+lmax_flt_neglim+"-FH"+lmax_flt_hsigma+"-FL"+lmax_flt_lsigma+"-FS"+lmax_flt_scale;
		if (center_targ) {
			ps+="-CENT";
		}
		return ps;
	}

	public static String getParametersSuffixRanging(
			 CLTParameters       clt_parameters,	
			 String              prefix) {
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset;
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs;
		
		int     temporal_um =    clt_parameters.imp.cuas_temporal_um;
		double  tum_threshold =  clt_parameters.imp.cuas_tum_threshold;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
		
		int     precorr_ra =     clt_parameters.imp.cuas_precorr_ra;
		int     corr_ra_step =   clt_parameters.imp.cuas_corr_step;
		double  rstr =           clt_parameters.imp.cuas_rstr;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
	    double  lma_rms =        clt_parameters.imp.cuas_lma_rms; // =       1.5;  // Maximal RMS, regardless of amplitude
	    double  lma_arms =       clt_parameters.imp.cuas_lma_arms; // =      0.06; // Maximal absolute RMS, sufficient for any amplitude
	    double  lma_rrms =       clt_parameters.imp.cuas_lma_rrms; // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
	    double  lma_mina =       clt_parameters.imp.cuas_lma_mina; // =      1.0;  // Minimal A (amplitude)
	    double  fat_zero =       clt_parameters.imp.cuas_fat_zero;
	    double  lmax_fraction =  clt_parameters.imp.cuas_lmax_fraction; // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
	    double  lmax_hack_ridge =  clt_parameters.imp.cuas_lmax_hack_ridge; // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
	    boolean center_targ =    clt_parameters.imp.cuas_center_targ;
	    
	    
		String ps = ((prefix != null)?prefix:"") + "-OFFS"+corr_offset+"-PAIRS"+corr_pairs+"-RSTR"+rstr+"-RMS"+lma_rms+
				"-SRMS"+lma_arms+"-RRMS"+lma_rrms+"-AMP"+lma_mina+"-FZ"+fat_zero+"-PCRA"+precorr_ra+"-PS"+corr_ra_step+
				"-TUM"+temporal_um+"-TT"+tum_threshold+"-MF"+lmax_fraction+
				"-HR"+lmax_hack_ridge+
///				"-NL"+lmax_flt_neglim+"-FH"+lmax_flt_hsigma+"-FL"+lmax_flt_lsigma+"-FS"+lmax_flt_scale+
				"-CB"+clt_parameters.imp.cuas_rng_combine+(clt_parameters.imp.cuas_rng_coswnd?"-WC":"-WR")+
				"-UM"+(clt_parameters.imp.cuas_rng_um2?"D":"") +clt_parameters.imp.cuas_rng_um_sigma+
				"-FZ"+clt_parameters.imp.cuas_rng_fz0+":"+clt_parameters.imp.cuas_rng_fz+
				"-R"+clt_parameters.imp.cuas_rng_radius0+":"+clt_parameters.imp.cuas_rng_radius+
				"-M"+clt_parameters.imp.cuas_mcorr_sel+"-"+clt_parameters.imp.cuas_mcorr_sel_lma;
		if (center_targ) {
			ps+="-CENT";
		}
		return ps;
	}
	
	
	
	public static String getParametersSuffixRslt(
			 CLTParameters       clt_parameters,	
			 String              prefix) {
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset;
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs;
		
		int     temporal_um =    clt_parameters.imp.cuas_temporal_um;
		double  tum_threshold =  clt_parameters.imp.cuas_tum_threshold;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
		
		int     precorr_ra =     clt_parameters.imp.cuas_precorr_ra;
		int     corr_ra_step =   clt_parameters.imp.cuas_corr_step;
		double  rstr =           clt_parameters.imp.cuas_rstr;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
	    double  lma_rms =        clt_parameters.imp.cuas_lma_rms; // =       1.5;  // Maximal RMS, regardless of amplitude
	    double  lma_arms =       clt_parameters.imp.cuas_lma_arms; // =      0.06; // Maximal absolute RMS, sufficient for any amplitude
	    double  lma_rrms =       clt_parameters.imp.cuas_lma_rrms; // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
	    double  lma_mina =       clt_parameters.imp.cuas_lma_mina; // =      1.0;  // Minimal A (amplitude)
	    double  fat_zero =       clt_parameters.imp.cuas_fat_zero;
	    double  lmax_fraction =  clt_parameters.imp.cuas_lmax_fraction; // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
	    double  lmax_hack_ridge= clt_parameters.imp.cuas_lmax_hack_ridge; // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
	    
	    double  lmax_flt_neglim= clt_parameters.imp.cuas_lmax_flt_neglim;   // -0.3;   // limit negative data to reduce ridge influence (make -10 to disable)        
	    double  lmax_flt_hsigma= clt_parameters.imp.cuas_lmax_flt_hsigma; // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile 
	    double  lmax_flt_lsigma= clt_parameters.imp.cuas_lmax_flt_lsigma; // 1.0 // LPF - GB result of the previous subtraction
	    double  lmax_flt_scale = clt_parameters.imp.cuas_lmax_flt_scale;   // 5.0 // scale filtering result
//	    boolean center_targ =    clt_parameters.imp.cuas_center_targ;
	    
		double  min_score_lma =  clt_parameters.imp.cuas_score_lma;
		int     min_seq=         clt_parameters.imp.cuas_min_seq;    // 3;     // minimal number of consecutive key frames for the same target
		int     enough_seq=      clt_parameters.imp.cuas_enough_seq; // 5;     // minimal number of consecutive key frames for the same target
		double  seq_travel=      clt_parameters.imp.cuas_seq_travel; // 3.0;   // minimal diagonal of the bounding box that includes sequence to be considered "cuas_enough_seq". Filtering out atmospheric fluctuations
		
	    
		String ps = ((prefix != null)?prefix:"") + "-OFFS"+corr_offset+"-PAIRS"+corr_pairs+"-RSTR"+rstr+"-RMS"+lma_rms+
				"-SRMS"+lma_arms+"-RRMS"+lma_rrms+"-AMP"+lma_mina+"-FZ"+fat_zero+"-PCRA"+precorr_ra+"-PS"+corr_ra_step+
				"-TUM"+temporal_um+"-TT"+tum_threshold+"-MF"+lmax_fraction+
				"-HR"+lmax_hack_ridge+
				"-NL"+lmax_flt_neglim+"-FH"+lmax_flt_hsigma+"-FL"+lmax_flt_lsigma+"-FS"+lmax_flt_scale+
				"-MS"+min_score_lma+"-ML"+min_seq+"-SL"+enough_seq+"-MT"+seq_travel+
				"-CB"+clt_parameters.imp.cuas_rng_combine+(clt_parameters.imp.cuas_rng_coswnd?"-WC":"-WR")+
				"-UM"+(clt_parameters.imp.cuas_rng_um2?"D":"") +clt_parameters.imp.cuas_rng_um_sigma+
				"-FZ"+clt_parameters.imp.cuas_rng_fz0+":"+clt_parameters.imp.cuas_rng_fz+
				"-R"+clt_parameters.imp.cuas_rng_radius0+":"+clt_parameters.imp.cuas_rng_radius+
				"-M"+clt_parameters.imp.cuas_mcorr_sel+"-"+clt_parameters.imp.cuas_mcorr_sel_lma;
		
		
		return ps;
	}
	
	
	
	
	// =============================== targets_multi ======================================
	
	
	// Uses 	final static public  int MAX_ALT_TARGETS = 10; // convenient to be power of 10

	public static double [][][][] getAccumulatedCoordinatesMulti(
			final boolean       keep_failed, // keep failed targets
			final int           when, // set if >=0 for failures
			final boolean       centered, // accum_data was centered, use target_sequence[][][CuasMotionLMA.RSLT_X], target_sequence[][][CuasMotionLMA.RSLT_Y] (if not NaN)
			final double [][][] probed_sequence, // contains vector_fields data, single target per tile
			final float  [][]   accum_data,    // should be around 0, no low-freq
		    final double        lmax_fraction, // 0.7;     // Check if local maximum is separated from the surrounding by this fraction of the maximum value
		    final double        lmax_flt_neglim,   // -0.3;   // limit negative data to reduce ridge influence (>= to disable)        
	        final double        lmax_flt_hsigma,	 // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile     
	        final double        lmax_flt_lsigma,   // 1.0 // LPF - GB result of the previous subtraction
	        final double        lmax_flt_scale,    // 5.0 // scale filtering result
	        final double []     sky_mask,
	        final double        sky_threshold,    // 0.9 // minimal value of the sky mask where target is possible
	        final double        lma_horizon,
		    final double        lmax_hack_ridge, // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
		    final double        lmax_radius,   // 3;       // look inside cuas_lmax_radius* 2 + 1 square for the local maximum isolation
		    final double        centered_radius,
		    final double        centered_radius_blur, 
		    final double        pix_tolerance, // 
//			final boolean       lmax_zero,     // true;    // zero all data outside this radius from the maximum
			final double        centroid_radius,
			final int           n_recenter,    //  re-center window around new maximum. 0 -no refines (single-pass)
			final int           tilesX,
			final boolean       no_border,
		// Moving target LMA	
			final double        lma_sigma,
			final double        wnd_pedestal,
			final double        lma_r0,
			final double        lma_ovrsht,
		// CUAS Motion LMA parameters
			final boolean       lma_fit_xy,		
			final boolean       lma_fit_a,						
			final boolean       lma_fit_c,
			final boolean       lma_fit_r,
			final boolean       lma_fit_k,			
			final double        lambda,
			final double        lambda_good,
			final double        lambda_bad,
			final double        lambda_max,
			final double        rms_diff,
			final int           num_iter,
			final double        pre_min_a,   // pre-filter minimal LMA-A (half finbal?)
			final int           min_keep, //  2; // keep at least this number of candidates before using cuas_lma_pre_mina filter
			final float [][]    accum_debug,
			final QuadCLT       parentCLT, // debug only, maybe null in other cases
			final int           debugLevel){
		// Will not set [RSLT_FAIL] = FAIL_NONE, but it is not final before
		// apply masking in centered mode
		// final int min_keep = 2; // Keep at least this number of (preliminary) best candidates
		final int tile1 = GPUTileProcessor.DTT_SIZE;
		final int tile2 = 2 * tile1;
		final int dbg_tile=-(12 + 23*80);
		final double [] mask_centered = (centered && (centered_radius > 0)) ? new double [tile2* tile2]: null;
		final boolean [] disabled_centered = (mask_centered != null) ? (new boolean [mask_centered.length]) : null;
		if (mask_centered != null) {
			double lmax_inner_radius = centered_radius * (1.0-centered_radius_blur);
			double lmax_outer_radius = centered_radius * (1.0+centered_radius_blur);
			double lmax_inner_radius2 = lmax_inner_radius * lmax_inner_radius;
			double lmax_outer_radius2 = lmax_outer_radius * lmax_outer_radius;
			
			
			for (int y = 0; y < tile2; y++) {
				int dy = y - tile1;
				int dy2 = dy*dy;
				for (int x = 0; x < tile2; x++) {
					int dx = x - tile1;
					int r2 = dy2 + dx*dx;
					int indx = x + tile2 * y;
					if (r2 < lmax_inner_radius2) {
						mask_centered[indx] = 1.0;
						disabled_centered[indx] = false;
					} else if (r2 > lmax_outer_radius2) {
						mask_centered[indx] = 0.0;
						disabled_centered[indx] = true;
					} else {
						double r = Math.sqrt(r2);
						mask_centered[indx] = 0.5 * (1.0 + Math.cos(Math.PI*(r-lmax_inner_radius)/(lmax_outer_radius-lmax_inner_radius)));
						disabled_centered[indx] = false;
					}
				}
			}
		}
		
		final double pix_tolerance2= pix_tolerance * pix_tolerance;
		if (debugLevel > 0 ) {
			System.out.println("getAccumulatedCoordinates(): "+((sky_mask == null)? "NOT ":"")+"using sky_mask");
		}
		final boolean dbg_hack_ridge = (debugLevel > 0); // true;
		final int lmax_iradius = (int) Math.floor(lmax_radius);
		final int num_seq = probed_sequence.length; // same as accum_data.length
		final int num_tiles = probed_sequence[0].length;
		final int num_pixels = accum_data[0].length;
		final int width = GPUTileProcessor.DTT_SIZE * tilesX;
		final int height = num_pixels / width; 
		final double [][][][] targets_new = new double[num_seq][num_tiles][][];
		
		
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		
		final boolean [] param_select = new boolean [CuasMotionLMA.INDX_LEN];
		param_select[CuasMotionLMA.INDX_X0] =  lma_fit_xy;
		param_select[CuasMotionLMA.INDX_Y0] =  lma_fit_xy;
		param_select[CuasMotionLMA.INDX_A] =   lma_fit_a;
		param_select[CuasMotionLMA.INDX_C] =   lma_fit_c;
		param_select[CuasMotionLMA.INDX_RR0] = lma_fit_r;
		param_select[CuasMotionLMA.INDX_K] =   lma_fit_k;
//		final int dbg_tile = -(38 + 45 * 80); //(38 + 45 * 80);
		final int dbg_seq = -48;
		boolean show_dbg = (dbg_tile >= 0) && (parentCLT != null);
		final String [] dbg_titles = {"mask","tile","masked","limited","blured","um","um-blured","um-blured-scaled",
				"centroid","lma-Y","fX","ymfx","weights"};
		double [][] dbg_stack = show_dbg ? new double [dbg_titles.length][] : null;
		if ((dbg_stack != null) && (mask_centered != null)) {
			dbg_stack [0] = mask_centered.clone();
		}
		final boolean use_filters = (lmax_flt_hsigma > 0) && (lmax_flt_lsigma > 0) && (lmax_flt_scale > 0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] pix_tile = new double [tile2 * tile2];
					double [] pix_tile_filtered0 = use_filters ? new double [tile2 * tile2]:null;
					double [] pix_tile_filtered =  use_filters ? new double [tile2 * tile2]:null;
					boolean [] disabled = new boolean[pix_tile.length];
					TileNeibs tn = new TileNeibs(tile2,tile2);
					DoubleGaussianBlur gb = new DoubleGaussianBlur();
					CuasMotionLMA cuasMotionLMA = new CuasMotionLMA(
							tile2,      // int         width,
							lma_sigma, // double     sigma);
							wnd_pedestal);
					// may be faster if process only where vector_field[nseq][ntile] is not null
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						if (accum_debug != null) {
							accum_debug[nSeq] = new float [num_pixels];
							Arrays.fill(accum_debug[nSeq], Float.NaN);
						}
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							boolean dbg_img = (nSeq==dbg_seq) &&  (dbg_stack != null) && (ntile == dbg_tile); 
							if (probed_sequence[nSeq][ntile] != null) {
								double [] probed_target = probed_sequence[nSeq][ntile].clone();
								if ((nSeq==dbg_seq) && (ntile == dbg_tile)) {
									System.out.println("getAccumulatedCoordinatesMulti(): nSeq="+nSeq+", ntile="+ntile);
								}
								int tileX = ntile % tilesX;
								int tileY = ntile / tilesX;
								int px0 = tileX * GPUTileProcessor.DTT_SIZE - GPUTileProcessor.DTT_SIZE/2; // top-left of 16x16 tile 
								int py0 = tileY * GPUTileProcessor.DTT_SIZE - GPUTileProcessor.DTT_SIZE/2; // top-left of 16x16 tile
								int horizon_y = (int) Math.ceil (lma_horizon)-py0;
								if (centered && !Double.isNaN(probed_target[CuasMotionLMA.RSLT_Y])) {
									horizon_y = (int) Math.ceil (lma_horizon - probed_target[CuasMotionLMA.RSLT_Y])-py0;
								}
								int y_mask_offs =  (centered && !Double.isNaN(probed_target[CuasMotionLMA.RSLT_Y])) ? ((int) Math.round(probed_target[CuasMotionLMA.RSLT_Y])):0;
								int x_mask_offs =  (centered && !Double.isNaN(probed_target[CuasMotionLMA.RSLT_X])) ? ((int) Math.round(probed_target[CuasMotionLMA.RSLT_X])):0;
								
								
								// copy 16x16 tile
								{
									int indx_dst = 0;
									for (int y = 0; y < tile2; y++) {
										int indx_src = px0 + (py0 +y) * width;
										if (y >=  horizon_y) {
											for (int x = 0; x < tile2; x++) {
												pix_tile[indx_dst++] = 0;
												indx_src++; // float-> double
											}
										} else {
											for (int x = 0; x < tile2; x++) {
												pix_tile[indx_dst++] = accum_data[nSeq][indx_src++]; // float-> double
											}
										}
									}
								}
								if (dbg_img) {
									dbg_stack [1] = pix_tile.clone();
								}
								
								
								// find absolute maximum (before hacking and masking)
								if (disabled_centered != null) {
									System.arraycopy(disabled_centered, 0, disabled, 0, disabled.length);
								} else {
									Arrays.fill(disabled,false);
								}
								int ntile_amax0 = TileNeibs.getAmaxTile(
										pix_tile); //double [] data)
								if (lmax_hack_ridge > 0) {
//									disabled = new boolean[pix_tile.length];
									double thresh = pix_tile[ntile_amax0] * lmax_hack_ridge;
									for (int row=0; row < tile2; row++) {
										double s = 0;
										int n = 0;
										for (int col = 0; col < tile2; col++) {
											double d = Math.abs(pix_tile[row*tile2+col]);
											if (!Double.isNaN(d)) {
												s += d;
												n++;
											}
										}
										s/= n;
										if (s > thresh) {
											for (int col = 0; col < tile2; col++) {
												int pindx = row*tile2+col;
												disabled[pindx] = true;
												pix_tile[pindx] = 0.0;
											}											
										}
									}
								}
								// in centered mode (and only in centered mode) mask out peripheral areas, keep only around the center
								if (mask_centered != null) {
									for (int i = 0; i < pix_tile.length; i++) {
										pix_tile[i] *= mask_centered[i];
									}
								}
								if (dbg_img) {
									dbg_stack [2] = pix_tile.clone();
								}
								
								if (sky_mask != null) {
									for (int row = 0; row < tile2; row++) {
										int mask_y = Math.min(height-1, Math.max(0, py0 + y_mask_offs + row));
										for (int col = 0; col < tile2; col++) {
											int mask_x = Math.min(width-1, Math.max(0, px0 + x_mask_offs + col));
											int pindx = col+row*tile2;
											double mask_val = sky_mask[mask_x + mask_y * width];
											pix_tile[pindx] *= mask_val;
											if (mask_val < sky_threshold) {
												disabled[pindx] = true;  // disallow maximums in this area
											}
										}
									}
									if ((nSeq==dbg_seq) && (ntile == dbg_tile)) {
										System.out.println("getAccumulatedCoordinatesMulti(): nSeq="+nSeq+", ntile="+ntile+
												" py0="+py0+" px0="+px0+", y_mask_offs="+y_mask_offs+", x_mask_offs="+x_mask_offs);
										for (int row = 0; row < tile2; row++) {
											int mask_y = Math.min(height-1, Math.max(0, py0 + y_mask_offs + row));
											for (int col = 0; col < tile2; col++) {
												int mask_x = Math.min(width-1, Math.max(0, px0 + x_mask_offs + col));
												int pindx = col+row*tile2;
												double mask_val = sky_mask[mask_x + mask_y * width];
												if (col == 0) {
													System.out.println("row="+row+", mask_y="+mask_y+"col="+col+", mask_x="+mask_x+", mask_val="+mask_val+", pindx="+pindx);
												}
											}
										}
									}
								}
								// find absolute maximum (after "hacking" and masking
								int ntile_amax = TileNeibs.getAmaxTile(
										pix_tile); //double [] data)
								// filtering tile for better maximum selection
								if (pix_tile_filtered != null) {
									System.arraycopy(pix_tile, 0, pix_tile_filtered, 0, pix_tile_filtered.length);
								  	if (lmax_flt_neglim <0) {
										for (int i = 0; i < pix_tile_filtered.length; i++) {
											pix_tile_filtered[i] = Math.max(lmax_flt_neglim, pix_tile_filtered[i]);
										}
								  	}
								    if (dbg_img) { // limited by -0.3
								        dbg_stack [3] = pix_tile_filtered.clone();
								    }
									System.arraycopy(pix_tile_filtered, 0, pix_tile_filtered0,  0, pix_tile_filtered.length);
									gb.blurDouble(
											pix_tile_filtered, //
											tile2,
											tile2,
											lmax_flt_hsigma,   // double sigmaX,
											lmax_flt_hsigma,   // double sigmaY,
											0.01);             // double accuracy)
								    if (dbg_img) { // blured
								        dbg_stack [4] = pix_tile_filtered.clone();
								    }
									for (int i = 0; i < pix_tile_filtered.length; i++) {
										pix_tile_filtered[i] = pix_tile_filtered0[i] - pix_tile_filtered[i];
									}
								    if (dbg_img) { // UM
								        dbg_stack [5] = pix_tile_filtered.clone();
								    }
									gb.blurDouble(
											pix_tile_filtered, //
											tile2,
											tile2,
											lmax_flt_lsigma,   // double sigmaX,
											lmax_flt_lsigma,   // double sigmaY,
											0.01);             // double accuracy)
								    if (dbg_img) { // UM-blured
								        dbg_stack [6] = pix_tile_filtered.clone();
								    }

									for (int i = 0; i < pix_tile_filtered.length; i++) {
										pix_tile_filtered[i] *= lmax_flt_scale;
									}
								    if (dbg_img) { // UM-blured-scaled
								        dbg_stack [7] = pix_tile_filtered.clone();
								    }
									ntile_amax = TileNeibs.getAmaxTile(
											pix_tile_filtered); //double [] data)
								}
								
								
								
								double [] pix_tile_centroid = (pix_tile_filtered != null) ? pix_tile_filtered : pix_tile;
								// accum_debug will apply circular mask only if no filtering. With filtering it will contain filtered (HPF+LPF) data
								if (accum_debug != null) {
									int indx_src = 0;
									for (int y = 0; y < tile2; y++) {
										int indx_dst = px0 + (py0 +y) * width;
										for (int x = 0; x < tile2; x++) {
											accum_debug[nSeq][indx_dst++] = (float) pix_tile_centroid[indx_src++]; // double -> float
										}
									}
								}
								
							    if (dbg_img) { // UM-blured-scaled
							        dbg_stack [8] = pix_tile_centroid.clone();
							        dbg_stack [9] = pix_tile.clone(); // lma
							    }
								
								
								double amax_val = pix_tile_centroid[ntile_amax];
								double lmax_val = amax_val;

								int [] max_indices = tn.getLocalMaxes(
										pix_tile_centroid, //double [] data,
										true); // boolean   exclude_margins)
								ArrayList<Integer> max_candidate = new ArrayList<Integer>();

								for (int indx:max_indices) {
									boolean isolated = tn.isMaxIsolated(
											pix_tile_centroid,      // double [] data,
											indx,          // int       ntile,
											lmax_fraction, // double    fraction,
											lmax_iradius); //int       radius)) {
									if (disabled[indx]) {
										if (dbg_hack_ridge && isolated) {
											System.out.println ("getAccumulatedCoordinatesMulti(): Removing ridge max for nSeq="+nSeq+", ntile="+ntile+", indx="+indx);
										}
										isolated = false;
									}
									if (isolated) {
										max_candidate.add(indx);
//										double max_val = pix_tile_centroid[indx];
									}
								}
								
								if (!max_candidate.isEmpty()) {
									// Sort descending values at maximal pixels
									Collections.sort(max_candidate, new Comparator<Integer>() {
										@Override
										public int compare(Integer lhs, Integer rhs) { // descending
											return (pix_tile_centroid[lhs] > pix_tile_centroid[rhs]) ? -1 : ((pix_tile_centroid[lhs] < pix_tile_centroid[rhs]) ? 1 : 0) ;
										}
									});
									double [][] targets = new double [max_candidate.size()][];
									ArrayList<Integer> targets_list = new ArrayList<Integer>();
									for (int indx = 0; indx <  max_candidate.size(); indx++) {
										int use_max = max_candidate.get(indx);
										double [] mv = Correlation2d.getMaxXYCm( // last, average (Will be relative to the center of the tile)
												pix_tile,        // corrs.length-1], // double [] data,
												tile2,           // int       data_width,      //  = 2 * transform_size - 1; // negative - will return center fraction
												centroid_radius, // double    radius, // 0 - all same weight, > 0 cosine(PI/2*sqrt(dx^2+dy^2)/rad)
												n_recenter,      // int       refine, //  re-center window around new maximum. 0 -no refines (single-pass)
												null,            //          fpn_mask,        // boolean [] fpn_mask,
												false,           // boolean    ignore_border, // only if fpn_mask != null - ignore tile if maximum touches fpn_mask
												no_border,       // boolean    exclude_margins,
												true,            // boolean    calc_fraction,
												use_max,         // int        imax,  // index of the maximum in data[]
												false);          // boolean   debug)
										if (mv != null) {
											double [] target = probed_target.clone(); // includes motion vectors and old Q_* values!
											// was a bug (04/08.2026): target[CuasMotionLMA.RSLT_FAIL] should be undefined!
											target[CuasMotionLMA.RSLT_FAIL] = Double.NaN;
											target[CuasMotionLMA.RSLT_WHEN] = Double.NaN; // just in case
											
											target[CuasMotionLMA.RSLT_CENT_MX] = 0.0;
											target[CuasMotionLMA.RSLT_CENT_X] =  mv[0];
											target[CuasMotionLMA.RSLT_CENT_Y] =  mv[1];
											target[CuasMotionLMA.RSLT_CENT_MX] = mv[2];
											if (mv.length > 3) {
												target[CuasMotionLMA.RSLT_CENT_F] =  mv[3];
											}
											cuasMotionLMA.prepareLMA(
													param_select, // boolean [] param_select,
													pix_tile, // double []  tile_data,
													mv[0], // double     xc, // relative to center =width/2
													mv[1], // double     yc, // relative to center =width/2
													lma_r0, // double     r0,
													lma_ovrsht, // double     k,
													lmax_val,   // double     lmax_val,
													debugLevel); // int        debugLevel)
											int rslt = cuasMotionLMA.runLma( // <0 - failed, >=0 iteration number (1 - immediately)
													lambda,      // double lambda,           // 0.1
													lambda_good, // double lambda_scale_good,// 0.5
													lambda_bad,  // double lambda_scale_bad, // 8.0
													lambda_max,  // double lambda_max,       // 100
													rms_diff,    // double rms_diff,         // 0.001
													num_iter,    // int    num_iter,         // 20
													nSeq,        // int    dbg_seq,
													ntile,       // int    dbg_tile,
													indx,        // int    dbg_index,
													debugLevel-3); // int    debug_level)
											target[CuasMotionLMA.RSLT_ITERS] = rslt; // will save -1 (failure) also
											if (rslt >= 0) { // negative amplitude (in both senses)-> failure
												cuasMotionLMA.setResult(target);
												if ((target[CuasMotionLMA.RSLT_A] < 0) || (cuasMotionLMA.getA() < 0)) {
													rslt = -1;
													target[CuasMotionLMA.RSLT_ITERS] = rslt;
												}
											}

											if (rslt >= 0) {
												cuasMotionLMA.setResult(target);
											    if (dbg_img) { // UM-blured-scaled
											        dbg_stack [10] = cuasMotionLMA.getFx();
											        dbg_stack [11] = cuasMotionLMA.getLastYMinusFx();
											        dbg_stack [12] = cuasMotionLMA.getWeights();
											        dbg_titles[10] = "fX_"+cuasMotionLMA.getA()+"-"+cuasMotionLMA.getMaxFx();
											        System.out.println("getACM(): nSeq="+nSeq+", ntile="+ntile+
											        		" indx="+indx+" use_max="+use_max +
											        		" X = "    + target[CuasMotionLMA.RSLT_X] +        
											        		" Y = "    + target[CuasMotionLMA.RSLT_Y] +   
											        		" A = "    + target[CuasMotionLMA.RSLT_A] +   
											        		" R0 = "   + target[CuasMotionLMA.RSLT_R0] +  
											        		" R1 = "   + target[CuasMotionLMA.RSLT_R1] +  
											        		" K = "    + target[CuasMotionLMA.RSLT_K] +   
											        		" C = "    + target[CuasMotionLMA.RSLT_C] +   
											        		" RMS = "  + target[CuasMotionLMA.RSLT_RMS] + 
											        		" RMS_A = "+ target[CuasMotionLMA.RSLT_RMS_A] +
											        		" MAX2A = "+ target[CuasMotionLMA.RSLT_MAX2A] +
											        		" ITERS = "+ target[CuasMotionLMA.RSLT_ITERS]); 
											    }
												int col = GPUTileProcessor.DTT_SIZE + (int) Math.round(target[CuasMotionLMA.RSLT_X]);
												int row = GPUTileProcessor.DTT_SIZE + (int) Math.round(target[CuasMotionLMA.RSLT_Y]);
												int pindx = col+row*tile2;
												if (!tn.isInside(pindx)) {
													target[CuasMotionLMA.RSLT_ITERS] = -2; // outside of the 16x16 tiles -> LMA failed to converge
													target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_LMA;
													if (when >=0) {
														target[CuasMotionLMA.RSLT_WHEN] = when;
													}
												} else if (disabled[pindx]) {
													target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_HORIZON;
													if (when >=0) {
														target[CuasMotionLMA.RSLT_WHEN] = when;
													}
													// make it only if there are more than (1? good ones)
													// TODO: Improve. Apply filter after sortig results
												} else if ((target[CuasMotionLMA.RSLT_A] < pre_min_a) && (targets_list.size() >= min_keep)) {
													target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_A_PRE;
													if (when >=0) {
														target[CuasMotionLMA.RSLT_WHEN] = when;
													}
												} 
												if (centered && !Double.isNaN(probed_sequence[nSeq][ntile][CuasMotionLMA.RSLT_X])) {
													double x0 = probed_sequence[nSeq][ntile][CuasMotionLMA.RSLT_X];
													double y0 = probed_sequence[nSeq][ntile][CuasMotionLMA.RSLT_Y];
													target[CuasMotionLMA.RSLT_CENT_X] += x0;
													target[CuasMotionLMA.RSLT_CENT_Y] += y0;
													target[CuasMotionLMA.RSLT_X] +=      x0;
													target[CuasMotionLMA.RSLT_Y] +=      y0;
												}
												if ((ntile == dbg_tile) && (nSeq == dbg_seq)) { // if ((nSeq==dbg_seq) && (ntile == dbg_tile)) {
													// px0, py0 - top/left corner of 16x16
													int ipx = (int) (px0 + GPUTileProcessor.DTT_SIZE + target[CuasMotionLMA.RSLT_X]);
													int ipy = (int) (py0 + GPUTileProcessor.DTT_SIZE + target[CuasMotionLMA.RSLT_Y]);
													int mask_x = Math.min(width-1,  Math.max(0, ipx));
													int mask_y = Math.min(height-1, Math.max(0, ipy));
													double mask_val = sky_mask[mask_x + mask_y * width];
													System.out.println("getAccumulatedCoordinatesMulti(): nSeq="+nSeq+", ntile="+ntile+
															", centered="+centered+", pindx="+pindx+
															", ipx="+ipx+", ipy="+ipy+
															", mask_x="+mask_x + ", mask_y="+mask_y+", mask_val = "+mask_val);
													System.out.println();
													for (int prow = 0; prow  < tile2; prow++) {
														for (int pcol=0; pcol<tile2; pcol++) {
															System.out.print(disabled[pcol+prow*tile2]?".":"+");
														}
														System.out.println();
													}

												}
												// show image for each lma run
												if (dbg_img) {
													ImagePlus imp_dbg = ShowDoubleFloatArrays.makeArrays(
															dbg_stack, // float[][] pixels,
															tile2, // int width,
															tile2, // int height,
															parentCLT.getImageName()+"-nSeq"+nSeq+"-tile"+ntile+"-indx"+indx+"-max"+use_max+"-cent_"+centered,					
															dbg_titles); //String [] titles)
													parentCLT.saveImagePlusInModelDirectory(imp_dbg);
												}
											} else { // if (rslt >= 0) {
												target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_LMA;
												if (when >=0) {
													target[CuasMotionLMA.RSLT_WHEN] = when;
												}
											}
											target[CuasMotionLMA.RSLT_CENTERED] = centered? 1.0 :0.0;
											if (keep_failed || Double.isNaN(target[CuasMotionLMA.RSLT_FAIL]) || (target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE )) {
												targets[indx] = target;
												targets_list.add(indx);
											}// keep_failed
										} // if (mv != null) {

									} // for (int indx = 0; indx <  max_candidate.size(); indx++) {
									// sort targets_list, descending LMA amplitude, failed after not failed
									// Sort descending values at maximal pixels
									Collections.sort(targets_list, new Comparator<Integer>() {
										@Override
										public int compare(Integer lhs, Integer rhs) { // descending
											if ((targets[lhs][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) && (targets[rhs][CuasMotionLMA.RSLT_FAIL] != CuasMotionLMA.FAIL_NONE)) {
												return -1;
											} else  if ((targets[lhs][CuasMotionLMA.RSLT_FAIL] != CuasMotionLMA.FAIL_NONE) && (targets[rhs][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) {
												return 1;
											}
											return (targets[lhs][CuasMotionLMA.RSLT_A]> targets[rhs][CuasMotionLMA.RSLT_A]) ? 
													-1 : ((targets[lhs][CuasMotionLMA.RSLT_A] < targets[rhs][CuasMotionLMA.RSLT_A]) ? 1 : 0);
										}
									});
									if (ntile == dbg_tile) {
										System.out.println("getAccumulatedCoordinatesMulti(): nSeq="+nSeq+", ntile="+ntile+", candidates:"+max_candidate.size()+", filtered:"+targets_list.size());
									}

									// Remove weaker duplicates if they converge to (almost) the same LMA x,y starting from the different pixels. 
									for (int indx2 = 1; indx2 < targets_list.size(); indx2++) {
										double tx2 = targets[targets_list.get(indx2)][CuasMotionLMA.RSLT_X];
										double ty2 = targets[targets_list.get(indx2)][CuasMotionLMA.RSLT_Y];
										for (int indx1 = 0; indx1 < indx2; indx1++) {
											double tx1 = targets[targets_list.get(indx1)][CuasMotionLMA.RSLT_X];
											double ty1 = targets[targets_list.get(indx1)][CuasMotionLMA.RSLT_Y];
											double dtx = tx2-tx1;
											double dty = ty2-ty1;
											double dt2 = dtx*dtx+dty*dty;
											if (dt2 < pix_tolerance2) {// indx2 is a duplicate, remove its reference from the list 
												targets_list.remove(indx2--); // indx2 will be incremented again at the end of the loop
												break; // inner cycle, continue with the for (int indx2 = 1; indx2 < targets_list.size(); indx2++)  cycle
											}
										}
									}

									// if too many - discard
									// Remove weaker duplicates if they converge to (almost) the same LMA x,y starting from the different pixels. 
									double [][] targets_sorted = new double [Math.min(targets_list.size(), MAX_ALT_TARGETS)][];
									for (int i = 0; i < targets_sorted.length; i++) {
										targets_sorted[i] = targets[targets_list.get(i)];
									}
									targets_new[nSeq][ntile] = targets_sorted; // will always put something, maybe just []
								}  else {// if (!max_candidate.isEmpty()) {
									targets_new[nSeq][ntile] = new double[0][];
								}// if (!max_candidate.isEmpty())  else
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return targets_new;
	}
	
	
	
	
	public static double [][][][] convertToMultiTarget(
			final double [][][] target_sequence) {
		final int num_seq = target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final double [][][][] target_multi = new double[num_seq][num_tiles][][];
		
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) if (target_sequence[nSeq][ntile] != null) {
							target_multi[nSeq][ntile] = new double[][] {target_sequence[nSeq][ntile]};
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return target_multi;
	}
	
	public static double [][][] convertFromMultiTarget(
			final double [][][][] target_multi){
		return convertFromMultiTarget(// single target per tile
				target_multi,  // final double [][][][] target_multi) {
				0,              //final double          minimal_score) {
				0,              // final int             min_seq) { // minimal sequence length
				0,              // final int             enough_seq, // good regardless of scores
				0,              // final double          seq_travel,
				null);          // final int []          remain){

		
	}
	
	public static double [][][] convertFromMultiTarget(
			final double [][][][] target_multi,
			final double          minimal_score,
			final int             min_seq, // minimal sequence length
			final int             enough_seq, // good regardless of scores
			final double          seq_travel,
			final int []          remain){
		final int num_seq = target_multi.length;
		final int num_tiles = target_multi[0].length;
		final double [][][] target_sequence = new double[num_seq][num_tiles][];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		System.out.println("convertFromMultiTarget(): minimal_score="+minimal_score+", min_seq="+min_seq+", enough_seq="+enough_seq);

		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						if (remain != null) {
							remain[nSeq] = 0;
						}
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = target_multi[nSeq][ntile];
							if ((targets != null) && (targets.length > 0)) {
								if (targets[0][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) {
									if (targets[0][CuasMotionLMA.RSLT_SEQ_TRAVEL] >= seq_travel) {
										if (targets[0][CuasMotionLMA.RSLT_MATCH_LENGTH] >=(enough_seq-1)) {
											target_sequence[nSeq][ntile] = targets[0];
											if (remain != null) {
												remain[nSeq]++;
											}
										} else if ((targets[0][CuasMotionLMA.RSLT_QSCORE] >= minimal_score) && (targets[0][CuasMotionLMA.RSLT_MATCH_LENGTH] >= (min_seq-1))) {
											target_sequence[nSeq][ntile] = targets[0];
											if (remain != null) {
												remain[nSeq]++;
											}
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return target_sequence;
	}

	
	public static void sortMultiTargets(
			final double [][][][] target_multi,
			final boolean         trim_nulls) { // trim null targets
		final int num_seq = target_multi.length;
		final int num_tiles = target_multi[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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = target_multi[nSeq][ntile];
							if ((targets != null) && (targets.length > 0)) { //&& (target_multi.length > 1)) {
								if (target_multi.length > 1) {
									Integer [] indices = new Integer[targets.length];
									for (int i = 0; i < indices.length; i++) {
										indices[i] = i;
									}
									Arrays.sort(indices, new Comparator<Integer>() { // decreasing weight
										@Override
										public int compare(Integer lhs, Integer rhs) { // descending score
											if      ((targets[lhs] != null) && (targets[rhs] == null)) return -1;
											else if ((targets[lhs] == null) && (targets[rhs] != null)) return  1;
											else if ((targets[lhs] == null) && (targets[rhs] == null)) return  0;
											// removed used from failed - they are ordered separately, all3 variants (centered, uncentered, used
											boolean fail_lhs = (targets[lhs] == null) ||
													(targets[lhs][CuasMotionLMA.RSLT_FAIL] != CuasMotionLMA.FAIL_NONE);
	//												 || (targets[lhs][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_USED); 
											boolean fail_rhs = (targets[rhs] == null) ||
													(targets[rhs][CuasMotionLMA.RSLT_FAIL] != CuasMotionLMA.FAIL_NONE);
//													 || (targets[rhs][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_USED); 
											if (!fail_lhs && fail_rhs) {
												return -1;
											} else if (fail_lhs && !fail_rhs) {
												return 1;
											}
											// centered are better than uncentered (before score) and used are the worst
											if (targets[lhs][CuasMotionLMA.RSLT_CENTERED] > targets[rhs][CuasMotionLMA.RSLT_CENTERED]) {
												return -1;
											} else if (targets[lhs][CuasMotionLMA.RSLT_CENTERED] < targets[rhs][CuasMotionLMA.RSLT_CENTERED]) {
												return 1;
											}
											return (targets[lhs][CuasMotionLMA.RSLT_QSCORE]> targets[rhs][CuasMotionLMA.RSLT_QSCORE]) ? 
													-1 : ((targets[lhs][CuasMotionLMA.RSLT_QSCORE] < targets[rhs][CuasMotionLMA.RSLT_QSCORE]) ? 1 : 0);
										}
									});
									double [][]  targets_orig = targets.clone(); // shallow
									for (int i = 0; i < targets.length; i++) {
										targets[i] = targets_orig[indices[i]];
									}
								}
								if (trim_nulls && (targets.length > 0) && (targets[targets.length-1] == null)) {
									int indx_null;
									for (indx_null = targets.length-1; (indx_null >=0) && (targets[indx_null] == null); indx_null--);
									double [][] targets_trimmed = new double[indx_null + 1][];
									for (int i = 0; i < targets_trimmed.length; i++) {
										targets_trimmed[i] = targets[i];
									}
									target_multi[nSeq][ntile] = targets_trimmed;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}

	public static int [] removeWeakerDuplicates( // needs sortMultiTargets and after
			final double [][][][] target_multi,
			final double          pix_tolerance) {
		final double pix_tolerance2 = pix_tolerance * pix_tolerance;
		final int num_seq = target_multi.length;
		final int num_tiles = target_multi[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger adup = new AtomicInteger(0);
		final AtomicInteger adup_good = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = target_multi[nSeq][ntile];
							if (targets != null) {
								for (int indx2 = 1; indx2 < targets.length; indx2++) if (targets[indx2] != null) {
									double tx2 = targets[indx2][CuasMotionLMA.RSLT_X];
									double ty2 = targets[indx2][CuasMotionLMA.RSLT_Y];
									for (int indx1 = 0; indx1 < indx2; indx1++) if (targets[indx1] != null){
										double tx1 = targets[indx1][CuasMotionLMA.RSLT_X];
										double ty1 = targets[indx1][CuasMotionLMA.RSLT_Y];
										double dtx = tx2-tx1;
										double dty = ty2-ty1;
										double dt2 = dtx*dtx+dty*dty;
										if (dt2 < pix_tolerance2) {// indx2 is a duplicate, remove its reference from the list
											if ((targets[indx2][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
													(targets[indx2][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) {
												adup_good.getAndIncrement();
											}
											targets[indx2] = null;
											adup.getAndIncrement();
											break; // for (int indx1 = 0; indx1 < indx2; indx1++) if (targets[indx1] != null){
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return new int[] {adup.get(), adup_good.get()};
	}
	
	
	
	
	
	
	/**
	 * Calculate image x,y for each target half keyframe interval before the keyframe and half-interval after.
	 * The results are stored in yet empty fields of the target data 
	 * It is used to filter out stray targets, these coordinates for the consecutive frames should be close:
	 * "after" for the earlier keyframe and "before" for the later one.
	 * @param targets [keyframe_number][tile][ntarget][] target positions of the targets relative to the tile center and motion vectors
	 * @param good_only exclude failed tiles
	 * @param interseq_scale multiply velocity vector to get pixel offset in the middle between the two keyframes
	 * @param tilesX number of tiles in a row   
	 * @param debugLevel  debug level
	 */
	public static void getHalfBeforeAfterPixXY(
			final double [][][][] targets_multi,
			final boolean         good_only,
			final double          interseq_scale,
			final int             tilesX,
			final int             debugLevel) {
		final int num_seq =   targets_multi.length;
		final int num_tiles = targets_multi[0].length;
		final int tileSize = GPUTileProcessor.DTT_SIZE;
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = targets_multi[nSeq][ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if (target != null) {
										if (!good_only || (target[CuasMotionLMA.RSLT_FAIL] == 0)) {
											int tileX = ntile % tilesX;
											int tileY = ntile / tilesX;
											double xc = tileSize * tileX + tileSize/2;  
											double yc = tileSize * tileY + tileSize/2;
											double xtk = xc + target[CuasMotionLMA.RSLT_X];  
											double ytk = yc + target[CuasMotionLMA.RSLT_Y];
											double dx = target[CuasMotionLMA.RSLT_VX] * interseq_scale;
											double dy = target[CuasMotionLMA.RSLT_VY] * interseq_scale;
											target[CuasMotionLMA.RSLT_BX] = xtk - dx; // before, x
											target[CuasMotionLMA.RSLT_BY] = ytk - dy; // before, y
											target[CuasMotionLMA.RSLT_AX] = xtk + dx; // after, x
											target[CuasMotionLMA.RSLT_AY] = ytk + dy; // after, y
											//									pix_xy[nSeq][ntile] = baxy;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	/*
	 * Needs RSLT_BX, RSLT_BY, RSLT_AX, RSLT_AY to be known
	 */
	public static int [][][] calcMatchingTargetsLengths( // calculate number of consecutive keyframes connected to each target
			final double [][][][] targets_multi,
			final boolean         calc_linked, // needed to calculate indices in the order of lengths. But only when there are few targets
			final double          max_mismatch,     // if <=0, do not calculate  mismatch_ba and filter
			final double          slow_fast_mismatch,
			final double          match_len_pwr, // 0.5; // raise matching length to this power for calculating score
			final double          seq_travel,
			final int             tilesX) {
		final double          good_mismatch = max_mismatch;
		final double max_mismatch2 = max_mismatch * max_mismatch; 
		final double slow_fast_mismatch2 = slow_fast_mismatch * slow_fast_mismatch;
		// Will only consider not-failed targets 
		final int num_seq = targets_multi.length;
		final int num_tiles = targets_multi[0].length;
		final int [][][][] len_ba = new int [2][num_seq][num_tiles][]; // first index is before/after
		final int [][][][] target_grp= new int [2][num_seq][num_tiles][]; // [ba][nseq][ntile][ntarg] target group (connected), separately for each direction
		final double [][][][] best_mismatch_ba = new double [2][num_seq][num_tiles][];
		final double [][][][] best_dirs_ba = new double [2][num_seq][num_tiles][];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai =   new AtomicInteger(0);
		final AtomicInteger agrp = new AtomicInteger(1);

		final int dbg_tile = -(32 + 44 * 80);
		if (dbg_tile >=0) {
			System.out.println("calcMathingTargetsLengths().0 max_mismatch="+max_mismatch+", max_mismatch2="+max_mismatch2);
		}
		int num_grp_before = 0;
		final double [][][][][] bbox_ba = new double [2][num_seq][num_tiles][][]; // minx, miny, maxx, maxy
		for (int aba = 0; aba <=1; aba++) {// first pass - calculating connected keyframes before each current keyframe tile/target alternative
			final int ba = aba;
			for (int nseq = 0; nseq < num_seq; nseq++) {
				final boolean first = (nseq == 0); 
				final int fnseq = (ba == 0) ? nseq : (num_seq - nseq -1) ;
				final int fnseq_other = (ba == 0) ? (nseq - 1) : (num_seq - nseq); // not valid for first == true 
				ai.set(0);
				for (int ithread = 0; ithread < threads.length; ithread++) {
					threads[ithread] = new Thread() {
						public void run() {
							TileNeibs tn = new TileNeibs(tilesX, num_tiles/tilesX); 
							for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile= ai.getAndIncrement()) if (targets_multi[fnseq][nTile] != null){
								double [][] targets =targets_multi[fnseq][nTile];
								int [] lengths = new int [targets.length];
								if (nTile==dbg_tile) {
									System.out.println("calcMathingTargetsLengths().1 ba="+ba+", fnseq="+fnseq+", targets.length="+targets.length);
								}
								bbox_ba[ba][fnseq][nTile] = new double [targets.length][];
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE)){
										double cent_x = 0.5*(target[CuasMotionLMA.RSLT_BX]+target[CuasMotionLMA.RSLT_AX]);
										double cent_y = 0.5*(target[CuasMotionLMA.RSLT_BY]+target[CuasMotionLMA.RSLT_AY]);
										bbox_ba[ba][fnseq][nTile][ntarg] = new double[] {cent_x, cent_y, cent_x, cent_y};
									}	
								}
								target_grp[ba][fnseq][nTile] = new int [targets.length];
								best_mismatch_ba[ba][fnseq][nTile] = new double [targets.length];
								best_dirs_ba[ba][fnseq][nTile] = new double [targets.length];
								Arrays.fill(best_mismatch_ba[ba][fnseq][nTile], Double.POSITIVE_INFINITY);
								if (first) {
									for (int ntarg = 0; ntarg < targets.length; ntarg++) {
										target_grp[ba][fnseq][nTile][ntarg] = agrp.getAndIncrement(); // unique group index
									}									
								} else { // if (!first) {
									for (int ntarg = 0; ntarg < targets.length; ntarg++) {
										double [] target = targets[ntarg];
										if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE)){
											double this_x = target[CuasMotionLMA.RSLT_BX + 2 * ba]; // this target's "before" (when ba=0) or "after" (when ba=1) absolute X 
											double this_y = target[CuasMotionLMA.RSLT_BY + 2 * ba]; // this target's absolute Y
//											double cent_x = 0.5*(target[CuasMotionLMA.RSLT_BX]+target[CuasMotionLMA.RSLT_AX]);
//											double cent_y = 0.5*(target[CuasMotionLMA.RSLT_BY]+target[CuasMotionLMA.RSLT_AY]);
											boolean this_slow = target[CuasMotionLMA.RSLT_SLOW] > 0;
											// compare with every previous (/next) neighbor tiles/targets
											for (int dy = -1; dy <= 1; dy++) {
												for (int dx = -1; dx <= 1; dx++) {
													int ntile_other = tn.getNeibIndex(nTile, dx, dy);
													if (ntile_other >= 0) {
														double [][] targets_other = targets_multi[fnseq_other][ntile_other];
														if (targets_other != null) {
															for (int ntarg_other = 0; ntarg_other < targets_other.length; ntarg_other++) {
																double [] target_other = targets_other[ntarg_other];
																if ((target_other != null) && (target_other[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE)){
																	if (len_ba[ba][fnseq_other][ntile_other][ntarg_other] >= (lengths[ntarg] -1)) { // can potentially improve
																		double other_x =     target_other[CuasMotionLMA.RSLT_BX + 2 * (1 - ba)];  // opposite direction
																		double other_y =     target_other[CuasMotionLMA.RSLT_BY + 2 * (1 - ba)];  // opposite direction
																		boolean other_slow = target_other[CuasMotionLMA.RSLT_SLOW] > 0;
																		double mm2 = (this_slow != other_slow) ? slow_fast_mismatch2 : max_mismatch2;

																		double mismatch_x = other_x - this_x;
																		double mismatch_y = other_y - this_y;
																		double mismatch2 = mismatch_x * mismatch_x + mismatch_y * mismatch_y;
																		if (mismatch2 <= mm2) { // max_mismatch2) {
																			double mismatch = Math.sqrt(mismatch2);
																			int proposed_len = len_ba[ba][fnseq_other][ntile_other][ntarg_other] + 1;
																			if ((proposed_len > lengths[ntarg]) ||
																					((proposed_len == lengths[ntarg]) &&
																							(mismatch < best_mismatch_ba[ba][fnseq][nTile][ntarg]))) {
																				int dir = 8;
																				for (int ndir = 0; ndir < TileNeibs.DIR_XY.length; ndir++) {
																					if ((TileNeibs.DIR_XY[ndir][0] == dx) && (TileNeibs.DIR_XY[ndir][1] == dy)) {
																						dir = ndir;
																						break;
																					}
																				}
																				int [] mm_dirs = {-1, -1};
																				int [] mm_alts = {0, 0};
																				mm_dirs[ba] = dir;
																				mm_alts[ba] = ntarg_other;
																				lengths[ntarg] = proposed_len;
																				target_grp[ba][fnseq][nTile][ntarg] = target_grp[ba][fnseq_other][ntile_other][ntarg_other]; // same group
																				best_mismatch_ba[ba][fnseq][nTile][ntarg] = mismatch;
																				best_dirs_ba[ba][fnseq][nTile][ntarg] = encodeDirs(mm_dirs, mm_alts);
																				// targets are ordered, starting with strongest.
																				// combine bbox-es
																				bbox_ba[ba][fnseq][nTile][ntarg] = getBbox(
																						bbox_ba[ba][fnseq_other][ntile_other][ntarg_other], // double [] bbox1,
																						bbox_ba[ba][fnseq]      [nTile]      [ntarg]); //double [] bbox2);
																				if (nTile==dbg_tile) {
																					System.out.println("calcMathingTargetsLengths().2 ba="+ba+", fnseq = "+fnseq+", nTile="+nTile+", ntarg="+ntarg+
																							", fnseq_other="+fnseq_other+", ntile_other="+ntile_other+", ntarg_other="+ntarg_other+
																							", mismatch2="+mismatch2+
																							", lengths["+ntarg+"]="+lengths[ntarg]+
																							", bbox={"+bbox_ba[ba][fnseq][nTile][ntarg][0]+","+
																							bbox_ba[ba][fnseq][nTile][ntarg][1]+","+
																							bbox_ba[ba][fnseq][nTile][ntarg][2]+","+
																							bbox_ba[ba][fnseq][nTile][ntarg][3]+"}");
																				}
																			}
																		} else {
																			if (nTile==dbg_tile) {
																				System.out.println("calcMathingTargetsLengths().3 ba="+ba+", fnseq = "+fnseq+", nTile="+nTile+", ntarg="+ntarg+
																						", fnseq_other="+fnseq_other+", ntile_other="+ntile_other+", ntarg_other="+ntarg_other+
																						", mismatch2="+mismatch2+
																						", lengths["+ntarg+"]="+lengths[ntarg]+
																						", len_ba["+ba+"]["+fnseq_other+"]["+ntile_other+"]["+ntarg_other+"]=" + len_ba[ba][fnseq_other][ntile_other][ntarg_other]);
																			}
																		}
																	}  else {
																		if (nTile==dbg_tile) {
																			System.out.println("calcMathingTargetsLengths().3b ba="+ba+", fnseq = "+fnseq+", nTile="+nTile+", ntarg="+ntarg+
																					", fnseq_other="+fnseq_other+", ntile_other="+ntile_other+", ntarg_other="+ntarg_other+
																					", lengths["+ntarg+"]="+lengths[ntarg]+" FAILED");
																		}
																		
																	}
																	
																} else {
																	if (nTile==dbg_tile) {
																		System.out.println("calcMathingTargetsLengths().3a ba="+ba+", fnseq = "+fnseq+", nTile="+nTile+", ntarg="+ntarg+
																				", fnseq_other="+fnseq_other+", ntile_other="+ntile_other+", ntarg_other="+ntarg_other+
																				((target_other != null)? (", target_other[CuasMotionLMA.RSLT_FAIL]="+ target_other[CuasMotionLMA.RSLT_FAIL]) :", target_other == null")+
																				", len_ba["+ba+"]["+fnseq_other+"]["+ntile_other+"]["+ntarg_other+"]=" + len_ba[ba][fnseq_other][ntile_other][ntarg_other]+
																				", lengths["+ntarg+"]="+lengths[ntarg]);
																	}
																	
																}
															}	// for (int ntarg_other = 0; ntarg_other < targets_other.length; ntarg_other++) {													
														}
													}
												}
											}
										} // if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE)){
										if (target_grp[ba][fnseq][nTile][ntarg] == 0) {
											target_grp[ba][fnseq][nTile][ntarg] = agrp.getAndIncrement(); // new unique group index 
										}
										
									} // for (int ntarg = 0; ntarg < targets.length; ntarg++) {
								}
								len_ba[ba][fnseq][nTile] = lengths;
							}
						}
					};
				}		      
				ImageDtt.startAndJoin(threads);
			}
			if (ba == 0) {
				num_grp_before = agrp.get();
			}
			System.out.println("calcMatchingTargetsLengths(): ba="+ba+", agrp="+agrp.get());
		}
		/*
//		final ArrayList<ConcurrentLinkedQueue<Integer>> cqueue_list = new ArrayList<ConcurrentLinkedQueue<Integer>>();
//		final ConcurrentLinkedQueue<ConcurrentLinkedQueue<Integer>> cqueue_list = new ConcurrentLinkedQueue<ConcurrentLinkedQueue<Integer>>();
		final ConcurrentLinkedQueue<Integer> [] cqueue_arr = new ConcurrentLinkedQueue<Integer>[num_grp_before];
		for (int bgrp = 1; bgrp < num_grp_before; bgrp++) {
			cqueue_list.add(new ConcurrentLinkedQueue<Integer>());
		}
		*/
		final int len_grp_before = num_grp_before;
		final int len_grp_after = agrp.get()-len_grp_before;
		System.out.println("calcMatchingTargetsLengths(): len_grp_before="+len_grp_before+", len_grp_after="+len_grp_after);
		/*
		 with
		 calcMatchingTargetsLengths(): ba=0, agrp=772980
         calcMatchingTargetsLengths(): ba=1, agrp=1545956
         running with calc_linked== false, got Java heap space (). It could not work (over 4 TB)
 		 */
		final boolean [][] ba_pairs = calc_linked ? (new boolean [len_grp_before -1][len_grp_after]): null;; 
		ai.set(0);
		// combine before/after into a single value
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets =targets_multi[nSeq][ntile];
							if (targets != null){
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) {
										target[CuasMotionLMA.RSLT_BEFORE_LENGTH] = len_ba[0][nSeq][ntile][ntarg];
										target[CuasMotionLMA.RSLT_AFTER_LENGTH] =  len_ba[1][nSeq][ntile][ntarg];
										
										target[CuasMotionLMA.RSLT_MATCH_LENGTH] = 0;
										for (int ba = 0; ba < len_ba.length; ba++) {
											if (len_ba[ba][nSeq][ntile] != null) {
												target[CuasMotionLMA.RSLT_MATCH_LENGTH]+= len_ba[ba][nSeq][ntile][ntarg];
											}
										}
										double [] bbox = getBbox(
									            bbox_ba[0][nSeq][ntile][ntarg], // double [] bbox1,
									            bbox_ba[1][nSeq][ntile][ntarg]); //double [] bbox2);
										target[CuasMotionLMA.RSLT_SEQ_TRAVEL] = Math.sqrt(getDiagonal2(bbox));
										
										// calculate related quality factors (moved from getEffective...()) 
										
										target[CuasMotionLMA.RSLT_QMATCH_LEN] = Math.pow(target[CuasMotionLMA.RSLT_MATCH_LENGTH], match_len_pwr);// 1.0 if in every scene;
										target[CuasMotionLMA.RSLT_QTRAVEL] =    Math.max((target[CuasMotionLMA.RSLT_SEQ_TRAVEL] - seq_travel)/seq_travel, 0);
										double MM_BEFORE = best_mismatch_ba[0][nSeq][ntile][ntarg];
										double MM_AFTER =  best_mismatch_ba[1][nSeq][ntile][ntarg];
										int [] dirs_before = decodeDirs(best_dirs_ba[0][nSeq][ntile][ntarg]);
										int [] dirs_after =  decodeDirs(best_dirs_ba[1][nSeq][ntile][ntarg]);
										target[CuasMotionLMA.RSLT_MISMATCH_BEFORE] = MM_BEFORE;
										target[CuasMotionLMA.RSLT_MISMATCH_AFTER] =  MM_AFTER;
										target[CuasMotionLMA.RSLT_MISMATCH_DIRS] = encodeDirs(
												new int [] {dirs_before[0], dirs_after[1]},
												new int [] {dirs_before[2], dirs_after[3]});
										target[CuasMotionLMA.RSLT_QMATCH] = 0;
										if (MM_BEFORE <= good_mismatch) {
											target[CuasMotionLMA.RSLT_QMATCH] += Math.max(0,(good_mismatch - MM_BEFORE)/good_mismatch); // 0 .. 1 
										}
										if (MM_AFTER <= good_mismatch) {
											target[CuasMotionLMA.RSLT_QMATCH] += Math.max(0,(good_mismatch - MM_AFTER)/good_mismatch); // 0 .. 1
										}
										int grp_before = target_grp[0][nSeq][ntile][ntarg]-1; 
										int grp_after =  target_grp[1][nSeq][ntile][ntarg] - len_grp_before; 
										if (ba_pairs != null) {
											ba_pairs[grp_before][grp_after] = true;
										}
										if (ntile == dbg_tile) {
											System.out.println("calcMathingTargetsLengths().4  nSeq = "+nSeq+", targets["+ntarg+"][CuasMotionLMA.RSLT_MATCH_LENGTH]="+
													target[CuasMotionLMA.RSLT_MATCH_LENGTH]+", travel="+target[CuasMotionLMA.RSLT_SEQ_TRAVEL]);
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if (!calc_linked) {
			return null;
		}
//		final int [][][][] linked_targets = new int [len_grp_before -1][len_grp_after][][]; // [nseq]{ntile,ntarg}
		final int [][] pair_indices = new int [len_grp_before -1][len_grp_after];
		int indx = 0;
		for (int nb = 0; nb < pair_indices.length; nb++) {
			Arrays.fill(pair_indices[nb], -1);
			for (int na = 0; na < pair_indices[nb].length; na++) {
				if (ba_pairs[nb][na]) {
//					linked_targets[nb][na] = new int [num_seq][];
					pair_indices[nb][na] = indx++; 
				}
			}
		}
		final int [][][] linked_targets = new int [indx][num_seq][]; // [nseq]{ntile,ntarg}
		
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets =targets_multi[nSeq][ntile];
							if (targets != null){
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if (target != null) {
										int grp_before = target_grp[0][nSeq][ntile][ntarg]-1; 
										int grp_after =  target_grp[1][nSeq][ntile][ntarg] - len_grp_before; 
										if ((grp_before >= 0) && (grp_after >= 0)) {
											int pair_index = pair_indices[grp_before][grp_after]; // should be >=0
											if (pair_index <0) {
												throw new IllegalArgumentException("calcMatchingTargetsLengths(): BUG: pair_indices["+grp_before+"]["+grp_after+
														"] <0 for nSeq="+ nSeq+", ntile="+ntile+", ntarg="+ntarg);
											}
											linked_targets[pair_index][nSeq] = new int [] {ntile, ntarg}; // null pointer
											target[CuasMotionLMA.RSLT_GLOBAL] = pair_index + 1; // starting with 1
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return linked_targets;
	}

	
	/**
	 * Calculate tiles/alts for each moving target using unique index in CuasMotionLMA.RSLT_GLOBAL field
	 * Creates array of such targets, each having the length of the input targets_multi array. Each element
	 * is either null (this target is not present in this scene sequence) or a pair of {tile, alternative}
	 * if it does. 
	 * @param targets_multi [num_sequences][num_tiles][num_alternatives][CuasMotionLMA.RSLT_LEN] source targets array
	 * @return variable-length (may be empty) arrays following targets
	 */
	public static int [][][] getLinkedTargets(
			final double [][][][] targets_multi){
		final int num_seq = targets_multi.length;
		final int num_tiles = targets_multi[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai =   new AtomicInteger(0);
		final AtomicInteger amax = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets =targets_multi[nSeq][ntile];
							if (targets != null){
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if (target != null) {
										double dindx = target[CuasMotionLMA.RSLT_GLOBAL];
										if (!Double.isNaN(dindx) && (dindx > 0)) {
											int indx = (int) dindx;
											amax.getAndAccumulate(indx, Math::max);
										}
									}									
								}
							}
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		final int num_targets = amax.get();
		final int [][][] linked_targets = new int [num_targets][][]; // [nseq]{ntile,ntarg}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets =targets_multi[nSeq][ntile];
							if (targets != null){
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if (target != null) {
										double dindx = target[CuasMotionLMA.RSLT_GLOBAL];
										if (!Double.isNaN(dindx) && (dindx > 0)) {
											int pair_index = (int) dindx - 1;
											linked_targets[pair_index][nSeq] = new int [] {ntile, ntarg};
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return linked_targets;
	}
	
	/**
	 * Same for single-layer targets (one target per tile)
	 * @param targets_single [num_sequences][num_tiles][CuasMotionLMA.RSLT_LEN] source targets array
	 * @return variable-length (may be empty) arrays following targets
	 */
	public static int [][][] getLinkedTargets(
			final double [][][] targets_single){
		final int num_seq = targets_single.length;
		final int num_tiles = targets_single[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai =   new AtomicInteger(0);
		final AtomicInteger amax = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] target = targets_single[nSeq][ntile];
							if (target != null) {
								double dindx = target[CuasMotionLMA.RSLT_GLOBAL];
								if (!Double.isNaN(dindx) && (dindx > 0)) {
									int indx = (int) dindx;
									amax.getAndAccumulate(indx, Math::max);
								}
							}									
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		final int num_targets = amax.get();
		final int [][][] linked_targets = new int [num_targets][num_seq][]; // [nseq]{ntile,ntarg}
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] target =targets_single[nSeq][ntile];
							if (target != null) {
								double dindx = target[CuasMotionLMA.RSLT_GLOBAL];
								if (!Double.isNaN(dindx) && (dindx > 0)) {
									int pair_index = (int) dindx - 1;
									linked_targets[pair_index][nSeq] = new int [] {ntile, 0};  // ntarg}; // null
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return linked_targets;
	}

	
	
	
	
	
	
	private static double getDiagonal2 (
			double [] bbox) {
		double w = bbox[2] - bbox[0];
		double h = bbox[3] - bbox[1];
		return w*w + h*h;
	}
	
	private static double [] getBbox (
			double [] bbox1,
			double [] bbox2) {
		return new double[] {Math.min(bbox1[0], bbox2[0]),Math.min(bbox1[1], bbox2[1]),Math.max(bbox1[2], bbox2[2]),Math.max(bbox1[3], bbox2[3])};
	}

	
	
	public static ImagePlus showTargetSequence(
			final double [][][][] targets_multi, //
			String []             scene_titles, // all slices*frames titles or just slice titles or null
			String                title,
			final boolean         no_bad_only,
			final boolean         show_empty, // show scenes with no (valid) targets
			boolean               show,
			final int             tilesX) {
		int num_fields = CuasMotionLMA.LMA_TITLES.length;
		int num_seq =    targets_multi.length;
		int num_tiles = targets_multi[0].length;
		final int [] max_num_alts = new int [num_seq];
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						max_num_alts[nSeq] = 0;
						for (int ntile = 0; ntile < num_tiles; ntile++){
							int num_alts = 0;
							double [][] targets = targets_multi[nSeq][ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									if ((targets[ntarg] != null) && (!no_bad_only || Double.isNaN(targets[ntarg][CuasMotionLMA.RSLT_FAIL]) || (targets[ntarg][CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE))) {
										num_alts++;
									}

								}
							}
							if (num_alts > max_num_alts[nSeq]) {
								max_num_alts[nSeq] = num_alts;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		
		int total_slices = 0;
		for (int nseq = 0; nseq < num_seq; nseq++ ) {
			total_slices += max_num_alts[nseq];
			if (show_empty && (max_num_alts[nseq] == 0)) {
				total_slices++;
			}
		}
		String [] titles = new String[total_slices];
		final int [] slice_indices = new int [num_seq]; 
		int slice_index=0;
		for (int nseq = 0; nseq < num_seq; nseq++ ) {
			slice_indices[nseq] = slice_index;
			if (max_num_alts[nseq] > 0) {
				for (int i = 0; i < max_num_alts[nseq]; i++) {
					titles[slice_index++] = scene_titles[nseq]+"-"+nseq+":"+i;
				}
				
			} else if (show_empty) {
				titles[slice_index++] = scene_titles[nseq];
			}
		}		
		String [] frame_titles = new String [CuasMotionLMA.LMA_TITLES.length + 1];
		for (int i = 0; i < CuasMotionLMA.LMA_TITLES.length; i++) {
			frame_titles[i] = CuasMotionLMA.LMA_TITLES[i];
		}
		frame_titles[CuasMotionLMA.LMA_TITLES.length] = "Targets";
		final double [][][] img_data = new double [num_fields+1][total_slices][num_tiles];
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						int nslice = slice_indices[nSeq];
						int num_slices = max_num_alts[nSeq];
						if (show_empty && (num_slices == 0)) {
							num_slices++;
						}
						if (num_slices > 0) {
							for (int nfield = 0; nfield < num_fields; nfield++) {
								for (int num_alt = 0; num_alt < num_slices; num_alt++) {
									Arrays.fill(img_data[nfield][nslice+num_alt], Double.NaN);
								}
							}
							for (int ntile = 0; ntile < num_tiles; ntile++){
								int num_alt = 0;
								double [][] targets = targets_multi[nSeq][ntile];
								if (targets != null) {
									for (int ntarg = 0; ntarg < targets.length; ntarg++) {
										double [] target = targets[ntarg]; 
										if ((target != null) && (!no_bad_only|| Double.isNaN(target[CuasMotionLMA.RSLT_FAIL]) || (target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE))) {
											for (int nfield = 0; nfield < num_fields; nfield++) {
												img_data[nfield][nslice+num_alt][ntile] = target[nfield];
											}
											img_data[num_fields][nslice+num_alt][ntile] = ((target[CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE) &&
													Double.isNaN(target[CuasMotionLMA.RSLT_STRONGER]))? 1:0;
											num_alt++;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				img_data,       // double[][][] pixels, 
				tilesX,         // int          width, 
				title,          // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				titles,         // String []    titles, // all slices*frames titles or just slice titles or null
				frame_titles,   // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				show);          // boolean      show)
		return imp;
	}

	public float [][] temporalUnsharpMask(
			final boolean         batch_mode,
			float [][]            fpixels,
			final int             debugLevel) {
		return 	temporalUnsharpMask(
				clt_parameters, // CLTParameters         clt_parameters,
				batch_mode,     // final boolean         batch_mode,
				parentCLT,      // QuadCLT               parentCLT,          //
				fpixels,        // float [][]            fpixels,
				this, // final CuasMotion      cuasMotion,
				scene_titles, // String []             scene_titles, // recreate slice_titles from scene titles?
				debugLevel); // final int             debugLevel) {
	}	
	
	
	
	public static float [][] temporalUnsharpMask(
			CLTParameters         clt_parameters,
			final boolean         batch_mode,
			QuadCLT               parentCLT,          //
			float [][]            fpixels,
			final CuasMotion      cuasMotion,
			String []             scene_titles, // recreate slice_titles from scene titles?
			final int             debugLevel) {
		int     temporal_um =    clt_parameters.imp.cuas_temporal_um;
		double  tum_threshold =  clt_parameters.imp.cuas_tum_threshold;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
		double [][] target_frac = new double [clt_parameters.imp.cuas_target_frac.length][2];
		double  input_range =    clt_parameters.imp.cuas_input_range; // 5;
		boolean intermed_high =  clt_parameters.imp.cuas_intermed_high;   //true;
   		boolean cuas_gaussian_ra = clt_parameters.imp.cuas_gaussian_ra; // use gaussian temporal Gaussian instead of running average
		for (int i = 0; i < target_frac.length; i++) {
			if (clt_parameters.imp.cuas_target_frac[i].length >= 2) {
				target_frac[i][0] = clt_parameters.imp.cuas_target_frac[i][0];
				target_frac[i][1] = clt_parameters.imp.cuas_target_frac[i][1];
			} else {
				System.out.println("testCuasScanMotion(): wrong format for a pair of strength, fraction values.");
			}
		}
		boolean debug_tum= true; // false;
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null)+(cuasMotion.slow_targets? "-SLOW":"-FAST");
		
		float [][] fpixels_tum = fpixels;
		if (temporal_um > 0) {
			if (intermed_high && debug_tum) {
				ImagePlus imp_src = ShowDoubleFloatArrays.makeArrays(
						fpixels,                     // float[][] pixels,
						cuasMotion.gpu_max_width,        // int width,
						cuasMotion.gpu_max_height,       // int height,
						model_prefix+"-SOURCE", //String title,
						scene_titles); //String [] titles)
				imp_src.getProcessor().setMinAndMax(-input_range/2, input_range/2);
				if (!batch_mode) {
					imp_src.show();
				}		
				parentCLT.saveImagePlusInModelDirectory(imp_src);            // ImagePlus   imp)
			}
			float [][] fpixels_ra;
			if (cuas_gaussian_ra) {
				fpixels_ra= runningGaussian(
						fpixels,                   // final float [][] fpixels,
						temporal_um,               // final int        ra_length,
						cuasMotion.gpu_max_width); // final int        width);
			} else {
				fpixels_ra= runningAverage(
						fpixels,                   // final float [][] fpixels,
						temporal_um,               // final int        ra_length,
						cuasMotion.gpu_max_width); // final int        width);
			}			
			if (intermed_high && debug_tum) {
				ImagePlus imp_src_ra = ShowDoubleFloatArrays.makeArrays(
						fpixels_ra,                     // float[][] pixels,
						cuasMotion.gpu_max_width,        // int width,
						cuasMotion.gpu_max_height,       // int height,
						model_prefix+(cuas_gaussian_ra?"-SOURCE-RG":"-SOURCE-RA")+temporal_um, //String title,
						scene_titles); //String [] titles)
				imp_src_ra.getProcessor().setMinAndMax(-input_range/2, input_range/2);
				if (!batch_mode) {
					imp_src_ra.show();
				}		
				parentCLT.saveImagePlusInModelDirectory(imp_src_ra);            // ImagePlus   imp)
			}
			
			if (tum_threshold > 0) {
				float [][] fpixels_nan = removeOutliers(
						fpixels,                // float [][] fpixels,
						fpixels_ra,             // float [][] fpixels_avg,
						tum_threshold);         // float      threshold)
				if (intermed_high && debug_tum) {
					ImagePlus imp_outliers = ShowDoubleFloatArrays.makeArrays(
							fpixels_nan,                     // float[][] pixels,
							cuasMotion.gpu_max_width,        // int width,
							cuasMotion.gpu_max_height,       // int height,
							model_prefix+"-SOURCE-OUTLIERS", //String title,
							scene_titles); //String [] titles)
					imp_outliers.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_outliers.show();
					}		
					parentCLT.saveImagePlusInModelDirectory(imp_outliers);            // ImagePlus   imp)
				}
				if (cuas_gaussian_ra) {
					fpixels_ra = runningGaussianNaN(
							fpixels_nan,               // final float [][] fpixels,
							temporal_um,               // final int        ra_length,
							cuasMotion.gpu_max_width); // final int        width);
				} else {
					fpixels_ra = runningAverageNaN(
							fpixels_nan,               // final float [][] fpixels,
							temporal_um,               // final int        ra_length,
							cuasMotion.gpu_max_width); // final int        width);
				}
				if (intermed_high && debug_tum) {
					ImagePlus imp_src_ra = ShowDoubleFloatArrays.makeArrays(
							fpixels_ra,                     // float[][] pixels,
							cuasMotion.gpu_max_width,        // int width,
							cuasMotion.gpu_max_height,       // int height,
							model_prefix+(cuas_gaussian_ra?"-SOURCE-RG":"-SOURCE-RA")+"-NOOUTLIERS"+temporal_um, //String title,
							scene_titles); //String [] titles)
					imp_src_ra.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_src_ra.show();
					}		
					parentCLT.saveImagePlusInModelDirectory(imp_src_ra);            // ImagePlus   imp)
				}

				// modify original fpixels
				fpixels_tum =  subtract(
						fpixels, // float [][] fpixels,
						fpixels_ra); // float [][] fpixels_avg)
				if (intermed_high) {
					ImagePlus imp_diff = ShowDoubleFloatArrays.makeArrays(
							fpixels_tum,                     // float[][] pixels,
							cuasMotion.gpu_max_width,        // int width,
							cuasMotion.gpu_max_height,       // int height,
							model_prefix+"-FPIXELS_TUM"+temporal_um, //String title,
							scene_titles); //String [] titles)
					imp_diff.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_diff.show();
					}		
					parentCLT.saveImagePlusInModelDirectory(imp_diff);            // ImagePlus   imp)
				}
			}
		}
		return fpixels_tum;
	}
	

	public double [][][] prepareMotionBasedSequence(
			final boolean         batch_mode,
			float [][]            fpixels_tum,
			boolean               slow_mode,
			int                   precorr_ra,
			final int             debugLevel) {
		return prepareMotionBasedSequence(
				clt_parameters,   // CLTParameters         clt_parameters,
				batch_mode,       // final boolean         batch_mode,
				parentCLT,        // QuadCLT               parentCLT,          //
				fpixels_tum,      // float [][]            fpixels_tum,
				slow_mode,        // boolean               slow_mode,
				precorr_ra,       // int                   precorr_ra,
				this,             // final CuasMotion      cuasMotion,
				scene_titles,     // String []             scene_titles, // recreate slice_titles from scene titles?
				slice_titles,     // String []             slice_titles,
				debugLevel);      // final int             debugLevel)
	}	
	
	public static double [][][] prepareMotionBasedSequence(
			CLTParameters         clt_parameters,
			final boolean         batch_mode,
			QuadCLT               parentCLT,          //
			float [][]            fpixels_tum,
			boolean               slow_mode,
			int                   precorr_ra,
			final CuasMotion      cuasMotion,
			String []             scene_titles, // recreate slice_titles from scene titles?
			String []             slice_titles,
			final int             debugLevel) {
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset; // 5 (20)
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs; // 20 (50)
		
		double  boost_pairs =    slow_mode? 1.0: clt_parameters.imp.cuas_boost_slow; //  4.0;   // if >1 and the motion vector is below tile_size/cuas_boost_slow, scale corr_pairs
		double  boost_mv_lim =   clt_parameters.imp.cuas_boost_mv_lim; // 0.2; // Do not boost if motion vector faster than this (before in code equiv 1.8 pix)  
		double  boost_mstr =     clt_parameters.imp.cuas_boost_mstr; // 0.007; // scale corr_pairs if motion strength is below  
		double  boost_xstr =     clt_parameters.imp.cuas_boost_xstr; // 0.007; // scale corr_pairs if motion strength is below  
		int     corr_ra_step =   clt_parameters.imp.cuas_corr_step;  //  2 ( 2)
		double  fat_zero =       clt_parameters.imp.cuas_fat_zero;
		double  cent_radius =    clt_parameters.imp.cuas_cent_radius;
		int     n_recenter =     clt_parameters.imp.cuas_n_recenter;
		double  rstr =           clt_parameters.imp.cuas_rstr;// 0.003; //  clt_parameters.imp.rln_sngl_rstr; // FIXME: ADD
		boolean smooth =         clt_parameters.imp.cuas_smooth; // true;
		boolean half_step =      clt_parameters.imp.cuas_half_step; // true;
		double [][] target_frac = new double [clt_parameters.imp.cuas_target_frac.length][2];
	    double  noisemax8 =      clt_parameters. imp.cuas_noisemax8;	    
	    double  target_horizon=  clt_parameters.imp.cuas_horizon;	    
		double  input_range =    clt_parameters.imp.cuas_input_range; // 5;
		boolean corr2d_save_show =  clt_parameters.imp.cuas_2d_save_show;    //true;
		boolean intermed_low =   clt_parameters.imp.cuas_intermed_low;    //true;
		boolean intermed_high =  clt_parameters.imp.cuas_intermed_high;   //true;
   		boolean cuas_gaussian_ra = clt_parameters.imp.cuas_gaussian_ra; // use gaussian temporal Gaussian instead of running average
   		
   		// slow_mode
	    int     slow_scale_samples = slow_mode ? clt_parameters.imp.cuas_mul_samples : 1;  // multiply number of samples
	    int     slow_scale_offset =  slow_mode ? clt_parameters.imp.cuas_mul_offset :  1;  // multiply offset
	    
	    
		for (int i = 0; i < target_frac.length; i++) {
			if (clt_parameters.imp.cuas_target_frac[i].length >= 2) {
				target_frac[i][0] = clt_parameters.imp.cuas_target_frac[i][0];
				target_frac[i][1] = clt_parameters.imp.cuas_target_frac[i][1];
			} else {
				System.out.println("testCuasScanMotion(): wrong format for a pair of strength, fraction values.");
			}
		}
		int start_frame = 0;
		int num_scenes = fpixels_tum.length; // .length; // .getStack().getSize()- first_corr; // includes average
		int seq_length = corr_offset + corr_pairs;
		int corr_inc = half_step ? (corr_offset/2) : corr_offset;
		int num_corr_samples = (num_scenes - seq_length - start_frame) / corr_inc;
		int [] num_all =        new int [num_corr_samples];
		int [] num_undef =      new int [num_corr_samples];
		int [] num_good =       new int [num_corr_samples];
		int [] num_bad =        new int [num_corr_samples];
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null)+(cuasMotion.slow_targets? "-SLOW":"-FAST");

		if (intermed_high) {
			ImagePlus imp_tum = ShowDoubleFloatArrays.makeArrays(
					fpixels_tum, // float[][] pixels,
					cuasMotion.gpu_max_width, // int width,
					cuasMotion.gpu_max_height, // int height,
					model_prefix+"-SOURCE", //String title,
					scene_titles); //String [] titles)
			imp_tum.getProcessor().setMinAndMax(-input_range/2, input_range/2);
			if (!batch_mode) {
				imp_tum.show();
			}		
			parentCLT.saveImagePlusInModelDirectory(imp_tum);            // ImagePlus   imp)
		}
		
		
		float [][] fpixels_ra = fpixels_tum; //fpixels;
		if (precorr_ra > 1) {
			if (cuas_gaussian_ra) {
				fpixels_ra = runningGaussian(
						fpixels_tum, // fpixels,                   // final float [][] fpixels,
						precorr_ra,                // final int        ra_length,
						cuasMotion.gpu_max_width); // final int        width)
			} else {
				fpixels_ra = runningAverage(
						fpixels_tum, // fpixels,                   // final float [][] fpixels,
						precorr_ra,                // final int        ra_length,
						cuasMotion.gpu_max_width); // final int        width)
			}
			if (intermed_high) {
				ImagePlus imp_ra = ShowDoubleFloatArrays.makeArrays(
						fpixels_ra, // float[][] pixels,
						cuasMotion.gpu_max_width, // int width,
						cuasMotion.gpu_max_height, // int height,
						model_prefix+(cuas_gaussian_ra?"-SOURCE-RG":"-SOURCE-RA")+precorr_ra, //String title,
						scene_titles); //String [] titles)
				imp_ra.getProcessor().setMinAndMax(-input_range/2, input_range/2);
				if (!batch_mode) {
					imp_ra.show();
				}		
				parentCLT.saveImagePlusInModelDirectory(imp_ra);            // ImagePlus   imp)
			}
		}
		double [][][] corr2d = corr2d_save_show? new double [num_corr_samples][][] : null;
		double [][][] corr2d_boosted = (corr2d_save_show && (boost_pairs > 1.0))? new double [num_corr_samples][][] : null;

		double [][][] target_sequence = 	getTargetsFromCorr2d(
				clt_parameters,     // CLTParameters     clt_parameters,
				batch_mode,         // false,     // boolean           batch_mode,
				cuasMotion,         // CuasMotion        cuasMotion,
				fpixels_ra,         // float [][]        fpixels,
				start_frame,        // int               start_frame,
				corr_pairs,         // int               corr_pairs,
				1.0,                // double            boost_pairs, // no boost
				corr_offset,        // int               corr_offset,
				corr_inc,           // int               corr_inc,
				corr_ra_step,       // int               corr_ra_step, //    5;      // correlation step when using rolling average
				slow_scale_samples, // int               slow_scale_samples,  // multiply number of samples
				slow_scale_offset,  // int               slow_scale_offset,   // multiply offset
				smooth,             // boolean           smooth,
				fat_zero,           // double            fat_zero,
				cent_radius,        // double            cent_radius,
				n_recenter,         // int               n_recenter,
				-rstr,              // double            rstr,
				model_prefix,       // String            title,
				corr2d,             // double [][][]     corr2d, // null or [(fpixels.length - seq_length - start_frame) / corr_inc)[][]
				debugLevel);        // int               debugLevel) {
		double [][][] target_sequence_boost = null;
		// TODO: only boost tiles if they are week or slow, replace in-place
		if (boost_pairs > 1.0) {
			target_sequence_boost = 	getTargetsFromCorr2d(
					clt_parameters,     // CLTParameters     clt_parameters,
					batch_mode,         // false,     // boolean           batch_mode,
					cuasMotion,         // CuasMotion        cuasMotion,
					fpixels_ra,         // float [][]        fpixels,
					start_frame,        // int               start_frame,
					corr_pairs,         // int               corr_pairs,
					boost_pairs,                // double            boost_pairs, // no boost
					corr_offset,        // int               corr_offset,
					corr_inc,           // int               corr_inc,
					corr_ra_step,       // int               corr_ra_step, //    5;      // correlation step when using rolling average
					slow_scale_samples, // int               slow_scale_samples,  // multiply number of samples
					slow_scale_offset,  // int               slow_scale_offset,   // multiply offset
					smooth,             // boolean           smooth,
					fat_zero,           // double            fat_zero,
					cent_radius,        // double            cent_radius,
					n_recenter,         // int               n_recenter,
					-rstr,              // double            rstr,
					model_prefix,       // String            title,
					corr2d_boosted,             // double [][][]     corr2d, // null or [(fpixels.length - seq_length - start_frame) / corr_inc)[][]
					debugLevel);        // int               debugLevel) {
			if (intermed_low ) {
				ImagePlus imp_initial = showTargetSequence(
						target_sequence,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-INITIAL-MOTION-FAST",// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_initial);
				ImagePlus imp_initial_boosted = showTargetSequence(
						target_sequence_boost,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-INITIAL-MOTION-BOOSTED"+boost_pairs,// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_initial_boosted);
			}
// combine motions			
			combineSlowBoosted(
					target_sequence,       // final double [][][] target_sequence,
					target_sequence_boost, // final double [][][] target_boosted,
					boost_pairs,           // final double        boost_slow, // 4.0;
					boost_mv_lim,          // final double        boost_mv_lim, 1.5-1.8        
					boost_mstr,            // final double        boost_mstr,
					boost_xstr,            // final double        boost_xstr, // 0.2 
					debugLevel);           // final int           debugLevel)

			if (corr2d_save_show) { // may be read to re-process with vector field
				int corr_size = 2 * GPUTileProcessor.DTT_SIZE -1; 
				double [][] dbg_2d_corrs_boosted = ImageDtt.corr_partial_dbg( // not used in lwir
						corr2d_boosted,                       // final double [][][]     corr_data,       // [layer][tile][(2*transform_size-1)*(2*transform_size-1)] // if null - will not calculate
						cuasMotion.tilesX,                              // final int               tilesX,
						corr_size,                           //final int               corr_size,       // 15
						clt_parameters.corr_border_contrast, // final double            border_contrast,
						debugLevel);                         // final int               globalDebugLevel)

				ImagePlus imp_corr2d = ShowDoubleFloatArrays.makeArrays(
						dbg_2d_corrs_boosted, // double[][] pixels,
						cuasMotion.tilesX * (corr_size + 1),
						cuasMotion.tilesY * (corr_size + 1),
						model_prefix+"-CORR2D-BOOSTED"+boost_pairs, // -OFFS"+corr_offset+"-PAIRS"+corr_pairs,   // String title,
						slice_titles);
				if (!batch_mode) {
					imp_corr2d.show();
				}
				parentCLT.saveImagePlusInModelDirectory(imp_corr2d);  // ImagePlus   imp)
			}
		}

		setBadHorizon(
				target_sequence,           // final double [][][] target_sequence,
				target_horizon,            // final double        horizon_y,
				cuasMotion.getNoiseMap8(), // final double []     noise8,    // null - do not filter by noise
				noisemax8,                 //  final double        noise_max,
				cuasMotion.tilesX);   // final int           tilesX)
		// After this target_sequence should not be null-ed, failure status should only change from NaN to 0/>0 values 
		
		if (intermed_low ) {
			ImagePlus imp_initial = showTargetSequence(
					target_sequence,          // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-INITIAL-MOTION",// String        title,
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_initial);
		}			

		if (corr2d_save_show) { // may be read to re-process with vector field
			int corr_size = 2 * GPUTileProcessor.DTT_SIZE -1; 
			double [][] dbg_2d_corrs = ImageDtt.corr_partial_dbg( // not used in lwir
					corr2d,                       // final double [][][]     corr_data,       // [layer][tile][(2*transform_size-1)*(2*transform_size-1)] // if null - will not calculate
					cuasMotion.tilesX,                              // final int               tilesX,
					corr_size,                           //final int               corr_size,       // 15
					clt_parameters.corr_border_contrast, // final double            border_contrast,
					debugLevel);                         // final int               globalDebugLevel)

			ImagePlus imp_corr2d = ShowDoubleFloatArrays.makeArrays(
					dbg_2d_corrs, // double[][] pixels,
					cuasMotion.tilesX * (corr_size + 1),
					cuasMotion.tilesY * (corr_size + 1),
					model_prefix+"-CORR2D", // -OFFS"+corr_offset+"-PAIRS"+corr_pairs,   // String title,
					slice_titles);
			if (!batch_mode) {
				imp_corr2d.show();
			}
			parentCLT.saveImagePlusInModelDirectory(imp_corr2d);  // ImagePlus   imp)
		}
		if (debugLevel > -4) {
			System.out.println("scan DONE");
		}

		// starting from the motion_scan[][][] - calculated or read from file. corr2d is curently unused, maybe we'll have to save restore
		getRemain(target_sequence, num_all, num_undef, num_good, num_bad);
		printStats ("initial", true,num_all, num_undef, null, null);
		if (intermed_low) {
				ImagePlus imp_sky_mask = ShowDoubleFloatArrays.makeArrays(
						cuasMotion.sky_mask, // double[][] pixels,
 						cuasMotion.gpu_max_width,
						cuasMotion.gpu_max_height, 
						model_prefix+"-SKY_MASK");   // Selected to test 
				if (!batch_mode) {
					imp_sky_mask.show();
				}
				parentCLT.saveImagePlusInModelDirectory(imp_sky_mask);  // ImagePlus   imp)
		}
		
		return target_sequence;
	}
	
	
	/**
	 * Combine motion vectors for fast targets calculated for the specified number of correlation pairs
	 * and "slow-boosted" - motion vectors calculated for scaled (>1) number of correlation pairs.
	 * TODO: boost on tile basis, only calculate boosted if tile matches offset and/or strength 
	 * @param target_sequence target sequence with only motion vectors defined for specified number of correlation pairs
	 * @param target_boosted same for increased number of correlation
	 * @param boost_slow use boosted if motion vector is smaller than this
	 * @param boost_mv_lim Do not boost if faster  
	 * @param boost_mstr use boosted if motion vector strength is smaller than this
	 * @param boost_xstr never boost if stronger  
	 * @param debugLevel debug level
	 */
	public static void combineSlowBoosted(
			 final double [][][] target_sequence,
			 final double [][][] target_boosted,
			 final double        boost_slow, // 4.0;
			 final double        boost_mv_lim,
			 final double        boost_mstr,
			 final double        boost_xstr, 
			 final int           debugLevel) {
		if (boost_slow >1) {
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
//			final double max_v = 0.9 * GPUTileProcessor.DTT_SIZE / boost_slow;
			final double max_v2 = boost_mv_lim * boost_mv_lim; // max_v * max_v;
			if (debugLevel > -4) {
				System.out.println("combineSlowBoosted(): boost_slow="+boost_slow+", boost_mstr="+boost_mstr+", max_v2="+max_v2);
			}
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nSeq = ai.getAndIncrement(); nSeq < target_sequence.length; nSeq = ai.getAndIncrement()){
							double [][] target =   target_sequence[nSeq];
							double [][] target_b = target_boosted[nSeq];
							if ((target != null) && (target_b != null)) {
								for (int ntile=0; ntile < target.length; ntile++) if (target_b[ntile] != null) {
									if (target[ntile] == null) {
										target[ntile] = target_b[ntile];
									} else {
										boolean replace = false;
										if (!(target[ntile][CuasMotionLMA.RSLT_VSTR] >= boost_mstr)) { // boost all weak MV
											replace = true;
										} else if (!(target[ntile][CuasMotionLMA.RSLT_VSTR] > boost_xstr)){ // do not boost strong MV
											double vx = target[ntile][CuasMotionLMA.RSLT_VX];
											double vy = target[ntile][CuasMotionLMA.RSLT_VY];
											double vv = vx*vx+vy*vy;
											if (!(vv > max_v2)) {
												replace = true;
											}
										}
										if (replace) {
											target[ntile][CuasMotionLMA.RSLT_VX] =    target_b[ntile][CuasMotionLMA.RSLT_VX];
											target[ntile][CuasMotionLMA.RSLT_VY] =    target_b[ntile][CuasMotionLMA.RSLT_VY];
											target[ntile][CuasMotionLMA.RSLT_VSTR] =  target_b[ntile][CuasMotionLMA.RSLT_VSTR];
											target[ntile][CuasMotionLMA.RSLT_VFRAC] = target_b[ntile][CuasMotionLMA.RSLT_VFRAC];
										}
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return;
	}

	
	
	public double [][][][] locateAndFreezeTargetsMulti(
			final boolean         batch_mode,
			float [][]            fpixels_tum,
			double [][][]         motion_sequence, // starts with just motion-based, then adds  other fields (single target per tile)
			boolean               slow_mode,
			final int             debugLevel) {
		return locateAndFreezeTargetsMulti(
				clt_parameters,     // CLTParameters         clt_parameters,
				batch_mode,         // final boolean         batch_mode,
				parentCLT,          // QuadCLT               parentCLT,          //
				fpixels_tum,        // float [][]            fpixels_tum,
				motion_sequence,    // double [][][]         motion_sequence, // starts with just motion-based, then adds  other fields (single target per tile)
				slow_mode,          // boolean               slow_mode,
				this,               // final CuasMotion      cuasMotion,
				scene_titles,       // String []             scene_titles, // recreate slice_titles from scene titles?
				slice_titles,       // String []             slice_titles,
				debugLevel);        // final int             debugLevel)
	}
	public static double [][][][] locateAndFreezeTargetsMulti(
			CLTParameters         clt_parameters,
			final boolean         batch_mode,
			QuadCLT               parentCLT,          //
			float [][]            fpixels_tum,
			double [][][]         motion_sequence, // starts with just motion-based, then adds  other fields (single target per tile)
			boolean               slow_mode,
			final CuasMotion      cuasMotion,
			String []             scene_titles, // recreate slice_titles from scene titles?
			String []             slice_titles,
			final int             debugLevel) {
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset;
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs;
		int     n_recenter =     clt_parameters.imp.cuas_n_recenter;
		double  speed_min =      clt_parameters.imp.cuas_speed_min;
		double  speed_pref =     clt_parameters.imp.cuas_speed_pref;
		double  speed_boost =    clt_parameters.imp.cuas_speed_boost;
		boolean smooth =         clt_parameters.imp.cuas_smooth; // true;
		boolean half_step =      clt_parameters.imp.cuas_half_step; // true;
		int     max_range =      clt_parameters.imp.cuas_max_range;
		int     num_cycles =     clt_parameters.imp.cuas_num_cycles;
	    double  lmax_fraction =  clt_parameters.imp.cuas_lmax_fraction;    // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
	    double  lmax_flt_neglim= clt_parameters.imp.cuas_lmax_flt_neglim;  // -0.3;   // limit negative data to reduce ridge influence (make -10 to disable)        
	    double  lmax_flt_hsigma= clt_parameters.imp.cuas_lmax_flt_hsigma;  // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile 
	    double  lmax_flt_lsigma= clt_parameters.imp.cuas_lmax_flt_lsigma;  // 1.0 // LPF - GB result of the previous subtraction
	    double  lmax_flt_scale=  clt_parameters.imp.cuas_lmax_flt_scale;   // 5.0 // scale filtering result
	    double  sky_threshold =  clt_parameters.imp.cuas_sky_threshold;     // 0.9;   // minimal value of the sky mask where target is possible
	    
	    double  lmax_hack_ridge= clt_parameters.imp.cuas_lmax_hack_ridge; // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
	    double  lmax_radius =    clt_parameters.imp.cuas_lmax_radius;   // 3.5;     // Look inside ((int)cuas_lmax_radius) * 2 + 1 square for the local maximum isolation 
//		boolean lmax_zero =      clt_parameters.imp.cuas_lmax_zero;     // true;    // zero all data outside this radius from the maximum
		
		double target_radius =   clt_parameters.imp.cuas_target_radius;
		double target_strength = clt_parameters.imp.cuas_target_strength;
		double [][] target_frac = new double [clt_parameters.imp.cuas_target_frac.length][2];
		boolean no_border=       clt_parameters.imp.cuas_no_border; // true;
	// Moving target LMA	
	    double  lma_sigma =      clt_parameters.imp.cuas_lma_sigma; // =     3.0;
	    double  wnd_pedestal =   clt_parameters.imp.cuas_wnd_pedestal; // =     3.0;
	    double  lma_r0 =         clt_parameters.imp.cuas_lma_r0; // =        3.0;  //maximum with with overshoot
	    double  lma_ovrsht =     clt_parameters.imp.cuas_lma_ovrsht; // =    2.0;
	// CUAS Motion LMA parameters
		boolean lma_fit_xy=      clt_parameters.imp.cuas_lma_fit_xy; // true;
		boolean lma_fit_a=       clt_parameters.imp.cuas_lma_fit_a; // true;
		boolean lma_fit_c=       clt_parameters.imp.cuas_lma_fit_c; // true;
		boolean lma_fit_r=       clt_parameters.imp.cuas_lma_fit_r; // true;
		boolean lma_fit_k=       clt_parameters.imp.cuas_lma_fit_k; // true;
	    double  lambda =         clt_parameters.imp.cuas_lambda; // =        0.1;
	    double  lambda_good =    clt_parameters.imp.cuas_lambda_good; // =   0.5;
	    double  lambda_bad =     clt_parameters.imp.cuas_lambda_bad; // =    8;
	    double  lambda_max =     clt_parameters.imp.cuas_lambda_max; // =  100;
	    double  rms_diff =       clt_parameters.imp.cuas_rms_diff; // =      0.001; // relative RMS improvement
	    int     num_iter =       clt_parameters.imp.cuas_num_iter; // =     20;
	// CUAS Motion LMA filter parameters
	    double  lma_rms =        clt_parameters.imp.cuas_lma_rms; // =       1.5;  // Maximal RMS, regardless of amplitude
	    double  lma_arms =       clt_parameters.imp.cuas_lma_arms; // =      0.06; // Maximal absolute RMS, sufficient for any amplitude
	    double  lma_rrms =       clt_parameters.imp.cuas_lma_rrms; // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
	    double  lma_mina =       clt_parameters.imp.cuas_lma_mina; // =      1.0;  // Minimal A (amplitude)
	    double  lma_pre_mina =   clt_parameters.imp.cuas_lma_pre_mina; // =      0.5;  // scale  cuas_lma_mina to filter initial candidates (if there are > one of them) 
	    int     min_keep =       clt_parameters.imp.cuas_min_keep; // 2 // keep at least this number of candidates before using cuas_lma_pre_mina filter
	    
	    //0.5 * lma_mina,// 			final double        pre_min_a
	    
	    double  lma_maxr =       clt_parameters.imp.cuas_lma_maxr; // =      5.0;  // Maximal radius (>3.8)
	    double  lma_minr1 =      clt_parameters.imp.cuas_lma_minr1; // =     1.0;  // Maximal inner radius
	    double  lma_mink =       clt_parameters.imp.cuas_lma_mink; // =      0.0;  // Minimal K (overshoot)  <0.007
	    double  lma_maxk =       clt_parameters.imp.cuas_lma_maxk; // =      5.0;  // Minimal K (overshoot) > 3.8
	    double  lma_a2a =        clt_parameters.imp.cuas_lma_a2a; // =       0.7;  // Minimal ratio of the maximal pixel to the amplitude
		double  slow_fast_mismatch =  clt_parameters.imp.cuas_slow_fast_mismatch;  // 1.5; allow larger mismatch between slow and fast
	    boolean fail_mismatch =  clt_parameters. imp.cuas_fail_mismatch;  // Fail on high mismatch early (when calculating scores);
	    double  offcenter =      clt_parameters. imp.cuas_offcenter;
	    double  noisemax =       clt_parameters. imp.cuas_noisemax;
	    double  noisemax8 =      clt_parameters. imp.cuas_noisemax8;	    
	    double  target_horizon=  clt_parameters.imp.cuas_horizon;	    
	// Handling overall target scores    
	    
		double min_score_mv =    clt_parameters.imp.cuas_score_mv;
		double factor_lim =      clt_parameters.imp.cuas_factor_lim;
		double factor_pow =      clt_parameters.imp.cuas_factor_pow;
		double [] score_coeff =  clt_parameters.imp.cuas_score_coeff.clone();
		double  slow_score =     clt_parameters.imp.cuas_slow_score;
		double  input_range =    clt_parameters.imp.cuas_input_range; // 5;
		int     iter_show =      clt_parameters.imp.cuas_iter_show;       //1; // Maximal enhancement iteration to show intermediate result (0 - none) 
		boolean intermed_low =   clt_parameters.imp.cuas_intermed_low;    //true;
		boolean intermed_high =  clt_parameters.imp.cuas_intermed_high;   //true;
		double  centered_radius=      clt_parameters.imp.cuas_centered_radius;      // 3.5; // in centered mode (target placed at [8,8] - zero out data outside peripheral areas, keep 0.5 at the radius 	
		double  centered_blur =       clt_parameters.imp.cuas_centered_blur;        // 0.5; // relative transition radius range (for 0.5 the mask inside (1-0.5) radius will be 1.0, outside (1+0.5) radius will be 0
		double  duplicate_tolerance = clt_parameters.imp.cuas_duplicate_tolerance ; // 2.0; // (pix) Remove weaker maximums closer than this to a stronger one   		
		boolean debug_more =     clt_parameters.imp.cuas_debug_more;
		double  boost_accum_pairs =    slow_mode? 1.0: clt_parameters.imp.cuas_boost_accum; //  4.0;   // if >1 and the motion vector is below tile_size/cuas_boost_slow, scale corr_pairs
		int     precorr_ra =           clt_parameters.imp.cuas_precorr_ra;   // By Claude on 05/07/2026
		int     accum_ra =             clt_parameters.imp.cuas_accum_ra;     // By Claude on 05/07/2026: LPF for shiftAndRenderAccumulate
		boolean cuas_gaussian_ra =     clt_parameters.imp.cuas_gaussian_ra;  // By Claude on 05/07/2026

		for (int i = 0; i < target_frac.length; i++) {
			if (clt_parameters.imp.cuas_target_frac[i].length >= 2) {
				target_frac[i][0] = clt_parameters.imp.cuas_target_frac[i][0];
				target_frac[i][1] = clt_parameters.imp.cuas_target_frac[i][1];
			} else {
				System.out.println("testCuasScanMotion(): wrong format for a pair of strength, fraction values.");
			}
		}
		boolean reuse_non_centered = true; // make a parameter
		
		
		
		double filter_below_horizon=5;
		int start_frame = 0;
		int num_scenes = fpixels_tum.length; // .getStack().getSize()- first_corr; // includes average
		int seq_length = corr_offset + corr_pairs;
		int corr_inc = half_step ? (corr_offset/2) : corr_offset;
		int num_corr_samples = (num_scenes - seq_length - start_frame) / corr_inc;
		final int     frame0 = start_frame + seq_length/2;
		final int half_accum_range = (slow_mode? clt_parameters.imp.cuas_mul_samples : 1) * corr_pairs/2 ; // slow_mode
		int [] filter5_remain = new int [num_corr_samples];
		int [] num_all =        new int [num_corr_samples];
		int [] num_undef =      new int [num_corr_samples];
		int [] num_good =       new int [num_corr_samples];
		int [] num_bad =        new int [num_corr_samples];
		int [] totals =         null; 
		int [] passes = new int [num_corr_samples]; // debugging filter5
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null)+(cuasMotion.slow_targets? "-SLOW":"-FAST");

//		final double [][][][] target_sequence_multi = null; 
		double [][][][] target_sequence_multi = null; 
		int niter=0; // maybe start from 19 to match skipped non-center and have the same file names
		if (reuse_non_centered)	{
			String x3d_path = parentCLT.getX3dDirectory();
			String noncentered_path = x3d_path + Prefs.getFileSeparator() + model_prefix+"-ROUND_ONE"+".tiff";
			target_sequence_multi = readDoubleHyperHyperstack( // slices should contain two ":"			
							noncentered_path);           // String      path,
			if (debugLevel > -4) {
				if (target_sequence_multi!= null) {
					System.out.println("locateAndFreezeTargetsMulti(): reusing non-centered data from "+noncentered_path);
				} else {
					System.out.println("locateAndFreezeTargetsMulti(): non-centered data not found: "+noncentered_path);
					System.out.println("Will calculate and save non-centered targets");
				}
			}
//			niter = 19; //
		}
		final int num_seq = motion_sequence.length;
		final int num_tiles = motion_sequence[0].length;
		// By Claude on 05/07/2026: temporal LPF for accumulation and MV refinement inputs
		// fpixels_accum: LPF for shiftAndRenderAccumulate (accum_ra); fpixels_refine: LPF for refineMotionVectors (precorr_ra)
		final float [][] fpixels_accum;
		if (accum_ra > 1) {
			fpixels_accum = cuas_gaussian_ra ?
					runningGaussian(fpixels_tum, accum_ra, cuasMotion.gpu_max_width) :
					runningAverage( fpixels_tum, accum_ra, cuasMotion.gpu_max_width);
		} else {
			fpixels_accum = fpixels_tum;
		}
		final float [][] fpixels_refine;
		if (precorr_ra > 1) {
			fpixels_refine = cuas_gaussian_ra ?
					runningGaussian(fpixels_tum, precorr_ra, cuasMotion.gpu_max_width) :
					runningAverage( fpixels_tum, precorr_ra, cuasMotion.gpu_max_width);
		} else {
			fpixels_refine = fpixels_tum;
		}
		if (target_sequence_multi == null) {
			//		final double [][][][]
			target_sequence_multi = new double [num_seq][num_tiles][][];
			initFromMotionSequence(motion_sequence, target_sequence_multi); // By Claude on 05/07/2026
			// first pass, using non-centered targets
			//		double        boost_accum_pairs = slow_mode? 1.0:4.0; // just for testing, unconditionally boost tracking cameras exposure time
			for (; niter < num_cycles; niter++) {
				boolean save_filtered_low =  intermed_low &&  (niter < iter_show);
				boolean save_filtered_high = intermed_high && (niter < iter_show);
				totals = getRemain(target_sequence_multi, num_all, num_undef, num_good, num_bad); // By Claude on 05/07/2026
				if (totals[TOTALS_UNDEFINED] == 0) {
					if (debugLevel > -4) System.out.println ("No undefined tiles left, breaking loop");
					break;
				}
				if (save_filtered_low && debug_more) {
					ImagePlus imp_ms1 = showTargetSequence(
							motion_sequence,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							model_prefix+"-MS1-n"+niter,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ms1);
				}			

				//	double [][] effective_strength = 
				// ==== does it need to be re-calculated ? =====
				// Only used in 			int [][] filter5 = filter5Targets( // will ignore failed tiles
				// for centered tile pass, trying to eliminate with null for the motion sequence
				getEffectiveStrengthMV( // calculate tiles effective strength by the motion vectors. Combine with the target LMA?
						motion_sequence,   // final double [][][] motion_scan,
						niter,             // int                 niter, // save iteration number on failure if >=
						cuasMotion.tilesX, // final int           tilesX)
						0,                 // final double        min_score_mv,
						speed_min,         // double              speed_min,
						speed_pref,        // double              speed_pref,
						speed_boost);      // double              speed_boost);
				syncMotionScoreToSentinels(motion_sequence, target_sequence_multi); // By Claude on 05/07/2026

				if (save_filtered_low  && debug_more) {
					ImagePlus imp_mv_strength = showTargetSequence(
							motion_sequence,       // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							model_prefix+"-MV_STRENGTH-n"+niter,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_mv_strength);
				}

				int [][] filter5 = filter5Targets( // will ignore failed tiles
						null,                  // By Claude on 05/07/2026: use FAIL_PENDING sentinels in target_sequence_multi
						target_sequence_multi, // final double [][][][] target_sequence_multi,
						// if     use motion and select_new will only consider tiles (and compare motion scores in motion_sequence) that have nulls in target_sequence_multi[nseq][ntile]
						// if not use motion and select_new will only consider (and compare scores) tiles that have [RSLT_CENTERED] = 0.0
						// if not select_new (assumes not  use motion) will return best (and good) target indices
						true,               // final boolean       use_motion,     // true - use motion vectors confidence, false - use target confidence
						true,               // final boolean       select_new,     // true - use only untested tiles, false - use good tiles 
						min_score_mv,       // double              min_confidence,
						target_horizon,     // final double        lma_horizon, // target below horizon
						cuasMotion.tilesX,  // final int           tilesX,
						max_range,          // final int           range, // 1 or 2
						filter5_remain,     // final int     []    remain){
						passes,             // final int     []    passes, // debugging - numer of passes required
						debugLevel);        // final int           debugLevel)
				boolean         good_only=    false;
				boolean         show_empty =  true; // false; // show scenes with no (valid) targets
				if (save_filtered_low) {
					ImagePlus imp_filter5 = ShowDoubleFloatArrays.makeArrays(
							filter5,
							cuasMotion.tilesX,
							cuasMotion.tilesY, 
							model_prefix+"-FILTER5_SELECTED_MULTI-n"+niter,   // Selected to test 
							slice_titles);
					if (!batch_mode) {
						imp_filter5.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_filter5);  // ImagePlus   imp)
					/*
				ImagePlus imp_target_sequence_multi = showTargetSequence(
						target_sequence_multi,    // final double [][][][] targets_multi,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-TARGET-SEQUENCE-MULTI-n"+niter,// String        title,
						good_only,            // final boolean         good_only,
						show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_target_sequence_multi);
					 */

				}

				if (debugLevel > -4) printStatsLine("filter5 remain",true,filter5_remain);
				int filter5_total = getTotal(filter5_remain);
				if (filter5_total == 0) {
					if (debugLevel > -4) System.out.println ("No filter5 tiles left, breaking loop");
					break;
				}

				// By Claude on 05/07/2026: sentinels in target_sequence_multi carry motion vector data cloned from motion_sequence
				double [][][] targets_nonoverlap = applyFilter(
						target_sequence_multi,    // double [][][][] target_sequence_multi
						filter5);                 // int [][] filter5
				// Anything remains? we'll see after extension
				double [][][] extended_scan = extendMotionScan(
						targets_nonoverlap, // final double [][][] motion_scan,
						null, // filter5,      // final boolean [][]  filtered, // centers, should be non-overlapped
						cuasMotion.tilesX,    // final int           tilesX)
						2,                    // final int           range, // 1 or 2
						null); // remain);              // final int     []    remain)

				if (save_filtered_low && debug_more) {

					// targets_nonoverlap will contain motion vectors used fro the next fpixels_accumulated
					ImagePlus imp_novl = showTargetSequence(
							targets_nonoverlap,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							model_prefix+"-SELECTED-TO-TRY-n"+niter,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_novl);

					ImagePlus imp_ext = showTargetSequence(
							extended_scan,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							model_prefix+"-EXTENDED-n"+niter,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ext);
					// show good and bad accumulated here too?
				}

				// perform new accumulations of shifted non-conflicting tiles
				float [][] fpixels_accumulated = cuasMotion.shiftAndRenderAccumulate(
						clt_parameters,   // CLTParameters       clt_parameters,
						false,            // final boolean       center,
						false,            // final boolean       fill_zeros,
						fpixels_accum,    // By Claude on 05/07/2026: LPF'd with accum_ra (was fpixels_tum)
						extended_scan,    // final double [][][] vector_field,
						frame0,           // final int           frame0,      // for vector_field[0]
						corr_inc,         // final int           frame_step,
						half_accum_range, // final int           half_range,
						boost_accum_pairs,// final double        boost_pairs,
						smooth,           // final boolean       smooth,
						corr_offset,      // final int           corr_offset, // interframe distance for correlation
						true);            // final boolean       batch_mode) {
				if (save_filtered_high) {
					String title = model_prefix+"-ACCUMULATED-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_acc = ShowDoubleFloatArrays.makeArrays(
							fpixels_accumulated, // double[][] pixels,
							cuasMotion.gpu_max_width,
							cuasMotion.gpu_max_height, 
							title, // model_prefix+"-ACCUMULATED-n"+niter,   // String title,
							slice_titles);
					imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_acc.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_acc);  // ImagePlus   imp)
				}

				// replace center frames with the accumulated ones
				if (debugLevel > -4) {
					System.out.println("Starting render, iteration = "+niter);
				}

				// show just fpixels_accumulated
				// move outside? compare to slice_titles known
				// Calculate both centroid and LMA target parametes, return both
				// targets_new will contain motion vectors, centroid, and LMA results combined
				//save_filtered_high
				float [][] accum_debug = save_filtered_high? new float [num_corr_samples][]:null; //fpixels_accumulated.length]
				boolean       keep_failed = false; // keep failed targets
				double [][][][] targets_new_multi = 	getAccumulatedCoordinatesMulti(
						keep_failed,         // final boolean       keep_failed, // keep failed targets
						niter, // 	final int           when, // set if >=0 for failures
						false,                // final boolean       centered,
						targets_nonoverlap,   // final double [][][] vector_fields, // centers
						fpixels_accumulated,  // final double [][]   accum_data,   // should be around 0, no low-freq
						lmax_fraction,        // final double        cuas_lmax_fraction, // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
						lmax_flt_neglim,      // final double        lmax_flt_neglim,   // -0.3;   // limit negative data to reduce ridge influence (make -10 to disable)
						lmax_flt_hsigma,	  // final double        lmax_flt_hsigma,	 // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile     
						lmax_flt_lsigma,      // final double        lmax_flt_lsigma,   // 1.0 // LPF - GB result of the previous subtraction
						lmax_flt_scale,       // final double        lmax_flt_scale,    // 5.0 // scale filtering result
						cuasMotion.sky_mask,  // final double        sky_mask,
						sky_threshold,    // final double        sky_threshold,    // 0.9 // minimal value of the sky mask where target is possible
						target_horizon+filter_below_horizon,       // final double        lma_horizon,
						lmax_hack_ridge,      // final double        lmax_hack_ridge, // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
						lmax_radius,          // final int           cuas_lmax_radius,   // 3;       // look inside cuas_lmax_radius* 2 + 1 square for the local maximum isolation
						centered_radius,      // final double        centered_radius,
						centered_blur,        // final double        centered_radius_blur,
						duplicate_tolerance,  // final double        pix_tolerance, // 
						//					lmax_zero,            // final boolean       cuas_lmax_zero,     // true;    // zero all data outside this radius from the maximum
						target_radius,        // final double        centroid_radius,
						n_recenter,           // final int           n_recenter, //  re-center window around new maximum. 0 -no refines (single-pass)
						cuasMotion.tilesX,    // final int           tilesX){
						no_border,            // final boolean       no_border,
						// Moving target LMA	
						lma_sigma,            // final double        lma_sigma,
						wnd_pedestal,         // final double        wnd_pedestal,
						lma_r0,               // final double        lma_r0,
						lma_ovrsht,  // final double        lma_ovrsht,
						// CUAS Motion LMA parameters
						lma_fit_xy,           // final boolean       lma_fit_xy,		
						lma_fit_a,            // final boolean       lma_fit_a,						
						lma_fit_c,            // final boolean       lma_fit_c,
						lma_fit_r,            // final boolean       lma_fit_r,
						lma_fit_k,            // final boolean       lma_fit_k,		
						lambda,               // final double        lambda,
						lambda_good,          // final double        lambda_good,
						lambda_bad,           // final double        lambda_bad,
						lambda_max,           // final double        lambda_max,
						rms_diff,             // final double        rms_diff,
						num_iter,             // final int           num_iter,0, // 			final double        pre_min_a,   // pre-filter minimal LMA-A (half finbal?)
						lma_pre_mina * lma_mina,// final double      pre_min_a,   // 0.5 //scale  cuas_lma_mina to filter initial candidates (if there are > one of them)
						min_keep,             // final int           min_keep, //  2; // keep at least this number of candidates before using cuas_lma_pre_mina filter
						accum_debug,          // final float [][]    accum_debug,
						parentCLT,            // final QuadCLT       parentCLT, // debug only, maybe null in other cases
						debugLevel);          // final int           debugLevel);

				if (accum_debug != null) {
					String title = model_prefix+"-ACCUMULATED_FILTERED-MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_acc = ShowDoubleFloatArrays.makeArrays(
							accum_debug, // double[][] pixels,
							cuasMotion.gpu_max_width,
							cuasMotion.gpu_max_height, 
							title, // model_prefix+"-ACCUMULATED_FILTERED-MULTI-n"+niter,   // String title,
							slice_titles);
					imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_acc.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_acc);  // ImagePlus   imp)
				}

				if (save_filtered_low  && debug_more) {
					String title = model_prefix+"-NEW-TARGETS-MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_new = showTargetSequence(
							targets_new_multi,    // final double [][][][] targets_multi,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							title, // model_prefix+"-NEW-TARGETS-MULTI-n"+niter,// String        title,
							good_only,            // final boolean         good_only,
							show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_new);  // ImagePlus   imp)
				}

				// evaluate scores here to reduce the number of attempts for centered targets
				getEffectiveStrengthLMA(
						targets_new_multi, // final double [][][] target_coords, // LMA
						target_strength,   // final double        target_strength,
						target_frac,       // final double [][]   target_frac, // pairs - strength, minimal fraction for that strength
						lma_rms,           // final double        lma_rms, // =       1.5;  // Maximal RMS (should always match, regardless if A)
						lma_arms,          // final double        lma_arms, // =      0.06; // Maximal absolute RMS (should match one of cuas_lma_arms OR cuas_lma_rrms (0.484)
						lma_rrms,          // final double        lma_rrms, // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
						lma_mina,          // final double        lma_mina, // =      1.0;  // Minimal A (amplitude)
						lma_maxr,          // final double        lma_maxr, // =      5.0;  // Minimal K (overshoot) = 3.0
						lma_minr1,         // final double        lma_minr1,// =      1.0;  // Minimal R1 (radius of positive peak) 
						lma_mink,          // final double        lma_mink, // =      0.0;  // Minimal K (overshoot) = 1.0
						lma_maxk,          // final double        lma_maxk, // =      5.0;  // Minimal K (overshoot) = 3.0//			final double        lma_a2a, 
						lma_a2a,           // final double        lma_a2a, 
						0, // max_mismatch,      // final double        max_mismatch, apply only during final, when mismatch scores are calculated 
						slow_fast_mismatch,// final double          slow_fast_mismatch, // // 1.5; allow larger mismatch between slow and fast
						fail_mismatch,     // final boolean       fail_mismatch,
						offcenter,   	   // final double          offcenter,
						cuasMotion.getNoiseMap(), // final double []       noise_map,
						noisemax,          // final double          noise_max,
						target_horizon,    // final double        lma_horizon, // horizon as maximal pixel Y
						cuasMotion.tilesX);            // final int         tilesX,
				getScore(
						targets_new_multi, // final double [][][] target_sequence, // modifies certain fields (scores)
						factor_lim,        // final double        importance_limit,
						factor_pow,        // final double        importance_power, // Raise each factor to this power before combining
						score_coeff,       // final double []     importance, // for now (each - squared?): [0] - Amplitude (A/A0), 1 - RMS (RMS0/RMS), 2 - RRMS((RMS/A0) / (RMS/A)
						slow_score,        // final double          slow_score,           // multiply total score for targets detected in slow target mode
						cuasMotion.tilesX);            // final int         tilesX,

				if (save_filtered_low) {
					String title = model_prefix+"-NEW-TARGETS_SCORES_MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_new_scores = showTargetSequence(
							targets_new_multi,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							title, // model_prefix+"-NEW-TARGETS_SCORES_MULTI-n"+niter,// String        title,
							good_only,            // final boolean         good_only,
							show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
				}

				int num_new = addNewResults(
						target_sequence_multi, // final double [][][] target_sequence, // will only process non-nulls here
						targets_new_multi,     // final double [][][] new_sequence,
						0, 			           // final int              num_best, // if >0, limit number of best results to add 
						niter,                // final int when_iter,
						debugLevel);           // final int             debugLevel);

				if (debugLevel > -4) {
					System.out.println("Added "+num_new+" new tiles as good/bad");
				}
				if (save_filtered_low) {
					String title = model_prefix+"-ADDED_NEW_MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_new_scores = showTargetSequence(
							target_sequence_multi,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							title, // model_prefix+"-ADDED_NEW_MULTI-n"+niter,// String        title,
							good_only,            // final boolean         good_only,
							show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
				}

				if (num_new == 0) {
					if (debugLevel > -4) System.out.println ("Nothing added from last try, breaking the loop.\n");
					break;  // breaks here
				}
				totals = getRemain(target_sequence_multi, num_all, num_undef, num_good, num_bad); // By Claude on 05/07/2026
				if (debugLevel > -4) printStats ("After iteration "+niter, true, num_all, num_undef, num_good, num_bad);
				if (debugLevel > -4) {
					System.out.println("Non-centered iteration "+niter+" DONE.\n");
				}
			} //for (niter=0; niter < max_iter; niter++)

			if (intermed_low || reuse_non_centered) {
				ImagePlus imp_new_scores = showTargetSequence(
						target_sequence_multi,          // double [][][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-ROUND_ONE",// String        title,
						false, // good_only,            // final boolean         good_only,
						false, // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
			}
		}
		

		//===========================================================================
		if (debugLevel > -4) {
			System.out.println("\n========================== Starting centered iterations =============================.\n");
		}
		// By Claude on 05/05/2026 — optional MV refinement parameters for centered loop
		boolean recalc_mv       = clt_parameters.imp.cuas_recalc_mv && !slow_mode;
		double  recalc_mv_boost = clt_parameters.imp.cuas_recalc_mv_boost;
		double  recalc_mv_r0    = clt_parameters.imp.cuas_recalc_mv_r0;
		double  recalc_mv_r1    = clt_parameters.imp.cuas_recalc_mv_r1;
		
	    lma_rrms =       clt_parameters.imp.cuas_lma_rrms2; // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
	    lma_mina =       clt_parameters.imp.cuas_lma_mina2; // =      1.0;  // Minimal A (amplitude)
		if (debugLevel > -4) {
			System.out.println("Updated for the centered pass lma_rrms="+lma_rrms);
			System.out.println("Updated for the centered pass lma_mina="+lma_mina);
		}
		
		// second pass, using non-centered targets
		int niter_lim = niter+ num_cycles;
		int iter_show1 = iter_show + niter;
		if (intermed_low  && debug_more) {
			ImagePlus imp_mv_strength = showTargetSequence(
					motion_sequence,       // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-MOTION_SEQUENCE-PRE-CENT",// String        title,
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_mv_strength);
		}
		int niter0 = niter; // for debugging, so (niter - niter0) will start from 0
		for (; niter < niter_lim; niter++) {
			boolean         good_only=    false;
			boolean         show_empty =  true; // false; // show scenes with no (valid) targets
			
			boolean save_filtered_low =  intermed_low &&  (niter < iter_show1);
			boolean save_filtered_high = intermed_high && (niter < iter_show1);
			totals = getRemain(target_sequence_multi, num_all, num_undef, num_good, num_bad);
			if (totals[TOTALS_UNDEFINED] == 0) {
				if (debugLevel > -4) System.out.println ("No undefined tiles left, breaking loop");
				break;
			}

			int [][] filter5 = filter5Targets( // will ignore failed tiles
					null, // motion_sequence,       // final double [][][] target_sequence,
					target_sequence_multi, // final double [][][][] target_sequence_multi,non-centered marked as "used"
					// if     use motion and select_new will only consider tiles (and compare motion scores in motion_sequence) that have nulls in target_sequence_multi[nseq][ntile]
					// if not use motion and select_new will only consider (and compare scores) tiles that have [RSLT_CENTERED] = 0.0
					// if not select_new (assumes not  use motion) will return best (and good) target indices
					false,              // final boolean       use_motion,     // true - use motion vectors confidence, false - use target confidence
					true,               // final boolean       select_new,     // true - use only untested tiles, false - use good tiles 
					min_score_mv,       // double              min_confidence,
					target_horizon,     // final double        lma_horizon, // target below horizon
					cuasMotion.tilesX,  // final int           tilesX,
					max_range,          // final int           range, // 1 or 2
					filter5_remain,     // final int     []    remain){
					passes,             // final int     []    passes, // debugging - number of passes required
					debugLevel);        // final int           debugLevel)

			if (save_filtered_low) {
				ImagePlus imp_filter5 = ShowDoubleFloatArrays.makeArrays(
						filter5,
						cuasMotion.tilesX,
						cuasMotion.tilesY, 
						model_prefix+"-FILTER5_SELECTED_MULTI_CENTERED-n"+niter,   // Selected to test 
						slice_titles);
				if (!batch_mode) {
					imp_filter5.show();
				}
				parentCLT.saveImagePlusInModelDirectory(imp_filter5);  // ImagePlus   imp)
			}

			if (save_filtered_low  && debug_more) {
				ImagePlus imp_marked_used = showTargetSequence(
						target_sequence_multi,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-MARKED_USED-n"+niter,// String        title,
						good_only,            // final boolean         good_only,
						show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_marked_used);  // ImagePlus   imp)
			}
			
			
			if (debugLevel > -4) printStatsLine("filter5 remain",true,filter5_remain);
			int filter5_total = getTotal(filter5_remain);
			if (filter5_total == 0) {
				if (debugLevel > -4) System.out.println ("No filter5 non-centered tiles left, breaking loop");
				break;
			}
			// render and process in centered mode
//			double [][][] targets_nonoverlap = applyFilter( // motion vectors // will have nulls not top try 
//					motion_sequence,          // double [][][] motion_scan,
//					filter5);                 // boolean [][] filter5)
			
			double [][][] targets_nonoverlap = applyFilter( // motion vectors // will have nulls not top try 
					target_sequence_multi,    // double [][][][] target_sequence_multi
					filter5);                 // boolean [][] filter5)
			
			// Anything remains? we'll see after extension
			/*
			double [][][] extended_scan = extendMotionScan(
					targets_nonoverlap, // final double [][][] motion_scan,
					null, // filter5,      // final boolean [][]  filtered, // centers, should be non-overlapped
					cuasMotion.tilesX,    // final int           tilesX)
					2,                    // final int           range, // 1 or 2
					null); // remain);              // final int     []    remain)
			 */
			if (save_filtered_low  && debug_more) {
				// targets_nonoverlap will contain motion vectors used fro the next fpixels_accumulated
				ImagePlus imp_novl = showTargetSequence(
						targets_nonoverlap,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-SELECTED-TO-TRY-CENTERED-n"+niter,// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_novl);
/*
				ImagePlus imp_ext = showTargetSequence(
						extended_scan,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-EXTENDED-CENTERED-n"+niter,// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_ext);
				// show good and bad accumulated here too?
*/
			}

			// Andrey 05/05/2026 moved here (earlier) so shiftAndRenderAccumulate() will use updated motion vector
			// By Claude on 05/05/2026 — re-correlate with spatial mask around known target
			if (recalc_mv) {
				refineMotionVectors(
						clt_parameters,      // CLTParameters   clt_parameters,
						batch_mode,          // boolean         batch_mode,
						cuasMotion,          // CuasMotion      cuasMotion,
						recalc_mv_boost,     // double          recalc_mv_boost,
						recalc_mv_r0,        // double          recalc_mv_r0,
						recalc_mv_r1,        // double          recalc_mv_r1,
						fpixels_refine,      // By Claude on 05/07/2026: LPF'd with precorr_ra (was fpixels_tum)
						targets_nonoverlap,  // double [][][]   targets_nonoverlap,
						frame0,              // int             frame0,
						corr_inc,            // int             corr_inc,
						half_accum_range,    // int             half_accum_range,
						smooth,              // boolean         smooth,
						corr_offset,         // int             corr_offset,
						niter - niter0,      // int                 dbg_iter_index,
						-1); // debugLevel);         // int             debugLevel)
			}
			double [][][] extended_scan = extendMotionScan(
					targets_nonoverlap, // final double [][][] motion_scan,
					null, // filter5,      // final boolean [][]  filtered, // centers, should be non-overlapped
					cuasMotion.tilesX,    // final int           tilesX)
					2,                    // final int           range, // 1 or 2
					null); // remain);              // final int     []    remain)
			if (save_filtered_low  && debug_more) {
				ImagePlus imp_ext = showTargetSequence(
						extended_scan,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						model_prefix+"-EXTENDED-CENTERED-n"+niter,// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_ext);
			}
//			double [][][][] targets_new_multi = null;
			int num_new = 0;
			{
				// perform new accumulations of shifted non-conflicting tiles
				float [][] fpixels_accumulated = cuasMotion.shiftAndRenderAccumulate(
						clt_parameters,   // CLTParameters       clt_parameters,
						true, // false,            // final boolean       center, // why is it false here?
						false,            // final boolean       fill_zeros,
						fpixels_accum,    // By Claude on 05/07/2026: LPF'd with accum_ra (was fpixels_tum)
						extended_scan,    // final double [][][] vector_field,
						frame0,           // final int           frame0,      // for vector_field[0]
						corr_inc,         // final int           frame_step,  // keep for slow
						half_accum_range, // final int           half_range,  // scale for slow
						boost_accum_pairs,// final double        boost_pairs,
						smooth,           // final boolean       smooth,
						corr_offset,      // final int           corr_offset, // interframe distance for correlation
						true);            // final boolean       batch_mode) {
				if (save_filtered_high) {
					String title = model_prefix+"-ACCUMULATED-CENTERED-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_acc = ShowDoubleFloatArrays.makeArrays(
							fpixels_accumulated, // double[][] pixels,
							cuasMotion.gpu_max_width,
							cuasMotion.gpu_max_height, 
							title, // model_prefix+"-ACCUMULATED-CENTERED-n"+niter,   // String title,
							slice_titles);
					imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_acc.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_acc);  // ImagePlus   imp)
				}

				// replace center frames with the accumulated ones
				if (debugLevel > -4) {
					System.out.println("Starting render, iteration = "+niter);
				}

				// show just fpixels_accumulated
				// move outside? compare to slice_titles known
				// Calculate both centroid and LMA target parametes, return both
				// targets_new will contain motion vectors, centroid, and LMA results combined
				//save_filtered_high

				float [][] accum_debug = save_filtered_high? new float [num_corr_samples][]:null; //fpixels_accumulated.length]
				boolean    keep_failed = false; // keep failed targets
				double [][][][] targets_new_multi = 	getAccumulatedCoordinatesMulti(
						keep_failed,          // final boolean       keep_failed, // keep failed targets
						niter,                // final int           when, // set if >=0 for failures
						true,                 // final boolean       centered,
						targets_nonoverlap,   // final double [][][] vector_fields, // centers
						fpixels_accumulated,  // final double [][]   accum_data,   // should be around 0, no low-freq
						lmax_fraction,        // final double        cuas_lmax_fraction, // 0.7;     // Check if local maximum is separated from tye surrounding by this fraction of the maximum value
						lmax_flt_neglim,      // final double        lmax_flt_neglim,   // -0.3;   // limit negative data to reduce ridge influence (make -10 to disable)
						lmax_flt_hsigma,	  // final double        lmax_flt_hsigma,	 // 1.0 // HPF (~UM) subtract GB with this sigma from the data tile     
						lmax_flt_lsigma,      // final double        lmax_flt_lsigma,   // 1.0 // LPF - GB result of the previous subtraction
						lmax_flt_scale,       // final double        lmax_flt_scale,    // 5.0 // scale filtering result
						cuasMotion.sky_mask,  // final double        sky_mask,
						sky_threshold,        // final double        sky_threshold,    // 0.9 // minimal value of the sky mask where target is possible
						target_horizon+filter_below_horizon,       // final double        lma_horizon,
						lmax_hack_ridge,      // final double        lmax_hack_ridge, // 0.45;    // Hack for ridges: remove horizontal streaks, where average(abs()) for the line exceeds fraction of abs max 0.32-good, 0.55 - bad
						lmax_radius,          // final int           cuas_lmax_radius,   // 3;       // look inside cuas_lmax_radius* 2 + 1 square for the local maximum isolation
						lmax_radius,          // final double        centered_radius,
						0.5,                  // final double        centered_radius_blur, 
						1.0,                  // final double        pix_tolerance, // 
						//					lmax_zero,            // final boolean       cuas_lmax_zero,     // true;    // zero all data outside this radius from the maximum
						target_radius,        // final double        centroid_radius,
						n_recenter,           // final int           n_recenter, //  re-center window around new maximum. 0 -no refines (single-pass)
						cuasMotion.tilesX,    // final int           tilesX){
						no_border,            // final boolean       no_border,
						// Moving target LMA	
						lma_sigma,            // final double        lma_sigma,
						wnd_pedestal,         // final double        wnd_pedestal,
						lma_r0,               // final double        lma_r0,
						lma_ovrsht,  // final double        lma_ovrsht,
						// CUAS Motion LMA parameters
						lma_fit_xy,           // final boolean       lma_fit_xy,		
						lma_fit_a,            // final boolean       lma_fit_a,						
						lma_fit_c,            // final boolean       lma_fit_c,
						lma_fit_r,            // final boolean       lma_fit_r,
						lma_fit_k,            // final boolean       lma_fit_k,		
						lambda,               // final double        lambda,
						lambda_good,          // final double        lambda_good,
						lambda_bad,           // final double        lambda_bad,
						lambda_max,           // final double        lambda_max,
						rms_diff,             // final double        rms_diff,
						num_iter,             // final int           num_iter,0, // 			final double        pre_min_a,   // pre-filter minimal LMA-A (half finbal?)
						lma_pre_mina*lma_mina,// final double      pre_min_a,   // 0.5 //scale  cuas_lma_mina to filter initial candidates (if there are > one of them)
						min_keep,             // final int           min_keep, //  2; // keep at least this number of candidates before using cuas_lma_pre_mina filter
						accum_debug,          // final float [][]    accum_debug,
						parentCLT,            // final QuadCLT       parentCLT, // debug only, maybe null in other cases
						debugLevel);          // final int           debugLevel);

				if (accum_debug != null) {
					String title = model_prefix+"-ACCUMULATED_FILTERED-CENTERED-MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}
					ImagePlus imp_acc = ShowDoubleFloatArrays.makeArrays(
							accum_debug, // double[][] pixels,
							cuasMotion.gpu_max_width,
							cuasMotion.gpu_max_height, 
							title, // model_prefix+"-ACCUMULATED_FILTERED-CENTERED-MULTI-n"+niter,   // String title,
							slice_titles);
					imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
					if (!batch_mode) {
						imp_acc.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_acc);  // ImagePlus   imp)
				}
				// no need to to evaluate scores now - can be done all at once when all non-centered will be replaced by centered, removed (almost) duplicates
				// evaluating scores, but it is not needed
				// new results are sorted by decreasing amplitude, there should be only one - test it. If not still use only the first
				// continue niter for "when"
				// keep only one (best) result for centered getAccumulatedCoordinatesMulti()



				if (save_filtered_low   && debug_more) {
					String title = model_prefix+"-NEW-TARGETS-CENTERED-MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}

					ImagePlus imp_new = showTargetSequence(
							targets_new_multi,    // final double [][][][] targets_multi,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							title, // model_prefix+"-NEW-TARGETS-CENTERED-MULTI-n"+niter,// String        title,
							good_only,            // final boolean         good_only,
							show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_new);  // ImagePlus   imp)
				}

				// evaluate scores here to reduce the number of attempts for centered targets
				getEffectiveStrengthLMA(
						targets_new_multi, // final double [][][] target_coords, // LMA
						target_strength,   // final double        target_strength,
						target_frac,       // final double [][]   target_frac, // pairs - strength, minimal fraction for that strength
						lma_rms,           // final double        lma_rms, // =       1.5;  // Maximal RMS (should always match, regardless if A)
						lma_arms,          // final double        lma_arms, // =      0.06; // Maximal absolute RMS (should match one of cuas_lma_arms OR cuas_lma_rrms (0.484)
						lma_rrms,          // final double        lma_rrms, // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
						lma_mina,          // final double        lma_mina, // =      1.0;  // Minimal A (amplitude)
						lma_maxr,          // final double        lma_maxr, // =      5.0;  // Minimal K (overshoot) = 3.0
						lma_minr1,         // final double        lma_minr1,// =      1.0;  // Minimal R1 (radius of positive peak) 
						lma_mink,          // final double        lma_mink, // =      0.0;  // Minimal K (overshoot) = 1.0
						lma_maxk,          // final double        lma_maxk, // =      5.0;  // Minimal K (overshoot) = 3.0//			final double        lma_a2a, 
						lma_a2a,           // final double        lma_a2a, 
						0, // max_mismatch,      // final double        max_mismatch, apply only during final, when mismatch scores are calculated 
						slow_fast_mismatch,// final double          slow_fast_mismatch, // // 1.5; allow larger mismatch between slow and fast
						fail_mismatch,     // final boolean       fail_mismatch,
						offcenter,   	   // final double          offcenter,
						cuasMotion.getNoiseMap(), // final double []       noise_map,
						noisemax,          // final double          noise_max,
						target_horizon,    // final double        lma_horizon, // horizon as maximal pixel Y
						cuasMotion.tilesX);            // final int         tilesX,
				int [] remain_es_centered = getScore(
						targets_new_multi, // final double [][][] target_sequence, // modifies certain fields (scores)
						factor_lim,        // final double        importance_limit,
						factor_pow,        // final double        importance_power, // Raise each factor to this power before combining
						score_coeff,       // final double []     importance, // for now (each - squared?): [0] - Amplitude (A/A0), 1 - RMS (RMS0/RMS), 2 - RRMS((RMS/A0) / (RMS/A)
						slow_score,        // final double          slow_score,           // multiply total score for targets detected in slow target mode
						cuasMotion.tilesX);            // final int         tilesX,

				if (save_filtered_low) {
					String title = model_prefix+"-NEW-TARGETS_CENTERED_SCORES_MULTI-n"+niter;
					if (boost_accum_pairs > 1.0) {
						title+="-BOOST"+boost_accum_pairs;
					}

					ImagePlus imp_new_scores = showTargetSequence(
							targets_new_multi,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							title, // model_prefix+"-NEW-TARGETS_CENTERED_SCORES_MULTI-n"+niter,// String        title,
							good_only,            // final boolean         good_only,
							show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
				}

				//int 
				num_new += addNewResults(
						target_sequence_multi, // final double [][][] target_sequence, // will only process non-nulls here
						targets_new_multi,     // final double [][][] new_sequence,
						// add just one best here?
						1, 			           // final int              num_best, // if >0, limit number of best results to add 
						niter,                 // final int when_iter,
						debugLevel);           // final int             debugLevel);

				if (debugLevel > -4) {
					System.out.println("Added "+num_new+" new tiles as good/bad at iteration "+niter+".");
				}
			}
			
			
			if (save_filtered_low) {
				String title = model_prefix+"-ADDED_NEW_CENTERED_MULTI-n"+niter;
				if (boost_accum_pairs > 1.0) {
					title+="-BOOST"+boost_accum_pairs;
				}
				ImagePlus imp_new_scores = showTargetSequence(
						target_sequence_multi,          // double [][][] vector_fields_sequence,
						slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
						title, // model_prefix+"-ADDED_NEW_CENTERED_MULTI-n"+niter,// String        title,
						good_only,            // final boolean         good_only,
						show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
			}

			if (num_new == 0) {
				if (debugLevel > -4) System.out.println ("Nothing added from last try, breaking the loop.\n");
				break;  // breaks here
			}
			totals = getRemain(target_sequence_multi, num_all, num_undef, num_good, num_bad);
			if (debugLevel > -4) printStats ("After centered iteration "+niter, true, num_all, num_undef, num_good, num_bad);
			if (debugLevel > -4) {
				System.out.println("Centered iteration "+niter+" DONE.\n");
			}
		} //for (niter=0; niter < max_iter; niter++)
		boolean         good_only=    false;
		boolean         show_empty =  true; // false; // show scenes with no (valid) targets
		
		
		if (intermed_low) { // targets_good now has
			ImagePlus imp_good = showTargetSequence(// tried 80*64*243815 slices
					target_sequence_multi,   // double [][][] vector_fields_sequence,
					slice_titles,            // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-ROUND_TWO",// String        title,
					good_only,               // final boolean         good_only,
					show_empty,              // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,             // boolean       show,
					cuasMotion.tilesX);      //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_good);  // ImagePlus   imp)
		}
		return target_sequence_multi; // target_sequence; // contains all tiles, good or bad, their data and when they were defined
	}
	/**
	 * 		if (select_new && !use_motion) , fill mark all selected non-centered as "used"
	 * @param motion_sequence
	 * @param target_sequence_multi
	 * @param use_motion_in
	 * @param select_new_in
	 * @param min_confidence
	 * @param lma_horizon
	 * @param tilesX
	 * @param range
	 * @param remain
	 * @param passes
	 * @param debugLevel
	 * @return
	 */
	public static int [][] filter5Targets( // should work for motion vectors and target coordinates returns index of selected target or -1 for unselected
			final double [][][]   motion_sequence, // may be null
			final double [][][][] target_sequence_multi, // not null
			// if     use motion and select_new will only consider tiles (and compare motion scores in motion_sequence) that have nulls in target_sequence_multi[nseq][ntile]
			// if not use motion and select_new will only consider (and compare scores) tiles that have [RSLT_CENTERED] = 0.0. After selection will mark those as 
			// if not select_new (assumes not  use motion) will return best (and good) target indices
			final boolean         use_motion_in,     // true - use motion vectors confidence, false - use target confidence
			final boolean         select_new_in,     // true - use only untested tiles, false - use good tiles 
			// when using good ones - will select first good one (best if correctly ordered)
			final double          min_confidence, // 0 OK
			final double          lma_horizon, // target below horizon
			final int             tilesX,
			final int             range, // 1 or 2
			final int     []      remain,
			final int     []      passes, // debugging - number of passes required
			final int             debugLevel){
		final boolean  use_motion = use_motion_in; // By Claude on 05/07/2026: decouple from motion_sequence presence
		final boolean  select_new = select_new_in; // By Claude on 05/07/2026: decouple from motion_sequence
		boolean debug_now = (debugLevel<5);
		final int num_seq = target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
		final int tilesY = num_tiles/ tilesX;
		final int [][] filter5 = new int [num_seq][num_tiles];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int dbg_tile = debug_now ?(50+38*80) : -1;
		final int dbg_seq0 = debug_now ? 20: -1; 
		final int dbg_seq1 = debug_now ? 21: -1; 
		final int ihorizon = (lma_horizon > 0) ?( (int) Math.ceil(lma_horizon/GPUTileProcessor.DTT_SIZE)) : -1; // tiles with tileY >= ihorizon are removed
		if (dbg_tile >=0) {
			System.out.println("filter5Targets()):00: use_motion="+use_motion+", select_new="+select_new+", ihorizon="+ihorizon);
		}
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(tilesX, tilesY);
					boolean [] prohibit = new boolean [num_tiles];
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						Arrays.fill(filter5[nSeq], -1);
						double [][] motion = (motion_sequence != null) ? motion_sequence[nSeq] : null;
						int num_total = 0;
						Arrays.fill(prohibit, false);
						// set initially prohibited tiles - ones not to consider when competing
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							if ((motion != null) && ((motion[ntile] == null) || !(motion[ntile][CuasMotionLMA.RSLT_MSCORE] >= min_confidence))) { // defined: good or bad
								if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
									System.out.println("filter5Targets()):0: ntile="+ntile+", nSeq="+nSeq);
								}
								prohibit[ntile] = true;
							}  else if (select_new){
								if (use_motion ) {
									if (motion != null) { // original path: prohibit already-tested tiles
										if (target_sequence_multi[nSeq][ntile] != null) {
											prohibit[ntile] = true;
										}
									} else { // By Claude on 05/07/2026: motion==null, find FAIL_PENDING+CENTERED_NO sentinels
										boolean has_pending = false;
										double [][] targets = target_sequence_multi[nSeq][ntile];
										if (targets != null) {
											for (int i = 0; i < targets.length; i++) if (targets[i] != null) {
												if (targets[i][CuasMotionLMA.RSLT_FAIL]     == CuasMotionLMA.FAIL_PENDING &&
													targets[i][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO) {
													has_pending = true;
													break;
												}
											}
										}
										prohibit[ntile] |= !has_pending;
									}
								} else { // !use_motion - find if it has at least one good with that have [RSLT_CENTERED] = 0.0
									boolean prohibit_this = true;
									double [][] targets = target_sequence_multi[nSeq][ntile]; 
									if (targets != null) {
										for (int i = 0; i < targets.length; i++) if (targets[i] != null) {
											if ((targets[i][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) && (targets[i][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO)) {
												prohibit_this = false; // still have some potential targets that were not tried in centered mode	
											}
										}
									}
									prohibit[ntile] |= prohibit_this;
								}

							} else { // valid motion (or motion_sequence==null), not select new - prohibit if no good targets for this tile
								boolean prohibit_this = true;
								double [][] targets = target_sequence_multi[nSeq][ntile]; 
								if (targets != null) {
									for (int i = 0; i < targets.length; i++)  if (targets[i] != null){
										if ((targets[i][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
												(targets[i][CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED)) {
											prohibit_this = false; // still have some potential targets that were not tried in centered mode
											break;
										}
									}
								}
								prohibit[ntile] |= prohibit_this;
							}
						}
						// is it still used?
						if ((ihorizon > 0) && (ihorizon < tilesY)) {
							Arrays.fill(prohibit, ihorizon*tilesX, num_tiles, true);
						}
						if ((nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
							System.out.println("filter5Targets()):2: nSeq="+nSeq);
						}
						int num_this_pass = 0;
						int npass = 0;
						do {
							npass++;
							num_this_pass = 0;
							for (int tileY = 0; tileY < tilesY; tileY++) {
								for (int tileX = 0; tileX < tilesX; tileX++) {
									int ntile = tileX + tilesX * tileY;
									if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
										System.out.println("filter5Targets()):1: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+", num_this_pass="+num_this_pass+", num_total="+num_total);
									}
									if (!prohibit[ntile]) { // targets[ntile][conf_index] >0 here by the prohibit preset
										boolean ismax = true;
										if (select_new) {
											if (use_motion) {
												if (motion != null) {
													check_max_motion:{
														double cval = motion[ntile][CuasMotionLMA.RSLT_MSCORE] - min_confidence;
														if (cval <= 0) {
															ismax = false;
															prohibit[ntile] = true; // not needed
															break check_max_motion;
														}
														for (int dy = -range; dy <= range; dy++) {
															for (int dx = -range; dx <= range; dx++) {
																int indx = tn.getNeibIndex(ntile, dx, dy);
																if ((indx >= 0) && !prohibit[indx]) {
																	double val = motion[indx][CuasMotionLMA.RSLT_MSCORE]- min_confidence;
																	if (val > cval) {
																		ismax = false;
																		break check_max_motion;
																	}
																}
															}
														}
														filter5[nSeq][ntile] = 0;
													}
												} else { // By Claude on 05/07/2026: motion==null, rank FAIL_PENDING+CENTERED_NO sentinels by QSCORE
													double [][] targets = target_sequence_multi[nSeq][ntile];
													if (targets != null) {
														int best_pending = -1;
														check_max_pending:{
															for (int ntarg = 0; ntarg < targets.length; ntarg++) if (targets[ntarg] != null &&
																	targets[ntarg][CuasMotionLMA.RSLT_FAIL]     == CuasMotionLMA.FAIL_PENDING &&
																	targets[ntarg][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO) {
																double qval = targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
																if ((best_pending < 0) || (qval > targets[best_pending][CuasMotionLMA.RSLT_QSCORE])) {
																	best_pending = ntarg;
																}
															}
															if (best_pending < 0) {
																ismax = false;
																prohibit[ntile] = true;
																break check_max_pending;
															}
															double cval = targets[best_pending][CuasMotionLMA.RSLT_QSCORE] - min_confidence;
															if (cval <= 0) {
																ismax = false;
																prohibit[ntile] = true;
																break check_max_pending;
															}
															for (int dy = -range; dy <= range; dy++) {
																for (int dx = -range; dx <= range; dx++) {
																	int ntile1 = tn.getNeibIndex(ntile, dx, dy);
																	if ((ntile1 >= 0) && !prohibit[ntile1]) {
																		double [][] other_targets = target_sequence_multi[nSeq][ntile1];
																		if (other_targets != null) {
																			for (int ntarg = 0; ntarg < other_targets.length; ntarg++) if (other_targets[ntarg] != null &&
																					other_targets[ntarg][CuasMotionLMA.RSLT_FAIL]     == CuasMotionLMA.FAIL_PENDING &&
																					other_targets[ntarg][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO) {
																				double val = other_targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
																				if (val > cval + min_confidence) {
																					ismax = false;
																					break check_max_pending;
																				}
																			}
																		}
																	}
																}
															}
															filter5[nSeq][ntile] = best_pending;
														} // check_max_pending
													} else {
														prohibit[ntile] = true;
													}
												}
											} else { // not using motion, only consider non-centered good tiles
												double [][] targets = target_sequence_multi[nSeq][ntile]; 
												if (targets !=  null) {
													// find the best non-centered good target in this tile
													int best_targ = -1;
													check_max_target:{
														for (int ntarg = 0; ntarg < targets.length; ntarg++) if ((targets[ntarg] != null) &&
																(targets[ntarg][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
																(targets[ntarg][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO)) {
															double cval = targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
															if ((cval > 0) && ((best_targ <0) || (cval > targets[best_targ][CuasMotionLMA.RSLT_QSCORE]))) {
																best_targ = ntarg;
															}
														}
														if (best_targ < 0) {
															ismax = false;
															prohibit[ntile] = true; // not needed
															break check_max_target; 
														}
														double cval = targets[best_targ][CuasMotionLMA.RSLT_QSCORE];
														for (int dy = -range; dy <= range; dy++) {
															for (int dx = -range; dx <= range; dx++) {
																int ntile1 = tn.getNeibIndex(ntile, dx, dy);
																if ((ntile1 >= 0) && !prohibit[ntile1]) {
//																	targets =  target_sequence_multi[nSeq][ntile]; // other targets
																	double [][] other_targets = target_sequence_multi[nSeq][ntile1]; // other targets
																	if (other_targets != null) {
																		for (int ntarg = 0; ntarg < other_targets.length; ntarg++) if ((other_targets[ntarg] != null) &&
																				(other_targets[ntarg][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
																				(other_targets[ntarg][CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_NO)) {
																			double val = other_targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
																			if (val > cval) {
																				ismax = false;
																				break check_max_target;
																			}
																		}

																	}
																}
															}
														}
														filter5[nSeq][ntile] = best_targ;
													} // check_max_target:{
												} else {
													prohibit[ntile] = true; // probably not needed
												}
											}
										} else {//if (select_new) find best good target (assuming non-motion)
											double [][] targets = target_sequence_multi[nSeq][ntile]; 
											if (targets !=  null) {
												// find the best good target in this tile ?
												int best_targ = -1;
												check_max_target_any:{
													for (int ntarg = 0; ntarg < targets.length; ntarg++) if ((targets[ntarg] != null) &&
															(targets[ntarg][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
															(targets[ntarg][CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED)) {
														double cval = targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
														if ((cval > 0) && ((best_targ <0) || (cval > targets[best_targ][CuasMotionLMA.RSLT_QSCORE]))) {
															best_targ = ntarg;
														}
													}
													if (best_targ < 0) {
														ismax = false;
														prohibit[ntile] = true; // not needed
														break check_max_target_any; 
													}
													double cval = targets[best_targ][CuasMotionLMA.RSLT_QSCORE];
													for (int dy = -range; dy <= range; dy++) {
														for (int dx = -range; dx <= range; dx++) {
															int ntile1 = tn.getNeibIndex(ntile, dx, dy);
															if ((ntile1 >= 0) && !prohibit[ntile1]) {
//																targets =  target_sequence_multi[nSeq][ntile1]; // other targets
																double [][] other_targets = target_sequence_multi[nSeq][ntile1]; // other targets
																if (other_targets != null) {
																	for (int ntarg = 0; ntarg < other_targets.length; ntarg++) if ((other_targets[ntarg] != null) &&
																			(other_targets[ntarg][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) &&
																			(other_targets[ntarg][CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED)) {
																		double val = other_targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
																		if (val > cval) {
																			ismax = false;
																			break check_max_target_any;
																		}
																	}
																}
															}
														}
													}
													filter5[nSeq][ntile] = best_targ;
												} // check_max_target_any
											}
										} //if (select_new) else
										if (ismax) {
// 											filter5[nSeq][ntile] = true; // ismax; already set
											// prohibit all around, including this one
											for (int dy = -range; dy <= range; dy++) {
												for (int dx = -range; dx <= range; dx++) {
													int indx = tn.getNeibIndex(ntile, dx, dy);
													if (indx >= 0) { // && ((filtered == null) || !filtered[nSeq][indx])){
														prohibit[indx] = true;
													}
												}
											}
											num_this_pass++;
										}
									} // if (!prohibit[ntile]) { // targets[ntile][conf_index] >0 here by the prohibit preset
								} // for (int tileX = 0; tileX < tilesX; tileX++)
							} // for (int tileY = 0; tileY < tilesY; tileY++) {
							num_total+=num_this_pass;
						} while (num_this_pass > 0);
						if (remain != null) {
							remain[nSeq] = num_total;
						}
						if (passes != null) {
							passes[nSeq] = npass;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// By Claude on 05/07/2026: also mark FAIL_PENDING sentinels when use_motion=true, motion==null
		if (select_new && (!use_motion || (motion_sequence == null))) {
			ai.set(0);
			// mark selected targets as "used" (CENTERED_USED): FAIL_NONE+CENTERED_NO or FAIL_PENDING+CENTERED_NO
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
							for (int ntile = 0; ntile < num_tiles; ntile++) { // if (target_sequence[nSeq][ntile] != null) {
								if ((ntile == dbg_tile)&& (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
									System.out.println("filter5Targets()):4: filter5["+nSeq+"]["+ntile+"]="+filter5[nSeq][ntile]);
								}

								if (filter5[nSeq][ntile] >= 0) {
									target_sequence_multi[nSeq][ntile][filter5[nSeq][ntile]][CuasMotionLMA.RSLT_CENTERED] = CuasMotionLMA.CENTERED_USED;
									if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
										System.out.println("target_sequence_multi["+nSeq+"]["+ntile+"]["+filter5[nSeq][ntile]+"]["+CuasMotionLMA.RSLT_CENTERED+"] ="+
									target_sequence_multi[nSeq][ntile][filter5[nSeq][ntile]][CuasMotionLMA.RSLT_CENTERED]);
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return filter5;
	}
	
	// mark all "used" targets (non-centered, after all centered are added) as failed 
	public static void failUsed(
			final double [][][][] target_sequence) {
		final int num_seq = target_sequence.length;
		final int num_tiles = target_sequence[0].length;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		ai.set(0);
		// mark selected (non-centered) targets as "used"
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++)  if (target_sequence[nSeq][ntile] != null) {
							for (int ntarg = 0; ntarg < target_sequence[nSeq][ntile].length; ntarg++) {
								double [] target = target_sequence[nSeq][ntile][ntarg];
								if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) && (target[CuasMotionLMA.RSLT_CENTERED] == CuasMotionLMA.CENTERED_USED)) {
									target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_USED;
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
	}
	

	public static void getEffectiveStrengthLMA(
			final double [][][][] target_sequence_multi, // modifies certain fields (scores)
			final double          target_strength,
			final double [][]     target_frac, // pairs - strength, minimal fraction for that strength
			final double          lma_rms, // =       1.5;  // Maximal RMS (should always match, regardless if A)
			final double          lma_arms, // =      0.06; // Maximal absolute RMS (should match one of cuas_lma_arms OR cuas_lma_rrms (0.484)
			final double          lma_rrms, // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
			final double          lma_mina, // =      1.0;  // Minimal A (amplitude)
			final double          lma_maxr, // =      5.0;  // Minimal K (overshoot) = 3.0
            final double          lma_minr1,// =      1.0;  // Minimal R1 (radius of positive peak) 
			final double          lma_mink, // =      0.0;  // Minimal K (overshoot) = 1.0
			final double          lma_maxk, // =      5.0;  // Minimal K (overshoot) = 3.0
			final double          lma_a2a,
			final double          max_mismatch, 
			final double          slow_fast_mismatch,
			final boolean         fail_mismatch,
			final double          offcenter,
			final double []       noise_map,
			final double          noise_max,
			final double          lma_horizon, // horizon as maximal pixel Y
			final int             tilesX	) {
		// if centroid maximum is safely good, ignore lma_a2a ratio
		final double SAFE_CENT_MX = 3 * target_strength; // Find a more elegant solution
		final int num_seq = target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
//		final double center_scale = 0.25*Math.PI/GPUTileProcessor.DTT_SIZE;
		final double center_scale = 0.125*Math.PI/GPUTileProcessor.DTT_SIZE/offcenter;
		final int dbg_tile = -(34 + 34*80);
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							if (ntile == dbg_tile) {
								System.out.println("filterTargetsLMA(): ntile = "+ntile);
							}
							double [][] targets = target_sequence_multi[nSeq][ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) if (targets[ntarg] != null){
									double [] lma_rslts = targets[ntarg];
//									if ((lma_rslts != null) && (Double.isNaN(lma_rslts[CuasMotionLMA.RSLT_FAIL]) || (lma_rslts[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE))) {
									
									// this method should be called only when [CuasMotionLMA.RSLT_FAIL] is not yet defined, only for the new LMA data is calculated    
									if ((lma_rslts != null) && Double.isNaN(lma_rslts[CuasMotionLMA.RSLT_FAIL])) {
										lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_NONE;
										double CENT_STR = lma_rslts[CuasMotionLMA.RSLT_CENT_MX];
										double CENT_FRAC= lma_rslts[CuasMotionLMA.RSLT_CENT_F];
										double A =        lma_rslts[CuasMotionLMA.RSLT_A];
										double RMSE =     lma_rslts[CuasMotionLMA.RSLT_RMS];
										double RMSE_A =   lma_rslts[CuasMotionLMA.RSLT_RMS_A];
										double R1 =       lma_rslts[CuasMotionLMA.RSLT_R1];
										double MAX2A =    lma_rslts[CuasMotionLMA.RSLT_MAX2A];
										double Y = (ntile/tilesX +0.5) * GPUTileProcessor.DTT_SIZE + lma_rslts[CuasMotionLMA.RSLT_Y];
										double MM_BEFORE = lma_rslts[CuasMotionLMA.RSLT_MISMATCH_BEFORE];
										double MM_AFTER =  lma_rslts[CuasMotionLMA.RSLT_MISMATCH_AFTER];
										double x = lma_rslts[CuasMotionLMA.RSLT_X];
										double y = lma_rslts[CuasMotionLMA.RSLT_Y];
										double cxy = 1.0;
										/*
										 * Filtering by "far" (center belongs to the neighbor tile, not this one) worked well
										 * except very low above the skyline. In that case the tile below resulted in gross wrong motion vector
										 * and then the target correlation. The low targets are important, so it will make sense for an exception:
										 * allowing "far" there (recognizing by some values), including preselecting "border tiles", or moving target
										 * with its parameters, including motion vectors to that other tile. Will first just try easing "far" limitations
										 * AF 04/25/2026 
										 */
//										if ((Math.abs(x) > 0.5*GPUTileProcessor.DTT_SIZE) || (Math.abs(y) > 0.5*GPUTileProcessor.DTT_SIZE)) {
										if ((Math.abs(x) > offcenter*GPUTileProcessor.DTT_SIZE) || (Math.abs(y) > offcenter*GPUTileProcessor.DTT_SIZE)) {
											cxy = 0.0;
										} else {
//											double ax = Math.abs(x) - 0.25*GPUTileProcessor.DTT_SIZE;
//											double ay = Math.abs(y) - 0.25*GPUTileProcessor.DTT_SIZE; // was same as ax 04/09/2026 
											double ax = Math.abs(x) - 0.5*offcenter*GPUTileProcessor.DTT_SIZE;
											double ay = Math.abs(y) - 0.5*offcenter*GPUTileProcessor.DTT_SIZE; // was same as ax 04/09/2026 
											if (ax > 0) {
												cxy *= Math.cos(ax * center_scale);
											}
											if (ay > 0) {
												cxy *= Math.cos(ay * center_scale);
											}
										}
										//	double cxy= Math.max(0,Math.cos(x * center_scale)) * Math.max(0,Math.cos(y * center_scale));
										boolean failed = true;
										try_failures: {
											// start with centroid tests
											if (CENT_STR < target_strength) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_CENT_STR;
												break try_failures; // cemtroid maximum is too weak
											}
											for (double [] frac_pair : target_frac) {
												if ((CENT_STR > frac_pair[0]) && (CENT_FRAC < frac_pair[1])) {
													lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_CENT_FRAC;
													break try_failures; // centroid fraction is too small
												}
											}
											if (lma_rslts[CuasMotionLMA.RSLT_ITERS] < 0) { // fitting has not failed
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_LMA;
												break try_failures; // LMA failed
											}

											if (A < lma_mina) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_A_LOW;
												break try_failures; // too weak
											}

											if (MAX2A < lma_a2a) {
												if (CENT_STR < SAFE_CENT_MX) {
													lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_ACENT;
													break try_failures; // ratio of maximal value to A is too low
												}
											}

											if (RMSE > lma_rms) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_RMSE;
												break try_failures; // too high RMSE regardless of A
											}

											if ((RMSE > lma_arms) && (RMSE_A > lma_rrms)) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_RMSE_R;
												break try_failures; // too high RMSE
											}

											if (lma_rslts[CuasMotionLMA.RSLT_R0] > lma_maxr) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_R0_HIGH;
												break try_failures; // Radius is too high
											}
											if (R1 < lma_minr1) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_R1_LOW;
												break try_failures; // Inner radius too small
											}
											if (lma_rslts[CuasMotionLMA.RSLT_K] < lma_mink) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_K_LOW;
												break try_failures; // K is too low
											}

											if (lma_rslts[CuasMotionLMA.RSLT_K] > lma_maxk) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_K_HIGH;
												break try_failures; // K is too high
											}
											if (fail_mismatch && !(MM_BEFORE <= max_mismatch) && !(MM_AFTER <= max_mismatch)) { // on both ends, handles NaN
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_MISMATCH;
												break try_failures; // Mismatch is too high
											}
											if ((lma_horizon >0) && (Y > lma_horizon)) {
												lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_HORIZON;
												break try_failures; // below horizon line
											}
											if (cxy==0) {
												// do not fail if average image has high variations for this tile
												// near horizon allow targets far ftom the tile center. AF 04/26/2026
												if ((noise_map == null) || (noise_map[ntile] < noise_max)) {
													lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_FAR;
													break try_failures; // below horizon line
												}
												// see if it is completely outside 
												if ((Math.abs(x) >= (GPUTileProcessor.DTT_SIZE-1)) ||
														(Math.abs(y) >= (GPUTileProcessor.DTT_SIZE-1))) {
													lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_FAR;
													break try_failures; // below horizon line
												}
												
											}
											failed = false; // all tests passed
										}

										double [] quality_factors = new double [IMPORTANCE_LENGTH];
										if (!failed) {
											lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_NONE;
											double    inv_rms =         1.0 / RMSE;
											double    inv_rms_lim =     1.0 / lma_arms;
											double    inv_rel_rms =     1.0 / RMSE_A; // A/RMSE;
											double    inv_rel_rms_lim = 1.0 / lma_rrms;

											quality_factors[IMPORTANCE_A] =     Math.max(0,(A-            lma_mina)/        lma_mina);
											quality_factors[IMPORTANCE_RMS] =   Math.max(0,(inv_rms -     inv_rms_lim)/     inv_rms_lim); // only >0 for small max-es
											quality_factors[IMPORTANCE_RMS_A] = Math.max(0,(inv_rel_rms - inv_rel_rms_lim)/ inv_rel_rms_lim); // only >0 for small max-es

											quality_factors[IMPORTANCE_CENTER] =   cxy*cxy;

//											if (MM_BEFORE <= good_mismatch) {
//												quality_factors[IMPORTANCE_MISMATCH] += Math.max(0,(good_mismatch - MM_BEFORE)/good_mismatch); // 0 .. 1
//											}
//											if (MM_AFTER <= good_mismatch) {
//												quality_factors[IMPORTANCE_MISMATCH] += Math.max(0,(good_mismatch - MM_AFTER)/good_mismatch); // 0 .. 1
//											}
											
											
											
///											quality_factors[IMPORTANCE_MATCH_LEN] = Math.pow(lma_rslts[CuasMotionLMA.RSLT_MATCH_LENGTH], match_len_pwr);// 1.0 if in every scene
///											quality_factors[IMPORTANCE_TRAVEL] =   Math.max((lma_rslts[CuasMotionLMA.RSLT_SEQ_TRAVEL] - seq_travel)/seq_travel, 0);
											//			final double          seq_travel,

										}
										// save quality factors
										lma_rslts[CuasMotionLMA.RSLT_QA] =         quality_factors[IMPORTANCE_A];
										lma_rslts[CuasMotionLMA.RSLT_QRMS] =       quality_factors[IMPORTANCE_RMS];
										lma_rslts[CuasMotionLMA.RSLT_QRMS_A] =     quality_factors[IMPORTANCE_RMS_A];
///										lma_rslts[CuasMotionLMA.RSLT_QMATCH] =     quality_factors[IMPORTANCE_MISMATCH];
										lma_rslts[CuasMotionLMA.RSLT_QCENTER] =    quality_factors[IMPORTANCE_CENTER];
///										lma_rslts[CuasMotionLMA.RSLT_QMATCH_LEN] = quality_factors[IMPORTANCE_MATCH_LEN];
///										lma_rslts[CuasMotionLMA.RSLT_QTRAVEL] =    quality_factors[IMPORTANCE_TRAVEL];
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
		
	public static int [] getScore(
			final double [][][][] target_sequence_multi,// modifies certain fields (scores)
			final double          importance_limit,
			final double          importance_power,     // Raise each factor to this power before combining
			final double []       importance,           // for now (each - squared?): [0] - Amplitude (A/A0), 1 - RMS (RMS0/RMS), 2 - RRMS((RMS/A0) / (RMS/A)
			final double          slow_score,           // multiply total score for targets detected in slow target mode
			final int             tilesX	) {
		double sw = 0;
		for (int i = 0; i < importance.length; i++) {
			sw += importance[i];
		}
		for (int i = 0; i < importance.length; i++) {
			importance[i]/=sw;
		}
		// if centroid maximum is safely good, ignore lma_a2a ratio
		final int num_seq = target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
		int [] remain = new int [num_seq];
		final int dbg_tile = -(12 + 23*80);
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++)  { // if (target_sequence_multi[nSeq][ntile] != null) {
							if (ntile == dbg_tile) {
								System.out.println("filterTargetsLMA(): ntile = "+ntile);
							}
							double [][] targets =target_sequence_multi[nSeq][ntile];
							if (targets != null ) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] lma_rslts = targets[ntarg];
									if (lma_rslts != null) {
										lma_rslts[CuasMotionLMA.RSLT_QSCORE] = 0; // CuasMotionLMA.FAIL_NONE;
										if (lma_rslts[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE) {
											double [] quality_factors = new double [IMPORTANCE_LENGTH];
											quality_factors[IMPORTANCE_A] =        lma_rslts[CuasMotionLMA.RSLT_QA];      
											quality_factors[IMPORTANCE_RMS] =      lma_rslts[CuasMotionLMA.RSLT_QRMS];    
											quality_factors[IMPORTANCE_RMS_A] =    lma_rslts[CuasMotionLMA.RSLT_QRMS_A];  
											quality_factors[IMPORTANCE_MISMATCH] = lma_rslts[CuasMotionLMA.RSLT_QMATCH];  
											quality_factors[IMPORTANCE_CENTER] =   lma_rslts[CuasMotionLMA.RSLT_QCENTER]; 
											quality_factors[IMPORTANCE_MATCH_LEN]= lma_rslts[CuasMotionLMA.RSLT_QMATCH_LEN]; 
											quality_factors[IMPORTANCE_TRAVEL]=    lma_rslts[CuasMotionLMA.RSLT_QTRAVEL]; 
											
											for (int i = 0; i < importance.length; i++) {
												if (Double.isNaN(quality_factors[i])) {
													quality_factors[i] = 0.0;
												}
												quality_factors[i] = Math.min(quality_factors[i], importance_limit);
											}								

											double quality = 0;
											for (int i = 0; i < importance.length; i++) {
												if (!Double.isNaN(quality_factors[i])) {
													double factor_contrib = Math.pow(quality_factors[i], importance_power);
													quality += importance[i] * factor_contrib;
												}
											}
											quality = Math.pow(quality, 1.0/importance_power);
											lma_rslts[CuasMotionLMA.RSLT_QSCORE] = quality;
											remain[nSeq]++;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return remain;
	}

	
	// add a separate filter to remove almost duplicates and reorder
	public static int addNewResults(
			final double [][][][] target_sequence_multi, // will only process non-nulls here
			final double [][][][] new_sequence_multi,
			final int             num_best, // if >0, limit number of best results to add 
			final int             when_iter,
			final int             debugLevel) {
		final int num_seq =   target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
		boolean debug_now = (debugLevel > -5); // NEVER
		final int dbg_tile = debug_now ?(56+38*80) : -1;
		final int dbg_seq0 = debug_now ? 197: -1; 
		final int dbg_seq1 = debug_now ? 197: -1; 		
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final AtomicInteger anew = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) { // if (target_sequence[nSeq][ntile] != null) {
							double [][] tg =  target_sequence_multi[nSeq][ntile];
							double [][] ns = new_sequence_multi[nSeq][ntile];
							boolean debug_this = ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1));
							if (debug_this) {
							    System.out.println("\naddNewResults()): ntile="+ntile+", nSeq="+nSeq+", num_best="+num_best);
							}
							if ((tg != null) || (ns != null)) {
								if (tg == null) tg = new double [0][];
								if (ns == null) ns = new double [0][];
								int new_len = ns.length;
								if (num_best > 0) {
									new_len = Math.min(new_len,  num_best);
								}
								int ct_len = tg.length + new_len;
								if (debug_this) {
								    System.out.println("addNewResults()): tg.length="+tg.length+", ns.length="+ns.length+", new_len="+new_len+", ct_len="+ct_len);
								}

								if (ct_len > 0) {
									double [][] ct = new double [ct_len][];
									for (int i = 0; i < tg.length; i++) {
										ct[i] = tg[i];
									}
									for (int i = 0; i < new_len; i++) {
										int idest = i + tg.length; 
										ct[idest] = ns[i].clone();
										if (!Double.isNaN(ct[idest][CuasMotionLMA.RSLT_FAIL]) && (when_iter >= 0)) {
											ct[idest][CuasMotionLMA.RSLT_WHEN] = when_iter;
										}
										anew.getAndIncrement();
									}
									target_sequence_multi[nSeq][ntile] = ct;
									
								} else { // need to put 0-length to mark tile as tested, not to retry again
									target_sequence_multi[nSeq][ntile] = new double[0][];
								}
								if (debug_this) {
									tg =  target_sequence_multi[nSeq][ntile];
									for (int ntg = 0; ntg < tg.length; ntg++) {
										System.out.println("\ntg="+ntg+"("+tg.length+")");
										for (int i = 0; i < 5; i++) {
											System.out.print(tg[ntg][i]+", ");
										}
										System.out.println();
									}
								}									
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return anew.get();
	}
	
	public static double [][][][] combineFastSlow(
			final double [][][][] targets_fast, // will only process non-nulls here
			final double [][][][] targets_slow,
			final double          scale_slow){    // if >0, limit number of best results to add 
		final int num_seq =   targets_fast.length;
		final int num_tiles = targets_fast[0].length;
		final double [][][][] targets_combo = new double[num_seq][num_tiles][][];
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) { // if (target_sequence[nSeq][ntile] != null) {
							double [][] tf = targets_fast[nSeq][ntile];
							double [][] ts = targets_slow[nSeq][ntile];
							if ((tf != null) || (ts != null)) {
								if (tf == null) tf = new double [0][];
								if (ts == null) ts = new double [0][];
								double [][] tc = new double [tf.length+ts.length][];
								for (int i = 0; i < tf.length; i++) if (tf[i] != null){
									tc[i] = tf[i].clone();
									tc[i][CuasMotionLMA.RSLT_SLOW] = 0;
								}
								for (int i = 0; i < ts.length; i++) if (ts[i] != null){
									tc[i + tf.length] = ts[i].clone();
									tc[i + tf.length][CuasMotionLMA.RSLT_SLOW] = 1;
									tc[i + tf.length][CuasMotionLMA.RSLT_QSCORE] *= scale_slow;
								}
								targets_combo[nSeq][ntile] = tc;
							}							
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return targets_combo;
	}

	
	
	
	
	
	
	public static int [] getRemain(
			final double [][][][] target_sequence_multi,
			int []                num_all_in,
			int []                num_noncentered_in,
			int []                num_good_in,
			int []                num_bad_in) {
		final int num_seq =   target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
		final int [] num_all =         (num_all_in !=   null) ? num_all_in :   new int [num_seq];
		final int [] num_good =        (num_good_in !=  null) ? num_good_in :  new int [num_seq];
		final int [] num_bad =         (num_bad_in !=   null) ? num_bad_in :   new int [num_seq];
		final int [] num_noncentered = (num_noncentered_in != null) ? num_noncentered_in : new int [num_seq];
		Arrays.fill(num_all,   0);       // tiles that have good, bad or undefined targets
		Arrays.fill(num_good,  0);       // good centered targets (may be multiple per tile)
		Arrays.fill(num_bad,   0);       // bad centered targets (may be multiple per tile)
		Arrays.fill(num_noncentered, 0); // not centered (and not yet used for centering) targets
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] tsm = target_sequence_multi[nSeq][ntile];
							if (tsm != null) {
								num_all[nSeq]++;
								for (int ntarg = 0; ntarg < tsm.length; ntarg++) if (tsm[ntarg] != null){
									double ts_fail = tsm[ntarg][CuasMotionLMA.RSLT_FAIL];
									int centered = (int)  tsm[ntarg][CuasMotionLMA.RSLT_CENTERED];
									if (centered != CuasMotionLMA.CENTERED_USED) {
										if (ts_fail == CuasMotionLMA.FAIL_PENDING) { // By Claude on 05/07/2026: unprocessed sentinel
											num_noncentered[nSeq]++;
										} else if ((centered == CuasMotionLMA.CENTERED_NO) && (ts_fail == CuasMotionLMA.FAIL_NONE)) {
											num_noncentered[nSeq]++;
										} else if (ts_fail == CuasMotionLMA.FAIL_NONE) {
											num_good[nSeq]++;
										} else {
											num_bad[nSeq]++;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return getTotals(
				num_all,   // int [] num_all,
				num_noncentered, // int [] num_undef,
				num_good,  // int [] num_good,
				num_bad);  // int [] num_bad)
	}
	
	
	public static int [] getRemain(
			final double [][][]   motion_sequence,
			final double [][][][] target_sequence_multi,
			int []                num_all_in,
			int []                num_undef_in,
			int []                num_good_in,
			int []                num_bad_in) {
		final int num_seq =   target_sequence_multi.length;
		final int num_tiles = target_sequence_multi[0].length;
		final int [] num_all =   (num_all_in !=   null) ? num_all_in :   new int [num_seq];
		final int [] num_good =  (num_good_in !=  null) ? num_good_in :  new int [num_seq];
		final int [] num_bad =   (num_bad_in !=   null) ? num_bad_in :   new int [num_seq];
		final int [] num_undef = (num_undef_in != null) ? num_undef_in : new int [num_seq];
		Arrays.fill(num_all,   0); // tiles that have good, bad or undefined targets
		Arrays.fill(num_good,  0); // good targets (may be multiple per tile)
		Arrays.fill(num_bad,   0); // bad targets (may be multiple per tile)
		Arrays.fill(num_undef, 0); // exist in motion_sequence, but not in target_sequence_multi
		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 nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) if (motion_sequence[nSeq][ntile] != null){
							double [][] tsm = target_sequence_multi[nSeq][ntile];
							if (tsm != null) {
								num_all[nSeq]++;
								for (int ntarg = 0; ntarg < tsm.length; ntarg++) if (tsm[ntarg] != null){
									double ts_fail = tsm[ntarg][CuasMotionLMA.RSLT_FAIL];
									if (ts_fail == CuasMotionLMA.FAIL_NONE) {
										num_good[nSeq]++;
									} else {
										num_bad[nSeq]++;
									}
								}
							} else {
								num_undef[nSeq]++;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return getTotals(
				num_all,   // int [] num_all,
				num_undef, // int [] num_undef,
				num_good,  // int [] num_good,
				num_bad);  // int [] num_bad)
	}

	final public double [][][] resolveTargetsConflictsMulti(
			boolean           batch_mode,
			float [][]        fpixels,
			double [][][][]   targets_multi, // targets_good, // all targets, including conflicting
			int               debugLevel){
		return resolveTargetsConflictsMulti(
				clt_parameters,  // CLTParameters     clt_parameters,
				batch_mode,      // boolean           batch_mode,
				parentCLT,       // QuadCLT           parentCLT,          //
				fpixels,         // float [][]        fpixels,
				this,            // CuasMotion        cuasMotion,
				targets_multi,   //  double [][][][]   targets_multi, // targets_good, // all targets, including conflicting
				slice_titles,    // String []         slice_titles,
				debugLevel);     // int               debugLevel)
	}
	
	final static public double [][][] resolveTargetsConflictsMulti(
			CLTParameters     clt_parameters,
			boolean           batch_mode,
			QuadCLT           parentCLT,          //
			float [][]        fpixels,
			CuasMotion        cuasMotion,
			double [][][][]   targets_multi, // targets_good, // all targets, including conflicting
			String []         slice_titles,
			int               debugLevel){
		int     corr_offset =    clt_parameters.imp.cuas_corr_offset;
		int     corr_pairs =     clt_parameters.imp.cuas_corr_pairs;
		boolean half_step =      clt_parameters.imp.cuas_half_step; // true;
		int     fin_range =      clt_parameters.imp.cuas_fin_range;

		double [][] target_frac = new double [clt_parameters.imp.cuas_target_frac.length][2];
		double  good_mismatch =  clt_parameters.imp.cuas_good_mismatch;  // 0.4;
		double  slow_fast_mismatch =  clt_parameters.imp.cuas_slow_fast_mismatch;  // 1.5; allow larger mismatch between slow and fast
		
		double  match_len_pwr =  clt_parameters.imp.cuas_match_len_pwr;   // raise matching length to this power for calculating score
	    double  target_horizon=  clt_parameters.imp.cuas_horizon;	    
	// Handling overall target scores    
	    
		double min_score_lma =   clt_parameters.imp.cuas_score_lma;
		int min_seq=             clt_parameters.imp.cuas_min_seq;    // 3;     // minimal number of consecutive key frames for the same target
		int enough_seq=          clt_parameters.imp.cuas_enough_seq; // 5;     // minimal number of consecutive key frames for the same target
		double seq_travel=       clt_parameters.imp.cuas_seq_travel; // 3.0;   // minimal diagonal of the bounding box that includes sequence to be considered "cuas_enough_seq". Filtering out atmospheric fluctuations
		double factor_lim =      clt_parameters.imp.cuas_factor_lim;
		double factor_pow =      clt_parameters.imp.cuas_factor_pow;
		double [] score_coeff =  clt_parameters.imp.cuas_score_coeff.clone();
		double  slow_score =     clt_parameters.imp.cuas_slow_score;
		boolean intermed_low =   clt_parameters.imp.cuas_intermed_low;    //true;
		boolean debug_more =     clt_parameters.imp.cuas_debug_more;
		for (int i = 0; i < target_frac.length; i++) {
			if (clt_parameters.imp.cuas_target_frac[i].length >= 2) {
				target_frac[i][0] = clt_parameters.imp.cuas_target_frac[i][0];
				target_frac[i][1] = clt_parameters.imp.cuas_target_frac[i][1];
			} else {
				System.out.println("testCuasScanMotion(): wrong format for a pair of strength, fraction values.");
			}
		}
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null);
		int start_frame = 0;
		int num_scenes = fpixels.length; // .getStack().getSize()- first_corr; // includes average
		int seq_length = corr_offset + corr_pairs;
		int corr_inc = half_step ? (corr_offset/2) : corr_offset;
		int num_corr_samples = (num_scenes - seq_length - start_frame) / corr_inc;
		int [] filter5_remain = new int [num_corr_samples];
		int [] num_all =        new int [num_corr_samples];
		int [] num_undef =      new int [num_corr_samples];
		int [] num_good =       new int [num_corr_samples];
		int [] num_bad =        new int [num_corr_samples];
		int [] passes = new int [num_corr_samples]; // debugging filter5
		// there could be many conflicting tiles in targets_good. Calculate scores and then filter to keep the best ones that do not conflict
		// take into account before/after matching
//		double interseq_scale =  0.5* corr_inc/corr_offset;  // multiply target velocity to get offset in the middle between the key frames
		double interseq_scale =  cuasMotion.getInterseqScale();  // multiply target velocity to get offset in the middle between the key frames
		
		if (debugLevel > -4) System.out.println("\nStarting resolution of conflicting tiles.");
		boolean         good_only=    false; // true;
		boolean         show_empty =  true; // false; // show scenes with no (valid) targets
		boolean         trim_nulls =  true;
		final double    duplicate_tolerance = clt_parameters.imp.cuas_duplicate_tolerance ; // 2.0; // (pix) Remove weaker maximums closer than this to a stronger one   		
		if (intermed_low && debug_more) {
			ImagePlus imp_in = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-BEFORE_RESOLVE_MULTI",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_in);
		}
		
		// Used non-centered tiles are not needed anymore, mark them as failed to simplify processing
		failUsed(targets_multi); //	final double [][][][] target_sequence) {
		
		sortMultiTargets(
				targets_multi,        // final double [][][][] target_multi,
				trim_nulls);          // final boolean         trim_nulls) { // trim null targets
		int [] num_duplicates = removeWeakerDuplicates( // needs sortMultiTargets and after
				targets_multi,        // final double [][][][] target_multi,
				duplicate_tolerance); // final double          pix_tolerance) 
		if (debugLevel > -4) System.out.println("Removed "+num_duplicates[1]+" good duplicate targets (total with bad - "+num_duplicates[0]+").");
		sortMultiTargets(
				targets_multi,        // final double [][][][] target_multi,
				trim_nulls);          // final boolean         trim_nulls) { // trim null targets
		
		if (intermed_low && debug_more) {
			ImagePlus imp_dup = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-AFTER_DUPLICATES",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_dup);
		}
		
		// Calculate sequences lengths
		// Calculate image x,y for each target half keyframe interval before the keyframe and half-interval after.
		getHalfBeforeAfterPixXY(
				targets_multi,       // final double [][][][] targets_multi,
				true,                // final boolean       good_only,
				interseq_scale,      // , // final double        interseq_scale,
				cuasMotion.tilesX,   // final int           tilesX,
				debugLevel);         //	final int           debugLevel) {
		// still calculate BA in addition to lengths
		
		// TODO: untested yet
		
		calculateMismatchBeforeAfter(
				targets_multi,       // final double [][][][]  targets_multi,
				true, // final boolean          good_only,
				cuasMotion.tilesX);   //  int           tilesX) {		
		
		calcMatchingTargetsLengths(  // calculate number of consecutive keyframes connected to each target
				targets_multi,       // final double [][][][] targets_multi,
				false,               // final boolean         calc_linked,
				// which is better here?
				good_mismatch, // max_mismatch,        // final double          max_mismatch,     // if <=0, do not calculate  mismatch_ba and filter
				slow_fast_mismatch, // final double          slow_fast_mismatch,
				match_len_pwr,     // final double          match_len_pwr, // 0.5; // raise matching length to this power for calculating score
				seq_travel,        // final double          seq_travel,
				cuasMotion.tilesX);   //final int             tilesX)
		
		
		if (intermed_low && debug_more) {
			ImagePlus imp_lengths = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-MATCHING_LENGTHS",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_lengths);
		}
		
				
// order/auto order with getEffectiveStrengthLMA()?		
//		int [] remain_es = 
		getScore( // calculates composite scenes
				targets_multi,     // final double [][][][] target_sequence_multi, // modifies certain fields (scores)
				factor_lim,        // final double        importance_limit,
				factor_pow,        // final double        importance_power, // Raise each factor to this power before combining
				score_coeff,       // final double []     importance, // for now (each - squared?): [0] - Amplitude (A/A0), 1 - RMS (RMS0/RMS), 2 - RRMS((RMS/A0) / (RMS/A)
				slow_score,        // final double          slow_score,           // multiply total score for targets detected in slow target mode
				cuasMotion.tilesX);            // final int         tilesX,

		getRemain(targets_multi, num_all, num_undef, num_good, num_bad);
		if (debugLevel > -4) printStats ("Resolving, after matching before/after", true,num_all, num_undef, num_good, num_bad);
		if (intermed_low  && debug_more) {
			ImagePlus imp_failures = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-SCORES_WITH_LENGTHS",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_failures);
		}
		
		// no extra recalculations here as was in pre-Multi?
		// Use here the real threshold to remove weak
		int [][] filter5 = filter5Targets( // will ignore failed tiles
				null,               // final double [][][] target_sequence,
				targets_multi,      // final double [][][][] target_sequence_multi,
				// if     use motion and select_new will only consider tiles (and compare motion scores in motion_sequence) that have nulls in target_sequence_multi[nseq][ntile]
				// if not use motion and select_new will only consider (and compare scores) tiles that have [RSLT_CENTERED] = 0.0
				// if not select_new (assumes not  use motion) will return best (and good) target indices
				false,              // final boolean       use_motion,     // true - use motion vectors confidence, false - use target confidence
				false,              // final boolean       select_new,     // true - use only untested tiles, false - use good tiles 
				0, // min_score_lma,      // double              min_confidence,
				target_horizon,     // final double        lma_horizon, // target below horizon
				cuasMotion.tilesX,  // final int           tilesX,
				fin_range,          // final int           range, // 1 or 2
				filter5_remain,     // final int     []    remain){
				passes,             // final int     []    passes, // debugging - numer of passes required
				1); // debugLevel);        // final int           debugLevel)
		if (debugLevel > -4) printStatsLine("After filter5 remaining non-conflicting targets", true,filter5_remain);

		if (intermed_low  && debug_more) {
			ImagePlus imp_filter5_lma = ShowDoubleFloatArrays.makeArrays(
					filter5, // double[][] pixels,
					cuasMotion.tilesX,
					cuasMotion.tilesY, 
					model_prefix+"-FILTER5_FINAL_R"+fin_range,   // String title,
					slice_titles);
			if (!batch_mode) {
				imp_filter5_lma.show();
			}
			parentCLT.saveImagePlusInModelDirectory(imp_filter5_lma);
		}
		
		// apply filter5 to targets_good
		findStrongerNeighbor(// does not modify "when"
				targets_multi,        // final double [][][][] target_multi,
				filter5,              // final boolean [][] filter5)
				true,                 // false,                // final boolean       mark_failed,
				cuasMotion.tilesX);   // final int           tilesX) {	
		if (intermed_low  && debug_more) {
			ImagePlus imp_neibs = showTargetSequence(
					targets_multi,         // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-STRONGER-NEIBS",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_neibs);  // ImagePlus   imp)
		}
		
		
		
		// now failed by neighbors are marked as non-NaN (tile index or -1), not as failed
		// here failed by stronger neighbors are also removed (balance weights if needed)
		// maybe also needed to remove all under threshold here

		sortMultiTargets( // stronger neibs will be incorrect
				targets_multi, // final double [][][][] target_multi,
				true); // trim_nulls);   // final boolean         trim_nulls) { // trim null targets
		// final with all multi-target data
		if (intermed_low) { // save always 
			ImagePlus imp_good = showTargetSequence(
					targets_multi,         // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-TARGETS-MULTI",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_good);  // ImagePlus   imp)
		}
		if (debugLevel > -4) System.out.println("Cutting below minimal score = "+min_score_lma);
		double [][][] target_single = convertFromMultiTarget(// single target per tile
				targets_multi,  // final double [][][][] target_multi) {
				min_score_lma,  //final double          minimal_score) {
				min_seq,        // final int             min_seq) { // minimal sequence length
				enough_seq,     // final int             enough_seq, // good regardless of scores
				seq_travel,     // final double          seq_travel,
				num_good);      // final int []          remain){
		if (debugLevel > -4) printStatsLine("converted to single targets per tile", true, num_good);

		if (intermed_low) { // save always
			ImagePlus imp_failures = showTargetSequence(
					target_single,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-TARGETS_SINGLE-FIRST",// String        title,
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_failures);
		}
		// Convert back to multi, recalculate lengths, re-run convertFromMultiTarget() to remove short sequences
		double [][][][] targets_multi2 = convertToMultiTarget(
				target_single); // final double [][][] target_sequence) {
		
		calcMatchingTargetsLengths(  // calculate number of consecutive keyframes connected to each target
				targets_multi2,      // final double [][][][] targets_multi,
				true,                // final boolean         calc_linked,
				// which is better here?
				good_mismatch, // max_mismatch,        // final double          max_mismatch,     // if <=0, do not calculate  mismatch_ba and filter
				slow_fast_mismatch, // final double          slow_fast_mismatch,
				match_len_pwr,     // final double          match_len_pwr, // 0.5; // raise matching length to this power for calculating score
				seq_travel,        // final double          seq_travel,
				cuasMotion.tilesX);   //final int             tilesX)
		if (intermed_low) { //  && debug_more) {
			ImagePlus imp_lengths = showTargetSequence(
					targets_multi2,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-MATCHING_LENGTHS2",// String        title,
					good_only,            // final boolean         good_only,
					show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_lengths);
		}

		target_single = convertFromMultiTarget(// single target per tile
				targets_multi2,  // final double [][][][] target_multi) {
				min_score_lma,  //final double          minimal_score) {
				min_seq,        // final int             min_seq) { // minimal sequence length
				enough_seq,     // final int             enough_seq, // good regardless of scores
				seq_travel,     // final double          seq_travel,
				num_good);      // final int []          remain){
		if (debugLevel > -4) printStatsLine("converted to single targets per tile after removing shorts", true, num_good);

		if (intermed_low) { // save always
			ImagePlus imp_failures = showTargetSequence(
					target_single,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-TARGETS_SINGLE-FINAL",// String        title,
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_failures);
		}
		return target_single; // targets_multi; // target_sequence; // non-overlap if consider marked stronger tiles
	}
		
	public static void findStrongerNeighbor(// does not modify "when"
			final double [][][][] target_multi,
			final int [][]        filter5,
			final boolean         mark_failed,
			final int             tilesX) {
		final int range = 2;
		final int num_seq = target_multi.length;
		final int num_tiles = target_multi[0].length;
		final int tilesY = num_tiles/tilesX;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
	 // use filter5 values?
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(tilesX, tilesY);
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++)  { // && (target_sequence[nSeq][ntile][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) { // &&
							double [][] targets = target_multi[nSeq][ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) if (targets[ntarg] != null){
									double [] target = targets[ntarg];
									if ((target != null) && (target[CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) { //  && (target[CuasMotionLMA.RSLT_CENTERED] != CuasMotionLMA.CENTERED_USED)) {
										if (filter5[nSeq][ntile] >=0) {
											if (filter5[nSeq][ntile] == ntarg) {
												targets[ntarg][CuasMotionLMA.RSLT_STRONGER] = Double.NaN;
											} else {
												targets[ntarg][CuasMotionLMA.RSLT_STRONGER] = ntile + num_tiles * filter5[nSeq][ntile]; // encode tile and variant (ntarg)
											}
										} else { // find a better around
											check_nebs: {
												for (int dy = -range; dy <= range; dy++) {	
													for (int dx = -range; dx <= range; dx++) {
														int ntile1 = tn.getNeibIndex(ntile, dx, dy);
														if ((ntile1 >= 0) && (filter5[nSeq][ntile1] >=0)) {
															targets[ntarg][CuasMotionLMA.RSLT_STRONGER] = ntile1 + num_tiles * filter5[nSeq][ntile1]; // encode tile and variant (ntarg)
															break check_nebs;
														}
													}
												}
												System.out.println("findStrongerNeighbor(): FIXME: no selected neighbor in 5x5 vicinity, but this is not selected either. nSeq="+
														nSeq+", ntile="+ntile+", ntarg="+ntarg);
												throw new IllegalArgumentException("findStrongerNeighbor(): FIXME: no selected neighbor in 5x5 vicinity, but this is not selected either. nSeq="+
														nSeq+", ntile="+ntile+", ntarg="+ntarg);
											}
										}
										if (mark_failed && (filter5[nSeq][ntile] != ntarg)) {
											targets[ntarg][CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_NEIGHBOR;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	public void processMovingTargetsMulti(
			final boolean         batch_mode,
			final float [][]      fpixels,
			final float []        fpixels_avg,
			final int             debugLevel) {
		int      start_frame = 0;
		boolean  half_step =      clt_parameters.imp.cuas_half_step; // true;
		boolean  slow_en =        clt_parameters.imp.cuas_slow_en;
		double   slow_ra =        clt_parameters.imp.cuas_slow_ra;
		double   slow_score =     clt_parameters.imp.cuas_slow_score;
		boolean  reuse_targets = clt_parameters.imp.cuas_reuse_targets; // true;  // read previously calculated non-conflict (one per tile) targets
		String   reuse_path =    clt_parameters.imp.cuas_reuse_path;    //     "";    // either suffix (all parameters the same) or the full path (contains "/") 

		
		double [][][] targets_nonconflict = null;

		int seq_length = corr_offset + corr_pairs;
		int corr_inc = half_step ? (corr_offset/2) : corr_offset;
		int num_corr_samples = (fpixels.length - seq_length - start_frame) / corr_inc;
		String [] slice_titles = new String [num_corr_samples];
		for (int nscan = 0; nscan < num_corr_samples; nscan++) {
			int frame_cent = start_frame + corr_inc * nscan + seq_length/2; // debug only
			slice_titles[nscan] =  scene_titles[frame_cent];
		}
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null);

		if (reuse_targets && (reuse_path.length() > 0)) {
			if (reuse_path.indexOf(Prefs.getFileSeparator()) < 0) {
				String x3d_path = parentCLT.getX3dDirectory();
				reuse_path = x3d_path + Prefs.getFileSeparator() + model_prefix + reuse_path+".tiff";
			}
			targets_nonconflict = getTargetsFromHyperAugment(reuse_path);
			if (targets_nonconflict == null) {
				System.out.println("processMovingTargetsMulti(): FAILED TO READ TARGET DATA FROM "+reuse_path);
			} else {
				System.out.println("processMovingTargetsMulti(): re-using target data from "+reuse_path);
			}
		}

		if (targets_nonconflict == null){
			// Detect motion candidates on two temporal views of the same merged sequence:
			// a temporal-unsharp path for faster targets and a slow running-average path
			// for dim/slow targets, then merge and de-conflict the two result sets.
			// The selected motion field is later consumed by generateExtractFilterMovingTargets().
			// process fast targets		
			slow_targets = false; // just for filenames
			int     precorr_ra =     clt_parameters.imp.cuas_precorr_ra; //  4 (10)
			float [][] fpixels_fast = temporalUnsharpMask( // all good targets
					batch_mode,           // final boolean         batch_mode,
					fpixels,              // final float [][]      fpixels,
					debugLevel);          // final int             debugLevel)
			double [][][] motion_sequence_fast = prepareMotionBasedSequence( // all good targets
					batch_mode,           // final boolean         batch_mode,
					fpixels_fast,         // float [][]            fpixels_tum,
					false,                // boolean               slow_mode,
					precorr_ra,           // int                   precorr_ra,
					debugLevel);          // final int             debugLevel)
			double [][][][] targets_fast =   locateAndFreezeTargetsMulti( // all good targets
					batch_mode,           // final boolean         batch_mode,
					fpixels_fast,         // float [][]            fpixels_tum,
					motion_sequence_fast, // double [][][]         target_sequence,
					false,                // boolean               slow_mode,
					debugLevel);          // final int             debugLevel)

			double [][][][] targets_good_multi = targets_fast;
			/*
			 * Before running with slow_en== true, verify temporal (scene) offset between slow and fast mode.
			 * It seems that when the same target is recognized as fast for some scenes, and as slow for anothers,
			 * it gets offset in x,y - likely caused by incorrect center scene matching. AF 05/02/2026 
			 */
			if (slow_en) {
				// process slow targets		
				this.slow_targets = true;    // just for filenames
				float [][] fpixels_slow = runningGaussian(
						fpixels,             // final float [][] fpixels,
						slow_ra,             // final double     ra_length,
						this.gpu_max_width); // final int        width)
				precorr_ra = (int) Math.round(slow_ra/2);
				double [][][] motion_sequence_slow = prepareMotionBasedSequence( // all good targets
						batch_mode,          // final boolean         batch_mode,
						fpixels_slow,        // float [][]            fpixels_tum,
						true,                // boolean               slow_mode,
						precorr_ra,          // int                   precorr_ra,
						debugLevel);         // final int             debugLevel)
				double [][][][] targets_slow =   locateAndFreezeTargetsMulti( // all good targets
						batch_mode,          // final boolean         batch_mode,
						fpixels_slow,        // float [][]            fpixels_tum,
						motion_sequence_slow,// double [][][]         target_sequence,
						true,                // boolean               slow_mode,
						debugLevel);         // final int             debugLevel)
				targets_good_multi = combineFastSlow(
						targets_fast,       // final double [][][][] targets_fast, // will only process non-nulls here
						targets_slow,       // final double [][][][] targets_slow,
						slow_score);        // final double          scale_slow){    // if >0, limit number of best results to add
			}
			
			sortMultiTargets( // maybe only needed after slow branch?
					targets_good_multi, // final double [][][][] target_multi,
					true);   
			
			
			// final boolean         trim_nulls) { // trim null targets
			targets_nonconflict = resolveTargetsConflictsMulti(
					batch_mode,     // boolean               batch_mode,
					fpixels,        // float [][]            fpixels,
					targets_good_multi,   // double [][][][] targets_good_multi, // all targets, including conflicting
					debugLevel);    // final int             debugLevel)

		}
		setTargets(targets_nonconflict);
		return;
	}
	
	final void recalcOmegas(
			boolean       recalc,
			double [][][] target_single,
			boolean       batch_mode,
			int           debugLevel){

		double  good_mismatch =  clt_parameters.imp.cuas_good_mismatch;  // 0.4;
		double  slow_fast_mismatch =  clt_parameters.imp.cuas_slow_fast_mismatch;  // 1.5; allow larger mismatch between slow and fast
		double  match_len_pwr =  clt_parameters.imp.cuas_match_len_pwr;   // raise matching length to this power for calculating score
		double seq_travel=       clt_parameters.imp.cuas_seq_travel; // 3.0;   // minimal diagonal of the bounding box that includes sequence to be considered "cuas_enough_seq". Filtering out atmospheric fluctuations
		boolean intermed_low =   clt_parameters.imp.cuas_intermed_low;    //true;

		double interseq_scale =  getInterseqScale();
		double [][][][] targets_multi = convertToMultiTarget(
				target_single); // final double [][][] target_sequence) {
		getHalfBeforeAfterPixXY(
				targets_multi,        // final double [][][][] targets_multi,
				true,                 // final boolean       good_only,
				interseq_scale,       // , // final double        interseq_scale,
				tilesX,               // final int           tilesX,
				debugLevel);          //	final int           debugLevel) {
		calculateMismatchBeforeAfter( // fixing bug in previous software that incorrectly calculated direction of the first/last in the sequence
				targets_multi,        // final double [][][][]  targets_multi,
				true,                 // final boolean          good_only,
				tilesX);              //  int           tilesX) {
		if (recalc) {
			recalcOmegas(
					targets_multi,        // double [][][][] targets_multi,
					debugLevel);          // int           debugLevel){
		}
		getHalfBeforeAfterPixXY(
				targets_multi,        // final double [][][][] targets_multi,
				true,                 // final boolean       good_only,
				interseq_scale,       // , // final double        interseq_scale,
				tilesX,               // final int           tilesX,
				debugLevel);          //	final int           debugLevel) {
		calculateMismatchBeforeAfter( // fixing bug in previous software that incorrectly calculated direction of the first/last in the sequence
				targets_multi,        // final double [][][][]  targets_multi,
				true,                 // final boolean          good_only,
				tilesX);              //  int           tilesX) {
		calcMatchingTargetsLengths(   // calculate number of consecutive keyframes connected to each target
				targets_multi,        // final double [][][][] targets_multi,
				true,                 // final boolean         calc_linked,
				// which is better here?
				good_mismatch, // max_mismatch,        // final double          max_mismatch,     // if <=0, do not calculate  mismatch_ba and filter
				slow_fast_mismatch, // final double          slow_fast_mismatch,
				match_len_pwr,     // final double          match_len_pwr, // 0.5; // raise matching length to this power for calculating score
				seq_travel,        // final double          seq_travel,
				tilesX);   //final int             tilesX)
		String model_prefix = parentCLT.getImageName()+getParametersSuffix(clt_parameters,null);
		/*
		if (intermed_low) {
			ImagePlus imp_omegas = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-TEST-OMEGAS1",// String        title,
					false,            // final boolean         good_only,
					false,           // final boolean         show_empty, // show scenes with no (valid) targets
					true, // !batch_mode,          // boolean       show,
					tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_omegas);
		}
		*/
		double [][][] target_single_new = convertFromMultiTarget(// single target per tile
				targets_multi);  // final double [][][][] target_multi) {
		if (target_single_new != null) {
			System.arraycopy(target_single_new, 0, target_single, 0, Math.min(target_single_new.length, target_single.length));
		}
		if (intermed_low) {
			ImagePlus imp_omegas = showTargetSequence(
					targets_multi,        // double [][][] vector_fields_sequence,
					slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
					model_prefix+"-SMOOTH_OMEGAS_SINGLE",// String        title,
					false,               // final boolean         good_only,
					true, // false,               // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_omegas);
		}
		return;
	}
	
	
	
	
	final void recalcOmegas(
			double [][][][] targets_multi,
			int           debugLevel){
		final int num_seq =   targets_multi.length;
		final int num_tiles = targets_multi[0].length;
		final int tileSize = GPUTileProcessor.DTT_SIZE;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final double  interseq_scale = getInterseqScale();		
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(tilesX, num_tiles/tilesX); 
					for (int nSeq = ai.getAndIncrement(); nSeq < num_seq; nSeq = ai.getAndIncrement()) {
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = targets_multi[nSeq][ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									double [] target = targets[ntarg];
									if (target != null) {
										int tileX = ntile % tilesX;
										int tileY = ntile / tilesX;
//										double xc = tileSize * tileX + tileSize/2;  
//										double yc = tileSize * tileY + tileSize/2;
										double [] xy = {
												tileSize * tileX + tileSize/2 + target[CuasMotionLMA.RSLT_X],  
												tileSize * tileY + tileSize/2 + target[CuasMotionLMA.RSLT_Y]};
										double [][] dxy_other = new double[2][]; //{before, after}{x,y}
										double [] vxy = new double[2];
										int num_neibs = 0;
										double ddirs = target[CuasMotionLMA.RSLT_MISMATCH_DIRS]; 
										int [] lldd = decodeDirs(ddirs);// {dir_before, dir_after, ntarg_before, ntarg_after
										for (int ba = 0; ba < 2; ba++) if (lldd[ba] >= 0){
											int ntile_other = tn.getNeibIndex(ntile, lldd[ba]);
											// assuming consistent data, not testing
											int ntarg_other = lldd[ba + 2];
											int tileX_other = ntile_other % tilesX;
											int tileY_other = ntile_other / tilesX;
											int nseq_other = nSeq + ((ba > 0) ? 1 : -1);											// double target_other
											double [] target_other = targets_multi[nseq_other][ntile_other][ntarg_other];
											dxy_other[ba] = new double [] {
													tileSize * tileX_other + tileSize/2 + target_other[CuasMotionLMA.RSLT_X] - xy[0], 
													tileSize * tileY_other + tileSize/2 + target_other[CuasMotionLMA.RSLT_Y] - xy[1]};
											for (int idir = 0; idir < vxy.length; idir++) {
												if (ba == 0) {
													vxy[idir] -= dxy_other[ba][idir];
												} else {
													vxy[idir] += dxy_other[ba][idir];
												}
											}
											num_neibs++;
										}
										if (num_neibs > 0) {
											for (int idir = 0; idir < vxy.length; idir++) {
												target[CuasMotionLMA.RSLT_VX+idir] = 0.5 * vxy[idir] / (num_neibs * interseq_scale);
											}											
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	
}
