Skip to content

Commit 5e06b92

Browse files
committed
Explorer: Change the position order in Refine Z autofocus to reduce stage travel
1 parent e383e8d commit 5e06b92

1 file changed

Lines changed: 107 additions & 2 deletions

File tree

plugins/Explorer/src/main/java/org/micromanager/explorer/ExplorerManager.java

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,104 @@ private java.util.List<Tile> chooseSpreadTiles(java.util.List<Tile> tiles, int n
20782078
return result;
20792079
}
20802080

2081+
/**
2082+
* Orders the chosen autofocus tiles into a short stage route to minimize XY travel. Uses a
2083+
* greedy nearest-neighbor tour seeded at the current stage position (so the first hop is short
2084+
* too), then a 2-opt cleanup pass to remove the obvious crossings the greedy pass leaves. Point
2085+
* counts here are small (a handful per run), so the O(n^2)/O(n^3) cost is negligible. On
2086+
* multi-well plates the chosen points are already spatially clustered per well, so nearest-
2087+
* neighbor naturally finishes one well before crossing to the next.
2088+
*/
2089+
private java.util.List<Tile> orderForTravel(java.util.List<Tile> chosen) {
2090+
if (chosen.size() <= 2) {
2091+
return chosen;
2092+
}
2093+
java.util.List<Tile> remaining = new java.util.ArrayList<>(chosen);
2094+
2095+
// Seed at the tile nearest the current stage position when it is readable, else the first.
2096+
double curX = 0;
2097+
double curY = 0;
2098+
boolean haveCur = false;
2099+
try {
2100+
String xy = studio_.core().getXYStageDevice();
2101+
if (xy != null && !xy.isEmpty()) {
2102+
curX = studio_.core().getXPosition(xy);
2103+
curY = studio_.core().getYPosition(xy);
2104+
haveCur = true;
2105+
}
2106+
} catch (Exception ignore) {
2107+
// Stage position unavailable -- start from the first chosen tile.
2108+
}
2109+
Tile start = remaining.get(0);
2110+
if (haveCur) {
2111+
double best = Double.MAX_VALUE;
2112+
for (Tile t : remaining) {
2113+
double d = dist2(t.stageX, t.stageY, curX, curY);
2114+
if (d < best) {
2115+
best = d;
2116+
start = t;
2117+
}
2118+
}
2119+
}
2120+
remaining.remove(start);
2121+
java.util.List<Tile> route = new java.util.ArrayList<>(chosen.size());
2122+
route.add(start);
2123+
2124+
// Greedy nearest-neighbor.
2125+
while (!remaining.isEmpty()) {
2126+
Tile last = route.get(route.size() - 1);
2127+
Tile next = remaining.get(0);
2128+
double best = Double.MAX_VALUE;
2129+
for (Tile t : remaining) {
2130+
double d = dist2(last.stageX, last.stageY, t.stageX, t.stageY);
2131+
if (d < best) {
2132+
best = d;
2133+
next = t;
2134+
}
2135+
}
2136+
remaining.remove(next);
2137+
route.add(next);
2138+
}
2139+
2140+
twoOpt(route);
2141+
return route;
2142+
}
2143+
2144+
/** In-place 2-opt improvement of an open tour (route start/end are not joined). */
2145+
private void twoOpt(java.util.List<Tile> route) {
2146+
int n = route.size();
2147+
boolean improved = true;
2148+
while (improved) {
2149+
improved = false;
2150+
for (int i = 0; i < n - 1; i++) {
2151+
for (int k = i + 1; k < n; k++) {
2152+
// Reversing route[i..k] changes only the edges entering i and leaving k.
2153+
double before = (i > 0 ? edge(route, i - 1, i) : 0)
2154+
+ (k < n - 1 ? edge(route, k, k + 1) : 0);
2155+
double after = (i > 0 ? edge(route, i - 1, k) : 0)
2156+
+ (k < n - 1 ? edge(route, i, k + 1) : 0);
2157+
if (after + 1e-6 < before) {
2158+
java.util.Collections.reverse(route.subList(i, k + 1));
2159+
improved = true;
2160+
}
2161+
}
2162+
}
2163+
}
2164+
}
2165+
2166+
/** Euclidean stage distance between route entries a and b. */
2167+
private double edge(java.util.List<Tile> route, int a, int b) {
2168+
Tile ta = route.get(a);
2169+
Tile tb = route.get(b);
2170+
return Math.sqrt(dist2(ta.stageX, ta.stageY, tb.stageX, tb.stageY));
2171+
}
2172+
2173+
private static double dist2(double x1, double y1, double x2, double y2) {
2174+
double dx = x1 - x2;
2175+
double dy = y1 - y2;
2176+
return dx * dx + dy * dy;
2177+
}
2178+
20812179
/** Farthest-point sampling of {@code count} tiles from {@code group} (Euclidean stage XY). */
20822180
private java.util.List<Tile> farthestPointSample(java.util.List<Tile> group, int count) {
20832181
java.util.List<Tile> chosen = new java.util.ArrayList<>();
@@ -2855,18 +2953,25 @@ public void startRefineZAutomatic(int nPoints, String afMethodName, boolean with
28552953
if (edgeMargin > 0) {
28562954
java.util.List<Tile> interior = excludeEdgeTiles(tiles, edgeMargin);
28572955
// Only apply the exclusion if it leaves something to focus on; otherwise the grid is
2858-
// too thin for the requested margin and we fall back to the full set.
2956+
// too thin for the requested margin and we fall back to the full set. Warn modally so
2957+
// the fallback is not lost under the "Refining Z..."/progress status updates that follow.
28592958
if (!interior.isEmpty()) {
28602959
tiles = interior;
28612960
} else {
2862-
setRefineZStatus("Edge margin too large for this grid; using all tiles.");
2961+
JOptionPane.showMessageDialog(refineZFrame_ != null ? refineZFrame_ : frame_,
2962+
"The edge margin of " + edgeMargin + " tile(s) excludes every tile in this "
2963+
+ "grid.\nRefine Z will use all tiles instead.",
2964+
"Refine Z", JOptionPane.WARNING_MESSAGE);
28632965
}
28642966
}
28652967
java.util.List<Tile> chosen = chooseSpreadTiles(tiles, nPoints);
28662968
if (chosen.isEmpty()) {
28672969
setRefineZStatus("No tiles to refine.");
28682970
return;
28692971
}
2972+
// The spread-sampling order is travel-arbitrary; reorder into a short stage route so
2973+
// autofocus visits points with minimal XY motion.
2974+
chosen = orderForTravel(chosen);
28702975
setRefineZRunningUi(true);
28712976
setRefineZStatus("Refining Z...");
28722977
refineZWorker_ = new RefineZWorker(chosen, zStages, afMethodName);

0 commit comments

Comments
 (0)