Commit c0096034 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: RT rowcol: untrusted-line guard (inlier fraction) + physical cap

ROWCOL-04 localization: RT rows regression (18.0 vs 10.6 pre-rework) is
CONTENT BIAS, zero in the sky and +10..22 counts in terrain rows - with
dense high-contrast content nearly every |UM residual| exceeds max_abs,
all pixels get the outlier weight and the weighted mean degenerates to
the row's content mean (the small weights cancel), stamping bias bands.
lineAverage now zeroes a line's correction when its inlier fraction is
below rowcol_min_inlier (default 0.33 - content-dominated lines cannot
yield a line-noise measurement) or when |estimate| > max_abs (line noise
cannot physically exceed the gate scale). Both guards are no-ops with
the gate off (max_abs INF - every pixel is an inlier).
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 8db6269b
......@@ -57,6 +57,7 @@ public class CuasConditioning {
public double rowcol_um_sigma = 10.0; // UM sigma for the profile MEASUREMENT (content suppression borrowed from the oracle CorrectionFPN path: gate works on residual counts); 0 = measure on the data as-is (pre-07/07 behavior) // By Claude on 07/07/2026
public double rowcol_max_abs = 50.0; // only average pixels with |UM residual| within this (outlier reject, calibrated counts - meaningful now that rowcol runs post-photometric on UM residuals); +INF = no gate (the old default: on raw absolute counts the gate was unusable) // By Claude on 07/07/2026
public double rowcol_weight_outlier= 0.01; // small weight for out-of-range pixels
public double rowcol_min_inlier = 0.33; // minimal INLIER (|UM residual| <= max_abs) pixel fraction of a line to trust its estimate; below it (content-dominated line - e.g. terrain rows, where the outlier-weighted mean degenerates to the CONTENT mean and stamps bias bands, measured +10..22 counts in ROWCOL-04 terrain) the line gets ZERO correction. Also |estimate| > max_abs -> 0 (physical cap: line noise cannot exceed the gate scale). // By Claude on 07/07/2026
public boolean photometric = true; // apply scale*(raw-offset) [+ scales2 quadratic]
public boolean apply_fpn = true; // subtract per-pixel FPN (the offset map's per-pixel part)
// Zero-DC (Andrey 2026-07-05, restoring a lost pre-CUAS invariant): the MCLT has no
......@@ -306,7 +307,8 @@ public class CuasConditioning {
// low frequencies at least as well as the sigma-8 profile HPF did. By Claude on 07/07/2026.
final boolean prof_hpf = cfg.rowcol_hpf && (cfg.rowcol_um_sigma <= 0);
// --- per-row profile (average along x for each row y) ---
double [] rowProf = lineAverage(meas, width, height, true, cfg.rowcol_max_abs, cfg.rowcol_weight_outlier);
double [] rowProf = lineAverage(meas, width, height, true, cfg.rowcol_max_abs, cfg.rowcol_weight_outlier,
cfg.rowcol_min_inlier);
if (prof_hpf) rowProf = highpass(rowProf, cfg.rowcol_hpf_sigma);
for (int y = 0; y < height; y++) {
final double sub = rowProf[y];
......@@ -317,7 +319,8 @@ public class CuasConditioning {
}
}
// --- per-col profile (average along y for each col x), recomputed on row-corrected data ---
double [] colProf = lineAverage(meas, width, height, false, cfg.rowcol_max_abs, cfg.rowcol_weight_outlier);
double [] colProf = lineAverage(meas, width, height, false, cfg.rowcol_max_abs, cfg.rowcol_weight_outlier,
cfg.rowcol_min_inlier);
if (prof_hpf) colProf = highpass(colProf, cfg.rowcol_hpf_sigma);
for (int y = 0; y < height; y++) {
final int row0 = y * width;
......@@ -326,28 +329,46 @@ public class CuasConditioning {
}
/**
* Outlier-weighted average producing a 1-D profile.
* Outlier-weighted average producing a 1-D profile, with the untrusted-line guard
* (By Claude on 07/07/2026, ROWCOL-04 finding): a CONTENT-DOMINATED line (inlier
* fraction below min_inlier - e.g. terrain rows where nearly every |UM residual|
* exceeds max_abs) makes the outlier-weighted mean degenerate to the content mean
* (the small weights cancel) and stamps bias bands into the image; such lines get 0
* (no correction). Also 0 when |estimate| > max_abs (line noise cannot physically
* exceed the gate scale). Guards inactive when max_abs is INFINITY (no gate - every
* pixel is an inlier).
* @param perRow true -> length-height profile, averaging along x (per-row offset);
* false -> length-width profile, averaging along y (per-col offset).
*/
private static double [] lineAverage(
final double [] px, final int width, final int height,
final boolean perRow, final double max_abs, final double weight_outlier) {
final boolean perRow, final double max_abs, final double weight_outlier,
final double min_inlier) {
final int n = perRow ? height : width;
final double [] sum = new double [n];
final double [] wgt = new double [n];
final int [] num = new int [n]; // finite samples
final int [] inl = new int [n]; // inliers (|d| <= max_abs)
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 double w = (Math.abs(d) <= max_abs) ? 1.0 : weight_outlier;
final int k = perRow ? y : x;
final boolean inlier = (Math.abs(d) <= max_abs);
final double w = inlier ? 1.0 : weight_outlier;
sum[k] += w * d;
wgt[k] += w;
num[k]++;
if (inlier) inl[k]++;
}
}
for (int k = 0; k < n; k++) sum[k] = (wgt[k] > 0.0) ? (sum[k] / wgt[k]) : 0.0;
for (int k = 0; k < n; k++) {
double v = (wgt[k] > 0.0) ? (sum[k] / wgt[k]) : 0.0;
if ((num[k] > 0) && (inl[k] < min_inlier * num[k])) v = 0.0; // content-dominated line
else if (Math.abs(v) > max_abs) v = 0.0; // physical cap (no-op when max_abs is INF)
sum[k] = v;
}
return sum;
}
......
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