Commit ebef0b23 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: per-sensor lwir photometric recalibration + JNA bayer-guard fix

curt_cond_test rework: both PERSENSOR stacks now converted with the test's
own uniform sensor-domain task grid (scale 1.0) instead of leftover GPU
state (MB secondary tasks with negative fractional scales made 'raw'
renders = -1/6 x input; leftover virtual-view grid lost the same border
ROI on every sensor). perSensorFromRawJp4 no longer overwrites the scene's
conditioned image_data.

GpuQuadJna.setBayerImages(force,center) restored the base-class skip-guard
via a native-side jna_bayer_set flag (gpuTileProcessor is null in JNA shell
instances): every execConvertDirect unconditionally re-pulled
quadCLT.getResetImageData(), silently clobbering explicit uploads - made
the raw baseline bit-identical to the conditioned render.

CuasMotion.perSensorLinearFit(): per-sensor a+b*x photometric fit over
safe tiles (weak strength<0.5 or far disparity<1 from -INTER-INTRA-LMA,
inner rect, 8x8 tile->pixel map) against the cross-sensor mean, gauge
keep_averages (mean offset 0, mean scale 1), 3-sigma outlier rejection.
Validated on 1773135476_186641: sensor-mean spread 1353->5 counts,
cross-sensor RMS 358->17 (inliers), b in 0.83..1.11.

