From 3984e7e99906184ec1b7051c887b547b41f2b9ba Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Mon, 1 Dec 2025 20:43:41 -0500 Subject: [PATCH 1/5] Create first straightening client with Claude --- .../client/StackStraighteningClient.java | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java new file mode 100644 index 000000000..21c81f88f --- /dev/null +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java @@ -0,0 +1,222 @@ +package org.janelia.render.client; + +import java.io.IOException; +import java.util.List; + +import org.janelia.alignment.spec.Bounds; +import org.janelia.alignment.spec.LeafTransformSpec; +import org.janelia.alignment.spec.ResolvedTileSpecCollection; +import org.janelia.alignment.spec.stack.StackMetaData; +import org.janelia.render.client.parameter.CommandLineParameters; +import org.janelia.render.client.parameter.RenderWebServiceParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; + +import mpicbg.trakem2.transform.AffineModel2D; + +/** + * Java client for straightening a stack by gradually applying an offset + * computed from the midpoints of the first and last layer bounding boxes. + * + * The offset is linearly interpolated across layers: + * layer z is moved by (z - zmin) / (zmax - zmin) * offset + * + * @author Michael Innerberger + */ +public class StackStraighteningClient { + + public static class Parameters extends CommandLineParameters { + + @ParametersDelegate + public RenderWebServiceParameters renderWeb = new RenderWebServiceParameters(); + + @Parameter( + names = "--stack", + description = "Name of source stack", + required = true) + public String stack; + + @Parameter( + names = "--targetOwner", + description = "Name of target stack owner (default is same as source stack owner)") + public String targetOwner; + + @Parameter( + names = "--targetProject", + description = "Name of target stack project (default is same as source stack project)") + public String targetProject; + + @Parameter( + names = "--targetStack", + description = "Name of target stack", + required = true) + public String targetStack; + + @Parameter( + names = "--completeTargetStack", + description = "Complete the target stack after processing all layers", + arity = 0) + public boolean completeTargetStack = false; + + public String getTargetOwner() { + return targetOwner == null ? renderWeb.owner : targetOwner; + } + + public String getTargetProject() { + return targetProject == null ? renderWeb.project : targetProject; + } + } + + 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); + + LOG.info("runClient: entry, parameters={}", parameters); + + final StackStraighteningClient client = new StackStraighteningClient(parameters); + + client.setupDerivedStack(); + client.straightenStack(); + + if (parameters.completeTargetStack) { + client.completeTargetStack(); + } + } + }; + clientRunner.run(); + } + + private final Parameters parameters; + private final RenderDataClient sourceDataClient; + private final RenderDataClient targetDataClient; + private final List zValues; + + private StackStraighteningClient(final Parameters parameters) + throws IOException { + + this.parameters = parameters; + + this.sourceDataClient = parameters.renderWeb.getDataClient(); + this.targetDataClient = new RenderDataClient(parameters.renderWeb.baseDataUrl, + parameters.getTargetOwner(), + parameters.getTargetProject()); + + this.zValues = sourceDataClient.getStackZValues(parameters.stack); + + if (zValues.size() < 2) { + throw new IllegalArgumentException("Stack must have at least 2 layers for straightening"); + } + } + + private void setupDerivedStack() throws IOException { + final StackMetaData sourceStackMetaData = sourceDataClient.getStackMetaData(parameters.stack); + targetDataClient.setupDerivedStack(sourceStackMetaData, parameters.targetStack); + } + + private void completeTargetStack() throws Exception { + targetDataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); + } + + private void straightenStack() throws Exception { + + final double zMin = zValues.get(0); + final double zMax = zValues.get(zValues.size() - 1); + + LOG.info("straightenStack: zMin={}, zMax={}", zMin, zMax); + + // Get bounding box midpoints for first and last layers + final double[] firstLayerMidpoint = getLayerBoundingBoxMidpoint(zMin); + final double[] lastLayerMidpoint = getLayerBoundingBoxMidpoint(zMax); + + // Compute the total offset between first and last layer midpoints + final double totalOffsetX = lastLayerMidpoint[0] - firstLayerMidpoint[0]; + final double totalOffsetY = lastLayerMidpoint[1] - firstLayerMidpoint[1]; + + LOG.info("straightenStack: firstLayerMidpoint=({}, {}), lastLayerMidpoint=({}, {}), totalOffset=({}, {})", + firstLayerMidpoint[0], firstLayerMidpoint[1], + lastLayerMidpoint[0], lastLayerMidpoint[1], + totalOffsetX, totalOffsetY); + + final double zRange = zMax - zMin; + + // Process each layer + for (final Double z : zValues) { + straightenLayer(z, zMin, zRange, totalOffsetX, totalOffsetY); + } + + LOG.info("straightenStack: exit, processed {} layers", zValues.size()); + } + + private double[] getLayerBoundingBoxMidpoint(final double z) throws Exception { + final ResolvedTileSpecCollection tiles = sourceDataClient.getResolvedTiles(parameters.stack, z); + final Bounds layerBounds = tiles.toBounds(); + + if (layerBounds == null || layerBounds.getMinX() == null) { + throw new IllegalArgumentException("Cannot compute bounding box for layer z=" + z); + } + + final double midX = (layerBounds.getMinX() + layerBounds.getMaxX()) / 2.0; + final double midY = (layerBounds.getMinY() + layerBounds.getMaxY()) / 2.0; + + LOG.info("getLayerBoundingBoxMidpoint: z={}, bounds={}, midpoint=({}, {})", + z, layerBounds, midX, midY); + + return new double[] { midX, midY }; + } + + private void straightenLayer(final Double z, + final double zMin, + final double zRange, + final double totalOffsetX, + final double totalOffsetY) + throws Exception { + + LOG.info("straightenLayer: entry, z={}", z); + + final ResolvedTileSpecCollection tiles = sourceDataClient.getResolvedTiles(parameters.stack, z); + + if (tiles.getTileCount() == 0) { + LOG.info("straightenLayer: no tiles for z={}, skipping", z); + return; + } + + // Compute the interpolation factor for this layer + // At zMin, factor = 0 (no movement) + // At zMax, factor = 1 (full offset applied, but negated to bring it back to first layer position) + final double factor = (z - zMin) / zRange; + + // The translation needed to straighten this layer + // We negate the offset because we want to move layers back toward the first layer's position + final double translateX = -factor * totalOffsetX; + final double translateY = -factor * totalOffsetY; + + if (Math.abs(translateX) > 0.001 || Math.abs(translateY) > 0.001) { + final AffineModel2D translationModel = new AffineModel2D(); + translationModel.set(1, 0, 0, 1, translateX, translateY); + + final LeafTransformSpec translationTransform = new LeafTransformSpec( + translationModel.getClass().getName(), + translationModel.toDataString()); + + tiles.preConcatenateTransformToAllTiles(translationTransform); + + LOG.info("straightenLayer: applied translation ({}, {}) to {} tiles for z={}", + translateX, translateY, tiles.getTileCount(), z); + } else { + LOG.info("straightenLayer: no significant translation needed for z={}", z); + } + + targetDataClient.saveResolvedTiles(tiles, parameters.targetStack, z); + + LOG.info("straightenLayer: exit, saved {} tiles for z={}", tiles.getTileCount(), z); + } + + private static final Logger LOG = LoggerFactory.getLogger(StackStraighteningClient.class); +} From ef96411af090120a2ffdc4ffb515f1702e8e36d0 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Mon, 1 Dec 2025 20:52:23 -0500 Subject: [PATCH 2/5] Review previously generated code --- .../client/StackStraighteningClient.java | 85 +++++-------------- 1 file changed, 19 insertions(+), 66 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java index 21c81f88f..520de5df9 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java @@ -23,8 +23,6 @@ * * The offset is linearly interpolated across layers: * layer z is moved by (z - zmin) / (zmax - zmin) * offset - * - * @author Michael Innerberger */ public class StackStraighteningClient { @@ -39,35 +37,11 @@ public static class Parameters extends CommandLineParameters { required = true) public String stack; - @Parameter( - names = "--targetOwner", - description = "Name of target stack owner (default is same as source stack owner)") - public String targetOwner; - - @Parameter( - names = "--targetProject", - description = "Name of target stack project (default is same as source stack project)") - public String targetProject; - @Parameter( names = "--targetStack", description = "Name of target stack", required = true) public String targetStack; - - @Parameter( - names = "--completeTargetStack", - description = "Complete the target stack after processing all layers", - arity = 0) - public boolean completeTargetStack = false; - - public String getTargetOwner() { - return targetOwner == null ? renderWeb.owner : targetOwner; - } - - public String getTargetProject() { - return targetProject == null ? renderWeb.project : targetProject; - } } public static void main(final String[] args) { @@ -81,51 +55,33 @@ public void runClient(final String[] args) throws Exception { LOG.info("runClient: entry, parameters={}", parameters); final StackStraighteningClient client = new StackStraighteningClient(parameters); - - client.setupDerivedStack(); client.straightenStack(); - - if (parameters.completeTargetStack) { - client.completeTargetStack(); - } } }; clientRunner.run(); } private final Parameters parameters; - private final RenderDataClient sourceDataClient; - private final RenderDataClient targetDataClient; + private final RenderDataClient renderDataClient; private final List zValues; private StackStraighteningClient(final Parameters parameters) throws IOException { this.parameters = parameters; - - this.sourceDataClient = parameters.renderWeb.getDataClient(); - this.targetDataClient = new RenderDataClient(parameters.renderWeb.baseDataUrl, - parameters.getTargetOwner(), - parameters.getTargetProject()); - - this.zValues = sourceDataClient.getStackZValues(parameters.stack); + this.renderDataClient = parameters.renderWeb.getDataClient(); + this.zValues = renderDataClient.getStackZValues(parameters.stack); if (zValues.size() < 2) { throw new IllegalArgumentException("Stack must have at least 2 layers for straightening"); } } - private void setupDerivedStack() throws IOException { - final StackMetaData sourceStackMetaData = sourceDataClient.getStackMetaData(parameters.stack); - targetDataClient.setupDerivedStack(sourceStackMetaData, parameters.targetStack); - } - - private void completeTargetStack() throws Exception { - targetDataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); - } - private void straightenStack() throws Exception { + final StackMetaData sourceStackMetaData = renderDataClient.getStackMetaData(parameters.stack); + renderDataClient.setupDerivedStack(sourceStackMetaData, parameters.targetStack); + final double zMin = zValues.get(0); final double zMax = zValues.get(zValues.size() - 1); @@ -151,11 +107,13 @@ private void straightenStack() throws Exception { straightenLayer(z, zMin, zRange, totalOffsetX, totalOffsetY); } + renderDataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); + LOG.info("straightenStack: exit, processed {} layers", zValues.size()); } private double[] getLayerBoundingBoxMidpoint(final double z) throws Exception { - final ResolvedTileSpecCollection tiles = sourceDataClient.getResolvedTiles(parameters.stack, z); + final ResolvedTileSpecCollection tiles = renderDataClient.getResolvedTiles(parameters.stack, z); final Bounds layerBounds = tiles.toBounds(); if (layerBounds == null || layerBounds.getMinX() == null) { @@ -180,7 +138,7 @@ private void straightenLayer(final Double z, LOG.info("straightenLayer: entry, z={}", z); - final ResolvedTileSpecCollection tiles = sourceDataClient.getResolvedTiles(parameters.stack, z); + final ResolvedTileSpecCollection tiles = renderDataClient.getResolvedTiles(parameters.stack, z); if (tiles.getTileCount() == 0) { LOG.info("straightenLayer: no tiles for z={}, skipping", z); @@ -193,27 +151,22 @@ private void straightenLayer(final Double z, final double factor = (z - zMin) / zRange; // The translation needed to straighten this layer - // We negate the offset because we want to move layers back toward the first layer's position final double translateX = -factor * totalOffsetX; final double translateY = -factor * totalOffsetY; - if (Math.abs(translateX) > 0.001 || Math.abs(translateY) > 0.001) { - final AffineModel2D translationModel = new AffineModel2D(); - translationModel.set(1, 0, 0, 1, translateX, translateY); + final AffineModel2D translationModel = new AffineModel2D(); + translationModel.set(1, 0, 0, 1, translateX, translateY); - final LeafTransformSpec translationTransform = new LeafTransformSpec( - translationModel.getClass().getName(), - translationModel.toDataString()); + final LeafTransformSpec translationTransform = new LeafTransformSpec( + translationModel.getClass().getName(), + translationModel.toDataString()); - tiles.preConcatenateTransformToAllTiles(translationTransform); + tiles.preConcatenateTransformToAllTiles(translationTransform); - LOG.info("straightenLayer: applied translation ({}, {}) to {} tiles for z={}", - translateX, translateY, tiles.getTileCount(), z); - } else { - LOG.info("straightenLayer: no significant translation needed for z={}", z); - } + LOG.info("straightenLayer: applied translation ({}, {}) to {} tiles for z={}", + translateX, translateY, tiles.getTileCount(), z); - targetDataClient.saveResolvedTiles(tiles, parameters.targetStack, z); + renderDataClient.saveResolvedTiles(tiles, parameters.targetStack, z); LOG.info("straightenLayer: exit, saved {} tiles for z={}", tiles.getTileCount(), z); } From 3c5ca489bd9752ca3e871b5247052ce61b1dc05a Mon Sep 17 00:00:00 2001 From: Eric Trautman Date: Tue, 2 Dec 2025 16:11:37 -0500 Subject: [PATCH 3/5] replace getResolvedTiles call with getLayerBounds call --- .../render/client/StackStraighteningClient.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java index 520de5df9..cc5b7b888 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java @@ -1,8 +1,13 @@ package org.janelia.render.client; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; + import java.io.IOException; import java.util.List; +import mpicbg.trakem2.transform.AffineModel2D; + import org.janelia.alignment.spec.Bounds; import org.janelia.alignment.spec.LeafTransformSpec; import org.janelia.alignment.spec.ResolvedTileSpecCollection; @@ -12,15 +17,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParametersDelegate; - -import mpicbg.trakem2.transform.AffineModel2D; - /** * Java client for straightening a stack by gradually applying an offset * computed from the midpoints of the first and last layer bounding boxes. - * + *
* The offset is linearly interpolated across layers: * layer z is moved by (z - zmin) / (zmax - zmin) * offset */ @@ -113,8 +113,7 @@ private void straightenStack() throws Exception { } private double[] getLayerBoundingBoxMidpoint(final double z) throws Exception { - final ResolvedTileSpecCollection tiles = renderDataClient.getResolvedTiles(parameters.stack, z); - final Bounds layerBounds = tiles.toBounds(); + final Bounds layerBounds = renderDataClient.getLayerBounds(parameters.stack, z); if (layerBounds == null || layerBounds.getMinX() == null) { throw new IllegalArgumentException("Cannot compute bounding box for layer z=" + z); From 483c52691bb28a0488c8ccb35898e74f3eb74e87 Mon Sep 17 00:00:00 2001 From: Eric Trautman Date: Tue, 2 Dec 2025 16:24:07 -0500 Subject: [PATCH 4/5] keep z range variables as class members so that they don't need to be passed to private methods --- .../client/StackStraighteningClient.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java index cc5b7b888..555925d38 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java @@ -64,6 +64,9 @@ public void runClient(final String[] args) throws Exception { private final Parameters parameters; private final RenderDataClient renderDataClient; private final List zValues; + private final double minZ; + private final double maxZ; + private final double zRange; private StackStraighteningClient(final Parameters parameters) throws IOException { @@ -75,6 +78,10 @@ private StackStraighteningClient(final Parameters parameters) if (zValues.size() < 2) { throw new IllegalArgumentException("Stack must have at least 2 layers for straightening"); } + + this.minZ = zValues.get(0); + this.maxZ = zValues.get(zValues.size() - 1); + this.zRange = maxZ - minZ; } private void straightenStack() throws Exception { @@ -82,14 +89,11 @@ private void straightenStack() throws Exception { final StackMetaData sourceStackMetaData = renderDataClient.getStackMetaData(parameters.stack); renderDataClient.setupDerivedStack(sourceStackMetaData, parameters.targetStack); - final double zMin = zValues.get(0); - final double zMax = zValues.get(zValues.size() - 1); - - LOG.info("straightenStack: zMin={}, zMax={}", zMin, zMax); + LOG.info("straightenStack: minZ={}, maxZ={}", minZ, maxZ); // Get bounding box midpoints for first and last layers - final double[] firstLayerMidpoint = getLayerBoundingBoxMidpoint(zMin); - final double[] lastLayerMidpoint = getLayerBoundingBoxMidpoint(zMax); + final double[] firstLayerMidpoint = getLayerBoundingBoxMidpoint(minZ); + final double[] lastLayerMidpoint = getLayerBoundingBoxMidpoint(maxZ); // Compute the total offset between first and last layer midpoints final double totalOffsetX = lastLayerMidpoint[0] - firstLayerMidpoint[0]; @@ -100,11 +104,9 @@ private void straightenStack() throws Exception { lastLayerMidpoint[0], lastLayerMidpoint[1], totalOffsetX, totalOffsetY); - final double zRange = zMax - zMin; - // Process each layer for (final Double z : zValues) { - straightenLayer(z, zMin, zRange, totalOffsetX, totalOffsetY); + straightenLayer(z, totalOffsetX, totalOffsetY); } renderDataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); @@ -129,8 +131,6 @@ private double[] getLayerBoundingBoxMidpoint(final double z) throws Exception { } private void straightenLayer(final Double z, - final double zMin, - final double zRange, final double totalOffsetX, final double totalOffsetY) throws Exception { @@ -147,7 +147,7 @@ private void straightenLayer(final Double z, // Compute the interpolation factor for this layer // At zMin, factor = 0 (no movement) // At zMax, factor = 1 (full offset applied, but negated to bring it back to first layer position) - final double factor = (z - zMin) / zRange; + final double factor = (z - minZ) / zRange; // The translation needed to straighten this layer final double translateX = -factor * totalOffsetX; From 291319879ebee24d6d253a1fb496148d34af7b73 Mon Sep 17 00:00:00 2001 From: Eric Trautman Date: Tue, 2 Dec 2025 17:16:56 -0500 Subject: [PATCH 5/5] reduce web service requests by processing z layers in "chunks" instead of individually --- .../client/StackStraighteningClient.java | 102 +++++++++++++----- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java index 555925d38..f9e183b5c 100644 --- a/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java +++ b/render-ws-java-client/src/main/java/org/janelia/render/client/StackStraighteningClient.java @@ -4,13 +4,17 @@ import com.beust.jcommander.ParametersDelegate; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import mpicbg.trakem2.transform.AffineModel2D; import org.janelia.alignment.spec.Bounds; import org.janelia.alignment.spec.LeafTransformSpec; import org.janelia.alignment.spec.ResolvedTileSpecCollection; +import org.janelia.alignment.spec.TileSpec; import org.janelia.alignment.spec.stack.StackMetaData; import org.janelia.render.client.parameter.CommandLineParameters; import org.janelia.render.client.parameter.RenderWebServiceParameters; @@ -42,6 +46,11 @@ public static class Parameters extends CommandLineParameters { description = "Name of target stack", required = true) public String targetStack; + + @Parameter( + names = "--numberOfZLayersPerChunk", + description = "Number of Z layers to process per chunk") + public int numberOfZLayersPerChunk = 1000; } public static void main(final String[] args) { @@ -82,6 +91,10 @@ private StackStraighteningClient(final Parameters parameters) this.minZ = zValues.get(0); this.maxZ = zValues.get(zValues.size() - 1); this.zRange = maxZ - minZ; + + if (parameters.numberOfZLayersPerChunk < 1) { + throw new IllegalArgumentException("numberOfZLayersPerChunk must be at least 1"); + } } private void straightenStack() throws Exception { @@ -104,9 +117,10 @@ private void straightenStack() throws Exception { lastLayerMidpoint[0], lastLayerMidpoint[1], totalOffsetX, totalOffsetY); - // Process each layer - for (final Double z : zValues) { - straightenLayer(z, totalOffsetX, totalOffsetY); + for (int start = 0; start < zValues.size(); start += parameters.numberOfZLayersPerChunk) { + final int end = Math.min(start + parameters.numberOfZLayersPerChunk, zValues.size()); + final List zLayersChunk = zValues.subList(start, end); + straightenLayers(zLayersChunk, totalOffsetX, totalOffsetY); } renderDataClient.setStackState(parameters.targetStack, StackMetaData.StackState.COMPLETE); @@ -130,44 +144,74 @@ private double[] getLayerBoundingBoxMidpoint(final double z) throws Exception { return new double[] { midX, midY }; } - private void straightenLayer(final Double z, - final double totalOffsetX, - final double totalOffsetY) + private void straightenLayers(final List zLayersChunk, + final double totalOffsetX, + final double totalOffsetY) throws Exception { - LOG.info("straightenLayer: entry, z={}", z); + final Double firstZ = zLayersChunk.get(0); + final Double lastZ = zLayersChunk.get(zLayersChunk.size() - 1); - final ResolvedTileSpecCollection tiles = renderDataClient.getResolvedTiles(parameters.stack, z); + LOG.info("straightenLayers: entry, firstZ={}, lastZ={}", firstZ, lastZ); - if (tiles.getTileCount() == 0) { - LOG.info("straightenLayer: no tiles for z={}, skipping", z); - return; - } + final ResolvedTileSpecCollection resolvedTiles = renderDataClient.getResolvedTiles(parameters.stack, + firstZ, + lastZ, + null, + null, + null, + null, + null, + null); + + final Map> zToTileSpecs = new HashMap<>(); + final Map zToTranslation = new HashMap<>(); + + for (final TileSpec tileSpec : resolvedTiles.getTileSpecs()) { + + final List tileSpecsForZ = zToTileSpecs.computeIfAbsent(tileSpec.getZ(), + k -> new ArrayList<>()); + tileSpecsForZ.add(tileSpec); - // Compute the interpolation factor for this layer - // At zMin, factor = 0 (no movement) - // At zMax, factor = 1 (full offset applied, but negated to bring it back to first layer position) - final double factor = (z - minZ) / zRange; + if (! zToTranslation.containsKey(tileSpec.getZ())) { - // The translation needed to straighten this layer - final double translateX = -factor * totalOffsetX; - final double translateY = -factor * totalOffsetY; + final Double z = tileSpec.getZ(); - final AffineModel2D translationModel = new AffineModel2D(); - translationModel.set(1, 0, 0, 1, translateX, translateY); + // Compute the interpolation factor for this layer + // At zMin, factor = 0 (no movement) + // At zMax, factor = 1 (full offset applied, but negated to bring it back to first layer position) + final double factor = (z - minZ) / zRange; - final LeafTransformSpec translationTransform = new LeafTransformSpec( - translationModel.getClass().getName(), - translationModel.toDataString()); + // The translation needed to straighten this layer + final double translateX = -factor * totalOffsetX; + final double translateY = -factor * totalOffsetY; - tiles.preConcatenateTransformToAllTiles(translationTransform); + final AffineModel2D translationModel = new AffineModel2D(); + translationModel.set(1, 0, 0, 1, translateX, translateY); - LOG.info("straightenLayer: applied translation ({}, {}) to {} tiles for z={}", - translateX, translateY, tiles.getTileCount(), z); + zToTranslation.put(z, new LeafTransformSpec(translationModel.getClass().getName(), + translationModel.toDataString())); + } + } + + for (final Double z : zLayersChunk) { + + final List layerTileSpecs = zToTileSpecs.get(z); + final LeafTransformSpec transformSpec = zToTranslation.get(z); + + for (final TileSpec tileSpec : layerTileSpecs) { + resolvedTiles.addTransformSpecToTile(tileSpec.getTileId(), + transformSpec, + ResolvedTileSpecCollection.TransformApplicationMethod.PRE_CONCATENATE_LAST); + } + + LOG.debug("straightenLayers: applied affine transform {} to {} tiles for z={}", + transformSpec.getDataString(), layerTileSpecs.size(), z); + } - renderDataClient.saveResolvedTiles(tiles, parameters.targetStack, z); + renderDataClient.saveResolvedTiles(resolvedTiles, parameters.targetStack, null); - LOG.info("straightenLayer: exit, saved {} tiles for z={}", tiles.getTileCount(), z); + LOG.info("straightenLayers: exit, saved {} tiles for z {} to {}", resolvedTiles.getTileCount(), firstZ, lastZ); } private static final Logger LOG = LoggerFactory.getLogger(StackStraighteningClient.class);