/**
 **
 ** 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.Calendar;
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.text.SimpleDateFormat;
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_MV =       0;
	final static public int IMPORTANCE_A =        1;
	final static public int IMPORTANCE_RMS =      2;
	final static public int IMPORTANCE_RMS_A =    3;
	final static public int IMPORTANCE_CENTER =   4;
	final static public int IMPORTANCE_MISMATCH = 5;
	final static public int IMPORTANCE_MATCH_LEN= 6;
	final static public int IMPORTANCE_TRAVEL=    7;
	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 final int       WHEN_PHASE1 =  0;   // phase 1 RSLT_WHEN start
	public static final int       WHEN_PHASE2 =  100; // phase 2 RSLT_WHEN start
	public static final int       WHEN_FINISH =  200; // RSLT_WHEN start after phase 2
	public int    when_fail = 0; // will maintain (increment) and use for "RSLT_WHEN" when setting RSLT_FAIL
	
	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 []                  noise_neibs = 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 int [] getNoiseNeibs () {
		return noise_neibs;
	}
	
	
	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. This method returns the idealised real-valued half-step.
	 *
	 * NOTE: do NOT use this for position calculations (BX/AX/BY/AY) or RSLT_VX/VY back-
	 * conversion (recalcOmegas). Those must use the actual integer keyframe step
	 * {@link #getCorrInc(CLTParameters)} so that BX/AX positions fall at real frame
	 * positions and the velocity round-trip is consistent.
	 */
	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;
	}

	// By Claude on 05/30/2026: use integer getCorrInc() (not getCorrIncExact()) so that BX/AX positions
	// sit at actual raw-frame positions and recalcOmegas() round-trips correctly.
	// With half_step=true and odd corr_offset (e.g. 5): getCorrInc()=2, getCorrIncExact()=2.5.
	// Using 2.5 caused after-BX = 0.5*VX instead of the correct 0.4*VX (= VX*corr_inc/corr_offset).
	public double getInterseqScale() {   // multiply target velocity to get offset to midpoint between adjacent keyframes
		return 0.5 * getCorrInc(clt_parameters) / (double) 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)
		double  noisemax8 =      clt_parameters. imp.cuas_noisemax8;	    
//		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 = GpuQuad.createRectilinear( // single camera; backend-aware (JCUDA legacy or native JNA)
					gpuTileProcessor,   // GPUTileProcessor gpuTileProcessor (null/unused in JNA mode),
					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)
		
		noise_neibs = getNoiseNeibs(
				noise_map8, // final double []     noise8,    // null - do not filter by noise
				noisemax8,  // final double        noise_max,
				tilesX);    // final int           tilesX);
		
		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-variant 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;
				}

				// By Claude on 05/24/2026: per-param min/max accumulators (NaN = no data yet)
				double[][] minMax = new double[params.length][2];
				for (double[] mm : minMax) { mm[0] = Double.NaN; mm[1] = Double.NaN; }

				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 pi = 0; pi < params.length; pi++) {
										int npar = params[pi];
										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]);
												targObj.put("vboost", multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VFRAC]);
												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) {
													double val = multi_targets[nseq][ntile][ntarg][npar];
													targObj.put(CuasMotionLMA.LMA_TITLES[npar], val); // By Claude on 05/08/2026
													if (!Double.isNaN(val) && !Double.isInfinite(val)) {
														if (!(val >= minMax[pi][0])) minMax[pi][0] = val;
														if (!(val <= minMax[pi][1])) minMax[pi][1] = val;
													}
												}
										}
									}
									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);
				// By Claude on 05/24/2026: add min/max for each scalar param
				org.json.simple.JSONObject minMaxObj = new org.json.simple.JSONObject();
				for (int pi = 0; pi < params.length; pi++) {
					int npar = params[pi];
					if (npar >= 0 && npar < CuasMotionLMA.LMA_TITLES.length && !Double.isNaN(minMax[pi][0])) {
						org.json.simple.JSONObject mmEntry = new org.json.simple.JSONObject();
						mmEntry.put("min", minMax[pi][0]);
						mmEntry.put("max", minMax[pi][1]);
						minMaxObj.put(CuasMotionLMA.LMA_TITLES[npar], mmEntry);
					}
				}
				if (!minMaxObj.isEmpty()) {
					res.put("min_max", minMaxObj);
				}
				
			} 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/vboost", "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);
				double [] min_max = {Double.NaN, Double.NaN};
				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];
								if (has_targ[ty-ty0][tx-tx0] || !skip_empty) {
									if ((theight * twidth) > 1) {
										System.out.print(String.format("%5s,%2d:%2d, ",ntile,tx,ty));			
									}
									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];
												double vboost = multi_targets[nseq][ntile][ntarg][CuasMotionLMA.RSLT_VBOOST];
												System.out.print(String.format("(%8.4f, %8.4f, %8.5f, %4.1f)",vx,vy,vconf, vboost));
												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 (!Double.isNaN(multi_targets[nseq][ntile][ntarg][npar]) &&
														!Double.isInfinite(multi_targets[nseq][ntile][ntarg][npar])) {
													if (!(multi_targets[nseq][ntile][ntarg][npar] >= min_max[0])) {
														min_max[0] = multi_targets[nseq][ntile][ntarg][npar];
													}
													if (!(multi_targets[nseq][ntile][ntarg][npar] <= min_max[1])) {
														min_max[1] = multi_targets[nseq][ntile][ntarg][npar];
													}
												}
												
											}
//											if (ntarg < (num_targ-1)) {
												System.out.print(", ");
//											}
										}
									}
