@@ -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 ());
0 commit comments