def test_binning(self): """ Changing the binning shouldn't affect the position binning x2 => same position (so shift in pixel / 2) """ # At pos 0, 0 im0 = self.camera.data.get() # Move a little bit in X,Y => should have a different image shifted by the same amount self.camera.updateMetadata({model.MD_POS: (10e-6, 20e-6)}) im_move1 = self.camera.data.get() # Change to binning 2 self.camera.binning.value = (2, 2) self.camera.updateMetadata({model.MD_PIXEL_SIZE: (2e-6, 2e-6) }) # Normally updated by the mdupdater im_move2 = self.camera.data.get() assert_tuple_almost_equal((0, 0), MeasureShift(im_move1[::2, ::2], im_move2, 10), delta=0.5) assert_tuple_almost_equal((5, -10), MeasureShift(im0[::2, ::2], im_move2, 10), delta=0.5)
def test_different_precisions(self): """ Tests for image drifted by random drift value using different precisions. """ drift = MeasureShift(self.data[0], self.data_random_drifted, 1) numpy.testing.assert_almost_equal(drift, (self.deltac, self.deltar), 0) drift = MeasureShift(self.data[0], self.data_random_drifted, 10) numpy.testing.assert_almost_equal(drift, (self.deltac, self.deltar), 1) drift = MeasureShift(self.data[0], self.data_random_drifted, 100) numpy.testing.assert_almost_equal(drift, (self.deltac, self.deltar), 2) drift = MeasureShift(self.data[0], self.data_random_drifted, 1000) numpy.testing.assert_almost_equal(drift, (self.deltac, self.deltar), 3)
def test_small_random_drift_noisy(self): """ Tests for image drifted by random drift value after noise is added. """ drift = MeasureShift(self.small_data, self.small_data_random_drifted_noisy, 10) numpy.testing.assert_almost_equal( drift, (self.small_deltac, self.small_deltar), 0)
def estimate(self): """ Estimate the additional drift since previous acquisition+estimation. Note: It should be only called once after every acquisition. To read the value again, use .drift. To read the total drift since the first acquisition, use .tot_drift return (float, float): estimated extra drift in X/Y SEM px since last estimation. """ # Calculate the drift between the last two frames and # between the last and first frame if len(self.raw) > 1: # Note: prev_drift and drift, don't represent exactly the same # value as the previous image also had drifted. So we need to # include also the drift of the previous image. # Also, MeasureShift return the shift in image pixels, which is # different (usually bigger) from the SEM px. prev_drift = MeasureShift(self.raw[-2], self.raw[-1], 10) prev_drift = (prev_drift[0] * self._scale[0] + self.drift[0], prev_drift[1] * self._scale[1] + self.drift[1]) orig_drift = MeasureShift(self.raw[0], self.raw[-1], 10) self.drift = (orig_drift[0] * self._scale[0], orig_drift[1] * self._scale[1]) logging.debug("Current drift: %s", self.drift) logging.debug("Previous frame diff: %s", prev_drift) if (abs(self.drift[0] - prev_drift[0]) > 5 * self._scale[0] or abs(self.drift[1] - prev_drift[1]) > 5 * self._scale[1]): # TODO: in such case, add the previous and current image to .raw logging.warning("Drift cannot be measured precisely, " "hesitating between %s and %s px", self.drift, prev_drift) # Update drift since the original position self.tot_drift = (self.tot_drift[0] + self.drift[0], self.tot_drift[1] + self.drift[1]) # Update maximum drift if math.hypot(*self.tot_drift) > math.hypot(*self.max_drift): self.max_drift = self.tot_drift return self.drift
def test_move_around(self): """ Test that moving the "stage" moves the image """ # At pos 0, 0 im0 = self.camera.data.get() self.assertEqual(self.camera.resolution.value[::-1], im0.shape[:2]) im1 = self.camera.data.get() assert_tuple_almost_equal((0, 0), MeasureShift(im0, im1, 10), delta=0.5) # Move a little bit in X,Y => should have a different image shifted by the same amount self.camera.updateMetadata({model.MD_POS: (10e-6, 20e-6)}) im_move1 = self.camera.data.get() # Y is opposite direction in pixels, compared to physical assert_tuple_almost_equal((10, -20), MeasureShift(im0, im_move1, 10), delta=0.5) # Move a little bit => should have a different image self.camera.updateMetadata({model.MD_POS: (100e-6, 200e-6)}) im_move1 = self.camera.data.get() # Note: images are always different, due to synthetic noise test.assert_array_not_equal(im0, im_move1) # Move the opposite direction self.camera.updateMetadata({model.MD_POS: (-100e-6, -200e-6)}) im_move2 = self.camera.data.get() test.assert_array_not_equal(im0, im_move2) test.assert_array_not_equal(im_move1, im_move2) # Move far => should "block" on the border self.camera.updateMetadata({model.MD_POS: (-1000e-6, -2000e-6)}) im_move_f1 = self.camera.data.get() # test.assert_array_not_equal(im0, im_move_f1) # Move even further => no change self.camera.updateMetadata({model.MD_POS: (-10000e-6, -20000e-6)}) im_move_f2 = self.camera.data.get()
def measure_shift(da, db, use_md=True): """ use_md (bool): if False, will not use metadata and assume the 2 images are of the same area return (float, float): shift of the second image compared to the first one, in pixels of the first image. """ da_res = da.shape[1], da.shape[0] # X/Y are inverted db_res = db.shape[1], db.shape[0] if any(sa < sb for sa, sb in zip(da_res, db_res)): logging.warning("Comparing a large image %s to a small image %s, you should do the opposite", db_res, da_res) # if db FoV is smaller than da, crop da if use_md: dafov = [pxs * s for pxs, s in zip(da.metadata[model.MD_PIXEL_SIZE], da_res)] dbfov = [pxs * s for pxs, s in zip(db.metadata[model.MD_PIXEL_SIZE], db_res)] fov_ratio = [fa / fb for fa, fb in zip(dafov, dbfov)] else: fov_ratio = (1, 1) if any(r < 1 for r in fov_ratio): logging.warning("Cannot compare an image with a large FoV %g to a small FoV %g", dbfov, dafov) shift_px = measure_shift(db, da) return [-s for s in shift_px] crop_res = [int(s / r) for s, r in zip(da_res, fov_ratio)] logging.debug("Cropping da to %s", crop_res) da_ctr = [s // 2 for s in da_res] da_lt = [int(c - r // 2) for c, r in zip(da_ctr, crop_res)] da_crop = da[da_lt[1]: da_lt[1] + crop_res[1], da_lt[0]: da_lt[0] + crop_res[0]] scale = [sa / sb for sa, sb in zip(da_crop.shape, db.shape)] if scale[0] != scale[1]: raise ValueError("Comparing images with different zooms levels %s on each axis is not supported" % (scale,)) # Resample the smaller image to fit the resolution of the larger image if scale[0] < 1: # The "big" image has actually less pixels than the small FoV image # => zoom the big image, and compensate later for the shift logging.info("Rescaling large FoV image by scale %f", 1 / scale[0]) da_crop = zoom(da_crop, 1 / scale[0]) db_big = db shift_ratio = scale[0] else: logging.info("Rescaling small FoV image by scale %f", scale[0]) db_big = zoom(db, scale[0]) shift_ratio = 1 # Apply phase correlation shift_px = MeasureShift(da_crop, db_big, 10) return shift_px[0] * shift_ratio, shift_px[1] * shift_ratio
def test_small_identical_inputs_noisy(self): """ Tests for input of identical images after noise is added. """ drift = MeasureShift(self.small_data, self.small_data_noisy, 1) numpy.testing.assert_almost_equal(drift, (0, 0), 0)
def test_small_identical_inputs(self): """ Tests for input of identical images. """ drift = MeasureShift(self.small_data, self.small_data, 1) numpy.testing.assert_almost_equal(drift, (0, 0), 0)
def test_known_drift_noisy(self): """ Tests for image drifted by known drift value after noise is added. """ drift = MeasureShift(self.data[0], self.data_drifted_noisy, 1) numpy.testing.assert_almost_equal(drift, (-3, 5), 1)
def test_random_drift(self): """ Tests for image drifted by random drift value. """ drift = MeasureShift(self.data[0], self.data_random_drifted, 10) numpy.testing.assert_almost_equal(drift, (self.deltac, self.deltar), 1)
def test_known_drift(self): """ Tests for image drifted by known drift value. """ drift = MeasureShift(self.data[0], self.data_drifted[0], 1) numpy.testing.assert_almost_equal(drift, (-3, 5), 1)
def read_timelapse(infn, emfn, fmfn): """ infn (str): pattern for input filename emfn: sem output filename fmfn: fluorescence output filename """ infiles = sorted(glob.glob(infn)) if not infiles: raise ValueError("No file fitting '%s'" % (infn, )) emdata = {} # timestamp -> tuple of info (X/Y, overlay X/Y) fmdata = {} # timestamp -> tuple of info (X/Y) emda_prev = emda0 = fmda_prev = fmda0 = None for i, infl in enumerate(infiles): logging.info("Processing %s (%d/%d)", infl, i + 1, len(infiles)) try: # Read the file reader = dataio.find_fittest_converter(infl) das = reader.read_data(infl) # Read the metadata (we expect one fluo image and one SEM image) fmpos = empos = emda = fmda = fmfoc = emfoc = None for da in das: if model.MD_IN_WL in da.metadata: # Fluo image fmpos = da.metadata[ model. MD_POS] # this one has the overlay translation included fmdate = da.metadata[model.MD_ACQ_DATE] fmda = da fmpxs = da.metadata[model.MD_PIXEL_SIZE] fmfoc = autofocus.MeasureOpticalFocus(da) else: # SEM empos = da.metadata[model.MD_POS] emdate = da.metadata[model.MD_ACQ_DATE] emda = da empxs = da.metadata[model.MD_PIXEL_SIZE] emfoc = autofocus.MeasureSEMFocus(da) # Overlay translation ovlpos = fmpos[0] - empos[0], fmpos[1] - empos[1] # Compute drift from first image and previous image if i == 0: emda0 = emda fmda0 = fmda emdriftm = 0, 0 empdriftm = 0, 0 fmdriftm = 0, 0 fmpdriftm = 0, 0 else: emdrift = MeasureShift(emda0, emda, 10) # in pixels emdriftm = emdrift[0] * empxs[0], emdrift[1] * empxs[1] logging.info("Computed total EM drift of %s px = %s m", emdrift, emdriftm) empdrift = MeasureShift(emda_prev, emda, 10) # in pixels empdriftm = empdrift[0] * empxs[0], empdrift[1] * empxs[1] logging.info("Computed previous EM drift of %s px = %s m", empdrift, empdriftm) fmdrift = MeasureShift(fmda0, fmda, 10) # in pixels fmdriftm = fmdrift[0] * fmpxs[0], fmdrift[1] * fmpxs[1] logging.info("Computed total FM drift of %s px = %s m", fmdrift, fmdriftm) fmpdrift = MeasureShift(fmda_prev, fmda, 10) # in pixels fmpdriftm = fmpdrift[0] * fmpxs[0], fmpdrift[1] * fmpxs[1] logging.info("Computed previous FM drift of %s px = %s m", fmpdrift, fmpdriftm) emdata[emdate] = (empos[0], empos[1], ovlpos[0], ovlpos[1], emdriftm[0], emdriftm[1], empdriftm[0], empdriftm[1], emfoc) fmdata[fmdate] = (fmpos[0], fmpos[1], fmdriftm[0], fmdriftm[1], fmpdriftm[0], fmpdriftm[1], fmfoc) emda_prev = emda fmda_prev = fmda except KeyboardInterrupt: logging.info("Closing after only %d images processed", i) return except Exception: logging.exception("Failed to read %s", infl) # export the data logging.info("Exporting the data...") export_csv( emdata, emfn, "timestamp, posx, posy, overlay x, overlay y, total drift x, total drift y, prev drift x, prev drift y, focus level\n" ) export_csv( fmdata, fmfn, "timestamp, posx, posy, total drift x, total drift y, prev drift x, prev drift y, focus level\n" ) return 0