Commit af6ef9ec authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: TILE-CALIB bootstrap + per-sequence RT-STATE.json + INTERFRAME poses-only

Missing-file rule (Andrey): calibrations are FILE-gated; missing -> rebuild
(slow first run), present -> reuse, erase -> force. Config keeps parameters
only.

- TILE-CALIB bootstrap: testPoseSequence = wrapper over poseSequencePass;
  missing -POSE-RT-TILE-CALIB (or pose_recalc ON) -> FULL pass generates and
  saves it, then AUTOMATICALLY re-runs filtered (~150 cluster tiles) - first
  run on a v013_min copy ends in the warmed-system state, per sequence
  (mixed-state batches self-serve). Bootstrap re-run ignores pose_recalc;
  pose_full still never reads/writes the calibration.
- NEW CuasRtState (-RT-STATE.json, Gson): per-sequence RT-only persistent
  variables in the CENTER model dir; global params stay with quadCLT_main /
  corr-xml (GUI save + compare/combine). Data-pure (paths, no QuadCLT),
  forward-compatible, failure-safe load. First tenant: dc_center - AUTO now
  reuses/derives-and-stores per sequence; the corr-xml config is never
  mutated (nonzero = manual override).
- INTERFRAME is QuadCLT-workflow/POSES ONLY (Andrey): rtPhotometricCalibration
  no longer writes the ref INTERFRAME; photometric persists via the explicit
  corr-xml save after a calib-ON run (quadCLT_main carries the values).
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 22d49ee1
......@@ -4156,13 +4156,15 @@ public class CuasMotion {
* 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 + persists them ({@link #applyLwirLinearCalibration}): reference scene's
* INTERFRAME corr-xml is saved, quadCLT_main carries them to the top-menu-saved main configuration and to
* newly created QuadCLT instances. 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.
* 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 + its INTERFRAME corr-xml saved.
* @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.
......@@ -4207,9 +4209,9 @@ public class CuasMotion {
// 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; the recalibrated
// values live only in the ref INTERFRAME + memory unless the main config is saved
// after a calib-ON run). By Claude on 07/05/2026.
// 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;
......@@ -4219,7 +4221,7 @@ public class CuasMotion {
applyLwirLinearCalibration(
phys, // QuadCLT phys_scene
apply_to, // QuadCLT [] apply_to (in-memory)
ref_scene, // QuadCLT save_scene -> INTERFRAME corr-xml
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
......
......@@ -549,6 +549,17 @@ public class CuasPoseRT {
/**
* Re-generate per-scene poses against the virtual-center reference, RT-style.
*
* TILE-CALIBRATION BOOTSTRAP (Andrey's missing-file rule, 07/05/2026): calibrations
* are FILE-gated, per scene sequence (the file lives in this sequence's CENTER model
* directory). If -POSE-RT-TILE-CALIB is missing (fresh v013_min copy) or
* curt.pose_recalc is ON, pass 1 runs FULL (all strength-selected tiles), generates
* and saves the calibration, and the sequence AUTOMATICALLY re-runs filtered
* (~150 cluster tiles): the first run is slow but ends in the same state as a warmed
* system, and all outputs are the filtered-run products (pass-1 outputs are
* overwritten; only the calibration file survives from pass 1). Any other sequence
* with its calibration already present bypasses the bootstrap entirely.
* Force a rebuild by ERASING the file - no configuration changes needed.
*
* @param clt_parameters processing parameters (uses curt.pose_str threshold,
* imp.lma_use_R, LMA/offset limits as the offline flow)
* @param center_CLT virtual-center reference: must have CenterClt (fclt) and
......@@ -563,6 +574,32 @@ public class CuasPoseRT {
final QuadCLT center_CLT,
final QuadCLT [] quadCLTs,
final int debugLevel) {
final boolean [] saved_calib = new boolean [1];
double [][][] fitted = poseSequencePass(clt_parameters, center_CLT, quadCLTs,
true, // allow_recalc: honor curt.pose_recalc on the first pass
saved_calib, // out: set when this pass generated+saved the tile calibration
debugLevel);
if (saved_calib[0]) { // bootstrap: calibration was just generated - re-run filtered
System.out.println("CuasPoseRT.testPoseSequence(): BOOTSTRAP - "+TILE_CALIB_SUFFIX+
" was just generated; re-running the sequence with the filtered selection"+
" (all outputs are overwritten by the filtered run)");
fitted = poseSequencePass(clt_parameters, center_CLT, quadCLTs,
false, // allow_recalc: ignore curt.pose_recalc - use the fresh calibration
null, // saved_calib: a filtered pass never writes the calibration
debugLevel);
}
return fitted;
}
/** The single-pass body of {@link #testPoseSequence}: one FULL or filtered sequence.
* By Claude on 07/05/2026 (bootstrap split). */
private static double [][][] poseSequencePass(
final CLTParameters clt_parameters,
final QuadCLT center_CLT,
final QuadCLT [] quadCLTs,
final boolean allow_recalc, // false on the bootstrap re-run: ignore curt.pose_recalc
final boolean [] saved_calib, // out (may be null): [0]=true if the tile calibration was saved
final int debugLevel) {
final double min_str = clt_parameters.curt.pose_str; // e.g. 1.0
final int margin = clt_parameters.imp.margin;
final double mb_tau = clt_parameters.imp.mb_tau;
......@@ -617,9 +654,10 @@ public class CuasPoseRT {
boolean full_selection = true; // the tile calibration is (re)saved only from a full-selection run
// pose_full (DEBUG): all strength-selected tiles, calibration neither read nor written
// (a measurement-debug run must not pollute the persistent tile calibration). By Claude on 07/04/2026.
ImagePlus imp_max = (clt_parameters.curt.pose_recalc || clt_parameters.curt.pose_full) ? null :
final boolean force_recalc = allow_recalc && clt_parameters.curt.pose_recalc; // bootstrap re-run never re-forces // By Claude on 07/05/2026
ImagePlus imp_max = (force_recalc || clt_parameters.curt.pose_full) ? null :
center_CLT.readImagePlusFromModelDirectory(TILE_CALIB_SUFFIX);
if ((imp_max == null) && !clt_parameters.curt.pose_recalc && !clt_parameters.curt.pose_full) { // legacy name fallback
if ((imp_max == null) && !force_recalc && !clt_parameters.curt.pose_full) { // legacy name fallback
imp_max = center_CLT.readImagePlusFromModelDirectory(TILE_CALIB_SUFFIX_OLD);
}
if (clt_parameters.curt.pose_full) {
......@@ -644,8 +682,9 @@ public class CuasPoseRT {
num_filt+" tiles (of "+num_reliable+" by strength)");
} else {
System.out.println("CuasPoseRT.testPoseSequence(): "+
(clt_parameters.curt.pose_recalc ? "curt_pose_recalc ON" : TILE_CALIB_SUFFIX+" not found")+
" - FULL run, will generate the calibration");
(force_recalc ? "curt_pose_recalc ON" : TILE_CALIB_SUFFIX+" not found")+
" - FULL run, will generate the calibration"+
(clt_parameters.curt.pose_full ? "" : ", then re-run filtered (bootstrap)"));
}
final double [][] pXpYD_center = OpticalFlow.transformToScenePxPyD(
null, // final Rectangle [] extra_woi,
......@@ -1093,6 +1132,7 @@ public class CuasPoseRT {
}
center_CLT.saveImagePlusInModelDirectory(null, new ImagePlus(
center_CLT.getImageName()+TILE_CALIB_SUFFIX, new FloatProcessor(tilesX, tilesY, fmax)));
if (saved_calib != null) saved_calib[0] = true; // triggers the bootstrap filtered re-run // By Claude on 07/05/2026
final boolean [] keep = deriveSelection(
fmax,
tilesX,
......
......@@ -102,13 +102,14 @@ public class CuasRT {
/**
* Per-sensor photometric (re)calibration (curt.calib): fit a+b*x over safe (weak/far)
* tiles, fold into the 16+16 lwir offsets/scales, apply in memory (master_CLT +
* reference scene + quadCLT_main + the ALREADY-SPAWNED per-scene instances) and save
* the reference scene's INTERFRAME corr-xml. Idempotent (a~0, b~1 when already
* calibrated). NOTE: the recalibrated values persist across sessions only via the
* ref INTERFRAME and a top-menu configuration save after a calib-ON run - a stale
* main config silently re-imposes the old photometric on every fresh session
* (POSE-09 lesson: 1470-count per-sensor sky spread vs 17 calibrated).
* tiles, fold into the 16+16 lwir offsets/scales, apply IN MEMORY ONLY (master_CLT +
* reference scene + quadCLT_main + the ALREADY-SPAWNED per-scene instances).
* Idempotent (a~0, b~1 when already calibrated). Persistence is deliberate and
* user-controlled: save the corr-xml configuration after the run, then propagate
* between configs with the compare/combine GUI (Andrey's workflow, 07/05/2026) -
* INTERFRAME is QuadCLT-workflow/POSES ONLY, never photometric. A stale config with
* calib OFF re-imposes the old photometric on every fresh session (POSE-09 lesson:
* 1470-count per-sensor sky spread vs 17 calibrated).
* By Claude on 07/03/2026 (moved 07/05/2026).
*/
private static void calibratePhotometric(
......@@ -146,10 +147,11 @@ public class CuasRT {
* over many TD points and costs float32 mantissa bits; LWIR is ADDITIVE (day/night and
* seasons shift the level, never scale it) - the borrowed center reflects its own
* footage vintage.
* Level: curt.dc_center; 0 = AUTO - median of the top (sky) half of the reference
* buffer render itself (the exact data being zeroed), then ADOPTED into curt.dc_center
* (persisted on the next configuration save - borrowable later, like the photometric
* calibration).
* Level: curt.dc_center nonzero = manual override (global config); 0 = AUTO - the
* per-sequence value from <center>-RT-STATE.json, derived when absent as the median of
* the top (sky) half of the reference buffer render itself (the exact data being
* zeroed) and stored back there. Erase the file/field to force a re-derive; the
* corr-xml config is never mutated.
* Mechanics: full-grid conditioned-disparity tasks (the ONE RT disparity source) ->
* setReferenceGPU (raw center up, tasks built) -> AUTO level from the reference render
* -> DC pattern = CLT of a UNIFORM image through the back-propagation convert
......@@ -195,8 +197,22 @@ public class CuasRT {
clt_parameters.imp.mb_max_gain_inter, // double mb_max_gain,
null, // double [][] mb_vectors (center does not move)
debugLevel); // int debug_level
// Level resolution (Andrey's persistence split, 07/05/2026): a NONZERO curt.dc_center
// = manual override from the global corr-xml config; 0 = AUTO - reuse the
// PER-SEQUENCE value from <center>-RT-STATE.json, deriving and storing it there when
// absent (the missing-file rule: erase the file/field to force a re-derive). The
// config is never mutated. By Claude on 07/05/2026.
double level = clt_parameters.curt.dc_center;
if (level == 0.0) { // auto: median of the top (sky) half of the reference buffer render
CuasRtState rt_state = null;
if (level == 0.0) { // auto: per-sequence persisted value first
rt_state = CuasRtState.load(center_CLT.getX3dDirectory(), center_CLT.getImageName());
if (rt_state.dc_center != null) {
level = rt_state.dc_center;
System.out.println("appropriateCenter(): center DC level = "+level+" (reused from "+
CuasRtState.SUFFIX+": "+rt_state.dc_center_source+")");
}
}
if (level == 0.0) { // derive: median of the top (sky) half of the reference buffer render
// Level from the reference render ITSELF (the exact data being zeroed),
// just uploaded by setReferenceGPU above. By Claude on 07/05/2026.
final float [] apx = CuasMotion.perSensorImagesFromTD(gpuQuad, true)[0];
......@@ -215,9 +231,11 @@ public class CuasRT {
}
Arrays.sort(samples, 0, n);
level = samples[n/2];
clt_parameters.curt.dc_center = level; // adopt: persisted on the next config save
rt_state.dc_center = level;
rt_state.dc_center_source = "auto: sky-half median of the reference buffer render";
rt_state.save(center_CLT.getX3dDirectory(), center_CLT.getImageName());
System.out.println("appropriateCenter(): AUTO center DC level = "+level+
" (median of the sky half of the reference buffer render), adopted into curt.dc_center");
" (median of the sky half of the reference buffer render), stored in "+CuasRtState.SUFFIX);
}
// the DC pattern: CLT of a uniform image through the back-propagation convert.
// The uniform image MUST be set on the GpuQuad's ASSOCIATED QuadCLT: both backends
......
/**
** CuasRtState.java - per-sequence persistent state of the CUAS RT mode (JSON)
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** CuasRtState.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.rt;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Persistent RT-ONLY state of ONE scene sequence: <center_name>-RT-STATE.json in the
* sequence's CENTER model directory (Andrey's ruling 07/05/2026). Scope split:
* - GLOBAL (all sequences) parameters stay with quadCLT_main / corr-xml (saved from
* the GUI, merged with the compare/combine tool);
* - PER-SEQUENCE derived values (vintage-specific calibrations the RT run computes
* and reuses) live HERE, in modern JSON - easy to read, easy to tell apart from
* the legacy parameter files, easy to erase for a forced re-derive (the
* missing-file rule: absent -> recompute+save, present -> reuse).
*
* The class is data-pure: it knows file paths, not QuadCLT - callers (CuasRT, the
* borrow boundary) pass the directory/name. Unknown JSON fields are ignored on load
* (forward compatibility); missing fields stay at their defaults (null = not yet
* derived). Add new RT persistent variables as public fields with a provenance
* string where useful.
*
* By Claude on 07/05/2026, from Andrey's design.
*/
public class CuasRtState {
public static final String SUFFIX = "-RT-STATE.json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
public int version = 1;
public String updated = null; // timestamp of the last save (informational)
// ---- persistent RT variables (per scene sequence) ----
public Double dc_center = null; // adopted borrowed-center DC level (counts); null = not yet derived
public String dc_center_source = null; // provenance, e.g. "auto: sky-half median of the reference render"
/** @return the state file path for this sequence. */
public static String path(String x3d_directory, String center_name) {
return x3d_directory + java.io.File.separator + center_name + SUFFIX;
}
/**
* Load the sequence state, or return a fresh (all-defaults) instance when the file
* is missing or unreadable (unreadable prints a warning - never breaks the run).
*/
public static CuasRtState load(String x3d_directory, String center_name) {
final String path = path(x3d_directory, center_name);
if (!Files.exists(Paths.get(path))) {
return new CuasRtState();
}
try (FileReader reader = new FileReader(path)) {
CuasRtState state = GSON.fromJson(reader, CuasRtState.class);
return (state != null) ? state : new CuasRtState();
} catch (IOException | RuntimeException e) { // malformed JSON -> JsonSyntaxException
System.out.println("CuasRtState.load(): FAILED to read "+path+" ("+e.getMessage()+
") - starting with a fresh state (derived values will be recomputed)");
return new CuasRtState();
}
}
/** Save the sequence state (pretty JSON, stamped). @return true on success. */
public boolean save(String x3d_directory, String center_name) {
final String path = path(x3d_directory, center_name);
this.updated = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
try (FileWriter writer = new FileWriter(path)) {
GSON.toJson(this, writer);
System.out.println("CuasRtState.save(): wrote "+path);
return true;
} catch (IOException e) {
System.out.println("CuasRtState.save(): FAILED to write "+path+" ("+e.getMessage()+")");
return false;
}
}
}
......@@ -21,7 +21,7 @@ public class CuasRtParameters {
public double pose_str = 1.0; // reliable-tile strength threshold over the combo-DSI strength for the pose test tile selection (1.0 ~ old getReliableTiles population). // By Claude on 07/03/2026
public double 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 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 pose_recalc = false; // force a FULL-selection pose run that regenerates the -POSE-RT-TILE-CALIB calibration even if it exists. Default (false): automatic FPN-style reuse - if -POSE-RT-TILE-CALIB 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 pose_recalc = false; // force regeneration of the -POSE-RT-TILE-CALIB calibration even if it exists: FULL-selection pass 1 rebuilds+saves it, then the sequence automatically re-runs filtered (bootstrap). Default (false): file-gated reuse - if -POSE-RT-TILE-CALIB exists, use it (NMAD gate + rank-N selection); if missing, the same full-then-filtered bootstrap runs automatically, so the PREFERRED force method is simply ERASING the file (this checkbox is a convenience, normally leave OFF). // By Claude on 07/05/2026
public boolean 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
public boolean pose_lean = false; // phase B measurement engine: TD-average the 16 sensors (CuasTD, CPU bridge) then ONE conj-multiply vs the virtual-center TD -> FZ-normalize -> PD -> argmax+eigen (getMaxXYCmEig) -> 3-angle LMA. All existing GPU kernels. v1: NO motion-blur compensation - compare vs the NOMB baseline. // By Claude on 07/05/2026
public boolean pose_full = false; // DEBUG: use ALL strength-selected tiles (~1074) - the -POSE-RT-TILE-CALIB calibration is neither read nor written (temporary bypass of the 150-tile filter for measurement debugging). // By Claude on 07/04/2026
......@@ -31,7 +31,7 @@ public class CuasRtParameters {
public boolean rend_test = false; // MINIMAL RT ingest+render test (CuasRender.testRenderSequence, takes precedence over pose test): per scene raw /jp4/ -> conditionSceneToGpu -> virtual-grid render at BORROWED stored pose+ERS rates -> -CUAS-RT-RENDER hyperstack [s00..s15, merged][scenes], comparable to -CUAS-INDIVIDUAL/MERGED-CUAS-DBG. Certify THIS before the pose test. // By Claude on 07/05/2026
public double fz_inter = 10000.0; // RT fat zero, INTER (pose/motion: scene x virtual-center correlation). DECOUPLED from the legacy gpu_fatz*/AUX switches (which stay untouched for non-RT/FOPEN). Value is for a single physical-pair amplitude; FZ ~ amplitude^2 in the GPU normalize - scale down x4..x16 for consolidated-average applications as needed. 10000 = the proven oracle INTER operating point (the lean code erroneously used the INTRA value before). // By Claude on 07/05/2026
public double fz_intra = 2000.0; // RT fat zero, INTRA (ranging/disparity: sensor-pair correlations within one scene). Reserved for the RT ranging path; same decoupling/scaling notes as fz_inter. // By Claude on 07/05/2026
public double dc_center = 0.0; // DC level of the BORROWED center (CLT-RESTORED/average): subtracted from the reference CLT at appropriation so it matches the zero-sky conditioned scenes (MCLT has no clean DC/AC split - a DC constant costs float32 accuracy). 0 = AUTO: compute as the median of the top (sky) half of the reference buffer render (the borrowed center as uploaded), adopt the value here (persisted on config save - borrowable later without our data, like the photometric calibration). Slow targets do not influence this shift. // By Claude on 07/05/2026
public double dc_center = 0.0; // DC level of the BORROWED center (CLT-RESTORED/average): subtracted from the center CLT at appropriation (CuasRT initialization) so it matches the zero-sky conditioned scenes (MCLT has no clean DC/AC split - a DC constant costs float32 accuracy). 0 = AUTO: reuse the PER-SEQUENCE value from <center>-RT-STATE.json; when absent, derive as the median of the top (sky) half of the reference buffer render and store it there (erase the file/field to re-derive - the missing-file rule). Nonzero = manual override; this config value is never mutated. Slow targets do not influence this shift. // By Claude on 07/05/2026
public double psf_radius = 1.0; // sensor PSF radius for LoG pre-filter
public double n_sigma = 4.0; // cutoff LoG kernel array, number of sigmas
public int pyramid = 7; // temporal pyramid levels
......
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