Commit 275f812b authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: ROWCOL edge erosion + RT rowcol rework (order/UM/gate) + QC harness

1. calculateRowColBackProject: ERODE the coverage mask (blurred mask >=
   ROWCOL_BP_ERODE=0.95 ~ 16px inside at sigma 10) before the masked UM
   and averaging - the back-projection is unreliable near its own
   boundary and boundary garbage dominated the partially-covered EDGE
   columns in v013-ROWCOL-02 (per-x maxima ~1000 for x<14, interior
   clean at 13-39 counts). Excluded edge lines drop below MIN_COVER and
   take the legacy per-line fallback.

2. RT conditioning row/col (problem 2, borrowed from the oracle path,
   /jp4/ input unchanged):
   - REORDER: row/col now AFTER photometric+FPN. Measuring on raw
     captured the FPN's static line structure (rms ~6/12 counts) which
     the later FPN subtraction removed AGAIN - negative FPN line
     structure was injected into the output.
   - UM before averaging (new curt.rowcol_um_sigma, default 10, 0=off):
     profiles measured on an unsharp-masked copy, subtracted from the
     original - content no longer biases the averages and the max_abs
     gate acts on residual counts.
   - rowcol_max_abs default 50 (calibrated counts; configs saved before
     this change carry 0 = no gate - set 50 to enable).

