Commit 8db6269b authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: ROWCOL frame-edge margin + physical line cap; RT: single high-pass (QC/ROWCOL-03 findings)

ROWCOL-03 QC FAIL (rms 52.9 unchanged by erosion) proved the junk edge
columns are FULLY covered with DEFINED-BUT-WRONG back-projection values
(left-edge convert_direct artifact class) - erosion cannot see them:
- exclude a 16px frame-edge band from the content-excluded estimator by
  geometry (ROWCOL_BP_EDGE_MARGIN); those lines take the legacy fallback;
- physical cap in the per-line merge: a content-excluded line value over
  max_abs cannot be line noise - use legacy (also blunts any future
  ROWCOL-01-class estimator failure).

RT conditioning: profile HPF now applies ONLY when rowcol_um_sigma==0.
Stacking it on the UM measurement double-high-passed the row-offset
estimate ((1-G10)(1-G8)): removed fraction at period ~20 rows dropped
96% -> 91%, residual mid-band line noise x2 - measured merged RT-RENDER
rows metric 10.60 (ROWCOL-02, pre-rework) -> 18.73 (ROWCOL-03). Single
sigma-10 high-pass removes strictly more noise at every period than the
old sigma-8-only path and protects the LoG band at least as well.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 66bd669a
...@@ -2082,6 +2082,7 @@ public class CorrectionFPN { ...@@ -2082,6 +2082,7 @@ public class CorrectionFPN {
*/ */
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 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 final double ROWCOL_BP_ERODE = 0.95; // coverage EROSION: keep only pixels whose blurred (sigma=um_sigma) mask >= this - the back-projection is unreliable near its own boundary (edge tiles/disparity conditioning), and boundary garbage dominated the partially-covered EDGE columns in v013-ROWCOL-02 (per-x maxima ~1000 in x<14, interior clean). 0.95 ~ 1.6*sigma ~ 16 px inside at sigma 10; excluded pixels push edge lines below MIN_COVER -> legacy fallback. // By Claude on 07/07/2026 public static final double ROWCOL_BP_ERODE = 0.95; // coverage EROSION: keep only pixels whose blurred (sigma=um_sigma) mask >= this - the back-projection is unreliable near its own boundary (edge tiles/disparity conditioning), and boundary garbage dominated the partially-covered EDGE columns in v013-ROWCOL-02 (per-x maxima ~1000 in x<14, interior clean). 0.95 ~ 1.6*sigma ~ 16 px inside at sigma 10; excluded pixels push edge lines below MIN_COVER -> legacy fallback. // By Claude on 07/07/2026
public static final int ROWCOL_BP_EDGE_MARGIN = 16; // frame-edge band treated as UNCOVERED for the content-excluded estimator: the QC FAIL on the erosion-only run (rms 52.9 unchanged) proved the edge columns are FULLY covered with DEFINED-BUT-WRONG render values (the known left-edge convert_direct artifact class) - erosion cannot see them, exclusion by geometry can. // 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,
...@@ -2178,12 +2179,19 @@ public class CorrectionFPN { ...@@ -2178,12 +2179,19 @@ public class CorrectionFPN {
// never mix (see the method javadoc) // never mix (see the method javadoc)
final double [] mk = new double [num_pix]; // coverage mask (blurred in place for erosion) final double [] mk = new double [num_pix]; // coverage mask (blurred in place for erosion)
final double [] resid = new double [num_pix]; final double [] resid = new double [num_pix];
for (int i = 0; i < num_pix; i++) { final int hgt = num_pix/width;
if (!Double.isNaN(df[i])) { final int em = ROWCOL_BP_EDGE_MARGIN;
resid[i] = base[i] - (raw[i] - df[i]); for (int y = 0; y < hgt; y++) {
mk[i] = 1.0; final boolean y_edge = (y < em) || (y >= hgt - em);
} else { for (int x = 0; x < width; x++) {
resid[i] = Double.NaN; final int i = y*width + x;
// frame-edge band: the render is defined but untrustworthy there
if (!Double.isNaN(df[i]) && !y_edge && (x >= em) && (x < width - em)) {
resid[i] = base[i] - (raw[i] - df[i]);
mk[i] = 1.0;
} else {
resid[i] = Double.NaN;
}
} }
} }
final DoubleGaussianBlur gb = new DoubleGaussianBlur(); final DoubleGaussianBlur gb = new DoubleGaussianBlur();
...@@ -2224,8 +2232,8 @@ public class CorrectionFPN { ...@@ -2224,8 +2232,8 @@ public class CorrectionFPN {
double [][] cols_legacy = getColAvg(data_legacy, width, max_abs, weight_outlier); double [][] cols_legacy = getColAvg(data_legacy, width, max_abs, weight_outlier);
double [][][] rows_excl = getLineAvgNaN(data_excl, width, true, max_abs, weight_outlier); double [][][] rows_excl = getLineAvgNaN(data_excl, width, true, max_abs, weight_outlier);
double [][][] cols_excl = getLineAvgNaN(data_excl, width, false, max_abs, weight_outlier); double [][][] cols_excl = getLineAvgNaN(data_excl, width, false, max_abs, weight_outlier);
rows[fScene] = mergeLineProfiles(rows_excl[0], rows_excl[1], rows_legacy, min_cover_rows, num_fb_lines); rows[fScene] = mergeLineProfiles(rows_excl[0], rows_excl[1], rows_legacy, min_cover_rows, max_abs, num_fb_lines);
cols[fScene] = mergeLineProfiles(cols_excl[0], cols_excl[1], cols_legacy, min_cover_cols, num_fb_lines); cols[fScene] = mergeLineProfiles(cols_excl[0], cols_excl[1], cols_legacy, min_cover_cols, max_abs, num_fb_lines);
} }
final long total_lines = (long) num_scenes*num_sens*(width+height); final long total_lines = (long) num_scenes*num_sens*(width+height);
System.out.println(String.format( System.out.println(String.format(
...@@ -2280,21 +2288,26 @@ public class CorrectionFPN { ...@@ -2280,21 +2288,26 @@ public class CorrectionFPN {
/** /**
* Per-line merge: use the content-excluded estimate where its coverage count is * Per-line merge: use the content-excluded estimate where its coverage count is
* sufficient, the legacy estimate otherwise (never a mixed-baseline value). * sufficient AND its magnitude is physical, the legacy estimate otherwise (never a
* By Claude on 07/07/2026. * mixed-baseline value). The PHYSICAL CAP (|value| <= max_abs): line noise cannot
* exceed the outlier-gate scale; a bigger estimate means content/garbage leaked into
* the estimator (the legitimate takeover case - legacy's bright-spot bias - is tens
* of counts, well under the cap). By Claude on 07/07/2026.
*/ */
private static double [][] mergeLineProfiles( private static double [][] mergeLineProfiles(
final double [][] avg_excl, // may have null channels (no back-projection for the sensor/scene) final double [][] avg_excl, // may have null channels (no back-projection for the sensor/scene)
final double [][] cnt_excl, final double [][] cnt_excl,
final double [][] legacy, final double [][] legacy,
final int min_count, final int min_count,
final double max_abs,
final AtomicLong fallback_lines) { final AtomicLong fallback_lines) {
final double [][] out = new double [legacy.length][]; final double [][] out = new double [legacy.length][];
for (int nchn = 0; nchn < legacy.length; nchn++) if (legacy[nchn] != null) { for (int nchn = 0; nchn < legacy.length; nchn++) if (legacy[nchn] != null) {
out[nchn] = legacy[nchn].clone(); out[nchn] = legacy[nchn].clone();
if ((avg_excl != null) && (avg_excl[nchn] != null)) { if ((avg_excl != null) && (avg_excl[nchn] != null)) {
for (int k = 0; k < out[nchn].length; k++) { for (int k = 0; k < out[nchn].length; k++) {
if ((cnt_excl[nchn][k] >= min_count) && !Double.isNaN(avg_excl[nchn][k])) { if ((cnt_excl[nchn][k] >= min_count) && !Double.isNaN(avg_excl[nchn][k])
&& (Math.abs(avg_excl[nchn][k]) <= max_abs)) {
out[nchn][k] = avg_excl[nchn][k]; out[nchn][k] = avg_excl[nchn][k];
} else { } else {
fallback_lines.incrementAndGet(); fallback_lines.incrementAndGet();
......
...@@ -52,7 +52,7 @@ public class CuasConditioning { ...@@ -52,7 +52,7 @@ public class CuasConditioning {
public static class Config { public static class Config {
public boolean rowcol_enable = true; // subtract per-row and per-col noise (runs AFTER photometric+FPN since 07/07/2026 - see condition()) public boolean rowcol_enable = true; // subtract per-row and per-col noise (runs AFTER photometric+FPN since 07/07/2026 - see condition())
public boolean rowcol_hpf = true; // high-pass the 1-D avg profile before subtracting public boolean rowcol_hpf = true; // high-pass the 1-D avg profile before subtracting - applies ONLY when rowcol_um_sigma==0 (the UM measurement already band-limits; stacking both under-removed mid-band lines x2, see subtractRowColSensor) // By Claude on 07/07/2026
public double rowcol_hpf_sigma = 8.0; // px; ~ the LoG scale (KR3-ish) - noise band the LoG would pass public double rowcol_hpf_sigma = 8.0; // px; ~ the LoG scale (KR3-ish) - noise band the LoG would pass
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_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_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
...@@ -298,9 +298,16 @@ public class CuasConditioning { ...@@ -298,9 +298,16 @@ public class CuasConditioning {
cfg.rowcol_um_sigma, cfg.rowcol_um_sigma, 0.01); cfg.rowcol_um_sigma, cfg.rowcol_um_sigma, 0.01);
for (int i = 0; i < meas.length; i++) meas[i] -= blurred[i]; for (int i = 0; i < meas.length; i++) meas[i] -= blurred[i];
} }
// Profile HPF only WITHOUT the UM (measured, v013-ROWCOL-03 vs -02): the UM already
// high-passes the measured profile by (1-G_um); stacking the profile HPF multiplies
// in (1-G_hpf) - at period ~20 rows the removed fraction dropped 96% -> 91%, i.e.
// residual mid-band line noise x2 (rows metric 10.6 -> 18.7). A single high-pass at
// the UM sigma removes strictly more noise at every period AND protects the LoG's
// 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) --- // --- 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);
if (cfg.rowcol_hpf) rowProf = highpass(rowProf, cfg.rowcol_hpf_sigma); if (prof_hpf) rowProf = highpass(rowProf, cfg.rowcol_hpf_sigma);
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
final double sub = rowProf[y]; final double sub = rowProf[y];
if (sub != 0.0) { if (sub != 0.0) {
...@@ -311,7 +318,7 @@ public class CuasConditioning { ...@@ -311,7 +318,7 @@ public class CuasConditioning {
} }
// --- per-col profile (average along y for each col x), recomputed on row-corrected data --- // --- 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);
if (cfg.rowcol_hpf) colProf = highpass(colProf, cfg.rowcol_hpf_sigma); if (prof_hpf) colProf = highpass(colProf, cfg.rowcol_hpf_sigma);
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
final int row0 = y * width; final int row0 = y * width;
for (int x = 0; x < width; x++) px[row0 + x] -= colProf[x]; for (int x = 0; x < width; x++) px[row0 + x] -= colProf[x];
......
...@@ -145,7 +145,7 @@ public class CuasRtParameters { ...@@ -145,7 +145,7 @@ public class CuasRtParameters {
gd.addCheckbox ("Subtract row/col noise (RT conditioning)", this.rowcol_en, // By Claude on 07/07/2026 gd.addCheckbox ("Subtract row/col noise (RT conditioning)", this.rowcol_en, // By Claude on 07/07/2026
"Subtract per-row/per-column line noise from each raw scene during RT conditioning (CuasConditioning). Turn OFF to bypass when the FLIR internal row/column correction is verified sufficient."); "Subtract per-row/per-column line noise from each raw scene during RT conditioning (CuasConditioning). Turn OFF to bypass when the FLIR internal row/column correction is verified sufficient.");
gd.addCheckbox ("Row/col high-pass profiles", this.rowcol_hpf, // By Claude on 07/07/2026 gd.addCheckbox ("Row/col high-pass profiles", this.rowcol_hpf, // By Claude on 07/07/2026
"High-pass the 1-D row/col average profiles before subtracting, preserving the low frequencies for the downstream LoG."); "High-pass the 1-D row/col average profiles before subtracting, preserving the low frequencies for the downstream LoG. Applies ONLY when the unsharp-mask sigma below is 0 - the UM measurement already band-limits, and stacking both under-removes mid-band line noise (measured x2).");
gd.addNumericField("Row/col high-pass sigma", this.rowcol_hpf_sigma, 3,7,"pix", // By Claude on 07/07/2026 gd.addNumericField("Row/col high-pass sigma", this.rowcol_hpf_sigma, 3,7,"pix", // By Claude on 07/07/2026
"Gaussian sigma of the profile high-pass, ~ the LoG scale (the noise band the LoG would pass)."); "Gaussian sigma of the profile high-pass, ~ the LoG scale (the noise band the LoG would pass).");
gd.addNumericField("Row/col unsharp mask sigma (0 - off)", this.rowcol_um_sigma, 3,7,"pix", // By Claude on 07/07/2026 gd.addNumericField("Row/col unsharp mask sigma (0 - off)", this.rowcol_um_sigma, 3,7,"pix", // By Claude on 07/07/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