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

CLAUDE: curt_calib - photometric calibration as first step of the RT flow

New curt_calib parameter (CUAS RT dialog, saved/restored) runs/bypasses the
per-sensor photometric (re)calibration as the first step of the CUAS RT
processing flow, before detection (no longer tied to the diagnostic).
Extracted CuasMotion.rtPhotometricCalibration() = convertFromData() (upload
+ own uniform grid convert, split out of perSensorFromData) + fit + apply/
save. Production step converts and calibrates without saving stacks; the
curt_cond_test diagnostic (replaces detection) keeps the raw-vs-conditioned
stack compare and makes the calibration step save -CUAS-PERSENSOR[-ADJ].
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 0594db87
......@@ -4085,11 +4085,37 @@ public class CuasMotion {
int threadsMax,
int debugLevel) {
final GpuQuad gpuQuad = save_qclt.getGPUQuad();
final QuadCLT qclt = gpuQuad.getQuadCLT(); // physical scene bound to the GPU (geometry already loaded)
if (qclt == null) {
System.out.println("perSensorFromData(): no physical scene bound to the GPU - skipping");
if (!convertFromData(clt_parameters, gpuQuad, data, name_tag, threadsMax, debugLevel)) {
return null;
}
return perSensorAveragesFromTD(gpuQuad, false, name_tag, save_qclt);
}
/**
* Upload the given per-sensor pixel data and rebuild the main transform-domain buffer with the calibration
* task grid (uniform, sensor-domain, scale 1.0 - see {@link #uniformGridTasks}). Extracted from
* {@link #perSensorFromData} so the RT calibration step can convert without rendering/saving.
* By Claude on 07/03/2026.
* @param clt_parameters processing parameters (task grid, LPF sigmas).
* @param gpuQuad the GPU to use (its bound physical scene provides the geometry).
* @param data [sensor][color][pixel] pixel data to convert.
* @param name_tag tag for log messages only.
* @param threadsMax max threads.
* @param debugLevel debug verbosity.
* @return true on success, false if no physical scene is bound to the GPU.
*/
public static boolean convertFromData( // By Claude on 07/03/2026
CLTParameters clt_parameters,
GpuQuad gpuQuad,
double [][][] data,
String name_tag,
int threadsMax,
int debugLevel) {
final QuadCLT qclt = gpuQuad.getQuadCLT(); // physical scene bound to the GPU (geometry already loaded)
if (qclt == null) {
System.out.println("convertFromData(): no physical scene bound to the GPU - skipping");
return false;
}
final TpTask [] tp_tasks = uniformGridTasks(
qclt, // final QuadCLT qclt,
clt_parameters.imp.disparity_corr + qclt.getDispInfinityRef(), // final double disparity_corr,
......@@ -4103,10 +4129,10 @@ public class CuasMotion {
qclt.isLwir(),
clt_parameters.getScaleStrength(qclt.isAux()),
gpuQuad);
// By Claude on 07/02/2026: print the host-side data level (raw ~ +20800 counts, conditioned ~ -3500
// with the current stale offsets) so a clobbered upload is immediately visible against the saved stack.
System.out.println("perSensorFromData("+name_tag+"): host data s00 mean="+
meanFinite(data[0][0], gpuQuad.getImageWidth()*gpuQuad.getImageHeight()));
if (debugLevel > -3) { // host-side data level (raw ~ +20800 counts, conditioned ~ offset-subtracted)
System.out.println("convertFromData("+name_tag+"): host data s00 mean="+
meanFinite(data[0][0], gpuQuad.getImageWidth()*gpuQuad.getImageHeight()));
}
gpuQuad.setBayerImages(data, true); // FORCE upload the data under test
qclt.setHasNewImageData(false); // GPU now holds 'data': makes execConvertDirect()'s internal
// setBayerImages(false,...) skip its re-pull (base and JNA backends)
......@@ -4123,7 +4149,69 @@ public class CuasMotion {
clt_parameters.gpu_sigma_m, // final double gpu_sigma_m,
threadsMax, // final int threadsMax,
debugLevel); // final int globalDebugLevel)
return perSensorAveragesFromTD(gpuQuad, false, name_tag, save_qclt);
return true;
}
/**
* First step of the CUAS RT processing flow (curt_calib): per-sensor photometric (re)calibration.
* Converts the current scene's conditioned image_data with the calibration task grid, fits the per-sensor
* linear corrections over safe tiles ({@link #perSensorLinearFit}), folds them into the 16+16 lwir
* offsets/scales and applies + 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.
* @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 quadCLT_main top-level parameter-transfer instance (may be null).
* @param save_stacks save the -CUAS-PERSENSOR / -CUAS-PERSENSOR-ADJ debug stacks (test/diagnostic mode).
* @param threadsMax max threads.
* @param debugLevel debug verbosity.
* @return {a[num_sens], b[num_sens]} of the applied correction, or null if calibration was not possible.
*/
public static double [][] rtPhotometricCalibration( // By Claude on 07/03/2026
CLTParameters clt_parameters,
QuadCLT master_CLT,
QuadCLT ref_scene,
QuadCLT quadCLT_main,
boolean save_stacks,
int threadsMax,
int debugLevel) {
final GpuQuad gpuQuad = master_CLT.getGPUQuad();
final QuadCLT phys = gpuQuad.getQuadCLT(); // physical scene bound to the GPU: conditioned data + geometry
if (phys == null) {
System.out.println("rtPhotometricCalibration(): no physical scene bound to the GPU - skipping");
return null;
}
if (save_stacks) { // render + save the conditioned per-sensor stack (also prints the sensor-average spread)
perSensorFromData(clt_parameters, master_CLT, phys.getOrigImageData(),
"-CUAS-PERSENSOR", threadsMax, debugLevel);
} else { // convert only - no rendering/saving in the production RT step
if (!convertFromData(clt_parameters, gpuQuad, phys.getOrigImageData(),
"-CUAS-PERSENSOR", threadsMax, debugLevel)) {
return null;
}
}
final float [][] cond_images = perSensorImagesFromTD(gpuQuad, false);
final double [][] ab = perSensorLinearFit(
master_CLT, // QuadCLT center_CLT (combo DSI source, save target)
cond_images, // float [][] per_sensor (conditioned, FPN-corrected)
gpuQuad.getImageWidth(), // int width
0.5, // double max_strength, // weak tiles: strength < 0.5
1.0, // double max_disparity, // far tiles: disparity < 1
save_stacks, // boolean save_adjusted (-CUAS-PERSENSOR-ADJ)
debugLevel, // int debugLevel
true); // boolean keep_averages: average offset = 0, average scale = 1.0
if (ab != null) {
applyLwirLinearCalibration(
phys, // QuadCLT phys_scene
new QuadCLT [] {master_CLT, ref_scene, quadCLT_main}, // QuadCLT [] apply_to (in-memory)
ref_scene, // QuadCLT save_scene -> INTERFRAME corr-xml
ab[0], // double [] a,
ab[1], // double [] b,
debugLevel); // int debugLevel
}
return ab;
}
/** NaN-tolerant mean of the first num_pix elements (or all when num_pix <= 0). By Claude on 07/02/2026. */
......
......@@ -1125,6 +1125,7 @@ min_str_neib_fpn 0.35
// CUAS Realtime
public boolean curt_en = true; // enable cuas rt calculation (not needed with a separate button)
public boolean curt_calib = false; // first step of the CUAS RT processing flow: per-sensor photometric (re)calibration - fit a+b*x over safe (weak/far) tiles, fold into the 16+16 lwir offsets/scales, apply + save (reference scene INTERFRAME corr-xml, quadCLT_main -> main config). Bypass when off. // By Claude on 07/03/2026
public boolean curt_cond_test = false; // conditioning/calibration isolation test inside the curt_en branch: build the QuadCLT instances (borrowed calibration) then print per-sensor average spread (CuasMotion.perSensorAveragesFromTD) instead of normal RT detection; well-calibrated -> 16 sensor averages match (spread ~0). // By Claude on 07/01/2026
//=== LoG prefilter ===
public double curt_psf_radius = 1.0; // sensor PSF radius for LoG pre-filter
......@@ -3411,6 +3412,8 @@ min_str_neib_fpn 0.35
gd.addTab("CUAS RT", "CUAS Real Time");
gd.addCheckbox ("CUAS realtime enable", this.curt_en,
"Enable testing of the realtime CUAS detection.");
gd.addCheckbox ("CUAS RT photometric calibration", this.curt_calib, // By Claude on 07/03/2026
"First step of the RT processing flow: per-sensor photometric (re)calibration - fit a+b*x over safe (weak/far) tiles, fold into the 16+16 lwir offsets/scales, apply and save (reference scene INTERFRAME corr-xml, quadCLT_main for the main configuration). Bypass when off.");
gd.addCheckbox ("CUAS RT conditioning test", this.curt_cond_test,
"Isolation test inside the curt_en branch: build QuadCLT instances (borrowed calibration), then print per-sensor average spread (CuasMotion.perSensorAveragesFromTD) instead of normal RT detection. Well-calibrated -> the 16 sensor averages match (spread ~0).");
......@@ -5003,6 +5006,7 @@ min_str_neib_fpn 0.35
this.cuas_rng_limit = gd.getNextNumber();
this.curt_en = gd.getNextBoolean();
this.curt_calib = gd.getNextBoolean(); // By Claude on 07/03/2026
this.curt_cond_test = gd.getNextBoolean();
this.curt_psf_radius = gd.getNextNumber();
......@@ -6368,6 +6372,7 @@ min_str_neib_fpn 0.35
properties.setProperty(prefix+"cuas_rng_limit", this.cuas_rng_limit+""); // double
properties.setProperty(prefix+"curt_en", this.curt_en+""); // boolean
properties.setProperty(prefix+"curt_calib", this.curt_calib+""); // boolean // By Claude on 07/03/2026
properties.setProperty(prefix+"curt_cond_test", this.curt_cond_test+""); // boolean
properties.setProperty(prefix+"curt_psf_radius", this.curt_psf_radius+""); // double
......@@ -6767,6 +6772,7 @@ min_str_neib_fpn 0.35
private void getPropertiesCuasRT(String prefix,Properties properties){ // split out of getProperties() - 64KB method limit // By Claude on 06/12/2026
if (properties.getProperty(prefix+"curt_en")!=null) this.curt_en=Boolean.parseBoolean(properties.getProperty(prefix+"curt_en"));
if (properties.getProperty(prefix+"curt_calib")!=null) this.curt_calib=Boolean.parseBoolean(properties.getProperty(prefix+"curt_calib")); // By Claude on 07/03/2026
if (properties.getProperty(prefix+"curt_cond_test")!=null) this.curt_cond_test=Boolean.parseBoolean(properties.getProperty(prefix+"curt_cond_test"));
if (properties.getProperty(prefix+"curt_psf_radius")!=null) this.curt_psf_radius=Double.parseDouble(properties.getProperty(prefix+"curt_psf_radius"));
......@@ -9051,6 +9057,7 @@ min_str_neib_fpn 0.35
imp.cuas_rng_limit = this.cuas_rng_limit;
imp.curt_en = this.curt_en;
imp.curt_calib = this.curt_calib; // By Claude on 07/03/2026
imp.curt_cond_test = this.curt_cond_test;
imp.curt_psf_radius = this.curt_psf_radius;
imp.curt_n_sigma = this.curt_n_sigma;
......
......@@ -7274,54 +7274,35 @@ java.lang.NullPointerException
ImagePlus imp_targets = cuasRangingRT.prepareFpixels(); // GPU generator (explicit, CUDA-sensitive); also builds the QuadCLT instances = borrowed-calibration source
if (clt_parameters.imp.curt_calib) {
// By Claude on 07/03/2026: first step of the CUAS RT processing flow - per-sensor photometric
// (re)calibration: 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 -> top-menu config, template for
// new instances) and save the reference scene's INTERFRAME corr-xml. Idempotent (a~0, b~1 when
// already calibrated). Debug stacks (-CUAS-PERSENSOR[-ADJ]) saved only in curt_cond_test mode.
CuasMotion.rtPhotometricCalibration(
clt_parameters, // CLTParameters clt_parameters,
master_CLT, // QuadCLT master_CLT (combo DSI source, in-memory apply)
quadCLTs[ref_index], // QuadCLT ref_scene (photometric owner, INTERFRAME corr-xml)
quadCLT_main, // QuadCLT quadCLT_main (main config + new-instance template)
clt_parameters.imp.curt_cond_test, // boolean save_stacks
ImageDtt.THREADS_MAX, // int threadsMax,
debugLevel); // int debugLevel
}
if (clt_parameters.imp.curt_cond_test) {
// Conditioning/calibration isolation test (curt_cond_test): reuse the QuadCLT instances just built as the
// borrowed-calibration source and print the per-sensor average spread. Baseline reads the current-scene TD
// as conditioned by the existing path; the next step will feed raw jp4 through CuasConditioning.condition()
// and compare. Well-calibrated -> the 16 per-sensor averages match (spread ~0). Runs instead of RT detection.
// By Claude on 07/01/2026
// Conditioning/calibration diagnostic (curt_cond_test), runs INSTEAD of RT detection:
// raw /jp4/ baseline (no photometric/FPN/conditioning), saved as -CUAS-PERSENSOR-RAW for
// side-by-side compare with the conditioned -CUAS-PERSENSOR (saved by the calibration step when
// curt_calib is on, or rendered here otherwise). Both stacks use the same uniform sensor-domain
// task grid - never leftover GPU task state. By Claude on 07/01/2026, restructured 07/03/2026.
System.out.println("===== CUAS RT conditioning test (curt_cond_test): per-sensor average spread =====");
// By Claude on 07/02/2026: both stacks are now converted with the test's OWN uniform sensor-domain
// task grid (scale 1.0) instead of leftover GPU state: after an MB-enabled render the leftover tasks
// are the MB secondary set with negative fractional scales (-> "negative raw" images = -1/6 x input),
// and the leftover virtual-view grid loses the same border ROI on every sensor.
QuadCLT cond_phys = master_CLT.getGPUQuad().getQuadCLT(); // physical scene bound to the GPU
if (cond_phys != null) { // conditioned baseline: the actual image_data (bypass image_data_alt), same grid as raw
CuasMotion.perSensorFromData(clt_parameters, master_CLT, cond_phys.getOrigImageData(),
"-CUAS-PERSENSOR", ImageDtt.THREADS_MAX, debugLevel);
// By Claude on 07/02/2026: per-sensor linear (a + b*x) photometric fit over "safe" tiles
// (weak: no details, or far: small parallax), to become the new per-sensor scale/offset
// calibration: scale' = b*scale, additive map' = b*FPN - a. Prints the table and saves
// -CUAS-PERSENSOR-ADJ for visual verification; application to the stored calibration TBD.
float [][] cond_images = CuasMotion.perSensorImagesFromTD(master_CLT.getGPUQuad(), false); // TD still conditioned
double [][] ab = CuasMotion.perSensorLinearFit(
master_CLT, // QuadCLT center_CLT (combo DSI source, save target)
cond_images, // float [][] per_sensor (conditioned, FPN-corrected)
master_CLT.getGPUQuad().getImageWidth(), // int width
0.5, // double max_strength, // weak tiles: strength < 0.5
1.0, // double max_disparity, // far tiles: disparity < 1
true, // boolean save_adjusted (-CUAS-PERSENSOR-ADJ)
debugLevel, // int debugLevel
true); // boolean keep_averages: average offset = 0, average scale = 1.0
if (ab != null) { // recalculate the 16+16 lwir offsets/scales (like runPhotometric()/photoEach())
// By Claude on 07/02/2026: top-menu-saved/restored parameters are applied to master_CLT,
// so quadCLT_main is the durable carrier (main configuration file); the physical reference
// scene's -INTERFRAME.corr-xml gets the in-run copy (the -CENTER file only carries poses).
CuasMotion.applyLwirLinearCalibration(
cond_phys, // QuadCLT phys_scene (its offsets/scales conditioned the fit input)
new QuadCLT [] { // QuadCLT [] apply_to (in-memory)
master_CLT, // immediate use in this run
quadCLTs[ref_index], // physical reference scene (photometric owner)
quadCLT_main}, // next sequences + saved in the main config file
quadCLTs[ref_index], // QuadCLT save_scene -> <scene>-INTERFRAME.corr-xml
ab[0], // double [] a,
ab[1], // double [] b,
debugLevel); // int debugLevel
if (!clt_parameters.imp.curt_calib) { // conditioned baseline not yet rendered by the calibration step
QuadCLT cond_phys = master_CLT.getGPUQuad().getQuadCLT(); // physical scene bound to the GPU
if (cond_phys != null) { // the actual image_data (bypass image_data_alt), same grid as raw
CuasMotion.perSensorFromData(clt_parameters, master_CLT, cond_phys.getOrigImageData(),
"-CUAS-PERSENSOR", ImageDtt.THREADS_MAX, debugLevel);
}
}
// Raw /jp4/ baseline (no photometric/FPN/conditioning), saved as -CUAS-PERSENSOR-RAW for side-by-side compare.
// RT-seed: Java reads the source tiffs and force-uploads them H2D; later the compute moves to GPU (Java = oracle).
// By Claude on 07/01/2026
CuasMotion.perSensorFromRawJp4(clt_parameters, master_CLT, ImageDtt.THREADS_MAX, debugLevel);
} else {
cuasRangingRT.saveUasFlightLogCsv(uasLogReader, imp_targets); // UAS flight-log truth -> <name>-UAS_DATA.tsv (mode-0 only; needs QuadCLT pose). By Claude on 06/24/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