3. CuasQC (Andrey's regression-protection design): per-product health
   statistics with sequence-independent pass bands, appended per run to
   <center>-QC.json; FAIL prints loudly, never aborts. Increment 1:
   ROWCOL vectors (rms<=25, scene-max p95<=500 - both of today's
   defects would have failed) + FPN (rms, row/col line-structure
   energy), hooked after calculation AND reuse in CorrectionFPN.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 947c199c
...@@ -356,6 +356,9 @@ public class CorrectionFPN { ...@@ -356,6 +356,9 @@ public class CorrectionFPN {
System.out.println("Reusing FPN."); System.out.println("Reusing FPN.");
} }
} }
CuasQC.checkFpn( // health bands + <center>-QC.json entry (regression protection) // By Claude on 07/07/2026
center_CLT.getX3dDirectory(), center_CLT.getImageName(),
fpn, fpn_width, created_fpn);
// center_CLT.setImageData(fpn); // included in center_CLT.setApplyFPN(). // setting FPN images to the virtual (center) scene // center_CLT.setImageData(fpn); // included in center_CLT.setApplyFPN(). // setting FPN images to the virtual (center) scene
if (created_fpn || show_fpn) { if (created_fpn || show_fpn) {
center_CLT.getCorrectionFPN().saveShowFPN( center_CLT.getCorrectionFPN().saveShowFPN(
...@@ -435,6 +438,9 @@ public class CorrectionFPN { ...@@ -435,6 +438,9 @@ public class CorrectionFPN {
cuas_outliers_rowcol); // final double weight_outlier // = 0.001;) cuas_outliers_rowcol); // final double weight_outlier // = 0.001;)
is_new = (row_col != null); is_new = (row_col != null);
} }
CuasQC.checkRowCol( // health bands + <center>-QC.json entry (regression protection) // By Claude on 07/07/2026
center_CLT.getX3dDirectory(), center_CLT.getImageName(),
row_col, is_new);
center_CLT.getCorrectionFPN().saveShowRowCol( center_CLT.getCorrectionFPN().saveShowRowCol(
row_col[0], // final double [][][] rows, // [scene][sensor][width] row_col[0], // final double [][][] rows, // [scene][sensor][width]
row_col[1], // final double [][][] cols, // [scene][sensor][height] row_col[1], // final double [][][] cols, // [scene][sensor][height]
...@@ -2075,6 +2081,7 @@ public class CorrectionFPN { ...@@ -2075,6 +2081,7 @@ public class CorrectionFPN {
* uses). Slower - the borrowed/non-RT path only. By Claude on 07/07/2026. * 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 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 double [][][][] calculateRowColBackProject( public static double [][][][] calculateRowColBackProject(
CLTParameters clt_parameters, CLTParameters clt_parameters,
final QuadCLT center_CLT, final QuadCLT center_CLT,
...@@ -2169,31 +2176,40 @@ public class CorrectionFPN { ...@@ -2169,31 +2176,40 @@ public class CorrectionFPN {
// content-excluded residual: base - (raw - diff) = regression residual // content-excluded residual: base - (raw - diff) = regression residual
// (baseline ~0) WITHIN coverage, NaN outside - the two baselines must // (baseline ~0) WITHIN coverage, NaN outside - the two baselines must
// never mix (see the method javadoc) // 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 (blurred in place for erosion)
final double [] mk = new double [num_pix]; // coverage mask
final double [] resid = new double [num_pix]; final double [] resid = new double [num_pix];
long ndef = 0;
for (int i = 0; i < num_pix; i++) { for (int i = 0; i < num_pix; i++) {
if (!Double.isNaN(df[i])) { if (!Double.isNaN(df[i])) {
final double r = base[i] - (raw[i] - df[i]); resid[i] = base[i] - (raw[i] - df[i]);
resid[i] = r;
vm[i] = r;
mk[i] = 1.0; mk[i] = 1.0;
ndef++;
} else { } else {
resid[i] = Double.NaN; resid[i] = Double.NaN;
} }
} }
final DoubleGaussianBlur gb = new DoubleGaussianBlur();
gb.blurDouble(mk, width, num_pix/width, um_sigma, um_sigma, 0.01); // -> erosion measure
// ERODED support: only well-inside coverage (see ROWCOL_BP_ERODE)
final double [] vm2 = new double [num_pix];
final double [] mk2 = new double [num_pix];
long ndef = 0;
for (int i = 0; i < num_pix; i++) {
if (!Double.isNaN(resid[i]) && (mk[i] >= ROWCOL_BP_ERODE)) {
vm2[i] = resid[i];
mk2[i] = 1.0;
ndef++;
} else {
resid[i] = Double.NaN; // boundary band: exclude from UM support AND averages
}
}
num_excl.addAndGet(ndef); num_excl.addAndGet(ndef);
if (ndef > 0) { if (ndef > 0) {
// masked UM: highpass = resid - blur(resid*mask)/blur(mask), inside coverage only // masked UM: highpass = resid - blur(resid*mask)/blur(mask), inside eroded coverage only
final DoubleGaussianBlur gb = new DoubleGaussianBlur(); gb.blurDouble(vm2, width, num_pix/width, um_sigma, um_sigma, 0.01);
gb.blurDouble(vm, width, num_pix/width, um_sigma, um_sigma, 0.01); gb.blurDouble(mk2, 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]; hp = new double [num_pix];
for (int i = 0; i < num_pix; i++) { for (int i = 0; i < num_pix; i++) {
hp[i] = (!Double.isNaN(resid[i]) && (mk[i] > 1.0E-6)) ? hp[i] = (!Double.isNaN(resid[i]) && (mk2[i] > 1.0E-6)) ?
(resid[i] - vm[i]/mk[i]) : Double.NaN; (resid[i] - vm2[i]/mk2[i]) : Double.NaN;
} }
} }
} }
......
/**
**
** CuasQC.java - per-product quality metrics with pass bands (regression protection)
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** CuasQC.java is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
**
*/
package com.elphel.imagej.cuas;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Quality-metrics harness (Andrey's design 07/07/2026, after the v013-ROWCOL-01 junk):
* once a pipeline step is "done and verified", detect regressions by per-product HEALTH
* STATISTICS with pass bands - NOT bit comparison (a photometric recalibration shifts
* values a little; these invariants survive it and transfer across image sequences).
* Bands are sequence-independent (defined here in code, in physical/calibrated units for
* this LWIR16 hardware class); per-sequence VALUES land in <center>-QC.json next to the
* products (append-per-run, with timestamps), so any run answers "was it healthy" and
* trends are visible. A FAIL prints loudly but never aborts the run.
*
* Increment 1 (07/07/2026): ROWCOL vectors + FPN. The ROWCOL bands would have caught both
* of today's defects (baseline-mix rms 83, edge-column p95 ~1000 vs legacy 8.5/338).
* By Claude on 07/07/2026, from Andrey's design.
*/
public class CuasQC {
public static final int QC_VERSION = 1;
public static final String SUFFIX = "-QC.json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
// --- pass bands (sequence-independent, LWIR16 class; reference measurements in comments) ---
public static final double ROWCOL_MAX_RMS = 25.0; // legacy method 8.5; broken baseline-mix 83; broken edges 53
public static final double ROWCOL_MAX_SCENE_P95 = 500.0; // p95 of per-scene max |vector|; legacy 338 (own x=2 edge artifact included)
public static final double FPN_MAX_RMS = 150.0; // measured 40.5 (2026-07-07 vintage, rot 164.9)
public static final double FPN_MAX_LINE_ROW = 25.0; // FPN line-structure rms, per-row means; measured 6.4
public static final double FPN_MAX_LINE_COL = 25.0; // per-col means; measured 12.3
public static class Check {
public String name;
public double value;
public double max;
public boolean pass;
public Check(String name, double value, double max) {
this.name = name; this.value = value; this.max = max; this.pass = (value <= max);
}
}
public static class Entry {
public String saved; // timestamp of this QC evaluation
public String product; // "ROWCOL", "FPN", ...
public boolean calculated; // true - just calculated this run; false - reused from file
public boolean pass;
public LinkedHashMap<String, Double> metrics;
public ArrayList<Check> checks;
}
public static class QcFile {
public int qc_version = QC_VERSION;
public ArrayList<Entry> entries = new ArrayList<Entry>();
}
/** ROWCOL health: vector magnitudes must stay at the line-noise scale.
* @param row_col {rows[scene][sensor][width], cols[scene][sensor][height]} */
public static boolean checkRowCol(
String x3d_directory,
String center_name,
double [][][][] row_col,
boolean calculated) {
double sum2 = 0.0; long n = 0;
final int num_scenes = row_col[0].length;
final double [] scene_max = new double [num_scenes];
for (int f = 0; f < row_col.length; f++) {
for (int nscene = 0; nscene < num_scenes; nscene++) if (row_col[f][nscene] != null) {
for (double [] vec : row_col[f][nscene]) if (vec != null) {
for (double v : vec) if (!Double.isNaN(v)) {
sum2 += v*v; n++;
final double a = Math.abs(v);
if (a > scene_max[nscene]) scene_max[nscene] = a;
}
}
}
}
final double rms = Math.sqrt(sum2/Math.max(n,1));
final LinkedHashMap<String,Double> metrics = new LinkedHashMap<String,Double>();
metrics.put("rms", rms);
metrics.put("scene_max_med", percentile(scene_max, 50));
metrics.put("scene_max_p95", percentile(scene_max, 95));
metrics.put("scene_max_max", percentile(scene_max, 100));
final ArrayList<Check> checks = new ArrayList<Check>();
checks.add(new Check("rms", rms, ROWCOL_MAX_RMS));
checks.add(new Check("scene_max_p95", metrics.get("scene_max_p95"), ROWCOL_MAX_SCENE_P95));
return report(x3d_directory, center_name, "ROWCOL", calculated, metrics, checks);
}
/** FPN health: overall scale + row/col line-structure energy (per-sensor line means
* after removing each sensor's global mean, rms pooled over sensors). */
public static boolean checkFpn(
String x3d_directory,
String center_name,
double [][][] fpn,
int width,
boolean calculated) {
double sum2 = 0.0; long n = 0;
double line_row2 = 0.0, line_col2 = 0.0; long nrow = 0, ncol = 0;
for (int nsens = 0; nsens < fpn.length; nsens++) if ((fpn[nsens] != null) && (fpn[nsens][0] != null)) {
final double [] px = fpn[nsens][0];
final int height = px.length/width;
double mean = 0.0; long nm = 0;
for (double v : px) if (!Double.isNaN(v)) { mean += v; nm++; }
mean /= Math.max(nm,1);
final double [] rowm = new double [height];
final double [] coln = new double [width];
final double [] rown = new double [height];
final double [] colm = new double [width];
int idx = 0;
for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) {
final double v = px[idx++];
if (Double.isNaN(v)) continue;
final double d = v - mean;
sum2 += d*d; n++;
rowm[y] += d; rown[y]++;
colm[x] += d; coln[x]++;
}
for (int y = 0; y < height; y++) if (rown[y] > 0) { final double d = rowm[y]/rown[y]; line_row2 += d*d; nrow++; }
for (int x = 0; x < width; x++) if (coln[x] > 0) { final double d = colm[x]/coln[x]; line_col2 += d*d; ncol++; }
}
final LinkedHashMap<String,Double> metrics = new LinkedHashMap<String,Double>();
metrics.put("rms", Math.sqrt(sum2/Math.max(n,1)));
metrics.put("line_row", Math.sqrt(line_row2/Math.max(nrow,1)));
metrics.put("line_col", Math.sqrt(line_col2/Math.max(ncol,1)));
final ArrayList<Check> checks = new ArrayList<Check>();
checks.add(new Check("rms", metrics.get("rms"), FPN_MAX_RMS));
checks.add(new Check("line_row", metrics.get("line_row"), FPN_MAX_LINE_ROW));
checks.add(new Check("line_col", metrics.get("line_col"), FPN_MAX_LINE_COL));
return report(x3d_directory, center_name, "FPN", calculated, metrics, checks);
}
private static double percentile(double [] a, double pct) {
final double [] s = a.clone();
Arrays.sort(s);
final int k = (int) Math.min(s.length-1, Math.round(0.01*pct*(s.length-1)));
return s[k];
}
/** Print the QC block, append the entry to <center>-QC.json, return overall pass. */
private static boolean report(
String x3d_directory, String center_name, String product, boolean calculated,
LinkedHashMap<String,Double> metrics, ArrayList<Check> checks) {
final Entry entry = new Entry();
entry.saved = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
entry.product = product;
entry.calculated = calculated;
entry.metrics = metrics;
entry.checks = checks;
entry.pass = true;
for (Check c : checks) entry.pass &= c.pass;
final StringBuilder sb = new StringBuilder();
sb.append("QC: ").append(product).append(calculated ? " (calculated)" : " (reused)")
.append(entry.pass ? " PASS" : " *** FAIL ***");
for (Check c : checks) if (!c.pass) {
sb.append(String.format(" [%s=%.2f > %.2f]", c.name, c.value, c.max));
}
sb.append(" metrics: ");
for (java.util.Map.Entry<String,Double> m : metrics.entrySet()) {
sb.append(String.format("%s=%.2f ", m.getKey(), m.getValue()));
}
System.out.println(sb.toString());
if (!entry.pass) {
System.out.println("QC: *** "+product+" is OUT OF ITS HEALTH BANDS - the product is suspect, "+
"inspect before trusting this run (bands in CuasQC.java) ***");
}
try {
final String path = x3d_directory + java.io.File.separator + center_name + SUFFIX;
QcFile qc = null;
if (Files.exists(Paths.get(path))) {
try (FileReader fr = new FileReader(path)) {
qc = GSON.fromJson(fr, QcFile.class);
} catch (Exception e) {
System.out.println("QC: could not parse existing "+path+" ("+e.getMessage()+") - starting a new file");
}
}
if ((qc == null) || (qc.entries == null)) qc = new QcFile();
qc.entries.add(entry);
try (FileWriter fw = new FileWriter(path)) {
GSON.toJson(qc, fw);
}
} catch (IOException e) {
System.out.println("QC: failed to save QC json: "+e.getMessage());
}
return entry.pass;
}
}
...@@ -23,6 +23,7 @@ package com.elphel.imagej.cuas.rt; ...@@ -23,6 +23,7 @@ package com.elphel.imagej.cuas.rt;
** **
*/ */
import com.elphel.imagej.common.DoubleGaussianBlur;
import com.elphel.imagej.cuas.CuasMotion; import com.elphel.imagej.cuas.CuasMotion;
import com.elphel.imagej.gpu.GpuQuad; import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.tileprocessor.QuadCLT; import com.elphel.imagej.tileprocessor.QuadCLT;
...@@ -50,10 +51,11 @@ import com.elphel.imagej.tileprocessor.QuadCLT; ...@@ -50,10 +51,11 @@ import com.elphel.imagej.tileprocessor.QuadCLT;
public class CuasConditioning { public class CuasConditioning {
public static class Config { public static class Config {
public boolean rowcol_enable = true; // subtract per-row and per-col noise 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
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_max_abs = Double.POSITIVE_INFINITY; // clip |pixel| for the average (outlier reject) 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_weight_outlier= 0.01; // small weight for out-of-range pixels
public boolean photometric = true; // apply scale*(raw-offset) [+ scales2 quadratic] 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) public boolean apply_fpn = true; // subtract per-pixel FPN (the offset map's per-pixel part)
...@@ -82,6 +84,7 @@ public class CuasConditioning { ...@@ -82,6 +84,7 @@ public class CuasConditioning {
cfg.rowcol_enable = curt.rowcol_en; cfg.rowcol_enable = curt.rowcol_en;
cfg.rowcol_hpf = curt.rowcol_hpf; cfg.rowcol_hpf = curt.rowcol_hpf;
cfg.rowcol_hpf_sigma = curt.rowcol_hpf_sigma; cfg.rowcol_hpf_sigma = curt.rowcol_hpf_sigma;
cfg.rowcol_um_sigma = curt.rowcol_um_sigma; // By Claude on 07/07/2026
cfg.rowcol_max_abs = (curt.rowcol_max_abs > 0) ? curt.rowcol_max_abs : Double.POSITIVE_INFINITY; cfg.rowcol_max_abs = (curt.rowcol_max_abs > 0) ? curt.rowcol_max_abs : Double.POSITIVE_INFINITY;
cfg.rowcol_weight_outlier = curt.rowcol_outlier; cfg.rowcol_weight_outlier = curt.rowcol_outlier;
return cfg; return cfg;
...@@ -109,9 +112,6 @@ public class CuasConditioning { ...@@ -109,9 +112,6 @@ public class CuasConditioning {
final int num_sens = image_data.length; final int num_sens = image_data.length;
for (int s = 0; s < num_sens; s++) if (image_data[s] != null && image_data[s][0] != null) { for (int s = 0; s < num_sens; s++) if (image_data[s] != null && image_data[s][0] != null) {
final double [] px = image_data[s][0]; final double [] px = image_data[s][0];
if (cfg.rowcol_enable) {
subtractRowColSensor(px, width, cfg);
}
if (cfg.photometric || cfg.apply_fpn) { if (cfg.photometric || cfg.apply_fpn) {
final double B0 = (scales != null) ? scales [s] : 1.0; final double B0 = (scales != null) ? scales [s] : 1.0;
final double C0 = (offsets != null) ? offsets[s] : 0.0; final double C0 = (offsets != null) ? offsets[s] : 0.0;
...@@ -127,6 +127,15 @@ public class CuasConditioning { ...@@ -127,6 +127,15 @@ public class CuasConditioning {
px[i] = p; px[i] = p;
} }
} }
// Row/Col LAST (reordered 07/07/2026, oracle semantics "on top of the FPN"): the
// FPN carries strong STATIC line structure (measured rms ~6/12 counts row/col);
// measuring row/col on RAW pixels captured it (scaled exactly through photometric)
// and the later FPN subtraction removed it AGAIN - negative FPN line structure was
// injected into the output. Post-FPN measurement sees only the per-frame varying
// line noise, in calibrated counts (the max_abs gate units). By Claude on 07/07/2026.
if (cfg.rowcol_enable) {
subtractRowColSensor(px, width, cfg);
}
} }
// Zero-DC: one common constant subtracted from ALL sensors (see Config docs). // Zero-DC: one common constant subtracted from ALL sensors (see Config docs).
// AUTO level = MEDIAN OF THE TOP HALF of the frame (y < height/2), pooled over all // AUTO level = MEDIAN OF THE TOP HALF of the frame (y < height/2), pooled over all
...@@ -274,21 +283,34 @@ public class CuasConditioning { ...@@ -274,21 +283,34 @@ public class CuasConditioning {
return true; return true;
} }
/** Row/Col denoise for one sensor's flat pixel array (in place): subtract per-row, then per-col. */ /** Row/Col denoise for one sensor's flat pixel array (in place): subtract per-row, then per-col.
* Profiles are MEASURED on an unsharp-masked copy when rowcol_um_sigma > 0 (content
* suppression borrowed from the oracle CorrectionFPN path - the max_abs gate then works
* on residual counts instead of full-scale values), and SUBTRACTED from the original.
* By Claude on 07/07/2026 (was: measured on the data as-is). */
public static void subtractRowColSensor(final double [] px, final int width, final Config cfg) { public static void subtractRowColSensor(final double [] px, final int width, final Config cfg) {
final int height = px.length / width; final int height = px.length / width;
double [] meas = px; // measurement copy (UM'd); profiles subtract from px
if (cfg.rowcol_um_sigma > 0) {
meas = px.clone();
final double [] blurred = px.clone();
new DoubleGaussianBlur().blurDouble(blurred, width, height,
cfg.rowcol_um_sigma, cfg.rowcol_um_sigma, 0.01);
for (int i = 0; i < meas.length; i++) meas[i] -= blurred[i];
}
// --- per-row profile (average along x for each row y) --- // --- per-row profile (average along x for each row y) ---
double [] rowProf = lineAverage(px, 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 (cfg.rowcol_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) {
final int row0 = y * width; final int row0 = y * width;
for (int x = 0; x < width; x++) px[row0 + x] -= sub; for (int x = 0; x < width; x++) px[row0 + x] -= sub;
if (meas != px) for (int x = 0; x < width; x++) meas[row0 + x] -= sub; // keep the measurement copy row-corrected
} }
} }
// --- 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(px, 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 (cfg.rowcol_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;
......
...@@ -38,7 +38,8 @@ public class CuasRtParameters { ...@@ -38,7 +38,8 @@ public class CuasRtParameters {
public boolean rowcol_en = true; // RT conditioning: subtract per-row/per-column line noise from each raw scene (CuasConditioning). OPTIONAL/bypassable: turn OFF when the FLIR internal row/col correction is verified sufficient (indoor camera test planned) - roadmap RT step 2a (Andrey 07/07/2026). // By Claude on 07/07/2026 public boolean rowcol_en = true; // RT conditioning: subtract per-row/per-column line noise from each raw scene (CuasConditioning). OPTIONAL/bypassable: turn OFF when the FLIR internal row/col correction is verified sufficient (indoor camera test planned) - roadmap RT step 2a (Andrey 07/07/2026). // By Claude on 07/07/2026
public boolean rowcol_hpf = true; // RT conditioning: high-pass the 1-D row/col average profiles before subtracting (preserve low frequencies for the downstream LoG). // By Claude on 07/07/2026 public boolean rowcol_hpf = true; // RT conditioning: high-pass the 1-D row/col average profiles before subtracting (preserve low frequencies for the downstream LoG). // By Claude on 07/07/2026
public double rowcol_hpf_sigma = 8.0; // RT conditioning: Gaussian sigma (px) of the profile high-pass, ~ the LoG scale (the noise band the LoG would pass). // By Claude on 07/07/2026 public double rowcol_hpf_sigma = 8.0; // RT conditioning: Gaussian sigma (px) of the profile high-pass, ~ the LoG scale (the noise band the LoG would pass). // By Claude on 07/07/2026
public double rowcol_max_abs = 0.0; // RT conditioning: only average pixels with |value| within this limit (outlier reject, raw pre-photometric counts); 0 = no limit (matches the previous hardcoded INFINITY - gate OFF). // By Claude on 07/07/2026 public double rowcol_um_sigma = 10.0; // RT conditioning: unsharp-mask sigma for the row/col profile MEASUREMENT (content suppression borrowed from the oracle path; the max_abs gate then acts 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; // RT conditioning: only average pixels with |UM residual| within this limit (outlier reject, calibrated counts - rowcol now runs AFTER photometric+FPN); 0 = no limit. NOTE: configs saved before 07/07/2026 evening carry 0 (the gate was unusable on raw counts then) - set 50 to enable. // By Claude on 07/07/2026
public double rowcol_outlier = 0.01; // RT conditioning: weight of out-of-range pixels in the line averages (prevents undefined values on all-outlier lines). // By Claude on 07/07/2026 public double rowcol_outlier = 0.01; // RT conditioning: weight of out-of-range pixels in the line averages (prevents undefined values on all-outlier lines). // By Claude on 07/07/2026
public double psf_radius = 1.0; // sensor PSF radius for LoG pre-filter public double psf_radius = 1.0; // sensor PSF radius for LoG pre-filter
public double n_sigma = 4.0; // cutoff LoG kernel array, number of sigmas public double n_sigma = 4.0; // cutoff LoG kernel array, number of sigmas
...@@ -145,8 +146,10 @@ public class CuasRtParameters { ...@@ -145,8 +146,10 @@ public class CuasRtParameters {
"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.");
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
"Measure the row/col profiles on an unsharp-masked copy (content suppression, borrowed from the oracle path) - the gate below then acts on residual counts. 0 = measure on the data as-is.");
gd.addNumericField("Row/col maximal |value| (0 - no limit)", this.rowcol_max_abs, 3,9,"counts", // By Claude on 07/07/2026 gd.addNumericField("Row/col maximal |value| (0 - no limit)", this.rowcol_max_abs, 3,9,"counts", // By Claude on 07/07/2026
"Only average pixels with |value| within this limit (outlier reject, raw pre-photometric counts). 0 = no limit (previous hardcoded behavior)."); "Only average pixels with |UM residual| within this limit (outlier reject, calibrated counts - row/col runs after photometric+FPN). 0 = no limit. Configs saved before 07/07 evening carry 0 - set 50 to enable.");
gd.addNumericField("Row/col outlier weight", this.rowcol_outlier, 5,7,"x", // By Claude on 07/07/2026 gd.addNumericField("Row/col outlier weight", this.rowcol_outlier, 5,7,"x", // By Claude on 07/07/2026
"Weight of out-of-range pixels in the line averages (prevents undefined values on all-outlier lines)."); "Weight of out-of-range pixels in the line averages (prevents undefined values on all-outlier lines).");
...@@ -295,6 +298,7 @@ public class CuasRtParameters { ...@@ -295,6 +298,7 @@ public class CuasRtParameters {
this.rowcol_en = gd.getNextBoolean(); // By Claude on 07/07/2026 this.rowcol_en = gd.getNextBoolean(); // By Claude on 07/07/2026
this.rowcol_hpf = gd.getNextBoolean(); // By Claude on 07/07/2026 this.rowcol_hpf = gd.getNextBoolean(); // By Claude on 07/07/2026
this.rowcol_hpf_sigma = gd.getNextNumber(); // By Claude on 07/07/2026 this.rowcol_hpf_sigma = gd.getNextNumber(); // By Claude on 07/07/2026
this.rowcol_um_sigma = gd.getNextNumber(); // By Claude on 07/07/2026
this.rowcol_max_abs = gd.getNextNumber(); // By Claude on 07/07/2026 this.rowcol_max_abs = gd.getNextNumber(); // By Claude on 07/07/2026
this.rowcol_outlier = gd.getNextNumber(); // By Claude on 07/07/2026 this.rowcol_outlier = gd.getNextNumber(); // By Claude on 07/07/2026
...@@ -387,6 +391,7 @@ public class CuasRtParameters { ...@@ -387,6 +391,7 @@ public class CuasRtParameters {
properties.setProperty(prefix+"rowcol_en", this.rowcol_en+""); // boolean // By Claude on 07/07/2026 properties.setProperty(prefix+"rowcol_en", this.rowcol_en+""); // boolean // By Claude on 07/07/2026
properties.setProperty(prefix+"rowcol_hpf", this.rowcol_hpf+""); // boolean // By Claude on 07/07/2026 properties.setProperty(prefix+"rowcol_hpf", this.rowcol_hpf+""); // boolean // By Claude on 07/07/2026
properties.setProperty(prefix+"rowcol_hpf_sigma",this.rowcol_hpf_sigma+"");// double // By Claude on 07/07/2026 properties.setProperty(prefix+"rowcol_hpf_sigma",this.rowcol_hpf_sigma+"");// double // By Claude on 07/07/2026
properties.setProperty(prefix+"rowcol_um_sigma", this.rowcol_um_sigma+"");// double // By Claude on 07/07/2026
properties.setProperty(prefix+"rowcol_max_abs", this.rowcol_max_abs+""); // double // By Claude on 07/07/2026 properties.setProperty(prefix+"rowcol_max_abs", this.rowcol_max_abs+""); // double // By Claude on 07/07/2026
properties.setProperty(prefix+"rowcol_outlier", this.rowcol_outlier+""); // double // By Claude on 07/07/2026 properties.setProperty(prefix+"rowcol_outlier", this.rowcol_outlier+""); // double // By Claude on 07/07/2026
...@@ -479,6 +484,7 @@ public class CuasRtParameters { ...@@ -479,6 +484,7 @@ public class CuasRtParameters {
if (properties.getProperty(prefix+"rowcol_en")!=null) this.rowcol_en=Boolean.parseBoolean(properties.getProperty(prefix+"rowcol_en")); // By Claude on 07/07/2026 if (properties.getProperty(prefix+"rowcol_en")!=null) this.rowcol_en=Boolean.parseBoolean(properties.getProperty(prefix+"rowcol_en")); // By Claude on 07/07/2026
if (properties.getProperty(prefix+"rowcol_hpf")!=null) this.rowcol_hpf=Boolean.parseBoolean(properties.getProperty(prefix+"rowcol_hpf")); // By Claude on 07/07/2026 if (properties.getProperty(prefix+"rowcol_hpf")!=null) this.rowcol_hpf=Boolean.parseBoolean(properties.getProperty(prefix+"rowcol_hpf")); // By Claude on 07/07/2026
if (properties.getProperty(prefix+"rowcol_hpf_sigma")!=null)this.rowcol_hpf_sigma=Double.parseDouble(properties.getProperty(prefix+"rowcol_hpf_sigma"));// By Claude on 07/07/2026 if (properties.getProperty(prefix+"rowcol_hpf_sigma")!=null)this.rowcol_hpf_sigma=Double.parseDouble(properties.getProperty(prefix+"rowcol_hpf_sigma"));// By Claude on 07/07/2026
if (properties.getProperty(prefix+"rowcol_um_sigma")!=null) this.rowcol_um_sigma=Double.parseDouble(properties.getProperty(prefix+"rowcol_um_sigma")); // By Claude on 07/07/2026
if (properties.getProperty(prefix+"rowcol_max_abs")!=null) this.rowcol_max_abs=Double.parseDouble(properties.getProperty(prefix+"rowcol_max_abs")); // By Claude on 07/07/2026 if (properties.getProperty(prefix+"rowcol_max_abs")!=null) this.rowcol_max_abs=Double.parseDouble(properties.getProperty(prefix+"rowcol_max_abs")); // By Claude on 07/07/2026
if (properties.getProperty(prefix+"rowcol_outlier")!=null) this.rowcol_outlier=Double.parseDouble(properties.getProperty(prefix+"rowcol_outlier")); // By Claude on 07/07/2026 if (properties.getProperty(prefix+"rowcol_outlier")!=null) this.rowcol_outlier=Double.parseDouble(properties.getProperty(prefix+"rowcol_outlier")); // By Claude on 07/07/2026
...@@ -573,6 +579,7 @@ public class CuasRtParameters { ...@@ -573,6 +579,7 @@ public class CuasRtParameters {
cp.rowcol_en = this.rowcol_en; // By Claude on 07/07/2026 cp.rowcol_en = this.rowcol_en; // By Claude on 07/07/2026
cp.rowcol_hpf = this.rowcol_hpf; // By Claude on 07/07/2026 cp.rowcol_hpf = this.rowcol_hpf; // By Claude on 07/07/2026
cp.rowcol_hpf_sigma = this.rowcol_hpf_sigma; // By Claude on 07/07/2026 cp.rowcol_hpf_sigma = this.rowcol_hpf_sigma; // By Claude on 07/07/2026
cp.rowcol_um_sigma = this.rowcol_um_sigma; // By Claude on 07/07/2026
cp.rowcol_max_abs = this.rowcol_max_abs; // By Claude on 07/07/2026 cp.rowcol_max_abs = this.rowcol_max_abs; // By Claude on 07/07/2026
cp.rowcol_outlier = this.rowcol_outlier; // By Claude on 07/07/2026 cp.rowcol_outlier = this.rowcol_outlier; // By Claude on 07/07/2026
cp.psf_radius = this.psf_radius; cp.psf_radius = this.psf_radius;
......
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