Commit a65cd04a authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: A2 step 1 - raw-jp4 + CuasConditioning ingest feeding the GPU directly (curt_pose_raw)

Per Andrey: bypass the QuadCLT image_data mechanics entirely and feed the GPU
directly - the permanent RT shape (more data becomes GPU-memory-resident;
QuadCLT stays for geometry/poses only).

- CuasMotion.readRawImageData(): raw /jp4/ reader extracted from
  perSensorFromRawJp4 (oracle getJp4Tiff, one thread/sensor, instrumentation);
  perSensorFromRawJp4 rewired, behavior unchanged.
- CuasConditioning.conditionSceneToGpu(): raw read -> condition() with the
  CURRENT calibration (curt_calib-updated lwir scales/offsets/scales2 +
  per-pixel FPN from the scene QuadCLT) -> bind scene (saveQuadClt,
  conditional) -> clear hasNewImageData -> setBayerImages(data,true) force-H2D.
  The bayer guard (ebef0b23 fix) keeps the upload alive through
  interCorrPair's own setBayerImages(false). TELL if the guard ever breaks:
  results become identical to the prepared-data path.
- CuasPoseRT: curt_pose_raw flag (new checkbox 'Pose test raw-jp4 ingest (A2)')
  runs the ingest per scene before the fit; ingest failure coasts the
  prediction and records an empty CSV/hyper row (fail=-1).

Acceptance (recorded): A2 legitimately diverges from phase A (old
Photogrammetric Calibration was broken, frozen from unrelated footage) -
judge by A2's OWN dstored/corr-RMS/convergence; systematically worse = red flag.

