Source code for magni.imaging.measurements._lissajous

"""
..
    Copyright (c) 2016-2017, Magni developers.
    All rights reserved.
    See LICENSE.rst for further information.

Module providing public functions for the magni.imaging.measurements
subpackage.

Routine listings
----------------
lissajous_sample_image(h, w, scan_length, num_points, f_y=1., f_x=1.,
    theta_y=0., theta_x=np.pi / 2)
    Function for lissajous sampling an image.
lissajous_sample_surface(l, w, speed, sample_rate, time, f_y=1., f_x=1.,
    theta_y=0., theta_x=np.pi / 2, speed_mode=0)
    Function for lissajous sampling a surface.

"""

from __future__ import division

import numpy as np

from magni.imaging.measurements import _util
from magni.utils.validation import decorate_validation as _decorate_validation
from magni.utils.validation import validate_numeric as _numeric


__all__ = ['lissajous_sample_image', 'lissajous_sample_surface']

_min_l = _util.min_l
_min_w = _util.min_w
_min_speed = _util.min_speed
_min_sample_rate = _util.min_sample_rate
_min_time = _util.min_time
_min_scan_length = _util.min_scan_length
_min_num_points = _util.min_num_points


[docs]def lissajous_sample_image(h, w, scan_length, num_points, f_y=1., f_x=1., theta_y=0., theta_x=np.pi / 2): """ Sample an image using a lissajous pattern. The coordinates (in units of pixels) resulting from sampling an image of size `h` times `w` using a lissajous pattern are determined. The `scan_length` determines the length of the path scanned whereas `num_points` indicates the number of samples taken on that path. Parameters ---------- h : int The height of the area to scan in units of pixels. w : int The width of the area to scan in units of pixels. scan_length : float The length of the path to scan in units of pixels. num_points : int The number of samples to take on the scanned path. f_y : float The frequency of the y-sinusoid (the default value is 1.0). f_x : float The frequency of the x-sinusoid (the default value is 1.0). theta_y : float The starting phase of the y-sinusoid (the default is 0.0). theta_x : float The starting phase of the x-sinusoid (the default is pi / 2). Returns ------- coords : ndarray The coordinates of the samples arranged into a 2D array, such that each row is a coordinate pair (x, y). Notes ----- The orientation of the coordinate system is such that the width `w` is measured along the x-axis whereas the height `h` is measured along the y-axis. Examples -------- For example, >>> import numpy as np >>> from magni.imaging.measurements import lissajous_sample_image >>> h = 10 >>> w = 10 >>> scan_length = 50.0 >>> num_points = 12 >>> np.set_printoptions(suppress=True) >>> lissajous_sample_image(h, w, scan_length, num_points) array([[ 5. , 9.5 ], [ 1.40370042, 7.70492686], [ 0.67656563, 3.75183526], [ 3.39871123, 0.79454232], [ 7.39838148, 1.19240676], [ 9.48459832, 4.62800824], [ 7.99295651, 8.36038857], [ 4.11350322, 9.41181634], [ 0.94130617, 6.94345168], [ 1.0071768 , 2.92458128], [ 4.25856283, 0.56150128], [ 8.10147506, 1.7395012 ], [ 9.4699986 , 5.51876059]]) """ @_decorate_validation def validate_input(): _numeric('h', 'integer', range_='[2;inf)') _numeric('w', 'integer', range_='[2;inf)') _numeric('scan_length', 'floating', range_='[{};inf)'.format(_min_scan_length)) _numeric('num_points', 'integer', range_='[{};inf)'.format(_min_num_points)) _numeric('f_y', 'floating', range_='(0;inf)') _numeric('f_x', 'floating', range_='(0;inf)') _numeric('theta_y', 'floating', range_='(-inf;inf)') _numeric('theta_x', 'floating', range_='(-inf;inf)') validate_input() coords = lissajous_sample_surface( float(h - 1), float(w - 1), scan_length, float(num_points), 1., f_y=f_y, f_x=f_x, theta_y=theta_y, theta_x=theta_x) coords = coords + 0.5 return coords
[docs]def lissajous_sample_surface(l, w, speed, sample_rate, time, f_y=1., f_x=1., theta_y=0., theta_x=np.pi / 2, speed_mode=0): """ Sample a surface area using a lissajous pattern. The coordinates (in units of meters) resulting from sampling an area of size `l` times `w` using a lissajous pattern are determined. The scanned path is determined from the probe `speed` and the scan `time`. Parameters ---------- l : float The length of the area to scan in units of meters. w : float The width of the area to scan in units of meters. speed : float The probe speed in units of meters/second. sample_rate : float The sample rate in units of Hertz. time : float The scan time in units of seconds. f_y : float The frequency of the y-sinusoid (the default value is 1.0). f_x : float The frequency of the x-sinusoid (the default value is 1.0). theta_y : float The starting phase of the y-sinusoid (the default is 0.0). theta_x : float The starting phase of the x-sinusoid (the default is pi / 2). speed_mode : int The speed mode used to select sampling points (the default is 0 which implies that the speed argument determines the speed, and f_y and f_x determine the ratio between the relative frequencies used). Returns ------- coords : ndarray The coordinates of the samples arranged into a 2D array, such that each row is a coordinate pair (x, y). Notes ----- The orientation of the coordinate system is such that the width `w` is measured along the x-axis whereas the length `l` is measured along the y-axis. Generally, the lissajous sampling pattern does not provide constant speed, and this cannot be compensated for without violating f_y, f_x, or both. Therefore, `speed_mode` allows the user to determine how this issue is handled: In `speed_mode` 0, constant speed equal to `speed` is ensured by non-uniform sampling of a lissajous curve, whereby `f_y` and `f_x` are not constant frequencies. In `speed_mode` 1, average speed equal to `speed` is ensured by scaling `f_y` and `f_x` by the same constant. In `speed_mode` 2, `f_y` and `f_x` are kept constant and the `speed` is only used to determine the path length in combination with `time`. Examples -------- For example, >>> import numpy as np >>> from magni.imaging.measurements import lissajous_sample_surface >>> l = 1e-6 >>> w = 1e-6 >>> speed = 7e-7 >>> sample_rate = 1.0 >>> time = 12.0 >>> np.set_printoptions(suppress=True) >>> lissajous_sample_surface(l, w, speed, sample_rate, time) array([[ 0.0000005 , 0.000001 ], [ 0.00000001, 0.00000058], [ 0.00000033, 0.00000003], [ 0.00000094, 0.00000025], [ 0.00000082, 0.00000089], [ 0.00000017, 0.00000088], [ 0.00000007, 0.00000024], [ 0.00000068, 0.00000003], [ 0.00000099, 0.0000006 ], [ 0.00000048, 0.000001 ], [ 0. , 0.00000057], [ 0.00000035, 0.00000002], [ 0.00000094, 0.00000027]]) """ @_decorate_validation def validate_input(): _numeric('l', 'floating', range_='[{};inf)'.format(_min_l)) _numeric('w', 'floating', range_='[{};inf)'.format(_min_w)) _numeric('speed', 'floating', range_='[{};inf)'.format(_min_speed)) _numeric('sample_rate', 'floating', range_='[{};inf)'.format(_min_sample_rate)) _numeric('time', 'floating', range_='[{};inf)'.format(_min_time)) _numeric('f_y', 'floating', range_='(0;inf)') _numeric('f_x', 'floating', range_='(0;inf)') _numeric('theta_y', 'floating', range_='(-inf;inf)') _numeric('theta_x', 'floating', range_='(-inf;inf)') _numeric('speed_mode', 'integer', range_='[0;2]') validate_input() s_x = w / 2 s_y = l / 2 if speed_mode in (0, 1): # The probe moves 4 * s_x * f_x and 4 * s_y * f_y pixels a second in # the x-direction and y-direction, respectively, and the 2-norm of this # is a lower bound on the distance per second. Thus, t is an upper # bound on the scan time. t = speed * time / np.sqrt((4 * s_x * f_x)**2 + (4 * s_y * f_y)**2) # The above assumes that f_x * t and f_y * t are integral numbers and # so t is increased to ensure the upper bound. t = max(np.ceil(f_x * t) / f_x, np.ceil(f_y * t) / f_y) # The distance between sampling points on the curve is chosen small # enough to approximate the curve by straight line segments. dt = 1 / (10**4 * max(f_x, f_y)) t = np.linspace(0, t, int(t / dt)) x = s_x * np.cos(2 * np.pi * f_x * t + theta_x) + s_x y = s_y * np.cos(2 * np.pi * f_y * t + theta_y) + s_y dx = x[1:] - x[:-1] dy = y[1:] - y[:-1] l = np.zeros(t.shape) l[1:] = np.cumsum((dx**2 + dy**2)**(1 / 2)) if speed_mode == 0: # Constant speed entails constant distance between samples. l_mode_0 = np.linspace(0, speed * time, sample_rate * time + 1) t = np.interp(l_mode_0, l, t) else: # speed_mode == 1 # The value of t where the desired scan length is reached. t_end = np.argmax(l > speed * time) * dt t = np.linspace(0, t_end, sample_rate * time + 1) else: # speed_mode == 2 t = np.linspace(0, time, sample_rate * time + 1) x = s_x * np.cos(2 * np.pi * f_x * t + theta_x) + s_x y = s_y * np.cos(2 * np.pi * f_y * t + theta_y) + s_y return np.column_stack((x, y))