Skip to content

Commit 85f436e

Browse files
authored
Merge pull request micro-manager#2403 from nicost/explorer
Explorer: Refine Z Autofocus can now exclude edge tiles
2 parents ac9a8dc + e285a01 commit 85f436e

3 files changed

Lines changed: 231 additions & 8 deletions

File tree

libraries/TiledDataViewer/src/main/java/org/micromanager/tileddataviewer/internal/gui/ImageMaker.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ public synchronized Image makeOrGetImage(DataViewCoords viewCoords) {
210210
TaggedImage imageForDisplay = getDisplayImage(axes, viewCoords.getResolutionIndex(),
211211
viewOffsetAtResX, viewOffsetAtResY, imagePixelWidth, imagePixelHeight);
212212

213+
// The requested resolution level / region may have no data yet - e.g. a zoom
214+
// that requests a pyramid level still being generated while tiles are acquired
215+
// concurrently. Skip this channel for now (keeping the last frame for it); the
216+
// next render fills it in once the data is ready. Without this guard the null
217+
// deref below throws, aborts the render, and leaves the canvas blank until the
218+
// zoom level changes.
219+
if (imageForDisplay == null) {
220+
continue;
221+
}
222+
213223
if (latestTags_ == null
214224
|| (viewCoords.getAxesPositions().containsKey(TiledDataViewer.CHANNEL_AXIS)
215225
&& viewCoords.getAxesPositions().get(TiledDataViewer.CHANNEL_AXIS)

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

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,63 @@ private java.util.List<Tile> collectAcceptedTiles(boolean withinVesselOnly) thro
19891989
return computePositionGrid(withinVesselOnly).tiles;
19901990
}
19911991

1992+
/**
1993+
* Drops tiles within {@code margin} grid tiles of the edge of the occupied region, per well.
1994+
* The edge is followed raggedly: a tile is excluded when it is within {@code margin} of the
1995+
* first/last occupied column in its own grid row OR of the first/last occupied row in its own
1996+
* grid column. This removes the outer ring(s) of tiles - which often only partially overlap the
1997+
* sample and make autofocus fail - while tracking a non-rectangular (curved/diagonal) ROI
1998+
* outline rather than a plain bounding box. Returns the kept tiles (input order preserved).
1999+
*/
2000+
private java.util.List<Tile> excludeEdgeTiles(java.util.List<Tile> tiles, int margin) {
2001+
if (margin <= 0 || tiles.isEmpty()) {
2002+
return new java.util.ArrayList<>(tiles);
2003+
}
2004+
// Per-well occupied extents: for each (well,row) the min/max col, for each (well,col) the
2005+
// min/max row. Non-plate tiles share a single well key of 0.
2006+
java.util.Map<Long, int[]> colRangeByRow = new java.util.HashMap<>(); // {minCol,maxCol}
2007+
java.util.Map<Long, int[]> rowRangeByCol = new java.util.HashMap<>(); // {minRow,maxRow}
2008+
for (Tile t : tiles) {
2009+
long well = wellKeyOf(t);
2010+
long rowKey = (well << 24) ^ (t.row & 0xffffffL);
2011+
long colKey = (well << 24) ^ (t.col & 0xffffffL);
2012+
int[] cr = colRangeByRow.get(rowKey);
2013+
if (cr == null) {
2014+
colRangeByRow.put(rowKey, new int[] {t.col, t.col});
2015+
} else {
2016+
cr[0] = Math.min(cr[0], t.col);
2017+
cr[1] = Math.max(cr[1], t.col);
2018+
}
2019+
int[] rr = rowRangeByCol.get(colKey);
2020+
if (rr == null) {
2021+
rowRangeByCol.put(colKey, new int[] {t.row, t.row});
2022+
} else {
2023+
rr[0] = Math.min(rr[0], t.row);
2024+
rr[1] = Math.max(rr[1], t.row);
2025+
}
2026+
}
2027+
java.util.List<Tile> kept = new java.util.ArrayList<>();
2028+
for (Tile t : tiles) {
2029+
long well = wellKeyOf(t);
2030+
int[] cr = colRangeByRow.get((well << 24) ^ (t.row & 0xffffffL));
2031+
int[] rr = rowRangeByCol.get((well << 24) ^ (t.col & 0xffffffL));
2032+
boolean edge = (t.col - cr[0] < margin) || (cr[1] - t.col < margin)
2033+
|| (t.row - rr[0] < margin) || (rr[1] - t.row < margin);
2034+
if (!edge) {
2035+
kept.add(t);
2036+
}
2037+
}
2038+
return kept;
2039+
}
2040+
2041+
/** Packs a tile's well {row,col} (or 0 for non-plate) into a stable long key. */
2042+
private static long wellKeyOf(Tile t) {
2043+
if (t.well == null) {
2044+
return 0L;
2045+
}
2046+
return ((long) t.well[0] << 32) | (t.well[1] & 0xffffffffL);
2047+
}
2048+
19922049
/**
19932050
* Chooses up to {@code n} tiles TOTAL, spread across the given set using farthest-point sampling
19942051
* in stage space. On multi-well plates the total budget {@code n} is distributed across the
@@ -2003,11 +2060,7 @@ private java.util.List<Tile> chooseSpreadTiles(java.util.List<Tile> tiles, int n
20032060
// Group by well so the budget is spread across wells; non-plate -> single group.
20042061
java.util.Map<Long, java.util.List<Tile>> byWell = new java.util.LinkedHashMap<>();
20052062
for (Tile t : tiles) {
2006-
long key = 0;
2007-
if (t.well != null) {
2008-
key = ((long) t.well[0] << 32) | (t.well[1] & 0xffffffffL);
2009-
}
2010-
byWell.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(t);
2063+
byWell.computeIfAbsent(wellKeyOf(t), k -> new java.util.ArrayList<>()).add(t);
20112064
}
20122065
int nWells = byWell.size();
20132066
// Distribute n across wells: a base count each, plus one extra to the first 'remainder'
@@ -2025,6 +2078,104 @@ private java.util.List<Tile> chooseSpreadTiles(java.util.List<Tile> tiles, int n
20252078
return result;
20262079
}
20272080

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+
20282179
/** Farthest-point sampling of {@code count} tiles from {@code group} (Euclidean stage XY). */
20292180
private java.util.List<Tile> farthestPointSample(java.util.List<Tile> group, int count) {
20302181
java.util.List<Tile> chosen = new java.util.ArrayList<>();
@@ -2778,7 +2929,8 @@ public void cancelRefineZ() {
27782929
* runs the selected autofocus method, and records the resulting Z of every checked Z stage.
27792930
* Runs off the EDT.
27802931
*/
2781-
public void startRefineZAutomatic(int nPoints, String afMethodName, boolean withinVesselOnly) {
2932+
public void startRefineZAutomatic(int nPoints, String afMethodName, boolean withinVesselOnly,
2933+
int edgeMargin) {
27822934
if (dataSource_ == null || !exploring_ || loadedData_ || !hasPositionRoi()) {
27832935
setRefineZStatus("Draw an ROI first.");
27842936
return;
@@ -2798,11 +2950,28 @@ public void startRefineZAutomatic(int nPoints, String afMethodName, boolean with
27982950
setRefineZStatus(ex.getMessage());
27992951
return;
28002952
}
2953+
if (edgeMargin > 0) {
2954+
java.util.List<Tile> interior = excludeEdgeTiles(tiles, edgeMargin);
2955+
// Only apply the exclusion if it leaves something to focus on; otherwise the grid is
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.
2958+
if (!interior.isEmpty()) {
2959+
tiles = interior;
2960+
} else {
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);
2965+
}
2966+
}
28012967
java.util.List<Tile> chosen = chooseSpreadTiles(tiles, nPoints);
28022968
if (chosen.isEmpty()) {
28032969
setRefineZStatus("No tiles to refine.");
28042970
return;
28052971
}
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);
28062975
setRefineZRunningUi(true);
28072976
setRefineZStatus("Refining Z...");
28082977
refineZWorker_ = new RefineZWorker(chosen, zStages, afMethodName);

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.micromanager.Studio;
1717
import org.micromanager.internal.positionlist.utils.ZGenerator;
1818
import org.micromanager.internal.utils.WindowPositioning;
19+
import org.micromanager.propertymap.MutablePropertyMapView;
1920

2021
/**
2122
* Stand-alone window for Refine Z. Opened from the Explorer dialog's "Refine Z..." button so the
@@ -28,15 +29,20 @@
2829
*/
2930
public class RefineZFrame extends JFrame {
3031

32+
private static final String EDGE_MARGIN = "RefineZEdgeMargin";
33+
private static final String AF_METHOD = "RefineZAutofocusMethod";
34+
3135
private final Studio studio_;
3236
private final ExplorerManager explorerManager_;
3337
private final ExplorerFrame explorerFrame_;
38+
private final MutablePropertyMapView settings_;
3439

3540
private final JRadioButton autoRadio_;
3641
private final JRadioButton manualRadio_;
3742
private final JComboBox<ZGenerator.Type> interpCombo_;
3843
private final JComboBox<String> afCombo_;
3944
private final JSpinner pointsSpinner_;
45+
private final JSpinner edgeMarginSpinner_;
4046
private final JButton startButton_;
4147
private final JButton cancelButton_;
4248
private final JButton setButton_;
@@ -62,6 +68,7 @@ public RefineZFrame(Studio studio, ExplorerManager explorerManager,
6268
studio_ = studio;
6369
explorerManager_ = explorerManager;
6470
explorerFrame_ = explorerFrame;
71+
settings_ = studio_.profile().getSettings(RefineZFrame.class);
6572

6673
setTitle("Refine Z");
6774
URL iconUrl = getClass().getResource("/org/micromanager/icons/microscope.gif");
@@ -104,6 +111,7 @@ public RefineZFrame(Studio studio, ExplorerManager explorerManager,
104111
afCombo_.addActionListener(e -> {
105112
Object sel = afCombo_.getSelectedItem();
106113
if (sel != null && !syncingAf_) {
114+
settings_.putString(AF_METHOD, sel.toString());
107115
try {
108116
studio_.getAutofocusManager().setAutofocusMethodByName(sel.toString());
109117
} catch (Exception ex) {
@@ -112,16 +120,29 @@ public RefineZFrame(Studio studio, ExplorerManager explorerManager,
112120
}
113121
});
114122
panel.add(afCombo_);
123+
// Created before the Start button so its action listener can read it (final field).
124+
edgeMarginSpinner_ = new JSpinner(new SpinnerNumberModel(
125+
settings_.getInteger(EDGE_MARGIN, 0), 0, 99, 1));
126+
edgeMarginSpinner_.setToolTipText(
127+
"Exclude reference points within this many tiles of the grid edge. "
128+
+ "Outer tiles often only partially overlap the sample and can fail autofocus. "
129+
+ "0 = use all tiles.");
130+
edgeMarginSpinner_.addChangeListener(e ->
131+
settings_.putInteger(EDGE_MARGIN, (Integer) edgeMarginSpinner_.getValue()));
115132
startButton_ = new JButton("Start");
116133
startButton_.addActionListener(e -> {
117134
Object af = afCombo_.getSelectedItem();
118135
explorerManager_.startRefineZAutomatic(
119136
(Integer) pointsSpinner_.getValue(),
120137
af != null ? af.toString() : null,
121-
explorerFrame_.isWithinVesselSelected());
138+
explorerFrame_.isWithinVesselSelected(),
139+
(Integer) edgeMarginSpinner_.getValue());
122140
});
123141
panel.add(startButton_, "wrap");
124142

143+
panel.add(new JLabel("Exclude edge margin (tiles):"), "split 2");
144+
panel.add(edgeMarginSpinner_, "wrap");
145+
125146
cancelButton_ = new JButton("Cancel");
126147
cancelButton_.setToolTipText("Cancel the running automatic Refine Z");
127148
cancelButton_.addActionListener(e -> explorerManager_.cancelRefineZ());
@@ -155,8 +176,11 @@ public RefineZFrame(Studio studio, ExplorerManager explorerManager,
155176

156177
add(panel, "growx, wrap");
157178

158-
// Populate autofocus methods and select the current interpolation method.
179+
// Populate autofocus methods. Restore the method saved in the profile when it is still
180+
// available, applying it to the main MM window so the two stay in sync; otherwise fall back
181+
// to whatever method MM currently has active.
159182
refreshAfMethods();
183+
restoreSavedAfMethod();
160184
explorerManager_.setRefineZMethod((ZGenerator.Type) interpCombo_.getSelectedItem());
161185

162186
// Re-sync the AF method from the main window each time this window regains focus, since the
@@ -197,6 +221,25 @@ private void refreshAfMethods() {
197221
}
198222
}
199223

224+
/**
225+
* Selects the autofocus method remembered in the profile (if present and still installed) and
226+
* pushes it to the main MM window. Called once at construction so a remembered choice wins over
227+
* MM's current method; later focus-driven {@link #refreshAfMethods()} calls follow MM.
228+
*/
229+
private void restoreSavedAfMethod() {
230+
String saved = settings_.getString(AF_METHOD, null);
231+
if (saved == null || saved.isEmpty()) {
232+
return;
233+
}
234+
for (int i = 0; i < afCombo_.getItemCount(); i++) {
235+
if (saved.equals(afCombo_.getItemAt(i))) {
236+
// Not syncing: we want this selection to propagate to the AutofocusManager.
237+
afCombo_.setSelectedItem(saved);
238+
return;
239+
}
240+
}
241+
}
242+
200243
/** Notifies whether an automatic Refine-Z run is in progress; switches to EDT. */
201244
public void setRefineZRunning(boolean running) {
202245
SwingUtilities.invokeLater(() -> {
@@ -218,6 +261,7 @@ private void updateEnabled() {
218261
autoRadio_.setEnabled(!running_);
219262
manualRadio_.setEnabled(!running_);
220263
pointsSpinner_.setEnabled(auto && !running_);
264+
edgeMarginSpinner_.setEnabled(auto && !running_);
221265
afCombo_.setEnabled(auto && !running_);
222266
startButton_.setEnabled(auto && !running_);
223267
cancelButton_.setEnabled(auto && running_);

0 commit comments

Comments
 (0)