mvn compile clean.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 6ecc13ad
......@@ -4261,6 +4261,30 @@ public class CuasMotion {
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();
......@@ -4280,14 +4304,12 @@ public class CuasMotion {
}
}
if (found == 0) {
System.out.println("perSensorFromRawJp4(): no source files found for set '"+set_name+
"' - is the jp4 source dir available? Skipping raw render.");
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
// (matches the RT model: 4 SATA paths x 4 sensors read concurrently). Reuses the existing oracle reader;
// a custom fast tiff read (skip Exif/telemetry parse on the uncompressed 16-bit BE tiff) is deferred until/
// unless this read becomes CPU-bound.
// 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);
......@@ -4296,7 +4318,7 @@ public class CuasMotion {
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("perSensorFromRawJp4(): no source file for sensor "+ncam); continue; }
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,
......@@ -4310,27 +4332,24 @@ public class CuasMotion {
};
}
ImageDtt.startAndJoin(threads);
// By Claude on 07/02/2026: convert with the test's OWN uniform task grid (leftover GPU tasks after an
// MB-enabled render are the secondary set with negative fractional scales -> "negative raw" images, and
// a leftover virtual-view grid loses the same border ROI on every sensor). Do NOT setImageData(raw): it
// destroyed the scene's conditioned data and armed the hasNewImageData/image_data_alt re-pull trap.
{ // By Claude on 07/02/2026: instrumentation - verify what was actually read (raw LWIR should be ~+20800)
{ // 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)));
}
System.out.println("perSensorFromRawJp4(): read raw source for set '"+set_name+"' ("+found+
" source files, "+num_null+" null sensors) means:"+sb);
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("perSensorFromRawJp4(): "+num_null+" sensors have NO raw data - aborting raw render "+
"(would crash or silently keep previous GPU bayer)");
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;
}
}
// 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);
return raw;
}
......
......@@ -23,6 +23,10 @@ package com.elphel.imagej.cuas.rt;
**
*/
import com.elphel.imagej.cuas.CuasMotion;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.tileprocessor.QuadCLT;
/**
* Lean, self-contained per-sensor conditioning for the CUAS real-time / tile-processor path.
* By Claude on 06/30/2026.
......@@ -99,6 +103,52 @@ public class CuasConditioning {
}
}
/**
* The A2/RT ingest: read a scene's RAW /jp4/ source, condition it with the CURRENT
* calibration (curt_calib-updated lwir scales/offsets/scales2 + per-pixel FPN from the
* scene QuadCLT), and feed the GPU DIRECTLY - completely bypassing the QuadCLT
* image_data mechanics (per Andrey: this is the permanent RT shape, especially as more
* data becomes GPU-memory-resident; QuadCLT stays for geometry/poses only).
*
* GPU handoff contract (validated by the bayer-guard fix, ebef0b23): bind the scene
* FIRST (saveQuadClt - conditional, resets the guard only on instance switch), clear
* hasNewImageData, then FORCE-upload - setBayerImages(data,true) sets bayer_set, so the
* pipeline's later pull-variant setBayerImages(false) skips its re-pull and the
* conditioned pixels survive through convert_direct. TELL if this ever breaks: the
* downstream results become identical to the prepared-data path (silent re-pull).
*
* By Claude on 07/05/2026, from Andrey's design.
* @param scene physical scene: /jp4/ source, calibration globals, FPN, geometry
* @param cfg conditioning configuration (null - defaults: rowcol+photometric+FPN)
* @param threadsMax max read threads
* @param debugLevel debug verbosity
* @return true on success; false if raw source missing (GPU state untouched)
*/
public static boolean conditionSceneToGpu(
final QuadCLT scene,
final Config cfg,
final int threadsMax,
final int debugLevel) {
final double [][][] data = CuasMotion.readRawImageData(scene, threadsMax, debugLevel);
if (data == null) {
return false;
}
final int width = scene.getGeometryCorrection().getSensorWH()[0];
condition(
data, // double [][][] image_data (in place)
width, // int width
scene.getLwirScales(), // double [] scales
scene.getLwirOffsets(), // double [] offsets
scene.getLwirScales2(), // double [] scales2
scene.getFPN(), // double [][][] fpn (null - none)
cfg); // Config (null - defaults)
final GpuQuad gpuQuad = scene.getGPUQuad();
scene.saveQuadClt(); // bind this scene (conditional; resets bayer guard on switch)
scene.setHasNewImageData(false); // the guard's other re-pull trigger - must be clear
gpuQuad.setBayerImages(data, true); // force H2D; sets bayer_set -> pipeline skips its pull
return true;
}
/** Row/Col denoise for one sensor's flat pixel array (in place): subtract per-row, then per-col. */
public static void subtractRowColSensor(final double [] px, final int width, final Config cfg) {
final int height = px.length / width;
......
......@@ -32,6 +32,7 @@ import java.util.Arrays;
import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.tileprocessor.ErsCorrection;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.Interscene;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
......@@ -402,6 +403,36 @@ public class CuasPoseRT {
}
quadCLTs[nscene].getErsCorrection().setErsDt(
QuadCLTCPU.scaleDtToErs(clt_parameters, dxyzatr_dt));
// Phase A2 ingest (curt_pose_raw): read RAW /jp4/, condition with the CURRENT
// calibration and force-upload straight to the GPU - bypasses the prepared
// image_data (old broken Photogrammetric Calibration). The bayer guard keeps
// the upload alive through interCorrPair's own setBayerImages(false).
// A2 results legitimately diverge from phase A (cleaner input) - judge by own
// dstored quality. By Claude on 07/05/2026, from Andrey's design.
if (clt_parameters.imp.curt_pose_raw) {
final boolean raw_ok = CuasConditioning.conditionSceneToGpu(
quadCLTs[nscene], // QuadCLT scene (jp4 + calibration + FPN)
null, // Config (defaults: rowcol + photometric + FPN)
ImageDtt.THREADS_MAX, // read threads
debugLevel - 2); // debug
if (!raw_ok) {
System.out.println("CuasPoseRT: scene "+nscene+" ("+ts_name+
") raw-jp4 ingest FAILED - coasting prediction");
fitted[nscene] = new double [][] {predicted[0].clone(), predicted[1].clone()};
num_fail++;
// keep CSV/hyper alignment: record an empty measurement row
final float [][] nan_comps = new float [HYPER_COMPONENTS.length][num_pix];
for (float [] c : nan_comps) Arrays.fill(c, Float.NaN);
hyper_scenes.add(nan_comps);
hyper_ts.add(ts_name);
sb.append(nscene+", "+ts_name);
for (int j = 0; j < 12; j++) sb.append(", NaN");
sb.append(", NaN, 0.0, 0, -1\n");
prev_pose = predicted;
prev_ts = ts;
continue;
}
}
// Motion-blur compensation (gated by the same imp.mb_en as offline): per-tile blur
// vectors from the finite-difference rates -> interCorrPair takes the
// setInterTasksMotionBlur/interCorrTDMotionBlur path (convert_direct runs twice,
......
......@@ -1132,6 +1132,7 @@ min_str_neib_fpn 0.35
public double curt_pose_dxy_k = 0.75; // tile-selection outlier gate: keep tiles with max-over-scenes residual <= median + k*NMAD of the finite per-tile maxes (scale-free - adapts to footage quality/sequence length; NaN-in-any-scene = +inf, always rejected). <=0 - skip the gate. // By Claude on 07/04/2026
public int curt_pose_num_tiles = 150; // tile-selection compute budget: after the gate, keep this many BEST (smallest max-residual) tiles; threshold-free rank - always yields the best available population. <=0 - no cap. // By Claude on 07/04/2026
public boolean curt_pose_recalc = false; // force a FULL-selection pose run that regenerates the -POSE-RT-MAXDXY calibration even if it exists. Default (false): automatic FPN-style reuse - if -POSE-RT-MAXDXY exists, derive the selection from it (NMAD gate + rank-N) and use it; if not, run full and generate it. // By Claude on 07/04/2026
public boolean curt_pose_raw = false; // phase A2 ingest: per scene read RAW /jp4/, condition with the current calibration (CuasConditioning: rowcol + photometric + FPN) and FORCE-upload straight to the GPU, bypassing the QuadCLT prepared image_data (which carries the old broken Photogrammetric Calibration). Judge by own dstored quality, not agreement with phase A. // By Claude on 07/05/2026
//=== LoG prefilter ===
public double curt_psf_radius = 1.0; // sensor PSF radius for LoG pre-filter
public double curt_n_sigma = 4.0; // cutoff LoG kernel array, number of sigmas
......@@ -3431,6 +3432,8 @@ min_str_neib_fpn 0.35
"After the gate keep this many best (smallest max-residual) tiles - the RT compute budget. <=0 - no cap.");
gd.addCheckbox ("Pose test force recalc calibration", this.curt_pose_recalc, // By Claude on 07/04/2026
"Force a FULL pose run that regenerates -POSE-RT-MAXDXY even if it exists. Default OFF = automatic reuse: use the calibration if present (filtered run), else run full and generate it.");
gd.addCheckbox ("Pose test raw-jp4 ingest (A2)", this.curt_pose_raw, // By Claude on 07/05/2026
"Per scene: read RAW /jp4/, condition with the CURRENT calibration (rowcol+photometric+FPN, CuasConditioning) and force-upload straight to the GPU, bypassing the prepared image_data (old broken Photogrammetric Calibration).");
gd.addMessage("=== LoG prefilter ===");
gd.addNumericField("Optical PSF radius", this.curt_psf_radius, 6,8,"pix",
......@@ -5028,6 +5031,7 @@ min_str_neib_fpn 0.35
this.curt_pose_dxy_k = gd.getNextNumber(); // By Claude on 07/04/2026
this.curt_pose_num_tiles =(int) gd.getNextNumber(); // By Claude on 07/04/2026
this.curt_pose_recalc = gd.getNextBoolean(); // By Claude on 07/04/2026
this.curt_pose_raw = gd.getNextBoolean(); // By Claude on 07/05/2026
this.curt_psf_radius = gd.getNextNumber();
this.curt_n_sigma = gd.getNextNumber();
......@@ -6399,6 +6403,7 @@ min_str_neib_fpn 0.35
properties.setProperty(prefix+"curt_pose_dxy_k", this.curt_pose_dxy_k+""); // double // By Claude on 07/04/2026
properties.setProperty(prefix+"curt_pose_num_tiles", this.curt_pose_num_tiles+""); // int // By Claude on 07/04/2026
properties.setProperty(prefix+"curt_pose_recalc", this.curt_pose_recalc+""); // boolean // By Claude on 07/04/2026
properties.setProperty(prefix+"curt_pose_raw", this.curt_pose_raw+""); // boolean // By Claude on 07/05/2026
properties.setProperty(prefix+"curt_psf_radius", this.curt_psf_radius+""); // double
properties.setProperty(prefix+"curt_n_sigma", this.curt_n_sigma+""); // double
......@@ -6804,6 +6809,7 @@ min_str_neib_fpn 0.35
if (properties.getProperty(prefix+"curt_pose_dxy_k")!=null) this.curt_pose_dxy_k=Double.parseDouble(properties.getProperty(prefix+"curt_pose_dxy_k")); // By Claude on 07/04/2026
if (properties.getProperty(prefix+"curt_pose_num_tiles")!=null) this.curt_pose_num_tiles=Integer.parseInt(properties.getProperty(prefix+"curt_pose_num_tiles")); // By Claude on 07/04/2026
if (properties.getProperty(prefix+"curt_pose_recalc")!=null) this.curt_pose_recalc=Boolean.parseBoolean(properties.getProperty(prefix+"curt_pose_recalc")); // By Claude on 07/04/2026
if (properties.getProperty(prefix+"curt_pose_raw")!=null) this.curt_pose_raw=Boolean.parseBoolean(properties.getProperty(prefix+"curt_pose_raw")); // By Claude on 07/05/2026
if (properties.getProperty(prefix+"curt_psf_radius")!=null) this.curt_psf_radius=Double.parseDouble(properties.getProperty(prefix+"curt_psf_radius"));
if (properties.getProperty(prefix+"curt_n_sigma")!=null) this.curt_n_sigma=Double.parseDouble(properties.getProperty(prefix+"curt_n_sigma"));
......@@ -9094,6 +9100,7 @@ min_str_neib_fpn 0.35
imp.curt_pose_dxy_k = this.curt_pose_dxy_k; // By Claude on 07/04/2026
imp.curt_pose_num_tiles = this.curt_pose_num_tiles; // By Claude on 07/04/2026
imp.curt_pose_recalc = this.curt_pose_recalc; // By Claude on 07/04/2026
imp.curt_pose_raw = this.curt_pose_raw; // By Claude on 07/05/2026
imp.curt_psf_radius = this.curt_psf_radius;
imp.curt_n_sigma = this.curt_n_sigma;
// rleak0 imp.X copy removed 2026-06-20 (LReLU now LINEAR); predecessor code at git tag cuas-layer1. // By Claude on 06/20/2026
......
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