//									System.out.println();
								}
							}
						}
						System.out.println();
					}
				}
				System.out.println("\n"+choices[npar+extra_choices.length]+" Rectangle("+tx0+", "+ty0+", "+twidth+", "+theight+")"+
				" scenes:["+nseq0+", "+nseq1+"] min = "+min_max[0]+",  max = "+min_max[1]);
				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;
	}
	/**
	 * Create initial multi-target array having only single target with motion parameters set up.
	 * It will be used to select to try instead of the original motion_sequence array using targets[0]
	 * targets[0][CuasMotionLMA.RSLT_CENTERED] will be initially NaN, set to -1 (CENTERED_USED) after usage
	 * To prevent re-trying it
	 * @param motion_sequence 
	 * @return
	 */
	public static double [][][][] initTargetsFromMotion(
			final double [][][]   motion_sequence){
		final int num_seq   = motion_sequence.length;
		final int num_tiles = motion_sequence[0].length;
		final double [][][][] target_sequence_multi = new double [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()) {
						double [][] mv_tiles = motion_sequence[nSeq];
						target_sequence_multi[nSeq] = new double[num_tiles][][];
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [] mv_tile = mv_tiles[ntile];
							if (mv_tile != null) {
								double [] target = CuasMotionLMA.getEmpty();
								target[CuasMotionLMA.RSLT_VX] =     mv_tile[CuasMotionLMA.RSLT_VX];
								target[CuasMotionLMA.RSLT_VY] =     mv_tile[CuasMotionLMA.RSLT_VY];
								target[CuasMotionLMA.RSLT_VSTR] =   mv_tile[CuasMotionLMA.RSLT_VSTR];
								target[CuasMotionLMA.RSLT_VFRAC] =  mv_tile[CuasMotionLMA.RSLT_VFRAC];
								target[CuasMotionLMA.RSLT_VBOOST] = mv_tile[CuasMotionLMA.RSLT_VBOOST];
								target[CuasMotionLMA.RSLT_VCORR] =  mv_tile[CuasMotionLMA.RSLT_VCORR];
//									target[CuasMotionLMA.RSLT_CENTERED] = Double.NaN; // alreadytCuasMotionLMA.CENTERED_NO;
								target_sequence_multi[nSeq][ntile] = new double[][] {target};
							}							
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return target_sequence_multi;
	}
	

	/**
	 * When used, each tile has a single target
	 * @param targets_sequence_multi
	 * @param niter
	 * @param tilesX
	 * @param min_score_mv
	 * @param speed_min
	 * @param speed_pref
	 * @param speed_boost
	 */
	public static void setEffectiveStrengthMV( // calculate tiles effective strength by the motion vectors. Combine with the target LMA?
			final double [][][][] targets_sequence_multi,
			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_multi.length;
		final int num_tiles = targets_sequence_multi[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 [][][] tile_targets = targets_sequence_multi[nSeq];
						for (int ntile = 0; ntile < num_tiles; ntile++) {
							double [][] targets = tile_targets[ntile];
							if (targets != null) {
								for (int ntarg = 0; ntarg < targets.length; ntarg++) {
									if (targets[ntarg] != null) {
										double es =	getEffectiveStrengthMV(
												targets[ntarg],  // double [] target,
												speed_min,  // double speed_min,		
												speed_pref, // double speed_pref,		
												speed_boost); //double speed_boost)
										targets[ntarg][CuasMotionLMA.RSLT_MSCORE] = es;
//										effective_strength[nSeq][ntile] = es;
										if ((niter >= 0) && !(es >= min_score_mv)) {
											targets[ntarg][CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_MOTION;
											targets[ntarg][CuasMotionLMA.RSLT_WHEN] = niter;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}

	
	
	
	
/*
	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 void setBadHorizon(
			final double [][][] target_sequence,
			final double        horizon_y,   // <0 - do not filter by horizon
			final int []        noise_neibs, // which neighbor is good (&=(1 << dir), 1 << 8 - this one itself)
			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) ||  (noise_neibs != 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 (noise_neibs != null) {
								for (int ntile = 0; ntile < ntile_horizon; ntile++) if (noise_neibs[ntile] == 0) {
									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);
		}
	}
	
	
	public static int [] getNoiseNeibs(
			final double []     noise8,    // null - do not filter by noise
			final double        noise_max,
			final int           tilesX){
		int [] noise_neibs = new int [noise8.length];
		final int tilesY = noise8.length/tilesX;

		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() {
					TileNeibs tn = new TileNeibs(tilesX, tilesY);
					for (int nTile = ai.getAndIncrement(); nTile < noise8.length; nTile = ai.getAndIncrement()) if(noise8[nTile] <= noise_max) {
						int neibs = 0;
						for (int dir = 0; dir <=8; dir++) { // 8 is self;
							int ntile1 = tn.getNeibIndex(nTile, dir);
							if ((ntile1>=0) && (noise8[ntile1] <= noise_max)) {
								neibs |= 1 << dir;
							}
						}
						noise_neibs[nTile] = neibs;
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return noise_neibs;
	}
	
	
	
	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);
		final int dbg_nseq = 26; // 125; // 100; // 161;      // 36; // 28; // 97; // 40; // 41; // 157; // -(116); // 57; // 20; // -1;
		final int dbg_tile = 19+34*80; // 15+33*80; // 12+37*80; //55+38*80; // 51+38*80; // 54+38*80; //52+38*80;  // 50+38*80; // 55+38*80; // -(17 + 33*80); // 48+32*80; // 50+38*80; // -1;
		for (int irange = range; irange > 0; irange--) { // to try 5x5, but make sure 3x3 are always as needed
			final int firange = irange;
			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 (nSeq == dbg_nseq) {
								System.out.println("extendMotionScan(): nSeq="+nSeq+" irange="+firange+" has_filter="+has_filter);
							}
							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));
								}
							}
							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 ((nSeq == dbg_nseq) && (ntile == dbg_tile)) {
										System.out.println("extendMotionScan(): nSeq="+nSeq+" irange="+firange+" ntile="+ntile);
									}
									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][TDCorrTile.INDX_VX] * scale_vxy;
				target[CuasMotionLMA.RSLT_VY] =     vector_field[ntile][TDCorrTile.INDX_VY] * scale_vxy;
				target[CuasMotionLMA.RSLT_VSTR] =   vector_field[ntile][TDCorrTile.INDX_STR];
				target[CuasMotionLMA.RSLT_VFRAC] =  vector_field[ntile][TDCorrTile.INDX_FRAC];
				target[CuasMotionLMA.RSLT_VBOOST] = boost_pairs; // save what boost for the number of pairs was used 
				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                 corr_offset_scale,
			double              mv_diff_max,        // if > 0, fail for high MV correction
			// next 4 -to calulate RSLT_MSCORE, similar to setEffectiveStrengthMV
			double              min_score_mv,
			double              speed_min,		
			double              speed_pref,		
			double              speed_boost,				
			String              dbg_suffix,
			int                 dbg_iter_index,     // to generate different image names for different iteration to simplify "save as" images
			int                 debugLevel) {
//		debugLevel = -1; // DEBUG OFF
//		final int max_index_dbg = 3;// 5; //  // 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);
		// By Claude on 05/14/2026: clamp corr_offset_scale so corr_offset_eff <= corr_pairs_ref (keep >=1)
		final int corr_offset_eff = corr_offset * Math.max(1, Math.min(corr_offset_scale, corr_pairs_ref / corr_offset));

		// Hard-coded debug selectors: set >= 0 to enable per-scan/per-tile visualisation
		final int dbg_nseq = 26; // 125; // 100; // 161;      // 36; // 28; // 97; // 40; // 41; // 157; // -(116); // 57; // 20; // -1;
		final int dbg_tile = 19+34*80; // 15+33*80; // 12+37*80; //55+38*80; // 51+38*80; // 54+38*80; //52+38*80;  // 50+38*80; // 55+38*80; // -(17 + 33*80); // 48+32*80; // 50+38*80; // -1;
		final int corr_offset_scale_eff = corr_offset_eff / corr_offset; // actual scale after clamping
		if ((dbg_nseq < 0) || (dbg_nseq >= targets_nonoverlap.length) || (dbg_tile < 0) || (dbg_tile >= targets_nonoverlap[dbg_nseq].length)) {
			debugLevel = -2;
		}
		if (debugLevel >= 0) {
			// See if it contains specified dbg_nseq, dbg_tile
			double[] target = targets_nonoverlap[dbg_nseq][dbg_tile];
			if (target == null || Double.isNaN(target[CuasMotionLMA.RSLT_X])) {
				debugLevel = -3;	
			};
		}
		
		if (debugLevel >= 0) {
			System.out.println("\nrefineMotionVectors(): dbg_iter_index="+dbg_iter_index+", corr_offset="+corr_offset+", corr_offset_scale="+corr_offset_scale+
					", corr_offset_eff="+corr_offset_eff+
					", recalc_mv_boost="+recalc_mv_boost+
					", corr_pairs_ref="+corr_pairs_ref+
					(corr_offset_scale_eff != corr_offset_scale ? " (CLAMPED from "+corr_offset_scale+" to "+corr_offset_scale_eff+")" : "")+
					", dbg_nseq="+dbg_nseq+", dbg_tile="+dbg_tile);
		}
		// 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];
		double sum_w = 0.0;
		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;
				sum_w+=w;
			}
		}
		double scale_w = 4.0*GPUTileProcessor.DTT_SIZE*GPUTileProcessor.DTT_SIZE/sum_w;
		// maybe scale: cosine integral divided by mask is multiplied by the cosine 
		for (int i = 0; i < mask_kernel.length; i++) {
			mask_kernel[i] *= scale_w; 
		}
//		if ((dbg_nseq >= 0 || dbg_tile >= 0) && (debugLevel >= 0) && (dbg_iter_index<= max_index_dbg)) {
		if ((dbg_nseq >= 0 || dbg_tile >= 0) && (debugLevel >= 0)) {
			ShowDoubleFloatArrays.showArrays(mask_kernel.clone(), mside, mside,
					"refineMotionVectors-mask-boost"+recalc_mv_boost+"-r0_" + recalc_mv_r0 + "-r1_" + recalc_mv_r1+"-niter"+dbg_iter_index+dbg_suffix);
		}

		// 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;
		// By Claude on 05/24/2026: profiling accumulators
		long t_clear = 0, t_mask = 0, t_corr = 0, t_pd = 0;
		int prof_nseq = 0, prof_targets = 0;
		long t_corr_sparse = 0, t_corr_dense = 0; // sparse: <10 targets, dense: >=10
		int prof_nseq_sparse = 0, prof_nseq_dense = 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_eff;

			// 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
			long t0 = System.nanoTime(); // By Claude on 05/24/2026
			for (int f = fmin_alloc; f <= fmax_alloc; f++) Arrays.fill(fpixels_masked[f], 0.0f);
			long t1 = System.nanoTime(); // By Claude on 05/24/2026

			// 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];
			}

			// By Claude on 05/24/2026: count valid targets for profiling
			int nseq_targets = 0;
			for (int ntile = 0; ntile < ntiles; ntile++) if (tile_valid[ntile]) nseq_targets++;
			// 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);
			long t2 = System.nanoTime(); // By Claude on 05/24/2026

			// 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-boost"+recalc_mv_boost+"-masked-nseq" + nseq+"-niter"+dbg_iter_index+dbg_suffix, 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-boost"+recalc_mv_boost+"-nseq" + nseq+"-niter"+dbg_iter_index+dbg_suffix, stack_titles);
			}

			long t3 = System.nanoTime(); // By Claude on 05/24/2026
			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);

			long t4 = System.nanoTime(); // By Claude on 05/24/2026
			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, INDX_STR=2, INDX_FRAC = 3;;
			double[][] vector_field = TDCorrTile.getMismatchVector(
					corr_tiles_pd,
					-rstr,        // rmax < 0: no relative-strength filter (same convention as getTargetsFromCorr2d)
					cent_radius,
					n_recenter,
					true);


			long t5 = System.nanoTime(); // By Claude on 05/24/2026
			// 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;
				double scaled_dvx = vector_field[ntile][TDCorrTile.INDX_VX] / corr_offset_scale; // By Claude on 05/13/2026
				double scaled_dvy = vector_field[ntile][TDCorrTile.INDX_VY] / corr_offset_scale;
				if (ntile == dbg_tile && debugLevel >= 0) {
					String nseq_mark = (nseq == dbg_nseq) ? "*": "";
					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 VSTR=%9.4f %s\n",
							nseq, ntile,
							target[CuasMotionLMA.RSLT_VX], target[CuasMotionLMA.RSLT_VY],
							scaled_dvx, scaled_dvy,
							target[CuasMotionLMA.RSLT_VX] + scaled_dvx,
							target[CuasMotionLMA.RSLT_VY] + scaled_dvy,
							vector_field[ntile][TDCorrTile.INDX_STR],
							nseq_mark);
				}
				target[CuasMotionLMA.RSLT_VX] += scaled_dvx;
				target[CuasMotionLMA.RSLT_VY] += scaled_dvy;
				target[CuasMotionLMA.RSLT_VSTR] = vector_field[ntile][TDCorrTile.INDX_STR];
				double es =	getEffectiveStrengthMV(
						target,  // double [] target,
						speed_min,  // double speed_min,		
						speed_pref, // double speed_pref,		
						speed_boost); //double speed_boost)
				target[CuasMotionLMA.RSLT_MSCORE] = es;
				target[CuasMotionLMA.RSLT_VFRAC] =  vector_field[ntile][TDCorrTile.INDX_FRAC];
				target[CuasMotionLMA.RSLT_VBOOST] = recalc_mv_boost;
				target[CuasMotionLMA.RSLT_VCORR] =  Math.sqrt(scaled_dvx*scaled_dvx + scaled_dvy*scaled_dvy);
				if ((mv_diff_max > 0) && (target[CuasMotionLMA.RSLT_VCORR] > mv_diff_max) && !(target[CuasMotionLMA.RSLT_FAIL] > 0 )) {
					target[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_VCORR; // set when?
					// set RSLT_WHEN - will be set in public static int addNewResults()
				}
			}
			// By Claude on 05/24/2026: accumulate profiling times
			t_clear += t1 - t0;
			t_mask  += t2 - t1;
			t_corr  += t4 - t3;
			t_pd    += t5 - t4;
			prof_nseq++;
			prof_targets += nseq_targets;
			if (nseq_targets < 10) { t_corr_sparse += t4 - t3; prof_nseq_sparse++; }
			else                   { t_corr_dense  += t4 - t3; prof_nseq_dense++;  }
		}
		// By Claude on 05/24/2026: profiling summary
		if (prof_nseq > 0) {
			double ms = 1e-6;
			System.out.printf("refineMotionVectors() profiling (%d nseq, %d total targets, avg %.1f/iter, boost=%.1f):%n",
					prof_nseq, prof_targets, (double)prof_targets/prof_nseq, recalc_mv_boost);
			System.out.printf("  clear (Arrays.fill)           : %8.1f ms total, %6.3f ms/iter%n", t_clear*ms, t_clear*ms/prof_nseq);
			System.out.printf("  mask  (startAndJoin)          : %8.1f ms total, %6.3f ms/iter%n", t_mask *ms, t_mask *ms/prof_nseq);
			System.out.printf("  corr  (correlatePairs) total  : %8.1f ms total, %6.3f ms/iter%n", t_corr *ms, t_corr *ms/prof_nseq);
			if (prof_nseq_sparse > 0) System.out.printf("    corr sparse (<10 targ, %3d iters): %8.1f ms, %6.3f ms/iter%n",
					prof_nseq_sparse, t_corr_sparse*ms, t_corr_sparse*ms/prof_nseq_sparse);
			if (prof_nseq_dense  > 0) System.out.printf("    corr dense  (>=10 targ, %3d iters): %8.1f ms, %6.3f ms/iter%n",
					prof_nseq_dense,  t_corr_dense *ms, t_corr_dense *ms/prof_nseq_dense);
			System.out.printf("  pd    (convertTD+getMismatch) : %8.1f ms total, %6.3f ms/iter%n", t_pd*ms, t_pd*ms/prof_nseq);
			System.out.printf("  total accounted               : %8.1f ms%n", (t_clear+t_mask+t_corr+t_pd)*ms);
		}
		// 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-boost"+recalc_mv_boost+"-n"+dbg_iter_index+dbg_suffix,
					slice_titles_ref).show();
		}
		return;
	}

	/**
	 * 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();
	}

	/**
	 * Render all sensors from the current transform domain (IMCLT) and return the per-sensor
	 * average pixel value. Also SAVES the 16 per-sensor rendered images as one multi-slice TIFF in
	 * the model directory (via the GpuQuad's QuadCLT, the same save method the oracle path uses), so
	 * the spread can be inspected visually (DC offset vs. structured row/col/FPN patterns) rather than
	 * only as a number - the per-slice label carries that sensor's average.
	 * Good conditioning / photometric calibration is indicated by the per-sensor averages matching
	 * (approximately) - the 16 sensors view ~the same scene. Prints spread (max-min) and std as an
	 * at-a-glance quality metric. Path-2 calibration/conditioning test tool.
	 * By Claude on 06/30/2026; per-sensor image save added 07/01/2026.
	 * @param gpuQuad the (16-sensor) GpuQuad whose transform-domain data to render, e.g. QuadCLT.getGPUQuad().
	 * @param use_reference render the reference-scene TD (gpu_clt_ref) instead of the current scene.
	 * @return per-sensor mean of finite pixels, length getNumSensors().
	 */
	public static double [] perSensorAveragesFromTD(GpuQuad gpuQuad, boolean use_reference) {
		return perSensorAveragesFromTD(gpuQuad, use_reference, "-CUAS-PERSENSOR", null);
	}
	/** As {@link #perSensorAveragesFromTD(GpuQuad, boolean)} but with an explicit filename tag and save target.
	 * @param name_tag filename tag for the saved per-sensor stack (e.g. "-CUAS-PERSENSOR-RAW").
	 * @param save_qclt QuadCLT whose model directory receives the stack - pass the virtual "-CENTER" instance
	 *   (main CUAS results live there), NOT the physical scene. null -> fall back to gpuQuad.getQuadCLT() (physical).
	 * By Claude on 07/01/2026. */
	public static double [] perSensorAveragesFromTD(GpuQuad gpuQuad, boolean use_reference, String name_tag, QuadCLT save_qclt) {
		final int num_sens = gpuQuad.getNumSensors();
		final float [][] per_sensor = perSensorImagesFromTD(gpuQuad, use_reference); // one w*h slice per sensor
		final int width    = gpuQuad.getImageWidth();   // valid after last IMCLT
		final int height   = gpuQuad.getImageHeight();
		final double [] avg = new double [num_sens];
		for (int ncam = 0; ncam < num_sens; ncam++) {
			final float []   px  = per_sensor[ncam];
			double sum = 0.0; int n = 0;
			for (int i = 0; i < px.length; i++) {
				final float v = px[i];
				if (!Float.isNaN(v)) { sum += v; n++; }
			}
			avg[ncam] = (n > 0) ? (sum / n) : Double.NaN;
		}
		// spread across sensors = at-a-glance calibration-quality metric (should be ~0 when well calibrated)
		double mn = Double.POSITIVE_INFINITY, mx = Double.NEGATIVE_INFINITY, s = 0, s2 = 0; int m = 0;
		for (double a : avg) if (!Double.isNaN(a)) { mn = Math.min(mn, a); mx = Math.max(mx, a); s += a; s2 += a * a; m++; }
		if (m > 0) {
			double mean = s / m, std = Math.sqrt(Math.max(0.0, s2 / m - mean * mean));
			System.out.printf("perSensorAveragesFromTD(ref=%b): mean=%.3f spread(max-min)=%.3f std=%.3f over %d sensors%n",
					use_reference, mean, (mx - mn), std, m);
		}
		// Save the per-sensor stack into the model dir. Prefer the explicit save target (the virtual "-CENTER"
		// instance where main CUAS results live); fall back to the GpuQuad's bound (physical) QuadCLT.
		final QuadCLT qclt = (save_qclt != null) ? save_qclt : gpuQuad.getQuadCLT();
		if (qclt != null) {
			final String [] titles = new String [num_sens];
			for (int ncam = 0; ncam < num_sens; ncam++) {
				titles[ncam] = String.format("s%02d_avg=%.1f", ncam, avg[ncam]);
			}
			final String title = qclt.getImageName() + name_tag + (use_reference ? "-REF" : "");
			final ImagePlus imp = ShowDoubleFloatArrays.makeArrays(per_sensor, width, height, title, titles);
			qclt.saveImagePlusInModelDirectory(null, imp); // null suffix -> use imp title as filename
		} else {
			System.out.println("perSensorAveragesFromTD(): gpuQuad.getQuadCLT()==null, skipping per-sensor image save");
		}
		return avg;
	}

	/** Render the current transform-domain data and return the per-sensor mono images (color 0).
	 * Extracted from {@link #perSensorAveragesFromTD} so the conditioning-test/calibration code can get the
	 * pixel arrays directly. By Claude on 07/02/2026.
	 * @param gpuQuad the GpuQuad whose TD to render.
	 * @param use_reference render the reference-scene TD (gpu_clt_ref) instead of the current scene.
	 * @return [sensor][pixel] mono images, width/height = gpuQuad.getImageWidth()/getImageHeight().
	 */
	public static float [][] perSensorImagesFromTD(GpuQuad gpuQuad, boolean use_reference) { // By Claude on 07/02/2026
		gpuQuad.execImcltRbgAll(
				(gpuQuad.getNumColors() <= 1), // isMonochrome()
				use_reference,
				null); // int [] wh
		final int num_sens = gpuQuad.getNumSensors();
		final float [][] per_sensor = new float [num_sens][];
		for (int ncam = 0; ncam < num_sens; ncam++) {
			per_sensor[ncam] = gpuQuad.getRBG(ncam)[0]; // mono (color 0)
		}
		return per_sensor;
	}

	/**
	 * Per-sensor linear photometric calibration fit: find per-sensor (a + b*x) that makes the 16 conditioned
	 * sensor images match each other, using only tiles where direct pixel comparison is valid:
	 * - far tiles (disparity &lt; max_disparity): small inter-sensor parallax -&gt; small image mismatch, safe to compare;
	 * - weak tiles (strength &lt; max_strength, e.g. sky): no details to match -&gt; also safe to compare pixel values.
	 * The excluded complement (high disparity AND high strength) is contrast near objects, where pixels mismatch
	 * because of the inter-sensor parallax. Outer tiles (1-tile border) are always discarded.
	 * Tile-&gt;pixel map: tile (tx,ty) -&gt; Rectangle(8*tx, 8*ty, 8, 8) (GPU transform size).
	 * Fit: iterate {reference = cross-sensor mean of corrected pixels; per-sensor least-squares (a+b*x) against
	 * the reference; gauge-normalize so mean(b)=1, mean(a)=0 (the overall level/scale is otherwise unconstrained
	 * and drifts); reject pixels with any-sensor residual &gt; 3 sigma (moving targets/clouds inside "safe" tiles)}.
	 * Converges in 2 iterations (validated offline: per-sensor mean spread 1353 -&gt; 5 counts, cross-sensor RMS
	 * 358 -&gt; 85 on 1773135476_186641). The result folds into the 2-map photometric calibration:
	 * scale'_s = b_s * scale_s, additive map FPN'_s(p) = b_s * FPN_s(p) - a_s (see {@link #adjustFpnLinear}).
	 * By Claude on 07/02/2026.
	 * @param center_CLT virtual "-CENTER" QuadCLT: source of the combo DSI (-INTER-INTRA-LMA) and debug-save target.
	 * @param per_sensor [sensor][pixel] conditioned (photometric+FPN corrected) images, e.g. from
	 *        {@link #perSensorImagesFromTD} after the conditioned convert.
	 * @param width image width, pixels.
	 * @param max_strength select weak tiles: strength &lt; max_strength (e.g. 0.5).
	 * @param max_disparity or far tiles: disparity &lt; max_disparity (e.g. 1.0).
	 * @param save_adjusted save the corrected stack as &lt;name&gt;-CUAS-PERSENSOR-ADJ for visual verification.
	 * @param debugLevel debug verbosity.
	 * @param keep_averages keep average offset = 0, average scale = 1.0 (gauge normalization each iteration:
	 *        the corrections then only redistribute between sensors, preserving the overall level and scale).
	 * @return {a[num_sens], b[num_sens]} or null if the combo DSI is not available.
	 */
	public static double [][] perSensorLinearFit( // By Claude on 07/02/2026
			QuadCLT       center_CLT,
			float [][]    per_sensor,
			int           width,
			double        max_strength,
			double        max_disparity,
			boolean       save_adjusted,
			int           debugLevel,
			boolean       keep_averages) { // By Claude on 07/02/2026: keep average offset=0, average scale=1.0
		final double [][] combo_dsn = center_CLT.restoreComboDSI(true); // reads <name>-INTER-INTRA-LMA.tiff
		if ((combo_dsn == null) || (combo_dsn.length <= OpticalFlow.COMBO_DSN_INDX_STRENGTH)) {
			System.out.println("perSensorLinearFit(): no combo DSI (-INTER-INTRA-LMA) available - skipping");
			return null;
		}
		final double [] disparity = combo_dsn[OpticalFlow.COMBO_DSN_INDX_DISP];     // slice 1 (1-based)
		final double [] strength =  combo_dsn[OpticalFlow.COMBO_DSN_INDX_STRENGTH]; // slice 2 (1-based)
		final int num_sens = per_sensor.length;
		final int height = per_sensor[0].length / width;
		final int tile_size = GPUTileProcessor.DTT_SIZE; // 8 (1 << 3) - GPU transform size
		final int tilesX = width / tile_size;
		final int tilesY = height / tile_size;
		// tile selection -> pixel mask
		final boolean [] pix_mask = new boolean [width*height];
		int num_sel_tiles = 0;
		for (int ty = 1; ty < (tilesY - 1); ty++) { // discard the outer tiles (1-tile border)
			for (int tx = 1; tx < (tilesX - 1); tx++) {
				int nt = ty*tilesX + tx;
				if ((strength[nt] < max_strength) || (disparity[nt] < max_disparity)) { // NaN -> excluded
					num_sel_tiles++;
					for (int y = 0; y < tile_size; y++) {
						int row = (ty*tile_size + y)*width + tx*tile_size;
						for (int x = 0; x < tile_size; x++) {
							pix_mask[row + x] = true;
						}
					}
				}
			}
		}
		// require all sensors finite
		final ArrayList<Integer> pix_list = new ArrayList<Integer>();
		for (int npix = 0; npix < pix_mask.length; npix++) if (pix_mask[npix]) {
			boolean all_finite = true;
			for (int s = 0; s < num_sens; s++) if (Float.isNaN(per_sensor[s][npix])) { all_finite = false; break; }
			if (all_finite) pix_list.add(npix);
		}
		final int num_pix = pix_list.size();
		if (num_pix < 100) {
			System.out.println("perSensorLinearFit(): only "+num_pix+" usable pixels - skipping");
			return null;
		}
		final double [] a = new double [num_sens];
		final double [] b = new double [num_sens];
		Arrays.fill(b, 1.0);
		final boolean [] keep = new boolean [num_pix];
		Arrays.fill(keep, true);
		final int num_iter = 3;
		double rms_before = Double.NaN, rms_after = Double.NaN;
		int num_keep = num_pix;
		for (int iter = 0; iter < num_iter; iter++) {
			// reference = cross-sensor mean of currently corrected pixels
			final double [] ref = new double [num_pix];
			for (int i = 0; i < num_pix; i++) {
				int npix = pix_list.get(i);
				double sum = 0.0;
				for (int s = 0; s < num_sens; s++) sum += a[s] + b[s]*per_sensor[s][npix];
				ref[i] = sum/num_sens;
			}
			// per-sensor least squares (a + b*x) against the reference, inlier pixels only
			for (int s = 0; s < num_sens; s++) {
				double sx = 0, sxx = 0, sy = 0, sxy = 0; int n = 0;
				for (int i = 0; i < num_pix; i++) if (keep[i]) {
					double x = per_sensor[s][pix_list.get(i)];
					sx += x; sxx += x*x; sy += ref[i]; sxy += x*ref[i]; n++;
				}
				double den = n*sxx - sx*sx;
				if (den != 0) {
					b[s] = (n*sxy - sx*sy)/den;
					a[s] = (sy - b[s]*sx)/n;
				}
			}
			if (keep_averages) { // gauge fix: mean correction == identity (average offset = 0, average scale = 1.0)
				double mean_a = 0, mean_b = 0;
				for (int s = 0; s < num_sens; s++) { mean_a += a[s]; mean_b += b[s]; }
				mean_a /= num_sens; mean_b /= num_sens;
				for (int s = 0; s < num_sens; s++) { b[s] /= mean_b; a[s] = (a[s] - mean_a)/mean_b; }
			}
			// residuals + outlier rejection for the next pass (moving targets/clouds inside "safe" tiles)
			double s2_all = 0, s2_keep = 0; long n_all = 0, n_keep = 0;
			final double [] res = new double [num_sens];
			double sigma = 0; // first pass over residuals to get sigma
			{
				double s2 = 0; long n = 0;
				for (int i = 0; i < num_pix; i++) {
					int npix = pix_list.get(i);
					double mean = 0;
					for (int s = 0; s < num_sens; s++) mean += a[s] + b[s]*per_sensor[s][npix];
					mean /= num_sens;
					for (int s = 0; s < num_sens; s++) { double r = a[s] + b[s]*per_sensor[s][npix] - mean; s2 += r*r; n++; }
				}
				sigma = Math.sqrt(s2/n);
				s2_all = s2; n_all = n;
			}
			num_keep = 0;
			for (int i = 0; i < num_pix; i++) {
				int npix = pix_list.get(i);
				double mean = 0;
				for (int s = 0; s < num_sens; s++) { res[s] = a[s] + b[s]*per_sensor[s][npix]; mean += res[s]; }
				mean /= num_sens;
				boolean good = true;
				double s2 = 0;
				for (int s = 0; s < num_sens; s++) { double r = res[s] - mean; s2 += r*r; if (Math.abs(r) > 3*sigma) good = false; }
				keep[i] = good;
				if (good) { s2_keep += s2; num_keep++; }
			}
			rms_after = Math.sqrt(s2_keep/(num_keep*num_sens));
			if (debugLevel > -3) {
				System.out.println(String.format("perSensorLinearFit() iter %d: rms(inliers)=%.3f rms(all)=%.3f kept=%.1f%%",
						iter, rms_after, Math.sqrt(s2_all/n_all), 100.0*num_keep/num_pix));
			}
		}
		{ // "before" RMS for the report (identity correction)
			double s2 = 0; long n = 0;
			for (int i = 0; i < num_pix; i++) {
				int npix = pix_list.get(i);
				double mean = 0;
				for (int s = 0; s < num_sens; s++) mean += per_sensor[s][npix];
				mean /= num_sens;
				for (int s = 0; s < num_sens; s++) { double r = per_sensor[s][npix] - mean; s2 += r*r; n++; }
			}
			rms_before = Math.sqrt(s2/n);
		}
		System.out.println("perSensorLinearFit(): "+num_sel_tiles+" tiles, "+num_pix+" pixels ("+num_keep+
				" inliers), cross-sensor RMS: before="+String.format("%.2f", rms_before)+
				" after="+String.format("%.2f", rms_after));
		System.out.println(" s |     a     |    b     ");
		for (int s = 0; s < num_sens; s++) {
			System.out.println(String.format("%2d | %9.3f | %8.5f", s, a[s], b[s]));
		}
		if (save_adjusted) { // corrected stack for visual verification
			final float [][] adj = new float [num_sens][per_sensor[0].length];
			final String [] titles = new String [num_sens];
			for (int s = 0; s < num_sens; s++) {
				for (int npix = 0; npix < adj[s].length; npix++) {
					adj[s][npix] = (float) (a[s] + b[s]*per_sensor[s][npix]);
				}
				titles[s] = String.format("s%02d_a=%.1f_b=%.4f", s, a[s], b[s]);
			}
			final ImagePlus imp = ShowDoubleFloatArrays.makeArrays(adj, width, height,
					center_CLT.getImageName()+"-CUAS-PERSENSOR-ADJ", titles);
			center_CLT.saveImagePlusInModelDirectory(null, imp);
		}
		return new double [][] {a, b};
	}

	/**
	 * Recalculate the 16+16 per-sensor lwir photometric parameters (offsets + scales) from the linear fit and
	 * save them to the corr-xml, so that when applied the same way as now (channelLwirApplyEqualize at load:
	 * scale*(raw-offset)) they compensate the remaining per-sensor mismatch between the /jp4/*.tiff images.
	 * Folding: corrected y' = a + b*y with y = scale*(raw-offset) is achieved by
	 *   scale'  = b * scale
	 *   offset' = offset - a / scale'
	 * (the residual per-pixel term (1-b)*FPN(p) is second order - b is within ~15% of 1 and the FPN map is
	 * recomputed against the new photometric anyway). Persistence mimics the established photometric machinery
	 * (runPhotometric()/photoEach()): the values are set on the PHYSICAL reference scene and its
	 * &lt;scene&gt;-INTERFRAME.corr-xml is saved (the virtual -CENTER INTERFRAME file only carries poses/velocities),
	 * plus on quadCLT_main so they apply to the next sequences and are saved in the main configuration file.
	 * By Claude on 07/02/2026.
	 * @param phys_scene the physical scene whose current offsets/scales produced the fitted conditioned images.
	 * @param apply_to instances to update in memory (e.g. {physical reference scene, center_CLT, quadCLT_main});
	 *        nulls and duplicates are allowed and handled.
	 * @param save_scene whose &lt;scene&gt;-INTERFRAME.corr-xml to save (the photometric owner - normally the
	 *        physical reference scene quadCLTs[ref_index]); null - do not save, update in memory only.
	 * @param a per-sensor additive corrections from {@link #perSensorLinearFit}.
	 * @param b per-sensor multiplicative corrections from {@link #perSensorLinearFit}.
	 * @param debugLevel debug verbosity.
	 */
	public static void applyLwirLinearCalibration( // By Claude on 07/02/2026
			QuadCLT    phys_scene,
			QuadCLT [] apply_to,
			QuadCLT    save_scene,
			double []  a,
			double []  b,
			int        debugLevel) {
		final int num_sens = a.length;
		final double [] offsets = phys_scene.getLwirOffsets(); // current (what conditioned the fitted images)
		final double [] scales  = phys_scene.getLwirScales();
		final double [] offsets_new = new double [num_sens];
		final double [] scales_new  = new double [num_sens];
		System.out.println("applyLwirLinearCalibration(): recalculated per-sensor lwir offsets/scales"+
				((save_scene != null)? " (saving to "+save_scene.getImageName()+" INTERFRAME corr-xml)":" (in-memory only)"));
		System.out.println(" s |   offset old  ->   offset new  | scale old -> scale new");
		for (int s = 0; s < num_sens; s++) {
			scales_new[s]  = b[s]*scales[s];
			offsets_new[s] = offsets[s] - a[s]/scales_new[s];
			System.out.println(String.format("%2d | %12.3f -> %12.3f | %8.5f -> %8.5f",
					s, offsets[s], offsets_new[s], scales[s], scales_new[s]));
		}
		final String photometric_scene = phys_scene.getImageName(); // provenance: recalibrated on this scene
		if (apply_to != null) {
			for (QuadCLT qclt : apply_to) if (qclt != null) { // duplicates are harmless (idempotent set)
				qclt.setLwirOffsets(offsets_new.clone());
				qclt.setLwirScales (scales_new.clone());
				qclt.setPhotometricScene(photometric_scene);
			}
		}
		if (save_scene != null) {
			save_scene.saveInterProperties( // writes <scene>-INTERFRAME.corr-xml into its x3d directory
					null,        // String path - null: default name in the x3d directory
					debugLevel);
		}
	}

	/**
	 * Fold the per-sensor linear corrections into the 2-map photometric calibration: the corrected pixel
	 * y' = a + b*y with y = scale*(raw-offset) - FPN(p) is equivalent to scale' = b*scale and
	 * FPN'(p) = b*FPN(p) - a. Returns NEW arrays (does not touch the input - the original may be referenced
	 * by the applied-FPN bookkeeping of already-conditioned scenes). By Claude on 07/02/2026.
	 * @param fpn [sensor][color][pixel] current per-pixel additive map (may be null -&gt; treated as zeros).
	 * @param num_pix number of pixels per sensor (used when fpn == null).
	 * @param a per-sensor additive correction from {@link #perSensorLinearFit}.
	 * @param b per-sensor multiplicative correction from {@link #perSensorLinearFit}.
	 * @return new [sensor][color][pixel] additive map = b*FPN - a.
	 */
	public static double [][][] adjustFpnLinear( // By Claude on 07/02/2026
			double [][][] fpn,
			int           num_pix,
			double []     a,
			double []     b) {
		final int num_sens = a.length;
		final double [][][] fpn_adj = new double [num_sens][][];
		for (int s = 0; s < num_sens; s++) {
			double [] fp = ((fpn != null) && (fpn[s] != null)) ? fpn[s][0] : null;
			int np = (fp != null) ? fp.length : num_pix;
			fpn_adj[s] = new double [1][np];
			for (int npix = 0; npix < np; npix++) {
				fpn_adj[s][0][npix] = b[s]*((fp != null)? fp[npix] : 0.0) - a[s];
			}
		}
		return fpn_adj;
	}

	/**
	 * Build a plain full-frame task grid for the conditioning test: uniform tile centers in the SENSOR domain
	 * (no virtual-view warp), disparity 0 (infinity), scale 1.0 (plain SET, no accumulation).
	 * The conditioning test must NOT reuse whatever tasks are left in the GPU: after a motion-blur render the
	 * leftover set is the MB secondary tasks with NEGATIVE fractional scales (measured -1/6 on LWIR16 CUAS -
	 * the "negative raw" images), and any leftover render grid is virtual-view warped, losing the same border
	 * ROI on every sensor. By Claude on 07/02/2026.
	 * @param qclt physical scene (geometry source), must be the QuadCLT currently bound to the GPU.
	 * @param disparity_corr disparity correction, pass clt_parameters.imp.disparity_corr + qclt.getDispInfinityRef().
	 * @param threadsMax max threads.
	 * @return TpTask[] covering all tiles, per-sensor xy to be computed by the GPU from its loaded geometry.
	 */
	public static TpTask [] uniformGridTasks( // By Claude on 07/02/2026
			final QuadCLT qclt,
			final double  disparity_corr,
			final int     threadsMax) {
		final int tilesX = qclt.getTilesX();
		final int tilesY = qclt.getTilesY();
		final double [][] pXpYD = new double [tilesX*tilesY][];
		for (int nt = 0; nt < pXpYD.length; nt++) {
			int ty = nt / tilesX, tx = nt % tilesX;
			pXpYD[nt] = new double [] {
					(tx + 0.5) * GPUTileProcessor.DTT_SIZE,
					(ty + 0.5) * GPUTileProcessor.DTT_SIZE,
					0.0};
		}
		return GpuQuad.setInterTasks(         // accumulate=false, global_scale=1.0 -> plain SET tasks
				qclt.getNumSensors(),         // final int                 num_cams,
				qclt.getErsCorrection().getSensorWH()[0], // final int    img_width, // should match pXpYD
				!qclt.hasGPU(),               // final boolean             calcPortsCoordinatesAndDerivatives,
				pXpYD,                        // final double [][]         pXpYD,
				null,                         // final boolean []          selection,
				qclt.getErsCorrection(),      // final GeometryCorrection  geometryCorrection,
				disparity_corr,               // final double              disparity_corr,
				-1,                           // final int                 margin,
				null,                         // final boolean []          valid_tiles,
				threadsMax);                  // final int                 threadsMax)
	}

	/**
	 * Conditioning-test worker: upload the given per-sensor pixel data to the GPU, rebuild the transform domain
	 * with this test's OWN uniform sensor-domain task grid (see {@link #uniformGridTasks}), then render and save
	 * the per-sensor stack via {@link #perSensorAveragesFromTD}. Used for both the conditioned image_data and the
	 * raw /jp4/ baseline so the two stacks are converted with IDENTICAL tasks/kernels and differ only by the data.
	 * Does NOT modify the QuadCLT image_data (a previous version staged raw via setImageData() - that both destroyed
	 * the conditioned data and armed the hasNewImageData/image_data_alt re-pull trap inside execConvertDirect).
	 * By Claude on 07/02/2026.
	 * @param clt_parameters processing parameters (task grid, LPF sigmas).
	 * @param save_qclt the virtual "-CENTER" QuadCLT where the result stack is saved.
	 * @param data [sensor][color][pixel] pixel data to convert (e.g. getOrigImageData() or raw jp4 read).
	 * @param name_tag filename tag, e.g. "-CUAS-PERSENSOR" / "-CUAS-PERSENSOR-RAW".
	 * @param threadsMax max threads.
	 * @param debugLevel debug verbosity.
	 * @return per-sensor mean of finite pixels.
	 */
	public static double [] perSensorFromData( // By Claude on 07/02/2026
			CLTParameters clt_parameters,
			QuadCLT       save_qclt,
			double [][][] data,
			String        name_tag,
			int           threadsMax,
			int           debugLevel) {
		final GpuQuad gpuQuad = save_qclt.getGPUQuad();
		if (!convertFromData(clt_parameters, gpuQuad, data, name_tag, threadsMax, debugLevel)) {
			return null;
		}
		return perSensorAveragesFromTD(gpuQuad, false, name_tag, save_qclt);
	}

	/**
	 * Upload the given per-sensor pixel data and rebuild the main transform-domain buffer with the calibration
	 * task grid (uniform, sensor-domain, scale 1.0 - see {@link #uniformGridTasks}). Extracted from
	 * {@link #perSensorFromData} so the RT calibration step can convert without rendering/saving.
	 * By Claude on 07/03/2026.
	 * @param clt_parameters processing parameters (task grid, LPF sigmas).
	 * @param gpuQuad the GPU to use (its bound physical scene provides the geometry).
	 * @param data [sensor][color][pixel] pixel data to convert.
	 * @param name_tag tag for log messages only.
	 * @param threadsMax max threads.
	 * @param debugLevel debug verbosity.
	 * @return true on success, false if no physical scene is bound to the GPU.
	 */
	public static boolean convertFromData( // By Claude on 07/03/2026
			CLTParameters clt_parameters,
			GpuQuad       gpuQuad,
			double [][][] data,
			String        name_tag,
			int           threadsMax,
			int           debugLevel) {
		final QuadCLT qclt = gpuQuad.getQuadCLT(); // physical scene bound to the GPU (geometry already loaded)
		if (qclt == null) {
			System.out.println("convertFromData(): no physical scene bound to the GPU - skipping");
			return false;
		}
		final TpTask [] tp_tasks = uniformGridTasks(
				qclt,                                                          // final QuadCLT qclt,
				clt_parameters.imp.disparity_corr + qclt.getDispInfinityRef(), // final double  disparity_corr,
				threadsMax);                                                   // final int     threadsMax)
		final ImageDtt image_dtt = new ImageDtt(
				qclt.getNumSensors(),
				clt_parameters.transform_size,
				clt_parameters.img_dtt,
				qclt.isAux(),
				qclt.isMonochrome(),
				qclt.isLwir(),
				clt_parameters.getScaleStrength(qclt.isAux()),
				gpuQuad);
		if (debugLevel > -3) { // host-side data level (raw ~ +20800 counts, conditioned ~ offset-subtracted)
			System.out.println("convertFromData("+name_tag+"): host data s00 mean="+
					meanFinite(data[0][0], gpuQuad.getImageWidth()*gpuQuad.getImageHeight()));
		}
		gpuQuad.setBayerImages(data, true); // FORCE upload the data under test
		qclt.setHasNewImageData(false);     // GPU now holds 'data': makes execConvertDirect()'s internal
		                                    // setBayerImages(false,...) skip its re-pull (base and JNA backends)
		image_dtt.setReferenceTD(           // set OWN tasks + convert into the main TD buffer (kernels applied)
				null,                       // final float [][]          fclt,
				1,                          // final int                 erase_clt, // 1: erase to NaN (honest borders)
				null,                       // final int []              wh, // use sensor dimensions
				clt_parameters.img_dtt,     // final ImageDttParameters  imgdtt_params,
				false,                      // final boolean             use_reference_buffer,
				tp_tasks,                   // 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,
				threadsMax,                 // final int                 threadsMax,
				debugLevel);                // final int                 globalDebugLevel)
		return true;
	}

	/**
	 * First step of the CUAS RT processing flow (curt_calib): per-sensor photometric (re)calibration.
	 * Converts the current scene's conditioned image_data with the calibration task grid, fits the per-sensor
	 * linear corrections over safe tiles ({@link #perSensorLinearFit}), folds them into the 16+16 lwir
	 * offsets/scales and applies them IN MEMORY ONLY ({@link #applyLwirLinearCalibration}): quadCLT_main
	 * carries them to the top-menu configuration SAVED AT WILL after the run (then propagated between
	 * configs with the compare/combine GUI), and to newly created QuadCLT instances. NO file is written
	 * here - INTERFRAME is QuadCLT-workflow/POSES ONLY (Andrey 07/05/2026). Idempotent: with a valid
	 * calibration the fit returns identity (a~0, b~1) and the parameters only get polished.
	 * By Claude on 07/03/2026.
	 * @param clt_parameters processing parameters.
	 * @param master_CLT virtual "-CENTER": combo DSI source (-INTER-INTRA-LMA), in-memory apply, debug saves.
	 * @param ref_scene physical reference scene (photometric owner): applied in memory.
	 * @param quadCLT_main top-level parameter-transfer instance (may be null).
	 * @param save_stacks save the -CUAS-PERSENSOR / -CUAS-PERSENSOR-ADJ debug stacks (test/diagnostic mode).
	 * @param threadsMax max threads.
	 * @param debugLevel debug verbosity.
	 * @return {a[num_sens], b[num_sens]} of the applied correction, or null if calibration was not possible.
	 */
	public static double [][] rtPhotometricCalibration( // By Claude on 07/03/2026
			CLTParameters clt_parameters,
			QuadCLT       master_CLT,
			QuadCLT       ref_scene,
			QuadCLT       quadCLT_main,
			QuadCLT []    scenes,        // already-spawned per-scene instances (may be null) // By Claude on 07/05/2026
			boolean       save_stacks,
			int           threadsMax,
			int           debugLevel) {
		final GpuQuad gpuQuad = master_CLT.getGPUQuad();
		final QuadCLT phys = gpuQuad.getQuadCLT(); // physical scene bound to the GPU: conditioned data + geometry
		if (phys == null) {
			System.out.println("rtPhotometricCalibration(): no physical scene bound to the GPU - skipping");
			return null;
		}
		if (save_stacks) { // render + save the conditioned per-sensor stack (also prints the sensor-average spread)
			perSensorFromData(clt_parameters, master_CLT, phys.getOrigImageData(),
					"-CUAS-PERSENSOR", threadsMax, debugLevel);
		} else { // convert only - no rendering/saving in the production RT step
			if (!convertFromData(clt_parameters, gpuQuad, phys.getOrigImageData(),
					"-CUAS-PERSENSOR", threadsMax, debugLevel)) {
				return null;
			}
		}
		final float [][] cond_images = perSensorImagesFromTD(gpuQuad, false);
		final double [][] ab = perSensorLinearFit(
				master_CLT,               // QuadCLT       center_CLT (combo DSI source, save target)
				cond_images,              // float [][]    per_sensor (conditioned, FPN-corrected)
				gpuQuad.getImageWidth(),  // int           width
				0.5,                      // double        max_strength,  // weak tiles: strength < 0.5
				1.0,                      // double        max_disparity, // far tiles: disparity < 1
				save_stacks,              // boolean       save_adjusted (-CUAS-PERSENSOR-ADJ)
				debugLevel,               // int           debugLevel
				true);                    // boolean       keep_averages: average offset = 0, average scale = 1.0
		if (ab != null) {
			// Apply to the ALREADY-SPAWNED scene instances too: prepareFpixels creates them
			// BEFORE this step with the pre-calibration template values, so without this the
			// SAME run's measurements still condition with stale photometric (POSE-09: 1470-
			// count per-sensor sky spread vs the 17-count calibrated state). Persistence is
			// the user's explicit corr-xml save after the run - nothing is written here.
			// By Claude on 07/05/2026.
			final int num_extra = (scenes != null) ? scenes.length : 0;
			final QuadCLT [] apply_to = new QuadCLT [3 + num_extra];
			apply_to[0] = master_CLT;
			apply_to[1] = ref_scene;
			apply_to[2] = quadCLT_main;
			for (int i = 0; i < num_extra; i++) apply_to[3 + i] = scenes[i]; // nulls are skipped
			applyLwirLinearCalibration(
					phys,                                            // QuadCLT    phys_scene
					apply_to,                                        // QuadCLT [] apply_to (in-memory)
					null,                                            // QuadCLT    save_scene: NONE - INTERFRAME is QuadCLT-workflow/POSES ONLY (Andrey 07/05/2026); photometric persists via an explicit corr-xml save after the run
					ab[0],                                           // double []  a,
					ab[1],                                           // double []  b,
					debugLevel);                                     // int        debugLevel
		}
		return ab;
	}

	/** NaN-tolerant mean of the first num_pix elements (or all when num_pix <= 0). By Claude on 07/02/2026. */
	private static double meanFinite(double [] data, int num_pix) {
		if (data == null) return Double.NaN;
		int n = ((num_pix > 0) && (num_pix < data.length)) ? num_pix : data.length;
		double s = 0.0; int m = 0;
		for (int i = 0; i < n; i++) if (!Double.isNaN(data[i])) { s += data[i]; m++; }
		return (m > 0) ? (s/m) : Double.NaN;
	}
	private static double meanFinite(float [] data) { // By Claude on 07/02/2026
		if (data == null) return Double.NaN;
		double s = 0.0; int m = 0;
		for (int i = 0; i < data.length; i++) if (!Float.isNaN(data[i])) { s += data[i]; m++; }
		return (m > 0) ? (s/m) : Double.NaN;
	}

	/**
	 * RT-seed / conditioning-test companion: render the per-sensor images from RAW /jp4/ source data with
	 * NO photometric / FPN / conditioning correction, saved as &lt;name&gt;-CUAS-PERSENSOR-RAW, to compare against
	 * the conditioned -CUAS-PERSENSOR render. The calibration stays a separate "cheat" (prior/slow software, or
	 * computed in the background of RT tracking) and is never applied on this raw path.
	 * Java-first oracle for the future GPU RT path: Java locates + reads the scene's source tiffs via getJp4Tiff
	 * (the oracle reader - it parses the Boson telemetry first line and restores the ch-6 floating bit-12 needed
	 * for older footage), then FORCE-uploads them H2D and rebuilds the transform domain with the test's own
	 * uniform sensor-domain task grid (see {@link #perSensorFromData}). In RT this same force-H2D is fed by the
	 * four SATA devices (4 sensors each). TIFF-read acceleration (skip Exif/telemetry parse on the uncompressed
	 * 16-bit BE tiff) is deferred - only if RT becomes CPU-bound; SATA-2 covers the ~158 MB/s per path.
	 * By Claude on 07/01/2026; reworked 07/02/2026 to stop reusing leftover GPU tasks (negative-MB-scale /
	 * warped-border artifacts) and to stop overwriting the scene's conditioned image_data.
	 * @param clt_parameters processing parameters (task grid, LPF sigmas). // By Claude on 07/02/2026
	 * @param save_qclt the virtual "-CENTER" QuadCLT where the result stack is saved (main CUAS results live there);
	 *   the raw /jp4/ source, geometry, and GPU are taken from the physical scene currently bound to that GPU
	 *   (gpuQuad.getQuadCLT()), since the virtual center frame has no /jp4/ files of its own.
	 * @param threadsMax max read threads (one per sensor).
	 * @param debugLevel debug verbosity.
	 * @return per-sensor mean of finite raw pixels, or null if no physical scene / source files were found.
	 */
	public static double [] perSensorFromRawJp4(
			CLTParameters clt_parameters, // By Claude on 07/02/2026
			QuadCLT save_qclt,
			int     threadsMax,
			int     debugLevel) {
		final GpuQuad  gpuQuad     = save_qclt.getGPUQuad();
		final QuadCLT  qclt        = gpuQuad.getQuadCLT(); // physical scene bound to the GPU: has the /jp4/ source + geometry
		if (qclt == null) {
			System.out.println("perSensorFromRawJp4(): no physical scene bound to the GPU (getQuadCLT()==null) - skipping raw render");
			return null;
		}
		final double [][][] raw = readRawImageData(qclt, threadsMax, debugLevel);
		if (raw == null) {
			return null;
		}
		// Upload + convert + render + save the raw per-sensor stack to the -CENTER instance, alongside the conditioned one.
		return perSensorFromData(clt_parameters, save_qclt, raw, "-CUAS-PERSENSOR-RAW", threadsMax, debugLevel);
	}

	/**
	 * Read a scene's RAW per-sensor source images from /jp4/ (NO conditioning applied) -
	 * extracted from perSensorFromRawJp4() so the A2/RT ingest (CuasConditioning.
	 * conditionSceneToGpu) can reuse the oracle reader. One thread per sensor (matches
	 * the RT model: 4 SATA paths x 4 sensors read concurrently). getJp4Tiff parses the
	 * Boson telemetry first line and restores the ch-6 floating bit-12 of older footage.
	 * By Claude on 07/05/2026 (extraction; logic unchanged from 07/02/2026).
	 * @param qclt physical scene: /jp4/ source paths + geometry.
	 * @param threadsMax max read threads.
	 * @param debugLevel debug verbosity.
	 * @return [sensor][color][pixel] raw data, or null if source files missing/incomplete.
	 */
	public static double [][][] readRawImageData(
			final QuadCLT qclt,
			final int     threadsMax,
			final int     debugLevel) {
		final int      num_sens    = qclt.getNumSensors();
		final String   set_name    = qclt.getImageName();
		final String [] sourceFiles = qclt.correctionsParameters.getSourcePaths();
		// Map each sensor channel -> index into sourceFiles for THIS scene (matched by set name).
		final int [] channelFiles = new int [num_sens];
		java.util.Arrays.fill(channelFiles, -1);
		int found = 0;
		for (int nFile = 0; nFile < sourceFiles.length; nFile++) {
			if ((sourceFiles[nFile] == null) || (sourceFiles[nFile].length() <= 1)) continue;
			if (!set_name.equals(qclt.correctionsParameters.getNameFromSourceTiff(sourceFiles[nFile]))) continue;
			final int [] channels = qclt.fileChannelToSensorChannels(
					qclt.correctionsParameters.getChannelFromSourceTiff(sourceFiles[nFile]));
			if (channels == null) continue;
			for (int ch : channels) if ((ch >= 0) && (ch < num_sens) && qclt.eyesisCorrections.isChannelEnabled(ch)) {
				channelFiles[ch] = nFile;
				found++;
			}
		}
		if (found == 0) {
			System.out.println("readRawImageData(): no source files found for set '"+set_name+
					"' - is the jp4 source dir available?");
			return null;
		}
		// Read raw per-sensor bayer (NO conditioning) -> [sensor][color][pixel], one thread per sensor.
		// TIFF-read acceleration (skip Exif/telemetry parse) is deferred - only if RT becomes CPU-bound.
		final double [][][] raw = new double [num_sens][][];
		final Thread[] threads = ImageDtt.newThreadArray(threadsMax);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int ncam = ai.getAndIncrement(); ncam < num_sens; ncam = ai.getAndIncrement()) {
						final int nFile = channelFiles[ncam];
						if (nFile < 0) { System.out.println("readRawImageData(): no source file for sensor "+ncam); continue; }
						final ImagePlus imp_src = qclt.eyesisCorrections.getJp4Tiff(
								sourceFiles[nFile],
								qclt.geometryCorrection.woi_tops,
								qclt.geometryCorrection.camera_heights);
						raw[ncam] = qclt.eyesisCorrections.bayerToDoubleStack(
								imp_src, // source Bayer image, linearized, 32-bit float
								null,    // no margins, no oversample
								qclt.isMonochrome());
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		{ // instrumentation - verify what was actually read (raw LWIR should be ~+20800)
			int num_null = 0;
			StringBuffer sb = new StringBuffer();
			for (int ncam = 0; ncam < num_sens; ncam++) {
				if ((raw[ncam] == null) || (raw[ncam][0] == null)) { num_null++; continue; }
				if ((ncam % 4) == 0) sb.append(String.format(" s%02d=%.0f", ncam, meanFinite(raw[ncam][0], 0)));
			}
			if (debugLevel > -3) {
				System.out.println("readRawImageData(): read raw source for set '"+set_name+"' ("+found+
						" source files, "+num_null+" null sensors) means:"+sb);
			}
			if (num_null > 0) {
				System.out.println("readRawImageData(): "+num_null+" sensors have NO raw data for set '"+set_name+
						"' - returning null (would crash or silently keep previous GPU bayer)");
				return null;
			}
		}
		return raw;
	}


	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        boost_exposure, // scale number of accumulated pairs (virtual tracking camera exposure time)
			final double        exposure_boost_factor, // RSLT_A *= pow(boost_exposure, factor); 0.5=sqrt, 0=no scale // By Claude on 05/24/2026
			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){
		// Increase relative amplitude of the boosted-exposure maximum to win against non-boosted.
		// Why is it needed - compensate blur from mis-registration?
		double scale_rslt_a = Math.pow(boost_exposure, exposure_boost_factor); // By Claude on 05/24/2026
		// 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);
												target[CuasMotionLMA.RSLT_A] *= scale_rslt_a;
												// keep [CuasMotionLMA.RSLT_RMS_A]
												
											    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
											// set RSLT_BOOST
											target[CuasMotionLMA.RSLT_EBOOST] = boost_exposure;
										} // 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;
	}
	/**
	 * Removes weaker targets for (almost) same x,y. "Weaker" means later in the targets array, so it needs sortMultiTargets() before.
	 * It also needs sortMultiTargets() after - to compact the array and remove null targets
	 * @param target_multi
	 * @param pix_tolerance 
	 * @return number of removed targets
	 */
	public static int [] removeWeakerDuplicates( // needs sortMultiTargets before (assuming earlier - stronger) and after (to remove null)
			final double [][][][] target_multi,
			final double          pix_tolerance,
			final int             when_fail) {
		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) && (targets[indx2][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)) {
									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) && (targets[indx1][CuasMotionLMA.RSLT_FAIL] == CuasMotionLMA.FAIL_NONE)){
										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
											// FAIL_NONE for both is now tested above
											adup_good.getAndIncrement();
//											targets[indx2] = null;
											targets[indx2][CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_DUPLICATE;
											targets[indx2][CuasMotionLMA.RSLT_WHEN] = when_fail;
											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()};
	}
	

	public static void trimTargets( // limit number of targets per tile
			final double [][][][] target_multi,
			final int             max_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 > max_targets)){
								double [][] targets_trimmed = new double[max_targets][];
								System.arraycopy(
										targets, 0,
										targets_trimmed, 0, max_targets);
								target_multi[nSeq][ntile]=targets_trimmed;
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	public static void setPhase1Score( // Set Phase 1 target index and score (to fine-tune thresholds to reduce false positives
			final double [][][][] target_multi) {
		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){
								for (int ntarg = 0; ntarg <targets.length; ntarg++) {
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_INDEX] =  ntarg;
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_SCORE] =  targets[ntarg][CuasMotionLMA.RSLT_QSCORE];
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_AMP] =    targets[ntarg][CuasMotionLMA.RSLT_A];
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_MSCORE] = targets[ntarg][CuasMotionLMA.RSLT_MSCORE];
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_VSTR] =   targets[ntarg][CuasMotionLMA.RSLT_VSTR];
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_VBOOST] = targets[ntarg][CuasMotionLMA.RSLT_VBOOST];
									targets[ntarg][CuasMotionLMA.RSLT_PHASE1_EBOOST] = targets[ntarg][CuasMotionLMA.RSLT_EBOOST];
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	/**
	 * Fail early after phase 1 to increase performance
	 * Use the following procedure to select values:
	 * 1) Relax them and run the program
	 * 2) Analyze RSLT_PHASE1_* parameters of the survived targets at the last stage (currently, *-CENT.tiff)
	 * 3) Set the threshold values somewhat lower.
	 * Each threshold below, if set  
	 * @param target_multi target data to be modified [nseq][ntile][ntarget][nparam] 
	 * @param niter        current iteration number, will be applied to [RSLT_WHEN]
	 * @param min_mscore   minimal value for [RSLT_MSCORE]
	 * @param early_boosts enforce boosted exposure for boosted motion
	 * @param min_vstr     minimal value for [RSLT_VSTR]
	 * @param min_score    minimal value for [RSLT_QSCORE]
	 * @param min_amp      minimal value for [RSLT_A]
	 * @return             int [] : total number of failures and for each of the 4 input parameters (same target may fail by several parameters 
	 */
	public static int [] earlyFail( // Fail after Phase 1
			final double [][][][] target_multi,
			final int             niter,
			final boolean         early_boosts,
			final double          min_mscore,
			final double          min_vstr,			
			final double          min_score,
			final double          min_amp) {
		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 afail =   new AtomicInteger(0);
		final AtomicInteger aboosts = new AtomicInteger(0);
		final AtomicInteger amscore = new AtomicInteger(0);
		final AtomicInteger avstr =   new AtomicInteger(0);
		final AtomicInteger ascore =  new AtomicInteger(0);
		final AtomicInteger aamp =    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 ntarg = 0; ntarg <targets.length; ntarg++) if (targets[ntarg] != null){
									if (Double.isNaN(targets[ntarg][CuasMotionLMA.RSLT_FAIL]) || (targets[ntarg][CuasMotionLMA.RSLT_FAIL]==CuasMotionLMA.FAIL_NONE)) {
										int fail_type = 0;
										if (early_boosts && (targets[ntarg][CuasMotionLMA.RSLT_VBOOST] > 1) && (targets[ntarg][CuasMotionLMA.RSLT_EBOOST] <= 1)) {
											if (fail_type == 0) fail_type = CuasMotionLMA.FAIL_BOOSTS;
											aboosts.getAndIncrement();
										}
										if (!Double.isNaN(min_mscore) && !(targets[ntarg][CuasMotionLMA.RSLT_MSCORE] >= min_mscore)) {
											if (fail_type == 0) fail_type = CuasMotionLMA.FAIL_MOTION;
											amscore.getAndIncrement();
										}
										if (!Double.isNaN(min_vstr) && !(targets[ntarg][CuasMotionLMA.RSLT_VSTR] >= min_vstr)) {
											if (fail_type == 0) fail_type = CuasMotionLMA.FAIL_MOTION;
											avstr.getAndIncrement();
										}
										if (!Double.isNaN(min_score) && !(targets[ntarg][CuasMotionLMA.RSLT_QSCORE] >= min_score)) {
											if (fail_type == 0) fail_type = CuasMotionLMA.FAIL_Q_LOW;
											ascore.getAndIncrement();
										}
										if (!Double.isNaN(min_amp) && !(targets[ntarg][CuasMotionLMA.RSLT_A] >= min_amp)) {
											if (fail_type == 0) fail_type = CuasMotionLMA.FAIL_A_LOW;
											aamp.getAndIncrement();
										}
										if (fail_type != 0) {
											targets[ntarg][CuasMotionLMA.RSLT_FAIL] = fail_type;
											targets[ntarg][CuasMotionLMA.RSLT_WHEN] = niter;
											afail.getAndIncrement();
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return new int [] {
				afail.get(),
				aboosts.get(),
				amscore.get(),
				avstr.get(),
				ascore.get(),
				aamp.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)
		*/		
		setBadHorizon(
				target_sequence,           // final double [][][] target_sequence,
				target_horizon,            // final double        horizon_y,
				cuasMotion.getNoiseNeibs(), // final double []     noise8,    // null - do not filter by noise
				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];
											target[ntile][CuasMotionLMA.RSLT_VBOOST] = target_b[ntile][CuasMotionLMA.RSLT_VBOOST];
										}
									}
								}
							}
						}
					}
				};
			}		      
			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
		boolean phase1_boosted =       clt_parameters.imp.cuas_phase1_boosted;// By AF on 05/12/2026
		// at least one of phase1_boosted, phase1_base should be used, normally both
		boolean phase1_base =          clt_parameters.imp.cuas_phase1_base || !phase1_boosted;   // By AF on 05/12/2026
		boolean accum_boosted =        clt_parameters.imp.cuas_accum_boosted;// By AF on 05/12/2026
		// at least one of accum_boosted, accum_base should be used, normally both
		boolean accum_base =           clt_parameters.imp.cuas_accum_base || !accum_boosted;   // By AF on 05/12/2026
		
		boolean early_en =        clt_parameters.imp.cuas_early_en;     // Apply early fail filter after phase 1
		boolean early_boosts =    clt_parameters.imp.cuas_early_boosts;     // Apply early fail filter after phase 1
		double  early_mscore =    clt_parameters.imp.cuas_early_mscore; // Minimal motion score  (RSLT_MSCORE)
		double  early_vstr =      clt_parameters.imp.cuas_early_vstr;   // Minimal MV confidence (RSLT_VSTR)
		double  early_score =     clt_parameters.imp.cuas_early_score;  // Minimal score         (RSLT_QSCORE)
		double  early_amp =       clt_parameters.imp.cuas_early_amp;    // Minimal amplitude     (RSLT_A)
		
		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 = clt_parameters.imp.cuas_reuse_non_centered; // By Claude on 05/24/2026
		
		
		
		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");
		cuasMotion.when_fail = CuasMotion.WHEN_PHASE1;
		if (clt_parameters.imp.cuas_reuse_centered) { // By Claude on 05/24/2026
			String x3d_path = parentCLT.getX3dDirectory();
			String centered_path = x3d_path + Prefs.getFileSeparator() + model_prefix+"-ROUND_TWO_GOOD"+".tiff";
			double [][][][] target_sequence_centered = readDoubleHyperHyperstack(centered_path);
			if (target_sequence_centered != null) {
				if (debugLevel > -4) {
					System.out.println("locateAndFreezeTargetsMulti(): reusing phase 2 (centered) data from "+centered_path+", bypassing both phases.");
				}
				return target_sequence_centered; // bypass both phases // By Claude on 05/24/2026
			} else if (debugLevel > -4) {
				System.out.println("locateAndFreezeTargetsMulti(): phase 2 (centered) data not found: "+centered_path+", will compute.");
			}
		} // By Claude on 05/24/2026

