package com.elphel.imagej.cuas.rt;

import java.awt.Rectangle;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.cuas.rt.TemporalKernelGenerator.ConsolidationKernel;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.TileNeibs;

import ij.ImagePlus;

public class CuasRTUtils {
	public static double [] IMP_MINMAX= {-5,5}; //default range for
	final double         psf_radius;
	final double         n_sigma; // for LoG kernel size
	final double []      kernel2d;
	final private int    kernel2d_rad;
	// contains pixel indices, approximately in reverse bit order for multithreading efficiency
	// for the center area only, does not need to care about borders 
	final private int [] indx_center_2d;
	// remaining near-border pixel indices
	final private int [] indx_margins_2d;
	// all pixels indices
	final private int [] indx_all_2d;
	// 3x3-velocity kernel STATE (kernel3d3_rad + indx_*_3d3) removed 2026-06-20 (dead after 3d3/MF cleanup); see tag cuas-layer1. // By Claude

	final private int    pixel_decimate;
	final private int    velocity_decimate;
	final private int    velocity_radius;
	
	final private double [] temporal_weights;
	final private double [][][][][] kernel5d;
	final private int    kernel5d_rad;
	final private int [] indx_all_5d;
	final private int [] indx_center_5d;
	final private int [] indx_margins_5d;
	
	final private int    vel_suppr_rad;
	final private int [] indx_all_vsupp;
	final private int [] indx_center_vsupp;
	final private int [] indx_margins_vsupp;
	
	final ConsolidationKernel consolidationKernel;

	// Direct pixel-domain fine-velocity kernel (consolidationKernelDirect + kernel5dp_*) removed 2026-06-20 with the C5P path; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026

	// balance kernel - center = +1, sum of other weights = -1.
	final private int    width;
	final private int    height;
//	final private int    tl_offset;
	// c5_matched constructor variant removed 2026-06-20 with the C5P path; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026
	public CuasRTUtils (
			double   psf_radius,
			double   n_sigma,
			// kernel3d3_rad ctor arg removed 2026-06-20 (dead after 3d3/MF cleanup); see tag cuas-layer1. // By Claude
			double[] temporal_weights,
            int      pixel_decimate,
            int      velocity_decimate,
            int      velocity_radius,
            int      vel_suppr_rad,
			int      width,
			int      height) {
		this.psf_radius = psf_radius;
		this.width =  width;
		this.height = height;
		this.n_sigma = n_sigma; 
		this.vel_suppr_rad=vel_suppr_rad;
		// Setup 2D pre-filter kernel, based on LoG
		kernel2d_rad = getRadiusLoGKernel(psf_radius,n_sigma);
		kernel2d = getLoGKenel(psf_radius, kernel2d_rad);
		int [][] indices = getIndices(
				kernel2d_rad,
				width,
				height);
		indx_all_2d =      indices[0];
		indx_center_2d =   indices[1];
		indx_margins_2d =  indices[2];
		
		// 3x3 coarse-velocity kernel setup (kernel3d3_rad + indx_*_3d3) removed 2026-06-20 (dead after 3d3/MF cleanup); see tag cuas-layer1. // By Claude
		// Setup 5D kernel for generating fine velocities at increased spatial resolution
		this.temporal_weights = temporal_weights.clone();
		normalize1d (this.temporal_weights);
		this.pixel_decimate =    pixel_decimate;
		this.velocity_decimate = velocity_decimate;
		this.velocity_radius =   velocity_radius;

		consolidationKernel = TemporalKernelGenerator.generateKernel(
				this.temporal_weights,  // double[] temporal_weights,
				pixel_decimate,    // int pixel_decimate,
				velocity_decimate, // int velocity_decimate,
				velocity_radius);  // int velocity_radius)
		kernel5d =     consolidationKernel.getKernel();
		kernel5d_rad = consolidationKernel.getSpatialRadius();
		indices = getIndices(
				kernel5d_rad,
				width,
				height);
		indx_all_5d =     indices[0];
		indx_center_5d =  indices[1];
		indx_margins_5d = indices[2];
		// Direct pixel-domain C5P kernel setup (tw_direct, consolidationKernelDirect, kernel5dp_*) removed 2026-06-20; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026
		indices = getIndices(
				this.vel_suppr_rad,
				width,
				height);
		indx_all_vsupp =     indices[0];
		indx_center_vsupp =  indices[1];
		indx_margins_vsupp = indices[2];
	}
	
	public double getPsfRadius() {
		return psf_radius;
	}
	
	public int getKernel2dRadius() {
		return kernel2d_rad;
	}
	
	// get3d3Radius() removed 2026-06-20 (dead after 3d3/MF cleanup); see tag cuas-layer1. // By Claude

	public int getNumHist() {
		return kernel5d.length;
	}

	/** Number of output velocities (vout) and sub-pixel tiling of the 5D render - for memory estimates. // By Claude on 06/14/2026 */
	public int getNumVout() { return kernel5d[0].length; }
	public int getSubDim()  { return (int) Math.sqrt(kernel5d[0][0].length); }

	// getNumHistDirect() removed 2026-06-20 with the C5P direct kernel; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026

	public double [] getKernel2d() {
		return kernel2d;
	}
	
