From f781e6b674b7f569ac092f0d015b98d14709af7a Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Sun, 19 Oct 2025 21:29:11 +0100 Subject: [PATCH] Image intermediate locations between sample and background --- .../scan_planners.py | 97 +++++++++++++++--- tests/test_scan_planners.py | 14 +-- tests/utilities/example_smart_spiral_core.pkl | Bin 4139 -> 10004 bytes .../utilities/example_smart_spiral_lobed.pkl | Bin 8295 -> 18596 bytes .../example_smart_spiral_regular.pkl | Bin 5877 -> 12935 bytes tests/utilities/scan_test_helpers.py | 2 + 6 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index f3ace6f1..6f04fa95 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -341,6 +341,19 @@ class SmartSpiral(ScanPlanner): super().__init__(initial_position, planner_settings) self._distance_cutoff: float = max([self._dx, self._dy]) * 1.1 + def _is_primary_location(self, location: FutureScanLocation) -> bool: + """Return True if input is a primary location not a secondary (intermediate) location.""" + return location.planner_data["primary"] + + @property + def secondary_locations(self) -> XYZPosList: + """A list of all secondary (intermediate) locations.""" + return [ + loc.xyz_tuple + for loc in self._path_history + if not self._is_primary_location(loc) + ] + def _parse(self, planner_settings: Optional[dict] = None) -> None: """Parse SmartSpiral Settings dictionary. @@ -366,7 +379,7 @@ class SmartSpiral(ScanPlanner): For smart spiral this is just the first point """ - return [FutureScanLocation(self._initial_position)] + return [FutureScanLocation(self._initial_position, primary=True)] def mark_location_visited( self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True @@ -381,8 +394,11 @@ class SmartSpiral(ScanPlanner): super().mark_location_visited(xyz_pos, imaged, focused) xy_pos = enforce_xy_tuple(xyz_pos[:2]) - if imaged: - self._add_surrounding_positions(xy_pos) + if self._is_primary_location(self._path_history[-1]): + if imaged: + self._add_surrounding_positions(xy_pos) + else: + self._add_intermediate_positions(xy_pos) self._re_sort_remaining_locations(xy_pos) def _add_surrounding_positions(self, xy_pos: XYPos) -> None: @@ -394,24 +410,81 @@ class SmartSpiral(ScanPlanner): * too far away * already planned * already visited + + If the already visited position was not imaged then an intermediate location is + added. See also self._add_intermediate_positions() for adding intermediate + locations after visiting a location that was not imaged. """ - new_positions = [ - FutureScanLocation((xy_pos[0] - self._dx, xy_pos[1])), - FutureScanLocation((xy_pos[0] + self._dx, xy_pos[1])), - FutureScanLocation((xy_pos[0], xy_pos[1] - self._dy)), - FutureScanLocation((xy_pos[0], xy_pos[1] + self._dy)), - ] + new_positions = self._adjacent_positions(xy_pos) for new_pos in new_positions: - # Skip position if already planned or visited - if self.position_planned(new_pos) or self.position_visited(new_pos): + # Skip position if already planned + if self.position_planned(new_pos): + continue + if self.position_visited(new_pos): + # Get the VisitedScanLocation object if already visited + visited = self._path_history[self._path_history.index(new_pos)] + if visited.imaged: + # If this adjacent position was imaged succsfully already then skip. + continue + # If it wasn't imaged add an intimediate location between the + # last imaged position and this one. + i_pos = self._intermediate_position(xy_pos, new_pos) + # Set primary=False surrounding images are not added once imaged. + i_loc = FutureScanLocation(i_pos, primary=False) + # Append instantly without checking max_distance as this is between + # imaged points. + self._remaining_locations.append(i_loc) continue dist = distance_between(new_pos, self._initial_position) if dist > self._max_dist: LOGGER.debug("Rejected moving to %s as it is out of range", new_pos) continue - self._remaining_locations.append(new_pos) + new_loc = FutureScanLocation(new_pos, primary=True) + self._remaining_locations.append(new_loc) + + def _add_intermediate_positions(self, xy_pos: XYPos) -> None: + """Add intermediate points after locating a background location. + + This is called after an image is recorded that was background. Intermediate + locations are added between any adjacent locations that were successfully + imaged. + + Note that in the case that an imaged location has an adjacent background image + then adding the intermediate image will be handled by + _add_surrounding_positions(). + """ + surrounding_positions = self._adjacent_positions(xy_pos) + + for surr_pos in surrounding_positions: + if self.position_visited(surr_pos): + # Get the VisitedScanLocation object if already visited + visited = self._path_history[self._path_history.index(surr_pos)] + if not visited.imaged: + # If it wasn't imaged then skip this position + continue + # If surrpounding location was imaged add an intimediate location + # between the most recent position and this imaged position. + i_pos = self._intermediate_position(xy_pos, surr_pos) + # Set primary=False surrounding images are not added once imaged. + i_loc = FutureScanLocation(i_pos, primary=False) + # Append instantly without checking max_distance as this is between + # imaged points. + self._remaining_locations.append(i_loc) + + def _adjacent_positions(self, xy_pos: XYPos) -> XYPosList: + """Return 4 points +/-dx and +/-dy from the input location.""" + return [ + (xy_pos[0] - self._dx, xy_pos[1]), + (xy_pos[0] + self._dx, xy_pos[1]), + (xy_pos[0], xy_pos[1] - self._dy), + (xy_pos[0], xy_pos[1] + self._dy), + ] + + def _intermediate_position(self, xy_pos1: XYPos, xy_pos2: XYPos) -> XYPos: + """Return an (x,y) position halfway between two input positions.""" + return tuple((i + j) // 2 for i, j in zip(xy_pos1, xy_pos2, strict=True)) def _re_sort_remaining_locations(self, current_pos: XYPos) -> None: """Sort the remaining positions based on the current location.""" diff --git a/tests/test_scan_planners.py b/tests/test_scan_planners.py index 940257d0..f18d6c81 100644 --- a/tests/test_scan_planners.py +++ b/tests/test_scan_planners.py @@ -302,14 +302,6 @@ def test_example_smart_spiral(): expected_planner = ( scan_test_helpers.get_expected_result_for_example_smart_spiral(sample_name) ) - # Use hasattr to check if the saved test data is the old object where imaged - # locations were saved directly as a list of tuples rather than being generated - # from a history of VisitedScanLocation objects. - # This "if" section of this if-else block can be deleted once we are confident - # of the new format and the pickles are updated - if hasattr(expected_planner, "_imaged_locations"): - assert planner.path_history == expected_planner._path_history - assert planner.imaged_locations == expected_planner._imaged_locations - else: - assert planner.path_history == expected_planner.path_history - assert planner.imaged_locations == expected_planner.imaged_locations + + assert planner.path_history == expected_planner.path_history + assert planner.imaged_locations == expected_planner.imaged_locations diff --git a/tests/utilities/example_smart_spiral_core.pkl b/tests/utilities/example_smart_spiral_core.pkl index 5b7ba2de52d49d0611a2eae688f1d446428e50e6..b3b47db8adccf808706d79aa6eee7f0849f6b4a7 100644 GIT binary patch literal 10004 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 D4qSL} diff --git a/tests/utilities/example_smart_spiral_lobed.pkl b/tests/utilities/example_smart_spiral_lobed.pkl index a935e1620650c6113a53c3ddd9bd23f840ef4784..21bcf3e1bbab6bcc69da41d7198655632dbeebb7 100644 GIT binary patch literal 18596 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 literal 8295 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 diff --git a/tests/utilities/example_smart_spiral_regular.pkl b/tests/utilities/example_smart_spiral_regular.pkl index ddf784c8d469a1f5cb2d4bddb6d5d65e58aab204..aa9b847b85d4bdca9608b2339298485e0039b300 100644 GIT binary patch literal 12935 zcmZo*omykW00uqU`30$YX*sDCrA4XnxtYmD`Nhfk1*!4HsYPX}MS8`_iFxq_If;3B zsYS(8dbopg6N^fM3o?rmbEZt5qS-j5c8W%iV0>m?W=UpZPJBUrab`(oe%=&s25*M8 zDLu^bDHT(EH?V*hl~a5bn0q+ma}z7#Q!{LA{`l`9T{S^Q!*q_6?p~x{{Np?b)ax*pF+SeUj=6TR(dlK zRN}jVg%~x2+~d1}g$S>KR2}$9D4@IWGL%kUB!4;RRAe zFjyg~9N6&3A=sn7gj9Jm5L5z+X+p;MCU6oqCxMfo*L)K=35Ahw0w*ykCV`WP$OD-L z3Ud4*2}*s0Re3Xj;|0G_;7B1x9mqan)P4B)|3A^{h)DHd^N2{zAa#Ud$_u23V3b2t z5lUbXRiM;?w{!yq8#oQ&RRted+M?~@j+1T)zQ27qZtwgBWz~V(%l{W(siXgd_uwh^y5@Q}H zLlC3R;qCwbM5_a(c><*l*gPU~KUf_RxgVsCP%82QDI%C0A*u-Gey~}DvJu3r1|9-| z0g41)f~r8FN>~-RBq3y$Hv>U~KqUzwV?ZSdK{Wz=gzY*YM3fpr!R>oMh;WH?K!{)x z@I4?zC@uLO5F(~TIv_+$iF7~+oL}*$5|E9AGd@UF!UuxomH?j@5vme6iBYwIg$PyN z3`8h`lx~C_1`aJ^%mZaCV$^{$GcoEWy!ij0Xmx}m%L{BCC|~1`dWbqAN=vYLM3j~w zb%atl*iwR71)_>zX$i8BV1oi;7NG(KqG|y@fgl4#sV_lQpaeo#6)1)Xs{)t2glzO? zAZQS%WKcD;ch zB=`}k(4rHR%)u>UkXeXCiIzA(i5H71Y-T+Wh1iJGEKml-ViuZ>pzKUo6)1fXRt2uR zVc~_#UET}?4Fc8Ogp2{z-2~ML@DaA_fDmCd1>%I&D9FIoAQAy;VEdksgsVXW4{GH4 zD##FSCn(7H5=wBs7Zl*;;B-%g92Pa0?je+&KyD_KF+pkwwn-FZ2o@H;3NnOB9bW|* zV%j7MGQ_k=6lB0PBmTSwvXO964N^t8!U3rQWnGAkIHOKM#*1*JQx%$7p!rU?9}za9xeGLpPEZwS zy#YZ*kTQ^PP(aE+BGiHN5)tZVya%mwz~VmC00B=0dJzs@h&sXn4DkmM=7IA%!aPJ& zpt%p6*NHF>td6jGpk)mN(~>s>Qc}X@GEl)m*j*r1gk1$HUWhO&fs+__Dac^0gV2)r z2}!J~P($DMf&$W*G0q~PLJq4cOtTQpHGF1aQ-x_3C;>qV4t!=|Q-x_3qKA&pEQBhY zY2tzcQX?3jS=jpe8190#x$zhTQ-a4u@YX#ZW8iA=nFH%d;4=rN2A?^wUJE{RFx6m$ z4t(AXpElfX$>%?1{NYP=Z;DgjqJgjIoZ2Vqs9 zDu=KtP$QH`RSc4ljEhJlsD%@_vhgLT3ha7oS literal 5877 zcmZo*o%%|Q0StPy^9xe*(sEKON{dqCb2F2R@{5!63sU2YQ;W({i}Z?<6Z7H=auW0M zQj3bG^l%5~CKi?oF{JbgN4BiZF zQ+k-=Q!1wTZeRg1DyR4=F!ylA=O$Lfr(_nFOz~RK#>mhk5?_>>o0yrGnU@})lb@Uj z(q23zb_&SU+{EE?acsXfZPQU@=f4`i6wACwfiP;Ld^6{;6%7JffMcskT}RXkoh1#z}VglV4Fe0 zU~3UPkO~Cv!^i*s(RgsbgXQ4<1Mwhk@d7cS4ur5F9)PexE^eO!HWTc6SbRXly&0f_ zAb&%IK>miZ1o&XG0(`Kr5a5Gapv+ zHZ*=fN?`E=WGe8AF@dgnB#T%3*zz35( zAOvF-h$FEe4)84yhdK+yf;bk$f)s(i1>z8Y`4)&H;<7*-5tjwx;LrxSAEX8rcpx?` zIzViY*T8BH2!WF)m@UAEU?*@Q*c({jY;OiQ6B5TTgTS^T;g;$V55S%0sJdipB59E3T59EG0&zk|x1l8*><3M#8oGrkIP;)>C z!7dO-un!1%F+7Jj1ym=(*$JEo@eM3+wl@Qu3HB?32X-q=J2(yya-i5j@W6fqyD%Mj0y$dJsC%8<^G$&k&E%aG4d$WY8s%23Ww$xzKu%TUkI$k5Et%Fxcx z$$}r9_$uP|@%P`Nd$gs??%COF`$*|3^%dpRI$Z*VX%5ctb$#BhZ z%W%)|$nebY%J9zc$?(na%ka+#$Oy~`$_UN~$q3B|%Lvbi$cW5{%81U0$%xH}%ZSfN z$Vkjc%1F*g$wh$;i#f%gE0t$SBMx$|%k#$tcYz%P7yN$f(Sy z%Baq$$*9e!%c#$2$Y{)H%4p7L$!N`J%V^K&$mq=I%IMDM$>`1K%jnOTkTEf1QpV(r zDH&5Ure#den2|9vV^+rOj5!%|Gv;N?&sdPLFk?~1;*2F3OEZ>bEYDbxu`**-#_EhU z8EZ4vWvtKGkg+jiQ^w|uEg4%gwqCFP9M3qBaWdmn#_5bR8D}%jWt`8rka02NQpV+sD;ZZau4P=$xRG%)<5tG)j5`^3 zGwx;F&v=mWFym3ig31k$Fo*`b22^ddO#x+CsB56jc8D0r z4zOmh(O?%sgdi@2vOyG70VKzGG8zYWHN*W4jqsfxOoi;9S|23I$&A=)Ua-w;tOt8LuhcQft%D2 zJ~*^MH9D9J3oU3<8X^X=5W<3Z(D#54xIGIAUr^Nx2|*|e+I$9wAVdfff>1WdG^loP zl*8B{@59*ODhjF|%!XJDZp*;Z8(16?r4Vsw$b!VdAq!$aLKe&dhb)){3IebeNGO8E z7VtwO%NNW7r%Esj@U=5%qE?B%k9BdJY53(9W zgB$~*!J!Xo0Jlv6HDMuM0I?t*1F=93fXG4u0VDdF8{!;rEegvIU^Nh@L&PB-g0Lap1hc_0 z1yT%&DKHBZIABRg9srAhk~2gM6r>Q=0im`jkaz^MK+g3Avp}AJus|+=u)whgk_9(v z!GQvn0L30i7*ukA=>wp~EI7a+@(MC=8e}!7!3uH=hz7?dxGCE<1;hu(CaCoV=ECw4 zv`~UxgL)r_>0;LxS3ltj=7ASroEKsTe zvp`V@WnN3dF(9Y;Y{WSOR=77PL(du>;gp2gM3h5*#lu zHrS021>ksqiGyMX#s>Qpss@zWpiTg%MsV=}tutY4u$dr*u)+_*hPLq`Zh{Db{0(J6 tTlWzEAjA$l0JUa7>Y%I*EM5>6XlxcN3Goj^5FBk#1rYy0#8XR?^Z>L3qk#Yb diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index cec99249..675e36dd 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -62,6 +62,7 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi ax.add_artist(sample.patch) xh, yh = zip(*planner.path_history, strict=True) xi, yi, _zi = zip(*planner.imaged_locations, strict=True) + xs, ys, _zs = zip(*planner.secondary_locations, strict=True) # convert history to numpy array so can calculate quiver arrows xh = np.array(xh) @@ -78,6 +79,7 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi ) plt.plot(xh, yh, "r.") plt.plot(xi, yi, "g*") + plt.plot(xs, ys, "o", mfc="none", mec="blue") ax.axis("equal") return fig