//		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
		cuasMotion.when_fail = CuasMotion.WHEN_PHASE1;
		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 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;
		}
		// TODO: Skip calculation of motion_sequence if it is not needed (data restored from the file)
		
		if (target_sequence_multi == null) {
			//		final double [][][][]
			target_sequence_multi = initTargetsFromMotion(motion_sequence);
			// Calculate and set motion vectors strengths/failures
			setEffectiveStrengthMV( // calculate tiles effective strength by the motion vectors. Combine with the target LMA?
					target_sequence_multi, // final double [][][] motion_scan,
					cuasMotion.when_fail,                 // int                 niter, // save iteration number on failure if >=
					cuasMotion.tilesX,     // final int           tilesX)
					// Let them fail here and not be considered at all
					min_score_mv, // 0,                     // final double        min_score_mv,
					speed_min,             // double              speed_min,
					speed_pref,            // double              speed_pref,
					speed_boost);          // double              speed_boost);
			
			// first pass, using non-centered targets
			for (int it_indx=0; it_indx < num_cycles; it_indx++) { // local iteration index for WHEN
				cuasMotion.when_fail = CuasMotion.WHEN_PHASE1+it_indx;
				boolean save_filtered_low =  intermed_low &&  (it_indx < iter_show);
				boolean save_filtered_high = intermed_high && (it_indx < iter_show);
				totals = getRemain(
						target_sequence_multi,  // final double [][][][] target_sequence_multi,
						num_all,                // int []                num_all_in,
						num_undef,              // int []                num_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
						// next will be ignored if num_motion_undef_in != null
						null,                   // int []                num_noncentered_in,
						num_good,               // int []                num_good_in,
						num_bad);               // int []                num_bad_in)
				// for motion-based selection true undefined is
				if (debugLevel > -4) {
					for (int i = 0; i <totals.length; i++) {
						System.out.print(" totals["+i+"]="+totals[i]);
					}
					System.out.println();
				}
				
				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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ms1);
				}			

				if (save_filtered_low  && debug_more) {
					String title = model_prefix+"-PRE-SELECT-n"+cuasMotion.when_fail;
					ImagePlus imp_pre_select = 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,
							false, // good_only,            // final boolean         good_only,
							true, // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_pre_select);  // ImagePlus   imp)
				}
				
				int [][] filter5 = filter5Targets( // will ignore failed tiles
						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 already applied
						0, // 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"+cuasMotion.when_fail,   // Selected to test 
							slice_titles);
					if (!batch_mode) {
						imp_filter5.show();
					}
					parentCLT.saveImagePlusInModelDirectory(imp_filter5);  // 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 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"+cuasMotion.when_fail,// 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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ext);
					// show good and bad accumulated here too?
				}

				// Try both boosted and non-boosted
				int num_new = 0;