	private void normalize1d(double [] data) {
		double s = 0;
		for (double d : data) s+= d;
		for (int i = 0; i < data.length; i++) {
			data[i] /= s;
		}
	}
			
	
	private static int [][] getIndices(
			int kernel_rad,
			int width,
			int height) {
		int kernel_size = 2 * kernel_rad+1;
		int [] i_all =     new int [width*height];
		int cent_width = width-kernel_size+1;
		int cent_height = height-kernel_size+1;
		int cent_size = cent_width*cent_height;
		int [] i_center =  new int [cent_size];
		int [] i_margins = new int [i_all.length - i_center.length];
		int n;
		for (n = 0; (1 << n) < i_all.length; n++);
		int shft = 32-n;
		int n_all = 1 << n;
//		int mask = n_all-1; 
		int iall = 0, icent=0, imarg=0;
		for (int i = 0; i < n_all; i++) {
			int ri = Integer.reverse(i)>>>shft;
			if (ri < i_all.length) {
				int y = ri/width;
				int x = ri%width;
				i_all[iall++] = ri;
				if ((x < kernel_rad) || (y < kernel_rad) || (x >= (width-kernel_rad)) ||  (y >= (height-kernel_rad))) {
					i_margins[imarg++] = ri;
				} else {
					i_center[icent++] = ri;
				}
			}
		}
		int [][] indices = {i_all, i_center, i_margins};
		return indices;
	}
	
	
	public void checkData(double [] data) {
		if (data.length != (width * height)) {
			throw new IllegalArgumentException("Data size = "+data.length+" does not match dimensions "+width+"*"+height+"="+(width*height));
		}
	}

	public void checkData(double [][] data) {
		if (data.length != (width * height)) {
			throw new IllegalArgumentException("Data size = "+data.length+" does not match dimensions "+width+"*"+height+"="+(width*height));
		}
	}
	
	public void checkKernel(double [] kernel, int rad) {
		int kernel_size = 2 * rad + 1;
		if (kernel.length != (kernel_size * kernel_size)) {
			throw new IllegalArgumentException("Kernel length = "+kernel.length+" does not match kernel size (2*"+kernel2d_rad+"+1) * (2*"+kernel2d_rad+"+1) ="+(kernel_size * kernel_size));
		}
	}
	