CuasMotion.applyLwirLinearCalibration(): folds the fit into the 16+16
lwir offsets/scales (scale'=b*scale, offset'=offset-a/scale'), updates the
center instance + photometric_scene provenance, saves -INTERFRAME.corr-xml.
Applied the standard way at load they compensate the remaining per-sensor
mismatch of the raw /jp4/ tiffs.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 96b1c3d5
...@@ -73,16 +73,24 @@ public class GpuQuadJna extends GpuQuad { ...@@ -73,16 +73,24 @@ public class GpuQuadJna extends GpuQuad {
// (num_pairs / sel_pairs / color_weights / corr_out_rad) is known at first use. // (num_pairs / sel_pairs / color_weights / corr_out_rad) is known at first use.
} }
// By Claude on 07/02/2026: native-side equivalent of gpuTileProcessor.bayer_set (that field's owner is null
// here). Tracks whether the native bayer buffers hold the bound quadCLT's current data, so that
// execConvertDirect()'s internal setBayerImages(false,...) SKIPS the re-pull exactly like the JCuda base
// does. Without it every convert unconditionally re-uploaded quadCLT.getResetImageData(), silently
// clobbering any explicitly uploaded data (setBayerImages(data, true) / setBayerImage()) - found by the
// curt_cond_test raw baseline coming out bit-identical to the conditioned render.
private boolean jna_bayer_set = false;
// Switch this shared GpuQuad to a different scene's QuadCLT. Base also clears // Switch this shared GpuQuad to a different scene's QuadCLT. Base also clears
// gpuTileProcessor.bayer_set (null here) — N/A natively: bayer + geometry are re-uploaded on the next // gpuTileProcessor.bayer_set (null here) — mirrored with jna_bayer_set: the next convert re-uploads.
// exec, kernels are unchanged. resetGeometryCorrection*/() are flag-only (safe to call). // resetGeometryCorrection*/() are flag-only (safe to call).
@Override public void updateQuadCLT(final QuadCLT quadCLT) { @Override public void updateQuadCLT(final QuadCLT quadCLT) {
this.quadCLT = quadCLT; this.quadCLT = quadCLT;
resetGeometryCorrection(); resetGeometryCorrection();
resetGeometryCorrectionVector(); resetGeometryCorrectionVector();
jna_bayer_set = false; // By Claude on 07/02/2026: match base (scene switch invalidates uploaded bayer)
} }
// Native re-uploads bayer each convert_direct -> nothing to reset (base clears the null gpuTileProcessor flag). @Override public void resetBayer() { jna_bayer_set = false; } // By Claude on 07/02/2026: match base semantics
@Override public void resetBayer() { /* no-op for the native backend */ }
// CLT sizing (base derefs null gpu_clt_wh) — full-frame dims; matches TpProc's slice. // CLT sizing (base derefs null gpu_clt_wh) — full-frame dims; matches TpProc's slice.
@Override public int getCltSize(boolean use_ref) { @Override public int getCltSize(boolean use_ref) {
...@@ -194,6 +202,11 @@ public class GpuQuadJna extends GpuQuad { ...@@ -194,6 +202,11 @@ public class GpuQuadJna extends GpuQuad {
if (f != null) lib.tp_proc_set_center_image(proc, f); // broadcast to all sensors if (f != null) lib.tp_proc_set_center_image(proc, f); // broadcast to all sensors
return; return;
} }
// By Claude on 07/02/2026: same guard as the JCuda base - do NOT re-pull quadCLT data over an
// explicit upload when the native buffers are current and the quadCLT has no new data.
if (!force && jna_bayer_set && !quadCLT.hasNewImageData()) {
return;
}
setBayerImages(quadCLT.getResetImageData(), true); setBayerImages(quadCLT.getResetImageData(), true);
} }
@Override public void setBayerImages(double[][][] bayer_data, boolean force) { @Override public void setBayerImages(double[][][] bayer_data, boolean force) {
...@@ -201,6 +214,7 @@ public class GpuQuadJna extends GpuQuad { ...@@ -201,6 +214,7 @@ public class GpuQuadJna extends GpuQuad {
float[] f = combineChannels(bayer_data[ncam]); float[] f = combineChannels(bayer_data[ncam]);
if (f != null) lib.tp_proc_set_image(proc, ncam, f); if (f != null) lib.tp_proc_set_image(proc, ncam, f);
} }
jna_bayer_set = true; // By Claude on 07/02/2026: match base (gpuTileProcessor.bayer_set = true)
} }
// sum the (1 or 3) split-color channels into one image, as GpuQuad.setBayerImages does // sum the (1 or 3) split-color channels into one image, as GpuQuad.setBayerImages does
private static float[] combineChannels(double[][] chans) { private static float[] combineChannels(double[][] chans) {
......
...@@ -7281,11 +7281,42 @@ java.lang.NullPointerException ...@@ -7281,11 +7281,42 @@ java.lang.NullPointerException
// and compare. Well-calibrated -> the 16 per-sensor averages match (spread ~0). Runs instead of RT detection. // and compare. Well-calibrated -> the 16 per-sensor averages match (spread ~0). Runs instead of RT detection.
// By Claude on 07/01/2026 // By Claude on 07/01/2026
System.out.println("===== CUAS RT conditioning test (curt_cond_test): per-sensor average spread ====="); System.out.println("===== CUAS RT conditioning test (curt_cond_test): per-sensor average spread =====");
CuasMotion.perSensorAveragesFromTD(master_CLT.getGPUQuad(), false, "-CUAS-PERSENSOR", master_CLT); // conditioned TD, saved to the -CENTER instance // 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, save to the center corr-xml
CuasMotion.applyLwirLinearCalibration( // By Claude on 07/02/2026
master_CLT, // QuadCLT center_CLT (calibration carrier, corr-xml save)
cond_phys, // QuadCLT phys_scene (its offsets/scales conditioned the fit input)
ab[0], // double [] a,
ab[1], // double [] b,
true, // boolean save_corr_xml
debugLevel); // int debugLevel
}
}
// Raw /jp4/ baseline (no photometric/FPN/conditioning), saved as -CUAS-PERSENSOR-RAW for side-by-side compare. // 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). // 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 // By Claude on 07/01/2026
CuasMotion.perSensorFromRawJp4(master_CLT, ImageDtt.THREADS_MAX, debugLevel); CuasMotion.perSensorFromRawJp4(clt_parameters, master_CLT, ImageDtt.THREADS_MAX, debugLevel);
} else { } 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 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
new CuasDetectRT( new CuasDetectRT(
......
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