Commit 8726cc75 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: -CUAS-RT-RENDER-EVAL: log / log-diff / diff hyperstack (Andrey's spec)

New evaluation product with the RT render (replaces the manual
run-UM-in-ImageJ workflow): merged component only, z = robust all-scenes
average + each scene, t ordered for ImageJ auto-range: 1) pixel LoG (the
pre-DNN kernel, LINEAR) of the image, 2) LoG of (scene - average),
3) plain (scene - average); t2/t3 are 0 for the average frame. The
average is the new robust one (edge-eroded + outlier-rejected),
extracted into shared helpers (erodedMasks/robustAverage) used by both
saveRenderHyper and the EVAL builder.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent c8cd84bd
......@@ -378,6 +378,12 @@ public class CuasRender {
mb_en, mb_tau, mb_max_gain, ts_names, debugLevel);
saveRenderHyper(center_CLT, "-CUAS-RT-RENDER", rendered, ts_names,
img_width, img_height, num_comps);
// ---- Evaluation product (Andrey 07/07/2026, replaces the manual ImageJ UM
// workflow): -CUAS-RT-RENDER-EVAL, merged component only, z = average + scenes,
// t (auto-range order): 1) LoG (no subtraction), 2) LoG of (scene - robust
// average), 3) difference without LoG; t2/t3 are 0 for the average frame.
saveRenderEvalLog(clt_parameters, center_CLT, rendered, ts_names,
img_width, img_height, num_comps); // By Claude on 07/07/2026
// ---- LoG A/B (curt.log_test / curt.log_ident): the aberration+LoG
// kernel-fold validation (roadmap RT-seed step 1b). Comparable hyperstacks:
// log_test: -LOG-ORACLE (product above x the EXISTING pre-DNN pixel LoG,
......@@ -636,69 +642,10 @@ public class CuasRender {
final int nr = (nc == 0) ? (num_comps - 1) : (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);
}
final boolean [][] eroded = erodedMasks(rendered, nr, img_width);
for (int navg = 0; navg < avg_ranges.length; navg++) {
// pass 1: mean + sigma from the ERODED contributions
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++) {
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]) {
final double d = px[i];
sum[i] += d;
sum2[i] += d*d;
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++) {
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],
robustAverage(rendered, nr, avg_ranges[navg][0], avg_ranges[navg][1], eroded, npix));
}
for (int ns = 0; ns < rendered.size(); ns++) {
final float [][] r = rendered.get(ns);
......@@ -717,6 +664,175 @@ public class CuasRender {
"averages: edge-eroded "+AVG_EDGE_ERODE+"px + "+AVG_OUTLIER_SIGMA+"-sigma outlier-rejected)");
}
/**
* Evaluation hyperstack (Andrey's spec 07/07/2026): merged component only,
* z (frames) = "average" (the robust all-scenes average) + each scene,
* t (channels, ordered so ImageJ auto-ranges on the LoG view):
* 1) "log" - pixel LoG (pre-DNN kernel, LINEAR) of the image itself;
* 2) "log-diff" - LoG of (scene - average); 0 for the average frame;
* 3) "diff" - (scene - average) without LoG; 0 for the average frame.
* Replaces the manual run-UM-in-ImageJ evaluation workflow. By Claude on 07/07/2026.
*/
private static void saveRenderEvalLog(
final CLTParameters clt_parameters,
final QuadCLT center_CLT,
final ArrayList<float [][]> rendered,
final ArrayList<String> ts_names,
final int img_width,
final int img_height,
final int num_comps) {
final int npix = img_width * img_height;
final int nr = num_comps - 1; // merged
final CuasRTUtils cuasRTUtils = new CuasRTUtils ( // same construction as CuasDetectRT.detectTargets
clt_parameters.curt.psf_radius, // double psf_radius,
clt_parameters.curt.n_sigma, // double n_sigma,
clt_parameters.curt.temp_weights.clone(), // double[] temporal_weights,
clt_parameters.curt.pix_decimate, // int pixel_decimate,
clt_parameters.curt.vel_decimate, // int velocity_decimate,
clt_parameters.curt.vel_radius, // int velocity_radius,
clt_parameters.curt.vel_suppr_rad, // int vel_suppr_rad,
img_width, // int width,
img_height); // int height
final double [] kernel2d = cuasRTUtils.getKernel2d();
final boolean [][] eroded = erodedMasks(rendered, nr, img_width);
final float [] avg = robustAverage(rendered, nr, 0, rendered.size()-1, eroded, npix);
final int num_z = rendered.size() + 1; // average first
// per z-frame: [0]=log, [1]=log-diff, [2]=diff (computed once, added comp-major below)
final float [][][] comps = new float [3][num_z][];
comps[0][0] = logSlice(avg, kernel2d, cuasRTUtils);
comps[1][0] = new float [npix]; // zeros for the average frame
comps[2][0] = new float [npix];
for (int ns = 0; ns < rendered.size(); ns++) {
final float [][] r = rendered.get(ns);
final float [] px = ((r != null) && (r[nr] != null)) ? r[nr] : null;
final float [] diff = new float [npix];
if (px != null) {
for (int i = 0; i < npix; i++) {
diff[i] = px[i] - avg[i]; // NaN where either is NaN
}
comps[0][ns+1] = logSlice(px, kernel2d, cuasRTUtils);
comps[1][ns+1] = logSlice(diff, kernel2d, cuasRTUtils);
comps[2][ns+1] = diff;
} else {
java.util.Arrays.fill(diff, Float.NaN);
comps[0][ns+1] = diff;
comps[1][ns+1] = diff;
comps[2][ns+1] = diff;
}
}
final String [] comp_names = {"log", "log-diff", "diff"};
final ImageStack stack = new ImageStack(img_width, img_height);
for (int nc = 0; nc < comps.length; nc++) {
stack.addSlice(comp_names[nc]+":average", comps[nc][0]);
for (int ns = 0; ns < rendered.size(); ns++) {
stack.addSlice(comp_names[nc]+":"+ts_names.get(ns), comps[nc][ns+1]);
}
}
final ImagePlus imp = new ImagePlus(center_CLT.getImageName()+"-CUAS-RT-RENDER-EVAL", stack);
imp.setDimensions(1, num_z, comps.length);
imp.setOpenAsHyperStack(true);
imp.getProcessor().resetMinAndMax();
center_CLT.saveImagePlusInModelDirectory(null, imp); // title as filename
System.out.println("testRenderSequence(): saved -CUAS-RT-RENDER-EVAL (merged only; t = log, log-diff, diff; "+
"z = robust average + "+rendered.size()+" scenes)");
}
/** Pixel LoG (pre-DNN kernel, LINEAR alpha=1) of one float slice. By Claude on 07/07/2026. */
private static float [] logSlice(final float [] px, final double [] kernel2d, final CuasRTUtils cuasRTUtils) {
final double [] dslice = new double [px.length];
for (int i = 0; i < dslice.length; i++) dslice[i] = px[i];
final double [] dlog = cuasRTUtils.convolve2DLReLU(
dslice, // final double [] data,
kernel2d, // final double [] kernel, // square
null, // double [] result_in,
1.0); // final double alpha) - LINEAR (the standing ruling)
final float [] out = new float [px.length];
for (int i = 0; i < out.length; i++) out[i] = (float) dlog[i];
return out;
}
/** Per-scene eroded validity masks for the robust averages (see class constants).
* By Claude on 07/07/2026. */
private static boolean [][] erodedMasks(
final ArrayList<float [][]> rendered,
final int nr,
final int img_width) {
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);
return eroded;
}
/** Robust temporal average of one component over [ns_from..ns_to]: eroded per-scene
* contributions (partial-tile edge junk excluded by geometry), then outlier-rejected
* (AVG_OUTLIER_SIGMA vs the first-pass mean when >= AVG_OUTLIER_MIN providers);
* zero eroded providers -> NaN. By Claude on 07/07/2026. */
private static float [] robustAverage(
final ArrayList<float [][]> rendered,
final int nr,
final int ns_from,
final int ns_to,
final boolean [][] eroded,
final int npix) {
// pass 1: mean + sigma from the ERODED contributions
final double [] sum = new double [npix];
final double [] sum2 = new double [npix];
final int [] cnt = new int [npix];
for (int ns = ns_from; ns <= ns_to; 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]) {
final double d = px[i];
sum[i] += d;
sum2[i] += d*d;
cnt[i]++;
}
}
// pass 2: outlier-rejected mean (rejection only with enough providers)
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 = ns_from; ns <= ns_to; 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]++;
}
}
final float [] avg = new float [npix];
for (int i = 0; i < npix; i++) {
avg[i] = (cntk[i] > 0) ? ((float) (sumk[i]/cntk[i])) : Float.NaN;
}
return avg;
}
/** 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.
......
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