//				double effective_boost = boost_accum_pairs; // default virtual exposure scale
				if (phase1_boosted) {
					if (debugLevel > -4) {
						System.out.println ("Accumulating targets with exposure time boosted x"+boost_accum_pairs);
					}
					double [][][][] targets_new_multi=getNewTargets(
							false,              // 			boolean          centered,
							clt_parameters,     // CLTParameters    clt_parameters,
							cuasMotion,         // CuasMotion       cuasMotion,
							fpixels_accum,      // float [][] fpixels_accum,
							targets_nonoverlap, // double [][][]    targets_nonoverlap,
							extended_scan,      // double [][][]    extended_scan,
							frame0,             // int              frame0,
							corr_inc,           // int	            corr_inc,
							half_accum_range,   // int              half_accum_range,
							boost_accum_pairs,                // boost_accum_pairs,  // double           boost_accum_pairs,
							smooth,             // boolean          smooth,
							corr_offset,        // int              corr_offset,      // interframe distance for correlation
							save_filtered_high, // boolean          save_filtered_high,
							model_prefix,       // String           model_prefix,
							cuasMotion.when_fail, // int              niter,
							input_range,        // double           input_range,

							lmax_fraction,   	  //  double          lmax_fraction,
							lmax_flt_neglim,	  //  double          lmax_flt_neglim,
							lmax_flt_hsigma,	  //  double          lmax_flt_hsigma,
							lmax_flt_lsigma,	  //  double          lmax_flt_lsigma,
							lmax_flt_scale,	      //  double          lmax_flt_scale,
							sky_threshold,	      //  double          sky_threshold,
							target_horizon,	      //  double          target_horizon,
							filter_below_horizon, //  double          filter_below_horizon,
							lmax_hack_ridge,	  //  double          lmax_hack_ridge,
							lmax_radius,	      //  double		  lmax_radius,

							centered_radius,      //       lmax_radius,          // double           centered_radius,
							centered_blur,        // 0.5,                  // double           centered_radius_blur,
							duplicate_tolerance,  // 1.0,                  // double           pix_tolerance, // 		

							target_radius,	      //  double		  target_radius,
							n_recenter,	          //  int			n_recenter,
							no_border,	          //  boolean         no_border,
							// Moving target LMA
							lma_sigma,			//	double          lma_sigma,
							wnd_pedestal,		//	double          wnd_pedestal,
							lma_r0,				//	double          lma_r0,
							lma_ovrsht,			//	double          lma_ovrsht,
							// CUAS Motion LMA	//							 	// CUAS Motion LMA parameters
							lma_fit_xy,			//	boolean         lma_fit_xy,
							lma_fit_a,			//	boolean         lma_fit_a,
							lma_fit_c,			//	boolean         lma_fit_c,
							lma_fit_r,			//	boolean         lma_fit_r,
							lma_fit_k,			//	boolean         lma_fit_k,
							lambda,				//	double          lambda,
							lambda_good,		//	double          lambda_good,
							lambda_bad,			//	double          lambda_bad,
							lambda_max,			//	double          lambda_max,
							rms_diff,			//	double          rms_diff,
							num_iter,			//	int             num_iter,
							min_keep,			//	int             min_keep,
							save_filtered_low,	//	boolean         save_filtered_low,
							debug_more,			//	boolean         debug_more,
							good_only,			//	boolean         good_only,
							show_empty,			//	boolean         show_empty,
							num_corr_samples,	//	int             num_corr_samples,
							target_strength,	//	double          target_strength,
							target_frac,		// double [][]     target_frac,
							lma_rms,			// double          lma_rms,
							lma_arms,			// double          lma_arms,
							lma_pre_mina,		// double          lma_pre_mina,
							// use lma_mina2, lma_rrms2 !
							lma_mina,			// double          lma_mina,
							lma_rrms,			// double          lma_rrms,
							lma_maxr,			// double          lma_maxr,
							lma_minr1,			// double          lma_minr1,
							lma_mink,			// double          lma_mink,
							lma_maxk,			// double          lma_maxk,
							lma_a2a,			// double          lma_a2a,
							slow_fast_mismatch,	// double          slow_fast_mismatch,
							fail_mismatch,		// boolean         fail_mismatch,
							offcenter,			// double          offcenter,
							noisemax,			// double          noisemax,
							factor_lim,			// double			factor_lim,
							factor_pow,			// double			factor_pow,
							score_coeff,		// double []		score_coeff,
							slow_score,			// double          slow_score,
							batch_mode,			// boolean         batch_mode,
							debugLevel);		// int             debugLevel);

					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 
							cuasMotion.when_fail,  // final int when_iter,
							debugLevel);           // final int             debugLevel);

					if (debugLevel > -4) {
						System.out.println("Added "+num_new+" new tiles as good/bad with BOOST="+boost_accum_pairs);
					}
					if (save_filtered_low) {
						String title = model_prefix+"-ADDED_NEW_MULTI-n"+cuasMotion.when_fail;
						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 (phase1_base) {
					if (debugLevel > -4) {
						System.out.println ("Accumulating targets with nominal exposure time");
					}
					double [][][][] targets_new_multi=getNewTargets(
							false,              // 			boolean          centered,
							clt_parameters,     // CLTParameters    clt_parameters,
							cuasMotion,         // CuasMotion       cuasMotion,
							fpixels_accum,      // float [][] fpixels_accum,
							targets_nonoverlap, // double [][][]    targets_nonoverlap,
							extended_scan,      // double [][][]    extended_scan,
							frame0,             // int              frame0,
							corr_inc,           // int	            corr_inc,
							half_accum_range,   // int              half_accum_range,
							1.0,                // boost_accum_pairs,                // boost_accum_pairs,  // double           boost_accum_pairs,
							smooth,             // boolean          smooth,
							corr_offset,        // int              corr_offset,      // interframe distance for correlation
							save_filtered_high, // boolean          save_filtered_high,
							model_prefix,       // String           model_prefix,
							cuasMotion.when_fail,              // int              niter,
							input_range,        // double           input_range,

							lmax_fraction,   	  //  double          lmax_fraction,
							lmax_flt_neglim,	  //  double          lmax_flt_neglim,
							lmax_flt_hsigma,	  //  double          lmax_flt_hsigma,
							lmax_flt_lsigma,	  //  double          lmax_flt_lsigma,
							lmax_flt_scale,	      //  double          lmax_flt_scale,
							sky_threshold,	      //  double          sky_threshold,
							target_horizon,	      //  double          target_horizon,
							filter_below_horizon, //  double          filter_below_horizon,
							lmax_hack_ridge,	  //  double          lmax_hack_ridge,
							lmax_radius,	      //  double		  lmax_radius,

							centered_radius,      //       lmax_radius,          // double           centered_radius,
							centered_blur,        // 0.5,                  // double           centered_radius_blur,
							duplicate_tolerance,  // 1.0,                  // double           pix_tolerance, // 		

							target_radius,	      //  double		  target_radius,
							n_recenter,	          //  int			n_recenter,
							no_border,	          //  boolean         no_border,
							// Moving target LMA
							lma_sigma,			//	double          lma_sigma,
							wnd_pedestal,		//	double          wnd_pedestal,
							lma_r0,				//	double          lma_r0,
							lma_ovrsht,			//	double          lma_ovrsht,
							// CUAS Motion LMA	//							 	// CUAS Motion LMA parameters
							lma_fit_xy,			//	boolean         lma_fit_xy,
							lma_fit_a,			//	boolean         lma_fit_a,
							lma_fit_c,			//	boolean         lma_fit_c,
							lma_fit_r,			//	boolean         lma_fit_r,
							lma_fit_k,			//	boolean         lma_fit_k,
							lambda,				//	double          lambda,
							lambda_good,		//	double          lambda_good,
							lambda_bad,			//	double          lambda_bad,
							lambda_max,			//	double          lambda_max,
							rms_diff,			//	double          rms_diff,
							num_iter,			//	int             num_iter,
							min_keep,			//	int             min_keep,
							save_filtered_low,	//	boolean         save_filtered_low,
							debug_more,			//	boolean         debug_more,
							good_only,			//	boolean         good_only,
							show_empty,			//	boolean         show_empty,
							num_corr_samples,	//	int             num_corr_samples,
							target_strength,	//	double          target_strength,
							target_frac,		// double [][]     target_frac,
							lma_rms,			// double          lma_rms,
							lma_arms,			// double          lma_arms,
							lma_pre_mina,		// double          lma_pre_mina,
							// use lma_mina2, lma_rrms2 !
							lma_mina,			// double          lma_mina,
							lma_rrms,			// double          lma_rrms,
							lma_maxr,			// double          lma_maxr,
							lma_minr1,			// double          lma_minr1,
							lma_mink,			// double          lma_mink,
							lma_maxk,			// double          lma_maxk,
							lma_a2a,			// double          lma_a2a,
							slow_fast_mismatch,	// double          slow_fast_mismatch,
							fail_mismatch,		// boolean         fail_mismatch,
							offcenter,			// double          offcenter,
							noisemax,			// double          noisemax,
							factor_lim,			// double			factor_lim,
							factor_pow,			// double			factor_pow,
							score_coeff,		// double []		score_coeff,
							slow_score,			// double          slow_score,
							batch_mode,			// boolean         batch_mode,
							debugLevel);		// int             debugLevel);

					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 
							cuasMotion.when_fail,                // final int when_iter,
							debugLevel);           // final int             debugLevel);

					if (debugLevel > -4) {
						System.out.println("Added "+num_new+" new tiles as good/bad, base (no boost mode)");
					}
					if (save_filtered_low) {
						String title = model_prefix+"-ADDED_NEW_MULTI-n"+cuasMotion.when_fail;
						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) { // total for boost/no-boost
					if (debugLevel > -4) System.out.println ("Nothing added from last try, breaking the loop.\n");
					break;  // breaks here
				}
				totals = getRemain(
						target_sequence_multi,  // final double [][][][] target_sequence_multi,
						num_all,                // int []                num_all_in,
						num_undef,              // int []                num_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
						// next will be ignored if num_motion_undef_in != null
						null,                   // int []                num_noncentered_in,
						num_good,               // int []                num_good_in,
						num_bad);               // int []                num_bad_in)
				if (debugLevel > -4) printStats ("After iteration "+cuasMotion.when_fail, true, num_all, num_undef, num_good, num_bad);
				if (debugLevel > -4) {
					System.out.println("Non-centered iteration "+cuasMotion.when_fail+" 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)
			}
		} // end of phase 1
		
		
		
		if (debugLevel > -4) {
			System.out.println("\n========================== Starting centered iterations =============================.\n");
		}
		cuasMotion.when_fail = CuasMotion.WHEN_PHASE2; // whill be overwritten inside loop
		// By Claude on 05/05/2026 — optional MV refinement parameters for centered loop
		int     recalc_mv_num =   slow_mode ? 0 : clt_parameters.imp.cuas_recalc_mv_num;
		double  recalc_mv_boost = clt_parameters.imp.cuas_recalc_mv_boost;
		double  recalc_mv_corr  =  clt_parameters.imp.cuas_recalc_mv_corr;
		double  recalc_mv_r0    = clt_parameters.imp.cuas_recalc_mv_r0;
		double  recalc_mv_r1    = clt_parameters.imp.cuas_recalc_mv_r1;
		double  recalc_mv_r0f   = clt_parameters.imp.cuas_recalc_mv_r0f;
		double  recalc_mv_r1f   = clt_parameters.imp.cuas_recalc_mv_r1f;
		double  recalc_mv_max2  = clt_parameters.imp.cuas_recalc_mv_max2;
		int     recalc_mv_icorr = (int) Math.round(recalc_mv_corr);
		
		int trim_first = clt_parameters.imp.cuas_trim_first;
		
	    double lma_rrms2 =       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)
	    double lma_mina2 =       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
		// optionally trim pass-one targets
		if (trim_first > 0) {
			if (debugLevel > -4) {
				System.out.println("Will trim Phase One (noncentered) data to keep not more than "+clt_parameters.imp.cuas_trim_first+" tasrgets in each tile");
			}
			// evaluate scores here to reduce the number of attempts for centered targets
			getEffectiveStrengthLMA(
					target_sequence_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.getNoiseNeibs(), // final int []          noise_neibs,
					target_horizon,    // final double        lma_horizon, // horizon as maximal pixel Y
					cuasMotion.tilesX);            // final int         tilesX,
			getScore(
					target_sequence_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 (early_en) {
				int [] early_fail =  earlyFail( // Fail after Phase 1
						target_sequence_multi,  // final double [][][][] target_multi,
						cuasMotion.when_fail,   // final int             niter,
						early_boosts,           // final boolean         early_boosts,
						early_mscore,           // final double          min_mscore,
						early_vstr,             // final double          min_vstr,			
						early_score,            // final double          min_score,
						early_amp);             // final double          min_amp);
				if (debugLevel > -4) {
					System.out.println("Early failed:");
					System.out.println(String.format("     total: %5d", early_fail[0]));
					System.out.println(String.format("    boosts: %5d, enforced=%b", early_fail[1],early_boosts));
					System.out.println(String.format("    mscore: %5d (<%f)", early_fail[2],early_mscore));
					System.out.println(String.format("      vstr: %5d (<%f)", early_fail[3],early_vstr));
					System.out.println(String.format("     score: %5d (<%f)", early_fail[4],early_score));
					System.out.println(String.format(" amplitude: %5d (<%f)", early_fail[5],early_amp));
				}
			} else {
				if (debugLevel > -4) {
					System.out.println("Early fail DISABLED.");
				}
			}
			
			sortMultiTargets(
					target_sequence_multi, // final double [][][][] target_multi,
					true);                 // final boolean         trim_nulls) { // trim null targets
			int [] num_duplicates = removeWeakerDuplicates( // needs sortMultiTargets and after
					target_sequence_multi, // final double [][][][] target_multi,
					duplicate_tolerance,  // final double          pix_tolerance)
					cuasMotion.when_fail); // final int             when_fail)
			if (debugLevel > -4) System.out.println("Removed "+num_duplicates[1]+" good duplicate targets (total with bad - "+num_duplicates[0]+").");

			sortMultiTargets( // moves FAILED after good (and duplicates are now FAILED
					target_sequence_multi, // final double [][][][] target_multi,
					true);                 // final boolean         trim_nulls) { // trim null targets
			
	  	    trimTargets( // limit number of targets per tile
	  	    		target_sequence_multi, // final double [][][][] target_multi,
	  	    		trim_first);           // final int             max_targets) {
	  	    
	  		setPhase1Score( // Set Phase 1 target index and score (to fine-tune thresholds to reduce false positives
	  				target_sequence_multi); // final double [][][][] target_multi) {
	  	    
			if (intermed_low) {
				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_TRIMMED",// 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)
			}
		} else {
			if (debugLevel > -4) {
				System.out.println("Will not trim Phase One (noncentered) data, as clt_parameters.imp.cuas_trim_first="+clt_parameters.imp.cuas_trim_first);
			}

		}
		/*
		int niter_lim = niter+ num_cycles;
		int iter_show1 = iter_show + niter;
		int niter0 = niter; // for debugging, so (niter - niter0) will start from 0
		*/
//		for (; niter < niter_lim; niter++) {
		for (int it_indx2=0; it_indx2 < num_cycles; it_indx2++) {
			cuasMotion.when_fail = CuasMotion.WHEN_PHASE2+it_indx2;
			boolean         good_only=    false;
			boolean         show_empty =  true; // false; // show scenes with no (valid) targets
			boolean save_filtered_low =  intermed_low &&  (it_indx2 < iter_show);
			boolean save_filtered_high = intermed_high && (it_indx2 < iter_show);
			totals = getRemain(
					target_sequence_multi,  // final double [][][][] target_sequence_multi,
					num_all,                // int []                num_all_in,
					null,                   // int []                num_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
					// next will be ignored if num_motion_undef_in != null
					num_undef,              // int []                num_noncentered_in,
					num_good,               // int []                num_good_in,
					num_bad);               // int []                num_bad_in)
			
			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
					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"+cuasMotion.when_fail,   // 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"+cuasMotion.when_fail,// 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 
					target_sequence_multi,    // double [][][][] target_sequence_multi
					filter5);                 // boolean [][] filter5)
			
			// Anything remains? we'll see after extension
			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"+cuasMotion.when_fail,// String        title,
						!batch_mode,          // boolean       show,
						cuasMotion.tilesX);   //  int           tilesX) {
				parentCLT.saveImagePlusInModelDirectory(imp_novl);
			}
			*/

			int num_new = 0;
			boolean need_sel_to_try = true;
			double effective_boost = boost_accum_pairs; // default virtual exposure scale
			if (accum_boosted) {
				double [][][] targets_nonoverlap = applyFilter( // motion vectors // will have nulls not top try 
						target_sequence_multi,    // double [][][][] target_sequence_multi
						filter5);                 // boolean [][] filter5)
				if (save_filtered_low  && debug_more && need_sel_to_try) {
					// 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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_novl);
					need_sel_to_try = false;
				}
				
				// 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_num > 0) {
					effective_boost = recalc_mv_boost; // virtual exposure scale used in refineMotionVectors()
//					int corr_offset_scale = (int) Math.round(recalc_mv_boost); // TODO: promote to config or derive from other params	
					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,
							recalc_mv_icorr,     // int           corr_offset_scale,
							0,                   // double              mv_diff_max,        // if > 0, fail for high MV correction
							0, // min_score_mv, // 0,                     // final double        min_score_mv,
							speed_min,           // double              speed_min,
							speed_pref,          // double              speed_pref,
							speed_boost,         // double              speed_boost);
							"-first",            // String              dbg_suffix,
							cuasMotion.when_fail, // int                 dbg_iter_index,
							debugLevel); // -1); // debugLevel);         // int             debugLevel)
					if (recalc_mv_num > 1) {
					refineMotionVectors( // just testing conversion
							clt_parameters,      // CLTParameters   clt_parameters,
							batch_mode,          // boolean         batch_mode,
							cuasMotion,          // CuasMotion      cuasMotion,
							recalc_mv_boost,     // double          recalc_mv_boost,
							recalc_mv_r0f,       // double          recalc_mv_r0,
							recalc_mv_r1f,       // 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,
							recalc_mv_icorr,     // final int           corr_offset_scale,
							recalc_mv_max2,      // double              mv_diff_max,        // if > 0, fail for high MV correction
							0, // min_score_mv, // 0,                     // final double        min_score_mv,
							speed_min,           // double              speed_min,
							speed_pref,          // double              speed_pref,
							speed_boost,         // double              speed_boost);
							"-second",           // String              dbg_suffix,
							cuasMotion.when_fail,// int                 dbg_iter_index,
							debugLevel); // -1); // debugLevel);         // int             debugLevel)
					}
				}
				boolean debug_extra = debugLevel> 5; //-5;
				if (save_filtered_low  && debug_more && debug_extra) {
					ImagePlus imp_ext = showTargetSequence(
							targets_nonoverlap,          // double [][][] vector_fields_sequence,
							slice_titles,         // String []     titles, // all slices*frames titles or just slice titles or null
							model_prefix+"-BEFORE-EXTENDED-CENTERED-n"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ext);
				}
				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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ext);
				}
				
				double [][][][] targets_new_multi=getNewTargets(
						true,               // 			boolean          centered,
						clt_parameters,     // CLTParameters    clt_parameters,
						cuasMotion,         // CuasMotion       cuasMotion,
						fpixels_accum,      // float [][] fpixels_accum,
						targets_nonoverlap, // double [][][]    targets_nonoverlap,
						extended_scan,      // double [][][]    extended_scan,
						frame0,             // int              frame0,
						corr_inc,           // int	            corr_inc,
						half_accum_range,   // int              half_accum_range,
						// next is switched before the default exposure boost (same as pass one) and recalc_mv_boost
						effective_boost, // boost_accum_pairs,  // double           boost_accum_pairs,
						smooth,             // boolean          smooth,
						corr_offset,        // int              corr_offset,      // interframe distance for correlation
						save_filtered_high, // boolean          save_filtered_high,
						model_prefix,       // String           model_prefix,
						cuasMotion.when_fail,// int              niter,
						input_range,        // double           input_range,

						lmax_fraction,   	  //  double          lmax_fraction,
						lmax_flt_neglim,	  //  double          lmax_flt_neglim,
						lmax_flt_hsigma,	  //  double          lmax_flt_hsigma,
						lmax_flt_lsigma,	  //  double          lmax_flt_lsigma,
						lmax_flt_scale,	      //  double          lmax_flt_scale,
						sky_threshold,	      //  double          sky_threshold,
						target_horizon,	      //  double          target_horizon,
						filter_below_horizon, //  double          filter_below_horizon,
						lmax_hack_ridge,	  //  double          lmax_hack_ridge,
						lmax_radius,	      //  double		  lmax_radius,
						
						lmax_radius,          // double        centered_radius,
						0.5,                  // double        centered_radius_blur,
						1.0,                  // double        pix_tolerance, // 		
						
						target_radius,	      //  double		  target_radius,
						n_recenter,	          //  int			n_recenter,
						no_border,	          //  boolean         no_border,
						// Moving target LMA
						lma_sigma,			//	double          lma_sigma,
						wnd_pedestal,		//	double          wnd_pedestal,
						lma_r0,				//	double          lma_r0,
						lma_ovrsht,			//	double          lma_ovrsht,
						// CUAS Motion LMA	//							 	// CUAS Motion LMA parameters
						lma_fit_xy,			//	boolean         lma_fit_xy,
						lma_fit_a,			//	boolean         lma_fit_a,
						lma_fit_c,			//	boolean         lma_fit_c,
						lma_fit_r,			//	boolean         lma_fit_r,
						lma_fit_k,			//	boolean         lma_fit_k,
						lambda,				//	double          lambda,
						lambda_good,		//	double          lambda_good,
						lambda_bad,			//	double          lambda_bad,
						lambda_max,			//	double          lambda_max,
						rms_diff,			//	double          rms_diff,
						num_iter,			//	int             num_iter,
						min_keep,			//	int             min_keep,
						save_filtered_low,	//	boolean         save_filtered_low,
						debug_more,			//	boolean         debug_more,
						good_only,			//	boolean         good_only,
						show_empty,			//	boolean         show_empty,
						num_corr_samples,	//	int             num_corr_samples,
						target_strength,	//	double          target_strength,
						target_frac,		// double [][]     target_frac,
						lma_rms,			// double          lma_rms,
						lma_arms,			// double          lma_arms,
						lma_pre_mina,		// double          lma_pre_mina,
						// use lma_mina2, lma_rrms2 !
						lma_mina2,			// double          lma_mina,
						lma_rrms2,			// double          lma_rrms,
						lma_maxr,			// double          lma_maxr,
						lma_minr1,			// double          lma_minr1,
						lma_mink,			// double          lma_mink,
						lma_maxk,			// double          lma_maxk,
						lma_a2a,			// double          lma_a2a,
						slow_fast_mismatch,	// double          slow_fast_mismatch,
						fail_mismatch,		// boolean         fail_mismatch,
						offcenter,			// double          offcenter,
						noisemax,			// double          noisemax,
						factor_lim,			// double			factor_lim,
						factor_pow,			// double			factor_pow,
						score_coeff,		// double []		score_coeff,
						slow_score,			// double          slow_score,
						batch_mode,			// boolean         batch_mode,
						debugLevel);		// int             debugLevel);
				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 
						cuasMotion.when_fail,  // final int when_iter,
						debugLevel);           // final int             debugLevel);

				if (debugLevel > -4) {
					System.out.println("Added "+num_new+" new tiles as good/bad at boosted iteration "+cuasMotion.when_fail+".");
				}
			}
			if (accum_base) {
				double [][][] targets_nonoverlap = applyFilter( // motion vectors // will have nulls not top try 
						target_sequence_multi,    // double [][][][] target_sequence_multi
						filter5);                 // boolean [][] filter5)
				if (save_filtered_low  && debug_more && need_sel_to_try) {
					// 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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_novl);
				}
				
				// 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_num > 0) {
					refineMotionVectors(
							clt_parameters,      // CLTParameters   clt_parameters,
							batch_mode,          // boolean         batch_mode,
							cuasMotion,          // CuasMotion      cuasMotion,
							1.0, // 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,
							recalc_mv_icorr,     // final int           corr_offset_scale,
							0,                   // double              mv_diff_max,        // if > 0, fail for high MV correction
							0, // min_score_mv, // 0,                     // final double        min_score_mv,
							speed_min,           // double              speed_min,
							speed_pref,          // double              speed_pref,
							speed_boost,         // double              speed_boost);
							"-first",            // String              dbg_suffix,
							cuasMotion.when_fail,      // int                 dbg_iter_index,
							debugLevel); // -1); // debugLevel);         // int             debugLevel)
					if (recalc_mv_num > 1) {					
					refineMotionVectors(
							clt_parameters,      // CLTParameters   clt_parameters,
							batch_mode,          // boolean         batch_mode,
							cuasMotion,          // CuasMotion      cuasMotion,
							1.0, // recalc_mv_boost,     // double          recalc_mv_boost,
							recalc_mv_r0f,        // double          recalc_mv_r0,
							recalc_mv_r1f,        // 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,
							recalc_mv_icorr,     // final int           corr_offset_scale,
							recalc_mv_max2,      // double              mv_diff_max,        // if > 0, fail for high MV correction
							0, // min_score_mv, // 0,                     // final double        min_score_mv,
							speed_min,           // double              speed_min,
							speed_pref,          // double              speed_pref,
							speed_boost,         // double              speed_boost);
							"-second",           // String              dbg_suffix,
							cuasMotion.when_fail,      // int                 dbg_iter_index,
							debugLevel); // -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"+cuasMotion.when_fail,// String        title,
							!batch_mode,          // boolean       show,
							cuasMotion.tilesX);   //  int           tilesX) {
					parentCLT.saveImagePlusInModelDirectory(imp_ext);
				}
				
				double [][][][] targets_new_multi=getNewTargets(
						true,               // 			boolean          centered,
						clt_parameters,     // CLTParameters    clt_parameters,
						cuasMotion,         // CuasMotion       cuasMotion,
						fpixels_accum,      // float [][] fpixels_accum,
						targets_nonoverlap, // double [][][]    targets_nonoverlap,
						extended_scan,      // double [][][]    extended_scan,
						frame0,             // int              frame0,
						corr_inc,           // int	            corr_inc,
						half_accum_range,   // int              half_accum_range,
						1.0,                // boost_accum_pairs,  // double           boost_accum_pairs,
						smooth,             // boolean          smooth,
						corr_offset,        // int              corr_offset,      // interframe distance for correlation
						save_filtered_high, // boolean          save_filtered_high,
						model_prefix,       // String           model_prefix,
						cuasMotion.when_fail,// int              niter,
						input_range,        // double           input_range,

						lmax_fraction,   	  //  double          lmax_fraction,
						lmax_flt_neglim,	  //  double          lmax_flt_neglim,
						lmax_flt_hsigma,	  //  double          lmax_flt_hsigma,
						lmax_flt_lsigma,	  //  double          lmax_flt_lsigma,
						lmax_flt_scale,	      //  double          lmax_flt_scale,
						sky_threshold,	      //  double          sky_threshold,
						target_horizon,	      //  double          target_horizon,
						filter_below_horizon, //  double          filter_below_horizon,
						lmax_hack_ridge,	  //  double          lmax_hack_ridge,
						lmax_radius,	      //  double		  lmax_radius,
						
						lmax_radius,          // double           centered_radius,
						0.5,                  // double           centered_radius_blur,
						1.0,                  // double           pix_tolerance, // 		
						
						target_radius,	      //  double		  target_radius,
						n_recenter,	          //  int			n_recenter,
						no_border,	          //  boolean         no_border,
						// Moving target LMA
						lma_sigma,			//	double          lma_sigma,
						wnd_pedestal,		//	double          wnd_pedestal,
						lma_r0,				//	double          lma_r0,
						lma_ovrsht,			//	double          lma_ovrsht,
						// CUAS Motion LMA	//							 	// CUAS Motion LMA parameters
						lma_fit_xy,			//	boolean         lma_fit_xy,
						lma_fit_a,			//	boolean         lma_fit_a,
						lma_fit_c,			//	boolean         lma_fit_c,
						lma_fit_r,			//	boolean         lma_fit_r,
						lma_fit_k,			//	boolean         lma_fit_k,
						lambda,				//	double          lambda,
						lambda_good,		//	double          lambda_good,
						lambda_bad,			//	double          lambda_bad,
						lambda_max,			//	double          lambda_max,
						rms_diff,			//	double          rms_diff,
						num_iter,			//	int             num_iter,
						min_keep,			//	int             min_keep,
						save_filtered_low,	//	boolean         save_filtered_low,
						debug_more,			//	boolean         debug_more,
						good_only,			//	boolean         good_only,
						show_empty,			//	boolean         show_empty,
						num_corr_samples,	//	int             num_corr_samples,
						target_strength,	//	double          target_strength,
						target_frac,		// double [][]     target_frac,
						lma_rms,			// double          lma_rms,
						lma_arms,			// double          lma_arms,
						lma_pre_mina,		// double          lma_pre_mina,
						// use lma_mina2, lma_rrms2 !
						lma_mina2,			// double          lma_mina,
						lma_rrms2,			// double          lma_rrms,
						lma_maxr,			// double          lma_maxr,
						lma_minr1,			// double          lma_minr1,
						lma_mink,			// double          lma_mink,
						lma_maxk,			// double          lma_maxk,
						lma_a2a,			// double          lma_a2a,
						slow_fast_mismatch,	// double          slow_fast_mismatch,
						fail_mismatch,		// boolean         fail_mismatch,
						offcenter,			// double          offcenter,
						noisemax,			// double          noisemax,
						factor_lim,			// double			factor_lim,
						factor_pow,			// double			factor_pow,
						score_coeff,		// double []		score_coeff,
						slow_score,			// double          slow_score,
						batch_mode,			// boolean         batch_mode,
						debugLevel);		// int             debugLevel);

				//int 
				int num_new1 = 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 
						cuasMotion.when_fail,                 // final int when_iter,
						debugLevel);           // final int             debugLevel);
				num_new += num_new1;
				if (debugLevel > -4) {
					System.out.println("Added "+num_new1+" new tiles as good/bad at base iteration "+cuasMotion.when_fail+".");
				}
				if (save_filtered_low) {
					String title = model_prefix+"-ADDED_NEW_CENTERED_MULTI-n"+cuasMotion.when_fail;
					title+="-BOOST"+effective_boost+"-NOBOOST"; // 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,  // final double [][][][] target_sequence_multi,
					num_all,                // int []                num_all_in,
					null,                   // int []                num_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
					// next will be ignored if num_motion_undef_in != null
					num_undef,              // int []                num_noncentered_in,
					num_good,               // int []                num_good_in,
					num_bad);               // int []                num_bad_in)
			
			if (debugLevel > -4) printStats ("After centered iteration "+cuasMotion.when_fail, true, num_all, num_undef, num_good, num_bad);
			if (debugLevel > -4) {
				System.out.print((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+": "));
				System.out.println("Centered iteration "+cuasMotion.when_fail+" 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)
		}
		// this save always!
		ImagePlus imp_good_only = 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_GOOD",// String        title,
				true,                    // final boolean         good_only,
				false,                   // final boolean         show_empty, // show scenes with no (valid) targets
				!batch_mode,             // boolean       show,
				cuasMotion.tilesX);      //  int           tilesX) {
		parentCLT.saveImagePlusInModelDirectory(imp_good_only);  // ImagePlus   imp)
		return target_sequence_multi; // target_sequence; // contains all tiles, good or bad, their data and when they were defined
	}
	
	private static double [][][][] getNewTargets(
			boolean          centered,
			CLTParameters    clt_parameters,
			CuasMotion       cuasMotion,
			float [][] fpixels_accum,
			double [][][]    targets_nonoverlap,   // final double [][][] vector_fields, // centers
			double [][][]    extended_scan,
			int              frame0,           // for vector_field[0]
			int	             corr_inc,         // final int           frame_step,  // keep for slow
			int              half_accum_range, // final int           half_range,  // scale for slow
			double           boost_accum_pairs,// final double        boost_pairs,
			boolean          smooth,           // final boolean       smooth,
			int              corr_offset,      // interframe distance for correlation
			boolean          save_filtered_high,
			String           model_prefix,
			int              niter,
			double           input_range,
			double          lmax_fraction,
			double          lmax_flt_neglim,
			double          lmax_flt_hsigma,
			double          lmax_flt_lsigma,
			double          lmax_flt_scale,
			double          sky_threshold,
			double          target_horizon,
			double          filter_below_horizon,
			double          lmax_hack_ridge,
			double			lmax_radius,
			
			double          centered_radius,
			double          centered_radius_blur,
			double          duplicate_tolerance, // 		
			
			double			target_radius,        // final double        centroid_radius,
			int				n_recenter,           // final int           n_recenter, //  re-center window around new maximum. 0 -no refines (single-pass)
			boolean         no_border,
							// Moving target LMA
			double          lma_sigma,
			double          wnd_pedestal,
			double          lma_r0,
			double          lma_ovrsht,
						 	// CUAS Motion LMA parameters
			boolean         lma_fit_xy,
			boolean         lma_fit_a,
			boolean         lma_fit_c,
			boolean         lma_fit_r,
			boolean         lma_fit_k,
			double          lambda,
			double          lambda_good,
			double          lambda_bad,
			double          lambda_max,
			double          rms_diff,
			int             num_iter,
			int             min_keep, //  2; // keep at least this number of candidates before using cuas_lma_pre_mina filter
			boolean         save_filtered_low,
			boolean         debug_more,
			boolean         good_only,
			boolean         show_empty, // show scenes with no (valid) targets
			int             num_corr_samples,
			double          target_strength,
			double [][]     target_frac, // pairs - strength, minimal fraction for that strength
			double          lma_rms, // =       1.5;  // Maximal RMS (should always match, regardless if A)
			double          lma_arms, // =      0.06; // Maximal absolute RMS (should match one of cuas_lma_arms OR cuas_lma_rrms (0.484)
			double          lma_pre_mina,
			// use lma_mina2, lma_rrms2 !
			double          lma_mina,
			double          lma_rrms, // =      0.15; // Maximal relative to A rms. OK is when (RMS < cuas_lma_arms) || (RMS < cuas_lma_rrms * A)
			double          lma_maxr, // =      5.0;  // Minimal K (overshoot) = 3.0
			double          lma_minr1,// =      1.0;  // Minimal R1 (radius of positive peak)
			double          lma_mink, // =      0.0;  // Minimal K (overshoot) = 1.0
			double          lma_maxk, // =      5.0;  // Minimal K (overshoot) = 3.0//			final double        lma_a2a,
			double          lma_a2a,
			double          slow_fast_mismatch, // // 1.5; allow larger mismatch between slow and fast
			boolean         fail_mismatch,
			double          offcenter,
			double          noisemax,
			double			factor_lim,
			double			factor_pow,
			double []		score_coeff,
			double          slow_score,           // multiply total score for targets detected in slow target mode
			boolean         batch_mode,
			int             debugLevel){
		// perform new accumulations of shifted non-conflicting tiles
		float [][] fpixels_accumulated = cuasMotion.shiftAndRenderAccumulate(
				clt_parameters,   // CLTParameters       clt_parameters,
				centered,         // 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?"-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,
					cuasMotion.slice_titles);
			imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
			if (!batch_mode) {
				imp_acc.show();
			}
			cuasMotion.parentCLT.saveImagePlusInModelDirectory(imp_acc);  // ImagePlus   imp)
		}

		// replace center frames with the accumulated ones
		if (debugLevel > -4) {
			System.out.println("Starting render,"+(centered?"centered ":"")+" 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
				centered,             // final boolean       centered,
				boost_accum_pairs,    // final double        boost_exposure,
				clt_parameters.imp.cuas_exposure_boost_factor, // final double  exposure_boost_factor, // By Claude on 05/24/2026
				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,      //lmax_radius,    // final double        centered_radius,
				centered_radius_blur, // 0.5,                  // final double        centered_radius_blur,
				duplicate_tolerance,  // 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,
				cuasMotion.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?"-CENTERED":"")+"-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,
					cuasMotion.slice_titles);
			imp_acc.getProcessor().setMinAndMax(-input_range/2, input_range/2);
			if (!batch_mode) {
				imp_acc.show();
			}
			cuasMotion.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?"-CENTERED":"")+"-n"+niter;
			if (boost_accum_pairs > 1.0) {
				title+="-BOOST"+boost_accum_pairs;
			}
			ImagePlus imp_new = showTargetSequence(
					targets_new_multi,    // final double [][][][] targets_multi,
					cuasMotion.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) {
			cuasMotion.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.getNoiseNeibs(), // final int []          noise_neibs,
				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"+(centered?"_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,
					cuasMotion.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,
					false, // good_only,            // final boolean         good_only,
					true,  // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			cuasMotion.parentCLT.saveImagePlusInModelDirectory(imp_new_scores);  // ImagePlus   imp)
		}
		return targets_new_multi;
	}
	
	/**
	 * 		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 [][][][] 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 ?(55+38*80) : -1;
		final int dbg_seq0 = debug_now ? 166:-1; //27: -1; 
		final int dbg_seq1 = debug_now ? 166:-1; // 29: -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);
						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 ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
								System.out.println("filter5Targets()):0: ntile="+ntile+", nSeq="+nSeq);
							}
							if (select_new){
								if (use_motion ) {  // Get motion from target 0 - it has only motion vectors
									// should have a single target containing vlaid (not to weak) motion vector.
									// now weak have failure already set, so ignore only failed or used
									// use only target[0] here, others are added
									double [][] targets = target_sequence_multi[nSeq][ntile];
									prohibit[ntile] = (targets == null) ||
											(targets.length != 1) ||
											(!Double.isNaN(targets[0][CuasMotionLMA.RSLT_FAIL]) && //
											(targets[0][CuasMotionLMA.RSLT_FAIL]!= CuasMotionLMA.FAIL_NONE));
								} 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; // number of new tiles added at this pass
							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+" prohibit["+ntile+"]="+prohibit[ntile]);
									}
									if (!prohibit[ntile]) { // targets[ntile][conf_index] >0 here by the prohibit preset
										boolean ismax = true;
										if (select_new) {
											double [][] targets = target_sequence_multi[nSeq][ntile];
											if (use_motion) {
												check_max_motion:{
													double cval = targets[0][CuasMotionLMA.RSLT_MSCORE] - min_confidence;
//													double cval = motion[ntile][CuasMotionLMA.RSLT_MSCORE] - min_confidence;
													// should be already >= 0, filtered once before iterations
													if (cval <= 0) {
														ismax = false;
														prohibit[ntile] = true; // not needed
														if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
															System.out.println("filter5Targets()):10: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																	", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
														}
														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;
																double val = target_sequence_multi[nSeq][indx][0][CuasMotionLMA.RSLT_MSCORE]- min_confidence;
																if (val > cval) {
																	ismax = false;
																	break check_max_motion;
																}
															}
														}
													}
													filter5[nSeq][ntile] = 0;
													// Also mark used
													target_sequence_multi[nSeq][ntile][0][CuasMotionLMA.RSLT_CENTERED] =  CuasMotionLMA.CENTERED_USED;
												}
											} else { // not using motion, only consider non-centered good tiles
												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
															if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
																System.out.println("filter5Targets()):11: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																		", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
															}
															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;
																				if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
																					System.out.println("filter5Targets()):12: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																							", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
																				}
																				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
														if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
															System.out.println("filter5Targets()):13: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																	", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
														}
														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;
																			if ((ntile == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
																				System.out.println("filter5Targets()):14: ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																						", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
																			}
																			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;
														if ((indx == dbg_tile) && (nSeq >= dbg_seq0) && (nSeq <= dbg_seq1)) {
															System.out.println("filter5Targets()):15: indx="+indx+", ntile="+ntile+", nSeq="+nSeq+", npass="+npass+
																	", num_this_pass="+num_this_pass+", num_total="+num_total+" prohibit["+ntile+"]="+prohibit[ntile]);
														}
													}
												}
											}
											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) { // && (!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("16.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 when_fail) {
		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;
									target[CuasMotionLMA.RSLT_WHEN] = when_fail;
								}
							}
						}
					}
				}
			};
		}		      
		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 int []          noise_neibs,
//			final double []       noise_map,
//			final double          noise_max,
			final double          lma_horizon, // horizon as maximal pixel Y
			final int             tilesX	) {
		final int all_neibs = (1 << 9) -1; // 511
		final double abs_offcenter= offcenter*GPUTileProcessor.DTT_SIZE;
//		final double []       noise_map = null;
//		final double          noise_max = 0;
		
		// 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 = -( 51+38*80); // -(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;
//										double MV = lma_rslts[CuasMotionLMA.RSLT_Y];
										/*
										 * 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*abs_offcenter;
											double ay = Math.abs(y) - 0.5*abs_offcenter; // 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_neibs == null) || (noise_neibs[ntile] == all_neibs) || // faster shortcut not to check direction 
														((noise_neibs[ntile] & (1 << TileNeibs.getDir(x,y,abs_offcenter))) != 0 )) {
													lma_rslts[CuasMotionLMA.RSLT_FAIL] = CuasMotionLMA.FAIL_FAR;
													break try_failures; // There is a valid tile in that direction - it should care about this target
												}
												// 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;
										}
										// 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_QCENTER] =    quality_factors[IMPORTANCE_CENTER];
									}
								}
							}
						}
					}
				}
			};
		}		      
		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];
											if (! Double.isNaN(lma_rslts[CuasMotionLMA.RSLT_MSCORE])) {
												quality_factors[IMPORTANCE_MV] =       lma_rslts[CuasMotionLMA.RSLT_MSCORE];
											}
											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_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
			// next will be ignored if num_motion_undef_in != null
			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 boolean motion_mode = num_motion_undef_in != null;
		final int [] num_all =          (num_all_in !=   null) ?       num_all_in :          new int [num_seq];
		final int [] num_motion_undef = motion_mode ?                  num_motion_undef_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_motion_undef, 0); // tiles, not yet tested from motion
		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 (Double.isNaN(tsm[ntarg][CuasMotionLMA.RSLT_CENTERED]) && Double.isNaN(ts_fail)){ // happens only in motion mode
											num_motion_undef[nSeq]++;
											// in non-motion mode non-centered good are "undefined", in motion - just good/bad
										} else if (!motion_mode && (centered == CuasMotionLMA.CENTERED_NO) && (ts_fail == CuasMotionLMA.FAIL_NONE)) { // only count good uncentered as undefined
											num_noncentered[nSeq]++;
										} else if (ts_fail == CuasMotionLMA.FAIL_NONE) {
											num_good[nSeq]++;
											// both [RSLT_FAIL] is NaN and [RSLT_CENTERED] is NaN - MV-only
										} else{ //
											num_bad[nSeq]++;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return getTotals(
				num_all,         // int [] num_all,
				(num_motion_undef_in !=   null) ? num_motion_undef: 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
		if (debugLevel > -4) System.out.println("Using "+cuasMotion.when_fail+" for RSLT_WHEN in failUsed()");
		failUsed(targets_multi,           //	final double [][][][] target_sequence) {
				cuasMotion.when_fail++ ); // final int when_fail);
		boolean debug_extra = debugLevel > -5;
		if (intermed_low && debug_more && debug_extra) {
			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_FAIL_USED",// String        title,
					false, // good_only,            // final boolean         good_only,
					true, // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_dup);
		}
		
		sortMultiTargets(
				targets_multi,        // final double [][][][] target_multi,
				trim_nulls);          // final boolean         trim_nulls) { // trim null targets
		if (intermed_low && debug_more && debug_extra) {
			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_SORT_MULTI",// String        title,
					false, // good_only,            // final boolean         good_only,
					true, // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_dup);
		}
		// Will not mark as failed for duplicates - implement?
		if (debugLevel > -4) System.out.println("Using "+cuasMotion.when_fail+" for RSLT_WHEN in removeWeakerDuplicates()");
		int [] num_duplicates = removeWeakerDuplicates( // needs sortMultiTargets and after 
				targets_multi,           // final double [][][][] target_multi,
				duplicate_tolerance,     // final double          pix_tolerance)
				cuasMotion.when_fail++); // final int             when_fail)
		if (intermed_low && debug_more && debug_extra) {
			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_DUP",// String        title,
					false, // good_only,            // final boolean         good_only,
					true, // show_empty,           // final boolean         show_empty, // show scenes with no (valid) targets
					!batch_mode,          // boolean       show,
					cuasMotion.tilesX);   //  int           tilesX) {
			parentCLT.saveImagePlusInModelDirectory(imp_dup);
		}

		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,  // final double [][][][] target_sequence_multi,
				num_all,                // int []                num_all_in,
				null,                   // int []                num_motion_undef_in, // if non-null, will use it instead of num_noncentered_in // AF 05/20/2026
				// next will be ignored if num_motion_undef_in != null
				num_undef,              // int []                num_noncentered_in,
				num_good,               // int []                num_good_in,
				num_bad);               // int []                num_bad_in)
		
		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
		if (debugLevel > -4) System.out.println("Using "+cuasMotion.when_fail+" for RSLT_WHEN in findStrongerNeighbor()");

		findStrongerNeighbor(// does not modify "when" - now 05.30.2026 does
				targets_multi,          // final double [][][][] target_multi,
				filter5,                // final boolean [][] filter5)
				true,                   // false,                // final boolean       mark_failed,
				cuasMotion.when_fail++, // final int             when_fail,
				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 before removing shorts", true, num_good);
			System.out.println("min_score_lma="+min_score_lma);
			System.out.println("enough_seq=   "+enough_seq);
			System.out.println("seq_travel=   "+seq_travel);
		}


		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);
			System.out.println("min_score_lma="+min_score_lma);
			System.out.println("enough_seq=   "+enough_seq);
			System.out.println("seq_travel=   "+seq_travel);
		}

		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             when_fail,
			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;
											targets[ntarg][CuasMotionLMA.RSLT_WHEN] = when_fail;
										}
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	public void processMovingTargetsMulti(
			final boolean         batch_mode,
			final float [][]      fpixels,
			final float []        fpixels_avg, // not used
			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
			}
			this.when_fail = CuasMotion.WHEN_FINISH;

			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;
	}
	
	
}
