From bd6df7851e5edda856ba3e04bc6fe8dd19925007 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 14:00:20 -0500 Subject: [PATCH 01/56] Set up example dataset for background correction --- .../embackground/CorrectBackground.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java new file mode 100644 index 000000000..f56458e74 --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -0,0 +1,23 @@ +package org.janelia.render.client.embackground; + +import ij.ImageJ; +import net.imglib2.img.Img; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import org.janelia.saalfeldlab.n5.N5FSReader; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; + +public class CorrectBackground { + private static final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; + private static final String dataset = "data/s4"; + + public static void main(final String[] args) { + try (final N5Reader reader = new N5FSReader(containerPath)) { + final Img stack = N5Utils.open(reader, dataset); + + new ImageJ(); + ImageJFunctions.show(stack); + } + } +} From 125207878f3019949baf982763e922b6743feebb Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 14:40:41 -0500 Subject: [PATCH 02/56] Add first proof of concept for background correction --- .../embackground/CorrectBackground.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index f56458e74..ffab76f35 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -1,23 +1,49 @@ package org.janelia.render.client.embackground; import ij.ImageJ; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealRandomAccessible; +import net.imglib2.converter.Converters; import net.imglib2.img.Img; import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.position.FunctionRealRandomAccessible; import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.view.Views; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; public class CorrectBackground { private static final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; - private static final String dataset = "data/s4"; + private static final String dataset = "data"; + private static final int scale = 4; public static void main(final String[] args) { try (final N5Reader reader = new N5FSReader(containerPath)) { - final Img stack = N5Utils.open(reader, dataset); + final Img stack = N5Utils.open(reader, dataset + "/s" + scale); + + final int width = (int) stack.dimension(0); + final int height = (int) stack.dimension(1); + + final int midX = width / 2; + final int midY = height / 2; + + final RealRandomAccessible background = new FunctionRealRandomAccessible<>(3, (pos, value) -> { + final double x = (pos.getDoublePosition(0) - midX) / width; + final double y = (pos.getDoublePosition(1) - midY) / height; + value.setReal(1 - (x * x + y * y) / 2.0); + }, FloatType::new); + + final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), stack); + + final RandomAccessibleInterval corrected = Converters.convert(stack, materializedBackground, (s, b, o) -> { + o.set((int) (s.getRealDouble() / b.getRealDouble())); + }, new UnsignedByteType()); new ImageJ(); ImageJFunctions.show(stack); + ImageJFunctions.show(corrected); } } } From d5507657ee1ac0d595f3b67a5c27cb72d35e15e6 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 16:51:50 -0500 Subject: [PATCH 03/56] Add quadratic background model --- .../embackground/QuadraticBackground.java | 114 ++++++++++++++++++ .../embackground/QuadraticBackgroundTest.java | 52 ++++++++ 2 files changed, 166 insertions(+) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java create mode 100644 render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java new file mode 100644 index 000000000..f12e029ed --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java @@ -0,0 +1,114 @@ +package org.janelia.render.client.embackground; + +import mpicbg.models.AbstractModel; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.PointMatch; +import org.ejml.data.DMatrixRMaj; +import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; +import org.ejml.interfaces.linsol.LinearSolverDense; + +import java.util.Arrays; +import java.util.Collection; + +public class QuadraticBackground extends AbstractModel { + + private final int N_COEFFICIENTS = 6; + private final double[] coefficients = new double[N_COEFFICIENTS]; + + + public QuadraticBackground() { + Arrays.fill(coefficients, 0); + } + + public QuadraticBackground(final double[] coefficients) { + System.arraycopy(coefficients, 0, this.coefficients, 0, N_COEFFICIENTS); + } + + public QuadraticBackground(final QuadraticBackground background) { + this(background.coefficients); + } + + @Override + public int getMinNumMatches() { + return N_COEFFICIENTS; + } + + @Override + public

void fit(final Collection

matches) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final DMatrixRMaj ATA = new DMatrixRMaj(N_COEFFICIENTS, N_COEFFICIENTS, true, new double[N_COEFFICIENTS * N_COEFFICIENTS]); + final DMatrixRMaj ATb = new DMatrixRMaj(N_COEFFICIENTS, 1); + + final double[] rowA = new double[N_COEFFICIENTS]; + + for (final P match : matches) { + final double x = match.getP1().getL()[0]; + final double y = match.getP1().getL()[1]; + final double z = match.getP2().getL()[0]; + + // compute one row of the least-squares matrix A + rowA[0] = x * x; + rowA[1] = x * y; + rowA[2] = y * y; + rowA[3] = x; + rowA[4] = y; + rowA[5] = 1; + + // update upper triangle of A^T * A + for (int i = 0; i < N_COEFFICIENTS; i++) { + for (int j = i; j < N_COEFFICIENTS; j++) { + ATA.data[i * N_COEFFICIENTS + j] += rowA[i] * rowA[j]; + } + } + + // update right-hand side A^T * b + for (int i = 0; i < N_COEFFICIENTS; i++) { + ATb.data[i] += rowA[i] * z; + } + } + + // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) + final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(3); + solver.setA(ATA); + + // coefficients are modified in place + final DMatrixRMaj x = new DMatrixRMaj(N_COEFFICIENTS, 1); + x.setData(coefficients); + solver.solve(ATb, x); + } + + @Override + public void set(final QuadraticBackground quadraticBackground) { + + } + + @Override + public QuadraticBackground copy() { + return null; + } + + @Override + public double[] apply(final double[] location) { + final double[] result = location.clone(); + applyInPlace(result); + return result; + } + + @Override + public void applyInPlace(final double[] location) { + final double x = location[0]; + final double y = location[1]; + final double result = coefficients[0] * x * x + + coefficients[1] * x * y + + coefficients[2] * y * y + + coefficients[3] * x + + coefficients[4] * y + + coefficients[5]; + location[0] = result; + location[1] = 0.0; + } + + public double[] getCoefficients() { + return coefficients; + } +} diff --git a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java new file mode 100644 index 000000000..b83262cc7 --- /dev/null +++ b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java @@ -0,0 +1,52 @@ +package org.janelia.render.client.embackground; + +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.Point; +import mpicbg.models.PointMatch; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class QuadraticBackgroundTest { + + @Test + public void simpleModelIsComputedCorrectly() throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final List matches = new ArrayList<>(); + + // minimal set of points to fit 0.5 * x^2 + 0.5 * y^2 + matches.add(new PointMatch(new Point(new double[]{0, 0}), new Point(new double[]{0}))); + matches.add(new PointMatch(new Point(new double[]{1, 1}), new Point(new double[]{1}))); + matches.add(new PointMatch(new Point(new double[]{-1, 1}), new Point(new double[]{1}))); + matches.add(new PointMatch(new Point(new double[]{1, -1}), new Point(new double[]{1}))); + matches.add(new PointMatch(new Point(new double[]{-1, -1}), new Point(new double[]{1}))); + matches.add(new PointMatch(new Point(new double[]{0, 1}), new Point(new double[]{0.5}))); + + final QuadraticBackground background = new QuadraticBackground(); + background.fit(matches); + + // order of coefficients: {x^2, xy, y^2, x, y, 1} + Assert.assertArrayEquals(new double[]{0.5, 0, 0.5, 0, 0, 0}, background.getCoefficients(), 1e-12); + } + + @Test + public void modelCanBeRecoveredWithManyPoints() throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final QuadraticBackground background = new QuadraticBackground(new double[]{1, 2, 3, 4, 5, 6}); + + final List matches = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + final double x = Math.random() * 2 - 1; + final double y = Math.random() * 2 - 1; + final double[] location = new double[]{x, y}; + background.applyInPlace(location); + matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{location[0]}))); + } + + final QuadraticBackground recovered = new QuadraticBackground(); + recovered.fit(matches); + + Assert.assertArrayEquals(background.getCoefficients(), recovered.getCoefficients(), 1e-12); + } +} From 203645627db2ebf73a3585a7392e12a25c085d0d Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 17:27:40 -0500 Subject: [PATCH 04/56] Only use first slice for correction example --- .../client/embackground/CorrectBackground.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index ffab76f35..829f77d22 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -22,6 +22,7 @@ public class CorrectBackground { public static void main(final String[] args) { try (final N5Reader reader = new N5FSReader(containerPath)) { final Img stack = N5Utils.open(reader, dataset + "/s" + scale); + final RandomAccessibleInterval firstSlice = Views.hyperSlice(stack, 2, 0); final int width = (int) stack.dimension(0); final int height = (int) stack.dimension(1); @@ -29,21 +30,22 @@ public static void main(final String[] args) { final int midX = width / 2; final int midY = height / 2; - final RealRandomAccessible background = new FunctionRealRandomAccessible<>(3, (pos, value) -> { + final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { final double x = (pos.getDoublePosition(0) - midX) / width; final double y = (pos.getDoublePosition(1) - midY) / height; value.setReal(1 - (x * x + y * y) / 2.0); }, FloatType::new); - final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), stack); - final RandomAccessibleInterval corrected = Converters.convert(stack, materializedBackground, (s, b, o) -> { + final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), firstSlice); + + final RandomAccessibleInterval corrected = Converters.convert(firstSlice, materializedBackground, (s, b, o) -> { o.set((int) (s.getRealDouble() / b.getRealDouble())); }, new UnsignedByteType()); new ImageJ(); - ImageJFunctions.show(stack); - ImageJFunctions.show(corrected); + ImageJFunctions.show(firstSlice, "Original"); + ImageJFunctions.show(corrected, "Corrected"); } } } From a22c35943017dfea150ff86b950227233c47c2b8 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 17:42:07 -0500 Subject: [PATCH 05/56] Add toString method for better printing --- .../client/embackground/QuadraticBackground.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java index f12e029ed..ef7f39924 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java @@ -111,4 +111,15 @@ public void applyInPlace(final double[] location) { public double[] getCoefficients() { return coefficients; } + + @Override + public String toString() { + return "QuadraticBackground{ " + + coefficients[0] + " * x^2 + " + + coefficients[1] + " * xy + " + + coefficients[2] + " * y^2 + " + + coefficients[3] + " * x + " + + coefficients[4] + " * y + " + + coefficients[5] + " }"; + } } From 7c93790801918a3478570df1361c100a65e753e7 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 17:42:30 -0500 Subject: [PATCH 06/56] Fit model from first slice --- .../embackground/CorrectBackground.java | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 829f77d22..1f96a2f30 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -1,6 +1,11 @@ package org.janelia.render.client.embackground; import ij.ImageJ; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.Point; +import mpicbg.models.PointMatch; +import net.imglib2.Cursor; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.converter.Converters; @@ -14,12 +19,15 @@ import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import java.util.ArrayList; +import java.util.List; + public class CorrectBackground { private static final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; private static final String dataset = "data"; private static final int scale = 4; - public static void main(final String[] args) { + public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException { try (final N5Reader reader = new N5FSReader(containerPath)) { final Img stack = N5Utils.open(reader, dataset + "/s" + scale); final RandomAccessibleInterval firstSlice = Views.hyperSlice(stack, 2, 0); @@ -30,13 +38,32 @@ public static void main(final String[] args) { final int midX = width / 2; final int midY = height / 2; + // fit a quadratic background model with all the points in the first slice + final QuadraticBackground backgroundModel = new QuadraticBackground(); + final List matches = new ArrayList<>(); + final Cursor cursor = Views.iterable(firstSlice).localizingCursor(); + while (cursor.hasNext()) { + cursor.fwd(); + final double x = (cursor.getDoublePosition(0) - midX) / width; + final double y = (cursor.getDoublePosition(1) - midY) / height; + final double z = cursor.get().getRealDouble(); + matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); + } + backgroundModel.fit(matches); + System.out.println("Fitted background model: " + backgroundModel); + + // we assume that the model is concave, so the offset is the maximum value + final double maxValue = backgroundModel.getCoefficients()[5]; + + // create a background image from the model + final double[] location = new double[2]; final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { - final double x = (pos.getDoublePosition(0) - midX) / width; - final double y = (pos.getDoublePosition(1) - midY) / height; - value.setReal(1 - (x * x + y * y) / 2.0); + location[0] = (pos.getDoublePosition(0) - midX) / width; + location[1] = (pos.getDoublePosition(1) - midY) / height; + backgroundModel.applyInPlace(location); + value.setReal(location[0] / maxValue); }, FloatType::new); - final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), firstSlice); final RandomAccessibleInterval corrected = Converters.convert(firstSlice, materializedBackground, (s, b, o) -> { @@ -45,6 +72,7 @@ public static void main(final String[] args) { new ImageJ(); ImageJFunctions.show(firstSlice, "Original"); + ImageJFunctions.show(materializedBackground, "Background"); ImageJFunctions.show(corrected, "Corrected"); } } From 496e0c98a1bc53892932af70da1d4089831b861c Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 5 Nov 2024 18:04:55 -0500 Subject: [PATCH 07/56] Deal with overflow errors --- .../janelia/render/client/embackground/CorrectBackground.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 1f96a2f30..b9e23438c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -39,6 +39,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException final int midY = height / 2; // fit a quadratic background model with all the points in the first slice + final long start = System.currentTimeMillis(); final QuadraticBackground backgroundModel = new QuadraticBackground(); final List matches = new ArrayList<>(); final Cursor cursor = Views.iterable(firstSlice).localizingCursor(); @@ -51,6 +52,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } backgroundModel.fit(matches); System.out.println("Fitted background model: " + backgroundModel); + System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); // we assume that the model is concave, so the offset is the maximum value final double maxValue = backgroundModel.getCoefficients()[5]; @@ -67,7 +69,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), firstSlice); final RandomAccessibleInterval corrected = Converters.convert(firstSlice, materializedBackground, (s, b, o) -> { - o.set((int) (s.getRealDouble() / b.getRealDouble())); + o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() / b.getRealDouble()))); }, new UnsignedByteType()); new ImageJ(); From 3ac4319fab129a458ba4bf8701aa735a6cacc17a Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 11:55:28 -0500 Subject: [PATCH 08/56] Add test for model evaluation --- .../embackground/QuadraticBackgroundTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java index b83262cc7..216bdd8cd 100644 --- a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java +++ b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java @@ -12,6 +12,32 @@ public class QuadraticBackgroundTest { + @Test + public void simpleModelProducesCorrectResults() throws NotEnoughDataPointsException, IllDefinedDataPointsException { + // 0.5 * x^2 + 0.5 * y^2 + 1 + final QuadraticBackground background = new QuadraticBackground(new double[]{0.5, 0, 0.5, 0, 0, 1}); + + final double[] location1 = new double[]{0, 0}; + background.applyInPlace(location1); + Assert.assertEquals(1, location1[0], 1e-12); + + final double[] location2 = new double[]{1, 1}; + background.applyInPlace(location2); + Assert.assertEquals(2, location2[0], 1e-12); + + final double[] location3 = new double[]{-1, 1}; + background.applyInPlace(location3); + Assert.assertEquals(2, location3[0], 1e-12); + + final double[] location4 = new double[]{1, 0}; + background.applyInPlace(location4); + Assert.assertEquals(1.5, location4[0], 1e-12); + + final double[] location5 = new double[]{0, 1}; + background.applyInPlace(location5); + Assert.assertEquals(1.5, location5[0], 1e-12); + } + @Test public void simpleModelIsComputedCorrectly() throws NotEnoughDataPointsException, IllDefinedDataPointsException { final List matches = new ArrayList<>(); From cbac9d5cc60a9939114d59a88854864b847ecd82 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 12:00:53 -0500 Subject: [PATCH 09/56] Flip order of coefficients in quadratic model --- .../embackground/CorrectBackground.java | 2 +- .../embackground/QuadraticBackground.java | 42 ++++++++++--------- .../embackground/QuadraticBackgroundTest.java | 6 +-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index b9e23438c..99c6d4733 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -55,7 +55,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); // we assume that the model is concave, so the offset is the maximum value - final double maxValue = backgroundModel.getCoefficients()[5]; + final double maxValue = backgroundModel.getCoefficients()[0]; // create a background image from the model final double[] location = new double[2]; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java index ef7f39924..eac621ac8 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java @@ -11,6 +11,10 @@ import java.util.Arrays; import java.util.Collection; + +/** + * A quadratic model for background correction in 2D slices of EM data. + */ public class QuadraticBackground extends AbstractModel { private final int N_COEFFICIENTS = 6; @@ -47,12 +51,12 @@ public

void fit(final Collection

matches) throws NotEn final double z = match.getP2().getL()[0]; // compute one row of the least-squares matrix A - rowA[0] = x * x; - rowA[1] = x * y; - rowA[2] = y * y; - rowA[3] = x; - rowA[4] = y; - rowA[5] = 1; + rowA[0] = 1; + rowA[1] = y; + rowA[2] = x; + rowA[3] = y * y; + rowA[4] = x * y; + rowA[5] = x * x; // update upper triangle of A^T * A for (int i = 0; i < N_COEFFICIENTS; i++) { @@ -68,7 +72,7 @@ public

void fit(final Collection

matches) throws NotEn } // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) - final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(3); + final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(N_COEFFICIENTS); solver.setA(ATA); // coefficients are modified in place @@ -98,12 +102,12 @@ public double[] apply(final double[] location) { public void applyInPlace(final double[] location) { final double x = location[0]; final double y = location[1]; - final double result = coefficients[0] * x * x - + coefficients[1] * x * y - + coefficients[2] * y * y - + coefficients[3] * x - + coefficients[4] * y - + coefficients[5]; + final double result = coefficients[5] * x * x + + coefficients[4] * x * y + + coefficients[3] * y * y + + coefficients[2] * x + + coefficients[1] * y + + coefficients[0]; location[0] = result; location[1] = 0.0; } @@ -115,11 +119,11 @@ public double[] getCoefficients() { @Override public String toString() { return "QuadraticBackground{ " - + coefficients[0] + " * x^2 + " - + coefficients[1] + " * xy + " - + coefficients[2] + " * y^2 + " - + coefficients[3] + " * x + " - + coefficients[4] + " * y + " - + coefficients[5] + " }"; + + coefficients[5] + " x^2 + " + + coefficients[4] + " xy + " + + coefficients[3] + " y^2 + " + + coefficients[2] + " x + " + + coefficients[1] + " y + " + + coefficients[0] + " }"; } } diff --git a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java index 216bdd8cd..6c4fc0bc0 100644 --- a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java +++ b/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java @@ -15,7 +15,7 @@ public class QuadraticBackgroundTest { @Test public void simpleModelProducesCorrectResults() throws NotEnoughDataPointsException, IllDefinedDataPointsException { // 0.5 * x^2 + 0.5 * y^2 + 1 - final QuadraticBackground background = new QuadraticBackground(new double[]{0.5, 0, 0.5, 0, 0, 1}); + final QuadraticBackground background = new QuadraticBackground(new double[]{1, 0, 0, 0.5, 0, 0.5}); final double[] location1 = new double[]{0, 0}; background.applyInPlace(location1); @@ -53,8 +53,8 @@ public void simpleModelIsComputedCorrectly() throws NotEnoughDataPointsException final QuadraticBackground background = new QuadraticBackground(); background.fit(matches); - // order of coefficients: {x^2, xy, y^2, x, y, 1} - Assert.assertArrayEquals(new double[]{0.5, 0, 0.5, 0, 0, 0}, background.getCoefficients(), 1e-12); + // order of coefficients: {1, y, x, y^2, x*y, x^2} + Assert.assertArrayEquals(new double[]{0, 0, 0, 0.5, 0, 0.5}, background.getCoefficients(), 1e-12); } @Test From 040c88406f3860a09c2e3fc7ed846ea5b279c26f Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 12:09:36 -0500 Subject: [PATCH 10/56] Add fourth-order model --- .../embackground/FourthOrderBackground.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java new file mode 100644 index 000000000..f0ced78bf --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java @@ -0,0 +1,143 @@ +package org.janelia.render.client.embackground; + +import mpicbg.models.AbstractModel; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.PointMatch; +import org.ejml.data.DMatrixRMaj; +import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; +import org.ejml.interfaces.linsol.LinearSolverDense; + +import java.util.Arrays; +import java.util.Collection; + + +/** + * A 4th order model for background correction in 2D slices of EM data. + * To ensure that the model is concave, no 3rd order terms are included. + */ +public class FourthOrderBackground extends AbstractModel { + + private final int N_COEFFICIENTS = 9; + private final double[] coefficients = new double[N_COEFFICIENTS]; + + + public FourthOrderBackground() { + Arrays.fill(coefficients, 0); + } + + public FourthOrderBackground(final double[] coefficients) { + System.arraycopy(coefficients, 0, this.coefficients, 0, N_COEFFICIENTS); + } + + public FourthOrderBackground(final FourthOrderBackground background) { + this(background.coefficients); + } + + @Override + public int getMinNumMatches() { + return N_COEFFICIENTS; + } + + @Override + public

void fit(final Collection

matches) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final DMatrixRMaj ATA = new DMatrixRMaj(N_COEFFICIENTS, N_COEFFICIENTS, true, new double[N_COEFFICIENTS * N_COEFFICIENTS]); + final DMatrixRMaj ATb = new DMatrixRMaj(N_COEFFICIENTS, 1); + + final double[] rowA = new double[N_COEFFICIENTS]; + + for (final P match : matches) { + final double x = match.getP1().getL()[0]; + final double y = match.getP1().getL()[1]; + final double z = match.getP2().getL()[0]; + + // compute one row of the least-squares matrix A + final double xx = x * x; + final double yy = y * y; + rowA[0] = 1; + rowA[1] = y; + rowA[2] = x; + rowA[3] = yy; + rowA[4] = x * y; + rowA[5] = xx; + rowA[6] = yy * yy; + rowA[7] = xx * yy; + rowA[8] = xx * xx; + + // update upper triangle of A^T * A + for (int i = 0; i < N_COEFFICIENTS; i++) { + for (int j = i; j < N_COEFFICIENTS; j++) { + ATA.data[i * N_COEFFICIENTS + j] += rowA[i] * rowA[j]; + } + } + + // update right-hand side A^T * b + for (int i = 0; i < N_COEFFICIENTS; i++) { + ATb.data[i] += rowA[i] * z; + } + } + + // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) + final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(N_COEFFICIENTS); + solver.setA(ATA); + + // coefficients are modified in place + final DMatrixRMaj x = new DMatrixRMaj(N_COEFFICIENTS, 1); + x.setData(coefficients); + solver.solve(ATb, x); + } + + @Override + public void set(final FourthOrderBackground quadraticBackground) { + + } + + @Override + public FourthOrderBackground copy() { + return null; + } + + @Override + public double[] apply(final double[] location) { + final double[] result = location.clone(); + applyInPlace(result); + return result; + } + + @Override + public void applyInPlace(final double[] location) { + final double x = location[0]; + final double y = location[1]; + final double xx = x * x; + final double yy = y * y; + final double result = coefficients[8] * xx * xx + + coefficients[7] * xx * yy + + coefficients[6] * yy * yy + + coefficients[5] * xx + + coefficients[4] * x * y + + coefficients[3] * yy + + coefficients[2] * x + + coefficients[1] * y + + coefficients[0]; + location[0] = result; + location[1] = 0.0; + } + + public double[] getCoefficients() { + return coefficients; + } + + @Override + public String toString() { + return "FourthOrderBackground{ " + + coefficients[8] + " x^4 + " + + coefficients[7] + " x^2 y^2 + " + + coefficients[6] + " y^4 + " + + coefficients[5] + " x^2 + " + + coefficients[4] + " xy + " + + coefficients[3] + " y^2 + " + + coefficients[2] + " x + " + + coefficients[1] + " y + " + + coefficients[0] + " }"; + } +} From 7d7e9ad75adb5c8616e44335a268ea85036d5ce3 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 14:07:41 -0500 Subject: [PATCH 11/56] Load ROIs from disk --- .../embackground/CorrectBackground.java | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 99c6d4733..ba8be0b64 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -1,6 +1,8 @@ package org.janelia.render.client.embackground; import ij.ImageJ; +import ij.gui.Roi; +import ij.io.RoiDecoder; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; @@ -19,15 +21,20 @@ import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import java.io.IOException; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public class CorrectBackground { private static final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; + private static final String roiPath = containerPath + "/roi-set.zip"; private static final String dataset = "data"; private static final int scale = 4; - public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException, IOException { try (final N5Reader reader = new N5FSReader(containerPath)) { final Img stack = N5Utils.open(reader, dataset + "/s" + scale); final RandomAccessibleInterval firstSlice = Views.hyperSlice(stack, 2, 0); @@ -38,9 +45,14 @@ public static void main(final String[] args) throws NotEnoughDataPointsException final int midX = width / 2; final int midY = height / 2; + final List rois = readRois(roiPath); + if (rois.isEmpty()) { + throw new IllegalArgumentException("No ROIs found in " + roiPath); + } + // fit a quadratic background model with all the points in the first slice final long start = System.currentTimeMillis(); - final QuadraticBackground backgroundModel = new QuadraticBackground(); + final FourthOrderBackground backgroundModel = new FourthOrderBackground(); final List matches = new ArrayList<>(); final Cursor cursor = Views.iterable(firstSlice).localizingCursor(); while (cursor.hasNext()) { @@ -78,4 +90,37 @@ public static void main(final String[] args) throws NotEnoughDataPointsException ImageJFunctions.show(corrected, "Corrected"); } } + + private static List readRois(final String path) throws IOException { + if (path.endsWith(".zip")) { + return extractRoisFromZip(path); + } else { + final RoiDecoder rd = new RoiDecoder(path); + final Roi roi = rd.getRoi(); + return List.of(roi); + } + } + + private static List extractRoisFromZip(final String path) throws IOException { + final List rois = new ArrayList<>(); + + try (final ZipFile zipFile = new ZipFile(path)) { + final Enumeration entries = zipFile.entries(); + + while (entries.hasMoreElements()) { + final ZipEntry entry = entries.nextElement(); + final String name = entry.getName(); + + if (name.endsWith(".roi")) { + final RoiDecoder rd = new RoiDecoder(zipFile.getInputStream(entry).readAllBytes(), entry.getName()); + final Roi roi = rd.getRoi(); + if (roi != null) { + rois.add(roi); + } + } + } + } + + return rois; + } } From 4731a558ba7305a6998a774db1de2b96ec6fd168 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 14:30:03 -0500 Subject: [PATCH 12/56] Use only points in ROI to fit model --- .../embackground/CorrectBackground.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index ba8be0b64..2b8da6f6e 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -7,11 +7,12 @@ import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; import mpicbg.models.PointMatch; -import net.imglib2.Cursor; +import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.converter.Converters; import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.position.FunctionRealRandomAccessible; import net.imglib2.type.numeric.integer.UnsignedByteType; @@ -23,8 +24,11 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -42,24 +46,35 @@ public static void main(final String[] args) throws NotEnoughDataPointsException final int width = (int) stack.dimension(0); final int height = (int) stack.dimension(1); - final int midX = width / 2; - final int midY = height / 2; + final double midX = width / 2.0; + final double midY = height / 2.0; final List rois = readRois(roiPath); if (rois.isEmpty()) { throw new IllegalArgumentException("No ROIs found in " + roiPath); } + final Set pointsOfInterest = new HashSet<>(); + for (final Roi roi : rois) { + final java.awt.Point[] containedPoints = roi.getContainedPoints(); + Collections.addAll(pointsOfInterest, containedPoints); + } + + final Img roiImg = ArrayImgs.unsignedBytes(width, height); + final RandomAccess roiRa = roiImg.randomAccess(); + for (final java.awt.Point point : pointsOfInterest) { + roiRa.setPositionAndGet(point.x, point.y).set(255); + } + // fit a quadratic background model with all the points in the first slice final long start = System.currentTimeMillis(); final FourthOrderBackground backgroundModel = new FourthOrderBackground(); final List matches = new ArrayList<>(); - final Cursor cursor = Views.iterable(firstSlice).localizingCursor(); - while (cursor.hasNext()) { - cursor.fwd(); - final double x = (cursor.getDoublePosition(0) - midX) / width; - final double y = (cursor.getDoublePosition(1) - midY) / height; - final double z = cursor.get().getRealDouble(); + final RandomAccess ra = firstSlice.randomAccess(); + for (final java.awt.Point point : pointsOfInterest) { + final double x = (point.x - midX) / width; + final double y = (point.y - midY) / height; + final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble(); matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); } backgroundModel.fit(matches); @@ -88,6 +103,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException ImageJFunctions.show(firstSlice, "Original"); ImageJFunctions.show(materializedBackground, "Background"); ImageJFunctions.show(corrected, "Corrected"); + ImageJFunctions.show(roiImg, "ROI"); } } From fef0e151ddf7afab64f087d651d562afbe7b0b46 Mon Sep 17 00:00:00 2001 From: Stephan Preibisch Date: Wed, 6 Nov 2024 15:11:30 -0500 Subject: [PATCH 13/56] added basic layout for IJ plugin with a hack --- .../render/client/embackground/BG_Plugin.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java new file mode 100644 index 000000000..11eed30a3 --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -0,0 +1,101 @@ +package org.janelia.render.client.embackground; + +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import org.janelia.saalfeldlab.n5.N5FSReader; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; + +import ij.IJ; +import ij.ImageJ; +import ij.gui.GenericDialog; +import ij.gui.Roi; +import ij.plugin.PlugIn; +import ij.plugin.frame.RoiManager; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.view.Views; + +public class BG_Plugin implements PlugIn +{ + public static int defaultType = 0; + public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; + + @Override + public void run(String arg) + { + GenericDialog gd = new GenericDialog( "fit..." ); + + gd.addChoice( "fit_type", fitTypes, fitTypes[ defaultType ]); + gd.showDialog(); + + if (gd.wasCanceled() ) + return; + + int type = defaultType = gd.getNextChoiceIndex(); + + fit( type ); + } + + public static Roi getROIs() + { + RoiManager rm = RoiManager.getInstance(); + + if (rm == null || rm.getCount() == 0) + { + IJ.log( "please define rois ... " ); + return null; + } + + int numRois = rm.getCount(); + Roi r = null; + + return r; + } + + public static void fit( int type ) + { + IJ.log( "fitting with ... " + fitTypes[ type ] ); + } + + private static void showNonBlockingDialog() { + new Thread(() -> { + JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false); + dialog.setLayout(new FlowLayout()); + + JButton closeButton = new JButton("Run Background plugin..."); + closeButton.addActionListener(e -> { + new BG_Plugin().run( null ); + }); + dialog.add(closeButton); + + dialog.pack(); + dialog.setVisible(true); + }).start(); + } + + public static void main( String[] args ) + { + new ImageJ(); + + SwingUtilities.invokeLater(() -> + { + showNonBlockingDialog(); + }); + + // open image + String n5Path = "/Volumes/public/ForPreibisch/cerebellum-3.n5"; + + RandomAccessibleInterval img = N5Utils.open( new N5FSReader( n5Path ), "data/s4" ); + int z = 0; + img = Views.hyperSlice( img, 2, z ); + ImageJFunctions.show( img ); + + // + //new BG_Plugin().run( null ); + } +} From 111b4a94c027ce5c4112ccb85f0e92731a46840f Mon Sep 17 00:00:00 2001 From: Stephan Preibisch Date: Wed, 6 Nov 2024 15:12:59 -0500 Subject: [PATCH 14/56] add roi call --- .../org/janelia/render/client/embackground/BG_Plugin.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index 11eed30a3..9f4aa4d86 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -28,6 +28,11 @@ public class BG_Plugin implements PlugIn @Override public void run(String arg) { + Roi rois = getROIs(); + + if ( rois == null ) + return; + GenericDialog gd = new GenericDialog( "fit..." ); gd.addChoice( "fit_type", fitTypes, fitTypes[ defaultType ]); From 8e13997a387adedb5b354bebbbaec52a19a0ca26 Mon Sep 17 00:00:00 2001 From: Stephan Preibisch Date: Wed, 6 Nov 2024 15:15:43 -0500 Subject: [PATCH 15/56] small fixes --- .../org/janelia/render/client/embackground/BG_Plugin.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index 9f4aa4d86..c3b485cc5 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -57,9 +57,7 @@ public static Roi getROIs() } int numRois = rm.getCount(); - Roi r = null; - - return r; + return rm.getRoi( 0 ); } public static void fit( int type ) @@ -99,7 +97,8 @@ public static void main( String[] args ) int z = 0; img = Views.hyperSlice( img, 2, z ); ImageJFunctions.show( img ); - + + new RoiManager(); // Create a new instance // //new BG_Plugin().run( null ); } From df805d8f1a9efb0ffc6d919fbdfea8296d396198 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 16:09:18 -0500 Subject: [PATCH 16/56] Fix IDE warnings --- .../render/client/embackground/BG_Plugin.java | 73 ++++++++----------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index c3b485cc5..187588473 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -20,60 +20,56 @@ import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.view.Views; -public class BG_Plugin implements PlugIn -{ +public class BG_Plugin implements PlugIn { + public static int defaultType = 0; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; @Override - public void run(String arg) - { - Roi rois = getROIs(); + public void run(final String arg) { + final Roi rois = getROIs(); - if ( rois == null ) + if (rois == null) { return; + } - GenericDialog gd = new GenericDialog( "fit..." ); + final GenericDialog gd = new GenericDialog("fit..."); - gd.addChoice( "fit_type", fitTypes, fitTypes[ defaultType ]); + gd.addChoice("fit_type", fitTypes, fitTypes[defaultType]); gd.showDialog(); - if (gd.wasCanceled() ) + if (gd.wasCanceled()) { return; + } - int type = defaultType = gd.getNextChoiceIndex(); + defaultType = gd.getNextChoiceIndex(); + final int type = gd.getNextChoiceIndex(); - fit( type ); + fit(type); } - public static Roi getROIs() - { - RoiManager rm = RoiManager.getInstance(); + public static Roi getROIs() { + final RoiManager rm = RoiManager.getInstance(); - if (rm == null || rm.getCount() == 0) - { - IJ.log( "please define rois ... " ); + if (rm == null || rm.getCount() == 0) { + IJ.log("please define rois ... "); return null; } - int numRois = rm.getCount(); return rm.getRoi( 0 ); } - public static void fit( int type ) - { - IJ.log( "fitting with ... " + fitTypes[ type ] ); + public static void fit(final int type) { + IJ.log("fitting with ... " + fitTypes[type]); } private static void showNonBlockingDialog() { new Thread(() -> { - JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false); + final JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false); dialog.setLayout(new FlowLayout()); - JButton closeButton = new JButton("Run Background plugin..."); - closeButton.addActionListener(e -> { - new BG_Plugin().run( null ); - }); + final JButton closeButton = new JButton("Run Background plugin..."); + closeButton.addActionListener(e -> new BG_Plugin().run(null )); dialog.add(closeButton); dialog.pack(); @@ -81,25 +77,18 @@ private static void showNonBlockingDialog() { }).start(); } - public static void main( String[] args ) - { + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void main(final String[] args) { new ImageJ(); - SwingUtilities.invokeLater(() -> - { - showNonBlockingDialog(); - }); - - // open image - String n5Path = "/Volumes/public/ForPreibisch/cerebellum-3.n5"; + SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog); - RandomAccessibleInterval img = N5Utils.open( new N5FSReader( n5Path ), "data/s4" ); - int z = 0; - img = Views.hyperSlice( img, 2, z ); - ImageJFunctions.show( img ); + final String n5Path = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; + RandomAccessibleInterval img = N5Utils.open(new N5FSReader(n5Path), "data/s4"); + final int z = 0; + img = Views.hyperSlice(img, 2, z); + ImageJFunctions.show(img); - new RoiManager(); // Create a new instance - // - //new BG_Plugin().run( null ); + new RoiManager(); } } From fa439c2fc6452bcf6500e60164f0f315092a4359 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 17:58:51 -0500 Subject: [PATCH 17/56] Modularize CorrectBackground --- .../embackground/CorrectBackground.java | 145 +++++++++++------- 1 file changed, 91 insertions(+), 54 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 2b8da6f6e..e83f2fed1 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -12,10 +12,12 @@ import net.imglib2.RealRandomAccessible; import net.imglib2.converter.Converters; import net.imglib2.img.Img; -import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.position.FunctionRealRandomAccessible; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; import org.janelia.saalfeldlab.n5.N5FSReader; @@ -33,81 +35,116 @@ import java.util.zip.ZipFile; public class CorrectBackground { - private static final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; - private static final String roiPath = containerPath + "/roi-set.zip"; - private static final String dataset = "data"; - private static final int scale = 4; public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException, IOException { + final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; + final String roiPath = containerPath + "/roi-set.zip"; + final String dataset = "data"; + final int scale = 4; + try (final N5Reader reader = new N5FSReader(containerPath)) { final Img stack = N5Utils.open(reader, dataset + "/s" + scale); final RandomAccessibleInterval firstSlice = Views.hyperSlice(stack, 2, 0); - - final int width = (int) stack.dimension(0); - final int height = (int) stack.dimension(1); - - final double midX = width / 2.0; - final double midY = height / 2.0; - final List rois = readRois(roiPath); - if (rois.isEmpty()) { - throw new IllegalArgumentException("No ROIs found in " + roiPath); - } - - final Set pointsOfInterest = new HashSet<>(); - for (final Roi roi : rois) { - final java.awt.Point[] containedPoints = roi.getContainedPoints(); - Collections.addAll(pointsOfInterest, containedPoints); - } - final Img roiImg = ArrayImgs.unsignedBytes(width, height); - final RandomAccess roiRa = roiImg.randomAccess(); - for (final java.awt.Point point : pointsOfInterest) { - roiRa.setPositionAndGet(point.x, point.y).set(255); + if (rois.isEmpty()) { + throw new IllegalArgumentException("No ROIs found"); } - // fit a quadratic background model with all the points in the first slice final long start = System.currentTimeMillis(); - final FourthOrderBackground backgroundModel = new FourthOrderBackground(); - final List matches = new ArrayList<>(); - final RandomAccess ra = firstSlice.randomAccess(); - for (final java.awt.Point point : pointsOfInterest) { - final double x = (point.x - midX) / width; - final double y = (point.y - midY) / height; - final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble(); - matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); - } - backgroundModel.fit(matches); + final FourthOrderBackground backgroundModel = fitBackgroundModel(rois, firstSlice); System.out.println("Fitted background model: " + backgroundModel); System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); - // we assume that the model is concave, so the offset is the maximum value - final double maxValue = backgroundModel.getCoefficients()[0]; + final RandomAccessibleInterval background = createBackgroundImage(backgroundModel, firstSlice); + final RandomAccessibleInterval corrected = correctBackground(firstSlice, background, new UnsignedByteType()); - // create a background image from the model - final double[] location = new double[2]; - final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { - location[0] = (pos.getDoublePosition(0) - midX) / width; - location[1] = (pos.getDoublePosition(1) - midY) / height; - backgroundModel.applyInPlace(location); - value.setReal(location[0] / maxValue); - }, FloatType::new); + new ImageJ(); + ImageJFunctions.show(firstSlice, "Original"); + ImageJFunctions.show(background, "Background"); + ImageJFunctions.show(corrected, "Corrected"); + } + } - final RandomAccessibleInterval materializedBackground = Views.interval(Views.raster(background), firstSlice); + @SuppressWarnings({"rawtypes", "unchecked"}) + public static & RealType> + RandomAccessibleInterval correctBackground(final RandomAccessibleInterval slice, final RandomAccessibleInterval background, final T type) { - final RandomAccessibleInterval corrected = Converters.convert(firstSlice, materializedBackground, (s, b, o) -> { + final RandomAccessibleInterval corrected; + if (type instanceof UnsignedByteType) { + corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() / b.getRealDouble()))); }, new UnsignedByteType()); + } else if (type instanceof UnsignedShortType) { + corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { + o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() / b.getRealDouble()))); + }, new UnsignedShortType()); + } else { + throw new IllegalArgumentException("Unsupported type: " + type.getClass()); + } - new ImageJ(); - ImageJFunctions.show(firstSlice, "Original"); - ImageJFunctions.show(materializedBackground, "Background"); - ImageJFunctions.show(corrected, "Corrected"); - ImageJFunctions.show(roiImg, "ROI"); + return corrected; + } + + + public static & RealType> + RandomAccessibleInterval createBackgroundImage(final FourthOrderBackground backgroundModel, final RandomAccessibleInterval slice) { + final int width = (int) slice.dimension(0); + final int height = (int) slice.dimension(1); + + final double midX = width / 2.0; + final double midY = height / 2.0; + + // we assume that the model is concave, so the offset is the maximum value + final double maxValue = backgroundModel.getCoefficients()[0]; + + final double[] location = new double[2]; + final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { + location[0] = (pos.getDoublePosition(0) - midX) / width; + location[1] = (pos.getDoublePosition(1) - midY) / height; + backgroundModel.applyInPlace(location); + value.setReal(location[0] / maxValue); + }, FloatType::new); + + return Views.interval(Views.raster(background), slice); + } + + public static & RealType> + FourthOrderBackground fitBackgroundModel(final List rois, final RandomAccessibleInterval slice) + throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final int width = (int) slice.dimension(0); + final int height = (int) slice.dimension(1); + + final double midX = width / 2.0; + final double midY = height / 2.0; + + // fit a quadratic background model with all the points in the first slice + final FourthOrderBackground backgroundModel = new FourthOrderBackground(); + final List matches = new ArrayList<>(); + final RandomAccess ra = slice.randomAccess(); + + for (final java.awt.Point point : extractInterestPoints(rois)) { + final double x = (point.x - midX) / width; + final double y = (point.y - midY) / height; + final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble(); + matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); + } + backgroundModel.fit(matches); + + return backgroundModel; + } + + private static Set extractInterestPoints(final List rois) { + + final Set pointsOfInterest = new HashSet<>(); + for (final Roi roi : rois) { + final java.awt.Point[] containedPoints = roi.getContainedPoints(); + Collections.addAll(pointsOfInterest, containedPoints); } + return pointsOfInterest; } - private static List readRois(final String path) throws IOException { + public static List readRois(final String path) throws IOException { if (path.endsWith(".zip")) { return extractRoisFromZip(path); } else { From 77db485e91580398d023fc5060576c5a47dfd182 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 18:23:22 -0500 Subject: [PATCH 18/56] Extract abstract BackgroundModel super class --- .../client/embackground/BackgroundModel.java | 109 ++++++++++++++++ .../embackground/CorrectBackground.java | 4 +- .../embackground/FourthOrderBackground.java | 117 ++++-------------- .../embackground/QuadraticBackground.java | 104 +++------------- 4 files changed, 154 insertions(+), 180 deletions(-) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java new file mode 100644 index 000000000..e82b12931 --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java @@ -0,0 +1,109 @@ +package org.janelia.render.client.embackground; + +import mpicbg.models.AbstractModel; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.PointMatch; +import org.ejml.data.DMatrixRMaj; +import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; +import org.ejml.interfaces.linsol.LinearSolverDense; + +import java.util.Collection; +import java.util.List; + + +/** + * An abstract base model for background correction in 2D slices of EM data. + */ +public abstract class BackgroundModel> extends AbstractModel { + + private final double[] coefficients; + + protected abstract int nCoefficients(); + protected abstract void fillRowA(final double[] rowA, final double x, final double y); + protected abstract List coefficientNames(); + + + public BackgroundModel() { + coefficients = new double[nCoefficients()]; + } + + public BackgroundModel(final double[] coefficients) { + this(); + System.arraycopy(coefficients, 0, this.coefficients, 0, nCoefficients()); + } + + public double[] getCoefficients() { + return coefficients; + } + + @Override + public int getMinNumMatches() { + return nCoefficients(); + } + + @Override + public

void fit(final Collection

matches) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + final DMatrixRMaj ATA = new DMatrixRMaj(nCoefficients(), nCoefficients(), true, new double[nCoefficients() * nCoefficients()]); + final DMatrixRMaj ATb = new DMatrixRMaj(nCoefficients(), 1); + + final double[] rowA = new double[nCoefficients()]; + + for (final P match : matches) { + final double x = match.getP1().getL()[0]; + final double y = match.getP1().getL()[1]; + final double z = match.getP2().getL()[0]; + + // compute one row of the least-squares matrix A + fillRowA(rowA, x, y); + + // update upper triangle of A^T * A + for (int i = 0; i < nCoefficients(); i++) { + for (int j = i; j < nCoefficients(); j++) { + ATA.data[i * nCoefficients() + j] += rowA[i] * rowA[j]; + } + } + + // update right-hand side A^T * b + for (int i = 0; i < nCoefficients(); i++) { + ATb.data[i] += rowA[i] * z; + } + } + + // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) + final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(nCoefficients()); + solver.setA(ATA); + + // coefficients are modified in place + final DMatrixRMaj x = new DMatrixRMaj(nCoefficients(), 1); + x.setData(coefficients); + solver.solve(ATb, x); + } + + @Override + public double[] apply(final double[] location) { + final double[] result = location.clone(); + applyInPlace(result); + return result; + } + + @Override + public void set(final T model) { + System.arraycopy(model.getCoefficients(), 0, coefficients, 0, nCoefficients()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BackgroundModel{ "); + + for (int i = nCoefficients() - 1; i > 0; i--) { + sb.append(coefficients[i]) + .append(" ") + .append(coefficientNames().get(i)) + .append(" + "); + } + + sb.append(coefficients[0]); + return sb.append("}").toString(); + } +} diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index e83f2fed1..0662ca528 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -52,7 +52,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } final long start = System.currentTimeMillis(); - final FourthOrderBackground backgroundModel = fitBackgroundModel(rois, firstSlice); + final BackgroundModel backgroundModel = fitBackgroundModel(rois, firstSlice); System.out.println("Fitted background model: " + backgroundModel); System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); @@ -88,7 +88,7 @@ RandomAccessibleInterval correctBackground(final RandomAccessibleInterval public static & RealType> - RandomAccessibleInterval createBackgroundImage(final FourthOrderBackground backgroundModel, final RandomAccessibleInterval slice) { + RandomAccessibleInterval createBackgroundImage(final BackgroundModel backgroundModel, final RandomAccessibleInterval slice) { final int width = (int) slice.dimension(0); final int height = (int) slice.dimension(1); diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java index f0ced78bf..6cf8d9678 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java @@ -1,107 +1,50 @@ package org.janelia.render.client.embackground; -import mpicbg.models.AbstractModel; -import mpicbg.models.IllDefinedDataPointsException; -import mpicbg.models.NotEnoughDataPointsException; -import mpicbg.models.PointMatch; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; - -import java.util.Arrays; -import java.util.Collection; +import java.util.List; /** * A 4th order model for background correction in 2D slices of EM data. * To ensure that the model is concave, no 3rd order terms are included. */ -public class FourthOrderBackground extends AbstractModel { - - private final int N_COEFFICIENTS = 9; - private final double[] coefficients = new double[N_COEFFICIENTS]; - +public class FourthOrderBackground extends BackgroundModel { public FourthOrderBackground() { - Arrays.fill(coefficients, 0); + super(); } public FourthOrderBackground(final double[] coefficients) { - System.arraycopy(coefficients, 0, this.coefficients, 0, N_COEFFICIENTS); + super(coefficients); } public FourthOrderBackground(final FourthOrderBackground background) { - this(background.coefficients); - } - - @Override - public int getMinNumMatches() { - return N_COEFFICIENTS; + super(background.getCoefficients()); } - @Override - public

void fit(final Collection

matches) throws NotEnoughDataPointsException, IllDefinedDataPointsException { - final DMatrixRMaj ATA = new DMatrixRMaj(N_COEFFICIENTS, N_COEFFICIENTS, true, new double[N_COEFFICIENTS * N_COEFFICIENTS]); - final DMatrixRMaj ATb = new DMatrixRMaj(N_COEFFICIENTS, 1); - - final double[] rowA = new double[N_COEFFICIENTS]; - - for (final P match : matches) { - final double x = match.getP1().getL()[0]; - final double y = match.getP1().getL()[1]; - final double z = match.getP2().getL()[0]; - - // compute one row of the least-squares matrix A - final double xx = x * x; - final double yy = y * y; - rowA[0] = 1; - rowA[1] = y; - rowA[2] = x; - rowA[3] = yy; - rowA[4] = x * y; - rowA[5] = xx; - rowA[6] = yy * yy; - rowA[7] = xx * yy; - rowA[8] = xx * xx; - - // update upper triangle of A^T * A - for (int i = 0; i < N_COEFFICIENTS; i++) { - for (int j = i; j < N_COEFFICIENTS; j++) { - ATA.data[i * N_COEFFICIENTS + j] += rowA[i] * rowA[j]; - } - } - - // update right-hand side A^T * b - for (int i = 0; i < N_COEFFICIENTS; i++) { - ATb.data[i] += rowA[i] * z; - } - } - - // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) - final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(N_COEFFICIENTS); - solver.setA(ATA); - - // coefficients are modified in place - final DMatrixRMaj x = new DMatrixRMaj(N_COEFFICIENTS, 1); - x.setData(coefficients); - solver.solve(ATb, x); - } @Override - public void set(final FourthOrderBackground quadraticBackground) { - + protected int nCoefficients() { + return 9; } @Override - public FourthOrderBackground copy() { - return null; + protected void fillRowA(final double[] rowA, final double x, final double y) { + final double xx = x * x; + final double yy = y * y; + rowA[0] = 1; + rowA[1] = y; + rowA[2] = x; + rowA[3] = yy; + rowA[4] = x * y; + rowA[5] = xx; + rowA[6] = yy * yy; + rowA[7] = xx * yy; + rowA[8] = xx * xx; } @Override - public double[] apply(final double[] location) { - final double[] result = location.clone(); - applyInPlace(result); - return result; + protected List coefficientNames() { + return List.of("x^4", "x^2 y^2", "y^4", "x^2", "xy", "y^2", "x", "y", "1"); } @Override @@ -110,6 +53,7 @@ public void applyInPlace(final double[] location) { final double y = location[1]; final double xx = x * x; final double yy = y * y; + final double[] coefficients = getCoefficients(); final double result = coefficients[8] * xx * xx + coefficients[7] * xx * yy + coefficients[6] * yy * yy @@ -123,21 +67,8 @@ public void applyInPlace(final double[] location) { location[1] = 0.0; } - public double[] getCoefficients() { - return coefficients; - } - @Override - public String toString() { - return "FourthOrderBackground{ " - + coefficients[8] + " x^4 + " - + coefficients[7] + " x^2 y^2 + " - + coefficients[6] + " y^4 + " - + coefficients[5] + " x^2 + " - + coefficients[4] + " xy + " - + coefficients[3] + " y^2 + " - + coefficients[2] + " x + " - + coefficients[1] + " y + " - + coefficients[0] + " }"; + public FourthOrderBackground copy() { + return new FourthOrderBackground(this); } } diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java index eac621ac8..38da87e47 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java @@ -1,107 +1,51 @@ package org.janelia.render.client.embackground; -import mpicbg.models.AbstractModel; -import mpicbg.models.IllDefinedDataPointsException; -import mpicbg.models.NotEnoughDataPointsException; -import mpicbg.models.PointMatch; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; - -import java.util.Arrays; -import java.util.Collection; +import java.util.List; /** * A quadratic model for background correction in 2D slices of EM data. */ -public class QuadraticBackground extends AbstractModel { - - private final int N_COEFFICIENTS = 6; - private final double[] coefficients = new double[N_COEFFICIENTS]; - +public class QuadraticBackground extends BackgroundModel { public QuadraticBackground() { - Arrays.fill(coefficients, 0); + super(); } public QuadraticBackground(final double[] coefficients) { - System.arraycopy(coefficients, 0, this.coefficients, 0, N_COEFFICIENTS); + super(coefficients); } public QuadraticBackground(final QuadraticBackground background) { - this(background.coefficients); - } - - @Override - public int getMinNumMatches() { - return N_COEFFICIENTS; + super(background.getCoefficients()); } - @Override - public

void fit(final Collection

matches) throws NotEnoughDataPointsException, IllDefinedDataPointsException { - final DMatrixRMaj ATA = new DMatrixRMaj(N_COEFFICIENTS, N_COEFFICIENTS, true, new double[N_COEFFICIENTS * N_COEFFICIENTS]); - final DMatrixRMaj ATb = new DMatrixRMaj(N_COEFFICIENTS, 1); - - final double[] rowA = new double[N_COEFFICIENTS]; - - for (final P match : matches) { - final double x = match.getP1().getL()[0]; - final double y = match.getP1().getL()[1]; - final double z = match.getP2().getL()[0]; - - // compute one row of the least-squares matrix A - rowA[0] = 1; - rowA[1] = y; - rowA[2] = x; - rowA[3] = y * y; - rowA[4] = x * y; - rowA[5] = x * x; - - // update upper triangle of A^T * A - for (int i = 0; i < N_COEFFICIENTS; i++) { - for (int j = i; j < N_COEFFICIENTS; j++) { - ATA.data[i * N_COEFFICIENTS + j] += rowA[i] * rowA[j]; - } - } - - // update right-hand side A^T * b - for (int i = 0; i < N_COEFFICIENTS; i++) { - ATb.data[i] += rowA[i] * z; - } - } - - // set up Cholesky decomposition for A^T * A x = A^T * b (only upper triangle of A^T * A is used) - final LinearSolverDense solver = LinearSolverFactory_DDRM.chol(N_COEFFICIENTS); - solver.setA(ATA); - - // coefficients are modified in place - final DMatrixRMaj x = new DMatrixRMaj(N_COEFFICIENTS, 1); - x.setData(coefficients); - solver.solve(ATb, x); - } @Override - public void set(final QuadraticBackground quadraticBackground) { - + protected int nCoefficients() { + return 6; } @Override - public QuadraticBackground copy() { - return null; + protected void fillRowA(final double[] rowA, final double x, final double y) { + rowA[0] = 1; + rowA[1] = y; + rowA[2] = x; + rowA[3] = y * y; + rowA[4] = x * y; + rowA[5] = x * x; } @Override - public double[] apply(final double[] location) { - final double[] result = location.clone(); - applyInPlace(result); - return result; + protected List coefficientNames() { + return List.of("x^2", "xy", "y^2", "x", "y", "1"); } @Override public void applyInPlace(final double[] location) { final double x = location[0]; final double y = location[1]; + final double[] coefficients = getCoefficients(); final double result = coefficients[5] * x * x + coefficients[4] * x * y + coefficients[3] * y * y @@ -112,18 +56,8 @@ public void applyInPlace(final double[] location) { location[1] = 0.0; } - public double[] getCoefficients() { - return coefficients; - } - @Override - public String toString() { - return "QuadraticBackground{ " - + coefficients[5] + " x^2 + " - + coefficients[4] + " xy + " - + coefficients[3] + " y^2 + " - + coefficients[2] + " x + " - + coefficients[1] + " y + " - + coefficients[0] + " }"; + public QuadraticBackground copy() { + return new QuadraticBackground(this); } } From de6a040b8b3ed3ea7ca15c7fb361c8ea16358cf1 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 18:25:43 -0500 Subject: [PATCH 19/56] Make model fit generic --- .../render/client/embackground/CorrectBackground.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 0662ca528..d1f9b1b59 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -52,7 +52,8 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } final long start = System.currentTimeMillis(); - final BackgroundModel backgroundModel = fitBackgroundModel(rois, firstSlice); + final BackgroundModel backgroundModel = new FourthOrderBackground(); + fitBackgroundModel(rois, firstSlice, backgroundModel); System.out.println("Fitted background model: " + backgroundModel); System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); @@ -110,7 +111,7 @@ RandomAccessibleInterval createBackgroundImage(final BackgroundModel< } public static & RealType> - FourthOrderBackground fitBackgroundModel(final List rois, final RandomAccessibleInterval slice) + void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final BackgroundModel backgroundModel) throws NotEnoughDataPointsException, IllDefinedDataPointsException { final int width = (int) slice.dimension(0); final int height = (int) slice.dimension(1); @@ -119,7 +120,6 @@ FourthOrderBackground fitBackgroundModel(final List rois, final RandomAcces final double midY = height / 2.0; // fit a quadratic background model with all the points in the first slice - final FourthOrderBackground backgroundModel = new FourthOrderBackground(); final List matches = new ArrayList<>(); final RandomAccess ra = slice.randomAccess(); @@ -130,8 +130,6 @@ FourthOrderBackground fitBackgroundModel(final List rois, final RandomAcces matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); } backgroundModel.fit(matches); - - return backgroundModel; } private static Set extractInterestPoints(final List rois) { From 863bc3943b6fea72641e718aa29cd9bd626791ca Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 18:40:08 -0500 Subject: [PATCH 20/56] Make plugin actually compute background --- .../render/client/embackground/BG_Plugin.java | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index 187588473..bec18ae4f 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -1,12 +1,18 @@ package org.janelia.render.client.embackground; import java.awt.FlowLayout; +import java.util.Arrays; +import java.util.List; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.SwingUtilities; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.real.FloatType; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; @@ -25,42 +31,71 @@ public class BG_Plugin implements PlugIn { public static int defaultType = 0; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; + public static RandomAccessibleInterval img; + @Override public void run(final String arg) { - final Roi rois = getROIs(); + final List rois = getROIs(); - if (rois == null) { + if (rois == null || rois.isEmpty()) { + IJ.log("No ROIs specified."); return; } - final GenericDialog gd = new GenericDialog("fit..."); + final GenericDialog gd = new GenericDialog("Fit background correction"); - gd.addChoice("fit_type", fitTypes, fitTypes[defaultType]); + gd.addChoice("Fit type", fitTypes, fitTypes[defaultType]); gd.showDialog(); if (gd.wasCanceled()) { return; } - defaultType = gd.getNextChoiceIndex(); final int type = gd.getNextChoiceIndex(); + defaultType = type; - fit(type); + try { + fit(type, rois); + } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { + IJ.log("Fitting failed: " + e.getMessage()); + } } - public static Roi getROIs() { + public static List getROIs() { final RoiManager rm = RoiManager.getInstance(); if (rm == null || rm.getCount() == 0) { - IJ.log("please define rois ... "); + IJ.log("Please define ROIs first before running background correction."); return null; } - return rm.getRoi( 0 ); + return Arrays.asList(rm.getRoisAsArray()); } - public static void fit(final int type) { - IJ.log("fitting with ... " + fitTypes[type]); + public static void fit(final int type, final List rois) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + IJ.log("Fitting with " + fitTypes[type] + " model..."); + + final BackgroundModel backgroundModel; + if (fitTypes[type].equals("Quadratic")) { + backgroundModel = new QuadraticBackground(); + } else if (fitTypes[type].equals("Fourth Order")) { + backgroundModel = new FourthOrderBackground(); + } else { + throw new IllegalArgumentException("Unknown fit type: " + fitTypes[type]); + } + + final long start = System.currentTimeMillis(); + CorrectBackground.fitBackgroundModel(rois, img, backgroundModel); + IJ.log("Fitted background model: " + backgroundModel); + IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms."); + IJ.log("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients())); + + final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img); + final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType()); + + ImageJFunctions.show(background, "Background"); + ImageJFunctions.show(corrected, "Corrected"); + } private static void showNonBlockingDialog() { @@ -77,14 +112,13 @@ private static void showNonBlockingDialog() { }).start(); } - @SuppressWarnings({"unchecked", "rawtypes"}) public static void main(final String[] args) { new ImageJ(); SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog); final String n5Path = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; - RandomAccessibleInterval img = N5Utils.open(new N5FSReader(n5Path), "data/s4"); + img = N5Utils.open(new N5FSReader(n5Path), "data/s4"); final int z = 0; img = Views.hyperSlice(img, 2, z); ImageJFunctions.show(img); From ee0e584a9f758f7e341eadc0789ecc6efdff80b1 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 6 Nov 2024 18:51:56 -0500 Subject: [PATCH 21/56] Add option to show background image or not --- .../render/client/embackground/BG_Plugin.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index bec18ae4f..77df41bc1 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -29,6 +29,7 @@ public class BG_Plugin implements PlugIn { public static int defaultType = 0; + public static boolean defaultShowBackground = false; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; public static RandomAccessibleInterval img; @@ -45,6 +46,7 @@ public void run(final String arg) { final GenericDialog gd = new GenericDialog("Fit background correction"); gd.addChoice("Fit type", fitTypes, fitTypes[defaultType]); + gd.addCheckbox("Show background image", defaultShowBackground); gd.showDialog(); if (gd.wasCanceled()) { @@ -54,8 +56,11 @@ public void run(final String arg) { final int type = gd.getNextChoiceIndex(); defaultType = type; + final boolean showBackground = gd.getNextBoolean(); + defaultShowBackground = showBackground; + try { - fit(type, rois); + fit(type, rois, showBackground); } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { IJ.log("Fitting failed: " + e.getMessage()); } @@ -72,7 +77,7 @@ public static List getROIs() { return Arrays.asList(rm.getRoisAsArray()); } - public static void fit(final int type, final List rois) throws NotEnoughDataPointsException, IllDefinedDataPointsException { + public static void fit(final int type, final List rois, final boolean showBackground) throws NotEnoughDataPointsException, IllDefinedDataPointsException { IJ.log("Fitting with " + fitTypes[type] + " model..."); final BackgroundModel backgroundModel; @@ -93,7 +98,9 @@ public static void fit(final int type, final List rois) throws NotEnoughDat final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img); final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType()); - ImageJFunctions.show(background, "Background"); + if (showBackground) { + ImageJFunctions.show(background, "Background"); + } ImageJFunctions.show(corrected, "Corrected"); } From 837b44aa17b470bc7e7e482297891dc302ac2504 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 11:34:43 -0500 Subject: [PATCH 22/56] Fix and improve BackgroundModel::toString --- .../client/embackground/BackgroundModel.java | 19 ++++++++++++++----- .../embackground/FourthOrderBackground.java | 2 +- .../embackground/QuadraticBackground.java | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java index e82b12931..5bf922d0e 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java @@ -94,16 +94,25 @@ public void set(final T model) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("BackgroundModel{ "); + final StringBuilder sb = new StringBuilder("BackgroundModel{"); + + if (coefficients[nCoefficients() - 1] < 0) { + sb.append("-"); + } for (int i = nCoefficients() - 1; i > 0; i--) { - sb.append(coefficients[i]) + sb.append(String.format("%.2f", Math.abs(coefficients[i]))) .append(" ") - .append(coefficientNames().get(i)) - .append(" + "); + .append(coefficientNames().get(i)); + + if (coefficients[i - 1] >= 0) { + sb.append(" + "); + } else { + sb.append(" - "); + } } - sb.append(coefficients[0]); + sb.append(String.format("%.2f", Math.abs(coefficients[0]))); return sb.append("}").toString(); } } diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java index 6cf8d9678..d03278928 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java @@ -44,7 +44,7 @@ protected void fillRowA(final double[] rowA, final double x, final double y) { @Override protected List coefficientNames() { - return List.of("x^4", "x^2 y^2", "y^4", "x^2", "xy", "y^2", "x", "y", "1"); + return List.of("1", "y", "x", "y^2", "xy", "x^2", "y^4", "x^2 y^2", "x^4"); } @Override diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java index 38da87e47..b51795315 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java @@ -38,7 +38,7 @@ protected void fillRowA(final double[] rowA, final double x, final double y) { @Override protected List coefficientNames() { - return List.of("x^2", "xy", "y^2", "x", "y", "1"); + return List.of("1", "y", "x", "y^2", "xy", "x^2"); } @Override From 51e95e4eb75637a292a8747de47cd36463090b6a Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 11:50:53 -0500 Subject: [PATCH 23/56] Add normalization and correct background additively --- .../client/embackground/BackgroundModel.java | 23 +++++++++++++ .../embackground/CorrectBackground.java | 34 ++++++++----------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java index 5bf922d0e..4add63c4c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java @@ -37,6 +37,29 @@ public double[] getCoefficients() { return coefficients; } + /** + * Normalize the model so that it approximately integrates to zero over [-1, 1] x [-1, 1]. + * This assumes that the constant term is stored in the first coefficient. + */ + public void normalize() { + final int N = 10 * nCoefficients(); + final double[] location = new double[2]; + double avg = 0; + + // approximate integral over [-1, 1] x [-1, 1] by averaging the function values at a grid of points + for (int i = 0; i < N; i++) { + for (int j = 0; j < N; j++) { + location[0] = 2.0 * i / N - 1.0; + location[1] = 2.0 * j / N - 1.0; + applyInPlace(location); + avg += location[0]; + } + } + + avg /= (N * N); + coefficients[0] -= avg; + } + @Override public int getMinNumMatches() { return nCoefficients(); diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index d1f9b1b59..2b57cf9dc 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -37,6 +37,7 @@ public class CorrectBackground { public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException, IOException { + final String containerPath = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; final String roiPath = containerPath + "/roi-set.zip"; final String dataset = "data"; @@ -74,11 +75,11 @@ RandomAccessibleInterval correctBackground(final RandomAccessibleInterval final RandomAccessibleInterval corrected; if (type instanceof UnsignedByteType) { corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { - o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() / b.getRealDouble()))); + o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() - b.getRealDouble()))); }, new UnsignedByteType()); } else if (type instanceof UnsignedShortType) { corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { - o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() / b.getRealDouble()))); + o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() - b.getRealDouble()))); }, new UnsignedShortType()); } else { throw new IllegalArgumentException("Unsupported type: " + type.getClass()); @@ -87,24 +88,19 @@ RandomAccessibleInterval correctBackground(final RandomAccessibleInterval return corrected; } - public static & RealType> RandomAccessibleInterval createBackgroundImage(final BackgroundModel backgroundModel, final RandomAccessibleInterval slice) { - final int width = (int) slice.dimension(0); - final int height = (int) slice.dimension(1); - - final double midX = width / 2.0; - final double midY = height / 2.0; - // we assume that the model is concave, so the offset is the maximum value - final double maxValue = backgroundModel.getCoefficients()[0]; + // transform pixel coordinates into [-1, 1] x [-1, 1] + final double scaleX = slice.dimension(0) / 2.0; + final double scaleY = slice.dimension(1) / 2.0; final double[] location = new double[2]; final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { - location[0] = (pos.getDoublePosition(0) - midX) / width; - location[1] = (pos.getDoublePosition(1) - midY) / height; + location[0] = (pos.getDoublePosition(0) - scaleX) / scaleX; + location[1] = (pos.getDoublePosition(1) - scaleY) / scaleY; backgroundModel.applyInPlace(location); - value.setReal(location[0] / maxValue); + value.setReal(location[0]); }, FloatType::new); return Views.interval(Views.raster(background), slice); @@ -113,23 +109,23 @@ RandomAccessibleInterval createBackgroundImage(final BackgroundModel< public static & RealType> void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final BackgroundModel backgroundModel) throws NotEnoughDataPointsException, IllDefinedDataPointsException { - final int width = (int) slice.dimension(0); - final int height = (int) slice.dimension(1); - final double midX = width / 2.0; - final double midY = height / 2.0; + // transform pixel coordinates into [-1, 1] x [-1, 1] + final double scaleX = slice.dimension(0) / 2.0; + final double scaleY = slice.dimension(1) / 2.0; // fit a quadratic background model with all the points in the first slice final List matches = new ArrayList<>(); final RandomAccess ra = slice.randomAccess(); for (final java.awt.Point point : extractInterestPoints(rois)) { - final double x = (point.x - midX) / width; - final double y = (point.y - midY) / height; + final double x = (point.x - scaleX) / scaleX; + final double y = (point.y - scaleY) / scaleY; final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble(); matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); } backgroundModel.fit(matches); + backgroundModel.normalize(); } private static Set extractInterestPoints(final List rois) { From 4c94b426398604a4cd46e56f2de9f17d02e0bc1f Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 11:53:15 -0500 Subject: [PATCH 24/56] Reorder methods in CorrectBackground --- .../embackground/CorrectBackground.java | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java index 2b57cf9dc..f6fe61e12 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java @@ -68,44 +68,6 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } } - @SuppressWarnings({"rawtypes", "unchecked"}) - public static & RealType> - RandomAccessibleInterval correctBackground(final RandomAccessibleInterval slice, final RandomAccessibleInterval background, final T type) { - - final RandomAccessibleInterval corrected; - if (type instanceof UnsignedByteType) { - corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { - o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() - b.getRealDouble()))); - }, new UnsignedByteType()); - } else if (type instanceof UnsignedShortType) { - corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { - o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() - b.getRealDouble()))); - }, new UnsignedShortType()); - } else { - throw new IllegalArgumentException("Unsupported type: " + type.getClass()); - } - - return corrected; - } - - public static & RealType> - RandomAccessibleInterval createBackgroundImage(final BackgroundModel backgroundModel, final RandomAccessibleInterval slice) { - - // transform pixel coordinates into [-1, 1] x [-1, 1] - final double scaleX = slice.dimension(0) / 2.0; - final double scaleY = slice.dimension(1) / 2.0; - - final double[] location = new double[2]; - final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { - location[0] = (pos.getDoublePosition(0) - scaleX) / scaleX; - location[1] = (pos.getDoublePosition(1) - scaleY) / scaleY; - backgroundModel.applyInPlace(location); - value.setReal(location[0]); - }, FloatType::new); - - return Views.interval(Views.raster(background), slice); - } - public static & RealType> void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final BackgroundModel backgroundModel) throws NotEnoughDataPointsException, IllDefinedDataPointsException { @@ -138,6 +100,44 @@ private static Set extractInterestPoints(final List rois) { return pointsOfInterest; } + public static & RealType> + RandomAccessibleInterval createBackgroundImage(final BackgroundModel backgroundModel, final RandomAccessibleInterval slice) { + + // transform pixel coordinates into [-1, 1] x [-1, 1] + final double scaleX = slice.dimension(0) / 2.0; + final double scaleY = slice.dimension(1) / 2.0; + + final double[] location = new double[2]; + final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { + location[0] = (pos.getDoublePosition(0) - scaleX) / scaleX; + location[1] = (pos.getDoublePosition(1) - scaleY) / scaleY; + backgroundModel.applyInPlace(location); + value.setReal(location[0]); + }, FloatType::new); + + return Views.interval(Views.raster(background), slice); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static & RealType> + RandomAccessibleInterval correctBackground(final RandomAccessibleInterval slice, final RandomAccessibleInterval background, final T type) { + + final RandomAccessibleInterval corrected; + if (type instanceof UnsignedByteType) { + corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { + o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() - b.getRealDouble()))); + }, new UnsignedByteType()); + } else if (type instanceof UnsignedShortType) { + corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { + o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() - b.getRealDouble()))); + }, new UnsignedShortType()); + } else { + throw new IllegalArgumentException("Unsupported type: " + type.getClass()); + } + + return corrected; + } + public static List readRois(final String path) throws IOException { if (path.endsWith(".zip")) { return extractRoisFromZip(path); From ed7fcea0b6638b576661760ce50ba8a95e2d7042 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 14:15:17 -0500 Subject: [PATCH 25/56] Make BG_Plugin use command line parameters --- .../render/client/embackground/BG_Plugin.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index 77df41bc1..7c05d4d13 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -9,10 +9,12 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; +import com.beust.jcommander.Parameter; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.real.FloatType; +import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; @@ -28,6 +30,24 @@ public class BG_Plugin implements PlugIn { + private static class Parameters extends CommandLineParameters { + @Parameter(names = "--n5Path", + description = "Path to the N5 container", + required = true) + public String n5Path; + + @Parameter(names = "--dataset", + description = "Name of the dataset", + required = true) + public String dataset; + + @Parameter(names = "--z", + description = "Z slice to process", + required = true) + public int z; + } + + public static int defaultType = 0; public static boolean defaultShowBackground = false; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; @@ -120,14 +140,16 @@ private static void showNonBlockingDialog() { } public static void main(final String[] args) { - new ImageJ(); + final Parameters params = new Parameters(); + params.parse(args); + new ImageJ(); SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog); - final String n5Path = System.getenv("HOME") + "/big-data/render-exports/cerebellum-3.n5"; - img = N5Utils.open(new N5FSReader(n5Path), "data/s4"); - final int z = 0; - img = Views.hyperSlice(img, 2, z); + img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset); + if (img.numDimensions() > 2) { + img = Views.hyperSlice(img, 2, params.z); + } ImageJFunctions.show(img); new RoiManager(); From 8e5290f5aeb8976812679569d80f3190af0d461a Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 15:02:20 -0500 Subject: [PATCH 26/56] Add skeleton for BackgroundCorrection spark client --- .../BackgroundCorrectionClient.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java new file mode 100644 index 000000000..12a693c36 --- /dev/null +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -0,0 +1,132 @@ +package org.janelia.render.client.spark.intensityadjust; + +import com.beust.jcommander.Parameter; +import org.apache.spark.SparkConf; +import org.apache.spark.api.java.JavaRDD; +import org.apache.spark.api.java.JavaSparkContext; +import org.janelia.alignment.util.Grid; +import org.janelia.render.client.ClientRunner; +import org.janelia.render.client.embackground.BackgroundModel; +import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5FSReader; +import org.janelia.saalfeldlab.n5.N5FSWriter; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.N5Writer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Spark client for background correction by a layer-wise quadratic or fourth order model. + * The client takes as input an N5 container with a 3D dataset and a parameter file, and writes the corrected data to a + * new dataset in a given container. + *

+ * The parameter file is a json file containing a list of z values and corresponding models. The model for each z value + * is valid for all z layers starting at the given z value until the next z value in the list. + * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, + * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}. + */ +public class BackgroundCorrectionClient implements Serializable { + + public static class Parameters extends CommandLineParameters { + @Parameter(names = "--n5In", + description = "Path to the input N5 container", + required = true) + public String n5In; + + @Parameter(names = "--datasetIn", + description = "Name of the input dataset", + required = true) + public String datasetIn; + + @Parameter(names = "--n5Out", + description = "Path to the output N5 container", + required = true) + public String n5Out; + + @Parameter(names = "--datasetOut", + description = "Name of the output dataset", + required = true) + public String datasetOut; + + @Parameter(names = "--parameterFile", + description = "Path to the parameter json file containing a list of z values and their corresponding models. See class description for details.", + required = true) + public String parameterFile; + } + + public static void main(final String[] args) { + final ClientRunner clientRunner = new ClientRunner(args) { + @Override + public void runClient(final String[] args) throws Exception { + final Parameters parameters = new Parameters(); + parameters.parse(args); + final BackgroundCorrectionClient client = new BackgroundCorrectionClient(parameters); + client.run(); + } + }; + clientRunner.run(); + } + + private final Parameters parameters; + + public BackgroundCorrectionClient(final Parameters parameters) { + LOG.info("init: parameters={}", parameters); + this.parameters = parameters; + } + + public void run() throws IOException { + final SparkConf conf = new SparkConf().setAppName("BackgroundCorrectionClient"); + try (final JavaSparkContext sparkContext = new JavaSparkContext(conf)) { + final String sparkAppId = sparkContext.getConf().getAppId(); + LOG.info("run: appId is {}", sparkAppId); + runWithContext(sparkContext); + } + } + + public void runWithContext(final JavaSparkContext sparkContext) { + + LOG.info("runWithContext: entry"); + + // read parameters + final Map> models = null; + + // set up input and output N5 datasets + final DatasetAttributes inputAttributes; + try (final N5Reader in = new N5FSReader(parameters.n5In)) { + inputAttributes = in.getDatasetAttributes(parameters.datasetIn); + final Map> otherAttributes = in.listAttributes(parameters.datasetIn); + + try (final N5Writer out = new N5FSWriter(parameters.n5Out)) { + if (out.exists(parameters.datasetOut)) { + throw new IllegalArgumentException("Output dataset already exists: " + parameters.datasetOut); + } + + out.createDataset(parameters.datasetOut, inputAttributes); + out.setAttribute(parameters.datasetOut, "BackgroundCorrectionClientParameters", parameters); + otherAttributes.forEach((key, clazz) -> { + final Object attribute = in.getAttribute(parameters.datasetIn, key, clazz); + out.setAttribute(parameters.datasetOut, key, attribute); + }); + } + } + + // parallelize computation over blocks of the input/output dataset + final List blocks = Grid.create(inputAttributes.getDimensions(), inputAttributes.getBlockSize()); + final JavaRDD blockRDD = sparkContext.parallelize(blocks, blocks.size()); + blockRDD.foreach(block -> processSingleBlock(parameters, models)); + + LOG.info("runWithContext: exit"); + } + + private static void processSingleBlock(final Parameters parameters, final Map> models) { + + } + + private static final Logger LOG = LoggerFactory.getLogger(BackgroundCorrectionClient.class); +} From 824359701b3ef7cb09bd26a34052a5c05f344f93 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 7 Nov 2024 18:22:52 -0500 Subject: [PATCH 27/56] Store unfinished work on parameter file --- .../client/embackground/BackgroundModel.java | 3 +- .../BackgroundCorrectionClient.java | 86 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java index 4add63c4c..fbc41b47f 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java @@ -8,6 +8,7 @@ import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; import org.ejml.interfaces.linsol.LinearSolverDense; +import java.io.Serializable; import java.util.Collection; import java.util.List; @@ -15,7 +16,7 @@ /** * An abstract base model for background correction in 2D slices of EM data. */ -public abstract class BackgroundModel> extends AbstractModel { +public abstract class BackgroundModel> extends AbstractModel implements Serializable { private final double[] coefficients; diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 12a693c36..3bf575eb3 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -1,12 +1,18 @@ package org.janelia.render.client.spark.intensityadjust; import com.beust.jcommander.Parameter; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.janelia.alignment.util.Grid; import org.janelia.render.client.ClientRunner; import org.janelia.render.client.embackground.BackgroundModel; +import org.janelia.render.client.embackground.FourthOrderBackground; +import org.janelia.render.client.embackground.QuadraticBackground; import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSReader; @@ -16,8 +22,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.FileReader; import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -30,6 +38,23 @@ * is valid for all z layers starting at the given z value until the next z value in the list. * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}. + *

+ * In particular, the parameter file should have the following format. There is one root object with a single key + * "models". The value of this key is an array of objects, each with three keys: "z", "model", and "coefficients"s, e.g.: + *
+ * {
+ *    "models": [ {
+ *          "z": 0,
+ *          "model": "quadratic",
+ *          "coefficients": [1.0, 0.0, 1.0, 0.0, 0.0, 0.0]
+ *       }, {
+ *          "z": 10,
+ *          "model": "fourthOrder",
+ *          "coefficients": [1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ *       }
+ *    ]
+ * }
+ * 
*/ public class BackgroundCorrectionClient implements Serializable { @@ -89,12 +114,13 @@ public void run() throws IOException { } } - public void runWithContext(final JavaSparkContext sparkContext) { + public void runWithContext(final JavaSparkContext sparkContext) throws IOException { LOG.info("runWithContext: entry"); // read parameters - final Map> models = null; + final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromFile(parameters.parameterFile); + System.exit(0); // set up input and output N5 datasets final DatasetAttributes inputAttributes; @@ -119,7 +145,7 @@ public void runWithContext(final JavaSparkContext sparkContext) { // parallelize computation over blocks of the input/output dataset final List blocks = Grid.create(inputAttributes.getDimensions(), inputAttributes.getBlockSize()); final JavaRDD blockRDD = sparkContext.parallelize(blocks, blocks.size()); - blockRDD.foreach(block -> processSingleBlock(parameters, models)); +// blockRDD.foreach(block -> processSingleBlock(parameters, models)); LOG.info("runWithContext: exit"); } @@ -128,5 +154,59 @@ private static void processSingleBlock(final Parameters parameters, final Map zValues; + private final List> models; + + private BackgroundModelProvider(final List zValues, final List> models) { + this.zValues = zValues; + this.models = models; + } + + public BackgroundModel getModel(final int z) { + for (int i = 0; i < zValues.size() - 1; i++) { + if (z >= zValues.get(i) && z < zValues.get(i + 1)) { + return models.get(i); + } + } + return null; + } + + + public static BackgroundModelProvider fromFile(final String fileName) throws IOException { + + final List zValues = new ArrayList<>(); + final List> models = new ArrayList<>(); + + final JsonArray root; + try (final FileReader reader = new FileReader(fileName)) { + root = JsonParser.parseReader(reader).getAsJsonObject().getAsJsonArray("models"); + } + + for (final JsonElement jsonElement : root) { + final JsonObject parameterSet = jsonElement.getAsJsonObject(); + final int z = parameterSet.get("fromZ").getAsInt(); + final String modelType = parameterSet.get("model").getAsString(); + final double[] coefficients = parameterSet.getAsJsonArray("coefficients").asList().stream() + .map(JsonElement::getAsDouble) + .mapToDouble(Double::doubleValue).toArray(); + + LOG.info("Extract model from parameters file: fromZ={}, modelType={}, coefficients={}", z, modelType, coefficients); + + zValues.add(z); + if (modelType.equals("quadratic")) { + models.add(new QuadraticBackground(coefficients)); + } else if (modelType.equals("fourthOrder")) { + models.add(new FourthOrderBackground(coefficients)); + } else { + throw new IllegalArgumentException("Unknown model type: " + modelType); + } + } + + return new BackgroundModelProvider(zValues, models); + } + } + private static final Logger LOG = LoggerFactory.getLogger(BackgroundCorrectionClient.class); } From 0054565c0e24472b83c2f640f11c4cb2bcbdafca Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 8 Nov 2024 09:08:32 -0500 Subject: [PATCH 28/56] Make json deserialization nicer --- .../BackgroundCorrectionClient.java | 99 +++++++++++-------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 3bf575eb3..4fb30b7e3 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -1,9 +1,8 @@ package org.janelia.render.client.spark.intensityadjust; import com.beust.jcommander.Parameter; -import com.google.gson.JsonArray; +import com.google.gson.Gson; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; @@ -26,6 +25,9 @@ import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -39,21 +41,19 @@ * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}. *

- * In particular, the parameter file should have the following format. There is one root object with a single key - * "models". The value of this key is an array of objects, each with three keys: "z", "model", and "coefficients"s, e.g.: + * In particular, the parameter file should have the following format. There is one root array, whose elements have + * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: *
- * {
- *    "models": [ {
- *          "z": 0,
- *          "model": "quadratic",
- *          "coefficients": [1.0, 0.0, 1.0, 0.0, 0.0, 0.0]
- *       }, {
- *          "z": 10,
- *          "model": "fourthOrder",
- *          "coefficients": [1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
- *       }
- *    ]
- * }
+ * [ {
+ *     "fromZ": 1,
+ *     "modelType": "quadratic",
+ *     "coefficients": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]
+ *   }, {
+ *     "fromZ": 3,
+ *     "modelType": "fourthOrder",
+ *     "coefficients": [ 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0
+ *   }
+ * ]
  * 
*/ public class BackgroundCorrectionClient implements Serializable { @@ -119,7 +119,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti LOG.info("runWithContext: entry"); // read parameters - final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromFile(parameters.parameterFile); + final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile); System.exit(0); // set up input and output N5 datasets @@ -156,55 +156,68 @@ private static void processSingleBlock(final Parameters parameters, final Map zValues; - private final List> models; + private final List sortedModelSpecs; - private BackgroundModelProvider(final List zValues, final List> models) { - this.zValues = zValues; - this.models = models; + private BackgroundModelProvider(final List modelSpecs) { + this.sortedModelSpecs = modelSpecs; + this.sortedModelSpecs.sort(Collections.reverseOrder(Comparator.comparingInt(ModelSpec::getZ))); } public BackgroundModel getModel(final int z) { - for (int i = 0; i < zValues.size() - 1; i++) { - if (z >= zValues.get(i) && z < zValues.get(i + 1)) { - return models.get(i); + for (final ModelSpec modelSpec : sortedModelSpecs) { + if (z >= modelSpec.getZ()) { + return modelSpec.getModel(); } } return null; } + public static BackgroundModelProvider fromJsonFile(final String fileName) throws IOException { + LOG.info("Reading model specs from file: {}", fileName); + try (final FileReader reader = new FileReader(fileName)) { + return fromJson(JsonParser.parseReader(reader)); + } + } - public static BackgroundModelProvider fromFile(final String fileName) throws IOException { + public static BackgroundModelProvider fromJson(final JsonElement jsonData) throws IOException { - final List zValues = new ArrayList<>(); - final List> models = new ArrayList<>(); + final List modelSpecs = new ArrayList<>(); + Collections.addAll(modelSpecs, new Gson().fromJson(jsonData, ModelSpec[].class)); - final JsonArray root; - try (final FileReader reader = new FileReader(fileName)) { - root = JsonParser.parseReader(reader).getAsJsonObject().getAsJsonArray("models"); + // validation of json data + for (final ModelSpec modelSpec : modelSpecs) { + LOG.info("Found model spec: {}", modelSpec); + final BackgroundModel ignored = modelSpec.getModel(); } - for (final JsonElement jsonElement : root) { - final JsonObject parameterSet = jsonElement.getAsJsonObject(); - final int z = parameterSet.get("fromZ").getAsInt(); - final String modelType = parameterSet.get("model").getAsString(); - final double[] coefficients = parameterSet.getAsJsonArray("coefficients").asList().stream() - .map(JsonElement::getAsDouble) - .mapToDouble(Double::doubleValue).toArray(); + return new BackgroundModelProvider(modelSpecs); + } + + + private static class ModelSpec implements Serializable { + private int fromZ; + private String modelType; + private double[] coefficients; - LOG.info("Extract model from parameters file: fromZ={}, modelType={}, coefficients={}", z, modelType, coefficients); + // no explicit constructor; meant to be deserialized from json - zValues.add(z); + public int getZ() { + return fromZ; + } + + public BackgroundModel getModel() { if (modelType.equals("quadratic")) { - models.add(new QuadraticBackground(coefficients)); + return new QuadraticBackground(coefficients); } else if (modelType.equals("fourthOrder")) { - models.add(new FourthOrderBackground(coefficients)); + return new FourthOrderBackground(coefficients); } else { throw new IllegalArgumentException("Unknown model type: " + modelType); } } - return new BackgroundModelProvider(zValues, models); + public String toString() { + return "ModelSpec{fromZ=" + fromZ + ", modelType=" + modelType + ", coefficients=" + Arrays.toString(coefficients) + "}"; + } } } From ac6aa5a6db56fd1c95594c439308d81e02e1e4a4 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 8 Nov 2024 09:45:39 -0500 Subject: [PATCH 29/56] Add first complete version of BackgroundCorrectionClient --- .../BackgroundCorrectionClient.java | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 4fb30b7e3..1c51e3ad6 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -4,9 +4,15 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import net.imglib2.Cursor; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.view.Views; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.broadcast.Broadcast; import org.janelia.alignment.util.Grid; import org.janelia.render.client.ClientRunner; import org.janelia.render.client.embackground.BackgroundModel; @@ -18,6 +24,7 @@ import org.janelia.saalfeldlab.n5.N5FSWriter; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5Writer; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,6 +92,10 @@ public static class Parameters extends CommandLineParameters { public String parameterFile; } + private static final Logger LOG = LoggerFactory.getLogger(BackgroundCorrectionClient.class); + + private final Parameters parameters; + public static void main(final String[] args) { final ClientRunner clientRunner = new ClientRunner(args) { @Override @@ -98,7 +109,6 @@ public void runClient(final String[] args) throws Exception { clientRunner.run(); } - private final Parameters parameters; public BackgroundCorrectionClient(final Parameters parameters) { LOG.info("init: parameters={}", parameters); @@ -120,7 +130,6 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti // read parameters final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile); - System.exit(0); // set up input and output N5 datasets final DatasetAttributes inputAttributes; @@ -143,15 +152,62 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti } // parallelize computation over blocks of the input/output dataset + final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); + final Broadcast parametersBroadcast = sparkContext.broadcast(parameters); + final List blocks = Grid.create(inputAttributes.getDimensions(), inputAttributes.getBlockSize()); final JavaRDD blockRDD = sparkContext.parallelize(blocks, blocks.size()); -// blockRDD.foreach(block -> processSingleBlock(parameters, models)); + + blockRDD.foreach(block -> processSingleBlock(parametersBroadcast.value(), modelProviderBroadcast.value(), block)); LOG.info("runWithContext: exit"); } - private static void processSingleBlock(final Parameters parameters, final Map> models) { + private static void processSingleBlock(final Parameters parameters, final BackgroundModelProvider modelProvider, final Grid.Block block) { + LOG.info("processSingleBlock: block={}", block); + + try (final N5Reader in = new N5FSReader(parameters.n5In); + final N5Writer out = new N5FSWriter(parameters.n5Out)) { + + // load block + final Img img = N5Utils.open(in, parameters.datasetIn); + final RandomAccessibleInterval croppedImg = Views.interval(img, block); + final long[] dimensions = in.getDatasetAttributes(parameters.datasetIn).getDimensions(); + + // scale pixel coordinates to [-1, 1] x [-1, 1] + final double xScale = dimensions[0] / 2.0; + final double yScale = dimensions[1] / 2.0; + + // process block z-slice by z-slice + final int xShift = (int) block.min(0); + final int yShift = (int) block.min(1); + final int zShift = (int) block.min(2); + for (int z = (int) block.min(2); z <= block.max(2); z++) { + final BackgroundModel model = modelProvider.getModel(z); + if (model == null) { + LOG.info("No model found for z={}", z); + continue; + } + + final RandomAccessibleInterval slice = Views.hyperSlice(croppedImg, 2, z - zShift); + final Cursor cursor = Views.iterable(slice).localizingCursor(); + final double[] location = new double[2]; + + // apply model to slice + while (cursor.hasNext()) { + final UnsignedByteType pixel = cursor.next(); + location[0] = (xShift + cursor.getIntPosition(0) - xScale) / xScale; + location[1] = (yShift + cursor.getIntPosition(1) - yScale) / yScale; + + model.applyInPlace(location); + pixel.setReal(UnsignedByteType.getCodedSignedByteChecked((int) (pixel.getRealDouble() - location[0]))); + } + } + + // save block + N5Utils.saveNonEmptyBlock(croppedImg, out, parameters.datasetOut, block.offset, new UnsignedByteType()); + } } @@ -179,7 +235,7 @@ public static BackgroundModelProvider fromJsonFile(final String fileName) throws } } - public static BackgroundModelProvider fromJson(final JsonElement jsonData) throws IOException { + public static BackgroundModelProvider fromJson(final JsonElement jsonData) { final List modelSpecs = new ArrayList<>(); Collections.addAll(modelSpecs, new Gson().fromJson(jsonData, ModelSpec[].class)); @@ -194,7 +250,8 @@ public static BackgroundModelProvider fromJson(final JsonElement jsonData) throw } - private static class ModelSpec implements Serializable { + @SuppressWarnings("unused") + private static class ModelSpec implements Serializable { private int fromZ; private String modelType; private double[] coefficients; @@ -220,6 +277,4 @@ public String toString() { } } } - - private static final Logger LOG = LoggerFactory.getLogger(BackgroundCorrectionClient.class); } From 1848cae8a2749ab94f751730154969c4ba649c22 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 8 Nov 2024 10:23:31 -0500 Subject: [PATCH 30/56] Fix some bugs to make background correction work --- .../BackgroundCorrectionClient.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 1c51e3ad6..376c5f904 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -179,26 +179,27 @@ private static void processSingleBlock(final Parameters parameters, final Backgr final double yScale = dimensions[1] / 2.0; // process block z-slice by z-slice - final int xShift = (int) block.min(0); - final int yShift = (int) block.min(1); - final int zShift = (int) block.min(2); - for (int z = (int) block.min(2); z <= block.max(2); z++) { final BackgroundModel model = modelProvider.getModel(z); if (model == null) { - LOG.info("No model found for z={}", z); + LOG.warn("No model found for z={}", z); continue; } - final RandomAccessibleInterval slice = Views.hyperSlice(croppedImg, 2, z - zShift); + final RandomAccessibleInterval slice = Views.hyperSlice(croppedImg, 2, z - (int) block.min(2)); final Cursor cursor = Views.iterable(slice).localizingCursor(); final double[] location = new double[2]; // apply model to slice while (cursor.hasNext()) { final UnsignedByteType pixel = cursor.next(); - location[0] = (xShift + cursor.getIntPosition(0) - xScale) / xScale; - location[1] = (yShift + cursor.getIntPosition(1) - yScale) / yScale; + if (pixel.getInteger() == 0) { + // background should not be corrected + continue; + } + + location[0] = (cursor.getDoublePosition(0) - xScale) / xScale; + location[1] = (cursor.getDoublePosition(1) - yScale) / yScale; model.applyInPlace(location); pixel.setReal(UnsignedByteType.getCodedSignedByteChecked((int) (pixel.getRealDouble() - location[0]))); @@ -206,7 +207,7 @@ private static void processSingleBlock(final Parameters parameters, final Backgr } // save block - N5Utils.saveNonEmptyBlock(croppedImg, out, parameters.datasetOut, block.offset, new UnsignedByteType()); + N5Utils.saveNonEmptyBlock(croppedImg, out, parameters.datasetOut, block.gridPosition, new UnsignedByteType()); } } @@ -262,6 +263,10 @@ public int getZ() { return fromZ; } + public void setCoefficients(final double[] coefficients) { + this.coefficients = coefficients; + } + public BackgroundModel getModel() { if (modelType.equals("quadratic")) { return new QuadraticBackground(coefficients); From 53f9e8d7ea83360536516b11d3497699bf6ff0f3 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 13 Nov 2024 18:01:03 -0500 Subject: [PATCH 31/56] Simplify plugin using ImageJ2 automatic detection --- render-ws-java-client/pom.xml | 10 ++ .../render/client/embackground/BG_Plugin.java | 157 ------------------ .../BackgroundCorrectionPlugin.java | 124 ++++++++++++++ .../BackgroundCorrectionClient.java | 2 +- 4 files changed, 135 insertions(+), 158 deletions(-) delete mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java diff --git a/render-ws-java-client/pom.xml b/render-ws-java-client/pom.xml index dec2851ef..6632aab59 100644 --- a/render-ws-java-client/pom.xml +++ b/render-ws-java-client/pom.xml @@ -163,6 +163,16 @@ bigdataviewer-vistools + + net.imagej + imagej + + + + net.imagej + imagej-legacy + + sc.fiji z_spacing diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java deleted file mode 100644 index 7c05d4d13..000000000 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ /dev/null @@ -1,157 +0,0 @@ -package org.janelia.render.client.embackground; - -import java.awt.FlowLayout; -import java.util.Arrays; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; - -import com.beust.jcommander.Parameter; -import mpicbg.models.IllDefinedDataPointsException; -import mpicbg.models.NotEnoughDataPointsException; -import net.imglib2.type.numeric.integer.UnsignedByteType; -import net.imglib2.type.numeric.real.FloatType; -import org.janelia.render.client.parameter.CommandLineParameters; -import org.janelia.saalfeldlab.n5.N5FSReader; -import org.janelia.saalfeldlab.n5.imglib2.N5Utils; - -import ij.IJ; -import ij.ImageJ; -import ij.gui.GenericDialog; -import ij.gui.Roi; -import ij.plugin.PlugIn; -import ij.plugin.frame.RoiManager; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.img.display.imagej.ImageJFunctions; -import net.imglib2.view.Views; - -public class BG_Plugin implements PlugIn { - - private static class Parameters extends CommandLineParameters { - @Parameter(names = "--n5Path", - description = "Path to the N5 container", - required = true) - public String n5Path; - - @Parameter(names = "--dataset", - description = "Name of the dataset", - required = true) - public String dataset; - - @Parameter(names = "--z", - description = "Z slice to process", - required = true) - public int z; - } - - - public static int defaultType = 0; - public static boolean defaultShowBackground = false; - public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; - - public static RandomAccessibleInterval img; - - @Override - public void run(final String arg) { - final List rois = getROIs(); - - if (rois == null || rois.isEmpty()) { - IJ.log("No ROIs specified."); - return; - } - - final GenericDialog gd = new GenericDialog("Fit background correction"); - - gd.addChoice("Fit type", fitTypes, fitTypes[defaultType]); - gd.addCheckbox("Show background image", defaultShowBackground); - gd.showDialog(); - - if (gd.wasCanceled()) { - return; - } - - final int type = gd.getNextChoiceIndex(); - defaultType = type; - - final boolean showBackground = gd.getNextBoolean(); - defaultShowBackground = showBackground; - - try { - fit(type, rois, showBackground); - } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { - IJ.log("Fitting failed: " + e.getMessage()); - } - } - - public static List getROIs() { - final RoiManager rm = RoiManager.getInstance(); - - if (rm == null || rm.getCount() == 0) { - IJ.log("Please define ROIs first before running background correction."); - return null; - } - - return Arrays.asList(rm.getRoisAsArray()); - } - - public static void fit(final int type, final List rois, final boolean showBackground) throws NotEnoughDataPointsException, IllDefinedDataPointsException { - IJ.log("Fitting with " + fitTypes[type] + " model..."); - - final BackgroundModel backgroundModel; - if (fitTypes[type].equals("Quadratic")) { - backgroundModel = new QuadraticBackground(); - } else if (fitTypes[type].equals("Fourth Order")) { - backgroundModel = new FourthOrderBackground(); - } else { - throw new IllegalArgumentException("Unknown fit type: " + fitTypes[type]); - } - - final long start = System.currentTimeMillis(); - CorrectBackground.fitBackgroundModel(rois, img, backgroundModel); - IJ.log("Fitted background model: " + backgroundModel); - IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms."); - IJ.log("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients())); - - final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img); - final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType()); - - if (showBackground) { - ImageJFunctions.show(background, "Background"); - } - ImageJFunctions.show(corrected, "Corrected"); - - } - - private static void showNonBlockingDialog() { - new Thread(() -> { - final JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false); - dialog.setLayout(new FlowLayout()); - - final JButton closeButton = new JButton("Run Background plugin..."); - closeButton.addActionListener(e -> new BG_Plugin().run(null )); - dialog.add(closeButton); - - dialog.pack(); - dialog.setVisible(true); - }).start(); - } - - public static void main(final String[] args) { - final Parameters params = new Parameters(); - params.parse(args); - - new ImageJ(); - SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog); - - img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset); - if (img.numDimensions() > 2) { - img = Views.hyperSlice(img, 2, params.z); - } - ImageJFunctions.show(img); - - new RoiManager(); - } -} diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java new file mode 100644 index 000000000..5a122c04b --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java @@ -0,0 +1,124 @@ +package org.janelia.render.client.embackground; + +import ij.ImagePlus; +import ij.gui.Roi; +import ij.plugin.frame.RoiManager; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import net.imagej.Dataset; +import net.imagej.ImageJ; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.view.Views; +import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.saalfeldlab.n5.N5FSReader; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import org.scijava.command.Command; +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +import java.util.Arrays; +import java.util.List; + +@Plugin(type=Command.class, headless=true, menuPath="Render Tools>Background Correction") +public class BackgroundCorrectionPlugin implements Command { + + private static class Parameters extends CommandLineParameters { + @com.beust.jcommander.Parameter(names = "--n5Path", + description = "Path to the N5 container", + required = true) + public String n5Path; + + @com.beust.jcommander.Parameter(names = "--dataset", + description = "Name of the dataset", + required = true) + public String dataset; + + @com.beust.jcommander.Parameter(names = "--z", + description = "Z slice to process", + required = true) + public int z; + } + + @Parameter + private Dataset source; + + @Parameter + private LogService log; + + @Parameter(label="Fit type:", choices={"Quadratic", "Fourth Order"}, style="radioButtonHorizontal") + private String fitType; + + @Parameter(label="Show background:") + private boolean showBackground; + + @Override + public void run() { + final Img img = source.typedImg(new UnsignedByteType()); + + log.info("Fitting with " + fitType + " model..."); + + final BackgroundModel backgroundModel; + if (fitType.equals("Quadratic")) { + backgroundModel = new QuadraticBackground(); + } else if (fitType.equals("Fourth Order")) { + backgroundModel = new FourthOrderBackground(); + } else { + throw new IllegalArgumentException("Unknown fit type: " + fitType); + } + + final List rois = getROIs(); + + final long start = System.currentTimeMillis(); + try { + CorrectBackground.fitBackgroundModel(rois, img, backgroundModel); + } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { + log.error("Fitting failed: " + e.getMessage()); + return; + } + log.info("Fitted background model: " + backgroundModel); + log.info("Fitting took " + (System.currentTimeMillis() - start) + "ms."); + log.info("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients())); + + final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img); + final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType()); + + if (showBackground) { + ImageJFunctions.show(background, "Background"); + } + ImageJFunctions.show(corrected, "Corrected"); + + } + + private List getROIs() { + final RoiManager rm = RoiManager.getInstance(); + + if (rm == null || rm.getCount() == 0) { + log.error("Please define ROIs first before running background correction."); + return null; + } + + return Arrays.asList(rm.getRoisAsArray()); + } + + public static void main(final String[] args) { + final Parameters params = new Parameters(); + params.parse(args); + + RandomAccessibleInterval img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset); + if (img.numDimensions() > 2) { + img = Views.hyperSlice(img, 2, params.z); + } + + // select ROIs and press "t" to add them to the ROI manager + // then run the plugin through the "Render Tools" menu + final ImageJ ij = new ImageJ(); + ij.launch(); + final ImagePlus imgPlus = ImageJFunctions.wrap(img, "Original"); + imgPlus.show(); + } +} diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 376c5f904..88307293f 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -49,7 +49,7 @@ * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}. *

* In particular, the parameter file should have the following format. There is one root array, whose elements have - * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: + * the properties: "fromZ", "modelType", and "coefficients"s, e.g.: *
  * [ {
  *     "fromZ": 1,

From 8eecf5eb9d4012fe1e296bc8d20afeaeb80c4176 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Thu, 14 Nov 2024 10:44:14 -0500
Subject: [PATCH 32/56] Revert "Simplify plugin using ImageJ2 automatic
 detection"

This reverts commit 53f9e8d7ea83360536516b11d3497699bf6ff0f3.
The commit used ImageJ2 which clashes with some of the other
dependencies in the project.
---
 render-ws-java-client/pom.xml                 |  10 --
 .../render/client/embackground/BG_Plugin.java | 157 ++++++++++++++++++
 .../BackgroundCorrectionPlugin.java           | 124 --------------
 .../BackgroundCorrectionClient.java           |   2 +-
 4 files changed, 158 insertions(+), 135 deletions(-)
 create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
 delete mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java

diff --git a/render-ws-java-client/pom.xml b/render-ws-java-client/pom.xml
index 6632aab59..dec2851ef 100644
--- a/render-ws-java-client/pom.xml
+++ b/render-ws-java-client/pom.xml
@@ -163,16 +163,6 @@
             bigdataviewer-vistools
         
 
-        
-            net.imagej
-            imagej
-        
-
-        
-            net.imagej
-            imagej-legacy
-        
-
         
             sc.fiji
             z_spacing
diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
new file mode 100644
index 000000000..7c05d4d13
--- /dev/null
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
@@ -0,0 +1,157 @@
+package org.janelia.render.client.embackground;
+
+import java.awt.FlowLayout;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+import com.beust.jcommander.Parameter;
+import mpicbg.models.IllDefinedDataPointsException;
+import mpicbg.models.NotEnoughDataPointsException;
+import net.imglib2.type.numeric.integer.UnsignedByteType;
+import net.imglib2.type.numeric.real.FloatType;
+import org.janelia.render.client.parameter.CommandLineParameters;
+import org.janelia.saalfeldlab.n5.N5FSReader;
+import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
+
+import ij.IJ;
+import ij.ImageJ;
+import ij.gui.GenericDialog;
+import ij.gui.Roi;
+import ij.plugin.PlugIn;
+import ij.plugin.frame.RoiManager;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.view.Views;
+
+public class BG_Plugin implements PlugIn {
+
+	private static class Parameters extends CommandLineParameters {
+		@Parameter(names = "--n5Path",
+				description = "Path to the N5 container",
+				required = true)
+		public String n5Path;
+
+		@Parameter(names = "--dataset",
+				description = "Name of the dataset",
+				required = true)
+		public String dataset;
+
+		@Parameter(names = "--z",
+				description = "Z slice to process",
+				required = true)
+		public int z;
+	}
+
+
+	public static int defaultType = 0;
+	public static boolean defaultShowBackground = false;
+	public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" };
+
+	public static RandomAccessibleInterval img;
+
+	@Override
+	public void run(final String arg) {
+		final List rois = getROIs();
+
+		if (rois == null || rois.isEmpty()) {
+			IJ.log("No ROIs specified.");
+			return;
+		}
+
+		final GenericDialog gd = new GenericDialog("Fit background correction");
+
+		gd.addChoice("Fit type", fitTypes, fitTypes[defaultType]);
+		gd.addCheckbox("Show background image", defaultShowBackground);
+		gd.showDialog();
+
+		if (gd.wasCanceled()) {
+			return;
+		}
+
+		final int type = gd.getNextChoiceIndex();
+		defaultType = type;
+
+		final boolean showBackground = gd.getNextBoolean();
+		defaultShowBackground = showBackground;
+
+		try {
+			fit(type, rois, showBackground);
+		} catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) {
+			IJ.log("Fitting failed: " + e.getMessage());
+		}
+	}
+
+	public static List getROIs() {
+		final RoiManager rm = RoiManager.getInstance();
+
+		if (rm == null || rm.getCount() == 0) {
+			IJ.log("Please define ROIs first before running background correction.");
+			return null;
+		}
+
+		return Arrays.asList(rm.getRoisAsArray());
+	}
+
+	public static void fit(final int type, final List rois, final boolean showBackground) throws NotEnoughDataPointsException, IllDefinedDataPointsException {
+		IJ.log("Fitting with " + fitTypes[type] + " model...");
+
+		final BackgroundModel backgroundModel;
+		if (fitTypes[type].equals("Quadratic")) {
+			backgroundModel = new QuadraticBackground();
+		} else if (fitTypes[type].equals("Fourth Order")) {
+			backgroundModel = new FourthOrderBackground();
+		} else {
+			throw new IllegalArgumentException("Unknown fit type: " + fitTypes[type]);
+		}
+
+		final long start = System.currentTimeMillis();
+		CorrectBackground.fitBackgroundModel(rois, img, backgroundModel);
+		IJ.log("Fitted background model: " + backgroundModel);
+		IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms.");
+		IJ.log("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients()));
+
+		final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img);
+		final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType());
+
+		if (showBackground) {
+			ImageJFunctions.show(background, "Background");
+		}
+		ImageJFunctions.show(corrected, "Corrected");
+
+	}
+
+	private static void showNonBlockingDialog() {
+        new Thread(() -> {
+            final JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false);
+            dialog.setLayout(new FlowLayout());
+
+            final JButton closeButton = new JButton("Run Background plugin...");
+            closeButton.addActionListener(e -> new BG_Plugin().run(null ));
+            dialog.add(closeButton);
+
+            dialog.pack();
+            dialog.setVisible(true);
+        }).start();
+	}
+
+	public static void main(final String[] args) {
+		final Parameters params = new Parameters();
+		params.parse(args);
+
+		new ImageJ();
+		SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog);
+
+		img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset);
+		if (img.numDimensions() > 2) {
+			img = Views.hyperSlice(img, 2, params.z);
+		}
+		ImageJFunctions.show(img);
+
+		new RoiManager();
+	}
+}
diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java
deleted file mode 100644
index 5a122c04b..000000000
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundCorrectionPlugin.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package org.janelia.render.client.embackground;
-
-import ij.ImagePlus;
-import ij.gui.Roi;
-import ij.plugin.frame.RoiManager;
-import mpicbg.models.IllDefinedDataPointsException;
-import mpicbg.models.NotEnoughDataPointsException;
-import net.imagej.Dataset;
-import net.imagej.ImageJ;
-import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.img.Img;
-import net.imglib2.img.display.imagej.ImageJFunctions;
-import net.imglib2.type.numeric.integer.UnsignedByteType;
-import net.imglib2.type.numeric.real.FloatType;
-import net.imglib2.view.Views;
-import org.janelia.render.client.parameter.CommandLineParameters;
-import org.janelia.saalfeldlab.n5.N5FSReader;
-import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
-import org.scijava.command.Command;
-import org.scijava.log.LogService;
-import org.scijava.plugin.Parameter;
-import org.scijava.plugin.Plugin;
-
-import java.util.Arrays;
-import java.util.List;
-
-@Plugin(type=Command.class, headless=true, menuPath="Render Tools>Background Correction")
-public class BackgroundCorrectionPlugin implements Command {
-
-	private static class Parameters extends CommandLineParameters {
-		@com.beust.jcommander.Parameter(names = "--n5Path",
-				description = "Path to the N5 container",
-				required = true)
-		public String n5Path;
-
-		@com.beust.jcommander.Parameter(names = "--dataset",
-				description = "Name of the dataset",
-				required = true)
-		public String dataset;
-
-		@com.beust.jcommander.Parameter(names = "--z",
-				description = "Z slice to process",
-				required = true)
-		public int z;
-	}
-
-	@Parameter
-	private Dataset source;
-
-	@Parameter
-	private LogService log;
-
-	@Parameter(label="Fit type:", choices={"Quadratic", "Fourth Order"}, style="radioButtonHorizontal")
-	private String fitType;
-
-	@Parameter(label="Show background:")
-	private boolean showBackground;
-
-	@Override
-	public void run() {
-		final Img img = source.typedImg(new UnsignedByteType());
-
-		log.info("Fitting with " + fitType + " model...");
-
-		final BackgroundModel backgroundModel;
-		if (fitType.equals("Quadratic")) {
-			backgroundModel = new QuadraticBackground();
-		} else if (fitType.equals("Fourth Order")) {
-			backgroundModel = new FourthOrderBackground();
-		} else {
-			throw new IllegalArgumentException("Unknown fit type: " + fitType);
-		}
-
-		final List rois = getROIs();
-
-		final long start = System.currentTimeMillis();
-		try {
-			CorrectBackground.fitBackgroundModel(rois, img, backgroundModel);
-		} catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) {
-			log.error("Fitting failed: " + e.getMessage());
-			return;
-		}
-		log.info("Fitted background model: " + backgroundModel);
-		log.info("Fitting took " + (System.currentTimeMillis() - start) + "ms.");
-		log.info("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients()));
-
-		final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img);
-		final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType());
-
-		if (showBackground) {
-			ImageJFunctions.show(background, "Background");
-		}
-		ImageJFunctions.show(corrected, "Corrected");
-
-	}
-
-	private List getROIs() {
-		final RoiManager rm = RoiManager.getInstance();
-
-		if (rm == null || rm.getCount() == 0) {
-			log.error("Please define ROIs first before running background correction.");
-			return null;
-		}
-
-		return Arrays.asList(rm.getRoisAsArray());
-	}
-
-	public static void main(final String[] args) {
-		final Parameters params = new Parameters();
-		params.parse(args);
-
-		RandomAccessibleInterval img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset);
-		if (img.numDimensions() > 2) {
-			img = Views.hyperSlice(img, 2, params.z);
-		}
-
-		// select ROIs and press "t" to add them to the ROI manager
-		// then run the plugin through the "Render Tools" menu
-		final ImageJ ij = new ImageJ();
-		ij.launch();
-		final ImagePlus imgPlus = ImageJFunctions.wrap(img, "Original");
-		imgPlus.show();
-	}
-}
diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
index 88307293f..376c5f904 100644
--- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
+++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
@@ -49,7 +49,7 @@
  * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}.
  * 

* In particular, the parameter file should have the following format. There is one root array, whose elements have - * the properties: "fromZ", "modelType", and "coefficients"s, e.g.: + * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: *
  * [ {
  *     "fromZ": 1,

From 3da61060e4b4cabe6fffb714404874e1f4471947 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Thu, 14 Nov 2024 11:53:37 -0500
Subject: [PATCH 33/56] Make background correction plugin work more nicely

---
 .../render/client/embackground/BG_Plugin.java | 42 ++++++++++---------
 1 file changed, 22 insertions(+), 20 deletions(-)

diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
index 7c05d4d13..563d386d8 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
@@ -1,12 +1,10 @@
 package org.janelia.render.client.embackground;
 
-import java.awt.FlowLayout;
+import java.awt.*;
+import java.awt.event.KeyEvent;
 import java.util.Arrays;
 import java.util.List;
 
-import javax.swing.JButton;
-import javax.swing.JDialog;
-import javax.swing.JFrame;
 import javax.swing.SwingUtilities;
 
 import com.beust.jcommander.Parameter;
@@ -52,8 +50,6 @@ private static class Parameters extends CommandLineParameters {
 	public static boolean defaultShowBackground = false;
 	public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" };
 
-	public static RandomAccessibleInterval img;
-
 	@Override
 	public void run(final String arg) {
 		final List rois = getROIs();
@@ -110,6 +106,7 @@ public static void fit(final int type, final List rois, final boolean showB
 		}
 
 		final long start = System.currentTimeMillis();
+		final RandomAccessibleInterval img = ImageJFunctions.wrap(IJ.getImage());
 		CorrectBackground.fitBackgroundModel(rois, img, backgroundModel);
 		IJ.log("Fitted background model: " + backgroundModel);
 		IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms.");
@@ -125,18 +122,23 @@ public static void fit(final int type, final List rois, final boolean showB
 
 	}
 
-	private static void showNonBlockingDialog() {
-        new Thread(() -> {
-            final JDialog dialog = new JDialog((JFrame)null, "Run Background plugin...", false);
-            dialog.setLayout(new FlowLayout());
-
-            final JButton closeButton = new JButton("Run Background plugin...");
-            closeButton.addActionListener(e -> new BG_Plugin().run(null ));
-            dialog.add(closeButton);
-
-            dialog.pack();
-            dialog.setVisible(true);
-        }).start();
+	private static void addKeyListener() {
+		System.out.println("Mapped 'Background Correction' to F1.");
+
+        new Thread(() -> KeyboardFocusManager.getCurrentKeyboardFocusManager()
+				.addKeyEventDispatcher(e -> {
+					if (e.getID() == KeyEvent.KEY_PRESSED) {
+						switch (e.getKeyCode()) {
+							case KeyEvent.VK_F1:
+								new BG_Plugin().run(null);
+								break;
+							case KeyEvent.VK_F2:
+								System.out.println("F2 key pressed (not assigned)");
+								break;
+						}
+					}
+					return false;
+				})).start();
 	}
 
 	public static void main(final String[] args) {
@@ -144,9 +146,9 @@ public static void main(final String[] args) {
 		params.parse(args);
 
 		new ImageJ();
-		SwingUtilities.invokeLater(BG_Plugin::showNonBlockingDialog);
+		SwingUtilities.invokeLater(BG_Plugin::addKeyListener);
 
-		img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset);
+		RandomAccessibleInterval img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset);
 		if (img.numDimensions() > 2) {
 			img = Views.hyperSlice(img, 2, params.z);
 		}

From 2eeb63fd5decae45a215603b1270a27e8a60389e Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Thu, 14 Nov 2024 15:39:40 -0500
Subject: [PATCH 34/56] Add downscaling to background correction plugin

---
 .../render/client/embackground/BG_Plugin.java | 34 ++++++++++++++++---
 1 file changed, 29 insertions(+), 5 deletions(-)

diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
index 563d386d8..d7a63677b 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java
@@ -10,6 +10,7 @@
 import com.beust.jcommander.Parameter;
 import mpicbg.models.IllDefinedDataPointsException;
 import mpicbg.models.NotEnoughDataPointsException;
+import net.imglib2.algorithm.gauss3.Gauss3;
 import net.imglib2.type.numeric.integer.UnsignedByteType;
 import net.imglib2.type.numeric.real.FloatType;
 import org.janelia.render.client.parameter.CommandLineParameters;
@@ -40,9 +41,12 @@ private static class Parameters extends CommandLineParameters {
 		public String dataset;
 
 		@Parameter(names = "--z",
-				description = "Z slice to process",
-				required = true)
-		public int z;
+				description = "Z slice to process")
+		public int z = 1;
+
+		@Parameter(names = "--downscaleFactor",
+				description = "Downscale factor")
+		public int downscale = 0;
 	}
 
 
@@ -148,12 +152,32 @@ public static void main(final String[] args) {
 		new ImageJ();
 		SwingUtilities.invokeLater(BG_Plugin::addKeyListener);
 
+		IJ.log("Opening " + params.dataset + " from " + params.n5Path);
 		RandomAccessibleInterval img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset);
 		if (img.numDimensions() > 2) {
+			IJ.log("Showing slice " + params.z);
 			img = Views.hyperSlice(img, 2, params.z);
 		}
-		ImageJFunctions.show(img);
+		final RandomAccessibleInterval downscaled = downSample(img, params.downscale);
+		ImageJFunctions.show(downscaled, "Original");
+	}
 
-		new RoiManager();
+	private static RandomAccessibleInterval downSample(final RandomAccessibleInterval img, int downscale) {
+		if (downscale < 1) {
+			// automatically determine downscale factor
+			IJ.log("No downscaling factor given, choosing factor automatically...");
+			final long[] dims = img.dimensionsAsLongArray();
+			final long maxSize = 2000 * 2000;
+			downscale = (int) Math.ceil(Math.sqrt((double) (dims[0] * dims[1]) / maxSize));
+		}
+
+		if (downscale == 1) {
+			IJ.log("No downscaling, showing original image at full size.");
+			return img;
+		} else {
+			IJ.log("Isotropically downscaling by factor of " + downscale);
+			Gauss3.gauss(downscale / 2.0, Views.extendMirrorSingle(img), img);
+			return Views.subsample(img, downscale);
+		}
 	}
 }

From 605e29bd57a00194af35b021181e7cca41ab413c Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Tue, 19 Nov 2024 18:07:56 -0500
Subject: [PATCH 35/56] Add mask support to background correction

---
 .../BackgroundCorrectionClient.java           | 30 ++++++++++++++-----
 1 file changed, 22 insertions(+), 8 deletions(-)

diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
index 376c5f904..51d297b1f 100644
--- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
+++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
@@ -76,6 +76,11 @@ public static class Parameters extends CommandLineParameters {
                 required = true)
         public String datasetIn;
 
+        @Parameter(names = "--mask",
+                description = "Name of the mask dataset for the input",
+                required = true)
+        public String mask;
+
         @Parameter(names = "--n5Out",
                 description = "Path to the output N5 container",
                 required = true)
@@ -137,6 +142,10 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti
             inputAttributes = in.getDatasetAttributes(parameters.datasetIn);
             final Map> otherAttributes = in.listAttributes(parameters.datasetIn);
 
+            if (!in.exists(parameters.mask)) {
+                throw new IllegalArgumentException("Mask dataset does not exist: " + parameters.mask);
+            }
+
             try (final N5Writer out = new N5FSWriter(parameters.n5Out)) {
                 if (out.exists(parameters.datasetOut)) {
                     throw new IllegalArgumentException("Output dataset already exists: " + parameters.datasetOut);
@@ -172,6 +181,8 @@ private static void processSingleBlock(final Parameters parameters, final Backgr
             // load block
             final Img img = N5Utils.open(in, parameters.datasetIn);
             final RandomAccessibleInterval croppedImg = Views.interval(img, block);
+            final Img mask = N5Utils.open(in, parameters.mask);
+            final RandomAccessibleInterval croppedMask = Views.interval(mask, block);
             final long[] dimensions = in.getDatasetAttributes(parameters.datasetIn).getDimensions();
 
             // scale pixel coordinates to [-1, 1] x [-1, 1]
@@ -186,20 +197,23 @@ private static void processSingleBlock(final Parameters parameters, final Backgr
                     continue;
                 }
 
-                final RandomAccessibleInterval slice = Views.hyperSlice(croppedImg, 2, z - (int) block.min(2));
-                final Cursor cursor = Views.iterable(slice).localizingCursor();
+                final RandomAccessibleInterval imgSlice = Views.hyperSlice(croppedImg, 2, z - (int) block.min(2));
+                final RandomAccessibleInterval maskSlice = Views.hyperSlice(croppedMask, 2, z - (int) block.min(2));
+                final Cursor imgCursor = Views.iterable(imgSlice).localizingCursor();
+                final Cursor maskCursor = Views.iterable(maskSlice).localizingCursor();
                 final double[] location = new double[2];
 
                 // apply model to slice
-                while (cursor.hasNext()) {
-                    final UnsignedByteType pixel = cursor.next();
-                    if (pixel.getInteger() == 0) {
-                        // background should not be corrected
+                while (imgCursor.hasNext()) {
+                    final UnsignedByteType pixel = imgCursor.next();
+                    final UnsignedByteType maskPixel = maskCursor.next();
+                    if (maskPixel.get() == 0) {
+                        // don't apply model to background
                         continue;
                     }
 
-                    location[0] = (cursor.getDoublePosition(0) - xScale) / xScale;
-                    location[1] = (cursor.getDoublePosition(1) - yScale) / yScale;
+                    location[0] = (imgCursor.getDoublePosition(0) - xScale) / xScale;
+                    location[1] = (imgCursor.getDoublePosition(1) - yScale) / yScale;
 
                     model.applyInPlace(location);
                     pixel.setReal(UnsignedByteType.getCodedSignedByteChecked((int) (pixel.getRealDouble() - location[0])));

From 4d85665963eaf770f53ff276a28831579b031677 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Tue, 19 Nov 2024 18:15:09 -0500
Subject: [PATCH 36/56] Extract static method for coordinate rescaling

---
 .../render/client/embackground/BackgroundModel.java    | 10 ++++++++++
 .../render/client/embackground/CorrectBackground.java  |  8 ++++----
 .../intensityadjust/BackgroundCorrectionClient.java    |  4 ++--
 3 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
index fbc41b47f..4a9e60bc1 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
@@ -61,6 +61,16 @@ public void normalize() {
 		coefficients[0] -= avg;
 	}
 
+	/**
+	 * Scale the given coordinate to the model coordinates (in [-1, 1]).
+	 * @param x the coordinate to scale
+	 * @param scale the scale factor (usually half the image size in the respective dimension)
+	 * @return the scaled coordinate in the range [-1, 1]
+	 */
+	public static double scaleCoordinate(final double x, final double scale) {
+		return (x - scale) / scale;
+	}
+
 	@Override
 	public int getMinNumMatches() {
 		return nCoefficients();
diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
index f6fe61e12..049141958 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
@@ -81,8 +81,8 @@ void fitBackgroundModel(final List rois, final RandomAccessibleInterval
 		final RandomAccess ra = slice.randomAccess();
 
 		for (final java.awt.Point point : extractInterestPoints(rois)) {
-			final double x = (point.x - scaleX) / scaleX;
-			final double y = (point.y - scaleY) / scaleY;
+			final double x = BackgroundModel.scaleCoordinate(point.x, scaleX);
+			final double y = BackgroundModel.scaleCoordinate(point.x, scaleX);
 			final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble();
 			matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z})));
 		}
@@ -109,8 +109,8 @@ RandomAccessibleInterval createBackgroundImage(final BackgroundModel<
 
 		final double[] location = new double[2];
 		final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> {
-			location[0] = (pos.getDoublePosition(0) - scaleX) / scaleX;
-			location[1] = (pos.getDoublePosition(1) - scaleY) / scaleY;
+			location[0] = BackgroundModel.scaleCoordinate(pos.getDoublePosition(0), scaleX);
+			location[1] = BackgroundModel.scaleCoordinate(pos.getDoublePosition(1), scaleY);
 			backgroundModel.applyInPlace(location);
 			value.setReal(location[0]);
 		}, FloatType::new);
diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
index 51d297b1f..216c4f595 100644
--- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
+++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
@@ -212,8 +212,8 @@ private static void processSingleBlock(final Parameters parameters, final Backgr
                         continue;
                     }
 
-                    location[0] = (imgCursor.getDoublePosition(0) - xScale) / xScale;
-                    location[1] = (imgCursor.getDoublePosition(1) - yScale) / yScale;
+                    location[0] = BackgroundModel.scaleCoordinate(imgCursor.getDoublePosition(0), xScale);
+                    location[1] = BackgroundModel.scaleCoordinate(imgCursor.getDoublePosition(1), yScale);
 
                     model.applyInPlace(location);
                     pixel.setReal(UnsignedByteType.getCodedSignedByteChecked((int) (pixel.getRealDouble() - location[0])));

From 59b83cada33309067cd53f1004c26adab1a9a3b4 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Wed, 20 Nov 2024 17:53:40 -0500
Subject: [PATCH 37/56] Handle multiscale datasets correctly

---
 .../BackgroundCorrectionClient.java           | 57 ++++++++++++++-----
 1 file changed, 42 insertions(+), 15 deletions(-)

diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
index 216c4f595..0599369cd 100644
--- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
+++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
@@ -72,12 +72,12 @@ public static class Parameters extends CommandLineParameters {
         public String n5In;
 
         @Parameter(names = "--datasetIn",
-                description = "Name of the input dataset",
+                description = "Name of the input dataset (or group in case of multiscale pyramid; only s0 is processed)",
                 required = true)
         public String datasetIn;
 
         @Parameter(names = "--mask",
-                description = "Name of the mask dataset for the input",
+                description = "Name of the mask dataset for the input (or group in case of multiscale pyramid; only s0 is processed)",
                 required = true)
         public String mask;
 
@@ -87,7 +87,7 @@ public static class Parameters extends CommandLineParameters {
         public String n5Out;
 
         @Parameter(names = "--datasetOut",
-                description = "Name of the output dataset",
+                description = "Name of the output dataset (or group in case of multiscale pyramid; only s0 is processed)",
                 required = true)
         public String datasetOut;
 
@@ -137,13 +137,11 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti
         final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile);
 
         // set up input and output N5 datasets
-        final DatasetAttributes inputAttributes;
+        final int[] blockSize;
+        final long[] dimensions;
         try (final N5Reader in = new N5FSReader(parameters.n5In)) {
-            inputAttributes = in.getDatasetAttributes(parameters.datasetIn);
-            final Map> otherAttributes = in.listAttributes(parameters.datasetIn);
-
-            if (!in.exists(parameters.mask)) {
-                throw new IllegalArgumentException("Mask dataset does not exist: " + parameters.mask);
+            if (!in.exists(parameters.datasetIn) || !in.exists(parameters.mask)) {
+                throw new IllegalArgumentException("Mask dataset (" + parameters.mask + ") or input dataset (" + parameters.datasetIn + ") do not exist");
             }
 
             try (final N5Writer out = new N5FSWriter(parameters.n5Out)) {
@@ -151,12 +149,26 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti
                     throw new IllegalArgumentException("Output dataset already exists: " + parameters.datasetOut);
                 }
 
-                out.createDataset(parameters.datasetOut, inputAttributes);
+                // create output dataset (or group if input is multiscale)
+                final boolean isMultiScale = in.exists(parameters.datasetIn + "/s0");
+                cloneDatasetOrGroupAttributes(in, out, parameters, isMultiScale);
                 out.setAttribute(parameters.datasetOut, "BackgroundCorrectionClientParameters", parameters);
-                otherAttributes.forEach((key, clazz) -> {
-                    final Object attribute = in.getAttribute(parameters.datasetIn, key, clazz);
-                    out.setAttribute(parameters.datasetOut, key, attribute);
-                });
+
+                if (isMultiScale) {
+                    // since we only write s0, we need to set the scales attribute accordingly
+                    out.setAttribute(parameters.datasetOut, "scales", new int[][]{{1, 1, 1}});
+
+                    // also set up the output dataset for s0
+                    parameters.datasetIn = parameters.datasetIn + "/s0";
+                    parameters.mask = parameters.mask + "/s0";
+                    parameters.datasetOut = parameters.datasetOut + "/s0";
+                    cloneDatasetOrGroupAttributes(in, out, parameters, false);
+                }
+
+                // read block size and dimensions (parameters now refer to the actual dataset to be processed)
+                final DatasetAttributes inputAttributes = in.getDatasetAttributes(parameters.datasetIn);
+                blockSize = inputAttributes.getBlockSize();
+                dimensions = inputAttributes.getDimensions();
             }
         }
 
@@ -164,7 +176,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti
         final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider);
         final Broadcast parametersBroadcast = sparkContext.broadcast(parameters);
 
-        final List blocks = Grid.create(inputAttributes.getDimensions(), inputAttributes.getBlockSize());
+        final List blocks = Grid.create(dimensions, blockSize);
         final JavaRDD blockRDD = sparkContext.parallelize(blocks, blocks.size());
 
         blockRDD.foreach(block -> processSingleBlock(parametersBroadcast.value(), modelProviderBroadcast.value(), block));
@@ -172,6 +184,21 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti
         LOG.info("runWithContext: exit");
     }
 
+    private static void cloneDatasetOrGroupAttributes(final N5Reader in, final N5Writer out, final Parameters parameters, final boolean isGroup) {
+        if (isGroup) {
+            out.createGroup(parameters.datasetOut);
+        } else {
+            final DatasetAttributes datasetAttributes = in.getDatasetAttributes(parameters.datasetIn);
+            out.createDataset(parameters.datasetOut, datasetAttributes);
+        }
+
+        final Map> attributes = in.listAttributes(parameters.datasetIn);
+        attributes.forEach((key, clazz) -> {
+            final Object attribute = in.getAttribute(parameters.datasetIn, key, clazz);
+            out.setAttribute(parameters.datasetOut, key, attribute);
+        });
+    }
+
     private static void processSingleBlock(final Parameters parameters, final BackgroundModelProvider modelProvider, final Grid.Block block) {
         LOG.info("processSingleBlock: block={}", block);
 

From d53c46d3b175e83b6d90b7c3ac849b5325a4cd63 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Wed, 20 Nov 2024 18:25:08 -0500
Subject: [PATCH 38/56] Fix bug in background correction code

---
 .../janelia/render/client/embackground/BackgroundModel.java | 6 +++---
 .../render/client/embackground/CorrectBackground.java       | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
index 4a9e60bc1..4dfe81d91 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java
@@ -63,12 +63,12 @@ public void normalize() {
 
 	/**
 	 * Scale the given coordinate to the model coordinates (in [-1, 1]).
-	 * @param x the coordinate to scale
+	 * @param coordinate the coordinate to scale
 	 * @param scale the scale factor (usually half the image size in the respective dimension)
 	 * @return the scaled coordinate in the range [-1, 1]
 	 */
-	public static double scaleCoordinate(final double x, final double scale) {
-		return (x - scale) / scale;
+	public static double scaleCoordinate(final double coordinate, final double scale) {
+		return (coordinate - scale) / scale;
 	}
 
 	@Override
diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
index 049141958..f2cb23e75 100644
--- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
+++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java
@@ -82,7 +82,7 @@ void fitBackgroundModel(final List rois, final RandomAccessibleInterval
 
 		for (final java.awt.Point point : extractInterestPoints(rois)) {
 			final double x = BackgroundModel.scaleCoordinate(point.x, scaleX);
-			final double y = BackgroundModel.scaleCoordinate(point.x, scaleX);
+			final double y = BackgroundModel.scaleCoordinate(point.y, scaleY);
 			final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble();
 			matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z})));
 		}

From 20f33c18ead6b6d15ac571a2bb4ab2f87d6fa806 Mon Sep 17 00:00:00 2001
From: Michael Innerberger 
Date: Fri, 22 Nov 2024 15:44:17 -0500
Subject: [PATCH 39/56] Make background correction work on 16bit data

---
 .../BackgroundCorrectionClient.java           | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
index 0599369cd..25df8f19f 100644
--- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
+++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java
@@ -8,6 +8,7 @@
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.img.Img;
 import net.imglib2.type.numeric.integer.UnsignedByteType;
+import net.imglib2.type.numeric.integer.UnsignedShortType;
 import net.imglib2.view.Views;
 import org.apache.spark.SparkConf;
 import org.apache.spark.api.java.JavaRDD;
@@ -40,8 +41,8 @@
 
 /**
  * Spark client for background correction by a layer-wise quadratic or fourth order model.
- * The client takes as input an N5 container with a 3D dataset and a parameter file, and writes the corrected data to a
- * new dataset in a given container.
+ * The client takes as input an N5 container with a 3D 16bit dataset and a parameter file, and writes the corrected data
+ * to a new dataset in a given container.
  * 

* The parameter file is a json file containing a list of z values and corresponding models. The model for each z value * is valid for all z layers starting at the given z value until the next z value in the list. @@ -206,8 +207,8 @@ private static void processSingleBlock(final Parameters parameters, final Backgr final N5Writer out = new N5FSWriter(parameters.n5Out)) { // load block - final Img img = N5Utils.open(in, parameters.datasetIn); - final RandomAccessibleInterval croppedImg = Views.interval(img, block); + final Img img = N5Utils.open(in, parameters.datasetIn); + final RandomAccessibleInterval croppedImg = Views.interval(img, block); final Img mask = N5Utils.open(in, parameters.mask); final RandomAccessibleInterval croppedMask = Views.interval(mask, block); final long[] dimensions = in.getDatasetAttributes(parameters.datasetIn).getDimensions(); @@ -224,15 +225,15 @@ private static void processSingleBlock(final Parameters parameters, final Backgr continue; } - final RandomAccessibleInterval imgSlice = Views.hyperSlice(croppedImg, 2, z - (int) block.min(2)); + final RandomAccessibleInterval imgSlice = Views.hyperSlice(croppedImg, 2, z - (int) block.min(2)); final RandomAccessibleInterval maskSlice = Views.hyperSlice(croppedMask, 2, z - (int) block.min(2)); - final Cursor imgCursor = Views.iterable(imgSlice).localizingCursor(); + final Cursor imgCursor = Views.iterable(imgSlice).localizingCursor(); final Cursor maskCursor = Views.iterable(maskSlice).localizingCursor(); final double[] location = new double[2]; // apply model to slice while (imgCursor.hasNext()) { - final UnsignedByteType pixel = imgCursor.next(); + final UnsignedShortType pixel = imgCursor.next(); final UnsignedByteType maskPixel = maskCursor.next(); if (maskPixel.get() == 0) { // don't apply model to background @@ -243,12 +244,12 @@ private static void processSingleBlock(final Parameters parameters, final Backgr location[1] = BackgroundModel.scaleCoordinate(imgCursor.getDoublePosition(1), yScale); model.applyInPlace(location); - pixel.setReal(UnsignedByteType.getCodedSignedByteChecked((int) (pixel.getRealDouble() - location[0]))); + pixel.setReal(UnsignedShortType.getCodedSignedShortChecked((int) (pixel.getRealDouble() - location[0]))); } } // save block - N5Utils.saveNonEmptyBlock(croppedImg, out, parameters.datasetOut, block.gridPosition, new UnsignedByteType()); + N5Utils.saveNonEmptyBlock(croppedImg, out, parameters.datasetOut, block.gridPosition, new UnsignedShortType()); } } From 213baa74911df682d82deed44a95c96b752bcc10 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Mon, 25 Nov 2024 16:26:54 -0500 Subject: [PATCH 40/56] Pull images from render instead of N5 for BG plugin --- .../render/client/embackground/BG_Plugin.java | 84 +++++++++++-------- .../BackgroundCorrectionClient.java | 2 +- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java index d7a63677b..03d896273 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java @@ -2,20 +2,24 @@ import java.awt.*; import java.awt.event.KeyEvent; +import java.io.IOException; import java.util.Arrays; import java.util.List; import javax.swing.SwingUtilities; import com.beust.jcommander.Parameter; +import ij.ImagePlus; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; -import net.imglib2.algorithm.gauss3.Gauss3; +import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.real.FloatType; +import org.janelia.alignment.spec.Bounds; +import org.janelia.alignment.util.ImageProcessorCache; +import org.janelia.render.client.RenderDataClient; import org.janelia.render.client.parameter.CommandLineParameters; -import org.janelia.saalfeldlab.n5.N5FSReader; -import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import org.janelia.render.client.solver.visualize.RenderTools; import ij.IJ; import ij.ImageJ; @@ -25,33 +29,40 @@ import ij.plugin.frame.RoiManager; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.display.imagej.ImageJFunctions; -import net.imglib2.view.Views; public class BG_Plugin implements PlugIn { + public static final int MAX_SIZE = 2000 * 2000; + private static class Parameters extends CommandLineParameters { - @Parameter(names = "--n5Path", - description = "Path to the N5 container", + @Parameter(names = "--owner", + description = "Name of the owner in the render database", + required = true) + public String owner; + + @Parameter(names = "--project", + description = "Name of the render project", required = true) - public String n5Path; + public String project; - @Parameter(names = "--dataset", - description = "Name of the dataset", + @Parameter(names = "--stack", + description = "Name of the stack in the render project", required = true) - public String dataset; + public String stack; @Parameter(names = "--z", description = "Z slice to process") public int z = 1; - @Parameter(names = "--downscaleFactor", - description = "Downscale factor") - public int downscale = 0; + @Parameter(names = "--scale", + description = "Scale factor for downscaling") + public double scale = 0.0; } public static int defaultType = 0; public static boolean defaultShowBackground = false; + public static String baseUrl = "http://renderer-dev.int.janelia.org:8080/render-ws/v1"; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; @Override @@ -145,39 +156,46 @@ private static void addKeyListener() { })).start(); } - public static void main(final String[] args) { + public static void main(final String[] args) throws IOException { final Parameters params = new Parameters(); params.parse(args); new ImageJ(); SwingUtilities.invokeLater(BG_Plugin::addKeyListener); - IJ.log("Opening " + params.dataset + " from " + params.n5Path); - RandomAccessibleInterval img = N5Utils.open(new N5FSReader(params.n5Path), params.dataset); - if (img.numDimensions() > 2) { - IJ.log("Showing slice " + params.z); - img = Views.hyperSlice(img, 2, params.z); - } - final RandomAccessibleInterval downscaled = downSample(img, params.downscale); - ImageJFunctions.show(downscaled, "Original"); + IJ.log("Opening " + params.owner + "/" + params.project + "/" + params.stack); + IJ.log("Showing slice " + params.z); + + final ImagePlus img = renderImage(params); + img.show(); } - private static RandomAccessibleInterval downSample(final RandomAccessibleInterval img, int downscale) { - if (downscale < 1) { + private static ImagePlus renderImage(final Parameters params) throws IOException { + final RenderDataClient client = new RenderDataClient(baseUrl, params.owner, params.project); + final Bounds layerBounds = client.getLayerBounds(params.stack, (double) params.z); + final long x = layerBounds.getMinX().longValue(); + final long y = layerBounds.getMinY().longValue(); + final long w = layerBounds.getWidth(); + final long h = layerBounds.getHeight(); + + if (params.scale == 0.0) { // automatically determine downscale factor - IJ.log("No downscaling factor given, choosing factor automatically..."); - final long[] dims = img.dimensionsAsLongArray(); - final long maxSize = 2000 * 2000; - downscale = (int) Math.ceil(Math.sqrt((double) (dims[0] * dims[1]) / maxSize)); + IJ.log("No scale given, choosing automatically..."); + params.scale = Math.min(1, Math.sqrt((double) MAX_SIZE / (w * h))); } - if (downscale == 1) { + if (params.scale == 1) { IJ.log("No downscaling, showing original image at full size."); - return img; } else { - IJ.log("Isotropically downscaling by factor of " + downscale); - Gauss3.gauss(downscale / 2.0, Views.extendMirrorSingle(img), img); - return Views.subsample(img, downscale); + IJ.log("Isotropically downscaling by factor of " + params.scale); } + + final ImageProcessorWithMasks ipwm = RenderTools.renderImage(ImageProcessorCache.DISABLED_CACHE, + baseUrl, params.owner, params.project, params.stack, + x, y, params.z, w, h, + params.scale, false); + + final String title = params.project + "-" + params.stack + "(z=" + params.z + ")"; + return new ImagePlus(title, ipwm.ip); } } diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index 25df8f19f..f30e3f1a2 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -59,7 +59,7 @@ * }, { * "fromZ": 3, * "modelType": "fourthOrder", - * "coefficients": [ 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 + * "coefficients": [ 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 ] * } * ] *
From c201bafdbde3d1704d25fbb8197ac8d40fd459c7 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 26 Nov 2024 11:01:56 -0500 Subject: [PATCH 41/56] Reduce the number of partitions in spark client --- .../spark/intensityadjust/BackgroundCorrectionClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index f30e3f1a2..f38d3b4e9 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -178,7 +178,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti final Broadcast parametersBroadcast = sparkContext.broadcast(parameters); final List blocks = Grid.create(dimensions, blockSize); - final JavaRDD blockRDD = sparkContext.parallelize(blocks, blocks.size()); + final JavaRDD blockRDD = sparkContext.parallelize(blocks); blockRDD.foreach(block -> processSingleBlock(parametersBroadcast.value(), modelProviderBroadcast.value(), block)); @@ -201,7 +201,7 @@ private static void cloneDatasetOrGroupAttributes(final N5Reader in, final N5Wri } private static void processSingleBlock(final Parameters parameters, final BackgroundModelProvider modelProvider, final Grid.Block block) { - LOG.info("processSingleBlock: block={}", block); + LOG.info("processSingleBlock: block={}", block.gridPosition); try (final N5Reader in = new N5FSReader(parameters.n5In); final N5Writer out = new N5FSWriter(parameters.n5Out)) { From a90a7d991319bb775329bf84417625e133d06c90 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 11:07:34 -0500 Subject: [PATCH 42/56] Move BackgroundModel to render-app and rename packages --- .../alignment/filter/emshading}/BackgroundModel.java | 2 +- .../filter/emshading}/FourthOrderBackground.java | 2 +- .../filter/emshading}/QuadraticBackground.java | 2 +- .../client/{embackground => emshading}/BG_Plugin.java | 5 ++++- .../{embackground => emshading}/CorrectBackground.java | 4 +++- .../QuadraticBackgroundTest.java | 3 ++- .../intensityadjust/BackgroundCorrectionClient.java | 10 +++++----- 7 files changed, 17 insertions(+), 11 deletions(-) rename {render-ws-java-client/src/main/java/org/janelia/render/client/embackground => render-app/src/main/java/org/janelia/alignment/filter/emshading}/BackgroundModel.java (98%) rename {render-ws-java-client/src/main/java/org/janelia/render/client/embackground => render-app/src/main/java/org/janelia/alignment/filter/emshading}/FourthOrderBackground.java (97%) rename {render-ws-java-client/src/main/java/org/janelia/render/client/embackground => render-app/src/main/java/org/janelia/alignment/filter/emshading}/QuadraticBackground.java (96%) rename render-ws-java-client/src/main/java/org/janelia/render/client/{embackground => emshading}/BG_Plugin.java (96%) rename render-ws-java-client/src/main/java/org/janelia/render/client/{embackground => emshading}/CorrectBackground.java (97%) rename render-ws-java-client/src/test/java/org/janelia/render/client/{embackground => emshading}/QuadraticBackgroundTest.java (96%) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java similarity index 98% rename from render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java index 4dfe81d91..946b35943 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BackgroundModel.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java @@ -1,4 +1,4 @@ -package org.janelia.render.client.embackground; +package org.janelia.alignment.filter.emshading; import mpicbg.models.AbstractModel; import mpicbg.models.IllDefinedDataPointsException; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java similarity index 97% rename from render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java index d03278928..3df90797e 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/FourthOrderBackground.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java @@ -1,4 +1,4 @@ -package org.janelia.render.client.embackground; +package org.janelia.alignment.filter.emshading; import java.util.List; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java similarity index 96% rename from render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java index b51795315..96e806ddf 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/QuadraticBackground.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java @@ -1,4 +1,4 @@ -package org.janelia.render.client.embackground; +package org.janelia.alignment.filter.emshading; import java.util.List; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java similarity index 96% rename from render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java rename to render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java index 03d896273..7c9a4410c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java @@ -1,4 +1,4 @@ -package org.janelia.render.client.embackground; +package org.janelia.render.client.emshading; import java.awt.*; import java.awt.event.KeyEvent; @@ -15,6 +15,9 @@ import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.real.FloatType; +import org.janelia.alignment.filter.emshading.BackgroundModel; +import org.janelia.alignment.filter.emshading.FourthOrderBackground; +import org.janelia.alignment.filter.emshading.QuadraticBackground; import org.janelia.alignment.spec.Bounds; import org.janelia.alignment.util.ImageProcessorCache; import org.janelia.render.client.RenderDataClient; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java similarity index 97% rename from render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java rename to render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java index f2cb23e75..7485325df 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/embackground/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java @@ -1,4 +1,4 @@ -package org.janelia.render.client.embackground; +package org.janelia.render.client.emshading; import ij.ImageJ; import ij.gui.Roi; @@ -20,6 +20,8 @@ import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; +import org.janelia.alignment.filter.emshading.BackgroundModel; +import org.janelia.alignment.filter.emshading.FourthOrderBackground; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; diff --git a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java b/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java similarity index 96% rename from render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java rename to render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java index 6c4fc0bc0..7a34d6cc4 100644 --- a/render-ws-java-client/src/test/java/org/janelia/render/client/embackground/QuadraticBackgroundTest.java +++ b/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java @@ -1,9 +1,10 @@ -package org.janelia.render.client.embackground; +package org.janelia.render.client.emshading; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; import mpicbg.models.PointMatch; +import org.janelia.alignment.filter.emshading.QuadraticBackground; import org.junit.Assert; import org.junit.Test; diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java index f38d3b4e9..150bbd266 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java @@ -16,9 +16,9 @@ import org.apache.spark.broadcast.Broadcast; import org.janelia.alignment.util.Grid; import org.janelia.render.client.ClientRunner; -import org.janelia.render.client.embackground.BackgroundModel; -import org.janelia.render.client.embackground.FourthOrderBackground; -import org.janelia.render.client.embackground.QuadraticBackground; +import org.janelia.alignment.filter.emshading.BackgroundModel; +import org.janelia.alignment.filter.emshading.FourthOrderBackground; +import org.janelia.alignment.filter.emshading.QuadraticBackground; import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSReader; @@ -47,7 +47,7 @@ * The parameter file is a json file containing a list of z values and corresponding models. The model for each z value * is valid for all z layers starting at the given z value until the next z value in the list. * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, - * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.embackground.BG_Plugin}. + * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.emshading.BG_Plugin}. *

* In particular, the parameter file should have the following format. There is one root array, whose elements have * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: @@ -254,7 +254,7 @@ private static void processSingleBlock(final Parameters parameters, final Backgr } - private static class BackgroundModelProvider implements Serializable { + static class BackgroundModelProvider implements Serializable { private final List sortedModelSpecs; private BackgroundModelProvider(final List modelSpecs) { From c2a1502878273ec0f222e6f79e70577cdd9d9888 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 11:15:37 -0500 Subject: [PATCH 43/56] Rename background -> shading to avoid name clashes --- ...ackground.java => FourthOrderShading.java} | 16 +++---- ...cBackground.java => QuadraticShading.java} | 16 +++---- ...BackgroundModel.java => ShadingModel.java} | 10 ++-- ...ectBackground.java => CorrectShading.java} | 48 +++++++++---------- ...gin.java => ShadingCorrection_Plugin.java} | 36 +++++++------- .../emshading/QuadraticBackgroundTest.java | 10 ++-- ...ient.java => ShadingCorrectionClient.java} | 37 +++++++------- 7 files changed, 87 insertions(+), 86 deletions(-) rename render-app/src/main/java/org/janelia/alignment/filter/emshading/{FourthOrderBackground.java => FourthOrderShading.java} (74%) rename render-app/src/main/java/org/janelia/alignment/filter/emshading/{QuadraticBackground.java => QuadraticShading.java} (68%) rename render-app/src/main/java/org/janelia/alignment/filter/emshading/{BackgroundModel.java => ShadingModel.java} (92%) rename render-ws-java-client/src/main/java/org/janelia/render/client/emshading/{CorrectBackground.java => CorrectShading.java} (78%) rename render-ws-java-client/src/main/java/org/janelia/render/client/emshading/{BG_Plugin.java => ShadingCorrection_Plugin.java} (83%) rename render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/{BackgroundCorrectionClient.java => ShadingCorrectionClient.java} (90%) diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java similarity index 74% rename from render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java index 3df90797e..36978052c 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderBackground.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java @@ -4,21 +4,21 @@ /** - * A 4th order model for background correction in 2D slices of EM data. + * A 4th order model for shading correction in 2D slices of EM data. * To ensure that the model is concave, no 3rd order terms are included. */ -public class FourthOrderBackground extends BackgroundModel { +public class FourthOrderShading extends ShadingModel { - public FourthOrderBackground() { + public FourthOrderShading() { super(); } - public FourthOrderBackground(final double[] coefficients) { + public FourthOrderShading(final double[] coefficients) { super(coefficients); } - public FourthOrderBackground(final FourthOrderBackground background) { - super(background.getCoefficients()); + public FourthOrderShading(final FourthOrderShading shading) { + super(shading.getCoefficients()); } @@ -68,7 +68,7 @@ public void applyInPlace(final double[] location) { } @Override - public FourthOrderBackground copy() { - return new FourthOrderBackground(this); + public FourthOrderShading copy() { + return new FourthOrderShading(this); } } diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java similarity index 68% rename from render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java index 96e806ddf..96927ecd4 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticBackground.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java @@ -4,20 +4,20 @@ /** - * A quadratic model for background correction in 2D slices of EM data. + * A quadratic model for shading correction in 2D slices of EM data. */ -public class QuadraticBackground extends BackgroundModel { +public class QuadraticShading extends ShadingModel { - public QuadraticBackground() { + public QuadraticShading() { super(); } - public QuadraticBackground(final double[] coefficients) { + public QuadraticShading(final double[] coefficients) { super(coefficients); } - public QuadraticBackground(final QuadraticBackground background) { - super(background.getCoefficients()); + public QuadraticShading(final QuadraticShading shading) { + super(shading.getCoefficients()); } @@ -57,7 +57,7 @@ public void applyInPlace(final double[] location) { } @Override - public QuadraticBackground copy() { - return new QuadraticBackground(this); + public QuadraticShading copy() { + return new QuadraticShading(this); } } diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java similarity index 92% rename from render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java rename to render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java index 946b35943..bb7f61684 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/BackgroundModel.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java @@ -14,9 +14,9 @@ /** - * An abstract base model for background correction in 2D slices of EM data. + * An abstract base model for shading correction in 2D slices of EM data. */ -public abstract class BackgroundModel> extends AbstractModel implements Serializable { +public abstract class ShadingModel> extends AbstractModel implements Serializable { private final double[] coefficients; @@ -25,11 +25,11 @@ public abstract class BackgroundModel> extends Abst protected abstract List coefficientNames(); - public BackgroundModel() { + public ShadingModel() { coefficients = new double[nCoefficients()]; } - public BackgroundModel(final double[] coefficients) { + public ShadingModel(final double[] coefficients) { this(); System.arraycopy(coefficients, 0, this.coefficients, 0, nCoefficients()); } @@ -128,7 +128,7 @@ public void set(final T model) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("BackgroundModel{"); + final StringBuilder sb = new StringBuilder("ShadingModel{"); if (coefficients[nCoefficients() - 1] < 0) { sb.append("-"); diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java similarity index 78% rename from render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java rename to render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java index 7485325df..8263996e9 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectBackground.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java @@ -20,8 +20,8 @@ import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; -import org.janelia.alignment.filter.emshading.BackgroundModel; -import org.janelia.alignment.filter.emshading.FourthOrderBackground; +import org.janelia.alignment.filter.emshading.FourthOrderShading; +import org.janelia.alignment.filter.emshading.ShadingModel; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; @@ -36,7 +36,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -public class CorrectBackground { +public class CorrectShading { public static void main(final String[] args) throws NotEnoughDataPointsException, IllDefinedDataPointsException, IOException { @@ -55,41 +55,41 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } final long start = System.currentTimeMillis(); - final BackgroundModel backgroundModel = new FourthOrderBackground(); - fitBackgroundModel(rois, firstSlice, backgroundModel); - System.out.println("Fitted background model: " + backgroundModel); + final ShadingModel shadingModel = new FourthOrderShading(); + fitBackgroundModel(rois, firstSlice, shadingModel); + System.out.println("Fitted shading model: " + shadingModel); System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); - final RandomAccessibleInterval background = createBackgroundImage(backgroundModel, firstSlice); - final RandomAccessibleInterval corrected = correctBackground(firstSlice, background, new UnsignedByteType()); + final RandomAccessibleInterval shading = createBackgroundImage(shadingModel, firstSlice); + final RandomAccessibleInterval corrected = correctBackground(firstSlice, shading, new UnsignedByteType()); new ImageJ(); ImageJFunctions.show(firstSlice, "Original"); - ImageJFunctions.show(background, "Background"); + ImageJFunctions.show(shading, "Shading"); ImageJFunctions.show(corrected, "Corrected"); } } public static & RealType> - void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final BackgroundModel backgroundModel) + void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final ShadingModel shadingModel) throws NotEnoughDataPointsException, IllDefinedDataPointsException { // transform pixel coordinates into [-1, 1] x [-1, 1] final double scaleX = slice.dimension(0) / 2.0; final double scaleY = slice.dimension(1) / 2.0; - // fit a quadratic background model with all the points in the first slice + // fit a quadratic shading model with all the points in the first slice final List matches = new ArrayList<>(); final RandomAccess ra = slice.randomAccess(); for (final java.awt.Point point : extractInterestPoints(rois)) { - final double x = BackgroundModel.scaleCoordinate(point.x, scaleX); - final double y = BackgroundModel.scaleCoordinate(point.y, scaleY); + final double x = ShadingModel.scaleCoordinate(point.x, scaleX); + final double y = ShadingModel.scaleCoordinate(point.y, scaleY); final double z = ra.setPositionAndGet(point.x, point.y).getRealDouble(); matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{z}))); } - backgroundModel.fit(matches); - backgroundModel.normalize(); + shadingModel.fit(matches); + shadingModel.normalize(); } private static Set extractInterestPoints(final List rois) { @@ -103,34 +103,34 @@ private static Set extractInterestPoints(final List rois) { } public static & RealType> - RandomAccessibleInterval createBackgroundImage(final BackgroundModel backgroundModel, final RandomAccessibleInterval slice) { + RandomAccessibleInterval createBackgroundImage(final ShadingModel shadingModel, final RandomAccessibleInterval slice) { // transform pixel coordinates into [-1, 1] x [-1, 1] final double scaleX = slice.dimension(0) / 2.0; final double scaleY = slice.dimension(1) / 2.0; final double[] location = new double[2]; - final RealRandomAccessible background = new FunctionRealRandomAccessible<>(2, (pos, value) -> { - location[0] = BackgroundModel.scaleCoordinate(pos.getDoublePosition(0), scaleX); - location[1] = BackgroundModel.scaleCoordinate(pos.getDoublePosition(1), scaleY); - backgroundModel.applyInPlace(location); + final RealRandomAccessible shading = new FunctionRealRandomAccessible<>(2, (pos, value) -> { + location[0] = ShadingModel.scaleCoordinate(pos.getDoublePosition(0), scaleX); + location[1] = ShadingModel.scaleCoordinate(pos.getDoublePosition(1), scaleY); + shadingModel.applyInPlace(location); value.setReal(location[0]); }, FloatType::new); - return Views.interval(Views.raster(background), slice); + return Views.interval(Views.raster(shading), slice); } @SuppressWarnings({"rawtypes", "unchecked"}) public static & RealType> - RandomAccessibleInterval correctBackground(final RandomAccessibleInterval slice, final RandomAccessibleInterval background, final T type) { + RandomAccessibleInterval correctBackground(final RandomAccessibleInterval slice, final RandomAccessibleInterval shading, final T type) { final RandomAccessibleInterval corrected; if (type instanceof UnsignedByteType) { - corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { + corrected = (RandomAccessibleInterval) Converters.convert(slice, shading, (s, b, o) -> { o.set(UnsignedByteType.getCodedSignedByteChecked((int) (s.getRealDouble() - b.getRealDouble()))); }, new UnsignedByteType()); } else if (type instanceof UnsignedShortType) { - corrected = (RandomAccessibleInterval) Converters.convert(slice, background, (s, b, o) -> { + corrected = (RandomAccessibleInterval) Converters.convert(slice, shading, (s, b, o) -> { o.set(UnsignedShortType.getCodedSignedShortChecked((int) (s.getRealDouble() - b.getRealDouble()))); }, new UnsignedShortType()); } else { diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java similarity index 83% rename from render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java rename to render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java index 7c9a4410c..e085d0fea 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/BG_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java @@ -15,9 +15,9 @@ import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.real.FloatType; -import org.janelia.alignment.filter.emshading.BackgroundModel; -import org.janelia.alignment.filter.emshading.FourthOrderBackground; -import org.janelia.alignment.filter.emshading.QuadraticBackground; +import org.janelia.alignment.filter.emshading.QuadraticShading; +import org.janelia.alignment.filter.emshading.ShadingModel; +import org.janelia.alignment.filter.emshading.FourthOrderShading; import org.janelia.alignment.spec.Bounds; import org.janelia.alignment.util.ImageProcessorCache; import org.janelia.render.client.RenderDataClient; @@ -33,7 +33,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.display.imagej.ImageJFunctions; -public class BG_Plugin implements PlugIn { +public class ShadingCorrection_Plugin implements PlugIn { public static final int MAX_SIZE = 2000 * 2000; @@ -77,10 +77,10 @@ public void run(final String arg) { return; } - final GenericDialog gd = new GenericDialog("Fit background correction"); + final GenericDialog gd = new GenericDialog("Fit shading correction"); gd.addChoice("Fit type", fitTypes, fitTypes[defaultType]); - gd.addCheckbox("Show background image", defaultShowBackground); + gd.addCheckbox("Show shading image", defaultShowBackground); gd.showDialog(); if (gd.wasCanceled()) { @@ -104,7 +104,7 @@ public static List getROIs() { final RoiManager rm = RoiManager.getInstance(); if (rm == null || rm.getCount() == 0) { - IJ.log("Please define ROIs first before running background correction."); + IJ.log("Please define ROIs first before running shading correction."); return null; } @@ -114,27 +114,27 @@ public static List getROIs() { public static void fit(final int type, final List rois, final boolean showBackground) throws NotEnoughDataPointsException, IllDefinedDataPointsException { IJ.log("Fitting with " + fitTypes[type] + " model..."); - final BackgroundModel backgroundModel; + final ShadingModel shadingModel; if (fitTypes[type].equals("Quadratic")) { - backgroundModel = new QuadraticBackground(); + shadingModel = new QuadraticShading(); } else if (fitTypes[type].equals("Fourth Order")) { - backgroundModel = new FourthOrderBackground(); + shadingModel = new FourthOrderShading(); } else { throw new IllegalArgumentException("Unknown fit type: " + fitTypes[type]); } final long start = System.currentTimeMillis(); final RandomAccessibleInterval img = ImageJFunctions.wrap(IJ.getImage()); - CorrectBackground.fitBackgroundModel(rois, img, backgroundModel); - IJ.log("Fitted background model: " + backgroundModel); + CorrectShading.fitBackgroundModel(rois, img, shadingModel); + IJ.log("Fitted shading model: " + shadingModel); IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms."); - IJ.log("Raw coefficients: " + Arrays.toString(backgroundModel.getCoefficients())); + IJ.log("Raw coefficients: " + Arrays.toString(shadingModel.getCoefficients())); - final RandomAccessibleInterval background = CorrectBackground.createBackgroundImage(backgroundModel, img); - final RandomAccessibleInterval corrected = CorrectBackground.correctBackground(img, background, new UnsignedByteType()); + final RandomAccessibleInterval shading = CorrectShading.createBackgroundImage(shadingModel, img); + final RandomAccessibleInterval corrected = CorrectShading.correctBackground(img, shading, new UnsignedByteType()); if (showBackground) { - ImageJFunctions.show(background, "Background"); + ImageJFunctions.show(shading, "Shading"); } ImageJFunctions.show(corrected, "Corrected"); @@ -148,7 +148,7 @@ private static void addKeyListener() { if (e.getID() == KeyEvent.KEY_PRESSED) { switch (e.getKeyCode()) { case KeyEvent.VK_F1: - new BG_Plugin().run(null); + new ShadingCorrection_Plugin().run(null); break; case KeyEvent.VK_F2: System.out.println("F2 key pressed (not assigned)"); @@ -164,7 +164,7 @@ public static void main(final String[] args) throws IOException { params.parse(args); new ImageJ(); - SwingUtilities.invokeLater(BG_Plugin::addKeyListener); + SwingUtilities.invokeLater(ShadingCorrection_Plugin::addKeyListener); IJ.log("Opening " + params.owner + "/" + params.project + "/" + params.stack); IJ.log("Showing slice " + params.z); diff --git a/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java b/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java index 7a34d6cc4..48fa7d31d 100644 --- a/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java +++ b/render-ws-java-client/src/test/java/org/janelia/render/client/emshading/QuadraticBackgroundTest.java @@ -4,7 +4,7 @@ import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; import mpicbg.models.PointMatch; -import org.janelia.alignment.filter.emshading.QuadraticBackground; +import org.janelia.alignment.filter.emshading.QuadraticShading; import org.junit.Assert; import org.junit.Test; @@ -16,7 +16,7 @@ public class QuadraticBackgroundTest { @Test public void simpleModelProducesCorrectResults() throws NotEnoughDataPointsException, IllDefinedDataPointsException { // 0.5 * x^2 + 0.5 * y^2 + 1 - final QuadraticBackground background = new QuadraticBackground(new double[]{1, 0, 0, 0.5, 0, 0.5}); + final QuadraticShading background = new QuadraticShading(new double[]{1, 0, 0, 0.5, 0, 0.5}); final double[] location1 = new double[]{0, 0}; background.applyInPlace(location1); @@ -51,7 +51,7 @@ public void simpleModelIsComputedCorrectly() throws NotEnoughDataPointsException matches.add(new PointMatch(new Point(new double[]{-1, -1}), new Point(new double[]{1}))); matches.add(new PointMatch(new Point(new double[]{0, 1}), new Point(new double[]{0.5}))); - final QuadraticBackground background = new QuadraticBackground(); + final QuadraticShading background = new QuadraticShading(); background.fit(matches); // order of coefficients: {1, y, x, y^2, x*y, x^2} @@ -60,7 +60,7 @@ public void simpleModelIsComputedCorrectly() throws NotEnoughDataPointsException @Test public void modelCanBeRecoveredWithManyPoints() throws NotEnoughDataPointsException, IllDefinedDataPointsException { - final QuadraticBackground background = new QuadraticBackground(new double[]{1, 2, 3, 4, 5, 6}); + final QuadraticShading background = new QuadraticShading(new double[]{1, 2, 3, 4, 5, 6}); final List matches = new ArrayList<>(); for (int i = 0; i < 100; i++) { @@ -71,7 +71,7 @@ public void modelCanBeRecoveredWithManyPoints() throws NotEnoughDataPointsExcept matches.add(new PointMatch(new Point(new double[]{x, y}), new Point(new double[]{location[0]}))); } - final QuadraticBackground recovered = new QuadraticBackground(); + final QuadraticShading recovered = new QuadraticShading(); recovered.fit(matches); Assert.assertArrayEquals(background.getCoefficients(), recovered.getCoefficients(), 1e-12); diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java similarity index 90% rename from render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java rename to render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index 150bbd266..3ca3e7e4c 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/BackgroundCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -16,9 +16,10 @@ import org.apache.spark.broadcast.Broadcast; import org.janelia.alignment.util.Grid; import org.janelia.render.client.ClientRunner; -import org.janelia.alignment.filter.emshading.BackgroundModel; -import org.janelia.alignment.filter.emshading.FourthOrderBackground; -import org.janelia.alignment.filter.emshading.QuadraticBackground; +import org.janelia.alignment.filter.emshading.ShadingModel; +import org.janelia.alignment.filter.emshading.FourthOrderShading; +import org.janelia.alignment.filter.emshading.QuadraticShading; +import org.janelia.render.client.emshading.ShadingCorrection_Plugin; import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSReader; @@ -40,14 +41,14 @@ import java.util.Map; /** - * Spark client for background correction by a layer-wise quadratic or fourth order model. + * Spark client for shading correction by a layer-wise quadratic or fourth order model. * The client takes as input an N5 container with a 3D 16bit dataset and a parameter file, and writes the corrected data * to a new dataset in a given container. *

* The parameter file is a json file containing a list of z values and corresponding models. The model for each z value * is valid for all z layers starting at the given z value until the next z value in the list. * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, - * respectively). Coefficients can be found interactively using {@link org.janelia.render.client.emshading.BG_Plugin}. + * respectively). Coefficients can be found interactively using {@link ShadingCorrection_Plugin}. *

* In particular, the parameter file should have the following format. There is one root array, whose elements have * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: @@ -64,7 +65,7 @@ * ] *
*/ -public class BackgroundCorrectionClient implements Serializable { +public class ShadingCorrectionClient implements Serializable { public static class Parameters extends CommandLineParameters { @Parameter(names = "--n5In", @@ -98,7 +99,7 @@ public static class Parameters extends CommandLineParameters { public String parameterFile; } - private static final Logger LOG = LoggerFactory.getLogger(BackgroundCorrectionClient.class); + private static final Logger LOG = LoggerFactory.getLogger(ShadingCorrectionClient.class); private final Parameters parameters; @@ -108,7 +109,7 @@ public static void main(final String[] args) { public void runClient(final String[] args) throws Exception { final Parameters parameters = new Parameters(); parameters.parse(args); - final BackgroundCorrectionClient client = new BackgroundCorrectionClient(parameters); + final ShadingCorrectionClient client = new ShadingCorrectionClient(parameters); client.run(); } }; @@ -116,13 +117,13 @@ public void runClient(final String[] args) throws Exception { } - public BackgroundCorrectionClient(final Parameters parameters) { + public ShadingCorrectionClient(final Parameters parameters) { LOG.info("init: parameters={}", parameters); this.parameters = parameters; } public void run() throws IOException { - final SparkConf conf = new SparkConf().setAppName("BackgroundCorrectionClient"); + final SparkConf conf = new SparkConf().setAppName("ShadingCorrectionClient"); try (final JavaSparkContext sparkContext = new JavaSparkContext(conf)) { final String sparkAppId = sparkContext.getConf().getAppId(); LOG.info("run: appId is {}", sparkAppId); @@ -219,7 +220,7 @@ private static void processSingleBlock(final Parameters parameters, final Backgr // process block z-slice by z-slice for (int z = (int) block.min(2); z <= block.max(2); z++) { - final BackgroundModel model = modelProvider.getModel(z); + final ShadingModel model = modelProvider.getModel(z); if (model == null) { LOG.warn("No model found for z={}", z); continue; @@ -240,8 +241,8 @@ private static void processSingleBlock(final Parameters parameters, final Backgr continue; } - location[0] = BackgroundModel.scaleCoordinate(imgCursor.getDoublePosition(0), xScale); - location[1] = BackgroundModel.scaleCoordinate(imgCursor.getDoublePosition(1), yScale); + location[0] = ShadingModel.scaleCoordinate(imgCursor.getDoublePosition(0), xScale); + location[1] = ShadingModel.scaleCoordinate(imgCursor.getDoublePosition(1), yScale); model.applyInPlace(location); pixel.setReal(UnsignedShortType.getCodedSignedShortChecked((int) (pixel.getRealDouble() - location[0]))); @@ -262,7 +263,7 @@ private BackgroundModelProvider(final List modelSpecs) { this.sortedModelSpecs.sort(Collections.reverseOrder(Comparator.comparingInt(ModelSpec::getZ))); } - public BackgroundModel getModel(final int z) { + public ShadingModel getModel(final int z) { for (final ModelSpec modelSpec : sortedModelSpecs) { if (z >= modelSpec.getZ()) { return modelSpec.getModel(); @@ -286,7 +287,7 @@ public static BackgroundModelProvider fromJson(final JsonElement jsonData) { // validation of json data for (final ModelSpec modelSpec : modelSpecs) { LOG.info("Found model spec: {}", modelSpec); - final BackgroundModel ignored = modelSpec.getModel(); + final ShadingModel ignored = modelSpec.getModel(); } return new BackgroundModelProvider(modelSpecs); @@ -309,11 +310,11 @@ public void setCoefficients(final double[] coefficients) { this.coefficients = coefficients; } - public BackgroundModel getModel() { + public ShadingModel getModel() { if (modelType.equals("quadratic")) { - return new QuadraticBackground(coefficients); + return new QuadraticShading(coefficients); } else if (modelType.equals("fourthOrder")) { - return new FourthOrderBackground(coefficients); + return new FourthOrderShading(coefficients); } else { throw new IllegalArgumentException("Unknown model type: " + modelType); } From 7aa7f479ef254a928e9607475ed56a0597c83c36 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 11:41:30 -0500 Subject: [PATCH 44/56] Add first version of filter-based shading correction --- .../filter/ShadingCorrectionFilter.java | 101 ++++++++ .../ShadingCorrectionTileClient.java | 225 ++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java create mode 100644 render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java diff --git a/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java new file mode 100644 index 000000000..98322c07b --- /dev/null +++ b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java @@ -0,0 +1,101 @@ +package org.janelia.alignment.filter; + +import ij.process.ImageProcessor; +import org.janelia.alignment.filter.emshading.FourthOrderShading; +import org.janelia.alignment.filter.emshading.QuadraticShading; +import org.janelia.alignment.filter.emshading.ShadingModel; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +public class ShadingCorrectionFilter implements Filter { + + public enum ShadingCorrectionMethod { + QUADRATIC(QuadraticShading::new), + FOURTH_ORDER(FourthOrderShading::new); + + private final Function> modelFactory; + + ShadingCorrectionMethod(final Function> modelFactory) { + this.modelFactory = modelFactory; + } + + public ShadingModel create(final double[] coefficients) { + return modelFactory.apply(coefficients); + } + + public static ShadingCorrectionMethod fromString(final String method) { + for (final ShadingCorrectionMethod shadingCorrectionMethod : values()) { + if (shadingCorrectionMethod.name().equalsIgnoreCase(method)) { + return shadingCorrectionMethod; + } + } + throw new IllegalArgumentException("Unknown shading correction method: " + method); + } + + public static ShadingCorrectionMethod forModel(final ShadingModel model) { + if (model instanceof QuadraticShading) { + return QUADRATIC; + } else if (model instanceof FourthOrderShading) { + return FOURTH_ORDER; + } else { + throw new IllegalArgumentException("Unknown shading model class: " + model.getClass().getName()); + } + } + } + + private ShadingCorrectionMethod correctionMethod; + private double[] coefficients; + + // empty constructor required to create instances from specifications + @SuppressWarnings("unused") + public ShadingCorrectionFilter() { + } + + public ShadingCorrectionFilter(final ShadingModel model) { + this(ShadingCorrectionMethod.forModel(model), model.getCoefficients()); + } + + public ShadingCorrectionFilter(final ShadingCorrectionMethod correctionMethod, final double[] coefficients) { + this.correctionMethod = correctionMethod; + this.coefficients = coefficients; + } + + @Override + public void init(final Map params) { + this.correctionMethod = ShadingCorrectionMethod.fromString(Filter.getStringParameter("correctionMethod", params)); + final String[] rawCoefficients = Filter.getCommaSeparatedStringParameter("coefficients", params); + this.coefficients = Arrays.stream(rawCoefficients).mapToDouble(Double::parseDouble).toArray(); + } + + @Override + public Map toParametersMap() { + final Map map = new LinkedHashMap<>(); + map.put("correctionMethod", correctionMethod.name()); + map.put("coefficients", Arrays.toString(coefficients)); + return map; + } + + @Override + public void process(final ImageProcessor ip, final double scale) { + // transform pixel coordinates into [-1, 1] x [-1, 1] + final double scaleX = ip.getWidth() / 2.0; + final double scaleY = ip.getHeight() / 2.0; + final ShadingModel shadingModel = correctionMethod.create(coefficients); + + // subtract shading model from image + final double[] location = new double[2]; + for (int i = 0; i < ip.getWidth(); i++) { + for (int j = 0; j < ip.getHeight(); j++) { + location[0] = ShadingModel.scaleCoordinate(i, scaleX); + location[1] = ShadingModel.scaleCoordinate(j, scaleY); + shadingModel.applyInPlace(location); + + final double value = ip.getPixelValue(i, j) - location[0]; + ip.putPixelValue(i, j, value); + } + } + } +} diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java new file mode 100644 index 000000000..c592694e2 --- /dev/null +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -0,0 +1,225 @@ +package org.janelia.render.client.spark.intensityadjust; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; +import mpicbg.models.CoordinateTransform; +import mpicbg.models.CoordinateTransformList; +import mpicbg.models.IllDefinedDataPointsException; +import mpicbg.models.NotEnoughDataPointsException; +import mpicbg.models.Point; +import mpicbg.models.PointMatch; +import org.apache.spark.SparkConf; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.broadcast.Broadcast; +import org.janelia.alignment.filter.FilterSpec; +import org.janelia.alignment.filter.ShadingCorrectionFilter; +import org.janelia.alignment.spec.ResolvedTileSpecCollection; +import org.janelia.alignment.spec.SectionData; +import org.janelia.alignment.spec.TileSpec; +import org.janelia.alignment.spec.stack.StackMetaData; +import org.janelia.render.client.ClientRunner; +import org.janelia.render.client.RenderDataClient; +import org.janelia.alignment.filter.emshading.ShadingModel; +import org.janelia.render.client.emshading.ShadingCorrection_Plugin; +import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.render.client.parameter.RenderWebServiceParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.janelia.render.client.spark.intensityadjust.ShadingCorrectionClient.BackgroundModelProvider; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Spark client for shading correction by a layer-wise quadratic or fourth order model. + * The client takes as input a render stack and a parameter file and creates a new stack with appropriate shading + * correction filters. The shading is assumed to be an artifact in the imaging space, so it is applied by finding + * the position of a tile within the z-layer. + *

+ * The parameter file is a json file containing a list of z values and corresponding models. The model for each z value + * is valid for all z layers starting at the given z value until the next z value in the list. + * Models are specified by an identifier ("quadratic" or "fourthOrder") and a list of coefficients (6 or 9, + * respectively). Coefficients can be found interactively using {@link ShadingCorrection_Plugin}. + *

+ * In particular, the parameter file should have the following format. There is one root array, whose elements have + * exactly keys: "fromZ", "modelType", and "coefficients"s, e.g.: + *
+ * [ {
+ *     "fromZ": 1,
+ *     "modelType": "quadratic",
+ *     "coefficients": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]
+ *   }, {
+ *     "fromZ": 3,
+ *     "modelType": "fourthOrder",
+ *     "coefficients": [ 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 ]
+ *   }
+ * ]
+ * 
+ */ +public class ShadingCorrectionTileClient implements Serializable { + + public static class Parameters extends CommandLineParameters { + @ParametersDelegate + public RenderWebServiceParameters webservice = new RenderWebServiceParameters(); + + @Parameter(names = "--stack", + description = "Name of the source stack in the render project", + required = true) + public String stack; + + @Parameter(names = "--targetStack", + description = "Name of the target stack in the render project", + required = true) + public String targetStack; + + @Parameter(names = "--parameterFile", + description = "Path to the parameter json file containing a list of z values and their corresponding models. See class description for details.", + required = true) + public String parameterFile; + } + + private static final Logger LOG = LoggerFactory.getLogger(ShadingCorrectionTileClient.class); + + private final Parameters parameters; + + public static void main(final String[] args) { + final ClientRunner clientRunner = new ClientRunner(args) { + @Override + public void runClient(final String[] args) throws Exception { + final Parameters parameters = new Parameters(); + parameters.parse(args); + final ShadingCorrectionTileClient client = new ShadingCorrectionTileClient(parameters); + client.run(); + } + }; + clientRunner.run(); + } + + + public ShadingCorrectionTileClient(final Parameters parameters) { + LOG.info("init: parameters={}", parameters); + this.parameters = parameters; + } + + public void run() throws IOException { + final SparkConf conf = new SparkConf().setAppName("ShadingCorrectionClient"); + try (final JavaSparkContext sparkContext = new JavaSparkContext(conf)) { + final String sparkAppId = sparkContext.getConf().getAppId(); + LOG.info("run: appId is {}", sparkAppId); + runWithContext(sparkContext); + } + } + + public void runWithContext(final JavaSparkContext sparkContext) throws IOException { + + LOG.info("runWithContext: entry"); + + final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile); + + final RenderDataClient renderClient = parameters.webservice.getDataClient(); + setUpTargetStack(renderClient); + + final List zValues = renderClient.getStackZValues(parameters.stack); + final double minZ = zValues.get(0); + final double maxZ = zValues.get(zValues.size() - 1); + final ResolvedTileSpecCollection rtsc = renderClient.getResolvedTilesForZRange(parameters.stack, minZ, maxZ); + + final List sectionData = renderClient.getStackSectionData(parameters.stack, minZ, maxZ); + final Map sectionDataMap = new HashMap<>(); + sectionData.forEach(section -> sectionDataMap.put(section.getZ().intValue(), section)); + + // parallelize computation over tile specs (broadcasting some data that is needed for all tile specs) + final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); + final Broadcast> sectionDataBroadcast = sparkContext.broadcast(sectionDataMap); + + final List tileSpecs = new ArrayList<>(rtsc.getTileSpecs()); + final List enrichedTileSpecs = sparkContext.parallelize(tileSpecs) + .map(tileSpec -> addBackgroundCorrection(tileSpec, modelProviderBroadcast.value(), sectionDataBroadcast.value())) + .collect(); + + final ResolvedTileSpecCollection enrichedRtsc = new ResolvedTileSpecCollection(rtsc.getTransformSpecs(), enrichedTileSpecs); + renderClient.saveResolvedTiles(enrichedRtsc, parameters.targetStack, null); + + completeTargetStack(renderClient); + + LOG.info("runWithContext: exit"); + } + + private void setUpTargetStack(final RenderDataClient dataClient) throws IOException { + final StackMetaData sourceStackMetaData = dataClient.getStackMetaData(parameters.stack); + dataClient.setupDerivedStack(sourceStackMetaData, parameters.targetStack); + LOG.info("setUpTargetStack: setup stack {}", parameters.targetStack); + } + + private void completeTargetStack(final RenderDataClient dataClient) throws IOException { + dataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); + LOG.info("completeTargetStack: setup stack {}", parameters.targetStack); + } + + private TileSpec addBackgroundCorrection( + final TileSpec tileSpec, + final BackgroundModelProvider modelProvider, + final Map sectionData + ) { + final int z = tileSpec.getZ().intValue(); + final ShadingModel layerModel = modelProvider.getModel(z); + if (layerModel == null) { + LOG.warn("No model found for z={}", z); + return tileSpec; + } + + // get uniform grid of points in tile + final int nSamples = (int) Math.ceil(Math.sqrt(layerModel.getMinNumMatches())); + final List points = gridOnTile(tileSpec, nSamples); + + final SectionData section = sectionData.get(z); + final double scaleX = section.getMinX() + 0.5 * section.getWidth(); + final double scaleY = section.getMinY() + 0.5 * section.getHeight(); + + final CoordinateTransformList transforms = tileSpec.getTransformList(); + final List matches = new ArrayList<>(); + + for (final double[] tilePoint : points) { + // transform point to global space and evaluate shading model + final double[] globalPoint = transforms.apply(tilePoint); + final double[] value = layerModel.apply(globalPoint); + + // scale coordinates to [-1, 1] to fit a local model on the tile + tilePoint[0] = ShadingModel.scaleCoordinate(tilePoint[0], scaleX); + tilePoint[1] = ShadingModel.scaleCoordinate(tilePoint[1], scaleY); + matches.add(new PointMatch(new Point(tilePoint), new Point(value))); + } + + try { + layerModel.fit(matches); + } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { + throw new RuntimeException(e); + } + + // convert to filter and add to tile spec + final ShadingCorrectionFilter filter = new ShadingCorrectionFilter(layerModel); + tileSpec.setFilterSpec(FilterSpec.forFilter(filter)); + + return tileSpec; + } + + final List gridOnTile(final TileSpec tileSpec, final int n) { + final List points = new ArrayList<>(); + + final double incrementX = (double) tileSpec.getWidth() / (n - 1); + final double incrementY = (double) tileSpec.getHeight() / (n - 1); + + for (double x = tileSpec.getMinX(); x < tileSpec.getMaxX(); x += incrementX) { + for (double y = tileSpec.getMinY(); y < tileSpec.getMaxY(); y += incrementY) { + final double[] point = new double[] {x, y}; + points.add(point); + } + } + + return points; + } +} From b7d9dd775fc44c08209d22702c844732367de23c Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 11:50:07 -0500 Subject: [PATCH 45/56] Remove type argument from ShadingModel --- .../alignment/filter/ShadingCorrectionFilter.java | 12 ++++++------ .../filter/emshading/FourthOrderShading.java | 2 +- .../alignment/filter/emshading/QuadraticShading.java | 2 +- .../alignment/filter/emshading/ShadingModel.java | 4 ++-- .../render/client/emshading/CorrectShading.java | 6 +++--- .../client/emshading/ShadingCorrection_Plugin.java | 8 ++++---- .../intensityadjust/ShadingCorrectionClient.java | 8 ++++---- .../intensityadjust/ShadingCorrectionTileClient.java | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java index 98322c07b..e91d7e7de 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java @@ -16,13 +16,13 @@ public enum ShadingCorrectionMethod { QUADRATIC(QuadraticShading::new), FOURTH_ORDER(FourthOrderShading::new); - private final Function> modelFactory; + private final Function modelFactory; - ShadingCorrectionMethod(final Function> modelFactory) { + ShadingCorrectionMethod(final Function modelFactory) { this.modelFactory = modelFactory; } - public ShadingModel create(final double[] coefficients) { + public ShadingModel create(final double[] coefficients) { return modelFactory.apply(coefficients); } @@ -35,7 +35,7 @@ public static ShadingCorrectionMethod fromString(final String method) { throw new IllegalArgumentException("Unknown shading correction method: " + method); } - public static ShadingCorrectionMethod forModel(final ShadingModel model) { + public static ShadingCorrectionMethod forModel(final ShadingModel model) { if (model instanceof QuadraticShading) { return QUADRATIC; } else if (model instanceof FourthOrderShading) { @@ -54,7 +54,7 @@ public static ShadingCorrectionMethod forModel(final ShadingModel model) { public ShadingCorrectionFilter() { } - public ShadingCorrectionFilter(final ShadingModel model) { + public ShadingCorrectionFilter(final ShadingModel model) { this(ShadingCorrectionMethod.forModel(model), model.getCoefficients()); } @@ -83,7 +83,7 @@ public void process(final ImageProcessor ip, final double scale) { // transform pixel coordinates into [-1, 1] x [-1, 1] final double scaleX = ip.getWidth() / 2.0; final double scaleY = ip.getHeight() / 2.0; - final ShadingModel shadingModel = correctionMethod.create(coefficients); + final ShadingModel shadingModel = correctionMethod.create(coefficients); // subtract shading model from image final double[] location = new double[2]; diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java index 36978052c..8b8b1f8d9 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/FourthOrderShading.java @@ -7,7 +7,7 @@ * A 4th order model for shading correction in 2D slices of EM data. * To ensure that the model is concave, no 3rd order terms are included. */ -public class FourthOrderShading extends ShadingModel { +public class FourthOrderShading extends ShadingModel { public FourthOrderShading() { super(); diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java index 96927ecd4..0465fe39d 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/QuadraticShading.java @@ -6,7 +6,7 @@ /** * A quadratic model for shading correction in 2D slices of EM data. */ -public class QuadraticShading extends ShadingModel { +public class QuadraticShading extends ShadingModel { public QuadraticShading() { super(); diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java index bb7f61684..ce8c693b3 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java @@ -16,7 +16,7 @@ /** * An abstract base model for shading correction in 2D slices of EM data. */ -public abstract class ShadingModel> extends AbstractModel implements Serializable { +public abstract class ShadingModel extends AbstractModel implements Serializable { private final double[] coefficients; @@ -122,7 +122,7 @@ public double[] apply(final double[] location) { } @Override - public void set(final T model) { + public void set(final ShadingModel model) { System.arraycopy(model.getCoefficients(), 0, coefficients, 0, nCoefficients()); } diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java index 8263996e9..2f645b619 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/CorrectShading.java @@ -55,7 +55,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } final long start = System.currentTimeMillis(); - final ShadingModel shadingModel = new FourthOrderShading(); + final ShadingModel shadingModel = new FourthOrderShading(); fitBackgroundModel(rois, firstSlice, shadingModel); System.out.println("Fitted shading model: " + shadingModel); System.out.println("Fitting took " + (System.currentTimeMillis() - start) + "ms."); @@ -71,7 +71,7 @@ public static void main(final String[] args) throws NotEnoughDataPointsException } public static & RealType> - void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final ShadingModel shadingModel) + void fitBackgroundModel(final List rois, final RandomAccessibleInterval slice, final ShadingModel shadingModel) throws NotEnoughDataPointsException, IllDefinedDataPointsException { // transform pixel coordinates into [-1, 1] x [-1, 1] @@ -103,7 +103,7 @@ private static Set extractInterestPoints(final List rois) { } public static & RealType> - RandomAccessibleInterval createBackgroundImage(final ShadingModel shadingModel, final RandomAccessibleInterval slice) { + RandomAccessibleInterval createBackgroundImage(final ShadingModel shadingModel, final RandomAccessibleInterval slice) { // transform pixel coordinates into [-1, 1] x [-1, 1] final double scaleX = slice.dimension(0) / 2.0; diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java index e085d0fea..06efd3d0a 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java @@ -13,7 +13,7 @@ import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks; -import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.FloatType; import org.janelia.alignment.filter.emshading.QuadraticShading; import org.janelia.alignment.filter.emshading.ShadingModel; @@ -114,7 +114,7 @@ public static List getROIs() { public static void fit(final int type, final List rois, final boolean showBackground) throws NotEnoughDataPointsException, IllDefinedDataPointsException { IJ.log("Fitting with " + fitTypes[type] + " model..."); - final ShadingModel shadingModel; + final ShadingModel shadingModel; if (fitTypes[type].equals("Quadratic")) { shadingModel = new QuadraticShading(); } else if (fitTypes[type].equals("Fourth Order")) { @@ -124,14 +124,14 @@ public static void fit(final int type, final List rois, final boolean showB } final long start = System.currentTimeMillis(); - final RandomAccessibleInterval img = ImageJFunctions.wrap(IJ.getImage()); + final RandomAccessibleInterval img = ImageJFunctions.wrap(IJ.getImage()); CorrectShading.fitBackgroundModel(rois, img, shadingModel); IJ.log("Fitted shading model: " + shadingModel); IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms."); IJ.log("Raw coefficients: " + Arrays.toString(shadingModel.getCoefficients())); final RandomAccessibleInterval shading = CorrectShading.createBackgroundImage(shadingModel, img); - final RandomAccessibleInterval corrected = CorrectShading.correctBackground(img, shading, new UnsignedByteType()); + final RandomAccessibleInterval corrected = CorrectShading.correctBackground(img, shading, new UnsignedShortType()); if (showBackground) { ImageJFunctions.show(shading, "Shading"); diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index 3ca3e7e4c..8e0bd4cb8 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -220,7 +220,7 @@ private static void processSingleBlock(final Parameters parameters, final Backgr // process block z-slice by z-slice for (int z = (int) block.min(2); z <= block.max(2); z++) { - final ShadingModel model = modelProvider.getModel(z); + final ShadingModel model = modelProvider.getModel(z); if (model == null) { LOG.warn("No model found for z={}", z); continue; @@ -263,7 +263,7 @@ private BackgroundModelProvider(final List modelSpecs) { this.sortedModelSpecs.sort(Collections.reverseOrder(Comparator.comparingInt(ModelSpec::getZ))); } - public ShadingModel getModel(final int z) { + public ShadingModel getModel(final int z) { for (final ModelSpec modelSpec : sortedModelSpecs) { if (z >= modelSpec.getZ()) { return modelSpec.getModel(); @@ -287,7 +287,7 @@ public static BackgroundModelProvider fromJson(final JsonElement jsonData) { // validation of json data for (final ModelSpec modelSpec : modelSpecs) { LOG.info("Found model spec: {}", modelSpec); - final ShadingModel ignored = modelSpec.getModel(); + final ShadingModel ignored = modelSpec.getModel(); } return new BackgroundModelProvider(modelSpecs); @@ -310,7 +310,7 @@ public void setCoefficients(final double[] coefficients) { this.coefficients = coefficients; } - public ShadingModel getModel() { + public ShadingModel getModel() { if (modelType.equals("quadratic")) { return new QuadraticShading(coefficients); } else if (modelType.equals("fourthOrder")) { diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index c592694e2..6192f9033 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -166,7 +166,7 @@ private TileSpec addBackgroundCorrection( final Map sectionData ) { final int z = tileSpec.getZ().intValue(); - final ShadingModel layerModel = modelProvider.getModel(z); + final ShadingModel layerModel = modelProvider.getModel(z); if (layerModel == null) { LOG.warn("No model found for z={}", z); return tileSpec; From 4e3043a2a5377dcb3dcc0d2558b083511c8e94a8 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 14:37:16 -0500 Subject: [PATCH 46/56] Rename BackgroundModelProvider -> ShadingModelProvider --- .../alignment/filter/emshading/ShadingModel.java | 2 +- .../intensityadjust/ShadingCorrectionClient.java | 16 ++++++++-------- .../ShadingCorrectionTileClient.java | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java index ce8c693b3..0e902900e 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/emshading/ShadingModel.java @@ -128,7 +128,7 @@ public void set(final ShadingModel model) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("ShadingModel{"); + final StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()).append("{"); if (coefficients[nCoefficients() - 1] < 0) { sb.append("-"); diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index 8e0bd4cb8..fccf7ac2c 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -136,7 +136,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti LOG.info("runWithContext: entry"); // read parameters - final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile); + final ShadingModelProvider modelProvider = ShadingModelProvider.fromJsonFile(parameters.parameterFile); // set up input and output N5 datasets final int[] blockSize; @@ -175,7 +175,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti } // parallelize computation over blocks of the input/output dataset - final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); + final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); final Broadcast parametersBroadcast = sparkContext.broadcast(parameters); final List blocks = Grid.create(dimensions, blockSize); @@ -201,7 +201,7 @@ private static void cloneDatasetOrGroupAttributes(final N5Reader in, final N5Wri }); } - private static void processSingleBlock(final Parameters parameters, final BackgroundModelProvider modelProvider, final Grid.Block block) { + private static void processSingleBlock(final Parameters parameters, final ShadingModelProvider modelProvider, final Grid.Block block) { LOG.info("processSingleBlock: block={}", block.gridPosition); try (final N5Reader in = new N5FSReader(parameters.n5In); @@ -255,10 +255,10 @@ private static void processSingleBlock(final Parameters parameters, final Backgr } - static class BackgroundModelProvider implements Serializable { + static class ShadingModelProvider implements Serializable { private final List sortedModelSpecs; - private BackgroundModelProvider(final List modelSpecs) { + private ShadingModelProvider(final List modelSpecs) { this.sortedModelSpecs = modelSpecs; this.sortedModelSpecs.sort(Collections.reverseOrder(Comparator.comparingInt(ModelSpec::getZ))); } @@ -272,14 +272,14 @@ public ShadingModel getModel(final int z) { return null; } - public static BackgroundModelProvider fromJsonFile(final String fileName) throws IOException { + public static ShadingModelProvider fromJsonFile(final String fileName) throws IOException { LOG.info("Reading model specs from file: {}", fileName); try (final FileReader reader = new FileReader(fileName)) { return fromJson(JsonParser.parseReader(reader)); } } - public static BackgroundModelProvider fromJson(final JsonElement jsonData) { + public static ShadingModelProvider fromJson(final JsonElement jsonData) { final List modelSpecs = new ArrayList<>(); Collections.addAll(modelSpecs, new Gson().fromJson(jsonData, ModelSpec[].class)); @@ -290,7 +290,7 @@ public static BackgroundModelProvider fromJson(final JsonElement jsonData) { final ShadingModel ignored = modelSpec.getModel(); } - return new BackgroundModelProvider(modelSpecs); + return new ShadingModelProvider(modelSpecs); } diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index 6192f9033..d299592ea 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -25,7 +25,7 @@ import org.janelia.render.client.parameter.RenderWebServiceParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.janelia.render.client.spark.intensityadjust.ShadingCorrectionClient.BackgroundModelProvider; +import org.janelia.render.client.spark.intensityadjust.ShadingCorrectionClient.ShadingModelProvider; import java.io.IOException; import java.io.Serializable; From 403055649cda2a22eba94674b731c55a6e66d116 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 14:39:44 -0500 Subject: [PATCH 47/56] Parallelize ShadingCorrectionTileClient over z layers Otherwise, the webserver complains about the request for tile specs being too large --- .../ShadingCorrectionTileClient.java | 125 ++++++++++-------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index d299592ea..106d8a7ab 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -30,9 +30,10 @@ import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collection; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Spark client for shading correction by a layer-wise quadratic or fourth order model. @@ -118,31 +119,28 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti LOG.info("runWithContext: entry"); - final BackgroundModelProvider modelProvider = BackgroundModelProvider.fromJsonFile(parameters.parameterFile); + final ShadingModelProvider modelProvider = ShadingModelProvider.fromJsonFile(parameters.parameterFile); final RenderDataClient renderClient = parameters.webservice.getDataClient(); setUpTargetStack(renderClient); final List zValues = renderClient.getStackZValues(parameters.stack); - final double minZ = zValues.get(0); - final double maxZ = zValues.get(zValues.size() - 1); - final ResolvedTileSpecCollection rtsc = renderClient.getResolvedTilesForZRange(parameters.stack, minZ, maxZ); - - final List sectionData = renderClient.getStackSectionData(parameters.stack, minZ, maxZ); - final Map sectionDataMap = new HashMap<>(); - sectionData.forEach(section -> sectionDataMap.put(section.getZ().intValue(), section)); - - // parallelize computation over tile specs (broadcasting some data that is needed for all tile specs) - final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); - final Broadcast> sectionDataBroadcast = sparkContext.broadcast(sectionDataMap); - - final List tileSpecs = new ArrayList<>(rtsc.getTileSpecs()); - final List enrichedTileSpecs = sparkContext.parallelize(tileSpecs) - .map(tileSpec -> addBackgroundCorrection(tileSpec, modelProviderBroadcast.value(), sectionDataBroadcast.value())) - .collect(); - - final ResolvedTileSpecCollection enrichedRtsc = new ResolvedTileSpecCollection(rtsc.getTransformSpecs(), enrichedTileSpecs); - renderClient.saveResolvedTiles(enrichedRtsc, parameters.targetStack, null); + final List stackSectionData = renderClient.getStackSectionData(parameters.stack, null, null); + + // parallelize computation over z-layers (broadcasting some data that is needed for all tile specs) + final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); + final Broadcast parametersBroadcast = sparkContext.broadcast(parameters); + final Broadcast> sectionDataBroadcast = sparkContext.broadcast(stackSectionData); + final Broadcast> zValuesBroadcast = sparkContext.broadcast(zValues); + + final List zIndices = IntStream.range(0, zValues.size()).boxed().collect(Collectors.toList()); + sparkContext.parallelize(zIndices) + .foreach(zIndex -> { + final Double z = zValuesBroadcast.getValue().get(zIndex); + final ShadingModel layerModel = modelProviderBroadcast.getValue().getModel(z.intValue()); + final SectionData layerSectionData = sectionDataBroadcast.getValue().get(zIndex); + addShadingCorrectionToLayer(z, layerModel, parametersBroadcast.value(), layerSectionData); + }); completeTargetStack(renderClient); @@ -160,61 +158,84 @@ private void completeTargetStack(final RenderDataClient dataClient) throws IOExc LOG.info("completeTargetStack: setup stack {}", parameters.targetStack); } - private TileSpec addBackgroundCorrection( - final TileSpec tileSpec, - final BackgroundModelProvider modelProvider, - final Map sectionData - ) { - final int z = tileSpec.getZ().intValue(); - final ShadingModel layerModel = modelProvider.getModel(z); + private void addShadingCorrectionToLayer( + final Double z, + final ShadingModel layerModel, + final Parameters parameters, + final SectionData layerSectionData + ) throws IOException { + + final RenderDataClient renderClient = parameters.webservice.getDataClient(); + final ResolvedTileSpecCollection rtsc = renderClient.getResolvedTiles(parameters.stack, z); + if (layerModel == null) { LOG.warn("No model found for z={}", z); - return tileSpec; + } else { + final Collection tileSpecs = rtsc.getTileSpecs(); + LOG.info("Adding shading correction for {} tile specs, z={}", tileSpecs.size(), z); + + for (final TileSpec tileSpec : tileSpecs) { + addShadingCorrectionToTileSpec(tileSpec, layerModel, layerSectionData); + } } - // get uniform grid of points in tile + renderClient.saveResolvedTiles(rtsc, parameters.targetStack, z); + } + + private void addShadingCorrectionToTileSpec( + final TileSpec tileSpec, + final ShadingModel layerModel, + final SectionData sectionData + ) { + // get uniform grid of points in tile final int nSamples = (int) Math.ceil(Math.sqrt(layerModel.getMinNumMatches())); - final List points = gridOnTile(tileSpec, nSamples); + final List points = uniformGrid(nSamples); - final SectionData section = sectionData.get(z); - final double scaleX = section.getMinX() + 0.5 * section.getWidth(); - final double scaleY = section.getMinY() + 0.5 * section.getHeight(); + final double centerX = sectionData.getMinX() + 0.5 * sectionData.getWidth(); + final double centerY = sectionData.getMinY() + 0.5 * sectionData.getHeight(); final CoordinateTransformList transforms = tileSpec.getTransformList(); final List matches = new ArrayList<>(); - for (final double[] tilePoint : points) { - // transform point to global space and evaluate shading model - final double[] globalPoint = transforms.apply(tilePoint); - final double[] value = layerModel.apply(globalPoint); + final double[] transformedPoint = new double[2]; + for (final double[] tilePointNormalized : points) { + // transform normalized tile point (in [-1, 1] x [-1, 1]) to tile coordinates + transformedPoint[0] = (tilePointNormalized[0] + 1) * tileSpec.getWidth() / 2.0; + transformedPoint[1] = (tilePointNormalized[1] + 1) * tileSpec.getHeight() / 2.0; + + // transform tile point to global coordinate system + transforms.applyInPlace(transformedPoint); - // scale coordinates to [-1, 1] to fit a local model on the tile - tilePoint[0] = ShadingModel.scaleCoordinate(tilePoint[0], scaleX); - tilePoint[1] = ShadingModel.scaleCoordinate(tilePoint[1], scaleY); - matches.add(new PointMatch(new Point(tilePoint), new Point(value))); + // transform global coordinate to [-1, 1] x [-1, 1] wrt to the layer bounds + transformedPoint[0] = (transformedPoint[0] - centerX) / (sectionData.getWidth() / 2.0); + transformedPoint[1] = (transformedPoint[1] - centerY) / (sectionData.getHeight() / 2.0); + + // scale coordinates to [-1, 1] to evaluate the global model + layerModel.applyInPlace(transformedPoint); + final double[] value = new double[] {transformedPoint[0], 0}; + matches.add(new PointMatch(new Point(tilePointNormalized), new Point(value))); } + final ShadingModel tileModel = layerModel.copy(); try { - layerModel.fit(matches); + tileModel.fit(matches); } catch (final NotEnoughDataPointsException | IllDefinedDataPointsException e) { throw new RuntimeException(e); } // convert to filter and add to tile spec - final ShadingCorrectionFilter filter = new ShadingCorrectionFilter(layerModel); + final ShadingCorrectionFilter filter = new ShadingCorrectionFilter(tileModel); tileSpec.setFilterSpec(FilterSpec.forFilter(filter)); - - return tileSpec; } - final List gridOnTile(final TileSpec tileSpec, final int n) { + final List uniformGrid(final int n) { final List points = new ArrayList<>(); - final double incrementX = (double) tileSpec.getWidth() / (n - 1); - final double incrementY = (double) tileSpec.getHeight() / (n - 1); + final double increment = 2.0 / (n - 1); + final double eps = 1e-8; - for (double x = tileSpec.getMinX(); x < tileSpec.getMaxX(); x += incrementX) { - for (double y = tileSpec.getMinY(); y < tileSpec.getMaxY(); y += incrementY) { + for (double x = -1; x < 1 + eps; x += increment) { + for (double y = -1; y < 1 + eps; y += increment) { final double[] point = new double[] {x, y}; points.add(point); } From 0192c72e938d03ceac010d67133cac7da525d779 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 27 Nov 2024 15:30:02 -0500 Subject: [PATCH 48/56] Improve output/serialization of shading correction models --- .../janelia/alignment/filter/ShadingCorrectionFilter.java | 3 ++- .../render/client/emshading/ShadingCorrection_Plugin.java | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java index e91d7e7de..0380f285d 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/ShadingCorrectionFilter.java @@ -9,6 +9,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; public class ShadingCorrectionFilter implements Filter { @@ -74,7 +75,7 @@ public void init(final Map params) { public Map toParametersMap() { final Map map = new LinkedHashMap<>(); map.put("correctionMethod", correctionMethod.name()); - map.put("coefficients", Arrays.toString(coefficients)); + map.put("coefficients", Arrays.stream(coefficients).mapToObj(String::valueOf).collect(Collectors.joining(","))); return map; } diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java index 06efd3d0a..750c490f4 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java @@ -115,10 +115,13 @@ public static void fit(final int type, final List rois, final boolean showB IJ.log("Fitting with " + fitTypes[type] + " model..."); final ShadingModel shadingModel; + final String modelType; if (fitTypes[type].equals("Quadratic")) { shadingModel = new QuadraticShading(); + modelType = "quadratic"; } else if (fitTypes[type].equals("Fourth Order")) { shadingModel = new FourthOrderShading(); + modelType = "fourthOrder"; } else { throw new IllegalArgumentException("Unknown fit type: " + fitTypes[type]); } @@ -128,7 +131,8 @@ public static void fit(final int type, final List rois, final boolean showB CorrectShading.fitBackgroundModel(rois, img, shadingModel); IJ.log("Fitted shading model: " + shadingModel); IJ.log("Fitting took " + (System.currentTimeMillis() - start) + "ms."); - IJ.log("Raw coefficients: " + Arrays.toString(shadingModel.getCoefficients())); + IJ.log("\"modelType\": \"" + modelType + "\","); + IJ.log("\"coefficients\": " + Arrays.toString(shadingModel.getCoefficients())); final RandomAccessibleInterval shading = CorrectShading.createBackgroundImage(shadingModel, img); final RandomAccessibleInterval corrected = CorrectShading.correctBackground(img, shading, new UnsignedShortType()); From 580c47bef3f21cd83f96760ff13a4d1273795db5 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 5 Dec 2024 13:30:56 -0500 Subject: [PATCH 49/56] Render preview for plugin with stack bounds --- .../client/emshading/ShadingCorrection_Plugin.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java index 750c490f4..d370cd61c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java @@ -179,11 +179,11 @@ public static void main(final String[] args) throws IOException { private static ImagePlus renderImage(final Parameters params) throws IOException { final RenderDataClient client = new RenderDataClient(baseUrl, params.owner, params.project); - final Bounds layerBounds = client.getLayerBounds(params.stack, (double) params.z); - final long x = layerBounds.getMinX().longValue(); - final long y = layerBounds.getMinY().longValue(); - final long w = layerBounds.getWidth(); - final long h = layerBounds.getHeight(); + final Bounds stackBounds = client.getStackMetaData(params.stack).getStackBounds(); + final long x = stackBounds.getMinX().longValue(); + final long y = stackBounds.getMinY().longValue(); + final long w = stackBounds.getWidth(); + final long h = stackBounds.getHeight(); if (params.scale == 0.0) { // automatically determine downscale factor From 4a16ebbf98018fd97166b8c1f666f408439bef73 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 5 Dec 2024 19:25:39 -0500 Subject: [PATCH 50/56] Make per-tile shading correction use global bounds --- .../ShadingCorrectionTileClient.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index 106d8a7ab..f66d85a23 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -13,8 +13,8 @@ import org.apache.spark.broadcast.Broadcast; import org.janelia.alignment.filter.FilterSpec; import org.janelia.alignment.filter.ShadingCorrectionFilter; +import org.janelia.alignment.spec.Bounds; import org.janelia.alignment.spec.ResolvedTileSpecCollection; -import org.janelia.alignment.spec.SectionData; import org.janelia.alignment.spec.TileSpec; import org.janelia.alignment.spec.stack.StackMetaData; import org.janelia.render.client.ClientRunner; @@ -38,8 +38,8 @@ /** * Spark client for shading correction by a layer-wise quadratic or fourth order model. * The client takes as input a render stack and a parameter file and creates a new stack with appropriate shading - * correction filters. The shading is assumed to be an artifact in the imaging space, so it is applied by finding - * the position of a tile within the z-layer. + * correction filters. The shading is assumed to be an artifact in the global space, so it is applied in the bounds of + * the stack. *

* The parameter file is a json file containing a list of z values and corresponding models. The model for each z value * is valid for all z layers starting at the given z value until the next z value in the list. @@ -125,12 +125,12 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti setUpTargetStack(renderClient); final List zValues = renderClient.getStackZValues(parameters.stack); - final List stackSectionData = renderClient.getStackSectionData(parameters.stack, null, null); + final Bounds stackBounds = renderClient.getStackMetaData(parameters.stack).getStackBounds(); // parallelize computation over z-layers (broadcasting some data that is needed for all tile specs) final Broadcast modelProviderBroadcast = sparkContext.broadcast(modelProvider); final Broadcast parametersBroadcast = sparkContext.broadcast(parameters); - final Broadcast> sectionDataBroadcast = sparkContext.broadcast(stackSectionData); + final Broadcast stackBoundsBroadcast = sparkContext.broadcast(stackBounds); final Broadcast> zValuesBroadcast = sparkContext.broadcast(zValues); final List zIndices = IntStream.range(0, zValues.size()).boxed().collect(Collectors.toList()); @@ -138,8 +138,7 @@ public void runWithContext(final JavaSparkContext sparkContext) throws IOExcepti .foreach(zIndex -> { final Double z = zValuesBroadcast.getValue().get(zIndex); final ShadingModel layerModel = modelProviderBroadcast.getValue().getModel(z.intValue()); - final SectionData layerSectionData = sectionDataBroadcast.getValue().get(zIndex); - addShadingCorrectionToLayer(z, layerModel, parametersBroadcast.value(), layerSectionData); + addShadingCorrectionToLayer(z, layerModel, parametersBroadcast.value(), stackBoundsBroadcast.value()); }); completeTargetStack(renderClient); @@ -162,7 +161,7 @@ private void addShadingCorrectionToLayer( final Double z, final ShadingModel layerModel, final Parameters parameters, - final SectionData layerSectionData + final Bounds bounds ) throws IOException { final RenderDataClient renderClient = parameters.webservice.getDataClient(); @@ -175,7 +174,7 @@ private void addShadingCorrectionToLayer( LOG.info("Adding shading correction for {} tile specs, z={}", tileSpecs.size(), z); for (final TileSpec tileSpec : tileSpecs) { - addShadingCorrectionToTileSpec(tileSpec, layerModel, layerSectionData); + addShadingCorrectionToTileSpec(tileSpec, layerModel, bounds); } } @@ -185,14 +184,14 @@ private void addShadingCorrectionToLayer( private void addShadingCorrectionToTileSpec( final TileSpec tileSpec, final ShadingModel layerModel, - final SectionData sectionData + final Bounds bounds ) { // get uniform grid of points in tile final int nSamples = (int) Math.ceil(Math.sqrt(layerModel.getMinNumMatches())); final List points = uniformGrid(nSamples); - final double centerX = sectionData.getMinX() + 0.5 * sectionData.getWidth(); - final double centerY = sectionData.getMinY() + 0.5 * sectionData.getHeight(); + final double centerX = bounds.getMinX() + 0.5 * bounds.getWidth(); + final double centerY = bounds.getMinY() + 0.5 * bounds.getHeight(); final CoordinateTransformList transforms = tileSpec.getTransformList(); final List matches = new ArrayList<>(); @@ -207,8 +206,8 @@ private void addShadingCorrectionToTileSpec( transforms.applyInPlace(transformedPoint); // transform global coordinate to [-1, 1] x [-1, 1] wrt to the layer bounds - transformedPoint[0] = (transformedPoint[0] - centerX) / (sectionData.getWidth() / 2.0); - transformedPoint[1] = (transformedPoint[1] - centerY) / (sectionData.getHeight() / 2.0); + transformedPoint[0] = (transformedPoint[0] - centerX) / (bounds.getWidth() / 2.0); + transformedPoint[1] = (transformedPoint[1] - centerY) / (bounds.getHeight() / 2.0); // scale coordinates to [-1, 1] to evaluate the global model layerModel.applyInPlace(transformedPoint); From 4c8b90f4f0d30ded5b0090a312bdab2711d82089 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 11:08:25 -0500 Subject: [PATCH 51/56] Make url a parameter in shading correction plugin --- .../emshading/ShadingCorrection_Plugin.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java index d370cd61c..51e5f016c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/emshading/ShadingCorrection_Plugin.java @@ -9,6 +9,7 @@ import javax.swing.SwingUtilities; import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; import ij.ImagePlus; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.NotEnoughDataPointsException; @@ -22,6 +23,7 @@ import org.janelia.alignment.util.ImageProcessorCache; import org.janelia.render.client.RenderDataClient; import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.render.client.parameter.RenderWebServiceParameters; import org.janelia.render.client.solver.visualize.RenderTools; import ij.IJ; @@ -38,15 +40,8 @@ public class ShadingCorrection_Plugin implements PlugIn { public static final int MAX_SIZE = 2000 * 2000; private static class Parameters extends CommandLineParameters { - @Parameter(names = "--owner", - description = "Name of the owner in the render database", - required = true) - public String owner; - - @Parameter(names = "--project", - description = "Name of the render project", - required = true) - public String project; + @ParametersDelegate + public RenderWebServiceParameters renderWebService = new RenderWebServiceParameters(); @Parameter(names = "--stack", description = "Name of the stack in the render project", @@ -65,7 +60,6 @@ private static class Parameters extends CommandLineParameters { public static int defaultType = 0; public static boolean defaultShowBackground = false; - public static String baseUrl = "http://renderer-dev.int.janelia.org:8080/render-ws/v1"; public static String[] fitTypes = new String[] { "Quadratic", "Fourth Order" }; @Override @@ -170,7 +164,7 @@ public static void main(final String[] args) throws IOException { new ImageJ(); SwingUtilities.invokeLater(ShadingCorrection_Plugin::addKeyListener); - IJ.log("Opening " + params.owner + "/" + params.project + "/" + params.stack); + IJ.log("Opening " + params.renderWebService.owner + "/" + params.renderWebService.project + "/" + params.stack); IJ.log("Showing slice " + params.z); final ImagePlus img = renderImage(params); @@ -178,7 +172,7 @@ public static void main(final String[] args) throws IOException { } private static ImagePlus renderImage(final Parameters params) throws IOException { - final RenderDataClient client = new RenderDataClient(baseUrl, params.owner, params.project); + final RenderDataClient client = params.renderWebService.getDataClient(); final Bounds stackBounds = client.getStackMetaData(params.stack).getStackBounds(); final long x = stackBounds.getMinX().longValue(); final long y = stackBounds.getMinY().longValue(); @@ -198,11 +192,14 @@ private static ImagePlus renderImage(final Parameters params) throws IOException } final ImageProcessorWithMasks ipwm = RenderTools.renderImage(ImageProcessorCache.DISABLED_CACHE, - baseUrl, params.owner, params.project, params.stack, + params.renderWebService.baseDataUrl, + params.renderWebService.owner, + params.renderWebService.project, + params.stack, x, y, params.z, w, h, params.scale, false); - final String title = params.project + "-" + params.stack + "(z=" + params.z + ")"; + final String title = params.renderWebService.project + "-" + params.stack + "(z=" + params.z + ")"; return new ImagePlus(title, ipwm.ip); } } From f055bd0bb0e2040885719139182f75b192593e65 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 11:29:23 -0500 Subject: [PATCH 52/56] Set executor-context to improve log messages --- .../spark/intensityadjust/ShadingCorrectionClient.java | 6 +++++- .../spark/intensityadjust/ShadingCorrectionTileClient.java | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index fccf7ac2c..760c5f808 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -21,6 +21,7 @@ import org.janelia.alignment.filter.emshading.QuadraticShading; import org.janelia.render.client.emshading.ShadingCorrection_Plugin; import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.render.client.spark.LogUtilities; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.N5FSWriter; @@ -202,7 +203,10 @@ private static void cloneDatasetOrGroupAttributes(final N5Reader in, final N5Wri } private static void processSingleBlock(final Parameters parameters, final ShadingModelProvider modelProvider, final Grid.Block block) { - LOG.info("processSingleBlock: block={}", block.gridPosition); + // enable logging on executors and add block context to log messages + LogUtilities.setupExecutorLog4j(block.gridPosition[0] + ":" + block.gridPosition[1] + ":" + block.gridPosition[2]); + + LOG.info("processSingleBlock: entry"); try (final N5Reader in = new N5FSReader(parameters.n5In); final N5Writer out = new N5FSWriter(parameters.n5Out)) { diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index f66d85a23..5d4669d58 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -23,6 +23,7 @@ import org.janelia.render.client.emshading.ShadingCorrection_Plugin; import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.render.client.parameter.RenderWebServiceParameters; +import org.janelia.render.client.spark.LogUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.janelia.render.client.spark.intensityadjust.ShadingCorrectionClient.ShadingModelProvider; @@ -163,15 +164,17 @@ private void addShadingCorrectionToLayer( final Parameters parameters, final Bounds bounds ) throws IOException { + // enable logging on executors and add z-layer to log messages + LogUtilities.setupExecutorLog4j("z=" + z.intValue()); final RenderDataClient renderClient = parameters.webservice.getDataClient(); final ResolvedTileSpecCollection rtsc = renderClient.getResolvedTiles(parameters.stack, z); if (layerModel == null) { - LOG.warn("No model found for z={}", z); + LOG.warn("No model found"); } else { final Collection tileSpecs = rtsc.getTileSpecs(); - LOG.info("Adding shading correction for {} tile specs, z={}", tileSpecs.size(), z); + LOG.info("Adding shading correction for {} tile specs", tileSpecs.size()); for (final TileSpec tileSpec : tileSpecs) { addShadingCorrectionToTileSpec(tileSpec, layerModel, bounds); From 7a8dec89509305d9d9386788479cac5c6f0622da Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 11:40:47 -0500 Subject: [PATCH 53/56] Migrate json-parsing from gson to jackson --- .../ShadingCorrectionClient.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index 760c5f808..3de2b3997 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -1,9 +1,10 @@ package org.janelia.render.client.spark.intensityadjust; import com.beust.jcommander.Parameter; -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import net.imglib2.Cursor; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.Img; @@ -278,15 +279,16 @@ public ShadingModel getModel(final int z) { public static ShadingModelProvider fromJsonFile(final String fileName) throws IOException { LOG.info("Reading model specs from file: {}", fileName); + final ObjectMapper mapper = new ObjectMapper(); try (final FileReader reader = new FileReader(fileName)) { - return fromJson(JsonParser.parseReader(reader)); + return fromJson(mapper.readTree(reader)); } } - public static ShadingModelProvider fromJson(final JsonElement jsonData) { + public static ShadingModelProvider fromJson(final JsonNode jsonData) throws JsonProcessingException { - final List modelSpecs = new ArrayList<>(); - Collections.addAll(modelSpecs, new Gson().fromJson(jsonData, ModelSpec[].class)); + final ObjectMapper mapper = new ObjectMapper(); + final List modelSpecs = new ArrayList<>(Arrays.asList(mapper.treeToValue(jsonData, ModelSpec[].class))); // validation of json data for (final ModelSpec modelSpec : modelSpecs) { @@ -300,8 +302,11 @@ public static ShadingModelProvider fromJson(final JsonElement jsonData) { @SuppressWarnings("unused") private static class ModelSpec implements Serializable { + @JsonProperty("fromZ") private int fromZ; + @JsonProperty("modelType") private String modelType; + @JsonProperty("coefficients") private double[] coefficients; // no explicit constructor; meant to be deserialized from json From e27156965336c497db7e3f6ce904a37c9eedbb06 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 12:03:42 -0500 Subject: [PATCH 54/56] Change logic of model specification to enable skipping layer --- .../ShadingCorrectionClient.java | 25 ++++++++++++------- .../ShadingCorrectionTileClient.java | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java index 3de2b3997..796473fba 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionClient.java @@ -227,7 +227,7 @@ private static void processSingleBlock(final Parameters parameters, final Shadin for (int z = (int) block.min(2); z <= block.max(2); z++) { final ShadingModel model = modelProvider.getModel(z); if (model == null) { - LOG.warn("No model found for z={}", z); + // no model for this z value continue; } @@ -274,7 +274,7 @@ public ShadingModel getModel(final int z) { return modelSpec.getModel(); } } - return null; + throw new IllegalArgumentException("No model found for z=" + z); } public static ShadingModelProvider fromJsonFile(final String fileName) throws IOException { @@ -285,6 +285,10 @@ public static ShadingModelProvider fromJsonFile(final String fileName) throws IO } } + /** + * Provide a {@link ShadingModel} for a given z value. If no model is found for the given z value, the provider + * returns null. + */ public static ShadingModelProvider fromJson(final JsonNode jsonData) throws JsonProcessingException { final ObjectMapper mapper = new ObjectMapper(); @@ -320,13 +324,16 @@ public void setCoefficients(final double[] coefficients) { } public ShadingModel getModel() { - if (modelType.equals("quadratic")) { - return new QuadraticShading(coefficients); - } else if (modelType.equals("fourthOrder")) { - return new FourthOrderShading(coefficients); - } else { - throw new IllegalArgumentException("Unknown model type: " + modelType); - } + switch (modelType) { + case "quadratic": + return new QuadraticShading(coefficients); + case "fourthOrder": + return new FourthOrderShading(coefficients); + case "none": + return null; + default: + throw new IllegalArgumentException("Unknown model type: " + modelType); + } } public String toString() { diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index 5d4669d58..5895af44e 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -171,7 +171,7 @@ private void addShadingCorrectionToLayer( final ResolvedTileSpecCollection rtsc = renderClient.getResolvedTiles(parameters.stack, z); if (layerModel == null) { - LOG.warn("No model found"); + LOG.info("No shading correction"); } else { final Collection tileSpecs = rtsc.getTileSpecs(); LOG.info("Adding shading correction for {} tile specs", tileSpecs.size()); From e7903c0c21cc0e48b08c28c4fd3003076919abac Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 15:44:32 -0500 Subject: [PATCH 55/56] Add method to combine filter specs --- .../alignment/filter/CompositeFilter.java | 5 ++-- .../janelia/alignment/filter/FilterSpec.java | 28 +++++++++++++++++++ .../alignment/filter/FilterSpecTest.java | 28 +++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/render-app/src/main/java/org/janelia/alignment/filter/CompositeFilter.java b/render-app/src/main/java/org/janelia/alignment/filter/CompositeFilter.java index 94278e2ff..80cc3a7c2 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/CompositeFilter.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/CompositeFilter.java @@ -19,7 +19,6 @@ public class CompositeFilter implements Filter { // empty constructor required to create instances from specifications @SuppressWarnings("unused") public CompositeFilter() { - this((List) null); } public CompositeFilter(final Filter... filters) { @@ -27,7 +26,7 @@ public CompositeFilter(final Filter... filters) { } public CompositeFilter(final List filters) { - this.filters = filters; + this.filters = new ArrayList<>(filters); } @Override @@ -50,7 +49,7 @@ public Map toParametersMap() { return map; } - private static String filterKey(final int i) { + static String filterKey(final int i) { return "filter" + i; } diff --git a/render-app/src/main/java/org/janelia/alignment/filter/FilterSpec.java b/render-app/src/main/java/org/janelia/alignment/filter/FilterSpec.java index f0f64efcf..a269efaef 100644 --- a/render-app/src/main/java/org/janelia/alignment/filter/FilterSpec.java +++ b/render-app/src/main/java/org/janelia/alignment/filter/FilterSpec.java @@ -1,6 +1,7 @@ package org.janelia.alignment.filter; import java.io.Serializable; +import java.util.HashMap; import java.util.Map; import org.janelia.alignment.json.JsonUtils; @@ -96,6 +97,33 @@ public static FilterSpec forFilter(final Filter filter) { return new FilterSpec(filter.getClass().getName(), filter.toParametersMap()); } + public static FilterSpec combine(final FilterSpec first, final FilterSpec second) { + if (first == null || second == null) { + return first == null ? second : first; + } + + final FilterSpec firstAsComposite = asCompositeSpec(first); + final FilterSpec secondAsComposite = asCompositeSpec(second); + + // both are guaranteed to be composite filters -> unpack both, append, and repack + final Map parameters = new HashMap<>(firstAsComposite.parameters); + final int shift = firstAsComposite.parameters.size(); + for (int i = 0; i < secondAsComposite.parameters.size(); i++) { + final String key = CompositeFilter.filterKey(i + shift); + final String value = secondAsComposite.parameters.get(CompositeFilter.filterKey(i)); + parameters.put(key, value); + } + return new FilterSpec(CompositeFilter.class.getName(), parameters); + } + + private static FilterSpec asCompositeSpec(final FilterSpec filterSpec) { + if (filterSpec.className.equals(CompositeFilter.class.getName())) { + return filterSpec; + } + return new FilterSpec(CompositeFilter.class.getName(), + Map.of(CompositeFilter.filterKey(0), filterSpec.toJson())); + } + private Class getClazz() throws IllegalArgumentException { if (clazz == null) { if (className == null) { diff --git a/render-app/src/test/java/org/janelia/alignment/filter/FilterSpecTest.java b/render-app/src/test/java/org/janelia/alignment/filter/FilterSpecTest.java index ba1a983d9..18f3b344d 100644 --- a/render-app/src/test/java/org/janelia/alignment/filter/FilterSpecTest.java +++ b/render-app/src/test/java/org/janelia/alignment/filter/FilterSpecTest.java @@ -1,5 +1,6 @@ package org.janelia.alignment.filter; +import org.janelia.alignment.filter.emshading.QuadraticShading; import org.junit.Assert; import org.junit.Test; @@ -75,4 +76,31 @@ private static Filter parseAndBuildFilter(final FilterSpec filterSpec) { return parsedSpec.buildInstance(); } + @SuppressWarnings("DataFlowIssue") + @Test + public void compositeFilterSpecBuildsCorrectly() { + FilterSpec first = getTestFilterSpec(1); + FilterSpec second = getTestFilterSpec(2); + + // combine filters in all possible ways + first = FilterSpec.combine(first, null); + second = FilterSpec.combine(null, second); + + FilterSpec accumulate = FilterSpec.combine(first, second); + + accumulate = FilterSpec.combine(first, accumulate); + accumulate = FilterSpec.combine(accumulate, second); + + accumulate = FilterSpec.combine(accumulate, accumulate); + + // check if it can be built and has the correct number of parameters + final Filter recoveredFilter = accumulate.buildInstance(); + Assert.assertEquals(8, recoveredFilter.toParametersMap().size()); + } + + private static FilterSpec getTestFilterSpec(final int i) { + final QuadraticShading quadraticShading = new QuadraticShading(new double[]{i, 0, 0, 0, 0, 0}); + final Filter filter = new ShadingCorrectionFilter(quadraticShading); + return FilterSpec.forFilter(filter); + } } From a405ea4d964b9bb09d71bcc1cb1fbd8c17df9258 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Fri, 6 Dec 2024 15:53:37 -0500 Subject: [PATCH 56/56] Create (and use) a method to add a filter spec to a tile spec --- .../java/org/janelia/alignment/spec/TileSpec.java | 13 +++++++++++++ .../ShadingCorrectionTileClient.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/render-app/src/main/java/org/janelia/alignment/spec/TileSpec.java b/render-app/src/main/java/org/janelia/alignment/spec/TileSpec.java index d24bff75c..9c486e3fe 100644 --- a/render-app/src/main/java/org/janelia/alignment/spec/TileSpec.java +++ b/render-app/src/main/java/org/janelia/alignment/spec/TileSpec.java @@ -660,6 +660,10 @@ public FilterSpec getFilterSpec() { return spec; } + /** + * Set a filter spec for this tile spec (replacing any existing filter spec). + * @param filterSpec the filter spec to set for this tile spec + */ public void setFilterSpec(final FilterSpec filterSpec) { this.filterSpec = filterSpec; if (channels != null) { @@ -669,6 +673,15 @@ public void setFilterSpec(final FilterSpec filterSpec) { } } + /** + * Add a filter spec to this tile spec (combining with any existing filter spec). + * @param filterSpec the filter spec to add + */ + public void addFilterSpec(final FilterSpec filterSpec) { + final FilterSpec combinedFilterSpec = FilterSpec.combine(this.filterSpec, filterSpec); + setFilterSpec(combinedFilterSpec); + } + public boolean hasTransforms() { return ((transforms != null) && (transforms.size() > 0)); } diff --git a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java index 5895af44e..aaaf3f211 100644 --- a/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java +++ b/render-ws-spark-client/src/main/java/org/janelia/render/client/spark/intensityadjust/ShadingCorrectionTileClient.java @@ -227,7 +227,7 @@ private void addShadingCorrectionToTileSpec( // convert to filter and add to tile spec final ShadingCorrectionFilter filter = new ShadingCorrectionFilter(tileModel); - tileSpec.setFilterSpec(FilterSpec.forFilter(filter)); + tileSpec.addFilterSpec(FilterSpec.forFilter(filter)); } final List uniformGrid(final int n) {