From 480f3b27e128d28d2b7111999b94a39ea797e74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20L=C3=B6ffler?= Date: Mon, 24 Apr 2023 23:44:28 +0200 Subject: [PATCH] The JAMA subproject now provides its own solver implementation for image-registration. --- README.md | 3 +- jama/build.gradle | 4 + jama/src/main/java/module-info.java | 5 + .../java/net/raumzeitfalle/jama/Solver.java | 59 +++++++ ...alle.registration.solver.spi.SolverAdapter | 1 + .../net/raumzeitfalle/jama/SolverTest.java | 67 ++++++++ solver-test/build.gradle | 1 + .../IntegratedJamaNumericsTest.java | 162 ++++++++++++++++++ 8 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 jama/src/main/java/net/raumzeitfalle/jama/Solver.java create mode 100644 jama/src/main/resources/META-INF/services/net.raumzeitfalle.registration.solver.spi.SolverAdapter create mode 100644 jama/src/test/java/net/raumzeitfalle/jama/SolverTest.java create mode 100644 solver-test/src/test/java/net/raumzeitfalle/registration/solvertest/IntegratedJamaNumericsTest.java diff --git a/README.md b/README.md index a4ff37d..2a69b6a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ The solver is selected using the Java SPI (Service Provider Interface) mechanism - [x] Versions up to and including 0.0.5 run with Java-8 - [x] Version 0.0.5 will support different linear algebra libraries (will make use of service provider API) - [x] Version 0.0.6 will support ~~Java-8 and~~ Java-11 (~~utilize multi-release JARs~~ support for modules will be introduced) -- [ ] Version 0.0.7 will support Java-17 with records (JEP 359) +- [ ] Version 0.0.7 will no longer provide a bundle version, the core is now the `image-registration` API library. It is now mandatory to add the required solver as needed. +- [ ] Version 0.0.8 will support Java-17 with records (JEP 359) - [ ] Later versions will support higher order calculations (first: up to 3rd order, 20 coefficient model) These methods are used e.g. in photomask manufacturing, medical imaging or geospatial applications. diff --git a/jama/build.gradle b/jama/build.gradle index 45491c1..75f94b7 100644 --- a/jama/build.gradle +++ b/jama/build.gradle @@ -6,6 +6,10 @@ plugins { id 'de.jjohannes.extra-java-module-info' } +dependencies { + implementation project(':solver-api') +} + test { useJUnitPlatform() } diff --git a/jama/src/main/java/module-info.java b/jama/src/main/java/module-info.java index 8115351..e159b9b 100644 --- a/jama/src/main/java/module-info.java +++ b/jama/src/main/java/module-info.java @@ -1,6 +1,11 @@ /** * JAMA: A Java Matrix Package + * (also usable as a solver for image registration) */ open module net.raumzeitfalle.jama { + requires transitive net.raumzeitfalle.registration.solver; exports net.raumzeitfalle.jama; + + provides net.raumzeitfalle.registration.solver.spi.SolverAdapter + with net.raumzeitfalle.jama.Solver; } \ No newline at end of file diff --git a/jama/src/main/java/net/raumzeitfalle/jama/Solver.java b/jama/src/main/java/net/raumzeitfalle/jama/Solver.java new file mode 100644 index 0000000..4b8ea87 --- /dev/null +++ b/jama/src/main/java/net/raumzeitfalle/jama/Solver.java @@ -0,0 +1,59 @@ +/*- + * #%L + * Image-Registration + * %% + * Copyright (C) 2019, 2021 Oliver Loeffler, Raumzeitfalle.net + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package net.raumzeitfalle.jama; + +import net.raumzeitfalle.registration.solver.Deltas; +import net.raumzeitfalle.registration.solver.References; +import net.raumzeitfalle.registration.solver.Solution; +import net.raumzeitfalle.registration.solver.Solutions; +import net.raumzeitfalle.registration.solver.spi.SolverAdapter; + +public class Solver implements SolverAdapter { + + public Solution solve(References references, Deltas deviations) { + + Matrix refs = new Matrix(references.getArray()); + Matrix deltas = new Matrix(deviations.getArray(), deviations.rows()); + + QRDecomposition qr = new QRDecomposition(refs); + + Matrix rInverse = qr.getR().inverse(); + Matrix qTransposed = qr.getQ().transpose(); + Matrix solution = rInverse.times(qTransposed) + .times(deltas); + + double[] firstColumn = getFirstColumn(solution); + return Solutions.fromArray(firstColumn); + + } + + private double[] getFirstColumn(Matrix solution) { + double[] column = new double[solution.getRowDimension()]; + for (int row = 0; row < column.length; row++) { + column[row] = solution.get(row, 0); + } + return column; + } + + @Override + public Solution apply(References t, Deltas u) { + return solve(t, u); + } +} diff --git a/jama/src/main/resources/META-INF/services/net.raumzeitfalle.registration.solver.spi.SolverAdapter b/jama/src/main/resources/META-INF/services/net.raumzeitfalle.registration.solver.spi.SolverAdapter new file mode 100644 index 0000000..8dff341 --- /dev/null +++ b/jama/src/main/resources/META-INF/services/net.raumzeitfalle.registration.solver.spi.SolverAdapter @@ -0,0 +1 @@ +net.raumzeitfalle.jama.Solver \ No newline at end of file diff --git a/jama/src/test/java/net/raumzeitfalle/jama/SolverTest.java b/jama/src/test/java/net/raumzeitfalle/jama/SolverTest.java new file mode 100644 index 0000000..b65f187 --- /dev/null +++ b/jama/src/test/java/net/raumzeitfalle/jama/SolverTest.java @@ -0,0 +1,67 @@ +/*- + * #%L + * Image-Registration + * %% + * Copyright (C) 2019, 2021 Oliver Loeffler, Raumzeitfalle.net + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package net.raumzeitfalle.jama; + +import net.raumzeitfalle.registration.solver.Solution; +import net.raumzeitfalle.registration.solver.spi.SolverAdapter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SolverTest { + + private final SolverAdapter classUnderTest = new Solver(); + + private static final double TOLERANCE = 1E-11; + + @Test + void test() { + + double[][] design = { + { -75000.0, 0.0, 0.0, -70000.0, 1.0, 0.0 }, + { 0.0, -70000.0, 75000.0, 0.0, 0.0, 1.0 }, + { -75000.0, 0.0, 0.0, 70000.0, 1.0, 0.0 }, + { 0.0, 70000.0, 75000.0, 0.0, 0.0, 1.0 }, + { 75000.0, 0.0, 0.0, 70000.0, 1.0, 0.0 }, + { 0.0, 70000.0, -75000.0, 0.0, 0.0, 1.0 }, + { 75000.0, 0.0, 0.0, -70000.0, 1.0, 0.0 }, + { 0.0, -70000.0, -75000.0, 0.0, 0.0, 1.0 } }; + + double[] differences = { -0.075, 0.140, -0.075, -0.140, 0.075, -0.140, 0.075, 0.140 }; + + + Solution solution = classUnderTest.apply(() -> design, () -> differences); + + double[] result = { 1.0E-6, -2.0E-6, 0.0, 0.0, 0.0, 0.0 }; + + assertAll(() -> assertNotNull(solution, "must not be null"), + + () -> assertEquals(result[0], solution.get(0), TOLERANCE, "scale x"), + () -> assertEquals(result[1], solution.get(1), TOLERANCE, "scale y"), + + () -> assertEquals(result[2], solution.get(2), TOLERANCE, "ortho x"), + () -> assertEquals(result[3], solution.get(3), TOLERANCE, "ortho y"), + + () -> assertEquals(result[4], solution.get(4), TOLERANCE, "trans x"), + () -> assertEquals(result[5], solution.get(5), TOLERANCE, "trans y")); + + } + +} diff --git a/solver-test/build.gradle b/solver-test/build.gradle index dbaafb0..e0c3c20 100644 --- a/solver-test/build.gradle +++ b/solver-test/build.gradle @@ -6,6 +6,7 @@ plugins { dependencies { implementation project(':solver-api') implementation project(':image-registration') + testImplementation project(':jama') testImplementation project(':jama-solver') testImplementation project(':la4j-solver') testImplementation project(':ejml-solver') diff --git a/solver-test/src/test/java/net/raumzeitfalle/registration/solvertest/IntegratedJamaNumericsTest.java b/solver-test/src/test/java/net/raumzeitfalle/registration/solvertest/IntegratedJamaNumericsTest.java new file mode 100644 index 0000000..a4f2d39 --- /dev/null +++ b/solver-test/src/test/java/net/raumzeitfalle/registration/solvertest/IntegratedJamaNumericsTest.java @@ -0,0 +1,162 @@ +/*- + * #%L + * Image-Registration + * %% + * Copyright (C) 2019, 2021 Oliver Loeffler, Raumzeitfalle.net + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package net.raumzeitfalle.registration.solvertest; + +import net.raumzeitfalle.jama.Solver; +import net.raumzeitfalle.registration.solver.SolverProvider; +import net.raumzeitfalle.registration.solver.spi.SolverAdapter; +import net.raumzeitfalle.registration.solvertest.numerics.AffineTransformNumerics; +import net.raumzeitfalle.registration.solvertest.numerics.RigidTransformNumerics; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class IntegratedJamaNumericsTest { + + private final RigidTransformNumerics rigidTransformNumerics = new RigidTransformNumerics(); + + private final AffineTransformNumerics affineTransformNumerics = new AffineTransformNumerics(); + + @BeforeAll + public static void prepare() { + SolverProvider.setPreferredImplementation(Solver.class.getName()); + } + + @BeforeEach + void checkImplementation() { + SolverAdapter solver = new Solver(); + assertEquals(solver.getClass(), SolverProvider.getInstance().getSolver().getClass()); + } + + @Test + void translationX() { + rigidTransformNumerics.assertTranslationX(); + } + + @Test + void translationY() { + rigidTransformNumerics.assertTranslationY(); + } + + @Test + void translationXY() { + rigidTransformNumerics.assertTranslationXY(); + } + + @Test + void rotation() { + rigidTransformNumerics.assertRotation(); + } + + @Test + void rotationAndTranslation() { + rigidTransformNumerics.assertRotationAndTranslation(); + } + + @Test + void skipTransform() { + rigidTransformNumerics.assertSkipTransform(); + } + + @Test + void translationXonly1D() { + rigidTransformNumerics.assertTranslationXonly1D(); + } + + + @Test + void translationYonly1D() { + rigidTransformNumerics.assertTranslationYonly1D(); + } + + @Test + void alignmentOfDisplacementsAlongHorizontalLine() { + rigidTransformNumerics.assertDisplacementsAlongHorizontalLine(); + } + + @Test + void singularityXY() { + rigidTransformNumerics.assertSingularityXY(); + } + + @Test + void singularityX() { + rigidTransformNumerics.assertSingularityX(); + } + + @Test + void singularityY() { + rigidTransformNumerics.assertSingularityY(); + } + + // + @Test + void zeroTransform() { + affineTransformNumerics.assertZeroTransform(); + } + + @Test + void scalingX() { + affineTransformNumerics.assertScalingX(); + } + + @Test + void scalingY_withoutX() { + affineTransformNumerics.assert_scalingY_withoutX(); + } + + @Test + void scalingX_withoutY() { + affineTransformNumerics.assert_scalingX_withoutY(); + } + + @Test + void scalingXY() { + affineTransformNumerics.assertScalingXY(); + } + + @Test + void shearingX() { + affineTransformNumerics.assertShearingX(); + } + + @Test + void shearingY() { + affineTransformNumerics.assertShearingY(); + } + + @Test + void shearingXY() { + affineTransformNumerics.assertShearingXY(); + } + + @Test + void displacementsAlongVerticalLine() { + affineTransformNumerics.assertDisplacementsAlongVerticalLine(); + } + + @Test + void displacementsAlongHorizontalLine() { + affineTransformNumerics.assertDisplacementsAlongHorizontalLine(); + } +}