Commit c8cd84bd authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: RT-RENDER average slices: per-scene edge erosion + outlier rejection

The temporal averages accumulated per-scene edge junk into stable margin
artifacts (ROWCOL-04: worst columns x=12,20,28 = 8-px tile pitch -
partially covered virtual tiles at the warped-coverage edge; the wobble
sweeps which tiles are partial). For the two AVERAGE slices only
(individual scenes stay liberal/untouched): each scene's contribution is
box-eroded 24 px (3 tile pitches, AVG_EDGE_ERODE - clears the
partial-tile ring) and outlier-rejected (3 sigma vs the first-pass mean
when >= 4 providers, per Andrey's >1-non-NaN-provider rule); zero eroded
providers -> honest NaN.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent c0096034
...@@ -600,13 +600,21 @@ public class CuasRender { ...@@ -600,13 +600,21 @@ public class CuasRender {
return rendered; return rendered;
} }
public static final int AVG_EDGE_ERODE = 24; // px: erode each scene's DEFINED region for the AVERAGE slices only (Andrey 07/07/2026) - partially covered virtual tiles at the warped-coverage edge emit tile-pitch junk (accumulated worst columns x=12,20,28 = 8-px pitch in ROWCOL-04); 3 tile pitches clears the partial ring. Individual scene slices keep everything (liberal). // By Claude on 07/07/2026
public static final double AVG_OUTLIER_SIGMA = 3.0; // reject contributions beyond this many sigmas from the first-pass mean... // By Claude on 07/07/2026
public static final int AVG_OUTLIER_MIN = 4; // ...when the pixel has at least this many (eroded) providers. // By Claude on 07/07/2026
/** Save a render-product hyperstack organized like -CUAS-INDIVIDUAL-CUAS-DBG so /** Save a render-product hyperstack organized like -CUAS-INDIVIDUAL-CUAS-DBG so
* ImageJ can subtract the two directly (Andrey 07/06/2026): * ImageJ can subtract the two directly (Andrey 07/06/2026):
* t (channels): merged FIRST, then s00..s15; * t (channels): merged FIRST, then s00..s15;
* z (frames): "average" (all scenes), "average-f:l" (middle-half range, * z (frames): "average" (all scenes), "average-f:l" (middle-half range,
* the renderSceneSequence default f=last/4, l=3*last/4), * the renderSceneSequence default f=last/4, l=3*last/4),
* then the scenes. * then the scenes.
* Averages are NaN-tolerant per pixel (same as renderSceneSequence). * AVERAGE slices (Andrey 07/07/2026): per-scene contributions are edge-ERODED
* (AVG_EDGE_ERODE) and outlier-REJECTED (AVG_OUTLIER_SIGMA when >= AVG_OUTLIER_MIN
* providers) - the per-scene edge junk otherwise accumulates into stable margin
* artifacts; zero eroded providers -> honest NaN. Individual scene slices are saved
* as rendered (liberal - full coverage for inspection).
* By Claude on 07/06/2026 (extracted + reorganized from testRenderSequence). */ * By Claude on 07/06/2026 (extracted + reorganized from testRenderSequence). */
private static void saveRenderHyper( private static void saveRenderHyper(
final QuadCLT center_CLT, final QuadCLT center_CLT,
...@@ -627,19 +635,68 @@ public class CuasRender { ...@@ -627,19 +635,68 @@ public class CuasRender {
// merged (rendered index num_comps-1) goes FIRST, then s00..s15 // merged (rendered index num_comps-1) goes FIRST, then s00..s15
final int nr = (nc == 0) ? (num_comps - 1) : (nc - 1); final int nr = (nc == 0) ? (num_comps - 1) : (nc - 1);
final String comp = (nc == 0) ? "merged" : String.format("s%02d", nc - 1); final String comp = (nc == 0) ? "merged" : String.format("s%02d", nc - 1);
// eroded per-scene validity for the AVERAGE slices (see class constants)
final boolean [][] eroded = new boolean [rendered.size()][];
{
final Thread[] threads = ImageDtt.newThreadArray();
final java.util.concurrent.atomic.AtomicInteger ai = new java.util.concurrent.atomic.AtomicInteger(0);
for (int ithread = 0; ithread < threads.length; ithread++) {
threads[ithread] = new Thread() {
public void run() {
for (int ns = ai.getAndIncrement(); ns < rendered.size(); ns = ai.getAndIncrement()) {
final float [][] r = rendered.get(ns);
if ((r != null) && (r[nr] != null)) {
eroded[ns] = erodeDefined(r[nr], img_width, AVG_EDGE_ERODE);
}
}
}
};
}
ImageDtt.startAndJoin(threads);
}
for (int navg = 0; navg < avg_ranges.length; navg++) { for (int navg = 0; navg < avg_ranges.length; navg++) {
final float [] avg = new float [npix]; // pass 1: mean + sigma from the ERODED contributions
final int [] cnt = new int [npix]; final double [] sum = new double [npix];
final double [] sum2 = new double [npix];
final int [] cnt = new int [npix];
for (int ns = avg_ranges[navg][0]; ns <= avg_ranges[navg][1]; ns++) { for (int ns = avg_ranges[navg][0]; ns <= avg_ranges[navg][1]; ns++) {
final float [][] r = rendered.get(ns); final float [][] r = rendered.get(ns);
if ((r == null) || (r[nr] == null)) continue; if ((r == null) || (r[nr] == null) || (eroded[ns] == null)) continue;
for (int i = 0; i < npix; i++) if (!Float.isNaN(r[nr][i])) { final float [] px = r[nr];
avg[i] += r[nr][i]; final boolean [] ok = eroded[ns];
for (int i = 0; i < npix; i++) if (ok[i]) {
final double d = px[i];
sum[i] += d;
sum2[i] += d*d;
cnt[i]++; cnt[i]++;
} }
} }
// pass 2: outlier-rejected mean (rejection only with enough providers)
final float [] avg = new float [npix];
final double [] sumk = new double [npix];
final int [] cntk = new int [npix];
final double [] mean = new double [npix];
final double [] rej = new double [npix]; // rejection threshold (|x-mean|), infinite when too few providers
for (int i = 0; i < npix; i++) {
if (cnt[i] > 0) {
mean[i] = sum[i]/cnt[i];
final double var = sum2[i]/cnt[i] - mean[i]*mean[i];
rej[i] = (cnt[i] >= AVG_OUTLIER_MIN) ?
(AVG_OUTLIER_SIGMA * Math.sqrt(Math.max(var, 0.0))) : Double.POSITIVE_INFINITY;
}
}
for (int ns = avg_ranges[navg][0]; ns <= avg_ranges[navg][1]; ns++) {
final float [][] r = rendered.get(ns);
if ((r == null) || (r[nr] == null) || (eroded[ns] == null)) continue;
final float [] px = r[nr];
final boolean [] ok = eroded[ns];
for (int i = 0; i < npix; i++) if (ok[i] && (Math.abs(px[i] - mean[i]) <= rej[i])) {
sumk[i] += px[i];
cntk[i]++;
}
}
for (int i = 0; i < npix; i++) { for (int i = 0; i < npix; i++) {
avg[i] = (cnt[i] > 0) ? (avg[i] / cnt[i]) : Float.NaN; avg[i] = (cntk[i] > 0) ? ((float) (sumk[i]/cntk[i])) : Float.NaN;
} }
stack.addSlice(comp+":"+avg_names[navg], avg); stack.addSlice(comp+":"+avg_names[navg], avg);
} }
...@@ -650,11 +707,40 @@ public class CuasRender { ...@@ -650,11 +707,40 @@ public class CuasRender {
} }
final int num_frames = rendered.size() + avg_ranges.length; final int num_frames = rendered.size() + avg_ranges.length;
final ImagePlus imp = new ImagePlus(center_CLT.getImageName()+suffix, stack); final ImagePlus imp = new ImagePlus(center_CLT.getImageName()+suffix, stack);
// (average slices above: eroded + outlier-rejected; see class constants)
imp.setDimensions(1, num_frames, num_comps); imp.setDimensions(1, num_frames, num_comps);
imp.setOpenAsHyperStack(true); imp.setOpenAsHyperStack(true);
imp.getProcessor().resetMinAndMax(); imp.getProcessor().resetMinAndMax();
center_CLT.saveImagePlusInModelDirectory(null, imp); // title as filename center_CLT.saveImagePlusInModelDirectory(null, imp); // title as filename
System.out.println("testRenderSequence(): saved "+suffix+" ("+num_comps+" components x "+ System.out.println("testRenderSequence(): saved "+suffix+" ("+num_comps+" components x "+
num_frames+" frames = 2 averages + "+rendered.size()+" scenes; merged first)"); num_frames+" frames = 2 averages + "+rendered.size()+" scenes; merged first; "+
"averages: edge-eroded "+AVG_EDGE_ERODE+"px + "+AVG_OUTLIER_SIGMA+"-sigma outlier-rejected)");
}
/** Box erosion of the DEFINED (non-NaN) region: true only where the whole
* (2r+1)x(2r+1) neighborhood is defined AND inside the frame. Separable:
* per-row prefix counts of NaN, then per-column prefix counts of the row result.
* By Claude on 07/07/2026. */
private static boolean [] erodeDefined(final float [] px, final int width, final int r) {
final int npix = px.length;
final int height = npix/width;
final boolean [] okr = new boolean [npix]; // row-eroded
final int [] pre = new int [width+1];
for (int y = 0; y < height; y++) {
final int row0 = y*width;
for (int x = 0; x < width; x++) pre[x+1] = pre[x] + (Float.isNaN(px[row0+x]) ? 1 : 0);
for (int x = r; x < width-r; x++) {
okr[row0+x] = (pre[x+r+1] - pre[x-r]) == 0;
}
}
final boolean [] out = new boolean [npix];
final int [] prec = new int [height+1];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) prec[y+1] = prec[y] + (okr[y*width+x] ? 0 : 1);
for (int y = r; y < height-r; y++) {
out[y*width+x] = (prec[y+r+1] - prec[y-r]) == 0;
}
}
return out;
} }
} }
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