def test_find_center_big(self): """ Test FindCenterCoordinates on large data """ # Note: it's not very clear why, but the center is not exactly the same # as with the original data. expected_coordinates = [(-0.0003367114224783442, -0.022941682748052378), (0.42179351215760619, -0.25668360673638801), (0.054153514028894206, -0.046475569488448026), (0.15117193581594143, 0.20813363301021551), (0.1963834856403108, -0.18329597166583256), (0.23159684275306583, 1.3670166271550004), (-1.3363782613242998, 0.20192181693837058), (-0.14978764151902624, 0.66067572281822606), (-0.058984235874285897, 0.13071737132569164), (0.021009283646695891, -0.007037802630523865)] for i in range(10): data = hdf5.read_data( os.path.join(TEST_IMAGE_PATH, "image" + str(i + 1) + ".h5"))[0] data.shape = data.shape[-2:] Y, X = data.shape databig = numpy.zeros((200 + Y, 200 + X), data.dtype) databig += numpy.min(data) # We put it right at the center, so shouldn't change expected coordinates databig[100:100 + Y:, 100:100 + X] = data spot_coordinates = spot.FindCenterCoordinates(databig) numpy.testing.assert_almost_equal(spot_coordinates, expected_coordinates[i], 3)
def test_sanity(self): """ Create an image consisting of all zeros and a single pixel with value one. FindCenterCoordinates should return the coordinates of this one pixel. """ for n in range(5, 12): for m in range(5, 12): for i in range(2, n - 2): for j in range(2, m - 2): img = numpy.zeros((n, m)) img[i, j] = 1 xc, yc = spot.FindCenterCoordinates(img) self.assertAlmostEqual(j, xc + 0.5 * (m - 1)) self.assertAlmostEqual(i, yc + 0.5 * (n - 1))
def test_find_center_syn(self): """ Test FindCenterCoordinates on synthetic data """ offsets = [ (0, 0), (-1, -1), (3, 2), ] for ofs in offsets: data = numpy.zeros((201, 201), numpy.uint16) # Just one point, so it should be easy to find data[100 + ofs[1], 100 + ofs[0]] = 500 spot_coordinates = spot.FindCenterCoordinates(data) numpy.testing.assert_almost_equal(spot_coordinates, ofs, 3)
def test_divide_and_find_center_grid(self): """ Test DivideInNeighborhoods combined with FindCenterCoordinates """ grid_data = hdf5.read_data("grid_10x10.h5") C, T, Z, Y, X = grid_data[0].shape grid_data[0].shape = Y, X subimages, subimage_coordinates = coordinates.DivideInNeighborhoods( grid_data[0], (10, 10), 40) spot_coordinates = [spot.FindCenterCoordinates(i) for i in subimages] optical_coordinates = coordinates.ReconstructCoordinates( subimage_coordinates, spot_coordinates) self.assertEqual(len(subimages), 100)
def test_divide_and_find_center_grid_missing_point(self): """ Test DivideInNeighborhoods combined with FindCenterCoordinates for grid that misses one point """ grid_data = hdf5.read_data("grid_missing_point.h5") C, T, Z, Y, X = grid_data[0].shape grid_data[0].shape = Y, X # Add Gaussian noise noise = random.normal(0, 40, grid_data[0].size) noise_array = noise.reshape(grid_data[0].shape[0], grid_data[0].shape[1]) noisy_grid_data = grid_data[0] + noise_array subimages, subimage_coordinates = coordinates.DivideInNeighborhoods( noisy_grid_data, (10, 10), 40) spot_coordinates = [spot.FindCenterCoordinates(i) for i in subimages] optical_coordinates = coordinates.ReconstructCoordinates( subimage_coordinates, spot_coordinates) self.assertEqual(len(subimages), 99)
def test_find_center(self): """ Test FindCenterCoordinates """ expected_coordinates = [(-0.00019439548586790034, -0.023174120210179554), (0.47957790544657719, -0.82786251901339769), (0.05418032832973009, -0.046573726263258203), (0.15117173005078957, 0.20813259555303279), (-0.16400161706684502, 0.12399078936095265), (0.21457123595635252, 1.682698104874774), (-1.3480442345004007, 0.19789183664083154), (-0.13424061744712734, 0.73739434108133217), (-0.063230444692135013, 0.14718269387805094), (0.020941736978718473, -0.0071056828496776324)] for i in range(10): data = hdf5.read_data( os.path.join(TEST_IMAGE_PATH, "image" + str(i + 1) + ".h5"))[0] C, T, Z, Y, X = data.shape data.shape = Y, X spot_coordinates = spot.FindCenterCoordinates(data) numpy.testing.assert_almost_equal(spot_coordinates, expected_coordinates[i], 3)
def test_find_center(self): """ Test FindCenterCoordinates """ expected_coordinates = [(-0.00019439548586790034, -0.023174120210179554), (0.41813787193469681, -0.77556146879261101), (0.05418032832973009, -0.046573726263258203), (0.15117173005078957, 0.20813259555303279), (0.15372338817998937, -0.071307409462406962), (0.22214464176322843, 1.5448851668913044), (-1.3567379189595801, 0.20634334863259929), (-0.068717256379618827, 0.76902400758882417), (-0.064496044288789064, 0.14000630665134439), (0.020941736978718473, -0.0071056828496776324)] for i in range(10): data = hdf5.read_data( os.path.join(TEST_IMAGE_PATH, "image" + str(i + 1) + ".h5"))[0] C, T, Z, Y, X = data.shape data.shape = Y, X spot_coordinates = spot.FindCenterCoordinates(data) numpy.testing.assert_almost_equal(spot_coordinates, expected_coordinates[i], 3)
def _DoFindOverlay(future, repetitions, dwell_time, max_allowed_diff, escan, ccd, detector, skew=False): """ Scans a spots grid using the e-beam and captures the CCD image, isolates the spots in the CCD image and finds the coordinates of their centers, matches the coordinates of the spots in the CCD image to those of SEM image and calculates the transformation values from optical to electron image (i.e. ScanGrid-> DivideInNeighborhoods->FindCenterCoordinates-> ReconstructCoordinates->MatchCoordinates-> CalculateTransform). In case matching the coordinates is infeasible, it automatically repeats grid scan -and thus all steps until matching- with different parameters. future (model.ProgressiveFuture): Progressive future provided by the wrapper repetitions (tuple of ints): The number of CL spots are used dwell_time (float): Time to scan each spot (in s) max_allowed_diff (float): Maximum allowed difference (in m) between the spot coordinates and the estimated spot position based on the computed transformation (in m). If no transformation can be found to fit this limit, the procedure will fail. escan (model.Emitter): The e-beam scanner ccd (model.DigitalCamera): The CCD detector (model.Detector): The electron detector skew (boolean): If True, also compute skew returns tuple: Transformation parameters translation (Tuple of 2 floats) scaling (Float) rotation (Float) dict : Transformation metadata raises: CancelledError if cancelled ValueError if procedure failed """ # TODO: drop the "skew" argument (to always True) once we are convinced it # works fine # TODO: take the limits of the acceptable values for the metadata, and raise # an error when the data is not within range (or retry) logging.debug("Starting Overlay...") try: _set_blanker(escan, False) # Repeat until we can find overlay (matching coordinates is feasible) for trial in range(MAX_TRIALS_NUMBER): logging.debug("Trying with dwell time = %g s...", future._gscanner.dwell_time) # For making a report when a failure happens report = OrderedDict() # Description (str) -> value (str()'able) optical_image = None report["Grid size"] = repetitions report["SEM magnification"] = escan.magnification.value report["SEM pixel size"] = escan.pixelSize.value report["SEM FoV"] = tuple( s * p for s, p in zip(escan.shape, escan.pixelSize.value)) report["Maximum difference allowed"] = max_allowed_diff report["Dwell time"] = dwell_time subimages = [] try: # Grid scan if future._find_overlay_state == CANCELLED: raise CancelledError() # Update progress of the future (it may be the second trial) future.set_progress(end=time.time() + estimateOverlayTime( future._gscanner.dwell_time, repetitions)) # Wait for ScanGrid to finish optical_image, electron_coordinates, electron_scale = future._gscanner.DoAcquisition( ) report["Spots coordinates in SEM ref"] = electron_coordinates if future._find_overlay_state == CANCELLED: raise CancelledError() # Update remaining time to 6secs (hardcoded estimation) future.set_progress(end=time.time() + 6) # Check if ScanGrid gave one image or list of images # If it is a list, follow the "one image per spot" procedure logging.debug("Isolating spots...") if isinstance(optical_image, list): report["Acquisition method"] = "One image per spot" opxs = optical_image[0].metadata[model.MD_PIXEL_SIZE] opt_img_shape = optical_image[0].shape subimage_coordinates = [] for oimg in optical_image: subspots, subspot_coordinates = coordinates.DivideInNeighborhoods( oimg, (1, 1), oimg.shape[0] / 2) subimages.append(subspots[0]) subimage_coordinates.append(subspot_coordinates[0]) else: report["Acquisition method"] = "Whole image" # Distance between spots in the optical image (in optical pixels) opxs = optical_image.metadata[model.MD_PIXEL_SIZE] optical_dist = escan.pixelSize.value[0] * electron_scale[ 0] / opxs[0] opt_img_shape = optical_image.shape # Isolate spots if future._find_overlay_state == CANCELLED: raise CancelledError() subimages, subimage_coordinates = coordinates.DivideInNeighborhoods( optical_image, repetitions, optical_dist) if not subimages: raise OverlayError( "Overlay failure: failed to partition image") report["Optical pixel size"] = opxs report["Optical FoV"] = tuple( s * p for s, p in zip(opt_img_shape[::-1], opxs)) report[ "Coordinates of partitioned optical images"] = subimage_coordinates if max_allowed_diff < opxs[0] * 4: logging.warning( "The maximum distance is very small compared to the optical pixel size: " "%g m vs %g m", max_allowed_diff, opxs[0]) # Find the centers of the spots if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Finding spot centers with %d subimages...", len(subimages)) spot_coordinates = [ spot.FindCenterCoordinates(i) for i in subimages ] # Reconstruct the optical coordinates if future._find_overlay_state == CANCELLED: raise CancelledError() optical_coordinates = coordinates.ReconstructCoordinates( subimage_coordinates, spot_coordinates) # Check if SEM calibration is correct. If this is not the case # generate a warning message and provide the ratio of X/Y scale. ratio = _computeGridRatio(optical_coordinates, repetitions) report["SEM X/Y ratio"] = ratio if not (0.9 < ratio < 1.1): logging.warning( "SEM may needs calibration. X/Y ratio is %f.", ratio) else: logging.info("SEM X/Y ratio is %f.", ratio) opt_offset = (opt_img_shape[1] / 2, opt_img_shape[0] / 2) optical_coordinates = [(x - opt_offset[0], y - opt_offset[1]) for x, y in optical_coordinates] report[ "Spots coordinates in Optical ref"] = optical_coordinates # Estimate the scale by measuring the distance between the closest # two spots in optical and electron coordinates. # * For electrons, it's easy as we've placed them. # * For optical, we pick one spot, and measure the distance to the # closest spot. p1 = optical_coordinates[0] def dist_to_p1(p): return math.hypot(p1[0] - p[0], p1[1] - p[1]) optical_dist = min( dist_to_p1(p) for p in optical_coordinates[1:]) scale = electron_scale[0] / optical_dist report["Estimated scale"] = scale # max_allowed_diff in pixels max_allowed_diff_px = max_allowed_diff / escan.pixelSize.value[ 0] # Match the electron to optical coordinates if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Matching coordinates...") try: known_ec, known_oc, max_diff = coordinates.MatchCoordinates( optical_coordinates, electron_coordinates, scale, max_allowed_diff_px) except LookupError as exp: raise OverlayError( "Failed to match SEM and optical coordinates: %s" % (exp, )) report["Matched coordinates in SEM ref"] = known_ec report["Matched coordinates in Optical ref"] = known_oc report["Maximum distance between matches"] = max_diff # Calculate transformation parameters if future._find_overlay_state == CANCELLED: raise CancelledError() # We are almost done... about 1 s left future.set_progress(end=time.time() + 1) logging.debug("Calculating transformation...") try: ret = transform.CalculateTransform(known_ec, known_oc, skew) except ValueError as exp: raise OverlayError( "Failed to calculate transformation: %s" % (exp, )) if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Calculating transform metadata...") if skew is True: transform_d, skew_d = _transformMetadata( optical_image, ret, escan, ccd, skew) transform_data = (transform_d, skew_d) else: transform_d = _transformMetadata( optical_image, ret, escan, ccd, skew ) # Also indicate which dwell time eventually worked transform_data = transform_d transform_d[model.MD_DWELL_TIME] = dwell_time # Everything went fine # _MakeReport("No problem", report, optical_image, subimages) # DEBUG logging.debug("Overlay done.") return ret, transform_data except OverlayError as exp: # Make failure report _MakeReport(str(exp), report, optical_image, subimages) # Maybe it's just due to a bad SNR => retry with longer dwell time future._gscanner.dwell_time = future._gscanner.dwell_time * 1.2 + 0.1 else: raise ValueError("Overlay failure after %d attempts" % (MAX_TRIALS_NUMBER, )) except CancelledError: pass except Exception as exp: logging.debug("Finding overlay failed", exc_info=1) raise exp finally: _set_blanker(escan, True) with future._overlay_lock: future._done.set() if future._find_overlay_state == CANCELLED: raise CancelledError() future._find_overlay_state = FINISHED
def _DoFindOverlay(future, repetitions, dwell_time, max_allowed_diff, escan, ccd, detector, skew=False): """ Scans a spots grid using the e-beam and captures the CCD image, isolates the spots in the CCD image and finds the coordinates of their centers, matches the coordinates of the spots in the CCD image to those of SEM image and calculates the transformation values from optical to electron image (i.e. ScanGrid-> DivideInNeighborhoods->FindCenterCoordinates-> ReconstructCoordinates->MatchCoordinates-> CalculateTransform). In case matching the coordinates is infeasible, it automatically repeats grid scan -and thus all steps until matching- with different parameters. future (model.ProgressiveFuture): Progressive future provided by the wrapper repetitions (tuple of ints): The number of CL spots are used dwell_time (float): Time to scan each spot (in s) max_allowed_diff (float): Maximum allowed difference in electron coordinates #m escan (model.Emitter): The e-beam scanner ccd (model.DigitalCamera): The CCD detector (model.Detector): The electron detector skew (boolean): If True, also compute skew returns tuple: Transformation parameters translation (Tuple of 2 floats) scaling (Float) rotation (Float) dict : Transformation metadata raises: CancelledError if cancelled ValueError if procedure failed """ # TODO: drop the "skew" argument (to always True) once we are convinced it # works fine logging.debug("Starting Overlay...") try: # Repeat until we can find overlay (matching coordinates is feasible) for trial in range(MAX_TRIALS_NUMBER): # Grid scan if future._find_overlay_state == CANCELLED: raise CancelledError() # Update progress of the future (it may be the second trial) future.set_progress( end=time.time() + estimateOverlayTime(future._scanner.dwell_time, repetitions)) # Wait for ScanGrid to finish optical_image, electron_coordinates, electron_scale = future._scanner.DoAcquisition( ) if future._find_overlay_state == CANCELLED: raise CancelledError() # Update remaining time to 6secs (hardcoded estimation) future.set_progress(end=time.time() + 6) # Check if ScanGrid gave one image or list of images # If it is a list, follow the "one image per spot" procedure logging.debug("Isolating spots...") if isinstance(optical_image, list): opt_img_shape = optical_image[0].shape subimages = [] subimage_coordinates = [] for img in optical_image: subspots, subspot_coordinates = coordinates.DivideInNeighborhoods( img, (1, 1), img.shape[0] / 2) subimages.append(subspots[0]) subimage_coordinates.append(subspot_coordinates[0]) else: # Distance between spots in the optical image (in optical pixels) optical_dist = escan.pixelSize.value[0] * electron_scale[ 0] / optical_image.metadata[model.MD_PIXEL_SIZE][0] opt_img_shape = optical_image.shape # Isolate spots if future._find_overlay_state == CANCELLED: raise CancelledError() subimages, subimage_coordinates = coordinates.DivideInNeighborhoods( optical_image, repetitions, optical_dist) if not subimages: raise ValueError("Overlay failure") # Find the centers of the spots if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Finding spot centers with %d subimages...", len(subimages)) spot_coordinates = [ spot.FindCenterCoordinates(i) for i in subimages ] # Reconstruct the optical coordinates if future._find_overlay_state == CANCELLED: raise CancelledError() optical_coordinates = coordinates.ReconstructCoordinates( subimage_coordinates, spot_coordinates) # Check if SEM calibration is correct. If this is not the case # generate a warning message and provide the ratio of X/Y scale. ratio = _computeGridRatio(optical_coordinates, repetitions) if not (0.9 < ratio < 1.1): logging.warning("SEM may needs calibration. X/Y ratio is %f.", ratio) else: logging.info("SEM X/Y ratio is %f.", ratio) opt_offset = (opt_img_shape[1] / 2, opt_img_shape[0] / 2) optical_coordinates = [(x - opt_offset[0], y - opt_offset[1]) for x, y in optical_coordinates] # Estimate the scale by measuring the distance between the closest # two spots in optical and electron coordinates. # * For electrons, it's easy as we've placed them. # * For optical, we pick one spot, and measure the distance to the # closest spot. p1 = optical_coordinates[0] def dist_to_p1(p): return math.hypot(p1[0] - p[0], p1[1] - p[1]) optical_dist = min(dist_to_p1(p) for p in optical_coordinates[1:]) scale = electron_scale[0] / optical_dist # max_allowed_diff in pixels max_allowed_diff_px = max_allowed_diff / escan.pixelSize.value[0] # Match the electron to optical coordinates if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Matching coordinates...") known_ec, known_oc = coordinates.MatchCoordinates( optical_coordinates, electron_coordinates, scale, max_allowed_diff_px) if known_ec: break else: if trial < MAX_TRIALS_NUMBER - 1: future._scanner.dwell_time = future._scanner.dwell_time * 1.2 + 0.1 logging.warning("Trying with dwell time = %g s...", future._scanner.dwell_time) else: # Make failure report _MakeReport(optical_image, repetitions, escan.magnification.value, escan.pixelSize.value, dwell_time, electron_coordinates) raise ValueError("Overlay failure") # Calculate transformation parameters if future._find_overlay_state == CANCELLED: raise CancelledError() # We are almost done... about 1 s left future.set_progress(end=time.time() + 1) logging.debug("Calculating transformation...") try: ret = transform.CalculateTransform(known_ec, known_oc, skew) except ValueError as exp: # Make failure report _MakeReport(optical_image, repetitions, escan.magnification.value, escan.pixelSize.value, dwell_time, electron_coordinates) raise ValueError("Overlay failure: %s" % (exp, )) if future._find_overlay_state == CANCELLED: raise CancelledError() logging.debug("Calculating transform metadata...") if skew is True: transform_d, skew_d = _transformMetadata(optical_image, ret, escan, ccd, skew) transform_data = (transform_d, skew_d) else: transform_d = _transformMetadata( optical_image, ret, escan, ccd, skew) # Also indicate which dwell time eventually worked transform_data = transform_d transform_d[model.MD_DWELL_TIME] = dwell_time logging.debug("Overlay done.") return ret, transform_data except CancelledError: pass except Exception as exp: logging.debug("Finding overlay failed", exc_info=1) raise exp finally: with future._overlay_lock: future._done.set() if future._find_overlay_state == CANCELLED: raise CancelledError() future._find_overlay_state = FINISHED