@@ -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.\n Refine 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 );
0 commit comments