Commit 0b385735 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: 3-C rung 2 - 15-arg peak contract in the lean oracle, FZ-norm in...

CLAUDE: 3-C rung 2 - 15-arg peak contract in the lean oracle, FZ-norm in place, pose_corr checkpoint-2 export

Andrey's ruling 07/13/2026 (frozen GPU contract = 15-arg getMaxXYCmEig
superset; recentering runtime knob; de-bias optional, default off):

- CuasRtParameters: NEW 'CUAS RT' knobs pose_recenter (recentering passes,
  0 = validated single-pass baseline) + pose_debias (window-autocorr envelope
  de-bias fraction, 0 = off); corr-xml _curt_pose_recenter/_curt_pose_debias.
- CuasPoseRT.leanMeasure: peak measurement switched to the 15-arg
  getMaxXYCmEig (eig_fast2x2=true = the contract's closed-form eigen);
  windowAutocorrDebias() = the single source of the de-bias array (Java
  oracle, export case, JNA host).
- TDCorrTile.convertTDtoPDInPlace: corr2D_normalize runs on the inter corr
  TD buffer where execCorr2D_inter_TD left it (NULL weights == the repack
  path's uniform 1.0 -> bit-identical PD); the TD D2H->repack->H2D round
  trip is gone from the lean chain on both backends.
- PoseCorrExport: checkpoint 2 in the same case - expected_corr_pd
  (GPU-produced normalized PD, the tol-0 target for the in-place replay),
  expected_peaks (double Java oracle incl. weak-peak rejects as NaN rows),
  contract params + optional debias array in the manifest; compareResults
  round-trips corr_pd/peaks when the C++ test writes them.

mvn -DskipTests clean package OK; C++ side: tile_processor_gpu same-day
commit (corr2D_peak_eig + tests, synthetic gate PASS, regressions PASS).
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 18be472c
......@@ -431,30 +431,52 @@ public class CuasPoseRT {
// pose_corr capture: checkpoint 1 = the raw TD correlation + packed indices right
// here, BEFORE fat-zero normalization (rung 2's boundary). By Claude on 07/13/2026.
PoseCorrExport.iterCorr(gpuQuad, scene, col_weights);
// 5. TD tiles -> FZ-normalize -> pixel domain (the JNA-validated TDCorrTile path)
final TDCorrTile [] td_tiles = TDCorrTile.getFromGpu(gpuQuad);
final double [][] corr_pd = TDCorrTile.convertTDtoPD(
// 5. FZ-normalize IN PLACE (rung 2 first half, 07/13/2026): corr2D_normalize runs on
// the TD buffer where execCorr2D_inter_TD left it - the old getFromGpu/convertTDtoPD
// TD D2H -> repack -> H2D round trip is gone (PD result bit-identical: per-tile
// independent kernel, uniform weight 1.0 == null weights - see convertTDtoPDInPlace).
// The PD D2H below is the LAST remaining bulk transfer - it goes away with the
// rung-2 peak kernel. By Claude on 07/13/2026 (was the TDCorrTile round-trip path).
final double [][] corr_pd = TDCorrTile.convertTDtoPDInPlace(
gpuQuad,
td_tiles,
0xFE, // corr_type (sum slot, as CuasMotion)
clt_parameters.curt.fz_inter, // RT-decoupled INTER fat zero (was erroneously the INTRA getGpuFatZero) // By Claude on 07/05/2026
debugLevel);
if (corr_pd == null) {
System.out.println("leanMeasure(): convertTDtoPD returned null for scene "+scene.getImageName());
return null;
}
// pose_corr capture: checkpoint 2 = the FZ-normalized PD (in-place-normalize tol-0
// target) + the frozen 15-arg peak-contract oracle on every PD tile. Armed-only no-op.
// By Claude on 07/13/2026.
PoseCorrExport.iterPd(clt_parameters, gpuQuad, scene, corr_pd);
if (corr_pd_out != null) corr_pd_out[0] = corr_pd; // debug rendering (-POSE-RT-CORR2D) // By Claude on 07/04/2026
// (img_out is rendered in step 3, BEFORE the sum scaling - the true weighted average) // By Claude on 07/04/2026
// 6. peak + eigen per tile (the GPU argmax+eigen kernel's Java oracle)
// 6. peak + eigen per tile (the GPU argmax+eigen kernel's Java oracle).
// FROZEN RUNG-2 GPU CONTRACT (Andrey's ruling 07/13/2026): the 15-arg
// getMaxXYCmEig superset - closed-form 2x2 eigen (getEigen2x2, the GPU-portable
// path) + recentering as a RUNTIME parameter (curt.pose_recenter, 0 = the
// validated single-pass baseline) + optional envelope de-bias as a precomputed
// multiplicative array (curt.pose_debias, 0 = off; the kernel takes the array
// pointer, NULL = off). Oracle and kernel share this exact shape so the
// checkpoint-2 comparison is contract-vs-contract. By Claude on 07/13/2026.
final int corr_width = 2 * GPUTileProcessorDttSize() - 1;
final double [] debias = (clt_parameters.curt.pose_debias > 0) ?
windowAutocorrDebias(GPUTileProcessorDttSize(), clt_parameters.curt.pose_debias) : null;
final double [][] centers = new double [num_pix][];
final double [][] vector_XYS = new double [num_pix][];
final double [][] eigen = new double [num_pix][];
int num_meas = 0;
for (int i = 0; i < num_pix; i++) {
if ((i >= corr_pd.length) || (corr_pd[i] == null) || (pXpYD_center[i] == null)) continue;
double [] corr_tile = corr_pd[i]; // not modified by getMaxXYCmEig (fpn_mask == null)
if (debias != null) { // per-tile copy: corr_pd stays raw for -POSE-RT-CORR2D // By Claude on 07/13/2026
corr_tile = new double [corr_pd[i].length];
for (int k = 0; k < corr_tile.length; k++) {
corr_tile[k] = corr_pd[i][k] * debias[k];
}
}
final double [] r = Correlation2d.getMaxXYCmEig(
corr_pd[i], // will not be modified (fpn_mask == null)
corr_tile,
corr_width,
clt_parameters.imp.eig_min_abs, // abs_min
clt_parameters.imp.eig_min_rel, // rel_min
......@@ -463,9 +485,14 @@ public class CuasPoseRT {
// eigen lambdas -> sqrt(l) hits eig_max_sqrt -> setEigenTransform
// k=max(0,1/sqrt(l)-1/max) ZEROES wide-peak tiles (the eigen-ON
// 3x slow convergence). By Claude on 07/12/2026, Andrey's go.
clt_parameters.curt.pose_recenter,// recentering passes (0 = baseline single-pass) // By Claude on 07/13/2026
clt_parameters.imp.eig_sub_frac1, // pedestal suppression during recentering
clt_parameters.imp.eig_scale_axes,// recenter window: scale eigen-ellipse half-axes
clt_parameters.imp.eig_inc_axes, // recenter window: add to half-axes, pix
null, // fpn_mask (input is FPN-subtracted)
false, // ignore_border
null, // debug_data
true, // eig_fast2x2 FROZEN true: the closed-form eigen IS the contract
false); // debug
if (r == null) continue;
centers[i] = pXpYD_center[i];
......@@ -484,6 +511,47 @@ public class CuasPoseRT {
return com.elphel.imagej.gpu.GPUTileProcessor.DTT_SIZE;
}
/**
* Precomputed envelope de-bias array for the lean peak measurement (rung-2 optional
* hook, Andrey's ruling 07/13/2026 - default OFF). The pixel-domain correlation of two
* MCLT-windowed tiles is modulated by the window AUTOCORRELATION envelope (separable,
* peaked at center): a displaced peak rides an envelope that decays away from center,
* so its centroid is pulled back toward zero - the CM argmax under-reports offsets by
* ~0.75-0.9x (probed 07/13; the estimator itself is unbiased, the envelope is the bias).
* De-bias = multiply the correlation by envelope^-frac before peak extraction.
* The GPU kernel takes the SAME array as an optional pointer (NULL = off); this is the
* single source of the array for the Java oracle, the export case, and the JNA host.
* @param transform_size DTT size (8): window = sin(pi/(2*ts)*(i+0.5)), 2*ts samples
* @param frac exponent fraction: 0 = all-ones (no-op), 1 = full envelope divide
* @return [(2*ts-1)^2] multiplicative de-bias, 1.0 at the center pixel
* By Claude on 07/13/2026.
*/
public static double [] windowAutocorrDebias(final int transform_size, final double frac) {
final int wnd_len = 2 * transform_size; // MCLT window length (16)
final int corr_width = 2 * transform_size - 1; // correlation tile side (15)
final double [] wnd = new double [wnd_len];
for (int i = 0; i < wnd_len; i++) {
wnd[i] = Math.sin(Math.PI * (i + 0.5) / wnd_len);
}
final double [] acorr = new double [corr_width]; // 1D autocorr, index d+ (ts-1), d = -(ts-1)..(ts-1)
for (int d = -(transform_size - 1); d <= (transform_size - 1); d++) {
double s = 0.0;
for (int i = Math.max(0, -d); i < Math.min(wnd_len, wnd_len - d); i++) {
s += wnd[i] * wnd[i + d];
}
acorr[d + transform_size - 1] = s;
}
final double center = acorr[transform_size - 1]; // normalize so the center is 1.0
final double [] debias = new double [corr_width * corr_width];
for (int iy = 0; iy < corr_width; iy++) {
for (int ix = 0; ix < corr_width; ix++) {
final double env = (acorr[iy] / center) * (acorr[ix] / center); // separable 2D envelope
debias[iy * corr_width + ix] = Math.pow(env, -frac);
}
}
return debias;
}
/**
* Deterministic task-stream order (3-A3, GPU static shape): the setInterTasks*
* builders fill their output from worker threads (aTiles.getAndIncrement()), so the
......
......@@ -465,6 +465,66 @@ public class TDCorrTile {
return mapped_corrs;
}
/**
* IN-PLACE variant of getFromGpu + convertTDtoPD for the interscene (lean pose) path -
* rung 2 first half (roadmap 3-C, Andrey's ruling 07/13/2026): normalize the correlation
* TD buffer WHERE execCorr2D_inter_TD left it instead of the TD D2H -> host repack ->
* H2D -> normalize -> PD D2H round trip. corr2D_normalize is per-tile independent and
* stride-based, so normalizing ALL resident slots (per-sensor + sum; the lean mask-1 run
* has 2 per tile) yields sum-slot PD bit-identical to the repacked path: the repack
* passed per-tile weight = num-pairs-1 = 1.0 (TDCorrTile.getFromGpu weight arg), and
* fat_zero2/1.0f == fat_zero2 == the null-weights path (the JNA backend ignored weights
* already). The raw TD buffer is NOT modified (normalize reads TD, writes the PD buffer),
* so the pose_corr checkpoint-1 capture is unaffected. Only the small index array and
* the PD tiles cross D2H - the TD never leaves the GPU; the PD D2H itself goes away when
* the rung-2 peak kernel lands (only per-tile {dx,dy,str,eigen} will cross).
* NaN-poisoned tiles (consolidation OOB) map to null, as getFromGpu's skip did.
* By Claude on 07/13/2026.
* @param gpuQuad GPU quad instance (after execCorr2D_inter_TD)
* @param gpu_fat_zero absolute fat zero (curt.fz_inter)
* @param debug_level debug level
* @return sparse array in line-scan order, each element null or double[225]
*/
public static double [][] convertTDtoPDInPlace(
final GpuQuad gpuQuad,
final double gpu_fat_zero,
final int debug_level
){
final int gpu_corr_rad = GPUTileProcessor.DTT_SIZE - 1;
final int tilesX = gpuQuad.getImageWidth() / GpuQuad.getDttSize();
final int tilesY = gpuQuad.getImageHeight() / GpuQuad.getDttSize();
final int [] indices = gpuQuad.getCorrIndices(); // small D2H; also refreshes num_corr_tiles for normalize/getCorr2D
gpuQuad.execCorr2D_normalize(
false, // per-pair buffer (the inter output)
gpu_fat_zero,
null, // weights: null == the repacked path's uniform 1.0 (see header)
gpu_corr_rad);
final float [][] fcorr2D = gpuQuad.getCorr2D(gpu_corr_rad);
final double [][] mapped_corrs = new double [tilesX * tilesY][];
final Thread[] threads = ImageDtt.newThreadArray(ImageDtt.THREADS_MAX);
final AtomicInteger ai = new AtomicInteger(0);
for (int ithread = 0; ithread < threads.length; ithread++) {
threads[ithread] = new Thread() {
public void run() {
for (int i = ai.getAndIncrement(); i < indices.length; i = ai.getAndIncrement()) {
if ((indices[i] & 0xff) != 0xff) continue; // sum slot only (correlate2D_inter marks it 0xff)
final float [] ftile = fcorr2D[i];
final double [] dtile = new double [ftile.length];
copy_tile: {
for (int k = 0; k < ftile.length; k++) {
if (Float.isNaN(ftile[k])) break copy_tile; // poisoned (OOB) tile -> null
dtile[k] = ftile[k];
}
mapped_corrs[indices[i] >> GPUTileProcessor.CORR_NTILE_SHIFT] = dtile;
}
}
}
};
}
ImageDtt.startAndJoin(threads);
return mapped_corrs;
}
/**
* Calculate per-tile motion vectors {x,y,strength_over_min) using centroid approach
* @param tiles sparse array of correlation tiles (now 15*15=255 pix)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment