def test_out_of_fov(self): images = np.ones((10, self.image_source.size, self.image_source.size)) with self.assertRaises(ValueError) as ctx: rotation_axis(images) self.assertEqual("No sample tip points found.", str(ctx.exception))
async def test_out_of_fov(self): images = np.random.normal(size=(10, self.image_source.size, self.image_source.size)) with self.assertRaises(ValueError) as ctx: tips = await find_needle_tips(async_generate(images)) rotation_axis(tips) self.assertEqual("No sample tip points found.", str(ctx.exception))
async def align_check(self, x_angle, z_angle): images = self.make_images(x_angle, z_angle) tips = await find_needle_tips(images) phi, psi = rotation_axis(tips)[:2] assert phi - x_angle < self.eps assert np.abs(psi) - np.abs(z_angle) < self.eps
async def test_sphere_all_partially_outside(self): self.camera.size = 128 self.camera.rotation_radius = self.camera.size // 2 self.camera.scale = (.25, .25, .25) frames = (await self.acquire_frames())[0] centers = await find_sphere_centers(async_generate(frames), correlation_threshold=0.9) roll, pitch = rotation_axis(centers)[:2] # No sphere is completely in the FOV, filter the ones with low correlation coefficient and # at least coarsely close match should be found to the ellipse assert np.abs(roll - await self.z_motor.get_position()) < 1 * q.deg assert np.abs(pitch - await self.x_motor.get_position()) < 1 * q.deg
def center_check(self, images): center = rotation_axis(images)[2] assert np.abs(center[1] - self.image_source.ellipse_center[1]) < 2 assert np.abs(center[0] - self.image_source.ellipse_center[0]) < 2
def align_check(self, x_angle, z_angle): images = self.make_images(x_angle, z_angle) phi, psi = rotation_axis(images)[:2] assert phi + x_angle < self.eps assert np.abs(psi) - np.abs(z_angle) < self.eps
async def center_check(self, images): tips = await find_needle_tips(images) center = rotation_axis(tips)[2] assert np.abs(center[1] - self.image_source.ellipse_center[1]) < 2 assert np.abs(center[0] - self.image_source.ellipse_center[0]) < 2
async def align_rotation_axis(camera, rotation_motor, x_motor=None, z_motor=None, get_ellipse_points=find_needle_tips, num_frames=10, metric_eps=None, position_eps=0.1 * q.deg, max_iterations=5, initial_x_coeff=1 * q.dimensionless, initial_z_coeff=1 * q.dimensionless, shutter=None, flat_motor=None, flat_position=None, y_0=0, y_1=None, get_ellipse_points_kwargs=None, frame_consumer=None): """ align_rotation_axis(camera, rotation_motor, x_motor=None, z_motor=None, get_ellipse_points=find_needle_tips, num_frames=10, metric_eps=None, position_eps=0.1 * q.deg, max_iterations=5, initial_x_coeff=1 * q.dimensionless, initial_z_coeff=1 * q.dimensionless, shutter=None, flat_motor=None, flat_position=None, y_0=0, y_1=None, get_ellipse_points_kwargs=None, frame_consumer=None) Align rotation axis. *camera* is used to obtain frames, *rotation_motor* rotates the sample around the tomographic axis of rotation, *x_motor* turns the sample around x-axis, *z_motor* turns the sample around z-axis. *get_ellipse_points* is a function with one positional argument, a set of images. It computes the ellipse points from the sample positions as it rotates around the tomographic axis. You can use e.g. :func:`concert.imageprocessing.find_needle_tips` and :func:`concert.imageprocessing.find_sphere_centers` to extract the ellipse points from needle tips or sphere centers. You can pass additional keyword arguments to the *get_ellipse_points* function in the *get_ellipse_points_kwargs* dictionary. *num_frames* defines how many frames are acquired and passed to the *measure*. *metric_eps* is the metric threshold for stopping the procedure. If not specified, it is calculated automatically to not exceed 0.5 pixels vertically. If *max_iterations* is reached the procedure stops as well. *initial_[x|z]_coeff* is the coefficient applied` to the motor motion for the first iteration. If we move the camera instead of the rotation stage, it is often necessary to acquire fresh flat fields. In order to make an up-to-date flat correction, specify *shutter* if you want fresh dark fields and specify *flat_motor* and *flat_position* to acquire flat fields. Crop acquired images to *y_0* and *y_1*. *frame_consumer* is a coroutine function which will be fed with all acquired frames. The procedure finishes when it finds the minimum angle between an ellipse extracted from the sample movement and respective axes or the found angle drops below *metric_eps*. The axis of rotation after the procedure is (0,1,0), which is the direction perpendicular to the beam direction and the lateral direction. *x_motor* and *z_motor* do not have to move exactly by the computed angles but their relative motion must be linear with respect to computed angles (e.g. if the motors operate with steps it is fine, also rotation direction does not need to be known). """ if get_ellipse_points_kwargs is None: get_ellipse_points_kwargs = {} if not x_motor and not z_motor: raise ProcessError("At least one of the x, z motors must be given") async def make_step(i, motor, position_last, angle_last, angle_current, initial_coeff, rotation_type): cur_pos = await motor.get_position() LOG.debug( "%s: i: %d, last angle: %s, angle: %s, last position: %s, position: %s", rotation_type, i, angle_last.to(q.deg), angle_current.to(q.deg), position_last.to(q.deg), cur_pos.to(q.deg)) if i > 0: # Assume linear mapping between the computed angles and motor motion if angle_current == angle_last: coeff = 0 * q.dimensionless else: coeff = (cur_pos - position_last) / (angle_current - angle_last) else: coeff = initial_coeff position_last = cur_pos angle_last = angle_current # Move relative, i.e. if *angle_current* should go to 0, then we need to move in the # other direction with *coeff* applied LOG.debug("%s coeff: %s, Next position: %s", rotation_type, coeff.to_base_units(), (cur_pos - coeff * angle_current).to(q.deg)) await motor.move(-coeff * angle_current) return (position_last, angle_last) async def go_to_best_index(motor, history): positions, angles = list(zip(*history)) best_index = np.argmin( np.abs([angle.to_base_units().magnitude for angle in angles])) LOG.debug("Best iteration: %d, position: %s, angle: %s", best_index, positions[best_index].to(q.deg), angles[best_index].to(q.deg)) await motor.set_position(positions[best_index]) async def extract_points(producer): return await get_ellipse_points(producer, **get_ellipse_points_kwargs) roll_history = [] pitch_history = [] center = None if z_motor: roll_angle_last = 0 * q.deg roll_position_last = await z_motor.get_position() roll_continue = True if x_motor: pitch_angle_last = 0 * q.deg pitch_position_last = await x_motor.get_position() pitch_continue = True frames_result = Result() for i in range(max_iterations): acq_consumers = [extract_points, frames_result] if frame_consumer: acq_consumers.append(frame_consumer) tips_start = time.perf_counter() frame_producer = acquire_frames_360(camera, rotation_motor, num_frames, shutter=shutter, flat_motor=flat_motor, flat_position=flat_position, y_0=y_0, y_1=y_1) coros = broadcast(frame_producer, *acq_consumers) try: tips = (await asyncio.gather(*coros))[1] except Exception as tips_exc: raise ProcessError('Error finding reference points') from tips_exc LOG.debug('Found %d points in %g s', len(tips), time.perf_counter() - tips_start) roll_angle_current, pitch_angle_current, center = rotation_axis(tips) coros = [] x_coro = z_coro = None if metric_eps is None: metric_eps = np.rad2deg( np.arctan(1 / frames_result.result.shape[1])) * q.deg LOG.debug('Automatically computed metric epsilon: %s', metric_eps) if z_motor and roll_continue: z_pos = await z_motor.get_position() roll_history.append((z_pos, roll_angle_current)) if (np.abs(roll_angle_current) >= metric_eps and (np.abs(roll_position_last - z_pos) >= position_eps or i == 0)): z_coro = make_step(i, z_motor, roll_position_last, roll_angle_last, roll_angle_current, initial_z_coeff, 'roll') coros.append(z_coro) else: LOG.debug("Roll epsilon reached") roll_continue = False if x_motor and pitch_continue: x_pos = await x_motor.get_position() pitch_history.append((x_pos, pitch_angle_current)) if (np.abs(pitch_angle_current) >= metric_eps and (np.abs(pitch_position_last - x_pos) >= position_eps or i == 0)): x_coro = make_step(i, x_motor, pitch_position_last, pitch_angle_last, pitch_angle_current, initial_x_coeff, 'pitch') coros.append(x_coro) else: LOG.debug("Pitch epsilon reached") pitch_continue = False if not coros: # If there are no coros the motors have reached positions at which the computed # angles are below threshold break step_results = await asyncio.gather(*coros) if x_coro: # Regardless from x_coro and z_coro to be present, x_coro is always added last, so pop # it first pitch_position_last, pitch_angle_last = step_results.pop() if z_coro: roll_position_last, roll_angle_last = step_results.pop() if i == max_iterations - 1: LOG.info('Maximum iterations reached') # Move to the best known position coros = [] if z_motor: coros.append(go_to_best_index(z_motor, roll_history)) if x_motor: coros.append(go_to_best_index(x_motor, pitch_history)) await asyncio.gather(*coros) return (roll_history, pitch_history, center)