Commit 947c199c authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: fix ROWCOL back-project baseline mismatch (v013-ROWCOL-01 junk)

The first calculateRowColBackProject mixed two baselines in one image:
content-excluded residual (~0, regression baseline) inside the
back-projection coverage, full raw-level image-FPN outside it. Whole
lines fell outside the max_abs gate and the outlier-weighted average
degenerated to the content mean: vectors reached ~2300 counts (legacy
~340) and stamped junk bands at the coverage-window margins into all
per-sensor images (v013-ROWCOL-01 MERGED-DBG slices 141/408 etc).

Now the two baselines never mix: masked UM (blur(value*mask)/blur(mask))
high-passes the residual within coverage only; NaN-aware line averages
use covered pixels only (getLineAvgNaN); lines with under 10% coverage
(ROWCOL_BP_MIN_COVER) fall back to the pure legacy UM+gating estimate
per line (mergeLineProfiles). Console reports the line-fallback share.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent b70a9f6b
...@@ -2057,16 +2057,24 @@ public class CorrectionFPN { ...@@ -2057,16 +2057,24 @@ public class CorrectionFPN {
/** /**
* Content-excluding variant of {@link #calculateRowCol} (roadmap RT step 2b, Andrey's * Content-excluding variant of {@link #calculateRowCol} (roadmap RT step 2b, Andrey's
* plan 07/07/2026): before the UM filter + line averaging, subtract the back-projected * plan 07/07/2026): before the line averaging, subtract the back-projected
* (pose-transformed into each sensor's frame, photometrically matched) virtual center * (pose-transformed into each sensor's frame, photometrically matched) virtual center
* from each scene, so real content no longer biases the row/column averages (the * from each scene, so real content no longer biases the row/column averages (the
* bright-spot -> dark row/col line artifact of the UM+gating-only method). Where the * bright-spot -> dark row/col line artifact of the UM+gating-only method).
* back-projection is undefined (NaN synth coverage) the plain (image - FPN) value is * COVERAGE HANDLING (fixed 07/07/2026 after the v013-ROWCOL-01 run): the content-excluded
* kept - graceful degradation to the legacy behavior there (the line averagers and the * residual (regression baseline ~0) and the raw (image - FPN, full raw baseline) live on
* UM blur are not NaN-safe). The scene loop is SERIAL: each back-projection renders the * DIFFERENT baselines, so pixels outside the back-projection coverage must be EXCLUDED
* center on the GPU (same single-scene {@link #backPropagate} the FPN calculation uses). * from the estimate, never mixed in (the first version substituted raw there: whole lines
* Slower - the borrowed/non-RT path only. By Claude on 07/07/2026. * fell outside the max_abs gate and the outlier-weighted average degenerated to the
* content mean - vectors reached ~2300 counts, junk bands at the coverage-window margins).
* Now: masked UM (blur of value*mask normalized by blur of mask) high-passes the residual
* within coverage only; NaN-aware line averages use covered pixels only; lines with less
* than {@link #ROWCOL_BP_MIN_COVER} coverage fall back to the LEGACY (UM+gating on
* image-FPN) value for that line. The scene loop is SERIAL: each back-projection renders
* the center on the GPU (same single-scene {@link #backPropagate} the FPN calculation
* uses). Slower - the borrowed/non-RT path only. By Claude on 07/07/2026.
*/ */
public static final double ROWCOL_BP_MIN_COVER = 0.1; // min covered fraction of a line to use the content-excluded estimate // By Claude on 07/07/2026
public static double [][][][] calculateRowColBackProject( public static double [][][][] calculateRowColBackProject(
CLTParameters clt_parameters, CLTParameters clt_parameters,
final QuadCLT center_CLT, final QuadCLT center_CLT,
...@@ -2105,7 +2113,10 @@ public class CorrectionFPN { ...@@ -2105,7 +2113,10 @@ public class CorrectionFPN {
final int ncolor = 0; final int ncolor = 0;
final double [][][] rows = new double[num_scenes][][]; final double [][][] rows = new double[num_scenes][][];
final double [][][] cols = new double[num_scenes][][]; final double [][][] cols = new double[num_scenes][][];
final AtomicLong num_excl = new AtomicLong(0); // pixels with the content estimate subtracted final AtomicLong num_excl = new AtomicLong(0); // pixels with the content estimate subtracted
final AtomicLong num_fb_lines = new AtomicLong(0); // lines that fell back to the legacy estimate
final int min_cover_rows = (int) Math.max(16, ROWCOL_BP_MIN_COVER*height); // per-x profile: height samples per line
final int min_cover_cols = (int) Math.max(16, ROWCOL_BP_MIN_COVER*width); // per-y profile: width samples per line
final Thread[] threads = ImageDtt.newThreadArray(); final Thread[] threads = ImageDtt.newThreadArray();
final AtomicInteger ai = new AtomicInteger(0); final AtomicInteger ai = new AtomicInteger(0);
for (int nScene = 0; nScene < num_scenes; nScene++) { // SERIAL over scenes - backPropagate uses the GPU for (int nScene = 0; nScene < num_scenes; nScene++) { // SERIAL over scenes - backPropagate uses the GPU
...@@ -2132,7 +2143,8 @@ public class CorrectionFPN { ...@@ -2132,7 +2143,8 @@ public class CorrectionFPN {
System.out.println("calculateRowColBackProject(): back-projection FAILED for scene "+ System.out.println("calculateRowColBackProject(): back-projection FAILED for scene "+
quadCLTs[nScene].getImageName()+" - no content exclusion for this scene"); quadCLTs[nScene].getImageName()+" - no content exclusion for this scene");
} }
final double [][][] data_chn = new double [num_sens][][]; final double [][][] data_legacy = new double [num_sens][][]; // UM(image-FPN): the legacy field
final double [][][] data_excl = new double [num_sens][][]; // masked-UM content-excluded residual, NaN outside coverage
final boolean sub_fpn = (fpn != null) && (quadCLTs[nScene].getAppliedFPN() != fpn); final boolean sub_fpn = (fpn != null) && (quadCLTs[nScene].getAppliedFPN() != fpn);
final int fScene = nScene; final int fScene = nScene;
ai.set(0); ai.set(0);
...@@ -2142,50 +2154,143 @@ public class CorrectionFPN { ...@@ -2142,50 +2154,143 @@ public class CorrectionFPN {
for (int nsens = ai.getAndIncrement(); nsens < num_sens; nsens = ai.getAndIncrement()) { for (int nsens = ai.getAndIncrement(); nsens < num_sens; nsens = ai.getAndIncrement()) {
double [] raw = quadCLTs[fScene].getImageData()[nsens][ncolor]; double [] raw = quadCLTs[fScene].getImageData()[nsens][ncolor];
double [] df = (diff != null) ? diff[nsens] : null; double [] df = (diff != null) ? diff[nsens] : null;
double [] img = new double [num_pix]; double [] base = new double [num_pix]; // image - FPN (raw baseline)
long ndef = 0;
for (int i = 0; i < num_pix; i++) { for (int i = 0; i < num_pix; i++) {
double d = raw[i]; base[i] = raw[i] - (sub_fpn ? fpn[nsens][ncolor][i] : 0.0);
if (sub_fpn) {
d -= fpn[nsens][ncolor][i];
}
if ((df != null) && !Double.isNaN(df[i])) {
d -= raw[i] - df[i]; // subtract the content estimate; keeps (raw - FPN) where undefined
ndef++;
}
img[i] = d;
} }
num_excl.addAndGet(ndef); double [] legacy = base.clone();
OpticalFlow.applyUMDouble( OpticalFlow.applyUMDouble(
img, // final double [] data, legacy, // final double [] data,
width, // final int width, width, // final int width,
um_sigma, // final double um_sigma, um_sigma, // final double um_sigma,
1.0); // final double um_weight) 1.0); // final double um_weight)
data_chn[nsens] = new double [][] {img}; double [] hp = null;
if (df != null) {
// content-excluded residual: base - (raw - diff) = regression residual
// (baseline ~0) WITHIN coverage, NaN outside - the two baselines must
// never mix (see the method javadoc)
final double [] vm = new double [num_pix]; // residual, 0 outside coverage
final double [] mk = new double [num_pix]; // coverage mask
final double [] resid = new double [num_pix];
long ndef = 0;
for (int i = 0; i < num_pix; i++) {
if (!Double.isNaN(df[i])) {
final double r = base[i] - (raw[i] - df[i]);
resid[i] = r;
vm[i] = r;
mk[i] = 1.0;
ndef++;
} else {
resid[i] = Double.NaN;
}
}
num_excl.addAndGet(ndef);
if (ndef > 0) {
// masked UM: highpass = resid - blur(resid*mask)/blur(mask), inside coverage only
final DoubleGaussianBlur gb = new DoubleGaussianBlur();
gb.blurDouble(vm, width, num_pix/width, um_sigma, um_sigma, 0.01);
gb.blurDouble(mk, width, num_pix/width, um_sigma, um_sigma, 0.01);
hp = new double [num_pix];
for (int i = 0; i < num_pix; i++) {
hp[i] = (!Double.isNaN(resid[i]) && (mk[i] > 1.0E-6)) ?
(resid[i] - vm[i]/mk[i]) : Double.NaN;
}
}
}
data_legacy[nsens] = new double [][] {legacy};
data_excl[nsens] = (hp != null) ? (new double [][] {hp}) : null;
} }
} }
}; };
} }
ImageDtt.startAndJoin(threads); ImageDtt.startAndJoin(threads);
rows[fScene] = getRowAvg( double [][] rows_legacy = getRowAvg(data_legacy, width, max_abs, weight_outlier);
data_chn, // final double [][][] image_data, double [][] cols_legacy = getColAvg(data_legacy, width, max_abs, weight_outlier);
width, // final int width, double [][][] rows_excl = getLineAvgNaN(data_excl, width, true, max_abs, weight_outlier);
max_abs, // final double max_abs, // only average within +/- max_abs double [][][] cols_excl = getLineAvgNaN(data_excl, width, false, max_abs, weight_outlier);
weight_outlier); // final double weight_outlier) rows[fScene] = mergeLineProfiles(rows_excl[0], rows_excl[1], rows_legacy, min_cover_rows, num_fb_lines);
cols[fScene] = getColAvg( cols[fScene] = mergeLineProfiles(cols_excl[0], cols_excl[1], cols_legacy, min_cover_cols, num_fb_lines);
data_chn, // final double [][][] image_data,
width, // final int width,
max_abs, // final double max_abs, // only average within +/- max_abs
weight_outlier); // final double weight_outlier)
} }
final long total_lines = (long) num_scenes*num_sens*(width+height);
System.out.println(String.format( System.out.println(String.format(
"calculateRowColBackProject(): content excluded on %.2f%% of pixels "+ "calculateRowColBackProject(): content excluded on %.2f%% of pixels; "+
"(%d of %d; elsewhere legacy image-FPN used)", "%.2f%% of lines (%d of %d) fell back to the legacy estimate (insufficient coverage)",
100.0*num_excl.get()/((double) num_scenes*num_sens*num_pix), 100.0*num_excl.get()/((double) num_scenes*num_sens*num_pix),
num_excl.get(), (long) num_scenes*num_sens*num_pix)); 100.0*num_fb_lines.get()/((double) total_lines), num_fb_lines.get(), total_lines));
return new double [][][][] {rows,cols}; return new double [][][][] {rows,cols};
} }
/**
* NaN-aware line average + finite-sample counts (the content-excluded residual is NaN
* outside the back-projection coverage). perRow==true: length-width profile (average
* over y for each x, matching {@link #getRowAvg}); false: length-height (matching
* {@link #getColAvg}). Undefined lines (no finite samples) - NaN.
* @return {avg[nchn][n], count[nchn][n]}; avg[nchn]==null where image_data[nchn]==null.
* By Claude on 07/07/2026.
*/
private static double [][][] getLineAvgNaN(
final double [][][] image_data, final int width,
final boolean perRow, final double max_abs, final double weight_outlier) {
final int num_sens = image_data.length;
int height0 = 0;
for (double [][] chn : image_data) if (chn != null) { height0 = chn[0].length/width; break; }
final int height = height0;
final int n = perRow ? width : height;
final double [][] avg = new double [num_sens][];
final double [][] cnt = new double [num_sens][];
for (int nchn = 0; nchn < num_sens; nchn++) if (image_data[nchn] != null) {
final double [] px = image_data[nchn][0];
final double [] sum = new double [n];
final double [] wgt = new double [n];
final double [] num = new double [n];
int idx = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final double d = px[idx++];
if (Double.isNaN(d)) continue;
final int k = perRow ? x : y;
final double w = (Math.abs(d) <= max_abs) ? 1.0 : weight_outlier;
sum[k] += w*d;
wgt[k] += w;
num[k] += 1.0;
}
}
for (int k = 0; k < n; k++) sum[k] = (wgt[k] > 0.0) ? (sum[k]/wgt[k]) : Double.NaN;
avg[nchn] = sum;
cnt[nchn] = num;
}
return new double [][][] {avg, cnt};
}
/**
* Per-line merge: use the content-excluded estimate where its coverage count is
* sufficient, the legacy estimate otherwise (never a mixed-baseline value).
* By Claude on 07/07/2026.
*/
private static double [][] mergeLineProfiles(
final double [][] avg_excl, // may have null channels (no back-projection for the sensor/scene)
final double [][] cnt_excl,
final double [][] legacy,
final int min_count,
final AtomicLong fallback_lines) {
final double [][] out = new double [legacy.length][];
for (int nchn = 0; nchn < legacy.length; nchn++) if (legacy[nchn] != null) {
out[nchn] = legacy[nchn].clone();
if ((avg_excl != null) && (avg_excl[nchn] != null)) {
for (int k = 0; k < out[nchn].length; k++) {
if ((cnt_excl[nchn][k] >= min_count) && !Double.isNaN(avg_excl[nchn][k])) {
out[nchn][k] = avg_excl[nchn][k];
} else {
fallback_lines.incrementAndGet();
}
}
} else {
fallback_lines.addAndGet(out[nchn].length);
}
}
return out;
}
public static ImagePlus debugFPN( public static ImagePlus debugFPN(
final QuadCLT [] quadCLTs, final QuadCLT [] quadCLTs,
......
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