	public double [][][] convolve3d(
			final double [][][] data,         // outer index - historic layers, [0] - latest, second index - pixel, third - direction ([9])
			double [][][]     result_in,
			final double      min_frac_last){
		final int vel_size = 2*velocity_radius + 1;
		final int nlayers = data.length;
		final double [][][] result = (result_in != null) ? result_in : new double [width*height][pixel_decimate*pixel_decimate][vel_size * vel_size];
		final double [][][] last_layer = new double [width*height][pixel_decimate*pixel_decimate][vel_size * vel_size];
		final double [][][][][] kernel = consolidationKernel.getKernel();
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int [] coarse_vel_indx = consolidationKernel.getCoarseVIdx();
		final int decimate_size = pixel_decimate*pixel_decimate;
		System.out.print("convolve3d() nhist=");
		for (int nhist = 0; nhist < nlayers; nhist++) {
			System.out.print(nhist+" ");
			final int fhist = nhist;
			final int spat_rad = consolidationKernel.getSpatialRadius(); // may be later dependent on nhist for efficiency 
			final double [][][][] kernel_hist = kernel[fhist];
			final double [][]     data_hist = data[fhist];
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_center_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_center_5d[nPix];
							int target_x = ipix_dst % width; 
							int target_y = ipix_dst / width;
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >=0) {
										double accumulated_flux = 0.0;
										// Outer spatial neighborhood limits defined by the pre-computed kernel
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												// 1. Boundary check for source image coordinates - not here
												// 2. Locate the source macro-pixel index in your history array
												int src_pixel_idx = src_y * width + src_x;
												// 3. Pull historical energy from ONLY the mapped coarse velocity channel
												double historical_value = data_hist[src_pixel_idx][src_coarse_v];
												// 4. Multiply by the exact pre-calculated geometric weight for this sub-pixel layout
												int k_dy = dy + spat_rad;
												int k_dx = dx + spat_rad;
												double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
												accumulated_flux += historical_value * weight;
											}
										}
										result[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
										if (fhist == 0) {
											last_layer[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
										}
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_margins_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_margins_5d[nPix];
							int target_x = ipix_dst % width; 
							int target_y = ipix_dst / width;
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >=0) {
										double accumulated_flux = 0.0;
										// Outer spatial neighborhood limits defined by the pre-computed kernel
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												// 1. Boundary check for source image coordinates
												if (src_x >= 0 && src_x < width && src_y >= 0 && src_y < height) {
													// 2. Locate the source macro-pixel index in your history array
													int src_pixel_idx = src_y * width + src_x;
													// 3. Pull historical energy from ONLY the mapped coarse velocity channel
													double historical_value = data_hist[src_pixel_idx][src_coarse_v];
													// 4. Multiply by the exact pre-calculated geometric weight for this sub-pixel layout
													int k_dy = dy + spat_rad;
													int k_dx = dx + spat_rad;
													double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
													accumulated_flux += historical_value * weight;
												}
											}
										}
										result[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
										if (fhist == 0) {
											last_layer[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
										}
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		// Remove if last_layer < fraction of the result
		if (min_frac_last > 0) {
			final double [][][][] kernel_hist = kernel[0];
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_all_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_all_5d[nPix];
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									if (last_layer[ipix_dst][sub_idx][v_out_idx] < min_frac_last * result[ipix_dst][sub_idx][v_out_idx]) {
										result[ipix_dst][sub_idx][v_out_idx] = 0;
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		
		System.out.println();
		return result;
	}

	/**
	 * ROI variant: compute fine-velocity convolution for a pixel rectangle only.
	 * Result is indexed ROI-relatively: result[roi_pix][sub][vel] where
	 * roi_pix = (target_y - roi.y) * roi.width + (target_x - roi.x).
	 * Pixels outside roi are skipped; full-image source data is still used for
	 * the spatial neighbourhood reads.
	 */
	public double [][][] convolve3d(
			final double [][][] data,
			final Rectangle     roi,
			double [][][]       result_in,
			final double        min_frac_last){
		final int vel_size = 2*velocity_radius + 1;
		final int nlayers = data.length;
		final int roi_npix = roi.width * roi.height;
		final double [][][] result = (result_in != null) ? result_in :
				new double [roi_npix][pixel_decimate*pixel_decimate][vel_size * vel_size];
		final double [][][] last_layer = new double [roi_npix][pixel_decimate*pixel_decimate][vel_size * vel_size];
		final double [][][][][] kernel = consolidationKernel.getKernel();
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int [] coarse_vel_indx = consolidationKernel.getCoarseVIdx();
		final int decimate_size = pixel_decimate*pixel_decimate;
		final int roi_x0 = roi.x, roi_y0 = roi.y, roi_x1 = roi.x + roi.width, roi_y1 = roi.y + roi.height;
		for (int nhist = 0; nhist < nlayers; nhist++) {
			final int fhist = nhist;
			final int spat_rad = consolidationKernel.getSpatialRadius();
			final double [][][][] kernel_hist = kernel[fhist];
			final double [][]     data_hist = data[fhist];
			ai.set(0);
			// center pixels — no source boundary checks needed
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_center_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_center_5d[nPix];
							int target_x = ipix_dst % width;
							int target_y = ipix_dst / width;
							if (target_x < roi_x0 || target_x >= roi_x1 ||
								target_y < roi_y0 || target_y >= roi_y1) continue;
							int roi_pix = (target_y - roi_y0) * roi.width + (target_x - roi_x0);
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >= 0) {
										double accumulated_flux = 0.0;
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												int src_pixel_idx = src_y * width + src_x;
												double historical_value = data_hist[src_pixel_idx][src_coarse_v];
												int k_dy = dy + spat_rad;
												int k_dx = dx + spat_rad;
												double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
												accumulated_flux += historical_value * weight;
											}
										}
										result[roi_pix][sub_idx][v_out_idx] += accumulated_flux;
										if (fhist == 0) {
											last_layer[roi_pix][sub_idx][v_out_idx] += accumulated_flux;
										}
										
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			ai.set(0);
			// margin pixels — source boundary checks required
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_margins_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_margins_5d[nPix];
							int target_x = ipix_dst % width;
							int target_y = ipix_dst / width;
							if (target_x < roi_x0 || target_x >= roi_x1 ||
								target_y < roi_y0 || target_y >= roi_y1) continue;
							int roi_pix = (target_y - roi_y0) * roi.width + (target_x - roi_x0);
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >= 0) {
										double accumulated_flux = 0.0;
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												if (src_x >= 0 && src_x < width && src_y >= 0 && src_y < height) {
													int src_pixel_idx = src_y * width + src_x;
													double historical_value = data_hist[src_pixel_idx][src_coarse_v];
													int k_dy = dy + spat_rad;
													int k_dx = dx + spat_rad;
													double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
													accumulated_flux += historical_value * weight;
												}
											}
										}
										result[roi_pix][sub_idx][v_out_idx] += accumulated_flux;
										if (fhist == 0) {
											last_layer[roi_pix][sub_idx][v_out_idx] += accumulated_flux;
										}
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
		}
		// Remove if last_layer < fraction of the result
		if (min_frac_last > 0) {
			final double [][][][] kernel_hist = kernel[0];
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_all_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_all_5d[nPix];
							int target_x = ipix_dst % width;
							int target_y = ipix_dst / width;
							if (target_x < roi_x0 || target_x >= roi_x1 ||
								target_y < roi_y0 || target_y >= roi_y1) continue;
							int roi_pix = (target_y - roi_y0) * roi.width + (target_x - roi_x0);
							
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									if (last_layer[roi_pix][sub_idx][v_out_idx] < min_frac_last * result[roi_pix][sub_idx][v_out_idx]) {
										result[roi_pix][sub_idx][v_out_idx] = 0;
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return result;
	}

	// convolve3dDirect (single-stage velocity-matched filter, C5P direct path) removed 2026-06-20; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026

	// 3d3 coarse-velocity path (convolve3D3LReLU overloads) removed 2026-06-20; predecessor code at git tag cuas-layer1.
	// By Claude on 06/20/2026
	
	/**
	 * Combine (pairwise average)2 pixel arrays 		
	 * @param data_now  pixel data [width*height]
	 * @param data_prev pixel data [width*height]
	 * @param result_in allocated pixel array or null (to be allocated)
	 * @param alpha for LReLU (0 - classic ReLU, 1.0 - linear transfer, no ReLU)
	 * @return result pixel array, same as result_in if not null
	 */
	public double[] temporalAverageLReLU(
			final double [] data_now,
			final double [] data_prev,
			double []       result_in,
			final double    alpha) {
		// assuming symmetrical kernel, not inverting indices
		final double [] result = (result_in != null) ? result_in : new double [data_now.length];
		final int kernel_size = 2 * kernel2d_rad + 1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < indx_all_2d.length; nPix = ai.getAndIncrement()) {
						int ipix = indx_all_2d[nPix];
						double v = 0.5*(data_now[ipix]+data_prev[ipix]);
						result[ipix] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
	
	
	public double[] convolve2DLReLU(
			final double [] data,
			final double [] kernel, // square
			double []       result_in,
			final double    alpha) {
		// assuming symmetrical kernel, not inverting indices
		final double [] result = (result_in != null) ? result_in : new double [data.length];
		// verify dimensions;
		checkData(data);
		checkData(result);
		checkKernel(kernel, kernel2d_rad);
		final int tl_offset = kernel2d_rad * (width+1);
		final int kernel_size = 2 * kernel2d_rad + 1;
		final int row_inc = width- kernel_size;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < indx_center_2d.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_center_2d[nPix];
						// NaN-aware LoG: NaN out only if the CENTER is NaN; substitute center for NaN taps
						// (edge-replication) so a valid pixel never blooms to NaN from a NaN neighbor. By Claude on 07/01/2026
						double c = data[ipix_dst];
						if (Double.isNaN(c)) { result[ipix_dst] = Double.NaN; continue; }
						int ipix_src = ipix_dst- tl_offset;
						int ikern = 0;
						double v = 0;
						for (int iy = 0; iy < kernel_size; iy++) {
							for (int ix = 0; ix < kernel_size; ix++) {
								double d = data[ipix_src++];
								v += (Double.isNaN(d) ? c : d) * kernel[ikern++];
							}
							ipix_src+=row_inc;
						}
						result[ipix_dst] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// marginal area, check indices
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					for (int nPix = ai.getAndIncrement(); nPix < indx_margins_2d.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_margins_2d[nPix];
						double c = data[ipix_dst];
						if (Double.isNaN(c)) { result[ipix_dst] = Double.NaN; continue; }
						double v = 0;
						for (int iy = 0; iy < kernel_size; iy++) {
							int dy = iy-kernel2d_rad;
							for (int ix = 0; ix < kernel_size; ix++) {
								int dx = ix-kernel2d_rad;
								int ipix_src = tn.getNeibIndex(ipix_dst, dx, dy);
								if (ipix_src >= 0) {
									double d = data[ipix_src];
									v += (Double.isNaN(d) ? c : d) * kernel[ix + iy*kernel_size];
								}
							}
						}
						result[ipix_dst] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
    /**
     * Calculates the integer radius for the LoG kernel based on the PSF radius
     * and the desired truncation boundary (n_sigma).
     */
    public static int getRadiusLoGKernel(double psf_radius, double n_sigma) {
        double sigma = psf_radius / Math.sqrt(2.0);
        return (int) Math.ceil(sigma * n_sigma);
    }

    /**
     * Generates a 1D flattened flux-normalized 2D Laplacian of Gaussian kernel.
     * The sum of positive elements is exactly 1.0, and the sum of negative elements is exactly -1.0.
     */
    public static double[] getLoGKenel(double psf_radius, int kernel_radius) {
        double sigma = psf_radius / Math.sqrt(2.0);
        double sigma2 = sigma * sigma;
       
        int width = 2 * kernel_radius + 1;
        double[] kernel = new double[width * width];
       
        double sumPos = 0.0;
        double sumNeg = 0.0;
       
        // 1. Generate raw inverted LoG shape and accumulate sums
        for (int y = -kernel_radius; y <= kernel_radius; y++) {
            for (int x = -kernel_radius; x <= kernel_radius; x++) {
                double r2 = x * x + y * y;
               
                // Positive center, negative surround for bright blob detection
                double raw = (1.0 - (r2 / (2.0 * sigma2))) * Math.exp(-r2 / (2.0 * sigma2));
               
                int idx = (y + kernel_radius) * width + (x + kernel_radius);
                kernel[idx] = raw;
               
                if (raw > 0) {
                    sumPos += raw;
                } else {
                    sumNeg += raw;
                }
            }
        }
       
        // 2. Apply strict flux-normalization
        double absSumNeg = Math.abs(sumNeg);
        for (int i = 0; i < kernel.length; i++) {
            if (kernel[i] > 0) {
                kernel[i] /= sumPos;
            } else if (kernel[i] < 0) {
                kernel[i] /= absSumNeg;
            }
        }
       
        return kernel;
    }

// debug methods
    public ImagePlus showKernel2d(String title) {
    	if (title==null) {
    		title = "kernel2d";
    	}
    	ImagePlus imp_kernel2d = ShowDoubleFloatArrays.makeArrays(
    			kernel2d,
				2 * kernel2d_rad + 1,
				2 * kernel2d_rad + 1,
				title);
    	imp_kernel2d.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp_kernel2d;
    }
    
    public double [] render3d3Flat(double [][] coarse_vel) {
    	int width41 =  4 * width+1;
    	int height41 = 4 * height+1;
    	double [] render_flat = new double [width41 * height41];
    	Arrays.fill(render_flat, Double.NaN);
    	for (int npix=0; npix < coarse_vel.length; npix++) if (coarse_vel[npix] !=null){
    		int py = npix/width;
    		int px = npix%width;
    		int pix4 = (px * 4 + 1) + (py * 4 + 1) * width41;
    		int cvi = 0;
    		for (int dy = 0; dy < 3; dy++) {
        		for (int dx = 0; dx < 3; dx++) {
        			render_flat[pix4++] = coarse_vel[npix][cvi++];
        		}
        		pix4 += width41 - 3; // next row and 3 pixels back
    		}
    	}
    	return render_flat;
    }

    public double [][] render3d3Sliced(double [][] coarse_vel) {
    	double [][] render_sliced = new double [9][width * height];
    	for (double [] slice:render_sliced) {
    		Arrays.fill(slice, Double.NaN);
    	}
    	for (int npix=0; npix < coarse_vel.length; npix++) if (coarse_vel[npix] !=null){
    		for (int s = 0; s < coarse_vel[npix].length; s++) {
    			render_sliced[s][npix] = coarse_vel[npix][s];
    		}
    	}
    	return render_sliced;
    }
    
    // render3d3Hyperstack removed 2026-06-20 (dead after 3d3/MF cleanup); see tag cuas-layer1. // By Claude

    public String [] get9Arrows() {
    	String [] arrows= {
    			"\u2191\u2190",  // Up-Left   
    			"\u2191",        // Up        
    			"\u2192\u2191",  // Right-Up  
    			"\u2190",        // Left      
    			"\u25E6",        // Bullet    
    			"\u2192",        // Right     
    			"\u2193\u2190",  // Down-Left 
    			"\u2193",        // Down
    			"\u2192\u2193"}; // Right-Down
    	return arrows;
    }
    public String [] get9Arrows_diags() {
    	String [] arrows= {
    			"\u2196",  // Left-Up   
    			"\u2191",  // Up        
    			"\u2197",  // Right-Up  
    			"\u2190",  // Left      
    			"\u25E6",  // Bullet    
    			"\u2192",  // Right     
    			"\u2199",  // Left-Down
    			"\u2193",  // Down
    			"\u2198"};  // Right-Down
    	return arrows;
    }
    
    public ImagePlus showKernel5d(String title) {
    	// consolidationKernel 
    	// [num_subs][num_temps][pixels]
    	if (title == null) {
    		title = "kernel5d";
    	}
    	int spat_dim = consolidationKernel.getSpatialDim();
    	int coarse_dim = 3;
    	int coarse_num = coarse_dim*coarse_dim;
    	int render_dim = spat_dim * (coarse_dim + 1);
    	int num_hist = kernel5d.length;
    	int num_vout = kernel5d[0].length;
    	int vout_dim = (int) Math.sqrt(num_vout);
    	int vout_rad = (vout_dim-1)/2;
    	
    	int num_sub =  kernel5d[0][0].length;
    	int sub_dim = (int) Math.sqrt(num_sub);
    	
    	double [][][] rendered = new double [num_sub*num_hist][num_vout][(render_dim + 1) * (render_dim + 1)];
    	String [] top_titles =   new String[num_sub*num_hist];
    	String [] slice_titles = new String[num_vout];
    	for (double [][] slice_hist:rendered) {
    		for (double [] slice:slice_hist) {
    			Arrays.fill(slice, Double.NaN);
    		}
    	}
    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad ; 
    		int vout_h = nvout % vout_dim - vout_rad;
    		slice_titles[nvout] = nvout+ " ("+((vout_v >0) ?"+":"")+vout_v+":"+((vout_h >0) ?"+":"")+vout_h+")";
    	}
    	for (int nsub = 0; nsub < num_sub; nsub++) {
    		int sub_v = nsub / sub_dim; 
    		int sub_h = nsub % sub_dim;
        	for (int nhist = 0; nhist < num_hist; nhist++) {
        		int num_tslice = nsub * num_hist + nhist;
        		top_titles[num_tslice] = sub_v+":"+sub_h+" - " + nhist;
            	for (int nvout = 0; nvout < num_vout; nvout++) {
            		int coarse_index = consolidationKernel.getCoarseVIdx()[nvout];
            		for (int py = 0; py < spat_dim; py++) {
            			int py0_rend = py * (coarse_dim + 1) +1; // ) * render_dim;
            			for (int px = 0; px < spat_dim; px++) {
            				int px0_rend = px * (coarse_dim + 1) +1; //	
            				for (int coarse_i = 0; coarse_i < coarse_num; coarse_i++) {
            					double w = 0; // maybe opposite - use NaN here?
            					int coarse_v = coarse_i / coarse_dim; 
            					int coarse_h = coarse_i % coarse_dim;
            					if (coarse_i == coarse_index) {
            						w = kernel5d[nhist][nvout][nsub][py][px]; // unused will be 0, not NaN;
            					}
            					int pix_rend = (py0_rend + coarse_v) * (render_dim+1) + (px0_rend + coarse_h);
            					rendered[num_tslice][nvout][pix_rend] = w;
            				}
            			}
            		}
            	}        		
        	}    		
    	}
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				rendered,      // double[][][] pixels, 
				(render_dim+1),// int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				slice_titles,  // String []    titles, // all slices*frames titles or just slice titles or null
				top_titles,    // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }
   /*
    public ImagePlus showKernel5d_1(String title) {
    	// consolidationKernel 
    	// [num_subs][num_temps][pixels]
    	if (title == null) {
    		title = "kernel5d";
    	}
    	int spat_dim = consolidationKernel.getSpatialDim();
    	int coarse_dim = 3;
    	int coarse_num = coarse_dim*coarse_dim;
    	int render_dim = spat_dim * (coarse_dim+1);
    	int num_hist = kernel5d.length;
    	int num_vout = kernel5d[0].length;
    	int vout_dim = (int) Math.sqrt(num_vout);
    	int vout_rad = (vout_dim-1)/2;
    	
    	int num_sub =  kernel5d[0][0].length;
    	int sub_dim = (int) Math.sqrt(num_sub);
    	
//    	double [][][] rendered = new double [num_sub*num_hist][num_vout][(render_dim + 1) * (render_dim + 1)];
    	double [][][] rendered = new double [num_hist][num_vout][(render_dim + 1) * (render_dim + 1)];
    	String [] top_titles =   new String[num_sub*num_hist];
    	String [] slice_titles = new String[num_vout];
    	for (double [][] slice_hist:rendered) {
    		for (double [] slice:slice_hist) {
    			Arrays.fill(slice, Double.NaN);
    		}
    	}
    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad ; 
    		int vout_h = nvout % vout_dim - vout_rad;
    		slice_titles[nvout] = nvout+ " ("+((vout_v >0) ?"+":"")+vout_v+":"+((vout_h >0) ?"+":"")+vout_h+")";
    	}
//    	for (int nsub = 0; nsub < num_sub; nsub++) {
//    		int sub_v = nsub / sub_dim; 
//   		int sub_h = nsub % sub_dim;
        	for (int nhist = 0; nhist < num_hist; nhist++) {
        		int num_tslice = nsub * num_hist + nhist;
        		top_titles[num_tslice] = sub_v+":"+sub_h+" - " + nhist;
            	for (int nvout = 0; nvout < num_vout; nvout++) {
            		int coarse_index = consolidationKernel.getCoarseVIdx()[nvout];
            		for (int py = 0; py < spat_dim; py++) {
            			int py0_rend = py * (coarse_dim + 1) +1; // ) * render_dim;
            			for (int px = 0; px < spat_dim; px++) {
            				int px0_rend = px * (coarse_dim + 1) +1; //	
            				for (int coarse_i = 0; coarse_i < coarse_num; coarse_i++) {
            					double w = 0; // maybe opposite - use NaN here?
            					int coarse_v = coarse_i / coarse_dim; 
            					int coarse_h = coarse_i % coarse_dim;
            					if (coarse_i == coarse_index) {
            						w = kernel5d[nhist][nvout][nsub][py][px]; // unused will be 0, not NaN;
            					}
            					int pix_rend = (py0_rend + coarse_v) * render_dim + (px0_rend + coarse_h);
            					rendered[num_tslice][nvout][pix_rend] = w;
            				}
            			}
            		}
            	}        		
        	}    		
    	
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				rendered,      // double[][][] pixels, 
				width,         // int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				slice_titles,  // String []    titles, // all slices*frames titles or just slice titles or null
				top_titles,    // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }
*/    
    
    
    // Display convolution result [vout][2*width*2*height]. Or hyperstack with x and y sliders?
    /**
     * Display a sequence of velocities per subpixel (or pixel if no decimation) layers as
     * a hyperstack. The bottom slider (outer index) selects the output velocity, the top slider
     * (and a scroll wheel) - scenes (or averaged scenes named by the last integrated) 
     * @param data [nscene][npix][nsubpix][nvelocity]
     * @param scene_titles scene names or null
     * @param title  image title or null
     * @return
     */
    public ImagePlus showConvKernel5d(
    		double [][][][] data,
    		String []       scene_titles,
    		String          title) {
    	int num_scenes = data.length;
    	int num_vout = kernel5d[0].length;
    	int vout_dim = (int) Math.sqrt(num_vout);
    	int vout_rad = (vout_dim-1)/2;
    	int num_sub =  kernel5d[0][0].length;
    	int sub_dim = (int) Math.sqrt(num_sub);
    	int full_with =   width *  sub_dim;
    	int full_height = height * sub_dim;
    	if (title==null) {
    		title = "OutputConvKernel5D";
    	}
    	if (scene_titles == null) {
    		scene_titles = new String[num_scenes];
    		for (int i = 0; i < scene_titles.length; i++) {
    			scene_titles[i]= String.format("scene-%03d", i);
    		}
    	}
		String [] v_titles = new String[num_vout];

    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad ; 
    		int vout_h = nvout % vout_dim - vout_rad;
    		v_titles[nvout] = nvout+ " ("+((vout_v >0) ?"+":"")+vout_v+":"+((vout_h >0) ?"+":"")+vout_h+")";
    	}
    	
    	double [][][] rendered = new double [num_vout][num_scenes][full_with*full_height];
    	for (double [][] d2 : rendered) for (double [] d1 : d2) Arrays.fill(d1, Double.NaN);
	
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				rendered,      // double[][][] pixels, 
				width,         // int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				scene_titles,  // slice_titles,  // String []    titles, // all slices*frames titles or just slice titles or null
				v_titles,      // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }

    /**
     * Render fine-velocity output for a pixel ROI as a flat image stack (one slice per scene).
     * For each pixel in the ROI a (vout_dim × vout_dim) velocity-map square is drawn.
     * Squares are tiled in the output image with a 1-pixel NaN separator column/row
     * (renders as visible grid in ImageJ). Sub-pixels (pixel_decimate > 1) tile inside each square.
     *
     * Output dimensions:
     *   width  = roi.width  * (vout_dim + 1) * sub_dim
     *   height = roi.height * (vout_dim + 1) * sub_dim
     *
     * @param data         [nscene][width*height][nsub][nvel] — output of convolve3d
     * @param roi          pixel rectangle to render (full-image coordinates)
     * @param scene_titles per-slice labels, or null
     * @param title        image title, or null
     */
    public ImagePlus showConvKernel5d(
    		double [][][][] data,
    		Rectangle       roi,
    		String []       scene_titles,
    		String          title) {
    	int num_scenes = data.length;
    	int num_vout   = kernel5d[0].length;
    	int vout_dim   = (int) Math.sqrt(num_vout);
    	int num_sub    = kernel5d[0][0].length;
    	int sub_dim    = (int) Math.sqrt(num_sub);

    	int roi_width  = roi.width  * (vout_dim + 1) * sub_dim;
    	int roi_height = roi.height * (vout_dim + 1) * sub_dim;
    	int block_w    = (vout_dim + 1) * sub_dim;
    	int block_h    = (vout_dim + 1) * sub_dim;

    	if (title == null) title = "CONV5D-ROI";
    	if (scene_titles == null) {
    		scene_titles = new String[num_scenes];
    		for (int i = 0; i < scene_titles.length; i++) {
    			scene_titles[i] = String.format("scene-%03d", i);
    		}
    	}

    	double [][] rendered = new double[num_scenes][roi_width * roi_height];
    	for (double [] slice : rendered) Arrays.fill(slice, Double.NaN);

    	for (int nscene = 0; nscene < num_scenes; nscene++) {
    		for (int py_roi = 0; py_roi < roi.height; py_roi++) {
    			int py_img = roi.y + py_roi;
    			if (py_img < 0 || py_img >= height) continue;
    			for (int px_roi = 0; px_roi < roi.width; px_roi++) {
    				int px_img = roi.x + px_roi;
    				if (px_img < 0 || px_img >= width) continue;
    				int pixel_idx = py_roi * roi.width + px_roi;
    				int block_x   = px_roi * block_w;
    				int block_y   = py_roi * block_h;
    				double [][]   pix_data = data[nscene][pixel_idx]; // [nsub][nvel]
    				for (int sy = 0; sy < sub_dim; sy++) {
    					for (int sx = 0; sx < sub_dim; sx++) {
    						int sub_idx = sy * sub_dim + sx;
    						int slot_x  = block_x + sx * (vout_dim + 1);
    						int slot_y  = block_y + sy * (vout_dim + 1);
    						for (int vy = 0; vy < vout_dim; vy++) {
    							for (int vx = 0; vx < vout_dim; vx++) {
    								int v_out_idx = vy * vout_dim + vx;
    								int rx = slot_x + vx;
    								int ry = slot_y + vy;
    								rendered[nscene][ry * roi_width + rx] = pix_data[sub_idx][v_out_idx];
    							}
    						}
    					}
    				}
    			}
    		}
    	}

    	ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
    			rendered,
    			roi_width,
    			roi_height,
    			title,
    			scene_titles);
    	imp.setDisplayRange(IMP_MINMAX[0], IMP_MINMAX[1]);
    	return imp;
    }

    /**
     * Render fine-velocity ROI output as an ImageJ hyperstack.
     * Bottom slider (T) iterates over all num_vout velocity outputs, labelled "+vy:+vx".
     * Second slider (Z) iterates over scenes.
     * Image size: roi.width * pixel_decimate × roi.height * pixel_decimate — no gap grid.
     * Sub-pixels are spatially embedded: pixel (px,py), sub-pixel (sx,sy) → image pixel
     * (px*sub_dim+sx, py*sub_dim+sy).
     *
     * @param data         [nscene][roi_npix][nsub][nvel] — ROI-indexed output of convolve3d(roi)
     * @param roi          pixel rectangle (full-image coordinates)
     * @param scene_titles per-scene labels, or null
     * @param title        image title, or null
     */
    public ImagePlus showConvKernel5dHyperRect(
    		double [][][][] data,
    		Rectangle       roi,
    		String []       scene_titles,
    		String          title) {
    	int num_scenes = data.length;
    	int num_vout   = kernel5d[0].length;
    	int vout_dim   = (int) Math.sqrt(num_vout);
    	int vout_rad   = (vout_dim - 1) / 2;
    	int num_sub    = kernel5d[0][0].length;
    	int sub_dim    = (int) Math.sqrt(num_sub);

    	int img_width  = roi.width  * sub_dim;
    	int img_height = roi.height * sub_dim;

    	if (title == null) title = "CONV5D-HYPER-RECT";
    	if (scene_titles == null) {
    		scene_titles = new String[num_scenes];
    		for (int i = 0; i < scene_titles.length; i++) {
    			scene_titles[i] = String.format("scene-%03d", i);
    		}
    	}
    	// Extra leading velocity slice (index 0) = max over all velocities, a quick locator    // By Claude on 06/13/2026
    	// of non-empty pixels. It is the first slice ImageJ shows, and the display range below
    	// is scaled to it so the small (~0.1) maxima are immediately visible on open.
    	String[] v_titles = new String[num_vout + 1];
    	v_titles[0] = "MAX (all v)";
    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad;
    		int vout_h = nvout % vout_dim - vout_rad;
    		v_titles[nvout + 1] = nvout + " (" + ((vout_v > 0) ? "+" : "") + vout_v +
    				":" + ((vout_h > 0) ? "+" : "") + vout_h + ")";
    	}

    	// outer dim = velocities (T = bottom slider; index 0 = MAX-over-v), inner dim = scenes (Z = second slider)
    	double [][][] pixels_hyper = new double [num_vout + 1][num_scenes][img_width * img_height];
    	for (double [][] d2 : pixels_hyper) for (double [] d1 : d2) Arrays.fill(d1, Double.NaN);

    	for (int nscene = 0; nscene < num_scenes; nscene++) {
    		for (int py_roi = 0; py_roi < roi.height; py_roi++) {
    			int py_img = roi.y + py_roi;
    			if (py_img < 0 || py_img >= height) continue;
    			for (int px_roi = 0; px_roi < roi.width; px_roi++) {
    				int px_img = roi.x + px_roi;
    				if (px_img < 0 || px_img >= width) continue;
    				int roi_pix = py_roi * roi.width + px_roi;
    				double [][] pix_data = data[nscene][roi_pix]; // [nsub][nvel]
    				for (int sy = 0; sy < sub_dim; sy++) {
    					for (int sx = 0; sx < sub_dim; sx++) {
    						int sub_idx = sy * sub_dim + sx;
    						int ix      = px_roi * sub_dim + sx;
    						int iy      = py_roi * sub_dim + sy;
    						int img_pix = iy * img_width + ix;
    						double vmax = Double.NEGATIVE_INFINITY;                              // By Claude on 06/13/2026
    						for (int v_out_idx = 0; v_out_idx < num_vout; v_out_idx++) {
    							double v = pix_data[sub_idx][v_out_idx];
    							pixels_hyper[v_out_idx + 1][nscene][img_pix] = v;                 // velocities shifted by 1
    							if (v > vmax) vmax = v;
    						}
    						pixels_hyper[0][nscene][img_pix] = vmax;                              // leading MAX-over-v slice
    					}
    				}
    			}
    		}
    	}

    	// Display range from the MAX-over-v slice (the one that opens first), so non-empty      // By Claude on 06/13/2026
    	// pixels are auto-visible; velocity slices share it (their values are <= the max).
    	double disp_min = Double.POSITIVE_INFINITY, disp_max = Double.NEGATIVE_INFINITY;
    	for (double [] sc : pixels_hyper[0]) for (double v : sc) if (!Double.isNaN(v)) {
    		if (v < disp_min) disp_min = v;
    		if (v > disp_max) disp_max = v;
    	}
    	if (!(disp_max > disp_min)) { disp_min = IMP_MINMAX[0]; disp_max = IMP_MINMAX[1]; }       // fallback (empty/flat)

    	ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
    			pixels_hyper, // double[num_vout+1][num_scenes][img_w*img_h]
    			img_width,
    			title,
    			scene_titles, // Z slider (second)
    			v_titles,     // T slider (bottom)
    			false);
    	imp.setDisplayRange(disp_min, disp_max);
    	return imp;
    }

    /**
     * 4D non-maximum suppression on a ROI fine-velocity result.
     * For each (pixel, velocity) point, find the maximum M over the ±vel_suppr_rad neighbourhood
     * in both pixel space and velocity space (vx/vy), then:
     *   thresh = max(athresh, M*rthresh); suppress if (M - val) >= thresh
     *   otherwise keep val unchanged (including the weak-signal passthrough when M < athresh).
     *
     * Uses indx_center_vsupp / indx_margins_vsupp for bit-reversed multithreaded iteration
     * (kernel radius = vel_suppr_rad matches both pixel and velocity neighbourhood).
     *
     * @param data     [roi_npix][nsub][nvel] — ROI-indexed output of convolve3d(roi)
     * @param roi      pixel rectangle (full-image coordinates)
     * @param athresh  absolute threshold: M must reach this for suppression to activate
     * @param rthresh  relative threshold: val/M below this → zeroed (e.g. 0.5)
     * @param dbg_min  debug feature - zero if weaker to test recurrent with strong targets
     * @return new filtered array, same shape as data
     */
    public double [][][] filterConv5dROI(
    		final double [][][] data,
    		final Rectangle     roi,
    		final double        athresh,
    		final double        rthresh,
    		final double        dbg_min) { // zero if below, for testing with strong signals
    	final int num_vout = kernel5d[0].length;
    	final int vout_dim = (int) Math.sqrt(num_vout);
    	final int num_sub  = kernel5d[0][0].length;
    	final int roi_x0 = roi.x, roi_y0 = roi.y;
    	final int roi_x1 = roi.x + roi.width, roi_y1 = roi.y + roi.height;
    	final double [][][] result = new double [data.length][num_sub][num_vout];
    	final Thread[] threads = ImageDtt.newThreadArray();
    	final AtomicInteger ai = new AtomicInteger(0);

    	// Center pixels: all 8 pixel neighbours are within image bounds; only ROI check needed.
    	for (int ithread = 0; ithread < threads.length; ithread++) {
    		threads[ithread] = new Thread() { public void run() {
    			for (int nPix = ai.getAndIncrement(); nPix < indx_center_vsupp.length; nPix = ai.getAndIncrement()) {
    				int ipix = indx_center_vsupp[nPix];
    				int px = ipix % width, py = ipix / width;
    				if (px < roi_x0 || px >= roi_x1 || py < roi_y0 || py >= roi_y1) continue;
    				int roi_pix = (py - roi_y0) * roi.width + (px - roi_x0);
    				for (int sub_idx = 0; sub_idx < num_sub; sub_idx++) {
    					for (int v_out_idx = 0; v_out_idx < num_vout; v_out_idx++) {
    						double val = data[roi_pix][sub_idx][v_out_idx];
    						int vy = v_out_idx / vout_dim, vx = v_out_idx % vout_dim;
    						double M = 0;
    						for (int dpy = -vel_suppr_rad; dpy <= vel_suppr_rad; dpy++) {
    							int ny = py + dpy;
    							for (int dpx = -vel_suppr_rad; dpx <= vel_suppr_rad; dpx++) {
    								int nx = px + dpx;
    								if (nx < roi_x0 || nx >= roi_x1 || ny < roi_y0 || ny >= roi_y1) continue;
    								int nroi = (ny - roi_y0) * roi.width + (nx - roi_x0);
    								for (int dvy = -vel_suppr_rad; dvy <= vel_suppr_rad; dvy++) {
    									int nvy = vy + dvy;
    									if (nvy < 0 || nvy >= vout_dim) continue;
    									for (int dvx = -vel_suppr_rad; dvx <= vel_suppr_rad; dvx++) {
    										int nvx = vx + dvx;
    										if (nvx < 0 || nvx >= vout_dim) continue;
    										double d = data[nroi][sub_idx][nvy * vout_dim + nvx];
    										if (d > M) M = d;
    									}
    								}
    							}
    						}
    						double thresh = Math.max(athresh, M * rthresh);
    						result[roi_pix][sub_idx][v_out_idx] =((val < dbg_min) || ((M - val) >= thresh)) ? 0 : val;
    					}
    				}
    			}
    		}};
    	}
    	ImageDtt.startAndJoin(threads);

    	// Margin pixels: additionally check image bounds before ROI bounds.
    	ai.set(0);
    	for (int ithread = 0; ithread < threads.length; ithread++) {
    		threads[ithread] = new Thread() { public void run() {
    			for (int nPix = ai.getAndIncrement(); nPix < indx_margins_vsupp.length; nPix = ai.getAndIncrement()) {
    				int ipix = indx_margins_vsupp[nPix];
    				int px = ipix % width, py = ipix / width;
    				if (px < roi_x0 || px >= roi_x1 || py < roi_y0 || py >= roi_y1) continue;
    				int roi_pix = (py - roi_y0) * roi.width + (px - roi_x0);
    				for (int sub_idx = 0; sub_idx < num_sub; sub_idx++) {
    					for (int v_out_idx = 0; v_out_idx < num_vout; v_out_idx++) {
    						double val = data[roi_pix][sub_idx][v_out_idx];
    						int vy = v_out_idx / vout_dim, vx = v_out_idx % vout_dim;
    						double M = 0;
    						for (int dpy = -vel_suppr_rad; dpy <= vel_suppr_rad; dpy++) {
    							int ny = py + dpy;
    							if (ny < 0 || ny >= height) continue;
    							for (int dpx = -vel_suppr_rad; dpx <= vel_suppr_rad; dpx++) {
    								int nx = px + dpx;
    								if (nx < 0 || nx >= width) continue;
    								if (nx < roi_x0 || nx >= roi_x1 || ny < roi_y0 || ny >= roi_y1) continue;
    								int nroi = (ny - roi_y0) * roi.width + (nx - roi_x0);
    								for (int dvy = -vel_suppr_rad; dvy <= vel_suppr_rad; dvy++) {
    									int nvy = vy + dvy;
    									if (nvy < 0 || nvy >= vout_dim) continue;
    									for (int dvx = -vel_suppr_rad; dvx <= vel_suppr_rad; dvx++) {
    										int nvx = vx + dvx;
    										if (nvx < 0 || nvx >= vout_dim) continue;
    										double d = data[nroi][sub_idx][nvy * vout_dim + nvx];
    										if (d > M) M = d;
    									}
    								}
    							}
    						}
    						double thresh = Math.max(athresh, M * rthresh);
    						result[roi_pix][sub_idx][v_out_idx] =((val < dbg_min) || ((M - val) >= thresh)) ? 0 : val;
    					}
    				}
    			}
    		}};
    	}
    	ImageDtt.startAndJoin(threads);
    	return result;
    }

	// === C5P Bayesian posterior / kernel-energy / velocity-prior / score / softmax / whitening / posterior-gate / lambda-hyperstack section removed 2026-06-20; predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026

}
