Skip to content

Commit c2ae2d2

Browse files
committed
Apply offset fixes to Spectral classes
1 parent 00ab360 commit c2ae2d2

2 files changed

Lines changed: 201 additions & 17 deletions

File tree

src/main/java/sc/fiji/snt/analysis/MultiSpectralRefiner.java

Lines changed: 147 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ public class MultiSpectralRefiner implements Callable<Path> {
105105
private final double ySpacing;
106106
private final double zSpacing;
107107

108+
// Pixel offset of (0,0,0) in the true-world grid Path nodes are stored in,
109+
// relative to imp's own pixel grid (see SNT#getActiveCanvasPixelOffset()).
110+
// Zero by default: this class stays SNT-agnostic, but a caller working off
111+
// a materialized crop or a shifted world origin must supply it, or
112+
// initWorkingNodes() will index the wrong voxels
113+
private final double xOffset;
114+
private final double yOffset;
115+
private final double zOffset;
116+
108117
// Path data
109118
private final Path path;
110119

@@ -132,8 +141,8 @@ public class MultiSpectralRefiner implements Callable<Path> {
132141
// A sphere of radius r in physical space maps to an ellipsoid in pixel space:
133142
// (dx*xSpacing)² + (dy*ySpacing)² + (dz*zSpacing)² < (r*xSpacing)²
134143
// Dividing by xSpacing²: dx² + dy²*(ySpacing/xSpacing)² + dz²*(zSpacing/xSpacing)² < r²
135-
private double yAniso2; // (ySpacing / xSpacing)²
136-
private double zAniso2; // (zSpacing / xSpacing)²
144+
private final double yAniso2; // (ySpacing / xSpacing)²
145+
private final double zAniso2; // (zSpacing / xSpacing)²
137146

138147
/**
139148
* Creates a refiner for a single path using an ImagePlus.
@@ -143,6 +152,24 @@ public class MultiSpectralRefiner implements Callable<Path> {
143152
* @throws IllegalArgumentException if the image has fewer than 2 channels
144153
*/
145154
public MultiSpectralRefiner(final ImagePlus imp, final Path path) {
155+
this(imp, path, 0, 0, 0);
156+
}
157+
158+
/**
159+
* Creates a refiner for a single path using an ImagePlus, where the path's
160+
* node coordinates and imp's own pixel grid are related by a fixed offset
161+
* (e.g. a materialized crop, or a source with a non-zero world origin
162+
* offset). See {@code SNT#getActiveCanvasPixelOffset()}.
163+
*
164+
* @param imp the multichannel image (Brainbow, etc.)
165+
* @param path the path to refine
166+
* @param xOffset pixel offset of world (0,0,0) on imp's x axis
167+
* @param yOffset pixel offset of world (0,0,0) on imp's y axis
168+
* @param zOffset pixel offset of world (0,0,0) on imp's z axis
169+
* @throws IllegalArgumentException if the image has fewer than 2 channels
170+
*/
171+
public MultiSpectralRefiner(final ImagePlus imp, final Path path, final double xOffset,
172+
final double yOffset, final double zOffset) {
146173
if (imp.getNChannels() < 2)
147174
throw new IllegalArgumentException("Multispectral refinement requires at least 2 channels");
148175
if (path == null || path.size() < 2)
@@ -155,6 +182,9 @@ public MultiSpectralRefiner(final ImagePlus imp, final Path path) {
155182
this.xSpacing = imp.getCalibration().pixelWidth;
156183
this.ySpacing = imp.getCalibration().pixelHeight;
157184
this.zSpacing = imp.getCalibration().pixelDepth;
185+
this.xOffset = xOffset;
186+
this.yOffset = yOffset;
187+
this.zOffset = zOffset;
158188
this.channelStacks = new ImageStack[nChannels];
159189
for (int i = 1; i <= nChannels; i++) {
160190
channelStacks[i - 1] = ImpUtils.getChannelStack(imp, i);
@@ -185,7 +215,25 @@ public MultiSpectralRefiner(final ImagePlus imp, final Path path) {
185215
* @see ImgUtils#toImagePlus(ImgPlus)
186216
*/
187217
public <T extends NumericType<T>> MultiSpectralRefiner(final ImgPlus<T> img, final Path path) {
188-
this(ImgUtils.toImagePlus(img), path);
218+
this(ImgUtils.toImagePlus(img), path, 0, 0, 0);
219+
}
220+
221+
/**
222+
* Creates a refiner for a single path using an ImgPlus, where the path's
223+
* node coordinates and img's own pixel grid are related by a fixed offset.
224+
* See {@link #MultiSpectralRefiner(ImagePlus, Path, double, double, double)}.
225+
*
226+
* @param img the multichannel image (Brainbow, etc.)
227+
* @param path the path to refine
228+
* @param xOffset pixel offset of world (0,0,0) on img's x axis
229+
* @param yOffset pixel offset of world (0,0,0) on img's y axis
230+
* @param zOffset pixel offset of world (0,0,0) on img's z axis
231+
* @throws IllegalArgumentException if the image has fewer than 2 channels
232+
* @see ImgUtils#toImagePlus(ImgPlus)
233+
*/
234+
public <T extends NumericType<T>> MultiSpectralRefiner(final ImgPlus<T> img, final Path path,
235+
final double xOffset, final double yOffset, final double zOffset) {
236+
this(ImgUtils.toImagePlus(img), path, xOffset, yOffset, zOffset);
189237
}
190238

191239
/**
@@ -506,6 +554,9 @@ public Path call() {
506554
}
507555
}
508556

557+
/** Suffix appended to path names after multi-spectral refinement. */
558+
private static final String REFINED_SUFFIX = " [Refined*]";
559+
509560
/**
510561
* Applies the refinement result to the original path by replacing all its
511562
* nodes with the refined geometry. This preserves hierarchical relationships
@@ -514,9 +565,6 @@ public Path call() {
514565
*
515566
* @throws IllegalStateException if {@link #call()} has not been run or failed
516567
*/
517-
/** Suffix appended to path names after multi-spectral refinement. */
518-
private static final String REFINED_SUFFIX = " [Refined*]";
519-
520568
public void apply() {
521569
if (refined == null || !succeeded)
522570
throw new IllegalStateException("No refinement result to apply");
@@ -546,7 +594,8 @@ private void initWorkingNodes() {
546594
workingNodes = new ArrayList<>(path.size());
547595
for (int i = 0; i < path.size(); i++) {
548596
final PointInImage node = path.getNode(i);
549-
final int[] px = SpectralSimilarity.nodeToPixelCoords(node, xSpacing, ySpacing, zSpacing);
597+
final int[] px = SpectralSimilarity.nodeToPixelCoords(node, xSpacing, ySpacing, zSpacing,
598+
xOffset, yOffset, zOffset);
550599
// Use interpolated radius if available, otherwise the node's own radius
551600
double radius = node.radius;
552601
if (interpolated != null && interpolated.containsKey(i))
@@ -645,7 +694,7 @@ private void autoTuneParameters() {
645694
}
646695
if (validR > 0) {
647696
final int suggested = (int) Math.ceil((sumR / validR) * 2.5);
648-
final int newMax = Math.max(3, Math.min(30, suggested));
697+
final int newMax = Math.clamp(suggested, 3, 30);
649698
if (newMax != maxRadius) {
650699
SNTUtils.log("MSRefiner: Auto-tune maxRadius: " + maxRadius + " -> " + newMax
651700
+ " (mean path radius=" + String.format("%.1f", sumR / validR) + "px)");
@@ -678,7 +727,7 @@ private void autoTuneParameters() {
678727
final double simMean = simSum / n;
679728
final double simStd = Math.sqrt(Math.max(0, simSumSq / n - simMean * simMean));
680729
final double suggested = simMean - 2.0 * simStd;
681-
final double newThreshold = Math.max(0.70, Math.min(0.98, suggested));
730+
final double newThreshold = Math.clamp(suggested, 0.70, 0.98);
682731
if (Math.abs(newThreshold - cosSimilarityThreshold) > 0.01) {
683732
SNTUtils.log("MSRefiner: Auto-tune cosSimilarityThreshold: "
684733
+ String.format("%.3f -> %.3f", cosSimilarityThreshold, newThreshold)
@@ -1086,10 +1135,27 @@ private static double channelSum(final double[] color) {
10861135
* @return the number of paths successfully refined
10871136
*/
10881137
public static int refineTree(final ImagePlus imp, final Tree tree) {
1138+
return refineTree(imp, tree, 0, 0, 0);
1139+
}
1140+
1141+
/**
1142+
* Refines all paths in a tree, processing each path in parallel, where the
1143+
* tree's node coordinates and imp's own pixel grid are related by a fixed
1144+
* offset. See {@link #MultiSpectralRefiner(ImagePlus, Path, double, double, double)}.
1145+
*
1146+
* @param imp the multichannel image
1147+
* @param tree the tree to refine
1148+
* @param xOffset pixel offset of world (0,0,0) on imp's x axis
1149+
* @param yOffset pixel offset of world (0,0,0) on imp's y axis
1150+
* @param zOffset pixel offset of world (0,0,0) on imp's z axis
1151+
* @return the number of paths successfully refined
1152+
*/
1153+
public static int refineTree(final ImagePlus imp, final Tree tree, final double xOffset,
1154+
final double yOffset, final double zOffset) {
10891155
final List<MultiSpectralRefiner> refiners = new ArrayList<>();
10901156
for (final Path p : tree.list()) {
10911157
if (p.size() >= 2) {
1092-
refiners.add(new MultiSpectralRefiner(imp, p));
1158+
refiners.add(new MultiSpectralRefiner(imp, p, xOffset, yOffset, zOffset));
10931159
}
10941160
}
10951161
// Parallel refinement (thread-safe)
@@ -1117,6 +1183,23 @@ public static <T extends NumericType<T>> int refineTree(final ImgPlus<T> img, fi
11171183
return refineTree(ImgUtils.toImagePlus(img), tree);
11181184
}
11191185

1186+
/**
1187+
* Refines all paths in a tree, processing each path in parallel, with an
1188+
* offset. See {@link #refineTree(ImagePlus, Tree, double, double, double)}.
1189+
*
1190+
* @param img the multichannel image (ImgPlus)
1191+
* @param tree the tree to refine
1192+
* @param xOffset pixel offset of world (0,0,0) on img's x axis
1193+
* @param yOffset pixel offset of world (0,0,0) on img's y axis
1194+
* @param zOffset pixel offset of world (0,0,0) on img's z axis
1195+
* @return the number of paths successfully refined
1196+
* @see ImgUtils#toImagePlus(ImgPlus)
1197+
*/
1198+
public static <T extends NumericType<T>> int refineTree(final ImgPlus<T> img, final Tree tree,
1199+
final double xOffset, final double yOffset, final double zOffset) {
1200+
return refineTree(ImgUtils.toImagePlus(img), tree, xOffset, yOffset, zOffset);
1201+
}
1202+
11201203
/**
11211204
* Refines all paths in a tree with custom parameters.
11221205
*
@@ -1140,10 +1223,28 @@ public static <T extends NumericType<T>> int refineTree(final ImgPlus<T> img, fi
11401223
* @return the number of paths successfully refined
11411224
*/
11421225
public static int refineTree(final ImagePlus imp, final Tree tree, final Parameters params) {
1226+
return refineTree(imp, tree, params, 0, 0, 0);
1227+
}
1228+
1229+
/**
1230+
* Refines all paths in a tree with custom parameters, where the tree's node
1231+
* coordinates and imp's own pixel grid are related by a fixed offset. See
1232+
* {@link #MultiSpectralRefiner(ImagePlus, Path, double, double, double)}.
1233+
*
1234+
* @param imp the multichannel image
1235+
* @param tree the tree to refine
1236+
* @param params the parameter set to apply to all refiners
1237+
* @param xOffset pixel offset of world (0,0,0) on imp's x axis
1238+
* @param yOffset pixel offset of world (0,0,0) on imp's y axis
1239+
* @param zOffset pixel offset of world (0,0,0) on imp's z axis
1240+
* @return the number of paths successfully refined
1241+
*/
1242+
public static int refineTree(final ImagePlus imp, final Tree tree, final Parameters params,
1243+
final double xOffset, final double yOffset, final double zOffset) {
11431244
final List<MultiSpectralRefiner> refiners = new ArrayList<>();
11441245
for (final Path p : tree.list()) {
11451246
if (p.size() >= 2) {
1146-
final MultiSpectralRefiner r = new MultiSpectralRefiner(imp, p);
1247+
final MultiSpectralRefiner r = new MultiSpectralRefiner(imp, p, xOffset, yOffset, zOffset);
11471248
params.applyTo(r);
11481249
refiners.add(r);
11491250
}
@@ -1172,6 +1273,23 @@ public static <T extends NumericType<T>> double[] computeNodeCosts(final ImgPlus
11721273
return computeNodeCosts(ImgUtils.toImagePlus(img), path);
11731274
}
11741275

1276+
/**
1277+
* Computes the cost at each node of a path without modifying it, with an
1278+
* offset. See {@link #computeNodeCosts(ImagePlus, Path, double, double, double)}.
1279+
*
1280+
* @param img the multichannel image (ImgPlus)
1281+
* @param path the path to evaluate
1282+
* @param xOffset pixel offset of world (0,0,0) on img's x axis
1283+
* @param yOffset pixel offset of world (0,0,0) on img's y axis
1284+
* @param zOffset pixel offset of world (0,0,0) on img's z axis
1285+
* @return per-node cost values (same length as path.size())
1286+
* @see ImgUtils#toImagePlus(ImgPlus)
1287+
*/
1288+
public static <T extends NumericType<T>> double[] computeNodeCosts(final ImgPlus<T> img, final Path path,
1289+
final double xOffset, final double yOffset, final double zOffset) {
1290+
return computeNodeCosts(ImgUtils.toImagePlus(img), path, xOffset, yOffset, zOffset);
1291+
}
1292+
11751293
/**
11761294
* Computes the cost at each node of a path without modifying it.
11771295
* Useful for diagnostics and for the "Color Drift" plausibility check.
@@ -1181,7 +1299,24 @@ public static <T extends NumericType<T>> double[] computeNodeCosts(final ImgPlus
11811299
* @return per-node cost values (same length as path.size())
11821300
*/
11831301
public static double[] computeNodeCosts(final ImagePlus imp, final Path path) {
1184-
final MultiSpectralRefiner refiner = new MultiSpectralRefiner(imp, path);
1302+
return computeNodeCosts(imp, path, 0, 0, 0);
1303+
}
1304+
1305+
/**
1306+
* Computes the cost at each node of a path without modifying it, where the
1307+
* path's node coordinates and imp's own pixel grid are related by a fixed
1308+
* offset. See {@link #MultiSpectralRefiner(ImagePlus, Path, double, double, double)}.
1309+
*
1310+
* @param imp the multichannel image
1311+
* @param path the path to evaluate
1312+
* @param xOffset pixel offset of world (0,0,0) on imp's x axis
1313+
* @param yOffset pixel offset of world (0,0,0) on imp's y axis
1314+
* @param zOffset pixel offset of world (0,0,0) on imp's z axis
1315+
* @return per-node cost values (same length as path.size())
1316+
*/
1317+
public static double[] computeNodeCosts(final ImagePlus imp, final Path path, final double xOffset,
1318+
final double yOffset, final double zOffset) {
1319+
final MultiSpectralRefiner refiner = new MultiSpectralRefiner(imp, path, xOffset, yOffset, zOffset);
11851320
refiner.initWorkingNodes();
11861321
refiner.voxelCache = new HashMap<>();
11871322
refiner.colorRef = refiner.computeReferenceColor(0, refiner.workingNodes.size());

src/main/java/sc/fiji/snt/filter/SpectralSimilarity.java

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,16 @@ public double[] getReferenceColor() {
129129
return referenceColor.clone();
130130
}
131131

132-
// -- Shared static utilities (used by MultiSpectralRefiner, ComputeSecondaryImg, etc.) --
133132

134133
/**
135134
* Converts a path node from calibrated (real-world) coordinates to pixel
136135
* coordinates by dividing by the voxel spacing and rounding.
136+
* <p>
137+
* Equivalent to calling {@link #nodeToPixelCoords(sc.fiji.snt.util.PointInImage, double, double,
138+
* double, double, double, double)} with a zero offset - only correct when the target image's own
139+
* pixel (0,0,0) coincides with the node's calibrated (0,0,0), i.e. no materialized crop and no
140+
* {@code SNT#getWorldOriginOffset()}. Callers indexing into {@code SNT#getLoadedData()}/an
141+
* {@code SNT#getImagePlus()}-derived image should prefer the offset-aware overload instead.
137142
*
138143
* @param node the node in calibrated coordinates
139144
* @param xSpacing voxel width
@@ -144,10 +149,34 @@ public double[] getReferenceColor() {
144149
public static int[] nodeToPixelCoords(final sc.fiji.snt.util.PointInImage node,
145150
final double xSpacing, final double ySpacing,
146151
final double zSpacing) {
152+
return nodeToPixelCoords(node, xSpacing, ySpacing, zSpacing, 0, 0, 0);
153+
}
154+
155+
/**
156+
* As {@link #nodeToPixelCoords(sc.fiji.snt.util.PointInImage, double, double, double)}, but also
157+
* adding a pixel-space offset after scaling - typically {@code SNT#getActiveCanvasPixelOffset()},
158+
* needed whenever the target image is the crop-local grid of a materialized crop, or the raw
159+
* streamed source's own voxel grid under a non-zero {@code SNT#getWorldOriginOffset()} (see
160+
* {@code SNT#createSearch(double, double, double, double, double, double)} for the same
161+
* conversion, applied to A* search endpoints instead of Path nodes).
162+
*
163+
* @param node the node in calibrated coordinates
164+
* @param xSpacing voxel width
165+
* @param ySpacing voxel height
166+
* @param zSpacing voxel depth
167+
* @param xOffset pixel-space offset added after scaling, x axis
168+
* @param yOffset pixel-space offset added after scaling, y axis
169+
* @param zOffset pixel-space offset added after scaling, z axis
170+
* @return pixel coordinates as {@code [x, y, z]}
171+
*/
172+
public static int[] nodeToPixelCoords(final sc.fiji.snt.util.PointInImage node,
173+
final double xSpacing, final double ySpacing,
174+
final double zSpacing, final double xOffset,
175+
final double yOffset, final double zOffset) {
147176
return new int[]{
148-
(int) Math.round(node.x / xSpacing),
149-
(int) Math.round(node.y / ySpacing),
150-
(int) Math.round(node.z / zSpacing)
177+
(int) Math.round(node.x / xSpacing + xOffset),
178+
(int) Math.round(node.y / ySpacing + yOffset),
179+
(int) Math.round(node.z / zSpacing + zOffset)
151180
};
152181
}
153182

@@ -380,6 +409,25 @@ public static <T extends RealType<T>> double[] averageColorFromPaths(
380409
final RandomAccessibleInterval<T> input,
381410
final java.util.List<sc.fiji.snt.Path> paths,
382411
final double xSpacing, final double ySpacing, final double zSpacing) {
412+
return averageColorFromPaths(input, paths, xSpacing, ySpacing, zSpacing, 0, 0, 0);
413+
}
414+
415+
/**
416+
* As {@link #averageColorFromPaths(RandomAccessibleInterval, java.util.List, double, double,
417+
* double)}, but also adding a pixel-space offset after scaling (see
418+
* {@link #nodeToPixelCoords(sc.fiji.snt.util.PointInImage, double, double, double, double, double,
419+
* double)}) - needed whenever {@code input} is the crop-local grid of a materialized crop, or the
420+
* raw streamed source's own voxel grid under a non-zero {@code SNT#getWorldOriginOffset()}.
421+
*
422+
* @param xOffset pixel-space offset added after scaling, x axis
423+
* @param yOffset pixel-space offset added after scaling, y axis
424+
* @param zOffset pixel-space offset added after scaling, z axis
425+
*/
426+
public static <T extends RealType<T>> double[] averageColorFromPaths(
427+
final RandomAccessibleInterval<T> input,
428+
final java.util.List<sc.fiji.snt.Path> paths,
429+
final double xSpacing, final double ySpacing, final double zSpacing,
430+
final double xOffset, final double yOffset, final double zOffset) {
383431
// Count total nodes
384432
int totalNodes = 0;
385433
for (final sc.fiji.snt.Path p : paths) totalNodes += p.size();
@@ -390,7 +438,8 @@ public static <T extends RealType<T>> double[] averageColorFromPaths(
390438
int idx = 0;
391439
for (final sc.fiji.snt.Path p : paths) {
392440
for (int i = 0; i < p.size(); i++) {
393-
positions[idx++] = nodeToPixelCoords(p.getNode(i), xSpacing, ySpacing, zSpacing);
441+
positions[idx++] = nodeToPixelCoords(p.getNode(i), xSpacing, ySpacing, zSpacing,
442+
xOffset, yOffset, zOffset);
394443
}
395444
}
396445
return averageColorAtPositions(input, positions);

0 commit comments

Comments
 (0)